Compare commits

...

38 Commits

Author SHA1 Message Date
kennethreitz 5212de79d3 v0.2.2 2018-10-23 08:05:07 -04:00
kennethreitz b61c02e5df Merge pull request #132 from vuonghv/show-exception-background-task
Show traceback info when background tasks raise exceptions
2018-10-23 08:02:59 -04:00
kennethreitz f982954e8f versions 2018-10-23 08:03:28 -04:00
kennethreitz 3ba20e69ba requests session 2018-10-23 08:02:30 -04:00
kennethreitz aea01fd893 Revert "idk what's happening"
This reverts commit e34cb539d2.
2018-10-23 08:00:56 -04:00
kennethreitz 950be14eca Revert "Merge branch 'master' of github.com:kennethreitz/responder"
This reverts commit 446deffc17, reversing
changes made to e0863115ee.
2018-10-23 08:00:30 -04:00
kennethreitz 446deffc17 Merge branch 'master' of github.com:kennethreitz/responder 2018-10-23 07:59:07 -04:00
kennethreitz e0863115ee use api.requests 2018-10-23 07:59:00 -04:00
kennethreitz e34cb539d2 idk what's happening 2018-10-23 07:58:48 -04:00
kennethreitz d8ade8638a merge 2018-10-23 07:57:23 -04:00
Vuong Hoang 3067080474 Show traceback when background tasks raise exceptions 2018-10-23 18:23:22 +07:00
kennethreitz 886cc0f214 Merge pull request #131 from daikeren/master
Fix Route.is_function
2018-10-23 07:01:40 -04:00
Andy Dai 071d34b016 Fix Route.is_function 2018-10-23 17:07:42 +08:00
kennethreitz a1564ca003 Merge pull request #123 from taoufik07/patch-7
Quick fix
2018-10-22 17:25:15 -04:00
Taoufik 60f0e765c2 Quick fix 2018-10-22 22:14:18 +01:00
kennethreitz 3f0ecea4bf Merge pull request #120 from Pentusha/master
Depend on marshmallow>=3.0.0b7
2018-10-22 17:06:00 -04:00
kennethreitz 2c9e6572c5 Update tour.rst 2018-10-22 17:05:50 -04:00
Ivan Larin 371a83f20f Depend on marshmallow>=3.0.0b7 kennethreitz/responder#119 2018-10-22 19:46:55 +03:00
kennethreitz b8cff1655a websockets 2018-10-22 10:18:37 -04:00
kennethreitz 232856ca3a Merge branch 'master' of github.com:kennethreitz/responder 2018-10-22 10:07:38 -04:00
kennethreitz 3f168ac6fd slots 2018-10-22 10:06:55 -04:00
kennethreitz c59cb1d0d3 websocket 2018-10-22 10:06:03 -04:00
kennethreitz ec13df75d0 kinda test websocket support 2018-10-22 10:05:20 -04:00
kennethreitz 6fc02964ba cleanup 2018-10-22 09:59:38 -04:00
kennethreitz ed79e45680 Merge pull request #116 from tkamenoko/patch-1
doc: fix Class-based views
2018-10-22 09:30:14 -04:00
kennethreitz 1be983bf89 cleanup 2018-10-22 09:28:14 -04:00
T.Kameyama b09d6a9d04 doc: fix Class-based views
In Class-based views, each method needs `self` as 1st argument.
2018-10-22 14:37:55 +09:00
taoufik07 db143d845d cleanup 2018-10-21 18:17:56 +01:00
taoufik07 2e23501f9d Fix check_existing 2018-10-21 18:15:13 +01:00
taoufik07 bd6addcd3a Add websocket support 2018-10-21 18:00:25 +01:00
taoufik07 631e1fb604 Add WebSocket 2018-10-21 17:36:21 +01:00
kennethreitz 30ee6726a8 Merge pull request #113 from metakermit/extend-static-docs
Extend static=True docs
2018-10-21 06:25:56 -04:00
kennethreitz 1c397db9d8 cleanup 2018-10-21 06:23:02 -04:00
kennethreitz cc23ca80f4 Merge pull request #112 from Nitish18/feat/server_debug_mode
feat: added debug mode for uvicorn server
2018-10-21 06:20:16 -04:00
Dražen Lučanin 449379a0ed extend static=True docs 2018-10-21 11:58:54 +02:00
Nitish Chauhan b3208b1c5b feat: added debug mode for uvicorn server 2018-10-21 15:20:08 +05:30
kennethreitz 4df60b55a6 Merge pull request #110 from sheb/patch-2
fix an AttributeError when route does not exist
2018-10-20 13:54:58 -07:00
Sébastien Geffroy 379553a1a5 fix an AttributeError when route does not exist 2018-10-20 21:55:43 +02:00
8 changed files with 143 additions and 76 deletions
+9
View File
@@ -1,3 +1,12 @@
# v0.2.2
- Show traceback info when background tasks raise exceptions.
# v0.2.1
- api.requests.
# v0.2.0
- WebSocket support.
# v0.1.6
- 500 support.
+1 -1
View File
@@ -1 +1 @@
__version__ = "0.1.6"
__version__ = "0.2.2"
+43 -13
View File
@@ -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
@@ -41,6 +43,7 @@ class API:
def __init__(
self,
*,
debug=False,
title=None,
version=None,
openapi=None,
@@ -86,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)
@@ -98,6 +103,9 @@ class API:
autoescape=jinja2.select_autoescape(["html", "xml"] if auto_escape else []),
)
self.jinja_values_base = {"api": self} # Give reference to self.
self.requests = (
self.session()
) #: A Requests session that is connected to the ASGI app.
@property
def _apispec(self):
@@ -150,7 +158,9 @@ class API:
nonlocal scope, self
req = models.Request(scope, receive=receive, api=self)
resp = await self._dispatch_request(req)
resp = await self._dispatch_request(
req, scope=scope, send=send, receive=receive
)
await resp(receive, send)
return asgi
@@ -210,20 +220,25 @@ 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
# Get the route.
route = self.path_matches_route(req.url.path)
route = self.routes.get(route)
params = route.incoming_matches(req.url.path)
# Create the response object.
resp = models.Response(req=req, formats=self.formats)
cont = False
if route:
if not route.uses_websocket:
resp = models.Response(req=req, formats=self.formats)
else:
resp = WebSocket(**options)
params = route.incoming_matches(req.url.path)
if route.is_graphql:
await self.graphql_response(req, resp, schema=route.endpoint)
@@ -275,6 +290,7 @@ class API:
self.default_response(req, resp, error=True)
else:
resp = models.Response(req=req, formats=self.formats)
self.default_response(req, resp, notfound=True)
self.default_response(req, resp)
@@ -285,7 +301,14 @@ class API:
return resp
def add_route(
self, route, endpoint=None, *, default=False, static=False, check_existing=True
self,
route,
endpoint=None,
*,
default=False,
static=False,
check_existing=True,
websocket=False,
):
"""Add a route to the API.
@@ -311,7 +334,7 @@ class API:
except AttributeError:
pass
self.routes[route] = Route(route, endpoint)
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())
@@ -444,6 +467,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.
@@ -451,11 +481,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):
@@ -490,15 +518,17 @@ class API:
template = self.jinja_env.from_string(s_)
return template.render(**values)
def run(self, address=None, port=None, **options):
def run(self, address=None, port=None, debug=False, **options):
"""Runs the application with uvicorn. If the ``PORT`` environment
variable is set, requests will be served on that port automatically to all
known hosts.
:param address: The address to bind to.
:param port: The port to bind to. If none is provided, one will be selected at random.
:param debug: Run uvicorn server in debug mode.
:param options: Additional keyword arguments to send to ``uvicorn.run()``.
"""
if "PORT" in os.environ:
if address is None:
address = "0.0.0.0"
@@ -509,4 +539,4 @@ class API:
if port is None:
port = 5042
uvicorn.run(self, host=address, port=port, **options)
uvicorn.run(self, host=address, port=port, debug=debug, **options)
+8
View File
@@ -1,3 +1,4 @@
import traceback
import multiprocessing
import concurrent.futures
@@ -20,8 +21,15 @@ class BackgroundQueue:
return f
def task(self, f):
def on_future_done(fs):
try:
fs.result()
except:
traceback.print_exc()
def do_task(*args, **kwargs):
result = self.run(f, *args, **kwargs)
result.add_done_callback(on_future_done)
return result
return do_task
+1 -1
View File
@@ -34,7 +34,7 @@ async def format_files(r, encode=False):
if encode:
pass
else:
decoded = decoder.MultipartDecoder(await r.content, r.headers["Content-Type"])
decoded = decoder.MultipartDecoder(await r.content, r.mimetype)
dump = {}
for part in decoded.parts:
header = part.headers[b"Content-Disposition"].decode("utf-8")
+2 -1
View File
@@ -15,9 +15,10 @@ def memoize(f):
class Route:
_param_pattern = re.compile(r"{([^{}]*)}")
def __init__(self, route, endpoint):
def __init__(self, route, endpoint, websocket=False):
self.route = route
self.endpoint = endpoint
self.uses_websocket = websocket
self._memo = {}
def __repr__(self):
+1 -1
View File
@@ -21,7 +21,7 @@ def api():
@pytest.fixture
def session(api):
return api.session()
return api.requests
@pytest.fixture
+78 -59
View File
@@ -73,16 +73,17 @@ def test_class_based_view_parameters(api):
def test_requests_session(api):
assert api.session()
assert api.requests
def test_requests_session_works(api, session, url):
def test_requests_session_works(api, url):
TEXT = "spiral out"
@api.route("/")
def hello(req, resp):
resp.text = TEXT
assert session.get(url("/")).text == TEXT
assert api.requests.get(url("/")).text == TEXT
def test_status_code(api):
@@ -91,7 +92,7 @@ def test_status_code(api):
resp.text = "keep going"
resp.status_code = responder.status_codes.HTTP_416
assert api.session().get("http://;/").status_code == responder.status_codes.HTTP_416
assert api.requests.get("http://;/").status_code == responder.status_codes.HTTP_416
def test_json_media(api):
@@ -101,7 +102,7 @@ def test_json_media(api):
def media(req, resp):
resp.media = dump
r = api.session().get("http://;/")
r = api.requests.get("http://;/")
assert "json" in r.headers["Content-Type"]
assert r.json() == dump
@@ -114,7 +115,7 @@ def test_yaml_media(api):
def media(req, resp):
resp.media = dump
r = api.session().get("http://;/", headers={"Accept": "yaml"})
r = api.requests.get("http://;/", headers={"Accept": "yaml"})
assert "yaml" in r.headers["Content-Type"]
assert yaml.load(r.content) == dump
@@ -123,38 +124,38 @@ def test_yaml_media(api):
def test_graphql_schema_query_querying(api, schema):
api.add_route("/", schema)
r = api.session().get("http://;/?q={ hello }", headers={"Accept": "json"})
r = api.requests.get("http://;/?q={ hello }", headers={"Accept": "json"})
assert r.json() == {"data": {"hello": "Hello stranger"}}
def test_argumented_routing(api, session):
def test_argumented_routing(api):
@api.route("/{name}")
def hello(req, resp, *, name):
resp.text = f"Hello, {name}."
r = session.get(api.url_for(hello, name="sean"))
r = api.requests.get(api.url_for(hello, name="sean"))
assert r.text == "Hello, sean."
def test_mote_argumented_routing(api, session):
def test_mote_argumented_routing(api):
@api.route("/{greeting}/{name}")
def hello(req, resp, *, greeting, name):
resp.text = f"{greeting}, {name}."
r = session.get(api.url_for(hello, greeting="hello", name="lyndsy"))
r = api.requests.get(api.url_for(hello, greeting="hello", name="lyndsy"))
assert r.text == "hello, lyndsy."
def test_request_and_get(api, session):
def test_request_and_get(api):
@api.route("/")
class ThingsResource:
def on_request(self, req, resp):
resp.headers.update({"DEATH": "666"})
def on_get(self, request, resp):
def on_get(self, req, resp):
resp.headers.update({"LIFE": "42"})
r = session.get(api.url_for(ThingsResource))
r = api.requests.get(api.url_for(ThingsResource))
assert "DEATH" in r.headers
assert "LIFE" in r.headers
@@ -165,58 +166,58 @@ def test_class_based_view_status_code(api):
def on_request(self, req, resp):
resp.status_code = responder.status_codes.HTTP_416
assert api.session().get("http://;/").status_code == responder.status_codes.HTTP_416
assert api.requests.get("http://;/").status_code == responder.status_codes.HTTP_416
def test_query_params(api, url, session):
def test_query_params(api, url):
@api.route("/")
def route(req, resp):
resp.media = {"params": req.params}
r = session.get(api.url_for(route), params={"q": "q"})
r = api.requests.get(api.url_for(route), params={"q": "q"})
assert r.json()["params"] == {"q": "q"}
r = session.get(url("/?q=1&q=2&q=3"))
r = api.requests.get(url("/?q=1&q=2&q=3"))
assert r.json()["params"] == {"q": "3"}
# Requires https://github.com/encode/starlette/pull/102
def test_form_data(api, session):
def test_form_data(api):
@api.route("/")
async def route(req, resp):
resp.media = {"form": await req.media("form")}
dump = {"q": "q"}
r = session.get(api.url_for(route), data=dump)
r = api.requests.get(api.url_for(route), data=dump)
assert r.json()["form"] == dump
def test_async_function(api, session):
def test_async_function(api):
content = "The Emerald Tablet of Hermes"
@api.route("/")
async def route(req, resp):
resp.text = content
r = session.get(api.url_for(route))
r = api.requests.get(api.url_for(route))
assert r.text == content
def test_media_parsing(api, session):
def test_media_parsing(api):
dump = {"hello": "sam"}
@api.route("/")
def route(req, resp):
resp.media = dump
r = session.get(api.url_for(route))
r = api.requests.get(api.url_for(route))
assert r.json() == dump
r = session.get(api.url_for(route), headers={"Accept": "application/x-yaml"})
r = api.requests.get(api.url_for(route), headers={"Accept": "application/x-yaml"})
assert r.text == "{hello: sam}\n"
def test_background(api, session):
def test_background(api):
@api.route("/")
def route(req, resp):
@api.background.task
@@ -228,11 +229,11 @@ def test_background(api, session):
task()
api.text = "ok"
r = session.get(api.url_for(route))
r = api.requests.get(api.url_for(route))
assert r.ok
def test_multiple_routes(api, session):
def test_multiple_routes(api):
@api.route("/1")
def route1(req, resp):
resp.text = "1"
@@ -241,45 +242,45 @@ def test_multiple_routes(api, session):
def route2(req, resp):
resp.text = "2"
r = session.get(api.url_for(route1))
r = api.requests.get(api.url_for(route1))
assert r.text == "1"
r = session.get(api.url_for(route2))
r = api.requests.get(api.url_for(route2))
assert r.text == "2"
def test_graphql_schema_json_query(api, schema):
api.add_route("/", schema)
r = api.session().post("http://;/", json={"query": "{ hello }"})
r = api.requests.post("http://;/", json={"query": "{ hello }"})
assert r.ok
def test_graphiql(api, schema):
api.add_route("/", schema)
r = api.session().get("http://;/", headers={"Accept": "text/html"})
r = api.requests.get("http://;/", headers={"Accept": "text/html"})
assert r.ok
assert "GraphiQL" in r.text
def test_json_uploads(api, session):
def test_json_uploads(api):
@api.route("/")
async def route(req, resp):
resp.media = await req.media()
dump = {"complicated": "times"}
r = session.post(api.url_for(route), json=dump)
r = api.requests.post(api.url_for(route), json=dump)
assert r.json() == dump
def test_yaml_uploads(api, session):
def test_yaml_uploads(api):
@api.route("/")
async def route(req, resp):
resp.media = await req.media()
dump = {"complicated": "times"}
r = session.post(
r = api.requests.post(
api.url_for(route),
data=yaml.dump(dump),
headers={"Content-Type": "application/x-yaml"},
@@ -287,35 +288,39 @@ def test_yaml_uploads(api, session):
assert r.json() == dump
def test_form_uploads(api, session):
def test_form_uploads(api):
@api.route("/")
async def route(req, resp):
resp.media = await req.media()
dump = {"complicated": "times"}
r = session.post(api.url_for(route), data=dump)
r = api.requests.post(api.url_for(route), data=dump)
assert r.json() == dump
def test_json_downloads(api, session):
def test_json_downloads(api):
dump = {"testing": "123"}
@api.route("/")
def route(req, resp):
resp.media = dump
r = session.get(api.url_for(route), headers={"Content-Type": "application/json"})
r = api.requests.get(
api.url_for(route), headers={"Content-Type": "application/json"}
)
assert r.json() == dump
def test_yaml_downloads(api, session):
def test_yaml_downloads(api):
dump = {"testing": "123"}
@api.route("/")
def route(req, resp):
resp.media = dump
r = session.get(api.url_for(route), headers={"Content-Type": "application/x-yaml"})
r = api.requests.get(
api.url_for(route), headers={"Content-Type": "application/x-yaml"}
)
assert yaml.safe_load(r.content) == dump
@@ -343,59 +348,59 @@ def test_schema_generation():
"""
resp.media = PetSchema().dump({"name": "little orange"})
r = api.session().get("http://;/schema.yml")
r = api.requests.get("http://;/schema.yml")
dump = yaml.safe_load(r.content)
assert dump
assert dump["openapi"] == "3.0"
def test_mount_wsgi_app(api, flask, session):
def test_mount_wsgi_app(api, flask):
@api.route("/")
def hello(req, resp):
resp.text = "hello"
api.mount("/flask", flask)
r = session.get("http://;/flask")
r = api.requests.get("http://;/flask")
assert r.ok
def test_async_class_based_views(api, session):
def test_async_class_based_views(api):
@api.route("/")
class Resource:
async def on_post(self, req, resp):
resp.text = await req.text
data = "frame"
r = session.post(api.url_for(Resource), data=data)
r = api.requests.post(api.url_for(Resource), data=data)
assert r.text == data
def test_cookies(api, session):
def test_cookies(api):
@api.route("/")
def cookies(req, resp):
resp.media = {"cookies": req.cookies}
resp.cookies["sent"] = "true"
r = session.get(api.url_for(cookies), cookies={"hello": "universe"})
r = api.requests.get(api.url_for(cookies), cookies={"hello": "universe"})
assert r.json() == {"cookies": {"hello": "universe"}}
assert "sent" in r.cookies
r = session.get(api.url_for(cookies))
r = api.requests.get(api.url_for(cookies))
assert r.json() == {"cookies": {"sent": "true"}}
def test_sessions(api, session):
def test_sessions(api):
@api.route("/")
def view(req, resp):
resp.session["hello"] = "world"
resp.media = resp.session
r = session.get(api.url_for(view))
r = api.requests.get(api.url_for(view))
assert "Responder-Session" in r.cookies
r = session.get(api.url_for(view))
r = api.requests.get(api.url_for(view))
assert (
r.cookies["Responder-Session"]
== '{"hello": "world"}.lJVWJULPqR9kdao_oT4pUglV281bxHfGvcKQ7XF8qNqaiIZlRcMvqKNdA1-d5z7DycAx5eqmzJZoqWPP759-Cw'
@@ -403,16 +408,16 @@ def test_sessions(api, session):
assert r.json() == {"hello": "world"}
def test_template_rendering(api, session):
def test_template_rendering(api):
@api.route("/")
def view(req, resp):
resp.content = api.template_string("{{ var }}", var="hello")
r = session.get(api.url_for(view))
r = api.requests.get(api.url_for(view))
assert r.text == "hello"
def test_file_uploads(api, session):
def test_file_uploads(api):
@api.route("/")
async def upload(req, resp):
@@ -422,14 +427,28 @@ def test_file_uploads(api, session):
world = io.StringIO("world")
data = {"hello": world}
r = session.post(api.url_for(upload), files=data)
r = api.requests.post(api.url_for(upload), files=data)
assert r.json() == {"files": {"hello": "world"}}
def test_500(api, session):
def test_500(api):
@api.route("/")
def view(rea, resp):
def view(req, resp):
raise ValueError
r = session.get(api.url_for(view))
r = api.requests.get(api.url_for(view))
assert not r.ok
def test_404(api):
r = api.requests.get("/foo")
assert r.status_code == responder.status_codes.HTTP_404
def test_kinda_websockets(api):
@api.route("/ws", websocket=True)
async def websocket(ws):
await ws.accept()
await ws.send_text("Hello via websocket!")
await ws.close()