From aea01fd893423939c4d12c873ae77116a60fe30b Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Tue, 23 Oct 2018 08:00:56 -0400 Subject: [PATCH] Revert "idk what's happening" This reverts commit e34cb539d25a0a747f3764e90e2cedbe08b41708. --- CHANGELOG.md | 3 - Pipfile.lock | 20 +++--- docs/source/index.rst | 1 - docs/source/tour.rst | 19 +----- responder/__version__.py | 2 +- responder/api.py | 142 +++++++++++++++++---------------------- responder/models.py | 63 ----------------- 7 files changed, 75 insertions(+), 175 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fff4991..3232e8c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,3 @@ -# v0.2.0 -- WebSocket support. - # v0.1.6 - 500 support. diff --git a/Pipfile.lock b/Pipfile.lock index e6fd3b1..1573d49 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -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": [ diff --git a/docs/source/index.rst b/docs/source/index.rst index 383b123..2ebba01 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -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. diff --git a/docs/source/tour.rst b/docs/source/tour.rst index 089908a..5791723 100644 --- a/docs/source/tour.rst +++ b/docs/source/tour.rst @@ -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 `_. +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) ------------------------ diff --git a/responder/__version__.py b/responder/__version__.py index d3ec452..0a8da88 100644 --- a/responder/__version__.py +++ b/responder/__version__.py @@ -1 +1 @@ -__version__ = "0.2.0" +__version__ = "0.1.6" diff --git a/responder/api.py b/responder/api.py index 1671a7e..c3fdd6b 100644 --- a/responder/api.py +++ b/responder/api.py @@ -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): diff --git a/responder/models.py b/responder/models.py index a8e6ff0..ebce351 100644 --- a/responder/models.py +++ b/responder/models.py @@ -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)