OpenSecurity/install/web.py-0.37/build/lib/web/webapi.py
author om
Mon, 02 Dec 2013 14:02:05 +0100
changeset 3 65432e6c6042
permissions -rwxr-xr-x
initial deployment and project layout commit
om@3
     1
"""
om@3
     2
Web API (wrapper around WSGI)
om@3
     3
(from web.py)
om@3
     4
"""
om@3
     5
om@3
     6
__all__ = [
om@3
     7
    "config",
om@3
     8
    "header", "debug",
om@3
     9
    "input", "data",
om@3
    10
    "setcookie", "cookies",
om@3
    11
    "ctx", 
om@3
    12
    "HTTPError", 
om@3
    13
om@3
    14
    # 200, 201, 202
om@3
    15
    "OK", "Created", "Accepted",    
om@3
    16
    "ok", "created", "accepted",
om@3
    17
    
om@3
    18
    # 301, 302, 303, 304, 307
om@3
    19
    "Redirect", "Found", "SeeOther", "NotModified", "TempRedirect", 
om@3
    20
    "redirect", "found", "seeother", "notmodified", "tempredirect",
om@3
    21
om@3
    22
    # 400, 401, 403, 404, 405, 406, 409, 410, 412, 415
om@3
    23
    "BadRequest", "Unauthorized", "Forbidden", "NotFound", "NoMethod", "NotAcceptable", "Conflict", "Gone", "PreconditionFailed", "UnsupportedMediaType",
om@3
    24
    "badrequest", "unauthorized", "forbidden", "notfound", "nomethod", "notacceptable", "conflict", "gone", "preconditionfailed", "unsupportedmediatype",
om@3
    25
om@3
    26
    # 500
om@3
    27
    "InternalError", 
om@3
    28
    "internalerror",
om@3
    29
]
om@3
    30
om@3
    31
import sys, cgi, Cookie, pprint, urlparse, urllib
om@3
    32
from utils import storage, storify, threadeddict, dictadd, intget, safestr
om@3
    33
om@3
    34
config = storage()
om@3
    35
config.__doc__ = """
om@3
    36
A configuration object for various aspects of web.py.
om@3
    37
om@3
    38
`debug`
om@3
    39
   : when True, enables reloading, disabled template caching and sets internalerror to debugerror.
om@3
    40
"""
om@3
    41
om@3
    42
class HTTPError(Exception):
om@3
    43
    def __init__(self, status, headers={}, data=""):
om@3
    44
        ctx.status = status
om@3
    45
        for k, v in headers.items():
om@3
    46
            header(k, v)
om@3
    47
        self.data = data
om@3
    48
        Exception.__init__(self, status)
om@3
    49
        
om@3
    50
def _status_code(status, data=None, classname=None, docstring=None):
om@3
    51
    if data is None:
om@3
    52
        data = status.split(" ", 1)[1]
om@3
    53
    classname = status.split(" ", 1)[1].replace(' ', '') # 304 Not Modified -> NotModified    
om@3
    54
    docstring = docstring or '`%s` status' % status
om@3
    55
om@3
    56
    def __init__(self, data=data, headers={}):
om@3
    57
        HTTPError.__init__(self, status, headers, data)
om@3
    58
        
om@3
    59
    # trick to create class dynamically with dynamic docstring.
om@3
    60
    return type(classname, (HTTPError, object), {
om@3
    61
        '__doc__': docstring,
om@3
    62
        '__init__': __init__
om@3
    63
    })
om@3
    64
om@3
    65
ok = OK = _status_code("200 OK", data="")
om@3
    66
created = Created = _status_code("201 Created")
om@3
    67
accepted = Accepted = _status_code("202 Accepted")
om@3
    68
om@3
    69
class Redirect(HTTPError):
om@3
    70
    """A `301 Moved Permanently` redirect."""
om@3
    71
    def __init__(self, url, status='301 Moved Permanently', absolute=False):
om@3
    72
        """
om@3
    73
        Returns a `status` redirect to the new URL. 
om@3
    74
        `url` is joined with the base URL so that things like 
om@3
    75
        `redirect("about") will work properly.
om@3
    76
        """
om@3
    77
        newloc = urlparse.urljoin(ctx.path, url)
om@3
    78
om@3
    79
        if newloc.startswith('/'):
om@3
    80
            if absolute:
om@3
    81
                home = ctx.realhome
