Skip to content

Commit 2a3146f

Browse files
committed
First pass at brotli support for sanic
1 parent ac15361 commit 2a3146f

1 file changed

Lines changed: 43 additions & 3 deletions

File tree

src/datastar_py/sanic.py

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,13 @@
88

99
from sanic import HTTPResponse, Request
1010

11+
try:
12+
import brotli
13+
14+
BROTLI_AVAILABLE = True
15+
except ImportError:
16+
BROTLI_AVAILABLE = False
17+
1118
from . import _read_signals
1219
from .sse import SSE_HEADERS, DatastarEvent, DatastarEvents, ServerSentEventGenerator
1320

@@ -28,7 +35,10 @@ def __init__(
2835
content: DatastarEvent | Collection[DatastarEvent] | None = None,
2936
status: int | None = None,
3037
headers: Mapping[str, str] | None = None,
38+
brotli: bool = False,
3139
) -> None:
40+
self._brotli = brotli
41+
self._brotli_compressor = None
3242
if not content:
3343
status = status or 204
3444
elif not isinstance(content, str):
@@ -47,13 +57,37 @@ async def send(
4757
# When the response is created with no content, it's set to a 204 by default
4858
# if we end up streaming to it, change the status code to 200 before sending.
4959
self.status = 200
50-
await super().send(event, end_stream=end_stream)
60+
if not event and end_stream is None:
61+
end_stream = True
62+
data = event.encode("utf-8") if event else b""
63+
if self._brotli:
64+
if not BROTLI_AVAILABLE:
65+
raise ImportError("brotli is not installed")
66+
if self._brotli_compressor is None and event:
67+
self._brotli_compressor = brotli.Compressor(
68+
mode=brotli.MODE_TEXT
69+
) # TODO: compression/window settings
70+
self.headers["Content-Encoding"] = "br"
71+
if data:
72+
data = self._brotli_compressor.process(data)
73+
data += self._brotli_compressor.flush()
74+
if end_stream and self._brotli_compressor:
75+
data += self._brotli_compressor.finish()
76+
await super().send(data, end_stream=end_stream)
5177

5278

5379
async def datastar_respond(
54-
request: Request, *, status: int = 200, headers: Mapping[str, str] | None = None
80+
request: Request,
81+
*,
82+
status: int = 200,
83+
headers: Mapping[str, str] | None = None,
84+
brotli: bool = False,
5585
) -> DatastarResponse:
56-
return await request.respond(DatastarResponse(status=status, headers=headers))
86+
return await request.respond(
87+
DatastarResponse(
88+
status=status, headers=headers, brotli=brotli and _client_accepts_brotli(request)
89+
)
90+
)
5791

5892

5993
P = ParamSpec("P")
@@ -98,3 +132,9 @@ async def wrapper(*args: P.args, **kwargs: P.kwargs) -> DatastarResponse | None:
98132

99133
async def read_signals(request: Request) -> dict[str, Any] | None:
100134
return _read_signals(request.method, request.headers, request.args, request.body)
135+
136+
137+
def _client_accepts_brotli(request: Request) -> bool:
138+
"""Return True if the client's Accept-Encoding includes brotli."""
139+
accept_encoding = request.headers.get("Accept-Encoding", "")
140+
return any(part.split(";")[0].strip() == "br" for part in accept_encoding.split(","))

0 commit comments

Comments
 (0)