source: publico/il.spdo/trunk/Paste-1.7.5.1-py2.6.egg/paste/wsgiwrappers.py @ 5327

Última Alteração nesse arquivo desde 5327 foi 5327, incluída por fabianosantos, 8 anos atrás

Import inicial.

File size: 21.6 KB
Linha 
1# (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org)
2# Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php
3"""WSGI Wrappers for a Request and Response
4
5The WSGIRequest and WSGIResponse objects are light wrappers to make it easier
6to deal with an incoming request and sending a response.
7"""
8import re
9import warnings
10from pprint import pformat
11from Cookie import SimpleCookie
12from paste.request import EnvironHeaders, get_cookie_dict, \
13    parse_dict_querystring, parse_formvars
14from paste.util.multidict import MultiDict, UnicodeMultiDict
15from paste.registry import StackedObjectProxy
16from paste.response import HeaderDict
17from paste.wsgilib import encode_unicode_app_iter
18from paste.httpheaders import ACCEPT_LANGUAGE
19from paste.util.mimeparse import desired_matches
20
21__all__ = ['WSGIRequest', 'WSGIResponse']
22
23_CHARSET_RE = re.compile(r';\s*charset=([^;]*)', re.I)
24
25class DeprecatedSettings(StackedObjectProxy):
26    def _push_object(self, obj):
27        warnings.warn('paste.wsgiwrappers.settings is deprecated: Please use '
28                      'paste.wsgiwrappers.WSGIRequest.defaults instead',
29                      DeprecationWarning, 3)
30        WSGIResponse.defaults._push_object(obj)
31        StackedObjectProxy._push_object(self, obj)
32
33# settings is deprecated: use WSGIResponse.defaults instead
34settings = DeprecatedSettings(default=dict())
35
36class environ_getter(object):
37    """For delegating an attribute to a key in self.environ."""
38    # @@: Also __set__?  Should setting be allowed?
39    def __init__(self, key, default='', default_factory=None):
40        self.key = key
41        self.default = default
42        self.default_factory = default_factory
43    def __get__(self, obj, type=None):
44        if type is None:
45            return self
46        if self.key not in obj.environ:
47            if self.default_factory:
48                val = obj.environ[self.key] = self.default_factory()
49                return val
50            else:
51                return self.default
52        return obj.environ[self.key]
53
54    def __repr__(self):
55        return '<Proxy for WSGI environ %r key>' % self.key
56
57class WSGIRequest(object):
58    """WSGI Request API Object
59
60    This object represents a WSGI request with a more friendly interface.
61    This does not expose every detail of the WSGI environment, and attempts
62    to express nothing beyond what is available in the environment
63    dictionary.
64
65    The only state maintained in this object is the desired ``charset``,
66    its associated ``errors`` handler, and the ``decode_param_names``
67    option.
68
69    The incoming parameter values will be automatically coerced to unicode
70    objects of the ``charset`` encoding when ``charset`` is set. The
71    incoming parameter names are not decoded to unicode unless the
72    ``decode_param_names`` option is enabled.
73
74    When unicode is expected, ``charset`` will overridden by the the
75    value of the ``Content-Type`` header's charset parameter if one was
76    specified by the client.
77
78    The class variable ``defaults`` specifies default values for
79    ``charset``, ``errors``, and ``langauge``. These can be overridden for the
80    current request via the registry.
81       
82    The ``language`` default value is considered the fallback during i18n
83    translations to ensure in odd cases that mixed languages don't occur should
84    the ``language`` file contain the string but not another language in the
85    accepted languages list. The ``language`` value only applies when getting
86    a list of accepted languages from the HTTP Accept header.
87   
88    This behavior is duplicated from Aquarium, and may seem strange but is
89    very useful. Normally, everything in the code is in "en-us".  However,
90    the "en-us" translation catalog is usually empty.  If the user requests
91    ``["en-us", "zh-cn"]`` and a translation isn't found for a string in
92    "en-us", you don't want gettext to fallback to "zh-cn".  You want it to
93    just use the string itself.  Hence, if a string isn't found in the
94    ``language`` catalog, the string in the source code will be used.
95
96    *All* other state is kept in the environment dictionary; this is
97    essential for interoperability.
98
99    You are free to subclass this object.
100
101    """
102    defaults = StackedObjectProxy(default=dict(charset=None, errors='replace',
103                                               decode_param_names=False,
104                                               language='en-us'))
105    def __init__(self, environ):
106        self.environ = environ
107        # This isn't "state" really, since the object is derivative:
108        self.headers = EnvironHeaders(environ)
109       
110        defaults = self.defaults._current_obj()
111        self.charset = defaults.get('charset')
112        if self.charset:
113            # There's a charset: params will be coerced to unicode. In that
114            # case, attempt to use the charset specified by the browser
115            browser_charset = self.determine_browser_charset()
116            if browser_charset:
117                self.charset = browser_charset
118        self.errors = defaults.get('errors', 'strict')
119        self.decode_param_names = defaults.get('decode_param_names', False)
120        self._languages = None
121   
122    body = environ_getter('wsgi.input')
123    scheme = environ_getter('wsgi.url_scheme')
124    method = environ_getter('REQUEST_METHOD')
125    script_name = environ_getter('SCRIPT_NAME')
126    path_info = environ_getter('PATH_INFO')
127
128    def urlvars(self):
129        """
130        Return any variables matched in the URL (e.g.,
131        ``wsgiorg.routing_args``).
132        """
133        if 'paste.urlvars' in self.environ:
134            return self.environ['paste.urlvars']
135        elif 'wsgiorg.routing_args' in self.environ:
136            return self.environ['wsgiorg.routing_args'][1]
137        else:
138            return {}
139    urlvars = property(urlvars, doc=urlvars.__doc__)
140   
141    def is_xhr(self):
142        """Returns a boolean if X-Requested-With is present and a XMLHttpRequest"""
143        return self.environ.get('HTTP_X_REQUESTED_WITH', '') == 'XMLHttpRequest'
144    is_xhr = property(is_xhr, doc=is_xhr.__doc__)
145   
146    def host(self):
147        """Host name provided in HTTP_HOST, with fall-back to SERVER_NAME"""
148        return self.environ.get('HTTP_HOST', self.environ.get('SERVER_NAME'))
149    host = property(host, doc=host.__doc__)
150
151    def languages(self):
152        """Return a list of preferred languages, most preferred first.
153       
154        The list may be empty.
155        """
156        if self._languages is not None:
157            return self._languages
158        acceptLanguage = self.environ.get('HTTP_ACCEPT_LANGUAGE')
159        langs = ACCEPT_LANGUAGE.parse(self.environ)
160        fallback = self.defaults.get('language', 'en-us')
161        if not fallback:
162            return langs
163        if fallback not in langs:
164            langs.append(fallback)
165        index = langs.index(fallback)
166        langs[index+1:] = []
167        self._languages = langs
168        return self._languages
169    languages = property(languages, doc=languages.__doc__)
170   
171    def _GET(self):
172        return parse_dict_querystring(self.environ)
173
174    def GET(self):
175        """
176        Dictionary-like object representing the QUERY_STRING
177        parameters. Always present, if possibly empty.
178
179        If the same key is present in the query string multiple times, a
180        list of its values can be retrieved from the ``MultiDict`` via
181        the ``getall`` method.
182
183        Returns a ``MultiDict`` container or a ``UnicodeMultiDict`` when
184        ``charset`` is set.
185        """
186        params = self._GET()
187        if self.charset:
188            params = UnicodeMultiDict(params, encoding=self.charset,
189                                      errors=self.errors,
190                                      decode_keys=self.decode_param_names)
191        return params
192    GET = property(GET, doc=GET.__doc__)
193
194    def _POST(self):
195        return parse_formvars(self.environ, include_get_vars=False)
196
197    def POST(self):
198        """Dictionary-like object representing the POST body.
199
200        Most values are encoded strings, or unicode strings when
201        ``charset`` is set. There may also be FieldStorage objects
202        representing file uploads. If this is not a POST request, or the
203        body is not encoded fields (e.g., an XMLRPC request) then this
204        will be empty.
205
206        This will consume wsgi.input when first accessed if applicable,
207        but the raw version will be put in
208        environ['paste.parsed_formvars'].
209
210        Returns a ``MultiDict`` container or a ``UnicodeMultiDict`` when
211        ``charset`` is set.
212        """
213        params = self._POST()
214        if self.charset:
215            params = UnicodeMultiDict(params, encoding=self.charset,
216                                      errors=self.errors,
217                                      decode_keys=self.decode_param_names)
218        return params
219    POST = property(POST, doc=POST.__doc__)
220
221    def params(self):
222        """Dictionary-like object of keys from POST, GET, URL dicts
223
224        Return a key value from the parameters, they are checked in the
225        following order: POST, GET, URL
226
227        Additional methods supported:
228
229        ``getlist(key)``
230            Returns a list of all the values by that key, collected from
231            POST, GET, URL dicts
232
233        Returns a ``MultiDict`` container or a ``UnicodeMultiDict`` when
234        ``charset`` is set.
235        """
236        params = MultiDict()
237        params.update(self._POST())
238        params.update(self._GET())
239        if self.charset:
240            params = UnicodeMultiDict(params, encoding=self.charset,
241                                      errors=self.errors,
242                                      decode_keys=self.decode_param_names)
243        return params
244    params = property(params, doc=params.__doc__)
245
246    def cookies(self):
247        """Dictionary of cookies keyed by cookie name.
248
249        Just a plain dictionary, may be empty but not None.
250       
251        """
252        return get_cookie_dict(self.environ)
253    cookies = property(cookies, doc=cookies.__doc__)
254
255    def determine_browser_charset(self):
256        """
257        Determine the encoding as specified by the browser via the
258        Content-Type's charset parameter, if one is set
259        """
260        charset_match = _CHARSET_RE.search(self.headers.get('Content-Type', ''))
261        if charset_match:
262            return charset_match.group(1)
263
264    def match_accept(self, mimetypes):
265        """Return a list of specified mime-types that the browser's HTTP Accept
266        header allows in the order provided."""
267        return desired_matches(mimetypes,
268                               self.environ.get('HTTP_ACCEPT', '*/*'))
269
270    def __repr__(self):
271        """Show important attributes of the WSGIRequest"""
272        pf = pformat
273        msg = '<%s.%s object at 0x%x method=%s,' % \
274            (self.__class__.__module__, self.__class__.__name__,
275             id(self), pf(self.method))
276        msg += '\nscheme=%s, host=%s, script_name=%s, path_info=%s,' % \
277            (pf(self.scheme), pf(self.host), pf(self.script_name),
278             pf(self.path_info))
279        msg += '\nlanguages=%s,' % pf(self.languages)
280        if self.charset:
281            msg += ' charset=%s, errors=%s,' % (pf(self.charset),
282                                                pf(self.errors))
283        msg += '\nGET=%s,' % pf(self.GET)
284        msg += '\nPOST=%s,' % pf(self.POST)
285        msg += '\ncookies=%s>' % pf(self.cookies)
286        return msg
287
288class WSGIResponse(object):
289    """A basic HTTP response with content, headers, and out-bound cookies
290
291    The class variable ``defaults`` specifies default values for
292    ``content_type``, ``charset`` and ``errors``. These can be overridden
293    for the current request via the registry.
294
295    """
296    defaults = StackedObjectProxy(
297        default=dict(content_type='text/html', charset='utf-8',
298                     errors='strict', headers={'Cache-Control':'no-cache'})
299        )
300    def __init__(self, content='', mimetype=None, code=200):
301        self._iter = None
302        self._is_str_iter = True
303
304        self.content = content
305        self.headers = HeaderDict()
306        self.cookies = SimpleCookie()
307        self.status_code = code
308
309        defaults = self.defaults._current_obj()
310        if not mimetype:
311            mimetype = defaults.get('content_type', 'text/html')
312            charset = defaults.get('charset')
313            if charset:
314                mimetype = '%s; charset=%s' % (mimetype, charset)
315        self.headers.update(defaults.get('headers', {}))
316        self.headers['Content-Type'] = mimetype
317        self.errors = defaults.get('errors', 'strict')
318
319    def __str__(self):
320        """Returns a rendition of the full HTTP message, including headers.
321
322        When the content is an iterator, the actual content is replaced with the
323        output of str(iterator) (to avoid exhausting the iterator).
324        """
325        if self._is_str_iter:
326            content = ''.join(self.get_content())
327        else:
328            content = str(self.content)
329        return '\n'.join(['%s: %s' % (key, value)
330            for key, value in self.headers.headeritems()]) \
331            + '\n\n' + content
332   
333    def __call__(self, environ, start_response):
334        """Convenience call to return output and set status information
335       
336        Conforms to the WSGI interface for calling purposes only.
337       
338        Example usage:
339       
340        .. code-block:: python
341
342            def wsgi_app(environ, start_response):
343                response = WSGIResponse()
344                response.write("Hello world")
345                response.headers['Content-Type'] = 'latin1'
346                return response(environ, start_response)
347       
348        """
349        status_text = STATUS_CODE_TEXT[self.status_code]
350        status = '%s %s' % (self.status_code, status_text)
351        response_headers = self.headers.headeritems()
352        for c in self.cookies.values():
353            response_headers.append(('Set-Cookie', c.output(header='')))
354        start_response(status, response_headers)
355        is_file = isinstance(self.content, file)
356        if 'wsgi.file_wrapper' in environ and is_file:
357            return environ['wsgi.file_wrapper'](self.content)
358        elif is_file:
359            return iter(lambda: self.content.read(), '')
360        return self.get_content()
361   
362    def determine_charset(self):
363        """
364        Determine the encoding as specified by the Content-Type's charset
365        parameter, if one is set
366        """
367        charset_match = _CHARSET_RE.search(self.headers.get('Content-Type', ''))
368        if charset_match:
369            return charset_match.group(1)
370   
371    def has_header(self, header):
372        """
373        Case-insensitive check for a header
374        """
375        warnings.warn('WSGIResponse.has_header is deprecated, use '
376                      'WSGIResponse.headers.has_key instead', DeprecationWarning,
377                      2)
378        return self.headers.has_key(header)
379
380    def set_cookie(self, key, value='', max_age=None, expires=None, path='/',
381                   domain=None, secure=None, httponly=None):
382        """
383        Define a cookie to be sent via the outgoing HTTP headers
384        """
385        self.cookies[key] = value
386        for var_name, var_value in [
387            ('max_age', max_age), ('path', path), ('domain', domain),
388            ('secure', secure), ('expires', expires), ('httponly', httponly)]:
389            if var_value is not None and var_value is not False:
390                self.cookies[key][var_name.replace('_', '-')] = var_value
391
392    def delete_cookie(self, key, path='/', domain=None):
393        """
394        Notify the browser the specified cookie has expired and should be
395        deleted (via the outgoing HTTP headers)
396        """
397        self.cookies[key] = ''
398        if path is not None:
399            self.cookies[key]['path'] = path
400        if domain is not None:
401            self.cookies[key]['domain'] = domain
402        self.cookies[key]['expires'] = 0
403        self.cookies[key]['max-age'] = 0
404
405    def _set_content(self, content):
406        if hasattr(content, '__iter__'):
407            self._iter = content
408            if isinstance(content, list):
409                self._is_str_iter = True
410            else:
411                self._is_str_iter = False
412        else:
413            self._iter = [content]
414            self._is_str_iter = True
415    content = property(lambda self: self._iter, _set_content,
416                       doc='Get/set the specified content, where content can '
417                       'be: a string, a list of strings, a generator function '
418                       'that yields strings, or an iterable object that '
419                       'produces strings.')
420
421    def get_content(self):
422        """
423        Returns the content as an iterable of strings, encoding each element of
424        the iterator from a Unicode object if necessary.
425        """
426        charset = self.determine_charset()
427        if charset:
428            return encode_unicode_app_iter(self.content, charset, self.errors)
429        else:
430            return self.content
431   
432    def wsgi_response(self):
433        """
434        Return this WSGIResponse as a tuple of WSGI formatted data, including:
435        (status, headers, iterable)
436        """
437        status_text = STATUS_CODE_TEXT[self.status_code]
438        status = '%s %s' % (self.status_code, status_text)
439        response_headers = self.headers.headeritems()
440        for c in self.cookies.values():
441            response_headers.append(('Set-Cookie', c.output(header='')))
442        return status, response_headers, self.get_content()
443   
444    # The remaining methods partially implement the file-like object interface.
445    # See http://docs.python.org/lib/bltin-file-objects.html
446    def write(self, content):
447        if not self._is_str_iter:
448            raise IOError, "This %s instance's content is not writable: (content " \
449                'is an iterator)' % self.__class__.__name__
450        self.content.append(content)
451
452    def flush(self):
453        pass
454
455    def tell(self):
456        if not self._is_str_iter:
457            raise IOError, 'This %s instance cannot tell its position: (content ' \
458                'is an iterator)' % self.__class__.__name__
459        return sum([len(chunk) for chunk in self._iter])
460
461    ########################################
462    ## Content-type and charset
463
464    def charset__get(self):
465        """
466        Get/set the charset (in the Content-Type)
467        """
468        header = self.headers.get('content-type')
469        if not header:
470            return None
471        match = _CHARSET_RE.search(header)
472        if match:
473            return match.group(1)
474        return None
475
476    def charset__set(self, charset):
477        if charset is None:
478            del self.charset
479            return
480        try:
481            header = self.headers.pop('content-type')
482        except KeyError:
483            raise AttributeError(
484                "You cannot set the charset when no content-type is defined")
485        match = _CHARSET_RE.search(header)
486        if match:
487            header = header[:match.start()] + header[match.end():]
488        header += '; charset=%s' % charset
489        self.headers['content-type'] = header
490
491    def charset__del(self):
492        try:
493            header = self.headers.pop('content-type')
494        except KeyError:
495            # Don't need to remove anything
496            return
497        match = _CHARSET_RE.search(header)
498        if match:
499            header = header[:match.start()] + header[match.end():]
500        self.headers['content-type'] = header
501
502    charset = property(charset__get, charset__set, charset__del, doc=charset__get.__doc__)
503
504    def content_type__get(self):
505        """
506        Get/set the Content-Type header (or None), *without* the
507        charset or any parameters.
508
509        If you include parameters (or ``;`` at all) when setting the
510        content_type, any existing parameters will be deleted;
511        otherwise they will be preserved.
512        """
513        header = self.headers.get('content-type')
514        if not header:
515            return None
516        return header.split(';', 1)[0]
517
518    def content_type__set(self, value):
519        if ';' not in value:
520            header = self.headers.get('content-type', '')
521            if ';' in header:
522                params = header.split(';', 1)[1]
523                value += ';' + params
524        self.headers['content-type'] = value
525
526    def content_type__del(self):
527        try:
528            del self.headers['content-type']
529        except KeyError:
530            pass
531
532    content_type = property(content_type__get, content_type__set,
533                            content_type__del, doc=content_type__get.__doc__)
534
535## @@ I'd love to remove this, but paste.httpexceptions.get_exception
536##    doesn't seem to work...
537# See http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
538STATUS_CODE_TEXT = {
539    100: 'CONTINUE',
540    101: 'SWITCHING PROTOCOLS',
541    200: 'OK',
542    201: 'CREATED',
543    202: 'ACCEPTED',
544    203: 'NON-AUTHORITATIVE INFORMATION',
545    204: 'NO CONTENT',
546    205: 'RESET CONTENT',
547    206: 'PARTIAL CONTENT',
548    226: 'IM USED',
549    300: 'MULTIPLE CHOICES',
550    301: 'MOVED PERMANENTLY',
551    302: 'FOUND',
552    303: 'SEE OTHER',
553    304: 'NOT MODIFIED',
554    305: 'USE PROXY',
555    306: 'RESERVED',
556    307: 'TEMPORARY REDIRECT',
557    400: 'BAD REQUEST',
558    401: 'UNAUTHORIZED',
559    402: 'PAYMENT REQUIRED',
560    403: 'FORBIDDEN',
561    404: 'NOT FOUND',
562    405: 'METHOD NOT ALLOWED',
563    406: 'NOT ACCEPTABLE',
564    407: 'PROXY AUTHENTICATION REQUIRED',
565    408: 'REQUEST TIMEOUT',
566    409: 'CONFLICT',
567    410: 'GONE',
568    411: 'LENGTH REQUIRED',
569    412: 'PRECONDITION FAILED',
570    413: 'REQUEST ENTITY TOO LARGE',
571    414: 'REQUEST-URI TOO LONG',
572    415: 'UNSUPPORTED MEDIA TYPE',
573    416: 'REQUESTED RANGE NOT SATISFIABLE',
574    417: 'EXPECTATION FAILED',
575    500: 'INTERNAL SERVER ERROR',
576    501: 'NOT IMPLEMENTED',
577    502: 'BAD GATEWAY',
578    503: 'SERVICE UNAVAILABLE',
579    504: 'GATEWAY TIMEOUT',
580    505: 'HTTP VERSION NOT SUPPORTED',
581}
Note: Veja TracBrowser para ajuda no uso do navegador do trac.
 

The contents and data of this website are published under license:
Creative Commons 4.0 Brasil - Atribuir Fonte - Compartilhar Igual.