Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add JSONResponse class #2569

Merged
merged 37 commits into from Dec 11, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
c5a77d5
Move response stuff into folder
prryplatypus Oct 12, 2022
1878dcc
Add JSON response
prryplatypus Oct 12, 2022
600ea26
Fixes
prryplatypus Oct 12, 2022
fc008c0
Add tests
prryplatypus Oct 12, 2022
b51a8fc
Add JSONResponse to init
prryplatypus Oct 12, 2022
8ac9aa7
Fix style
prryplatypus Oct 12, 2022
d45a52c
Rename files
prryplatypus Oct 12, 2022
ae96139
Fix test
prryplatypus Oct 12, 2022
8bbeadc
Update URL
prryplatypus Oct 12, 2022
b4fc113
Add docstring
prryplatypus Oct 16, 2022
e44e3af
Make kwargs public
prryplatypus Oct 16, 2022
f505037
Add common useful methods
prryplatypus Oct 16, 2022
c41b5ab
Different approach
prryplatypus Oct 21, 2022
ce12d6f
Undo previous changes to existing test
prryplatypus Oct 23, 2022
bb6cfb7
Remove unused import
prryplatypus Oct 23, 2022
6159145
Modify JSONResponse working
prryplatypus Oct 29, 2022
662063e
Add tests layout
prryplatypus Oct 29, 2022
0d99522
Add some tests
prryplatypus Oct 30, 2022
829ccbe
Fix custom kwargs
prryplatypus Nov 5, 2022
dee45b4
Add remaining tests
prryplatypus Nov 5, 2022
6580b7c
Merge branch 'main' of github.com:sanic-org/sanic into prry/json-resp…
prryplatypus Nov 5, 2022
49a99b8
Make body type same as superclass
prryplatypus Nov 5, 2022
023708c
Mypy doesn't like body being a property
prryplatypus Nov 5, 2022
43f484d
Add useful methods
prryplatypus Nov 13, 2022
433d025
Improve docstrings
prryplatypus Nov 13, 2022
2d92a59
Add raw_body warning
prryplatypus Nov 13, 2022
d517ab2
Add tests
prryplatypus Nov 13, 2022
15e9a96
Merge branch 'main' of github.com:sanic-org/sanic into prry/json-resp…
prryplatypus Nov 13, 2022
c82d6ef
Merge branch 'main' into prry/json-response
ahopkins Dec 6, 2022
a149ae4
Add default arg for dicts
prryplatypus Dec 8, 2022
31ea38f
Add default always if it's been passed
prryplatypus Dec 8, 2022
a76c7ca
Fix import order
prryplatypus Dec 8, 2022
c89b1a3
Raise a proper exception if default argument passed to list
prryplatypus Dec 8, 2022
2d3344c
Improve message
prryplatypus Dec 8, 2022
27e5594
Update tests
prryplatypus Dec 8, 2022
869d57a
Update return type
prryplatypus Dec 8, 2022
13562d8
Merge branch 'main' of github.com:sanic-org/sanic into prry/json-resp…
prryplatypus Dec 8, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
36 changes: 36 additions & 0 deletions sanic/response/__init__.py
@@ -0,0 +1,36 @@
from .convenience import (
empty,
file,
file_stream,
html,
json,
raw,
redirect,
text,
validate_file,
)
from .types import (
BaseHTTPResponse,
HTTPResponse,
JSONResponse,
ResponseStream,
json_dumps,
)


__all__ = (
"BaseHTTPResponse",
"HTTPResponse",
"JSONResponse",
"ResponseStream",
"empty",
"json",
"text",
"raw",
"html",
"validate_file",
"file",
"redirect",
"file_stream",
"json_dumps",
)
285 changes: 10 additions & 275 deletions sanic/response.py → sanic/response/convenience.py
Expand Up @@ -2,212 +2,20 @@

from datetime import datetime, timezone
from email.utils import formatdate, parsedate_to_datetime
from functools import partial
from mimetypes import guess_type
from os import path
from pathlib import PurePath
from time import time
from typing import (
TYPE_CHECKING,
Any,
AnyStr,
Callable,
Coroutine,
Dict,
Iterator,
Optional,
Tuple,
TypeVar,
Union,
)
from typing import Any, AnyStr, Callable, Dict, Optional, Union
from urllib.parse import quote_plus

from sanic.compat import Header, open_async, stat_async
from sanic.constants import DEFAULT_HTTP_CONTENT_TYPE
from sanic.cookies import CookieJar
from sanic.exceptions import SanicException, ServerError
from sanic.helpers import (
Default,
_default,
has_message_body,
remove_entity_headers,
)
from sanic.http import Http
from sanic.helpers import Default, _default
from sanic.log import logger
from sanic.models.protocol_types import HTMLProtocol, Range


if TYPE_CHECKING:
from sanic.asgi import ASGIApp
from sanic.http.http3 import HTTPReceiver
from sanic.request import Request
else:
Request = TypeVar("Request")


try:
from ujson import dumps as json_dumps
except ImportError:
# This is done in order to ensure that the JSON response is
# kept consistent across both ujson and inbuilt json usage.
from json import dumps

json_dumps = partial(dumps, separators=(",", ":"))


class BaseHTTPResponse:
"""
The base class for all HTTP Responses
"""

__slots__ = (
"asgi",
"body",
"content_type",
"stream",
"status",
"headers",
"_cookies",
)

_dumps = json_dumps

def __init__(self):
self.asgi: bool = False
self.body: Optional[bytes] = None
self.content_type: Optional[str] = None
self.stream: Optional[Union[Http, ASGIApp, HTTPReceiver]] = None
self.status: int = None
self.headers = Header({})
self._cookies: Optional[CookieJar] = None

