Source code for kobin.responses

"""
Response
========

In contrast to :class:`Request` objects, which are created automatically,
:class:`Response` objects are your responsibility.
Each view functions you write is responsible
for instantiating and returning an :class:`Response` or its child classes.

In addition to the :class:`Response` class, Kobin provides :class:`TemplateResponse` ,
 :class:`JSONResponse` , :class:`RedirectResponse` and :class:`HTTPError`.
"""
import base64
import hashlib
import hmac
import time
import json
import pickle
import http.client as http_client
from urllib.parse import urljoin
from http.cookies import SimpleCookie
from wsgiref.headers import Headers

from .requests import request


HTTP_CODES = http_client.responses.copy()
_HTTP_STATUS_LINES = dict((k, '%d %s' % (k, v)) for (k, v) in HTTP_CODES.items())


[docs]class BaseResponse: """Base class for Response.""" default_status = 200 default_content_type = 'text/plain;' def __init__(self, body=None, status=None, headers=None): self._body = body if body else [b''] self._status_code = status or self.default_status self.headers = Headers() self._cookies = SimpleCookie() if headers: for name, value in headers.items(): self.headers.add_header(name, value) @property def body(self): return self._body @property def status_code(self): """ The HTTP status code as an integer (e.g. 404).""" return self._status_code @property def status(self): """ The HTTP status line as a string (e.g. ``404 Not Found``).""" status = _HTTP_STATUS_LINES.get(self._status_code) return str(status or ('{} Unknown'.format(self._status_code))) @status.setter def status(self, status_code): if not 100 <= status_code <= 999: raise ValueError('Status code out of range.') self._status_code = status_code @property def headerlist(self): """ WSGI conform list of (header, value) tuples. """ if 'Content-Type' not in self.headers: self.headers.add_header('Content-Type', self.default_content_type) if self._cookies: for c in self._cookies.values(): self.headers.add_header('Set-Cookie', c.OutputString()) return self.headers.items() def set_cookie(self, key, value, expires=None, max_age=None, path='/', secret=None, digestmod=hashlib.sha256): from kobin.app import current_config if secret is None: secret = current_config('SECRET_KEY') if secret: if isinstance(secret, str): secret = secret.encode('utf-8') encoded = base64.b64encode(pickle.dumps((key, value), pickle.HIGHEST_PROTOCOL)) sig = base64.b64encode(hmac.new(secret, encoded, digestmod=digestmod).digest()) value_bytes = b'!' + sig + b'?' + encoded value = value_bytes.decode('utf-8') self._cookies[key] = value if len(key) + len(value) > 3800: raise ValueError('Content does not fit into a cookie.') if max_age is not None: if isinstance(max_age, int): max_age_value = max_age else: max_age_value = max_age.seconds + max_age.days * 24 * 3600 self._cookies[key]['max-age'] = max_age_value if expires is not None: if isinstance(expires, int): expires_value = expires else: expires_value = time.strftime("%a, %d %b %Y %H:%M:%S GMT", expires.timetuple()) self._cookies[key]['expires'] = expires_value if path: self._cookies[key]['path'] = path def delete_cookie(self, key, **kwargs): kwargs['max_age'] = -1 kwargs['expires'] = 0 self.set_cookie(key, '', **kwargs)
[docs]class Response(BaseResponse): """Returns a plain text from unicode object.""" default_content_type = 'text/plain; charset=UTF-8' def __init__(self, body='', status=None, headers=None, charset='utf-8'): if isinstance(body, str): body = body.encode(charset) iterable_body = [body] super().__init__(iterable_body, status, headers) self.charset = charset
[docs]class JSONResponse(BaseResponse): """Returns a HTML text from dict or OrderedDict.""" default_content_type = 'application/json; charset=UTF-8' def __init__(self, dic, status=200, headers=None, charset='utf-8', **dump_args): body = [json.dumps(dic, **dump_args).encode(charset)] super().__init__(body, status=status, headers=headers)
[docs]class TemplateResponse(BaseResponse): """Returns a html using jinja2 template engine""" default_content_type = 'text/html; charset=UTF-8' def __init__(self, filename, status=200, headers=None, charset='utf-8', **tpl_args): from .app import current_config template_env = current_config('TEMPLATE_ENVIRONMENT') if template_env is None: raise HTTPError('TEMPLATE_ENVIRONMENT is not found in your config.') template = template_env.get_template(filename) body = [template.render(**tpl_args).encode(charset)] super().__init__(body, status=status, headers=headers)
[docs]class RedirectResponse(BaseResponse): """Redirect the specified url.""" def __init__(self, url): status = 303 if request.get('SERVER_PROTOCOL') == "HTTP/1.1" else 302 super().__init__([b''], status=status, headers={'Location': urljoin(request.url, url)})
[docs]class HTTPError(Response, Exception): """Return the error message when raise this class.""" default_status = 500 def __init__(self, body, status, headers=None, charset='utf-8'): super().__init__(body=body, status=status, headers=headers, charset=charset)