Compare commits

...

10 Commits

Author SHA1 Message Date
kennethreitz 37c9cba42e version 2018-10-18 10:20:06 -07:00
kennethreitz c1544f66bb tour 2018-10-18 10:14:20 -07:00
kennethreitz d37f41f6a5 docstrings 2018-10-18 10:08:57 -07:00
kennethreitz b245dd2d51 Merge pull request #96 from kennethreitz/sessions
sessions
2018-10-18 10:05:16 -07:00
kennethreitz a1fcf11399 Merge pull request #95 from taoufik07/patch-4
Use HTTPSRedirectMiddleware
2018-10-18 10:04:41 -07:00
kennethreitz 8f876da245 sessions 2018-10-18 10:03:56 -07:00
Taoufik 23b8e5a2b3 Use HTTPSRedirectMiddleware 2018-10-18 18:01:41 +01:00
kennethreitz 3b7e7c7192 Merge pull request #94 from mathiasose/graphql-variables-and-operation-name
Find GraphQL variables, operation name from JSON
2018-10-18 07:49:34 -07:00
Mathias Ose b7ecf6e2e0 Find GraphQL variables, operation name from JSON
Make `_resolve_graphql_query` return *three* things from the JSON query: query (as before), variables and operation names. These values are all passed on to `schema.execute`.

