mirror of
https://github.com/kennethreitz/responder.git
synced 2026-06-05 23:00:17 +00:00
Compare commits
112 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c8392b65b6 | |||
| c0ace9c2e5 | |||
| dfcab7dcbf | |||
| eb0870deb1 | |||
| 5b7ef34523 | |||
| 6ec728e466 | |||
| f12a562a08 | |||
| 17c4c95593 | |||
| 9b72c90944 | |||
| ec34da60a1 | |||
| daa4b6368a | |||
| 931a7a1a6c | |||
| 69d5790078 | |||
| 7571c18a55 | |||
| ff7ce9bdd0 | |||
| e5fc801899 | |||
| b362aa6813 | |||
| 652b961ac8 | |||
| 652713aec4 | |||
| 387b2f166b | |||
| 164b4a056a | |||
| 29e514fea6 | |||
| 310fff78c6 | |||
| f2efdc007c | |||
| b3be767923 | |||
| e86f2f3873 | |||
| 13d84f73d4 | |||
| e31342d3ba | |||
| daf0538bf3 | |||
| 451ce8b0c7 | |||
| b8cce14705 | |||
| bf1c9c650e | |||
| 8f6387536c | |||
| 56535ece11 | |||
| f1767719cb | |||
| c925b06114 | |||
| 402426884d | |||
| df6c8a5a75 | |||
| 99f5ae7125 | |||
| d50a1b7d07 | |||
| fab3bb76f7 | |||
| 5025c66bb2 | |||
| 800c153e96 | |||
| 71bbda0fb7 | |||
| 6e6bac429a | |||
| 1ce091a4d9 | |||
| a8f889be74 | |||
| 5f33c6bfee | |||
| 6a290c49d8 | |||
| b304d5d784 | |||
| cfe83b97d9 | |||
| 2fec2bf560 | |||
| 73dc1a7839 | |||
| 66fe951831 | |||
| 7991bcbf1a | |||
| de9516563a | |||
| 27fefb821c | |||
| c195894db9 | |||
| 6777b4d370 | |||
| 09269c22a2 | |||
| 2e24a2f079 | |||
| 5d9932dd61 | |||
| 062064213a | |||
| a2ae3ffb2b | |||
| 6cb4a0a3eb | |||
| f17c49091f | |||
| c16afc07df | |||
| 1616a96b2c | |||
| 261601230a | |||
| 453a38df54 | |||
| 5b004a849f | |||
| 29d811d3fd | |||
| 36c5739318 | |||
| b3f9c67d34 | |||
| bc8eb802f7 | |||
| a138eead74 | |||
| a700a0e1b1 | |||
| 205a33a241 | |||
| c88fd94c8b | |||
| a2b4e2e87c | |||
| 4a8f1e95ba | |||
| 3a847d921e | |||
| 806fdb9541 | |||
| cf1adbdb01 | |||
| 349d08e799 | |||
| d680c7ed83 | |||
| d4cb7a711b | |||
| bb6e19e7cd | |||
| 1c3ea53e63 | |||
| 88e17029c5 | |||
| 588e91b19f | |||
| 8cc2e7b6f1 | |||
| 222353b532 | |||
| b88b266fd5 | |||
| 60e6fb99af | |||
| 65b60e57b2 | |||
| 16a8402bf4 | |||
| 5896411136 | |||
| 0bb74a7885 | |||
| 86dfb9231f | |||
| 7198ce3eb0 | |||
| 08fecf1eb2 | |||
| 3eda26ca94 | |||
| d907914c7c | |||
| 266ab48fed | |||
| 3325cffa91 | |||
| 43469ac62a | |||
| a5c953fdb6 | |||
| 627c46e458 | |||
| 205eb34adc | |||
| 125e14d377 | |||
| a51c8a700b |
+10
@@ -1,4 +1,14 @@
|
||||
.vscode/
|
||||
.cache
|
||||
.idea
|
||||
.coverage
|
||||
.pytest_cache
|
||||
.DS_Store
|
||||
coverage.xml
|
||||
|
||||
__pycache__
|
||||
tests/__pycache__
|
||||
|
||||
build
|
||||
responder.egg-info/
|
||||
dist/
|
||||
|
||||
@@ -8,6 +8,7 @@ responder = {editable = true, path = "."}
|
||||
uvicorn = "*"
|
||||
starlette = "*"
|
||||
aiofiles = "*"
|
||||
docopt = "*"
|
||||
|
||||
[dev-packages]
|
||||
pytest = "*"
|
||||
|
||||
@@ -12,8 +12,6 @@
|
||||
|
||||
The Python world certainly doesn't need more web frameworks. But, it does need more creativity, so I thought I'd spread some [Hacktoberfest](https://hacktoberfest.digitalocean.com/) spirit around, bring some of my ideas to the table, and see what I could come up with.
|
||||
|
||||
## An Example Web Service:
|
||||
|
||||
```python
|
||||
import responder
|
||||
|
||||
@@ -126,7 +124,7 @@ Boom.
|
||||
|
||||
# Installing Responder
|
||||
|
||||
Install the latest release (might be out of date with the docs a bit):
|
||||
Install the latest release:
|
||||
|
||||
|
||||
$ pipenv install responder
|
||||
@@ -139,47 +137,6 @@ Or, install from the development branch:
|
||||
|
||||
Only **Python 3.6+** is supported.
|
||||
|
||||
# Web Service Performance Charecteristics
|
||||
The objective of these benchmark tests is not testing deployment (like uwsgi vs gunicorn vs uvicorn etc) but instead test the performance of python-response against other popular Python web frameworks.
|
||||
|
||||
### Methodology
|
||||
The results below were gotten running the performance tests on a Lenovo W530, Intel(R) Core(TM) i7-3740QM CPU @ 2.70GHz, MEM: 32GB, Linux Mint 19. I used Python 3.7.0 with the WRK utility with params:
|
||||
wrk -d20s -t10 -c200 (i.e. 10 threads and 200 connections).
|
||||
|
||||
1. #### Simple "Hello World" benchmark
|
||||
|
||||
python-responder v0.0.1 (Master branch)
|
||||
Requests/sec: 1368.23
|
||||
Transfer/sec: 163.01KB
|
||||
|
||||
|
||||
|
||||
|
||||
Django v2.1.2 (i18n == False)
|
||||
Requests/sec: 544.54
|
||||
Transfer/sec: 103.18KB
|
||||
|
||||
|
||||
|
||||
|
||||
Django v2.1.2 (i18n == True)
|
||||
Requests/sec: 535.12
|
||||
Transfer/sec: 101.38KB
|
||||
|
||||
|
||||
|
||||
Django v2.1.2 (Minimal 1 file Django Application)
|
||||
https://gist.github.com/aitoehigie/ebcc1d3e460e66cd51e5501fa2636798
|
||||
Requests/sec: 701.53
|
||||
Transfer/sec: 99.34KB
|
||||
|
||||
|
||||
|
||||
|
||||
Flask v1.0.2
|
||||
Requests/sec: 896.24
|
||||
Transfer/sec: 144.41KB
|
||||
|
||||
|
||||
# The Basic Idea
|
||||
|
||||
|
||||
@@ -75,6 +75,11 @@
|
||||
display: none;
|
||||
}
|
||||
|
||||
#testimonials p.attribution {
|
||||
margin-top: -1em;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* "Quick Search" should be not be shown for now. */
|
||||
div#searchbox h3 {
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
|
||||
API Documentation
|
||||
=================
|
||||
|
||||
|
||||
Web Service (API) Class
|
||||
-----------------------
|
||||
.. module:: responder
|
||||
|
||||
.. autoclass:: API
|
||||
:inherited-members:
|
||||
|
||||
Requests & Responses
|
||||
--------------------
|
||||
|
||||
|
||||
.. autoclass:: Request
|
||||
:inherited-members:
|
||||
|
||||
.. autoclass:: Response
|
||||
:inherited-members:
|
||||
|
||||
|
||||
Utility Functions
|
||||
-----------------
|
||||
|
||||
.. autofunction:: responder.API.status_codes.is_100
|
||||
|
||||
.. autofunction:: responder.API.status_codes.is_200
|
||||
|
||||
.. autofunction:: responder.API.status_codes.is_300
|
||||
|
||||
.. autofunction:: responder.API.status_codes.is_400
|
||||
|
||||
.. autofunction:: responder.API.status_codes.is_500
|
||||
+28
-161
@@ -24,9 +24,6 @@ A familiar HTTP Service Framework
|
||||
The Python world certainly doesn't need more web frameworks. But, it does need more creativity, so I thought I'd
|
||||
spread some `Hacktoberfest <https://hacktoberfest.digitalocean.com/>`_ spirit around, bring some of my ideas to the table, and see what I could come up with.
|
||||
|
||||
An Example Web Service:
|
||||
-----------------------
|
||||
|
||||
.. code:: python
|
||||
|
||||
import responder
|
||||
@@ -52,117 +49,58 @@ Testimonials
|
||||
|
||||
“Pleasantly very taken with python-responder.
|
||||
`@kennethreitz <https://twitter.com/kennethreitz>`_ at his absolute
|
||||
best.” —Rudraksh M.K.
|
||||
best.”
|
||||
|
||||
—Rudraksh M.K.
|
||||
|
||||
|
||||
|
||||
..
|
||||
|
||||
“Buckle up!” —Tom Christie of `APIStar`_ and `Django REST Framework`_
|
||||
“Buckle up!”
|
||||
|
||||
“I love that you are exploring new patterns. Go go go!” — Danny
|
||||
Greenfield, author of `Two Scoops of Django`_
|
||||
—Tom Christie of `APIStar`_ and `Django REST Framework`_
|
||||
|
||||
..
|
||||
|
||||
“Love what I have seen while it’s in progress! Many features of
|
||||
Responder are from my wishlist for Flask, and it’s even faster and
|
||||
even easier than Flask!” — Luna C.
|
||||
|
||||
“I love that you are exploring new patterns. Go go go!”
|
||||
|
||||
— Danny Greenfield, author of `Two Scoops of Django`_
|
||||
|
||||
|
||||
..
|
||||
|
||||
|
||||
“The most ambitious crossover event in history.” —Pablo Cabezas, `on
|
||||
Tom Christie joining the project`_
|
||||
“The most ambitious crossover event in history.”
|
||||
|
||||
—Pablo Cabezas, `on Tom Christie joining the project`_
|
||||
|
||||
|
||||
.. _APIStar: https://github.com/encode/apistar
|
||||
.. _Django REST Framework: https://www.django-rest-framework.org/
|
||||
.. _Two Scoops of Django:
|
||||
.. _on Tom Christie joining the project: https://twitter.com/pabloteleco/status/1050841098321620992?s=20
|
||||
|
||||
More Examples
|
||||
-------------
|
||||
User Guides
|
||||
-----------
|
||||
|
||||
Class-based views (and setting some headers and stuff)::
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
@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::
|
||||
|
||||
|
||||
@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::
|
||||
|
||||
@api.route("/")
|
||||
def hello(req, resp):
|
||||
|
||||
@api.background.task
|
||||
def sleep(s=10):
|
||||
time.sleep(s)
|
||||
print("slept!")
|
||||
|
||||
sleep()
|
||||
resp.content = "processing"
|
||||
|
||||
|
||||
Serve a GraphQL API::
|
||||
|
||||
import graphene
|
||||
|
||||
class Query(graphene.ObjectType):
|
||||
hello = graphene.String(name=graphene.String(default_value="stranger"))
|
||||
|
||||
def resolve_hello(self, info, name):
|
||||
return "Hello " + name
|
||||
|
||||
api.add_route("/graph", graphene.Schema(query=Query))
|
||||
|
||||
|
||||
We can then send a query to our service::
|
||||
|
||||
>>> requests = api.session()
|
||||
>>> r = requests.get("http://;/graph", params={"query": "{ hello }"})
|
||||
>>> r.json()
|
||||
{'data': {'hello': 'Hello stranger'}}
|
||||
|
||||
|
||||
Or, request YAML back::
|
||||
|
||||
>>> 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.
|
||||
quickstart
|
||||
tour
|
||||
api
|
||||
|
||||
|
||||
Installing Responder
|
||||
====================
|
||||
--------------------
|
||||
|
||||
Install the latest release (might be out of date with the docs a bit):
|
||||
Install the latest release:
|
||||
|
||||
.. code-block:: shell
|
||||
|
||||
$ pipenv install git+https://github.com/kennethreitz/responder.git#egg=responder
|
||||
$ pipenv install responder
|
||||
✨🍰✨
|
||||
|
||||
|
||||
@@ -176,50 +114,13 @@ Or, install from the development branch:
|
||||
Only **Python 3.6+** is supported.
|
||||
|
||||
|
||||
|
||||
Web Service Performance Charecteristics
|
||||
---------------------------------------
|
||||
|
||||
The objective of these benchmark tests is not testing deployment (like uwsgi vs gunicorn vs uvicorn etc) but instead test the performance of python-response against other popular Python web frameworks.
|
||||
|
||||
Methodology
|
||||
~~~~~~~~~~~
|
||||
|
||||
The results below were gotten running the performance tests on a Lenovo
|
||||
W530, Intel(R) Core(TM) i7-3740QM CPU @ 2.70GHz, MEM: 32GB, Linux Mint
|
||||
19. I used Python 3.7.0 with the WRK utility with params: wrk -d20s -t10
|
||||
-c200 (i.e. 10 threads and 200 connections).
|
||||
|
||||
1. .. rubric:: Simple “Hello World” benchmark
|
||||
:name: simple-hello-world-benchmark
|
||||
|
||||
| python-responder v0.0.1 (Master branch)
|
||||
| Requests/sec: 1368.23
|
||||
| Transfer/sec: 163.01KB
|
||||
|
||||
| Django v2.1.2 (i18n == False)
|
||||
| Requests/sec: 544.54
|
||||
| Transfer/sec: 103.18KB
|
||||
|
||||
| Django v2.1.2 (i18n == True)
|
||||
| Requests/sec: 535.12
|
||||
| Transfer/sec: 101.38KB
|
||||
|
||||
| Django v2.1.2 (Minimal 1 file Django Application)
|
||||
| https://gist.github.com/aitoehigie/ebcc1d3e460e66cd51e5501fa2636798
|
||||
| Requests/sec: 701.53
|
||||
| Transfer/sec: 99.34KB
|
||||
|
||||
| Flask v1.0.2
|
||||
| Requests/sec: 896.24
|
||||
| Transfer/sec: 144.41KB
|
||||
The Basic Idea
|
||||
--------------
|
||||
|
||||
The primary concept here is to bring the nicities that are brought forth from both Flask and Falcon and unify them into a single framework, along with some new ideas I have. I also wanted to take some of the API primitives that are instilled in the Requests library and put them into a web framework. So, you'll find a lot of parallels here with Requests.
|
||||
The primary concept here is to bring the niceties that are brought forth from both Flask and Falcon and unify them into a single framework, along with some new ideas I have. I also wanted to take some of the API primitives that are instilled in the Requests library and put them into a web framework. So, you'll find a lot of parallels here with Requests.
|
||||
|
||||
- Setting ``resp.text`` sends back unicode, while setting ``resp.content`` sends back bytes.
|
||||
- Setting `resp.media` sends back JSON/YAML (``.text``/``.content`` override this).
|
||||
- Setting ``resp.media`` sends back JSON/YAML (``.text``/``.content`` override this).
|
||||
- Case-insensitive ``req.headers`` dict (from Requests directly).
|
||||
- ``resp.status_code``, ``req.method``, ``req.url``, and other familiar friends.
|
||||
|
||||
@@ -240,44 +141,10 @@ Ideas
|
||||
Future Ideas
|
||||
------------
|
||||
|
||||
- Cookie-based sessions are currently an afterthrought, as this is an API framework, but websites are APIs too.
|
||||
- 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.
|
||||
|
||||
|
||||
API Documentation
|
||||
=================
|
||||
|
||||
|
||||
Web Service (API) Class
|
||||
-----------------------
|
||||
.. module:: responder
|
||||
|
||||
.. autoclass:: API
|
||||
:inherited-members:
|
||||
|
||||
Requests & Responses
|
||||
--------------------
|
||||
|
||||
|
||||
.. autoclass:: Request
|
||||
:inherited-members:
|
||||
|
||||
.. autoclass:: Response
|
||||
:inherited-members:
|
||||
|
||||
|
||||
Utility Functions
|
||||
-----------------
|
||||
|
||||
.. autofunction:: responder.API.status_codes.is_100
|
||||
|
||||
.. autofunction:: responder.API.status_codes.is_200
|
||||
|
||||
.. autofunction:: responder.API.status_codes.is_300
|
||||
|
||||
.. autofunction:: responder.API.status_codes.is_400
|
||||
|
||||
.. autofunction:: responder.API.status_codes.is_500
|
||||
|
||||
Indices and tables
|
||||
==================
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
Quick Start!
|
||||
============
|
||||
|
||||
This section of the documentation exists to provide an introduction to the Responder interface,
|
||||
as well as educate the user on basic functionality.
|
||||
|
||||
|
||||
Declare a Web Service
|
||||
---------------------
|
||||
|
||||
The first thing you need to do is declare a web service::
|
||||
|
||||
import responder
|
||||
|
||||
api = responder.API()
|
||||
|
||||
Hello World!
|
||||
------------
|
||||
|
||||
Then, you can add a view / route to it.
|
||||
|
||||
Here, we'll make the root URL say "hello world!"::
|
||||
|
||||
@api.route("/")
|
||||
def hello_world(req, resp):
|
||||
resp.text = "hello, world!"
|
||||
|
||||
Run the Server
|
||||
--------------
|
||||
|
||||
Next, we can run our web service easily, with ``api.run()``::
|
||||
|
||||
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.
|
||||
|
||||
|
||||
Accept Route Arguments
|
||||
----------------------
|
||||
|
||||
If you want dynamic URLs, you can use Python's familiar *f-string syntax* to declare variables in your routes::
|
||||
|
||||
@api.route("/hello/{who}")
|
||||
def hello_to(req, resp, *, who):
|
||||
resp.text = f"hello, {who}!"
|
||||
|
||||
A ``GET`` request to ``/hello/brettcannon`` will result in a response of ``hello, brettcannon!``.
|
||||
|
||||
Returning JSON / YAML
|
||||
---------------------
|
||||
|
||||
If you want your API to send back JSON, simply set the ``resp.media`` property to a JSON-serializable Python object::
|
||||
|
||||
|
||||
@api.route("/hello/{who}/json")
|
||||
def hello_to(req, resp, *, who):
|
||||
resp.media = {"hello": who}
|
||||
|
||||
A ``GET`` request to ``/hello/guido/json`` will result in a response of ``{'hello': 'guido'}``.
|
||||
|
||||
If the client requests YAML instead (with a header of ``Accept: application/x-yaml``), YAML will be sent.
|
||||
|
||||
Rendering a Template
|
||||
--------------------
|
||||
|
||||
If you want to render a template, simply use ``api.template``. No need for additional imports::
|
||||
|
||||
@api.route("/hello/{who}/html")
|
||||
def hello_html(req, resp, *, who):
|
||||
resp.content = api.template('hello.html', who=who)
|
||||
|
||||
The ``api`` instance is available as an object during template rendering.
|
||||
|
||||
Setting Response Status Code
|
||||
----------------------------
|
||||
|
||||
If you want to set the response status code, simply set ``resp.status_code``::
|
||||
|
||||
@api.route("/416")
|
||||
def teapot(req, resp):
|
||||
resp.status_code = api.status_codes.HTTP_416 # ...or 416
|
||||
|
||||
|
||||
Setting Response Headers
|
||||
------------------------
|
||||
|
||||
If you want to set a response header, like ``X-Pizza: 42``, simply modify the ``resp.headers`` dictionary::
|
||||
|
||||
@api.route("/pizza")
|
||||
def pizza_pizza(req, resp):
|
||||
resp.headers['X-Pizza'] = 42
|
||||
|
||||
That's it!
|
||||
|
||||
|
||||
Receiving Data & Background Tasks
|
||||
---------------------------------
|
||||
|
||||
If you're expecting to read any request data, on the server, you need to declare your view as async and await the content.
|
||||
|
||||
Here, we'll process our data in the background, while responding immediately to the client::
|
||||
|
||||
import time
|
||||
|
||||
@api.route("/incoming")
|
||||
async def receive_incoming(req, resp):
|
||||
|
||||
@api.background.task
|
||||
def process_data(data):
|
||||
"""Just sleeps for three seconds, as a demo."""
|
||||
time.sleep(3)
|
||||
|
||||
|
||||
# Parse the incoming data as form-encoded.
|
||||
# Note: 'json' and 'yaml' formats are also automatically supported.
|
||||
data = await resp.media()
|
||||
|
||||
# Process the data (in the background).
|
||||
process_data(data)
|
||||
|
||||
# Immediately respond that upload was successful.
|
||||
resp.media = {'success': True}
|
||||
|
||||
A ``POST`` request to ``/incoming`` will result in an immediate response of ``{'success': true}``.
|
||||
@@ -0,0 +1,79 @@
|
||||
Feature Tour
|
||||
============
|
||||
|
||||
|
||||
Class-Based Views
|
||||
-----------------
|
||||
|
||||
Class-based views (and setting some headers and stuff)::
|
||||
|
||||
@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
|
||||
|
||||
|
||||
Background Tasks
|
||||
----------------
|
||||
|
||||
Here, you can spawn off a background thread to run any function, out-of-request::
|
||||
|
||||
@api.route("/")
|
||||
def hello(req, resp):
|
||||
|
||||
@api.background.task
|
||||
def sleep(s=10):
|
||||
time.sleep(s)
|
||||
print("slept!")
|
||||
|
||||
sleep()
|
||||
resp.content = "processing"
|
||||
|
||||
|
||||
GraphQL
|
||||
-------
|
||||
|
||||
Serve a GraphQL API::
|
||||
|
||||
import graphene
|
||||
|
||||
class Query(graphene.ObjectType):
|
||||
hello = graphene.String(name=graphene.String(default_value="stranger"))
|
||||
|
||||
def resolve_hello(self, info, name):
|
||||
return "Hello " + name
|
||||
|
||||
api.add_route("/graph", graphene.Schema(query=Query))
|
||||
|
||||
|
||||
Built-in Testing Client (Requests)
|
||||
----------------------------------
|
||||
|
||||
We can then send a query to our service::
|
||||
|
||||
>>> requests = api.session()
|
||||
>>> r = requests.get("http://;/graph", params={"query": "{ hello }"})
|
||||
>>> r.json()
|
||||
{'data': {'hello': 'Hello stranger'}}
|
||||
|
||||
|
||||
Or, request YAML back::
|
||||
|
||||
>>> r = requests.get("http://;/graph", params={"query": "{ hello(name:\"john\") }"}, headers={"Accept": "application/x-yaml"})
|
||||
>>> print(r.text)
|
||||
data: {hello: Hello john}
|
||||
|
||||
|
||||
HSTS (Redirect to HTTPS)
|
||||
------------------------
|
||||
|
||||
Want HSTS?
|
||||
|
||||
::
|
||||
|
||||
api = responder.API(enable_hsts=True)
|
||||
|
||||
|
||||
Boom.
|
||||
@@ -0,0 +1,4 @@
|
||||
[pytest]
|
||||
; addopts= -rsxX -s -v --strict
|
||||
filterwarnings =
|
||||
error::UserWarning
|
||||
@@ -1 +1 @@
|
||||
__version__ = "0.0.2"
|
||||
__version__ = "0.0.4"
|
||||
|
||||
+26
-33
@@ -13,24 +13,12 @@ from starlette.staticfiles import StaticFiles
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
from . import models
|
||||
from .status_codes import HTTP_404
|
||||
from . import status_codes
|
||||
from .routes import Route
|
||||
from .formats import get_formats
|
||||
from .background import BackgroundQueue
|
||||
|
||||
|
||||
def memoize(f):
|
||||
memo = {}
|
||||
|
||||
def helper(self, name, auto_escape=True, **values):
|
||||
if repr(values) not in memo:
|
||||
memo[repr(values)] = f(self, name, auto_escape=True, **values)
|
||||
return memo[repr(values)]
|
||||
|
||||
return helper
|
||||
|
||||
|
||||
# TODO: consider moving status codes here
|
||||
class API:
|
||||
"""The primary web-service class.
|
||||
@@ -46,12 +34,13 @@ class API:
|
||||
self, static_dir="static", templates_dir="templates", enable_hsts=False
|
||||
):
|
||||
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.routes = {}
|
||||
|
||||
self.hsts_enabled = enable_hsts
|
||||
self.static_files = StaticFiles(directory=str(self.static_dir))
|
||||
self.apps = {"/static": self.static_files}
|
||||
self.apps = {self.static_route: self.static_files}
|
||||
|
||||
self.formats = get_formats()
|
||||
|
||||
@@ -120,15 +109,16 @@ class API:
|
||||
try:
|
||||
# GraphQL Schema.
|
||||
assert hasattr(view, "execute")
|
||||
self.graphql_response(req, resp, schema=view)
|
||||
await self.graphql_response(req, resp, schema=view)
|
||||
except AssertionError:
|
||||
# WSGI App.
|
||||
try:
|
||||
return view(
|
||||
environ=req._environ, start_response=req._start_response
|
||||
)
|
||||
except TypeError:
|
||||
pass
|
||||
# try:
|
||||
# return view(
|
||||
# environ=req._environ, start_response=req._start_response
|
||||
# )
|
||||
# except TypeError:
|
||||
# pass
|
||||
pass
|
||||
|
||||
# Run on_request first.
|
||||
try:
|
||||
@@ -163,7 +153,7 @@ class API:
|
||||
self.routes[route] = Route(route, endpoint)
|
||||
|
||||
def default_response(self, req, resp):
|
||||
resp.status_code = HTTP_404
|
||||
resp.status_code = status_codes.HTTP_404
|
||||
resp.text = "Not found."
|
||||
|
||||
def redirect(
|
||||
@@ -185,9 +175,9 @@ class API:
|
||||
resp.headers.update({"Location": location})
|
||||
|
||||
@staticmethod
|
||||
def _resolve_graphql_query(req):
|
||||
async def _resolve_graphql_query(req):
|
||||
if "json" in req.mimetype:
|
||||
return req.json()["query"]
|
||||
return (await req.media("json"))["query"]
|
||||
|
||||
# Support query/q in form data.
|
||||
# Form data is awaiting https://github.com/encode/starlette/pull/102
|
||||
@@ -206,8 +196,8 @@ class API:
|
||||
# TODO: Make some assertions about content-type here.
|
||||
return req.text
|
||||
|
||||
def graphql_response(self, req, resp, schema):
|
||||
query = self._resolve_graphql_query(req)
|
||||
async def graphql_response(self, req, resp, schema):
|
||||
query = await self._resolve_graphql_query(req)
|
||||
result = schema.execute(query)
|
||||
result, status_code = encode_execution_results(
|
||||
[result],
|
||||
@@ -219,13 +209,13 @@ class API:
|
||||
return (query, result, status_code)
|
||||
|
||||
def route(self, route, **options):
|
||||
"""Decorator for creating new routes around function and class defenitions.
|
||||
"""Decorator for creating new routes around function and class definitions.
|
||||
|
||||
Usage::
|
||||
|
||||
@api.route("/hello")
|
||||
def hello(req, resp):
|
||||
req.text = "hello, world!"
|
||||
resp.text = "hello, world!"
|
||||
|
||||
"""
|
||||
|
||||
@@ -253,7 +243,7 @@ class API:
|
||||
self._session = TestClient(self)
|
||||
return self._session
|
||||
|
||||
def url_for(self, endpoint, absolute_url=False, **params):
|
||||
def url_for(self, endpoint, testing=False, **params):
|
||||
# TODO: Absolute_url
|
||||
"""Given an endpoint, returns a rendered URL for its route.
|
||||
|
||||
@@ -262,10 +252,13 @@ class API:
|
||||
"""
|
||||
for (route, route_object) in self.routes.items():
|
||||
if route_object.endpoint == endpoint:
|
||||
return route_object.url(**params)
|
||||
return route_object.url(testing=testing, **params)
|
||||
raise ValueError
|
||||
|
||||
@memoize
|
||||
def static_url(self, asset):
|
||||
"""Given a static asset, return its URL path."""
|
||||
return f"{self.static_route}/{str(asset)}"
|
||||
|
||||
def template(self, name, auto_escape=True, **values):
|
||||
"""Renders the given `jinja2 <http://jinja.pocoo.org/docs/>`_ template, with provided values supplied.
|
||||
|
||||
@@ -322,18 +315,18 @@ class API:
|
||||
return template.render(**values)
|
||||
|
||||
def run(self, address=None, port=None, **options):
|
||||
"""Runs the application with Waitress. If the ``PORT`` environment
|
||||
"""Runs the application with uvicorn. If the ``PORT`` environment
|
||||
variable is set, requests will be served on that port automatically to all
|
||||
known hosts.
|
||||
|
||||
:param address: The address to bind to.
|
||||
:param port: The port to bind to. If none is provided, one will be selected at random.
|
||||
:param kwargs: Additional keyword arguments to send to ``waitress.serve()``.
|
||||
:param options: Additional keyword arguments to send to ``uvicorn.run()``.
|
||||
"""
|
||||
if "PORT" in os.environ:
|
||||
if address is None:
|
||||
address = "0.0.0.0"
|
||||
port = os.environ["PORT"]
|
||||
port = int(os.environ["PORT"])
|
||||
|
||||
if address is None:
|
||||
address = "127.0.0.1"
|
||||
|
||||
+1
-13
@@ -10,7 +10,6 @@ class BackgroundQueue:
|
||||
self.n = n
|
||||
self.pool = concurrent.futures.ThreadPoolExecutor(max_workers=n)
|
||||
self.results = []
|
||||
self.callbacks = []
|
||||
|
||||
def run(self, f, *args, **kwargs):
|
||||
self.pool._max_workers = self.n
|
||||
@@ -18,22 +17,11 @@ class BackgroundQueue:
|
||||
|
||||
f = self.pool.submit(f, *args, **kwargs)
|
||||
self.results.append(f)
|
||||
return f
|
||||
|
||||
def task(self, f):
|
||||
def do_task(*args, **kwargs):
|
||||
result = self.run(f, *args, **kwargs)
|
||||
|
||||
for cb in self.callbacks:
|
||||
result.add_done_callback(cb)
|
||||
|
||||
return result
|
||||
|
||||
return do_task
|
||||
|
||||
def callback(self, f):
|
||||
self.callbacks.append(f)
|
||||
|
||||
def register_callback():
|
||||
f()
|
||||
|
||||
return register_callback
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
import docopt
|
||||
@@ -7,20 +7,20 @@ async def format_form(r, encode=False):
|
||||
return await r._starlette.form()
|
||||
|
||||
|
||||
def format_yaml(r, encode=False):
|
||||
async def format_yaml(r, encode=False):
|
||||
if encode:
|
||||
r.headers.update({"Content-Type": "application/x-yaml"})
|
||||
return yaml.dump(r.media)
|
||||
else:
|
||||
return yaml.safe_load(r.content)
|
||||
return yaml.safe_load(await r.content)
|
||||
|
||||
|
||||
def format_json(r, encode=False):
|
||||
async def format_json(r, encode=False):
|
||||
if encode:
|
||||
r.headers.update({"Content-Type": "application/json"})
|
||||
return json.dumps(r.media)
|
||||
else:
|
||||
return json.loads(r.content)
|
||||
return json.loads(await r.content)
|
||||
|
||||
|
||||
def get_formats():
|
||||
|
||||
+61
-23
@@ -2,6 +2,7 @@ import io
|
||||
import json
|
||||
import gzip
|
||||
|
||||
import chardet
|
||||
import rfc3986
|
||||
import graphene
|
||||
import yaml
|
||||
@@ -14,6 +15,7 @@ from starlette.responses import Response as StarletteResponse
|
||||
from urllib.parse import parse_qs
|
||||
|
||||
from .status_codes import HTTP_200
|
||||
from .statics import DEFAULT_ENCODING
|
||||
|
||||
|
||||
class QueryDict(dict):
|
||||
@@ -95,11 +97,13 @@ class Request:
|
||||
"full_url",
|
||||
"url",
|
||||
"params",
|
||||
"_encoding",
|
||||
]
|
||||
|
||||
def __init__(self, scope, receive):
|
||||
self._starlette = StarletteRequest(scope, receive)
|
||||
self.formats = None
|
||||
self._encoding = None
|
||||
|
||||
headers = CaseInsensitiveDict()
|
||||
for header, value in self._starlette.headers.items():
|
||||
@@ -107,7 +111,7 @@ class Request:
|
||||
|
||||
self.headers = (
|
||||
headers
|
||||
) #: A case-insensitive dictionary, containg all headers sent in the Request.
|
||||
) #: A case-insensitive dictionary, containing all headers sent in the Request.
|
||||
|
||||
self.mimetype = self.headers.get("Content-Type", "")
|
||||
|
||||
@@ -123,19 +127,53 @@ class Request:
|
||||
try:
|
||||
self.params = QueryDict(
|
||||
self.url.query
|
||||
) #: A dictionary of the parsed query paramaters used for the Request.
|
||||
) #: A dictionary of the parsed query parameters used for the Request.
|
||||
except AttributeError:
|
||||
self.params = {}
|
||||
|
||||
@property
|
||||
async def encoding(self):
|
||||
"""The encoding of the Request's body. Can be set, manually. Must be awaited."""
|
||||
# Use the user-set encoding first.
|
||||
if self._encoding:
|
||||
return self._encoding
|
||||
|
||||
# Then try what's defined by the Request.
|
||||
elif await self.declared_encoding:
|
||||
return self.declared_encoding
|
||||
|
||||
# Then, automatically detect the encoding.
|
||||
else:
|
||||
return await self.apparent_encoding
|
||||
|
||||
@encoding.setter
|
||||
def encoding(self, value):
|
||||
self._encoding = value
|
||||
|
||||
@property
|
||||
async def content(self):
|
||||
"""The Request body, as bytes."""
|
||||
return (await self._starlette.body()).encode(self.encoding)
|
||||
"""The Request body, as bytes. Must be awaited."""
|
||||
return await self._starlette.body()
|
||||
|
||||
@property
|
||||
async def text(self):
|
||||
"""The Request body, as unicode."""
|
||||
return await self._starlette.body()
|
||||
"""The Request body, as unicode. Must be awaited."""
|
||||
return (await self._starlette.body()).decode(await self.encoding)
|
||||
|
||||
@property
|
||||
async def declared_encoding(self):
|
||||
if "Encoding" in self.headers:
|
||||
return self.headers["Encoding"]
|
||||
|
||||
@property
|
||||
async def apparent_encoding(self):
|
||||
"""The apparent encoding, provided by the chardet library. Must be awaited."""
|
||||
declared_encoding = await self.declared_encoding
|
||||
|
||||
if declared_encoding:
|
||||
return declared_encoding
|
||||
else:
|
||||
return chardet.detect(await self.content)["encoding"]
|
||||
|
||||
@property
|
||||
def is_secure(self):
|
||||
@@ -143,9 +181,9 @@ class Request:
|
||||
|
||||
def accepts(self, content_type):
|
||||
"""Returns ``True`` if the incoming Request accepts the given ``content_type``."""
|
||||
return content_type in self.headers["Accept"]
|
||||
return content_type in self.headers.get("Accept", [])
|
||||
|
||||
def media(self, format=None):
|
||||
async def media(self, format=None):
|
||||
"""Renders incoming json/yaml/form data as Python objects.
|
||||
|
||||
:param format: The name of the format being used. Alternatively accepts a custom callable for the format type.
|
||||
@@ -153,11 +191,12 @@ class Request:
|
||||
|
||||
if format is None:
|
||||
format = "yaml" if "yaml" in self.mimetype or "" else "json"
|
||||
format = "form" if "form" in self.mimetype or "" else format
|
||||
|
||||
if format in self.formats:
|
||||
return self.formats[format](self)
|
||||
return await self.formats[format](self)
|
||||
else:
|
||||
return format(self)
|
||||
return await format(self)
|
||||
|
||||
|
||||
class Response:
|
||||
@@ -177,7 +216,7 @@ class Response:
|
||||
self.status_code = HTTP_200 #: The HTTP Status Code to use for the Response.
|
||||
self.text = None #: A unicode representation of the response body.
|
||||
self.content = None #: A bytes representation of the response body.
|
||||
self.encoding = "utf-8"
|
||||
self.encoding = DEFAULT_ENCODING
|
||||
self.media = (
|
||||
None
|
||||
) #: A Python object that will be content-negotiated and sent back to the client. Typically, in JSON formatting.
|
||||
@@ -187,7 +226,7 @@ class Response:
|
||||
self.formats = formats
|
||||
|
||||
@property
|
||||
def body(self):
|
||||
async def body(self):
|
||||
if self.content:
|
||||
return (self.content, {})
|
||||
|
||||
@@ -196,19 +235,18 @@ class Response:
|
||||
|
||||
for format in self.formats:
|
||||
if self.req.accepts(format):
|
||||
return self.formats[format](self, encode=True), {}
|
||||
return (await self.formats[format](self, encode=True)), {}
|
||||
|
||||
# Default to JSON anyway.
|
||||
else:
|
||||
return (
|
||||
self.formats["json"](self, encode=True),
|
||||
{"Content-Type": "application/json"},
|
||||
)
|
||||
return (
|
||||
await self.formats["json"](self, encode=True),
|
||||
{"Content-Type": "application/json"},
|
||||
)
|
||||
|
||||
@property
|
||||
def gzipped_body(self):
|
||||
async def gzipped_body(self):
|
||||
|
||||
body, headers = self.body
|
||||
body, headers = await self.body
|
||||
|
||||
if isinstance(body, str):
|
||||
body = body.encode(self.encoding)
|
||||
@@ -231,9 +269,9 @@ class Response:
|
||||
return (body, headers)
|
||||
|
||||
async def __call__(self, receive, send):
|
||||
body, headers = self.body
|
||||
if len(self.body) > 500:
|
||||
body, headers = self.gzipped_body
|
||||
body, headers = await self.body
|
||||
if len(await self.body) > 500:
|
||||
body, headers = await self.gzipped_body
|
||||
if self.headers:
|
||||
headers.update(self.headers)
|
||||
|
||||
|
||||
+10
-8
@@ -2,12 +2,11 @@ from parse import parse, search
|
||||
|
||||
|
||||
def memoize(f):
|
||||
memo = {}
|
||||
|
||||
def helper(self, s):
|
||||
if s not in memo:
|
||||
memo[s] = f(self, s)
|
||||
return memo[s]
|
||||
memoize_key = f'{f.__name__}:{s}'
|
||||
if memoize_key not in self._memo:
|
||||
self._memo[memoize_key] = f(self, s)
|
||||
return self._memo[memoize_key]
|
||||
|
||||
return helper
|
||||
|
||||
@@ -16,6 +15,7 @@ class Route:
|
||||
def __init__(self, route, endpoint):
|
||||
self.route = route
|
||||
self.endpoint = endpoint
|
||||
self._memo = {}
|
||||
|
||||
def __repr__(self):
|
||||
return f"<Route {self.route!r}={self.endpoint!r}>"
|
||||
@@ -45,7 +45,9 @@ class Route:
|
||||
results = parse(self.route, s)
|
||||
return results.named if results else {}
|
||||
|
||||
def url(self, **params):
|
||||
return self.route.format(**params)
|
||||
def url(self, testing=False, **params):
|
||||
url = self.route.format(**params)
|
||||
if testing:
|
||||
url = f"http://;{url}"
|
||||
|
||||
# def is_graphql, is_wsgi
|
||||
return url
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
DEFAULT_ENCODING = "utf-8"
|
||||
@@ -34,6 +34,7 @@ required = [
|
||||
"uvloop ; sys_platform != 'win32'",
|
||||
"rfc3986",
|
||||
"python-multipart",
|
||||
"chardet",
|
||||
]
|
||||
|
||||
|
||||
@@ -114,7 +115,7 @@ setup(
|
||||
url="https://github.com/kennethreitz/responder",
|
||||
packages=find_packages(exclude=["tests"]),
|
||||
# entry_points={
|
||||
# "console_scripts": ["pipenv=pipenv:cli", "pipenv-resolver=pipenv.resolver:main"]
|
||||
# "console_scripts": ["responder=responder:cli"]
|
||||
# },
|
||||
package_data={
|
||||
# "": ["LICENSE", "NOTICES"],
|
||||
|
||||
+1
-94
@@ -1,96 +1,3 @@
|
||||
this is a test
|
||||
|
||||
Lorem ipsum dolor, sit amet consectetur adipisicing elit. Maiores in, ea beatae praesentium quis enim exercitationem
|
||||
voluptate repellat possimus laborum provident voluptates numquam id atque tempora. Quidem et repudiandae aliquam?
|
||||
|
||||
Lorem ipsum dolor sit amet consectetur adipisicing elit. Libero reiciendis consequuntur deserunt iure nesciunt autem
|
||||
saepe magnam quas, debitis aliquam molestias possimus necessitatibus cumque enim modi fuga tenetur hic natus?
|
||||
|
||||
Lorem ipsum dolor sit amet consectetur adipisicing elit. Quia magni maxime, optio aliquid tempore dignissimos aperiam
|
||||
voluptatibus, quae sunt vel iste nesciunt. Commodi saepe ipsam architecto omnis neque sequi beatae.
|
||||
|
||||
Lorem ipsum, dolor sit amet consectetur adipisicing elit. Neque quam sequi quidem corporis repudiandae quo, fugiat
|
||||
ullam inventore, ratione cupiditate maiores nobis autem asperiores earum dolorum praesentium quod consequuntur nostrum!
|
||||
|
||||
Lorem ipsum dolor sit amet consectetur adipisicing elit. Dolorum, in? Officiis ratione veritatis distinctio quas illo
|
||||
voluptatibus quia velit corrupti. Tempora ipsam perspiciatis ullam sapiente itaque esse doloribus error culpa.this is a
|
||||
test
|
||||
|
||||
Lorem ipsum dolor, sit amet consectetur adipisicing elit. Maiores in, ea beatae praesentium quis enim exercitationem
|
||||
voluptate repellat possimus laborum provident voluptates numquam id atque tempora. Quidem et repudiandae aliquam?
|
||||
|
||||
Lorem ipsum dolor sit amet consectetur adipisicing elit. Libero reiciendis consequuntur deserunt iure nesciunt autem
|
||||
saepe magnam quas, debitis aliquam molestias possimus necessitatibus cumque enim modi fuga tenetur hic natus?
|
||||
|
||||
Lorem ipsum dolor sit amet consectetur adipisicing elit. Quia magni maxime, optio aliquid tempore dignissimos aperiam
|
||||
voluptatibus, quae sunt vel iste nesciunt. Commodi saepe ipsam architecto omnis neque sequi beatae.
|
||||
|
||||
Lorem ipsum, dolor sit amet consectetur adipisicing elit. Neque quam sequi quidem corporis repudiandae quo, fugiat
|
||||
ullam inventore, ratione cupiditate maiores nobis autem asperiores earum dolorum praesentium quod consequuntur nostrum!
|
||||
|
||||
Lorem ipsum dolor sit amet consectetur adipisicing elit. Dolorum, in? Officiis ratione veritatis distinctio quas illo
|
||||
voluptatibus quia velit corrupti. Tempora ipsam perspiciatis ullam sapiente itaque esse doloribus error culpa.this is a
|
||||
test
|
||||
|
||||
Lorem ipsum dolor, sit amet consectetur adipisicing elit. Maiores in, ea beatae praesentium quis enim exercitationem
|
||||
voluptate repellat possimus laborum provident voluptates numquam id atque tempora. Quidem et repudiandae aliquam?
|
||||
|
||||
Lorem ipsum dolor sit amet consectetur adipisicing elit. Libero reiciendis consequuntur deserunt iure nesciunt autem
|
||||
saepe magnam quas, debitis aliquam molestias possimus necessitatibus cumque enim modi fuga tenetur hic natus?
|
||||
|
||||
Lorem ipsum dolor sit amet consectetur adipisicing elit. Quia magni maxime, optio aliquid tempore dignissimos aperiam
|
||||
voluptatibus, quae sunt vel iste nesciunt. Commodi saepe ipsam architecto omnis neque sequi beatae.
|
||||
|
||||
Lorem ipsum, dolor sit amet consectetur adipisicing elit. Neque quam sequi quidem corporis repudiandae quo, fugiat
|
||||
ullam inventore, ratione cupiditate maiores nobis autem asperiores earum dolorum praesentium quod consequuntur nostrum!
|
||||
|
||||
Lorem ipsum dolor sit amet consectetur adipisicing elit. Dolorum, in? Officiis ratione veritatis distinctio quas illo
|
||||
voluptatibus quia velit corrupti. Tempora ipsam perspiciatis ullam sapiente itaque esse doloribus error culpa.this is a
|
||||
test
|
||||
|
||||
Lorem ipsum dolor, sit amet consectetur adipisicing elit. Maiores in, ea beatae praesentium quis enim exercitationem
|
||||
voluptate repellat possimus laborum provident voluptates numquam id atque tempora. Quidem et repudiandae aliquam?
|
||||
|
||||
Lorem ipsum dolor sit amet consectetur adipisicing elit. Libero reiciendis consequuntur deserunt iure nesciunt autem
|
||||
saepe magnam quas, debitis aliquam molestias possimus necessitatibus cumque enim modi fuga tenetur hic natus?
|
||||
|
||||
Lorem ipsum dolor sit amet consectetur adipisicing elit. Quia magni maxime, optio aliquid tempore dignissimos aperiam
|
||||
voluptatibus, quae sunt vel iste nesciunt. Commodi saepe ipsam architecto omnis neque sequi beatae.
|
||||
|
||||
Lorem ipsum, dolor sit amet consectetur adipisicing elit. Neque quam sequi quidem corporis repudiandae quo, fugiat
|
||||
ullam inventore, ratione cupiditate maiores nobis autem asperiores earum dolorum praesentium quod consequuntur nostrum!
|
||||
|
||||
Lorem ipsum dolor sit amet consectetur adipisicing elit. Dolorum, in? Officiis ratione veritatis distinctio quas illo
|
||||
voluptatibus quia velit corrupti. Tempora ipsam perspiciatis ullam sapiente itaque esse doloribus error culpa.this is a
|
||||
test
|
||||
|
||||
Lorem ipsum dolor, sit amet consectetur adipisicing elit. Maiores in, ea beatae praesentium quis enim exercitationem
|
||||
voluptate repellat possimus laborum provident voluptates numquam id atque tempora. Quidem et repudiandae aliquam?
|
||||
|
||||
Lorem ipsum dolor sit amet consectetur adipisicing elit. Libero reiciendis consequuntur deserunt iure nesciunt autem
|
||||
saepe magnam quas, debitis aliquam molestias possimus necessitatibus cumque enim modi fuga tenetur hic natus?
|
||||
|
||||
Lorem ipsum dolor sit amet consectetur adipisicing elit. Quia magni maxime, optio aliquid tempore dignissimos aperiam
|
||||
voluptatibus, quae sunt vel iste nesciunt. Commodi saepe ipsam architecto omnis neque sequi beatae.
|
||||
|
||||
Lorem ipsum, dolor sit amet consectetur adipisicing elit. Neque quam sequi quidem corporis repudiandae quo, fugiat
|
||||
ullam inventore, ratione cupiditate maiores nobis autem asperiores earum dolorum praesentium quod consequuntur nostrum!
|
||||
|
||||
Lorem ipsum dolor sit amet consectetur adipisicing elit. Dolorum, in? Officiis ratione veritatis distinctio quas illo
|
||||
voluptatibus quia velit corrupti. Tempora ipsam perspiciatis ullam sapiente itaque esse doloribus error culpa.this is a
|
||||
test
|
||||
|
||||
Lorem ipsum dolor, sit amet consectetur adipisicing elit. Maiores in, ea beatae praesentium quis enim exercitationem
|
||||
voluptate repellat possimus laborum provident voluptates numquam id atque tempora. Quidem et repudiandae aliquam?
|
||||
|
||||
Lorem ipsum dolor sit amet consectetur adipisicing elit. Libero reiciendis consequuntur deserunt iure nesciunt autem
|
||||
saepe magnam quas, debitis aliquam molestias possimus necessitatibus cumque enim modi fuga tenetur hic natus?
|
||||
|
||||
Lorem ipsum dolor sit amet consectetur adipisicing elit. Quia magni maxime, optio aliquid tempore dignissimos aperiam
|
||||
voluptatibus, quae sunt vel iste nesciunt. Commodi saepe ipsam architecto omnis neque sequi beatae.
|
||||
|
||||
Lorem ipsum, dolor sit amet consectetur adipisicing elit. Neque quam sequi quidem corporis repudiandae quo, fugiat
|
||||
ullam inventore, ratione cupiditate maiores nobis autem asperiores earum dolorum praesentium quod consequuntur nostrum!
|
||||
|
||||
Lorem ipsum dolor sit amet consectetur adipisicing elit. Dolorum, in? Officiis ratione veritatis distinctio quas illo
|
||||
voluptatibus quia velit corrupti. Tempora ipsam perspiciatis ullam sapiente itaque esse doloribus error culpa.
|
||||
{{ api.static_url('test') }}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import graphene
|
||||
import responder
|
||||
from pathlib import Path
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def data_dir(current_dir):
|
||||
yield current_dir / "data"
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def current_dir():
|
||||
yield Path(__file__).parent
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def api():
|
||||
return responder.API()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def session(api):
|
||||
return api.session()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def url():
|
||||
def url_for(s):
|
||||
return f"http://;{s}"
|
||||
|
||||
return url_for
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def flask():
|
||||
from flask import Flask
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
@app.route("/")
|
||||
def hello():
|
||||
return "Hello World!"
|
||||
|
||||
return app
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def schema():
|
||||
class Query(graphene.ObjectType):
|
||||
hello = graphene.String(name=graphene.String(default_value="stranger"))
|
||||
|
||||
def resolve_hello(self, info, name):
|
||||
return f"Hello {name}"
|
||||
|
||||
return graphene.Schema(query=Query)
|
||||
@@ -0,0 +1,36 @@
|
||||
import pytest
|
||||
|
||||
|
||||
def test_custom_encoding(api, session):
|
||||
data = "hi alex!"
|
||||
|
||||
@api.route("/")
|
||||
async def route(req, resp):
|
||||
req.encoding = "ascii"
|
||||
resp.text = await req.text
|
||||
|
||||
r = session.get(api.url_for(route), data=data)
|
||||
assert r.text == data
|
||||
|
||||
|
||||
def test_bytes_encoding(api, session):
|
||||
data = b"hi lenny!"
|
||||
|
||||
@api.route("/")
|
||||
async def route(req, resp):
|
||||
resp.text = (await req.content).decode("utf-8")
|
||||
|
||||
r = session.get(api.url_for(route), data=data)
|
||||
assert r.content == data
|
||||
|
||||
|
||||
def test_false_encoding_raises(api, session):
|
||||
data = "hi mom!"
|
||||
|
||||
@api.route("/")
|
||||
async def route(req, resp):
|
||||
req.encoding = "non-existient"
|
||||
resp.text = await req.text
|
||||
|
||||
with pytest.raises(LookupError):
|
||||
session.get(api.url_for(route), data=data)
|
||||
@@ -0,0 +1,60 @@
|
||||
import inspect
|
||||
import pytest
|
||||
|
||||
from responder import models
|
||||
|
||||
|
||||
_default_query = "q=%7b%20hello%20%7d&name=myname&user_name=test_user"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"query, expected",
|
||||
[
|
||||
pytest.param(
|
||||
_default_query,
|
||||
{"q": ["{ hello }"], "name": ["myname"], "user_name": ["test_user"]},
|
||||
id="parse query with unique keys",
|
||||
),
|
||||
pytest.param(
|
||||
"q=1&q=2&q=3", {"q": ["1", "2", "3"]}, id="parse query with the same key"
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_query_dict(query, expected):
|
||||
d = models.QueryDict(query)
|
||||
assert d == expected
|
||||
|
||||
|
||||
def test_query_dict_get():
|
||||
d = models.QueryDict(_default_query)
|
||||
|
||||
assert d["user_name"] == "test_user"
|
||||
assert d.get("key_none_exist") is None
|
||||
|
||||
|
||||
def test_query_dict_get_list():
|
||||
d = models.QueryDict(_default_query)
|
||||
|
||||
assert d.get_list("user_name") == ["test_user"]
|
||||
assert d.get_list("key_none_exist") == []
|
||||
assert d.get_list("key_none_exist", ["foo"]) == ["foo"]
|
||||
|
||||
|
||||
def test_query_dict_items_list():
|
||||
d = models.QueryDict(_default_query)
|
||||
|
||||
items_list = d.items_list()
|
||||
assert inspect.isgenerator(items_list)
|
||||
assert dict(items_list) == {
|
||||
"q": ["{ hello }"],
|
||||
"name": ["myname"],
|
||||
"user_name": ["test_user"],
|
||||
}
|
||||
|
||||
|
||||
def test_query_dict_items():
|
||||
d = models.QueryDict(_default_query)
|
||||
|
||||
items = d.items()
|
||||
assert inspect.isgenerator(items)
|
||||
assert dict(items) == {"q": "{ hello }", "name": "myname", "user_name": "test_user"}
|
||||
@@ -1,49 +1,6 @@
|
||||
import graphene
|
||||
import pytest
|
||||
import responder
|
||||
import yaml
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def api():
|
||||
return responder.API()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def session(api):
|
||||
return api.session()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def url():
|
||||
def url_for(s):
|
||||
return f"http://;{s}"
|
||||
|
||||
return url_for
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def flask():
|
||||
import flask
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
@app.route("/")
|
||||
def hello():
|
||||
return "Hello World!"
|
||||
|
||||
return app
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def schema():
|
||||
class Query(graphene.ObjectType):
|
||||
hello = graphene.String(name=graphene.String(default_value="stranger"))
|
||||
|
||||
def resolve_hello(self, info, name):
|
||||
return f"Hello {name}"
|
||||
|
||||
return graphene.Schema(query=Query)
|
||||
import responder
|
||||
|
||||
|
||||
def test_api_basic_route(api):
|
||||
@@ -160,25 +117,25 @@ def test_graphql_schema_query_querying(api, schema):
|
||||
assert r.json() == {"data": {"hello": "Hello stranger"}}
|
||||
|
||||
|
||||
def test_argumented_routing(api):
|
||||
def test_argumented_routing(api, session):
|
||||
@api.route("/{name}")
|
||||
def hello(req, resp, *, name):
|
||||
resp.text = f"Hello, {name}."
|
||||
|
||||
r = api.session().get("http://;/sean")
|
||||
r = session.get(api.url_for(hello, name="sean"))
|
||||
assert r.text == "Hello, sean."
|
||||
|
||||
|
||||
def test_mote_argumented_routing(api):
|
||||
def test_mote_argumented_routing(api, session):
|
||||
@api.route("/{greeting}/{name}")
|
||||
def hello(req, resp, *, greeting, name):
|
||||
resp.text = f"{greeting}, {name}."
|
||||
|
||||
r = api.session().get("http://;/hello/lyndsy")
|
||||
r = session.get(api.url_for(hello, greeting="hello", name="lyndsy"))
|
||||
assert r.text == "hello, lyndsy."
|
||||
|
||||
|
||||
def test_request_and_get(api):
|
||||
def test_request_and_get(api, session):
|
||||
@api.route("/")
|
||||
class ThingsResource:
|
||||
def on_request(self, req, resp):
|
||||
@@ -187,40 +144,93 @@ def test_request_and_get(api):
|
||||
def on_get(self, request, resp):
|
||||
resp.headers.update({"LIFE": "42"})
|
||||
|
||||
r = api.session().get("http://;/")
|
||||
r = session.get(api.url_for(ThingsResource))
|
||||
assert "DEATH" in r.headers
|
||||
assert "LIFE" in r.headers
|
||||
|
||||
|
||||
def test_query_params(api):
|
||||
def test_query_params(api, url, session):
|
||||
@api.route("/")
|
||||
def route(req, resp):
|
||||
resp.media = {"params": req.params}
|
||||
|
||||
r = api.session().get("http://;/?q=q")
|
||||
r = session.get(api.url_for(route), params={"q": "q"})
|
||||
assert r.json()["params"] == {"q": "q"}
|
||||
|
||||
r = api.session().get("http://;/?q=1&q=2&q=3")
|
||||
r = session.get(url("/?q=1&q=2&q=3"))
|
||||
assert r.json()["params"] == {"q": "3"}
|
||||
|
||||
|
||||
# Requires https://github.com/encode/starlette/pull/102
|
||||
def test_form_data(api):
|
||||
def test_form_data(api, session):
|
||||
@api.route("/")
|
||||
async def route(req, resp):
|
||||
resp.media = {"form": await req.media("form")}
|
||||
|
||||
dump = {"q": "q"}
|
||||
r = api.session().get("http://;/", data=dump)
|
||||
r = session.get(api.url_for(route), data=dump)
|
||||
assert r.json()["form"] == dump
|
||||
|
||||
|
||||
def test_async_function(api, session, url):
|
||||
def test_async_function(api, session):
|
||||
content = "The Emerald Tablet of Hermes"
|
||||
|
||||
@api.route("/")
|
||||
async def route(req, resp):
|
||||
resp.text = content
|
||||
|
||||
r = session.get(url("/"))
|
||||
r = session.get(api.url_for(route))
|
||||
assert r.text == content
|
||||
|
||||
|
||||
def test_media_parsing(api, session):
|
||||
dump = {"hello": "sam"}
|
||||
|
||||
@api.route("/")
|
||||
def route(req, resp):
|
||||
resp.media = dump
|
||||
|
||||
r = session.get(api.url_for(route))
|
||||
assert r.json() == dump
|
||||
|
||||
r = session.get(api.url_for(route), headers={"Accept": "application/x-yaml"})
|
||||
assert r.text == "{hello: sam}\n"
|
||||
|
||||
|
||||
def test_background(api, session):
|
||||
@api.route("/")
|
||||
def route(req, resp):
|
||||
@api.background.task
|
||||
def task():
|
||||
import time
|
||||
|
||||
time.sleep(3)
|
||||
|
||||
task()
|
||||
api.text = "ok"
|
||||
|
||||
r = session.get(api.url_for(route))
|
||||
assert r.ok
|
||||
|
||||
|
||||
def test_multiple_routes(api, session):
|
||||
@api.route("/1")
|
||||
def route1(req, resp):
|
||||
resp.text = "1"
|
||||
|
||||
@api.route("/2")
|
||||
def route2(req, resp):
|
||||
resp.text = "2"
|
||||
|
||||
r = session.get(api.url_for(route1))
|
||||
assert r.text == "1"
|
||||
|
||||
r = session.get(api.url_for(route2))
|
||||
assert r.text == "2"
|
||||
|
||||
|
||||
def test_graphql_schema_json_query(api, schema):
|
||||
api.add_route("/", schema)
|
||||
|
||||
r = api.session().post("http://;/", json={"query": "{ hello }"})
|
||||
assert r.ok
|
||||
@@ -0,0 +1,83 @@
|
||||
import pytest
|
||||
from responder import routes
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"route, expected",
|
||||
[
|
||||
pytest.param("/", False, id="home path without params"),
|
||||
pytest.param("/test_path", False, id="sub path without params"),
|
||||
pytest.param("/{test_path}", True, id="path with params"),
|
||||
],
|
||||
)
|
||||
def test_parameter(route, expected):
|
||||
r = routes.Route(route, "test_endpoint")
|
||||
assert r.has_parameters is expected
|
||||
|
||||
|
||||
def test_url():
|
||||
r = routes.Route("/{my_path}", "test_endpoint")
|
||||
url = r.url(my_path="path")
|
||||
assert url == "/path"
|
||||
|
||||
|
||||
def test_equal():
|
||||
r = routes.Route("/{path_param}", "test_endpoint")
|
||||
r2 = routes.Route("/{path_param}", "test_endpoint")
|
||||
r3 = routes.Route("/test_path", "test_endpoint")
|
||||
|
||||
assert r == r2
|
||||
assert r != r3
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path_param, actual, match",
|
||||
[
|
||||
pytest.param(
|
||||
"/{greetings}", "/hello", {"greetings": "hello"}, id="with one strformat"
|
||||
),
|
||||
pytest.param(
|
||||
"/{greetings}.{name}",
|
||||
"/hi.jane",
|
||||
{"greetings": "hi", "name": "jane"},
|
||||
id="with dot in url and two strformat",
|
||||
),
|
||||
pytest.param(
|
||||
"/{greetings}/{name}",
|
||||
"/hi/john",
|
||||
{"greetings": "hi", "name": "john"},
|
||||
id="with sub url and two strformat",
|
||||
),
|
||||
pytest.param(
|
||||
"/concrete_path", "/foo", {}, id="test concrete path with no match"
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_incoming_matches(path_param, actual, match):
|
||||
r = routes.Route(path_param, "test_endpoint")
|
||||
assert r.incoming_matches(actual) == match
|
||||
|
||||
|
||||
def test_incoming_matches_with_concrete_path_no_match():
|
||||
r = routes.Route("/concrete_path", "test_endpoint")
|
||||
assert r.incoming_matches("hello") == {}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"route, match, expected",
|
||||
[
|
||||
pytest.param(
|
||||
"/{path_param}",
|
||||
"/{path_param}",
|
||||
True,
|
||||
id="with both parametrized path match",
|
||||
),
|
||||
pytest.param(
|
||||
"/concrete", "/concrete", True, id="with both concrete path match"
|
||||
),
|
||||
pytest.param("/concrete", "/no_match", False, id="with no match"),
|
||||
],
|
||||
)
|
||||
def test_does_match_with_route(route, match, expected):
|
||||
r = routes.Route(route, "test_endpoint")
|
||||
assert r.does_match(match) == expected
|
||||
Reference in New Issue
Block a user