om@3
    82
            else:
om@3
    83
                home = ctx.home
om@3
    84
            newloc = home + newloc
om@3
    85
om@3
    86
        headers = {
om@3
    87
            'Content-Type': 'text/html',
om@3
    88
            'Location': newloc
om@3
    89
        }
om@3
    90
        HTTPError.__init__(self, status, headers, "")
om@3
    91
om@3
    92
redirect = Redirect
om@3
    93
om@3
    94
class Found(Redirect):
om@3
    95
    """A `302 Found` redirect."""
om@3
    96
    def __init__(self, url, absolute=False):
om@3
    97
        Redirect.__init__(self, url, '302 Found', absolute=absolute)
om@3
    98
om@3
    99
found = Found
om@3
   100
om@3
   101
class SeeOther(Redirect):
om@3
   102
    """A `303 See Other` redirect."""
om@3
   103
    def __init__(self, url, absolute=False):
om@3
   104
        Redirect.__init__(self, url, '303 See Other', absolute=absolute)
om@3
   105
    
om@3
   106
seeother = SeeOther
om@3
   107
om@3
   108
class NotModified(HTTPError):
om@3
   109
    """A `304 Not Modified` status."""
om@3
   110
    def __init__(self):
om@3
   111
        HTTPError.__init__(self, "304 Not Modified")
om@3
   112
om@3
   113
notmodified = NotModified
om@3
   114
om@3
   115
class TempRedirect(Redirect):
om@3
   116
    """A `307 Temporary Redirect` redirect."""
om@3
   117
    def __init__(self, url, absolute=False):
om@3
   118
        Redirect.__init__(self, url, '307 Temporary Redirect', absolute=absolute)
om@3
   119
om@3
   120
tempredirect = TempRedirect
om@3
   121
om@3
   122
class BadRequest(HTTPError):
om@3
   123
    """`400 Bad Request` error."""
om@3
   124
    message = "bad request"
om@3
   125
    def __init__(self, message=None):
om@3
   126
        status = "400 Bad Request"
om@3
   127
        headers = {'Content-Type': 'text/html'}
om@3
   128
        HTTPError.__init__(self, status, headers, message or self.message)
om@3
   129
om@3
   130
badrequest = BadRequest
om@3
   131
om@3
   132
class Unauthorized(HTTPError):
om@3
   133
    """`401 Unauthorized` error."""
om@3
   134
    message = "unauthorized"
om@3
   135
    def __init__(self):
om@3
   136
        status = "401 Unauthorized"
om@3
   137
        headers = {'Content-Type': 'text/html'}
om@3
   138
        HTTPError.__init__(self, status, headers, self.message)
om@3
   139
om@3
   140
unauthorized = Unauthorized
om@3
   141
om@3
   142
class Forbidden(HTTPError):
om@3
   143
    """`403 Forbidden` error."""
om@3
   144
    message = "forbidden"
om@3
   145
    def __init__(self):
om@3
   146
        status = "403 Forbidden"
om@3
   147
        headers = {'Content-Type': 'text/html'}
om@3
   148
        HTTPError.__init__(self, status, headers, self.message)
om@3
   149
om@3
   150
forbidden = Forbidden
om@3
   151
om@3
   152
class _NotFound(HTTPError):
om@3
   153
    """`404 Not Found` error."""
om@3
   154
    message = "not found"
om@3
   155
    def __init__(self, message=None):
om@3
   156
        status = '404 Not Found'
om@3
   157
        headers = {'Content-Type': 'text/html'}
om@3
   158
        HTTPError.__init__(self, status, headers, message or self.message)
om@3
   159
om@3
   160
def NotFound(message=None):
om@3
   161
    """Returns HTTPError with '404 Not Found' error from the active application.
om@3
   162
    """
om@3
   163
    if message:
om@3
   164
        return _NotFound(message)
om@3
   165
    elif ctx.get('app_stack'):
om@3
   166
        return ctx.app_stack[-1].notfound()
om@3
   167
    else:
om@3
   168
        return _NotFound()
om@3
   169
om@3
   170
notfound = NotFound
om@3
   171
om@3
   172
class NoMethod(HTTPError):
om@3
   173
    """A `405 Method Not Allowed` error."""
om@3
   174
    def __init__(self, cls=None):
om@3
   175
        status = '405 Method Not Allowed'