def __repr__(self):
class_name = self.__class__.__name__
return f"<{class_name}: {self.status} {self.content_type}>"

def _encode_body(self, data: Optional[AnyStr]):
if data is None:
return b""
return (
data.encode() if hasattr(data, "encode") else data # type: ignore
)

@property
def cookies(self) -> CookieJar:
"""
The response cookies. Cookies should be set and written as follows:

.. code-block:: python

response.cookies["test"] = "It worked!"
response.cookies["test"]["domain"] = ".yummy-yummy-cookie.com"
response.cookies["test"]["httponly"] = True

`See user guide re: cookies
<https://sanicframework.org/guide/basics/cookies.html>`__

:return: the cookie jar
:rtype: CookieJar
"""
if self._cookies is None:
self._cookies = CookieJar(self.headers)
return self._cookies

@property
def processed_headers(self) -> Iterator[Tuple[bytes, bytes]]:
"""
Obtain a list of header tuples encoded in bytes for sending.

Add and remove headers based on status and content_type.

:return: response headers
:rtype: Tuple[Tuple[bytes, bytes], ...]
"""
# TODO: Make a blacklist set of header names and then filter with that
if self.status in (304, 412): # Not Modified, Precondition Failed
self.headers = remove_entity_headers(self.headers)
if has_message_body(self.status):
self.headers.setdefault("content-type", self.content_type)
# Encode headers into bytes
return (
(name.encode("ascii"), f"{value}".encode(errors="surrogateescape"))
for name, value in self.headers.items()
)

async def send(
self,
data: Optional[AnyStr] = None,
end_stream: Optional[bool] = None,
) -> None:
"""
Send any pending response headers and the given data as body.

:param data: str or bytes to be written
:param end_stream: whether to close the stream after this block
"""
if data is None and end_stream is None:
end_stream = True
if self.stream is None:
raise SanicException(
"No stream is connected to the response object instance."
)
if self.stream.send is None:
if end_stream and not data:
return
raise ServerError(
"Response stream was ended, no more response data is "
"allowed to be sent."
)
data = (
data.encode() # type: ignore
if hasattr(data, "encode")
else data or b""
)
await self.stream.send(
data, # type: ignore
end_stream=end_stream or False,
)


class HTTPResponse(BaseHTTPResponse):
"""
HTTP response to be sent back to the client.

:param body: the body content to be returned
:type body: Optional[bytes]
:param status: HTTP response number. **Default=200**
:type status: int
:param headers: headers to be returned
:type headers: Optional;
:param content_type: content type to be returned (as a header)
:type content_type: Optional[str]
"""

__slots__ = ()

def __init__(
self,
body: Optional[AnyStr] = None,
status: int = 200,
headers: Optional[Union[Header, Dict[str, str]]] = None,
content_type: Optional[str] = None,
):
super().__init__()

self.content_type: Optional[str] = content_type
self.body = self._encode_body(body)
self.status = status
self.headers = Header(headers or {})
self._cookies = None

async def eof(self):
await self.send("", True)

async def __aenter__(self):
return self.send

async def __aexit__(self, *_):
await self.eof()
from .types import HTTPResponse, JSONResponse, ResponseStream


def empty(
Expand All @@ -229,7 +37,7 @@ def json(
content_type: str = "application/json",
dumps: Optional[Callable[..., str]] = None,
**kwargs: Any,
) -> HTTPResponse:
) -> JSONResponse:
"""
Returns response object with body in json format.

Expand All @@ -238,13 +46,14 @@ def json(
:param headers: Custom Headers.
:param kwargs: Remaining arguments that are passed to the json encoder.
"""
if not dumps:
dumps = BaseHTTPResponse._dumps
return HTTPResponse(
dumps(body, **kwargs),
headers=headers,

return JSONResponse(
body,
status=status,
headers=headers,
content_type=content_type,
dumps=dumps,
**kwargs,
)


Expand Down Expand Up @@ -465,80 +274,6 @@ def redirect(
)


class ResponseStream:
"""
ResponseStream is a compat layer to bridge the gap after the deprecation
of StreamingHTTPResponse. It will be removed when:
- file_stream is moved to new style streaming
- file and file_stream are combined into a single API
"""

__slots__ = (
"_cookies",
"content_type",
"headers",
"request",
"response",
"status",
"streaming_fn",
)

def __init__(
self,
streaming_fn: Callable[
[Union[BaseHTTPResponse, ResponseStream]],
Coroutine[Any, Any, None],
],
status: int = 200,
headers: Optional[Union[Header, Dict[str, str]]] = None,
content_type: Optional[str] = None,
):
self.streaming_fn = streaming_fn
self.status = status
self.headers = headers or Header()
self.content_type = content_type
self.request: Optional[Request] = None
self._cookies: Optional[CookieJar] = None

async def write(self, message: str):
await self.response.send(message)

async def stream(self) -> HTTPResponse:
if not self.request:
raise ServerError("Attempted response to unknown request")
self.response = await self.request.respond(
headers=self.headers,
status=self.status,
content_type=self.content_type,
)
await self.streaming_fn(self)
return self.response

async def eof(self) -> None:
await self.response.eof()

@property
def cookies(self) -> CookieJar:
if self._cookies is None:
self._cookies = CookieJar(self.headers)
return self._cookies

@property
def processed_headers(self):
return self.response.processed_headers

@property
def body(self):
return self.response.body

def __call__(self, request: Request) -> ResponseStream:
self.request = request
return self

def __await__(self):
return self.stream().__await__()


async def file_stream(
location: Union[str, PurePath],
status: int = 200,
Expand Down