Skip to content

Commit

Permalink
Add request body class (#4)
Browse files Browse the repository at this point in the history
* change build to pdm

* create Body class to read request body

* improve pydoc
  • Loading branch information
livioribeiro authored Dec 31, 2024
1 parent 1d6e748 commit 64e8248
Show file tree
Hide file tree
Showing 25 changed files with 393 additions and 235 deletions.
12 changes: 4 additions & 8 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,8 @@ jobs:
id-token: write
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v4
with:
python-version: "3.x"
- run: pip install pipx
- run: pipx install poetry
- run: poetry version ${VERSION#v}
- run: poetry build
- uses: pdm-project/setup-pdm@v4
- name: Define version number
run: echo "__version__ = \"${VERSION#v}\"" | tee src/asgikit/__version__.py
- name: Publish package distributions to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
run: pdm publish
12 changes: 5 additions & 7 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,11 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
version: [ "3.11", "3.12" ]
version: ["3.11", "3.12", "3.13"]
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
- uses: actions/checkout@v4
- uses: pdm-project/setup-pdm@v4
with:
python-version: ${{ matrix.version }}
- run: pip install pipx
- run: pipx install poetry
- run: poetry install --no-interaction --with test
- run: poetry run pytest
- run: pdm install --with test
- run: pdm run pytest
5 changes: 3 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,9 @@ venv.bak/
.dmypy.json
dmypy.json

# poetry
poetry.lock
# pdm
pdm.lock
.pdm-python

# IDEs
.idea/
Expand Down
53 changes: 27 additions & 26 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,32 +56,32 @@ from asgikit.responses import respond_json


async def main(scope, receive, send):
request = Request(scope, receive, send)

# request method
method = request.method

# request path
path = request.path

# request headers
headers = request.headers

# read body as json
body_json = await read_json(request)

data = {
"lang": "Python",
"async": True,
"platform": "asgi",
"method": method,
"path": path,
"headers": dict(headers.items()),
"body": body_json,
}

# send json response
await respond_json(request.response, data)
request = Request(scope, receive, send)
# request method
method = request.method
# request path
path = request.path
# request headers
headers = request.headers
# read body as json
body_json = await read_json(request)
data = {
"lang": "Python",
"async": True,
"platform": "asgi",
"method": method,
"path": path,
"headers": dict(headers.items()),
"body": body_json,
}
# send json response
await respond_json(request.response, data)
```

## Example websocket
Expand All @@ -90,6 +90,7 @@ async def main(scope, receive, send):
from asgikit.requests import Request
from asgikit.errors.websocket import WebSocketDisconnectError


async def app(scope, receive, send):
request = Request(scope, receive, send)
ws = request.websocket
Expand Down
4 changes: 2 additions & 2 deletions examples/asgi_callback_wrapper.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from asgikit.requests import Request, read_json
from asgikit.responses import respond_text
from asgikit.request import Request, read_json
from asgikit.response import respond_text


async def receive_wrapper(receive) -> dict:
Expand Down
4 changes: 2 additions & 2 deletions examples/echo.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from asgikit.requests import Request, read_json
from asgikit.responses import respond_json
from asgikit.request import Request, read_json
from asgikit.response import respond_json


async def app(scope, receive, send):
Expand Down
4 changes: 2 additions & 2 deletions examples/hello_world.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from asgikit.requests import Request
from asgikit.responses import respond_text
from asgikit.request import Request
from asgikit.response import respond_text


async def app(scope, receive, send):
Expand Down
4 changes: 2 additions & 2 deletions examples/hello_world_json.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from asgikit.requests import Request
from asgikit.responses import respond_json
from asgikit.request import Request
from asgikit.response import respond_json


async def app(scope, receive, send):
Expand Down
4 changes: 2 additions & 2 deletions examples/responses/response_file.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from pathlib import Path

from asgikit.requests import Request
from asgikit.responses import respond_file
from asgikit.request import Request
from asgikit.response import respond_file


async def app(scope, receive, send):
Expand Down
4 changes: 2 additions & 2 deletions examples/responses/response_streaming.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from asgikit.requests import Request
from asgikit.responses import respond_stream, stream_writer
from asgikit.request import Request
from asgikit.response import respond_stream, stream_writer

from . import fibonacci

Expand Down
4 changes: 2 additions & 2 deletions examples/responses/response_streaming_json.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from collections.abc import AsyncIterable

from asgikit.requests import Request
from asgikit.responses import respond_stream
from asgikit.request import Request
from asgikit.response import respond_stream

from . import fibonacci

Expand Down
6 changes: 3 additions & 3 deletions examples/websockets/echo_chat.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
from pathlib import Path

from asgikit.errors.websocket import WebSocketDisconnectError
from asgikit.requests import Request
from asgikit.responses import HTTPStatus, respond_file, respond_status
from asgikit.websockets import WebSocket
from asgikit.request import Request
from asgikit.response import HTTPStatus, respond_file, respond_status
from asgikit.websocket import WebSocket

clients: set[WebSocket] = set()

Expand Down
79 changes: 41 additions & 38 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"

[tool.poetry]
[project]
name = "asgikit"
version = "0.8.0"
description = "Toolkit for building ASGI applications and libraries"
authors = ["Livio Ribeiro <[email protected]>"]
license = "MIT"
authors = [
{name = "Livio Ribeiro", email = "[email protected]"},
]
license = {text = "MIT"}
readme = "README.md"
repository = "https://github.com/livioribeiro/asgikit"
keywords = ["asgi", "toolkit", "asyncio", "web"]
Expand All @@ -25,44 +22,46 @@ classifiers = [
"Topic :: Internet :: WWW/HTTP :: Dynamic Content",
"Topic :: Software Development :: Libraries :: Application Frameworks",
]
packages = [
{ include = "asgikit", from = "src" },
{ include = "asgikit/py.typed", from = "src" },
]

[tool.poetry.dependencies]
python = "^3.11"
python-multipart = "*"
aiofiles = "^23.2"
dynamic = ["version"]

[tool.poetry.group.dev]
optional = true
requires-python = ">=3.11"

[tool.poetry.group.dev.dependencies]
uvicorn = { version = "^0.29", extras = ["standard"] }
granian = "^1.3"
pylint = "^3.1"
flake8 = "^7.0"
mypy = "^1.10"
isort = "^5.13"
black = "^24.4"
ruff = "^0.4"
dependencies = [
"python-multipart~=0.0.20",
"aiofiles~=24.1.0",
]

[tool.poetry.group.test]
optional = true
[dependency-groups]
dev = [
"uvicorn[standard]~=0.34.0",
"granian~=1.7.1",
"mypy~=1.14.0",
"isort~=5.13.2",
"black~=24.10.0",
"ruff~=0.8.3",
]

[tool.poetry.group.test.dependencies]
pytest = "^8.2"
pytest-asyncio = "^0.23"
pytest-cov = "^5.0"
coverage = { version = "^7.5", extras = ["toml"] }
httpx = "^0.27"
asgiref = "^3.8"
orjson = "^3.10"
msgspec = "^0.18"
test = [
"pytest~=8.3.4",
"pytest-asyncio~=0.25.0",
"pytest-cov~=6.0.0",
"coverage[toml]~=7.6.9",
"httpx~=0.28.1",
"asgiref~=3.8.1",
"orjson~=3.10.12",
]

[tool.pdm]
distribution = true

[tool.pdm.version]
source = "file"
path = "src/asgikit/__version__.py"

[tool.pytest.ini_options]
asyncio_mode = "auto"
asyncio_default_fixture_loop_scope = "function"
testpaths = [
"tests",
]
Expand All @@ -75,3 +74,7 @@ disable = ["C0114", "C0115", "C0116", "R0902", "R0913"]

[tool.mypy]
ignore_missing_imports = true

[build-system]
requires = ["pdm-backend"]
build-backend = "pdm.backend"
1 change: 1 addition & 0 deletions src/asgikit/__version__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
__version__ = "0.0.0"
9 changes: 1 addition & 8 deletions src/asgikit/asgi.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,13 @@
from collections.abc import Awaitable, Callable
from typing import Any, NamedTuple, TypeAlias
from typing import Any, TypeAlias

__all__ = (
"AsgiScope",
"AsgiReceive",
"AsgiSend",
"AsgiProtocol",
)


AsgiScope: TypeAlias = dict[str, Any]
AsgiReceive: TypeAlias = Callable[..., Awaitable[dict]]
AsgiSend: TypeAlias = Callable[[dict], Awaitable]


class AsgiProtocol(NamedTuple):
scope: AsgiScope
receive: AsgiReceive
send: AsgiSend
3 changes: 3 additions & 0 deletions src/asgikit/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,6 @@
SCOPE_RESPONSE_ENCODING = "encoding"
SCOPE_RESPONSE_IS_STARTED = "is_started"
SCOPE_RESPONSE_IS_FINISHED = "is_finished"

DEFAULT_ENCODING = "utf-8"
HEADER_ENCODING = "latin-1"
16 changes: 16 additions & 0 deletions src/asgikit/errors/http.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,19 @@ class HttpError(AsgiError):

class ClientDisconnectError(HttpError):
pass


class RequestBodyAlreadyConsumedError(HttpError):
pass


class ResponseAlreadyStartedError(HttpError):
pass


class ResponseNotStartedError(HttpError):
pass


class ResponseAlreadyEndedError(HttpError):
pass
8 changes: 4 additions & 4 deletions src/asgikit/headers.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Iterable, Optional
from typing import Iterable

from asgikit.util.multi_value_dict import MultiStrValueDict

Expand All @@ -24,15 +24,15 @@ def __init__(
key, value = key_raw.decode(encoding).lower(), value_raw.decode(encoding)
self._parsed[key] = [i.strip() for i in value.split(",")]

def get(self, key: str, default: str = None) -> Optional[str]:
def get(self, key: str, default: str = None) -> str | None:
key = key.lower()
return value[0] if (value := self._parsed.get(key)) else default

def get_all(self, key: str, default: list[str] = None) -> Optional[list[str]]:
def get_all(self, key: str, default: list[str] = None) -> list[str] | None:
key = key.lower()
return self._parsed.get(key, default)

def get_raw(self, key: bytes, default: bytes = None) -> Optional[bytes]:
def get_raw(self, key: bytes, default: bytes = None) -> bytes | None:
return self._raw.get(key, default)

def items(self) -> Iterable[tuple[str, list[str]]]:
Expand Down
Loading

0 comments on commit 64e8248

Please sign in to comment.