om@3
   176
        headers = {}
om@3
   177
        headers['Content-Type'] = 'text/html'
om@3
   178
        
om@3
   179
        methods = ['GET', 'HEAD', 'POST', 'PUT', 'DELETE']
om@3
   180
        if cls:
om@3
   181
            methods = [method for method in methods if hasattr(cls, method)]
om@3
   182
om@3
   183
        headers['Allow'] = ', '.join(methods)
om@3
   184
        data = None
om@3
   185
        HTTPError.__init__(self, status, headers, data)
om@3
   186
        
om@3
   187
nomethod = NoMethod
om@3
   188
om@3
   189
class NotAcceptable(HTTPError):
om@3
   190
    """`406 Not Acceptable` error."""
om@3
   191
    message = "not acceptable"
om@3
   192
    def __init__(self):
om@3
   193
        status = "406 Not Acceptable"
om@3
   194
        headers = {'Content-Type': 'text/html'}
om@3
   195
        HTTPError.__init__(self, status, headers, self.message)
om@3
   196
om@3
   197
notacceptable = NotAcceptable
om@3
   198
om@3
   199
class Conflict(HTTPError):
om@3
   200
    """`409 Conflict` error."""
om@3
   201
    message = "conflict"
om@3
   202
    def __init__(self):
om@3
   203
        status = "409 Conflict"
om@3
   204
        headers = {'Content-Type': 'text/html'}
om@3
   205
        HTTPError.__init__(self, status, headers, self.message)
om@3
   206
om@3
   207
conflict = Conflict
om@3
   208
om@3
   209
class Gone(HTTPError):
om@3
   210
    """`410 Gone` error."""
om@3
   211
    message = "gone"
om@3
   212
    def __init__(self):
om@3
   213
        status = '410 Gone'
om@3
   214
        headers = {'Content-Type': 'text/html'}
om@3
   215
        HTTPError.__init__(self, status, headers, self.message)
om@3
   216
om@3
   217
gone = Gone
om@3
   218
om@3
   219
class PreconditionFailed(HTTPError):
om@3
   220
    """`412 Precondition Failed` error."""
om@3
   221
    message = "precondition failed"
om@3
   222
    def __init__(self):
om@3
   223
        status = "412 Precondition Failed"
om@3
   224
        headers = {'Content-Type': 'text/html'}
om@3
   225
        HTTPError.__init__(self, status, headers, self.message)
om@3
   226
om@3
   227
preconditionfailed = PreconditionFailed
om@3
   228
om@3
   229
class UnsupportedMediaType(HTTPError):
om@3
   230
    """`415 Unsupported Media Type` error."""
om@3
   231
    message = "unsupported media type"
om@3
   232
    def __init__(self):
om@3
   233
        status = "415 Unsupported Media Type"
om@3
   234
        headers = {'Content-Type': 'text/html'}
om@3
   235
        HTTPError.__init__(self, status, headers, self.message)
om@3
   236
om@3
   237
unsupportedmediatype = UnsupportedMediaType
om@3
   238
om@3
   239
class _InternalError(HTTPError):
om@3
   240
    """500 Internal Server Error`."""
om@3
   241
    message = "internal server error"
om@3
   242
    
om@3
   243
    def __init__(self, message=None):
om@3
   244
        status = '500 Internal Server Error'
om@3
   245
        headers = {'Content-Type': 'text/html'}
om@3
   246
        HTTPError.__init__(self, status, headers, message or self.message)
om@3
   247
om@3
   248
def InternalError(message=None):
om@3
   249
    """Returns HTTPError with '500 internal error' error from the active application.
om@3
   250
    """
om@3
   251
    if message:
om@3
   252
        return _InternalError(message)
om@3
   253
    elif ctx.get('app_stack'):
om@3
   254
        return ctx.app_stack[-1].internalerror()
om@3
   255
    else:
om@3
   256
        return _InternalError()
om@3
   257
om@3
   258
internalerror = InternalError
om@3
   259
om@3
   260
def header(hdr, value, unique=False):
om@3
   261
    """
om@3
   262
    Adds the header `hdr: value` with the response.
om@3
   263
    
om@3
   264
    If `unique` is True and a header with that name already exists,
om@3
   265
    it doesn't add a new one. 
om@3
   266
    """
om@3
   267
    hdr, value = safestr(hdr), safestr(value)
