From fa3ff32f66ce7894c73db16f27227dc00d94f8e4 Mon Sep 17 00:00:00 2001 From: Walter Oggioni Date: Wed, 15 Jul 2026 13:39:58 +0000 Subject: [PATCH] Implemented modular code structure Refactored repository into kaya-core and kaya-rsgi packages --- .gitea/workflows/build.yaml | 13 +- .gitignore | 4 +- README.md | 28 ++- example/hello.py | 13 +- packages/kaya-core/README.md | 5 + packages/kaya-core/pyproject.toml | 57 +++++++ .../kaya-core/src/kaya/core}/__init__.py | 6 +- .../kaya-core/src/kaya/core}/_app.py | 37 +--- .../kaya-core/src/kaya/core}/_asgi.py | 0 .../kaya-core/src/kaya/core}/_http_context.py | 0 .../kaya-core/src/kaya/core}/_http_method.py | 0 .../kaya-core/src/kaya/core}/_path_handler.py | 0 .../kaya-core/src/kaya/core}/_path_matcher.py | 0 .../kaya-core/src/kaya/core}/_tree.py | 0 .../src/kaya/core/_types/__init__.py | 31 ++++ .../kaya-core/src/kaya/core}/_types/asgi.py | 0 .../kaya-core/src/kaya/core}/_types/base.py | 0 .../kaya-core/src/kaya/core}/_websocket.py | 0 .../kaya-core/src/kaya/core}/py.typed | 0 .../kaya-core/tests}/test_asgi.py | 2 +- .../kaya-core/tests}/test_tree.py | 3 +- .../kaya-core/tests}/test_websocket.py | 24 +-- packages/kaya-rsgi/README.md | 5 + .../kaya-rsgi/pyproject.toml | 24 +-- packages/kaya-rsgi/src/kaya/rsgi/__init__.py | 11 ++ .../kaya-rsgi/src/kaya/rsgi}/_rsgi.py | 43 +++-- .../kaya-rsgi/src/kaya/rsgi/_types.py | 16 +- packages/kaya-rsgi/src/kaya/rsgi/py.typed | 0 packages/kaya-rsgi/tests/test_rsgi.py | 22 +++ requirements-dev.txt | 160 ++---------------- requirements.txt | 17 -- src/kaya/_node.py | 12 -- src/kaya/_types/__init__.py | 92 ---------- 33 files changed, 238 insertions(+), 387 deletions(-) create mode 100644 packages/kaya-core/README.md create mode 100644 packages/kaya-core/pyproject.toml rename {src/kaya => packages/kaya-core/src/kaya/core}/__init__.py (71%) rename {src/kaya => packages/kaya-core/src/kaya/core}/_app.py (84%) rename {src/kaya => packages/kaya-core/src/kaya/core}/_asgi.py (100%) rename {src/kaya => packages/kaya-core/src/kaya/core}/_http_context.py (100%) rename {src/kaya => packages/kaya-core/src/kaya/core}/_http_method.py (100%) rename {src/kaya => packages/kaya-core/src/kaya/core}/_path_handler.py (100%) rename {src/kaya => packages/kaya-core/src/kaya/core}/_path_matcher.py (100%) rename {src/kaya => packages/kaya-core/src/kaya/core}/_tree.py (100%) create mode 100644 packages/kaya-core/src/kaya/core/_types/__init__.py rename {src/kaya => packages/kaya-core/src/kaya/core}/_types/asgi.py (100%) rename {src/kaya => packages/kaya-core/src/kaya/core}/_types/base.py (100%) rename {src/kaya => packages/kaya-core/src/kaya/core}/_websocket.py (100%) rename {src/kaya => packages/kaya-core/src/kaya/core}/py.typed (100%) rename {tests => packages/kaya-core/tests}/test_asgi.py (99%) rename {tests => packages/kaya-core/tests}/test_tree.py (97%) rename {tests => packages/kaya-core/tests}/test_websocket.py (89%) create mode 100644 packages/kaya-rsgi/README.md rename pyproject.toml => packages/kaya-rsgi/pyproject.toml (75%) create mode 100644 packages/kaya-rsgi/src/kaya/rsgi/__init__.py rename {src/kaya => packages/kaya-rsgi/src/kaya/rsgi}/_rsgi.py (86%) rename src/kaya/_types/rsgi.py => packages/kaya-rsgi/src/kaya/rsgi/_types.py (83%) create mode 100644 packages/kaya-rsgi/src/kaya/rsgi/py.typed create mode 100644 packages/kaya-rsgi/tests/test_rsgi.py delete mode 100644 requirements.txt delete mode 100644 src/kaya/_node.py delete mode 100644 src/kaya/_types/__init__.py diff --git a/.gitea/workflows/build.yaml b/.gitea/workflows/build.yaml index 4bf9d48..95aa07b 100644 --- a/.gitea/workflows/build.yaml +++ b/.gitea/workflows/build.yaml @@ -26,14 +26,17 @@ jobs: .venv/bin/pip install -r requirements-dev.txt - name: Unit tests run: | - .venv/bin/pip install . - .venv/bin/python -m mypy -p src - .venv/bin/python -m unittest discover -s tests + .venv/bin/pip install packages/kaya-core packages/kaya-rsgi + .venv/bin/python -m mypy -p kaya.core + .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 env: TWINE_REPOSITORY_URL: ${{ vars.PYPI_REGISTRY_URL }} TWINE_USERNAME: ${{ vars.PUBLISHER_USERNAME }} TWINE_PASSWORD: ${{ secrets.PUBLISHER_TOKEN }} run: | - .venv/bin/pyproject-build - .venv/bin/twine upload --repository gitea dist/*.whl dist/*.tar.gz + .venv/bin/pyproject-build packages/kaya-core + .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 diff --git a/.gitignore b/.gitignore index 5f4ddbf..dd133ef 100644 --- a/.gitignore +++ b/.gitignore @@ -4,5 +4,5 @@ __pycache__ .mypy_cache _version.py *.egg-info -/build -/dist +build/ +dist/ diff --git a/README.md b/README.md index 9161710..be16f69 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,15 @@ # 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 @@ -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 ``` -Build the package: +Install the packages in development mode: ```bash -python -m build +pip install -e packages/kaya-core -e packages/kaya-rsgi ``` Run the example: @@ -25,11 +34,20 @@ python example/hello.py ## Tests ```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 ```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 ``` diff --git a/example/hello.py b/example/hello.py index 4015a80..5ceb9ee 100644 --- a/example/hello.py +++ b/example/hello.py @@ -1,15 +1,6 @@ -from kaya import KayaApp, HttpContext, HttpMethod, WebSocket +from kaya.core import KayaApp, HttpContext, HttpMethod, WebSocket 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() @@ -53,4 +44,4 @@ async def echo(ws: WebSocket) -> None: elif msg.kind == 'binary': data = msg.data assert isinstance(data, bytes) - await ws.send_bytes(data) \ No newline at end of file + await ws.send_bytes(data) diff --git a/packages/kaya-core/README.md b/packages/kaya-core/README.md new file mode 100644 index 0000000..e58a60f --- /dev/null +++ b/packages/kaya-core/README.md @@ -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. diff --git a/packages/kaya-core/pyproject.toml b/packages/kaya-core/pyproject.toml new file mode 100644 index 0000000..6470281 --- /dev/null +++ b/packages/kaya-core/pyproject.toml @@ -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/" diff --git a/src/kaya/__init__.py b/packages/kaya-core/src/kaya/core/__init__.py similarity index 71% rename from src/kaya/__init__.py rename to packages/kaya-core/src/kaya/core/__init__.py index f1cf9e3..f30c647 100644 --- a/src/kaya/__init__.py +++ b/packages/kaya-core/src/kaya/core/__init__.py @@ -1,17 +1,19 @@ -from ._app import KayaApp +from ._app import AbstractKayaApp, KayaApp from ._http_method import HttpMethod from ._http_context import HttpContext from ._tree import Tree, PathIterator -from ._path_handler import PathHandler +from ._path_handler import PathHandler, Matches from ._websocket import WebSocket, WebSocketMessage __all__ = [ + 'AbstractKayaApp', 'HttpMethod', 'KayaApp', 'HttpContext', 'Tree', 'PathHandler', + 'Matches', 'PathIterator', 'WebSocket', 'WebSocketMessage' diff --git a/src/kaya/_app.py b/packages/kaya-core/src/kaya/core/_app.py similarity index 84% rename from src/kaya/_app.py rename to packages/kaya-core/src/kaya/core/_app.py index 0399719..3292241 100644 --- a/src/kaya/_app.py +++ b/packages/kaya-core/src/kaya/core/_app.py @@ -2,31 +2,13 @@ from abc import ABC, abstractmethod from asyncio import Queue, AbstractEventLoop from asyncio import get_running_loop from logging import getLogger -from typing import Callable, Awaitable, Any, Mapping, Sequence, Optional, Unpack, Tuple, TYPE_CHECKING, cast -from pathlib import Path, PurePath +from typing import Callable, Awaitable, Any, Mapping, Sequence, Optional, Unpack, Tuple, cast from pwo import Maybe, AsyncQueueIterator -from hashlib import md5 from ._http_context import HttpContext from ._http_method import HttpMethod from ._path_handler import Context from ._types import StrOrStrings 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 ._tree import Tree 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: 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): _tree: Tree @@ -115,7 +83,6 @@ class KayaApp(AbstractKayaApp): await handler.handle_request(ctx, captured) else: await ctx.send_empty(404) - pass async def handle_websocket(self, ws: WebSocket) -> None: result = self._tree.get_handler(ws.path, HttpMethod.WS) @@ -124,7 +91,6 @@ class KayaApp(AbstractKayaApp): await handler.handle_request(ws, captured) else: await ws.close(1000) - pass def route(self, paths: StrOrStrings, @@ -179,4 +145,3 @@ class KayaApp(AbstractKayaApp): def PATCH(self, path: str, recursive: bool = False) -> Callable[[HttpHandler], HttpHandler]: return self.route(path, (HttpMethod.PATCH,), recursive) - diff --git a/src/kaya/_asgi.py b/packages/kaya-core/src/kaya/core/_asgi.py similarity index 100% rename from src/kaya/_asgi.py rename to packages/kaya-core/src/kaya/core/_asgi.py diff --git a/src/kaya/_http_context.py b/packages/kaya-core/src/kaya/core/_http_context.py similarity index 100% rename from src/kaya/_http_context.py rename to packages/kaya-core/src/kaya/core/_http_context.py diff --git a/src/kaya/_http_method.py b/packages/kaya-core/src/kaya/core/_http_method.py similarity index 100% rename from src/kaya/_http_method.py rename to packages/kaya-core/src/kaya/core/_http_method.py diff --git a/src/kaya/_path_handler.py b/packages/kaya-core/src/kaya/core/_path_handler.py similarity index 100% rename from src/kaya/_path_handler.py rename to packages/kaya-core/src/kaya/core/_path_handler.py diff --git a/src/kaya/_path_matcher.py b/packages/kaya-core/src/kaya/core/_path_matcher.py similarity index 100% rename from src/kaya/_path_matcher.py rename to packages/kaya-core/src/kaya/core/_path_matcher.py diff --git a/src/kaya/_tree.py b/packages/kaya-core/src/kaya/core/_tree.py similarity index 100% rename from src/kaya/_tree.py rename to packages/kaya-core/src/kaya/core/_tree.py diff --git a/packages/kaya-core/src/kaya/core/_types/__init__.py b/packages/kaya-core/src/kaya/core/_types/__init__.py new file mode 100644 index 0000000..17738f9 --- /dev/null +++ b/packages/kaya-core/src/kaya/core/_types/__init__.py @@ -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' +] diff --git a/src/kaya/_types/asgi.py b/packages/kaya-core/src/kaya/core/_types/asgi.py similarity index 100% rename from src/kaya/_types/asgi.py rename to packages/kaya-core/src/kaya/core/_types/asgi.py diff --git a/src/kaya/_types/base.py b/packages/kaya-core/src/kaya/core/_types/base.py similarity index 100% rename from src/kaya/_types/base.py rename to packages/kaya-core/src/kaya/core/_types/base.py diff --git a/src/kaya/_websocket.py b/packages/kaya-core/src/kaya/core/_websocket.py similarity index 100% rename from src/kaya/_websocket.py rename to packages/kaya-core/src/kaya/core/_websocket.py diff --git a/src/kaya/py.typed b/packages/kaya-core/src/kaya/core/py.typed similarity index 100% rename from src/kaya/py.typed rename to packages/kaya-core/src/kaya/core/py.typed diff --git a/tests/test_asgi.py b/packages/kaya-core/tests/test_asgi.py similarity index 99% rename from tests/test_asgi.py rename to packages/kaya-core/tests/test_asgi.py index 401d364..7a6f33d 100644 --- a/tests/test_asgi.py +++ b/packages/kaya-core/tests/test_asgi.py @@ -2,7 +2,7 @@ import unittest import json import httpx from pwo import async_test -from kaya import KayaApp, HttpContext, HttpMethod +from kaya.core import KayaApp, HttpContext, HttpMethod from typing import Sequence, List diff --git a/tests/test_tree.py b/packages/kaya-core/tests/test_tree.py similarity index 97% rename from tests/test_tree.py rename to packages/kaya-core/tests/test_tree.py index bf748ca..7063bfc 100644 --- a/tests/test_tree.py +++ b/packages/kaya-core/tests/test_tree.py @@ -1,7 +1,6 @@ from typing import Sequence, Tuple, Optional, List -from kaya import Tree, PathHandler, HttpContext, HttpMethod, PathIterator -from kaya import HttpMethod +from kaya.core import Tree, PathHandler, HttpContext, HttpMethod, PathIterator from pwo import Maybe import unittest diff --git a/tests/test_websocket.py b/packages/kaya-core/tests/test_websocket.py similarity index 89% rename from tests/test_websocket.py rename to packages/kaya-core/tests/test_websocket.py index e848226..6d36615 100644 --- a/tests/test_websocket.py +++ b/packages/kaya-core/tests/test_websocket.py @@ -1,7 +1,7 @@ import unittest from typing import Any, Callable, Awaitable, List, Mapping, Optional 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]: @@ -151,25 +151,3 @@ class WebSocketTest(unittest.TestCase): self.assertEqual(sent_messages[0]['type'], 'websocket.accept') 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)) diff --git a/packages/kaya-rsgi/README.md b/packages/kaya-rsgi/README.md new file mode 100644 index 0000000..f9f34f2 --- /dev/null +++ b/packages/kaya-rsgi/README.md @@ -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. diff --git a/pyproject.toml b/packages/kaya-rsgi/pyproject.toml similarity index 75% rename from pyproject.toml rename to packages/kaya-rsgi/pyproject.toml index e4ac94e..d34c050 100644 --- a/pyproject.toml +++ b/packages/kaya-rsgi/pyproject.toml @@ -3,42 +3,43 @@ requires = ["setuptools>=61.0", "setuptools-scm>=8"] build-backend = "setuptools.build_meta" [project] -name = "kaya" +name = "kaya-rsgi" dynamic = ["version"] authors = [ { 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" requires-python = ">=3.10" +license = "MIT" classifiers = [ 'Development Status :: 3 - Alpha', 'Topic :: Utilities', - 'License :: OSI Approved :: MIT License', 'Intended Audience :: System Administrators', 'Intended Audience :: Developers', 'Environment :: Console', - 'License :: OSI Approved :: MIT License', 'Programming Language :: Python :: 3', ] dependencies = [ + "kaya-core", + "granian>=2.0", "pwo", ] [project.optional-dependencies] dev = [ - "build", "mypy", "ipdb", "twine", "granian", "httpx" -] - -rsgi = [ - "granian" + "build", "mypy", "ipdb", "twine" ] [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 @@ -50,7 +51,8 @@ exclude = ["scripts", "docs", "test"] strict = true [tool.setuptools_scm] -version_file = "src/kaya/_version.py" +root = "../.." +version_file = "src/kaya/rsgi/_version.py" [tool.setuptools_scm.tag] -prefix = "release/" \ No newline at end of file +prefix = "release/" diff --git a/packages/kaya-rsgi/src/kaya/rsgi/__init__.py b/packages/kaya-rsgi/src/kaya/rsgi/__init__.py new file mode 100644 index 0000000..0b230f2 --- /dev/null +++ b/packages/kaya-rsgi/src/kaya/rsgi/__init__.py @@ -0,0 +1,11 @@ +from ._rsgi import RsgiApplication, RsgiContext, RsgiWebSocket +from ._types import HTTPScope, WebSocketScope + + +__all__ = [ + 'RsgiApplication', + 'RsgiContext', + 'RsgiWebSocket', + 'HTTPScope', + 'WebSocketScope', +] diff --git a/src/kaya/_rsgi.py b/packages/kaya-rsgi/src/kaya/rsgi/_rsgi.py similarity index 86% rename from src/kaya/_rsgi.py rename to packages/kaya-rsgi/src/kaya/rsgi/_rsgi.py index 9f18637..3222b17 100644 --- a/src/kaya/_rsgi.py +++ b/packages/kaya-rsgi/src/kaya/rsgi/_rsgi.py @@ -14,13 +14,17 @@ from typing import ( 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 ._types import StrOrStrings -from ._http_context import HttpContext -from ._http_method import HttpMethod -from ._websocket import WebSocket, WebSocketMessage +from kaya.core import AbstractKayaApp, HttpContext, HttpMethod, WebSocket, WebSocketMessage +from kaya.core._types import StrOrStrings class RsgiContext(HttpContext): @@ -56,12 +60,6 @@ class RsgiContext(HttpContext): self.request_body = cast(AsyncIterator[bytes], 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 def _rearrange_headers(headers: Mapping[str, StrOrStrings]) -> List[Tuple[str, str]]: result = [] @@ -177,3 +175,26 @@ class RsgiWebSocket(WebSocket): if message.kind == 'close': raise StopAsyncIteration 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) diff --git a/src/kaya/_types/rsgi.py b/packages/kaya-rsgi/src/kaya/rsgi/_types.py similarity index 83% rename from src/kaya/_types/rsgi.py rename to packages/kaya-rsgi/src/kaya/rsgi/_types.py index 5c4f86c..152f8b2 100644 --- a/src/kaya/_types/rsgi.py +++ b/packages/kaya-rsgi/src/kaya/rsgi/_types.py @@ -1,17 +1,11 @@ from typing import ( - Sequence, TypedDict, Literal, - Iterable, - Tuple, Optional, - NotRequired, - Dict, - Any, - Union, Mapping, ) + class HTTPScope(TypedDict): proto: Literal['http'] rsgi_version: str @@ -37,4 +31,10 @@ class WebSocketScope(TypedDict): path: str query_string: str headers: Mapping[str, str] - authority: Optional[str] \ No newline at end of file + authority: Optional[str] + + +__all__ = [ + 'HTTPScope', + 'WebSocketScope', +] diff --git a/packages/kaya-rsgi/src/kaya/rsgi/py.typed b/packages/kaya-rsgi/src/kaya/rsgi/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/packages/kaya-rsgi/tests/test_rsgi.py b/packages/kaya-rsgi/tests/test_rsgi.py new file mode 100644 index 0000000..096850b --- /dev/null +++ b/packages/kaya-rsgi/tests/test_rsgi.py @@ -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)) diff --git a/requirements-dev.txt b/requirements-dev.txt index f36e39e..5aed869 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,154 +1,16 @@ # -# This file is autogenerated by pip-compile with Python 3.14 -# by the following command: -# -# pip-compile --allow-unsafe --extra=dev --extra=rsgi --output-file=requirements-dev.txt pyproject.toml +# Development dependencies for the Kaya monorepo. +# Install with: +# pip install -r requirements-dev.txt # --index-url https://gitea.woggioni.net/api/packages/woggioni/pypi/simple --extra-index-url https://pypi.org/simple -anyio==4.14.1 - # via httpx -ast-serialize==0.5.0 - # via mypy -asttokens==3.0.1 - # via stack-data -build==1.5.0 - # via kaya (pyproject.toml) -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 +build +mypy +ipdb +twine +httpx +granian +pwo +typing-extensions diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index b6492f1..0000000 --- a/requirements.txt +++ /dev/null @@ -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 diff --git a/src/kaya/_node.py b/src/kaya/_node.py deleted file mode 100644 index afafaa2..0000000 --- a/src/kaya/_node.py +++ /dev/null @@ -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 - - - diff --git a/src/kaya/_types/__init__.py b/src/kaya/_types/__init__.py deleted file mode 100644 index 5be2c60..0000000 --- a/src/kaya/_types/__init__.py +++ /dev/null @@ -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' -]