mirror of
https://github.com/kennethreitz/responder.git
synced 2026-06-05 23:00:17 +00:00
@@ -1,6 +1,3 @@
|
||||
# v0.2.0
|
||||
- WebSocket support.
|
||||
|
||||
# v0.1.6
|
||||
- 500 support.
|
||||
|
||||
|
||||
Generated
+10
-10
@@ -215,9 +215,9 @@
|
||||
},
|
||||
"starlette": {
|
||||
"hashes": [
|
||||
"sha256:eac0f6cab6b48846a0c1af16615430ae0e7a95f669ee0841a7e2f242d51d8935"
|
||||
"sha256:ce5c684fad4edb2967cd491518cd3c2724e420508202c2d48f519ea68dcec9d6"
|
||||
],
|
||||
"version": "==0.5.5"
|
||||
"version": "==0.5.4"
|
||||
},
|
||||
"urllib3": {
|
||||
"hashes": [
|
||||
@@ -228,9 +228,9 @@
|
||||
},
|
||||
"uvicorn": {
|
||||
"hashes": [
|
||||
"sha256:e2b742fdaa0b52f4aac92fd2c078e7f1f17d11322bb3efb09d341d5c6998b4b5"
|
||||
"sha256:7c4550c7e6f7c8727fa5ccd5200baf62c9e055895e058933ee88f5d0c246ca0c"
|
||||
],
|
||||
"version": "==0.3.16"
|
||||
"version": "==0.3.14"
|
||||
},
|
||||
"websockets": {
|
||||
"hashes": [
|
||||
@@ -591,11 +591,11 @@
|
||||
},
|
||||
"pytest": {
|
||||
"hashes": [
|
||||
"sha256:212be78a6fa5352c392738a49b18f74ae9aeec1040f47c81cadbfd8d1233c310",
|
||||
"sha256:6f6c1efc8d0ccc21f8f6c34d8330baca883cf109b66b3df954b0a117e5528fb4"
|
||||
"sha256:10e59f84267370ab20cec9305bafe7505ba4d6b93ecbf66a1cce86193ed511d5",
|
||||
"sha256:8c827e7d4816dfe13e9329c8226aef8e6e75d65b939bc74fda894143b6d1df59"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==3.9.2"
|
||||
"version": "==3.9.1"
|
||||
},
|
||||
"pytest-cov": {
|
||||
"hashes": [
|
||||
@@ -671,10 +671,10 @@
|
||||
},
|
||||
"tqdm": {
|
||||
"hashes": [
|
||||
"sha256:3c4d4a5a41ef162dd61f1edb86b0e1c7859054ab656b2e7c7b77e7fbf6d9f392",
|
||||
"sha256:5b4d5549984503050883bc126280b386f5f4ca87e6c023c5d015655ad75bdebb"
|
||||
"sha256:a0be569511161220ff709a5b60d0890d47921f746f1c737a11d965e1b29e7b2e",
|
||||
"sha256:e293e6d7a7f41a529a27f8d6624ab11544ccbfe82a205af6fad102545099fc21"
|
||||
],
|
||||
"version": "==4.28.1"
|
||||
"version": "==4.27.0"
|
||||
},
|
||||
"twine": {
|
||||
"hashes": [
|
||||
|
||||
@@ -50,7 +50,6 @@ Features
|
||||
- A pleasant API, with a single import statement.
|
||||
- Class-based views without inheritence.
|
||||
- ASGI framework, the future of Python web services.
|
||||
- WebSocket support!
|
||||
- The ability to mount any ASGI / WSGI app at a subroute.
|
||||
- *f-string syntax* route declaration.
|
||||
- Mutable response object, passed into each view. No need to return anything.
|
||||
|
||||
+2
-17
@@ -9,7 +9,7 @@ Class-based views (and setting some headers and stuff)::
|
||||
|
||||
@api.route("/{greeting}")
|
||||
class GreetingResource:
|
||||
def on_request(self, req, resp, *, greeting): # or on_get...
|
||||
def on_request(req, resp, *, greeting): # or on_get...
|
||||
resp.text = f"{greeting}, world!"
|
||||
resp.headers.update({'X-Life': '42'})
|
||||
resp.status_code = api.status_codes.HTTP_416
|
||||
@@ -147,11 +147,7 @@ If you have a single-page webapp, you can tell Responder to serve up your ``stat
|
||||
|
||||
api.add_route("/", static=True)
|
||||
|
||||
This will make ``index.html`` the default response to all undefined routes. Responder's CLI comes with a ``build`` command that will call ``npm run build`` for you::
|
||||
|
||||
responder build
|
||||
|
||||
For an example of how to seamlessly integrate a React single page app with Responder check out `this project <https://github.com/metakermit/responder-react>`_.
|
||||
This will make ``index.html`` the default response to all undefined routes.
|
||||
|
||||
Reading / Writing Cookies
|
||||
-------------------------
|
||||
@@ -180,17 +176,6 @@ You can easily read a Request's session data, that can be trusted to have origin
|
||||
|
||||
**Note**: if you are using this in production, you should pass the ``secret_key`` argument to ``API(...)``.
|
||||
|
||||
WebSocket Support
|
||||
-----------------
|
||||
|
||||
Responder supports WebSockets::
|
||||
|
||||
@api.ws_route('/ws')
|
||||
async def hello(ws):
|
||||
await ws.accept()
|
||||
await ws.send_text("Hello via websocket!")
|
||||
await ws.close()
|
||||
|
||||
|
||||
HSTS (Redirect to HTTPS)
|
||||
------------------------
|
||||
|
||||
@@ -1 +1 @@
|
||||
__version__ = "0.2.0"
|
||||
__version__ = "0.1.6"
|
||||
|
||||
+62
-80
@@ -9,6 +9,8 @@ import asyncio
|
||||
import jinja2
|
||||
import itsdangerous
|
||||
from graphql_server import encode_execution_results, json_encode, default_format_error
|
||||
from starlette.websockets import WebSocket
|
||||
from starlette.debug import DebugMiddleware
|
||||
from starlette.routing import Router
|
||||
from starlette.staticfiles import StaticFiles
|
||||
from starlette.testclient import TestClient
|
||||
@@ -25,7 +27,6 @@ from .routes import Route
|
||||
from .formats import get_formats
|
||||
from .background import BackgroundQueue
|
||||
from .templates import GRAPHIQL
|
||||
from .models import WebSocket
|
||||
|
||||
# TODO: consider moving status codes here
|
||||
class API:
|
||||
@@ -42,6 +43,7 @@ class API:
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
debug=False,
|
||||
title=None,
|
||||
version=None,
|
||||
openapi=None,
|
||||
@@ -87,6 +89,8 @@ class API:
|
||||
self.default_endpoint = None
|
||||
self.app = self.dispatch
|
||||
self.add_middleware(GZipMiddleware)
|
||||
if debug:
|
||||
self.add_middleware(DebugMiddleware)
|
||||
if self.hsts_enabled:
|
||||
self.add_middleware(HTTPSRedirectMiddleware)
|
||||
|
||||
@@ -100,8 +104,6 @@ class API:
|
||||
)
|
||||
self.jinja_values_base = {"api": self} # Give reference to self.
|
||||
|
||||
self.requests = self.session()
|
||||
|
||||
@property
|
||||
def _apispec(self):
|
||||
spec = APISpec(
|
||||
@@ -151,22 +153,15 @@ class API:
|
||||
# Call the main dispatcher.
|
||||
async def asgi(receive, send):
|
||||
nonlocal scope, self
|
||||
if scope["type"] == "websocket":
|
||||
|
||||
ws = WebSocket(scope, receive, send)
|
||||
await self._dispatch_ws(ws)
|
||||
else:
|
||||
req = models.Request(scope, receive=receive, api=self)
|
||||
resp = await self._dispatch_request(req)
|
||||
await resp(receive, send)
|
||||
req = models.Request(scope, receive=receive, api=self)
|
||||
resp = await self._dispatch_request(
|
||||
req, scope=scope, send=send, receive=receive
|
||||
)
|
||||
await resp(receive, send)
|
||||
|
||||
return asgi
|
||||
|
||||
async def _dispatch_ws(self, ws):
|
||||
route = self.path_matches_route(ws.url.path, protocol="ws")
|
||||
route = self.routes.get(route)
|
||||
await self._dispatch(route, ws=ws)
|
||||
|
||||
def add_schema(self, name, schema, check_existing=True):
|
||||
"""Adds a mashmallow schema to the API specification."""
|
||||
if check_existing:
|
||||
@@ -182,7 +177,7 @@ class API:
|
||||
from marshmallow import Schema, fields
|
||||
|
||||
@api.schema("Pet")
|
||||
class PetScrhema(Schema):
|
||||
class PetSchema(Schema):
|
||||
name = fields.Str()
|
||||
|
||||
"""
|
||||
@@ -193,17 +188,17 @@ class API:
|
||||
|
||||
return decorator
|
||||
|
||||
# TODO: Remove protocol
|
||||
def path_matches_route(self, path, protocol="http"):
|
||||
def path_matches_route(self, path):
|
||||
"""Given a path portion of a URL, tests that it matches against any registered route.
|
||||
|
||||
:param path: The path portion of a URL, to test all known routes against.
|
||||
"""
|
||||
for (route, route_object) in self.routes.items():
|
||||
if route_object.does_match(path, protocol):
|
||||
if route_object.does_match(path):
|
||||
return route
|
||||
|
||||
def _prepare_cookies(self, resp):
|
||||
# print(resp.cookies)
|
||||
if resp.cookies:
|
||||
header = " ".join([f"{k}={v}" for k, v in resp.cookies.items()])
|
||||
resp.headers["Set-Cookie"] = header
|
||||
@@ -222,7 +217,7 @@ class API:
|
||||
def no_response(req, resp, **params):
|
||||
pass
|
||||
|
||||
async def _dispatch_request(self, req):
|
||||
async def _dispatch_request(self, req, **options):
|
||||
# Set formats on Request object.
|
||||
req.formats = self.formats
|
||||
|
||||
@@ -231,43 +226,31 @@ class API:
|
||||
route = self.routes.get(route)
|
||||
|
||||
# Create the response object.
|
||||
resp = models.Response(req=req, formats=self.formats)
|
||||
self.default_response(req, resp)
|
||||
|
||||
await self._dispatch(route, req=req, resp=resp)
|
||||
|
||||
self._prepare_session(resp)
|
||||
self._prepare_cookies(resp)
|
||||
|
||||
return resp
|
||||
|
||||
async def _dispatch(self, route, **kwargs):
|
||||
|
||||
cont = False
|
||||
|
||||
if route:
|
||||
if "req" in kwargs:
|
||||
params = route.incoming_matches(kwargs["req"].url.path)
|
||||
elif "ws" in kwargs:
|
||||
params = route.incoming_matches(kwargs["ws"].url.path)
|
||||
if not route.uses_websocket:
|
||||
resp = models.Response(req=req, formats=self.formats)
|
||||
else:
|
||||
params = {}
|
||||
resp = WebSocket(**options)
|
||||
|
||||
params = route.incoming_matches(req.url.path)
|
||||
|
||||
if route.is_graphql:
|
||||
await self.graphql_response(schema=route.endpoint, **kwargs)
|
||||
await self.graphql_response(req, resp, schema=route.endpoint)
|
||||
|
||||
elif route.is_function:
|
||||
try:
|
||||
try:
|
||||
# Run the view.
|
||||
r = route.endpoint(**kwargs, **params)
|
||||
r = route.endpoint(req, resp, **params)
|
||||
# If it's async, await it.
|
||||
if hasattr(r, "cr_running"):
|
||||
await r
|
||||
except TypeError as e:
|
||||
cont = True
|
||||
except Exception:
|
||||
self.default_response(error=True, **kwargs)
|
||||
self.default_response(req, resp, error=True)
|
||||
|
||||
if route.is_class_based or cont:
|
||||
try:
|
||||
@@ -279,38 +262,40 @@ class API:
|
||||
try:
|
||||
# Run the view.
|
||||
r = getattr(view, "on_request", self.no_response)(
|
||||
**kwargs, **params
|
||||
)
|
||||
# If it's async, await it.
|
||||
if hasattr(r, "send"):
|
||||
await r
|
||||
except Exception:
|
||||
self.default_response(error=True, **kwargs)
|
||||
|
||||
# Then on_get.
|
||||
if "req" in kwargs:
|
||||
method = kwargs["req"].method
|
||||
elif "ws" in kwargs:
|
||||
method = kwargs["ws"].method
|
||||
else:
|
||||
method = "get"
|
||||
|
||||
# Run on_request first.
|
||||
try:
|
||||
# Run the view.
|
||||
r = getattr(view, f"on_{method}", self.no_response)(
|
||||
**kwargs, **params
|
||||
req, resp, **params
|
||||
)
|
||||
# If it's async, await it.
|
||||
if hasattr(r, "send"):
|
||||
await r
|
||||
except Exception as e:
|
||||
self.default_response(error=True, **kwargs)
|
||||
self.default_response(req, resp, error=True)
|
||||
|
||||
# Then on_get.
|
||||
method = req.method
|
||||
|
||||
# Run on_request first.
|
||||
try:
|
||||
# Run the view.
|
||||
r = getattr(view, f"on_{method}", self.no_response)(
|
||||
req, resp, **params
|
||||
)
|
||||
# If it's async, await it.
|
||||
if hasattr(r, "send"):
|
||||
await r
|
||||
except Exception as e:
|
||||
|
||||
self.default_response(req, resp, error=True)
|
||||
|
||||
else:
|
||||
self.default_response(notfound=True, **kwargs)
|
||||
resp = models.Response(req=req, formats=self.formats)
|
||||
self.default_response(req, resp, notfound=True)
|
||||
|
||||
return kwargs
|
||||
self.default_response(req, resp)
|
||||
|
||||
self._prepare_session(resp)
|
||||
self._prepare_cookies(resp)
|
||||
|
||||
return resp
|
||||
|
||||
def add_route(
|
||||
self,
|
||||
@@ -318,28 +303,20 @@ class API:
|
||||
endpoint=None,
|
||||
*,
|
||||
default=False,
|
||||
websocket=False,
|
||||
static=False,
|
||||
check_existing=True,
|
||||
websocket=False,
|
||||
):
|
||||
"""Add a route to the API.
|
||||
|
||||
:param route: A string representation of the route.
|
||||
:param endpoint: The endpoint for the route -- can be a callable, a class, or graphene schema (GraphQL).
|
||||
:param default: If ``True``, all unknown requests will route to this view.
|
||||
:param websocket: If ``True``, Requests to route will be treated as websockets.
|
||||
:param static: If ``True``, and no endpoint was passed, render "static/index.html", and it will become a default route.
|
||||
:param check_existing: If ``True``, an AssertionError will be raised, if the route is already defined.
|
||||
"""
|
||||
if websocket:
|
||||
protocol = "ws"
|
||||
else:
|
||||
protocol = "http"
|
||||
|
||||
if check_existing:
|
||||
assert not (
|
||||
route in self.routes and self.routes[route].protocol == protocol
|
||||
)
|
||||
assert route not in self.routes
|
||||
|
||||
if not endpoint and static:
|
||||
endpoint = self.static_response
|
||||
@@ -354,7 +331,7 @@ class API:
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
self.routes[route] = Route(route, endpoint, protocol)
|
||||
self.routes[route] = Route(route, endpoint, websocket=websocket)
|
||||
# TODO: A better datastructer or sort it once the app is loaded
|
||||
self.routes = dict(
|
||||
sorted(self.routes.items(), key=lambda item: item[1]._weight())
|
||||
@@ -487,6 +464,13 @@ class API:
|
||||
self._session = TestClient(self)
|
||||
return self._session
|
||||
|
||||
def _route_for(self, endpoint):
|
||||
for (route, route_object) in self.routes.items():
|
||||
if route_object.endpoint == endpoint:
|
||||
return route_object
|
||||
elif route_object.endpoint_name == endpoint:
|
||||
return route_object
|
||||
|
||||
def url_for(self, endpoint, testing=False, **params):
|
||||
# TODO: Absolute_url
|
||||
"""Given an endpoint, returns a rendered URL for its route.
|
||||
@@ -494,11 +478,9 @@ class API:
|
||||
:param view: The route endpoint you're searching for.
|
||||
:param params: Data to pass into the URL generator (for parameterized URLs).
|
||||
"""
|
||||
for (route, route_object) in self.routes.items():
|
||||
if route_object.endpoint == endpoint:
|
||||
return route_object.url(testing=testing, **params)
|
||||
elif route_object.endpoint_name == endpoint:
|
||||
return route_object.url(testing=testing, **params)
|
||||
route_object = self._route_for(endpoint)
|
||||
if route_object:
|
||||
return route_object.url(testing=testing, **params)
|
||||
raise ValueError
|
||||
|
||||
def static_url(self, asset):
|
||||
|
||||
@@ -13,7 +13,6 @@ from requests.cookies import RequestsCookieJar
|
||||
from starlette.datastructures import MutableHeaders
|
||||
from starlette.requests import Request as StarletteRequest
|
||||
from starlette.responses import Response as StarletteResponse
|
||||
from starlette.websockets import WebSocket as StarletteWebSocket
|
||||
|
||||
from urllib.parse import parse_qs
|
||||
|
||||
@@ -288,65 +287,3 @@ class Response:
|
||||
body, status_code=self.status_code, headers=headers
|
||||
)
|
||||
await response(receive, send)
|
||||
|
||||
|
||||
class WebSocket:
|
||||
__slots__ = ("_starlette", "_headers")
|
||||
|
||||
def __init__(self, scope, receive, send):
|
||||
self._starlette = StarletteWebSocket(scope, receive, send)
|
||||
|
||||
headers = CaseInsensitiveDict()
|
||||
for header, value in self._starlette.headers.items():
|
||||
headers[header] = value
|
||||
|
||||
self._headers = headers
|
||||
|
||||
@property
|
||||
def url(self):
|
||||
return rfc3986.urlparse(str(self._starlette.url))
|
||||
|
||||
@property
|
||||
def params(self):
|
||||
"""A dictionary of the parsed query parameters used for the Request."""
|
||||
try:
|
||||
return QueryDict(self.url.query)
|
||||
except AttributeError:
|
||||
return QueryDict({})
|
||||
|
||||
async def accept(self):
|
||||
return await self._starlette.accept()
|
||||
|
||||
async def receive(self):
|
||||
return await self._starlette.receive()
|
||||
|
||||
async def content(self):
|
||||
"""Receive bytes."""
|
||||
return await self._starlette.receive_bytes()
|
||||
|
||||
async def text(self):
|
||||
"""Receive text."""
|
||||
return await self._starlette.receive_text()
|
||||
|
||||
# async def json(self):
|
||||
# """Receive json"""
|
||||
# return await self._starlette.receive_json()
|
||||
|
||||
# async def media(self):
|
||||
# """Receive json"""
|
||||
# return (await self.receive_json())
|
||||
|
||||
async def send(self):
|
||||
await self._starlette.send()
|
||||
|
||||
async def send_text(self, data):
|
||||
await self._starlette.send_text(data)
|
||||
|
||||
# async def send_json(self, data):
|
||||
# await self._starlette.send_json(data)
|
||||
|
||||
# async def send_media(self, data):
|
||||
# await self.send_json(data)
|
||||
|
||||
async def close(self, code=1000):
|
||||
await self._starlette.close(code)
|
||||
|
||||
Reference in New Issue
Block a user