om@3
   268
    # protection against HTTP response splitting attack
om@3
   269
    if '\n' in hdr or '\r' in hdr or '\n' in value or '\r' in value:
om@3
   270
        raise ValueError, 'invalid characters in header'
om@3
   271
        
om@3
   272
    if unique is True:
om@3
   273
        for h, v in ctx.headers:
om@3
   274
            if h.lower() == hdr.lower(): return
om@3
   275
    
om@3
   276
    ctx.headers.append((hdr, value))
om@3
   277
    
om@3
   278
def rawinput(method=None):
om@3
   279
    """Returns storage object with GET or POST arguments.
om@3
   280
    """
om@3
   281
    method = method or "both"
om@3
   282
    from cStringIO import StringIO
om@3
   283
om@3
   284
    def dictify(fs): 
om@3
   285
        # hack to make web.input work with enctype='text/plain.
om@3
   286
        if fs.list is None:
om@3
   287
            fs.list = [] 
om@3
   288
om@3
   289
        return dict([(k, fs[k]) for k in fs.keys()])
om@3
   290
    
om@3
   291
    e = ctx.env.copy()
om@3
   292
    a = b = {}
om@3
   293
    
om@3
   294
    if method.lower() in ['both', 'post', 'put']:
om@3
   295
        if e['REQUEST_METHOD'] in ['POST', 'PUT']:
om@3
   296
            if e.get('CONTENT_TYPE', '').lower().startswith('multipart/'):
om@3
   297
                # since wsgi.input is directly passed to cgi.FieldStorage, 
om@3
   298
                # it can not be called multiple times. Saving the FieldStorage
om@3
   299
                # object in ctx to allow calling web.input multiple times.
om@3
   300
                a = ctx.get('_fieldstorage')
om@3
   301
                if not a:
om@3
   302
                    fp = e['wsgi.input']
om@3
   303
                    a = cgi.FieldStorage(fp=fp, environ=e, keep_blank_values=1)
om@3
   304
                    ctx._fieldstorage = a
om@3
   305
            else:
om@3
   306
                fp = StringIO(data())
om@3
   307
                a = cgi.FieldStorage(fp=fp, environ=e, keep_blank_values=1)
om@3
   308
            a = dictify(a)
om@3
   309
om@3
   310
    if method.lower() in ['both', 'get']:
om@3
   311
        e['REQUEST_METHOD'] = 'GET'
om@3
   312
        b = dictify(cgi.FieldStorage(environ=e, keep_blank_values=1))
om@3
   313
om@3
   314
    def process_fieldstorage(fs):
om@3
   315
        if isinstance(fs, list):
om@3
   316
            return [process_fieldstorage(x) for x in fs]
om@3
   317
        elif fs.filename is None:
om@3
   318
            return fs.value
om@3
   319
        else:
om@3
   320
            return fs
om@3
   321
om@3
   322
    return storage([(k, process_fieldstorage(v)) for k, v in dictadd(b, a).items()])
om@3
   323
om@3
   324
def input(*requireds, **defaults):
om@3
   325
    """
om@3
   326
    Returns a `storage` object with the GET and POST arguments. 
om@3
   327
    See `storify` for how `requireds` and `defaults` work.
om@3
   328
    """
om@3
   329
    _method = defaults.pop('_method', 'both')
om@3
   330
    out = rawinput(_method)
om@3
   331
    try:
om@3
   332
        defaults.setdefault('_unicode', True) # force unicode conversion by default.
om@3
   333
        return storify(out, *requireds, **defaults)
om@3
   334
    except KeyError:
om@3
   335
        raise badrequest()
om@3
   336
om@3
   337
def data():
om@3
   338
    """Returns the data sent with the request."""
om@3
   339
    if 'data' not in ctx:
om@3
   340
        cl = intget(ctx.env.get('CONTENT_LENGTH'), 0)
om@3
   341
        ctx.data = ctx.env['wsgi.input'].read(cl)
om@3
   342
    return ctx.data
om@3
   343
om@3
   344
def setcookie(name, value, expires='', domain=None,
om@3
   345
              secure=False, httponly=False, path=None):
om@3
   346
    """Sets a cookie."""
om@3
   347
    morsel = Cookie.Morsel()
om@3
   348
    name, value = safestr(name), safestr(value)
om@3
   349
    morsel.set(name, value, urllib.quote(value))
