-
-
Notifications
You must be signed in to change notification settings - Fork 930
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #5 from encode/routing
Add Router, Path, PathPrefix
- Loading branch information
Showing
8 changed files
with
220 additions
and
16 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,16 +1,20 @@ | ||
from starlette.decorators import asgi_application | ||
from starlette.response import HTMLResponse, JSONResponse, Response, StreamingResponse | ||
from starlette.request import Request | ||
from starlette.routing import Path, PathPrefix, Router | ||
from starlette.testclient import TestClient | ||
|
||
|
||
__all__ = ( | ||
"asgi_application", | ||
"HTMLResponse", | ||
"JSONResponse", | ||
"Path", | ||
"PathPrefix", | ||
"Response", | ||
"Router", | ||
"StreamingResponse", | ||
"Request", | ||
"TestClient", | ||
) | ||
__version__ = "0.1.2" | ||
__version__ = "0.1.3" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
from starlette import Response | ||
from starlette.types import Scope, ASGIApp, ASGIInstance | ||
import re | ||
import typing | ||
|
||
|
||
class Route: | ||
def matches(self, scope: Scope) -> typing.Tuple[bool, Scope]: | ||
raise NotImplementedError() # pragma: no cover | ||
|
||
def __call__(self, scope: Scope) -> ASGIInstance: | ||
raise NotImplementedError() # pragma: no cover | ||
|
||
|
||
class Path(Route): | ||
def __init__( | ||
self, path: str, app: ASGIApp, methods: typing.Sequence[str] = () | ||
) -> None: | ||
self.path = path | ||
self.app = app | ||
self.methods = methods | ||
regex = "^" + path + "$" | ||
regex = re.sub("{([a-zA-Z_][a-zA-Z0-9_]*)}", r"(?P<\1>[^/]+)", regex) | ||
self.path_regex = re.compile(regex) | ||
|
||
def matches(self, scope: Scope) -> typing.Tuple[bool, Scope]: | ||
match = self.path_regex.match(scope["path"]) | ||
if match: | ||
kwargs = dict(scope.get("kwargs", {})) | ||
kwargs.update(match.groupdict()) | ||
child_scope = dict(scope) | ||
child_scope["kwargs"] = kwargs | ||
return True, child_scope | ||
return False, {} | ||
|
||
def __call__(self, scope: Scope) -> ASGIInstance: | ||
if self.methods and scope["method"] not in self.methods: | ||
return Response("Method not allowed", 406, media_type="text/plain") | ||
return self.app(scope) | ||
|
||
|
||
class PathPrefix(Route): | ||
def __init__( | ||
self, path: str, app: ASGIApp, methods: typing.Sequence[str] = () | ||
) -> None: | ||
self.path = path | ||
self.app = app | ||
self.methods = methods | ||
regex = "^" + path | ||
regex = re.sub("{([a-zA-Z_][a-zA-Z0-9_]*)}", r"(?P<\1>[^/]*)", regex) | ||
self.path_regex = re.compile(regex) | ||
|
||
def matches(self, scope: Scope) -> typing.Tuple[bool, Scope]: | ||
match = self.path_regex.match(scope["path"]) | ||
if match: | ||
kwargs = dict(scope.get("kwargs", {})) | ||
kwargs.update(match.groupdict()) | ||
child_scope = dict(scope) | ||
child_scope["kwargs"] = kwargs | ||
child_scope["root_path"] = scope.get("root_path", "") + match.string | ||
child_scope["path"] = scope["path"][match.span()[1] :] | ||
return True, child_scope | ||
return False, {} | ||
|
||
def __call__(self, scope: Scope) -> ASGIInstance: | ||
if self.methods and scope["method"] not in self.methods: | ||
return Response("Method not allowed", 406, media_type="text/plain") | ||
return self.app(scope) | ||
|
||
|
||
class Router: | ||
def __init__(self, routes: typing.List[Route], default: ASGIApp = None) -> None: | ||
self.routes = routes | ||
self.default = self.not_found if default is None else default | ||
|
||
def __call__(self, scope: Scope) -> ASGIInstance: | ||
for route in self.routes: | ||
matched, child_scope = route.matches(scope) | ||
if matched: | ||
return route(child_scope) | ||
return self.not_found(scope) | ||
|
||
def not_found(self, scope: Scope) -> ASGIInstance: | ||
return Response("Not found", 404, media_type="text/plain") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
import typing | ||
|
||
|
||
Scope = typing.Mapping[str, typing.Any] | ||
Message = typing.Mapping[str, typing.Any] | ||
|
||
Receive = typing.Callable[[], typing.Awaitable[Message]] | ||
Send = typing.Callable[[Message], typing.Awaitable[None]] | ||
|
||
ASGIInstance = typing.Callable[[Receive, Send], typing.Awaitable[None]] | ||
ASGIApp = typing.Callable[[Scope], ASGIInstance] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
from starlette import Response, Path, PathPrefix, Router, TestClient | ||
|
||
|
||
def homepage(scope): | ||
return Response("Hello, world", media_type="text/plain") | ||
|
||
|
||
def users(scope): | ||
return Response("All users", media_type="text/plain") | ||
|
||
|
||
def user(scope): | ||
content = "User " + scope["kwargs"]["username"] | ||
return Response(content, media_type="text/plain") | ||
|
||
|
||
def staticfiles(scope): | ||
return Response("xxxxx", media_type="image/png") | ||
|
||
|
||
app = Router( | ||
[ | ||
Path("/", app=homepage, methods=["GET"]), | ||
PathPrefix( | ||
"/users", app=Router([Path("", app=users), Path("/{username}", app=user)]) | ||
), | ||
PathPrefix("/static", app=staticfiles, methods=["GET"]), | ||
] | ||
) | ||
|
||
|
||
def test_router(): | ||
client = TestClient(app) | ||
|
||
response = client.get("/") | ||
assert response.status_code == 200 | ||
assert response.text == "Hello, world" | ||
|
||
response = client.post("/") | ||
assert response.status_code == 406 | ||
assert response.text == "Method not allowed" | ||
|
||
response = client.get("/foo") | ||
assert response.status_code == 404 | ||
assert response.text == "Not found" | ||
|
||
response = client.get("/users") | ||
assert response.status_code == 200 | ||
assert response.text == "All users" | ||
|
||
response = client.get("/users/tomchristie") | ||
assert response.status_code == 200 | ||
assert response.text == "User tomchristie" | ||
|
||
response = client.get("/static/123") | ||
assert response.status_code == 200 | ||
assert response.text == "xxxxx" | ||
|
||
response = client.post("/static/123") | ||
assert response.status_code == 406 | ||
assert response.text == "Method not allowed" |