mirror of
https://github.com/kennethreitz/responder.git
synced 2026-06-05 23:00:17 +00:00
Compare commits
30 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5f60e4fedb | |||
| 96971a33a7 | |||
| 9a7409f521 | |||
| 80aa7e305b | |||
| 27d513cb01 | |||
| 9bf5cc8c03 | |||
| 7994b210cd | |||
| 46555bbe3f | |||
| 4d15dbc465 | |||
| 855d3c4320 | |||
| 4564862acc | |||
| 176dd70073 | |||
| a5e6f0c196 | |||
| 083bb5a96c | |||
| 04522281be | |||
| 0e8bb49b59 | |||
| 9abf6eea16 | |||
| 1d7a04ce7b | |||
| 49fb5792c3 | |||
| 5eebba09c5 | |||
| b86974688e | |||
| 74afe2ed13 | |||
| ed53a0b624 | |||
| 23e15d6459 | |||
| 71ea19d1c1 | |||
| fa621d076d | |||
| 4902f1328a | |||
| 2ee8ff484d | |||
| c872fe3c78 | |||
| f46ac08cff |
@@ -1,3 +1,15 @@
|
||||
# v1.1.0
|
||||
- Support for `before_request`.
|
||||
|
||||
# v1.0.4
|
||||
- Potential bufix for cookies.
|
||||
|
||||
# v1.0.3
|
||||
- Bugfix for redirects.
|
||||
|
||||
# v1.0.2
|
||||
- Improvement for static file hosting.
|
||||
|
||||
# v1.0.1
|
||||
- Improve cors configuration settings.
|
||||
|
||||
|
||||
@@ -7,20 +7,8 @@
|
||||
[](https://pypi.org/project/responder/)
|
||||
[](https://github.com/kennethreitz/responder/graphs/contributors)
|
||||
|
||||
[](http://python-responder.org/)
|
||||
[](http://python-responder.org/)
|
||||
|
||||
```python
|
||||
import responder
|
||||
|
||||
api = responder.API()
|
||||
|
||||
@api.route("/{greeting}")
|
||||
async def greet_world(req, resp, *, greeting):
|
||||
resp.text = f"{greeting}, world!"
|
||||
|
||||
if __name__ == '__main__':
|
||||
api.run()
|
||||
```
|
||||
|
||||
Powered by [Starlette](https://www.starlette.io/). That `async` declaration is optional. [View documentation](http://python-responder.org).
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ Assuming existing ``api.py`` and ``Pipfile.lock`` containing ``responder``.
|
||||
|
||||
``Dockerfile``::
|
||||
|
||||
from kennethreitz/pipenv
|
||||
FROM kennethreitz/pipenv
|
||||
|
||||
COPY . /app
|
||||
CMD python3 api.py
|
||||
|
||||
@@ -90,7 +90,7 @@ If you want to set a response header, like ``X-Pizza: 42``, simply modify the ``
|
||||
|
||||
@api.route("/pizza")
|
||||
def pizza_pizza(req, resp):
|
||||
resp.headers['X-Pizza'] = 42
|
||||
resp.headers['X-Pizza'] = '42'
|
||||
|
||||
That's it!
|
||||
|
||||
|
||||
+13
-2
@@ -46,7 +46,7 @@ Serve a GraphQL API::
|
||||
return f"Hello {name}"
|
||||
|
||||
schema = graphene.Schema(query=Query)
|
||||
view = responder.ext.GraphQLView(query=query, api=api)
|
||||
view = responder.ext.GraphQLView(api=api, schema=schema)
|
||||
|
||||
api.add_route("/graph", view)
|
||||
|
||||
@@ -173,6 +173,17 @@ You can easily read a Request's session data, that can be trusted to have origin
|
||||
|
||||
api = responder.API(secret_key=os.environ['SECRET_KEY'])
|
||||
|
||||
Using ``before_request``
|
||||
------------------------
|
||||
|
||||
If you'd like a view to be executed before every request, simply do the following::
|
||||
|
||||
@api.route(before_request=True)
|
||||
def prepare_response(req, resp):
|
||||
resp.headers["X-Pizza"] = "42"
|
||||
|
||||
Now all requests to your HTTP Service will include an ``X-Pizza`` header.
|
||||
|
||||
Using Requests Test Client
|
||||
--------------------------
|
||||
|
||||
@@ -220,7 +231,7 @@ Want `CORS <https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS/>`_ ?
|
||||
|
||||
The default parameters used by **Responder** are restrictive by default, so you'll need to explicitly enable particular origins, methods, or headers, in order for browsers to be permitted to use them in a Cross-Domain context.
|
||||
|
||||
In order to set custom parameters, you need to set the ``cors_params`` argument of ``api``, a dictionnary containing the following entries:
|
||||
In order to set custom parameters, you need to set the ``cors_params`` argument of ``api``, a dictionary containing the following entries:
|
||||
|
||||
* ``allow_origins`` - A list of origins that should be permitted to make cross-origin requests. eg. ``['https://example.org', 'https://www.example.org']``. You can use ``['*']`` to allow any origin.
|
||||
* ``allow_origin_regex`` - A regex string to match against origins that should be permitted to make cross-origin requests. eg. ``'https://.*\.example\.org'``.
|
||||
|
||||
@@ -1 +1 @@
|
||||
__version__ = "1.0.1"
|
||||
__version__ = "1.1.0"
|
||||
|
||||
+81
-67
@@ -1,7 +1,9 @@
|
||||
import json
|
||||
import os
|
||||
|
||||
from uuid import uuid4
|
||||
from pathlib import Path
|
||||
from base64 import b64encode
|
||||
|
||||
import apistar
|
||||
import itsdangerous
|
||||
@@ -89,9 +91,7 @@ class API:
|
||||
for _dir in (self.static_dir, self.templates_dir):
|
||||
os.makedirs(_dir, exist_ok=True)
|
||||
|
||||
self.whitenoise = WhiteNoise(
|
||||
application=self._default_wsgi_app, index_file=True
|
||||
)
|
||||
self.whitenoise = WhiteNoise(application=self._default_wsgi_app)
|
||||
self.whitenoise.add_files(str(self.static_dir))
|
||||
|
||||
self.whitenoise.add_files(
|
||||
@@ -146,6 +146,15 @@ class API:
|
||||
def _default_wsgi_app(*args, **kwargs):
|
||||
pass
|
||||
|
||||
@property
|
||||
def before_requests(self):
|
||||
def gen():
|
||||
for route in self.routes:
|
||||
if self.routes[route].before_request:
|
||||
yield self.routes[route]
|
||||
|
||||
return [g for g in gen()]
|
||||
|
||||
@property
|
||||
def _apispec(self):
|
||||
spec = APISpec(
|
||||
@@ -244,7 +253,7 @@ class API:
|
||||
|
||||
def _prepare_cookies(self, resp):
|
||||
if resp.cookies:
|
||||
header = " ".join([f"{k}={v}" for k, v in resp.cookies.items()])
|
||||
header = " ".join([f"{k}={v};" for k, v in resp.cookies.items()])
|
||||
resp.headers["Set-Cookie"] = header
|
||||
|
||||
@property
|
||||
@@ -254,7 +263,9 @@ class API:
|
||||
def _prepare_session(self, resp):
|
||||
|
||||
if resp.session:
|
||||
data = self._signer.sign(json.dumps(resp.session).encode("utf-8"))
|
||||
data = self._signer.sign(
|
||||
b64encode(json.dumps(resp.session).encode("utf-8"))
|
||||
)
|
||||
resp.cookies[self.session_cookie] = data.decode("utf-8")
|
||||
|
||||
@staticmethod
|
||||
@@ -269,69 +280,16 @@ class API:
|
||||
route = self.path_matches_route(req.url.path)
|
||||
route = self.routes.get(route)
|
||||
|
||||
# Create the response object.
|
||||
cont = False
|
||||
if route:
|
||||
if route.uses_websocket:
|
||||
resp = WebSocket(**options)
|
||||
|
||||
else:
|
||||
resp = models.Response(req=req, formats=self.formats)
|
||||
|
||||
params = route.incoming_matches(req.url.path)
|
||||
|
||||
if route.is_function:
|
||||
try:
|
||||
try:
|
||||
# Run the view.
|
||||
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(req, resp, error=True)
|
||||
raise
|
||||
|
||||
elif route.is_class_based or cont:
|
||||
try:
|
||||
view = route.endpoint(**params)
|
||||
except TypeError:
|
||||
try:
|
||||
view = route.endpoint()
|
||||
except TypeError:
|
||||
view = route.endpoint
|
||||
|
||||
# Run on_request first.
|
||||
try:
|
||||
# Run the view.
|
||||
r = getattr(view, "on_request", self.no_response)(
|
||||
req, resp, **params
|
||||
)
|
||||
# If it's async, await it.
|
||||
if hasattr(r, "send"):
|
||||
await r
|
||||
except Exception:
|
||||
self.default_response(req, resp, error=True)
|
||||
raise
|
||||
|
||||
# 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)
|
||||
for before_request in self.before_requests:
|
||||
await self._execute_route(route=before_request, req=req, resp=resp)
|
||||
|
||||
await self._execute_route(route=route, req=req, resp=resp, **options)
|
||||
else:
|
||||
resp = models.Response(req=req, formats=self.formats)
|
||||
self.default_response(req, resp, notfound=True)
|
||||
@@ -342,8 +300,58 @@ class API:
|
||||
|
||||
return resp
|
||||
|
||||
async def _execute_route(self, *, route, req, resp, **options):
|
||||
|
||||
params = route.incoming_matches(req.url.path)
|
||||
|
||||
if route.is_function:
|
||||
try:
|
||||
try:
|
||||
# Run the view.
|
||||
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(req, resp, error=True)
|
||||
raise
|
||||
|
||||
elif route.is_class_based or cont:
|
||||
try:
|
||||
view = route.endpoint(**params)
|
||||
except TypeError:
|
||||
try:
|
||||
view = route.endpoint()
|
||||
except TypeError:
|
||||
view = route.endpoint
|
||||
|
||||
# Run on_request first.
|
||||
try:
|
||||
# Run the view.
|
||||
r = getattr(view, "on_request", self.no_response)(req, resp, **params)
|
||||
# If it's async, await it.
|
||||
if hasattr(r, "send"):
|
||||
await r
|
||||
except Exception:
|
||||
self.default_response(req, resp, error=True)
|
||||
raise
|
||||
|
||||
# Then run on_method.
|
||||
method = req.method
|
||||
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)
|
||||
|
||||
def add_event_handler(self, event_type, handler):
|
||||
"""Add a event handler to the API.
|
||||
"""Adds an event handler to the API.
|
||||
|
||||
:param event_type: A string in ("startup", "shutdown")
|
||||
:param handler: The function to run. Can be either a function or a coroutine.
|
||||
@@ -353,15 +361,16 @@ class API:
|
||||
|
||||
def add_route(
|
||||
self,
|
||||
route,
|
||||
route=None,
|
||||
endpoint=None,
|
||||
*,
|
||||
default=False,
|
||||
static=False,
|
||||
check_existing=True,
|
||||
websocket=False,
|
||||
before_request=False,
|
||||
):
|
||||
"""Add a route to the API.
|
||||
"""Adds a route to the API.
|
||||
|
||||
:param route: A string representation of the route.
|
||||
:param endpoint: The endpoint for the route -- can be a callable, or a class.
|
||||
@@ -369,6 +378,9 @@ class API:
|
||||
: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 route is None:
|
||||
route = f"/{uuid4().hex}"
|
||||
|
||||
if check_existing:
|
||||
assert route not in self.routes
|
||||
|
||||
@@ -379,7 +391,9 @@ class API:
|
||||
if default:
|
||||
self.default_endpoint = endpoint
|
||||
|
||||
self.routes[route] = Route(route, endpoint, websocket=websocket)
|
||||
self.routes[route] = Route(
|
||||
route, endpoint, websocket=websocket, before_request=before_request
|
||||
)
|
||||
# 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())
|
||||
@@ -425,7 +439,7 @@ class API:
|
||||
:param status_code: an `API.status_codes` attribute, or an integer, representing the HTTP status code of the redirect.
|
||||
"""
|
||||
|
||||
assert resp.status_code.is_300(status_code)
|
||||
# assert resp.status_code.is_300(status_code)
|
||||
|
||||
resp.status_code = status_code
|
||||
if set_text:
|
||||
@@ -458,7 +472,7 @@ class API:
|
||||
|
||||
return decorator
|
||||
|
||||
def route(self, route, **options):
|
||||
def route(self, route=None, **options):
|
||||
"""Decorator for creating new routes around function and class definitions.
|
||||
|
||||
Usage::
|
||||
|
||||
+5
-1
@@ -1,6 +1,7 @@
|
||||
import io
|
||||
import json
|
||||
import gzip
|
||||
from base64 import b64decode
|
||||
from http.cookies import SimpleCookie
|
||||
|
||||
|
||||
@@ -109,8 +110,11 @@ class Request:
|
||||
def session(self):
|
||||
"""The session data, in dict form, from the Request."""
|
||||
if "Responder-Session" in self.cookies:
|
||||
|
||||
data = self.cookies[self.api.session_cookie]
|
||||
|
||||
data = self.api._signer.unsign(data)
|
||||
data = b64decode(data)
|
||||
return json.loads(data)
|
||||
return {}
|
||||
|
||||
@@ -142,7 +146,7 @@ class Request:
|
||||
def cookies(self):
|
||||
"""The cookies sent in the Request, as a dictionary."""
|
||||
cookies = RequestsCookieJar()
|
||||
cookie_header = self.headers.get("cookie", "")
|
||||
cookie_header = self.headers.get("Cookie", "")
|
||||
|
||||
bc = SimpleCookie(cookie_header)
|
||||
for k, v in bc.items():
|
||||
|
||||
+2
-1
@@ -15,10 +15,11 @@ def memoize(f):
|
||||
class Route:
|
||||
_param_pattern = re.compile(r"{([^{}]*)}")
|
||||
|
||||
def __init__(self, route, endpoint, *, websocket=False):
|
||||
def __init__(self, route, endpoint, *, websocket=False, before_request=False):
|
||||
self.route = route
|
||||
self.endpoint = endpoint
|
||||
self.uses_websocket = websocket
|
||||
self.before_request = before_request
|
||||
self._memo = {}
|
||||
|
||||
def __repr__(self):
|
||||
|
||||
+45
-1
@@ -424,6 +424,7 @@ def test_cookies(api):
|
||||
assert r.json() == {"cookies": {"sent": "true"}}
|
||||
|
||||
|
||||
@pytest.mark.xfail
|
||||
def test_sessions(api):
|
||||
@api.route("/")
|
||||
def view(req, resp):
|
||||
@@ -436,7 +437,7 @@ def test_sessions(api):
|
||||
r = api.requests.get(api.url_for(view))
|
||||
assert (
|
||||
r.cookies["Responder-Session"]
|
||||
== '{"hello": "world"}.lJVWJULPqR9kdao_oT4pUglV281bxHfGvcKQ7XF8qNqaiIZlRcMvqKNdA1-d5z7DycAx5eqmzJZoqWPP759-Cw'
|
||||
== '{"hello": "world"}.r3EB04hEEyLYIJaAXCEq3d4YEbs'
|
||||
)
|
||||
assert r.json() == {"hello": "world"}
|
||||
|
||||
@@ -515,3 +516,46 @@ def test_startup(api, session):
|
||||
|
||||
r = requests.get(f"http://localhost:5042/hello")
|
||||
assert r.text == "hello, world!"
|
||||
|
||||
|
||||
def test_redirects(api, session):
|
||||
@api.route("/2")
|
||||
def two(req, resp):
|
||||
api.redirect(resp, location="/1")
|
||||
|
||||
@api.route("/1")
|
||||
def one(req, resp):
|
||||
resp.text = "redirected"
|
||||
|
||||
assert session.get("/1").url == "http://testserver/1"
|
||||
|
||||
|
||||
def test_session_thoroughly(api, session):
|
||||
@api.route("/set")
|
||||
def set(req, resp):
|
||||
resp.session["hello"] = "world"
|
||||
api.redirect(resp, location="/get")
|
||||
|
||||
@api.route("/get")
|
||||
def get(req, resp):
|
||||
resp.media = {"session": req.session}
|
||||
|
||||
r = session.get(api.url_for(set))
|
||||
print(r.headers)
|
||||
r = session.get(api.url_for(get))
|
||||
print(r.request.headers)
|
||||
assert r.json() == {"session": {"hello": "world"}}
|
||||
|
||||
def test_before_responpse(api, session):
|
||||
|
||||
@api.route("/get")
|
||||
def get(req, resp):
|
||||
resp.media = req.session
|
||||
|
||||
|
||||
@api.route(before_request=True)
|
||||
def before_request(req, resp):
|
||||
resp.headers["x-pizza"] = "1"
|
||||
|
||||
r = session.get(api.url_for(get))
|
||||
assert 'x-pizza' in r.headers
|
||||
|
||||
Reference in New Issue
Block a user