om@3
   350
    if expires < 0:
om@3
   351
        expires = -1000000000
om@3
   352
    morsel['expires'] = expires
om@3
   353
    morsel['path'] = path or ctx.homepath+'/'
om@3
   354
    if domain:
om@3
   355
        morsel['domain'] = domain
om@3
   356
    if secure:
om@3
   357
        morsel['secure'] = secure
om@3
   358
    value = morsel.OutputString()
om@3
   359
    if httponly:
om@3
   360
        value += '; httponly'
om@3
   361
    header('Set-Cookie', value)
om@3
   362
        
om@3
   363
def decode_cookie(value):
om@3
   364
    r"""Safely decodes a cookie value to unicode. 
om@3
   365
    
om@3
   366
    Tries us-ascii, utf-8 and io8859 encodings, in that order.
om@3
   367
om@3
   368
    >>> decode_cookie('')
om@3
   369
    u''
om@3
   370
    >>> decode_cookie('asdf')
om@3
   371
    u'asdf'
om@3
   372
    >>> decode_cookie('foo \xC3\xA9 bar')
om@3
   373
    u'foo \xe9 bar'
om@3
   374
    >>> decode_cookie('foo \xE9 bar')
om@3
   375
    u'foo \xe9 bar'
om@3
   376
    """
om@3
   377
    try:
om@3
   378
        # First try plain ASCII encoding
om@3
   379
        return unicode(value, 'us-ascii')
om@3
   380
    except UnicodeError:
om@3
   381
        # Then try UTF-8, and if that fails, ISO8859
om@3
   382
        try:
om@3
   383
            return unicode(value, 'utf-8')
om@3
   384
        except UnicodeError:
om@3
   385
            return unicode(value, 'iso8859', 'ignore')
om@3
   386
om@3
   387
def parse_cookies(http_cookie):
om@3
   388
    r"""Parse a HTTP_COOKIE header and return dict of cookie names and decoded values.
om@3
   389
        
om@3
   390
    >>> sorted(parse_cookies('').items())
om@3
   391
    []
om@3
   392
    >>> sorted(parse_cookies('a=1').items())
om@3
   393
    [('a', '1')]
om@3
   394
    >>> sorted(parse_cookies('a=1%202').items())
om@3
   395
    [('a', '1 2')]
om@3
   396
    >>> sorted(parse_cookies('a=Z%C3%A9Z').items())
om@3
   397
    [('a', 'Z\xc3\xa9Z')]
om@3
   398
    >>> sorted(parse_cookies('a=1; b=2; c=3').items())
om@3
   399
    [('a', '1'), ('b', '2'), ('c', '3')]
om@3
   400
    >>> sorted(parse_cookies('a=1; b=w("x")|y=z; c=3').items())
om@3
   401
    [('a', '1'), ('b', 'w('), ('c', '3')]
om@3
   402
    >>> sorted(parse_cookies('a=1; b=w(%22x%22)|y=z; c=3').items())
om@3
   403
    [('a', '1'), ('b', 'w("x")|y=z'), ('c', '3')]
om@3
   404
om@3
   405
    >>> sorted(parse_cookies('keebler=E=mc2').items())
om@3
   406
    [('keebler', 'E=mc2')]
om@3
   407
    >>> sorted(parse_cookies(r'keebler="E=mc2; L=\"Loves\"; fudge=\012;"').items())
om@3
   408
    [('keebler', 'E=mc2; L="Loves"; fudge=\n;')]
om@3
   409
    """
om@3
   410
    #print "parse_cookies"
om@3
   411
    if '"' in http_cookie:
om@3
   412
        # HTTP_COOKIE has quotes in it, use slow but correct cookie parsing
om@3
   413
        cookie = Cookie.SimpleCookie()
om@3
   414
        try:
om@3
   415
            cookie.load(http_cookie)
om@3
   416
        except Cookie.CookieError:
om@3
   417
            # If HTTP_COOKIE header is malformed, try at least to load the cookies we can by
om@3
   418
            # first splitting on ';' and loading each attr=value pair separately
om@3
   419
            cookie = Cookie.SimpleCookie()
om@3
   420
            for attr_value in http_cookie.split(';'):
om@3
   421
                try:
om@3
   422
                    cookie.load(attr_value)
om@3
   423
                except Cookie.CookieError:
om@3
   424
                    pass
