Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4ce95d8365
|
||
|
|
59f38f067f |
@@ -24,19 +24,27 @@ jobs:
|
||||
run: |
|
||||
python -m venv .venv
|
||||
.venv/bin/pip install -r requirements-dev.txt
|
||||
- name: Unit tests
|
||||
- name: Check kaya-core
|
||||
run: |
|
||||
.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
|
||||
- name: Check kaya-rsgi
|
||||
run: |
|
||||
.venv/bin/python -m mypy -p kaya.core
|
||||
.venv/bin/python -m unittest discover -s packages/kaya-core/tests
|
||||
- name: Publish kaya-core artifacts
|
||||
env:
|
||||
TWINE_REPOSITORY_URL: ${{ vars.PYPI_REGISTRY_URL }}
|
||||
TWINE_USERNAME: ${{ vars.PUBLISHER_USERNAME }}
|
||||
TWINE_PASSWORD: ${{ secrets.PUBLISHER_TOKEN }}
|
||||
run: |
|
||||
.venv/bin/pyproject-build packages/kaya-core
|
||||
.venv/bin/twine upload --repository gitea packages/kaya-core/dist/*.whl packages/kaya-core/dist/*.tar.gz
|
||||
- name: Publish kaya-rsgi artifacts
|
||||
env:
|
||||
TWINE_REPOSITORY_URL: ${{ vars.PYPI_REGISTRY_URL }}
|
||||
TWINE_USERNAME: ${{ vars.PUBLISHER_USERNAME }}
|
||||
TWINE_PASSWORD: ${{ secrets.PUBLISHER_TOKEN }}
|
||||
run: |
|
||||
.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
|
||||
.venv/bin/twine upload --repository gitea packages/kaya-rsgi/dist/*.whl packages/kaya-rsgi/dist/*.tar.gz
|
||||
|
||||
@@ -28,7 +28,7 @@ dependencies = [
|
||||
|
||||
[project.optional-dependencies]
|
||||
dev = [
|
||||
"build", "mypy", "ipdb", "twine", "httpx"
|
||||
"build", "mypy", "ipdb", "twine", "httpx", "httpx-ws"
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
|
||||
@@ -1,25 +1,9 @@
|
||||
import unittest
|
||||
from typing import Any, Callable, Awaitable, List, Mapping, Optional
|
||||
import httpx
|
||||
from pwo import async_test
|
||||
from kaya.core import KayaApp, WebSocket, WebSocketMessage
|
||||
|
||||
|
||||
def websocket_scope(path: str = '/ws') -> Mapping[str, Any]:
|
||||
return {
|
||||
'type': 'websocket',
|
||||
'asgi': {'spec_version': '2.3', 'version': '3.0'},
|
||||
'http_version': '1.1',
|
||||
'scheme': 'ws',
|
||||
'path': path,
|
||||
'raw_path': path.encode(),
|
||||
'query_string': b'',
|
||||
'root_path': '',
|
||||
'headers': [],
|
||||
'client': ('127.0.0.1', 12345),
|
||||
'server': ('127.0.0.1', 80),
|
||||
'subprotocols': [],
|
||||
'extensions': None,
|
||||
}
|
||||
from httpx_ws import aconnect_ws, WebSocketDisconnect
|
||||
from httpx_ws.transport import ASGIWebSocketTransport
|
||||
from kaya.core import KayaApp, WebSocket
|
||||
|
||||
|
||||
class WebSocketTest(unittest.TestCase):
|
||||
@@ -48,106 +32,49 @@ class WebSocketTest(unittest.TestCase):
|
||||
|
||||
@async_test
|
||||
async def test_echo_text(self):
|
||||
sent_messages: List[Mapping[str, Any]] = []
|
||||
received_messages: List[Mapping[str, Any]] = []
|
||||
transport = ASGIWebSocketTransport(app=self.app)
|
||||
|
||||
async def receive() -> Mapping[str, Any]:
|
||||
if not received_messages:
|
||||
received_messages.append({'type': 'websocket.connect'})
|
||||
return received_messages[-1]
|
||||
if len(received_messages) == 1:
|
||||
received_messages.append({'type': 'websocket.receive', 'text': 'hello'})
|
||||
return received_messages[-1]
|
||||
received_messages.append({'type': 'websocket.disconnect', 'code': 1000})
|
||||
return received_messages[-1]
|
||||
|
||||
async def send(message: Mapping[str, Any]) -> None:
|
||||
sent_messages.append(message)
|
||||
|
||||
await self.app(websocket_scope('/echo'), receive, send)
|
||||
|
||||
self.assertEqual(sent_messages[0]['type'], 'websocket.accept')
|
||||
self.assertEqual(sent_messages[1]['type'], 'websocket.send')
|
||||
self.assertEqual(sent_messages[1]['text'], 'echo: hello')
|
||||
async with httpx.AsyncClient(transport=transport, base_url="http://testserver") as client:
|
||||
async with aconnect_ws("/echo", client) as ws:
|
||||
await ws.send_text("hello")
|
||||
message = await ws.receive_text()
|
||||
self.assertEqual(message, "echo: hello")
|
||||
|
||||
@async_test
|
||||
async def test_echo_binary(self):
|
||||
sent_messages: List[Mapping[str, Any]] = []
|
||||
received_messages: List[Mapping[str, Any]] = []
|
||||
transport = ASGIWebSocketTransport(app=self.app)
|
||||
|
||||
async def receive() -> Mapping[str, Any]:
|
||||
if not received_messages:
|
||||
received_messages.append({'type': 'websocket.connect'})
|
||||
return received_messages[-1]
|
||||
if len(received_messages) == 1:
|
||||
received_messages.append({'type': 'websocket.receive', 'bytes': b'hello'})
|
||||
return received_messages[-1]
|
||||
received_messages.append({'type': 'websocket.disconnect', 'code': 1000})
|
||||
return received_messages[-1]
|
||||
|
||||
async def send(message: Mapping[str, Any]) -> None:
|
||||
sent_messages.append(message)
|
||||
|
||||
await self.app(websocket_scope('/echo'), receive, send)
|
||||
|
||||
self.assertEqual(sent_messages[0]['type'], 'websocket.accept')
|
||||
self.assertEqual(sent_messages[1]['type'], 'websocket.send')
|
||||
self.assertEqual(sent_messages[1]['bytes'], b'hello')
|
||||
async with httpx.AsyncClient(transport=transport, base_url="http://testserver") as client:
|
||||
async with aconnect_ws("/echo", client) as ws:
|
||||
await ws.send_bytes(b"hello")
|
||||
message = await ws.receive_bytes()
|
||||
self.assertEqual(message, b"hello")
|
||||
|
||||
@async_test
|
||||
async def test_path_parameter(self):
|
||||
sent_messages: List[Mapping[str, Any]] = []
|
||||
received_messages: List[Mapping[str, Any]] = []
|
||||
transport = ASGIWebSocketTransport(app=self.app)
|
||||
|
||||
async def receive() -> Mapping[str, Any]:
|
||||
if not received_messages:
|
||||
received_messages.append({'type': 'websocket.connect'})
|
||||
return received_messages[-1]
|
||||
if len(received_messages) == 1:
|
||||
received_messages.append({'type': 'websocket.receive', 'text': 'hi'})
|
||||
return received_messages[-1]
|
||||
received_messages.append({'type': 'websocket.disconnect', 'code': 1000})
|
||||
return received_messages[-1]
|
||||
|
||||
async def send(message: Mapping[str, Any]) -> None:
|
||||
sent_messages.append(message)
|
||||
|
||||
await self.app(websocket_scope('/room/general'), receive, send)
|
||||
|
||||
self.assertEqual(sent_messages[0]['type'], 'websocket.accept')
|
||||
self.assertEqual(sent_messages[1]['text'], '[general] hi')
|
||||
async with httpx.AsyncClient(transport=transport, base_url="http://testserver") as client:
|
||||
async with aconnect_ws("/room/general", client) as ws:
|
||||
await ws.send_text("hi")
|
||||
message = await ws.receive_text()
|
||||
self.assertEqual(message, "[general] hi")
|
||||
|
||||
@async_test
|
||||
async def test_no_handler(self):
|
||||
sent_messages: List[Mapping[str, Any]] = []
|
||||
transport = ASGIWebSocketTransport(app=self.app)
|
||||
|
||||
async def receive() -> Mapping[str, Any]:
|
||||
return {'type': 'websocket.connect'}
|
||||
async with httpx.AsyncClient(transport=transport, base_url="http://testserver") as client:
|
||||
with self.assertRaises(WebSocketDisconnect) as cm:
|
||||
async with aconnect_ws("/unknown", client) as ws:
|
||||
pass
|
||||
|
||||
async def send(message: Mapping[str, Any]) -> None:
|
||||
sent_messages.append(message)
|
||||
|
||||
await self.app(websocket_scope('/unknown'), receive, send)
|
||||
|
||||
self.assertEqual(sent_messages[0]['type'], 'websocket.close')
|
||||
self.assertEqual(sent_messages[0]['code'], 1000)
|
||||
self.assertEqual(cm.exception.code, 1000)
|
||||
|
||||
@async_test
|
||||
async def test_close_from_client(self):
|
||||
sent_messages: List[Mapping[str, Any]] = []
|
||||
received_messages: List[Mapping[str, Any]] = []
|
||||
transport = ASGIWebSocketTransport(app=self.app)
|
||||
|
||||
async def receive() -> Mapping[str, Any]:
|
||||
if not received_messages:
|
||||
received_messages.append({'type': 'websocket.connect'})
|
||||
return received_messages[-1]
|
||||
received_messages.append({'type': 'websocket.disconnect', 'code': 1001})
|
||||
return received_messages[-1]
|
||||
|
||||
async def send(message: Mapping[str, Any]) -> None:
|
||||
sent_messages.append(message)
|
||||
|
||||
await self.app(websocket_scope('/echo'), receive, send)
|
||||
|
||||
self.assertEqual(sent_messages[0]['type'], 'websocket.accept')
|
||||
self.assertEqual(len(sent_messages), 1)
|
||||
async with httpx.AsyncClient(transport=transport, base_url="http://testserver") as client:
|
||||
async with aconnect_ws("/echo", client) as ws:
|
||||
pass
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
kaya-core @ file:./packages/kaya-core
|
||||
kaya-rsgi @ file:./packages/kaya-rsgi
|
||||
build
|
||||
mypy
|
||||
ipdb
|
||||
twine
|
||||
httpx
|
||||
httpx-ws
|
||||
+170
-11
@@ -1,16 +1,175 @@
|
||||
#
|
||||
# Development dependencies for the Kaya monorepo.
|
||||
# Install with:
|
||||
# pip install -r requirements-dev.txt
|
||||
# This file is autogenerated by pip-compile with Python 3.14
|
||||
# by the following command:
|
||||
#
|
||||
# pip-compile --allow-unsafe --extra-index-url=https://pypi.org/simple --index-url=https://gitea.woggioni.net/api/packages/woggioni/pypi/simple --output-file=requirements-dev.txt requirements-dev.in
|
||||
#
|
||||
--index-url https://gitea.woggioni.net/api/packages/woggioni/pypi/simple
|
||||
--extra-index-url https://pypi.org/simple
|
||||
|
||||
build
|
||||
mypy
|
||||
ipdb
|
||||
twine
|
||||
httpx
|
||||
granian
|
||||
pwo
|
||||
typing-extensions
|
||||
anyio==4.14.2
|
||||
# via
|
||||
# httpx
|
||||
# httpx-ws
|
||||
ast-serialize==0.6.0
|
||||
# via mypy
|
||||
asttokens==3.0.2
|
||||
# via stack-data
|
||||
build==1.5.0
|
||||
# via -r requirements-dev.in
|
||||
certifi==2026.6.17
|
||||
# via
|
||||
# httpcore
|
||||
# httpx
|
||||
# requests
|
||||
cffi==2.1.0
|
||||
# via cryptography
|
||||
charset-normalizer==3.4.9
|
||||
# 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.9
|
||||
# via kaya-rsgi
|
||||
h11==0.16.0
|
||||
# via
|
||||
# httpcore
|
||||
# wsproto
|
||||
httpcore==1.0.9
|
||||
# via
|
||||
# httpx
|
||||
# httpx-ws
|
||||
httpx==0.28.1
|
||||
# via
|
||||
# -r requirements-dev.in
|
||||
# httpx-ws
|
||||
httpx-ws==0.9.0
|
||||
# via -r requirements-dev.in
|
||||
id==1.6.1
|
||||
# via twine
|
||||
idna==3.18
|
||||
# via
|
||||
# anyio
|
||||
# httpx
|
||||
# requests
|
||||
ipdb==0.13.13
|
||||
# via -r requirements-dev.in
|
||||
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.6.0
|
||||
# via keyring
|
||||
jedi==0.20.0
|
||||
# via ipython
|
||||
jeepney==0.9.0
|
||||
# via
|
||||
# keyring
|
||||
# secretstorage
|
||||
file:./packages/kaya-core
|
||||
# via
|
||||
# -r requirements-dev.in
|
||||
# kaya-rsgi
|
||||
file:./packages/kaya-rsgi
|
||||
# via -r requirements-dev.in
|
||||
keyring==25.7.0
|
||||
# via twine
|
||||
librt==0.13.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.3.0
|
||||
# via -r requirements-dev.in
|
||||
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-core
|
||||
# kaya-rsgi
|
||||
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 -r requirements-dev.in
|
||||
typing-extensions==4.16.0
|
||||
# via
|
||||
# kaya-core
|
||||
# mypy
|
||||
# pwo
|
||||
urllib3==2.7.0
|
||||
# via
|
||||
# id
|
||||
# requests
|
||||
# twine
|
||||
wcwidth==0.8.2
|
||||
# via prompt-toolkit
|
||||
wsproto==1.3.2
|
||||
# via httpx-ws
|
||||
|
||||
Reference in New Issue
Block a user