1
from cStringIO import StringIO
8
from Cookie import BaseCookie
9
from rfc822 import parsedate_tz, mktime_tz, formatdate
10
from datetime import datetime, date, timedelta, tzinfo
15
from webob.datastruct import EnvironHeaders
16
from webob.multidict import MultiDict, UnicodeMultiDict, NestedMultiDict, NoVars
17
from webob.etag import AnyETag, NoETag, ETagMatcher, IfRange, NoIfRange
18
from webob.headerdict import HeaderDict
19
from webob.statusreasons import status_reasons
20
from webob.cachecontrol import CacheControl, serialize_cache_control
21
from webob.acceptparse import Accept, MIMEAccept, NilAccept, MIMENilAccept, NoAccept
22
from webob.byterange import Range, ContentRange
24
_CHARSET_RE = re.compile(r';\s*charset=([^;]*)', re.I)
25
_SCHEME_RE = re.compile(r'^[a-z]+:', re.I)
26
_PARAM_RE = re.compile(r'([a-z0-9]+)=(?:"([^"]*)"|([a-z0-9_.-]*))', re.I)
27
_OK_PARAM_RE = re.compile(r'^[a-z0-9_.-]+$', re.I)
29
__all__ = ['Request', 'Response', 'UTC', 'day', 'week', 'hour', 'minute', 'second', 'month', 'year', 'html_escape']
34
def utcoffset(self, dt):
44
"""HTML-escape a string or object
46
This converts any non-string objects passed into it to strings
47
(actually, using ``unicode()``). All values returned are
48
non-unicode strings (using ``&#num;`` entities for all non-ASCII
51
None is treated specially, and returns the empty string.
55
if not isinstance(s, basestring):
56
if hasattr(s, '__unicode__'):
60
s = cgi.escape(s, True)
61
if isinstance(s, unicode):
62
s = s.encode('ascii', 'xmlcharrefreplace')
65
def timedelta_to_seconds(td):
67
Converts a timedelta instance to seconds.
69
return td.seconds + (td.days*24*60*60)
71
day = timedelta(days=1)
72
week = timedelta(weeks=1)
73
hour = timedelta(hours=1)
74
minute = timedelta(minutes=1)
75
second = timedelta(seconds=1)
76
# Estimate, I know; good enough for expirations
77
month = timedelta(days=30)
78
year = timedelta(days=365)
83
NoDefault = _NoDefault()
85
class environ_getter(object):
86
"""For delegating an attribute to a key in self.environ."""
88
def __init__(self, key, default='', default_factory=None,
89
settable=True, deletable=True, doc=None,
92
self.default = default
93
self.default_factory = default_factory
94
self.settable = settable
95
self.deletable = deletable
98
docstring += " and sets"
100
docstring += " and deletes"
101
docstring += " the %r key from the environment." % self.key
102
docstring += _rfc_reference(self.key, rfc_section)
104
docstring += '\n\n' + textwrap.dedent(doc)
105
self.__doc__ = docstring
107
def __get__(self, obj, type=None):
110
if self.key not in obj.environ:
111
if self.default_factory:
112
val = obj.environ[self.key] = self.default_factory()
116
return obj.environ[self.key]
118
def __set__(self, obj, value):
119
if not self.settable:
120
raise AttributeError("Read-only attribute (key %r)" % self.key)
122
if self.key in obj.environ:
123
del obj.environ[self.key]
125
obj.environ[self.key] = value
127
def __delete__(self, obj):
128
if not self.deletable:
129
raise AttributeError("You cannot delete the key %r" % self.key)
130
del obj.environ[self.key]
133
return '<Proxy for WSGI environ %r key>' % self.key
135
class header_getter(object):
136
"""For delegating an attribute to a header in self.headers"""
138
def __init__(self, header, default=None,
139
settable=True, deletable=True, doc=None, rfc_section=None):
141
self.default = default
142
self.settable = settable
143
self.deletable = deletable
146
docstring += " and sets"
148
docstring += " and deletes"
149
docstring += " they header %s from the headers" % self.header
150
docstring += _rfc_reference(self.header, rfc_section)
152
docstring += '\n\n' + textwrap.dedent(doc)
153
self.__doc__ = docstring
155
def __get__(self, obj, type=None):
158
if self.header not in obj.headers:
161
return obj.headers[self.header]
163
def __set__(self, obj, value):
164
if not self.settable:
165
raise AttributeError("Read-only attribute (header %s)" % self.header)
167
if self.header in obj.headers:
168
del obj.headers[self.header]
170
obj.headers[self.header] = value
172
def __delete__(self, obj):
173
if not self.deletable:
174
raise AttributeError("You cannot delete the header %s" % self.header)
175
del obj.headers[self.header]
178
return '<Proxy for header %s>' % self.header
180
class converter(object):
182
Wraps a decorator, and applies conversion for that decorator
184
def __init__(self, decorator, getter_converter, setter_converter, convert_name=None, doc=None, converter_args=()):
185
self.decorator = decorator
186
self.getter_converter = getter_converter
187
self.setter_converter = setter_converter
188
self.convert_name = convert_name
189
self.converter_args = converter_args
190
docstring = decorator.__doc__ or ''
191
docstring += " Converts it as a "
193
docstring += convert_name + '.'
195
docstring += "%r and %r." % (getter_converter, setter_converter)
197
docstring += '\n\n' + textwrap.dedent(doc)
198
self.__doc__ = docstring
200
def __get__(self, obj, type=None):
203
value = self.decorator.__get__(obj, type)
204
return self.getter_converter(value, *self.converter_args)
206
def __set__(self, obj, value):
207
value = self.setter_converter(value, *self.converter_args)
208
self.decorator.__set__(obj, value)
210
def __delete__(self, obj):
211
self.decorator.__delete__(obj)
214
if self.convert_name:
215
name = ' %s' % self.convert_name
218
return '<Converted %r%s>' % (self.decorator, name)
220
def _rfc_reference(header, section):
223
major_section = section.split('.')[0]
224
link = 'http://www.w3.org/Protocols/rfc2616/rfc2616-sec%s.html#sec%s' % (
225
major_section, section)
226
if header.startswith('HTTP_'):
227
header = header[5:].title().replace('_', '-')
228
return " For more information on %s see `section %s <%s>`_." % (
229
header, section, link)
231
class deprecated_property(object):
233
Wraps a decorator, with a deprecation warning or error
235
def __init__(self, decorator, attr, message, warning=True):
236
self.decorator = decorator
238
self.message = message
239
self.warning = warning
241
def __get__(self, obj, type=None):
245
return self.decorator.__get__(obj, type)
247
def __set__(self, obj, value):
249
self.decorator.__set__(obj, value)
251
def __delete__(self, obj):
253
self.decorator.__delete__(obj)
256
return '<Deprecated attribute %s: %r>' % (
262
raise DeprecationWarning(
263
'The attribute %s is deprecated: %s' % (self.attr, self.message))
266
'The attribute %s is deprecated: %s' % (self.attr, self.message),
270
def _parse_date(value):
273
t = parsedate_tz(value)
278
return datetime.fromtimestamp(t, UTC)
280
def _serialize_date(dt):
283
if isinstance(dt, unicode):
284
dt = dt.encode('ascii')
285
if isinstance(dt, str):
287
if isinstance(dt, timedelta):
288
dt = datetime.now() + dt
289
if isinstance(dt, (datetime, date)):
291
if isinstance(dt, (tuple, time.struct_time)):
292
dt = calendar.timegm(dt)
293
if not isinstance(dt, (float, int)):
295
"You must pass in a datetime, date, time tuple, or integer object, not %r" % dt)
296
return formatdate(dt)
298
def _parse_date_delta(value):
300
like _parse_date, but also handle delta seconds
309
delta = timedelta(seconds=value)
310
return datetime.now() + delta
311
return _parse_date(value)
313
def _serialize_date_delta(value):
314
if not value and value != 0:
316
if isinstance(value, (float, int)):
317
return str(int(value))
318
return _serialize_date(value)
320
def _parse_etag(value, default=True):
323
value = value.strip()
332
return ETagMatcher.parse(value)
334
def _serialize_etag(value, default=True):
344
def _parse_if_range(value):
348
return IfRange.parse(value)
350
def _serialize_if_range(value):
353
if isinstance(value, (datetime, date)):
354
return _serialize_date(value)
355
if not isinstance(value, str):
359
def _parse_range(value):
362
# Might return None too:
363
return Range.parse(value)
365
def _serialize_range(value):
366
if isinstance(value, (list, tuple)):
369
"If setting .range to a list or tuple, it must be of length 2 (not %r)"
371
value = Range([value])
377
def _parse_int(value):
378
if value is None or value == '':
382
def _parse_int_safe(value):
383
if value is None or value == '':
390
def _serialize_int(value):
395
def _parse_content_range(value):
396
if not value or not value.strip():
398
# May still return None
399
return ContentRange.parse(value)
401
def _serialize_content_range(value):
404
if isinstance(value, (tuple, list)):
405
if len(value) not in (2, 3):
407
"When setting content_range to a list/tuple, it must "
408
"be length 2 or 3 (not %r)" % value)
413
begin, end, length = value
414
value = ContentRange(begin, end, length)
415
value = str(value).strip()
420
def _parse_list(value):
423
value = value.strip()
426
return [v.strip() for v in value.split(',')
429
def _serialize_list(value):
432
if isinstance(value, unicode):
434
if isinstance(value, str):
436
return ', '.join(map(str, value))
438
def _parse_accept(value, header_name, AcceptClass, NilClass):
440
return NilClass(header_name)
441
return AcceptClass(header_name, value)
443
def _serialize_accept(value, header_name, AcceptClass, NilClass):
444
if not value or isinstance(value, NilClass):
446
if isinstance(value, (list, tuple, dict)):
447
value = NilClass(header_name) + value
448
value = str(value).strip()
453
class Request(object):
457
unicode_errors = 'strict'
458
decode_param_names = False
459
## The limit after which request bodies should be stored on disk
460
## if they are read in (under this, and the request body is stored
462
request_body_tempfile_limit = 10*1024
464
def __init__(self, environ=None, environ_getter=None, charset=NoDefault, unicode_errors=NoDefault,
465
decode_param_names=NoDefault):
466
if environ is None and environ_getter is None:
468
"You must provide one of environ or environ_getter")
469
if environ is not None and environ_getter is not None:
471
"You can only provide one of the environ and environ_getter arguments")
473
self._environ_getter = environ_getter
475
if not isinstance(environ, dict):
477
"Bad type for environ: %s" % type(environ))
478
self._environ = environ
479
if charset is not NoDefault:
480
self.__dict__['charset'] = charset
481
if unicode_errors is not NoDefault:
482
self.__dict__['unicode_errors'] = unicode_errors
483
if decode_param_names is not NoDefault:
484
self.__dict__['decode_param_names'] = decode_param_names
486
def __setattr__(self, attr, value, DEFAULT=[]):
487
## FIXME: I don't know why I need this guard (though experimentation says I do)
488
if getattr(self.__class__, attr, DEFAULT) is not DEFAULT or attr.startswith('_'):
489
object.__setattr__(self, attr, value)
491
self.environ.setdefault('webob.adhoc_attrs', {})[attr] = value
493
def __getattr__(self, attr):
494
## FIXME: I don't know why I need this guard (though experimentation says I do)
495
if attr in self.__class__.__dict__:
496
return object.__getattribute__(self, attr)
498
return self.environ['webob.adhoc_attrs'][attr]
500
raise AttributeError(attr)
502
def __delattr__(self, attr):
503
## FIXME: I don't know why I need this guard (though experimentation says I do)
504
if attr in self.__class__.__dict__:
505
return object.__delattr__(self, attr)
507
del self.environ['webob.adhoc_attrs'][attr]
509
raise AttributeError(attr)
513
The WSGI environment dictionary for this request
515
return self._environ_getter()
516
environ = property(environ, doc=environ.__doc__)
518
def _environ_getter(self):
521
def _body_file__get(self):
523
Access the body of the request (wsgi.input) as a file-like
526
If you set this value, CONTENT_LENGTH will also be updated
527
(either set to -1, 0 if you delete the attribute, or if you
528
set the attribute to a string then the length of the string).
530
return self.environ['wsgi.input']
531
def _body_file__set(self, value):
532
if isinstance(value, str):
534
value = StringIO(value)
537
self.environ['wsgi.input'] = value
538
self.environ['CONTENT_LENGTH'] = str(length)
539
def _body_file__del(self):
540
self.environ['wsgi.input'] = StringIO('')
541
self.environ['CONTENT_LENGTH'] = '0'
542
body_file = property(_body_file__get, _body_file__set, _body_file__del, doc=_body_file__get.__doc__)
544
scheme = environ_getter('wsgi.url_scheme')
545
method = environ_getter('REQUEST_METHOD')
546
script_name = environ_getter('SCRIPT_NAME')
547
path_info = environ_getter('PATH_INFO')
548
## FIXME: should I strip out parameters?:
549
content_type = environ_getter('CONTENT_TYPE', rfc_section='14.17')
550
content_length = converter(
551
environ_getter('CONTENT_LENGTH', rfc_section='14.13'),
552
_parse_int_safe, _serialize_int, 'int')
553
remote_user = environ_getter('REMOTE_USER', default=None)
554
remote_addr = environ_getter('REMOTE_ADDR', default=None)
555
query_string = environ_getter('QUERY_STRING')
556
server_name = environ_getter('SERVER_NAME')
557
server_port = converter(
558
environ_getter('SERVER_PORT'),
559
_parse_int, _serialize_int, 'int')
563
def _headers__get(self):
565
All the request headers as a case-insensitive dictionary-like
568
if self._headers is None:
569
self._headers = EnvironHeaders(self.environ)
572
def _headers__set(self, value):
574
self.headers.update(value)
576
headers = property(_headers__get, _headers__set, doc=_headers__get.__doc__)
580
The URL through the host (no path)
583
url = e['wsgi.url_scheme'] + '://'
584
if e.get('HTTP_HOST'):
585
host = e['HTTP_HOST']
587
host, port = host.split(':', 1)
592
host = e['SERVER_NAME']
593
port = e['SERVER_PORT']
594
if self.environ['wsgi.url_scheme'] == 'https':
597
elif self.environ['wsgi.url_scheme'] == 'http':
604
host_url = property(host_url, doc=host_url.__doc__)
606
def application_url(self):
608
The URL including SCRIPT_NAME (no PATH_INFO or query string)
610
return self.host_url + urllib.quote(self.environ.get('SCRIPT_NAME', ''))
611
application_url = property(application_url, doc=application_url.__doc__)
615
The URL including SCRIPT_NAME and PATH_INFO, but not QUERY_STRING
617
return self.application_url + urllib.quote(self.environ.get('PATH_INFO', ''))
618
path_url = property(path_url, doc=path_url.__doc__)
622
The path of the request, without host or query string
624
return urllib.quote(self.script_name) + urllib.quote(self.path_info)
625
path = property(path, doc=path.__doc__)
629
The path of the request, without host but with query string
632
qs = self.environ.get('QUERY_STRING')
636
path_qs = property(path_qs, doc=path_qs.__doc__)
640
The full request URL, including QUERY_STRING
643
if self.environ.get('QUERY_STRING'):
644
url += '?' + self.environ['QUERY_STRING']
646
url = property(url, doc=url.__doc__)
648
def relative_url(self, other_url, to_application=False):
650
Resolve other_url relative to the request URL.
652
If ``to_application`` is True, then resolve it relative to the
653
URL with only SCRIPT_NAME
656
url = self.application_url
657
if not url.endswith('/'):
661
return urlparse.urljoin(url, other_url)
663
def path_info_pop(self):
665
'Pops' off the next segment of PATH_INFO, pushing it onto
666
SCRIPT_NAME, and returning the popped segment. Returns None if
667
there is nothing left on PATH_INFO.
669
Does not return ``''`` when there's an empty segment (like
670
``/path//path``); these segments are just ignored.
672
path = self.path_info
675
while path.startswith('/'):
676
self.script_name += '/'
679
self.script_name += path
683
segment, path = path.split('/', 1)
684
self.path_info = '/' + path
685
self.script_name += segment
688
def path_info_peek(self):
690
Returns the next segment on PATH_INFO, or None if there is no
691
next segment. Doesn't modify the environment.
693
path = self.path_info
696
path = path.lstrip('/')
697
return path.split('/', 1)[0]
699
def _urlvars__get(self):
701
Return any *named* variables matched in the URL.
703
Takes values from ``environ['wsgiorg.routing_args']``.
704
Systems like ``routes`` set this value.
706
if 'paste.urlvars' in self.environ:
707
return self.environ['paste.urlvars']
708
elif 'wsgiorg.routing_args' in self.environ:
709
return self.environ['wsgiorg.routing_args'][1]
712
self.environ['wsgiorg.routing_args'] = ((), result)
715
def _urlvars__set(self, value):
716
environ = self.environ
717
if 'wsgiorg.routing_args' in environ:
718
environ['wsgiorg.routing_args'] = (environ['wsgiorg.routing_args'][0], value)
719
if 'paste.urlvars' in environ:
720
del environ['paste.urlvars']
721
elif 'paste.urlvars' in environ:
722
environ['paste.urlvars'] = value
724
environ['wsgiorg.routing_args'] = ((), value)
726
def _urlvars__del(self):
727
if 'paste.urlvars' in self.environ:
728
del self.environ['paste.urlvars']
729
if 'wsgiorg.routing_args' in self.environ:
730
if not self.environ['wsgiorg.routing_args'][0]:
731
del self.environ['wsgiorg.routing_args']
733
self.environ['wsgiorg.routing_args'] = (self.environ['wsgiorg.routing_args'][0], {})
735
urlvars = property(_urlvars__get, _urlvars__set, _urlvars__del, doc=_urlvars__get.__doc__)
737
def _urlargs__get(self):
739
Return any *positional* variables matched in the URL.
741
Takes values from ``environ['wsgiorg.routing_args']``.
742
Systems like ``routes`` set this value.
744
if 'wsgiorg.routing_args' in self.environ:
745
return self.environ['wsgiorg.routing_args'][0]
747
# Since you can't update this value in-place, we don't need
748
# to set the key in the environment
751
def _urlargs__set(self, value):
752
environ = self.environ
753
if 'paste.urlvars' in environ:
754
# Some overlap between this and wsgiorg.routing_args; we need
755
# wsgiorg.routing_args to make this work
756
routing_args = (value, environ.pop('paste.urlvars'))
757
elif 'wsgiorg.routing_args' in environ:
758
routing_args = (value, environ['wsgiorg.routing_args'][1])
760
routing_args = (value, {})
761
environ['wsgiorg.routing_args'] = routing_args
763
def _urlargs__del(self):
764
if 'wsgiorg.routing_args' in self.environ:
765
if not self.environ['wsgiorg.routing_args'][1]:
766
del self.environ['wsgiorg.routing_args']
768
self.environ['wsgiorg.routing_args'] = ((), self.environ['wsgiorg.routing_args'][1])
770
urlargs = property(_urlargs__get, _urlargs__set, _urlargs__del, _urlargs__get.__doc__)
773
"""Returns a boolean if X-Requested-With is present and ``XMLHttpRequest``
775
Note: this isn't set by every XMLHttpRequest request, it is
776
only set if you are using a Javascript library that sets it
777
(or you set the header yourself manually). Currently
778
Prototype and jQuery are known to set this header."""
779
return self.environ.get('HTTP_X_REQUESTED_WITH', '') == 'XMLHttpRequest'
780
is_xhr = property(is_xhr, doc=is_xhr.__doc__)
782
def _host__get(self):
783
"""Host name provided in HTTP_HOST, with fall-back to SERVER_NAME"""
784
if 'HTTP_HOST' in self.environ:
785
return self.environ['HTTP_HOST']
787
return '%(SERVER_NAME)s:%(SERVER_PORT)s' % self.environ
788
def _host__set(self, value):
789
self.environ['HTTP_HOST'] = value
790
def _host__del(self):
791
if 'HTTP_HOST' in self.environ:
792
del self.environ['HTTP_HOST']
793
host = property(_host__get, _host__set, _host__del, doc=_host__get.__doc__)
795
def _body__get(self):
797
Return the content of the request body.
800
length = int(self.environ.get('CONTENT_LENGTH', '0'))
803
c = self.body_file.read(length)
804
tempfile_limit = self.request_body_tempfile_limit
805
if tempfile_limit and len(c) > tempfile_limit:
806
fileobj = tempfile.TemporaryFile()
810
fileobj = StringIO(c)
811
# We don't want/need to lose CONTENT_LENGTH here (as setting
812
# self.body_file would do):
813
self.environ['wsgi.input'] = fileobj
816
def _body__set(self, value):
820
if not isinstance(value, str):
822
"You can only set Request.body to a str (not %r)" % type(value))
823
body_file = StringIO(value)
824
self.body_file = body_file
825
self.environ['CONTENT_LENGTH'] = str(len(value))
827
def _body__del(self, value):
830
body = property(_body__get, _body__set, _body__del, doc=_body__get.__doc__)
834
Return a MultiDict containing all the variables from a POST
835
form request. Does *not* return anything for non-POST
836
requests or for non-form requests (returns empty dict-like
837
object in that case).
840
if self.method != 'POST':
841
return NoVars('Not a POST request')
842
if 'webob._parsed_post_vars' in env:
843
vars, body_file = env['webob._parsed_post_vars']
844
if body_file is self.body_file:
846
# Paste compatibility:
847
if 'paste.parsed_formvars' in env:
848
# from paste.request.parse_formvars
849
vars, body_file = env['paste.parsed_formvars']
850
if body_file is self.body_file:
851
# FIXME: is it okay that this isn't *our* MultiDict?
853
content_type = self.content_type
854
if ';' in content_type:
855
content_type = content_type.split(';', 1)[0]
856
if content_type not in ('', 'application/x-www-form-urlencoded',
857
'multipart/form-data'):
858
# Not an HTML form submission
859
return NoVars('Not an HTML form submission (Content-Type: %s)'
861
if 'CONTENT_LENGTH' not in env:
862
# FieldStorage assumes a default CONTENT_LENGTH of -1, but a
863
# default of 0 is better:
864
env['CONTENT_TYPE'] = '0'
865
fs_environ = env.copy()
866
fs_environ['QUERY_STRING'] = ''
867
fs = cgi.FieldStorage(fp=self.body_file,
869
keep_blank_values=True)
870
vars = MultiDict.from_fieldstorage(fs)
871
FakeCGIBody.update_environ(env, vars)
872
env['webob._parsed_post_vars'] = (vars, self.body_file)
875
str_POST = property(str_POST, doc=str_POST.__doc__)
877
str_postvars = deprecated_property(str_POST, 'str_postvars',
878
'use str_POST instead')
882
Like ``.str_POST``, but may decode values and keys
886
vars = UnicodeMultiDict(vars, encoding=self.charset,
887
errors=self.unicode_errors,
888
decode_keys=self.decode_param_names)
891
POST = property(POST, doc=POST.__doc__)
893
postvars = deprecated_property(POST, 'postvars',
898
Return a MultiDict containing all the variables from the
902
source = env.get('QUERY_STRING', '')
903
if 'webob._parsed_query_vars' in env:
904
vars, qs = env['webob._parsed_query_vars']
910
vars = MultiDict(cgi.parse_qsl(
911
source, keep_blank_values=True,
912
strict_parsing=False))
913
env['webob._parsed_query_vars'] = (vars, source)
916
str_GET = property(str_GET, doc=str_GET.__doc__)
918
str_queryvars = deprecated_property(str_GET, 'str_queryvars',
919
'use str_GET instead')
924
Like ``.str_GET``, but may decode values and keys
928
vars = UnicodeMultiDict(vars, encoding=self.charset,
929
errors=self.unicode_errors,
930
decode_keys=self.decode_param_names)
933
GET = property(GET, doc=GET.__doc__)
935
queryvars = deprecated_property(GET, 'queryvars',
938
def str_params(self):
940
A dictionary-like object containing both the parameters from
941
the query string and request body.
943
return NestedMultiDict(self.str_GET, self.str_POST)
945
str_params = property(str_params, doc=str_params.__doc__)
949
Like ``.str_params``, but may decode values and keys
951
params = self.str_params
953
params = UnicodeMultiDict(params, encoding=self.charset,
954
errors=self.unicode_errors,
955
decode_keys=self.decode_param_names)
958
params = property(params, doc=params.__doc__)
960
def str_cookies(self):
962
Return a *plain* dictionary of cookies as found in the request.
965
source = env.get('HTTP_COOKIE', '')
966
if 'webob._parsed_cookies' in env:
967
vars, var_source = env['webob._parsed_cookies']
968
if var_source == source:
972
cookies = BaseCookie()
975
vars[name] = cookies[name].value
976
env['webob._parsed_cookies'] = (vars, source)
979
str_cookies = property(str_cookies, doc=str_cookies.__doc__)
983
Like ``.str_cookies``, but may decode values and keys
985
vars = self.str_cookies
987
vars = UnicodeMultiDict(vars, encoding=self.charset,
988
errors=self.unicode_errors,
989
decode_keys=self.decode_param_names)
992
cookies = property(cookies, doc=cookies.__doc__)
996
Copy the request and environment object.
998
This only does a shallow copy, except of wsgi.input
1000
env = self.environ.copy()
1002
tempfile_limit = self.request_body_tempfile_limit
1003
if tempfile_limit and len(data) > tempfile_limit:
1004
fileobj = tempfile.TemporaryFile()
1008
fileobj = StringIO(data)
1009
env['wsgi.input'] = fileobj
1010
return self.__class__(env)
1014
Copies the request and environment object, but turning this request
1015
into a GET along the way. If this was a POST request (or any other verb)
1016
then it becomes GET, and the request body is thrown away.
1018
env = self.environ.copy()
1019
env['wsgi.input'] = StringIO('')
1020
env['CONTENT_LENGTH'] = '0'
1021
if 'CONTENT_TYPE' in env:
1022
del env['CONTENT_TYPE']
1023
env['REQUEST_METHOD'] = 'GET'
1024
return self.__class__(env)
1026
def remove_conditional_headers(self, remove_encoding=True):
1028
Remove headers that make the request conditional.
1030
These headers can cause the response to be 304 Not Modified,
1031
which in some cases you may not want to be possible.
1033
This does not remove headers like If-Match, which are used for
1036
for key in ['HTTP_IF_MATCH', 'HTTP_IF_MODIFIED_SINCE',
1037
'HTTP_IF_RANGE', 'HTTP_RANGE']:
1038
if key in self.environ:
1039
del self.environ[key]
1041
if 'HTTP_ACCEPT_ENCODING' in self.environ:
1042
del self.environ['HTTP_ACCEPT_ENCODING']
1045
environ_getter('HTTP_ACCEPT', rfc_section='14.1'),
1046
_parse_accept, _serialize_accept, 'MIME Accept',
1047
converter_args=('Accept', MIMEAccept, MIMENilAccept))
1049
accept_charset = converter(
1050
environ_getter('HTTP_ACCEPT_CHARSET', rfc_section='14.2'),
1051
_parse_accept, _serialize_accept, 'accept header',
1052
converter_args=('Accept-Charset', Accept, NilAccept))
1054
accept_encoding = converter(
1055
environ_getter('HTTP_ACCEPT_ENCODING', rfc_section='14.3'),
1056
_parse_accept, _serialize_accept, 'accept header',
1057
converter_args=('Accept-Encoding', Accept, NoAccept))
1059
accept_language = converter(
1060
environ_getter('HTTP_ACCEPT_LANGUAGE', rfc_section='14.4'),
1061
_parse_accept, _serialize_accept, 'accept header',
1062
converter_args=('Accept-Language', Accept, NilAccept))
1064
## FIXME: 14.8 Authorization
1065
## http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.8
1067
def _cache_control__get(self):
1069
Get/set/modify the Cache-Control header (section `14.9
1070
<http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9>`_)
1073
value = env.get('HTTP_CACHE_CONTROL', '')
1074
cache_header, cache_obj = env.get('webob._cache_control', (None, None))
1075
if cache_obj is not None and cache_header == value:
1077
cache_obj = CacheControl.parse(value, type='request')
1078
env['webob._cache_control'] = (value, cache_obj)
1081
def _cache_control__set(self, value):
1085
if isinstance(value, dict):
1086
value = CacheControl(value, type='request')
1087
elif isinstance(value, CacheControl):
1088
str_value = str(value)
1089
env['HTTP_CACHE_CONTROL'] = str_value
1090
env['webob._cache_control'] = (str_value, value)
1092
env['HTTP_CACHE_CONTROL'] = str(value)
1093
if 'webob._cache_control' in env:
1094
del env['webob._cache_control']
1096
def _cache_control__del(self, value):
1098
if 'HTTP_CACHE_CONTROL' in env:
1099
del env['HTTP_CACHE_CONTROL']
1100
if 'webob._cache_control' in env:
1101
del env['webob._cache_control']
1103
cache_control = property(_cache_control__get, _cache_control__set, _cache_control__del, doc=_cache_control__get.__doc__)
1106
environ_getter('HTTP_DATE', rfc_section='14.8'),
1107
_parse_date, _serialize_date, 'HTTP date')
1109
if_match = converter(
1110
environ_getter('HTTP_IF_MATCH', rfc_section='14.24'),
1111
_parse_etag, _serialize_etag, 'ETag', converter_args=(True,))
1113
if_modified_since = converter(
1114
environ_getter('HTTP_IF_MODIFIED_SINCE', rfc_section='14.25'),
1115
_parse_date, _serialize_date, 'HTTP date')
1117
if_none_match = converter(
1118
environ_getter('HTTP_IF_NONE_MATCH', rfc_section='14.26'),
1119
_parse_etag, _serialize_etag, 'ETag', converter_args=(False,))
1121
if_range = converter(
1122
environ_getter('HTTP_IF_RANGE', rfc_section='14.27'),
1123
_parse_if_range, _serialize_if_range, 'IfRange object')
1125
if_unmodified_since = converter(
1126
environ_getter('HTTP_IF_UNMODIFIED_SINCE', rfc_section='14.28'),
1127
_parse_date, _serialize_date, 'HTTP date')
1129
max_forwards = converter(
1130
environ_getter('HTTP_MAX_FORWARDS', rfc_section='14.31'),
1131
_parse_int, _serialize_int, 'int')
1133
pragma = environ_getter('HTTP_PRAGMA', rfc_section='14.32')
1136
environ_getter('HTTP_RANGE', rfc_section='14.35'),
1137
_parse_range, _serialize_range, 'Range object')
1139
referer = environ_getter('HTTP_REFERER', rfc_section='14.36')
1142
user_agent = environ_getter('HTTP_USER_AGENT', rfc_section='14.43')
1145
msg = '<%s at %x %s %s>' % (
1146
self.__class__.__name__,
1147
abs(id(self)), self.method, self.url)
1152
host = self.host_url
1153
assert url.startswith(host)
1154
url = url[len(host):]
1155
if 'Host' not in self.headers:
1156
self.headers['Host'] = self.host
1157
parts = ['%s %s' % (self.method, url)]
1158
for name, value in sorted(self.headers.items()):
1159
parts.append('%s: %s' % (name, value))
1161
parts.append(self.body)
1162
return '\r\n'.join(parts)
1164
def call_application(self, application, catch_exc_info=False):
1166
Call the given WSGI application, returning ``(status_string,
1167
headerlist, app_iter)``
1169
Be sure to call ``app_iter.close()`` if it's there.
1171
If catch_exc_info is true, then returns ``(status_string,
1172
headerlist, app_iter, exc_info)``, where the fourth item may
1173
be None, but won't be if there was an exception. If you don't
1174
do this and there was an exception, the exception will be
1179
def start_response(status, headers, exc_info=None):
1180
if exc_info is not None and not catch_exc_info:
1181
raise exc_info[0], exc_info[1], exc_info[2]
1182
captured[:] = [status, headers, exc_info]
1183
return output.append
1184
app_iter = application(self.environ, start_response)
1188
output.extend(app_iter)
1190
if hasattr(app_iter, 'close'):
1194
return (captured[0], captured[1], app_iter, captured[2])
1196
return (captured[0], captured[1], app_iter)
1198
# Will be filled in later:
1199
ResponseClass = None
1201
def get_response(self, application, catch_exc_info=False):
1203
Like ``.call_application(application)``, except returns a
1204
response object with ``.status``, ``.headers``, and ``.body``
1207
This will use ``self.ResponseClass`` to figure out the class
1208
of the response object to return.
1211
status, headers, app_iter, exc_info = self.call_application(
1212
application, catch_exc_info=True)
1215
status, headers, app_iter = self.call_application(
1216
application, catch_exc_info=False)
1217
return self.ResponseClass(
1218
status=status, headerlist=headers, app_iter=app_iter,
1222
def blank(cls, path, environ=None, base_url=None, headers=None):
1224
Create a blank request environ (and Request wrapper) with the
1225
given path (path should be urlencoded), and any keys from
1228
The path will become path_info, with any query string split
1231
All necessary keys will be added to the environ, but the
1232
values you pass in will take precedence. If you pass in
1233
base_url then wsgi.url_scheme, HTTP_HOST, and SCRIPT_NAME will
1234
be filled in from that value.
1236
if _SCHEME_RE.search(path):
1237
scheme, netloc, path, qs, fragment = urlparse.urlsplit(path)
1240
"Path cannot contain a fragment (%r)" % fragment)
1243
if ':' not in netloc:
1244
if scheme == 'http':
1246
elif scheme == 'https':
1249
raise TypeError("Unknown scheme: %r" % scheme)
1252
netloc = 'localhost:80'
1253
if path and '?' in path:
1254
path_info, query_string = path.split('?', 1)
1255
path_info = urllib.unquote(path_info)
1257
path_info = urllib.unquote(path)
1260
'REQUEST_METHOD': 'GET',
1262
'PATH_INFO': path_info or '',
1263
'QUERY_STRING': query_string,
1264
'SERVER_NAME': netloc.split(':')[0],
1265
'SERVER_PORT': netloc.split(':')[1],
1266
'HTTP_HOST': netloc,
1267
'SERVER_PROTOCOL': 'HTTP/1.0',
1268
'wsgi.version': (1, 0),
1269
'wsgi.url_scheme': scheme,
1270
'wsgi.input': StringIO(''),
1271
'wsgi.errors': sys.stderr,
1272
'wsgi.multithread': False,
1273
'wsgi.multiprocess': False,
1274
'wsgi.run_once': False,
1277
scheme, netloc, path, query, fragment = urlparse.urlsplit(base_url)
1278
if query or fragment:
1280
"base_url (%r) cannot have a query or fragment"
1283
env['wsgi.url_scheme'] = scheme
1285
if ':' not in netloc:
1286
if scheme == 'http':
1288
elif scheme == 'https':
1292
"Unknown scheme: %r" % scheme)
1293
host, port = netloc.split(':', 1)
1294
env['SERVER_PORT'] = port
1295
env['SERVER_NAME'] = host
1296
env['HTTP_HOST'] = netloc
1298
env['SCRIPT_NAME'] = urllib.unquote(path)
1302
if headers is not None:
1303
obj.headers.update(headers)
1306
blank = classmethod(blank)
1308
class Response(object):
1311
Represents a WSGI response
1314
default_content_type = 'text/html'
1315
default_charset = 'utf8'
1316
default_conditional_response = False
1318
def __init__(self, body=None, status='200 OK', headerlist=None, app_iter=None,
1319
request=None, content_type=None, conditional_response=NoDefault,
1321
if app_iter is None:
1324
elif body is not None:
1326
"You may only give one of the body and app_iter arguments")
1327
self.status = status
1328
if headerlist is None:
1329
self._headerlist = []
1331
self._headerlist = headerlist
1332
self._headers = None
1333
if request is not None:
1334
if hasattr(request, 'environ'):
1335
self._environ = request.environ
1336
self._request = request
1338
self._environ = request
1339
self._request = None
1341
self._environ = self._request = None
1342
if content_type is not None:
1343
self.content_type = content_type
1344
elif self.default_content_type is not None and headerlist is None:
1345
self.content_type = self.default_content_type
1346
if conditional_response is NoDefault:
1347
self.conditional_response = self.default_conditional_response
1349
self.conditional_response = conditional_response
1351
# We set this early, so something like unicode_body works later
1352
value = kw.pop('charset')
1354
self.charset = value
1355
elif self.default_charset and not self.charset and headerlist is None:
1356
ct = self.content_type
1357
if ct and (ct.startswith('text/') or ct.startswith('application/xml')
1358
or (ct.startswith('application/') and ct.endswith('+xml'))):
1359
self.charset = self.default_charset
1360
if app_iter is not None:
1361
self._app_iter = app_iter
1364
if isinstance(body, unicode):
1365
self.unicode_body = body
1368
self._app_iter = None
1369
for name, value in kw.items():
1370
if not hasattr(self.__class__, name):
1371
# Not a basic attribute
1373
"Unexpected keyword: %s=%r in %r" % (name, value))
1374
setattr(self, name, value)
1377
return '<%s %x %s>' % (
1378
self.__class__.__name__,
1383
return (self.status + '\n'
1384
+ '\n'.join(['%s: %s' % (name, value)
1385
for name, value in self.headerlist])
1389
def _status__get(self):
1395
def _status__set(self, value):
1396
if isinstance(value, int):
1398
if not isinstance(value, str):
1400
"You must set status to a string or integer (not %s)"
1402
if ' ' not in value:
1403
# Need to add a reason:
1405
reason = status_reasons[code]
1406
value += ' ' + reason
1407
self._status = value
1409
status = property(_status__get, _status__set, doc=_status__get.__doc__)
1411
def _status_int__get(self):
1413
The status as an integer
1415
return int(self.status.split()[0])
1416
def _status_int__set(self, value):
1418
status_int = property(_status_int__get, _status_int__set, doc=_status_int__get.__doc__)
1420
def _headerlist__get(self):
1422
The list of response headers
1424
return self._headerlist
1426
def _headerlist__set(self, value):
1427
self._headers = None
1428
if not isinstance(value, list):
1429
if hasattr(value, 'items'):
1430
value = value.items()
1432
self._headerlist = value
1434
def _headerlist__del(self):
1435
self.headerlist = []
1437
headerlist = property(_headerlist__get, _headerlist__set, _headerlist__del, doc=_headerlist__get.__doc__)
1439
def _charset__get(self):
1441
Get/set the charset (in the Content-Type)
1443
header = self.headers.get('content-type')
1446
match = _CHARSET_RE.search(header)
1448
return match.group(1)
1451
def _charset__set(self, charset):
1456
header = self.headers.pop('content-type')
1458
raise AttributeError(
1459
"You cannot set the charset when no content-type is defined")
1460
match = _CHARSET_RE.search(header)
1462
header = header[:match.start()] + header[match.end():]
1463
header += '; charset=%s' % charset
1464
self.headers['content-type'] = header
1466
def _charset__del(self):
1468
header = self.headers.pop('content-type')
1470
# Don't need to remove anything
1472
match = _CHARSET_RE.search(header)
1474
header = header[:match.start()] + header[match.end():]
1475
self.headers['content-type'] = header
1477
charset = property(_charset__get, _charset__set, _charset__del, doc=_charset__get.__doc__)
1479
def _content_type__get(self):
1481
Get/set the Content-Type header (or None), *without* the
1482
charset or any parameters.
1484
If you include parameters (or ``;`` at all) when setting the
1485
content_type, any existing parameters will be deleted;
1486
otherwise they will be preserved.
1488
header = self.headers.get('content-type')
1491
return header.split(';', 1)[0]
1493
def _content_type__set(self, value):
1494
if ';' not in value:
1495
header = self.headers.get('content-type', '')
1497
params = header.split(';', 1)[1]
1498
value += ';' + params
1499
self.headers['content-type'] = value
1501
def _content_type__del(self):
1503
del self.headers['content-type']
1507
content_type = property(_content_type__get, _content_type__set,
1508
_content_type__del, doc=_content_type__get.__doc__)
1510
def _content_type_params__get(self):
1512
Returns a dictionary of all the parameters in the content type.
1514
params = self.headers.get('content-type', '')
1515
if ';' not in params:
1517
params = params.split(';', 1)[1]
1519
for match in _PARAM_RE.finditer(params):
1520
result[match.group(1)] = match.group(2) or match.group(3) or ''
1523
def _content_type_params__set(self, value_dict):
1525
del self.content_type_params
1528
for k, v in sorted(value_dict.items()):
1529
if not _OK_PARAM_RE.search(v):
1530
## FIXME: I'm not sure what to do with "'s in the parameter value
1531
## I think it might be simply illegal
1532
v = '"%s"' % v.replace('"', '\\"')
1533
params.append('; %s=%s' % (k, v))
1534
ct = self.headers.pop('content-type', '').split(';', 1)[0]
1535
ct += ''.join(params)
1536
self.headers['content-type'] = ct
1538
def _content_type_params__del(self, value):
1539
self.headers['content-type'] = self.headers.get('content-type', '').split(';', 1)[0]
1541
content_type_params = property(_content_type_params__get, _content_type_params__set, _content_type_params__del, doc=_content_type_params__get.__doc__)
1543
def _headers__get(self):
1545
The headers in a dictionary-like object
1547
if self._headers is None:
1548
self._headers = HeaderDict.view_list(self.headerlist)
1549
return self._headers
1551
def _headers__set(self, value):
1552
if hasattr(value, 'items'):
1553
value = value.items()
1554
self.headerlist = value
1555
self._headers = None
1557
headers = property(_headers__get, _headers__set, doc=_headers__get.__doc__)
1559
def _body__get(self):
1561
The body of the response, as a ``str``. This will read in the
1562
entire app_iter if necessary.
1564
if self._body is None:
1565
if self._app_iter is None:
1566
raise AttributeError(
1567
"No body has been set")
1569
self._body = ''.join(self._app_iter)
1571
if hasattr(self._app_iter, 'close'):
1572
self._app_iter.close()
1573
self._app_iter = None
1574
self.content_length = len(self._body)
1577
def _body__set(self, value):
1578
if isinstance(value, unicode):
1580
"You cannot set Response.body to a unicode object (use Response.unicode_body)")
1581
if not isinstance(value, str):
1583
"You can only set the body to a str (not %s)"
1586
self.content_length = len(value)
1587
self._app_iter = None
1589
def _body__del(self):
1591
self.content_length = None
1592
self._app_iter = None
1594
body = property(_body__get, _body__set, _body__del, doc=_body__get.__doc__)
1596
def _body_file__get(self):
1598
Returns a file-like object that can be used to write to the
1599
body. If you passed in a list app_iter, that app_iter will be
1602
return ResponseBodyFile(self)
1604
def _body_file__del(self):
1607
body_file = property(_body_file__get, fdel=_body_file__del, doc=_body_file__get.__doc__)
1609
def write(self, text):
1610
if isinstance(text, unicode):
1611
self.unicode_body += text
1615
def _unicode_body__get(self):
1617
Get/set the unicode value of the body (using the charset of the Content-Type)
1619
if not self.charset:
1620
raise AttributeError(
1621
"You cannot access Response.unicode_body unless charset is set")
1623
return body.decode(self.charset)
1625
def _unicode_body__set(self, value):
1626
if not self.charset:
1627
raise AttributeError(
1628
"You cannot access Response.unicode_body unless charset is set")
1629
if not isinstance(value, unicode):
1631
"You can only set Response.unicode_body to a unicode string (not %s)" % type(value))
1632
self.body = value.encode(self.charset)
1634
def _unicode_body__del(self):
1637
unicode_body = property(_unicode_body__get, _unicode_body__set, _unicode_body__del, doc=_unicode_body__get.__doc__)
1639
def _app_iter__get(self):
1641
Returns the app_iter of the response.
1643
If body was set, this will create an app_iter from that body
1644
(a single-item list)
1646
if self._app_iter is None:
1647
if self._body is None:
1648
raise AttributeError(
1649
"No body or app_iter has been set")
1652
return self._app_iter
1654
def _app_iter__set(self, value):
1655
if self._body is not None:
1656
# Undo the automatically-set content-length
1657
self.content_length = None
1658
self._app_iter = value
1661
def _app_iter__del(self):
1662
self.content_length = None
1663
self._app_iter = self._body = None
1665
app_iter = property(_app_iter__get, _app_iter__set, _app_iter__del, doc=_app_iter__get.__doc__)
1667
def set_cookie(self, key, value='', max_age=None,
1668
path='/', domain=None, secure=None, httponly=False,
1669
version=None, comment=None):
1671
Set (add) a cookie for the response
1673
cookies = BaseCookie()
1674
cookies[key] = value
1675
for var_name, var_value in [
1676
('max_age', max_age),
1680
('HttpOnly', httponly),
1681
('version', version),
1682
('comment', comment),
1684
if var_value is not None and var_value is not False:
1685
cookies[key][var_name.replace('_', '-')] = str(var_value)
1686
header_value = cookies[key].output(header='').lstrip()
1687
self.headerlist.append(('Set-Cookie', header_value))
1689
def delete_cookie(self, key, path='/', domain=None):
1691
Delete a cookie from the client. Note that path and domain must match
1692
how the cookie was originally set.
1694
This sets the cookie to the empty string, and max_age=0 so
1695
that it should expire immediately.
1697
self.set_cookie(key, '', path=path, domain=domain,
1700
def unset_cookie(self, key):
1702
Unset a cookie with the given name (remove it from the
1703
response). If there are multiple cookies (e.g., two cookies
1704
with the same name and different paths or domains), all such
1705
cookies will be deleted.
1707
existing = self.headers.getall('Set-Cookie')
1710
"No cookies at all have been set")
1711
del self.headers['Set-Cookie']
1713
for header in existing:
1714
cookies = BaseCookie()
1715
cookies.load(header)
1719
header = cookies.output(header='').lstrip()
1721
self.headers.add('Set-Cookie', header)
1724
"No cookie has been set with the name %r" % key)
1726
def _location__get(self):
1728
Retrieve the Location header of the response, or None if there
1729
is no header. If the header is not absolute and this response
1730
is associated with a request, make the header absolute.
1732
For more information see `section 14.30
1733
<http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.30>`_.
1735
if 'location' not in self.headers:
1737
location = self.headers['location']
1738
if _SCHEME_RE.search(location):
1741
if self.request is not None:
1742
base_uri = self.request.url
1743
location = urlparse.urljoin(base_uri, location)
1746
def _location__set(self, value):
1747
if not _SCHEME_RE.search(value):
1748
# Not absolute, see if we can make it absolute
1749
if self.request is not None:
1750
value = urlparse.urljoin(self.request.url, value)
1751
self.headers['location'] = value
1753
def _location__del(self):
1754
if 'location' in self.headers:
1755
del self.headers['location']
1757
location = property(_location__get, _location__set, _location__del, doc=_location__get.__doc__)
1759
accept_ranges = header_getter('Accept-Ranges', rfc_section='14.5')
1762
header_getter('Age', rfc_section='14.6'),
1763
_parse_int_safe, _serialize_int, 'int')
1766
header_getter('Allow', rfc_section='14.7'),
1767
_parse_list, _serialize_list, 'list')
1769
_cache_control_obj = None
1771
def _cache_control__get(self):
1773
Get/set/modify the Cache-Control header (section `14.9
1774
<http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9>`_)
1776
value = self.headers.get('cache-control', '')
1777
if self._cache_control_obj is None:
1778
self._cache_control_obj = CacheControl.parse(value, updates_to=self._update_cache_control, type='response')
1779
self._cache_control_obj.header_value = value
1780
if self._cache_control_obj.header_value != value:
1781
new_obj = CacheControl.parse(value, type='response')
1782
self._cache_control_obj.properties.clear()
1783
self._cache_control_obj.properties.update(new_obj.properties)
1784
self._cache_control_obj.header_value = value
1785
return self._cache_control_obj
1787
def _cache_control__set(self, value):
1788
# This actually becomes a copy
1791
if isinstance(value, dict):
1792
value = CacheControl(value, 'response')
1793
if isinstance(value, unicode):
1795
if isinstance(value, str):
1796
if self._cache_control_obj is None:
1797
self.headers['Cache-Control'] = value
1799
value = CacheControl.parse(value, 'response')
1800
cache = self.cache_control
1801
cache.properties.clear()
1802
cache.properties.update(value.properties)
1804
def _cache_control__del(self):
1805
self.cache_control = {}
1807
def _update_cache_control(self, prop_dict):
1808
value = serialize_cache_control(prop_dict)
1810
if 'Cache-Control' in self.headers:
1811
del self.headers['Cache-Control']
1813
self.headers['Cache-Control'] = value
1815
cache_control = property(_cache_control__get, _cache_control__set, _cache_control__del, doc=_cache_control__get.__doc__)
1817
def cache_expires(self, seconds=0, **kw):
1819
Set expiration on this request. This sets the response to
1820
expire in the given seconds, and any other attributes are used
1821
for cache_control (e.g., private=True, etc).
1823
cache_control = self.cache_control
1824
if isinstance(seconds, timedelta):
1825
seconds = timedelta_to_seconds(seconds)
1827
# To really expire something, you have to force a
1828
# bunch of these cache control attributes, and IE may
1829
# not pay attention to those still so we also set
1831
cache_control.no_store = True
1832
cache_control.no_cache = True
1833
cache_control.must_revalidate = True
1834
cache_control.max_age = 0
1835
cache_control.post_check = 0
1836
cache_control.pre_check = 0
1837
self.expires = datetime.utcnow()
1838
if 'last-modified' not in self.headers:
1839
self.last_modified = datetime.utcnow()
1840
self.pragma = 'no-cache'
1842
cache_control.max_age = seconds
1843
self.expires = datetime.utcnow() + timedelta(seconds=seconds)
1844
for name, value in kw.items():
1845
setattr(cache_control, name, value)
1847
content_encoding = header_getter('Content-Encoding', rfc_section='14.11')
1849
def encode_content(self, encoding='gzip'):
1851
Encode the content with the given encoding (only gzip and
1852
identity are supported).
1854
if encoding == 'identity':
1856
if encoding != 'gzip':
1858
"Unknown encoding: %r" % encoding)
1859
if self.content_encoding:
1860
if self.content_encoding == encoding:
1862
self.decode_content()
1863
from webob.util.safegzip import GzipFile
1865
gzip_f = GzipFile(filename='', mode='w', fileobj=f)
1866
gzip_f.write(self.body)
1868
new_body = f.getvalue()
1870
self.content_encoding = 'gzip'
1871
self.body = new_body
1873
def decode_content(self):
1874
content_encoding = self.content_encoding
1875
if not content_encoding or content_encoding == 'identity':
1877
if content_encoding != 'gzip':
1879
"I don't know how to decode the content %s" % content_encoding)
1880
from webob.util.safegzip import GzipFile
1881
f = StringIO(self.body)
1882
gzip_f = GzipFile(filename='', mode='r', fileobj=f)
1883
new_body = gzip_f.read()
1886
self.content_encoding = None
1887
self.body = new_body
1889
content_language = converter(
1890
header_getter('Content-Language', rfc_section='14.12'),
1891
_parse_list, _serialize_list, 'list')
1893
content_location = header_getter(
1894
'Content-Location', rfc_section='14.14')
1896
content_md5 = header_getter(
1897
'Content-MD5', rfc_section='14.14')
1899
content_range = converter(
1900
header_getter('Content-Range', rfc_section='14.16'),
1901
_parse_content_range, _serialize_content_range, 'ContentRange object')
1903
content_length = converter(
1904
header_getter('Content-Length', rfc_section='14.17'),
1905
_parse_int, _serialize_int, 'int')
1908
header_getter('Date', rfc_section='14.18'),
1909
_parse_date, _serialize_date, 'HTTP date')
1911
etag = header_getter('ETag', rfc_section='14.19')
1913
def md5_etag(self, body=None):
1915
Generate an etag for the response object using an MD5 hash of
1916
the body (the body parameter, or ``self.body`` if not given)
1924
self.etag = h.digest().encode('base64').replace('\n', '').strip('=')
1926
expires = converter(
1927
header_getter('Expires', rfc_section='14.21'),
1928
_parse_date, _serialize_date, 'HTTP date')
1930
last_modified = converter(
1931
header_getter('Last-Modified', rfc_section='14.29'),
1932
_parse_date, _serialize_date, 'HTTP date')
1934
pragma = header_getter('Pragma', rfc_section='14.32')
1936
retry_after = converter(
1937
header_getter('Retry-After', rfc_section='14.37'),
1938
_parse_date_delta, _serialize_date_delta, 'HTTP date or delta seconds')
1940
server = header_getter('Server', rfc_section='14.38')
1942
## FIXME: I realize response.vary += 'something' won't work. It should.
1943
## Maybe for all listy headers.
1945
header_getter('Vary', rfc_section='14.44'),
1946
_parse_list, _serialize_list, 'list')
1948
## FIXME: 14.47 WWW-Authenticate
1949
## http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.47
1952
def _request__get(self):
1954
Return the request associated with this response if any.
1956
if self._request is None and self._environ is not None:
1957
self._request = self.RequestClass(self._environ)
1958
return self._request
1960
def _request__set(self, value):
1964
if isinstance(value, dict):
1965
self._environ = value
1966
self._request = None
1968
self._request = value
1969
self._environ = value.environ
1971
def _request__del(self):
1972
self._request = self._environ = None
1974
request = property(_request__get, _request__set, _request__del, doc=_request__get.__doc__)
1976
def _environ__get(self):
1978
Get/set the request environ associated with this response, if
1981
return self._environ
1983
def _environ__set(self, value):
1986
self._environ = value
1987
self._request = None
1989
def _environ__del(self):
1990
self._request = self._environ = None
1992
environ = property(_environ__get, _environ__set, _environ__del, doc=_environ__get.__doc__)
1994
def __call__(self, environ, start_response):
1996
WSGI application interface
1998
if self.conditional_response:
1999
return self.conditional_response_app(environ, start_response)
2000
start_response(self.status, self.headerlist)
2001
if environ['REQUEST_METHOD'] == 'HEAD':
2002
# Special case here...
2004
return self.app_iter
2006
_safe_methods = ('GET', 'HEAD')
2008
def conditional_response_app(self, environ, start_response):
2010
Like the normal __call__ interface, but checks conditional headers:
2012
* If-Modified-Since (304 Not Modified; only on GET, HEAD)
2013
* If-None-Match (304 Not Modified; only on GET, HEAD)
2014
* Range (406 Partial Content; only on GET, HEAD)
2016
req = self.RequestClass(environ)
2018
if req.method in self._safe_methods:
2019
if req.if_modified_since and self.last_modified and self.last_modified <= req.if_modified_since:
2021
if req.if_none_match and self.etag:
2022
## FIXME: should a weak match be okay?
2023
if self.etag in req.if_none_match:
2026
# Even if If-Modified-Since matched, if ETag doesn't then reject it
2029
start_response('304 Not Modified', self.headerlist)
2031
if req.method == 'HEAD':
2032
start_response(self.status, self.headerlist)
2034
if (req.range and req.if_range.match_response(self)
2035
and self.content_range is None
2036
and req.method == 'GET'
2037
and self.status_int == 200):
2038
content_range = req.range.content_range(self.content_length)
2039
if content_range is not None:
2040
app_iter = self.app_iter_range(content_range.start, content_range.stop)
2041
if app_iter is not None:
2042
headers = list(self.headerlist)
2043
headers.append(('Content-Range', str(content_range)))
2044
start_response('206 Partial Content', headers)
2046
start_response(self.status, self.headerlist)
2047
return self.app_iter
2049
def app_iter_range(self, start, stop):
2051
Return a new app_iter built from the response app_iter, that
2052
serves up only the given ``start:stop`` range.
2054
if self._app_iter is None:
2055
return [self.body[start:stop]]
2056
app_iter = self.app_iter
2057
if hasattr(app_iter, 'app_iter_range'):
2058
return app_iter.app_iter_range(start, stop)
2059
return AppIterRange(app_iter, start, stop)
2062
Request.ResponseClass = Response
2063
Response.RequestClass = Request
2065
def _cgi_FieldStorage__repr__patch(self):
2066
""" monkey patch for FieldStorage.__repr__
2068
Unbelievely, the default __repr__ on FieldStorage reads
2069
the entire file content instead of being sane about it.
2070
This is a simple replacement that doesn't do that
2073
return "FieldStorage(%r, %r)" % (
2074
self.name, self.filename)
2075
return "FieldStorage(%r, %r, %r)" % (
2076
self.name, self.filename, self.value)
2078
cgi.FieldStorage.__repr__ = _cgi_FieldStorage__repr__patch
2080
class FakeCGIBody(object):
2082
def __init__(self, vars):
2087
def read(self, size=-1):
2088
body = self._get_body()
2090
v = body[self.position:]
2091
self.position = len(body)
2094
v = body[self.position:self.position+size]
2095
self.position = min(len(body), self.position+size)
2098
def _get_body(self):
2099
if self._body is None:
2100
self._body = urllib.urlencode(self.vars.items())
2103
def readline(self, size=None):
2104
# We ignore size, but allow it to be hinted
2105
rest = self._get_body()[self.position:]
2106
next = rest.find('\r\n')
2109
self.position += next+2
2110
return rest[:next+2]
2112
def readlines(self, hint=None):
2113
# Again, allow hint but ignore
2114
body = self._get_body()
2115
rest = body[self.position:]
2116
self.position = len(body)
2119
next = rest.find('\r\n')
2123
result.append(rest[:next+2])
2124
rest = rest[next+2:]
2128
return iter(self.readlines())
2131
inner = repr(self.vars)
2133
inner = inner[:15] + '...' + inner[-5:]
2134
return '<%s at %x viewing %s>' % (
2135
self.__class__.__name__,
2136
abs(id(self)), inner)
2139
def update_environ(cls, environ, vars):
2141
environ['CONTENT_LENGTH'] = '-1'
2142
environ['wsgi.input'] = obj
2144
update_environ = classmethod(update_environ)
2146
class ResponseBodyFile(object):
2148
def __init__(self, response):
2149
self.response = response
2152
return '<body_file for %r>' % (
2156
raise NotImplementedError(
2157
"Response bodies cannot be closed")
2163
if isinstance(s, unicode):
2164
if self.response.charset is not None:
2165
s = s.encode(self.response.charset)
2168
"You can only write unicode to Response.body_file "
2169
"if charset has been set")
2170
if not isinstance(s, str):
2172
"You can only write str to a Response.body_file, not %s"
2174
if not isinstance(self.response._app_iter, list):
2175
body = self.response.body
2177
self.response.app_iter = [body]
2179
self.response.app_iter = []
2180
self.response.app_iter.append(s)
2182
def writelines(self, seq):
2190
The encoding of the file (inherited from response.charset)
2192
return self.response.charset
2194
encoding = property(encoding, doc=encoding.__doc__)
2198
class AppIterRange(object):
2200
Wraps an app_iter, returning just a range of bytes
2203
def __init__(self, app_iter, start, stop):
2204
assert start >= 0, "Bad start: %r" % start
2205
assert stop is None or (stop >= 0 and stop >= start), (
2206
"Bad stop: %r" % stop)
2207
self.app_iter = app_iter
2208
self.app_iterator = iter(app_iter)
2213
self.length = stop - start
2218
if hasattr(app_iter, 'close'):
2219
self.close = app_iter.close
2225
if self._served is None:
2226
# Haven't served anything; need to skip some leading bytes
2230
chunk = self.app_iterator.next()
2231
skipped += len(chunk)
2232
extra = skipped - start
2237
self._served = extra
2238
return chunk[-extra:]
2239
length = self.length
2243
chunk = self.app_iterator.next()
2246
if self._served + len(chunk) > length:
2247
extra = self._served + len(chunk) - length
2249
return chunk[:-extra]
2250
self._served += len(chunk)