Skip to content

request_response

pigwig.request_response

Request(app: PigWig, method: str, path: str, query: typing.Mapping[str, str | list[str]], headers: HTTPHeaders, body: dict, cookies: http.cookies.BaseCookie, wsgi_environ: dict[str, typing.Any])

an instance of this class is passed to every route handler. has the following instance attrs:

  • app - an instance of :class:.PigWig
  • method - the request method/verb (GET, POST, etc.)
  • path - WSGI environ PATH_INFO (/foo/bar)
  • query - dict of parsed query string. duplicate keys appear as lists
  • headers - HTTPHeaders of the headers
  • body - dict of parsed body content. see PigWig.content_handlers for a list of supported content types
  • cookies - an instance of http.cookies.SimpleCookie
  • wsgi_environ - the raw WSGI environ handed down from the server
Source code in pigwig/request_response.py
34
35
36
37
38
39
40
41
42
43
44
def __init__(self, app: PigWig, method: str, path: str, query: typing.Mapping[str, str |  list[str]],
			headers: HTTPHeaders, body: dict, cookies: http.cookies.BaseCookie,
			wsgi_environ: dict[str, typing.Any]) -> None:
	self.app = app
	self.method = method
	self.path = path
	self.query = query
	self.headers = headers
	self.body = body
	self.cookies = cookies
	self.wsgi_environ = wsgi_environ

decode and verify a cookie set with Response.set_secure_cookie

Parameters:

  • key (str) –

    key passed to set_secure_cookie

  • max_time (datetime.timedelta) –

    amount of time since cookie was set that it should be considered valid for. this is normally equal to the max_age passed to set_secure_cookie. longer times mean larger windows during which a replay attack is valid. this can be None, in which case no expiry check is performed

Source code in pigwig/request_response.py
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
def get_secure_cookie(self, key: str, max_time: datetime.timedelta) -> str | None:
	"""
	decode and verify a cookie set with [Response.set_secure_cookie][]

	:param key: ``key`` passed to ``set_secure_cookie``
	:param max_time: amount of time since cookie was set that it should be considered valid for.
	  this is normally equal to the ``max_age`` passed to ``set_secure_cookie``. longer times mean
	  larger windows during which a replay attack is valid. this can be ``None``, in which case no
	  expiry check is performed
	"""
	assert self.app.cookie_secret is not None
	try:
		cookie = self.cookies[key].value
	except KeyError:
		return None
	try:
		value, ts_str, signature = cookie.rsplit('|', 2)
		ts_int = int(ts_str)
	except ValueError:
		raise exceptions.HTTPException(400, 'invalid %s cookie: %s' % (key, cookie))
	value_ts = '%s|%d' % (value, ts_int)
	if hmac.compare_digest(signature, _hash(key + '|' + value_ts, self.app.cookie_secret)):
		if max_time is not None and ts_int + max_time.total_seconds() < time.time(): # cookie has expired
			return None
		return value
	else:
		return None

Response(body: str | bytes | typing.Iterator[bytes] | None = None, code: int = 200, content_type: str = 'text/plain', location: str | None = None, extra_headers: list[tuple[str, str]] | None = None)

every route handler should return an instance of this class (or raise an exceptions.HTTPException)

Parameters:

  • body (str | bytes | typing.Iterator[bytes] | None, default: None ) –
    • if None, the response body is empty
    • if a str, the response body is UTF-8 encoded
    • if a bytes, the response body is sent as-is
    • if a generator, the response streams the yielded bytes
  • code (int, default: 200 ) –

    HTTP status code; the "reason phrase" is generated automatically from http.client.responses

  • content_type (str, default: 'text/plain' ) –

    sets the Content-Type header

  • location (str | None, default: None ) –

    if not None, sets the Location header. you must still specify a 3xx code

  • extra_headers (list[tuple[str, str]] | None, default: None ) –

    if not None, an iterable of extra header 2-tuples to be sent

Source code in pigwig/request_response.py
105
106
107
108
109
110
111
112
113
114
115
116
117
def __init__(self, body: str | bytes | typing.Iterator[bytes] | None=None, code: int=200,
			content_type: str='text/plain', location: str | None=None,
			extra_headers: list[tuple[str, str]] | None=None) -> None:
	self.body = body
	self.code = code

	headers = list(self.DEFAULT_HEADERS)
	headers.append(('Content-Type', content_type))
	if location:
		headers.append(('Location', location))
	if extra_headers:
		headers.extend(extra_headers)
	self.headers = headers

json_encoder = jsonlib.JSONEncoder(indent='\t') class-attribute instance-attribute

body: str | bytes | typing.Iterator[bytes] | None = body instance-attribute

code: int = code instance-attribute

headers: list[tuple[str, str]] = headers instance-attribute

adds a Set-Cookie header

