web.web.webapi

Package web :: Package web :: Module webapi |
[hide private]
[frames] | no frames]

# Source Code for Module web.web.webapi

      1  """
      2  Web API (wrapper around WSGI)
      3  (from web.py)
      4  """
      5
      6  __all__ = [
      7      "config",
      8      "header", "debug",
      9      "input", "data",
     10      "setcookie", "cookies",
     11      "ctx",
     12      "HTTPError",
     13
     14      # 200, 201, 202
     15      "OK", "Created", "Accepted",
     16      "ok", "created", "accepted",
     17
     18      # 301, 302, 303, 304, 307
     19      "Redirect", "Found", "SeeOther", "NotModified", "TempRedirect",
     20      "redirect", "found", "seeother", "notmodified", "tempredirect",
     21
     22      # 400, 401, 403, 404, 405, 406, 409, 410, 412
     23      "BadRequest", "Unauthorized", "Forbidden", "NotFound", "NoMethod", "NotAcceptable", "Conflict", "Gone", "PreconditionFailed",
     24      "badrequest", "unauthorized", "forbidden", "notfound", "nomethod", "notacceptable", "conflict", "gone", "preconditionfailed",
     25
     26      # 500
     27      "InternalError",
     28      "internalerror",
     29  ]
     30
     31  import sys, cgi, Cookie, pprint, urlparse, urllib
     32  from utils import storage, storify, threadeddict, dictadd, intget, utf8
     33
     34  config = storage()
     35  config.__doc__ = """
     36  A configuration object for various aspects of web.py.
     37
     38  `debug`
     39     : when True, enables reloading, disabled template caching and sets internalerror to debugerror.
     40  """
     41
     42 -class HTTPError(Exception):
     43 -    def __init__(self, status, headers={}, data=""):
     44          ctx.status = status
     45          for k, v in headers.items():
     46              header(k, v)
     47          self.data = data
     48          Exception.__init__(self, status)
     49
     50 -def _status_code(status, data=None, classname=None, docstring=None):
     51      if data is None:
     52          data = status.split(" ", 1)[1]
     53      classname = status.split(" ", 1)[1].replace(' ', '') # 304 Not Modified -> NotModified
     54      docstring = docstring or '`%s` status' % status
     55
     56      def __init__(self, data=data, headers={}):
     57          HTTPError.__init__(self, status, headers, data)
     58
     59      # trick to create class dynamically with dynamic docstring.
     60      return type(classname, (HTTPError, object), {
     61          '__doc__': docstring,
     62          '__init__': __init__
     63      })
     64
     65  ok = OK = _status_code("200 OK", data="")
     66  created = Created = _status_code("201 Created")
     67  accepted = Accepted = _status_code("202 Accepted")
     68
     69 -class Redirect(HTTPError):
     70      """A `301 Moved Permanently` redirect."""
     71 -    def __init__(self, url, status='301 Moved Permanently', absolute=False):
     72          """
     73          Returns a `status` redirect to the new URL.
     74          `url` is joined with the base URL so that things like
     75          `redirect("about") will work properly.
     76          """
     77          newloc = urlparse.urljoin(ctx.path, url)
     78
     79          if newloc.startswith('/'):
     80              if absolute:
     81                  home = ctx.realhome
     82              else:
     83                  home = ctx.home
     84              newloc = home + newloc
     85
     86          headers = {
     87              'Content-Type': 'text/html',
     88              'Location': newloc
     89          }
     90          HTTPError.__init__(self, status, headers, "")
     91
     92  redirect = Redirect
     93
     94 -class Found(Redirect):
     95      """A `302 Found` redirect."""
     96 -    def __init__(self, url, absolute=False):
     97          Redirect.__init__(self, url, '302 Found', absolute=absolute)
     98
     99  found = Found
    100
    101 -class SeeOther(Redirect):
    102      """A `303 See Other` redirect."""
    103 -    def __init__(self, url, absolute=False):
    104          Redirect.__init__(self, url, '303 See Other', absolute=absolute)
    105
    106  seeother = SeeOther
    107
    108 -class NotModified(HTTPError):
    109      """A `304 Not Modified` status."""
    110 -    def __init__(self):
    111          HTTPError.__init__(self, "304 Not Modified")
    112
    113  notmodified = NotModified
    114
    115 -class TempRedirect(Redirect):
    116      """A `307 Temporary Redirect` redirect."""
    117 -    def __init__(self, url, absolute=False):
    118          Redirect.__init__(self, url, '307 Temporary Redirect', absolute=absolute)
    119
    120  tempredirect = TempRedirect
    121
    122 -class BadRequest(HTTPError):
    123      """`400 Bad Request` error."""
    124      message = "bad request"
    125 -    def __init__(self):
    126          status = "400 Bad Request"
    127          headers = {'Content-Type': 'text/html'}
    128          HTTPError.__init__(self, status, headers, self.message)
    129
    130  badrequest = BadRequest
    131
    132 -class _NotFound(HTTPError):
    133      """`404 Not Found` error."""
    134      message = "not found"
    135 -    def __init__(self, message=None):
    136          status = '404 Not Found'
    137          headers = {'Content-Type': 'text/html'}
    138          HTTPError.__init__(self, status, headers, message or self.message)
    139
    140 -def NotFound(message=None):
    141      """Returns HTTPError with '404 Not Found' error from the active application.
    142      """
    143      if message:
    144          return _NotFound(message)
    145      elif ctx.get('app_stack'):
    146          return ctx.app_stack[-1].notfound()
    147      else:
    148          return _NotFound()
    149
    150  notfound = NotFound
    151
    152  unauthorized = Unauthorized = _status_code("401 Unauthorized")
    153  forbidden = Forbidden = _status_code("403 Forbidden")
    154  notacceptable = NotAcceptable = _status_code("406 Not Acceptable")
    155  conflict = Conflict = _status_code("409 Conflict")
    156  preconditionfailed = PreconditionFailed = _status_code("412 Precondition Failed")
    157
    158 -class NoMethod(HTTPError):
    159      """A `405 Method Not Allowed` error."""
    160 -    def __init__(self, cls=None):
    161          status = '405 Method Not Allowed'
    162          headers = {}
    163          headers['Content-Type'] = 'text/html'
    164
    165          methods = ['GET', 'HEAD', 'POST', 'PUT', 'DELETE']
    166          if cls:
    167              methods = [method for method in methods if hasattr(cls, method)]
    168
    169          headers['Allow'] = ', '.join(methods)
    170          data = None
    171          HTTPError.__init__(self, status, headers, data)
    172
    173  nomethod = NoMethod
    174
    175 -class Gone(HTTPError):
    176      """`410 Gone` error."""
    177      message = "gone"
    178 -    def __init__(self):
    179          status = '410 Gone'
    180          headers = {'Content-Type': 'text/html'}
    181          HTTPError.__init__(self, status, headers, self.message)
    182
    183  gone = Gone
    184
    185 -class _InternalError(HTTPError):
    186      """500 Internal Server Error`."""
    187      message = "internal server error"
    188
    189 -    def __init__(self, message=None):
    190          status = '500 Internal Server Error'
    191          headers = {'Content-Type': 'text/html'}
    192          HTTPError.__init__(self, status, headers, message or self.message)
    193
    194 -def InternalError(message=None):
    195      """Returns HTTPError with '500 internal error' error from the active application.
    196      """
    197      if message:
    198          return _InternalError(message)
    199      elif ctx.get('app_stack'):
    200          return ctx.app_stack[-1].internalerror()
    201      else:
    202          return _InternalError()
    203
    204  internalerror = InternalError
    205
    206 -def header(hdr, value, unique=False):
    207      """
    208      Adds the header `hdr: value` with the response.
    209
    210      If `unique` is True and a header with that name already exists,
    211      it doesn't add a new one.
    212      """
    213      hdr, value = utf8(hdr), utf8(value)
    214      # protection against HTTP response splitting attack
    215      if '\n' in hdr or '\r' in hdr or '\n' in value or '\r' in value:
    216          raise ValueError, 'invalid characters in header'
    217
    218      if unique is True:
    219          for h, v in ctx.headers:
    220              if h.lower() == hdr.lower(): return
    221
    222      ctx.headers.append((hdr, value))
    223
    224 -def rawinput(method=None):
    225      """Returns storage object with GET or POST arguments.
    226      """
    227      method = method or "both"
    228      from cStringIO import StringIO
    229
    230      def dictify(fs):
    231          # hack to make web.input work with enctype='text/plain.
    232          if fs.list is None:
    233              fs.list = []
    234
    235          return dict([(k, fs[k]) for k in fs.keys()])
    236
    237      e = ctx.env.copy()
    238      a = b = {}
    239
    240      if method.lower() in ['both', 'post', 'put']:
    241          if e['REQUEST_METHOD'] in ['POST', 'PUT']:
    242              if e.get('CONTENT_TYPE', '').lower().startswith('multipart/'):
    243                  # since wsgi.input is directly passed to cgi.FieldStorage,
    244                  # it can not be called multiple times. Saving the FieldStorage
    245                  # object in ctx to allow calling web.input multiple times.
    246                  a = ctx.get('_fieldstorage')
    247                  if not a:
    248                      fp = e['wsgi.input']
    249                      a = cgi.FieldStorage(fp=fp, environ=e, keep_blank_values=1)
    250                      ctx._fieldstorage = a
    251              else:
    252                  fp = StringIO(data())
    253                  a = cgi.FieldStorage(fp=fp, environ=e, keep_blank_values=1)
    254              a = dictify(a)
    255
    256      if method.lower() in ['both', 'get']:
    257          e['REQUEST_METHOD'] = 'GET'
    258          b = dictify(cgi.FieldStorage(environ=e, keep_blank_values=1))
    259
    260      def process_fieldstorage(fs):
    261          if isinstance(fs, list):
    262              return [process_fieldstorage(x) for x in fs]
    263          elif fs.filename is None:
    264              return fs.value
    265          else:
    266              return fs
    267
    268      return storage([(k, process_fieldstorage(v)) for k, v in dictadd(b, a).items()])
    269
    270 -def input(*requireds, **defaults):
    271      """
    272      Returns a `storage` object with the GET and POST arguments.
    273      See `storify` for how `requireds` and `defaults` work.
    274      """
    275      _method = defaults.pop('_method', 'both')
    276      out = rawinput(_method)
    277      try:
    278          defaults.setdefault('_unicode', True) # force unicode conversion by default.
    279          return storify(out, *requireds, **defaults)
    280      except KeyError:
    281          raise badrequest()
    282
    283 -def data():
    284      """Returns the data sent with the request."""
    285      if 'data' not in ctx:
    286          cl = intget(ctx.env.get('CONTENT_LENGTH'), 0)
    287          ctx.data = ctx.env['wsgi.input'].read(cl)
    288      return ctx.data
    289
    290 -def setcookie(name, value, expires="", domain=None, secure=False):
    291      """Sets a cookie."""
    292      if expires < 0:
    293          expires = -1000000000
    294      kargs = {'expires': expires, 'path':'/'}
    295      if domain:
    296          kargs['domain'] = domain
    297      if secure:
    298          kargs['secure'] = secure
    299      # @@ should we limit cookies to a different path?
    300      cookie = Cookie.SimpleCookie()
    301      cookie[name] = urllib.quote(utf8(value))
    302      for key, val in kargs.iteritems():
    303          cookie[name][key] = val
    304      header('Set-Cookie', cookie.items()[0][1].OutputString())
    305
    306 -def cookies(*requireds, **defaults):
    307      """
    308      Returns a `storage` object with all the cookies in it.
    309      See `storify` for how `requireds` and `defaults` work.
    310      """
    311      cookie = Cookie.SimpleCookie()
    312      cookie.load(ctx.env.get('HTTP_COOKIE', ''))
    313      try:
    314          d = storify(cookie, *requireds, **defaults)
    315          for k, v in d.items():
    316              d[k] = v and urllib.unquote(v)
    317          return d
    318      except KeyError:
    319          badrequest()
    320          raise StopIteration
    321
    322 -def debug(*args):
    323      """
    324      Prints a prettyprinted version of `args` to stderr.
    325      """
    326      try:
    327          out = ctx.environ['wsgi.errors']
    328      except:
    329          out = sys.stderr
    330      for arg in args:
    331          print >> out, pprint.pformat(arg)
    332      return ''
    333
    334 -def _debugwrite(x):
    335      try:
    336          out = ctx.environ['wsgi.errors']
    337      except:
    338          out = sys.stderr
    339      out.write(x)
    340  debug.write = _debugwrite
    341
    342  ctx = context = threadeddict()
    343
    344  ctx.__doc__ = """
    345  A `storage` object containing various information about the request:
    346
    347  `environ` (aka `env`)
    348     : A dictionary containing the standard WSGI environment variables.
    349
    350  `host`
    351     : The domain (`Host` header) requested by the user.
    352
    353  `home`
    354     : The base path for the application.
    355
    356  `ip`
    357     : The IP address of the requester.
    358
    359  `method`
    360     : The HTTP method used.
    361
    362  `path`
    363     : The path request.
    364
    365  `query`
    366     : If there are no query arguments, the empty string. Otherwise, a `?` followed
    367       by the query string.
    368
    369  `fullpath`
    370     : The full path requested, including query arguments (`== path + query`).
    371
    372  ### Response Data
    373
    374  `status` (default: "200 OK")
    375     : The status code to be used in the response.
    376
    377  `headers`
    378     : A list of 2-tuples to be used in the response.
    379
    380  `output`
    381     : A string to be used as the response.
    382  """
    383