88
99from sanic import HTTPResponse , Request
1010
11+ try :
12+ import brotli
13+
14+ BROTLI_AVAILABLE = True
15+ except ImportError :
16+ BROTLI_AVAILABLE = False
17+
1118from . import _read_signals
1219from .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
5379async 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
5993P = ParamSpec ("P" )
@@ -98,3 +132,9 @@ async def wrapper(*args: P.args, **kwargs: P.kwargs) -> DatastarResponse | None:
98132
99133async 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