git-svn-id: http://googleappengine.googlecode.com/svn/trunk/python@245 80f5ef21-4148-0410-bacc-cfb02402ada8
diff --git a/RELEASE_NOTES b/RELEASE_NOTES
index 38fa05e..e46a0c2 100644
--- a/RELEASE_NOTES
+++ b/RELEASE_NOTES
@@ -3,6 +3,54 @@
App Engine Python SDK - Release Notes
+Version 1.6.4
+===============================
+- Billed applications that have specified additional logs retention over 1 GB
+ are now being charged for that storage at $0.24/GB/month (the first gigabyte
+ of logs storage is free). All logs beyond an application's specified storage
+ limit will be deleted. Please examine your Application Settings page to verify
+ you are retaining the desired amount of logs.
+- Datastore statistics now show the amount of storage used by application
+ indexes.
+- We have released an experimental utility for migrating your application's
+ blobs at the same time you migrate your datastore data. You can opt-in to
+ blob migration in the Admin Console when you start your migration.
+- We have updated the experimental Backup/Restore functionality to include
+ the option to backup and restore to Google Cloud Storage.
+- The NDB datastore API is now generally available. For full release notes
+ on the version 0.9.9 and 1.0.0 fixes that have been integrated into the
+ API see:
+ http://code.google.com/p/appengine-ndb-experiment/source/browse/RELEASE_NOTES?name=sandbox
+- In the Python 2.7 runtime, Background threads are available as an
+ experimental release when using App Engine backends.
+- Using the Blobstore API's serve_blob() method, your application can serve
+ objects hosted on Google Storage for Developers.
+- The Admin Console now provides a Memcache viewer that lists Memcache stats and
+ can display Memcache content based on key.
+- In the Capabilities API stub in the SDK, you can now enable or disable
+ a capability using SetPackagedEnabled.
+- The Windows installer now prompts to install Python 2.7 instead of
+ Python 2.5.
+- The Testbed API now supports the Capabilities API.
+- GQL queries in the Admin Console no longer throw an error when a trailing
+ semi-colon is included.
+- The Datastore API now includes a NonTransactional decorator to ensure that
+ a function is run outside of a transaction. Existing transactions are paused
+ while the function is executing.
+- The Datastore Admin tab in the Admin Console now shows entities from every
+ namespace.
+ http://code.google.com/p/googleappengine/issues/detail?id=3962
+- Fixed an issue with _strptime when threadsafe was specified.
+ http://code.google.com/p/googleappengine/issues/detail?id=6489
+- Fixed an issue where DatastoreFileStub.__del__ fails on tempfile.msktemp.
+ http://code.google.com/p/googleappengine/issues/detail?id=6749
+- WebOb 1.1.1 is now included in the SDK, and used by default there when
+ Python 2.7 is specified.
+ http://code.google.com/p/googleappengine/issues/detail?id=7014
+- Fixed an issue where the index.yaml file was cleared if your skip_files entry
+ differs from the default skip_files list.
+ http://code.google.com/p/googleappengine/issues/detail?id=7031
+
Version 1.6.3
===============================
- In the Admin Console, you can use new the Traffic Splitting feature to send a
diff --git a/VERSION b/VERSION
index 5cd702c..d0b9dea 100644
--- a/VERSION
+++ b/VERSION
@@ -1,3 +1,3 @@
-release: "1.6.3"
-timestamp: 1328043246
+release: "1.6.4"
+timestamp: 1330713524
api_versions: ['1']
diff --git a/api_server.py b/api_server.py
index 0276d25..90e2c47 100755
--- a/api_server.py
+++ b/api_server.py
@@ -51,7 +51,7 @@
os.path.join(DIR_PATH, 'lib', 'jinja2'),
os.path.join(DIR_PATH, 'lib', 'protorpc'),
os.path.join(DIR_PATH, 'lib', 'markupsafe'),
- os.path.join(DIR_PATH, 'lib', 'webob'),
+ os.path.join(DIR_PATH, 'lib', 'webob_0_9'),
os.path.join(DIR_PATH, 'lib', 'webapp2'),
os.path.join(DIR_PATH, 'lib', 'yaml', 'lib'),
os.path.join(DIR_PATH, 'lib', 'simplejson'),
diff --git a/appcfg.py b/appcfg.py
index 0276d25..90e2c47 100755
--- a/appcfg.py
+++ b/appcfg.py
@@ -51,7 +51,7 @@
os.path.join(DIR_PATH, 'lib', 'jinja2'),
os.path.join(DIR_PATH, 'lib', 'protorpc'),
os.path.join(DIR_PATH, 'lib', 'markupsafe'),
- os.path.join(DIR_PATH, 'lib', 'webob'),
+ os.path.join(DIR_PATH, 'lib', 'webob_0_9'),
os.path.join(DIR_PATH, 'lib', 'webapp2'),
os.path.join(DIR_PATH, 'lib', 'yaml', 'lib'),
os.path.join(DIR_PATH, 'lib', 'simplejson'),
diff --git a/bulkload_client.py b/bulkload_client.py
index 0276d25..90e2c47 100755
--- a/bulkload_client.py
+++ b/bulkload_client.py
@@ -51,7 +51,7 @@
os.path.join(DIR_PATH, 'lib', 'jinja2'),
os.path.join(DIR_PATH, 'lib', 'protorpc'),
os.path.join(DIR_PATH, 'lib', 'markupsafe'),
- os.path.join(DIR_PATH, 'lib', 'webob'),
+ os.path.join(DIR_PATH, 'lib', 'webob_0_9'),
os.path.join(DIR_PATH, 'lib', 'webapp2'),
os.path.join(DIR_PATH, 'lib', 'yaml', 'lib'),
os.path.join(DIR_PATH, 'lib', 'simplejson'),
diff --git a/bulkloader.py b/bulkloader.py
index 0276d25..90e2c47 100755
--- a/bulkloader.py
+++ b/bulkloader.py
@@ -51,7 +51,7 @@
os.path.join(DIR_PATH, 'lib', 'jinja2'),
os.path.join(DIR_PATH, 'lib', 'protorpc'),
os.path.join(DIR_PATH, 'lib', 'markupsafe'),
- os.path.join(DIR_PATH, 'lib', 'webob'),
+ os.path.join(DIR_PATH, 'lib', 'webob_0_9'),
os.path.join(DIR_PATH, 'lib', 'webapp2'),
os.path.join(DIR_PATH, 'lib', 'yaml', 'lib'),
os.path.join(DIR_PATH, 'lib', 'simplejson'),
diff --git a/demos/guestbook/app.yaml b/demos/guestbook/app.yaml
index 18b446e..ee014df 100644
--- a/demos/guestbook/app.yaml
+++ b/demos/guestbook/app.yaml
@@ -1,8 +1,13 @@
application: guestbook
version: 1
-runtime: python
+runtime: python27
api_version: 1
+threadsafe: yes
handlers:
- url: .*
- script: guestbook.py
+ script: guestbook.app
+
+libraries:
+- name: webapp2
+ version: "2.5.1"
diff --git a/demos/guestbook/guestbook.py b/demos/guestbook/guestbook.py
index c52a0ee..52caa5d 100755
--- a/demos/guestbook/guestbook.py
+++ b/demos/guestbook/guestbook.py
@@ -16,12 +16,10 @@
#
import cgi
import datetime
-
+import webapp2
from google.appengine.ext import db
from google.appengine.api import users
-from google.appengine.ext import webapp
-from google.appengine.ext.webapp import util
class Greeting(db.Model):
author = db.UserProperty()
@@ -29,7 +27,7 @@
date = db.DateTimeProperty(auto_now_add=True)
-class MainPage(webapp.RequestHandler):
+class MainPage(webapp2.RequestHandler):
def get(self):
self.response.out.write('<html><body>')
@@ -55,7 +53,7 @@
</html>""")
-class Guestbook(webapp.RequestHandler):
+class Guestbook(webapp2.RequestHandler):
def post(self):
greeting = Greeting()
@@ -67,15 +65,7 @@
self.redirect('/')
-application = webapp.WSGIApplication([
+app = webapp2.WSGIApplication([
('/', MainPage),
('/sign', Guestbook)
], debug=True)
-
-
-def main():
- util.run_wsgi_app(application)
-
-
-if __name__ == '__main__':
- main()
diff --git a/dev_appserver.py b/dev_appserver.py
index 0276d25..90e2c47 100755
--- a/dev_appserver.py
+++ b/dev_appserver.py
@@ -51,7 +51,7 @@
os.path.join(DIR_PATH, 'lib', 'jinja2'),
os.path.join(DIR_PATH, 'lib', 'protorpc'),
os.path.join(DIR_PATH, 'lib', 'markupsafe'),
- os.path.join(DIR_PATH, 'lib', 'webob'),
+ os.path.join(DIR_PATH, 'lib', 'webob_0_9'),
os.path.join(DIR_PATH, 'lib', 'webapp2'),
os.path.join(DIR_PATH, 'lib', 'yaml', 'lib'),
os.path.join(DIR_PATH, 'lib', 'simplejson'),
diff --git a/gen_protorpc.py b/gen_protorpc.py
index 0276d25..90e2c47 100755
--- a/gen_protorpc.py
+++ b/gen_protorpc.py
@@ -51,7 +51,7 @@
os.path.join(DIR_PATH, 'lib', 'jinja2'),
os.path.join(DIR_PATH, 'lib', 'protorpc'),
os.path.join(DIR_PATH, 'lib', 'markupsafe'),
- os.path.join(DIR_PATH, 'lib', 'webob'),
+ os.path.join(DIR_PATH, 'lib', 'webob_0_9'),
os.path.join(DIR_PATH, 'lib', 'webapp2'),
os.path.join(DIR_PATH, 'lib', 'yaml', 'lib'),
os.path.join(DIR_PATH, 'lib', 'simplejson'),
diff --git a/google/appengine/api/blobstore/blobstore.py b/google/appengine/api/blobstore/blobstore.py
index ed3e59e..f3ea641 100755
--- a/google/appengine/api/blobstore/blobstore.py
+++ b/google/appengine/api/blobstore/blobstore.py
@@ -63,6 +63,8 @@
'delete_async',
'fetch_data',
'fetch_data_async',
+ 'create_gs_key',
+ 'create_gs_key_async',
]
@@ -113,7 +115,6 @@
"""Raised when permissions are lacking for a requested operation."""
-
def _ToBlobstoreError(error):
"""Translate an application error to a datastore Error, if possible.
@@ -430,3 +431,67 @@
return _make_async_call(rpc, 'FetchData', request, response,
_get_result_hook, lambda rpc: rpc.response.data())
+
+
+def create_gs_key(filename, rpc=None):
+ """Create an encoded key for a Google Storage file.
+
+ The created blob key will include short lived access token using the
+ applications service account for authorization.
+
+ This blob key should not be stored permanently as the access token will
+ expire.
+
+ Args:
+ filename: The filename of the google storage object to create the key for.
+ rpc: Optional UserRPC object.
+
+ Returns:
+ An encrypted blob key object that also contains a short term access token
+ that represents the applications service account.
+ """
+ rpc = create_gs_key_async(filename, rpc)
+ return rpc.get_result()
+
+
+def create_gs_key_async(filename, rpc=None):
+ """Create an encoded key for a google storage file - async version.
+
+ The created blob key will include short lived access token using the
+ applications service account for authorization.
+
+ This blob key should not be stored permanently as the access token will
+ expire.
+
+ Args:
+ filename: The filename of the google storage object to create the
+ key for.
+ rpc: Optional UserRPC object.
+
+ Returns:
+ A UserRPC whose result will be a str as returned by create_gs_key.
+
+ Raises:
+ TypeError: If filename is not a string.
+ ValueError: If filename is not in the format '/gs/bucket_name/object_name'
+ """
+
+ if not isinstance(filename, basestring):
+ raise TypeError('filename must be str: %s' % filename)
+ if not filename.startswith('/gs/'):
+ raise ValueError('filename must start with "/gs/": %s' % filename)
+ if not '/' in filename[4:]:
+ raise ValueError('filename must have the format '
+ '"/gs/bucket_name/object_name": %s' % filename)
+
+ request = blobstore_service_pb.CreateEncodedGoogleStorageKeyRequest()
+ response = blobstore_service_pb.CreateEncodedGoogleStorageKeyResponse()
+
+ request.set_filename(filename)
+
+ return _make_async_call(rpc,
+ 'CreateEncodedGoogleStorageKey',
+ request,
+ response,
+ _get_result_hook,
+ lambda rpc: rpc.response.blob_key())
diff --git a/google/appengine/api/blobstore/blobstore_service_pb.py b/google/appengine/api/blobstore/blobstore_service_pb.py
index 9a5265d..b538130 100644
--- a/google/appengine/api/blobstore/blobstore_service_pb.py
+++ b/google/appengine/api/blobstore/blobstore_service_pb.py
@@ -1259,7 +1259,207 @@
_STYLE = """"""
_STYLE_CONTENT_TYPE = """"""
_PROTO_DESCRIPTOR_NAME = 'apphosting.DecodeBlobKeyResponse'
+class CreateEncodedGoogleStorageKeyRequest(ProtocolBuffer.ProtocolMessage):
+ has_filename_ = 0
+ filename_ = ""
+
+ def __init__(self, contents=None):
+ if contents is not None: self.MergeFromString(contents)
+
+ def filename(self): return self.filename_
+
+ def set_filename(self, x):
+ self.has_filename_ = 1
+ self.filename_ = x
+
+ def clear_filename(self):
+ if self.has_filename_:
+ self.has_filename_ = 0
+ self.filename_ = ""
+
+ def has_filename(self): return self.has_filename_
+
+
+ def MergeFrom(self, x):
+ assert x is not self
+ if (x.has_filename()): self.set_filename(x.filename())
+
+ def Equals(self, x):
+ if x is self: return 1
+ if self.has_filename_ != x.has_filename_: return 0
+ if self.has_filename_ and self.filename_ != x.filename_: return 0
+ return 1
+
+ def IsInitialized(self, debug_strs=None):
+ initialized = 1
+ if (not self.has_filename_):
+ initialized = 0
+ if debug_strs is not None:
+ debug_strs.append('Required field: filename not set.')
+ return initialized
+
+ def ByteSize(self):
+ n = 0
+ n += self.lengthString(len(self.filename_))
+ return n + 1
+
+ def ByteSizePartial(self):
+ n = 0
+ if (self.has_filename_):
+ n += 1
+ n += self.lengthString(len(self.filename_))
+ return n
+
+ def Clear(self):
+ self.clear_filename()
+
+ def OutputUnchecked(self, out):
+ out.putVarInt32(10)
+ out.putPrefixedString(self.filename_)
+
+ def OutputPartial(self, out):
+ if (self.has_filename_):
+ out.putVarInt32(10)
+ out.putPrefixedString(self.filename_)
+
+ def TryMerge(self, d):
+ while d.avail() > 0:
+ tt = d.getVarInt32()
+ if tt == 10:
+ self.set_filename(d.getPrefixedString())
+ continue
+
+
+ if (tt == 0): raise ProtocolBuffer.ProtocolBufferDecodeError
+ d.skipData(tt)
+
+
+ def __str__(self, prefix="", printElemNumber=0):
+ res=""
+ if self.has_filename_: res+=prefix+("filename: %s\n" % self.DebugFormatString(self.filename_))
+ return res
+
+
+ def _BuildTagLookupTable(sparse, maxtag, default=None):
+ return tuple([sparse.get(i, default) for i in xrange(0, 1+maxtag)])
+
+ kfilename = 1
+
+ _TEXT = _BuildTagLookupTable({
+ 0: "ErrorCode",
+ 1: "filename",
+ }, 1)
+
+ _TYPES = _BuildTagLookupTable({
+ 0: ProtocolBuffer.Encoder.NUMERIC,
+ 1: ProtocolBuffer.Encoder.STRING,
+ }, 1, ProtocolBuffer.Encoder.MAX_TYPE)
+
+
+ _STYLE = """"""
+ _STYLE_CONTENT_TYPE = """"""
+ _PROTO_DESCRIPTOR_NAME = 'apphosting.CreateEncodedGoogleStorageKeyRequest'
+class CreateEncodedGoogleStorageKeyResponse(ProtocolBuffer.ProtocolMessage):
+ has_blob_key_ = 0
+ blob_key_ = ""
+
+ def __init__(self, contents=None):
+ if contents is not None: self.MergeFromString(contents)
+
+ def blob_key(self): return self.blob_key_
+
+ def set_blob_key(self, x):
+ self.has_blob_key_ = 1
+ self.blob_key_ = x
+
+ def clear_blob_key(self):
+ if self.has_blob_key_:
+ self.has_blob_key_ = 0
+ self.blob_key_ = ""
+
+ def has_blob_key(self): return self.has_blob_key_
+
+
+ def MergeFrom(self, x):
+ assert x is not self
+ if (x.has_blob_key()): self.set_blob_key(x.blob_key())
+
+ def Equals(self, x):
+ if x is self: return 1
+ if self.has_blob_key_ != x.has_blob_key_: return 0
+ if self.has_blob_key_ and self.blob_key_ != x.blob_key_: return 0
+ return 1
+
+ def IsInitialized(self, debug_strs=None):
+ initialized = 1
+ if (not self.has_blob_key_):
+ initialized = 0
+ if debug_strs is not None:
+ debug_strs.append('Required field: blob_key not set.')
+ return initialized
+
+ def ByteSize(self):
+ n = 0
+ n += self.lengthString(len(self.blob_key_))
+ return n + 1
+
+ def ByteSizePartial(self):
+ n = 0
+ if (self.has_blob_key_):
+ n += 1
+ n += self.lengthString(len(self.blob_key_))
+ return n
+
+ def Clear(self):
+ self.clear_blob_key()
+
+ def OutputUnchecked(self, out):
+ out.putVarInt32(10)
+ out.putPrefixedString(self.blob_key_)
+
+ def OutputPartial(self, out):
+ if (self.has_blob_key_):
+ out.putVarInt32(10)
+ out.putPrefixedString(self.blob_key_)
+
+ def TryMerge(self, d):
+ while d.avail() > 0:
+ tt = d.getVarInt32()
+ if tt == 10:
+ self.set_blob_key(d.getPrefixedString())
+ continue
+
+
+ if (tt == 0): raise ProtocolBuffer.ProtocolBufferDecodeError
+ d.skipData(tt)
+
+
+ def __str__(self, prefix="", printElemNumber=0):
+ res=""
+ if self.has_blob_key_: res+=prefix+("blob_key: %s\n" % self.DebugFormatString(self.blob_key_))
+ return res
+
+
+ def _BuildTagLookupTable(sparse, maxtag, default=None):
+ return tuple([sparse.get(i, default) for i in xrange(0, 1+maxtag)])
+
+ kblob_key = 1
+
+ _TEXT = _BuildTagLookupTable({
+ 0: "ErrorCode",
+ 1: "blob_key",
+ }, 1)
+
+ _TYPES = _BuildTagLookupTable({
+ 0: ProtocolBuffer.Encoder.NUMERIC,
+ 1: ProtocolBuffer.Encoder.STRING,
+ }, 1, ProtocolBuffer.Encoder.MAX_TYPE)
+
+
+ _STYLE = """"""
+ _STYLE_CONTENT_TYPE = """"""
+ _PROTO_DESCRIPTOR_NAME = 'apphosting.CreateEncodedGoogleStorageKeyResponse'
if _extension_runtime:
pass
-__all__ = ['BlobstoreServiceError','CreateUploadURLRequest','CreateUploadURLResponse','DeleteBlobRequest','FetchDataRequest','FetchDataResponse','CloneBlobRequest','CloneBlobResponse','DecodeBlobKeyRequest','DecodeBlobKeyResponse']
+__all__ = ['BlobstoreServiceError','CreateUploadURLRequest','CreateUploadURLResponse','DeleteBlobRequest','FetchDataRequest','FetchDataResponse','CloneBlobRequest','CloneBlobResponse','DecodeBlobKeyRequest','DecodeBlobKeyResponse','CreateEncodedGoogleStorageKeyRequest','CreateEncodedGoogleStorageKeyResponse']
diff --git a/google/appengine/api/blobstore/blobstore_stub.py b/google/appengine/api/blobstore/blobstore_stub.py
index 2b424f3..2a6fd16 100755
--- a/google/appengine/api/blobstore/blobstore_stub.py
+++ b/google/appengine/api/blobstore/blobstore_stub.py
@@ -33,6 +33,7 @@
+import base64
import os
import time
@@ -337,6 +338,22 @@
for blob_key in request.blob_key_list():
response.add_decoded(blob_key.decode('base64'))
+ def _Dynamic_CreateEncodedGoogleStorageKey(self, request, response):
+ """Create an encoded blob key that represents a bigstore file.
+
+ For now we'll just base64 encode the bigstore filename, APIs that accept
+ encoded blob keys will need to be able to support Google Storage files or
+ blobstore files based on decoding this key.
+
+ Args:
+ request: A fully-initialized CreateEncodedGoogleStorageKeyRequest
+ instance.
+ response: A CreateEncodedGoogleStorageKeyResponse instance.
+ """
+ filename = request.filename()
+ response.set_blob_key('encoded_gs_file:' +
+ base64.urlsafe_b64encode(filename))
+
def CreateBlob(self, blob_key, content):
"""Create new blob and put in storage and Datastore.
diff --git a/google/appengine/api/capabilities/capability_stub.py b/google/appengine/api/capabilities/capability_stub.py
index 47eff56..a29a00d 100755
--- a/google/appengine/api/capabilities/capability_stub.py
+++ b/google/appengine/api/capabilities/capability_stub.py
@@ -44,6 +44,18 @@
service_name: Service name expected for all calls.
"""
super(CapabilityServiceStub, self).__init__(service_name)
+ self._packages = {}
+
+ def SetPackageEnabled(self, package, enabled):
+ """Set all features of a given package to enabled.
+
+ Args:
+ package: Name of package.
+ enabled: True to enable, False to disable.
+ """
+
+ self._packages[package] = enabled
+
def _Dynamic_IsEnabled(self, request, response):
@@ -53,9 +65,16 @@
request: An IsEnabledRequest.
response: An IsEnabledResponse.
"""
- response.set_summary_status(IsEnabledResponse.ENABLED)
+ package_enabled = self._packages.get(request.package(), True)
+ if package_enabled:
+ response.set_summary_status(IsEnabledResponse.ENABLED)
+ else:
+ response.set_summary_status(IsEnabledResponse.DISABLED)
default_config = response.add_config()
default_config.set_package('')
default_config.set_capability('')
- default_config.set_status(CapabilityConfig.ENABLED)
+ if package_enabled:
+ default_config.set_status(CapabilityConfig.ENABLED)
+ else:
+ default_config.set_status(CapabilityConfig.DISABLED)
diff --git a/google/appengine/api/datastore.py b/google/appengine/api/datastore.py
index 401084a..dfa1abc 100755
--- a/google/appengine/api/datastore.py
+++ b/google/appengine/api/datastore.py
@@ -282,7 +282,7 @@
return self.__id
def _Kind(self):
- """Returns the index kind, a string, or None if none."""
+ """Returns the index kind, a string. Empty string ('') if none."""
return self.__kind
def _HasAncestor(self):
@@ -2457,27 +2457,43 @@
+ options = datastore_rpc.TransactionOptions(options)
+ if IsInTransaction():
+ if options.propagation in (None, datastore_rpc.TransactionOptions.NESTED):
- retries = datastore_rpc.TransactionOptions.retries(options)
+ raise datastore_errors.BadRequestError(
+ 'Nested transactions are not supported.')
+ elif options.propagation is datastore_rpc.TransactionOptions.INDEPENDENT:
+
+
+ txn_connection = _GetConnection()
+ _SetConnection(_thread_local.old_connection)
+ try:
+ return RunInTransactionOptions(options, function, *args, **kwargs)
+ finally:
+ _SetConnection(txn_connection)
+ return function(*args, **kwargs)
+
+ if options.propagation is datastore_rpc.TransactionOptions.MANDATORY:
+ raise datastore_errors.BadRequestError(
+ 'Requires an existing transaction.')
+
+
+ retries = options.retries
if retries is None:
retries = DEFAULT_TRANSACTION_RETRIES
-
- if IsInTransaction():
- raise datastore_errors.BadRequestError(
- 'Nested transactions are not supported.')
-
- old_connection = _GetConnection()
+ _thread_local.old_connection = _GetConnection()
for _ in range(0, retries + 1):
- new_connection = old_connection.new_transaction(options)
+ new_connection = _thread_local.old_connection.new_transaction(options)
_SetConnection(new_connection)
try:
ok, result = _DoOneTry(new_connection, function, args, kwargs)
if ok:
return result
finally:
- _SetConnection(old_connection)
+ _SetConnection(_thread_local.old_connection)
raise datastore_errors.TransactionFailedError(
@@ -2550,34 +2566,80 @@
return isinstance(_GetConnection(), datastore_rpc.TransactionalConnection)
-datastore_rpc._positional(1)
-def Transactional(_func=None, require_new=False, **kwargs):
+def Transactional(_func=None, **kwargs):
"""A decorator that makes sure a function is run in a transaction.
+ Defaults propagation to datastore_rpc.TransactionOptions.ALLOWED, which means
+ any existing transaction will be used in place of creating a new one.
+
WARNING: Reading from the datastore while in a transaction will not see any
changes made in the same transaction. If the function being decorated relies
- on seeing all changes made in the calling scoope, set require_new=True.
+ on seeing all changes made in the calling scoope, set
+ propagation=datastore_rpc.TransactionOptions.NESTED.
Args:
_func: do not use.
- require_new: A bool that indicates the function requires its own transaction
- and cannot share a transaction with the calling scope (nested transactions
- are not currently supported by the datastore).
**kwargs: TransactionOptions configuration options.
Returns:
A wrapper for the given function that creates a new transaction if needed.
"""
- if _func is None:
- return lambda function: Transactional(_func=function,
- require_new=require_new,
- **kwargs)
+
+ if _func is not None:
+ return Transactional()(_func)
+
+
+ if not kwargs.pop('require_new', None):
+
+ kwargs.setdefault('propagation', datastore_rpc.TransactionOptions.ALLOWED)
+
options = datastore_rpc.TransactionOptions(**kwargs)
- def wrapper(*args, **kwds):
- if not require_new and IsInTransaction():
- return _func(*args, **kwds)
- return RunInTransactionOptions(options, _func, *args, **kwds)
- return wrapper
+
+ def outer_wrapper(func):
+ def inner_wrapper(*args, **kwds):
+ return RunInTransactionOptions(options, func, *args, **kwds)
+ return inner_wrapper
+ return outer_wrapper
+
+
+@datastore_rpc._positional(1)
+def NonTransactional(_func=None, allow_existing=True):
+ """A decorator that insures a function is run outside a transaction.
+
+ If there is an existing transaction (and allow_existing=True), the existing
+ transaction is paused while the function is executed.
+
+ Args:
+ _func: do not use
+ allow_existing: If false, throw an exception if called from within a
+ transaction
+
+ Returns:
+ A wrapper for the decorated function that ensures it runs outside a
+ transaction.
+ """
+
+ if _func is not None:
+ return NonTransactional()(_func)
+
+ def outer_wrapper(func):
+ def inner_wrapper(*args, **kwds):
+ if not IsInTransaction():
+ return func(*args, **kwds)
+
+ if not allow_existing:
+ raise datastore_errors.BadRequestError(
+ 'Function cannot be called from within a transaction.')
+
+
+ txn_connection = _GetConnection()
+ _SetConnection(_thread_local.old_connection)
+ try:
+ return func(*args, **kwds)
+ finally:
+ _SetConnection(txn_connection)
+ return inner_wrapper
+ return outer_wrapper
def _GetCompleteKeyOrError(arg):
diff --git a/google/appengine/api/datastore_file_stub.py b/google/appengine/api/datastore_file_stub.py
index faf7c99..335ec87 100755
--- a/google/appengine/api/datastore_file_stub.py
+++ b/google/appengine/api/datastore_file_stub.py
@@ -684,8 +684,3 @@
self.__id_lock.release()
return (start, end)
-
-
-
- def _OnApply(self):
- self.__WriteDatastore()
diff --git a/google/appengine/api/datastore_types.py b/google/appengine/api/datastore_types.py
index aa80a19..8eb1ace 100755
--- a/google/appengine/api/datastore_types.py
+++ b/google/appengine/api/datastore_types.py
@@ -556,7 +556,12 @@
def has_id_or_name(self):
"""Returns True if this entity has an id or name, False otherwise.
"""
- return self.id_or_name() is not None
+ elems = self.__reference.path().element_list()
+ if elems:
+ e = elems[-1]
+ return bool(e.name() or e.id())
+ else:
+ return False
def parent(self):
"""Returns this entity's parent, as a Key. If this entity has no parent,
diff --git a/google/appengine/api/files/blobstore.py b/google/appengine/api/files/blobstore.py
index 0ab374e..5b035dc 100755
--- a/google/appengine/api/files/blobstore.py
+++ b/google/appengine/api/files/blobstore.py
@@ -34,7 +34,7 @@
-_BLOBSTORE_FILESYSTEM = 'blobstore'
+_BLOBSTORE_FILESYSTEM = files.BLOBSTORE_FILESYSTEM
_BLOBSTORE_DIRECTORY = '/' + _BLOBSTORE_FILESYSTEM + '/'
_BLOBSTORE_NEW_FILE_NAME = 'new'
_CREATION_HANDLE_PREFIX = 'writable:'
diff --git a/google/appengine/api/files/file.py b/google/appengine/api/files/file.py
index 5cb5cdd..d5483e4 100755
--- a/google/appengine/api/files/file.py
+++ b/google/appengine/api/files/file.py
@@ -25,12 +25,15 @@
__all__ = [
'ApiTemporaryUnavailableError',
+ 'BLOBSTORE_FILESYSTEM',
'Error',
'ExclusiveLockFailedError',
'ExistenceError',
'FileNotOpenedError',
'FileTemporaryUnavailableError',
+ 'FILESYSTEMS',
'FinalizationError',
+ 'GS_FILESYSTEM',
'InvalidArgumentError',
'InvalidFileNameError',
'InvalidParameterError',
@@ -53,7 +56,6 @@
'BufferedFile',
]
-import logging
import gc
import os
@@ -62,6 +64,11 @@
from google.appengine.runtime import apiproxy_errors
+BLOBSTORE_FILESYSTEM = 'blobstore'
+GS_FILESYSTEM = 'gs'
+FILESYSTEMS = (BLOBSTORE_FILESYSTEM, GS_FILESYSTEM)
+
+
class Error(Exception):
"""Base error class for this module."""
@@ -597,3 +604,17 @@
self._buffer_pos = 0
else:
raise InvalidArgumentError('Whence mode %d is not supported', whence)
+
+
+def _default_gs_bucket_name():
+ """Return the default Google Storage bucket name for the application.
+
+ Returns:
+ A string that is the default bucket name for the application.
+ """
+ request = file_service_pb.GetDefaultGsBucketNameRequest()
+ response = file_service_pb.GetDefaultGsBucketNameResponse()
+
+ _make_call('GetDefaultGsBucketName', request, response)
+
+ return response.default_gs_bucket_name()
diff --git a/google/appengine/api/files/file_service_pb.py b/google/appengine/api/files/file_service_pb.py
index 0fa343b..de4bb46 100755
--- a/google/appengine/api/files/file_service_pb.py
+++ b/google/appengine/api/files/file_service_pb.py
@@ -4939,7 +4939,167 @@
_STYLE = """"""
_STYLE_CONTENT_TYPE = """"""
_PROTO_DESCRIPTOR_NAME = 'apphosting.files.FinalizeResponse'
+class GetDefaultGsBucketNameRequest(ProtocolBuffer.ProtocolMessage):
+
+ def __init__(self, contents=None):
+ pass
+ if contents is not None: self.MergeFromString(contents)
+
+
+ def MergeFrom(self, x):
+ assert x is not self
+
+ def Equals(self, x):
+ if x is self: return 1
+ return 1
+
+ def IsInitialized(self, debug_strs=None):
+ initialized = 1
+ return initialized
+
+ def ByteSize(self):
+ n = 0
+ return n
+
+ def ByteSizePartial(self):
+ n = 0
+ return n
+
+ def Clear(self):
+ pass
+
+ def OutputUnchecked(self, out):
+ pass
+
+ def OutputPartial(self, out):
+ pass
+
+ def TryMerge(self, d):
+ while d.avail() > 0:
+ tt = d.getVarInt32()
+
+
+ if (tt == 0): raise ProtocolBuffer.ProtocolBufferDecodeError
+ d.skipData(tt)
+
+
+ def __str__(self, prefix="", printElemNumber=0):
+ res=""
+ return res
+
+
+ def _BuildTagLookupTable(sparse, maxtag, default=None):
+ return tuple([sparse.get(i, default) for i in xrange(0, 1+maxtag)])
+
+
+ _TEXT = _BuildTagLookupTable({
+ 0: "ErrorCode",
+ }, 0)
+
+ _TYPES = _BuildTagLookupTable({
+ 0: ProtocolBuffer.Encoder.NUMERIC,
+ }, 0, ProtocolBuffer.Encoder.MAX_TYPE)
+
+
+ _STYLE = """"""
+ _STYLE_CONTENT_TYPE = """"""
+ _PROTO_DESCRIPTOR_NAME = 'apphosting.files.GetDefaultGsBucketNameRequest'
+class GetDefaultGsBucketNameResponse(ProtocolBuffer.ProtocolMessage):
+ has_default_gs_bucket_name_ = 0
+ default_gs_bucket_name_ = ""
+
+ def __init__(self, contents=None):
+ if contents is not None: self.MergeFromString(contents)
+
+ def default_gs_bucket_name(self): return self.default_gs_bucket_name_
+
+ def set_default_gs_bucket_name(self, x):
+ self.has_default_gs_bucket_name_ = 1
+ self.default_gs_bucket_name_ = x
+
+ def clear_default_gs_bucket_name(self):
+ if self.has_default_gs_bucket_name_:
+ self.has_default_gs_bucket_name_ = 0
+ self.default_gs_bucket_name_ = ""
+
+ def has_default_gs_bucket_name(self): return self.has_default_gs_bucket_name_
+
+
+ def MergeFrom(self, x):
+ assert x is not self
+ if (x.has_default_gs_bucket_name()): self.set_default_gs_bucket_name(x.default_gs_bucket_name())
+
+ def Equals(self, x):
+ if x is self: return 1
+ if self.has_default_gs_bucket_name_ != x.has_default_gs_bucket_name_: return 0
+ if self.has_default_gs_bucket_name_ and self.default_gs_bucket_name_ != x.default_gs_bucket_name_: return 0
+ return 1
+
+ def IsInitialized(self, debug_strs=None):
+ initialized = 1
+ return initialized
+
+ def ByteSize(self):
+ n = 0
+ if (self.has_default_gs_bucket_name_): n += 1 + self.lengthString(len(self.default_gs_bucket_name_))
+ return n
+
+ def ByteSizePartial(self):
+ n = 0
+ if (self.has_default_gs_bucket_name_): n += 1 + self.lengthString(len(self.default_gs_bucket_name_))
+ return n
+
+ def Clear(self):
+ self.clear_default_gs_bucket_name()
+
+ def OutputUnchecked(self, out):
+ if (self.has_default_gs_bucket_name_):
+ out.putVarInt32(10)
+ out.putPrefixedString(self.default_gs_bucket_name_)
+
+ def OutputPartial(self, out):
+ if (self.has_default_gs_bucket_name_):
+ out.putVarInt32(10)
+ out.putPrefixedString(self.default_gs_bucket_name_)
+
+ def TryMerge(self, d):
+ while d.avail() > 0:
+ tt = d.getVarInt32()
+ if tt == 10:
+ self.set_default_gs_bucket_name(d.getPrefixedString())
+ continue
+
+
+ if (tt == 0): raise ProtocolBuffer.ProtocolBufferDecodeError
+ d.skipData(tt)
+
+
+ def __str__(self, prefix="", printElemNumber=0):
+ res=""
+ if self.has_default_gs_bucket_name_: res+=prefix+("default_gs_bucket_name: %s\n" % self.DebugFormatString(self.default_gs_bucket_name_))
+ return res
+
+
+ def _BuildTagLookupTable(sparse, maxtag, default=None):
+ return tuple([sparse.get(i, default) for i in xrange(0, 1+maxtag)])
+
+ kdefault_gs_bucket_name = 1
+
+ _TEXT = _BuildTagLookupTable({
+ 0: "ErrorCode",
+ 1: "default_gs_bucket_name",
+ }, 1)
+
+ _TYPES = _BuildTagLookupTable({
+ 0: ProtocolBuffer.Encoder.NUMERIC,
+ 1: ProtocolBuffer.Encoder.STRING,
+ }, 1, ProtocolBuffer.Encoder.MAX_TYPE)
+
+
+ _STYLE = """"""
+ _STYLE_CONTENT_TYPE = """"""
+ _PROTO_DESCRIPTOR_NAME = 'apphosting.files.GetDefaultGsBucketNameResponse'
if _extension_runtime:
pass
-__all__ = ['FileServiceErrors','KeyValue','KeyValues','FileContentType','CreateRequest_Parameter','CreateRequest','CreateResponse','OpenRequest','OpenResponse','CloseRequest','CloseResponse','FileStat','StatRequest','StatResponse','AppendRequest','AppendResponse','DeleteRequest','DeleteResponse','ReadRequest','ReadResponse','ReadKeyValueRequest','ReadKeyValueResponse_KeyValue','ReadKeyValueResponse','ShuffleEnums','ShuffleInputSpecification','ShuffleOutputSpecification','ShuffleRequest_Callback','ShuffleRequest','ShuffleResponse','GetShuffleStatusRequest','GetShuffleStatusResponse','GetCapabilitiesRequest','GetCapabilitiesResponse','FinalizeRequest','FinalizeResponse']
+__all__ = ['FileServiceErrors','KeyValue','KeyValues','FileContentType','CreateRequest_Parameter','CreateRequest','CreateResponse','OpenRequest','OpenResponse','CloseRequest','CloseResponse','FileStat','StatRequest','StatResponse','AppendRequest','AppendResponse','DeleteRequest','DeleteResponse','ReadRequest','ReadResponse','ReadKeyValueRequest','ReadKeyValueResponse_KeyValue','ReadKeyValueResponse','ShuffleEnums','ShuffleInputSpecification','ShuffleOutputSpecification','ShuffleRequest_Callback','ShuffleRequest','ShuffleResponse','GetShuffleStatusRequest','GetShuffleStatusResponse','GetCapabilitiesRequest','GetCapabilitiesResponse','FinalizeRequest','FinalizeResponse','GetDefaultGsBucketNameRequest','GetDefaultGsBucketNameResponse']
diff --git a/google/appengine/api/files/file_service_stub.py b/google/appengine/api/files/file_service_stub.py
index e9381c9..9e5eccf 100755
--- a/google/appengine/api/files/file_service_stub.py
+++ b/google/appengine/api/files/file_service_stub.py
@@ -46,6 +46,8 @@
MAX_REQUEST_SIZE = 32 << 20
+GS_INFO_KIND = '__GsFileInfo__'
+
_now_function = datetime.datetime.now
@@ -101,6 +103,18 @@
self.blob_storage.StoreBlob(self.get_blob_key(upload.key), upload.buf)
del self.sequence_keys[filename]
+
+ encoded_key = blobstore.create_gs_key(upload.key)
+ file_info = datastore.Entity(GS_INFO_KIND,
+ name=encoded_key,
+ namespace='')
+ file_info['creation'] = _now_function()
+ file_info['filename'] = upload.key
+ file_info['size'] = upload.buf.len
+ file_info['content_type'] = upload.content_type
+ file_info['storage_key'] = self.get_blob_key(upload.key)
+ datastore.Put(file_info)
+
@staticmethod
def get_blob_key(key):
"""Converts a bigstore key into a base64 encoded blob key/filename."""
@@ -146,6 +160,12 @@
self.uploads[writable_name] = self._Upload(
StringIO.StringIO(), mime_type, gs_filename)
self.sequence_keys[writable_name] = None
+
+
+ datastore.Delete(
+ datastore.Key.from_path(GS_INFO_KIND,
+ blobstore.create_gs_key(gs_filename),
+ namespace=''))
return writable_name
def append(self, filename, data, sequence_key):
@@ -531,3 +551,7 @@
response.add_filesystem('blobstore')
response.add_filesystem('gs')
response.set_shuffle_available(False)
+
+ def _Dynamic_GetDefaultGsBucketName(self, request, response):
+ """Handler for GetDefaultGsBucketName RPC call."""
+ response.set_default_gs_bucket_name('app_default_bucket')
diff --git a/google/appengine/api/files/gs.py b/google/appengine/api/files/gs.py
index 004248d..0fa5e7e 100644
--- a/google/appengine/api/files/gs.py
+++ b/google/appengine/api/files/gs.py
@@ -34,8 +34,8 @@
-_GS_FILESYSTEM = 'gs'
-_GS_PREFIX = '/gs/'
+_GS_FILESYSTEM = files.GS_FILESYSTEM
+_GS_PREFIX = '/' + _GS_FILESYSTEM + '/'
_MIME_TYPE_PARAMETER = 'content_type'
_CANNED_ACL_PARAMETER = 'acl'
_CONTENT_ENCODING_PARAMETER = 'content_encoding'
@@ -51,7 +51,7 @@
content_encoding=None,
content_disposition=None,
user_metadata=None):
- """Create a writable blobstore file.
+ """Create a writable googlestore file.
Args:
filename: Google Storage object name (/gs/bucket/object)
@@ -120,3 +120,12 @@
'Expected string for value in user_metadata for key: ', key)
params[_USER_METADATA_PREFIX + key] = value
return files._create(_GS_FILESYSTEM, filename=filename, params=params)
+
+
+def default_bucket_name():
+ """Obtain the default Google Storage bucket name for this application.
+
+ Returns:
+ A string that is the name of the default bucket.
+ """
+ return files._default_gs_bucket_name()
diff --git a/google/appengine/api/logservice/log_service_pb.py b/google/appengine/api/logservice/log_service_pb.py
index ca4af07..50ebcf4 100755
--- a/google/appengine/api/logservice/log_service_pb.py
+++ b/google/appengine/api/logservice/log_service_pb.py
@@ -3725,9 +3725,12 @@
class LogUsageResponse(ProtocolBuffer.ProtocolMessage):
has_summary_ = 0
summary_ = None
+ has_limited_summary_ = 0
+ limited_summary_ = None
def __init__(self, contents=None):
self.usage_ = []
+ self.limited_usage_ = []
self.lazy_init_lock_ = thread.allocate_lock()
if contents is not None: self.MergeFromString(contents)
@@ -3766,11 +3769,48 @@
def has_summary(self): return self.has_summary_
+ def limited_usage_size(self): return len(self.limited_usage_)
+ def limited_usage_list(self): return self.limited_usage_
+
+ def limited_usage(self, i):
+ return self.limited_usage_[i]
+
+ def mutable_limited_usage(self, i):
+ return self.limited_usage_[i]
+
+ def add_limited_usage(self):
+ x = LogUsageRecord()
+ self.limited_usage_.append(x)
+ return x
+
+ def clear_limited_usage(self):
+ self.limited_usage_ = []
+ def limited_summary(self):
+ if self.limited_summary_ is None:
+ self.lazy_init_lock_.acquire()
+ try:
+ if self.limited_summary_ is None: self.limited_summary_ = LogUsageRecord()
+ finally:
+ self.lazy_init_lock_.release()
+ return self.limited_summary_
+
+ def mutable_limited_summary(self): self.has_limited_summary_ = 1; return self.limited_summary()
+
+ def clear_limited_summary(self):
+
+ if self.has_limited_summary_:
+ self.has_limited_summary_ = 0;
+ if self.limited_summary_ is not None: self.limited_summary_.Clear()
+
+ def has_limited_summary(self): return self.has_limited_summary_
+
def MergeFrom(self, x):
assert x is not self
for i in xrange(x.usage_size()): self.add_usage().CopyFrom(x.usage(i))
if (x.has_summary()): self.mutable_summary().MergeFrom(x.summary())
+ for i in xrange(x.limited_usage_size()): self.add_limited_usage().CopyFrom(x.limited_usage(i))
+ if (x.has_limited_summary()): self.mutable_limited_summary().MergeFrom(x.limited_summary())
def Equals(self, x):
if x is self: return 1
@@ -3779,6 +3819,11 @@
if e1 != e2: return 0
if self.has_summary_ != x.has_summary_: return 0
if self.has_summary_ and self.summary_ != x.summary_: return 0
+ if len(self.limited_usage_) != len(x.limited_usage_): return 0
+ for e1, e2 in zip(self.limited_usage_, x.limited_usage_):
+ if e1 != e2: return 0
+ if self.has_limited_summary_ != x.has_limited_summary_: return 0
+ if self.has_limited_summary_ and self.limited_summary_ != x.limited_summary_: return 0
return 1
def IsInitialized(self, debug_strs=None):
@@ -3786,6 +3831,9 @@
for p in self.usage_:
if not p.IsInitialized(debug_strs): initialized=0
if (self.has_summary_ and not self.summary_.IsInitialized(debug_strs)): initialized = 0
+ for p in self.limited_usage_:
+ if not p.IsInitialized(debug_strs): initialized=0
+ if (self.has_limited_summary_ and not self.limited_summary_.IsInitialized(debug_strs)): initialized = 0
return initialized
def ByteSize(self):
@@ -3793,6 +3841,9 @@
n += 1 * len(self.usage_)
for i in xrange(len(self.usage_)): n += self.lengthString(self.usage_[i].ByteSize())
if (self.has_summary_): n += 1 + self.lengthString(self.summary_.ByteSize())
+ n += 1 * len(self.limited_usage_)
+ for i in xrange(len(self.limited_usage_)): n += self.lengthString(self.limited_usage_[i].ByteSize())
+ if (self.has_limited_summary_): n += 1 + self.lengthString(self.limited_summary_.ByteSize())
return n
def ByteSizePartial(self):
@@ -3800,11 +3851,16 @@
n += 1 * len(self.usage_)
for i in xrange(len(self.usage_)): n += self.lengthString(self.usage_[i].ByteSizePartial())
if (self.has_summary_): n += 1 + self.lengthString(self.summary_.ByteSizePartial())
+ n += 1 * len(self.limited_usage_)
+ for i in xrange(len(self.limited_usage_)): n += self.lengthString(self.limited_usage_[i].ByteSizePartial())
+ if (self.has_limited_summary_): n += 1 + self.lengthString(self.limited_summary_.ByteSizePartial())
return n
def Clear(self):
self.clear_usage()
self.clear_summary()
+ self.clear_limited_usage()
+ self.clear_limited_summary()
def OutputUnchecked(self, out):
for i in xrange(len(self.usage_)):
@@ -3815,6 +3871,14 @@
out.putVarInt32(18)
out.putVarInt32(self.summary_.ByteSize())
self.summary_.OutputUnchecked(out)
+ for i in xrange(len(self.limited_usage_)):
+ out.putVarInt32(34)
+ out.putVarInt32(self.limited_usage_[i].ByteSize())
+ self.limited_usage_[i].OutputUnchecked(out)
+ if (self.has_limited_summary_):
+ out.putVarInt32(42)
+ out.putVarInt32(self.limited_summary_.ByteSize())
+ self.limited_summary_.OutputUnchecked(out)
def OutputPartial(self, out):
for i in xrange(len(self.usage_)):
@@ -3825,6 +3889,14 @@
out.putVarInt32(18)
out.putVarInt32(self.summary_.ByteSizePartial())
self.summary_.OutputPartial(out)
+ for i in xrange(len(self.limited_usage_)):
+ out.putVarInt32(34)
+ out.putVarInt32(self.limited_usage_[i].ByteSizePartial())
+ self.limited_usage_[i].OutputPartial(out)
+ if (self.has_limited_summary_):
+ out.putVarInt32(42)
+ out.putVarInt32(self.limited_summary_.ByteSizePartial())
+ self.limited_summary_.OutputPartial(out)
def TryMerge(self, d):
while d.avail() > 0:
@@ -3841,6 +3913,18 @@
d.skip(length)
self.mutable_summary().TryMerge(tmp)
continue
+ if tt == 34:
+ length = d.getVarInt32()
+ tmp = ProtocolBuffer.Decoder(d.buffer(), d.pos(), d.pos() + length)
+ d.skip(length)
+ self.add_limited_usage().TryMerge(tmp)
+ continue
+ if tt == 42:
+ length = d.getVarInt32()
+ tmp = ProtocolBuffer.Decoder(d.buffer(), d.pos(), d.pos() + length)
+ d.skip(length)
+ self.mutable_limited_summary().TryMerge(tmp)
+ continue
if (tt == 0): raise ProtocolBuffer.ProtocolBufferDecodeError
@@ -3861,6 +3945,18 @@
res+=prefix+"summary <\n"
res+=self.summary_.__str__(prefix + " ", printElemNumber)
res+=prefix+">\n"
+ cnt=0
+ for e in self.limited_usage_:
+ elm=""
+ if printElemNumber: elm="(%d)" % cnt
+ res+=prefix+("limited_usage%s <\n" % elm)
+ res+=e.__str__(prefix + " ", printElemNumber)
+ res+=prefix+">\n"
+ cnt+=1
+ if self.has_limited_summary_:
+ res+=prefix+"limited_summary <\n"
+ res+=self.limited_summary_.__str__(prefix + " ", printElemNumber)
+ res+=prefix+">\n"
return res
@@ -3869,18 +3965,24 @@
kusage = 1
ksummary = 2
+ klimited_usage = 4
+ klimited_summary = 5
_TEXT = _BuildTagLookupTable({
0: "ErrorCode",
1: "usage",
2: "summary",
- }, 2)
+ 4: "limited_usage",
+ 5: "limited_summary",
+ }, 5)
_TYPES = _BuildTagLookupTable({
0: ProtocolBuffer.Encoder.NUMERIC,
1: ProtocolBuffer.Encoder.STRING,
2: ProtocolBuffer.Encoder.STRING,
- }, 2, ProtocolBuffer.Encoder.MAX_TYPE)
+ 4: ProtocolBuffer.Encoder.STRING,
+ 5: ProtocolBuffer.Encoder.STRING,
+ }, 5, ProtocolBuffer.Encoder.MAX_TYPE)
_STYLE = """"""
diff --git a/google/appengine/api/mail.py b/google/appengine/api/mail.py
index dbaa0e7..7207dcb 100755
--- a/google/appengine/api/mail.py
+++ b/google/appengine/api/mail.py
@@ -674,6 +674,11 @@
'attachments',
])
+ ALLOWED_EMPTY_PROPERTIES = set([
+ 'subject',
+ 'body'
+ ])
+
PROPERTIES.update(('to', 'cc', 'bcc'))
@@ -748,8 +753,6 @@
"""
if not hasattr(self, 'sender'):
raise MissingSenderError()
- if not hasattr(self, 'subject'):
- raise MissingSubjectError()
found_body = False
@@ -774,10 +777,6 @@
html.decode()
found_body = True
-
- if not found_body:
- raise MissingBodyError()
-
if hasattr(self, 'attachments'):
for file_name, data in _attachment_sequence(self.attachments):
@@ -830,13 +829,17 @@
if hasattr(self, 'reply_to'):
message.set_replyto(_to_str(self.reply_to))
- message.set_subject(_to_str(self.subject))
+ if hasattr(self, 'subject'):
+ message.set_subject(_to_str(self.subject))
+ else:
+ message.set_subject('')
if hasattr(self, 'body'):
body = self.body
if isinstance(body, EncodedPayload):
body = body.decode()
message.set_textbody(_to_str(body))
+
if hasattr(self, 'html'):
html = self.html
if isinstance(html, EncodedPayload):
@@ -944,7 +947,7 @@
if attr in ['sender', 'reply_to']:
check_email_valid(value, attr)
- if not value:
+ if not value and not attr in self.ALLOWED_EMPTY_PROPERTIES:
raise ValueError('May not set empty value for \'%s\'' % attr)
diff --git a/google/appengine/api/memcache/__init__.py b/google/appengine/api/memcache/__init__.py
index da639f1..3b08085 100755
--- a/google/appengine/api/memcache/__init__.py
+++ b/google/appengine/api/memcache/__init__.py
@@ -321,7 +321,9 @@
pid=None,
make_sync_call=None,
_app_id=None,
- _num_memcacheg_backends=None):
+ _num_memcacheg_backends=None,
+ _ignore_shardlock=None,
+ _memcache_pool_hint=None):
"""Create a new Client object.
No parameters are required.
@@ -345,6 +347,10 @@
+
+
+
+
self._pickler_factory = pickler
self._unpickler_factory = unpickler
self._pickle_protocol = pickleProtocol
@@ -352,10 +358,12 @@
self._persistent_load = pload
self._app_id = _app_id
self._num_memcacheg_backends = _num_memcacheg_backends
+ self._ignore_shardlock = _ignore_shardlock
+ self._memcache_pool_hint = _memcache_pool_hint
self._cas_ids = {}
- if _app_id and not _num_memcacheg_backends:
- raise ValueError('If you specify an _app_id, you must also '
- 'provide _num_memcacheg_backends')
+ if _app_id and not(_num_memcacheg_backends and _memcache_pool_hint):
+ raise ValueError('If you specify an _app_id, you must also provide '
+ '_num_memcacheg_backends and _memcache_pool_hint')
def cas_reset(self):
"""Clear the remembered CAS ids."""
@@ -404,7 +412,7 @@
return unpickler.load()
def _add_app_id(self, message):
- """Populate the app_id and num_memcacheg_backends fields in a message.
+ """Populates override field in message if accessing another app's memcache.
Args:
message: A protocol buffer supporting the mutable_override() operation.
@@ -413,6 +421,9 @@
app_override = message.mutable_override()
app_override.set_app_id(self._app_id)
app_override.set_num_memcacheg_backends(self._num_memcacheg_backends)
+ if self._ignore_shardlock:
+ app_override.set_ignore_shardlock(self._ignore_shardlock)
+ app_override.set_memcache_pool_hint(self._memcache_pool_hint)
def set_servers(self, servers):
"""Sets the pool of memcache servers used by the client.
diff --git a/google/appengine/api/memcache/memcache_service_pb.py b/google/appengine/api/memcache/memcache_service_pb.py
index db79f56..6088b00 100644
--- a/google/appengine/api/memcache/memcache_service_pb.py
+++ b/google/appengine/api/memcache/memcache_service_pb.py
@@ -39,6 +39,7 @@
NAMESPACE_NOT_SET = 2
PERMISSION_DENIED = 3
NUM_BACKENDS_UNSPECIFIED = 4
+ MEMCACHE_POOL_HINT_UNSPECIFIED = 5
_ErrorCode_NAMES = {
0: "OK",
@@ -46,6 +47,7 @@
2: "NAMESPACE_NOT_SET",
3: "PERMISSION_DENIED",
4: "NUM_BACKENDS_UNSPECIFIED",
+ 5: "MEMCACHE_POOL_HINT_UNSPECIFIED",
}
def ErrorCode_Name(cls, x): return cls._ErrorCode_NAMES.get(x, "")
@@ -120,6 +122,10 @@
app_id_ = ""
has_num_memcacheg_backends_ = 0
num_memcacheg_backends_ = 0
+ has_ignore_shardlock_ = 0
+ ignore_shardlock_ = 0
+ has_memcache_pool_hint_ = 0
+ memcache_pool_hint_ = ""
def __init__(self, contents=None):
if contents is not None: self.MergeFromString(contents)
@@ -150,11 +156,39 @@
def has_num_memcacheg_backends(self): return self.has_num_memcacheg_backends_
+ def ignore_shardlock(self): return self.ignore_shardlock_
+
+ def set_ignore_shardlock(self, x):
+ self.has_ignore_shardlock_ = 1
+ self.ignore_shardlock_ = x
+
+ def clear_ignore_shardlock(self):
+ if self.has_ignore_shardlock_:
+ self.has_ignore_shardlock_ = 0
+ self.ignore_shardlock_ = 0
+
+ def has_ignore_shardlock(self): return self.has_ignore_shardlock_
+
+ def memcache_pool_hint(self): return self.memcache_pool_hint_
+
+ def set_memcache_pool_hint(self, x):
+ self.has_memcache_pool_hint_ = 1
+ self.memcache_pool_hint_ = x
+
+ def clear_memcache_pool_hint(self):
+ if self.has_memcache_pool_hint_:
+ self.has_memcache_pool_hint_ = 0
+ self.memcache_pool_hint_ = ""
+
+ def has_memcache_pool_hint(self): return self.has_memcache_pool_hint_
+
def MergeFrom(self, x):
assert x is not self
if (x.has_app_id()): self.set_app_id(x.app_id())
if (x.has_num_memcacheg_backends()): self.set_num_memcacheg_backends(x.num_memcacheg_backends())
+ if (x.has_ignore_shardlock()): self.set_ignore_shardlock(x.ignore_shardlock())
+ if (x.has_memcache_pool_hint()): self.set_memcache_pool_hint(x.memcache_pool_hint())
def Equals(self, x):
if x is self: return 1
@@ -162,6 +196,10 @@
if self.has_app_id_ and self.app_id_ != x.app_id_: return 0
if self.has_num_memcacheg_backends_ != x.has_num_memcacheg_backends_: return 0
if self.has_num_memcacheg_backends_ and self.num_memcacheg_backends_ != x.num_memcacheg_backends_: return 0
+ if self.has_ignore_shardlock_ != x.has_ignore_shardlock_: return 0
+ if self.has_ignore_shardlock_ and self.ignore_shardlock_ != x.ignore_shardlock_: return 0
+ if self.has_memcache_pool_hint_ != x.has_memcache_pool_hint_: return 0
+ if self.has_memcache_pool_hint_ and self.memcache_pool_hint_ != x.memcache_pool_hint_: return 0
return 1
def IsInitialized(self, debug_strs=None):
@@ -180,6 +218,8 @@
n = 0
n += self.lengthString(len(self.app_id_))
n += self.lengthVarInt64(self.num_memcacheg_backends_)
+ if (self.has_ignore_shardlock_): n += 2
+ if (self.has_memcache_pool_hint_): n += 1 + self.lengthString(len(self.memcache_pool_hint_))
return n + 2
def ByteSizePartial(self):
@@ -190,17 +230,27 @@
if (self.has_num_memcacheg_backends_):
n += 1
n += self.lengthVarInt64(self.num_memcacheg_backends_)
+ if (self.has_ignore_shardlock_): n += 2
+ if (self.has_memcache_pool_hint_): n += 1 + self.lengthString(len(self.memcache_pool_hint_))
return n
def Clear(self):
self.clear_app_id()
self.clear_num_memcacheg_backends()
+ self.clear_ignore_shardlock()
+ self.clear_memcache_pool_hint()
def OutputUnchecked(self, out):
out.putVarInt32(10)
out.putPrefixedString(self.app_id_)
out.putVarInt32(16)
out.putVarInt32(self.num_memcacheg_backends_)
+ if (self.has_ignore_shardlock_):
+ out.putVarInt32(24)
+ out.putBoolean(self.ignore_shardlock_)
+ if (self.has_memcache_pool_hint_):
+ out.putVarInt32(34)
+ out.putPrefixedString(self.memcache_pool_hint_)
def OutputPartial(self, out):
if (self.has_app_id_):
@@ -209,6 +259,12 @@
if (self.has_num_memcacheg_backends_):
out.putVarInt32(16)
out.putVarInt32(self.num_memcacheg_backends_)
+ if (self.has_ignore_shardlock_):
+ out.putVarInt32(24)
+ out.putBoolean(self.ignore_shardlock_)
+ if (self.has_memcache_pool_hint_):
+ out.putVarInt32(34)
+ out.putPrefixedString(self.memcache_pool_hint_)
def TryMerge(self, d):
while d.avail() > 0:
@@ -219,6 +275,12 @@
if tt == 16:
self.set_num_memcacheg_backends(d.getVarInt32())
continue
+ if tt == 24:
+ self.set_ignore_shardlock(d.getBoolean())
+ continue
+ if tt == 34:
+ self.set_memcache_pool_hint(d.getPrefixedString())
+ continue
if (tt == 0): raise ProtocolBuffer.ProtocolBufferDecodeError
@@ -229,6 +291,8 @@
res=""
if self.has_app_id_: res+=prefix+("app_id: %s\n" % self.DebugFormatString(self.app_id_))
if self.has_num_memcacheg_backends_: res+=prefix+("num_memcacheg_backends: %s\n" % self.DebugFormatInt32(self.num_memcacheg_backends_))
+ if self.has_ignore_shardlock_: res+=prefix+("ignore_shardlock: %s\n" % self.DebugFormatBool(self.ignore_shardlock_))
+ if self.has_memcache_pool_hint_: res+=prefix+("memcache_pool_hint: %s\n" % self.DebugFormatString(self.memcache_pool_hint_))
return res
@@ -237,18 +301,24 @@
kapp_id = 1
knum_memcacheg_backends = 2
+ kignore_shardlock = 3
+ kmemcache_pool_hint = 4
_TEXT = _BuildTagLookupTable({
0: "ErrorCode",
1: "app_id",
2: "num_memcacheg_backends",
- }, 2)
+ 3: "ignore_shardlock",
+ 4: "memcache_pool_hint",
+ }, 4)
_TYPES = _BuildTagLookupTable({
0: ProtocolBuffer.Encoder.NUMERIC,
1: ProtocolBuffer.Encoder.STRING,
2: ProtocolBuffer.Encoder.NUMERIC,
- }, 2, ProtocolBuffer.Encoder.MAX_TYPE)
+ 3: ProtocolBuffer.Encoder.NUMERIC,
+ 4: ProtocolBuffer.Encoder.STRING,
+ }, 4, ProtocolBuffer.Encoder.MAX_TYPE)
_STYLE = """"""
diff --git a/google/appengine/api/pagespeedinfo.py b/google/appengine/api/pagespeedinfo.py
new file mode 100644
index 0000000..90c081f
--- /dev/null
+++ b/google/appengine/api/pagespeedinfo.py
@@ -0,0 +1,103 @@
+#!/usr/bin/env python
+#
+# Copyright 2007 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+
+
+
+"""Page Speed configuration tools.
+
+Library for parsing pagespeed.yaml files and working with these in memory.
+"""
+
+
+
+
+
+
+
+import google
+
+from google.appengine.api import validation
+from google.appengine.api import yaml_builder
+from google.appengine.api import yaml_listener
+from google.appengine.api import yaml_object
+
+_URL_BLACKLIST_REGEX = r'http(s)?://\S{0,499}'
+_REWRITER_NAME_REGEX = r'[a-zA-Z0-9_]+'
+
+URL_BLACKLIST = 'url_blacklist'
+ENABLED_REWRITERS = 'enabled_rewriters'
+DISABLED_REWRITERS = 'disabled_rewriters'
+
+
+class MalformedPagespeedConfiguration(Exception):
+ """Configuration file for Page Speed API is malformed."""
+
+
+
+
+
+
+class PagespeedInfoExternal(validation.Validated):
+ """Describes the format of a pagespeed.yaml file.
+
+ URL blacklist entries are patterns (with '?' and '*' as wildcards). Any URLs
+ that match a pattern on the blacklist will not be optimized by Page Speed.
+
+ Rewriter names are strings (like 'CombineCss' or 'RemoveComments') describing
+ individual Page Speed rewriters. A full list of valid rewriter names can be
+ found in the Page Speed documentation.
+ """
+ ATTRIBUTES = {
+ URL_BLACKLIST: validation.Optional(
+ validation.Repeated(validation.Regex(_URL_BLACKLIST_REGEX))),
+ ENABLED_REWRITERS: validation.Optional(
+ validation.Repeated(validation.Regex(_REWRITER_NAME_REGEX))),
+ DISABLED_REWRITERS: validation.Optional(
+ validation.Repeated(validation.Regex(_REWRITER_NAME_REGEX))),
+ }
+
+
+def LoadSinglePagespeed(pagespeed_info):
+ """Load a pagespeed.yaml file or string and return a PagespeedInfoExternal.
+
+ Args:
+ pagespeed_info: The contents of a pagespeed.yaml file as a string, or an
+ open file object.
+
+ Returns:
+ A PagespeedInfoExternal instance which represents the contents of the parsed
+ yaml file.
+
+ Raises:
+ yaml_errors.EventError: An error occured while parsing the yaml file.
+ MalformedPagespeedConfiguration: The configuration is parseable but invalid.
+ """
+ builder = yaml_object.ObjectBuilder(PagespeedInfoExternal)
+ handler = yaml_builder.BuilderHandler(builder)
+ listener = yaml_listener.EventListener(handler)
+ listener.Parse(pagespeed_info)
+
+ parsed_yaml = handler.GetResults()
+ if not parsed_yaml:
+ return PagespeedInfoExternal()
+
+ if len(parsed_yaml) > 1:
+ raise MalformedPagespeedConfiguration(
+ 'Multiple configuration sections in pagespeed.yaml')
+
+ return parsed_yaml[0]
diff --git a/google/appengine/api/search/__init__.py b/google/appengine/api/search/__init__.py
index 8a08dcf..3d4bafc 100644
--- a/google/appengine/api/search/__init__.py
+++ b/google/appengine/api/search/__init__.py
@@ -27,6 +27,7 @@
from search import RemoveDocumentError
from search import RemoveDocumentResult
from search import Document
+from search import DocumentCursor
from search import DocumentOperationResult
from search import Error
from search import Field
@@ -38,9 +39,17 @@
from search import ListDocumentsResponse
from search import list_indexes
from search import ListIndexesResponse
+from search import MatchScorer
from search import NumberField
+from search import Query
+from search import QueryOptions
+from search import RescoringMatchScorer
+from search import ScoredDocument
from search import SearchResponse
from search import SearchResult
+from search import SearchResults
+from search import SortExpression
+from search import SortOption
from search import SortSpec
from search import TextField
from search import TransientError
diff --git a/google/appengine/api/search/search.py b/google/appengine/api/search/search.py
index 8135c3f..4ceb571 100644
--- a/google/appengine/api/search/search.py
+++ b/google/appengine/api/search/search.py
@@ -34,6 +34,7 @@
import re
import string
import sys
+import warnings
from google.appengine.datastore import document_pb
from google.appengine.api import apiproxy_stub_map
@@ -50,6 +51,7 @@
'AddDocumentResult',
'AtomField',
'DateField',
+ 'DocumentCursor',
'RemoveDocumentError',
'RemoveDocumentResult',
'Document',
@@ -63,9 +65,17 @@
'InvalidRequest',
'ListIndexesResponse',
'ListDocumentsResponse',
+ 'MatchScorer',
'NumberField',
- 'SearchResult',
+ 'Query',
+ 'QueryOptions',
+ 'RescoringMatchScorer',
+ 'ScoredDocument',
'SearchResponse',
+ 'SearchResult',
+ 'SearchResults',
+ 'SortExpression',
+ 'SortOption',
'SortSpec',
'TextField',
'TransientError',
@@ -82,6 +92,12 @@
_MAXIMUM_STRING_LENGTH = 500
_MAXIMUM_DOCS_PER_REQUEST = 200
_MAXIMUM_EXPRESSION_LENGTH = 5000
+_MAXIMUM_QUERY_LENGTH = 1000
+_MAXIMUM_RETURNED_DOCUMENTS = 800
+_MAXIMUM_FOUND_COUNT_ACCURACY = 10000
+_MAXIMUM_FIELDS_TO_RETURN = 100
+
+_MAXIMUM_SORT_LIMIT = 10000
_VISIBLE_PRINTABLE_ASCII = frozenset(
set(string.printable) - set(string.whitespace))
@@ -493,6 +509,11 @@
return language
+def _CheckSortLimit(limit):
+ """Checks the limit on number of docs to score is not too large."""
+ return _CheckInteger(limit, 'limit', upper_bound=_MAXIMUM_SORT_LIMIT)
+
+
def _Repr(class_instance, ordered_dictionary):
"""Generates an unambiguous representation for instance and ordered dict."""
return 'search.%s(%s)' % (class_instance.__class__.__name__, ', '.join(
@@ -961,13 +982,18 @@
return pb
+def _NewFieldsFromPb(field_list):
+ """Returns a list of Field copied from a document_pb.Document proto buf."""
+ return [_NewFieldFromPb(f) for f in field_list]
+
+
def _NewDocumentFromPb(doc_pb):
"""Constructs a Document from a document_pb.Document protocol buffer."""
- fields = [_NewFieldFromPb(f) for f in doc_pb.field_list()]
lang = None
if doc_pb.has_language():
lang = doc_pb.language()
- return Document(doc_id=doc_pb.id(), fields=fields,
+ return Document(doc_id=doc_pb.id(),
+ fields=_NewFieldsFromPb(doc_pb.field_list()),
language=lang,
order_id=doc_pb.order_id())
@@ -980,9 +1006,9 @@
"""Represents an expression that will be computed for each result returned.
For example,
- FieldExpression(name='content-snippet',
+ FieldExpression(name='content_snippet',
expression='snippet("very important", content)')
- means a computed field 'content-snippet' will be returned with each search
+ means a computed field 'content_snippet' will be returned with each search
result, which contains HTML snippets of the 'content' field which match
the query 'very important'.
"""
@@ -1032,6 +1058,194 @@
pb.set_expression(field_expression.expression)
+class SortOption(object):
+ """Represents a single dimension for sorting on.
+
+ Use subclasses of this class: MatchScorer, RescoringMatchScorer or
+ SortExpression.
+ """
+
+
+ ASCENDING, DESCENDING = ('ASCENDING', 'DESCENDING')
+
+ _DIRECTIONS = frozenset([ASCENDING, DESCENDING])
+
+ def __init__(self, limit=1000):
+ """Initializer.
+
+ Args:
+ limit: The limit on the number of documents to score. Applicable if
+ using a scorer, ignored otherwise.
+
+ Raises:
+ TypeError: If any of the parameters has an invalid type, or an unknown
+ attribute is passed.
+ ValueError: If any of the parameters has an invalid value.
+ """
+ self._limit = _CheckSortLimit(limit)
+
+ @property
+ def limit(self):
+ """Returns the limit on the number of documents to score."""
+ return self._limit
+
+ @property
+ def direction(self):
+ """Returns the direction to sort documents based on score."""
+ return self.DESCENDING
+
+ def _CheckDirection(self, direction):
+ """Checks direction is a valid SortOption direction and returns it."""
+ return _CheckEnum(direction, 'direction', values=self._DIRECTIONS)
+
+ def __repr__(self):
+ return _Repr(
+ self, [('direction', self.direction),
+ ('limit', self.limit)])
+
+
+class MatchScorer(SortOption):
+ """Sort documents in ascending order of score.
+
+ Match scorer assigns a document a score based on term frequency
+ divided by document frequency.
+ """
+
+ def __init__(self, limit=1000):
+ """Initializer.
+
+ Args:
+ limit: The limit on the number of documents to score. Applicable if
+ using a scorer, ignored otherwise.
+
+ Raises:
+ TypeError: If any of the parameters has an invalid type, or an unknown
+ attribute is passed.
+ ValueError: If any of the parameters has an invalid value.
+ """
+ super(MatchScorer, self).__init__(limit=limit)
+
+
+class RescoringMatchScorer(SortOption):
+ """Sort documents in ascending order of score weighted on document parts.
+
+ A rescoring match scorer assigns a document a score based on
+ a match scorer and further assigning weights to document parts.
+ """
+
+ def __init__(self, limit=1000):
+ """Initializer.
+
+ Args:
+ limit: The limit on the number of documents to score. Applicable if
+ using a scorer, ignored otherwise.
+
+ Raises:
+ TypeError: If any of the parameters has an invalid type, or an unknown
+ attribute is passed.
+ ValueError: If any of the parameters has an invalid value.
+ """
+ super(RescoringMatchScorer, self).__init__(limit=limit)
+
+
+def _CopySortExpressionToProtocolBuffer(sort_expression, pb):
+ """Copies a SortExpression to a search_service_pb.SortSpec protocol buffer."""
+ pb.set_sort_expression(sort_expression.expression)
+ if sort_expression.direction == SortOption.ASCENDING:
+ pb.set_sort_descending(False)
+ if sort_expression.default_value is not None:
+ if isinstance(sort_expression.default_value, basestring):
+ pb.set_default_value_text(sort_expression.default_value)
+ else:
+ pb.set_default_value_numeric(sort_expression.default_value)
+ return pb
+
+
+def _CopySortOptionToScorerSpecProtocolBuffer(sort_option, pb):
+ """Copies a SortOption to a search_service_pb.ScorerSpec."""
+ if isinstance(sort_option, RescoringMatchScorer):
+ pb.set_scorer(search_service_pb.ScorerSpec.RESCORING_MATCH_SCORER)
+ elif isinstance(sort_option, MatchScorer):
+ pb.set_scorer(search_service_pb.ScorerSpec.MATCH_SCORER)
+ elif isinstance(sort_option, SortSpec):
+ _CopySortSpecToScorerSpecProtocolBuffer(sort_option, pb)
+ else:
+ raise TypeError('Expected MatchScorer or RescoringMatchRescorer but got %s'
+ % type(sort_option))
+ pb.set_limit(sort_option.limit)
+ return pb
+
+
+class SortExpression(SortOption):
+ """Sort by a user specified scoring expression."""
+
+
+ try:
+ MAX_FIELD_VALUE = unichr(0x10ffff) * 80
+ except ValueError:
+
+ MAX_FIELD_VALUE = unichr(0xffff) * 80
+
+ MIN_FIELD_VALUE = ''
+
+ def __init__(self, expression=None, direction=SortOption.DESCENDING,
+ default_value=None, limit=1000):
+ """Initializer.
+
+ Args:
+ expression: An expression to be evaluated on each matching document
+ to sort by. The expression can simply be a field name,
+ or some compound expression such as "score + count(likes) * 0.1"
+ which will add the score from a scorer to a count of the values
+ of a likes field times 0.1.
+ direction: The direction to sort the search results, either ASCENDING
+ or DESCENDING
+ default_value: The default value of the expression, if no field
+ present nor can be calculated for a document. A text value must
+ be specified for text sorts. A numeric value must be specified for
+ numeric sorts.
+ limit: The limit on the number of documents to score.
+
+ Raises:
+ TypeError: If any of the parameters has an invalid type, or an unknown
+ attribute is passed.
+ ValueError: If any of the parameters has an invalid value.
+ """
+ super(SortExpression, self).__init__(limit=limit)
+ self._expression = expression
+ self._direction = self._CheckDirection(direction)
+ self._default_value = default_value
+ if expression is None:
+ raise TypeError('expression required for SortExpression')
+ _CheckExpression(expression)
+ if isinstance(self.default_value, basestring):
+ _CheckText(self._default_value, 'default_value')
+ elif self._default_value is not None:
+ _CheckNumber(self._default_value, 'default_value')
+
+ @property
+ def expression(self):
+ """Returns the expression to sort by."""
+ return self._expression
+
+ @property
+ def direction(self):
+ """Returns the direction to sort expression: ASCENDING or DESCENDING."""
+ return self._direction
+
+ @property
+ def default_value(self):
+ """Returns a default value for the expression if no value computed."""
+ return self._default_value
+
+ def __repr__(self):
+ return _Repr(
+ self, [('expression', self.expression),
+ ('direction', self.direction),
+ ('default_value', self.default_value),
+ ('limit', self.limit)])
+
+
class SortSpec(object):
"""Sorting specification for a single dimension.
@@ -1052,8 +1266,6 @@
CUSTOM, MATCH_SCORER, RESCORING_MATCH_SCORER = (
'CUSTOM', 'MATCH_SCORER', 'RESCORING_MATCH_SCORER')
- _MAXIMUM_LIMIT = 10000
-
_DIRECTIONS = frozenset([ASCENDING, DESCENDING])
_TYPES = frozenset([CUSTOM, MATCH_SCORER, RESCORING_MATCH_SCORER])
@@ -1099,6 +1311,10 @@
attribute is passed.
ValueError: If any of the parameters has an invalid value.
"""
+ warnings.warn('search.Index.SortSpec is deprecated: '
+ 'use MatchScorer, RescoringMatchScorer or SortExpression in '
+ 'search.QueryOptions.sort_options instead.',
+ DeprecationWarning, stacklevel=2)
self._sort_type = self._CheckType(sort_type)
self._expression = expression
self._direction = self._CheckDirection(direction)
@@ -1116,7 +1332,7 @@
raise TypeError('expression only allowed in CUSTOM sort_type')
if default_value is not None:
raise TypeError('default_value only allowed in CUSTOM sort_type')
- self._limit = self._CheckLimit(limit)
+ self._limit = _CheckSortLimit(limit)
@property
def sort_type(self):
@@ -1151,10 +1367,6 @@
"""Checks direction is a valid SortSpec direction and returns it."""
return _CheckEnum(direction, 'direction', values=self._DIRECTIONS)
- def _CheckLimit(self, limit):
- """Checks the limit on number of docs to score is not too large."""
- return _CheckInteger(limit, 'limit', upper_bound=self._MAXIMUM_LIMIT)
-
def __repr__(self):
return _Repr(
self, [('sort_type', self.sort_type),
@@ -1193,7 +1405,6 @@
class SearchResult(object):
"""Represents a result of executing a search request."""
-
def __init__(self, document, sort_scores=None, expressions=None, cursor=None):
"""Initializer.
@@ -1206,7 +1417,7 @@
sorts; negative scores for descending.
expressions: The list of computed fields which are the result of
expressions requested.
- cursor: A cursor associated with the document.
+ cursor: A DocumentCursor associated with the document.
Raises:
TypeError: If any of the parameters have invalid types, or an unknown
@@ -1216,7 +1427,7 @@
self._document = document
self._sort_scores = self._CheckSortScores(_GetList(sort_scores))
self._expressions = _GetList(expressions)
- self._cursor = self._CheckCursor(cursor)
+ self._cursor = _CheckCursor(cursor)
@property
def document(self):
@@ -1282,14 +1493,180 @@
('cursor', self.cursor)])
-class SearchResponse(object):
+class ScoredDocument(Document):
+ """Represents a scored document returned from a search."""
+
+
+ def __init__(self, doc_id=None, fields=None, language='en', order_id=None,
+ sort_scores=None, expressions=None, cursor=None):
+ """Initializer.
+
+ Args:
+ doc_id: The visible printable ASCII string identifying the document which
+ does not start with '!'. Whitespace is excluded from ids. If no id is
+ provided, the search service will provide one.
+ fields: An iterable of Field instances representing the content of the
+ document.
+ language: The code of the language used in the field values.
+ order_id: The id used to specify the order this document will be returned
+ in search results, where 0 <= order_id <= sys.maxint. If not specified,
+ the number of seconds since 1st Jan 2011 is used. Documents are returned
+ in descending order of the order ID.
+ sort_scores: The list of scores assigned during sort evaluation. Each
+ sort dimension is included. Positive scores are used for ascending
+ sorts; negative scores for descending.
+ expressions: The list of computed fields which are the result of
+ expressions requested.
+ cursor: A cursor associated with the document.
+
+ Raises:
+ TypeError: If any of the parameters have invalid types, or an unknown
+ attribute is passed.
+ ValueError: If any of the parameters have invalid values.
+ """
+ super(ScoredDocument, self).__init__(doc_id=doc_id, fields=fields,
+ language=language, order_id=order_id)
+ self._sort_scores = self._CheckSortScores(_GetList(sort_scores))
+ self._expressions = _GetList(expressions)
+ if cursor is not None and not isinstance(cursor, DocumentCursor):
+ raise TypeError('expected a DocumentCursor but got %s' % type(cursor))
+ self._cursor = cursor
+
+ @property
+ def sort_scores(self):
+ """The list of scores assigned during sort evaluation.
+
+ Each sort dimension is included. Positive scores are used for ascending
+ sorts; negative scores for descending.
+
+ Returns:
+ The list of numeric sort scores.
+ """
+ return self._sort_scores
+
+ @property
+ def expressions(self):
+ """The list of computed fields the result of expression evaluation.
+
+ For example, if a request has
+ FieldExpression(name='snippet', 'snippet("good story", content)')
+ meaning to compute a snippet field containing HTML snippets extracted
+ from the matching of the query 'good story' on the field 'content'.
+ This means a field such as the following will be returned in expressions
+ for the search result:
+ HtmlField(name='snippet', value='that was a <b>good story</b> to finish')
+
+ Returns:
+ The computed fields.
+ """
+ return self._expressions
+
+ @property
+ def cursor(self):
+ """A cursor associated with a result, a continued search starting point.
+
+ To get this cursor to appear, set the Index.cursor_type to
+ Index.RESULT_CURSOR, otherwise this will be None.
+
+ Returns:
+ The result cursor.
+ """
+ return self._cursor
+
+ def _CheckSortScores(self, sort_scores):
+ """Checks sort_scores is a list of floats, and returns it."""
+ for sort_score in sort_scores:
+ _CheckNumber(sort_score, 'sort_scores')
+ return sort_scores
+
+ def _CheckCursor(self, cursor):
+ """Checks cursor is a string which is not too long, and returns it."""
+ return _ValidateString(cursor, 'cursor', _MAXIMUM_CURSOR_LENGTH,
+ empty_ok=True)
+
+ def __repr__(self):
+ return _Repr(self, [('doc_id', self.doc_id),
+ ('fields', self.fields),
+ ('language', self.language),
+ ('order_id', self.order_id),
+ ('sort_scores', self.sort_scores),
+ ('expressions', self.expressions),
+ ('cursor', self.cursor)])
+
+
+class SearchResults(object):
"""Represents the result of executing a search request."""
+ def __init__(self, number_found, results=None, cursor=None):
+ """Initializer.
+
+ Args:
+ number_found: The number of documents found for the query.
+ results: The list of ScoredDocuments returned from executing a
+ search request.
+ cursor: A DocumentCursor to continue the search from the end of the
+ search results.
+
+ Raises:
+ TypeError: If any of the parameters have an invalid type, or an unknown
+ attribute is passed.
+ ValueError: If any of the parameters have an invalid value.
+ """
+ self._number_found = _CheckInteger(number_found, 'number_found')
+ self._results = _GetList(results)
+ if cursor is not None and not isinstance(cursor, DocumentCursor):
+ raise TypeError('expected a DocumentCursor but got %s' % type(cursor))
+ self._cursor = cursor
+
+ def __iter__(self):
+
+ for result in self.results:
+ yield result
+
+ @property
+ def results(self):
+ """Returns the list of ScoredDocuments that matched the query."""
+ return self._results
+
+ @property
+ def number_found(self):
+ """Returns the number of documents which were found for the search.
+
+ Note that this is an approximation and not an exact count.
+ If QueryOptions.number_found_accuracy parameter is set to 100
+ for example, then number_found <= 100 is accurate.
+
+ Returns:
+ The number of documents found.
+ """
+ return self._number_found
+
+ @property
+ def cursor(self):
+ """Returns a cursor that can be used to continue search from last result.
+
+ This corresponds to using a ResultsCursor in QueryOptions,
+ otherwise this will be None.
+
+ Returns:
+ The results cursor.
+ """
+ return self._cursor
+
+ def __repr__(self):
+ return _Repr(self, [('results', self.results),
+ ('number_found', self.number_found),
+ ('cursor', self.cursor)])
+
+
+class SearchResponse(object):
+ """Represents the result of executing a search request. Deprecated."""
+
def __init__(self, matched_count, results=None, cursor=None):
"""Initializer.
Args:
- matched_count: The number of documents matched by the query.
+ matched_count: The count of documents that matched the query.
results: The list of SearchResult returned from executing a search
request.
cursor: A cursor to continue the search from the end of the
@@ -1425,6 +1802,434 @@
return _Repr(self, [('indexes', self.indexes)])
+class DocumentCursor(object):
+ """Specifies how to get the next page of results in a search.
+
+ A cursor returned in a previous set of search results to use as a starting
+ point to retrieve the next set of results. This can get you better
+ performance, and also improves the consistency of pagination through index
+ updates.
+
+ The following shows how to use the cursor to get the next page of results:
+
+ # get the first set of results, the first cursor is used to specify
+ # that cursors are to be returned in the SearchResults.
+ results = index.search(Query(query_string='some stuff',
+ QueryOptions(cursor=DocumentCursor()))
+
+ # get the next set of results
+ results = index.search(Query(query_string='some stuff',
+ QueryOptions(cursor=results.cursor)))
+
+ If you want to continue search from any one of the ScoredDocuments in
+ SearchResults, then you can set DocumentCursor.per_result to True.
+
+ # get the first set of results, the first cursor is used to specify
+ # that cursors are to be returned in the SearchResults.
+ results = index.search(Query(query_string='some stuff',
+ QueryOptions(cursor=DocumentCursor(per_result=True)))
+
+ # this shows how to access the per_document cursors returned from a search
+ per_document_cursor = None
+ for scored_document in results:
+ per_document_cursor = scored_document.cursor
+
+ # get the next set of results
+ results = index.search(Query(query_string='some stuff',
+ QueryOptions(cursor=per_document_cursor)))
+ """
+
+
+
+ def __init__(self, cursor_string=None, per_result=False):
+ """Initializer.
+
+ Args:
+ cursor_string: The cursor string returned from the search service to
+ be interpreted by the search service to get the next set of results.
+ per_result: A bool when true will return a cursor per ScoredDocument in
+ SearchResults, otherwise will return a single cursor for the whole
+ SearchResults. If using offset this is ignored, as the user is
+ responsible for calculating a next offset if any.
+ """
+ self._cursor_string = _CheckCursor(cursor_string)
+ self._per_result = per_result
+
+ @property
+ def cursor_string(self):
+ """Returns the cursor string generated by the search service."""
+ return self._cursor_string
+
+ @property
+ def per_result(self):
+ """Returns whether to return a cursor for each ScoredDocument in results."""
+ return self._per_result
+
+ def __repr__(self):
+ return _Repr(self, [('cursor_string', self.cursor_string),
+ ('per_result', self.per_result)])
+
+
+def _CheckQuery(query):
+ """Checks a query is a valid query string."""
+ _ValidateString(query, 'query', _MAXIMUM_QUERY_LENGTH, empty_ok=True)
+ if query is None:
+ raise ValueError('query must not be null')
+ if query.strip():
+ if isinstance(query, unicode):
+ query_parser.Parse(query)
+ else:
+ query_parser.Parse(unicode(query, 'utf-8'))
+ return query
+
+
+def _CheckLimit(limit):
+ """Checks the limit of documents to return is an integer within range."""
+ return _CheckInteger(
+ limit, 'limit', zero_ok=False,
+ upper_bound=_MAXIMUM_RETURNED_DOCUMENTS)
+
+
+def _CheckOffset(offset):
+ """Checks the offset in document list is an integer within range."""
+ return _CheckInteger(
+ offset, 'offset', zero_ok=True,
+ upper_bound=_MAXIMUM_RETURNED_DOCUMENTS)
+
+
+def _CheckNumberFoundAccuracy(number_found_accuracy):
+ """Checks the accuracy is an integer within range."""
+ return _CheckInteger(
+ number_found_accuracy, 'number_found_accuracy',
+ zero_ok=False, upper_bound=_MAXIMUM_FOUND_COUNT_ACCURACY)
+
+
+def _CheckCursor(cursor):
+ """Checks the cursor if specified is a string which is not too long."""
+ return _ValidateString(cursor, 'cursor', _MAXIMUM_CURSOR_LENGTH,
+ empty_ok=True)
+
+
+def _CheckNumberOfFields(returned_expressions, snippeted_fields,
+ returned_fields):
+ """Checks the count of all field kinds is less than limit."""
+ number_expressions = (len(returned_expressions) + len(snippeted_fields) +
+ len(returned_fields))
+ if number_expressions > _MAXIMUM_FIELDS_TO_RETURN:
+ raise ValueError(
+ 'too many fields, snippets or expressions to return %d > maximum %d'
+ % (number_expressions, _MAXIMUM_FIELDS_TO_RETURN))
+
+
+class QueryOptions(object):
+ """Options for post-processing results for a query.
+
+ Options include the ability to sort results, control which document fields
+ to return, produce snippets of fields and compute and sort by complex
+ scoring expressions.
+
+ If you wish to randomly access pages of search results, you can use an
+ offset:
+
+ # get the first set of results
+ page_size = 10
+ results = index.search(Query(query_string='some stuff',
+ QueryOptions(limit=page_size))
+
+ # calculate pages
+ pages = results.found_count / page_size
+
+ # user chooses page and hence an offset into results
+ next_page = ith * page_size
+
+ # get the search results for that page
+ results = index.search(Query(query_string='some stuff',
+ QueryOptions(limit=page_size, offset=next_page))
+ """
+
+ def __init__(self, limit=20, number_found_accuracy=100, cursor=None,
+ offset=None, sort_options=None, returned_fields=None,
+ ids_only=False, snippeted_fields=None,
+ returned_expressions=None):
+
+
+ """Initializer.
+
+ For example, the following code fragment requests a search for
+ documents where 'first' occurs in subject and 'good' occurs anywhere,
+ returning at most 20 documents, starting the search from 'cursor token',
+ returning another single cursor for the SearchResults, sorting by subject in
+ descending order, returning the author, subject, and summary fields as well
+ as a snippeted field content.
+
+ results = index.search(Query(
+ query='subject:first good',
+ options=QueryOptions(
+ limit=20,
+ cursor=DocumentCursor(),
+ sort_options=[
+ SortOptions(expression='subject', default_value='')],
+ returned_fields=['author', 'subject', 'summary'],
+ snippeted_fields=['content'])))
+
+ Args:
+ limit: The limit on number of documents to return in results.
+ number_found_accuracy: The minimum accuracy requirement for
+ SearchResults.number_found. If set, the number_found will be
+ accurate up to at least that number. For example, when set to 100,
+ any SearchResults with number_found <= 100 is accurate. This option
+ may add considerable latency/expense, especially when used with
+ returned_fields.
+ cursor: A DocumentCursor describing where to get the next set of results,
+ or to provide next cursors in SearchResults.
+ offset: The offset is number of documents to skip in search results. This
+ is an alternative to using a query cursor, but allows random access into
+ the results. Using offsets rather than cursors are more expensive. You
+ can only use either cursor or offset, but not both. Using an offset
+ means that no cursor is returned in SearchResults.cursor, nor in each
+ ScoredDocument.cursor.
+ sort_options: An iterable of SortOptions specifying a multi-dimensional
+ sort over the search results.
+ returned_fields: An iterable of names of fields to return in search
+ results.
+ ids_only: Only return document ids, do not return any fields.
+ snippeted_fields: An iterable of names of fields to snippet and return
+ in search result expressions.
+ returned_expressions: An iterable of FieldExpression to evaluate and
+ return in search results.
+ Raises:
+ TypeError: If an unknown iterator_options or sort_options is passed.
+ ValueError: If ids_only and returned_fields are used together.
+ """
+ self._limit = _CheckLimit(limit)
+ self._number_found_accuracy = _CheckNumberFoundAccuracy(
+ number_found_accuracy)
+ if cursor is not None and not isinstance(cursor, DocumentCursor):
+ raise TypeError('expected a DocumentCursor but got %s' % type(cursor))
+ if cursor is not None and offset is not None:
+ raise ValueError('cannot set cursor and offset together')
+ self._cursor = cursor
+ self._offset = _CheckOffset(offset)
+ self._sort_options = _GetList(sort_options)
+ for sort_option in self._sort_options:
+ if not isinstance(sort_option, SortOption):
+ raise TypeError('expected a subtype of SortOption but got %s' %
+ type(sort_option))
+
+ self._returned_fields = _ConvertToList(returned_fields)
+ _CheckFieldNames(self._returned_fields)
+ self._ids_only = ids_only
+ if self._ids_only and self._returned_fields:
+ raise ValueError('cannot have ids_only and returned_fields set together')
+ self._snippeted_fields = _ConvertToList(snippeted_fields)
+ _CheckFieldNames(self._snippeted_fields)
+ self._returned_expressions = _ConvertToList(returned_expressions)
+ for expression in self._returned_expressions:
+ _CheckFieldName(expression.name)
+ _CheckExpression(expression.expression)
+ _CheckNumberOfFields(self._returned_expressions, self._snippeted_fields,
+ self._returned_fields)
+
+ @property
+ def limit(self):
+ """Returns a limit on number of documents to return in results."""
+ return self._limit
+
+ @property
+ def number_found_accuracy(self):
+ """Returns minimum accuracy requirement for SearchResults.number_found."""
+ return self._number_found_accuracy
+
+ @property
+ def cursor(self):
+ """Returns the DocumentCursor for the query."""
+ return self._cursor
+
+ @property
+ def offset(self):
+ """Returns the number of documents in search results to skip."""
+ return self._offset
+
+ @property
+ def sort_options(self):
+ """Returns iterable of SortOptions specifying sort order over results."""
+ return self._sort_options
+
+ @property
+ def returned_fields(self):
+ """Returns an iterable of names of fields to return in search results."""
+ return self._returned_fields
+
+ @property
+ def ids_only(self):
+ """Returns whether to return only document ids in search results."""
+ return self._ids_only
+
+ @property
+ def snippeted_fields(self):
+ """Returns iterable of field names to snippet and return in results."""
+ return self._snippeted_fields
+
+ @property
+ def returned_expressions(self):
+ """Returns iterable of FieldExpression to return in results."""
+ return self._returned_expressions
+
+ def __repr__(self):
+ return _Repr(self, [('limit', self.limit),
+ ('number_found_accuracy', self.number_found_accuracy),
+ ('cursor', self.cursor),
+ ('sort_options', self.sort_options),
+ ('returned_fields', self.returned_fields),
+ ('ids_only', self.ids_only),
+ ('snippeted_fields', self.snippeted_fields),
+ ('returned_expressions', self.returned_expressions)])
+
+
+def _CopyQueryOptionsObjectToProtocolBuffer(query, options, params):
+ """Copies a QueryOptions object to a SearchParams proto buff."""
+ offset = 0
+ cursor_string = None
+ cursor_type = search_service_pb.SearchParams.NONE
+ offset = options.offset
+ if options.cursor:
+ cursor = options.cursor
+ if cursor.per_result:
+ cursor_type = search_service_pb.SearchParams.PER_RESULT
+ else:
+ cursor_type = search_service_pb.SearchParams.SINGLE
+ if cursor.cursor_string:
+ cursor_string = cursor.cursor_string
+ _CopyQueryOptionsToProtocolBuffer(
+ query, offset, options.limit, options.number_found_accuracy,
+ cursor_string, cursor_type, options.ids_only, options.returned_fields,
+ options.snippeted_fields, options.returned_expressions,
+ options.sort_options, params)
+
+
+def _CopyQueryOptionsToProtocolBuffer(
+ query, offset, limit, number_found_accuracy, cursor, cursor_type, ids_only,
+ returned_fields, snippeted_fields, returned_expressions, sort_options,
+ params):
+ """Copies fields of QueryOptions to params protobuf."""
+ if offset:
+ params.set_offset(offset)
+ params.set_limit(limit)
+ params.set_matched_count_accuracy(number_found_accuracy)
+ if cursor:
+ params.set_cursor(cursor)
+
+ params.set_cursor_type(cursor_type)
+ if ids_only:
+ params.set_keys_only(ids_only)
+ if returned_fields or snippeted_fields or returned_expressions:
+ field_spec_pb = params.mutable_field_spec()
+ for field in returned_fields:
+ field_spec_pb.add_name(field)
+ for snippeted_field in snippeted_fields:
+ _CopyFieldExpressionToProtocolBuffer(
+ FieldExpression(
+ name=snippeted_field,
+ expression='snippet(' + _QuoteString(query)
+ + ', ' + snippeted_field + ')'),
+ field_spec_pb.add_expression())
+ for expression in returned_expressions:
+ _CopyFieldExpressionToProtocolBuffer(
+ expression, field_spec_pb.add_expression())
+
+ sort_options = _GetList(sort_options)
+ if sort_options:
+ for sort_option in sort_options:
+ if isinstance(sort_option, SortExpression):
+ sort_spec_pb = params.add_sort_spec()
+ _CopySortExpressionToProtocolBuffer(sort_option, sort_spec_pb)
+ else:
+ _CopySortOptionToScorerSpecProtocolBuffer(
+ sort_option, params.mutable_scorer_spec())
+
+
+class Query(object):
+ """Represents a request on the search service to query the index."""
+
+ def __init__(self, query_string, options=None):
+
+
+
+ """Initializer.
+
+ For example, the following code fragment requests a search for
+ documents where 'first' occurs in subject and 'good' occurs anywhere,
+ returning at most 20 documents, starting the search from 'cursor token',
+ returning another single document cursor for the results, sorting by
+ subject in descending order, returning the author, subject, and summary
+ fields as well as a snippeted field content.
+
+ results = index.search(Query(
+ query_string='subject:first good',
+ options=QueryOptions(
+ limit=20,
+ cursor=DocumentCursor(),
+ sort_options=[
+ SortOptions(expression='subject', default_value='')],
+ returned_fields=['author', 'subject', 'summary'],
+ snippeted_fields=['content'])))
+
+ In order to a DocumentCursor, you specify it in the QueryOptions.cursor
+ and extract the next request from results.cursor
+ it to the next request, to continue from the last found document, as shown
+ below:
+
+ results = index.search(
+ Query(query_string='subject:first good',
+ options=QueryOptions(cursor=results.cursor)))
+
+ Args:
+ query_string: The query to match against documents in the index. A query
+ is a boolean expression containing terms. For example, the query
+ 'job tag:"very important" sent:[TO 2011-02-28]'
+ finds documents with the term job in any field, that contain the
+ phrase "very important" in a tag field, and a sent date up to and
+ including 28th February, 2011. You can use combinations of
+ '(cat OR feline) food NOT dog'
+ to find documents which contain the term cat or feline as well as food,
+ but do not mention the term dog. A further example,
+ 'category:televisions brand:sony price:[300 TO 400}'
+ will return documents which have televisions in a category field, a
+ sony brand and a price field which is 300 (inclusive) to 400
+ (exclusive).
+ options: A QueryOptions describing post-processing of search results.
+ """
+ self._query_string = query_string
+ _CheckQuery(query_string)
+ self._options = options
+
+ @property
+ def query_string(self):
+ """Returns the query string to be applied to search service."""
+ return self._query_string
+
+ @property
+ def options(self):
+ """Returns QueryOptions defining post-processing on the search results."""
+ return self._options
+
+
+def _CopyQueryToProtocolBuffer(query, params):
+ """Copies Query object to params protobuf."""
+ if isinstance(query, unicode):
+ params.set_query(query.encode('utf-8'))
+ else:
+ params.set_query(query)
+
+
+def _CopyQueryObjectToProtocolBuffer(query, params):
+ _CopyQueryToProtocolBuffer(query.query_string, params)
+ options = QueryOptions()
+ if query.options is not None:
+ options = query.options
+ _CopyQueryOptionsObjectToProtocolBuffer(query.query_string, options, params)
+
+
class Index(object):
"""Represents an index allowing indexing, deleting and searching documents.
@@ -1444,20 +2249,16 @@
# Index the document.
try:
index.add(doc)
- except search.AddDocumentError, e:
- result = e.document_results[0]
- if result.code == DocumentOperationResult.TRANSIENT_ERROR:
- # possibly retry indexing result.document_id
except search.Error, e:
- # possibly log the failure
+ # possibly retry indexing or log error
# Query the index.
try:
- response = index.search('subject:first body:here')
+ results = index.search('subject:first body:here')
# Iterate through the search results.
- for result in response:
- doc = result.document
+ for scored_document in results:
+ print scored_document
except search.Error, e:
# possibly log the failure
@@ -1493,10 +2294,6 @@
RESPONSE_CURSOR, RESULT_CURSOR = ('RESPONSE_CURSOR', 'RESULT_CURSOR')
_CURSOR_TYPES = frozenset([RESPONSE_CURSOR, RESULT_CURSOR])
- _MAXIMUM_QUERY_LENGTH = 1000
- _MAXIMUM_LIMIT = 800
- _MAXIMUM_MATCHED_COUNT_ACCURACY = 10000
- _MAXIMUM_FIELDS_TO_RETURN = 100
def __init__(self, name, namespace=None,
consistency=PER_DOCUMENT_CONSISTENT):
@@ -1724,6 +2521,36 @@
results=results, matched_count=response.matched_count(),
cursor=response_cursor)
+ def _NewScoredDocumentFromPb(self, doc_pb, sort_scores, expressions, cursor):
+ """Constructs a Document from a document_pb.Document protocol buffer."""
+ lang = None
+ if doc_pb.has_language():
+ lang = doc_pb.language()
+ return ScoredDocument(
+ doc_id=doc_pb.id(), fields=_NewFieldsFromPb(doc_pb.field_list()),
+ language=lang, order_id=doc_pb.order_id(), sort_scores=sort_scores,
+ expressions=_NewFieldsFromPb(expressions), cursor=cursor)
+
+ def _NewSearchResults(self, response, cursor):
+ """Returns a SearchResults populated from a search_service response pb."""
+ results = []
+ for result_pb in response.result_list():
+ per_result_cursor = None
+ if result_pb.has_cursor():
+ per_result_cursor = DocumentCursor(cursor_string=result_pb.cursor(),
+ per_result=cursor.per_result)
+ results.append(
+ self._NewScoredDocumentFromPb(
+ result_pb.document(), result_pb.score_list(),
+ result_pb.expression_list(), per_result_cursor))
+ results_cursor = None
+ if response.has_cursor():
+ results_cursor = DocumentCursor(cursor_string=response.cursor(),
+ per_result=cursor.per_result)
+ return SearchResults(
+ results=results, number_found=response.matched_count(),
+ cursor=results_cursor)
+
def search(self, query, offset=0, limit=20, matched_count_accuracy=100,
ids_only=False, cursor=None, cursor_type=None, sort_specs=None,
returned_fields=None, snippeted_fields=None,
@@ -1737,76 +2564,44 @@
descending order, returning the author, subject, and summary fields as well
as a snippeted field content.
- index.search(query='subject:first good',
- limit=20,
- cursor=cursor_from_previous_response,
- cursor_type=Index.RESPONSE_CURSOR,
- sort_specs=[
- SortSpec(expression='subject', default_value='')],
- returned_fields=['author', 'subject', 'summary'],
- snippeted_fields=['content'])
+ results = index.search(
+ query=Query('subject:first good',
+ options=QueryOptions(limit=20,
+ cursor=DocumentCursor(),
+ sortOptions=SortOptions(
+ [SortExpression(expression='subject', default_value='')]),
+ returned_fields=['author', 'subject', 'summary'],
+ snippeted_fields=['content'])))
- The following code fragment shows how to use a response cursor
+ The following code fragment shows how to use a results cursor
- response = index.search(cursor=previous_cursor,
- cursor_type=Index.RESPONSE_CURSOR)
-
- previous_cursor = response.cursor
+ cursor = results.cursor
for result in response:
# process result
- The following code fragment shows how to use a result cursor
+ results = index.search(
+ Query('subject:first good', options=QueryOptions(cursor=cursor)))
- response = index.search(cursor=previous_cursor,
- cursor_type=Index.RESULT_CURSOR)
+ The following code fragment shows how to use a per_result cursor
- for result in response:
- previous_cursor = result.cursor
+ results = index.search(
+ query=Query('subject:first good',
+ options=QueryOptions(limit=20,
+ cursor=DocumentCursor(per_result=True),
+ ...)))
+
+ cursor = None
+ for result in results:
+ cursor = result.cursor
+
+ results = index.search(
+ Query('subject:first good', options=QueryOptions(cursor=cursor)))
Args:
- query: The query to match against documents in the index. A query is a
- boolean expression containing terms. For example, the query
- 'job tag:"very important" sent:[TO 2011-02-28]'
- finds documents with the term job in any field, that contain the
- phrase "very important" in a tag field, and a sent date up to and
- including 28th February, 2011. You can use combinations of
- '(cat OR feline) food NOT dog'
- to find documents which contain the term cat or feline as well as food,
- but do not mention the term dog. A further example,
- 'category:televisions brand:sony price:[300 TO 400}'
- will return documents which have televisions in a category field, a
- sony brand and a price field which is 300 (inclusive) to 400
- (exclusive).
- offset: The offset is number of documents to skip in results.
- limit: The limit on number of documents to return in results.
- matched_count_accuracy: The minimum accuracy requirement for
- SearchResponse.matched_count. If set, the matched_count will be
- accurate up to at least that number. For example, when set to 100,
- any SearchResponse with matched_count <= 100 is accurate. This option
- may add considerable latency/expense, especially when used with
- returned_fields.
- ids_only: Only return document ids, do not return any fields.
- cursor: A cursor returned in a previous set of search results to use
- as a starting point to retrieve the next set of results. This can get
- you better performance, and also improves the consistency of pagination
- through index updates.
- cursor_type: The type of cursor returned results will have, if any.
- Possible types are:
- RESPONSE_CURSOR: A single cursor will be returned in the response
- to continue from the end of the results.
- RESULT_CURSOR: One cursor will be returned with each search
- result, so you can continue after any result.
- sort_specs: An iterable of SortSpecs specifying a multi-dimensional sort
- over the search results.
- returned_fields: An iterable of names of fields to return in search
- results.
- snippeted_fields: An iterable of names of fields to snippet and return
- in search result expressions.
- returned_expressions: An iterable of FieldExpression to evaluate and
- return in search results.
+ query: The Query to match against documents in the index.
Returns:
- A SearchResponse containing a list of documents matched, number returned
+ A SearchResults containing a list of documents matched, number returned
and number matched by the query.
Raises:
@@ -1832,67 +2627,63 @@
params = request.mutable_params()
_CopyMetadataToProtocolBuffer(self, params.mutable_index_spec())
- self._CheckQuery(query)
- if isinstance(query, unicode):
- params.set_query(query.encode('utf-8'))
+ if isinstance(query, Query):
+ _CopyQueryObjectToProtocolBuffer(query, params)
else:
- params.set_query(query)
+ if cursor is not None:
+ warnings.warn('search.Index.search(cursor) is deprecated: '
+ 'use search.QueryOptions(cursor) instead.',
+ DeprecationWarning, stacklevel=2)
+ if cursor_type is not None:
+ warnings.warn('search.Index.search(cursor_type) is deprecated: '
+ 'use search.QueryOptions(cursor) instead.',
+ DeprecationWarning, stacklevel=2)
+ if offset is not None:
+ warnings.warn('search.Index.search(offset) is deprecated: '
+ 'use search.QueryOptions(offset) instead.',
+ DeprecationWarning, stacklevel=2)
+ if sort_specs is not None:
+ warnings.warn('search.Index.search(sort_specs) is deprecated: '
+ 'use search.QueryOptions(sort_specs) instead.',
+ DeprecationWarning, stacklevel=2)
+ if returned_fields is not None:
+ warnings.warn('search.Index.search(returned_fields) is deprecated: '
+ 'use search.QueryOptions(returned_fields) instead.',
+ DeprecationWarning, stacklevel=2)
+ if snippeted_fields is not None:
+ warnings.warn('search.Index.search(snippeted_fields) is deprecated: '
+ 'use search.QueryOptions(snippeted_fields) instead.',
+ DeprecationWarning, stacklevel=2)
+ if returned_expressions is not None:
+ warnings.warn('search.Index.search(returned_expressions) is deprecated:'
+ ' use search.QueryOptions(returned_expressions) instead.',
+ DeprecationWarning, stacklevel=2)
- self._CheckOffset(offset)
- if offset:
- params.set_offset(offset)
-
- self._CheckLimit(limit)
- params.set_limit(limit)
-
- self._CheckMatchedCountAccuracy(matched_count_accuracy)
- params.set_matched_count_accuracy(matched_count_accuracy)
-
- if ids_only:
- params.set_keys_only(ids_only)
-
- self._CheckCursor(cursor)
- if cursor:
- params.set_cursor(cursor)
-
- self._CheckCursorType(cursor_type)
- params.set_cursor_type(_CURSOR_TYPE_PB_MAP.get(cursor_type))
-
- returned_fields = _ConvertToList(returned_fields)
- _CheckFieldNames(returned_fields)
- snippeted_fields = _ConvertToList(snippeted_fields)
- _CheckFieldNames(snippeted_fields)
- returned_expressions = _ConvertToList(returned_expressions)
- number_expressions = (len(returned_expressions) + len(snippeted_fields) +
- len(returned_fields))
- if number_expressions > self._MAXIMUM_FIELDS_TO_RETURN:
- raise ValueError(
- 'too many fields, snippets or expressions to return %d > maximum %d'
- % (number_expressions, self._MAXIMUM_FIELDS_TO_RETURN))
- if returned_fields or snippeted_fields or returned_expressions:
- field_spec_pb = params.mutable_field_spec()
- for field in returned_fields:
- field_spec_pb.add_name(field)
- for snippeted_field in snippeted_fields:
- _CopyFieldExpressionToProtocolBuffer(
- FieldExpression(
- name=snippeted_field,
- expression='snippet(' + _QuoteString(query)
- + ', ' + snippeted_field + ')'),
- field_spec_pb.add_expression())
+ _CheckQuery(query)
+ _CheckOffset(offset)
+ _CheckLimit(limit)
+ _CheckCursor(cursor)
+ _CheckNumberFoundAccuracy(matched_count_accuracy)
+ self._CheckCursorType(cursor_type)
+ returned_fields = _ConvertToList(returned_fields)
+ _CheckFieldNames(returned_fields)
+ snippeted_fields = _ConvertToList(snippeted_fields)
+ _CheckFieldNames(snippeted_fields)
+ returned_expressions = _ConvertToList(returned_expressions)
for expression in returned_expressions:
- _CopyFieldExpressionToProtocolBuffer(
- expression, field_spec_pb.add_expression())
+ _CheckFieldName(expression.name)
+ _CheckExpression(expression.expression)
+ _CheckNumberOfFields(returned_expressions, snippeted_fields,
+ returned_fields)
+ sort_specs = _GetList(sort_specs)
- sort_specs = _GetList(sort_specs)
- if sort_specs:
- for sort_spec in sort_specs:
- if sort_spec.sort_type == SortSpec.CUSTOM:
- sort_spec_pb = params.add_sort_spec()
- _CopySortSpecToProtocolBuffer(sort_spec, sort_spec_pb)
- else:
- _CopySortSpecToScorerSpecProtocolBuffer(
- sort_spec, params.mutable_scorer_spec())
+ _CopyQueryToProtocolBuffer(query, params)
+
+ _CopyQueryOptionsToProtocolBuffer(
+ query, offset, limit, matched_count_accuracy, cursor,
+ _CURSOR_TYPE_PB_MAP.get(cursor_type),
+ ids_only, returned_fields, snippeted_fields, returned_expressions,
+ sort_specs, params)
response = search_service_pb.SearchResponse()
@@ -1902,48 +2693,13 @@
raise _ToSearchError(e)
_CheckStatus(response.status())
- return self._NewSearchResponse(response)
-
- def _CheckQuery(self, query):
- """Checks a query is a valid query string."""
- _ValidateString(query, 'query', Index._MAXIMUM_QUERY_LENGTH,
- empty_ok=True)
- if query is None:
- raise ValueError('query must not be null')
- if query.strip():
- if isinstance(query, unicode):
- query_parser.Parse(query)
- else:
- query_parser.Parse(unicode(query, 'utf-8'))
- return query
-
- def _CheckLimit(self, limit):
- """Checks the limit of documents to return is an integer within range."""
- return _CheckInteger(
- limit, 'limit', zero_ok=False, upper_bound=self._MAXIMUM_LIMIT)
-
- def _CheckOffset(self, offset):
- """Checks the offset in document list is an integer within range."""
- return _CheckInteger(
- offset, 'offset', zero_ok=True, upper_bound=self._MAXIMUM_LIMIT)
-
- def _CheckMatchedCountAccuracy(self, matched_count_accuracy):
- """Checks the accuracy is an integer within range."""
- return _CheckInteger(
- matched_count_accuracy, 'matched_count_accuracy',
- zero_ok=False, upper_bound=self._MAXIMUM_MATCHED_COUNT_ACCURACY)
-
- def _CheckCursor(self, cursor):
- """Checks the cursor if specified is a string which is not too long."""
- return _ValidateString(cursor, 'cursor', _MAXIMUM_CURSOR_LENGTH,
- empty_ok=True)
-
- def _CheckCursorType(self, cursor_type):
- """Checks the cursor_type is one specified in _CURSOR_TYPES or None."""
- if cursor_type is None:
- return None
- return _CheckEnum(cursor_type, 'cursor_type',
- values=Index._CURSOR_TYPES)
+ if isinstance(query, Query):
+ cursor = None
+ if query.options:
+ cursor = query.options.cursor
+ return self._NewSearchResults(response, cursor)
+ else:
+ return self._NewSearchResponse(response)
def _NewListDocumentsResponse(self, response):
"""Returns a ListDocumentsResponse from the list_documents response pb."""
@@ -2000,6 +2756,12 @@
_CheckStatus(response.status())
return self._NewListDocumentsResponse(response)
+ def _CheckCursorType(self, cursor_type):
+ """Checks the cursor_type is one specified in _CURSOR_TYPES or None."""
+ if cursor_type is None:
+ return None
+ return _CheckEnum(cursor_type, 'cursor_type', values=Index._CURSOR_TYPES)
+
_CURSOR_TYPE_PB_MAP = {
None: search_service_pb.SearchParams.NONE,
@@ -2027,7 +2789,6 @@
spec_pb.set_namespace(index.namespace)
spec_pb.set_consistency(_CONSISTENCY_MODES_TO_PB_MAP.get(index.consistency))
-
_FIELD_TYPE_MAP = {
document_pb.FieldValue.TEXT: Field.TEXT,
document_pb.FieldValue.HTML: Field.HTML,
diff --git a/google/appengine/datastore/datastore_rpc.py b/google/appengine/datastore/datastore_rpc.py
index 346777c..b5bfc86 100755
--- a/google/appengine/datastore/datastore_rpc.py
+++ b/google/appengine/datastore/datastore_rpc.py
@@ -239,10 +239,19 @@
return type.__new__(metaclass, classname, bases, classDict)
- classDict['__slots__'] = ['_values']
+
+
+ if object in bases:
+ classDict['__slots__'] = ['_values']
+ else:
+ classDict['__slots__'] = []
cls = type.__new__(metaclass, classname, bases, classDict)
if object not in bases:
- cls._options = cls._options.copy()
+ options = {}
+ for c in reversed(cls.__mro__):
+ if '_options' in c.__dict__:
+ options.update(c.__dict__['_options'])
+ cls._options = options
for option, value in cls.__dict__.iteritems():
if isinstance(value, ConfigOption):
if cls._options.has_key(option):
@@ -1657,8 +1666,7 @@
(app,))
req = datastore_pb.BeginTransactionRequest()
req.set_app(app)
- if (TransactionOptions.xg(config, self.__config) or
- TransactionOptions.allow_multiple_entity_groups(config, self.__config)):
+ if (TransactionOptions.xg(config, self.__config)):
req.set_allow_multiple_eg(True)
resp = datastore_pb.Transaction()
rpc = self.make_rpc_call(config, 'BeginTransaction', req, resp,
@@ -1785,6 +1793,35 @@
class TransactionOptions(Configuration):
"""An immutable class that contains options for a transaction."""
+ NESTED = 1
+ """Create a nested transaction under an existing one."""
+
+ MANDATORY = 2
+ """Always propagate an exsiting transaction, throw an exception if there is no
+ exsiting transaction."""
+
+ ALLOWED = 3
+ """If there is an existing transaction propagate it."""
+
+ INDEPENDENT = 4
+ """Always use a new transaction, pausing any existing transactions."""
+
+ _PROPAGATION = frozenset((NESTED, MANDATORY, ALLOWED, INDEPENDENT))
+
+ @ConfigOption
+ def propagation(value):
+ """How existing transactions should be handled.
+
+ One of NESTED, MANDATORY, ALLOWED, INDEPENDENT. The interpertation of
+ these types is up to higher level run-in-transaction implementations.
+
+ Raises: datastore_errors.BadArgumentError if value is not reconized.
+ """
+ if value not in TransactionOptions._PROPAGATION:
+ raise datastore_errors.BadArgumentError('Unknown propagation value (%r)' %
+ (value,))
+ return value
+
@ConfigOption
def xg(value):
"""Whether to allow cross-group transactions.
@@ -1797,18 +1834,12 @@
return value
@ConfigOption
- def allow_multiple_entity_groups(value):
- """Deprecated, will be removed in 1.5.6. Use xg instead."""
- if not isinstance(value, bool):
- raise datastore_errors.BadArgumentError(
- 'allow_multiple_entity_groups argument should be bool (%r)' %
- (value,))
- return value
-
- @ConfigOption
def retries(value):
"""How many retries to attempt on the transaction.
+ The exact retry logic is implemented in higher level run-in-transaction
+ implementations.
+
Raises: datastore_errors.BadArgumentError if value is not an integer or
is not greater than zero.
"""
diff --git a/google/appengine/datastore/datastore_stub_index.py b/google/appengine/datastore/datastore_stub_index.py
index f41f6a7..b52d1a8 100644
--- a/google/appengine/datastore/datastore_stub_index.py
+++ b/google/appengine/datastore/datastore_stub_index.py
@@ -152,6 +152,10 @@
nothing to update).
"""
+
+
+
+
index_yaml_file = os.path.join(self.root_path, 'index.yaml')
@@ -188,7 +192,7 @@
- fh = open(index_yaml_file, 'rU')
+ fh = openfile(index_yaml_file, 'rU')
except IOError:
index_yaml_data = None
else:
diff --git a/google/appengine/datastore/datastore_stub_util.py b/google/appengine/datastore/datastore_stub_util.py
index 3d089a9..32d4c72 100644
--- a/google/appengine/datastore/datastore_stub_util.py
+++ b/google/appengine/datastore/datastore_stub_util.py
@@ -1999,12 +1999,6 @@
self._require_indexes = require_indexes
self._pseudo_kinds = {}
- def __del__(self):
-
-
- self.Flush()
- self.Write()
-
def Clear(self):
"""Clears out all stored values."""
@@ -2586,12 +2580,15 @@
allocate_ids_response.set_start(start)
allocate_ids_response.set_end(end)
- def _SetupIndexes(self):
+ def _SetupIndexes(self, _open=open):
"""Ensure that the set of existing composite indexes matches index.yaml.
Note: this is similar to the algorithm used by the admin console for
the same purpose.
"""
+
+
+
if not self._root_path:
return
index_yaml_file = os.path.join(self._root_path, 'index.yaml')
@@ -2602,7 +2599,7 @@
else:
try:
index_yaml_mtime = os.path.getmtime(index_yaml_file)
- fh = open(index_yaml_file, 'r')
+ fh = _open(index_yaml_file, 'r')
except (OSError, IOError):
index_yaml_data = None
else:
diff --git a/google/appengine/ext/admin/__init__.py b/google/appengine/ext/admin/__init__.py
index 38dd865..f593687 100755
--- a/google/appengine/ext/admin/__init__.py
+++ b/google/appengine/ext/admin/__init__.py
@@ -84,6 +84,9 @@
_DEBUG = True
+QUEUE_MODE = taskqueue_service_pb.TaskQueueMode
+
+
_UsecToSec = taskqueue_stub._UsecToSec
_FormatEta = taskqueue_stub._FormatEta
_EtaDelta = taskqueue_stub._EtaDelta
@@ -427,6 +430,7 @@
queues = []
for queue_proto in response.queue_list():
queue = {'name': queue_proto.queue_name(),
+ 'mode': queue_proto.mode(),
'rate': queue_proto.user_specified_rate(),
'bucket_size': queue_proto.bucket_capacity()}
queues.append(queue)
@@ -520,6 +524,27 @@
self._make_sync_call('PurgeQueue', request)
+class QueueBatch(object):
+ """Collection of push queues or pull queues."""
+
+ def __init__(self, title, run_manually, rate_limited, contents):
+ self.title = title
+ self.run_manually = run_manually
+ self.rate_limited = rate_limited
+ self.contents = contents
+
+ def __eq__(self, other):
+ if type(self) is not type(other):
+ return NotImplemented
+ return (self.title == other.title and
+ self.run_manually == other.run_manually and
+ self.rate_limited == other.rate_limited and
+ self.contents == other.contents)
+
+ def __iter__(self):
+ return self.contents.__iter__()
+
+
class QueuesPageHandler(BaseRequestHandler):
"""Shows information about configured (and default) task queues."""
PATH = '/queues'
@@ -530,10 +555,26 @@
def get(self):
"""Shows template displaying the configured task queues."""
+
+ def is_push_queue(queue):
+ return queue['mode'] == QUEUE_MODE.PUSH
+
+ def is_pull_queue(queue):
+ return queue['mode'] == QUEUE_MODE.PULL
+
now = datetime.datetime.utcnow()
values = {}
try:
- values['queues'] = self.helper.get_queues(now)
+ queues = self.helper.get_queues(now)
+ push_queues = QueueBatch('Push Queues',
+ True,
+ True,
+ filter(is_push_queue, queues))
+ pull_queues = QueueBatch('Pull Queues',
+ False,
+ False,
+ filter(is_pull_queue, queues))
+ values['queueBatches'] = [push_queues, pull_queues]
except apiproxy_errors.ApplicationError:
@@ -684,16 +725,24 @@
pages[-1]['has_gap'] = True
tasks = tasks[:self.per_page]
+
+ def is_this_push_queue(queue):
+ return (queue['name'] == self.queue_name and
+ queue['mode'] == QUEUE_MODE.PUSH)
+
values = {
- 'queue': self.queue_name,
- 'per_page': self.per_page,
- 'tasks': tasks,
- 'prev_page': self.prev_page,
- 'next_page': self.next_page,
- 'this_page': self.this_page,
- 'pages': pages,
- 'page_no': self.page_no,
+ 'queue': self.queue_name,
+ 'per_page': self.per_page,
+ 'tasks': tasks,
+ 'prev_page': self.prev_page,
+ 'next_page': self.next_page,
+ 'this_page': self.this_page,
+ 'pages': pages,
+ 'page_no': self.page_no,
}
+ if any(filter(is_this_push_queue, self.helper.get_queues(now))):
+ values['is_push_queue'] = 'true'
+
self.generate('tasks.html', values)
@xsrf_required
diff --git a/google/appengine/ext/admin/datastore_stats_generator.py b/google/appengine/ext/admin/datastore_stats_generator.py
index fbd9796..3dc3253 100644
--- a/google/appengine/ext/admin/datastore_stats_generator.py
+++ b/google/appengine/ext/admin/datastore_stats_generator.py
@@ -32,6 +32,7 @@
import logging
from google.appengine.api import datastore
+from google.appengine.api import datastore_admin
from google.appengine.api import datastore_types
from google.appengine.api import users
from google.appengine.ext.db import stats
@@ -44,27 +45,29 @@
+
+
_PROPERTY_TYPE_TO_DSS_NAME = {
- unicode: 'String',
- bool: 'Boolean',
- long: 'Integer',
- type(None): 'NULL',
- float: 'Float',
- datastore_types.Key: 'Key',
- datastore_types.Blob: 'Blob',
- datastore_types.ByteString: 'ShortBlob',
- datastore_types.Text: 'Text',
- users.User: 'User',
- datastore_types.Category: 'Category',
- datastore_types.Link: 'Link',
- datastore_types.Email: 'Email',
- datetime.datetime: 'Date/Time',
- datastore_types.GeoPt: 'GeoPt',
- datastore_types.IM: 'IM',
- datastore_types.PhoneNumber: 'PhoneNumber',
- datastore_types.PostalAddress: 'PostalAddress',
- datastore_types.Rating: 'Rating',
- datastore_types.BlobKey: 'BlobKey',
+ unicode: ('String', 'STRING'),
+ bool: ('Boolean', 'BOOLEAN'),
+ long: ('Integer', 'INT64'),
+ type(None): ('NULL', 'NULL'),
+ float: ('Float', 'DOUBLE'),
+ datastore_types.Key: ('Key', 'REFERENCE'),
+ datastore_types.Blob: ('Blob', 'STRING'),
+ datastore_types.ByteString: ('ShortBlob', 'STRING'),
+ datastore_types.Text: ('Text', 'STRING'),
+ users.User: ('User', 'USER'),
+ datastore_types.Category: ('Category', 'STRING'),
+ datastore_types.Link: ('Link', 'STRING'),
+ datastore_types.Email: ('Email', 'STRING'),
+ datetime.datetime: ('Date/Time', 'INT64'),
+ datastore_types.GeoPt: ('GeoPt', 'POINT'),
+ datastore_types.IM: ('IM', 'STRING'),
+ datastore_types.PhoneNumber: ('PhoneNumber', 'STRING'),
+ datastore_types.PostalAddress: ('PostalAddress', 'STRING'),
+ datastore_types.Rating: ('Rating', 'INT64'),
+ datastore_types.BlobKey: ('BlobKey', 'STRING'),
}
@@ -120,23 +123,70 @@
stat_kind = stats._DATASTORE_STATS_CLASSES_BY_KIND[entity.key().kind()]
self.old_stat_keys.append(entity.key())
- self.__AggregateTotal(proto_size, namespace, stat_kind)
+ self.__AggregateTotal(proto_size, entity.key(), proto, namespace,
+ stat_kind)
else:
self.__ProcessUserEntity(proto_size, entity.key(), proto, namespace)
+ def __GetPropertyIndexStat(self, namespace, kind_name,
+ entity_key_size, prop):
+ """Return the size and count of indexes for a property of an EntityProto."""
+
+ property_index_size = (len(self.app_id) + len(kind_name) +
+ len(prop.value().SerializeToString()) +
+ len(namespace) + entity_key_size)
+
+ return (property_index_size, 2)
+
+ def __GetTypeIndexStat(self, namespace, kind_name, entity_key_size):
+ """Return the size and count of indexes by type of an EntityProto."""
+ type_index_size = (len(self.app_id) + len(kind_name) + entity_key_size
+ + len(namespace))
+ return (type_index_size, 1)
+
def __ProcessUserEntity(self, proto_size, key, proto, namespace):
"""Increment datastore stats for a non stats record."""
- self.__AggregateTotal(proto_size, namespace, None)
+ self.__AggregateTotal(proto_size, key, proto, namespace, None)
kind_name = key.kind()
+ entity_key_size = (len(proto.key().app()) + len(namespace) +
+ len(proto.key().path().SerializeToString()) +
+ len(proto.entity_group().SerializeToString()))
+
+ self.__AggregateCompositeIndices(proto, namespace, kind_name,
+ entity_key_size)
+
+ type_index_size, type_index_count = self.__GetTypeIndexStat(namespace,
+ kind_name,
+ entity_key_size)
+ property_index_count = 0
+ property_index_size = 0
+ for prop_list in (proto.property_list(), proto.raw_property_list()):
+ for prop in prop_list:
+ index_size, index_count = self.__GetPropertyIndexStat(namespace,
+ kind_name,
+ entity_key_size,
+ prop)
+ property_index_size += index_size
+ property_index_count += index_count
+
+ builtin_index_size = type_index_size + property_index_size
+ builtin_index_count = type_index_count + property_index_count
+
self.__Increment(self.whole_app_stats, 1,
(stats.KindStat, kind_name, ''),
- proto_size, kind_name=kind_name)
+ proto_size,
+ builtin_index_count=builtin_index_count,
+ builtin_index_size=builtin_index_size,
+ kind_name=kind_name)
self.__Increment(self.namespace_stats, 1,
(stats.NamespaceKindStat, kind_name, namespace),
- proto_size, kind_name=kind_name)
+ proto_size,
+ builtin_index_count=builtin_index_count,
+ builtin_index_size=builtin_index_size,
+ kind_name=kind_name)
@@ -149,80 +199,300 @@
self.__Increment(self.whole_app_stats, 1,
(whole_app_model, kind_name, ''),
- proto_size, kind_name=kind_name)
+ proto_size,
+ kind_name=kind_name)
self.__Increment(self.namespace_stats, 1,
(namespace_model, kind_name, namespace),
- proto_size, kind_name=kind_name)
+ proto_size,
+ kind_name=kind_name)
self.__ProcessProperties(
kind_name,
namespace,
+ entity_key_size,
(proto.property_list(), proto.raw_property_list()))
- def __ProcessProperties(self, kind_name, namespace, prop_lists):
+ def __ProcessProperties(self, kind_name, namespace, entity_key_size,
+ prop_lists):
for prop_list in prop_lists:
for prop in prop_list:
try:
value = datastore_types.FromPropertyPb(prop)
- self.__AggregateProperty(kind_name, namespace, prop, value)
+ self.__AggregateProperty(kind_name, namespace, entity_key_size,
+ prop, value)
except (AssertionError, AttributeError, TypeError, ValueError), e:
logging.error('Cannot process property %r, exception %s' %
(prop, e))
- def __AggregateProperty(self, kind_name, namespace, prop, value):
+ def __AggregateProperty(self, kind_name, namespace, entity_key_size,
+ prop, value):
property_name = prop.name()
- property_type = _PROPERTY_TYPE_TO_DSS_NAME[type(value)]
+ property_type = _PROPERTY_TYPE_TO_DSS_NAME[type(value)][0]
+ index_property_type = _PROPERTY_TYPE_TO_DSS_NAME[type(value)][1]
size = len(prop.SerializeToString())
+ index_size, index_count = self.__GetPropertyIndexStat(namespace, kind_name,
+ entity_key_size, prop)
+
+
+
+
+
self.__Increment(self.whole_app_stats, 1,
(stats.PropertyTypeStat, property_type, ''),
- size, property_type=property_type)
+ size,
+ builtin_index_count=0,
+ builtin_index_size=0,
+ property_type=property_type)
+
+ self.__Increment(self.whole_app_stats, 0,
+ (stats.PropertyTypeStat, index_property_type, ''),
+ 0,
+ builtin_index_count=index_count,
+ builtin_index_size=index_size,
+ property_type=index_property_type)
self.__Increment(self.namespace_stats, 1,
(stats.NamespacePropertyTypeStat,
property_type, namespace),
- size, property_type=property_type)
+ size,
+ builtin_index_count=0,
+ builtin_index_size=0,
+ property_type=property_type)
+
+ self.__Increment(self.namespace_stats, 0,
+ (stats.NamespacePropertyTypeStat,
+ index_property_type, namespace),
+ 0,
+ builtin_index_count=index_count,
+ builtin_index_size=index_size,
+ property_type=index_property_type)
self.__Increment(self.whole_app_stats, 1,
(stats.KindPropertyTypeStat,
property_type + '_' + kind_name, ''),
- size, property_type=property_type, kind_name=kind_name)
+ size,
+ builtin_index_count=0,
+ builtin_index_size=0,
+ property_type=property_type, kind_name=kind_name)
+
+ self.__Increment(self.whole_app_stats, 0,
+ (stats.KindPropertyTypeStat,
+ index_property_type + '_' + kind_name, ''),
+ 0,
+ builtin_index_count=index_count,
+ builtin_index_size=index_size,
+ property_type=index_property_type, kind_name=kind_name)
self.__Increment(self.namespace_stats, 1,
(stats.NamespaceKindPropertyTypeStat,
property_type + '_' + kind_name, namespace),
- size, property_type=property_type, kind_name=kind_name)
+ size,
+ builtin_index_count=0,
+ builtin_index_size=0,
+ property_type=property_type, kind_name=kind_name)
+
+ self.__Increment(self.namespace_stats, 0,
+ (stats.NamespaceKindPropertyTypeStat,
+ index_property_type + '_' + kind_name, namespace),
+ 0,
+ builtin_index_count=index_count,
+ builtin_index_size=index_size,
+ property_type=index_property_type, kind_name=kind_name)
self.__Increment(self.whole_app_stats, 1,
(stats.KindPropertyNameStat,
property_name + '_' + kind_name, ''),
- size, property_name=property_name, kind_name=kind_name)
+ size,
+ builtin_index_count=index_count,
+ builtin_index_size=index_size,
+ property_name=property_name, kind_name=kind_name)
self.__Increment(self.namespace_stats, 1,
(stats.NamespaceKindPropertyNameStat,
property_name + '_' + kind_name, namespace),
- size, property_name=property_name, kind_name=kind_name)
+ size,
+ builtin_index_count=index_count,
+ builtin_index_size=index_size,
+ property_name=property_name, kind_name=kind_name)
self.__Increment(self.whole_app_stats, 1,
(stats.KindPropertyNamePropertyTypeStat,
property_type + '_' + property_name + '_' + kind_name,
- ''), size, property_type=property_type,
+ ''), size,
+ builtin_index_count=0,
+ builtin_index_size=0,
+ property_type=property_type,
+ property_name=property_name, kind_name=kind_name)
+
+ self.__Increment(self.whole_app_stats, 0,
+ (stats.KindPropertyNamePropertyTypeStat,
+ index_property_type + '_' + property_name + '_' +
+ kind_name,
+ ''), 0,
+ builtin_index_count=index_count,
+ builtin_index_size=index_size,
+ property_type=index_property_type,
property_name=property_name, kind_name=kind_name)
self.__Increment(self.namespace_stats, 1,
(stats.NamespaceKindPropertyNamePropertyTypeStat,
property_type + '_' + property_name + '_' + kind_name,
namespace),
- size, property_type=property_type,
+ size,
+ builtin_index_count=0,
+ builtin_index_size=0,
+ property_type=property_type,
property_name=property_name, kind_name=kind_name)
- def __AggregateTotal(self, size, namespace, stat_kind):
+ self.__Increment(self.namespace_stats, 0,
+ (stats.NamespaceKindPropertyNamePropertyTypeStat,
+ index_property_type + '_' + property_name + '_' +
+ kind_name,
+ namespace),
+ 0,
+ builtin_index_count=index_count,
+ builtin_index_size=index_size,
+ property_type=index_property_type,
+ property_name=property_name, kind_name=kind_name)
+
+ def __GetCompositeIndexStat(self, definition, proto, namespace, kind_name,
+ entity_key_size):
+ """Get statistics of composite index for a index definition of an entity."""
+
+
+
+
+
+
+ property_list = proto.property_list()
+ property_count = []
+ property_size = []
+ index_count = 1
+ for indexed_prop in definition.property_list():
+ name = indexed_prop.name()
+ count = 0
+ prop_size = 0
+ for prop in property_list:
+ if prop.name() == name:
+ count += 1
+ prop_size += len(prop.SerializeToString())
+
+ property_count.append(count)
+ property_size.append(prop_size)
+ index_count *= count
+
+ if index_count == 0:
+ return (0, 0)
+
+ index_only_size = 0
+ for i in range(len(property_size)):
+ index_only_size += property_size[i] * (index_count / property_count[i])
+
+
+
+
+
+ index_size = (index_count * (entity_key_size + len(kind_name) +
+ len(self.app_id) + len(namespace)) +
+ index_only_size * 2)
+
+ return (index_size, index_count)
+
+ def __AggregateCompositeIndices(self, proto, namespace, kind_name,
+ entity_key_size):
+ """Aggregate statistics of composite indexes for an entity."""
+ composite_indices = datastore_admin.GetIndices(self.app_id)
+ for index in composite_indices:
+ definition = index.definition()
+ if kind_name != definition.entity_type():
+ continue
+
+ index_size, index_count = self.__GetCompositeIndexStat(definition, proto,
+ namespace,
+ kind_name,
+ entity_key_size)
+
+ if index_count == 0:
+ continue
+
+
+ name_id = namespace
+ if not name_id:
+ name_id = 1
+
+
+ self.__Increment(self.whole_app_stats, 0, _GLOBAL_KEY, 0,
+ composite_index_count=index_count,
+ composite_index_size=index_size)
+
+ self.__Increment(self.whole_app_stats, 0,
+ (stats.NamespaceStat, name_id, ''), 0,
+ composite_index_count=index_count,
+ composite_index_size=index_size,
+ subject_namespace=namespace)
+
+ self.__Increment(self.namespace_stats, 0,
+ (stats.NamespaceGlobalStat, 'total_entity_usage',
+ namespace), 0,
+ composite_index_count=index_count,
+ composite_index_size=index_size)
+
+
+ self.__Increment(self.whole_app_stats, 0,
+ (stats.KindStat, kind_name, ''), 0,
+ composite_index_count=index_count,
+ composite_index_size=index_size,
+ kind_name=kind_name)
+
+ self.__Increment(self.namespace_stats, 0,
+ (stats.NamespaceKindStat, kind_name, namespace), 0,
+ composite_index_count=index_count,
+ composite_index_size=index_size,
+ kind_name=kind_name)
+
+
+ index_id = index.id()
+ self.__Increment(self.whole_app_stats, index_count,
+ (stats.KindCompositeIndexStat,
+ kind_name + '_%s' % index_id, ''), index_size,
+ kind_name=kind_name, index_id=index_id)
+
+ self.__Increment(self.namespace_stats, index_count,
+ (stats.NamespaceKindCompositeIndexStat,
+ kind_name + '_%s' % index_id, namespace), index_size,
+ kind_name=kind_name, index_id=index_id)
+
+ def __AggregateTotal(self, size, key, proto, namespace, stat_kind):
"""Aggregate total datastore stats."""
+ kind_name = key.kind()
+
+ entity_key_size = (len(proto.key().app()) +
+ len(proto.key().path().SerializeToString()) +
+ len(proto.entity_group().SerializeToString()))
+
+ type_index_size, type_index_count = self.__GetTypeIndexStat(namespace,
+ kind_name,
+ entity_key_size)
+ property_index_count = 0
+ property_index_size = 0
+ for prop_list in (proto.property_list(), proto.raw_property_list()):
+ for prop in prop_list:
+ index_size, index_count = self.__GetPropertyIndexStat(namespace,
+ kind_name,
+ entity_key_size,
+ prop)
+ property_index_size += index_size
+ property_index_count += index_count
+
+ builtin_index_size = type_index_size + property_index_size
+ builtin_index_count = type_index_count + property_index_count
+
if stat_kind == stats.GlobalStat:
count = 0
@@ -230,7 +500,9 @@
count = 1
- self.__Increment(self.whole_app_stats, count, _GLOBAL_KEY, size)
+ self.__Increment(self.whole_app_stats, count, _GLOBAL_KEY, size,
+ builtin_index_count=builtin_index_count,
+ builtin_index_size=builtin_index_size)
name_id = namespace
@@ -243,7 +515,10 @@
self.__Increment(self.whole_app_stats, count,
(stats.NamespaceStat, name_id, ''),
- size, subject_namespace=namespace)
+ size,
+ builtin_index_count=builtin_index_count,
+ builtin_index_size=builtin_index_size,
+ subject_namespace=namespace)
if stat_kind == stats.NamespaceGlobalStat:
count = 0
@@ -251,9 +526,13 @@
self.__Increment(
self.namespace_stats, count,
- (stats.NamespaceGlobalStat, 'total_entity_usage', namespace), size)
+ (stats.NamespaceGlobalStat, 'total_entity_usage', namespace), size,
+ builtin_index_count=builtin_index_count,
+ builtin_index_size=builtin_index_size)
- def __Increment(self, stats_dict, count, stat_key, size, **kwds):
+ def __Increment(self, stats_dict, count, stat_key, size,
+ builtin_index_count=0, builtin_index_size=0,
+ composite_index_count=0, composite_index_size=0, **kwds):
"""Increment stats for a particular kind.
Args:
@@ -263,6 +542,10 @@
count: The amount to increment the datastore stat by.
stat_key: A tuple of (db.Model of the stat, key value, namespace).
size: The "bytes" to increment the size by.
+ builtin_index_count: The bytes of builtin index to add in to a stat.
+ builtin_index_size: The count of builtin index to add in to a stat.
+ composite_index_count: The bytes of composite index to add in to a stat.
+ composite_index_size: The count of composite index to add in to a stat.
kwds: Name value pairs that are set on the created entities.
"""
@@ -277,12 +560,28 @@
for field, value in kwds.iteritems():
setattr(stat_model, field, value)
stat_model.count = count
- stat_model.bytes = size
+ if size:
+ stat_model.entity_bytes = size
+ if builtin_index_size:
+ stat_model.builtin_index_bytes = builtin_index_size
+ stat_model.builtin_index_count = builtin_index_count
+ if composite_index_size:
+ stat_model.composite_index_bytes = composite_index_size
+ stat_model.composite_index_count = composite_index_count
+ stat_model.bytes = size + builtin_index_size + composite_index_size
stat_model.timestamp = self.timestamp
else:
stat_model = stats_dict[stat_key]
stat_model.count += count
- stat_model.bytes += size
+ if size:
+ stat_model.entity_bytes += size
+ if builtin_index_size:
+ stat_model.builtin_index_bytes += builtin_index_size
+ stat_model.builtin_index_count += builtin_index_count
+ if composite_index_size:
+ stat_model.composite_index_bytes += composite_index_size
+ stat_model.composite_index_count += composite_index_count
+ stat_model.bytes += size + builtin_index_size + composite_index_size
def __Finalize(self):
"""Finishes processing, deletes all old stats and writes new ones."""
@@ -293,7 +592,8 @@
self.written = 0
for stat in self.whole_app_stats.itervalues():
- if stat.count:
+ if stat.count or not (isinstance(stat, stats.GlobalStat) or
+ isinstance(stat, stats.NamespaceStat)):
stat.put()
self.written += 1
@@ -301,7 +601,7 @@
if self.found_non_empty_namespace:
for stat in self.namespace_stats.itervalues():
- if stat.count:
+ if stat.count or not isinstance(stat, stats.NamespaceGlobalStat):
stat.put()
self.written += 1
@@ -314,14 +614,26 @@
def Report(self):
"""Produce a small report about the result."""
stat = self.whole_app_stats.get(_GLOBAL_KEY, None)
- total_size = 0
- total_count = 0
+ entity_size = 0
+ entity_count = 0
+ builtin_index_size = 0
+ builtin_index_count = 0
+ composite_index_size = 0
+ composite_index_count = 0
if stat:
- total_size = stat.bytes
- total_count = stat.count
+ entity_size = stat.entity_bytes
+ entity_count = stat.count
+ builtin_index_size = stat.builtin_index_bytes
+ builtin_index_count = stat.builtin_index_count
+ composite_index_size = stat.composite_index_bytes
+ composite_index_count = stat.composite_index_count
- if not total_count:
- total_count = 1
+ if not entity_count:
+ entity_count = 1
- return ('Scanned %d entities of total %d bytes. Inserted %d new records.'
- % (total_count, total_size, self.written))
+ return ('Scanned %d entities of total %d bytes, %d index entries of total '
+ '%d bytes and %d composite index entries of total %d bytes. '
+ 'Inserted %d new records.'
+ % (entity_count, entity_size, builtin_index_count,
+ builtin_index_size, composite_index_count, composite_index_size,
+ self.written))
diff --git a/google/appengine/ext/admin/templates/queues.html b/google/appengine/ext/admin/templates/queues.html
index 1265ec5..c80d931 100644
--- a/google/appengine/ext/admin/templates/queues.html
+++ b/google/appengine/ext/admin/templates/queues.html
@@ -14,62 +14,71 @@
{% block body %}
<h3>Task Queues</h3>
-{% if queues %}
- <p>
- Select a queue to run tasks manually.
- </p>
+{% for queueBatch in queueBatches %}
+ {% if queueBatch %}
+ <p>
+ {% if queueBatch.run_manually %}
+ Select a push queue to run tasks manually.
+ {% endif %}
+ </p>
- <table id="ah-queues" class="ae-table ae-table-striped">
- <thead>
- <tr>
- <th>Queue Name</th>
- <th>Maximum Rate</th>
- <th>Bucket Size</th>
- <th>Oldest Task (UTC)</th>
- <th>Tasks in Queue</th>
- <th></th>
- </tr>
- </thead>
- <tbody>
- {% for queue in queues %}
- <tr class="{% cycle ae-odd,ae-even %}">
- <td valign="top">
- <a href="{{ tasks_path }}?queue={{ queue.name|escape }}">
- {{ queue.name|escape }}</a>
- </td>
- <td valign="top">
- {{ queue.rate|escape }}
- </td>
- <td valign="top">
- {{ queue.bucket_size|escape }}
- </td>
- <td valign="top">
- {% if queue.oldest_task %}
- {{ queue.oldest_task|escape }}<br/>
- ({{ queue.eta_delta|escape }})
- {% else %}
- None
- {% endif %}
- </td>
- <td valign="top">
- {{ queue.tasks_in_queue|escape }}
- </td>
- <td valign="top">
- <form action="{{ queues_path }}" method="post">
- <input type="hidden" name="xsrf_token" value="{{ xsrf_token }}"/>
- <input type="hidden" name="queue" value="{{ queue.name|escape }}"/>
- <input type="submit" name="action:purgequeue" value="Purge Queue"
- onclick="return confirm('Are you sure you want to purge all ' +
- 'tasks from {{ queue.name|escape }}?');"/>
- </form>
- </td>
+ <table id="ah-queues" class="ae-table ae-table-striped">
+ <thead>
+ <tr>
+ <th>Queue Name</th>
+ {% if queueBatch.rate_limited %}
+ <th>Maximum Rate</th>
+ <th>Bucket Size</th>
+ {% endif %}
+ <th>Oldest Task (UTC)</th>
+ <th>Tasks in Queue</th>
+ <th></th>
</tr>
- {% endfor %}
- </tbody>
- </table>
-{% else %}
- This application doesn't define any task queues. See the documentation for more.
-{% endif %}
+ </thead>
+ <caption><strong>{{ queueBatch.title }}</strong></caption>
+ <tbody>
+ {% for queue in queueBatch %}
+ <tr class="{% cycle ae-odd,ae-even %}">
+ <td valign="top">
+ <a href="{{ tasks_path }}?queue={{ queue.name|escape }}">
+ {{ queue.name|escape }}</a>
+ </td>
+ {% if queueBatch.rate_limited %}
+ <td valign="top">
+ {{ queue.rate|escape }}
+ </td>
+ <td valign="top">
+ {{ queue.bucket_size|escape }}
+ </td>
+ {% endif %}
+ <td valign="top">
+ {% if queue.oldest_task %}
+ {{ queue.oldest_task|escape }}<br/>
+ ({{ queue.eta_delta|escape }})
+ {% else %}
+ None
+ {% endif %}
+ </td>
+ <td valign="top">
+ {{ queue.tasks_in_queue|escape }}
+ </td>
+ <td valign="top">
+ <form action="{{ queues_path }}" method="post">
+ <input type="hidden" name="xsrf_token" value="{{ xsrf_token }}"/>
+ <input type="hidden" name="queue" value="{{ queue.name|escape }}"/>
+ <input type="submit" name="action:purgequeue" value="Purge Queue"
+ onclick="return confirm('Are you sure you want to purge all ' +
+ 'tasks from {{ queue.name|escape }}?');"/>
+ </form>
+ </td>
+ </tr>
+ {% endfor %}
+ </tbody>
+ </table>
+ {% else %}
+ This application doesn't define any {{ queueBatch.title }}. See the documentation for more.
+ {% endif %}
+{% endfor %}
{% endblock %}
diff --git a/google/appengine/ext/admin/templates/tasks.html b/google/appengine/ext/admin/templates/tasks.html
index f7f09e7..d89c29d 100644
--- a/google/appengine/ext/admin/templates/tasks.html
+++ b/google/appengine/ext/admin/templates/tasks.html
@@ -36,9 +36,11 @@
<h3>Tasks for Queue: {{ queue_name|escape }}</h3>
{% if tasks %}
- <p>
- Push the 'Run' button to execute a task manually.
- </p>
+ {% if is_push_queue %}
+ <p>
+ Push the 'Run' button to execute a task manually.
+ </p>
+ {% endif %}
<table id="ah-tasks" class="ae-table ae-table-striped">
<thead>
@@ -68,15 +70,17 @@
{{ task.url|escape }}
</td>
<td valign="top">
- <form id="runform.{{ task.name|escape }}" action="{{ task.url|escape }}" method="{{ task.method|escape }}" onsubmit="(new Webhook('runform.{{ task.name|escape }}')).run(handleTaskResult); return false">
- <input type="hidden" name="xsrf_token" value="{{ xsrf_token }}"/>
- <input type="hidden" name="payload" value="{{ task.body|escape }}">
- {% for header in task.headers %}
- <input type="hidden" name="header:{{ header.0|escape }}"
- value="{{ header.1|escape }}"/>
- {% endfor %}
- <input type="submit" value="Run"/>
- </form>
+ {% if is_push_queue %}
+ <form id="runform.{{ task.name|escape }}" action="{{ task.url|escape }}" method="{{ task.method|escape }}" onsubmit="(new Webhook('runform.{{ task.name|escape }}')).run(handleTaskResult); return false">
+ <input type="hidden" name="xsrf_token" value="{{ xsrf_token }}"/>
+ <input type="hidden" name="payload" value="{{ task.body|escape }}">
+ {% for header in task.headers %}
+ <input type="hidden" name="header:{{ header.0|escape }}"
+ value="{{ header.1|escape }}"/>
+ {% endfor %}
+ <input type="submit" value="Run"/>
+ </form>
+ {% endif %}
</td>
<td valign="top">
<form id="deleteform.{{ task.name|escape }}" action="{{ tasks_path }}" method="post">
diff --git a/google/appengine/ext/appstats/datamodel_pb.py b/google/appengine/ext/appstats/datamodel_pb.py
index aba4a4b..fc1e961 100644
--- a/google/appengine/ext/appstats/datamodel_pb.py
+++ b/google/appengine/ext/appstats/datamodel_pb.py
@@ -31,6 +31,8 @@
_extension_runtime = False
_ExtendableProtocolMessage = ProtocolBuffer.ProtocolMessage
+from google.appengine.datastore.entity_pb import *
+import google.appengine.datastore.entity_pb
class AggregateRpcStatsProto(ProtocolBuffer.ProtocolMessage):
has_service_call_name_ = 0
service_call_name_ = ""
@@ -535,6 +537,366 @@
_STYLE = """"""
_STYLE_CONTENT_TYPE = """"""
_PROTO_DESCRIPTOR_NAME = 'apphosting.StackFrameProto'
+class DatastoreCallDetailsProto(ProtocolBuffer.ProtocolMessage):
+ has_query_kind_ = 0
+ query_kind_ = ""
+ has_query_ancestor_ = 0
+ query_ancestor_ = None
+ has_query_thiscursor_ = 0
+ query_thiscursor_ = 0
+ has_query_nextcursor_ = 0
+ query_nextcursor_ = 0
+
+ def __init__(self, contents=None):
+ self.get_successful_fetch_ = []
+ self.keys_read_ = []
+ self.keys_written_ = []
+ self.lazy_init_lock_ = thread.allocate_lock()
+ if contents is not None: self.MergeFromString(contents)
+
+ def query_kind(self): return self.query_kind_
+
+ def set_query_kind(self, x):
+ self.has_query_kind_ = 1
+ self.query_kind_ = x
+
+ def clear_query_kind(self):
+ if self.has_query_kind_:
+ self.has_query_kind_ = 0
+ self.query_kind_ = ""
+
+ def has_query_kind(self): return self.has_query_kind_
+
+ def query_ancestor(self):
+ if self.query_ancestor_ is None:
+ self.lazy_init_lock_.acquire()
+ try:
+ if self.query_ancestor_ is None: self.query_ancestor_ = Reference()
+ finally:
+ self.lazy_init_lock_.release()
+ return self.query_ancestor_
+
+ def mutable_query_ancestor(self): self.has_query_ancestor_ = 1; return self.query_ancestor()
+
+ def clear_query_ancestor(self):
+
+ if self.has_query_ancestor_:
+ self.has_query_ancestor_ = 0;
+ if self.query_ancestor_ is not None: self.query_ancestor_.Clear()
+
+ def has_query_ancestor(self): return self.has_query_ancestor_
+
+ def query_thiscursor(self): return self.query_thiscursor_
+
+ def set_query_thiscursor(self, x):
+ self.has_query_thiscursor_ = 1
+ self.query_thiscursor_ = x
+
+ def clear_query_thiscursor(self):
+ if self.has_query_thiscursor_:
+ self.has_query_thiscursor_ = 0
+ self.query_thiscursor_ = 0
+
+ def has_query_thiscursor(self): return self.has_query_thiscursor_
+
+ def query_nextcursor(self): return self.query_nextcursor_
+
+ def set_query_nextcursor(self, x):
+ self.has_query_nextcursor_ = 1
+ self.query_nextcursor_ = x
+
+ def clear_query_nextcursor(self):
+ if self.has_query_nextcursor_:
+ self.has_query_nextcursor_ = 0
+ self.query_nextcursor_ = 0
+
+ def has_query_nextcursor(self): return self.has_query_nextcursor_
+
+ def get_successful_fetch_size(self): return len(self.get_successful_fetch_)
+ def get_successful_fetch_list(self): return self.get_successful_fetch_
+
+ def get_successful_fetch(self, i):
+ return self.get_successful_fetch_[i]
+
+ def set_get_successful_fetch(self, i, x):
+ self.get_successful_fetch_[i] = x
+
+ def add_get_successful_fetch(self, x):
+ self.get_successful_fetch_.append(x)
+
+ def clear_get_successful_fetch(self):
+ self.get_successful_fetch_ = []
+
+ def keys_read_size(self): return len(self.keys_read_)
+ def keys_read_list(self): return self.keys_read_
+
+ def keys_read(self, i):
+ return self.keys_read_[i]
+
+ def mutable_keys_read(self, i):
+ return self.keys_read_[i]
+
+ def add_keys_read(self):
+ x = Reference()
+ self.keys_read_.append(x)
+ return x
+
+ def clear_keys_read(self):
+ self.keys_read_ = []
+ def keys_written_size(self): return len(self.keys_written_)
+ def keys_written_list(self): return self.keys_written_
+
+ def keys_written(self, i):
+ return self.keys_written_[i]
+
+ def mutable_keys_written(self, i):
+ return self.keys_written_[i]
+
+ def add_keys_written(self):
+ x = Reference()
+ self.keys_written_.append(x)
+ return x
+
+ def clear_keys_written(self):
+ self.keys_written_ = []
+
+ def MergeFrom(self, x):
+ assert x is not self
+ if (x.has_query_kind()): self.set_query_kind(x.query_kind())
+ if (x.has_query_ancestor()): self.mutable_query_ancestor().MergeFrom(x.query_ancestor())
+ if (x.has_query_thiscursor()): self.set_query_thiscursor(x.query_thiscursor())
+ if (x.has_query_nextcursor()): self.set_query_nextcursor(x.query_nextcursor())
+ for i in xrange(x.get_successful_fetch_size()): self.add_get_successful_fetch(x.get_successful_fetch(i))
+ for i in xrange(x.keys_read_size()): self.add_keys_read().CopyFrom(x.keys_read(i))
+ for i in xrange(x.keys_written_size()): self.add_keys_written().CopyFrom(x.keys_written(i))
+
+ def Equals(self, x):
+ if x is self: return 1
+ if self.has_query_kind_ != x.has_query_kind_: return 0
+ if self.has_query_kind_ and self.query_kind_ != x.query_kind_: return 0
+ if self.has_query_ancestor_ != x.has_query_ancestor_: return 0
+ if self.has_query_ancestor_ and self.query_ancestor_ != x.query_ancestor_: return 0
+ if self.has_query_thiscursor_ != x.has_query_thiscursor_: return 0
+ if self.has_query_thiscursor_ and self.query_thiscursor_ != x.query_thiscursor_: return 0
+ if self.has_query_nextcursor_ != x.has_query_nextcursor_: return 0
+ if self.has_query_nextcursor_ and self.query_nextcursor_ != x.query_nextcursor_: return 0
+ if len(self.get_successful_fetch_) != len(x.get_successful_fetch_): return 0
+ for e1, e2 in zip(self.get_successful_fetch_, x.get_successful_fetch_):
+ if e1 != e2: return 0
+ if len(self.keys_read_) != len(x.keys_read_): return 0
+ for e1, e2 in zip(self.keys_read_, x.keys_read_):
+ if e1 != e2: return 0
+ if len(self.keys_written_) != len(x.keys_written_): return 0
+ for e1, e2 in zip(self.keys_written_, x.keys_written_):
+ if e1 != e2: return 0
+ return 1
+
+ def IsInitialized(self, debug_strs=None):
+ initialized = 1
+ if (self.has_query_ancestor_ and not self.query_ancestor_.IsInitialized(debug_strs)): initialized = 0
+ for p in self.keys_read_:
+ if not p.IsInitialized(debug_strs): initialized=0
+ for p in self.keys_written_:
+ if not p.IsInitialized(debug_strs): initialized=0
+ return initialized
+
+ def ByteSize(self):
+ n = 0
+ if (self.has_query_kind_): n += 1 + self.lengthString(len(self.query_kind_))
+ if (self.has_query_ancestor_): n += 1 + self.lengthString(self.query_ancestor_.ByteSize())
+ if (self.has_query_thiscursor_): n += 9
+ if (self.has_query_nextcursor_): n += 9
+ n += 2 * len(self.get_successful_fetch_)
+ n += 1 * len(self.keys_read_)
+ for i in xrange(len(self.keys_read_)): n += self.lengthString(self.keys_read_[i].ByteSize())
+ n += 1 * len(self.keys_written_)
+ for i in xrange(len(self.keys_written_)): n += self.lengthString(self.keys_written_[i].ByteSize())
+ return n
+
+ def ByteSizePartial(self):
+ n = 0
+ if (self.has_query_kind_): n += 1 + self.lengthString(len(self.query_kind_))
+ if (self.has_query_ancestor_): n += 1 + self.lengthString(self.query_ancestor_.ByteSizePartial())
+ if (self.has_query_thiscursor_): n += 9
+ if (self.has_query_nextcursor_): n += 9
+ n += 2 * len(self.get_successful_fetch_)
+ n += 1 * len(self.keys_read_)
+ for i in xrange(len(self.keys_read_)): n += self.lengthString(self.keys_read_[i].ByteSizePartial())
+ n += 1 * len(self.keys_written_)
+ for i in xrange(len(self.keys_written_)): n += self.lengthString(self.keys_written_[i].ByteSizePartial())
+ return n
+
+ def Clear(self):
+ self.clear_query_kind()
+ self.clear_query_ancestor()
+ self.clear_query_thiscursor()
+ self.clear_query_nextcursor()
+ self.clear_get_successful_fetch()
+ self.clear_keys_read()
+ self.clear_keys_written()
+
+ def OutputUnchecked(self, out):
+ if (self.has_query_kind_):
+ out.putVarInt32(10)
+ out.putPrefixedString(self.query_kind_)
+ if (self.has_query_ancestor_):
+ out.putVarInt32(18)
+ out.putVarInt32(self.query_ancestor_.ByteSize())
+ self.query_ancestor_.OutputUnchecked(out)
+ if (self.has_query_thiscursor_):
+ out.putVarInt32(25)
+ out.put64(self.query_thiscursor_)
+ if (self.has_query_nextcursor_):
+ out.putVarInt32(33)
+ out.put64(self.query_nextcursor_)
+ for i in xrange(len(self.get_successful_fetch_)):
+ out.putVarInt32(40)
+ out.putBoolean(self.get_successful_fetch_[i])
+ for i in xrange(len(self.keys_read_)):
+ out.putVarInt32(50)
+ out.putVarInt32(self.keys_read_[i].ByteSize())
+ self.keys_read_[i].OutputUnchecked(out)
+ for i in xrange(len(self.keys_written_)):
+ out.putVarInt32(58)
+ out.putVarInt32(self.keys_written_[i].ByteSize())
+ self.keys_written_[i].OutputUnchecked(out)
+
+ def OutputPartial(self, out):
+ if (self.has_query_kind_):
+ out.putVarInt32(10)
+ out.putPrefixedString(self.query_kind_)
+ if (self.has_query_ancestor_):
+ out.putVarInt32(18)
+ out.putVarInt32(self.query_ancestor_.ByteSizePartial())
+ self.query_ancestor_.OutputPartial(out)
+ if (self.has_query_thiscursor_):
+ out.putVarInt32(25)
+ out.put64(self.query_thiscursor_)
+ if (self.has_query_nextcursor_):
+ out.putVarInt32(33)
+ out.put64(self.query_nextcursor_)
+ for i in xrange(len(self.get_successful_fetch_)):
+ out.putVarInt32(40)
+ out.putBoolean(self.get_successful_fetch_[i])
+ for i in xrange(len(self.keys_read_)):
+ out.putVarInt32(50)
+ out.putVarInt32(self.keys_read_[i].ByteSizePartial())
+ self.keys_read_[i].OutputPartial(out)
+ for i in xrange(len(self.keys_written_)):
+ out.putVarInt32(58)
+ out.putVarInt32(self.keys_written_[i].ByteSizePartial())
+ self.keys_written_[i].OutputPartial(out)
+
+ def TryMerge(self, d):
+ while d.avail() > 0:
+ tt = d.getVarInt32()
+ if tt == 10:
+ self.set_query_kind(d.getPrefixedString())
+ continue
+ if tt == 18:
+ length = d.getVarInt32()
+ tmp = ProtocolBuffer.Decoder(d.buffer(), d.pos(), d.pos() + length)
+ d.skip(length)
+ self.mutable_query_ancestor().TryMerge(tmp)
+ continue
+ if tt == 25:
+ self.set_query_thiscursor(d.get64())
+ continue
+ if tt == 33:
+ self.set_query_nextcursor(d.get64())
+ continue
+ if tt == 40:
+ self.add_get_successful_fetch(d.getBoolean())
+ continue
+ if tt == 50:
+ length = d.getVarInt32()
+ tmp = ProtocolBuffer.Decoder(d.buffer(), d.pos(), d.pos() + length)
+ d.skip(length)
+ self.add_keys_read().TryMerge(tmp)
+ continue
+ if tt == 58:
+ length = d.getVarInt32()
+ tmp = ProtocolBuffer.Decoder(d.buffer(), d.pos(), d.pos() + length)
+ d.skip(length)
+ self.add_keys_written().TryMerge(tmp)
+ continue
+
+
+ if (tt == 0): raise ProtocolBuffer.ProtocolBufferDecodeError
+ d.skipData(tt)
+
+
+ def __str__(self, prefix="", printElemNumber=0):
+ res=""
+ if self.has_query_kind_: res+=prefix+("query_kind: %s\n" % self.DebugFormatString(self.query_kind_))
+ if self.has_query_ancestor_:
+ res+=prefix+"query_ancestor <\n"
+ res+=self.query_ancestor_.__str__(prefix + " ", printElemNumber)
+ res+=prefix+">\n"
+ if self.has_query_thiscursor_: res+=prefix+("query_thiscursor: %s\n" % self.DebugFormatFixed64(self.query_thiscursor_))
+ if self.has_query_nextcursor_: res+=prefix+("query_nextcursor: %s\n" % self.DebugFormatFixed64(self.query_nextcursor_))
+ cnt=0
+ for e in self.get_successful_fetch_:
+ elm=""
+ if printElemNumber: elm="(%d)" % cnt
+ res+=prefix+("get_successful_fetch%s: %s\n" % (elm, self.DebugFormatBool(e)))
+ cnt+=1
+ cnt=0
+ for e in self.keys_read_:
+ elm=""
+ if printElemNumber: elm="(%d)" % cnt
+ res+=prefix+("keys_read%s <\n" % elm)
+ res+=e.__str__(prefix + " ", printElemNumber)
+ res+=prefix+">\n"
+ cnt+=1
+ cnt=0
+ for e in self.keys_written_:
+ elm=""
+ if printElemNumber: elm="(%d)" % cnt
+ res+=prefix+("keys_written%s <\n" % elm)
+ res+=e.__str__(prefix + " ", printElemNumber)
+ res+=prefix+">\n"
+ cnt+=1
+ return res
+
+
+ def _BuildTagLookupTable(sparse, maxtag, default=None):
+ return tuple([sparse.get(i, default) for i in xrange(0, 1+maxtag)])
+
+ kquery_kind = 1
+ kquery_ancestor = 2
+ kquery_thiscursor = 3
+ kquery_nextcursor = 4
+ kget_successful_fetch = 5
+ kkeys_read = 6
+ kkeys_written = 7
+
+ _TEXT = _BuildTagLookupTable({
+ 0: "ErrorCode",
+ 1: "query_kind",
+ 2: "query_ancestor",
+ 3: "query_thiscursor",
+ 4: "query_nextcursor",
+ 5: "get_successful_fetch",
+ 6: "keys_read",
+ 7: "keys_written",
+ }, 7)
+
+ _TYPES = _BuildTagLookupTable({
+ 0: ProtocolBuffer.Encoder.NUMERIC,
+ 1: ProtocolBuffer.Encoder.STRING,
+ 2: ProtocolBuffer.Encoder.STRING,
+ 3: ProtocolBuffer.Encoder.DOUBLE,
+ 4: ProtocolBuffer.Encoder.DOUBLE,
+ 5: ProtocolBuffer.Encoder.NUMERIC,
+ 6: ProtocolBuffer.Encoder.STRING,
+ 7: ProtocolBuffer.Encoder.STRING,
+ }, 7, ProtocolBuffer.Encoder.MAX_TYPE)
+
+
+ _STYLE = """"""
+ _STYLE_CONTENT_TYPE = """"""
+ _PROTO_DESCRIPTOR_NAME = 'apphosting.DatastoreCallDetailsProto'
class IndividualRpcStatsProto(ProtocolBuffer.ProtocolMessage):
has_service_call_name_ = 0
service_call_name_ = ""
@@ -544,6 +906,8 @@
response_data_summary_ = ""
has_api_mcycles_ = 0
api_mcycles_ = 0
+ has_api_milliseconds_ = 0
+ api_milliseconds_ = 0
has_start_offset_milliseconds_ = 0
start_offset_milliseconds_ = 0
has_duration_milliseconds_ = 0
@@ -552,9 +916,12 @@
namespace_ = ""
has_was_successful_ = 0
was_successful_ = 1
+ has_datastore_details_ = 0
+ datastore_details_ = None
def __init__(self, contents=None):
self.call_stack_ = []
+ self.lazy_init_lock_ = thread.allocate_lock()
if contents is not None: self.MergeFromString(contents)
def service_call_name(self): return self.service_call_name_
@@ -609,6 +976,19 @@
def has_api_mcycles(self): return self.has_api_mcycles_
+ def api_milliseconds(self): return self.api_milliseconds_
+
+ def set_api_milliseconds(self, x):
+ self.has_api_milliseconds_ = 1
+ self.api_milliseconds_ = x
+
+ def clear_api_milliseconds(self):
+ if self.has_api_milliseconds_:
+ self.has_api_milliseconds_ = 0
+ self.api_milliseconds_ = 0
+
+ def has_api_milliseconds(self): return self.has_api_milliseconds_
+
def start_offset_milliseconds(self): return self.start_offset_milliseconds_
def set_start_offset_milliseconds(self, x):
@@ -677,6 +1057,25 @@
def clear_call_stack(self):
self.call_stack_ = []
+ def datastore_details(self):
+ if self.datastore_details_ is None:
+ self.lazy_init_lock_.acquire()
+ try:
+ if self.datastore_details_ is None: self.datastore_details_ = DatastoreCallDetailsProto()
+ finally:
+ self.lazy_init_lock_.release()
+ return self.datastore_details_
+
+ def mutable_datastore_details(self): self.has_datastore_details_ = 1; return self.datastore_details()
+
+ def clear_datastore_details(self):
+
+ if self.has_datastore_details_:
+ self.has_datastore_details_ = 0;
+ if self.datastore_details_ is not None: self.datastore_details_.Clear()
+
+ def has_datastore_details(self): return self.has_datastore_details_
+
def MergeFrom(self, x):
assert x is not self
@@ -684,11 +1083,13 @@
if (x.has_request_data_summary()): self.set_request_data_summary(x.request_data_summary())
if (x.has_response_data_summary()): self.set_response_data_summary(x.response_data_summary())
if (x.has_api_mcycles()): self.set_api_mcycles(x.api_mcycles())
+ if (x.has_api_milliseconds()): self.set_api_milliseconds(x.api_milliseconds())
if (x.has_start_offset_milliseconds()): self.set_start_offset_milliseconds(x.start_offset_milliseconds())
if (x.has_duration_milliseconds()): self.set_duration_milliseconds(x.duration_milliseconds())
if (x.has_namespace()): self.set_namespace(x.namespace())
if (x.has_was_successful()): self.set_was_successful(x.was_successful())
for i in xrange(x.call_stack_size()): self.add_call_stack().CopyFrom(x.call_stack(i))
+ if (x.has_datastore_details()): self.mutable_datastore_details().MergeFrom(x.datastore_details())
def Equals(self, x):
if x is self: return 1
@@ -700,6 +1101,8 @@
if self.has_response_data_summary_ and self.response_data_summary_ != x.response_data_summary_: return 0
if self.has_api_mcycles_ != x.has_api_mcycles_: return 0
if self.has_api_mcycles_ and self.api_mcycles_ != x.api_mcycles_: return 0
+ if self.has_api_milliseconds_ != x.has_api_milliseconds_: return 0
+ if self.has_api_milliseconds_ and self.api_milliseconds_ != x.api_milliseconds_: return 0
if self.has_start_offset_milliseconds_ != x.has_start_offset_milliseconds_: return 0
if self.has_start_offset_milliseconds_ and self.start_offset_milliseconds_ != x.start_offset_milliseconds_: return 0
if self.has_duration_milliseconds_ != x.has_duration_milliseconds_: return 0
@@ -711,6 +1114,8 @@
if len(self.call_stack_) != len(x.call_stack_): return 0
for e1, e2 in zip(self.call_stack_, x.call_stack_):
if e1 != e2: return 0
+ if self.has_datastore_details_ != x.has_datastore_details_: return 0
+ if self.has_datastore_details_ and self.datastore_details_ != x.datastore_details_: return 0
return 1
def IsInitialized(self, debug_strs=None):
@@ -725,6 +1130,7 @@
debug_strs.append('Required field: start_offset_milliseconds not set.')
for p in self.call_stack_:
if not p.IsInitialized(debug_strs): initialized=0
+ if (self.has_datastore_details_ and not self.datastore_details_.IsInitialized(debug_strs)): initialized = 0
return initialized
def ByteSize(self):
@@ -733,12 +1139,14 @@
if (self.has_request_data_summary_): n += 1 + self.lengthString(len(self.request_data_summary_))
if (self.has_response_data_summary_): n += 1 + self.lengthString(len(self.response_data_summary_))
if (self.has_api_mcycles_): n += 1 + self.lengthVarInt64(self.api_mcycles_)
+ if (self.has_api_milliseconds_): n += 1 + self.lengthVarInt64(self.api_milliseconds_)
n += self.lengthVarInt64(self.start_offset_milliseconds_)
if (self.has_duration_milliseconds_): n += 1 + self.lengthVarInt64(self.duration_milliseconds_)
if (self.has_namespace_): n += 1 + self.lengthString(len(self.namespace_))
if (self.has_was_successful_): n += 2
n += 1 * len(self.call_stack_)
for i in xrange(len(self.call_stack_)): n += self.lengthString(self.call_stack_[i].ByteSize())
+ if (self.has_datastore_details_): n += 1 + self.lengthString(self.datastore_details_.ByteSize())
return n + 2
def ByteSizePartial(self):
@@ -749,6 +1157,7 @@
if (self.has_request_data_summary_): n += 1 + self.lengthString(len(self.request_data_summary_))
if (self.has_response_data_summary_): n += 1 + self.lengthString(len(self.response_data_summary_))
if (self.has_api_mcycles_): n += 1 + self.lengthVarInt64(self.api_mcycles_)
+ if (self.has_api_milliseconds_): n += 1 + self.lengthVarInt64(self.api_milliseconds_)
if (self.has_start_offset_milliseconds_):
n += 1
n += self.lengthVarInt64(self.start_offset_milliseconds_)
@@ -757,6 +1166,7 @@
if (self.has_was_successful_): n += 2
n += 1 * len(self.call_stack_)
for i in xrange(len(self.call_stack_)): n += self.lengthString(self.call_stack_[i].ByteSizePartial())
+ if (self.has_datastore_details_): n += 1 + self.lengthString(self.datastore_details_.ByteSizePartial())
return n
def Clear(self):
@@ -764,11 +1174,13 @@
self.clear_request_data_summary()
self.clear_response_data_summary()
self.clear_api_mcycles()
+ self.clear_api_milliseconds()
self.clear_start_offset_milliseconds()
self.clear_duration_milliseconds()
self.clear_namespace()
self.clear_was_successful()
self.clear_call_stack()
+ self.clear_datastore_details()
def OutputUnchecked(self, out):
out.putVarInt32(10)
@@ -797,6 +1209,13 @@
out.putVarInt32(82)
out.putVarInt32(self.call_stack_[i].ByteSize())
self.call_stack_[i].OutputUnchecked(out)
+ if (self.has_api_milliseconds_):
+ out.putVarInt32(88)
+ out.putVarInt64(self.api_milliseconds_)
+ if (self.has_datastore_details_):
+ out.putVarInt32(98)
+ out.putVarInt32(self.datastore_details_.ByteSize())
+ self.datastore_details_.OutputUnchecked(out)
def OutputPartial(self, out):
if (self.has_service_call_name_):
@@ -827,6 +1246,13 @@
out.putVarInt32(82)
out.putVarInt32(self.call_stack_[i].ByteSizePartial())
self.call_stack_[i].OutputPartial(out)
+ if (self.has_api_milliseconds_):
+ out.putVarInt32(88)
+ out.putVarInt64(self.api_milliseconds_)
+ if (self.has_datastore_details_):
+ out.putVarInt32(98)
+ out.putVarInt32(self.datastore_details_.ByteSizePartial())
+ self.datastore_details_.OutputPartial(out)
def TryMerge(self, d):
while d.avail() > 0:
@@ -861,6 +1287,15 @@
d.skip(length)
self.add_call_stack().TryMerge(tmp)
continue
+ if tt == 88:
+ self.set_api_milliseconds(d.getVarInt64())
+ continue
+ if tt == 98:
+ length = d.getVarInt32()
+ tmp = ProtocolBuffer.Decoder(d.buffer(), d.pos(), d.pos() + length)
+ d.skip(length)
+ self.mutable_datastore_details().TryMerge(tmp)
+ continue
if (tt == 0): raise ProtocolBuffer.ProtocolBufferDecodeError
@@ -873,6 +1308,7 @@
if self.has_request_data_summary_: res+=prefix+("request_data_summary: %s\n" % self.DebugFormatString(self.request_data_summary_))
if self.has_response_data_summary_: res+=prefix+("response_data_summary: %s\n" % self.DebugFormatString(self.response_data_summary_))
if self.has_api_mcycles_: res+=prefix+("api_mcycles: %s\n" % self.DebugFormatInt64(self.api_mcycles_))
+ if self.has_api_milliseconds_: res+=prefix+("api_milliseconds: %s\n" % self.DebugFormatInt64(self.api_milliseconds_))
if self.has_start_offset_milliseconds_: res+=prefix+("start_offset_milliseconds: %s\n" % self.DebugFormatInt64(self.start_offset_milliseconds_))
if self.has_duration_milliseconds_: res+=prefix+("duration_milliseconds: %s\n" % self.DebugFormatInt64(self.duration_milliseconds_))
if self.has_namespace_: res+=prefix+("namespace: %s\n" % self.DebugFormatString(self.namespace_))
@@ -885,6 +1321,10 @@
res+=e.__str__(prefix + " ", printElemNumber)
res+=prefix+">\n"
cnt+=1
+ if self.has_datastore_details_:
+ res+=prefix+"datastore_details <\n"
+ res+=self.datastore_details_.__str__(prefix + " ", printElemNumber)
+ res+=prefix+">\n"
return res
@@ -895,11 +1335,13 @@
krequest_data_summary = 3
kresponse_data_summary = 4
kapi_mcycles = 5
+ kapi_milliseconds = 11
kstart_offset_milliseconds = 6
kduration_milliseconds = 7
knamespace = 8
kwas_successful = 9
kcall_stack = 10
+ kdatastore_details = 12
_TEXT = _BuildTagLookupTable({
0: "ErrorCode",
@@ -912,7 +1354,9 @@
8: "namespace",
9: "was_successful",
10: "call_stack",
- }, 10)
+ 11: "api_milliseconds",
+ 12: "datastore_details",
+ }, 12)
_TYPES = _BuildTagLookupTable({
0: ProtocolBuffer.Encoder.NUMERIC,
@@ -925,7 +1369,9 @@
8: ProtocolBuffer.Encoder.STRING,
9: ProtocolBuffer.Encoder.NUMERIC,
10: ProtocolBuffer.Encoder.STRING,
- }, 10, ProtocolBuffer.Encoder.MAX_TYPE)
+ 11: ProtocolBuffer.Encoder.NUMERIC,
+ 12: ProtocolBuffer.Encoder.STRING,
+ }, 12, ProtocolBuffer.Encoder.MAX_TYPE)
_STYLE = """"""
@@ -1538,4 +1984,4 @@
if _extension_runtime:
pass
-__all__ = ['AggregateRpcStatsProto','KeyValProto','StackFrameProto','IndividualRpcStatsProto','RequestStatProto']
+__all__ = ['AggregateRpcStatsProto','KeyValProto','StackFrameProto','DatastoreCallDetailsProto','IndividualRpcStatsProto','RequestStatProto']
diff --git a/google/appengine/ext/appstats/recording.py b/google/appengine/ext/appstats/recording.py
index 999d231..92ea651 100755
--- a/google/appengine/ext/appstats/recording.py
+++ b/google/appengine/ext/appstats/recording.py
@@ -108,6 +108,12 @@
+
+
+ DATASTORE_DETAILS = False
+
+
+
def should_record(env):
"""Return a bool indicating whether we should record this request.
@@ -250,6 +256,110 @@
self.traces.append(trace)
self.overhead += (now - pre_now)
+ def record_datastore_details(self, call, request, response, trace):
+ """Records entity information relating to datastore related RPCs.
+
+ Parses requests and responses of datastore related RPCs, and records
+ the primary keys of entities that are put into the datastore or
+ fetched from the datastore. Non-datastore RPCs are ignored. Keys are
+ recorded in the form of Reference protos. Currently the information
+ is logged for the following calls: Get, Put, RunQuery and Next. The
+ code may be extended in the future to cover more RPC calls. In
+ addition to the entity keys, useful information specific to each
+ call is recorded. E.g., for queries, the entity kind and cursor
+ information is recorded; For gets, a flag indicating if the
+ requested entity key is present or not is recorded.
+
+ Args:
+ call: The call name, e.g. 'Get'.
+ request: The request protocol message corresponding to the call.
+ response: The response protocol message corresponding to the call.
+ trace: IndividualStatsProto where information must be recorded.
+ """
+ if call == 'Put':
+ self.record_put_details(response, trace)
+ elif call in ('RunQuery', 'Next'):
+ self.record_query_details(call, request, response, trace)
+ elif call == 'Get':
+ self.record_get_details(request, response, trace)
+
+
+ def record_put_details(self, response, trace):
+ """Records keys of entities written by datastore put calls.
+
+ Args:
+ response: The response protocol message of the Put RPC call.
+ trace: IndividualStatsProto where information must be recorded.
+ """
+ details = trace.mutable_datastore_details()
+ for key in response.key_list():
+ newent = details.add_keys_written()
+ newent.CopyFrom(key)
+
+ def record_get_details(self, request, response, trace):
+ """Records keys of entities requested by datastore gets.
+
+ Also records if each requested key was sucessfully fetched.
+
+ Args:
+ request: The request protocol message of the Get RPC call.
+ response: The response protocol message of the Get RPC call.
+ trace: IndividualStatsProto where information must be recorded.
+ """
+ details = trace.mutable_datastore_details()
+ for key in request.key_list():
+ newent = details.add_keys_read()
+ newent.CopyFrom(key)
+ for entity_present in response.entity_list():
+ details.add_get_successful_fetch(entity_present.has_entity())
+
+ def record_query_details(self, call, request, response, trace):
+ """Records keys of entities fetched by a datastore query.
+
+ Information is recorded for both the RunQuery and Next calls.
+ For RunQuery calls, we record the entity kind and ancestor (if
+ applicable) and cursor information (which can help correlate
+ the RunQuery with a subsequent Next call). For Next calls, we
+ record cursor information of the Request (which helps associate
+ this call with the previous RunQuery/Next call), and the Response
+ (which helps associate this call with the subsequent Next call).
+ For key only queries, entity keys are not recorded since entities
+ are not actually fetched. In the future, we might want to record
+ the entities but also record a flag indicating whether this is a
+ key only query.
+
+ Args:
+ call: The call name, e.g. 'RunQuery' or 'Next'
+ request: The request protocol message of the RPC call.
+ response: The response protocol message of the RPC call.
+ trace: IndividualStatsProto where information must be recorded.
+ """
+ details = trace.mutable_datastore_details()
+ if not response.keys_only():
+
+
+ for entity in response.result_list():
+ newent = details.add_keys_read()
+ newent.CopyFrom(entity.key())
+ if call == 'RunQuery':
+
+ if request.has_kind():
+ details.set_query_kind(request.kind())
+ if request.has_ancestor():
+ ancestor = details.mutable_query_ancestor()
+ ancestor.CopyFrom(request.ancestor())
+
+
+ if response.has_cursor():
+ details.set_query_nextcursor(response.cursor().cursor())
+ elif call == 'Next':
+
+
+
+ details.set_query_thiscursor(request.cursor().cursor())
+ if response.has_cursor():
+ details.set_query_nextcursor(response.cursor().cursor())
+
def record_rpc_request(self, service, call, request, response, rpc):
"""Record the request of an RPC call.
@@ -305,9 +415,13 @@
if 0 <= index < len(self.traces):
trace = self.traces[index]
trace.set_response_data_summary(sresp)
+ trace.set_api_milliseconds(mcycles_to_msecs(api_mcycles))
trace.set_api_mcycles(api_mcycles)
duration = delta - trace.start_offset_milliseconds()
trace.set_duration_milliseconds(duration)
+ if config.DATASTORE_DETAILS and service == 'datastore_v3':
+ self.record_datastore_details(call, request,
+ response, trace)
self.overhead += (time.time() - now)
return
else:
@@ -436,7 +550,7 @@
x.set_key(key)
x.set_value(value)
with self._lock:
- proto.individual_stats_list()[:] = self.traces
+ proto.individual_stats_list().extend(self.traces)
def json(self):
"""Return a JSON-ifyable representation of the pertinent data.
@@ -528,7 +642,7 @@
traces_mc = [trace.api_mcycles() for trace in self.traces]
mcycles = 0
for trace_mc in traces_mc:
- if isinstance(trace_mc, int):
+ if isinstance(trace_mc, (int, long)):
mcycles += trace_mc
return mcycles
@@ -562,7 +676,7 @@
trace.start_offset_milliseconds(),
trace.service_call_name(),
trace.duration_milliseconds(),
- trace.api_mcycles())
+ trace.api_milliseconds())
logging.info(' REQ : %s', trace.request_data_summary())
logging.info(' RESP : %s', trace.response_data_summary())
if level <= 1:
@@ -706,7 +820,7 @@
return formatting._format_value(val, config.MAX_REPR, config.MAX_DEPTH)
-class StatsProto(datamodel_pb.RequestStatProto):
+class StatsProto(object):
"""A subclass if RequestStatProto with a number of extra attributes.
This exists mainly so that ui.py can pass an instance of this class
@@ -733,16 +847,15 @@
"""
def __init__(self, *args, **kwds):
- """Constructor.
-
- This exists solely so it can pre-populate the .api_milliseconds
- attributes of the entries in .individual_stats_list().
- """
- datamodel_pb.RequestStatProto.__init__(self, *args, **kwds)
+ self._proto = datamodel_pb.RequestStatProto(*args, **kwds)
for r in self.individual_stats_list():
- r.api_milliseconds = mcycles_to_msecs(r.api_mcycles())
+ if not r.has_api_milliseconds():
+ r.set_api_milliseconds(mcycles_to_msecs(r.api_mcycles()))
+
+ def __getattr__(self, key):
+ return getattr(self._proto, key)
def start_time_formatted(self):
"""Return a string representing .start_timestamp_milliseconds()."""
@@ -756,16 +869,14 @@
warnings.warn('processor_mcycles does not return correct values',
DeprecationWarning,
stacklevel=2)
- return datamodel_pb.RequestStatProto.processor_mcycles(self)
+ return self._proto.processor_mcycles()
def processor_milliseconds(self):
"""Return an int giving .processor_mcycles() converted to milliseconds."""
warnings.warn('processor_milliseconds does not return correct values',
DeprecationWarning,
stacklevel=2)
-
- return mcycles_to_msecs(
- datamodel_pb.RequestStatProto.processor_mcycles(self))
+ return mcycles_to_msecs(self._proto.processor_mcycles())
__combined_rpc_count = None
diff --git a/google/appengine/ext/appstats/sample_appengine_config.py b/google/appengine/ext/appstats/sample_appengine_config.py
index 2479a07..3d9fdcb 100755
--- a/google/appengine/ext/appstats/sample_appengine_config.py
+++ b/google/appengine/ext/appstats/sample_appengine_config.py
@@ -227,4 +227,3 @@
# remoteapi_CUSTOM_ENVIRONMENT_AUTHENTICATION = (
# 'HTTP_X_APPENGINE_INBOUND_APPID', ['a trusted appid here'])
-
diff --git a/google/appengine/ext/appstats/static/appstats_js.js b/google/appengine/ext/appstats/static/appstats_js.js
index ab9333c..457b4a3 100755
--- a/google/appengine/ext/appstats/static/appstats_js.js
+++ b/google/appengine/ext/appstats/static/appstats_js.js
@@ -1,77 +1,78 @@
/* Copyright 2008-10 Google Inc. All Rights Reserved. */ (function(){function e(a){throw a;}
-var h=void 0,j=!0,k=null,n=!1,o,p=this,aa=function(a,b){var c=a.split("."),d=p;!(c[0]in d)&&d.execScript&&d.execScript("var "+c[0]);for(var f;c.length&&(f=c.shift());)!c.length&&b!==h?d[f]=b:d=d[f]?d[f]:d[f]={}},ba=function(a){a.P=function(){return a.Zb||(a.Zb=new a)}},ca=function(a){var b=typeof a;if("object"==b)if(a){if(a instanceof Array)return"array";if(a instanceof Object)return b;var c=Object.prototype.toString.call(a);if("[object Window]"==c)return"object";if("[object Array]"==c||"number"==
+var h=void 0,j=!0,k=null,n=!1,o,p=this,aa=function(a,b){var c=a.split("."),d=p;!(c[0]in d)&&d.execScript&&d.execScript("var "+c[0]);for(var f;c.length&&(f=c.shift());)!c.length&&b!==h?d[f]=b:d=d[f]?d[f]:d[f]={}},ba=function(a){a.P=function(){return a.Gb?a.Gb:a.Gb=new a}},ca=function(a){var b=typeof a;if("object"==b)if(a){if(a instanceof Array)return"array";if(a instanceof Object)return b;var c=Object.prototype.toString.call(a);if("[object Window]"==c)return"object";if("[object Array]"==c||"number"==
typeof a.length&&"undefined"!=typeof a.splice&&"undefined"!=typeof a.propertyIsEnumerable&&!a.propertyIsEnumerable("splice"))return"array";if("[object Function]"==c||"undefined"!=typeof a.call&&"undefined"!=typeof a.propertyIsEnumerable&&!a.propertyIsEnumerable("call"))return"function"}else return"null";else if("function"==b&&"undefined"==typeof a.call)return"object";return b},da=function(a){return"array"==ca(a)},ea=function(a){var b=ca(a);return"array"==b||"object"==b&&"number"==typeof a.length},
q=function(a){return"string"==typeof a},r=function(a){return"function"==ca(a)},fa=function(a){var b=typeof a;return"object"==b&&a!=k||"function"==b},u=function(a){return a[ga]||(a[ga]=++ha)},ga="closure_uid_"+Math.floor(2147483648*Math.random()).toString(36),ha=0,ia=function(a,b){var c=Array.prototype.slice.call(arguments,1);return function(){var b=Array.prototype.slice.call(arguments);b.unshift.apply(b,c);return a.apply(this,b)}},v=function(a,b){function c(){}c.prototype=b.prototype;a.d=b.prototype;
-a.prototype=new c;a.prototype.constructor=a};var ja=function(a){this.stack=Error().stack||"";a&&(this.message=""+a)};v(ja,Error);ja.prototype.name="CustomError";var ka=function(a,b){for(var c=1;c<arguments.length;c++)var d=(""+arguments[c]).replace(/\$/g,"$$$$"),a=a.replace(/\%s/,d);return a},la=function(a){return a.replace(/^[\s\xa0]+|[\s\xa0]+$/g,"")},ra=function(a){if(!ma.test(a))return a;-1!=a.indexOf("&")&&(a=a.replace(na,"&"));-1!=a.indexOf("<")&&(a=a.replace(oa,"<"));-1!=a.indexOf(">")&&(a=a.replace(pa,">"));-1!=a.indexOf('"')&&(a=a.replace(qa,"""));return a},na=/&/g,oa=/</g,pa=/>/g,qa=/\"/g,ma=/[&<>\"]/;var sa=function(a,b){b.unshift(a);ja.call(this,ka.apply(k,b));b.shift()};v(sa,ja);sa.prototype.name="AssertionError";var w=function(a,b,c){if(!a){var d=Array.prototype.slice.call(arguments,2),f="Assertion failed";if(b)var f=f+(": "+b),g=d;e(new sa(""+f,g||[]))}};var x=Array.prototype,ta=x.indexOf?function(a,b,c){w(a.length!=k);return x.indexOf.call(a,b,c)}:function(a,b,c){c=c==k?0:0>c?Math.max(0,a.length+c):c;if(q(a))return!q(b)||1!=b.length?-1:a.indexOf(b,c);for(;c<a.length;c++)if(c in a&&a[c]===b)return c;return-1},ua=x.forEach?function(a,b,c){w(a.length!=k);x.forEach.call(a,b,c)}:function(a,b,c){for(var d=a.length,f=q(a)?a.split(""):a,g=0;g<d;g++)g in f&&b.call(c,f[g],g,a)},va=x.every?function(a,b,c){w(a.length!=k);return x.every.call(a,b,c)}:function(a,
-b,c){for(var d=a.length,f=q(a)?a.split(""):a,g=0;g<d;g++)if(g in f&&!b.call(c,f[g],g,a))return n;return j},wa=function(a,b){return 0<=ta(a,b)},xa=function(a,b){var c=ta(a,b);0<=c&&(w(a.length!=k),x.splice.call(a,c,1))},ya=function(a){return x.concat.apply(x,arguments)},za=function(a){if(da(a))return ya(a);for(var b=[],c=0,d=a.length;c<d;c++)b[c]=a[c];return b},Ba=function(a,b,c,d){w(a.length!=k);x.splice.apply(a,Aa(arguments,1))},Aa=function(a,b,c){w(a.length!=k);return 2>=arguments.length?x.slice.call(a,
-b):x.slice.call(a,b,c)};var Ca=function(a,b){for(var c in a)b.call(h,a[c],c,a)},Da=function(a,b,c){b in a&&e(Error('The object already contains the key "'+b+'"'));a[b]=c},Ea=function(a){var b={},c;for(c in a)b[a[c]]=c;return b},Fa="constructor,hasOwnProperty,isPrototypeOf,propertyIsEnumerable,toLocaleString,toString,valueOf".split(","),Ga=function(a,b){for(var c,d,f=1;f<arguments.length;f++){d=arguments[f];for(c in d)a[c]=d[c];for(var g=0;g<Fa.length;g++)c=Fa[g],Object.prototype.hasOwnProperty.call(d,c)&&(a[c]=d[c])}};var Ha,Ia,Ja,Ka,La=function(){return p.navigator?p.navigator.userAgent:k};Ka=Ja=Ia=Ha=n;var Ma;if(Ma=La()){var Na=p.navigator;Ha=0==Ma.indexOf("Opera");Ia=!Ha&&-1!=Ma.indexOf("MSIE");Ja=!Ha&&-1!=Ma.indexOf("WebKit");Ka=!Ha&&!Ja&&"Gecko"==Na.product}var Oa=Ha,y=Ia,A=Ka,B=Ja,Pa=p.navigator,Ra=-1!=(Pa&&Pa.platform||"").indexOf("Mac"),Sa;
-a:{var Ta="",Ua;if(Oa&&p.opera)var Va=p.opera.version,Ta="function"==typeof Va?Va():Va;else if(A?Ua=/rv\:([^\);]+)(\)|;)/:y?Ua=/MSIE\s+([^\);]+)(\)|;)/:B&&(Ua=/WebKit\/(\S+)/),Ua)var Wa=Ua.exec(La()),Ta=Wa?Wa[1]:"";if(y){var Xa,Ya=p.document;Xa=Ya?Ya.documentMode:h;if(Xa>parseFloat(Ta)){Sa=""+Xa;break a}}Sa=Ta}
-var Za=Sa,$a={},C=function(a){var b;if(!(b=$a[a])){b=0;for(var c=la(""+Za).split("."),d=la(""+a).split("."),f=Math.max(c.length,d.length),g=0;0==b&&g<f;g++){var i=c[g]||"",l=d[g]||"",m=RegExp("(\\d*)(\\D*)","g"),s=RegExp("(\\d*)(\\D*)","g");do{var z=m.exec(i)||["","",""],t=s.exec(l)||["","",""];if(0==z[0].length&&0==t[0].length)break;b=((0==z[1].length?0:parseInt(z[1],10))<(0==t[1].length?0:parseInt(t[1],10))?-1:(0==z[1].length?0:parseInt(z[1],10))>(0==t[1].length?0:parseInt(t[1],10))?1:0)||((0==
-z[2].length)<(0==t[2].length)?-1:(0==z[2].length)>(0==t[2].length)?1:0)||(z[2]<t[2]?-1:z[2]>t[2]?1:0)}while(0==b)}b=$a[a]=0<=b}return b},ab={},bb=function(){return ab[9]||(ab[9]=y&&!!document.documentMode&&9<=document.documentMode)};var cb,db=!y||bb();!A&&!y||y&&bb()||A&&C("1.9.1");var eb=y&&!C("9");var fb=function(a){return(a=a.className)&&"function"==typeof a.split?a.split(/\s+/):[]},D=function(a,b){var c=fb(a),d=Aa(arguments,1),f;f=c;for(var g=0,i=0;i<d.length;i++)wa(f,d[i])||(f.push(d[i]),g++);f=g==d.length;a.className=c.join(" ");return f},gb=function(a,b){var c=fb(a),d=Aa(arguments,1),f;f=c;for(var g=0,i=0;i<f.length;i++)wa(d,f[i])&&(Ba(f,i--,1),g++);f=g==d.length;a.className=c.join(" ");return f};var jb=function(a){return a?new hb(ib(a)):cb||(cb=new hb)},kb=function(a){return q(a)?document.getElementById(a):a},lb=function(a,b,c){c=c||document;a=a&&"*"!=a?a.toUpperCase():"";if(c.querySelectorAll&&c.querySelector&&(!B||"CSS1Compat"==document.compatMode||C("528"))&&(a||b))return c.querySelectorAll(a+(b?"."+b:""));if(b&&c.getElementsByClassName){c=c.getElementsByClassName(b);if(a){for(var d={},f=0,g=0,i;i=c[g];g++)a==i.nodeName&&(d[f++]=i);d.length=f;return d}return c}c=c.getElementsByTagName(a||
-"*");if(b){d={};for(g=f=0;i=c[g];g++)a=i.className,"function"==typeof a.split&&wa(a.split(/\s+/),b)&&(d[f++]=i);d.length=f;return d}return c},nb=function(a,b){Ca(b,function(b,d){"style"==d?a.style.cssText=b:"class"==d?a.className=b:"for"==d?a.htmlFor=b:d in mb?a.setAttribute(mb[d],b):0==d.lastIndexOf("aria-",0)?a.setAttribute(d,b):a[d]=b})},mb={cellpadding:"cellPadding",cellspacing:"cellSpacing",colspan:"colSpan",rowspan:"rowSpan",valign:"vAlign",height:"height",width:"width",usemap:"useMap",frameborder:"frameBorder",
-maxlength:"maxLength",type:"type"},pb=function(a,b,c){return ob(document,arguments)},ob=function(a,b){var c=b[0],d=b[1];if(!db&&d&&(d.name||d.type)){c=["<",c];d.name&&c.push(' name="',ra(d.name),'"');if(d.type){c.push(' type="',ra(d.type),'"');var f={};Ga(f,d);d=f;delete d.type}c.push(">");c=c.join("")}c=a.createElement(c);d&&(q(d)?c.className=d:da(d)?D.apply(k,[c].concat(d)):nb(c,d));2<b.length&&qb(a,c,b);return c},qb=function(a,b,c){function d(c){c&&b.appendChild(q(c)?a.createTextNode(c):c)}for(var f=
-2;f<c.length;f++){var g=c[f];if(ea(g)&&!(fa(g)&&0<g.nodeType)){var i;a:{if(g&&"number"==typeof g.length){if(fa(g)){i="function"==typeof g.item||"string"==typeof g.item;break a}if(r(g)){i="function"==typeof g.item;break a}}i=n}ua(i?za(g):g,d)}else d(g)}},rb=function(a){a&&a.parentNode&&a.parentNode.removeChild(a)},sb=function(a){for(;a&&1!=a.nodeType;)a=a.nextSibling;return a},tb=function(a,b){if(a.contains&&1==b.nodeType)return a==b||a.contains(b);if("undefined"!=typeof a.compareDocumentPosition)return a==
-b||Boolean(a.compareDocumentPosition(b)&16);for(;b&&a!=b;)b=b.parentNode;return b==a},ib=function(a){return 9==a.nodeType?a:a.ownerDocument||a.document},ub=function(a,b){if("textContent"in a)a.textContent=b;else if(a.firstChild&&3==a.firstChild.nodeType){for(;a.lastChild!=a.firstChild;)a.removeChild(a.lastChild);a.firstChild.data=b}else{for(var c;c=a.firstChild;)a.removeChild(c);a.appendChild(ib(a).createTextNode(b))}},vb={SCRIPT:1,STYLE:1,HEAD:1,IFRAME:1,OBJECT:1},wb={IMG:" ",BR:"\n"},xb=function(a){var b=
-a.getAttributeNode("tabindex");return b&&b.specified?(a=a.tabIndex,"number"==typeof a&&0<=a&&32768>a):n},yb=function(a,b,c){if(!(a.nodeName in vb))if(3==a.nodeType)c?b.push((""+a.nodeValue).replace(/(\r\n|\r|\n)/g,"")):b.push(a.nodeValue);else if(a.nodeName in wb)b.push(wb[a.nodeName]);else for(a=a.firstChild;a;)yb(a,b,c),a=a.nextSibling},hb=function(a){this.C=a||p.document||document};o=hb.prototype;o.Ia=jb;o.a=function(a){return q(a)?this.C.getElementById(a):a};
-o.k=function(a,b,c){return ob(this.C,arguments)};o.createElement=function(a){return this.C.createElement(a)};o.createTextNode=function(a){return this.C.createTextNode(a)};o.appendChild=function(a,b){a.appendChild(b)};o.contains=tb;var zb=function(a){zb[" "](a);return a};zb[" "]=function(){};var Ab=!y||bb(),Bb=!y||bb(),Cb=y&&!C("8");!B||C("528");A&&C("1.9b")||y&&C("8")||Oa&&C("9.5")||B&&C("528");!A||C("8");var Db=function(){};Db.prototype.bb=n;Db.prototype.I=function(){this.bb||(this.bb=j,this.f())};Db.prototype.f=function(){this.gc&&Eb.apply(k,this.gc)};var Fb=function(a){a&&"function"==typeof a.I&&a.I()},Eb=function(a){for(var b=0,c=arguments.length;b<c;++b){var d=arguments[b];ea(d)?Eb.apply(k,d):Fb(d)}};var E=function(a,b){this.type=a;this.currentTarget=this.target=b};v(E,Db);o=E.prototype;o.f=function(){delete this.type;delete this.target;delete this.currentTarget};o.ba=n;o.ua=j;o.stopPropagation=function(){this.ba=j};o.preventDefault=function(){this.ua=n};var F=function(a,b){a&&this.sa(a,b)};v(F,E);var Gb=[1,4,2];o=F.prototype;o.target=k;o.relatedTarget=k;o.offsetX=0;o.offsetY=0;o.clientX=0;o.clientY=0;o.screenX=0;o.screenY=0;o.button=0;o.keyCode=0;o.charCode=0;o.ctrlKey=n;o.altKey=n;o.shiftKey=n;o.metaKey=n;o.cb=n;o.L=k;
-o.sa=function(a,b){var c=this.type=a.type;E.call(this,c);this.target=a.target||a.srcElement;this.currentTarget=b;var d=a.relatedTarget;if(d){if(A){var f;a:{try{zb(d.nodeName);f=j;break a}catch(g){}f=n}f||(d=k)}}else"mouseover"==c?d=a.fromElement:"mouseout"==c&&(d=a.toElement);this.relatedTarget=d;this.offsetX=B||a.offsetX!==h?a.offsetX:a.layerX;this.offsetY=B||a.offsetY!==h?a.offsetY:a.layerY;this.clientX=a.clientX!==h?a.clientX:a.pageX;this.clientY=a.clientY!==h?a.clientY:a.pageY;this.screenX=a.screenX||
-0;this.screenY=a.screenY||0;this.button=a.button;this.keyCode=a.keyCode||0;this.charCode=a.charCode||("keypress"==c?a.keyCode:0);this.ctrlKey=a.ctrlKey;this.altKey=a.altKey;this.shiftKey=a.shiftKey;this.metaKey=a.metaKey;this.cb=Ra?a.metaKey:a.ctrlKey;this.state=a.state;this.L=a;delete this.ua;delete this.ba};var Hb=function(a){return Ab?0==a.L.button:"click"==a.type?j:!!(a.L.button&Gb[0])};
-F.prototype.stopPropagation=function(){F.d.stopPropagation.call(this);this.L.stopPropagation?this.L.stopPropagation():this.L.cancelBubble=j};F.prototype.preventDefault=function(){F.d.preventDefault.call(this);var a=this.L;if(a.preventDefault)a.preventDefault();else if(a.returnValue=n,Cb)try{if(a.ctrlKey||112<=a.keyCode&&123>=a.keyCode)a.keyCode=-1}catch(b){}};F.prototype.f=function(){F.d.f.call(this);this.relatedTarget=this.currentTarget=this.target=this.L=k};var Ib=function(){},Jb=0;o=Ib.prototype;o.key=0;o.aa=n;o.Eb=n;o.sa=function(a,b,c,d,f,g){r(a)?this.Cb=j:a&&a.handleEvent&&r(a.handleEvent)?this.Cb=n:e(Error("Invalid listener argument"));this.fa=a;this.vb=b;this.src=c;this.type=d;this.capture=!!f;this.Da=g;this.Eb=n;this.key=++Jb;this.aa=n};o.handleEvent=function(a){return this.Cb?this.fa.call(this.Da||this.src,a):this.fa.handleEvent.call(this.fa,a)};var Kb={},G={},H={},Lb={},I=function(a,b,c,d,f){if(b){if(da(b)){for(var g=0;g<b.length;g++)I(a,b[g],c,d,f);return k}var d=!!d,i=G;b in i||(i[b]={G:0,w:0});i=i[b];d in i||(i[d]={G:0,w:0},i.G++);var i=i[d],l=u(a),m;i.w++;if(i[l]){m=i[l];for(g=0;g<m.length;g++)if(i=m[g],i.fa==c&&i.Da==f){if(i.aa)break;return m[g].key}}else m=i[l]=[],i.G++;g=Mb();g.src=a;i=new Ib;i.sa(c,g,a,b,d,f);c=i.key;g.key=c;m.push(i);Kb[c]=i;H[l]||(H[l]=[]);H[l].push(i);a.addEventListener?(a==p||!a.ub)&&a.addEventListener(b,g,d):
-a.attachEvent(b in Lb?Lb[b]:Lb[b]="on"+b,g);return c}e(Error("Invalid event type"))},Mb=function(){var a=Nb,b=Bb?function(c){return a.call(b.src,b.key,c)}:function(c){c=a.call(b.src,b.key,c);if(!c)return c};return b},Ob=function(a,b,c,d,f){if(da(b))for(var g=0;g<b.length;g++)Ob(a,b[g],c,d,f);else if(d=!!d,a=Pb(a,b,d))for(g=0;g<a.length;g++)if(a[g].fa==c&&a[g].capture==d&&a[g].Da==f){J(a[g].key);break}},J=function(a){if(!Kb[a])return n;var b=Kb[a];if(b.aa)return n;var c=b.src,d=b.type,f=b.vb,g=b.capture;
-c.removeEventListener?(c==p||!c.ub)&&c.removeEventListener(d,f,g):c.detachEvent&&c.detachEvent(d in Lb?Lb[d]:Lb[d]="on"+d,f);c=u(c);f=G[d][g][c];if(H[c]){var i=H[c];xa(i,b);0==i.length&&delete H[c]}b.aa=j;f.Ab=j;Qb(d,g,c,f);delete Kb[a];return j},Qb=function(a,b,c,d){if(!d.Ja&&d.Ab){for(var f=0,g=0;f<d.length;f++)d[f].aa?d[f].vb.src=k:(f!=g&&(d[g]=d[f]),g++);d.length=g;d.Ab=n;0==g&&(delete G[a][b][c],G[a][b].G--,0==G[a][b].G&&(delete G[a][b],G[a].G--),0==G[a].G&&delete G[a])}},Rb=function(a){var b,
-c=0,d=b==k;b=!!b;if(a==k)Ca(H,function(a){for(var f=a.length-1;0<=f;f--){var g=a[f];if(d||b==g.capture)J(g.key),c++}});else if(a=u(a),H[a])for(var a=H[a],f=a.length-1;0<=f;f--){var g=a[f];if(d||b==g.capture)J(g.key),c++}},Pb=function(a,b,c){var d=G;return b in d&&(d=d[b],c in d&&(d=d[c],a=u(a),d[a]))?d[a]:k},Tb=function(a,b,c,d,f){var g=1,b=u(b);if(a[b]){a.w--;a=a[b];a.Ja?a.Ja++:a.Ja=1;try{for(var i=a.length,l=0;l<i;l++){var m=a[l];m&&!m.aa&&(g&=Sb(m,f)!==n)}}finally{a.Ja--,Qb(c,d,b,a)}}return Boolean(g)},
-Sb=function(a,b){var c=a.handleEvent(b);a.Eb&&J(a.key);return c},Nb=function(a,b){if(!Kb[a])return j;var c=Kb[a],d=c.type,f=G;if(!(d in f))return j;var f=f[d],g,i;if(!Bb){var l;if(!(l=b))a:{l=["window","event"];for(var m=p;g=l.shift();)if(m[g]!=k)m=m[g];else{l=k;break a}l=m}g=l;l=j in f;m=n in f;if(l){if(0>g.keyCode||g.returnValue!=h)return j;a:{var s=n;if(0==g.keyCode)try{g.keyCode=-1;break a}catch(z){s=j}if(s||g.returnValue==h)g.returnValue=j}}s=new F;s.sa(g,this);g=j;try{if(l){for(var t=[],Qa=
-s.currentTarget;Qa;Qa=Qa.parentNode)t.push(Qa);i=f[j];i.w=i.G;for(var S=t.length-1;!s.ba&&0<=S&&i.w;S--)s.currentTarget=t[S],g&=Tb(i,t[S],d,j,s);if(m){i=f[n];i.w=i.G;for(S=0;!s.ba&&S<t.length&&i.w;S++)s.currentTarget=t[S],g&=Tb(i,t[S],d,n,s)}}else g=Sb(c,s)}finally{t&&(t.length=0),s.I()}return g}d=new F(b,this);try{g=Sb(c,d)}finally{d.I()}return g};var K=function(a){this.Fb=a;this.La=[]};v(K,Db);var Ub=[],L=function(a,b,c,d){da(c)||(Ub[0]=c,c=Ub);for(var f=0;f<c.length;f++)a.La.push(I(b,c[f],d||a,n,a.Fb||a));return a},M=function(a,b,c,d,f,g){if(da(c))for(var i=0;i<c.length;i++)M(a,b,c[i],d,f,g);else{a:{d=d||a;g=g||a.Fb||a;f=!!f;if(b=Pb(b,c,f))for(c=0;c<b.length;c++)if(!b[c].aa&&b[c].fa==d&&b[c].capture==f&&b[c].Da==g){b=b[c];break a}b=k}b&&(b=b.key,J(b),xa(a.La,b))}return a},Vb=function(a){ua(a.La,J);a.La.length=0};
-K.prototype.f=function(){K.d.f.call(this);Vb(this)};K.prototype.handleEvent=function(){e(Error("EventHandler.handleEvent not implemented"))};var Wb=function(){};v(Wb,Db);o=Wb.prototype;o.ub=j;o.Ca=k;o.eb=function(a){this.Ca=a};o.addEventListener=function(a,b,c,d){I(this,a,b,c,d)};o.removeEventListener=function(a,b,c,d){Ob(this,a,b,c,d)};
-o.dispatchEvent=function(a){var b=a.type||a,c=G;if(b in c){if(q(a))a=new E(a,this);else if(a instanceof E)a.target=a.target||this;else{var d=a,a=new E(b,this);Ga(a,d)}var d=1,f,c=c[b],b=j in c,g;if(b){f=[];for(g=this;g;g=g.Ca)f.push(g);g=c[j];g.w=g.G;for(var i=f.length-1;!a.ba&&0<=i&&g.w;i--)a.currentTarget=f[i],d&=Tb(g,f[i],a.type,j,a)&&a.ua!=n}if(n in c)if(g=c[n],g.w=g.G,b)for(i=0;!a.ba&&i<f.length&&g.w;i++)a.currentTarget=f[i],d&=Tb(g,f[i],a.type,n,a)&&a.ua!=n;else for(f=this;!a.ba&&f&&g.w;f=f.Ca)a.currentTarget=
-f,d&=Tb(g,f,a.type,n,a)&&a.ua!=n;a=Boolean(d)}else a=j;return a};o.f=function(){Wb.d.f.call(this);Rb(this);this.Ca=k};var Xb=function(a,b){a.style.display=b?"":"none"},Yb=A?"MozUserSelect":B?"WebkitUserSelect":k,Zb=function(a,b,c){c=!c?a.getElementsByTagName("*"):k;if(Yb){if(b=b?"none":"",a.style[Yb]=b,c)for(var a=0,d;d=c[a];a++)d.style[Yb]=b}else if(y||Oa)if(b=b?"on":"",a.setAttribute("unselectable",b),c)for(a=0;d=c[a];a++)d.setAttribute("unselectable",b)};var $b=function(){};ba($b);$b.prototype.Yb=0;$b.P();var N=function(a){this.m=a||jb();this.ra=ac};v(N,Wb);N.prototype.Xb=$b.P();var ac=k,bc=function(a,b){switch(a){case 1:return b?"disable":"enable";case 2:return b?"highlight":"unhighlight";case 4:return b?"activate":"deactivate";case 8:return b?"select":"unselect";case 16:return b?"check":"uncheck";case 32:return b?"focus":"blur";case 64:return b?"open":"close"}e(Error("Invalid component state"))};o=N.prototype;o.ga=k;o.e=n;o.c=k;o.ra=k;o.q=k;o.r=k;o.t=k;o.mb=n;
-var cc=function(a){return a.ga||(a.ga=":"+(a.Xb.Yb++).toString(36))},dc=function(a,b){if(a.q&&a.q.t){var c=a.q.t,d=a.ga;d in c&&delete c[d];Da(a.q.t,b,a)}a.ga=b};N.prototype.a=function(){return this.c};var ec=function(a){return a.da||(a.da=new K(a))},gc=function(a,b){a==b&&e(Error("Unable to set parent component"));b&&a.q&&a.ga&&fc(a.q,a.ga)&&a.q!=b&&e(Error("Unable to set parent component"));a.q=b;N.d.eb.call(a,b)};o=N.prototype;o.getParent=function(){return this.q};
-o.eb=function(a){this.q&&this.q!=a&&e(Error("Method not supported"));N.d.eb.call(this,a)};o.Ia=function(){return this.m};o.k=function(){this.c=this.m.createElement("div")};o.K=function(a){this.e&&e(Error("Component already rendered"));if(a&&this.$(a)){this.mb=j;if(!this.m||this.m.C!=ib(a))this.m=jb(a);this.Va(a);this.s()}else e(Error("Invalid element to decorate"))};o.$=function(){return j};o.Va=function(a){this.c=a};o.s=function(){this.e=j;hc(this,function(a){!a.e&&a.a()&&a.s()})};
-o.U=function(){hc(this,function(a){a.e&&a.U()});this.da&&Vb(this.da);this.e=n};o.f=function(){N.d.f.call(this);this.e&&this.U();this.da&&(this.da.I(),delete this.da);hc(this,function(a){a.I()});!this.mb&&this.c&&rb(this.c);this.q=this.c=this.t=this.r=k};o.Ba=function(a,b){this.Sa(a,ic(this),b)};
-o.Sa=function(a,b,c){a.e&&(c||!this.e)&&e(Error("Component already rendered"));(0>b||b>ic(this))&&e(Error("Child component index out of bounds"));if(!this.t||!this.r)this.t={},this.r=[];a.getParent()==this?(this.t[cc(a)]=a,xa(this.r,a)):Da(this.t,cc(a),a);gc(a,this);Ba(this.r,b,0,a);a.e&&this.e&&a.getParent()==this?(c=this.A(),c.insertBefore(a.a(),c.childNodes[b]||k)):c?(this.c||this.k(),c=O(this,b+1),b=this.A(),c=c?c.c:k,a.e&&e(Error("Component already rendered")),a.c||a.k(),b?b.insertBefore(a.c,
-c||k):a.m.C.body.appendChild(a.c),(!a.q||a.q.e)&&a.s()):this.e&&!a.e&&a.c&&a.s()};o.A=function(){return this.c};var jc=function(a){if(a.ra==k){var b;a:{b=a.e?a.c:a.m.C.body;var c=ib(b);if(c.defaultView&&c.defaultView.getComputedStyle&&(b=c.defaultView.getComputedStyle(b,k))){b=b.direction||b.getPropertyValue("direction");break a}b=""}a.ra="rtl"==(b||((a.e?a.c:a.m.C.body).currentStyle?(a.e?a.c:a.m.C.body).currentStyle.direction:k)||(a.e?a.c:a.m.C.body).style&&(a.e?a.c:a.m.C.body).style.direction)}return a.ra};
-N.prototype.oa=function(a){this.e&&e(Error("Component already rendered"));this.ra=a};var ic=function(a){return a.r?a.r.length:0},fc=function(a,b){return a.t&&b?(b in a.t?a.t[b]:h)||k:k},O=function(a,b){return a.r?a.r[b]||k:k},hc=function(a,b,c){a.r&&ua(a.r,b,c)},kc=function(a,b){return a.r&&b?ta(a.r,b):-1};
-N.prototype.removeChild=function(a,b){if(a){var c=q(a)?a:cc(a),a=fc(this,c);if(c&&a){var d=this.t;c in d&&delete d[c];xa(this.r,a);b&&(a.U(),a.c&&rb(a.c));gc(a,k)}}a||e(Error("Child is not in parent component"));return a};var lc=function(a,b){a.setAttribute("role",b);a.lc=b};var nc=function(a,b,c,d,f){if(!y&&(!B||!C("525")))return j;if(Ra&&f)return mc(a);if(f&&!d||!c&&(17==b||18==b)||y&&d&&b==a)return n;switch(a){case 13:return!(y&&bb());case 27:return!B}return mc(a)},mc=function(a){if(48<=a&&57>=a||96<=a&&106>=a||65<=a&&90>=a||B&&0==a)return j;switch(a){case 32:case 63:case 107:case 109:case 110:case 111:case 186:case 59:case 189:case 187:case 188:case 190:case 191:case 192:case 222:case 219:case 220:case 221:return j;default:return n}};var P=function(a,b){a&&oc(this,a,b)};v(P,Wb);o=P.prototype;o.c=k;o.Fa=k;o.Xa=k;o.Ga=k;o.R=-1;o.Q=-1;
-var pc={3:13,12:144,63232:38,63233:40,63234:37,63235:39,63236:112,63237:113,63238:114,63239:115,63240:116,63241:117,63242:118,63243:119,63244:120,63245:121,63246:122,63247:123,63248:44,63272:46,63273:36,63275:35,63276:33,63277:34,63289:144,63302:45},qc={Up:38,Down:40,Left:37,Right:39,Enter:13,F1:112,F2:113,F3:114,F4:115,F5:116,F6:117,F7:118,F8:119,F9:120,F10:121,F11:122,F12:123,"U+007F":46,Home:36,End:35,PageUp:33,PageDown:34,Insert:45},rc={61:187,59:186},sc=y||B&&C("525");
-P.prototype.Qb=function(a){if(B&&(17==this.R&&!a.ctrlKey||18==this.R&&!a.altKey))this.Q=this.R=-1;sc&&!nc(a.keyCode,this.R,a.shiftKey,a.ctrlKey,a.altKey)?this.handleEvent(a):this.Q=A&&a.keyCode in rc?rc[a.keyCode]:a.keyCode};P.prototype.Rb=function(){this.Q=this.R=-1};
-P.prototype.handleEvent=function(a){var b=a.L,c,d;y&&"keypress"==a.type?(c=this.Q,d=13!=c&&27!=c?b.keyCode:0):B&&"keypress"==a.type?(c=this.Q,d=0<=b.charCode&&63232>b.charCode&&mc(c)?b.charCode:0):Oa?(c=this.Q,d=mc(c)?b.keyCode:0):(c=b.keyCode||this.Q,d=b.charCode||0,Ra&&63==d&&!c&&(c=191));var f=c,g=b.keyIdentifier;c?63232<=c&&c in pc?f=pc[c]:25==c&&a.shiftKey&&(f=9):g&&g in qc&&(f=qc[g]);a=f==this.R;this.R=f;b=new tc(f,d,a,b);try{this.dispatchEvent(b)}finally{b.I()}};P.prototype.a=function(){return this.c};
-var oc=function(a,b,c){a.Ga&&a.detach();a.c=b;a.Fa=I(a.c,"keypress",a,c);a.Xa=I(a.c,"keydown",a.Qb,c,a);a.Ga=I(a.c,"keyup",a.Rb,c,a)};P.prototype.detach=function(){this.Fa&&(J(this.Fa),J(this.Xa),J(this.Ga),this.Ga=this.Xa=this.Fa=k);this.c=k;this.Q=this.R=-1};P.prototype.f=function(){P.d.f.call(this);this.detach()};var tc=function(a,b,c,d){d&&this.sa(d,h);this.type="key";this.keyCode=a;this.charCode=b;this.repeat=c};v(tc,F);var vc=function(a,b){a||e(Error("Invalid class name "+a));r(b)||e(Error("Invalid decorator function "+b));uc[a]=b},wc={},uc={};var Q=function(){},xc;ba(Q);o=Q.prototype;o.M=function(){};o.k=function(a){var b=a.Ia().k("div",this.ta(a).join(" "),a.xa);yc(a,b);return b};o.A=function(a){return a};o.qa=function(a,b,c){if(a=a.a?a.a():a)if(y&&!C("7")){var d=zc(fb(a),b);d.push(b);ia(c?D:gb,a).apply(k,d)}else c?D(a,b):gb(a,b)};o.$=function(){return j};
-o.K=function(a,b){b.id&&dc(a,b.id);var c=this.A(b);a.xa=c&&c.firstChild?c.firstChild.nextSibling?za(c.childNodes):c.firstChild:k;var d=0,f=this.n(),g=this.n(),i=n,l=n,c=n,m=fb(b);ua(m,function(a){if(!i&&a==f)i=j,g==f&&(l=j);else if(!l&&a==g)l=j;else{var b=d;this.sb||(this.Ea||Ac(this),this.sb=Ea(this.Ea));a=parseInt(this.sb[a],10);d=b|(isNaN(a)?0:a)}},this);a.g=d;i||(m.push(f),g==f&&(l=j));l||m.push(g);var s=a.D;s&&m.push.apply(m,s);if(y&&!C("7")){var z=zc(m);0<z.length&&(m.push.apply(m,z),c=j)}if(!i||
-!l||s||c)b.className=m.join(" ");yc(a,b);return b};o.Ma=function(a){jc(a)&&this.oa(a.a(),j);a.isEnabled()&&this.la(a,a.H())};var yc=function(a,b){w(a);w(b);a.isEnabled()||Bc(b,1,j);a.g&8&&Bc(b,8,j);a.o&16&&Bc(b,16,!!(a.g&16));a.o&64&&Bc(b,64,!!(a.g&64))};o=Q.prototype;o.ya=function(a,b){Zb(a,!b,!y&&!Oa)};o.oa=function(a,b){this.qa(a,this.n()+"-rtl",b)};o.T=function(a){var b;return a.o&32&&(b=a.j())?xb(b):n};
-o.la=function(a,b){var c;if(a.o&32&&(c=a.j())){if(!b&&a.g&32){try{c.blur()}catch(d){}a.g&32&&a.ma(k)}xb(c)!=b&&(b?c.tabIndex=0:(c.tabIndex=-1,c.removeAttribute("tabIndex")))}};o.ja=function(a,b){Xb(a,b)};o.v=function(a,b,c){var d=a.a();if(d){var f=Cc(this,b);f&&this.qa(a,f,c);Bc(d,b,c)}};var Bc=function(a,b,c){xc||(xc={1:"disabled",8:"selected",16:"checked",64:"expanded"});(b=xc[b])&&a.setAttribute("aria-"+b,c)};Q.prototype.j=function(a){return a.a()};Q.prototype.n=function(){return"goog-control"};
-Q.prototype.ta=function(a){var b=this.n(),c=[b],d=this.n();d!=b&&c.push(d);b=a.g;for(d=[];b;){var f=b&-b;d.push(Cc(this,f));b&=~f}c.push.apply(c,d);(a=a.D)&&c.push.apply(c,a);y&&!C("7")&&c.push.apply(c,zc(c));return c};
-var zc=function(a,b){var c=[];b&&(a=a.concat([b]));ua([],function(d){va(d,ia(wa,a))&&(!b||wa(d,b))&&c.push(d.join("_"))});return c},Cc=function(a,b){a.Ea||Ac(a);return a.Ea[b]},Ac=function(a){var b=a.n();a.Ea={1:b+"-disabled",2:b+"-hover",4:b+"-active",8:b+"-selected",16:b+"-checked",32:b+"-focused",64:b+"-open"}};var R=function(a,b,c){N.call(this,c);if(!b){for(var b=this.constructor,d;b;){d=u(b);if(d=wc[d])break;b=b.d?b.d.constructor:k}b=d?r(d.P)?d.P():new d:k}this.b=b;this.xa=a};v(R,N);o=R.prototype;o.xa=k;o.g=0;o.o=39;o.Wb=255;o.V=0;o.p=j;o.D=k;o.ha=j;o.wa=n;o.ob=k;o.pb=function(){return this.ha};o.Pa=function(a){this.e&&a!=this.ha&&Dc(this,a);this.ha=a};o.j=function(){return this.b.j(this)};o.za=function(){return this.u||(this.u=new P)};o.zb=function(){return this.b};
-o.qa=function(a,b){b?a&&(this.D?wa(this.D,a)||this.D.push(a):this.D=[a],this.b.qa(this,a,j)):a&&this.D&&(xa(this.D,a),0==this.D.length&&(this.D=k),this.b.qa(this,a,n))};o.k=function(){var a=this.b.k(this);this.c=a;var b=this.ob||this.b.M();b&&lc(a,b);this.wa||this.b.ya(a,n);this.H()||this.b.ja(a,n)};o.A=function(){return this.b.A(this.a())};o.$=function(a){return this.b.$(a)};o.Va=function(a){this.c=a=this.b.K(this,a);var b=this.ob||this.b.M();b&&lc(a,b);this.wa||this.b.ya(a,n);this.p="none"!=a.style.display};
-o.s=function(){R.d.s.call(this);this.b.Ma(this);if(this.o&-2&&(this.pb()&&Dc(this,j),this.o&32)){var a=this.j();if(a){var b=this.za();oc(b,a);L(L(L(ec(this),b,"key",this.N),a,"focus",this.na),a,"blur",this.ma)}}};var Dc=function(a,b){var c=ec(a),d=a.a();b?(L(L(L(L(c,d,"mouseover",a.$a),d,"mousedown",a.ka),d,"mouseup",a.ab),d,"mouseout",a.Za),y&&L(c,d,"dblclick",a.wb)):(M(M(M(M(c,d,"mouseover",a.$a),d,"mousedown",a.ka),d,"mouseup",a.ab),d,"mouseout",a.Za),y&&M(c,d,"dblclick",a.wb))};o=R.prototype;
+a.prototype=new c;a.prototype.constructor=a};var ja=function(a){this.stack=Error().stack||"";a&&(this.message=""+a)};v(ja,Error);ja.prototype.name="CustomError";var ka=function(a,b){for(var c=1;c<arguments.length;c++)var d=(""+arguments[c]).replace(/\$/g,"$$$$"),a=a.replace(/\%s/,d);return a},la=function(a){return a.replace(/^[\s\xa0]+|[\s\xa0]+$/g,"")},ra=function(a){if(!ma.test(a))return a;-1!=a.indexOf("&")&&(a=a.replace(na,"&"));-1!=a.indexOf("<")&&(a=a.replace(oa,"<"));-1!=a.indexOf(">")&&(a=a.replace(pa,">"));-1!=a.indexOf('"')&&(a=a.replace(qa,"""));return a},na=/&/g,oa=/</g,pa=/>/g,qa=/\"/g,ma=/[&<>\"]/;var sa=function(a,b){b.unshift(a);ja.call(this,ka.apply(k,b));b.shift()};v(sa,ja);sa.prototype.name="AssertionError";var w=function(a,b,c){if(!a){var d=Array.prototype.slice.call(arguments,2),f="Assertion failed";if(b)var f=f+(": "+b),g=d;e(new sa(""+f,g||[]))}};var x=Array.prototype,ta=x.indexOf?function(a,b,c){w(a.length!=k);return x.indexOf.call(a,b,c)}:function(a,b,c){c=c==k?0:0>c?Math.max(0,a.length+c):c;if(q(a))return!q(b)||1!=b.length?-1:a.indexOf(b,c);for(;c<a.length;c++)if(c in a&&a[c]===b)return c;return-1},ua=x.forEach?function(a,b,c){w(a.length!=k);x.forEach.call(a,b,c)}:function(a,b,c){for(var d=a.length,f=q(a)?a.split(""):a,g=0;g<d;g++)g in f&&b.call(c,f[g],g,a)},va=x.filter?function(a,b,c){w(a.length!=k);return x.filter.call(a,b,c)}:function(a,
+b,c){for(var d=a.length,f=[],g=0,i=q(a)?a.split(""):a,l=0;l<d;l++)if(l in i){var m=i[l];b.call(c,m,l,a)&&(f[g++]=m)}return f},wa=x.every?function(a,b,c){w(a.length!=k);return x.every.call(a,b,c)}:function(a,b,c){for(var d=a.length,f=q(a)?a.split(""):a,g=0;g<d;g++)if(g in f&&!b.call(c,f[g],g,a))return n;return j},xa=function(a,b){return 0<=ta(a,b)},ya=function(a,b){var c=ta(a,b);0<=c&&(w(a.length!=k),x.splice.call(a,c,1))},za=function(a){return x.concat.apply(x,arguments)},Aa=function(a){if(da(a))return za(a);
+for(var b=[],c=0,d=a.length;c<d;c++)b[c]=a[c];return b},Ca=function(a,b,c,d){w(a.length!=k);x.splice.apply(a,Ba(arguments,1))},Ba=function(a,b,c){w(a.length!=k);return 2>=arguments.length?x.slice.call(a,b):x.slice.call(a,b,c)};var Da=function(a,b){for(var c in a)b.call(h,a[c],c,a)},Ea=function(a,b,c){b in a&&e(Error('The object already contains the key "'+b+'"'));a[b]=c},Fa=function(a){var b={},c;for(c in a)b[a[c]]=c;return b},Ga="constructor,hasOwnProperty,isPrototypeOf,propertyIsEnumerable,toLocaleString,toString,valueOf".split(","),Ha=function(a,b){for(var c,d,f=1;f<arguments.length;f++){d=arguments[f];for(c in d)a[c]=d[c];for(var g=0;g<Ga.length;g++)c=Ga[g],Object.prototype.hasOwnProperty.call(d,c)&&(a[c]=d[c])}};var Ia,Ja,Ka,La,Ma=function(){return p.navigator?p.navigator.userAgent:k};La=Ka=Ja=Ia=n;var Na;if(Na=Ma()){var Oa=p.navigator;Ia=0==Na.indexOf("Opera");Ja=!Ia&&-1!=Na.indexOf("MSIE");Ka=!Ia&&-1!=Na.indexOf("WebKit");La=!Ia&&!Ka&&"Gecko"==Oa.product}var Pa=Ia,y=Ja,z=La,A=Ka,Ra=p.navigator,Sa=-1!=(Ra&&Ra.platform||"").indexOf("Mac"),Ta;
+a:{var Ua="",Va;if(Pa&&p.opera)var Wa=p.opera.version,Ua="function"==typeof Wa?Wa():Wa;else if(z?Va=/rv\:([^\);]+)(\)|;)/:y?Va=/MSIE\s+([^\);]+)(\)|;)/:A&&(Va=/WebKit\/(\S+)/),Va)var Xa=Va.exec(Ma()),Ua=Xa?Xa[1]:"";if(y){var Ya,Za=p.document;Ya=Za?Za.documentMode:h;if(Ya>parseFloat(Ua)){Ta=""+Ya;break a}}Ta=Ua}
+var $a=Ta,ab={},C=function(a){var b;if(!(b=ab[a])){b=0;for(var c=la(""+$a).split("."),d=la(""+a).split("."),f=Math.max(c.length,d.length),g=0;0==b&&g<f;g++){var i=c[g]||"",l=d[g]||"",m=RegExp("(\\d*)(\\D*)","g"),s=RegExp("(\\d*)(\\D*)","g");do{var B=m.exec(i)||["","",""],t=s.exec(l)||["","",""];if(0==B[0].length&&0==t[0].length)break;b=((0==B[1].length?0:parseInt(B[1],10))<(0==t[1].length?0:parseInt(t[1],10))?-1:(0==B[1].length?0:parseInt(B[1],10))>(0==t[1].length?0:parseInt(t[1],10))?1:0)||((0==
+B[2].length)<(0==t[2].length)?-1:(0==B[2].length)>(0==t[2].length)?1:0)||(B[2]<t[2]?-1:B[2]>t[2]?1:0)}while(0==b)}b=ab[a]=0<=b}return b},bb={},cb=function(){return bb[9]||(bb[9]=y&&!!document.documentMode&&9<=document.documentMode)};var db,eb=!y||cb();!z&&!y||y&&cb()||z&&C("1.9.1");var fb=y&&!C("9");var gb=function(a){a=a.className;return q(a)&&a.match(/\S+/g)||[]},D=function(a,b){for(var c=gb(a),d=Ba(arguments,1),f=c.length+d.length,g=c,i=0;i<d.length;i++)xa(g,d[i])||g.push(d[i]);a.className=c.join(" ");return c.length==f},ib=function(a,b){var c=gb(a),d=Ba(arguments,1),f=hb(c,d);a.className=f.join(" ");return f.length==c.length-d.length},hb=function(a,b){return va(a,function(a){return!xa(b,a)})};var lb=function(a){return a?new jb(kb(a)):db||(db=new jb)},mb=function(a){return q(a)?document.getElementById(a):a},nb=function(a,b,c){c=c||document;a=a&&"*"!=a?a.toUpperCase():"";if(c.querySelectorAll&&c.querySelector&&(!A||"CSS1Compat"==document.compatMode||C("528"))&&(a||b))return c.querySelectorAll(a+(b?"."+b:""));if(b&&c.getElementsByClassName){c=c.getElementsByClassName(b);if(a){for(var d={},f=0,g=0,i;i=c[g];g++)a==i.nodeName&&(d[f++]=i);d.length=f;return d}return c}c=c.getElementsByTagName(a||
+"*");if(b){d={};for(g=f=0;i=c[g];g++)a=i.className,"function"==typeof a.split&&xa(a.split(/\s+/),b)&&(d[f++]=i);d.length=f;return d}return c},pb=function(a,b){Da(b,function(b,d){"style"==d?a.style.cssText=b:"class"==d?a.className=b:"for"==d?a.htmlFor=b:d in ob?a.setAttribute(ob[d],b):0==d.lastIndexOf("aria-",0)?a.setAttribute(d,b):a[d]=b})},ob={cellpadding:"cellPadding",cellspacing:"cellSpacing",colspan:"colSpan",rowspan:"rowSpan",valign:"vAlign",height:"height",width:"width",usemap:"useMap",frameborder:"frameBorder",
+maxlength:"maxLength",type:"type"},rb=function(a,b,c){return qb(document,arguments)},qb=function(a,b){var c=b[0],d=b[1];if(!eb&&d&&(d.name||d.type)){c=["<",c];d.name&&c.push(' name="',ra(d.name),'"');if(d.type){c.push(' type="',ra(d.type),'"');var f={};Ha(f,d);d=f;delete d.type}c.push(">");c=c.join("")}c=a.createElement(c);d&&(q(d)?c.className=d:da(d)?D.apply(k,[c].concat(d)):pb(c,d));2<b.length&&sb(a,c,b);return c},sb=function(a,b,c){function d(c){c&&b.appendChild(q(c)?a.createTextNode(c):c)}for(var f=
+2;f<c.length;f++){var g=c[f];if(ea(g)&&!(fa(g)&&0<g.nodeType)){var i;a:{if(g&&"number"==typeof g.length){if(fa(g)){i="function"==typeof g.item||"string"==typeof g.item;break a}if(r(g)){i="function"==typeof g.item;break a}}i=n}ua(i?Aa(g):g,d)}else d(g)}},tb=function(a){a&&a.parentNode&&a.parentNode.removeChild(a)},ub=function(a){for(;a&&1!=a.nodeType;)a=a.nextSibling;return a},vb=function(a,b){if(a.contains&&1==b.nodeType)return a==b||a.contains(b);if("undefined"!=typeof a.compareDocumentPosition)return a==
+b||Boolean(a.compareDocumentPosition(b)&16);for(;b&&a!=b;)b=b.parentNode;return b==a},kb=function(a){return 9==a.nodeType?a:a.ownerDocument||a.document},wb=function(a,b){if("textContent"in a)a.textContent=b;else if(a.firstChild&&3==a.firstChild.nodeType){for(;a.lastChild!=a.firstChild;)a.removeChild(a.lastChild);a.firstChild.data=b}else{for(var c;c=a.firstChild;)a.removeChild(c);a.appendChild(kb(a).createTextNode(b))}},xb={SCRIPT:1,STYLE:1,HEAD:1,IFRAME:1,OBJECT:1},yb={IMG:" ",BR:"\n"},zb=function(a){var b=
+a.getAttributeNode("tabindex");return b&&b.specified?(a=a.tabIndex,"number"==typeof a&&0<=a&&32768>a):n},Ab=function(a,b,c){if(!(a.nodeName in xb))if(3==a.nodeType)c?b.push((""+a.nodeValue).replace(/(\r\n|\r|\n)/g,"")):b.push(a.nodeValue);else if(a.nodeName in yb)b.push(yb[a.nodeName]);else for(a=a.firstChild;a;)Ab(a,b,c),a=a.nextSibling},jb=function(a){this.C=a||p.document||document};o=jb.prototype;o.Ia=lb;o.a=function(a){return q(a)?this.C.getElementById(a):a};
+o.k=function(a,b,c){return qb(this.C,arguments)};o.createElement=function(a){return this.C.createElement(a)};o.createTextNode=function(a){return this.C.createTextNode(a)};o.appendChild=function(a,b){a.appendChild(b)};o.contains=vb;var Bb=function(a){Bb[" "](a);return a};Bb[" "]=function(){};var Cb=!y||cb(),Db=!y||cb(),Eb=y&&!C("8");!A||C("528");z&&C("1.9b")||y&&C("8")||Pa&&C("9.5")||A&&C("528");z&&!C("8")||y&&C("9");var Fb=function(){};Fb.prototype.bb=n;Fb.prototype.I=function(){this.bb||(this.bb=j,this.f())};Fb.prototype.f=function(){this.gc&&Gb.apply(k,this.gc)};var Hb=function(a){a&&"function"==typeof a.I&&a.I()},Gb=function(a){for(var b=0,c=arguments.length;b<c;++b){var d=arguments[b];ea(d)?Gb.apply(k,d):Hb(d)}};var E=function(a,b){this.type=a;this.currentTarget=this.target=b};v(E,Fb);o=E.prototype;o.f=function(){delete this.type;delete this.target;delete this.currentTarget};o.ba=n;o.ua=j;o.stopPropagation=function(){this.ba=j};o.preventDefault=function(){this.ua=n};var F=function(a,b){a&&this.sa(a,b)};v(F,E);var Ib=[1,4,2];o=F.prototype;o.target=k;o.relatedTarget=k;o.offsetX=0;o.offsetY=0;o.clientX=0;o.clientY=0;o.screenX=0;o.screenY=0;o.button=0;o.keyCode=0;o.charCode=0;o.ctrlKey=n;o.altKey=n;o.shiftKey=n;o.metaKey=n;o.cb=n;o.L=k;
+o.sa=function(a,b){var c=this.type=a.type;E.call(this,c);this.target=a.target||a.srcElement;this.currentTarget=b;var d=a.relatedTarget;if(d){if(z){var f;a:{try{Bb(d.nodeName);f=j;break a}catch(g){}f=n}f||(d=k)}}else"mouseover"==c?d=a.fromElement:"mouseout"==c&&(d=a.toElement);this.relatedTarget=d;this.offsetX=A||a.offsetX!==h?a.offsetX:a.layerX;this.offsetY=A||a.offsetY!==h?a.offsetY:a.layerY;this.clientX=a.clientX!==h?a.clientX:a.pageX;this.clientY=a.clientY!==h?a.clientY:a.pageY;this.screenX=a.screenX||
+0;this.screenY=a.screenY||0;this.button=a.button;this.keyCode=a.keyCode||0;this.charCode=a.charCode||("keypress"==c?a.keyCode:0);this.ctrlKey=a.ctrlKey;this.altKey=a.altKey;this.shiftKey=a.shiftKey;this.metaKey=a.metaKey;this.cb=Sa?a.metaKey:a.ctrlKey;this.state=a.state;this.L=a;delete this.ua;delete this.ba};var Jb=function(a){return Cb?0==a.L.button:"click"==a.type?j:!!(a.L.button&Ib[0])};
+F.prototype.stopPropagation=function(){F.d.stopPropagation.call(this);this.L.stopPropagation?this.L.stopPropagation():this.L.cancelBubble=j};F.prototype.preventDefault=function(){F.d.preventDefault.call(this);var a=this.L;if(a.preventDefault)a.preventDefault();else if(a.returnValue=n,Eb)try{if(a.ctrlKey||112<=a.keyCode&&123>=a.keyCode)a.keyCode=-1}catch(b){}};F.prototype.f=function(){F.d.f.call(this);this.relatedTarget=this.currentTarget=this.target=this.L=k};var Kb=function(){},Lb=0;o=Kb.prototype;o.key=0;o.aa=n;o.Eb=n;o.sa=function(a,b,c,d,f,g){r(a)?this.Cb=j:a&&a.handleEvent&&r(a.handleEvent)?this.Cb=n:e(Error("Invalid listener argument"));this.fa=a;this.vb=b;this.src=c;this.type=d;this.capture=!!f;this.Da=g;this.Eb=n;this.key=++Lb;this.aa=n};o.handleEvent=function(a){return this.Cb?this.fa.call(this.Da||this.src,a):this.fa.handleEvent.call(this.fa,a)};var Mb={},G={},H={},Nb={},I=function(a,b,c,d,f){if(b){if(da(b)){for(var g=0;g<b.length;g++)I(a,b[g],c,d,f);return k}var d=!!d,i=G;b in i||(i[b]={G:0,w:0});i=i[b];d in i||(i[d]={G:0,w:0},i.G++);var i=i[d],l=u(a),m;i.w++;if(i[l]){m=i[l];for(g=0;g<m.length;g++)if(i=m[g],i.fa==c&&i.Da==f){if(i.aa)break;return m[g].key}}else m=i[l]=[],i.G++;g=Ob();g.src=a;i=new Kb;i.sa(c,g,a,b,d,f);c=i.key;g.key=c;m.push(i);Mb[c]=i;H[l]||(H[l]=[]);H[l].push(i);a.addEventListener?(a==p||!a.ub)&&a.addEventListener(b,g,d):
+a.attachEvent(b in Nb?Nb[b]:Nb[b]="on"+b,g);return c}e(Error("Invalid event type"))},Ob=function(){var a=Pb,b=Db?function(c){return a.call(b.src,b.key,c)}:function(c){c=a.call(b.src,b.key,c);if(!c)return c};return b},Qb=function(a,b,c,d,f){if(da(b))for(var g=0;g<b.length;g++)Qb(a,b[g],c,d,f);else if(d=!!d,a=Rb(a,b,d))for(g=0;g<a.length;g++)if(a[g].fa==c&&a[g].capture==d&&a[g].Da==f){J(a[g].key);break}},J=function(a){if(!Mb[a])return n;var b=Mb[a];if(b.aa)return n;var c=b.src,d=b.type,f=b.vb,g=b.capture;
+c.removeEventListener?(c==p||!c.ub)&&c.removeEventListener(d,f,g):c.detachEvent&&c.detachEvent(d in Nb?Nb[d]:Nb[d]="on"+d,f);c=u(c);f=G[d][g][c];if(H[c]){var i=H[c];ya(i,b);0==i.length&&delete H[c]}b.aa=j;f.Ab=j;Sb(d,g,c,f);delete Mb[a];return j},Sb=function(a,b,c,d){if(!d.Ja&&d.Ab){for(var f=0,g=0;f<d.length;f++)d[f].aa?d[f].vb.src=k:(f!=g&&(d[g]=d[f]),g++);d.length=g;d.Ab=n;0==g&&(delete G[a][b][c],G[a][b].G--,0==G[a][b].G&&(delete G[a][b],G[a].G--),0==G[a].G&&delete G[a])}},Tb=function(a){var b,
+c=0,d=b==k;b=!!b;if(a==k)Da(H,function(a){for(var f=a.length-1;0<=f;f--){var g=a[f];if(d||b==g.capture)J(g.key),c++}});else if(a=u(a),H[a])for(var a=H[a],f=a.length-1;0<=f;f--){var g=a[f];if(d||b==g.capture)J(g.key),c++}},Rb=function(a,b,c){var d=G;return b in d&&(d=d[b],c in d&&(d=d[c],a=u(a),d[a]))?d[a]:k},Vb=function(a,b,c,d,f){var g=1,b=u(b);if(a[b]){a.w--;a=a[b];a.Ja?a.Ja++:a.Ja=1;try{for(var i=a.length,l=0;l<i;l++){var m=a[l];m&&!m.aa&&(g&=Ub(m,f)!==n)}}finally{a.Ja--,Sb(c,d,b,a)}}return Boolean(g)},
+Ub=function(a,b){var c=a.handleEvent(b);a.Eb&&J(a.key);return c},Pb=function(a,b){if(!Mb[a])return j;var c=Mb[a],d=c.type,f=G;if(!(d in f))return j;var f=f[d],g,i;if(!Db){var l;if(!(l=b))a:{l=["window","event"];for(var m=p;g=l.shift();)if(m[g]!=k)m=m[g];else{l=k;break a}l=m}g=l;l=j in f;m=n in f;if(l){if(0>g.keyCode||g.returnValue!=h)return j;a:{var s=n;if(0==g.keyCode)try{g.keyCode=-1;break a}catch(B){s=j}if(s||g.returnValue==h)g.returnValue=j}}s=new F;s.sa(g,this);g=j;try{if(l){for(var t=[],Qa=
+s.currentTarget;Qa;Qa=Qa.parentNode)t.push(Qa);i=f[j];i.w=i.G;for(var T=t.length-1;!s.ba&&0<=T&&i.w;T--)s.currentTarget=t[T],g&=Vb(i,t[T],d,j,s);if(m){i=f[n];i.w=i.G;for(T=0;!s.ba&&T<t.length&&i.w;T++)s.currentTarget=t[T],g&=Vb(i,t[T],d,n,s)}}else g=Ub(c,s)}finally{t&&(t.length=0),s.I()}return g}d=new F(b,this);try{g=Ub(c,d)}finally{d.I()}return g};var K=function(a){this.Fb=a;this.La=[]};v(K,Fb);var Wb=[],L=function(a,b,c,d){da(c)||(Wb[0]=c,c=Wb);for(var f=0;f<c.length;f++)a.La.push(I(b,c[f],d||a,n,a.Fb||a));return a},M=function(a,b,c,d,f,g){if(da(c))for(var i=0;i<c.length;i++)M(a,b,c[i],d,f,g);else{a:{d=d||a;g=g||a.Fb||a;f=!!f;if(b=Rb(b,c,f))for(c=0;c<b.length;c++)if(!b[c].aa&&b[c].fa==d&&b[c].capture==f&&b[c].Da==g){b=b[c];break a}b=k}b&&(b=b.key,J(b),ya(a.La,b))}return a},Xb=function(a){ua(a.La,J);a.La.length=0};
+K.prototype.f=function(){K.d.f.call(this);Xb(this)};K.prototype.handleEvent=function(){e(Error("EventHandler.handleEvent not implemented"))};var Yb=function(){};v(Yb,Fb);o=Yb.prototype;o.ub=j;o.Ca=k;o.eb=function(a){this.Ca=a};o.addEventListener=function(a,b,c,d){I(this,a,b,c,d)};o.removeEventListener=function(a,b,c,d){Qb(this,a,b,c,d)};
+o.dispatchEvent=function(a){var b=a.type||a,c=G;if(b in c){if(q(a))a=new E(a,this);else if(a instanceof E)a.target=a.target||this;else{var d=a,a=new E(b,this);Ha(a,d)}var d=1,f,c=c[b],b=j in c,g;if(b){f=[];for(g=this;g;g=g.Ca)f.push(g);g=c[j];g.w=g.G;for(var i=f.length-1;!a.ba&&0<=i&&g.w;i--)a.currentTarget=f[i],d&=Vb(g,f[i],a.type,j,a)&&a.ua!=n}if(n in c)if(g=c[n],g.w=g.G,b)for(i=0;!a.ba&&i<f.length&&g.w;i++)a.currentTarget=f[i],d&=Vb(g,f[i],a.type,n,a)&&a.ua!=n;else for(f=this;!a.ba&&f&&g.w;f=f.Ca)a.currentTarget=
+f,d&=Vb(g,f,a.type,n,a)&&a.ua!=n;a=Boolean(d)}else a=j;return a};o.f=function(){Yb.d.f.call(this);Tb(this);this.Ca=k};var Zb=function(a,b){a.style.display=b?"":"none"},$b=z?"MozUserSelect":A?"WebkitUserSelect":k,ac=function(a,b,c){c=!c?a.getElementsByTagName("*"):k;if($b){if(b=b?"none":"",a.style[$b]=b,c)for(var a=0,d;d=c[a];a++)d.style[$b]=b}else if(y||Pa)if(b=b?"on":"",a.setAttribute("unselectable",b),c)for(a=0;d=c[a];a++)d.setAttribute("unselectable",b)};var bc=function(){};ba(bc);bc.prototype.Zb=0;bc.P();var N=function(a){this.m=a||lb();this.ra=cc};v(N,Yb);N.prototype.Yb=bc.P();var cc=k,dc=function(a,b){switch(a){case 1:return b?"disable":"enable";case 2:return b?"highlight":"unhighlight";case 4:return b?"activate":"deactivate";case 8:return b?"select":"unselect";case 16:return b?"check":"uncheck";case 32:return b?"focus":"blur";case 64:return b?"open":"close"}e(Error("Invalid component state"))};o=N.prototype;o.ga=k;o.e=n;o.c=k;o.ra=k;o.q=k;o.r=k;o.t=k;o.mb=n;
+var ec=function(a){return a.ga||(a.ga=":"+(a.Yb.Zb++).toString(36))},fc=function(a,b){if(a.q&&a.q.t){var c=a.q.t,d=a.ga;d in c&&delete c[d];Ea(a.q.t,b,a)}a.ga=b};N.prototype.a=function(){return this.c};var gc=function(a){return a.da||(a.da=new K(a))},ic=function(a,b){a==b&&e(Error("Unable to set parent component"));b&&a.q&&a.ga&&hc(a.q,a.ga)&&a.q!=b&&e(Error("Unable to set parent component"));a.q=b;N.d.eb.call(a,b)};o=N.prototype;o.getParent=function(){return this.q};
+o.eb=function(a){this.q&&this.q!=a&&e(Error("Method not supported"));N.d.eb.call(this,a)};o.Ia=function(){return this.m};o.k=function(){this.c=this.m.createElement("div")};o.K=function(a){this.e&&e(Error("Component already rendered"));if(a&&this.$(a)){this.mb=j;if(!this.m||this.m.C!=kb(a))this.m=lb(a);this.Va(a);this.s()}else e(Error("Invalid element to decorate"))};o.$=function(){return j};o.Va=function(a){this.c=a};o.s=function(){this.e=j;jc(this,function(a){!a.e&&a.a()&&a.s()})};
+o.U=function(){jc(this,function(a){a.e&&a.U()});this.da&&Xb(this.da);this.e=n};o.f=function(){N.d.f.call(this);this.e&&this.U();this.da&&(this.da.I(),delete this.da);jc(this,function(a){a.I()});!this.mb&&this.c&&tb(this.c);this.q=this.c=this.t=this.r=k};o.Ba=function(a,b){this.Sa(a,kc(this),b)};
+o.Sa=function(a,b,c){a.e&&(c||!this.e)&&e(Error("Component already rendered"));(0>b||b>kc(this))&&e(Error("Child component index out of bounds"));if(!this.t||!this.r)this.t={},this.r=[];a.getParent()==this?(this.t[ec(a)]=a,ya(this.r,a)):Ea(this.t,ec(a),a);ic(a,this);Ca(this.r,b,0,a);a.e&&this.e&&a.getParent()==this?(c=this.A(),c.insertBefore(a.a(),c.childNodes[b]||k)):c?(this.c||this.k(),c=O(this,b+1),b=this.A(),c=c?c.c:k,a.e&&e(Error("Component already rendered")),a.c||a.k(),b?b.insertBefore(a.c,
+c||k):a.m.C.body.appendChild(a.c),(!a.q||a.q.e)&&a.s()):this.e&&!a.e&&a.c&&a.s()};o.A=function(){return this.c};var lc=function(a){if(a.ra==k){var b;a:{b=a.e?a.c:a.m.C.body;var c=kb(b);if(c.defaultView&&c.defaultView.getComputedStyle&&(b=c.defaultView.getComputedStyle(b,k))){b=b.direction||b.getPropertyValue("direction");break a}b=""}a.ra="rtl"==(b||((a.e?a.c:a.m.C.body).currentStyle?(a.e?a.c:a.m.C.body).currentStyle.direction:k)||(a.e?a.c:a.m.C.body).style&&(a.e?a.c:a.m.C.body).style.direction)}return a.ra};
+N.prototype.oa=function(a){this.e&&e(Error("Component already rendered"));this.ra=a};var kc=function(a){return a.r?a.r.length:0},hc=function(a,b){return a.t&&b?(b in a.t?a.t[b]:h)||k:k},O=function(a,b){return a.r?a.r[b]||k:k},jc=function(a,b,c){a.r&&ua(a.r,b,c)},mc=function(a,b){return a.r&&b?ta(a.r,b):-1};
+N.prototype.removeChild=function(a,b){if(a){var c=q(a)?a:ec(a),a=hc(this,c);if(c&&a){var d=this.t;c in d&&delete d[c];ya(this.r,a);b&&(a.U(),a.c&&tb(a.c));ic(a,k)}}a||e(Error("Child is not in parent component"));return a};var nc=function(a,b){a.setAttribute("role",b);a.lc=b};var pc=function(a,b,c,d,f){if(!y&&(!A||!C("525")))return j;if(Sa&&f)return oc(a);if(f&&!d||!c&&(17==b||18==b)||y&&d&&b==a)return n;switch(a){case 13:return!(y&&cb());case 27:return!A}return oc(a)},oc=function(a){if(48<=a&&57>=a||96<=a&&106>=a||65<=a&&90>=a||A&&0==a)return j;switch(a){case 32:case 63:case 107:case 109:case 110:case 111:case 186:case 59:case 189:case 187:case 61:case 188:case 190:case 191:case 192:case 222:case 219:case 220:case 221:return j;default:return n}},qc=function(a){switch(a){case 61:return 187;
+case 59:return 186;case 224:return 91;case 0:return 224;default:return a}};var P=function(a,b){a&&rc(this,a,b)};v(P,Yb);o=P.prototype;o.c=k;o.Fa=k;o.Xa=k;o.Ga=k;o.R=-1;o.Q=-1;
+var sc={3:13,12:144,63232:38,63233:40,63234:37,63235:39,63236:112,63237:113,63238:114,63239:115,63240:116,63241:117,63242:118,63243:119,63244:120,63245:121,63246:122,63247:123,63248:44,63272:46,63273:36,63275:35,63276:33,63277:34,63289:144,63302:45},tc={Up:38,Down:40,Left:37,Right:39,Enter:13,F1:112,F2:113,F3:114,F4:115,F5:116,F6:117,F7:118,F8:119,F9:120,F10:121,F11:122,F12:123,"U+007F":46,Home:36,End:35,PageUp:33,PageDown:34,Insert:45},uc=y||A&&C("525");
+P.prototype.Rb=function(a){if(A&&(17==this.R&&!a.ctrlKey||18==this.R&&!a.altKey))this.Q=this.R=-1;uc&&!pc(a.keyCode,this.R,a.shiftKey,a.ctrlKey,a.altKey)?this.handleEvent(a):this.Q=z?qc(a.keyCode):a.keyCode};P.prototype.Sb=function(){this.Q=this.R=-1};
+P.prototype.handleEvent=function(a){var b=a.L,c,d;y&&"keypress"==a.type?(c=this.Q,d=13!=c&&27!=c?b.keyCode:0):A&&"keypress"==a.type?(c=this.Q,d=0<=b.charCode&&63232>b.charCode&&oc(c)?b.charCode:0):Pa?(c=this.Q,d=oc(c)?b.keyCode:0):(c=b.keyCode||this.Q,d=b.charCode||0,Sa&&63==d&&224==c&&(c=191));var f=c,g=b.keyIdentifier;c?63232<=c&&c in sc?f=sc[c]:25==c&&a.shiftKey&&(f=9):g&&g in tc&&(f=tc[g]);a=f==this.R;this.R=f;b=new vc(f,d,a,b);try{this.dispatchEvent(b)}finally{b.I()}};P.prototype.a=function(){return this.c};
+var rc=function(a,b,c){a.Ga&&a.detach();a.c=b;a.Fa=I(a.c,"keypress",a,c);a.Xa=I(a.c,"keydown",a.Rb,c,a);a.Ga=I(a.c,"keyup",a.Sb,c,a)};P.prototype.detach=function(){this.Fa&&(J(this.Fa),J(this.Xa),J(this.Ga),this.Ga=this.Xa=this.Fa=k);this.c=k;this.Q=this.R=-1};P.prototype.f=function(){P.d.f.call(this);this.detach()};var vc=function(a,b,c,d){d&&this.sa(d,h);this.type="key";this.keyCode=a;this.charCode=b;this.repeat=c};v(vc,F);var xc=function(a,b){a||e(Error("Invalid class name "+a));r(b)||e(Error("Invalid decorator function "+b));wc[a]=b},yc={},wc={};var Q=function(){},zc;ba(Q);o=Q.prototype;o.M=function(){};o.k=function(a){var b=a.Ia().k("div",this.ta(a).join(" "),a.xa);Ac(a,b);return b};o.A=function(a){return a};o.qa=function(a,b,c){if(a=a.a?a.a():a)if(y&&!C("7")){var d=Bc(gb(a),b);d.push(b);ia(c?D:ib,a).apply(k,d)}else c?D(a,b):ib(a,b)};o.$=function(){return j};
+o.K=function(a,b){b.id&&fc(a,b.id);var c=this.A(b);a.xa=c&&c.firstChild?c.firstChild.nextSibling?Aa(c.childNodes):c.firstChild:k;var d=0,f=this.n(),g=this.n(),i=n,l=n,c=n,m=gb(b);ua(m,function(a){if(!i&&a==f)i=j,g==f&&(l=j);else if(!l&&a==g)l=j;else{var b=d;this.sb||(this.Ea||Cc(this),this.sb=Fa(this.Ea));a=parseInt(this.sb[a],10);d=b|(isNaN(a)?0:a)}},this);a.g=d;i||(m.push(f),g==f&&(l=j));l||m.push(g);var s=a.D;s&&m.push.apply(m,s);if(y&&!C("7")){var B=Bc(m);0<B.length&&(m.push.apply(m,B),c=j)}if(!i||
+!l||s||c)b.className=m.join(" ");Ac(a,b);return b};o.Ma=function(a){lc(a)&&this.oa(a.a(),j);a.isEnabled()&&this.la(a,a.H())};var Ac=function(a,b){w(a);w(b);a.isEnabled()||Dc(b,1,j);a.g&8&&Dc(b,8,j);a.o&16&&Dc(b,16,!!(a.g&16));a.o&64&&Dc(b,64,!!(a.g&64))};o=Q.prototype;o.ya=function(a,b){ac(a,!b,!y&&!Pa)};o.oa=function(a,b){this.qa(a,this.n()+"-rtl",b)};o.T=function(a){var b;return a.o&32&&(b=a.j())?zb(b):n};
+o.la=function(a,b){var c;if(a.o&32&&(c=a.j())){if(!b&&a.g&32){try{c.blur()}catch(d){}a.g&32&&a.ma(k)}zb(c)!=b&&(b?c.tabIndex=0:(c.tabIndex=-1,c.removeAttribute("tabIndex")))}};o.ja=function(a,b){Zb(a,b)};o.v=function(a,b,c){var d=a.a();if(d){var f=Ec(this,b);f&&this.qa(a,f,c);Dc(d,b,c)}};var Dc=function(a,b,c){zc||(zc={1:"disabled",8:"selected",16:"checked",64:"expanded"});(b=zc[b])&&a.setAttribute("aria-"+b,c)};Q.prototype.j=function(a){return a.a()};Q.prototype.n=function(){return"goog-control"};
+Q.prototype.ta=function(a){var b=this.n(),c=[b],d=this.n();d!=b&&c.push(d);b=a.g;for(d=[];b;){var f=b&-b;d.push(Ec(this,f));b&=~f}c.push.apply(c,d);(a=a.D)&&c.push.apply(c,a);y&&!C("7")&&c.push.apply(c,Bc(c));return c};
+var Bc=function(a,b){var c=[];b&&(a=a.concat([b]));ua([],function(d){wa(d,ia(xa,a))&&(!b||xa(d,b))&&c.push(d.join("_"))});return c},Ec=function(a,b){a.Ea||Cc(a);return a.Ea[b]},Cc=function(a){var b=a.n();a.Ea={1:b+"-disabled",2:b+"-hover",4:b+"-active",8:b+"-selected",16:b+"-checked",32:b+"-focused",64:b+"-open"}};var R=function(a,b,c){N.call(this,c);if(!b){for(var b=this.constructor,d;b;){d=u(b);if(d=yc[d])break;b=b.d?b.d.constructor:k}b=d?r(d.P)?d.P():new d:k}this.b=b;this.xa=a};v(R,N);o=R.prototype;o.xa=k;o.g=0;o.o=39;o.Xb=255;o.V=0;o.p=j;o.D=k;o.ha=j;o.wa=n;o.ob=k;o.pb=function(){return this.ha};o.Pa=function(a){this.e&&a!=this.ha&&Fc(this,a);this.ha=a};o.j=function(){return this.b.j(this)};o.za=function(){return this.u||(this.u=new P)};o.zb=function(){return this.b};
+o.qa=function(a,b){b?a&&(this.D?xa(this.D,a)||this.D.push(a):this.D=[a],this.b.qa(this,a,j)):a&&this.D&&(ya(this.D,a),0==this.D.length&&(this.D=k),this.b.qa(this,a,n))};o.k=function(){var a=this.b.k(this);this.c=a;var b=this.ob||this.b.M();b&&nc(a,b);this.wa||this.b.ya(a,n);this.H()||this.b.ja(a,n)};o.A=function(){return this.b.A(this.a())};o.$=function(a){return this.b.$(a)};o.Va=function(a){this.c=a=this.b.K(this,a);var b=this.ob||this.b.M();b&&nc(a,b);this.wa||this.b.ya(a,n);this.p="none"!=a.style.display};
+o.s=function(){R.d.s.call(this);this.b.Ma(this);if(this.o&-2&&(this.pb()&&Fc(this,j),this.o&32)){var a=this.j();if(a){var b=this.za();rc(b,a);L(L(L(gc(this),b,"key",this.N),a,"focus",this.na),a,"blur",this.ma)}}};var Fc=function(a,b){var c=gc(a),d=a.a();b?(L(L(L(L(c,d,"mouseover",a.$a),d,"mousedown",a.ka),d,"mouseup",a.ab),d,"mouseout",a.Za),y&&L(c,d,"dblclick",a.wb)):(M(M(M(M(c,d,"mouseover",a.$a),d,"mousedown",a.ka),d,"mouseup",a.ab),d,"mouseout",a.Za),y&&M(c,d,"dblclick",a.wb))};o=R.prototype;
o.U=function(){R.d.U.call(this);this.u&&this.u.detach();this.H()&&this.isEnabled()&&this.b.la(this,n)};o.f=function(){R.d.f.call(this);this.u&&(this.u.I(),delete this.u);delete this.b;this.D=this.xa=k};o.oa=function(a){R.d.oa.call(this,a);var b=this.a();b&&this.b.oa(b,a)};o.ya=function(a){this.wa=a;var b=this.a();b&&this.b.ya(b,a)};o.H=function(){return this.p};
-o.ja=function(a,b){if(b||this.p!=a&&this.dispatchEvent(a?"show":"hide")){var c=this.a();c&&this.b.ja(c,a);this.isEnabled()&&this.b.la(this,a);this.p=a;return j}return n};o.isEnabled=function(){return!(this.g&1)};o.pa=function(a){var b=this.getParent();if((!b||"function"!=typeof b.isEnabled||b.isEnabled())&&T(this,1,!a))a||(this.setActive(n),this.B(n)),this.H()&&this.b.la(this,a),this.v(1,!a)};o.B=function(a){T(this,2,a)&&this.v(2,a)};o.setActive=function(a){T(this,4,a)&&this.v(4,a)};
-var Ec=function(a,b){T(a,8,b)&&a.v(8,b)},Fc=function(a,b){T(a,64,b)&&a.v(64,b)};R.prototype.v=function(a,b){this.o&a&&b!=!!(this.g&a)&&(this.b.v(this,a,b),this.g=b?this.g|a:this.g&~a)};var Gc=function(a,b,c){a.e&&a.g&b&&!c&&e(Error("Component already rendered"));!c&&a.g&b&&a.v(b,n);a.o=c?a.o|b:a.o&~b},U=function(a,b){return!!(a.Wb&b)&&!!(a.o&b)},T=function(a,b,c){return!!(a.o&b)&&!!(a.g&b)!=c&&(!(a.V&b)||a.dispatchEvent(bc(b,c)))&&!a.bb};o=R.prototype;
-o.$a=function(a){(!a.relatedTarget||!tb(this.a(),a.relatedTarget))&&this.dispatchEvent("enter")&&this.isEnabled()&&U(this,2)&&this.B(j)};o.Za=function(a){if((!a.relatedTarget||!tb(this.a(),a.relatedTarget))&&this.dispatchEvent("leave"))U(this,4)&&this.setActive(n),U(this,2)&&this.B(n)};o.ka=function(a){if(this.isEnabled()&&(U(this,2)&&this.B(j),Hb(a)&&(!B||!Ra||!a.ctrlKey)))U(this,4)&&this.setActive(j),this.b.T(this)&&this.j().focus();!this.wa&&Hb(a)&&(!B||!Ra||!a.ctrlKey)&&a.preventDefault()};
-o.ab=function(a){this.isEnabled()&&(U(this,2)&&this.B(j),this.g&4&&Hc(this,a)&&U(this,4)&&this.setActive(n))};o.wb=function(a){this.isEnabled()&&Hc(this,a)};var Hc=function(a,b){if(U(a,16)){var c=!(a.g&16);T(a,16,c)&&a.v(16,c)}U(a,8)&&Ec(a,j);U(a,64)&&Fc(a,!(a.g&64));c=new E("action",a);b&&(c.altKey=b.altKey,c.ctrlKey=b.ctrlKey,c.metaKey=b.metaKey,c.shiftKey=b.shiftKey,c.cb=b.cb);return a.dispatchEvent(c)};R.prototype.na=function(){U(this,32)&&T(this,32,j)&&this.v(32,j)};
-R.prototype.ma=function(){U(this,4)&&this.setActive(n);U(this,32)&&T(this,32,n)&&this.v(32,n)};R.prototype.N=function(a){return this.H()&&this.isEnabled()&&this.kb(a)?(a.preventDefault(),a.stopPropagation(),j):n};R.prototype.kb=function(a){return 13==a.keyCode&&Hc(this,a)};r(R)||e(Error("Invalid component class "+R));r(Q)||e(Error("Invalid renderer class "+Q));var Ic=u(R);wc[Ic]=Q;vc("goog-control",function(){return new R(k)});var Jc=function(){};v(Jc,Q);ba(Jc);Jc.prototype.k=function(a){return a.Ia().k("div",this.n())};Jc.prototype.K=function(a,b){b.id&&dc(a,b.id);if("HR"==b.tagName){var c=b,b=this.k(a);c.parentNode&&c.parentNode.insertBefore(b,c);rb(c)}else D(b,this.n());return b};Jc.prototype.n=function(){return"goog-menuseparator"};var Kc=function(a,b){R.call(this,k,a||Jc.P(),b);Gc(this,1,n);Gc(this,2,n);Gc(this,4,n);Gc(this,32,n);this.g=1};v(Kc,R);Kc.prototype.s=function(){Kc.d.s.call(this);lc(this.a(),"separator")};vc("goog-menuseparator",function(){return new Kc});var V=function(){};ba(V);V.prototype.M=function(){};var Lc=function(a,b){a&&(a.tabIndex=b?0:-1)};o=V.prototype;o.k=function(a){return a.Ia().k("div",this.ta(a).join(" "))};o.A=function(a){return a};o.$=function(a){return"DIV"==a.tagName};o.K=function(a,b){b.id&&dc(a,b.id);var c=this.n(),d=n,f=fb(b);f&&ua(f,function(b){b==c?d=j:b&&this.Wa(a,b,c)},this);d||D(b,c);Mc(a,this.A(b));return b};o.Wa=function(a,b,c){b==c+"-disabled"?a.pa(n):b==c+"-horizontal"?Nc(a,"horizontal"):b==c+"-vertical"&&Nc(a,"vertical")};
-var Mc=function(a,b){if(b)for(var c=b.firstChild,d;c&&c.parentNode==b;){d=c.nextSibling;if(1==c.nodeType){var f;a:{f=h;for(var g=fb(c),i=0,l=g.length;i<l;i++)if(f=g[i]in uc?uc[g[i]]():k)break a;f=k}f&&(f.c=c,a.isEnabled()||f.pa(n),a.Ba(f),f.K(c))}else(!c.nodeValue||""==la(c.nodeValue))&&b.removeChild(c);c=d}};V.prototype.Ma=function(a){a=a.a();Zb(a,j,A);y&&(a.hideFocus=j);var b=this.M();b&&lc(a,b)};V.prototype.j=function(a){return a.a()};V.prototype.n=function(){return"goog-container"};
-V.prototype.ta=function(a){var b=this.n(),c=[b,"horizontal"==a.O?b+"-horizontal":b+"-vertical"];a.isEnabled()||c.push(b+"-disabled");return c};var W=function(a,b,c){N.call(this,c);this.b=b||V.P();this.O=a||"vertical"};v(W,N);o=W.prototype;o.Oa=k;o.u=k;o.b=k;o.O=k;o.p=j;o.X=j;o.Ya=j;o.i=-1;o.h=k;o.ca=n;o.Pb=n;o.Ob=j;o.J=k;o.j=function(){return this.Oa||this.b.j(this)};o.za=function(){return this.u||(this.u=new P(this.j()))};o.zb=function(){return this.b};o.k=function(){this.c=this.b.k(this)};o.A=function(){return this.b.A(this.a())};o.$=function(a){return this.b.$(a)};
-o.Va=function(a){this.c=this.b.K(this,a);"none"==a.style.display&&(this.p=n)};o.s=function(){W.d.s.call(this);hc(this,function(a){a.e&&Oc(this,a)},this);var a=this.a();this.b.Ma(this);this.ja(this.p,j);L(L(L(L(L(L(L(L(ec(this),this,"enter",this.Jb),this,"highlight",this.Kb),this,"unhighlight",this.Mb),this,"open",this.Lb),this,"close",this.Hb),a,"mousedown",this.ka),ib(a),"mouseup",this.Ib),a,["mousedown","mouseup","mouseover","mouseout"],this.Gb);this.T()&&Pc(this,j)};
-var Pc=function(a,b){var c=ec(a),d=a.j();b?L(L(L(c,d,"focus",a.na),d,"blur",a.ma),a.za(),"key",a.N):M(M(M(c,d,"focus",a.na),d,"blur",a.ma),a.za(),"key",a.N)};o=W.prototype;o.U=function(){Qc(this,-1);this.h&&Fc(this.h,n);this.ca=n;W.d.U.call(this)};o.f=function(){W.d.f.call(this);this.u&&(this.u.I(),this.u=k);this.b=this.h=this.J=this.Oa=k};o.Jb=function(){return j};
-o.Kb=function(a){var b=kc(this,a.target);if(-1<b&&b!=this.i){var c=O(this,this.i);c&&c.B(n);this.i=b;c=O(this,this.i);this.ca&&c.setActive(j);this.Ob&&this.h&&c!=this.h&&(c.o&64?Fc(c,j):Fc(this.h,n))}this.a().setAttribute("aria-activedescendant",a.target.a().id)};o.Mb=function(a){a.target==O(this,this.i)&&(this.i=-1);this.a().setAttribute("aria-activedescendant","")};o.Lb=function(a){if((a=a.target)&&a!=this.h&&a.getParent()==this)this.h&&Fc(this.h,n),this.h=a};
-o.Hb=function(a){a.target==this.h&&(this.h=k)};o.ka=function(a){this.X&&(this.ca=j);var b=this.j();b&&xb(b)?b.focus():a.preventDefault()};o.Ib=function(){this.ca=n};o.Gb=function(a){var b;a:{b=a.target;if(this.J)for(var c=this.a();b&&b!==c;){var d=b.id;if(d in this.J){b=this.J[d];break a}b=b.parentNode}b=k}if(b)switch(a.type){case "mousedown":b.ka(a);break;case "mouseup":b.ab(a);break;case "mouseover":b.$a(a);break;case "mouseout":b.Za(a)}};o.na=function(){};
-o.ma=function(){Qc(this,-1);this.ca=n;this.h&&Fc(this.h,n)};o.N=function(a){return this.isEnabled()&&this.H()&&(0!=ic(this)||this.Oa)&&this.kb(a)?(a.preventDefault(),a.stopPropagation(),j):n};
-o.kb=function(a){var b=O(this,this.i);if(b&&"function"==typeof b.N&&b.N(a)||this.h&&this.h!=b&&"function"==typeof this.h.N&&this.h.N(a))return j;if(a.shiftKey||a.ctrlKey||a.metaKey||a.altKey)return n;switch(a.keyCode){case 27:if(this.T())this.j().blur();else return n;break;case 36:Rc(this);break;case 35:Sc(this);break;case 38:if("vertical"==this.O)Tc(this);else return n;break;case 37:if("horizontal"==this.O)jc(this)?Uc(this):Tc(this);else return n;break;case 40:if("vertical"==this.O)Uc(this);else return n;
-break;case 39:if("horizontal"==this.O)jc(this)?Tc(this):Uc(this);else return n;break;default:return n}return j};var Oc=function(a,b){var c=b.a(),c=c.id||(c.id=cc(b));a.J||(a.J={});a.J[c]=b};W.prototype.Ba=function(a,b){W.d.Ba.call(this,a,b)};W.prototype.Sa=function(a,b,c){a.V|=2;a.V|=64;(this.T()||!this.Pb)&&Gc(a,32,n);a.Pa(n);W.d.Sa.call(this,a,b,c);a.e&&this.e&&Oc(this,a);b<=this.i&&this.i++};
-W.prototype.removeChild=function(a,b){if(a=q(a)?fc(this,a):a){var c=kc(this,a);-1!=c&&(c==this.i?a.B(n):c<this.i&&this.i--);var d=a.a();d&&d.id&&this.J&&(c=this.J,d=d.id,d in c&&delete c[d])}a=W.d.removeChild.call(this,a,b);a.Pa(j);return a};var Nc=function(a,b){a.a()&&e(Error("Component already rendered"));a.O=b};o=W.prototype;o.H=function(){return this.p};
-o.ja=function(a,b){if(b||this.p!=a&&this.dispatchEvent(a?"show":"hide")){this.p=a;var c=this.a();c&&(Xb(c,a),this.T()&&Lc(this.j(),this.X&&this.p),b||this.dispatchEvent(this.p?"aftershow":"afterhide"));return j}return n};o.isEnabled=function(){return this.X};o.pa=function(a){if(this.X!=a&&this.dispatchEvent(a?"enable":"disable"))a?(this.X=j,hc(this,function(a){a.tb?delete a.tb:a.pa(j)})):(hc(this,function(a){a.isEnabled()?a.pa(n):a.tb=j}),this.ca=this.X=n),this.T()&&Lc(this.j(),a&&this.p)};o.T=function(){return this.Ya};
-o.la=function(a){a!=this.Ya&&this.e&&Pc(this,a);this.Ya=a;this.X&&this.p&&Lc(this.j(),a)};var Qc=function(a,b){var c=O(a,b);c?c.B(j):-1<a.i&&O(a,a.i).B(n)};W.prototype.B=function(a){Qc(this,kc(this,a))};
-var Rc=function(a){Vc(a,function(a,c){return(a+1)%c},ic(a)-1)},Sc=function(a){Vc(a,function(a,c){a--;return 0>a?c-1:a},0)},Uc=function(a){Vc(a,function(a,c){return(a+1)%c},a.i)},Tc=function(a){Vc(a,function(a,c){a--;return 0>a?c-1:a},a.i)},Vc=function(a,b,c){for(var c=0>c?kc(a,a.h):c,d=ic(a),c=b.call(a,c,d),f=0;f<=d;){var g=O(a,c);if(g&&g.H()&&g.isEnabled()&&g.o&2){a.Ua(c);break}f++;c=b.call(a,c,d)}};W.prototype.Ua=function(a){Qc(this,a)};var Wc=function(){};v(Wc,Q);ba(Wc);o=Wc.prototype;o.n=function(){return"goog-tab"};o.M=function(){return"tab"};o.k=function(a){var b=Wc.d.k.call(this,a);(a=a.Ra())&&this.Ta(b,a);return b};o.K=function(a,b){var b=Wc.d.K.call(this,a,b),c=this.Ra(b);c&&(a.qb=c);if(a.g&8&&(c=a.getParent())&&r(c.W))a.v(8,n),c.W(a);return b};o.Ra=function(a){return a.title||""};o.Ta=function(a,b){a&&(a.title=b||"")};var Xc=function(a,b,c){R.call(this,a,b||Wc.P(),c);Gc(this,8,j);this.V|=9};v(Xc,R);Xc.prototype.Ra=function(){return this.qb};Xc.prototype.Ta=function(a){this.zb().Ta(this.a(),a);this.qb=a};vc("goog-tab",function(){return new Xc(k)});var X=function(){};v(X,V);ba(X);X.prototype.n=function(){return"goog-tab-bar"};X.prototype.M=function(){return"tablist"};X.prototype.Wa=function(a,b,c){this.yb||(this.Ha||Yc(this),this.yb=Ea(this.Ha));var d=this.yb[b];d?(Nc(a,Zc(d)),a.rb=d):X.d.Wa.call(this,a,b,c)};X.prototype.ta=function(a){var b=X.d.ta.call(this,a);this.Ha||Yc(this);b.push(this.Ha[a.rb]);return b};var Yc=function(a){var b=a.n();a.Ha={top:b+"-top",bottom:b+"-bottom",start:b+"-start",end:b+"-end"}};var Y=function(a,b,c){a=a||"top";Nc(this,Zc(a));this.rb=a;W.call(this,this.O,b||X.P(),c);$c(this)};v(Y,W);o=Y.prototype;o.Sb=j;o.F=k;o.s=function(){Y.d.s.call(this);$c(this)};o.f=function(){Y.d.f.call(this);this.F=k};o.removeChild=function(a,b){ad(this,a);return Y.d.removeChild.call(this,a,b)};o.Ua=function(a){Y.d.Ua.call(this,a);this.Sb&&this.W(O(this,a))};o.W=function(a){a?Ec(a,j):this.F&&Ec(this.F,n)};
-var ad=function(a,b){if(b&&b==a.F){for(var c=kc(a,b),d=c-1;b=O(a,d);d--)if(b.H()&&b.isEnabled()){a.W(b);return}for(c+=1;b=O(a,c);c++)if(b.H()&&b.isEnabled()){a.W(b);return}a.W(k)}};o=Y.prototype;o.bc=function(a){this.F&&this.F!=a.target&&Ec(this.F,n);this.F=a.target};o.cc=function(a){a.target==this.F&&(this.F=k)};o.$b=function(a){ad(this,a.target)};o.ac=function(a){ad(this,a.target)};o.na=function(){O(this,this.i)||this.B(this.F||O(this,0))};
-var $c=function(a){L(L(L(L(ec(a),a,"select",a.bc),a,"unselect",a.cc),a,"disable",a.$b),a,"hide",a.ac)},Zc=function(a){return"start"==a||"end"==a?"vertical":"horizontal"};vc("goog-tab-bar",function(){return new Y});var Z=function(a,b,c,d,f){function g(a){a&&(a.tabIndex=0,lc(a,i.M()),D(a,"goog-zippy-header"),bd(i,a),a&&L(i.nb,a,"keydown",i.Nb))}this.m=f||jb();this.Y=this.m.a(a)||k;this.Aa=this.m.a(d||k);this.ea=(this.Qa=r(b)?b:k)||!b?k:this.m.a(b);this.l=c==j;this.nb=new K(this);this.Na=new K(this);var i=this;g(this.Y);g(this.Aa);this.Z(this.l)};v(Z,Wb);o=Z.prototype;o.ha=j;o.kc=j;o.f=function(){Z.d.f.call(this);Fb(this.nb);Fb(this.Na)};o.M=function(){return"tab"};o.A=function(){return this.ea};o.toggle=function(){this.Z(!this.l)};
-o.Z=function(a){this.ea?Xb(this.ea,a):a&&this.Qa&&(this.ea=this.Qa());this.ea&&D(this.ea,"goog-zippy-content");if(this.Aa)Xb(this.Y,!a),Xb(this.Aa,a);else if(this.Y){var b=this.Y;a?D(b,"goog-zippy-expanded"):gb(b,"goog-zippy-expanded");b=this.Y;!a?D(b,"goog-zippy-collapsed"):gb(b,"goog-zippy-collapsed");this.Y.setAttribute("aria-expanded",a)}this.l=a;this.dispatchEvent(new cd("toggle",this))};o.pb=function(){return this.kc};
-o.Pa=function(a){this.ha!=a&&((this.ha=a)?(bd(this,this.Y),bd(this,this.Aa)):Vb(this.Na))};var bd=function(a,b){b&&L(a.Na,b,"click",a.ec)};Z.prototype.Nb=function(a){if(13==a.keyCode||32==a.keyCode)this.toggle(),this.dispatchEvent(new E("action",this)),a.preventDefault(),a.stopPropagation()};Z.prototype.ec=function(){this.toggle();this.dispatchEvent(new E("action",this))};var cd=function(a,b){E.call(this,a,b)};v(cd,E);var ed=function(a,b){this.lb=[];for(var c=kb(a),c=lb("span","ae-zippy",c),d=0,f;f=c[d];d++)this.lb.push(new Z(f,f.parentNode.parentNode.parentNode.nextElementSibling!=h?f.parentNode.parentNode.parentNode.nextElementSibling:sb(f.parentNode.parentNode.parentNode.nextSibling),n));this.dc=new dd(this.lb,kb(b))};ed.prototype.ic=function(){return this.dc};ed.prototype.jc=function(){return this.lb};
-var dd=function(a,b){this.va=a;if(this.va.length)for(var c=0,d;d=this.va[c];c++)I(d,"toggle",this.Ub,n,this);this.Ka=0;this.l=n;c="ae-toggle ae-plus ae-action";this.va.length||(c+=" ae-disabled");this.S=pb("span",{className:c},"Expand All");I(this.S,"click",this.Tb,n,this);b&&b.appendChild(this.S)};dd.prototype.Tb=function(){this.va.length&&this.Z(!this.l)};
-dd.prototype.Ub=function(a){a=a.currentTarget;this.Ka=a.l?this.Ka+1:this.Ka-1;a.l!=this.l&&(a.l?(this.l=j,fd(this,j)):0==this.Ka&&(this.l=n,fd(this,n)))};dd.prototype.Z=function(a){this.l=a;for(var a=0,b;b=this.va[a];a++)b.l!=this.l&&b.Z(this.l);fd(this)};
-var fd=function(a,b){(b!==h?b:a.l)?(gb(a.S,"ae-plus"),D(a.S,"ae-minus"),ub(a.S,"Collapse All")):(gb(a.S,"ae-minus"),D(a.S,"ae-plus"),ub(a.S,"Expand All"))},gd=function(a){this.Vb=a;this.Db={};var b,c=pb("div",{},b=pb("div",{id:"ae-stats-details-tabs",className:"goog-tab-bar goog-tab-bar-top"}),pb("div",{className:"goog-tab-bar-clear"}),a=pb("div",{id:"ae-stats-details-tabs-content",className:"goog-tab-content"})),d=new Y;d.K(b);I(d,"select",this.Bb,n,this);I(d,"unselect",this.Bb,n,this);b=0;for(var f;f=
-this.Vb[b];b++)if(f=kb("ae-stats-details-"+f)){var g=lb("h2",k,f)[0],i;i=g;var l=h;eb&&"innerText"in i?l=i.innerText.replace(/(\r\n|\r|\n)/g,"\n"):(l=[],yb(i,l,j),l=l.join(""));l=l.replace(/ \xAD /g," ").replace(/\xAD/g,"");l=l.replace(/\u200B/g,"");eb||(l=l.replace(/ +/g," "));" "!=l&&(l=l.replace(/^\s*/,""));i=l;rb(g);g=new Xc(i);this.Db[u(g)]=f;d.Ba(g,j);a.appendChild(f);0==b?d.W(g):Xb(f,n)}kb("bd").appendChild(c)};gd.prototype.Bb=function(a){var b=this.Db[u(a.target)];Xb(b,"select"==a.type)};
-aa("ae.Stats.Details.Tabs",gd);aa("goog.ui.Zippy",Z);Z.prototype.setExpanded=Z.prototype.Z;aa("ae.Stats.MakeZippys",ed);ed.prototype.getExpandCollapse=ed.prototype.ic;ed.prototype.getZippys=ed.prototype.jc;dd.prototype.setExpanded=dd.prototype.Z;var $=function(){this.fb=[];this.jb=[]},hd=[[5,0.2,1],[6,0.2,1.2],[5,0.25,1.25],[6,0.25,1.5],[4,0.5,2],[5,0.5,2.5],[6,0.5,3],[4,1,4],[5,1,5],[6,1,6],[4,2,8],[5,2,10]],id=function(a){if(0>=a)return[2,0.5,1];for(var b=1;1>a;)a*=10,b/=10;for(;10<=a;)a/=10,b*=10;for(var c=0;c<hd.length;c++)if(a<=hd[c][2])return[hd[c][0],hd[c][1]*b,hd[c][2]*b];return[5,2*b,10*b]};$.prototype.ib="stats/static/pix.gif";$.prototype.z="ae-stats-gantt-";$.prototype.hb=0;$.prototype.write=function(a){this.jb.push(a)};
-var jd=function(a,b,c,d){a.write('<tr class="'+a.z+'axisrow"><td width="20%"></td><td>');a.write('<div class="'+a.z+'axis">');for(var f=0;f<=b;f++)a.write('<img class="'+a.z+'tick" src="'+a.ib+'" alt="" '),a.write('style="left:'+f*c*d+'%"\n>'),a.write('<span class="'+a.z+'scale" style="left:'+f*c*d+'%">'),a.write(" "+f*c+"</span>");a.write("</div></td></tr>\n")};
-$.prototype.hc=function(){this.jb=[];var a=id(this.hb),b=a[0],c=a[1],a=100/a[2];this.write('<table class="'+this.z+'table">\n');jd(this,b,c,a);for(var d=0;d<this.fb.length;d++){var f=this.fb[d];this.write('<tr class="'+this.z+'datarow"><td width="20%">');0<f.label.length&&(0<f.ia.length&&this.write('<a class="'+this.z+'link" href="'+f.ia+'">'),this.write(f.label),0<f.ia.length&&this.write("</a>"));this.write("</td>\n<td>");this.write('<div class="'+this.z+'container">');0<f.ia.length&&this.write('<a class="'+
+o.ja=function(a,b){if(b||this.p!=a&&this.dispatchEvent(a?"show":"hide")){var c=this.a();c&&this.b.ja(c,a);this.isEnabled()&&this.b.la(this,a);this.p=a;return j}return n};o.isEnabled=function(){return!(this.g&1)};o.pa=function(a){var b=this.getParent();if((!b||"function"!=typeof b.isEnabled||b.isEnabled())&&S(this,1,!a))a||(this.setActive(n),this.B(n)),this.H()&&this.b.la(this,a),this.v(1,!a)};o.B=function(a){S(this,2,a)&&this.v(2,a)};o.setActive=function(a){S(this,4,a)&&this.v(4,a)};
+var Gc=function(a,b){S(a,8,b)&&a.v(8,b)},Hc=function(a,b){S(a,64,b)&&a.v(64,b)};R.prototype.v=function(a,b){this.o&a&&b!=!!(this.g&a)&&(this.b.v(this,a,b),this.g=b?this.g|a:this.g&~a)};var Ic=function(a,b,c){a.e&&a.g&b&&!c&&e(Error("Component already rendered"));!c&&a.g&b&&a.v(b,n);a.o=c?a.o|b:a.o&~b},U=function(a,b){return!!(a.Xb&b)&&!!(a.o&b)},S=function(a,b,c){return!!(a.o&b)&&!!(a.g&b)!=c&&(!(a.V&b)||a.dispatchEvent(dc(b,c)))&&!a.bb};o=R.prototype;
+o.$a=function(a){(!a.relatedTarget||!vb(this.a(),a.relatedTarget))&&this.dispatchEvent("enter")&&this.isEnabled()&&U(this,2)&&this.B(j)};o.Za=function(a){if((!a.relatedTarget||!vb(this.a(),a.relatedTarget))&&this.dispatchEvent("leave"))U(this,4)&&this.setActive(n),U(this,2)&&this.B(n)};o.ka=function(a){if(this.isEnabled()&&(U(this,2)&&this.B(j),Jb(a)&&(!A||!Sa||!a.ctrlKey)))U(this,4)&&this.setActive(j),this.b.T(this)&&this.j().focus();!this.wa&&Jb(a)&&(!A||!Sa||!a.ctrlKey)&&a.preventDefault()};
+o.ab=function(a){this.isEnabled()&&(U(this,2)&&this.B(j),this.g&4&&Jc(this,a)&&U(this,4)&&this.setActive(n))};o.wb=function(a){this.isEnabled()&&Jc(this,a)};var Jc=function(a,b){if(U(a,16)){var c=!(a.g&16);S(a,16,c)&&a.v(16,c)}U(a,8)&&Gc(a,j);U(a,64)&&Hc(a,!(a.g&64));c=new E("action",a);b&&(c.altKey=b.altKey,c.ctrlKey=b.ctrlKey,c.metaKey=b.metaKey,c.shiftKey=b.shiftKey,c.cb=b.cb);return a.dispatchEvent(c)};R.prototype.na=function(){U(this,32)&&S(this,32,j)&&this.v(32,j)};
+R.prototype.ma=function(){U(this,4)&&this.setActive(n);U(this,32)&&S(this,32,n)&&this.v(32,n)};R.prototype.N=function(a){return this.H()&&this.isEnabled()&&this.kb(a)?(a.preventDefault(),a.stopPropagation(),j):n};R.prototype.kb=function(a){return 13==a.keyCode&&Jc(this,a)};r(R)||e(Error("Invalid component class "+R));r(Q)||e(Error("Invalid renderer class "+Q));var Kc=u(R);yc[Kc]=Q;xc("goog-control",function(){return new R(k)});var Lc=function(){};v(Lc,Q);ba(Lc);Lc.prototype.k=function(a){return a.Ia().k("div",this.n())};Lc.prototype.K=function(a,b){b.id&&fc(a,b.id);if("HR"==b.tagName){var c=b,b=this.k(a);c.parentNode&&c.parentNode.insertBefore(b,c);tb(c)}else D(b,this.n());return b};Lc.prototype.n=function(){return"goog-menuseparator"};var Mc=function(a,b){R.call(this,k,a||Lc.P(),b);Ic(this,1,n);Ic(this,2,n);Ic(this,4,n);Ic(this,32,n);this.g=1};v(Mc,R);Mc.prototype.s=function(){Mc.d.s.call(this);nc(this.a(),"separator")};xc("goog-menuseparator",function(){return new Mc});var V=function(){};ba(V);V.prototype.M=function(){};var Nc=function(a,b){a&&(a.tabIndex=b?0:-1)};o=V.prototype;o.k=function(a){return a.Ia().k("div",this.ta(a).join(" "))};o.A=function(a){return a};o.$=function(a){return"DIV"==a.tagName};o.K=function(a,b){b.id&&fc(a,b.id);var c=this.n(),d=n,f=gb(b);f&&ua(f,function(b){b==c?d=j:b&&this.Wa(a,b,c)},this);d||D(b,c);Oc(a,this.A(b));return b};o.Wa=function(a,b,c){b==c+"-disabled"?a.pa(n):b==c+"-horizontal"?Pc(a,"horizontal"):b==c+"-vertical"&&Pc(a,"vertical")};
+var Oc=function(a,b){if(b)for(var c=b.firstChild,d;c&&c.parentNode==b;){d=c.nextSibling;if(1==c.nodeType){var f;a:{f=h;for(var g=gb(c),i=0,l=g.length;i<l;i++)if(f=g[i]in wc?wc[g[i]]():k)break a;f=k}f&&(f.c=c,a.isEnabled()||f.pa(n),a.Ba(f),f.K(c))}else(!c.nodeValue||""==la(c.nodeValue))&&b.removeChild(c);c=d}};V.prototype.Ma=function(a){a=a.a();ac(a,j,z);y&&(a.hideFocus=j);var b=this.M();b&&nc(a,b)};V.prototype.j=function(a){return a.a()};V.prototype.n=function(){return"goog-container"};
+V.prototype.ta=function(a){var b=this.n(),c=[b,"horizontal"==a.O?b+"-horizontal":b+"-vertical"];a.isEnabled()||c.push(b+"-disabled");return c};var W=function(a,b,c){N.call(this,c);this.b=b||V.P();this.O=a||"vertical"};v(W,N);o=W.prototype;o.Oa=k;o.u=k;o.b=k;o.O=k;o.p=j;o.X=j;o.Ya=j;o.i=-1;o.h=k;o.ca=n;o.Qb=n;o.Pb=j;o.J=k;o.j=function(){return this.Oa||this.b.j(this)};o.za=function(){return this.u||(this.u=new P(this.j()))};o.zb=function(){return this.b};o.k=function(){this.c=this.b.k(this)};o.A=function(){return this.b.A(this.a())};o.$=function(a){return this.b.$(a)};
+o.Va=function(a){this.c=this.b.K(this,a);"none"==a.style.display&&(this.p=n)};o.s=function(){W.d.s.call(this);jc(this,function(a){a.e&&Qc(this,a)},this);var a=this.a();this.b.Ma(this);this.ja(this.p,j);L(L(L(L(L(L(L(L(gc(this),this,"enter",this.Kb),this,"highlight",this.Lb),this,"unhighlight",this.Nb),this,"open",this.Mb),this,"close",this.Ib),a,"mousedown",this.ka),kb(a),"mouseup",this.Jb),a,["mousedown","mouseup","mouseover","mouseout"],this.Hb);this.T()&&Rc(this,j)};
+var Rc=function(a,b){var c=gc(a),d=a.j();b?L(L(L(c,d,"focus",a.na),d,"blur",a.ma),a.za(),"key",a.N):M(M(M(c,d,"focus",a.na),d,"blur",a.ma),a.za(),"key",a.N)};o=W.prototype;o.U=function(){Sc(this,-1);this.h&&Hc(this.h,n);this.ca=n;W.d.U.call(this)};o.f=function(){W.d.f.call(this);this.u&&(this.u.I(),this.u=k);this.b=this.h=this.J=this.Oa=k};o.Kb=function(){return j};
+o.Lb=function(a){var b=mc(this,a.target);if(-1<b&&b!=this.i){var c=O(this,this.i);c&&c.B(n);this.i=b;c=O(this,this.i);this.ca&&c.setActive(j);this.Pb&&this.h&&c!=this.h&&(c.o&64?Hc(c,j):Hc(this.h,n))}this.a().setAttribute("aria-activedescendant",a.target.a().id)};o.Nb=function(a){a.target==O(this,this.i)&&(this.i=-1);this.a().setAttribute("aria-activedescendant","")};o.Mb=function(a){if((a=a.target)&&a!=this.h&&a.getParent()==this)this.h&&Hc(this.h,n),this.h=a};
+o.Ib=function(a){a.target==this.h&&(this.h=k)};o.ka=function(a){this.X&&(this.ca=j);var b=this.j();b&&zb(b)?b.focus():a.preventDefault()};o.Jb=function(){this.ca=n};o.Hb=function(a){var b;a:{b=a.target;if(this.J)for(var c=this.a();b&&b!==c;){var d=b.id;if(d in this.J){b=this.J[d];break a}b=b.parentNode}b=k}if(b)switch(a.type){case "mousedown":b.ka(a);break;case "mouseup":b.ab(a);break;case "mouseover":b.$a(a);break;case "mouseout":b.Za(a)}};o.na=function(){};
+o.ma=function(){Sc(this,-1);this.ca=n;this.h&&Hc(this.h,n)};o.N=function(a){return this.isEnabled()&&this.H()&&(0!=kc(this)||this.Oa)&&this.kb(a)?(a.preventDefault(),a.stopPropagation(),j):n};
+o.kb=function(a){var b=O(this,this.i);if(b&&"function"==typeof b.N&&b.N(a)||this.h&&this.h!=b&&"function"==typeof this.h.N&&this.h.N(a))return j;if(a.shiftKey||a.ctrlKey||a.metaKey||a.altKey)return n;switch(a.keyCode){case 27:if(this.T())this.j().blur();else return n;break;case 36:Tc(this);break;case 35:Uc(this);break;case 38:if("vertical"==this.O)Vc(this);else return n;break;case 37:if("horizontal"==this.O)lc(this)?Wc(this):Vc(this);else return n;break;case 40:if("vertical"==this.O)Wc(this);else return n;
+break;case 39:if("horizontal"==this.O)lc(this)?Vc(this):Wc(this);else return n;break;default:return n}return j};var Qc=function(a,b){var c=b.a(),c=c.id||(c.id=ec(b));a.J||(a.J={});a.J[c]=b};W.prototype.Ba=function(a,b){W.d.Ba.call(this,a,b)};W.prototype.Sa=function(a,b,c){a.V|=2;a.V|=64;(this.T()||!this.Qb)&&Ic(a,32,n);a.Pa(n);W.d.Sa.call(this,a,b,c);a.e&&this.e&&Qc(this,a);b<=this.i&&this.i++};
+W.prototype.removeChild=function(a,b){if(a=q(a)?hc(this,a):a){var c=mc(this,a);-1!=c&&(c==this.i?a.B(n):c<this.i&&this.i--);var d=a.a();d&&d.id&&this.J&&(c=this.J,d=d.id,d in c&&delete c[d])}a=W.d.removeChild.call(this,a,b);a.Pa(j);return a};var Pc=function(a,b){a.a()&&e(Error("Component already rendered"));a.O=b};o=W.prototype;o.H=function(){return this.p};
+o.ja=function(a,b){if(b||this.p!=a&&this.dispatchEvent(a?"show":"hide")){this.p=a;var c=this.a();c&&(Zb(c,a),this.T()&&Nc(this.j(),this.X&&this.p),b||this.dispatchEvent(this.p?"aftershow":"afterhide"));return j}return n};o.isEnabled=function(){return this.X};o.pa=function(a){if(this.X!=a&&this.dispatchEvent(a?"enable":"disable"))a?(this.X=j,jc(this,function(a){a.tb?delete a.tb:a.pa(j)})):(jc(this,function(a){a.isEnabled()?a.pa(n):a.tb=j}),this.ca=this.X=n),this.T()&&Nc(this.j(),a&&this.p)};o.T=function(){return this.Ya};
+o.la=function(a){a!=this.Ya&&this.e&&Rc(this,a);this.Ya=a;this.X&&this.p&&Nc(this.j(),a)};var Sc=function(a,b){var c=O(a,b);c?c.B(j):-1<a.i&&O(a,a.i).B(n)};W.prototype.B=function(a){Sc(this,mc(this,a))};
+var Tc=function(a){Xc(a,function(a,c){return(a+1)%c},kc(a)-1)},Uc=function(a){Xc(a,function(a,c){a--;return 0>a?c-1:a},0)},Wc=function(a){Xc(a,function(a,c){return(a+1)%c},a.i)},Vc=function(a){Xc(a,function(a,c){a--;return 0>a?c-1:a},a.i)},Xc=function(a,b,c){for(var c=0>c?mc(a,a.h):c,d=kc(a),c=b.call(a,c,d),f=0;f<=d;){var g=O(a,c);if(g&&g.H()&&g.isEnabled()&&g.o&2){a.Ua(c);break}f++;c=b.call(a,c,d)}};W.prototype.Ua=function(a){Sc(this,a)};var Yc=function(){};v(Yc,Q);ba(Yc);o=Yc.prototype;o.n=function(){return"goog-tab"};o.M=function(){return"tab"};o.k=function(a){var b=Yc.d.k.call(this,a);(a=a.Ra())&&this.Ta(b,a);return b};o.K=function(a,b){var b=Yc.d.K.call(this,a,b),c=this.Ra(b);c&&(a.qb=c);if(a.g&8&&(c=a.getParent())&&r(c.W))a.v(8,n),c.W(a);return b};o.Ra=function(a){return a.title||""};o.Ta=function(a,b){a&&(a.title=b||"")};var Zc=function(a,b,c){R.call(this,a,b||Yc.P(),c);Ic(this,8,j);this.V|=9};v(Zc,R);Zc.prototype.Ra=function(){return this.qb};Zc.prototype.Ta=function(a){this.zb().Ta(this.a(),a);this.qb=a};xc("goog-tab",function(){return new Zc(k)});var X=function(){};v(X,V);ba(X);X.prototype.n=function(){return"goog-tab-bar"};X.prototype.M=function(){return"tablist"};X.prototype.Wa=function(a,b,c){this.yb||(this.Ha||$c(this),this.yb=Fa(this.Ha));var d=this.yb[b];d?(Pc(a,ad(d)),a.rb=d):X.d.Wa.call(this,a,b,c)};X.prototype.ta=function(a){var b=X.d.ta.call(this,a);this.Ha||$c(this);b.push(this.Ha[a.rb]);return b};var $c=function(a){var b=a.n();a.Ha={top:b+"-top",bottom:b+"-bottom",start:b+"-start",end:b+"-end"}};var Y=function(a,b,c){a=a||"top";Pc(this,ad(a));this.rb=a;W.call(this,this.O,b||X.P(),c);bd(this)};v(Y,W);o=Y.prototype;o.Tb=j;o.F=k;o.s=function(){Y.d.s.call(this);bd(this)};o.f=function(){Y.d.f.call(this);this.F=k};o.removeChild=function(a,b){cd(this,a);return Y.d.removeChild.call(this,a,b)};o.Ua=function(a){Y.d.Ua.call(this,a);this.Tb&&this.W(O(this,a))};o.W=function(a){a?Gc(a,j):this.F&&Gc(this.F,n)};
+var cd=function(a,b){if(b&&b==a.F){for(var c=mc(a,b),d=c-1;b=O(a,d);d--)if(b.H()&&b.isEnabled()){a.W(b);return}for(c+=1;b=O(a,c);c++)if(b.H()&&b.isEnabled()){a.W(b);return}a.W(k)}};o=Y.prototype;o.bc=function(a){this.F&&this.F!=a.target&&Gc(this.F,n);this.F=a.target};o.cc=function(a){a.target==this.F&&(this.F=k)};o.$b=function(a){cd(this,a.target)};o.ac=function(a){cd(this,a.target)};o.na=function(){O(this,this.i)||this.B(this.F||O(this,0))};
+var bd=function(a){L(L(L(L(gc(a),a,"select",a.bc),a,"unselect",a.cc),a,"disable",a.$b),a,"hide",a.ac)},ad=function(a){return"start"==a||"end"==a?"vertical":"horizontal"};xc("goog-tab-bar",function(){return new Y});var Z=function(a,b,c,d,f){function g(a){a&&(a.tabIndex=0,nc(a,i.M()),D(a,"goog-zippy-header"),dd(i,a),a&&L(i.nb,a,"keydown",i.Ob))}this.m=f||lb();this.Y=this.m.a(a)||k;this.Aa=this.m.a(d||k);this.ea=(this.Qa=r(b)?b:k)||!b?k:this.m.a(b);this.l=c==j;this.nb=new K(this);this.Na=new K(this);var i=this;g(this.Y);g(this.Aa);this.Z(this.l)};v(Z,Yb);o=Z.prototype;o.ha=j;o.kc=j;o.f=function(){Z.d.f.call(this);Hb(this.nb);Hb(this.Na)};o.M=function(){return"tab"};o.A=function(){return this.ea};o.toggle=function(){this.Z(!this.l)};
+o.Z=function(a){this.ea?Zb(this.ea,a):a&&this.Qa&&(this.ea=this.Qa());this.ea&&D(this.ea,"goog-zippy-content");if(this.Aa)Zb(this.Y,!a),Zb(this.Aa,a);else if(this.Y){var b=this.Y;a?D(b,"goog-zippy-expanded"):ib(b,"goog-zippy-expanded");b=this.Y;!a?D(b,"goog-zippy-collapsed"):ib(b,"goog-zippy-collapsed");this.Y.setAttribute("aria-expanded",a)}this.l=a;this.dispatchEvent(new ed("toggle",this))};o.pb=function(){return this.kc};
+o.Pa=function(a){this.ha!=a&&((this.ha=a)?(dd(this,this.Y),dd(this,this.Aa)):Xb(this.Na))};var dd=function(a,b){b&&L(a.Na,b,"click",a.ec)};Z.prototype.Ob=function(a){if(13==a.keyCode||32==a.keyCode)this.toggle(),this.dispatchEvent(new E("action",this)),a.preventDefault(),a.stopPropagation()};Z.prototype.ec=function(){this.toggle();this.dispatchEvent(new E("action",this))};var ed=function(a,b){E.call(this,a,b)};v(ed,E);var gd=function(a,b){this.lb=[];for(var c=mb(a),c=nb("span","ae-zippy",c),d=0,f;f=c[d];d++)this.lb.push(new Z(f,f.parentNode.parentNode.parentNode.nextElementSibling!=h?f.parentNode.parentNode.parentNode.nextElementSibling:ub(f.parentNode.parentNode.parentNode.nextSibling),n));this.dc=new fd(this.lb,mb(b))};gd.prototype.ic=function(){return this.dc};gd.prototype.jc=function(){return this.lb};
+var fd=function(a,b){this.va=a;if(this.va.length)for(var c=0,d;d=this.va[c];c++)I(d,"toggle",this.Vb,n,this);this.Ka=0;this.l=n;c="ae-toggle ae-plus ae-action";this.va.length||(c+=" ae-disabled");this.S=rb("span",{className:c},"Expand All");I(this.S,"click",this.Ub,n,this);b&&b.appendChild(this.S)};fd.prototype.Ub=function(){this.va.length&&this.Z(!this.l)};
+fd.prototype.Vb=function(a){a=a.currentTarget;this.Ka=a.l?this.Ka+1:this.Ka-1;a.l!=this.l&&(a.l?(this.l=j,hd(this,j)):0==this.Ka&&(this.l=n,hd(this,n)))};fd.prototype.Z=function(a){this.l=a;for(var a=0,b;b=this.va[a];a++)b.l!=this.l&&b.Z(this.l);hd(this)};
+var hd=function(a,b){(b!==h?b:a.l)?(ib(a.S,"ae-plus"),D(a.S,"ae-minus"),wb(a.S,"Collapse All")):(ib(a.S,"ae-minus"),D(a.S,"ae-plus"),wb(a.S,"Expand All"))},id=function(a){this.Wb=a;this.Db={};var b,c=rb("div",{},b=rb("div",{id:"ae-stats-details-tabs",className:"goog-tab-bar goog-tab-bar-top"}),rb("div",{className:"goog-tab-bar-clear"}),a=rb("div",{id:"ae-stats-details-tabs-content",className:"goog-tab-content"})),d=new Y;d.K(b);I(d,"select",this.Bb,n,this);I(d,"unselect",this.Bb,n,this);b=0;for(var f;f=
+this.Wb[b];b++)if(f=mb("ae-stats-details-"+f)){var g=nb("h2",k,f)[0],i;i=g;var l=h;fb&&"innerText"in i?l=i.innerText.replace(/(\r\n|\r|\n)/g,"\n"):(l=[],Ab(i,l,j),l=l.join(""));l=l.replace(/ \xAD /g," ").replace(/\xAD/g,"");l=l.replace(/\u200B/g,"");fb||(l=l.replace(/ +/g," "));" "!=l&&(l=l.replace(/^\s*/,""));i=l;tb(g);g=new Zc(i);this.Db[u(g)]=f;d.Ba(g,j);a.appendChild(f);0==b?d.W(g):Zb(f,n)}mb("bd").appendChild(c)};id.prototype.Bb=function(a){var b=this.Db[u(a.target)];Zb(b,"select"==a.type)};
+aa("ae.Stats.Details.Tabs",id);aa("goog.ui.Zippy",Z);Z.prototype.setExpanded=Z.prototype.Z;aa("ae.Stats.MakeZippys",gd);gd.prototype.getExpandCollapse=gd.prototype.ic;gd.prototype.getZippys=gd.prototype.jc;fd.prototype.setExpanded=fd.prototype.Z;var $=function(){this.fb=[];this.jb=[]},jd=[[5,0.2,1],[6,0.2,1.2],[5,0.25,1.25],[6,0.25,1.5],[4,0.5,2],[5,0.5,2.5],[6,0.5,3],[4,1,4],[5,1,5],[6,1,6],[4,2,8],[5,2,10]],kd=function(a){if(0>=a)return[2,0.5,1];for(var b=1;1>a;)a*=10,b/=10;for(;10<=a;)a/=10,b*=10;for(var c=0;c<jd.length;c++)if(a<=jd[c][2])return[jd[c][0],jd[c][1]*b,jd[c][2]*b];return[5,2*b,10*b]};$.prototype.ib="stats/static/pix.gif";$.prototype.z="ae-stats-gantt-";$.prototype.hb=0;$.prototype.write=function(a){this.jb.push(a)};
+var ld=function(a,b,c,d){a.write('<tr class="'+a.z+'axisrow"><td width="20%"></td><td>');a.write('<div class="'+a.z+'axis">');for(var f=0;f<=b;f++)a.write('<img class="'+a.z+'tick" src="'+a.ib+'" alt="" '),a.write('style="left:'+f*c*d+'%"\n>'),a.write('<span class="'+a.z+'scale" style="left:'+f*c*d+'%">'),a.write(" "+f*c+"</span>");a.write("</div></td></tr>\n")};
+$.prototype.hc=function(){this.jb=[];var a=kd(this.hb),b=a[0],c=a[1],a=100/a[2];this.write('<table class="'+this.z+'table">\n');ld(this,b,c,a);for(var d=0;d<this.fb.length;d++){var f=this.fb[d];this.write('<tr class="'+this.z+'datarow"><td width="20%">');0<f.label.length&&(0<f.ia.length&&this.write('<a class="'+this.z+'link" href="'+f.ia+'">'),this.write(f.label),0<f.ia.length&&this.write("</a>"));this.write("</td>\n<td>");this.write('<div class="'+this.z+'container">');0<f.ia.length&&this.write('<a class="'+
this.z+'link" href="'+f.ia+'"\n>');this.write('<img class="'+this.z+'bar" src="'+this.ib+'" alt="" ');this.write('style="left:'+f.start*a+"%;width:"+f.duration*a+'%;min-width:1px"\n>');0<f.gb&&(this.write('<img class="'+this.z+'extra" src="'+this.ib+'" alt="" '),this.write('style="left:'+f.start*a+"%;width:"+f.gb*a+'%"\n>'));0<f.xb.length&&(this.write('<span class="'+this.z+'inline" style="left:'+(f.start+Math.max(f.duration,f.gb))*a+'%"> '),this.write(f.xb),this.write("</span>"));0<f.ia.length&&
-this.write("</a>");this.write("</div></td></tr>\n")}jd(this,b,c,a);this.write("</table>\n");return this.jb.join("")};$.prototype.fc=function(a,b,c,d,f,g){this.hb=Math.max(this.hb,Math.max(b+c,b+d));this.fb.push({label:a,start:b,duration:c,gb:d,xb:f,ia:g})};aa("Gantt",$);$.prototype.add_bar=$.prototype.fc;$.prototype.draw=$.prototype.hc;})();
+this.write("</a>");this.write("</div></td></tr>\n")}ld(this,b,c,a);this.write("</table>\n");return this.jb.join("")};$.prototype.fc=function(a,b,c,d,f,g){this.hb=Math.max(this.hb,Math.max(b+c,b+d));this.fb.push({label:a,start:b,duration:c,gb:d,xb:f,ia:g})};aa("Gantt",$);$.prototype.add_bar=$.prototype.fc;$.prototype.draw=$.prototype.hc;})();
diff --git a/google/appengine/ext/blobstore/blobstore.py b/google/appengine/ext/blobstore/blobstore.py
index 27dc98a..aebcc2a 100755
--- a/google/appengine/ext/blobstore/blobstore.py
+++ b/google/appengine/ext/blobstore/blobstore.py
@@ -66,6 +66,8 @@
'delete_async',
'fetch_data',
'fetch_data_async',
+ 'create_gs_key',
+ 'create_gs_key_async',
'get',
'parse_blob_info']
@@ -83,6 +85,8 @@
create_upload_url_async = blobstore.create_upload_url_async
delete = blobstore.delete
delete_async = blobstore.delete_async
+create_gs_key = blobstore.create_gs_key
+create_gs_key_async = blobstore.create_gs_key_async
class BlobInfoParseError(Error):
diff --git a/google/appengine/ext/bulkload/bulkload_deprecated.py b/google/appengine/ext/bulkload/bulkload_deprecated.py
index d938688..8ed0f3c 100755
--- a/google/appengine/ext/bulkload/bulkload_deprecated.py
+++ b/google/appengine/ext/bulkload/bulkload_deprecated.py
@@ -37,8 +37,6 @@
import httplib
import os
import traceback
-
-import google
import wsgiref.handlers
from google.appengine.api import datastore
diff --git a/google/appengine/ext/datastore_admin/backup_handler.py b/google/appengine/ext/datastore_admin/backup_handler.py
index 59e9a3f..6d385c8 100644
--- a/google/appengine/ext/datastore_admin/backup_handler.py
+++ b/google/appengine/ext/datastore_admin/backup_handler.py
@@ -73,7 +73,7 @@
"""
namespace = handler.request.get('namespace', None)
has_namespace = namespace is not None
- kinds = handler.request.get('kind', allow_multiple=True)
+ kinds = handler.request.get_all('kind')
sizes_known, size_total, remainder = utils.ParseKindsAndSizes(kinds)
notreadonly_warning = capabilities.CapabilitySet(
'datastore_v3', capabilities=['write']).is_enabled()
@@ -110,14 +110,15 @@
Args:
handler: the webapp.RequestHandler invoking the method
"""
- backup_ids = handler.request.get_all('backup_id')
- if backup_ids:
- backups = db.get(backup_ids)
- backup_ids = [backup.key() for backup in backups]
- backup_names = [backup.name for backup in backups]
- else:
- backup_names = []
- backup_ids = []
+ requested_backup_ids = handler.request.get_all('backup_id')
+ backup_names = []
+ backup_ids = []
+ gs_warning = False
+ if requested_backup_ids:
+ for backup in db.get(requested_backup_ids):
+ backup_ids.append(backup.key())
+ backup_names.append(backup.name)
+ gs_warning |= backup.filesystem == files.GS_FILESYSTEM
template_params = {
'form_target': DoBackupDeleteHandler.SUFFIX,
'app_id': handler.request.get('app_id'),
@@ -125,6 +126,7 @@
'backup_ids': backup_ids,
'backup_names': backup_names,
'xsrf_token': utils.CreateXsrfToken(XSRF_ACTION),
+ 'gs_warning': gs_warning
}
utils.RenderToResponse(handler, 'confirm_delete_backup.html',
template_params)
@@ -170,8 +172,8 @@
Status of executed jobs is displayed.
"""
- jobs = self.request.get('job', allow_multiple=True)
- tasks = self.request.get('task', allow_multiple=True)
+ jobs = self.request.get_all('job')
+ tasks = self.request.get_all('task')
error = self.request.get('error', '')
xsrf_error = self.request.get('xsrf_error', '')
@@ -244,7 +246,7 @@
BACKUP_HANDLER = __name__ + '.BackupEntity.map'
BACKUP_COMPLETE_HANDLER = __name__ + '.BackupCompleteHandler'
INPUT_READER = input_readers.__name__ + '.DatastoreEntityInputReader'
- OUTPUT_WRITER = output_writers.__name__ + '.BlobstoreRecordsOutputWriter'
+ OUTPUT_WRITER = output_writers.__name__ + '.FileRecordsOutputWriter'
_get_html_page = 'do_backup.html'
_get_post_html_page = SUFFIX
@@ -257,32 +259,39 @@
if BackupInformation.name_exists(backup):
return [('error', 'Backup "%s" already exists.' % backup)]
- kinds = self.request.get('kind', allow_multiple=True)
+ kinds = self.request.get_all('kind')
queue = self.request.get('queue')
+ filesystem = self.request.get('destination_type',
+ default_value=files.BLOBSTORE_FILESYSTEM)
job_name = 'datastore_backup_%s_%%(kind)s' % re.sub(r'[^\w]', '_', backup)
try:
job_operation = utils.StartOperation('Backup: %s' % backup)
backup_info = BackupInformation(parent=job_operation)
+ backup_info.filesystem = filesystem
backup_info.name = backup
backup_info.kinds = kinds
backup_info.put(config=datastore_rpc.Configuration(force_writes=True))
mapreduce_params = {
'done_callback_handler': self.BACKUP_COMPLETE_HANDLER,
'backup_info_pk': str(backup_info.key()),
- 'force_ops_writes': True
+ 'force_ops_writes': True,
}
+ mapper_spec = dict(self._GetBasicMapperParams())
+ mapper_spec['filesystem'] = filesystem
+ if filesystem == files.GS_FILESYSTEM:
+ mapper_spec['gs_bucket_name'] = self.request.get('gs_bucket_name')
if len(kinds) <= 10:
return [('job', job) for job in _run_map_jobs(
job_operation.key(), backup_info.key(), kinds, job_name,
self.BACKUP_HANDLER, self.INPUT_READER, self.OUTPUT_WRITER,
- self._GetBasicMapperParams(), mapreduce_params, queue)]
+ mapper_spec, mapreduce_params, queue)]
else:
retry_options = taskqueue.TaskRetryOptions(task_retry_limit=1)
return [('task', deferred.defer(_run_map_jobs, job_operation.key(),
backup_info.key(), kinds, job_name,
self.BACKUP_HANDLER, self.INPUT_READER,
self.OUTPUT_WRITER,
- self._GetBasicMapperParams(),
+ mapper_spec,
mapreduce_params,
queue, _queue=queue,
_url=utils.ConfigDefaults.DEFERRED_PATH,
@@ -337,8 +346,11 @@
try:
for backup_info in db.get(backup_ids):
if backup_info:
- blobstore_api.delete([files.blobstore.get_blob_key(filename)
- for filename in backup_info.blob_files])
+
+ if backup_info.filesystem == files.BLOBSTORE_FILESYSTEM:
+
+ blobstore_api.delete([files.blobstore.get_blob_key(filename)
+ for filename in backup_info.blob_files])
backup_info.delete()
except Exception, e:
logging.exception('Failed to delete datastore backup.')
@@ -405,6 +417,7 @@
name = db.StringProperty()
kinds = db.StringListProperty()
+ filesystem = db.StringProperty(default=files.BLOBSTORE_FILESYSTEM)
start_time = db.DateTimeProperty(auto_now_add=True)
active_jobs = db.StringListProperty()
completed_jobs = db.StringListProperty()
@@ -431,6 +444,8 @@
filenames = mapreduce_state.writer_state['filenames']
+ if backup_info.filesystem == files.BLOBSTORE_FILESYSTEM:
+ filenames = drop_empty_files(filenames)
backup_info.blob_files = list(set(backup_info.blob_files + filenames))
if job_id in backup_info.active_jobs:
backup_info.active_jobs.remove(job_id)
@@ -438,23 +453,20 @@
set(backup_info.completed_jobs + [job_id]))
backup_info.put(config=datastore_rpc.Configuration(force_writes=True))
if operation.status == utils.DatastoreAdminOperation.STATUS_COMPLETED:
- deferred.defer(finalize_backup_info, backup_info.key(),
- _url=utils.ConfigDefaults.DEFERRED_PATH)
+ finalize_backup_info(backup_info)
else:
logging.warn('BackupInfo was not found for %s',
mapreduce_spec.params['backup_info_pk'])
-def finalize_backup_info(backup_info_key):
- backup_info = BackupInformation.get(backup_info_key)
+def finalize_backup_info(backup_info):
backup_info.complete_time = datetime.datetime.now()
-
- backup_info.blob_files = drop_empty_files(backup_info.blob_files)
backup_info.put(config=datastore_rpc.Configuration(force_writes=True))
logging.info('Backup %s completed', backup_info.name)
+@db.non_transactional
def drop_empty_files(filenames):
"""Deletes empty files and returns filenames minus the deleted ones."""
non_empty_filenames = []
diff --git a/google/appengine/ext/datastore_admin/copy_handler.py b/google/appengine/ext/datastore_admin/copy_handler.py
index 2b685b5..97f0660 100755
--- a/google/appengine/ext/datastore_admin/copy_handler.py
+++ b/google/appengine/ext/datastore_admin/copy_handler.py
@@ -63,7 +63,7 @@
handler: the webapp.RequestHandler invoking the method
"""
namespace = handler.request.get('namespace')
- kinds = handler.request.get('kind', allow_multiple=True)
+ kinds = handler.request.get_all('kind')
sizes_known, size_total, remainder = utils.ParseKindsAndSizes(kinds)
(namespace_str, kind_str) = utils.GetPrintableStrs(namespace, kinds)
@@ -108,7 +108,7 @@
Status of executed jobs is displayed.
"""
- jobs = self.request.get('job', allow_multiple=True)
+ jobs = self.request.get_all('job')
error = self.request.get('error', '')
xsrf_error = self.request.get('xsrf_error', '')
@@ -127,7 +127,7 @@
Jobs are executed and user is redirected to the get handler.
"""
namespace = self.request.get('namespace')
- kinds = self.request.get('kind', allow_multiple=True)
+ kinds = self.request.get_all('kind')
(namespace_str, kinds_str) = utils.GetPrintableStrs(namespace, kinds)
token = self.request.get('xsrf_token')
remote_url = self.request.get('remote_url')
diff --git a/google/appengine/ext/datastore_admin/delete_handler.py b/google/appengine/ext/datastore_admin/delete_handler.py
index b750208..7a946c7 100755
--- a/google/appengine/ext/datastore_admin/delete_handler.py
+++ b/google/appengine/ext/datastore_admin/delete_handler.py
@@ -81,7 +81,7 @@
handler: the webapp.RequestHandler invoking the method
"""
namespace = handler.request.get('namespace')
- kinds = handler.request.get('kind', allow_multiple=True)
+ kinds = handler.request.get_all('kind')
sizes_known, size_total, remainder = utils.ParseKindsAndSizes(kinds)
(namespace_str, kind_str) = utils.GetPrintableStrs(namespace, kinds)
@@ -119,7 +119,7 @@
Status of executed jobs is displayed.
"""
- jobs = self.request.get('job', allow_multiple=True)
+ jobs = self.request.get_all('job')
error = self.request.get('error', '')
xsrf_error = self.request.get('xsrf_error', '')
@@ -138,7 +138,7 @@
Jobs are executed and user is redirected to the get handler.
"""
namespace = self.request.get('namespace')
- kinds = self.request.get('kind', allow_multiple=True)
+ kinds = self.request.get_all('kind')
(namespace_str, kinds_str) = utils.GetPrintableStrs(namespace, kinds)
token = self.request.get('xsrf_token')
diff --git a/google/appengine/ext/datastore_admin/main.py b/google/appengine/ext/datastore_admin/main.py
index aeadd79..6b66243 100755
--- a/google/appengine/ext/datastore_admin/main.py
+++ b/google/appengine/ext/datastore_admin/main.py
@@ -176,19 +176,60 @@
def post(self):
self.RouteAction(GET_ACTIONS)
- def GetKinds(self):
- """Obtain list of all entity kinds from the datastore."""
- kinds = metadata.Kind.all().fetch(99999999)
+ def GetKinds(self, all_ns=True):
+ """Obtain a list of all kind names from the datastore.
+
+ Args:
+ all_ns: If true, list kind names for all namespaces.
+ If false, list kind names only for the current namespace.
+
+ Returns:
+ An alphabetized list of kinds for the specified namespace(s).
+ """
+ if all_ns:
+ result = self.GetKindsForAllNamespaces()
+ else:
+ result = self.GetKindsForCurrentNamespace()
+ return result
+
+ def GetKindsForAllNamespaces(self):
+ """Obtain a list of all kind names from the datastore, *regardless*
+ of namespace. The result is alphabetized and deduped."""
+
+
+ namespace_list = [ns.namespace_name
+ for ns in metadata.Namespace.all().run(limit=99999999)]
+ kind_itr_list = [metadata.Kind.all(namespace=ns).run(limit=99999999,
+ batch_size=99999999)
+ for ns in namespace_list]
+
+
+ kind_name_set = set()
+ for kind_itr in kind_itr_list:
+ for kind in kind_itr:
+ kind_name = kind.kind_name
+ if self.__IsVisibleKindName(kind_name):
+ kind_name_set.add(kind.kind_name)
+
+ kind_name_list = sorted(kind_name_set)
+ return kind_name_list
+
+ def GetKindsForCurrentNamespace(self):
+ """Obtain a list of all kind names from the datastore for the
+ current namespace. The result is alphabetized."""
+ kinds = metadata.Kind.all().order('__key__').fetch(99999999)
kind_names = []
for kind in kinds:
kind_name = kind.kind_name
- if (kind_name.startswith('__') or
- kind_name == utils.DatastoreAdminOperation.kind() or
- kind_name == backup_handler.BackupInformation.kind()):
- continue
- kind_names.append(kind_name)
+ if self.__IsVisibleKindName(kind_name):
+ kind_names.append(kind_name)
return kind_names
+ def __IsVisibleKindName(self, kind_name):
+ return not (kind_name.startswith('__') or
+ kind_name == utils.DatastoreAdminOperation.kind() or
+ kind_name == backup_handler.BackupInformation.kind())
+
def GetOperations(self, active=False, limit=100):
"""Obtain a list of operation, ordered by last_updated."""
query = utils.DatastoreAdminOperation.all()
diff --git a/google/appengine/ext/datastore_admin/static/css/compiled.css b/google/appengine/ext/datastore_admin/static/css/compiled.css
index 164e81a..7106632 100755
--- a/google/appengine/ext/datastore_admin/static/css/compiled.css
+++ b/google/appengine/ext/datastore_admin/static/css/compiled.css
@@ -1,2 +1,2 @@
/* Copyright 2012 Google Inc. All Rights Reserved. */
-html,body,div,h1,h2,h3,h4,h5,h6,p,img,dl,dt,dd,ol,ul,li,table,caption,tbody,tfoot,thead,tr,th,td,form,fieldset,embed,object,applet{margin:0;padding:0;border:0;}body{font-size:62.5%;font-family:Arial,sans-serif;color:#000;background:#fff}a{color:#00c}a:active{color:#f00}a:visited{color:#551a8b}table{border-collapse:collapse;border-width:0;empty-cells:show}ul{padding:0 0 1em 1em}ol{padding:0 0 1em 1.3em}li{line-height:1.5em;padding:0 0 .5em 0}p{padding:0 0 1em 0}h1,h2,h3,h4,h5{padding:0 0 1em 0}h1,h2{font-size:1.3em}h3{font-size:1.1em}h4,h5,table{font-size:1em}sup,sub{font-size:.7em}input,select,textarea,option{font-family:inherit;font-size:inherit}.g-doc,.g-doc-1024,.g-doc-800{font-size:130%}.g-doc{width:100%;text-align:left}.g-section{width:100%;vertical-align:top;display:inline-block}*:first-child+html .g-section{display:block}* html .g-section{overflow:hidden}@-moz-document url-prefix(''){.g-section{overflow:hidden}}@-moz-document url-prefix(''){.g-section,tt:default{overflow:visible}}.g-section,.g-unit{zoom:1}.g-split .g-unit{text-align:right}.g-split .g-first{text-align:left}.g-doc-1024{width:73.074em;min-width:950px;margin:0 auto;text-align:left}* html .g-doc-1024{width:71.313em}*+html .g-doc-1024{width:71.313em}.g-doc-800{width:57.69em;min-width:750px;margin:0 auto;text-align:left}* html .g-doc-800{width:56.3em}*+html .g-doc-800{width:56.3em}.g-tpl-160 .g-unit,.g-unit .g-tpl-160 .g-unit,.g-unit .g-unit .g-tpl-160 .g-unit,.g-unit .g-unit .g-unit .g-tpl-160 .g-unit{margin:0 0 0 160px;width:auto;float:none}.g-unit .g-unit .g-unit .g-tpl-160 .g-first,.g-unit .g-unit .g-tpl-160 .g-first,.g-unit .g-tpl-160 .g-first,.g-tpl-160 .g-first{margin:0;width:160px;float:left}.g-tpl-160-alt .g-unit,.g-unit .g-tpl-160-alt .g-unit,.g-unit .g-unit .g-tpl-160-alt .g-unit,.g-unit .g-unit .g-unit .g-tpl-160-alt .g-unit{margin:0 160px 0 0;width:auto;float:none}.g-unit .g-unit .g-unit .g-tpl-160-alt .g-first,.g-unit .g-unit .g-tpl-160-alt .g-first,.g-unit .g-tpl-160-alt .g-first,.g-tpl-160-alt .g-first{margin:0;width:160px;float:right}.g-tpl-180 .g-unit,.g-unit .g-tpl-180 .g-unit,.g-unit .g-unit .g-tpl-180 .g-unit,.g-unit .g-unit .g-unit .g-tpl-180 .g-unit{margin:0 0 0 180px;width:auto;float:none}.g-unit .g-unit .g-unit .g-tpl-180 .g-first,.g-unit .g-unit .g-tpl-180 .g-first,.g-unit .g-tpl-180 .g-first,.g-tpl-180 .g-first{margin:0;width:180px;float:left}.g-tpl-180-alt .g-unit,.g-unit .g-tpl-180-alt .g-unit,.g-unit .g-unit .g-tpl-180-alt .g-unit,.g-unit .g-unit .g-unit .g-tpl-180-alt .g-unit{margin:0 180px 0 0;width:auto;float:none}.g-unit .g-unit .g-unit .g-tpl-180-alt .g-first,.g-unit .g-unit .g-tpl-180-alt .g-first,.g-unit .g-tpl-180-alt .g-first,.g-tpl-180-alt .g-first{margin:0;width:180px;float:right}.g-tpl-300 .g-unit,.g-unit .g-tpl-300 .g-unit,.g-unit .g-unit .g-tpl-300 .g-unit,.g-unit .g-unit .g-unit .g-tpl-300 .g-unit{margin:0 0 0 300px;width:auto;float:none}.g-unit .g-unit .g-unit .g-tpl-300 .g-first,.g-unit .g-unit .g-tpl-300 .g-first,.g-unit .g-tpl-300 .g-first,.g-tpl-300 .g-first{margin:0;width:300px;float:left}.g-tpl-300-alt .g-unit,.g-unit .g-tpl-300-alt .g-unit,.g-unit .g-unit .g-tpl-300-alt .g-unit,.g-unit .g-unit .g-unit .g-tpl-300-alt .g-unit{margin:0 300px 0 0;width:auto;float:none}.g-unit .g-unit .g-unit .g-tpl-300-alt .g-first,.g-unit .g-unit .g-tpl-300-alt .g-first,.g-unit .g-tpl-300-alt .g-first,.g-tpl-300-alt .g-first{margin:0;width:300px;float:right}.g-tpl-25-75 .g-unit,.g-unit .g-tpl-25-75 .g-unit,.g-unit .g-unit .g-tpl-25-75 .g-unit,.g-unit .g-unit .g-unit .g-tpl-25-75 .g-unit{width:74.999%;float:right;margin:0}.g-unit .g-unit .g-unit .g-tpl-25-75 .g-first,.g-unit .g-unit .g-tpl-25-75 .g-first,.g-unit .g-tpl-25-75 .g-first,.g-tpl-25-75 .g-first{width:24.999%;float:left;margin:0}.g-tpl-25-75-alt .g-unit,.g-unit .g-tpl-25-75-alt .g-unit,.g-unit .g-unit .g-tpl-25-75-alt .g-unit,.g-unit .g-unit .g-unit .g-tpl-25-75-alt .g-unit{width:24.999%;float:left;margin:0}.g-unit .g-unit .g-unit .g-tpl-25-75-alt .g-first,.g-unit .g-unit .g-tpl-25-75-alt .g-first,.g-unit .g-tpl-25-75-alt .g-first,.g-tpl-25-75-alt .g-first{width:74.999%;float:right;margin:0}.g-tpl-75-25 .g-unit,.g-unit .g-tpl-75-25 .g-unit,.g-unit .g-unit .g-tpl-75-25 .g-unit,.g-unit .g-unit .g-unit .g-tpl-75-25 .g-unit{width:24.999%;float:right;margin:0}.g-unit .g-unit .g-unit .g-tpl-75-25 .g-first,.g-unit .g-unit .g-tpl-75-25 .g-first,.g-unit .g-tpl-75-25 .g-first,.g-tpl-75-25 .g-first{width:74.999%;float:left;margin:0}.g-tpl-75-25-alt .g-unit,.g-unit .g-tpl-75-25-alt .g-unit,.g-unit .g-unit .g-tpl-75-25-alt .g-unit,.g-unit .g-unit .g-unit .g-tpl-75-25-alt .g-unit{width:74.999%;float:left;margin:0}.g-unit .g-unit .g-unit .g-tpl-75-25-alt .g-first,.g-unit .g-unit .g-tpl-75-25-alt .g-first,.g-unit .g-tpl-75-25-alt .g-first,.g-tpl-75-25-alt .g-first{width:24.999%;float:right;margin:0}.g-tpl-33-67 .g-unit,.g-unit .g-tpl-33-67 .g-unit,.g-unit .g-unit .g-tpl-33-67 .g-unit,.g-unit .g-unit .g-unit .g-tpl-33-67 .g-unit{width:66.999%;float:right;margin:0}.g-unit .g-unit .g-unit .g-tpl-33-67 .g-first,.g-unit .g-unit .g-tpl-33-67 .g-first,.g-unit .g-tpl-33-67 .g-first,.g-tpl-33-67 .g-first{width:32.999%;float:left;margin:0}.g-tpl-33-67-alt .g-unit,.g-unit .g-tpl-33-67-alt .g-unit,.g-unit .g-unit .g-tpl-33-67-alt .g-unit,.g-unit .g-unit .g-unit .g-tpl-33-67-alt .g-unit{width:32.999%;float:left;margin:0}.g-unit .g-unit .g-unit .g-tpl-33-67-alt .g-first,.g-unit .g-unit .g-tpl-33-67-alt .g-first,.g-unit .g-tpl-33-67-alt .g-first,.g-tpl-33-67-alt .g-first{width:66.999%;float:right;margin:0}.g-tpl-67-33 .g-unit,.g-unit .g-tpl-67-33 .g-unit,.g-unit .g-unit .g-tpl-67-33 .g-unit,.g-unit .g-unit .g-unit .g-tpl-67-33 .g-unit{width:32.999%;float:right;margin:0}.g-unit .g-unit .g-unit .g-tpl-67-33 .g-first,.g-unit .g-unit .g-tpl-67-33 .g-first,.g-unit .g-tpl-67-33 .g-first,.g-tpl-67-33 .g-first{width:66.999%;float:left;margin:0}.g-tpl-67-33-alt .g-unit,.g-unit .g-tpl-67-33-alt .g-unit,.g-unit .g-unit .g-tpl-67-33-alt .g-unit,.g-unit .g-unit .g-unit .g-tpl-67-33-alt .g-unit{width:66.999%;float:left;margin:0}.g-unit .g-unit .g-unit .g-tpl-67-33-alt .g-first,.g-unit .g-unit .g-tpl-67-33-alt .g-first,.g-unit .g-tpl-67-33-alt .g-first,.g-tpl-67-33-alt .g-first{width:32.999%;float:right;margin:0}.g-tpl-50-50 .g-unit,.g-unit .g-tpl-50-50 .g-unit,.g-unit .g-unit .g-tpl-50-50 .g-unit,.g-unit .g-unit .g-unit .g-tpl-50-50 .g-unit{width:49.999%;float:right;margin:0}.g-unit .g-unit .g-unit .g-tpl-50-50 .g-first,.g-unit .g-unit .g-tpl-50-50 .g-first,.g-unit .g-tpl-50-50 .g-first,.g-tpl-50-50 .g-first{width:49.999%;float:left;margin:0}.g-tpl-50-50-alt .g-unit,.g-unit .g-tpl-50-50-alt .g-unit,.g-unit .g-unit .g-tpl-50-50-alt .g-unit,.g-unit .g-unit .g-unit .g-tpl-50-50-alt .g-unit{width:49.999%;float:left;margin:0}.g-unit .g-unit .g-unit .g-tpl-50-50-alt .g-first,.g-unit .g-unit .g-tpl-50-50-alt .g-first,.g-unit .g-tpl-50-50-alt .g-first,.g-tpl-50-50-alt .g-first{width:49.999%;float:right;margin:0}.g-tpl-nest{width:auto}.g-tpl-nest .g-section{display:inline}.g-tpl-nest .g-unit,.g-unit .g-tpl-nest .g-unit,.g-unit .g-unit .g-tpl-nest .g-unit,.g-unit .g-unit .g-unit .g-tpl-nest .g-unit{float:left;width:auto;margin:0}.g-tpl-nest-alt .g-unit,.g-unit .g-tpl-nest-alt .g-unit,.g-unit .g-unit .g-tpl-nest-alt .g-unit,.g-unit .g-unit .g-unit .g-tpl-nest-alt .g-unit{float:right;width:auto;margin:0}.goog-button{border-width:1px;border-style:solid;border-color:#bbb #999 #999 #bbb;border-radius:2px;-webkit-border-radius:2px;-moz-border-radius:2px;font:normal normal normal 13px/13px Arial,sans-serif;color:#000;text-align:middle;text-decoration:none;text-shadow:0 1px 1px rgba(255,255,255,1);background:#eee;background-image:-webkit-gradient(linear,0 0,0 100%,from(#fff),to(#ddd));background:-moz-linear-gradient(top,#fff,#ddd);filter:progid:DXImageTransform.Microsoft.Gradient(EndColorstr='#dddddd',StartColorstr='#ffffff',GradientType=0);cursor:pointer;margin:0;display:inline;display:-moz-inline-box;display:inline-block;*overflow:visible;padding:4px 8px 5px}a.goog-button,span.goog-button,div.goog-button{padding:4px 8px 5px}.goog-button:visited{color:#000}.goog-button{*display:inline}.goog-button:focus,.goog-button:hover{border-color:#000}.goog-button:active,.goog-button-active{color:#000;background-color:#bbb;border-color:#999 #bbb #bbb #999;background-image:-webkit-gradient(linear,0 0,0 100%,from(#ddd),to(#fff));background-image:-moz-linear-gradient(top,#ddd,#fff);filter:progid:DXImageTransform.Microsoft.Gradient(EndColorstr='#ffffff',StartColorstr='#dddddd',GradientType=0)}.goog-button[disabled],.goog-button[disabled]:active,.goog-button[disabled]:hover{color:#666;border-color:#ddd;background-color:#f3f3f3;background-image:none;text-shadow:none;cursor:auto}.goog-button{padding:5px 8px 4px }.goog-button{*padding:4px 7px 2px}html>body input.goog-button,x:-moz-any-link,x:default,html>body button.goog-button,x:-moz-any-link,x:default{padding-top:3px;padding-bottom:2px}a.goog-button,x:-moz-any-link,x:default,span.goog-button,x:-moz-any-link,x:default,div.goog-button,x:-moz-any-link,x:default{padding:4px 8px 5px}.goog-button-fixed{padding-left:0!important;padding-right:0!important;width:100%}button.goog-button-icon-c{padding-top:1px;padding-bottom:1px}button.goog-button-icon-c{padding-top:3px ;padding-bottom:2px }button.goog-button-icon-c{*padding-top:0;*padding-bottom:0}html>body button.goog-button-icon-c,x:-moz-any-link,x:default{padding-top:1px;padding-bottom:1px}.goog-button-icon{display:block;margin:0 auto;height:18px;width:18px}html>body .goog-inline-block{display:-moz-inline-box;display:inline-block;}.goog-inline-block{position:relative;display:inline-block}* html .goog-inline-block{display:inline}*:first-child+html .goog-inline-block{display:inline}.goog-custom-button{margin:0 2px 2px;border:0;padding:0;font:normal Tahoma,Arial,sans-serif;color:#000;text-decoration:none;list-style:none;vertical-align:middle;cursor:pointer;outline:none;background:#eee;background-image:-webkit-gradient(linear,0 0,0 100%,from(#fff),to(#ddd));background:-moz-linear-gradient(top,#fff,#ddd);filter:progid:DXImageTransform.Microsoft.Gradient(EndColorstr='#dddddd',StartColorstr='#ffffff',GradientType=0)}.goog-custom-button-outer-box,.goog-custom-button-inner-box{border-style:solid;border-color:#bbb #999 #999 #bbb;vertical-align:top}.goog-custom-button-outer-box{margin:0;border-width:1px 0;padding:0}.goog-custom-button-inner-box{margin:0 -1px;border-width:0 1px;padding:3px 4px}* html .goog-custom-button-inner-box{left:-1px}* html .goog-custom-button-rtl .goog-custom-button-outer-box{left:-1px}* html .goog-custom-button-rtl .goog-custom-button-inner-box{left:0}*:first-child+html .goog-custom-button-inner-box{left:-1px}*:first-child+html .goog-custom-button-collapse-right .goog-custom-button-inner-box{border-left-width:2px}*:first-child+html .goog-custom-button-collapse-left .goog-custom-button-inner-box{border-right-width:2px}*:first-child+html .goog-custom-button-collapse-right.goog-custom-button-collapse-left .goog-custom-button-inner-box{border-width:0 1px}*:first-child+html .goog-custom-button-rtl .goog-custom-button-inner-box{left:1px}::root .goog-custom-button,::root .goog-custom-button-outer-box{line-height:0}::root .goog-custom-button-inner-box{line-height:normal}.goog-custom-button-disabled{background-image:none!important;opacity:0.4;-moz-opacity:0.4;filter:alpha(opacity=40)}.goog-custom-button-disabled .goog-custom-button-outer-box,.goog-custom-button-disabled .goog-custom-button-inner-box{color:#333!important;border-color:#999!important}* html .goog-custom-button-disabled{margin:2px 1px!important;padding:0 1px!important}*:first-child+html .goog-custom-button-disabled{margin:2px 1px!important;padding:0 1px!important}.goog-custom-button-hover .goog-custom-button-outer-box,.goog-custom-button-hover .goog-custom-button-inner-box{border-color:#000!important;}.goog-custom-button-active,.goog-custom-button-checked{background-color:#bbb;background-position:bottom left;background-image:-webkit-gradient(linear,0 0,0 100%,from(#ddd),to(#fff));background:-moz-linear-gradient(top,#ddd,#fff);filter:progid:DXImageTransform.Microsoft.Gradient(EndColorstr='#ffffff',StartColorstr='#dddddd',GradientType=0)}.goog-custom-button-focused .goog-custom-button-outer-box,.goog-custom-button-focused .goog-custom-button-inner-box,.goog-custom-button-focused.goog-custom-button-collapse-left .goog-custom-button-inner-box,.goog-custom-button-focused.goog-custom-button-collapse-left.goog-custom-button-checked .goog-custom-button-inner-box{border-color:#000}.goog-custom-button-collapse-right,.goog-custom-button-collapse-right .goog-custom-button-outer-box,.goog-custom-button-collapse-right .goog-custom-button-inner-box{margin-right:0}.goog-custom-button-collapse-left,.goog-custom-button-collapse-left .goog-custom-button-outer-box,.goog-custom-button-collapse-left .goog-custom-button-inner-box{margin-left:0}.goog-custom-button-collapse-left .goog-custom-button-inner-box{border-left:1px solid #fff}.goog-custom-button-collapse-left.goog-custom-button-checked .goog-custom-button-inner-box{border-left:1px solid #ddd}* html .goog-custom-button-collapse-left .goog-custom-button-inner-box{left:0}*:first-child+html .goog-custom-button-collapse-left .goog-custom-button-inner-box{left:0}.goog-date-picker th,.goog-date-picker td{font-family:arial,sans-serif;text-align:center}.goog-date-picker th{font-size:.9em;font-weight:bold;color:#666667;background-color:#c3d9ff}.goog-date-picker td{vertical-align:middle;padding:2px 3px}.goog-date-picker{-moz-user-focus:normal;-moz-user-select:none;position:absolute;border:1px solid gray;float:left;font-family:arial,sans-serif;padding-left:1px;background:white}.goog-date-picker-menu{position:absolute;background:threedface;border:1px solid gray;-moz-user-focus:normal}.goog-date-picker-menu ul{list-style:none;margin:0;padding:0}.goog-date-picker-menu ul li{cursor:default}.goog-date-picker-menu-selected{background-color:#aaccee}.goog-date-picker td div{float:left}.goog-date-picker button{padding:0;margin:1px;border:1px outset gray}.goog-date-picker-week{padding:1px 3px}.goog-date-picker-wday{padding:1px 3px}.goog-date-picker-today-cont{text-align:left!important}.goog-date-picker-none-cont{text-align:right!important}.goog-date-picker-head td{text-align:center}.goog-date-picker-month{width:12ex}.goog-date-picker-year{width:6ex}.goog-date-picker table{border-collapse:collapse}.goog-date-picker-selected{background-color:#aaccee!important;color:blue!important}.goog-date-picker-today{font-weight:bold!important}.goog-date-picker-other-month{-moz-opacity:0.3;filter:Alpha(Opacity=30)}.sat,.sun{background:#eee}#button1,#button2{display:block;width:60px;text-align:center;margin:10px;padding:10px;font:normal .8em arial,sans-serif;border:1px solid #000}.goog-menu{position:absolute;color:#000;border:1px solid #b5b6b5;background-color:#f3f3f7;cursor:default;font:normal small arial,helvetica,sans-serif;margin:0;padding:0;outline:none}.goog-menuitem{padding:2px 5px;margin:0;list-style:none}.goog-menuitem-highlight{background-color:#4279a5;color:#fff}.goog-menuitem-disabled{color:#999}.goog-option{padding-left:15px!important}.goog-option-selected{background-image:url('/img/check.gif');background-position:4px 50%;background-repeat:no-repeat}.goog-menuseparator{position:relative;margin:2px 0;border-top:1px solid #999;padding:0;outline:none}.goog-submenu{position:relative}.goog-submenu-arrow{position:absolute;display:block;width:11px;height:11px;right:3px;top:4px;background-image:url('/img/menu-arrows.gif');background-repeat:no-repeat;background-position:0 0;font-size:1px}.goog-menuitem-highlight .goog-submenu-arrow{background-position:0 -11px}.goog-menuitem-disabled .goog-submenu-arrow{display:none}.goog-menu-filter{margin:2px;border:1px solid silver;background:white;overflow:hidden}.goog-menu-filter div{color:gray;position:absolute;padding:1px}.goog-menu-filter input{margin:0;border:0;background:transparent;width:100%}.goog-menuitem-partially-checked{background-image:url('/img/check-outline.gif');background-position:4px 50%;background-repeat:no-repeat}.goog-menuitem-fully-checked{background-image:url('/img/check.gif');background-position:4px 50%;background-repeat:no-repeat}.goog-menu-button{margin:0 2px 2px 2px;border:0;padding:0;font:normal Tahoma,Arial,sans-serif;color:#000;background:#ddd url("/img/button-bg.gif") repeat-x top left;text-decoration:none;list-style:none;vertical-align:middle;cursor:pointer;outline:none}.goog-menu-button-outer-box,.goog-menu-button-inner-box{border-style:solid;border-color:#aaa;vertical-align:middle}.goog-menu-button-outer-box{margin:0;border-width:1px 0;padding:0}.goog-menu-button-inner-box{margin:0 -1px;border-width:0 1px;padding:0 4px 2px 4px}* html .goog-menu-button-inner-box{left:-1px}* html .goog-menu-button-rtl .goog-menu-button-outer-box{left:-1px}* html .goog-menu-button-rtl .goog-menu-button-inner-box{left:0}*:first-child+html .goog-menu-button-inner-box{left:-1px}*:first-child+html .goog-menu-button-rtl .goog-menu-button-inner-box{left:1px}::root .goog-menu-button,::root .goog-menu-button-outer-box,::root .goog-menu-button-inner-box{line-height:0}::root .goog-menu-button-caption,::root .goog-menu-button-dropdown{line-height:normal}.goog-menu-button-disabled{background-image:none!important;opacity:0.4;-moz-opacity:0.4;filter:alpha(opacity=40)}.goog-menu-button-disabled .goog-menu-button-outer-box,.goog-menu-button-disabled .goog-menu-button-inner-box,.goog-menu-button-disabled .goog-menu-button-caption,.goog-menu-button-disabled .goog-menu-button-dropdown{color:#333!important;border-color:#999!important}* html .goog-menu-button-disabled{margin:2px 1px!important;padding:0 1px!important}*:first-child+html .goog-menu-button-disabled{margin:2px 1px!important;padding:0 1px!important}.goog-menu-button-hover .goog-menu-button-outer-box,.goog-menu-button-hover .goog-menu-button-inner-box{border-color:#9cf #69e #69e #7af!important;}.goog-menu-button-active,.goog-menu-button-open{background-color:#bbb;background-position:bottom left}.goog-menu-button-focused .goog-menu-button-outer-box,.goog-menu-button-focused .goog-menu-button-inner-box{border-color:#3366cc}.goog-menu-button-caption{padding:0 4px 0 0;vertical-align:middle}.goog-menu-button-rtl .goog-menu-button-caption{padding:0 0 0 4px}.goog-menu-button-dropdown{width:7px;background:url('/img/toolbar_icons.gif') no-repeat -176px;vertical-align:middle}.goog-flat-menu-button{margin:0 2px;padding:1px 4px;font:normal 95% Tahoma,Arial,sans-serif;color:#333;text-decoration:none;list-style:none;vertical-align:middle;cursor:pointer;outline:none;-moz-outline:none;border-width:1px;border-style:solid;border-color:#c9c9c9;background-color:#fff}.goog-flat-menu-button-disabled *{color:#999;border-color:#ccc;cursor:default}.goog-flat-menu-button-hover,.goog-flat-menu-button-hover{border-color:#9cf #69e #69e #7af!important;}.goog-flat-menu-button-active{background-color:#bbb;background-position:bottom left}.goog-flat-menu-button-focused{border-color:#3366cc}.goog-flat-menu-button-caption{padding-right:10px;vertical-align:middle}.goog-flat-menu-button-dropdown{width:7px;background:url('/img/toolbar_icons.gif') no-repeat -176px;vertical-align:middle}h1{font-size:1.8em}.g-doc{width:auto;margin:0 10px}.g-doc-1024{margin-left:10px}#ae-logo{background:url('//www.google.com/images/logos/app_engine_logo_sm.gif') 0 0 no-repeat;display:block;width:178px;height:30px;margin:4px 0 0 0}.ae-ir span{position:absolute;display:block;width:0;height:0;overflow:hidden}.ae-noscript{position:absolute;left:-5000px}#ae-lhs-nav{border-right:3px solid #e5ecf9}.ae-notification{margin-bottom:.6em;text-align:center}.ae-notification strong{display:block;width:55%;margin:0 auto;text-align:center;padding:.6em;background-color:#fff1a8;font-weight:bold}.ae-alert{font-weight:bold;background:url('/img/icn/warning.png') no-repeat;margin-bottom:.5em;padding-left:1.8em}.ae-info{background:url('/img/icn/icn-info.gif') no-repeat;margin-bottom:.5em;padding-left:1.8em}.ae-promo{padding:.5em .8em;margin:.6em 0;background-color:#fffbe8;border:1px solid #fff1a9;text-align:left}.ae-promo strong{position:relative;top:.3em}.ae-alert-text,.ae-warning-text{background-color:transparent;background-position:right 1px;padding:0 18px 0 0}.ae-alert-text{color:#c00}.ae-warning-text{color:#f90}.ae-alert-c span{display:inline-block}.ae-message{border:1px solid #e5ecf9;background-color:#f6f9ff;margin-bottom:1em;padding:.5em}.ae-errorbox{border:1px solid #f00;background-color:#fee;margin-bottom:1em;padding:1em}#bd .ae-errorbox ul{padding-bottom:0}.ae-form dt{font-weight:bold}.ae-form dt em,.ae-field-hint{margin-top:.2em;color:#666667;font-size:.85em}.ae-field-yyyymmdd,.ae-field-hhmmss{width:6em}.ae-field-hint-hhmmss{margin-left:2.3em}.ae-form label{display:block;margin:0 0 .2em 0;font-weight:bold}.ae-radio{margin-bottom:.3em}.ae-radio label{display:inline}.ae-form dd,.ae-input-row{margin-bottom:.6em}.ae-input-row-group{border:1px solid #fff1a9;background:#fffbe8;padding:8px}.ae-btn-row{margin-top:1.4em;margin-bottom:1em}.ae-btn-row-note{padding:5px 0 6px 0}.ae-btn-row-note span{padding-left:18px;padding-right:.5em;background:transparent url('/img/icn/icn-info.gif') 0 0 no-repeat}.ae-btn-primary{font-weight:bold}form .ae-cancel{margin-left:.5em}.ae-submit-inline{margin-left:.8em}.ae-radio-bullet{width:20px;float:left}.ae-label-hanging-indent{margin-left:5px}.ae-divider{margin:0 .6em 0 .5em}.ae-nowrap{white-space:nowrap}.ae-pre-wrap{white-space:pre-wrap;white-space:-moz-pre-wrap;white-space:-pre-wrap;white-space:-o-pre-wrap;word-wrap:break-word;_white-space:pre;}wbr:after{content:"\00200b"}a button{text-decoration:none}.ae-alert ul{margin-bottom:.75em;margin-top:.25em;line-height:1.5em}.ae-alert h4{color:#000;font-weight:bold;padding:0 0 .5em}.ae-form-simple-list{list-style-type:none;padding:0;margin-bottom:1em}.ae-form-simple-list li{padding:.3em 0 .5em .5em;border-bottom:1px solid #c3d9ff}div.ae-datastore-index-to-delete,div.ae-datastore-index-to-build{color:#aaa}#hd p{padding:0}#hd li{display:inline}ul{padding:0 0 1em 1.2em}#ae-userinfo{text-align:right;white-space:nowrap;}#ae-userinfo ul{padding-bottom:0;padding-top:5px}#ae-appbar-lrg{margin:0 0 1.25em 0;padding:.25em .5em;background-color:#e5ecf9;border-top:1px solid #36c}#ae-appbar-lrg h1{font-size:1.2em;padding:0}#ae-appbar-lrg h1 span{font-size:80%;font-weight:normal}#ae-appbar-lrg form{display:inline;padding-right:.1em;margin-right:.5em}#ae-appbar-lrg strong{white-space:nowrap}#ae-appbar-sml{margin:0 0 1.25em 0;height:8px;padding:0 .5em;background:#e5ecf9}.ae-rounded-sml{border-radius:3px;-moz-border-radius:3px;-webkit-border-radius:3px}#ae-appbar-lrg a{margin-top:.3em}a.ae-ext-link,a span.ae-ext-link{background:url('/img/icn/icn-open-in-new-window.png') no-repeat right;padding-right:18px;margin-right:8px}.ae-no-pad{padding-left:1em}.ae-message h4{margin-bottom:.3em;padding-bottom:0}#ft{text-align:center;margin:2.5em 0 1em;padding-top:.5em;border-top:2px solid #c3d9ff}#bd h3{font-weight:bold;font-size:1.4em}#bd h3 .ae-apps-switch{font-weight:normal;font-size:.7em;margin-left:2em}#bd p{padding:0 0 1em 0}#ae-content{padding-left:1em}.ae-unimportant{color:#666}.ae-new-usr td{border-top:1px solid #ccccce;background-color:#ffe}.ae-error-td td{border:2px solid #f00;background-color:#fee}.ae-delete{cursor:pointer;border:none;background:transparent;}.ae-btn-large{background:#039 url('/img/icn/button_back.png') repeat-x;color:#fff;font-weight:bold;font-size:1.2em;padding:.5em;border:2px outset #000;cursor:pointer}.ae-breadcrumb{margin:0 0 1em}.ae-disabled,a.ae-disabled,a.ae-disabled:hover,a.ae-disabled:active{color:#666!important;text-decoration:none!important;cursor:default!important;opacity:.4!important;-moz-opacity:.4!important;filter:alpha(opacity=40)!important}input.ae-readonly{border:2px solid transparent;border-left:0;background-color:transparent}span.ae-text-input-clone{padding:5px 5px 5px 0}.ae-loading{opacity:.4;-moz-opacity:.4;filter:alpha(opacity=40)}.ae-tip{margin:1em 0;background:url('/img/tip.png') top left no-repeat;padding:2px 0 0 25px}sup.ae-new-sup{color:red}.ae-action{color:#00c;cursor:pointer;text-decoration:underline}.ae-toggle{padding-left:16px;background-position:left center;background-repeat:no-repeat;cursor:pointer}.ae-minus{background-image:url('/img/wgt/minus.gif')}.ae-plus{background-image:url('/img/wgt/plus.gif')}.ae-print{background-image:url('/img/print.gif');padding-left:19px}.ae-currency,.ae-table thead th.ae-currency{text-align:right;white-space:nowrap}#ae-loading{font-size:1.2em;position:absolute;text-align:center;top:0;width:100%}#ae-loading div{margin:0 auto;background:#fff1a9;width:5em;font-weight:bold;padding:4px 10px;-moz-border-radius-bottomleft:3px;-moz-border-radius-bottomright:3px;-webkit-border-radius-bottomleft:3px;-webkit-border-radius-bottomright:3px}.ae-occlude{filter:alpha(opacity=0);position:absolute}.g-tpl-66-34 .g-unit,.g-unit .g-tpl-66-34 .g-unit,.g-unit .g-unit .g-tpl-66-34 .g-unit,.g-unit .g-unit .g-unit .g-tpl-66-34 .g-unit{display:inline;margin:0;width:33.999%;float:right}.g-unit .g-unit .g-unit .g-tpl-66-34 .g-first,.g-unit .g-unit .g-tpl-66-34 .g-first,.g-unit .g-tpl-66-34 .g-first,.g-tpl-66-34 .g-first{display:inline;margin:0;width:65.999%;float:left}.ae-ie6-c{_margin-right:-2000px;_position:relative;_width:100%;background:#fff}h2.ae-section-header{background:#e5ecf9;padding:.2em .4em;margin-bottom:.5em}.ae-field-span{padding:3px 0}ul.ae-admin-list li{margin:0 0;padding:.1em 0}select{font:13px/13px Arial,sans-serif;color:#000;border-width:1px;border-style:solid;border-color:#bbb #999 #999 #bbb;-webkit-border-radius:2px;-moz-border-radius:2px;background:#eee;background:-webkit-gradient(linear,0 0,0 100%,from(#fff),to(#ddd));background:-moz-linear-gradient(top,#fff,#ddd);filter:progid:DXImageTransform.Microsoft.Gradient(EndColorstr='#dddddd',StartColorstr='#ffffff',GradientType=0);cursor:pointer;padding:2px 1px;margin:0}select:hover{border-color:#000}select[disabled],select[disabled]:active{color:#666;border-color:#ddd;background-color:#f3f3f3;background-image:none;text-shadow:none;cursor:auto}.ae-table-plain{border-collapse:collapse;width:100%}.ae-table{border:1px solid #c5d7ef;border-collapse:collapse;width:100%}#bd h2.ae-table-title{background:#e5ecf9;margin:0;color:#000;font-size:1em;padding:3px 0 3px 5px;border-left:1px solid #c5d7ef;border-right:1px solid #c5d7ef;border-top:1px solid #c5d7ef}.ae-table-caption,.ae-table caption{border:1px solid #c5d7ef;background:#e5ecf9;-moz-margin-start:-1px}.ae-table caption{padding:3px 5px;text-align:left}.ae-table th,.ae-table td{background-color:#fff;padding:.35em 1em .25em .35em;margin:0}.ae-table thead th{font-weight:bold;text-align:left;background:#c5d7ef;vertical-align:bottom}.ae-table thead th .ae-no-bold{font-weight:normal}.ae-table tfoot tr td{border-top:1px solid #c5d7ef;background-color:#e5ecf9}.ae-table td{border-top:1px solid #c5d7ef;border-bottom:1px solid #c5d7ef}.ae-even>td,.ae-even th,.ae-even-top td,.ae-even-tween td,.ae-even-bottom td,ol.ae-even{background-color:#e9e9e9;border-top:1px solid #c5d7ef;border-bottom:1px solid #c5d7ef}.ae-even-top td{border-bottom:0}.ae-even-bottom td{border-top:0}.ae-even-tween td{border:0}.ae-table .ae-tween td{border:0}.ae-table .ae-tween-top td{border-bottom:0}.ae-table .ae-tween-bottom td{border-top:0}#bd .ae-table .cbc{width:1.5em;padding-right:0}.ae-table #ae-live td{background-color:#ffeac0}.ae-table-fixed{table-layout:fixed}.ae-table-fixed td,.ae-table-nowrap{overflow:hidden;white-space:nowrap}.ae-paginate strong{margin:0 .5em}tfoot .ae-paginate{text-align:right}.ae-table-caption .ae-paginate,.ae-table-caption .ae-orderby{padding:2px 5px}.modal-dialog{background:#c1d9ff;border:1px solid #3a5774;color:#000;padding:4px;position:absolute;font-size:1.3em;-moz-box-shadow:0 1px 4px #333;-webkit-box-shadow:0 1px 4px #333;box-shadow:0 1px 4px #333}.modal-dialog a,.modal-dialog a:link,.modal-dialog a:visited{color:#06c;cursor:pointer}.modal-dialog-bg{background:#666;left:0;position:absolute;top:0}.modal-dialog-title{background:#e0edfe;color:#000;cursor:pointer;font-size:120%;font-weight:bold;padding:8px 15px 8px 8px;position:relative;_zoom:1;}.modal-dialog-title-close{background:#e0edfe url('https://ssl.gstatic.com/editor/editortoolbar.png') no-repeat -528px 0;cursor:default;height:15px;position:absolute;right:10px;top:8px;width:15px;vertical-align:middle}.modal-dialog-buttons,.modal-dialog-content{background-color:#fff;padding:8px}.modal-dialog-buttons button{margin-right:.75em}.goog-buttonset-default{font-weight:bold}.goog-tab{position:relative;border:1px solid #8ac;padding:4px 9px;color:#000;background:#e5ecf9;border-top-left-radius:2px;border-top-right-radius:2px;-moz-border-radius-topleft:2px;-webkit-border-top-left-radius:2px;-moz-border-radius-topright:2px;-webkit-border-top-right-radius:2px}.goog-tab-bar-top .goog-tab{margin:1px 4px 0 0;border-bottom:0;float:left}.goog-tab-bar-bottom .goog-tab{margin:0 4px 1px 0;border-top:0;float:left}.goog-tab-bar-start .goog-tab{margin:0 0 4px 1px;border-right:0}.goog-tab-bar-end .goog-tab{margin:0 1px 4px 0;border-left:0}.goog-tab-hover{text-decoration:underline;cursor:pointer}.goog-tab-disabled{color:#fff;background:#ccc;border-color:#ccc}.goog-tab-selected{background:#fff!important;color:black;font-weight:bold}.goog-tab-bar-top .goog-tab-selected{top:1px;margin-top:0;padding-bottom:5px}.goog-tab-bar-bottom .goog-tab-selected{top:-1px;margin-bottom:0;padding-top:5px}.goog-tab-bar-start .goog-tab-selected{left:1px;margin-left:0;padding-right:9px}.goog-tab-bar-end .goog-tab-selected{left:-1px;margin-right:0;padding-left:9px}.goog-tab-content{padding:.1em .8em .8em .8em;border:1px solid #8ac;border-top:none}.goog-tab-bar{position:relative;margin:0 0 0 5px;border:0;padding:0;list-style:none;cursor:default;outline:none}.goog-tab-bar-clear{border-top:1px solid #8ac;clear:both;height:0;overflow:hidden}.goog-tab-bar-start{float:left}.goog-tab-bar-end{float:right}* html .goog-tab-bar-start{margin-right:-3px}* html .goog-tab-bar-end{margin-left:-3px}#ae-nav ul{list-style-type:none;margin:0;padding:1em 0}#ae-nav ul li{padding-left:.5em}#ae-nav .ae-nav-selected{color:#000;display:block;font-weight:bold;background-color:#e5ecf9;margin-right:-1px;border-top-left-radius:4px;-moz-border-radius-topleft:4px;-webkit-border-top-left-radius:4px;border-bottom-left-radius:4px;-moz-border-radius-bottomleft:4px;-webkit-border-bottom-left-radius:4px}#ae-nav .ae-nav-bold{font-weight:bold}#ae-nav ul li span.ae-nav-disabled{color:#666}#ae-nav ul ul{margin:0;padding:0 0 0 .5em}#ae-nav ul ul li{padding-left:.5em}#ae-nav ul li a,#ae-nav ul li span,#ae-nav ul ul li a{padding-left:.5em}#ae-nav li a:link,#ae-nav li a:visited{color:#00c}.ae-nav-group{padding:.5em;margin:0 .75em 0 0;background-color:#fffbe8;border:1px solid #fff1a9}.ae-nav-group h4{font-weight:bold;padding:auto auto .5em .5em;padding-left:.4em;margin-bottom:.5em;padding-bottom:0}.ae-nav-group ul{margin:0 0 .5em 0;padding:0 0 0 1.3em;list-style-type:none}.ae-nav-group ul li{padding-bottom:.5em}.ae-nav-group li a:link,.ae-nav-group li a:visited{color:#00c}.ae-nav-group li a:hover{color:#00c}@media print{body{font-size:13px;width:8.5in;background:#fff}table,.ae-table-fixed{table-layout:automatic}tr{display:table-row!important}.g-doc-1024{width:8.5in}#ae-appbar-lrg,.ae-table-caption,.ae-table-nowrap,.ae-nowrap,th,td{overflow:visible!important;white-space:normal!important;background:#fff!important}.ae-print,.ae-toggle{display:none}#ae-lhs-nav-c{display:none}#ae-content{margin:0;padding:0}.goog-zippy-collapsed,.goog-zippy-expanded{background:none!important;padding:0!important}}#ae-admin-dev-table{margin:0 0 1em 0}.ae-admin-dev-tip,.ae-admin-dev-tip.ae-tip{margin:-0.31em 0 2.77em}#ae-sms-countryselect{margin-right:.5em}#ae-admin-enable-form{margin-bottom:1em}#ae-admin-services-c{margin-top:2em}#ae-admin-services{padding:0 0 0 3em;margin-bottom:1em;font-weight:bold}#ae-admin-logs-table-c{_margin-right:-2000px;_position:relative;_width:100%;background:#fff}#ae-admin-logs-table{margin:0;padding:0}#ae-admin-logs-filters{padding:3px 0 3px 5px}#ae-admin-logs-pagination{padding:6px 5px 0 0;text-align:right;width:45%}#ae-admin-logs-pagination span.ae-disabled{color:#666;background-color:transparent}#ae-admin-logs-table td{white-space:nowrap}#ae-storage-content div.ae-alert{padding-bottom:5px}#ae-admin-performance-form input[type=text]{width:2em}.ae-admin-performance-value{font-weight:normal}.ae-admin-performance-static-value{color:#666}#ae-admin-performance-frontend-class{margin-left:0.5em}.goog-slider-horizontal,.goog-twothumbslider-horizontal{position:relative;width:502px;height:7px;display:block;outline:0;margin:1.0em 0 0.9em 3em}.ae-slider-rail:before{position:relative;top:-0.462em;float:left;content:'Min';margin:0 0 0 -3em;color:#999}.ae-slider-rail{position:absolute;background-color:#d9d9d9;top:0;right:8px;bottom:0;left:8px;border:solid 1px;border-color:#a6a6a6 #b3b3b3 #bfbfbf;border-radius:5px}.ae-slider-rail:after{position:relative;top:-0.462em;float:right;content:'Max';margin:0 -3em 0 0;color:#999}.goog-slider-horizontal .goog-slider-thumb,.goog-twothumbslider-horizontal .goog-twothumbslider-value-thumb,.goog-twothumbslider-horizontal .goog-twothumbslider-extent-thumb{position:absolute;width:17px;height:17px;background:transparent url('/img/slider_thumb-down.png') no-repeat;outline:0}.goog-slider-horizontal .goog-slider-thumb{top:-5px}.goog-twothumbslider-horizontal .goog-twothumbslider-value-thumb{top:-11px}.goog-twothumbslider-horizontal .goog-twothumbslider-extent-thumb{top:2px;background-image:url('/img/slider_thumb-up.png')}.ae-admin-performance-scale{position:relative;display:inline-block;width:502px;margin:0 0 2.7em 3em}.ae-admin-performance-scale .ae-admin-performance-scale-start{position:absolute;display:inline-block;top:0;width:100%;text-align:left}.ae-admin-performance-scale .ae-admin-performance-scale-mid{position:absolute;display:inline-block;top:0;width:100%;text-align:center}.ae-admin-performance-scale .ae-admin-performance-scale-end{position:absolute;display:inline-block;top:0;width:100%;text-align:right}.ae-absolute-container{display:inline-block;width:100%}.ae-hidden-range{display:none}.ae-default-version-radio-column{width:1em}#ae-settings-builtins-change{margin-bottom:1em}#ae-billing-form-c{_margin-right:-3000px;_position:relative;_width:100%}.ae-rounded-top-small{-moz-border-radius-topleft:3px;-webkit-border-top-left-radius:3px;-moz-border-radius-topright:3px;-webkit-border-top-right-radius:3px}.ae-progress-content{height:400px}#ae-billing-tos{text-align:left;width:100%;margin-bottom:.5em}.ae-billing-budget-section{margin-bottom:1.5em}.ae-billing-budget-section .g-unit,.g-unit .ae-billing-budget-section .g-unit,.g-unit .g-unit .ae-billing-budget-section .g-unit{margin:0 0 0 11em;width:auto;float:none}.g-unit .g-unit .ae-billing-budget-section .g-first,.g-unit .ae-billing-budget-section .g-first,.ae-billing-budget-section .g-first{margin:0;width:11em;float:left}#ae-billing-form .ae-btn-row{margin-left:11em}#ae-billing-form .ae-btn-row .ae-info{margin-top:10px}#ae-billing-checkout{width:150px;float:left}#ae-billing-alloc-table{border:1px solid #c5d7ef;border-bottom:none;width:100%;margin-top:.5em}#ae-billing-alloc-table th,#ae-billing-alloc-table td{padding:.35em 1em .25em .35em;border-bottom:1px solid #c5d7ef;color:#000;white-space:nowrap}.ae-billing-resource{background-color:transparent;font-weight:normal}#ae-billing-alloc-table tr th span{font-weight:normal}#ae-billing-alloc-table tr{vertical-align:baseline}#ae-billing-alloc-table th{white-space:nowrap}#ae-billing-alloc-table .ae-editable span.ae-text-input-clone,#ae-billing-alloc-table .ae-readonly input{display:none}#ae-billing-alloc-table .ae-readonly span.ae-text-input-clone,#ae-billing-alloc-table .ae-editable input{display:inline}#ae-billing-alloc-table td span.ae-billing-warn-note,#ae-billing-table-errors .ae-billing-warn-note{margin:0;background-repeat:no-repeat;display:inline-block;background-image:url('/img/icn/warning.png');text-align:right;padding-left:16px;padding-right:.1em;height:16px;font-weight:bold}#ae-billing-alloc-table td span.ae-billing-warn-note span,#ae-billing-table-errors .ae-billing-warn-note span{vertical-align:super;font-size:80%}#ae-billing-alloc-table td span.ae-billing-error-hidden,#ae-billing-table-errors .ae-billing-error-hidden{display:none}.ae-billing-percent{font-size:80%;color:#666;margin-left:3px}#ae-billing-week-info{margin-top:5px;line-height:1.4}#ae-billing-table-errors{margin-top:.3em}#ae-billing-allocation-noscript{margin-top:1.5em}#ae-billing-allocation-custom-opts{margin-left:2.2em}#ae-billing-settings h2{font-size:1em;display:inline}#ae-billing-settings p{padding:.3em 0 .5em}#ae-billing-settings-table{margin:.4em 0 .5em}#ae-settings-resource-col{width:19%}#ae-settings-budget-col{width:11%}#ae-billing-settings-table .ae-settings-budget-col{padding-right:2em}.ae-table th.ae-settings-unit-cell,.ae-table td.ae-settings-unit-cell,.ae-table th.ae-total-unit-cell,.ae-table td.ae-total-unit-cell{padding-left:1.2em}#ae-settings-unit-col{width:18%}#ae-settings-paid-col{width:15%}#ae-settings-free-col{width:15%}#ae-settings-total-col{width:22%}.ae-billing-inline-link{margin-left:.5em}.ae-billing-settings-section{margin-bottom:2em}#ae-billing-settings form{display:inline-block}#ae-billing-settings .ae-btn-row{margin-top:0.5em}#ae-billing-budget-setup-checkout{margin-bottom:0}#ae-billing-vat-c .ae-field-hint{width:85%}#ae-billing-checkout-note{margin-top:.8em}.ae-drachma-preset{background-color:#f6f9ff;margin-left:11em}.ae-drachma-preset p{margin-top:.5em}.ae-table thead th.ae-currency-th{text-align:right}#ae-billing-logs-date{width:15%}#ae-billing-logs-event{width:69%}#ae-billing-logs-amount{text-align:right;width:8%}#ae-billing-logs-balance{text-align:right;width:8%}#ae-billing-history-expand .ae-action{margin-left:1em}.ae-table .ae-billing-usage-premier,.ae-table .ae-billing-usage-report{width:100%;*width:auto;margin:0 0 1em 0}.ae-table .ae-billing-usage-report th,.ae-table .ae-billing-usage-premier th,.ae-billing-charges th{color:#666;border-top:0}.ae-table .ae-billing-usage-report th,.ae-table .ae-billing-usage-report td,.ae-table .ae-billing-usage-premier th,.ae-table .ae-billing-usage-premier td,.ae-billing-charges th,.ae-billing-charges td{background-color:transparent;padding:.4em 0;border-bottom:1px solid #ddd}.ae-table .ae-billing-usage-report tfoot td,.ae-billing-charges tfoot td{border-bottom:none}table.ae-billing-usage-report col.ae-billing-report-resource{width:30%}table.ae-billing-usage-report col.ae-billing-report-used{width:20%}table.ae-billing-usage-report col.ae-billing-report-free{width:16%}table.ae-billing-usage-report col.ae-billing-report-paid{width:17%}table.ae-billing-usage-report col.ae-billing-report-charge{width:17%}table.ae-billing-usage-premier col.ae-billing-report-resource{width:50%}table.ae-billing-usage-premier col.ae-billing-report-used{width:30%}table.ae-billing-usage-premier col.ae-billing-report-unit{width:20%}.ae-billing-change-resource{width:85%}.ae-billing-change-budget{width:15%}#ae-billing-always-on-label{display:inline}#ae-billing-budget-buffer-label{display:inline}.ae-billing-charges{width:50%}.ae-billing-charges-charge{text-align:right}.ae-billing-usage-report-container{padding:1em 1em 0 1em}#ae-billing-new-usage{background-color:#f6f9ff}.goog-zippy-expanded{background-image:url('/img/wgt/minus.gif');cursor:pointer;background-repeat:no-repeat;padding-left:17px}.goog-zippy-collapsed{background-image:url('/img/wgt/plus.gif');cursor:pointer;background-repeat:no-repeat;padding-left:17px}#ae-admin-logs-pagination{width:auto}.ae-usage-cycle-note{color:#555}#ae-createapp-start{background-color:#c6d5f1;padding:1em;padding-bottom:2em;text-align:center}#ae-admin-app_id_alias-check,#ae-createapp-id-check{margin:0 0 0 1em}#ae-admin-app_id_alias-message{display:block;margin:.4em 0}#ae-createapp-id-content{width:100%}#ae-createapp-id-content td{vertical-align:top}#ae-createapp-id-td{white-space:nowrap;width:1%}#ae-createapp-id-td #ae-createapp-id-error{position:absolute;width:24em;padding-left:1em;white-space:normal}#ae-createapp-id-error-td{padding-left:1em}#ae-admin-dev-invite label{float:left;width:3.6em;position:relative;top:.3em}#ae-admin-dev-invite .ae-radio{margin-left:3.6em}#ae-admin-dev-invite .ae-radio label{float:none;width:auto;font-weight:normal;position:static}#ae-admin-dev-invite .goog-button{margin-left:3.6em}#ae-admin-dev-invite .ae-field-hint{margin-left:4.2em}#ae-admin-dev-invite .ae-radio .ae-field-hint{margin-left:0}.ae-you{color:#008000}#ae-authdomain-opts{margin-bottom:1em}#ae-authdomain-content .ae-input-text,#ae-authdomain-content .ae-field-hint{margin:.3em 0 .4em 2.5em}#ae-authdomain-opts a{margin-left:1em}#ae-authdomain-opts-hint{margin-top:.2em;color:#666667;font-size:.85em}#ae-authdomain-content #ae-authdomain-desc .ae-field-hint{margin-left:0}#ae-storage-opts{margin-bottom:1em}#ae-storage-content .ae-input-text,#ae-storage-content .ae-field-hint{margin:.3em 0 .4em 2.5em}#ae-storage-opts a{margin-left:1em}#ae-storage-opts-hint{margin-top:.2em;color:#666667;font-size:.85em}#ae-storage-content #ae-storage-desc .ae-field-hint{margin-left:0}#ae-dash .g-section{margin:0 0 1em}#ae-dash * .g-section{margin:0}#ae-dash-quota .ae-alert{padding-left:1.5em}.ae-dash-email-disabled{background:url('/img/icn/exclamation_circle.png') no-repeat;margin-top:.5em;margin-bottom:.5em;min-height:16px;padding-left:1.5em}#ae-dash-email-disabled-footnote{padding-left:1.5em;margin:5px 0 0;font-weight:normal}#ae-dash-graph-c{border:1px solid #c5d7ef;padding:5px 0}#ae-dash-graph-change{margin:0 0 0 5px}#ae-dash-graph-img{padding:5px;margin-top:.5em;background-color:#fff;display:block}#ae-dash-graph-nodata{text-align:center}#ae-dash .ae-logs-severity{margin-right:.5em}#ae-dash .g-c{padding:0 0 0 .1em}#ae-dash .g-tpl-50-50 .g-unit .g-c{padding:0 0 0 1em}#ae-dash .g-tpl-50-50 .g-first .g-c{padding:0 1em 0 .1em}.ae-quota-warnings{background-color:#fffbe8;margin:0;padding:.5em .5em 0;text-align:left}.ae-quota-warnings div{padding:0 0 .5em}#ae-dash-quota-refresh-info{font-size:85%}#ae-dash #ae-dash-dollar-bucket-c #ae-dash-dollar-bucket{width:100%;float:none}#ae-dash #ae-dash-quota-bar-col,#ae-dash .ae-dash-quota-bar{width:100px}#ae-dash-quotadetails #ae-dash-quota-bar-col,#ae-dash-quotadetails .ae-dash-quota-bar{width:200px}#ae-dash-quota-percent-col{width:3.5em}#ae-dash-quota-cost-col{width:15%}#ae-dash-quota-alert-col{width:3.5em}#ae-dash .ae-dash-quota-alert-td{padding:0}.ae-dash-quota-alert-td a{display:block;width:16px;height:16px}#ae-dash .ae-dash-quota-alert-td .ae-alert{display:block;width:16px;height:16px;margin:0;padding:0}#ae-dash .ae-dash-quota-alert-td .ae-dash-email-disabled{display:block;width:16px;height:16px;margin:0;padding:0}#ae-dash-quota tbody th{font-weight:normal}#ae-dash-quota caption{padding:0}#ae-dash-quota caption .g-c{padding:3px}.ae-dash-quota-bar{float:left;background-color:#c0c0c0;height:13px;margin:.1em 0 0 0;position:relative}.ae-dash-quota-footnote{margin:5px 0 0;font-weight:normal}.ae-quota-warning{background-color:#f90}.ae-quota-alert{background-color:#c00}.ae-quota-normal{background-color:#0b0}.ae-quota-alert-text{color:#c00}.ae-favicon-text{font-size:.85em}#ae-dash-popular{width:97%}#ae-dash-popular-reqsec-col{width:6.5em}#ae-dash-popular-req-col{width:7em}#ae-dash-popular-cpu-avg-col{width:9.5em}#ae-dash-popular-cpu-percent-col{width:7em}#ae-dash-popular .ae-unimportant{font-size:80%}#ae-dash-popular .ae-nowrap,#ae-dash-errors .ae-nowrap{margin-right:5px;overflow:hidden}#ae-dash-popular th span,#ae-dash-errors th span{font-size:.8em;font-weight:normal;display:block}#ae-dash-errors caption .g-unit{width:9em}#ae-dash-errors-count-col{width:5em}#ae-dash-errors-percent-col{width:7em}#ae-dash-graph-chart-type{float:left;margin-right:1em}#ae-apps-all strong.ae-disabled{color:#000;background:#eee}.ae-quota-resource{width:30%}.ae-quota-safety-limit{width:10%}#ae-quota-details h3{padding-bottom:0;margin-bottom:.25em}#ae-quota-details table{margin-bottom:1.75em}#ae-quota-details table.ae-quota-requests{margin-bottom:.5em}#ae-quota-refresh-note p{text-align:right;padding-top:.5em;padding-bottom:0;margin-bottom:0}#ae-quota-first-api.g-section{padding-bottom:0;margin-bottom:.25em}#ae-instances-summary-table,#ae-instances-details-table{margin-bottom:1em}.ae-instances-details-availability-image{float:left;margin-right:.5em}.ae-instances-small-text{font-size:80%}.ae-instances-small-text .ae-separator{color:#666}.ae-instances-highlight td{background-color:#fff1a8}.ae-appbar-superuser-message strong{color:red}#ae-backends-table tr{vertical-align:baseline}.ae-backends-class-reminder{font-size:80%;color:#666;margin-left:3px}.ac-renderer{font:normal 13px Arial,sans-serif;position:absolute;background:#fff;border:1px solid #666;-moz-box-shadow:2px 2px 2px rgba(102,102,102,.4);-webkit-box-shadow:2px 2px 2px rgba(102,102,102,.4);width:202px}.ac-row{cursor:pointer;padding:.4em}.ac-highlighted{font-weight:bold}.ac-active{background-color:#b2b4bf}#ae-datastore-explorer-c{_margin-right:-3000px;_position:relative;_width:100%}#ae-datastore-explorer form dt{margin:1em 0 0 0}#ae-datastore-explorer #ae-datastore-explorer-labels{margin:0 0 3px}#ae-datastore-explorer-header .ae-action{margin-left:1em}#ae-datastore-explorer .id{white-space:nowrap}#ae-datastore-explorer caption{text-align:right;padding:5px}#ae-datastore-explorer-submit{margin-top:5px}#ae-datastore-explorer-namespace{margin-top:7px;margin-right:5px}#ae-datastore-stats-namespace-input,#ae-datastore-explorer-namespace-query,#ae-datastore-explorer-namespace-create{width:200px}#ae-datastore-explorer-gql-spacer{margin-top:22px}h4 #ae-datastore-explorer-gql-label{font-weight:normal}#ae-datastore-form em{font-style:normal;font-weight:normal;margin:0 0 0 .2em;color:#666}#ae-datastore-form dt{font-weight:bold}#ae-datastore-form dd{margin:.4em 0 .3em 1.5em;overflow:auto;zoom:1}#ae-datastore-form dd em{width:4em;float:left}#ae-datastore-form dd.ae-last{margin-bottom:1em}#ae-datastore-explorer-tabs-content{margin-bottom:1em}#ae-datastore-explorer-list .ae-label-row,#ae-datastore-explorer-new .ae-label-row{float:left;padding-top:.2em}#ae-datastore-explorer-list .ae-input-row,#ae-datastore-explorer-list .ae-btn-row,#ae-datastore-explorer-new .ae-input-row,#ae-datastore-explorer-new .ae-btn-row{margin-left:6em}#ae-datastore-explorer-list .ae-btn-row,#ae-datastore-explorer-new .ae-btn-row{margin-bottom:0}.ae-datastore-index-name{font-size:1.2em;font-weight:bold}.ae-table .ae-datastore-index-defs{padding-left:20px}.ae-datastore-index-defs-row{border-top:1px solid #ddd}.ae-datastore-index-defs .ae-unimportant{font-size:.8em}.ae-datastore-index-status{border:1px solid #c0dfbf;background:#f3f7f3;margin:0 25px 0 0;padding:3px}#ae-datastore-index-status-col{width:20%}#ae-datastore-index-stat-col{width:20%}.ae-datastore-index-status-Building{border-color:#edebcd;background:#fefdec}.ae-datastore-index-status-Deleting{border-color:#ccc;background:#eee}.ae-datastore-index-status-Error{border-color:#ffd3b4;background:#ffeae0}.ae-datastore-pathlink{font-size:.9em}#ae-datastore-stats-top-level-c{padding-bottom:1em;margin-bottom:1em;border-bottom:1px solid #e5ecf9}#ae-datastore-stats-top-level{width:100%}#ae-datastore-stats-piecharts-c{margin-bottom:1em}.ae-datastore-stats-piechart-label{font-size:.85em;font-weight:normal;text-align:center;padding:0}#ae-datastore-stats-property-type{width:60%}#ae-datastore-stats-size-all{width:20%}#ae-datastore-stats-index-size-all{width:20%}#ae-datastore-stats-property-name{width:40%}#ae-datastore-stats-type{width:10%}#ae-datastore-stats-size-entity{width:15%}#ae-datastore-stats-index-size-entity{width:15%}#ae-datastore-stats-percentage-size-entity{width:20%}#ae-datastore-blob-filter-form{margin-bottom:1em}#ae-datastore-blob-query-filter-label{padding-right:.5em}#ae-datastore-blob-filter-contents{padding-top:.5em}#ae-datastore-blob-date-after,#ae-datastore-blob-date-before{float:left}#ae-datastore-blob-date-after{margin-right:1em}#ae-datastore-blob-order label{font-weight:normal}#ae-datastore-blob-col-check{width:2%}#ae-datastore-blob-col-file{width:45%}#ae-datastore-blob-col-type{width:14%}#ae-datastore-blob-col-size{width:16%}#ae-blobstore-col-date{width:18%}#ae-blob-detail-filename{padding-bottom:0}#ae-blob-detail-filename span{font-weight:normal}#ae-blob-detail-key{font-size:85%}#ae-blob-detail-preview{margin-top:1em}#ae-blob-detail-dl{text-align:right}.ae-deployment-add-labels{padding:0 5px 0 20px}.ae-deployment-button-cell{width:95px}#ae-deployment-dm-dialog{width:400px}.ae-deployment-dm-selector{margin:20px 2px 20px 5px}#ae-deployment-exp-add{margin-top:5px}#ae-deployment-exp-contents{margin-top:5px;overflow:hidden}#ae-deployment-exp-desc{margin-bottom:15px}#ae-deployment-exp-div{background-color:#e5ecf9;border:1px solid #c5d7ef;margin:20px 0;padding:7px 4px}#ae-deployment-exp-hdr{font-weight:bold;margin:5px 0 5px}#ae-deployment-exp-tbl{width:400px}#ae-deployment-exp-toggle{font-weight:bold}.ae-deployment-set-button{width:22px}.ae-deployment-traffic-input{width:30px}#ae-domain-admins-list li{margin-bottom:.3em}#ae-domain-admins-list button{margin-left:.5em}#ae-new-app-dialog-c{width:500px}#ae-new-app-dialog-c .g-section{margin-bottom:1em}p.light-note{color:#555}.ae-bottom-message{margin-top:1em}#domsettings-form div.ae-radio{margin-left:1.7em}#domsettings-form div.ae-radio input{margin-left:-1.47em;float:left}#ae-logs-c{_margin-right:-2000px;_position:relative;_width:100%;background:#fff}#ae-logs{background-color:#c5d7ef;padding:1px;line-height:1.65}#ae-logs .ae-table-caption{border:0}#ae-logs-c ol,#ae-logs-c li{list-style:none;padding:0;margin:0}#ae-logs-c li li{margin:0 0 0 3px;padding:0 0 0 17px}.ae-log-noerror{padding-left:23px}#ae-logs-form .goog-inline-block{margin-top:0}.ae-logs-usage-info{padding-left:.5em}.ae-logs-reqlog .snippet{margin:.1em}.ae-logs-applog .snippet{color:#666}.ae-logs-severity{display:block;float:left;height:1.2em;width:1.2em;line-height:1.2;text-align:center;text-transform:capitalize;font-weight:bold;border-radius:2px;-moz-border-radius:2px;-webkit-border-radius:2px}.ae-logs-severity-4{background-color:#f22;color:#000}.ae-logs-severity-3{background-color:#f90;color:#000}.ae-logs-severity-2{background-color:#fd0}.ae-logs-severity-1{background-color:#3c0;color:#000}.ae-logs-severity-0{background-color:#09f;color:#000}#ae-logs-legend{margin:1em 0 0 0}#ae-logs-legend ul{list-style:none;margin:0;padding:0}#ae-logs-legend li,#ae-logs-legend strong{float:left;margin:0 1em 0 0}#ae-logs-legend li span{margin-right:.3em}.ae-logs-timestamp{padding:0 5px;font-size:85%}#ae-logs-form-c{margin-bottom:5px;padding-bottom:.5em;padding-left:1em}#ae-logs-form{padding:.3em 0 0}#ae-logs-form .ae-label-row{float:left;padding-top:.2em;margin-right:0.539em}#ae-logs-form .ae-input-row,#ae-logs-form .ae-btn-row{margin-left:4em}#ae-logs-form .ae-btn-row{margin-bottom:0}#ae-logs-requests-c{margin-bottom:.1em}#ae-logs-requests-c input{margin:0}#ae-logs-requests-all-label{margin-right:0.539em}#ae-logs-form-options{margin-top:8px}#ae-logs-tip{margin:.2em 0}#ae-logs-expand{margin-right:.2em}#ae-logs-severity-level-label{margin-top:.3em;display:block}#ae-logs-filter-hint-labels-list{margin:2px 0}#ae-logs-filter-hint-labels-list span{position:absolute}#ae-logs-filter-hint-labels-list ul{margin-left:5.5em;padding:0}#ae-logs-filter-hint-labels-list li{float:left;margin-right:.4em;line-height:1.2}.ae-toggle .ae-logs-getdetails,.ae-toggle pre{display:none}.ae-log-expanded .ae-toggle pre{display:block}#ae-logs-c .ae-log .ae-toggle{cursor:default;background:none;padding-left:0}#ae-logs-c .ae-log .ae-toggle h5{cursor:pointer;background-position:0 .55em;background-repeat:no-repeat;padding-left:17px}.ae-log .ae-plus h5{background-image:url('/img/wgt/plus.gif')}.ae-log .ae-minus h5{background-image:url('/img/wgt/minus.gif')}.ae-log{overflow:hidden;background-color:#fff;padding:.3em 0;line-height:1.65;border-bottom:1px solid #c5d7ef}.ae-log .ae-even{background-color:#e9e9e9;border:0}.ae-log h5{font-weight:normal;white-space:nowrap;padding:.4em 0 0 0}.ae-log span,.ae-log strong{margin:0 .3em}.ae-log .ae-logs-snippet{color:#666}.ae-log pre,.ae-logs-expanded{padding:.3em 0 .5em 1.5em;margin:0;font-family:"Courier New"}.ae-log .file{font-weight:bold}.ae-log.ae-log-expanded .file{white-space:pre-wrap;word-wrap:break-word}.ae-logs-app .ae-logs-req{display:none}.ae-logs-req .ae-app,.ae-logs-both .ae-app{padding-left:1em}#ae-dos-blacklist-rejects-table{text-align:left}#ae-dash-quota-percent-col{width:3.5em}.ae-cron-status-ok{color:#008000;font-size:90%;font-weight:bold}.ae-cron-status-error{color:#a03;font-size:90%;font-weight:bold}#ae-cronjobs-table .ae-table td{vertical-align:top}#ae-tasks-table td{vertical-align:top}#ae-tasks-quota{margin:0 0 1em 0}#ae-tasks-quota .ae-dash-quota-bar{width:150px}#ae-tasks-quota #ae-dash-quota-bar-col,#ae-tasks-quota .ae-dash-quota-bar{width:200px}.ae-tasks-paused-row{color:#666;font-style:italic;font-weight:bold}#ae-tasks-quota .ae-quota-safety-limit{width:30%}#ae-tasks-table{margin-top:1em}#ae-tasks-queuecontrols{margin-top:1em;margin-bottom:1em}#ae-tasks-delete-col{width:1em}#ae-tasks-eta-col,#ae-tasks-creation-col{width:11em}#ae-tasks-actions-col{width:7em}#ae-tasks-retry-col{width:4em}#ae-tasks-body-col{width:6em}#ae-tasks-headers-col{width:7em}.ae-tasks-hex-column,.ae-tasks-ascii-column{width:16em}#ae-tasks-table .ae-tasks-arrow{text-align:center}
\ No newline at end of file
+html,body,div,h1,h2,h3,h4,h5,h6,p,img,dl,dt,dd,ol,ul,li,table,caption,tbody,tfoot,thead,tr,th,td,form,fieldset,embed,object,applet{margin:0;padding:0;border:0;}body{font-size:62.5%;font-family:Arial,sans-serif;color:#000;background:#fff}a{color:#00c}a:active{color:#f00}a:visited{color:#551a8b}table{border-collapse:collapse;border-width:0;empty-cells:show}ul{padding:0 0 1em 1em}ol{padding:0 0 1em 1.3em}li{line-height:1.5em;padding:0 0 .5em 0}p{padding:0 0 1em 0}h1,h2,h3,h4,h5{padding:0 0 1em 0}h1,h2{font-size:1.3em}h3{font-size:1.1em}h4,h5,table{font-size:1em}sup,sub{font-size:.7em}input,select,textarea,option{font-family:inherit;font-size:inherit}.g-doc,.g-doc-1024,.g-doc-800{font-size:130%}.g-doc{width:100%;text-align:left}.g-section{width:100%;vertical-align:top;display:inline-block}*:first-child+html .g-section{display:block}* html .g-section{overflow:hidden}@-moz-document url-prefix(''){.g-section{overflow:hidden}}@-moz-document url-prefix(''){.g-section,tt:default{overflow:visible}}.g-section,.g-unit{zoom:1}.g-split .g-unit{text-align:right}.g-split .g-first{text-align:left}.g-doc-1024{width:73.074em;min-width:950px;margin:0 auto;text-align:left}* html .g-doc-1024{width:71.313em}*+html .g-doc-1024{width:71.313em}.g-doc-800{width:57.69em;min-width:750px;margin:0 auto;text-align:left}* html .g-doc-800{width:56.3em}*+html .g-doc-800{width:56.3em}.g-tpl-160 .g-unit,.g-unit .g-tpl-160 .g-unit,.g-unit .g-unit .g-tpl-160 .g-unit,.g-unit .g-unit .g-unit .g-tpl-160 .g-unit{margin:0 0 0 160px;width:auto;float:none}.g-unit .g-unit .g-unit .g-tpl-160 .g-first,.g-unit .g-unit .g-tpl-160 .g-first,.g-unit .g-tpl-160 .g-first,.g-tpl-160 .g-first{margin:0;width:160px;float:left}.g-tpl-160-alt .g-unit,.g-unit .g-tpl-160-alt .g-unit,.g-unit .g-unit .g-tpl-160-alt .g-unit,.g-unit .g-unit .g-unit .g-tpl-160-alt .g-unit{margin:0 160px 0 0;width:auto;float:none}.g-unit .g-unit .g-unit .g-tpl-160-alt .g-first,.g-unit .g-unit .g-tpl-160-alt .g-first,.g-unit .g-tpl-160-alt .g-first,.g-tpl-160-alt .g-first{margin:0;width:160px;float:right}.g-tpl-180 .g-unit,.g-unit .g-tpl-180 .g-unit,.g-unit .g-unit .g-tpl-180 .g-unit,.g-unit .g-unit .g-unit .g-tpl-180 .g-unit{margin:0 0 0 180px;width:auto;float:none}.g-unit .g-unit .g-unit .g-tpl-180 .g-first,.g-unit .g-unit .g-tpl-180 .g-first,.g-unit .g-tpl-180 .g-first,.g-tpl-180 .g-first{margin:0;width:180px;float:left}.g-tpl-180-alt .g-unit,.g-unit .g-tpl-180-alt .g-unit,.g-unit .g-unit .g-tpl-180-alt .g-unit,.g-unit .g-unit .g-unit .g-tpl-180-alt .g-unit{margin:0 180px 0 0;width:auto;float:none}.g-unit .g-unit .g-unit .g-tpl-180-alt .g-first,.g-unit .g-unit .g-tpl-180-alt .g-first,.g-unit .g-tpl-180-alt .g-first,.g-tpl-180-alt .g-first{margin:0;width:180px;float:right}.g-tpl-300 .g-unit,.g-unit .g-tpl-300 .g-unit,.g-unit .g-unit .g-tpl-300 .g-unit,.g-unit .g-unit .g-unit .g-tpl-300 .g-unit{margin:0 0 0 300px;width:auto;float:none}.g-unit .g-unit .g-unit .g-tpl-300 .g-first,.g-unit .g-unit .g-tpl-300 .g-first,.g-unit .g-tpl-300 .g-first,.g-tpl-300 .g-first{margin:0;width:300px;float:left}.g-tpl-300-alt .g-unit,.g-unit .g-tpl-300-alt .g-unit,.g-unit .g-unit .g-tpl-300-alt .g-unit,.g-unit .g-unit .g-unit .g-tpl-300-alt .g-unit{margin:0 300px 0 0;width:auto;float:none}.g-unit .g-unit .g-unit .g-tpl-300-alt .g-first,.g-unit .g-unit .g-tpl-300-alt .g-first,.g-unit .g-tpl-300-alt .g-first,.g-tpl-300-alt .g-first{margin:0;width:300px;float:right}.g-tpl-25-75 .g-unit,.g-unit .g-tpl-25-75 .g-unit,.g-unit .g-unit .g-tpl-25-75 .g-unit,.g-unit .g-unit .g-unit .g-tpl-25-75 .g-unit{width:74.999%;float:right;margin:0}.g-unit .g-unit .g-unit .g-tpl-25-75 .g-first,.g-unit .g-unit .g-tpl-25-75 .g-first,.g-unit .g-tpl-25-75 .g-first,.g-tpl-25-75 .g-first{width:24.999%;float:left;margin:0}.g-tpl-25-75-alt .g-unit,.g-unit .g-tpl-25-75-alt .g-unit,.g-unit .g-unit .g-tpl-25-75-alt .g-unit,.g-unit .g-unit .g-unit .g-tpl-25-75-alt .g-unit{width:24.999%;float:left;margin:0}.g-unit .g-unit .g-unit .g-tpl-25-75-alt .g-first,.g-unit .g-unit .g-tpl-25-75-alt .g-first,.g-unit .g-tpl-25-75-alt .g-first,.g-tpl-25-75-alt .g-first{width:74.999%;float:right;margin:0}.g-tpl-75-25 .g-unit,.g-unit .g-tpl-75-25 .g-unit,.g-unit .g-unit .g-tpl-75-25 .g-unit,.g-unit .g-unit .g-unit .g-tpl-75-25 .g-unit{width:24.999%;float:right;margin:0}.g-unit .g-unit .g-unit .g-tpl-75-25 .g-first,.g-unit .g-unit .g-tpl-75-25 .g-first,.g-unit .g-tpl-75-25 .g-first,.g-tpl-75-25 .g-first{width:74.999%;float:left;margin:0}.g-tpl-75-25-alt .g-unit,.g-unit .g-tpl-75-25-alt .g-unit,.g-unit .g-unit .g-tpl-75-25-alt .g-unit,.g-unit .g-unit .g-unit .g-tpl-75-25-alt .g-unit{width:74.999%;float:left;margin:0}.g-unit .g-unit .g-unit .g-tpl-75-25-alt .g-first,.g-unit .g-unit .g-tpl-75-25-alt .g-first,.g-unit .g-tpl-75-25-alt .g-first,.g-tpl-75-25-alt .g-first{width:24.999%;float:right;margin:0}.g-tpl-33-67 .g-unit,.g-unit .g-tpl-33-67 .g-unit,.g-unit .g-unit .g-tpl-33-67 .g-unit,.g-unit .g-unit .g-unit .g-tpl-33-67 .g-unit{width:66.999%;float:right;margin:0}.g-unit .g-unit .g-unit .g-tpl-33-67 .g-first,.g-unit .g-unit .g-tpl-33-67 .g-first,.g-unit .g-tpl-33-67 .g-first,.g-tpl-33-67 .g-first{width:32.999%;float:left;margin:0}.g-tpl-33-67-alt .g-unit,.g-unit .g-tpl-33-67-alt .g-unit,.g-unit .g-unit .g-tpl-33-67-alt .g-unit,.g-unit .g-unit .g-unit .g-tpl-33-67-alt .g-unit{width:32.999%;float:left;margin:0}.g-unit .g-unit .g-unit .g-tpl-33-67-alt .g-first,.g-unit .g-unit .g-tpl-33-67-alt .g-first,.g-unit .g-tpl-33-67-alt .g-first,.g-tpl-33-67-alt .g-first{width:66.999%;float:right;margin:0}.g-tpl-67-33 .g-unit,.g-unit .g-tpl-67-33 .g-unit,.g-unit .g-unit .g-tpl-67-33 .g-unit,.g-unit .g-unit .g-unit .g-tpl-67-33 .g-unit{width:32.999%;float:right;margin:0}.g-unit .g-unit .g-unit .g-tpl-67-33 .g-first,.g-unit .g-unit .g-tpl-67-33 .g-first,.g-unit .g-tpl-67-33 .g-first,.g-tpl-67-33 .g-first{width:66.999%;float:left;margin:0}.g-tpl-67-33-alt .g-unit,.g-unit .g-tpl-67-33-alt .g-unit,.g-unit .g-unit .g-tpl-67-33-alt .g-unit,.g-unit .g-unit .g-unit .g-tpl-67-33-alt .g-unit{width:66.999%;float:left;margin:0}.g-unit .g-unit .g-unit .g-tpl-67-33-alt .g-first,.g-unit .g-unit .g-tpl-67-33-alt .g-first,.g-unit .g-tpl-67-33-alt .g-first,.g-tpl-67-33-alt .g-first{width:32.999%;float:right;margin:0}.g-tpl-50-50 .g-unit,.g-unit .g-tpl-50-50 .g-unit,.g-unit .g-unit .g-tpl-50-50 .g-unit,.g-unit .g-unit .g-unit .g-tpl-50-50 .g-unit{width:49.999%;float:right;margin:0}.g-unit .g-unit .g-unit .g-tpl-50-50 .g-first,.g-unit .g-unit .g-tpl-50-50 .g-first,.g-unit .g-tpl-50-50 .g-first,.g-tpl-50-50 .g-first{width:49.999%;float:left;margin:0}.g-tpl-50-50-alt .g-unit,.g-unit .g-tpl-50-50-alt .g-unit,.g-unit .g-unit .g-tpl-50-50-alt .g-unit,.g-unit .g-unit .g-unit .g-tpl-50-50-alt .g-unit{width:49.999%;float:left;margin:0}.g-unit .g-unit .g-unit .g-tpl-50-50-alt .g-first,.g-unit .g-unit .g-tpl-50-50-alt .g-first,.g-unit .g-tpl-50-50-alt .g-first,.g-tpl-50-50-alt .g-first{width:49.999%;float:right;margin:0}.g-tpl-nest{width:auto}.g-tpl-nest .g-section{display:inline}.g-tpl-nest .g-unit,.g-unit .g-tpl-nest .g-unit,.g-unit .g-unit .g-tpl-nest .g-unit,.g-unit .g-unit .g-unit .g-tpl-nest .g-unit{float:left;width:auto;margin:0}.g-tpl-nest-alt .g-unit,.g-unit .g-tpl-nest-alt .g-unit,.g-unit .g-unit .g-tpl-nest-alt .g-unit,.g-unit .g-unit .g-unit .g-tpl-nest-alt .g-unit{float:right;width:auto;margin:0}.goog-button{border-width:1px;border-style:solid;border-color:#bbb #999 #999 #bbb;border-radius:2px;-webkit-border-radius:2px;-moz-border-radius:2px;font:normal normal normal 13px/13px Arial,sans-serif;color:#000;text-align:middle;text-decoration:none;text-shadow:0 1px 1px rgba(255,255,255,1);background:#eee;background-image:-webkit-gradient(linear,0 0,0 100%,from(#fff),to(#ddd));background:-moz-linear-gradient(top,#fff,#ddd);filter:progid:DXImageTransform.Microsoft.Gradient(EndColorstr='#dddddd',StartColorstr='#ffffff',GradientType=0);cursor:pointer;margin:0;display:inline;display:-moz-inline-box;display:inline-block;*overflow:visible;padding:4px 8px 5px}a.goog-button,span.goog-button,div.goog-button{padding:4px 8px 5px}.goog-button:visited{color:#000}.goog-button{*display:inline}.goog-button:focus,.goog-button:hover{border-color:#000}.goog-button:active,.goog-button-active{color:#000;background-color:#bbb;border-color:#999 #bbb #bbb #999;background-image:-webkit-gradient(linear,0 0,0 100%,from(#ddd),to(#fff));background-image:-moz-linear-gradient(top,#ddd,#fff);filter:progid:DXImageTransform.Microsoft.Gradient(EndColorstr='#ffffff',StartColorstr='#dddddd',GradientType=0)}.goog-button[disabled],.goog-button[disabled]:active,.goog-button[disabled]:hover{color:#666;border-color:#ddd;background-color:#f3f3f3;background-image:none;text-shadow:none;cursor:auto}.goog-button{padding:5px 8px 4px }.goog-button{*padding:4px 7px 2px}html>body input.goog-button,x:-moz-any-link,x:default,html>body button.goog-button,x:-moz-any-link,x:default{padding-top:3px;padding-bottom:2px}a.goog-button,x:-moz-any-link,x:default,span.goog-button,x:-moz-any-link,x:default,div.goog-button,x:-moz-any-link,x:default{padding:4px 8px 5px}.goog-button-fixed{padding-left:0!important;padding-right:0!important;width:100%}button.goog-button-icon-c{padding-top:1px;padding-bottom:1px}button.goog-button-icon-c{padding-top:3px ;padding-bottom:2px }button.goog-button-icon-c{*padding-top:0;*padding-bottom:0}html>body button.goog-button-icon-c,x:-moz-any-link,x:default{padding-top:1px;padding-bottom:1px}.goog-button-icon{display:block;margin:0 auto;height:18px;width:18px}html>body .goog-inline-block{display:-moz-inline-box;display:inline-block;}.goog-inline-block{position:relative;display:inline-block}* html .goog-inline-block{display:inline}*:first-child+html .goog-inline-block{display:inline}.goog-custom-button{margin:0 2px 2px;border:0;padding:0;font:normal Tahoma,Arial,sans-serif;color:#000;text-decoration:none;list-style:none;vertical-align:middle;cursor:pointer;outline:none;background:#eee;background-image:-webkit-gradient(linear,0 0,0 100%,from(#fff),to(#ddd));background:-moz-linear-gradient(top,#fff,#ddd);filter:progid:DXImageTransform.Microsoft.Gradient(EndColorstr='#dddddd',StartColorstr='#ffffff',GradientType=0)}.goog-custom-button-outer-box,.goog-custom-button-inner-box{border-style:solid;border-color:#bbb #999 #999 #bbb;vertical-align:top}.goog-custom-button-outer-box{margin:0;border-width:1px 0;padding:0}.goog-custom-button-inner-box{margin:0 -1px;border-width:0 1px;padding:3px 4px}* html .goog-custom-button-inner-box{left:-1px}* html .goog-custom-button-rtl .goog-custom-button-outer-box{left:-1px}* html .goog-custom-button-rtl .goog-custom-button-inner-box{left:0}*:first-child+html .goog-custom-button-inner-box{left:-1px}*:first-child+html .goog-custom-button-collapse-right .goog-custom-button-inner-box{border-left-width:2px}*:first-child+html .goog-custom-button-collapse-left .goog-custom-button-inner-box{border-right-width:2px}*:first-child+html .goog-custom-button-collapse-right.goog-custom-button-collapse-left .goog-custom-button-inner-box{border-width:0 1px}*:first-child+html .goog-custom-button-rtl .goog-custom-button-inner-box{left:1px}::root .goog-custom-button,::root .goog-custom-button-outer-box{line-height:0}::root .goog-custom-button-inner-box{line-height:normal}.goog-custom-button-disabled{background-image:none!important;opacity:0.4;-moz-opacity:0.4;filter:alpha(opacity=40)}.goog-custom-button-disabled .goog-custom-button-outer-box,.goog-custom-button-disabled .goog-custom-button-inner-box{color:#333!important;border-color:#999!important}* html .goog-custom-button-disabled{margin:2px 1px!important;padding:0 1px!important}*:first-child+html .goog-custom-button-disabled{margin:2px 1px!important;padding:0 1px!important}.goog-custom-button-hover .goog-custom-button-outer-box,.goog-custom-button-hover .goog-custom-button-inner-box{border-color:#000!important;}.goog-custom-button-active,.goog-custom-button-checked{background-color:#bbb;background-position:bottom left;background-image:-webkit-gradient(linear,0 0,0 100%,from(#ddd),to(#fff));background:-moz-linear-gradient(top,#ddd,#fff);filter:progid:DXImageTransform.Microsoft.Gradient(EndColorstr='#ffffff',StartColorstr='#dddddd',GradientType=0)}.goog-custom-button-focused .goog-custom-button-outer-box,.goog-custom-button-focused .goog-custom-button-inner-box,.goog-custom-button-focused.goog-custom-button-collapse-left .goog-custom-button-inner-box,.goog-custom-button-focused.goog-custom-button-collapse-left.goog-custom-button-checked .goog-custom-button-inner-box{border-color:#000}.goog-custom-button-collapse-right,.goog-custom-button-collapse-right .goog-custom-button-outer-box,.goog-custom-button-collapse-right .goog-custom-button-inner-box{margin-right:0}.goog-custom-button-collapse-left,.goog-custom-button-collapse-left .goog-custom-button-outer-box,.goog-custom-button-collapse-left .goog-custom-button-inner-box{margin-left:0}.goog-custom-button-collapse-left .goog-custom-button-inner-box{border-left:1px solid #fff}.goog-custom-button-collapse-left.goog-custom-button-checked .goog-custom-button-inner-box{border-left:1px solid #ddd}* html .goog-custom-button-collapse-left .goog-custom-button-inner-box{left:0}*:first-child+html .goog-custom-button-collapse-left .goog-custom-button-inner-box{left:0}.goog-date-picker th,.goog-date-picker td{font-family:arial,sans-serif;text-align:center}.goog-date-picker th{font-size:.9em;font-weight:bold;color:#666667;background-color:#c3d9ff}.goog-date-picker td{vertical-align:middle;padding:2px 3px}.goog-date-picker{-moz-user-focus:normal;-moz-user-select:none;position:absolute;border:1px solid gray;float:left;font-family:arial,sans-serif;padding-left:1px;background:white}.goog-date-picker-menu{position:absolute;background:threedface;border:1px solid gray;-moz-user-focus:normal}.goog-date-picker-menu ul{list-style:none;margin:0;padding:0}.goog-date-picker-menu ul li{cursor:default}.goog-date-picker-menu-selected{background-color:#aaccee}.goog-date-picker td div{float:left}.goog-date-picker button{padding:0;margin:1px;border:1px outset gray}.goog-date-picker-week{padding:1px 3px}.goog-date-picker-wday{padding:1px 3px}.goog-date-picker-today-cont{text-align:left!important}.goog-date-picker-none-cont{text-align:right!important}.goog-date-picker-head td{text-align:center}.goog-date-picker-month{width:12ex}.goog-date-picker-year{width:6ex}.goog-date-picker table{border-collapse:collapse}.goog-date-picker-selected{background-color:#aaccee!important;color:blue!important}.goog-date-picker-today{font-weight:bold!important}.goog-date-picker-other-month{-moz-opacity:0.3;filter:Alpha(Opacity=30)}.sat,.sun{background:#eee}#button1,#button2{display:block;width:60px;text-align:center;margin:10px;padding:10px;font:normal .8em arial,sans-serif;border:1px solid #000}.goog-menu{position:absolute;color:#000;border:1px solid #b5b6b5;background-color:#f3f3f7;cursor:default;font:normal small arial,helvetica,sans-serif;margin:0;padding:0;outline:none}.goog-menuitem{padding:2px 5px;margin:0;list-style:none}.goog-menuitem-highlight{background-color:#4279a5;color:#fff}.goog-menuitem-disabled{color:#999}.goog-option{padding-left:15px!important}.goog-option-selected{background-image:url('/img/check.gif');background-position:4px 50%;background-repeat:no-repeat}.goog-menuseparator{position:relative;margin:2px 0;border-top:1px solid #999;padding:0;outline:none}.goog-submenu{position:relative}.goog-submenu-arrow{position:absolute;display:block;width:11px;height:11px;right:3px;top:4px;background-image:url('/img/menu-arrows.gif');background-repeat:no-repeat;background-position:0 0;font-size:1px}.goog-menuitem-highlight .goog-submenu-arrow{background-position:0 -11px}.goog-menuitem-disabled .goog-submenu-arrow{display:none}.goog-menu-filter{margin:2px;border:1px solid silver;background:white;overflow:hidden}.goog-menu-filter div{color:gray;position:absolute;padding:1px}.goog-menu-filter input{margin:0;border:0;background:transparent;width:100%}.goog-menuitem-partially-checked{background-image:url('/img/check-outline.gif');background-position:4px 50%;background-repeat:no-repeat}.goog-menuitem-fully-checked{background-image:url('/img/check.gif');background-position:4px 50%;background-repeat:no-repeat}.goog-menu-button{margin:0 2px 2px 2px;border:0;padding:0;font:normal Tahoma,Arial,sans-serif;color:#000;background:#ddd url("/img/button-bg.gif") repeat-x top left;text-decoration:none;list-style:none;vertical-align:middle;cursor:pointer;outline:none}.goog-menu-button-outer-box,.goog-menu-button-inner-box{border-style:solid;border-color:#aaa;vertical-align:middle}.goog-menu-button-outer-box{margin:0;border-width:1px 0;padding:0}.goog-menu-button-inner-box{margin:0 -1px;border-width:0 1px;padding:0 4px 2px 4px}* html .goog-menu-button-inner-box{left:-1px}* html .goog-menu-button-rtl .goog-menu-button-outer-box{left:-1px}* html .goog-menu-button-rtl .goog-menu-button-inner-box{left:0}*:first-child+html .goog-menu-button-inner-box{left:-1px}*:first-child+html .goog-menu-button-rtl .goog-menu-button-inner-box{left:1px}::root .goog-menu-button,::root .goog-menu-button-outer-box,::root .goog-menu-button-inner-box{line-height:0}::root .goog-menu-button-caption,::root .goog-menu-button-dropdown{line-height:normal}.goog-menu-button-disabled{background-image:none!important;opacity:0.4;-moz-opacity:0.4;filter:alpha(opacity=40)}.goog-menu-button-disabled .goog-menu-button-outer-box,.goog-menu-button-disabled .goog-menu-button-inner-box,.goog-menu-button-disabled .goog-menu-button-caption,.goog-menu-button-disabled .goog-menu-button-dropdown{color:#333!important;border-color:#999!important}* html .goog-menu-button-disabled{margin:2px 1px!important;padding:0 1px!important}*:first-child+html .goog-menu-button-disabled{margin:2px 1px!important;padding:0 1px!important}.goog-menu-button-hover .goog-menu-button-outer-box,.goog-menu-button-hover .goog-menu-button-inner-box{border-color:#9cf #69e #69e #7af!important;}.goog-menu-button-active,.goog-menu-button-open{background-color:#bbb;background-position:bottom left}.goog-menu-button-focused .goog-menu-button-outer-box,.goog-menu-button-focused .goog-menu-button-inner-box{border-color:#3366cc}.goog-menu-button-caption{padding:0 4px 0 0;vertical-align:middle}.goog-menu-button-rtl .goog-menu-button-caption{padding:0 0 0 4px}.goog-menu-button-dropdown{width:7px;background:url('/img/toolbar_icons.gif') no-repeat -176px;vertical-align:middle}.goog-flat-menu-button{margin:0 2px;padding:1px 4px;font:normal 95% Tahoma,Arial,sans-serif;color:#333;text-decoration:none;list-style:none;vertical-align:middle;cursor:pointer;outline:none;-moz-outline:none;border-width:1px;border-style:solid;border-color:#c9c9c9;background-color:#fff}.goog-flat-menu-button-disabled *{color:#999;border-color:#ccc;cursor:default}.goog-flat-menu-button-hover,.goog-flat-menu-button-hover{border-color:#9cf #69e #69e #7af!important;}.goog-flat-menu-button-active{background-color:#bbb;background-position:bottom left}.goog-flat-menu-button-focused{border-color:#3366cc}.goog-flat-menu-button-caption{padding-right:10px;vertical-align:middle}.goog-flat-menu-button-dropdown{width:7px;background:url('/img/toolbar_icons.gif') no-repeat -176px;vertical-align:middle}h1{font-size:1.8em}.g-doc{width:auto;margin:0 10px}.g-doc-1024{margin-left:10px}#ae-logo{background:url('//www.google.com/images/logos/app_engine_logo_sm.gif') 0 0 no-repeat;display:block;width:178px;height:30px;margin:4px 0 0 0}.ae-ir span{position:absolute;display:block;width:0;height:0;overflow:hidden}.ae-noscript{position:absolute;left:-5000px}#ae-lhs-nav{border-right:3px solid #e5ecf9}.ae-notification{margin-bottom:.6em;text-align:center}.ae-notification strong{display:block;width:55%;margin:0 auto;text-align:center;padding:.6em;background-color:#fff1a8;font-weight:bold}.ae-alert{font-weight:bold;background:url('/img/icn/warning.png') no-repeat;margin-bottom:.5em;padding-left:1.8em}.ae-info{background:url('/img/icn/icn-info.gif') no-repeat;margin-bottom:.5em;padding-left:1.8em}.ae-promo{padding:.5em .8em;margin:.6em 0;background-color:#fffbe8;border:1px solid #fff1a9;text-align:left}.ae-promo strong{position:relative;top:.3em}.ae-alert-text,.ae-warning-text{background-color:transparent;background-position:right 1px;padding:0 18px 0 0}.ae-alert-text{color:#c00}.ae-warning-text{color:#f90}.ae-alert-c span{display:inline-block}.ae-message{border:1px solid #e5ecf9;background-color:#f6f9ff;margin-bottom:1em;padding:.5em}.ae-errorbox{border:1px solid #f00;background-color:#fee;margin-bottom:1em;padding:1em}#bd .ae-errorbox ul{padding-bottom:0}.ae-form dt{font-weight:bold}.ae-form dt em,.ae-field-hint{margin-top:.2em;color:#666667;font-size:.85em}.ae-field-yyyymmdd,.ae-field-hhmmss{width:6em}.ae-field-hint-hhmmss{margin-left:2.3em}.ae-form label{display:block;margin:0 0 .2em 0;font-weight:bold}.ae-radio{margin-bottom:.3em}.ae-radio label{display:inline}.ae-form dd,.ae-input-row{margin-bottom:.6em}.ae-input-row-group{border:1px solid #fff1a9;background:#fffbe8;padding:8px}.ae-btn-row{margin-top:1.4em;margin-bottom:1em}.ae-btn-row-note{padding:5px 0 6px 0}.ae-btn-row-note span{padding-left:18px;padding-right:.5em;background:transparent url('/img/icn/icn-info.gif') 0 0 no-repeat}.ae-btn-primary{font-weight:bold}form .ae-cancel{margin-left:.5em}.ae-submit-inline{margin-left:.8em}.ae-radio-bullet{width:20px;float:left}.ae-label-hanging-indent{margin-left:5px}.ae-divider{margin:0 .6em 0 .5em}.ae-nowrap{white-space:nowrap}.ae-pre-wrap{white-space:pre-wrap;white-space:-moz-pre-wrap;white-space:-pre-wrap;white-space:-o-pre-wrap;word-wrap:break-word;_white-space:pre;}wbr:after{content:"\00200b"}a button{text-decoration:none}.ae-alert ul{margin-bottom:.75em;margin-top:.25em;line-height:1.5em}.ae-alert h4{color:#000;font-weight:bold;padding:0 0 .5em}.ae-form-simple-list{list-style-type:none;padding:0;margin-bottom:1em}.ae-form-simple-list li{padding:.3em 0 .5em .5em;border-bottom:1px solid #c3d9ff}div.ae-datastore-index-to-delete,div.ae-datastore-index-to-build{color:#aaa}#hd p{padding:0}#hd li{display:inline}ul{padding:0 0 1em 1.2em}#ae-userinfo{text-align:right;white-space:nowrap;}#ae-userinfo ul{padding-bottom:0;padding-top:5px}#ae-appbar-lrg{margin:0 0 1.25em 0;padding:.25em .5em;background-color:#e5ecf9;border-top:1px solid #36c}#ae-appbar-lrg h1{font-size:1.2em;padding:0}#ae-appbar-lrg h1 span{font-size:80%;font-weight:normal}#ae-appbar-lrg form{display:inline;padding-right:.1em;margin-right:.5em}#ae-appbar-lrg strong{white-space:nowrap}#ae-appbar-sml{margin:0 0 1.25em 0;height:8px;padding:0 .5em;background:#e5ecf9}.ae-rounded-sml{border-radius:3px;-moz-border-radius:3px;-webkit-border-radius:3px}#ae-appbar-lrg a{margin-top:.3em}a.ae-ext-link,a span.ae-ext-link{background:url('/img/icn/icn-open-in-new-window.png') no-repeat right;padding-right:18px;margin-right:8px}.ae-no-pad{padding-left:1em}.ae-message h4{margin-bottom:.3em;padding-bottom:0}#ft{text-align:center;margin:2.5em 0 1em;padding-top:.5em;border-top:2px solid #c3d9ff}#bd h3{font-weight:bold;font-size:1.4em}#bd h3 .ae-apps-switch{font-weight:normal;font-size:.7em;margin-left:2em}#bd p{padding:0 0 1em 0}#ae-content{padding-left:1em}.ae-unimportant{color:#666}.ae-new-usr td{border-top:1px solid #ccccce;background-color:#ffe}.ae-error-td td{border:2px solid #f00;background-color:#fee}.ae-delete{cursor:pointer;border:none;background:transparent;}.ae-btn-large{background:#039 url('/img/icn/button_back.png') repeat-x;color:#fff;font-weight:bold;font-size:1.2em;padding:.5em;border:2px outset #000;cursor:pointer}.ae-breadcrumb{margin:0 0 1em}.ae-disabled,a.ae-disabled,a.ae-disabled:hover,a.ae-disabled:active{color:#666!important;text-decoration:none!important;cursor:default!important;opacity:.4!important;-moz-opacity:.4!important;filter:alpha(opacity=40)!important}input.ae-readonly{border:2px solid transparent;border-left:0;background-color:transparent}span.ae-text-input-clone{padding:5px 5px 5px 0}.ae-loading{opacity:.4;-moz-opacity:.4;filter:alpha(opacity=40)}.ae-tip{margin:1em 0;background:url('/img/tip.png') top left no-repeat;padding:2px 0 0 25px}sup.ae-new-sup{color:red}.ae-action{color:#00c;cursor:pointer;text-decoration:underline}.ae-toggle{padding-left:16px;background-position:left center;background-repeat:no-repeat;cursor:pointer}.ae-minus{background-image:url('/img/wgt/minus.gif')}.ae-plus{background-image:url('/img/wgt/plus.gif')}.ae-print{background-image:url('/img/print.gif');padding-left:19px}.ae-currency,.ae-table thead th.ae-currency{text-align:right;white-space:nowrap}#ae-loading{font-size:1.2em;position:absolute;text-align:center;top:0;width:100%}#ae-loading div{margin:0 auto;background:#fff1a9;width:5em;font-weight:bold;padding:4px 10px;-moz-border-radius-bottomleft:3px;-moz-border-radius-bottomright:3px;-webkit-border-radius-bottomleft:3px;-webkit-border-radius-bottomright:3px}.ae-occlude{filter:alpha(opacity=0);position:absolute}.g-tpl-66-34 .g-unit,.g-unit .g-tpl-66-34 .g-unit,.g-unit .g-unit .g-tpl-66-34 .g-unit,.g-unit .g-unit .g-unit .g-tpl-66-34 .g-unit{display:inline;margin:0;width:33.999%;float:right}.g-unit .g-unit .g-unit .g-tpl-66-34 .g-first,.g-unit .g-unit .g-tpl-66-34 .g-first,.g-unit .g-tpl-66-34 .g-first,.g-tpl-66-34 .g-first{display:inline;margin:0;width:65.999%;float:left}.ae-ie6-c{_margin-right:-2000px;_position:relative;_width:100%;background:#fff}h2.ae-section-header{background:#e5ecf9;padding:.2em .4em;margin-bottom:.5em}.ae-field-span{padding:3px 0}ul.ae-admin-list li{margin:0 0;padding:.1em 0}select{font:13px/13px Arial,sans-serif;color:#000;border-width:1px;border-style:solid;border-color:#bbb #999 #999 #bbb;-webkit-border-radius:2px;-moz-border-radius:2px;background:#eee;background:-webkit-gradient(linear,0 0,0 100%,from(#fff),to(#ddd));background:-moz-linear-gradient(top,#fff,#ddd);filter:progid:DXImageTransform.Microsoft.Gradient(EndColorstr='#dddddd',StartColorstr='#ffffff',GradientType=0);cursor:pointer;padding:2px 1px;margin:0}select:hover{border-color:#000}select[disabled],select[disabled]:active{color:#666;border-color:#ddd;background-color:#f3f3f3;background-image:none;text-shadow:none;cursor:auto}.ae-table-plain{border-collapse:collapse;width:100%}.ae-table{border:1px solid #c5d7ef;border-collapse:collapse;width:100%}#bd h2.ae-table-title{background:#e5ecf9;margin:0;color:#000;font-size:1em;padding:3px 0 3px 5px;border-left:1px solid #c5d7ef;border-right:1px solid #c5d7ef;border-top:1px solid #c5d7ef}.ae-table-caption,.ae-table caption{border:1px solid #c5d7ef;background:#e5ecf9;-moz-margin-start:-1px}.ae-table caption{padding:3px 5px;text-align:left}.ae-table th,.ae-table td{background-color:#fff;padding:.35em 1em .25em .35em;margin:0}.ae-table thead th{font-weight:bold;text-align:left;background:#c5d7ef;vertical-align:bottom}.ae-table thead th .ae-no-bold{font-weight:normal}.ae-table tfoot tr td{border-top:1px solid #c5d7ef;background-color:#e5ecf9}.ae-table td{border-top:1px solid #c5d7ef;border-bottom:1px solid #c5d7ef}.ae-even>td,.ae-even th,.ae-even-top td,.ae-even-tween td,.ae-even-bottom td,ol.ae-even{background-color:#e9e9e9;border-top:1px solid #c5d7ef;border-bottom:1px solid #c5d7ef}.ae-even-top td{border-bottom:0}.ae-even-bottom td{border-top:0}.ae-even-tween td{border:0}.ae-table .ae-tween td{border:0}.ae-table .ae-tween-top td{border-bottom:0}.ae-table .ae-tween-bottom td{border-top:0}#bd .ae-table .cbc{width:1.5em;padding-right:0}.ae-table #ae-live td{background-color:#ffeac0}.ae-table-fixed{table-layout:fixed}.ae-table-fixed td,.ae-table-nowrap{overflow:hidden;white-space:nowrap}.ae-paginate strong{margin:0 .5em}tfoot .ae-paginate{text-align:right}.ae-table-caption .ae-paginate,.ae-table-caption .ae-orderby{padding:2px 5px}.modal-dialog{background:#c1d9ff;border:1px solid #3a5774;color:#000;padding:4px;position:absolute;font-size:1.3em;-moz-box-shadow:0 1px 4px #333;-webkit-box-shadow:0 1px 4px #333;box-shadow:0 1px 4px #333}.modal-dialog a,.modal-dialog a:link,.modal-dialog a:visited{color:#06c;cursor:pointer}.modal-dialog-bg{background:#666;left:0;position:absolute;top:0}.modal-dialog-title{background:#e0edfe;color:#000;cursor:pointer;font-size:120%;font-weight:bold;padding:8px 15px 8px 8px;position:relative;_zoom:1;}.modal-dialog-title-close{background:#e0edfe url('https://ssl.gstatic.com/editor/editortoolbar.png') no-repeat -528px 0;cursor:default;height:15px;position:absolute;right:10px;top:8px;width:15px;vertical-align:middle}.modal-dialog-buttons,.modal-dialog-content{background-color:#fff;padding:8px}.modal-dialog-buttons button{margin-right:.75em}.goog-buttonset-default{font-weight:bold}.goog-tab{position:relative;border:1px solid #8ac;padding:4px 9px;color:#000;background:#e5ecf9;border-top-left-radius:2px;border-top-right-radius:2px;-moz-border-radius-topleft:2px;-webkit-border-top-left-radius:2px;-moz-border-radius-topright:2px;-webkit-border-top-right-radius:2px}.goog-tab-bar-top .goog-tab{margin:1px 4px 0 0;border-bottom:0;float:left}.goog-tab-bar-bottom .goog-tab{margin:0 4px 1px 0;border-top:0;float:left}.goog-tab-bar-start .goog-tab{margin:0 0 4px 1px;border-right:0}.goog-tab-bar-end .goog-tab{margin:0 1px 4px 0;border-left:0}.goog-tab-hover{text-decoration:underline;cursor:pointer}.goog-tab-disabled{color:#fff;background:#ccc;border-color:#ccc}.goog-tab-selected{background:#fff!important;color:black;font-weight:bold}.goog-tab-bar-top .goog-tab-selected{top:1px;margin-top:0;padding-bottom:5px}.goog-tab-bar-bottom .goog-tab-selected{top:-1px;margin-bottom:0;padding-top:5px}.goog-tab-bar-start .goog-tab-selected{left:1px;margin-left:0;padding-right:9px}.goog-tab-bar-end .goog-tab-selected{left:-1px;margin-right:0;padding-left:9px}.goog-tab-content{padding:.1em .8em .8em .8em;border:1px solid #8ac;border-top:none}.goog-tab-bar{position:relative;margin:0 0 0 5px;border:0;padding:0;list-style:none;cursor:default;outline:none}.goog-tab-bar-clear{border-top:1px solid #8ac;clear:both;height:0;overflow:hidden}.goog-tab-bar-start{float:left}.goog-tab-bar-end{float:right}* html .goog-tab-bar-start{margin-right:-3px}* html .goog-tab-bar-end{margin-left:-3px}#ae-nav ul{list-style-type:none;margin:0;padding:1em 0}#ae-nav ul li{padding-left:.5em}#ae-nav .ae-nav-selected{color:#000;display:block;font-weight:bold;background-color:#e5ecf9;margin-right:-1px;border-top-left-radius:4px;-moz-border-radius-topleft:4px;-webkit-border-top-left-radius:4px;border-bottom-left-radius:4px;-moz-border-radius-bottomleft:4px;-webkit-border-bottom-left-radius:4px}#ae-nav .ae-nav-bold{font-weight:bold}#ae-nav ul li span.ae-nav-disabled{color:#666}#ae-nav ul ul{margin:0;padding:0 0 0 .5em}#ae-nav ul ul li{padding-left:.5em}#ae-nav ul li a,#ae-nav ul li span,#ae-nav ul ul li a{padding-left:.5em}#ae-nav li a:link,#ae-nav li a:visited{color:#00c}.ae-nav-group{padding:.5em;margin:0 .75em 0 0;background-color:#fffbe8;border:1px solid #fff1a9}.ae-nav-group h4{font-weight:bold;padding:auto auto .5em .5em;padding-left:.4em;margin-bottom:.5em;padding-bottom:0}.ae-nav-group ul{margin:0 0 .5em 0;padding:0 0 0 1.3em;list-style-type:none}.ae-nav-group ul li{padding-bottom:.5em}.ae-nav-group li a:link,.ae-nav-group li a:visited{color:#00c}.ae-nav-group li a:hover{color:#00c}@media print{body{font-size:13px;width:8.5in;background:#fff}table,.ae-table-fixed{table-layout:automatic}tr{display:table-row!important}.g-doc-1024{width:8.5in}#ae-appbar-lrg,.ae-table-caption,.ae-table-nowrap,.ae-nowrap,th,td{overflow:visible!important;white-space:normal!important;background:#fff!important}.ae-print,.ae-toggle{display:none}#ae-lhs-nav-c{display:none}#ae-content{margin:0;padding:0}.goog-zippy-collapsed,.goog-zippy-expanded{background:none!important;padding:0!important}}#ae-admin-dev-table{margin:0 0 1em 0}.ae-admin-dev-tip,.ae-admin-dev-tip.ae-tip{margin:-0.31em 0 2.77em}#ae-sms-countryselect{margin-right:.5em}#ae-admin-enable-form{margin-bottom:1em}#ae-admin-services-c{margin-top:2em}#ae-admin-services{padding:0 0 0 3em;margin-bottom:1em;font-weight:bold}#ae-admin-logs-table-c{_margin-right:-2000px;_position:relative;_width:100%;background:#fff}#ae-admin-logs-table{margin:0;padding:0}#ae-admin-logs-filters{padding:3px 0 3px 5px}#ae-admin-logs-pagination{padding:6px 5px 0 0;text-align:right;width:45%}#ae-admin-logs-pagination span.ae-disabled{color:#666;background-color:transparent}#ae-admin-logs-table td{white-space:nowrap}#ae-storage-content div.ae-alert{padding-bottom:5px}#ae-admin-performance-form input[type=text]{width:2em}.ae-admin-performance-value{font-weight:normal}.ae-admin-performance-static-value{color:#666}#ae-admin-performance-frontend-class{margin-left:0.5em}.goog-slider-horizontal,.goog-twothumbslider-horizontal{position:relative;width:502px;height:7px;display:block;outline:0;margin:1.0em 0 0.9em 3em}.ae-slider-rail:before{position:relative;top:-0.462em;float:left;content:'Min';margin:0 0 0 -3em;color:#999}.ae-slider-rail{position:absolute;background-color:#d9d9d9;top:0;right:8px;bottom:0;left:8px;border:solid 1px;border-color:#a6a6a6 #b3b3b3 #bfbfbf;border-radius:5px}.ae-slider-rail:after{position:relative;top:-0.462em;float:right;content:'Max';margin:0 -3em 0 0;color:#999}.goog-slider-horizontal .goog-slider-thumb,.goog-twothumbslider-horizontal .goog-twothumbslider-value-thumb,.goog-twothumbslider-horizontal .goog-twothumbslider-extent-thumb{position:absolute;width:17px;height:17px;background:transparent url('/img/slider_thumb-down.png') no-repeat;outline:0}.goog-slider-horizontal .goog-slider-thumb{top:-5px}.goog-twothumbslider-horizontal .goog-twothumbslider-value-thumb{top:-11px}.goog-twothumbslider-horizontal .goog-twothumbslider-extent-thumb{top:2px;background-image:url('/img/slider_thumb-up.png')}.ae-admin-performance-scale{position:relative;display:inline-block;width:502px;margin:0 0 2.7em 3em}.ae-admin-performance-scale .ae-admin-performance-scale-start{position:absolute;display:inline-block;top:0;width:100%;text-align:left}.ae-admin-performance-scale .ae-admin-performance-scale-mid{position:absolute;display:inline-block;top:0;width:100%;text-align:center}.ae-admin-performance-scale .ae-admin-performance-scale-end{position:absolute;display:inline-block;top:0;width:100%;text-align:right}.ae-pagespeed-controls{margin:0 0 1em 8px}.ae-pagespeed-controls label{display:inline;font-weight:normal}#ae-pagespeed-flush-cache{margin-left:1em}#ae-pagespeed-flush-cache-status{margin-left:1em;font-weight:bold}.ae-absolute-container{display:inline-block;width:100%}.ae-hidden-range{display:none}.ae-default-version-radio-column{width:1em}#ae-settings-builtins-change{margin-bottom:1em}#ae-billing-form-c{_margin-right:-3000px;_position:relative;_width:100%}.ae-rounded-top-small{-moz-border-radius-topleft:3px;-webkit-border-top-left-radius:3px;-moz-border-radius-topright:3px;-webkit-border-top-right-radius:3px}.ae-progress-content{height:400px}#ae-billing-tos{text-align:left;width:100%;margin-bottom:.5em}.ae-billing-budget-section{margin-bottom:1.5em}.ae-billing-budget-section .g-unit,.g-unit .ae-billing-budget-section .g-unit,.g-unit .g-unit .ae-billing-budget-section .g-unit{margin:0 0 0 11em;width:auto;float:none}.g-unit .g-unit .ae-billing-budget-section .g-first,.g-unit .ae-billing-budget-section .g-first,.ae-billing-budget-section .g-first{margin:0;width:11em;float:left}#ae-billing-form .ae-btn-row{margin-left:11em}#ae-billing-form .ae-btn-row .ae-info{margin-top:10px}#ae-billing-checkout{width:150px;float:left}#ae-billing-alloc-table{border:1px solid #c5d7ef;border-bottom:none;width:100%;margin-top:.5em}#ae-billing-alloc-table th,#ae-billing-alloc-table td{padding:.35em 1em .25em .35em;border-bottom:1px solid #c5d7ef;color:#000;white-space:nowrap}.ae-billing-resource{background-color:transparent;font-weight:normal}#ae-billing-alloc-table tr th span{font-weight:normal}#ae-billing-alloc-table tr{vertical-align:baseline}#ae-billing-alloc-table th{white-space:nowrap}#ae-billing-alloc-table .ae-editable span.ae-text-input-clone,#ae-billing-alloc-table .ae-readonly input{display:none}#ae-billing-alloc-table .ae-readonly span.ae-text-input-clone,#ae-billing-alloc-table .ae-editable input{display:inline}#ae-billing-alloc-table td span.ae-billing-warn-note,#ae-billing-table-errors .ae-billing-warn-note{margin:0;background-repeat:no-repeat;display:inline-block;background-image:url('/img/icn/warning.png');text-align:right;padding-left:16px;padding-right:.1em;height:16px;font-weight:bold}#ae-billing-alloc-table td span.ae-billing-warn-note span,#ae-billing-table-errors .ae-billing-warn-note span{vertical-align:super;font-size:80%}#ae-billing-alloc-table td span.ae-billing-error-hidden,#ae-billing-table-errors .ae-billing-error-hidden{display:none}.ae-billing-percent{font-size:80%;color:#666;margin-left:3px}#ae-billing-week-info{margin-top:5px;line-height:1.4}#ae-billing-table-errors{margin-top:.3em}#ae-billing-allocation-noscript{margin-top:1.5em}#ae-billing-allocation-custom-opts{margin-left:2.2em}#ae-billing-settings h2{font-size:1em;display:inline}#ae-billing-settings p{padding:.3em 0 .5em}#ae-billing-settings-table{margin:.4em 0 .5em}#ae-settings-resource-col{width:19%}#ae-settings-budget-col{width:11%}#ae-billing-settings-table .ae-settings-budget-col{padding-right:2em}.ae-table th.ae-settings-unit-cell,.ae-table td.ae-settings-unit-cell,.ae-table th.ae-total-unit-cell,.ae-table td.ae-total-unit-cell{padding-left:1.2em}#ae-settings-unit-col{width:18%}#ae-settings-paid-col{width:15%}#ae-settings-free-col{width:15%}#ae-settings-total-col{width:22%}.ae-billing-inline-link{margin-left:.5em}.ae-billing-settings-section{margin-bottom:2em}#ae-billing-settings form{display:inline-block}#ae-billing-settings .ae-btn-row{margin-top:0.5em}#ae-billing-budget-setup-checkout{margin-bottom:0}#ae-billing-vat-c .ae-field-hint{width:85%}#ae-billing-checkout-note{margin-top:.8em}.ae-drachma-preset{background-color:#f6f9ff;margin-left:11em}.ae-drachma-preset p{margin-top:.5em}.ae-table thead th.ae-currency-th{text-align:right}#ae-billing-logs-date{width:15%}#ae-billing-logs-event{width:69%}#ae-billing-logs-amount{text-align:right;width:8%}#ae-billing-logs-balance{text-align:right;width:8%}#ae-billing-history-expand .ae-action{margin-left:1em}.ae-table .ae-billing-usage-premier,.ae-table .ae-billing-usage-report{width:100%;*width:auto;margin:0 0 1em 0}.ae-table .ae-billing-usage-report th,.ae-table .ae-billing-usage-premier th,.ae-billing-charges th{color:#666;border-top:0}.ae-table .ae-billing-usage-report th,.ae-table .ae-billing-usage-report td,.ae-table .ae-billing-usage-premier th,.ae-table .ae-billing-usage-premier td,.ae-billing-charges th,.ae-billing-charges td{background-color:transparent;padding:.4em 0;border-bottom:1px solid #ddd}.ae-table .ae-billing-usage-report tfoot td,.ae-billing-charges tfoot td{border-bottom:none}table.ae-billing-usage-report col.ae-billing-report-resource{width:30%}table.ae-billing-usage-report col.ae-billing-report-used{width:20%}table.ae-billing-usage-report col.ae-billing-report-free{width:16%}table.ae-billing-usage-report col.ae-billing-report-paid{width:17%}table.ae-billing-usage-report col.ae-billing-report-charge{width:17%}table.ae-billing-usage-premier col.ae-billing-report-resource{width:50%}table.ae-billing-usage-premier col.ae-billing-report-used{width:30%}table.ae-billing-usage-premier col.ae-billing-report-unit{width:20%}.ae-billing-change-resource{width:85%}.ae-billing-change-budget{width:15%}#ae-billing-always-on-label{display:inline}#ae-billing-budget-buffer-label{display:inline}.ae-billing-charges{width:50%}.ae-billing-charges-charge{text-align:right}.ae-billing-usage-report-container{padding:1em 1em 0 1em}#ae-billing-new-usage{background-color:#f6f9ff}.goog-zippy-expanded{background-image:url('/img/wgt/minus.gif');cursor:pointer;background-repeat:no-repeat;padding-left:17px}.goog-zippy-collapsed{background-image:url('/img/wgt/plus.gif');cursor:pointer;background-repeat:no-repeat;padding-left:17px}#ae-admin-logs-pagination{width:auto}.ae-usage-cycle-note{color:#555}#ae-createapp-start{background-color:#c6d5f1;padding:1em;padding-bottom:2em;text-align:center}#ae-admin-app_id_alias-check,#ae-createapp-id-check{margin:0 0 0 1em}#ae-admin-app_id_alias-message{display:block;margin:.4em 0}#ae-createapp-id-content{width:100%}#ae-createapp-id-content td{vertical-align:top}#ae-createapp-id-td{white-space:nowrap;width:1%}#ae-createapp-id-td #ae-createapp-id-error{position:absolute;width:24em;padding-left:1em;white-space:normal}#ae-createapp-id-error-td{padding-left:1em}#ae-admin-dev-invite label{float:left;width:3.6em;position:relative;top:.3em}#ae-admin-dev-invite .ae-radio{margin-left:3.6em}#ae-admin-dev-invite .ae-radio label{float:none;width:auto;font-weight:normal;position:static}#ae-admin-dev-invite .goog-button{margin-left:3.6em}#ae-admin-dev-invite .ae-field-hint{margin-left:4.2em}#ae-admin-dev-invite .ae-radio .ae-field-hint{margin-left:0}.ae-you{color:#008000}#ae-authdomain-opts{margin-bottom:1em}#ae-authdomain-content .ae-input-text,#ae-authdomain-content .ae-field-hint{margin:.3em 0 .4em 2.5em}#ae-authdomain-opts a{margin-left:1em}#ae-authdomain-opts-hint{margin-top:.2em;color:#666667;font-size:.85em}#ae-authdomain-content #ae-authdomain-desc .ae-field-hint{margin-left:0}#ae-storage-opts{margin-bottom:1em}#ae-storage-content .ae-input-text,#ae-storage-content .ae-field-hint{margin:.3em 0 .4em 2.5em}#ae-storage-opts a{margin-left:1em}#ae-storage-opts-hint{margin-top:.2em;color:#666667;font-size:.85em}#ae-storage-content #ae-storage-desc .ae-field-hint{margin-left:0}#ae-dash .g-section{margin:0 0 1em}#ae-dash * .g-section{margin:0}#ae-dash-quota .ae-alert{padding-left:1.5em}.ae-dash-email-disabled{background:url('/img/icn/exclamation_circle.png') no-repeat;margin-top:.5em;margin-bottom:.5em;min-height:16px;padding-left:1.5em}#ae-dash-email-disabled-footnote{padding-left:1.5em;margin:5px 0 0;font-weight:normal}#ae-dash-graph-c{border:1px solid #c5d7ef;padding:5px 0}#ae-dash-graph-change{margin:0 0 0 5px}#ae-dash-graph-img{padding:5px;margin-top:.5em;background-color:#fff;display:block}#ae-dash-graph-nodata{text-align:center}#ae-dash .ae-logs-severity{margin-right:.5em}#ae-dash .g-c{padding:0 0 0 .1em}#ae-dash .g-tpl-50-50 .g-unit .g-c{padding:0 0 0 1em}#ae-dash .g-tpl-50-50 .g-first .g-c{padding:0 1em 0 .1em}.ae-quota-warnings{background-color:#fffbe8;margin:0;padding:.5em .5em 0;text-align:left}.ae-quota-warnings div{padding:0 0 .5em}#ae-dash-quota-refresh-info{font-size:85%}#ae-dash #ae-dash-dollar-bucket-c #ae-dash-dollar-bucket{width:100%;float:none}#ae-dash #ae-dash-quota-bar-col,#ae-dash .ae-dash-quota-bar{width:100px}#ae-dash-quotadetails #ae-dash-quota-bar-col,#ae-dash-quotadetails .ae-dash-quota-bar{width:200px}#ae-dash-quota-percent-col{width:3.5em}#ae-dash-quota-cost-col{width:15%}#ae-dash-quota-alert-col{width:3.5em}#ae-dash .ae-dash-quota-alert-td{padding:0}.ae-dash-quota-alert-td a{display:block;width:16px;height:16px}#ae-dash .ae-dash-quota-alert-td .ae-alert{display:block;width:16px;height:16px;margin:0;padding:0}#ae-dash .ae-dash-quota-alert-td .ae-dash-email-disabled{display:block;width:16px;height:16px;margin:0;padding:0}#ae-dash-quota tbody th{font-weight:normal}#ae-dash-quota caption{padding:0}#ae-dash-quota caption .g-c{padding:3px}.ae-dash-quota-bar{float:left;background-color:#c0c0c0;height:13px;margin:.1em 0 0 0;position:relative}.ae-dash-quota-footnote{margin:5px 0 0;font-weight:normal}.ae-quota-warning{background-color:#f90}.ae-quota-alert{background-color:#c00}.ae-quota-normal{background-color:#0b0}.ae-quota-alert-text{color:#c00}.ae-favicon-text{font-size:.85em}#ae-dash-popular{width:97%}#ae-dash-popular-reqsec-col{width:6.5em}#ae-dash-popular-req-col{width:7em}#ae-dash-popular-mcycles-col{width:9.5em}#ae-dash-popular-latency-col{width:7em}#ae-dash-popular .ae-unimportant{font-size:80%}#ae-dash-popular .ae-nowrap,#ae-dash-errors .ae-nowrap{margin-right:5px;overflow:hidden}#ae-dash-popular th span,#ae-dash-errors th span{font-size:.8em;font-weight:normal;display:block}#ae-dash-errors caption .g-unit{width:9em}#ae-dash-errors-count-col{width:5em}#ae-dash-errors-percent-col{width:7em}#ae-dash-graph-chart-type{float:left;margin-right:1em}#ae-apps-all strong.ae-disabled{color:#000;background:#eee}.ae-quota-resource{width:30%}.ae-quota-safety-limit{width:10%}#ae-quota-details h3{padding-bottom:0;margin-bottom:.25em}#ae-quota-details table{margin-bottom:1.75em}#ae-quota-details table.ae-quota-requests{margin-bottom:.5em}#ae-quota-refresh-note p{text-align:right;padding-top:.5em;padding-bottom:0;margin-bottom:0}#ae-quota-first-api.g-section{padding-bottom:0;margin-bottom:.25em}#ae-instances-summary-table,#ae-instances-details-table{margin-bottom:1em}.ae-instances-details-availability-image{float:left;margin-right:.5em}.ae-instances-small-text{font-size:80%}.ae-instances-small-text .ae-separator{color:#666}.ae-instances-highlight td{background-color:#fff1a8}.ae-appbar-superuser-message strong{color:red}#ae-backends-table tr{vertical-align:baseline}.ae-backends-class-reminder{font-size:80%;color:#666;margin-left:3px}.ac-renderer{font:normal 13px Arial,sans-serif;position:absolute;background:#fff;border:1px solid #666;-moz-box-shadow:2px 2px 2px rgba(102,102,102,.4);-webkit-box-shadow:2px 2px 2px rgba(102,102,102,.4);width:202px}.ac-row{cursor:pointer;padding:.4em}.ac-highlighted{font-weight:bold}.ac-active{background-color:#b2b4bf}#ae-datastore-explorer-c{_margin-right:-3000px;_position:relative;_width:100%}#ae-datastore-explorer form dt{margin:1em 0 0 0}#ae-datastore-explorer #ae-datastore-explorer-labels{margin:0 0 3px}#ae-datastore-explorer-header .ae-action{margin-left:1em}#ae-datastore-explorer .id{white-space:nowrap}#ae-datastore-explorer caption{text-align:right;padding:5px}#ae-datastore-explorer-submit{margin-top:5px}#ae-datastore-explorer-namespace{margin-top:7px;margin-right:5px}#ae-datastore-stats-namespace-input,#ae-datastore-explorer-namespace-query,#ae-datastore-explorer-namespace-create{width:200px}#ae-datastore-explorer-gql-spacer{margin-top:22px}h4 #ae-datastore-explorer-gql-label{font-weight:normal}#ae-datastore-form em{font-style:normal;font-weight:normal;margin:0 0 0 .2em;color:#666}#ae-datastore-form dt{font-weight:bold}#ae-datastore-form dd{margin:.4em 0 .3em 1.5em;overflow:auto;zoom:1}#ae-datastore-form dd em{width:4em;float:left}#ae-datastore-form dd.ae-last{margin-bottom:1em}#ae-datastore-explorer-tabs-content{margin-bottom:1em}#ae-datastore-explorer-list .ae-label-row,#ae-datastore-explorer-new .ae-label-row{float:left;padding-top:.2em}#ae-datastore-explorer-list .ae-input-row,#ae-datastore-explorer-list .ae-btn-row,#ae-datastore-explorer-new .ae-input-row,#ae-datastore-explorer-new .ae-btn-row{margin-left:6em}#ae-datastore-explorer-list .ae-btn-row,#ae-datastore-explorer-new .ae-btn-row{margin-bottom:0}.ae-datastore-index-name{font-size:1.2em;font-weight:bold}.ae-table .ae-datastore-index-defs{padding-left:20px}.ae-datastore-index-defs-row{border-top:1px solid #ddd}.ae-datastore-index-defs .ae-unimportant{font-size:.8em}.ae-datastore-index-status{border:1px solid #c0dfbf;background:#f3f7f3;margin:0 25px 0 0;padding:3px}#ae-datastore-index-status-col{width:20%}#ae-datastore-index-stat-col{width:20%}.ae-datastore-index-status-Building{border-color:#edebcd;background:#fefdec}.ae-datastore-index-status-Deleting{border-color:#ccc;background:#eee}.ae-datastore-index-status-Error{border-color:#ffd3b4;background:#ffeae0}.ae-datastore-pathlink{font-size:.9em}#ae-datastore-stats-top-level-c{padding-bottom:1em;margin-bottom:1em;border-bottom:1px solid #e5ecf9}#ae-datastore-stats-top-level{width:100%}#ae-datastore-stats-piecharts-c{margin-bottom:1em}.ae-datastore-stats-piechart-label{font-size:.85em;font-weight:normal;text-align:center;padding:0}#ae-datastore-stats-property-type{width:60%}#ae-datastore-stats-size-all{width:20%}#ae-datastore-stats-index-size-all{width:20%}#ae-datastore-stats-property-name{width:40%}#ae-datastore-stats-type{width:10%}#ae-datastore-stats-size-entity{width:15%}#ae-datastore-stats-index-size-entity{width:15%}#ae-datastore-stats-percentage-size-entity{width:20%}#ae-datastore-blob-filter-form{margin-bottom:1em}#ae-datastore-blob-query-filter-label{padding-right:.5em}#ae-datastore-blob-filter-contents{padding-top:.5em}#ae-datastore-blob-date-after,#ae-datastore-blob-date-before{float:left}#ae-datastore-blob-date-after{margin-right:1em}#ae-datastore-blob-order label{font-weight:normal}#ae-datastore-blob-col-check{width:2%}#ae-datastore-blob-col-file{width:45%}#ae-datastore-blob-col-type{width:14%}#ae-datastore-blob-col-size{width:16%}#ae-blobstore-col-date{width:18%}#ae-blob-detail-filename{padding-bottom:0}#ae-blob-detail-filename span{font-weight:normal}#ae-blob-detail-key{font-size:85%}#ae-blob-detail-preview{margin-top:1em}#ae-blob-detail-dl{text-align:right}.ae-deployment-add-labels{padding:0 5px 0 20px}.ae-deployment-button-cell{width:95px}#ae-deployment-dm-dialog{width:400px}.ae-deployment-dm-selector{margin:20px 2px 20px 5px}#ae-deployment-exp-add{margin-top:5px}#ae-deployment-exp-contents{margin-top:5px;overflow:hidden}#ae-deployment-exp-desc{margin-bottom:15px}#ae-deployment-exp-div{background-color:#e5ecf9;border:1px solid #c5d7ef;margin:20px 0;padding:7px 4px}#ae-deployment-exp-hdr{font-weight:bold;margin:5px 0 5px}#ae-deployment-exp-tbl{width:400px}#ae-deployment-exp-toggle{font-weight:bold}.ae-deployment-set-button{width:22px}.ae-deployment-traffic-input{width:30px}#ae-domain-admins-list li{margin-bottom:.3em}#ae-domain-admins-list button{margin-left:.5em}#ae-new-app-dialog-c{width:500px}#ae-new-app-dialog-c .g-section{margin-bottom:1em}p.light-note{color:#555}.ae-bottom-message{margin-top:1em}#domsettings-form div.ae-radio{margin-left:1.7em}#domsettings-form div.ae-radio input{margin-left:-1.47em;float:left}#ae-logs-c{_margin-right:-2000px;_position:relative;_width:100%;background:#fff}#ae-logs{background-color:#c5d7ef;padding:1px;line-height:1.65}#ae-logs .ae-table-caption{border:0}#ae-logs-c ol,#ae-logs-c li{list-style:none;padding:0;margin:0}#ae-logs-c li li{margin:0 0 0 3px;padding:0 0 0 17px}.ae-log-noerror{padding-left:23px}#ae-logs-form .goog-inline-block{margin-top:0}.ae-logs-usage-info{padding-left:.5em}.ae-logs-reqlog .snippet{margin:.1em}.ae-logs-applog .snippet{color:#666}.ae-logs-severity{display:block;float:left;height:1.2em;width:1.2em;line-height:1.2;text-align:center;text-transform:capitalize;font-weight:bold;border-radius:2px;-moz-border-radius:2px;-webkit-border-radius:2px}.ae-logs-severity-4{background-color:#f22;color:#000}.ae-logs-severity-3{background-color:#f90;color:#000}.ae-logs-severity-2{background-color:#fd0}.ae-logs-severity-1{background-color:#3c0;color:#000}.ae-logs-severity-0{background-color:#09f;color:#000}#ae-logs-legend{margin:1em 0 0 0}#ae-logs-legend ul{list-style:none;margin:0;padding:0}#ae-logs-legend li,#ae-logs-legend strong{float:left;margin:0 1em 0 0}#ae-logs-legend li span{margin-right:.3em}.ae-logs-timestamp{padding:0 5px;font-size:85%}#ae-logs-form-c{margin-bottom:5px;padding-bottom:.5em;padding-left:1em}#ae-logs-form{padding:.3em 0 0}#ae-logs-form .ae-label-row{float:left;padding-top:.2em;margin-right:0.539em}#ae-logs-form .ae-input-row,#ae-logs-form .ae-btn-row{margin-left:4em}#ae-logs-form .ae-btn-row{margin-bottom:0}#ae-logs-requests-c{margin-bottom:.1em}#ae-logs-requests-c input{margin:0}#ae-logs-requests-all-label{margin-right:0.539em}#ae-logs-form-options{margin-top:8px}#ae-logs-tip{margin:.2em 0}#ae-logs-expand{margin-right:.2em}#ae-logs-severity-level-label{margin-top:.3em;display:block}#ae-logs-filter-hint-labels-list{margin:2px 0}#ae-logs-filter-hint-labels-list span{position:absolute}#ae-logs-filter-hint-labels-list ul{margin-left:5.5em;padding:0}#ae-logs-filter-hint-labels-list li{float:left;margin-right:.4em;line-height:1.2}.ae-toggle .ae-logs-getdetails,.ae-toggle pre{display:none}.ae-log-expanded .ae-toggle pre{display:block}#ae-logs-c .ae-log .ae-toggle{cursor:default;background:none;padding-left:0}#ae-logs-c .ae-log .ae-toggle h5{cursor:pointer;background-position:0 .55em;background-repeat:no-repeat;padding-left:17px}.ae-log .ae-plus h5{background-image:url('/img/wgt/plus.gif')}.ae-log .ae-minus h5{background-image:url('/img/wgt/minus.gif')}.ae-log{overflow:hidden;background-color:#fff;padding:.3em 0;line-height:1.65;border-bottom:1px solid #c5d7ef}.ae-log .ae-even{background-color:#e9e9e9;border:0}.ae-log h5{font-weight:normal;white-space:nowrap;padding:.4em 0 0 0}.ae-log span,.ae-log strong{margin:0 .3em}.ae-log .ae-logs-snippet{color:#666}.ae-log pre,.ae-logs-expanded{padding:.3em 0 .5em 1.5em;margin:0;font-family:"Courier New"}.ae-log .file{font-weight:bold}.ae-log.ae-log-expanded .file{white-space:pre-wrap;word-wrap:break-word}.ae-logs-app .ae-logs-req{display:none}.ae-logs-req .ae-app,.ae-logs-both .ae-app{padding-left:1em}#ae-dos-blacklist-rejects-table{text-align:left}#ae-dash-quota-percent-col{width:3.5em}.ae-cron-status-ok{color:#008000;font-size:90%;font-weight:bold}.ae-cron-status-error{color:#a03;font-size:90%;font-weight:bold}#ae-cronjobs-table .ae-table td{vertical-align:top}#ae-tasks-table td{vertical-align:top}#ae-tasks-quota{margin:0 0 1em 0}#ae-tasks-quota .ae-dash-quota-bar{width:150px}#ae-tasks-quota #ae-dash-quota-bar-col,#ae-tasks-quota .ae-dash-quota-bar{width:200px}.ae-tasks-paused-row{color:#666;font-style:italic;font-weight:bold}#ae-tasks-quota .ae-quota-safety-limit{width:30%}#ae-tasks-table{margin-top:1em}#ae-tasks-queuecontrols{margin-top:1em;margin-bottom:1em}#ae-tasks-delete-col{width:1em}#ae-tasks-eta-col,#ae-tasks-creation-col{width:11em}#ae-tasks-actions-col{width:7em}#ae-tasks-retry-col{width:4em}#ae-tasks-body-col{width:6em}#ae-tasks-headers-col{width:7em}.ae-tasks-hex-column,.ae-tasks-ascii-column{width:16em}#ae-tasks-table .ae-tasks-arrow{text-align:center}
\ No newline at end of file
diff --git a/google/appengine/ext/datastore_admin/static/js/compiled.js b/google/appengine/ext/datastore_admin/static/js/compiled.js
index 3ea3fad..b156101 100755
--- a/google/appengine/ext/datastore_admin/static/js/compiled.js
+++ b/google/appengine/ext/datastore_admin/static/js/compiled.js
@@ -1,18 +1,18 @@
var h=void 0,j=!0,k=null,l=!1,n=document;function aa(a,b){return a.length=b}function p(a,b){return a.disabled=b}function q(a,b){return a.currentTarget=b}function ba(a,b){return a.target=b}
-var s="push",t="length",ca="propertyIsEnumerable",u="prototype",w="slice",x="replace",y="split",z="indexOf",A="target",B="call",da="keyCode",ea="handleEvent",C="type",E="apply",fa="name",F,G=this,H=function(a){var b=typeof a;if("object"==b)if(a){if(a instanceof Array)return"array";if(a instanceof Object)return b;var d=Object[u].toString[B](a);if("[object Window]"==d)return"object";if("[object Array]"==d||"number"==typeof a[t]&&"undefined"!=typeof a.splice&&"undefined"!=typeof a[ca]&&!a[ca]("splice"))return"array";
-if("[object Function]"==d||"undefined"!=typeof a[B]&&"undefined"!=typeof a[ca]&&!a[ca]("call"))return"function"}else return"null";else if("function"==b&&"undefined"==typeof a[B])return"object";return b},ga=function(a){var b=H(a);return"array"==b||"object"==b&&"number"==typeof a[t]},I=function(a){return"string"==typeof a},ha=function(a){var b=typeof a;return"object"==b&&a!=k||"function"==b},K="closure_uid_"+Math.floor(2147483648*Math.random()).toString(36),ia=0,ja=function(a,b){function d(){}d.prototype=
-b[u];a.B=b[u];a.prototype=new d};var ka=function(a){this.stack=Error().stack||"";a&&(this.message=""+a)};ja(ka,Error);ka[u].name="CustomError";var la=function(a,b){for(var d=1;d<arguments[t];d++)var e=(""+arguments[d])[x](/\$/g,"$$$$"),a=a[x](/\%s/,e);return a},ra=function(a,b){if(b)return a[x](ma,"&")[x](na,"<")[x](oa,">")[x](pa,""");if(!qa.test(a))return a;-1!=a[z]("&")&&(a=a[x](ma,"&"));-1!=a[z]("<")&&(a=a[x](na,"<"));-1!=a[z](">")&&(a=a[x](oa,">"));-1!=a[z]('"')&&(a=a[x](pa,"""));return a},ma=/&/g,na=/</g,oa=/>/g,pa=/\"/g,qa=/[&<>\"]/;Math.random();var sa=function(a,b){b.unshift(a);ka[B](this,la[E](k,b));b.shift()};ja(sa,ka);sa[u].name="AssertionError";var ta=function(a,b,d){if(!a){var e=Array[u][w][B](arguments,2),f="Assertion failed";if(b)var f=f+(": "+b),c=e;throw new sa(""+f,c||[]);}return a};var L=Array[u],ua=L[z]?function(a,b,d){ta(a[t]!=k);return L[z][B](a,b,d)}:function(a,b,d){d=d==k?0:0>d?Math.max(0,a[t]+d):d;if(I(a))return!I(b)||1!=b[t]?-1:a[z](b,d);for(;d<a[t];d++)if(d in a&&a[d]===b)return d;return-1},va=L.forEach?function(a,b,d){ta(a[t]!=k);L.forEach[B](a,b,d)}:function(a,b,d){for(var e=a[t],f=I(a)?a[y](""):a,c=0;c<e;c++)c in f&&b[B](d,f[c],c,a)},wa=function(a){return L.concat[E](L,arguments)},xa=function(a){if("array"==H(a))return wa(a);for(var b=[],d=0,e=a[t];d<e;d++)b[d]=a[d];
-return b},ya=function(a,b,d){ta(a[t]!=k);return 2>=arguments[t]?L[w][B](a,b):L[w][B](a,b,d)};var za=function(a,b,d){for(var e in a)b[B](d,a[e],e,a)},Aa="constructor,hasOwnProperty,isPrototypeOf,propertyIsEnumerable,toLocaleString,toString,valueOf".split(","),Ba=function(a,b){for(var d,e,f=1;f<arguments[t];f++){e=arguments[f];for(d in e)a[d]=e[d];for(var c=0;c<Aa[t];c++)d=Aa[c],Object[u].hasOwnProperty[B](e,d)&&(a[d]=e[d])}};var M,Ca,Da,Ea,Fa=function(){return G.navigator?G.navigator.userAgent:k},Ga=function(){return G.navigator};Ea=Da=Ca=M=l;var N;if(N=Fa()){var Ha=Ga();M=0==N[z]("Opera");Ca=!M&&-1!=N[z]("MSIE");(Da=!M&&-1!=N[z]("WebKit"))&&N[z]("Mobile");Ea=!M&&!Da&&"Gecko"==Ha.product}var Ia=M,O=Ca,P=Ea,Q=Da,Ja=Ga(),Ka=Ja&&Ja.platform||"";Ka[z]("Mac");Ka[z]("Win");Ka[z]("Linux");Ga()&&(Ga().appVersion||"")[z]("X11");var La;
-a:{var Ma="",R;if(Ia&&G.opera)var Na=G.opera.version,Ma="function"==typeof Na?Na():Na;else if(P?R=/rv\:([^\);]+)(\)|;)/:O?R=/MSIE\s+([^\);]+)(\)|;)/:Q&&(R=/WebKit\/(\S+)/),R)var Oa=R.exec(Fa()),Ma=Oa?Oa[1]:"";if(O){var Pa,Qa=G.document;Pa=Qa?Qa.documentMode:h;if(Pa>parseFloat(Ma)){La=""+Pa;break a}}La=Ma}
-var Ra=La,Sa={},S=function(a){var b;if(!(b=Sa[a])){var d=0;b=(""+Ra)[x](/^[\s\xa0]+|[\s\xa0]+$/g,"")[y](".");for(var e=(""+a)[x](/^[\s\xa0]+|[\s\xa0]+$/g,"")[y]("."),f=Math.max(b[t],e[t]),c=0;0==d&&c<f;c++){var g=b[c]||"",i=e[c]||"",m=RegExp("(\\d*)(\\D*)","g"),o=RegExp("(\\d*)(\\D*)","g");do{var v=m.exec(g)||["","",""],r=o.exec(i)||["","",""];if(0==v[0][t]&&0==r[0][t])break;var d=0==v[1][t]?0:parseInt(v[1],10),J=0==r[1][t]?0:parseInt(r[1],10),d=(d<J?-1:d>J?1:0)||((0==v[2][t])<(0==r[2][t])?-1:(0==
-v[2][t])>(0==r[2][t])?1:0)||(v[2]<r[2]?-1:v[2]>r[2]?1:0)}while(0==d)}b=Sa[a]=0<=d}return b},Ta={},Ua=function(a){return Ta[a]||(Ta[a]=O&&!!n.documentMode&&n.documentMode>=a)};var Va=!O||Ua(9);!P&&!O||O&&Ua(9)||P&&S("1.9.1");O&&S("9");var Wa=function(a,b){var d;d=(d=a.className)&&"function"==typeof d[y]?d[y](/\s+/):[];var e=ya(arguments,1),f;f=d;for(var c=0,g=0;g<e[t];g++)0<=ua(f,e[g])||(f[s](e[g]),c++);f=c==e[t];a.className=d.join(" ");return f};var Xa=function(a,b,d,e){var a=e||a,f=b&&"*"!=b?b.toUpperCase():"";if(a.querySelectorAll&&a.querySelector&&(!Q||"CSS1Compat"==n.compatMode||S("528"))&&(f||d))return a.querySelectorAll(f+(d?"."+d:""));if(d&&a.getElementsByClassName){b=a.getElementsByClassName(d);if(f){for(var a={},c=e=0,g;g=b[c];c++)f==g.nodeName&&(a[e++]=g);aa(a,e);return a}return b}b=a.getElementsByTagName(f||"*");if(d){a={};for(c=e=0;g=b[c];c++){var f=g.className,i;if(i="function"==typeof f[y])f=f[y](/\s+/),i=0<=ua(f,d);i&&(a[e++]=
-g)}aa(a,e);return a}return b},Za=function(a,b){za(b,function(b,e){"style"==e?a.style.cssText=b:"class"==e?a.className=b:"for"==e?a.htmlFor=b:e in Ya?a.setAttribute(Ya[e],b):0==e.lastIndexOf("aria-",0)?a.setAttribute(e,b):a[e]=b})},Ya={cellpadding:"cellPadding",cellspacing:"cellSpacing",colspan:"colSpan",rowspan:"rowSpan",valign:"vAlign",height:"height",width:"width",usemap:"useMap",frameborder:"frameBorder",maxlength:"maxLength",type:"type"},ab=function(a,b,d,e){function f(c){c&&b.appendChild(I(c)?
-a.createTextNode(c):c)}for(;e<d[t];e++){var c=d[e];ga(c)&&!(ha(c)&&0<c.nodeType)?va($a(c)?xa(c):c,f):f(c)}},bb=function(a,b,d){var e=n,f=arguments,c=f[0],g=f[1];if(!Va&&g&&(g[fa]||g[C])){c=["<",c];g[fa]&&c[s](' name="',ra(g[fa]),'"');if(g[C]){c[s](' type="',ra(g[C]),'"');var i={};Ba(i,g);g=i;delete g[C]}c[s](">");c=c.join("")}c=e.createElement(c);g&&(I(g)?c.className=g:"array"==H(g)?Wa[E](k,[c].concat(g)):Za(c,g));2<f[t]&&ab(e,c,f,2);return c},$a=function(a){if(a&&"number"==typeof a[t]){if(ha(a))return"function"==
-typeof a.item||"string"==typeof a.item;if("function"==H(a))return"function"==typeof a.item}return l};var cb=function(a){cb[" "](a);return a};cb[" "]=function(){};!O||Ua(9);var db=!O||Ua(9);O&&S("8");!Q||S("528");P&&S("1.9b")||O&&S("8")||Ia&&S("9.5")||Q&&S("528");!P||S("8");var eb=function(){};eb[u].u=l;eb[u].m=function(){this.u||(this.u=j,this.n())};eb[u].n=function(){this.D&&fb[E](k,this.D)};var fb=function(a){for(var b=0,d=arguments[t];b<d;++b){var e=arguments[b];ga(e)?fb[E](k,e):e&&"function"==typeof e.m&&e.m()}};var T=function(a,b){this.type=a;ba(this,b);q(this,this[A])};ja(T,eb);T[u].n=function(){delete this[C];delete this[A];delete this.currentTarget};T[u].o=l;T[u].C=j;var U=function(a,b){a&&this.i(a,b)};ja(U,T);F=U[u];ba(F,k);F.relatedTarget=k;F.offsetX=0;F.offsetY=0;F.clientX=0;F.clientY=0;F.screenX=0;F.screenY=0;F.button=0;F.keyCode=0;F.charCode=0;F.ctrlKey=l;F.altKey=l;F.shiftKey=l;F.metaKey=l;
-F.i=function(a,b){var d=this.type=a[C];T[B](this,d);ba(this,a[A]||a.srcElement);q(this,b);var e=a.relatedTarget;if(e){if(P){var f;a:{try{cb(e.nodeName);f=j;break a}catch(c){}f=l}f||(e=k)}}else"mouseover"==d?e=a.fromElement:"mouseout"==d&&(e=a.toElement);this.relatedTarget=e;this.offsetX=Q||a.offsetX!==h?a.offsetX:a.layerX;this.offsetY=Q||a.offsetY!==h?a.offsetY:a.layerY;this.clientX=a.clientX!==h?a.clientX:a.pageX;this.clientY=a.clientY!==h?a.clientY:a.pageY;this.screenX=a.screenX||0;this.screenY=
-a.screenY||0;this.button=a.button;this.keyCode=a[da]||0;this.charCode=a.charCode||("keypress"==d?a[da]:0);this.ctrlKey=a.ctrlKey;this.altKey=a.altKey;this.shiftKey=a.shiftKey;this.metaKey=a.metaKey;this.state=a.state;delete this.C;delete this.o};F.n=function(){U.B.n[B](this);ba(this,k);q(this,k);this.relatedTarget=k};var gb=function(){},hb=0;F=gb[u];F.b=0;F.e=l;F.s=l;F.i=function(a,b,d,e,f,c){if("function"==H(a))this.t=j;else if(a&&a[ea]&&"function"==H(a[ea]))this.t=l;else throw Error("Invalid listener argument");this.h=a;this.q=b;this.src=d;this.type=e;this.w=!!f;this.p=c;this.s=l;this.b=++hb;this.e=l};F.handleEvent=function(a){return this.t?this.h[B](this.p||this.src,a):this.h[ea][B](this.h,a)};var V={},W={},X={},Y={},ib=function(a,b,d,e,f){if(b){if("array"==H(b)){for(var c=0;c<b[t];c++)ib(a,b[c],d,e,f);return k}var e=!!e,g=W;b in g||(g[b]={a:0,c:0});g=g[b];e in g||(g[e]={a:0,c:0},g.a++);var g=g[e],i=a[K]||(a[K]=++ia),m;g.c++;if(g[i]){m=g[i];for(c=0;c<m[t];c++)if(g=m[c],g.h==d&&g.p==f){if(g.e)break;return m[c].b}}else m=g[i]=[],g.a++;c=jb();c.src=a;g=new gb;g.i(d,c,a,b,e,f);d=g.b;c.b=d;m[s](g);V[d]=g;X[i]||(X[i]=[]);X[i][s](g);a.addEventListener?(a==G||!a.v)&&a.addEventListener(b,c,e):a.attachEvent(b in
-Y?Y[b]:Y[b]="on"+b,c);return d}throw Error("Invalid event type");},jb=function(){var a=kb,b=db?function(d){return a[B](b.src,b.b,d)}:function(d){d=a[B](b.src,b.b,d);if(!d)return d};return b},lb=function(a,b,d,e){if(!e.l&&e.r){for(var f=0,c=0;f<e[t];f++)e[f].e?e[f].q.src=k:(f!=c&&(e[c]=e[f]),c++);aa(e,c);e.r=l;0==c&&(delete W[a][b][d],W[a][b].a--,0==W[a][b].a&&(delete W[a][b],W[a].a--),0==W[a].a&&delete W[a])}},nb=function(a,b,d,e,f){var c=1,b=b[K]||(b[K]=++ia);if(a[b]){a.c--;a=a[b];a.l?a.l++:a.l=
-1;try{for(var g=a[t],i=0;i<g;i++){var m=a[i];m&&!m.e&&(c&=mb(m,f)!==l)}}finally{a.l--,lb(d,e,b,a)}}return Boolean(c)},mb=function(a,b){var d=a[ea](b);if(a.s){var e=a.b;if(V[e]){var f=V[e];if(!f.e){var c=f.src,g=f[C],i=f.q,m=f.w;c.removeEventListener?(c==G||!c.v)&&c.removeEventListener(g,i,m):c.detachEvent&&c.detachEvent(g in Y?Y[g]:Y[g]="on"+g,i);c=c[K]||(c[K]=++ia);i=W[g][m][c];if(X[c]){var o=X[c],v=ua(o,f);0<=v&&(ta(o[t]!=k),L.splice[B](o,v,1));0==o[t]&&delete X[c]}f.e=j;i.r=j;lb(g,m,c,i);delete V[e]}}}return d},
-kb=function(a,b){if(!V[a])return j;var d=V[a],e=d[C],f=W;if(!(e in f))return j;var f=f[e],c,g;if(!db){var i;if(!(i=b))a:{i=["window","event"];for(var m=G;c=i.shift();)if(m[c]!=k)m=m[c];else{i=k;break a}i=m}c=i;i=j in f;m=l in f;if(i){if(0>c[da]||c.returnValue!=h)return j;a:{var o=l;if(0==c[da])try{c.keyCode=-1;break a}catch(v){o=j}if(o||c.returnValue==h)c.returnValue=j}}o=new U;o.i(c,this);c=j;try{if(i){for(var r=[],J=o.currentTarget;J;J=J.parentNode)r[s](J);g=f[j];g.c=g.a;for(var D=r[t]-1;!o.o&&
-0<=D&&g.c;D--)q(o,r[D]),c&=nb(g,r[D],e,j,o);if(m){g=f[l];g.c=g.a;for(D=0;!o.o&&D<r[t]&&g.c;D++)q(o,r[D]),c&=nb(g,r[D],e,l,o)}}else c=mb(d,o)}finally{r&&aa(r,0),o.m()}return c}e=new U(b,this);try{c=mb(d,e)}finally{e.m()}return c};var ob=function(a,b){var d=[];1<arguments[t]&&(d=Array[u][w][B](arguments)[w](1));var e=Xa(n,"th","tct-selectall",a);if(0!=e[t]){var e=e[0],f=0,c=Xa(n,"tbody",k,a);c[t]&&(f=c[0].rows[t]);this.f=bb("input",{type:"checkbox"});e.appendChild(this.f);f?ib(this.f,"click",this.A,l,this):p(this.f,j);this.g=[];this.j=[];this.k=[];e=Xa(n,"input",k,a);for(f=0;c=e[f];f++)"checkbox"==c[C]&&c!=this.f?(this.g[s](c),ib(c,"click",this.z,l,this)):"action"==c[fa]&&(0<=d[z](c.value)?this.k[s](c):this.j[s](c),p(c,j))}};
-F=ob[u];F.g=k;F.d=0;F.f=k;F.j=k;F.k=k;F.A=function(a){for(var b=a[A].checked,d=a=0,e;e=this.g[d];d++)e.checked=b,a+=1;this.d=b?this.g[t]:0;for(d=0;b=this.j[d];d++)p(b,!this.d);for(d=0;b=this.k[d];d++)p(b,1!=a?j:l)};F.z=function(a){this.d+=a[A].checked?1:-1;this.f.checked=this.d==this.g[t];for(var a=0,b;b=this.j[a];a++)p(b,!this.d);for(a=0;b=this.k[a];a++)p(b,1!=this.d?j:l)};var pb=function(){var a=I("kinds")?n.getElementById("kinds"):"kinds";a&&new ob(a);(a=I("backups")?n.getElementById("backups"):"backups")&&new ob(a,"Restore")},Z=["ae","Datastore","Admin","init"],$=G;!(Z[0]in $)&&$.execScript&&$.execScript("var "+Z[0]);for(var qb;Z[t]&&(qb=Z.shift());)!Z[t]&&pb!==h?$[qb]=pb:$=$[qb]?$[qb]:$[qb]={};
+var s="push",t="length",ca="propertyIsEnumerable",u="prototype",v="slice",x="replace",y="split",z="value",A="indexOf",B="target",C="call",da="keyCode",ea="handleEvent",E="type",F="apply",fa="name",G,H=this,J=function(a){var b=typeof a;if("object"==b)if(a){if(a instanceof Array)return"array";if(a instanceof Object)return b;var c=Object[u].toString[C](a);if("[object Window]"==c)return"object";if("[object Array]"==c||"number"==typeof a[t]&&"undefined"!=typeof a.splice&&"undefined"!=typeof a[ca]&&!a[ca]("splice"))return"array";
+if("[object Function]"==c||"undefined"!=typeof a[C]&&"undefined"!=typeof a[ca]&&!a[ca]("call"))return"function"}else return"null";else if("function"==b&&"undefined"==typeof a[C])return"object";return b},ga=function(a){var b=J(a);return"array"==b||"object"==b&&"number"==typeof a[t]},K=function(a){return"string"==typeof a},ha=function(a){var b=typeof a;return"object"==b&&a!=k||"function"==b},L="closure_uid_"+Math.floor(2147483648*Math.random()).toString(36),ia=0,ja=function(a,b){function c(){}c.prototype=
+b[u];a.B=b[u];a.prototype=new c};var ka=function(a){this.stack=Error().stack||"";a&&(this.message=""+a)};ja(ka,Error);ka[u].name="CustomError";var la=function(a,b){for(var c=1;c<arguments[t];c++)var e=(""+arguments[c])[x](/\$/g,"$$$$"),a=a[x](/\%s/,e);return a},ra=function(a,b){if(b)return a[x](ma,"&")[x](na,"<")[x](oa,">")[x](pa,""");if(!qa.test(a))return a;-1!=a[A]("&")&&(a=a[x](ma,"&"));-1!=a[A]("<")&&(a=a[x](na,"<"));-1!=a[A](">")&&(a=a[x](oa,">"));-1!=a[A]('"')&&(a=a[x](pa,"""));return a},ma=/&/g,na=/</g,oa=/>/g,pa=/\"/g,qa=/[&<>\"]/;Math.random();var sa=function(a,b){b.unshift(a);ka[C](this,la[F](k,b));b.shift()};ja(sa,ka);sa[u].name="AssertionError";var ta=function(a,b,c){if(!a){var e=Array[u][v][C](arguments,2),g="Assertion failed";if(b)var g=g+(": "+b),d=e;throw new sa(""+g,d||[]);}return a};var M=Array[u],ua=M[A]?function(a,b,c){ta(a[t]!=k);return M[A][C](a,b,c)}:function(a,b,c){c=c==k?0:0>c?Math.max(0,a[t]+c):c;if(K(a))return!K(b)||1!=b[t]?-1:a[A](b,c);for(;c<a[t];c++)if(c in a&&a[c]===b)return c;return-1},va=M.forEach?function(a,b,c){ta(a[t]!=k);M.forEach[C](a,b,c)}:function(a,b,c){for(var e=a[t],g=K(a)?a[y](""):a,d=0;d<e;d++)d in g&&b[C](c,g[d],d,a)},wa=function(a){return M.concat[F](M,arguments)},xa=function(a){if("array"==J(a))return wa(a);for(var b=[],c=0,e=a[t];c<e;c++)b[c]=a[c];
+return b},ya=function(a,b,c){ta(a[t]!=k);return 2>=arguments[t]?M[v][C](a,b):M[v][C](a,b,c)};var za=function(a,b,c){for(var e in a)b[C](c,a[e],e,a)},Aa="constructor,hasOwnProperty,isPrototypeOf,propertyIsEnumerable,toLocaleString,toString,valueOf".split(","),Ba=function(a,b){for(var c,e,g=1;g<arguments[t];g++){e=arguments[g];for(c in e)a[c]=e[c];for(var d=0;d<Aa[t];d++)c=Aa[d],Object[u].hasOwnProperty[C](e,c)&&(a[c]=e[c])}};var N,Ca,Da,Ea,Fa=function(){return H.navigator?H.navigator.userAgent:k},Ga=function(){return H.navigator};Ea=Da=Ca=N=l;var O;if(O=Fa()){var Ha=Ga();N=0==O[A]("Opera");Ca=!N&&-1!=O[A]("MSIE");(Da=!N&&-1!=O[A]("WebKit"))&&O[A]("Mobile");Ea=!N&&!Da&&"Gecko"==Ha.product}var Ia=N,P=Ca,Q=Ea,R=Da,Ja=Ga(),Ka=Ja&&Ja.platform||"";Ka[A]("Mac");Ka[A]("Win");Ka[A]("Linux");Ga()&&(Ga().appVersion||"")[A]("X11");var La;
+a:{var Ma="",S;if(Ia&&H.opera)var Na=H.opera.version,Ma="function"==typeof Na?Na():Na;else if(Q?S=/rv\:([^\);]+)(\)|;)/:P?S=/MSIE\s+([^\);]+)(\)|;)/:R&&(S=/WebKit\/(\S+)/),S)var Oa=S.exec(Fa()),Ma=Oa?Oa[1]:"";if(P){var Pa,Qa=H.document;Pa=Qa?Qa.documentMode:h;if(Pa>parseFloat(Ma)){La=""+Pa;break a}}La=Ma}
+var Ra=La,Sa={},T=function(a){var b;if(!(b=Sa[a])){var c=0;b=(""+Ra)[x](/^[\s\xa0]+|[\s\xa0]+$/g,"")[y](".");for(var e=(""+a)[x](/^[\s\xa0]+|[\s\xa0]+$/g,"")[y]("."),g=Math.max(b[t],e[t]),d=0;0==c&&d<g;d++){var f=b[d]||"",i=e[d]||"",m=RegExp("(\\d*)(\\D*)","g"),o=RegExp("(\\d*)(\\D*)","g");do{var w=m.exec(f)||["","",""],r=o.exec(i)||["","",""];if(0==w[0][t]&&0==r[0][t])break;var c=0==w[1][t]?0:parseInt(w[1],10),I=0==r[1][t]?0:parseInt(r[1],10),c=(c<I?-1:c>I?1:0)||((0==w[2][t])<(0==r[2][t])?-1:(0==
+w[2][t])>(0==r[2][t])?1:0)||(w[2]<r[2]?-1:w[2]>r[2]?1:0)}while(0==c)}b=Sa[a]=0<=c}return b},Ta={},Ua=function(a){return Ta[a]||(Ta[a]=P&&!!n.documentMode&&n.documentMode>=a)};var Va=!P||Ua(9);!Q&&!P||P&&Ua(9)||Q&&T("1.9.1");P&&T("9");var Wa=function(a,b){var c;c=a.className;c=K(c)&&c.match(/\S+/g)||[];for(var e=ya(arguments,1),g=c[t]+e[t],d=c,f=0;f<e[t];f++)0<=ua(d,e[f])||d[s](e[f]);a.className=c.join(" ");return c[t]==g};var Xa=function(a){return K(a)?n.getElementById(a):a},Ya=function(a,b,c,e){var a=e||a,g=b&&"*"!=b?b.toUpperCase():"";if(a.querySelectorAll&&a.querySelector&&(!R||"CSS1Compat"==n.compatMode||T("528"))&&(g||c))return a.querySelectorAll(g+(c?"."+c:""));if(c&&a.getElementsByClassName){b=a.getElementsByClassName(c);if(g){for(var a={},d=e=0,f;f=b[d];d++)g==f.nodeName&&(a[e++]=f);aa(a,e);return a}return b}b=a.getElementsByTagName(g||"*");if(c){a={};for(d=e=0;f=b[d];d++){var g=f.className,i;if(i="function"==
+typeof g[y])g=g[y](/\s+/),i=0<=ua(g,c);i&&(a[e++]=f)}aa(a,e);return a}return b},$a=function(a,b){za(b,function(b,e){"style"==e?a.style.cssText=b:"class"==e?a.className=b:"for"==e?a.htmlFor=b:e in Za?a.setAttribute(Za[e],b):0==e.lastIndexOf("aria-",0)?a.setAttribute(e,b):a[e]=b})},Za={cellpadding:"cellPadding",cellspacing:"cellSpacing",colspan:"colSpan",rowspan:"rowSpan",valign:"vAlign",height:"height",width:"width",usemap:"useMap",frameborder:"frameBorder",maxlength:"maxLength",type:"type"},bb=function(a,
+b,c){var e=n,g=arguments,d=g[0],f=g[1];if(!Va&&f&&(f[fa]||f[E])){d=["<",d];f[fa]&&d[s](' name="',ra(f[fa]),'"');if(f[E]){d[s](' type="',ra(f[E]),'"');var i={};Ba(i,f);f=i;delete f[E]}d[s](">");d=d.join("")}d=e.createElement(d);f&&(K(f)?d.className=f:"array"==J(f)?Wa[F](k,[d].concat(f)):$a(d,f));2<g[t]&&ab(e,d,g,2);return d},ab=function(a,b,c,e){function g(c){c&&b.appendChild(K(c)?a.createTextNode(c):c)}for(;e<c[t];e++){var d=c[e];ga(d)&&!(ha(d)&&0<d.nodeType)?va(cb(d)?xa(d):d,g):g(d)}},cb=function(a){if(a&&
+"number"==typeof a[t]){if(ha(a))return"function"==typeof a.item||"string"==typeof a.item;if("function"==J(a))return"function"==typeof a.item}return l};var db=function(a){var b=a[E];if(b===h)return k;switch(b.toLowerCase()){case "checkbox":case "radio":return a.checked?a[z]:k;case "select-one":return b=a.selectedIndex,0<=b?a.options[b][z]:k;case "select-multiple":for(var b=[],c,e=0;c=a.options[e];e++)c.selected&&b[s](c[z]);return b[t]?b:k;default:return a[z]!==h?a[z]:k}};var eb=function(a){eb[" "](a);return a};eb[" "]=function(){};!P||Ua(9);var fb=!P||Ua(9);P&&T("8");!R||T("528");Q&&T("1.9b")||P&&T("8")||Ia&&T("9.5")||R&&T("528");Q&&!T("8")||P&&T("9");var gb=function(){};gb[u].u=l;gb[u].m=function(){this.u||(this.u=j,this.n())};gb[u].n=function(){this.D&&hb[F](k,this.D)};var hb=function(a){for(var b=0,c=arguments[t];b<c;++b){var e=arguments[b];ga(e)?hb[F](k,e):e&&"function"==typeof e.m&&e.m()}};var U=function(a,b){this.type=a;ba(this,b);q(this,this[B])};ja(U,gb);U[u].n=function(){delete this[E];delete this[B];delete this.currentTarget};U[u].o=l;U[u].C=j;var V=function(a,b){a&&this.i(a,b)};ja(V,U);G=V[u];ba(G,k);G.relatedTarget=k;G.offsetX=0;G.offsetY=0;G.clientX=0;G.clientY=0;G.screenX=0;G.screenY=0;G.button=0;G.keyCode=0;G.charCode=0;G.ctrlKey=l;G.altKey=l;G.shiftKey=l;G.metaKey=l;
+G.i=function(a,b){var c=this.type=a[E];U[C](this,c);ba(this,a[B]||a.srcElement);q(this,b);var e=a.relatedTarget;if(e){if(Q){var g;a:{try{eb(e.nodeName);g=j;break a}catch(d){}g=l}g||(e=k)}}else"mouseover"==c?e=a.fromElement:"mouseout"==c&&(e=a.toElement);this.relatedTarget=e;this.offsetX=R||a.offsetX!==h?a.offsetX:a.layerX;this.offsetY=R||a.offsetY!==h?a.offsetY:a.layerY;this.clientX=a.clientX!==h?a.clientX:a.pageX;this.clientY=a.clientY!==h?a.clientY:a.pageY;this.screenX=a.screenX||0;this.screenY=
+a.screenY||0;this.button=a.button;this.keyCode=a[da]||0;this.charCode=a.charCode||("keypress"==c?a[da]:0);this.ctrlKey=a.ctrlKey;this.altKey=a.altKey;this.shiftKey=a.shiftKey;this.metaKey=a.metaKey;this.state=a.state;delete this.C;delete this.o};G.n=function(){V.B.n[C](this);ba(this,k);q(this,k);this.relatedTarget=k};var ib=function(){},jb=0;G=ib[u];G.b=0;G.e=l;G.s=l;G.i=function(a,b,c,e,g,d){if("function"==J(a))this.t=j;else if(a&&a[ea]&&"function"==J(a[ea]))this.t=l;else throw Error("Invalid listener argument");this.h=a;this.q=b;this.src=c;this.type=e;this.w=!!g;this.p=d;this.s=l;this.b=++jb;this.e=l};G.handleEvent=function(a){return this.t?this.h[C](this.p||this.src,a):this.h[ea][C](this.h,a)};var W={},X={},Y={},Z={},kb=function(a,b,c,e,g){if(b){if("array"==J(b)){for(var d=0;d<b[t];d++)kb(a,b[d],c,e,g);return k}var e=!!e,f=X;b in f||(f[b]={a:0,c:0});f=f[b];e in f||(f[e]={a:0,c:0},f.a++);var f=f[e],i=a[L]||(a[L]=++ia),m;f.c++;if(f[i]){m=f[i];for(d=0;d<m[t];d++)if(f=m[d],f.h==c&&f.p==g){if(f.e)break;return m[d].b}}else m=f[i]=[],f.a++;d=lb();d.src=a;f=new ib;f.i(c,d,a,b,e,g);c=f.b;d.b=c;m[s](f);W[c]=f;Y[i]||(Y[i]=[]);Y[i][s](f);a.addEventListener?(a==H||!a.v)&&a.addEventListener(b,d,e):a.attachEvent(b in
+Z?Z[b]:Z[b]="on"+b,d);return c}throw Error("Invalid event type");},lb=function(){var a=mb,b=fb?function(c){return a[C](b.src,b.b,c)}:function(c){c=a[C](b.src,b.b,c);if(!c)return c};return b},nb=function(a,b,c,e){if(!e.l&&e.r){for(var g=0,d=0;g<e[t];g++)e[g].e?e[g].q.src=k:(g!=d&&(e[d]=e[g]),d++);aa(e,d);e.r=l;0==d&&(delete X[a][b][c],X[a][b].a--,0==X[a][b].a&&(delete X[a][b],X[a].a--),0==X[a].a&&delete X[a])}},pb=function(a,b,c,e,g){var d=1,b=b[L]||(b[L]=++ia);if(a[b]){a.c--;a=a[b];a.l?a.l++:a.l=
+1;try{for(var f=a[t],i=0;i<f;i++){var m=a[i];m&&!m.e&&(d&=ob(m,g)!==l)}}finally{a.l--,nb(c,e,b,a)}}return Boolean(d)},ob=function(a,b){var c=a[ea](b);if(a.s){var e=a.b;if(W[e]){var g=W[e];if(!g.e){var d=g.src,f=g[E],i=g.q,m=g.w;d.removeEventListener?(d==H||!d.v)&&d.removeEventListener(f,i,m):d.detachEvent&&d.detachEvent(f in Z?Z[f]:Z[f]="on"+f,i);d=d[L]||(d[L]=++ia);i=X[f][m][d];if(Y[d]){var o=Y[d],w=ua(o,g);0<=w&&(ta(o[t]!=k),M.splice[C](o,w,1));0==o[t]&&delete Y[d]}g.e=j;i.r=j;nb(f,m,d,i);delete W[e]}}}return c},
+mb=function(a,b){if(!W[a])return j;var c=W[a],e=c[E],g=X;if(!(e in g))return j;var g=g[e],d,f;if(!fb){var i;if(!(i=b))a:{i=["window","event"];for(var m=H;d=i.shift();)if(m[d]!=k)m=m[d];else{i=k;break a}i=m}d=i;i=j in g;m=l in g;if(i){if(0>d[da]||d.returnValue!=h)return j;a:{var o=l;if(0==d[da])try{d.keyCode=-1;break a}catch(w){o=j}if(o||d.returnValue==h)d.returnValue=j}}o=new V;o.i(d,this);d=j;try{if(i){for(var r=[],I=o.currentTarget;I;I=I.parentNode)r[s](I);f=g[j];f.c=f.a;for(var D=r[t]-1;!o.o&&
+0<=D&&f.c;D--)q(o,r[D]),d&=pb(f,r[D],e,j,o);if(m){f=g[l];f.c=f.a;for(D=0;!o.o&&D<r[t]&&f.c;D++)q(o,r[D]),d&=pb(f,r[D],e,l,o)}}else d=ob(c,o)}finally{r&&aa(r,0),o.m()}return d}e=new V(b,this);try{d=ob(c,e)}finally{e.m()}return d};var qb=function(a,b){var c=[];1<arguments[t]&&(c=Array[u][v][C](arguments)[v](1));var e=Ya(n,"th","tct-selectall",a);if(0!=e[t]){var e=e[0],g=0,d=Ya(n,"tbody",k,a);d[t]&&(g=d[0].rows[t]);this.f=bb("input",{type:"checkbox"});e.appendChild(this.f);g?kb(this.f,"click",this.A,l,this):p(this.f,j);this.g=[];this.j=[];this.k=[];e=Ya(n,"input",k,a);for(g=0;d=e[g];g++)"checkbox"==d[E]&&d!=this.f?(this.g[s](d),kb(d,"click",this.z,l,this)):"action"==d[fa]&&(0<=c[A](d[z])?this.k[s](d):this.j[s](d),p(d,j))}};
+G=qb[u];G.g=k;G.d=0;G.f=k;G.j=k;G.k=k;G.A=function(a){for(var b=a[B].checked,c=a=0,e;e=this.g[c];c++)e.checked=b,a+=1;this.d=b?this.g[t]:0;for(c=0;b=this.j[c];c++)p(b,!this.d);for(c=0;b=this.k[c];c++)p(b,1!=a?j:l)};G.z=function(a){this.d+=a[B].checked?1:-1;this.f.checked=this.d==this.g[t];for(var a=0,b;b=this.j[a];a++)p(b,!this.d);for(a=0;b=this.k[a];a++)p(b,1!=this.d?j:l)};var rb=function(){var a=Xa("kinds");a&&new qb(a);(a=Xa("backups"))&&new qb(a,"Restore");var b=Xa("ae-datastore-admin-destination");b&&kb(b,"change",function(){var a=Xa("gs_bucket_tr"),e="gs"==db(b);a.style.display=e?"":"none"})},sb=["ae","Datastore","Admin","init"],$=H;!(sb[0]in $)&&$.execScript&&$.execScript("var "+sb[0]);for(var tb;sb[t]&&(tb=sb.shift());)!sb[t]&&rb!==h?$[tb]=rb:$=$[tb]?$[tb]:$[tb]={};
diff --git a/google/appengine/ext/datastore_admin/templates/confirm_backup.html b/google/appengine/ext/datastore_admin/templates/confirm_backup.html
index 5e00f0b..c23c3ff 100644
--- a/google/appengine/ext/datastore_admin/templates/confirm_backup.html
+++ b/google/appengine/ext/datastore_admin/templates/confirm_backup.html
@@ -1,7 +1,7 @@
{% extends "base.html" %}
{% block title %}Confirm Backup of {{kind_str|escape}}{% endblock %}
{% block body %}
- <h2>Datastore Admin: Backup to Blobstore</h2>
+ <h2>Datastore Admin: Backup to Blobstore or Google Cloud Storage</h2>
{% if kind_list %}
{% comment %}
size_total represents the total size of the figures for which we have
@@ -64,6 +64,24 @@
{% endif %}
</p>
+ <table>
+ <tr>
+ <td>
+ Backup storage destination:
+ <select name="destination_type" id="ae-datastore-admin-destination">
+ <option value="blobstore" selected="selected">Blobstore</option>
+ <option value="gs">Google Cloud Storage</option>
+ </select>
+ </td>
+ </tr>
+ <tr style="display:none;" id="gs_bucket_tr">
+ <td>
+ Google Cloud Storage bucket name:
+ <input type="text" id="gs_bucket_name" name="gs_bucket_name" value="" />
+ </td>
+ </tr>
+ </table>
+
<table style="margin-top: 1em;"><tr>
<td style="padding-right: 0.5em;">
<input class="goog-button" type="submit" name="backup"
diff --git a/google/appengine/ext/datastore_admin/templates/confirm_delete_backup.html b/google/appengine/ext/datastore_admin/templates/confirm_delete_backup.html
index 4b2740c..788ef00 100644
--- a/google/appengine/ext/datastore_admin/templates/confirm_delete_backup.html
+++ b/google/appengine/ext/datastore_admin/templates/confirm_delete_backup.html
@@ -9,6 +9,9 @@
<li>{{ backup_name }}</li>
{% endfor %}
</ul>
+ {% if gs_warning %}
+ <p>Warning, backup files stored in Google Cloud Storage can only be deleted manually and will not be deleted by this operation.</p>
+ {% endif %}
<form action="{{base_path}}/{{form_target}}" method="post" style="width:39.39em;">
{% for backup_id in backup_ids %}
<input type="hidden" name="backup_id" value="{{backup_id}}">
diff --git a/google/appengine/ext/db/__init__.py b/google/appengine/ext/db/__init__.py
index 803e9f0..ad9ea7a 100755
--- a/google/appengine/ext/db/__init__.py
+++ b/google/appengine/ext/db/__init__.py
@@ -3860,6 +3860,7 @@
transactional = datastore.Transactional
+non_transactional = datastore.NonTransactional
create_config = datastore.CreateConfig
diff --git a/google/appengine/ext/db/stats.py b/google/appengine/ext/db/stats.py
index fd1eb33..195dd7e 100755
--- a/google/appengine/ext/db/stats.py
+++ b/google/appengine/ext/db/stats.py
@@ -39,10 +39,12 @@
class BaseStatistic(db.Model):
"""Base Statistic Model class.
- The 'bytes' attribute represents the total number of bytes taken up in the
- datastore for the statistic instance. The 'count' attribute is the
- total number of occurrences of the statistic in the datastore. The
- 'timestamp' is when the statistic instance was written to the datastore.
+ Attributes:
+ bytes: the total number of bytes taken up in the datastore for the
+ statistic instance.
+ count: attribute is the total number of occurrences of the statistic
+ in the datastore.
+ timestamp: the time the statistic instance was written to the datastore.
"""
STORED_KIND_NAME = '__BaseStatistic__'
@@ -65,8 +67,10 @@
class BaseKindStatistic(BaseStatistic):
"""Base Statistic Model class for stats associated with kinds.
- The 'kind_name' attribute represents the name of the kind associated with the
- statistic instance.
+ Attributes:
+ kind_name: the name of the kind associated with the statistic instance.
+ entity_bytes: the number of bytes taken up to store the statistic
+ in the datastore minus the cost of storing indices.
"""
STORED_KIND_NAME = '__BaseKindStatistic__'
@@ -75,15 +79,45 @@
kind_name = db.StringProperty()
+
+
+ entity_bytes = db.IntegerProperty(default=0L)
+
+
class GlobalStat(BaseStatistic):
"""An aggregate of all entities across the entire application.
This statistic only has a single instance in the datastore that contains the
total number of entities stored and the total number of bytes they take up.
+
+ Attributes:
+ entity_bytes: the number of bytes taken up to store the statistic
+ in the datastore minus the cost of storing indices.
+ builtin_index_bytes: the number of bytes taken up to store builtin-in
+ index entries
+ builtin_index_count: the number of built-in index entries.
+ composite_index_bytes: the number of bytes taken up to store composite
+ index entries
+ composite_index_count: the number of composite index entries.
"""
STORED_KIND_NAME = '__Stat_Total__'
+ entity_bytes = db.IntegerProperty(default=0L)
+
+
+ builtin_index_bytes = db.IntegerProperty(default=0L)
+
+
+ builtin_index_count = db.IntegerProperty(default=0L)
+
+
+ composite_index_bytes = db.IntegerProperty(default=0L)
+
+
+ composite_index_count = db.IntegerProperty(default=0L)
+
+
class NamespaceStat(BaseStatistic):
"""An aggregate of all entities across an entire namespace.
@@ -91,6 +125,17 @@
represented namespace. NamespaceStat entities will only be found
in the namespace "" (empty string). It contains the total
number of entities stored and the total number of bytes they take up.
+
+ Attributes:
+ subject_namespace: the namespace associated with the statistic instance.
+ entity_bytes: the number of bytes taken up to store the statistic
+ in the datastore minus the cost of storing indices.
+ builtin_index_bytes: the number of bytes taken up to store builtin-in
+ index entries
+ builtin_index_count: the number of built-in index entries.
+ composite_index_bytes: the number of bytes taken up to store composite
+ index entries
+ composite_index_count: the number of composite index entries.
"""
STORED_KIND_NAME = '__Stat_Namespace__'
@@ -98,15 +143,50 @@
subject_namespace = db.StringProperty()
+ entity_bytes = db.IntegerProperty(default=0L)
+
+
+ builtin_index_bytes = db.IntegerProperty(default=0L)
+
+
+ builtin_index_count = db.IntegerProperty(default=0L)
+
+
+ composite_index_bytes = db.IntegerProperty(default=0L)
+
+
+ composite_index_count = db.IntegerProperty(default=0L)
+
+
class KindStat(BaseKindStatistic):
"""An aggregate of all entities at the granularity of their Kind.
There is an instance of the KindStat for every Kind that is in the
application's datastore. This stat contains per-Kind statistics.
+
+ Attributes:
+ builtin_index_bytes: the number of bytes taken up to store builtin-in
+ index entries
+ builtin_index_count: the number of built-in index entries.
+ composite_index_bytes: the number of bytes taken up to store composite
+ index entries
+ composite_index_count: the number of composite index entries.
"""
STORED_KIND_NAME = '__Stat_Kind__'
+ builtin_index_bytes = db.IntegerProperty(default=0L)
+
+
+ builtin_index_count = db.IntegerProperty(default=0L)
+
+
+ composite_index_bytes = db.IntegerProperty(default=0L)
+
+
+ composite_index_count = db.IntegerProperty(default=0L)
+
+
class KindRootEntityStat(BaseKindStatistic):
"""Statistics of the number of root entities in the datastore by Kind.
@@ -133,6 +213,14 @@
There is an instance of the PropertyTypeStat for every property type
(google.appengine.api.datastore_types._PROPERTY_TYPES) in use by the
application in its datastore.
+
+ Attributes:
+ property_type: the property type associated with the statistic instance.
+ entity_bytes: the number of bytes taken up to store the statistic
+ in the datastore minus the cost of storing indices.
+ builtin_index_bytes: the number of bytes taken up to store builtin-in
+ index entries
+ builtin_index_count: the number of built-in index entries.
"""
STORED_KIND_NAME = '__Stat_PropertyType__'
@@ -140,11 +228,26 @@
property_type = db.StringProperty()
+ entity_bytes = db.IntegerProperty(default=0L)
+
+
+ builtin_index_bytes = db.IntegerProperty(default=0L)
+
+
+ builtin_index_count = db.IntegerProperty(default=0L)
+
+
class KindPropertyTypeStat(BaseKindStatistic):
"""Statistics on (kind, property_type) tuples in the app's datastore.
There is an instance of the KindPropertyTypeStat for every
(kind, property_type) tuple in the application's datastore.
+
+ Attributes:
+ property_type: the property type associated with the statistic instance.
+ builtin_index_bytes: the number of bytes taken up to store builtin-in
+ index entries
+ builtin_index_count: the number of built-in index entries.
"""
STORED_KIND_NAME = '__Stat_PropertyType_Kind__'
@@ -152,11 +255,24 @@
property_type = db.StringProperty()
+ builtin_index_bytes = db.IntegerProperty(default=0L)
+
+
+ builtin_index_count = db.IntegerProperty(default=0L)
+
+
class KindPropertyNameStat(BaseKindStatistic):
"""Statistics on (kind, property_name) tuples in the app's datastore.
There is an instance of the KindPropertyNameStat for every
(kind, property_name) tuple in the application's datastore.
+
+ Attributes:
+ property_name: the name of the property associated with the statistic
+ instance.
+ builtin_index_bytes: the number of bytes taken up to store builtin-in
+ index entries
+ builtin_index_count: the number of built-in index entries.
"""
STORED_KIND_NAME = '__Stat_PropertyName_Kind__'
@@ -164,11 +280,25 @@
property_name = db.StringProperty()
+ builtin_index_bytes = db.IntegerProperty(default=0L)
+
+
+ builtin_index_count = db.IntegerProperty(default=0L)
+
+
class KindPropertyNamePropertyTypeStat(BaseKindStatistic):
"""Statistic on (kind, property_name, property_type) tuples in the datastore.
There is an instance of the KindPropertyNamePropertyTypeStat for every
(kind, property_name, property_type) tuple in the application's datastore.
+
+ Attributes:
+ property_type: the property type associated with the statistic instance.
+ property_name: the name of the property associated with the statistic
+ instance.
+ builtin_index_bytes: the number of bytes taken up to store builtin-in
+ index entries
+ builtin_index_count: the number of built-in index entries.
"""
STORED_KIND_NAME = '__Stat_PropertyType_PropertyName_Kind__'
@@ -179,6 +309,32 @@
property_name = db.StringProperty()
+ builtin_index_bytes = db.IntegerProperty(default=0L)
+
+
+ builtin_index_count = db.IntegerProperty(default=0L)
+
+
+class KindCompositeIndexStat(BaseStatistic):
+ """Statistic on (kind, composite_index_id) tuples in the datastore.
+
+ There is an instance of the KindCompositeIndexStat for every unique
+ (kind, composite_index_id) tuple in the application's datastore indexes.
+
+ Attributes:
+ index_id: the id of the composite index associated with the statistic
+ instance.
+ kind_name: the name of the kind associated with the statistic instance.
+ """
+ STORED_KIND_NAME = '__Stat_Kind_CompositeIndex__'
+
+
+ index_id = db.IntegerProperty()
+
+
+ kind_name = db.StringProperty()
+
+
@@ -258,6 +414,15 @@
STORED_KIND_NAME = '__Stat_Ns_PropertyType_PropertyName_Kind__'
+class NamespaceKindCompositeIndexStat(KindCompositeIndexStat):
+ """KindCompositeIndexStat equivalent for a specific namespace.
+
+ These may be found in each specific namespace and represent stats for
+ that particular namespace.
+ """
+ STORED_KIND_NAME = '__Stat_Ns_Kind_CompositeIndex__'
+
+
_DATASTORE_STATS_CLASSES_BY_KIND = {
@@ -271,6 +436,7 @@
KindPropertyNameStat.STORED_KIND_NAME: KindPropertyNameStat,
KindPropertyNamePropertyTypeStat.STORED_KIND_NAME:
KindPropertyNamePropertyTypeStat,
+ KindCompositeIndexStat.STORED_KIND_NAME: KindCompositeIndexStat,
NamespaceGlobalStat.STORED_KIND_NAME: NamespaceGlobalStat,
NamespaceKindStat.STORED_KIND_NAME: NamespaceKindStat,
NamespaceKindRootEntityStat.STORED_KIND_NAME: NamespaceKindRootEntityStat,
@@ -282,5 +448,7 @@
NamespaceKindPropertyNameStat.STORED_KIND_NAME:
NamespaceKindPropertyNameStat,
NamespaceKindPropertyNamePropertyTypeStat.STORED_KIND_NAME:
- NamespaceKindPropertyNamePropertyTypeStat}
-
+ NamespaceKindPropertyNamePropertyTypeStat,
+ NamespaceKindCompositeIndexStat.STORED_KIND_NAME:
+ NamespaceKindCompositeIndexStat,
+ }
diff --git a/google/appengine/ext/ereporter/ereporter.py b/google/appengine/ext/ereporter/ereporter.py
index b881082..6e38880 100755
--- a/google/appengine/ext/ereporter/ereporter.py
+++ b/google/appengine/ext/ereporter/ereporter.py
@@ -62,7 +62,7 @@
this to your index.yaml:
indexes:
- - kind: __google_ExceptionRecord
+ - kind: ExceptionRecord
properties:
- name: date
- name: major_version
diff --git a/google/appengine/ext/go/__init__.py b/google/appengine/ext/go/__init__.py
index 3a3f72f..f032323 100644
--- a/google/appengine/ext/go/__init__.py
+++ b/google/appengine/ext/go/__init__.py
@@ -49,6 +49,7 @@
import getpass
import logging
import os
+import random
import re
import shutil
import signal
@@ -68,10 +69,9 @@
GAB_WORK_DIR = None
GO_APP = None
GO_APP_NAME = '_go_app'
+GO_HTTP_PORT = 0
+GO_API_PORT = 0
RAPI_HANDLER = None
-SOCKET_HTTP = os.path.join(tempfile.gettempdir(),
- 'dev_appserver_%s_socket_http')
-SOCKET_API = os.path.join(tempfile.gettempdir(), 'dev_appserver_%s_socket_api')
HEALTH_CHECK_PATH = '/_appengine_delegate_health_check'
INTERNAL_SERVER_ERROR = ('Status: 500 Internal Server Error\r\n' +
'Content-Type: text/plain\r\n\r\nInternal Server Error')
@@ -98,6 +98,20 @@
APP_CONFIG = None
+def pick_unused_port():
+ for _ in range(10):
+ port = int(random.uniform(32768, 60000))
+ s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ try:
+ s.bind(('127.0.0.1', port))
+ return port
+ except socket.error:
+ logging.info('could not bind to port %d', port)
+ finally:
+ s.close()
+ raise dev_appserver.ExecuteError('could not pick an unused port')
+
+
def gab_work_dir():
base = os.getenv('XDG_CACHE_HOME')
if not base:
@@ -124,18 +138,13 @@
shutil.rmtree(GAB_WORK_DIR)
except:
pass
- for fn in [SOCKET_HTTP, SOCKET_API]:
- try:
- os.remove(fn)
- except:
- pass
class DelegateClient(asyncore.dispatcher):
def __init__(self, http_req):
asyncore.dispatcher.__init__(self)
- self.create_socket(socket.AF_UNIX, socket.SOCK_STREAM)
- self.connect(SOCKET_HTTP)
+ self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
+ self.connect(('127.0.0.1', GO_HTTP_PORT))
self.buffer = http_req
self.result = ''
self.closed = False
@@ -161,12 +170,8 @@
class DelegateServer(asyncore.dispatcher):
def __init__(self):
asyncore.dispatcher.__init__(self)
- self.create_socket(socket.AF_UNIX, socket.SOCK_STREAM)
- try:
- os.remove(SOCKET_API)
- except OSError:
- pass
- self.bind(SOCKET_API)
+ self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
+ self.bind(('127.0.0.1', GO_API_PORT))
self.listen(5)
def handle_accept(self):
@@ -289,8 +294,8 @@
if proc.poll():
raise dev_appserver.ExecuteError('Go app failed during init', tee.buf)
try:
- s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
- s.connect(SOCKET_HTTP)
+ s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ s.connect(('127.0.0.1', GO_HTTP_PORT))
s.send('HEAD %s HTTP/1.0\r\n\r\n' % HEALTH_CHECK_PATH)
s.close()
return
@@ -358,6 +363,7 @@
def cleanup(self):
if self.proc:
os.kill(self.proc.pid, signal.SIGTERM)
+ self.proc = None
def make_and_run(self, env):
app_files = find_app_files(self.root_path)
@@ -389,7 +395,8 @@
if not self.proc or self.proc.poll() is not None:
- logging.info('running ' + GO_APP_NAME)
+ logging.info('running %s, HTTP port = %d, API port = %d',
+ GO_APP_NAME, GO_HTTP_PORT, GO_API_PORT)
limited_env = {
'PWD': self.root_path,
@@ -400,8 +407,8 @@
limited_env[k] = v
self.proc_start = app_mtime
self.proc = subprocess.Popen([bin_name,
- '-addr_http', 'unix:' + SOCKET_HTTP,
- '-addr_api', 'unix:' + SOCKET_API],
+ '-addr_http', 'tcp:127.0.0.1:%d' % GO_HTTP_PORT,
+ '-addr_api', 'tcp:127.0.0.1:%d' % GO_API_PORT],
stderr=subprocess.PIPE,
cwd=self.root_path, env=limited_env)
tee = Tee(self.proc.stderr, sys.stderr)
@@ -431,15 +438,25 @@
raise dev_appserver.CompileError(p.stdout.read() + '\n' + p.stderr.read())
+OldSigTermHandler = None
+
+def SigTermHandler(signum, frame):
+ if GO_APP:
+ GO_APP.cleanup()
+ if OldSigTermHandler:
+ OldSigTermHandler(signum, frame)
+
def execute_go_cgi(root_path, handler_path, cgi_path, env, infile, outfile):
- global RAPI_HANDLER, GAB_WORK_DIR, SOCKET_HTTP, SOCKET_API, GO_APP
+ global RAPI_HANDLER, GAB_WORK_DIR, GO_APP, GO_HTTP_PORT, GO_API_PORT
+ global OldSigTermHandler
if not RAPI_HANDLER:
user_port = '%s_%s' % (getpass.getuser(), env['SERVER_PORT'])
GAB_WORK_DIR = gab_work_dir() % user_port
- SOCKET_HTTP = SOCKET_HTTP % user_port
- SOCKET_API = SOCKET_API % user_port
+ GO_HTTP_PORT = pick_unused_port()
+ GO_API_PORT = pick_unused_port()
atexit.register(cleanup)
+ OldSigTermHandler = signal.signal(signal.SIGTERM, SigTermHandler)
DelegateServer()
RAPI_HANDLER = handler.ApiCallHandler()
GO_APP = GoApp(root_path)
diff --git a/google/appengine/ext/gql/__init__.py b/google/appengine/ext/gql/__init__.py
index e438010..4a5a14e 100755
--- a/google/appengine/ext/gql/__init__.py
+++ b/google/appengine/ext/gql/__init__.py
@@ -93,7 +93,7 @@
"""A GQL interface to the datastore.
GQL is a SQL-like language which supports more object-like semantics
- in a langauge that is familiar to SQL users. The language supported by
+ in a language that is familiar to SQL users. The language supported by
GQL will change over time, but will start off with fairly simple
semantics.
@@ -108,6 +108,7 @@
[LIMIT [<offset>,]<count>]
[OFFSET <offset>]
[HINT (ORDER_FIRST | HINT FILTER_FIRST | HINT ANCESTOR_FIRST)]
+ [;]
<condition> := <property> {< | <= | > | >= | = | != | IN} <value>
<condition> := <property> {< | <= | > | >= | = | != | IN} CAST(<value>)
@@ -815,7 +816,7 @@
@property
def _entity(self):
- logging.warning('GQL._entity is deprecated. Please use GQL.kind().')
+ logging.error('GQL._entity is deprecated. Please use GQL.kind().')
return self._kind
@@ -916,7 +917,7 @@
return None
def __AcceptTerminal(self):
- """Only accept an empty string.
+ """Accept either a single semi-colon or an empty string.
Returns:
True
@@ -924,6 +925,9 @@
Raises:
BadQueryError if there are unconsumed symbols in the query.
"""
+
+ self.__Accept(';')
+
if self.__next_symbol < len(self.__symbols):
self.__Error('Expected no additional symbols')
return True
diff --git a/google/appengine/ext/mapreduce/output_writers.py b/google/appengine/ext/mapreduce/output_writers.py
index 0b6e756..76c0587 100755
--- a/google/appengine/ext/mapreduce/output_writers.py
+++ b/google/appengine/ext/mapreduce/output_writers.py
@@ -39,7 +39,11 @@
"BlobstoreOutputWriter",
"BlobstoreOutputWriterBase",
"BlobstoreRecordsOutputWriter",
+ "FileOutputWriter",
+ "FileOutputWriterBase",
+ "FileRecordsOutputWriter",
"KeyValueBlobstoreOutputWriter",
+ "KeyValueFileOutputWriter",
"COUNTER_IO_WRITE_BYTES",
"COUNTER_IO_WRITE_MSEC",
"OutputWriter",
@@ -47,8 +51,8 @@
]
import gc
+import itertools
import logging
-import string
import time
from google.appengine.api import files
@@ -176,8 +180,7 @@
list of filenames this writer writes to or None if writer
doesn't write to a file.
"""
- raise NotImplementedError("get_filenames() not implemented in %s" %
- self.__class__)
+ raise NotImplementedError("get_filenames() not implemented in %s" % cls)
_FILES_API_FLUSH_SIZE = 128*1024
@@ -349,9 +352,7 @@
self.flush()
-def _get_output_sharding(
- mapreduce_state=None,
- mapper_spec=None):
+def _get_output_sharding(mapreduce_state=None, mapper_spec=None):
"""Get output sharding parameter value from mapreduce state or mapper spec.
At least one of the parameters should not be None.
@@ -361,17 +362,17 @@
mapper_spec: mapper specification as model.MapperSpec
"""
if mapper_spec:
- return string.lower(mapper_spec.params.get(
- BlobstoreOutputWriterBase.OUTPUT_SHARDING_PARAM,
- BlobstoreOutputWriterBase.OUTPUT_SHARDING_NONE))
+ return mapper_spec.params.get(
+ FileOutputWriterBase.OUTPUT_SHARDING_PARAM,
+ FileOutputWriterBase.OUTPUT_SHARDING_NONE).lower()
if mapreduce_state:
mapper_spec = mapreduce_state.mapreduce_spec.mapper
return _get_output_sharding(mapper_spec=mapper_spec)
raise errors.Error("Neither mapreduce_state nor mapper_spec specified.")
-class BlobstoreOutputWriterBase(OutputWriter):
- """Base class for all blobstore output writers."""
+class FileOutputWriterBase(OutputWriter):
+ """Base class for all file output writers."""
OUTPUT_SHARDING_PARAM = "output_sharding"
@@ -382,20 +383,35 @@
OUTPUT_SHARDING_INPUT_SHARDS = "input"
+ OUTPUT_FILESYSTEM_PARAM = "filesystem"
+
+ GS_BUCKET_NAME_PARAM = "gs_bucket_name"
+
class _State(object):
"""Writer state. Stored in MapreduceState.
State list all files which were created for the job.
"""
- def __init__(self, filenames):
+
+ def __init__(self, filenames, request_filenames):
+ """State initializer.
+
+ Args:
+ filenames: writable or finalized filenames as returned by the files api.
+ request_filenames: filenames as given to the files create api.
+ """
self.filenames = filenames
+ self.request_filenames = request_filenames
def to_json(self):
- return {"filenames": self.filenames}
+ return {
+ "filenames": self.filenames,
+ "request_filenames": self.request_filenames
+ }
@classmethod
def from_json(cls, json):
- return cls(json["filenames"])
+ return cls(json["filenames"], json["request_filenames"])
def __init__(self, filename):
self._filename = filename
@@ -416,6 +432,20 @@
raise errors.BadWriterParamsError(
"Invalid output_sharding value: %s" % output_sharding)
+ filesystem = cls._get_filesystem(mapper_spec)
+ if filesystem not in files.FILESYSTEMS:
+ raise errors.BadWriterParamsError(
+ "Filesystem '%s' is not supported. Should be one of %s" %
+ (filesystem, files.FILESYSTEMS))
+ if filesystem == files.GS_FILESYSTEM:
+ if not cls.GS_BUCKET_NAME_PARAM in mapper_spec.params:
+ raise errors.BadWriterParamsError(
+ "bucket_name is required for Google store filesystem")
+ else:
+ if mapper_spec.params.get(cls.GS_BUCKET_NAME_PARAM) is not None:
+ raise errors.BadWriterParamsError(
+ "bucket_name can only be provided for Google store filesystem")
+
@classmethod
def init_job(cls, mapreduce_state):
"""Initialize job-level writer state.
@@ -427,24 +457,55 @@
output_sharding = _get_output_sharding(mapreduce_state=mapreduce_state)
mapper_spec = mapreduce_state.mapreduce_spec.mapper
mime_type = mapper_spec.params.get("mime_type", "application/octet-stream")
+ filesystem = cls._get_filesystem(mapper_spec=mapper_spec)
+ bucket = mapper_spec.params.get(cls.GS_BUCKET_NAME_PARAM)
- number_of_files = 1
if output_sharding == cls.OUTPUT_SHARDING_INPUT_SHARDS:
- mapper_spec = mapreduce_state.mapreduce_spec.mapper
- number_of_files = mapper_spec.shard_count
+ number_of_files = mapreduce_state.mapreduce_spec.mapper.shard_count
+ else:
+ number_of_files = 1
filenames = []
+ request_filenames = []
for i in range(number_of_files):
- blob_file_name = (mapreduce_state.mapreduce_spec.name +
- "-" + mapreduce_state.mapreduce_spec.mapreduce_id +
- "-output")
+ filename = (mapreduce_state.mapreduce_spec.name + "-" +
+ mapreduce_state.mapreduce_spec.mapreduce_id + "-output")
if number_of_files > 1:
- blob_file_name += "-" + str(i)
- filenames.append(files.blobstore.create(
- mime_type=mime_type,
- _blobinfo_uploaded_filename=blob_file_name))
+ filename += "-" + str(i)
+ if bucket is not None:
+ filename = "%s/%s" % (bucket, filename)
+ request_filenames.append(filename)
+ filenames.append(cls._create_file(filesystem, filename, mime_type))
- mapreduce_state.writer_state = cls._State(filenames).to_json()
+ mapreduce_state.writer_state = cls._State(
+ filenames, request_filenames).to_json()
+
+ @classmethod
+ def _get_filesystem(cls, mapper_spec):
+ return mapper_spec.params.get(cls.OUTPUT_FILESYSTEM_PARAM, "").lower()
+
+ @classmethod
+ def _create_file(cls, filesystem, filename, mime_type, **kwargs):
+ """Creates a file and returns its created filename."""
+ if filesystem == files.BLOBSTORE_FILESYSTEM:
+ return files.blobstore.create(mime_type, filename)
+ elif filesystem == files.GS_FILESYSTEM:
+ return files.gs.create("/gs/%s" % filename, mime_type, **kwargs)
+ else:
+ raise errors.BadWriterParamsError(
+ "Filesystem '%s' is not supported" % filesystem)
+
+ @classmethod
+ def _get_finalized_filename(cls, fs, create_filename, request_filename):
+ """Returns the finalized filename for the created filename."""
+ if fs == "blobstore":
+ return files.blobstore.get_file_name(
+ files.blobstore.get_blob_key(create_filename))
+ elif fs == "gs":
+ return "/gs/" + request_filename
+ else:
+ raise errors.BadWriterParamsError(
+ "Filesystem '%s' is not supported" % fs)
@classmethod
def finalize_job(cls, mapreduce_state):
@@ -454,20 +515,20 @@
mapreduce_state: an instance of model.MapreduceState describing current
job.
"""
- state = cls._State.from_json(
- mapreduce_state.writer_state)
-
+ state = cls._State.from_json(mapreduce_state.writer_state)
output_sharding = _get_output_sharding(mapreduce_state=mapreduce_state)
-
+ filesystem = cls._get_filesystem(mapreduce_state.mapreduce_spec.mapper)
finalized_filenames = []
- for filename in state.filenames:
+ for create_filename, request_filename in itertools.izip(
+ state.filenames, state.request_filenames):
if output_sharding != cls.OUTPUT_SHARDING_INPUT_SHARDS:
- files.finalize(filename)
- finalized_filenames.append(
- files.blobstore.get_file_name(
- files.blobstore.get_blob_key(filename)))
+ files.finalize(create_filename)
+ finalized_filenames.append(cls._get_finalized_filename(filesystem,
+ create_filename,
+ request_filename))
state.filenames = finalized_filenames
+ state.request_filenames = []
mapreduce_state.writer_state = state.to_json()
@classmethod
@@ -475,7 +536,7 @@
"""Creates an instance of the OutputWriter for the given json state.
Args:
- state: The OutputWriter state as a dict-like object.
+ state: The OutputWriter state as a json object (dict like).
Returns:
An instance of the OutputWriter configured using the values of json.
@@ -504,8 +565,7 @@
if output_sharding == cls.OUTPUT_SHARDING_INPUT_SHARDS:
file_index = shard_number
- state = cls._State.from_json(
- mapreduce_state.writer_state)
+ state = cls._State.from_json(mapreduce_state.writer_state)
return cls(state.filenames[file_index])
def finalize(self, ctx, shard_number):
@@ -533,13 +593,12 @@
Returns:
list of filenames this writer writes to.
"""
- state = cls._State.from_json(
- mapreduce_state.writer_state)
+ state = cls._State.from_json(mapreduce_state.writer_state)
return state.filenames
-class BlobstoreOutputWriter(BlobstoreOutputWriterBase):
- """An implementation of OutputWriter which outputs data into blobstore."""
+class FileOutputWriter(FileOutputWriterBase):
+ """An implementation of OutputWriter which outputs data into file."""
def write(self, data, ctx):
"""Write data.
@@ -553,8 +612,8 @@
ctx.get_pool("file_pool").append(self._filename, str(data))
-class BlobstoreRecordsOutputWriter(BlobstoreOutputWriterBase):
- """An OutputWriter which outputs data into records format."""
+class FileRecordsOutputWriter(FileOutputWriterBase):
+ """A File OutputWriter which outputs data using leveldb log format."""
@classmethod
def validate(cls, mapper_spec):
@@ -568,7 +627,7 @@
"output_sharding should not be specified for %s" % cls.__name__)
mapper_spec.params[cls.OUTPUT_SHARDING_PARAM] = (
cls.OUTPUT_SHARDING_INPUT_SHARDS)
- super(BlobstoreRecordsOutputWriter, cls).validate(mapper_spec)
+ super(FileRecordsOutputWriter, cls).validate(mapper_spec)
def write(self, data, ctx):
"""Write data.
@@ -585,8 +644,8 @@
ctx.get_pool("records_pool").append(str(data))
-class KeyValueBlobstoreOutputWriter(BlobstoreRecordsOutputWriter):
- """Output writer for KeyValue records files in blobstore."""
+class KeyValueFileOutputWriter(FileRecordsOutputWriter):
+ """A file output writer for KeyValue records."""
def write(self, data, ctx):
if len(data) != 2:
@@ -603,5 +662,26 @@
proto = file_service_pb.KeyValue()
proto.set_key(key)
proto.set_value(value)
- BlobstoreRecordsOutputWriter.write(self, proto.Encode(), ctx)
+ FileRecordsOutputWriter.write(self, proto.Encode(), ctx)
+
+class BlobstoreOutputWriterBase(FileOutputWriterBase):
+ """A base class of OutputWriter which outputs data into blobstore."""
+
+ @classmethod
+ def _get_filesystem(cls, mapper_spec):
+ return "blobstore"
+
+
+class BlobstoreOutputWriter(FileOutputWriter, BlobstoreOutputWriterBase):
+ """An implementation of OutputWriter which outputs data into blobstore."""
+
+
+class BlobstoreRecordsOutputWriter(FileRecordsOutputWriter,
+ BlobstoreOutputWriterBase):
+ """An OutputWriter which outputs data into records format."""
+
+
+class KeyValueBlobstoreOutputWriter(KeyValueFileOutputWriter,
+ BlobstoreOutputWriterBase):
+ """Output writer for KeyValue records files in blobstore."""
diff --git a/google/appengine/ext/ndb/__init__.py b/google/appengine/ext/ndb/__init__.py
index 5d3602e..c0ec115 100644
--- a/google/appengine/ext/ndb/__init__.py
+++ b/google/appengine/ext/ndb/__init__.py
@@ -1,6 +1,6 @@
"""NDB -- A new datastore API for the Google App Engine Python runtime."""
-__version__ = '0.9.7+'
+__version__ = '1.0'
__all__ = []
diff --git a/google/appengine/ext/ndb/context.py b/google/appengine/ext/ndb/context.py
index d2325a0..2880cb6 100644
--- a/google/appengine/ext/ndb/context.py
+++ b/google/appengine/ext/ndb/context.py
@@ -19,7 +19,7 @@
from . import eventloop
from . import utils
-__all__ = ['Context', 'ContextOptions', 'AutoBatcher',
+__all__ = ['Context', 'ContextOptions', 'TransactionOptions', 'AutoBatcher',
'EVENTUAL_CONSISTENCY',
]
@@ -31,7 +31,7 @@
EVENTUAL_CONSISTENCY = datastore_rpc.Configuration.EVENTUAL_CONSISTENCY
-class ContextOptions(datastore_rpc.TransactionOptions):
+class ContextOptions(datastore_rpc.Configuration):
"""Configuration options that may be passed along with get/put/delete."""
@datastore_rpc.ConfigOption
@@ -70,24 +70,30 @@
return value
+class TransactionOptions(ContextOptions, datastore_rpc.TransactionOptions):
+ """Support both context options and transaction options."""
+
+
+
# options and config can be used interchangeably.
_OPTION_TRANSLATIONS = {
'options': 'config',
}
-def _make_ctx_options(ctx_options):
+def _make_ctx_options(ctx_options, config_cls=ContextOptions):
"""Helper to construct a ContextOptions object from keyword arguments.
Args:
- ctx_options: a dict of keyword arguments.
+ ctx_options: A dict of keyword arguments.
+ config_cls: Optional Configuration class to use, default ContextOptions.
Note that either 'options' or 'config' can be used to pass another
- ContextOptions object, but not both. If another ContextOptions
+ Configuration object, but not both. If another Configuration
object is given it provides default values.
Returns:
- A ContextOptions object, or None if ctx_options is empty.
+ A Configuration object, or None if ctx_options is empty.
"""
if not ctx_options:
return None
@@ -98,7 +104,7 @@
raise ValueError('Cannot specify %s and %s at the same time' %
(key, translation))
ctx_options[translation] = ctx_options.pop(key)
- return ContextOptions(**ctx_options)
+ return config_cls(**ctx_options)
class AutoBatcher(object):
@@ -172,13 +178,15 @@
class Context(object):
- def __init__(self, conn=None, auto_batcher_class=AutoBatcher, config=None):
+ def __init__(self, conn=None, auto_batcher_class=AutoBatcher, config=None,
+ parent_context=None):
# NOTE: If conn is not None, config is only used to get the
# auto-batcher limits.
if conn is None:
conn = model.make_connection(config)
self._conn = conn
self._auto_batcher_class = auto_batcher_class
+ self._parent_context = parent_context # For transaction nesting.
# Get the get/put/delete limits (defaults 1000, 500, 500).
# Note that the explicit config passed in overrides the config
# attached to the connection, if it was passed in.
@@ -740,24 +748,9 @@
batch, i, ent = yield inq.getq()
except EOFError:
break
- if isinstance(ent, model.Key):
- pass # It was a keys-only query and ent is really a Key.
- else:
- key = ent._key
- if self._use_cache(key, options):
- # Update the cache. If this key was already in the
- # cache, update the cached entity in-place and return
- # the cached entity, to maintain the invariant that
- # there is only one copy of each cached entity.
- cached_ent = self._cache.get(key)
- if cached_ent is not None and cached_ent.key == key:
- # TODO: Do the in-place update more subtly, so that
- # mutable property values (e.g. repeated or structured
- # properties) keep their identity.
- cached_ent._values = ent._values
- ent = cached_ent
- else:
- self._cache[key] = ent
+ ent = self._update_cache_from_query_result(ent, options)
+ if ent is None:
+ continue
if callback is None:
val = ent
else:
@@ -779,6 +772,25 @@
helper()
return mfut
+ def _update_cache_from_query_result(self, ent, options):
+ if isinstance(ent, model.Key):
+ return ent # It was a keys-only query and ent is really a Key.
+ key = ent._key
+ if not self._use_cache(key, options):
+ return ent # This key should not be cached.
+
+ # Check the cache. If there is a valid cached entry, substitute
+ # that for the result, even if the cache has an explicit None.
+ if key in self._cache:
+ cached_ent = self._cache[key]
+ if (cached_ent is None or
+ cached_ent.key == key and cached_ent.__class__ is ent.__class__):
+ return cached_ent
+
+ # Update the cache.
+ self._cache[key] = ent
+ return ent
+
@utils.positional(2)
def iter_query(self, query, callback=None, pass_batch_into_callback=None,
options=None):
@@ -791,30 +803,58 @@
# Will invoke callback() one or more times with the default
# context set to a new, transactional Context. Returns a Future.
# Callback may be a tasklet.
- options = _make_ctx_options(ctx_options)
- app = ContextOptions.app(options) or key_module._DefaultAppId()
+ options = _make_ctx_options(ctx_options, TransactionOptions)
+ propagation = TransactionOptions.propagation(options)
+ if propagation is None:
+ propagation = TransactionOptions.NESTED
+
+ parent = self
+ if propagation == TransactionOptions.NESTED:
+ if self.in_transaction():
+ raise datastore_errors.BadRequestError(
+ 'Nested transactions are not supported.')
+ elif propagation == TransactionOptions.MANDATORY:
+ if not self.in_transaction():
+ raise datastore_errors.BadRequestError(
+ 'Requires an existing transaction.')
+ raise tasklets.Return(callback())
+ elif propagation == TransactionOptions.ALLOWED:
+ if self.in_transaction():
+ raise tasklets.Return(callback())
+ elif propagation == TransactionOptions.INDEPENDENT:
+ while parent.in_transaction():
+ parent = parent._parent_context
+ if parent is None:
+ raise datastore_errors.BadRequestError(
+ 'Context without non-transactional ancestor')
+ else:
+ raise datastore_errors.BadArgumentError(
+ 'Invalid propagation value (%s).' % (propagation,))
+
+ app = TransactionOptions.app(options) or key_module._DefaultAppId()
# Note: zero retries means try it once.
- retries = ContextOptions.retries(options)
+ retries = TransactionOptions.retries(options)
if retries is None:
retries = 3
- yield self.flush()
+ yield parent.flush()
for _ in xrange(1 + max(0, retries)):
- transaction = yield self._conn.async_begin_transaction(options, app)
+ transaction = yield parent._conn.async_begin_transaction(options, app)
tconn = datastore_rpc.TransactionalConnection(
- adapter=self._conn.adapter,
- config=self._conn.config,
+ adapter=parent._conn.adapter,
+ config=parent._conn.config,
transaction=transaction)
old_ds_conn = datastore._GetConnection()
- tctx = self.__class__(conn=tconn,
- auto_batcher_class=self._auto_batcher_class)
+ tctx = parent.__class__(conn=tconn,
+ auto_batcher_class=parent._auto_batcher_class,
+ parent_context=parent)
try:
# Copy memcache policies. Note that get() will never use
# memcache in a transaction, but put and delete should do their
# memcache thing (which is to mark the key as deleted for
# _LOCK_TIME seconds). Also note that the in-process cache and
# datastore policies keep their default (on) state.
- tctx.set_memcache_policy(self.get_memcache_policy())
- tctx.set_memcache_timeout_policy(self.get_memcache_timeout_policy())
+ tctx.set_memcache_policy(parent.get_memcache_policy())
+ tctx.set_memcache_timeout_policy(parent.get_memcache_timeout_policy())
tasklets.set_context(tctx)
datastore._SetConnection(tconn) # For taskqueue coordination
try:
@@ -837,9 +877,8 @@
else:
ok = yield tconn.async_commit(options)
if ok:
- # TODO: This is questionable when self is transactional.
- self._cache.update(tctx._cache)
- yield self._clear_memcache(tctx._cache)
+ parent._cache.update(tctx._cache)
+ yield parent._clear_memcache(tctx._cache)
raise tasklets.Return(result)
finally:
datastore._SetConnection(old_ds_conn)
diff --git a/google/appengine/ext/ndb/model.py b/google/appengine/ext/ndb/model.py
index 745e84d..4de499a 100644
--- a/google/appengine/ext/ndb/model.py
+++ b/google/appengine/ext/ndb/model.py
@@ -297,7 +297,6 @@
# NOTE: 'key' is a common local variable name.
from . import key as key_module
Key = key_module.Key # For export.
-_MAX_LONG = key_module._MAX_LONG
# NOTE: Property and Error classes are added later.
__all__ = ['Key', 'BlobKey', 'GeoPt', 'Rollback',
@@ -305,7 +304,7 @@
'ModelAdapter', 'ModelAttribute',
'ModelKey', 'MetaModel', 'Model', 'Expando',
'transaction', 'transaction_async',
- 'in_transaction', 'transactional',
+ 'in_transaction', 'transactional', 'non_transactional',
'get_multi', 'get_multi_async',
'put_multi', 'put_multi_async',
'delete_multi', 'delete_multi_async',
@@ -330,6 +329,9 @@
class ComputedPropertyError(datastore_errors.Error):
"""Raised when attempting to assign a value to a computed property."""
+# Various imported limits.
+_MAX_LONG = key_module._MAX_LONG
+_MAX_STRING_LENGTH = datastore_types._MAX_STRING_LENGTH
# Map index directions to human-readable strings.
_DIR_MAP = {
@@ -681,7 +683,7 @@
if not isinstance(value, <top type>):
raise TypeError(...) # Or datastore_errors.BadValueError(...).
- def _to_base_type(sellf, value):
+ def _to_base_type(self, value):
'(Strict) user value to base value.'
if isinstance(value, <user type>):
return <base type>(value)
@@ -777,8 +779,8 @@
# value.lower() or value.strip() is fine, but one that returns
# value + '$' is not.
if not hasattr(validator, '__call__'):
- raise TypeError('validator must be callable or None; received %r'
- % validator)
+ raise TypeError('validator must be callable or None; received %r' %
+ validator)
self._validator = validator
def __repr__(self):
@@ -823,7 +825,6 @@
'Cannot query for unindexed property %s' % self._name)
from .query import FilterNode # Import late to avoid circular imports.
if value is not None:
- # TODO: Allow query.Binding instances?
value = self._do_validate(value)
value = self._call_to_base_type(value)
value = self._datastore_type(value)
@@ -1448,10 +1449,15 @@
'indexed at the same time.' % self._name)
def _validate(self, value):
- # TODO: Enforce size limit when indexed.
if not isinstance(value, str):
raise datastore_errors.BadValueError('Expected str, got %r' %
(value,))
+ if (self._indexed and
+ not isinstance(self, TextProperty) and
+ len(value) > _MAX_STRING_LENGTH):
+ raise datastore_errors.BadValueError(
+ 'Indexed value %s must be at most %d bytes' %
+ (self._name, _MAX_STRING_LENGTH))
def _to_base_type(self, value):
if self._compressed:
@@ -1500,10 +1506,20 @@
"""An unindexed Property whose value is a text string of unlimited length."""
def _validate(self, value):
- # TODO: Enforce size limit when indexed.
- if not isinstance(value, basestring):
+ if isinstance(value, str):
+ # Decode from UTF-8 -- if this fails, we can't write it.
+ try:
+ value = unicode(value, 'utf-8')
+ except UnicodeError:
+ raise datastore_errors.BadValueError('Expected valid UTF-8, got %r' %
+ (value,))
+ elif not isinstance(value, unicode):
raise datastore_errors.BadValueError('Expected string, got %r' %
(value,))
+ if self._indexed and len(value) > _MAX_STRING_LENGTH:
+ raise datastore_errors.BadValueError(
+ 'Indexed value %s must be at most %d characters' %
+ (self._name, _MAX_STRING_LENGTH))
def _to_base_type(self, value):
if isinstance(value, unicode):
@@ -1512,8 +1528,13 @@
def _from_base_type(self, value):
if isinstance(value, str):
try:
- return value.decode('utf-8')
+ return unicode(value, 'utf-8')
except UnicodeDecodeError:
+ # Since older versions of NDB could write non-UTF-8 TEXT
+ # properties, we can't just reject these. But _validate() now
+ # rejects these, so you can't write new non-UTF-8 TEXT
+ # properties.
+ # TODO: Eventually we should close this hole.
pass
def _db_set_uncompressed_meaning(self, p):
@@ -2209,8 +2230,42 @@
the datastore but not represented in the Model subclass) but can
also be used explicitly for properties with dynamically-typed
values.
+
+ This supports compressed=True, which is only effective for str
+ values (not for unicode), and implies indexed=False.
"""
+ _compressed = False
+
+ _attributes = Property._attributes + ['_compressed']
+
+ @utils.positional(1 + Property._positional)
+ def __init__(self, name=None, compressed=False, **kwds):
+ if compressed: # Compressed implies unindexed.
+ kwds.setdefault('indexed', False)
+ super(GenericProperty, self).__init__(name=name, **kwds)
+ self._compressed = compressed
+ if compressed and self._indexed:
+ # TODO: Allow this, but only allow == and IN comparisons?
+ raise NotImplementedError('GenericProperty %s cannot be compressed and '
+ 'indexed at the same time.' % self._name)
+
+ def _to_base_type(self, value):
+ if self._compressed and isinstance(value, str):
+ return _CompressedValue(zlib.compress(value))
+
+ def _from_base_type(self, value):
+ if isinstance(value, _CompressedValue):
+ return zlib.decompress(value.z_val)
+
+ def _validate(self, value):
+ if (isinstance(value, basestring) and
+ self._indexed and
+ len(value) > _MAX_STRING_LENGTH):
+ raise datastore_errors.BadValueError(
+ 'Indexed value %s must be at most %d bytes' %
+ (self._name, _MAX_STRING_LENGTH))
+
def _db_get_value(self, v, p):
# This is awkward but there seems to be no faster way to inspect
# what union member is present. datastore_types.FromPropertyPb(),
@@ -2221,8 +2276,10 @@
meaning = p.meaning()
if meaning == entity_pb.Property.BLOBKEY:
sval = BlobKey(sval)
- elif meaning not in (entity_pb.Property.BLOB,
- entity_pb.Property.BYTESTRING):
+ elif meaning == entity_pb.Property.BLOB:
+ if p.meaning_uri() == _MEANING_URI_COMPRESSED:
+ sval = _CompressedValue(sval)
+ elif meaning != entity_pb.Property.BYTESTRING:
try:
sval.decode('ascii')
# If this passes, don't return unicode.
@@ -2305,6 +2362,11 @@
elif isinstance(value, BlobKey):
v.set_stringvalue(str(value))
p.set_meaning(entity_pb.Property.BLOBKEY)
+ elif isinstance(value, _CompressedValue):
+ value = value.z_val
+ v.set_stringvalue(value)
+ p.set_meaning_uri(_MEANING_URI_COMPRESSED)
+ p.set_meaning(entity_pb.Property.BLOB)
else:
raise NotImplementedError('Property %s does not support %s types.' %
(self._name, type(value)))
@@ -2423,19 +2485,20 @@
_key = ModelKey()
key = _key
- @utils.positional(1)
- def __init__(self, key=None, id=None, parent=None, **kwds):
+ def __init__(*args, **kwds):
"""Creates a new instance of this model (a.k.a. an entity).
The new entity must be written to the datastore using an explicit
call to .put().
- Args:
+ Keyword Args:
key: Key instance for this model. If key is used, id and parent must
be None.
id: Key id for this model. If id is used, key must be None.
parent: Key instance for the parent model or None for a top-level one.
If parent is used, key must be None.
+ namespace: Optional namespace.
+ app: Optional app ID.
**kwds: Keyword arguments mapping to properties of this model.
Note: you cannot define a property named key; the .key attribute
@@ -2444,26 +2507,39 @@
through the constructor, but can be assigned to entity attributes
after the entity has been created.
"""
- # TODO: Use the same signature hacks as in get_or_insert() to
- # fully support properties named id, parent or key?
+ (self,) = args
+ get_arg = self.__get_arg
+ key = get_arg(kwds, 'key')
+ id = get_arg(kwds, 'id')
+ app = get_arg(kwds, 'app')
+ namespace = get_arg(kwds, 'namespace')
+ parent = get_arg(kwds, 'parent')
if key is not None:
- if id is not None:
+ if (id is not None or parent is not None or
+ app is not None or namespace is not None):
raise datastore_errors.BadArgumentError(
- 'Model constructor accepts key or id, not both.')
- if parent is not None:
- raise datastore_errors.BadArgumentError(
- 'Model constructor accepts key or parent, not both.')
+ 'Model constructor given key= does not accept '
+ 'id=, app=, namespace=, or parent=.')
self._key = _validate_key(key, entity=self)
- elif id is not None or parent is not None:
- # When parent is set but id is not, we have an incomplete key.
- # Key construction will fail with invalid ids or parents, so no check
- # is needed.
- # TODO: should this be restricted to string ids?
- self._key = Key(self._get_kind(), id, parent=parent)
-
+ elif (id is not None or parent is not None or
+ app is not None or namespace is not None):
+ self._key = Key(self._get_kind(), id,
+ parent=parent, app=app, namespace=namespace)
self._values = {}
self._set_attributes(kwds)
+ @classmethod
+ def __get_arg(cls, kwds, kwd):
+ """Helper method to parse keywords that may be property names."""
+ alt_kwd = '_' + kwd
+ if alt_kwd in kwds:
+ return kwds.pop(alt_kwd)
+ if kwd in kwds:
+ obj = getattr(cls, kwd, None)
+ if not isinstance(obj, Property) or isinstance(obj, ModelKey):
+ return kwds.pop(kwd)
+ return None
+
def __getstate__(self):
return self._to_pb().Encode()
@@ -2687,9 +2763,11 @@
prop = StructuredProperty(Expando, next)
prop._store_value(self, _BaseValue(Expando()))
else:
+ compressed = p.meaning_uri() == _MEANING_URI_COMPRESSED
prop = GenericProperty(next,
repeated=p.multiple(),
- indexed=indexed)
+ indexed=indexed,
+ compressed=compressed)
prop._code_name = next
self._properties[prop._name] = prop
return prop
@@ -2841,8 +2919,12 @@
def _get_or_insert(*args, **kwds):
"""Transactionally retrieves an existing entity or creates a new one.
- Args:
+ Positional Args:
name: Key name to retrieve or create.
+
+ Keyword Args:
+ namespace: Optional namespace.
+ app: Optional app ID.
parent: Parent entity key, if any.
context_options: ContextOptions object (not keyword args!) or None.
**kwds: Keyword arguments to pass to the constructor of the model class
@@ -2868,21 +2950,11 @@
# models with properties named e.g. 'cls' or 'name'.
from . import tasklets
cls, name = args # These must always be positional.
- our_kwds = {}
- for kwd in 'app', 'namespace', 'parent', 'context_options':
- # For each of these keyword arguments, if there is a property
- # with the same name, the caller *must* use _foo=..., otherwise
- # they may use either _foo=... or foo=..., but _foo=... wins.
- alt_kwd = '_' + kwd
- if alt_kwd in kwds:
- our_kwds[kwd] = kwds.pop(alt_kwd)
- elif (kwd in kwds and
- not isinstance(getattr(cls, kwd, None), Property)):
- our_kwds[kwd] = kwds.pop(kwd)
- app = our_kwds.get('app')
- namespace = our_kwds.get('namespace')
- parent = our_kwds.get('parent')
- context_options = our_kwds.get('context_options')
+ get_arg = cls.__get_arg
+ app = get_arg(kwds, 'app')
+ namespace = get_arg(kwds, 'namespace')
+ parent = get_arg(kwds, 'parent')
+ context_options = get_arg(kwds, 'context_options')
# (End of super-special argument parsing.)
# TODO: Test the heck out of this, in all sorts of evil scenarios.
if not isinstance(name, basestring):
@@ -2893,17 +2965,23 @@
@tasklets.tasklet
def internal_tasklet():
- ent = yield key.get_async(options=context_options)
- if ent is None:
- @tasklets.tasklet
- def txn():
- ent = yield key.get_async(options=context_options)
- if ent is None:
- ent = cls(**kwds) # TODO: Use _populate().
- ent._key = key
- yield ent.put_async(options=context_options)
- raise tasklets.Return(ent)
- ent = yield transaction_async(txn)
+ @tasklets.tasklet
+ def txn():
+ ent = yield key.get_async(options=context_options)
+ if ent is None:
+ ent = cls(**kwds) # TODO: Use _populate().
+ ent._key = key
+ yield ent.put_async(options=context_options)
+ raise tasklets.Return(ent)
+ if in_transaction():
+ # Run txn() in existing transaction.
+ ent = yield txn()
+ else:
+ # Maybe avoid a transaction altogether.
+ ent = yield key.get_async(options=context_options)
+ if ent is None:
+ # Run txn() in new transaction.
+ ent = yield transaction_async(txn)
raise tasklets.Return(ent)
return internal_tasklet()
@@ -3105,7 +3183,27 @@
Args:
callback: A function or tasklet to be called.
- **ctx_options: Context options.
+ **ctx_options: Transaction options.
+
+ Useful options include:
+ retries=N: Retry up to N times (i.e. try up to N+1 times)
+ propagation=<flag>: Determines how an existing transaction should be
+ propagated, where <flag> can be one of the following:
+ TransactionOptions.NESTED: Start a nested transaction (this is the
+ default; but actual nested transactions are not yet implemented,
+ so effectively you can only use this outsie an existing transaction).
+ TransactionOptions.MANDATORY: A transaction must already be in progress.
+ TransactionOptions.ALLOWED: If a transaction is in progress, join it.
+ TransactionOptions.INDEPENDENT: Always start a new parallel transaction.
+ xg=True: On the High Replication Datastore, enable cross-group
+ transactions, i.e. allow writing to up to 5 entity groups.
+
+ WARNING: Using anything other than NESTED for the propagation flag
+ can have strange consequences. When using ALLOWED or MANDATORY, if
+ an exception is raised, the transaction is likely not safe to
+ commit. When using INDEPENDENT it is not generally safe to return
+ values read to the caller (as they were not read in the caller's
+ transaction).
Returns:
Whatever callback() returns.
@@ -3125,13 +3223,13 @@
@utils.positional(1)
-def transaction_async(callback, **kwds):
+def transaction_async(callback, **ctx_options):
"""Run a callback in a transaction.
This is the asynchronous version of transaction().
"""
from . import tasklets
- return tasklets.get_context().transaction(callback, **kwds)
+ return tasklets.get_context().transaction(callback, **ctx_options)
def in_transaction():
@@ -3141,10 +3239,13 @@
@utils.positional(1)
-def transactional(func=None, **ctx_options):
+def transactional(_func=None, **ctx_options):
"""Decorator to make a function automatically run in a transaction.
- If we're already in a transaction this is a no-op.
+ Args:
+ _func: Do not use.
+ **ctx_options: Transaction options (see transaction(), but propagation
+ default to TransactionOptions.ALLOWED).
This supports two forms:
@@ -3158,24 +3259,76 @@
def callback(arg):
...
"""
- if func is not None:
+ if _func is not None:
# Form (1), vanilla.
if ctx_options:
raise TypeError('@transactional() does not take positional arguments')
- return transactional()(func)
+ # TODO: Avoid recursion, call outer_transactional_wrapper() directly?
+ return transactional()(_func)
+
+ ctx_options.setdefault('propagation',
+ datastore_rpc.TransactionOptions.ALLOWED)
# Form (2), with options.
def outer_transactional_wrapper(func):
@utils.wrapping(func)
- def transactional_wrapper(*args, **kwds):
- if in_transaction():
- return func(*args, **kwds)
- else:
- return transaction(lambda: func(*args, **kwds), **ctx_options)
- return transactional_wrapper
+ def inner_transactional_wrapper(*args, **kwds):
+ f = func
+ if args or kwds:
+ f = lambda: func(*args, **kwds)
+ return transaction(f, **ctx_options)
+ return inner_transactional_wrapper
return outer_transactional_wrapper
+@utils.positional(1)
+def non_transactional(_func=None, allow_existing=True):
+ """A decorator that ensures a function is run outside a transaction.
+
+ If there is an existing transaction (and allow_existing=True), the
+ existing transaction is paused while the function is executed.
+
+ Args:
+ _func: Do not use.
+ allow_existing: If false, throw an exception if called from within
+ a transaction. If true, temporarily re-establish the
+ previous non-transactional context. Defaults to True.
+
+ This supports two forms, similar to transactional().
+
+ Returns:
+ A wrapper for the decorated function that ensures it runs outside a
+ transaction.
+ """
+ if _func is not None:
+ # TODO: Avoid recursion, call outer_non_transactional_wrapper() directly?
+ return non_transactional()(_func)
+
+ def outer_non_transactional_wrapper(func):
+ from . import tasklets
+ @utils.wrapping(func)
+ def inner_non_transactional_wrapper(*args, **kwds):
+ ctx = tasklets.get_context()
+ if not ctx.in_transaction():
+ return func(*args, **kwds)
+ if not allow_existing:
+ raise datastore_errors.BadRequestError(
+ '%s cannot be called within a transaction.' % func.__name__)
+ save_ctx = ctx
+ while ctx.in_transaction():
+ ctx = ctx._parent_context
+ if ctx is None:
+ raise datastore_errors.BadRequestError(
+ 'Context without non-transactional ancestor')
+ try:
+ tasklets.set_context(ctx)
+ return func(*args, **kwds)
+ finally:
+ tasklets.set_context(save_ctx)
+ return inner_non_transactional_wrapper
+ return outer_non_transactional_wrapper
+
+
def get_multi_async(keys, **ctx_options):
"""Fetches a sequence of keys.
diff --git a/google/appengine/ext/ndb/query.py b/google/appengine/ext/ndb/query.py
index a62d5f0..19c10b7 100644
--- a/google/appengine/ext/ndb/query.py
+++ b/google/appengine/ext/ndb/query.py
@@ -137,6 +137,7 @@
from .google_imports import datastore_query
from . import model
+from . import context
from . import tasklets
from . import utils
@@ -149,7 +150,6 @@
]
# Re-export some useful classes from the lower-level module.
-QueryOptions = datastore_query.QueryOptions
Cursor = datastore_query.Cursor
# Some local renamings.
@@ -165,6 +165,10 @@
_MAX_LIMIT = 2 ** 31 - 1
+class QueryOptions(context.ContextOptions, datastore_query.QueryOptions):
+ """Support both context options and query options (esp. use_cache)."""
+
+
class RepeatedStructuredPropertyPredicate(datastore_query.FilterPredicate):
# Used by model.py.
@@ -883,17 +887,9 @@
batch = yield rpc
rpc = batch.next_batch_async(options)
for result in batch.results:
- # Update cache, copying code from Context().map_query().
- if not options or not options.keys_only:
- key = result._key
- if ctx._use_cache(key, options):
- cached_result = ctx._cache.get(key)
- if cached_result is not None and cached_result.key == key:
- cached_result._values = result._values
- result = cached_result
- else:
- ctx._cache[key] = result
- results.append(result)
+ result = ctx._update_cache_from_query_result(result, options)
+ if result is not None:
+ results.append(result)
raise tasklets.Return(results)
diff --git a/google/appengine/ext/ndb/tasklets.py b/google/appengine/ext/ndb/tasklets.py
index ba5e973..d0b8011 100644
--- a/google/appengine/ext/ndb/tasklets.py
+++ b/google/appengine/ext/ndb/tasklets.py
@@ -378,14 +378,18 @@
except Exception, err:
_, _, tb = sys.exc_info()
if isinstance(err, _flow_exceptions):
+ # Flow exceptions aren't logged except in "heavy debug" mode,
+ # and then only at DEBUG level, without a traceback.
_logging_debug('%s raised %s(%s)',
info, err.__class__.__name__, err)
- elif logging.getLogger().level > logging.INFO:
- logging.warning('%s raised %s(%s)',
- info, err.__class__.__name__, err)
- else:
+ elif utils.DEBUG and logging.getLogger().level < logging.DEBUG:
+ # In "heavy debug" mode, log a warning with traceback.
+ # (This is the same condition as used in utils.logging_debug().)
logging.warning('%s raised %s(%s)',
info, err.__class__.__name__, err, exc_info=True)
+ else:
+ # Otherwise, log a warning without a traceback.
+ logging.warning('%s raised %s(%s)', info, err.__class__.__name__, err)
self.set_exception(err, tb)
return
diff --git a/google/appengine/ext/ndb/test_utils.py b/google/appengine/ext/ndb/test_utils.py
deleted file mode 100644
index 9444486..0000000
--- a/google/appengine/ext/ndb/test_utils.py
+++ /dev/null
@@ -1,87 +0,0 @@
-"""Test utilities for writing NDB tests.
-
-Useful set of utilities for correctly setting up the appengine testing
-environment. Functions and test-case base classes that configure stubs
-and other environment variables.
-"""
-
-import logging
-import unittest
-
-from google.appengine.ext import testbed
-
-from . import model
-from . import tasklets
-from . import eventloop
-
-
-class NDBTest(unittest.TestCase):
- """Base class for tests that interact with API stubs or create Models.
-
- NOTE: Care must be used when working with model classes using this test
- class. The kind-map is reset on each iteration. The general practice
- should be to declare test models in the sub-classes setUp method AFTER
- calling this classes setUp method.
- """
-
- APP_ID = '_'
-
- def setUp(self):
- """Set up test framework.
-
- Configures basic environment variables, stubs and creates a default
- connection.
- """
- self.testbed = testbed.Testbed()
- self.testbed.setup_env(app_id=self.APP_ID)
- self.testbed.activate()
- self.testbed.init_datastore_v3_stub()
- self.testbed.init_memcache_stub()
- self.testbed.init_taskqueue_stub()
-
- self.conn = model.make_connection()
-
- self.ResetKindMap()
- self.SetupContextCache()
-
- self._logger = logging.getLogger()
- self._old_log_level = self._logger.getEffectiveLevel()
-
- def ExpectErrors(self):
- if self.DefaultLogging():
- self._logger.setLevel(logging.CRITICAL)
-
- def ExpectWarnings(self):
- if self.DefaultLogging():
- self._logger.setLevel(logging.ERROR)
-
- def DefaultLogging(self):
- return self._old_log_level == logging.WARNING
-
- def tearDown(self):
- """Tear down test framework."""
- self._logger.setLevel(self._old_log_level)
- ev = eventloop.get_event_loop()
- stragglers = 0
- while ev.run1():
- stragglers += 1
- if stragglers:
- logging.info('Processed %d straggler events after test completed',
- stragglers)
- self.ResetKindMap()
- self.testbed.deactivate()
-
- def ResetKindMap(self):
- model.Model._reset_kind_map()
-
- def SetupContextCache(self):
- """Set up the context cache.
-
- We only need cache active when testing the cache, so the default behavior
- is to disable it to avoid misleading test results. Override this when
- needed.
- """
- ctx = tasklets.make_default_context()
- tasklets.set_context(ctx)
- ctx.set_cache_policy(False)
- ctx.set_memcache_policy(False)
diff --git a/google/appengine/ext/testbed/__init__.py b/google/appengine/ext/testbed/__init__.py
index 2fbd568..1421f0b 100755
--- a/google/appengine/ext/testbed/__init__.py
+++ b/google/appengine/ext/testbed/__init__.py
@@ -517,3 +517,36 @@
return
stub = xmpp_service_stub.XmppServiceStub()
self._register_stub(XMPP_SERVICE_NAME, stub)
+
+ def _init_stub(self, service_name, *args, **kwargs):
+ """Enable a stub by service name.
+
+ Args:
+ service_name: Name of service to initialize. This name should be the
+ name used by the service stub.
+
+ Additional arguments are passed along to the specific stub initializer.
+
+ Raises:
+ NotActivatedError: When this function is called before testbed is
+ activated or after it is deactivated.
+ StubNotSupportedError: When an unsupported service_name is provided.
+ """
+ if not self._activated:
+ raise NotActivatedError('The testbed is not activated.')
+ if service_name not in SUPPORTED_SERVICES:
+ msg = 'The "%s" service is not supported by testbed' % service_name
+ raise StubNotSupportedError(msg)
+ method_name = 'init_%s_stub' % service_name
+ method = getattr(self, method_name)
+ method(*args, **kwargs)
+
+ def init_all_stubs(self, enable=True):
+ """Enable all known testbed stubs.
+
+ Args:
+ enable: True, if the fake services should be enabled, False if real
+ services should be disabled.
+ """
+ for service_name in SUPPORTED_SERVICES:
+ self._init_stub(service_name, enable)
diff --git a/google/appengine/ext/webapp/blobstore_handlers.py b/google/appengine/ext/webapp/blobstore_handlers.py
index 82073c3..6359fd5 100755
--- a/google/appengine/ext/webapp/blobstore_handlers.py
+++ b/google/appengine/ext/webapp/blobstore_handlers.py
@@ -253,6 +253,10 @@
if isinstance(blob_key_or_info, blobstore.BlobInfo):
blob_key = blob_key_or_info.key()
blob_info = blob_key_or_info
+ elif isinstance(blob_key_or_info, str) and blob_key_or_info.startswith(
+ '/gs/'):
+ blob_key = blobstore.create_gs_key(blob_key_or_info)
+ blob_info = None
else:
blob_key = blob_key_or_info
blob_info = None
diff --git a/google/appengine/tools/api_server.py b/google/appengine/tools/api_server.py
index 7beca32..4256630 100644
--- a/google/appengine/tools/api_server.py
+++ b/google/appengine/tools/api_server.py
@@ -29,7 +29,6 @@
import BaseHTTPServer
import httplib
import logging
-import os
import os.path
import pickle
import socket
@@ -405,6 +404,15 @@
apiproxy_stub_map.apiproxy.GetStub('taskqueue')))
+def _TearDownStubs():
+ """Clean up any stubs that need cleanup."""
+
+ logging.info('Applying all pending transactions and saving the datastore')
+ datastore_stub = apiproxy_stub_map.apiproxy.GetStub('datastore_v3')
+ datastore_stub.Flush()
+ datastore_stub.Write()
+
+
def ParseCommandArguments(args):
"""Parses and the application's command line arguments.
@@ -633,7 +641,7 @@
return 'http://%s:%d' % (self._host, self._port)
def __repr__(self):
- return '<APIServerProcess command=%r>' % ' '.join(self.args)
+ return '<APIServerProcess command=%r>' % ' '.join(self._args)
def Start(self):
"""Starts the API Server process."""
@@ -703,6 +711,11 @@
def main():
+
+ logging.basicConfig(
+ level=logging.INFO,
+ format='[API Server] [%(filename)s:%(lineno)d] %(levelname)s %(message)s')
+
args = ParseCommandArguments(sys.argv[1:])
if args.clear_datastore:
@@ -753,9 +766,11 @@
taskqueue_default_http_server=application_address,
user_login_url=args.user_login_url,
user_logout_url=args.user_logout_url)
-
server = APIServer((args.api_host, args.api_port), args.application)
- server.serve_forever()
+ try:
+ server.serve_forever()
+ finally:
+ _TearDownStubs()
if __name__ == '__main__':
diff --git a/google/appengine/tools/appcfg.py b/google/appengine/tools/appcfg.py
index c11198d..7b94ffd 100755
--- a/google/appengine/tools/appcfg.py
+++ b/google/appengine/tools/appcfg.py
@@ -60,6 +60,7 @@
from google.appengine.api import backendinfo
from google.appengine.api import croninfo
from google.appengine.api import dosinfo
+from google.appengine.api import pagespeedinfo
from google.appengine.api import queueinfo
from google.appengine.api import validation
from google.appengine.api import yaml_errors
@@ -847,6 +848,31 @@
payload=self.dos.ToYAML())
+class PagespeedEntryUpload(object):
+ """Provides facilities to upload pagespeed configs to the hosting service."""
+
+ def __init__(self, rpcserver, config, pagespeed):
+ """Creates a new PagespeedEntryUpload.
+
+ Args:
+ rpcserver: The RPC server to use. Should be an instance of a subclass of
+ AbstractRpcServer.
+ config: The AppInfoExternal object derived from the app.yaml file.
+ pagespeed: The PagespeedInfoExternal object from pagespeed.yaml.
+ """
+ self.rpcserver = rpcserver
+ self.config = config
+ self.pagespeed = pagespeed
+
+ def DoUpload(self):
+ """Uploads the pagespeed entries."""
+ StatusUpdate('Uploading Page Speed configuration.')
+ self.rpcserver.Send('/api/pagespeed/update',
+ app_id=self.config.application,
+ version=self.config.version,
+ payload=self.pagespeed.ToYAML())
+
+
class DefaultVersionSet(object):
"""Provides facilities to set the default (serving) version."""
@@ -2540,7 +2566,9 @@
action_names.sort()
desc = ''
for action_name in action_names:
- desc += ' %s: %s\n' % (action_name, self.actions[action_name].short_desc)
+ if not self.actions[action_name].hidden:
+ desc += ' %s: %s\n' % (action_name,
+ self.actions[action_name].short_desc)
return desc
def _GetOptionParser(self):
@@ -2879,6 +2907,18 @@
"""
return self._ParseYamlFile(basepath, 'dos', dosinfo.LoadSingleDos)
+ def _ParsePagespeedYaml(self, basepath):
+ """Parses the pagespeed.yaml file.
+
+ Args:
+ basepath: the directory of the application.
+
+ Returns:
+ A PagespeedInfoExternal object or None if the file does not exist.
+ """
+ return self._ParseYamlFile(basepath, 'pagespeed',
+ pagespeedinfo.LoadSinglePagespeed)
+
def Help(self, action=None):
"""Prints help for a specific action.
@@ -3014,6 +3054,13 @@
dos_upload = DosEntryUpload(rpcserver, appyaml, dos_yaml)
dos_upload.DoUpload()
+
+ pagespeed_yaml = self._ParsePagespeedYaml(self.basepath)
+ if pagespeed_yaml:
+ pagespeed_upload = PagespeedEntryUpload(
+ rpcserver, appyaml, pagespeed_yaml)
+ pagespeed_upload.DoUpload()
+
def _UpdateOptions(self, parser):
"""Adds update-specific options to 'parser'.
@@ -3117,6 +3164,21 @@
dos_upload = DosEntryUpload(rpcserver, appyaml, dos_yaml)
dos_upload.DoUpload()
+ def UpdatePagespeed(self):
+ """Updates any new or changed pagespeed configuration."""
+ if self.args:
+ self.parser.error('Expected a single <directory> argument.')
+
+ appyaml = self._ParseAppYaml(self.basepath)
+ rpcserver = self._GetRpcServer()
+
+
+ pagespeed_yaml = self._ParsePagespeedYaml(self.basepath)
+ if pagespeed_yaml:
+ pagespeed_upload = PagespeedEntryUpload(
+ rpcserver, appyaml, pagespeed_yaml)
+ pagespeed_upload.DoUpload()
+
def BackendsAction(self):
"""Placeholder; we never expect this action to be invoked."""
pass
@@ -3722,6 +3784,7 @@
object.
uses_basepath: Does the action use a basepath/app-directory (and hence
app.yaml).
+ hidden: Should this command be shown in the help listing.
"""
@@ -3731,7 +3794,7 @@
def __init__(self, function, usage, short_desc, long_desc='',
error_desc=None, options=lambda obj, parser: None,
- uses_basepath=True):
+ uses_basepath=True, hidden=False):
"""Initializer for the class attributes."""
self.function = function
self.usage = usage
@@ -3740,6 +3803,7 @@
self.error_desc = error_desc
self.options = options
self.uses_basepath = uses_basepath
+ self.hidden = hidden
def __call__(self, appcfg):
"""Invoke this Action on the specified AppCfg.
@@ -3818,6 +3882,15 @@
The 'update_dos' command will update any new, removed or changed dos
definitions from the optional dos.yaml file."""),
+ 'update_pagespeed': Action(
+ function='UpdatePagespeed',
+ usage='%prog [options] update_pagespeed <directory>',
+ short_desc='Update application pagespeed definitions.',
+ long_desc="""
+The 'update_pagespeed' command will update your Page Speed configuration
+from the optional pagesped.yaml file.""",
+ hidden=True),
+
'backends': Action(
function='BackendsAction',
usage='%prog [options] backends <directory> <action>',
diff --git a/google/appengine/tools/appengine_rpc_httplib2.py b/google/appengine/tools/appengine_rpc_httplib2.py
index a0e9524..b5f501c 100644
--- a/google/appengine/tools/appengine_rpc_httplib2.py
+++ b/google/appengine/tools/appengine_rpc_httplib2.py
@@ -37,15 +37,9 @@
import httplib2
-try:
- from oauth2client import client
- from oauth2client import file as oauth2client_file
- from oauth2client import tools
-except ImportError:
-
- from apiclient.oauth2client import client
- from apiclient.oauth2client import file as oauth2client_file
- from apiclient.oauth2client import tools
+from oauth2client import client
+from oauth2client import file as oauth2client_file
+from oauth2client import tools
logger = logging.getLogger('google.appengine.tools.appengine_rpc')
diff --git a/google/appengine/tools/dev-channel-js.js b/google/appengine/tools/dev-channel-js.js
index 73ad08b..ad8fb8c 100755
--- a/google/appengine/tools/dev-channel-js.js
+++ b/google/appengine/tools/dev-channel-js.js
@@ -42,17 +42,22 @@
goog.basePath = "";
goog.nullFunction = function() {
};
-goog.identityFunction = function(var_args) {
- return var_args
+goog.identityFunction = function(opt_returnValue) {
+ return opt_returnValue
};
goog.abstractMethod = function() {
throw Error("unimplemented abstract method");
};
goog.addSingletonGetter = function(ctor) {
ctor.getInstance = function() {
- return ctor.instance_ || (ctor.instance_ = new ctor)
+ if(ctor.instance_) {
+ return ctor.instance_
+ }
+ goog.DEBUG && (goog.instantiatedSingletons_[goog.instantiatedSingletons_.length] = ctor);
+ return ctor.instance_ = new ctor
}
};
+goog.instantiatedSingletons_ = [];
goog.typeOf = function(value) {
var s = typeof value;
if("object" == s) {
@@ -1391,29 +1396,28 @@
};
goog.dom.classes.get = function(element) {
var className = element.className;
- return className && "function" == typeof className.split ? className.split(/\s+/) : []
+ return goog.isString(className) && className.match(/\S+/g) || []
};
goog.dom.classes.add = function(element, var_args) {
- var classes = goog.dom.classes.get(element), args = goog.array.slice(arguments, 1), b = goog.dom.classes.add_(classes, args);
+ var classes = goog.dom.classes.get(element), args = goog.array.slice(arguments, 1), expectedCount = classes.length + args.length;
+ goog.dom.classes.add_(classes, args);
element.className = classes.join(" ");
- return b
+ return classes.length == expectedCount
};
goog.dom.classes.remove = function(element, var_args) {
- var classes = goog.dom.classes.get(element), args = goog.array.slice(arguments, 1), b = goog.dom.classes.remove_(classes, args);
- element.className = classes.join(" ");
- return b
+ var classes = goog.dom.classes.get(element), args = goog.array.slice(arguments, 1), newClasses = goog.dom.classes.getDifference_(classes, args);
+ element.className = newClasses.join(" ");
+ return newClasses.length == classes.length - args.length
};
goog.dom.classes.add_ = function(classes, args) {
- for(var rv = 0, i = 0;i < args.length;i++) {
- goog.array.contains(classes, args[i]) || (classes.push(args[i]), rv++)
+ for(var i = 0;i < args.length;i++) {
+ goog.array.contains(classes, args[i]) || classes.push(args[i])
}
- return rv == args.length
};
-goog.dom.classes.remove_ = function(classes, args) {
- for(var rv = 0, i = 0;i < classes.length;i++) {
- goog.array.contains(args, classes[i]) && (goog.array.splice(classes, i--, 1), rv++)
- }
- return rv == args.length
+goog.dom.classes.getDifference_ = function(arr1, arr2) {
+ return goog.array.filter(arr1, function(item) {
+ return!goog.array.contains(arr2, item)
+ })
};
goog.dom.classes.swap = function(element, fromClass, toClass) {
for(var classes = goog.dom.classes.get(element), removed = !1, i = 0;i < classes.length;i++) {
@@ -1424,7 +1428,7 @@
};
goog.dom.classes.addRemove = function(element, classesToRemove, classesToAdd) {
var classes = goog.dom.classes.get(element);
- goog.isString(classesToRemove) ? goog.array.remove(classes, classesToRemove) : goog.isArray(classesToRemove) && goog.dom.classes.remove_(classes, classesToRemove);
+ goog.isString(classesToRemove) ? goog.array.remove(classes, classesToRemove) : goog.isArray(classesToRemove) && (classes = goog.dom.classes.getDifference_(classes, classesToRemove));
goog.isString(classesToAdd) && !goog.array.contains(classes, classesToAdd) ? classes.push(classesToAdd) : goog.isArray(classesToAdd) && goog.dom.classes.add_(classes, classesToAdd);
element.className = classes.join(" ")
};
@@ -2013,13 +2017,16 @@
return!1
};
goog.dom.getAncestorByTagNameAndClass = function(element, opt_tag, opt_class) {
+ if(!opt_tag && !opt_class) {
+ return null
+ }
var tagName = opt_tag ? opt_tag.toUpperCase() : null;
return goog.dom.getAncestor(element, function(node) {
return(!tagName || node.nodeName == tagName) && (!opt_class || goog.dom.classes.has(node, opt_class))
}, !0)
};
-goog.dom.getAncestorByClass = function(element, opt_class) {
- return goog.dom.getAncestorByTagNameAndClass(element, null, opt_class)
+goog.dom.getAncestorByClass = function(element, className) {
+ return goog.dom.getAncestorByTagNameAndClass(element, null, className)
};
goog.dom.getAncestor = function(element, matcher, opt_includeNode, opt_maxSearchSteps) {
opt_includeNode || (element = element.parentNode);
@@ -3254,7 +3261,7 @@
};
goog.events = {};
goog.events.BrowserFeature = {HAS_W3C_BUTTON:!goog.userAgent.IE || goog.userAgent.isDocumentMode(9), HAS_W3C_EVENT_SUPPORT:!goog.userAgent.IE || goog.userAgent.isDocumentMode(9), SET_KEY_CODE_TO_PREVENT_DEFAULT:goog.userAgent.IE && !goog.userAgent.isVersion("8"), HAS_NAVIGATOR_ONLINE_PROPERTY:!goog.userAgent.WEBKIT || goog.userAgent.isVersion("528"), HAS_HTML5_NETWORK_EVENT_SUPPORT:goog.userAgent.GECKO && goog.userAgent.isVersion("1.9b") || goog.userAgent.IE && goog.userAgent.isVersion("8") || goog.userAgent.OPERA &&
-goog.userAgent.isVersion("9.5") || goog.userAgent.WEBKIT && goog.userAgent.isVersion("528"), HTML5_NETWORK_EVENTS_FIRE_ON_WINDOW:!goog.userAgent.GECKO || goog.userAgent.isVersion("8")};
+goog.userAgent.isVersion("9.5") || goog.userAgent.WEBKIT && goog.userAgent.isVersion("528"), HTML5_NETWORK_EVENTS_FIRE_ON_BODY:goog.userAgent.GECKO && !goog.userAgent.isVersion("8") || goog.userAgent.IE && !goog.userAgent.isVersion("9")};
goog.events.Event = function(type, opt_target) {
goog.Disposable.call(this);
this.type = type;
diff --git a/google/appengine/tools/dev_appserver.py b/google/appengine/tools/dev_appserver.py
index 9566ee1..05f2c04 100755
--- a/google/appengine/tools/dev_appserver.py
+++ b/google/appengine/tools/dev_appserver.py
@@ -40,7 +40,6 @@
import __builtin__
import BaseHTTPServer
-import Cookie
import base64
import cStringIO
import cgi
@@ -56,7 +55,6 @@
import mimetools
import mimetypes
import os
-import pdb
import select
import shutil
import simplejson
@@ -99,7 +97,6 @@
from google.appengine.api import app_logging
from google.appengine.api import blobstore
from google.appengine.api import croninfo
-from google.appengine.api import datastore_admin
from google.appengine.api import datastore_file_stub
from google.appengine.api import lib_config
from google.appengine.api import mail
@@ -613,6 +610,7 @@
def __init__(self,
config,
login_url,
+ module_manager,
url_matchers,
get_user_info=dev_appserver_login.GetUserInfo,
login_redirect=dev_appserver_login.LoginRedirect):
@@ -621,12 +619,15 @@
Args:
config: AppInfoExternal instance representing the parsed app.yaml file.
login_url: Relative URL which should be used for handling user logins.
+ module_manager: ModuleManager instance that is used to detect and reload
+ modules if the matched Dispatcher is dynamic.
url_matchers: Sequence of URLMatcher objects.
get_user_info: Used for dependency injection.
login_redirect: Used for dependency injection.
"""
self._config = config
self._login_url = login_url
+ self._module_manager = module_manager
self._url_matchers = tuple(url_matchers)
self._get_user_info = get_user_info
self._login_redirect = login_redirect
@@ -677,6 +678,15 @@
% (httplib.FORBIDDEN, email_addr))
else:
request.path = matched_path
+
+
+
+
+
+ if (not isinstance(dispatcher, FileDispatcher) and
+ self._module_manager.AreModuleFilesModified()):
+ self._module_manager.ResetModules()
+
forward_request = dispatcher.Dispatch(request,
outfile,
base_env_dict=base_env_dict)
@@ -2332,6 +2342,9 @@
self._modification_times = {}
+
+ self._dirty = True
+
@staticmethod
def GetModuleFile(module, is_file=os.path.isfile):
"""Helper method to try to determine modules source file.
@@ -2362,6 +2375,7 @@
Returns:
True if one or more files have been modified, False otherwise.
"""
+ self._dirty = True
for name, (mtime, fname) in self._modification_times.iteritems():
if name not in self._modules:
@@ -2381,6 +2395,9 @@
def UpdateModuleFileModificationTimes(self):
"""Records the current modification times of all monitored modules."""
+ if not self._dirty:
+ return
+
self._modification_times.clear()
for name, module in self._modules.items():
if not isinstance(module, types.ModuleType):
@@ -2395,6 +2412,8 @@
if e.errno not in FILE_MISSING_EXCEPTIONS:
raise e
+ self._dirty = False
+
def ResetModules(self):
"""Clear modules so that when request is run they are reloaded."""
lib_config._default_registry.reset()
@@ -2682,7 +2701,7 @@
static_caching=static_caching, default_partition=default_partition)
- if not from_cache or self.module_manager.AreModuleFilesModified():
+ if not from_cache:
self.module_manager.ResetModules()
@@ -2741,7 +2760,7 @@
user_agent=self.headers.get('user-agent'),
host=host_name)
- dispatcher = MatcherDispatcher(config, login_url,
+ dispatcher = MatcherDispatcher(config, login_url, self.module_manager,
[implicit_matcher, explicit_matcher])
@@ -3263,7 +3282,7 @@
if not multiprocess.GlobalProcess().MaybeConfigureRemoteDataApis():
- """Configures local versions of datastore, memcache, and taskqueue."""
+
apiproxy_stub_map.apiproxy = apiproxy_stub_map.APIProxyStubMap()
if use_sqlite:
@@ -3397,6 +3416,18 @@
apiproxy_stub_map.apiproxy.RegisterStub('system', system_service_stub)
+def TearDownStubs():
+ """Clean up any stubs that need cleanup."""
+
+ datastore_stub = apiproxy_stub_map.apiproxy.GetStub('datastore_v3')
+
+
+ if isinstance(datastore_stub, datastore_stub_util.BaseTransactionManager):
+ logging.info('Applying all pending transactions and saving the datastore')
+ datastore_stub.Flush()
+ datastore_stub.Write()
+
+
def CreateImplicitMatcher(
config,
module_dict,
diff --git a/google/appengine/tools/dev_appserver_blobstore.py b/google/appengine/tools/dev_appserver_blobstore.py
index a9ada20..eae2b47 100755
--- a/google/appengine/tools/dev_appserver_blobstore.py
+++ b/google/appengine/tools/dev_appserver_blobstore.py
@@ -38,16 +38,14 @@
import logging
import mimetools
import re
-import sys
from google.appengine.api import apiproxy_stub_map
from google.appengine.api import blobstore
from google.appengine.api import datastore
from google.appengine.api import datastore_errors
+from google.appengine.api.files import file_service_stub
from google.appengine.tools import dev_appserver_upload
-from webob import byterange
-
UPLOAD_URL_PATH = '_ah/upload/'
@@ -81,119 +79,12 @@
return apiproxy_stub_map.apiproxy.GetStub('blobstore').storage
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-_BYTESRANGE_IS_EXCLUSIVE = not hasattr(byterange.Range, 'serialize_bytes')
-
-if _BYTESRANGE_IS_EXCLUSIVE:
-
- ParseRange = byterange.Range.parse_bytes
-
- MakeContentRange = byterange.ContentRange
-
- def GetContentRangeStop(content_range):
- return content_range.stop
-
-
-
-
-
-
-
- _orig_is_content_range_valid = byterange._is_content_range_valid
-
- def _new_is_content_range_valid(start, stop, length, response=False):
- return _orig_is_content_range_valid(start, stop, length, False)
-
- def ParseContentRange(content_range_header):
-
-
-
- try:
- byterange._is_content_range_valid = _new_is_content_range_valid
- return byterange.ContentRange.parse(content_range_header)
- finally:
- byterange._is_content_range_valid = _orig_is_content_range_valid
-
-else:
-
- def ParseRange(range_header):
-
-
- original_stdout = sys.stdout
- sys.stdout = cStringIO.StringIO()
- try:
- parse_result = byterange.Range.parse_bytes(range_header)
- finally:
- sys.stdout = original_stdout
- if parse_result is None:
- return None
- else:
- ranges = []
- for start, end in parse_result[1]:
- if end is not None:
- end += 1
- ranges.append((start, end))
- return parse_result[0], ranges
-
- class _FixedContentRange(byterange.ContentRange):
-
-
-
-
-
-
-
- def __init__(self, start, stop, length):
-
- self.start = start
- self.stop = stop
- self.length = length
-
-
-
-
-
-
-
-
-
- def MakeContentRange(start, stop, length):
- if stop is not None:
- stop -= 2
- content_range = _FixedContentRange(start, stop, length)
- return content_range
-
- def GetContentRangeStop(content_range):
- stop = content_range.stop
- if stop is not None:
- stop += 2
- return stop
-
- def ParseContentRange(content_range_header):
- return _FixedContentRange.parse(content_range_header)
-
-
def ParseRangeHeader(range_header):
"""Parse HTTP Range header.
Args:
- range_header: Range header as retrived from Range or X-AppEngine-BlobRange.
+ range_header: A str representing the value of a range header as retrived
+ from Range or X-AppEngine-BlobRange.
Returns:
Tuple (start, end):
@@ -203,12 +94,107 @@
"""
if not range_header:
return None, None
- parsed_range = ParseRange(range_header)
- if parsed_range:
- range_tuple = parsed_range[1]
- if len(range_tuple) == 1:
- return range_tuple[0]
- return None, None
+ try:
+
+ range_type, ranges = range_header.split('=', 1)
+ if range_type != 'bytes':
+ return None, None
+ ranges = ranges.lstrip()
+ if ',' in ranges:
+ return None, None
+ end = None
+ if ranges.startswith('-'):
+ start = int(ranges)
+ if start == 0:
+ return None, None
+ else:
+ split_range = ranges.split('-', 1)
+ start = int(split_range[0])
+ if len(split_range) == 2 and split_range[1].strip():
+ end = int(split_range[1]) + 1
+ if start > end:
+ return None, None
+ return start, end
+ except ValueError:
+ return None, None
+
+
+def _GetGoogleStorageFileMetadata(blob_key):
+ """Retreive metadata about a GS blob from the blob_key.
+
+ Args:
+ blob_key: The BlobKey of the blob.
+
+ Returns:
+ Tuple (size, content_type, open_key):
+ size: The size of the blob.
+ content_type: The content type of the blob.
+ open_key: The key used as an argument to BlobStorage to open the blob
+ for reading.
+ (None, None, None) if the blob metadata was not found.
+ """
+ try:
+ gs_info = datastore.Get(
+ datastore.Key.from_path(file_service_stub.GS_INFO_KIND,
+ blob_key,
+ namespace=''))
+ return gs_info['size'], gs_info['content_type'], gs_info['storage_key']
+ except datastore_errors.EntityNotFoundError:
+ return None, None, None
+
+
+def _GetBlobstoreMetadata(blob_key):
+ """Retreive metadata about a blobstore blob from the blob_key.
+
+ Args:
+ blob_key: The BlobKey of the blob.
+
+ Returns:
+ Tuple (size, content_type, open_key):
+ size: The size of the blob.
+ content_type: The content type of the blob.
+ open_key: The key used as an argument to BlobStorage to open the blob
+ for reading.
+ (None, None, None) if the blob metadata was not found.
+ """
+ try:
+ blob_info = datastore.Get(
+ datastore.Key.from_path(blobstore.BLOB_INFO_KIND,
+ blob_key,
+ namespace=''))
+ return blob_info['size'], blob_info['content_type'], blob_key
+ except datastore_errors.EntityNotFoundError:
+ return None, None, None
+
+
+def _GetBlobMetadata(blob_key):
+ """Retrieve the metadata about a blob from the blob_key.
+
+ Args:
+ blob_key: The BlobKey of the blob.
+
+ Returns:
+ Tuple (size, content_type, open_key):
+ size: The size of the blob.
+ content_type: The content type of the blob.
+ open_key: The key used as an argument to BlobStorage to open the blob
+ for reading.
+ (None, None, None) if the blob metadata was not found.
+ """
+ size, content_type, open_key = _GetGoogleStorageFileMetadata(blob_key)
+ if size is None:
+ size, content_type, open_key = _GetBlobstoreMetadata(blob_key)
+ return size, content_type, open_key
+
+
+def _SetRangeRequestNotSatisfiable(response):
+ """Short circuit response and return 416 error."""
+ response.status_code = 416
+ response.status_message = 'Requested Range Not Satisfiable'
+ response.body = cStringIO.StringIO('')
+ response.headers['Content-Length'] = '0'
+ del response.headers['Content-Type']
+ del response.headers['Content-Range']
def DownloadRewriter(response, request_headers):
@@ -223,10 +209,9 @@
If the application itself provides a content-type header, it will override
the content-type stored in the action blob.
- If Content-Range header is provided, blob will be partially served. The
- application can set blobstore.BLOB_RANGE_HEADER if the size of the blob is
- not known. If Range is present, and not blobstore.BLOB_RANGE_HEADER, will
- use Range instead.
+ If blobstore.BLOB_RANGE_HEADER header is provided, blob will be partially
+ served. If Range is present, and not blobstore.BLOB_RANGE_HEADER, will use
+ Range instead.
Args:
response: Response object to be rewritten.
@@ -237,80 +222,53 @@
if blob_key:
del response.headers[blobstore.BLOB_KEY_HEADER]
+ blob_size, blob_content_type, blob_open_key = _GetBlobMetadata(blob_key)
- try:
- blob_info = datastore.Get(
- datastore.Key.from_path(blobstore.BLOB_INFO_KIND,
- blob_key,
- namespace=''))
- content_range_header = response.headers.getheader('Content-Range')
- blob_size = blob_info['size']
+ if blob_size is not None and blob_content_type is not None:
range_header = response.headers.getheader(blobstore.BLOB_RANGE_HEADER)
if range_header is not None:
del response.headers[blobstore.BLOB_RANGE_HEADER]
else:
range_header = request_headers.getheader('Range')
- def not_satisfiable():
- """Short circuit response and return 416 error."""
- response.status_code = 416
- response.status_message = 'Requested Range Not Satisfiable'
- response.body = cStringIO.StringIO('')
- response.headers['Content-Length'] = '0'
- del response.headers['Content-Type']
- del response.headers['Content-Range']
-
- if range_header:
- start, end = ParseRangeHeader(range_header)
- if start is not None:
- if end is None:
- if start >= 0:
- content_range_start = start
- else:
- content_range_start = blob_size + start
- content_range = MakeContentRange(
- content_range_start, blob_size, blob_size)
- content_range_header = str(content_range)
- else:
- content_range = MakeContentRange(start, min(end, blob_size),
- blob_size)
- content_range_header = str(content_range)
- response.headers['Content-Range'] = content_range_header
- else:
- not_satisfiable()
- return
-
- content_range_header = response.headers.getheader('Content-Range')
content_length = blob_size
start = 0
end = content_length
- if content_range_header is not None:
- content_range = ParseContentRange(content_range_header)
- if content_range:
- start = content_range.start
- stop = GetContentRangeStop(content_range)
- content_length = min(stop, blob_size) - start
- stop = start + content_length
- content_range = MakeContentRange(start, stop, blob_size)
- response.headers['Content-Range'] = str(content_range)
- else:
- not_satisfiable()
- return
- blob_stream = GetBlobStorage().OpenBlob(blob_key)
+ if range_header:
+ start, end = ParseRangeHeader(range_header)
+ if start is None:
+ _SetRangeRequestNotSatisfiable(response)
+ return
+ else:
+ if start < 0:
+ start = max(blob_size + start, 0)
+ elif start >= blob_size:
+ _SetRangeRequestNotSatisfiable(response)
+ return
+ if end is not None:
+ end = min(end, blob_size)
+ else:
+ end = blob_size
+ content_length = min(end, blob_size) - start
+ end = start + content_length
+ response.headers['Content-Range'] = 'bytes %d-%d/%d' % (
+ start, end - 1, blob_size)
+
+ blob_stream = GetBlobStorage().OpenBlob(blob_open_key)
blob_stream.seek(start)
response.body = cStringIO.StringIO(blob_stream.read(content_length))
response.headers['Content-Length'] = str(content_length)
content_type = response.headers.getheader('Content-Type')
if not content_type or content_type == AUTO_MIME_TYPE:
- response.headers['Content-Type'] = blob_info['content_type']
+ response.headers['Content-Type'] = blob_content_type
response.large_response = True
- except datastore_errors.EntityNotFoundError:
+ else:
response.status_code = 500
response.status_message = 'Internal Error'
diff --git a/google/appengine/tools/dev_appserver_import_hook.py b/google/appengine/tools/dev_appserver_import_hook.py
index 9bbc180..9948915 100644
--- a/google/appengine/tools/dev_appserver_import_hook.py
+++ b/google/appengine/tools/dev_appserver_import_hook.py
@@ -1235,6 +1235,11 @@
list(self._white_list_partial_modules['socket']) +
['getdefaulttimeout', 'setdefaulttimeout'])
+
+ webob_path = os.path.join(SDK_ROOT, 'lib', 'webob_1_1_1')
+ if webob_path not in sys.path:
+ sys.path.insert(1, webob_path)
+
for libentry in self._config.GetAllLibraries():
self._enabled_modules.append(libentry.name)
extra = self.__PY27_OPTIONAL_ALLOWED_MODULES.get(libentry.name)
diff --git a/google/appengine/tools/dev_appserver_main.py b/google/appengine/tools/dev_appserver_main.py
index fd31020..a8e22bf 100755
--- a/google/appengine/tools/dev_appserver_main.py
+++ b/google/appengine/tools/dev_appserver_main.py
@@ -162,7 +162,6 @@
-
DEFAULT_ADMIN_CONSOLE_SERVER = 'appengine.google.com'
@@ -575,7 +574,7 @@
default_partition = option_dict[ARG_DEFAULT_PARTITION]
appinfo = None
try:
- appinfo, matcher, _ = dev_appserver.LoadAppConfig(
+ appinfo, _, _ = dev_appserver.LoadAppConfig(
root_path, {}, default_partition=default_partition)
except yaml_errors.EventListenerError, e:
logging.error('Fatal error when loading application configuration:\n%s', e)
@@ -681,6 +680,7 @@
done = True
except KeyboardInterrupt:
pass
+ dev_appserver.TearDownStubs()
return 0
diff --git a/google/appengine/tools/dev_appserver_multiprocess.py b/google/appengine/tools/dev_appserver_multiprocess.py
index 0d22232..95fb610 100644
--- a/google/appengine/tools/dev_appserver_multiprocess.py
+++ b/google/appengine/tools/dev_appserver_multiprocess.py
@@ -396,9 +396,6 @@
self.backends = backends
self.options = options
- if not self.backends:
- raise Error('Entering multiprocess mode with no backends configured.')
-
self.app_id = appinfo.application
self.host = options[ARG_ADDRESS]
self.port = options[ARG_PORT]
@@ -1061,23 +1058,17 @@
if ARG_BACKENDS not in options:
return
- backends_fh = None
- try:
- backends_fh = open(os.path.join(root_path, 'backends.yaml'))
- except IOError:
- return
+ backends_path = os.path.join(root_path, 'backends.yaml')
+ if not os.path.exists(backends_path):
+ backends = []
+ else:
+ backends_fh = open(backends_path)
+ try:
+ backend_info = backendinfo.LoadBackendInfo(backends_fh.read())
+ finally:
+ backends_fh.close()
+ backends = backend_info.backends
- backend_info = None
- try:
- backend_info = backendinfo.LoadBackendInfo(backends_fh.read())
- finally:
- backends_fh.close()
-
- if not backend_info:
- logging.info('No backends, running in single-process mode.')
- return
-
- backends = backend_info.backends
backend_set = set()
for backend in backends:
if backend.name in backend_set:
diff --git a/google/net/proto2/proto/descriptor_pb2.py b/google/net/proto2/proto/descriptor_pb2.py
index 826f8b7..1757bc6 100755
--- a/google/net/proto2/proto/descriptor_pb2.py
+++ b/google/net/proto2/proto/descriptor_pb2.py
@@ -16,6 +16,7 @@
#
+
from google.net.proto2.python.public import descriptor
from google.net.proto2.python.public import message
from google.net.proto2.python.public import reflection
diff --git a/google/net/proto2/python/internal/containers.py b/google/net/proto2/python/internal/containers.py
index a2f938a..987112b 100755
--- a/google/net/proto2/python/internal/containers.py
+++ b/google/net/proto2/python/internal/containers.py
@@ -67,8 +67,13 @@
def __repr__(self):
return repr(self._values)
- def sort(self, sort_function=cmp):
- self._values.sort(sort_function)
+ def sort(self, *args, **kwargs):
+
+
+
+ if 'sort_function' in kwargs:
+ kwargs['cmp'] = kwargs.pop('sort_function')
+ self._values.sort(*args, **kwargs)
class RepeatedScalarFieldContainer(BaseContainer):
diff --git a/google/net/proto2/python/internal/decoder.py b/google/net/proto2/python/internal/decoder.py
index fb2017d..ba33f24 100755
--- a/google/net/proto2/python/internal/decoder.py
+++ b/google/net/proto2/python/internal/decoder.py
@@ -565,6 +565,7 @@
local_SkipField = SkipField
def DecodeItem(buffer, pos, end, message, field_dict):
+ message_set_item_start = pos
type_id = -1
message_start = -1
message_end = -1
@@ -603,6 +604,11 @@
raise _DecodeError('Unexpected end-group tag.')
+ else:
+ if not message._unknown_fields:
+ message._unknown_fields = []
+ message._unknown_fields.append((MESSAGE_SET_ITEM_TAG,
+ buffer[message_set_item_start:pos]))
return pos
diff --git a/google/net/proto2/python/internal/python_message.py b/google/net/proto2/python/internal/python_message.py
index 557dfc1..a2e364c 100755
--- a/google/net/proto2/python/internal/python_message.py
+++ b/google/net/proto2/python/internal/python_message.py
@@ -154,6 +154,7 @@
dictionary['__slots__'] = ['_cached_byte_size',
'_cached_byte_size_dirty',
'_fields',
+ '_unknown_fields',
'_is_present_in_parent',
'_listener',
'_listener_for_children',
@@ -280,6 +281,9 @@
self._cached_byte_size = 0
self._cached_byte_size_dirty = len(kwargs) > 0
self._fields = {}
+
+
+ self._unknown_fields = ()
self._is_present_in_parent = False
self._listener = message_listener_mod.NullMessageListener()
self._listener_for_children = _Listener(self)
@@ -618,6 +622,7 @@
def Clear(self):
self._fields = {}
+ self._unknown_fields = ()
self._Modified()
cls.Clear = Clear
@@ -647,7 +652,16 @@
if self is other:
return True
- return self.ListFields() == other.ListFields()
+ if not self.ListFields() == other.ListFields():
+ return False
+
+
+ unknown_fields = list(self._unknown_fields)
+ unknown_fields.sort()
+ other_unknown_fields = list(other._unknown_fields)
+ other_unknown_fields.sort()
+
+ return unknown_fields == other_unknown_fields
cls.__eq__ = __eq__
@@ -708,6 +722,9 @@
for field_descriptor, field_value in self.ListFields():
size += field_descriptor._sizer(field_value)
+ for tag_bytes, value_bytes in self._unknown_fields:
+ size += len(tag_bytes) + len(value_bytes)
+
self._cached_byte_size = size
self._cached_byte_size_dirty = False
self._listener_for_children.dirty = False
@@ -742,6 +759,9 @@
def InternalSerialize(self, write_bytes):
for field_descriptor, field_value in self.ListFields():
field_descriptor._encoder(write_bytes, field_value)
+ for tag_bytes, value_bytes in self._unknown_fields:
+ write_bytes(tag_bytes)
+ write_bytes(value_bytes)
cls._InternalSerialize = InternalSerialize
@@ -768,13 +788,18 @@
def InternalParse(self, buffer, pos, end):
self._Modified()
field_dict = self._fields
+ unknown_field_list = self._unknown_fields
while pos != end:
(tag_bytes, new_pos) = local_ReadTag(buffer, pos)
field_decoder = decoders_by_tag.get(tag_bytes)
if field_decoder is None:
+ value_start_pos = new_pos
new_pos = local_SkipField(buffer, new_pos, end, tag_bytes)
if new_pos == -1:
return pos
+ if not unknown_field_list:
+ unknown_field_list = self._unknown_fields = []
+ unknown_field_list.append((tag_bytes, buffer[value_start_pos:new_pos]))
pos = new_pos
else:
pos = field_decoder(buffer, new_pos, end, self, field_dict)
@@ -896,6 +921,12 @@
field_value.MergeFrom(value)
else:
self._fields[field] = value
+
+ if msg._unknown_fields:
+ if not self._unknown_fields:
+ self._unknown_fields = []
+ self._unknown_fields.extend(msg._unknown_fields)
+
cls.MergeFrom = MergeFrom
diff --git a/google/net/proto2/python/public/descriptor.py b/google/net/proto2/python/public/descriptor.py
index 5a1ee48..361da2b 100755
--- a/google/net/proto2/python/public/descriptor.py
+++ b/google/net/proto2/python/public/descriptor.py
@@ -374,7 +374,9 @@
TYPE_FLOAT: CPPTYPE_FLOAT,
TYPE_ENUM: CPPTYPE_ENUM,
TYPE_INT64: CPPTYPE_INT64,
+ TYPE_UINT64: CPPTYPE_UINT64,
TYPE_INT32: CPPTYPE_INT32,
+ TYPE_UINT32: CPPTYPE_UINT32,
TYPE_STRING: CPPTYPE_STRING,
TYPE_BOOL: CPPTYPE_BOOL,
TYPE_MESSAGE: CPPTYPE_MESSAGE
diff --git a/google/net/proto2/python/public/message.py b/google/net/proto2/python/public/message.py
index a408afd..912f3ac 100755
--- a/google/net/proto2/python/public/message.py
+++ b/google/net/proto2/python/public/message.py
@@ -62,6 +62,7 @@
return clone
def __eq__(self, other_msg):
+ """Recursively compares two messages by value and structure."""
raise NotImplementedError
def __ne__(self, other_msg):
@@ -72,9 +73,11 @@
raise TypeError('unhashable object')
def __str__(self):
+ """Outputs a human-readable representation of the message."""
raise NotImplementedError
def __unicode__(self):
+ """Outputs a human-readable representation of the message."""
raise NotImplementedError
def MergeFrom(self, other_msg):
diff --git a/google/storage/speckle/proto/client_error_code_pb2.py b/google/storage/speckle/proto/client_error_code_pb2.py
index 2cd328d..233cdcc 100644
--- a/google/storage/speckle/proto/client_error_code_pb2.py
+++ b/google/storage/speckle/proto/client_error_code_pb2.py
@@ -16,6 +16,7 @@
#
+
from google.net.proto2.python.public import descriptor
from google.net.proto2.python.public import message
from google.net.proto2.python.public import reflection
diff --git a/google/storage/speckle/proto/client_pb2.py b/google/storage/speckle/proto/client_pb2.py
index fe3d39a..58c3e34 100755
--- a/google/storage/speckle/proto/client_pb2.py
+++ b/google/storage/speckle/proto/client_pb2.py
@@ -16,6 +16,7 @@
#
+
from google.net.proto2.python.public import descriptor
from google.net.proto2.python.public import message
from google.net.proto2.python.public import reflection
diff --git a/google/storage/speckle/proto/sql_pb2.py b/google/storage/speckle/proto/sql_pb2.py
index be86035..8f9efaf 100755
--- a/google/storage/speckle/proto/sql_pb2.py
+++ b/google/storage/speckle/proto/sql_pb2.py
@@ -16,6 +16,7 @@
#
+
from google.net.proto2.python.public import descriptor
from google.net.proto2.python.public import message
from google.net.proto2.python.public import reflection
diff --git a/google_sql.py b/google_sql.py
index 0276d25..90e2c47 100755
--- a/google_sql.py
+++ b/google_sql.py
@@ -51,7 +51,7 @@
os.path.join(DIR_PATH, 'lib', 'jinja2'),
os.path.join(DIR_PATH, 'lib', 'protorpc'),
os.path.join(DIR_PATH, 'lib', 'markupsafe'),
- os.path.join(DIR_PATH, 'lib', 'webob'),
+ os.path.join(DIR_PATH, 'lib', 'webob_0_9'),
os.path.join(DIR_PATH, 'lib', 'webapp2'),
os.path.join(DIR_PATH, 'lib', 'yaml', 'lib'),
os.path.join(DIR_PATH, 'lib', 'simplejson'),
diff --git a/lib/cacerts/urlfetch_cacerts.txt b/lib/cacerts/urlfetch_cacerts.txt
index 38256ed..29c8da8 100755
--- a/lib/cacerts/urlfetch_cacerts.txt
+++ b/lib/cacerts/urlfetch_cacerts.txt
@@ -33,7 +33,7 @@
# the terms of any one of the MPL, the GPL or the LGPL.
#
# ***** END LICENSE BLOCK *****
-CVS_ID "@(#) $RCSfile: certdata.txt,v $ $Revision: 1.80 $ $Date: 2011/11/03 15:11:58 $"
+CVS_ID "@(#) $RCSfile: certdata.txt,v $ $Revision: 1.81 $ $Date: 2012/01/17 22:02:37 $"
subject= /C=US/O=GTE Corporation/OU=GTE CyberTrust Solutions, Inc./CN=GTE CyberTrust Global Root
serial=01A5
@@ -163,41 +163,6 @@
B3luHtgZg3Pe9T7Qtd7nS2h9Qy4qIOF+oHhEngj1mPnHfxsb1gYgAlihw6ID
-----END CERTIFICATE-----
-subject= /C=US/O=VeriSign, Inc./OU=Class 1 Public Primary Certification Authority
-serial=CDBA7F56F0DFE4BC54FE22ACB372AA55
------BEGIN CERTIFICATE-----
-MIICPTCCAaYCEQDNun9W8N/kvFT+IqyzcqpVMA0GCSqGSIb3DQEBAgUAMF8xCzAJ
-BgNVBAYTAlVTMRcwFQYDVQQKEw5WZXJpU2lnbiwgSW5jLjE3MDUGA1UECxMuQ2xh
-c3MgMSBQdWJsaWMgUHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw05
-NjAxMjkwMDAwMDBaFw0yODA4MDEyMzU5NTlaMF8xCzAJBgNVBAYTAlVTMRcwFQYD
-VQQKEw5WZXJpU2lnbiwgSW5jLjE3MDUGA1UECxMuQ2xhc3MgMSBQdWJsaWMgUHJp
-bWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTCBnzANBgkqhkiG9w0BAQEFAAOB
-jQAwgYkCgYEA5Rm/baNWYS2ZSHH2Z965jeu3noaACpEO+jglr0aIguVzqKCbJF0N
-H8xlbgyw0FaEGIeaBpsQoXPftFg5a27B9hXVqKg/qhIGjTGsf7A01480Z4gJzRQR
-4k5FVmkfeAKA2txHkSm7NsljXMXg1y2He6G3MrB7MLoqLzGq7qNn2tsCAwEAATAN
-BgkqhkiG9w0BAQIFAAOBgQBMP7iLxmjf7kMzDl3ppssHhE16M/+SG/Q2rdiVIjZo
-EWx8QszznC7EBz8UsA9P/5CSdvnivErpj82ggAr3xSnxgiJduLHdgSOjeyUVRjB5
-FvjqBUuUfx3CHMjjt/QQQDwTw18fU+hI5Ia0e6E1sHslurjTjqs/OJ0ANACY89Fx
-lA==
------END CERTIFICATE-----
-
-subject= /C=US/O=VeriSign, Inc./OU=Class 2 Public Primary Certification Authority
-serial=2D1BFC4A178DA391EBE7FFF58B45BE0B
------BEGIN CERTIFICATE-----
-MIICPDCCAaUCEC0b/EoXjaOR6+f/9YtFvgswDQYJKoZIhvcNAQECBQAwXzELMAkG
-A1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMTcwNQYDVQQLEy5DbGFz
-cyAyIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTk2
-MDEyOTAwMDAwMFoXDTI4MDgwMTIzNTk1OVowXzELMAkGA1UEBhMCVVMxFzAVBgNV
-BAoTDlZlcmlTaWduLCBJbmMuMTcwNQYDVQQLEy5DbGFzcyAyIFB1YmxpYyBQcmlt
-YXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIGfMA0GCSqGSIb3DQEBAQUAA4GN
-ADCBiQKBgQC2WoujDWojg4BrzzmH9CETMwZMJaLtVRKXxaeAufqDwSCg+i8VDXyh
-YGt+eSz6Bg86rvYbb7HS/y8oUl+DfUvEerf4Zh+AVPy3wo5ZShRXRtGak75BkQO7
-FYCTXOvnzAhsPz6zSvz/S2wj1VCCJkQZjiPDceoZJEcEnnW/yKYAHwIDAQABMA0G
-CSqGSIb3DQEBAgUAA4GBAIobK/o5wXTXXtgZZKJYSi034DNHD6zt96rbHuSLBlxg
-J8pFUs4W7z8GZOeUaHxgMxURaa+dYo2jA1Rrpr7l7gUYYAS/QoD90KioHgE796Nc
-r6Pc5iaAIzy4RHT3Cq5Ji2F4zCS/iIqnDupzGUH9TQPwiNHleI2lKk/2lw0Xd8rY
------END CERTIFICATE-----
-
subject= /C=US/O=VeriSign, Inc./OU=Class 3 Public Primary Certification Authority
serial=70BAE41D10D92934B638CA7B03CCBABF
-----BEGIN CERTIFICATE-----
@@ -215,50 +180,6 @@
AA9WjQKZ7aKQRUzkuxCkPfAyAw7xzvjoyVGM5mKf5p/AfbdynMk2OmufTqj/ZA1k
-----END CERTIFICATE-----
-subject= /C=US/O=VeriSign, Inc./OU=Class 1 Public Primary Certification Authority - G2/OU=(c) 1998 VeriSign, Inc. - For authorized use only/OU=VeriSign Trust Network
-serial=4CC7EAAA983E71D39310F83D3A899192
------BEGIN CERTIFICATE-----
-MIIDAjCCAmsCEEzH6qqYPnHTkxD4PTqJkZIwDQYJKoZIhvcNAQEFBQAwgcExCzAJ
-BgNVBAYTAlVTMRcwFQYDVQQKEw5WZXJpU2lnbiwgSW5jLjE8MDoGA1UECxMzQ2xh
-c3MgMSBQdWJsaWMgUHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEcy
-MTowOAYDVQQLEzEoYykgMTk5OCBWZXJpU2lnbiwgSW5jLiAtIEZvciBhdXRob3Jp
-emVkIHVzZSBvbmx5MR8wHQYDVQQLExZWZXJpU2lnbiBUcnVzdCBOZXR3b3JrMB4X
-DTk4MDUxODAwMDAwMFoXDTI4MDgwMTIzNTk1OVowgcExCzAJBgNVBAYTAlVTMRcw
-FQYDVQQKEw5WZXJpU2lnbiwgSW5jLjE8MDoGA1UECxMzQ2xhc3MgMSBQdWJsaWMg
-UHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEcyMTowOAYDVQQLEzEo
-YykgMTk5OCBWZXJpU2lnbiwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5
-MR8wHQYDVQQLExZWZXJpU2lnbiBUcnVzdCBOZXR3b3JrMIGfMA0GCSqGSIb3DQEB
-AQUAA4GNADCBiQKBgQCq0Lq+Fi24g9TK0g+8djHKlNgdk4xWArzZbxpvUjZudVYK
-VdPfQ4chEWWKfo+9Id5rMj8bhDSVBZ1BNeuS65bdqlk/AVNtmU/t5eIqWpDBucSm
-Fc/IReumXY6cPvBkJHalzasab7bYe1FhbqZ/h8jit+U03EGI6glAvnOSPWvndQID
-AQABMA0GCSqGSIb3DQEBBQUAA4GBAKlPww3HZ74sy9mozS11534Vnjty637rXC0J
-h9ZrbWB85a7FkCMMXErQr7Fd88e2CtvgFZMN3QO8x3aKtd1Pw5sTdbgBwObJW2ul
-uIncrKTdcu1OofdPvAbT6shkdHvClUGcZXNY8ZCaPGqxmMnEh7zPRW1F4m4iP/68
-DzFc6PLZ
------END CERTIFICATE-----
-
-subject= /C=US/O=VeriSign, Inc./OU=Class 2 Public Primary Certification Authority - G2/OU=(c) 1998 VeriSign, Inc. - For authorized use only/OU=VeriSign Trust Network
-serial=B92F60CC889FA17A4609B85B706C8AAF
------BEGIN CERTIFICATE-----
-MIIDAzCCAmwCEQC5L2DMiJ+hekYJuFtwbIqvMA0GCSqGSIb3DQEBBQUAMIHBMQsw
-CQYDVQQGEwJVUzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xPDA6BgNVBAsTM0Ns
-YXNzIDIgUHVibGljIFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgLSBH
-MjE6MDgGA1UECxMxKGMpIDE5OTggVmVyaVNpZ24sIEluYy4gLSBGb3IgYXV0aG9y
-aXplZCB1c2Ugb25seTEfMB0GA1UECxMWVmVyaVNpZ24gVHJ1c3QgTmV0d29yazAe
-Fw05ODA1MTgwMDAwMDBaFw0yODA4MDEyMzU5NTlaMIHBMQswCQYDVQQGEwJVUzEX
-MBUGA1UEChMOVmVyaVNpZ24sIEluYy4xPDA6BgNVBAsTM0NsYXNzIDIgUHVibGlj
-IFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgLSBHMjE6MDgGA1UECxMx
-KGMpIDE5OTggVmVyaVNpZ24sIEluYy4gLSBGb3IgYXV0aG9yaXplZCB1c2Ugb25s
-eTEfMB0GA1UECxMWVmVyaVNpZ24gVHJ1c3QgTmV0d29yazCBnzANBgkqhkiG9w0B
-AQEFAAOBjQAwgYkCgYEAp4gBIXQs5xoD8JjhlzwPIQjxnNuX6Zr8wgQGE75fUsjM
-HiwSViy4AWkszJkfrbCWrnkE8hM5wXuYuggs6MKEEyyqaekJ9MepAqRCwiNPStjw
-DqL7MWzJ5m+ZJwf15vRMeJ5t60aG+rmGyVTyssSv1EYcWskVMP8NbPUtDm3Of3cC
-AwEAATANBgkqhkiG9w0BAQUFAAOBgQByLvl/0fFx+8Se9sVeUYpAmLho+Jscg9ji
-nb3/7aHmZuovCfTK1+qlK5X2JGCGTUQug6XELaDTrnhpb3LabK4I8GOSN+a7xDAX
-rXfMSTWqz9iP0b63GJZHc2pUIjRkLbYWm1lbtFFZOrMLFPQS32eg9K0yZF6xRnIn
-jBJ7xUS0rg==
------END CERTIFICATE-----
-
subject= /C=US/O=VeriSign, Inc./OU=Class 3 Public Primary Certification Authority - G2/OU=(c) 1998 VeriSign, Inc. - For authorized use only/OU=VeriSign Trust Network
serial=7DD9FE07CFA81EB7107967FBA78934C6
-----BEGIN CERTIFICATE-----
@@ -281,28 +202,6 @@
oJ2daZH9
-----END CERTIFICATE-----
-subject= /C=US/O=VeriSign, Inc./OU=Class 4 Public Primary Certification Authority - G2/OU=(c) 1998 VeriSign, Inc. - For authorized use only/OU=VeriSign Trust Network
-serial=32888E9AD2F5EB1347F87FC4203725F8
------BEGIN CERTIFICATE-----
-MIIDAjCCAmsCEDKIjprS9esTR/h/xCA3JfgwDQYJKoZIhvcNAQEFBQAwgcExCzAJ
-BgNVBAYTAlVTMRcwFQYDVQQKEw5WZXJpU2lnbiwgSW5jLjE8MDoGA1UECxMzQ2xh
-c3MgNCBQdWJsaWMgUHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEcy
-MTowOAYDVQQLEzEoYykgMTk5OCBWZXJpU2lnbiwgSW5jLiAtIEZvciBhdXRob3Jp
-emVkIHVzZSBvbmx5MR8wHQYDVQQLExZWZXJpU2lnbiBUcnVzdCBOZXR3b3JrMB4X
-DTk4MDUxODAwMDAwMFoXDTI4MDgwMTIzNTk1OVowgcExCzAJBgNVBAYTAlVTMRcw
-FQYDVQQKEw5WZXJpU2lnbiwgSW5jLjE8MDoGA1UECxMzQ2xhc3MgNCBQdWJsaWMg
-UHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEcyMTowOAYDVQQLEzEo
-YykgMTk5OCBWZXJpU2lnbiwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5
-MR8wHQYDVQQLExZWZXJpU2lnbiBUcnVzdCBOZXR3b3JrMIGfMA0GCSqGSIb3DQEB
-AQUAA4GNADCBiQKBgQC68OTP+cSuhVS5B1f5j8V/aBH4xBewRNzjMHPVKmIquNDM
-HO0oW369atyzkSTKQWI8/AIBvxwWMZQFl3Zuoq29YRdsTjCG8FE3KlDHqGKB3FtK
-qsGgtG7rL+VXxbErQHDbWk2hjh+9Ax/YA9SPTJlxvOKCzFjomDqG04Y48wApHwID
-AQABMA0GCSqGSIb3DQEBBQUAA4GBAIWMEsGnuVAVess+rLhDityq3RS6iYF+ATwj
-cSGIL4LcY/oCRaxFWdcqWERbt5+BO5JoPeI3JPV7bI92NZYJqFmduc4jq3TWg/0y
-cyfYaT5DdPauxYma51N86Xv2S/PBZYPejYqcPIiNOVn8qj8ijaHBZlCBckztImRP
-T8qAkbYp
------END CERTIFICATE-----
-
subject= /C=BE/O=GlobalSign nv-sa/OU=Root CA/CN=GlobalSign Root CA
serial=040000000001154B5AC394
-----BEGIN CERTIFICATE-----
@@ -415,60 +314,6 @@
PhmcGcwTTYJBtYze4D1gCCAPRX5ron+jjBXu
-----END CERTIFICATE-----
-subject= /C=US/O=VeriSign, Inc./OU=VeriSign Trust Network/OU=(c) 1999 VeriSign, Inc. - For authorized use only/CN=VeriSign Class 1 Public Primary Certification Authority - G3
-serial=8B5B75568454850B00CFAF3848CEB1A4
------BEGIN CERTIFICATE-----
-MIIEGjCCAwICEQCLW3VWhFSFCwDPrzhIzrGkMA0GCSqGSIb3DQEBBQUAMIHKMQsw
-CQYDVQQGEwJVUzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZl
-cmlTaWduIFRydXN0IE5ldHdvcmsxOjA4BgNVBAsTMShjKSAxOTk5IFZlcmlTaWdu
-LCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxRTBDBgNVBAMTPFZlcmlT
-aWduIENsYXNzIDEgUHVibGljIFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3Jp
-dHkgLSBHMzAeFw05OTEwMDEwMDAwMDBaFw0zNjA3MTYyMzU5NTlaMIHKMQswCQYD
-VQQGEwJVUzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZlcmlT
-aWduIFRydXN0IE5ldHdvcmsxOjA4BgNVBAsTMShjKSAxOTk5IFZlcmlTaWduLCBJ
-bmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxRTBDBgNVBAMTPFZlcmlTaWdu
-IENsYXNzIDEgUHVibGljIFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkg
-LSBHMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAN2E1Lm0+afY8wR4
-nN493GwTFtl63SRRZsDHJlkNrAYIwpTRMx/wgzUfbhvI3qpuFU5UJ+/EbRrsC+MO
-8ESlV8dAWB6jRx9x7GD2bZTIGDnt/kIYVt/kTEkQeE4BdjVjEjbdZrwBBDajVWjV
-ojYJrKshJlQGrT/KFOCsyq0GHZXi+J3x4GD/wn91K0zM2v6HmSHquv4+VNfSWXjb
-PG7PoBMAGrgnoeS+Z5bKoMWznN3JdZ7rMJpfo83ZrngZPyPpXNspva1VyBtUjGP2
-6KbqxzcSXKMpHgLZ2x87tNcPVkeBFQRKr4Mn0cVYiMHd9qqnoxjaaKptEVHhv2Vr
-n5Z20T0CAwEAATANBgkqhkiG9w0BAQUFAAOCAQEAq2aN17O6x5q25lXQBfGfMY1a
-qtmqRiYPce2lrVNWYgFHKkTp/j90CxObufRNG7LRX7K20ohcs5/Ny9Sn2WCVhDr4
-wTcdYcrnsMXlkdpUpqwxga6X3s0IrLjAl4B/bnKk52kTlWUfxJM8/XmPBNQ+T+r3
-ns7NZ3xPZQL/kYVUc8f/NveGLezQXk//EZ9yBta4GvFMDSZl4kSAHsef493oCtrs
-pSCAaWihT37ha88HQfqDjrw43bAuEbFrskLMmrz5SCJ5ShkPshw+IHTZasO+8ih4
-E1Z5T21Q6huwtVexN2ZYI/PcD98Kh8TvhgXVOBRgmaNL3gaWcSzy27YfpO8/7g==
------END CERTIFICATE-----
-
-subject= /C=US/O=VeriSign, Inc./OU=VeriSign Trust Network/OU=(c) 1999 VeriSign, Inc. - For authorized use only/CN=VeriSign Class 2 Public Primary Certification Authority - G3
-serial=6170CB498C5F984529E7B0A6D9505B7A
------BEGIN CERTIFICATE-----
-MIIEGTCCAwECEGFwy0mMX5hFKeewptlQW3owDQYJKoZIhvcNAQEFBQAwgcoxCzAJ
-BgNVBAYTAlVTMRcwFQYDVQQKEw5WZXJpU2lnbiwgSW5jLjEfMB0GA1UECxMWVmVy
-aVNpZ24gVHJ1c3QgTmV0d29yazE6MDgGA1UECxMxKGMpIDE5OTkgVmVyaVNpZ24s
-IEluYy4gLSBGb3IgYXV0aG9yaXplZCB1c2Ugb25seTFFMEMGA1UEAxM8VmVyaVNp
-Z24gQ2xhc3MgMiBQdWJsaWMgUHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0
-eSAtIEczMB4XDTk5MTAwMTAwMDAwMFoXDTM2MDcxNjIzNTk1OVowgcoxCzAJBgNV
-BAYTAlVTMRcwFQYDVQQKEw5WZXJpU2lnbiwgSW5jLjEfMB0GA1UECxMWVmVyaVNp
-Z24gVHJ1c3QgTmV0d29yazE6MDgGA1UECxMxKGMpIDE5OTkgVmVyaVNpZ24sIElu
-Yy4gLSBGb3IgYXV0aG9yaXplZCB1c2Ugb25seTFFMEMGA1UEAxM8VmVyaVNpZ24g
-Q2xhc3MgMiBQdWJsaWMgUHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAt
-IEczMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArwoNwtUs22e5LeWU
-J92lvuCwTY+zYVY81nzD9M0+hsuiiOLh2KRpxbXiv8GmR1BeRjmL1Za6tW8UvxDO
-JxOeBUebMXoT2B/Z0wI3i60sR/COgQanDTAM6/c8DyAd3HJG7qUCyFvDyVZpTMUY
-wZF7C9UTAJu878NIPkZgIIUq1ZC2zYugzDLdt/1AVbJQHFauzI13TccgTacxdu9o
-koqQHgiBVrKtaaNS0MscxCM9H5n+TOgWY47GCI72MfbS+uV23bUckqNJzc0BzWjN
-qWm6o+sdDZykIKbBoMXRRkwXbdKsZj+WjOCE1Db/IlnF+RFgqF8EffIa9iVCYQ/E
-Srg+iQIDAQABMA0GCSqGSIb3DQEBBQUAA4IBAQA0JhU8wI1NQ0kdvekhktdmnLfe
-xbjQ5F1fdiLAJvmEOjr5jLX77GDx6M4EsMjdpwOPMPOY36TmpDHf0xwLRtxyID+u
-7gU8pDM/CzmscHhzS5kr3zDCVLCoO1Wh/hYozUK9dG6A2ydEp85EXdQbkJgNHkKU
-sQAsBNB0owIFImNjzYO1+8FtYmtpdf1dcEG59b98377BMnMiIYtYgXsVkXq642RI
-sH/7NiXaldDxJBQX3RiAa0YjOVT1jmIJBB2UkKab5iXiQkWquJCtvgiPqQtCGJTP
-cjnhsUPgKM+351psE2tJs//jGHyJizNdrDPXp/naOlXJWBD5qu9ats9LS98q
------END CERTIFICATE-----
-
subject= /C=US/O=VeriSign, Inc./OU=VeriSign Trust Network/OU=(c) 1999 VeriSign, Inc. - For authorized use only/CN=VeriSign Class 3 Public Primary Certification Authority - G3
serial=9B7E0649A33E62B9D5EE90487129EF57
-----BEGIN CERTIFICATE-----
@@ -946,35 +791,6 @@
QMAJKOSLakhT2+zNVVXxxvjpoixMptEmX36vWkzaH6byHCx+rgIW0lbQL1dTR+iS
-----END CERTIFICATE-----
-subject= /C=US/ST=UT/L=Salt Lake City/O=The USERTRUST Network/OU=http://www.usertrust.com/CN=UTN-USERFirst-Network Applications
-serial=44BE0C8B500024B411D336304BC03377
------BEGIN CERTIFICATE-----
-MIIEZDCCA0ygAwIBAgIQRL4Mi1AAJLQR0zYwS8AzdzANBgkqhkiG9w0BAQUFADCB
-ozELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAlVUMRcwFQYDVQQHEw5TYWx0IExha2Ug
-Q2l0eTEeMBwGA1UEChMVVGhlIFVTRVJUUlVTVCBOZXR3b3JrMSEwHwYDVQQLExho
-dHRwOi8vd3d3LnVzZXJ0cnVzdC5jb20xKzApBgNVBAMTIlVUTi1VU0VSRmlyc3Qt
-TmV0d29yayBBcHBsaWNhdGlvbnMwHhcNOTkwNzA5MTg0ODM5WhcNMTkwNzA5MTg1
-NzQ5WjCBozELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAlVUMRcwFQYDVQQHEw5TYWx0
-IExha2UgQ2l0eTEeMBwGA1UEChMVVGhlIFVTRVJUUlVTVCBOZXR3b3JrMSEwHwYD
-VQQLExhodHRwOi8vd3d3LnVzZXJ0cnVzdC5jb20xKzApBgNVBAMTIlVUTi1VU0VS
-Rmlyc3QtTmV0d29yayBBcHBsaWNhdGlvbnMwggEiMA0GCSqGSIb3DQEBAQUAA4IB
-DwAwggEKAoIBAQCz+5Gh5DZVhawGNFugmliy+LUPBXeDrjKxdpJo7CNKyXY/45y2
-N3kDuatpjQclthln5LAbGHNhSuh+zdMvZOOmfAz6F4CjDUeJT1FxL+78P/m4FoCH
-iZMlIJpDgmkkdihZNaEdwH+DBmQWICzTSaSFtMBhf1EI+GgVkYDLpdXuOzr0hARe
-YFmnjDRy7rh4xdE7EkpvfmUnuaRVxblvQ6TFHSyZwFKkeEwVs0CYCGtDxgGwenv1
-axwiP8vv/6jQOkt2FZ7S0cYu49tXGzKiuG/ohqY/cKvlcJKrRB5AUPuco2LkbG6g
-yN7igEL66S/ozjIEj3yNtxyjNTwV3Z7DrpelAgMBAAGjgZEwgY4wCwYDVR0PBAQD
-AgHGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFPqGydvguul49Uuo1hXf8NPh
-ahQ8ME8GA1UdHwRIMEYwRKBCoECGPmh0dHA6Ly9jcmwudXNlcnRydXN0LmNvbS9V
-VE4tVVNFUkZpcnN0LU5ldHdvcmtBcHBsaWNhdGlvbnMuY3JsMA0GCSqGSIb3DQEB
-BQUAA4IBAQCk8yXM0dSRgyLQzDKrm5ZONJFUICU0YV8qAhXhi6r/fWRRzwr/vH3Y
-IWp4yy9Rb/hCHTO967V7lMPDqaAt39EpHx3+jz+7qEUqf9FuVSTiuwL7MT++6Lzs
-QCv4AdRWOOTKRIK1YSAhZ2X28AvnNPilwpyjXEAfhZOVBt5P1CeptqX8Fs1zMT+4
-ZSfP1FMa8Kxun08FDAOBp4QpxFq9ZFdyrTvPNximmMatBrTcCKME1SmklpoSZ0qM
-YEWd8SOasACcaLWYUNPvji6SZbFIPiG+FTAqDbUMo2s/rn9X9R+WfN9v3YIwLGUb
-QErNaLly7HF27FSOH4UMAWr6pjisH8SE
------END CERTIFICATE-----
-
subject= /C=US/O=America Online Inc./CN=America Online Root Certification Authority 1
serial=01
-----BEGIN CERTIFICATE-----
@@ -1304,28 +1120,6 @@
RSflMMFe8toTyyVCUZVHA4xsIcx0Qu1T/zOLjw9XARYvz6buyXAiFL39vmwLAw==
-----END CERTIFICATE-----
-subject= /C=FI/O=Sonera/CN=Sonera Class1 CA
-serial=24
------BEGIN CERTIFICATE-----
-MIIDIDCCAgigAwIBAgIBJDANBgkqhkiG9w0BAQUFADA5MQswCQYDVQQGEwJGSTEP
-MA0GA1UEChMGU29uZXJhMRkwFwYDVQQDExBTb25lcmEgQ2xhc3MxIENBMB4XDTAx
-MDQwNjEwNDkxM1oXDTIxMDQwNjEwNDkxM1owOTELMAkGA1UEBhMCRkkxDzANBgNV
-BAoTBlNvbmVyYTEZMBcGA1UEAxMQU29uZXJhIENsYXNzMSBDQTCCASIwDQYJKoZI
-hvcNAQEBBQADggEPADCCAQoCggEBALWJHytPZwp5/8Ue+H887dF+2rDNbS82rDTG
-29lkFwhjMDMiikzujrsPDUJVyZ0upe/3p4zDq7mXy47vPxVnqIJyY1MPQYx9EJUk
-oVqlBvqSV536pQHydekfvFYmUk54GWVYVQNYwBSujHxVX3BbdyMGNpfzJLWaRpXk
-3w0LBUXl0fIdgrvGE+D+qnr9aTCU89JFhfzyMlsy3uhsXR/LpCJ0sICOXZT3BgBL
-qdReLjVQCfOAl/QMF6452F/NM8EcyonCIvdFEu1eEpOdY6uCLrnrQkFEy0oaAIIN
-nvmLVz5MxxftLItyM19yejhW1ebZrgUaHXVFsculJRwSVzb9IjcCAwEAAaMzMDEw
-DwYDVR0TAQH/BAUwAwEB/zARBgNVHQ4ECgQIR+IMi/ZTiFIwCwYDVR0PBAQDAgEG
-MA0GCSqGSIb3DQEBBQUAA4IBAQCLGrLJXWG04bkruVPRsoWdd44W7hE928Jj2VuX
-ZfsSZ9gqXLar5V7DtxYvyOirHYr9qxp81V9jz9yw3Xe5qObSIjiHBxTZ/75Wtf0H
-DjxVyhbMp6Z3N/vbXB9OWQaHowND9Rart4S9Tu+fMTfwRvFAttEMpWT4Y14h21VO
-TzF2nBBhjrZTOqMRvq9tfB69ri3iDGnHhVNoomG6xT60eVR4ngrHAr5i0RGCS2Uv
-kVrCqIexVmiUefkl98HVrhq4uz2PqYo4Ffdz0Fpg0YCw8NzVUM1O7pJIae2yIx4w
-zMiUyLb1O4Z/P6Yun/Y+LLWSlj7fLJOK/4GMDw9ZIRlXvVWa
------END CERTIFICATE-----
-
subject= /C=FI/O=Sonera/CN=Sonera Class2 CA
serial=1D
-----BEGIN CERTIFICATE-----
@@ -1401,39 +1195,6 @@
aQNiuJkFBT1reBK9sG9l
-----END CERTIFICATE-----
-subject= /C=DK/O=TDC/CN=TDC OCES CA
-serial=3E48BDC4
------BEGIN CERTIFICATE-----
-MIIFGTCCBAGgAwIBAgIEPki9xDANBgkqhkiG9w0BAQUFADAxMQswCQYDVQQGEwJE
-SzEMMAoGA1UEChMDVERDMRQwEgYDVQQDEwtUREMgT0NFUyBDQTAeFw0wMzAyMTEw
-ODM5MzBaFw0zNzAyMTEwOTA5MzBaMDExCzAJBgNVBAYTAkRLMQwwCgYDVQQKEwNU
-REMxFDASBgNVBAMTC1REQyBPQ0VTIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A
-MIIBCgKCAQEArGL2YSCyz8DGhdfjeebM7fI5kqSXLmSjhFuHnEz9pPPEXyG9VhDr
-2y5h7JNp46PMvZnDBfwGuMo2HP6QjklMxFaaL1a8z3sM8W9Hpg1DTeLpHTk0zY0s
-2RKY+ePhwUp8hjjEqcRhiNJerxomTdXkoCJHhNlktxmW/OwZ5LKXJk5KTMuPJItU
-GBxIYXvViGjaXbXqzRowwYCDdlCqT9HU3Tjw7xb04QxQBr/q+3pJoSgrHPb8FTKj
-dGqPqcNiKXEx5TukYBdedObaE+3pHx8b0bJoc8YQNHVGEBDjkAB2QMuLt0MJIf+r
-TpPGWOmlgtt3xDqZsXKVSQTwtyv6e1mO3QIDAQABo4ICNzCCAjMwDwYDVR0TAQH/
-BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwgewGA1UdIASB5DCB4TCB3gYIKoFQgSkB
-AQEwgdEwLwYIKwYBBQUHAgEWI2h0dHA6Ly93d3cuY2VydGlmaWthdC5kay9yZXBv
-c2l0b3J5MIGdBggrBgEFBQcCAjCBkDAKFgNUREMwAwIBARqBgUNlcnRpZmlrYXRl
-ciBmcmEgZGVubmUgQ0EgdWRzdGVkZXMgdW5kZXIgT0lEIDEuMi4yMDguMTY5LjEu
-MS4xLiBDZXJ0aWZpY2F0ZXMgZnJvbSB0aGlzIENBIGFyZSBpc3N1ZWQgdW5kZXIg
-T0lEIDEuMi4yMDguMTY5LjEuMS4xLjARBglghkgBhvhCAQEEBAMCAAcwgYEGA1Ud
-HwR6MHgwSKBGoESkQjBAMQswCQYDVQQGEwJESzEMMAoGA1UEChMDVERDMRQwEgYD
-VQQDEwtUREMgT0NFUyBDQTENMAsGA1UEAxMEQ1JMMTAsoCqgKIYmaHR0cDovL2Ny
-bC5vY2VzLmNlcnRpZmlrYXQuZGsvb2Nlcy5jcmwwKwYDVR0QBCQwIoAPMjAwMzAy
-MTEwODM5MzBagQ8yMDM3MDIxMTA5MDkzMFowHwYDVR0jBBgwFoAUYLWF7FZkfhIZ
-J2cdUBVLc647+RIwHQYDVR0OBBYEFGC1hexWZH4SGSdnHVAVS3OuO/kSMB0GCSqG
-SIb2fQdBAAQQMA4bCFY2LjA6NC4wAwIEkDANBgkqhkiG9w0BAQUFAAOCAQEACrom
-JkbTc6gJ82sLMJn9iuFXehHTuJTXCRBuo7E4A9G28kNBKWKnctj7fAXmMXAnVBhO
-inxO5dHKjHiIzxvTkIvmI/gLDjNDfZziChmPyQE+dF10yYscA+UYyAFMP8uXBV2Y
-caaYb7Z8vTd/vuGTJW1v8AqtFxjhA7wHKcitJuj4YfD9IQl+mo6paH1IYnK9AOoB
-mbgGglGBTvH1tJFUuSN6AJqfXY3gPGS5GhKSKseCRHI53OI8xthV9RVOyAUO28bQ
-YqbsFbS1AoLbrIyigfCbmTH1ICCoiGEKB5+U/NDXG8wuF/MEJ3Zn61SD/aSQfgY9
-BKNDLdr8C2LqL19iUw==
------END CERTIFICATE-----
-
subject= /C=US/ST=UT/L=Salt Lake City/O=The USERTRUST Network/OU=http://www.usertrust.com/CN=UTN - DATACorp SGC
serial=44BE0C8B500021B411D32A6806A9AD69
-----BEGIN CERTIFICATE-----
@@ -1463,36 +1224,6 @@
mfnGV/TJVTl4uix5yaaIK/QI
-----END CERTIFICATE-----
-subject= /C=US/ST=UT/L=Salt Lake City/O=The USERTRUST Network/OU=http://www.usertrust.com/CN=UTN-USERFirst-Client Authentication and Email
-serial=44BE0C8B500024B411D336252567C989
------BEGIN CERTIFICATE-----
-MIIEojCCA4qgAwIBAgIQRL4Mi1AAJLQR0zYlJWfJiTANBgkqhkiG9w0BAQUFADCB
-rjELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAlVUMRcwFQYDVQQHEw5TYWx0IExha2Ug
-Q2l0eTEeMBwGA1UEChMVVGhlIFVTRVJUUlVTVCBOZXR3b3JrMSEwHwYDVQQLExho
-dHRwOi8vd3d3LnVzZXJ0cnVzdC5jb20xNjA0BgNVBAMTLVVUTi1VU0VSRmlyc3Qt
-Q2xpZW50IEF1dGhlbnRpY2F0aW9uIGFuZCBFbWFpbDAeFw05OTA3MDkxNzI4NTBa
-Fw0xOTA3MDkxNzM2NThaMIGuMQswCQYDVQQGEwJVUzELMAkGA1UECBMCVVQxFzAV
-BgNVBAcTDlNhbHQgTGFrZSBDaXR5MR4wHAYDVQQKExVUaGUgVVNFUlRSVVNUIE5l
-dHdvcmsxITAfBgNVBAsTGGh0dHA6Ly93d3cudXNlcnRydXN0LmNvbTE2MDQGA1UE
-AxMtVVROLVVTRVJGaXJzdC1DbGllbnQgQXV0aGVudGljYXRpb24gYW5kIEVtYWls
-MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsjmFpPJ9q0E7YkY3rs3B
-YHW8OWX5ShpHornMSMxqmNVNNRm5pELlzkniii8efNIxB8dOtINknS4p1aJkxIW9
-hVE1eaROaJB7HHqkkqgX8pgV8pPMyaQylbsMTzC9mKALi+VuG6JG+ni8om+rWV6l
-L8/K2m2qL+usobNqqrcuZzWLeeEeaYji5kbNoKXqvgvOdjp6Dpvq/NonWz1zHyLm
-SGHGTPNpsaguG7bUMSAsvIKKjqQOpdeJQ/wWWq8dcdcRWdq6hw2v+vPhwvCkxWeM
-1tZUOt4KpLoDd7NlyP0e03RiqhjKaJMeoYV+9Udly/hNVyh00jT/MLbu9mIwFIws
-6wIDAQABo4G5MIG2MAsGA1UdDwQEAwIBxjAPBgNVHRMBAf8EBTADAQH/MB0GA1Ud
-DgQWBBSJgmd9xJ0mcABLtFBIfN49rgRufTBYBgNVHR8EUTBPME2gS6BJhkdodHRw
-Oi8vY3JsLnVzZXJ0cnVzdC5jb20vVVROLVVTRVJGaXJzdC1DbGllbnRBdXRoZW50
-aWNhdGlvbmFuZEVtYWlsLmNybDAdBgNVHSUEFjAUBggrBgEFBQcDAgYIKwYBBQUH
-AwQwDQYJKoZIhvcNAQEFBQADggEBALFtYV2mGn98q0rkMPxTbyUkxsrt4jFcKw7u
-7mFVbwQ+zznexRtJlOTrIEy05p5QLnLZjfWqo7NK2lYcYJeA3IKirUq9iiv/Cwm0
-xtcgBEXkzYABurorbs6q15L+5K/r9CYdFip/bDCVNy8zEqx/3cfREYxRmLLQo5HQ
-rfafnoOTHh1CuEava2bwm3/q4wMC5QJRwarVNZ1yQAOJujEdxRBoUp7fooXFXAim
-eOZTT7Hot9MUnpOmw2TjrH5xzbyf6QMbzPvprDHBr3wVdAKZw7JHpsIyYdfHb0gk
-USeh1YdV8nuPmD0Wnu51tvjQjvLzxq4oW6fw8zYX/MMF08oDSlQ=
------END CERTIFICATE-----
-
subject= /C=US/ST=UT/L=Salt Lake City/O=The USERTRUST Network/OU=http://www.usertrust.com/CN=UTN-USERFirst-Hardware
serial=44BE0C8B500024B411D3362AFE650AFD
-----BEGIN CERTIFICATE-----
@@ -1522,35 +1253,6 @@
KqMiDP+JJn1fIytH1xUdqWqeUQ0qUZ6B+dQ7XnASfxAynB67nfhmqA==
-----END CERTIFICATE-----
-subject= /C=US/ST=UT/L=Salt Lake City/O=The USERTRUST Network/OU=http://www.usertrust.com/CN=UTN-USERFirst-Object
-serial=44BE0C8B500024B411D3362DE0B35F1B
------BEGIN CERTIFICATE-----
-MIIEZjCCA06gAwIBAgIQRL4Mi1AAJLQR0zYt4LNfGzANBgkqhkiG9w0BAQUFADCB
-lTELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAlVUMRcwFQYDVQQHEw5TYWx0IExha2Ug
-Q2l0eTEeMBwGA1UEChMVVGhlIFVTRVJUUlVTVCBOZXR3b3JrMSEwHwYDVQQLExho
-dHRwOi8vd3d3LnVzZXJ0cnVzdC5jb20xHTAbBgNVBAMTFFVUTi1VU0VSRmlyc3Qt
-T2JqZWN0MB4XDTk5MDcwOTE4MzEyMFoXDTE5MDcwOTE4NDAzNlowgZUxCzAJBgNV
-BAYTAlVTMQswCQYDVQQIEwJVVDEXMBUGA1UEBxMOU2FsdCBMYWtlIENpdHkxHjAc
-BgNVBAoTFVRoZSBVU0VSVFJVU1QgTmV0d29yazEhMB8GA1UECxMYaHR0cDovL3d3
-dy51c2VydHJ1c3QuY29tMR0wGwYDVQQDExRVVE4tVVNFUkZpcnN0LU9iamVjdDCC
-ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAM6qgT+jo2F4qjEAVZURnicP
-HxzfOpuCaDDASmEd8S8O+r5596Uj71VRloTN2+O5bj4x2AogZ8f02b+U60cEPgLO
-KqJdhwQJ9jCdGIqXsqoc/EHSoTbL+z2RuufZcDX65OeQw5ujm9M89RKZd7G3CeBo
-5hy485RjiGpq/gt2yb70IuRnuasaXnfBhQfdDWy/7gbHd2pBnqcP1/vulBe3/IW+
-pKvEHDHd17bR5PDv3xaPslKT16HUiaEHLr/hARJCHhrh2JU022R5KP+6LhHC5ehb
-kkj7RwvCbNqtMoNB86XlQXD9ZZBt+vpRxPm9lisZBCzTbafc8H9vg2XiaquHhnUC
-AwEAAaOBrzCBrDALBgNVHQ8EBAMCAcYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4E
-FgQU2u1kdBScFDyr3ZmpvVsoTYs8ydgwQgYDVR0fBDswOTA3oDWgM4YxaHR0cDov
-L2NybC51c2VydHJ1c3QuY29tL1VUTi1VU0VSRmlyc3QtT2JqZWN0LmNybDApBgNV
-HSUEIjAgBggrBgEFBQcDAwYIKwYBBQUHAwgGCisGAQQBgjcKAwQwDQYJKoZIhvcN
-AQEFBQADggEBAAgfUrE3RHjb/c652pWWmKpVZIC1WkDdIaXFwfNfLEzIR1pp6ujw
-NTX00CXzyKakh0q9G7FzCL3Uw8q2NbtZhncxzaeAFK4T7/yxSPlrJSUtUbYsbUXB
-mMiKVl0+7kNOPmsnjtA6S4ULX9Ptaqd1y9Fahy85dRNacrACgZ++8A+EVCBibGnU
-4U3GDZlDAQ0Slox4nb9QorFEqmrPF3rPbw/U+CRVX/A0FklmPlBGyWNxODFiuGK5
-81OtbLUrohKqGU8J2l7nk8aOFAj+8DCAGKCGhU3IfdeLA/5u1fedFqySLKAj5ZyR
-Uh+U3xeUc8OzwcFxBSAAeL0TUh2oPs0AH8g=
------END CERTIFICATE-----
-
subject= /C=EU/O=AC Camerfirma SA CIF A82743287/OU=http://www.chambersign.org/CN=Chambers of Commerce Root
serial=00
-----BEGIN CERTIFICATE-----
@@ -1613,48 +1315,6 @@
AYoFWpO+ocH/+OcOZ6RHSXZddZAa9SaP8A==
-----END CERTIFICATE-----
-subject= /C=HU/L=Budapest/O=NetLock Halozatbiztonsagi Kft./OU=Tanusitvanykiadok/CN=NetLock Minositett Kozjegyzoi (Class QA) Tanusitvanykiado/emailAddress=info@netlock.hu
-serial=7B
------BEGIN CERTIFICATE-----
-MIIG0TCCBbmgAwIBAgIBezANBgkqhkiG9w0BAQUFADCByTELMAkGA1UEBhMCSFUx
-ETAPBgNVBAcTCEJ1ZGFwZXN0MScwJQYDVQQKEx5OZXRMb2NrIEhhbG96YXRiaXp0
-b25zYWdpIEtmdC4xGjAYBgNVBAsTEVRhbnVzaXR2YW55a2lhZG9rMUIwQAYDVQQD
-EzlOZXRMb2NrIE1pbm9zaXRldHQgS296amVneXpvaSAoQ2xhc3MgUUEpIFRhbnVz
-aXR2YW55a2lhZG8xHjAcBgkqhkiG9w0BCQEWD2luZm9AbmV0bG9jay5odTAeFw0w
-MzAzMzAwMTQ3MTFaFw0yMjEyMTUwMTQ3MTFaMIHJMQswCQYDVQQGEwJIVTERMA8G
-A1UEBxMIQnVkYXBlc3QxJzAlBgNVBAoTHk5ldExvY2sgSGFsb3phdGJpenRvbnNh
-Z2kgS2Z0LjEaMBgGA1UECxMRVGFudXNpdHZhbnlraWFkb2sxQjBABgNVBAMTOU5l
-dExvY2sgTWlub3NpdGV0dCBLb3pqZWd5em9pIChDbGFzcyBRQSkgVGFudXNpdHZh
-bnlraWFkbzEeMBwGCSqGSIb3DQEJARYPaW5mb0BuZXRsb2NrLmh1MIIBIjANBgkq
-hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAx1Ilstg91IRVCacbvWy5FPSKAtt2/Goq
-eKvld/Bu4IwjZ9ulZJm53QE+b+8tmjwi8F3JV6BVQX/yQ15YglMxZc4e8ia6AFQe
-r7C8HORSjKAyr7c3sVNnaHRnUPYtLmTeriZ539+Zhqurf4XsoPuAzPS4DB6TRWO5
-3Lhbm+1bOdRfYrCnjnxmOCyqsQhjF2d9zL2z8cM/z1A57dEZgxXbhxInlrfa6uWd
-vLrqOU+L73Sa58XQ0uqGURzk/mQIKAR5BevKxXEOC++r6uwSEaEYBTJp0QwsGj0l
-mT+1fMptsK6ZmfoIYOcZwvK9UdPM0wKswREMgM6r3JSda6M5UzrWhQIDAMV9o4IC
-wDCCArwwEgYDVR0TAQH/BAgwBgEB/wIBBDAOBgNVHQ8BAf8EBAMCAQYwggJ1Bglg
-hkgBhvhCAQ0EggJmFoICYkZJR1lFTEVNISBFemVuIHRhbnVzaXR2YW55IGEgTmV0
-TG9jayBLZnQuIE1pbm9zaXRldHQgU3pvbGdhbHRhdGFzaSBTemFiYWx5emF0YWJh
-biBsZWlydCBlbGphcmFzb2sgYWxhcGphbiBrZXN6dWx0LiBBIG1pbm9zaXRldHQg
-ZWxla3Ryb25pa3VzIGFsYWlyYXMgam9naGF0YXMgZXJ2ZW55ZXN1bGVzZW5laywg
-dmFsYW1pbnQgZWxmb2dhZGFzYW5hayBmZWx0ZXRlbGUgYSBNaW5vc2l0ZXR0IFN6
-b2xnYWx0YXRhc2kgU3phYmFseXphdGJhbiwgYXogQWx0YWxhbm9zIFN6ZXJ6b2Rl
-c2kgRmVsdGV0ZWxla2JlbiBlbG9pcnQgZWxsZW5vcnplc2kgZWxqYXJhcyBtZWd0
-ZXRlbGUuIEEgZG9rdW1lbnR1bW9rIG1lZ3RhbGFsaGF0b2sgYSBodHRwczovL3d3
-dy5uZXRsb2NrLmh1L2RvY3MvIGNpbWVuIHZhZ3kga2VyaGV0b2sgYXogaW5mb0Bu
-ZXRsb2NrLm5ldCBlLW1haWwgY2ltZW4uIFdBUk5JTkchIFRoZSBpc3N1YW5jZSBh
-bmQgdGhlIHVzZSBvZiB0aGlzIGNlcnRpZmljYXRlIGFyZSBzdWJqZWN0IHRvIHRo
-ZSBOZXRMb2NrIFF1YWxpZmllZCBDUFMgYXZhaWxhYmxlIGF0IGh0dHBzOi8vd3d3
-Lm5ldGxvY2suaHUvZG9jcy8gb3IgYnkgZS1tYWlsIGF0IGluZm9AbmV0bG9jay5u
-ZXQwHQYDVR0OBBYEFAlqYhaSsFq7VQ7LdTI6MuWyIckoMA0GCSqGSIb3DQEBBQUA
-A4IBAQCRalCc23iBmz+LQuM7/KbD7kPgz/PigDVJRXYC4uMvBcXxKufAQTPGtpvQ
-MznNwNuhrWw3AkxYQTvyl5LGSKjN5Yo5iWH5Upfpvfb5lHTocQ68d4bDBsxafEp+
-NFAwLvt/MpqNPfMgW/hqyobzMUwsWYACff44yTB1HLdV47yfuqhthCgFdbOLDcCR
-VCHnpgu0mfVRQdzNo0ci2ccBgcTcR08m6h/t280NmPSjnLRzMkqWmf68f8glWPhY
-83ZmiVSkpj7EUFy6iRiCdUgh0k8T6GB+B3bbELVR5qq5aKrN9p2QdRLqOBrKROi3
-macqaJVmlaut74nLYKkGEsaUR+ko
------END CERTIFICATE-----
-
subject= /C=HU/ST=Hungary/L=Budapest/O=NetLock Halozatbiztonsagi Kft./OU=Tanusitvanykiadok/CN=NetLock Kozjegyzoi (Class A) Tanusitvanykiado
serial=0103
-----BEGIN CERTIFICATE-----
@@ -2225,42 +1885,6 @@
y3Z9fYjYHcgFHW68lKlmjHdxx/qR+i9Rnuk5UrbnBEI=
-----END CERTIFICATE-----
-subject= /C=CH/O=SwissSign AG/CN=SwissSign Platinum CA - G2
-serial=4EB200670C035D4F
------BEGIN CERTIFICATE-----
-MIIFwTCCA6mgAwIBAgIITrIAZwwDXU8wDQYJKoZIhvcNAQEFBQAwSTELMAkGA1UE
-BhMCQ0gxFTATBgNVBAoTDFN3aXNzU2lnbiBBRzEjMCEGA1UEAxMaU3dpc3NTaWdu
-IFBsYXRpbnVtIENBIC0gRzIwHhcNMDYxMDI1MDgzNjAwWhcNMzYxMDI1MDgzNjAw
-WjBJMQswCQYDVQQGEwJDSDEVMBMGA1UEChMMU3dpc3NTaWduIEFHMSMwIQYDVQQD
-ExpTd2lzc1NpZ24gUGxhdGludW0gQ0EgLSBHMjCCAiIwDQYJKoZIhvcNAQEBBQAD
-ggIPADCCAgoCggIBAMrfogLi2vj8Bxax3mCq3pZcZB/HL37PZ/pEQtZ2Y5Wu669y
-IIpFR4ZieIbWIDkm9K6j/SPnpZy1IiEZtzeTIsBQnIJ71NUERFzLtMKfkr4k2Htn
-IuJpX+UFeNSH2XFwMyVTtIc7KZAoNppVRDBopIOXfw0enHb/FZ1glwCNioUD7IC+
-6ixuEFGSzH7VozPY1kneWCqv9hbrS3uQMpe5up1Y8fhXSQQeol0GcN1x2/ndi5ob
-jM89o03Oy3z2u5yg+gnOI2Ky6Q0f4nIoj5+saCB9bzuohTEJfwvH6GXp43gOCWcw
-izSC+13gzJ2BbWLuCB4ELE6b7P6pT1/9aXjvCR+htL/68++QHkwFix7qepF6w9fl
-+zC8bBsQWJj3Gl/QKTIDE0ZNYWqFTFJ0LwYfexHihJfGmfNtf9dng34TaNhxKFrY
-zt3oEBSa/m0jh26OWnA81Y0JAKeqvLAxN23IhBQeW71FYyBrS3SMvds6DsHPWhaP
-pZjydomyExI7C3d3rLvlPClKknLKYRorXkzig3R3+jVIeoVNjZpTxN94ypeRSCtF
-KwH3HBqi7Ri6Cr2D+m+8jVeTO9TUps4e8aCxzqv9KyiaTxvXw3LbpMS/XUz13XuW
-ae5ogObnmLo2t/5u7Su9IPhlGdpVCX4l3P5hYnL5fhgC72O00Puv5TtjjGePAgMB
-AAGjgawwgakwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0O
-BBYEFFCvzAeHFUdvOMW0ZdHelarp35zMMB8GA1UdIwQYMBaAFFCvzAeHFUdvOMW0
-ZdHelarp35zMMEYGA1UdIAQ/MD0wOwYJYIV0AVkBAQEBMC4wLAYIKwYBBQUHAgEW
-IGh0dHA6Ly9yZXBvc2l0b3J5LnN3aXNzc2lnbi5jb20vMA0GCSqGSIb3DQEBBQUA
-A4ICAQAIhab1Fgz8RBrBY+D5VUYI/HAcQiiWjrfFwUF1TglxeeVtlspLpYhg0DB0
-uMoI3LQwnkAHFmtllXcBrqS3NQuB2nEVqXQXOHtYyvkv+8Bldo1bAbl93oI9ZLi+
-FHSjClTTLJUYFzX1UWs/j6KWYTl4a0vlpqD4U99REJNi54Av4tHgvI42Rncz7Lj7
-jposiU0xEQ8mngS7twSNC/K5/FqdOxa3L8iYq/6KUFkuozv8KV2LwUvJ4ooTHbG/
-u0IdUt1O2BReEMYxB+9xJ/cbOQncguqLs5WGXv312l0xpuAxtpTmREl0xRbl9x8D
-YSjFyMsSoEJL+WuICI20MhjzdZ/EfwBPBZWcoxcCw7NTm6ogOSkrZvqdr16zktK1
-puEa+S1BaYEUtLS17Yk9zvupnTVCRLEcFHOBzyoBNZox1S2PbYTfgE1X4z/FhHXa
-icYwu+uPyyIIoK6q8QNsOktNCaUOcsZWayFCTiMlFGiudgp8DAdwZPmaL/YFOSbG
-DI8Zf0NebvRbFS/bYV3mZy8/CJT5YLSYMdp08YSTcU1f+2BY0fvEwW2JorsgH51x
-kcsymxM9Pn2SUjWskpSi0xjCfMfqr3YFFt1nJ8J+HAciIfNAChs0B0QTwoRqjt8Z
-Wr9/6x3iGjjRXK9HkmuAtTClyY3YqzGBH9/CZjfTk6mFhnll0g==
------END CERTIFICATE-----
-
subject= /C=CH/O=SwissSign AG/CN=SwissSign Gold CA - G2
serial=BB401C43F55E4FB0
-----BEGIN CERTIFICATE-----
@@ -2648,35 +2272,6 @@
/L7fCg0=
-----END CERTIFICATE-----
-subject= /C=DE/ST=Baden-Wuerttemberg (BW)/L=Stuttgart/O=Deutscher Sparkassen Verlag GmbH/CN=S-TRUST Authentication and Encryption Root CA 2005:PN
-serial=371918E653547C1AB5B8CB595ADB35B7
------BEGIN CERTIFICATE-----
-MIIEezCCA2OgAwIBAgIQNxkY5lNUfBq1uMtZWts1tzANBgkqhkiG9w0BAQUFADCB
-rjELMAkGA1UEBhMCREUxIDAeBgNVBAgTF0JhZGVuLVd1ZXJ0dGVtYmVyZyAoQlcp
-MRIwEAYDVQQHEwlTdHV0dGdhcnQxKTAnBgNVBAoTIERldXRzY2hlciBTcGFya2Fz
-c2VuIFZlcmxhZyBHbWJIMT4wPAYDVQQDEzVTLVRSVVNUIEF1dGhlbnRpY2F0aW9u
-IGFuZCBFbmNyeXB0aW9uIFJvb3QgQ0EgMjAwNTpQTjAeFw0wNTA2MjIwMDAwMDBa
-Fw0zMDA2MjEyMzU5NTlaMIGuMQswCQYDVQQGEwJERTEgMB4GA1UECBMXQmFkZW4t
-V3VlcnR0ZW1iZXJnIChCVykxEjAQBgNVBAcTCVN0dXR0Z2FydDEpMCcGA1UEChMg
-RGV1dHNjaGVyIFNwYXJrYXNzZW4gVmVybGFnIEdtYkgxPjA8BgNVBAMTNVMtVFJV
-U1QgQXV0aGVudGljYXRpb24gYW5kIEVuY3J5cHRpb24gUm9vdCBDQSAyMDA1OlBO
-MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2bVKwdMz6tNGs9HiTNL1
-toPQb9UY6ZOvJ44TzbUlNlA0EmQpoVXhOmCTnijJ4/Ob4QSwI7+Vio5bG0F/WsPo
-TUzVJBY+h0jUJ67m91MduwwA7z5hca2/OnpYH5Q9XIHV1W/fuJvS9eXLg3KSwlOy
-ggLrra1fFi2SU3bxibYs9cEv4KdKb6AwajLrmnQDaHgTncovmwsdvs91DSaXm8f1
-XgqfeN+zvOyauu9VjxuapgdjKRdZYgkqeQd3peDRF2npW932kKvimAoA0SVtnteF
-hy+S8dF2g08LOlk3KC8zpxdQ1iALCvQm+Z845y2kuJuJja2tyWp9iRe79n+Ag3rm
-7QIDAQABo4GSMIGPMBIGA1UdEwEB/wQIMAYBAf8CAQAwDgYDVR0PAQH/BAQDAgEG
-MCkGA1UdEQQiMCCkHjAcMRowGAYDVQQDExFTVFJvbmxpbmUxLTIwNDgtNTAdBgNV
-HQ4EFgQUD8oeXHngovMpttKFswtKtWXsa1IwHwYDVR0jBBgwFoAUD8oeXHngovMp
-ttKFswtKtWXsa1IwDQYJKoZIhvcNAQEFBQADggEBAK8B8O0ZPCjoTVy7pWMciDMD
-pwCHpB8gq9Yc4wYfl35UvbfRssnV2oDsF9eK9XvCAPbpEW+EoFolMeKJ+aQAPzFo
-LtU96G7m1R08P7K9n3frndOMusDXtk3sU5wPBG7qNWdX4wple5A64U8+wwCSersF
-iXOMy6ZNwPv2AtawB6MDwidAnwzkhYItr5pCHdDHjfhA7p0GVxzZotiAFP7hYy0y
-h9WUUpY6RsZxlj33mA6ykaqP2vROJAA5VeitF7nTNCtKqUDMFypVZUF0Qn71wK/I
-k63yGFs9iQzbRzkk+OBM8h+wPQrKBU6JIRrjKpms/H+h8Q8bHz2eBIPdltkdOpQ=
------END CERTIFICATE-----
-
subject= /C=HU/L=Budapest/O=Microsec Ltd./OU=e-Szigno CA/CN=Microsec e-Szigno Root CA
serial=CCB8E7BF4E291AFDA2DC66A51C2C0F11
-----BEGIN CERTIFICATE-----
@@ -2899,31 +2494,6 @@
Cm26OWMohpLzGITY+9HPBVZkVw==
-----END CERTIFICATE-----
-subject= /CN=ComSign CA/O=ComSign/C=IL
-serial=1413968314558CEA7B63E5FC34877744
------BEGIN CERTIFICATE-----
-MIIDkzCCAnugAwIBAgIQFBOWgxRVjOp7Y+X8NId3RDANBgkqhkiG9w0BAQUFADA0
-MRMwEQYDVQQDEwpDb21TaWduIENBMRAwDgYDVQQKEwdDb21TaWduMQswCQYDVQQG
-EwJJTDAeFw0wNDAzMjQxMTMyMThaFw0yOTAzMTkxNTAyMThaMDQxEzARBgNVBAMT
-CkNvbVNpZ24gQ0ExEDAOBgNVBAoTB0NvbVNpZ24xCzAJBgNVBAYTAklMMIIBIjAN
-BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA8ORUaSvTx49qROR+WCf4C9DklBKK
-8Rs4OC8fMZwG1Cyn3gsqrhqg455qv588x26i+YtkbDqthVVRVKU4VbirgwTyP2Q2
-98CNQ0NqZtH3FyrV7zb6MBBC11PN+fozc0yz6YQgitZBJzXkOPqUm7h65HkfM/sb
-2CEJKHxNGGleZIp6GZPKfuzzcuc3B1hZKKxC+cX/zT/npfo4sdAMx9lSGlPWgcxC
-ejVb7Us6eva1jsz/D3zkYDaHL63woSV9/9JLEYhwVKZBqGdTUkJe5DSe5L6j7Kpi
-Xd3DTKaCQeQzC6zJMw9kglcq/QytNuEMrkvF7zuZ2SOzW120V+x0cAwqTwIDAQAB
-o4GgMIGdMAwGA1UdEwQFMAMBAf8wPQYDVR0fBDYwNDAyoDCgLoYsaHR0cDovL2Zl
-ZGlyLmNvbXNpZ24uY28uaWwvY3JsL0NvbVNpZ25DQS5jcmwwDgYDVR0PAQH/BAQD
-AgGGMB8GA1UdIwQYMBaAFEsBmz5WGmU2dst7l6qSBe4y5ygxMB0GA1UdDgQWBBRL
-AZs+VhplNnbLe5eqkgXuMucoMTANBgkqhkiG9w0BAQUFAAOCAQEA0Nmlfv4pYEWd
-foPPbrxHbvUanlR2QnG0PFg/LUAlQvaBnPGJEMgOqnhPOAlXsDzACPw1jvFIUY0M
-cXS6hMTXcpuEfDhOZAYnKuGntewImbQKDdSFc8gS4TXt8QUxHXOZDOuWyt3T5oWq
-8Ir7dcHyCTxlZWTzTNity4hp8+SDtwy9F1qWF8pb/627HOkthIDYIb6FUtnUdLlp
-hbpN7Sgy6/lhSuTENh4Z3G+EER+V9YMoGKgzkkMn3V0TBEVPh9VGzT2ouvDzuFYk
-Res3x+F2T3I5GN9+dHLHcy056mDmrRGiVod7w2ia/viMcKjfZTL0pECMocJEAw6U
-AGegcQCCSA==
------END CERTIFICATE-----
-
subject= /CN=ComSign Secured CA/O=ComSign/C=IL
serial=C7284709B3B86C458C1DFA24F5364EE9
-----BEGIN CERTIFICATE-----
@@ -3551,23 +3121,6 @@
Z05phkOTOPu220+DkdRgfks+KzgHVZhepA==
-----END CERTIFICATE-----
-subject= /C=US/O=VeriSign, Inc./OU=Class 1 Public Primary Certification Authority
-serial=3F691E819CF09A4AF373FFB948A2E4DD
------BEGIN CERTIFICATE-----
-MIICPDCCAaUCED9pHoGc8JpK83P/uUii5N0wDQYJKoZIhvcNAQEFBQAwXzELMAkG
-A1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMTcwNQYDVQQLEy5DbGFz
-cyAxIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTk2
-MDEyOTAwMDAwMFoXDTI4MDgwMjIzNTk1OVowXzELMAkGA1UEBhMCVVMxFzAVBgNV
-BAoTDlZlcmlTaWduLCBJbmMuMTcwNQYDVQQLEy5DbGFzcyAxIFB1YmxpYyBQcmlt
-YXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIGfMA0GCSqGSIb3DQEBAQUAA4GN
-ADCBiQKBgQDlGb9to1ZhLZlIcfZn3rmN67eehoAKkQ76OCWvRoiC5XOooJskXQ0f
-zGVuDLDQVoQYh5oGmxChc9+0WDlrbsH2FdWoqD+qEgaNMax/sDTXjzRniAnNFBHi
-TkVWaR94AoDa3EeRKbs2yWNcxeDXLYd7obcysHswuiovMaruo2fa2wIDAQABMA0G
-CSqGSIb3DQEBBQUAA4GBAFgVKTk8d6PaXCUDfGD67gmZPCcQcMgMCeazh88K4hiW
-NWLMv5sneYlfycQJ9M61Hd8qveXbhpxoJeUwfLaJFf5n0a3hUKw8fGJLj7qE1xIV
-Gx/KXQ/BUpQqEZnae88MNhPVNdwQGVnqlMEAv3WP2fr9dgTbYruQagPZRjXZ+Hxb
------END CERTIFICATE-----
-
subject= /C=US/O=VeriSign, Inc./OU=Class 3 Public Primary Certification Authority
serial=3C9131CB1FF6D01B0E9AB8D044BF12BE
-----BEGIN CERTIFICATE-----
@@ -4175,6 +3728,58 @@
aspHYcN6+NOSBB+4IIThNlQWx0DeO4pz3N/GCUzf7Nr/1FNCocnyYh0igzyXxfkZ
YiesZSLX0zzG5Y6yU8xJzrww/nsOM5D77dIUkR8Hrw==
-----END CERTIFICATE-----
+
+subject= /C=JP/O=SECOM Trust Systems CO.,LTD./OU=Security Communication RootCA2
+serial=00
+-----BEGIN CERTIFICATE-----
+MIIDdzCCAl+gAwIBAgIBADANBgkqhkiG9w0BAQsFADBdMQswCQYDVQQGEwJKUDEl
+MCMGA1UEChMcU0VDT00gVHJ1c3QgU3lzdGVtcyBDTy4sTFRELjEnMCUGA1UECxMe
+U2VjdXJpdHkgQ29tbXVuaWNhdGlvbiBSb290Q0EyMB4XDTA5MDUyOTA1MDAzOVoX
+DTI5MDUyOTA1MDAzOVowXTELMAkGA1UEBhMCSlAxJTAjBgNVBAoTHFNFQ09NIFRy
+dXN0IFN5c3RlbXMgQ08uLExURC4xJzAlBgNVBAsTHlNlY3VyaXR5IENvbW11bmlj
+YXRpb24gUm9vdENBMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANAV
+OVKxUrO6xVmCxF1SrjpDZYBLx/KWvNs2l9amZIyoXvDjChz335c9S672XewhtUGr
+zbl+dp+++T42NKA7wfYxEUV0kz1XgMX5iZnK5atq1LXaQZAQwdbWQonCv/Q4EpVM
+VAX3NuRFg3sUZdbcDE3R3n4MqzvEFb46VqZab3ZpUql6ucjrappdUtAtCms1FgkQ
+hNBqyjoGADdH5H5XTz+L62e4iKrFvlNVspHEfbmwhRkGeC7bYRr6hfVKkaHnFtWO
+ojnflLhwHyg/i/xAXmODPIMqGplrz95Zajv8bxbXH/1KEOtOghY6rCcMU/Gt1SSw
+awNQwS08Ft1ENCcadfsCAwEAAaNCMEAwHQYDVR0OBBYEFAqFqXdlBZh8QIH4D5cs
+OPEK7DzPMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3
+DQEBCwUAA4IBAQBMOqNErLlFsceTfsgLCkLfZOoc7llsCLqJX2rKSpWeeo8HxdpF
+coJxDjrSzG+ntKEju/Ykn8sX/oymzsLS28yN/HH8AynBbF0zX2S2ZTuJbxh2ePXc
+okgfGT+Ok+vx+hfuzU7jBBJV1uXk3fs+BXziHV7Gp7yXT2g69ekuCkO2r1dcYmh8
+t/2jioSgrGK+KwmHNPBqAbubKVY8/gA3zyNs8U6qtnRGEmyR7jTV7JqR50S+kDFy
+1UkC9gLl9B/rfNmWVan/7Ir5mUf/NVoCqgTLiluHcSmRvaS0eg29mvVXIwAHIRc/
+SjnRBUkLp7Y3gaVdjKozXoEofKd9J+sAro03
+-----END CERTIFICATE-----
+
+subject= /C=GR/O=Hellenic Academic and Research Institutions Cert. Authority/CN=Hellenic Academic and Research Institutions RootCA 2011
+serial=00
+-----BEGIN CERTIFICATE-----
+MIIEMTCCAxmgAwIBAgIBADANBgkqhkiG9w0BAQUFADCBlTELMAkGA1UEBhMCR1Ix
+RDBCBgNVBAoTO0hlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1
+dGlvbnMgQ2VydC4gQXV0aG9yaXR5MUAwPgYDVQQDEzdIZWxsZW5pYyBBY2FkZW1p
+YyBhbmQgUmVzZWFyY2ggSW5zdGl0dXRpb25zIFJvb3RDQSAyMDExMB4XDTExMTIw
+NjEzNDk1MloXDTMxMTIwMTEzNDk1MlowgZUxCzAJBgNVBAYTAkdSMUQwQgYDVQQK
+EztIZWxsZW5pYyBBY2FkZW1pYyBhbmQgUmVzZWFyY2ggSW5zdGl0dXRpb25zIENl
+cnQuIEF1dGhvcml0eTFAMD4GA1UEAxM3SGVsbGVuaWMgQWNhZGVtaWMgYW5kIFJl
+c2VhcmNoIEluc3RpdHV0aW9ucyBSb290Q0EgMjAxMTCCASIwDQYJKoZIhvcNAQEB
+BQADggEPADCCAQoCggEBAKlTAOMupvaO+mDYLZU++CwqVE7NuYRhlFhPjz2L5EPz
+dYmNUeTDN9KKiE15HrcS3UN4SoqS5tdI1Q+kOilENbgH9mgdVc04UfCMJDGFr4PJ
+fel3r+0ae50X+bOdOFAPplp5kYCvN66m0zH7tSYJnTxa71HFK9+WXesyHgLacEns
+bgzImjeN9/E2YEsmLIKe0HjzDQ9jpFEw4fkrJxIH2Oq9GGKYsFk3fb7u8yBRQlqD
+75O6aRXxYp2fmTmCobd0LovUxQt7L/DICto9eQqakxylKHJzkUOap9FNhYS5qXSP
+FEDH3N6sQWRstBmbAmNtJGSPRLIl6s5ddAxjMlyNh+UCAwEAAaOBiTCBhjAPBgNV
+HRMBAf8EBTADAQH/MAsGA1UdDwQEAwIBBjAdBgNVHQ4EFgQUppFC/RNhSiOeCKQp
+5dgTBCPuQSUwRwYDVR0eBEAwPqA8MAWCAy5ncjAFggMuZXUwBoIELmVkdTAGggQu
+b3JnMAWBAy5ncjAFgQMuZXUwBoEELmVkdTAGgQQub3JnMA0GCSqGSIb3DQEBBQUA
+A4IBAQAf73lB4XtuP7KMhjdCSk4cNx6NZrokgclPEg8hwAOXhiVtXdMiKahsog2p
+6z0GW5k6x8zDmjR/qw7IThzh+uTczQ2+vyT+bOdrwg3IBp5OjWEopmr95fZi6hg8
+TqBTnbI6nOulnJEWtk2C4AwFSKls9cz4y51JtPACpf1wA+2KIaWuE4ZJwzNzvoc7
+dIsXRSZMFpGD/md9zU1jZ/rzAxKWeAaNsWftjj++n08C9bMJL/NMh98qy5V8Acys
+Nnq/onN694/BtZqhFLKPM58N7yLcZnuEvUUXBj08yrl3NI/K6s8/MT7jiOOASSXI
+l7WdmplNsDz4SgCbZN2fOUvRJ9e4
+-----END CERTIFICATE-----
# ***** BEGIN LICENSE BLOCK *****
# This file is a collection of intermediate certificates accumulated
# from mapreducing valid certificate chains.
@@ -5508,33 +5113,6 @@
HBtFP+DIpNCr0Iai5G1wY5rfdmdz9TaJPi3YtwXLdNfZAf9KfJv5q547rQ==
-----END CERTIFICATE-----
-subject= /C=CN/O=CNNIC SSL/CN=CNNIC SSL
-serial=4287A2A0
------BEGIN CERTIFICATE-----
-MIIEFzCCA4CgAwIBAgIEQoeioDANBgkqhkiG9w0BAQUFADCBwzELMAkGA1UEBhMC
-VVMxFDASBgNVBAoTC0VudHJ1c3QubmV0MTswOQYDVQQLEzJ3d3cuZW50cnVzdC5u
-ZXQvQ1BTIGluY29ycC4gYnkgcmVmLiAobGltaXRzIGxpYWIuKTElMCMGA1UECxMc
-KGMpIDE5OTkgRW50cnVzdC5uZXQgTGltaXRlZDE6MDgGA1UEAxMxRW50cnVzdC5u
-ZXQgU2VjdXJlIFNlcnZlciBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0wNzA1
-MTExNDAzMjJaFw0xMjAzMDEwNTAwMDBaMDUxCzAJBgNVBAYTAkNOMRIwEAYDVQQK
-EwlDTk5JQyBTU0wxEjAQBgNVBAMTCUNOTklDIFNTTDCCASIwDQYJKoZIhvcNAQEB
-BQADggEPADCCAQoCggEBAJi/7qMONmdodRqKb5QMZM8XaQLXy26EjTnBblL+WC1y
-PGXxTFyYSjrJKB3iUKP1Inj9dymNfQqD0F0p/aWh6RClHb/NoAwR2zjIf8xrStVO
-eUCieuDz5l7TVKZ/aklW/UcIzImj9SjRzixzJ0Qdd7L0SW11WgzUd/IEqHHy2DHq
-3qJC3qUnfkWLmmPlE7QUJ4cg62kFwZ2vznHb9tlwGEPd2ik4iACBUL8X17x4BV//
-DeOl1z75DpDMLjmUTi/vwmsc/1Ko9ElwrfkjF4L7XY/hbQ/N/qmhMMn8OBiIwbTI
-14oRS/8eiUqTe0L0KwrC+Yo96HBTBRObAZWgV/X0ZxkCAwEAAaOCAR8wggEbMA4G
-A1UdDwEB/wQEAwIBBjASBgNVHRMBAf8ECDAGAQH/AgEAMDMGCCsGAQUFBwEBBCcw
-JTAjBggrBgEFBQcwAYYXaHR0cDovL29jc3AuZW50cnVzdC5uZXQwMwYDVR0fBCww
-KjAooCagJIYiaHR0cDovL2NybC5lbnRydXN0Lm5ldC9zZXJ2ZXIxLmNybDARBgNV
-HSAECjAIMAYGBFUdIAAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMB8G
-A1UdIwQYMBaAFPAXYhNVPbP/CgBr+1CEl/PtYtAaMB0GA1UdDgQWBBRL1eFRFqKn
-7aOlx+D/sYcYDsDj1TAZBgkqhkiG9n0HQQAEDDAKGwRWNy4xAwIAgTANBgkqhkiG
-9w0BAQUFAAOBgQBE8Wd1YSChOVKq/nmol4vS1BFliayedJBiTW9EfeyCtfPvCb9m
-RjpT2+k4P2y9xJSFOB7Z9FsvcjMwqUWdf9qDMWqm4QXddxlztDtZ6PZiSvZ3H+mX
-iWa/mCLLmFhoTElB/XYY4aqg/smxWaVJzHcbcP0ED5PeCxQiFpgbHeRVig==
------END CERTIFICATE-----
-
subject= /CN=Ford Motor Company - Enterprise CA
serial=04000000000116AA006682
-----BEGIN CERTIFICATE-----
@@ -7659,6 +7237,35 @@
LA6r/3tnveM7CW15sBz8ijJA3NquOg9zrc0=
-----END CERTIFICATE-----
+subject= /C=US/ST=UT/L=Salt Lake City/O=The USERTRUST Network/OU=http://www.usertrust.com/CN=UTN-USERFirst-Object
+serial=44BE0C8B500024B411D3362DE0B35F1B
+-----BEGIN CERTIFICATE-----
+MIIEZjCCA06gAwIBAgIQRL4Mi1AAJLQR0zYt4LNfGzANBgkqhkiG9w0BAQUFADCB
+lTELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAlVUMRcwFQYDVQQHEw5TYWx0IExha2Ug
+Q2l0eTEeMBwGA1UEChMVVGhlIFVTRVJUUlVTVCBOZXR3b3JrMSEwHwYDVQQLExho
+dHRwOi8vd3d3LnVzZXJ0cnVzdC5jb20xHTAbBgNVBAMTFFVUTi1VU0VSRmlyc3Qt
+T2JqZWN0MB4XDTk5MDcwOTE4MzEyMFoXDTE5MDcwOTE4NDAzNlowgZUxCzAJBgNV
+BAYTAlVTMQswCQYDVQQIEwJVVDEXMBUGA1UEBxMOU2FsdCBMYWtlIENpdHkxHjAc
+BgNVBAoTFVRoZSBVU0VSVFJVU1QgTmV0d29yazEhMB8GA1UECxMYaHR0cDovL3d3
+dy51c2VydHJ1c3QuY29tMR0wGwYDVQQDExRVVE4tVVNFUkZpcnN0LU9iamVjdDCC
+ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAM6qgT+jo2F4qjEAVZURnicP
+HxzfOpuCaDDASmEd8S8O+r5596Uj71VRloTN2+O5bj4x2AogZ8f02b+U60cEPgLO
+KqJdhwQJ9jCdGIqXsqoc/EHSoTbL+z2RuufZcDX65OeQw5ujm9M89RKZd7G3CeBo
+5hy485RjiGpq/gt2yb70IuRnuasaXnfBhQfdDWy/7gbHd2pBnqcP1/vulBe3/IW+
+pKvEHDHd17bR5PDv3xaPslKT16HUiaEHLr/hARJCHhrh2JU022R5KP+6LhHC5ehb
+kkj7RwvCbNqtMoNB86XlQXD9ZZBt+vpRxPm9lisZBCzTbafc8H9vg2XiaquHhnUC
+AwEAAaOBrzCBrDALBgNVHQ8EBAMCAcYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4E
+FgQU2u1kdBScFDyr3ZmpvVsoTYs8ydgwQgYDVR0fBDswOTA3oDWgM4YxaHR0cDov
+L2NybC51c2VydHJ1c3QuY29tL1VUTi1VU0VSRmlyc3QtT2JqZWN0LmNybDApBgNV
+HSUEIjAgBggrBgEFBQcDAwYIKwYBBQUHAwgGCisGAQQBgjcKAwQwDQYJKoZIhvcN
+AQEFBQADggEBAAgfUrE3RHjb/c652pWWmKpVZIC1WkDdIaXFwfNfLEzIR1pp6ujw
+NTX00CXzyKakh0q9G7FzCL3Uw8q2NbtZhncxzaeAFK4T7/yxSPlrJSUtUbYsbUXB
+mMiKVl0+7kNOPmsnjtA6S4ULX9Ptaqd1y9Fahy85dRNacrACgZ++8A+EVCBibGnU
+4U3GDZlDAQ0Slox4nb9QorFEqmrPF3rPbw/U+CRVX/A0FklmPlBGyWNxODFiuGK5
+81OtbLUrohKqGU8J2l7nk8aOFAj+8DCAGKCGhU3IfdeLA/5u1fedFqySLKAj5ZyR
+Uh+U3xeUc8OzwcFxBSAAeL0TUh2oPs0AH8g=
+-----END CERTIFICATE-----
+
subject= /C=SE/O=AddTrust AB/OU=AddTrust External TTP Network/CN=AddTrust External CA Root
serial=51260A931CE27F9CC3A55F79E072AE82
-----BEGIN CERTIFICATE-----
@@ -15956,40 +15563,6 @@
w6wUSuncMSd0IrwDLcLA6ayxuQaFzChSIHaTnwdpvAv9QEu4
-----END CERTIFICATE-----
-subject= /C=US/O=Intel Corporation/CN=Intel External Basic Issuing CA 3B
-serial=612CA5FE000000000006
------BEGIN CERTIFICATE-----
-MIIFYzCCBEugAwIBAgIKYSyl/gAAAAAABjANBgkqhkiG9w0BAQUFADBSMQswCQYD
-VQQGEwJVUzEaMBgGA1UEChMRSW50ZWwgQ29ycG9yYXRpb24xJzAlBgNVBAMTHklu
-dGVsIEV4dGVybmFsIEJhc2ljIFBvbGljeSBDQTAeFw0wNjAzMjIyMjIyNDdaFw0x
-MjAzMjIyMjMyNDdaMFYxCzAJBgNVBAYTAlVTMRowGAYDVQQKExFJbnRlbCBDb3Jw
-b3JhdGlvbjErMCkGA1UEAxMiSW50ZWwgRXh0ZXJuYWwgQmFzaWMgSXNzdWluZyBD
-QSAzQjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMDoHJYUGvcdR4k0
-Qv8/RrwNX4+vEe5PN2LnGn4xfIWwdUbk5+zvrRy1HjoKdG/AShZfxAuFoVyNIah5
-GJLpiS/vGbsDYKK+EZcIcmtPfvbB9Yg09EnQ0OpsZ2ORRd5lYUfMUU+zkpgL96vK
-FdkiG+eeHEwnSS3VikGVrF7UDdKfvxZwRwQCx6rEHAFPCjBDVfGYZuYT1EDhSDvz
-EiU7KzBsxrdBCFU4RYL7kh+do+vyMDlBzpw3WKvMVbf3s5lxYiY7AjEhU8OSyZEQ
-lgtK8PNyifHjmyrSaeJrPshM3PuLFG4d3gm1UJEiGNzxcHGb6YLP29IQAwY5mAk8
-j+UOVcsCAwEAAaOCAjUwggIxMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFG/D
-cREooZ0ZV/VnV8/ZKSaI1aWiMAsGA1UdDwQEAwIBhjAQBgkrBgEEAYI3FQEEAwIB
-ADAZBgkrBgEEAYI3FAIEDB4KAFMAdQBiAEMAQTAfBgNVHSMEGDAWgBQaxgxKxEdv
-qNutK/D0Vgaj7TdUDDCBvQYDVR0fBIG1MIGyMIGvoIGsoIGphk5odHRwOi8vd3d3
-LmludGVsLmNvbS9yZXBvc2l0b3J5L0NSTC9JbnRlbCUyMEV4dGVybmFsJTIwQmFz
-aWMlMjBQb2xpY3klMjBDQS5jcmyGV2h0dHA6Ly9jZXJ0aWZpY2F0ZXMuaW50ZWwu
-Y29tL3JlcG9zaXRvcnkvQ1JML0ludGVsJTIwRXh0ZXJuYWwlMjBCYXNpYyUyMFBv
-bGljeSUyMENBLmNybDCB4wYIKwYBBQUHAQEEgdYwgdMwYwYIKwYBBQUHMAKGV2h0
-dHA6Ly93d3cuaW50ZWwuY29tL3JlcG9zaXRvcnkvY2VydGlmaWNhdGVzL0ludGVs
-JTIwRXh0ZXJuYWwlMjBCYXNpYyUyMFBvbGljeSUyMENBLmNydDBsBggrBgEFBQcw
-AoZgaHR0cDovL2NlcnRpZmljYXRlcy5pbnRlbC5jb20vcmVwb3NpdG9yeS9jZXJ0
-aWZpY2F0ZXMvSW50ZWwlMjBFeHRlcm5hbCUyMEJhc2ljJTIwUG9saWN5JTIwQ0Eu
-Y3J0MA0GCSqGSIb3DQEBBQUAA4IBAQBAiWWHL6T21QXFZDyaUIye0jh7f40GXqi7
-5Ro5JbpROtlZBqyRq6oPMAG6Igxd5Bd9TMtNkOvguknFnK1y17UdMhLOl+FuInE+
-FAWk803aRW27OsX+ot0KmPioKnZ3L1K5qcQS3tp6or7AaZzd0w0qFitm4UfP9nnT
-XQtY9XM5QJ07IKFDE3tlOlamKhjbiJV3FKigCVesPZSx9h2G7Ve4irEfz915Vks2
-6AJHGZagEHNxM9exkgem4ENsG+t0vYqn9x7YsqJ6fmbOSyrzjzb3CYeEfyE5SL+f
-3Vx9uMsQDyb8LVCDBXQtxLEHnFEBPrRSSF9kPmyjavx4UqFdPEyC
------END CERTIFICATE-----
-
subject= /C=TW/O=Chunghwa Telecom Co., Ltd./OU=Public Certification Authority
serial=C953FEEEB895E91884ABB22A68A42A7D
-----BEGIN CERTIFICATE-----
@@ -18259,6 +17832,50 @@
-----END CERTIFICATE-----
subject= /C=US/O=VeriSign, Inc./OU=Class 4 Public Primary Certification Authority - G2/OU=(c) 1998 VeriSign, Inc. - For authorized use only/OU=VeriSign Trust Network
+serial=32888E9AD2F5EB1347F87FC4203725F8
+-----BEGIN CERTIFICATE-----
+MIIDAjCCAmsCEDKIjprS9esTR/h/xCA3JfgwDQYJKoZIhvcNAQEFBQAwgcExCzAJ
+BgNVBAYTAlVTMRcwFQYDVQQKEw5WZXJpU2lnbiwgSW5jLjE8MDoGA1UECxMzQ2xh
+c3MgNCBQdWJsaWMgUHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEcy
+MTowOAYDVQQLEzEoYykgMTk5OCBWZXJpU2lnbiwgSW5jLiAtIEZvciBhdXRob3Jp
+emVkIHVzZSBvbmx5MR8wHQYDVQQLExZWZXJpU2lnbiBUcnVzdCBOZXR3b3JrMB4X
+DTk4MDUxODAwMDAwMFoXDTI4MDgwMTIzNTk1OVowgcExCzAJBgNVBAYTAlVTMRcw
+FQYDVQQKEw5WZXJpU2lnbiwgSW5jLjE8MDoGA1UECxMzQ2xhc3MgNCBQdWJsaWMg
+UHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEcyMTowOAYDVQQLEzEo
+YykgMTk5OCBWZXJpU2lnbiwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5
+MR8wHQYDVQQLExZWZXJpU2lnbiBUcnVzdCBOZXR3b3JrMIGfMA0GCSqGSIb3DQEB
+AQUAA4GNADCBiQKBgQC68OTP+cSuhVS5B1f5j8V/aBH4xBewRNzjMHPVKmIquNDM
+HO0oW369atyzkSTKQWI8/AIBvxwWMZQFl3Zuoq29YRdsTjCG8FE3KlDHqGKB3FtK
+qsGgtG7rL+VXxbErQHDbWk2hjh+9Ax/YA9SPTJlxvOKCzFjomDqG04Y48wApHwID
+AQABMA0GCSqGSIb3DQEBBQUAA4GBAIWMEsGnuVAVess+rLhDityq3RS6iYF+ATwj
+cSGIL4LcY/oCRaxFWdcqWERbt5+BO5JoPeI3JPV7bI92NZYJqFmduc4jq3TWg/0y
+cyfYaT5DdPauxYma51N86Xv2S/PBZYPejYqcPIiNOVn8qj8ijaHBZlCBckztImRP
+T8qAkbYp
+-----END CERTIFICATE-----
+
+subject= /C=US/O=VeriSign, Inc./OU=Class 1 Public Primary Certification Authority - G2/OU=(c) 1998 VeriSign, Inc. - For authorized use only/OU=VeriSign Trust Network
+serial=4CC7EAAA983E71D39310F83D3A899192
+-----BEGIN CERTIFICATE-----
+MIIDAjCCAmsCEEzH6qqYPnHTkxD4PTqJkZIwDQYJKoZIhvcNAQEFBQAwgcExCzAJ
+BgNVBAYTAlVTMRcwFQYDVQQKEw5WZXJpU2lnbiwgSW5jLjE8MDoGA1UECxMzQ2xh
+c3MgMSBQdWJsaWMgUHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEcy
+MTowOAYDVQQLEzEoYykgMTk5OCBWZXJpU2lnbiwgSW5jLiAtIEZvciBhdXRob3Jp
+emVkIHVzZSBvbmx5MR8wHQYDVQQLExZWZXJpU2lnbiBUcnVzdCBOZXR3b3JrMB4X
+DTk4MDUxODAwMDAwMFoXDTI4MDgwMTIzNTk1OVowgcExCzAJBgNVBAYTAlVTMRcw
+FQYDVQQKEw5WZXJpU2lnbiwgSW5jLjE8MDoGA1UECxMzQ2xhc3MgMSBQdWJsaWMg
+UHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEcyMTowOAYDVQQLEzEo
+YykgMTk5OCBWZXJpU2lnbiwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5
+MR8wHQYDVQQLExZWZXJpU2lnbiBUcnVzdCBOZXR3b3JrMIGfMA0GCSqGSIb3DQEB
+AQUAA4GNADCBiQKBgQCq0Lq+Fi24g9TK0g+8djHKlNgdk4xWArzZbxpvUjZudVYK
+VdPfQ4chEWWKfo+9Id5rMj8bhDSVBZ1BNeuS65bdqlk/AVNtmU/t5eIqWpDBucSm
+Fc/IReumXY6cPvBkJHalzasab7bYe1FhbqZ/h8jit+U03EGI6glAvnOSPWvndQID
+AQABMA0GCSqGSIb3DQEBBQUAA4GBAKlPww3HZ74sy9mozS11534Vnjty637rXC0J
+h9ZrbWB85a7FkCMMXErQr7Fd88e2CtvgFZMN3QO8x3aKtd1Pw5sTdbgBwObJW2ul
+uIncrKTdcu1OofdPvAbT6shkdHvClUGcZXNY8ZCaPGqxmMnEh7zPRW1F4m4iP/68
+DzFc6PLZ
+-----END CERTIFICATE-----
+
+subject= /C=US/O=VeriSign, Inc./OU=Class 4 Public Primary Certification Authority - G2/OU=(c) 1998 VeriSign, Inc. - For authorized use only/OU=VeriSign Trust Network
serial=7EB611AB32C841DB5EAC01E3959EFFFD
-----BEGIN CERTIFICATE-----
MIIDAjCCAmsCEH62EasyyEHbXqwB45We//0wDQYJKoZIhvcNAQEFBQAwgcExCzAJ
@@ -18281,6 +17898,28 @@
-----END CERTIFICATE-----
subject= /C=US/O=VeriSign, Inc./OU=Class 2 Public Primary Certification Authority - G2/OU=(c) 1998 VeriSign, Inc. - For authorized use only/OU=VeriSign Trust Network
+serial=B92F60CC889FA17A4609B85B706C8AAF
+-----BEGIN CERTIFICATE-----
+MIIDAzCCAmwCEQC5L2DMiJ+hekYJuFtwbIqvMA0GCSqGSIb3DQEBBQUAMIHBMQsw
+CQYDVQQGEwJVUzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xPDA6BgNVBAsTM0Ns
+YXNzIDIgUHVibGljIFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgLSBH
+MjE6MDgGA1UECxMxKGMpIDE5OTggVmVyaVNpZ24sIEluYy4gLSBGb3IgYXV0aG9y
+aXplZCB1c2Ugb25seTEfMB0GA1UECxMWVmVyaVNpZ24gVHJ1c3QgTmV0d29yazAe
+Fw05ODA1MTgwMDAwMDBaFw0yODA4MDEyMzU5NTlaMIHBMQswCQYDVQQGEwJVUzEX
+MBUGA1UEChMOVmVyaVNpZ24sIEluYy4xPDA6BgNVBAsTM0NsYXNzIDIgUHVibGlj
+IFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgLSBHMjE6MDgGA1UECxMx
+KGMpIDE5OTggVmVyaVNpZ24sIEluYy4gLSBGb3IgYXV0aG9yaXplZCB1c2Ugb25s
+eTEfMB0GA1UECxMWVmVyaVNpZ24gVHJ1c3QgTmV0d29yazCBnzANBgkqhkiG9w0B
+AQEFAAOBjQAwgYkCgYEAp4gBIXQs5xoD8JjhlzwPIQjxnNuX6Zr8wgQGE75fUsjM
+HiwSViy4AWkszJkfrbCWrnkE8hM5wXuYuggs6MKEEyyqaekJ9MepAqRCwiNPStjw
+DqL7MWzJ5m+ZJwf15vRMeJ5t60aG+rmGyVTyssSv1EYcWskVMP8NbPUtDm3Of3cC
+AwEAATANBgkqhkiG9w0BAQUFAAOBgQByLvl/0fFx+8Se9sVeUYpAmLho+Jscg9ji
+nb3/7aHmZuovCfTK1+qlK5X2JGCGTUQug6XELaDTrnhpb3LabK4I8GOSN+a7xDAX
+rXfMSTWqz9iP0b63GJZHc2pUIjRkLbYWm1lbtFFZOrMLFPQS32eg9K0yZF6xRnIn
+jBJ7xUS0rg==
+-----END CERTIFICATE-----
+
+subject= /C=US/O=VeriSign, Inc./OU=Class 2 Public Primary Certification Authority - G2/OU=(c) 1998 VeriSign, Inc. - For authorized use only/OU=VeriSign Trust Network
serial=1F42285F3C880F8E3C89B384B3AB1F1C
-----BEGIN CERTIFICATE-----
MIIDAjCCAmsCEB9CKF88iA+OPImzhLOrHxwwDQYJKoZIhvcNAQEFBQAwgcExCzAJ
@@ -18302,6 +17941,60 @@
0Q3O7x0x
-----END CERTIFICATE-----
+subject= /C=US/O=VeriSign, Inc./OU=VeriSign Trust Network/OU=(c) 1999 VeriSign, Inc. - For authorized use only/CN=VeriSign Class 2 Public Primary Certification Authority - G3
+serial=6170CB498C5F984529E7B0A6D9505B7A
+-----BEGIN CERTIFICATE-----
+MIIEGTCCAwECEGFwy0mMX5hFKeewptlQW3owDQYJKoZIhvcNAQEFBQAwgcoxCzAJ
+BgNVBAYTAlVTMRcwFQYDVQQKEw5WZXJpU2lnbiwgSW5jLjEfMB0GA1UECxMWVmVy
+aVNpZ24gVHJ1c3QgTmV0d29yazE6MDgGA1UECxMxKGMpIDE5OTkgVmVyaVNpZ24s
+IEluYy4gLSBGb3IgYXV0aG9yaXplZCB1c2Ugb25seTFFMEMGA1UEAxM8VmVyaVNp
+Z24gQ2xhc3MgMiBQdWJsaWMgUHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0
+eSAtIEczMB4XDTk5MTAwMTAwMDAwMFoXDTM2MDcxNjIzNTk1OVowgcoxCzAJBgNV
+BAYTAlVTMRcwFQYDVQQKEw5WZXJpU2lnbiwgSW5jLjEfMB0GA1UECxMWVmVyaVNp
+Z24gVHJ1c3QgTmV0d29yazE6MDgGA1UECxMxKGMpIDE5OTkgVmVyaVNpZ24sIElu
+Yy4gLSBGb3IgYXV0aG9yaXplZCB1c2Ugb25seTFFMEMGA1UEAxM8VmVyaVNpZ24g
+Q2xhc3MgMiBQdWJsaWMgUHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAt
+IEczMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArwoNwtUs22e5LeWU
+J92lvuCwTY+zYVY81nzD9M0+hsuiiOLh2KRpxbXiv8GmR1BeRjmL1Za6tW8UvxDO
+JxOeBUebMXoT2B/Z0wI3i60sR/COgQanDTAM6/c8DyAd3HJG7qUCyFvDyVZpTMUY
+wZF7C9UTAJu878NIPkZgIIUq1ZC2zYugzDLdt/1AVbJQHFauzI13TccgTacxdu9o
+koqQHgiBVrKtaaNS0MscxCM9H5n+TOgWY47GCI72MfbS+uV23bUckqNJzc0BzWjN
+qWm6o+sdDZykIKbBoMXRRkwXbdKsZj+WjOCE1Db/IlnF+RFgqF8EffIa9iVCYQ/E
+Srg+iQIDAQABMA0GCSqGSIb3DQEBBQUAA4IBAQA0JhU8wI1NQ0kdvekhktdmnLfe
+xbjQ5F1fdiLAJvmEOjr5jLX77GDx6M4EsMjdpwOPMPOY36TmpDHf0xwLRtxyID+u
+7gU8pDM/CzmscHhzS5kr3zDCVLCoO1Wh/hYozUK9dG6A2ydEp85EXdQbkJgNHkKU
+sQAsBNB0owIFImNjzYO1+8FtYmtpdf1dcEG59b98377BMnMiIYtYgXsVkXq642RI
+sH/7NiXaldDxJBQX3RiAa0YjOVT1jmIJBB2UkKab5iXiQkWquJCtvgiPqQtCGJTP
+cjnhsUPgKM+351psE2tJs//jGHyJizNdrDPXp/naOlXJWBD5qu9ats9LS98q
+-----END CERTIFICATE-----
+
+subject= /C=US/O=VeriSign, Inc./OU=VeriSign Trust Network/OU=(c) 1999 VeriSign, Inc. - For authorized use only/CN=VeriSign Class 1 Public Primary Certification Authority - G3
+serial=8B5B75568454850B00CFAF3848CEB1A4
+-----BEGIN CERTIFICATE-----
+MIIEGjCCAwICEQCLW3VWhFSFCwDPrzhIzrGkMA0GCSqGSIb3DQEBBQUAMIHKMQsw
+CQYDVQQGEwJVUzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZl
+cmlTaWduIFRydXN0IE5ldHdvcmsxOjA4BgNVBAsTMShjKSAxOTk5IFZlcmlTaWdu
+LCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxRTBDBgNVBAMTPFZlcmlT
+aWduIENsYXNzIDEgUHVibGljIFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3Jp
+dHkgLSBHMzAeFw05OTEwMDEwMDAwMDBaFw0zNjA3MTYyMzU5NTlaMIHKMQswCQYD
+VQQGEwJVUzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZlcmlT
+aWduIFRydXN0IE5ldHdvcmsxOjA4BgNVBAsTMShjKSAxOTk5IFZlcmlTaWduLCBJ
+bmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxRTBDBgNVBAMTPFZlcmlTaWdu
+IENsYXNzIDEgUHVibGljIFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkg
+LSBHMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAN2E1Lm0+afY8wR4
+nN493GwTFtl63SRRZsDHJlkNrAYIwpTRMx/wgzUfbhvI3qpuFU5UJ+/EbRrsC+MO
+8ESlV8dAWB6jRx9x7GD2bZTIGDnt/kIYVt/kTEkQeE4BdjVjEjbdZrwBBDajVWjV
+ojYJrKshJlQGrT/KFOCsyq0GHZXi+J3x4GD/wn91K0zM2v6HmSHquv4+VNfSWXjb
+PG7PoBMAGrgnoeS+Z5bKoMWznN3JdZ7rMJpfo83ZrngZPyPpXNspva1VyBtUjGP2
+6KbqxzcSXKMpHgLZ2x87tNcPVkeBFQRKr4Mn0cVYiMHd9qqnoxjaaKptEVHhv2Vr
+n5Z20T0CAwEAATANBgkqhkiG9w0BAQUFAAOCAQEAq2aN17O6x5q25lXQBfGfMY1a
+qtmqRiYPce2lrVNWYgFHKkTp/j90CxObufRNG7LRX7K20ohcs5/Ny9Sn2WCVhDr4
+wTcdYcrnsMXlkdpUpqwxga6X3s0IrLjAl4B/bnKk52kTlWUfxJM8/XmPBNQ+T+r3
+ns7NZ3xPZQL/kYVUc8f/NveGLezQXk//EZ9yBta4GvFMDSZl4kSAHsef493oCtrs
+pSCAaWihT37ha88HQfqDjrw43bAuEbFrskLMmrz5SCJ5ShkPshw+IHTZasO+8ih4
+E1Z5T21Q6huwtVexN2ZYI/PcD98Kh8TvhgXVOBRgmaNL3gaWcSzy27YfpO8/7g==
+-----END CERTIFICATE-----
+
subject= /C=LV/O=VAS Latvijas Pasts - Vien.reg.Nr.40003052790/OU=Sertifikacijas pakalpojumi/CN=VAS Latvijas Pasts SSI(RCA)
serial=630686A7C53765A54390A86A58CCD432
-----BEGIN CERTIFICATE-----
@@ -18348,6 +18041,65 @@
58bfHvwhX56GPbx+8Jy9cp70R4JbcWfz+txUTKhc2FnH0AcOEzMnvPRp8Gsh
-----END CERTIFICATE-----
+subject= /C=US/ST=UT/L=Salt Lake City/O=The USERTRUST Network/OU=http://www.usertrust.com/CN=UTN-USERFirst-Network Applications
+serial=44BE0C8B500024B411D336304BC03377
+-----BEGIN CERTIFICATE-----
+MIIEZDCCA0ygAwIBAgIQRL4Mi1AAJLQR0zYwS8AzdzANBgkqhkiG9w0BAQUFADCB
+ozELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAlVUMRcwFQYDVQQHEw5TYWx0IExha2Ug
+Q2l0eTEeMBwGA1UEChMVVGhlIFVTRVJUUlVTVCBOZXR3b3JrMSEwHwYDVQQLExho
+dHRwOi8vd3d3LnVzZXJ0cnVzdC5jb20xKzApBgNVBAMTIlVUTi1VU0VSRmlyc3Qt
+TmV0d29yayBBcHBsaWNhdGlvbnMwHhcNOTkwNzA5MTg0ODM5WhcNMTkwNzA5MTg1
+NzQ5WjCBozELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAlVUMRcwFQYDVQQHEw5TYWx0
+IExha2UgQ2l0eTEeMBwGA1UEChMVVGhlIFVTRVJUUlVTVCBOZXR3b3JrMSEwHwYD
+VQQLExhodHRwOi8vd3d3LnVzZXJ0cnVzdC5jb20xKzApBgNVBAMTIlVUTi1VU0VS
+Rmlyc3QtTmV0d29yayBBcHBsaWNhdGlvbnMwggEiMA0GCSqGSIb3DQEBAQUAA4IB
+DwAwggEKAoIBAQCz+5Gh5DZVhawGNFugmliy+LUPBXeDrjKxdpJo7CNKyXY/45y2
+N3kDuatpjQclthln5LAbGHNhSuh+zdMvZOOmfAz6F4CjDUeJT1FxL+78P/m4FoCH
+iZMlIJpDgmkkdihZNaEdwH+DBmQWICzTSaSFtMBhf1EI+GgVkYDLpdXuOzr0hARe
+YFmnjDRy7rh4xdE7EkpvfmUnuaRVxblvQ6TFHSyZwFKkeEwVs0CYCGtDxgGwenv1
+axwiP8vv/6jQOkt2FZ7S0cYu49tXGzKiuG/ohqY/cKvlcJKrRB5AUPuco2LkbG6g
+yN7igEL66S/ozjIEj3yNtxyjNTwV3Z7DrpelAgMBAAGjgZEwgY4wCwYDVR0PBAQD
+AgHGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFPqGydvguul49Uuo1hXf8NPh
+ahQ8ME8GA1UdHwRIMEYwRKBCoECGPmh0dHA6Ly9jcmwudXNlcnRydXN0LmNvbS9V
+VE4tVVNFUkZpcnN0LU5ldHdvcmtBcHBsaWNhdGlvbnMuY3JsMA0GCSqGSIb3DQEB
+BQUAA4IBAQCk8yXM0dSRgyLQzDKrm5ZONJFUICU0YV8qAhXhi6r/fWRRzwr/vH3Y
+IWp4yy9Rb/hCHTO967V7lMPDqaAt39EpHx3+jz+7qEUqf9FuVSTiuwL7MT++6Lzs
+QCv4AdRWOOTKRIK1YSAhZ2X28AvnNPilwpyjXEAfhZOVBt5P1CeptqX8Fs1zMT+4
+ZSfP1FMa8Kxun08FDAOBp4QpxFq9ZFdyrTvPNximmMatBrTcCKME1SmklpoSZ0qM
+YEWd8SOasACcaLWYUNPvji6SZbFIPiG+FTAqDbUMo2s/rn9X9R+WfN9v3YIwLGUb
+QErNaLly7HF27FSOH4UMAWr6pjisH8SE
+-----END CERTIFICATE-----
+
+subject= /C=US/ST=UT/L=Salt Lake City/O=The USERTRUST Network/OU=http://www.usertrust.com/CN=UTN-USERFirst-Client Authentication and Email
+serial=44BE0C8B500024B411D336252567C989
+-----BEGIN CERTIFICATE-----
+MIIEojCCA4qgAwIBAgIQRL4Mi1AAJLQR0zYlJWfJiTANBgkqhkiG9w0BAQUFADCB
+rjELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAlVUMRcwFQYDVQQHEw5TYWx0IExha2Ug
+Q2l0eTEeMBwGA1UEChMVVGhlIFVTRVJUUlVTVCBOZXR3b3JrMSEwHwYDVQQLExho
+dHRwOi8vd3d3LnVzZXJ0cnVzdC5jb20xNjA0BgNVBAMTLVVUTi1VU0VSRmlyc3Qt
+Q2xpZW50IEF1dGhlbnRpY2F0aW9uIGFuZCBFbWFpbDAeFw05OTA3MDkxNzI4NTBa
+Fw0xOTA3MDkxNzM2NThaMIGuMQswCQYDVQQGEwJVUzELMAkGA1UECBMCVVQxFzAV
+BgNVBAcTDlNhbHQgTGFrZSBDaXR5MR4wHAYDVQQKExVUaGUgVVNFUlRSVVNUIE5l
+dHdvcmsxITAfBgNVBAsTGGh0dHA6Ly93d3cudXNlcnRydXN0LmNvbTE2MDQGA1UE
+AxMtVVROLVVTRVJGaXJzdC1DbGllbnQgQXV0aGVudGljYXRpb24gYW5kIEVtYWls
+MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsjmFpPJ9q0E7YkY3rs3B
+YHW8OWX5ShpHornMSMxqmNVNNRm5pELlzkniii8efNIxB8dOtINknS4p1aJkxIW9
+hVE1eaROaJB7HHqkkqgX8pgV8pPMyaQylbsMTzC9mKALi+VuG6JG+ni8om+rWV6l
+L8/K2m2qL+usobNqqrcuZzWLeeEeaYji5kbNoKXqvgvOdjp6Dpvq/NonWz1zHyLm
+SGHGTPNpsaguG7bUMSAsvIKKjqQOpdeJQ/wWWq8dcdcRWdq6hw2v+vPhwvCkxWeM
+1tZUOt4KpLoDd7NlyP0e03RiqhjKaJMeoYV+9Udly/hNVyh00jT/MLbu9mIwFIws
+6wIDAQABo4G5MIG2MAsGA1UdDwQEAwIBxjAPBgNVHRMBAf8EBTADAQH/MB0GA1Ud
+DgQWBBSJgmd9xJ0mcABLtFBIfN49rgRufTBYBgNVHR8EUTBPME2gS6BJhkdodHRw
+Oi8vY3JsLnVzZXJ0cnVzdC5jb20vVVROLVVTRVJGaXJzdC1DbGllbnRBdXRoZW50
+aWNhdGlvbmFuZEVtYWlsLmNybDAdBgNVHSUEFjAUBggrBgEFBQcDAgYIKwYBBQUH
+AwQwDQYJKoZIhvcNAQEFBQADggEBALFtYV2mGn98q0rkMPxTbyUkxsrt4jFcKw7u
+7mFVbwQ+zznexRtJlOTrIEy05p5QLnLZjfWqo7NK2lYcYJeA3IKirUq9iiv/Cwm0
+xtcgBEXkzYABurorbs6q15L+5K/r9CYdFip/bDCVNy8zEqx/3cfREYxRmLLQo5HQ
+rfafnoOTHh1CuEava2bwm3/q4wMC5QJRwarVNZ1yQAOJujEdxRBoUp7fooXFXAim
+eOZTT7Hot9MUnpOmw2TjrH5xzbyf6QMbzPvprDHBr3wVdAKZw7JHpsIyYdfHb0gk
+USeh1YdV8nuPmD0Wnu51tvjQjvLzxq4oW6fw8zYX/MMF08oDSlQ=
+-----END CERTIFICATE-----
+
subject= /C=CN/O=UniTrust/CN=UCA Root
serial=09
-----BEGIN CERTIFICATE-----
@@ -18624,6 +18376,39 @@
9DkVWlIBe94y1k049hJcBlDfBVu9FEuh3ym6O0GN92NWod8isQ==
-----END CERTIFICATE-----
+subject= /C=DK/O=TDC/CN=TDC OCES CA
+serial=3E48BDC4
+-----BEGIN CERTIFICATE-----
+MIIFGTCCBAGgAwIBAgIEPki9xDANBgkqhkiG9w0BAQUFADAxMQswCQYDVQQGEwJE
+SzEMMAoGA1UEChMDVERDMRQwEgYDVQQDEwtUREMgT0NFUyBDQTAeFw0wMzAyMTEw
+ODM5MzBaFw0zNzAyMTEwOTA5MzBaMDExCzAJBgNVBAYTAkRLMQwwCgYDVQQKEwNU
+REMxFDASBgNVBAMTC1REQyBPQ0VTIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A
+MIIBCgKCAQEArGL2YSCyz8DGhdfjeebM7fI5kqSXLmSjhFuHnEz9pPPEXyG9VhDr
+2y5h7JNp46PMvZnDBfwGuMo2HP6QjklMxFaaL1a8z3sM8W9Hpg1DTeLpHTk0zY0s
+2RKY+ePhwUp8hjjEqcRhiNJerxomTdXkoCJHhNlktxmW/OwZ5LKXJk5KTMuPJItU
+GBxIYXvViGjaXbXqzRowwYCDdlCqT9HU3Tjw7xb04QxQBr/q+3pJoSgrHPb8FTKj
+dGqPqcNiKXEx5TukYBdedObaE+3pHx8b0bJoc8YQNHVGEBDjkAB2QMuLt0MJIf+r
+TpPGWOmlgtt3xDqZsXKVSQTwtyv6e1mO3QIDAQABo4ICNzCCAjMwDwYDVR0TAQH/
+BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwgewGA1UdIASB5DCB4TCB3gYIKoFQgSkB
+AQEwgdEwLwYIKwYBBQUHAgEWI2h0dHA6Ly93d3cuY2VydGlmaWthdC5kay9yZXBv
+c2l0b3J5MIGdBggrBgEFBQcCAjCBkDAKFgNUREMwAwIBARqBgUNlcnRpZmlrYXRl
+ciBmcmEgZGVubmUgQ0EgdWRzdGVkZXMgdW5kZXIgT0lEIDEuMi4yMDguMTY5LjEu
+MS4xLiBDZXJ0aWZpY2F0ZXMgZnJvbSB0aGlzIENBIGFyZSBpc3N1ZWQgdW5kZXIg
+T0lEIDEuMi4yMDguMTY5LjEuMS4xLjARBglghkgBhvhCAQEEBAMCAAcwgYEGA1Ud
+HwR6MHgwSKBGoESkQjBAMQswCQYDVQQGEwJESzEMMAoGA1UEChMDVERDMRQwEgYD
+VQQDEwtUREMgT0NFUyBDQTENMAsGA1UEAxMEQ1JMMTAsoCqgKIYmaHR0cDovL2Ny
+bC5vY2VzLmNlcnRpZmlrYXQuZGsvb2Nlcy5jcmwwKwYDVR0QBCQwIoAPMjAwMzAy
+MTEwODM5MzBagQ8yMDM3MDIxMTA5MDkzMFowHwYDVR0jBBgwFoAUYLWF7FZkfhIZ
+J2cdUBVLc647+RIwHQYDVR0OBBYEFGC1hexWZH4SGSdnHVAVS3OuO/kSMB0GCSqG
+SIb2fQdBAAQQMA4bCFY2LjA6NC4wAwIEkDANBgkqhkiG9w0BAQUFAAOCAQEACrom
+JkbTc6gJ82sLMJn9iuFXehHTuJTXCRBuo7E4A9G28kNBKWKnctj7fAXmMXAnVBhO
+inxO5dHKjHiIzxvTkIvmI/gLDjNDfZziChmPyQE+dF10yYscA+UYyAFMP8uXBV2Y
+caaYb7Z8vTd/vuGTJW1v8AqtFxjhA7wHKcitJuj4YfD9IQl+mo6paH1IYnK9AOoB
+mbgGglGBTvH1tJFUuSN6AJqfXY3gPGS5GhKSKseCRHI53OI8xthV9RVOyAUO28bQ
+YqbsFbS1AoLbrIyigfCbmTH1ICCoiGEKB5+U/NDXG8wuF/MEJ3Zn61SD/aSQfgY9
+BKNDLdr8C2LqL19iUw==
+-----END CERTIFICATE-----
+
subject= /C=DE/O=TC TrustCenter GmbH/OU=TC TrustCenter Universal CA/CN=TC TrustCenter Universal CA II
serial=193300010002281A9A04BCF25545
-----BEGIN CERTIFICATE-----
@@ -18686,6 +18471,71 @@
ZHQqMc7cdalUR6SnQnIJ5+ECpkeyBM1CE+FhDOB4OiIgohxgQoaH96Xm
-----END CERTIFICATE-----
+subject= /C=CH/O=SwissSign AG/CN=SwissSign Platinum CA - G2
+serial=4EB200670C035D4F
+-----BEGIN CERTIFICATE-----
+MIIFwTCCA6mgAwIBAgIITrIAZwwDXU8wDQYJKoZIhvcNAQEFBQAwSTELMAkGA1UE
+BhMCQ0gxFTATBgNVBAoTDFN3aXNzU2lnbiBBRzEjMCEGA1UEAxMaU3dpc3NTaWdu
+IFBsYXRpbnVtIENBIC0gRzIwHhcNMDYxMDI1MDgzNjAwWhcNMzYxMDI1MDgzNjAw
+WjBJMQswCQYDVQQGEwJDSDEVMBMGA1UEChMMU3dpc3NTaWduIEFHMSMwIQYDVQQD
+ExpTd2lzc1NpZ24gUGxhdGludW0gQ0EgLSBHMjCCAiIwDQYJKoZIhvcNAQEBBQAD
+ggIPADCCAgoCggIBAMrfogLi2vj8Bxax3mCq3pZcZB/HL37PZ/pEQtZ2Y5Wu669y
+IIpFR4ZieIbWIDkm9K6j/SPnpZy1IiEZtzeTIsBQnIJ71NUERFzLtMKfkr4k2Htn
+IuJpX+UFeNSH2XFwMyVTtIc7KZAoNppVRDBopIOXfw0enHb/FZ1glwCNioUD7IC+
+6ixuEFGSzH7VozPY1kneWCqv9hbrS3uQMpe5up1Y8fhXSQQeol0GcN1x2/ndi5ob
+jM89o03Oy3z2u5yg+gnOI2Ky6Q0f4nIoj5+saCB9bzuohTEJfwvH6GXp43gOCWcw
+izSC+13gzJ2BbWLuCB4ELE6b7P6pT1/9aXjvCR+htL/68++QHkwFix7qepF6w9fl
++zC8bBsQWJj3Gl/QKTIDE0ZNYWqFTFJ0LwYfexHihJfGmfNtf9dng34TaNhxKFrY
+zt3oEBSa/m0jh26OWnA81Y0JAKeqvLAxN23IhBQeW71FYyBrS3SMvds6DsHPWhaP
+pZjydomyExI7C3d3rLvlPClKknLKYRorXkzig3R3+jVIeoVNjZpTxN94ypeRSCtF
+KwH3HBqi7Ri6Cr2D+m+8jVeTO9TUps4e8aCxzqv9KyiaTxvXw3LbpMS/XUz13XuW
+ae5ogObnmLo2t/5u7Su9IPhlGdpVCX4l3P5hYnL5fhgC72O00Puv5TtjjGePAgMB
+AAGjgawwgakwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0O
+BBYEFFCvzAeHFUdvOMW0ZdHelarp35zMMB8GA1UdIwQYMBaAFFCvzAeHFUdvOMW0
+ZdHelarp35zMMEYGA1UdIAQ/MD0wOwYJYIV0AVkBAQEBMC4wLAYIKwYBBQUHAgEW
+IGh0dHA6Ly9yZXBvc2l0b3J5LnN3aXNzc2lnbi5jb20vMA0GCSqGSIb3DQEBBQUA
+A4ICAQAIhab1Fgz8RBrBY+D5VUYI/HAcQiiWjrfFwUF1TglxeeVtlspLpYhg0DB0
+uMoI3LQwnkAHFmtllXcBrqS3NQuB2nEVqXQXOHtYyvkv+8Bldo1bAbl93oI9ZLi+
+FHSjClTTLJUYFzX1UWs/j6KWYTl4a0vlpqD4U99REJNi54Av4tHgvI42Rncz7Lj7
+jposiU0xEQ8mngS7twSNC/K5/FqdOxa3L8iYq/6KUFkuozv8KV2LwUvJ4ooTHbG/
+u0IdUt1O2BReEMYxB+9xJ/cbOQncguqLs5WGXv312l0xpuAxtpTmREl0xRbl9x8D
+YSjFyMsSoEJL+WuICI20MhjzdZ/EfwBPBZWcoxcCw7NTm6ogOSkrZvqdr16zktK1
+puEa+S1BaYEUtLS17Yk9zvupnTVCRLEcFHOBzyoBNZox1S2PbYTfgE1X4z/FhHXa
+icYwu+uPyyIIoK6q8QNsOktNCaUOcsZWayFCTiMlFGiudgp8DAdwZPmaL/YFOSbG
+DI8Zf0NebvRbFS/bYV3mZy8/CJT5YLSYMdp08YSTcU1f+2BY0fvEwW2JorsgH51x
+kcsymxM9Pn2SUjWskpSi0xjCfMfqr3YFFt1nJ8J+HAciIfNAChs0B0QTwoRqjt8Z
+Wr9/6x3iGjjRXK9HkmuAtTClyY3YqzGBH9/CZjfTk6mFhnll0g==
+-----END CERTIFICATE-----
+
+subject= /C=DE/ST=Baden-Wuerttemberg (BW)/L=Stuttgart/O=Deutscher Sparkassen Verlag GmbH/CN=S-TRUST Authentication and Encryption Root CA 2005:PN
+serial=371918E653547C1AB5B8CB595ADB35B7
+-----BEGIN CERTIFICATE-----
+MIIEezCCA2OgAwIBAgIQNxkY5lNUfBq1uMtZWts1tzANBgkqhkiG9w0BAQUFADCB
+rjELMAkGA1UEBhMCREUxIDAeBgNVBAgTF0JhZGVuLVd1ZXJ0dGVtYmVyZyAoQlcp
+MRIwEAYDVQQHEwlTdHV0dGdhcnQxKTAnBgNVBAoTIERldXRzY2hlciBTcGFya2Fz
+c2VuIFZlcmxhZyBHbWJIMT4wPAYDVQQDEzVTLVRSVVNUIEF1dGhlbnRpY2F0aW9u
+IGFuZCBFbmNyeXB0aW9uIFJvb3QgQ0EgMjAwNTpQTjAeFw0wNTA2MjIwMDAwMDBa
+Fw0zMDA2MjEyMzU5NTlaMIGuMQswCQYDVQQGEwJERTEgMB4GA1UECBMXQmFkZW4t
+V3VlcnR0ZW1iZXJnIChCVykxEjAQBgNVBAcTCVN0dXR0Z2FydDEpMCcGA1UEChMg
+RGV1dHNjaGVyIFNwYXJrYXNzZW4gVmVybGFnIEdtYkgxPjA8BgNVBAMTNVMtVFJV
+U1QgQXV0aGVudGljYXRpb24gYW5kIEVuY3J5cHRpb24gUm9vdCBDQSAyMDA1OlBO
+MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2bVKwdMz6tNGs9HiTNL1
+toPQb9UY6ZOvJ44TzbUlNlA0EmQpoVXhOmCTnijJ4/Ob4QSwI7+Vio5bG0F/WsPo
+TUzVJBY+h0jUJ67m91MduwwA7z5hca2/OnpYH5Q9XIHV1W/fuJvS9eXLg3KSwlOy
+ggLrra1fFi2SU3bxibYs9cEv4KdKb6AwajLrmnQDaHgTncovmwsdvs91DSaXm8f1
+XgqfeN+zvOyauu9VjxuapgdjKRdZYgkqeQd3peDRF2npW932kKvimAoA0SVtnteF
+hy+S8dF2g08LOlk3KC8zpxdQ1iALCvQm+Z845y2kuJuJja2tyWp9iRe79n+Ag3rm
+7QIDAQABo4GSMIGPMBIGA1UdEwEB/wQIMAYBAf8CAQAwDgYDVR0PAQH/BAQDAgEG
+MCkGA1UdEQQiMCCkHjAcMRowGAYDVQQDExFTVFJvbmxpbmUxLTIwNDgtNTAdBgNV
+HQ4EFgQUD8oeXHngovMpttKFswtKtWXsa1IwHwYDVR0jBBgwFoAUD8oeXHngovMp
+ttKFswtKtWXsa1IwDQYJKoZIhvcNAQEFBQADggEBAK8B8O0ZPCjoTVy7pWMciDMD
+pwCHpB8gq9Yc4wYfl35UvbfRssnV2oDsF9eK9XvCAPbpEW+EoFolMeKJ+aQAPzFo
+LtU96G7m1R08P7K9n3frndOMusDXtk3sU5wPBG7qNWdX4wple5A64U8+wwCSersF
+iXOMy6ZNwPv2AtawB6MDwidAnwzkhYItr5pCHdDHjfhA7p0GVxzZotiAFP7hYy0y
+h9WUUpY6RsZxlj33mA6ykaqP2vROJAA5VeitF7nTNCtKqUDMFypVZUF0Qn71wK/I
+k63yGFs9iQzbRzkk+OBM8h+wPQrKBU6JIRrjKpms/H+h8Q8bHz2eBIPdltkdOpQ=
+-----END CERTIFICATE-----
+
subject= /C=US/ST=Arizona/L=Scottsdale/O=Starfield Technologies, Inc./OU=http://certificates.starfieldtech.com/repository//CN=Starfield Services Root Certificate Authority
serial=00
-----BEGIN CERTIFICATE-----
@@ -18830,6 +18680,28 @@
MzAb3Sn48EKjbkKlbvpWpalQg9EFZhaLLfvmktHmbAvVWiltK89519naT/Botg==
-----END CERTIFICATE-----
+subject= /C=FI/O=Sonera/CN=Sonera Class1 CA
+serial=24
+-----BEGIN CERTIFICATE-----
+MIIDIDCCAgigAwIBAgIBJDANBgkqhkiG9w0BAQUFADA5MQswCQYDVQQGEwJGSTEP
+MA0GA1UEChMGU29uZXJhMRkwFwYDVQQDExBTb25lcmEgQ2xhc3MxIENBMB4XDTAx
+MDQwNjEwNDkxM1oXDTIxMDQwNjEwNDkxM1owOTELMAkGA1UEBhMCRkkxDzANBgNV
+BAoTBlNvbmVyYTEZMBcGA1UEAxMQU29uZXJhIENsYXNzMSBDQTCCASIwDQYJKoZI
+hvcNAQEBBQADggEPADCCAQoCggEBALWJHytPZwp5/8Ue+H887dF+2rDNbS82rDTG
+29lkFwhjMDMiikzujrsPDUJVyZ0upe/3p4zDq7mXy47vPxVnqIJyY1MPQYx9EJUk
+oVqlBvqSV536pQHydekfvFYmUk54GWVYVQNYwBSujHxVX3BbdyMGNpfzJLWaRpXk
+3w0LBUXl0fIdgrvGE+D+qnr9aTCU89JFhfzyMlsy3uhsXR/LpCJ0sICOXZT3BgBL
+qdReLjVQCfOAl/QMF6452F/NM8EcyonCIvdFEu1eEpOdY6uCLrnrQkFEy0oaAIIN
+nvmLVz5MxxftLItyM19yejhW1ebZrgUaHXVFsculJRwSVzb9IjcCAwEAAaMzMDEw
+DwYDVR0TAQH/BAUwAwEB/zARBgNVHQ4ECgQIR+IMi/ZTiFIwCwYDVR0PBAQDAgEG
+MA0GCSqGSIb3DQEBBQUAA4IBAQCLGrLJXWG04bkruVPRsoWdd44W7hE928Jj2VuX
+ZfsSZ9gqXLar5V7DtxYvyOirHYr9qxp81V9jz9yw3Xe5qObSIjiHBxTZ/75Wtf0H
+DjxVyhbMp6Z3N/vbXB9OWQaHowND9Rart4S9Tu+fMTfwRvFAttEMpWT4Y14h21VO
+TzF2nBBhjrZTOqMRvq9tfB69ri3iDGnHhVNoomG6xT60eVR4ngrHAr5i0RGCS2Uv
+kVrCqIexVmiUefkl98HVrhq4uz2PqYo4Ffdz0Fpg0YCw8NzVUM1O7pJIae2yIx4w
+zMiUyLb1O4Z/P6Yun/Y+LLWSlj7fLJOK/4GMDw9ZIRlXvVWa
+-----END CERTIFICATE-----
+
subject= /C=si/O=state-institutions/OU=sigov-ca
serial=3A5C701A
-----BEGIN CERTIFICATE-----
@@ -19368,29 +19240,46 @@
tQOGuCBmwEhvuazUSgNVsjffBN0iDFOGKkoqocE4PjzlPN91lw==
-----END CERTIFICATE-----
-subject= /C=JP/O=Japanese Government/OU=MPHPT/OU=MPHPT Certification Authority
-serial=00
+subject= /C=HU/L=Budapest/O=NetLock Halozatbiztonsagi Kft./OU=Tanusitvanykiadok/CN=NetLock Minositett Kozjegyzoi (Class QA) Tanusitvanykiado/emailAddress=info@netlock.hu
+serial=7B
-----BEGIN CERTIFICATE-----
-MIIDtDCCApygAwIBAgIBADANBgkqhkiG9w0BAQUFADBjMQswCQYDVQQGEwJKUDEc
-MBoGA1UEChMTSmFwYW5lc2UgR292ZXJubWVudDEOMAwGA1UECxMFTVBIUFQxJjAk
-BgNVBAsTHU1QSFBUIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTAyMDMxNDA3
-NTAyNloXDTEyMDMxMzE0NTk1OVowYzELMAkGA1UEBhMCSlAxHDAaBgNVBAoTE0ph
-cGFuZXNlIEdvdmVybm1lbnQxDjAMBgNVBAsTBU1QSFBUMSYwJAYDVQQLEx1NUEhQ
-VCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTCCASIwDQYJKoZIhvcNAQEBBQADggEP
-ADCCAQoCggEBAI3GUWlK9G9FVm8DhpKu5t37oxZbj6lZcFvEZY07YrYojWO657ub
-z56WE7q/PI/6Sm7i7qYE+Vp80r6thJvfmn7SS3BENrRqiapSenhooYD12jIe3iZQ
-2SXqx7WgYwyBGdQwGaYTijzbRFpgc0K8o4a99fIoHhz9J8AKqXasddMCqfJRaH30
-YJ7HnOvRYGL6HBrGhJ7X4Rzijyk9a9+3VOBsYcnIlx9iODoiYhA6r0ojuIu8/JA1
-oTTZrS0MyU/SLdFdJze2O1wnqTULXQybzJz3ad6oC/F5a69c0m92akYd9nGBrPxj
-EhucaQynC/QoCLs3aciLgioAnEJqy7i3EgUCAwEAAaNzMHEwHwYDVR0jBBgwFoAU
-YML3pLoA0h93Yngl8Gb/UgAh73owHQYDVR0OBBYEFGDC96S6ANIfd2J4JfBm/1IA
-Ie96MAwGA1UdEwQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMBEGCWCGSAGG+EIBAQQE
-AwIABTANBgkqhkiG9w0BAQUFAAOCAQEANPR8DN66iWZBs/lSm1vOzhqRkXDLT6xL
-LvJtjPLqmE469szGyFSKzsof6y+/8YgZlOoeX1inF4ox/SH1ATnwdIIsPbXuRLjt
-axboXvBh5y2ffC3hmzJVvJ87tb6mVWQeL9VFUhNhAI0ib+9OIZVEYI/64MFkDk4e
-iWG5ts6oqIJH1V7dVZg6pQ1Tc0Ckhn6N1m1hD30S0/zoPn/20Wq6OCF3he8VJrRG
-dcW9BD/Bkesko1HKhMBDjHVrJ8cFwbnDSoo+Ki47eJWaz/cOzaSsaMVUsR5POava
-/abhhgHn/eOJdXiVslyK0DYscjsdB3aBUfwZlomxYOzG6CgjQPhJdw==
+MIIG0TCCBbmgAwIBAgIBezANBgkqhkiG9w0BAQUFADCByTELMAkGA1UEBhMCSFUx
+ETAPBgNVBAcTCEJ1ZGFwZXN0MScwJQYDVQQKEx5OZXRMb2NrIEhhbG96YXRiaXp0
+b25zYWdpIEtmdC4xGjAYBgNVBAsTEVRhbnVzaXR2YW55a2lhZG9rMUIwQAYDVQQD
+EzlOZXRMb2NrIE1pbm9zaXRldHQgS296amVneXpvaSAoQ2xhc3MgUUEpIFRhbnVz
+aXR2YW55a2lhZG8xHjAcBgkqhkiG9w0BCQEWD2luZm9AbmV0bG9jay5odTAeFw0w
+MzAzMzAwMTQ3MTFaFw0yMjEyMTUwMTQ3MTFaMIHJMQswCQYDVQQGEwJIVTERMA8G
+A1UEBxMIQnVkYXBlc3QxJzAlBgNVBAoTHk5ldExvY2sgSGFsb3phdGJpenRvbnNh
+Z2kgS2Z0LjEaMBgGA1UECxMRVGFudXNpdHZhbnlraWFkb2sxQjBABgNVBAMTOU5l
+dExvY2sgTWlub3NpdGV0dCBLb3pqZWd5em9pIChDbGFzcyBRQSkgVGFudXNpdHZh
+bnlraWFkbzEeMBwGCSqGSIb3DQEJARYPaW5mb0BuZXRsb2NrLmh1MIIBIjANBgkq
+hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAx1Ilstg91IRVCacbvWy5FPSKAtt2/Goq
+eKvld/Bu4IwjZ9ulZJm53QE+b+8tmjwi8F3JV6BVQX/yQ15YglMxZc4e8ia6AFQe
+r7C8HORSjKAyr7c3sVNnaHRnUPYtLmTeriZ539+Zhqurf4XsoPuAzPS4DB6TRWO5
+3Lhbm+1bOdRfYrCnjnxmOCyqsQhjF2d9zL2z8cM/z1A57dEZgxXbhxInlrfa6uWd
+vLrqOU+L73Sa58XQ0uqGURzk/mQIKAR5BevKxXEOC++r6uwSEaEYBTJp0QwsGj0l
+mT+1fMptsK6ZmfoIYOcZwvK9UdPM0wKswREMgM6r3JSda6M5UzrWhQIDAMV9o4IC
+wDCCArwwEgYDVR0TAQH/BAgwBgEB/wIBBDAOBgNVHQ8BAf8EBAMCAQYwggJ1Bglg
+hkgBhvhCAQ0EggJmFoICYkZJR1lFTEVNISBFemVuIHRhbnVzaXR2YW55IGEgTmV0
+TG9jayBLZnQuIE1pbm9zaXRldHQgU3pvbGdhbHRhdGFzaSBTemFiYWx5emF0YWJh
+biBsZWlydCBlbGphcmFzb2sgYWxhcGphbiBrZXN6dWx0LiBBIG1pbm9zaXRldHQg
+ZWxla3Ryb25pa3VzIGFsYWlyYXMgam9naGF0YXMgZXJ2ZW55ZXN1bGVzZW5laywg
+dmFsYW1pbnQgZWxmb2dhZGFzYW5hayBmZWx0ZXRlbGUgYSBNaW5vc2l0ZXR0IFN6
+b2xnYWx0YXRhc2kgU3phYmFseXphdGJhbiwgYXogQWx0YWxhbm9zIFN6ZXJ6b2Rl
+c2kgRmVsdGV0ZWxla2JlbiBlbG9pcnQgZWxsZW5vcnplc2kgZWxqYXJhcyBtZWd0
+ZXRlbGUuIEEgZG9rdW1lbnR1bW9rIG1lZ3RhbGFsaGF0b2sgYSBodHRwczovL3d3
+dy5uZXRsb2NrLmh1L2RvY3MvIGNpbWVuIHZhZ3kga2VyaGV0b2sgYXogaW5mb0Bu
+ZXRsb2NrLm5ldCBlLW1haWwgY2ltZW4uIFdBUk5JTkchIFRoZSBpc3N1YW5jZSBh
+bmQgdGhlIHVzZSBvZiB0aGlzIGNlcnRpZmljYXRlIGFyZSBzdWJqZWN0IHRvIHRo
+ZSBOZXRMb2NrIFF1YWxpZmllZCBDUFMgYXZhaWxhYmxlIGF0IGh0dHBzOi8vd3d3
+Lm5ldGxvY2suaHUvZG9jcy8gb3IgYnkgZS1tYWlsIGF0IGluZm9AbmV0bG9jay5u
+ZXQwHQYDVR0OBBYEFAlqYhaSsFq7VQ7LdTI6MuWyIckoMA0GCSqGSIb3DQEBBQUA
+A4IBAQCRalCc23iBmz+LQuM7/KbD7kPgz/PigDVJRXYC4uMvBcXxKufAQTPGtpvQ
+MznNwNuhrWw3AkxYQTvyl5LGSKjN5Yo5iWH5Upfpvfb5lHTocQ68d4bDBsxafEp+
+NFAwLvt/MpqNPfMgW/hqyobzMUwsWYACff44yTB1HLdV47yfuqhthCgFdbOLDcCR
+VCHnpgu0mfVRQdzNo0ci2ccBgcTcR08m6h/t280NmPSjnLRzMkqWmf68f8glWPhY
+83ZmiVSkpj7EUFy6iRiCdUgh0k8T6GB+B3bbELVR5qq5aKrN9p2QdRLqOBrKROi3
+macqaJVmlaut74nLYKkGEsaUR+ko
-----END CERTIFICATE-----
subject= /DC=com/DC=microsoft/CN=Microsoft Root Certificate Authority
@@ -20591,6 +20480,31 @@
Zb9PlfQdvcS6yU2BUcI/WtkS9CEb1pXqPZD+qZPi
-----END CERTIFICATE-----
+subject= /CN=ComSign CA/O=ComSign/C=IL
+serial=1413968314558CEA7B63E5FC34877744
+-----BEGIN CERTIFICATE-----
+MIIDkzCCAnugAwIBAgIQFBOWgxRVjOp7Y+X8NId3RDANBgkqhkiG9w0BAQUFADA0
+MRMwEQYDVQQDEwpDb21TaWduIENBMRAwDgYDVQQKEwdDb21TaWduMQswCQYDVQQG
+EwJJTDAeFw0wNDAzMjQxMTMyMThaFw0yOTAzMTkxNTAyMThaMDQxEzARBgNVBAMT
+CkNvbVNpZ24gQ0ExEDAOBgNVBAoTB0NvbVNpZ24xCzAJBgNVBAYTAklMMIIBIjAN
+BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA8ORUaSvTx49qROR+WCf4C9DklBKK
+8Rs4OC8fMZwG1Cyn3gsqrhqg455qv588x26i+YtkbDqthVVRVKU4VbirgwTyP2Q2
+98CNQ0NqZtH3FyrV7zb6MBBC11PN+fozc0yz6YQgitZBJzXkOPqUm7h65HkfM/sb
+2CEJKHxNGGleZIp6GZPKfuzzcuc3B1hZKKxC+cX/zT/npfo4sdAMx9lSGlPWgcxC
+ejVb7Us6eva1jsz/D3zkYDaHL63woSV9/9JLEYhwVKZBqGdTUkJe5DSe5L6j7Kpi
+Xd3DTKaCQeQzC6zJMw9kglcq/QytNuEMrkvF7zuZ2SOzW120V+x0cAwqTwIDAQAB
+o4GgMIGdMAwGA1UdEwQFMAMBAf8wPQYDVR0fBDYwNDAyoDCgLoYsaHR0cDovL2Zl
+ZGlyLmNvbXNpZ24uY28uaWwvY3JsL0NvbVNpZ25DQS5jcmwwDgYDVR0PAQH/BAQD
+AgGGMB8GA1UdIwQYMBaAFEsBmz5WGmU2dst7l6qSBe4y5ygxMB0GA1UdDgQWBBRL
+AZs+VhplNnbLe5eqkgXuMucoMTANBgkqhkiG9w0BAQUFAAOCAQEA0Nmlfv4pYEWd
+foPPbrxHbvUanlR2QnG0PFg/LUAlQvaBnPGJEMgOqnhPOAlXsDzACPw1jvFIUY0M
+cXS6hMTXcpuEfDhOZAYnKuGntewImbQKDdSFc8gS4TXt8QUxHXOZDOuWyt3T5oWq
+8Ir7dcHyCTxlZWTzTNity4hp8+SDtwy9F1qWF8pb/627HOkthIDYIb6FUtnUdLlp
+hbpN7Sgy6/lhSuTENh4Z3G+EER+V9YMoGKgzkkMn3V0TBEVPh9VGzT2ouvDzuFYk
+Res3x+F2T3I5GN9+dHLHcy056mDmrRGiVod7w2ia/viMcKjfZTL0pECMocJEAw6U
+AGegcQCCSA==
+-----END CERTIFICATE-----
+
subject= /CN=ComSign Advanced Security CA
serial=7A5DE933D0049EB34A1A1774CB169B6D
-----BEGIN CERTIFICATE-----
@@ -20728,6 +20642,23 @@
jwhhJBY=
-----END CERTIFICATE-----
+subject= /C=US/O=VeriSign, Inc./OU=Class 2 Public Primary Certification Authority
+serial=2D1BFC4A178DA391EBE7FFF58B45BE0B
+-----BEGIN CERTIFICATE-----
+MIICPDCCAaUCEC0b/EoXjaOR6+f/9YtFvgswDQYJKoZIhvcNAQECBQAwXzELMAkG
+A1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMTcwNQYDVQQLEy5DbGFz
+cyAyIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTk2
+MDEyOTAwMDAwMFoXDTI4MDgwMTIzNTk1OVowXzELMAkGA1UEBhMCVVMxFzAVBgNV
+BAoTDlZlcmlTaWduLCBJbmMuMTcwNQYDVQQLEy5DbGFzcyAyIFB1YmxpYyBQcmlt
+YXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIGfMA0GCSqGSIb3DQEBAQUAA4GN
+ADCBiQKBgQC2WoujDWojg4BrzzmH9CETMwZMJaLtVRKXxaeAufqDwSCg+i8VDXyh
+YGt+eSz6Bg86rvYbb7HS/y8oUl+DfUvEerf4Zh+AVPy3wo5ZShRXRtGak75BkQO7
+FYCTXOvnzAhsPz6zSvz/S2wj1VCCJkQZjiPDceoZJEcEnnW/yKYAHwIDAQABMA0G
+CSqGSIb3DQEBAgUAA4GBAIobK/o5wXTXXtgZZKJYSi034DNHD6zt96rbHuSLBlxg
+J8pFUs4W7z8GZOeUaHxgMxURaa+dYo2jA1Rrpr7l7gUYYAS/QoD90KioHgE796Nc
+r6Pc5iaAIzy4RHT3Cq5Ji2F4zCS/iIqnDupzGUH9TQPwiNHleI2lKk/2lw0Xd8rY
+-----END CERTIFICATE-----
+
subject= /C=US/O=VeriSign, Inc./OU=Class 1 Public Primary Certification Authority
serial=325033CF50D156F35C81AD655C4FC825
-----BEGIN CERTIFICATE-----
@@ -20745,6 +20676,24 @@
VgQlDHx8h50kp9jwMim1pN9dokzFFjKoQvZFprY2ueC/ZTaTwtLXa9zeWdaiNfhF
-----END CERTIFICATE-----
+subject= /C=US/O=VeriSign, Inc./OU=Class 1 Public Primary Certification Authority
+serial=CDBA7F56F0DFE4BC54FE22ACB372AA55
+-----BEGIN CERTIFICATE-----
+MIICPTCCAaYCEQDNun9W8N/kvFT+IqyzcqpVMA0GCSqGSIb3DQEBAgUAMF8xCzAJ
+BgNVBAYTAlVTMRcwFQYDVQQKEw5WZXJpU2lnbiwgSW5jLjE3MDUGA1UECxMuQ2xh
+c3MgMSBQdWJsaWMgUHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw05
+NjAxMjkwMDAwMDBaFw0yODA4MDEyMzU5NTlaMF8xCzAJBgNVBAYTAlVTMRcwFQYD
+VQQKEw5WZXJpU2lnbiwgSW5jLjE3MDUGA1UECxMuQ2xhc3MgMSBQdWJsaWMgUHJp
+bWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTCBnzANBgkqhkiG9w0BAQEFAAOB
+jQAwgYkCgYEA5Rm/baNWYS2ZSHH2Z965jeu3noaACpEO+jglr0aIguVzqKCbJF0N
+H8xlbgyw0FaEGIeaBpsQoXPftFg5a27B9hXVqKg/qhIGjTGsf7A01480Z4gJzRQR
+4k5FVmkfeAKA2txHkSm7NsljXMXg1y2He6G3MrB7MLoqLzGq7qNn2tsCAwEAATAN
+BgkqhkiG9w0BAQIFAAOBgQBMP7iLxmjf7kMzDl3ppssHhE16M/+SG/Q2rdiVIjZo
+EWx8QszznC7EBz8UsA9P/5CSdvnivErpj82ggAr3xSnxgiJduLHdgSOjeyUVRjB5
+FvjqBUuUfx3CHMjjt/QQQDwTw18fU+hI5Ia0e6E1sHslurjTjqs/OJ0ANACY89Fx
+lA==
+-----END CERTIFICATE-----
+
subject= /C=FR/O=Certplus/CN=Class 1 Primary CA
serial=86FE1D5FC381F847D7332C7394757B37
-----BEGIN CERTIFICATE-----
diff --git a/lib/google-api-python-client/google_api_python_client.egg-info/PKG-INFO b/lib/google-api-python-client/google_api_python_client.egg-info/PKG-INFO
deleted file mode 100644
index 6d89aa9..0000000
--- a/lib/google-api-python-client/google_api_python_client.egg-info/PKG-INFO
+++ /dev/null
@@ -1,17 +0,0 @@
-Metadata-Version: 1.0
-Name: google-api-python-client
-Version: 1.0beta6
-Summary: Google API Client Library for Python
-Home-page: http://code.google.com/p/google-api-python-client/
-Author: Joe Gregorio
-Author-email: jcgregorio@google.com
-License: Apache 2.0
-Description: The Google API Client for Python is a client library for
- accessing the Buzz, Moderator, and Latitude APIs.
-Keywords: google api client
-Platform: UNKNOWN
-Classifier: Development Status :: 4 - Beta
-Classifier: Intended Audience :: Developers
-Classifier: License :: OSI Approved :: Apache Software License
-Classifier: Operating System :: POSIX
-Classifier: Topic :: Internet :: WWW/HTTP
diff --git a/lib/google-api-python-client/google_api_python_client.egg-info/SOURCES.txt b/lib/google-api-python-client/google_api_python_client.egg-info/SOURCES.txt
deleted file mode 100644
index 7323c76..0000000
--- a/lib/google-api-python-client/google_api_python_client.egg-info/SOURCES.txt
+++ /dev/null
@@ -1,295 +0,0 @@
-MANIFEST.in
-README
-runsamples.py
-runtests.py
-setpath.sh
-setup.py
-apiclient/__init__.py
-apiclient/anyjson.py
-apiclient/discovery.py
-apiclient/errors.py
-apiclient/http.py
-apiclient/mimeparse.py
-apiclient/model.py
-apiclient/oauth.py
-apiclient/contrib/__init__.py
-apiclient/contrib/buzz/__init__.py
-apiclient/contrib/buzz/future.json
-apiclient/contrib/latitude/__init__.py
-apiclient/contrib/latitude/future.json
-apiclient/contrib/moderator/__init__.py
-apiclient/contrib/moderator/future.json
-apiclient/ext/__init__.py
-apiclient/ext/appengine.py
-apiclient/ext/authtools.py
-apiclient/ext/django_orm.py
-apiclient/ext/file.py
-bin/enable-app-engine-project
-docs/apiclient.anyjson.html
-docs/apiclient.contrib.buzz.html
-docs/apiclient.contrib.html
-docs/apiclient.contrib.latitude.html
-docs/apiclient.contrib.moderator.html
-docs/apiclient.discovery.html
-docs/apiclient.errors.html
-docs/apiclient.ext.appengine.html
-docs/apiclient.ext.authtools.html
-docs/apiclient.ext.django_orm.html
-docs/apiclient.ext.file.html
-docs/apiclient.ext.html
-docs/apiclient.html
-docs/apiclient.http.html
-docs/apiclient.mimeparse.html
-docs/apiclient.model.html
-docs/apiclient.oauth.html
-docs/httplib2.html
-docs/httplib2.iri2uri.html
-docs/httplib2.socks.html
-docs/oauth2client.appengine.html
-docs/oauth2client.client.html
-docs/oauth2client.clientsecrets.html
-docs/oauth2client.django_orm.html
-docs/oauth2client.file.html
-docs/oauth2client.html
-docs/oauth2client.multistore_file.html
-docs/oauth2client.tools.html
-docs/uritemplate.html
-docs/dyn/adexchangebuyer.v1.accounts.html
-docs/dyn/adexchangebuyer.v1.html
-docs/dyn/adsense.v1.adclients.html
-docs/dyn/adsense.v1.adunits.html
-docs/dyn/adsense.v1.customchannels.html
-docs/dyn/adsense.v1.html
-docs/dyn/adsense.v1.reports.html
-docs/dyn/adsense.v1.urlchannels.html
-docs/dyn/analytics.v3.html
-docs/dyn/analytics.v3.management.accounts.html
-docs/dyn/analytics.v3.management.goals.html
-docs/dyn/analytics.v3.management.html
-docs/dyn/analytics.v3.management.profiles.html
-docs/dyn/analytics.v3.management.segments.html
-docs/dyn/analytics.v3.management.webproperties.html
-docs/dyn/audit.v1.activities.html
-docs/dyn/audit.v1.html
-docs/dyn/blogger.v2.blogs.html
-docs/dyn/blogger.v2.comments.html
-docs/dyn/blogger.v2.html
-docs/dyn/blogger.v2.pages.html
-docs/dyn/blogger.v2.posts.html
-docs/dyn/blogger.v2.users.blogs.html
-docs/dyn/blogger.v2.users.html
-docs/dyn/books.v1.bookshelves.html
-docs/dyn/books.v1.bookshelves.volumes.html
-docs/dyn/books.v1.html
-docs/dyn/books.v1.mylibrary.bookshelves.html
-docs/dyn/books.v1.mylibrary.bookshelves.volumes.html
-docs/dyn/books.v1.mylibrary.html
-docs/dyn/books.v1.volumes.html
-docs/dyn/buzz.v1.activities.html
-docs/dyn/buzz.v1.comments.html
-docs/dyn/buzz.v1.groups.html
-docs/dyn/buzz.v1.html
-docs/dyn/buzz.v1.people.html
-docs/dyn/buzz.v1.photoAlbums.html
-docs/dyn/buzz.v1.photos.html
-docs/dyn/buzz.v1.related.html
-docs/dyn/chromewebstore.v1.html
-docs/dyn/chromewebstore.v1.licenses.html
-docs/dyn/customsearch.v1.cse.html
-docs/dyn/customsearch.v1.html
-docs/dyn/diacritize.v1.diacritize.corpus.html
-docs/dyn/diacritize.v1.diacritize.html
-docs/dyn/diacritize.v1.html
-docs/dyn/discovery.v1.apis.html
-docs/dyn/discovery.v1.html
-docs/dyn/freebase.v1-dev.html
-docs/dyn/freebase.v1-dev.text.html
-docs/dyn/freebase.v1-dev.user.html
-docs/dyn/freebase.v1.html
-docs/dyn/freebase.v1.text.html
-docs/dyn/latitude.v1.currentLocation.html
-docs/dyn/latitude.v1.html
-docs/dyn/latitude.v1.location.html
-docs/dyn/moderator.v1.featured.html
-docs/dyn/moderator.v1.featured.series.html
-docs/dyn/moderator.v1.global.html
-docs/dyn/moderator.v1.global.series.html
-docs/dyn/moderator.v1.html
-docs/dyn/moderator.v1.my.html
-docs/dyn/moderator.v1.my.series.html
-docs/dyn/moderator.v1.myrecent.html
-docs/dyn/moderator.v1.myrecent.series.html
-docs/dyn/moderator.v1.profiles.html
-docs/dyn/moderator.v1.responses.html
-docs/dyn/moderator.v1.series.html
-docs/dyn/moderator.v1.series.responses.html
-docs/dyn/moderator.v1.series.submissions.html
-docs/dyn/moderator.v1.submissions.html
-docs/dyn/moderator.v1.tags.html
-docs/dyn/moderator.v1.topics.html
-docs/dyn/moderator.v1.topics.submissions.html
-docs/dyn/moderator.v1.votes.html
-docs/dyn/orkut.v2.acl.html
-docs/dyn/orkut.v2.activities.html
-docs/dyn/orkut.v2.activityVisibility.html
-docs/dyn/orkut.v2.badges.html
-docs/dyn/orkut.v2.comments.html
-docs/dyn/orkut.v2.counters.html
-docs/dyn/orkut.v2.html
-docs/dyn/pagespeedonline.v1.html
-docs/dyn/pagespeedonline.v1.pagespeedapi.html
-docs/dyn/plus.v1.activities.html
-docs/dyn/plus.v1.comments.html
-docs/dyn/plus.v1.html
-docs/dyn/plus.v1.people.html
-docs/dyn/prediction.v1.1.html
-docs/dyn/prediction.v1.1.training.html
-docs/dyn/prediction.v1.2.hostedmodels.html
-docs/dyn/prediction.v1.2.html
-docs/dyn/prediction.v1.2.training.html
-docs/dyn/prediction.v1.3.hostedmodels.html
-docs/dyn/prediction.v1.3.html
-docs/dyn/prediction.v1.3.training.html
-docs/dyn/prediction.v1.4.hostedmodels.html
-docs/dyn/prediction.v1.4.html
-docs/dyn/prediction.v1.4.trainedmodels.html
-docs/dyn/shopping.v1.html
-docs/dyn/shopping.v1.products.html
-docs/dyn/siteVerification.v1.html
-docs/dyn/siteVerification.v1.webResource.html
-docs/dyn/taskqueue.v1beta1.html
-docs/dyn/taskqueue.v1beta1.taskqueues.html
-docs/dyn/taskqueue.v1beta1.tasks.html
-docs/dyn/tasks.v1.html
-docs/dyn/tasks.v1.tasklists.html
-docs/dyn/tasks.v1.tasks.html
-docs/dyn/translate.v2.detections.html
-docs/dyn/translate.v2.html
-docs/dyn/translate.v2.languages.html
-docs/dyn/translate.v2.translations.html
-docs/dyn/transparencyreport.v1.html
-docs/dyn/transparencyreport.v1.traffic.html
-docs/dyn/urlshortener.v1.html
-docs/dyn/urlshortener.v1.url.html
-docs/dyn/webfonts.v1.html
-docs/dyn/webfonts.v1.webfonts.html
-functional_tests/__init__.py
-functional_tests/test_services.py
-google_api_python_client.egg-info/PKG-INFO
-google_api_python_client.egg-info/SOURCES.txt
-google_api_python_client.egg-info/dependency_links.txt
-google_api_python_client.egg-info/requires.txt
-google_api_python_client.egg-info/top_level.txt
-oauth2client/__init__.py
-oauth2client/appengine.py
-oauth2client/client.py
-oauth2client/clientsecrets.py
-oauth2client/django_orm.py
-oauth2client/file.py
-oauth2client/multistore_file.py
-oauth2client/tools.py
-samples/analytics/management_v3_reference.py
-samples/api-python-client-doc/app.yaml
-samples/api-python-client-doc/gadget.html
-samples/api-python-client-doc/index.html
-samples/api-python-client-doc/index.yaml
-samples/api-python-client-doc/main.py
-samples/appengine/app.yaml
-samples/appengine/index.yaml
-samples/appengine/main.py
-samples/appengine/welcome.html
-samples/appengine_with_decorator/app.yaml
-samples/appengine_with_decorator/better.py
-samples/appengine_with_decorator/index.yaml
-samples/appengine_with_decorator/main.py
-samples/appengine_with_decorator2/app.yaml
-samples/appengine_with_decorator2/client_secrets.json
-samples/appengine_with_decorator2/grant.html
-samples/appengine_with_decorator2/index.yaml
-samples/appengine_with_decorator2/main.py
-samples/appengine_with_decorator2/welcome.html
-samples/appengine_with_robots/app.yaml
-samples/appengine_with_robots/main.py
-samples/appengine_with_robots/welcome.html
-samples/audit/audit.py
-samples/buzz/buzz.py
-samples/buzz/client_secrets.json
-samples/customsearch/main.py
-samples/debugging/main.py
-samples/diacritize/main.py
-samples/django_sample/__init__.py
-samples/django_sample/manage.py
-samples/django_sample/settings.py
-samples/django_sample/urls.py
-samples/django_sample/buzz/__init__.py
-samples/django_sample/buzz/models.py
-samples/django_sample/buzz/tests.py
-samples/django_sample/buzz/views.py
-samples/django_sample/templates/buzz/login.html
-samples/django_sample/templates/buzz/welcome.html
-samples/gan/ccoffers/offers.py
-samples/gan/ccoffers/offers_template.html
-samples/gan/events/events.py
-samples/gan/events/events_template.html
-samples/gtaskqueue_sample/setup.py
-samples/gtaskqueue_sample/gtaskqueue/client_task.py
-samples/gtaskqueue_sample/gtaskqueue/gen_appengine_access_token
-samples/gtaskqueue_sample/gtaskqueue/gtaskqueue
-samples/gtaskqueue_sample/gtaskqueue/gtaskqueue_puller
-samples/gtaskqueue_sample/gtaskqueue/task_cmds.py
-samples/gtaskqueue_sample/gtaskqueue/taskqueue_client.py
-samples/gtaskqueue_sample/gtaskqueue/taskqueue_cmd_base.py
-samples/gtaskqueue_sample/gtaskqueue/taskqueue_cmds.py
-samples/gtaskqueue_sample/gtaskqueue/taskqueue_logger.py
-samples/latitude/latitude.py
-samples/local/main.py
-samples/localdiscovery/buzz.json
-samples/localdiscovery/buzz.py
-samples/moderator/moderator.py
-samples/new_project_template/app.yaml
-samples/new_project_template/index.yaml
-samples/new_project_template/main.py
-samples/new_project_template/welcome.html
-samples/prediction/client_secrets.json
-samples/prediction/prediction.py
-samples/prediction/prediction_language_id.py
-samples/prediction/prediction_number.py
-samples/searchforshopping/basic.py
-samples/searchforshopping/crowding.py
-samples/searchforshopping/fulltextsearch.py
-samples/searchforshopping/histograms.py
-samples/searchforshopping/main.py
-samples/searchforshopping/pagination.py
-samples/searchforshopping/ranking.py
-samples/searchforshopping/restricting.py
-samples/src/buzz.py
-samples/src/moderator.py
-samples/src/prediction.py
-samples/src/urlshortener.py
-samples/tasks_appengine/app.yaml
-samples/tasks_appengine/main.py
-samples/tasks_appengine/templates/index.html
-samples/threadqueue/main.py
-samples/translate/main.py
-samples/urlshortener/urlshortener.py
-tests/__init__.py
-tests/test_discovery.py
-tests/test_errors.py
-tests/test_http.py
-tests/test_json_model.py
-tests/test_mocks.py
-tests/test_model.py
-tests/test_oauth.py
-tests/test_oauth2client.py
-tests/test_oauth2client_appengine.py
-tests/test_oauth2client_clientsecrets.py
-tests/test_oauth2client_django_orm.py
-tests/test_oauth2client_file.py
-tests/test_protobuf_model.py
-tests/data/buzz.json
-tests/data/latitude.json
-tests/data/malformed.json
-tests/data/moderator.json
-tests/data/tasks.json
-tests/data/zoo.json
-uritemplate/__init__.py
\ No newline at end of file
diff --git a/lib/google-api-python-client/google_api_python_client.egg-info/dependency_links.txt b/lib/google-api-python-client/google_api_python_client.egg-info/dependency_links.txt
deleted file mode 100644
index 8b13789..0000000
--- a/lib/google-api-python-client/google_api_python_client.egg-info/dependency_links.txt
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/lib/google-api-python-client/google_api_python_client.egg-info/requires.txt b/lib/google-api-python-client/google_api_python_client.egg-info/requires.txt
deleted file mode 100644
index c060c17..0000000
--- a/lib/google-api-python-client/google_api_python_client.egg-info/requires.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-httplib2
-oauth2
-python-gflags
\ No newline at end of file
diff --git a/lib/google-api-python-client/google_api_python_client.egg-info/top_level.txt b/lib/google-api-python-client/google_api_python_client.egg-info/top_level.txt
deleted file mode 100644
index 46edc8b..0000000
--- a/lib/google-api-python-client/google_api_python_client.egg-info/top_level.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-oauth2client
-apiclient
-uritemplate
diff --git a/lib/webob/PKG-INFO b/lib/webob_0_9/PKG-INFO
similarity index 100%
rename from lib/webob/PKG-INFO
rename to lib/webob_0_9/PKG-INFO
diff --git a/lib/webob/WebOb.egg-info/PKG-INFO b/lib/webob_0_9/WebOb.egg-info/PKG-INFO
similarity index 100%
rename from lib/webob/WebOb.egg-info/PKG-INFO
rename to lib/webob_0_9/WebOb.egg-info/PKG-INFO
diff --git a/lib/webob/WebOb.egg-info/SOURCES.txt b/lib/webob_0_9/WebOb.egg-info/SOURCES.txt
similarity index 100%
rename from lib/webob/WebOb.egg-info/SOURCES.txt
rename to lib/webob_0_9/WebOb.egg-info/SOURCES.txt
diff --git a/lib/webob/WebOb.egg-info/dependency_links.txt b/lib/webob_0_9/WebOb.egg-info/dependency_links.txt
similarity index 100%
rename from lib/webob/WebOb.egg-info/dependency_links.txt
rename to lib/webob_0_9/WebOb.egg-info/dependency_links.txt
diff --git a/lib/webob/WebOb.egg-info/top_level.txt b/lib/webob_0_9/WebOb.egg-info/top_level.txt
similarity index 100%
rename from lib/webob/WebOb.egg-info/top_level.txt
rename to lib/webob_0_9/WebOb.egg-info/top_level.txt
diff --git a/lib/webob/WebOb.egg-info/zip-safe b/lib/webob_0_9/WebOb.egg-info/zip-safe
similarity index 100%
rename from lib/webob/WebOb.egg-info/zip-safe
rename to lib/webob_0_9/WebOb.egg-info/zip-safe
diff --git a/lib/webob/docs/comment-example-code/example.py b/lib/webob_0_9/docs/comment-example-code/example.py
old mode 100755
new mode 100644
similarity index 100%
rename from lib/webob/docs/comment-example-code/example.py
rename to lib/webob_0_9/docs/comment-example-code/example.py
diff --git a/lib/webob/docs/comment-example.txt b/lib/webob_0_9/docs/comment-example.txt
similarity index 100%
rename from lib/webob/docs/comment-example.txt
rename to lib/webob_0_9/docs/comment-example.txt
diff --git a/lib/webob/docs/differences.txt b/lib/webob_0_9/docs/differences.txt
similarity index 100%
rename from lib/webob/docs/differences.txt
rename to lib/webob_0_9/docs/differences.txt
diff --git a/lib/webob/docs/file-example.txt b/lib/webob_0_9/docs/file-example.txt
similarity index 100%
rename from lib/webob/docs/file-example.txt
rename to lib/webob_0_9/docs/file-example.txt
diff --git a/lib/webob/docs/index.txt b/lib/webob_0_9/docs/index.txt
similarity index 100%
rename from lib/webob/docs/index.txt
rename to lib/webob_0_9/docs/index.txt
diff --git a/lib/webob/docs/license.txt b/lib/webob_0_9/docs/license.txt
similarity index 100%
rename from lib/webob/docs/license.txt
rename to lib/webob_0_9/docs/license.txt
diff --git a/lib/webob/docs/news.txt b/lib/webob_0_9/docs/news.txt
similarity index 100%
rename from lib/webob/docs/news.txt
rename to lib/webob_0_9/docs/news.txt
diff --git a/lib/webob/docs/reference.txt b/lib/webob_0_9/docs/reference.txt
similarity index 100%
rename from lib/webob/docs/reference.txt
rename to lib/webob_0_9/docs/reference.txt
diff --git a/lib/webob/docs/test-file.txt b/lib/webob_0_9/docs/test-file.txt
similarity index 100%
rename from lib/webob/docs/test-file.txt
rename to lib/webob_0_9/docs/test-file.txt
diff --git a/lib/webob/docs/wiki-example-code/example.py b/lib/webob_0_9/docs/wiki-example-code/example.py
old mode 100755
new mode 100644
similarity index 100%
rename from lib/webob/docs/wiki-example-code/example.py
rename to lib/webob_0_9/docs/wiki-example-code/example.py
diff --git a/lib/webob/docs/wiki-example.txt b/lib/webob_0_9/docs/wiki-example.txt
similarity index 100%
rename from lib/webob/docs/wiki-example.txt
rename to lib/webob_0_9/docs/wiki-example.txt
diff --git a/lib/webob/setup.cfg b/lib/webob_0_9/setup.cfg
similarity index 100%
rename from lib/webob/setup.cfg
rename to lib/webob_0_9/setup.cfg
diff --git a/lib/webob/setup.py b/lib/webob_0_9/setup.py
old mode 100755
new mode 100644
similarity index 100%
rename from lib/webob/setup.py
rename to lib/webob_0_9/setup.py
diff --git a/lib/webob/test b/lib/webob_0_9/test
old mode 100755
new mode 100644
similarity index 100%
rename from lib/webob/test
rename to lib/webob_0_9/test
diff --git a/lib/webob/tests/__init__.py b/lib/webob_0_9/tests/__init__.py
old mode 100755
new mode 100644
similarity index 100%
rename from lib/webob/tests/__init__.py
rename to lib/webob_0_9/tests/__init__.py
diff --git a/lib/webob/tests/conftest.py b/lib/webob_0_9/tests/conftest.py
old mode 100755
new mode 100644
similarity index 100%
rename from lib/webob/tests/conftest.py
rename to lib/webob_0_9/tests/conftest.py
diff --git a/lib/webob/tests/test_request.py b/lib/webob_0_9/tests/test_request.py
old mode 100755
new mode 100644
similarity index 100%
rename from lib/webob/tests/test_request.py
rename to lib/webob_0_9/tests/test_request.py
diff --git a/lib/webob/tests/test_request.txt b/lib/webob_0_9/tests/test_request.txt
similarity index 100%
rename from lib/webob/tests/test_request.txt
rename to lib/webob_0_9/tests/test_request.txt
diff --git a/lib/webob/tests/test_response.py b/lib/webob_0_9/tests/test_response.py
old mode 100755
new mode 100644
similarity index 100%
rename from lib/webob/tests/test_response.py
rename to lib/webob_0_9/tests/test_response.py
diff --git a/lib/webob/tests/test_response.txt b/lib/webob_0_9/tests/test_response.txt
similarity index 100%
rename from lib/webob/tests/test_response.txt
rename to lib/webob_0_9/tests/test_response.txt
diff --git a/lib/webob/webob/__init__.py b/lib/webob_0_9/webob/__init__.py
old mode 100755
new mode 100644
similarity index 100%
rename from lib/webob/webob/__init__.py
rename to lib/webob_0_9/webob/__init__.py
diff --git a/lib/webob/webob/acceptparse.py b/lib/webob_0_9/webob/acceptparse.py
old mode 100755
new mode 100644
similarity index 100%
rename from lib/webob/webob/acceptparse.py
rename to lib/webob_0_9/webob/acceptparse.py
diff --git a/lib/webob/webob/byterange.py b/lib/webob_0_9/webob/byterange.py
old mode 100755
new mode 100644
similarity index 100%
rename from lib/webob/webob/byterange.py
rename to lib/webob_0_9/webob/byterange.py
diff --git a/lib/webob/webob/cachecontrol.py b/lib/webob_0_9/webob/cachecontrol.py
old mode 100755
new mode 100644
similarity index 100%
rename from lib/webob/webob/cachecontrol.py
rename to lib/webob_0_9/webob/cachecontrol.py
diff --git a/lib/webob/webob/datastruct.py b/lib/webob_0_9/webob/datastruct.py
old mode 100755
new mode 100644
similarity index 100%
rename from lib/webob/webob/datastruct.py
rename to lib/webob_0_9/webob/datastruct.py
diff --git a/lib/webob/webob/etag.py b/lib/webob_0_9/webob/etag.py
old mode 100755
new mode 100644
similarity index 100%
rename from lib/webob/webob/etag.py
rename to lib/webob_0_9/webob/etag.py
diff --git a/lib/webob/webob/exc.py b/lib/webob_0_9/webob/exc.py
old mode 100755
new mode 100644
similarity index 100%
rename from lib/webob/webob/exc.py
rename to lib/webob_0_9/webob/exc.py
diff --git a/lib/webob/webob/headerdict.py b/lib/webob_0_9/webob/headerdict.py
old mode 100755
new mode 100644
similarity index 100%
rename from lib/webob/webob/headerdict.py
rename to lib/webob_0_9/webob/headerdict.py
diff --git a/lib/webob/webob/multidict.py b/lib/webob_0_9/webob/multidict.py
old mode 100755
new mode 100644
similarity index 100%
rename from lib/webob/webob/multidict.py
rename to lib/webob_0_9/webob/multidict.py
diff --git a/lib/webob/webob/statusreasons.py b/lib/webob_0_9/webob/statusreasons.py
old mode 100755
new mode 100644
similarity index 100%
rename from lib/webob/webob/statusreasons.py
rename to lib/webob_0_9/webob/statusreasons.py
diff --git a/lib/webob/webob/updatedict.py b/lib/webob_0_9/webob/updatedict.py
old mode 100755
new mode 100644
similarity index 100%
rename from lib/webob/webob/updatedict.py
rename to lib/webob_0_9/webob/updatedict.py
diff --git a/lib/webob/webob/util/__init__.py b/lib/webob_0_9/webob/util/__init__.py
old mode 100755
new mode 100644
similarity index 100%
rename from lib/webob/webob/util/__init__.py
rename to lib/webob_0_9/webob/util/__init__.py
diff --git a/lib/webob/webob/util/dictmixin.py b/lib/webob_0_9/webob/util/dictmixin.py
old mode 100755
new mode 100644
similarity index 100%
rename from lib/webob/webob/util/dictmixin.py
rename to lib/webob_0_9/webob/util/dictmixin.py
diff --git a/lib/webob/webob/util/reversed.py b/lib/webob_0_9/webob/util/reversed.py
old mode 100755
new mode 100644
similarity index 100%
rename from lib/webob/webob/util/reversed.py
rename to lib/webob_0_9/webob/util/reversed.py
diff --git a/lib/webob/webob/util/safegzip.py b/lib/webob_0_9/webob/util/safegzip.py
old mode 100755
new mode 100644
similarity index 100%
rename from lib/webob/webob/util/safegzip.py
rename to lib/webob_0_9/webob/util/safegzip.py
diff --git a/lib/webob/webob/util/stringtemplate.py b/lib/webob_0_9/webob/util/stringtemplate.py
old mode 100755
new mode 100644
similarity index 100%
rename from lib/webob/webob/util/stringtemplate.py
rename to lib/webob_0_9/webob/util/stringtemplate.py
diff --git a/lib/webob_1_1_1/LICENSE b/lib/webob_1_1_1/LICENSE
new file mode 100644
index 0000000..2369791
--- /dev/null
+++ b/lib/webob_1_1_1/LICENSE
@@ -0,0 +1,23 @@
+License
+=======
+
+Copyright (c) 2007 Ian Bicking and Contributors
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/lib/webob_1_1_1/MANIFEST.in b/lib/webob_1_1_1/MANIFEST.in
new file mode 100644
index 0000000..13a224e
--- /dev/null
+++ b/lib/webob_1_1_1/MANIFEST.in
@@ -0,0 +1,4 @@
+recursive-include tests *
+recursive-include docs *
+prune *.pyc
+prune *.pyo
\ No newline at end of file
diff --git a/lib/webob_1_1_1/PKG-INFO b/lib/webob_1_1_1/PKG-INFO
new file mode 100644
index 0000000..546f4b2
--- /dev/null
+++ b/lib/webob_1_1_1/PKG-INFO
@@ -0,0 +1,37 @@
+Metadata-Version: 1.0
+Name: WebOb
+Version: 1.1.1
+Summary: WSGI request and response object
+Home-page: http://webob.org/
+Author: Sergey Schetinin
+Author-email: sergey@maluke.com
+License: MIT
+Description: WebOb provides wrappers around the WSGI request environment, and an
+ object to help create WSGI responses.
+
+ The objects map much of the specified behavior of HTTP, including
+ header parsing and accessors for other standard parts of the
+ environment.
+
+ You may install the `in-development version of WebOb
+ <http://bitbucket.org/ianb/webob/get/tip.gz#egg=WebOb-dev>`_ with
+ ``pip install WebOb==dev`` (or ``easy_install WebOb==dev``).
+
+ * `WebOb reference <http://docs.webob.org/en/latest/reference.html>`_
+ * `Bug tracker <https://bitbucket.org/ianb/webob/issues>`_
+ * `Browse source code <https://bitbucket.org/ianb/webob/src>`_
+ * `Mailing list <http://bit.ly/paste-users>`_
+ * `Release news <http://docs.webob.org/en/latest/news.html>`_
+ * `Detailed changelog <https://bitbucket.org/ianb/webob/changesets>`_
+
+Keywords: wsgi request web http
+Platform: UNKNOWN
+Classifier: Development Status :: 6 - Mature
+Classifier: Intended Audience :: Developers
+Classifier: License :: OSI Approved :: MIT License
+Classifier: Topic :: Internet :: WWW/HTTP :: WSGI
+Classifier: Topic :: Internet :: WWW/HTTP :: WSGI :: Application
+Classifier: Topic :: Internet :: WWW/HTTP :: WSGI :: Middleware
+Classifier: Programming Language :: Python :: 2.5
+Classifier: Programming Language :: Python :: 2.6
+Classifier: Programming Language :: Python :: 2.7
diff --git a/lib/webob_1_1_1/WebOb.egg-info/PKG-INFO b/lib/webob_1_1_1/WebOb.egg-info/PKG-INFO
new file mode 100644
index 0000000..546f4b2
--- /dev/null
+++ b/lib/webob_1_1_1/WebOb.egg-info/PKG-INFO
@@ -0,0 +1,37 @@
+Metadata-Version: 1.0
+Name: WebOb
+Version: 1.1.1
+Summary: WSGI request and response object
+Home-page: http://webob.org/
+Author: Sergey Schetinin
+Author-email: sergey@maluke.com
+License: MIT
+Description: WebOb provides wrappers around the WSGI request environment, and an
+ object to help create WSGI responses.
+
+ The objects map much of the specified behavior of HTTP, including
+ header parsing and accessors for other standard parts of the
+ environment.
+
+ You may install the `in-development version of WebOb
+ <http://bitbucket.org/ianb/webob/get/tip.gz#egg=WebOb-dev>`_ with
+ ``pip install WebOb==dev`` (or ``easy_install WebOb==dev``).
+
+ * `WebOb reference <http://docs.webob.org/en/latest/reference.html>`_
+ * `Bug tracker <https://bitbucket.org/ianb/webob/issues>`_
+ * `Browse source code <https://bitbucket.org/ianb/webob/src>`_
+ * `Mailing list <http://bit.ly/paste-users>`_
+ * `Release news <http://docs.webob.org/en/latest/news.html>`_
+ * `Detailed changelog <https://bitbucket.org/ianb/webob/changesets>`_
+
+Keywords: wsgi request web http
+Platform: UNKNOWN
+Classifier: Development Status :: 6 - Mature
+Classifier: Intended Audience :: Developers
+Classifier: License :: OSI Approved :: MIT License
+Classifier: Topic :: Internet :: WWW/HTTP :: WSGI
+Classifier: Topic :: Internet :: WWW/HTTP :: WSGI :: Application
+Classifier: Topic :: Internet :: WWW/HTTP :: WSGI :: Middleware
+Classifier: Programming Language :: Python :: 2.5
+Classifier: Programming Language :: Python :: 2.6
+Classifier: Programming Language :: Python :: 2.7
diff --git a/lib/webob_1_1_1/WebOb.egg-info/SOURCES.txt b/lib/webob_1_1_1/WebOb.egg-info/SOURCES.txt
new file mode 100644
index 0000000..0ac6321
--- /dev/null
+++ b/lib/webob_1_1_1/WebOb.egg-info/SOURCES.txt
@@ -0,0 +1,69 @@
+MANIFEST.in
+setup.cfg
+setup.py
+WebOb.egg-info/PKG-INFO
+WebOb.egg-info/SOURCES.txt
+WebOb.egg-info/dependency_links.txt
+WebOb.egg-info/top_level.txt
+WebOb.egg-info/zip-safe
+docs/comment-example.txt
+docs/conf.py
+docs/differences.txt
+docs/do-it-yourself.txt
+docs/doctests.py
+docs/file-example.txt
+docs/index.txt
+docs/jsonrpc-example.txt
+docs/license.txt
+docs/news.txt
+docs/reference.txt
+docs/test-file.txt
+docs/test_dec.txt
+docs/test_request.txt
+docs/test_response.txt
+docs/wiki-example.txt
+docs/comment-example-code/example.py
+docs/jsonrpc-example-code/jsonrpc.py
+docs/jsonrpc-example-code/test_jsonrpc.py
+docs/jsonrpc-example-code/test_jsonrpc.txt
+docs/modules/dec.txt
+docs/modules/webob.txt
+docs/pycon2011/pycon-py3k-sprint.txt
+docs/pycon2011/request_table.rst
+docs/pycon2011/response_table.rst
+docs/wiki-example-code/example.py
+tests/__init__.py
+tests/conftest.py
+tests/performance_test.py
+tests/test_acceptparse.py
+tests/test_byterange.py
+tests/test_cachecontrol.py
+tests/test_cookies.py
+tests/test_datetime_utils.py
+tests/test_dec.py
+tests/test_descriptors.py
+tests/test_etag.py
+tests/test_exc.py
+tests/test_headers.py
+tests/test_in_wsgiref.py
+tests/test_misc.py
+tests/test_multidict.py
+tests/test_request.py
+tests/test_request_nose.py
+tests/test_response.py
+tests/test_util.py
+webob/__init__.py
+webob/acceptparse.py
+webob/byterange.py
+webob/cachecontrol.py
+webob/cookies.py
+webob/datetime_utils.py
+webob/dec.py
+webob/descriptors.py
+webob/etag.py
+webob/exc.py
+webob/headers.py
+webob/multidict.py
+webob/request.py
+webob/response.py
+webob/util.py
\ No newline at end of file
diff --git a/lib/webob/WebOb.egg-info/dependency_links.txt b/lib/webob_1_1_1/WebOb.egg-info/dependency_links.txt
similarity index 100%
copy from lib/webob/WebOb.egg-info/dependency_links.txt
copy to lib/webob_1_1_1/WebOb.egg-info/dependency_links.txt
diff --git a/lib/webob/WebOb.egg-info/top_level.txt b/lib/webob_1_1_1/WebOb.egg-info/top_level.txt
similarity index 100%
copy from lib/webob/WebOb.egg-info/top_level.txt
copy to lib/webob_1_1_1/WebOb.egg-info/top_level.txt
diff --git a/lib/webob/WebOb.egg-info/zip-safe b/lib/webob_1_1_1/WebOb.egg-info/zip-safe
similarity index 100%
copy from lib/webob/WebOb.egg-info/zip-safe
copy to lib/webob_1_1_1/WebOb.egg-info/zip-safe
diff --git a/lib/webob/docs/comment-example-code/example.py b/lib/webob_1_1_1/docs/comment-example-code/example.py
old mode 100755
new mode 100644
similarity index 100%
copy from lib/webob/docs/comment-example-code/example.py
copy to lib/webob_1_1_1/docs/comment-example-code/example.py
diff --git a/lib/webob_1_1_1/docs/comment-example.txt b/lib/webob_1_1_1/docs/comment-example.txt
new file mode 100644
index 0000000..c0915db
--- /dev/null
+++ b/lib/webob_1_1_1/docs/comment-example.txt
@@ -0,0 +1,414 @@
+Comment Example
+===============
+
+.. contents::
+
+Introduction
+------------
+
+This is an example of how to write WSGI middleware with WebOb. The
+specific example adds a simple comment form to HTML web pages; any
+page served through the middleware that is HTML gets a comment form
+added to it, and shows any existing comments.
+
+Code
+----
+
+The finished code for this is available in
+`docs/comment-example-code/example.py
+<http://bitbucket.org/ianb/webob/src/tip/docs/comment-example-code/example.py>`_
+-- you can run that file as a script to try it out.
+
+Instantiating Middleware
+------------------------
+
+Middleware of any complexity at all is usually best created as a
+class with its configuration as arguments to that class.
+
+Every middleware needs an application (``app``) that it wraps. This
+middleware also needs a location to store the comments; we'll put them
+all in a single directory.
+
+.. code-block:: python
+
+ import os
+
+ class Commenter(object):
+ def __init__(self, app, storage_dir):
+ self.app = app
+ self.storage_dir = storage_dir
+ if not os.path.exists(storage_dir):
+ os.makedirs(storage_dir)
+
+When you use this middleware, you'll use it like:
+
+.. code-block:: python
+
+ app = ... make the application ...
+ app = Commenter(app, storage_dir='./comments')
+
+For our application we'll use a simple static file server that is
+included with `Paste <http://pythonpaste.org>`_ (use ``easy_install
+Paste`` to install this). The setup is all at the bottom of
+``example.py``, and looks like this:
+
+.. code-block:: python
+
+ if __name__ == '__main__':
+ import optparse
+ parser = optparse.OptionParser(
+ usage='%prog --port=PORT BASE_DIRECTORY'
+ )
+ parser.add_option(
+ '-p', '--port',
+ default='8080',
+ dest='port',
+ type='int',
+ help='Port to serve on (default 8080)')
+ parser.add_option(
+ '--comment-data',
+ default='./comments',
+ dest='comment_data',
+ help='Place to put comment data into (default ./comments/)')
+ options, args = parser.parse_args()
+ if not args:
+ parser.error('You must give a BASE_DIRECTORY')
+ base_dir = args[0]
+ from paste.urlparser import StaticURLParser
+ app = StaticURLParser(base_dir)
+ app = Commenter(app, options.comment_data)
+ from wsgiref.simple_server import make_server
+ httpd = make_server('localhost', options.port, app)
+ print 'Serving on http://localhost:%s' % options.port
+ try:
+ httpd.serve_forever()
+ except KeyboardInterrupt:
+ print '^C'
+
+I won't explain it here, but basically it takes some options, creates
+an application that serves static files
+(``StaticURLParser(base_dir)``), wraps it with ``Commenter(app,
+options.comment_data)`` then serves that.
+
+The Middleware
+--------------
+
+While we've created the class structure for the middleware, it doesn't
+actually do anything. Here's a kind of minimal version of the
+middleware (using WebOb):
+
+.. code-block:: python
+
+ from webob import Request
+
+ class Commenter(object):
+
+ def __init__(self, app, storage_dir):
+ self.app = app
+ self.storage_dir = storage_dir
+ if not os.path.exists(storage_dir):
+ os.makedirs(storage_dir)
+
+ def __call__(self, environ, start_response):
+ req = Request(environ)
+ resp = req.get_response(self.app)
+ return resp(environ, start_response)
+
+This doesn't modify the response it any way. You could write it like
+this without WebOb:
+
+.. code-block:: python
+
+ class Commenter(object):
+ ...
+ def __call__(self, environ, start_response):
+ return self.app(environ, start_response)
+
+But it won't be as convenient later. First, lets create a little bit
+of infrastructure for our middleware. We need to save and load
+per-url data (the comments themselves). We'll keep them in pickles,
+where each url has a pickle named after the url (but double-quoted, so
+``http://localhost:8080/index.html`` becomes
+``http%3A%2F%2Flocalhost%3A8080%2Findex.html``).
+
+.. code-block:: python
+
+ from cPickle import load, dump
+
+ class Commenter(object):
+ ...
+
+ def get_data(self, url):
+ filename = self.url_filename(url)
+ if not os.path.exists(filename):
+ return []
+ else:
+ f = open(filename, 'rb')
+ data = load(f)
+ f.close()
+ return data
+
+ def save_data(self, url, data):
+ filename = self.url_filename(url)
+ f = open(filename, 'wb')
+ dump(data, f)
+ f.close()
+
+ def url_filename(self, url):
+ # Double-quoting makes the filename safe
+ return os.path.join(self.storage_dir, urllib.quote(url, ''))
+
+You can get the full request URL with ``req.url``, so to get the
+comment data with these methods you do ``data =
+self.get_data(req.url)``.
+
+Now we'll update the ``__call__`` method to filter *some* responses,
+and get the comment data for those. We don't want to change responses
+that were error responses (anything but ``200``), nor do we want to
+filter responses that aren't HTML. So we get:
+
+.. code-block:: python
+
+ class Commenter(object):
+ ...
+
+ def __call__(self, environ, start_response):
+ req = Request(environ)
+ resp = req.get_response(self.app)
+ if resp.content_type != 'text/html' or resp.status_int != 200:
+ return resp(environ, start_response)
+ data = self.get_data(req.url)
+ ... do stuff with data, update resp ...
+ return resp(environ, start_response)
+
+So far we're punting on actually adding the comments to the page. We
+also haven't defined what ``data`` will hold. Let's say it's a list
+of dictionaries, where each dictionary looks like ``{'name': 'John
+Doe', 'homepage': 'http://blog.johndoe.com', 'comments': 'Great
+site!'}``.
+
+We'll also need a simple method to add stuff to the page. We'll use a
+regular expression to find the end of the page and put text in:
+
+.. code-block:: python
+
+ import re
+
+ class Commenter(object):
+ ...
+
+ _end_body_re = re.compile(r'</body.*?>', re.I|re.S)
+
+ def add_to_end(self, html, extra_html):
+ """
+ Adds extra_html to the end of the html page (before </body>)
+ """
+ match = self._end_body_re.search(html)
+ if not match:
+ return html + extra_html
+ else:
+ return html[:match.start()] + extra_html + html[match.start():]
+
+And then we'll use it like:
+
+.. code-block:: python
+
+ data = self.get_data(req.url)
+ body = resp.body
+ body = self.add_to_end(body, self.format_comments(data))
+ resp.body = body
+ return resp(environ, start_response)
+
+We get the body, update it, and put it back in the response. This
+also updates ``Content-Length``. Then we define:
+
+.. code-block:: python
+
+ from webob import html_escape
+
+ class Commenter(object):
+ ...
+
+ def format_comments(self, comments):
+ if not comments:
+ return ''
+ text = []
+ text.append('<hr>')
+ text.append('<h2><a name="comment-area"></a>Comments (%s):</h2>' % len(comments))
+ for comment in comments:
+ text.append('<h3><a href="%s">%s</a> at %s:</h3>' % (
+ html_escape(comment['homepage']), html_escape(comment['name']),
+ time.strftime('%c', comment['time'])))
+ # Susceptible to XSS attacks!:
+ text.append(comment['comments'])
+ return ''.join(text)
+
+We put in a header (with an anchor we'll use later), and a section for
+each comment. Note that ``html_escape`` is the same as ``cgi.escape``
+and just turns ``&`` into ``&``, etc.
+
+Because we put in some text without quoting it is susceptible to a
+`Cross-Site Scripting
+<http://en.wikipedia.org/wiki/Cross-site_scripting>`_ attack. Fixing
+that is beyond the scope of this tutorial; you could quote it or clean
+it with something like `lxml.html.clean
+<http://codespeak.net/lxml/lxmlhtml.html#cleaning-up-html>`_.
+
+Accepting Comments
+------------------
+
+All of those pieces *display* comments, but still no one can actually
+make comments. To handle this we'll take a little piece of the URL
+space for our own, everything under ``/.comments``, so when someone
+POSTs there it will add a comment.
+
+When the request comes in there are two parts to the path:
+``SCRIPT_NAME`` and ``PATH_INFO``. Everything in ``SCRIPT_NAME`` has
+already been parsed, and everything in ``PATH_INFO`` has yet to be
+parsed. That means that the URL *without* ``PATH_INFO`` is the path
+to the middleware; we can intercept anything else below
+``SCRIPT_NAME`` but nothing above it. The name for the URL without
+``PATH_INFO`` is ``req.application_url``. We have to capture it early
+to make sure it doesn't change (since the WSGI application we are
+wrapping may update ``SCRIPT_NAME`` and ``PATH_INFO``).
+
+So here's what this all looks like:
+
+.. code-block:: python
+
+ class Commenter(object):
+ ...
+
+ def __call__(self, environ, start_response):
+ req = Request(environ)
+ if req.path_info_peek() == '.comments':
+ return self.process_comment(req)(environ, start_response)
+ # This is the base path of *this* middleware:
+ base_url = req.application_url
+ resp = req.get_response(self.app)
+ if resp.content_type != 'text/html' or resp.status_int != 200:
+ # Not an HTML response, we don't want to
+ # do anything to it
+ return resp(environ, start_response)
+ # Make sure the content isn't gzipped:
+ resp.decode_content()
+ comments = self.get_data(req.url)
+ body = resp.body
+ body = self.add_to_end(body, self.format_comments(comments))
+ body = self.add_to_end(body, self.submit_form(base_url, req))
+ resp.body = body
+ return resp(environ, start_response)
+
+``base_url`` is the path where the middleware is located (if you run
+the example server, it will be ``http://localhost:PORT/``). We use
+``req.path_info_peek()`` to look at the next segment of the URL --
+what comes after base_url. If it is ``.comments`` then we handle it
+internally and don't pass the request on.
+
+We also put in a little guard, ``resp.decode_content()`` in case the
+application returns a gzipped response.
+
+Then we get the data, add the comments, add the *form* to make new
+comments, and return the result.
+
+submit_form
+~~~~~~~~~~~
+
+Here's what the form looks like:
+
+.. code-block:: python
+
+ class Commenter(object):
+ ...
+
+ def submit_form(self, base_path, req):
+ return '''<h2>Leave a comment:</h2>
+ <form action="%s/.comments" method="POST">
+ <input type="hidden" name="url" value="%s">
+ <table width="100%%">
+ <tr><td>Name:</td>
+ <td><input type="text" name="name" style="width: 100%%"></td></tr>
+ <tr><td>URL:</td>
+ <td><input type="text" name="homepage" style="width: 100%%"></td></tr>
+ </table>
+ Comments:<br>
+ <textarea name="comments" rows=10 style="width: 100%%"></textarea><br>
+ <input type="submit" value="Submit comment">
+ </form>
+ ''' % (base_path, html_escape(req.url))
+
+Nothing too exciting. It submits a form with the keys ``url`` (the
+URL being commented on), ``name``, ``homepage``, and ``comments``.
+
+process_comment
+~~~~~~~~~~~~~~~
+
+If you look at the method call, what we do is call the method then
+treat the result as a WSGI application:
+
+.. code-block:: python
+
+ return self.process_comment(req)(environ, start_response)
+
+You could write this as:
+
+.. code-block:: python
+
+ response = self.process_comment(req)
+ return response(environ, start_response)
+
+A common pattern in WSGI middleware that *doesn't* use WebOb is to
+just do:
+
+.. code-block:: python
+
+ return self.process_comment(environ, start_response)
+
+But the WebOb style makes it easier to modify the response if you want
+to; modifying a traditional WSGI response/application output requires
+changing your logic flow considerably.
+
+Here's the actual processing code:
+
+.. code-block:: python
+
+ from webob import exc
+ from webob import Response
+
+ class Commenter(object):
+ ...
+
+ def process_comment(self, req):
+ try:
+ url = req.params['url']
+ name = req.params['name']
+ homepage = req.params['homepage']
+ comments = req.params['comments']
+ except KeyError, e:
+ resp = exc.HTTPBadRequest('Missing parameter: %s' % e)
+ return resp
+ data = self.get_data(url)
+ data.append(dict(
+ name=name,
+ homepage=homepage,
+ comments=comments,
+ time=time.gmtime()))
+ self.save_data(url, data)
+ resp = exc.HTTPSeeOther(location=url+'#comment-area')
+ return resp
+
+We either give a Bad Request response (if the form submission is
+somehow malformed), or a redirect back to the original page.
+
+The classes in ``webob.exc`` (like ``HTTPBadRequest`` and
+``HTTPSeeOther``) are Response subclasses that can be used to quickly
+create responses for these non-200 cases where the response body
+usually doesn't matter much.
+
+Conclusion
+----------
+
+This shows how to make response modifying middleware, which is
+probably the most difficult kind of middleware to write with WSGI --
+modifying the request is quite simple in comparison, as you simply
+update ``environ``.
diff --git a/lib/webob_1_1_1/docs/conf.py b/lib/webob_1_1_1/docs/conf.py
new file mode 100644
index 0000000..a3ff681
--- /dev/null
+++ b/lib/webob_1_1_1/docs/conf.py
@@ -0,0 +1,37 @@
+# -*- coding: utf-8 -*-
+
+# http://sphinx.pocoo.org/config.html#build-config
+
+extensions = ['sphinx.ext.autodoc']
+
+source_suffix = '.txt' # The suffix of source filenames.
+master_doc = 'index' # The master toctree document.
+
+project = 'WebOb'
+copyright = '2011, Ian Bicking and contributors'
+version = release = '1.1.1'
+exclude_patterns = ['jsonrpc-example-code/*']
+
+modindex_common_prefix = ['webob.']
+
+# If true, '()' will be appended to :func: etc. cross-reference text.
+#add_function_parentheses = True
+
+# If true, the current module name will be prepended to all description
+# unit titles (such as .. function::).
+#add_module_names = True
+
+
+# html_favicon = ...
+html_add_permalinks = False
+#html_show_sourcelink = True # ?set to False?
+
+# Content template for the index page.
+#html_index = ''
+
+# Custom sidebar templates, maps document names to template names.
+#html_sidebars = {}
+
+# Output file base name for HTML help builder.
+htmlhelp_basename = 'WebObdoc'
+
diff --git a/lib/webob_1_1_1/docs/differences.txt b/lib/webob_1_1_1/docs/differences.txt
new file mode 100644
index 0000000..7cc1777
--- /dev/null
+++ b/lib/webob_1_1_1/docs/differences.txt
@@ -0,0 +1,698 @@
+Differences Between WebOb and Other Systems
++++++++++++++++++++++++++++++++++++++++++++
+
+This document points out some of the API differences between the
+Request and Response object, and the objects in other systems.
+
+.. contents::
+
+paste.wsgiwrappers and Pylons
+=============================
+
+The Pylons ``request`` and ``response`` object are based on
+``paste.wsgiwrappers.WSGIRequest`` and ``WSGIResponse``
+
+There is no concept of ``defaults`` in WebOb. In Paste/Pylons these
+serve as threadlocal settings that control certain policies on the
+request and response object. In WebOb you should make your own
+subclasses to control policy (though in many ways simply being
+explicit elsewhere removes the need for this policy).
+
+Request
+-------
+
+``body``:
+ This is a file-like object in WSGIRequest. In WebOb it is a
+ string (to match Response.body) and the file-like object is
+ available through ``req.body_file``
+
+``languages()``:
+ This is available through ``req.accept_language``, particularly
+ ``req.accept_language.best_matches(fallback_language)``
+
+``match_accept(mimetypes)``:
+ This is available through ``req.accept.first_match(mimetypes)``;
+ or if you trust the client's quality ratings, you can use
+ ``req.accept.best_match(mimetypes)``
+
+``errors``:
+ This controls how unicode decode errors are handled; it is now
+ named ``unicode_errors``
+
+There are also many extra methods and attributes on WebOb Request
+objects.
+
+Response
+--------
+
+``determine_charset()``:
+ Is now available as ``res.charset``
+
+``has_header(header)``:
+ Should be done with ``header in res.headers``
+
+``get_content()`` and ``wsgi_response()``:
+ These are gone; you should use ``res.body`` or ``res(environ,
+ start_response)``
+
+``write(content)``:
+ Available in ``res.body_file.write(content)``.
+
+``flush()`` and ``tell()``:
+ Not available.
+
+There are also many extra methods and attributes on WebOb Response
+objects.
+
+Django
+======
+
+This is a quick summary from reading `the Django documentation
+<http://www.djangoproject.com/documentation/request_response/>`_.
+
+Request
+-------
+
+``encoding``:
+ Is ``req.charset``
+
+``REQUEST``:
+ Is ``req.params``
+
+``FILES``:
+ File uploads are ``cgi.FieldStorage`` objects directly in
+ ``res.POST``
+
+``META``:
+ Is ``req.environ``
+
+``user``:
+ No equivalent (too connected to application model for WebOb).
+ There is ``req.remote_user``, which is only ever a string.
+
+``session``:
+ No equivalent
+
+``raw_post_data``:
+ Available with ``req.body``
+
+``__getitem__(key)``:
+ You have to use ``req.params``
+
+``is_secure()``:
+ No equivalent; you could use ``req.scheme == 'https'``.
+
+QueryDict
+---------
+
+QueryDict is the way Django represents the multi-key dictionary-like
+objects that are request variables (query string and POST body
+variables). The equivalent in WebOb is MultiDict.
+
+Mutability:
+ WebOb dictionaries are sometimes mutable (req.GET is,
+ req.params is not)
+
+Ordering:
+ I believe Django does not order the keys fully; MultiDict is a
+ full ordering. Methods that iterate over the parameters iterate
+ over keys in their order in the original request.
+
+``keys()``, ``items()``, ``values()`` (plus ``iter*``):
+ These return all values in MultiDict, but only the last value for
+ a QueryDict. That is, given ``a=1&a=2`` with MultiDict
+ ``d.items()`` returns ``[('a', '1'), ('a', '2')]``, but QueryDict
+ returns ``[('a', '1')]``
+
+``getlist(key)``:
+ Available as ``d.getall(key)``
+
+``setlist(key)``:
+ No direct equivalent
+
+``appendlist(key, value)``:
+ Available as ``d.add(key, value)``
+
+``setlistdefault(key, default_list)``:
+ No direct equivalent
+
+``lists()``:
+ Is ``d.dict_of_lists()``
+
+The MultiDict object has a ``d.getone(key)`` method, that raises
+KeyError if there is not exactly one key. There is a method
+``d.mixed()`` which returns a version where values are lists *if*
+there are multiple values for a list. This is similar to how many
+cgi-based request forms are represented.
+
+Response
+--------
+
+Constructor:
+ Somewhat different. WebOb takes any keyword arguments as
+ attribute assignments. Django only takes a couple arguments. The
+ ``mimetype`` argument is ``content_type``, and ``content_type`` is
+ the entire ``Content-Type`` header (including charset).
+
+dictionary-like:
+ The Django response object is somewhat dictionary-like, setting
+ headers. The equivalent dictionary-like object is
+ ``res.headers``. In WebOb this is a MultiDict.
+
+``has_header(header)``:
+ Use ``header in res.headers``
+
+``flush()``, ``tell()``:
+ Not available
+
+``content``:
+ Use ``res.body`` for the ``str`` value, ``res.unicode_body`` for
+ the ``unicode`` value
+
+Response Subclasses
+-------------------
+
+These are generally like ``webob.exc`` objects.
+``HttpResponseNotModified`` is ``HTTPNotModified``; this naming
+translation generally works.
+
+CherryPy/TurboGears
+===================
+
+The `CherryPy request object
+<http://www.cherrypy.org/wiki/RequestObject>`_ is also used by
+TurboGears 1.x.
+
+Request
+-------
+
+``app``:
+ No equivalent
+
+``base``:
+ ``req.application_url``
+
+``close()``:
+ No equivalent
+
+``closed``:
+ No equivalent
+
+``config``:
+ No equivalent
+
+``cookie``:
+ A ``SimpleCookie`` object in CherryPy; a dictionary in WebOb
+ (``SimpleCookie`` can represent cookie parameters, but cookie
+ parameters are only sent with responses not requests)
+
+``dispatch``:
+ No equivalent (this is the object dispatcher in CherryPy).
+
+``error_page``, ``error_response``, ``handle_error``:
+ No equivalent
+
+``get_resource()``:
+ Similar to ``req.get_response(app)``
+
+``handler``:
+ No equivalent
+
+``headers``, ``header_list``:
+ The WSGI environment represents headers as a dictionary, available
+ through ``req.headers`` (no list form is available in the request).
+
+``hooks``:
+ No equivalent
+
+``local``:
+ No equivalent
+
+``methods_with_bodies``:
+ This represents methods where CherryPy will automatically try to
+ read the request body. WebOb lazily reads POST requests with the
+ correct content type, and no other bodies.
+
+``namespaces``:
+ No equivalent
+
+``protocol``:
+ As ``req.environ['SERVER_PROTOCOL']``
+
+``query_string``:
+ As ``req.query_string``
+
+``remote``:
+ ``remote.ip`` is like ``req.remote_addr``. ``remote.port`` is not
+ available. ``remote.name`` is in
+ ``req.environ.get('REMOTE_HOST')``
+
+``request_line``:
+ No equivalent
+
+``respond()``:
+ A method that is somewhat similar to ``req.get_response()``.
+
+``rfile``:
+ ``req.body_file``
+
+``run``:
+ No equivalent
+
+``server_protocol``:
+ As ``req.environ['SERVER_PROTOCOL']``
+
+``show_tracebacks``:
+ No equivalent
+
+``throw_errors``:
+ No equivalent
+
+``throws``:
+ No equivalent
+
+``toolmaps``:
+ No equivalent
+
+``wsgi_environ``:
+ As ``req.environ``
+
+Response
+--------
+
+From information `from the wiki
+<http://www.cherrypy.org/wiki/ResponseObject>`_.
+
+``body``:
+ This is an iterable in CherryPy, a string in WebOb;
+ ``res.app_iter`` gives an iterable in WebOb.
+
+``check_timeout``:
+ No equivalent
+
+``collapse_body()``:
+ This turns a stream/iterator body into a single string. Accessing
+ ``res.body`` will do this automatically.
+
+``cookie``:
+ Accessible through ``res.set_cookie(...)``, ``res.delete_cookie``,
+ ``res.unset_cookie()``
+
+``finalize()``:
+ No equivalent
+
+``header_list``:
+ In ``res.headerlist``
+
+``stream``:
+ This can make CherryPy stream the response body out directory.
+ There is direct no equivalent; you can use a dynamically generated
+ iterator to do something similar.
+
+``time``:
+ No equivalent
+
+``timed_out``:
+ No equivalent
+
+Yaro
+====
+
+`Yaro <http://lukearno.com/projects/yaro/>`_ is a small wrapper around
+the WSGI environment, much like WebOb in scope.
+
+The WebOb objects have many more methods and attributes. The Yaro
+Response object is a much smaller subset of WebOb's Response.
+
+Request
+-------
+
+``query``:
+ As ``req.GET``
+
+``form``:
+ As ``req.POST``
+
+``cookie``:
+ A ``SimpleCookie`` object in Yaro; a dictionary in WebOb
+ (``SimpleCookie`` can represent cookie parameters, but cookie
+ parameters are only sent with responses not requests)
+
+``uri``:
+ Returns a URI object, no equivalent (only string URIs available).
+
+``redirect``:
+ Not available (response-related). ``webob.exc.HTTPFound()`` can
+ be useful here.
+
+``forward(yaroapp)``, ``wsgi_forward(wsgiapp)``:
+ Available with ``req.get_response(app)`` and
+ ``req.call_application(app)``. In both cases it is a WSGI
+ application in WebOb, there is no special kind of communication;
+ ``req.call_application()`` just returns a ``webob.Response`` object.
+
+``res``:
+ The request object in WebOb *may* have a ``req.response``
+ attribute.
+
+Werkzeug
+========
+
+Probably not that many people know about this library, which is a
+offshoot of `Pocoo <http://pocoo.org>`_, and used to go by another
+name (Columbrid?) This library is based around WSGI, similar to Paste
+and Yaro.
+
+This is taken from the `wrapper documentation
+<http://werkzeug.pocoo.org/documentation/wrappers>`_.
+
+Request
+-------
+
+path:
+ As ``req.path_info``
+args:
+ As ``req.GET``
+form:
+ As ``req.POST``
+values:
+ As ``req.params``
+files:
+ In ``req.POST`` (as FieldStorage objects)
+data:
+ In ``req.body_file``
+
+Response
+--------
+
+response:
+ In ``res.body`` (settable as ``res.body`` or ``res.app_iter``)
+status:
+ In ``res.status_int``
+mimetype:
+ In ``res.content_type``
+
+Zope 3
+======
+
+From the Zope 3 interfaces for the `Request
+<http://apidoc.zope.org/++apidoc++/Interface/zope.publisher.interfaces.browser.IBrowserRequest/index.html>`_
+and `Response
+<http://apidoc.zope.org/++apidoc++/Interface/zope.publisher.interfaces.http.IHTTPResponse/index.html>`_.
+
+Request
+-------
+
+``locale``, ``setupLocale()``:
+ This is not fully calculated, but information is available in
+ ``req.accept_languages``.
+
+``principal``, ``setPrincipal(principal)``:
+ ``req.remote_user`` gives the username, but there is no standard
+ place for a user *object*.
+
+``publication``, ``setPublication()``,
+ These are associated with the object publishing system in Zope.
+ This kind of publishing system is outside the scope of WebOb.
+
+``traverse(object)``, ``getTraversalStack()``, ``setTraversalStack()``:
+ These all relate to traversal, which is part of the publishing
+ system.
+
+``processInputs()``, ``setPathSuffix(steps)``:
+ Also associated with traversal and preparing the request.
+
+``environment``:
+ In ``req.environ``
+
+``bodyStream``:
+ In ``req.body_file``
+
+``interaction``:
+ This is the security context for the request; all the possible
+ participants or principals in the request. There's no
+ equivalent.
+
+``annotations``:
+ Extra information associated with the request. This would
+ generally go in custom keys of ``req.environ``, or if you set
+ attributes those attributes are stored in
+ ``req.environ['webob.adhoc_attrs']``.
+
+``debug``:
+ There is no standard debug flag for WebOb.
+
+``__getitem__(key)``, ``get(key)``, etc:
+ These treat the request like a dictionary, which WebOb does not
+ do. They seem to take values from the environment, not
+ parameters. Also on the Zope request object is ``items()``,
+ ``__contains__(key)``, ``__iter__()``, ``keys()``, ``__len__()``,
+ ``values()``.
+
+``getPositionalArguments()``:
+ I'm not sure what the equivalent would be, as there are no
+ positional arguments during instantiation (it doesn't fit into
+ WSGI). Maybe ``wsgiorg.urlvars``?
+
+``retry()``, ``supportsRetry()``:
+ Creates a new request that can be used to retry a request.
+ Similar to ``req.copy()``.
+
+``close()``, ``hold(obj)``:
+ This closes resources associated with the request, including any
+ "held" objects. There's nothing similar.
+
+Response
+--------
+
+``authUser``:
+ Not sure what this is or does.
+
+``reset()``:
+ No direct equivalent; you'd have to do ``res.headers = [];
+ res.body = ''; res.status = 200``
+
+``setCookie(name, value, **kw)``:
+ Is ``res.set_cookie(...)``.
+
+``getCookie(name)``:
+ No equivalent. Hm.
+
+``expireCookie(name)``:
+ Is ``res.delete_cookie(name)``.
+
+``appendToCookie(name, value)``:
+ This appends the value to any existing cookie (separating values
+ with a colon). WebOb does not do this.
+
+``setStatus(status)``:
+ Availble by setting ``res.status`` (can be set to an integer or a
+ string of "code reason").
+
+``getHeader(name, default=None)``:
+ Is ``res.headers.get(name)``.
+
+``getStatus()``:
+ Is ``res.status_int`` (or ``res.status`` to include reason)
+
+``addHeader(name, value)``:
+ Is ``res.headers.add(name, value)`` (in Zope and WebOb, this does
+ not clobber any previous value).
+
+``getHeaders()``:
+ Is ``res.headerlist``.
+
+``setHeader(name, value)``:
+ Is ``res.headers[name] = value``.
+
+``getStatusString()``:
+ Is ``res.status``.
+
+``consumeBody()``:
+ This consumes any non-string body to turn the body into a single
+ string. Any access to ``res.body`` will do this (e.g., when you
+ have set the ``res.app_iter``).
+
+``internalError()``:
+ This is available with ``webob.exc.HTTP*()``.
+
+``handleException(exc_info)``:
+ This is provided with a tool like ``paste.exceptions``.
+
+``consumeBodyIter()``:
+ This returns the iterable for the body, even if the body was a
+ string. Anytime you access ``res.app_iter`` you will get an
+ iterable. ``res.body`` and ``res.app_iter`` can be interchanged
+ and accessed as many times as you want, unlike the Zope
+ equivalents.
+
+``setResult(result)``:
+ You can achieve the same thing through ``res.body = result``, or
+ ``res.app_iter = result``. ``res.body`` accepts None, a unicode
+ string (*if* you have set a charset) or a normal string.
+ ``res.app_iter`` only accepts None and an interable. You can't
+ update all of a response with one call.
+
+ Like in Zope, WebOb updates Content-Length. Unlike Zope, it does
+ not automatically calculate a charset.
+
+
+mod_python
+==========
+
+Some key attributes from the `mod_python
+<http://modpython.org/live/current/doc-html/pyapi-mprequest-mem.html>`_
+request object.
+
+Request
+-------
+
+``req.uri``:
+ In ``req.path``.
+
+``req.user``:
+ In ``req.remote_user``.
+
+``req.get_remote_host()``:
+ In ``req.environ['REMOTE_ADDR']`` or ``req.remote_addr``.
+
+``req.headers_in.get('referer')``:
+ In ``req.headers.get('referer')`` or ``req.referer`` (same pattern
+ for other request headers, presumably).
+
+Response
+--------
+
+``util.redirect`` or ``req.status = apache.HTTP_MOVED_TEMPORARILY``:
+
+.. code-block:: python
+
+ from webob.exc import HTTPTemporaryRedirect
+ exc = HTTPTemporaryRedirect(location=url)
+ return exc(environ, start_response)
+
+``req.content_type = "application/x-csv"`` and
+``req.headers_out.add('Content-Disposition', 'attachment;filename=somefile.csv')``:
+
+.. code-block:: python
+
+ res = req.ResponseClass()
+ res.content_type = 'application/x-csv'
+ res.headers.add('Content-Disposition', 'attachment;filename=somefile.csv')
+ return res(environ, start_response)
+
+webapp Response
+===============
+
+The Google App Engine `webapp
+<http://code.google.com/appengine/docs/python/tools/webapp/>`_
+framework uses the WebOb Request object, but does not use its Response
+object.
+
+The constructor for ``webapp.Response`` does not take any arguments.
+The response is created by the framework, so you don't use it like
+``return Response(...)``, instead you use ``self.response``. Also the
+response object automatically has ``Cache-Control: no-cache`` set,
+while the WebOb response does not set any cache headers.
+
+``resp.set_status(code, message=None)``:
+ This is handled by setting the ``resp.status`` attribute.
+
+``resp.clear()``:
+ You'd do ``resp.body = ""``
+
+``resp.wsgi_write(start_response)``:
+ This writes the response using the ``start_response`` callback,
+ and using the ``start_response`` writer. The WebOb response
+ object is called as a WSGI app (``resp(environ, start_response)``)
+ to do the equivalent.
+
+``resp.out.write(text)``:
+ This writes to an internal ``StringIO`` instance of the response.
+ This uses the ability of the standard StringIO object to hold
+ either unicode or ``str`` text, and so long as you are always
+ consistent it will encode your content (but it does not respect
+ your preferred encoding, it always uses UTF-8). The WebOb method
+ ``resp.write(text)`` is basically equivalent, and also accepts
+ unicode (using ``resp.charset`` for the encoding). You can also
+ write to ``resp.body_file``, but it does not allow unicode.
+
+Besides exposing a ``.headers`` attribute (based on
+`wsgiref.headers.Headers
+<http://docs.python.org/library/wsgiref.html#wsgiref.headers.Headers>`_)
+there is no other API for the webapp response object. This means the
+response lacks:
+
+* A usefully readable body or status.
+* A useful constructor that makes it easy to treat responses like
+ objects.
+* Providing a non-string ``app_iter`` for the body (like a generator).
+* Parsing of the Content-Type charset.
+* Getter/setters for parsed forms of headers, specifically
+ cache_control and last_modified.
+* The ``cache_expires`` method
+* ``set_cookie``, ``delete_cookie``, and ``unset_cookie``. Instead
+ you have to simply manually set the Set-Cookie header.
+* ``encode_content`` and ``decode_content`` for handling gzip encoding.
+* ``md5_etag()`` for generating an etag from the body.
+* Conditional responses that will return 304 based on the response and
+ request headers.
+* The ability to serve Range request automatically.
+
+PHP
+===
+
+PHP does not have anything really resembling a request and response
+object. Instead these are encoded in a set of global objects for the
+request and functions for the response.
+
+``$_POST``, ``$_GET``, ``$_FILES``
+----------------------------------
+
+These represent ``req.POST`` and ``req.GET``.
+
+PHP uses the variable names to tell whether a variable can hold
+multiple values. For instance ``$_POST['name[]']``, which will be an
+array. In WebOb any variable can have multiple values, and you can
+get these through ``req.POST.getall('name')``.
+
+The files in ``$_FILES`` are simply in ``req.POST`` in WebOb, as
+FieldStorage instances.
+
+``$_COOKIES``
+-------------
+
+This is in ``req.cookies``.
+
+``$_SERVER``, ``$_REQUEST``, ``$_ENV``
+--------------------------------------
+
+These are all in ``req.environ``. These are not split up like they
+are in PHP, it's all just one dictionary. Everything that would
+typically be in ``$_ENV`` is technically optional, and outside of a
+couple CGI-standard keys in ``$_SERVER`` most of those are also
+optional, but it is common for WSGI servers to populate the request
+with similar information as PHP.
+
+``$HTTP_RAW_POST_DATA``
+-----------------------
+
+This contains the unparsed data in the request body. This is in
+``req.body``.
+
+The response
+------------
+
+Response headers in PHP are sent with ``header("Header-Name:
+value")``. In WebOb there is a dictionary in ``resp.headers`` that
+can have values set; the headers aren't actually sent until you send
+the response. You can add headers without overwriting (the equivalent
+of ``header("...", false)``) with ``resp.headers.add('Header-Name',
+'value')``.
+
+The status in PHP is sent with ``http_send_status(code)``. In WebOb
+this is ``resp.status = code``.
+
+The body in PHP is sent implicitly through the rendering of the PHP
+body (or with ``echo`` or any other functions that send output).
+
diff --git a/lib/webob_1_1_1/docs/do-it-yourself.txt b/lib/webob_1_1_1/docs/do-it-yourself.txt
new file mode 100644
index 0000000..564fc1c
--- /dev/null
+++ b/lib/webob_1_1_1/docs/do-it-yourself.txt
@@ -0,0 +1,580 @@
+Another Do-It-Yourself Framework
+================================
+
+.. contents::
+
+Introduction and Audience
+-------------------------
+
+It's been over two years since I wrote the `first version of this tutorial <http://pythonpaste.org/do-it-yourself-framework.html>`_. I decided to give it another run with some of the tools that have come about since then (particularly `WebOb <http://webob.org/>`_).
+
+Sometimes Python is accused of having too many web frameworks. And it's true, there are a lot. That said, I think writing a framework is a useful exercise. It doesn't let you skip over too much without understanding it. It removes the magic. So even if you go on to use another existing framework (which I'd probably advise you do), you'll be able to understand it better if you've written something like it on your own.
+
+This tutorial shows you how to create a web framework of your own, using WSGI and WebOb. No other libraries will be used.
+
+For the longer sections I will try to explain any tricky parts on a line-by line basis following the example.
+
+What Is WSGI?
+-------------
+
+At its simplest WSGI is an interface between web servers and web applications. We'll explain the mechanics of WSGI below, but a higher level view is to say that WSGI lets code pass around web requests in a fairly formal way. That's the simplest summary, but there is more -- WSGI lets you add annotation to the request, and adds some more metadata to the request.
+
+WSGI more specifically is made up of an *application* and a *server*. The application is a function that receives the request and produces the response. The server is the thing that calls the application function.
+
+A very simple application looks like this:
+
+.. code-block:: python
+
+ >>> def application(environ, start_response):
+ ... start_response('200 OK', [('Content-Type', 'text/html')])
+ ... return ['Hello World!']
+
+The ``environ`` argument is a dictionary with values like the environment in a CGI request. The header ``Host:``, for instance, goes in ``environ['HTTP_HOST']``. The path is in ``environ['SCRIPT_NAME']`` (which is the path leading *up to* the application), and ``environ['PATH_INFO']`` (the remaining path that the application should interpret).
+
+We won't focus much on the server, but we will use WebOb to handle the application. WebOb in a way has a simple server interface. To use it you create a new request with ``req = webob.Request.blank('http://localhost/test')``, and then call the application with ``resp = req.get_response(app)``. For example:
+
+.. code-block:: python
+
+ >>> from webob import Request
+ >>> req = Request.blank('http://localhost/test')
+ >>> resp = req.get_response(application)
+ >>> print resp
+ 200 OK
+ Content-Type: text/html
+ <BLANKLINE>
+ Hello World!
+
+This is an easy way to test applications, and we'll use it to test the framework we're creating.
+
+About WebOb
+-----------
+
+WebOb is a library to create a request and response object. It's centered around the WSGI model. Requests are wrappers around the environment. For example:
+
+.. code-block:: python
+
+ >>> req = Request.blank('http://localhost/test')
+ >>> req.environ['HTTP_HOST']
+ 'localhost:80'
+ >>> req.host
+ 'localhost:80'
+ >>> req.path_info
+ '/test'
+
+Responses are objects that represent the... well, response. The status, headers, and body:
+
+.. code-block:: python
+
+ >>> from webob import Response
+ >>> resp = Response(body='Hello World!')
+ >>> resp.content_type
+ 'text/html'
+ >>> resp.content_type = 'text/plain'
+ >>> print resp
+ 200 OK
+ Content-Length: 12
+ Content-Type: text/plain; charset=UTF-8
+ <BLANKLINE>
+ Hello World!
+
+Responses also happen to be WSGI applications. That means you can call ``resp(environ, start_response)``. Of course it's much less *dynamic* than a normal WSGI application.
+
+These two pieces solve a lot of the more tedious parts of making a framework. They deal with parsing most HTTP headers, generating valid responses, and a number of unicode issues.
+
+Serving Your Application
+------------------------
+
+While we can test the application using WebOb, you might want to serve the application. Here's the basic recipe, using the `Paste <http://pythonpaste.org>`_ HTTP server:
+
+.. code-block:: python
+
+ if __name__ == '__main__':
+ from paste import httpserver
+ httpserver.serve(app, host='127.0.0.1', port=8080)
+
+you could also use `wsgiref <http://python.org/doc/current/lib/module-wsgiref.simpleserver.html>`_ from the standard library, but this is mostly appropriate for testing as it is single-threaded:
+
+.. code-block:: python
+
+ if __name__ == '__main__':
+ from wsgiref.simple_server import make_server
+ server = make_server('127.0.0.1', 8080, app)
+ server.serve_forever()
+
+Making A Framework
+------------------
+
+Well, now we need to start work on our framework.
+
+Here's the basic model we'll be creating:
+
+* We'll define routes that point to controllers
+
+* We'll create a simple framework for creating controllers
+
+Routing
+-------
+
+We'll use explicit routes using URI templates (minus the domains) to match paths. We'll add a little extension that you can use ``{name:regular expression}``, where the named segment must then match that regular expression. The matches will include a "controller" variable, which will be a string like "module_name:function_name". For our examples we'll use a simple blog.
+
+So here's what a route would look like:
+
+.. code-block:: python
+
+ app = Router()
+ app.add_route('/', controller='controllers:index')
+ app.add_route('/{year:\d\d\d\d}/',
+ controller='controllers:archive')
+ app.add_route('/{year:\d\d\d\d}/{month:\d\d}/',
+ controller='controllers:archive')
+ app.add_route('/{year:\d\d\d\d}/{month:\d\d}/{slug}',
+ controller='controllers:view')
+ app.add_route('/post', controller='controllers:post')
+
+To do this we'll need a couple pieces:
+
+* Something to match those URI template things.
+* Something to load the controller
+* The object to patch them together (``Router``)
+
+Routing: Templates
+~~~~~~~~~~~~~~~~~~
+
+To do the matching, we'll compile those templates to regular expressions.
+
+.. code-block:: python
+ :linenos:
+
+ >>> import re
+ >>> var_regex = re.compile(r'''
+ ... \{ # The exact character "{"
+ ... (\w+) # The variable name (restricted to a-z, 0-9, _)
+ ... (?::([^}]+))? # The optional :regex part
+ ... \} # The exact character "}"
+ ... ''', re.VERBOSE)
+ >>> def template_to_regex(template):
+ ... regex = ''
+ ... last_pos = 0
+ ... for match in var_regex.finditer(template):
+ ... regex += re.escape(template[last_pos:match.start()])
+ ... var_name = match.group(1)
+ ... expr = match.group(2) or '[^/]+'
+ ... expr = '(?P<%s>%s)' % (var_name, expr)
+ ... regex += expr
+ ... last_pos = match.end()
+ ... regex += re.escape(template[last_pos:])
+ ... regex = '^%s$' % regex
+ ... return regex
+
+**line 2:** Here we create the regular expression. The ``re.VERBOSE`` flag makes the regular expression parser ignore whitespace and allow comments, so we can avoid some of the feel of line-noise. This matches any variables, i.e., ``{var:regex}`` (where ``:regex`` is optional). Note that there are two groups we capture: ``match.group(1)`` will be the variable name, and ``match.group(2)`` will be the regular expression (or None when there is no regular expression). Note that ``(?:...)?`` means that the section is optional.
+
+**line 10**: This variable will hold the regular expression that we are creating.
+
+**line 11**: This contains the position of the end of the last match.
+
+**line 12**: The ``finditer`` method yields all the matches.
+
+**line 13**: We're getting all the non-``{}`` text from after the last match, up to the beginning of this match. We call ``re.escape`` on that text, which escapes any characters that have special meaning. So ``.html`` will be escaped as ``\.html``.
+
+**line 14**: The first match is the variable name.
+
+**line 15**: ``expr`` is the regular expression we'll match against, the optional second match. The default is ``[^/]+``, which matches any non-empty, non-/ string. Which seems like a reasonable default to me.
+
+**line 16**: Here we create the actual regular expression. ``(?P<name>...)`` is a grouped expression that is named. When you get a match, you can look at ``match.groupdict()`` and get the names and values.
+
+**line 17, 18**: We add the expression on to the complete regular expression and save the last position.
+
+**line 19**: We add remaining non-variable text to the regular expression.
+
+**line 20**: And then we make the regular expression match the complete string (``^`` to force it to match from the start, ``$`` to make sure it matches up to the end).
+
+To test it we can try some translations. You could put these directly in the docstring of the ``template_to_regex`` function and use `doctest <http://python.org/doc/current/lib/module-doctest.html>`_ to test that. But I'm using doctest to test *this* document, so I can't put a docstring doctest inside the doctest itself. Anyway, here's what a test looks like:
+
+.. code-block:: python
+
+ >>> print template_to_regex('/a/static/path')
+ ^\/a\/static\/path$
+ >>> print template_to_regex('/{year:\d\d\d\d}/{month:\d\d}/{slug}')
+ ^\/(?P<year>\d\d\d\d)\/(?P<month>\d\d)\/(?P<slug>[^/]+)$
+
+Routing: controller loading
+~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+To load controllers we have to import the module, then get the function out of it. We'll use the ``__import__`` builtin to import the module. The return value of ``__import__`` isn't very useful, but it puts the module into ``sys.modules``, a dictionary of all the loaded modules.
+
+Also, some people don't know how exactly the string method ``split`` works. It takes two arguments -- the first is the character to split on, and the second is the maximum number of splits to do. We want to split on just the first ``:`` character, so we'll use a maximum number of splits of 1.
+
+.. code-block:: python
+
+ >>> import sys
+ >>> def load_controller(string):
+ ... module_name, func_name = string.split(':', 1)
+ ... __import__(module_name)
+ ... module = sys.modules[module_name]
+ ... func = getattr(module, func_name)
+ ... return func
+
+Routing: putting it together
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Now, the ``Router`` class. The class has the ``add_route`` method, and also a ``__call__`` method. That ``__call__`` method makes the Router object itself a WSGI application. So when a request comes in, it looks at ``PATH_INFO`` (also known as ``req.path_info``) and hands off the request to the controller that matches that path.
+
+.. code-block:: python
+ :linenos:
+
+ >>> from webob import Request
+ >>> from webob import exc
+ >>> class Router(object):
+ ... def __init__(self):
+ ... self.routes = []
+ ...
+ ... def add_route(self, template, controller, **vars):
+ ... if isinstance(controller, basestring):
+ ... controller = load_controller(controller)
+ ... self.routes.append((re.compile(template_to_regex(template)),
+ ... controller,
+ ... vars))
+ ...
+ ... def __call__(self, environ, start_response):
+ ... req = Request(environ)
+ ... for regex, controller, vars in self.routes:
+ ... match = regex.match(req.path_info)
+ ... if match:
+ ... req.urlvars = match.groupdict()
+ ... req.urlvars.update(vars)
+ ... return controller(environ, start_response)
+ ... return exc.HTTPNotFound()(environ, start_response)
+
+**line 5**: We are going to keep the route options in an ordered list. Each item will be ``(regex, controller, vars)``: ``regex`` is the regular expression object to match against, ``controller`` is the controller to run, and ``vars`` are any extra (constant) variables.
+
+**line 8, 9**: We will allow you to call ``add_route`` with a string (that will be imported) or a controller object. We test for a string here, and then import it if necessary.
+
+**line 13**: Here we add a ``__call__`` method. This is the method used when you call an object like a function. You should recognize this as the WSGI signature.
+
+**line 14**: We create a request object. Note we'll only use this request object in this function; if the controller wants a request object it'll have to make on of its own.
+
+**line 16**: We test the regular expression against ``req.path_info``. This is the same as ``environ['PATH_INFO']``. That's all the request path left to be processed.
+
+**line 18**: We set ``req.urlvars`` to the dictionary of matches in the regular expression. This variable actually maps to ``environ['wsgiorg.routing_args']``. Any attributes you set on a request will, in one way or another, map to the environment dictionary: the request holds no state of its own.
+
+**line 19**: We also add in any explicit variables passed in through ``add_route()``.
+
+**line 20**: Then we call the controller as a WSGI application itself. Any fancy framework stuff the controller wants to do, it'll have to do itself.
+
+**line 21**: If nothing matches, we return a 404 Not Found response. ``webob.exc.HTTPNotFound()`` is a WSGI application that returns 404 responses. You could add a message too, like ``webob.exc.HTTPNotFound('No route matched')``. Then, of course, we call the application.
+
+Controllers
+-----------
+
+The router just passes the request on to the controller, so the controllers are themselves just WSGI applications. But we'll want to set up something to make those applications friendlier to write.
+
+To do that we'll write a `decorator <http://www.ddj.com/web-development/184406073>`_. A decorator is a function that wraps another function. After decoration the function will be a WSGI application, but it will be decorating a function with a signature like ``controller_func(req, **urlvars)``. The controller function will return a response object (which, remember, is a WSGI application on its own).
+
+.. code-block:: python
+ :linenos:
+
+ >>> from webob import Request, Response
+ >>> from webob import exc
+ >>> def controller(func):
+ ... def replacement(environ, start_response):
+ ... req = Request(environ)
+ ... try:
+ ... resp = func(req, **req.urlvars)
+ ... except exc.HTTPException, e:
+ ... resp = e
+ ... if isinstance(resp, basestring):
+ ... resp = Response(body=resp)
+ ... return resp(environ, start_response)
+ ... return replacement
+
+**line 3**: This is the typical signature for a decorator -- it takes one function as an argument, and returns a wrapped function.
+
+**line 4**: This is the replacement function we'll return. This is called a `closure <http://en.wikipedia.org/wiki/Closure_(computer_science)>`_ -- this function will have access to ``func``, and everytime you decorate a new function there will be a new ``replacement`` function with its own value of ``func``. As you can see, this is a WSGI application.
+
+**line 5**: We create a request.
+
+**line 6**: Here we catch any ``webob.exc.HTTPException`` exceptions. This is so you can do ``raise webob.exc.HTTPNotFound()`` in your function. These exceptions are themselves WSGI applications.
+
+**line 7**: We call the function with the request object, any any variables in ``req.urlvars``. And we get back a response.
+
+**line 10**: We'll allow the function to return a full response object, or just a string. If they return a string, we'll create a ``Response`` object with that (and with the standard ``200 OK`` status, ``text/html`` content type, and ``utf8`` charset/encoding).
+
+**line 12**: We pass the request on to the response. Which *also* happens to be a WSGI application. WSGI applications are falling from the sky!
+
+**line 13**: We return the function object itself, which will take the place of the function.
+
+You use this controller like:
+
+.. code-block:: python
+
+ >>> @controller
+ ... def index(req):
+ ... return 'This is the index'
+
+Putting It Together
+-------------------
+
+Now we'll show a basic application. Just a hello world application for now. Note that this document is the module ``__main__``.
+
+.. code-block:: python
+
+ >>> @controller
+ ... def hello(req):
+ ... if req.method == 'POST':
+ ... return 'Hello %s!' % req.params['name']
+ ... elif req.method == 'GET':
+ ... return '''<form method="POST">
+ ... You're name: <input type="text" name="name">
+ ... <input type="submit">
+ ... </form>'''
+ >>> hello_world = Router()
+ >>> hello_world.add_route('/', controller=hello)
+
+Now let's test that application:
+
+.. code-block:: python
+
+ >>> req = Request.blank('/')
+ >>> resp = req.get_response(hello_world)
+ >>> print resp
+ 200 OK
+ Content-Type: text/html; charset=UTF-8
+ Content-Length: 131
+ <BLANKLINE>
+ <form method="POST">
+ You're name: <input type="text" name="name">
+ <input type="submit">
+ </form>
+ >>> req.method = 'POST'
+ >>> req.body = 'name=Ian'
+ >>> resp = req.get_response(hello_world)
+ >>> print resp
+ 200 OK
+ Content-Type: text/html; charset=UTF-8
+ Content-Length: 10
+ <BLANKLINE>
+ Hello Ian!
+
+
+Another Controller
+------------------
+
+There's another pattern that might be interesting to try for a controller. Instead of a function, we can make a class with methods like ``get``, ``post``, etc. The ``urlvars`` will be used to instantiate the class.
+
+We could do this as a superclass, but the implementation will be more elegant as a wrapper, like the decorator is a wrapper. Python 3.0 will add `class decorators <http://www.python.org/dev/peps/pep-3129/>`_ which will work like this.
+
+We'll allow an extra ``action`` variable, which will define the method (actually ``action_method``, where ``_method`` is the request method). If no action is given, we'll use just the method (i.e., ``get``, ``post``, etc).
+
+.. code-block:: python
+ :linenos:
+
+ >>> def rest_controller(cls):
+ ... def replacement(environ, start_response):
+ ... req = Request(environ)
+ ... try:
+ ... instance = cls(req, **req.urlvars)
+ ... action = req.urlvars.get('action')
+ ... if action:
+ ... action += '_' + req.method.lower()
+ ... else:
+ ... action = req.method.lower()
+ ... try:
+ ... method = getattr(instance, action)
+ ... except AttributeError:
+ ... raise exc.HTTPNotFound("No action %s" % action)
+ ... resp = method()
+ ... if isinstance(resp, basestring):
+ ... resp = Response(body=resp)
+ ... except exc.HTTPException, e:
+ ... resp = e
+ ... return resp(environ, start_response)
+ ... return replacement
+
+**line 1**: Here we're kind of decorating a class. But really we'll just create a WSGI application wrapper.
+
+**line 2-4**: The replacement WSGI application, also a closure. And we create a request and catch exceptions, just like in the decorator.
+
+**line 5**: We instantiate the class with both the request and ``req.urlvars`` to initialize it. The instance will only be used for one request. (Note that the *instance* then doesn't have to be thread safe.)
+
+**line 6**: We get the action variable out, if there is one.
+
+**line 7, 8**: If there was one, we'll use the method name ``{action}_{method}``...
+
+**line 8, 9**: ... otherwise we'll use just the method for the method name.
+
+**line 10-13**: We'll get the method from the instance, or respond with a 404 error if there is not such method.
+
+**line 14**: Call the method, get the response
+
+**line 15, 16**: If the response is just a string, create a full response object from it.
+
+**line 19**: and then we forward the request...
+
+**line 20**: ... and return the wrapper object we've created.
+
+Here's the hello world:
+
+.. code-block:: python
+
+ >>> class Hello(object):
+ ... def __init__(self, req):
+ ... self.request = req
+ ... def get(self):
+ ... return '''<form method="POST">
+ ... You're name: <input type="text" name="name">
+ ... <input type="submit">
+ ... </form>'''
+ ... def post(self):
+ ... return 'Hello %s!' % self.request.params['name']
+ >>> hello = rest_controller(Hello)
+
+We'll run the same test as before:
+
+.. code-block:: python
+
+ >>> hello_world = Router()
+ >>> hello_world.add_route('/', controller=hello)
+ >>> req = Request.blank('/')
+ >>> resp = req.get_response(hello_world)
+ >>> print resp
+ 200 OK
+ Content-Type: text/html; charset=UTF-8
+ Content-Length: 131
+ <BLANKLINE>
+ <form method="POST">
+ You're name: <input type="text" name="name">
+ <input type="submit">
+ </form>
+ >>> req.method = 'POST'
+ >>> req.body = 'name=Ian'
+ >>> resp = req.get_response(hello_world)
+ >>> print resp
+ 200 OK
+ Content-Type: text/html; charset=UTF-8
+ Content-Length: 10
+ <BLANKLINE>
+ Hello Ian!
+
+URL Generation and Request Access
+---------------------------------
+
+You can use hard-coded links in your HTML, but this can have problems. Relative links are hard to manage, and absolute links presume that your application lives at a particular location. WSGI gives a variable ``SCRIPT_NAME``, which is the portion of the path that led up to this application. If you are writing a blog application, for instance, someone might want to install it at ``/blog/``, and then SCRIPT_NAME would be ``"/blog"``. We should generate links with that in mind.
+
+The base URL using SCRIPT_NAME is ``req.application_url``. So, if we have access to the request we can make a URL. But what if we don't have access?
+
+We can use thread-local variables to make it easy for any function to get access to the currect request. A "thread-local" variable is a variable whose value is tracked separately for each thread, so if there are multiple requests in different threads, their requests won't clobber each other.
+
+The basic means of using a thread-local variable is ``threading.local()``. This creates a blank object that can have thread-local attributes assigned to it. I find the best way to get *at* a thread-local value is with a function, as this makes it clear that you are fetching the object, as opposed to getting at some global object.
+
+Here's the basic structure for the local:
+
+.. code-block:: python
+
+ >>> import threading
+ >>> class Localized(object):
+ ... def __init__(self):
+ ... self.local = threading.local()
+ ... def register(self, object):
+ ... self.local.object = object
+ ... def unregister(self):
+ ... del self.local.object
+ ... def __call__(self):
+ ... try:
+ ... return self.local.object
+ ... except AttributeError:
+ ... raise TypeError("No object has been registered for this thread")
+ >>> get_request = Localized()
+
+Now we need some *middleware* to register the request object. Middleware is something that wraps an application, possibly modifying the request on the way in or the way out. In a sense the ``Router`` object was middleware, though not exactly because it didn't wrap a single application.
+
+This registration middleware looks like:
+
+.. code-block:: python
+
+ >>> class RegisterRequest(object):
+ ... def __init__(self, app):
+ ... self.app = app
+ ... def __call__(self, environ, start_response):
+ ... req = Request(environ)
+ ... get_request.register(req)
+ ... try:
+ ... return self.app(environ, start_response)
+ ... finally:
+ ... get_request.unregister()
+
+Now if we do:
+
+ >>> hello_world = RegisterRequest(hello_world)
+
+then the request will be registered each time. Now, lets create a URL generation function:
+
+.. code-block:: python
+
+ >>> import urllib
+ >>> def url(*segments, **vars):
+ ... base_url = get_request().application_url
+ ... path = '/'.join(str(s) for s in segments)
+ ... if not path.startswith('/'):
+ ... path = '/' + path
+ ... if vars:
+ ... path += '?' + urllib.urlencode(vars)
+ ... return base_url + path
+
+Now, to test:
+
+.. code-block:: python
+
+ >>> get_request.register(Request.blank('http://localhost/'))
+ >>> url('article', 1)
+ 'http://localhost/article/1'
+ >>> url('search', q='some query')
+ 'http://localhost/search?q=some+query'
+
+Templating
+----------
+
+Well, we don't *really* need to factor templating into our framework. After all, you return a string from your controller, and you can figure out on your own how to get a rendered string from a template.
+
+But we'll add a little helper, because I think it shows a clever trick.
+
+We'll use `Tempita <http://pythonpaste.org/tempita/>`_ for templating, mostly because it's very simplistic about how it does loading. The basic form is:
+
+.. code-block:: python
+
+ import tempita
+ template = tempita.HTMLTemplate.from_filename('some-file.html')
+
+But we'll be implementing a function ``render(template_name, **vars)`` that will render the named template, treating it as a path *relative to the location of the render() call*. That's the trick.
+
+To do that we use ``sys._getframe``, which is a way to look at information in the calling scope. Generally this is frowned upon, but I think this case is justifiable.
+
+We'll also let you pass an instantiated template in instead of a template name, which will be useful in places like a doctest where there aren't other files easily accessible.
+
+.. code-block:: python
+
+ >>> import os
+ >>> import tempita #doctest: +SKIP
+ >>> def render(template, **vars):
+ ... if isinstance(template, basestring):
+ ... caller_location = sys._getframe(1).f_globals['__file__']
+ ... filename = os.path.join(os.path.dirname(caller_location), template)
+ ... template = tempita.HTMLTemplate.from_filename(filename)
+ ... vars.setdefault('request', get_request())
+ ... return template.substitute(vars)
+
+Conclusion
+----------
+
+Well, that's a framework. Ta-da!
+
+Of course, this doesn't deal with some other stuff. In particular:
+
+* Configuration
+* Making your routes debuggable
+* Exception catching and other basic infrastructure
+* Database connections
+* Form handling
+* Authentication
+
+But, for now, that's outside the scope of this document.
+
diff --git a/lib/webob_1_1_1/docs/doctests.py b/lib/webob_1_1_1/docs/doctests.py
new file mode 100644
index 0000000..9aecb31
--- /dev/null
+++ b/lib/webob_1_1_1/docs/doctests.py
@@ -0,0 +1,17 @@
+import unittest
+import doctest
+
+def test_suite():
+ flags = doctest.ELLIPSIS|doctest.NORMALIZE_WHITESPACE
+ return unittest.TestSuite((
+ doctest.DocFileSuite('test_request.txt', optionflags=flags),
+ doctest.DocFileSuite('test_response.txt', optionflags=flags),
+ doctest.DocFileSuite('test_dec.txt', optionflags=flags),
+ doctest.DocFileSuite('do-it-yourself.txt', optionflags=flags),
+ doctest.DocFileSuite('file-example.txt', optionflags=flags),
+ doctest.DocFileSuite('index.txt', optionflags=flags),
+ doctest.DocFileSuite('reference.txt', optionflags=flags),
+ ))
+
+if __name__ == '__main__':
+ unittest.main(defaultTest='test_suite')
diff --git a/lib/webob_1_1_1/docs/file-example.txt b/lib/webob_1_1_1/docs/file-example.txt
new file mode 100644
index 0000000..16d4d19
--- /dev/null
+++ b/lib/webob_1_1_1/docs/file-example.txt
@@ -0,0 +1,214 @@
+WebOb File-Serving Example
+==========================
+
+This document shows how you can make a static-file-serving application
+using WebOb. We'll quickly build this up from minimal functionality
+to a high-quality file serving application.
+
+.. comment:
+
+ >>> import webob, os
+ >>> base_dir = os.path.dirname(os.path.dirname(webob.__file__))
+ >>> doc_dir = os.path.join(base_dir, 'docs')
+ >>> from doctest import ELLIPSIS
+
+First we'll setup a really simple shim around our application, which
+we can use as we improve our application:
+
+.. code-block:: python
+
+ >>> from webob import Request, Response
+ >>> import os
+ >>> class FileApp(object):
+ ... def __init__(self, filename):
+ ... self.filename = filename
+ ... def __call__(self, environ, start_response):
+ ... res = make_response(self.filename)
+ ... return res(environ, start_response)
+ >>> import mimetypes
+ >>> def get_mimetype(filename):
+ ... type, encoding = mimetypes.guess_type(filename)
+ ... # We'll ignore encoding, even though we shouldn't really
+ ... return type or 'application/octet-stream'
+
+Now we can make different definitions of ``make_response``. The
+simplest version:
+
+.. code-block:: python
+
+ >>> def make_response(filename):
+ ... res = Response(content_type=get_mimetype(filename))
+ ... res.body = open(filename, 'rb').read()
+ ... return res
+
+Let's give it a go. We'll test it out with a file ``test-file.txt``
+in the WebOb doc directory:
+
+.. code-block:: python
+
+ >>> fn = os.path.join(doc_dir, 'test-file.txt')
+ >>> open(fn).read()
+ 'This is a test. Hello test people!'
+ >>> app = FileApp(fn)
+ >>> req = Request.blank('/')
+ >>> print req.get_response(app)
+ 200 OK
+ Content-Type: text/plain; charset=UTF-8
+ Content-Length: 35
+ <BLANKLINE>
+ This is a test. Hello test people!
+
+Well, that worked. But it's not a very fancy object. First, it reads
+everything into memory, and that's bad. We'll create an iterator instead:
+
+.. code-block:: python
+
+ >>> class FileIterable(object):
+ ... def __init__(self, filename):
+ ... self.filename = filename
+ ... def __iter__(self):
+ ... return FileIterator(self.filename)
+ >>> class FileIterator(object):
+ ... chunk_size = 4096
+ ... def __init__(self, filename):
+ ... self.filename = filename
+ ... self.fileobj = open(self.filename, 'rb')
+ ... def __iter__(self):
+ ... return self
+ ... def next(self):
+ ... chunk = self.fileobj.read(self.chunk_size)
+ ... if not chunk:
+ ... raise StopIteration
+ ... return chunk
+ >>> def make_response(filename):
+ ... res = Response(content_type=get_mimetype(filename))
+ ... res.app_iter = FileIterable(filename)
+ ... res.content_length = os.path.getsize(filename)
+ ... return res
+
+And testing:
+
+.. code-block:: python
+
+ >>> req = Request.blank('/')
+ >>> print req.get_response(app)
+ 200 OK
+ Content-Type: text/plain; charset=UTF-8
+ Content-Length: 35
+ <BLANKLINE>
+ This is a test. Hello test people!
+
+Well, that doesn't *look* different, but lets *imagine* that it's
+different because we know we changed some code. Now to add some basic
+metadata to the response:
+
+.. code-block:: python
+
+ >>> def make_response(filename):
+ ... res = Response(content_type=get_mimetype(filename),
+ ... conditional_response=True)
+ ... res.app_iter = FileIterable(filename)
+ ... res.content_length = os.path.getsize(filename)
+ ... res.last_modified = os.path.getmtime(filename)
+ ... res.etag = '%s-%s-%s' % (os.path.getmtime(filename),
+ ... os.path.getsize(filename), hash(filename))
+ ... return res
+
+Now, with ``conditional_response`` on, and with ``last_modified`` and
+``etag`` set, we can do conditional requests:
+
+.. code-block:: python
+
+ >>> req = Request.blank('/')
+ >>> res = req.get_response(app)
+ >>> print res
+ 200 OK
+ Content-Type: text/plain; charset=UTF-8
+ Content-Length: 35
+ Last-Modified: ... GMT
+ ETag: ...-...
+ <BLANKLINE>
+ This is a test. Hello test people!
+ >>> req2 = Request.blank('/')
+ >>> req2.if_none_match = res.etag
+ >>> req2.get_response(app)
+ <Response ... 304 Not Modified>
+ >>> req3 = Request.blank('/')
+ >>> req3.if_modified_since = res.last_modified
+ >>> req3.get_response(app)
+ <Response ... 304 Not Modified>
+
+We can even do Range requests, but it will currently involve iterating
+through the file unnecessarily. When there's a range request (and you
+set ``conditional_response=True``) the application will satisfy that
+request. But with an arbitrary iterator the only way to do that is to
+run through the beginning of the iterator until you get to the chunk
+that the client asked for. We can do better because we can use
+``fileobj.seek(pos)`` to move around the file much more efficiently.
+
+So we'll add an extra method, ``app_iter_range``, that ``Response``
+looks for:
+
+.. code-block:: python
+
+ >>> class FileIterable(object):
+ ... def __init__(self, filename, start=None, stop=None):
+ ... self.filename = filename
+ ... self.start = start
+ ... self.stop = stop
+ ... def __iter__(self):
+ ... return FileIterator(self.filename, self.start, self.stop)
+ ... def app_iter_range(self, start, stop):
+ ... return self.__class__(self.filename, start, stop)
+ >>> class FileIterator(object):
+ ... chunk_size = 4096
+ ... def __init__(self, filename, start, stop):
+ ... self.filename = filename
+ ... self.fileobj = open(self.filename, 'rb')
+ ... if start:
+ ... self.fileobj.seek(start)
+ ... if stop is not None:
+ ... self.length = stop - start
+ ... else:
+ ... self.length = None
+ ... def __iter__(self):
+ ... return self
+ ... def next(self):
+ ... if self.length is not None and self.length <= 0:
+ ... raise StopIteration
+ ... chunk = self.fileobj.read(self.chunk_size)
+ ... if not chunk:
+ ... raise StopIteration
+ ... if self.length is not None:
+ ... self.length -= len(chunk)
+ ... if self.length < 0:
+ ... # Chop off the extra:
+ ... chunk = chunk[:self.length]
+ ... return chunk
+
+Now we'll test it out:
+
+.. code-block:: python
+
+ >>> req = Request.blank('/')
+ >>> res = req.get_response(app)
+ >>> req2 = Request.blank('/')
+ >>> # Re-fetch the first 5 bytes:
+ >>> req2.range = (0, 5)
+ >>> res2 = req2.get_response(app)
+ >>> res2
+ <Response ... 206 Partial Content>
+ >>> # Let's check it's our custom class:
+ >>> res2.app_iter
+ <FileIterable object at ...>
+ >>> res2.body
+ 'This '
+ >>> # Now, conditional range support:
+ >>> req3 = Request.blank('/')
+ >>> req3.if_range = res.etag
+ >>> req3.range = (0, 5)
+ >>> req3.get_response(app)
+ <Response ... 206 Partial Content>
+ >>> req3.if_range = 'invalid-etag'
+ >>> req3.get_response(app)
+ <Response ... 200 OK>
diff --git a/lib/webob_1_1_1/docs/index.txt b/lib/webob_1_1_1/docs/index.txt
new file mode 100644
index 0000000..2e86c01
--- /dev/null
+++ b/lib/webob_1_1_1/docs/index.txt
@@ -0,0 +1,334 @@
+WebOb
++++++
+
+.. toctree::
+
+ reference
+ modules/webob
+ modules/dec
+ differences
+ file-example
+ wiki-example
+ comment-example
+ jsonrpc-example
+ do-it-yourself
+ news
+ license
+
+.. contents::
+
+.. comment:
+
+ >>> from doctest import ELLIPSIS
+
+
+Status & License
+================
+
+WebOb is an extraction and refinement of pieces from `Paste
+<http://pythonpaste.org/>`_. It is under active development.
+Discussion should happen on the `Paste mailing lists
+<http://pythonpaste.org/community/>`_, and bugs can go on the `issue tracker
+<https://bitbucket.org/ianb/webob/issues>`_. It was originally
+written by `Ian Bicking <http://ianbicking.org/>`_, and the primary
+maintainer is now `Sergey Schetinin <http://self.maluke.com/>`_.
+
+WebOb is released under an `MIT-style license <license.html>`_.
+
+WebOb is in a Mercurial repository at
+`http://bitbucket.org/ianb/webob/
+<http://bitbucket.org/ianb/webob/>`_, and installable via `easy_install
+webob==dev <http://bitbucket.org/ianb/webob/get/tip.gz>`__. You
+can check it out with::
+
+ $ hg clone http://bitbucket.org/ianb/webob
+
+Introduction
+============
+
+WebOb provides objects for HTTP requests and responses. Specifically
+it does this by wrapping the `WSGI <http://wsgi.org>`_ request
+environment and response status/headers/app_iter(body).
+
+The request and response objects provide many conveniences for parsing
+HTTP request and forming HTTP responses. Both objects are read/write:
+as a result, WebOb is also a nice way to create HTTP requests and
+parse HTTP responses; however, we won't cover that use case in this
+document. The `reference documentation <reference.html>`_ shows many
+examples of creating requests.
+
+Request
+=======
+
+The request object is a wrapper around the `WSGI environ dictionary
+<http://www.python.org/dev/peps/pep-0333/#environ-variables>`_. This
+dictionary contains keys for each header, keys that describe the
+request (including the path and query string), a file-like object for
+the request body, and a variety of custom keys. You can always access
+the environ with ``req.environ``.
+
+Some of the most important/interesting attributes of a request
+object:
+
+``req.method``:
+ The request method, e.g., ``'GET'``, ``'POST'``
+
+``req.GET``:
+ A `dictionary-like object`_ with all the variables in the query
+ string.
+
+``req.POST``:
+ A `dictionary-like object`_ with all the variables in the request
+ body. This only has variables if the request was a ``POST`` and
+ it is a form submission.
+
+``req.params``:
+ A `dictionary-like object`_ with a combination of everything in
+ ``req.GET`` and ``req.POST``.
+
+``req.body``:
+ The contents of the body of the request. This contains the entire
+ request body as a string. This is useful when the request is a
+ ``POST`` that is *not* a form submission, or a request like a
+ ``PUT``. You can also get ``req.body_file`` for a file-like
+ object.
+
+``req.cookies``:
+ A simple dictionary of all the cookies.
+
+``req.headers``:
+ A dictionary of all the headers. This is dictionary is case-insensitive.
+
+``req.urlvars`` and ``req.urlargs``:
+ ``req.urlvars`` is the keyword parameters associated with the
+ request URL. ``req.urlargs`` are the positional parameters.
+ These are set by products like `Routes
+ <http://routes.groovie.org/>`_ and `Selector
+ <http://lukearno.com/projects/selector/>`_.
+
+.. _`dictionary-like object`: #multidict
+
+Also, for standard HTTP request headers there are usually attributes,
+for instance: ``req.accept_language``, ``req.content_length``,
+``req.user_agent``, as an example. These properties expose the
+*parsed* form of each header, for whatever parsing makes sense. For
+instance, ``req.if_modified_since`` returns a `datetime
+<http://python.org/doc/current/lib/datetime-datetime.html>`_ object
+(or None if the header is was not provided). Details are in the
+`Request reference <class-webob.Request.html>`_.
+
+URLs
+----
+
+In addition to these attributes, there are several ways to get the URL
+of the request. I'll show various values for an example URL
+``http://localhost/app-root/doc?article_id=10``, where the application
+is mounted at ``http://localhost/app-root``.
+
+``req.url``:
+ The full request URL, with query string, e.g.,
+ ``'http://localhost/app-root/doc?article_id=10'``
+
+``req.application_url``:
+ The URL of the application (just the SCRIPT_NAME portion of the
+ path, not PATH_INFO). E.g., ``'http://localhost/app-root'``
+
+``req.host_url``:
+ The URL with the host, e.g., ``'http://localhost'``
+
+``req.relative_url(url, to_application=False)``:
+ Gives a URL, relative to the current URL. If ``to_application``
+ is True, then resolves it relative to ``req.application_url``.
+
+Methods
+-------
+
+There are `several methods <class-webob.Request.html#__init__>`_ but
+only a few you'll use often:
+
+``Request.blank(base_url)``:
+ Creates a new request with blank information, based at the given
+ URL. This can be useful for subrequests and artificial requests.
+ You can also use ``req.copy()`` to copy an existing request, or
+ for subrequests ``req.copy_get()`` which copies the request but
+ always turns it into a GET (which is safer to share for
+ subrequests).
+
+``req.get_response(wsgi_application)``:
+ This method calls the given WSGI application with this request,
+ and returns a `Response`_ object. You can also use this for
+ subrequests or testing.
+
+Unicode
+-------
+
+Many of the properties in the request object will return unicode
+values if the request encoding/charset is provided. The client *can*
+indicate the charset with something like ``Content-Type:
+application/x-www-form-urlencoded; charset=utf8``, but browsers seldom
+set this. You can set the charset with ``req.charset = 'utf8'``, or
+during instantiation with ``Request(environ, charset='utf8'). If you
+subclass ``Request`` you can also set ``charset`` as a class-level
+attribute.
+
+If it is set, then ``req.POST``, ``req.GET``, ``req.params``, and
+``req.cookies`` will contain unicode strings. Each has a
+corresponding ``req.str_*`` (like ``req.str_POST``) that is always
+``str`` and never unicode.
+
+Response
+========
+
+The response object looks a lot like the request object, though with
+some differences. The request object wraps a single ``environ``
+object; the response object has three fundamental parts (based on
+WSGI):
+
+``response.status``:
+ The response code plus message, like ``'200 OK'``. To set the
+ code without the reason, use ``response.status_int = 200``.
+
+``response.headerlist``:
+ A list of all the headers, like ``[('Content-Type',
+ 'text/html')]``. There's a case-insensitive `dictionary-like
+ object`_ in ``response.headers`` that also allows you to access
+ these same headers.
+
+``response.app_iter``:
+ An iterable (such as a list or generator) that will produce the
+ content of the response. This is also accessible as
+ ``response.body`` (a string), ``response.unicode_body`` (a
+ unicode object, informed by ``response.charset``), and
+ ``response.body_file`` (a file-like object; writing to it appends
+ to ``app_iter``).
+
+Everything else in the object derives from this underlying state.
+Here's the highlights:
+
+``response.content_type``:
+ The content type *not* including the ``charset`` parameter.
+ Typical use: ``response.content_type = 'text/html'``. You can
+ subclass ``Response`` and add a class-level attribute
+ ``default_content_type`` to set this automatically on
+ instantiation.
+
+``response.charset``:
+ The ``charset`` parameter of the content-type, it also informs
+ encoding in ``response.unicode_body``.
+ ``response.content_type_params`` is a dictionary of all the
+ parameters.
+
+``response.request``:
+ This optional attribute can point to the request object associated
+ with this response object.
+
+``response.set_cookie(key, value, max_age=None, path='/', domain=None, secure=None, httponly=False, version=None, comment=None)``:
+ Set a cookie. The keyword arguments control the various cookie
+ parameters. The ``max_age`` argument is the length for the cookie
+ to live in seconds (you may also use a timedelta object). The
+ `Expires`` key will also be set based on the value of ``max_age``.
+
+``response.delete_cookie(key, path='/', domain=None)``:
+ Delete a cookie from the client. This sets ``max_age`` to 0 and
+ the cookie value to ``''``.
+
+``response.cache_expires(seconds=0)``:
+ This makes this response cachable for the given number of seconds,
+ or if ``seconds`` is 0 then the response is uncacheable (this also
+ sets the ``Expires`` header).
+
+``response(environ, start_response)``: The response object is a WSGI
+ application. As an application, it acts according to how you
+ creat it. It *can* do conditional responses if you pass
+ ``conditional_response=True`` when instantiating (or set that
+ attribute later). It can also do HEAD and Range requests.
+
+Headers
+-------
+
+Like the request, most HTTP response headers are available as
+properties. These are parsed, so you can do things like
+``response.last_modified = os.path.getmtime(filename)``.
+
+The details are available in the `extracted Response documentation
+<class-webob.Response.html>`_.
+
+Instantiating the Response
+--------------------------
+
+Of course most of the time you just want to *make* a response.
+Generally any attribute of the response can be passed in as a keyword
+argument to the class; e.g.:
+
+.. code-block:: python
+
+ response = Response(body='hello world!', content_type='text/plain')
+
+The status defaults to ``'200 OK'``. The content_type does not
+default to anything, though if you subclass ``Response`` and set
+``default_content_type`` you can override this behavior.
+
+Exceptions
+==========
+
+To facilitate error responses like 404 Not Found, the module
+``webob.exc`` contains classes for each kind of error response. These
+include boring but appropriate error bodies.
+
+Each class is named ``webob.exc.HTTP*``, where ``*`` is the reason for
+the error. For instance, ``webob.exc.HTTPNotFound``. It subclasses
+``Response``, so you can manipulate the instances in the same way. A
+typical example is:
+
+.. code-block:: python
+
+ response = HTTPNotFound('There is no such resource')
+ # or:
+ response = HTTPMovedPermanently(location=new_url)
+
+You can use this like:
+
+.. code-block:: python
+
+ try:
+ ... stuff ...
+ raise HTTPNotFound('No such resource')
+ except HTTPException, e:
+ return e(environ, start_response)
+
+The exceptions are still WSGI applications, but you cannot set
+attributes like ``content_type``, ``charset``, etc. on these exception
+objects.
+
+Multidict
+=========
+
+Several parts of WebOb use a "multidict"; this is a dictionary where a
+key can have multiple values. The quintessential example is a query
+string like ``?pref=red&pref=blue``; the ``pref`` variable has two
+values: ``red`` and ``blue``.
+
+In a multidict, when you do ``request.GET['pref']`` you'll get back
+only ``'blue'`` (the last value of ``pref``). Sometimes returning a
+string, and sometimes returning a list, is the cause of frequent
+exceptions. If you want *all* the values back, use
+``request.GET.getall('pref')``. If you want to be sure there is *one
+and only one* value, use ``request.GET.getone('pref')``, which will
+raise an exception if there is zero or more than one value for
+``pref``.
+
+When you use operations like ``request.GET.items()`` you'll get back
+something like ``[('pref', 'red'), ('pref', 'blue')]``. All the
+key/value pairs will show up. Similarly ``request.GET.keys()``
+returns ``['pref', 'pref']``. Multidict is a view on a list of
+tuples; all the keys are ordered, and all the values are ordered.
+
+Example
+=======
+
+I haven't figured out the example I want to use here. The
+`file-serving example <file-example.html>`_ shows how to do more
+advanced HTTP techniques, while the `comment middleware example
+<comment-example.html>`_ shows middleware. For applications it's more
+reasonable to use WebOb in the context of a larger framework. `Pylons
+<http://pylonshq.com>`_ uses WebOb in 0.9.7+.
diff --git a/lib/webob_1_1_1/docs/jsonrpc-example-code/jsonrpc.py b/lib/webob_1_1_1/docs/jsonrpc-example-code/jsonrpc.py
new file mode 100644
index 0000000..ccd3519
--- /dev/null
+++ b/lib/webob_1_1_1/docs/jsonrpc-example-code/jsonrpc.py
@@ -0,0 +1,193 @@
+# A reaction to: http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/552751
+from webob import Request, Response
+from webob import exc
+from simplejson import loads, dumps
+import traceback
+import sys
+
+class JsonRpcApp(object):
+ """
+ Serve the given object via json-rpc (http://json-rpc.org/)
+ """
+
+ def __init__(self, obj):
+ self.obj = obj
+
+ def __call__(self, environ, start_response):
+ req = Request(environ)
+ try:
+ resp = self.process(req)
+ except ValueError, e:
+ resp = exc.HTTPBadRequest(str(e))
+ except exc.HTTPException, e:
+ resp = e
+ return resp(environ, start_response)
+
+ def process(self, req):
+ if not req.method == 'POST':
+ raise exc.HTTPMethodNotAllowed(
+ "Only POST allowed",
+ allowed='POST')
+ try:
+ json = loads(req.body)
+ except ValueError, e:
+ raise ValueError('Bad JSON: %s' % e)
+ try:
+ method = json['method']
+ params = json['params']
+ id = json['id']
+ except KeyError, e:
+ raise ValueError(
+ "JSON body missing parameter: %s" % e)
+ if method.startswith('_'):
+ raise exc.HTTPForbidden(
+ "Bad method name %s: must not start with _" % method)
+ if not isinstance(params, list):
+ raise ValueError(
+ "Bad params %r: must be a list" % params)
+ try:
+ method = getattr(self.obj, method)
+ except AttributeError:
+ raise ValueError(
+ "No such method %s" % method)
+ try:
+ result = method(*params)
+ except:
+ text = traceback.format_exc()
+ exc_value = sys.exc_info()[1]
+ error_value = dict(
+ name='JSONRPCError',
+ code=100,
+ message=str(exc_value),
+ error=text)
+ return Response(
+ status=500,
+ content_type='application/json',
+ body=dumps(dict(result=None,
+ error=error_value,
+ id=id)))
+ return Response(
+ content_type='application/json',
+ body=dumps(dict(result=result,
+ error=None,
+ id=id)))
+
+
+class ServerProxy(object):
+ """
+ JSON proxy to a remote service.
+ """
+
+ def __init__(self, url, proxy=None):
+ self._url = url
+ if proxy is None:
+ from wsgiproxy.exactproxy import proxy_exact_request
+ proxy = proxy_exact_request
+ self.proxy = proxy
+
+ def __getattr__(self, name):
+ if name.startswith('_'):
+ raise AttributeError(name)
+ return _Method(self, name)
+
+ def __repr__(self):
+ return '<%s for %s>' % (
+ self.__class__.__name__, self._url)
+
+class _Method(object):
+
+ def __init__(self, parent, name):
+ self.parent = parent
+ self.name = name
+
+ def __call__(self, *args):
+ json = dict(method=self.name,
+ id=None,
+ params=list(args))
+ req = Request.blank(self.parent._url)
+ req.method = 'POST'
+ req.content_type = 'application/json'
+ req.body = dumps(json)
+ resp = req.get_response(self.parent.proxy)
+ if resp.status_int != 200 and not (
+ resp.status_int == 500
+ and resp.content_type == 'application/json'):
+ raise ProxyError(
+ "Error from JSON-RPC client %s: %s"
+ % (self.parent._url, resp.status),
+ resp)
+ json = loads(resp.body)
+ if json.get('error') is not None:
+ e = Fault(
+ json['error'].get('message'),
+ json['error'].get('code'),
+ json['error'].get('error'),
+ resp)
+ raise e
+ return json['result']
+
+class ProxyError(Exception):
+ """
+ Raised when a request via ServerProxy breaks
+ """
+ def __init__(self, message, response):
+ Exception.__init__(self, message)
+ self.response = response
+
+class Fault(Exception):
+ """
+ Raised when there is a remote error
+ """
+ def __init__(self, message, code, error, response):
+ Exception.__init__(self, message)
+ self.code = code
+ self.error = error
+ self.response = response
+ def __str__(self):
+ return 'Method error calling %s: %s\n%s' % (
+ self.response.request.url,
+ self.args[0],
+ self.error)
+
+class DemoObject(object):
+ """
+ Something interesting to attach to
+ """
+ def add(self, *args):
+ return sum(args)
+ def average(self, *args):
+ return sum(args) / float(len(args))
+ def divide(self, a, b):
+ return a / b
+
+def make_app(expr):
+ module, expression = expr.split(':', 1)
+ __import__(module)
+ module = sys.modules[module]
+ obj = eval(expression, module.__dict__)
+ return JsonRpcApp(obj)
+
+def main(args=None):
+ import optparse
+ from wsgiref import simple_server
+ parser = optparse.OptionParser(
+ usage='%prog [OPTIONS] MODULE:EXPRESSION')
+ parser.add_option(
+ '-p', '--port', default='8080',
+ help='Port to serve on (default 8080)')
+ parser.add_option(
+ '-H', '--host', default='127.0.0.1',
+ help='Host to serve on (default localhost; 0.0.0.0 to make public)')
+ options, args = parser.parse_args()
+ if not args or len(args) > 1:
+ print 'You must give a single object reference'
+ parser.print_help()
+ sys.exit(2)
+ app = make_app(args[0])
+ server = simple_server.make_server(options.host, int(options.port), app)
+ print 'Serving on http://%s:%s' % (options.host, options.port)
+ server.serve_forever()
+ # Try python jsonrpc.py 'jsonrpc:DemoObject()'
+
+if __name__ == '__main__':
+ main()
diff --git a/lib/webob_1_1_1/docs/jsonrpc-example-code/test_jsonrpc.py b/lib/webob_1_1_1/docs/jsonrpc-example-code/test_jsonrpc.py
new file mode 100644
index 0000000..a418516
--- /dev/null
+++ b/lib/webob_1_1_1/docs/jsonrpc-example-code/test_jsonrpc.py
@@ -0,0 +1,3 @@
+if __name__ == '__main__':
+ import doctest
+ doctest.testfile('test_jsonrpc.txt')
diff --git a/lib/webob_1_1_1/docs/jsonrpc-example-code/test_jsonrpc.txt b/lib/webob_1_1_1/docs/jsonrpc-example-code/test_jsonrpc.txt
new file mode 100644
index 0000000..3aa271e
--- /dev/null
+++ b/lib/webob_1_1_1/docs/jsonrpc-example-code/test_jsonrpc.txt
@@ -0,0 +1,27 @@
+This is a test of the ``jsonrpc.py`` module::
+
+ >>> class Divider(object):
+ ... def divide(self, a, b):
+ ... return a / b
+ >>> from jsonrpc import *
+ >>> app = JsonRpcApp(Divider())
+ >>> proxy = ServerProxy('http://localhost:8080', proxy=app)
+ >>> proxy.divide(10, 4)
+ 2
+ >>> proxy.divide(10, 4.0)
+ 2.5
+ >>> proxy.divide(10, 0) # doctest: +ELLIPSIS
+ Traceback (most recent call last):
+ ...
+ Fault: Method error calling http://localhost:8080: integer division or modulo by zero
+ Traceback (most recent call last):
+ File ...
+ result = method(*params)
+ File ...
+ return a / b
+ ZeroDivisionError: integer division or modulo by zero
+ <BLANKLINE>
+ >>> proxy.add(1, 1)
+ Traceback (most recent call last):
+ ...
+ ProxyError: Error from JSON-RPC client http://localhost:8080: 400 Bad Request
diff --git a/lib/webob_1_1_1/docs/jsonrpc-example.txt b/lib/webob_1_1_1/docs/jsonrpc-example.txt
new file mode 100644
index 0000000..fa9a77f
--- /dev/null
+++ b/lib/webob_1_1_1/docs/jsonrpc-example.txt
@@ -0,0 +1,654 @@
+JSON-RPC Example
+================
+
+.. contents::
+
+:author: Ian Bicking
+
+Introduction
+------------
+
+This is an example of how to write a web service using WebOb. The
+example shows how to create a `JSON-RPC <http://json-rpc.org/>`_
+endpoint using WebOb and the `simplejson
+<http://www.undefined.org/python/#simplejson>`_ JSON library. This
+also shows how to use WebOb as a client library using `WSGIProxy
+<http://pythonpaste.org/wsgiproxy/>`_.
+
+While this example presents JSON-RPC, this is not an endorsement of
+JSON-RPC. In fact I don't like JSON-RPC. It's unnecessarily
+un-RESTful, and modelled too closely on `XML-RPC
+<http://www.xmlrpc.com/>`_.
+
+Code
+----
+
+The finished code for this is available in
+`docs/json-example-code/jsonrpc.py
+<http://bitbucket.org/ianb/webob/src/tip/trunk/docs/json-example-code/jsonrpc.py>`_
+-- you can run that file as a script to try it out, or import it.
+
+Concepts
+--------
+
+JSON-RPC wraps an object, allowing you to call methods on that object
+and get the return values. It also provides a way to get error
+responses.
+
+The `specification
+<http://json-rpc.org/wd/JSON-RPC-1-1-WD-20060807.html>`_ goes into the
+details (though in a vague sort of way). Here's the basics:
+
+* All access goes through a POST to a single URL.
+
+* The POST contains a JSON body that looks like::
+
+ {"method": "methodName",
+ "id": "arbitrary-something",
+ "params": [arg1, arg2, ...]}
+
+* The ``id`` parameter is just a convenience for the client to keep
+ track of which response goes with which request. This makes
+ asynchronous calls (like an XMLHttpRequest) easier. We just send
+ the exact same id back as we get, we never look at it.
+
+* The response is JSON. A successful response looks like::
+
+ {"result": the_result,
+ "error": null,
+ "id": "arbitrary-something"}
+
+* The error response looks like::
+
+ {"result": null,
+ "error": {"name": "JSONRPCError",
+ "code": (number 100-999),
+ "message": "Some Error Occurred",
+ "error": "whatever you want\n(a traceback?)"},
+ "id": "arbitrary-something"}
+
+* It doesn't seem to indicate if an error response should have a 200
+ response or a 500 response. So as not to be completely stupid about
+ HTTP, we choose a 500 resonse, as giving an error with a 200
+ response is irresponsible.
+
+Infrastructure
+--------------
+
+To make this easier to test, we'll set up a bit of infrastructure.
+This will open up a server (using `wsgiref
+<http://python.org/doc/current/lib/module-wsgiref.simpleserver.html>`_)
+and serve up our application (note that *creating* the application is
+left out to start with):
+
+.. code-block:: python
+
+ import sys
+
+ def main(args=None):
+ import optparse
+ from wsgiref import simple_server
+ parser = optparse.OptionParser(
+ usage="%prog [OPTIONS] MODULE:EXPRESSION")
+ parser.add_option(
+ '-p', '--port', default='8080',
+ help='Port to serve on (default 8080)')
+ parser.add_option(
+ '-H', '--host', default='127.0.0.1',
+ help='Host to serve on (default localhost; 0.0.0.0 to make public)')
+ if args is None:
+ args = sys.argv[1:]
+ options, args = parser.parse_args()
+ if not args or len(args) > 1:
+ print 'You must give a single object reference'
+ parser.print_help()
+ sys.exit(2)
+ app = make_app(args[0])
+ server = simple_server.make_server(
+ options.host, int(options.port),
+ app)
+ print 'Serving on http://%s:%s' % (options.host, options.port)
+ server.serve_forever()
+
+ if __name__ == '__main__':
+ main()
+
+I won't describe this much. It starts a server, serving up just the
+app created by ``make_app(args[0])``. ``make_app`` will have to load
+up the object and wrap it in our WSGI/WebOb wrapper. We'll be calling
+that wrapper ``JSONRPC(obj)``, so here's how it'll go:
+
+.. code-block:: python
+
+ def make_app(expr):
+ module, expression = expr.split(':', 1)
+ __import__(module)
+ module = sys.modules[module]
+ obj = eval(expression, module.__dict__)
+ return JsonRpcApp(obj)
+
+We use ``__import__(module)`` to import the module, but its return
+value is wonky. We can find the thing it imported in ``sys.modules``
+(a dictionary of all the loaded modules). Then we evaluate the second
+part of the expression in the namespace of the module. This lets you
+do something like ``smtplib:SMTP('localhost')`` to get a fully
+instantiated SMTP object.
+
+That's all the infrastructure we'll need for the server side. Now we
+just have to implement ``JsonRpcApp``.
+
+The Application Wrapper
+-----------------------
+
+Note that I'm calling this an "application" because that's the
+terminology WSGI uses. Everything that gets *called* is an
+"application", and anything that calls an application is called a
+"server".
+
+The instantiation of the server is already figured out:
+
+.. code-block:: python
+
+ class JsonRpcApp(object):
+
+ def __init__(self, obj):
+ self.obj = obj
+
+ def __call__(self, environ, start_response):
+ ... the WSGI interface ...
+
+So the server is an instance bound to the particular object being
+exposed, and ``__call__`` implements the WSGI interface.
+
+We'll start with a simple outline of the WSGI interface, using a kind
+of standard WebOb setup:
+
+.. code-block:: python
+
+ from webob import Request, Response
+ from webob import exc
+
+ class JsonRpcApp(object):
+ ...
+ def __call__(self, environ, start_response):
+ req = Request(environ)
+ try:
+ resp = self.process(req)
+ except ValueError, e:
+ resp = exc.HTTPBadRequest(str(e))
+ except exc.HTTPException, e:
+ resp = e
+ return resp(environ, start_response)
+
+We first create a request object. The request object just wraps the
+WSGI environment. Then we create the response object in the
+``process`` method (which we still have to write). We also do some
+exception catching. We'll turn any ``ValueError`` into a ``400 Bad
+Request`` response. We'll also let ``process`` raise any
+``web.exc.HTTPException`` exception. There's an exception defined in
+that module for all the HTTP error responses, like ``405 Method Not
+Allowed``. These exceptions are themselves WSGI applications (as is
+``webob.Response``), and so we call them like WSGI applications and
+return the result.
+
+The ``process`` method
+----------------------
+
+The ``process`` method of course is where all the fancy stuff
+happens. We'll start with just the most minimal implementation, with
+no error checking or handling:
+
+.. code-block:: python
+
+ from simplejson import loads, dumps
+
+ class JsonRpcApp(object):
+ ...
+ def process(self, req):
+ json = loads(req.body)
+ method = json['method']
+ params = json['params']
+ id = json['id']
+ method = getattr(self.obj, method)
+ result = method(*params)
+ resp = Response(
+ content_type='application/json',
+ body=dumps(dict(result=result,
+ error=None,
+ id=id)))
+ return resp
+
+As long as the request is properly formed and the method doesn't raise
+any exceptions, you are pretty much set. But of course that's not a
+reasonable expectation. There's a whole bunch of things that can go
+wrong. For instance, it has to be a POST method:
+
+.. code-block:: python
+
+ if not req.method == 'POST':
+ raise exc.HTTPMethodNotAllowed(
+ "Only POST allowed",
+ allowed='POST')
+
+And maybe the request body doesn't contain valid JSON:
+
+.. code-block:: python
+
+ try:
+ json = loads(req.body)
+ except ValueError, e:
+ raise ValueError('Bad JSON: %s' % e)
+
+And maybe all the keys aren't in the dictionary:
+
+.. code-block:: python
+
+ try:
+ method = json['method']
+ params = json['params']
+ id = json['id']
+ except KeyError, e:
+ raise ValueError(
+ "JSON body missing parameter: %s" % e)
+
+And maybe it's trying to acces a private method (a method that starts
+with ``_``) -- that's not just a bad request, we'll call that case
+``403 Forbidden``.
+
+.. code-block:: python
+
+ if method.startswith('_'):
+ raise exc.HTTPForbidden(
+ "Bad method name %s: must not start with _" % method)
+
+And maybe ``json['params']`` isn't a list:
+
+.. code-block:: python
+
+ if not isinstance(params, list):
+ raise ValueError(
+ "Bad params %r: must be a list" % params)
+
+And maybe the method doesn't exist:
+
+.. code-block:: python
+
+ try:
+ method = getattr(self.obj, method)
+ except AttributeError:
+ raise ValueError(
+ "No such method %s" % method)
+
+The last case is the error we actually can expect: that the method
+raises some exception.
+
+.. code-block:: python
+
+ try:
+ result = method(*params)
+ except:
+ tb = traceback.format_exc()
+ exc_value = sys.exc_info()[1]
+ error_value = dict(
+ name='JSONRPCError',
+ code=100,
+ message=str(exc_value),
+ error=tb)
+ return Response(
+ status=500,
+ content_type='application/json',
+ body=dumps(dict(result=None,
+ error=error_value,
+ id=id)))
+
+That's a complete server.
+
+The Complete Code
+-----------------
+
+Since we showed all the error handling in pieces, here's the complete
+code:
+
+.. code-block:: python
+
+ from webob import Request, Response
+ from webob import exc
+ from simplejson import loads, dumps
+ import traceback
+ import sys
+
+ class JsonRpcApp(object):
+ """
+ Serve the given object via json-rpc (http://json-rpc.org/)
+ """
+
+ def __init__(self, obj):
+ self.obj = obj
+
+ def __call__(self, environ, start_response):
+ req = Request(environ)
+ try:
+ resp = self.process(req)
+ except ValueError, e:
+ resp = exc.HTTPBadRequest(str(e))
+ except exc.HTTPException, e:
+ resp = e
+ return resp(environ, start_response)
+
+ def process(self, req):
+ if not req.method == 'POST':
+ raise exc.HTTPMethodNotAllowed(
+ "Only POST allowed",
+ allowed='POST')
+ try:
+ json = loads(req.body)
+ except ValueError, e:
+ raise ValueError('Bad JSON: %s' % e)
+ try:
+ method = json['method']
+ params = json['params']
+ id = json['id']
+ except KeyError, e:
+ raise ValueError(
+ "JSON body missing parameter: %s" % e)
+ if method.startswith('_'):
+ raise exc.HTTPForbidden(
+ "Bad method name %s: must not start with _" % method)
+ if not isinstance(params, list):
+ raise ValueError(
+ "Bad params %r: must be a list" % params)
+ try:
+ method = getattr(self.obj, method)
+ except AttributeError:
+ raise ValueError(
+ "No such method %s" % method)
+ try:
+ result = method(*params)
+ except:
+ text = traceback.format_exc()
+ exc_value = sys.exc_info()[1]
+ error_value = dict(
+ name='JSONRPCError',
+ code=100,
+ message=str(exc_value),
+ error=text)
+ return Response(
+ status=500,
+ content_type='application/json',
+ body=dumps(dict(result=None,
+ error=error_value,
+ id=id)))
+ return Response(
+ content_type='application/json',
+ body=dumps(dict(result=result,
+ error=None,
+ id=id)))
+
+The Client
+----------
+
+It would be nice to have a client to test out our server. Using
+`WSGIProxy`_ we can use WebOb
+Request and Response to do actual HTTP connections.
+
+The basic idea is that you can create a blank Request:
+
+.. code-block:: python
+
+ >>> from webob import Request
+ >>> req = Request.blank('http://python.org')
+
+Then you can send that request to an application:
+
+.. code-block:: python
+
+ >>> from wsgiproxy.exactproxy import proxy_exact_request
+ >>> resp = req.get_response(proxy_exact_request)
+
+This particular application (``proxy_exact_request``) sends the
+request over HTTP:
+
+.. code-block:: python
+
+ >>> resp.content_type
+ 'text/html'
+ >>> resp.body[:10]
+ '<!DOCTYPE '
+
+So we're going to create a proxy object that constructs WebOb-based
+jsonrpc requests, and sends those using ``proxy_exact_request``.
+
+The Proxy Client
+----------------
+
+The proxy client is instantiated with its base URL. We'll also let
+you pass in a proxy application, in case you want to do local requests
+(e.g., to do direct tests against a ``JsonRpcApp`` instance):
+
+.. code-block:: python
+
+ class ServerProxy(object):
+
+ def __init__(self, url, proxy=None):
+ self._url = url
+ if proxy is None:
+ from wsgiproxy.exactproxy import proxy_exact_request
+ proxy = proxy_exact_request
+ self.proxy = proxy
+
+This ServerProxy object itself doesn't do much, but you can call methods on
+it. We can intercept any access ``ServerProxy(...).method`` with the
+magic function ``__getattr__``. Whenever you get an attribute that
+doesn't exist in an instance, Python will call
+``inst.__getattr__(attr_name)`` and return that. When you *call* a
+method, you are calling the object that ``.method`` returns. So we'll
+create a helper object that is callable, and our ``__getattr__`` will
+just return that:
+
+.. code-block:: python
+
+ class ServerProxy(object):
+ ...
+ def __getattr__(self, name):
+ # Note, even attributes like __contains__ can get routed
+ # through __getattr__
+ if name.startswith('_'):
+ raise AttributeError(name)
+ return _Method(self, name)
+
+ class _Method(object):
+ def __init__(self, parent, name):
+ self.parent = parent
+ self.name = name
+
+Now when we call the method we'll be calling ``_Method.__call__``, and
+the HTTP endpoint will be ``self.parent._url``, and the method name
+will be ``self.name``.
+
+Here's the code to do the call:
+
+.. code-block:: python
+
+ class _Method(object):
+ ...
+
+ def __call__(self, *args):
+ json = dict(method=self.name,
+ id=None,
+ params=list(args))
+ req = Request.blank(self.parent._url)
+ req.method = 'POST'
+ req.content_type = 'application/json'
+ req.body = dumps(json)
+ resp = req.get_response(self.parent.proxy)
+ if resp.status_int != 200 and not (
+ resp.status_int == 500
+ and resp.content_type == 'application/json'):
+ raise ProxyError(
+ "Error from JSON-RPC client %s: %s"
+ % (self._url, resp.status),
+ resp)
+ json = loads(resp.body)
+ if json.get('error') is not None:
+ e = Fault(
+ json['error'].get('message'),
+ json['error'].get('code'),
+ json['error'].get('error'),
+ resp)
+ raise e
+ return json['result']
+
+We raise two kinds of exceptions here. ``ProxyError`` is when
+something unexpected happens, like a ``404 Not Found``. ``Fault`` is
+when a more expected exception occurs, i.e., the underlying method
+raised an exception.
+
+In both cases we'll keep the response object around, as that can be
+interesting. Note that you can make exceptions have any methods or
+signature you want, which we'll do:
+
+.. code-block:: python
+
+ class ProxyError(Exception):
+ """
+ Raised when a request via ServerProxy breaks
+ """
+ def __init__(self, message, response):
+ Exception.__init__(self, message)
+ self.response = response
+
+ class Fault(Exception):
+ """
+ Raised when there is a remote error
+ """
+ def __init__(self, message, code, error, response):
+ Exception.__init__(self, message)
+ self.code = code
+ self.error = error
+ self.response = response
+ def __str__(self):
+ return 'Method error calling %s: %s\n%s' % (
+ self.response.request.url,
+ self.args[0],
+ self.error)
+
+Using Them Together
+-------------------
+
+Good programmers start with tests. But at least we'll end with a
+test. We'll use `doctest
+<http://python.org/doc/current/lib/module-doctest.html>`_ for our
+tests. The test is in `docs/json-example-code/test_jsonrpc.txt
+<http://bitbucket.org/ianb/webob/src/tip/docs/json-example-code/test_jsonrpc.txt>`_
+and you can run it with `docs/json-example-code/test_jsonrpc.py
+<http://bitbucket.org/ianb/webob/src/tip/docs/json-example-code/test_jsonrpc.py>`_,
+which looks like:
+
+.. code-block:: python
+
+ if __name__ == '__main__':
+ import doctest
+ doctest.testfile('test_jsonrpc.txt')
+
+As you can see, it's just a stub to run the doctest. We'll need a
+simple object to expose. We'll make it real simple:
+
+.. code-block:: python
+
+ >>> class Divider(object):
+ ... def divide(self, a, b):
+ ... return a / b
+
+Then we'll get the app setup:
+
+.. code-block:: python
+
+ >>> from jsonrpc import *
+ >>> app = JsonRpcApp(Divider())
+
+And attach the client *directly* to it:
+
+.. code-block:: python
+
+ >>> proxy = ServerProxy('http://localhost:8080', proxy=app)
+
+Because we gave the app itself as the proxy, the URL doesn't actually
+matter.
+
+Now, if you are used to testing you might ask: is this kosher? That
+is, we are shortcircuiting HTTP entirely. Is this a realistic test?
+
+One thing you might be worried about in this case is that there are
+more shared objects than you'd have with HTTP. That is, everything
+over HTTP is serialized to headers and bodies. Without HTTP, we can
+send stuff around that can't go over HTTP. This *could* happen, but
+we're mostly protected because the only thing the application's share
+is the WSGI ``environ``. Even though we use a ``webob.Request``
+object on both side, it's not the *same* request object, and all the
+state is studiously kept in the environment. We *could* share things
+in the environment that couldn't go over HTTP. For instance, we could
+set ``environ['jsonrpc.request_value'] = dict(...)``, and avoid
+``simplejson.dumps`` and ``simplejson.loads``. We *could* do that,
+and if we did then it is possible our test would work even though the
+libraries were broken over HTTP. But of course inspection shows we
+*don't* do that. A little discipline is required to resist playing clever
+tricks (or else you can play those tricks and do more testing).
+Generally it works well.
+
+So, now we have a proxy, lets use it:
+
+.. code-block:: python
+
+ >>> proxy.divide(10, 4)
+ 2
+ >>> proxy.divide(10, 4.0)
+ 2.5
+
+Lastly, we'll test a couple error conditions. First a method error:
+
+.. code-block:: python
+
+ >>> proxy.divide(10, 0) # doctest: +ELLIPSIS
+ Traceback (most recent call last):
+ ...
+ Fault: Method error calling http://localhost:8080: integer division or modulo by zero
+ Traceback (most recent call last):
+ File ...
+ result = method(*params)
+ File ...
+ return a / b
+ ZeroDivisionError: integer division or modulo by zero
+ <BLANKLINE>
+
+It's hard to actually predict this exception, because the test of the
+exception itself contains the traceback from the underlying call, with
+filenames and line numbers that aren't stable. We use ``# doctest:
++ELLIPSIS`` so that we can replace text we don't care about with
+``...``. This is actually figured out through copy-and-paste, and
+visual inspection to make sure it looks sensible.
+
+The other exception can be:
+
+.. code-block:: python
+
+ >>> proxy.add(1, 1)
+ Traceback (most recent call last):
+ ...
+ ProxyError: Error from JSON-RPC client http://localhost:8080: 400 Bad Request
+
+Here the exception isn't a JSON-RPC method exception, but a more basic
+ProxyError exception.
+
+Conclusion
+----------
+
+Hopefully this will give you ideas about how to implement web services
+of different kinds using WebOb. I hope you also can appreciate the
+elegance of the symmetry of the request and response objects, and the
+client and server for the protocol.
+
+Many of these techniques would be better used with a `RESTful
+<http://en.wikipedia.org/wiki/Representational_State_Transfer>`_
+service, so do think about that direction if you are implementing your
+own protocol.
+
diff --git a/lib/webob_1_1_1/docs/license.txt b/lib/webob_1_1_1/docs/license.txt
new file mode 100644
index 0000000..2369791
--- /dev/null
+++ b/lib/webob_1_1_1/docs/license.txt
@@ -0,0 +1,23 @@
+License
+=======
+
+Copyright (c) 2007 Ian Bicking and Contributors
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/lib/webob_1_1_1/docs/modules/dec.txt b/lib/webob_1_1_1/docs/modules/dec.txt
new file mode 100644
index 0000000..3c9e7a2
--- /dev/null
+++ b/lib/webob_1_1_1/docs/modules/dec.txt
@@ -0,0 +1,10 @@
+:mod:`webob.dec` -- WSGIfy decorator
+====================================
+
+.. automodule:: webob.dec
+
+Decorator
+---------
+
+.. autoclass:: wsgify
+ :members:
diff --git a/lib/webob_1_1_1/docs/modules/webob.txt b/lib/webob_1_1_1/docs/modules/webob.txt
new file mode 100644
index 0000000..5eed7cf
--- /dev/null
+++ b/lib/webob_1_1_1/docs/modules/webob.txt
@@ -0,0 +1,95 @@
+:mod:`webob` -- Request/Response objects
+========================================
+
+Request
+-------
+
+.. autoclass:: webob.request.BaseRequest
+ :members:
+
+Response
+--------
+
+.. autoclass:: webob.response.Response
+ :members:
+.. autoclass:: webob.response.AppIterRange
+ :members:
+
+
+Headers
+-------
+
+Accept-*
+~~~~~~~~
+
+.. automodule:: webob.acceptparse
+.. autoclass:: Accept
+ :members:
+.. autoclass:: MIMEAccept
+ :members:
+
+Cache-Control
+~~~~~~~~~~~~~
+.. autoclass:: webob.cachecontrol.CacheControl
+ :members:
+
+Range and related headers
+~~~~~~~~~~~~~~~~~~~~~~~~~
+.. autoclass:: webob.byterange.Range
+ :members:
+.. autoclass:: webob.byterange.ContentRange
+ :members:
+.. autoclass:: webob.etag.IfRange
+ :members:
+
+ETag
+~~~~
+.. autoclass:: webob.etag.ETagMatcher
+ :members:
+
+
+
+
+
+Misc Functions and Internals
+----------------------------
+
+.. autofunction:: webob.html_escape
+
+.. comment:
+ not sure what to do with these constants; not autoclass
+ .. autoclass:: webob.day
+ .. autoclass:: webob.week
+ .. autoclass:: webob.hour
+ .. autoclass:: webob.minute
+ .. autoclass:: webob.second
+ .. autoclass:: webob.month
+ .. autoclass:: webob.year
+
+.. autoclass:: webob.headers.ResponseHeaders
+ :members:
+.. autoclass:: webob.headers.EnvironHeaders
+ :members:
+
+.. automodule:: webob.multidict
+.. autoclass:: MultiDict
+ :members:
+.. autoclass:: UnicodeMultiDict
+ :members:
+.. autoclass:: NestedMultiDict
+ :members:
+.. autoclass:: NoVars
+ :members:
+
+.. autoclass:: webob.cachecontrol.UpdateDict
+ :members:
+
+
+.. comment:
+ Descriptors
+ -----------
+
+ .. autoclass:: webob.descriptors.environ_getter
+ .. autoclass:: webob.descriptors.header_getter
+ .. autoclass:: webob.descriptors.converter
+ .. autoclass:: webob.descriptors.deprecated_property
diff --git a/lib/webob_1_1_1/docs/news.txt b/lib/webob_1_1_1/docs/news.txt
new file mode 100644
index 0000000..c508b95
--- /dev/null
+++ b/lib/webob_1_1_1/docs/news.txt
@@ -0,0 +1,759 @@
+News
+====
+
+.. contents::
+
+1.1.1
+---------
+
+* Fix disconnect detection being incorrect in some cases (`issue 21
+ <https://bitbucket.org/ianb/webob/issue/21>`_)
+
+* Fix exception when calling ``.accept.best_match(..)`` on a header containing
+ '*' (instead of '*/*')
+
+* Split ``Accept`` class into appropriate subclasses (``AcceptCharset``,
+ ``AcceptLanguage``)
+
+* Improve language matching code so that ``'en' in AcceptLanguage('en-gb')``
+ (the app can now offer a generic 'en' and it will match any of the
+ accepted dialects) and ``'en_GB' in AcceptLanguage('en-gb')`` (normalization
+ of the dash/underscode in language names).
+
+* Deprecate ``req.etag.weak_match(..)``
+
+* Deprecate ``Response.request`` and ``Response.environ`` attrs.
+
+
+1.1
+---------
+
+* Remove deprecation warnings for ``unicode_body`` and ``ubody``.
+
+
+1.1rc1
+---------
+
+* Deprecate ``Response.ubody`` / ``.unicode_body`` in favor of new ``.text`` attribute
+ (the old names will be removed in 1.3 or even later).
+
+* Make ``Response.write`` much more efficient (`issue 18
+ <https://bitbucket.org/ianb/webob/issue/18>`_)
+
+* Make sure copying responses does not reset Content-Length or Content-MD5 of the
+ original (and that of future copies).
+
+* Change ``del res.body`` semantics so that it doesn't make the response invalid,
+ but only removes the response body.
+
+* Remove ``Response._body`` so the ``_app_iter`` is the only representation.
+
+
+1.1b2
+---------
+
+* Add detection for browser / user-agent disconnects. If the client disconnected
+ before sending the entire request body (POST / PUT), ``req.POST``, ``req.body``
+ and other related properties and methods will raise an exception.
+ Previously this caused the application get a truncated request with no indication that it
+ is incomplete.
+
+* Make ``Response.body_file`` settable. This is now valid:
+ ``Response(body_file=open('foo.bin'), content_type=...)``
+
+* Revert the restriction on req.body not being settable for GET and some
+ other requests. Such requests actually can have a body according to HTTP BIS
+ (see also `commit message <https://bitbucket.org/ianb/webob/changeset/b3ef34c57936>`_)
+
+* Add support for file upload testing via ``Request.blank(POST=..)``. Patch contributed by
+ Tim Perevezentsev. See also:
+ `ticket <https://bitbucket.org/ianb/webob/issue/15>`_,
+ `changeset <https://bitbucket.org/ianb/webob/changeset/4ba9ab0c3f99>`_.
+
+* Deprecate ``req.str_GET``, ``str_POST``, ``str_params`` and ``str_cookies`` (warning).
+
+
+* Deprecate ``req.decode_param_names`` (warning).
+
+* Change ``req.decode_param_names`` default to ``True``. This means that ``.POST``, ``.GET``,
+ ``.params`` and ``.cookies`` keys are now unicode. This is necessary for WebOb to behave
+ as close as possible on Python 2 and Python 3.
+
+
+1.1b1
+---------
+
+* We have acquired the webob.org domain, docs are now hosted at `docs.webob.org
+ <http://docs.webob.org/>`_
+
+* Make ``accept.quality(..)`` return best match quality, not first match quality.
+
+* Fix ``Range.satisfiable(..)`` edge cases.
+
+* Make sure ``WSGIHTTPException`` instances return the same headers for ``HEAD``
+ and ``GET`` requests.
+
+* Drop Python 2.4 support
+
+* Deprecate ``HTTPException.exception`` (warning on use).
+
+* Deprecate ``accept.first_match(..)`` (warning on use).
+ Use ``.best_match(..)`` instead.
+
+* Complete deprecation of ``req.[str_]{post|query}vars`` properties
+ (exception on use).
+
+* Remove ``FakeCGIBody.seek`` hack (no longer necessary).
+
+
+1.0.8
+------
+
+* Escape commas in cookie values (see also:
+ `stdlib Cookie bug <http://bugs.python.org/issue9824>`_)
+
+* Change cookie serialization to more closely match how cookies usually
+ are serialized (unquoted expires, semicolon separators even between morsels)
+
+* Fix some rare cases in cookie parsing
+
+* Enhance the req.is_body_readable to always guess GET, HEAD, DELETE and TRACE
+ as unreadable and PUT and POST as readable
+ (`issue 12 <https://bitbucket.org/ianb/webob/issue/12>`_)
+
+* Deny setting req.body or req.body_file to non-empty values for GET, HEAD and
+ other bodiless requests
+
+* Fix running nosetests with arguments on UNIX systems
+ (`issue 11 <https://bitbucket.org/ianb/webob/issue/11>`_)
+
+
+1.0.7
+------
+
+* Fix ``Accept`` header matching for items with zero-quality
+ (`issue 10 <https://bitbucket.org/ianb/webob/issue/10>`_)
+
+* Hide password values in ``MultiDict.__repr__``
+
+1.0.6
+------
+
+* Use ``environ['wsgi.input'].read()`` instead of ``.read(-1)`` because the former
+ is explicitly mentioned in PEP-3333 and CherryPy server does not support the latter.
+
+* Add new ``environ['webob.is_body_readable']`` flag which specifies if the
+ input stream is readable even if the ``CONTENT_LENGTH`` is not set.
+ WebOb now only ever reads the input stream if the content-length is known
+ or this flag is set.
+
+* The two changes above fix a hangup with CherryPy and wsgiref servers
+ (`issue 6 <https://bitbucket.org/ianb/webob/issue/6>`_)
+
+* ``req.body_file`` is now safer to read directly. For ``GET`` and other similar requests
+ it returns an empty ``StringIO`` or ``BytesIO`` object even if the server passed in
+ something else.
+
+* Setting ``req.body_file`` to a string now produces a PendingDeprecationWarning.
+ It will produce DeprecationWarning in 1.1 and raise an error in 1.2. Either
+ set ``req.body_file`` to a file-like object or set ``req.body`` to a string value.
+
+* Fix ``.pop()`` and ``.setdefault(..)`` methods of ``req/resp.cache_control``
+
+* Thanks to the participants of `Pyramid sprint at the PyCon US 2011
+ <https://bitbucket.org/ianb/webob/changeset/7b7dc3ec6159>`_ WebOb now has
+ 100% test coverage.
+
+1.0.5
+------
+* Restore Python 2.4 compatibility.
+
+1.0.4
+------
+
+* The field names escaping bug semi-fixed in 1.0.3 and originally blamed on cgi module
+ was in fact a ``webob.request._encode_multipart`` bug (also in Google Chrome) and was
+ lurking in webob code for quite some time -- 1.0.2 just made it trigger more often.
+ Now it is fixed properly.
+
+* Make sure that req.url and related properties do not unnecessarily escape some chars
+ (``:@&+$``) in the URI path (`issue 5 <https://bitbucket.org/ianb/webob/issue/5>`_)
+
+* Revert some changes from 1.0.3 that have broken backwards compatibility for some apps.
+ Getting ``req.body_file`` does not make input stream seekable, but there's a new property
+ ``req.body_file_seekable`` that does.
+
+* ``Request.get_response`` and ``Request.call_application`` seek the input body to start
+ before calling the app (if possible).
+
+* Accessing ``req.body`` 'rewinds' the input stream back to pos 0 as well.
+
+* When accessing ``req.POST`` we now avoid making the body seekable as the input stream data
+ are preserved in ``FakeCGIBody`` anyway.
+
+* Add new method ``Request.from_string``.
+
+* Make sure ``Request.as_string()`` uses CRLF to separate headers.
+
+* Improve parity between ``Request.as_string()`` and ``.from_file``/``.from_string``
+ methods, so that the latter can parse output of the former and create a similar
+ request object which wasn't always the case previously.
+
+1.0.3
+------
+
+* Correct a caching issue introduced in WebOb 1.0.2 that was causing unnecessary reparsing
+ of POST requests.
+
+* Fix a bug regarding field names escaping for forms submitted as ``multipart/form-data``.
+ For more infromation see `the bug report and discussion
+ <https://bitbucket.org/ianb/webob/issue/2>`_ and 1.0.4 notes for further fix.
+
+* Add ``req.http_version`` attribute.
+
+1.0.2
+------
+
+* Primary maintainer is now `Sergey Schetinin <http://self.maluke.com/>`_.
+
+* Issue tracker moved from `Trac <http://bit.ly/webob-tickets>`_ to bitbucket's `issue
+ tracker <https://bitbucket.org/ianb/webob/issues>`_
+
+* WebOb 1.0.1 changed the behavior of ``MultiDict.update`` to be more in line with
+ other dict-like objects. We now also issue a warning when we detect that the
+ client code seems to expect the old, extending semantics.
+
+* Make ``Response.set_cookie(key, None)`` set the 'delete-cookie' (same as ``.delete_cookie(key)``)
+
+* Make ``req.upath_info`` and ``req.uscript_name`` settable
+
+* Add :meth:``Request.as_string()`` method
+
+* Add a ``req.is_body_seekable`` property
+
+* Support for the ``deflate`` method with ``resp.decode_content()``
+
+* To better conform to WSGI spec we no longer attempt to use seek on ``wsgi.input`` file
+ instead we assume it is not seekable unless ``env['webob.is_body_seekable']`` is set.
+ When making the body seekable we set that flag.
+
+* A call to ``req.make_body_seekable()`` now guarantees that the body is seekable, is at 0 position
+ and that a correct ``req.content_length`` is present.
+
+* ``req.body_file`` is always seekable. To access ``env['wsgi.input']`` without any processing,
+ use ``req.body_file_raw``. (Partially reverted in 1.0.4)
+
+* Fix responses to HEAD requests with Range.
+
+* Fix ``del resp.content_type``, ``del req.body``, ``del req.cache_control``
+
+* Fix ``resp.merge_cookies()`` when called with an argument that is not a Response instance.
+
+* Fix ``resp.content_body = None`` (was removing Cache-Control instead)
+
+* Fix ``req.body_file = f`` setting ``CONTENT_LENGTH`` to ``-1`` (now removes from environ)
+
+* Fix: make sure req.copy() leaves the original with seekable body
+
+* Fix handling of WSGI environs with missing ``SCRIPT_NAME``
+
+* A lot of tests were added by Mariano Mara and Danny Navarro.
+
+
+
+1.0.1
+-----
+
+* As WebOb requires Python 2.4 or later, drop some compatibility modules
+ and update the code to use the decorator syntax.
+
+* Implement optional on-the-fly response compression (``resp.encode_content(lazy=True)``)
+
+* Drop ``util.safezip`` module and make ``util`` a module instead of a subpackage.
+ Merge ``statusreasons`` into it.
+
+* Instead of using stdlib ``Cookie`` with monkeypatching, add a derived
+ but thoroughly rewritten, cleaner, safer and faster ``webob.cookies`` module.
+
+* Fix: ``Response.merge_cookies`` now copies the headers before modification instead of
+ doing it in-place.
+
+* Fix: setting request header attribute to ``None`` deletes that header.
+ (Bug only affected the 1.0 release).
+
+* Use ``io.BytesIO`` for the request body file on Python 2.7 and newer.
+
+* If a UnicodeMultiDict was used as the ``multi`` argument of another
+ UnicodeMultiDict, and a ``cgi.FieldStorage`` with a ``filename``
+ with high-order characters was present in the underlying
+ UnicodeMultiDict, a ``UnicodeEncodeError`` would be raised when any
+ helper method caused the ``_decode_value`` method to be called,
+ because the method would try to decode an already decoded string.
+
+* Fix tests to pass under Python 2.4.
+
+* Add descriptive docstrings to each exception in ``webob.exc``.
+
+* Change the behaviour of ``MultiDict.update`` to overwrite existing header
+ values instead of adding new headers. The extending semantics are now available
+ via the ``extend`` method.
+
+* Fix a bug in ``webob.exc.WSGIHTTPException.__init__``. If a list of
+ ``headers`` was passed as a sequence which contained duplicate keys (for
+ example, multiple ``Set-Cookie`` headers), all but one of those headers
+ would be lost, because the list was effectively flattened into a dictionary
+ as the result of calling ``self.headers.update``. Fixed via calling
+ ``self.headers.extend`` instead.
+
+1.0
+---
+
+* 1.0, yay!
+
+* Pull in werkzeug Cookie fix for malformed cookie bug.
+
+* Implement :meth:`Request.from_file` and
+ :meth:`Response.from_file` which are kind of the inversion of
+ ``str(req)`` and ``str(resp)``
+
+* Add optional ``pattern`` argument to :meth:`Request.path_info_pop` that requires
+ the ``path_info`` segment to match the passed regexp to get popped and returned.
+
+* Rewrite most of descriptor implementations for speed.
+
+* Reorder descriptor declarations to group them by their semantics.
+
+* Move code around so that there are fewer compat modules.
+
+* Change :meth:``HTTPError.__str__`` to better conform to PEP 352.
+
+* Make :attr:`Request.cache_control` a view on the headers.
+
+* Correct Accept-Language and Accept-Charset matching to fully conform to the HTTP spec.
+
+* Expose parts of :meth:`Request.blank` as :func:`environ_from_url`
+ and :func:`environ_add_POST`
+
+* Fix Authorization header parsing for some corner cases.
+
+* Fix an error generated if the user-agent sends a 'Content_Length' header
+ (note the underscore).
+
+* Kill :attr:`Request.default_charset`. Request charset defaults to UTF-8.
+ This ensures that all values in ``req.GET``, ``req.POST`` and ``req.params``
+ are always unicode.
+
+* Fix the ``headerlist`` and ``content_type`` constructor arguments priorities
+ for :class:`HTTPError` and subclasses.
+
+* Add support for weak etags to conditional Response objects.
+
+* Fix locale-dependence for some cookie dates strings.
+
+* Improve overall test coverage.
+
+* Rename class ``webob.datastruct.EnvironHeaders`` to ``webob.headers.EnvironHeaders``
+
+* Rename class ``webob.headerdict.HeaderDict`` to ``webob.headers.ResponseHeaders``
+
+* Rename class ``webob.updatedict.UpdateDict`` to ``webob.cachecontrol.UpdateDict``
+
+0.9.8
+-----
+
+* Fix issue with WSGIHTTPException inadvertently generating unicode body
+ and failing to encode it
+
+* WWW-Authenticate response header is accessible as
+ ``response.www_authenticate``
+
+* ``response.www_authenticate`` and ``request.authorization`` hold None
+ or tuple ``(auth_method, params)`` where ``params`` is a dictionary
+ (or a string when ``auth_method`` is not one of known auth schemes
+ and for Authenticate: Basic ...)
+
+* Don't share response headers when getting a response like ``resp =
+ req.get_response(some_app)``; this can avoid some funny errors with
+ modifying headers and reusing Response objects.
+
+* Add `overwrite` argument to :meth:`Response.set_cookie` that make the
+ new value overwrite the previously set. `False` by default.
+
+* Add `strict` argument to :meth:`Response.unset_cookie` that controls
+ if an exception should be raised in case there are no cookies to unset.
+ `True` by default.
+
+* Fix ``req.GET.copy()``
+
+* Make sure that 304 Not Modified responses generated by
+ :meth:`Response.conditional_response_app` exclude Content-{Length/Type}
+ headers
+
+* Fix ``Response.copy()`` not being an independent copy
+
+* When the requested range is not satisfiable, return a 416 error
+ (was returning entire body)
+
+* Truncate response for range requests that go beyond the end of body
+ (was treating as invalid).
+
+0.9.7.1
+-------
+
+* Fix an import problem with Pylons
+
+0.9.7
+-----
+
+* Moved repository from svn location to
+ http://bitbucket.org/ianb/webob/
+
+* Arguments to :meth:`Accept.best_match` must be specific types,
+ not wildcards. The server should know a list of specic types it can
+ offer and use ``best_match`` to select a specific one.
+
+* With ``req.accept.best_match([types])`` prefer the first type in the
+ list (previously it preferred later types).
+
+* Also, make sure that if the user-agent accepts multiple types and
+ there are multiple matches to the types that the application offers,
+ ``req.accept.best_match([..])`` returns the most specific match.
+ So if the server can satisfy either ``image/*`` or ``text/plain``
+ types, the latter will be picked independent from the order the accepted
+ or offered types are listed (given they have the same quality rating).
+
+* Fix Range, Content-Range and AppIter support all of which were broken
+ in many ways, incorrectly parsing ranges, reporting incorrect
+ content-ranges, failing to generate the correct body to satisfy the range
+ from ``app_iter`` etc.
+
+* Fix assumption that presense of a ``seek`` method means that the stream
+ is seekable.
+
+* Add ``ubody`` alias for ``Response.unicode_body``
+
+* Add Unicode versions of ``Request.script_name`` and ``path_info``:
+ ``uscript_name`` and ``upath_info``.
+
+* Split __init__.py into four modules: request, response, descriptors and
+ datetime_utils.
+
+* Fix ``Response.body`` access resetting Content-Length to zero
+ for HEAD responses.
+
+* Support passing Unicode bodies to :class:`WSGIHTTPException`
+ constructors.
+
+* Make ``bool(req.accept)`` return ``False`` for requests with missing
+ Accept header.
+
+* Add HTTP version to :meth:`Request.__str__` output.
+
+* Resolve deprecation warnings for parse_qsl on Python 2.6 and newer.
+
+* Fix :meth:`Response.md5_etag` setting Content-MD5 in incorrect
+ format.
+
+* Add ``Request.authorization`` property for Authorization header.
+
+* Make sure ETag value is always quoted (required by RFC)
+
+* Moved most ``Request`` behavior into a new class named
+ ``BaseRequest``. The ``Request`` class is now a superclass for
+ ``BaseRequest`` and a simple mixin which manages
+ ``environ['webob.adhoc_attrs']`` when ``__setitem__``,
+ ``__delitem__`` and ``__getitem__`` are called. This allows
+ framework developers who do not want the
+ ``environ['webob.adhoc_attrs']`` mutation behavior from
+ ``__setattr__``. (chrism)
+
+* Added response attribute ``response.content_disposition`` for its
+ associated header.
+
+* Changed how ``charset`` is determined on :class:`webob.Request`
+ objects. Now the ``charset`` parameter is read on the Content-Type
+ header, if it is present. Otherwise a ``default_charset`` parameter
+ is read, or the ``charset`` argument to the Request constructor.
+ This is more similar to how :class:`webob.Response` handles the
+ charset.
+
+* Made the case of the Content-Type header consistent (note: this
+ might break some doctests).
+
+* Make ``req.GET`` settable, such that ``req.environ['QUERY_STRING']``
+ is updated.
+
+* Fix problem with ``req.POST`` causing a re-parse of the body when
+ you instantiate multiple ``Request`` objects over the same environ
+ (e.g., when using middleware that looks at ``req.POST``).
+
+* Recreate the request body properly when a ``POST`` includes file
+ uploads.
+
+* When ``req.POST`` is updated, the generated body will include the
+ new values.
+
+* Added a ``POST`` parameter to :meth:`webob.Request.blank`; when
+ given this will create a request body for the POST parameters (list
+ of two-tuples or dictionary-like object). Note: this does not
+ handle unicode or file uploads.
+
+* Added method :meth:`webob.Response.merge_cookies`, which takes the
+ ``Set-Cookie`` headers from a Response, and merges them with another
+ response or WSGI application. (This is useful for flash messages.)
+
+* Fix a problem with creating exceptions like
+ ``webob.exc.HTTPNotFound(body='<notfound/>',
+ content_type='application/xml')`` (i.e., non-HTML exceptions).
+
+* When a Location header is not absolute in a Response, it will be
+ made absolute when the Response is called as a WSGI application.
+ This makes the response less bound to a specific request.
+
+* Added :mod:`webob.dec`, a decorator for making WSGI applications
+ from functions with the signature ``resp = app(req)``.
+
+0.9.6.1
+-------
+
+* Fixed :meth:`Response.__init__`, which for some content types would
+ raise an exception.
+
+* The ``req.body`` property will not recreate a StringIO object
+ unnecessarily when rereading the body.
+
+0.9.6
+-----
+
+* Removed `environ_getter` from :class:`webob.Request`. This
+ largely-unused option allowed a Request object to be instantiated
+ with a dynamic underlying environ. Since it wasn't used much, and
+ might have been ill-advised from the beginning, and affected
+ performance, it has been removed (from Chris McDonough).
+
+* Speed ups for :meth:`webob.Response.__init__` and
+ :meth:`webob.Request.__init__`
+
+* Fix defaulting of ``CONTENT_TYPE`` instead of ``CONTENT_LENGTH`` to
+ 0 in ``Request.str_POST``.
+
+* Added :meth:`webob.Response.copy`
+
+0.9.5
+-----
+
+* Fix ``Request.blank('/').copy()`` raising an exception.
+
+* Fix a potential memory leak with HEAD requests and 304 responses.
+
+* Make :func:`webob.html_escape` respect the ``.__html__()`` magic
+ method, which allows you to use HTML in
+ :class`webob.exc.HTTPException` instances.
+
+* Handle unicode values for ``resp.location``.
+
+* Allow arbitrary keyword arguments to ``exc.HTTP*`` (the same
+ keywords you can send to :class:`webob.Response`).
+
+* Allow setting :meth:`webob.Response.cache_expires` (usually it is
+ called as a method). This is primarily to allow
+ ``Response(cache_expires=True)``.
+
+0.9.4
+-----
+
+* Quiet Python 2.6 deprecation warnings.
+
+* Added an attribute ``unicode_errors`` to :class:`webob.Response` --
+ if set to something like ``unicode_errors='replace'`` it will decode
+ ``resp.body`` appropriately. The default is ``strict`` (which was
+ the former un-overridable behavior).
+
+0.9.3
+-----
+
+* Make sure that if changing the body the Content-MD5 header is
+ removed. (Otherwise a lot of middleware would accidentally
+ corrupt responses).
+
+* Fixed ``Response.encode_content('identity')`` case (was a no-op even
+ for encoded bodies).
+
+* Fixed :meth:`Request.remove_conditional_headers` that was removing
+ If-Match header instead of If-None-Match.
+
+* Fixed ``resp.set_cookie(max_age=timedelta(...))``
+
+* ``request.POST`` now supports PUT requests with the appropriate
+ Content-Type.
+
+0.9.2
+-----
+
+* Add more arguments to :meth:`Request.remove_conditional_headers`
+ for more fine-grained control: `remove_encoding`, `remove_range`,
+ `remove_match`, `remove_modified`. All of them are `True` by default.
+
+* Add an `set_content_md5` argument to :meth:`Response.md5_etag`
+ that calculates and sets Content-MD5 reponse header from current
+ body.
+
+* Change formatting of cookie expires, to use the more traditional
+ format ``Wed, 5-May-2001 15:34:10 GMT`` (dashes instead of spaces).
+ Browsers should deal with either format, but some other code expects
+ dashes.
+
+* Added in ``sorted`` function for backward compatibility with Python
+ 2.3.
+
+* Allow keyword arguments to :class:`webob.Request`, which assign
+ attributes (possibly overwriting values in the environment).
+
+* Added methods :meth:`webob.Request.make_body_seekable` and
+ :meth:`webob.Request.copy_body`, which make it easier to share a
+ request body among different consuming applications, doing something
+ like `req.make_body_seekable(); req.body_file.seek(0)`
+
+0.9.1
+-----
+
+* ``request.params.copy()`` now returns a writable MultiDict (before
+ it returned an unwritable object).
+
+* There were several things broken with ``UnicodeMultiDict`` when
+ ``decode_param_names`` is turned on (when the dictionary keys are
+ unicode).
+
+* You can pass keyword arguments to ``Request.blank()`` that will be
+ used to construct ``Request`` (e.g., ``Request.blank('/',
+ decode_param_names=True)``).
+
+* If you set headers like ``response.etag`` to a unicode value, they
+ will be encoded as ISO-8859-1 (however, they will remain encoded,
+ and ``response.etag`` will not be a unicode value).
+
+* When parsing, interpret times with no timezone as UTC (previously
+ they would be interpreted as local time).
+
+* Set the Expires property on cookies when using
+ ``response.set_cookie()``. This is inherited from ``max_age``.
+
+* Support Unicode cookie values
+
+0.9
+---
+
+* Added ``req.urlarg``, which represents positional arguments in
+ ``environ['wsgiorg.routing_args']``.
+
+* For Python 2.4, added attribute get/set proxies on exception objects
+ from, for example, ``webob.exc.HTTPNotFound().exception``, so that
+ they act more like normal response objects (despite not being
+ new-style classes or ``webob.Response`` objects). In Python 2.5 the
+ exceptions are ``webob.Response`` objects.
+
+Backward Incompatible Changes
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+* The ``Response`` constructor has changed: it is now ``Response([body],
+ [status], ...)`` (before it was ``Response([status], [body], ...)``).
+ Body may be str or unicode.
+
+* The ``Response`` class defaults to ``text/html`` for the
+ Content-Type, and ``utf8`` for the charset (charset is only set on
+ ``text/*`` and ``application/*+xml`` responses).
+
+Bugfixes and Small Changes
+~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+* Use ``BaseCookie`` instead of ``SimpleCookie`` for parsing cookies.
+
+* Added ``resp.write(text)`` method, which is equivalent to
+ ``resp.body += text`` or ``resp.unicode_body += text``, depending on
+ the type of ``text``.
+
+* The ``decode_param_names`` argument (used like
+ ``Request(decode_param_names=True)``) was being ignored.
+
+* Unicode decoding of file uploads and file upload filenames were
+ causing errors when decoding non-file-upload fields (both fixes from
+ Ryan Barrett).
+
+0.8.5
+-----
+
+* Added response methods ``resp.encode_content()`` and
+ ``resp.decode_content()`` to gzip or ungzip content.
+
+* ``Response(status=404)`` now works (before you would have to use
+ ``status="404 Not Found"``).
+
+* Bugfix (typo) with reusing POST body.
+
+* Added ``226 IM Used`` response status.
+
+* Backport of ``string.Template`` included for Python 2.3
+ compatibility.
+
+0.8.4
+-----
+
+* ``__setattr__`` would keep ``Request`` subclasses from having
+ properly settable environ proxies (like ``req.path_info``).
+
+0.8.3
+-----
+
+* ``request.POST`` was giving FieldStorage objects for *every*
+ attribute, not just file uploads. This is fixed now.
+
+
+* Added request attributes ``req.server_name`` and ``req.server_port``
+ for the environ keys ``SERVER_NAME`` and ``SERVER_PORT``.
+
+* Avoid exceptions in ``req.content_length``, even if
+ ``environ['CONTENT_LENGTH']`` is somehow invalid.
+
+0.8.2
+-----
+
+* Python 2.3 compatibility: backport of ``reversed(seq)``
+
+* Made separate ``.exception`` attribute on ``webob.exc`` objects,
+ since new-style classes can't be raised as exceptions.
+
+* Deprecate ``req.postvars`` and ``req.queryvars``, instead using the
+ sole names ``req.GET`` and ``req.POST`` (also ``req.str_GET`` and
+ ``req.str_POST``). The old names give a warning; will give an error
+ in next release, and be completely gone in the following release.
+
+* ``req.user_agent`` is now just a simple string (parsing the
+ User-Agent header was just too volatile, and required too much
+ knowledge about current browsers). Similarly,
+ ``req.referer_search_query()`` is gone.
+
+* Added parameters ``version`` and ``comment`` to
+ ``Response.set_cookie()``, per William Dode's suggestion.
+
+* Was accidentally consuming file uploads, instead of putting the
+ ``FieldStorage`` object directly in the parameters.
+
+0.8.1
+-----
+
+* Added ``res.set_cookie(..., httponly=True)`` to set the ``HttpOnly``
+ attribute on the cookie, which keeps Javascript from reading the
+ cookie.
+
+* Added some WebDAV-related responses to ``webob.exc``
+
+* Set default ``Last-Modified`` when using ``response.cache_expire()``
+ (fixes issue with Opera)
+
+* Generally fix ``.cache_control``
+
+0.8
+---
+
+First release. Nothing is new, or everything is new, depending on how
+you think about it.
diff --git a/lib/webob_1_1_1/docs/pycon2011/pycon-py3k-sprint.txt b/lib/webob_1_1_1/docs/pycon2011/pycon-py3k-sprint.txt
new file mode 100644
index 0000000..d5e7182
--- /dev/null
+++ b/lib/webob_1_1_1/docs/pycon2011/pycon-py3k-sprint.txt
@@ -0,0 +1,75 @@
+Python 3 Sprint Outcomes
+========================
+
+We provided WebOb with 100% statement coverage at the 2011 PyCon Pyramid
+sprint in Atlanta GA.
+
+Participated:
+
+Alexandre Conrad, Patricio Paez, Whit Morriss, Rob Miller, Reed O'Brien,
+Chris Shenton, Joe Dallago, Tres Seaver, Casey Duncan, Kai Groner, Chris
+McDonough.
+
+In doing so, we added roughly 700-800 unit tests, and disused existing
+doctests as coverage (they are still runnable, but don't get run during
+``setup.py test``).
+
+We never did get around to actually doing any porting to Python 3. Adding
+comprehensive test coverage proved to be enough work to fill the sprint days.
+
+The bitbucket fork on which this work was done is at
+https://bitbucket.org/chrism/webob-py3k. I've made a tag in that repository
+named "sprint-coverage" which represents a reasonable place to pull from for
+integration into mainline.
+
+Testing Normally
+----------------
+
+ $ python2.x setup.py test
+
+Testing Coverage
+----------------
+
+ $ python2.X setup.py nosetests --with-coverage
+
+Testing Documentation
+---------------------
+
+Doctests don't run when you run "setup.py test" anymore. To run them
+manually, do:
+
+ $ cd webob
+ $ $MYVENV/bin/python setup.py develop
+ $ cd docs
+ $ $MYVENV/bin/python doctests.py
+
+Blamelist
+---------
+
+- webob.acceptparse (aconrad)
+
+- webob.byterange (ppaez)
+
+- webob.cachecontrol (whit)
+
+- webob.dec (rafrombrc)
+
+- webob.descriptors (reedobrien)
+
+- webob.etag (shentonfreude)
+
+- webob.multidict (joe)
+
+- webob.request (tseaver)
+
+- webob.response (caseman/mcdonc)
+
+- webob.exc (joe)
+
+Doctest-to-Unit Test Conversion
+-------------------------------
+
+- tests/test_request.txt (aconrad)
+
+- tests/test_response.txt (groner)
+
diff --git a/lib/webob_1_1_1/docs/pycon2011/request_table.rst b/lib/webob_1_1_1/docs/pycon2011/request_table.rst
new file mode 100644
index 0000000..927f031
--- /dev/null
+++ b/lib/webob_1_1_1/docs/pycon2011/request_table.rst
@@ -0,0 +1,145 @@
+==========================
+ Request Comparison Table
+==========================
+
+b=WebBob
+z=Werkzeug
+x=both
+
+
+WEBOB NAME write read WERKZEUG NAME NOTES
+================================= ===== ==== ================================= ===========================================
+
+Read-Write Properties Read-Write Properties
++++++++++++++++++++++ +++++++++++++++++++++
+
+content_type content_type CommonRequestDescriptorMixin
+charset charset "utf-8"
+headers headers cached_property
+urlvars
+urlargs
+host host cached_property
+body
+unicode_errors 'strict' encoding_errors 'ignore'
+decode_param_names F
+request_body_tempfile_limit 10*1024 max_content_length None Not sure if these are the same
+ is_behind_proxy F
+ max_form_memory_size None
+ parameter_storage_class ImmutableMultiDict
+ list_storage_class ImmutableList
+ dict_storage_class ImmutableTypeConversionDict
+environ environ
+ populate_request T
+ shallow F
+
+
+Environ Getter Properties
++++++++++++++++++++++++++
+
+body_file_raw
+scheme
+method method
+http_version
+script_name script_root cached_property
+path_info ???path cached_property
+content_length content_type CommonRequestDescriptorMixin
+remote_user remote_user
+remote_addr remote_addr
+query_string query_string
+server_name host (with port)
+server_port host (with name)
+uscript_name
+upath_info
+is_body_seekable
+authorization authorization cached_property
+pragma pragma cached_property
+date date CommonRequestDescriptorMixin
+max_forwards max_forwards CommonRequestDescriptorMixin
+range
+if_range
+referer/referrer referrer CommonRequestDescriptorMixin
+user_agent user_agent cached_property
+ input_stream
+ mimetype CommonRequestDescriptorMixin
+
+
+Read-Only Properties
+++++++++++++++++++++
+
+host_url host_url cached_property
+application_url base_url cached_property Not sure if same
+path_url ???path cached_property
+path ???path cached_property
+path_qs ???path cached_property
+url url cached_property
+is_xhr is_xhr
+str_POST
+POST
+str_GET
+GET
+str_params
+params
+str_cookies
+cookies cookies cached_property
+ url_charset
+ stream cached_property
+ args cached_property Maybe maps to params
+ data cached_property
+ form cached_property
+ values cached_property Maybe maps to params
+ files cached_property
+ url_root cached_property
+ access_route cached_property
+ is_secure
+ is_multithread
+ is_multiprocess
+ is_run_once
+
+
+Accept Properties
++++++++++++++++++
+
+accept accept_mimetypes
+accept_charset accept_charsets
+accept_encoding accept_encodings
+accept_language accept_languages
+
+Etag Properties
++++++++++++++++
+
+cache_control cache_control cached_property
+if_match if_match cached_property
+if_none_match if_none_match cached_property
+if_modified_since if_modified_since cached_property
+if_unmodified_since if_unmodified_since cached_property
+
+Methods
+++++++
+
+relative_url
+path_info_pop
+path_info_peek
+copy
+copy_get
+make_body_seekable
+copy_body
+make_tempfile
+remove_conditional_headers
+as_string (__str__)
+call_application
+get_response
+
+Classmethods
+++++++++++++
+
+from_string (classmethod)
+from_file
+blank
+ from_values
+ application
+
+Notes
+-----
+
+ <mitsuhiko> mcdonc: script_root and path in werkzeug are not quite script_name and path_info in webob
+[17:51] <mitsuhiko> the behavior regarding slashes is different for easier url joining
diff --git a/lib/webob_1_1_1/docs/pycon2011/response_table.rst b/lib/webob_1_1_1/docs/pycon2011/response_table.rst
new file mode 100644
index 0000000..8f98ab1
--- /dev/null
+++ b/lib/webob_1_1_1/docs/pycon2011/response_table.rst
@@ -0,0 +1,68 @@
+===========================
+ Response Comparison Table
+===========================
+
+b=WebBob
+z=Werkzeug
+x=both
+ =neither
+
+WEBOB NAME write read WERKZEUG NAME NOTES
+================================= ===== ==== ================================= ===========================================
+default_content_type x x default_mimetype wb default: "text/html", wz: "text/plain"
+default_charset b b wz uses class var default for charset
+charset x x charset
+unicode_errors b b
+default_conditional_response b b
+from_file() (classmethod) b b
+copy b b
+status (string) x x status
+status_int x x status_code
+ z default_status
+headers b b
+body b b
+unicode_body x x data
+body_file b File-like obj returned is writeable
+app_iter b x get_app_iter()
+ z iter_encoded()
+allow b x allow
+vary b x vary
+content_type x x content_type
+content_type_params x x mime_type_params
+ z z mime_type content_type str wo parameters
+content_length x x content_length
+content_encoding x x content_encoding
+content_language b x content_language
+content_location x x content_location
+content_md5 x x content_md5
+content_disposition b b
+accept_ranges b b
+content_range b b
+date x x date
+expires x x expires
+last_modified x x last_modified
+cache_control b z cache_control
+cache_expires (dwim) b b
+conditional_response (bool) b x make_conditional()
+etag b x add_etag()
+etag b x get_etag()
+etag b x set_etag()
+ z freeze()
+location x x location
+pragma b b
+age x x age
+retry_after x x retry_after
+server b b
+www_authenticate b z www_authenticate
+ x x date
+retry_after x x retry_after
+set_cookie() set_cookie()
+delete_cookie() delete_cookie()
+unset_cookie()
+ z is_streamed
+ z is_sequence
+body_file x stream
+ close()
+ get_wsgi_headers()
+ get_wsgi_response()
+__call__() __call__()
diff --git a/lib/webob_1_1_1/docs/reference.txt b/lib/webob_1_1_1/docs/reference.txt
new file mode 100644
index 0000000..f44d80e
--- /dev/null
+++ b/lib/webob_1_1_1/docs/reference.txt
@@ -0,0 +1,1016 @@
+WebOb Reference
++++++++++++++++
+
+.. contents::
+
+.. comment:
+
+ >>> from doctest import ELLIPSIS
+
+Introduction
+============
+
+This document covers all the details of the Request and Response
+objects. It is written to be testable with `doctest
+<http://python.org/doc/current/lib/module-doctest.html>`_ -- this
+effects the flavor of the documentation, perhaps to its detriment.
+But it also means you can feel confident that the documentation is
+correct.
+
+This is a somewhat different approach to reference documentation
+compared to the extracted documentation for the `request
+<class-webob.Request.html>`_ and `response
+<class-webob.Response.html>`_.
+
+Request
+=======
+
+The primary object in WebOb is ``webob.Request``, a wrapper around a
+`WSGI environment <http://www.python.org/dev/peps/pep-0333/>`_.
+
+The basic way you create a request object is simple enough:
+
+.. code-block:: python
+
+ >>> from webob import Request
+ >>> environ = {'wsgi.url_scheme': 'http', ...} #doctest: +SKIP
+ >>> req = Request(environ) #doctest: +SKIP
+
+(Note that the WSGI environment is a dictionary with a dozen required
+keys, so it's a bit lengthly to show a complete example of what it
+would look like -- usually your WSGI server will create it.)
+
+The request object *wraps* the environment; it has very little
+internal state of its own. Instead attributes you access read and
+write to the environment dictionary.
+
+You don't have to understand the details of WSGI to use this library;
+this library handles those details for you. You also don't have to
+use this exclusively of other libraries. If those other libraries
+also keep their state in the environment, multiple wrappers can
+coexist. Examples of libraries that can coexist include
+`paste.wsgiwrappers.Request
+<http://pythonpaste.org/class-paste.wsgiwrappers.WSGIRequest.html>`_
+(used by Pylons) and `yaro.Request
+<http://lukearno.com/projects/yaro/>`_.
+
+The WSGI environment has a number of required variables. To make it
+easier to test and play around with, the ``Request`` class has a
+constructor that will fill in a minimal environment:
+
+.. code-block:: python
+
+ >>> req = Request.blank('/article?id=1')
+ >>> from pprint import pprint
+ >>> pprint(req.environ)
+ {'HTTP_HOST': 'localhost:80',
+ 'PATH_INFO': '/article',
+ 'QUERY_STRING': 'id=1',
+ 'REQUEST_METHOD': 'GET',
+ 'SCRIPT_NAME': '',
+ 'SERVER_NAME': 'localhost',
+ 'SERVER_PORT': '80',
+ 'SERVER_PROTOCOL': 'HTTP/1.0',
+ 'wsgi.errors': <open file '<stderr>', mode 'w' at ...>,
+ 'wsgi.input': <...IO... object at ...>,
+ 'wsgi.multiprocess': False,
+ 'wsgi.multithread': False,
+ 'wsgi.run_once': False,
+ 'wsgi.url_scheme': 'http',
+ 'wsgi.version': (1, 0)}
+
+Request Body
+------------
+
+``req.body`` is a file-like object that gives the body of the request
+(e.g., a POST form, the body of a PUT, etc). It's kind of boring to
+start, but you can set it to a string and that will be turned into a
+file-like object. You can read the entire body with
+``req.body``.
+
+.. code-block:: python
+
+ >>> hasattr(req.body_file, 'read')
+ True
+ >>> req.body
+ ''
+ >>> req.method = 'PUT'
+ >>> req.body = 'test'
+ >>> hasattr(req.body_file, 'read')
+ True
+ >>> req.body
+ 'test'
+
+Method & URL
+------------
+
+All the normal parts of a request are also accessible through the
+request object:
+
+.. code-block:: python
+
+ >>> req.method
+ 'PUT'
+ >>> req.scheme
+ 'http'
+ >>> req.script_name # The base of the URL
+ ''
+ >>> req.script_name = '/blog' # make it more interesting
+ >>> req.path_info # The yet-to-be-consumed part of the URL
+ '/article'
+ >>> req.content_type # Content-Type of the request body
+ ''
+ >>> print req.remote_user # The authenticated user (there is none set)
+ None
+ >>> print req.remote_addr # The remote IP
+ None
+ >>> req.host
+ 'localhost:80'
+ >>> req.host_url
+ 'http://localhost'
+ >>> req.application_url
+ 'http://localhost/blog'
+ >>> req.path_url
+ 'http://localhost/blog/article'
+ >>> req.url
+ 'http://localhost/blog/article?id=1'
+ >>> req.path
+ '/blog/article'
+ >>> req.path_qs
+ '/blog/article?id=1'
+ >>> req.query_string
+ 'id=1'
+
+You can make new URLs:
+
+.. code-block:: python
+
+ >>> req.relative_url('archive')
+ 'http://localhost/blog/archive'
+
+For parsing the URLs, it is often useful to deal with just the next
+path segment on PATH_INFO:
+
+.. code-block:: python
+
+ >>> req.path_info_peek() # Doesn't change request
+ 'article'
+ >>> req.path_info_pop() # Does change request!
+ 'article'
+ >>> req.script_name
+ '/blog/article'
+ >>> req.path_info
+ ''
+
+Headers
+-------
+
+All request headers are available through a dictionary-like object
+``req.headers``. Keys are case-insensitive.
+
+.. code-block:: python
+
+ >>> req.headers['Content-Type'] = 'application/x-www-urlencoded'
+ >>> sorted(req.headers.items())
+ [('Content-Length', '4'), ('Content-Type', 'application/x-www-urlencoded'), ('Host', 'localhost:80')]
+ >>> req.environ['CONTENT_TYPE']
+ 'application/x-www-urlencoded'
+
+Query & POST variables
+----------------------
+
+Requests can have variables in one of two locations: the query string
+(``?id=1``), or in the body of the request (generally a POST form).
+Note that even POST requests can have a query string, so both kinds of
+variables can exist at the same time. Also, a variable can show up
+more than once, as in ``?check=a&check=b``.
+
+For these variables WebOb uses a `MultiDict
+<class-webob.multidict.MultiDict.html>`_, which is basically a
+dictionary wrapper on a list of key/value pairs. It looks like a
+single-valued dictionary, but you can access all the values of a key
+with ``.getall(key)`` (which always returns a list, possibly an empty
+list). You also get all key/value pairs when using ``.items()`` and
+all values with ``.values()``.
+
+Some examples:
+
+.. code-block:: python
+
+ >>> req = Request.blank('/test?check=a&check=b&name=Bob')
+ >>> req.str_GET
+ GET([('check', 'a'), ('check', 'b'), ('name', 'Bob')])
+ >>> req.str_GET['check']
+ 'b'
+ >>> req.str_GET.getall('check')
+ ['a', 'b']
+ >>> req.str_GET.items()
+ [('check', 'a'), ('check', 'b'), ('name', 'Bob')]
+
+We'll have to create a request body and change the method to get
+POST. Until we do that, the variables are boring:
+
+.. code-block:: python
+
+ >>> req.str_POST
+ <NoVars: Not a form request>
+ >>> req.str_POST.items() # NoVars can be read like a dict, but not written
+ []
+ >>> req.method = 'POST'
+ >>> req.body = 'name=Joe&email=joe@example.com'
+ >>> req.str_POST
+ MultiDict([('name', 'Joe'), ('email', 'joe@example.com')])
+ >>> req.str_POST['name']
+ 'Joe'
+
+Often you won't care where the variables come from. (Even if you care
+about the method, the location of the variables might not be
+important.) There is a dictionary called ``req.params`` that
+contains variables from both sources:
+
+.. code-block:: python
+
+ >>> req.str_params
+ NestedMultiDict([('check', 'a'), ('check', 'b'), ('name', 'Bob'), ('name', 'Joe'), ('email', 'joe@example.com')])
+ >>> req.str_params['name']
+ 'Bob'
+ >>> req.str_params.getall('name')
+ ['Bob', 'Joe']
+ >>> for name, value in req.str_params.items():
+ ... print '%s: %r' % (name, value)
+ check: 'a'
+ check: 'b'
+ name: 'Bob'
+ name: 'Joe'
+ email: 'joe@example.com'
+
+The ``POST`` and ``GET`` nomenclature is historical -- ``req.GET`` can
+be used for non-GET requests to access query parameters, and
+``req.POST`` can also be used for PUT requests with the appropriate
+Content-Type.
+
+ >>> req = Request.blank('/test?check=a&check=b&name=Bob')
+ >>> req.method = 'PUT'
+ >>> req.body = body = 'var1=value1&var2=value2&rep=1&rep=2'
+ >>> req.environ['CONTENT_LENGTH'] = str(len(req.body))
+ >>> req.environ['CONTENT_TYPE'] = 'application/x-www-form-urlencoded'
+ >>> req.str_GET
+ GET([('check', 'a'), ('check', 'b'), ('name', 'Bob')])
+ >>> req.str_POST
+ MultiDict([('var1', 'value1'), ('var2', 'value2'), ('rep', '1'), ('rep', '2')])
+
+Unicode Variables
+~~~~~~~~~~~~~~~~~
+
+Submissions are non-unicode (``str``) strings, unless some character
+set is indicated. A client can indicate the character set with
+``Content-Type: application/x-www-form-urlencoded; charset=utf8``, but
+very few clients actually do this (sometimes XMLHttpRequest requests
+will do this, as JSON is always UTF8 even when a page is served with a
+different character set). You can force a charset, which will effect
+all the variables:
+
+.. code-block:: python
+
+ >>> req.charset = 'utf8'
+ >>> req.GET
+ UnicodeMultiDict([(u'check', u'a'), (u'check', u'b'), (u'name', u'Bob')])
+
+If you always want ``str`` values, you can use ``req.str_GET``
+and ``str_POST``.
+
+Cookies
+-------
+
+Cookies are presented in a simple dictionary. Like other variables,
+they will be decoded into Unicode strings if you set the charset.
+
+.. code-block:: python
+
+ >>> req.headers['Cookie'] = 'test=value'
+ >>> req.cookies
+ UnicodeMultiDict([(u'test', u'value')])
+ >>> req.charset = None
+ >>> req.str_cookies
+ {'test': 'value'}
+
+Modifying the request
+---------------------
+
+The headers are all modifiable, as are other environmental variables
+(like ``req.remote_user``, which maps to
+``request.environ['REMOTE_USER']``).
+
+If you want to copy the request you can use ``req.copy()``; this
+copies the ``environ`` dictionary, and the request body from
+``environ['wsgi.input']``.
+
+The method ``req.remove_conditional_headers(remove_encoding=True)``
+can be used to remove headers that might result in a ``304 Not
+Modified`` response. If you are writing some intermediary it can be
+useful to avoid these headers. Also if ``remove_encoding`` is true
+(the default) then any ``Accept-Encoding`` header will be removed,
+which can result in gzipped responses.
+
+Header Getters
+--------------
+
+In addition to ``req.headers``, there are attributes for most of the
+request headers defined by the HTTP 1.1 specification. These
+attributes often return parsed forms of the headers.
+
+Accept-* headers
+~~~~~~~~~~~~~~~~
+
+There are several request headers that tell the server what the client
+accepts. These are ``accept`` (the Content-Type that is accepted),
+``accept_charset`` (the charset accepted), ``accept_encoding``
+(the Content-Encoding, like gzip, that is accepted), and
+``accept_language`` (generally the preferred language of the client).
+
+The objects returned support containment to test for acceptability.
+E.g.:
+
+.. code-block:: python
+
+ >>> 'text/html' in req.accept
+ True
+
+Because no header means anything is potentially acceptable, this is
+returning True. We can set it to see more interesting behavior (the
+example means that ``text/html`` is okay, but
+``application/xhtml+xml`` is preferred):
+
+.. code-block:: python
+
+ >>> req.accept = 'text/html;q=0.5, application/xhtml+xml;q=1'
+ >>> req.accept
+ <MIMEAccept('text/html;q=0.5, application/xhtml+xml')>
+ >>> 'text/html' in req.accept
+ True
+
+There's three methods for different strategies of finding a match.
+First, when you trust the server's preference over the client (a good
+idea for Accept):
+
+.. code-block:: python
+
+ >>> req.accept.first_match(['text/html', 'application/xhtml+xml'])
+ 'text/html'
+
+Because ``text/html`` is at least *somewhat* acceptible, it is
+returned, even if the client says it prefers
+``application/xhtml+xml``. If we trust the client more:
+
+.. code-block:: python
+
+ >>> req.accept.best_match(['text/html', 'application/xhtml+xml'])
+ 'application/xhtml+xml'
+
+If we just want to know everything the client prefers, in the order it
+is preferred:
+
+.. code-block:: python
+
+ >>> req.accept.best_matches()
+ ['application/xhtml+xml', 'text/html']
+
+For languages you'll often have a "fallback" language. E.g., if there's
+nothing better then use ``en-US`` (and if ``en-US`` is okay, ignore
+any less preferrable languages):
+
+.. code-block:: python
+
+ >>> req.accept_language = 'es, pt-BR'
+ >>> req.accept_language.best_matches('en-US')
+ ['es', 'pt-BR', 'en-US']
+ >>> req.accept_language.best_matches('es')
+ ['es']
+
+Conditional Requests
+~~~~~~~~~~~~~~~~~~~~
+
+There a number of ways to make a conditional request. A conditional
+request is made when the client has a document, but it is not sure if
+the document is up to date. If it is not, it wants a new version. If
+the document is up to date then it doesn't want to waste the
+bandwidth, and expects a ``304 Not Modified`` response.
+
+ETags are generally the best technique for these kinds of requests;
+this is an opaque string that indicates the identity of the object.
+For instance, it's common to use the mtime (last modified) of the file,
+plus the number of bytes, and maybe a hash of the filename (if there's
+a possibility that the same URL could point to two different
+server-side filenames based on other variables). To test if a 304
+response is appropriate, you can use:
+
+.. code-block:: python
+
+ >>> server_token = 'opaque-token'
+ >>> server_token in req.if_none_match # You shouldn't return 304
+ False
+ >>> req.if_none_match = server_token
+ >>> req.if_none_match
+ <ETag opaque-token>
+ >>> server_token in req.if_none_match # You *should* return 304
+ True
+
+For date-based comparisons If-Modified-Since is used:
+
+.. code-block:: python
+
+ >>> from webob import UTC
+ >>> from datetime import datetime
+ >>> req.if_modified_since = datetime(2006, 1, 1, 12, 0, tzinfo=UTC)
+ >>> req.headers['If-Modified-Since']
+ 'Sun, 01 Jan 2006 12:00:00 GMT'
+ >>> server_modified = datetime(2005, 1, 1, 12, 0, tzinfo=UTC)
+ >>> req.if_modified_since and req.if_modified_since >= server_modified
+ True
+
+For range requests there are two important headers, If-Range (which is
+form of conditional request) and Range (which requests a range). If
+the If-Range header fails to match then the full response (not a
+range) should be returned:
+
+.. code-block:: python
+
+ >>> req.if_range
+ <Empty If-Range>
+ >>> req.if_range.match(etag='some-etag', last_modified=datetime(2005, 1, 1, 12, 0))
+ True
+ >>> req.if_range = 'opaque-etag'
+ >>> req.if_range.match(etag='other-etag')
+ False
+ >>> req.if_range.match(etag='opaque-etag')
+ True
+
+You can also pass in a response object with:
+
+.. code-block:: python
+
+ >>> from webob import Response
+ >>> res = Response(etag='opaque-etag')
+ >>> req.if_range.match_response(res)
+ True
+
+To get the range information:
+
+ >>> req.range = 'bytes=0-100'
+ >>> req.range
+ <Range ranges=(0, 101)>
+ >>> cr = req.range.content_range(length=1000)
+ >>> cr.start, cr.stop, cr.length
+ (0, 101, 1000)
+
+Note that the range headers use *inclusive* ranges (the last byte
+indexed is included), where Python always uses a range where the last
+index is excluded from the range. The ``.stop`` index is in the
+Python form.
+
+Another kind of conditional request is a request (typically PUT) that
+includes If-Match or If-Unmodified-Since. In this case you are saying
+"here is an update to a resource, but don't apply it if someone else
+has done something since I last got the resource". If-Match means "do
+this if the current ETag matches the ETag I'm giving".
+If-Unmodified-Since means "do this if the resource has remained
+unchanged".
+
+.. code-block:: python
+
+ >>> server_token in req.if_match # No If-Match means everything is ok
+ True
+ >>> req.if_match = server_token
+ >>> server_token in req.if_match # Still OK
+ True
+ >>> req.if_match = 'other-token'
+ >>> # Not OK, should return 412 Precondition Failed:
+ >>> server_token in req.if_match
+ False
+
+For more on this kind of conditional request, see `Detecting the Lost
+Update Problem Using Unreserved Checkout
+<http://www.w3.org/1999/04/Editing/>`_.
+
+Calling WSGI Applications
+-------------------------
+
+The request object can be used to make handy subrequests or test
+requests against WSGI applications. If you want to make subrequests,
+you should copy the request (with ``req.copy()``) before sending it to
+multiple applications, since applications might modify the request
+when they are run.
+
+There's two forms of the subrequest. The more primitive form is
+this:
+
+.. code-block:: python
+
+ >>> req = Request.blank('/')
+ >>> def wsgi_app(environ, start_response):
+ ... start_response('200 OK', [('Content-type', 'text/plain')])
+ ... return ['Hi!']
+ >>> req.call_application(wsgi_app)
+ ('200 OK', [('Content-type', 'text/plain')], ['Hi!'])
+
+Note it returns ``(status_string, header_list, app_iter)``. If
+``app_iter.close()`` exists, it is your responsibility to call it.
+
+A handier response can be had with:
+
+.. code-block:: python
+
+ >>> res = req.get_response(wsgi_app)
+ >>> res
+ <Response ... 200 OK>
+ >>> res.status
+ '200 OK'
+ >>> res.headers
+ ResponseHeaders([('Content-type', 'text/plain')])
+ >>> res.body
+ 'Hi!'
+
+You can learn more about this response object in the Response_ section.
+
+Ad-Hoc Attributes
+-----------------
+
+You can assign attributes to your request objects. They will all go
+in ``environ['webob.adhoc_attrs']`` (a dictionary).
+
+.. code-block:: python
+
+ >>> req = Request.blank('/')
+ >>> req.some_attr = 'blah blah blah'
+ >>> new_req = Request(req.environ)
+ >>> new_req.some_attr
+ 'blah blah blah'
+ >>> req.environ['webob.adhoc_attrs']
+ {'some_attr': 'blah blah blah'}
+
+Response
+========
+
+The ``webob.Response`` object contains everything necessary to make a
+WSGI response. Instances of it are in fact WSGI applications, but it
+can also represent the result of calling a WSGI application (as noted
+in `Calling WSGI Applications`_). It can also be a way of
+accumulating a response in your WSGI application.
+
+A WSGI response is made up of a status (like ``200 OK``), a list of
+headers, and a body (or iterator that will produce a body).
+
+Core Attributes
+---------------
+
+The core attributes are unsurprising:
+
+.. code-block:: python
+
+ >>> from webob import Response
+ >>> res = Response()
+ >>> res.status
+ '200 OK'
+ >>> res.headerlist
+ [('Content-Type', 'text/html; charset=UTF-8'), ('Content-Length', '0')]
+ >>> res.body
+ ''
+
+You can set any of these attributes, e.g.:
+
+.. code-block:: python
+
+ >>> res.status = 404
+ >>> res.status
+ '404 Not Found'
+ >>> res.status_int
+ 404
+ >>> res.headerlist = [('Content-type', 'text/html')]
+ >>> res.body = 'test'
+ >>> print res
+ 404 Not Found
+ Content-type: text/html
+ Content-Length: 4
+ <BLANKLINE>
+ test
+ >>> res.body = u"test"
+ Traceback (most recent call last):
+ ...
+ TypeError: You cannot set Response.body to a unicode object (use Response.text)
+ >>> res.text = u"test"
+ Traceback (most recent call last):
+ ...
+ AttributeError: You cannot access Response.text unless charset is set
+ >>> res.charset = 'utf8'
+ >>> res.text = u"test"
+ >>> res.body
+ 'test'
+
+You can set any attribute with the constructor, like
+``Response(charset='utf8')``
+
+Headers
+-------
+
+In addition to ``res.headerlist``, there is dictionary-like view on
+the list in ``res.headers``:
+
+.. code-block:: python
+
+ >>> res.headers
+ ResponseHeaders([('Content-Type', 'text/html; charset=utf8'), ('Content-Length', '4')])
+
+This is case-insensitive. It can support multiple values for a key,
+though only if you use ``res.headers.add(key, value)`` or read them
+with ``res.headers.getall(key)``.
+
+Body & app_iter
+---------------
+
+The ``res.body`` attribute represents the entire body of the request
+as a single string (not unicode, though you can set it to unicode if
+you have a charset defined). There is also a ``res.app_iter``
+attribute that reprsents the body as an iterator. WSGI applications
+return these ``app_iter`` iterators instead of strings, and sometimes
+it can be problematic to load the entire iterator at once (for
+instance, if it returns the contents of a very large file). Generally
+it is not a problem, and often the iterator is something simple like a
+one-item list containing a string with the entire body.
+
+If you set the body then Content-Length will also be set, and an
+``res.app_iter`` will be created for you. If you set ``res.app_iter``
+then Content-Length will be cleared, but it won't be set for you.
+
+There is also a file-like object you can access, which will update the
+app_iter in-place (turning the app_iter into a list if necessary):
+
+.. code-block:: python
+
+ >>> res = Response(content_type='text/plain', charset=None)
+ >>> f = res.body_file
+ >>> f.write('hey')
+ >>> f.write(u'test')
+ Traceback (most recent call last):
+ . . .
+ TypeError: You can only write unicode to Response if charset has been set
+ >>> f.encoding
+ >>> res.charset = 'utf8'
+ >>> f.encoding
+ 'utf8'
+ >>> f.write(u'test')
+ >>> res.app_iter
+ ['', 'hey', 'test']
+ >>> res.body
+ 'heytest'
+
+Header Getters
+--------------
+
+Like Request, HTTP response headers are also available as individual
+properties. These represent parsed forms of the headers.
+
+Content-Type is a special case, as the type and the charset are
+handled through two separate properties:
+
+.. code-block:: python
+
+ >>> res = Response()
+ >>> res.content_type = 'text/html'
+ >>> res.charset = 'utf8'
+ >>> res.content_type
+ 'text/html'
+ >>> res.headers['content-type']
+ 'text/html; charset=utf8'
+ >>> res.content_type = 'application/atom+xml'
+ >>> res.content_type_params
+ {'charset': 'utf8'}
+ >>> res.content_type_params = {'type': 'entry', 'charset': 'utf8'}
+ >>> res.headers['content-type']
+ 'application/atom+xml; charset=utf8; type=entry'
+
+Other headers:
+
+.. code-block:: python
+
+ >>> # Used with a redirect:
+ >>> res.location = 'http://localhost/foo'
+
+ >>> # Indicates that the server accepts Range requests:
+ >>> res.accept_ranges = 'bytes'
+
+ >>> # Used by caching proxies to tell the client how old the
+ >>> # response is:
+ >>> res.age = 120
+
+ >>> # Show what methods the client can do; typically used in
+ >>> # a 405 Method Not Allowed response:
+ >>> res.allow = ['GET', 'PUT']
+
+ >>> # Set the cache-control header:
+ >>> res.cache_control.max_age = 360
+ >>> res.cache_control.no_transform = True
+
+ >>> # Tell the browser to treat the response as an attachment:
+ >>> res.content_disposition = 'attachment; filename=foo.xml'
+
+ >>> # Used if you had gzipped the body:
+ >>> res.content_encoding = 'gzip'
+
+ >>> # What language(s) are in the content:
+ >>> res.content_language = ['en']
+
+ >>> # Seldom used header that tells the client where the content
+ >>> # is from:
+ >>> res.content_location = 'http://localhost/foo'
+
+ >>> # Seldom used header that gives a hash of the body:
+ >>> res.content_md5 = 'big-hash'
+
+ >>> # Means we are serving bytes 0-500 inclusive, out of 1000 bytes total:
+ >>> # you can also use the range setter shown earlier
+ >>> res.content_range = (0, 501, 1000)
+
+ >>> # The length of the content; set automatically if you set
+ >>> # res.body:
+ >>> res.content_length = 4
+
+ >>> # Used to indicate the current date as the server understands
+ >>> # it:
+ >>> res.date = datetime.now()
+
+ >>> # The etag:
+ >>> res.etag = 'opaque-token'
+ >>> # You can generate it from the body too:
+ >>> res.md5_etag()
+ >>> res.etag
+ '1B2M2Y8AsgTpgAmY7PhCfg'
+
+ >>> # When this page should expire from a cache (Cache-Control
+ >>> # often works better):
+ >>> import time
+ >>> res.expires = time.time() + 60*60 # 1 hour
+
+ >>> # When this was last modified, of course:
+ >>> res.last_modified = datetime(2007, 1, 1, 12, 0, tzinfo=UTC)
+
+ >>> # Used with 503 Service Unavailable to hint the client when to
+ >>> # try again:
+ >>> res.retry_after = 160
+
+ >>> # Indicate the server software:
+ >>> res.server = 'WebOb/1.0'
+
+ >>> # Give a list of headers that the cache should vary on:
+ >>> res.vary = ['Cookie']
+
+Note in each case you can general set the header to a string to avoid
+any parsing, and set it to None to remove the header (or do something
+like ``del res.vary``).
+
+In the case of date-related headers you can set the value to a
+``datetime`` instance (ideally with a UTC timezone), a time tuple, an
+integer timestamp, or a properly-formatted string.
+
+After setting all these headers, here's the result:
+
+.. code-block:: python
+
+ >>> for name, value in res.headerlist:
+ ... print '%s: %s' % (name, value)
+ Content-Type: application/atom+xml; charset=utf8; type=entry
+ Location: http://localhost/foo
+ Accept-Ranges: bytes
+ Age: 120
+ Allow: GET, PUT
+ Cache-Control: max-age=360, no-transform
+ Content-Disposition: attachment; filename=foo.xml
+ Content-Encoding: gzip
+ Content-Language: en
+ Content-Location: http://localhost/foo
+ Content-MD5: big-hash
+ Content-Range: bytes 0-500/1000
+ Content-Length: 4
+ Date: ... GMT
+ ETag: ...
+ Expires: ... GMT
+ Last-Modified: Mon, 01 Jan 2007 12:00:00 GMT
+ Retry-After: 160
+ Server: WebOb/1.0
+ Vary: Cookie
+
+You can also set Cache-Control related attributes with
+``req.cache_expires(seconds, **attrs)``, like:
+
+.. code-block:: python
+
+ >>> res = Response()
+ >>> res.cache_expires(10)
+ >>> res.headers['Cache-Control']
+ 'max-age=10'
+ >>> res.cache_expires(0)
+ >>> res.headers['Cache-Control']
+ 'max-age=0, must-revalidate, no-cache, no-store'
+ >>> res.headers['Expires']
+ '... GMT'
+
+You can also use the `timedelta
+<http://python.org/doc/current/lib/datetime-timedelta.html>`_
+constants defined, e.g.:
+
+.. code-block:: python
+
+ >>> from webob import *
+ >>> res = Response()
+ >>> res.cache_expires(2*day+4*hour)
+ >>> res.headers['Cache-Control']
+ 'max-age=187200'
+
+Cookies
+-------
+
+Cookies (and the Set-Cookie header) are handled with a couple
+methods. Most importantly:
+
+.. code-block:: python
+
+ >>> res.set_cookie('key', 'value', max_age=360, path='/',
+ ... domain='example.org', secure=True)
+ >>> res.headers['Set-Cookie']
+ 'key=value; Domain=example.org; Max-Age=360; Path=/; expires=... GMT; secure'
+ >>> # To delete a cookie previously set in the client:
+ >>> res.delete_cookie('bad_cookie')
+ >>> res.headers['Set-Cookie']
+ 'bad_cookie=; Max-Age=0; Path=/; expires=... GMT'
+
+The only other real method of note (note that this does *not* delete
+the cookie from clients, only from the response object):
+
+.. code-block:: python
+
+ >>> res.unset_cookie('key')
+ >>> res.unset_cookie('bad_cookie')
+ >>> print res.headers.get('Set-Cookie')
+ None
+
+Binding a Request
+-----------------
+
+You can bind a request (or request WSGI environ) to the response
+object. This is available through ``res.request`` or
+``res.environ``. This is currently only used in setting
+``res.location``, to make the location absolute if necessary.
+
+Response as a WSGI application
+------------------------------
+
+A response is a WSGI application, in that you can do:
+
+.. code-block:: python
+
+ >>> req = Request.blank('/')
+ >>> status, headers, app_iter = req.call_application(res)
+
+A possible pattern for your application might be:
+
+.. code-block:: python
+
+ >>> def my_app(environ, start_response):
+ ... req = Request(environ)
+ ... res = Response()
+ ... res.content_type = 'text/plain'
+ ... parts = []
+ ... for name, value in sorted(req.environ.items()):
+ ... parts.append('%s: %r' % (name, value))
+ ... res.body = '\n'.join(parts)
+ ... return res(environ, start_response)
+ >>> req = Request.blank('/')
+ >>> res = req.get_response(my_app)
+ >>> print res
+ 200 OK
+ Content-Type: text/plain; charset=UTF-8
+ Content-Length: ...
+ <BLANKLINE>
+ HTTP_HOST: 'localhost:80'
+ PATH_INFO: '/'
+ QUERY_STRING: ''
+ REQUEST_METHOD: 'GET'
+ SCRIPT_NAME: ''
+ SERVER_NAME: 'localhost'
+ SERVER_PORT: '80'
+ SERVER_PROTOCOL: 'HTTP/1.0'
+ wsgi.errors: <open file '<stderr>', mode 'w' at ...>
+ wsgi.input: <...IO... object at ...>
+ wsgi.multiprocess: False
+ wsgi.multithread: False
+ wsgi.run_once: False
+ wsgi.url_scheme: 'http'
+ wsgi.version: (1, 0)
+
+Exceptions
+==========
+
+In addition to Request and Response objects, there are a set of Python
+exceptions for different HTTP responses (3xx, 4xx, 5xx codes).
+
+These provide a simple way to provide these non-200 response. A very
+simple body is provided.
+
+.. code-block:: python
+
+ >>> from webob.exc import *
+ >>> exc = HTTPTemporaryRedirect(location='foo')
+ >>> req = Request.blank('/path/to/something')
+ >>> print str(req.get_response(exc)).strip()
+ 307 Temporary Redirect
+ Location: http://localhost/path/to/foo
+ Content-Length: 126
+ Content-Type: text/plain; charset=UTF-8
+ <BLANKLINE>
+ 307 Temporary Redirect
+ <BLANKLINE>
+ The resource has been moved to http://localhost/path/to/foo; you should be redirected automatically.
+
+Note that only if there's an ``Accept: text/html`` header in the
+request will an HTML response be given:
+
+.. code-block:: python
+
+ >>> req.accept += 'text/html'
+ >>> print str(req.get_response(exc)).strip()
+ 307 Temporary Redirect
+ Location: http://localhost/path/to/foo
+ Content-Length: 270
+ Content-Type: text/html; charset=UTF-8
+ <BLANKLINE>
+ <html>
+ <head>
+ <title>307 Temporary Redirect</title>
+ </head>
+ <body>
+ <h1>307 Temporary Redirect</h1>
+ The resource has been moved to <a href="http://localhost/path/to/foo">http://localhost/path/to/foo</a>;
+ you should be redirected automatically.
+ <BLANKLINE>
+ <BLANKLINE>
+ </body>
+ </html>
+
+
+This is taken from `paste.httpexceptions
+<http://pythonpaste.org/module-paste.httpexceptions.html>`_, and if
+you have Paste installed then these exceptions will be subclasses of
+the Paste exceptions.
+
+
+Conditional WSGI Application
+----------------------------
+
+The Response object can handle your conditional responses for you,
+checking If-None-Match, If-Modified-Since, and Range/If-Range.
+
+To enable this you must create the response like
+``Response(conditional_request=True)``, or make a subclass like:
+
+.. code-block:: python
+
+ >>> class AppResponse(Response):
+ ... default_content_type = 'text/html'
+ ... default_conditional_response = True
+ >>> res = AppResponse(body='0123456789',
+ ... last_modified=datetime(2005, 1, 1, 12, 0, tzinfo=UTC))
+ >>> req = Request.blank('/')
+ >>> req.if_modified_since = datetime(2006, 1, 1, 12, 0, tzinfo=UTC)
+ >>> req.get_response(res)
+ <Response ... 304 Not Modified>
+ >>> del req.if_modified_since
+ >>> res.etag = 'opaque-tag'
+ >>> req.if_none_match = 'opaque-tag'
+ >>> req.get_response(res)
+ <Response ... 304 Not Modified>
+
+ >>> req.if_none_match = '*'
+ >>> 'x' in req.if_none_match
+ True
+ >>> req.if_none_match = req.if_none_match
+ >>> 'x' in req.if_none_match
+ True
+ >>> req.if_none_match = None
+ >>> 'x' in req.if_none_match
+ False
+ >>> req.if_match = None
+ >>> 'x' in req.if_match
+ True
+ >>> req.if_match = req.if_match
+ >>> 'x' in req.if_match
+ True
+ >>> req.headers.get('If-Match')
+ '*'
+
+ >>> del req.if_none_match
+
+ >>> req.range = (1, 5)
+ >>> result = req.get_response(res)
+ >>> result.headers['content-range']
+ 'bytes 1-4/10'
+ >>> result.body
+ '1234'
diff --git a/lib/webob_1_1_1/docs/test-file.txt b/lib/webob_1_1_1/docs/test-file.txt
new file mode 100644
index 0000000..63ca1ff
--- /dev/null
+++ b/lib/webob_1_1_1/docs/test-file.txt
@@ -0,0 +1 @@
+This is a test. Hello test people!
\ No newline at end of file
diff --git a/lib/webob_1_1_1/docs/test_dec.txt b/lib/webob_1_1_1/docs/test_dec.txt
new file mode 100644
index 0000000..df0461c
--- /dev/null
+++ b/lib/webob_1_1_1/docs/test_dec.txt
@@ -0,0 +1,103 @@
+A test of the decorator module::
+
+ >>> from doctest import ELLIPSIS
+ >>> from webob.dec import wsgify
+ >>> from webob import Response, Request
+ >>> from webob import exc
+ >>> @wsgify
+ ... def test_app(req):
+ ... return 'hey, this is a test: %s' % req.url
+ >>> def testit(app, req):
+ ... if isinstance(req, basestring):
+ ... req = Request.blank(req)
+ ... resp = req.get_response(app)
+ ... print resp
+ >>> testit(test_app, '/a url')
+ 200 OK
+ Content-Type: text/html; charset=UTF-8
+ Content-Length: 45
+ <BLANKLINE>
+ hey, this is a test: http://localhost/a%20url
+ >>> test_app
+ wsgify(test_app)
+
+Now some middleware testing::
+
+ >>> @wsgify.middleware
+ ... def set_urlvar(req, app, **vars):
+ ... req.urlvars.update(vars)
+ ... return app(req)
+ >>> @wsgify
+ ... def show_vars(req):
+ ... return 'These are the vars: %r' % (sorted(req.urlvars.items()))
+ >>> show_vars2 = set_urlvar(show_vars, a=1, b=2)
+ >>> show_vars2
+ wsgify.middleware(set_urlvar)(wsgify(show_vars), a=1, b=2)
+ >>> testit(show_vars2, '/path')
+ 200 OK
+ Content-Type: text/html; charset=UTF-8
+ Content-Length: 40
+ <BLANKLINE>
+ These are the vars: [('a', 1), ('b', 2)]
+
+Some examples from Sergey::
+
+ >>> class HostMap(dict):
+ ... @wsgify
+ ... def __call__(self, req):
+ ... return self[req.host.split(':')[0]]
+ >>> app = HostMap()
+ >>> app['example.com'] = Response('1')
+ >>> app['other.com'] = Response('2')
+ >>> print Request.blank('http://example.com/').get_response(wsgify(app))
+ 200 OK
+ Content-Type: text/html; charset=UTF-8
+ Content-Length: 1
+ <BLANKLINE>
+ 1
+
+ >>> @wsgify.middleware
+ ... def override_https(req, normal_app, secure_app):
+ ... if req.scheme == 'https':
+ ... return secure_app
+ ... else:
+ ... return normal_app
+ >>> app = override_https(Response('http'), secure_app=Response('https'))
+ >>> print Request.blank('http://x.com/').get_response(app)
+ 200 OK
+ Content-Type: text/html; charset=UTF-8
+ Content-Length: 4
+ <BLANKLINE>
+ http
+
+A status checking middleware::
+
+ >>> @wsgify.middleware
+ ... def catch(req, app, catchers):
+ ... resp = req.get_response(app)
+ ... return catchers.get(resp.status_int, resp)
+ >>> @wsgify
+ ... def simple(req):
+ ... return other_app # Just to mess around
+ >>> @wsgify
+ ... def other_app(req):
+ ... return Response('hey', status_int=int(req.path_info.strip('/')))
+ >>> app = catch(simple, catchers={500: Response('error!'), 404: Response('nothing')})
+ >>> print Request.blank('/200').get_response(app)
+ 200 OK
+ Content-Type: text/html; charset=UTF-8
+ Content-Length: 3
+ <BLANKLINE>
+ hey
+ >>> print Request.blank('/500').get_response(app)
+ 200 OK
+ Content-Type: text/html; charset=UTF-8
+ Content-Length: 6
+ <BLANKLINE>
+ error!
+ >>> print Request.blank('/404').get_response(app)
+ 200 OK
+ Content-Type: text/html; charset=UTF-8
+ Content-Length: 7
+ <BLANKLINE>
+ nothing
diff --git a/lib/webob_1_1_1/docs/test_request.txt b/lib/webob_1_1_1/docs/test_request.txt
new file mode 100644
index 0000000..f708e40
--- /dev/null
+++ b/lib/webob_1_1_1/docs/test_request.txt
@@ -0,0 +1,549 @@
+This demonstrates how the Request object works, and tests it.
+
+You can instantiate a request using ``Request.blank()``, to create a
+fresh environment dictionary with all the basic keys such a dictionary
+should have.
+
+ >>> import sys
+ >>> if sys.version >= '2.7':
+ ... from io import BytesIO as InputType
+ ... else:
+ ... from cStringIO import InputType
+ >>> from doctest import ELLIPSIS, NORMALIZE_WHITESPACE
+ >>> from webob import Request, UTC
+ >>> req = Request.blank('/')
+ >>> req # doctest: +ELLIPSIS
+ <Request at ... GET http://localhost/>
+ >>> print req
+ GET / HTTP/1.0
+ Host: localhost:80
+ >>> req.environ # doctest: +ELLIPSIS
+ {...}
+ >>> isinstance(req.body_file, InputType)
+ True
+ >>> req.scheme
+ 'http'
+ >>> req.method
+ 'GET'
+ >>> req.script_name
+ ''
+ >>> req.path_info
+ '/'
+ >>> req.upath_info
+ u'/'
+ >>> req.content_type
+ ''
+ >>> print req.remote_user
+ None
+ >>> req.host_url
+ 'http://localhost'
+ >>> req.script_name = '/foo'
+ >>> req.path_info = '/bar/'
+ >>> req.environ['QUERY_STRING'] = 'a=b'
+ >>> req.application_url
+ 'http://localhost/foo'
+ >>> req.path_url
+ 'http://localhost/foo/bar/'
+ >>> req.url
+ 'http://localhost/foo/bar/?a=b'
+ >>> req.relative_url('baz')
+ 'http://localhost/foo/bar/baz'
+ >>> req.relative_url('baz', to_application=True)
+ 'http://localhost/foo/baz'
+ >>> req.relative_url('http://example.org')
+ 'http://example.org'
+ >>> req.path_info_peek()
+ 'bar'
+ >>> req.path_info_pop()
+ 'bar'
+ >>> req.script_name, req.path_info
+ ('/foo/bar', '/')
+ >>> print req.environ.get('wsgiorg.routing_args')
+ None
+ >>> req.urlvars
+ {}
+ >>> req.environ['wsgiorg.routing_args']
+ ((), {})
+ >>> req.urlvars = dict(x='y')
+ >>> req.environ['wsgiorg.routing_args']
+ ((), {'x': 'y'})
+ >>> req.urlargs
+ ()
+ >>> req.urlargs = (1, 2, 3)
+ >>> req.environ['wsgiorg.routing_args']
+ ((1, 2, 3), {'x': 'y'})
+ >>> del req.urlvars
+ >>> req.environ['wsgiorg.routing_args']
+ ((1, 2, 3), {})
+ >>> req.urlvars = {'test': 'value'}
+ >>> del req.urlargs
+ >>> req.environ['wsgiorg.routing_args']
+ ((), {'test': 'value'})
+ >>> req.is_xhr
+ False
+ >>> req.environ['HTTP_X_REQUESTED_WITH'] = 'XMLHttpRequest'
+ >>> req.is_xhr
+ True
+ >>> req.host
+ 'localhost:80'
+
+There are also variables to access the variables and body:
+
+ >>> from cStringIO import StringIO
+ >>> body = 'var1=value1&var2=value2&rep=1&rep=2'
+ >>> req = Request.blank('/')
+ >>> req.method = 'POST'
+ >>> req.body_file = StringIO(body)
+ >>> req.environ['CONTENT_LENGTH'] = str(len(body))
+ >>> vars = req.str_POST
+ >>> vars
+ MultiDict([('var1', 'value1'), ('var2', 'value2'), ('rep', '1'), ('rep', '2')])
+ >>> vars is req.POST
+ False
+ >>> req.POST
+ UnicodeMultiDict([(u'var1', u'value1'), (u'var2', u'value2'), (u'rep', u'1'), (u'rep', u'2')])
+ >>> req.decode_param_names
+ True
+
+Note that the variables are there for GET requests and non-form requests,
+but they are empty and read-only:
+
+ >>> req = Request.blank('/')
+ >>> req.str_POST
+ <NoVars: Not a form request>
+ >>> req.str_POST.items()
+ []
+ >>> req.str_POST['x'] = 'y'
+ Traceback (most recent call last):
+ ...
+ KeyError: 'Cannot add variables: Not a form request'
+ >>> req.method = 'POST'
+ >>> req.str_POST
+ MultiDict([])
+ >>> req.content_type = 'text/xml'
+ >>> req.body_file = StringIO('<xml></xml>')
+ >>> req.str_POST
+ <NoVars: Not an HTML form submission (Content-Type: text/xml)>
+ >>> req.body
+ '<xml></xml>'
+
+You can also get access to the query string variables, of course:
+
+ >>> req = Request.blank('/?a=b&d=e&d=f')
+ >>> req.str_GET
+ GET([('a', 'b'), ('d', 'e'), ('d', 'f')])
+ >>> req.GET['d']
+ u'f'
+ >>> req.GET.getall('d')
+ [u'e', u'f']
+ >>> req.method = 'POST'
+ >>> req.body = 'x=y&d=g'
+ >>> req.environ['CONTENT_LENGTH']
+ '7'
+ >>> req.params
+ UnicodeMultiDict([(u'a', u'b'), (u'd', u'e'), (u'd', u'f'), (u'x', u'y'), (u'd', u'g')])
+ >>> req.params['d']
+ u'f'
+ >>> req.params.getall('d')
+ [u'e', u'f', u'g']
+
+Cookies are viewed as a dictionary (*view only*):
+
+ >>> req = Request.blank('/')
+ >>> req.environ['HTTP_COOKIE'] = 'var1=value1; var2=value2'
+ >>> sorted(req.str_cookies.items())
+ [('var1', 'value1'), ('var2', 'value2')]
+ >>> sorted(req.cookies.items())
+ [(u'var1', u'value1'), (u'var2', u'value2')]
+ >>> req.charset = 'utf8'
+ >>> sorted(req.cookies.items())
+ [(u'var1', u'value1'), (u'var2', u'value2')]
+
+Sometimes conditional headers are problematic. You can remove them:
+
+ >>> from datetime import datetime
+ >>> req = Request.blank('/')
+ >>> req.if_none_match = 'some-etag'
+ >>> req.if_modified_since = datetime(2005, 1, 1, 12, 0)
+ >>> req.environ['HTTP_ACCEPT_ENCODING'] = 'gzip'
+ >>> print sorted(req.headers.items())
+ [('Accept-Encoding', 'gzip'), ('Host', 'localhost:80'), ('If-Modified-Since', 'Sat, 01 Jan 2005 12:00:00 GMT'), ('If-None-Match', 'some-etag')]
+ >>> req.remove_conditional_headers()
+ >>> print req.headers
+ {'Host': 'localhost:80'}
+
+Some headers are handled specifically (more should be added):
+
+ >>> req = Request.blank('/')
+ >>> req.if_none_match = 'xxx'
+ >>> 'xxx' in req.if_none_match
+ True
+ >>> 'yyy' in req.if_none_match
+ False
+ >>> req.if_modified_since = datetime(2005, 1, 1, 12, 0)
+ >>> req.if_modified_since < datetime(2006, 1, 1, 12, 0, tzinfo=UTC)
+ True
+ >>> req.user_agent is None
+ True
+ >>> req.user_agent = 'MSIE-Win'
+ >>> req.user_agent
+ 'MSIE-Win'
+
+ >>> req.cache_control
+ <CacheControl ''>
+ >>> req.cache_control.no_cache = True
+ >>> req.cache_control.max_age = 0
+ >>> req.cache_control
+ <CacheControl 'max-age=0, no-cache'>
+
+.cache_control is a view:
+
+ >>> 'cache-control' in req.headers
+ True
+ >>> req.headers['cache-control']
+ 'max-age=0, no-cache'
+ >>> req.cache_control = {'no-transform': None, 'max-age': 100}
+ >>> req.headers['cache-control']
+ 'max-age=100, no-transform'
+
+
+
+Accept-* headers are parsed into read-only objects that support
+containment tests, and some useful methods. Note that parameters on
+mime types are not supported.
+
+ >>> req = Request.blank('/')
+ >>> req.environ['HTTP_ACCEPT'] = "text/*;q=0.3, text/html;q=0.7, text/html;level=1, text/html;level=2;q=0.4, */*;q=0.1"
+ >>> req.accept # doctest: +ELLIPSIS
+ <MIMEAccept('text/*;q=0.3, text/html;q=0.7, text/html, text/html;q=0.4, */*;q=0.1')>
+ >>> for item, quality in req.accept._parsed:
+ ... print '%s: %0.1f' % (item, quality)
+ text/*: 0.3
+ text/html: 0.7
+ text/html: 1.0
+ text/html: 0.4
+ */*: 0.1
+ >>> '%0.1f' % req.accept.quality('text/plain')
+ '0.3'
+ >>> '%0.1f' % req.accept.quality('text/html')
+ '1.0'
+ >>> req.accept.first_match(['text/plain', 'text/html', 'image/png'])
+ 'text/plain'
+ >>> 'image/png' in req.accept
+ True
+ >>> req.environ['HTTP_ACCEPT'] = "text/html, application/xml; q=0.7, text/*; q=0.5, */*; q=0.1"
+ >>> req.accept # doctest: +ELLIPSIS
+ <MIMEAccept('text/html, application/xml;q=0.7, text/*;q=0.5, */*;q=0.1')>
+ >>> req.accept.best_match(['text/plain', 'application/xml'])
+ 'application/xml'
+ >>> req.accept.first_match(['application/xml', 'text/html'])
+ 'application/xml'
+ >>> req.accept = "text/html, application/xml, text/*; q=0.5"
+ >>> 'image/png' in req.accept
+ False
+ >>> 'text/plain' in req.accept
+ True
+ >>> req.accept_charset = 'utf8'
+ >>> 'UTF8' in req.accept_charset
+ True
+ >>> 'gzip' in req.accept_encoding
+ False
+ >>> req.accept_encoding = 'gzip'
+ >>> 'GZIP' in req.accept_encoding
+ True
+ >>> req.accept_language = {'en-US': 0.5, 'es': 0.7}
+ >>> str(req.accept_language)
+ 'es;q=0.7, en-US;q=0.5'
+ >>> req.headers['Accept-Language']
+ 'es;q=0.7, en-US;q=0.5'
+ >>> req.accept_language.best_matches('en-GB')
+ ['es', 'en-US', 'en-GB']
+ >>> req.accept_language.best_matches('es')
+ ['es']
+ >>> req.accept_language.best_matches('ES')
+ ['ES']
+
+ >>> req = Request.blank('/', accept_language='en;q=0.5')
+ >>> req.accept_language.best_match(['en-gb'])
+ 'en-gb'
+
+ >>> req = Request.blank('/', accept_charset='utf-8;q=0.5')
+ >>> req.accept_charset.best_match(['iso-8859-1', 'utf-8'])
+ 'iso-8859-1'
+
+The If-Range header is a combination of a possible conditional date or
+etag match::
+
+ >>> req = Request.blank('/')
+ >>> req.if_range = 'asdf'
+ >>> req.if_range
+ <IfRange etag="asdf", date=*>
+ >>> from webob import Response
+ >>> res = Response()
+ >>> res.etag = 'asdf'
+ >>> req.if_range.match_response(res)
+ True
+ >>> res.etag = None
+ >>> req.if_range.match_response(res)
+ False
+ >>> res.last_modified = datetime(2005, 1, 1, 12, 0, tzinfo=UTC)
+ >>> req.if_range = datetime(2006, 1, 1, 12, 0, tzinfo=UTC)
+ >>> req.if_range
+ <IfRange etag=*, date=Sun, 01 Jan 2006 12:00:00 GMT>
+ >>> req.if_range.match_response(res)
+ True
+ >>> res.last_modified = datetime(2007, 1, 1, 12, 0, tzinfo=UTC)
+ >>> req.if_range.match_response(res)
+ False
+ >>> req = Request.blank('/')
+ >>> req.if_range
+ <Empty If-Range>
+ >>> req.if_range.match_response(res)
+ True
+
+Ranges work like so::
+
+ >>> req = Request.blank('/')
+ >>> req.range = (0, 100)
+ >>> req.range
+ <Range ranges=(0, 100)>
+ >>> str(req.range)
+ 'bytes=0-99'
+
+You can use them with responses::
+
+ >>> res = Response()
+ >>> res.content_range = req.range.content_range(1000)
+ >>> res.content_range
+ <ContentRange bytes 0-99/1000>
+ >>> str(res.content_range)
+ 'bytes 0-99/1000'
+ >>> start, end, length = res.content_range
+ >>> start, end, length
+ (0, 100, 1000)
+
+A quick test of caching the request body:
+
+ >>> from cStringIO import StringIO
+ >>> length = Request.request_body_tempfile_limit+10
+ >>> data = StringIO('x'*length)
+ >>> req = Request.blank('/')
+ >>> req.content_length = length
+ >>> req.method = 'PUT'
+ >>> req.body_file = data
+ >>> req.body_file_raw
+ <...IO... object at ...>
+ >>> len(req.body)
+ 10250
+ >>> req.body_file
+ <open file ..., mode 'w+b' at ...>
+ >>> int(req.body_file.tell())
+ 0
+ >>> req.POST
+ UnicodeMultiDict([])
+ >>> int(req.body_file.tell())
+ 0
+
+Some query tests:
+
+ >>> req = Request.blank('/')
+ >>> req.GET.get('unknown')
+ >>> req.GET.get('unknown', '?')
+ '?'
+ >>> req.POST.get('unknown')
+ >>> req.POST.get('unknown', '?')
+ '?'
+ >>> req.params.get('unknown')
+ >>> req.params.get('unknown', '?')
+ '?'
+
+Some updating of the query string:
+
+ >>> req = Request.blank('http://localhost/foo?a=b')
+ >>> req.str_GET
+ GET([('a', 'b')])
+ >>> req.str_GET['c'] = 'd'
+ >>> req.query_string
+ 'a=b&c=d'
+
+And for dealing with file uploads:
+
+ >>> req = Request.blank('/posty')
+ >>> req.method = 'POST'
+ >>> req.content_type = 'multipart/form-data; boundary="foobar"'
+ >>> req.body = '''\
+ ... --foobar
+ ... Content-Disposition: form-data; name="a"
+ ...
+ ... b
+ ... --foobar
+ ... Content-Disposition: form-data; name="upload"; filename="test.html"
+ ... Content-Type: text/html
+ ...
+ ... <html>Some text...</html>
+ ... --foobar--
+ ... '''
+ >>> req.str_POST
+ MultiDict([('a', 'b'), ('upload', FieldStorage('upload', 'test.html'))])
+ >>> print req.body.replace('\r', '') # doctest: +REPORT_UDIFF
+ --foobar
+ Content-Disposition: form-data; name="a"
+ <BLANKLINE>
+ b
+ --foobar
+ Content-Disposition: form-data; name="upload"; filename="test.html"
+ Content-type: text/html
+ <BLANKLINE>
+ <html>Some text...</html>
+ --foobar--
+ >>> req.POST['c'] = 'd'
+ >>> req.str_POST
+ MultiDict([('a', 'b'), ('upload', FieldStorage('upload', 'test.html')), ('c', 'd')])
+ >>> req.body_file_raw
+ <FakeCGIBody at ... viewing MultiDict([('a'...d')])>
+ >>> sorted(req.str_POST.keys())
+ ['a', 'c', 'upload']
+ >>> print req.body.replace('\r', '') # doctestx: +REPORT_UDIFF
+ --foobar
+ Content-Disposition: form-data; name="a"
+ <BLANKLINE>
+ b
+ --foobar
+ Content-Disposition: form-data; name="upload"; filename="test.html"
+ Content-type: text/html
+ <BLANKLINE>
+ <html>Some text...</html>
+ --foobar
+ Content-Disposition: form-data; name="c"
+ <BLANKLINE>
+ d
+ --foobar--
+
+FakeCGIBody have both readline and readlines methods:
+
+ >>> req_ = Request.blank('/posty')
+ >>> req_.method = 'POST'
+ >>> req_.content_type = 'multipart/form-data; boundary="foobar"'
+ >>> req_.body = '''\
+ ... --foobar
+ ... Content-Disposition: form-data; name="a"
+ ...
+ ... b
+ ... --foobar
+ ... Content-Disposition: form-data; name="upload"; filename="test.html"
+ ... Content-Type: text/html
+ ...
+ ... <html>Some text...</html>
+ ... --foobar--
+ ... '''
+ >>> req_.str_POST
+ MultiDict([('a', 'b'), ('upload', FieldStorage('upload', 'test.html'))])
+ >>> print req_.body.replace('\r', '') # doctest: +REPORT_UDIFF
+ --foobar
+ Content-Disposition: form-data; name="a"
+ <BLANKLINE>
+ b
+ --foobar
+ Content-Disposition: form-data; name="upload"; filename="test.html"
+ Content-type: text/html
+ <BLANKLINE>
+ <html>Some text...</html>
+ --foobar--
+ >>> req_.POST['c'] = 'd'
+ >>> req_.str_POST
+ MultiDict([('a', 'b'), ('upload', FieldStorage('upload', 'test.html')), ('c', 'd')])
+ >>> req_.body_file_raw.readline()
+ '--foobar\r\n'
+ >>> [n.replace('\r', '') for n in req_.body_file.readlines()]
+ ['Content-Disposition: form-data; name="a"\n', '\n', 'b\n', '--foobar\n', 'Content-Disposition: form-data; name="upload"; filename="test.html"\n', 'Content-type: text/html\n', '\n', '<html>Some text...</html>\n', '--foobar\n', 'Content-Disposition: form-data; name="c"\n', '\n', 'd\n', '--foobar--']
+
+Also reparsing works through the fake body:
+
+ >>> del req.environ['webob._parsed_post_vars']
+ >>> req.str_POST
+ MultiDict([('a', 'b'), ('upload', FieldStorage('upload', 'test.html')), ('c', 'd')])
+
+A ``BaseRequest`` class exists for the purpose of usage by web
+frameworks that want a less featureful ``Request``.
+
+For example, the ``Request`` class mutates the
+``environ['webob.adhoc_attrs']`` attribute when its ``__getattr__``,
+``__setattr__``, and ``__delattr__`` are invoked.
+
+The ``BaseRequest`` class omits the mutation annotation behavior
+provided by the default ``Request`` implementation. Instead, the of
+the ``BaseRequest`` class actually mutates the ``__dict__`` of the
+request instance itself.
+
+ >>> from webob import BaseRequest
+ >>> req = BaseRequest.blank('/')
+ >>> req.foo = 1
+ >>> req.environ['webob.adhoc_attrs']
+ Traceback (most recent call last):
+ ...
+ KeyError: 'webob.adhoc_attrs'
+ >>> req.foo
+ 1
+ >>> del req.foo
+ >>> req.foo
+ Traceback (most recent call last):
+ ...
+ AttributeError: 'BaseRequest' object has no attribute 'foo'
+
+
+
+ >>> req = BaseRequest.blank('//foo')
+ >>> print req.path_info_pop('x')
+ None
+ >>> req.script_name
+ ''
+ >>> print BaseRequest.blank('/foo').path_info_pop('/')
+ None
+ >>> BaseRequest.blank('/foo').path_info_pop('foo')
+ 'foo'
+ >>> BaseRequest.blank('/foo').path_info_pop('fo+')
+ 'foo'
+ >>> BaseRequest.blank('//1000').path_info_pop('\d+')
+ '1000'
+ >>> BaseRequest.blank('/1000/x').path_info_pop('\d+')
+ '1000'
+
+
+ >>> req = Request.blank('/', method='PUT', body='x'*10)
+
+str(req) returns the request as HTTP request string
+
+ >>> print req
+ PUT / HTTP/1.0
+ Content-Length: 10
+ Host: localhost:80
+ <BLANKLINE>
+ xxxxxxxxxx
+
+req.as_string() does the same but also can take additional argument `skip_body`
+skip_body=True excludes the body from the result
+
+ >>> print req.as_string(skip_body=True)
+ PUT / HTTP/1.0
+ Content-Length: 10
+ Host: localhost:80
+
+
+skip_body=<int> excludes the body from the result if it's longer than that number
+
+ >>> print req.as_string(skip_body=5)
+ PUT / HTTP/1.0
+ Content-Length: 10
+ Host: localhost:80
+ <BLANKLINE>
+ <body skipped (len=10)>
+
+but not if it's shorter
+
+ >>> print req.as_string(skip_body=100)
+ PUT / HTTP/1.0
+ Content-Length: 10
+ Host: localhost:80
+ <BLANKLINE>
+ xxxxxxxxxx
+
diff --git a/lib/webob_1_1_1/docs/test_response.txt b/lib/webob_1_1_1/docs/test_response.txt
new file mode 100644
index 0000000..5a1ce70
--- /dev/null
+++ b/lib/webob_1_1_1/docs/test_response.txt
@@ -0,0 +1,445 @@
+This demonstrates how the Response object works, and tests it at the
+same time.
+
+ >>> from doctest import ELLIPSIS
+ >>> from webob import Response, UTC
+ >>> from datetime import datetime
+ >>> res = Response('Test', status='200 OK')
+
+This is a minimal response object. We can do things like get and set
+the body:
+
+ >>> res.body
+ 'Test'
+ >>> res.body = 'Another test'
+ >>> res.body
+ 'Another test'
+ >>> res.body = 'Another'
+ >>> res.write(' test')
+ >>> res.app_iter
+ ['Another', ' test']
+ >>> res.content_length
+ 12
+ >>> res.headers['content-length']
+ '12'
+
+Content-Length is only applied when setting the body to a string; you
+have to set it manually otherwise. There are also getters and setters
+for the various pieces:
+
+ >>> res.app_iter = ['test']
+ >>> print res.content_length
+ None
+ >>> res.content_length = 4
+ >>> res.status
+ '200 OK'
+ >>> res.status_int
+ 200
+ >>> res.headers
+ ResponseHeaders([('Content-Type', 'text/html; charset=UTF-8'), ('Content-Length', '4')])
+ >>> res.headerlist
+ [('Content-Type', 'text/html; charset=UTF-8'), ('Content-Length', '4')]
+
+Content-type and charset are handled separately as properties, though
+they are both in the ``res.headers['content-type']`` header:
+
+ >>> res.content_type
+ 'text/html'
+ >>> res.content_type = 'text/html'
+ >>> res.content_type
+ 'text/html'
+ >>> res.charset
+ 'UTF-8'
+ >>> res.charset = 'iso-8859-1'
+ >>> res.charset
+ 'iso-8859-1'
+ >>> res.content_type
+ 'text/html'
+ >>> res.headers['content-type']
+ 'text/html; charset=iso-8859-1'
+
+Cookie handling is done through methods:
+
+ >>> res.set_cookie('test', 'value')
+ >>> res.headers['set-cookie']
+ 'test=value; Path=/'
+ >>> res.set_cookie('test2', 'value2', max_age=10000)
+ >>> res.headers['set-cookie'] # We only see the last header
+ 'test2=value2; Max-Age=10000; Path=/; expires=... GMT'
+ >>> res.headers.getall('set-cookie')
+ ['test=value; Path=/', 'test2=value2; Max-Age=10000; Path=/; expires=... GMT']
+ >>> res.unset_cookie('test')
+ >>> res.headers.getall('set-cookie')
+ ['test2=value2; Max-Age=10000; Path=/; expires=... GMT']
+ >>> res.set_cookie('test2', 'value2-add')
+ >>> res.headers.getall('set-cookie')
+ ['test2=value2; Max-Age=10000; Path=/; expires=... GMT', 'test2=value2-add; Path=/']
+ >>> res.set_cookie('test2', 'value2-replace', overwrite=True)
+ >>> res.headers.getall('set-cookie')
+ ['test2=value2-replace; Path=/']
+
+
+ >>> r = Response()
+ >>> r.set_cookie('x', 'x')
+ >>> r.set_cookie('y', 'y')
+ >>> r.set_cookie('z', 'z')
+ >>> r.headers.getall('set-cookie')
+ ['x=x; Path=/', 'y=y; Path=/', 'z=z; Path=/']
+ >>> r.unset_cookie('y')
+ >>> r.headers.getall('set-cookie')
+ ['x=x; Path=/', 'z=z; Path=/']
+
+
+Most headers are available in a parsed getter/setter form through
+properties:
+
+ >>> res.age = 10
+ >>> res.age, res.headers['age']
+ (10, '10')
+ >>> res.allow = ['GET', 'PUT']
+ >>> res.allow, res.headers['allow']
+ (('GET', 'PUT'), 'GET, PUT')
+ >>> res.cache_control
+ <CacheControl ''>
+ >>> print res.cache_control.max_age
+ None
+ >>> res.cache_control.properties['max-age'] = None
+ >>> print res.cache_control.max_age
+ -1
+ >>> res.cache_control.max_age = 10
+ >>> res.cache_control
+ <CacheControl 'max-age=10'>
+ >>> res.headers['cache-control']
+ 'max-age=10'
+ >>> res.cache_control.max_stale = 10
+ Traceback (most recent call last):
+ ...
+ AttributeError: The property max-stale only applies to request Cache-Control
+ >>> res.cache_control = {}
+ >>> res.cache_control
+ <CacheControl ''>
+ >>> res.content_disposition = 'attachment; filename=foo.xml'
+ >>> (res.content_disposition, res.headers['content-disposition'])
+ ('attachment; filename=foo.xml', 'attachment; filename=foo.xml')
+ >>> res.content_encoding = 'gzip'
+ >>> (res.content_encoding, res.headers['content-encoding'])
+ ('gzip', 'gzip')
+ >>> res.content_language = 'en'
+ >>> (res.content_language, res.headers['content-language'])
+ (('en',), 'en')
+ >>> res.content_location = 'http://localhost:8080'
+ >>> res.headers['content-location']
+ 'http://localhost:8080'
+ >>> res.content_range = (0, 100, 1000)
+ >>> (res.content_range, res.headers['content-range'])
+ (<ContentRange bytes 0-99/1000>, 'bytes 0-99/1000')
+ >>> res.date = datetime(2005, 1, 1, 12, 0, tzinfo=UTC)
+ >>> (res.date, res.headers['date'])
+ (datetime.datetime(2005, 1, 1, 12, 0, tzinfo=UTC), 'Sat, 01 Jan 2005 12:00:00 GMT')
+ >>> print res.etag
+ None
+ >>> res.etag = 'foo'
+ >>> (res.etag, res.headers['etag'])
+ ('foo', '"foo"')
+ >>> res.etag = 'something-with-"quotes"'
+ >>> (res.etag, res.headers['etag'])
+ ('something-with-"quotes"', '"something-with-\\"quotes\\""')
+ >>> res.expires = res.date
+ >>> res.retry_after = 120 # two minutes
+ >>> res.retry_after
+ datetime.datetime(...)
+ >>> res.server = 'Python/foo'
+ >>> res.headers['server']
+ 'Python/foo'
+ >>> res.vary = ['Cookie']
+ >>> (res.vary, res.headers['vary'])
+ (('Cookie',), 'Cookie')
+
+The location header will absolutify itself when the response
+application is actually served. We can force this with
+``req.get_response``::
+
+ >>> res.location = '/test.html'
+ >>> from webob import Request
+ >>> req = Request.blank('/')
+ >>> res.location
+ '/test.html'
+ >>> req.get_response(res).location
+ 'http://localhost/test.html'
+ >>> res.location = '/test2.html'
+ >>> req.get_response(res).location
+ 'http://localhost/test2.html'
+
+There's some conditional response handling too (you have to turn on
+conditional_response)::
+
+ >>> res = Response('abc', conditional_response=True) # doctest: +ELLIPSIS
+ >>> req = Request.blank('/')
+ >>> res.etag = 'tag'
+ >>> req.if_none_match = 'tag'
+ >>> req.get_response(res)
+ <Response ... 304 Not Modified>
+ >>> res.etag = 'other-tag'
+ >>> req.get_response(res)
+ <Response ... 200 OK>
+ >>> del req.if_none_match
+ >>> req.if_modified_since = datetime(2005, 1, 1, 12, 1, tzinfo=UTC)
+ >>> res.last_modified = datetime(2005, 1, 1, 12, 1, tzinfo=UTC)
+ >>> print req.get_response(res)
+ 304 Not Modified
+ ETag: "other-tag"
+ Last-Modified: Sat, 01 Jan 2005 12:01:00 GMT
+ >>> res.last_modified = datetime(2006, 1, 1, 12, 1, tzinfo=UTC)
+ >>> req.get_response(res)
+ <Response ... 200 OK>
+ >>> res.last_modified = None
+ >>> req.get_response(res)
+ <Response ... 200 OK>
+
+Weak etags::
+
+ >>> req = Request.blank('/', if_none_match='W/"test"')
+ >>> res = Response(conditional_response=True, etag='test')
+ >>> req.get_response(res).status
+ '304 Not Modified'
+
+Also range response::
+
+ >>> res = Response('0123456789', conditional_response=True)
+ >>> req = Request.blank('/', range=(1, 5))
+ >>> req.range
+ <Range ranges=(1, 5)>
+ >>> str(req.range)
+ 'bytes=1-4'
+ >>> result = req.get_response(res)
+ >>> result.body
+ '1234'
+ >>> result.content_range.stop
+ 5
+ >>> result.content_range
+ <ContentRange bytes 1-4/10>
+ >>> tuple(result.content_range)
+ (1, 5, 10)
+ >>> result.content_length
+ 4
+
+
+ >>> req.range = (5, 20)
+ >>> str(req.range)
+ 'bytes=5-19'
+ >>> result = req.get_response(res)
+ >>> print result
+ 206 Partial Content
+ Content-Length: 5
+ Content-Range: bytes 5-9/10
+ Content-Type: text/html; charset=UTF-8
+ <BLANKLINE>
+ 56789
+ >>> tuple(result.content_range)
+ (5, 10, 10)
+
+ >>> req_head = req.copy()
+ >>> req_head.method = 'HEAD'
+ >>> print req_head.get_response(res)
+ 206 Partial Content
+ Content-Length: 5
+ Content-Range: bytes 5-9/10
+ Content-Type: text/html; charset=UTF-8
+
+And an invalid requested range:
+
+ >>> req.range = (10, 20)
+ >>> result = req.get_response(res)
+ >>> print result
+ 416 Requested Range Not Satisfiable
+ Content-Length: 44
+ Content-Range: bytes */10
+ Content-Type: text/plain
+ <BLANKLINE>
+ Requested range not satisfiable: bytes=10-19
+ >>> str(result.content_range)
+ 'bytes */10'
+
+ >>> req_head = req.copy()
+ >>> req_head.method = 'HEAD'
+ >>> print req_head.get_response(res)
+ 416 Requested Range Not Satisfiable
+ Content-Length: 44
+ Content-Range: bytes */10
+ Content-Type: text/plain
+
+ >>> Request.blank('/', range=(1,2)).get_response(
+ ... Response('0123456789', conditional_response=True)).content_length
+ 1
+
+
+That was easier; we'll try it with a iterator for the body::
+
+ >>> res = Response(conditional_response=True)
+ >>> res.app_iter = ['01234', '567', '89']
+ >>> req = Request.blank('/')
+ >>> req.range = (1, 5)
+ >>> result = req.get_response(res)
+
+Because we don't know the length of the app_iter, this doesn't work::
+
+ >>> result.body
+ '0123456789'
+ >>> print result.content_range
+ None
+
+But it will, if we set content_length::
+ >>> res.content_length = 10
+ >>> req.range = (5, None)
+ >>> result = req.get_response(res)
+ >>> result.body
+ '56789'
+ >>> result.content_range
+ <ContentRange bytes 5-9/10>
+
+
+Ranges requesting x last bytes are supported too:
+
+ >>> req.range = 'bytes=-1'
+ >>> req.range
+ <Range ranges=(-1, None)>
+ >>> result = req.get_response(res)
+ >>> result.body
+ '9'
+ >>> result.content_range
+ <ContentRange bytes 9-9/10>
+ >>> result.content_length
+ 1
+
+
+If those ranges are not satisfiable, a 416 error is returned:
+
+ >>> req.range = 'bytes=-100'
+ >>> result = req.get_response(res)
+ >>> result.status
+ '416 Requested Range Not Satisfiable'
+ >>> result.content_range
+ <ContentRange bytes */10>
+ >>> result.body
+ 'Requested range not satisfiable: bytes=-100'
+
+
+If we set Content-Length then we can use it with an app_iter
+
+ >>> res.content_length = 10
+ >>> req.range = (1, 5) # python-style range
+ >>> req.range
+ <Range ranges=(1, 5)>
+ >>> result = req.get_response(res)
+ >>> result.body
+ '1234'
+ >>> result.content_range
+ <ContentRange bytes 1-4/10>
+ >>> # And trying If-modified-since
+ >>> res.etag = 'foobar'
+ >>> req.if_range = 'foobar'
+ >>> req.if_range
+ <IfRange etag="foobar", date=*>
+ >>> result = req.get_response(res)
+ >>> result.content_range
+ <ContentRange bytes 1-4/10>
+ >>> req.if_range = 'blah'
+ >>> result = req.get_response(res)
+ >>> result.content_range
+ >>> req.if_range = datetime(2005, 1, 1, 12, 0, tzinfo=UTC)
+ >>> res.last_modified = datetime(2005, 1, 1, 12, 0, tzinfo=UTC)
+ >>> result = req.get_response(res)
+ >>> result.content_range
+ <ContentRange bytes 1-4/10>
+ >>> res.last_modified = datetime(2006, 1, 1, 12, 0, tzinfo=UTC)
+ >>> result = req.get_response(res)
+ >>> result.content_range
+
+Some tests of Content-Range parsing::
+
+ >>> from webob.byterange import ContentRange
+ >>> ContentRange.parse('bytes */*')
+ <ContentRange bytes */*>
+ >>> ContentRange.parse('bytes */10')
+ <ContentRange bytes */10>
+ >>> ContentRange.parse('bytes 5-9/10')
+ <ContentRange bytes 5-9/10>
+ >>> ContentRange.parse('bytes 5-10/*')
+ <ContentRange bytes 5-10/*>
+ >>> print ContentRange.parse('bytes 5-10/10')
+ None
+ >>> print ContentRange.parse('bytes 5-4/10')
+ None
+ >>> print ContentRange.parse('bytes 5-*/10')
+ None
+
+Some tests of exceptions::
+
+ >>> from webob import exc
+ >>> res = exc.HTTPNotFound('Not found!')
+ >>> res.content_type = 'text/plain'
+ >>> res.content_type
+ 'text/plain'
+ >>> res = exc.HTTPNotModified()
+ >>> res.headers
+ ResponseHeaders([])
+
+Headers can be set to unicode values::
+
+ >>> res = Response('test')
+ >>> res.etag = u'fran\xe7ais'
+
+But they come out as str::
+
+ >>> res.etag
+ 'fran\xe7ais'
+
+
+Unicode can come up in unexpected places, make sure it doesn't break things
+(this particular case could be caused by a `from __future__ import unicode_literals`)::
+
+ >>> Request.blank('/', method=u'POST').get_response(exc.HTTPMethodNotAllowed())
+ <Response at ... 405 Method Not Allowed>
+
+Copying Responses should copy their internal structures
+
+ >>> r = Response(app_iter=[])
+ >>> r2 = r.copy()
+ >>> r.headerlist is r2.headerlist
+ False
+ >>> r.app_iter is r2.app_iter
+ False
+
+ >>> r = Response(app_iter=iter(['foo']))
+ >>> r2 = r.copy()
+ >>> del r2.content_type
+ >>> r2.body_file.write(' bar')
+ >>> print r
+ 200 OK
+ Content-Type: text/html; charset=UTF-8
+ <BLANKLINE>
+ foo
+ >>> print r2
+ 200 OK
+ Content-Length: 7
+ <BLANKLINE>
+ foo bar
+
+
+Additional Response constructor keywords are used to set attributes
+
+ >>> r = Response(cache_expires=True)
+ >>> r.headers['Cache-Control']
+ 'max-age=0, must-revalidate, no-cache, no-store'
+
+
+
+ >>> from webob.exc import HTTPBadRequest
+ >>> raise HTTPBadRequest('bad data')
+ Traceback (most recent call last):
+ ...
+ HTTPBadRequest: bad data
+ >>> raise HTTPBadRequest()
+ Traceback (most recent call last):
+ ...
+ HTTPBadRequest: The server could not comply with the request since it is either malformed or otherwise incorrect.
diff --git a/lib/webob_1_1_1/docs/wiki-example-code/example.py b/lib/webob_1_1_1/docs/wiki-example-code/example.py
new file mode 100644
index 0000000..1687dc2
--- /dev/null
+++ b/lib/webob_1_1_1/docs/wiki-example-code/example.py
@@ -0,0 +1,200 @@
+import os
+import re
+from webob import Request, Response
+from webob import exc
+from tempita import HTMLTemplate
+
+VIEW_TEMPLATE = HTMLTemplate("""\
+<html>
+ <head>
+ <title>{{page.title}}</title>
+ </head>
+ <body>
+<h1>{{page.title}}</h1>
+{{if message}}
+<div style="background-color: #99f">{{message}}</div>
+{{endif}}
+
+<div>{{page.content|html}}</div>
+
+<hr>
+<a href="{{req.url}}?action=edit">Edit</a>
+ </body>
+</html>
+""")
+
+EDIT_TEMPLATE = HTMLTemplate("""\
+<html>
+ <head>
+ <title>Edit: {{page.title}}</title>
+ </head>
+ <body>
+{{if page.exists}}
+<h1>Edit: {{page.title}}</h1>
+{{else}}
+<h1>Create: {{page.title}}</h1>
+{{endif}}
+
+<form action="{{req.path_url}}" method="POST">
+ <input type="hidden" name="mtime" value="{{page.mtime}}">
+ Title: <input type="text" name="title" style="width: 70%" value="{{page.title}}"><br>
+ Content: <input type="submit" value="Save">
+ <a href="{{req.path_url}}">Cancel</a>
+ <br>
+ <textarea name="content" style="width: 100%; height: 75%" rows="40">{{page.content}}</textarea>
+ <br>
+ <input type="submit" value="Save">
+ <a href="{{req.path_url}}">Cancel</a>
+</form>
+</body></html>
+""")
+
+class WikiApp(object):
+
+ view_template = VIEW_TEMPLATE
+ edit_template = EDIT_TEMPLATE
+
+ def __init__(self, storage_dir):
+ self.storage_dir = os.path.abspath(os.path.normpath(storage_dir))
+
+ def __call__(self, environ, start_response):
+ req = Request(environ)
+ action = req.params.get('action', 'view')
+ page = self.get_page(req.path_info)
+ try:
+ try:
+ meth = getattr(self, 'action_%s_%s' % (action, req.method))
+ except AttributeError:
+ raise exc.HTTPBadRequest('No such action %r' % action)
+ resp = meth(req, page)
+ except exc.HTTPException, e:
+ resp = e
+ return resp(environ, start_response)
+
+ def get_page(self, path):
+ path = path.lstrip('/')
+ if not path:
+ path = 'index'
+ path = os.path.join(self.storage_dir, path)
+ path = os.path.normpath(path)
+ if path.endswith('/'):
+ path += 'index'
+ if not path.startswith(self.storage_dir):
+ raise exc.HTTPBadRequest("Bad path")
+ path += '.html'
+ return Page(path)
+
+ def action_view_GET(self, req, page):
+ if not page.exists:
+ return exc.HTTPTemporaryRedirect(
+ location=req.url + '?action=edit')
+ if req.cookies.get('message'):
+ message = req.cookies['message']
+ else:
+ message = None
+ text = self.view_template.substitute(
+ page=page, req=req, message=message)
+ resp = Response(text)
+ if message:
+ resp.delete_cookie('message')
+ else:
+ resp.last_modified = page.mtime
+ resp.conditional_response = True
+ return resp
+
+ def action_view_POST(self, req, page):
+ submit_mtime = int(req.params.get('mtime') or '0') or None
+ if page.mtime != submit_mtime:
+ return exc.HTTPPreconditionFailed(
+ "The page has been updated since you started editing it")
+ page.set(
+ title=req.params['title'],
+ content=req.params['content'])
+ resp = exc.HTTPSeeOther(
+ location=req.path_url)
+ resp.set_cookie('message', 'Page updated')
+ return resp
+
+ def action_edit_GET(self, req, page):
+ text = self.edit_template.substitute(
+ page=page, req=req)
+ return Response(text)
+
+class Page(object):
+ def __init__(self, filename):
+ self.filename = filename
+
+ @property
+ def exists(self):
+ return os.path.exists(self.filename)
+
+ @property
+ def title(self):
+ if not self.exists:
+ # we need to guess the title
+ basename = os.path.splitext(os.path.basename(self.filename))[0]
+ basename = re.sub(r'[_-]', ' ', basename)
+ return basename.capitalize()
+ content = self.full_content
+ match = re.search(r'<title>(.*?)</title>', content, re.I|re.S)
+ return match.group(1)
+
+ @property
+ def full_content(self):
+ f = open(self.filename, 'rb')
+ try:
+ return f.read()
+ finally:
+ f.close()
+
+ @property
+ def content(self):
+ if not self.exists:
+ return ''
+ content = self.full_content
+ match = re.search(r'<body[^>]*>(.*?)</body>', content, re.I|re.S)
+ return match.group(1)
+
+ @property
+ def mtime(self):
+ if not self.exists:
+ return None
+ else:
+ return os.stat(self.filename).st_mtime
+
+ def set(self, title, content):
+ dir = os.path.dirname(self.filename)
+ if not os.path.exists(dir):
+ os.makedirs(dir)
+ new_content = """<html><head><title>%s</title></head><body>%s</body></html>""" % (
+ title, content)
+ f = open(self.filename, 'wb')
+ f.write(new_content)
+ f.close()
+
+if __name__ == '__main__':
+ import optparse
+ parser = optparse.OptionParser(
+ usage='%prog --port=PORT'
+ )
+ parser.add_option(
+ '-p', '--port',
+ default='8080',
+ dest='port',
+ type='int',
+ help='Port to serve on (default 8080)')
+ parser.add_option(
+ '--wiki-data',
+ default='./wiki',
+ dest='wiki_data',
+ help='Place to put wiki data into (default ./wiki/)')
+ options, args = parser.parse_args()
+ print 'Writing wiki pages to %s' % options.wiki_data
+ app = WikiApp(options.wiki_data)
+ from wsgiref.simple_server import make_server
+ httpd = make_server('localhost', options.port, app)
+ print 'Serving on http://localhost:%s' % options.port
+ try:
+ httpd.serve_forever()
+ except KeyboardInterrupt:
+ print '^C'
diff --git a/lib/webob_1_1_1/docs/wiki-example.txt b/lib/webob_1_1_1/docs/wiki-example.txt
new file mode 100644
index 0000000..91d3200
--- /dev/null
+++ b/lib/webob_1_1_1/docs/wiki-example.txt
@@ -0,0 +1,687 @@
+Wiki Example
+============
+
+:author: Ian Bicking <ianb@colorstudy.com>
+
+.. contents::
+
+Introduction
+------------
+
+This is an example of how to write a WSGI application using WebOb.
+WebOb isn't itself intended to write applications -- it is not a web
+framework on its own -- but it is *possible* to write applications
+using just WebOb.
+
+The `file serving example <file-example.html>`_ is a better example of
+advanced HTTP usage. The `comment middleware example
+<comment-example.html>`_ is a better example of using middleware.
+This example provides some completeness by showing an
+application-focused end point.
+
+This example implements a very simple wiki.
+
+Code
+----
+
+The finished code for this is available in
+`docs/wiki-example-code/example.py
+<http://bitbucket.org/ianb/webob/src/tip/docs/wiki-example-code/example.py>`_
+-- you can run that file as a script to try it out.
+
+Creating an Application
+-----------------------
+
+A common pattern for creating small WSGI applications is to have a
+class which is instantiated with the configuration. For our
+application we'll be storing the pages under a directory.
+
+.. code-block:: python
+
+ class WikiApp(object):
+
+ def __init__(self, storage_dir):
+ self.storage_dir = os.path.abspath(os.path.normpath(storage_dir))
+
+WSGI applications are callables like ``wsgi_app(environ,
+start_response)``. *Instances* of `WikiApp` are WSGI applications, so
+we'll implement a ``__call__`` method:
+
+.. code-block:: python
+
+ class WikiApp(object):
+ ...
+ def __call__(self, environ, start_response):
+ # what we'll fill in
+
+To make the script runnable we'll create a simple command-line
+interface:
+
+.. code-block:: python
+
+ if __name__ == '__main__':
+ import optparse
+ parser = optparse.OptionParser(
+ usage='%prog --port=PORT'
+ )
+ parser.add_option(
+ '-p', '--port',
+ default='8080',
+ dest='port',
+ type='int',
+ help='Port to serve on (default 8080)')
+ parser.add_option(
+ '--wiki-data',
+ default='./wiki',
+ dest='wiki_data',
+ help='Place to put wiki data into (default ./wiki/)')
+ options, args = parser.parse_args()
+ print 'Writing wiki pages to %s' % options.wiki_data
+ app = WikiApp(options.wiki_data)
+ from wsgiref.simple_server import make_server
+ httpd = make_server('localhost', options.port, app)
+ print 'Serving on http://localhost:%s' % options.port
+ try:
+ httpd.serve_forever()
+ except KeyboardInterrupt:
+ print '^C'
+
+There's not much to talk about in this code block. The application is
+instantiated and served with the built-in module
+`wsgiref.simple_server
+<http://www.python.org/doc/current/lib/module-wsgiref.simple_server.html>`_.
+
+The WSGI Application
+--------------------
+
+Of course all the interesting stuff is in that ``__call__`` method.
+WebOb lets you ignore some of the details of WSGI, like what
+``start_response`` really is. ``environ`` is a CGI-like dictionary,
+but ``webob.Request`` gives an object interface to it.
+``webob.Response`` represents a response, and is itself a WSGI
+application. Here's kind of the hello world of WSGI applications
+using these objects:
+
+.. code-block:: python
+
+ from webob import Request, Response
+
+ class WikiApp(object):
+ ...
+
+ def __call__(self, environ, start_response):
+ req = Request(environ)
+ resp = Response(
+ 'Hello %s!' % req.params.get('name', 'World'))
+ return resp(environ, start_response)
+
+``req.params.get('name', 'World')`` gets any query string parameter
+(like ``?name=Bob``), or if it's a POST form request it will look for
+a form parameter ``name``. We instantiate the response with the body
+of the response. You could also give keyword arguments like
+``content_type='text/plain'`` (``text/html`` is the default content
+type and ``200 OK`` is the default status).
+
+For the wiki application we'll support a couple different kinds of
+screens, and we'll make our ``__call__`` method dispatch to different
+methods depending on the request. We'll support an ``action``
+parameter like ``?action=edit``, and also dispatch on the method (GET,
+POST, etc, in ``req.method``). We'll pass in the request and expect a
+response object back.
+
+Also, WebOb has a series of exceptions in ``webob.exc``, like
+``webob.exc.HTTPNotFound``, ``webob.exc.HTTPTemporaryRedirect``, etc.
+We'll also let the method raise one of these exceptions and turn it
+into a response.
+
+One last thing we'll do in our ``__call__`` method is create our
+``Page`` object, which represents a wiki page.
+
+All this together makes:
+
+.. code-block:: python
+
+ from webob import Request, Response
+ from webob import exc
+
+ class WikiApp(object):
+ ...
+
+ def __call__(self, environ, start_response):
+ req = Request(environ)
+ action = req.params.get('action', 'view')
+ # Here's where we get the Page domain object:
+ page = self.get_page(req.path_info)
+ try:
+ try:
+ # The method name is action_{action_param}_{request_method}:
+ meth = getattr(self, 'action_%s_%s' % (action, req.method))
+ except AttributeError:
+ # If the method wasn't found there must be
+ # something wrong with the request:
+ raise exc.HTTPBadRequest('No such action %r' % action)
+ resp = meth(req, page)
+ except exc.HTTPException, e:
+ # The exception object itself is a WSGI application/response:
+ resp = e
+ return resp(environ, start_response)
+
+The Domain Object
+-----------------
+
+The ``Page`` domain object isn't really related to the web, but it is
+important to implementing this. Each ``Page`` is just a file on the
+filesystem. Our ``get_page`` method figures out the filename given
+the path (the path is in ``req.path_info``, which is all the path
+after the base path). The ``Page`` class handles getting and setting
+the title and content.
+
+Here's the method to figure out the filename:
+
+.. code-block:: python
+
+ import os
+
+ class WikiApp(object):
+ ...
+
+ def get_page(self, path):
+ path = path.lstrip('/')
+ if not path:
+ # The path was '/', the home page
+ path = 'index'
+ path = os.path.join(self.storage_dir)
+ path = os.path.normpath(path)
+ if path.endswith('/'):
+ path += 'index'
+ if not path.startswith(self.storage_dir):
+ raise exc.HTTPBadRequest("Bad path")
+ path += '.html'
+ return Page(path)
+
+Mostly this is just the kind of careful path construction you have to
+do when mapping a URL to a filename. While the server *may* normalize
+the path (so that a path like ``/../../`` can't be requested), you can
+never really be sure. By using ``os.path.normpath`` we eliminate
+these, and then we make absolutely sure that the resulting path is
+under our ``self.storage_dir`` with ``if not
+path.startswith(self.storage_dir): raise exc.HTTPBadRequest("Bad
+path")``.
+
+Here's the actual domain object:
+
+.. code-block:: python
+
+ class Page(object):
+ def __init__(self, filename):
+ self.filename = filename
+
+ @property
+ def exists(self):
+ return os.path.exists(self.filename)
+
+ @property
+ def title(self):
+ if not self.exists:
+ # we need to guess the title
+ basename = os.path.splitext(os.path.basename(self.filename))[0]
+ basename = re.sub(r'[_-]', ' ', basename)
+ return basename.capitalize()
+ content = self.full_content
+ match = re.search(r'<title>(.*?)</title>', content, re.I|re.S)
+ return match.group(1)
+
+ @property
+ def full_content(self):
+ f = open(self.filename, 'rb')
+ try:
+ return f.read()
+ finally:
+ f.close()
+
+ @property
+ def content(self):
+ if not self.exists:
+ return ''
+ content = self.full_content
+ match = re.search(r'<body[^>]*>(.*?)</body>', content, re.I|re.S)
+ return match.group(1)
+
+ @property
+ def mtime(self):
+ if not self.exists:
+ return None
+ else:
+ return os.stat(self.filename).st_mtime
+
+ def set(self, title, content):
+ dir = os.path.dirname(self.filename)
+ if not os.path.exists(dir):
+ os.makedirs(dir)
+ new_content = """<html><head><title>%s</title></head><body>%s</body></html>""" % (
+ title, content)
+ f = open(self.filename, 'wb')
+ f.write(new_content)
+ f.close()
+
+Basically it provides a ``.title`` attribute, a ``.content``
+attribute, the ``.mtime`` (last modified time), and the page can exist
+or not (giving appropriate guesses for title and content when the page
+does not exist). It encodes these on the filesystem as a simple HTML
+page that is parsed by some regular expressions.
+
+None of this really applies much to the web or WebOb, so I'll leave it
+to you to figure out the details of this.
+
+URLs, PATH_INFO, and SCRIPT_NAME
+--------------------------------
+
+This is an aside for the tutorial, but an important concept. In WSGI,
+and accordingly with WebOb, the URL is split up into several pieces.
+Some of these are obvious and some not.
+
+An example::
+
+ http://example.com:8080/wiki/article/12?version=10
+
+There are several components here:
+
+* req.scheme: ``http``
+* req.host: ``example.com:8080``
+* req.server_name: ``example.com``
+* req.server_port: 8080
+* req.script_name: ``/wiki``
+* req.path_info: ``/article/12``
+* req.query_string: ``version=10``
+
+One non-obvious part is ``req.script_name`` and ``req.path_info``.
+These correspond to the CGI environmental variables ``SCRIPT_NAME``
+and ``PATH_INFO``. ``req.script_name`` points to the *application*.
+You might have several applications in your site at different paths:
+one at ``/wiki``, one at ``/blog``, one at ``/``. Each application
+doesn't necessarily know about the others, but it has to construct its
+URLs properly -- so any internal links to the wiki application should
+start with ``/wiki``.
+
+Just as there are pieces to the URL, there are several properties in
+WebOb to construct URLs based on these:
+
+* req.host_url: ``http://example.com:8080``
+* req.application_url: ``http://example.com:8080/wiki``
+* req.path_url: ``http://example.com:8080/wiki/article/12``
+* req.path: ``/wiki/article/12``
+* req.path_qs: ``/wiki/article/12?version=10``
+* req.url: ``http://example.com:8080/wiki/article/12?version10``
+
+You can also create URLs with
+``req.relative_url('some/other/page')``. In this example that would
+resolve to ``http://example.com:8080/wiki/article/some/other/page``.
+You can also create a relative URL to the application URL
+(SCRIPT_NAME) like ``req.relative_url('some/other/page', True)`` which
+would be ``http://example.com:8080/wiki/some/other/page``.
+
+Back to the Application
+-----------------------
+
+We have a dispatching function with ``__call__`` and we have a domain
+object with ``Page``, but we aren't actually doing anything.
+
+The dispatching goes to ``action_ACTION_METHOD``, where ACTION
+defaults to ``view``. So a simple page view will be
+``action_view_GET``. Let's implement that:
+
+.. code-block:: python
+
+ class WikiApp(object):
+ ...
+
+ def action_view_GET(self, req, page):
+ if not page.exists:
+ return exc.HTTPTemporaryRedirect(
+ location=req.url + '?action=edit')
+ text = self.view_template.substitute(
+ page=page, req=req)
+ resp = Response(text)
+ resp.last_modified = page.mtime
+ resp.conditional_response = True
+ return resp
+
+The first thing we do is redirect the user to the edit screen if the
+page doesn't exist. ``exc.HTTPTemporaryRedirect`` is a response that
+gives a ``307 Temporary Redirect`` response with the given location.
+
+Otherwise we fill in a template. The template language we're going to
+use in this example is `Tempita <http://pythonpaste.org/tempita/>`_, a
+very simple template language with a similar interface to
+`string.Template <http://python.org/doc/current/lib/node40.html>`_.
+
+The template actually looks like this:
+
+.. code-block:: python
+
+ from tempita import HTMLTemplate
+
+ VIEW_TEMPLATE = HTMLTemplate("""\
+ <html>
+ <head>
+ <title>{{page.title}}</title>
+ </head>
+ <body>
+ <h1>{{page.title}}</h1>
+
+ <div>{{page.content|html}}</div>
+
+ <hr>
+ <a href="{{req.url}}?action=edit">Edit</a>
+ </body>
+ </html>
+ """)
+
+ class WikiApp(object):
+ view_template = VIEW_TEMPLATE
+ ...
+
+As you can see it's a simple template using the title and the body,
+and a link to the edit screen. We copy the template object into a
+class method (``view_template = VIEW_TEMPLATE``) so that potentially a
+subclass could override these templates.
+
+``tempita.HTMLTemplate`` is a template that does automatic HTML
+escaping. Our wiki will just be written in plain HTML, so we disable
+escaping of the content with ``{{page.content|html}}``.
+
+So let's look at the ``action_view_GET`` method again:
+
+.. code-block:: python
+
+ def action_view_GET(self, req, page):
+ if not page.exists:
+ return exc.HTTPTemporaryRedirect(
+ location=req.url + '?action=edit')
+ text = self.view_template.substitute(
+ page=page, req=req)
+ resp = Response(text)
+ resp.last_modified = page.mtime
+ resp.conditional_response = True
+ return resp
+
+The template should be pretty obvious now. We create a response with
+``Response(text)``, which already has a default Content-Type of
+``text/html``.
+
+To allow conditional responses we set ``resp.last_modified``. You can
+set this attribute to a date, None (effectively removing the header),
+a time tuple (like produced by ``time.localtime()``), or as in this
+case to an integer timestamp. If you get the value back it will
+always be a `datetime
+<http://python.org/doc/current/lib/datetime-datetime.html>`_ object
+(or None). With this header we can process requests with
+If-Modified-Since headers, and return ``304 Not Modified`` if
+appropriate. It won't actually do that unless you set
+``resp.conditional_response`` to True.
+
+.. note::
+
+ If you subclass ``webob.Response`` you can set the class attribute
+ ``default_conditional_response = True`` and this setting will be
+ on by default. You can also set other defaults, like the
+ ``default_charset`` (``"utf8"``), or ``default_content_type``
+ (``"text/html"``).
+
+The Edit Screen
+---------------
+
+The edit screen will be implemented in the method
+``action_edit_GET``. There's a template and a very simple method:
+
+.. code-block:: python
+
+ EDIT_TEMPLATE = HTMLTemplate("""\
+ <html>
+ <head>
+ <title>Edit: {{page.title}}</title>
+ </head>
+ <body>
+ {{if page.exists}}
+ <h1>Edit: {{page.title}}</h1>
+ {{else}}
+ <h1>Create: {{page.title}}</h1>
+ {{endif}}
+
+ <form action="{{req.path_url}}" method="POST">
+ <input type="hidden" name="mtime" value="{{page.mtime}}">
+ Title: <input type="text" name="title" style="width: 70%" value="{{page.title}}"><br>
+ Content: <input type="submit" value="Save">
+ <a href="{{req.path_url}}">Cancel</a>
+ <br>
+ <textarea name="content" style="width: 100%; height: 75%" rows="40">{{page.content}}</textarea>
+ <br>
+ <input type="submit" value="Save">
+ <a href="{{req.path_url}}">Cancel</a>
+ </form>
+ </body></html>
+ """)
+
+ class WikiApp(object):
+ ...
+
+ edit_template = EDIT_TEMPLATE
+
+ def action_edit_GET(self, req, page):
+ text = self.edit_template.substitute(
+ page=page, req=req)
+ return Response(text)
+
+As you can see, all the action here is in the template.
+
+In ``<form action="{{req.path_url}}" method="POST">`` we submit to
+``req.path_url``; that's everything *but* ``?action=edit``. So we are
+POSTing right over the view page. This has the nice side effect of
+automatically invalidating any caches of the original page. It also
+is vaguely `RESTful
+<http://en.wikipedia.org/wiki/Representational_State_Transfer>`_.
+
+We save the last modified time in a hidden ``mtime`` field. This way
+we can detect concurrent updates. If start editing the page who's
+mtime is 100000, and someone else edits and saves a revision changing
+the mtime to 100010, we can use this hidden field to detect that
+conflict. Actually resolving the conflict is a little tricky and
+outside the scope of this particular tutorial, we'll just note the
+conflict to the user in an error.
+
+From there we just have a very straight-forward HTML form. Note that
+we don't quote the values because that is done automatically by
+``HTMLTemplate``; if you are using something like ``string.Template``
+or a templating language that doesn't do automatic quoting, you have
+to be careful to quote all the field values.
+
+We don't have any error conditions in our application, but if there
+were error conditions we might have to re-display this form with the
+input values the user already gave. In that case we'd do something
+like::
+
+ <input type="text" name="title"
+ value="{{req.params.get('title', page.title)}}">
+
+This way we use the value in the request (``req.params`` is both the
+query string parameters and any variables in a POST response), but if
+there is no value (e.g., first request) then we use the page values.
+
+Processing the Form
+-------------------
+
+The form submits to ``action_view_POST`` (``view`` is the default
+action). So we have to implement that method:
+
+.. code-block:: python
+
+ class WikiApp(object):
+ ...
+
+ def action_view_POST(self, req, page):
+ submit_mtime = int(req.params.get('mtime') or '0') or None
+ if page.mtime != submit_mtime:
+ return exc.HTTPPreconditionFailed(
+ "The page has been updated since you started editing it")
+ page.set(
+ title=req.params['title'],
+ content=req.params['content'])
+ resp = exc.HTTPSeeOther(
+ location=req.path_url)
+ return resp
+
+The first thing we do is check the mtime value. It can be an empty
+string (when there's no mtime, like when you are creating a page) or
+an integer. ``int(req.params.get('time') or '0') or None`` basically
+makes sure we don't pass ``""`` to ``int()`` (which is an error) then
+turns 0 into None (``0 or None`` will evaluate to None in Python --
+``false_value or other_value`` in Python resolves to ``other_value``).
+If it fails we just give a not-very-helpful error message, using ``412
+Precondition Failed`` (typically preconditions are HTTP headers like
+``If-Unmodified-Since``, but we can't really get the browser to send
+requests like that, so we use the hidden field instead).
+
+.. note::
+
+ Error statuses in HTTP are often under-used because people think
+ they need to either return an error (useful for machines) or an
+ error message or interface (useful for humans). In fact you can
+ do both: you can give any human readable error message with your
+ error response.
+
+ One problem is that Internet Explorer will replace error messages
+ with its own incredibly unhelpful error messages. However, it
+ will only do this if the error message is short. If it's fairly
+ large (4Kb is large enough) it will show the error message it was
+ given. You can load your error with a big HTML comment to
+ accomplish this, like ``"<!-- %s -->" % ('x'*4000)``.
+
+ You can change the status of any response with ``resp.status_int =
+ 412``, or you can change the body of an ``exc.HTTPSomething`` with
+ ``resp.body = new_body``. The primary advantage of using the
+ classes in ``webob.exc`` is giving the response a clear name and a
+ boilerplate error message.
+
+After we check the mtime we get the form parameters from
+``req.params`` and issue a redirect back to the original view page.
+``303 See Other`` is a good response to give after accepting a POST
+form submission, as it gets rid of the POST (no warning messages for the
+user if they try to go back).
+
+In this example we've used ``req.params`` for all the form values. If
+we wanted to be specific about where we get the values from, they
+could come from ``req.GET`` (the query string, a misnomer since the
+query string is present even in POST requests) or ``req.POST`` (a POST
+form body). While sometimes it's nice to distinguish between these
+two locations, for the most part it doesn't matter. If you want to
+check the request method (e.g., make sure you can't change a page with
+a GET request) there's no reason to do it by accessing these
+method-specific getters. It's better to just handle the method
+specifically. We do it here by including the request method in our
+dispatcher (dispatching to ``action_view_GET`` or
+``action_view_POST``).
+
+
+Cookies
+-------
+
+One last little improvement we can do is show the user a message when
+they update the page, so it's not quite so mysteriously just another
+page view.
+
+A simple way to do this is to set a cookie after the save, then
+display it in the page view. To set it on save, we add a little to
+``action_view_POST``:
+
+.. code-block:: python
+
+ def action_view_POST(self, req, page):
+ ...
+ resp = exc.HTTPSeeOther(
+ location=req.path_url)
+ resp.set_cookie('message', 'Page updated')
+ return resp
+
+And then in ``action_view_GET``:
+
+.. code-block:: python
+
+
+ VIEW_TEMPLATE = HTMLTemplate("""\
+ ...
+ {{if message}}
+ <div style="background-color: #99f">{{message}}</div>
+ {{endif}}
+ ...""")
+
+ class WikiApp(object):
+ ...
+
+ def action_view_GET(self, req, page):
+ ...
+ if req.cookies.get('message'):
+ message = req.cookies['message']
+ else:
+ message = None
+ text = self.view_template.substitute(
+ page=page, req=req, message=message)
+ resp = Response(text)
+ if message:
+ resp.delete_cookie('message')
+ else:
+ resp.last_modified = page.mtime
+ resp.conditional_response = True
+ return resp
+
+``req.cookies`` is just a dictionary, and we also delete the cookie if
+it is present (so the message doesn't keep getting set). The
+conditional response stuff only applies when there isn't any
+message, as messages are private. Another alternative would be to
+display the message with Javascript, like::
+
+ <script type="text/javascript">
+ function readCookie(name) {
+ var nameEQ = name + "=";
+ var ca = document.cookie.split(';');
+ for (var i=0; i < ca.length; i++) {
+ var c = ca[i];
+ while (c.charAt(0) == ' ') c = c.substring(1,c.length);
+ if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length,c.length);
+ }
+ return null;
+ }
+
+ function createCookie(name, value, days) {
+ if (days) {
+ var date = new Date();
+ date.setTime(date.getTime()+(days*24*60*60*1000));
+ var expires = "; expires="+date.toGMTString();
+ } else {
+ var expires = "";
+ }
+ document.cookie = name+"="+value+expires+"; path=/";
+ }
+
+ function eraseCookie(name) {
+ createCookie(name, "", -1);
+ }
+
+ function showMessage() {
+ var message = readCookie('message');
+ if (message) {
+ var el = document.getElementById('message');
+ el.innerHTML = message;
+ el.style.display = '';
+ eraseCookie('message');
+ }
+ }
+ </script>
+
+Then put ``<div id="messaage" style="display: none"></div>`` in the
+page somewhere. This has the advantage of being very cacheable and
+simple on the server side.
+
+Conclusion
+----------
+
+We're done, hurrah!
diff --git a/lib/webob_1_1_1/setup.cfg b/lib/webob_1_1_1/setup.cfg
new file mode 100644
index 0000000..97bee09
--- /dev/null
+++ b/lib/webob_1_1_1/setup.cfg
@@ -0,0 +1,14 @@
+[aliases]
+distribute = register sdist upload
+
+[nosetests]
+detailed-errors = True
+cover-erase = True
+cover-package = webob
+nocapture = True
+
+[egg_info]
+tag_build =
+tag_date = 0
+tag_svn_revision = 0
+
diff --git a/lib/webob_1_1_1/setup.py b/lib/webob_1_1_1/setup.py
new file mode 100644
index 0000000..3845e74
--- /dev/null
+++ b/lib/webob_1_1_1/setup.py
@@ -0,0 +1,50 @@
+from setuptools import setup
+
+version = '1.1.1'
+
+setup(
+ name='WebOb',
+ version=version,
+ description="WSGI request and response object",
+ long_description="""\
+WebOb provides wrappers around the WSGI request environment, and an
+object to help create WSGI responses.
+
+The objects map much of the specified behavior of HTTP, including
+header parsing and accessors for other standard parts of the
+environment.
+
+You may install the `in-development version of WebOb
+<http://bitbucket.org/ianb/webob/get/tip.gz#egg=WebOb-dev>`_ with
+``pip install WebOb==dev`` (or ``easy_install WebOb==dev``).
+
+* `WebOb reference <http://docs.webob.org/en/latest/reference.html>`_
+* `Bug tracker <https://bitbucket.org/ianb/webob/issues>`_
+* `Browse source code <https://bitbucket.org/ianb/webob/src>`_
+* `Mailing list <http://bit.ly/paste-users>`_
+* `Release news <http://docs.webob.org/en/latest/news.html>`_
+* `Detailed changelog <https://bitbucket.org/ianb/webob/changesets>`_
+""",
+ classifiers=[
+ "Development Status :: 6 - Mature",
+ "Intended Audience :: Developers",
+ "License :: OSI Approved :: MIT License",
+ "Topic :: Internet :: WWW/HTTP :: WSGI",
+ "Topic :: Internet :: WWW/HTTP :: WSGI :: Application",
+ "Topic :: Internet :: WWW/HTTP :: WSGI :: Middleware",
+ "Programming Language :: Python :: 2.5",
+ "Programming Language :: Python :: 2.6",
+ "Programming Language :: Python :: 2.7",
+ ],
+ keywords='wsgi request web http',
+ author='Ian Bicking',
+ author_email='ianb@colorstudy.com',
+ maintainer='Sergey Schetinin',
+ maintainer_email='sergey@maluke.com',
+ url='http://webob.org/',
+ license='MIT',
+ packages=['webob'],
+ zip_safe=True,
+ test_suite='nose.collector',
+ tests_require=['nose', 'WebTest'],
+)
diff --git a/lib/webob/tests/__init__.py b/lib/webob_1_1_1/tests/__init__.py
old mode 100755
new mode 100644
similarity index 100%
copy from lib/webob/tests/__init__.py
copy to lib/webob_1_1_1/tests/__init__.py
diff --git a/lib/webob/tests/conftest.py b/lib/webob_1_1_1/tests/conftest.py
old mode 100755
new mode 100644
similarity index 100%
copy from lib/webob/tests/conftest.py
copy to lib/webob_1_1_1/tests/conftest.py
diff --git a/lib/webob_1_1_1/tests/performance_test.py b/lib/webob_1_1_1/tests/performance_test.py
new file mode 100644
index 0000000..79f01af
--- /dev/null
+++ b/lib/webob_1_1_1/tests/performance_test.py
@@ -0,0 +1,50 @@
+#!/usr/bin/env python
+import webob
+
+def make_middleware(app):
+ from repoze.profile.profiler import AccumulatingProfileMiddleware
+ return AccumulatingProfileMiddleware(
+ app,
+ log_filename='/tmp/profile.log',
+ discard_first_request=True,
+ flush_at_shutdown=True,
+ path='/__profile__')
+
+def simple_app(environ, start_response):
+ resp = webob.Response('Hello world!')
+ return resp(environ, start_response)
+
+if __name__ == '__main__':
+ import sys
+ import os
+ import signal
+ if sys.argv[1:]:
+ arg = sys.argv[1]
+ else:
+ arg = None
+ if arg in ['open', 'run']:
+ import subprocess
+ import webbrowser
+ import time
+ os.environ['SHOW_OUTPUT'] = '0'
+ proc = subprocess.Popen([sys.executable, __file__])
+ time.sleep(1)
+ subprocess.call(['ab', '-n', '1000', 'http://localhost:8080/'])
+ if arg == 'open':
+ webbrowser.open('http://localhost:8080/__profile__')
+ print 'Hit ^C to end'
+ try:
+ while 1:
+ raw_input()
+ finally:
+ os.kill(proc.pid, signal.SIGKILL)
+ else:
+ from paste.httpserver import serve
+ if os.environ.get('SHOW_OUTPUT') != '0':
+ print 'Note you can also use:'
+ print ' %s %s open' % (sys.executable, __file__)
+ print 'to run ab and open a browser (or "run" to just run ab)'
+ print 'Now do:'
+ print 'ab -n 1000 http://localhost:8080/'
+ print 'wget -O - http://localhost:8080/__profile__'
+ serve(make_middleware(simple_app))
diff --git a/lib/webob_1_1_1/tests/test_acceptparse.py b/lib/webob_1_1_1/tests/test_acceptparse.py
new file mode 100644
index 0000000..bf951eb
--- /dev/null
+++ b/lib/webob_1_1_1/tests/test_acceptparse.py
@@ -0,0 +1,320 @@
+from webob import Request
+from webob.acceptparse import Accept, MIMEAccept, NilAccept, NoAccept, accept_property, AcceptLanguage, AcceptCharset
+from nose.tools import eq_, assert_raises
+
+def test_parse_accept_badq():
+ assert list(Accept.parse("value1; q=0.1.2")) == [('value1', 1)]
+
+def test_init_accept_content_type():
+ accept = Accept('text/html')
+ assert accept._parsed == [('text/html', 1)]
+
+def test_init_accept_accept_charset():
+ accept = AcceptCharset('iso-8859-5, unicode-1-1;q=0.8')
+ assert accept._parsed == [('iso-8859-5', 1),
+ ('unicode-1-1', 0.80000000000000004),
+ ('iso-8859-1', 1)]
+
+def test_init_accept_accept_charset_with_iso_8859_1():
+ accept = Accept('iso-8859-1')
+ assert accept._parsed == [('iso-8859-1', 1)]
+
+def test_init_accept_accept_charset_wildcard():
+ accept = Accept('*')
+ assert accept._parsed == [('*', 1)]
+
+def test_init_accept_accept_language():
+ accept = AcceptLanguage('da, en-gb;q=0.8, en;q=0.7')
+ assert accept._parsed == [('da', 1),
+ ('en-gb', 0.80000000000000004),
+ ('en', 0.69999999999999996)]
+
+def test_init_accept_invalid_value():
+ accept = AcceptLanguage('da, q, en-gb;q=0.8')
+ # The "q" value should not be there.
+ assert accept._parsed == [('da', 1),
+ ('en-gb', 0.80000000000000004)]
+
+def test_init_accept_invalid_q_value():
+ accept = AcceptLanguage('da, en-gb;q=foo')
+ # I can't get to cover line 40-41 (webob.acceptparse) as the regex
+ # will prevent from hitting these lines (aconrad)
+ assert accept._parsed == [('da', 1), ('en-gb', 1)]
+
+def test_accept_repr():
+ accept = Accept('text/html')
+ assert repr(accept) == "<Accept('text/html')>"
+
+def test_accept_str():
+ accept = Accept('text/html')
+ assert str(accept) == 'text/html'
+
+def test_zero_quality():
+ assert Accept('bar, *;q=0').best_match(['foo']) is None
+ assert 'foo' not in Accept('*;q=0')
+ assert Accept('foo, *;q=0').first_match(['bar', 'foo']) == 'foo'
+
+
+def test_accept_str_with_q_not_1():
+ value = 'text/html;q=0.5'
+ accept = Accept(value)
+ assert str(accept) == value
+
+def test_accept_str_with_q_not_1_multiple():
+ value = 'text/html;q=0.5, foo/bar'
+ accept = Accept(value)
+ assert str(accept) == value
+
+def test_accept_add_other_accept():
+ accept = Accept('text/html') + Accept('foo/bar')
+ assert str(accept) == 'text/html, foo/bar'
+ accept += Accept('bar/baz;q=0.5')
+ assert str(accept) == 'text/html, foo/bar, bar/baz;q=0.5'
+
+def test_accept_add_other_list_of_tuples():
+ accept = Accept('text/html')
+ accept += [('foo/bar', 1)]
+ assert str(accept) == 'text/html, foo/bar'
+ accept += [('bar/baz', 0.5)]
+ assert str(accept) == 'text/html, foo/bar, bar/baz;q=0.5'
+ accept += ['she/bangs', 'the/house']
+ assert str(accept) == ('text/html, foo/bar, bar/baz;q=0.5, '
+ 'she/bangs, the/house')
+
+def test_accept_add_other_dict():
+ accept = Accept('text/html')
+ accept += {'foo/bar': 1}
+ assert str(accept) == 'text/html, foo/bar'
+ accept += {'bar/baz': 0.5}
+ assert str(accept) == 'text/html, foo/bar, bar/baz;q=0.5'
+
+def test_accept_add_other_empty_str():
+ accept = Accept('text/html')
+ accept += ''
+ assert str(accept) == 'text/html'
+
+def test_accept_with_no_value_add_other_str():
+ accept = Accept('')
+ accept += 'text/html'
+ assert str(accept) == 'text/html'
+
+def test_contains():
+ accept = Accept('text/html')
+ assert 'text/html' in accept
+
+def test_contains_not():
+ accept = Accept('text/html')
+ assert not 'foo/bar' in accept
+
+def test_quality():
+ accept = Accept('text/html')
+ assert accept.quality('text/html') == 1
+ accept = Accept('text/html;q=0.5')
+ assert accept.quality('text/html') == 0.5
+
+def test_quality_not_found():
+ accept = Accept('text/html')
+ assert accept.quality('foo/bar') is None
+
+def test_first_match():
+ accept = Accept('text/html, foo/bar')
+ assert accept.first_match(['text/html', 'foo/bar']) == 'text/html'
+ assert accept.first_match(['foo/bar', 'text/html']) == 'foo/bar'
+ assert accept.first_match(['xxx/xxx', 'text/html']) == 'text/html'
+ assert accept.first_match(['xxx/xxx']) == 'xxx/xxx'
+ assert accept.first_match([None, 'text/html']) is None
+ assert accept.first_match(['text/html', None]) == 'text/html'
+ assert accept.first_match(['foo/bar', None]) == 'foo/bar'
+ assert_raises(ValueError, accept.first_match, [])
+
+def test_best_match():
+ accept = Accept('text/html, foo/bar')
+ assert accept.best_match(['text/html', 'foo/bar']) == 'text/html'
+ assert accept.best_match(['foo/bar', 'text/html']) == 'foo/bar'
+ assert accept.best_match([('foo/bar', 0.5),
+ 'text/html']) == 'text/html'
+ assert accept.best_match([('foo/bar', 0.5),
+ ('text/html', 0.4)]) == 'foo/bar'
+ assert_raises(ValueError, accept.best_match, ['text/*'])
+
+def test_best_match_with_one_lower_q():
+ accept = Accept('text/html, foo/bar;q=0.5')
+ assert accept.best_match(['text/html', 'foo/bar']) == 'text/html'
+ accept = Accept('text/html;q=0.5, foo/bar')
+ assert accept.best_match(['text/html', 'foo/bar']) == 'foo/bar'
+
+def test_best_matches():
+ accept = Accept('text/html, foo/bar')
+ assert accept.best_matches() == ['text/html', 'foo/bar']
+ accept = Accept('text/html, foo/bar;q=0.5')
+ assert accept.best_matches() == ['text/html', 'foo/bar']
+ accept = Accept('text/html;q=0.5, foo/bar')
+ assert accept.best_matches() == ['foo/bar', 'text/html']
+
+def test_best_matches_with_fallback():
+ accept = Accept('text/html, foo/bar')
+ assert accept.best_matches('xxx/yyy') == ['text/html',
+ 'foo/bar',
+ 'xxx/yyy']
+ accept = Accept('text/html;q=0.5, foo/bar')
+ assert accept.best_matches('xxx/yyy') == ['foo/bar',
+ 'text/html',
+ 'xxx/yyy']
+ assert accept.best_matches('foo/bar') == ['foo/bar']
+ assert accept.best_matches('text/html') == ['foo/bar', 'text/html']
+
+def test_accept_match():
+ for mask in ['*', 'text/html', 'TEXT/HTML']:
+ assert 'text/html' in Accept(mask)
+ assert 'text/html' not in Accept('foo/bar')
+
+def test_accept_match_lang():
+ for mask, lang in [
+ ('*', 'da'),
+ ('da', 'DA'),
+ ('en', 'en-gb'),
+ ('en-gb', 'en-gb'),
+ ('en-gb', 'en'),
+ ('en-gb', 'en_GB'),
+ ]:
+ assert lang in AcceptLanguage(mask)
+ for mask, lang in [
+ ('en-gb', 'en-us'),
+ ('en-gb', 'fr-fr'),
+ ('en-gb', 'fr'),
+ ('en', 'fr-fr'),
+ ]:
+ assert lang not in AcceptLanguage(mask)
+
+# NilAccept tests
+
+def test_nil():
+ nilaccept = NilAccept()
+ eq_(repr(nilaccept),
+ "<NilAccept: <class 'webob.acceptparse.Accept'>>"
+ )
+ assert not nilaccept
+ assert str(nilaccept) == ''
+ assert nilaccept.quality('dummy') == 0
+ assert nilaccept.best_matches() == []
+ assert nilaccept.best_matches('foo') == ['foo']
+
+
+def test_nil_add():
+ nilaccept = NilAccept()
+ accept = Accept('text/html')
+ assert nilaccept + accept is accept
+ new_accept = nilaccept + nilaccept
+ assert isinstance(new_accept, accept.__class__)
+ assert new_accept.header_value == ''
+ new_accept = nilaccept + 'foo'
+ assert isinstance(new_accept, accept.__class__)
+ assert new_accept.header_value == 'foo'
+
+def test_nil_radd():
+ nilaccept = NilAccept()
+ accept = Accept('text/html')
+ assert isinstance('foo' + nilaccept, accept.__class__)
+ assert ('foo' + nilaccept).header_value == 'foo'
+ # How to test ``if isinstance(item, self.MasterClass): return item``
+ # under NilAccept.__radd__ ??
+
+def test_nil_radd_masterclass():
+ # Is this "reaching into" __radd__ legit?
+ nilaccept = NilAccept()
+ accept = Accept('text/html')
+ assert nilaccept.__radd__(accept) is accept
+
+def test_nil_contains():
+ nilaccept = NilAccept()
+ assert 'anything' in nilaccept
+
+def test_nil_first_match():
+ nilaccept = NilAccept()
+ # NilAccept.first_match always returns element 0 of the list
+ assert nilaccept.first_match(['dummy', '']) == 'dummy'
+ assert nilaccept.first_match(['', 'dummy']) == ''
+
+def test_nil_best_match():
+ nilaccept = NilAccept()
+ assert nilaccept.best_match(['foo', 'bar']) == 'foo'
+ assert nilaccept.best_match([('foo', 1), ('bar', 0.5)]) == 'foo'
+ assert nilaccept.best_match([('foo', 0.5), ('bar', 1)]) == 'bar'
+ assert nilaccept.best_match([('foo', 0.5), 'bar']) == 'bar'
+ # default_match has no effect on NilAccept class
+ assert nilaccept.best_match([('foo', 0.5), 'bar'],
+ default_match=True) == 'bar'
+ assert nilaccept.best_match([('foo', 0.5), 'bar'],
+ default_match=False) == 'bar'
+
+
+# NoAccept tests
+def test_noaccept_contains():
+ assert 'text/plain' not in NoAccept()
+
+
+# MIMEAccept tests
+
+def test_mime_init():
+ mimeaccept = MIMEAccept('image/jpg')
+ assert mimeaccept._parsed == [('image/jpg', 1)]
+ mimeaccept = MIMEAccept('image/png, image/jpg;q=0.5')
+ assert mimeaccept._parsed == [('image/png', 1), ('image/jpg', 0.5)]
+ mimeaccept = MIMEAccept('image, image/jpg;q=0.5')
+ assert mimeaccept._parsed == [('image/jpg', 0.5)]
+ mimeaccept = MIMEAccept('*/*')
+ assert mimeaccept._parsed == [('*/*', 1)]
+ mimeaccept = MIMEAccept('*/png')
+ assert mimeaccept._parsed == []
+ mimeaccept = MIMEAccept('image/*')
+ assert mimeaccept._parsed == [('image/*', 1)]
+
+def test_accept_html():
+ mimeaccept = MIMEAccept('image/jpg')
+ assert not mimeaccept.accept_html()
+ mimeaccept = MIMEAccept('image/jpg, text/html')
+ assert mimeaccept.accept_html()
+
+def test_match():
+ mimeaccept = MIMEAccept('image/jpg')
+ assert mimeaccept._match('image/jpg', 'image/jpg')
+ assert mimeaccept._match('image/*', 'image/jpg')
+ assert mimeaccept._match('*/*', 'image/jpg')
+ assert not mimeaccept._match('text/html', 'image/jpg')
+ assert_raises(ValueError, mimeaccept._match, 'image/jpg', '*/*')
+
+def test_accept_json():
+ mimeaccept = MIMEAccept('text/html, *; q=.2, */*; q=.2')
+ assert mimeaccept.best_match(['application/json']) == 'application/json'
+
+# property tests
+
+def test_accept_property_fget():
+ desc = accept_property('Accept-Charset', '14.2')
+ req = Request.blank('/', environ={'envkey': 'envval'})
+ desc.fset(req, 'val')
+ eq_(desc.fget(req).header_value, 'val')
+
+def test_accept_property_fget_nil():
+ desc = accept_property('Accept-Charset', '14.2')
+ req = Request.blank('/')
+ eq_(type(desc.fget(req)), NilAccept)
+
+def test_accept_property_fset():
+ desc = accept_property('Accept-Charset', '14.2')
+ req = Request.blank('/', environ={'envkey': 'envval'})
+ desc.fset(req, 'baz')
+ eq_(desc.fget(req).header_value, 'baz')
+
+def test_accept_property_fset_acceptclass():
+ req = Request.blank('/', environ={'envkey': 'envval'})
+ req.accept_charset = ['utf-8', 'latin-11']
+ eq_(req.accept_charset.header_value, 'utf-8, latin-11, iso-8859-1')
+
+def test_accept_property_fdel():
+ desc = accept_property('Accept-Charset', '14.2')
+ req = Request.blank('/', environ={'envkey': 'envval'})
+ desc.fset(req, 'val')
+ assert desc.fget(req).header_value == 'val'
+ desc.fdel(req)
+ eq_(type(desc.fget(req)), NilAccept)
diff --git a/lib/webob_1_1_1/tests/test_byterange.py b/lib/webob_1_1_1/tests/test_byterange.py
new file mode 100644
index 0000000..104c247
--- /dev/null
+++ b/lib/webob_1_1_1/tests/test_byterange.py
@@ -0,0 +1,238 @@
+from webob.byterange import Range, ContentRange, _is_content_range_valid
+
+from nose.tools import assert_true, assert_false, assert_equal, assert_raises
+
+# Range class
+
+def test_satisfiable():
+ range = Range( ((0,99),) )
+ assert_true(range.satisfiable(100))
+ assert_true(range.satisfiable(99))
+
+def test_not_satisfiable():
+ range = Range.parse('bytes=-100')
+ assert_false(range.satisfiable(50))
+ range = Range.parse('bytes=100-')
+ assert_false(range.satisfiable(50))
+
+
+def test_range_for_length():
+ range = Range( ((0,99), (100,199) ) )
+ assert_equal( range.range_for_length( 'None'), None )
+
+def test_range_content_range_length_none():
+ range = Range( ((0, 100),) )
+ assert_equal( range.content_range( None ), None )
+
+def test_range_content_range_length_ok():
+ range = Range( ((0, 100),) )
+ assert_true( range.content_range( 100 ).__class__, ContentRange )
+
+def test_range_for_length_more_than_one_range():
+ # More than one range
+ range = Range( ((0,99), (100,199) ) )
+ assert_equal( range.range_for_length(100), None )
+
+def test_range_for_length_one_range_and_length_none():
+ # One range and length is None
+ range = Range( ((0,99), ) )
+ assert_equal( range.range_for_length( None ), None )
+
+def test_range_for_length_end_is_none():
+ # End is None
+ range = Range( ((0, None), ) )
+ assert_equal( range.range_for_length(100), (0,100) )
+
+def test_range_for_length_end_is_none_negative_start():
+ # End is None and start is negative
+ range = Range( ((-5, None), ) )
+ assert_equal( range.range_for_length(100), (95,100) )
+
+def test_range_start_none():
+ # Start is None
+ range = Range( ((None, 99), ) )
+ assert_equal( range.range_for_length(100), None )
+
+def test_range_str_end_none():
+ range = Range( ((0, 100), ) )
+ # Manually set test values
+ range.ranges = ( (0, None), )
+ assert_equal( str(range), 'bytes=0-' )
+
+def test_range_str_end_none_negative_start():
+ range = Range( ((0, 100), ) )
+ # Manually set test values
+ range.ranges = ( (-5, None), )
+ assert_equal( str(range), 'bytes=-5' )
+
+def test_range_str_1():
+ # Single range
+ range = Range( ((0, 100), ) )
+ assert_equal( str(range), 'bytes=0-99' )
+
+def test_range_str_2():
+ # Two ranges
+ range = Range( ((0, 100), (101, 200) ) )
+ assert_equal( str(range), 'bytes=0-99,101-199' )
+
+def test_range_str_3():
+ # Negative start
+ range = Range( ((-1, 100),) )
+ assert_raises( ValueError, range.__str__ )
+
+def test_range_str_4():
+ # Negative end
+ range = Range( ((0, 100),) )
+ # Manually set test values
+ range.ranges = ( (0, -100), )
+ assert_raises( ValueError, range.__str__ )
+
+def test_range_repr():
+ range = Range( ((0, 99),) )
+ assert_true( range.__repr__(), '<Range bytes 0-98>' )
+
+def test_parse_valid_input():
+ range = Range( ((0, 100),) )
+ assert_equal( range.parse( 'bytes=0-99' ).__class__, Range )
+
+def test_parse_missing_equals_sign():
+ range = Range( ((0, 100),) )
+ assert_equal( range.parse( 'bytes 0-99' ), None )
+
+def test_parse_invalid_units():
+ range = Range( ((0, 100),) )
+ assert_equal( range.parse( 'words=0-99' ), None )
+
+def test_parse_bytes_valid_input():
+ range = Range( ((0, 100),) )
+ assert_equal( range.parse_bytes( 'bytes=0-99' ), ('bytes', [(0,100)] ) )
+
+def test_parse_bytes_missing_equals_sign():
+ range = Range( ((0, 100),) )
+ assert_equal( range.parse_bytes( 'bytes 0-99'), None )
+
+def test_parse_bytes_missing_dash():
+ range = Range( ((0, 100),) )
+ assert_equal( range.parse_bytes( 'bytes=0 99'), None )
+
+def test_parse_bytes_null_input():
+ range = Range( ((0, 100),) )
+ assert_raises( TypeError, range.parse_bytes, '' )
+
+def test_parse_bytes_two_many_ranges():
+ range = Range( ((0, 100),) )
+ assert_equal( range.parse_bytes( 'bytes=-100,-100' ), None )
+
+def test_parse_bytes_negative_start():
+ range = Range( ((0, 100),) )
+ assert_equal( range.parse_bytes( 'bytes=-0-99' ), None )
+
+def test_parse_bytes_start_greater_than_end():
+ range = Range( ((0, 100),) )
+ assert_equal( range.parse_bytes( 'bytes=99-0'), None )
+
+def test_parse_bytes_start_greater_than_last_end():
+ range = Range( ((0, 100),) )
+ assert_equal( range.parse_bytes( 'bytes=0-99,0-199'), None )
+
+def test_parse_bytes_only_start():
+ range = Range( ((0, 100),) )
+ assert_equal( range.parse_bytes( 'bytes=0-'), ('bytes', [(0, None)]) )
+
+# ContentRange class
+
+def test_contentrange_bad_input():
+ assert_raises( ValueError, ContentRange, None, 99, None )
+
+def test_contentrange_repr():
+ contentrange = ContentRange( 0, 99, 100 )
+ assert_true( contentrange.__repr__(), '<ContentRange bytes 0-98/100>' )
+
+def test_contentrange_str_length_none():
+ contentrange = ContentRange( 0, 99, 100 )
+ contentrange.length = None
+ assert_equal( str(contentrange), 'bytes 0-98/*' )
+
+def test_contentrange_str_start_none():
+ contentrange = ContentRange( 0, 99, 100 )
+ contentrange.start = None
+ contentrange.stop = None
+ assert_equal( str(contentrange), 'bytes */100' )
+
+def test_contentrange_iter():
+ contentrange = ContentRange( 0, 99, 100 )
+ assert_true( type(contentrange.__iter__()), iter )
+
+def test_cr_parse_ok():
+ contentrange = ContentRange( 0, 99, 100 )
+ assert_true( contentrange.parse( 'bytes 0-99/100' ).__class__, ContentRange )
+
+def test_cr_parse_none():
+ contentrange = ContentRange( 0, 99, 100 )
+ assert_equal( contentrange.parse( None ), None )
+
+def test_cr_parse_no_bytes():
+ contentrange = ContentRange( 0, 99, 100 )
+ assert_equal( contentrange.parse( '0-99 100' ), None )
+
+def test_cr_parse_missing_slash():
+ contentrange = ContentRange( 0, 99, 100 )
+ assert_equal( contentrange.parse( 'bytes 0-99 100' ), None )
+
+def test_cr_parse_invalid_length():
+ contentrange = ContentRange( 0, 99, 100 )
+ assert_equal( contentrange.parse( 'bytes 0-99/xxx' ), None )
+
+def test_cr_parse_no_range():
+ contentrange = ContentRange( 0, 99, 100 )
+ assert_equal( contentrange.parse( 'bytes 0 99/100' ), None )
+
+def test_cr_parse_range_star():
+ contentrange = ContentRange( 0, 99, 100 )
+ assert_equal( contentrange.parse( 'bytes */100' ).__class__, ContentRange )
+
+def test_cr_parse_parse_problem_1():
+ contentrange = ContentRange( 0, 99, 100 )
+ assert_equal( contentrange.parse( 'bytes A-99/100' ), None )
+
+def test_cr_parse_parse_problem_2():
+ contentrange = ContentRange( 0, 99, 100 )
+ assert_equal( contentrange.parse( 'bytes 0-B/100' ), None )
+
+def test_cr_parse_content_invalid():
+ contentrange = ContentRange( 0, 99, 100 )
+ assert_equal( contentrange.parse( 'bytes 99-0/100' ), None )
+
+def test_contentrange_str_length_start():
+ contentrange = ContentRange( 0, 99, 100 )
+ assert_equal( contentrange.parse('bytes 0 99/*'), None )
+
+# _is_content_range_valid function
+
+def test_is_content_range_valid_start_none():
+ contentrange = ContentRange( 0, 99, 100 )
+ assert_equal( _is_content_range_valid( None, 99, 90), False )
+
+def test_is_content_range_valid_stop_none():
+ contentrange = ContentRange( 0, 99, 100 )
+ assert_equal( _is_content_range_valid( 99, None, 90), False )
+
+def test_is_content_range_valid_start_stop_none():
+ contentrange = ContentRange( 0, 99, 100 )
+ assert_equal( _is_content_range_valid( None, None, 90), True )
+
+def test_is_content_range_valid_start_none():
+ contentrange = ContentRange( 0, 99, 100 )
+ assert_equal( _is_content_range_valid( None, 99, 90), False )
+
+def test_is_content_range_valid_length_none():
+ contentrange = ContentRange( 0, 99, 100 )
+ assert_equal( _is_content_range_valid( 0, 99, None), True )
+
+def test_is_content_range_valid_stop_greater_than_length_response():
+ contentrange = ContentRange( 0, 99, 100 )
+ assert_equal( _is_content_range_valid( 0, 99, 90, response=True), False )
+
+def test_is_content_range_valid_stop_greater_than_length():
+ contentrange = ContentRange( 0, 99, 100 )
+ assert_equal( _is_content_range_valid( 0, 99, 90), True )
diff --git a/lib/webob_1_1_1/tests/test_cachecontrol.py b/lib/webob_1_1_1/tests/test_cachecontrol.py
new file mode 100644
index 0000000..2388020
--- /dev/null
+++ b/lib/webob_1_1_1/tests/test_cachecontrol.py
@@ -0,0 +1,266 @@
+from nose.tools import eq_
+from nose.tools import raises
+import unittest
+
+
+def test_cache_control_object_max_age_None():
+ from webob.cachecontrol import CacheControl
+ cc = CacheControl({}, 'a')
+ cc.properties['max-age'] = None
+ eq_(cc.max_age, -1)
+
+
+class TestUpdateDict(unittest.TestCase):
+
+ def setUp(self):
+ self.call_queue = []
+ def callback(args):
+ self.call_queue.append("Called with: %s" % repr(args))
+ self.callback = callback
+
+ def make_one(self, callback):
+ from webob.cachecontrol import UpdateDict
+ ud = UpdateDict()
+ ud.updated = callback
+ return ud
+
+ def test_clear(self):
+ newone = self.make_one(self.callback)
+ newone['first'] = 1
+ assert len(newone) == 1
+ newone.clear()
+ assert len(newone) == 0
+
+ def test_update(self):
+ newone = self.make_one(self.callback)
+ d = {'one' : 1 }
+ newone.update(d)
+ assert newone == d
+
+ def test_set_delete(self):
+ newone = self.make_one(self.callback)
+ newone['first'] = 1
+ assert len(self.call_queue) == 1
+ assert self.call_queue[-1] == "Called with: {'first': 1}"
+
+ del newone['first']
+ assert len(self.call_queue) == 2
+ assert self.call_queue[-1] == 'Called with: {}'
+
+ def test_setdefault(self):
+ newone = self.make_one(self.callback)
+ assert newone.setdefault('haters', 'gonna-hate') == 'gonna-hate'
+ assert len(self.call_queue) == 1
+ assert self.call_queue[-1] == "Called with: {'haters': 'gonna-hate'}", self.call_queue[-1]
+
+ # no effect if failobj is not set
+ assert newone.setdefault('haters', 'gonna-love') == 'gonna-hate'
+ assert len(self.call_queue) == 1
+
+ def test_pop(self):
+ newone = self.make_one(self.callback)
+ newone['first'] = 1
+ newone.pop('first')
+ assert len(self.call_queue) == 2
+ assert self.call_queue[-1] == 'Called with: {}', self.call_queue[-1]
+
+ def test_popitem(self):
+ newone = self.make_one(self.callback)
+ newone['first'] = 1
+ assert newone.popitem() == ('first', 1)
+ assert len(self.call_queue) == 2
+ assert self.call_queue[-1] == 'Called with: {}', self.call_queue[-1]
+
+ def test_callback_args(self):
+ assert True
+ #assert False
+
+
+class TestExistProp(unittest.TestCase):
+ """
+ Test webob.cachecontrol.exists_property
+ """
+
+ def setUp(self):
+ pass
+
+ def make_one(self):
+ from webob.cachecontrol import exists_property
+
+ class Dummy(object):
+ properties = dict(prop=1)
+ type = 'dummy'
+ prop = exists_property('prop', 'dummy')
+ badprop = exists_property('badprop', 'big_dummy')
+
+ return Dummy
+
+ def test_get_on_class(self):
+ from webob.cachecontrol import exists_property
+ Dummy = self.make_one()
+ assert isinstance(Dummy.prop, exists_property), Dummy.prop
+
+ def test_get_on_instance(self):
+ obj = self.make_one()()
+ assert obj.prop is True
+
+ @raises(AttributeError)
+ def test_type_mismatch_raise(self):
+ obj = self.make_one()()
+ obj.badprop = True
+
+ def test_set_w_value(self):
+ obj = self.make_one()()
+ obj.prop = True
+ assert obj.prop is True
+ assert obj.properties['prop'] is None
+
+ def test_del_value(self):
+ obj = self.make_one()()
+ del obj.prop
+ assert not 'prop' in obj.properties
+
+
+class TestValueProp(unittest.TestCase):
+ """
+ Test webob.cachecontrol.exists_property
+ """
+
+ def setUp(self):
+ pass
+
+ def make_one(self):
+ from webob.cachecontrol import value_property
+
+ class Dummy(object):
+ properties = dict(prop=1)
+ type = 'dummy'
+ prop = value_property('prop', 'dummy')
+ badprop = value_property('badprop', 'big_dummy')
+
+ return Dummy
+
+ def test_get_on_class(self):
+ from webob.cachecontrol import value_property
+ Dummy = self.make_one()
+ assert isinstance(Dummy.prop, value_property), Dummy.prop
+
+ def test_get_on_instance(self):
+ dummy = self.make_one()()
+ assert dummy.prop, dummy.prop
+ #assert isinstance(Dummy.prop, value_property), Dummy.prop
+
+ def test_set_on_instance(self):
+ dummy = self.make_one()()
+ dummy.prop = "new"
+ assert dummy.prop == "new", dummy.prop
+ assert dummy.properties['prop'] == "new", dict(dummy.properties)
+
+ def test_set_on_instance_bad_attribute(self):
+ dummy = self.make_one()()
+ dummy.prop = "new"
+ assert dummy.prop == "new", dummy.prop
+ assert dummy.properties['prop'] == "new", dict(dummy.properties)
+
+ def test_set_wrong_type(self):
+ from webob.cachecontrol import value_property
+ class Dummy(object):
+ properties = dict(prop=1, type='fail')
+ type = 'dummy'
+ prop = value_property('prop', 'dummy', type='failingtype')
+ dummy = Dummy()
+ def assign():
+ dummy.prop = 'foo'
+ self.assertRaises(AttributeError, assign)
+
+ def test_set_type_true(self):
+ dummy = self.make_one()()
+ dummy.prop = True
+ self.assertEquals(dummy.prop, None)
+
+ def test_set_on_instance_w_default(self):
+ dummy = self.make_one()()
+ dummy.prop = "dummy"
+ assert dummy.prop == "dummy", dummy.prop
+ #@@ this probably needs more tests
+
+ def test_del(self):
+ dummy = self.make_one()()
+ dummy.prop = 'Ian Bicking likes to skip'
+ del dummy.prop
+ assert dummy.prop == "dummy", dummy.prop
+
+
+def test_copy_cc():
+ from webob.cachecontrol import CacheControl
+ cc = CacheControl({'header':'%', "msg":'arewerichyet?'}, 'request')
+ cc2 = cc.copy()
+ assert cc.properties is not cc2.properties
+ assert cc.type is cc2.type
+
+# 212
+
+def test_serialize_cache_control_emptydict():
+ from webob.cachecontrol import serialize_cache_control
+ result = serialize_cache_control(dict())
+ assert result == ''
+
+def test_serialize_cache_control_cache_control_object():
+ from webob.cachecontrol import serialize_cache_control, CacheControl
+ result = serialize_cache_control(CacheControl({}, 'request'))
+ assert result == ''
+
+def test_serialize_cache_control_object_with_headers():
+ from webob.cachecontrol import serialize_cache_control, CacheControl
+ result = serialize_cache_control(CacheControl({'header':'a'}, 'request'))
+ assert result == 'header=a'
+
+def test_serialize_cache_control_value_is_None():
+ from webob.cachecontrol import serialize_cache_control, CacheControl
+ result = serialize_cache_control(CacheControl({'header':None}, 'request'))
+ assert result == 'header'
+
+def test_serialize_cache_control_value_needs_quote():
+ from webob.cachecontrol import serialize_cache_control, CacheControl
+ result = serialize_cache_control(CacheControl({'header':'""'}, 'request'))
+ assert result == 'header=""""'
+
+class TestCacheControl(unittest.TestCase):
+ def make_one(self, props, typ):
+ from webob.cachecontrol import CacheControl
+ return CacheControl(props, typ)
+
+ def test_ctor(self):
+ cc = self.make_one({'a':1}, 'typ')
+ self.assertEquals(cc.properties, {'a':1})
+ self.assertEquals(cc.type, 'typ')
+
+ def test_parse(self):
+ from webob.cachecontrol import CacheControl
+ cc = CacheControl.parse("public, max-age=315360000")
+ self.assertEquals(type(cc), CacheControl)
+ self.assertEquals(cc.max_age, 315360000)
+ self.assertEquals(cc.public, True)
+
+ def test_parse_updates_to(self):
+ from webob.cachecontrol import CacheControl
+ def foo(arg): return { 'a' : 1 }
+ cc = CacheControl.parse("public, max-age=315360000", updates_to=foo)
+ self.assertEquals(type(cc), CacheControl)
+ self.assertEquals(cc.max_age, 315360000)
+
+ def test_parse_valueerror_int(self):
+ from webob.cachecontrol import CacheControl
+ def foo(arg): return { 'a' : 1 }
+ cc = CacheControl.parse("public, max-age=abc")
+ self.assertEquals(type(cc), CacheControl)
+ self.assertEquals(cc.max_age, 'abc')
+
+ def test_repr(self):
+ cc = self.make_one({'a':'1'}, 'typ')
+ result = repr(cc)
+ self.assertEqual(result, "<CacheControl 'a=1'>")
+
+
+
+
diff --git a/lib/webob_1_1_1/tests/test_cookies.py b/lib/webob_1_1_1/tests/test_cookies.py
new file mode 100644
index 0000000..d091a58
--- /dev/null
+++ b/lib/webob_1_1_1/tests/test_cookies.py
@@ -0,0 +1,128 @@
+# -*- coding: utf-8 -*-
+from datetime import timedelta
+from webob import cookies
+from nose.tools import eq_
+
+def test_cookie_empty():
+ c = cookies.Cookie() # empty cookie
+ eq_(repr(c), '<Cookie: []>')
+
+def test_cookie_one_value():
+ c = cookies.Cookie('dismiss-top=6')
+ eq_(repr(c), "<Cookie: [<Morsel: dismiss-top='6'>]>")
+
+def test_cookie_one_value_with_trailing_semi():
+ c = cookies.Cookie('dismiss-top=6;')
+ eq_(repr(c), "<Cookie: [<Morsel: dismiss-top='6'>]>")
+
+def test_cookie_complex():
+ c = cookies.Cookie('dismiss-top=6; CP=null*, '\
+ 'PHPSESSID=0a539d42abc001cdc762809248d4beed, a="42,"')
+ c_dict = dict((k,v.value) for k,v in c.items())
+ eq_(c_dict, {'a': '42,',
+ 'CP': 'null*',
+ 'PHPSESSID': '0a539d42abc001cdc762809248d4beed',
+ 'dismiss-top': '6'
+ })
+
+def test_cookie_complex_serialize():
+ c = cookies.Cookie('dismiss-top=6; CP=null*, '\
+ 'PHPSESSID=0a539d42abc001cdc762809248d4beed, a="42,"')
+ eq_(c.serialize(),
+ 'CP=null*; PHPSESSID=0a539d42abc001cdc762809248d4beed; a="42\\054"; '
+ 'dismiss-top=6')
+
+def test_cookie_load_multiple():
+ c = cookies.Cookie('a=1; Secure=true')
+ eq_(repr(c), "<Cookie: [<Morsel: a='1'>]>")
+ eq_(c['a']['secure'], 'true')
+
+def test_cookie_secure():
+ c = cookies.Cookie()
+ c['foo'] = 'bar'
+ c['foo'].secure = True
+ eq_(c.serialize(), 'foo=bar; secure')
+
+def test_cookie_httponly():
+ c = cookies.Cookie()
+ c['foo'] = 'bar'
+ c['foo'].httponly = True
+ eq_(c.serialize(), 'foo=bar; HttpOnly')
+
+def test_cookie_reserved_keys():
+ c = cookies.Cookie('dismiss-top=6; CP=null*; $version=42; a=42')
+ assert '$version' not in c
+ c = cookies.Cookie('$reserved=42; a=$42')
+ eq_(c.keys(), ['a'])
+
+def test_serialize_cookie_date():
+ """
+ Testing webob.cookies.serialize_cookie_date.
+ Missing scenarios:
+ * input value is an str, should be returned verbatim
+ * input value is an int, should be converted to timedelta and we
+ should continue the rest of the process
+ """
+ eq_(cookies.serialize_cookie_date('Tue, 04-Jan-2011 13:43:50 GMT'),
+ 'Tue, 04-Jan-2011 13:43:50 GMT')
+ eq_(cookies.serialize_cookie_date(None), None)
+ cdate_delta = cookies.serialize_cookie_date(timedelta(seconds=10))
+ cdate_int = cookies.serialize_cookie_date(10)
+ eq_(cdate_delta, cdate_int)
+
+def test_ch_unquote():
+ eq_(cookies._unquote(u'"hello world'), u'"hello world')
+ eq_(cookies._unquote(u'hello world'), u'hello world')
+ for unq, q in [
+ ('hello world', '"hello world"'),
+ # quotation mark is escaped w/ backslash
+ ('"', r'"\""'),
+ # misc byte escaped as octal
+ ('\xff', r'"\377"'),
+ # combination
+ ('a"\xff', r'"a\"\377"'),
+ ]:
+ eq_(cookies._unquote(q), unq)
+ eq_(cookies._quote(unq), q)
+
+def test_cookie_setitem_needs_quoting():
+ c = cookies.Cookie()
+ c['La Pe\xc3\xb1a'] = '1'
+ eq_(len(c), 0)
+
+def test_morsel_serialize_with_expires():
+ morsel = cookies.Morsel('bleh', 'blah')
+ morsel.expires = 'Tue, 04-Jan-2011 13:43:50 GMT'
+ result = morsel.serialize()
+ eq_(result, 'bleh=blah; expires=Tue, 04-Jan-2011 13:43:50 GMT')
+
+def test_serialize_max_age_timedelta():
+ import datetime
+ val = datetime.timedelta(86400)
+ result = cookies.serialize_max_age(val)
+ eq_(result, '7464960000')
+
+def test_serialize_max_age_int():
+ val = 86400
+ result = cookies.serialize_max_age(val)
+ eq_(result, '86400')
+
+def test_serialize_max_age_str():
+ val = '86400'
+ result = cookies.serialize_max_age(val)
+ eq_(result, '86400')
+
+def test_escape_comma():
+ c = cookies.Cookie()
+ c['x'] = '";,"'
+ eq_(c.serialize(True), r'x="\"\073\054\""')
+
+def test_parse_qmark_in_val():
+ v = r'x="\"\073\054\""; expires=Sun, 12-Jun-2011 23:16:01 GMT'
+ c = cookies.Cookie(v)
+ eq_(c['x'].value, r'";,"')
+
+def test_parse_expires_no_quoting():
+ v = r'x="\"\073\054\""; expires=Sun, 12-Jun-2011 23:16:01 GMT'
+ c = cookies.Cookie(v)
+ eq_(c['x'].expires, 'Sun, 12-Jun-2011 23:16:01 GMT')
diff --git a/lib/webob_1_1_1/tests/test_datetime_utils.py b/lib/webob_1_1_1/tests/test_datetime_utils.py
new file mode 100644
index 0000000..9c19d80
--- /dev/null
+++ b/lib/webob_1_1_1/tests/test_datetime_utils.py
@@ -0,0 +1,116 @@
+# -*- coding: utf-8 -*-
+
+import datetime
+import calendar
+from email.utils import formatdate
+from webob import datetime_utils
+from nose.tools import ok_, eq_, assert_raises
+
+def test_UTC():
+ """Test missing function in _UTC"""
+ x = datetime_utils.UTC
+ ok_(x.tzname(datetime.datetime.now())=='UTC')
+ eq_(x.dst(datetime.datetime.now()), datetime.timedelta(0))
+ eq_(x.utcoffset(datetime.datetime.now()), datetime.timedelta(0))
+ eq_(repr(x), 'UTC')
+
+def test_parse_date():
+ """Testing datetime_utils.parse_date.
+ We need to verify the following scenarios:
+ * a nil submitted value
+ * a submitted value that cannot be parse into a date
+ * a valid RFC2822 date with and without timezone
+ """
+ ret = datetime_utils.parse_date(None)
+ ok_(ret is None, "We passed a None value "
+ "to parse_date. We should get None but instead we got %s" %\
+ ret)
+ ret = datetime_utils.parse_date(u'Hi There')
+ ok_(ret is None, "We passed an invalid value "
+ "to parse_date. We should get None but instead we got %s" %\
+ ret)
+ ret = datetime_utils.parse_date(1)
+ ok_(ret is None, "We passed an invalid value "
+ "to parse_date. We should get None but instead we got %s" %\
+ ret)
+ ret = datetime_utils.parse_date(u'á')
+ ok_(ret is None, "We passed an invalid value "
+ "to parse_date. We should get None but instead we got %s" %\
+ ret)
+ ret = datetime_utils.parse_date('Mon, 20 Nov 1995 19:12:08 -0500')
+ eq_(ret, datetime.datetime(
+ 1995, 11, 21, 0, 12, 8, tzinfo=datetime_utils.UTC))
+ ret = datetime_utils.parse_date('Mon, 20 Nov 1995 19:12:08')
+ eq_(ret,
+ datetime.datetime(1995, 11, 20, 19, 12, 8, tzinfo=datetime_utils.UTC))
+
+def test_serialize_date():
+ """Testing datetime_utils.serialize_date
+ We need to verify the following scenarios:
+ * passing an unicode date, return the same date but str
+ * passing a timedelta, return now plus the delta
+ * passing an invalid object, should raise ValueError
+ """
+ ret = datetime_utils.serialize_date(u'Mon, 20 Nov 1995 19:12:08 GMT')
+ assert type(ret) is (str)
+ eq_(ret, 'Mon, 20 Nov 1995 19:12:08 GMT')
+ dt = formatdate(
+ calendar.timegm(
+ (datetime.datetime.now()+datetime.timedelta(1)).timetuple()), usegmt=True)
+ eq_(dt, datetime_utils.serialize_date(datetime.timedelta(1)))
+ assert_raises(ValueError, datetime_utils.serialize_date, None)
+
+def test_parse_date_delta():
+ """Testing datetime_utils.parse_date_delta
+ We need to verify the following scenarios:
+ * passing a nil value, should return nil
+ * passing a value that fails the conversion to int, should call
+ parse_date
+ """
+ ok_(datetime_utils.parse_date_delta(None) is None, 'Passing none value, '
+ 'should return None')
+ ret = datetime_utils.parse_date_delta('Mon, 20 Nov 1995 19:12:08 -0500')
+ eq_(ret, datetime.datetime(
+ 1995, 11, 21, 0, 12, 8, tzinfo=datetime_utils.UTC))
+ WHEN = datetime.datetime(2011, 3, 16, 10, 10, 37, tzinfo=datetime_utils.UTC)
+ #with _NowRestorer(WHEN): Dammit, only Python 2.5 w/ __future__
+ nr = _NowRestorer(WHEN)
+ nr.__enter__()
+ try:
+ ret = datetime_utils.parse_date_delta(1)
+ eq_(ret, WHEN + datetime.timedelta(0, 1))
+ finally:
+ nr.__exit__(None, None, None)
+
+
+def test_serialize_date_delta():
+ """Testing datetime_utils.serialize_date_delta
+ We need to verify the following scenarios:
+ * if we pass something that's not an int or float, it should delegate
+ the task to serialize_date
+ """
+ eq_(datetime_utils.serialize_date_delta(1), '1')
+ eq_(datetime_utils.serialize_date_delta(1.5), '1')
+ ret = datetime_utils.serialize_date_delta(u'Mon, 20 Nov 1995 19:12:08 GMT')
+ assert type(ret) is (str)
+ eq_(ret, 'Mon, 20 Nov 1995 19:12:08 GMT')
+
+def test_timedelta_to_seconds():
+ val = datetime.timedelta(86400)
+ result = datetime_utils.timedelta_to_seconds(val)
+ eq_(result, 7464960000)
+
+
+class _NowRestorer(object):
+ def __init__(self, new_now):
+ self._new_now = new_now
+ self._old_now = None
+
+ def __enter__(self):
+ import webob.datetime_utils
+ self._old_now = webob.datetime_utils._now
+ webob.datetime_utils._now = lambda: self._new_now
+
+ def __exit__(self, exc_type, exc_value, traceback):
+ import webob.datetime_utils
+ webob.datetime_utils._now = self._old_now
diff --git a/lib/webob_1_1_1/tests/test_dec.py b/lib/webob_1_1_1/tests/test_dec.py
new file mode 100644
index 0000000..fefa118
--- /dev/null
+++ b/lib/webob_1_1_1/tests/test_dec.py
@@ -0,0 +1,272 @@
+import unittest
+from webob import Request
+from webob import Response
+from webob.dec import _format_args
+from webob.dec import _func_name
+from webob.dec import wsgify
+
+class DecoratorTests(unittest.TestCase):
+ def _testit(self, app, req):
+ if isinstance(req, basestring):
+ req = Request.blank(req)
+ resp = req.get_response(app)
+ return resp
+
+ def test_wsgify(self):
+ resp_str = 'hey, this is a test: %s'
+ @wsgify
+ def test_app(req):
+ return resp_str % req.url
+ resp = self._testit(test_app, '/a url')
+ self.assertEqual(resp.body, resp_str % 'http://localhost/a%20url')
+ self.assertEqual(resp.content_length, 45)
+ self.assertEqual(resp.content_type, 'text/html')
+ self.assertEqual(resp.charset, 'UTF-8')
+ self.assertEqual('%r' % (test_app,), 'wsgify(tests.test_dec.test_app)')
+
+ def test_wsgify_empty_repr(self):
+ self.assertEqual('%r' % (wsgify(),), 'wsgify()')
+
+ def test_wsgify_args(self):
+ resp_str = 'hey hey my my'
+ @wsgify(args=(resp_str,))
+ def test_app(req, strarg):
+ return strarg
+ resp = self._testit(test_app, '/a url')
+ self.assertEqual(resp.body, resp_str)
+ self.assertEqual(resp.content_length, 13)
+ self.assertEqual(resp.content_type, 'text/html')
+ self.assertEqual(resp.charset, 'UTF-8')
+ self.assertEqual('%r' % (test_app,),
+ "wsgify(tests.test_dec.test_app, args=('%s',))" % resp_str)
+
+ def test_wsgify_kwargs(self):
+ resp_str = 'hey hey my my'
+ @wsgify(kwargs=dict(strarg=resp_str))
+ def test_app(req, strarg=''):
+ return strarg
+ resp = self._testit(test_app, '/a url')
+ self.assertEqual(resp.body, resp_str)
+ self.assertEqual(resp.content_length, 13)
+ self.assertEqual(resp.content_type, 'text/html')
+ self.assertEqual(resp.charset, 'UTF-8')
+ self.assertEqual('%r' % test_app,
+ "wsgify(tests.test_dec.test_app, "
+ "kwargs={'strarg': '%s'})" % resp_str)
+
+ def test_wsgify_raise_httpexception(self):
+ from webob.exc import HTTPBadRequest
+ @wsgify
+ def test_app(req):
+ raise HTTPBadRequest
+ resp = self._testit(test_app, '/a url')
+ self.assert_(resp.body.startswith('400 Bad Request'))
+ self.assertEqual(resp.content_type, 'text/plain')
+ self.assertEqual(resp.charset, 'UTF-8')
+ self.assertEqual('%r' % test_app,
+ "wsgify(tests.test_dec.test_app)")
+
+ def test_wsgify_no___get__(self):
+ # use a class instance instead of a fn so we wrap something w/
+ # no __get__
+ class TestApp(object):
+ def __call__(self, req):
+ return 'nothing to see here'
+ test_app = wsgify(TestApp())
+ resp = self._testit(test_app, '/a url')
+ self.assertEqual(resp.body, 'nothing to see here')
+ self.assert_(test_app.__get__(test_app) is test_app)
+
+ def test_wsgify_args_no_func(self):
+ test_app = wsgify(None, args=(1,))
+ self.assertRaises(TypeError, self._testit, test_app, '/a url')
+
+ def test_wsgify_wrong_sig(self):
+ @wsgify
+ def test_app(req):
+ return 'What have you done for me lately?'
+ req = dict()
+ self.assertRaises(TypeError, test_app, req, 1, 2)
+ self.assertRaises(TypeError, test_app, req, 1, key='word')
+
+ def test_wsgify_none_response(self):
+ @wsgify
+ def test_app(req):
+ return
+ resp = self._testit(test_app, '/a url')
+ self.assertEqual(resp.body, '')
+ self.assertEqual(resp.content_type, 'text/html')
+ self.assertEqual(resp.content_length, 0)
+
+ def test_wsgify_get(self):
+ resp_str = "What'choo talkin' about, Willis?"
+ @wsgify
+ def test_app(req):
+ return Response(resp_str)
+ resp = test_app.get('/url/path')
+ self.assertEqual(resp.body, resp_str)
+
+ def test_wsgify_post(self):
+ post_dict = dict(speaker='Robin',
+ words='Holy test coverage, Batman!')
+ @wsgify
+ def test_app(req):
+ return Response('%s: %s' % (req.POST['speaker'],
+ req.POST['words']))
+ resp = test_app.post('/url/path', post_dict)
+ self.assertEqual(resp.body, '%s: %s' % (post_dict['speaker'],
+ post_dict['words']))
+
+ def test_wsgify_request_method(self):
+ resp_str = 'Nice body!'
+ @wsgify
+ def test_app(req):
+ self.assertEqual(req.method, 'PUT')
+ return Response(req.body)
+ resp = test_app.request('/url/path', method='PUT',
+ body=resp_str)
+ self.assertEqual(resp.body, resp_str)
+ self.assertEqual(resp.content_length, 10)
+ self.assertEqual(resp.content_type, 'text/html')
+
+ def test_wsgify_undecorated(self):
+ def test_app(req):
+ return Response('whoa')
+ wrapped_test_app = wsgify(test_app)
+ self.assert_(wrapped_test_app.undecorated is test_app)
+
+ def test_wsgify_custom_request(self):
+ resp_str = 'hey, this is a test: %s'
+ class MyRequest(Request):
+ pass
+ @wsgify(RequestClass=MyRequest)
+ def test_app(req):
+ return resp_str % req.url
+ resp = self._testit(test_app, '/a url')
+ self.assertEqual(resp.body, resp_str % 'http://localhost/a%20url')
+ self.assertEqual(resp.content_length, 45)
+ self.assertEqual(resp.content_type, 'text/html')
+ self.assertEqual(resp.charset, 'UTF-8')
+ self.assertEqual('%r' % (test_app,), "wsgify(tests.test_dec.test_app, "
+ "RequestClass=<class 'tests.test_dec.MyRequest'>)")
+
+ def test_middleware(self):
+ resp_str = "These are the vars: %s"
+ @wsgify.middleware
+ def set_urlvar(req, app, **vars):
+ req.urlvars.update(vars)
+ return app(req)
+ from webob.dec import _MiddlewareFactory
+ self.assert_(set_urlvar.__class__ is _MiddlewareFactory)
+ repr = '%r' % (set_urlvar,)
+ self.assert_(repr.startswith('wsgify.middleware(<function set_urlvar at '))
+ @wsgify
+ def show_vars(req):
+ return resp_str % (sorted(req.urlvars.items()))
+ show_vars2 = set_urlvar(show_vars, a=1, b=2)
+ self.assertEqual('%r' % (show_vars2,),
+ 'wsgify.middleware(tests.test_dec.set_urlvar)'
+ '(wsgify(tests.test_dec.show_vars), a=1, b=2)')
+ resp = self._testit(show_vars2, '/path')
+ self.assertEqual(resp.body, resp_str % "[('a', 1), ('b', 2)]")
+ self.assertEqual(resp.content_type, 'text/html')
+ self.assertEqual(resp.charset, 'UTF-8')
+ self.assertEqual(resp.content_length, 40)
+
+ def test_unbound_middleware(self):
+ @wsgify
+ def test_app(req):
+ return Response('Say wha!?')
+ unbound = wsgify.middleware(None, test_app, some='thing')
+ from webob.dec import _UnboundMiddleware
+ self.assert_(unbound.__class__ is _UnboundMiddleware)
+ self.assertEqual(unbound.kw, dict(some='thing'))
+ self.assertEqual('%r' % (unbound,),
+ "wsgify.middleware(wsgify(tests.test_dec.test_app), "
+ "some='thing')")
+ @unbound
+ def middle(req, app, **kw):
+ return app(req)
+ self.assert_(middle.__class__ is wsgify)
+ self.assertEqual('%r' % (middle,),
+ "wsgify.middleware(tests.test_dec.middle)"
+ "(wsgify(tests.test_dec.test_app), some='thing')")
+
+ def test_unbound_middleware_no_app(self):
+ unbound = wsgify.middleware(None, None)
+ from webob.dec import _UnboundMiddleware
+ self.assert_(unbound.__class__ is _UnboundMiddleware)
+ self.assertEqual(unbound.kw, dict())
+ self.assertEqual('%r' % (unbound,),
+ "wsgify.middleware()")
+
+ def test_classapp(self):
+ class HostMap(dict):
+ @wsgify
+ def __call__(self, req):
+ return self[req.host.split(':')[0]]
+ app = HostMap()
+ app['example.com'] = Response('1')
+ app['other.com'] = Response('2')
+ resp = Request.blank('http://example.com/').get_response(wsgify(app))
+ self.assertEqual(resp.content_type, 'text/html')
+ self.assertEqual(resp.charset, 'UTF-8')
+ self.assertEqual(resp.content_length, 1)
+ self.assertEqual(resp.body, '1')
+
+ def test__func_name(self):
+ def func():
+ pass
+ name = _func_name(func)
+ self.assertEqual(name, 'tests.test_dec.func')
+ name = _func_name('a')
+ self.assertEqual(name, "'a'")
+ class Klass(object):
+ @classmethod
+ def classmeth(cls):
+ pass
+ def meth(self):
+ pass
+ name = _func_name(Klass)
+ self.assertEqual(name, 'tests.test_dec.Klass')
+ k = Klass()
+ kname = _func_name(k)
+ self.assert_(kname.startswith('<tests.test_dec.Klass object at 0x'))
+ name = _func_name(k.meth)
+ self.assert_(name.startswith('tests.test_dec.%s' % kname))
+ self.assert_(name.endswith('>.meth'))
+ name = _func_name(Klass.meth)
+ self.assertEqual(name, 'tests.test_dec.Klass.meth')
+ name = _func_name(Klass.classmeth)
+ self.assertEqual(name, "tests.test_dec.<class "
+ "'tests.test_dec.Klass'>.classmeth")
+
+ def test__format_args(self):
+ args_rep = _format_args()
+ self.assertEqual(args_rep, '')
+ kw = dict(a=4, b=5, c=6)
+ args_rep = _format_args(args=(1, 2, 3), kw=kw)
+ self.assertEqual(args_rep, '1, 2, 3, a=4, b=5, c=6')
+ args_rep = _format_args(args=(1, 2, 3), kw=kw, leading_comma=True)
+ self.assertEqual(args_rep, ', 1, 2, 3, a=4, b=5, c=6')
+ class Klass(object):
+ a = 1
+ b = 2
+ c = 3
+ names = ['a', 'b', 'c']
+ obj = Klass()
+ self.assertRaises(AssertionError, _format_args, names=names)
+ args_rep = _format_args(obj=obj, names='a')
+ self.assertEqual(args_rep, 'a=1')
+ args_rep = _format_args(obj=obj, names=names)
+ self.assertEqual(args_rep, 'a=1, b=2, c=3')
+ args_rep = _format_args(kw=kw, defaults=dict(a=4, b=5))
+ self.assertEqual(args_rep, 'c=6')
+
+ def test_middleware_direct_call(self):
+ @wsgify.middleware
+ def mw(req, app):
+ return 'foo'
+
+ app = mw(Response())
+ self.assertEqual(app(Request.blank('/')), 'foo')
diff --git a/lib/webob_1_1_1/tests/test_descriptors.py b/lib/webob_1_1_1/tests/test_descriptors.py
new file mode 100644
index 0000000..35b7e38
--- /dev/null
+++ b/lib/webob_1_1_1/tests/test_descriptors.py
@@ -0,0 +1,633 @@
+# -*- coding: utf-8 -*-
+
+from datetime import tzinfo
+from datetime import timedelta
+
+from nose.tools import eq_
+from nose.tools import ok_
+from nose.tools import assert_raises
+
+from webob import Request
+
+
+class GMT(tzinfo):
+ """UTC"""
+ ZERO = timedelta(0)
+ def utcoffset(self, dt):
+ return self.ZERO
+
+ def tzname(self, dt):
+ return "UTC"
+
+ def dst(self, dt):
+ return self.ZERO
+
+
+class MockDescriptor:
+ _val = 'avalue'
+ def __get__(self, obj, type=None):
+ return self._val
+ def __set__(self, obj, val):
+ self._val = val
+ def __delete__(self, obj):
+ self._val = None
+
+
+def test_environ_getter_docstring():
+ from webob.descriptors import environ_getter
+ desc = environ_getter('akey')
+ eq_(desc.__doc__, "Gets and sets the ``akey`` key in the environment.")
+
+def test_environ_getter_nodefault_keyerror():
+ from webob.descriptors import environ_getter
+ req = Request.blank('/')
+ desc = environ_getter('akey')
+ assert_raises(KeyError, desc.fget, req)
+
+def test_environ_getter_nodefault_fget():
+ from webob.descriptors import environ_getter
+ req = Request.blank('/')
+ desc = environ_getter('akey')
+ desc.fset(req, 'bar')
+ eq_(req.environ['akey'], 'bar')
+
+def test_environ_getter_nodefault_fdel():
+ from webob.descriptors import environ_getter
+ desc = environ_getter('akey')
+ eq_(desc.fdel, None)
+
+def test_environ_getter_default_fget():
+ from webob.descriptors import environ_getter
+ req = Request.blank('/')
+ desc = environ_getter('akey', default='the_default')
+ eq_(desc.fget(req), 'the_default')
+
+def test_environ_getter_default_fset():
+ from webob.descriptors import environ_getter
+ req = Request.blank('/')
+ desc = environ_getter('akey', default='the_default')
+ desc.fset(req, 'bar')
+ eq_(req.environ['akey'], 'bar')
+
+def test_environ_getter_default_fset_none():
+ from webob.descriptors import environ_getter
+ req = Request.blank('/')
+ desc = environ_getter('akey', default='the_default')
+ desc.fset(req, 'baz')
+ desc.fset(req, None)
+ ok_('akey' not in req.environ)
+
+def test_environ_getter_default_fdel():
+ from webob.descriptors import environ_getter
+ req = Request.blank('/')
+ desc = environ_getter('akey', default='the_default')
+ desc.fset(req, 'baz')
+ assert 'akey' in req.environ
+ desc.fdel(req)
+ ok_('akey' not in req.environ)
+
+def test_environ_getter_rfc_section():
+ from webob.descriptors import environ_getter
+ desc = environ_getter('HTTP_X_AKEY', rfc_section='14.3')
+ eq_(desc.__doc__, "Gets and sets the ``X-Akey`` header "
+ "(`HTTP spec section 14.3 "
+ "<http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.3>`_)."
+ )
+
+
+def test_upath_property_fget():
+ from webob.descriptors import upath_property
+ req = Request.blank('/')
+ desc = upath_property('akey')
+ eq_(desc.fget(req), '')
+
+def test_upath_property_fset():
+ from webob.descriptors import upath_property
+ req = Request.blank('/')
+ desc = upath_property('akey')
+ desc.fset(req, 'avalue')
+ eq_(desc.fget(req), 'avalue')
+
+def test_header_getter_doc():
+ from webob.descriptors import header_getter
+ desc = header_getter('X-Header', '14.3')
+ assert 'http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.3' in desc.__doc__
+ assert '``X-Header`` header' in desc.__doc__
+
+def test_header_getter_fget():
+ from webob.descriptors import header_getter
+ from webob import Response
+ resp = Response('aresp')
+ desc = header_getter('AHEADER', '14.3')
+ eq_(desc.fget(resp), None)
+
+def test_header_getter_fset():
+ from webob.descriptors import header_getter
+ from webob import Response
+ resp = Response('aresp')
+ desc = header_getter('AHEADER', '14.3')
+ desc.fset(resp, 'avalue')
+ eq_(desc.fget(resp), 'avalue')
+
+def test_header_getter_fset_none():
+ from webob.descriptors import header_getter
+ from webob import Response
+ resp = Response('aresp')
+ desc = header_getter('AHEADER', '14.3')
+ desc.fset(resp, 'avalue')
+ desc.fset(resp, None)
+ eq_(desc.fget(resp), None)
+
+def test_header_getter_fdel():
+ from webob.descriptors import header_getter
+ from webob import Response
+ resp = Response('aresp')
+ desc = header_getter('AHEADER', '14.3')
+ desc.fset(resp, 'avalue2')
+ desc.fdel(resp)
+ eq_(desc.fget(resp), None)
+
+def test_header_getter_unicode_fget_none():
+ from webob.descriptors import header_getter
+ from webob import Response
+ resp = Response('aresp')
+ desc = header_getter('AHEADER', '14.3')
+ eq_(desc.fget(resp), None)
+
+def test_header_getter_unicode_fget():
+ from webob.descriptors import header_getter
+ from webob import Response
+ resp = Response('aresp')
+ desc = header_getter('AHEADER', '14.3')
+ desc.fset(resp, u'avalue')
+ eq_(desc.fget(resp), u'avalue')
+
+def test_header_getter_unicode_fset_none():
+ from webob.descriptors import header_getter
+ from webob import Response
+ resp = Response('aresp')
+ desc = header_getter('AHEADER', '14.3')
+ desc.fset(resp, None)
+ eq_(desc.fget(resp), None)
+
+def test_header_getter_unicode_fset():
+ from webob.descriptors import header_getter
+ from webob import Response
+ resp = Response('aresp')
+ desc = header_getter('AHEADER', '14.3')
+ desc.fset(resp, u'avalue2')
+ eq_(desc.fget(resp), u'avalue2')
+
+def test_header_getter_unicode_fdel():
+ from webob.descriptors import header_getter
+ from webob import Response
+ resp = Response('aresp')
+ desc = header_getter('AHEADER', '14.3')
+ desc.fset(resp, u'avalue3')
+ desc.fdel(resp)
+ eq_(desc.fget(resp), None)
+
+def test_converter_not_prop():
+ from webob.descriptors import converter
+ from webob.descriptors import parse_int_safe
+ from webob.descriptors import serialize_int
+ assert_raises(AssertionError,converter,
+ ('CONTENT_LENGTH', None, '14.13'),
+ parse_int_safe, serialize_int, 'int')
+
+def test_converter_with_name_docstring():
+ from webob.descriptors import converter
+ from webob.descriptors import environ_getter
+ from webob.descriptors import parse_int_safe
+ from webob.descriptors import serialize_int
+ desc = converter(
+ environ_getter('CONTENT_LENGTH', '666', '14.13'),
+ parse_int_safe, serialize_int, 'int')
+
+ assert 'http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.13' in desc.__doc__
+ assert '``Content-Length`` header' in desc.__doc__
+
+def test_converter_with_name_fget():
+ from webob.descriptors import converter
+ from webob.descriptors import environ_getter
+ from webob.descriptors import parse_int_safe
+ from webob.descriptors import serialize_int
+ req = Request.blank('/')
+ desc = converter(
+ environ_getter('CONTENT_LENGTH', '666', '14.13'),
+ parse_int_safe, serialize_int, 'int')
+ eq_(desc.fget(req), 666)
+
+def test_converter_with_name_fset():
+ from webob.descriptors import converter
+ from webob.descriptors import environ_getter
+ from webob.descriptors import parse_int_safe
+ from webob.descriptors import serialize_int
+ req = Request.blank('/')
+ desc = converter(
+ environ_getter('CONTENT_LENGTH', '666', '14.13'),
+ parse_int_safe, serialize_int, 'int')
+ desc.fset(req, '999')
+ eq_(desc.fget(req), 999)
+
+def test_converter_without_name_fget():
+ from webob.descriptors import converter
+ from webob.descriptors import environ_getter
+ from webob.descriptors import parse_int_safe
+ from webob.descriptors import serialize_int
+ req = Request.blank('/')
+ desc = converter(
+ environ_getter('CONTENT_LENGTH', '666', '14.13'),
+ parse_int_safe, serialize_int)
+ eq_(desc.fget(req), 666)
+
+def test_converter_without_name_fset():
+ from webob.descriptors import converter
+ from webob.descriptors import environ_getter
+ from webob.descriptors import parse_int_safe
+ from webob.descriptors import serialize_int
+ req = Request.blank('/')
+ desc = converter(
+ environ_getter('CONTENT_LENGTH', '666', '14.13'),
+ parse_int_safe, serialize_int)
+ desc.fset(req, '999')
+ eq_(desc.fget(req), 999)
+
+def test_converter_none_for_wrong_type():
+ from webob.descriptors import converter
+ from webob.descriptors import environ_getter
+ from webob.descriptors import parse_int_safe
+ from webob.descriptors import serialize_int
+ req = Request.blank('/')
+ desc = converter(
+ ## XXX: Should this fail if the type is wrong?
+ environ_getter('CONTENT_LENGTH', 'sixsixsix', '14.13'),
+ parse_int_safe, serialize_int, 'int')
+ eq_(desc.fget(req), None)
+
+def test_converter_delete():
+ from webob.descriptors import converter
+ from webob.descriptors import environ_getter
+ from webob.descriptors import parse_int_safe
+ from webob.descriptors import serialize_int
+ req = Request.blank('/')
+ desc = converter(
+ ## XXX: Should this fail if the type is wrong?
+ environ_getter('CONTENT_LENGTH', '666', '14.13'),
+ parse_int_safe, serialize_int, 'int')
+ assert_raises(KeyError, desc.fdel, req)
+
+def test_list_header():
+ from webob.descriptors import list_header
+ desc = list_header('CONTENT_LENGTH', '14.13')
+ eq_(type(desc), property)
+
+def test_parse_list_single():
+ from webob.descriptors import parse_list
+ result = parse_list('avalue')
+ eq_(result, ('avalue',))
+
+def test_parse_list_multiple():
+ from webob.descriptors import parse_list
+ result = parse_list('avalue,avalue2')
+ eq_(result, ('avalue', 'avalue2'))
+
+def test_parse_list_none():
+ from webob.descriptors import parse_list
+ result = parse_list(None)
+ eq_(result, None)
+
+def test_parse_list_unicode_single():
+ from webob.descriptors import parse_list
+ result = parse_list(u'avalue')
+ eq_(result, ('avalue',))
+
+def test_parse_list_unicode_multiple():
+ from webob.descriptors import parse_list
+ result = parse_list(u'avalue,avalue2')
+ eq_(result, ('avalue', 'avalue2'))
+
+def test_serialize_list():
+ from webob.descriptors import serialize_list
+ result = serialize_list(('avalue', 'avalue2'))
+ eq_(result, 'avalue, avalue2')
+
+def test_serialize_list_string():
+ from webob.descriptors import serialize_list
+ result = serialize_list('avalue')
+ eq_(result, 'avalue')
+
+def test_serialize_list_unicode():
+ from webob.descriptors import serialize_list
+ result = serialize_list(u'avalue')
+ eq_(result, u'avalue')
+
+def test_converter_date():
+ import datetime
+ from webob.descriptors import converter_date
+ from webob.descriptors import environ_getter
+ req = Request.blank('/')
+ UTC = GMT()
+ desc = converter_date(environ_getter(
+ "HTTP_DATE", "Tue, 15 Nov 1994 08:12:31 GMT", "14.8"))
+ eq_(desc.fget(req),
+ datetime.datetime(1994, 11, 15, 8, 12, 31, tzinfo=UTC))
+
+def test_converter_date_docstring():
+ from webob.descriptors import converter_date
+ from webob.descriptors import environ_getter
+ desc = converter_date(environ_getter(
+ "HTTP_DATE", "Tue, 15 Nov 1994 08:12:31 GMT", "14.8"))
+ assert 'http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.8' in desc.__doc__
+ assert '``Date`` header' in desc.__doc__
+
+
+def test_date_header_fget_none():
+ from webob import Response
+ from webob.descriptors import date_header
+ resp = Response('aresponse')
+ desc = date_header('HTTP_DATE', "14.8")
+ eq_(desc.fget(resp), None)
+
+def test_date_header_fset_fget():
+ import datetime
+ from webob import Response
+ from webob.descriptors import date_header
+ resp = Response('aresponse')
+ UTC = GMT()
+ desc = date_header('HTTP_DATE', "14.8")
+ desc.fset(resp, "Tue, 15 Nov 1994 08:12:31 GMT")
+ eq_(desc.fget(resp), datetime.datetime(1994, 11, 15, 8, 12, 31, tzinfo=UTC))
+
+def test_date_header_fdel():
+ from webob import Response
+ from webob.descriptors import date_header
+ resp = Response('aresponse')
+ desc = date_header('HTTP_DATE', "14.8")
+ desc.fset(resp, "Tue, 15 Nov 1994 08:12:31 GMT")
+ desc.fdel(resp)
+ eq_(desc.fget(resp), None)
+
+def test_deprecated_property():
+ req = Request.blank('/')
+ assert_raises(DeprecationWarning, getattr, req, 'postvars')
+ assert_raises(DeprecationWarning, setattr, req, 'postvars', {})
+ assert_raises(DeprecationWarning, delattr, req, 'postvars')
+ eq_(Request.postvars.__repr__(), "<Deprecated attribute postvars>")
+
+def test_parse_etag_response():
+ from webob.descriptors import parse_etag_response
+ etresp = parse_etag_response("etag")
+ eq_(etresp, "etag")
+
+def test_parse_etag_response_quoted():
+ from webob.descriptors import parse_etag_response
+ etresp = parse_etag_response('"etag"')
+ eq_(etresp, "etag")
+
+def test_parse_etag_response_is_none():
+ from webob.descriptors import parse_etag_response
+ etresp = parse_etag_response(None)
+ eq_(etresp, None)
+
+def test_serialize_etag_response():
+ from webob.descriptors import serialize_etag_response
+ etresp = serialize_etag_response("etag")
+ eq_(etresp, '"etag"')
+
+def test_parse_if_range_is_None():
+ from webob.descriptors import parse_if_range
+ from webob.descriptors import NoIfRange
+ eq_(NoIfRange, parse_if_range(None))
+
+def test_parse_if_range_date_ifr():
+ from webob.descriptors import parse_if_range
+ from webob.descriptors import IfRange
+ ifr = parse_if_range("2011-03-15 01:24:43.272409")
+ eq_(type(ifr), IfRange)
+
+def test_parse_if_range_date_etagmatcher():
+ from webob.descriptors import parse_if_range
+ from webob.etag import ETagMatcher
+ ifr = parse_if_range("2011-03-15 01:24:43.272409")
+ eq_(type(ifr.etag), ETagMatcher)
+
+def test_serialize_if_range_string():
+ from webob.descriptors import serialize_if_range
+ val = serialize_if_range("avalue")
+ eq_(val, "avalue")
+
+def test_serialize_if_range_unicode():
+ from webob.descriptors import serialize_if_range
+ val = serialize_if_range(u"avalue")
+ eq_(val, u"avalue")
+
+def test_serialize_if_range_datetime():
+ import datetime
+ from webob.descriptors import serialize_if_range
+ UTC = GMT()
+ val = serialize_if_range(datetime.datetime(1994, 11, 15, 8, 12, 31, tzinfo=UTC))
+ eq_(val, "Tue, 15 Nov 1994 08:12:31 GMT")
+
+def test_serialize_if_range_other():
+ from webob.descriptors import serialize_if_range
+ val = serialize_if_range(123456)
+ eq_(val, '123456')
+
+def test_parse_range_none():
+ from webob.descriptors import parse_range
+ val = parse_range(None)
+ eq_(val, None)
+
+def test_parse_range_type():
+ from webob.byterange import Range
+ from webob.descriptors import parse_range
+ val = parse_range("bytes=1-500")
+ eq_(type(val), type(Range.parse("bytes=1-500")))
+
+def test_parse_range_values():
+ from webob.byterange import Range
+ from webob.descriptors import parse_range
+ val = parse_range("bytes=1-500")
+ eq_(val.ranges, Range.parse("bytes=1-500").ranges)
+
+def test_serialize_range_none():
+ from webob.descriptors import serialize_range
+ val = serialize_range(None)
+ eq_(val, None)
+
+def test_serialize_range():
+ from webob.descriptors import serialize_range
+ val = serialize_range((1,500))
+ eq_(val, 'bytes=1-499')
+
+def test_serialize_invalid_len():
+ from webob.descriptors import serialize_range
+ assert_raises(ValueError, serialize_range, (1,))
+
+def test_parse_int_none():
+ from webob.descriptors import parse_int
+ val = parse_int(None)
+ eq_(val, None)
+
+def test_parse_int_emptystr():
+ from webob.descriptors import parse_int
+ val = parse_int('')
+ eq_(val, None)
+
+def test_parse_int():
+ from webob.descriptors import parse_int
+ val = parse_int('123')
+ eq_(val, 123)
+
+def test_parse_int_invalid():
+ from webob.descriptors import parse_int
+ assert_raises(ValueError, parse_int, 'abc')
+
+def test_parse_int_safe_none():
+ from webob.descriptors import parse_int_safe
+ eq_(parse_int_safe(None), None)
+
+def test_parse_int_safe_emptystr():
+ from webob.descriptors import parse_int_safe
+ eq_(parse_int_safe(''), None)
+
+def test_parse_int_safe():
+ from webob.descriptors import parse_int_safe
+ eq_(parse_int_safe('123'), 123)
+
+def test_parse_int_safe_invalid():
+ from webob.descriptors import parse_int_safe
+ eq_(parse_int_safe('abc'), None)
+
+def test_serialize_int():
+ from webob.descriptors import serialize_int
+ assert serialize_int is str
+
+def test_parse_content_range_none():
+ from webob.descriptors import parse_content_range
+ eq_(parse_content_range(None), None)
+
+def test_parse_content_range_emptystr():
+ from webob.descriptors import parse_content_range
+ eq_(parse_content_range(' '), None)
+
+def test_parse_content_range_length():
+ from webob.byterange import ContentRange
+ from webob.descriptors import parse_content_range
+ val = parse_content_range("bytes 0-499/1234")
+ eq_(val.length, ContentRange.parse("bytes 0-499/1234").length)
+
+def test_parse_content_range_start():
+ from webob.byterange import ContentRange
+ from webob.descriptors import parse_content_range
+ val = parse_content_range("bytes 0-499/1234")
+ eq_(val.start, ContentRange.parse("bytes 0-499/1234").start)
+
+def test_parse_content_range_stop():
+ from webob.byterange import ContentRange
+ from webob.descriptors import parse_content_range
+ val = parse_content_range("bytes 0-499/1234")
+ eq_(val.stop, ContentRange.parse("bytes 0-499/1234").stop)
+
+def test_serialize_content_range_none():
+ from webob.descriptors import serialize_content_range
+ eq_(serialize_content_range(None), 'None') ### XXX: Seems wrong
+
+def test_serialize_content_range_emptystr():
+ from webob.descriptors import serialize_content_range
+ eq_(serialize_content_range(''), None)
+
+def test_serialize_content_range_invalid():
+ from webob.descriptors import serialize_content_range
+ assert_raises(ValueError, serialize_content_range, (1,))
+
+def test_serialize_content_range_asterisk():
+ from webob.descriptors import serialize_content_range
+ eq_(serialize_content_range((0, 500)), 'bytes 0-499/*')
+
+def test_serialize_content_range_defined():
+ from webob.descriptors import serialize_content_range
+ eq_(serialize_content_range((0, 500, 1234)), 'bytes 0-499/1234')
+
+def test_parse_auth_params_leading_capital_letter():
+ from webob.descriptors import parse_auth_params
+ val = parse_auth_params('Basic Realm=WebOb')
+ eq_(val, {'ealm': 'WebOb'})
+
+def test_parse_auth_params_trailing_capital_letter():
+ from webob.descriptors import parse_auth_params
+ val = parse_auth_params('Basic realM=WebOb')
+ eq_(val, {})
+
+def test_parse_auth_params_doublequotes():
+ from webob.descriptors import parse_auth_params
+ val = parse_auth_params('Basic realm="Web Object"')
+ eq_(val, {'realm': 'Web Object'})
+
+def test_parse_auth_params_multiple_values():
+ from webob.descriptors import parse_auth_params
+ val = parse_auth_params("foo='blah &&234', qop=foo, nonce='qwerty1234'")
+ eq_(val, {'nonce': "'qwerty1234'", 'foo': "'blah &&234'", 'qop': 'foo'})
+
+def test_parse_auth_params_truncate_on_comma():
+ from webob.descriptors import parse_auth_params
+ val = parse_auth_params("Basic realm=WebOb,this_will_truncate")
+ eq_(val, {'realm': 'WebOb'})
+
+def test_parse_auth_params_emptystr():
+ from webob.descriptors import parse_auth_params
+ eq_(parse_auth_params(''), {})
+
+def test_parse_auth_none():
+ from webob.descriptors import parse_auth
+ eq_(parse_auth(None), None)
+
+def test_parse_auth_emptystr():
+ from webob.descriptors import parse_auth
+ assert_raises(ValueError, parse_auth, '')
+
+def test_parse_auth_basic():
+ from webob.descriptors import parse_auth
+ eq_(parse_auth("Basic realm=WebOb"), ('Basic', 'realm=WebOb'))
+
+def test_parse_auth_basic_quoted():
+ from webob.descriptors import parse_auth
+ eq_(parse_auth('Basic realm="Web Ob"'), ('Basic', {'realm': 'Web Ob'}))
+
+def test_parse_auth_basic_quoted_multiple_unknown():
+ from webob.descriptors import parse_auth
+ eq_(parse_auth("foo='blah &&234', qop=foo, nonce='qwerty1234'"),
+ ("foo='blah", "&&234', qop=foo, nonce='qwerty1234'"))
+
+def test_parse_auth_basic_quoted_known_multiple():
+ from webob.descriptors import parse_auth
+ eq_(parse_auth("Basic realm='blah &&234', qop=foo, nonce='qwerty1234'"),
+ ('Basic', "realm='blah &&234', qop=foo, nonce='qwerty1234'"))
+
+def test_serialize_auth_none():
+ from webob.descriptors import serialize_auth
+ eq_(serialize_auth(None), None)
+
+def test_serialize_auth_emptystr():
+ from webob.descriptors import serialize_auth
+ eq_(serialize_auth(''), '')
+
+def test_serialize_auth_basic_quoted():
+ from webob.descriptors import serialize_auth
+ val = serialize_auth(('Basic', 'realm="WebOb"'))
+ eq_(val, 'Basic realm="WebOb"')
+
+def test_serialize_auth_digest_multiple():
+ from webob.descriptors import serialize_auth
+ val = serialize_auth(('Digest', 'realm="WebOb", nonce=abcde12345, qop=foo'))
+ flags = val[len('Digest'):]
+ result = sorted([ x.strip() for x in flags.split(',') ])
+ eq_(result, ['nonce=abcde12345', 'qop=foo', 'realm="WebOb"'])
+
+def test_serialize_auth_digest_tuple():
+ from webob.descriptors import serialize_auth
+ val = serialize_auth(('Digest', {'realm':'"WebOb"', 'nonce':'abcde12345', 'qop':'foo'}))
+ flags = val[len('Digest'):]
+ result = sorted([ x.strip() for x in flags.split(',') ])
+ eq_(result, ['nonce="abcde12345"', 'qop="foo"', 'realm=""WebOb""'])
diff --git a/lib/webob_1_1_1/tests/test_etag.py b/lib/webob_1_1_1/tests/test_etag.py
new file mode 100644
index 0000000..181b891
--- /dev/null
+++ b/lib/webob_1_1_1/tests/test_etag.py
@@ -0,0 +1,399 @@
+import unittest
+from webob.etag import ETagMatcher
+
+class etag_propertyTests(unittest.TestCase):
+ def _getTargetClass(self):
+ from webob.etag import etag_property
+ return etag_property
+
+ def _makeOne(self, *args, **kw):
+ return self._getTargetClass()(*args, **kw)
+
+ def _makeDummyRequest(self, **kw):
+ """
+ Return a DummyRequest object with attrs from kwargs.
+ Use like: dr = _makeDummyRequest(environment={'userid': 'johngalt'})
+ Then you can: uid = dr.environment.get('userid', 'SomeDefault')
+ """
+ class Dummy(object):
+ def __init__(self, **kwargs):
+ self.__dict__.update(**kwargs)
+ d = Dummy(**kw)
+ return d
+
+ def test_fget_missing_key(self):
+ ep = self._makeOne("KEY", "DEFAULT", "RFC_SECTION")
+ req = self._makeDummyRequest(environ={})
+ self.assertEquals(ep.fget(req), "DEFAULT")
+
+ def test_fget_found_key(self):
+ ep = self._makeOne("KEY", "DEFAULT", "RFC_SECTION")
+ req = self._makeDummyRequest(environ={'KEY':'VALUE'})
+ res = ep.fget(req)
+ self.assertEquals(res.etags, ['VALUE'])
+ self.assertEquals(res.weak_etags, [])
+
+ def test_fget_star_key(self):
+ ep = self._makeOne("KEY", "DEFAULT", "RFC_SECTION")
+ req = self._makeDummyRequest(environ={'KEY':'*'})
+ res = ep.fget(req)
+ import webob.etag
+ self.assertEquals(type(res), webob.etag._AnyETag)
+ self.assertEquals(res.__dict__, {})
+
+ def test_fset_None(self):
+ ep = self._makeOne("KEY", "DEFAULT", "RFC_SECTION")
+ req = self._makeDummyRequest(environ={'KEY':'*'})
+ res = ep.fset(req, None)
+ self.assertEquals(res, None)
+
+ def test_fset_not_None(self):
+ ep = self._makeOne("KEY", "DEFAULT", "RFC_SECTION")
+ req = self._makeDummyRequest(environ={'KEY':'OLDVAL'})
+ res = ep.fset(req, "NEWVAL")
+ self.assertEquals(res, None)
+ self.assertEquals(req.environ['KEY'], 'NEWVAL')
+
+ def test_fedl(self):
+ ep = self._makeOne("KEY", "DEFAULT", "RFC_SECTION")
+ req = self._makeDummyRequest(environ={'KEY':'VAL', 'QUAY':'VALYOU'})
+ res = ep.fdel(req)
+ self.assertEquals(res, None)
+ self.assertFalse('KEY' in req.environ)
+ self.assertEquals(req.environ['QUAY'], 'VALYOU')
+
+class AnyETagTests(unittest.TestCase):
+ def _getTargetClass(self):
+ from webob.etag import _AnyETag
+ return _AnyETag
+
+ def _makeOne(self, *args, **kw):
+ return self._getTargetClass()(*args, **kw)
+
+ def test___repr__(self):
+ etag = self._makeOne()
+ self.assertEqual(etag.__repr__(), '<ETag *>')
+
+ def test___nonzero__(self):
+ etag = self._makeOne()
+ self.assertEqual(etag.__nonzero__(), False)
+
+ def test___contains__something(self):
+ etag = self._makeOne()
+ self.assertEqual('anything' in etag, True)
+
+ def test_weak_match_something(self):
+ etag = self._makeOne()
+ self.assertEqual(etag.weak_match('anything'), True)
+
+ def test___str__(self):
+ etag = self._makeOne()
+ self.assertEqual(str(etag), '*')
+
+class NoETagTests(unittest.TestCase):
+ def _getTargetClass(self):
+ from webob.etag import _NoETag
+ return _NoETag
+
+ def _makeOne(self, *args, **kw):
+ return self._getTargetClass()(*args, **kw)
+
+ def test___repr__(self):
+ etag = self._makeOne()
+ self.assertEqual(etag.__repr__(), '<No ETag>')
+
+ def test___nonzero__(self):
+ etag = self._makeOne()
+ self.assertEqual(etag.__nonzero__(), False)
+
+ def test___contains__something(self):
+ etag = self._makeOne()
+ assert 'anything' not in etag
+
+ def test_weak_match_None(self):
+ etag = self._makeOne()
+ self.assertEqual(etag.weak_match(None), False)
+
+ def test_weak_match_something(self):
+ etag = self._makeOne()
+ assert not etag.weak_match('anything')
+
+ def test___str__(self):
+ etag = self._makeOne()
+ self.assertEqual(str(etag), '')
+
+class ETagMatcherTests(unittest.TestCase):
+ def _getTargetClass(self):
+ return ETagMatcher
+
+ def _makeOne(self, *args, **kw):
+ return self._getTargetClass()(*args, **kw)
+
+ def test___init__wo_weak_etags(self):
+ matcher = self._makeOne(("ETAGS",))
+ self.assertEqual(matcher.etags, ("ETAGS",))
+ self.assertEqual(matcher.weak_etags, ())
+
+ def test___init__w_weak_etags(self):
+ matcher = self._makeOne(("ETAGS",), ("WEAK",))
+ self.assertEqual(matcher.etags, ("ETAGS",))
+ self.assertEqual(matcher.weak_etags, ("WEAK",))
+
+ def test___contains__tags(self):
+ matcher = self._makeOne(("ETAGS",), ("WEAK",))
+ self.assertTrue("ETAGS" in matcher)
+
+ def test___contains__weak_tags(self):
+ matcher = self._makeOne(("ETAGS",), ("WEAK",))
+ self.assertTrue("WEAK" in matcher)
+
+ def test___contains__not(self):
+ matcher = self._makeOne(("ETAGS",), ("WEAK",))
+ self.assertFalse("BEER" in matcher)
+
+ def test___contains__None(self):
+ matcher = self._makeOne(("ETAGS",), ("WEAK",))
+ self.assertFalse(None in matcher)
+
+ def test_weak_match_etags(self):
+ matcher = self._makeOne(("ETAGS",), ("WEAK",))
+ self.assertTrue(matcher.weak_match("W/ETAGS"))
+
+ def test_weak_match_weak_etags(self):
+ matcher = self._makeOne(("ETAGS",), ("WEAK",))
+ self.assertTrue(matcher.weak_match("W/WEAK"))
+
+ def test_weak_match_weak_not(self):
+ matcher = self._makeOne(("ETAGS",), ("WEAK",))
+ self.assertFalse(matcher.weak_match("W/BEER"))
+
+ def test_weak_match_weak_wo_wslash(self):
+ matcher = self._makeOne(("ETAGS",), ("WEAK",))
+ self.assertTrue(matcher.weak_match("ETAGS"))
+
+ def test_weak_match_weak_wo_wslash_not(self):
+ matcher = self._makeOne(("ETAGS",), ("WEAK",))
+ self.assertFalse(matcher.weak_match("BEER"))
+
+ def test___repr__one(self):
+ matcher = self._makeOne(("ETAGS",), ("WEAK",))
+ self.assertEqual(matcher.__repr__(), '<ETag ETAGS>')
+
+ def test___repr__multi(self):
+ matcher = self._makeOne(("ETAG1","ETAG2"), ("WEAK",))
+ self.assertEqual(matcher.__repr__(), '<ETag ETAG1 or ETAG2>')
+
+ def test___str__etag(self):
+ matcher = self._makeOne(("ETAG",))
+ self.assertEqual(str(matcher), '"ETAG"')
+
+ def test___str__etag_w_weak(self):
+ matcher = self._makeOne(("ETAG",), ("WEAK",))
+ self.assertEqual(str(matcher), '"ETAG", W/"WEAK"')
+
+
+
+class ParseTests(unittest.TestCase):
+ def test_parse_None(self):
+ et = ETagMatcher.parse(None)
+ self.assertEqual(et.etags, [])
+ self.assertEqual(et.weak_etags, [])
+
+ def test_parse_anyetag(self):
+ # these tests smell bad, are they useful?
+ et = ETagMatcher.parse('*')
+ self.assertEqual(et.__dict__, {})
+ self.assertEqual(et.__repr__(), '<ETag *>')
+
+ def test_parse_one(self):
+ et = ETagMatcher.parse('ONE')
+ self.assertEqual(et.etags, ['ONE'])
+ self.assertEqual(et.weak_etags, [])
+
+ def test_parse_commasep(self):
+ et = ETagMatcher.parse('ONE, TWO')
+ self.assertEqual(et.etags, ['ONE', 'TWO'])
+ self.assertEqual(et.weak_etags, [])
+
+ def test_parse_commasep_w_weak(self):
+ et = ETagMatcher.parse('ONE, w/TWO')
+ self.assertEqual(et.etags, ['ONE'])
+ self.assertEqual(et.weak_etags, ['TWO'])
+
+ def test_parse_quoted(self):
+ et = ETagMatcher.parse('"ONE"')
+ self.assertEqual(et.etags, ['ONE'])
+ self.assertEqual(et.weak_etags, [])
+
+ def test_parse_quoted_two(self):
+ et = ETagMatcher.parse('"ONE", "TWO"')
+ self.assertEqual(et.etags, ['ONE', 'TWO'])
+ self.assertEqual(et.weak_etags, [])
+
+ def test_parse_quoted_two_weak(self):
+ et = ETagMatcher.parse('"ONE", W/"TWO"')
+ self.assertEqual(et.etags, ['ONE'])
+ self.assertEqual(et.weak_etags, ['TWO'])
+
+ def test_parse_wo_close_quote(self):
+ # Unsure if this is testing likely input
+ et = ETagMatcher.parse('"ONE')
+ self.assertEqual(et.etags, ['ONE'])
+ self.assertEqual(et.weak_etags, [])
+
+class IfRangeTests(unittest.TestCase):
+ def _getTargetClass(self):
+ from webob.etag import IfRange
+ return IfRange
+
+ def _makeOne(self, *args, **kw):
+ return self._getTargetClass()(*args, **kw)
+
+ def test___init__(self):
+ ir = self._makeOne()
+ self.assertEqual(ir.etag, None)
+ self.assertEqual(ir.date, None)
+
+ def test___init__etag(self):
+ ir = self._makeOne(etag='ETAG')
+ self.assertEqual(ir.etag, 'ETAG')
+ self.assertEqual(ir.date, None)
+
+ def test___init__date(self):
+ ir = self._makeOne(date='DATE')
+ self.assertEqual(ir.etag, None)
+ self.assertEqual(ir.date, 'DATE')
+
+ def test___init__etag_date(self):
+ ir = self._makeOne(etag='ETAG', date='DATE')
+ self.assertEqual(ir.etag, 'ETAG')
+ self.assertEqual(ir.date, 'DATE')
+
+ def test___repr__(self):
+ ir = self._makeOne()
+ self.assertEqual(ir.__repr__(), '<IfRange etag=*, date=*>')
+
+ def test___repr__etag(self):
+ ir = self._makeOne(etag='ETAG')
+ self.assertEqual(ir.__repr__(), '<IfRange etag=ETAG, date=*>')
+
+ def test___repr__date(self):
+ ir = self._makeOne(date='Fri, 09 Nov 2001 01:08:47 -0000')
+ self.assertEqual(ir.__repr__(),
+ '<IfRange etag=*, ' +
+ 'date=Fri, 09 Nov 2001 01:08:47 -0000>')
+
+ def test___repr__etag_date(self):
+ ir = self._makeOne(etag='ETAG', date='Fri, 09 Nov 2001 01:08:47 -0000')
+ self.assertEqual(ir.__repr__(),
+ '<IfRange etag=ETAG, ' +
+ 'date=Fri, 09 Nov 2001 01:08:47 -0000>')
+
+ def test___str__(self):
+ ir = self._makeOne()
+ self.assertEqual(str(ir), '')
+
+ def test___str__etag(self):
+ ir = self._makeOne(etag='ETAG', date='Fri, 09 Nov 2001 01:08:47 -0000')
+ self.assertEqual(str(ir), 'ETAG')
+
+ def test___str__date(self):
+ ir = self._makeOne(date='Fri, 09 Nov 2001 01:08:47 -0000')
+ self.assertEqual(str(ir), 'Fri, 09 Nov 2001 01:08:47 -0000')
+
+ def test_match(self):
+ ir = self._makeOne()
+ self.assertTrue(ir.match())
+
+ def test_match_date_none(self):
+ ir = self._makeOne(date='Fri, 09 Nov 2001 01:08:47 -0000')
+ self.assertFalse(ir.match())
+
+ def test_match_date_earlier(self):
+ ir = self._makeOne(date='Fri, 09 Nov 2001 01:08:47 -0000')
+ self.assertTrue(ir.match(last_modified=
+ 'Fri, 09 Nov 2001 01:00:00 -0000'))
+
+ def test_match_etag_none(self):
+ ir = self._makeOne(etag="ETAG")
+ self.assertFalse(ir.match())
+
+ def test_match_etag_different(self):
+ ir = self._makeOne(etag="ETAG")
+ self.assertFalse(ir.match("DIFFERENT"))
+
+ def test_match_response_no_date(self):
+ class DummyResponse(object):
+ etag = "ETAG"
+ last_modified = None
+ ir = self._makeOne(etag="ETAG", date='Fri, 09 Nov 2001 01:08:47 -0000')
+ response = DummyResponse()
+ self.assertFalse(ir.match_response(response))
+
+ def test_match_response_w_date_earlier(self):
+ class DummyResponse(object):
+ etag = "ETAG"
+ last_modified = 'Fri, 09 Nov 2001 01:00:00 -0000'
+ ir = self._makeOne(etag="ETAG", date='Fri, 09 Nov 2001 01:08:47 -0000')
+ response = DummyResponse()
+ self.assertTrue(ir.match_response(response))
+
+ def test_match_response_etag(self):
+ class DummyResponse(object):
+ etag = "ETAG"
+ last_modified = None
+ ir = self._makeOne(etag="ETAG")
+ response = DummyResponse()
+ self.assertTrue(ir.match_response(response))
+
+ def test_parse_none(self):
+ ir = self._makeOne(etag="ETAG")
+ # I believe this identifies a bug: '_NoETag' object is not callable
+ self.assertRaises(TypeError, ir.parse, None)
+
+ def test_parse_wo_gmt(self):
+ ir = self._makeOne(etag="ETAG")
+ res = ir.parse('INTERPRETED_AS_ETAG')
+ self.assertEquals(res.etag.etags, ['INTERPRETED_AS_ETAG'])
+ self.assertEquals(res.date, None)
+
+ def test_parse_with_gmt(self):
+ import datetime
+ class UTC(datetime.tzinfo):
+ def utcoffset(self, dt):
+ return datetime.timedelta(0)
+ def tzname(self, dt):
+ return 'UTC'
+ ir = self._makeOne(etag="ETAG")
+ res = ir.parse('Fri, 09 Nov 2001 01:08:47 GMT')
+ self.assertEquals(res.etag, None)
+ dt = datetime.datetime(2001,11,9, 1,8,47,0, UTC())
+ self.assertEquals(res.date, dt)
+
+class NoIfRangeTests(unittest.TestCase):
+ def _getTargetClass(self):
+ from webob.etag import _NoIfRange
+ return _NoIfRange
+
+ def _makeOne(self, *args, **kw):
+ return self._getTargetClass()(*args, **kw)
+
+ def test___repr__(self):
+ ir = self._makeOne()
+ self.assertEquals(ir.__repr__(), '<Empty If-Range>')
+
+ def test___str__(self):
+ ir = self._makeOne()
+ self.assertEquals(str(ir), '')
+
+ def test___nonzero__(self):
+ ir = self._makeOne()
+ self.assertEquals(ir.__nonzero__(), False)
+
+ def test_match(self):
+ ir = self._makeOne()
+ self.assertEquals(ir.match(), True)
+
+ def test_match_response(self):
+ ir = self._makeOne()
+ self.assertEquals(ir.match_response("IGNORED"), True)
diff --git a/lib/webob_1_1_1/tests/test_exc.py b/lib/webob_1_1_1/tests/test_exc.py
new file mode 100644
index 0000000..f0c6f69
--- /dev/null
+++ b/lib/webob_1_1_1/tests/test_exc.py
@@ -0,0 +1,357 @@
+from webob.request import Request
+from webob.dec import wsgify
+from webob.exc import sys
+from webob.exc import no_escape
+from webob.exc import strip_tags
+from webob.exc import HTTPException
+from webob.exc import WSGIHTTPException
+from webob.exc import HTTPError
+from webob.exc import HTTPRedirection
+from webob.exc import HTTPRedirection
+from webob.exc import HTTPOk
+from webob.exc import HTTPCreated
+from webob.exc import HTTPAccepted
+from webob.exc import HTTPNonAuthoritativeInformation
+from webob.exc import HTTPNoContent
+from webob.exc import HTTPResetContent
+from webob.exc import HTTPPartialContent
+from webob.exc import _HTTPMove
+from webob.exc import HTTPMultipleChoices
+from webob.exc import HTTPMovedPermanently
+from webob.exc import HTTPFound
+from webob.exc import HTTPSeeOther
+from webob.exc import HTTPNotModified
+from webob.exc import HTTPUseProxy
+from webob.exc import HTTPTemporaryRedirect
+from webob.exc import HTTPClientError
+from webob.exc import HTTPBadRequest
+from webob.exc import HTTPUnauthorized
+from webob.exc import HTTPPaymentRequired
+from webob.exc import HTTPForbidden
+from webob.exc import HTTPNotFound
+from webob.exc import HTTPMethodNotAllowed
+from webob.exc import HTTPNotAcceptable
+from webob.exc import HTTPProxyAuthenticationRequired
+from webob.exc import HTTPRequestTimeout
+from webob.exc import HTTPConflict
+from webob.exc import HTTPGone
+from webob.exc import HTTPLengthRequired
+from webob.exc import HTTPPreconditionFailed
+from webob.exc import HTTPRequestEntityTooLarge
+from webob.exc import HTTPRequestURITooLong
+from webob.exc import HTTPUnsupportedMediaType
+from webob.exc import HTTPRequestRangeNotSatisfiable
+from webob.exc import HTTPExpectationFailed
+from webob.exc import HTTPUnprocessableEntity
+from webob.exc import HTTPLocked
+from webob.exc import HTTPFailedDependency
+from webob.exc import HTTPServerError
+from webob.exc import HTTPInternalServerError
+from webob.exc import HTTPNotImplemented
+from webob.exc import HTTPBadGateway
+from webob.exc import HTTPServiceUnavailable
+from webob.exc import HTTPGatewayTimeout
+from webob.exc import HTTPVersionNotSupported
+from webob.exc import HTTPInsufficientStorage
+from webob.exc import HTTPExceptionMiddleware
+from webob import exc
+
+from nose.tools import eq_, ok_, assert_equal, assert_raises
+
+@wsgify
+def method_not_allowed_app(req):
+ if req.method != 'GET':
+ raise HTTPMethodNotAllowed()
+ return 'hello!'
+
+def test_noescape_null():
+ assert_equal(no_escape(None), '')
+
+def test_noescape_not_basestring():
+ assert_equal(no_escape(42), '42')
+
+def test_noescape_unicode():
+ class DummyUnicodeObject(object):
+ def __unicode__(self):
+ return u'42'
+ duo = DummyUnicodeObject()
+ assert_equal(no_escape(duo), u'42')
+
+def test_strip_tags_empty():
+ assert_equal(strip_tags(''), '')
+
+def test_strip_tags_newline_to_space():
+ assert_equal(strip_tags('a\nb'), 'a b')
+
+def test_strip_tags_zaps_carriage_return():
+ assert_equal(strip_tags('a\rb'), 'ab')
+
+def test_strip_tags_br_to_newline():
+ assert_equal(strip_tags('a<br/>b'), 'a\nb')
+
+def test_strip_tags_zaps_comments():
+ assert_equal(strip_tags('a<!--b-->'), 'ab')
+
+def test_strip_tags_zaps_tags():
+ assert_equal(strip_tags('foo<bar>baz</bar>'), 'foobaz')
+
+def test_HTTPException():
+ _called = []
+ _result = object()
+ def _response(environ, start_response):
+ _called.append((environ, start_response))
+ return _result
+ environ = {}
+ start_response = object()
+ exc = HTTPException('testing', _response)
+ ok_(exc.wsgi_response is _response)
+ ok_(exc.exception is exc)
+ result = exc(environ, start_response)
+ ok_(result is result)
+ assert_equal(_called, [(environ, start_response)])
+
+def test_exception_with_unicode_data():
+ req = Request.blank('/', method=u'POST')
+ res = req.get_response(method_not_allowed_app)
+ assert res.status_int == 405
+
+def test_WSGIHTTPException_headers():
+ exc = WSGIHTTPException(headers=[('Set-Cookie', 'a=1'),
+ ('Set-Cookie', 'a=2')])
+ mixed = exc.headers.mixed()
+ assert mixed['set-cookie'] == ['a=1', 'a=2']
+
+def test_WSGIHTTPException_w_body_template():
+ from string import Template
+ TEMPLATE = '$foo: $bar'
+ exc = WSGIHTTPException(body_template = TEMPLATE)
+ assert_equal(exc.body_template, TEMPLATE)
+ ok_(isinstance(exc.body_template_obj, Template))
+ eq_(exc.body_template_obj.substitute({'foo': 'FOO', 'bar': 'BAR'}),
+ 'FOO: BAR')
+
+def test_WSGIHTTPException_w_empty_body():
+ class EmptyOnly(WSGIHTTPException):
+ empty_body = True
+ exc = EmptyOnly(content_type='text/plain', content_length=234)
+ ok_('content_type' not in exc.__dict__)
+ ok_('content_length' not in exc.__dict__)
+
+def test_WSGIHTTPException___str__():
+ exc1 = WSGIHTTPException(detail='Detail')
+ eq_(str(exc1), 'Detail')
+ class Explain(WSGIHTTPException):
+ explanation = 'Explanation'
+ eq_(str(Explain()), 'Explanation')
+
+def test_WSGIHTTPException_plain_body_no_comment():
+ class Explain(WSGIHTTPException):
+ code = '999'
+ title = 'Testing'
+ explanation = 'Explanation'
+ exc = Explain(detail='Detail')
+ eq_(exc.plain_body({}),
+ '999 Testing\n\nExplanation\n\n Detail ')
+
+def test_WSGIHTTPException_html_body_w_comment():
+ class Explain(WSGIHTTPException):
+ code = '999'
+ title = 'Testing'
+ explanation = 'Explanation'
+ exc = Explain(detail='Detail', comment='Comment')
+ eq_(exc.html_body({}),
+ '<html>\n'
+ ' <head>\n'
+ ' <title>999 Testing</title>\n'
+ ' </head>\n'
+ ' <body>\n'
+ ' <h1>999 Testing</h1>\n'
+ ' Explanation<br /><br />\n'
+ 'Detail\n'
+ '<!-- Comment -->\n\n'
+ ' </body>\n'
+ '</html>'
+ )
+
+def test_WSGIHTTPException_generate_response():
+ def start_response(status, headers, exc_info=None):
+ pass
+ environ = {
+ 'wsgi.url_scheme': 'HTTP',
+ 'SERVER_NAME': 'localhost',
+ 'SERVER_PORT': '80',
+ 'REQUEST_METHOD': 'PUT',
+ 'HTTP_ACCEPT': 'text/html'
+ }
+ excep = WSGIHTTPException()
+ assert_equal( excep(environ,start_response), [
+ '<html>\n'
+ ' <head>\n'
+ ' <title>None None</title>\n'
+ ' </head>\n'
+ ' <body>\n'
+ ' <h1>None None</h1>\n'
+ ' <br /><br />\n'
+ '\n'
+ '\n\n'
+ ' </body>\n'
+ '</html>' ]
+ )
+
+def test_WSGIHTTPException_call_w_body():
+ def start_response(status, headers, exc_info=None):
+ pass
+ environ = {
+ 'wsgi.url_scheme': 'HTTP',
+ 'SERVER_NAME': 'localhost',
+ 'SERVER_PORT': '80',
+ 'REQUEST_METHOD': 'PUT'
+ }
+ excep = WSGIHTTPException()
+ excep.body = 'test'
+ assert_equal( excep(environ,start_response), ['test'] )
+
+
+def test_WSGIHTTPException_wsgi_response():
+ def start_response(status, headers, exc_info=None):
+ pass
+ environ = {
+ 'wsgi.url_scheme': 'HTTP',
+ 'SERVER_NAME': 'localhost',
+ 'SERVER_PORT': '80',
+ 'REQUEST_METHOD': 'HEAD'
+ }
+ excep = WSGIHTTPException()
+ assert_equal( excep.wsgi_response(environ,start_response), [] )
+
+def test_WSGIHTTPException_exception_newstyle():
+ def start_response(status, headers, exc_info=None):
+ pass
+ environ = {
+ 'wsgi.url_scheme': 'HTTP',
+ 'SERVER_NAME': 'localhost',
+ 'SERVER_PORT': '80',
+ 'REQUEST_METHOD': 'HEAD'
+ }
+ excep = WSGIHTTPException()
+ exc.newstyle_exceptions = True
+ assert_equal( excep(environ,start_response), [] )
+
+def test_WSGIHTTPException_exception_no_newstyle():
+ def start_response(status, headers, exc_info=None):
+ pass
+ environ = {
+ 'wsgi.url_scheme': 'HTTP',
+ 'SERVER_NAME': 'localhost',
+ 'SERVER_PORT': '80',
+ 'REQUEST_METHOD': 'HEAD'
+ }
+ excep = WSGIHTTPException()
+ exc.newstyle_exceptions = False
+ assert_equal( excep(environ,start_response), [] )
+
+def test_HTTPMove():
+ def start_response(status, headers, exc_info=None):
+ pass
+ environ = {
+ 'wsgi.url_scheme': 'HTTP',
+ 'SERVER_NAME': 'localhost',
+ 'SERVER_PORT': '80',
+ 'REQUEST_METHOD': 'HEAD'
+ }
+ m = _HTTPMove()
+ assert_equal( m( environ, start_response ), [] )
+
+def test_HTTPMove_location_not_none():
+ def start_response(status, headers, exc_info=None):
+ pass
+ environ = {
+ 'wsgi.url_scheme': 'HTTP',
+ 'SERVER_NAME': 'localhost',
+ 'SERVER_PORT': '80',
+ 'REQUEST_METHOD': 'HEAD'
+ }
+ m = _HTTPMove(location='http://example.com')
+ assert_equal( m( environ, start_response ), [] )
+
+def test_HTTPMove_add_slash_and_location():
+ def start_response(status, headers, exc_info=None):
+ pass
+ environ = {
+ 'wsgi.url_scheme': 'HTTP',
+ 'SERVER_NAME': 'localhost',
+ 'SERVER_PORT': '80',
+ 'REQUEST_METHOD': 'HEAD'
+ }
+ assert_raises( TypeError, _HTTPMove, location='http://example.com', add_slash=True )
+
+def test_HTTPMove_call_add_slash():
+ def start_response(status, headers, exc_info=None):
+ pass
+ environ = {
+ 'wsgi.url_scheme': 'HTTP',
+ 'SERVER_NAME': 'localhost',
+ 'SERVER_PORT': '80',
+ 'REQUEST_METHOD': 'HEAD'
+ }
+ m = _HTTPMove()
+ m.add_slash = True
+ assert_equal( m( environ, start_response ), [] )
+
+def test_HTTPMove_call_query_string():
+ def start_response(status, headers, exc_info=None):
+ pass
+ environ = {
+ 'wsgi.url_scheme': 'HTTP',
+ 'SERVER_NAME': 'localhost',
+ 'SERVER_PORT': '80',
+ 'REQUEST_METHOD': 'HEAD'
+ }
+ m = _HTTPMove()
+ m.add_slash = True
+ environ[ 'QUERY_STRING' ] = 'querystring'
+ assert_equal( m( environ, start_response ), [] )
+
+def test_HTTPExceptionMiddleware_ok():
+ def app( environ, start_response ):
+ return '123'
+ application = app
+ m = HTTPExceptionMiddleware(application)
+ environ = {}
+ start_response = None
+ res = m( environ, start_response )
+ assert_equal( res, '123' )
+
+def test_HTTPExceptionMiddleware_exception():
+ def wsgi_response( environ, start_response):
+ return '123'
+ def app( environ, start_response ):
+ raise HTTPException( None, wsgi_response )
+ application = app
+ m = HTTPExceptionMiddleware(application)
+ environ = {}
+ start_response = None
+ res = m( environ, start_response )
+ assert_equal( res, '123' )
+
+def test_HTTPExceptionMiddleware_exception_exc_info_none():
+ class DummySys:
+ def exc_info(self):
+ return None
+ def wsgi_response( environ, start_response):
+ return start_response('200 OK', [], exc_info=None)
+ def app( environ, start_response ):
+ raise HTTPException( None, wsgi_response )
+ application = app
+ m = HTTPExceptionMiddleware(application)
+ environ = {}
+ def start_response(status, headers, exc_info):
+ pass
+ try:
+ from webob import exc
+ old_sys = exc.sys
+ sys = DummySys()
+ res = m( environ, start_response )
+ assert_equal( res, None )
+ finally:
+ exc.sys = old_sys
diff --git a/lib/webob_1_1_1/tests/test_headers.py b/lib/webob_1_1_1/tests/test_headers.py
new file mode 100644
index 0000000..8bcf2d6
--- /dev/null
+++ b/lib/webob_1_1_1/tests/test_headers.py
@@ -0,0 +1,113 @@
+# -*- coding: utf-8 -*-
+from webob import headers
+from nose.tools import ok_, assert_raises, eq_
+
+class TestError(Exception):
+ pass
+
+def test_ResponseHeaders_delitem_notpresent():
+ """Deleting a missing key from ResponseHeaders should raise a KeyError"""
+ d = headers.ResponseHeaders()
+ assert_raises(KeyError, d.__delitem__, 'b')
+
+def test_ResponseHeaders_delitem_present():
+ """
+ Deleting a present key should not raise an error at all
+ """
+ d = headers.ResponseHeaders(a=1)
+ del d['a']
+ ok_('a' not in d)
+
+def test_ResponseHeaders_setdefault():
+ """Testing set_default for ResponseHeaders"""
+ d = headers.ResponseHeaders(a=1)
+ res = d.setdefault('b', 1)
+ assert res == d['b'] == 1
+ res = d.setdefault('b', 10)
+ assert res == d['b'] == 1
+ res = d.setdefault('B', 10)
+ assert res == d['b'] == d['B'] == 1
+
+def test_ResponseHeader_pop():
+ """Testing if pop return TypeError when more than len(*args)>1 plus other
+ assorted tests"""
+ d = headers.ResponseHeaders(a=1, b=2, c=3, d=4)
+ assert_raises(TypeError, d.pop, 'a', 'z', 'y')
+ eq_(d.pop('a'), 1)
+ ok_('a' not in d)
+ eq_(d.pop('B'), 2)
+ ok_('b' not in d)
+ eq_(d.pop('c', 'u'), 3)
+ ok_('c' not in d)
+ eq_(d.pop('e', 'u'), 'u')
+ ok_('e' not in d)
+ assert_raises(KeyError, d.pop, 'z')
+
+def test_ResponseHeaders_getitem_miss():
+ d = headers.ResponseHeaders()
+ assert_raises(KeyError, d.__getitem__, 'a')
+
+def test_ResponseHeaders_getall():
+ d = headers.ResponseHeaders()
+ d.add('a', 1)
+ d.add('a', 2)
+ result = d.getall('a')
+ eq_(result, [1,2])
+
+def test_ResponseHeaders_mixed():
+ d = headers.ResponseHeaders()
+ d.add('a', 1)
+ d.add('a', 2)
+ d['b'] = 1
+ result = d.mixed()
+ eq_(result, {'a':[1,2], 'b':1})
+
+def test_ResponseHeaders_setitem_scalar_replaces_seq():
+ d = headers.ResponseHeaders()
+ d.add('a', 2)
+ d['a'] = 1
+ result = d.getall('a')
+ eq_(result, [1])
+
+def test_ResponseHeaders_contains():
+ d = headers.ResponseHeaders()
+ d['a'] = 1
+ ok_('a' in d)
+ ok_(not 'b' in d)
+
+def test_EnvironHeaders_delitem():
+ d = headers.EnvironHeaders({'CONTENT_LENGTH': '10'})
+ del d['CONTENT-LENGTH']
+ assert not d
+ assert_raises(KeyError, d.__delitem__, 'CONTENT-LENGTH')
+
+def test_EnvironHeaders_getitem():
+ d = headers.EnvironHeaders({'CONTENT_LENGTH': '10'})
+ eq_(d['CONTENT-LENGTH'], '10')
+
+def test_EnvironHeaders_setitem():
+ d = headers.EnvironHeaders({})
+ d['abc'] = '10'
+ eq_(d['abc'], '10')
+
+def test_EnvironHeaders_contains():
+ d = headers.EnvironHeaders({})
+ d['a'] = '10'
+ ok_('a' in d)
+ ok_(not 'b' in d)
+
+def test__trans_key_not_basestring():
+ result = headers._trans_key(None)
+ eq_(result, None)
+
+def test__trans_key_not_a_header():
+ result = headers._trans_key('')
+ eq_(result, None)
+
+def test__trans_key_key2header():
+ result = headers._trans_key('CONTENT_TYPE')
+ eq_(result, 'Content-Type')
+
+def test__trans_key_httpheader():
+ result = headers._trans_key('HTTP_FOO_BAR')
+ eq_(result, 'Foo-Bar')
diff --git a/lib/webob_1_1_1/tests/test_in_wsgiref.py b/lib/webob_1_1_1/tests/test_in_wsgiref.py
new file mode 100644
index 0000000..449a381
--- /dev/null
+++ b/lib/webob_1_1_1/tests/test_in_wsgiref.py
@@ -0,0 +1,169 @@
+from __future__ import with_statement
+from webob import Request, Response
+import sys, logging, threading, random, urllib2, socket, cgi
+from contextlib import contextmanager
+from nose.tools import assert_raises, eq_ as eq
+from wsgiref.simple_server import make_server, WSGIRequestHandler, WSGIServer, ServerHandler
+from Queue import Queue, Empty
+
+log = logging.getLogger(__name__)
+
+__test__ = (sys.version >= '2.6') # skip these tests on py2.5
+
+
+
+def test_request_reading():
+ """
+ Test actual request/response cycle in the presence of Request.copy()
+ and other methods that can potentially hang.
+ """
+ with serve(_test_app_req_reading) as server:
+ for key in _test_ops_req_read:
+ resp = urllib2.urlopen(server.url+key, timeout=3)
+ assert resp.read() == "ok"
+
+def _test_app_req_reading(env, sr):
+ req = Request(env)
+ log.debug('starting test operation: %s', req.path_info)
+ test_op = _test_ops_req_read[req.path_info]
+ test_op(req)
+ log.debug('done')
+ r = Response("ok")
+ return r(env, sr)
+
+_test_ops_req_read = {
+ '/copy': lambda req: req.copy(),
+ '/read-all': lambda req: req.body_file.read(),
+ '/read-0': lambda req: req.body_file.read(0),
+ '/make-seekable': lambda req: req.make_body_seekable()
+}
+
+
+
+
+# TODO: remove server logging for interrupted requests
+# TODO: test interrupted body directly
+
+def test_interrupted_request():
+ with serve(_test_app_req_interrupt) as server:
+ for path in _test_ops_req_interrupt:
+ _send_interrupted_req(server, path)
+ try:
+ assert _global_res.get(timeout=1)
+ except Empty:
+ raise AssertionError("Error during test %s", path)
+
+_global_res = Queue()
+
+def _test_app_req_interrupt(env, sr):
+ req = Request(env)
+ assert req.content_length == 100000
+ op = _test_ops_req_interrupt[req.path_info]
+ log.info("Running test: %s", req.path_info)
+ assert_raises(IOError, op, req)
+ _global_res.put(True)
+ sr('200 OK', [])
+ return []
+
+def _req_int_cgi(req):
+ assert req.body_file.read(0) == ''
+ #req.environ.setdefault('CONTENT_LENGTH', '0')
+ d = cgi.FieldStorage(
+ fp=req.body_file,
+ environ=req.environ,
+ )
+
+def _req_int_readline(req):
+ try:
+ eq(req.body_file.readline(), 'a=b\n')
+ req.body_file.readline()
+ except IOError:
+ # too early to detect disconnect
+ raise AssertionError
+ req.body_file.readline()
+
+
+_test_ops_req_interrupt = {
+ '/copy': lambda req: req.copy(),
+ '/read-body': lambda req: req.body,
+ '/read-post': lambda req: req.POST,
+ '/read-all': lambda req: req.body_file.read(),
+ '/read-too-much': lambda req: req.body_file.read(1<<30),
+ '/readline': _req_int_readline,
+ '/readlines': lambda req: req.body_file.readlines(),
+ '/read-cgi': _req_int_cgi,
+ '/make-seekable': lambda req: req.make_body_seekable()
+}
+
+
+def _send_interrupted_req(server, path='/'):
+ sock = socket.socket()
+ sock.connect(('localhost', server.server_port))
+ f = sock.makefile('wb')
+ f.write(_interrupted_req % path)
+ f.flush()
+ f.close()
+ sock.close()
+
+_interrupted_req = (
+ "POST %s HTTP/1.0\r\n"
+ "content-type: application/x-www-form-urlencoded\r\n"
+ "content-length: 100000\r\n"
+ "\r\n"
+)
+_interrupted_req += 'a=b\nz='+'x'*10000
+
+
+@contextmanager
+def serve(app):
+ server = _make_test_server(app)
+ try:
+ #worker = threading.Thread(target=server.handle_request)
+ worker = threading.Thread(target=server.serve_forever)
+ worker.setDaemon(True)
+ worker.start()
+ server.url = "http://localhost:%d" % server.server_port
+ log.debug("server started on %s", server.url)
+ yield server
+ finally:
+ log.debug("shutting server down")
+ server.shutdown()
+ worker.join(1)
+ if worker.isAlive():
+ log.warning('worker is hanged')
+ else:
+ log.debug("server stopped")
+
+
+class QuietHanlder(WSGIRequestHandler):
+ def log_request(self, *args):
+ pass
+
+ServerHandler.handle_error = lambda: None
+
+class QuietServer(WSGIServer):
+ def handle_error(self, req, addr):
+ pass
+
+def _make_test_server(app):
+ maxport = ((1<<16)-1)
+ # we'll make 3 attempts to find a free port
+ for i in range(3, 0, -1):
+ try:
+ port = random.randint(maxport/2, maxport)
+ server = make_server('localhost', port, app,
+ server_class=QuietServer,
+ handler_class=QuietHanlder
+ )
+ server.timeout = 5
+ return server
+ except:
+ if i == 1:
+ raise
+
+
+
+if __name__ == '__main__':
+ #test_request_reading()
+ test_interrupted_request()
+
diff --git a/lib/webob_1_1_1/tests/test_misc.py b/lib/webob_1_1_1/tests/test_misc.py
new file mode 100644
index 0000000..c18f4fe
--- /dev/null
+++ b/lib/webob_1_1_1/tests/test_misc.py
@@ -0,0 +1,143 @@
+import cgi, sys
+from cStringIO import StringIO
+from webob import html_escape, Response
+from webob.multidict import *
+from nose.tools import eq_ as eq, assert_raises
+
+
+def test_html_escape():
+ for v, s in [
+ # unsafe chars
+ ('these chars: < > & "', 'these chars: < > & "'),
+ (' ', ' '),
+ ('è', '&egrave;'),
+ # The apostrophe is *not* escaped, which some might consider to be
+ # a serious bug (see, e.g. http://www.cvedetails.com/cve/CVE-2010-2480/)
+ (u'the majestic m\xf8ose', 'the majestic møose'),
+ #("'", "'")
+
+ # 8-bit strings are passed through
+ (u'\xe9', 'é'),
+ (u'the majestic m\xf8ose'.encode('utf-8'), 'the majestic m\xc3\xb8ose'),
+
+ # ``None`` is treated specially, and returns the empty string.
+ (None, ''),
+
+ # Objects that define a ``__html__`` method handle their own escaping
+ (t_esc_HTML(), '<div>hello</div>'),
+
+ # Things that are not strings are converted to strings and then escaped
+ (42, '42'),
+ (Exception("expected a '<'."), "expected a '<'."),
+
+ # If an object implements both ``__str__`` and ``__unicode__``, the latter
+ # is preferred
+ (t_esc_SuperMoose(), 'møose'),
+ (t_esc_Unicode(), 'é'),
+ (t_esc_UnsafeAttrs(), '<UnsafeAttrs>'),
+ ]:
+ eq(html_escape(v), s)
+
+
+class t_esc_HTML(object):
+ def __html__(self):
+ return '<div>hello</div>'
+
+
+class t_esc_Unicode(object):
+ def __unicode__(self):
+ return u'\xe9'
+
+class t_esc_UnsafeAttrs(object):
+ attr = 'value'
+ def __getattr__(self):
+ return self.attr
+ def __repr__(self):
+ return '<UnsafeAttrs>'
+
+class t_esc_SuperMoose(object):
+ def __str__(self):
+ return u'm\xf8ose'.encode('UTF-8')
+ def __unicode__(self):
+ return u'm\xf8ose'
+
+
+
+
+
+
+def test_multidict():
+ d = MultiDict(a=1, b=2)
+ eq(d['a'], 1)
+ eq(d.getall('c'), [])
+
+ d.add('a', 2)
+ eq(d['a'], 2)
+ eq(d.getall('a'), [1, 2])
+
+ d['b'] = 4
+ eq(d.getall('b'), [4])
+ eq(d.keys(), ['a', 'a', 'b'])
+ eq(d.items(), [('a', 1), ('a', 2), ('b', 4)])
+ eq(d.mixed(), {'a': [1, 2], 'b': 4})
+
+ # test getone
+
+ # KeyError: "Multiple values match 'a': [1, 2]"
+ assert_raises(KeyError, d.getone, 'a')
+ eq(d.getone('b'), 4)
+ # KeyError: "Key not found: 'g'"
+ assert_raises(KeyError, d.getone, 'g')
+
+ eq(d.dict_of_lists(), {'a': [1, 2], 'b': [4]})
+ assert 'b' in d
+ assert 'e' not in d
+ d.clear()
+ assert 'b' not in d
+ d['a'] = 4
+ d.add('a', 5)
+ e = d.copy()
+ assert 'a' in e
+ e.clear()
+ e['f'] = 42
+ d.update(e)
+ eq(d, MultiDict([('a', 4), ('a', 5), ('f', 42)]))
+ f = d.pop('a')
+ eq(f, 4)
+ eq(d['a'], 5)
+
+
+ eq(d.pop('g', 42), 42)
+ assert_raises(KeyError, d.pop, 'n')
+ # TypeError: pop expected at most 2 arguments, got 3
+ assert_raises(TypeError, d.pop, 4, 2, 3)
+ d.setdefault('g', []).append(4)
+ eq(d, MultiDict([('a', 5), ('f', 42), ('g', [4])]))
+
+
+
+def test_multidict_init():
+ d = MultiDict([('a', 'b')], c=2)
+ eq(repr(d), "MultiDict([('a', 'b'), ('c', 2)])")
+ eq(d, MultiDict([('a', 'b')], c=2))
+
+ # TypeError: MultiDict can only be called with one positional argument
+ assert_raises(TypeError, MultiDict, 1, 2, 3)
+
+ # TypeError: MultiDict.view_list(obj) takes only actual list objects, not None
+ assert_raises(TypeError, MultiDict.view_list, None)
+
+
+
+def test_multidict_cgi():
+ env = {'QUERY_STRING': ''}
+ fs = cgi.FieldStorage(environ=env)
+ fs.filename = '\xc3\xb8'
+ plain = MultiDict(key='\xc3\xb8', fs=fs)
+ ua = UnicodeMultiDict(multi=plain, encoding='utf-8')
+ eq(ua.getall('key'), [u'\xf8'])
+ eq(repr(ua.getall('fs')), "[FieldStorage(None, u'\\xf8', [])]")
+ ub = UnicodeMultiDict(multi=ua, encoding='utf-8')
+ eq(ub.getall('key'), [u'\xf8'])
+ eq(repr(ub.getall('fs')), "[FieldStorage(None, u'\\xf8', [])]")
+
diff --git a/lib/webob_1_1_1/tests/test_multidict.py b/lib/webob_1_1_1/tests/test_multidict.py
new file mode 100644
index 0000000..a93fdf8
--- /dev/null
+++ b/lib/webob_1_1_1/tests/test_multidict.py
@@ -0,0 +1,410 @@
+# -*- coding: utf-8 -*-
+
+import unittest
+from webob import multidict
+
+class BaseDictTests(object):
+ def setUp(self):
+ self._list = [('a', u'\xe9'), ('a', 'e'), ('a', 'f'), ('b', 1)]
+ self.data = multidict.MultiDict(self._list)
+ self.d = self._get_instance()
+
+ def _get_instance(self, **kwargs):
+ if kwargs:
+ data = multidict.MultiDict(kwargs)
+ else:
+ data = self.data.copy()
+ return self.klass(data)
+
+ def test_len(self):
+ self.assertEqual(len(self.d), 4)
+
+ def test_getone(self):
+ self.assertEqual(self.d.getone('b'), 1)
+
+ def test_getone_missing(self):
+ self.assertRaises(KeyError, self.d.getone, 'z')
+
+ def test_getone_multiple_raises(self):
+ self.assertRaises(KeyError, self.d.getone, 'a')
+
+ def test_getall(self):
+ self.assertEqual(self.d.getall('b'), [1])
+
+ def test_dict_of_lists(self):
+ self.assertEqual(
+ self.d.dict_of_lists(),
+ {'a': [u'\xe9', u'e', u'f'], 'b': [1]})
+
+ def test_dict_api(self):
+ self.assertTrue('a' in self.d.mixed())
+ self.assertTrue('a' in self.d.keys())
+ self.assertTrue('a' in self.d.iterkeys())
+ self.assertTrue(('b', 1) in self.d.items())
+ self.assertTrue(('b', 1) in self.d.iteritems())
+ self.assertTrue(1 in self.d.values())
+ self.assertTrue(1 in self.d.itervalues())
+ self.assertEqual(len(self.d), 4)
+
+ def test_set_del_item(self):
+ d = self._get_instance()
+ self.assertTrue('a' in d)
+ del d['a']
+ self.assertTrue(not 'a' in d)
+
+ def test_pop(self):
+ d = self._get_instance()
+ d['a'] = 1
+ self.assertEqual(d.pop('a'), 1)
+ self.assertEqual(d.pop('x', 1), 1)
+
+ def test_pop_wrong_args(self):
+ d = self._get_instance()
+ self.assertRaises(TypeError, d.pop, 'a', 1, 1)
+
+ def test_pop_missing(self):
+ d = self._get_instance()
+ self.assertRaises(KeyError, d.pop, 'z')
+
+ def test_popitem(self):
+ d = self._get_instance()
+ self.assertEqual(d.popitem(), ('b', 1))
+
+ def test_update(self):
+ d = self._get_instance()
+ d.update(e=1)
+ self.assertTrue('e' in d)
+ d.update(dict(x=1))
+ self.assertTrue('x' in d)
+ d.update([('y', 1)])
+ self.assertTrue('y' in d)
+
+ def test_setdefault(self):
+ d = self._get_instance()
+ d.setdefault('a', 1)
+ self.assertNotEqual(d['a'], 1)
+ d.setdefault('e', 1)
+ self.assertTrue('e' in d)
+
+ def test_add(self):
+ d = self._get_instance()
+ d.add('b', 3)
+ self.assertEqual(d.getall('b'), [1, 3])
+
+ def test_copy(self):
+ assert self.d.copy() is not self.d
+ if hasattr(self.d, 'multi'):
+ self.assertFalse(self.d.copy().multi is self.d.multi)
+ self.assertFalse(self.d.copy() is self.d.multi)
+
+ def test_clear(self):
+ d = self._get_instance()
+ d.clear()
+ self.assertEqual(len(d), 0)
+
+ def test_nonzero(self):
+ d = self._get_instance()
+ self.assertTrue(d)
+ d.clear()
+ self.assertFalse(d)
+
+ def test_repr(self):
+ self.assertTrue(repr(self._get_instance()))
+
+ def test_too_many_args(self):
+ from webob.multidict import MultiDict
+ self.assertRaises(TypeError, MultiDict, 1, 2)
+
+ def test_no_args(self):
+ from webob.multidict import MultiDict
+ md = MultiDict()
+ self.assertEqual(md._items, [])
+
+ def test_kwargs(self):
+ from webob.multidict import MultiDict
+ md = MultiDict(kw1='val1')
+ self.assertEqual(md._items, [('kw1','val1')])
+
+ def test_view_list_not_list(self):
+ from webob.multidict import MultiDict
+ d = MultiDict()
+ self.assertRaises(TypeError, d.view_list, 42)
+
+ def test_view_list(self):
+ from webob.multidict import MultiDict
+ d = MultiDict()
+ self.assertEqual(d.view_list([1,2])._items, [1,2])
+
+ def test_from_fieldstorage_with_filename(self):
+ from webob.multidict import MultiDict
+ d = MultiDict()
+ fs = DummyFieldStorage('a', '1', 'file')
+ self.assertEqual(d.from_fieldstorage(fs), MultiDict({'a':fs.list[0]}))
+
+ def test_from_fieldstorage_without_filename(self):
+ from webob.multidict import MultiDict
+ d = MultiDict()
+ fs = DummyFieldStorage('a', '1')
+ self.assertEqual(d.from_fieldstorage(fs), MultiDict({'a':'1'}))
+
+class MultiDictTestCase(BaseDictTests, unittest.TestCase):
+ klass = multidict.MultiDict
+
+ def test_update_behavior_warning(self):
+ import warnings
+ class Foo(dict):
+ def __len__(self):
+ return 0
+ foo = Foo()
+ foo['a'] = 1
+ d = self._get_instance()
+ try:
+ warnings.simplefilter('error')
+ self.assertRaises(UserWarning, d.update, foo)
+ finally:
+ warnings.resetwarnings()
+
+ def test_repr_with_password(self):
+ d = self._get_instance(password='pwd')
+ self.assertEqual(repr(d), "MultiDict([('password', '******')])")
+
+class UnicodeMultiDictTestCase(BaseDictTests, unittest.TestCase):
+ klass = multidict.UnicodeMultiDict
+
+ def test_decode_key(self):
+ d = self._get_instance()
+ d.decode_keys = True
+
+ class Key(object):
+ pass
+
+ key = Key()
+ self.assertEquals(key, d._decode_key(key))
+
+ def test_decode_value(self):
+ import cgi
+
+ d = self._get_instance()
+ d.decode_keys = True
+
+ env = {'QUERY_STRING': ''}
+ fs = cgi.FieldStorage(environ=env)
+ fs.name = 'a'
+ self.assertEqual(d._decode_value(fs).name, 'a')
+
+ def test_encode_key(self):
+ d = self._get_instance()
+ value = unicode('a')
+ d.decode_keys = True
+ self.assertEquals(d._encode_key(value),'a')
+
+ def test_encode_value(self):
+ d = self._get_instance()
+ value = unicode('a')
+ self.assertEquals(d._encode_value(value),'a')
+
+ def test_repr_with_password(self):
+ d = self._get_instance(password='pwd')
+ self.assertEqual(repr(d), "UnicodeMultiDict([('password', '******')])")
+
+class NestedMultiDictTestCase(BaseDictTests, unittest.TestCase):
+ klass = multidict.NestedMultiDict
+
+ def test_getitem(self):
+ d = self.klass({'a':1})
+ self.assertEqual(d['a'], 1)
+
+ def test_getitem_raises(self):
+ d = self._get_instance()
+ self.assertRaises(KeyError, d.__getitem__, 'z')
+
+ def test_contains(self):
+ d = self._get_instance()
+ assert 'a' in d
+ assert 'z' not in d
+
+ def test_add(self):
+ d = self._get_instance()
+ self.assertRaises(KeyError, d.add, 'b', 3)
+
+ def test_set_del_item(self):
+ d = self._get_instance()
+ self.assertRaises(KeyError, d.__delitem__, 'a')
+ self.assertRaises(KeyError, d.__setitem__, 'a', 1)
+
+ def test_update(self):
+ d = self._get_instance()
+ self.assertRaises(KeyError, d.update, e=1)
+ self.assertRaises(KeyError, d.update, dict(x=1))
+ self.assertRaises(KeyError, d.update, [('y', 1)])
+
+ def test_setdefault(self):
+ d = self._get_instance()
+ self.assertRaises(KeyError, d.setdefault, 'a', 1)
+
+ def test_pop(self):
+ d = self._get_instance()
+ self.assertRaises(KeyError, d.pop, 'a')
+ self.assertRaises(KeyError, d.pop, 'a', 1)
+
+ def test_popitem(self):
+ d = self._get_instance()
+ self.assertRaises(KeyError, d.popitem, 'a')
+
+ def test_pop_wrong_args(self):
+ d = self._get_instance()
+ self.assertRaises(KeyError, d.pop, 'a', 1, 1)
+
+ def test_clear(self):
+ d = self._get_instance()
+ self.assertRaises(KeyError, d.clear)
+
+ def test_nonzero(self):
+ d = self._get_instance()
+ self.assertEqual(d.__nonzero__(), True)
+ d.dicts = [{}]
+ self.assertEqual(d.__nonzero__(), False)
+ assert not d
+
+class TrackableMultiDict(BaseDictTests, unittest.TestCase):
+ klass = multidict.TrackableMultiDict
+
+ def _get_instance(self, **kwargs):
+ if kwargs:
+ data = multidict.MultiDict(kwargs)
+ else:
+ data = self.data.copy()
+ def tracker(*args, **kwargs): pass
+ return self.klass(data, __tracker=tracker, __name='tracker')
+
+ def test_inititems(self):
+ #The first argument passed into the __init__ method
+ class Arg:
+ def items(self):
+ return [('a', u'\xe9'), ('a', 'e'), ('a', 'f'), ('b', 1)]
+
+ d = self._get_instance()
+ d._items = None
+ d.__init__(Arg())
+ self.assertEquals(self.d._items, self._list)
+
+ def test_nullextend(self):
+ d = self._get_instance()
+ self.assertEqual(d.extend(), None)
+ d.extend(test = 'a')
+ self.assertEqual(d['test'], 'a')
+
+ def test_listextend(self):
+ class Other:
+ def items(self):
+ return [u'\xe9', u'e', r'f', 1]
+
+ other = Other()
+ d = self._get_instance()
+ d.extend(other)
+
+ _list = [u'\xe9', u'e', r'f', 1]
+ for v in _list:
+ self.assertTrue(v in d._items)
+
+ def test_dictextend(self):
+ class Other:
+ def __getitem__(self, item):
+ return {'a':1, 'b':2, 'c':3}.get(item)
+
+ def keys(self):
+ return ['a', 'b', 'c']
+
+ other = Other()
+ d = self._get_instance()
+ d.extend(other)
+
+ _list = [('a', 1), ('b', 2), ('c', 3)]
+ for v in _list:
+ self.assertTrue(v in d._items)
+
+ def test_otherextend(self):
+ class Other(object):
+ def __iter__(self):
+ return iter([('a', 1)])
+
+ other = Other()
+ d = self._get_instance()
+ d.extend(other)
+
+ _list = [('a', 1)]
+ for v in _list:
+ self.assertTrue(v in d._items)
+
+ def test_repr_with_password(self):
+ d = self._get_instance(password='pwd')
+ self.assertEqual(repr(d), "tracker([('password', '******')])")
+
+class NoVarsTestCase(unittest.TestCase):
+ klass = multidict.NoVars
+
+ def _get_instance(self):
+ return self.klass()
+
+ def test_getitem(self):
+ d = self._get_instance()
+ self.assertRaises(KeyError, d.__getitem__, 'a')
+
+ def test_setitem(self):
+ d = self._get_instance()
+ self.assertRaises(KeyError, d.__setitem__, 'a')
+
+ def test_delitem(self):
+ d = self._get_instance()
+ self.assertRaises(KeyError, d.__delitem__, 'a')
+
+ def test_get(self):
+ d = self._get_instance()
+ self.assertEqual(d.get('a', default = 'b'), 'b')
+
+ def test_getall(self):
+ d = self._get_instance()
+ self.assertEqual(d.getall('a'), [])
+
+ def test_getone(self):
+ d = self._get_instance()
+ self.assertRaises(KeyError, d.getone, 'a')
+
+ def test_mixed(self):
+ d = self._get_instance()
+ self.assertEqual(d.mixed(), {})
+
+ def test_contains(self):
+ d = self._get_instance()
+ assert 'a' not in d
+
+ def test_copy(self):
+ d = self._get_instance()
+ self.assertEqual(d.copy(), d)
+
+ def test_len(self):
+ d = self._get_instance()
+ self.assertEqual(len(d), 0)
+
+ def test_repr(self):
+ d = self._get_instance()
+ self.assertEqual(repr(d), '<NoVars: N/A>')
+
+ def test_keys(self):
+ d = self._get_instance()
+ self.assertEqual(d.keys(), [])
+
+ def test_iterkeys(self):
+ d = self._get_instance()
+ self.assertEqual(list(d.iterkeys()), [])
+
+class DummyField(object):
+ def __init__(self, name, value, filename=None):
+ self.name = name
+ self.value = value
+ self.filename = filename
+
+class DummyFieldStorage(object):
+ def __init__(self, name, value, filename=None):
+ self.list = [DummyField(name, value, filename)]
+
diff --git a/lib/webob_1_1_1/tests/test_request.py b/lib/webob_1_1_1/tests/test_request.py
new file mode 100644
index 0000000..72609e3
--- /dev/null
+++ b/lib/webob_1_1_1/tests/test_request.py
@@ -0,0 +1,2501 @@
+import unittest, warnings
+from webob import Request, BaseRequest, UTC
+
+_marker = object()
+
+warnings.showwarning = lambda *args, **kw: None
+
+class BaseRequestTests(unittest.TestCase):
+ def _makeStringIO(self, text):
+ try:
+ from io import BytesIO
+ except ImportError: # Python < 2.6
+ from StringIO import StringIO as BytesIO
+ return BytesIO(text)
+
+ def test_ctor_environ_getter_raises_WTF(self):
+ self.assertRaises(TypeError, Request, {}, environ_getter=object())
+
+ def test_ctor_wo_environ_raises_WTF(self):
+ self.assertRaises(TypeError, Request, None)
+
+ def test_ctor_w_environ(self):
+ environ = {}
+ req = BaseRequest(environ)
+ self.assertEqual(req.environ, environ)
+
+ def test_body_file_getter(self):
+ body = 'input'
+ INPUT = self._makeStringIO(body)
+ environ = {'wsgi.input': INPUT,
+ 'CONTENT_LENGTH': len(body),
+ 'REQUEST_METHOD': 'POST',
+ }
+ req = BaseRequest(environ)
+ self.assert_(req.body_file is not INPUT)
+
+ def test_body_file_getter_seekable(self):
+ body = 'input'
+ INPUT = self._makeStringIO(body)
+ environ = {'wsgi.input': INPUT,
+ 'CONTENT_LENGTH': len(body),
+ 'REQUEST_METHOD': 'POST',
+ 'webob.is_body_seekable': True,
+ }
+ req = BaseRequest(environ)
+ self.assert_(req.body_file is INPUT)
+
+ def test_body_file_getter_cache(self):
+ body = 'input'
+ INPUT = self._makeStringIO(body)
+ environ = {'wsgi.input': INPUT,
+ 'CONTENT_LENGTH': len(body),
+ 'REQUEST_METHOD': 'POST',
+ }
+ req = BaseRequest(environ)
+ self.assert_(req.body_file is req.body_file)
+
+ def test_body_file_getter_unreadable(self):
+ body = 'input'
+ INPUT = self._makeStringIO(body)
+ environ = {'wsgi.input': INPUT, 'REQUEST_METHOD': 'FOO'}
+ req = BaseRequest(environ)
+ assert req.body_file_raw is INPUT
+ assert req.body_file is not INPUT
+ assert req.body_file.read() == ''
+
+ def test_body_file_setter_w_string(self):
+ BEFORE = self._makeStringIO('before')
+ AFTER = str('AFTER')
+ environ = {'wsgi.input': BEFORE,
+ 'CONTENT_LENGTH': len('before'),
+ 'REQUEST_METHOD': 'POST',
+ }
+ req = BaseRequest(environ)
+ warnings.simplefilter('ignore', PendingDeprecationWarning)
+ req.body_file = AFTER
+ warnings.resetwarnings()
+ self.assertEqual(req.content_length, len(AFTER))
+ self.assertEqual(req.body_file.read(), AFTER)
+ del req.body_file
+ self.assertEqual(req.content_length, 0)
+ assert req.is_body_seekable
+ req.body_file.seek(0)
+ self.assertEqual(req.body_file.read(), '')
+
+ def test_body_file_setter_non_string(self):
+ BEFORE = self._makeStringIO('before')
+ AFTER = self._makeStringIO('after')
+ environ = {'wsgi.input': BEFORE,
+ 'CONTENT_LENGTH': len('before'),
+ 'REQUEST_METHOD': 'POST'
+ }
+ req = BaseRequest(environ)
+ req.body_file = AFTER
+ self.assert_(req.body_file is AFTER)
+ self.assertEqual(req.content_length, None)
+
+ def test_body_file_deleter(self):
+ INPUT = self._makeStringIO('before')
+ environ = {'wsgi.input': INPUT,
+ 'CONTENT_LENGTH': len('before'),
+ 'REQUEST_METHOD': 'POST',
+ }
+ req = BaseRequest(environ)
+ del req.body_file
+ self.assertEqual(req.body_file.getvalue(), '')
+ self.assertEqual(req.content_length, 0)
+
+ def test_body_file_raw(self):
+ INPUT = self._makeStringIO('input')
+ environ = {'wsgi.input': INPUT,
+ 'CONTENT_LENGTH': len('input'),
+ 'REQUEST_METHOD': 'POST',
+ }
+ req = BaseRequest(environ)
+ self.assert_(req.body_file_raw is INPUT)
+
+ def test_body_file_seekable_input_not_seekable(self):
+ INPUT = self._makeStringIO('input')
+ INPUT.seek(1, 0) # consume
+ environ = {'wsgi.input': INPUT,
+ 'webob.is_body_seekable': False,
+ 'CONTENT_LENGTH': len('input')-1,
+ 'REQUEST_METHOD': 'POST',
+ }
+ req = BaseRequest(environ)
+ seekable = req.body_file_seekable
+ self.assert_(seekable is not INPUT)
+ self.assertEqual(seekable.getvalue(), 'nput')
+
+ def test_body_file_seekable_input_is_seekable(self):
+ INPUT = self._makeStringIO('input')
+ INPUT.seek(1, 0) # consume
+ environ = {'wsgi.input': INPUT,
+ 'webob.is_body_seekable': True,
+ 'CONTENT_LENGTH': len('input')-1,
+ 'REQUEST_METHOD': 'POST',
+ }
+ req = BaseRequest(environ)
+ seekable = req.body_file_seekable
+ self.assert_(seekable is INPUT)
+
+ def test_scheme(self):
+ environ = {'wsgi.url_scheme': 'something:',
+ }
+ req = BaseRequest(environ)
+ self.assertEqual(req.scheme, 'something:')
+
+ def test_method(self):
+ environ = {'REQUEST_METHOD': 'OPTIONS',
+ }
+ req = BaseRequest(environ)
+ self.assertEqual(req.method, 'OPTIONS')
+
+ def test_http_version(self):
+ environ = {'SERVER_PROTOCOL': '1.1',
+ }
+ req = BaseRequest(environ)
+ self.assertEqual(req.http_version, '1.1')
+
+ def test_script_name(self):
+ environ = {'SCRIPT_NAME': '/script',
+ }
+ req = BaseRequest(environ)
+ self.assertEqual(req.script_name, '/script')
+
+ def test_path_info(self):
+ environ = {'PATH_INFO': '/path/info',
+ }
+ req = BaseRequest(environ)
+ self.assertEqual(req.path_info, '/path/info')
+
+ def test_content_length_getter(self):
+ environ = {'CONTENT_LENGTH': '1234',
+ }
+ req = BaseRequest(environ)
+ self.assertEqual(req.content_length, 1234)
+
+ def test_content_length_setter_w_str(self):
+ environ = {'CONTENT_LENGTH': '1234',
+ }
+ req = BaseRequest(environ)
+ req.content_length = '3456'
+ self.assertEqual(req.content_length, 3456)
+
+ def test_remote_user(self):
+ environ = {'REMOTE_USER': 'phred',
+ }
+ req = BaseRequest(environ)
+ self.assertEqual(req.remote_user, 'phred')
+
+ def test_remote_addr(self):
+ environ = {'REMOTE_ADDR': '1.2.3.4',
+ }
+ req = BaseRequest(environ)
+ self.assertEqual(req.remote_addr, '1.2.3.4')
+
+ def test_query_string(self):
+ environ = {'QUERY_STRING': 'foo=bar&baz=bam',
+ }
+ req = BaseRequest(environ)
+ self.assertEqual(req.query_string, 'foo=bar&baz=bam')
+
+ def test_server_name(self):
+ environ = {'SERVER_NAME': 'somehost.tld',
+ }
+ req = BaseRequest(environ)
+ self.assertEqual(req.server_name, 'somehost.tld')
+
+ def test_server_port_getter(self):
+ environ = {'SERVER_PORT': '6666',
+ }
+ req = BaseRequest(environ)
+ self.assertEqual(req.server_port, 6666)
+
+ def test_server_port_setter_with_string(self):
+ environ = {'SERVER_PORT': '6666',
+ }
+ req = BaseRequest(environ)
+ req.server_port = '6667'
+ self.assertEqual(req.server_port, 6667)
+
+ def test_uscript_name(self):
+ environ = {'SCRIPT_NAME': '/script',
+ }
+ req = BaseRequest(environ)
+ self.assert_(isinstance(req.uscript_name, unicode))
+ self.assertEqual(req.uscript_name, '/script')
+
+ def test_upath_info(self):
+ environ = {'PATH_INFO': '/path/info',
+ }
+ req = BaseRequest(environ)
+ self.assert_(isinstance(req.upath_info, unicode))
+ self.assertEqual(req.upath_info, '/path/info')
+
+ def test_content_type_getter_no_parameters(self):
+ environ = {'CONTENT_TYPE': 'application/xml+foobar',
+ }
+ req = BaseRequest(environ)
+ self.assertEqual(req.content_type, 'application/xml+foobar')
+
+ def test_content_type_getter_w_parameters(self):
+ environ = {'CONTENT_TYPE': 'application/xml+foobar;charset="utf8"',
+ }
+ req = BaseRequest(environ)
+ self.assertEqual(req.content_type, 'application/xml+foobar')
+
+ def test_content_type_setter_w_None(self):
+ environ = {'CONTENT_TYPE': 'application/xml+foobar;charset="utf8"',
+ }
+ req = BaseRequest(environ)
+ req.content_type = None
+ self.assertEqual(req.content_type, '')
+ self.assert_('CONTENT_TYPE' not in environ)
+
+ def test_content_type_setter_existing_paramter_no_new_paramter(self):
+ environ = {'CONTENT_TYPE': 'application/xml+foobar;charset="utf8"',
+ }
+ req = BaseRequest(environ)
+ req.content_type = 'text/xml'
+ self.assertEqual(req.content_type, 'text/xml')
+ self.assertEqual(environ['CONTENT_TYPE'], 'text/xml;charset="utf8"')
+
+ def test_content_type_deleter_clears_environ_value(self):
+ environ = {'CONTENT_TYPE': 'application/xml+foobar;charset="utf8"',
+ }
+ req = BaseRequest(environ)
+ del req.content_type
+ self.assertEqual(req.content_type, '')
+ self.assert_('CONTENT_TYPE' not in environ)
+
+ def test_content_type_deleter_no_environ_value(self):
+ environ = {}
+ req = BaseRequest(environ)
+ del req.content_type
+ self.assertEqual(req.content_type, '')
+ self.assert_('CONTENT_TYPE' not in environ)
+
+ def test_charset_getter_cache_hit(self):
+ CT = 'application/xml+foobar'
+ environ = {'CONTENT_TYPE': CT,
+ }
+ req = BaseRequest(environ)
+ req._charset_cache = (CT, 'cp1252')
+ self.assertEqual(req.charset, 'cp1252')
+
+ def test_charset_getter_cache_miss_w_parameter(self):
+ CT = 'application/xml+foobar;charset="utf8"'
+ environ = {'CONTENT_TYPE': CT,
+ }
+ req = BaseRequest(environ)
+ self.assertEqual(req.charset, 'utf8')
+ self.assertEqual(req._charset_cache, (CT, 'utf8'))
+
+ def test_charset_getter_cache_miss_wo_parameter(self):
+ CT = 'application/xml+foobar'
+ environ = {'CONTENT_TYPE': CT,
+ }
+ req = BaseRequest(environ)
+ self.assertEqual(req.charset, 'UTF-8')
+ self.assertEqual(req._charset_cache, (CT, 'UTF-8'))
+
+ def test_charset_setter_None_w_parameter(self):
+ CT = 'application/xml+foobar;charset="utf8"'
+ environ = {'CONTENT_TYPE': CT,
+ }
+ req = BaseRequest(environ)
+ req.charset = None
+ self.assertEqual(environ['CONTENT_TYPE'], 'application/xml+foobar')
+ self.assertEqual(req.charset, 'UTF-8')
+
+ def test_charset_setter_empty_w_parameter(self):
+ CT = 'application/xml+foobar;charset="utf8"'
+ environ = {'CONTENT_TYPE': CT,
+ }
+ req = BaseRequest(environ)
+ req.charset = ''
+ self.assertEqual(environ['CONTENT_TYPE'], 'application/xml+foobar')
+ self.assertEqual(req.charset, 'UTF-8')
+
+ def test_charset_setter_nonempty_w_parameter(self):
+ CT = 'application/xml+foobar;charset="utf8"'
+ environ = {'CONTENT_TYPE': CT,
+ }
+ req = BaseRequest(environ)
+ req.charset = 'cp1252'
+ self.assertEqual(environ['CONTENT_TYPE'],
+ #'application/xml+foobar; charset="cp1252"') WTF?
+ 'application/xml+foobar;charset=cp1252',
+ )
+ self.assertEqual(req.charset, 'cp1252')
+
+ def test_charset_setter_nonempty_wo_parameter(self):
+ CT = 'application/xml+foobar'
+ environ = {'CONTENT_TYPE': CT,
+ }
+ req = BaseRequest(environ)
+ req.charset = 'cp1252'
+ self.assertEqual(environ['CONTENT_TYPE'],
+ 'application/xml+foobar; charset="cp1252"',
+ #'application/xml+foobar;charset=cp1252', WTF?
+ )
+ self.assertEqual(req.charset, 'cp1252')
+
+ def test_charset_deleter_w_parameter(self):
+ CT = 'application/xml+foobar;charset="utf8"'
+ environ = {'CONTENT_TYPE': CT,
+ }
+ req = BaseRequest(environ)
+ del req.charset
+ self.assertEqual(environ['CONTENT_TYPE'], 'application/xml+foobar')
+ self.assertEqual(req.charset, 'UTF-8')
+
+ def test_headers_getter_miss(self):
+ CONTENT_TYPE = 'application/xml+foobar;charset="utf8"'
+ environ = {'CONTENT_TYPE': CONTENT_TYPE,
+ 'CONTENT_LENGTH': '123',
+ }
+ req = BaseRequest(environ)
+ headers = req.headers
+ self.assertEqual(headers,
+ {'Content-Type': CONTENT_TYPE,
+ 'Content-Length': '123'})
+ self.assertEqual(req._headers, headers)
+
+ def test_headers_getter_hit(self):
+ CONTENT_TYPE = 'application/xml+foobar;charset="utf8"'
+ environ = {'CONTENT_TYPE': CONTENT_TYPE,
+ 'CONTENT_LENGTH': '123',
+ }
+ req = BaseRequest(environ)
+ req._headers = {'Foo': 'Bar'}
+ self.assertEqual(req.headers,
+ {'Foo': 'Bar'})
+
+ def test_headers_setter(self):
+ CONTENT_TYPE = 'application/xml+foobar;charset="utf8"'
+ environ = {'CONTENT_TYPE': CONTENT_TYPE,
+ 'CONTENT_LENGTH': '123',
+ }
+ req = BaseRequest(environ)
+ req._headers = {'Foo': 'Bar'}
+ req.headers = {'Qux': 'Spam'}
+ self.assertEqual(req.headers,
+ {'Qux': 'Spam'})
+
+ def test_no_headers_deleter(self):
+ CONTENT_TYPE = 'application/xml+foobar;charset="utf8"'
+ environ = {'CONTENT_TYPE': CONTENT_TYPE,
+ 'CONTENT_LENGTH': '123',
+ }
+ req = BaseRequest(environ)
+ def _test():
+ del req.headers
+ self.assertRaises(AttributeError, _test)
+
+ def test_host_url_w_http_host_and_no_port(self):
+ environ = {'wsgi.url_scheme': 'http',
+ 'HTTP_HOST': 'example.com',
+ }
+ req = BaseRequest(environ)
+ self.assertEqual(req.host_url, 'http://example.com')
+
+ def test_host_url_w_http_host_and_standard_port(self):
+ environ = {'wsgi.url_scheme': 'http',
+ 'HTTP_HOST': 'example.com:80',
+ }
+ req = BaseRequest(environ)
+ self.assertEqual(req.host_url, 'http://example.com')
+
+ def test_host_url_w_http_host_and_oddball_port(self):
+ environ = {'wsgi.url_scheme': 'http',
+ 'HTTP_HOST': 'example.com:8888',
+ }
+ req = BaseRequest(environ)
+ self.assertEqual(req.host_url, 'http://example.com:8888')
+
+ def test_host_url_w_http_host_https_and_no_port(self):
+ environ = {'wsgi.url_scheme': 'https',
+ 'HTTP_HOST': 'example.com',
+ }
+ req = BaseRequest(environ)
+ self.assertEqual(req.host_url, 'https://example.com')
+
+ def test_host_url_w_http_host_https_and_standard_port(self):
+ environ = {'wsgi.url_scheme': 'https',
+ 'HTTP_HOST': 'example.com:443',
+ }
+ req = BaseRequest(environ)
+ self.assertEqual(req.host_url, 'https://example.com')
+
+ def test_host_url_w_http_host_https_and_oddball_port(self):
+ environ = {'wsgi.url_scheme': 'https',
+ 'HTTP_HOST': 'example.com:4333',
+ }
+ req = BaseRequest(environ)
+ self.assertEqual(req.host_url, 'https://example.com:4333')
+
+ def test_host_url_wo_http_host(self):
+ environ = {'wsgi.url_scheme': 'https',
+ 'SERVER_NAME': 'example.com',
+ 'SERVER_PORT': '4333',
+ }
+ req = BaseRequest(environ)
+ self.assertEqual(req.host_url, 'https://example.com:4333')
+
+ def test_application_url(self):
+ environ = {'wsgi.url_scheme': 'http',
+ 'SERVER_NAME': 'example.com',
+ 'SERVER_PORT': '80',
+ 'SCRIPT_NAME': '/script',
+ }
+ req = BaseRequest(environ)
+ self.assertEqual(req.application_url, 'http://example.com/script')
+
+ def test_path_url(self):
+ environ = {'wsgi.url_scheme': 'http',
+ 'SERVER_NAME': 'example.com',
+ 'SERVER_PORT': '80',
+ 'SCRIPT_NAME': '/script',
+ 'PATH_INFO': '/path/info',
+ }
+ req = BaseRequest(environ)
+ self.assertEqual(req.path_url, 'http://example.com/script/path/info')
+
+ def test_path(self):
+ environ = {'wsgi.url_scheme': 'http',
+ 'SERVER_NAME': 'example.com',
+ 'SERVER_PORT': '80',
+ 'SCRIPT_NAME': '/script',
+ 'PATH_INFO': '/path/info',
+ }
+ req = BaseRequest(environ)
+ self.assertEqual(req.path, '/script/path/info')
+
+ def test_path_qs_no_qs(self):
+ environ = {'wsgi.url_scheme': 'http',
+ 'SERVER_NAME': 'example.com',
+ 'SERVER_PORT': '80',
+ 'SCRIPT_NAME': '/script',
+ 'PATH_INFO': '/path/info',
+ }
+ req = BaseRequest(environ)
+ self.assertEqual(req.path_qs, '/script/path/info')
+
+ def test_path_qs_w_qs(self):
+ environ = {'wsgi.url_scheme': 'http',
+ 'SERVER_NAME': 'example.com',
+ 'SERVER_PORT': '80',
+ 'SCRIPT_NAME': '/script',
+ 'PATH_INFO': '/path/info',
+ 'QUERY_STRING': 'foo=bar&baz=bam'
+ }
+ req = BaseRequest(environ)
+ self.assertEqual(req.path_qs, '/script/path/info?foo=bar&baz=bam')
+
+ def test_url_no_qs(self):
+ environ = {'wsgi.url_scheme': 'http',
+ 'SERVER_NAME': 'example.com',
+ 'SERVER_PORT': '80',
+ 'SCRIPT_NAME': '/script',
+ 'PATH_INFO': '/path/info',
+ }
+ req = BaseRequest(environ)
+ self.assertEqual(req.url, 'http://example.com/script/path/info')
+
+ def test_url_w_qs(self):
+ environ = {'wsgi.url_scheme': 'http',
+ 'SERVER_NAME': 'example.com',
+ 'SERVER_PORT': '80',
+ 'SCRIPT_NAME': '/script',
+ 'PATH_INFO': '/path/info',
+ 'QUERY_STRING': 'foo=bar&baz=bam'
+ }
+ req = BaseRequest(environ)
+ self.assertEqual(req.url,
+ 'http://example.com/script/path/info?foo=bar&baz=bam')
+
+ def test_relative_url_to_app_true_wo_leading_slash(self):
+ environ = {'wsgi.url_scheme': 'http',
+ 'SERVER_NAME': 'example.com',
+ 'SERVER_PORT': '80',
+ 'SCRIPT_NAME': '/script',
+ 'PATH_INFO': '/path/info',
+ 'QUERY_STRING': 'foo=bar&baz=bam'
+ }
+ req = BaseRequest(environ)
+ self.assertEqual(req.relative_url('other/page', True),
+ 'http://example.com/script/other/page')
+
+ def test_relative_url_to_app_true_w_leading_slash(self):
+ environ = {'wsgi.url_scheme': 'http',
+ 'SERVER_NAME': 'example.com',
+ 'SERVER_PORT': '80',
+ 'SCRIPT_NAME': '/script',
+ 'PATH_INFO': '/path/info',
+ 'QUERY_STRING': 'foo=bar&baz=bam'
+ }
+ req = BaseRequest(environ)
+ self.assertEqual(req.relative_url('/other/page', True),
+ 'http://example.com/other/page')
+
+ def test_relative_url_to_app_false_other_w_leading_slash(self):
+ environ = {'wsgi.url_scheme': 'http',
+ 'SERVER_NAME': 'example.com',
+ 'SERVER_PORT': '80',
+ 'SCRIPT_NAME': '/script',
+ 'PATH_INFO': '/path/info',
+ 'QUERY_STRING': 'foo=bar&baz=bam'
+ }
+ req = BaseRequest(environ)
+ self.assertEqual(req.relative_url('/other/page', False),
+ 'http://example.com/other/page')
+
+ def test_relative_url_to_app_false_other_wo_leading_slash(self):
+ environ = {'wsgi.url_scheme': 'http',
+ 'SERVER_NAME': 'example.com',
+ 'SERVER_PORT': '80',
+ 'SCRIPT_NAME': '/script',
+ 'PATH_INFO': '/path/info',
+ 'QUERY_STRING': 'foo=bar&baz=bam'
+ }
+ req = BaseRequest(environ)
+ self.assertEqual(req.relative_url('other/page', False),
+ 'http://example.com/script/path/other/page')
+
+ def test_path_info_pop_empty(self):
+ environ = {'wsgi.url_scheme': 'http',
+ 'SERVER_NAME': 'example.com',
+ 'SERVER_PORT': '80',
+ 'SCRIPT_NAME': '/script',
+ 'PATH_INFO': '',
+ }
+ req = BaseRequest(environ)
+ popped = req.path_info_pop()
+ self.assertEqual(popped, None)
+ self.assertEqual(environ['SCRIPT_NAME'], '/script')
+
+ def test_path_info_pop_just_leading_slash(self):
+ environ = {'wsgi.url_scheme': 'http',
+ 'SERVER_NAME': 'example.com',
+ 'SERVER_PORT': '80',
+ 'SCRIPT_NAME': '/script',
+ 'PATH_INFO': '/',
+ }
+ req = BaseRequest(environ)
+ popped = req.path_info_pop()
+ self.assertEqual(popped, '')
+ self.assertEqual(environ['SCRIPT_NAME'], '/script/')
+ self.assertEqual(environ['PATH_INFO'], '')
+
+ def test_path_info_pop_non_empty_no_pattern(self):
+ environ = {'wsgi.url_scheme': 'http',
+ 'SERVER_NAME': 'example.com',
+ 'SERVER_PORT': '80',
+ 'SCRIPT_NAME': '/script',
+ 'PATH_INFO': '/path/info',
+ }
+ req = BaseRequest(environ)
+ popped = req.path_info_pop()
+ self.assertEqual(popped, 'path')
+ self.assertEqual(environ['SCRIPT_NAME'], '/script/path')
+ self.assertEqual(environ['PATH_INFO'], '/info')
+
+ def test_path_info_pop_non_empty_w_pattern_miss(self):
+ import re
+ PATTERN = re.compile('miss')
+ environ = {'wsgi.url_scheme': 'http',
+ 'SERVER_NAME': 'example.com',
+ 'SERVER_PORT': '80',
+ 'SCRIPT_NAME': '/script',
+ 'PATH_INFO': '/path/info',
+ }
+ req = BaseRequest(environ)
+ popped = req.path_info_pop(PATTERN)
+ self.assertEqual(popped, None)
+ self.assertEqual(environ['SCRIPT_NAME'], '/script')
+ self.assertEqual(environ['PATH_INFO'], '/path/info')
+
+ def test_path_info_pop_non_empty_w_pattern_hit(self):
+ import re
+ PATTERN = re.compile('path')
+ environ = {'wsgi.url_scheme': 'http',
+ 'SERVER_NAME': 'example.com',
+ 'SERVER_PORT': '80',
+ 'SCRIPT_NAME': '/script',
+ 'PATH_INFO': '/path/info',
+ }
+ req = BaseRequest(environ)
+ popped = req.path_info_pop(PATTERN)
+ self.assertEqual(popped, 'path')
+ self.assertEqual(environ['SCRIPT_NAME'], '/script/path')
+ self.assertEqual(environ['PATH_INFO'], '/info')
+
+ def test_path_info_pop_skips_empty_elements(self):
+ environ = {'wsgi.url_scheme': 'http',
+ 'SERVER_NAME': 'example.com',
+ 'SERVER_PORT': '80',
+ 'SCRIPT_NAME': '/script',
+ 'PATH_INFO': '//path/info',
+ }
+ req = BaseRequest(environ)
+ popped = req.path_info_pop()
+ self.assertEqual(popped, 'path')
+ self.assertEqual(environ['SCRIPT_NAME'], '/script//path')
+ self.assertEqual(environ['PATH_INFO'], '/info')
+
+ def test_path_info_peek_empty(self):
+ environ = {'wsgi.url_scheme': 'http',
+ 'SERVER_NAME': 'example.com',
+ 'SERVER_PORT': '80',
+ 'SCRIPT_NAME': '/script',
+ 'PATH_INFO': '',
+ }
+ req = BaseRequest(environ)
+ peeked = req.path_info_peek()
+ self.assertEqual(peeked, None)
+ self.assertEqual(environ['SCRIPT_NAME'], '/script')
+ self.assertEqual(environ['PATH_INFO'], '')
+
+ def test_path_info_peek_just_leading_slash(self):
+ environ = {'wsgi.url_scheme': 'http',
+ 'SERVER_NAME': 'example.com',
+ 'SERVER_PORT': '80',
+ 'SCRIPT_NAME': '/script',
+ 'PATH_INFO': '/',
+ }
+ req = BaseRequest(environ)
+ peeked = req.path_info_peek()
+ self.assertEqual(peeked, '')
+ self.assertEqual(environ['SCRIPT_NAME'], '/script')
+ self.assertEqual(environ['PATH_INFO'], '/')
+
+ def test_path_info_peek_non_empty(self):
+ environ = {'wsgi.url_scheme': 'http',
+ 'SERVER_NAME': 'example.com',
+ 'SERVER_PORT': '80',
+ 'SCRIPT_NAME': '/script',
+ 'PATH_INFO': '/path',
+ }
+ req = BaseRequest(environ)
+ peeked = req.path_info_peek()
+ self.assertEqual(peeked, 'path')
+ self.assertEqual(environ['SCRIPT_NAME'], '/script')
+ self.assertEqual(environ['PATH_INFO'], '/path')
+
+ def test_urlvars_getter_w_paste_key(self):
+ environ = {'paste.urlvars': {'foo': 'bar'},
+ }
+ req = BaseRequest(environ)
+ self.assertEqual(req.urlvars, {'foo': 'bar'})
+
+ def test_urlvars_getter_w_wsgiorg_key(self):
+ environ = {'wsgiorg.routing_args': ((), {'foo': 'bar'}),
+ }
+ req = BaseRequest(environ)
+ self.assertEqual(req.urlvars, {'foo': 'bar'})
+
+ def test_urlvars_getter_wo_keys(self):
+ environ = {}
+ req = BaseRequest(environ)
+ self.assertEqual(req.urlvars, {})
+ self.assertEqual(environ['wsgiorg.routing_args'], ((), {}))
+
+ def test_urlvars_setter_w_paste_key(self):
+ environ = {'paste.urlvars': {'foo': 'bar'},
+ }
+ req = BaseRequest(environ)
+ req.urlvars = {'baz': 'bam'}
+ self.assertEqual(req.urlvars, {'baz': 'bam'})
+ self.assertEqual(environ['paste.urlvars'], {'baz': 'bam'})
+ self.assert_('wsgiorg.routing_args' not in environ)
+
+ def test_urlvars_setter_w_wsgiorg_key(self):
+ environ = {'wsgiorg.routing_args': ((), {'foo': 'bar'}),
+ 'paste.urlvars': {'qux': 'spam'},
+ }
+ req = BaseRequest(environ)
+ req.urlvars = {'baz': 'bam'}
+ self.assertEqual(req.urlvars, {'baz': 'bam'})
+ self.assertEqual(environ['wsgiorg.routing_args'], ((), {'baz': 'bam'}))
+ self.assert_('paste.urlvars' not in environ)
+
+ def test_urlvars_setter_wo_keys(self):
+ environ = {}
+ req = BaseRequest(environ)
+ req.urlvars = {'baz': 'bam'}
+ self.assertEqual(req.urlvars, {'baz': 'bam'})
+ self.assertEqual(environ['wsgiorg.routing_args'], ((), {'baz': 'bam'}))
+ self.assert_('paste.urlvars' not in environ)
+
+ def test_urlvars_deleter_w_paste_key(self):
+ environ = {'paste.urlvars': {'foo': 'bar'},
+ }
+ req = BaseRequest(environ)
+ del req.urlvars
+ self.assertEqual(req.urlvars, {})
+ self.assert_('paste.urlvars' not in environ)
+ self.assertEqual(environ['wsgiorg.routing_args'], ((), {}))
+
+ def test_urlvars_deleter_w_wsgiorg_key_non_empty_tuple(self):
+ environ = {'wsgiorg.routing_args': (('a', 'b'), {'foo': 'bar'}),
+ 'paste.urlvars': {'qux': 'spam'},
+ }
+ req = BaseRequest(environ)
+ del req.urlvars
+ self.assertEqual(req.urlvars, {})
+ self.assertEqual(environ['wsgiorg.routing_args'], (('a', 'b'), {}))
+ self.assert_('paste.urlvars' not in environ)
+
+ def test_urlvars_deleter_w_wsgiorg_key_empty_tuple(self):
+ environ = {'wsgiorg.routing_args': ((), {'foo': 'bar'}),
+ 'paste.urlvars': {'qux': 'spam'},
+ }
+ req = BaseRequest(environ)
+ del req.urlvars
+ self.assertEqual(req.urlvars, {})
+ self.assertEqual(environ['wsgiorg.routing_args'], ((), {}))
+ self.assert_('paste.urlvars' not in environ)
+
+ def test_urlvars_deleter_wo_keys(self):
+ environ = {}
+ req = BaseRequest(environ)
+ del req.urlvars
+ self.assertEqual(req.urlvars, {})
+ self.assertEqual(environ['wsgiorg.routing_args'], ((), {}))
+ self.assert_('paste.urlvars' not in environ)
+
+ def test_urlargs_getter_w_paste_key(self):
+ environ = {'paste.urlvars': {'foo': 'bar'},
+ }
+ req = BaseRequest(environ)
+ self.assertEqual(req.urlargs, ())
+
+ def test_urlargs_getter_w_wsgiorg_key(self):
+ environ = {'wsgiorg.routing_args': (('a', 'b'), {'foo': 'bar'}),
+ }
+ req = BaseRequest(environ)
+ self.assertEqual(req.urlargs, ('a', 'b'))
+
+ def test_urlargs_getter_wo_keys(self):
+ environ = {}
+ req = BaseRequest(environ)
+ self.assertEqual(req.urlargs, ())
+ self.assert_('wsgiorg.routing_args' not in environ)
+
+ def test_urlargs_setter_w_paste_key(self):
+ environ = {'paste.urlvars': {'foo': 'bar'},
+ }
+ req = BaseRequest(environ)
+ req.urlargs = ('a', 'b')
+ self.assertEqual(req.urlargs, ('a', 'b'))
+ self.assertEqual(environ['wsgiorg.routing_args'],
+ (('a', 'b'), {'foo': 'bar'}))
+ self.assert_('paste.urlvars' not in environ)
+
+ def test_urlargs_setter_w_wsgiorg_key(self):
+ environ = {'wsgiorg.routing_args': ((), {'foo': 'bar'}),
+ }
+ req = BaseRequest(environ)
+ req.urlargs = ('a', 'b')
+ self.assertEqual(req.urlargs, ('a', 'b'))
+ self.assertEqual(environ['wsgiorg.routing_args'],
+ (('a', 'b'), {'foo': 'bar'}))
+
+ def test_urlargs_setter_wo_keys(self):
+ environ = {}
+ req = BaseRequest(environ)
+ req.urlargs = ('a', 'b')
+ self.assertEqual(req.urlargs, ('a', 'b'))
+ self.assertEqual(environ['wsgiorg.routing_args'],
+ (('a', 'b'), {}))
+ self.assert_('paste.urlvars' not in environ)
+
+ def test_urlargs_deleter_w_wsgiorg_key(self):
+ environ = {'wsgiorg.routing_args': (('a', 'b'), {'foo': 'bar'}),
+ }
+ req = BaseRequest(environ)
+ del req.urlargs
+ self.assertEqual(req.urlargs, ())
+ self.assertEqual(environ['wsgiorg.routing_args'],
+ ((), {'foo': 'bar'}))
+
+ def test_urlargs_deleter_w_wsgiorg_key_empty(self):
+ environ = {'wsgiorg.routing_args': ((), {}),
+ }
+ req = BaseRequest(environ)
+ del req.urlargs
+ self.assertEqual(req.urlargs, ())
+ self.assert_('paste.urlvars' not in environ)
+ self.assert_('wsgiorg.routing_args' not in environ)
+
+ def test_urlargs_deleter_wo_keys(self):
+ environ = {}
+ req = BaseRequest(environ)
+ del req.urlargs
+ self.assertEqual(req.urlargs, ())
+ self.assert_('paste.urlvars' not in environ)
+ self.assert_('wsgiorg.routing_args' not in environ)
+
+ def test_str_cookies_empty_environ(self):
+ req = BaseRequest({})
+ self.assertEqual(req.str_cookies, {})
+
+ def test_str_cookies_w_webob_parsed_cookies_matching_source(self):
+ environ = {
+ 'HTTP_COOKIE': 'a=b',
+ 'webob._parsed_cookies': ('a=b', {'a': 'b'}),
+ }
+ req = BaseRequest(environ)
+ self.assertEqual(req.str_cookies, {'a': 'b'})
+
+ def test_str_cookies_w_webob_parsed_cookies_mismatched_source(self):
+ environ = {
+ 'HTTP_COOKIE': 'a=b',
+ 'webob._parsed_cookies': ('a=b;c=d', {'a': 'b', 'c': 'd'}),
+ }
+ req = BaseRequest(environ)
+ self.assertEqual(req.str_cookies, {'a': 'b'})
+
+ def test_is_xhr_no_header(self):
+ req = BaseRequest({})
+ self.assert_(not req.is_xhr)
+
+ def test_is_xhr_header_miss(self):
+ environ = {'HTTP_X_REQUESTED_WITH': 'notAnXMLHTTPRequest'}
+ req = BaseRequest(environ)
+ self.assert_(not req.is_xhr)
+
+ def test_is_xhr_header_hit(self):
+ environ = {'HTTP_X_REQUESTED_WITH': 'XMLHttpRequest'}
+ req = BaseRequest(environ)
+ self.assert_(req.is_xhr)
+
+ # host
+ def test_host_getter_w_HTTP_HOST(self):
+ environ = {'HTTP_HOST': 'example.com:8888'}
+ req = BaseRequest(environ)
+ self.assertEqual(req.host, 'example.com:8888')
+
+ def test_host_getter_wo_HTTP_HOST(self):
+ environ = {'SERVER_NAME': 'example.com',
+ 'SERVER_PORT': '8888'}
+ req = BaseRequest(environ)
+ self.assertEqual(req.host, 'example.com:8888')
+
+ def test_host_setter(self):
+ environ = {}
+ req = BaseRequest(environ)
+ req.host = 'example.com:8888'
+ self.assertEqual(environ['HTTP_HOST'], 'example.com:8888')
+
+ def test_host_deleter_hit(self):
+ environ = {'HTTP_HOST': 'example.com:8888'}
+ req = BaseRequest(environ)
+ del req.host
+ self.assert_('HTTP_HOST' not in environ)
+
+ def test_host_deleter_miss(self):
+ environ = {}
+ req = BaseRequest(environ)
+ del req.host # doesn't raise
+
+ # body
+ def test_body_getter(self):
+ INPUT = self._makeStringIO('input')
+ environ = {'wsgi.input': INPUT,
+ 'webob.is_body_seekable': True,
+ 'CONTENT_LENGTH': len('input'),
+ 'REQUEST_METHOD': 'POST'
+ }
+ req = BaseRequest(environ)
+ self.assertEqual(req.body, 'input')
+ self.assertEqual(req.content_length, len('input'))
+ def test_body_setter_None(self):
+ INPUT = self._makeStringIO('input')
+ environ = {'wsgi.input': INPUT,
+ 'webob.is_body_seekable': True,
+ 'CONTENT_LENGTH': len('input'),
+ 'REQUEST_METHOD': 'POST'
+ }
+ req = BaseRequest(environ)
+ req.body = None
+ self.assertEqual(req.body, '')
+ self.assertEqual(req.content_length, 0)
+ self.assert_(req.is_body_seekable)
+ def test_body_setter_non_string_raises(self):
+ req = BaseRequest({})
+ def _test():
+ req.body = object()
+ self.assertRaises(TypeError, _test)
+ def test_body_setter_value(self):
+ BEFORE = self._makeStringIO('before')
+ environ = {'wsgi.input': BEFORE,
+ 'webob.is_body_seekable': True,
+ 'CONTENT_LENGTH': len('before'),
+ 'REQUEST_METHOD': 'POST'
+ }
+ req = BaseRequest(environ)
+ req.body = 'after'
+ self.assertEqual(req.body, 'after')
+ self.assertEqual(req.content_length, len('after'))
+ self.assert_(req.is_body_seekable)
+ def test_body_deleter_None(self):
+ INPUT = self._makeStringIO('input')
+ environ = {'wsgi.input': INPUT,
+ 'webob.is_body_seekable': True,
+ 'CONTENT_LENGTH': len('input'),
+ 'REQUEST_METHOD': 'POST',
+ }
+ req = BaseRequest(environ)
+ del req.body
+ self.assertEqual(req.body, '')
+ self.assertEqual(req.content_length, 0)
+ self.assert_(req.is_body_seekable)
+
+ def test_str_POST_not_POST_or_PUT(self):
+ from webob.multidict import NoVars
+ environ = {'REQUEST_METHOD': 'GET',
+ }
+ req = BaseRequest(environ)
+ result = req.str_POST
+ self.assert_(isinstance(result, NoVars))
+ self.assert_(result.reason.startswith('Not a form request'))
+
+ def test_str_POST_existing_cache_hit(self):
+ INPUT = self._makeStringIO('input')
+ environ = {'wsgi.input': INPUT,
+ 'REQUEST_METHOD': 'POST',
+ 'webob._parsed_post_vars': ({'foo': 'bar'}, INPUT),
+ }
+ req = BaseRequest(environ)
+ result = req.str_POST
+ self.assertEqual(result, {'foo': 'bar'})
+
+ def test_str_PUT_missing_content_type(self):
+ from webob.multidict import NoVars
+ INPUT = self._makeStringIO('input')
+ environ = {'wsgi.input': INPUT,
+ 'REQUEST_METHOD': 'PUT',
+ }
+ req = BaseRequest(environ)
+ result = req.str_POST
+ self.assert_(isinstance(result, NoVars))
+ self.assert_(result.reason.startswith('Not an HTML form submission'))
+
+ def test_str_PUT_bad_content_type(self):
+ from webob.multidict import NoVars
+ INPUT = self._makeStringIO('input')
+ environ = {'wsgi.input': INPUT,
+ 'REQUEST_METHOD': 'PUT',
+ 'CONTENT_TYPE': 'text/plain',
+ }
+ req = BaseRequest(environ)
+ result = req.str_POST
+ self.assert_(isinstance(result, NoVars))
+ self.assert_(result.reason.startswith('Not an HTML form submission'))
+
+ def test_str_POST_multipart(self):
+ BODY_TEXT = (
+ '------------------------------deb95b63e42a\n'
+ 'Content-Disposition: form-data; name="foo"\n'
+ '\n'
+ 'foo\n'
+ '------------------------------deb95b63e42a\n'
+ 'Content-Disposition: form-data; name="bar"; filename="bar.txt"\n'
+ 'Content-type: application/octet-stream\n'
+ '\n'
+ 'these are the contents of the file "bar.txt"\n'
+ '\n'
+ '------------------------------deb95b63e42a--\n')
+ INPUT = self._makeStringIO(BODY_TEXT)
+ environ = {'wsgi.input': INPUT,
+ 'webob.is_body_seekable': True,
+ 'REQUEST_METHOD': 'POST',
+ 'CONTENT_TYPE': 'multipart/form-data; '
+ 'boundary=----------------------------deb95b63e42a',
+ 'CONTENT_LENGTH': len(BODY_TEXT),
+ }
+ req = BaseRequest(environ)
+ result = req.str_POST
+ self.assertEqual(result['foo'], 'foo')
+ bar = result['bar']
+ self.assertEqual(bar.name, 'bar')
+ self.assertEqual(bar.filename, 'bar.txt')
+ self.assertEqual(bar.file.read(),
+ 'these are the contents of the file "bar.txt"\n')
+
+ # POST
+ # str_GET
+ def test_str_GET_reflects_query_string(self):
+ environ = {
+ 'QUERY_STRING': 'foo=123',
+ }
+ req = BaseRequest(environ)
+ result = req.str_GET
+ self.assertEqual(result, {'foo': '123'})
+ req.query_string = 'foo=456'
+ result = req.str_GET
+ self.assertEqual(result, {'foo': '456'})
+ req.query_string = ''
+ result = req.str_GET
+ self.assertEqual(result, {})
+
+ def test_str_GET_updates_query_string(self):
+ environ = {
+ }
+ req = BaseRequest(environ)
+ result = req.query_string
+ self.assertEqual(result, '')
+ req.str_GET['foo'] = '123'
+ result = req.query_string
+ self.assertEqual(result, 'foo=123')
+ del req.str_GET['foo']
+ result = req.query_string
+ self.assertEqual(result, '')
+
+ # GET
+ # str_postvars
+ # postvars
+ # str_queryvars
+ # queryvars
+ # is_xhr
+ # str_params
+ # params
+
+ def test_str_cookies_wo_webob_parsed_cookies(self):
+ environ = {
+ 'HTTP_COOKIE': 'a=b',
+ }
+ req = Request.blank('/', environ)
+ self.assertEqual(req.str_cookies, {'a': 'b'})
+
+ # cookies
+ # copy
+
+ def test_copy_get(self):
+ environ = {
+ 'HTTP_COOKIE': 'a=b',
+ }
+ req = Request.blank('/', environ)
+ clone = req.copy_get()
+ for k, v in req.environ.items():
+ if k in ('CONTENT_LENGTH', 'webob.is_body_seekable'):
+ self.assert_(k not in clone.environ)
+ elif k == 'wsgi.input':
+ self.assert_(clone.environ[k] is not v)
+ else:
+ self.assertEqual(clone.environ[k], v)
+
+ def test_remove_conditional_headers_accept_encoding(self):
+ req = Request.blank('/')
+ req.accept_encoding='gzip,deflate'
+ req.remove_conditional_headers()
+ self.assertEqual(bool(req.accept_encoding), False)
+
+ def test_remove_conditional_headers_if_modified_since(self):
+ from datetime import datetime
+ req = Request.blank('/')
+ req.if_modified_since = datetime(2006, 1, 1, 12, 0, tzinfo=UTC)
+ req.remove_conditional_headers()
+ self.assertEqual(req.if_modified_since, None)
+
+ def test_remove_conditional_headers_if_none_match(self):
+ req = Request.blank('/')
+ req.if_none_match = 'foo, bar'
+ req.remove_conditional_headers()
+ self.assertEqual(bool(req.if_none_match), False)
+
+ def test_remove_conditional_headers_if_range(self):
+ req = Request.blank('/')
+ req.if_range = 'foo, bar'
+ req.remove_conditional_headers()
+ self.assertEqual(bool(req.if_range), False)
+
+ def test_remove_conditional_headers_range(self):
+ req = Request.blank('/')
+ req.range = 'bytes=0-100'
+ req.remove_conditional_headers()
+ self.assertEqual(req.range, None)
+
+ def test_is_body_readable_POST(self):
+ req = Request.blank('/', environ={'REQUEST_METHOD':'POST'})
+ self.assertTrue(req.is_body_readable)
+
+ def test_is_body_readable_GET(self):
+ req = Request.blank('/', environ={'REQUEST_METHOD':'GET'})
+ self.assertFalse(req.is_body_readable)
+
+ def test_is_body_readable_unknown_method_and_content_length(self):
+ req = Request.blank('/', environ={'REQUEST_METHOD':'WTF'})
+ req.content_length = 10
+ self.assertTrue(req.is_body_readable)
+
+ def test_is_body_readable_special_flag(self):
+ req = Request.blank('/', environ={'REQUEST_METHOD':'WTF',
+ 'webob.is_body_readable': True})
+ self.assertTrue(req.is_body_readable)
+
+
+ # is_body_seekable
+ # make_body_seekable
+ # copy_body
+ # make_tempfile
+ # remove_conditional_headers
+ # accept
+ # accept_charset
+ # accept_encoding
+ # accept_language
+ # authorization
+
+ # cache_control
+ def test_cache_control_reflects_environ(self):
+ environ = {
+ 'HTTP_CACHE_CONTROL': 'max-age=5',
+ }
+ req = BaseRequest(environ)
+ result = req.cache_control
+ self.assertEqual(result.properties, {'max-age': 5})
+ req.environ.update(HTTP_CACHE_CONTROL='max-age=10')
+ result = req.cache_control
+ self.assertEqual(result.properties, {'max-age': 10})
+ req.environ.update(HTTP_CACHE_CONTROL='')
+ result = req.cache_control
+ self.assertEqual(result.properties, {})
+
+ def test_cache_control_updates_environ(self):
+ environ = {}
+ req = BaseRequest(environ)
+ req.cache_control.max_age = 5
+ result = req.environ['HTTP_CACHE_CONTROL']
+ self.assertEqual(result, 'max-age=5')
+ req.cache_control.max_age = 10
+ result = req.environ['HTTP_CACHE_CONTROL']
+ self.assertEqual(result, 'max-age=10')
+ req.cache_control = None
+ result = req.environ['HTTP_CACHE_CONTROL']
+ self.assertEqual(result, '')
+ del req.cache_control
+ self.assert_('HTTP_CACHE_CONTROL' not in req.environ)
+
+ def test_cache_control_set_dict(self):
+ environ = {}
+ req = BaseRequest(environ)
+ req.cache_control = {'max-age': 5}
+ result = req.cache_control
+ self.assertEqual(result.max_age, 5)
+
+ def test_cache_control_set_object(self):
+ from webob.cachecontrol import CacheControl
+ environ = {}
+ req = BaseRequest(environ)
+ req.cache_control = CacheControl({'max-age': 5}, type='request')
+ result = req.cache_control
+ self.assertEqual(result.max_age, 5)
+
+ def test_cache_control_gets_cached(self):
+ environ = {}
+ req = BaseRequest(environ)
+ self.assert_(req.cache_control is req.cache_control)
+
+ #if_match
+ #if_none_match
+
+ #date
+ #if_modified_since
+ #if_unmodified_since
+ #if_range
+ #max_forwards
+ #pragma
+ #range
+ #referer
+ #referrer
+ #user_agent
+ #__repr__
+ #__str__
+ #from_file
+
+ #call_application
+ def test_call_application_calls_application(self):
+ environ = {}
+ req = BaseRequest(environ)
+ def application(environ, start_response):
+ start_response('200 OK', [('content-type', 'text/plain')])
+ return ['...\n']
+ status, headers, output = req.call_application(application)
+ self.assertEqual(status, '200 OK')
+ self.assertEqual(headers, [('content-type', 'text/plain')])
+ self.assertEqual(''.join(output), '...\n')
+
+ def test_call_application_provides_write(self):
+ environ = {}
+ req = BaseRequest(environ)
+ def application(environ, start_response):
+ write = start_response('200 OK', [('content-type', 'text/plain')])
+ write('...\n')
+ return []
+ status, headers, output = req.call_application(application)
+ self.assertEqual(status, '200 OK')
+ self.assertEqual(headers, [('content-type', 'text/plain')])
+ self.assertEqual(''.join(output), '...\n')
+
+ def test_call_application_closes_iterable_when_mixed_with_write_calls(self):
+ environ = {
+ 'test._call_application_called_close': False
+ }
+ req = BaseRequest(environ)
+ def application(environ, start_response):
+ write = start_response('200 OK', [('content-type', 'text/plain')])
+ class AppIter(object):
+ def __iter__(self):
+ yield '...\n'
+ def close(self):
+ environ['test._call_application_called_close'] = True
+ write('...\n')
+ return AppIter()
+ status, headers, output = req.call_application(application)
+ self.assertEqual(''.join(output), '...\n...\n')
+ self.assertEqual(environ['test._call_application_called_close'], True)
+
+ def test_call_application_raises_exc_info(self):
+ environ = {}
+ req = BaseRequest(environ)
+ def application(environ, start_response):
+ try:
+ raise RuntimeError('OH NOES')
+ except:
+ import sys
+ exc_info = sys.exc_info()
+ start_response('200 OK', [('content-type', 'text/plain')], exc_info)
+ return ['...\n']
+ self.assertRaises(RuntimeError, req.call_application, application)
+
+ def test_call_application_returns_exc_info(self):
+ environ = {}
+ req = BaseRequest(environ)
+ def application(environ, start_response):
+ try:
+ raise RuntimeError('OH NOES')
+ except:
+ import sys
+ exc_info = sys.exc_info()
+ start_response('200 OK', [('content-type', 'text/plain')], exc_info)
+ return ['...\n']
+ status, headers, output, exc_info = req.call_application(application, True)
+ self.assertEqual(status, '200 OK')
+ self.assertEqual(headers, [('content-type', 'text/plain')])
+ self.assertEqual(''.join(output), '...\n')
+ self.assertEqual(exc_info[0], RuntimeError)
+
+ #get_response
+ def test_blank__method_subtitution(self):
+ request = BaseRequest.blank('/', environ={'REQUEST_METHOD': 'PUT'})
+ self.assertEqual(request.method, 'PUT')
+
+ request = BaseRequest.blank('/', environ={'REQUEST_METHOD': 'PUT'}, POST={})
+ self.assertEqual(request.method, 'PUT')
+
+ request = BaseRequest.blank('/', environ={'REQUEST_METHOD': 'HEAD'}, POST={})
+ self.assertEqual(request.method, 'POST')
+
+ def test_blank__ctype_in_env(self):
+ request = BaseRequest.blank('/', environ={'CONTENT_TYPE': 'application/json'})
+ self.assertEqual(request.content_type, 'application/json')
+ self.assertEqual(request.method, 'GET')
+
+ request = BaseRequest.blank('/', environ={'CONTENT_TYPE': 'application/json'},
+ POST='')
+ self.assertEqual(request.content_type, 'application/json')
+ self.assertEqual(request.method, 'POST')
+
+ def test_blank__ctype_in_headers(self):
+ request = BaseRequest.blank('/', headers={'Content-type': 'application/json'})
+ self.assertEqual(request.content_type, 'application/json')
+ self.assertEqual(request.method, 'GET')
+
+ request = BaseRequest.blank('/', headers={'Content-Type': 'application/json'},
+ POST='')
+ self.assertEqual(request.content_type, 'application/json')
+ self.assertEqual(request.method, 'POST')
+
+ def test_blank__ctype_as_kw(self):
+ request = BaseRequest.blank('/', content_type='application/json')
+ self.assertEqual(request.content_type, 'application/json')
+ self.assertEqual(request.method, 'GET')
+
+ request = BaseRequest.blank('/', content_type='application/json',
+ POST='')
+ self.assertEqual(request.content_type, 'application/json')
+ self.assertEqual(request.method, 'POST')
+
+ def test_blank__str_post_data_for_unsupported_ctype(self):
+ self.assertRaises(ValueError, BaseRequest.blank, '/', content_type='application/json',
+ POST={})
+
+ def test_blank__post_urlencoded(self):
+ request = Request.blank('/', POST={'first':1, 'second':2})
+ self.assertEqual(request.method, 'POST')
+ self.assertEqual(request.content_type, 'application/x-www-form-urlencoded')
+ self.assertEqual(request.body, 'first=1&second=2')
+ self.assertEqual(request.content_length, 16)
+
+ def test_blank__post_multipart(self):
+ request = Request.blank('/', POST={'first':'1', 'second':'2'},
+ content_type='multipart/form-data; boundary=boundary')
+ self.assertEqual(request.method, 'POST')
+ self.assertEqual(request.content_type, 'multipart/form-data')
+ self.assertEqual(request.body, '--boundary\r\n'
+ 'Content-Disposition: form-data; name="first"\r\n\r\n'
+ '1\r\n'
+ '--boundary\r\n'
+ 'Content-Disposition: form-data; name="second"\r\n\r\n'
+ '2\r\n'
+ '--boundary--')
+ self.assertEqual(request.content_length, 139)
+
+ def test_blank__post_files(self):
+ import cgi
+ from StringIO import StringIO
+ from webob.request import _get_multipart_boundary
+ request = Request.blank('/', POST={'first':('filename1', StringIO('1')),
+ 'second':('filename2', '2'),
+ 'third': '3'})
+ self.assertEqual(request.method, 'POST')
+ self.assertEqual(request.content_type, 'multipart/form-data')
+ boundary = _get_multipart_boundary(request.headers['content-type'])
+ body_norm = request.body.replace(boundary, 'boundary')
+ self.assertEqual(body_norm, '--boundary\r\n'
+ 'Content-Disposition: form-data; name="first"; filename="filename1"\r\n\r\n'
+ '1\r\n'
+ '--boundary\r\n'
+ 'Content-Disposition: form-data; name="second"; filename="filename2"\r\n\r\n'
+ '2\r\n'
+ '--boundary\r\n'
+ 'Content-Disposition: form-data; name="third"\r\n\r\n'
+ '3\r\n'
+ '--boundary--')
+ self.assertEqual(request.content_length, 294)
+ self.assertTrue(isinstance(request.POST['first'], cgi.FieldStorage))
+ self.assertTrue(isinstance(request.POST['second'], cgi.FieldStorage))
+ self.assertEqual(request.POST['first'].value, '1')
+ self.assertEqual(request.POST['second'].value, '2')
+ self.assertEqual(request.POST['third'], '3')
+
+ def test_blank__post_file_w_wrong_ctype(self):
+ self.assertRaises(ValueError, Request.blank, '/', POST={'first':('filename1', '1')},
+ content_type='application/x-www-form-urlencoded')
+
+ #from_string
+ def test_from_string_extra_data(self):
+ from webob import BaseRequest
+ _test_req_copy = _test_req.replace('Content-Type',
+ 'Content-Length: 337\r\nContent-Type')
+ self.assertRaises(ValueError, BaseRequest.from_string,
+ _test_req_copy+'EXTRA!')
+
+ #as_string
+ def test_as_string_skip_body(self):
+ from webob import BaseRequest
+ req = BaseRequest.from_string(_test_req)
+ body = req.as_string(skip_body=True)
+ self.assertEqual(body.count('\r\n\r\n'), 0)
+ self.assertEqual(req.as_string(skip_body=337), req.as_string())
+ body = req.as_string(337-1).split('\r\n\r\n', 1)[1]
+ self.assertEqual(body, '<body skipped (len=337)>')
+
+ def test_adhoc_attrs_set(self):
+ req = Request.blank('/')
+ req.foo = 1
+ self.assertEqual(req.environ['webob.adhoc_attrs'], {'foo': 1})
+
+ def test_adhoc_attrs_set_nonadhoc(self):
+ req = Request.blank('/', environ={'webob.adhoc_attrs':{}})
+ req.request_body_tempfile_limit = 1
+ self.assertEqual(req.environ['webob.adhoc_attrs'], {})
+
+ def test_adhoc_attrs_get(self):
+ req = Request.blank('/', environ={'webob.adhoc_attrs': {'foo': 1}})
+ self.assertEqual(req.foo, 1)
+
+ def test_adhoc_attrs_get_missing(self):
+ req = Request.blank('/')
+ self.assertRaises(AttributeError, getattr, req, 'some_attr')
+
+ def test_adhoc_attrs_del(self):
+ req = Request.blank('/', environ={'webob.adhoc_attrs': {'foo': 1}})
+ del req.foo
+ self.assertEqual(req.environ['webob.adhoc_attrs'], {})
+
+ def test_adhoc_attrs_del_missing(self):
+ req = Request.blank('/')
+ self.assertRaises(AttributeError, delattr, req, 'some_attr')
+
+class RequestTests_functional(unittest.TestCase):
+ def test_gets(self):
+ from webtest import TestApp
+ app = TestApp(simpleapp)
+ res = app.get('/')
+ self.assert_('Hello' in res)
+ self.assert_("get is GET([])" in res)
+ self.assert_("post is <NoVars: Not a form request>" in res)
+
+ res = app.get('/?name=george')
+ res.mustcontain("get is GET([('name', 'george')])")
+ res.mustcontain("Val is george")
+
+ def test_language_parsing(self):
+ from webtest import TestApp
+ app = TestApp(simpleapp)
+ res = app.get('/')
+ self.assert_("The languages are: ['en-US']" in res)
+
+ res = app.get('/',
+ headers={'Accept-Language': 'da, en-gb;q=0.8, en;q=0.7'})
+ self.assert_("languages are: ['da', 'en-gb', 'en-US']" in res)
+
+ res = app.get('/',
+ headers={'Accept-Language': 'en-gb;q=0.8, da, en;q=0.7'})
+ self.assert_("languages are: ['da', 'en-gb', 'en-US']" in res)
+
+ def test_mime_parsing(self):
+ from webtest import TestApp
+ app = TestApp(simpleapp)
+ res = app.get('/', headers={'Accept':'text/html'})
+ self.assert_("accepttypes is: text/html" in res)
+
+ res = app.get('/', headers={'Accept':'application/xml'})
+ self.assert_("accepttypes is: application/xml" in res)
+
+ res = app.get('/', headers={'Accept':'application/xml,*/*'})
+ self.assert_("accepttypes is: application/xml" in res)
+
+ def test_accept_best_match(self):
+ self.assert_(not Request.blank('/').accept)
+ self.assert_(not Request.blank('/', headers={'Accept': ''}).accept)
+ req = Request.blank('/', headers={'Accept':'text/plain'})
+ self.assert_(req.accept)
+ self.assertRaises(ValueError, req.accept.best_match, ['*/*'])
+ req = Request.blank('/', accept=['*/*','text/*'])
+ self.assertEqual(
+ req.accept.best_match(['application/x-foo', 'text/plain']),
+ 'text/plain')
+ self.assertEqual(
+ req.accept.best_match(['text/plain', 'application/x-foo']),
+ 'text/plain')
+ req = Request.blank('/', accept=['text/plain', 'message/*'])
+ self.assertEqual(
+ req.accept.best_match(['message/x-foo', 'text/plain']),
+ 'text/plain')
+ self.assertEqual(
+ req.accept.best_match(['text/plain', 'message/x-foo']),
+ 'text/plain')
+
+ def test_from_mimeparse(self):
+ # http://mimeparse.googlecode.com/svn/trunk/mimeparse.py
+ supported = ['application/xbel+xml', 'application/xml']
+ tests = [('application/xbel+xml', 'application/xbel+xml'),
+ ('application/xbel+xml; q=1', 'application/xbel+xml'),
+ ('application/xml; q=1', 'application/xml'),
+ ('application/*; q=1', 'application/xbel+xml'),
+ ('*/*', 'application/xbel+xml')]
+
+ for accept, get in tests:
+ req = Request.blank('/', headers={'Accept':accept})
+ self.assertEqual(req.accept.best_match(supported), get)
+
+ supported = ['application/xbel+xml', 'text/xml']
+ tests = [('text/*;q=0.5,*/*; q=0.1', 'text/xml'),
+ ('text/html,application/atom+xml; q=0.9', None)]
+
+ for accept, get in tests:
+ req = Request.blank('/', headers={'Accept':accept})
+ self.assertEqual(req.accept.best_match(supported), get)
+
+ supported = ['application/json', 'text/html']
+ tests = [
+ ('application/json, text/javascript, */*', 'application/json'),
+ ('application/json, text/html;q=0.9', 'application/json'),
+ ]
+
+ for accept, get in tests:
+ req = Request.blank('/', headers={'Accept':accept})
+ self.assertEqual(req.accept.best_match(supported), get)
+
+ offered = ['image/png', 'application/xml']
+ tests = [
+ ('image/png', 'image/png'),
+ ('image/*', 'image/png'),
+ ('image/*, application/xml', 'application/xml'),
+ ]
+
+ for accept, get in tests:
+ req = Request.blank('/', accept=accept)
+ self.assertEqual(req.accept.best_match(offered), get)
+
+ def test_headers(self):
+ from webtest import TestApp
+ app = TestApp(simpleapp)
+ headers = {
+ 'If-Modified-Since': 'Sat, 29 Oct 1994 19:43:31 GMT',
+ 'Cookie': 'var1=value1',
+ 'User-Agent': 'Mozilla 4.0 (compatible; MSIE)',
+ 'If-None-Match': '"etag001", "etag002"',
+ 'X-Requested-With': 'XMLHttpRequest',
+ }
+ res = app.get('/?foo=bar&baz', headers=headers)
+ res.mustcontain(
+ 'if_modified_since: ' +
+ 'datetime.datetime(1994, 10, 29, 19, 43, 31, tzinfo=UTC)',
+ "user_agent: 'Mozilla",
+ 'is_xhr: True',
+ "cookies is {'var1': 'value1'}",
+ "params is NestedMultiDict([('foo', 'bar'), ('baz', '')])",
+ "if_none_match: <ETag etag001 or etag002>",
+ )
+
+ def test_bad_cookie(self):
+ req = Request.blank('/')
+ req.headers['Cookie'] = '070-it-:><?0'
+ self.assertEqual(req.cookies, {})
+ req.headers['Cookie'] = 'foo=bar'
+ self.assertEqual(req.cookies, {'foo': 'bar'})
+ req.headers['Cookie'] = '...'
+ self.assertEqual(req.cookies, {})
+ req.headers['Cookie'] = '=foo'
+ self.assertEqual(req.cookies, {})
+ req.headers['Cookie'] = ('dismiss-top=6; CP=null*; '
+ 'PHPSESSID=0a539d42abc001cdc762809248d4beed; a=42')
+ self.assertEqual(req.cookies, {
+ 'CP': u'null*',
+ 'PHPSESSID': u'0a539d42abc001cdc762809248d4beed',
+ 'a': u'42',
+ 'dismiss-top': u'6'
+ })
+ req.headers['Cookie'] = 'fo234{=bar blub=Blah'
+ self.assertEqual(req.cookies, {'blub': 'Blah'})
+
+ def test_cookie_quoting(self):
+ req = Request.blank('/')
+ req.headers['Cookie'] = 'foo="?foo"; Path=/'
+ self.assertEqual(req.cookies, {'foo': '?foo'})
+
+ def test_path_quoting(self):
+ path = '/:@&+$,/bar'
+ req = Request.blank(path)
+ self.assertEqual(req.path, path)
+ self.assert_(req.url.endswith(path))
+
+ def test_params(self):
+ req = Request.blank('/?a=1&b=2')
+ req.method = 'POST'
+ req.body = 'b=3'
+ self.assertEqual(req.params.items(),
+ [('a', '1'), ('b', '2'), ('b', '3')])
+ new_params = req.params.copy()
+ self.assertEqual(new_params.items(),
+ [('a', '1'), ('b', '2'), ('b', '3')])
+ new_params['b'] = '4'
+ self.assertEqual(new_params.items(), [('a', '1'), ('b', '4')])
+ # The key name is \u1000:
+ req = Request.blank('/?%E1%80%80=x',
+ decode_param_names=True, charset='UTF-8')
+ self.assert_(req.decode_param_names)
+ self.assert_(u'\u1000' in req.GET.keys())
+ self.assertEqual(req.GET[u'\u1000'], 'x')
+
+ def test_copy_body(self):
+ req = Request.blank('/', method='POST', body='some text',
+ request_body_tempfile_limit=1)
+ old_body_file = req.body_file_raw
+ req.copy_body()
+ self.assert_(req.body_file_raw is not old_body_file)
+ req = Request.blank('/', method='POST',
+ body_file=UnseekableInput('0123456789'), content_length=10)
+ self.assert_(not hasattr(req.body_file_raw, 'seek'))
+ old_body_file = req.body_file_raw
+ req.make_body_seekable()
+ self.assert_(req.body_file_raw is not old_body_file)
+ self.assertEqual(req.body, '0123456789')
+ old_body_file = req.body_file
+ req.make_body_seekable()
+ self.assert_(req.body_file_raw is old_body_file)
+ self.assert_(req.body_file is old_body_file)
+
+ def test_broken_seek(self):
+ # copy() should work even when the input has a broken seek method
+ req = Request.blank('/', method='POST',
+ body_file=UnseekableInputWithSeek('0123456789'),
+ content_length=10)
+ self.assert_(hasattr(req.body_file_raw, 'seek'))
+ self.assertRaises(IOError, req.body_file_raw.seek, 0)
+ old_body_file = req.body_file
+ req2 = req.copy()
+ self.assert_(req2.body_file_raw is req2.body_file is not old_body_file)
+ self.assertEqual(req2.body, '0123456789')
+
+ def test_set_body(self):
+ from webob import BaseRequest
+ req = BaseRequest.blank('/', method='PUT', body='foo')
+ self.assert_(req.is_body_seekable)
+ self.assertEqual(req.body, 'foo')
+ self.assertEqual(req.content_length, 3)
+ del req.body
+ self.assertEqual(req.body, '')
+ self.assertEqual(req.content_length, 0)
+
+ def test_broken_clen_header(self):
+ # if the UA sends "content_length: ..' header (the name is wrong)
+ # it should not break the req.headers.items()
+ req = Request.blank('/')
+ req.environ['HTTP_CONTENT_LENGTH'] = '0'
+ req.headers.items()
+
+ def test_nonstr_keys(self):
+ # non-string env keys shouldn't break req.headers
+ req = Request.blank('/')
+ req.environ[1] = 1
+ req.headers.items()
+
+
+ def test_authorization(self):
+ req = Request.blank('/')
+ req.authorization = 'Digest uri="/?a=b"'
+ self.assertEqual(req.authorization, ('Digest', {'uri': '/?a=b'}))
+
+ def test_authorization2(self):
+ from webob.descriptors import parse_auth_params
+ for s, d in [
+ ('x=y', {'x': 'y'}),
+ ('x="y"', {'x': 'y'}),
+ ('x=y,z=z', {'x': 'y', 'z': 'z'}),
+ ('x=y, z=z', {'x': 'y', 'z': 'z'}),
+ ('x="y",z=z', {'x': 'y', 'z': 'z'}),
+ ('x="y", z=z', {'x': 'y', 'z': 'z'}),
+ ('x="y,x", z=z', {'x': 'y,x', 'z': 'z'}),
+ ]:
+ self.assertEqual(parse_auth_params(s), d)
+
+
+ def test_from_file(self):
+ req = Request.blank('http://example.com:8000/test.html?params')
+ self.equal_req(req)
+
+ req = Request.blank('http://example.com/test2')
+ req.method = 'POST'
+ req.body = 'test=example'
+ self.equal_req(req)
+
+ def test_req_kw_none_val(self):
+ request = Request({}, content_length=None)
+ self.assert_('content-length' not in request.headers)
+ self.assert_('content-type' not in request.headers)
+
+ def test_env_keys(self):
+ req = Request.blank('/')
+ # SCRIPT_NAME can be missing
+ del req.environ['SCRIPT_NAME']
+ self.assertEqual(req.script_name, '')
+ self.assertEqual(req.uscript_name, u'')
+
+ def test_repr_nodefault(self):
+ from webob.request import NoDefault
+ nd = NoDefault
+ self.assertEqual(repr(nd), '(No Default)')
+
+ def test_request_noenviron_param(self):
+ # Environ is a a mandatory not null param in Request.
+ self.assertRaises(TypeError, Request, environ=None)
+
+ def test_unicode_errors(self):
+ # Passing unicode_errors != NoDefault should assign value to
+ # dictionary['unicode_errors'], else not
+ r = Request({'a':1}, unicode_errors='strict')
+ self.assert_('unicode_errors' in r.__dict__)
+ r = Request({'a':1})
+ self.assert_('unicode_errors' not in r.__dict__)
+
+ def test_unexpected_kw(self):
+ # Passed an attr in kw that does not exist in the class, should
+ # raise an error
+ # Passed an attr in kw that does exist in the class, should be ok
+ self.assertRaises(TypeError,
+ Request, {'a':1}, this_does_not_exist=1)
+ r = Request({'a':1}, **{'charset':'utf-8', 'server_name':'127.0.0.1'})
+ self.assertEqual(getattr(r, 'charset', None), 'utf-8')
+ self.assertEqual(getattr(r, 'server_name', None), '127.0.0.1')
+
+ def test_conttype_set_del(self):
+ # Deleting content_type attr from a request should update the
+ # environ dict
+ # Assigning content_type should replace first option of the environ
+ # dict
+ r = Request({'a':1}, **{'content_type':'text/html'})
+ self.assert_('CONTENT_TYPE' in r.environ)
+ self.assert_(hasattr(r, 'content_type'))
+ del r.content_type
+ self.assert_('CONTENT_TYPE' not in r.environ)
+ a = Request({'a':1},
+ content_type='charset=utf-8;application/atom+xml;type=entry')
+ self.assert_(a.environ['CONTENT_TYPE']==
+ 'charset=utf-8;application/atom+xml;type=entry')
+ a.content_type = 'charset=utf-8'
+ self.assert_(a.environ['CONTENT_TYPE']==
+ 'charset=utf-8;application/atom+xml;type=entry')
+
+ def test_headers2(self):
+ # Setting headers in init and later with a property, should update
+ # the info
+ headers = {'Host': 'www.example.com',
+ 'Accept-Language': 'en-us,en;q=0.5',
+ 'Accept-Encoding': 'gzip,deflate',
+ 'Accept-Charset': 'ISO-8859-1,utf-8;q=0.7,*;q=0.7',
+ 'Keep-Alive': '115',
+ 'Connection': 'keep-alive',
+ 'Cache-Control': 'max-age=0'}
+ r = Request({'a':1}, headers=headers)
+ for i in headers.keys():
+ self.assert_(i in r.headers and
+ 'HTTP_'+i.upper().replace('-', '_') in r.environ)
+ r.headers = {'Server':'Apache'}
+ self.assertEqual(r.environ.keys(), ['a', 'HTTP_SERVER'])
+
+ def test_host_url(self):
+ # Request has a read only property host_url that combines several
+ # keys to create a host_url
+ a = Request({'wsgi.url_scheme':'http'}, **{'host':'www.example.com'})
+ self.assertEqual(a.host_url, 'http://www.example.com')
+ a = Request({'wsgi.url_scheme':'http'}, **{'server_name':'localhost',
+ 'server_port':5000})
+ self.assertEqual(a.host_url, 'http://localhost:5000')
+ a = Request({'wsgi.url_scheme':'https'}, **{'server_name':'localhost',
+ 'server_port':443})
+ self.assertEqual(a.host_url, 'https://localhost')
+
+ def test_path_info_p(self):
+ # Peek path_info to see what's coming
+ # Pop path_info until there's nothing remaining
+ a = Request({'a':1}, **{'path_info':'/foo/bar','script_name':''})
+ self.assertEqual(a.path_info_peek(), 'foo')
+ self.assertEqual(a.path_info_pop(), 'foo')
+ self.assertEqual(a.path_info_peek(), 'bar')
+ self.assertEqual(a.path_info_pop(), 'bar')
+ self.assertEqual(a.path_info_peek(), None)
+ self.assertEqual(a.path_info_pop(), None)
+
+ def test_urlvars_property(self):
+ # Testing urlvars setter/getter/deleter
+ a = Request({'wsgiorg.routing_args':((),{'x':'y'}),
+ 'paste.urlvars':{'test':'value'}})
+ a.urlvars = {'hello':'world'}
+ self.assert_('paste.urlvars' not in a.environ)
+ self.assertEqual(a.environ['wsgiorg.routing_args'],
+ ((), {'hello':'world'}))
+ del a.urlvars
+ self.assert_('wsgiorg.routing_args' not in a.environ)
+ a = Request({'paste.urlvars':{'test':'value'}})
+ self.assertEqual(a.urlvars, {'test':'value'})
+ a.urlvars = {'hello':'world'}
+ self.assertEqual(a.environ['paste.urlvars'], {'hello':'world'})
+ del a.urlvars
+ self.assert_('paste.urlvars' not in a.environ)
+
+ def test_urlargs_property(self):
+ # Testing urlargs setter/getter/deleter
+ a = Request({'paste.urlvars':{'test':'value'}})
+ self.assertEqual(a.urlargs, ())
+ a.urlargs = {'hello':'world'}
+ self.assertEqual(a.environ['wsgiorg.routing_args'],
+ ({'hello':'world'}, {'test':'value'}))
+ a = Request({'a':1})
+ a.urlargs = {'hello':'world'}
+ self.assertEqual(a.environ['wsgiorg.routing_args'],
+ ({'hello':'world'}, {}))
+ del a.urlargs
+ self.assert_('wsgiorg.routing_args' not in a.environ)
+
+ def test_host_property(self):
+ # Testing host setter/getter/deleter
+ a = Request({'wsgi.url_scheme':'http'}, server_name='localhost',
+ server_port=5000)
+ self.assertEqual(a.host, "localhost:5000")
+ a.host = "localhost:5000"
+ self.assert_('HTTP_HOST' in a.environ)
+ del a.host
+ self.assert_('HTTP_HOST' not in a.environ)
+
+ def test_body_property(self):
+ # Testing body setter/getter/deleter plus making sure body has a
+ # seek method
+ #a = Request({'a':1}, **{'CONTENT_LENGTH':'?'})
+ # I cannot think of a case where somebody would put anything else
+ # than a # numerical value in CONTENT_LENGTH, Google didn't help
+ # either
+ #self.assertEqual(a.body, '')
+ # I need to implement a not seekable stringio like object.
+ import string
+ from cStringIO import StringIO
+ class DummyIO(object):
+ def __init__(self, txt):
+ self.txt = txt
+ def read(self, n=-1):
+ return self.txt[0:n]
+ limit = BaseRequest.request_body_tempfile_limit
+ len_strl = limit // len(string.letters) + 1
+ r = Request({'a':1, 'REQUEST_METHOD': 'POST'}, body_file=DummyIO(string.letters * len_strl))
+ self.assertEqual(len(r.body), len(string.letters*len_strl)-1)
+ self.assertRaises(TypeError,
+ setattr, r, 'body', unicode('hello world'))
+ r.body = None
+ self.assertEqual(r.body, '')
+ r = Request({'a':1}, method='PUT', body_file=DummyIO(string.letters))
+ self.assert_(not hasattr(r.body_file_raw, 'seek'))
+ r.make_body_seekable()
+ self.assert_(hasattr(r.body_file_raw, 'seek'))
+ r = Request({'a':1}, method='PUT', body_file=StringIO(string.letters))
+ self.assert_(hasattr(r.body_file_raw, 'seek'))
+ r.make_body_seekable()
+ self.assert_(hasattr(r.body_file_raw, 'seek'))
+
+ def test_repr_invalid(self):
+ # If we have an invalid WSGI environ, the repr should tell us.
+ from webob import BaseRequest
+ req = BaseRequest({'CONTENT_LENGTH':'0', 'body':''})
+ self.assert_(repr(req).endswith('(invalid WSGI environ)>'))
+
+ def test_from_garbage_file(self):
+ # If we pass a file with garbage to from_file method it should
+ # raise an error plus missing bits in from_file method
+ from cStringIO import StringIO
+ from webob import BaseRequest
+ self.assertRaises(ValueError,
+ BaseRequest.from_file, StringIO('hello world'))
+ val_file = StringIO(
+ "GET /webob/ HTTP/1.1\n"
+ "Host: pythonpaste.org\n"
+ "User-Agent: Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.2.13)"
+ "Gecko/20101206 Ubuntu/10.04 (lucid) Firefox/3.6.13\n"
+ "Accept: "
+ "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;"
+ "q=0.8\n"
+ "Accept-Language: en-us,en;q=0.5\n"
+ "Accept-Encoding: gzip,deflate\n"
+ "Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7\n"
+ # duplicate on purpose
+ "Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7\n"
+ "Keep-Alive: 115\n"
+ "Connection: keep-alive\n"
+ )
+ req = BaseRequest.from_file(val_file)
+ self.assert_(isinstance(req, BaseRequest))
+ self.assert_(not repr(req).endswith('(invalid WSGI environ)>'))
+ val_file = StringIO(
+ "GET /webob/ HTTP/1.1\n"
+ "Host pythonpaste.org\n"
+ )
+ self.assertRaises(ValueError, BaseRequest.from_file, val_file)
+
+ def test_from_string(self):
+ # A valid request without a Content-Length header should still read
+ # the full body.
+ # Also test parity between as_string and from_string / from_file.
+ import cgi
+ from webob import BaseRequest
+ req = BaseRequest.from_string(_test_req)
+ self.assert_(isinstance(req, BaseRequest))
+ self.assert_(not repr(req).endswith('(invalid WSGI environ)>'))
+ self.assert_('\n' not in req.http_version or '\r' in req.http_version)
+ self.assert_(',' not in req.host)
+ self.assert_(req.content_length is not None)
+ self.assertEqual(req.content_length, 337)
+ self.assert_('foo' in req.body)
+ bar_contents = "these are the contents of the file 'bar.txt'\r\n"
+ self.assert_(bar_contents in req.body)
+ self.assertEqual(req.params['foo'], 'foo')
+ bar = req.params['bar']
+ self.assert_(isinstance(bar, cgi.FieldStorage))
+ self.assertEqual(bar.type, 'application/octet-stream')
+ bar.file.seek(0)
+ self.assertEqual(bar.file.read(), bar_contents)
+ # out should equal contents, except for the Content-Length header,
+ # so insert that.
+ _test_req_copy = _test_req.replace('Content-Type',
+ 'Content-Length: 337\r\nContent-Type')
+ self.assertEqual(str(req), _test_req_copy)
+
+ req2 = BaseRequest.from_string(_test_req2)
+ self.assert_('host' not in req2.headers)
+ self.assertEqual(str(req2), _test_req2.rstrip())
+ self.assertRaises(ValueError,
+ BaseRequest.from_string, _test_req2 + 'xx')
+
+ def test_blank(self):
+ # BaseRequest.blank class method
+ from webob import BaseRequest
+ self.assertRaises(ValueError, BaseRequest.blank,
+ 'www.example.com/foo?hello=world', None,
+ 'www.example.com/foo?hello=world')
+ self.assertRaises(ValueError, BaseRequest.blank,
+ 'gopher.example.com/foo?hello=world', None,
+ 'gopher://gopher.example.com')
+ req = BaseRequest.blank('www.example.com/foo?hello=world', None,
+ 'http://www.example.com')
+ self.assertEqual(req.environ.get('HTTP_HOST', None),
+ 'www.example.com:80')
+ self.assertEqual(req.environ.get('PATH_INFO', None),
+ 'www.example.com/foo')
+ self.assertEqual(req.environ.get('QUERY_STRING', None),
+ 'hello=world')
+ self.assertEqual(req.environ.get('REQUEST_METHOD', None), 'GET')
+ req = BaseRequest.blank('www.example.com/secure?hello=world', None,
+ 'https://www.example.com/secure')
+ self.assertEqual(req.environ.get('HTTP_HOST', None),
+ 'www.example.com:443')
+ self.assertEqual(req.environ.get('PATH_INFO', None),
+ 'www.example.com/secure')
+ self.assertEqual(req.environ.get('QUERY_STRING', None), 'hello=world')
+ self.assertEqual(req.environ.get('REQUEST_METHOD', None), 'GET')
+ self.assertEqual(req.environ.get('SCRIPT_NAME', None), '/secure')
+ self.assertEqual(req.environ.get('SERVER_NAME', None),
+ 'www.example.com')
+ self.assertEqual(req.environ.get('SERVER_PORT', None), '443')
+
+ def test_environ_from_url(self):
+ # Generating an environ just from an url plus testing environ_add_POST
+ from webob.request import environ_add_POST
+ from webob.request import environ_from_url
+ self.assertRaises(TypeError, environ_from_url,
+ 'http://www.example.com/foo?bar=baz#qux')
+ self.assertRaises(TypeError, environ_from_url,
+ 'gopher://gopher.example.com')
+ req = environ_from_url('http://www.example.com/foo?bar=baz')
+ self.assertEqual(req.get('HTTP_HOST', None), 'www.example.com:80')
+ self.assertEqual(req.get('PATH_INFO', None), '/foo')
+ self.assertEqual(req.get('QUERY_STRING', None), 'bar=baz')
+ self.assertEqual(req.get('REQUEST_METHOD', None), 'GET')
+ self.assertEqual(req.get('SCRIPT_NAME', None), '')
+ self.assertEqual(req.get('SERVER_NAME', None), 'www.example.com')
+ self.assertEqual(req.get('SERVER_PORT', None), '80')
+ req = environ_from_url('https://www.example.com/foo?bar=baz')
+ self.assertEqual(req.get('HTTP_HOST', None), 'www.example.com:443')
+ self.assertEqual(req.get('PATH_INFO', None), '/foo')
+ self.assertEqual(req.get('QUERY_STRING', None), 'bar=baz')
+ self.assertEqual(req.get('REQUEST_METHOD', None), 'GET')
+ self.assertEqual(req.get('SCRIPT_NAME', None), '')
+ self.assertEqual(req.get('SERVER_NAME', None), 'www.example.com')
+ self.assertEqual(req.get('SERVER_PORT', None), '443')
+ environ_add_POST(req, None)
+ self.assert_('CONTENT_TYPE' not in req)
+ self.assert_('CONTENT_LENGTH' not in req)
+ environ_add_POST(req, {'hello':'world'})
+ self.assert_(req.get('HTTP_HOST', None), 'www.example.com:443')
+ self.assertEqual(req.get('PATH_INFO', None), '/foo')
+ self.assertEqual(req.get('QUERY_STRING', None), 'bar=baz')
+ self.assertEqual(req.get('REQUEST_METHOD', None), 'POST')
+ self.assertEqual(req.get('SCRIPT_NAME', None), '')
+ self.assertEqual(req.get('SERVER_NAME', None), 'www.example.com')
+ self.assertEqual(req.get('SERVER_PORT', None), '443')
+ self.assertEqual(req.get('CONTENT_LENGTH', None),'11')
+ self.assertEqual(req.get('CONTENT_TYPE', None),
+ 'application/x-www-form-urlencoded')
+ self.assertEqual(req['wsgi.input'].read(), 'hello=world')
+
+
+ def test_post_does_not_reparse(self):
+ # test that there's no repetitive parsing is happening on every
+ # req.POST access
+ req = Request.blank('/',
+ content_type='multipart/form-data; boundary=boundary',
+ POST=_cgi_escaping_body
+ )
+ f0 = req.body_file_raw
+ post1 = req.str_POST
+ f1 = req.body_file_raw
+ self.assert_(f1 is not f0)
+ post2 = req.str_POST
+ f2 = req.body_file_raw
+ self.assert_(post1 is post2)
+ self.assert_(f1 is f2)
+
+
+ def test_middleware_body(self):
+ def app(env, sr):
+ sr('200 OK', [])
+ return [env['wsgi.input'].read()]
+
+ def mw(env, sr):
+ req = Request(env)
+ data = req.body_file.read()
+ resp = req.get_response(app)
+ resp.headers['x-data'] = data
+ return resp(env, sr)
+
+ req = Request.blank('/', method='PUT', body='abc')
+ resp = req.get_response(mw)
+ self.assertEqual(resp.body, 'abc')
+ self.assertEqual(resp.headers['x-data'], 'abc')
+
+ def test_body_file_noseek(self):
+ req = Request.blank('/', method='PUT', body='abc')
+ lst = [req.body_file.read(1) for i in range(3)]
+ self.assertEqual(lst, ['a','b','c'])
+
+ def test_cgi_escaping_fix(self):
+ req = Request.blank('/',
+ content_type='multipart/form-data; boundary=boundary',
+ POST=_cgi_escaping_body
+ )
+ self.assertEqual(req.POST.keys(), ['%20%22"'])
+ req.body_file.read()
+ self.assertEqual(req.POST.keys(), ['%20%22"'])
+
+ def test_content_type_none(self):
+ r = Request.blank('/', content_type='text/html')
+ self.assertEqual(r.content_type, 'text/html')
+ r.content_type = None
+
+ def test_charset_in_content_type(self):
+ r = Request({'CONTENT_TYPE':'text/html;charset=ascii'})
+ r.charset = 'shift-jis'
+ self.assertEqual(r.charset, 'shift-jis')
+
+ def test_body_file_seekable(self):
+ from cStringIO import StringIO
+ r = Request.blank('/', method='POST')
+ r.body_file = StringIO('body')
+ self.assertEqual(r.body_file_seekable.read(), 'body')
+
+ def test_request_init(self):
+ # port from doctest (docs/reference.txt)
+ req = Request.blank('/article?id=1')
+ self.assertEqual(req.environ['HTTP_HOST'], 'localhost:80')
+ self.assertEqual(req.environ['PATH_INFO'], '/article')
+ self.assertEqual(req.environ['QUERY_STRING'], 'id=1')
+ self.assertEqual(req.environ['REQUEST_METHOD'], 'GET')
+ self.assertEqual(req.environ['SCRIPT_NAME'], '')
+ self.assertEqual(req.environ['SERVER_NAME'], 'localhost')
+ self.assertEqual(req.environ['SERVER_PORT'], '80')
+ self.assertEqual(req.environ['SERVER_PROTOCOL'], 'HTTP/1.0')
+ self.assert_(hasattr(req.environ['wsgi.errors'], 'write') and
+ hasattr(req.environ['wsgi.errors'], 'flush'))
+ self.assert_(hasattr(req.environ['wsgi.input'], 'next'))
+ self.assertEqual(req.environ['wsgi.multiprocess'], False)
+ self.assertEqual(req.environ['wsgi.multithread'], False)
+ self.assertEqual(req.environ['wsgi.run_once'], False)
+ self.assertEqual(req.environ['wsgi.url_scheme'], 'http')
+ self.assertEqual(req.environ['wsgi.version'], (1, 0))
+
+ # Test body
+ self.assert_(hasattr(req.body_file, 'read'))
+ self.assertEqual(req.body, '')
+ req.method = 'PUT'
+ req.body = 'test'
+ self.assert_(hasattr(req.body_file, 'read'))
+ self.assertEqual(req.body, 'test')
+
+ # Test method & URL
+ self.assertEqual(req.method, 'PUT')
+ self.assertEqual(req.scheme, 'http')
+ self.assertEqual(req.script_name, '') # The base of the URL
+ req.script_name = '/blog' # make it more interesting
+ self.assertEqual(req.path_info, '/article')
+ # Content-Type of the request body
+ self.assertEqual(req.content_type, '')
+ # The auth'ed user (there is none set)
+ self.assert_(req.remote_user is None)
+ self.assert_(req.remote_addr is None)
+ self.assertEqual(req.host, 'localhost:80')
+ self.assertEqual(req.host_url, 'http://localhost')
+ self.assertEqual(req.application_url, 'http://localhost/blog')
+ self.assertEqual(req.path_url, 'http://localhost/blog/article')
+ self.assertEqual(req.url, 'http://localhost/blog/article?id=1')
+ self.assertEqual(req.path, '/blog/article')
+ self.assertEqual(req.path_qs, '/blog/article?id=1')
+ self.assertEqual(req.query_string, 'id=1')
+ self.assertEqual(req.relative_url('archive'),
+ 'http://localhost/blog/archive')
+
+ # Doesn't change request
+ self.assertEqual(req.path_info_peek(), 'article')
+ # Does change request!
+ self.assertEqual(req.path_info_pop(), 'article')
+ self.assertEqual(req.script_name, '/blog/article')
+ self.assertEqual(req.path_info, '')
+
+ # Headers
+ req.headers['Content-Type'] = 'application/x-www-urlencoded'
+ self.assertEqual(sorted(req.headers.items()),
+ [('Content-Length', '4'),
+ ('Content-Type', 'application/x-www-urlencoded'),
+ ('Host', 'localhost:80')])
+ self.assertEqual(req.environ['CONTENT_TYPE'],
+ 'application/x-www-urlencoded')
+
+ def test_request_query_and_POST_vars(self):
+ # port from doctest (docs/reference.txt)
+
+ # Query & POST variables
+ from webob.multidict import MultiDict
+ from webob.multidict import NestedMultiDict
+ from webob.multidict import NoVars
+ from webob.multidict import TrackableMultiDict
+ req = Request.blank('/test?check=a&check=b&name=Bob')
+ GET = TrackableMultiDict([('check', 'a'),
+ ('check', 'b'),
+ ('name', 'Bob')])
+ self.assertEqual(req.str_GET, GET)
+ self.assertEqual(req.str_GET['check'], 'b')
+ self.assertEqual(req.str_GET.getall('check'), ['a', 'b'])
+ self.assertEqual(req.str_GET.items(),
+ [('check', 'a'), ('check', 'b'), ('name', 'Bob')])
+
+ self.assert_(isinstance(req.str_POST, NoVars))
+ # NoVars can be read like a dict, but not written
+ self.assertEqual(req.str_POST.items(), [])
+ req.method = 'POST'
+ req.body = 'name=Joe&email=joe@example.com'
+ self.assertEqual(req.str_POST,
+ MultiDict([('name', 'Joe'),
+ ('email', 'joe@example.com')]))
+ self.assertEqual(req.str_POST['name'], 'Joe')
+
+ self.assert_(isinstance(req.str_params, NestedMultiDict))
+ self.assertEqual(req.str_params.items(),
+ [('check', 'a'),
+ ('check', 'b'),
+ ('name', 'Bob'),
+ ('name', 'Joe'),
+ ('email', 'joe@example.com')])
+ self.assertEqual(req.str_params['name'], 'Bob')
+ self.assertEqual(req.str_params.getall('name'), ['Bob', 'Joe'])
+
+ def test_request_put(self):
+ from datetime import datetime
+ from webob import Response
+ from webob import UTC
+ from webob.acceptparse import MIMEAccept
+ from webob.byterange import Range
+ from webob.etag import ETagMatcher
+ from webob.etag import _NoIfRange
+ from webob.multidict import MultiDict
+ from webob.multidict import TrackableMultiDict
+ from webob.multidict import UnicodeMultiDict
+ req = Request.blank('/test?check=a&check=b&name=Bob')
+ req.method = 'PUT'
+ req.body = 'var1=value1&var2=value2&rep=1&rep=2'
+ req.environ['CONTENT_LENGTH'] = str(len(req.body))
+ req.environ['CONTENT_TYPE'] = 'application/x-www-form-urlencoded'
+ GET = TrackableMultiDict([('check', 'a'),
+ ('check', 'b'),
+ ('name', 'Bob')])
+ self.assertEqual(req.str_GET, GET)
+ self.assertEqual(req.str_POST, MultiDict(
+ [('var1', 'value1'),
+ ('var2', 'value2'),
+ ('rep', '1'),
+ ('rep', '2')]))
+
+ # Unicode
+ req.charset = 'utf8'
+ self.assert_(isinstance(req.GET, UnicodeMultiDict))
+ self.assertEqual(req.GET.items(),
+ [('check', u'a'), ('check', u'b'), ('name', u'Bob')])
+
+ # Cookies
+ req.headers['Cookie'] = 'test=value'
+ self.assert_(isinstance(req.cookies, UnicodeMultiDict))
+ self.assertEqual(req.cookies.items(), [('test', u'value')])
+ req.charset = None
+ self.assertEqual(req.str_cookies, {'test': 'value'})
+
+ # Accept-* headers
+ self.assert_('text/html' in req.accept)
+ req.accept = 'text/html;q=0.5, application/xhtml+xml;q=1'
+ self.assert_(isinstance(req.accept, MIMEAccept))
+ self.assert_('text/html' in req.accept)
+
+ self.assertEqual(req.accept.first_match(['text/html',
+ 'application/xhtml+xml']), 'text/html')
+ self.assertEqual(req.accept.best_match(['text/html',
+ 'application/xhtml+xml']),
+ 'application/xhtml+xml')
+ self.assertEqual(req.accept.best_matches(),
+ ['application/xhtml+xml', 'text/html'])
+
+ req.accept_language = 'es, pt-BR'
+ self.assertEqual(req.accept_language.best_matches('en-US'),
+ ['es', 'pt-BR', 'en-US'])
+ self.assertEqual(req.accept_language.best_matches('es'), ['es'])
+
+ # Conditional Requests
+ server_token = 'opaque-token'
+ # shouldn't return 304
+ self.assert_(not server_token in req.if_none_match)
+ req.if_none_match = server_token
+ self.assert_(isinstance(req.if_none_match, ETagMatcher))
+ # You *should* return 304
+ self.assert_(server_token in req.if_none_match)
+
+ req.if_modified_since = datetime(2006, 1, 1, 12, 0, tzinfo=UTC)
+ self.assertEqual(req.headers['If-Modified-Since'],
+ 'Sun, 01 Jan 2006 12:00:00 GMT')
+ server_modified = datetime(2005, 1, 1, 12, 0, tzinfo=UTC)
+ self.assert_(req.if_modified_since)
+ self.assert_(req.if_modified_since >= server_modified)
+
+ self.assert_(isinstance(req.if_range, _NoIfRange))
+ self.assert_(req.if_range.match(etag='some-etag',
+ last_modified=datetime(2005, 1, 1, 12, 0)))
+ req.if_range = 'opaque-etag'
+ self.assert_(not req.if_range.match(etag='other-etag'))
+ self.assert_(req.if_range.match(etag='opaque-etag'))
+
+ res = Response(etag='opaque-etag')
+ self.assert_(req.if_range.match_response(res))
+
+ req.range = 'bytes=0-100'
+ self.assert_(isinstance(req.range, Range))
+ self.assertEqual(req.range.ranges, [(0, 101)])
+ cr = req.range.content_range(length=1000)
+ self.assertEqual((cr.start, cr.stop, cr.length), (0, 101, 1000))
+
+ self.assert_(server_token in req.if_match)
+ # No If-Match means everything is ok
+ req.if_match = server_token
+ self.assert_(server_token in req.if_match)
+ # Still OK
+ req.if_match = 'other-token'
+ # Not OK, should return 412 Precondition Failed:
+ self.assert_(not server_token in req.if_match)
+
+ def test_call_WSGI_app(self):
+ req = Request.blank('/')
+ def wsgi_app(environ, start_response):
+ start_response('200 OK', [('Content-type', 'text/plain')])
+ return ['Hi!']
+ self.assertEqual(req.call_application(wsgi_app),
+ ('200 OK', [('Content-type', 'text/plain')], ['Hi!']))
+
+ res = req.get_response(wsgi_app)
+ from webob.response import Response
+ self.assert_(isinstance(res, Response))
+ self.assertEqual(res.status, '200 OK')
+ from webob.headers import ResponseHeaders
+ self.assert_(isinstance(res.headers, ResponseHeaders))
+ self.assertEqual(res.headers.items(), [('Content-type', 'text/plain')])
+ self.assertEqual(res.body, 'Hi!')
+
+ def equal_req(self, req):
+ from cStringIO import StringIO
+ input = StringIO(str(req))
+ req2 = Request.from_file(input)
+ self.assertEqual(req.url, req2.url)
+ headers1 = dict(req.headers)
+ headers2 = dict(req2.headers)
+ self.assertEqual(int(headers1.get('Content-Length', '0')),
+ int(headers2.get('Content-Length', '0')))
+ if 'Content-Length' in headers1:
+ del headers1['Content-Length']
+ if 'Content-Length' in headers2:
+ del headers2['Content-Length']
+ self.assertEqual(headers1, headers2)
+ self.assertEqual(req.body, req2.body)
+
+
+def simpleapp(environ, start_response):
+ status = '200 OK'
+ response_headers = [('Content-type','text/plain')]
+ start_response(status, response_headers)
+ request = Request(environ)
+ request.remote_user = 'bob'
+ return [
+ 'Hello world!\n',
+ 'The get is %r' % request.str_GET,
+ ' and Val is %s\n' % request.str_GET.get('name'),
+ 'The languages are: %s\n' %
+ request.accept_language.best_matches('en-US'),
+ 'The accepttypes is: %s\n' %
+ request.accept.best_match(['application/xml', 'text/html']),
+ 'post is %r\n' % request.str_POST,
+ 'params is %r\n' % request.str_params,
+ 'cookies is %r\n' % request.str_cookies,
+ 'body: %r\n' % request.body,
+ 'method: %s\n' % request.method,
+ 'remote_user: %r\n' % request.environ['REMOTE_USER'],
+ 'host_url: %r; application_url: %r; path_url: %r; url: %r\n' %
+ (request.host_url,
+ request.application_url,
+ request.path_url,
+ request.url),
+ 'urlvars: %r\n' % request.urlvars,
+ 'urlargs: %r\n' % (request.urlargs, ),
+ 'is_xhr: %r\n' % request.is_xhr,
+ 'if_modified_since: %r\n' % request.if_modified_since,
+ 'user_agent: %r\n' % request.user_agent,
+ 'if_none_match: %r\n' % request.if_none_match,
+ ]
+
+
+
+_cgi_escaping_body = '''--boundary
+Content-Disposition: form-data; name="%20%22""
+
+
+--boundary--'''
+
+def _norm_req(s):
+ return '\r\n'.join(s.strip().replace('\r','').split('\n'))
+
+_test_req = """
+POST /webob/ HTTP/1.0
+Accept: */*
+Cache-Control: max-age=0
+Content-Type: multipart/form-data; boundary=----------------------------deb95b63e42a
+Host: pythonpaste.org
+User-Agent: UserAgent/1.0 (identifier-version) library/7.0 otherlibrary/0.8
+
+------------------------------deb95b63e42a
+Content-Disposition: form-data; name="foo"
+
+foo
+------------------------------deb95b63e42a
+Content-Disposition: form-data; name="bar"; filename="bar.txt"
+Content-type: application/octet-stream
+
+these are the contents of the file 'bar.txt'
+
+------------------------------deb95b63e42a--
+"""
+
+_test_req2 = """
+POST / HTTP/1.0
+Content-Length: 0
+
+"""
+
+_test_req = _norm_req(_test_req)
+_test_req2 = _norm_req(_test_req2) + '\r\n'
+
+class UnseekableInput(object):
+ def __init__(self, data):
+ self.data = data
+ self.pos = 0
+ def read(self, size=-1):
+ if size == -1:
+ t = self.data[self.pos:]
+ self.pos = len(self.data)
+ return t
+ else:
+ assert(self.pos + size <= len(self.data))
+ t = self.data[self.pos:self.pos+size]
+ self.pos += size
+ return t
+
+class UnseekableInputWithSeek(UnseekableInput):
+ def seek(self, pos, rel=0):
+ raise IOError("Invalid seek!")
+
+
+class FakeCGIBodyTests(unittest.TestCase):
+
+ def test_encode_multipart_value_type_options(self):
+ from StringIO import StringIO
+ from cgi import FieldStorage
+ from webob.request import BaseRequest, FakeCGIBody
+ from webob.multidict import MultiDict
+ multipart_type = 'multipart/form-data; boundary=foobar'
+ multipart_body = StringIO(
+ '--foobar\r\n'
+ 'Content-Disposition: form-data; name="bananas"; filename="bananas.txt"\r\n'
+ 'Content-type: text/plain; charset="utf-9"\r\n'
+ '\r\n'
+ "these are the contents of the file 'bananas.txt'\r\n"
+ '\r\n'
+ '--foobar--'
+ )
+ environ = BaseRequest.blank('/').environ
+ environ.update(CONTENT_TYPE=multipart_type)
+ environ.update(REQUEST_METHOD='POST')
+ fs = FieldStorage(multipart_body, environ=environ)
+ vars = MultiDict.from_fieldstorage(fs)
+ self.assertEqual(vars['bananas'].__class__, FieldStorage)
+ body = FakeCGIBody(vars, multipart_type)
+ self.assertEqual(body.read(), multipart_body.getvalue())
+
+ def test_encode_multipart_no_boundary(self):
+ from webob.request import FakeCGIBody
+ self.assertRaises(ValueError, FakeCGIBody, {}, 'multipart/form-data')
+
+ def test_repr(self):
+ from webob.request import FakeCGIBody
+ body = FakeCGIBody({'bananas': 'bananas'}, 'multipart/form-data; boundary=foobar')
+ body.read(1)
+ import re
+ self.assertEqual(
+ re.sub(r'\b0x[0-9a-f]+\b', '<whereitsat>', repr(body)),
+ "<FakeCGIBody at <whereitsat> viewing {'bananas': 'ba...nas'} at position 1>",
+ )
+
+ def test_iter(self):
+ from webob.request import FakeCGIBody
+ body = FakeCGIBody({'bananas': 'bananas'}, 'multipart/form-data; boundary=foobar')
+ self.assertEqual(list(body), [
+ '--foobar\r\n',
+ 'Content-Disposition: form-data; name="bananas"\r\n',
+ '\r\n',
+ 'bananas\r\n',
+ '--foobar--',
+ ])
+
+ def test_readline(self):
+ from webob.request import FakeCGIBody
+ body = FakeCGIBody({'bananas': 'bananas'}, 'multipart/form-data; boundary=foobar')
+ self.assertEqual(body.readline(), '--foobar\r\n')
+ self.assertEqual(body.readline(), 'Content-Disposition: form-data; name="bananas"\r\n')
+ self.assertEqual(body.readline(), '\r\n')
+ self.assertEqual(body.readline(), 'bananas\r\n')
+ self.assertEqual(body.readline(), '--foobar--')
+ # subsequent calls to readline will return ''
+
+ def test_read_bad_content_type(self):
+ from webob.request import FakeCGIBody
+ body = FakeCGIBody({'bananas': 'bananas'}, 'application/jibberjabber')
+ self.assertRaises(AssertionError, body.read)
+
+ def test_read_urlencoded(self):
+ from webob.request import FakeCGIBody
+ body = FakeCGIBody({'bananas': 'bananas'}, 'application/x-www-form-urlencoded')
+ self.assertEqual(body.read(), 'bananas=bananas')
+
+ def test_tell(self):
+ from webob.request import FakeCGIBody
+ body = FakeCGIBody({'bananas': 'bananas'},
+ 'application/x-www-form-urlencoded')
+ body.position = 1
+ self.assertEqual(body.tell(), 1)
+
+class Test_cgi_FieldStorage__repr__patch(unittest.TestCase):
+ def _callFUT(self, fake):
+ from webob.request import _cgi_FieldStorage__repr__patch
+ return _cgi_FieldStorage__repr__patch(fake)
+
+ def test_with_file(self):
+ class Fake(object):
+ name = 'name'
+ file = 'file'
+ filename = 'filename'
+ value = 'value'
+ fake = Fake()
+ result = self._callFUT(fake)
+ self.assertEqual(result, "FieldStorage('name', 'filename')")
+
+ def test_without_file(self):
+ class Fake(object):
+ name = 'name'
+ file = None
+ filename = 'filename'
+ value = 'value'
+ fake = Fake()
+ result = self._callFUT(fake)
+ self.assertEqual(result, "FieldStorage('name', 'filename', 'value')")
diff --git a/lib/webob_1_1_1/tests/test_request_nose.py b/lib/webob_1_1_1/tests/test_request_nose.py
new file mode 100644
index 0000000..9ce9dfa
--- /dev/null
+++ b/lib/webob_1_1_1/tests/test_request_nose.py
@@ -0,0 +1,131 @@
+import webob
+from webob import Request
+from nose.tools import eq_ as eq, assert_raises
+
+def test_request_no_method():
+ assert Request({}).method == 'GET'
+
+def test_request_read_no_content_length():
+ req, input = _make_read_tracked_request('abc', 'FOO')
+ assert req.content_length is None
+ assert req.body == ''
+ assert not input.was_read
+
+def test_request_read_no_content_length_POST():
+ req, input = _make_read_tracked_request('abc', 'POST')
+ assert req.content_length is None
+ assert req.body == 'abc'
+ assert input.was_read
+
+def test_request_read_no_flag_but_content_length_is_present():
+ req, input = _make_read_tracked_request('abc')
+ req.content_length = 3
+ assert req.body == 'abc'
+ assert input.was_read
+
+def test_request_read_no_content_length_but_flagged_readable():
+ req, input = _make_read_tracked_request('abc')
+ req.is_body_readable = True
+ assert req.body == 'abc'
+ assert input.was_read
+
+def test_request_read_after_setting_body_file():
+ req = _make_read_tracked_request()[0]
+ input = req.body_file = ReadTracker('abc')
+ assert req.content_length is None
+ assert not req.is_body_seekable
+ assert req.body == 'abc'
+ # reading body made the input seekable and set the clen
+ assert req.content_length == 3
+ assert req.is_body_seekable
+ assert input.was_read
+
+def test_request_readlines():
+ req = Request.blank('/', POST='a\n'*3)
+ req.is_body_seekable = False
+ eq(req.body_file.readlines(), ['a\n'] * 3)
+
+def test_request_delete_with_body():
+ req = Request.blank('/', method='DELETE')
+ assert not req.is_body_readable
+ req.body = 'abc'
+ assert req.is_body_readable
+ assert req.body_file.read() == 'abc'
+
+
+def _make_read_tracked_request(data='', method='PUT'):
+ input = ReadTracker(data)
+ env = {
+ 'REQUEST_METHOD': method,
+ 'wsgi.input': input,
+ }
+ return Request(env), input
+
+class ReadTracker(object):
+ """
+ Helper object to determine if the input was read or not
+ """
+ def __init__(self, data):
+ self.data = data
+ self.was_read = False
+ def read(self, size=-1):
+ if size < 0:
+ size = len(self.data)
+ assert size == len(self.data)
+ self.was_read = True
+ return self.data
+
+
+def test_limited_length_file_repr():
+ req = Request.blank('/', POST='x')
+ req.body_file_raw = 'dummy'
+ req.is_body_seekable = False
+ eq(repr(req.body_file), "<LimitedLengthFile('dummy', maxlen=1)>")
+
+def test_request_wrong_clen(is_seekable=False):
+ tlen = 1<<20
+ req = Request.blank('/', POST='x'*tlen)
+ eq(req.content_length, tlen)
+ req.body_file = _Helper_test_request_wrong_clen(req.body_file)
+ eq(req.content_length, None)
+ req.content_length = tlen + 100
+ req.is_body_seekable = is_seekable
+ eq(req.content_length, tlen+100)
+ # this raises AssertionError if the body reading
+ # trusts content_length too much
+ assert_raises(IOError, req.copy_body)
+
+def test_request_wrong_clen_seekable():
+ test_request_wrong_clen(is_seekable=True)
+
+def test_webob_version():
+ assert isinstance(webob.__version__, str)
+
+class _Helper_test_request_wrong_clen(object):
+ def __init__(self, f):
+ self.f = f
+ self.file_ended = False
+
+ def read(self, *args):
+ r = self.f.read(*args)
+ if not r:
+ if self.file_ended:
+ raise AssertionError("Reading should stop after first empty string")
+ self.file_ended = True
+ return r
+
+
+def test_disconnect_detection_cgi():
+ data = 'abc'*(1<<20)
+ req = Request.blank('/', POST={'file':('test-file', data)})
+ req.is_body_seekable = False
+ req.POST # should not raise exceptions
+
+def test_disconnect_detection_hinted_readline():
+ data = 'abc'*(1<<20)
+ req = Request.blank('/', POST=data)
+ req.is_body_seekable = False
+ line = req.body_file.readline(1<<16)
+ assert line
+ assert data.startswith(line)
+
diff --git a/lib/webob_1_1_1/tests/test_response.py b/lib/webob_1_1_1/tests/test_response.py
new file mode 100644
index 0000000..04d70f4
--- /dev/null
+++ b/lib/webob_1_1_1/tests/test_response.py
@@ -0,0 +1,1002 @@
+import sys
+import zlib
+if sys.version >= '2.7':
+ from io import BytesIO as StringIO
+else:
+ from cStringIO import StringIO
+try:
+ from hashlib import md5
+except ImportError:
+ from md5 import md5
+
+from nose.tools import eq_, ok_, assert_raises
+
+from webob import BaseRequest, Request, Response
+
+def simple_app(environ, start_response):
+ start_response('200 OK', [
+ ('Content-Type', 'text/html; charset=utf8'),
+ ])
+ return ['OK']
+
+def test_response():
+ req = BaseRequest.blank('/')
+ res = req.get_response(simple_app)
+ assert res.status == '200 OK'
+ assert res.status_int == 200
+ assert res.body == "OK"
+ assert res.charset == 'utf8'
+ assert res.content_type == 'text/html'
+ res.status = 404
+ assert res.status == '404 Not Found'
+ assert res.status_int == 404
+ res.body = 'Not OK'
+ assert ''.join(res.app_iter) == 'Not OK'
+ res.charset = 'iso8859-1'
+ assert res.headers['content-type'] == 'text/html; charset=iso8859-1'
+ res.content_type = 'text/xml'
+ assert res.headers['content-type'] == 'text/xml; charset=iso8859-1'
+ res.headers = {'content-type': 'text/html'}
+ assert res.headers['content-type'] == 'text/html'
+ assert res.headerlist == [('content-type', 'text/html')]
+ res.set_cookie('x', 'y')
+ assert res.headers['set-cookie'].strip(';') == 'x=y; Path=/'
+ res = Response('a body', '200 OK', content_type='text/html')
+ res.encode_content()
+ assert res.content_encoding == 'gzip'
+ eq_(res.body, '\x1f\x8b\x08\x00\x00\x00\x00\x00\x02\xffKTH\xcaO\xa9\x04\x00\xf6\x86GI\x06\x00\x00\x00')
+ res.decode_content()
+ assert res.content_encoding is None
+ assert res.body == 'a body'
+ res.set_cookie('x', u'foo') # test unicode value
+ assert_raises(TypeError, Response, app_iter=iter(['a']),
+ body="somebody")
+ del req.environ
+ eq_(Response(request=req)._environ, req)
+ eq_(Response(request=req)._request, None)
+ assert_raises(TypeError, Response, charset=None,
+ body=u"unicode body")
+ assert_raises(TypeError, Response, wrong_key='dummy')
+
+def test_content_type():
+ r = Response()
+ # default ctype and charset
+ eq_(r.content_type, 'text/html')
+ eq_(r.charset, 'UTF-8')
+ # setting to none, removes the header
+ r.content_type = None
+ eq_(r.content_type, None)
+ eq_(r.charset, None)
+ # can set missing ctype
+ r.content_type = None
+ eq_(r.content_type, None)
+
+def test_cookies():
+ res = Response()
+ res.set_cookie('x', u'\N{BLACK SQUARE}') # test unicode value
+ eq_(res.headers.getall('set-cookie'), ['x="\\342\\226\\240"; Path=/']) # uft8 encoded
+ r2 = res.merge_cookies(simple_app)
+ r2 = BaseRequest.blank('/').get_response(r2)
+ eq_(r2.headerlist,
+ [('Content-Type', 'text/html; charset=utf8'),
+ ('Set-Cookie', 'x="\\342\\226\\240"; Path=/'),
+ ]
+ )
+
+def test_http_only_cookie():
+ req = Request.blank('/')
+ res = req.get_response(Response('blah'))
+ res.set_cookie("foo", "foo", httponly=True)
+ eq_(res.headers['set-cookie'], 'foo=foo; Path=/; HttpOnly')
+
+def test_headers():
+ r = Response()
+ tval = 'application/x-test'
+ r.headers.update({'content-type': tval})
+ eq_(r.headers.getall('content-type'), [tval])
+
+def test_response_copy():
+ r = Response(app_iter=iter(['a']))
+ r2 = r.copy()
+ eq_(r.body, 'a')
+ eq_(r2.body, 'a')
+
+def test_response_copy_content_md5():
+ res = Response()
+ res.md5_etag(set_content_md5=True)
+ assert res.content_md5
+ res2 = res.copy()
+ assert res.content_md5
+ assert res2.content_md5
+ eq_(res.content_md5, res2.content_md5)
+
+def test_HEAD_closes():
+ req = Request.blank('/')
+ req.method = 'HEAD'
+ app_iter = StringIO('foo')
+ res = req.get_response(Response(app_iter=app_iter))
+ eq_(res.status_int, 200)
+ eq_(res.body, '')
+ ok_(app_iter.closed)
+
+def test_HEAD_conditional_response_returns_empty_response():
+ from webob.response import EmptyResponse
+ req = Request.blank('/')
+ req.method = 'HEAD'
+ res = Response(request=req, conditional_response=True)
+ class FakeRequest:
+ method = 'HEAD'
+ if_none_match = 'none'
+ if_modified_since = False
+ range = False
+ def __init__(self, env):
+ self.env = env
+ def start_response(status, headerlist):
+ pass
+ res.RequestClass = FakeRequest
+ result = res({}, start_response)
+ ok_(isinstance(result, EmptyResponse))
+
+def test_HEAD_conditional_response_range_empty_response():
+ from webob.response import EmptyResponse
+ req = Request.blank('/')
+ req.method = 'HEAD'
+ res = Response(request=req, conditional_response=True)
+ res.status_int = 200
+ res.body = 'Are we not men?'
+ res.content_length = len(res.body)
+ class FakeRequest:
+ method = 'HEAD'
+ if_none_match = 'none'
+ if_modified_since = False
+ def __init__(self, env):
+ self.env = env
+ self.range = self # simulate inner api
+ self.if_range = self
+ def content_range(self, length):
+ """range attr"""
+ class Range:
+ start = 4
+ stop = 5
+ return Range
+ def match_response(self, res):
+ """if_range_match attr"""
+ return True
+ def start_response(status, headerlist):
+ pass
+ res.RequestClass = FakeRequest
+ result = res({}, start_response)
+ ok_(isinstance(result, EmptyResponse), result)
+
+def test_conditional_response_if_none_match_false():
+ req = Request.blank('/', if_none_match='foo')
+ resp = Response(app_iter=['foo\n'],
+ conditional_response=True, etag='foo')
+ resp = req.get_response(resp)
+ eq_(resp.status_int, 304)
+
+def test_conditional_response_if_none_match_true():
+ req = Request.blank('/', if_none_match='foo')
+ resp = Response(app_iter=['foo\n'],
+ conditional_response=True, etag='bar')
+ resp = req.get_response(resp)
+ eq_(resp.status_int, 200)
+
+def test_conditional_response_if_modified_since_false():
+ from datetime import datetime, timedelta
+ req = Request.blank('/', if_modified_since=datetime(2011, 3, 17, 13, 0, 0))
+ resp = Response(app_iter=['foo\n'], conditional_response=True,
+ last_modified=req.if_modified_since-timedelta(seconds=1))
+ resp = req.get_response(resp)
+ eq_(resp.status_int, 304)
+
+def test_conditional_response_if_modified_since_true():
+ from datetime import datetime, timedelta
+ req = Request.blank('/', if_modified_since=datetime(2011, 3, 17, 13, 0, 0))
+ resp = Response(app_iter=['foo\n'], conditional_response=True,
+ last_modified=req.if_modified_since+timedelta(seconds=1))
+ resp = req.get_response(resp)
+ eq_(resp.status_int, 200)
+
+def test_conditional_response_range_not_satisfiable_response():
+ req = Request.blank('/', range='bytes=100-200')
+ resp = Response(app_iter=['foo\n'], content_length=4,
+ conditional_response=True)
+ resp = req.get_response(resp)
+ eq_(resp.status_int, 416)
+ eq_(resp.content_range.start, None)
+ eq_(resp.content_range.stop, None)
+ eq_(resp.content_range.length, 4)
+ eq_(resp.body, 'Requested range not satisfiable: bytes=100-200')
+
+def test_HEAD_conditional_response_range_not_satisfiable_response():
+ req = Request.blank('/', method='HEAD', range='bytes=100-200')
+ resp = Response(app_iter=['foo\n'], content_length=4,
+ conditional_response=True)
+ resp = req.get_response(resp)
+ eq_(resp.status_int, 416)
+ eq_(resp.content_range.start, None)
+ eq_(resp.content_range.stop, None)
+ eq_(resp.content_range.length, 4)
+ eq_(resp.body, '')
+
+def test_del_environ():
+ res = Response()
+ res.environ = {'yo': 'mama'}
+ eq_(res.environ, {'yo': 'mama'})
+ del res.environ
+ eq_(res.environ, None)
+ eq_(res.request, None)
+
+def test_set_request_environ():
+ res = Response()
+ class FakeRequest:
+ environ = {'jo': 'mama'}
+ res.request = FakeRequest
+ eq_(res.environ, {'jo': 'mama'})
+ eq_(res.request, FakeRequest)
+ res.environ = None
+ eq_(res.environ, None)
+ eq_(res.request, None)
+
+def test_del_request():
+ res = Response()
+ class FakeRequest:
+ environ = {}
+ res.request = FakeRequest
+ del res.request
+ eq_(res.environ, None)
+ eq_(res.request, None)
+
+def test_set_environ_via_request_subterfuge():
+ class FakeRequest:
+ def __init__(self, env):
+ self.environ = env
+ res = Response()
+ res.RequestClass = FakeRequest
+ res.request = {'action': 'dwim'}
+ eq_(res.environ, {'action': 'dwim'})
+ ok_(isinstance(res.request, FakeRequest))
+ eq_(res.request.environ, res.environ)
+
+def test_set_request():
+ res = Response()
+ class FakeRequest:
+ environ = {'foo': 'bar'}
+ res.request = FakeRequest
+ eq_(res.request, FakeRequest)
+ eq_(res.environ, FakeRequest.environ)
+ res.request = None
+ eq_(res.environ, None)
+ eq_(res.request, None)
+
+def test_md5_etag():
+ res = Response()
+ res.body = """\
+In A.D. 2101
+War was beginning.
+Captain: What happen ?
+Mechanic: Somebody set up us the bomb.
+Operator: We get signal.
+Captain: What !
+Operator: Main screen turn on.
+Captain: It's You !!
+Cats: How are you gentlemen !!
+Cats: All your base are belong to us.
+Cats: You are on the way to destruction.
+Captain: What you say !!
+Cats: You have no chance to survive make your time.
+Cats: HA HA HA HA ....
+Captain: Take off every 'zig' !!
+Captain: You know what you doing.
+Captain: Move 'zig'.
+Captain: For great justice."""
+ res.md5_etag()
+ ok_(res.etag)
+ ok_('\n' not in res.etag)
+ eq_(res.etag,
+ md5(res.body).digest().encode('base64').replace('\n', '').strip('='))
+ eq_(res.content_md5, None)
+
+def test_md5_etag_set_content_md5():
+ res = Response()
+ b = 'The quick brown fox jumps over the lazy dog'
+ res.md5_etag(b, set_content_md5=True)
+ ok_(res.content_md5,
+ md5(b).digest().encode('base64').replace('\n', '').strip('='))
+
+def test_decode_content_defaults_to_identity():
+ res = Response()
+ res.body = 'There be dragons'
+ res.decode_content()
+ eq_(res.body, 'There be dragons')
+
+def test_decode_content_with_deflate():
+ res = Response()
+ b = 'Hey Hey Hey'
+ # Simulate inflate by chopping the headers off
+ # the gzip encoded data
+ res.body = zlib.compress(b)[2:-4]
+ res.content_encoding = 'deflate'
+ res.decode_content()
+ eq_(res.body, b)
+ eq_(res.content_encoding, None)
+
+def test_content_length():
+ r0 = Response('x'*10, content_length=10)
+
+ req_head = Request.blank('/', method='HEAD')
+ r1 = req_head.get_response(r0)
+ eq_(r1.status_int, 200)
+ eq_(r1.body, '')
+ eq_(r1.content_length, 10)
+
+ req_get = Request.blank('/')
+ r2 = req_get.get_response(r0)
+ eq_(r2.status_int, 200)
+ eq_(r2.body, 'x'*10)
+ eq_(r2.content_length, 10)
+
+ r3 = Response(app_iter=['x']*10)
+ eq_(r3.content_length, None)
+ eq_(r3.body, 'x'*10)
+ eq_(r3.content_length, 10)
+
+ r4 = Response(app_iter=['x']*10, content_length=20) # wrong content_length
+ eq_(r4.content_length, 20)
+ assert_raises(AssertionError, lambda: r4.body)
+
+ req_range = Request.blank('/', range=(0,5))
+ r0.conditional_response = True
+ r5 = req_range.get_response(r0)
+ eq_(r5.status_int, 206)
+ eq_(r5.body, 'xxxxx')
+ eq_(r5.content_length, 5)
+
+def test_app_iter_range():
+ req = Request.blank('/', range=(2,5))
+ for app_iter in [
+ ['012345'],
+ ['0', '12345'],
+ ['0', '1234', '5'],
+ ['01', '2345'],
+ ['01', '234', '5'],
+ ['012', '34', '5'],
+ ['012', '3', '4', '5'],
+ ['012', '3', '45'],
+ ['0', '12', '34', '5'],
+ ['0', '12', '345'],
+ ]:
+ r = Response(
+ app_iter=app_iter,
+ content_length=6,
+ conditional_response=True,
+ )
+ res = req.get_response(r)
+ eq_(list(res.content_range), [2,5,6])
+ eq_(res.body, '234', 'body=%r; app_iter=%r' % (res.body, app_iter))
+
+def test_app_iter_range_inner_method():
+ class FakeAppIter:
+ def app_iter_range(self, start, stop):
+ return 'you win', start, stop
+ res = Response(app_iter=FakeAppIter())
+ eq_(res.app_iter_range(30, 40), ('you win', 30, 40))
+
+def test_content_type_in_headerlist():
+ # Couldn't manage to clone Response in order to modify class
+ # attributes safely. Shouldn't classes be fresh imported for every
+ # test?
+ default_content_type = Response.default_content_type
+ Response.default_content_type = None
+ try:
+ res = Response(headerlist=[('Content-Type', 'text/html')],
+ charset='utf8')
+ ok_(res._headerlist)
+ eq_(res.charset, 'utf8')
+ finally:
+ Response.default_content_type = default_content_type
+
+def test_from_file():
+ res = Response('test')
+ equal_resp(res)
+ res = Response(app_iter=iter(['test ', 'body']),
+ content_type='text/plain')
+ equal_resp(res)
+
+def equal_resp(res):
+ input_ = StringIO(str(res))
+ res2 = Response.from_file(input_)
+ eq_(res.body, res2.body)
+ eq_(res.headers, res2.headers)
+
+def test_from_file_w_leading_space_in_header():
+ # Make sure the removal of code dealing with leading spaces is safe
+ res1 = Response()
+ file_w_space = StringIO('200 OK\n\tContent-Type: text/html; charset=UTF-8')
+ res2 = Response.from_file(file_w_space)
+ eq_(res1.headers, res2.headers)
+
+def test_file_bad_header():
+ file_w_bh = StringIO('200 OK\nBad Header')
+ assert_raises(ValueError, Response.from_file, file_w_bh)
+
+def test_set_status():
+ res = Response()
+ res.status = u"OK 200"
+ eq_(res.status, "OK 200")
+ assert_raises(TypeError, setattr, res, 'status', float(200))
+
+def test_set_headerlist():
+ res = Response()
+ # looks like a list
+ res.headerlist = (('Content-Type', 'text/html; charset=UTF-8'),)
+ eq_(res.headerlist, [('Content-Type', 'text/html; charset=UTF-8')])
+ # has items
+ res.headerlist = {'Content-Type': 'text/html; charset=UTF-8'}
+ eq_(res.headerlist, [('Content-Type', 'text/html; charset=UTF-8')])
+ del res.headerlist
+ eq_(res.headerlist, [])
+
+def test_request_uri_no_script_name():
+ from webob.response import _request_uri
+ environ = {
+ 'wsgi.url_scheme': 'http',
+ 'HTTP_HOST': 'test.com',
+ 'SCRIPT_NAME': '/foobar',
+ }
+ eq_(_request_uri(environ), 'http://test.com/foobar')
+
+def test_request_uri_https():
+ from webob.response import _request_uri
+ environ = {
+ 'wsgi.url_scheme': 'https',
+ 'SERVER_NAME': 'test.com',
+ 'SERVER_PORT': '443',
+ 'SCRIPT_NAME': '/foobar',
+ }
+ eq_(_request_uri(environ), 'https://test.com/foobar')
+
+def test_app_iter_range_starts_after_iter_end():
+ from webob.response import AppIterRange
+ range = AppIterRange(iter([]), start=1, stop=1)
+ eq_(list(range), [])
+
+def test_resp_write_app_iter_non_list():
+ res = Response(app_iter=('a','b'))
+ eq_(res.content_length, None)
+ res.write('c')
+ eq_(res.body, 'abc')
+ eq_(res.content_length, 3)
+
+def test_response_file_body_writelines():
+ from webob.response import ResponseBodyFile
+ res = Response(app_iter=['foo'])
+ rbo = ResponseBodyFile(res)
+ rbo.writelines(['bar', 'baz'])
+ eq_(res.app_iter, ['foo', 'bar', 'baz'])
+ rbo.flush() # noop
+ eq_(res.app_iter, ['foo', 'bar', 'baz'])
+
+def test_response_write_non_str():
+ res = Response()
+ assert_raises(TypeError, res.write, object())
+
+def test_response_file_body_write_empty_app_iter():
+ from webob.response import ResponseBodyFile
+ res = Response('foo')
+ res.write('baz')
+ eq_(res.app_iter, ['foo', 'baz'])
+
+def test_response_file_body_write_empty_body():
+ res = Response('')
+ res.write('baz')
+ eq_(res.app_iter, ['', 'baz'])
+
+def test_response_file_body_close_not_implemented():
+ rbo = Response().body_file
+ assert_raises(NotImplementedError, rbo.close)
+
+def test_response_file_body_repr():
+ rbo = Response().body_file
+ rbo.response = 'yo'
+ eq_(repr(rbo), "<body_file for 'yo'>")
+
+def test_body_get_is_none():
+ res = Response()
+ res._app_iter = None
+ assert_raises(TypeError, Response, app_iter=iter(['a']),
+ body="somebody")
+ assert_raises(AttributeError, res.__getattribute__, 'body')
+
+def test_body_get_is_unicode_notverylong():
+ res = Response(app_iter=(u'foo',))
+ assert_raises(TypeError, res.__getattribute__, 'body')
+
+def test_body_get_is_unicode():
+ res = Response(app_iter=(['x'] * 51 + [u'x']))
+ assert_raises(TypeError, res.__getattribute__, 'body')
+
+def test_body_set_not_unicode_or_str():
+ res = Response()
+ assert_raises(TypeError, res.__setattr__, 'body', object())
+
+def test_body_set_unicode():
+ res = Response()
+ assert_raises(TypeError, res.__setattr__, 'body', u'abc')
+
+def test_body_set_under_body_doesnt_exist():
+ res = Response('abc')
+ eq_(res.body, 'abc')
+ eq_(res.content_length, 3)
+
+def test_body_del():
+ res = Response('123')
+ del res.body
+ eq_(res.body, '')
+ eq_(res.content_length, 0)
+
+def test_text_get_no_charset():
+ res = Response(charset=None)
+ assert_raises(AttributeError, res.__getattribute__, 'text')
+
+def test_unicode_body():
+ res = Response()
+ res.charset = 'utf-8'
+ bbody = 'La Pe\xc3\xb1a' # binary string
+ ubody = unicode(bbody, 'utf-8') # unicode string
+ res.body = bbody
+ eq_(res.unicode_body, ubody)
+ res.ubody = ubody
+ eq_(res.body, bbody)
+ del res.ubody
+ eq_(res.body, '')
+
+def test_text_get_decode():
+ res = Response()
+ res.charset = 'utf-8'
+ res.body = 'La Pe\xc3\xb1a'
+ eq_(res.text, unicode('La Pe\xc3\xb1a', 'utf-8'))
+
+def test_text_set_no_charset():
+ res = Response()
+ res.charset = None
+ assert_raises(AttributeError, res.__setattr__, 'text', 'abc')
+
+def test_text_set_not_unicode():
+ res = Response()
+ res.charset = 'utf-8'
+ assert_raises(TypeError, res.__setattr__, 'text',
+ 'La Pe\xc3\xb1a')
+
+def test_text_del():
+ res = Response('123')
+ del res.text
+ eq_(res.body, '')
+ eq_(res.content_length, 0)
+
+def test_body_file_del():
+ res = Response()
+ res.body = '123'
+ eq_(res.content_length, 3)
+ eq_(res.app_iter, ['123'])
+ del res.body_file
+ eq_(res.body, '')
+ eq_(res.content_length, 0)
+
+def test_write_unicode():
+ res = Response()
+ res.text = unicode('La Pe\xc3\xb1a', 'utf-8')
+ res.write(u'a')
+ eq_(res.text, unicode('La Pe\xc3\xb1aa', 'utf-8'))
+
+def test_write_unicode_no_charset():
+ res = Response(charset=None)
+ assert_raises(TypeError, res.write, u'a')
+
+def test_write_text():
+ res = Response()
+ res.body = 'abc'
+ res.write(u'a')
+ eq_(res.text, 'abca')
+
+def test_app_iter_del():
+ res = Response(
+ content_length=3,
+ app_iter=['123'],
+ )
+ del res.app_iter
+ eq_(res.body, '')
+ eq_(res.content_length, None)
+
+def test_charset_set_no_content_type_header():
+ res = Response()
+ res.headers.pop('Content-Type', None)
+ assert_raises(AttributeError, res.__setattr__, 'charset', 'utf-8')
+
+def test_charset_del_no_content_type_header():
+ res = Response()
+ res.headers.pop('Content-Type', None)
+ eq_(res._charset__del(), None)
+
+def test_content_type_params_get_no_semicolon_in_content_type_header():
+ res = Response()
+ res.headers['Content-Type'] = 'foo'
+ eq_(res.content_type_params, {})
+
+def test_content_type_params_get_semicolon_in_content_type_header():
+ res = Response()
+ res.headers['Content-Type'] = 'foo;encoding=utf-8'
+ eq_(res.content_type_params, {'encoding':'utf-8'})
+
+def test_content_type_params_set_value_dict_empty():
+ res = Response()
+ res.headers['Content-Type'] = 'foo;bar'
+ res.content_type_params = None
+ eq_(res.headers['Content-Type'], 'foo')
+
+def test_content_type_params_set_ok_param_quoting():
+ res = Response()
+ res.content_type_params = {'a':''}
+ eq_(res.headers['Content-Type'], 'text/html; a=""')
+
+def test_set_cookie_overwrite():
+ res = Response()
+ res.set_cookie('a', '1')
+ res.set_cookie('a', '2', overwrite=True)
+ eq_(res.headerlist[-1], ('Set-Cookie', 'a=2; Path=/'))
+
+def test_set_cookie_value_is_None():
+ res = Response()
+ res.set_cookie('a', None)
+ eq_(res.headerlist[-1][0], 'Set-Cookie')
+ val = [ x.strip() for x in res.headerlist[-1][1].split(';')]
+ assert len(val) == 4
+ val.sort()
+ eq_(val[0], 'Max-Age=0')
+ eq_(val[1], 'Path=/')
+ eq_(val[2], 'a=')
+ assert val[3].startswith('expires')
+
+def test_set_cookie_expires_is_None_and_max_age_is_int():
+ res = Response()
+ res.set_cookie('a', '1', max_age=100)
+ eq_(res.headerlist[-1][0], 'Set-Cookie')
+ val = [ x.strip() for x in res.headerlist[-1][1].split(';')]
+ assert len(val) == 4
+ val.sort()
+ eq_(val[0], 'Max-Age=100')
+ eq_(val[1], 'Path=/')
+ eq_(val[2], 'a=1')
+ assert val[3].startswith('expires')
+
+def test_set_cookie_expires_is_None_and_max_age_is_timedelta():
+ from datetime import timedelta
+ res = Response()
+ res.set_cookie('a', '1', max_age=timedelta(seconds=100))
+ eq_(res.headerlist[-1][0], 'Set-Cookie')
+ val = [ x.strip() for x in res.headerlist[-1][1].split(';')]
+ assert len(val) == 4
+ val.sort()
+ eq_(val[0], 'Max-Age=100')
+ eq_(val[1], 'Path=/')
+ eq_(val[2], 'a=1')
+ assert val[3].startswith('expires')
+
+def test_set_cookie_expires_is_not_None_and_max_age_is_None():
+ import datetime
+ res = Response()
+ then = datetime.datetime.utcnow() + datetime.timedelta(days=1)
+ res.set_cookie('a', '1', expires=then)
+ eq_(res.headerlist[-1][0], 'Set-Cookie')
+ val = [ x.strip() for x in res.headerlist[-1][1].split(';')]
+ assert len(val) == 4
+ val.sort()
+ ok_(val[0] in ('Max-Age=86399', 'Max-Age=86400'))
+ eq_(val[1], 'Path=/')
+ eq_(val[2], 'a=1')
+ assert val[3].startswith('expires')
+
+def test_set_cookie_value_is_unicode():
+ res = Response()
+ val = unicode('La Pe\xc3\xb1a', 'utf-8')
+ res.set_cookie('a', val)
+ eq_(res.headerlist[-1], (r'Set-Cookie', 'a="La Pe\\303\\261a"; Path=/'))
+
+def test_delete_cookie():
+ res = Response()
+ res.headers['Set-Cookie'] = 'a=2; Path=/'
+ res.delete_cookie('a')
+ eq_(res.headerlist[-1][0], 'Set-Cookie')
+ val = [ x.strip() for x in res.headerlist[-1][1].split(';')]
+ assert len(val) == 4
+ val.sort()
+ eq_(val[0], 'Max-Age=0')
+ eq_(val[1], 'Path=/')
+ eq_(val[2], 'a=')
+ assert val[3].startswith('expires')
+
+def test_delete_cookie_with_path():
+ res = Response()
+ res.headers['Set-Cookie'] = 'a=2; Path=/'
+ res.delete_cookie('a', path='/abc')
+ eq_(res.headerlist[-1][0], 'Set-Cookie')
+ val = [ x.strip() for x in res.headerlist[-1][1].split(';')]
+ assert len(val) == 4
+ val.sort()
+ eq_(val[0], 'Max-Age=0')
+ eq_(val[1], 'Path=/abc')
+ eq_(val[2], 'a=')
+ assert val[3].startswith('expires')
+
+def test_delete_cookie_with_domain():
+ res = Response()
+ res.headers['Set-Cookie'] = 'a=2; Path=/'
+ res.delete_cookie('a', path='/abc', domain='example.com')
+ eq_(res.headerlist[-1][0], 'Set-Cookie')
+ val = [ x.strip() for x in res.headerlist[-1][1].split(';')]
+ assert len(val) == 5
+ val.sort()
+ eq_(val[0], 'Domain=example.com')
+ eq_(val[1], 'Max-Age=0')
+ eq_(val[2], 'Path=/abc')
+ eq_(val[3], 'a=')
+ assert val[4].startswith('expires')
+
+def test_unset_cookie_not_existing_and_not_strict():
+ res = Response()
+ result = res.unset_cookie('a', strict=False)
+ assert result is None
+
+def test_unset_cookie_not_existing_and_strict():
+ res = Response()
+ assert_raises(KeyError, res.unset_cookie, 'a')
+
+def test_unset_cookie_key_in_cookies():
+ res = Response()
+ res.headers.add('Set-Cookie', 'a=2; Path=/')
+ res.headers.add('Set-Cookie', 'b=3; Path=/')
+ res.unset_cookie('a')
+ eq_(res.headers.getall('Set-Cookie'), ['b=3; Path=/'])
+
+def test_merge_cookies_no_set_cookie():
+ res = Response()
+ result = res.merge_cookies('abc')
+ eq_(result, 'abc')
+
+def test_merge_cookies_resp_is_Response():
+ inner_res = Response()
+ res = Response()
+ res.set_cookie('a', '1')
+ result = res.merge_cookies(inner_res)
+ eq_(result.headers.getall('Set-Cookie'), ['a=1; Path=/'])
+
+def test_merge_cookies_resp_is_wsgi_callable():
+ L = []
+ def dummy_wsgi_callable(environ, start_response):
+ L.append((environ, start_response))
+ return 'abc'
+ res = Response()
+ res.set_cookie('a', '1')
+ wsgiapp = res.merge_cookies(dummy_wsgi_callable)
+ environ = {}
+ def dummy_start_response(status, headers, exc_info=None):
+ eq_(headers, [('Set-Cookie', 'a=1; Path=/')])
+ result = wsgiapp(environ, dummy_start_response)
+ assert result == 'abc'
+ assert len(L) == 1
+ L[0][1]('200 OK', []) # invoke dummy_start_response assertion
+
+def test_body_get_body_is_None_len_app_iter_is_zero():
+ res = Response()
+ res._app_iter = StringIO()
+ res._body = None
+ result = res.body
+ eq_(result, '')
+
+def test_cache_control_get():
+ res = Response()
+ eq_(repr(res.cache_control), "<CacheControl ''>")
+ eq_(res.cache_control.max_age, None)
+
+def test_location():
+ # covers webob/response.py:934-938
+ res = Response()
+ res.location = '/test.html'
+ eq_(res.location, '/test.html')
+ req = Request.blank('/')
+ eq_(req.get_response(res).location, 'http://localhost/test.html')
+ res.location = '/test2.html'
+ eq_(req.get_response(res).location, 'http://localhost/test2.html')
+
+def test_request_uri_http():
+ # covers webob/response.py:1152
+ from webob.response import _request_uri
+ environ = {
+ 'wsgi.url_scheme': 'http',
+ 'SERVER_NAME': 'test.com',
+ 'SERVER_PORT': '80',
+ 'SCRIPT_NAME': '/foobar',
+ }
+ eq_(_request_uri(environ), 'http://test.com/foobar')
+
+def test_request_uri_no_script_name2():
+ # covers webob/response.py:1160
+ # There is a test_request_uri_no_script_name in test_response.py, but it
+ # sets SCRIPT_NAME.
+ from webob.response import _request_uri
+ environ = {
+ 'wsgi.url_scheme': 'http',
+ 'HTTP_HOST': 'test.com',
+ 'PATH_INFO': '/foobar',
+ }
+ eq_(_request_uri(environ), 'http://test.com/foobar')
+
+def test_cache_control_object_max_age_ten():
+ res = Response()
+ res.cache_control.max_age = 10
+ eq_(repr(res.cache_control), "<CacheControl 'max-age=10'>")
+ eq_(res.headers['cache-control'], 'max-age=10')
+
+def test_cache_control_set_object_error():
+ res = Response()
+ assert_raises(AttributeError, setattr, res.cache_control, 'max_stale', 10)
+
+def test_cache_expires_set():
+ res = Response()
+ res.cache_expires = True
+ eq_(repr(res.cache_control),
+ "<CacheControl 'max-age=0, must-revalidate, no-cache, no-store'>")
+
+def test_status_int_set():
+ res = Response()
+ res.status_int = 400
+ eq_(res._status, '400 Bad Request')
+
+def test_cache_control_set_dict():
+ res = Response()
+ res.cache_control = {'a':'b'}
+ eq_(repr(res.cache_control), "<CacheControl 'a=b'>")
+
+def test_cache_control_set_None():
+ res = Response()
+ res.cache_control = None
+ eq_(repr(res.cache_control), "<CacheControl ''>")
+
+def test_cache_control_set_unicode():
+ res = Response()
+ res.cache_control = u'abc'
+ eq_(repr(res.cache_control), "<CacheControl 'abc'>")
+
+def test_cache_control_set_control_obj_is_not_None():
+ class DummyCacheControl(object):
+ def __init__(self):
+ self.header_value = 1
+ self.properties = {'bleh':1}
+ res = Response()
+ res._cache_control_obj = DummyCacheControl()
+ res.cache_control = {}
+ eq_(res.cache_control.properties, {})
+
+def test_cache_control_del():
+ res = Response()
+ del res.cache_control
+ eq_(repr(res.cache_control), "<CacheControl ''>")
+
+def test_body_file_get():
+ res = Response()
+ result = res.body_file
+ from webob.response import ResponseBodyFile
+ eq_(result.__class__, ResponseBodyFile)
+
+def test_body_file_write_no_charset():
+ res = Response
+ assert_raises(TypeError, res.write, u'foo')
+
+def test_body_file_write_unicode_encodes():
+ from webob.response import ResponseBodyFile
+ s = unicode('La Pe\xc3\xb1a', 'utf-8')
+ res = Response()
+ res.write(s)
+ eq_(res.app_iter, ['', 'La Pe\xc3\xb1a'])
+
+def test_repr():
+ res = Response()
+ ok_(repr(res).endswith('200 OK>'))
+
+def test_cache_expires_set_timedelta():
+ res = Response()
+ from datetime import timedelta
+ delta = timedelta(seconds=60)
+ res.cache_expires(seconds=delta)
+ eq_(res.cache_control.max_age, 60)
+
+def test_cache_expires_set_int():
+ res = Response()
+ res.cache_expires(seconds=60)
+ eq_(res.cache_control.max_age, 60)
+
+def test_cache_expires_set_None():
+ res = Response()
+ res.cache_expires(seconds=None, a=1)
+ eq_(res.cache_control.a, 1)
+
+def test_cache_expires_set_zero():
+ res = Response()
+ res.cache_expires(seconds=0)
+ eq_(res.cache_control.no_store, True)
+ eq_(res.cache_control.no_cache, '*')
+ eq_(res.cache_control.must_revalidate, True)
+ eq_(res.cache_control.max_age, 0)
+ eq_(res.cache_control.post_check, 0)
+
+def test_encode_content_unknown():
+ res = Response()
+ assert_raises(AssertionError, res.encode_content, 'badencoding')
+
+def test_encode_content_identity():
+ res = Response()
+ result = res.encode_content('identity')
+ eq_(result, None)
+
+def test_encode_content_gzip_already_gzipped():
+ res = Response()
+ res.content_encoding = 'gzip'
+ result = res.encode_content('gzip')
+ eq_(result, None)
+
+def test_encode_content_gzip_notyet_gzipped():
+ res = Response()
+ res.app_iter = StringIO('foo')
+ result = res.encode_content('gzip')
+ eq_(result, None)
+ eq_(res.content_length, 23)
+ eq_(res.app_iter, ['\x1f\x8b\x08\x00\x00\x00\x00\x00\x02\xff', '',
+ 'K\xcb\xcf\x07\x00', '!es\x8c\x03\x00\x00\x00'])
+
+def test_encode_content_gzip_notyet_gzipped_lazy():
+ res = Response()
+ res.app_iter = StringIO('foo')
+ result = res.encode_content('gzip', lazy=True)
+ eq_(result, None)
+ eq_(res.content_length, None)
+ eq_(list(res.app_iter), ['\x1f\x8b\x08\x00\x00\x00\x00\x00\x02\xff', '',
+ 'K\xcb\xcf\x07\x00', '!es\x8c\x03\x00\x00\x00'])
+
+def test_decode_content_identity():
+ res = Response()
+ res.content_encoding = 'identity'
+ result = res.decode_content()
+ eq_(result, None)
+
+def test_decode_content_weird():
+ res = Response()
+ res.content_encoding = 'weird'
+ assert_raises(ValueError, res.decode_content)
+
+def test_decode_content_gzip():
+ from gzip import GzipFile
+ io = StringIO()
+ gzip_f = GzipFile(filename='', mode='w', fileobj=io)
+ gzip_f.write('abc')
+ gzip_f.close()
+ body = io.getvalue()
+ res = Response()
+ res.content_encoding = 'gzip'
+ res.body = body
+ res.decode_content()
+ eq_(res.body, 'abc')
+
+def test__abs_headerlist_location_with_scheme():
+ res = Response()
+ res.content_encoding = 'gzip'
+ res.headerlist = [('Location', 'http:')]
+ result = res._abs_headerlist({})
+ eq_(result, [('Location', 'http:')])
+
+def test_response_set_body_file():
+ for data in ['abc', 'abcdef'*1024]:
+ file = StringIO(data)
+ r = Response(body_file=file)
+ assert r.body == data
+
diff --git a/lib/webob_1_1_1/tests/test_util.py b/lib/webob_1_1_1/tests/test_util.py
new file mode 100644
index 0000000..b363af6
--- /dev/null
+++ b/lib/webob_1_1_1/tests/test_util.py
@@ -0,0 +1,77 @@
+import unittest
+from webob import Request, Response
+
+class Test_warn_deprecation(unittest.TestCase):
+ def setUp(self):
+ import warnings
+ self.oldwarn = warnings.warn
+ warnings.warn = self._warn
+ self.warnings = []
+
+ def tearDown(self):
+ import warnings
+ warnings.warn = self.oldwarn
+ del self.warnings
+
+ def _callFUT(self, text, version, stacklevel):
+ from webob.util import warn_deprecation
+ return warn_deprecation(text, version, stacklevel)
+
+ def _warn(self, text, type, stacklevel=1):
+ self.warnings.append(locals())
+
+ def test_not_1_2(self):
+ self._callFUT('text', 'version', 1)
+ self.assertEqual(len(self.warnings), 2)
+ unknown_version_warning = self.warnings[0]
+ self.assertEqual(unknown_version_warning['text'],
+ "Unknown warn_deprecation version arg: 'version'")
+ self.assertEqual(unknown_version_warning['type'], RuntimeWarning)
+ self.assertEqual(unknown_version_warning['stacklevel'], 1)
+ deprecation_warning = self.warnings[1]
+ self.assertEqual(deprecation_warning['text'], 'text')
+ self.assertEqual(deprecation_warning['type'], DeprecationWarning)
+ self.assertEqual(deprecation_warning['stacklevel'], 2)
+
+ def test_is_1_2(self):
+ self._callFUT('text', '1.2', 1)
+ self.assertEqual(len(self.warnings), 1)
+ deprecation_warning = self.warnings[0]
+ self.assertEqual(deprecation_warning['text'], 'text')
+ self.assertEqual(deprecation_warning['type'], DeprecationWarning)
+ self.assertEqual(deprecation_warning['stacklevel'], 2)
+
+
+ def test_decode_param_names_arg(self):
+ from webob import Request
+ env = Request.blank('?a=b').environ
+ req = Request(env, decode_param_names=False)
+ self.assertEqual(len(self.warnings), 1)
+ deprecation_warning = self.warnings[0]
+ self.assertEqual(deprecation_warning['type'], DeprecationWarning)
+
+ def test_decode_param_names_attr(self):
+ class BadRequest(Request):
+ decode_param_names = False
+ req = BadRequest.blank('?a=b')
+ self.assertEqual(len(self.warnings), 1)
+ deprecation_warning = self.warnings[0]
+ self.assertEqual(deprecation_warning['type'], DeprecationWarning)
+
+ def test_multidict_update_warning(self):
+ # test warning when duplicate keys are passed
+ r = Response()
+ r.headers.update([
+ ('Set-Cookie', 'a=b'),
+ ('Set-Cookie', 'x=y'),
+ ])
+ self.assertEqual(len(self.warnings), 1)
+ deprecation_warning = self.warnings[0]
+ self.assertEqual(deprecation_warning['type'], UserWarning)
+ assert 'Consider using .extend()' in deprecation_warning['text']
+
+ def test_multidict_update_warning_unnecessary(self):
+ # no warning on normal operation
+ r = Response()
+ r.headers.update([('Set-Cookie', 'a=b')])
+ self.assertEqual(len(self.warnings), 0)
diff --git a/lib/webob_1_1_1/webob/__init__.py b/lib/webob_1_1_1/webob/__init__.py
new file mode 100644
index 0000000..b9cc815
--- /dev/null
+++ b/lib/webob_1_1_1/webob/__init__.py
@@ -0,0 +1,19 @@
+from webob.datetime_utils import *
+from webob.request import *
+from webob.response import *
+from webob.util import html_escape
+
+__all__ = [
+ 'Request', 'Response',
+ 'UTC', 'day', 'week', 'hour', 'minute', 'second', 'month', 'year',
+ 'html_escape'
+]
+
+BaseRequest.ResponseClass = Response
+Response.RequestClass = Request
+
+__version__ = '1.1'
+
+
+
+
diff --git a/lib/webob_1_1_1/webob/acceptparse.py b/lib/webob_1_1_1/webob/acceptparse.py
new file mode 100644
index 0000000..d4d28e5
--- /dev/null
+++ b/lib/webob_1_1_1/webob/acceptparse.py
@@ -0,0 +1,361 @@
+"""
+Parses a variety of ``Accept-*`` headers.
+
+These headers generally take the form of::
+
+ value1; q=0.5, value2; q=0
+
+Where the ``q`` parameter is optional. In theory other parameters
+exists, but this ignores them.
+"""
+
+import re
+from webob.util import header_docstring, warn_deprecation
+from webob.headers import _trans_name as header_to_key
+
+part_re = re.compile(
+ r',\s*([^\s;,\n]+)(?:[^,]*?;\s*q=([0-9.]*))?')
+
+
+
+
+def _warn_first_match():
+ # TODO: remove .first_match in version 1.3
+ warn_deprecation("Use best_match instead", '1.2', 3)
+
+class Accept(object):
+ """
+ Represents a generic ``Accept-*`` style header.
+
+ This object should not be modified. To add items you can use
+ ``accept_obj + 'accept_thing'`` to get a new object
+ """
+
+ def __init__(self, header_value):
+ self.header_value = header_value
+ self._parsed = list(self.parse(header_value))
+ self._parsed_nonzero = [(m,q) for (m,q) in self._parsed if q]
+
+ @staticmethod
+ def parse(value):
+ """
+ Parse ``Accept-*`` style header.
+
+ Return iterator of ``(value, quality)`` pairs.
+ ``quality`` defaults to 1.
+ """
+ for match in part_re.finditer(','+value):
+ name = match.group(1)
+ if name == 'q':
+ continue
+ quality = match.group(2) or ''
+ if quality:
+ try:
+ quality = max(min(float(quality), 1), 0)
+ yield (name, quality)
+ continue
+ except ValueError:
+ pass
+ yield (name, 1)
+
+
+ def __repr__(self):
+ return '<%s(%r)>' % (self.__class__.__name__, str(self))
+
+ def __str__(self):
+ result = []
+ for mask, quality in self._parsed:
+ if quality != 1:
+ mask = '%s;q=%0.1f' % (mask, quality)
+ result.append(mask)
+ return ', '.join(result)
+
+ def __add__(self, other, reversed=False):
+ if isinstance(other, Accept):
+ other = other.header_value
+ if hasattr(other, 'items'):
+ other = sorted(other.items(), key=lambda item: -item[1])
+ if isinstance(other, (list, tuple)):
+ result = []
+ for item in other:
+ if isinstance(item, (list, tuple)):
+ name, quality = item
+ result.append('%s; q=%s' % (name, quality))
+ else:
+ result.append(item)
+ other = ', '.join(result)
+ other = str(other)
+ my_value = self.header_value
+ if reversed:
+ other, my_value = my_value, other
+ if not other:
+ new_value = my_value
+ elif not my_value:
+ new_value = other
+ else:
+ new_value = my_value + ', ' + other
+ return self.__class__(new_value)
+
+ def __radd__(self, other):
+ return self.__add__(other, True)
+
+ def __contains__(self, offer):
+ """
+ Returns true if the given object is listed in the accepted
+ types.
+ """
+ for mask, quality in self._parsed_nonzero:
+ if self._match(mask, offer):
+ return True
+
+ def quality(self, offer, modifier=1):
+ """
+ Return the quality of the given offer. Returns None if there
+ is no match (not 0).
+ """
+ bestq = 0
+ for mask, q in self._parsed:
+ if self._match(mask, offer):
+ bestq = max(bestq, q * modifier)
+ return bestq or None
+
+ def first_match(self, offers):
+ """
+ DEPRECATED
+ Returns the first allowed offered type. Ignores quality.
+ Returns the first offered type if nothing else matches; or if you include None
+ at the end of the match list then that will be returned.
+ """
+ _warn_first_match()
+ if not offers:
+ raise ValueError("You must pass in a non-empty list")
+ for offer in offers:
+ if offer is None:
+ return None
+ for mask, quality in self._parsed_nonzero:
+ if self._match(mask, offer):
+ return offer
+ return offers[0]
+
+ def best_match(self, offers, default_match=None):
+ """
+ Returns the best match in the sequence of offered types.
+
+ The sequence can be a simple sequence, or you can have
+ ``(match, server_quality)`` items in the sequence. If you
+ have these tuples then the client quality is multiplied by the
+ server_quality to get a total. If two matches have equal
+ weight, then the one that shows up first in the `offers` list
+ will be returned.
+
+ But among matches with the same quality the match to a more specific
+ requested type will be chosen. For example a match to text/* trumps */*.
+
+ default_match (default None) is returned if there is no intersection.
+ """
+ best_quality = -1
+ best_offer = default_match
+ matched_by = '*/*'
+ for offer in offers:
+ if isinstance(offer, (tuple, list)):
+ offer, server_quality = offer
+ else:
+ server_quality = 1
+ for mask, quality in self._parsed_nonzero:
+ possible_quality = server_quality * quality
+ if possible_quality < best_quality:
+ continue
+ elif possible_quality == best_quality:
+ # 'text/plain' overrides 'message/*' overrides '*/*'
+ # (if all match w/ the same q=)
+ if matched_by.count('*') <= mask.count('*'):
+ continue
+ if self._match(mask, offer):
+ best_quality = possible_quality
+ best_offer = offer
+ matched_by = mask
+ return best_offer
+
+ def best_matches(self, fallback=None):
+ """
+ Return all the matches in order of quality, with fallback (if
+ given) at the end.
+ """
+ items = [i for i, q in sorted(self._parsed, key=lambda iq: -iq[1])]
+ if fallback:
+ for index, item in enumerate(items):
+ if self._match(item, fallback):
+ items[index:] = [fallback]
+ break
+ else:
+ items.append(fallback)
+ return items
+
+ def _match(self, mask, offer):
+ _check_offer(offer)
+ return mask == '*' or offer.lower() == mask.lower()
+
+
+
+class NilAccept(object):
+
+ """
+ Represents an Accept header with no value.
+ """
+
+ MasterClass = Accept
+
+ def __repr__(self):
+ return '<%s: %s>' % (self.__class__.__name__, self.MasterClass)
+
+ def __str__(self):
+ return ''
+
+ def __nonzero__(self):
+ return False
+
+ def __add__(self, item):
+ if isinstance(item, self.MasterClass):
+ return item
+ else:
+ return self.MasterClass('') + item
+
+ def __radd__(self, item):
+ if isinstance(item, self.MasterClass):
+ return item
+ else:
+ return item + self.MasterClass('')
+
+ def __contains__(self, item):
+ _check_offer(item)
+ return True
+
+ def quality(self, offer, default_quality=1):
+ return 0
+
+ def first_match(self, offers):
+ _warn_first_match()
+ return offers[0]
+
+ def best_match(self, offers, default_match=None):
+ best_quality = -1
+ best_match = default_match
+ for offer in offers:
+ _check_offer(offer)
+ if isinstance(offer, (list, tuple)):
+ offer, quality = offer
+ else:
+ quality = 1
+ if quality > best_quality:
+ best_offer = offer
+ best_quality = quality
+ return best_offer
+
+ def best_matches(self, fallback=None):
+ if fallback:
+ return [fallback]
+ else:
+ return []
+
+class NoAccept(NilAccept):
+ def __contains__(self, item):
+ return False
+
+class AcceptCharset(Accept):
+ @staticmethod
+ def parse(value):
+ latin1_found = False
+ for m, q in Accept.parse(value):
+ if m == '*' or m == 'iso-8859-1':
+ latin1_found = True
+ yield m, q
+ if not latin1_found:
+ yield ('iso-8859-1', 1)
+
+class AcceptLanguage(Accept):
+ def _match(self, mask, item):
+ item = item.replace('_', '-').lower()
+ mask = mask.lower()
+ return (mask == '*'
+ or item == mask
+ or item.split('-')[0] == mask
+ or item == mask.split('-')[0]
+ )
+
+
+class MIMEAccept(Accept):
+ """
+ Represents the ``Accept`` header, which is a list of mimetypes.
+
+ This class knows about mime wildcards, like ``image/*``
+ """
+ @staticmethod
+ def parse(value):
+ for mask, q in Accept.parse(value):
+ try:
+ mask_major, mask_minor = mask.split('/')
+ except ValueError:
+ continue
+ if mask_major == '*' and mask_minor != '*':
+ continue
+ yield (mask, q)
+
+ def accept_html(self):
+ """
+ Returns true if any HTML-like type is accepted
+ """
+ return ('text/html' in self
+ or 'application/xhtml+xml' in self
+ or 'application/xml' in self
+ or 'text/xml' in self)
+
+ accepts_html = property(accept_html) # note the plural
+
+ def _match(self, mask, offer):
+ """
+ Check if the offer is covered by the mask
+ """
+ _check_offer(offer)
+ if '*' not in mask:
+ return offer == mask
+ elif mask == '*/*':
+ return True
+ else:
+ assert mask.endswith('/*')
+ mask_major = mask[:-2]
+ offer_major = offer.split('/', 1)[0]
+ return offer_major == mask_major
+
+
+class MIMENilAccept(NilAccept):
+ MasterClass = MIMEAccept
+
+def _check_offer(offer):
+ if '*' in offer:
+ raise ValueError("The application should offer specific types, got %r" % offer)
+
+
+
+def accept_property(header, rfc_section,
+ AcceptClass=Accept, NilClass=NilAccept
+):
+ key = header_to_key(header)
+ doc = header_docstring(header, rfc_section)
+ #doc += " Converts it as a %s." % convert_name
+ def fget(req):
+ value = req.environ.get(key)
+ if not value:
+ return NilClass()
+ return AcceptClass(value)
+ def fset(req, val):
+ if val:
+ if isinstance(val, (list, tuple, dict)):
+ val = AcceptClass('') + val
+ val = str(val)
+ req.environ[key] = val or None
+ def fdel(req):
+ del req.environ[key]
+ return property(fget, fset, fdel, doc)
+
+
+
diff --git a/lib/webob_1_1_1/webob/byterange.py b/lib/webob_1_1_1/webob/byterange.py
new file mode 100644
index 0000000..efe9685
--- /dev/null
+++ b/lib/webob_1_1_1/webob/byterange.py
@@ -0,0 +1,233 @@
+class Range(object):
+ """
+ Represents the Range header.
+
+ This only represents ``bytes`` ranges, which are the only kind
+ specified in HTTP. This can represent multiple sets of ranges,
+ but no place else is this multi-range facility supported.
+ """
+
+ def __init__(self, ranges): # expect non-inclusive
+ for begin, end in ranges:
+ assert end is None or end >= 0, "Bad ranges: %r" % ranges
+ self.ranges = ranges
+
+ def satisfiable(self, length):
+ """
+ Returns true if this range can be satisfied by the resource
+ with the given byte length.
+ """
+ return self.range_for_length(length) is not None
+
+ def range_for_length(self, length):
+ """
+ *If* there is only one range, and *if* it is satisfiable by
+ the given length, then return a (begin, end) non-inclusive range
+ of bytes to serve. Otherwise return None
+ """
+ if length is None or len(self.ranges) != 1:
+ return None
+ start, end = self.ranges[0]
+ if end is None:
+ end = length
+ if start < 0:
+ start += length
+ if _is_content_range_valid(start, end, length):
+ stop = min(end, length)
+ return (start, stop)
+ else:
+ return None
+
+ def content_range(self, length):
+ """
+ Works like range_for_length; returns None or a ContentRange object
+
+ You can use it like::
+
+ response.content_range = req.range.content_range(response.content_length)
+
+ Though it's still up to you to actually serve that content range!
+ """
+ range = self.range_for_length(length)
+ if range is None:
+ return None
+ return ContentRange(range[0], range[1], length)
+
+ def __str__(self):
+ parts = []
+ for begin, end in self.ranges:
+ if end is None:
+ if begin >= 0:
+ parts.append('%s-' % begin)
+ else:
+ parts.append(str(begin))
+ else:
+ if begin < 0:
+ raise ValueError("(%r, %r) should have a non-negative first value"
+ % (begin, end))
+ if end <= 0:
+ raise ValueError("(%r, %r) should have a positive second value"
+ % (begin, end))
+ parts.append('%s-%s' % (begin, end-1))
+ return 'bytes=%s' % ','.join(parts)
+
+ def __repr__(self):
+ return '<%s ranges=%s>' % (
+ self.__class__.__name__,
+ ', '.join(map(repr, self.ranges)))
+
+ @classmethod
+ def parse(cls, header):
+ """
+ Parse the header; may return None if header is invalid
+ """
+ bytes = cls.parse_bytes(header)
+ if bytes is None:
+ return None
+ units, ranges = bytes
+ if units != 'bytes' or ranges is None:
+ return None
+ return cls(ranges)
+
+ @staticmethod
+ def parse_bytes(header):
+ """
+ Parse a Range header into (bytes, list_of_ranges).
+ ranges in list_of_ranges are non-inclusive (unlike the HTTP header).
+
+ Will return None if the header is invalid
+ """
+ if not header:
+ raise TypeError("The header must not be empty")
+ ranges = []
+ last_end = 0
+ try:
+ (units, range) = header.split("=", 1)
+ units = units.strip().lower()
+ for item in range.split(","):
+ if '-' not in item:
+ raise ValueError()
+ if item.startswith('-'):
+ # This is a range asking for a trailing chunk.
+ if last_end < 0:
+ raise ValueError('too many end ranges')
+ begin = int(item)
+ end = None
+ last_end = -1
+ else:
+ (begin, end) = item.split("-", 1)
+ begin = int(begin)
+ if begin < last_end or last_end < 0:
+ raise ValueError('begin<last_end, or last_end<0')
+ if end.strip():
+ end = int(end) + 1 # return val is non-inclusive
+ if begin >= end:
+ raise ValueError('begin>end')
+ else:
+ end = None
+ last_end = end
+ ranges.append((begin, end))
+ except ValueError, e:
+ # In this case where the Range header is malformed,
+ # section 14.16 says to treat the request as if the
+ # Range header was not present. How do I log this?
+ return None
+ return (units, ranges)
+
+
+class ContentRange(object):
+
+ """
+ Represents the Content-Range header
+
+ This header is ``start-stop/length``, where start-stop and length
+ can be ``*`` (represented as None in the attributes).
+ """
+
+ def __init__(self, start, stop, length):
+ if not _is_content_range_valid(start, stop, length):
+ raise ValueError("Bad start:stop/length: %r-%r/%r" % (start, stop, length))
+ self.start = start
+ self.stop = stop # this is python-style range end (non-inclusive)
+ self.length = length
+
+ def __repr__(self):
+ return '<%s %s>' % (self.__class__.__name__, self)
+
+ def __str__(self):
+ if self.length is None:
+ length = '*'
+ else:
+ length = self.length
+ if self.start is None:
+ assert self.stop is None
+ return 'bytes */%s' % length
+ stop = self.stop - 1 # from non-inclusive to HTTP-style
+ return 'bytes %s-%s/%s' % (self.start, stop, length)
+
+ def __iter__(self):
+ """
+ Mostly so you can unpack this, like:
+
+ start, stop, length = res.content_range
+ """
+ return iter([self.start, self.stop, self.length])
+
+ @classmethod
+ def parse(cls, value):
+ """
+ Parse the header. May return None if it cannot parse.
+ """
+ if value is None:
+ return None
+ value = value.strip()
+ if not value.startswith('bytes '):
+ # Unparseable
+ return None
+ value = value[len('bytes '):].strip()
+ if '/' not in value:
+ # Invalid, no length given
+ return None
+ range, length = value.split('/', 1)
+ if length == '*':
+ length = None
+ elif length.isdigit():
+ length = int(length)
+ else:
+ return None # invalid length
+
+ if range == '*':
+ return cls(None, None, length)
+ elif '-' not in range:
+ # Invalid, no range
+ return None
+ else:
+ start, stop = range.split('-', 1)
+ try:
+ start = int(start)
+ stop = int(stop)
+ stop += 1 # convert to non-inclusive
+ except ValueError:
+ # Parse problem
+ return None
+ if _is_content_range_valid(start, stop, length, response=True):
+ return cls(start, stop, length)
+ return None
+
+
+
+def _is_content_range_valid(start, stop, length, response=False):
+ if (start is None) != (stop is None):
+ return False
+ elif start is None:
+ return length is None or length >= 0
+ elif length is None:
+ return 0 <= start < stop
+ elif start >= stop:
+ return False
+ elif response and stop > length:
+ # "content-range: bytes 0-50/10" is invalid for a response
+ # "range: bytes 0-50" is valid for a request to a 10-bytes entity
+ return False
+ else:
+ return 0 <= start < length
diff --git a/lib/webob_1_1_1/webob/cachecontrol.py b/lib/webob_1_1_1/webob/cachecontrol.py
new file mode 100644
index 0000000..cc84360
--- /dev/null
+++ b/lib/webob_1_1_1/webob/cachecontrol.py
@@ -0,0 +1,227 @@
+"""
+Represents the Cache-Control header
+"""
+import re
+
+class UpdateDict(dict):
+ """
+ Dict that has a callback on all updates
+ """
+ # these are declared as class attributes so that
+ # we don't need to override constructor just to
+ # set some defaults
+ updated = None
+ updated_args = None
+
+ def _updated(self):
+ """
+ Assign to new_dict.updated to track updates
+ """
+ updated = self.updated
+ if updated is not None:
+ args = self.updated_args
+ if args is None:
+ args = (self,)
+ updated(*args)
+
+ def __setitem__(self, key, item):
+ dict.__setitem__(self, key, item)
+ self._updated()
+
+ def __delitem__(self, key):
+ dict.__delitem__(self, key)
+ self._updated()
+
+ def clear(self):
+ dict.clear(self)
+ self._updated()
+
+ def update(self, *args, **kw):
+ dict.update(self, *args, **kw)
+ self._updated()
+
+ def setdefault(self, key, value=None):
+ val = dict.setdefault(self, key, value)
+ if val is value:
+ self._updated()
+ return val
+
+ def pop(self, *args):
+ v = dict.pop(self, *args)
+ self._updated()
+ return v
+
+ def popitem(self):
+ v = dict.popitem(self)
+ self._updated()
+ return v
+
+
+token_re = re.compile(
+ r'([a-zA-Z][a-zA-Z_-]*)\s*(?:=(?:"([^"]*)"|([^ \t",;]*)))?')
+need_quote_re = re.compile(r'[^a-zA-Z0-9._-]')
+
+
+class exists_property(object):
+ """
+ Represents a property that either is listed in the Cache-Control
+ header, or is not listed (has no value)
+ """
+ def __init__(self, prop, type=None):
+ self.prop = prop
+ self.type = type
+
+ def __get__(self, obj, type=None):
+ if obj is None:
+ return self
+ return self.prop in obj.properties
+
+ def __set__(self, obj, value):
+ if (self.type is not None
+ and self.type != obj.type):
+ raise AttributeError(
+ "The property %s only applies to %s Cache-Control" % (self.prop, self.type))
+
+ if value:
+ obj.properties[self.prop] = None
+ else:
+ if self.prop in obj.properties:
+ del obj.properties[self.prop]
+
+ def __delete__(self, obj):
+ self.__set__(obj, False)
+
+
+class value_property(object):
+ """
+ Represents a property that has a value in the Cache-Control header.
+
+ When no value is actually given, the value of self.none is returned.
+ """
+ def __init__(self, prop, default=None, none=None, type=None):
+ self.prop = prop
+ self.default = default
+ self.none = none
+ self.type = type
+
+ def __get__(self, obj, type=None):
+ if obj is None:
+ return self
+ if self.prop in obj.properties:
+ value = obj.properties[self.prop]
+ if value is None:
+ return self.none
+ else:
+ return value
+ else:
+ return self.default
+
+ def __set__(self, obj, value):
+ if (self.type is not None
+ and self.type != obj.type):
+ raise AttributeError(
+ "The property %s only applies to %s Cache-Control" % (self.prop, self.type))
+ if value == self.default:
+ if self.prop in obj.properties:
+ del obj.properties[self.prop]
+ elif value is True:
+ obj.properties[self.prop] = None # Empty value, but present
+ else:
+ obj.properties[self.prop] = value
+
+ def __delete__(self, obj):
+ if self.prop in obj.properties:
+ del obj.properties[self.prop]
+
+
+class CacheControl(object):
+
+ """
+ Represents the Cache-Control header.
+
+ By giving a type of ``'request'`` or ``'response'`` you can
+ control what attributes are allowed (some Cache-Control values
+ only apply to requests or responses).
+ """
+
+ update_dict = UpdateDict
+
+ def __init__(self, properties, type):
+ self.properties = properties
+ self.type = type
+
+ @classmethod
+ def parse(cls, header, updates_to=None, type=None):
+ """
+ Parse the header, returning a CacheControl object.
+
+ The object is bound to the request or response object
+ ``updates_to``, if that is given.
+ """
+ if updates_to:
+ props = cls.update_dict()
+ props.updated = updates_to
+ else:
+ props = {}
+ for match in token_re.finditer(header):
+ name = match.group(1)
+ value = match.group(2) or match.group(3) or None
+ if value:
+ try:
+ value = int(value)
+ except ValueError:
+ pass
+ props[name] = value
+ obj = cls(props, type=type)
+ if updates_to:
+ props.updated_args = (obj,)
+ return obj
+
+ def __repr__(self):
+ return '<CacheControl %r>' % str(self)
+
+ # Request values:
+ # no-cache shared (below)
+ # no-store shared (below)
+ # max-age shared (below)
+ max_stale = value_property('max-stale', none='*', type='request')
+ min_fresh = value_property('min-fresh', type='request')
+ # no-transform shared (below)
+ only_if_cached = exists_property('only-if-cached', type='request')
+
+ # Response values:
+ public = exists_property('public', type='response')
+ private = value_property('private', none='*', type='response')
+ no_cache = value_property('no-cache', none='*')
+ no_store = exists_property('no-store')
+ no_transform = exists_property('no-transform')
+ must_revalidate = exists_property('must-revalidate', type='response')
+ proxy_revalidate = exists_property('proxy-revalidate', type='response')
+ max_age = value_property('max-age', none=-1)
+ s_maxage = value_property('s-maxage', type='response')
+ s_max_age = s_maxage
+
+ def __str__(self):
+ return serialize_cache_control(self.properties)
+
+ def copy(self):
+ """
+ Returns a copy of this object.
+ """
+ return self.__class__(self.properties.copy(), type=self.type)
+
+
+def serialize_cache_control(properties):
+ if isinstance(properties, CacheControl):
+ properties = properties.properties
+ parts = []
+ for name, value in sorted(properties.items()):
+ if value is None:
+ parts.append(name)
+ continue
+ value = str(value)
+ if need_quote_re.search(value):
+ value = '"%s"' % value
+ parts.append('%s=%s' % (name, value))
+ return ', '.join(parts)
+
diff --git a/lib/webob_1_1_1/webob/cookies.py b/lib/webob_1_1_1/webob/cookies.py
new file mode 100644
index 0000000..e4f7582
--- /dev/null
+++ b/lib/webob_1_1_1/webob/cookies.py
@@ -0,0 +1,204 @@
+import re, time, string
+from datetime import datetime, date, timedelta
+
+__all__ = ['Cookie']
+
+class Cookie(dict):
+ def __init__(self, input=None):
+ if input:
+ self.load(input)
+
+ def load(self, data):
+ ckey = None
+ for key, val in _rx_cookie.findall(data):
+ if key.lower() in _c_keys:
+ if ckey:
+ self[ckey][key] = _unquote(val)
+ elif key[0] == '$':
+ # RFC2109: NAMEs that begin with $ are reserved for other uses
+ # and must not be used by applications.
+ continue
+ else:
+ self[key] = _unquote(val)
+ ckey = key
+
+ def __setitem__(self, key, val):
+ if _valid_cookie_name(key):
+ dict.__setitem__(self, key, Morsel(key, val))
+
+ def serialize(self, full=True):
+ return '; '.join(m.serialize(full) for m in self.values())
+
+ def values(self):
+ return [m for _,m in sorted(self.items())]
+
+ __str__ = serialize
+
+ def __repr__(self):
+ return '<%s: [%s]>' % (self.__class__.__name__,
+ ', '.join(map(repr, self.values())))
+
+
+def cookie_property(key, serialize=lambda v: v):
+ def fset(self, v):
+ self[key] = serialize(v)
+ return property(lambda self: self[key], fset)
+
+def serialize_max_age(v):
+ if isinstance(v, timedelta):
+ return str(v.seconds + v.days*24*60*60)
+ elif isinstance(v, int):
+ return str(v)
+ else:
+ return v
+
+def serialize_cookie_date(v):
+ if v is None:
+ return None
+ elif isinstance(v, str):
+ return v
+ elif isinstance(v, int):
+ v = timedelta(seconds=v)
+ if isinstance(v, timedelta):
+ v = datetime.utcnow() + v
+ if isinstance(v, (datetime, date)):
+ v = v.timetuple()
+ r = time.strftime('%%s, %d-%%s-%Y %H:%M:%S GMT', v)
+ return r % (weekdays[v[6]], months[v[1]])
+
+class Morsel(dict):
+ __slots__ = ('name', 'value')
+ def __init__(self, name, value):
+ assert name.lower() not in _c_keys
+ assert _valid_cookie_name(name)
+ assert isinstance(value, str)
+ self.name = name
+ # we can encode the unicode value as UTF-8 here,
+ # but then the decoded cookie would still be str,
+ # so we don't do that
+ self.value = value
+ self.update(dict.fromkeys(_c_keys, None))
+
+ path = cookie_property('path')
+ domain = cookie_property('domain')
+ comment = cookie_property('comment')
+ expires = cookie_property('expires', serialize_cookie_date)
+ max_age = cookie_property('max-age', serialize_max_age)
+ httponly = cookie_property('httponly', bool)
+ secure = cookie_property('secure', bool)
+
+ def __setitem__(self, k, v):
+ k = k.lower()
+ if k in _c_keys:
+ dict.__setitem__(self, k, v)
+
+ def serialize(self, full=True):
+ result = []
+ add = result.append
+ add("%s=%s" % (self.name, _quote(self.value)))
+ if full:
+ for k in _c_valkeys:
+ v = self[k]
+ if v:
+ assert isinstance(v, str), v
+ add("%s=%s" % (_c_renames[k], _quote(v)))
+ expires = self['expires']
+ if expires:
+ add("expires=%s" % expires)
+ if self.secure:
+ add('secure')
+ if self.httponly:
+ add('HttpOnly')
+ return '; '.join(result)
+
+ __str__ = serialize
+
+ def __repr__(self):
+ return '<%s: %s=%s>' % (self.__class__.__name__,
+ self.name, repr(self.value))
+
+def _valid_cookie_name(key):
+ try:
+ key = key.encode('ascii')
+ except UnicodeError:
+ return False
+ return not needs_quoting(key)
+
+_c_renames = {
+ "path" : "Path",
+ "comment" : "Comment",
+ "domain" : "Domain",
+ "max-age" : "Max-Age",
+}
+_c_valkeys = sorted(_c_renames)
+_c_keys = set(_c_renames)
+_c_keys.update(['expires', 'secure', 'httponly'])
+
+
+
+
+#
+# parsing
+#
+
+_re_quoted = r'"(?:\\"|.)*?"' # any doublequoted string
+_legal_special_chars = "~!@#$%^&*()_+=-`.?|:/(){}<>'"
+_re_legal_char = r"[\w\d%s]" % re.escape(_legal_special_chars)
+_re_expires_val = r"\w{3},\s[\w\d-]{9,11}\s[\d:]{8}\sGMT"
+_rx_cookie = re.compile(
+ # key
+ (r"(%s+?)" % _re_legal_char)
+ # =
+ + r"\s*=\s*"
+ # val
+ + r"(%s|%s|%s*)" % (_re_quoted, _re_expires_val, _re_legal_char)
+)
+
+_rx_unquote = re.compile(r'\\([0-3][0-7][0-7]|.)')
+
+def _unquote(v):
+ if v and v[0] == v[-1] == '"':
+ v = v[1:-1]
+ def _ch_unquote(m):
+ v = m.group(1)
+ if v.isdigit():
+ return chr(int(v, 8))
+ return v
+ v = _rx_unquote.sub(_ch_unquote, v)
+ return v
+
+
+
+#
+# serializing
+#
+
+_notrans = ' '*256
+
+# these chars can be in cookie value w/o causing it to be quoted
+_no_escape_special_chars = "!#$%&'*+-.^_`|~/"
+_no_escape_chars = string.ascii_letters + string.digits + \
+ _no_escape_special_chars
+# these chars never need to be quoted
+_escape_noop_chars = _no_escape_chars+': '
+# this is a map used to escape the values
+_escape_map = dict((chr(i), '\\%03o' % i) for i in xrange(256))
+_escape_map.update(zip(_escape_noop_chars, _escape_noop_chars.decode('ascii')))
+_escape_map['"'] = u'\\"'
+_escape_map['\\'] = u'\\\\'
+_escape_char = _escape_map.__getitem__
+
+
+weekdays = ('Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun')
+months = (None, 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep',
+ 'Oct', 'Nov', 'Dec')
+
+
+def needs_quoting(v):
+ return v.translate(_notrans, _no_escape_chars)
+
+def _quote(v):
+ #assert isinstance(v, str)
+ if needs_quoting(v):
+ return '"' + ''.join(map(_escape_char, v)) + '"'
+ return v
diff --git a/lib/webob_1_1_1/webob/datetime_utils.py b/lib/webob_1_1_1/webob/datetime_utils.py
new file mode 100644
index 0000000..8915aa1
--- /dev/null
+++ b/lib/webob_1_1_1/webob/datetime_utils.py
@@ -0,0 +1,99 @@
+import time
+import calendar
+from datetime import datetime, date, timedelta, tzinfo
+from email.utils import parsedate_tz, mktime_tz, formatdate
+
+__all__ = [
+ 'UTC', 'timedelta_to_seconds',
+ 'year', 'month', 'week', 'day', 'hour', 'minute', 'second',
+ 'parse_date', 'serialize_date',
+ 'parse_date_delta', 'serialize_date_delta',
+]
+
+_now = datetime.now # hook point for unit tests
+
+class _UTC(tzinfo):
+ def dst(self, dt):
+ return timedelta(0)
+ def utcoffset(self, dt):
+ return timedelta(0)
+ def tzname(self, dt):
+ return 'UTC'
+ def __repr__(self):
+ return 'UTC'
+
+UTC = _UTC()
+
+
+
+def timedelta_to_seconds(td):
+ """
+ Converts a timedelta instance to seconds.
+ """
+ return td.seconds + (td.days*24*60*60)
+
+day = timedelta(days=1)
+week = timedelta(weeks=1)
+hour = timedelta(hours=1)
+minute = timedelta(minutes=1)
+second = timedelta(seconds=1)
+# Estimate, I know; good enough for expirations
+month = timedelta(days=30)
+year = timedelta(days=365)
+
+
+def parse_date(value):
+ if not value:
+ return None
+ try:
+ value = str(value)
+ except:
+ return None
+ t = parsedate_tz(value)
+ if t is None:
+ # Could not parse
+ return None
+ if t[-1] is None:
+ # No timezone given. None would mean local time, but we'll force UTC
+ t = t[:9] + (0,)
+ t = mktime_tz(t)
+ return datetime.fromtimestamp(t, UTC)
+
+def serialize_date(dt):
+ if isinstance(dt, unicode):
+ dt = dt.encode('ascii')
+ if isinstance(dt, str):
+ return dt
+ if isinstance(dt, timedelta):
+ dt = _now() + dt
+ if isinstance(dt, (datetime, date)):
+ dt = dt.timetuple()
+ if isinstance(dt, (tuple, time.struct_time)):
+ dt = calendar.timegm(dt)
+ if not isinstance(dt, (float, int, long)):
+ raise ValueError(
+ "You must pass in a datetime, date, time tuple, or integer object, not %r" % dt)
+ return formatdate(dt, usegmt=True)
+
+
+
+def parse_date_delta(value):
+ """
+ like parse_date, but also handle delta seconds
+ """
+ if not value:
+ return None
+ try:
+ value = int(value)
+ except ValueError:
+ return parse_date(value)
+ else:
+ return _now() + timedelta(seconds=value)
+
+
+def serialize_date_delta(value):
+ if isinstance(value, (float, int)):
+ return str(int(value))
+ else:
+ return serialize_date(value)
+
diff --git a/lib/webob_1_1_1/webob/dec.py b/lib/webob_1_1_1/webob/dec.py
new file mode 100644
index 0000000..86ef9eb
--- /dev/null
+++ b/lib/webob_1_1_1/webob/dec.py
@@ -0,0 +1,359 @@
+"""
+Decorators to wrap functions to make them WSGI applications.
+
+The main decorator :class:`wsgify` turns a function into a WSGI
+application (while also allowing normal calling of the method with an
+instantiated request).
+"""
+
+import webob
+import webob.exc
+from types import ClassType
+
+__all__ = ['wsgify']
+
+class wsgify(object):
+ """Turns a request-taking, response-returning function into a WSGI
+ app
+
+ You can use this like::
+
+ @wsgify
+ def myfunc(req):
+ return webob.Response('hey there')
+
+ With that ``myfunc`` will be a WSGI application, callable like
+ ``app_iter = myfunc(environ, start_response)``. You can also call
+ it like normal, e.g., ``resp = myfunc(req)``. (You can also wrap
+ methods, like ``def myfunc(self, req)``.)
+
+ If you raise exceptions from :mod:`webob.exc` they will be turned
+ into WSGI responses.
+
+ There are also several parameters you can use to customize the
+ decorator. Most notably, you can use a :class:`webob.Request`
+ subclass, like::
+
+ class MyRequest(webob.Request):
+ @property
+ def is_local(self):
+ return self.remote_addr == '127.0.0.1'
+ @wsgify(RequestClass=MyRequest)
+ def myfunc(req):
+ if req.is_local:
+ return Response('hi!')
+ else:
+ raise webob.exc.HTTPForbidden
+
+ Another customization you can add is to add `args` (positional
+ arguments) or `kwargs` (of course, keyword arguments). While
+ generally not that useful, you can use this to create multiple
+ WSGI apps from one function, like::
+
+ import simplejson
+ def serve_json(req, json_obj):
+ return Response(json.dumps(json_obj),
+ content_type='application/json')
+
+ serve_ob1 = wsgify(serve_json, args=(ob1,))
+ serve_ob2 = wsgify(serve_json, args=(ob2,))
+
+ You can return several things from a function:
+
+ * A :class:`webob.Response` object (or subclass)
+ * *Any* WSGI application
+ * None, and then ``req.response`` will be used (a pre-instantiated
+ Response object)
+ * A string, which will be written to ``req.response`` and then that
+ response will be used.
+ * Raise an exception from :mod:`webob.exc`
+
+ Also see :func:`wsgify.middleware` for a way to make middleware.
+
+ You can also subclass this decorator; the most useful things to do
+ in a subclass would be to change `RequestClass` or override
+ `call_func` (e.g., to add ``req.urlvars`` as keyword arguments to
+ the function).
+ """
+
+ RequestClass = webob.Request
+
+ def __init__(self, func=None, RequestClass=None,
+ args=(), kwargs=None, middleware_wraps=None):
+ self.func = func
+ if (RequestClass is not None
+ and RequestClass is not self.RequestClass):
+ self.RequestClass = RequestClass
+ self.args = tuple(args)
+ if kwargs is None:
+ kwargs = {}
+ self.kwargs = kwargs
+ self.middleware_wraps = middleware_wraps
+
+ def __repr__(self):
+ if self.func is None:
+ args = []
+ else:
+ args = [_func_name(self.func)]
+ if self.RequestClass is not self.__class__.RequestClass:
+ args.append('RequestClass=%r' % self.RequestClass)
+ if self.args:
+ args.append('args=%r' % (self.args,))
+ my_name = self.__class__.__name__
+ if self.middleware_wraps is not None:
+ my_name = '%s.middleware' % my_name
+ else:
+ if self.kwargs:
+ args.append('kwargs=%r' % self.kwargs)
+ r = '%s(%s)' % (my_name, ', '.join(args))
+ if self.middleware_wraps is not None:
+ args = [repr(self.middleware_wraps)]
+ if self.kwargs:
+ args.extend(['%s=%r' % (name, value)
+ for name, value in sorted(self.kwargs.items())])
+ r += '(%s)' % ', '.join(args)
+ return r
+
+ def __get__(self, obj, type=None):
+ # This handles wrapping methods
+ if hasattr(self.func, '__get__'):
+ return self.clone(self.func.__get__(obj, type))
+ else:
+ return self
+
+ def __call__(self, req, *args, **kw):
+ """Call this as a WSGI application or with a request"""
+ func = self.func
+ if func is None:
+ if args or kw:
+ raise TypeError(
+ "Unbound %s can only be called with the function it will wrap"
+ % self.__class__.__name__)
+ func = req
+ return self.clone(func)
+ if isinstance(req, dict):
+ if len(args) != 1 or kw:
+ raise TypeError(
+ "Calling %r as a WSGI app with the wrong signature")
+ environ = req
+ start_response = args[0]
+ req = self.RequestClass(environ)
+ req.response = req.ResponseClass()
+ req.response.request = req
+ try:
+ args = self.args
+ if self.middleware_wraps:
+ args = (self.middleware_wraps,) + args
+ resp = self.call_func(req, *args, **self.kwargs)
+ except webob.exc.HTTPException, resp:
+ pass
+ if resp is None:
+ ## FIXME: I'm not sure what this should be?
+ resp = req.response
+ elif isinstance(resp, basestring):
+ body = resp
+ resp = req.response
+ resp.write(body)
+ if resp is not req.response:
+ resp = req.response.merge_cookies(resp)
+ return resp(environ, start_response)
+ else:
+ if self.middleware_wraps:
+ args = (self.middleware_wraps,) + args
+ return self.func(req, *args, **kw)
+
+ def get(self, url, **kw):
+ """Run a GET request on this application, returning a Response.
+
+ This creates a request object using the given URL, and any
+ other keyword arguments are set on the request object (e.g.,
+ ``last_modified=datetime.now()``).
+
+ ::
+
+ resp = myapp.get('/article?id=10')
+ """
+ kw.setdefault('method', 'GET')
+ req = self.RequestClass.blank(url, **kw)
+ return self(req)
+
+ def post(self, url, POST=None, **kw):
+ """Run a POST request on this application, returning a Response.
+
+ The second argument (`POST`) can be the request body (a
+ string), or a dictionary or list of two-tuples, that give the
+ POST body.
+
+ ::
+
+ resp = myapp.post('/article/new',
+ dict(title='My Day',
+ content='I ate a sandwich'))
+ """
+ kw.setdefault('method', 'POST')
+ req = self.RequestClass.blank(url, POST=POST, **kw)
+ return self(req)
+
+ def request(self, url, **kw):
+ """Run a request on this application, returning a Response.
+
+ This can be used for DELETE, PUT, etc requests. E.g.::
+
+ resp = myapp.request('/article/1', method='PUT', body='New article')
+ """
+ req = self.RequestClass.blank(url, **kw)
+ return self(req)
+
+ def call_func(self, req, *args, **kwargs):
+ """Call the wrapped function; override this in a subclass to
+ change how the function is called."""
+ return self.func(req, *args, **kwargs)
+
+ def clone(self, func=None, **kw):
+ """Creates a copy/clone of this object, but with some
+ parameters rebound
+ """
+ kwargs = {}
+ if func is not None:
+ kwargs['func'] = func
+ if self.RequestClass is not self.__class__.RequestClass:
+ kwargs['RequestClass'] = self.RequestClass
+ if self.args:
+ kwargs['args'] = self.args
+ if self.kwargs:
+ kwargs['kwargs'] = self.kwargs
+ kwargs.update(kw)
+ return self.__class__(**kwargs)
+
+ # To match @decorator:
+ @property
+ def undecorated(self):
+ return self.func
+
+ @classmethod
+ def middleware(cls, middle_func=None, app=None, **kw):
+ """Creates middleware
+
+ Use this like::
+
+ @wsgify.middleware
+ def restrict_ip(app, req, ips):
+ if req.remote_addr not in ips:
+ raise webob.exc.HTTPForbidden('Bad IP: %s' % req.remote_addr)
+ return app
+
+ @wsgify
+ def app(req):
+ return 'hi'
+
+ wrapped = restrict_ip(app, ips=['127.0.0.1'])
+
+ Or if you want to write output-rewriting middleware::
+
+ @wsgify.middleware
+ def all_caps(app, req):
+ resp = req.get_response(app)
+ resp.body = resp.body.upper()
+ return resp
+
+ wrapped = all_caps(app)
+
+ Note that you must call ``req.get_response(app)`` to get a WebOb response
+ object. If you are not modifying the output, you can just return the app.
+
+ As you can see, this method doesn't actually create an application, but
+ creates "middleware" that can be bound to an application, along with
+ "configuration" (that is, any other keyword arguments you pass when
+ binding the application).
+ """
+ if middle_func is None:
+ return _UnboundMiddleware(cls, app, kw)
+ if app is None:
+ return _MiddlewareFactory(cls, middle_func, kw)
+ return cls(middle_func, middleware_wraps=app, kwargs=kw)
+
+class _UnboundMiddleware(object):
+ """A `wsgify.middleware` invocation that has not yet wrapped a
+ middleware function; the intermediate object when you do
+ something like ``@wsgify.middleware(RequestClass=Foo)``
+ """
+
+ def __init__(self, wrapper_class, app, kw):
+ self.wrapper_class = wrapper_class
+ self.app = app
+ self.kw = kw
+ def __repr__(self):
+ if self.app:
+ args = (self.app,)
+ else:
+ args = ()
+ return '%s.middleware(%s)' % (
+ self.wrapper_class.__name__,
+ _format_args(args, self.kw))
+ def __call__(self, func, app=None):
+ if app is None:
+ app = self.app
+ return self.wrapper_class.middleware(func, app=app, **self.kw)
+
+class _MiddlewareFactory(object):
+ """A middleware that has not yet been bound to an application or
+ configured.
+ """
+
+ def __init__(self, wrapper_class, middleware, kw):
+ self.wrapper_class = wrapper_class
+ self.middleware = middleware
+ self.kw = kw
+ def __repr__(self):
+ return '%s.middleware(%s)' % (
+ self.wrapper_class.__name__,
+ _format_args((self.middleware,), self.kw))
+ def __call__(self, app, **config):
+ kw = self.kw.copy()
+ kw.update(config)
+ return self.wrapper_class.middleware(self.middleware, app, **kw)
+
+def _func_name(func):
+ """Returns the string name of a function, or method, as best it can"""
+ if isinstance(func, (type, ClassType)):
+ name = func.__name__
+ if func.__module__ not in ('__main__', '__builtin__'):
+ name = '%s.%s' % (func.__module__, name)
+ return name
+ name = getattr(func, 'func_name', None)
+ if name is None:
+ name = repr(func)
+ else:
+ name_self = getattr(func, 'im_self', None)
+ if name_self is not None:
+ name = '%r.%s' % (name_self, name)
+ else:
+ name_class = getattr(func, 'im_class', None)
+ if name_class is not None:
+ name = '%s.%s' % (name_class.__name__, name)
+ module = getattr(func, 'func_globals', {}).get('__name__')
+ if module and module != '__main__':
+ name = '%s.%s' % (module, name)
+ return name
+
+def _format_args(args=(), kw=None, leading_comma=False, obj=None, names=None, defaults=None):
+ if kw is None:
+ kw = {}
+ all = [repr(arg) for arg in args]
+ if names is not None:
+ assert obj is not None
+ kw = {}
+ if isinstance(names, basestring):
+ names = names.split()
+ for name in names:
+ kw[name] = getattr(obj, name)
+ if defaults is not None:
+ kw = kw.copy()
+ for name, value in defaults.items():
+ if name in kw and value == kw[name]:
+ del kw[name]
+ all.extend(['%s=%r' % (name, value) for name, value in sorted(kw.items())])
+ result = ', '.join(all)
+ if result and leading_comma:
+ result = ', ' + result
+ return result
diff --git a/lib/webob_1_1_1/webob/descriptors.py b/lib/webob_1_1_1/webob/descriptors.py
new file mode 100644
index 0000000..6beee48
--- /dev/null
+++ b/lib/webob_1_1_1/webob/descriptors.py
@@ -0,0 +1,279 @@
+import re
+from datetime import datetime, date
+
+from webob.byterange import Range, ContentRange
+from webob.etag import IfRange, NoIfRange
+from webob.datetime_utils import parse_date, serialize_date
+from webob.util import header_docstring
+
+CHARSET_RE = re.compile(r';\s*charset=([^;]*)', re.I)
+QUOTES_RE = re.compile('"(.*)"')
+SCHEME_RE = re.compile(r'^[a-z]+:', re.I)
+
+
+_not_given = object()
+
+def environ_getter(key, default=_not_given, rfc_section=None):
+ if rfc_section:
+ doc = header_docstring(key, rfc_section)
+ else:
+ doc = "Gets and sets the ``%s`` key in the environment." % key
+ if default is _not_given:
+ def fget(req):
+ return req.environ[key]
+ def fset(req, val):
+ req.environ[key] = val
+ fdel = None
+ else:
+ def fget(req):
+ return req.environ.get(key, default)
+ def fset(req, val):
+ if val is None:
+ if key in req.environ:
+ del req.environ[key]
+ else:
+ req.environ[key] = val
+ def fdel(req):
+ del req.environ[key]
+ return property(fget, fset, fdel, doc=doc)
+
+
+def upath_property(key):
+ def fget(req):
+ return req.environ.get(key, '').decode('UTF8', req.unicode_errors)
+ def fset(req, val):
+ req.environ[key] = val.encode('UTF8', req.unicode_errors)
+ return property(fget, fset, doc='upath_property(%r)' % key)
+
+
+def header_getter(header, rfc_section):
+ doc = header_docstring(header, rfc_section)
+ key = header.lower()
+
+ def fget(r):
+ for k, v in r._headerlist:
+ if k.lower() == key:
+ return v
+
+ def fset(r, value):
+ fdel(r)
+ if value is not None:
+ if isinstance(value, unicode):
+ value = value.encode('ISO-8859-1') # standard encoding for headers
+ r._headerlist.append((header, value))
+
+ def fdel(r):
+ items = r._headerlist
+ for i in range(len(items)-1, -1, -1):
+ if items[i][0].lower() == key:
+ del items[i]
+
+ return property(fget, fset, fdel, doc)
+
+
+
+
+def converter(prop, parse, serialize, convert_name=None):
+ assert isinstance(prop, property)
+ convert_name = convert_name or "``%s`` and ``%s``" % (parse.__name__,
+ serialize.__name__)
+ doc = prop.__doc__ or ''
+ doc += " Converts it using %s." % convert_name
+ hget, hset = prop.fget, prop.fset
+ def fget(r):
+ return parse(hget(r))
+ def fset(r, val):
+ if val is not None:
+ val = serialize(val)
+ hset(r, val)
+ return property(fget, fset, prop.fdel, doc)
+
+
+
+def list_header(header, rfc_section):
+ prop = header_getter(header, rfc_section)
+ return converter(prop, parse_list, serialize_list, 'list')
+
+def parse_list(value):
+ if not value:
+ return None
+ return tuple(filter(None, [v.strip() for v in value.split(',')]))
+
+def serialize_list(value):
+ if isinstance(value, unicode):
+ return str(value)
+ elif isinstance(value, str):
+ return value
+ else:
+ return ', '.join(map(str, value))
+
+
+
+
+def converter_date(prop):
+ return converter(prop, parse_date, serialize_date, 'HTTP date')
+
+def date_header(header, rfc_section):
+ return converter_date(header_getter(header, rfc_section))
+
+
+
+
+
+
+
+class deprecated_property(object):
+ """
+ Wraps a descriptor, with a deprecation warning or error
+ """
+ def __init__(self, attr, message):
+ self.attr = attr
+ self.message = message
+
+ def __get__(self, obj, type=None):
+ if obj is None:
+ return self
+ self.warn()
+
+ def __set__(self, obj, value):
+ self.warn()
+
+ def __delete__(self, obj):
+ self.warn()
+
+ def __repr__(self):
+ return '<Deprecated attribute %s>' % self.attr
+
+ def warn(self):
+ raise DeprecationWarning('The attribute %s is deprecated: %s'
+ % (self.attr, self.message)
+ )
+
+
+
+########################
+## Converter functions
+########################
+
+
+def parse_etag_response(value):
+ """
+ Parse a response ETag. Weak ETags are dropped.
+ See:
+ * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.19
+ * http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.11
+ """
+ if value and not value.startswith('W/'):
+ unquote_match = QUOTES_RE.match(value)
+ if unquote_match is not None:
+ value = unquote_match.group(1)
+ value = value.replace('\\"', '"')
+ return value
+
+def serialize_etag_response(value):
+ return '"%s"' % value.replace('"', '\\"')
+
+def parse_if_range(value):
+ if not value:
+ return NoIfRange
+ else:
+ return IfRange.parse(value)
+
+def serialize_if_range(value):
+ if isinstance(value, (datetime, date)):
+ return serialize_date(value)
+ if not isinstance(value, str):
+ value = str(value)
+ return value or None
+
+def parse_range(value):
+ if not value:
+ return None
+ # Might return None too:
+ return Range.parse(value)
+
+def serialize_range(value):
+ if isinstance(value, (list, tuple)):
+ if len(value) != 2:
+ raise ValueError(
+ "If setting .range to a list or tuple, it must be of length 2 (not %r)"
+ % value)
+ value = Range([value])
+ if value is None:
+ return None
+ value = str(value)
+ return value or None
+
+def parse_int(value):
+ if value is None or value == '':
+ return None
+ return int(value)
+
+def parse_int_safe(value):
+ if value is None or value == '':
+ return None
+ try:
+ return int(value)
+ except ValueError:
+ return None
+
+serialize_int = str
+
+def parse_content_range(value):
+ if not value or not value.strip():
+ return None
+ # May still return None
+ return ContentRange.parse(value)
+
+def serialize_content_range(value):
+ if isinstance(value, (tuple, list)):
+ if len(value) not in (2, 3):
+ raise ValueError(
+ "When setting content_range to a list/tuple, it must "
+ "be length 2 or 3 (not %r)" % value)
+ if len(value) == 2:
+ begin, end = value
+ length = None
+ else:
+ begin, end, length = value
+ value = ContentRange(begin, end, length)
+ value = str(value).strip()
+ if not value:
+ return None
+ return value
+
+
+
+
+_rx_auth_param = re.compile(r'([a-z]+)=(".*?"|[^,]*)(?:\Z|, *)')
+
+def parse_auth_params(params):
+ r = {}
+ for k, v in _rx_auth_param.findall(params):
+ r[k] = v.strip('"')
+ return r
+
+# see http://lists.w3.org/Archives/Public/ietf-http-wg/2009OctDec/0297.html
+known_auth_schemes = ['Basic', 'Digest', 'WSSE', 'HMACDigest', 'GoogleLogin', 'Cookie', 'OpenID']
+known_auth_schemes = dict.fromkeys(known_auth_schemes, None)
+
+def parse_auth(val):
+ if val is not None:
+ authtype, params = val.split(' ', 1)
+ if authtype in known_auth_schemes:
+ if authtype == 'Basic' and '"' not in params:
+ # this is the "Authentication: Basic XXXXX==" case
+ pass
+ else:
+ params = parse_auth_params(params)
+ return authtype, params
+ return val
+
+def serialize_auth(val):
+ if isinstance(val, (tuple, list)):
+ authtype, params = val
+ if isinstance(params, dict):
+ params = ', '.join(map('%s="%s"'.__mod__, params.items()))
+ assert isinstance(params, str)
+ return '%s %s' % (authtype, params)
+ return val
diff --git a/lib/webob_1_1_1/webob/etag.py b/lib/webob_1_1_1/webob/etag.py
new file mode 100644
index 0000000..154d442
--- /dev/null
+++ b/lib/webob_1_1_1/webob/etag.py
@@ -0,0 +1,242 @@
+"""
+Does parsing of ETag-related headers: If-None-Matches, If-Matches
+
+Also If-Range parsing
+"""
+
+from webob.datetime_utils import *
+from webob.util import header_docstring, warn_deprecation
+
+__all__ = ['AnyETag', 'NoETag', 'ETagMatcher', 'IfRange', 'NoIfRange', 'etag_property']
+
+
+def etag_property(key, default, rfc_section):
+ doc = header_docstring(key, rfc_section)
+ doc += " Converts it as a Etag."
+ def fget(req):
+ value = req.environ.get(key)
+ if not value:
+ return default
+ elif value == '*':
+ return AnyETag
+ else:
+ return ETagMatcher.parse(value)
+ def fset(req, val):
+ if val is None:
+ req.environ[key] = None
+ else:
+ req.environ[key] = str(val)
+ def fdel(req):
+ del req.environ[key]
+ return property(fget, fset, fdel, doc=doc)
+
+def _warn_weak_match_deprecated():
+ warn_deprecation("weak_match is deprecated", '1.2', 3)
+
+
+class _AnyETag(object):
+ """
+ Represents an ETag of *, or a missing ETag when matching is 'safe'
+ """
+
+ def __repr__(self):
+ return '<ETag *>'
+
+ def __nonzero__(self):
+ return False
+
+ def __contains__(self, other):
+ return True
+
+ def weak_match(self, other):
+ _warn_weak_match_deprecated()
+ return True
+
+ def __str__(self):
+ return '*'
+
+AnyETag = _AnyETag()
+
+class _NoETag(object):
+ """
+ Represents a missing ETag when matching is unsafe
+ """
+
+ def __repr__(self):
+ return '<No ETag>'
+
+ def __nonzero__(self):
+ return False
+
+ def __contains__(self, other):
+ return False
+
+ def weak_match(self, other):
+ _warn_weak_match_deprecated()
+ return False
+
+ def __str__(self):
+ return ''
+
+NoETag = _NoETag()
+
+class ETagMatcher(object):
+ """
+ Represents an ETag request. Supports containment to see if an
+ ETag matches. You can also use
+ ``etag_matcher.weak_contains(etag)`` to allow weak ETags to match
+ (allowable for conditional GET requests, but not ranges or other
+ methods).
+ """
+
+ def __init__(self, etags, weak_etags=()):
+ self.etags = etags
+ self.weak_etags = weak_etags
+
+ def __contains__(self, other):
+ return other in self.etags or other in self.weak_etags
+
+ def weak_match(self, other):
+ _warn_weak_match_deprecated()
+ if other.lower().startswith('w/'):
+ other = other[2:]
+ return other in self.etags or other in self.weak_etags
+
+ def __repr__(self):
+ return '<ETag %s>' % (
+ ' or '.join(self.etags))
+
+ @classmethod
+ def parse(cls, value):
+ """
+ Parse this from a header value
+ """
+ results = []
+ weak_results = []
+ while value:
+ if value.lower().startswith('w/'):
+ # Next item is weak
+ weak = True
+ value = value[2:]
+ else:
+ weak = False
+ if value.startswith('"'):
+ try:
+ etag, rest = value[1:].split('"', 1)
+ except ValueError:
+ etag = value.strip(' ",')
+ rest = ''
+ else:
+ rest = rest.strip(', ')
+ else:
+ if ',' in value:
+ etag, rest = value.split(',', 1)
+ rest = rest.strip()
+ else:
+ etag = value
+ rest = ''
+ if etag == '*':
+ return AnyETag
+ if etag:
+ if weak:
+ weak_results.append(etag)
+ else:
+ results.append(etag)
+ value = rest
+ return cls(results, weak_results)
+
+ def __str__(self):
+ items = map('"%s"'.__mod__, self.etags)
+ for weak in self.weak_etags:
+ items.append('W/"%s"' % weak)
+ return ', '.join(items)
+
+class IfRange(object):
+ """
+ Parses and represents the If-Range header, which can be
+ an ETag *or* a date
+ """
+ def __init__(self, etag=None, date=None):
+ self.etag = etag
+ self.date = date
+
+ def __repr__(self):
+ if self.etag is None:
+ etag = '*'
+ else:
+ etag = str(self.etag)
+ if self.date is None:
+ date = '*'
+ else:
+ date = serialize_date(self.date)
+ return '<%s etag=%s, date=%s>' % (
+ self.__class__.__name__,
+ etag, date)
+
+ def __str__(self):
+ if self.etag is not None:
+ return str(self.etag)
+ elif self.date:
+ return serialize_date(self.date)
+ else:
+ return ''
+
+ def match(self, etag=None, last_modified=None):
+ """
+ Return True if the If-Range header matches the given etag or last_modified
+ """
+ if self.date is not None:
+ if last_modified is None:
+ # Conditional with nothing to base the condition won't work
+ return False
+ return last_modified <= self.date
+ elif self.etag is not None:
+ if not etag:
+ return False
+ return etag in self.etag
+ return True
+
+ def match_response(self, response):
+ """
+ Return True if this matches the given ``webob.Response`` instance.
+ """
+ return self.match(etag=response.etag, last_modified=response.last_modified)
+
+ @classmethod
+ def parse(cls, value):
+ """
+ Parse this from a header value.
+ """
+ date = etag = None
+ if not value:
+ etag = NoETag()
+ elif value and value.endswith(' GMT'):
+ # Must be a date
+ date = parse_date(value)
+ else:
+ etag = ETagMatcher.parse(value)
+ return cls(etag=etag, date=date)
+
+class _NoIfRange(object):
+ """
+ Represents a missing If-Range header
+ """
+
+ def __repr__(self):
+ return '<Empty If-Range>'
+
+ def __str__(self):
+ return ''
+
+ def __nonzero__(self):
+ return False
+
+ def match(self, etag=None, last_modified=None):
+ return True
+
+ def match_response(self, response):
+ return True
+
+NoIfRange = _NoIfRange()
+
+
diff --git a/lib/webob_1_1_1/webob/exc.py b/lib/webob_1_1_1/webob/exc.py
new file mode 100644
index 0000000..5f078e2
--- /dev/null
+++ b/lib/webob_1_1_1/webob/exc.py
@@ -0,0 +1,1059 @@
+"""
+HTTP Exception
+--------------
+This module processes Python exceptions that relate to HTTP exceptions
+by defining a set of exceptions, all subclasses of HTTPException.
+Each exception, in addition to being a Python exception that can be
+raised and caught, is also a WSGI application and ``webob.Response``
+object.
+
+This module defines exceptions according to RFC 2068 [1]_ : codes with
+100-300 are not really errors; 400's are client errors, and 500's are
+server errors. According to the WSGI specification [2]_ , the application
+can call ``start_response`` more then once only under two conditions:
+(a) the response has not yet been sent, or (b) if the second and
+subsequent invocations of ``start_response`` have a valid ``exc_info``
+argument obtained from ``sys.exc_info()``. The WSGI specification then
+requires the server or gateway to handle the case where content has been
+sent and then an exception was encountered.
+
+Exception
+ HTTPException
+ HTTPOk
+ * 200 - HTTPOk
+ * 201 - HTTPCreated
+ * 202 - HTTPAccepted
+ * 203 - HTTPNonAuthoritativeInformation
+ * 204 - HTTPNoContent
+ * 205 - HTTPResetContent
+ * 206 - HTTPPartialContent
+ HTTPRedirection
+ * 300 - HTTPMultipleChoices
+ * 301 - HTTPMovedPermanently
+ * 302 - HTTPFound
+ * 303 - HTTPSeeOther
+ * 304 - HTTPNotModified
+ * 305 - HTTPUseProxy
+ * 306 - Unused (not implemented, obviously)
+ * 307 - HTTPTemporaryRedirect
+ HTTPError
+ HTTPClientError
+ * 400 - HTTPBadRequest
+ * 401 - HTTPUnauthorized
+ * 402 - HTTPPaymentRequired
+ * 403 - HTTPForbidden
+ * 404 - HTTPNotFound
+ * 405 - HTTPMethodNotAllowed
+ * 406 - HTTPNotAcceptable
+ * 407 - HTTPProxyAuthenticationRequired
+ * 408 - HTTPRequestTimeout
+ * 409 - HTTPConflict
+ * 410 - HTTPGone
+ * 411 - HTTPLengthRequired
+ * 412 - HTTPPreconditionFailed
+ * 413 - HTTPRequestEntityTooLarge
+ * 414 - HTTPRequestURITooLong
+ * 415 - HTTPUnsupportedMediaType
+ * 416 - HTTPRequestRangeNotSatisfiable
+ * 417 - HTTPExpectationFailed
+ HTTPServerError
+ * 500 - HTTPInternalServerError
+ * 501 - HTTPNotImplemented
+ * 502 - HTTPBadGateway
+ * 503 - HTTPServiceUnavailable
+ * 504 - HTTPGatewayTimeout
+ * 505 - HTTPVersionNotSupported
+
+Subclass usage notes:
+---------------------
+
+The HTTPException class is complicated by 4 factors:
+
+ 1. The content given to the exception may either be plain-text or
+ as html-text.
+
+ 2. The template may want to have string-substitutions taken from
+ the current ``environ`` or values from incoming headers. This
+ is especially troublesome due to case sensitivity.
+
+ 3. The final output may either be text/plain or text/html
+ mime-type as requested by the client application.
+
+ 4. Each exception has a default explanation, but those who
+ raise exceptions may want to provide additional detail.
+
+Subclass attributes and call parameters are designed to provide an easier path
+through the complications.
+
+Attributes:
+
+ ``code``
+ the HTTP status code for the exception
+
+ ``title``
+ remainder of the status line (stuff after the code)
+
+ ``explanation``
+ a plain-text explanation of the error message that is
+ not subject to environment or header substitutions;
+ it is accessible in the template via %(explanation)s
+
+ ``detail``
+ a plain-text message customization that is not subject
+ to environment or header substitutions; accessible in
+ the template via %(detail)s
+
+ ``body_template``
+ a content fragment (in HTML) used for environment and
+ header substitution; the default template includes both
+ the explanation and further detail provided in the
+ message
+
+Parameters:
+
+ ``detail``
+ a plain-text override of the default ``detail``
+
+ ``headers``
+ a list of (k,v) header pairs
+
+ ``comment``
+ a plain-text additional information which is
+ usually stripped/hidden for end-users
+
+ ``body_template``
+ a string.Template object containing a content fragment in HTML
+ that frames the explanation and further detail
+
+To override the template (which is HTML content) or the plain-text
+explanation, one must subclass the given exception; or customize it
+after it has been created. This particular breakdown of a message
+into explanation, detail and template allows both the creation of
+plain-text and html messages for various clients as well as
+error-free substitution of environment variables and headers.
+
+
+The subclasses of :class:`~_HTTPMove`
+(:class:`~HTTPMultipleChoices`, :class:`~HTTPMovedPermanently`,
+:class:`~HTTPFound`, :class:`~HTTPSeeOther`, :class:`~HTTPUseProxy` and
+:class:`~HTTPTemporaryRedirect`) are redirections that require a ``Location``
+field. Reflecting this, these subclasses have two additional keyword arguments:
+``location`` and ``add_slash``.
+
+Parameters:
+
+ ``location``
+ to set the location immediately
+
+ ``add_slash``
+ set to True to redirect to the same URL as the request, except with a
+ ``/`` appended
+
+Relative URLs in the location will be resolved to absolute.
+
+References:
+
+.. [1] http://www.python.org/peps/pep-0333.html#error-handling
+.. [2] http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.5
+
+
+"""
+
+import re
+import urlparse
+import sys
+import types
+from string import Template
+from webob import Response, Request, html_escape
+from webob.util import warn_deprecation
+
+tag_re = re.compile(r'<.*?>', re.S)
+br_re = re.compile(r'<br.*?>', re.I|re.S)
+comment_re = re.compile(r'<!--|-->')
+
+def no_escape(value):
+ if value is None:
+ return ''
+ if not isinstance(value, basestring):
+ if hasattr(value, '__unicode__'):
+ value = unicode(value)
+ else:
+ value = str(value)
+ return value
+
+def strip_tags(value):
+ value = value.replace('\n', ' ')
+ value = value.replace('\r', '')
+ value = br_re.sub('\n', value)
+ value = comment_re.sub('', value)
+ value = tag_re.sub('', value)
+ return value
+
+class HTTPException(Exception):
+ def __init__(self, message, wsgi_response):
+ Exception.__init__(self, message)
+ self.wsgi_response = wsgi_response
+
+ def __call__(self, environ, start_response):
+ return self.wsgi_response(environ, start_response)
+
+ # TODO: remove in version 1.3
+ @property
+ def exception(self):
+ warn_deprecation("Raise HTTP exceptions directly", '1.2', 2)
+ return self
+
+class WSGIHTTPException(Response, HTTPException):
+
+ ## You should set in subclasses:
+ # code = 200
+ # title = 'OK'
+ # explanation = 'why this happens'
+ # body_template_obj = Template('response template')
+ code = None
+ title = None
+ explanation = ''
+ body_template_obj = Template('''\
+${explanation}<br /><br />
+${detail}
+${html_comment}
+''')
+
+ plain_template_obj = Template('''\
+${status}
+
+${body}''')
+
+ html_template_obj = Template('''\
+<html>
+ <head>
+ <title>${status}</title>
+ </head>
+ <body>
+ <h1>${status}</h1>
+ ${body}
+ </body>
+</html>''')
+
+ ## Set this to True for responses that should have no request body
+ empty_body = False
+
+ def __init__(self, detail=None, headers=None, comment=None,
+ body_template=None, **kw):
+ Response.__init__(self,
+ status='%s %s' % (self.code, self.title),
+ **kw)
+ Exception.__init__(self, detail)
+ if headers:
+ self.headers.extend(headers)
+ self.detail = detail
+ self.comment = comment
+ if body_template is not None:
+ self.body_template = body_template
+ self.body_template_obj = Template(body_template)
+ if self.empty_body:
+ del self.content_type
+ del self.content_length
+
+ def __str__(self):
+ return self.detail or self.explanation
+
+ def _make_body(self, environ, escape):
+ args = {
+ 'explanation': escape(self.explanation),
+ 'detail': escape(self.detail or ''),
+ 'comment': escape(self.comment or ''),
+ }
+ if self.comment:
+ args['html_comment'] = '<!-- %s -->' % escape(self.comment)
+ else:
+ args['html_comment'] = ''
+ body_tmpl = self.body_template_obj
+ if WSGIHTTPException.body_template_obj is not self.body_template_obj:
+ # Custom template; add headers to args
+ for k, v in environ.items():
+ args[k] = escape(v)
+ for k, v in self.headers.items():
+ args[k.lower()] = escape(v)
+ t_obj = self.body_template_obj
+ return t_obj.substitute(args)
+
+ def plain_body(self, environ):
+ body = self._make_body(environ, no_escape)
+ body = strip_tags(body)
+ return self.plain_template_obj.substitute(status=self.status,
+ title=self.title,
+ body=body)
+
+ def html_body(self, environ):
+ body = self._make_body(environ, html_escape)
+ return self.html_template_obj.substitute(status=self.status,
+ body=body)
+
+ def generate_response(self, environ, start_response):
+ if self.content_length is not None:
+ del self.content_length
+ headerlist = list(self.headerlist)
+ accept = environ.get('HTTP_ACCEPT', '')
+ if accept and 'html' in accept or '*/*' in accept:
+ content_type = 'text/html'
+ body = self.html_body(environ)
+ else:
+ content_type = 'text/plain'
+ body = self.plain_body(environ)
+ extra_kw = {}
+ if isinstance(body, unicode):
+ extra_kw.update(charset='utf-8')
+ resp = Response(body,
+ status=self.status,
+ headerlist=headerlist,
+ content_type=content_type,
+ **extra_kw
+ )
+ resp.content_type = content_type
+ return resp(environ, start_response)
+
+ def __call__(self, environ, start_response):
+ if self.body or self.empty_body:
+ app_iter = Response.__call__(self, environ, start_response)
+ else:
+ app_iter = self.generate_response(environ, start_response)
+ if environ['REQUEST_METHOD'] == 'HEAD':
+ app_iter = []
+ return app_iter
+
+ @property
+ def wsgi_response(self):
+ return self
+
+
+
+class HTTPError(WSGIHTTPException):
+ """
+ base class for status codes in the 400's and 500's
+
+ This is an exception which indicates that an error has occurred,
+ and that any work in progress should not be committed. These are
+ typically results in the 400's and 500's.
+ """
+
+class HTTPRedirection(WSGIHTTPException):
+ """
+ base class for 300's status code (redirections)
+
+ This is an abstract base class for 3xx redirection. It indicates
+ that further action needs to be taken by the user agent in order
+ to fulfill the request. It does not necessarly signal an error
+ condition.
+ """
+
+class HTTPOk(WSGIHTTPException):
+ """
+ Base class for the 200's status code (successful responses)
+
+ code: 200, title: OK
+ """
+ code = 200
+ title = 'OK'
+
+############################################################
+## 2xx success
+############################################################
+
+class HTTPCreated(HTTPOk):
+ """
+ subclass of :class:`~HTTPOk`
+
+ This indicates that request has been fulfilled and resulted in a new
+ resource being created.
+
+ code: 201, title: Created
+ """
+ code = 201
+ title = 'Created'
+
+class HTTPAccepted(HTTPOk):
+ """
+ subclass of :class:`~HTTPOk`
+
+ This indicates that the request has been accepted for processing, but the
+ processing has not been completed.
+
+ code: 202, title: Accepted
+ """
+ code = 202
+ title = 'Accepted'
+ explanation = 'The request is accepted for processing.'
+
+class HTTPNonAuthoritativeInformation(HTTPOk):
+ """
+ subclass of :class:`~HTTPOk`
+
+ This indicates that the returned metainformation in the entity-header is
+ not the definitive set as available from the origin server, but is
+ gathered from a local or a third-party copy.
+
+ code: 203, title: Non-Authoritative Information
+ """
+ code = 203
+ title = 'Non-Authoritative Information'
+
+class HTTPNoContent(HTTPOk):
+ """
+ subclass of :class:`~HTTPOk`
+
+ This indicates that the server has fulfilled the request but does
+ not need to return an entity-body, and might want to return updated
+ metainformation.
+
+ code: 204, title: No Content
+ """
+ code = 204
+ title = 'No Content'
+ empty_body = True
+
+class HTTPResetContent(HTTPOk):
+ """
+ subclass of :class:`~HTTPOk`
+
+ This indicates that the the server has fulfilled the request and
+ the user agent SHOULD reset the document view which caused the
+ request to be sent.
+
+ code: 205, title: Reset Content
+ """
+ code = 205
+ title = 'Reset Content'
+ empty_body = True
+
+class HTTPPartialContent(HTTPOk):
+ """
+ subclass of :class:`~HTTPOk`
+
+ This indicates that the server has fulfilled the partial GET
+ request for the resource.
+
+ code: 206, title: Partial Content
+ """
+ code = 206
+ title = 'Partial Content'
+
+############################################################
+## 3xx redirection
+############################################################
+
+class _HTTPMove(HTTPRedirection):
+ """
+ redirections which require a Location field
+
+ Since a 'Location' header is a required attribute of 301, 302, 303,
+ 305 and 307 (but not 304), this base class provides the mechanics to
+ make this easy.
+
+ You can provide a location keyword argument to set the location
+ immediately. You may also give ``add_slash=True`` if you want to
+ redirect to the same URL as the request, except with a ``/`` added
+ to the end.
+
+ Relative URLs in the location will be resolved to absolute.
+ """
+ explanation = 'The resource has been moved to'
+ body_template_obj = Template('''\
+${explanation} <a href="${location}">${location}</a>;
+you should be redirected automatically.
+${detail}
+${html_comment}''')
+
+ def __init__(self, detail=None, headers=None, comment=None,
+ body_template=None, location=None, add_slash=False):
+ super(_HTTPMove, self).__init__(
+ detail=detail, headers=headers, comment=comment,
+ body_template=body_template)
+ if location is not None:
+ self.location = location
+ if add_slash:
+ raise TypeError(
+ "You can only provide one of the arguments location and add_slash")
+ self.add_slash = add_slash
+
+ def __call__(self, environ, start_response):
+ req = Request(environ)
+ if self.add_slash:
+ url = req.path_url
+ url += '/'
+ if req.environ.get('QUERY_STRING'):
+ url += '?' + req.environ['QUERY_STRING']
+ self.location = url
+ self.location = urlparse.urljoin(req.path_url, self.location)
+ return super(_HTTPMove, self).__call__(
+ environ, start_response)
+
+class HTTPMultipleChoices(_HTTPMove):
+ """
+ subclass of :class:`~_HTTPMove`
+
+ This indicates that the requested resource corresponds to any one
+ of a set of representations, each with its own specific location,
+ and agent-driven negotiation information is being provided so that
+ the user can select a preferred representation and redirect its
+ request to that location.
+
+ code: 300, title: Multiple Choices
+ """
+ code = 300
+ title = 'Multiple Choices'
+
+class HTTPMovedPermanently(_HTTPMove):
+ """
+ subclass of :class:`~_HTTPMove`
+
+ This indicates that the requested resource has been assigned a new
+ permanent URI and any future references to this resource SHOULD use
+ one of the returned URIs.
+
+ code: 301, title: Moved Permanently
+ """
+ code = 301
+ title = 'Moved Permanently'
+
+class HTTPFound(_HTTPMove):
+ """
+ subclass of :class:`~_HTTPMove`
+
+ This indicates that the requested resource resides temporarily under
+ a different URI.
+
+ code: 302, title: Found
+ """
+ code = 302
+ title = 'Found'
+ explanation = 'The resource was found at'
+
+# This one is safe after a POST (the redirected location will be
+# retrieved with GET):
+class HTTPSeeOther(_HTTPMove):
+ """
+ subclass of :class:`~_HTTPMove`
+
+ This indicates that the response to the request can be found under
+ a different URI and SHOULD be retrieved using a GET method on that
+ resource.
+
+ code: 303, title: See Other
+ """
+ code = 303
+ title = 'See Other'
+
+class HTTPNotModified(HTTPRedirection):
+ """
+ subclass of :class:`~HTTPRedirection`
+
+ This indicates that if the client has performed a conditional GET
+ request and access is allowed, but the document has not been
+ modified, the server SHOULD respond with this status code.
+
+ code: 304, title: Not Modified
+ """
+ # TODO: this should include a date or etag header
+ code = 304
+ title = 'Not Modified'
+ empty_body = True
+
+class HTTPUseProxy(_HTTPMove):
+ """
+ subclass of :class:`~_HTTPMove`
+
+ This indicates that the requested resource MUST be accessed through
+ the proxy given by the Location field.
+
+ code: 305, title: Use Proxy
+ """
+ # Not a move, but looks a little like one
+ code = 305
+ title = 'Use Proxy'
+ explanation = (
+ 'The resource must be accessed through a proxy located at')
+
+class HTTPTemporaryRedirect(_HTTPMove):
+ """
+ subclass of :class:`~_HTTPMove`
+
+ This indicates that the requested resource resides temporarily
+ under a different URI.
+
+ code: 307, title: Temporary Redirect
+ """
+ code = 307
+ title = 'Temporary Redirect'
+
+############################################################
+## 4xx client error
+############################################################
+
+class HTTPClientError(HTTPError):
+ """
+ base class for the 400's, where the client is in error
+
+ This is an error condition in which the client is presumed to be
+ in-error. This is an expected problem, and thus is not considered
+ a bug. A server-side traceback is not warranted. Unless specialized,
+ this is a '400 Bad Request'
+ """
+ code = 400
+ title = 'Bad Request'
+ explanation = ('The server could not comply with the request since\r\n'
+ 'it is either malformed or otherwise incorrect.\r\n')
+
+class HTTPBadRequest(HTTPClientError):
+ pass
+
+class HTTPUnauthorized(HTTPClientError):
+ """
+ subclass of :class:`~HTTPClientError`
+
+ This indicates that the request requires user authentication.
+
+ code: 401, title: Unauthorized
+ """
+ code = 401
+ title = 'Unauthorized'
+ explanation = (
+ 'This server could not verify that you are authorized to\r\n'
+ 'access the document you requested. Either you supplied the\r\n'
+ 'wrong credentials (e.g., bad password), or your browser\r\n'
+ 'does not understand how to supply the credentials required.\r\n')
+
+class HTTPPaymentRequired(HTTPClientError):
+ """
+ subclass of :class:`~HTTPClientError`
+
+ code: 402, title: Payment Required
+ """
+ code = 402
+ title = 'Payment Required'
+ explanation = ('Access was denied for financial reasons.')
+
+class HTTPForbidden(HTTPClientError):
+ """
+ subclass of :class:`~HTTPClientError`
+
+ This indicates that the server understood the request, but is
+ refusing to fulfill it.
+
+ code: 403, title: Forbidden
+ """
+ code = 403
+ title = 'Forbidden'
+ explanation = ('Access was denied to this resource.')
+
+class HTTPNotFound(HTTPClientError):
+ """
+ subclass of :class:`~HTTPClientError`
+
+ This indicates that the server did not find anything matching the
+ Request-URI.
+
+ code: 404, title: Not Found
+ """
+ code = 404
+ title = 'Not Found'
+ explanation = ('The resource could not be found.')
+
+class HTTPMethodNotAllowed(HTTPClientError):
+ """
+ subclass of :class:`~HTTPClientError`
+
+ This indicates that the method specified in the Request-Line is
+ not allowed for the resource identified by the Request-URI.
+
+ code: 405, title: Method Not Allowed
+ """
+ code = 405
+ title = 'Method Not Allowed'
+ # override template since we need an environment variable
+ body_template_obj = Template('''\
+The method ${REQUEST_METHOD} is not allowed for this resource. <br /><br />
+${detail}''')
+
+class HTTPNotAcceptable(HTTPClientError):
+ """
+ subclass of :class:`~HTTPClientError`
+
+ This indicates the resource identified by the request is only
+ capable of generating response entities which have content
+ characteristics not acceptable according to the accept headers
+ sent in the request.
+
+ code: 406, title: Not Acceptable
+ """
+ code = 406
+ title = 'Not Acceptable'
+ # override template since we need an environment variable
+ template = Template('''\
+The resource could not be generated that was acceptable to your browser
+(content of type ${HTTP_ACCEPT}. <br /><br />
+${detail}''')
+
+class HTTPProxyAuthenticationRequired(HTTPClientError):
+ """
+ subclass of :class:`~HTTPClientError`
+
+ This is similar to 401, but indicates that the client must first
+ authenticate itself with the proxy.
+
+ code: 407, title: Proxy Authentication Required
+ """
+ code = 407
+ title = 'Proxy Authentication Required'
+ explanation = ('Authentication with a local proxy is needed.')
+
+class HTTPRequestTimeout(HTTPClientError):
+ """
+ subclass of :class:`~HTTPClientError`
+
+ This indicates that the client did not produce a request within
+ the time that the server was prepared to wait.
+
+ code: 408, title: Request Timeout
+ """
+ code = 408
+ title = 'Request Timeout'
+ explanation = ('The server has waited too long for the request to '
+ 'be sent by the client.')
+
+class HTTPConflict(HTTPClientError):
+ """
+ subclass of :class:`~HTTPClientError`
+
+ This indicates that the request could not be completed due to a
+ conflict with the current state of the resource.
+
+ code: 409, title: Conflict
+ """
+ code = 409
+ title = 'Conflict'
+ explanation = ('There was a conflict when trying to complete '
+ 'your request.')
+
+class HTTPGone(HTTPClientError):
+ """
+ subclass of :class:`~HTTPClientError`
+
+ This indicates that the requested resource is no longer available
+ at the server and no forwarding address is known.
+
+ code: 410, title: Gone
+ """
+ code = 410
+ title = 'Gone'
+ explanation = ('This resource is no longer available. No forwarding '
+ 'address is given.')
+
+class HTTPLengthRequired(HTTPClientError):
+ """
+ subclass of :class:`~HTTPClientError`
+
+ This indicates that the the server refuses to accept the request
+ without a defined Content-Length.
+
+ code: 411, title: Length Required
+ """
+ code = 411
+ title = 'Length Required'
+ explanation = ('Content-Length header required.')
+
+class HTTPPreconditionFailed(HTTPClientError):
+ """
+ subclass of :class:`~HTTPClientError`
+
+ This indicates that the precondition given in one or more of the
+ request-header fields evaluated to false when it was tested on the
+ server.
+
+ code: 412, title: Precondition Failed
+ """
+ code = 412
+ title = 'Precondition Failed'
+ explanation = ('Request precondition failed.')
+
+class HTTPRequestEntityTooLarge(HTTPClientError):
+ """
+ subclass of :class:`~HTTPClientError`
+
+ This indicates that the server is refusing to process a request
+ because the request entity is larger than the server is willing or
+ able to process.
+
+ code: 413, title: Request Entity Too Large
+ """
+ code = 413
+ title = 'Request Entity Too Large'
+ explanation = ('The body of your request was too large for this server.')
+
+class HTTPRequestURITooLong(HTTPClientError):
+ """
+ subclass of :class:`~HTTPClientError`
+
+ This indicates that the server is refusing to service the request
+ because the Request-URI is longer than the server is willing to
+ interpret.
+
+ code: 414, title: Request-URI Too Long
+ """
+ code = 414
+ title = 'Request-URI Too Long'
+ explanation = ('The request URI was too long for this server.')
+
+class HTTPUnsupportedMediaType(HTTPClientError):
+ """
+ subclass of :class:`~HTTPClientError`
+
+ This indicates that the server is refusing to service the request
+ because the entity of the request is in a format not supported by
+ the requested resource for the requested method.
+
+ code: 415, title: Unsupported Media Type
+ """
+ code = 415
+ title = 'Unsupported Media Type'
+ # override template since we need an environment variable
+ template_obj = Template('''\
+The request media type ${CONTENT_TYPE} is not supported by this server.
+<br /><br />
+${detail}''')
+
+class HTTPRequestRangeNotSatisfiable(HTTPClientError):
+ """
+ subclass of :class:`~HTTPClientError`
+
+ The server SHOULD return a response with this status code if a
+ request included a Range request-header field, and none of the
+ range-specifier values in this field overlap the current extent
+ of the selected resource, and the request did not include an
+ If-Range request-header field.
+
+ code: 416, title: Request Range Not Satisfiable
+ """
+ code = 416
+ title = 'Request Range Not Satisfiable'
+ explanation = ('The Range requested is not available.')
+
+class HTTPExpectationFailed(HTTPClientError):
+ """
+ subclass of :class:`~HTTPClientError`
+
+ This indidcates that the expectation given in an Expect
+ request-header field could not be met by this server.
+
+ code: 417, title: Expectation Failed
+ """
+ code = 417
+ title = 'Expectation Failed'
+ explanation = ('Expectation failed.')
+
+class HTTPUnprocessableEntity(HTTPClientError):
+ """
+ subclass of :class:`~HTTPClientError`
+
+ This indicates that the server is unable to process the contained
+ instructions. Only for WebDAV.
+
+ code: 422, title: Unprocessable Entity
+ """
+ ## Note: from WebDAV
+ code = 422
+ title = 'Unprocessable Entity'
+ explanation = 'Unable to process the contained instructions'
+
+class HTTPLocked(HTTPClientError):
+ """
+ subclass of :class:`~HTTPClientError`
+
+ This indicates that the resource is locked. Only for WebDAV
+
+ code: 423, title: Locked
+ """
+ ## Note: from WebDAV
+ code = 423
+ title = 'Locked'
+ explanation = ('The resource is locked')
+
+class HTTPFailedDependency(HTTPClientError):
+ """
+ subclass of :class:`~HTTPClientError`
+
+ This indicates that the method could not be performed because the
+ requested action depended on another action and that action failed.
+ Only for WebDAV.
+
+ code: 424, title: Failed Dependency
+ """
+ ## Note: from WebDAV
+ code = 424
+ title = 'Failed Dependency'
+ explanation = ('The method could not be performed because the requested '
+ 'action dependended on another action and that action failed')
+
+############################################################
+## 5xx Server Error
+############################################################
+# Response status codes beginning with the digit "5" indicate cases in
+# which the server is aware that it has erred or is incapable of
+# performing the request. Except when responding to a HEAD request, the
+# server SHOULD include an entity containing an explanation of the error
+# situation, and whether it is a temporary or permanent condition. User
+# agents SHOULD display any included entity to the user. These response
+# codes are applicable to any request method.
+
+class HTTPServerError(HTTPError):
+ """
+ base class for the 500's, where the server is in-error
+
+ This is an error condition in which the server is presumed to be
+ in-error. This is usually unexpected, and thus requires a traceback;
+ ideally, opening a support ticket for the customer. Unless specialized,
+ this is a '500 Internal Server Error'
+ """
+ code = 500
+ title = 'Internal Server Error'
+ explanation = (
+ 'The server has either erred or is incapable of performing\r\n'
+ 'the requested operation.\r\n')
+
+class HTTPInternalServerError(HTTPServerError):
+ pass
+
+class HTTPNotImplemented(HTTPServerError):
+ """
+ subclass of :class:`~HTTPServerError`
+
+ This indicates that the server does not support the functionality
+ required to fulfill the request.
+
+ code: 501, title: Not Implemented
+ """
+ code = 501
+ title = 'Not Implemented'
+ template = Template('''
+The request method ${REQUEST_METHOD} is not implemented for this server. <br /><br />
+${detail}''')
+
+class HTTPBadGateway(HTTPServerError):
+ """
+ subclass of :class:`~HTTPServerError`
+
+ This indicates that the server, while acting as a gateway or proxy,
+ received an invalid response from the upstream server it accessed
+ in attempting to fulfill the request.
+
+ code: 502, title: Bad Gateway
+ """
+ code = 502
+ title = 'Bad Gateway'
+ explanation = ('Bad gateway.')
+
+class HTTPServiceUnavailable(HTTPServerError):
+ """
+ subclass of :class:`~HTTPServerError`
+
+ This indicates that the server is currently unable to handle the
+ request due to a temporary overloading or maintenance of the server.
+
+ code: 503, title: Service Unavailable
+ """
+ code = 503
+ title = 'Service Unavailable'
+ explanation = ('The server is currently unavailable. '
+ 'Please try again at a later time.')
+
+class HTTPGatewayTimeout(HTTPServerError):
+ """
+ subclass of :class:`~HTTPServerError`
+
+ This indicates that the server, while acting as a gateway or proxy,
+ did not receive a timely response from the upstream server specified
+ by the URI (e.g. HTTP, FTP, LDAP) or some other auxiliary server
+ (e.g. DNS) it needed to access in attempting to complete the request.
+
+ code: 504, title: Gateway Timeout
+ """
+ code = 504
+ title = 'Gateway Timeout'
+ explanation = ('The gateway has timed out.')
+
+class HTTPVersionNotSupported(HTTPServerError):
+ """
+ subclass of :class:`~HTTPServerError`
+
+ This indicates that the server does not support, or refuses to
+ support, the HTTP protocol version that was used in the request
+ message.
+
+ code: 505, title: HTTP Version Not Supported
+ """
+ code = 505
+ title = 'HTTP Version Not Supported'
+ explanation = ('The HTTP version is not supported.')
+
+class HTTPInsufficientStorage(HTTPServerError):
+ """
+ subclass of :class:`~HTTPServerError`
+
+ This indicates that the server does not have enough space to save
+ the resource.
+
+ code: 507, title: Insufficient Storage
+ """
+ code = 507
+ title = 'Insufficient Storage'
+ explanation = ('There was not enough space to save the resource')
+
+class HTTPExceptionMiddleware(object):
+ """
+ Middleware that catches exceptions in the sub-application. This
+ does not catch exceptions in the app_iter; only during the initial
+ calling of the application.
+
+ This should be put *very close* to applications that might raise
+ these exceptions. This should not be applied globally; letting
+ *expected* exceptions raise through the WSGI stack is dangerous.
+ """
+
+ def __init__(self, application):
+ self.application = application
+ def __call__(self, environ, start_response):
+ try:
+ return self.application(environ, start_response)
+ except HTTPException, exc:
+ parent_exc_info = sys.exc_info()
+ def repl_start_response(status, headers, exc_info=None):
+ if exc_info is None:
+ exc_info = parent_exc_info
+ return start_response(status, headers, exc_info)
+ return exc(environ, repl_start_response)
+
+try:
+ from paste import httpexceptions
+except ImportError: # pragma: no cover
+ # Without Paste we don't need to do this fixup
+ pass
+else: # pragma: no cover
+ for name in dir(httpexceptions):
+ obj = globals().get(name)
+ if (obj and isinstance(obj, type) and issubclass(obj, HTTPException)
+ and obj is not HTTPException
+ and obj is not WSGIHTTPException):
+ obj.__bases__ = obj.__bases__ + (getattr(httpexceptions, name),)
+ del name, obj, httpexceptions
+
+__all__ = ['HTTPExceptionMiddleware', 'status_map']
+status_map={}
+for name, value in globals().items():
+ if (isinstance(value, (type, types.ClassType)) and issubclass(value, HTTPException)
+ and not name.startswith('_')):
+ __all__.append(name)
+ if getattr(value, 'code', None):
+ status_map[value.code]=value
+ if hasattr(value, 'explanation'):
+ value.explanation = ' '.join(value.explanation.strip().split())
+del name, value
diff --git a/lib/webob_1_1_1/webob/headers.py b/lib/webob_1_1_1/webob/headers.py
new file mode 100644
index 0000000..32658f0
--- /dev/null
+++ b/lib/webob_1_1_1/webob/headers.py
@@ -0,0 +1,147 @@
+from webob.multidict import MultiDict
+from UserDict import DictMixin
+
+__all__ = ['ResponseHeaders', 'EnvironHeaders']
+
+class ResponseHeaders(MultiDict):
+ """
+ Dictionary view on the response headerlist.
+ Keys are normalized for case and whitespace.
+ """
+ def __getitem__(self, key):
+ key = key.lower()
+ for k, v in reversed(self._items):
+ if k.lower() == key:
+ return v
+ raise KeyError(key)
+
+ def getall(self, key):
+ key = key.lower()
+ result = []
+ for k, v in self._items:
+ if k.lower() == key:
+ result.append(v)
+ return result
+
+ def mixed(self):
+ r = self.dict_of_lists()
+ for key, val in r.iteritems():
+ if len(val) == 1:
+ r[key] = val[0]
+ return r
+
+ def dict_of_lists(self):
+ r = {}
+ for key, val in self.iteritems():
+ r.setdefault(key.lower(), []).append(val)
+ return r
+
+ def __setitem__(self, key, value):
+ norm_key = key.lower()
+ items = self._items
+ for i in range(len(items)-1, -1, -1):
+ if items[i][0].lower() == norm_key:
+ del items[i]
+ self._items.append((key, value))
+
+ def __delitem__(self, key):
+ key = key.lower()
+ items = self._items
+ found = False
+ for i in range(len(items)-1, -1, -1):
+ if items[i][0].lower() == key:
+ del items[i]
+ found = True
+ if not found:
+ raise KeyError(key)
+
+ def __contains__(self, key):
+ key = key.lower()
+ for k, v in self._items:
+ if k.lower() == key:
+ return True
+ return False
+
+ has_key = __contains__
+
+ def setdefault(self, key, default=None):
+ c_key = key.lower()
+ for k, v in self._items:
+ if k.lower() == c_key:
+ return v
+ self._items.append((key, default))
+ return default
+
+ def pop(self, key, *args):
+ if len(args) > 1:
+ raise TypeError, "pop expected at most 2 arguments, got "\
+ + repr(1 + len(args))
+ key = key.lower()
+ for i in range(len(self._items)):
+ if self._items[i][0].lower() == key:
+ v = self._items[i][1]
+ del self._items[i]
+ return v
+ if args:
+ return args[0]
+ else:
+ raise KeyError(key)
+
+
+
+
+
+
+key2header = {
+ 'CONTENT_TYPE': 'Content-Type',
+ 'CONTENT_LENGTH': 'Content-Length',
+ 'HTTP_CONTENT_TYPE': 'Content_Type',
+ 'HTTP_CONTENT_LENGTH': 'Content_Length',
+}
+
+header2key = dict([(v.upper(),k) for (k,v) in key2header.items()])
+
+def _trans_key(key):
+ if not isinstance(key, basestring):
+ return None
+ elif key in key2header:
+ return key2header[key]
+ elif key.startswith('HTTP_'):
+ return key[5:].replace('_', '-').title()
+ else:
+ return None
+
+def _trans_name(name):
+ name = name.upper()
+ if name in header2key:
+ return header2key[name]
+ return 'HTTP_'+name.replace('-', '_')
+
+class EnvironHeaders(DictMixin):
+ """An object that represents the headers as present in a
+ WSGI environment.
+
+ This object is a wrapper (with no internal state) for a WSGI
+ request object, representing the CGI-style HTTP_* keys as a
+ dictionary. Because a CGI environment can only hold one value for
+ each key, this dictionary is single-valued (unlike outgoing
+ headers).
+ """
+
+ def __init__(self, environ):
+ self.environ = environ
+
+ def __getitem__(self, hname):
+ return self.environ[_trans_name(hname)]
+
+ def __setitem__(self, hname, value):
+ self.environ[_trans_name(hname)] = value
+
+ def __delitem__(self, hname):
+ del self.environ[_trans_name(hname)]
+
+ def keys(self):
+ return filter(None, map(_trans_key, self.environ))
+
+ def __contains__(self, hname):
+ return _trans_name(hname) in self.environ
diff --git a/lib/webob_1_1_1/webob/multidict.py b/lib/webob_1_1_1/webob/multidict.py
new file mode 100644
index 0000000..f803ee5
--- /dev/null
+++ b/lib/webob_1_1_1/webob/multidict.py
@@ -0,0 +1,633 @@
+# (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org)
+# Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php
+"""
+Gives a multi-value dictionary object (MultiDict) plus several wrappers
+"""
+import cgi, copy, sys, warnings, urllib
+from UserDict import DictMixin
+
+
+__all__ = ['MultiDict', 'UnicodeMultiDict', 'NestedMultiDict', 'NoVars',
+ 'TrackableMultiDict']
+
+class MultiDict(DictMixin):
+ """
+ An ordered dictionary that can have multiple values for each key.
+ Adds the methods getall, getone, mixed and extend and add to the normal
+ dictionary interface.
+ """
+
+ def __init__(self, *args, **kw):
+ if len(args) > 1:
+ raise TypeError("MultiDict can only be called with one positional argument")
+ if args:
+ if hasattr(args[0], 'iteritems'):
+ items = list(args[0].iteritems())
+ elif hasattr(args[0], 'items'):
+ items = args[0].items()
+ else:
+ items = list(args[0])
+ self._items = items
+ else:
+ self._items = []
+ if kw:
+ self._items.extend(kw.iteritems())
+
+ @classmethod
+ def view_list(cls, lst):
+ """
+ Create a dict that is a view on the given list
+ """
+ if not isinstance(lst, list):
+ raise TypeError(
+ "%s.view_list(obj) takes only actual list objects, not %r"
+ % (cls.__name__, lst))
+ obj = cls()
+ obj._items = lst
+ return obj
+
+ @classmethod
+ def from_fieldstorage(cls, fs):
+ """
+ Create a dict from a cgi.FieldStorage instance
+ """
+ obj = cls()
+ # fs.list can be None when there's nothing to parse
+ for field in fs.list or ():
+ if field.filename:
+ obj.add(field.name, field)
+ else:
+ obj.add(field.name, field.value)
+ return obj
+
+ def __getitem__(self, key):
+ for k, v in reversed(self._items):
+ if k == key:
+ return v
+ raise KeyError(key)
+
+ def __setitem__(self, key, value):
+ try:
+ del self[key]
+ except KeyError:
+ pass
+ self._items.append((key, value))
+
+ def add(self, key, value):
+ """
+ Add the key and value, not overwriting any previous value.
+ """
+ self._items.append((key, value))
+
+ def getall(self, key):
+ """
+ Return a list of all values matching the key (may be an empty list)
+ """
+ result = []
+ for k, v in self._items:
+ if key == k:
+ result.append(v)
+ return result
+
+ def getone(self, key):
+ """
+ Get one value matching the key, raising a KeyError if multiple
+ values were found.
+ """
+ v = self.getall(key)
+ if not v:
+ raise KeyError('Key not found: %r' % key)
+ if len(v) > 1:
+ raise KeyError('Multiple values match %r: %r' % (key, v))
+ return v[0]
+
+ def mixed(self):
+ """
+ Returns a dictionary where the values are either single
+ values, or a list of values when a key/value appears more than
+ once in this dictionary. This is similar to the kind of
+ dictionary often used to represent the variables in a web
+ request.
+ """
+ result = {}
+ multi = {}
+ for key, value in self.iteritems():
+ if key in result:
+ # We do this to not clobber any lists that are
+ # *actual* values in this dictionary:
+ if key in multi:
+ result[key].append(value)
+ else:
+ result[key] = [result[key], value]
+ multi[key] = None
+ else:
+ result[key] = value
+ return result
+
+ def dict_of_lists(self):
+ """
+ Returns a dictionary where each key is associated with a list of values.
+ """
+ r = {}
+ for key, val in self.iteritems():
+ r.setdefault(key, []).append(val)
+ return r
+
+ def __delitem__(self, key):
+ items = self._items
+ found = False
+ for i in range(len(items)-1, -1, -1):
+ if items[i][0] == key:
+ del items[i]
+ found = True
+ if not found:
+ raise KeyError(key)
+
+ def __contains__(self, key):
+ for k, v in self._items:
+ if k == key:
+ return True
+ return False
+
+ has_key = __contains__
+
+ def clear(self):
+ self._items = []
+
+ def copy(self):
+ return self.__class__(self)
+
+ def setdefault(self, key, default=None):
+ for k, v in self._items:
+ if key == k:
+ return v
+ self._items.append((key, default))
+ return default
+
+ def pop(self, key, *args):
+ if len(args) > 1:
+ raise TypeError, "pop expected at most 2 arguments, got "\
+ + repr(1 + len(args))
+ for i in range(len(self._items)):
+ if self._items[i][0] == key:
+ v = self._items[i][1]
+ del self._items[i]
+ return v
+ if args:
+ return args[0]
+ else:
+ raise KeyError(key)
+
+ def popitem(self):
+ return self._items.pop()
+
+ def update(self, *args, **kw):
+ if args:
+ lst = args[0]
+ if len(lst) != len(dict(lst)):
+ # this does not catch the cases where we overwrite existing
+ # keys, but those would produce too many warning
+ msg = ("Behavior of MultiDict.update() has changed "
+ "and overwrites duplicate keys. Consider using .extend()"
+ )
+ warnings.warn(msg, UserWarning, stacklevel=2)
+ DictMixin.update(self, *args, **kw)
+
+ def extend(self, other=None, **kwargs):
+ if other is None:
+ pass
+ elif hasattr(other, 'items'):
+ self._items.extend(other.items())
+ elif hasattr(other, 'keys'):
+ for k in other.keys():
+ self._items.append((k, other[k]))
+ else:
+ for k, v in other:
+ self._items.append((k, v))
+ if kwargs:
+ self.update(kwargs)
+
+ def __repr__(self):
+ items = map('(%r, %r)'.__mod__, _hide_passwd(self.iteritems()))
+ return '%s([%s])' % (self.__class__.__name__, ', '.join(items))
+
+ def __len__(self):
+ return len(self._items)
+
+ ##
+ ## All the iteration:
+ ##
+
+ def keys(self):
+ return [k for k, v in self._items]
+
+ def iterkeys(self):
+ for k, v in self._items:
+ yield k
+
+ __iter__ = iterkeys
+
+ def items(self):
+ return self._items[:]
+
+ def iteritems(self):
+ return iter(self._items)
+
+ def values(self):
+ return [v for k, v in self._items]
+
+ def itervalues(self):
+ for k, v in self._items:
+ yield v
+
+class UnicodeMultiDict(DictMixin):
+ """
+ A MultiDict wrapper that decodes returned values to unicode on the
+ fly. Decoding is not applied to assigned values.
+
+ The key/value contents are assumed to be ``str``/``strs`` or
+ ``str``/``FieldStorages`` (as is returned by the ``paste.request.parse_``
+ functions).
+
+ Can optionally also decode keys when the ``decode_keys`` argument is
+ True.
+
+ ``FieldStorage`` instances are cloned, and the clone's ``filename``
+ variable is decoded. Its ``name`` variable is decoded when ``decode_keys``
+ is enabled.
+
+ """
+ def __init__(self, multi, encoding=None, errors='strict',
+ decode_keys=False):
+ self.multi = multi
+ if encoding is None:
+ encoding = sys.getdefaultencoding()
+ self.encoding = encoding
+ self.errors = errors
+ self.decode_keys = decode_keys
+
+ def _decode_key(self, key):
+ if self.decode_keys:
+ try:
+ key = key.decode(self.encoding, self.errors)
+ except AttributeError:
+ pass
+ return key
+
+ def _encode_key(self, key):
+ if self.decode_keys and isinstance(key, unicode):
+ return key.encode(self.encoding, self.errors)
+ return key
+
+ def _decode_value(self, value):
+ """
+ Decode the specified value to unicode. Assumes value is a ``str`` or
+ `FieldStorage`` object.
+
+ ``FieldStorage`` objects are specially handled.
+ """
+ if isinstance(value, cgi.FieldStorage):
+ # decode FieldStorage's field name and filename
+ value = copy.copy(value)
+ if self.decode_keys:
+ if not isinstance(value.name, unicode):
+ value.name = value.name.decode(self.encoding, self.errors)
+ if value.filename:
+ if not isinstance(value.filename, unicode):
+ value.filename = value.filename.decode(self.encoding,
+ self.errors)
+ elif not isinstance(value, unicode):
+ try:
+ value = value.decode(self.encoding, self.errors)
+ except AttributeError:
+ pass
+ return value
+
+ def _encode_value(self, value):
+ if isinstance(value, unicode):
+ value = value.encode(self.encoding, self.errors)
+ return value
+
+ def __getitem__(self, key):
+ return self._decode_value(self.multi.__getitem__(self._encode_key(key)))
+
+ def __setitem__(self, key, value):
+ self.multi.__setitem__(self._encode_key(key), self._encode_value(value))
+
+ def add(self, key, value):
+ """
+ Add the key and value, not overwriting any previous value.
+ """
+ self.multi.add(self._encode_key(key), self._encode_value(value))
+
+ def getall(self, key):
+ """
+ Return a list of all values matching the key (may be an empty list)
+ """
+ return map(self._decode_value, self.multi.getall(self._encode_key(key)))
+
+ def getone(self, key):
+ """
+ Get one value matching the key, raising a KeyError if multiple
+ values were found.
+ """
+ return self._decode_value(self.multi.getone(self._encode_key(key)))
+
+ def mixed(self):
+ """
+ Returns a dictionary where the values are either single
+ values, or a list of values when a key/value appears more than
+ once in this dictionary. This is similar to the kind of
+ dictionary often used to represent the variables in a web
+ request.
+ """
+ unicode_mixed = {}
+ for key, value in self.multi.mixed().iteritems():
+ if isinstance(value, list):
+ value = [self._decode_value(value) for value in value]
+ else:
+ value = self._decode_value(value)
+ unicode_mixed[self._decode_key(key)] = value
+ return unicode_mixed
+
+ def dict_of_lists(self):
+ """
+ Returns a dictionary where each key is associated with a
+ list of values.
+ """
+ unicode_dict = {}
+ for key, value in self.multi.dict_of_lists().iteritems():
+ value = [self._decode_value(value) for value in value]
+ unicode_dict[self._decode_key(key)] = value
+ return unicode_dict
+
+ def __delitem__(self, key):
+ self.multi.__delitem__(self._encode_key(key))
+
+ def __contains__(self, key):
+ return self.multi.__contains__(self._encode_key(key))
+
+ has_key = __contains__
+
+ def clear(self):
+ self.multi.clear()
+
+ def copy(self):
+ return UnicodeMultiDict(self.multi.copy(), self.encoding, self.errors)
+
+ def setdefault(self, key, default=None):
+ return self._decode_value(
+ self.multi.setdefault(self._encode_key(key),
+ self._encode_value(default)))
+
+ def pop(self, key, *args):
+ return self._decode_value(self.multi.pop(self._encode_key(key), *args))
+
+ def popitem(self):
+ k, v = self.multi.popitem()
+ return (self._decode_key(k), self._decode_value(v))
+
+ def __repr__(self):
+ items = map('(%r, %r)'.__mod__, _hide_passwd(self.iteritems()))
+ return '%s([%s])' % (self.__class__.__name__, ', '.join(items))
+
+ def __len__(self):
+ return self.multi.__len__()
+
+ ##
+ ## All the iteration:
+ ##
+
+ def keys(self):
+ return [self._decode_key(k) for k in self.multi.iterkeys()]
+
+ def iterkeys(self):
+ for k in self.multi.iterkeys():
+ yield self._decode_key(k)
+
+ __iter__ = iterkeys
+
+ def items(self):
+ return [(self._decode_key(k), self._decode_value(v))
+ for k, v in self.multi.iteritems()]
+
+ def iteritems(self):
+ for k, v in self.multi.iteritems():
+ yield (self._decode_key(k), self._decode_value(v))
+
+ def values(self):
+ return [self._decode_value(v) for v in self.multi.itervalues()]
+
+ def itervalues(self):
+ for v in self.multi.itervalues():
+ yield self._decode_value(v)
+
+_dummy = object()
+
+class TrackableMultiDict(MultiDict):
+ tracker = None
+ name = None
+ def __init__(self, *args, **kw):
+ if '__tracker' in kw:
+ self.tracker = kw.pop('__tracker')
+ if '__name' in kw:
+ self.name = kw.pop('__name')
+ MultiDict.__init__(self, *args, **kw)
+ def __setitem__(self, key, value):
+ MultiDict.__setitem__(self, key, value)
+ self.tracker(self, key, value)
+ def add(self, key, value):
+ MultiDict.add(self, key, value)
+ self.tracker(self, key, value)
+ def __delitem__(self, key):
+ MultiDict.__delitem__(self, key)
+ self.tracker(self, key)
+ def clear(self):
+ MultiDict.clear(self)
+ self.tracker(self)
+ def setdefault(self, key, default=None):
+ result = MultiDict.setdefault(self, key, default)
+ self.tracker(self, key, result)
+ return result
+ def pop(self, key, *args):
+ result = MultiDict.pop(self, key, *args)
+ self.tracker(self, key)
+ return result
+ def popitem(self):
+ result = MultiDict.popitem(self)
+ self.tracker(self)
+ return result
+ def update(self, *args, **kwargs):
+ MultiDict.update(self, *args, **kwargs)
+ self.tracker(self)
+ def __repr__(self):
+ items = map('(%r, %r)'.__mod__, _hide_passwd(self.iteritems()))
+ return '%s([%s])' % (self.name or self.__class__.__name__, ', '.join(items))
+ def copy(self):
+ # Copies shouldn't be tracked
+ return MultiDict(self)
+
+class NestedMultiDict(MultiDict):
+ """
+ Wraps several MultiDict objects, treating it as one large MultiDict
+ """
+
+ def __init__(self, *dicts):
+ self.dicts = dicts
+
+ def __getitem__(self, key):
+ for d in self.dicts:
+ value = d.get(key, _dummy)
+ if value is not _dummy:
+ return value
+ raise KeyError(key)
+
+ def _readonly(self, *args, **kw):
+ raise KeyError("NestedMultiDict objects are read-only")
+ __setitem__ = _readonly
+ add = _readonly
+ __delitem__ = _readonly
+ clear = _readonly
+ setdefault = _readonly
+ pop = _readonly
+ popitem = _readonly
+ update = _readonly
+
+ def getall(self, key):
+ result = []
+ for d in self.dicts:
+ result.extend(d.getall(key))
+ return result
+
+ # Inherited:
+ # getone
+ # mixed
+ # dict_of_lists
+
+ def copy(self):
+ return MultiDict(self)
+
+ def __contains__(self, key):
+ for d in self.dicts:
+ if key in d:
+ return True
+ return False
+
+ has_key = __contains__
+
+ def __len__(self):
+ v = 0
+ for d in self.dicts:
+ v += len(d)
+ return v
+
+ def __nonzero__(self):
+ for d in self.dicts:
+ if d:
+ return True
+ return False
+
+ def items(self):
+ return list(self.iteritems())
+
+ def iteritems(self):
+ for d in self.dicts:
+ for item in d.iteritems():
+ yield item
+
+ def values(self):
+ return list(self.itervalues())
+
+ def itervalues(self):
+ for d in self.dicts:
+ for value in d.itervalues():
+ yield value
+
+ def keys(self):
+ return list(self.iterkeys())
+
+ def __iter__(self):
+ for d in self.dicts:
+ for key in d:
+ yield key
+
+ iterkeys = __iter__
+
+class NoVars(object):
+ """
+ Represents no variables; used when no variables
+ are applicable.
+
+ This is read-only
+ """
+
+ def __init__(self, reason=None):
+ self.reason = reason or 'N/A'
+
+ def __getitem__(self, key):
+ raise KeyError("No key %r: %s" % (key, self.reason))
+
+ def __setitem__(self, *args, **kw):
+ raise KeyError("Cannot add variables: %s" % self.reason)
+
+ add = __setitem__
+ setdefault = __setitem__
+ update = __setitem__
+
+ def __delitem__(self, *args, **kw):
+ raise KeyError("No keys to delete: %s" % self.reason)
+ clear = __delitem__
+ pop = __delitem__
+ popitem = __delitem__
+
+ def get(self, key, default=None):
+ return default
+
+ def getall(self, key):
+ return []
+
+ def getone(self, key):
+ return self[key]
+
+ def mixed(self):
+ return {}
+ dict_of_lists = mixed
+
+ def __contains__(self, key):
+ return False
+ has_key = __contains__
+
+ def copy(self):
+ return self
+
+ def __repr__(self):
+ return '<%s: %s>' % (self.__class__.__name__,
+ self.reason)
+
+ def __len__(self):
+ return 0
+
+ def __cmp__(self, other):
+ return cmp({}, other)
+
+ def keys(self):
+ return []
+ def iterkeys(self):
+ return iter([])
+ __iter__ = iterkeys
+ items = keys
+ iteritems = iterkeys
+ values = keys
+ itervalues = iterkeys
+
+
+
+def _hide_passwd(items):
+ for k, v in items:
+ if ('password' in k
+ or 'passwd' in k
+ or 'pwd' in k
+ ):
+ yield k, '******'
+ else:
+ yield k, v
diff --git a/lib/webob_1_1_1/webob/request.py b/lib/webob_1_1_1/webob/request.py
new file mode 100644
index 0000000..6e761f6
--- /dev/null
+++ b/lib/webob_1_1_1/webob/request.py
@@ -0,0 +1,1462 @@
+import sys, os, tempfile
+import urllib, urlparse, cgi
+if sys.version >= '2.7':
+ from io import BytesIO as StringIO # pragma nocover
+else:
+ from cStringIO import StringIO # pragma nocover
+
+from webob.headers import EnvironHeaders
+from webob.acceptparse import accept_property, Accept, MIMEAccept, AcceptCharset, NilAccept, MIMENilAccept, NoAccept, AcceptLanguage
+from webob.multidict import TrackableMultiDict, MultiDict, UnicodeMultiDict, NestedMultiDict, NoVars
+from webob.cachecontrol import CacheControl, serialize_cache_control
+from webob.etag import etag_property, AnyETag, NoETag
+
+from webob.descriptors import *
+from webob.datetime_utils import *
+from webob.cookies import Cookie
+from webob.util import warn_deprecation
+
+__all__ = ['BaseRequest', 'Request']
+
+if sys.version >= '2.6':
+ parse_qsl = urlparse.parse_qsl
+else:
+ parse_qsl = cgi.parse_qsl # pragma nocover
+
+class _NoDefault:
+ def __repr__(self):
+ return '(No Default)'
+NoDefault = _NoDefault()
+
+PATH_SAFE = '/:@&+$,'
+
+http_method_probably_has_body = dict.fromkeys(('GET', 'HEAD', 'DELETE', 'TRACE'), False)
+http_method_probably_has_body.update(dict.fromkeys(('POST', 'PUT'), True))
+
+class BaseRequest(object):
+ ## Options:
+ unicode_errors = 'strict'
+ decode_param_names = True # TODO: deprecate
+ ## The limit after which request bodies should be stored on disk
+ ## if they are read in (under this, and the request body is stored
+ ## in memory):
+ request_body_tempfile_limit = 10*1024
+
+ def __init__(self, environ,
+ charset=NoDefault, unicode_errors=NoDefault, decode_param_names=NoDefault,
+ **kw
+ ):
+ if type(environ) is not dict:
+ raise TypeError("WSGI environ must be a dict")
+ d = self.__dict__
+ d['environ'] = environ
+ if charset is not NoDefault:
+ self.charset = charset
+ if unicode_errors is not NoDefault:
+ d['unicode_errors'] = unicode_errors
+ if decode_param_names is not NoDefault:
+ warn_decode_deprecation()
+ d['decode_param_names'] = decode_param_names
+ elif not self.decode_param_names:
+ warn_decode_deprecation()
+ if kw:
+ cls = self.__class__
+ if 'method' in kw:
+ # set method first, because .body setters
+ # depend on it for checks
+ self.method = kw.pop('method')
+ for name, value in kw.iteritems():
+ if not hasattr(cls, name):
+ raise TypeError(
+ "Unexpected keyword: %s=%r" % (name, value))
+ setattr(self, name, value)
+
+ # this is necessary for correct warnings depth for both
+ # BaseRequest and Request (due to AdhocAttrMixin.__setattr__)
+ _setattr_stacklevel = 2
+
+ def _body_file__get(self):
+ """
+ Input stream of the request (wsgi.input).
+ Setting this property resets the content_length and seekable flag
+ (unlike setting req.body_file_raw).
+ """
+ if not self.is_body_readable:
+ return StringIO('')
+ r = self.body_file_raw
+ clen = self.content_length
+ if not self.is_body_seekable and clen is not None:
+ # we need to wrap input in LimitedLengthFile
+ # but we have to cache the instance as well
+ # otherwise this would stop working
+ # (.remaining counter would reset between calls):
+ # req.body_file.read(100)
+ # req.body_file.read(100)
+ env = self.environ
+ wrapped, raw = env.get('webob._body_file', (0,0))
+ if raw is not r:
+ wrapped = LimitedLengthFile(r, clen)
+ env['webob._body_file'] = wrapped, r
+ r = wrapped
+ return r
+
+ def _body_file__set(self, value):
+ if isinstance(value, str):
+ warn_deprecation(
+ "Please use req.body = 'str' or req.body_file = fileobj",
+ '1.2',
+ self._setattr_stacklevel
+ )
+ self.body = value
+ return
+ self.content_length = None
+ self.body_file_raw = value
+ self.is_body_seekable = False
+ self.is_body_readable = True
+ def _body_file__del(self):
+ self.body = ''
+ body_file = property(_body_file__get,
+ _body_file__set,
+ _body_file__del,
+ doc=_body_file__get.__doc__)
+ body_file_raw = environ_getter('wsgi.input')
+ @property
+ def body_file_seekable(self):
+ """
+ Get the body of the request (wsgi.input) as a seekable file-like
+ object. Middleware and routing applications should use this
+ attribute over .body_file.
+
+ If you access this value, CONTENT_LENGTH will also be updated.
+ """
+ if not self.is_body_seekable:
+ self.make_body_seekable()
+ return self.body_file_raw
+
+ scheme = environ_getter('wsgi.url_scheme')
+ method = environ_getter('REQUEST_METHOD', 'GET')
+ http_version = environ_getter('SERVER_PROTOCOL')
+ script_name = environ_getter('SCRIPT_NAME', '')
+ path_info = environ_getter('PATH_INFO')
+ content_length = converter(
+ environ_getter('CONTENT_LENGTH', None, '14.13'),
+ parse_int_safe, serialize_int, 'int')
+ remote_user = environ_getter('REMOTE_USER', None)
+ remote_addr = environ_getter('REMOTE_ADDR', None)
+ query_string = environ_getter('QUERY_STRING', '')
+ server_name = environ_getter('SERVER_NAME')
+ server_port = converter(
+ environ_getter('SERVER_PORT'),
+ parse_int, serialize_int, 'int')
+
+ uscript_name = upath_property('SCRIPT_NAME')
+ upath_info = upath_property('PATH_INFO')
+
+
+ def _content_type__get(self):
+ """Return the content type, but leaving off any parameters (like
+ charset, but also things like the type in ``application/atom+xml;
+ type=entry``)
+
+ If you set this property, you can include parameters, or if
+ you don't include any parameters in the value then existing
+ parameters will be preserved.
+ """
+ return self.environ.get('CONTENT_TYPE', '').split(';', 1)[0]
+ def _content_type__set(self, value):
+ if value is None:
+ del self.content_type
+ return
+ value = str(value)
+ if ';' not in value:
+ content_type = self.environ.get('CONTENT_TYPE', '')
+ if ';' in content_type:
+ value += ';' + content_type.split(';', 1)[1]
+ self.environ['CONTENT_TYPE'] = value
+ def _content_type__del(self):
+ if 'CONTENT_TYPE' in self.environ:
+ del self.environ['CONTENT_TYPE']
+
+ content_type = property(_content_type__get,
+ _content_type__set,
+ _content_type__del,
+ _content_type__get.__doc__)
+
+ _charset_cache = (None, None)
+
+ def _charset__get(self):
+ """Get the charset of the request.
+
+ If the request was sent with a charset parameter on the
+ Content-Type, that will be used. Otherwise if there is a
+ default charset (set during construction, or as a class
+ attribute) that will be returned. Otherwise None.
+
+ Setting this property after request instantiation will always
+ update Content-Type. Deleting the property updates the
+ Content-Type to remove any charset parameter (if none exists,
+ then deleting the property will do nothing, and there will be
+ no error).
+ """
+ content_type = self.environ.get('CONTENT_TYPE', '')
+ cached_ctype, cached_charset = self._charset_cache
+ if cached_ctype == content_type:
+ return cached_charset
+ charset_match = CHARSET_RE.search(content_type)
+ if charset_match:
+ result = charset_match.group(1).strip('"').strip()
+ else:
+ result = 'UTF-8'
+ self._charset_cache = (content_type, result)
+ return result
+ def _charset__set(self, charset):
+ if charset is None or charset == '':
+ del self.charset
+ return
+ charset = str(charset)
+ content_type = self.environ.get('CONTENT_TYPE', '')
+ charset_match = CHARSET_RE.search(self.environ.get('CONTENT_TYPE', ''))
+ if charset_match:
+ content_type = (content_type[:charset_match.start(1)] +
+ charset + content_type[charset_match.end(1):])
+ # comma to separate params? there's nothing like that in RFCs AFAICT
+ #elif ';' in content_type:
+ # content_type += ', charset="%s"' % charset
+ else:
+ content_type += '; charset="%s"' % charset
+ self.environ['CONTENT_TYPE'] = content_type
+ def _charset__del(self):
+ new_content_type = CHARSET_RE.sub('', self.environ.get('CONTENT_TYPE', ''))
+ new_content_type = new_content_type.rstrip().rstrip(';').rstrip(',')
+ self.environ['CONTENT_TYPE'] = new_content_type
+
+ charset = property(_charset__get, _charset__set, _charset__del,
+ _charset__get.__doc__)
+
+ _headers = None
+
+ def _headers__get(self):
+ """
+ All the request headers as a case-insensitive dictionary-like
+ object.
+ """
+ if self._headers is None:
+ self._headers = EnvironHeaders(self.environ)
+ return self._headers
+
+ def _headers__set(self, value):
+ self.headers.clear()
+ self.headers.update(value)
+
+ headers = property(_headers__get, _headers__set, doc=_headers__get.__doc__)
+
+ @property
+ def host_url(self):
+ """
+ The URL through the host (no path)
+ """
+ e = self.environ
+ url = e['wsgi.url_scheme'] + '://'
+ if e.get('HTTP_HOST'):
+ host = e['HTTP_HOST']
+ if ':' in host:
+ host, port = host.split(':', 1)
+ else:
+
+ port = None
+ else:
+ host = e['SERVER_NAME']
+ port = e['SERVER_PORT']
+ if self.environ['wsgi.url_scheme'] == 'https':
+ if port == '443':
+ port = None
+ elif self.environ['wsgi.url_scheme'] == 'http':
+ if port == '80':
+ port = None
+ url += host
+ if port:
+ url += ':%s' % port
+ return url
+
+ @property
+ def application_url(self):
+ """
+ The URL including SCRIPT_NAME (no PATH_INFO or query string)
+ """
+ return self.host_url + urllib.quote(
+ self.environ.get('SCRIPT_NAME', ''), PATH_SAFE)
+
+ @property
+ def path_url(self):
+ """
+ The URL including SCRIPT_NAME and PATH_INFO, but not QUERY_STRING
+ """
+ return self.application_url + urllib.quote(
+ self.environ.get('PATH_INFO', ''), PATH_SAFE)
+
+ @property
+ def path(self):
+ """
+ The path of the request, without host or query string
+ """
+ return (urllib.quote(self.script_name, PATH_SAFE) +
+ urllib.quote(self.path_info, PATH_SAFE))
+
+ @property
+ def path_qs(self):
+ """
+ The path of the request, without host but with query string
+ """
+ path = self.path
+ qs = self.environ.get('QUERY_STRING')
+ if qs:
+ path += '?' + qs
+ return path
+
+ @property
+ def url(self):
+ """
+ The full request URL, including QUERY_STRING
+ """
+ url = self.path_url
+ if self.environ.get('QUERY_STRING'):
+ url += '?' + self.environ['QUERY_STRING']
+ return url
+
+
+ def relative_url(self, other_url, to_application=False):
+ """
+ Resolve other_url relative to the request URL.
+
+ If ``to_application`` is True, then resolve it relative to the
+ URL with only SCRIPT_NAME
+ """
+ if to_application:
+ url = self.application_url
+ if not url.endswith('/'):
+ url += '/'
+ else:
+ url = self.path_url
+ return urlparse.urljoin(url, other_url)
+
+ def path_info_pop(self, pattern=None):
+ """
+ 'Pops' off the next segment of PATH_INFO, pushing it onto
+ SCRIPT_NAME, and returning the popped segment. Returns None if
+ there is nothing left on PATH_INFO.
+
+ Does not return ``''`` when there's an empty segment (like
+ ``/path//path``); these segments are just ignored.
+
+ Optional ``pattern`` argument is a regexp to match the return value
+ before returning. If there is no match, no changes are made to the
+ request and None is returned.
+ """
+ path = self.path_info
+ if not path:
+ return None
+ slashes = ''
+ while path.startswith('/'):
+ slashes += '/'
+ path = path[1:]
+ idx = path.find('/')
+ if idx == -1:
+ idx = len(path)
+ r = path[:idx]
+ if pattern is None or re.match(pattern, r):
+ self.script_name += slashes + r
+ self.path_info = path[idx:]
+ return r
+
+ def path_info_peek(self):
+ """
+ Returns the next segment on PATH_INFO, or None if there is no
+ next segment. Doesn't modify the environment.
+ """
+ path = self.path_info
+ if not path:
+ return None
+ path = path.lstrip('/')
+ return path.split('/', 1)[0]
+
+ def _urlvars__get(self):
+ """
+ Return any *named* variables matched in the URL.
+
+ Takes values from ``environ['wsgiorg.routing_args']``.
+ Systems like ``routes`` set this value.
+ """
+ if 'paste.urlvars' in self.environ:
+ return self.environ['paste.urlvars']
+ elif 'wsgiorg.routing_args' in self.environ:
+ return self.environ['wsgiorg.routing_args'][1]
+ else:
+ result = {}
+ self.environ['wsgiorg.routing_args'] = ((), result)
+ return result
+
+ def _urlvars__set(self, value):
+ environ = self.environ
+ if 'wsgiorg.routing_args' in environ:
+ environ['wsgiorg.routing_args'] = (
+ environ['wsgiorg.routing_args'][0], value)
+ if 'paste.urlvars' in environ:
+ del environ['paste.urlvars']
+ elif 'paste.urlvars' in environ:
+ environ['paste.urlvars'] = value
+ else:
+ environ['wsgiorg.routing_args'] = ((), value)
+
+ def _urlvars__del(self):
+ if 'paste.urlvars' in self.environ:
+ del self.environ['paste.urlvars']
+ if 'wsgiorg.routing_args' in self.environ:
+ if not self.environ['wsgiorg.routing_args'][0]:
+ del self.environ['wsgiorg.routing_args']
+ else:
+ self.environ['wsgiorg.routing_args'] = (
+ self.environ['wsgiorg.routing_args'][0], {})
+
+ urlvars = property(_urlvars__get,
+ _urlvars__set,
+ _urlvars__del,
+ doc=_urlvars__get.__doc__)
+
+ def _urlargs__get(self):
+ """
+ Return any *positional* variables matched in the URL.
+
+ Takes values from ``environ['wsgiorg.routing_args']``.
+ Systems like ``routes`` set this value.
+ """
+ if 'wsgiorg.routing_args' in self.environ:
+ return self.environ['wsgiorg.routing_args'][0]
+ else:
+ # Since you can't update this value in-place, we don't need
+ # to set the key in the environment
+ return ()
+
+ def _urlargs__set(self, value):
+ environ = self.environ
+ if 'paste.urlvars' in environ:
+ # Some overlap between this and wsgiorg.routing_args; we need
+ # wsgiorg.routing_args to make this work
+ routing_args = (value, environ.pop('paste.urlvars'))
+ elif 'wsgiorg.routing_args' in environ:
+ routing_args = (value, environ['wsgiorg.routing_args'][1])
+ else:
+ routing_args = (value, {})
+ environ['wsgiorg.routing_args'] = routing_args
+
+ def _urlargs__del(self):
+ if 'wsgiorg.routing_args' in self.environ:
+ if not self.environ['wsgiorg.routing_args'][1]:
+ del self.environ['wsgiorg.routing_args']
+ else:
+ self.environ['wsgiorg.routing_args'] = (
+ (), self.environ['wsgiorg.routing_args'][1])
+
+ urlargs = property(_urlargs__get,
+ _urlargs__set,
+ _urlargs__del,
+ _urlargs__get.__doc__)
+
+ @property
+ def is_xhr(self):
+ """Is X-Requested-With header present and equal to ``XMLHttpRequest``?
+
+ Note: this isn't set by every XMLHttpRequest request, it is
+ only set if you are using a Javascript library that sets it
+ (or you set the header yourself manually). Currently
+ Prototype and jQuery are known to set this header."""
+ return self.environ.get('HTTP_X_REQUESTED_WITH', ''
+ ) == 'XMLHttpRequest'
+
+ def _host__get(self):
+ """Host name provided in HTTP_HOST, with fall-back to SERVER_NAME"""
+ if 'HTTP_HOST' in self.environ:
+ return self.environ['HTTP_HOST']
+ else:
+ return '%(SERVER_NAME)s:%(SERVER_PORT)s' % self.environ
+ def _host__set(self, value):
+ self.environ['HTTP_HOST'] = value
+ def _host__del(self):
+ if 'HTTP_HOST' in self.environ:
+ del self.environ['HTTP_HOST']
+ host = property(_host__get, _host__set, _host__del, doc=_host__get.__doc__)
+
+ def _body__get(self):
+ """
+ Return the content of the request body.
+ """
+ if not self.is_body_readable:
+ return ''
+ self.make_body_seekable() # we need this to have content_length
+ r = self.body_file.read(self.content_length)
+ self.body_file.seek(0)
+ return r
+ def _body__set(self, value):
+ if value is None:
+ value = ''
+ if not isinstance(value, str):
+ raise TypeError("You can only set Request.body to a str (not %r)"
+ % type(value))
+ if not http_method_probably_has_body.get(self.method, True):
+ if not value:
+ self.content_length = None
+ self.body_file_raw = StringIO('')
+ return
+ self.content_length = len(value)
+ self.body_file_raw = StringIO(value)
+ self.is_body_seekable = True
+ def _body__del(self):
+ self.body = ''
+ body = property(_body__get, _body__set, _body__del, doc=_body__get.__doc__)
+
+
+ @property
+ def str_POST(self):
+ """
+ Return a MultiDict containing all the variables from a form
+ request. Returns an empty dict-like object for non-form
+ requests.
+
+ Form requests are typically POST requests, however PUT requests
+ with an appropriate Content-Type are also supported.
+ """
+ warn_str_deprecation()
+ return self._str_POST
+
+
+ @property
+ def _str_POST(self):
+ env = self.environ
+ if self.method not in ('POST', 'PUT'):
+ return NoVars('Not a form request')
+ if 'webob._parsed_post_vars' in env:
+ vars, body_file = env['webob._parsed_post_vars']
+ if body_file is self.body_file_raw:
+ return vars
+ content_type = self.content_type
+ if ((self.method == 'PUT' and not content_type)
+ or content_type not in
+ ('', 'application/x-www-form-urlencoded',
+ 'multipart/form-data')
+ ):
+ # Not an HTML form submission
+ return NoVars('Not an HTML form submission (Content-Type: %s)'
+ % content_type)
+ if self.is_body_seekable:
+ self.body_file.seek(0)
+ fs_environ = env.copy()
+ # FieldStorage assumes a missing CONTENT_LENGTH, but a
+ # default of 0 is better:
+ fs_environ.setdefault('CONTENT_LENGTH', '0')
+ fs_environ['QUERY_STRING'] = ''
+ fs = cgi.FieldStorage(fp=self.body_file,
+ environ=fs_environ,
+ keep_blank_values=True)
+ vars = MultiDict.from_fieldstorage(fs)
+ #ctype = self.content_type or 'application/x-www-form-urlencoded'
+ ctype = env.get('CONTENT_TYPE', 'application/x-www-form-urlencoded')
+ self.body_file = FakeCGIBody(vars, ctype)
+ env['webob._parsed_post_vars'] = (vars, self.body_file_raw)
+ return vars
+
+
+
+ @property
+ def POST(self):
+ """
+ Like ``.str_POST``, but decodes values and keys
+ """
+ vars = self._str_POST
+ vars = UnicodeMultiDict(vars, encoding=self.charset,
+ errors=self.unicode_errors,
+ decode_keys=self.decode_param_names)
+ return vars
+
+
+
+ @property
+ def str_GET(self):
+ """
+ Return a MultiDict containing all the variables from the
+ QUERY_STRING.
+ """
+ warn_str_deprecation()
+ return self._str_GET
+
+ @property
+ def _str_GET(self):
+ env = self.environ
+ source = env.get('QUERY_STRING', '')
+ if 'webob._parsed_query_vars' in env:
+ vars, qs = env['webob._parsed_query_vars']
+ if qs == source:
+ return vars
+ if not source:
+ vars = TrackableMultiDict(__tracker=self._update_get, __name='GET')
+ else:
+ vars = TrackableMultiDict(parse_qsl(source,
+ keep_blank_values=True,
+ strict_parsing=False),
+ __tracker=self._update_get, __name='GET')
+ env['webob._parsed_query_vars'] = (vars, source)
+ return vars
+
+ def _update_get(self, vars, key=None, value=None):
+ env = self.environ
+ qs = urllib.urlencode(vars.items())
+ env['QUERY_STRING'] = qs
+ env['webob._parsed_query_vars'] = (vars, qs)
+
+
+ @property
+ def GET(self):
+ """
+ Like ``.str_GET``, but decodes values and keys
+ """
+ vars = self._str_GET
+ vars = UnicodeMultiDict(vars, encoding=self.charset,
+ errors=self.unicode_errors,
+ decode_keys=self.decode_param_names)
+ return vars
+
+ # TODO: remove in version 1.2
+ str_postvars = deprecated_property('str_postvars', 'use str_POST instead')
+ postvars = deprecated_property('postvars', 'use POST instead')
+ str_queryvars = deprecated_property('str_queryvars', 'use str_GET instead')
+ queryvars = deprecated_property('queryvars', 'use GET instead')
+
+
+ @property
+ def str_params(self):
+ """
+ A dictionary-like object containing both the parameters from
+ the query string and request body.
+ """
+ warn_str_deprecation()
+ return NestedMultiDict(self._str_GET, self._str_POST)
+
+
+ @property
+ def params(self):
+ """
+ Like ``.str_params``, but decodes values and keys
+ """
+ params = NestedMultiDict(self._str_GET, self._str_POST)
+ params = UnicodeMultiDict(params, encoding=self.charset,
+ errors=self.unicode_errors,
+ decode_keys=self.decode_param_names)
+ return params
+
+
+ @property
+ def str_cookies(self):
+ """
+ Return a *plain* dictionary of cookies as found in the request.
+ """
+ warn_str_deprecation()
+ return self._str_cookies
+
+ @property
+ def _str_cookies(self):
+ env = self.environ
+ source = env.get('HTTP_COOKIE', '')
+ if 'webob._parsed_cookies' in env:
+ vars, var_source = env['webob._parsed_cookies']
+ if var_source == source:
+ return vars
+ vars = {}
+ if source:
+ cookies = Cookie(source)
+ for name in cookies:
+ vars[name] = cookies[name].value
+ env['webob._parsed_cookies'] = (vars, source)
+ return vars
+
+ @property
+ def cookies(self):
+ """
+ Like ``.str_cookies``, but decodes values and keys
+ """
+ vars = self._str_cookies
+ vars = UnicodeMultiDict(vars, encoding=self.charset,
+ errors=self.unicode_errors,
+ decode_keys=self.decode_param_names)
+ return vars
+
+
+ def copy(self):
+ """
+ Copy the request and environment object.
+
+ This only does a shallow copy, except of wsgi.input
+ """
+ self.make_body_seekable()
+ env = self.environ.copy()
+ new_req = self.__class__(env)
+ new_req.copy_body()
+ return new_req
+
+ def copy_get(self):
+ """
+ Copies the request and environment object, but turning this request
+ into a GET along the way. If this was a POST request (or any other
+ verb) then it becomes GET, and the request body is thrown away.
+ """
+ env = self.environ.copy()
+ return self.__class__(env, method='GET', content_type=None, body='')
+
+ # webob.is_body_seekable marks input streams that are seekable
+ # this way we can have seekable input without testing the .seek() method
+ is_body_seekable = environ_getter('webob.is_body_seekable', False)
+
+ #is_body_readable = environ_getter('webob.is_body_readable', False)
+
+ def _is_body_readable__get(self):
+ """
+ webob.is_body_readable is a flag that tells us
+ that we can read the input stream even though
+ CONTENT_LENGTH is missing. This allows FakeCGIBody
+ to work and can be used by servers to support
+ chunked encoding in requests.
+ For background see https://bitbucket.org/ianb/webob/issue/6
+ """
+ if http_method_probably_has_body.get(self.method):
+ # known HTTP method with body
+ return True
+ elif self.content_length is not None:
+ # unknown HTTP method, but the Content-Length
+ # header is present
+ return True
+ else:
+ # last resort -- rely on the special flag
+ return self.environ.get('webob.is_body_readable', False)
+
+ def _is_body_readable__set(self, flag):
+ #@@ WARN
+ self.environ['webob.is_body_readable'] = bool(flag)
+
+ is_body_readable = property(_is_body_readable__get, _is_body_readable__set,
+ doc=_is_body_readable__get.__doc__
+ )
+
+
+
+ def make_body_seekable(self):
+ """
+ This forces ``environ['wsgi.input']`` to be seekable.
+ That means that, the content is copied into a StringIO or temporary
+ file and flagged as seekable, so that it will not be unnecessarily
+ copied again.
+
+ After calling this method the .body_file is always seeked to the
+ start of file and .content_length is not None.
+
+ The choice to copy to StringIO is made from
+ ``self.request_body_tempfile_limit``
+ """
+ if self.is_body_seekable:
+ self.body_file_raw.seek(0)
+ else:
+ self.copy_body()
+
+
+ def copy_body(self):
+ """
+ Copies the body, in cases where it might be shared with
+ another request object and that is not desired.
+
+ This copies the body in-place, either into a StringIO object
+ or a temporary file.
+ """
+ if not self.is_body_readable:
+ # there's no body to copy
+ self.body = ''
+ elif self.content_length is None:
+ # chunked body or FakeCGIBody
+ self.body = self.body_file_raw.read()
+ self._copy_body_tempfile()
+ else:
+ # try to read body into tempfile
+ did_copy = self._copy_body_tempfile()
+ if not did_copy:
+ # it wasn't necessary, so just read it into memory
+ self.body = self.body_file.read(self.content_length)
+
+ def _copy_body_tempfile(self):
+ """
+ Copy wsgi.input to tempfile if necessary. Returns True if it did.
+ """
+ tempfile_limit = self.request_body_tempfile_limit
+ todo = self.content_length
+ assert isinstance(todo, (int, long)), `todo`
+ if not tempfile_limit or todo <= tempfile_limit:
+ return False
+ fileobj = self.make_tempfile()
+ input = self.body_file
+ while todo > 0:
+ data = input.read(min(todo, 65536))
+ if not data:
+ # Normally this should not happen, because LimitedLengthFile should
+ # have raised an exception by now.
+ # It can happen if the is_body_seekable flag is incorrect.
+ raise DisconnectionError(
+ "Client disconnected (%s more bytes were expected)"
+ % todo
+ )
+ fileobj.write(data)
+ todo -= len(data)
+ fileobj.seek(0)
+ self.body_file_raw = fileobj
+ self.is_body_seekable = True
+ return True
+
+ def make_tempfile(self):
+ """
+ Create a tempfile to store big request body.
+ This API is not stable yet. A 'size' argument might be added.
+ """
+ return tempfile.TemporaryFile()
+
+
+ def remove_conditional_headers(self,
+ remove_encoding=True,
+ remove_range=True,
+ remove_match=True,
+ remove_modified=True):
+ """
+ Remove headers that make the request conditional.
+
+ These headers can cause the response to be 304 Not Modified,
+ which in some cases you may not want to be possible.
+
+ This does not remove headers like If-Match, which are used for
+ conflict detection.
+ """
+ check_keys = []
+ if remove_range:
+ check_keys += ['HTTP_IF_RANGE', 'HTTP_RANGE']
+ if remove_match:
+ check_keys.append('HTTP_IF_NONE_MATCH')
+ if remove_modified:
+ check_keys.append('HTTP_IF_MODIFIED_SINCE')
+ if remove_encoding:
+ check_keys.append('HTTP_ACCEPT_ENCODING')
+
+ for key in check_keys:
+ if key in self.environ:
+ del self.environ[key]
+
+
+ accept = accept_property('Accept', '14.1', MIMEAccept, MIMENilAccept)
+ accept_charset = accept_property('Accept-Charset', '14.2', AcceptCharset)
+ accept_encoding = accept_property('Accept-Encoding', '14.3', NilClass=NoAccept)
+ accept_language = accept_property('Accept-Language', '14.4', AcceptLanguage)
+
+ authorization = converter(
+ environ_getter('HTTP_AUTHORIZATION', None, '14.8'),
+ parse_auth, serialize_auth,
+ )
+
+
+ def _cache_control__get(self):
+ """
+ Get/set/modify the Cache-Control header (`HTTP spec section 14.9
+ <http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9>`_)
+ """
+ env = self.environ
+ value = env.get('HTTP_CACHE_CONTROL', '')
+ cache_header, cache_obj = env.get('webob._cache_control', (None, None))
+ if cache_obj is not None and cache_header == value:
+ return cache_obj
+ cache_obj = CacheControl.parse(value,
+ updates_to=self._update_cache_control,
+ type='request')
+ env['webob._cache_control'] = (value, cache_obj)
+ return cache_obj
+
+ def _cache_control__set(self, value):
+ env = self.environ
+ value = value or ''
+ if isinstance(value, dict):
+ value = CacheControl(value, type='request')
+ if isinstance(value, CacheControl):
+ str_value = str(value)
+ env['HTTP_CACHE_CONTROL'] = str_value
+ env['webob._cache_control'] = (str_value, value)
+ else:
+ env['HTTP_CACHE_CONTROL'] = str(value)
+ env['webob._cache_control'] = (None, None)
+
+ def _cache_control__del(self):
+ env = self.environ
+ if 'HTTP_CACHE_CONTROL' in env:
+ del env['HTTP_CACHE_CONTROL']
+ if 'webob._cache_control' in env:
+ del env['webob._cache_control']
+
+ def _update_cache_control(self, prop_dict):
+ self.environ['HTTP_CACHE_CONTROL'] = serialize_cache_control(prop_dict)
+
+ cache_control = property(_cache_control__get,
+ _cache_control__set,
+ _cache_control__del,
+ doc=_cache_control__get.__doc__)
+
+
+ if_match = etag_property('HTTP_IF_MATCH', AnyETag, '14.24')
+ if_none_match = etag_property('HTTP_IF_NONE_MATCH', NoETag, '14.26')
+
+ date = converter_date(environ_getter('HTTP_DATE', None, '14.8'))
+ if_modified_since = converter_date(
+ environ_getter('HTTP_IF_MODIFIED_SINCE', None, '14.25'))
+ if_unmodified_since = converter_date(
+ environ_getter('HTTP_IF_UNMODIFIED_SINCE', None, '14.28'))
+ if_range = converter(
+ environ_getter('HTTP_IF_RANGE', None, '14.27'),
+ parse_if_range, serialize_if_range, 'IfRange object')
+
+
+ max_forwards = converter(
+ environ_getter('HTTP_MAX_FORWARDS', None, '14.31'),
+ parse_int, serialize_int, 'int')
+
+ pragma = environ_getter('HTTP_PRAGMA', None, '14.32')
+
+ range = converter(
+ environ_getter('HTTP_RANGE', None, '14.35'),
+ parse_range, serialize_range, 'Range object')
+
+ referer = environ_getter('HTTP_REFERER', None, '14.36')
+ referrer = referer
+
+ user_agent = environ_getter('HTTP_USER_AGENT', None, '14.43')
+
+ def __repr__(self):
+ try:
+ name = '%s %s' % (self.method, self.url)
+ except KeyError:
+ name = '(invalid WSGI environ)'
+ msg = '<%s at 0x%x %s>' % (
+ self.__class__.__name__,
+ abs(id(self)), name)
+ return msg
+
+ def as_string(self, skip_body=False):
+ """
+ Return HTTP string representing this request.
+ If skip_body is True, exclude the body.
+ If skip_body is an integer larger than one, skip body
+ only if its length is bigger than that number.
+ """
+ url = self.url
+ host = self.host_url
+ assert url.startswith(host)
+ url = url[len(host):]
+ parts = ['%s %s %s' % (self.method, url, self.http_version)]
+ #self.headers.setdefault('Host', self.host)
+
+ # acquire body before we handle headers so that
+ # content-length will be set
+ body = None
+ if self.method in ('PUT', 'POST'):
+ if skip_body > 1:
+ if len(self.body) > skip_body:
+ body = '<body skipped (len=%s)>' % len(self.body)
+ else:
+ skip_body = False
+ if not skip_body:
+ body = self.body
+
+ parts += map('%s: %s'.__mod__, sorted(self.headers.items()))
+ if body:
+ parts.extend( ['',body] )
+ # HTTP clearly specifies CRLF
+ return '\r\n'.join(parts)
+
+ __str__ = as_string
+
+ @classmethod
+ def from_string(cls, s):
+ """
+ Create a request from HTTP string. If the string contains
+ extra data after the request, raise a ValueError.
+ """
+ f = StringIO(s)
+ r = cls.from_file(f)
+ if f.tell() != len(s):
+ raise ValueError("The string contains more data than expected")
+ return r
+
+ @classmethod
+ def from_file(cls, fp):
+ """Read a request from a file-like object (it must implement
+ ``.read(size)`` and ``.readline()``).
+
+ It will read up to the end of the request, not the end of the
+ file (unless the request is a POST or PUT and has no
+ Content-Length, in that case, the entire file is read).
+
+ This reads the request as represented by ``str(req)``; it may
+ not read every valid HTTP request properly."""
+ start_line = fp.readline()
+ try:
+ method, resource, http_version = start_line.rstrip('\r\n').split(None, 2)
+ except ValueError:
+ raise ValueError('Bad HTTP request line: %r' % start_line)
+ r = cls(environ_from_url(resource),
+ http_version=http_version,
+ method=method.upper()
+ )
+ del r.environ['HTTP_HOST']
+ while 1:
+ line = fp.readline()
+ if not line.strip():
+ # end of headers
+ break
+ hname, hval = line.split(':', 1)
+ hval = hval.strip()
+ if hname in r.headers:
+ hval = r.headers[hname] + ', ' + hval
+ r.headers[hname] = hval
+ if r.method in ('PUT', 'POST'):
+ clen = r.content_length
+ if clen is None:
+ r.body = fp.read()
+ else:
+ r.body = fp.read(clen)
+ return r
+
+ def call_application(self, application, catch_exc_info=False):
+ """
+ Call the given WSGI application, returning ``(status_string,
+ headerlist, app_iter)``
+
+ Be sure to call ``app_iter.close()`` if it's there.
+
+ If catch_exc_info is true, then returns ``(status_string,
+ headerlist, app_iter, exc_info)``, where the fourth item may
+ be None, but won't be if there was an exception. If you don't
+ do this and there was an exception, the exception will be
+ raised directly.
+ """
+ if self.is_body_seekable:
+ self.body_file_raw.seek(0)
+ captured = []
+ output = []
+ def start_response(status, headers, exc_info=None):
+ if exc_info is not None and not catch_exc_info:
+ raise exc_info[0], exc_info[1], exc_info[2]
+ captured[:] = [status, headers, exc_info]
+ return output.append
+ app_iter = application(self.environ, start_response)
+ if output or not captured:
+ try:
+ output.extend(app_iter)
+ finally:
+ if hasattr(app_iter, 'close'):
+ app_iter.close()
+ app_iter = output
+ if catch_exc_info:
+ return (captured[0], captured[1], app_iter, captured[2])
+ else:
+ return (captured[0], captured[1], app_iter)
+
+ # Will be filled in later:
+ ResponseClass = None
+
+ def get_response(self, application, catch_exc_info=False):
+ """
+ Like ``.call_application(application)``, except returns a
+ response object with ``.status``, ``.headers``, and ``.body``
+ attributes.
+
+ This will use ``self.ResponseClass`` to figure out the class
+ of the response object to return.
+ """
+ if catch_exc_info:
+ status, headers, app_iter, exc_info = self.call_application(
+ application, catch_exc_info=True)
+ del exc_info
+ else:
+ status, headers, app_iter = self.call_application(
+ application, catch_exc_info=False)
+ return self.ResponseClass(
+ status=status, headerlist=list(headers), app_iter=app_iter,
+ request=self)
+
+ @classmethod
+ def blank(cls, path, environ=None, base_url=None,
+ headers=None, POST=None, **kw):
+ """
+ Create a blank request environ (and Request wrapper) with the
+ given path (path should be urlencoded), and any keys from
+ environ.
+
+ The path will become path_info, with any query string split
+ off and used.
+
+ All necessary keys will be added to the environ, but the
+ values you pass in will take precedence. If you pass in
+ base_url then wsgi.url_scheme, HTTP_HOST, and SCRIPT_NAME will
+ be filled in from that value.
+
+ Any extra keyword will be passed to ``__init__`` (e.g.,
+ ``decode_param_names``).
+ """
+ env = environ_from_url(path)
+ if base_url:
+ scheme, netloc, path, query, fragment = urlparse.urlsplit(base_url)
+ if query or fragment:
+ raise ValueError(
+ "base_url (%r) cannot have a query or fragment"
+ % base_url)
+ if scheme:
+ env['wsgi.url_scheme'] = scheme
+ if netloc:
+ if ':' not in netloc:
+ if scheme == 'http':
+ netloc += ':80'
+ elif scheme == 'https':
+ netloc += ':443'
+ else:
+ raise ValueError(
+ "Unknown scheme: %r" % scheme)
+ host, port = netloc.split(':', 1)
+ env['SERVER_PORT'] = port
+ env['SERVER_NAME'] = host
+ env['HTTP_HOST'] = netloc
+ if path:
+ env['SCRIPT_NAME'] = urllib.unquote(path)
+ if environ:
+ env.update(environ)
+ content_type = kw.get('content_type', env.get('CONTENT_TYPE'))
+ if headers and 'Content-Type' in headers:
+ content_type = headers['Content-Type']
+ if content_type is not None:
+ kw['content_type'] = content_type
+ environ_add_POST(env, POST, content_type)
+ obj = cls(env, **kw)
+ if headers is not None:
+ obj.headers.update(headers)
+ return obj
+
+
+def environ_from_url(path):
+ if SCHEME_RE.search(path):
+ scheme, netloc, path, qs, fragment = urlparse.urlsplit(path)
+ if fragment:
+ raise TypeError("Path cannot contain a fragment (%r)" % fragment)
+ if qs:
+ path += '?' + qs
+ if ':' not in netloc:
+ if scheme == 'http':
+ netloc += ':80'
+ elif scheme == 'https':
+ netloc += ':443'
+ else:
+ raise TypeError("Unknown scheme: %r" % scheme)
+ else:
+ scheme = 'http'
+ netloc = 'localhost:80'
+ if path and '?' in path:
+ path_info, query_string = path.split('?', 1)
+ path_info = urllib.unquote(path_info)
+ else:
+ path_info = urllib.unquote(path)
+ query_string = ''
+ env = {
+ 'REQUEST_METHOD': 'GET',
+ 'SCRIPT_NAME': '',
+ 'PATH_INFO': path_info or '',
+ 'QUERY_STRING': query_string,
+ 'SERVER_NAME': netloc.split(':')[0],
+ 'SERVER_PORT': netloc.split(':')[1],
+ 'HTTP_HOST': netloc,
+ 'SERVER_PROTOCOL': 'HTTP/1.0',
+ 'wsgi.version': (1, 0),
+ 'wsgi.url_scheme': scheme,
+ 'wsgi.input': StringIO(''),
+ 'wsgi.errors': sys.stderr,
+ 'wsgi.multithread': False,
+ 'wsgi.multiprocess': False,
+ 'wsgi.run_once': False,
+ #'webob.is_body_seekable': True,
+ }
+ return env
+
+
+def environ_add_POST(env, data, content_type=None):
+ if data is None:
+ return
+ if env['REQUEST_METHOD'] not in ('POST', 'PUT'):
+ env['REQUEST_METHOD'] = 'POST'
+ has_files = False
+ if hasattr(data, 'items'):
+ data = data.items()
+ data.sort()
+ has_files = filter(lambda _: isinstance(_[1], (tuple, list)), data)
+ if content_type is None:
+ content_type = 'multipart/form-data' if has_files else 'application/x-www-form-urlencoded'
+ if content_type.startswith('multipart/form-data'):
+ if not isinstance(data, str):
+ content_type, data = _encode_multipart(data, content_type)
+ elif content_type.startswith('application/x-www-form-urlencoded'):
+ if has_files:
+ raise ValueError('Submiting files is not allowed for'
+ ' content type `%s`' % content_type)
+ if not isinstance(data, str):
+ data = urllib.urlencode(data)
+ else:
+ if not isinstance(data, str):
+ raise ValueError('Please provide `POST` data as string'
+ ' for content type `%s`' % content_type)
+ env['wsgi.input'] = StringIO(data)
+ env['webob.is_body_seekable'] = True
+ env['CONTENT_LENGTH'] = str(len(data))
+ env['CONTENT_TYPE'] = content_type
+
+
+
+class AdhocAttrMixin(object):
+ _setattr_stacklevel = 3
+
+ def __setattr__(self, attr, value, DEFAULT=object()):
+ if (getattr(self.__class__, attr, DEFAULT) is not DEFAULT or
+ attr.startswith('_')):
+ object.__setattr__(self, attr, value)
+ else:
+ self.environ.setdefault('webob.adhoc_attrs', {})[attr] = value
+
+ def __getattr__(self, attr, DEFAULT=object()):
+ try:
+ return self.environ['webob.adhoc_attrs'][attr]
+ except KeyError:
+ raise AttributeError(attr)
+
+ def __delattr__(self, attr, DEFAULT=object()):
+ if getattr(self.__class__, attr, DEFAULT) is not DEFAULT:
+ return object.__delattr__(self, attr)
+ try:
+ del self.environ['webob.adhoc_attrs'][attr]
+ except KeyError:
+ raise AttributeError(attr)
+
+
+class Request(AdhocAttrMixin, BaseRequest):
+ """ The default request implementation """
+
+
+
+#########################
+## Helper classes and monkeypatching
+#########################
+
+class DisconnectionError(IOError):
+ pass
+
+class LimitedLengthFile(object):
+ def __init__(self, file, maxlen):
+ self.file = file
+ self.maxlen = maxlen
+ self.remaining = maxlen
+
+ def __repr__(self):
+ return '<%s(%r, maxlen=%s)>' % (
+ self.__class__.__name__,
+ self.file,
+ self.maxlen
+ )
+
+ def read(self, sz=-1):
+ if sz is None or sz < 0:
+ sz = self.remaining
+ else:
+ sz = min(sz, self.remaining)
+ if not sz:
+ return ''
+ r = self.file.read(sz)
+ self.remaining -= len(r)
+ if len(r) < sz:
+ self._check_disconnect()
+ return r
+
+ def readline(self, hint=None):
+ hint = self._normhint(hint)
+ r = self.file.readline(hint)
+ self.remaining -= len(r)
+ if not r:
+ self._check_disconnect()
+ return r
+
+ def readlines(self, hint=None):
+ hint = self._normhint(hint)
+ r = self.file.readlines(hint)
+ total_len = sum(len(l) for l in r)
+ self.remaining -= total_len
+ if total_len < hint:
+ self._check_disconnect()
+ return r
+
+ def _normhint(self, hint):
+ return min(hint, self.remaining) if hint else self.remaining
+
+ def _check_disconnect(self):
+ if self.remaining:
+ raise DisconnectionError(
+ "The client disconnected while sending the POST/PUT body "
+ + "(%d more bytes were expected)" % self.remaining
+ )
+
+
+
+def _cgi_FieldStorage__repr__patch(self):
+ """ monkey patch for FieldStorage.__repr__
+
+ Unbelievably, the default __repr__ on FieldStorage reads
+ the entire file content instead of being sane about it.
+ This is a simple replacement that doesn't do that
+ """
+ if self.file:
+ return "FieldStorage(%r, %r)" % (self.name, self.filename)
+ return "FieldStorage(%r, %r, %r)" % (self.name, self.filename, self.value)
+
+cgi.FieldStorage.__repr__ = _cgi_FieldStorage__repr__patch
+
+class FakeCGIBody(object):
+ def __init__(self, vars, content_type):
+ if content_type.startswith('multipart/form-data'):
+ if not _get_multipart_boundary(content_type):
+ raise ValueError('Content-type: %r does not contain boundary'
+ % content_type)
+ self.vars = vars
+ self.content_type = content_type
+ self._body = None
+ self.position = 0
+
+ def tell(self):
+ return self.position
+
+ def read(self, size=-1):
+ body = self._get_body()
+ if size < 0:
+ v = body[self.position:]
+ self.position = len(body)
+ return v
+ else:
+ v = body[self.position:self.position+size]
+ self.position = min(len(body), self.position+size)
+ return v
+
+ def _get_body(self):
+ if self._body is None:
+ if self.content_type.startswith('application/x-www-form-urlencoded'):
+ self._body = urllib.urlencode(self.vars.items())
+ elif self.content_type.startswith('multipart/form-data'):
+ self._body = _encode_multipart(self.vars.iteritems(), self.content_type)[1]
+ else:
+ assert 0, ('Bad content type: %r' % self.content_type)
+ return self._body
+
+ def readline(self, size=None):
+ # We ignore size, but allow it to be hinted
+ rest = self._get_body()[self.position:]
+ next = rest.find('\r\n')
+ if next == -1:
+ return self.read()
+ self.position += next+2
+ return rest[:next+2]
+
+ def readlines(self, hint=None):
+ # Again, allow hint but ignore
+ body = self._get_body()
+ rest = body[self.position:]
+ self.position = len(body)
+ result = []
+ while 1:
+ next = rest.find('\r\n')
+ if next == -1:
+ result.append(rest)
+ break
+ result.append(rest[:next+2])
+ rest = rest[next+2:]
+ return result
+
+ def __iter__(self):
+ return iter(self.readlines())
+
+ def __repr__(self):
+ inner = repr(self.vars)
+ if len(inner) > 20:
+ inner = inner[:15] + '...' + inner[-5:]
+ if self.position:
+ inner += ' at position %s' % self.position
+ return '<%s at 0x%x viewing %s>' % (
+ self.__class__.__name__,
+ abs(id(self)), inner)
+
+
+def _get_multipart_boundary(ctype):
+ m = re.search(r'boundary=([^ ]+)', ctype, re.I)
+ if m:
+ return m.group(1).strip('"')
+
+
+def _encode_multipart(vars, content_type):
+ """Encode a multipart request body into a string"""
+ f = StringIO()
+ w = f.write
+ CRLF = '\r\n'
+ boundary = _get_multipart_boundary(content_type)
+ if not boundary:
+ boundary = os.urandom(10).encode('hex')
+ content_type += '; boundary=%s' % boundary
+ for name, value in vars:
+ w('--%s' % boundary)
+ w(CRLF)
+ assert name is not None, 'Value associated with no name: %r' % value
+ w('Content-Disposition: form-data; name="%s"' % name)
+ filename = None
+ if getattr(value, 'filename', None):
+ filename = value.filename
+ elif isinstance(value, (list, tuple)):
+ filename, value = value
+ if hasattr(value, 'read'):
+ value = value.read()
+ if filename is not None:
+ w('; filename="%s"' % filename)
+ w(CRLF)
+ # TODO: should handle value.disposition_options
+ if getattr(value, 'type', None):
+ w('Content-type: %s' % value.type)
+ if value.type_options:
+ for ct_name, ct_value in sorted(value.type_options.items()):
+ w('; %s="%s"' % (ct_name, ct_value))
+ w(CRLF)
+ w(CRLF)
+ if hasattr(value, 'value'):
+ w(value.value)
+ else:
+ w(value)
+ w(CRLF)
+ w('--%s--' % boundary)
+ return content_type, f.getvalue()
+
+def warn_str_deprecation():
+ warn_deprecation(
+ "req.str_* attrs are depreacted and will be disabled in WebOb 1.2, "
+ "use the unicode versions instead",
+ '1.2',
+ 3
+ )
+
+def warn_decode_deprecation():
+ warn_deprecation(
+ "decode_param_names is deprecated and will not be supported "
+ "starting with WebOb 1.2",
+ '1.2',
+ 3
+ )
diff --git a/lib/webob_1_1_1/webob/response.py b/lib/webob_1_1_1/webob/response.py
new file mode 100644
index 0000000..dc38808
--- /dev/null
+++ b/lib/webob_1_1_1/webob/response.py
@@ -0,0 +1,1205 @@
+import re, urlparse, zlib, struct
+from datetime import datetime, date, timedelta
+
+from webob.headers import ResponseHeaders
+from webob.cachecontrol import CacheControl, serialize_cache_control
+
+from webob.descriptors import *
+from webob.datetime_utils import *
+from webob.cookies import Cookie, Morsel
+from webob.util import status_reasons, warn_deprecation
+from webob.request import StringIO
+
+__all__ = ['Response']
+
+_PARAM_RE = re.compile(r'([a-z0-9]+)=(?:"([^"]*)"|([a-z0-9_.-]*))', re.I)
+_OK_PARAM_RE = re.compile(r'^[a-z0-9_.-]+$', re.I)
+
+_gzip_header = '\x1f\x8b\x08\x00\x00\x00\x00\x00\x02\xff'
+
+
+
+class Response(object):
+ """
+ Represents a WSGI response
+ """
+
+ default_content_type = 'text/html'
+ default_charset = 'UTF-8' # TODO: deprecate
+ unicode_errors = 'strict'
+ default_conditional_response = False
+
+ #
+ # __init__, from_file, copy
+ #
+
+ def __init__(self, body=None, status=None, headerlist=None, app_iter=None,
+ request=None, content_type=None, conditional_response=None,
+ **kw):
+ if app_iter is None:
+ if body is None:
+ body = ''
+ elif body is not None:
+ raise TypeError(
+ "You may only give one of the body and app_iter arguments")
+ if status is None:
+ self._status = '200 OK'
+ else:
+ self.status = status
+ if headerlist is None:
+ self._headerlist = []
+ else:
+ self._headerlist = headerlist
+ self._headers = None
+ if request is not None:
+ if hasattr(request, 'environ'):
+ self._environ = request.environ
+ self._request = request
+ else:
+ self._environ = request
+ self._request = None
+ else:
+ self._environ = self._request = None
+ if content_type is None:
+ content_type = self.default_content_type
+ charset = None
+ if 'charset' in kw:
+ charset = kw.pop('charset')
+ elif self.default_charset:
+ if content_type and (content_type == 'text/html'
+ or content_type.startswith('text/')
+ or content_type.startswith('application/xml')
+ or (content_type.startswith('application/')
+ and content_type.endswith('+xml'))):
+ charset = self.default_charset
+ if content_type and charset:
+ content_type += '; charset=' + charset
+ elif self._headerlist and charset:
+ self.charset = charset
+ if not self._headerlist and content_type:
+ self._headerlist.append(('Content-Type', content_type))
+ if conditional_response is None:
+ self.conditional_response = self.default_conditional_response
+ else:
+ self.conditional_response = bool(conditional_response)
+ if app_iter is None:
+ if isinstance(body, unicode):
+ if charset is None:
+ raise TypeError(
+ "You cannot set the body to a unicode value without a charset")
+ body = body.encode(charset)
+ app_iter = [body]
+ if headerlist is None:
+ self._headerlist.append(('Content-Length', str(len(body))))
+ else:
+ self.headers['Content-Length'] = str(len(body))
+ self._app_iter = app_iter
+ for name, value in kw.iteritems():
+ if not hasattr(self.__class__, name):
+ # Not a basic attribute
+ raise TypeError(
+ "Unexpected keyword: %s=%r" % (name, value))
+ setattr(self, name, value)
+
+
+ @classmethod
+ def from_file(cls, fp):
+ """Reads a response from a file-like object (it must implement
+ ``.read(size)`` and ``.readline()``).
+
+ It will read up to the end of the response, not the end of the
+ file.
+
+ This reads the response as represented by ``str(resp)``; it
+ may not read every valid HTTP response properly. Responses
+ must have a ``Content-Length``"""
+ headerlist = []
+ status = fp.readline().strip()
+ while 1:
+ line = fp.readline().strip()
+ if not line:
+ # end of headers
+ break
+ try:
+ header_name, value = line.split(':', 1)
+ except ValueError:
+ raise ValueError('Bad header line: %r' % line)
+ headerlist.append((header_name, value.strip()))
+ r = cls(
+ status=status,
+ headerlist=headerlist,
+ app_iter=(),
+ )
+ r.body = fp.read(r.content_length or 0)
+ return r
+
+ def copy(self):
+ """Makes a copy of the response"""
+ # we need to do this for app_iter to be reusable
+ app_iter = list(self._app_iter)
+ iter_close(self._app_iter)
+ # and this to make sure app_iter instances are different
+ self._app_iter = list(app_iter)
+ return self.__class__(
+ content_type=False,
+ status=self._status,
+ headerlist=self._headerlist[:],
+ app_iter=app_iter,
+ conditional_response=self.conditional_response)
+
+
+ #
+ # __repr__, __str__
+ #
+
+ def __repr__(self):
+ return '<%s at 0x%x %s>' % (self.__class__.__name__, abs(id(self)),
+ self.status)
+
+ def __str__(self, skip_body=False):
+ parts = [self.status]
+ if not skip_body:
+ # Force enumeration of the body (to set content-length)
+ self.body
+ parts += map('%s: %s'.__mod__, self.headerlist)
+ if not skip_body and self.body:
+ parts += ['', self.body]
+ return '\n'.join(parts)
+
+
+ #
+ # status, status_int
+ #
+
+ def _status__get(self):
+ """
+ The status string
+ """
+ return self._status
+
+ def _status__set(self, value):
+ if isinstance(value, unicode):
+ # Status messages have to be ASCII safe, so this is OK:
+ value = str(value)
+ if isinstance(value, int):
+ value = str(value)
+ if not isinstance(value, str):
+ raise TypeError(
+ "You must set status to a string or integer (not %s)"
+ % type(value))
+ if ' ' not in value:
+ # Need to add a reason:
+ code = int(value)
+ reason = status_reasons[code]
+ value += ' ' + reason
+ self._status = value
+
+ status = property(_status__get, _status__set, doc=_status__get.__doc__)
+
+ def _status_int__get(self):
+ """
+ The status as an integer
+ """
+ return int(self._status.split()[0])
+ def _status_int__set(self, code):
+ self._status = '%d %s' % (code, status_reasons[code])
+ status_int = property(_status_int__get, _status_int__set,
+ doc=_status_int__get.__doc__)
+
+ # TODO: remove in version 1.2
+ status_code = deprecated_property('status_code', 'use .status or .status_int instead')
+
+
+ #
+ # headerslist, headers
+ #
+
+ def _headerlist__get(self):
+ """
+ The list of response headers
+ """
+ return self._headerlist
+
+ def _headerlist__set(self, value):
+ self._headers = None
+ if not isinstance(value, list):
+ if hasattr(value, 'items'):
+ value = value.items()
+ value = list(value)
+ self._headerlist = value
+
+ def _headerlist__del(self):
+ self.headerlist = []
+
+ headerlist = property(_headerlist__get, _headerlist__set,
+ _headerlist__del, doc=_headerlist__get.__doc__)
+
+ def _headers__get(self):
+ """
+ The headers in a dictionary-like object
+ """
+ if self._headers is None:
+ self._headers = ResponseHeaders.view_list(self.headerlist)
+ return self._headers
+
+ def _headers__set(self, value):
+ if hasattr(value, 'items'):
+ value = value.items()
+ self.headerlist = value
+ self._headers = None
+
+ headers = property(_headers__get, _headers__set, doc=_headers__get.__doc__)
+
+
+ #
+ # body
+ #
+
+ def _body__get(self):
+ """
+ The body of the response, as a ``str``. This will read in the
+ entire app_iter if necessary.
+ """
+ app_iter = self._app_iter
+# try:
+# if len(app_iter) == 1:
+# return app_iter[0]
+# except:
+# pass
+ if isinstance(app_iter, list) and len(app_iter) == 1:
+ return app_iter[0]
+ if app_iter is None:
+ raise AttributeError("No body has been set")
+ try:
+ body = ''.join(app_iter)
+ finally:
+ iter_close(app_iter)
+ if isinstance(body, unicode):
+ raise _error_unicode_in_app_iter(app_iter, body)
+ self._app_iter = [body]
+ if len(body) == 0:
+ # if body-length is zero, we assume it's a HEAD response and
+ # leave content_length alone
+ pass # pragma: no cover (no idea why necessary, it's hit)
+ elif self.content_length is None:
+ self.content_length = len(body)
+ elif self.content_length != len(body):
+ raise AssertionError(
+ "Content-Length is different from actual app_iter length "
+ "(%r!=%r)"
+ % (self.content_length, len(body))
+ )
+ return body
+
+ def _body__set(self, value=''):
+ if not isinstance(value, str):
+ if isinstance(value, unicode):
+ msg = "You cannot set Response.body to a unicode object (use Response.text)"
+ else:
+ msg = "You can only set the body to a str (not %s)" % type(value)
+ raise TypeError(msg)
+ if self._app_iter is not None:
+ self.content_md5 = None
+ self._app_iter = [value]
+ self.content_length = len(value)
+
+# def _body__del(self):
+# self.body = ''
+# #self.content_length = None
+
+ body = property(_body__get, _body__set, _body__set)
+
+
+ #
+ # text, unicode_body, ubody
+ #
+
+ def _text__get(self):
+ """
+ Get/set the unicode value of the body (using the charset of the
+ Content-Type)
+ """
+ if not self.charset:
+ raise AttributeError(
+ "You cannot access Response.text unless charset is set")
+ body = self.body
+ return body.decode(self.charset, self.unicode_errors)
+
+ def _text__set(self, value):
+ if not self.charset:
+ raise AttributeError(
+ "You cannot access Response.text unless charset is set")
+ if not isinstance(value, unicode):
+ raise TypeError(
+ "You can only set Response.text to a unicode string "
+ "(not %s)" % type(value))
+ self.body = value.encode(self.charset)
+
+ def _text__del(self):
+ del self.body
+
+ text = property(_text__get, _text__set, _text__del, doc=_text__get.__doc__)
+
+
+# def _ubody__get(self):
+# """
+# Alias for text
+# """
+# _warn_ubody()
+# return self.text
+
+# def _ubody__set(self, val=None):
+# _warn_ubody()
+# if val is None:
+# del self.body
+# else:
+# self.text = val
+
+# unicode_body = ubody = property(_ubody__get, _ubody__set, _ubody__set)
+
+ unicode_body = ubody = property(
+ _text__get, _text__set, _text__del,
+ "Deprecated alias for .text"
+ )
+
+ #
+ # body_file, write(text)
+ #
+
+ def _body_file__get(self):
+ """
+ A file-like object that can be used to write to the
+ body. If you passed in a list app_iter, that app_iter will be
+ modified by writes.
+ """
+ return ResponseBodyFile(self)
+
+ def _body_file__set(self, file):
+ self.app_iter = iter_file(file)
+
+ def _body_file__del(self):
+ del self.body
+
+ body_file = property(_body_file__get, _body_file__set, _body_file__del,
+ doc=_body_file__get.__doc__)
+
+ def write(self, text):
+ if not isinstance(text, str):
+ if not isinstance(text, unicode):
+ msg = "You can only write str to a Response.body_file, not %s"
+ raise TypeError(msg % type(text))
+ if not self.charset:
+ msg = "You can only write unicode to Response if charset has been set"
+ raise TypeError(msg)
+ text = text.encode(self.charset)
+ app_iter = self._app_iter
+ if not isinstance(app_iter, list):
+ try:
+ new_app_iter = self._app_iter = list(app_iter)
+ finally:
+ iter_close(app_iter)
+ app_iter = new_app_iter
+ self.content_length = sum(len(chunk) for chunk in app_iter)
+ app_iter.append(text)
+ if self.content_length is not None:
+ self.content_length += len(text)
+
+
+
+ #
+ # app_iter
+ #
+
+ def _app_iter__get(self):
+ """
+ Returns the app_iter of the response.
+
+ If body was set, this will create an app_iter from that body
+ (a single-item list)
+ """
+ return self._app_iter
+
+ def _app_iter__set(self, value):
+ if self._app_iter is not None:
+ # Undo the automatically-set content-length
+ self.content_length = None
+ self.content_md5 = None
+ self._app_iter = value
+
+ def _app_iter__del(self):
+ self._app_iter = []
+ self.content_length = None
+
+ app_iter = property(_app_iter__get, _app_iter__set, _app_iter__del,
+ doc=_app_iter__get.__doc__)
+
+
+
+ #
+ # headers attrs
+ #
+
+ allow = list_header('Allow', '14.7')
+ # TODO: (maybe) support response.vary += 'something'
+ # TODO: same thing for all listy headers
+ vary = list_header('Vary', '14.44')
+
+ content_length = converter(
+ header_getter('Content-Length', '14.17'),
+ parse_int, serialize_int, 'int')
+
+ content_encoding = header_getter('Content-Encoding', '14.11')
+ content_language = list_header('Content-Language', '14.12')
+ content_location = header_getter('Content-Location', '14.14')
+ content_md5 = header_getter('Content-MD5', '14.14')
+ content_disposition = header_getter('Content-Disposition', '19.5.1')
+
+ accept_ranges = header_getter('Accept-Ranges', '14.5')
+ content_range = converter(
+ header_getter('Content-Range', '14.16'),
+ parse_content_range, serialize_content_range, 'ContentRange object')
+
+ date = date_header('Date', '14.18')
+ expires = date_header('Expires', '14.21')
+ last_modified = date_header('Last-Modified', '14.29')
+
+ etag = converter(
+ header_getter('ETag', '14.19'),
+ parse_etag_response, serialize_etag_response, 'Entity tag')
+
+ location = header_getter('Location', '14.30')
+ pragma = header_getter('Pragma', '14.32')
+ age = converter(
+ header_getter('Age', '14.6'),
+ parse_int_safe, serialize_int, 'int')
+
+ retry_after = converter(
+ header_getter('Retry-After', '14.37'),
+ parse_date_delta, serialize_date_delta, 'HTTP date or delta seconds')
+
+ server = header_getter('Server', '14.38')
+
+ # TODO: the standard allows this to be a list of challenges
+ www_authenticate = converter(
+ header_getter('WWW-Authenticate', '14.47'),
+ parse_auth, serialize_auth,
+ )
+
+
+ #
+ # charset
+ #
+
+ def _charset__get(self):
+ """
+ Get/set the charset (in the Content-Type)
+ """
+ header = self.headers.get('Content-Type')
+ if not header:
+ return None
+ match = CHARSET_RE.search(header)
+ if match:
+ return match.group(1)
+ return None
+
+ def _charset__set(self, charset):
+ if charset is None:
+ del self.charset
+ return
+ header = self.headers.pop('Content-Type', None)
+ if header is None:
+ raise AttributeError("You cannot set the charset when no "
+ "content-type is defined")
+ match = CHARSET_RE.search(header)
+ if match:
+ header = header[:match.start()] + header[match.end():]
+ header += '; charset=%s' % charset
+ self.headers['Content-Type'] = header
+
+ def _charset__del(self):
+ header = self.headers.pop('Content-Type', None)
+ if header is None:
+ # Don't need to remove anything
+ return
+ match = CHARSET_RE.search(header)
+ if match:
+ header = header[:match.start()] + header[match.end():]
+ self.headers['Content-Type'] = header
+
+ charset = property(_charset__get, _charset__set, _charset__del,
+ doc=_charset__get.__doc__)
+
+
+ #
+ # content_type
+ #
+
+ def _content_type__get(self):
+ """
+ Get/set the Content-Type header (or None), *without* the
+ charset or any parameters.
+
+ If you include parameters (or ``;`` at all) when setting the
+ content_type, any existing parameters will be deleted;
+ otherwise they will be preserved.
+ """
+ header = self.headers.get('Content-Type')
+ if not header:
+ return None
+ return header.split(';', 1)[0]
+
+ def _content_type__set(self, value):
+ if not value:
+ self._content_type__del()
+ return
+ if ';' not in value:
+ header = self.headers.get('Content-Type', '')
+ if ';' in header:
+ params = header.split(';', 1)[1]
+ value += ';' + params
+ self.headers['Content-Type'] = value
+
+ def _content_type__del(self):
+ self.headers.pop('Content-Type', None)
+
+ content_type = property(_content_type__get, _content_type__set,
+ _content_type__del, doc=_content_type__get.__doc__)
+
+
+ #
+ # content_type_params
+ #
+
+ def _content_type_params__get(self):
+ """
+ A dictionary of all the parameters in the content type.
+
+ (This is not a view, set to change, modifications of the dict would not be
+ applied otherwise)
+ """
+ params = self.headers.get('Content-Type', '')
+ if ';' not in params:
+ return {}
+ params = params.split(';', 1)[1]
+ result = {}
+ for match in _PARAM_RE.finditer(params):
+ result[match.group(1)] = match.group(2) or match.group(3) or ''
+ return result
+
+ def _content_type_params__set(self, value_dict):
+ if not value_dict:
+ del self.content_type_params
+ return
+ params = []
+ for k, v in sorted(value_dict.items()):
+ if not _OK_PARAM_RE.search(v):
+ v = '"%s"' % v.replace('"', '\\"')
+ params.append('; %s=%s' % (k, v))
+ ct = self.headers.pop('Content-Type', '').split(';', 1)[0]
+ ct += ''.join(params)
+ self.headers['Content-Type'] = ct
+
+ def _content_type_params__del(self):
+ self.headers['Content-Type'] = self.headers.get(
+ 'Content-Type', '').split(';', 1)[0]
+
+ content_type_params = property(
+ _content_type_params__get,
+ _content_type_params__set,
+ _content_type_params__del,
+ _content_type_params__get.__doc__
+ )
+
+
+
+
+ #
+ # set_cookie, unset_cookie, delete_cookie, merge_cookies
+ #
+
+ def set_cookie(self, key, value='', max_age=None,
+ path='/', domain=None, secure=False, httponly=False,
+ comment=None, expires=None, overwrite=False):
+ """
+ Set (add) a cookie for the response
+ """
+ if overwrite:
+ self.unset_cookie(key, strict=False)
+ if value is None: # delete the cookie from the client
+ value = ''
+ max_age = 0
+ expires = timedelta(days=-5)
+ elif expires is None and max_age is not None:
+ if isinstance(max_age, int):
+ max_age = timedelta(seconds=max_age)
+ expires = datetime.utcnow() + max_age
+ elif max_age is None and expires is not None:
+ max_age = expires - datetime.utcnow()
+
+ if isinstance(value, unicode):
+ value = value.encode('utf8')
+ m = Morsel(key, value)
+ m.path = path
+ m.domain = domain
+ m.comment = comment
+ m.expires = expires
+ m.max_age = max_age
+ m.secure = secure
+ m.httponly = httponly
+ self.headerlist.append(('Set-Cookie', str(m)))
+
+ def delete_cookie(self, key, path='/', domain=None):
+ """
+ Delete a cookie from the client. Note that path and domain must match
+ how the cookie was originally set.
+
+ This sets the cookie to the empty string, and max_age=0 so
+ that it should expire immediately.
+ """
+ self.set_cookie(key, None, path=path, domain=domain)
+
+ def unset_cookie(self, key, strict=True):
+ """
+ Unset a cookie with the given name (remove it from the
+ response).
+ """
+ existing = self.headers.getall('Set-Cookie')
+ if not existing and not strict:
+ return
+ cookies = Cookie()
+ for header in existing:
+ cookies.load(header)
+ if key in cookies:
+ del cookies[key]
+ del self.headers['Set-Cookie']
+ for m in cookies.values():
+ self.headerlist.append(('Set-Cookie', str(m)))
+ elif strict:
+ raise KeyError("No cookie has been set with the name %r" % key)
+
+
+ def merge_cookies(self, resp):
+ """Merge the cookies that were set on this response with the
+ given `resp` object (which can be any WSGI application).
+
+ If the `resp` is a :class:`webob.Response` object, then the
+ other object will be modified in-place.
+ """
+ if not self.headers.get('Set-Cookie'):
+ return resp
+ if isinstance(resp, Response):
+ for header in self.headers.getall('Set-Cookie'):
+ resp.headers.add('Set-Cookie', header)
+ return resp
+ else:
+ c_headers = [h for h in self.headerlist if
+ h[0].lower() == 'set-cookie']
+ def repl_app(environ, start_response):
+ def repl_start_response(status, headers, exc_info=None):
+ return start_response(status, headers+c_headers,
+ exc_info=exc_info)
+ return resp(environ, repl_start_response)
+ return repl_app
+
+
+ #
+ # cache_control
+ #
+
+ _cache_control_obj = None
+
+ def _cache_control__get(self):
+ """
+ Get/set/modify the Cache-Control header (`HTTP spec section 14.9
+ <http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9>`_)
+ """
+ value = self.headers.get('cache-control', '')
+ if self._cache_control_obj is None:
+ self._cache_control_obj = CacheControl.parse(
+ value, updates_to=self._update_cache_control, type='response')
+ self._cache_control_obj.header_value = value
+ if self._cache_control_obj.header_value != value:
+ new_obj = CacheControl.parse(value, type='response')
+ self._cache_control_obj.properties.clear()
+ self._cache_control_obj.properties.update(new_obj.properties)
+ self._cache_control_obj.header_value = value
+ return self._cache_control_obj
+
+ def _cache_control__set(self, value):
+ # This actually becomes a copy
+ if not value:
+ value = ""
+ if isinstance(value, dict):
+ value = CacheControl(value, 'response')
+ if isinstance(value, unicode):
+ value = str(value)
+ if isinstance(value, str):
+ if self._cache_control_obj is None:
+ self.headers['Cache-Control'] = value
+ return
+ value = CacheControl.parse(value, 'response')
+ cache = self.cache_control
+ cache.properties.clear()
+ cache.properties.update(value.properties)
+
+ def _cache_control__del(self):
+ self.cache_control = {}
+
+ def _update_cache_control(self, prop_dict):
+ value = serialize_cache_control(prop_dict)
+ if not value:
+ if 'Cache-Control' in self.headers:
+ del self.headers['Cache-Control']
+ else:
+ self.headers['Cache-Control'] = value
+
+ cache_control = property(
+ _cache_control__get, _cache_control__set,
+ _cache_control__del, doc=_cache_control__get.__doc__)
+
+
+ #
+ # cache_expires
+ #
+
+ def _cache_expires(self, seconds=0, **kw):
+ """
+ Set expiration on this request. This sets the response to
+ expire in the given seconds, and any other attributes are used
+ for cache_control (e.g., private=True, etc).
+ """
+ if seconds is True:
+ seconds = 0
+ elif isinstance(seconds, timedelta):
+ seconds = timedelta_to_seconds(seconds)
+ cache_control = self.cache_control
+ if seconds is None:
+ pass
+ elif not seconds:
+ # To really expire something, you have to force a
+ # bunch of these cache control attributes, and IE may
+ # not pay attention to those still so we also set
+ # Expires.
+ cache_control.no_store = True
+ cache_control.no_cache = True
+ cache_control.must_revalidate = True
+ cache_control.max_age = 0
+ cache_control.post_check = 0
+ cache_control.pre_check = 0
+ self.expires = datetime.utcnow()
+ if 'last-modified' not in self.headers:
+ self.last_modified = datetime.utcnow()
+ self.pragma = 'no-cache'
+ else:
+ cache_control.max_age = seconds
+ self.expires = datetime.utcnow() + timedelta(seconds=seconds)
+ for name, value in kw.items():
+ setattr(cache_control, name, value)
+
+ cache_expires = property(lambda self: self._cache_expires, _cache_expires)
+
+
+
+ #
+ # encode_content, decode_content, md5_etag
+ #
+
+ def encode_content(self, encoding='gzip', lazy=False):
+ """
+ Encode the content with the given encoding (only gzip and
+ identity are supported).
+ """
+ assert encoding in ('identity', 'gzip'), \
+ "Unknown encoding: %r" % encoding
+ if encoding == 'identity':
+ self.decode_content()
+ return
+ if self.content_encoding == 'gzip':
+ return
+ if lazy:
+ self.app_iter = gzip_app_iter(self._app_iter)
+ self.content_length = None
+ else:
+ self.app_iter = list(gzip_app_iter(self._app_iter))
+ self.content_length = sum(map(len, self._app_iter))
+ self.content_encoding = 'gzip'
+
+ def decode_content(self):
+ content_encoding = self.content_encoding or 'identity'
+ if content_encoding == 'identity':
+ return
+ if content_encoding not in ('gzip', 'deflate'):
+ raise ValueError(
+ "I don't know how to decode the content %s" % content_encoding)
+ if content_encoding == 'gzip':
+ from gzip import GzipFile
+ f = StringIO(self.body)
+ gzip_f = GzipFile(filename='', mode='r', fileobj=f)
+ self.body = gzip_f.read()
+ self.content_encoding = None
+ gzip_f.close()
+ f.close()
+ else:
+ # Weird feature: http://bugs.python.org/issue5784
+ self.body = zlib.decompress(self.body, -15)
+ self.content_encoding = None
+
+ def md5_etag(self, body=None, set_content_md5=False):
+ """
+ Generate an etag for the response object using an MD5 hash of
+ the body (the body parameter, or ``self.body`` if not given)
+
+ Sets ``self.etag``
+ If ``set_content_md5`` is True sets ``self.content_md5`` as well
+ """
+ if body is None:
+ body = self.body
+ try: # pragma: no cover
+ from hashlib import md5
+ except ImportError: # pragma: no cover
+ from md5 import md5
+ md5_digest = md5(body).digest().encode('base64').replace('\n', '')
+ self.etag = md5_digest.strip('=')
+ if set_content_md5:
+ self.content_md5 = md5_digest
+
+
+ #
+ # request
+ #
+
+ def _request__get(self):
+ """
+ Return the request associated with this response if any.
+ """
+ _warn_req()
+ if self._request is None and self._environ is not None:
+ self._request = self.RequestClass(self._environ)
+ return self._request
+
+ def _request__set(self, value):
+ _warn_req()
+ if value is None:
+ del self.request
+ return
+ if isinstance(value, dict):
+ self._environ = value
+ self._request = None
+ else:
+ self._request = value
+ self._environ = value.environ
+
+ def _request__del(self):
+ _warn_req()
+ self._request = self._environ = None
+
+ request = property(_request__get, _request__set, _request__del,
+ doc=_request__get.__doc__)
+
+
+ #
+ # environ
+ #
+
+ def _environ__get(self):
+ """
+ Get/set the request environ associated with this response, if
+ any.
+ """
+ _warn_req()
+ return self._environ
+
+ def _environ__set(self, value):
+ _warn_req()
+ if value is None:
+ del self.environ
+ self._environ = value
+ self._request = None
+
+ def _environ__del(self):
+ _warn_req()
+ self._request = self._environ = None
+
+ environ = property(_environ__get, _environ__set, _environ__del,
+ doc=_environ__get.__doc__)
+
+
+
+ #
+ # __call__, conditional_response_app
+ #
+
+ def __call__(self, environ, start_response):
+ """
+ WSGI application interface
+ """
+ if self.conditional_response:
+ return self.conditional_response_app(environ, start_response)
+ headerlist = self._abs_headerlist(environ)
+ start_response(self.status, headerlist)
+ if environ['REQUEST_METHOD'] == 'HEAD':
+ # Special case here...
+ return EmptyResponse(self._app_iter)
+ return self._app_iter
+
+ def _abs_headerlist(self, environ):
+ """Returns a headerlist, with the Location header possibly
+ made absolute given the request environ.
+ """
+ headerlist = self.headerlist
+ for name, value in headerlist:
+ if name.lower() == 'location':
+ if SCHEME_RE.search(value):
+ break
+ new_location = urlparse.urljoin(
+ _request_uri(environ), value)
+ headerlist = list(headerlist)
+ idx = headerlist.index((name, value))
+ headerlist[idx] = (name, new_location)
+ break
+ return headerlist
+
+ _safe_methods = ('GET', 'HEAD')
+
+ def conditional_response_app(self, environ, start_response):
+ """
+ Like the normal __call__ interface, but checks conditional headers:
+
+ * If-Modified-Since (304 Not Modified; only on GET, HEAD)
+ * If-None-Match (304 Not Modified; only on GET, HEAD)
+ * Range (406 Partial Content; only on GET, HEAD)
+ """
+ req = self.RequestClass(environ)
+ status304 = False
+ headerlist = self._abs_headerlist(environ)
+ if req.method in self._safe_methods:
+ if req.if_none_match and self.etag:
+ status304 = self.etag in req.if_none_match
+ elif req.if_modified_since and self.last_modified:
+ status304 = self.last_modified <= req.if_modified_since
+ if status304:
+ start_response('304 Not Modified', filter_headers(headerlist))
+ return EmptyResponse(self._app_iter)
+ if (req.range and req.if_range.match_response(self)
+ and self.content_range is None
+ and req.method in ('HEAD', 'GET')
+ and self.status_int == 200
+ and self.content_length is not None
+ ):
+ content_range = req.range.content_range(self.content_length)
+ # TODO: add support for If-Range
+ if content_range is None:
+ iter_close(self._app_iter)
+ body = "Requested range not satisfiable: %s" % req.range
+ headerlist = [
+ ('Content-Length', str(len(body))),
+ ('Content-Range', str(ContentRange(None, None, self.content_length))),
+ ('Content-Type', 'text/plain'),
+ ] + filter_headers(headerlist)
+ start_response('416 Requested Range Not Satisfiable', headerlist)
+ if req.method == 'HEAD':
+ return ()
+ return [body]
+ else:
+ app_iter = self.app_iter_range(content_range.start, content_range.stop)
+ if app_iter is not None:
+ # the following should be guaranteed by
+ # Range.range_for_length(length)
+ assert content_range.start is not None
+ headerlist = [
+ ('Content-Length', str(content_range.stop - content_range.start)),
+ ('Content-Range', str(content_range)),
+ ] + filter_headers(headerlist, ('content-length',))
+ start_response('206 Partial Content', headerlist)
+ if req.method == 'HEAD':
+ return EmptyResponse(app_iter)
+ return app_iter
+
+ start_response(self.status, headerlist)
+ if req.method == 'HEAD':
+ return EmptyResponse(self._app_iter)
+ return self._app_iter
+
+ def app_iter_range(self, start, stop):
+ """
+ Return a new app_iter built from the response app_iter, that
+ serves up only the given ``start:stop`` range.
+ """
+ app_iter = self._app_iter
+ if hasattr(app_iter, 'app_iter_range'):
+ return app_iter.app_iter_range(start, stop)
+ return AppIterRange(app_iter, start, stop)
+
+
+def filter_headers(hlist, remove_headers=('content-length', 'content-type')):
+ return [h for h in hlist if (h[0].lower() not in remove_headers)]
+
+
+def iter_file(file, block_size=1<<18): # 256Kb
+ while True:
+ data = file.read(block_size)
+ if not data:
+ break
+ yield data
+
+class ResponseBodyFile(object):
+ mode = 'wb'
+ closed = False
+
+ def __init__(self, response):
+ self.response = response
+ self.write = response.write
+
+ def __repr__(self):
+ return '<body_file for %r>' % self.response
+
+ encoding = property(
+ lambda self: self.response.charset,
+ doc="The encoding of the file (inherited from response.charset)"
+ )
+
+ def writelines(self, seq):
+ for item in seq:
+ self.write(item)
+
+ def close(self):
+ raise NotImplementedError("Response bodies cannot be closed")
+
+ def flush(self):
+ pass
+
+
+
+class AppIterRange(object):
+ """
+ Wraps an app_iter, returning just a range of bytes
+ """
+
+ def __init__(self, app_iter, start, stop):
+ assert start >= 0, "Bad start: %r" % start
+ assert stop is None or (stop >= 0 and stop >= start), (
+ "Bad stop: %r" % stop)
+ self.app_iter = iter(app_iter)
+ self._pos = 0 # position in app_iter
+ self.start = start
+ self.stop = stop
+
+ def __iter__(self):
+ return self
+
+ def _skip_start(self):
+ start, stop = self.start, self.stop
+ for chunk in self.app_iter:
+ self._pos += len(chunk)
+ if self._pos < start:
+ continue
+ elif self._pos == start:
+ return ''
+ else:
+ chunk = chunk[start-self._pos:]
+ if stop is not None and self._pos > stop:
+ chunk = chunk[:stop-self._pos]
+ assert len(chunk) == stop - start
+ return chunk
+ else:
+ raise StopIteration()
+
+
+ def next(self):
+ if self._pos < self.start:
+ # need to skip some leading bytes
+ return self._skip_start()
+ stop = self.stop
+ if stop is not None and self._pos >= stop:
+ raise StopIteration
+
+ chunk = self.app_iter.next()
+ self._pos += len(chunk)
+
+ if stop is None or self._pos <= stop:
+ return chunk
+ else:
+ return chunk[:stop-self._pos]
+
+ def close(self):
+ iter_close(self.app_iter)
+
+
+class EmptyResponse(object):
+ """An empty WSGI response.
+
+ An iterator that immediately stops. Optionally provides a close
+ method to close an underlying app_iter it replaces.
+ """
+
+ def __init__(self, app_iter=None):
+ if app_iter and hasattr(app_iter, 'close'):
+ self.close = app_iter.close
+
+ def __iter__(self):
+ return self
+
+ def __len__(self):
+ return 0
+
+ def next(self):
+ raise StopIteration()
+
+def _request_uri(environ):
+ """Like wsgiref.url.request_uri, except eliminates :80 ports
+
+ Return the full request URI"""
+ url = environ['wsgi.url_scheme']+'://'
+ from urllib import quote
+
+ if environ.get('HTTP_HOST'):
+ url += environ['HTTP_HOST']
+ else:
+ url += environ['SERVER_NAME'] + ':' + environ['SERVER_PORT']
+ if url.endswith(':80') and environ['wsgi.url_scheme'] == 'http':
+ url = url[:-3]
+ elif url.endswith(':443') and environ['wsgi.url_scheme'] == 'https':
+ url = url[:-4]
+
+ url += quote(environ.get('SCRIPT_NAME') or '/')
+ from urllib import quote
+ path_info = quote(environ.get('PATH_INFO',''))
+ if not environ.get('SCRIPT_NAME'):
+ url += path_info[1:]
+ else:
+ url += path_info
+ return url
+
+
+def iter_close(iter):
+ if hasattr(iter, 'close'):
+ iter.close()
+
+def gzip_app_iter(app_iter):
+ size = 0
+ crc = zlib.crc32("") & 0xffffffffL
+ compress = zlib.compressobj(9, zlib.DEFLATED, -zlib.MAX_WBITS,
+ zlib.DEF_MEM_LEVEL, 0)
+
+ yield _gzip_header
+ for item in app_iter:
+ size += len(item)
+ crc = zlib.crc32(item, crc) & 0xffffffffL
+ yield compress.compress(item)
+ yield compress.flush()
+ yield struct.pack("<2L", crc, size & 0xffffffffL)
+
+def _warn_ubody():
+ warn_deprecation(".unicode_body is deprecated in favour of Response.text", '1.3', 3)
+
+def _warn_req():
+ warn_deprecation("Response.request and Response.environ are deprecated", '1.2', 3)
+
+def _error_unicode_in_app_iter(app_iter, body):
+ app_iter_repr = repr(app_iter)
+ if len(app_iter_repr) > 50:
+ app_iter_repr = (
+ app_iter_repr[:30] + '...' + app_iter_repr[-10:])
+ raise TypeError(
+ 'An item of the app_iter (%s) was unicode, causing a '
+ 'unicode body: %r' % (app_iter_repr, body))
diff --git a/lib/webob_1_1_1/webob/util.py b/lib/webob_1_1_1/webob/util.py
new file mode 100644
index 0000000..2772c7c
--- /dev/null
+++ b/lib/webob_1_1_1/webob/util.py
@@ -0,0 +1,110 @@
+import cgi, warnings
+from webob.headers import _trans_key
+
+def html_escape(s):
+ """HTML-escape a string or object
+
+ This converts any non-string objects passed into it to strings
+ (actually, using ``unicode()``). All values returned are
+ non-unicode strings (using ``&#num;`` entities for all non-ASCII
+ characters).
+
+ None is treated specially, and returns the empty string.
+ """
+ if s is None:
+ return ''
+ if hasattr(s, '__html__'):
+ return s.__html__()
+ if not isinstance(s, basestring):
+ if hasattr(s, '__unicode__'):
+ s = unicode(s)
+ else:
+ s = str(s)
+ s = cgi.escape(s, True)
+ if isinstance(s, unicode):
+ s = s.encode('ascii', 'xmlcharrefreplace')
+ return s
+
+def header_docstring(header, rfc_section):
+ if header.isupper():
+ header = _trans_key(header)
+ major_section = rfc_section.split('.')[0]
+ link = 'http://www.w3.org/Protocols/rfc2616/rfc2616-sec%s.html#sec%s' % (major_section, rfc_section)
+ return "Gets and sets the ``%s`` header (`HTTP spec section %s <%s>`_)." \
+ % (header, rfc_section, link)
+
+def warn_deprecation(text, version, stacklevel):
+ # version specifies when to start raising exceptions instead of warnings
+ if version == '1.2':
+ cls = DeprecationWarning
+ elif version == '1.3':
+ cls = PendingDeprecationWarning
+ else:
+ cls = DeprecationWarning
+ warnings.warn("Unknown warn_deprecation version arg: %r" % version,
+ RuntimeWarning,
+ stacklevel=1
+ )
+ warnings.warn(text, cls, stacklevel=stacklevel+1)
+
+status_reasons = {
+ # Status Codes
+ # Informational
+ 100: 'Continue',
+ 101: 'Switching Protocols',
+ 102: 'Processing',
+
+ # Successful
+ 200: 'OK',
+ 201: 'Created',
+ 202: 'Accepted',
+ 203: 'Non-Authoritative Information',
+ 204: 'No Content',
+ 205: 'Reset Content',
+ 206: 'Partial Content',
+ 207: 'Multi Status',
+ 226: 'IM Used',
+
+ # Redirection
+ 300: 'Multiple Choices',
+ 301: 'Moved Permanently',
+ 302: 'Found',
+ 303: 'See Other',
+ 304: 'Not Modified',
+ 305: 'Use Proxy',
+ 307: 'Temporary Redirect',
+
+ # Client Error
+ 400: 'Bad Request',
+ 401: 'Unauthorized',
+ 402: 'Payment Required',
+ 403: 'Forbidden',
+ 404: 'Not Found',
+ 405: 'Method Not Allowed',
+ 406: 'Not Acceptable',
+ 407: 'Proxy Authentication Required',
+ 408: 'Request Timeout',
+ 409: 'Conflict',
+ 410: 'Gone',
+ 411: 'Length Required',
+ 412: 'Precondition Failed',
+ 413: 'Request Entity Too Large',
+ 414: 'Request URI Too Long',
+ 415: 'Unsupported Media Type',
+ 416: 'Requested Range Not Satisfiable',
+ 417: 'Expectation Failed',
+ 422: 'Unprocessable Entity',
+ 423: 'Locked',
+ 424: 'Failed Dependency',
+ 426: 'Upgrade Required',
+
+ # Server Error
+ 500: 'Internal Server Error',
+ 501: 'Not Implemented',
+ 502: 'Bad Gateway',
+ 503: 'Service Unavailable',
+ 504: 'Gateway Timeout',
+ 505: 'HTTP Version Not Supported',
+ 507: 'Insufficient Storage',
+ 510: 'Not Extended',
+}
diff --git a/new_project_template/app.yaml b/new_project_template/app.yaml
index 7e78559..73d761c 100644
--- a/new_project_template/app.yaml
+++ b/new_project_template/app.yaml
@@ -1,7 +1,8 @@
application: new-project-template
version: 1
-runtime: python
+runtime: python27
api_version: 1
+threadsafe: yes
handlers:
- url: /favicon\.ico
@@ -9,4 +10,8 @@
upload: favicon\.ico
- url: .*
- script: main.py
+ script: main.app
+
+libraries:
+- name: webapp2
+ version: "2.5.1"
diff --git a/new_project_template/main.py b/new_project_template/main.py
index 0e8f5ba..712f6c5 100755
--- a/new_project_template/main.py
+++ b/new_project_template/main.py
@@ -14,20 +14,11 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
-from google.appengine.ext import webapp
-from google.appengine.ext.webapp import util
+import webapp2
-
-class MainHandler(webapp.RequestHandler):
+class MainHandler(webapp2.RequestHandler):
def get(self):
self.response.out.write('Hello world!')
-
-def main():
- application = webapp.WSGIApplication([('/', MainHandler)],
- debug=True)
- util.run_wsgi_app(application)
-
-
-if __name__ == '__main__':
- main()
+app = webapp2.WSGIApplication([('/', MainHandler)],
+ debug=True)
diff --git a/remote_api_shell.py b/remote_api_shell.py
index 0276d25..90e2c47 100755
--- a/remote_api_shell.py
+++ b/remote_api_shell.py
@@ -51,7 +51,7 @@
os.path.join(DIR_PATH, 'lib', 'jinja2'),
os.path.join(DIR_PATH, 'lib', 'protorpc'),
os.path.join(DIR_PATH, 'lib', 'markupsafe'),
- os.path.join(DIR_PATH, 'lib', 'webob'),
+ os.path.join(DIR_PATH, 'lib', 'webob_0_9'),
os.path.join(DIR_PATH, 'lib', 'webapp2'),
os.path.join(DIR_PATH, 'lib', 'yaml', 'lib'),
os.path.join(DIR_PATH, 'lib', 'simplejson'),