Compare commits

...

42 Commits

Author SHA1 Message Date
kennethreitz 2710d7098f v0.2.3 2018-10-24 07:02:44 -04:00
kennethreitz 7f41ff4035 Merge pull request #138 from taoufik07/fix/cbv
Fix CBV
2018-10-24 06:59:51 -04:00
kennethreitz ed8d51014c Merge branch 'master' into fix/cbv 2018-10-24 06:57:28 -04:00
kennethreitz d09a51f47d Merge pull request #140 from taoufik07/patch-9
Typo
2018-10-24 06:56:53 -04:00
kennethreitz 59bae90454 Merge pull request #142 from taoufik07/fix/static_response
Fix static response
2018-10-24 06:56:42 -04:00
kennethreitz 13ee0ca94e Merge pull request #136 from taoufik07/fix/Route.is_function
Fix Route.is_function
2018-10-24 06:56:24 -04:00
kennethreitz 5abc095050 Merge pull request #139 from JayjeetAtGithub/master
Fix Typo in api.py
2018-10-24 06:56:02 -04:00
kennethreitz 7eb68c8388 Merge pull request #143 from frostming/patch-1
Typo in tour.rst
2018-10-24 06:55:50 -04:00
Frost Ming f69b644a77 Typo in tour.rst 2018-10-24 12:28:11 +08:00
taoufik07 fe41d4c863 Fix static response 2018-10-24 01:17:02 +01:00
Taoufik 29830455ed Typo 2018-10-24 00:11:27 +01:00
Jayjeet Chakraborty e50828093d Clean print statement 2018-10-24 02:56:43 +05:30
Jayjeet Chakraborty 880d29c5a9 Fix Typo in api.py 2018-10-24 02:46:33 +05:30
taoufik07 77b2e9ba7a tests 2018-10-23 21:20:09 +01:00
taoufik07 586fad7646 Fix CBV 2018-10-23 21:19:57 +01:00
kennethreitz fb636028fb improvements 2018-10-23 14:58:02 -04:00
taoufik07 a8c3f8fc46 Fix Route.is_function 2018-10-23 19:52:43 +01:00
kennethreitz 72f4227c5a remove redundancy in tour 2018-10-23 08:46:20 -04:00
kennethreitz 8ccace8ef9 testing 2018-10-23 08:36:20 -04:00
kennethreitz 6d40c6dfe5 assert 2018-10-23 08:34:32 -04:00
kennethreitz 0b5562cdec fix 2018-10-23 08:33:51 -04:00
kennethreitz eeff0816f3 doc updates 2018-10-23 08:29:02 -04:00
kennethreitz f1f16dea3f best practices for secret key 2018-10-23 08:14:00 -04:00
kennethreitz bfc6ef2049 test client docs 2018-10-23 08:12:40 -04:00
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
12 changed files with 272 additions and 260 deletions
+9
View File
@@ -1,3 +1,12 @@
# v0.3.2
- Overall improvements.
# v0.2.2
- Show traceback info when background tasks raise exceptions.
# v0.2.1
- api.requests.
# v0.2.0
- WebSocket support.
+1 -1
View File
@@ -123,7 +123,7 @@ Boom.
Install the latest release:
$ pipenv install responder
$ pipenv install responder --pre
✨🍰✨
+2 -2
View File
@@ -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.
@@ -106,6 +105,7 @@ User Guides
quickstart
tour
deployment
testing
api
@@ -114,7 +114,7 @@ Installing Responder
.. code-block:: shell
$ pipenv install responder
$ pipenv install responder --pre
✨🍰✨
Only **Python 3.6+** is supported.
+81
View File
@@ -0,0 +1,81 @@
Building and Testing with Responder
===================================
Responder comes with a first-class, well supported test client for your ASGI web services: **Requests**.
Here, we'll go over the basics of setting up a proper Python package and adding testing to it.
The Basics
----------
Your repository should look like this::
Pipfile Pipfile.lock api.py test_api.py
``$ cat api.py``::
import responder
api = responder.API()
@api.route("/")
def hello_world(req, resp):
resp.text = "hello, world!"
if __name__ == "__main__":
api.run()
``$ cat Pipfile``::
[[source]]
url = "https://pypi.org/simple"
verify_ssl = true
name = "pypi"
[packages]
responder = "*"
[dev-packages]
pytest = "*"
[requires]
python_version = "3.7"
[pipenv]
allow_prereleases = true
Writing Tests
-------------
``$ cat test_api.py``::
import pytest
import api as service
@pytest.fixture
def api():
return service.api
def test_hello_world(api):
r = api.requests.get("/")
assert r.text == "hello, world!"
``$ pytest``::
...
========================== 1 passed in 0.10 seconds ==========================
(Optional) Proper Python Package
--------------------------------
Optionally, you can not rely on relative imports, and instead install your api as a proper package. This requires:
1. A `proper setup.py <https://github.com/kennethreitz/setup.py>`_ file.
2. ``$ pipenv install -e . --dev``
This will allow you to only specify your dependencies once: in ``setup.py``. ``$ pipenv lock`` will automatically lock your transitive dependencies (e.g. Responder), even if it's not specified in the ``Pipfile``.
This will ensure that your application gets installed in every developer's environment, using Pipenv.
+25 -33
View File
@@ -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
@@ -50,23 +50,6 @@ Serve a GraphQL API::
Visiting the endpoint will render a *GraphiQL* instance, in the browser.
Built-in Testing Client (Requests)
----------------------------------
We can then send a query to our service::
>>> requests = api.session()
>>> r = requests.get("http://;/graph", params={"query": "{ hello }"})
>>> r.json()
{'data': {'hello': 'Hello stranger'}}
Or, request YAML back::
>>> r = requests.get("http://;/graph", params={"query": "{ hello(name:\"john\") }"}, headers={"Accept": "application/x-yaml"})
>>> print(r.text)
data: {hello: Hello john}
OpenAPI Schema Support
----------------------
@@ -147,11 +130,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
-------------------------
@@ -178,24 +157,37 @@ You can easily read a Request's session data, that can be trusted to have origin
>>> req.session
{'username': 'kennethreitz'}
**Note**: if you are using this in production, you should pass the ``secret_key`` argument to ``API(...)``.
**Note**: if you are using this in production, you should pass the ``secret_key`` argument to ``API(...)``::
WebSocket Support
-----------------
api = responder.API(secret_key=os.environ['SECRET_KEY'])
Responder supports WebSockets::
Using Requests Test Client
--------------------------
@api.ws_route('/ws')
async def hello(ws):
await ws.accept()
await ws.send_text("Hello via websocket!")
await ws.close()
Responder comes with a first-class, well supported test client for your ASGI web services: **Requests**.
Here's an example of a test (written with pytest)::
import myapi
@pytest.fixture
def api():
return myapi.api
def test_response(api):
hello = "hello, world!"
@api.route('/some-url')
def some_view(req, resp):
resp.text = hello
r = api.requests.get(url=api.url_for(some_view))
assert r.text == hello
HSTS (Redirect to HTTPS)
------------------------
Want HSTS?
Want HSTS (to redirect all traffic to HTTPS)?
::
+1 -1
View File
@@ -1 +1 @@
__version__ = "0.2.0"
__version__ = "0.2.3"
+68 -82
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
@@ -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)
@@ -99,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):
@@ -149,22 +156,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:
@@ -191,14 +191,13 @@ 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):
@@ -220,7 +219,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
@@ -229,86 +228,74 @@ 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:
elif route.is_class_based or cont:
try:
view = route.endpoint(**params)
except TypeError:
view = route.endpoint
view = route.endpoint()
# Run on_request first.
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)
self.default_response(req, resp)
return kwargs
self._prepare_session(resp)
self._prepare_cookies(resp)
return resp
def add_route(
self,
@@ -316,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
@@ -346,14 +325,15 @@ class API:
if default:
self.default_endpoint = endpoint
# Can we remove it ?
try:
if callable(endpoint):
endpoint.is_routed = True
except AttributeError:
pass
self.routes[route] = Route(route, endpoint, protocol)
# TODO: A better datastructer or sort it once the app is loaded
self.routes[route] = Route(route, endpoint, websocket=websocket)
# TODO: A better data structure or sort it once the app is loaded
self.routes = dict(
sorted(self.routes.items(), key=lambda item: item[1]._weight())
)
@@ -374,6 +354,7 @@ class API:
def static_response(self, req, resp):
index = (self.static_dir / "index.html").resolve()
resp.content = ""
if os.path.exists(index):
with open(index, "r") as f:
resp.text = f.read()
@@ -485,18 +466,23 @@ class API:
self._session = TestClient(self)
return self._session
def url_for(self, endpoint, testing=False, **params):
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, **params):
# TODO: Absolute_url
"""Given an endpoint, returns a rendered URL for its route.
: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(**params)
raise ValueError
def static_url(self, asset):
+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
-63
View File
@@ -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)
+11 -15
View File
@@ -3,10 +3,10 @@ from parse import parse
def memoize(f):
def helper(self, s, *args, **kwargs):
def helper(self, s):
memoize_key = f"{f.__name__}:{s}"
if memoize_key not in self._memo:
self._memo[memoize_key] = f(self, s, *args, **kwargs)
self._memo[memoize_key] = f(self, s)
return self._memo[memoize_key]
return helper
@@ -15,10 +15,10 @@ def memoize(f):
class Route:
_param_pattern = re.compile(r"{([^{}]*)}")
def __init__(self, route, endpoint, protocol="http"):
def __init__(self, route, endpoint, websocket=False):
self.route = route
self.endpoint = endpoint
self.protocol = protocol
self.uses_websocket = websocket
self._memo = {}
def __repr__(self):
@@ -45,10 +45,8 @@ class Route:
return bool(self._param_pattern.search(self.route))
@memoize
def does_match(self, s, protocol="http"):
def does_match(self, s):
if s == self.route:
if self.protocol != protocol:
return False
return True
named = self.incoming_matches(s)
@@ -59,12 +57,8 @@ class Route:
results = parse(self.route, s)
return results.named if results else {}
def url(self, testing=False, **params):
url = self.route.format(**params)
if testing:
url = f"http://;{url}"
return url
def url(self, **params):
return self.route.format(**params)
def _weight(self):
params = set(self._param_pattern.findall(self.route))
@@ -78,9 +72,11 @@ class Route:
@property
def is_class_based(self):
return hasattr(self.endpoint, "__class__")
@property
def is_function(self):
# TODO: Should we remove is_routed ?
routed = hasattr(self.endpoint, "is_routed")
code = hasattr(self.endpoint, "__code__")
kwdefaults = hasattr(self.endpoint, "__kwdefaults__")
return all((routed, code, kwdefaults))
return all((callable(self.endpoint), code, kwdefaults))
+1 -1
View File
@@ -21,7 +21,7 @@ def api():
@pytest.fixture
def session(api):
return api.session()
return api.requests
@pytest.fixture
+65 -62
View File
@@ -65,7 +65,7 @@ def test_class_based_view_registration(api):
def test_class_based_view_parameters(api):
@api.route("/{greeting}")
class Greeting:
def on_request(req, resp, *, greeting):
def on_request(self, req, resp, *, greeting):
resp.text = f"{greeting}, world!"
assert api.session().get("http://;/Hello").ok
@@ -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,29 +124,29 @@ 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):
@@ -154,7 +155,7 @@ def test_request_and_get(api, session):
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,21 +427,21 @@ 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(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(session):
r = session.get("/foo")
def test_404(api):
r = api.requests.get("/foo")
assert r.status_code == responder.status_codes.HTTP_404
@@ -447,5 +452,3 @@ def test_kinda_websockets(api):
await ws.accept()
await ws.send_text("Hello via websocket!")
await ws.close()