mirror of
https://github.com/kennethreitz/responder.git
synced 2026-06-05 23:00:17 +00:00
Compare commits
76 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| daf43009ba | |||
| 955d777ca5 | |||
| cc9472aa2f | |||
| e527f3cb1f | |||
| 3a375a8975 | |||
| 2698496592 | |||
| 0155d854e3 | |||
| c74cc8586f | |||
| 8eb89da9a0 | |||
| dee6ee3cef | |||
| beab89df09 | |||
| 5164d4ec32 | |||
| 878db851af | |||
| 686ff72ae0 | |||
| 2710d7098f | |||
| 7f41ff4035 | |||
| ed8d51014c | |||
| d09a51f47d | |||
| 59bae90454 | |||
| 13ee0ca94e | |||
| 5abc095050 | |||
| 7eb68c8388 | |||
| f69b644a77 | |||
| fe41d4c863 | |||
| 29830455ed | |||
| e50828093d | |||
| 880d29c5a9 | |||
| 77b2e9ba7a | |||
| 586fad7646 | |||
| fb636028fb | |||
| a8c3f8fc46 | |||
| 72f4227c5a | |||
| 8ccace8ef9 | |||
| 6d40c6dfe5 | |||
| 0b5562cdec | |||
| eeff0816f3 | |||
| f1f16dea3f | |||
| bfc6ef2049 | |||
| 5212de79d3 | |||
| b61c02e5df | |||
| f982954e8f | |||
| 3ba20e69ba | |||
| aea01fd893 | |||
| 950be14eca | |||
| 446deffc17 | |||
| e0863115ee | |||
| e34cb539d2 | |||
| d8ade8638a | |||
| 3067080474 | |||
| 886cc0f214 | |||
| 071d34b016 | |||
| a1564ca003 | |||
| 60f0e765c2 | |||
| 3f0ecea4bf | |||
| 2c9e6572c5 | |||
| 371a83f20f | |||
| b8cff1655a | |||
| 232856ca3a | |||
| 3f168ac6fd | |||
| c59cb1d0d3 | |||
| ec13df75d0 | |||
| 6fc02964ba | |||
| ed79e45680 | |||
| 1be983bf89 | |||
| b09d6a9d04 | |||
| db143d845d | |||
| 2e23501f9d | |||
| bd6addcd3a | |||
| 631e1fb604 | |||
| 30ee6726a8 | |||
| 1c397db9d8 | |||
| cc23ca80f4 | |||
| 449379a0ed | |||
| b3208b1c5b | |||
| 4df60b55a6 | |||
| 379553a1a5 |
@@ -1,3 +1,19 @@
|
||||
# v0.3.0
|
||||
- Interactive Documentation endpoint.
|
||||
- Minor improvements.
|
||||
|
||||
# v0.2.3
|
||||
- Overall improvements.
|
||||
|
||||
# v0.2.2
|
||||
- Show traceback info when background tasks raise exceptions.
|
||||
|
||||
# v0.2.1
|
||||
- api.requests.
|
||||
|
||||
# v0.2.0
|
||||
- WebSocket support.
|
||||
|
||||
# v0.1.6
|
||||
- 500 support.
|
||||
|
||||
|
||||
Generated
+31
-101
@@ -37,6 +37,12 @@
|
||||
],
|
||||
"version": "==1.0.0b3"
|
||||
},
|
||||
"apistar": {
|
||||
"hashes": [
|
||||
"sha256:4338b24468b49526ceac4a8f84046056081ee747f373ca8d0647bd6b2344c895"
|
||||
],
|
||||
"version": "==0.6.0"
|
||||
},
|
||||
"asgiref": {
|
||||
"hashes": [
|
||||
"sha256:9b05dcd41a6a89ca8c6e7f7e4089c3f3e76b5af60aebb81ae6d455ad81989c97",
|
||||
@@ -215,9 +221,9 @@
|
||||
},
|
||||
"starlette": {
|
||||
"hashes": [
|
||||
"sha256:ce5c684fad4edb2967cd491518cd3c2724e420508202c2d48f519ea68dcec9d6"
|
||||
"sha256:eac0f6cab6b48846a0c1af16615430ae0e7a95f669ee0841a7e2f242d51d8935"
|
||||
],
|
||||
"version": "==0.5.4"
|
||||
"version": "==0.5.5"
|
||||
},
|
||||
"urllib3": {
|
||||
"hashes": [
|
||||
@@ -228,9 +234,9 @@
|
||||
},
|
||||
"uvicorn": {
|
||||
"hashes": [
|
||||
"sha256:7c4550c7e6f7c8727fa5ccd5200baf62c9e055895e058933ee88f5d0c246ca0c"
|
||||
"sha256:e2b742fdaa0b52f4aac92fd2c078e7f1f17d11322bb3efb09d341d5c6998b4b5"
|
||||
],
|
||||
"version": "==0.3.14"
|
||||
"version": "==0.3.16"
|
||||
},
|
||||
"websockets": {
|
||||
"hashes": [
|
||||
@@ -317,43 +323,6 @@
|
||||
],
|
||||
"version": "==2018.10.15"
|
||||
},
|
||||
"cffi": {
|
||||
"hashes": [
|
||||
"sha256:151b7eefd035c56b2b2e1eb9963c90c6302dc15fbd8c1c0a83a163ff2c7d7743",
|
||||
"sha256:1553d1e99f035ace1c0544050622b7bc963374a00c467edafac50ad7bd276aef",
|
||||
"sha256:1b0493c091a1898f1136e3f4f991a784437fac3673780ff9de3bcf46c80b6b50",
|
||||
"sha256:2ba8a45822b7aee805ab49abfe7eec16b90587f7f26df20c71dd89e45a97076f",
|
||||
"sha256:3bb6bd7266598f318063e584378b8e27c67de998a43362e8fce664c54ee52d30",
|
||||
"sha256:3c85641778460581c42924384f5e68076d724ceac0f267d66c757f7535069c93",
|
||||
"sha256:3eb6434197633b7748cea30bf0ba9f66727cdce45117a712b29a443943733257",
|
||||
"sha256:495c5c2d43bf6cebe0178eb3e88f9c4aa48d8934aa6e3cddb865c058da76756b",
|
||||
"sha256:4c91af6e967c2015729d3e69c2e51d92f9898c330d6a851bf8f121236f3defd3",
|
||||
"sha256:57b2533356cb2d8fac1555815929f7f5f14d68ac77b085d2326b571310f34f6e",
|
||||
"sha256:770f3782b31f50b68627e22f91cb182c48c47c02eb405fd689472aa7b7aa16dc",
|
||||
"sha256:79f9b6f7c46ae1f8ded75f68cf8ad50e5729ed4d590c74840471fc2823457d04",
|
||||
"sha256:7a33145e04d44ce95bcd71e522b478d282ad0eafaf34fe1ec5bbd73e662f22b6",
|
||||
"sha256:857959354ae3a6fa3da6651b966d13b0a8bed6bbc87a0de7b38a549db1d2a359",
|
||||
"sha256:87f37fe5130574ff76c17cab61e7d2538a16f843bb7bca8ebbc4b12de3078596",
|
||||
"sha256:95d5251e4b5ca00061f9d9f3d6fe537247e145a8524ae9fd30a2f8fbce993b5b",
|
||||
"sha256:9d1d3e63a4afdc29bd76ce6aa9d58c771cd1599fbba8cf5057e7860b203710dd",
|
||||
"sha256:a36c5c154f9d42ec176e6e620cb0dd275744aa1d804786a71ac37dc3661a5e95",
|
||||
"sha256:a6a5cb8809091ec9ac03edde9304b3ad82ad4466333432b16d78ef40e0cce0d5",
|
||||
"sha256:ae5e35a2c189d397b91034642cb0eab0e346f776ec2eb44a49a459e6615d6e2e",
|
||||
"sha256:b0f7d4a3df8f06cf49f9f121bead236e328074de6449866515cea4907bbc63d6",
|
||||
"sha256:b75110fb114fa366b29a027d0c9be3709579602ae111ff61674d28c93606acca",
|
||||
"sha256:ba5e697569f84b13640c9e193170e89c13c6244c24400fc57e88724ef610cd31",
|
||||
"sha256:be2a9b390f77fd7676d80bc3cdc4f8edb940d8c198ed2d8c0be1319018c778e1",
|
||||
"sha256:ca1bd81f40adc59011f58159e4aa6445fc585a32bb8ac9badf7a2c1aa23822f2",
|
||||
"sha256:d5d8555d9bfc3f02385c1c37e9f998e2011f0db4f90e250e5bc0c0a85a813085",
|
||||
"sha256:e55e22ac0a30023426564b1059b035973ec82186ddddbac867078435801c7801",
|
||||
"sha256:e90f17980e6ab0f3c2f3730e56d1fe9bcba1891eeea58966e89d352492cc74f4",
|
||||
"sha256:ecbb7b01409e9b782df5ded849c178a0aa7c906cf8c5a67368047daab282b184",
|
||||
"sha256:ed01918d545a38998bfa5902c7c00e0fee90e957ce036a4000a88e3fe2264917",
|
||||
"sha256:edabd457cd23a02965166026fd9bfd196f4324fe6032e866d0f3bd0301cd486f",
|
||||
"sha256:fdf1c1dc5bafc32bc5d08b054f94d659422b05aba244d6be4ddc1c72d9aa70fb"
|
||||
],
|
||||
"version": "==1.11.5"
|
||||
},
|
||||
"chardet": {
|
||||
"hashes": [
|
||||
"sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae",
|
||||
@@ -368,39 +337,6 @@
|
||||
],
|
||||
"version": "==7.0"
|
||||
},
|
||||
"cmarkgfm": {
|
||||
"hashes": [
|
||||
"sha256:0186dccca79483e3405217993b83b914ba4559fe9a8396efc4eea56561b74061",
|
||||
"sha256:1a625afc6f62da428df96ec325dc30866cc5781520cbd904ff4ec44cf018171c",
|
||||
"sha256:207b7673ff4e177374c572feeae0e4ef33be620ec9171c08fd22e2b796e03e3d",
|
||||
"sha256:275905bb371a99285c74931700db3f0c078e7603bed383e8cf1a09f3ee05a3de",
|
||||
"sha256:50098f1c4950722521f0671e54139e0edc1837d63c990cf0f3d2c49607bb51a2",
|
||||
"sha256:50ed116d0b60a07df0dc7b180c28569064b9d37d1578d4c9021cff04d725cb63",
|
||||
"sha256:61a72def110eed903cd1848245897bcb80d295cd9d13944d4f9f30cba5b76655",
|
||||
"sha256:64186fb75d973a06df0e6ea12879533b71f6e7ba1ab01ffee7fc3e7534758889",
|
||||
"sha256:665303d34d7f14f10d7b0651082f25ebf7107f29ef3d699490cac16cdc0fc8ce",
|
||||
"sha256:70b18f843aec58e4e64aadce48a897fe7c50426718b7753aaee399e72df64190",
|
||||
"sha256:761ee7b04d1caee2931344ac6bfebf37102ffb203b136b676b0a71a3f0ea3c87",
|
||||
"sha256:811527e9b7280b136734ed6cb6845e5fbccaeaa132ddf45f0246cbe544016957",
|
||||
"sha256:987b0e157f70c72a84f3c2f9ef2d7ab0f26c08f2bf326c12c087ff9eebcb3ff5",
|
||||
"sha256:9fc6a2183d0a9b0974ec7cdcdad42bd78a3be674cc3e65f87dd694419b3b0ab7",
|
||||
"sha256:a3d17ee4ae739fe16f7501a52255c2e287ac817cfd88565b9859f70520afffea",
|
||||
"sha256:ba5b5488719c0f2ced0aa1986376f7baff1a1653a8eb5fdfcf3f84c7ce46ef8d",
|
||||
"sha256:c573ea89dd95d41b6d8cf36799c34b6d5b1eac4aed0212dee0f0a11fb7b01e8f",
|
||||
"sha256:c5f1b9e8592d2c448c44e6bc0d91224b16ea5f8293908b1561de1f6d2d0658b1",
|
||||
"sha256:cbe581456357d8f0674d6a590b1aaf46c11d01dd0a23af147a51a798c3818034",
|
||||
"sha256:cf219bec69e601fe27e3974b7307d2f06082ab385d42752738ad2eb630a47d65",
|
||||
"sha256:cf5014eb214d814a83a7a47407272d5db10b719dbeaf4d3cfe5969309d0fcf4b",
|
||||
"sha256:d08bad67fa18f7e8ff738c090628ee0cbf0505d74a991c848d6d04abfe67b697",
|
||||
"sha256:d6f716d7b1182bf35862b5065112f933f43dd1aa4f8097c9bcfb246f71528a34",
|
||||
"sha256:e08e479102627641c7cb4ece421c6ed4124820b1758765db32201136762282d9",
|
||||
"sha256:e20ac21418af0298437d29599f7851915497ce9f2866bc8e86b084d8911ee061",
|
||||
"sha256:e25f53c37e319241b9a412382140dffac98ca756ba8f360ac7ab5e30cad9670a",
|
||||
"sha256:e8932bddf159064f04e946fbb64693753488de21586f20e840b3be51745c8c09",
|
||||
"sha256:f20900f16377f2109783ae9348d34bc80530808439591c3d3df73d5c7ef1a00c"
|
||||
],
|
||||
"version": "==0.4.2"
|
||||
},
|
||||
"colorama": {
|
||||
"hashes": [
|
||||
"sha256:a3d89af5db9e9806a779a50296b5fdb466e281147c2c235e8225ecc6dbf7bbf3",
|
||||
@@ -451,11 +387,11 @@
|
||||
},
|
||||
"flake8": {
|
||||
"hashes": [
|
||||
"sha256:7253265f7abd8b313e3892944044a365e3f4ac3fcdcfb4298f55ee9ddf188ba0",
|
||||
"sha256:c7841163e2b576d435799169b78703ad6ac1bbb0f199994fc05f700b2a90ea37"
|
||||
"sha256:6a35f5b8761f45c5513e3405f110a86bea57982c3b75b766ce7b65217abe1670",
|
||||
"sha256:c01f8a3963b3571a8e6bd7a4063359aff90749e160778e03817cd9b71c9e07d2"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==3.5.0"
|
||||
"version": "==3.6.0"
|
||||
},
|
||||
"flask": {
|
||||
"hashes": [
|
||||
@@ -557,23 +493,17 @@
|
||||
},
|
||||
"pycodestyle": {
|
||||
"hashes": [
|
||||
"sha256:682256a5b318149ca0d2a9185d365d8864a768a28db66a84a2ea946bcc426766",
|
||||
"sha256:6c4245ade1edfad79c3446fadfc96b0de2759662dc29d07d80a6f27ad1ca6ba9"
|
||||
"sha256:cbc619d09254895b0d12c2c691e237b2e91e9b2ecf5e84c26b35400f93dcfb83",
|
||||
"sha256:cbfca99bd594a10f674d0cd97a3d802a1fdef635d4361e1a2658de47ed261e3a"
|
||||
],
|
||||
"version": "==2.3.1"
|
||||
},
|
||||
"pycparser": {
|
||||
"hashes": [
|
||||
"sha256:a988718abfad80b6b157acce7bf130a30876d27603738ac39f140993246b25b3"
|
||||
],
|
||||
"version": "==2.19"
|
||||
"version": "==2.4.0"
|
||||
},
|
||||
"pyflakes": {
|
||||
"hashes": [
|
||||
"sha256:08bd6a50edf8cffa9fa09a463063c425ecaaf10d1eb0335a7e8b1401aef89e6f",
|
||||
"sha256:8d616a382f243dbf19b54743f280b80198be0bca3a5396f1d2e1fca6223e8805"
|
||||
"sha256:9a7662ec724d0120012f6e29d6248ae3727d821bba522a0e6b356eff19126a49",
|
||||
"sha256:f661252913bc1dbe7fcfcbf0af0db3f42ab65aabd1a6ca68fe5d466bace94dae"
|
||||
],
|
||||
"version": "==1.6.0"
|
||||
"version": "==2.0.0"
|
||||
},
|
||||
"pygments": {
|
||||
"hashes": [
|
||||
@@ -591,11 +521,11 @@
|
||||
},
|
||||
"pytest": {
|
||||
"hashes": [
|
||||
"sha256:10e59f84267370ab20cec9305bafe7505ba4d6b93ecbf66a1cce86193ed511d5",
|
||||
"sha256:8c827e7d4816dfe13e9329c8226aef8e6e75d65b939bc74fda894143b6d1df59"
|
||||
"sha256:212be78a6fa5352c392738a49b18f74ae9aeec1040f47c81cadbfd8d1233c310",
|
||||
"sha256:6f6c1efc8d0ccc21f8f6c34d8330baca883cf109b66b3df954b0a117e5528fb4"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==3.9.1"
|
||||
"version": "==3.9.2"
|
||||
},
|
||||
"pytest-cov": {
|
||||
"hashes": [
|
||||
@@ -607,17 +537,17 @@
|
||||
},
|
||||
"pytz": {
|
||||
"hashes": [
|
||||
"sha256:a061aa0a9e06881eb8b3b2b43f05b9439d6583c206d0a6c340ff72a7b6669053",
|
||||
"sha256:ffb9ef1de172603304d9d2819af6f5ece76f2e85ec10692a524dd876e72bf277"
|
||||
"sha256:642253af8eae734d1509fc6ac9c1aee5e5b69d76392660889979b9870610a46b",
|
||||
"sha256:91e3ccf2c344ffaa6defba1ce7f38f97026943f675b7703f44789768e4cb0ece"
|
||||
],
|
||||
"version": "==2018.5"
|
||||
"version": "==2018.6"
|
||||
},
|
||||
"readme-renderer": {
|
||||
"hashes": [
|
||||
"sha256:237ca8705ffea849870de41101dba41543561da05c0ae45b2f1c547efa9843d2",
|
||||
"sha256:f75049a3a7afa57165551e030dd8f9882ebf688b9600535a3f7e23596651875d"
|
||||
"sha256:219a02f5359b6631f5ab952f6906c6c105bdd8bc4bf19c1ec5ee8bd9ea2dc1eb",
|
||||
"sha256:f8f122ad9fd6d138337531379575a01a0b6ca70aedca78f094cb833da38c8c0c"
|
||||
],
|
||||
"version": "==22.0"
|
||||
"version": "==23.0"
|
||||
},
|
||||
"requests": {
|
||||
"hashes": [
|
||||
@@ -671,10 +601,10 @@
|
||||
},
|
||||
"tqdm": {
|
||||
"hashes": [
|
||||
"sha256:a0be569511161220ff709a5b60d0890d47921f746f1c737a11d965e1b29e7b2e",
|
||||
"sha256:e293e6d7a7f41a529a27f8d6624ab11544ccbfe82a205af6fad102545099fc21"
|
||||
"sha256:3c4d4a5a41ef162dd61f1edb86b0e1c7859054ab656b2e7c7b77e7fbf6d9f392",
|
||||
"sha256:5b4d5549984503050883bc126280b386f5f4ca87e6c023c5d015655ad75bdebb"
|
||||
],
|
||||
"version": "==4.27.0"
|
||||
"version": "==4.28.1"
|
||||
},
|
||||
"twine": {
|
||||
"hashes": [
|
||||
|
||||
@@ -123,7 +123,7 @@ Boom.
|
||||
Install the latest release:
|
||||
|
||||
|
||||
$ pipenv install responder
|
||||
$ pipenv install responder --pre
|
||||
✨🍰✨
|
||||
|
||||
|
||||
|
||||
@@ -55,7 +55,7 @@ Features
|
||||
- Mutable response object, passed into each view. No need to return anything.
|
||||
- Background tasks, spawned off in a ``ThreadPoolExecutor``.
|
||||
- GraphQL (with *GraphiQL*) support!
|
||||
- OpenAPI schema generation.
|
||||
- OpenAPI schema generation, with interactive documentation!
|
||||
- Single-page webapp support!
|
||||
|
||||
Testimonials
|
||||
@@ -105,6 +105,7 @@ User Guides
|
||||
quickstart
|
||||
tour
|
||||
deployment
|
||||
testing
|
||||
api
|
||||
|
||||
|
||||
@@ -113,7 +114,7 @@ Installing Responder
|
||||
|
||||
.. code-block:: shell
|
||||
|
||||
$ pipenv install responder
|
||||
$ pipenv install responder --pre
|
||||
✨🍰✨
|
||||
|
||||
Only **Python 3.6+** is supported.
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
Building and Testing with Responder
|
||||
===================================
|
||||
|
||||
Responder comes with a first-class, well supported test client for your ASGI web services: **Requests**.
|
||||
|
||||
Here, we'll go over the basics of setting up a proper Python package and adding testing to it.
|
||||
|
||||
The Basics
|
||||
----------
|
||||
|
||||
Your repository should look like this::
|
||||
|
||||
Pipfile Pipfile.lock api.py test_api.py
|
||||
|
||||
``$ cat api.py``::
|
||||
|
||||
import responder
|
||||
|
||||
api = responder.API()
|
||||
|
||||
@api.route("/")
|
||||
def hello_world(req, resp):
|
||||
resp.text = "hello, world!"
|
||||
|
||||
if __name__ == "__main__":
|
||||
api.run()
|
||||
|
||||
|
||||
``$ cat Pipfile``::
|
||||
|
||||
[[source]]
|
||||
url = "https://pypi.org/simple"
|
||||
verify_ssl = true
|
||||
name = "pypi"
|
||||
|
||||
[packages]
|
||||
responder = "*"
|
||||
|
||||
[dev-packages]
|
||||
pytest = "*"
|
||||
|
||||
[requires]
|
||||
python_version = "3.7"
|
||||
|
||||
[pipenv]
|
||||
allow_prereleases = true
|
||||
|
||||
Writing Tests
|
||||
-------------
|
||||
|
||||
``$ cat test_api.py``::
|
||||
|
||||
import pytest
|
||||
import api as service
|
||||
|
||||
@pytest.fixture
|
||||
def api():
|
||||
return service.api
|
||||
|
||||
|
||||
def test_hello_world(api):
|
||||
r = api.requests.get("/")
|
||||
assert r.text == "hello, world!"
|
||||
|
||||
``$ pytest``::
|
||||
|
||||
...
|
||||
========================== 1 passed in 0.10 seconds ==========================
|
||||
|
||||
|
||||
(Optional) Proper Python Package
|
||||
--------------------------------
|
||||
|
||||
Optionally, you can not rely on relative imports, and instead install your api as a proper package. This requires:
|
||||
|
||||
1. A `proper setup.py <https://github.com/kennethreitz/setup.py>`_ file.
|
||||
2. ``$ pipenv install -e . --dev``
|
||||
|
||||
This will allow you to only specify your dependencies once: in ``setup.py``. ``$ pipenv lock`` will automatically lock your transitive dependencies (e.g. Responder), even if it's not specified in the ``Pipfile``.
|
||||
|
||||
This will ensure that your application gets installed in every developer's environment, using Pipenv.
|
||||
+35
-19
@@ -50,23 +50,6 @@ Serve a GraphQL API::
|
||||
Visiting the endpoint will render a *GraphiQL* instance, in the browser.
|
||||
|
||||
|
||||
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}
|
||||
|
||||
OpenAPI Schema Support
|
||||
----------------------
|
||||
|
||||
@@ -121,6 +104,15 @@ Responder comes with built-in support for OpenAPI / marshmallow::
|
||||
tags: []
|
||||
|
||||
|
||||
Interactive Documentation
|
||||
-------------------------
|
||||
|
||||
Responder can automatically supply API Documentation for you. Using the example above::
|
||||
|
||||
api = responder.API(title="Web Service", version="1.0", openapi="3.0", docs_route="/docs")
|
||||
|
||||
This will make ``/docs`` render interactive documentation for your API.
|
||||
|
||||
Mount a WSGI App (e.g. Flask)
|
||||
-----------------------------
|
||||
|
||||
@@ -174,13 +166,37 @@ You can easily read a Request's session data, that can be trusted to have origin
|
||||
>>> req.session
|
||||
{'username': 'kennethreitz'}
|
||||
|
||||
**Note**: if you are using this in production, you should pass the ``secret_key`` argument to ``API(...)``.
|
||||
**Note**: if you are using this in production, you should pass the ``secret_key`` argument to ``API(...)``::
|
||||
|
||||
api = responder.API(secret_key=os.environ['SECRET_KEY'])
|
||||
|
||||
Using Requests Test Client
|
||||
--------------------------
|
||||
|
||||
Responder comes with a first-class, well supported test client for your ASGI web services: **Requests**.
|
||||
|
||||
Here's an example of a test (written with pytest)::
|
||||
|
||||
import myapi
|
||||
|
||||
@pytest.fixture
|
||||
def api():
|
||||
return myapi.api
|
||||
|
||||
def test_response(api):
|
||||
hello = "hello, world!"
|
||||
|
||||
@api.route('/some-url')
|
||||
def some_view(req, resp):
|
||||
resp.text = hello
|
||||
|
||||
r = api.requests.get(url=api.url_for(some_view))
|
||||
assert r.text == hello
|
||||
|
||||
HSTS (Redirect to HTTPS)
|
||||
------------------------
|
||||
|
||||
Want HSTS?
|
||||
Want HSTS (to redirect all traffic to HTTPS)?
|
||||
|
||||
::
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
__version__ = "0.1.6"
|
||||
__version__ = "0.3.0"
|
||||
|
||||
+110
-27
@@ -4,13 +4,13 @@ from functools import partial
|
||||
from pathlib import Path
|
||||
|
||||
import uvicorn
|
||||
|
||||
import asyncio
|
||||
import apistar
|
||||
import yaml
|
||||
import jinja2
|
||||
import itsdangerous
|
||||
from graphql_server import encode_execution_results, json_encode, default_format_error
|
||||
from starlette.routing import Router
|
||||
from starlette.staticfiles import StaticFiles
|
||||
from starlette.websockets import WebSocket
|
||||
from starlette.debug import DebugMiddleware
|
||||
from starlette.testclient import TestClient
|
||||
from starlette.middleware.gzip import GZipMiddleware
|
||||
from starlette.middleware.httpsredirect import HTTPSRedirectMiddleware
|
||||
@@ -18,6 +18,7 @@ from apispec import APISpec
|
||||
from apispec.ext.marshmallow import MarshmallowPlugin
|
||||
from apispec import yaml_utils
|
||||
from asgiref.wsgi import WsgiToAsgi
|
||||
from whitenoise import WhiteNoise
|
||||
|
||||
from . import models
|
||||
from . import status_codes
|
||||
@@ -41,6 +42,7 @@ class API:
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
debug=False,
|
||||
title=None,
|
||||
version=None,
|
||||
openapi=None,
|
||||
@@ -51,6 +53,7 @@ class API:
|
||||
auto_escape=True,
|
||||
secret_key="NOTASECRET",
|
||||
enable_hsts=False,
|
||||
docs_route=None,
|
||||
):
|
||||
self.secret_key = secret_key
|
||||
self.title = title
|
||||
@@ -63,12 +66,27 @@ class API:
|
||||
os.path.abspath(os.path.dirname(__file__) + "/templates")
|
||||
)
|
||||
self.routes = {}
|
||||
self.docs_theme = "swaggerui"
|
||||
self.docs_route = docs_route
|
||||
self.schemas = {}
|
||||
self.session_cookie = "Responder-Session"
|
||||
|
||||
self.hsts_enabled = enable_hsts
|
||||
self.static_files = StaticFiles(directory=str(self.static_dir))
|
||||
self.apps = {self.static_route: self.static_files}
|
||||
|
||||
self.whitenoise = WhiteNoise(
|
||||
application=self._default_wsgi_app, index_file=True
|
||||
)
|
||||
self.whitenoise.add_files(str(self.static_dir))
|
||||
import apistar
|
||||
|
||||
self.whitenoise.add_files(
|
||||
(
|
||||
Path(apistar.__file__).parent / "themes" / self.docs_theme / "static"
|
||||
).resolve()
|
||||
)
|
||||
|
||||
self.apps = {}
|
||||
self.mount(self.static_route, self.whitenoise)
|
||||
|
||||
self.formats = get_formats()
|
||||
|
||||
@@ -83,9 +101,14 @@ class API:
|
||||
if self.openapi_version:
|
||||
self.add_route(openapi_route, self.schema_response)
|
||||
|
||||
if self.docs_route:
|
||||
self.add_route(self.docs_route, self.docs_response)
|
||||
|
||||
self.default_endpoint = None
|
||||
self.app = self.dispatch
|
||||
self.add_middleware(GZipMiddleware)
|
||||
if debug:
|
||||
self.add_middleware(DebugMiddleware)
|
||||
if self.hsts_enabled:
|
||||
self.add_middleware(HTTPSRedirectMiddleware)
|
||||
|
||||
@@ -98,6 +121,13 @@ class API:
|
||||
autoescape=jinja2.select_autoescape(["html", "xml"] if auto_escape else []),
|
||||
)
|
||||
self.jinja_values_base = {"api": self} # Give reference to self.
|
||||
self.requests = (
|
||||
self.session()
|
||||
) #: A Requests session that is connected to the ASGI app.
|
||||
|
||||
@staticmethod
|
||||
def _default_wsgi_app(*args, **kwargs):
|
||||
pass
|
||||
|
||||
@property
|
||||
def _apispec(self):
|
||||
@@ -150,7 +180,9 @@ class API:
|
||||
nonlocal scope, self
|
||||
|
||||
req = models.Request(scope, receive=receive, api=self)
|
||||
resp = await self._dispatch_request(req)
|
||||
resp = await self._dispatch_request(
|
||||
req, scope=scope, send=send, receive=receive
|
||||
)
|
||||
await resp(receive, send)
|
||||
|
||||
return asgi
|
||||
@@ -191,7 +223,6 @@ class API:
|
||||
return route
|
||||
|
||||
def _prepare_cookies(self, resp):
|
||||
# print(resp.cookies)
|
||||
if resp.cookies:
|
||||
header = " ".join([f"{k}={v}" for k, v in resp.cookies.items()])
|
||||
resp.headers["Set-Cookie"] = header
|
||||
@@ -210,20 +241,25 @@ class API:
|
||||
def no_response(req, resp, **params):
|
||||
pass
|
||||
|
||||
async def _dispatch_request(self, req):
|
||||
async def _dispatch_request(self, req, **options):
|
||||
# Set formats on Request object.
|
||||
req.formats = self.formats
|
||||
|
||||
# Get the route.
|
||||
route = self.path_matches_route(req.url.path)
|
||||
route = self.routes.get(route)
|
||||
params = route.incoming_matches(req.url.path)
|
||||
|
||||
# Create the response object.
|
||||
resp = models.Response(req=req, formats=self.formats)
|
||||
cont = False
|
||||
|
||||
if route:
|
||||
if route.uses_websocket:
|
||||
resp = WebSocket(**options)
|
||||
|
||||
else:
|
||||
resp = models.Response(req=req, formats=self.formats)
|
||||
|
||||
params = route.incoming_matches(req.url.path)
|
||||
|
||||
if route.is_graphql:
|
||||
await self.graphql_response(req, resp, schema=route.endpoint)
|
||||
|
||||
@@ -240,11 +276,11 @@ class API:
|
||||
except Exception:
|
||||
self.default_response(req, resp, error=True)
|
||||
|
||||
if route.is_class_based or cont:
|
||||
elif route.is_class_based or cont:
|
||||
try:
|
||||
view = route.endpoint(**params)
|
||||
except TypeError:
|
||||
view = route.endpoint
|
||||
view = route.endpoint()
|
||||
|
||||
# Run on_request first.
|
||||
try:
|
||||
@@ -275,8 +311,8 @@ class API:
|
||||
self.default_response(req, resp, error=True)
|
||||
|
||||
else:
|
||||
resp = models.Response(req=req, formats=self.formats)
|
||||
self.default_response(req, resp, notfound=True)
|
||||
|
||||
self.default_response(req, resp)
|
||||
|
||||
self._prepare_session(resp)
|
||||
@@ -285,7 +321,14 @@ class API:
|
||||
return resp
|
||||
|
||||
def add_route(
|
||||
self, route, endpoint=None, *, default=False, static=False, check_existing=True
|
||||
self,
|
||||
route,
|
||||
endpoint=None,
|
||||
*,
|
||||
default=False,
|
||||
static=False,
|
||||
check_existing=True,
|
||||
websocket=False,
|
||||
):
|
||||
"""Add a route to the API.
|
||||
|
||||
@@ -305,14 +348,15 @@ 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)
|
||||
# TODO: A better datastructer or sort it once the app is loaded
|
||||
self.routes[route] = Route(route, endpoint, websocket=websocket)
|
||||
# TODO: A better data structure or sort it once the app is loaded
|
||||
self.routes = dict(
|
||||
sorted(self.routes.items(), key=lambda item: item[1]._weight())
|
||||
)
|
||||
@@ -321,7 +365,7 @@ class API:
|
||||
if resp.status_code is None:
|
||||
resp.status_code = 200
|
||||
|
||||
if self.default_endpoint:
|
||||
if self.default_endpoint and notfound:
|
||||
self.default_endpoint(req, resp)
|
||||
else:
|
||||
if notfound:
|
||||
@@ -331,8 +375,12 @@ class API:
|
||||
resp.status_code = status_codes.HTTP_500
|
||||
resp.text = "Application error."
|
||||
|
||||
def docs_response(self, req, resp):
|
||||
resp.text = self.docs
|
||||
|
||||
def static_response(self, req, resp):
|
||||
index = (self.static_dir / "index.html").resolve()
|
||||
resp.content = ""
|
||||
if os.path.exists(index):
|
||||
with open(index, "r") as f:
|
||||
resp.text = f.read()
|
||||
@@ -444,24 +492,57 @@ class API:
|
||||
self._session = TestClient(self)
|
||||
return self._session
|
||||
|
||||
def url_for(self, endpoint, testing=False, **params):
|
||||
def _route_for(self, endpoint):
|
||||
for (route, route_object) in self.routes.items():
|
||||
if route_object.endpoint == endpoint:
|
||||
return route_object
|
||||
elif route_object.endpoint_name == endpoint:
|
||||
return route_object
|
||||
|
||||
def url_for(self, endpoint, **params):
|
||||
# TODO: Absolute_url
|
||||
"""Given an endpoint, returns a rendered URL for its route.
|
||||
|
||||
:param view: The route endpoint you're searching for.
|
||||
:param params: Data to pass into the URL generator (for parameterized URLs).
|
||||
"""
|
||||
for (route, route_object) in self.routes.items():
|
||||
if route_object.endpoint == endpoint:
|
||||
return route_object.url(testing=testing, **params)
|
||||
elif route_object.endpoint_name == endpoint:
|
||||
return route_object.url(testing=testing, **params)
|
||||
route_object = self._route_for(endpoint)
|
||||
if route_object:
|
||||
return route_object.url(**params)
|
||||
raise ValueError
|
||||
|
||||
def static_url(self, asset):
|
||||
"""Given a static asset, return its URL path."""
|
||||
return f"{self.static_route}/{str(asset)}"
|
||||
|
||||
@property
|
||||
def docs(self):
|
||||
|
||||
loader = jinja2.PrefixLoader(
|
||||
{
|
||||
self.docs_theme: jinja2.PackageLoader(
|
||||
"apistar", os.path.join("themes", self.docs_theme, "templates")
|
||||
)
|
||||
}
|
||||
)
|
||||
env = jinja2.Environment(autoescape=True, loader=loader)
|
||||
document = apistar.document.Document()
|
||||
document.content = yaml.safe_load(self.openapi)
|
||||
|
||||
template = env.get_template("/".join([self.docs_theme, "index.html"]))
|
||||
|
||||
def static_url(asset):
|
||||
return f"{self.static_route}/{asset}"
|
||||
# return asset
|
||||
|
||||
return template.render(
|
||||
document=document,
|
||||
langs=["javascript", "python"],
|
||||
code_style=None,
|
||||
static_url=static_url,
|
||||
schema_url="/schema.yml",
|
||||
)
|
||||
|
||||
def template(self, name_, **values):
|
||||
"""Renders the given `jinja2 <http://jinja.pocoo.org/docs/>`_ template, with provided values supplied.
|
||||
|
||||
@@ -490,15 +571,17 @@ class API:
|
||||
template = self.jinja_env.from_string(s_)
|
||||
return template.render(**values)
|
||||
|
||||
def run(self, address=None, port=None, **options):
|
||||
def run(self, address=None, port=None, debug=False, **options):
|
||||
"""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 debug: Run uvicorn server in debug mode.
|
||||
:param options: Additional keyword arguments to send to ``uvicorn.run()``.
|
||||
"""
|
||||
|
||||
if "PORT" in os.environ:
|
||||
if address is None:
|
||||
address = "0.0.0.0"
|
||||
@@ -509,4 +592,4 @@ class API:
|
||||
if port is None:
|
||||
port = 5042
|
||||
|
||||
uvicorn.run(self, host=address, port=port, **options)
|
||||
uvicorn.run(self, host=address, port=port, debug=debug, **options)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import traceback
|
||||
import multiprocessing
|
||||
import concurrent.futures
|
||||
|
||||
@@ -20,8 +21,15 @@ class BackgroundQueue:
|
||||
return f
|
||||
|
||||
def task(self, f):
|
||||
def on_future_done(fs):
|
||||
try:
|
||||
fs.result()
|
||||
except:
|
||||
traceback.print_exc()
|
||||
|
||||
def do_task(*args, **kwargs):
|
||||
result = self.run(f, *args, **kwargs)
|
||||
result.add_done_callback(on_future_done)
|
||||
return result
|
||||
|
||||
return do_task
|
||||
|
||||
@@ -34,7 +34,7 @@ async def format_files(r, encode=False):
|
||||
if encode:
|
||||
pass
|
||||
else:
|
||||
decoded = decoder.MultipartDecoder(await r.content, r.headers["Content-Type"])
|
||||
decoded = decoder.MultipartDecoder(await r.content, r.mimetype)
|
||||
dump = {}
|
||||
for part in decoded.parts:
|
||||
header = part.headers[b"Content-Disposition"].decode("utf-8")
|
||||
|
||||
+2
-2
@@ -100,8 +100,8 @@ class Request:
|
||||
self._content = None
|
||||
|
||||
headers = CaseInsensitiveDict()
|
||||
for header, value in self._starlette.headers.items():
|
||||
headers[header] = value
|
||||
for key, value in self._starlette.headers.items():
|
||||
headers[key] = value
|
||||
|
||||
self._headers = headers
|
||||
|
||||
|
||||
+8
-9
@@ -15,9 +15,10 @@ def memoize(f):
|
||||
class Route:
|
||||
_param_pattern = re.compile(r"{([^{}]*)}")
|
||||
|
||||
def __init__(self, route, endpoint):
|
||||
def __init__(self, route, endpoint, websocket=False):
|
||||
self.route = route
|
||||
self.endpoint = endpoint
|
||||
self.uses_websocket = websocket
|
||||
self._memo = {}
|
||||
|
||||
def __repr__(self):
|
||||
@@ -56,12 +57,8 @@ class Route:
|
||||
results = parse(self.route, s)
|
||||
return results.named if results else {}
|
||||
|
||||
def url(self, testing=False, **params):
|
||||
url = self.route.format(**params)
|
||||
if testing:
|
||||
url = f"http://;{url}"
|
||||
|
||||
return url
|
||||
def url(self, **params):
|
||||
return self.route.format(**params)
|
||||
|
||||
def _weight(self):
|
||||
params = set(self._param_pattern.findall(self.route))
|
||||
@@ -75,9 +72,11 @@ class Route:
|
||||
@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((routed, code, kwdefaults))
|
||||
return all((callable(self.endpoint), code, kwdefaults))
|
||||
|
||||
@@ -41,6 +41,7 @@ required = [
|
||||
"docopt",
|
||||
"itsdangerous",
|
||||
"requests-toolbelt",
|
||||
"apistar",
|
||||
]
|
||||
|
||||
|
||||
|
||||
+1
-1
@@ -21,7 +21,7 @@ def api():
|
||||
|
||||
@pytest.fixture
|
||||
def session(api):
|
||||
return api.session()
|
||||
return api.requests
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
||||
+79
-60
@@ -65,7 +65,7 @@ def test_class_based_view_registration(api):
|
||||
def test_class_based_view_parameters(api):
|
||||
@api.route("/{greeting}")
|
||||
class Greeting:
|
||||
def on_request(req, resp, *, greeting):
|
||||
def on_request(self, req, resp, *, greeting):
|
||||
resp.text = f"{greeting}, world!"
|
||||
|
||||
assert api.session().get("http://;/Hello").ok
|
||||
@@ -73,16 +73,17 @@ def test_class_based_view_parameters(api):
|
||||
|
||||
def test_requests_session(api):
|
||||
assert api.session()
|
||||
assert api.requests
|
||||
|
||||
|
||||
def test_requests_session_works(api, session, url):
|
||||
def test_requests_session_works(api, url):
|
||||
TEXT = "spiral out"
|
||||
|
||||
@api.route("/")
|
||||
def hello(req, resp):
|
||||
resp.text = TEXT
|
||||
|
||||
assert session.get(url("/")).text == TEXT
|
||||
assert api.requests.get(url("/")).text == TEXT
|
||||
|
||||
|
||||
def test_status_code(api):
|
||||
@@ -91,7 +92,7 @@ def test_status_code(api):
|
||||
resp.text = "keep going"
|
||||
resp.status_code = responder.status_codes.HTTP_416
|
||||
|
||||
assert api.session().get("http://;/").status_code == responder.status_codes.HTTP_416
|
||||
assert api.requests.get("http://;/").status_code == responder.status_codes.HTTP_416
|
||||
|
||||
|
||||
def test_json_media(api):
|
||||
@@ -101,7 +102,7 @@ def test_json_media(api):
|
||||
def media(req, resp):
|
||||
resp.media = dump
|
||||
|
||||
r = api.session().get("http://;/")
|
||||
r = api.requests.get("http://;/")
|
||||
|
||||
assert "json" in r.headers["Content-Type"]
|
||||
assert r.json() == dump
|
||||
@@ -114,7 +115,7 @@ def test_yaml_media(api):
|
||||
def media(req, resp):
|
||||
resp.media = dump
|
||||
|
||||
r = api.session().get("http://;/", headers={"Accept": "yaml"})
|
||||
r = api.requests.get("http://;/", headers={"Accept": "yaml"})
|
||||
|
||||
assert "yaml" in r.headers["Content-Type"]
|
||||
assert yaml.load(r.content) == dump
|
||||
@@ -123,38 +124,38 @@ def test_yaml_media(api):
|
||||
def test_graphql_schema_query_querying(api, schema):
|
||||
api.add_route("/", schema)
|
||||
|
||||
r = api.session().get("http://;/?q={ hello }", headers={"Accept": "json"})
|
||||
r = api.requests.get("http://;/?q={ hello }", headers={"Accept": "json"})
|
||||
assert r.json() == {"data": {"hello": "Hello stranger"}}
|
||||
|
||||
|
||||
def test_argumented_routing(api, session):
|
||||
def test_argumented_routing(api):
|
||||
@api.route("/{name}")
|
||||
def hello(req, resp, *, name):
|
||||
resp.text = f"Hello, {name}."
|
||||
|
||||
r = session.get(api.url_for(hello, name="sean"))
|
||||
r = api.requests.get(api.url_for(hello, name="sean"))
|
||||
assert r.text == "Hello, sean."
|
||||
|
||||
|
||||
def test_mote_argumented_routing(api, session):
|
||||
def test_mote_argumented_routing(api):
|
||||
@api.route("/{greeting}/{name}")
|
||||
def hello(req, resp, *, greeting, name):
|
||||
resp.text = f"{greeting}, {name}."
|
||||
|
||||
r = session.get(api.url_for(hello, greeting="hello", name="lyndsy"))
|
||||
r = api.requests.get(api.url_for(hello, greeting="hello", name="lyndsy"))
|
||||
assert r.text == "hello, lyndsy."
|
||||
|
||||
|
||||
def test_request_and_get(api, session):
|
||||
def test_request_and_get(api):
|
||||
@api.route("/")
|
||||
class ThingsResource:
|
||||
def on_request(self, req, resp):
|
||||
resp.headers.update({"DEATH": "666"})
|
||||
|
||||
def on_get(self, request, resp):
|
||||
def on_get(self, req, resp):
|
||||
resp.headers.update({"LIFE": "42"})
|
||||
|
||||
r = session.get(api.url_for(ThingsResource))
|
||||
r = api.requests.get(api.url_for(ThingsResource))
|
||||
assert "DEATH" in r.headers
|
||||
assert "LIFE" in r.headers
|
||||
|
||||
@@ -165,58 +166,58 @@ def test_class_based_view_status_code(api):
|
||||
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
|
||||
assert api.requests.get("http://;/").status_code == responder.status_codes.HTTP_416
|
||||
|
||||
|
||||
def test_query_params(api, url, session):
|
||||
def test_query_params(api, url):
|
||||
@api.route("/")
|
||||
def route(req, resp):
|
||||
resp.media = {"params": req.params}
|
||||
|
||||
r = session.get(api.url_for(route), params={"q": "q"})
|
||||
r = api.requests.get(api.url_for(route), params={"q": "q"})
|
||||
assert r.json()["params"] == {"q": "q"}
|
||||
|
||||
r = session.get(url("/?q=1&q=2&q=3"))
|
||||
r = api.requests.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, session):
|
||||
def test_form_data(api):
|
||||
@api.route("/")
|
||||
async def route(req, resp):
|
||||
resp.media = {"form": await req.media("form")}
|
||||
|
||||
dump = {"q": "q"}
|
||||
r = session.get(api.url_for(route), data=dump)
|
||||
r = api.requests.get(api.url_for(route), data=dump)
|
||||
assert r.json()["form"] == dump
|
||||
|
||||
|
||||
def test_async_function(api, session):
|
||||
def test_async_function(api):
|
||||
content = "The Emerald Tablet of Hermes"
|
||||
|
||||
@api.route("/")
|
||||
async def route(req, resp):
|
||||
resp.text = content
|
||||
|
||||
r = session.get(api.url_for(route))
|
||||
r = api.requests.get(api.url_for(route))
|
||||
assert r.text == content
|
||||
|
||||
|
||||
def test_media_parsing(api, session):
|
||||
def test_media_parsing(api):
|
||||
dump = {"hello": "sam"}
|
||||
|
||||
@api.route("/")
|
||||
def route(req, resp):
|
||||
resp.media = dump
|
||||
|
||||
r = session.get(api.url_for(route))
|
||||
r = api.requests.get(api.url_for(route))
|
||||
assert r.json() == dump
|
||||
|
||||
r = session.get(api.url_for(route), headers={"Accept": "application/x-yaml"})
|
||||
r = api.requests.get(api.url_for(route), headers={"Accept": "application/x-yaml"})
|
||||
assert r.text == "{hello: sam}\n"
|
||||
|
||||
|
||||
def test_background(api, session):
|
||||
def test_background(api):
|
||||
@api.route("/")
|
||||
def route(req, resp):
|
||||
@api.background.task
|
||||
@@ -228,11 +229,11 @@ def test_background(api, session):
|
||||
task()
|
||||
api.text = "ok"
|
||||
|
||||
r = session.get(api.url_for(route))
|
||||
r = api.requests.get(api.url_for(route))
|
||||
assert r.ok
|
||||
|
||||
|
||||
def test_multiple_routes(api, session):
|
||||
def test_multiple_routes(api):
|
||||
@api.route("/1")
|
||||
def route1(req, resp):
|
||||
resp.text = "1"
|
||||
@@ -241,45 +242,45 @@ def test_multiple_routes(api, session):
|
||||
def route2(req, resp):
|
||||
resp.text = "2"
|
||||
|
||||
r = session.get(api.url_for(route1))
|
||||
r = api.requests.get(api.url_for(route1))
|
||||
assert r.text == "1"
|
||||
|
||||
r = session.get(api.url_for(route2))
|
||||
r = api.requests.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 }"})
|
||||
r = api.requests.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"})
|
||||
r = api.requests.get("http://;/", headers={"Accept": "text/html"})
|
||||
assert r.ok
|
||||
assert "GraphiQL" in r.text
|
||||
|
||||
|
||||
def test_json_uploads(api, session):
|
||||
def test_json_uploads(api):
|
||||
@api.route("/")
|
||||
async def route(req, resp):
|
||||
resp.media = await req.media()
|
||||
|
||||
dump = {"complicated": "times"}
|
||||
r = session.post(api.url_for(route), json=dump)
|
||||
r = api.requests.post(api.url_for(route), json=dump)
|
||||
assert r.json() == dump
|
||||
|
||||
|
||||
def test_yaml_uploads(api, session):
|
||||
def test_yaml_uploads(api):
|
||||
@api.route("/")
|
||||
async def route(req, resp):
|
||||
resp.media = await req.media()
|
||||
|
||||
dump = {"complicated": "times"}
|
||||
r = session.post(
|
||||
r = api.requests.post(
|
||||
api.url_for(route),
|
||||
data=yaml.dump(dump),
|
||||
headers={"Content-Type": "application/x-yaml"},
|
||||
@@ -287,35 +288,39 @@ def test_yaml_uploads(api, session):
|
||||
assert r.json() == dump
|
||||
|
||||
|
||||
def test_form_uploads(api, session):
|
||||
def test_form_uploads(api):
|
||||
@api.route("/")
|
||||
async def route(req, resp):
|
||||
resp.media = await req.media()
|
||||
|
||||
dump = {"complicated": "times"}
|
||||
r = session.post(api.url_for(route), data=dump)
|
||||
r = api.requests.post(api.url_for(route), data=dump)
|
||||
assert r.json() == dump
|
||||
|
||||
|
||||
def test_json_downloads(api, session):
|
||||
def test_json_downloads(api):
|
||||
dump = {"testing": "123"}
|
||||
|
||||
@api.route("/")
|
||||
def route(req, resp):
|
||||
resp.media = dump
|
||||
|
||||
r = session.get(api.url_for(route), headers={"Content-Type": "application/json"})
|
||||
r = api.requests.get(
|
||||
api.url_for(route), headers={"Content-Type": "application/json"}
|
||||
)
|
||||
assert r.json() == dump
|
||||
|
||||
|
||||
def test_yaml_downloads(api, session):
|
||||
def test_yaml_downloads(api):
|
||||
dump = {"testing": "123"}
|
||||
|
||||
@api.route("/")
|
||||
def route(req, resp):
|
||||
resp.media = dump
|
||||
|
||||
r = session.get(api.url_for(route), headers={"Content-Type": "application/x-yaml"})
|
||||
r = api.requests.get(
|
||||
api.url_for(route), headers={"Content-Type": "application/x-yaml"}
|
||||
)
|
||||
assert yaml.safe_load(r.content) == dump
|
||||
|
||||
|
||||
@@ -343,59 +348,59 @@ def test_schema_generation():
|
||||
"""
|
||||
resp.media = PetSchema().dump({"name": "little orange"})
|
||||
|
||||
r = api.session().get("http://;/schema.yml")
|
||||
r = api.requests.get("http://;/schema.yml")
|
||||
dump = yaml.safe_load(r.content)
|
||||
|
||||
assert dump
|
||||
assert dump["openapi"] == "3.0"
|
||||
|
||||
|
||||
def test_mount_wsgi_app(api, flask, session):
|
||||
def test_mount_wsgi_app(api, flask):
|
||||
@api.route("/")
|
||||
def hello(req, resp):
|
||||
resp.text = "hello"
|
||||
|
||||
api.mount("/flask", flask)
|
||||
|
||||
r = session.get("http://;/flask")
|
||||
r = api.requests.get("http://;/flask")
|
||||
assert r.ok
|
||||
|
||||
|
||||
def test_async_class_based_views(api, session):
|
||||
def test_async_class_based_views(api):
|
||||
@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)
|
||||
r = api.requests.post(api.url_for(Resource), data=data)
|
||||
assert r.text == data
|
||||
|
||||
|
||||
def test_cookies(api, session):
|
||||
def test_cookies(api):
|
||||
@api.route("/")
|
||||
def cookies(req, resp):
|
||||
resp.media = {"cookies": req.cookies}
|
||||
resp.cookies["sent"] = "true"
|
||||
|
||||
r = session.get(api.url_for(cookies), cookies={"hello": "universe"})
|
||||
r = api.requests.get(api.url_for(cookies), cookies={"hello": "universe"})
|
||||
assert r.json() == {"cookies": {"hello": "universe"}}
|
||||
assert "sent" in r.cookies
|
||||
|
||||
r = session.get(api.url_for(cookies))
|
||||
r = api.requests.get(api.url_for(cookies))
|
||||
assert r.json() == {"cookies": {"sent": "true"}}
|
||||
|
||||
|
||||
def test_sessions(api, session):
|
||||
def test_sessions(api):
|
||||
@api.route("/")
|
||||
def view(req, resp):
|
||||
resp.session["hello"] = "world"
|
||||
resp.media = resp.session
|
||||
|
||||
r = session.get(api.url_for(view))
|
||||
r = api.requests.get(api.url_for(view))
|
||||
assert "Responder-Session" in r.cookies
|
||||
|
||||
r = session.get(api.url_for(view))
|
||||
r = api.requests.get(api.url_for(view))
|
||||
assert (
|
||||
r.cookies["Responder-Session"]
|
||||
== '{"hello": "world"}.lJVWJULPqR9kdao_oT4pUglV281bxHfGvcKQ7XF8qNqaiIZlRcMvqKNdA1-d5z7DycAx5eqmzJZoqWPP759-Cw'
|
||||
@@ -403,16 +408,16 @@ def test_sessions(api, session):
|
||||
assert r.json() == {"hello": "world"}
|
||||
|
||||
|
||||
def test_template_rendering(api, session):
|
||||
def test_template_rendering(api):
|
||||
@api.route("/")
|
||||
def view(req, resp):
|
||||
resp.content = api.template_string("{{ var }}", var="hello")
|
||||
|
||||
r = session.get(api.url_for(view))
|
||||
r = api.requests.get(api.url_for(view))
|
||||
assert r.text == "hello"
|
||||
|
||||
|
||||
def test_file_uploads(api, session):
|
||||
def test_file_uploads(api):
|
||||
@api.route("/")
|
||||
async def upload(req, resp):
|
||||
|
||||
@@ -422,14 +427,28 @@ def test_file_uploads(api, session):
|
||||
|
||||
world = io.StringIO("world")
|
||||
data = {"hello": world}
|
||||
r = session.post(api.url_for(upload), files=data)
|
||||
r = api.requests.post(api.url_for(upload), files=data)
|
||||
assert r.json() == {"files": {"hello": "world"}}
|
||||
|
||||
|
||||
def test_500(api, session):
|
||||
def test_500(api):
|
||||
@api.route("/")
|
||||
def view(rea, resp):
|
||||
def view(req, resp):
|
||||
raise ValueError
|
||||
|
||||
r = session.get(api.url_for(view))
|
||||
r = api.requests.get(api.url_for(view))
|
||||
assert not r.ok
|
||||
|
||||
|
||||
def test_404(api):
|
||||
r = api.requests.get("/foo")
|
||||
|
||||
assert r.status_code == responder.status_codes.HTTP_404
|
||||
|
||||
|
||||
def test_kinda_websockets(api):
|
||||
@api.route("/ws", websocket=True)
|
||||
async def websocket(ws):
|
||||
await ws.accept()
|
||||
await ws.send_text("Hello via websocket!")
|
||||
await ws.close()
|
||||
|
||||
Reference in New Issue
Block a user