Skip to content

Commit 5dc42f3

Browse files
Merge pull request #15 from CreatCodeBuild/dev
Dev
2 parents 48a8aab + 70879fa commit 5dc42f3

File tree

8 files changed

+215
-31
lines changed

8 files changed

+215
-31
lines changed

app.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,19 @@
22

33
if __name__ == '__main__':
44

5+
app = app.App()
6+
57
# A basic callback style API is provided
6-
async def get_name(http, stream):
8+
async def post_name(http, stream, parameters):
79
# this route essentially echo the data received back to client
810
print('data received:')
911
print(str(stream.data, encoding='utf8'))
1012
await http.send_and_end(stream, stream.data)
13+
app.post('name', post_name)
14+
15+
async def get_user(http, stream, parameters):
16+
print(parameters)
17+
await http.send_error(stream, 200)
18+
app.get('user/{userId}', get_user)
1119

12-
app = app.App(static_file_handle='auto', root_route='index.html')
13-
app.post('name', get_name)
1420
app.up()

hyper2web/abstract.py

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,6 @@ class AbstractApp:
77
def up(self):
88
raise NotImplementedError
99

10-
def register_route(self, method: str, route: str, handler):
11-
raise NotImplementedError
12-
1310
def get(self, route: str, handler):
1411
raise NotImplementedError
1512

@@ -18,3 +15,25 @@ def post(self, route: str, handler):
1815

1916
async def handle_route(self, http, stream):
2017
raise NotImplementedError
18+
19+
20+
class AbstractRouter:
21+
def register(self, method: str, route: str, handler):
22+
raise NotImplementedError
23+
24+
async def handle_route(self, http, stream):
25+
raise NotImplementedError
26+
27+
28+
class AbstractHTTP:
29+
async def handle_event(self, event):
30+
raise NotImplementedError
31+
32+
async def send_and_end(self, stream, data):
33+
raise NotImplementedError
34+
35+
async def send_file(self, stream, file_path):
36+
raise NotImplementedError
37+
38+
async def send_error(self, stream, error):
39+
raise NotImplementedError

hyper2web/app.py

Lines changed: 29 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,42 @@
44

55
from . import server, abstract
66
from .http import HTTP, Stream
7+
from .router import Router
78

89
AbstractApp = abstract.AbstractApp
910
h2_server = server.h2_server
1011

1112

1213
class App(AbstractApp):
13-
def __init__(self, port=5000, root='./public', static_file_handle='auto', root_route='index.html'):
14+
def __init__(self, port=5000, root='./public',
15+
auto_serve_static_file=True,
16+
default_file='index.html', router=Router):
17+
"""
18+
:param port: TCP port
19+
:param root: root directory to serve
20+
:param serve_static_file: automatically serve static files without manually register routes
21+
:param default_file: default static file to serve
22+
:param router: the router class to use
23+
"""
1424
self.port = port
1525
self.root = os.path.abspath(root)
16-
self.routes = {'GET': {}, 'POST': {}}
1726

1827
# todo: implement static_file_handle and root_route
19-
self.static_file_handle = static_file_handle
20-
self.root_route = root_route
28+
self.default_file = default_file
29+
30+
if auto_serve_static_file:
31+
async def default_get(http, stream, parameters):
32+
print('default_get')
33+
route = stream.headers[':path'].lstrip('/')
34+
full_path = os.path.join(self.root, route)
35+
if os.path.exists(full_path):
36+
await http.send_file(stream, full_path)
37+
else:
38+
await http.send_error(stream, 404)
39+
40+
self._router = router(default_get)
41+
else:
42+
self._router = router(None)
2143

2244
def up(self):
2345
kernel = Kernel()
@@ -27,27 +49,12 @@ def up(self):
2749
app=self),
2850
shutdown=True)
2951

30-
def register_route(self, method: str, route: str, handler):
31-
assert method in ['GET', 'POST']
32-
self.routes[method][route] = handler
33-
3452
def get(self, route: str, handler):
35-
self.register_route('GET', route, handler)
53+
self._router.register('GET', route, handler)
3654

3755
def post(self, route: str, handler):
38-
self.register_route('POST', route, handler)
56+
self._router.register('POST', route, handler)
3957

4058
# async
4159
async def handle_route(self, http: HTTP, stream: Stream):
42-
print('app.App.handle_route')
43-
44-
route = stream.headers[':path'].lstrip('/')
45-
if route in self.routes['GET'] or route in self.routes['POST']:
46-
await self.routes[stream.headers[':method']][route](http, stream)
47-
else:
48-
# if route is not registered, assume it is requesting files
49-
full_path = os.path.join(self.root, route)
50-
if os.path.exists(full_path):
51-
await http.send_file(stream, full_path)
52-
else:
53-
await http.send_error(stream, 404)
60+
await self._router.handle_route(http, stream)

hyper2web/http.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
from h2 import events
1313
from h2.connection import H2Connection
1414

15-
from .abstract import AbstractApp
15+
from .abstract import AbstractApp, AbstractHTTP
1616

1717
READ_CHUNK_SIZE = 8192
1818

@@ -60,7 +60,7 @@ def finalize(self):
6060
self.buffered_data = None
6161

6262

63-
class HTTP:
63+
class HTTP(AbstractHTTP):
6464
"""
6565
This class further implements complete HTTP2 on top of h2
6666
"""

