Skip to content

Commit

Permalink
move request and response state to scope
Browse files Browse the repository at this point in the history
  • Loading branch information
livioribeiro committed Dec 8, 2023
1 parent f1a1c3a commit 3d41654
Show file tree
Hide file tree
Showing 4 changed files with 170 additions and 66 deletions.
13 changes: 13 additions & 0 deletions src/asgikit/constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
SCOPE_ASGIKIT = "__asgikit__"
SCOPE_REQUEST = "request"
SCOPE_REQUEST_ATTRIBUTES = "attributes"
SCOPE_REQUEST_IS_CONSUMED = "is_consumed"
SCOPE_RESPONSE = "response"
SCOPE_RESPONSE_HEADERS = "headers"
SCOPE_RESPONSE_COOKIES = "cookies"
SCOPE_RESPONSE_CONTENT_TYPE = "content_type"
SCOPE_RESPONSE_CONTENT_LENGTH = "content_length"
SCOPE_RESPONSE_ENCODING = "encoding"
SCOPE_RESPONSE_IS_STARTED = "is_started"
SCOPE_RESPONSE_IS_FINISHED = "is_finished"
SCOPE_RESPONSE_STATUS = "status"
63 changes: 40 additions & 23 deletions src/asgikit/requests.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@
from multipart import multipart

from asgikit.asgi import AsgiProtocol, AsgiReceive, AsgiScope, AsgiSend
from asgikit.constants import (
SCOPE_ASGIKIT,
SCOPE_REQUEST,
SCOPE_REQUEST_ATTRIBUTES,
SCOPE_REQUEST_IS_CONSUMED,
)
from asgikit.errors.http import ClientDisconnectError
from asgikit.headers import Headers
from asgikit.query import Query
Expand Down Expand Up @@ -39,91 +45,102 @@ def _parse_cookie(data: str) -> dict[str, str]:

class Request:
__slots__ = (
"_asgi",
"asgi",
"_headers",
"_query",
"_cookie",
"_charset",
"attributes",
"is_consumed",
"response",
"websocket",
)

def __init__(self, scope: AsgiScope, receive: AsgiReceive, send: AsgiSend):
assert scope["type"] in ("http", "websocket")

self._asgi = AsgiProtocol(scope, receive, send)
scope.setdefault(SCOPE_ASGIKIT, {})
scope[SCOPE_ASGIKIT].setdefault(SCOPE_REQUEST, {})
scope[SCOPE_ASGIKIT][SCOPE_REQUEST].setdefault(SCOPE_REQUEST_ATTRIBUTES, {})
scope[SCOPE_ASGIKIT][SCOPE_REQUEST].setdefault(SCOPE_REQUEST_IS_CONSUMED, False)

self.asgi = AsgiProtocol(scope, receive, send)

self._headers: Headers | None = None
self._query: Query | None = None
self._charset = None
self._cookie = None

self.attributes: dict[str, Any] = {}
self.is_consumed = False
self.response = Response(*self.asgi) if self.is_http else None
self.websocket = WebSocket(*self.asgi) if self.is_websocket else None

@property
def attributes(self) -> dict[str, Any]:
return self.asgi.scope[SCOPE_ASGIKIT][SCOPE_REQUEST][SCOPE_REQUEST_ATTRIBUTES]

@property
def is_consumed(self) -> bool:
return self.asgi.scope[SCOPE_ASGIKIT][SCOPE_REQUEST][SCOPE_REQUEST_IS_CONSUMED]

self.response = Response(*self._asgi) if self.is_http else None
self.websocket = WebSocket(*self._asgi) if self.is_websocket else None
def __set_consumed(self):
self.asgi.scope[SCOPE_ASGIKIT][SCOPE_REQUEST][SCOPE_REQUEST_IS_CONSUMED] = True

@property
def is_http(self) -> bool:
return self._asgi.scope["type"] == "http"
return self.asgi.scope["type"] == "http"

@property
def is_websocket(self) -> bool:
return self._asgi.scope["type"] == "websocket"
return self.asgi.scope["type"] == "websocket"

@property
def http_version(self) -> str:
return self._asgi.scope["http_version"]
return self.asgi.scope["http_version"]

@property
def server(self):
return self._asgi.scope["server"]
return self.asgi.scope["server"]

@property
def client(self):
return self._asgi.scope["client"]
return self.asgi.scope["client"]

@property
def scheme(self):
return self._asgi.scope["scheme"]
return self.asgi.scope["scheme"]

