mirror of
https://github.com/kennethreitz/responder.git
synced 2026-06-05 23:00:17 +00:00
Compare commits
35 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 838d172512 | |||
| 2c02c51c37 | |||
| 67a4cbca2c | |||
| a2f97e727f | |||
| 462506113e | |||
| 5f2a72203f | |||
| d6febe2d02 | |||
| c2bd1e989a | |||
| f886c2c050 | |||
| ae770e603a | |||
| 7b79472d65 | |||
| 090a3a571b | |||
| f9d55fc425 | |||
| 4f57e8a5d1 | |||
| 1e6c9d935a | |||
| 00cfde169b | |||
| 02733ac718 | |||
| 55b55e62da | |||
| 5fccedd4c4 | |||
| b9ad78ec79 | |||
| 64ac6bcd1f | |||
| 45e4d80c4d | |||
| a5b1652d15 | |||
| f954eb7d88 | |||
| 53216813e5 | |||
| 1618203930 | |||
| 237a2ed426 | |||
| d33289503a | |||
| f5ff4c9725 | |||
| 62f932dcfc | |||
| b66112d0ca | |||
| b98354e63a | |||
| 94b3625718 | |||
| 6b93125ff2 | |||
| 43faef4569 |
@@ -1,3 +1,10 @@
|
||||
# v1.0.0
|
||||
- Move GraphQL support into a built-in plugin.
|
||||
|
||||
# v0.3.3
|
||||
- Improved exceptions.
|
||||
- CORS support.
|
||||
|
||||
# v0.3.2
|
||||
- Subtle improvements.
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ if __name__ == '__main__':
|
||||
api.run()
|
||||
```
|
||||
|
||||
That `async` declaration is optional. [View documentation](http://python-responder.org).
|
||||
Powered by [Starlette](https://www.starlette.io/). That `async` declaration is optional. [View documentation](http://python-responder.org).
|
||||
|
||||
This gets you a ASGI app, with a production static files server pre-installed, jinja2 templating (without additional imports), and a production webserver based on uvloop, serving up requests with gzip compression automatically.
|
||||
|
||||
@@ -41,81 +41,7 @@ This gets you a ASGI app, with a production static files server pre-installed, j
|
||||
|
||||
## More Examples
|
||||
|
||||
Class-based views (and setting some headers and stuff):
|
||||
|
||||
```python
|
||||
@api.route("/{greeting}")
|
||||
class GreetingResource:
|
||||
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
|
||||
```
|
||||
|
||||
Render a template, with arguments:
|
||||
|
||||
```python
|
||||
@api.route("/{greeting}")
|
||||
def greet_world(req, resp, *, greeting):
|
||||
resp.content = api.template("index.html", greeting=greeting)
|
||||
```
|
||||
|
||||
The `api` instance is available as an object during template rendering.
|
||||
|
||||
Here, you can spawn off a background thread to run any function, out-of-request:
|
||||
|
||||
```python
|
||||
@api.route("/")
|
||||
def hello(req, resp):
|
||||
|
||||
@api.background.task
|
||||
def sleep(s=10):
|
||||
time.sleep(s)
|
||||
print("slept!")
|
||||
|
||||
sleep()
|
||||
resp.content = "processing"
|
||||
```
|
||||
|
||||
And even serve a GraphQL API:
|
||||
|
||||
```python
|
||||
import graphene
|
||||
|
||||
class Query(graphene.ObjectType):
|
||||
hello = graphene.String(name=graphene.String(default_value="stranger"))
|
||||
|
||||
def resolve_hello(self, info, name):
|
||||
return f"Hello {name}"
|
||||
|
||||
api.add_route("/graph", graphene.Schema(query=Query))
|
||||
```
|
||||
|
||||
We can then send a query to our service:
|
||||
|
||||
```pycon
|
||||
>>> requests = api.session()
|
||||
>>> r = requests.get("http://;/graph", params={"query": "{ hello }"})
|
||||
>>> r.json()
|
||||
{'data': {'hello': 'Hello stranger'}}
|
||||
```
|
||||
|
||||
Or, request YAML back:
|
||||
|
||||
```pycon
|
||||
>>> r = requests.get("http://;/graph", params={"query": "{ hello(name:\"john\") }"}, headers={"Accept": "application/x-yaml"})
|
||||
>>> print(r.text)
|
||||
data: {hello: Hello john}
|
||||
|
||||
```
|
||||
|
||||
Want HSTS?
|
||||
|
||||
```
|
||||
api = responder.API(enable_hsts=True)
|
||||
```
|
||||
|
||||
Boom.
|
||||
See [the documentation's feature tour](http://python-responder.org/en/latest/tour.html) for more details on features available in Responder.
|
||||
|
||||
|
||||
# Installing Responder
|
||||
@@ -154,15 +80,7 @@ The primary concept here is to bring the niceties that are brought forth from bo
|
||||
- A production static file server is built-in.
|
||||
- Uvicorn built-in as a production web server. I would have chosen Gunicorn, but it doesn't run on Windows. Plus, Uvicorn serves well to protect against slowloris attacks, making nginx unnecessary in production.
|
||||
- GraphQL support, via Graphene. The goal here is to have any GraphQL query exposable at any route, magically.
|
||||
|
||||
## Future Ideas
|
||||
|
||||
- Cookie-based sessions are currently an afterthought, as this is an API framework, but websites are APIs too.
|
||||
- If frontend websites are supported, provide an official way to run webpack.
|
||||
|
||||
# The Goal
|
||||
|
||||
The primary goal here is to learn, not to get adoption. Though, who knows how these things will pan out.
|
||||
- Provide an official way to run webpack.
|
||||
|
||||
|
||||
----------
|
||||
|
||||
@@ -30,7 +30,7 @@ The basics::
|
||||
|
||||
Install Responder::
|
||||
|
||||
$ pipenv install responder
|
||||
$ pipenv install responder --pre
|
||||
...
|
||||
|
||||
Write out an ``api.py``::
|
||||
|
||||
@@ -37,7 +37,7 @@ spread some `Hacktoberfest <https://hacktoberfest.digitalocean.com/>`_ spirit ar
|
||||
if __name__ == '__main__':
|
||||
api.run()
|
||||
|
||||
That ``async`` declaration is optional.
|
||||
Powered by `Starlette <https://www.starlette.io/>`_. That ``async`` declaration is optional.
|
||||
|
||||
This gets you a ASGI app, with a production static files server (WhiteNoise)
|
||||
pre-installed, jinja2 templating (without additional imports), and a
|
||||
@@ -144,14 +144,6 @@ Ideas
|
||||
- GraphQL support, via Graphene. The goal here is to have any GraphQL query exposable at any route, magically.
|
||||
|
||||
|
||||
Future Ideas
|
||||
------------
|
||||
|
||||
- Cookie-based sessions are currently an afterthought, as this is an API framework, but websites are APIs too.
|
||||
- If frontend websites are supported, provide an official way to run webpack.
|
||||
|
||||
|
||||
|
||||
Indices and tables
|
||||
==================
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ Next, we can run our web service easily, with ``api.run()``::
|
||||
|
||||
This will spin up a production web server on port ``5042``, ready for incoming HTTP requests.
|
||||
|
||||
Note: you can pass ``port=5000`` if you want to customize the port. The ``PORT`` environment variable for established web service providers (e.g. Heroku) will automatically be honored.
|
||||
Note: you can pass ``port=5000`` if you want to customize the port. The ``PORT`` environment variable for established web service providers (e.g. Heroku) will automatically be honored and will set the listening address to ``0.0.0.0`` automatically (also configurable through the ``address`` keyword argument).
|
||||
|
||||
|
||||
Accept Route Arguments
|
||||
|
||||
+26
-1
@@ -45,7 +45,10 @@ Serve a GraphQL API::
|
||||
def resolve_hello(self, info, name):
|
||||
return f"Hello {name}"
|
||||
|
||||
api.add_route("/graph", graphene.Schema(query=Query))
|
||||
schema = graphene.Schema(query=Query)
|
||||
view = responder.ext.GraphQLView(query=query, api=api)
|
||||
|
||||
api.add_route("/graph", view)
|
||||
|
||||
Visiting the endpoint will render a *GraphiQL* instance, in the browser.
|
||||
|
||||
@@ -204,3 +207,25 @@ Want HSTS (to redirect all traffic to HTTPS)?
|
||||
|
||||
|
||||
Boom.
|
||||
|
||||
CORS
|
||||
----
|
||||
|
||||
Want `CORS <https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS/>`_ ?
|
||||
|
||||
::
|
||||
|
||||
api = responder.API(cors=True)
|
||||
|
||||
|
||||
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 pass the ``cors_params`` argument, a dictionnary 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'``.
|
||||
* ``allow_methods`` - A list of HTTP methods that should be allowed for cross-origin requests. Defaults to `['GET']`. You can use ``['*']`` to allow all standard methods.
|
||||
* ``allow_headers`` - A list of HTTP request headers that should be supported for cross-origin requests. Defaults to ``[]``. You can use ``['*']`` to allow all headers. The ``Accept``, ``Accept-Language``, ``Content-Language`` and ``Content-Type`` headers are always allowed for CORS requests.
|
||||
* ``allow_credentials`` - Indicate that cookies should be supported for cross-origin requests. Defaults to ``False``.
|
||||
* ``expose_headers`` - Indicate any response headers that should be made accessible to the browser. Defaults to ``[]``.
|
||||
* ``max_age`` - Sets a maximum time in seconds for browsers to cache CORS responses. Defaults to ``60``.
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
from .core import *
|
||||
from . import ext
|
||||
|
||||
@@ -1 +1 @@
|
||||
__version__ = "0.3.2"
|
||||
__version__ = "1.0.0"
|
||||
|
||||
+81
-76
@@ -1,32 +1,41 @@
|
||||
import os
|
||||
import json
|
||||
from functools import partial
|
||||
import os
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import uvicorn
|
||||
import apistar
|
||||
import yaml
|
||||
import jinja2
|
||||
import itsdangerous
|
||||
from graphql_server import encode_execution_results, json_encode, default_format_error
|
||||
from starlette.websockets import WebSocket
|
||||
import jinja2
|
||||
import uvicorn
|
||||
import yaml
|
||||
from apispec import APISpec, yaml_utils
|
||||
from apispec.ext.marshmallow import MarshmallowPlugin
|
||||
from asgiref.wsgi import WsgiToAsgi
|
||||
from starlette.debug import DebugMiddleware
|
||||
from starlette.testclient import TestClient
|
||||
from starlette.exceptions import ExceptionMiddleware
|
||||
from starlette.lifespan import LifespanHandler
|
||||
from starlette.middleware.cors import CORSMiddleware
|
||||
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
|
||||
from asgiref.wsgi import WsgiToAsgi
|
||||
from starlette.routing import Router
|
||||
from starlette.staticfiles import StaticFiles
|
||||
from starlette.testclient import TestClient
|
||||
from starlette.websockets import WebSocket
|
||||
from whitenoise import WhiteNoise
|
||||
|
||||
from . import models
|
||||
from . import status_codes
|
||||
from .routes import Route
|
||||
from .formats import get_formats
|
||||
from . import models, status_codes
|
||||
from .background import BackgroundQueue
|
||||
from .formats import get_formats
|
||||
from .routes import Route
|
||||
from .statics import (
|
||||
DEFAULT_API_THEME,
|
||||
DEFAULT_CORS_PARAMS,
|
||||
DEFAULT_SECRET_KEY,
|
||||
DEFAULT_SESSION_COOKIE,
|
||||
)
|
||||
from .templates import GRAPHIQL
|
||||
|
||||
|
||||
# TODO: consider moving status codes here
|
||||
class API:
|
||||
"""The primary web-service class.
|
||||
@@ -51,9 +60,11 @@ class API:
|
||||
static_route="/static",
|
||||
templates_dir="templates",
|
||||
auto_escape=True,
|
||||
secret_key="NOTASECRET",
|
||||
secret_key=DEFAULT_SECRET_KEY,
|
||||
enable_hsts=False,
|
||||
docs_route=None,
|
||||
cors=False,
|
||||
cors_params=DEFAULT_CORS_PARAMS,
|
||||
):
|
||||
self.secret_key = secret_key
|
||||
self.title = title
|
||||
@@ -66,13 +77,14 @@ class API:
|
||||
os.path.abspath(os.path.dirname(__file__) + "/templates")
|
||||
)
|
||||
self.routes = {}
|
||||
self.docs_theme = "swaggerui"
|
||||
self.docs_theme = DEFAULT_API_THEME
|
||||
self.docs_route = docs_route
|
||||
self.schemas = {}
|
||||
self.session_cookie = "Responder-Session"
|
||||
self.session_cookie = DEFAULT_SESSION_COOKIE
|
||||
|
||||
self.hsts_enabled = enable_hsts
|
||||
|
||||
self.cors = cors
|
||||
self.cors_params = cors_params
|
||||
# Make the static/templates directory if they don't exist.
|
||||
for _dir in (self.static_dir, self.templates_dir):
|
||||
os.makedirs(_dir, exist_ok=True)
|
||||
@@ -108,8 +120,14 @@ class API:
|
||||
self.add_middleware(GZipMiddleware)
|
||||
if debug:
|
||||
self.add_middleware(DebugMiddleware)
|
||||
|
||||
if self.hsts_enabled:
|
||||
self.add_middleware(HTTPSRedirectMiddleware)
|
||||
self.lifespan_handler = LifespanHandler()
|
||||
|
||||
if self.cors:
|
||||
self.add_middleware(CORSMiddleware, **self.cors_params)
|
||||
self.add_middleware(ExceptionMiddleware, debug=debug)
|
||||
|
||||
# Jinja enviroment
|
||||
self.jinja_env = jinja2.Environment(
|
||||
@@ -157,6 +175,9 @@ class API:
|
||||
self.app = middleware_cls(self.app, **middleware_config)
|
||||
|
||||
def __call__(self, scope):
|
||||
if scope["type"] == "lifespan":
|
||||
return self.lifespan_handler(scope)
|
||||
|
||||
path = scope["path"]
|
||||
root_path = scope.get("root_path", "")
|
||||
|
||||
@@ -259,10 +280,7 @@ class API:
|
||||
|
||||
params = route.incoming_matches(req.url.path)
|
||||
|
||||
if route.is_graphql:
|
||||
await self.graphql_response(req, resp, schema=route.endpoint)
|
||||
|
||||
elif route.is_function:
|
||||
if route.is_function:
|
||||
try:
|
||||
try:
|
||||
# Run the view.
|
||||
@@ -274,12 +292,16 @@ class API:
|
||||
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:
|
||||
view = route.endpoint()
|
||||
try:
|
||||
view = route.endpoint()
|
||||
except TypeError:
|
||||
view = route.endpoint
|
||||
|
||||
# Run on_request first.
|
||||
try:
|
||||
@@ -290,8 +312,9 @@ class API:
|
||||
# If it's async, await it.
|
||||
if hasattr(r, "send"):
|
||||
await r
|
||||
except Exception as e:
|
||||
except Exception:
|
||||
self.default_response(req, resp, error=True)
|
||||
raise
|
||||
|
||||
# Then on_get.
|
||||
method = req.method
|
||||
@@ -319,6 +342,15 @@ class API:
|
||||
|
||||
return resp
|
||||
|
||||
def add_event_handler(self, event_type, handler):
|
||||
"""Add a 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.
|
||||
"""
|
||||
|
||||
self.lifespan_handler.add_event_handler(event_type, handler)
|
||||
|
||||
def add_route(
|
||||
self,
|
||||
route,
|
||||
@@ -332,7 +364,7 @@ class API:
|
||||
"""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 endpoint: The endpoint for the route -- can be a callable, or a class.
|
||||
: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.
|
||||
@@ -347,13 +379,6 @@ 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, websocket=websocket)
|
||||
# TODO: A better data structure or sort it once the app is loaded
|
||||
self.routes = dict(
|
||||
@@ -407,54 +432,31 @@ class API:
|
||||
resp.text = f"Redirecting to: {location}"
|
||||
resp.headers.update({"Location": location})
|
||||
|
||||
@staticmethod
|
||||
async def _resolve_graphql_query(req):
|
||||
# TODO: Get variables and operation_name from form data, params, request text?
|
||||
def on_event(self, event_type: str, **args):
|
||||
"""Decorator for registering functions or coroutines to run at certain events
|
||||
Supported events: startup, cleanup, shutdown, tick
|
||||
|
||||
if "json" in req.mimetype:
|
||||
json_media = await req.media("json")
|
||||
return (
|
||||
json_media["query"],
|
||||
json_media.get("variables"),
|
||||
json_media.get("operationName"),
|
||||
)
|
||||
Usage::
|
||||
|
||||
# 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"], None, None
|
||||
# if "q" in req.media("form"):
|
||||
# return req.media("form")["q"], None, None
|
||||
@api.on_event('startup')
|
||||
async def open_database_connection_pool():
|
||||
...
|
||||
|
||||
# Support query/q in params.
|
||||
if "query" in req.params:
|
||||
return req.params["query"], None, None
|
||||
if "q" in req.params:
|
||||
return req.params["q"], None, None
|
||||
@api.on_event('tick', seconds=10)
|
||||
async def do_stuff():
|
||||
...
|
||||
|
||||
# Otherwise, the request text is used (typical).
|
||||
# TODO: Make some assertions about content-type here.
|
||||
return req.text, None, None
|
||||
@api.on_event('cleanup')
|
||||
async def close_database_connection_pool():
|
||||
...
|
||||
|
||||
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
|
||||
def decorator(func):
|
||||
self.add_event_handler(event_type, func, **args)
|
||||
return func
|
||||
|
||||
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,
|
||||
format_error=default_format_error,
|
||||
encode=partial(json_encode, pretty=False),
|
||||
)
|
||||
resp.media = json.loads(result)
|
||||
return (query, result, status_code)
|
||||
return decorator
|
||||
|
||||
def route(self, route, **options):
|
||||
"""Decorator for creating new routes around function and class definitions.
|
||||
@@ -591,4 +593,7 @@ class API:
|
||||
if port is None:
|
||||
port = 5042
|
||||
|
||||
uvicorn.run(self, host=address, port=port, debug=debug, **options)
|
||||
def spawn():
|
||||
uvicorn.run(self, host=address, port=port, debug=debug, **options)
|
||||
|
||||
spawn()
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
from .graphql import GraphQLView
|
||||
@@ -0,0 +1,64 @@
|
||||
import json
|
||||
from functools import partial
|
||||
|
||||
from graphql_server import default_format_error, encode_execution_results, json_encode
|
||||
|
||||
from ..templates import GRAPHIQL
|
||||
|
||||
|
||||
class GraphQLView:
|
||||
def __init__(self, *, api, schema):
|
||||
self.api = api
|
||||
self.schema = schema
|
||||
|
||||
@staticmethod
|
||||
async def _resolve_graphql_query(req):
|
||||
# TODO: Get variables and operation_name from form data, params, request text?
|
||||
|
||||
if "json" in req.mimetype:
|
||||
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"], None, None
|
||||
# if "q" in req.media("form"):
|
||||
# return req.media("form")["q"], None, None
|
||||
|
||||
# Support query/q in params.
|
||||
if "query" in req.params:
|
||||
return req.params["query"], None, None
|
||||
if "q" in req.params:
|
||||
return req.params["q"], None, None
|
||||
|
||||
# Otherwise, the request text is used (typical).
|
||||
# TODO: Make some assertions about content-type here.
|
||||
return req.text, None, None
|
||||
|
||||
async def graphql_response(self, req, resp, schema):
|
||||
show_graphiql = req.method == "get" and req.accepts("text/html")
|
||||
|
||||
if show_graphiql:
|
||||
resp.content = self.api.template_string(GRAPHIQL, endpoint=req.url.path)
|
||||
return
|
||||
|
||||
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,
|
||||
format_error=default_format_error,
|
||||
encode=partial(json_encode, pretty=False),
|
||||
)
|
||||
resp.media = json.loads(result)
|
||||
return (query, result, status_code)
|
||||
|
||||
async def on_request(self, req, resp):
|
||||
await self.graphql_response(req, resp, self.schema)
|
||||
+2
-8
@@ -15,7 +15,7 @@ def memoize(f):
|
||||
class Route:
|
||||
_param_pattern = re.compile(r"{([^{}]*)}")
|
||||
|
||||
def __init__(self, route, endpoint, websocket=False):
|
||||
def __init__(self, route, endpoint, *, websocket=False):
|
||||
self.route = route
|
||||
self.endpoint = endpoint
|
||||
self.uses_websocket = websocket
|
||||
@@ -65,18 +65,12 @@ class Route:
|
||||
params_count = len(params)
|
||||
return params_count != 0, -params_count
|
||||
|
||||
@property
|
||||
def is_graphql(self):
|
||||
return hasattr(self.endpoint, "get_graphql_type")
|
||||
|
||||
@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((callable(self.endpoint), code, kwdefaults))
|
||||
|
||||
@@ -1 +1,14 @@
|
||||
DEFAULT_ENCODING = "utf-8"
|
||||
DEFAULT_API_THEME = "swaggerui"
|
||||
DEFAULT_SESSION_COOKIE = "Responder-Session"
|
||||
DEFAULT_SECRET_KEY = "NOTASECRET"
|
||||
|
||||
DEFAULT_CORS_PARAMS = {
|
||||
"allow_origins": (),
|
||||
"allow_methods": ("GET",),
|
||||
"allow_headers": (),
|
||||
"allow_credentials": False,
|
||||
"allow_origin_regex": None,
|
||||
"expose_headers": (),
|
||||
"max_age": 600,
|
||||
}
|
||||
|
||||
+2
-1
@@ -2,6 +2,8 @@ import graphene
|
||||
import responder
|
||||
from pathlib import Path
|
||||
import pytest
|
||||
import multiprocessing
|
||||
import concurrent.futures
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -44,7 +46,6 @@ def flask():
|
||||
|
||||
return app
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def schema():
|
||||
class Query(graphene.ObjectType):
|
||||
|
||||
+38
-3
@@ -1,8 +1,13 @@
|
||||
import concurrent
|
||||
|
||||
import pytest
|
||||
import yaml
|
||||
import responder
|
||||
import requests
|
||||
import io
|
||||
|
||||
from starlette.responses import PlainTextResponse
|
||||
|
||||
|
||||
def test_api_basic_route(api):
|
||||
@api.route("/")
|
||||
@@ -122,7 +127,7 @@ def test_yaml_media(api):
|
||||
|
||||
|
||||
def test_graphql_schema_query_querying(api, schema):
|
||||
api.add_route("/", schema)
|
||||
api.add_route("/", responder.ext.GraphQLView(schema=schema, api=api))
|
||||
|
||||
r = api.requests.get("http://;/?q={ hello }", headers={"Accept": "json"})
|
||||
assert r.json() == {"data": {"hello": "Hello stranger"}}
|
||||
@@ -250,14 +255,14 @@ def test_multiple_routes(api):
|
||||
|
||||
|
||||
def test_graphql_schema_json_query(api, schema):
|
||||
api.add_route("/", schema)
|
||||
api.add_route("/", responder.ext.GraphQLView(schema=schema, api=api))
|
||||
|
||||
r = api.requests.post("http://;/", json={"query": "{ hello }"})
|
||||
assert r.ok
|
||||
|
||||
|
||||
def test_graphiql(api, schema):
|
||||
api.add_route("/", schema)
|
||||
api.add_route("/", responder.ext.GraphQLView(schema=schema, api=api))
|
||||
|
||||
r = api.requests.get("http://;/", headers={"Accept": "text/html"})
|
||||
assert r.ok
|
||||
@@ -460,12 +465,18 @@ def test_file_uploads(api):
|
||||
|
||||
|
||||
def test_500(api):
|
||||
def catcher(request, exc):
|
||||
return PlainTextResponse("Suppressed error", 500)
|
||||
|
||||
api.app.add_exception_handler(ValueError, catcher)
|
||||
|
||||
@api.route("/")
|
||||
def view(req, resp):
|
||||
raise ValueError
|
||||
|
||||
r = api.requests.get(api.url_for(view))
|
||||
assert not r.ok
|
||||
assert r.content == b"Suppressed error"
|
||||
|
||||
|
||||
def test_404(api):
|
||||
@@ -480,3 +491,27 @@ def test_kinda_websockets(api):
|
||||
await ws.accept()
|
||||
await ws.send_text("Hello via websocket!")
|
||||
await ws.close()
|
||||
|
||||
|
||||
@pytest.mark.xfail
|
||||
def test_startup(api, session):
|
||||
who = [None]
|
||||
|
||||
@api.route("/{greeting}")
|
||||
async def greet_world(req, resp, *, greeting):
|
||||
resp.text = f"{greeting}, {who[0]}!"
|
||||
|
||||
@api.on_event("startup")
|
||||
async def asd():
|
||||
who[0] = "world"
|
||||
print("startup")
|
||||
|
||||
@api.on_event("cleanup")
|
||||
async def asd():
|
||||
print("cleanup")
|
||||
|
||||
pool = concurrent.futures.ThreadPoolExecutor(max_workers=2)
|
||||
f = pool.submit(api.run)
|
||||
|
||||
r = requests.get(f"http://localhost:5042/hello")
|
||||
assert r.text == "hello, world!"
|
||||
|
||||
Reference in New Issue
Block a user