mirror of
https://github.com/kennethreitz/responder.git
synced 2026-06-05 23:00:17 +00:00
Merge branch 'master' into cli
This commit is contained in:
@@ -1,3 +1,9 @@
|
||||
# v0.0.8
|
||||
- GraphiQL Support
|
||||
|
||||
# v0.0.7
|
||||
- Immutable Request object.
|
||||
|
||||
# v0.0.6:
|
||||
- Ability to mount WSGI apps.
|
||||
- Supply content-type when serving up the schema.
|
||||
|
||||
@@ -51,7 +51,7 @@ Features
|
||||
- Class-based views without inheritence.
|
||||
- ASGI framework, the future of Python web services.
|
||||
- The ability to mount any ASGI / WSGI app at a subroute.
|
||||
- *f-string syntax* route declration.
|
||||
- *f-string syntax* route declaration.
|
||||
- Mutable response object, passed into each view. No need to return anything.
|
||||
- Background tasks, spawned off in a ``ThreadPoolExecutor``.
|
||||
- GraphQL support!
|
||||
|
||||
@@ -115,7 +115,7 @@ Here, we'll process our data in the background, while responding immediately to
|
||||
|
||||
# Parse the incoming data as form-encoded.
|
||||
# Note: 'json' and 'yaml' formats are also automatically supported.
|
||||
data = await resp.media()
|
||||
data = await req.media()
|
||||
|
||||
# Process the data (in the background).
|
||||
process_data(data)
|
||||
|
||||
@@ -1 +1 @@
|
||||
__version__ = "0.0.6"
|
||||
__version__ = "0.0.7"
|
||||
|
||||
+19
-21
@@ -51,6 +51,7 @@ class API:
|
||||
self.static_dir = Path(os.path.abspath(static_dir))
|
||||
self.static_route = f"/{static_dir}"
|
||||
self.templates_dir = Path(os.path.abspath(templates_dir))
|
||||
self.built_in_templates_dir = Path(os.path.abspath(os.path.dirname(__file__) + '/templates'))
|
||||
self.routes = {}
|
||||
self.schemas = {}
|
||||
|
||||
@@ -180,11 +181,10 @@ class API:
|
||||
view = self.routes[route].endpoint(**params)
|
||||
except TypeError:
|
||||
view = self.routes[route].endpoint
|
||||
try:
|
||||
# GraphQL Schema.
|
||||
assert hasattr(view, "execute")
|
||||
|
||||
if self.routes[route].is_graphql:
|
||||
await self.graphql_response(req, resp, schema=view)
|
||||
except AssertionError:
|
||||
else:
|
||||
# WSGI App.
|
||||
# try:
|
||||
# return view(
|
||||
@@ -213,7 +213,6 @@ class API:
|
||||
return resp
|
||||
|
||||
def add_route(self, route, endpoint, *, check_existing=True):
|
||||
# TODO: add graphiql
|
||||
"""Add a route to the API.
|
||||
|
||||
:param route: A string representation of the route.
|
||||
@@ -222,9 +221,9 @@ class API:
|
||||
"""
|
||||
if check_existing:
|
||||
assert route not in self.routes
|
||||
|
||||
# TODO: Support grpahiql.
|
||||
self.routes[route] = Route(route, endpoint)
|
||||
# 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()))
|
||||
|
||||
def default_response(self, req, resp):
|
||||
resp.status_code = status_codes.HTTP_404
|
||||
@@ -276,6 +275,12 @@ class API:
|
||||
return req.text
|
||||
|
||||
async def graphql_response(self, req, resp, schema):
|
||||
show_graphiql = req.method.lower() == 'get' and req.accepts('text/html')
|
||||
|
||||
if show_graphiql:
|
||||
resp.content = self.template('graphiql.html', endpoint=req.url.path)
|
||||
return
|
||||
|
||||
query = await self._resolve_graphql_query(req)
|
||||
result = schema.execute(query)
|
||||
result, status_code = encode_execution_results(
|
||||
@@ -350,20 +355,13 @@ class API:
|
||||
# Give reference to self.
|
||||
values.update(api=self)
|
||||
|
||||
if auto_escape:
|
||||
env = jinja2.Environment(
|
||||
loader=jinja2.FileSystemLoader(
|
||||
str(self.templates_dir), followlinks=True
|
||||
),
|
||||
autoescape=jinja2.select_autoescape(["html", "xml"]),
|
||||
)
|
||||
else:
|
||||
env = jinja2.Environment(
|
||||
loader=jinja2.FileSystemLoader(
|
||||
str(self.templates_dir), followlinks=True
|
||||
),
|
||||
autoescape=jinja2.select_autoescape([]),
|
||||
)
|
||||
env = jinja2.Environment(
|
||||
loader=jinja2.FileSystemLoader(
|
||||
[str(self.templates_dir), str(self.built_in_templates_dir)],
|
||||
followlinks=True
|
||||
),
|
||||
autoescape=jinja2.select_autoescape(["html", "xml"] if auto_escape else []),
|
||||
)
|
||||
|
||||
template = env.get_template(name)
|
||||
return template.render(**values)
|
||||
|
||||
+28
-21
@@ -91,12 +91,7 @@ class Request:
|
||||
__slots__ = [
|
||||
"_starlette",
|
||||
"formats",
|
||||
"headers",
|
||||
"mimetype",
|
||||
"method",
|
||||
"full_url",
|
||||
"url",
|
||||
"params",
|
||||
"_headers",
|
||||
"_encoding",
|
||||
]
|
||||
|
||||
@@ -109,27 +104,39 @@ class Request:
|
||||
for header, value in self._starlette.headers.items():
|
||||
headers[header] = value
|
||||
|
||||
self.headers = (
|
||||
headers
|
||||
) #: A case-insensitive dictionary, containing all headers sent in the Request.
|
||||
self._headers = headers
|
||||
|
||||
self.mimetype = self.headers.get("Content-Type", "")
|
||||
@property
|
||||
def headers(self):
|
||||
"""A case-insensitive dictionary, containing all headers sent in the Request."""
|
||||
return self._headers
|
||||
|
||||
self.method = (
|
||||
self._starlette.method.lower()
|
||||
) #: The incoming HTTP method used for the request, lower-cased.
|
||||
@property
|
||||
def mimetype(self):
|
||||
return self.headers.get("Content-Type", "")
|
||||
|
||||
self.full_url = str(
|
||||
self._starlette.url
|
||||
) #: The full URL of the Request, query parameters and all.
|
||||
@property
|
||||
def method(self):
|
||||
"""The incoming HTTP method used for the request, lower-cased."""
|
||||
return self._starlette.method.lower()
|
||||
|
||||
self.url = rfc3986.urlparse(self.full_url) #: The parsed URL of the Request
|
||||
@property
|
||||
def full_url(self):
|
||||
"""The full URL of the Request, query parameters and all."""
|
||||
return str(self._starlette.url)
|
||||
|
||||
@property
|
||||
def url(self):
|
||||
"""The parsed URL of the Request."""
|
||||
return rfc3986.urlparse(self.full_url)
|
||||
|
||||
@property
|
||||
def params(self):
|
||||
"""A dictionary of the parsed query parameters used for the Request."""
|
||||
try:
|
||||
self.params = QueryDict(
|
||||
self.url.query
|
||||
) #: A dictionary of the parsed query parameters used for the Request.
|
||||
return QueryDict(self.url.query)
|
||||
except AttributeError:
|
||||
self.params = {}
|
||||
return QueryDict({})
|
||||
|
||||
@property
|
||||
async def encoding(self):
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import re
|
||||
from graphql import GraphQLSchema
|
||||
from parse import parse, search
|
||||
|
||||
|
||||
@@ -55,3 +57,11 @@ class Route:
|
||||
url = f"http://;{url}"
|
||||
|
||||
return url
|
||||
|
||||
def _weight(self):
|
||||
params_count = -len(set(re.findall(r'{([a-zA-Z]\w*)}', self.route)))
|
||||
return params_count != 0, params_count
|
||||
|
||||
@property
|
||||
def is_graphql(self):
|
||||
return isinstance(self.endpoint, GraphQLSchema)
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
{% set GRAPHIQL_VERSION = '0.12.0' %}
|
||||
|
||||
<!--
|
||||
* Copyright (c) Facebook, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
-->
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<style>
|
||||
body {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
#graphiql {
|
||||
height: 100vh;
|
||||
}
|
||||
</style>
|
||||
|
||||
<!--
|
||||
This GraphiQL example depends on Promise and fetch, which are available in
|
||||
modern browsers, but can be "polyfilled" for older browsers.
|
||||
GraphiQL itself depends on React DOM.
|
||||
If you do not want to rely on a CDN, you can host these files locally or
|
||||
include them directly in your favored resource bunder.
|
||||
-->
|
||||
<link href="//cdn.jsdelivr.net/npm/graphiql@{{ GRAPHIQL_VERSION }}/graphiql.css" rel="stylesheet"/>
|
||||
<script src="//cdn.jsdelivr.net/npm/whatwg-fetch@2.0.3/fetch.min.js"></script>
|
||||
<script src="//cdn.jsdelivr.net/npm/react@16.2.0/umd/react.production.min.js"></script>
|
||||
<script src="//cdn.jsdelivr.net/npm/react-dom@16.2.0/umd/react-dom.production.min.js"></script>
|
||||
<script src="//cdn.jsdelivr.net/npm/graphiql@{{ GRAPHIQL_VERSION }}/graphiql.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="graphiql">Loading...</div>
|
||||
<script>
|
||||
|
||||
/**
|
||||
* This GraphiQL example illustrates how to use some of GraphiQL's props
|
||||
* in order to enable reading and updating the URL parameters, making
|
||||
* link sharing of queries a little bit easier.
|
||||
*
|
||||
* This is only one example of this kind of feature, GraphiQL exposes
|
||||
* various React params to enable interesting integrations.
|
||||
*/
|
||||
|
||||
// Parse the search string to get url parameters.
|
||||
var search = window.location.search;
|
||||
var parameters = {};
|
||||
search.substr(1).split('&').forEach(function (entry) {
|
||||
var eq = entry.indexOf('=');
|
||||
if (eq >= 0) {
|
||||
parameters[decodeURIComponent(entry.slice(0, eq))] =
|
||||
decodeURIComponent(entry.slice(eq + 1));
|
||||
}
|
||||
});
|
||||
|
||||
// if variables was provided, try to format it.
|
||||
if (parameters.variables) {
|
||||
try {
|
||||
parameters.variables =
|
||||
JSON.stringify(JSON.parse(parameters.variables), null, 2);
|
||||
} catch (e) {
|
||||
// Do nothing, we want to display the invalid JSON as a string, rather
|
||||
// than present an error.
|
||||
}
|
||||
}
|
||||
|
||||
// When the query and variables string is edited, update the URL bar so
|
||||
// that it can be easily shared
|
||||
function onEditQuery(newQuery) {
|
||||
parameters.query = newQuery;
|
||||
updateURL();
|
||||
}
|
||||
|
||||
function onEditVariables(newVariables) {
|
||||
parameters.variables = newVariables;
|
||||
updateURL();
|
||||
}
|
||||
|
||||
function onEditOperationName(newOperationName) {
|
||||
parameters.operationName = newOperationName;
|
||||
updateURL();
|
||||
}
|
||||
|
||||
function updateURL() {
|
||||
var newSearch = '?' + Object.keys(parameters).filter(function (key) {
|
||||
return Boolean(parameters[key]);
|
||||
}).map(function (key) {
|
||||
return encodeURIComponent(key) + '=' +
|
||||
encodeURIComponent(parameters[key]);
|
||||
}).join('&');
|
||||
history.replaceState(null, null, newSearch);
|
||||
}
|
||||
|
||||
// Defines a GraphQL fetcher using the fetch API. You're not required to
|
||||
// use fetch, and could instead implement graphQLFetcher however you like,
|
||||
// as long as it returns a Promise or Observable.
|
||||
function graphQLFetcher(graphQLParams) {
|
||||
// This example expects a GraphQL server at the path /graphql.
|
||||
// Change this to point wherever you host your GraphQL server.
|
||||
return fetch('{{ endpoint }}', {
|
||||
method: 'post',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(graphQLParams),
|
||||
credentials: 'include',
|
||||
}).then(function (response) {
|
||||
return response.text();
|
||||
}).then(function (responseBody) {
|
||||
try {
|
||||
return JSON.parse(responseBody);
|
||||
} catch (error) {
|
||||
return responseBody;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Render <GraphiQL /> into the body.
|
||||
// See the README in the top level of this module to learn more about
|
||||
// how you can customize GraphiQL by providing different values or
|
||||
// additional child elements.
|
||||
ReactDOM.render(
|
||||
React.createElement(GraphiQL, {
|
||||
fetcher: graphQLFetcher,
|
||||
query: parameters.query,
|
||||
variables: parameters.variables,
|
||||
operationName: parameters.operationName,
|
||||
onEditQuery: onEditQuery,
|
||||
onEditVariables: onEditVariables,
|
||||
onEditOperationName: onEditOperationName
|
||||
}),
|
||||
document.getElementById('graphiql')
|
||||
);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -149,6 +149,15 @@ def test_request_and_get(api, session):
|
||||
assert "LIFE" in r.headers
|
||||
|
||||
|
||||
def test_class_based_view_status_code(api):
|
||||
@api.route("/")
|
||||
class ThingsResource:
|
||||
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
|
||||
|
||||
|
||||
def test_query_params(api, url, session):
|
||||
@api.route("/")
|
||||
def route(req, resp):
|
||||
@@ -235,6 +244,13 @@ def test_graphql_schema_json_query(api, schema):
|
||||
r = api.session().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"})
|
||||
assert r.ok
|
||||
assert 'GraphiQL' in r.text
|
||||
|
||||
|
||||
def test_json_uploads(api, session):
|
||||
@api.route("/")
|
||||
|
||||
Reference in New Issue
Block a user