@property
def method(self) -> HTTPMethod | None:
"""Return None when request is websocket"""
if method := self._asgi.scope.get("method"):
if method := self.asgi.scope.get("method"):
return HTTPMethod(method)

return None

@property
def root_path(self):
return self._asgi.scope["root_path"]
return self.asgi.scope["root_path"]

@property
def path(self):
return self._asgi.scope["path"]
return self.asgi.scope["path"]

@property
def raw_path(self):
return self._asgi.scope["raw_path"]
return self.asgi.scope["raw_path"]

@property
def headers(self) -> Headers:
if not self._headers:
self._headers = Headers(self._asgi.scope["headers"])
self._headers = Headers(self.asgi.scope["headers"])
return self._headers

@property
def raw_query(self):
return unquote_plus(self._asgi.scope["query_string"].decode("ascii"))
return unquote_plus(self.asgi.scope["query_string"].decode("ascii"))

@property
def query(self) -> Query:
if not self._query:
self._query = Query(self._asgi.scope["query_string"])
self._query = Query(self.asgi.scope["query_string"])
return self._query

@property
Expand Down Expand Up @@ -169,10 +186,10 @@ async def __aiter__(self) -> AsyncIterable[bytes]:
if self.is_consumed:
raise RuntimeError("request has already been consumed")

self.is_consumed = True
self.__set_consumed()

while True:
message = await self._asgi.receive()
message = await self.asgi.receive()

if message["type"] == "http.request":
yield message["body"]
Expand Down
134 changes: 104 additions & 30 deletions src/asgikit/responses.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,18 @@
import aiofiles.os

from asgikit.asgi import AsgiProtocol, AsgiReceive, AsgiScope, AsgiSend
from asgikit.constants import (
SCOPE_ASGIKIT,
SCOPE_RESPONSE,
SCOPE_RESPONSE_CONTENT_LENGTH,
SCOPE_RESPONSE_CONTENT_TYPE,
SCOPE_RESPONSE_COOKIES,
SCOPE_RESPONSE_ENCODING,
SCOPE_RESPONSE_HEADERS,
SCOPE_RESPONSE_IS_FINISHED,
SCOPE_RESPONSE_IS_STARTED,
SCOPE_RESPONSE_STATUS,
)
from asgikit.errors.http import ClientDisconnectError
from asgikit.headers import MutableHeaders

Expand All @@ -41,31 +53,93 @@ class SameSitePolicy(StrEnum):
class Response:
ENCODING = "utf-8"

__slots__ = (
"_asgi",
"headers",
"cookies",
"content_type",
"content_length",
"encoding",
"is_started",
"is_finished",
"status",
)
__slots__ = ("asgi",)

def __init__(self, scope: AsgiScope, receive: AsgiReceive, send: AsgiSend):
self._asgi = AsgiProtocol(scope, receive, send)
scope.setdefault(SCOPE_ASGIKIT, {})
scope[SCOPE_ASGIKIT].setdefault(SCOPE_RESPONSE, {})
scope[SCOPE_ASGIKIT][SCOPE_RESPONSE].setdefault(
SCOPE_RESPONSE_HEADERS, MutableHeaders()
)
scope[SCOPE_ASGIKIT][SCOPE_RESPONSE].setdefault(
SCOPE_RESPONSE_COOKIES, SimpleCookie()
)
scope[SCOPE_ASGIKIT][SCOPE_RESPONSE].setdefault(
SCOPE_RESPONSE_ENCODING, self.ENCODING
)
scope[SCOPE_ASGIKIT][SCOPE_RESPONSE].setdefault(
SCOPE_RESPONSE_IS_STARTED, False
)
scope[SCOPE_ASGIKIT][SCOPE_RESPONSE].setdefault(
SCOPE_RESPONSE_IS_FINISHED, False
)

self.asgi = AsgiProtocol(scope, receive, send)

@property
def headers(self) -> MutableHeaders:
return self.asgi.scope[SCOPE_ASGIKIT][SCOPE_RESPONSE][SCOPE_RESPONSE_HEADERS]

@property
def cookies(self) -> SimpleCookie:
return self.asgi.scope[SCOPE_ASGIKIT][SCOPE_RESPONSE][SCOPE_RESPONSE_COOKIES]

@property
def content_type(self) -> str | None:
return self.asgi.scope[SCOPE_ASGIKIT][SCOPE_RESPONSE].get(
SCOPE_RESPONSE_CONTENT_TYPE
)