Parameters:

  • expires (datetime.datetime | None, default: None ) –

    if set to a value in the past, the cookie is deleted. if this and max_age are not set, the cookie becomes a session cookie.

  • max_age (datetime.timedelta | None, default: None ) –

    according to the spec, has precedence over expires. if you specify both, both are sent.

  • secure (bool, default: False ) –

    controls when the browser sends the cookie back - unrelated to set_secure_cookie

    see the docs for an explanation of the other params

Source code in pigwig/request_response.py
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
def set_cookie(self, key: str, value: typing.Any, domain: str | None=None, path: str='/',
		expires: datetime.datetime | None=None, max_age: datetime.timedelta | None=None, secure: bool=False,
		http_only: bool=False) -> None:
	"""
	adds a Set-Cookie header

	:param expires: if set to a value in the past, the cookie is deleted. if this and ``max_age`` are
	  not set, the cookie becomes a session cookie.
	:param max_age: according to the spec, has precedence over expires. if you specify both, both are sent.
	:param secure: controls when the browser sends the cookie back - unrelated to [set_secure_cookie][]

	see [the docs](https://tools.ietf.org/html/rfc6265#section-4.1) for an explanation of the other params
	"""
	cookie = '%s=%s' % (key, self.simple_cookie.value_encode(value)[1])
	if domain:
		cookie += '; Domain=%s' % domain
	if path:
		cookie += '; Path=%s' % path
	if expires:
		cookie += '; Expires=%s' % expires.strftime('%a, %d %b %Y %H:%M:%S GMT')
	if max_age is not None:
		cookie += '; Max-Age=%d' % max_age.total_seconds()
	if secure:
		cookie += '; Secure'
	if http_only:
		cookie += '; HttpOnly'
	self.headers.append(('Set-Cookie', cookie))

this function accepts the same keyword arguments as set_cookie but stores a timestamp and a signature based on request.app.cookie_secret. decode with Request.get_secure_cookie.

the signature is a SHA-256 hmac of the key, value, and timestamp. the value is not encrypted and is readable by the user, but is signed and tamper-proof (assuming the cookie_secret is secure). because we store the signing time, expiry is checked with Request.get_secure_cookie. you generally will want to pass this function a max_age equal to max_time used when reading the cookie.

Source code in pigwig/request_response.py
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
def set_secure_cookie(self, request: Request, key: str, value: typing.Any, **kwargs: typing.Any) -> None:
	"""
	this function accepts the same keyword arguments as [set_cookie][] but stores a
	timestamp and a signature based on ``request.app.cookie_secret``. decode with
	[Request.get_secure_cookie][].

	the signature is a SHA-256 [hmac][] of the key, value, and timestamp. the value is *not*
	encrypted and is readable by the user, but is signed and tamper-proof (assuming the
	``cookie_secret`` is secure). because we store the signing time, expiry is checked with
	[Request.get_secure_cookie][]. you generally will want to pass this function a
	``max_age`` equal to ``max_time`` used when reading the cookie.
	"""
	assert request.app.cookie_secret is not None
	ts = int(time.time())
	value_ts = '%s|%s' % (value, ts)
	signature = _hash(key + '|' + value_ts, request.app.cookie_secret)
	value_signed = '%s|%s' % (value_ts, signature)
	self.set_cookie(key, value_signed, **kwargs)

json(obj: typing.Any) -> Response classmethod

generate a streaming Response object from an object with an application/json content type. the default json_encoder indents with tabs - override if you want different indentation or need special encoding.

Source code in pigwig/request_response.py
166
167
168
169
170
171
172
173
174
@classmethod
def json(cls, obj: typing.Any) -> Response:
	"""
	generate a streaming [Response][] object from an object with an ``application/json``
	content type. the default [json_encoder][] indents with tabs - override if you want
	different indentation or need special encoding.
	"""
	body = cls._gen_json(obj)
	return Response(body, content_type='application/json; charset=utf-8')

render(request: Request, template: str, context: dict[str, typing.Any]) -> 'Response' classmethod

generate a streaming Response object from a template and a context with a text/html content type.

Parameters:

  • request (Request) –

    the request to generate the response for

  • template (str) –

    the template name to render, relative to request.app.template_dir

  • context (dict[str, typing.Any]) –

    if you used the default jinja2 template engine, this is a dict

Source code in pigwig/request_response.py
193
194
195
196
197
198
199
200
201
202
203
204
205
206
@classmethod
def render(cls, request: Request, template: str, context: dict[str, typing.Any]) -> 'Response':
	"""
	generate a streaming [Response][] object from a template and a context with a
	``text/html`` content type.

	:param request: the request to generate the response for
	:param template: the template name to render, relative to ``request.app.template_dir``
	:param context: if you used the default jinja2 template engine, this is a dict

	"""
	body = request.app.template_engine.render(template, context)
	response = cls(body, content_type='text/html; charset=utf-8')
	return response

HTTPHeaders

Bases: UserDict

behaves like a regular dict but casefolds the keys