Compare commits

..

41 Commits

Author SHA1 Message Date
kennethreitz cee5ca8873 v0.1.1 2018-10-17 06:01:41 -07:00
kennethreitz d961d4ab43 default routes 2018-10-17 06:01:27 -07:00
kennethreitz 5205150a89 default route 2018-10-17 05:53:23 -07:00
kennethreitz 48e58cde5d docker 2018-10-17 05:19:22 -07:00
kennethreitz 033e91f8df name 2018-10-17 05:15:25 -07:00
kennethreitz aab3705897 myapi 2018-10-17 05:14:19 -07:00
kennethreitz d02efa81f2 deployment 2018-10-17 05:12:11 -07:00
kennethreitz 95a8240da7 single-page webapps 2018-10-17 04:58:11 -07:00
kennethreitz dd0ddab610 single page 2018-10-17 04:52:02 -07:00
kennethreitz d23ac10f90 version 2018-10-17 04:49:00 -07:00
kennethreitz ec18290b8a changelog 2018-10-17 04:48:38 -07:00
kennethreitz 2c4cd39dc9 static application support 2018-10-17 04:48:33 -07:00
kennethreitz 830bad0b85 docs for #53 2018-10-17 04:47:39 -07:00
kennethreitz f14ef6fa15 #53 2018-10-17 04:45:12 -07:00
kennethreitz 7400b1c83d static support #53 2018-10-17 04:38:51 -07:00
kennethreitz e7caf39fba static_route 2018-10-17 04:25:09 -07:00
kennethreitz 09fd0fb0ca version 2018-10-17 04:19:38 -07:00
kennethreitz 72adb13c0f version 2018-10-17 04:16:22 -07:00
kennethreitz ea0e382f82 test for #71 2018-10-17 04:15:36 -07:00
kennethreitz e70cba5143 Fix for #71 2018-10-17 04:12:13 -07:00
kennethreitz 8aec244c31 openapi 2018-10-17 04:12:03 -07:00
kennethreitz 60e163164f v0.0.8 2018-10-17 03:58:23 -07:00
kennethreitz 86b9b5f3fa graphiql 2018-10-17 03:57:39 -07:00
kennethreitz 401a208767 changelog 2018-10-17 03:27:26 -07:00
kennethreitz 7d1f991ce4 changelog 2018-10-17 02:52:22 -07:00
kennethreitz 1b10378f58 merge 2018-10-17 02:33:49 -07:00
kennethreitz 2bbb379994 Merge pull request #70 from rpost/patch-1
Fix typo
2018-10-17 05:27:59 -04:00
kennethreitz a835f119e1 Merge pull request #67 from goodbadwolf/patch-1
Fix typo in quickstart example
2018-10-17 05:27:47 -04:00
kennethreitz 91d8bac680 Merge pull request #65 from taoufik07/routes_matching
Routes matching for humans
2018-10-17 05:27:32 -04:00
kennethreitz 3db10a4ce8 Merge pull request #63 from pesap/fix-typo
Fix typo in docs
2018-10-17 05:26:19 -04:00
kennethreitz 590640645b Merge pull request #62 from ybv/master
Add status code test for class based view
2018-10-17 05:26:03 -04:00
kennethreitz 7f02bfdf0c Merge pull request #61 from ewjoachim/patch-1
Fix typo
2018-10-17 05:25:49 -04:00
Radek Postołowicz e5cef0d9c0 Fix typo 2018-10-17 10:08:59 +02:00
ArtemGordinsky 85f9c33b2b Integrate GraphiQL 2018-10-17 08:00:03 +02:00
Manish P Mathai 148a430da4 Fix typo in quickstart example 2018-10-16 22:36:54 -07:00
Taoufik f7657679ac A verbose name 2018-10-17 05:07:29 +01:00
taoufik07 f0479019c3 Order the routes based on the weight 2018-10-17 04:38:08 +01:00
taoufik07 a9a4ceaa78 Add weight to Route 2018-10-17 04:37:31 +01:00
pesap c55c905621 Fix typo 2018-10-16 17:23:47 -07:00
ybv 4db2289b7e Add status code rest for class based view 2018-10-16 22:39:09 +05:30
Joachim Jablon 93172ea1d0 Fix typo 2018-10-16 14:41:30 +02:00
11 changed files with 329 additions and 33 deletions
+16
View File
@@ -1,3 +1,19 @@
# v0.1.1
- Default routes.
# v0.1.0
- Prototype of static application support.
# v0.0.10
- Bufgix for async class-based views.
# v0.0.9
- Bugfix for async class-based views.
# v0.0.8
- GraphiQL Support.
- Improvement to route selection.
# v0.0.7
- Immutable Request object.
+57
View File
@@ -0,0 +1,57 @@
Deploying Responder
===================
You can deploy Responder anywhere you can deploy a basic Python application.
Docker Deployment
-----------------
Assuming existing ``api.py`` and ``Pipfile.lock`` containing ``responder``.
``Dockerfile``::
from kenethreitz/pipenv
COPY . /app
CMD python3 api.py
That's it!
Heroku Deployment
-----------------
The basics::
$ mkdir my-api
$ cd my-api
$ git init
$ heroku create
...
Install Responder::
$ pipenv install responder
...
Write out a ``api.py``::
import responder
api = responder.API()
@api.route("/")
async def hello(req, resp):
resp.text = "hello, world!"
if __name__ == "__main__":
api.run()
Write out a ``Procfile``::
web: python api.py
That's it! Next, we commit and push to Heroku::
$ git add -A
$ git commit -m 'initial commit'
$ git push heroku master
+5 -2
View File
@@ -51,10 +51,12 @@ 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!
- GraphQL (with *GraphiQL*) support!
- OpenAPI schema generation.
- Single-page webapp support!
Testimonials
------------
@@ -102,6 +104,7 @@ User Guides
quickstart
tour
deployment
api
+1 -1
View File
@@ -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)
+11
View File
@@ -47,6 +47,8 @@ Serve a GraphQL API::
api.add_route("/graph", graphene.Schema(query=Query))
Visiting the endpoint will render a *GraphiQL* instance, in the browser.
Built-in Testing Client (Requests)
----------------------------------
@@ -138,6 +140,15 @@ Responder gives you the ability to mount another ASGI / WSGI app at a subroute::
That's it!
Single-Page Web Apps
--------------------
If you have a single-page webapp, you can tell Responder to serve up your ``static/index.html`` at a route, like so::
api.add_route("/", static=True)
This will make ``index.html`` the default response to all undefined routes.
HSTS (Redirect to HTTPS)
------------------------
+1 -1
View File
@@ -1 +1 @@
__version__ = "0.0.7"
__version__ = "0.1.1"
+56 -29
View File
@@ -21,7 +21,7 @@ from . import status_codes
from .routes import Route
from .formats import get_formats
from .background import BackgroundQueue
from .templates import GRAPHIQL
# TODO: consider moving status codes here
class API:
@@ -42,6 +42,7 @@ class API:
openapi=None,
openapi_route="/schema.yml",
static_dir="static",
static_route="/static",
templates_dir="templates",
enable_hsts=False,
):
@@ -49,8 +50,11 @@ class API:
self.version = version
self.openapi_version = openapi
self.static_dir = Path(os.path.abspath(static_dir))
self.static_route = f"/{static_dir}"
self.static_route = static_route
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 = {}
@@ -71,6 +75,8 @@ class API:
if self.openapi_version:
self.add_route(openapi_route, self.schema_response)
self.default_endpoint = None
@property
def _apispec(self):
spec = APISpec(
@@ -180,11 +186,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(
@@ -196,15 +201,19 @@ class API:
# Run on_request first.
try:
getattr(view, "on_request")(req, resp)
r = getattr(view, "on_request")(req, resp)
if hasattr(r, "send"):
await r
except AttributeError:
pass
# Then on_get.
method = req.method.lower()
method = req.method
try:
getattr(view, f"on_{method}")(req, resp)
r = getattr(view, f"on_{method}")(req, resp)
if hasattr(r, "send"):
await r
except AttributeError:
pass
else:
@@ -212,23 +221,42 @@ class API:
return resp
def add_route(self, route, endpoint, *, check_existing=True):
# TODO: add graphiql
def add_route(self, route, endpoint=None, *, default=False, static=False, check_existing=True):
"""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, a WSGI application, or graphene schema (GraphQL).
: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 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 check_existing:
assert route not in self.routes
# TODO: Support grpahiql.
if not endpoint and static:
endpoint = self.static_response
default = True
if default:
self.default_endpoint = endpoint
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
resp.text = "Not found."
if self.default_endpoint:
self.default_endpoint(req, resp)
else:
resp.status_code = status_codes.HTTP_404
resp.text = "Not found."
def static_response(self, req, resp):
index = (self.static_dir / "index.html").resolve()
if os.path.exists(index):
with open(index, "r") as f:
resp.text = f.read()
def schema_response(self, req, resp):
resp.status_code = status_codes.HTTP_200
@@ -276,6 +304,12 @@ class API:
return req.text
async def graphql_response(self, req, resp, schema):
show_graphiql = req.method == "get" and req.accepts("text/html")
if show_graphiql:
resp.content = self.template_string(GRAPHIQL, endpoint=req.url.path)
return
query = await self._resolve_graphql_query(req)
result = schema.execute(query)
result, status_code = encode_execution_results(
@@ -350,20 +384,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)
+9
View File
@@ -1,3 +1,4 @@
import re
from parse import parse, search
@@ -55,3 +56,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 hasattr(self.endpoint, "get_graphql_type")
+145
View File
@@ -0,0 +1,145 @@
GRAPHIQL = """
{% 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>
""".strip()
+1
View File
@@ -0,0 +1 @@
lorem
+27
View File
@@ -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("/")
@@ -332,3 +348,14 @@ def test_mount_wsgi_app(api, flask, session):
r = session.get("http://;/flask")
assert r.ok
def test_async_class_based_views(api, session):
@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)
assert r.text == data