Implemented modular code structure
Refactored repository into kaya-core and kaya-rsgi packages
This commit is contained in:
@@ -0,0 +1,192 @@
|
||||
import unittest
|
||||
import json
|
||||
import httpx
|
||||
from pwo import async_test
|
||||
from kaya.core import KayaApp, HttpContext, HttpMethod
|
||||
from typing import Sequence, List
|
||||
|
||||
|
||||
class AsgiTest(unittest.TestCase):
|
||||
app: KayaApp
|
||||
|
||||
def setUp(self):
|
||||
self.app = KayaApp()
|
||||
|
||||
@self.app.GET('/hello')
|
||||
@self.app.GET('/hello2')
|
||||
@self.app.route('/hello3')
|
||||
@self.app.GET('/hello/*')
|
||||
async def handle_request(ctx: HttpContext) -> None:
|
||||
async for chunk in ctx.request_body:
|
||||
print(chunk)
|
||||
await ctx.send_str(200, 'Hello World!')
|
||||
|
||||
@self.app.route(('/foo/bar',), HttpMethod.PUT, recursive=True)
|
||||
async def handle_request(ctx: HttpContext) -> None:
|
||||
async for chunk in ctx.request_body:
|
||||
print(chunk)
|
||||
await ctx.send_str(200, ctx.path)
|
||||
|
||||
@self.app.route(('/foo/*',), HttpMethod.PUT, recursive=True)
|
||||
async def handle_request(ctx: HttpContext, path: Sequence[str]) -> None:
|
||||
async for chunk in ctx.request_body:
|
||||
print(chunk)
|
||||
await ctx.send_str(200, json.dumps(path))
|
||||
|
||||
@self.app.route(('/foo/*',), recursive=True)
|
||||
async def handle_request(ctx: HttpContext, path: Sequence[str]) -> None:
|
||||
await ctx.send_str(500, f"Unable to handle request for {ctx.path}")
|
||||
|
||||
@self.app.GET('/employee/${employee_id}')
|
||||
async def handle_request(ctx: HttpContext, employee_id: str) -> None:
|
||||
async for chunk in ctx.request_body:
|
||||
print(chunk)
|
||||
await ctx.send_str(200, employee_id)
|
||||
|
||||
@self.app.GET('/square/${x:int}')
|
||||
async def handle_request(ctx: HttpContext, x: int) -> None:
|
||||
async for chunk in ctx.request_body:
|
||||
print(chunk)
|
||||
await ctx.send_str(200, str(x * x))
|
||||
|
||||
@self.app.GET('/department/${department_id:int}/employee/${employee_id:int}')
|
||||
async def handle_request(ctx: HttpContext, department_id: int, employee_id: int) -> None:
|
||||
async for chunk in ctx.request_body:
|
||||
print(chunk)
|
||||
await ctx.send_str(200, json.dumps({
|
||||
'department_id': department_id,
|
||||
'employee_id': employee_id
|
||||
}))
|
||||
|
||||
@self.app.PUT('/hello/*', recursive=True)
|
||||
async def handle_request(ctx: HttpContext, _: List[str]) -> None:
|
||||
await ctx.stream_body(200, (chunk async for chunk in ctx.request_body))
|
||||
|
||||
@async_test
|
||||
async def test_hello(self):
|
||||
transport = httpx.ASGITransport(app=self.app)
|
||||
|
||||
async with httpx.AsyncClient(transport=transport, base_url="http://127.0.0.1:80") as client:
|
||||
r = await client.get("/hello")
|
||||
self.assertEqual(200, r.status_code)
|
||||
self.assertEqual(r.text, "Hello World!")
|
||||
|
||||
r = await client.get("/hello2")
|
||||
self.assertEqual(200, r.status_code)
|
||||
self.assertEqual(r.text, "Hello World!")
|
||||
|
||||
r = await client.post("/hello3")
|
||||
self.assertEqual(200, r.status_code)
|
||||
self.assertEqual(r.text, "Hello World!")
|
||||
|
||||
r = await client.get("/hello4")
|
||||
self.assertEqual(404, r.status_code)
|
||||
self.assertTrue(len(r.text) == 0)
|
||||
|
||||
body = {'name': 'John', 'surname': 'Smith'}
|
||||
r = await client.put("/hello/foo/bar", json=body)
|
||||
self.assertEqual(200, r.status_code)
|
||||
ans = json.loads(r.text)
|
||||
self.assertTrue(body, ans)
|
||||
|
||||
@async_test
|
||||
async def test_foo(self):
|
||||
transport = httpx.ASGITransport(app=self.app)
|
||||
|
||||
async with httpx.AsyncClient(transport=transport, base_url="http://127.0.0.1:80") as client:
|
||||
r = await client.put("/foo/fizz/baz")
|
||||
self.assertEqual(200, r.status_code)
|
||||
response = json.loads(r.text)
|
||||
self.assertEqual(['fizz', 'baz'], response)
|
||||
|
||||
r = await client.get("/foo/not-put")
|
||||
self.assertEqual(500, r.status_code)
|
||||
self.assertEqual("Unable to handle request for /foo/not-put", r.text)
|
||||
|
||||
@async_test
|
||||
async def test_method_agnostic_fallback_order_independence(self):
|
||||
app = KayaApp()
|
||||
|
||||
@app.route(('/foo/*',), recursive=True)
|
||||
async def handle_request(ctx: HttpContext, path: Sequence[str]) -> None:
|
||||
await ctx.send_str(500, f"Unable to handle request for {ctx.path}")
|
||||
|
||||
@app.route(('/foo/*',), HttpMethod.PUT, recursive=True)
|
||||
async def handle_request(ctx: HttpContext, path: Sequence[str]) -> None:
|
||||
await ctx.send_str(200, json.dumps(path))
|
||||
|
||||
transport = httpx.ASGITransport(app=app)
|
||||
|
||||
async with httpx.AsyncClient(transport=transport, base_url="http://127.0.0.1:80") as client:
|
||||
r = await client.put("/foo/fizz/baz")
|
||||
self.assertEqual(200, r.status_code)
|
||||
self.assertEqual(['fizz', 'baz'], json.loads(r.text))
|
||||
|
||||
r = await client.get("/foo/not-put")
|
||||
self.assertEqual(500, r.status_code)
|
||||
self.assertEqual("Unable to handle request for /foo/not-put", r.text)
|
||||
|
||||
@async_test
|
||||
async def test_disjoint_method_specific_matchers(self):
|
||||
app = KayaApp()
|
||||
|
||||
@app.route(('/foo/*',), HttpMethod.PUT, recursive=True)
|
||||
async def handle_request(ctx: HttpContext, path: Sequence[str]) -> None:
|
||||
await ctx.send_str(200, "PUT")
|
||||
|
||||
@app.route(('/foo/*',), HttpMethod.GET, recursive=True)
|
||||
async def handle_request(ctx: HttpContext, path: Sequence[str]) -> None:
|
||||
await ctx.send_str(200, "GET")
|
||||
|
||||
transport = httpx.ASGITransport(app=app)
|
||||
|
||||
async with httpx.AsyncClient(transport=transport, base_url="http://127.0.0.1:80") as client:
|
||||
r = await client.put("/foo/bar")
|
||||
self.assertEqual(200, r.status_code)
|
||||
self.assertEqual("PUT", r.text)
|
||||
|
||||
r = await client.get("/foo/bar")
|
||||
self.assertEqual(200, r.status_code)
|
||||
self.assertEqual("GET", r.text)
|
||||
|
||||
@async_test
|
||||
async def test_foo_bar(self):
|
||||
transport = httpx.ASGITransport(app=self.app)
|
||||
|
||||
async with httpx.AsyncClient(transport=transport, base_url="http://127.0.0.1:80") as client:
|
||||
r = await client.put("/foo/bar/baz")
|
||||
self.assertEqual(200, r.status_code)
|
||||
self.assertEqual('/foo/bar/baz', r.text)
|
||||
|
||||
@async_test
|
||||
async def test_employee(self):
|
||||
transport = httpx.ASGITransport(app=self.app)
|
||||
|
||||
async with httpx.AsyncClient(transport=transport, base_url="http://127.0.0.1:80") as client:
|
||||
r = await client.get("/employee/101325")
|
||||
self.assertEqual(200, r.status_code)
|
||||
self.assertEqual(r.text, '101325')
|
||||
|
||||
@async_test
|
||||
async def test_square(self):
|
||||
transport = httpx.ASGITransport(app=self.app)
|
||||
|
||||
async with httpx.AsyncClient(transport=transport, base_url="http://127.0.0.1:80") as client:
|
||||
x = 30
|
||||
r = await client.get(f"/square/{x}")
|
||||
self.assertEqual(200, r.status_code)
|
||||
self.assertEqual(r.text, str(x * x))
|
||||
|
||||
@async_test
|
||||
async def test_department_employee(self):
|
||||
transport = httpx.ASGITransport(app=self.app)
|
||||
|
||||
async with httpx.AsyncClient(transport=transport, base_url="http://127.0.0.1:80") as client:
|
||||
r = await client.get("department/189350/employee/101325")
|
||||
self.assertEqual(200, r.status_code)
|
||||
response = json.loads(r.text)
|
||||
self.assertEqual({
|
||||
'department_id': 189350,
|
||||
'employee_id': 101325
|
||||
}, response)
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
from typing import Sequence, Tuple, Optional, List
|
||||
|
||||
from kaya.core import Tree, PathHandler, HttpContext, HttpMethod, PathIterator
|
||||
from pwo import Maybe
|
||||
import unittest
|
||||
|
||||
|
||||
class PathIteratorTest(unittest.TestCase):
|
||||
cases: Tuple[Tuple[str, Tuple[str, ...]], ...] = (
|
||||
('/', tuple()),
|
||||
('root/foo', ('root', 'foo')),
|
||||
('/root', ('root',)),
|
||||
('/root', ('root',)),
|
||||
('/root/', ('root',)),
|
||||
('/root/bar/', ('root', 'bar')),
|
||||
)
|
||||
|
||||
def test_path_iterator(self):
|
||||
for (case, expected) in self.cases:
|
||||
with self.subTest(case) as _:
|
||||
components = tuple((c for c in PathIterator(case)))
|
||||
self.assertEqual(expected, components)
|
||||
|
||||
|
||||
class TreeTest(unittest.TestCase):
|
||||
tree: Tree
|
||||
handlers: List[PathHandler]
|
||||
|
||||
def setUp(self):
|
||||
self.tree = Tree()
|
||||
|
||||
class TestHandler(PathHandler):
|
||||
|
||||
def handle_request(self, ctx: HttpContext):
|
||||
pass
|
||||
|
||||
@property
|
||||
def recursive(self) -> bool:
|
||||
return True
|
||||
|
||||
self.handlers = [TestHandler() for _ in range(20)]
|
||||
|
||||
routes: Tuple[Tuple[Tuple[str, ...], Optional[HttpMethod], PathHandler], ...] = (
|
||||
(('home', 'something'), HttpMethod.GET, self.handlers[0]),
|
||||
(('home', 'something_else'), HttpMethod.GET, self.handlers[1]),
|
||||
(('home', 'something_else'), HttpMethod.POST, self.handlers[2]),
|
||||
(('home', 'something', 'object'), HttpMethod.GET, self.handlers[3]),
|
||||
(('home', 'something_else', 'foo'), HttpMethod.GET, self.handlers[4]),
|
||||
(('home',), HttpMethod.GET, self.handlers[5]),
|
||||
(('home',), HttpMethod.POST, self.handlers[6]),
|
||||
(('home',), None, self.handlers[7]),
|
||||
(('home', '*.md'), None, self.handlers[8]),
|
||||
(('home', 'something', '*', 'blah', '*.md'), None, self.handlers[9]),
|
||||
(('home', 'bar', '*'), None, self.handlers[10]),
|
||||
|
||||
)
|
||||
|
||||
for path, method, handler in routes:
|
||||
self.tree.add((p for p in path), method, handler)
|
||||
|
||||
def test_tree(self):
|
||||
|
||||
cases: Tuple[Tuple[str, HttpMethod, Optional[int]], ...] = (
|
||||
('http://localhost:127.0.0.1:5432/home/something', HttpMethod.GET, 0),
|
||||
('http://localhost:127.0.0.1:5432/home/something_else', HttpMethod.GET, 1),
|
||||
('http://localhost:127.0.0.1:5432/home/something_else', HttpMethod.POST, 2),
|
||||
('http://localhost:127.0.0.1:5432/home/something/object', HttpMethod.GET, 3),
|
||||
('http://localhost:127.0.0.1:5432/home/something_else/foo', HttpMethod.GET, 4),
|
||||
('http://localhost:127.0.0.1:5432/', HttpMethod.GET, None),
|
||||
('http://localhost:127.0.0.1:5432/home', HttpMethod.GET, 5),
|
||||
('http://localhost:127.0.0.1:5432/home', HttpMethod.POST, 6),
|
||||
('http://localhost:127.0.0.1:5432/home', HttpMethod.PUT, 7),
|
||||
('http://localhost:127.0.0.1:5432/home/README.md', HttpMethod.GET, 8),
|
||||
('http://localhost:127.0.0.1:5432/home/something/ciao/blah/README.md', HttpMethod.GET, 9),
|
||||
('http://localhost:127.0.0.1:5432/home/bar/ciao/blah/README.md', HttpMethod.GET, 10),
|
||||
)
|
||||
for url, method, handler_num in cases:
|
||||
with self.subTest(f"{str(method)} {url}"):
|
||||
res = self.tree.get_handler(url, method)
|
||||
self.assertIs(Maybe.of(handler_num).map(self.handlers.__getitem__).or_none(),
|
||||
Maybe.of_nullable(res).map(lambda it: it[0]).or_none())
|
||||
|
||||
def test_two_method_agnostic_matchers_raise(self):
|
||||
tree = Tree()
|
||||
tree.add((p for p in ('foo', '*')), None, self.handlers[0])
|
||||
with self.assertRaises(ValueError):
|
||||
tree.add((p for p in ('foo', '*')), None, self.handlers[1])
|
||||
|
||||
def test_two_overlapping_method_specific_matchers_raise(self):
|
||||
tree = Tree()
|
||||
tree.add((p for p in ('foo', '${id:int}')), HttpMethod.PUT, self.handlers[0])
|
||||
with self.assertRaises(ValueError):
|
||||
tree.add((p for p in ('foo', '${name:str}')), HttpMethod.PUT, self.handlers[1])
|
||||
|
||||
def test_disjoint_method_specific_matchers_allowed(self):
|
||||
tree = Tree()
|
||||
tree.add((p for p in ('foo', '*')), HttpMethod.PUT, self.handlers[0])
|
||||
tree.add((p for p in ('foo', '*')), HttpMethod.GET, self.handlers[1])
|
||||
put_handler = Maybe.of_nullable(tree.get_handler('/foo/bar', HttpMethod.PUT)).map(lambda it: it[0]).or_none()
|
||||
get_handler = Maybe.of_nullable(tree.get_handler('/foo/bar', HttpMethod.GET)).map(lambda it: it[0]).or_none()
|
||||
self.assertIs(self.handlers[0], put_handler)
|
||||
self.assertIs(self.handlers[1], get_handler)
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
import unittest
|
||||
from typing import Any, Callable, Awaitable, List, Mapping, Optional
|
||||
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,
|
||||
}
|
||||
|
||||
|
||||
class WebSocketTest(unittest.TestCase):
|
||||
app: KayaApp
|
||||
|
||||
def setUp(self):
|
||||
self.app = KayaApp()
|
||||
|
||||
@self.app.websocket('/echo')
|
||||
async def echo(ws: WebSocket) -> None:
|
||||
await ws.accept()
|
||||
async for msg in ws:
|
||||
if msg.kind == 'text':
|
||||
await ws.send_text(f"echo: {msg.data}")
|
||||
elif msg.kind == 'binary':
|
||||
data = msg.data
|
||||
assert isinstance(data, bytes)
|
||||
await ws.send_bytes(data)
|
||||
|
||||
@self.app.websocket('/room/${room_id}')
|
||||
async def room(ws: WebSocket, room_id: str) -> None:
|
||||
await ws.accept()
|
||||
async for msg in ws:
|
||||
if msg.kind == 'text':
|
||||
await ws.send_text(f"[{room_id}] {msg.data}")
|
||||
|
||||
@async_test
|
||||
async def test_echo_text(self):
|
||||
sent_messages: List[Mapping[str, Any]] = []
|
||||
received_messages: List[Mapping[str, Any]] = []
|
||||
|
||||
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_test
|
||||
async def test_echo_binary(self):
|
||||
sent_messages: List[Mapping[str, Any]] = []
|
||||
received_messages: List[Mapping[str, Any]] = []
|
||||
|
||||
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_test
|
||||
async def test_path_parameter(self):
|
||||
sent_messages: List[Mapping[str, Any]] = []
|
||||
received_messages: List[Mapping[str, Any]] = []
|
||||
|
||||
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_test
|
||||
async def test_no_handler(self):
|
||||
sent_messages: List[Mapping[str, Any]] = []
|
||||
|
||||
async def receive() -> Mapping[str, Any]:
|
||||
return {'type': 'websocket.connect'}
|
||||
|
||||
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)
|
||||
|
||||
@async_test
|
||||
async def test_close_from_client(self):
|
||||
sent_messages: List[Mapping[str, Any]] = []
|
||||
received_messages: List[Mapping[str, Any]] = []
|
||||
|
||||
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)
|
||||
Reference in New Issue
Block a user