@content_type.setter
def content_type(self, value: str):
self.asgi.scope[SCOPE_ASGIKIT][SCOPE_RESPONSE][
SCOPE_RESPONSE_CONTENT_TYPE
] = value

@property
def content_length(self) -> int | None:
return self.asgi.scope[SCOPE_ASGIKIT][SCOPE_RESPONSE].get(
SCOPE_RESPONSE_CONTENT_LENGTH
)

@content_length.setter
def content_length(self, value: str):
self.asgi.scope[SCOPE_ASGIKIT][SCOPE_RESPONSE][
SCOPE_RESPONSE_CONTENT_LENGTH
] = value

@property
def encoding(self) -> str:
return self.asgi.scope[SCOPE_ASGIKIT][SCOPE_RESPONSE][SCOPE_RESPONSE_ENCODING]

@encoding.setter
def encoding(self, value: str):
self.asgi.scope[SCOPE_ASGIKIT][SCOPE_RESPONSE][SCOPE_RESPONSE_ENCODING] = value

@property
def is_started(self) -> bool:
return self.asgi.scope[SCOPE_ASGIKIT][SCOPE_RESPONSE][SCOPE_RESPONSE_IS_STARTED]

def __set_started(self):
self.asgi.scope[SCOPE_ASGIKIT][SCOPE_RESPONSE][SCOPE_RESPONSE_IS_STARTED] = True

@property
def is_finished(self) -> bool:
return self.asgi.scope[SCOPE_ASGIKIT][SCOPE_RESPONSE][
SCOPE_RESPONSE_IS_FINISHED
]

self.headers = MutableHeaders()
self.cookies = SimpleCookie()
def __set_finished(self):
self.asgi.scope[SCOPE_ASGIKIT][SCOPE_RESPONSE][
SCOPE_RESPONSE_IS_FINISHED
] = True

self.content_type: str | None = None
self.content_length: int | None = None
self.encoding = self.ENCODING
@property
def status(self) -> HTTPStatus | None:
return self.asgi.scope[SCOPE_ASGIKIT][SCOPE_RESPONSE].get(SCOPE_RESPONSE_STATUS)

self.is_started = False
self.is_finished = False
self.status: HTTPStatus | None = None
def __set_status(self, status: HTTPStatus):
self.asgi.scope[SCOPE_ASGIKIT][SCOPE_RESPONSE][SCOPE_RESPONSE_STATUS] = status

def header(self, name: str, value: str):
self.headers.set(name, value)
Expand Down Expand Up @@ -117,11 +191,11 @@ async def start(self, status=HTTPStatus.OK):
if self.is_finished:
raise RuntimeError("response has already ended")

self.is_started = True
self.status = status
self.__set_started()
self.__set_status(status)

headers = self._build_headers()
await self._asgi.send(
await self.asgi.send(
{
"type": "http.response.start",
"status": status,
Expand All @@ -135,7 +209,7 @@ async def write(self, data: bytes | str, *, end_response=False):
if not self.is_started:
raise RuntimeError("response was not started")

await self._asgi.send(
await self.asgi.send(
{
"type": "http.response.body",
"body": encoded_data,
Expand All @@ -144,7 +218,7 @@ async def write(self, data: bytes | str, *, end_response=False):
)

if end_response:
self.is_finished = True
self.__set_finished()

async def end(self):
if not self.is_started:
Expand Down Expand Up @@ -210,7 +284,7 @@ async def _listen_for_disconnect(receive):
@asynccontextmanager
async def stream_writer(response):
client_disconect = asyncio.create_task(
_listen_for_disconnect(response._asgi.receive)
_listen_for_disconnect(response.asgi.receive)
)

async def write(data: bytes | str):
Expand Down Expand Up @@ -265,18 +339,18 @@ async def respond_file(
response.content_length = content_length
response.headers.set("last-modified", last_modified)

if _supports_pathsend(response._asgi.scope):
await response._asgi.send(
if _supports_pathsend(response.asgi.scope):
await response.asgi.send(
{
"type": "http.response.pathsend",
"path": path,
}
)
return

if _supports_zerocopysend(response._asgi.scope):
if _supports_zerocopysend(response.asgi.scope):
file = await asyncio.to_thread(open, path, "rb")
await response._asgi.send(
await response.asgi.send(
{
"type": "http.response.zerocopysend",
"file": file.fileno(),
Expand Down
Loading

0 comments on commit 3d41654

Please sign in to comment.