TODO:
- Get variables and operation names from other requests types than JSON.
- Write tests.
- _Possibly_ refactor `_resolve_graphql_query` to return something a bit more structured than a 3-tuple.
2018-10-18 16:09:49 +02:00
kennethreitz 2ec6aaff03 docstrings 2018-10-18 06:42:26 -07:00
8 changed files with 162 additions and 50 deletions
+3
View File
@@ -1,3 +1,6 @@
# v0.1.3
- Sessions support.
# v0.1.2
- Cookies support.
Generated
+69 -23
View File
@@ -1,7 +1,7 @@
{
"_meta": {
"hash": {
"sha256": "9b959d9507c521f6088646507633207db03afec6ac31aeab07adf0d737dbb45b"
"sha256": "7bbe1f0addd73250027de73d6fb749aa2be3149af9744b107820c5e10498428e"
},
"pipfile-spec": 6,
"requires": {
@@ -112,6 +112,12 @@
],
"version": "==2.7"
},
"itsdangerous": {
"hashes": [
"sha256:cbb3fcf8d3e33df861709ecaf89d9e6629cff0a217bc2848f1b41cd30d360519"
],
"version": "==0.24"
},
"jinja2": {
"hashes": [
"sha256:74c935a1b8bb9a3947c50a54766a969d4846290e1e788ea44c1392163723c3bd",
@@ -163,10 +169,10 @@
},
"requests": {
"hashes": [
"sha256:63b52e3c866428a224f97cab011de738c36aec0185aa91cfacd418b5d58911d1",
"sha256:ec22d826a36ed72a7358ff3fe56cbd4ba69dd7a6718ffd450ff0e9df7a47ce6a"
"sha256:99dcfdaaeb17caf6e526f32b6a7b780461512ab3f1d992187801694cba42770c",
"sha256:a84b8c9ab6239b578f22d1c21d51b696dcfe004032bb80ea832398d6909d7279"
],
"version": "==2.19.1"
"version": "==2.20.0"
},
"responder": {
"editable": true,
@@ -195,22 +201,22 @@
},
"starlette": {
"hashes": [
"sha256:2c7ec085440fce7146a9be2b6d53b7110c3866ce6fa03d901efdc1fbe97e0f36"
"sha256:ce5c684fad4edb2967cd491518cd3c2724e420508202c2d48f519ea68dcec9d6"
],
"version": "==0.4.2"
"version": "==0.5.4"
},
"urllib3": {
"hashes": [
"sha256:a68ac5e15e76e7e5dd2b8f94007233e01effe3e50e8daddf69acfd81cb686baf",
"sha256:b5725a0bd4ba422ab0e66e89e030c806576753ea3ee08554382c14e685d117b5"
"sha256:41c3db2fc01e5b907288010dec72f9d0a74e37d6994e6eb56849f59fea2265ae",
"sha256:8819bba37a02d143296a4d032373c4dd4aca11f6d4c9973335ca75f9c8475f59"
],
"version": "==1.23"
"version": "==1.24"
},
"uvicorn": {
"hashes": [
"sha256:8de03999a936d8704f07cc3b1d3a3edb6922a068b64d84b4f5e49604c8b70a11"
"sha256:30096b58325cdca8e547a6f5f4300040d0b8763f573cb1843abfa96f81a49cf8"
],
"version": "==0.3.12"
"version": "==0.3.13"
},
"websockets": {
"hashes": [
@@ -389,6 +395,38 @@
"markers": "sys_platform == 'win32'",
"version": "==0.4.0"
},
"coverage": {
"hashes": [
"sha256:043d55226aec1d2baf4b2fcab5c204561ccf184a388096f41e396c1c092aff38",
"sha256:10bfd0b80b01d0684f968abbe1186bc19962e07b4b7601bb43b175b617cf689d",
"sha256:17e59864f19b3233032edb0566f26c25cc7f599503fb34d2645b5ce1fd6c2c3c",
"sha256:2105ee183c51fed27e2b6801029b3903f5c2774c78e3f53bd920ca468d0f5679",
"sha256:236505d15af6c7b7bfe2a9485db4b2bdea21d9239351483326184314418c79a8",
"sha256:237284425271db4f30d458b355decf388ab20b05278bdf8dc9a65de0973726c6",
"sha256:26d8eea4c840b73c61a1081d68bceb57b21a2d4f7afda6cac8ac38cb05226b00",
"sha256:39a3740f7721155f4269aedf67b211101c07bd2111b334dfd69b807156ab15d9",
"sha256:4bd0c42db8efc8a60965769796d43a5570906a870bc819f7388860aa72779d1b",
"sha256:4dcddadea47ac30b696956bd18365cd3a86724821656601151e263b86d34798f",
"sha256:51ea341289ac4456db946a25bd644f5635e5ae3793df262813cde875887d25c8",
"sha256:5415cafb082dad78935b3045c2e5d8907f436d15ad24c3fdb8e1839e084e4961",
"sha256:5631f1983074b33c35dbb84607f337b9d7e9808116d7f0f2cb7b9d6d4381d50e",
"sha256:5e9249bc361cd22565fd98590a53fd25a3dd666b74791ed7237fa99de938bbed",
"sha256:6a48746154f1331f28ef9e889c625b5b15a36cb86dd8021b4bdd1180a2186aa5",
"sha256:71d376dbac64855ed693bc1ca121794570fe603e8783cdfa304ec6825d4e768f",
"sha256:749ebd8a615337747592bd1523dfc4af7199b2bf6403b55f96c728668aeff91f",
"sha256:8ec528b585b95234e9c0c31dcd0a89152d8ed82b4567aa62dbcb3e9a0600deee",
"sha256:a1a9ccd879811437ca0307c914f136d6edb85bd0470e6d4966c6397927bcabd9",
"sha256:abd956c334752776230b779537d911a5a12fcb69d8fd3fe332ae63a140301ae6",
"sha256:ad18f836017f2e8881145795f483636564807aaed54223459915a0d4735300cf",
"sha256:b07ac0b1533298ddbc54c9bf3464664895f22899fec027b8d6c8d3ac59023283",
"sha256:d9385f1445e30e8e42b75a36a7899ea1fd0f5784233a626625d70f9b087de404",
"sha256:db2d1fcd32dbeeb914b2660af1838e9c178b75173f95fd221b1f9410b5d3ef1d",
"sha256:e1dec211147f1fd7cb7a0f9a96aeeca467a5af02d38911307b3b8c2324f9917e",
"sha256:e96dffc1fa57bb8c1c238f3d989341a97302492d09cb11f77df031112621c35c",
"sha256:ed4d97eb0ecdee29d0748acd84e6380729f78ce5ba0c7fe3401801634c25a1c5"
],
"version": "==5.0a3"
},
"docutils": {
"hashes": [
"sha256:02aec4bd92ab067f6ff27a38a38a41173bf01bed8f89157768c1573f53e474a6",
@@ -490,10 +528,10 @@
},
"pluggy": {
"hashes": [
"sha256:6e3836e39f4d36ae72840833db137f7b7d35105079aee6ec4a62d9f80d594dd1",
"sha256:95eb8364a4708392bae89035f45341871286a333f749c3141c20573d2b3876e1"
"sha256:447ba94990e8014ee25ec853339faf7b0fc8050cdc3289d4d71f7f410fb90095",
"sha256:bde19360a8ec4dfd8a20dcb811780a30998101f078fc7ded6162f0076f50508f"
],
"version": "==0.7.1"
"version": "==0.8.0"
},
"py": {
"hashes": [
@@ -538,11 +576,19 @@
},
"pytest": {
"hashes": [
"sha256:7e258ee50338f4e46957f9e09a0f10fb1c2d05493fa901d113a8dafd0790de4e",
"sha256:9332147e9af2dcf46cd7ceb14d5acadb6564744ddff1fe8c17f0ce60ece7d9a2"
"sha256:10e59f84267370ab20cec9305bafe7505ba4d6b93ecbf66a1cce86193ed511d5",
"sha256:8c827e7d4816dfe13e9329c8226aef8e6e75d65b939bc74fda894143b6d1df59"
],
"index": "pypi",
"version": "==3.8.2"
"version": "==3.9.1"
},
"pytest-cov": {
"hashes": [
"sha256:513c425e931a0344944f84ea47f3956be0e416d95acbd897a44970c8d926d5d7",
"sha256:e360f048b7dae3f2f2a9a4d067b2dd6b6a015d384d1577c994a43f3f7cbad762"
],
"index": "pypi",
"version": "==2.6.0"
},
"pytz": {
"hashes": [
@@ -560,10 +606,10 @@
},
"requests": {
"hashes": [
"sha256:63b52e3c866428a224f97cab011de738c36aec0185aa91cfacd418b5d58911d1",
"sha256:ec22d826a36ed72a7358ff3fe56cbd4ba69dd7a6718ffd450ff0e9df7a47ce6a"
"sha256:99dcfdaaeb17caf6e526f32b6a7b780461512ab3f1d992187801694cba42770c",
"sha256:a84b8c9ab6239b578f22d1c21d51b696dcfe004032bb80ea832398d6909d7279"
],
"version": "==2.19.1"
"version": "==2.20.0"
},
"requests-toolbelt": {
"hashes": [
@@ -625,10 +671,10 @@
},
"urllib3": {
"hashes": [
"sha256:a68ac5e15e76e7e5dd2b8f94007233e01effe3e50e8daddf69acfd81cb686baf",
"sha256:b5725a0bd4ba422ab0e66e89e030c806576753ea3ee08554382c14e685d117b5"
"sha256:41c3db2fc01e5b907288010dec72f9d0a74e37d6994e6eb56849f59fea2265ae",
"sha256:8819bba37a02d143296a4d032373c4dd4aca11f6d4c9973335ca75f9c8475f59"
],
"version": "==1.23"
"version": "==1.24"
},
"webencodings": {
"hashes": [
+23
View File
@@ -149,6 +149,29 @@ If you have a single-page webapp, you can tell Responder to serve up your ``stat
This will make ``index.html`` the default response to all undefined routes.
Reading / Writing Cookies
-------------------------
Responder makes it very easy to interact with cookies from a Request, or add some to a Response::
>>> resp.cookies["hello"] = "world"
>>> req.cookies
{"hello": "world"}
Using Cookie-Based Sessions
---------------------------
Responder has built-in support for cookie-based sessions. To enable cookie-based sessions, simply add something to the ``resp.session`` dictionary::
>>> resp.session['username'] = 'kennethreitz'
A cookie called ``Responder-Session`` will be set, which contains all the data in ``resp.session``. It is signed, for verification purposes.
**Note**: if you are using this in production, you should pass the ``secret_key`` argument to ``API(...)``.
HSTS (Redirect to HTTPS)
------------------------
+1 -1
View File
@@ -1 +1 @@
__version__ = "0.1.2"
__version__ = "0.1.3"
+30 -14
View File
@@ -7,11 +7,13 @@ import uvicorn
import asyncio
import jinja2
import itsdangerous
from graphql_server import encode_execution_results, json_encode, default_format_error
from starlette.routing import Router
from starlette.staticfiles import StaticFiles
from starlette.testclient import TestClient
from starlette.middleware.gzip import GZipMiddleware
from starlette.middleware.httpsredirect import HTTPSRedirectMiddleware
from apispec import APISpec
from apispec.ext.marshmallow import MarshmallowPlugin
from apispec import yaml_utils
@@ -45,8 +47,10 @@ class API:
static_dir="static",
static_route="/static",
templates_dir="templates",
secret_key="NOTASECRET",
enable_hsts=False,
):
self.secret_key = secret_key
self.title = title
self.version = version
self.openapi_version = openapi
@@ -79,6 +83,8 @@ class API:
self.default_endpoint = None
self.app = self.dispatch
self.add_middleware(GZipMiddleware)
if self.hsts_enabled:
self.add_middleware(HTTPSRedirectMiddleware)
@property
def _apispec(self):
@@ -130,7 +136,7 @@ class API:
async def asgi(receive, send):
nonlocal scope, self
req = models.Request(scope, receive=receive)
req = models.Request(scope, receive=receive, api=self)
resp = await self._dispatch_request(req)
await resp(receive, send)
@@ -172,10 +178,21 @@ class API:
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
@property
def _signer(self):
return itsdangerous.Signer(self.secret_key)
def _prepare_session(self, resp):
if resp.session:
data = self._signer.sign(json.dumps(resp.session).encode("utf-8"))
resp.cookies["Responder-Session"] = data.decode("utf-8")
async def _dispatch_request(self, req):
# Set formats on Request object.
req.formats = self.formats
@@ -183,11 +200,6 @@ class API:
route = self.path_matches_route(req.url.path)
resp = models.Response(req=req, formats=self.formats)
if self.hsts_enabled:
if req.url.startswith("http://"):
url = req.url.replace("http://", "https://", 1)
self.redirect(resp, location=url)
if route:
try:
params = self.routes[route].incoming_matches(req.url.path)
@@ -233,6 +245,7 @@ class API:
else:
self.default_response(req, resp)
self._prepare_session(resp)
self._prepare_cookies(resp)
return resp
@@ -301,25 +314,28 @@ class API:
@staticmethod
async def _resolve_graphql_query(req):
# TODO: Get variables and operation_name from form data, params, request text?
if "json" in req.mimetype:
return (await req.media("json"))["query"]
json_media = await req.media("json")
return json_media["query"], json_media.get("variables"), json_media.get("operationName")
# Support query/q in form data.
# Form data is awaiting https://github.com/encode/starlette/pull/102
# if "query" in req.media("form"):
# return req.media("form")["query"]
# return req.media("form")["query"], None, None
# if "q" in req.media("form"):
# return req.media("form")["q"]
# return req.media("form")["q"], None, None
# Support query/q in params.
if "query" in req.params:
return req.params["query"]
return req.params["query"], None, None
if "q" in req.params:
return req.params["q"]
return req.params["q"], None, None
# Otherwise, the request text is used (typical).
# TODO: Make some assertions about content-type here.
return req.text
return req.text, None, None
async def graphql_response(self, req, resp, schema):
show_graphiql = req.method == "get" and req.accepts("text/html")
@@ -328,8 +344,8 @@ class API:
resp.content = self.template_string(GRAPHIQL, endpoint=req.url.path)
return
query = await self._resolve_graphql_query(req)
result = schema.execute(query)
query, variables, operation_name = await self._resolve_graphql_query(req)
result = schema.execute(query, variables=variables, operation_name=operation_name)
result, status_code = encode_execution_results(
[result],
is_batch=False,
+16 -8
View File
@@ -90,12 +90,13 @@ class QueryDict(dict):
# TODO: add slots
class Request:
__slots__ = ["_starlette", "formats", "_headers", "_encoding"]
__slots__ = ["_starlette", "formats", "_headers", "_encoding", "api"]
def __init__(self, scope, receive):
def __init__(self, scope, receive, api=None):
self._starlette = StarletteRequest(scope, receive)
self.formats = None
self._encoding = None
self.api = api
headers = CaseInsensitiveDict()
for header, value in self._starlette.headers.items():
@@ -103,6 +104,15 @@ class Request:
self._headers = headers
@property
def session(self):
"""The session data, in dict form, from the Request."""
if "Responder-Session" in self.cookies:
data = self.cookies["Responder-Session"]
data = self.api._signer.unsign(data)
return json.loads(data)
return {}
@property
def headers(self):
"""A case-insensitive dictionary, containing all headers sent in the Request."""
@@ -129,6 +139,7 @@ class Request:
@property
def cookies(self):
"""The cookies sent in the Request, as a dictionary."""
cookies = RequestsCookieJar()
cookie_header = self.headers.get("cookie", "")
@@ -226,6 +237,7 @@ class Response:
"headers",
"formats",
"cookies",
"session",
]
def __init__(self, req, *, formats):
@@ -241,7 +253,8 @@ class Response:
{}
) #: A Python dictionary of {Key: value}, representing the headers of the response.
self.formats = formats
self.cookies = {} # req.cookies
self.cookies = {} #: The cookies set in the Response, as a dictionary
self.session = req.session.copy() #: """The *cookie-based* session data, in dict form, to add to the Response."""
@property
async def body(self):
@@ -270,8 +283,3 @@ class Response:
body, status_code=self.status_code, headers=headers
)
await response(receive, send)
class Schema(graphene.Schema):
def on_request(self, req, resp):
pass
+1
View File
@@ -38,6 +38,7 @@ required = [
"apispec>=1.0.0b1",
"marshmallow",
"asgiref",
"itsdangerous",
]
+19 -4
View File
@@ -370,15 +370,30 @@ def test_async_class_based_views(api, session):
r = session.post(api.url_for(Resource), data=data)
assert r.text == data
def test_cookies(api, session):
@api.route("/")
def cookies(req, resp):
resp.media = {'cookies': req.cookies}
resp.cookies['sent'] = 'true'
resp.media = {"cookies": req.cookies}
resp.cookies["sent"] = "true"
r = session.get(api.url_for(cookies), cookies={'hello': 'universe'})
r = session.get(api.url_for(cookies), cookies={"hello": "universe"})
assert r.json() == {"cookies": {"hello": "universe"}}
assert 'sent' in r.cookies
assert "sent" in r.cookies
r = session.get(api.url_for(cookies))
assert r.json() == {"cookies": {"sent": "true"}}
def test_sessions(api, session):
@api.route("/")
def view(req, resp):
resp.session["hello"] = "world"
resp.media = resp.session
r = session.get(api.url_for(view))
assert "Responder-Session" in r.cookies
r = session.get(api.url_for(view))
assert r.cookies['Responder-Session'] == '{"hello": "world"}.r3EB04hEEyLYIJaAXCEq3d4YEbs'
assert r.json() == {"hello": "world"}