om@3
   425
        cookies = dict((k, urllib.unquote(v.value)) for k, v in cookie.iteritems())
om@3
   426
    else:
om@3
   427
        # HTTP_COOKIE doesn't have quotes, use fast cookie parsing
om@3
   428
        cookies = {}
om@3
   429
        for key_value in http_cookie.split(';'):
om@3
   430
            key_value = key_value.split('=', 1)
om@3
   431
            if len(key_value) == 2:
om@3
   432
                key, value = key_value
om@3
   433
                cookies[key.strip()] = urllib.unquote(value.strip())
om@3
   434
    return cookies
om@3
   435
om@3
   436
def cookies(*requireds, **defaults):
om@3
   437
    r"""Returns a `storage` object with all the request cookies in it.
om@3
   438
    
om@3
   439
    See `storify` for how `requireds` and `defaults` work.
om@3
   440
om@3
   441
    This is forgiving on bad HTTP_COOKIE input, it tries to parse at least
om@3
   442
    the cookies it can.
om@3
   443
    
om@3
   444
    The values are converted to unicode if _unicode=True is passed.
om@3
   445
    """
om@3
   446
    # If _unicode=True is specified, use decode_cookie to convert cookie value to unicode 
om@3
   447
    if defaults.get("_unicode") is True:
om@3
   448
        defaults['_unicode'] = decode_cookie
om@3
   449
        
om@3
   450
    # parse cookie string and cache the result for next time.
om@3
   451
    if '_parsed_cookies' not in ctx:
om@3
   452
        http_cookie = ctx.env.get("HTTP_COOKIE", "")
om@3
   453
        ctx._parsed_cookies = parse_cookies(http_cookie)
om@3
   454
om@3
   455
    try:
om@3
   456
        return storify(ctx._parsed_cookies, *requireds, **defaults)
om@3
   457
    except KeyError:
om@3
   458
        badrequest()
om@3
   459
        raise StopIteration
om@3
   460
om@3
   461
def debug(*args):
om@3
   462
    """
om@3
   463
    Prints a prettyprinted version of `args` to stderr.
om@3
   464
    """
om@3
   465
    try: 
om@3
   466
        out = ctx.environ['wsgi.errors']
om@3
   467
    except: 
om@3
   468
        out = sys.stderr
om@3
   469
    for arg in args:
om@3
   470
        print >> out, pprint.pformat(arg)
om@3
   471
    return ''
om@3
   472
om@3
   473
def _debugwrite(x):
om@3
   474
    try: 
om@3
   475
        out = ctx.environ['wsgi.errors']
om@3
   476
    except: 
om@3
   477
        out = sys.stderr
om@3
   478
    out.write(x)
om@3
   479
debug.write = _debugwrite
om@3
   480
om@3
   481
ctx = context = threadeddict()
om@3
   482
om@3
   483
ctx.__doc__ = """
om@3
   484
A `storage` object containing various information about the request:
om@3
   485
  
om@3
   486
`environ` (aka `env`)
om@3
   487
   : A dictionary containing the standard WSGI environment variables.
om@3
   488
om@3
   489
`host`
om@3
   490
   : The domain (`Host` header) requested by the user.
om@3
   491
om@3
   492
`home`
om@3
   493
   : The base path for the application.
om@3
   494
om@3
   495
`ip`
om@3
   496
   : The IP address of the requester.
om@3
   497
om@3
   498
`method`
om@3
   499
   : The HTTP method used.
om@3
   500
om@3
   501
`path`
om@3
   502
   : The path request.
om@3
   503
   
om@3
   504
`query`
om@3
   505
   : If there are no query arguments, the empty string. Otherwise, a `?` followed
om@3
   506
     by the query string.
om@3
   507
om@3
   508
`fullpath`
om@3
   509
   : The full path requested, including query arguments (`== path + query`).
om@3
   510
om@3
   511
### Response Data
om@3
   512
om@3
   513
`status` (default: "200 OK")
om@3
   514
   : The status code to be used in the response.
om@3
   515
om@3
   516
`headers`
om@3
   517
   : A list of 2-tuples to be used in the response.
om@3
   518
om@3
   519
`output`
om@3
   520
   : A string to be used as the response.
om@3
   521
"""
om@3
   522
om@3
   523
if __name__ == "__main__":
om@3
   524
    import doctest
om@3
   525
    doctest.testmod()