Implemented modular code structure
Refactored repository into kaya-core and kaya-rsgi packages
This commit is contained in:
@@ -26,14 +26,17 @@ jobs:
|
|||||||
.venv/bin/pip install -r requirements-dev.txt
|
.venv/bin/pip install -r requirements-dev.txt
|
||||||
- name: Unit tests
|
- name: Unit tests
|
||||||
run: |
|
run: |
|
||||||
.venv/bin/pip install .
|
.venv/bin/pip install packages/kaya-core packages/kaya-rsgi
|
||||||
.venv/bin/python -m mypy -p src
|
.venv/bin/python -m mypy -p kaya.core
|
||||||
.venv/bin/python -m unittest discover -s tests
|
.venv/bin/python -m mypy -p kaya.rsgi
|
||||||
|
.venv/bin/python -m unittest discover -s packages/kaya-core/tests
|
||||||
|
.venv/bin/python -m unittest discover -s packages/kaya-rsgi/tests
|
||||||
- name: Publish artifacts
|
- name: Publish artifacts
|
||||||
env:
|
env:
|
||||||
TWINE_REPOSITORY_URL: ${{ vars.PYPI_REGISTRY_URL }}
|
TWINE_REPOSITORY_URL: ${{ vars.PYPI_REGISTRY_URL }}
|
||||||
TWINE_USERNAME: ${{ vars.PUBLISHER_USERNAME }}
|
TWINE_USERNAME: ${{ vars.PUBLISHER_USERNAME }}
|
||||||
TWINE_PASSWORD: ${{ secrets.PUBLISHER_TOKEN }}
|
TWINE_PASSWORD: ${{ secrets.PUBLISHER_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
.venv/bin/pyproject-build
|
.venv/bin/pyproject-build packages/kaya-core
|
||||||
.venv/bin/twine upload --repository gitea dist/*.whl dist/*.tar.gz
|
.venv/bin/pyproject-build packages/kaya-rsgi
|
||||||
|
.venv/bin/twine upload --repository gitea packages/kaya-core/dist/*.whl packages/kaya-core/dist/*.tar.gz packages/kaya-rsgi/dist/*.whl packages/kaya-rsgi/dist/*.tar.gz
|
||||||
|
|||||||
+2
-2
@@ -4,5 +4,5 @@ __pycache__
|
|||||||
.mypy_cache
|
.mypy_cache
|
||||||
_version.py
|
_version.py
|
||||||
*.egg-info
|
*.egg-info
|
||||||
/build
|
build/
|
||||||
/dist
|
dist/
|
||||||
|
|||||||
@@ -1,6 +1,15 @@
|
|||||||
# kaya
|
# kaya
|
||||||
|
|
||||||
A lightweight ASGI/RSGI web framework with method-aware routing, path matching, and recursive wildcard support.
|
A lightweight ASGI web framework with method-aware routing, path matching, and recursive wildcard support.
|
||||||
|
|
||||||
|
## Packages
|
||||||
|
|
||||||
|
This repository is a monorepo for the Kaya framework. The code is split into independent packages under `packages/`:
|
||||||
|
|
||||||
|
- **kaya-core** — core routing, HTTP/WS abstractions, and ASGI adapter (`packages/kaya-core/`)
|
||||||
|
- **kaya-rsgi** — RSGI/Granian integration (`packages/kaya-rsgi/`)
|
||||||
|
|
||||||
|
Additional `kaya-*` packages can be added as new directories under `packages/`.
|
||||||
|
|
||||||
## Build & run locally
|
## Build & run locally
|
||||||
|
|
||||||
@@ -10,10 +19,10 @@ Install dev dependencies:
|
|||||||
pip install --index-url https://gitea.woggioni.net/api/packages/woggioni/pypi/simple --extra-index-url https://pypi.org/simple -r requirements-dev.txt
|
pip install --index-url https://gitea.woggioni.net/api/packages/woggioni/pypi/simple --extra-index-url https://pypi.org/simple -r requirements-dev.txt
|
||||||
```
|
```
|
||||||
|
|
||||||
Build the package:
|
Install the packages in development mode:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python -m build
|
pip install -e packages/kaya-core -e packages/kaya-rsgi
|
||||||
```
|
```
|
||||||
|
|
||||||
Run the example:
|
Run the example:
|
||||||
@@ -25,11 +34,20 @@ python example/hello.py
|
|||||||
## Tests
|
## Tests
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
PYTHONPATH=src python -m unittest discover tests
|
python -m unittest discover -s packages/kaya-core/tests
|
||||||
|
python -m unittest discover -s packages/kaya-rsgi/tests
|
||||||
```
|
```
|
||||||
|
|
||||||
## Static analysis
|
## Static analysis
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
mypy src/kaya
|
mypy -p kaya.core
|
||||||
|
mypy -p kaya.rsgi
|
||||||
|
```
|
||||||
|
|
||||||
|
## Building packages
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python -m build packages/kaya-core
|
||||||
|
python -m build packages/kaya-rsgi
|
||||||
```
|
```
|
||||||
|
|||||||
+2
-11
@@ -1,15 +1,6 @@
|
|||||||
from kaya import KayaApp, HttpContext, HttpMethod, WebSocket
|
from kaya.core import KayaApp, HttpContext, HttpMethod, WebSocket
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
class Hello(KayaApp):
|
|
||||||
pass
|
|
||||||
|
|
||||||
# async def handle_request(self, ctx: HttpContext) -> None:
|
|
||||||
# async for chunk in ctx.request_body:
|
|
||||||
# print(chunk.decode())
|
|
||||||
# await ctx.send_str(200, 'Hello World')
|
|
||||||
|
|
||||||
|
|
||||||
app = KayaApp()
|
app = KayaApp()
|
||||||
|
|
||||||
|
|
||||||
@@ -53,4 +44,4 @@ async def echo(ws: WebSocket) -> None:
|
|||||||
elif msg.kind == 'binary':
|
elif msg.kind == 'binary':
|
||||||
data = msg.data
|
data = msg.data
|
||||||
assert isinstance(data, bytes)
|
assert isinstance(data, bytes)
|
||||||
await ws.send_bytes(data)
|
await ws.send_bytes(data)
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
# kaya-core
|
||||||
|
|
||||||
|
Core package of the Kaya web framework.
|
||||||
|
|
||||||
|
Provides method-aware routing, path matching, HTTP/WebSocket abstractions, and an ASGI adapter.
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
[build-system]
|
||||||
|
requires = ["setuptools>=61.0", "setuptools-scm>=8"]
|
||||||
|
build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
|
[project]
|
||||||
|
name = "kaya-core"
|
||||||
|
dynamic = ["version"]
|
||||||
|
authors = [
|
||||||
|
{ name="Walter Oggioni", email="oggioni.walter@gmail.com" },
|
||||||
|
]
|
||||||
|
description = "Core package of the Kaya lightweight ASGI web framework"
|
||||||
|
readme = "README.md"
|
||||||
|
requires-python = ">=3.10"
|
||||||
|
license = "MIT"
|
||||||
|
classifiers = [
|
||||||
|
'Development Status :: 3 - Alpha',
|
||||||
|
'Topic :: Utilities',
|
||||||
|
'Intended Audience :: System Administrators',
|
||||||
|
'Intended Audience :: Developers',
|
||||||
|
'Environment :: Console',
|
||||||
|
'Programming Language :: Python :: 3',
|
||||||
|
]
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
"pwo",
|
||||||
|
"typing-extensions",
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.optional-dependencies]
|
||||||
|
dev = [
|
||||||
|
"build", "mypy", "ipdb", "twine", "httpx"
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.urls]
|
||||||
|
"Homepage" = "https://github.com/woggioni/kaya"
|
||||||
|
"Bug Tracker" = "https://github.com/woggioni/kaya/issues"
|
||||||
|
|
||||||
|
[tool.setuptools.packages.find]
|
||||||
|
where = ["src"]
|
||||||
|
namespaces = true
|
||||||
|
|
||||||
|
[tool.mypy]
|
||||||
|
python_version = "3.12"
|
||||||
|
disallow_untyped_defs = true
|
||||||
|
show_error_codes = true
|
||||||
|
no_implicit_optional = true
|
||||||
|
warn_return_any = true
|
||||||
|
warn_unused_ignores = true
|
||||||
|
exclude = ["scripts", "docs", "test"]
|
||||||
|
strict = true
|
||||||
|
|
||||||
|
[tool.setuptools_scm]
|
||||||
|
root = "../.."
|
||||||
|
version_file = "src/kaya/core/_version.py"
|
||||||
|
|
||||||
|
[tool.setuptools_scm.tag]
|
||||||
|
prefix = "release/"
|
||||||
@@ -1,17 +1,19 @@
|
|||||||
from ._app import KayaApp
|
from ._app import AbstractKayaApp, KayaApp
|
||||||
from ._http_method import HttpMethod
|
from ._http_method import HttpMethod
|
||||||
from ._http_context import HttpContext
|
from ._http_context import HttpContext
|
||||||
from ._tree import Tree, PathIterator
|
from ._tree import Tree, PathIterator
|
||||||
from ._path_handler import PathHandler
|
from ._path_handler import PathHandler, Matches
|
||||||
from ._websocket import WebSocket, WebSocketMessage
|
from ._websocket import WebSocket, WebSocketMessage
|
||||||
|
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
|
'AbstractKayaApp',
|
||||||
'HttpMethod',
|
'HttpMethod',
|
||||||
'KayaApp',
|
'KayaApp',
|
||||||
'HttpContext',
|
'HttpContext',
|
||||||
'Tree',
|
'Tree',
|
||||||
'PathHandler',
|
'PathHandler',
|
||||||
|
'Matches',
|
||||||
'PathIterator',
|
'PathIterator',
|
||||||
'WebSocket',
|
'WebSocket',
|
||||||
'WebSocketMessage'
|
'WebSocketMessage'
|
||||||
@@ -2,31 +2,13 @@ from abc import ABC, abstractmethod
|
|||||||
from asyncio import Queue, AbstractEventLoop
|
from asyncio import Queue, AbstractEventLoop
|
||||||
from asyncio import get_running_loop
|
from asyncio import get_running_loop
|
||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
from typing import Callable, Awaitable, Any, Mapping, Sequence, Optional, Unpack, Tuple, TYPE_CHECKING, cast
|
from typing import Callable, Awaitable, Any, Mapping, Sequence, Optional, Unpack, Tuple, cast
|
||||||
from pathlib import Path, PurePath
|
|
||||||
from pwo import Maybe, AsyncQueueIterator
|
from pwo import Maybe, AsyncQueueIterator
|
||||||
from hashlib import md5
|
|
||||||
from ._http_context import HttpContext
|
from ._http_context import HttpContext
|
||||||
from ._http_method import HttpMethod
|
from ._http_method import HttpMethod
|
||||||
from ._path_handler import Context
|
from ._path_handler import Context
|
||||||
from ._types import StrOrStrings
|
from ._types import StrOrStrings
|
||||||
from ._websocket import WebSocket
|
from ._websocket import WebSocket
|
||||||
from base64 import b64encode, b64decode
|
|
||||||
from mimetypes import guess_type
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from _typeshed import StrOrBytesPath
|
|
||||||
try:
|
|
||||||
from ._rsgi import RsgiContext, RsgiWebSocket
|
|
||||||
from granian._granian import ( # type: ignore
|
|
||||||
RSGIHTTPProtocol,
|
|
||||||
RSGIHTTPScope,
|
|
||||||
RSGIWebsocketProtocol,
|
|
||||||
RSGIWebsocketScope as RSGIWebSocketScope,
|
|
||||||
)
|
|
||||||
except ImportError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
from ._asgi import AsgiContext, AsgiWebSocket
|
from ._asgi import AsgiContext, AsgiWebSocket
|
||||||
from ._tree import Tree
|
from ._tree import Tree
|
||||||
from ._types.asgi import LifespanScope, HTTPScope as ASGIHTTPScope, WebSocketScope as ASGIWebSocketScope
|
from ._types.asgi import LifespanScope, HTTPScope as ASGIHTTPScope, WebSocketScope as ASGIWebSocketScope
|
||||||
@@ -87,20 +69,6 @@ class AbstractKayaApp(ABC):
|
|||||||
async def handle_websocket(self, ws: WebSocket) -> None:
|
async def handle_websocket(self, ws: WebSocket) -> None:
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
def __rsgi_init__(self, loop: AbstractEventLoop) -> None:
|
|
||||||
self.setup(loop)
|
|
||||||
|
|
||||||
def __rsgi_del__(self, loop: AbstractEventLoop) -> None:
|
|
||||||
self.shutdown(loop)
|
|
||||||
|
|
||||||
async def __rsgi__(self, scope: RSGIHTTPScope | RSGIWebSocketScope, protocol: RSGIHTTPProtocol | RSGIWebsocketProtocol) -> None:
|
|
||||||
if scope.proto == 'ws':
|
|
||||||
ws = RsgiWebSocket(scope, protocol) # type: ignore[arg-type]
|
|
||||||
await self.handle_websocket(ws)
|
|
||||||
else:
|
|
||||||
ctx = RsgiContext(scope, protocol) # type: ignore[arg-type]
|
|
||||||
await self.handle_request(ctx)
|
|
||||||
|
|
||||||
|
|
||||||
class KayaApp(AbstractKayaApp):
|
class KayaApp(AbstractKayaApp):
|
||||||
_tree: Tree
|
_tree: Tree
|
||||||
@@ -115,7 +83,6 @@ class KayaApp(AbstractKayaApp):
|
|||||||
await handler.handle_request(ctx, captured)
|
await handler.handle_request(ctx, captured)
|
||||||
else:
|
else:
|
||||||
await ctx.send_empty(404)
|
await ctx.send_empty(404)
|
||||||
pass
|
|
||||||
|
|
||||||
async def handle_websocket(self, ws: WebSocket) -> None:
|
async def handle_websocket(self, ws: WebSocket) -> None:
|
||||||
result = self._tree.get_handler(ws.path, HttpMethod.WS)
|
result = self._tree.get_handler(ws.path, HttpMethod.WS)
|
||||||
@@ -124,7 +91,6 @@ class KayaApp(AbstractKayaApp):
|
|||||||
await handler.handle_request(ws, captured)
|
await handler.handle_request(ws, captured)
|
||||||
else:
|
else:
|
||||||
await ws.close(1000)
|
await ws.close(1000)
|
||||||
pass
|
|
||||||
|
|
||||||
def route(self,
|
def route(self,
|
||||||
paths: StrOrStrings,
|
paths: StrOrStrings,
|
||||||
@@ -179,4 +145,3 @@ class KayaApp(AbstractKayaApp):
|
|||||||
|
|
||||||
def PATCH(self, path: str, recursive: bool = False) -> Callable[[HttpHandler], HttpHandler]:
|
def PATCH(self, path: str, recursive: bool = False) -> Callable[[HttpHandler], HttpHandler]:
|
||||||
return self.route(path, (HttpMethod.PATCH,), recursive)
|
return self.route(path, (HttpMethod.PATCH,), recursive)
|
||||||
|
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
from typing import (
|
||||||
|
TypedDict,
|
||||||
|
Literal,
|
||||||
|
Iterable,
|
||||||
|
Tuple,
|
||||||
|
Optional,
|
||||||
|
NotRequired,
|
||||||
|
Dict,
|
||||||
|
Any,
|
||||||
|
Union,
|
||||||
|
Mapping,
|
||||||
|
Sequence
|
||||||
|
)
|
||||||
|
|
||||||
|
from .base import StrOrStrings, PathMatcherResult
|
||||||
|
from .asgi import ASGIVersions, HTTPScope, WebSocketScope, LifespanScope
|
||||||
|
from .._http_method import HttpMethod
|
||||||
|
|
||||||
|
type NodeType = (str | HttpMethod)
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
'HttpMethod',
|
||||||
|
'HTTPScope',
|
||||||
|
'LifespanScope',
|
||||||
|
'ASGIVersions',
|
||||||
|
'WebSocketScope',
|
||||||
|
'NodeType',
|
||||||
|
'StrOrStrings',
|
||||||
|
'PathMatcherResult'
|
||||||
|
]
|
||||||
@@ -2,7 +2,7 @@ import unittest
|
|||||||
import json
|
import json
|
||||||
import httpx
|
import httpx
|
||||||
from pwo import async_test
|
from pwo import async_test
|
||||||
from kaya import KayaApp, HttpContext, HttpMethod
|
from kaya.core import KayaApp, HttpContext, HttpMethod
|
||||||
from typing import Sequence, List
|
from typing import Sequence, List
|
||||||
|
|
||||||
|
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
from typing import Sequence, Tuple, Optional, List
|
from typing import Sequence, Tuple, Optional, List
|
||||||
|
|
||||||
from kaya import Tree, PathHandler, HttpContext, HttpMethod, PathIterator
|
from kaya.core import Tree, PathHandler, HttpContext, HttpMethod, PathIterator
|
||||||
from kaya import HttpMethod
|
|
||||||
from pwo import Maybe
|
from pwo import Maybe
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import unittest
|
import unittest
|
||||||
from typing import Any, Callable, Awaitable, List, Mapping, Optional
|
from typing import Any, Callable, Awaitable, List, Mapping, Optional
|
||||||
from pwo import async_test
|
from pwo import async_test
|
||||||
from kaya import KayaApp, WebSocket, WebSocketMessage
|
from kaya.core import KayaApp, WebSocket, WebSocketMessage
|
||||||
|
|
||||||
|
|
||||||
def websocket_scope(path: str = '/ws') -> Mapping[str, Any]:
|
def websocket_scope(path: str = '/ws') -> Mapping[str, Any]:
|
||||||
@@ -151,25 +151,3 @@ class WebSocketTest(unittest.TestCase):
|
|||||||
|
|
||||||
self.assertEqual(sent_messages[0]['type'], 'websocket.accept')
|
self.assertEqual(sent_messages[0]['type'], 'websocket.accept')
|
||||||
self.assertEqual(len(sent_messages), 1)
|
self.assertEqual(len(sent_messages), 1)
|
||||||
|
|
||||||
|
|
||||||
class RsgiWebSocketTest(unittest.TestCase):
|
|
||||||
|
|
||||||
def test_misconfigured_granian(self):
|
|
||||||
from kaya._rsgi import RsgiWebSocket
|
|
||||||
|
|
||||||
class FakeScope:
|
|
||||||
scheme = 'ws'
|
|
||||||
path = '/ws'
|
|
||||||
query_string = ''
|
|
||||||
headers = {}
|
|
||||||
client = '127.0.0.1:12345'
|
|
||||||
server = '127.0.0.1:80'
|
|
||||||
|
|
||||||
class FakeProtocol:
|
|
||||||
pass
|
|
||||||
|
|
||||||
with self.assertRaises(RuntimeError) as ctx:
|
|
||||||
RsgiWebSocket(FakeScope(), FakeProtocol()) # type: ignore[arg-type]
|
|
||||||
|
|
||||||
self.assertIn('Granian was not configured for websockets', str(ctx.exception))
|
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
# kaya-rsgi
|
||||||
|
|
||||||
|
RSGI/Granian integration for the Kaya web framework.
|
||||||
|
|
||||||
|
Provides `RsgiContext`, `RsgiWebSocket`, and `RsgiApplication` to run Kaya apps on Granian's RSGI protocol.
|
||||||
@@ -3,42 +3,43 @@ requires = ["setuptools>=61.0", "setuptools-scm>=8"]
|
|||||||
build-backend = "setuptools.build_meta"
|
build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "kaya"
|
name = "kaya-rsgi"
|
||||||
dynamic = ["version"]
|
dynamic = ["version"]
|
||||||
authors = [
|
authors = [
|
||||||
{ name="Walter Oggioni", email="oggioni.walter@gmail.com" },
|
{ name="Walter Oggioni", email="oggioni.walter@gmail.com" },
|
||||||
]
|
]
|
||||||
description = "A lightweight ASGI/RSGI web framework"
|
description = "RSGI/Granian integration for the Kaya web framework"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.10"
|
requires-python = ">=3.10"
|
||||||
|
license = "MIT"
|
||||||
classifiers = [
|
classifiers = [
|
||||||
'Development Status :: 3 - Alpha',
|
'Development Status :: 3 - Alpha',
|
||||||
'Topic :: Utilities',
|
'Topic :: Utilities',
|
||||||
'License :: OSI Approved :: MIT License',
|
|
||||||
'Intended Audience :: System Administrators',
|
'Intended Audience :: System Administrators',
|
||||||
'Intended Audience :: Developers',
|
'Intended Audience :: Developers',
|
||||||
'Environment :: Console',
|
'Environment :: Console',
|
||||||
'License :: OSI Approved :: MIT License',
|
|
||||||
'Programming Language :: Python :: 3',
|
'Programming Language :: Python :: 3',
|
||||||
]
|
]
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"kaya-core",
|
||||||
|
"granian>=2.0",
|
||||||
"pwo",
|
"pwo",
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.optional-dependencies]
|
[project.optional-dependencies]
|
||||||
dev = [
|
dev = [
|
||||||
"build", "mypy", "ipdb", "twine", "granian", "httpx"
|
"build", "mypy", "ipdb", "twine"
|
||||||
]
|
|
||||||
|
|
||||||
rsgi = [
|
|
||||||
"granian"
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.urls]
|
[project.urls]
|
||||||
"Homepage" = "https://github.com/woggioni/kaya"
|
"Homepage" = "https://github.com/woggioni/kaya"
|
||||||
"Bug Tracker" = "https://github.com/woggioni/kaya/issues"
|
"Bug Tracker" = "https://github.com/woggioni/kaya/issues"
|
||||||
|
|
||||||
|
[tool.setuptools.packages.find]
|
||||||
|
where = ["src"]
|
||||||
|
namespaces = true
|
||||||
|
|
||||||
[tool.mypy]
|
[tool.mypy]
|
||||||
python_version = "3.12"
|
python_version = "3.12"
|
||||||
disallow_untyped_defs = true
|
disallow_untyped_defs = true
|
||||||
@@ -50,7 +51,8 @@ exclude = ["scripts", "docs", "test"]
|
|||||||
strict = true
|
strict = true
|
||||||
|
|
||||||
[tool.setuptools_scm]
|
[tool.setuptools_scm]
|
||||||
version_file = "src/kaya/_version.py"
|
root = "../.."
|
||||||
|
version_file = "src/kaya/rsgi/_version.py"
|
||||||
|
|
||||||
[tool.setuptools_scm.tag]
|
[tool.setuptools_scm.tag]
|
||||||
prefix = "release/"
|
prefix = "release/"
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
from ._rsgi import RsgiApplication, RsgiContext, RsgiWebSocket
|
||||||
|
from ._types import HTTPScope, WebSocketScope
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
'RsgiApplication',
|
||||||
|
'RsgiContext',
|
||||||
|
'RsgiWebSocket',
|
||||||
|
'HTTPScope',
|
||||||
|
'WebSocketScope',
|
||||||
|
]
|
||||||
@@ -14,13 +14,17 @@ from typing import (
|
|||||||
cast
|
cast
|
||||||
)
|
)
|
||||||
|
|
||||||
from granian._granian import RSGIHTTPProtocol, RSGIHTTPScope, RSGIWebsocketProtocol, RSGIWebsocketScope, RSGIWebsocketTransport # type: ignore[attr-defined]
|
from granian._granian import ( # type: ignore[attr-defined]
|
||||||
|
RSGIHTTPProtocol,
|
||||||
|
RSGIHTTPScope,
|
||||||
|
RSGIWebsocketProtocol,
|
||||||
|
RSGIWebsocketScope,
|
||||||
|
RSGIWebsocketTransport,
|
||||||
|
)
|
||||||
from pwo import Maybe
|
from pwo import Maybe
|
||||||
|
|
||||||
from ._types import StrOrStrings
|
from kaya.core import AbstractKayaApp, HttpContext, HttpMethod, WebSocket, WebSocketMessage
|
||||||
from ._http_context import HttpContext
|
from kaya.core._types import StrOrStrings
|
||||||
from ._http_method import HttpMethod
|
|
||||||
from ._websocket import WebSocket, WebSocketMessage
|
|
||||||
|
|
||||||
|
|
||||||
class RsgiContext(HttpContext):
|
class RsgiContext(HttpContext):
|
||||||
@@ -56,12 +60,6 @@ class RsgiContext(HttpContext):
|
|||||||
self.request_body = cast(AsyncIterator[bytes], protocol)
|
self.request_body = cast(AsyncIterator[bytes], protocol)
|
||||||
self.protocol = protocol
|
self.protocol = protocol
|
||||||
|
|
||||||
# @staticmethod
|
|
||||||
# def _rearrange_headers(headers: Mapping[str, Sequence[str]]) -> List[Tuple[str, str]]:
|
|
||||||
# return list(
|
|
||||||
# ((key, value) for key, values in headers.items() for value in values)
|
|
||||||
# )
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _rearrange_headers(headers: Mapping[str, StrOrStrings]) -> List[Tuple[str, str]]:
|
def _rearrange_headers(headers: Mapping[str, StrOrStrings]) -> List[Tuple[str, str]]:
|
||||||
result = []
|
result = []
|
||||||
@@ -177,3 +175,26 @@ class RsgiWebSocket(WebSocket):
|
|||||||
if message.kind == 'close':
|
if message.kind == 'close':
|
||||||
raise StopAsyncIteration
|
raise StopAsyncIteration
|
||||||
return message
|
return message
|
||||||
|
|
||||||
|
|
||||||
|
class RsgiApplication:
|
||||||
|
_app: AbstractKayaApp
|
||||||
|
|
||||||
|
def __init__(self, app: AbstractKayaApp) -> None:
|
||||||
|
self._app = app
|
||||||
|
|
||||||
|
def __rsgi_init__(self, loop: Any) -> None:
|
||||||
|
self._app.setup(loop)
|
||||||
|
|
||||||
|
def __rsgi_del__(self, loop: Any) -> None:
|
||||||
|
self._app.shutdown(loop)
|
||||||
|
|
||||||
|
async def __rsgi__(self,
|
||||||
|
scope: RSGIHTTPScope | RSGIWebsocketScope,
|
||||||
|
protocol: RSGIHTTPProtocol | RSGIWebsocketProtocol) -> None:
|
||||||
|
if scope.proto == 'ws':
|
||||||
|
ws = RsgiWebSocket(scope, protocol) # type: ignore[arg-type]
|
||||||
|
await self._app.handle_websocket(ws)
|
||||||
|
else:
|
||||||
|
ctx = RsgiContext(scope, protocol) # type: ignore[arg-type]
|
||||||
|
await self._app.handle_request(ctx)
|
||||||
@@ -1,17 +1,11 @@
|
|||||||
from typing import (
|
from typing import (
|
||||||
Sequence,
|
|
||||||
TypedDict,
|
TypedDict,
|
||||||
Literal,
|
Literal,
|
||||||
Iterable,
|
|
||||||
Tuple,
|
|
||||||
Optional,
|
Optional,
|
||||||
NotRequired,
|
|
||||||
Dict,
|
|
||||||
Any,
|
|
||||||
Union,
|
|
||||||
Mapping,
|
Mapping,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class HTTPScope(TypedDict):
|
class HTTPScope(TypedDict):
|
||||||
proto: Literal['http']
|
proto: Literal['http']
|
||||||
rsgi_version: str
|
rsgi_version: str
|
||||||
@@ -37,4 +31,10 @@ class WebSocketScope(TypedDict):
|
|||||||
path: str
|
path: str
|
||||||
query_string: str
|
query_string: str
|
||||||
headers: Mapping[str, str]
|
headers: Mapping[str, str]
|
||||||
authority: Optional[str]
|
authority: Optional[str]
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
'HTTPScope',
|
||||||
|
'WebSocketScope',
|
||||||
|
]
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import unittest
|
||||||
|
from kaya.rsgi import RsgiWebSocket
|
||||||
|
|
||||||
|
|
||||||
|
class RsgiWebSocketTest(unittest.TestCase):
|
||||||
|
|
||||||
|
def test_misconfigured_granian(self):
|
||||||
|
class FakeScope:
|
||||||
|
scheme = 'ws'
|
||||||
|
path = '/ws'
|
||||||
|
query_string = ''
|
||||||
|
headers = {}
|
||||||
|
client = '127.0.0.1:12345'
|
||||||
|
server = '127.0.0.1:80'
|
||||||
|
|
||||||
|
class FakeProtocol:
|
||||||
|
pass
|
||||||
|
|
||||||
|
with self.assertRaises(RuntimeError) as ctx:
|
||||||
|
RsgiWebSocket(FakeScope(), FakeProtocol()) # type: ignore[arg-type]
|
||||||
|
|
||||||
|
self.assertIn('Granian was not configured for websockets', str(ctx.exception))
|
||||||
+11
-149
@@ -1,154 +1,16 @@
|
|||||||
#
|
#
|
||||||
# This file is autogenerated by pip-compile with Python 3.14
|
# Development dependencies for the Kaya monorepo.
|
||||||
# by the following command:
|
# Install with:
|
||||||
#
|
# pip install -r requirements-dev.txt
|
||||||
# pip-compile --allow-unsafe --extra=dev --extra=rsgi --output-file=requirements-dev.txt pyproject.toml
|
|
||||||
#
|
#
|
||||||
--index-url https://gitea.woggioni.net/api/packages/woggioni/pypi/simple
|
--index-url https://gitea.woggioni.net/api/packages/woggioni/pypi/simple
|
||||||
--extra-index-url https://pypi.org/simple
|
--extra-index-url https://pypi.org/simple
|
||||||
|
|
||||||
anyio==4.14.1
|
build
|
||||||
# via httpx
|
mypy
|
||||||
ast-serialize==0.5.0
|
ipdb
|
||||||
# via mypy
|
twine
|
||||||
asttokens==3.0.1
|
httpx
|
||||||
# via stack-data
|
granian
|
||||||
build==1.5.0
|
pwo
|
||||||
# via kaya (pyproject.toml)
|
typing-extensions
|
||||||
certifi==2026.6.17
|
|
||||||
# via
|
|
||||||
# httpcore
|
|
||||||
# httpx
|
|
||||||
# requests
|
|
||||||
cffi==2.0.0
|
|
||||||
# via cryptography
|
|
||||||
charset-normalizer==3.4.7
|
|
||||||
# via requests
|
|
||||||
click==8.4.2
|
|
||||||
# via granian
|
|
||||||
cryptography==49.0.0
|
|
||||||
# via secretstorage
|
|
||||||
decorator==5.3.1
|
|
||||||
# via
|
|
||||||
# ipdb
|
|
||||||
# ipython
|
|
||||||
docutils==0.23
|
|
||||||
# via readme-renderer
|
|
||||||
executing==2.2.1
|
|
||||||
# via stack-data
|
|
||||||
granian==2.7.7
|
|
||||||
# via kaya (pyproject.toml)
|
|
||||||
h11==0.16.0
|
|
||||||
# via httpcore
|
|
||||||
httpcore==1.0.9
|
|
||||||
# via httpx
|
|
||||||
httpx==0.28.1
|
|
||||||
# via kaya (pyproject.toml)
|
|
||||||
id==1.6.1
|
|
||||||
# via twine
|
|
||||||
idna==3.18
|
|
||||||
# via
|
|
||||||
# anyio
|
|
||||||
# httpx
|
|
||||||
# requests
|
|
||||||
ipdb==0.13.13
|
|
||||||
# via kaya (pyproject.toml)
|
|
||||||
ipython==9.15.0
|
|
||||||
# via ipdb
|
|
||||||
ipython-pygments-lexers==1.1.1
|
|
||||||
# via ipython
|
|
||||||
jaraco-classes==3.4.0
|
|
||||||
# via keyring
|
|
||||||
jaraco-context==6.1.2
|
|
||||||
# via keyring
|
|
||||||
jaraco-functools==4.5.0
|
|
||||||
# via keyring
|
|
||||||
jedi==0.20.0
|
|
||||||
# via ipython
|
|
||||||
jeepney==0.9.0
|
|
||||||
# via
|
|
||||||
# keyring
|
|
||||||
# secretstorage
|
|
||||||
keyring==25.7.0
|
|
||||||
# via twine
|
|
||||||
librt==0.11.0
|
|
||||||
# via mypy
|
|
||||||
markdown-it-py==4.2.0
|
|
||||||
# via rich
|
|
||||||
matplotlib-inline==0.2.2
|
|
||||||
# via ipython
|
|
||||||
mdurl==0.1.2
|
|
||||||
# via markdown-it-py
|
|
||||||
more-itertools==11.1.0
|
|
||||||
# via
|
|
||||||
# jaraco-classes
|
|
||||||
# jaraco-functools
|
|
||||||
mypy==2.1.0
|
|
||||||
# via kaya (pyproject.toml)
|
|
||||||
mypy-extensions==1.1.0
|
|
||||||
# via mypy
|
|
||||||
nh3==0.3.6
|
|
||||||
# via readme-renderer
|
|
||||||
packaging==26.2
|
|
||||||
# via
|
|
||||||
# build
|
|
||||||
# twine
|
|
||||||
parso==0.8.7
|
|
||||||
# via jedi
|
|
||||||
pathspec==1.1.1
|
|
||||||
# via mypy
|
|
||||||
pexpect==4.9.0
|
|
||||||
# via ipython
|
|
||||||
prompt-toolkit==3.0.52
|
|
||||||
# via ipython
|
|
||||||
psutil==7.2.2
|
|
||||||
# via ipython
|
|
||||||
ptyprocess==0.7.0
|
|
||||||
# via pexpect
|
|
||||||
pure-eval==0.2.3
|
|
||||||
# via stack-data
|
|
||||||
pwo==0.1.2
|
|
||||||
# via kaya (pyproject.toml)
|
|
||||||
pycparser==3.0
|
|
||||||
# via cffi
|
|
||||||
pygments==2.20.0
|
|
||||||
# via
|
|
||||||
# ipython
|
|
||||||
# ipython-pygments-lexers
|
|
||||||
# readme-renderer
|
|
||||||
# rich
|
|
||||||
pyproject-hooks==1.2.0
|
|
||||||
# via build
|
|
||||||
readme-renderer==45.0
|
|
||||||
# via twine
|
|
||||||
requests==2.34.2
|
|
||||||
# via
|
|
||||||
# requests-toolbelt
|
|
||||||
# twine
|
|
||||||
requests-toolbelt==1.0.0
|
|
||||||
# via twine
|
|
||||||
rfc3986==2.0.0
|
|
||||||
# via twine
|
|
||||||
rich==15.0.0
|
|
||||||
# via twine
|
|
||||||
secretstorage==3.5.0
|
|
||||||
# via keyring
|
|
||||||
stack-data==0.6.3
|
|
||||||
# via ipython
|
|
||||||
traitlets==5.15.1
|
|
||||||
# via
|
|
||||||
# ipython
|
|
||||||
# matplotlib-inline
|
|
||||||
twine==6.2.0
|
|
||||||
# via kaya (pyproject.toml)
|
|
||||||
typing-extensions==4.15.0
|
|
||||||
# via
|
|
||||||
# mypy
|
|
||||||
# pwo
|
|
||||||
urllib3==2.7.0
|
|
||||||
# via
|
|
||||||
# id
|
|
||||||
# requests
|
|
||||||
# twine
|
|
||||||
wcwidth==0.8.1
|
|
||||||
# via prompt-toolkit
|
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
#
|
|
||||||
# This file is autogenerated by pip-compile with Python 3.14
|
|
||||||
# by the following command:
|
|
||||||
#
|
|
||||||
# pip-compile --allow-unsafe --extra=rsgi --output-file=requirements.txt pyproject.toml
|
|
||||||
#
|
|
||||||
--index-url https://gitea.woggioni.net/api/packages/woggioni/pypi/simple
|
|
||||||
--extra-index-url https://pypi.org/simple
|
|
||||||
|
|
||||||
click==8.4.2
|
|
||||||
# via granian
|
|
||||||
granian==2.7.7
|
|
||||||
# via kaya (pyproject.toml)
|
|
||||||
pwo==0.1.2
|
|
||||||
# via kaya (pyproject.toml)
|
|
||||||
typing-extensions==4.15.0
|
|
||||||
# via pwo
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
from dataclasses import dataclass
|
|
||||||
from typing import (
|
|
||||||
Optional,
|
|
||||||
Dict,
|
|
||||||
List,
|
|
||||||
)
|
|
||||||
from ._types import NodeType
|
|
||||||
from ._path_handler import PathHandler
|
|
||||||
from ._path_matcher import PathMatcher
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
from typing import (
|
|
||||||
TypedDict,
|
|
||||||
Literal,
|
|
||||||
Iterable,
|
|
||||||
Tuple,
|
|
||||||
Optional,
|
|
||||||
NotRequired,
|
|
||||||
Dict,
|
|
||||||
Any,
|
|
||||||
Union,
|
|
||||||
Mapping,
|
|
||||||
Sequence
|
|
||||||
)
|
|
||||||
|
|
||||||
from .base import StrOrStrings, PathMatcherResult
|
|
||||||
|
|
||||||
from kaya._http_method import HttpMethod
|
|
||||||
|
|
||||||
type NodeType = (str | HttpMethod)
|
|
||||||
|
|
||||||
|
|
||||||
class ASGIVersions(TypedDict):
|
|
||||||
spec_version: str
|
|
||||||
version: Union[Literal["2.0"], Literal["3.0"]]
|
|
||||||
|
|
||||||
|
|
||||||
class HTTPScope(TypedDict):
|
|
||||||
type: Literal["http"]
|
|
||||||
asgi: ASGIVersions
|
|
||||||
http_version: str
|
|
||||||
method: str
|
|
||||||
scheme: str
|
|
||||||
path: str
|
|
||||||
raw_path: bytes
|
|
||||||
query_string: bytes
|
|
||||||
root_path: str
|
|
||||||
headers: Iterable[Tuple[bytes, bytes]]
|
|
||||||
client: Optional[Tuple[str, int]]
|
|
||||||
server: Optional[Tuple[str, Optional[int]]]
|
|
||||||
state: NotRequired[Dict[str, Any]]
|
|
||||||
extensions: Optional[Dict[str, Dict[object, object]]]
|
|
||||||
|
|
||||||
|
|
||||||
class WebSocketScope(TypedDict):
|
|
||||||
type: Literal["websocket"]
|
|
||||||
asgi: ASGIVersions
|
|
||||||
http_version: str
|
|
||||||
scheme: str
|
|
||||||
path: str
|
|
||||||
raw_path: bytes
|
|
||||||
query_string: bytes
|
|
||||||
root_path: str
|
|
||||||
headers: Iterable[Tuple[bytes, bytes]]
|
|
||||||
client: Optional[Tuple[str, int]]
|
|
||||||
server: Optional[Tuple[str, Optional[int]]]
|
|
||||||
subprotocols: Iterable[str]
|
|
||||||
state: NotRequired[Dict[str, Any]]
|
|
||||||
extensions: Optional[Dict[str, Dict[object, object]]]
|
|
||||||
|
|
||||||
|
|
||||||
class LifespanScope(TypedDict):
|
|
||||||
type: Literal["lifespan"]
|
|
||||||
asgi: ASGIVersions
|
|
||||||
state: NotRequired[Dict[str, Any]]
|
|
||||||
|
|
||||||
|
|
||||||
class RSGI:
|
|
||||||
class Scope(TypedDict):
|
|
||||||
proto: Literal['http'] # = 'http'
|
|
||||||
rsgi_version: str
|
|
||||||
http_version: str
|
|
||||||
server: str
|
|
||||||
client: str
|
|
||||||
scheme: str
|
|
||||||
method: str
|
|
||||||
path: str
|
|
||||||
query_string: str
|
|
||||||
headers: Mapping[str, str]
|
|
||||||
authority: Optional[str]
|
|
||||||
|
|
||||||
|
|
||||||
__all__ = [
|
|
||||||
'HttpMethod',
|
|
||||||
'HTTPScope',
|
|
||||||
'LifespanScope',
|
|
||||||
'RSGI',
|
|
||||||
'ASGIVersions',
|
|
||||||
'WebSocketScope',
|
|
||||||
'NodeType',
|
|
||||||
'StrOrStrings',
|
|
||||||
'PathMatcherResult'
|
|
||||||
]
|
|
||||||
Reference in New Issue
Block a user