chore: rename project coven → hack-house ⛧

Rebrand the Rust client crate (coven/ → hh/, package+binary "hack-house"),
README, CLI strings, and branch (coven → hack-house). Gitea repo renamed
cmd-chat → hack-house to match. Crypto/server logic unchanged; selftest +
golden-vector test still green, binary is now `hack-house`.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
leetcrypt
2026-05-30 13:29:14 -07:00
parent 82a04f3e12
commit bb1d662ee1
2730 changed files with 712933 additions and 46 deletions
@@ -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",
)
@@ -0,0 +1,412 @@
from __future__ import annotations
from datetime import datetime, timezone
from email.utils import formatdate, parsedate_to_datetime
from mimetypes import guess_type
from os import path
from pathlib import PurePath
from time import time
from typing import Any, AnyStr, Callable
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.helpers import Default, _default
from sanic.log import logger
from sanic.models.protocol_types import HTMLProtocol, Range
from .types import HTTPResponse, JSONResponse, ResponseStream
def empty(
status: int = 204, headers: dict[str, str] | None = None
) -> HTTPResponse:
"""Returns an empty response to the client.
Args:
status (int, optional): HTTP response code. Defaults to `204`.
headers ([type], optional): Custom HTTP headers. Defaults to `None`.
Returns:
HTTPResponse: An empty response to the client.
"""
return HTTPResponse(body=b"", status=status, headers=headers)
def json(
body: Any,
status: int = 200,
headers: dict[str, str] | None = None,
content_type: str = "application/json",
dumps: Callable[..., AnyStr] | None = None,
**kwargs: Any,
) -> JSONResponse:
"""Returns response object with body in json format.
Args:
body (Any): Response data to be serialized.
status (int, optional): HTTP response code. Defaults to `200`.
headers (Dict[str, str], optional): Custom HTTP headers. Defaults to `None`.
content_type (str, optional): The content type (string) of the response. Defaults to `"application/json"`.
dumps (Callable[..., AnyStr], optional): A custom json dumps function. Defaults to `None`.
**kwargs (Any): Remaining arguments that are passed to the json encoder.
Returns:
JSONResponse: A response object with body in json format.
""" # noqa: E501
return JSONResponse(
body,
status=status,
headers=headers,
content_type=content_type,
dumps=dumps,
**kwargs,
)
def text(
body: str,
status: int = 200,
headers: dict[str, str] | None = None,
content_type: str = "text/plain; charset=utf-8",
) -> HTTPResponse:
"""Returns response object with body in text format.
Args:
body (str): Response data.
status (int, optional): HTTP response code. Defaults to `200`.
headers (Dict[str, str], optional): Custom HTTP headers. Defaults to `None`.
content_type (str, optional): The content type (string) of the response. Defaults to `"text/plain; charset=utf-8"`.
Returns:
HTTPResponse: A response object with body in text format.
Raises:
TypeError: If the body is not a string.
""" # noqa: E501
if not isinstance(body, str):
raise TypeError(
f"Bad body type. Expected str, got {type(body).__name__})"
)
return HTTPResponse(
body, status=status, headers=headers, content_type=content_type
)
def raw(
body: AnyStr | None,
status: int = 200,
headers: dict[str, str] | None = None,
content_type: str = DEFAULT_HTTP_CONTENT_TYPE,
) -> HTTPResponse:
"""Returns response object without encoding the body.
Args:
body (Optional[AnyStr]): Response data.
status (int, optional): HTTP response code. Defaults to `200`.
headers (Dict[str, str], optional): Custom HTTP headers. Defaults to `None`.
content_type (str, optional): The content type (string) of the response. Defaults to `"application/octet-stream"`.
Returns:
HTTPResponse: A response object without encoding the body.
""" # noqa: E501
return HTTPResponse(
body=body,
status=status,
headers=headers,
content_type=content_type,
)
def html(
body: str | bytes | HTMLProtocol,
status: int = 200,
headers: dict[str, str] | None = None,
) -> HTTPResponse:
"""Returns response object with body in html format.
Body should be a `str` or `bytes` like object, or an object with `__html__` or `_repr_html_`.
Args:
body (Union[str, bytes, HTMLProtocol]): Response data.
status (int, optional): HTTP response code. Defaults to `200`.
headers (Dict[str, str], optional): Custom HTTP headers. Defaults to `None`.
Returns:
HTTPResponse: A response object with body in html format.
""" # noqa: E501
if not isinstance(body, (str, bytes)):
if hasattr(body, "__html__"):
body = body.__html__()
elif hasattr(body, "_repr_html_"):
body = body._repr_html_()
return HTTPResponse(
body,
status=status,
headers=headers,
content_type="text/html; charset=utf-8",
)
async def validate_file(
request_headers: Header, last_modified: datetime | float | int
) -> HTTPResponse | None:
"""Validate file based on request headers.
Args:
request_headers (Header): The request headers.
last_modified (Union[datetime, float, int]): The last modified date and time of the file.
Returns:
Optional[HTTPResponse]: A response object with status 304 if the file is not modified.
""" # noqa: E501
try:
if_modified_since = request_headers.getone("If-Modified-Since")
except KeyError:
return None
try:
if_modified_since = parsedate_to_datetime(if_modified_since)
except (TypeError, ValueError):
logger.warning(
"Ignorning invalid If-Modified-Since header received: '%s'",
if_modified_since,
)
return None
if not isinstance(last_modified, datetime):
last_modified = datetime.fromtimestamp(
float(last_modified), tz=timezone.utc
).replace(microsecond=0)
if (
last_modified.utcoffset() is None
and if_modified_since.utcoffset() is not None
):
logger.warning(
"Cannot compare tz-aware and tz-naive datetimes. To avoid "
"this conflict Sanic is converting last_modified to UTC."
)
last_modified.replace(tzinfo=timezone.utc)
elif (
last_modified.utcoffset() is not None
and if_modified_since.utcoffset() is None
):
logger.warning(
"Cannot compare tz-aware and tz-naive datetimes. To avoid "
"this conflict Sanic is converting if_modified_since to UTC."
)
if_modified_since.replace(tzinfo=timezone.utc)
if last_modified.timestamp() <= if_modified_since.timestamp():
return HTTPResponse(status=304)
return None
async def file(
location: str | PurePath,
status: int = 200,
request_headers: Header | None = None,
validate_when_requested: bool = True,
mime_type: str | None = None,
headers: dict[str, str] | None = None,
filename: str | None = None,
last_modified: datetime | float | int | Default | None = _default,
max_age: float | int | None = None,
no_store: bool | None = None,
_range: Range | None = None,
) -> HTTPResponse:
"""Return a response object with file data.
Args:
location (Union[str, PurePath]): Location of file on system.
status (int, optional): HTTP response code. Won't enforce the passed in status if only a part of the content will be sent (206) or file is being validated (304). Defaults to 200.
request_headers (Optional[Header], optional): The request headers.
validate_when_requested (bool, optional): If `True`, will validate the file when requested. Defaults to True.
mime_type (Optional[str], optional): Specific mime_type.
headers (Optional[Dict[str, str]], optional): Custom Headers.
filename (Optional[str], optional): Override filename.
last_modified (Optional[Union[datetime, float, int, Default]], optional): The last modified date and time of the file.
max_age (Optional[Union[float, int]], optional): Max age for cache control.
no_store (Optional[bool], optional): Any cache should not store this response. Defaults to None.
_range (Optional[Range], optional):
Returns:
HTTPResponse: The response object with the file data.
""" # noqa: E501
if isinstance(last_modified, datetime):
last_modified = last_modified.replace(microsecond=0).timestamp()
elif isinstance(last_modified, Default):
stat = await stat_async(location)
last_modified = stat.st_mtime
if (
validate_when_requested
and request_headers is not None
and last_modified
):
response = await validate_file(request_headers, last_modified)
if response:
return response
headers = headers or {}
if last_modified:
headers.setdefault(
"Last-Modified", formatdate(last_modified, usegmt=True)
)
if filename:
headers.setdefault(
"Content-Disposition", f'attachment; filename="{filename}"'
)
if no_store:
cache_control = "no-store"
elif max_age:
cache_control = f"public, max-age={max_age}"
headers.setdefault(
"expires",
formatdate(
time() + max_age,
usegmt=True,
),
)
else:
cache_control = "no-cache"
headers.setdefault("cache-control", cache_control)
filename = filename or path.split(location)[-1]
async with await open_async(location, mode="rb") as f:
if _range:
await f.seek(_range.start)
out_stream = await f.read(_range.size)
headers["Content-Range"] = (
f"bytes {_range.start}-{_range.end}/{_range.total}"
)
status = 206
else:
out_stream = await f.read()
content_type = mime_type or guess_content_type(
filename, fallback="text/plain; charset=utf-8"
)
return HTTPResponse(
body=out_stream,
status=status,
headers=headers,
content_type=content_type,
)
def redirect(
to: str,
headers: dict[str, str] | None = None,
status: int = 302,
content_type: str = "text/html; charset=utf-8",
) -> HTTPResponse:
"""Cause a HTTP redirect (302 by default) by setting a Location header.
Args:
to (str): path or fully qualified URL to redirect to
headers (Optional[Dict[str, str]], optional): optional dict of headers to include in the new request. Defaults to None.
status (int, optional): status code (int) of the new request, defaults to 302. Defaults to 302.
content_type (str, optional): the content type (string) of the response. Defaults to "text/html; charset=utf-8".
Returns:
HTTPResponse: A response object with the redirect.
""" # noqa: E501
headers = headers or {}
# URL Quote the URL before redirecting
safe_to = quote_plus(to, safe=":/%#?&=@[]!$&'()*+,;")
# According to RFC 7231, a relative URI is now permitted.
headers["Location"] = safe_to
return HTTPResponse(
status=status, headers=headers, content_type=content_type
)
async def file_stream(
location: str | PurePath,
status: int = 200,
chunk_size: int = 4096,
mime_type: str | None = None,
headers: dict[str, str] | None = None,
filename: str | None = None,
_range: Range | None = None,
) -> ResponseStream:
"""Return a streaming response object with file data.
:param location: Location of file on system.
:param chunk_size: The size of each chunk in the stream (in bytes)
:param mime_type: Specific mime_type.
:param headers: Custom Headers.
:param filename: Override filename.
:param _range:
Args:
location (Union[str, PurePath]): Location of file on system.
status (int, optional): HTTP response code. Won't enforce the passed in status if only a part of the content will be sent (206) or file is being validated (304). Defaults to `200`.
chunk_size (int, optional): The size of each chunk in the stream (in bytes). Defaults to `4096`.
mime_type (Optional[str], optional): Specific mime_type.
headers (Optional[Dict[str, str]], optional): Custom HTTP headers.
filename (Optional[str], optional): Override filename.
_range (Optional[Range], optional): The range of bytes to send.
""" # noqa: E501
headers = headers or {}
if filename:
headers.setdefault(
"Content-Disposition", f'attachment; filename="{filename}"'
)
filename = filename or path.split(location)[-1]
mime_type = mime_type or guess_type(filename)[0] or "text/plain"
if _range:
start = _range.start
end = _range.end
total = _range.total
headers["Content-Range"] = f"bytes {start}-{end}/{total}"
status = 206
async def _streaming_fn(response):
async with await open_async(location, mode="rb") as f:
if _range:
await f.seek(_range.start)
to_send = _range.size
while to_send > 0:
content = await f.read(min((_range.size, chunk_size)))
if len(content) < 1:
break
to_send -= len(content)
await response.write(content)
else:
while True:
content = await f.read(chunk_size)
if len(content) < 1:
break
await response.write(content)
return ResponseStream(
streaming_fn=_streaming_fn,
status=status,
headers=headers,
content_type=mime_type,
)
def guess_content_type(
file_path: str | PurePath,
fallback: str = DEFAULT_HTTP_CONTENT_TYPE,
) -> str:
"""Guess the content type (rather than MIME only) by the file extension."""
mediatype = guess_type(file_path)[0]
if mediatype is None:
return fallback
if mediatype.startswith("text/"):
return f"{mediatype}; charset=utf-8"
return mediatype
@@ -0,0 +1,553 @@
from __future__ import annotations
from collections.abc import Coroutine, Iterator
from datetime import datetime
from typing import (
TYPE_CHECKING,
Any,
AnyStr,
Callable,
TypeVar,
)
from sanic.compat import Header
from sanic.cookies import CookieJar
from sanic.cookies.response import Cookie, SameSite
from sanic.exceptions import SanicException, ServerError
from sanic.helpers import Default, _default, has_message_body, json_dumps
from sanic.http import Http
if TYPE_CHECKING:
from sanic.asgi import ASGIApp
from sanic.http.http3 import HTTPReceiver
from sanic.request import Request
else:
Request = TypeVar("Request")
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: bytes | None = None
self.content_type: str | None = None
self.stream: Http | ASGIApp | HTTPReceiver | None = None
self.status: int = None
self.headers = Header({})
self._cookies: CookieJar | None = None
def __repr__(self):
class_name = self.__class__.__name__
return f"<{class_name}: {self.status} {self.content_type}>"
def _encode_body(self, data: str | bytes | None):
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.
See [Cookies](/en/guide/basics/cookies.html)
Returns:
CookieJar: The response cookies
"""
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.
Returns:
Iterator[Tuple[bytes, bytes]]: A list of header tuples encoded in bytes for sending
""" # noqa: E501
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: AnyStr | None = None,
end_stream: bool | None = None,
) -> None:
"""Send any pending response headers and the given data as body.
Args:
data (Optional[AnyStr], optional): str or bytes to be written. Defaults to `None`.
end_stream (Optional[bool], optional): whether to close the stream after this block. Defaults to `None`.
""" # noqa: E501
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() if hasattr(data, "encode") else data or b"" # type: ignore
await self.stream.send(
data, # type: ignore
end_stream=end_stream or False,
)
def add_cookie(
self,
key: str,
value: str,
*,
path: str = "/",
domain: str | None = None,
secure: bool = True,
max_age: int | None = None,
expires: datetime | None = None,
httponly: bool = False,
samesite: SameSite | None = "Lax",
partitioned: bool = False,
comment: str | None = None,
host_prefix: bool = False,
secure_prefix: bool = False,
) -> Cookie:
"""Add a cookie to the CookieJar
See [Cookies](/en/guide/basics/cookies.html)
Args:
key (str): The key to be added
value (str): The value to be added
path (str, optional): Path of the cookie. Defaults to `"/"`.
domain (Optional[str], optional): Domain of the cookie. Defaults to `None`.
secure (bool, optional): Whether the cookie is secure. Defaults to `True`.
max_age (Optional[int], optional): Max age of the cookie. Defaults to `None`.
expires (Optional[datetime], optional): Expiry date of the cookie. Defaults to `None`.
httponly (bool, optional): Whether the cookie is http only. Defaults to `False`.
samesite (Optional[SameSite], optional): SameSite policy of the cookie. Defaults to `"Lax"`.
partitioned (bool, optional): Whether the cookie is partitioned. Defaults to `False`.
comment (Optional[str], optional): Comment of the cookie. Defaults to `None`.
host_prefix (bool, optional): Whether to add __Host- as a prefix to the key. This requires that path="/", domain=None, and secure=True. Defaults to `False`.
secure_prefix (bool, optional): Whether to add __Secure- as a prefix to the key. This requires that secure=True. Defaults to `False`.
Returns:
Cookie: The cookie that was added
""" # noqa: E501
return self.cookies.add_cookie(
key=key,
value=value,
path=path,
domain=domain,
secure=secure,
max_age=max_age,
expires=expires,
httponly=httponly,
samesite=samesite,
partitioned=partitioned,
comment=comment,
host_prefix=host_prefix,
secure_prefix=secure_prefix,
)
def delete_cookie(
self,
key: str,
*,
path: str = "/",
domain: str | None = None,
host_prefix: bool = False,
secure_prefix: bool = False,
) -> None:
"""Delete a cookie
This will effectively set it as Max-Age: 0, which a browser should
interpret it to mean: "delete the cookie".
Since it is a browser/client implementation, your results may vary
depending upon which client is being used.
See [Cookies](/en/guide/basics/cookies.html)
Args:
key (str): The key to be deleted
path (str, optional): Path of the cookie. Defaults to `"/"`.
domain (Optional[str], optional): Domain of the cookie. Defaults to `None`.
host_prefix (bool, optional): Whether to add __Host- as a prefix to the key. This requires that path="/", domain=None, and secure=True. Defaults to `False`.
secure_prefix (bool, optional): Whether to add __Secure- as a prefix to the key. This requires that secure=True. Defaults to `False`.
""" # noqa: E501
self.cookies.delete_cookie(
key=key,
path=path,
domain=domain,
host_prefix=host_prefix,
secure_prefix=secure_prefix,
)
class HTTPResponse(BaseHTTPResponse):
"""HTTP response to be sent back to the client.
Args:
body (Optional[Any], optional): The body content to be returned. Defaults to `None`.
status (int, optional): HTTP response number. Defaults to `200`.
headers (Optional[Union[Header, Dict[str, str]]], optional): Headers to be returned. Defaults to `None`.
content_type (Optional[str], optional): Content type to be returned (as a header). Defaults to `None`.
""" # noqa: E501
__slots__ = ()
def __init__(
self,
body: Any = None,
status: int = 200,
headers: Header | dict[str, str] | None = None,
content_type: str | None = None,
):
super().__init__()
self.content_type: str | None = content_type
self.body = self._encode_body(body)
self.status = status
self.headers = Header(headers or {})
self._cookies = None
async def eof(self):
"""Send a EOF (End of File) message to the client."""
await self.send("", True)
async def __aenter__(self):
return self.send
async def __aexit__(self, *_):
await self.eof()
class JSONResponse(HTTPResponse):
"""Convenience class for JSON responses
HTTP response to be sent back to the client, when the response
is of json type. Offers several utilities to manipulate common
json data types.
Args:
body (Optional[Any], optional): The body content to be returned. Defaults to `None`.
status (int, optional): HTTP response number. Defaults to `200`.
headers (Optional[Union[Header, Dict[str, str]]], optional): Headers to be returned. Defaults to `None`.
content_type (str, optional): Content type to be returned (as a header). Defaults to `"application/json"`.
dumps (Optional[Callable[..., AnyStr]], optional): The function to use for json encoding. Defaults to `None`.
**kwargs (Any, optional): The kwargs to pass to the json encoding function. Defaults to `{}`.
""" # noqa: E501
__slots__ = (
"_body",
"_body_manually_set",
"_initialized",
"_raw_body",
"_use_dumps",
"_use_dumps_kwargs",
)
def __init__(
self,
body: Any = None,
status: int = 200,
headers: Header | dict[str, str] | None = None,
content_type: str = "application/json",
dumps: Callable[..., AnyStr] | None = None,
**kwargs: Any,
):
self._initialized = False
self._body_manually_set = False
self._use_dumps: Callable[..., str | bytes] = (
dumps or BaseHTTPResponse._dumps
)
self._use_dumps_kwargs = kwargs
self._raw_body = body
super().__init__(
self._encode_body(self._use_dumps(body, **self._use_dumps_kwargs)),
headers=headers,
status=status,
content_type=content_type,
)
self._initialized = True
def _check_body_not_manually_set(self):
if self._body_manually_set:
raise SanicException(
"Cannot use raw_body after body has been manually set."
)
@property
def raw_body(self) -> Any:
"""Returns the raw body, as long as body has not been manually set previously.
NOTE: This object should not be mutated, as it will not be
reflected in the response body. If you need to mutate the
response body, consider using one of the provided methods in
this class or alternatively call set_body() with the mutated
object afterwards or set the raw_body property to it.
Returns:
Optional[Any]: The raw body
""" # noqa: E501
self._check_body_not_manually_set()
return self._raw_body
@raw_body.setter
def raw_body(self, value: Any):
self._body_manually_set = False
self._body = self._encode_body(
self._use_dumps(value, **self._use_dumps_kwargs)
)
self._raw_body = value
@property # type: ignore
def body(self) -> bytes | None: # type: ignore
"""Returns the response body.
Returns:
Optional[bytes]: The response body
"""
return self._body
@body.setter
def body(self, value: bytes | None):
self._body = value
if not self._initialized:
return
self._body_manually_set = True
def set_body(
self,
body: Any,
dumps: Callable[..., AnyStr] | None = None,
**dumps_kwargs: Any,
) -> None:
"""Set the response body to the given value, using the given dumps function
Sets a new response body using the given dumps function
and kwargs, or falling back to the defaults given when
creating the object if none are specified.
Args:
body (Any): The body to set
dumps (Optional[Callable[..., AnyStr]], optional): The function to use for json encoding. Defaults to `None`.
**dumps_kwargs (Any, optional): The kwargs to pass to the json encoding function. Defaults to `{}`.
Examples:
```python
response = JSONResponse({"foo": "bar"})
response.set_body({"bar": "baz"})
assert response.body == b'{"bar": "baz"}'
```
""" # noqa: E501
self._body_manually_set = False
self._raw_body = body
use_dumps = dumps or self._use_dumps
use_dumps_kwargs = dumps_kwargs if dumps else self._use_dumps_kwargs
self._body = self._encode_body(use_dumps(body, **use_dumps_kwargs))
def append(self, value: Any) -> None:
"""Appends a value to the response raw_body, ensuring that body is kept up to date.
This can only be used if raw_body is a list.
Args:
value (Any): The value to append
Raises:
SanicException: If the body is not a list
""" # noqa: E501
self._check_body_not_manually_set()
if not isinstance(self._raw_body, list):
raise SanicException("Cannot append to a non-list object.")
self._raw_body.append(value)
self.raw_body = self._raw_body
def extend(self, value: Any) -> None:
"""Extends the response's raw_body with the given values, ensuring that body is kept up to date.
This can only be used if raw_body is a list.
Args:
value (Any): The values to extend with
Raises:
SanicException: If the body is not a list
""" # noqa: E501
self._check_body_not_manually_set()
if not isinstance(self._raw_body, list):
raise SanicException("Cannot extend a non-list object.")
self._raw_body.extend(value)
self.raw_body = self._raw_body
def update(self, *args, **kwargs) -> None:
"""Updates the response's raw_body with the given values, ensuring that body is kept up to date.
This can only be used if raw_body is a dict.
Args:
*args: The args to update with
**kwargs: The kwargs to update with
Raises:
SanicException: If the body is not a dict
""" # noqa: E501
self._check_body_not_manually_set()
if not isinstance(self._raw_body, dict):
raise SanicException("Cannot update a non-dict object.")
self._raw_body.update(*args, **kwargs)
self.raw_body = self._raw_body
def pop(self, key: Any, default: Any = _default) -> Any:
"""Pops a key from the response's raw_body, ensuring that body is kept up to date.
This can only be used if raw_body is a dict or a list.
Args:
key (Any): The key to pop
default (Any, optional): The default value to return if the key is not found. Defaults to `_default`.
Raises:
SanicException: If the body is not a dict or a list
TypeError: If the body is a list and a default value is provided
Returns:
Any: The value that was popped
""" # noqa: E501
self._check_body_not_manually_set()
if not isinstance(self._raw_body, (list, dict)):
raise SanicException(
"Cannot pop from a non-list and non-dict object."
)
if isinstance(default, Default):
value = self._raw_body.pop(key)
elif isinstance(self._raw_body, list):
raise TypeError("pop doesn't accept a default argument for lists")
else:
value = self._raw_body.pop(key, default)
self.raw_body = self._raw_body
return value
class ResponseStream:
"""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
""" # noqa: E501
__slots__ = (
"_cookies",
"content_type",
"headers",
"request",
"response",
"status",
"streaming_fn",
)
def __init__(
self,
streaming_fn: Callable[
[BaseHTTPResponse | ResponseStream],
Coroutine[Any, Any, None],
],
status: int = 200,
headers: Header | dict[str, str] | None = None,
content_type: str | None = None,
):
if headers is None:
headers = Header()
elif not isinstance(headers, Header):
headers = Header(headers)
self.streaming_fn = streaming_fn
self.status = status
self.headers = headers or Header()
self.content_type = content_type
self.request: Request | None = None
self._cookies: CookieJar | None = 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__()