hyper2web/router.py

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import os
2+
3+
from .abstract import AbstractRouter
4+
from .http import HTTP, Stream
5+
6+
7+
class Router(AbstractRouter):
8+
"""User should never construct Router"""
9+
10+
# todo: I may want to change the constructor
11+
def __init__(self, default_get):
12+
self._routes = {
13+
'GET': {},
14+
'POST': {}
15+
}
16+
self.default_get = default_get
17+
18+
def register(self, method: str, route: str, handler):
19+
assert method in ['GET', 'POST'] # 这只是目前为了测试而加的限制
20+
self._routes[method][route] = handler
21+
22+
def find_match(self, path: str):
23+
"""
24+
'user/{userId}' should match 'user/abc'
25+
userId = abc
26+
return a tuple (matched, parameters)
27+
matched is the route which matches the incoming path
28+
parameters is a dict of parameters and their values
29+
"""
30+
# todo: now the problem is how to implement it
31+
# todo: pattern matching should be independent from :method,
32+
# todo: but the current implementation doesn't support it. Should improve it later.
33+
print(self._routes.values())
34+
for routes_of_this_method in self._routes.values():
35+
print(routes_of_this_method)
36+
for route in routes_of_this_method:
37+
matched, parameters = self._match(route, path)
38+
if matched:
39+
return route, parameters
40+
return None, None
41+
42+
@classmethod
43+
def _match(cls, route, path):
44+
# todo: it seems like that regular expression is not necessary
45+
# note: Could it be simpler? Could regex help?
46+
route = route.split('/')
47+
path = path.split('/')
48+
if len(route) != len(path):
49+
return False, None
50+
else:
51+
# todo: implement it
52+
parameters = {}
53+
for r, p in zip(route, path):
54+
if r[0] == '{' and r[-1] == '}':
55+
parameters[r[1:-1]] = p
56+
elif r != p:
57+
return False, None
58+
return True, parameters
59+
60+
# async
61+
async def handle_route(self, http: HTTP, stream: Stream):
62+
print('app.App.handle_route')
63+
64+
path = stream.headers[':path'].lstrip('/')
65+
method = stream.headers[':method']
66+
67+
route, parameters = self.find_match(path)
68+
print('app.App.handle_route', route)
69+
70+
# 如果没有任何匹配,就默认为静态文件读取
71+
if route is None:
72+
if method == 'GET':
73+
print('GET')
74+
handler = self.default_get
75+
else:
76+
handler = None
77+
else:
78+
handler = self._routes[method].get(route, None)
79+
80+
if handler is not None:
81+
print('handle')
82+
print(handler)
83+
await handler(http, stream, parameters)
84+
else:
85+
# maybe raise an error?
86+
raise Exception(path, 'is not a valid request path')

hyper2web/server.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
Requires Python 3.5+.
1010
"""
1111

12-
from curio import spawn
12+
from curio import spawn, socket
1313

1414
import h2.config
1515
import h2.connection
File renamed without changes.

test/test_router.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import unittest
2+
3+
from curio import Kernel
4+
5+
from hyper2web.http import Stream
6+
from hyper2web.router import Router
7+
8+
9+
k = Kernel()
10+
11+
class TestRouter(unittest.TestCase):
12+
13+
def test_raise_error_on_non_existing_route(self):
14+
"""If a route doesn't exist, should raise error"""
15+
router = Router(None, None)
16+
stream = Stream(1, {':path': 'x', ':method': 'GET'})
17+
18+
# should raise a more specific error in the future
19+
with self.assertRaises(Exception):
20+
coroutine = router.handle_route(None, stream)
21+
k.run(coroutine)
22+
23+
def test_get_existing_route(self):
24+
router = Router(None, None)
25+
stream = Stream(1, {':path': 'x', ':method': 'GET'})
26+
27+
async def f(http, stream):
28+
assert http is None
29+
assert stream.headers[':path'] == 'x'
30+
router.get('x', f)
31+
coroutine = router.handle_route(None, stream)
32+
k.run(coroutine)
33+
34+
def test_post_existing_route(self):
35+
router = Router(None, None)
36+
stream = Stream(1, {':path': 'x', ':method': 'POST'})
37+
38+
async def f(http, stream):
39+
assert http is None
40+
assert stream.headers[':path'] == 'x'
41+
router.post('x', f)
42+
coroutine = router.handle_route(None, stream)
43+
k.run(coroutine)
44+
45+
def test_match(self):
46+
# match true
47+
matched, parameters = Router._match('user/{userId}/name/{name}', 'user/123/name/John')
48+
self.assertTrue(matched)
49+
self.assertEqual(parameters['userId'], '123')
50+
self.assertEqual(parameters['name'], 'John')
51+
52+
# match false
53+
matched, parameters = Router._match('user/{userId}/name/{name}', 'user/123/nam/John')
54+
self.assertFalse(matched)
55+
56+
def test_parameterized_route(self):
57+
router = Router(None, None)
58+
async def f(http, stream, parameters):
59+
self.assertIsNone(http)
60+
self.assertEqual(parameters['userId'], '123')
61+
self.assertEqual(parameters['name'], 'John')
62+
router.get('user/{userId}/name/{name}', f)
63+
c = router.handle_route(None, Stream(1, {':path': 'user/123/name/John', ':method': 'GET'}))
64+
k.run(c)
65+
66+
# will want to test with unicode

0 commit comments

Comments
 (0)