Compare commits

..

183 Commits

Author SHA1 Message Date
kennethreitz dd0ddab610 single page 2018-10-17 04:52:02 -07:00
kennethreitz d23ac10f90 version 2018-10-17 04:49:00 -07:00
kennethreitz ec18290b8a changelog 2018-10-17 04:48:38 -07:00
kennethreitz 2c4cd39dc9 static application support 2018-10-17 04:48:33 -07:00
kennethreitz 830bad0b85 docs for #53 2018-10-17 04:47:39 -07:00
kennethreitz f14ef6fa15 #53 2018-10-17 04:45:12 -07:00
kennethreitz 7400b1c83d static support #53 2018-10-17 04:38:51 -07:00
kennethreitz e7caf39fba static_route 2018-10-17 04:25:09 -07:00
kennethreitz 09fd0fb0ca version 2018-10-17 04:19:38 -07:00
kennethreitz 72adb13c0f version 2018-10-17 04:16:22 -07:00
kennethreitz ea0e382f82 test for #71 2018-10-17 04:15:36 -07:00
kennethreitz e70cba5143 Fix for #71 2018-10-17 04:12:13 -07:00
kennethreitz 8aec244c31 openapi 2018-10-17 04:12:03 -07:00
kennethreitz 60e163164f v0.0.8 2018-10-17 03:58:23 -07:00
kennethreitz 86b9b5f3fa graphiql 2018-10-17 03:57:39 -07:00
kennethreitz 401a208767 changelog 2018-10-17 03:27:26 -07:00
kennethreitz 7d1f991ce4 changelog 2018-10-17 02:52:22 -07:00
kennethreitz 1b10378f58 merge 2018-10-17 02:33:49 -07:00
kennethreitz 2bbb379994 Merge pull request #70 from rpost/patch-1
Fix typo
2018-10-17 05:27:59 -04:00
kennethreitz a835f119e1 Merge pull request #67 from goodbadwolf/patch-1
Fix typo in quickstart example
2018-10-17 05:27:47 -04:00
kennethreitz 91d8bac680 Merge pull request #65 from taoufik07/routes_matching
Routes matching for humans
2018-10-17 05:27:32 -04:00
kennethreitz 3db10a4ce8 Merge pull request #63 from pesap/fix-typo
Fix typo in docs
2018-10-17 05:26:19 -04:00
kennethreitz 590640645b Merge pull request #62 from ybv/master
Add status code test for class based view
2018-10-17 05:26:03 -04:00
kennethreitz 7f02bfdf0c Merge pull request #61 from ewjoachim/patch-1
Fix typo
2018-10-17 05:25:49 -04:00
Radek Postołowicz e5cef0d9c0 Fix typo 2018-10-17 10:08:59 +02:00
ArtemGordinsky 85f9c33b2b Integrate GraphiQL 2018-10-17 08:00:03 +02:00
Manish P Mathai 148a430da4 Fix typo in quickstart example 2018-10-16 22:36:54 -07:00
Taoufik f7657679ac A verbose name 2018-10-17 05:07:29 +01:00
taoufik07 f0479019c3 Order the routes based on the weight 2018-10-17 04:38:08 +01:00
taoufik07 a9a4ceaa78 Add weight to Route 2018-10-17 04:37:31 +01:00
pesap c55c905621 Fix typo 2018-10-16 17:23:47 -07:00
ybv 4db2289b7e Add status code rest for class based view 2018-10-16 22:39:09 +05:30
Joachim Jablon 93172ea1d0 Fix typo 2018-10-16 14:41:30 +02:00
kennethreitz 2d935542e1 v0.0.7, immutable response object 2018-10-16 05:24:20 -07:00
kennethreitz a7ec1364f4 features 2018-10-16 04:42:44 -07:00
kennethreitz eb71ced092 graphql 2018-10-16 04:42:25 -07:00
kennethreitz 712ad0a73b mount a wsgi app 2018-10-16 04:35:30 -07:00
kennethreitz 48c0b137d5 version 2018-10-16 04:30:03 -07:00
kennethreitz dfccfcc3e5 wsgi apps 2018-10-16 04:29:44 -07:00
kennethreitz 6abe667efb Merge branch 'master' of github.com:kennethreitz/responder 2018-10-16 04:27:50 -07:00
kennethreitz c2472215ab features 2018-10-16 04:27:40 -07:00
kennethreitz ac3c1e149c Update README.md 2018-10-16 04:22:29 -07:00
kennethreitz cdf989427a mount flaks apps 2018-10-16 04:20:29 -07:00
kennethreitz ebf129edd3 /cc @tomchristie 2018-10-16 03:59:43 -07:00
kennethreitz 08c30f4baf Merge pull request #55 from sloria/upgrade-apispec
Depend on apispec>=1.0.0b1
2018-10-16 06:14:25 -04:00
kennethreitz cf6bdc20ef Merge pull request #57 from frostming/fix-docstring
Remove wsgi mentions in docstring
2018-10-16 06:14:15 -04:00
frostming 3ece644af8 Remove wsgi mentions in docstring 2018-10-16 12:54:28 +08:00
Steven Loria 3991c82c91 Depend on apispec>=1.0.0b1
The current code depends on the `apispec.yaml_utils`
module, which only exists in apispec 1.0.0b.

Close #52
2018-10-15 19:46:46 -04:00
kennethreitz 9b635253f0 Update index.rst 2018-10-15 12:29:50 -04:00
kennethreitz b62f41336e Merge pull request #54 from tomchristie/testimonial
Testimonial
2018-10-15 12:28:28 -04:00
Tom Christie f7b777c79e Testimonial 2018-10-15 16:47:12 +01:00
kennethreitz d18fa8e42a v0.0.6 2018-10-15 08:56:32 -04:00
kennethreitz 525c62ad26 content-type 2018-10-15 08:56:02 -04:00
kennethreitz 4000a6a48c fix 2018-10-15 08:45:22 -04:00
kennethreitz 5b173ed4c4 tour 2018-10-15 08:44:45 -04:00
kennethreitz f56ad73565 tour 2018-10-15 08:42:57 -04:00
kennethreitz 003991c8c6 fix 2018-10-15 08:41:39 -04:00
kennethreitz e2a32afb80 next version 2018-10-15 08:28:10 -04:00
kennethreitz f305a69bb3 tour 2018-10-15 08:26:13 -04:00
kennethreitz 84e8babd9e cleanups 2018-10-15 08:23:38 -04:00
kennethreitz aeb46d9b54 open_api spec 2018-10-15 08:21:40 -04:00
kennethreitz fafe0bd8e4 changelog 2018-10-15 07:19:01 -04:00
kennethreitz 9a2ab45957 safe dump 2018-10-15 07:18:19 -04:00
kennethreitz 66978a8cdc test yaml and form too 2018-10-15 07:14:14 -04:00
kennethreitz 1636012700 json uploads 2018-10-15 07:11:57 -04:00
kennethreitz 09206ae1e4 .pre 2018-10-15 07:05:59 -04:00
kennethreitz 9188475746 remove contributors file 2018-10-15 07:04:30 -04:00
kennethreitz 34d158a632 cleanup homepage 2018-10-15 07:02:53 -04:00
kennethreitz c06e6aa5ca changelog 2018-10-15 07:01:49 -04:00
kennethreitz f4f670f048 fix pipfile and pipfile.lock 2018-10-15 06:56:27 -04:00
kennethreitz 778d742b6e new lockfile 2018-10-15 06:55:31 -04:00
kennethreitz c8392b65b6 remove cli, next version 2018-10-15 06:53:41 -04:00
kennethreitz c0ace9c2e5 fix all the format things 2018-10-15 06:52:10 -04:00
kennethreitz dfcab7dcbf fix #47 2018-10-15 06:41:47 -04:00
kennethreitz eb0870deb1 Merge branch 'master' of github.com:kennethreitz/responder 2018-10-14 20:40:45 -04:00
kennethreitz 5b7ef34523 fix #45 2018-10-14 20:40:31 -04:00
kennethreitz 6ec728e466 Merge pull request #43 from sheb/fix-typo-route
fix typo in doc string
2018-10-14 17:22:15 -04:00
kennethreitz f12a562a08 Merge pull request #44 from kennethreitz/bnm_tests
added tests for models.QueryDict
2018-10-14 17:21:55 -04:00
Luna 17c4c95593 added tests for models.QueryDict 2018-10-14 18:20:50 +01:00
kennethreitz 9b72c90944 Merge branch 'master' of github.com:kennethreitz/responder 2018-10-14 11:57:22 -04:00
kennethreitz ec34da60a1 more tests 2018-10-14 11:57:15 -04:00
Sébastien Geffroy daa4b6368a fix typo in doc string 2018-10-14 17:51:21 +02:00
kennethreitz 931a7a1a6c Merge pull request #39 from rcatajar/patch-1
Fix typo in quickstart documentation
2018-10-14 11:47:25 -04:00
kennethreitz 69d5790078 Merge pull request #42 from kennethreitz/bnm_tests
Cleaned up redundant test function
2018-10-14 11:46:08 -04:00
kennethreitz 7571c18a55 Merge pull request #40 from tselepakis/fix-memoize
(fix) memoization functionality
2018-10-14 11:45:04 -04:00
Luna ff7ce9bdd0 linter error 2018-10-14 16:43:16 +01:00
Luna e5fc801899 added boilerplate cli 2018-10-14 16:40:57 +01:00
Konstantinos Tselepakis b362aa6813 (fix) memoization functionality
* Keep an object level cache
* Use func name as part of the dictionary key
2018-10-14 18:09:43 +03:00
Luna 652b961ac8 fixed conflict 2018-10-14 15:41:16 +01:00
Luna 652713aec4 clean up 2018-10-14 15:40:29 +01:00
kennethreitz 387b2f166b cleanup 2018-10-14 09:11:47 -04:00
kennethreitz 164b4a056a no memoize routes 2018-10-14 09:10:39 -04:00
Romain Catajar 29e514fea6 Fix typo in quickstart documentation 2018-10-14 15:10:29 +02:00
kennethreitz 310fff78c6 no benchmarks 2018-10-14 08:55:01 -04:00
kennethreitz f2efdc007c no benchmarks 2018-10-14 08:54:37 -04:00
kennethreitz b3be767923 upgrades 2018-10-14 08:46:33 -04:00
kennethreitz e86f2f3873 media() 2018-10-14 08:17:17 -04:00
kennethreitz 13d84f73d4 fix template rendering 2018-10-14 07:59:18 -04:00
kennethreitz e31342d3ba fix 2018-10-14 07:53:47 -04:00
kennethreitz daf0538bf3 fix 2018-10-14 07:52:52 -04:00
kennethreitz 451ce8b0c7 fix 2018-10-14 07:48:22 -04:00
kennethreitz b8cce14705 fix 2018-10-14 07:48:07 -04:00
kennethreitz bf1c9c650e fix 2018-10-14 07:47:45 -04:00
kennethreitz 8f6387536c fix 2018-10-14 07:46:15 -04:00
kennethreitz 56535ece11 fix 2018-10-14 07:44:13 -04:00
kennethreitz f1767719cb paragraphs 2018-10-14 07:43:43 -04:00
kennethreitz c925b06114 margin-top 2018-10-14 07:43:01 -04:00
kennethreitz 402426884d try this 2018-10-14 07:40:22 -04:00
kennethreitz df6c8a5a75 quotes 2018-10-14 07:39:51 -04:00
kennethreitz 99f5ae7125 another fix 2018-10-14 07:39:20 -04:00
kennethreitz d50a1b7d07 fix 2018-10-14 07:38:30 -04:00
kennethreitz fab3bb76f7 let's see if this works 2018-10-14 07:38:19 -04:00
kennethreitz 5025c66bb2 less testimonial 2018-10-14 07:37:22 -04:00
kennethreitz 800c153e96 Merge pull request #38 from kennethreitz/bnm_tests
Tests clean up
2018-10-14 07:29:07 -04:00
Luna 71bbda0fb7 clean up 2018-10-14 12:25:15 +01:00
kennethreitz 6e6bac429a order 2018-10-14 07:24:48 -04:00
kennethreitz 1ce091a4d9 feature tour 2018-10-14 07:24:14 -04:00
Luna a8f889be74 restructured tests 2018-10-14 12:23:33 +01:00
kennethreitz 5f33c6bfee rendering a template 2018-10-14 07:23:10 -04:00
Luna 6a290c49d8 Merge remote-tracking branch 'origin' into bnm_tests 2018-10-14 12:21:23 +01:00
kennethreitz b304d5d784 real fix 2018-10-14 07:12:45 -04:00
kennethreitz cfe83b97d9 fix 2018-10-14 07:12:27 -04:00
kennethreitz 2fec2bf560 response headers 2018-10-14 07:11:14 -04:00
kennethreitz 73dc1a7839 ! 2018-10-14 07:02:22 -04:00
kennethreitz 66fe951831 python 2018-10-14 07:01:13 -04:00
kennethreitz 7991bcbf1a note 2018-10-14 06:59:26 -04:00
kennethreitz de9516563a await 2018-10-14 06:58:56 -04:00
kennethreitz 27fefb821c quickstart 2018-10-14 06:58:14 -04:00
kennethreitz c195894db9 yaml 2018-10-14 06:57:39 -04:00
kennethreitz 6777b4d370 data 2018-10-14 06:55:54 -04:00
kennethreitz 09269c22a2 fix 2018-10-14 06:46:21 -04:00
kennethreitz 2e24a2f079 cleanup 2018-10-14 06:45:23 -04:00
kennethreitz 5d9932dd61 more quickstart 2018-10-14 06:44:18 -04:00
kennethreitz 062064213a improvements to docs 2018-10-14 06:34:04 -04:00
kennethreitz a2ae3ffb2b comments 2018-10-14 06:27:13 -04:00
kennethreitz 6cb4a0a3eb fix for encodings 2018-10-14 06:26:20 -04:00
kennethreitz f17c49091f cleanup 2018-10-14 06:23:23 -04:00
kennethreitz c16afc07df raises 2018-10-14 06:23:09 -04:00
kennethreitz 1616a96b2c fix 2018-10-14 06:21:59 -04:00
kennethreitz 261601230a better tests 2018-10-14 06:21:41 -04:00
kennethreitz 453a38df54 docstrings 2018-10-14 06:08:50 -04:00
kennethreitz 5b004a849f really fix encodings 2018-10-14 06:05:41 -04:00
kennethreitz 29d811d3fd conflict 2018-10-14 06:04:44 -04:00
kennethreitz 36c5739318 fix encoding 2018-10-14 06:03:57 -04:00
kennethreitz b3f9c67d34 cli 2018-10-14 05:47:04 -04:00
kennethreitz bc8eb802f7 remove heroku example 2018-10-14 05:46:36 -04:00
kennethreitz a138eead74 Merge pull request #35 from frostming/fix-req-content
Fix request.content
2018-10-14 05:00:13 -04:00
Frost Ming a700a0e1b1 Fix request.content 2018-10-14 10:31:03 +08:00
kennethreitz 205a33a241 Merge pull request #29 from ArtemGordinsky/ignore_missing_accept_header
Don't break when "Accept" header is missing
2018-10-13 22:08:10 -04:00
kennethreitz c88fd94c8b Merge pull request #33 from javad94/master
fixed typos
2018-10-13 22:07:40 -04:00
kennethreitz a2b4e2e87c Merge pull request #30 from gdamjan/master
fix docstring to remove mention of Waitress
2018-10-13 22:07:20 -04:00
Javad 4a8f1e95ba fixed typos 2018-10-14 01:18:14 +03:30
Javad 3a847d921e fixed typo 2018-10-14 01:15:11 +03:30
Javad 806fdb9541 fixed typos 2018-10-14 01:05:29 +03:30
Luna cf1adbdb01 Merge branch 'master' into bnm_tests 2018-10-13 21:57:29 +01:00
Luna 349d08e799 added conftest 2018-10-13 21:56:52 +01:00
Damjan Georgievski d680c7ed83 fix docstring to remove mention of Waitress 2018-10-13 19:45:31 +02:00
Artem Gordinsky d4cb7a711b Don't break when "Accept" header is missing 2018-10-13 17:30:52 +02:00
kennethreitz bb6e19e7cd requirements.txt 2018-10-13 09:54:53 -04:00
kennethreitz 1c3ea53e63 Merge branch 'master' of github.com:kennethreitz/responder 2018-10-13 09:50:19 -04:00
kennethreitz 88e17029c5 fixes 2018-10-13 09:50:13 -04:00
kennethreitz 588e91b19f fix 2018-10-13 09:46:34 -04:00
kennethreitz 8cc2e7b6f1 fix 2018-10-13 09:46:29 -04:00
kennethreitz 222353b532 version 2018-10-13 09:46:14 -04:00
kennethreitz b88b266fd5 profile 2018-10-13 09:40:33 -04:00
kennethreitz 60e6fb99af fix 2018-10-13 09:27:11 -04:00
kennethreitz 65b60e57b2 Merge pull request #27 from kennethreitz/pytest
added addopts in pytest
2018-10-13 09:26:40 -04:00
kennethreitz 16a8402bf4 api.static_url 2018-10-13 09:23:05 -04:00
Luna 5896411136 added addopts in pytest 2018-10-13 14:14:59 +01:00
kennethreitz 0bb74a7885 pytest.ini 2018-10-13 09:13:33 -04:00
kennethreitz 86dfb9231f oops 2018-10-13 09:10:51 -04:00
kennethreitz 7198ce3eb0 fomatting 2018-10-13 09:09:08 -04:00
kennethreitz 08fecf1eb2 Merge branch 'bnm_tests' 2018-10-13 09:08:16 -04:00
kennethreitz 3eda26ca94 Merge branch 'master' into bnm_tests 2018-10-13 09:07:44 -04:00
kennethreitz d907914c7c Merge pull request #25 from 0xflotus/patch-1
fixed Characteristics
2018-10-13 09:02:54 -04:00
kennethreitz 266ab48fed better tests 2018-10-13 09:03:20 -04:00
Luna 3325cffa91 added entry to gitignore 2018-10-13 14:02:17 +01:00
Luna 43469ac62a Merge branch 'master' of https://github.com/kennethreitz/responder into bnm_tests 2018-10-13 14:00:59 +01:00
0xflotus a5c953fdb6 fixed Characteristics 2018-10-13 15:00:38 +02:00
Luna 627c46e458 added test cases for routes.py 2018-10-13 13:59:46 +01:00
kennethreitz 205eb34adc cleanup 2018-10-13 08:51:01 -04:00
kennethreitz 125e14d377 responder 2018-10-13 08:50:23 -04:00
kennethreitz a51c8a700b fix 2018-10-13 08:49:15 -04:00
31 changed files with 1491 additions and 821 deletions
+11
View File
@@ -1,5 +1,16 @@
.vscode/
.cache
.idea
.coverage
.pytest_cache
.DS_Store
coverage.xml
__pycache__
tests/__pycache__
build
responder.egg-info/
dist/
app.py
app2.py
+36
View File
@@ -0,0 +1,36 @@
# v0.1.0
- Prototype of static application support.
# v0.0.10
- Bufgix for async class-based views.
# v0.0.9
- Bugfix for async class-based views.
# v0.0.8
- GraphiQL Support.
- Improvement to route selection.
# v0.0.7
- Immutable Request object.
# v0.0.6:
- Ability to mount WSGI apps.
- Supply content-type when serving up the schema.
# v0.0.5:
- OpenAPI Schema support.
- Safe load/dump yaml.
# v0.0.4:
- Asyncronous support for data uploads.
- Bug fixes.
# v0.0.3:
- Bug fixes.
# v0.0.2
- Switch to ASGI/Starlette.
# v0.0.1
- Conception!
-4
View File
@@ -1,4 +0,0 @@
- Kenneth Reitz (primary)
- Tom Christie
- Bruno Oliveira
- serhii73
+1 -5
View File
@@ -5,9 +5,6 @@ name = "pypi"
[packages]
responder = {editable = true, path = "."}
uvicorn = "*"
starlette = "*"
aiofiles = "*"
[dev-packages]
pytest = "*"
@@ -16,8 +13,7 @@ black = "*"
twine = "*"
flask = "*"
sphinx = "*"
locust = "*"
locustio = "*"
marshmallow = "*"
[requires]
python_version = "3.7"
Generated
+47 -134
View File
@@ -1,7 +1,7 @@
{
"_meta": {
"hash": {
"sha256": "620c4b82d439a27e1afdbe54a6a77130cc52060846d4a6e322a5b064ff68e8fd"
"sha256": "9b959d9507c521f6088646507633207db03afec6ac31aeab07adf0d737dbb45b"
},
"pipfile-spec": 6,
"requires": {
@@ -21,7 +21,6 @@
"sha256:021ea0ba314a86027c166ecc4b4c07f2d40fc0f4b3a950d1868a0f2571c2bbee",
"sha256:1e644c2573f953664368de28d2aa4c89dfd64550429d0c27c4680ccd3aa4985d"
],
"index": "pypi",
"version": "==0.4.0"
},
"aniso8601": {
@@ -31,12 +30,33 @@
],
"version": "==3.0.2"
},
"apispec": {
"hashes": [
"sha256:c2e6ac6471aaf7c6ec6d12714821898910c6b3c87c189de9a2e3754786b86ada",
"sha256:fa7dfa8a292bae9b1e70c44a50bf61901805821726c5b804568c9f2501f57ebb"
],
"version": "==1.0.0b3"
},
"asgiref": {
"hashes": [
"sha256:9b05dcd41a6a89ca8c6e7f7e4089c3f3e76b5af60aebb81ae6d455ad81989c97",
"sha256:b21dc4c43d7aba5a844f4c48b8f49d56277bc34937fd9f9cb93ec97fde7e3082"
],
"version": "==2.3.2"
},
"async-timeout": {
"hashes": [
"sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f",
"sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3"
],
"version": "==3.0.1"
},
"certifi": {
"hashes": [
"sha256:376690d6f16d32f9d1fe8932551d80b23e9d393a8578c5633a2ed39a64861638",
"sha256:456048c7e371c089d0a77a5212fb37a2c2dce1e24146e3b7e0261736aaeaa22a"
"sha256:339dc09518b07e2fa7eda5450740925974815557727d6bd35d319c1524a04a4c",
"sha256:6d58c986d22b038c8c0df30d639f23a3e6d172a05c3583e766f4c0b785c0986a"
],
"version": "==2018.8.24"
"version": "==2018.10.15"
},
"chardet": {
"hashes": [
@@ -105,6 +125,13 @@
],
"version": "==1.0"
},
"marshmallow": {
"hashes": [
"sha256:82b201ad767eb54de371c08cb1db6ca4ad2a728fa41b831e3781bf944815eb38",
"sha256:c250f37ac0e249a8287394a60d91f6240b674642ad999e66cd09463dbccd1d4f"
],
"version": "==3.0.0b18"
},
"parse": {
"hashes": [
"sha256:9dd6048ea212cd032a342f9f6aa2b7bc222f7407c7e37bdc2777fecd36897437"
@@ -168,10 +195,9 @@
},
"starlette": {
"hashes": [
"sha256:9f42bba2c3140402df7fe645b79aadc694cca80140d7bdd43b8a7175f84a8a70"
"sha256:2c7ec085440fce7146a9be2b6d53b7110c3866ce6fa03d901efdc1fbe97e0f36"
],
"index": "pypi",
"version": "==0.4.1"
"version": "==0.4.2"
},
"urllib3": {
"hashes": [
@@ -184,7 +210,6 @@
"hashes": [
"sha256:8de03999a936d8704f07cc3b1d3a3edb6922a068b64d84b4f5e49604c8b70a11"
],
"index": "pypi",
"version": "==0.3.12"
},
"websockets": {
@@ -267,10 +292,10 @@
},
"certifi": {
"hashes": [
"sha256:376690d6f16d32f9d1fe8932551d80b23e9d393a8578c5633a2ed39a64861638",
"sha256:456048c7e371c089d0a77a5212fb37a2c2dce1e24146e3b7e0261736aaeaa22a"
"sha256:339dc09518b07e2fa7eda5450740925974815557727d6bd35d319c1524a04a4c",
"sha256:6d58c986d22b038c8c0df30d639f23a3e6d172a05c3583e766f4c0b785c0986a"
],
"version": "==2018.8.24"
"version": "==2018.10.15"
},
"cffi": {
"hashes": [
@@ -307,7 +332,6 @@
"sha256:edabd457cd23a02965166026fd9bfd196f4324fe6032e866d0f3bd0301cd486f",
"sha256:fdf1c1dc5bafc32bc5d08b054f94d659422b05aba244d6be4ddc1c72d9aa70fb"
],
"markers": "sys_platform == 'win32' and platform_python_implementation == 'CPython'",
"version": "==1.11.5"
},
"chardet": {
@@ -395,59 +419,6 @@
],
"version": "==0.16.0"
},
"gevent": {
"hashes": [
"sha256:1f277c5cf060b30313c5f9b91588f4c645e11839e14a63c83fcf6f24b1bc9b95",
"sha256:298a04a334fb5e3dcd6f89d063866a09155da56041bc94756da59db412cb45b1",
"sha256:30e9b2878d5b57c68a40b3a08d496bcdaefc79893948989bb9b9fab087b3f3c0",
"sha256:33533bc5c6522883e4437e901059fe5afa3ea74287eeea27a130494ff130e731",
"sha256:3f06f4802824c577272960df003a304ce95b3e82eea01dad2637cc8609c80e2c",
"sha256:419fd562e4b94b91b58cccb3bd3f17e1a11f6162fca6c591a7822bc8a68f023d",
"sha256:4ea938f44b882e02cca9583069d38eb5f257cc15a03e918980c307e7739b1038",
"sha256:51143a479965e3e634252a0f4a1ea07e5307cf8dc773ef6bf9dfe6741785fb4c",
"sha256:5bf9bd1dd4951552d9207af3168f420575e3049016b9c10fe0c96760ce3555b7",
"sha256:6004512833707a1877cc1a5aea90fd182f569e089bc9ab22a81d480dad018f1b",
"sha256:640b3b52121ab519e0980cb38b572df0d2bc76941103a697e828c13d76ac8836",
"sha256:6951655cc18b8371d823e81c700883debb0f633aae76f425dfeb240f76b95a67",
"sha256:71eeb8d9146e8131b65c3364bb760b097c21b7b9fdbec91bf120685a510f997a",
"sha256:7c899e5a6f94d6310352716740f05e41eb8c52d995f27fc01e90380913aa8f22",
"sha256:8465f84ba31aaf52a080837e9c5ddd592ab0a21dfda3212239ce1e1796f4d503",
"sha256:99de2e38dde8669dd30a8a1261bdb39caee6bd00a5f928d01dfdb85ab0502562",
"sha256:9fa4284b44bc42bef6e437488d000ae37499ccee0d239013465638504c4565b7",
"sha256:a1beea0443d3404c03e069d4c4d9fc13d8ec001771c77f9a23f01911a41f0e49",
"sha256:a66a26b78d90d7c4e9bf9efb2b2bd0af49234604ac52eaca03ea79ac411e3f6d",
"sha256:a94e197bd9667834f7bb6bd8dff1736fab68619d0f8cd78a9c1cebe3c4944677",
"sha256:ac0331d3a3289a3d16627742be9c8969f293740a31efdedcd9087dadd6b2da57",
"sha256:d26b57c50bf52fb38dadf3df5bbecd2236f49d7ac98f3cf32d6b8a2d25afc27f",
"sha256:fd23b27387d76410eb6a01ea13efc7d8b4b95974ba212c336e8b1d6ab45a9578"
],
"version": "==1.3.7"
},
"greenlet": {
"hashes": [
"sha256:000546ad01e6389e98626c1367be58efa613fa82a1be98b0c6fc24b563acc6d0",
"sha256:0d48200bc50cbf498716712129eef819b1729339e34c3ae71656964dac907c28",
"sha256:23d12eacffa9d0f290c0fe0c4e81ba6d5f3a5b7ac3c30a5eaf0126bf4deda5c8",
"sha256:37c9ba82bd82eb6a23c2e5acc03055c0e45697253b2393c9a50cef76a3985304",
"sha256:51503524dd6f152ab4ad1fbd168fc6c30b5795e8c70be4410a64940b3abb55c0",
"sha256:8041e2de00e745c0e05a502d6e6db310db7faa7c979b3a5877123548a4c0b214",
"sha256:81fcd96a275209ef117e9ec91f75c731fa18dcfd9ffaa1c0adbdaa3616a86043",
"sha256:853da4f9563d982e4121fed8c92eea1a4594a2299037b3034c3c898cb8e933d6",
"sha256:8b4572c334593d449113f9dc8d19b93b7b271bdbe90ba7509eb178923327b625",
"sha256:9416443e219356e3c31f1f918a91badf2e37acf297e2fa13d24d1cc2380f8fbc",
"sha256:9854f612e1b59ec66804931df5add3b2d5ef0067748ea29dc60f0efdcda9a638",
"sha256:99a26afdb82ea83a265137a398f570402aa1f2b5dfb4ac3300c026931817b163",
"sha256:a19bf883b3384957e4a4a13e6bd1ae3d85ae87f4beb5957e35b0be287f12f4e4",
"sha256:a9f145660588187ff835c55a7d2ddf6abfc570c2651c276d3d4be8a2766db490",
"sha256:ac57fcdcfb0b73bb3203b58a14501abb7e5ff9ea5e2edfa06bb03035f0cff248",
"sha256:bcb530089ff24f6458a81ac3fa699e8c00194208a724b644ecc68422e1111939",
"sha256:beeabe25c3b704f7d56b573f7d2ff88fc99f0138e43480cecdfcaa3b87fe4f87",
"sha256:d634a7ea1fc3380ff96f9e44d8d22f38418c1c381d5fac680b272d7d90883720",
"sha256:d97b0661e1aead761f0ded3b769044bb00ed5d33e1ec865e891a8b128bf7c656"
],
"markers": "platform_python_implementation == 'CPython'",
"version": "==0.4.15"
},
"idna": {
"hashes": [
"sha256:156a6814fb5ac1fc6850fb002e0852d56c0c8d2531923a51032d1b70760e186e",
@@ -475,27 +446,19 @@
],
"version": "==2.10"
},
"locust": {
"hashes": [
"sha256:aaa38b525795e9c1a35ac3620543cf4e62f82948714f60a32023ea8c9b8edc2e"
],
"index": "pypi",
"version": "==0.0"
},
"locustio": {
"hashes": [
"sha256:be7b44468b8683def983e7451ab505cd85fff8d06f6b75ad7c899cedbbf789ac",
"sha256:c77b471e0e08e215c93a7af9a95b79193268072873fbbc0effca40f3d9b58be4"
],
"index": "pypi",
"version": "==0.9.0"
},
"markupsafe": {
"hashes": [
"sha256:a6be69091dac236ea9c6bc7d012beab42010fa914c459791d627dad4910eb665"
],
"version": "==1.0"
},
"marshmallow": {
"hashes": [
"sha256:82b201ad767eb54de371c08cb1db6ca4ad2a728fa41b831e3781bf944815eb38",
"sha256:c250f37ac0e249a8287394a60d91f6240b674642ad999e66cd09463dbccd1d4f"
],
"version": "==3.0.0b18"
},
"mccabe": {
"hashes": [
"sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42",
@@ -511,26 +474,6 @@
],
"version": "==4.3.0"
},
"msgpack": {
"hashes": [
"sha256:0b3b1773d2693c70598585a34ca2715873ba899565f0a7c9a1545baef7e7fbdc",
"sha256:0bae5d1538c5c6a75642f75a1781f3ac2275d744a92af1a453c150da3446138b",
"sha256:0ee8c8c85aa651be3aa0cd005b5931769eaa658c948ce79428766f1bd46ae2c3",
"sha256:1369f9edba9500c7a6489b70fdfac773e925342f4531f1e3d4c20ac3173b1ae0",
"sha256:22d9c929d1d539f37da3d1b0e16270fa9d46107beab8c0d4d2bddffffe895cee",
"sha256:2ff43e3247a1e11d544017bb26f580a68306cec7a6257d8818893c1fda665f42",
"sha256:31a98047355d34d047fcdb55b09cb19f633cf214c705a765bd745456c142130c",
"sha256:8767eb0032732c3a0da92cbec5ac186ef89a3258c6edca09161472ca0206c45f",
"sha256:8acc8910218555044e23826980b950e96685dc48124a290c86f6f41a296ea172",
"sha256:ab189a6365be1860a5ecf8159c248f12d33f79ea799ae9695fa6a29896dcf1d4",
"sha256:cfd6535feb0f1cf1c7cdb25773e965cc9f92928244a8c3ef6f8f8a8e1f7ae5c4",
"sha256:e274cd4480d8c76ec467a85a9c6635bbf2258f0649040560382ab58cabb44bcf",
"sha256:f86642d60dca13e93260187d56c2bef2487aa4d574a669e8ceefcf9f4c26fd00",
"sha256:f8a57cbda46a94ed0db55b73e6ab0c15e78b4ede8690fa491a0e55128d552bb0",
"sha256:fcea97a352416afcbccd7af9625159d80704a25c519c251c734527329bb20d0e"
],
"version": "==0.5.6"
},
"packaging": {
"hashes": [
"sha256:0886227f54515e592aaa2e5a553332c73962917f2831f1b0f9b9f4380a4b9807",
@@ -608,36 +551,6 @@
],
"version": "==2018.5"
},
"pyzmq": {
"hashes": [
"sha256:25a0715c8f69cf72f67cfe5a68a3f3ed391c67c063d2257bec0fe7fc2c7f08f8",
"sha256:2bab63759632c6b9e0d5bf19cc63c3b01df267d660e0abcf230cf0afaa966349",
"sha256:30ab49d99b24bf0908ebe1cdfa421720bfab6f93174e4883075b7ff38cc555ba",
"sha256:32c7ca9fc547a91e3c26fc6080b6982e46e79819e706eb414dd78f635a65d946",
"sha256:41219ae72b3cc86d97557fe5b1ef5d1adc1057292ec597b50050874a970a39cf",
"sha256:4b8c48a9a13cea8f1f16622f9bd46127108af14cd26150461e3eab71e0de3e46",
"sha256:55724997b4a929c0d01b43c95051318e26ddbae23565018e138ae2dc60187e59",
"sha256:65f0a4afae59d4fc0aad54a917ab599162613a761b760ba167d66cc646ac3786",
"sha256:6f88591a8b246f5c285ee6ce5c1bf4f6bd8464b7f090b1333a446b6240a68d40",
"sha256:75022a4c60dcd8765bb9ca32f6de75a0ec83b0d96e0309dc479f4c7b21f26cb7",
"sha256:76ea493bfab18dcb090d825f3662b5612e2def73dffc196d51a5194b0294a81d",
"sha256:7b60c045b80709e4e3c085bab9b691e71761b44c2b42dbb047b8b498e7bc16b3",
"sha256:8e6af2f736734aef8ed6f278f9f552ec7f37b1a6b98e59b887484a840757f67d",
"sha256:9ac2298e486524331e26390eac14e4627effd3f8e001d4266ed9d8f1d2d31cce",
"sha256:9ba650f493a9bc1f24feca1d90fce0e5dd41088a252ac9840131dfbdbf3815ca",
"sha256:a02a4a385e394e46012dc83d2e8fd6523f039bb52997c1c34a2e0dd49ed839c1",
"sha256:a3ceee84114d9f5711fa0f4db9c652af0e4636c89eabc9b7f03a3882569dd1ed",
"sha256:a72b82ac1910f2cf61a49139f4974f994984475f771b0faa730839607eeedddf",
"sha256:ab136ac51027e7c484c53138a0fab4a8a51e80d05162eb7b1585583bcfdbad27",
"sha256:c095b224300bcac61e6c445e27f9046981b1ac20d891b2f1714da89d34c637c8",
"sha256:c5cc52d16c06dc2521340d69adda78a8e1031705924e103c0eb8fc8af861d810",
"sha256:d612e9833a89e8177f8c1dc68d7b4ff98d3186cd331acd616b01bbdab67d3a7b",
"sha256:e828376a23c66c6fe90dcea24b4b72cd774f555a6ee94081670872918df87a19",
"sha256:e9767c7ab2eb552796440168d5c6e23a99ecaade08dda16266d43ad461730192",
"sha256:ebf8b800d42d217e4710d1582b0c8bff20cdcb4faad7c7213e52644034300924"
],
"version": "==17.1.2"
},
"readme-renderer": {
"hashes": [
"sha256:237ca8705ffea849870de41101dba41543561da05c0ae45b2f1c547efa9843d2",
@@ -697,10 +610,10 @@
},
"tqdm": {
"hashes": [
"sha256:18f1818ce951aeb9ea162ae1098b43f583f7d057b34d706f66939353d1208889",
"sha256:df02c0650160986bac0218bb07952245fc6960d23654648b5d5526ad5a4128c9"
"sha256:a0be569511161220ff709a5b60d0890d47921f746f1c737a11d965e1b29e7b2e",
"sha256:e293e6d7a7f41a529a27f8d6624ab11544ccbfe82a205af6fad102545099fc21"
],
"version": "==4.26.0"
"version": "==4.27.0"
},
"twine": {
"hashes": [
+3 -50
View File
@@ -6,14 +6,11 @@
[![image](https://img.shields.io/pypi/l/responder.svg)](https://pypi.org/project/responder/)
[![image](https://img.shields.io/pypi/pyversions/responder.svg)](https://pypi.org/project/responder/)
[![image](https://img.shields.io/github/contributors/kennethreitz/responder.svg)](https://github.com/kennethreitz/responder/graphs/contributors)
[![image](https://img.shields.io/badge/Say%20Thanks-!-1EAEDB.svg)](https://saythanks.io/to/kennethreitz)
[![](https://github.com/kennethreitz/responder/raw/master/ext/small.jpg)](http://python-responder.org/)
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
@@ -27,7 +24,7 @@ if __name__ == '__main__':
api.run()
```
That `async` declaration is optional.
That `async` declaration is optional. [View documentation](http://python-responder.org).
This gets you a ASGI app, with a production static files server pre-installed, jinja2 templating (without additional imports), and a production webserver based on uvloop, serving up requests with gzip compression automatically.
@@ -36,15 +33,12 @@ This gets you a ASGI app, with a production static files server pre-installed, j
> "Pleasantly very taken with python-responder. [@kennethreitz](https://twitter.com/kennethreitz) at his absolute best." —Rudraksh M.K.
> "Buckle up!" —Tom Christie of [APIStar](https://github.com/encode/apistar) and [Django REST Framework](https://www.django-rest-framework.org/)
> "ASGI is going to enable all sorts of new high-performance web services. It's awesome to see Responder starting to take advantage of that." — Tom Christie author of [Django REST Framework](https://www.django-rest-framework.org/)
> "I love that you are exploring new patterns. Go go go!" — Danny Greenfield, author of [Two Scoops of Django]()
> "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.
> "The most ambitious crossover event in history." —Pablo Cabezas, [on Tom Christie joining the project](https://twitter.com/pabloteleco/status/1050841098321620992?s=20)
## More Examples
Class-based views (and setting some headers and stuff):
@@ -126,7 +120,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 +133,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
+6
View File
@@ -22,6 +22,7 @@
}
pre,
.pre,
.class em,
.descname,
.method em {
@@ -75,6 +76,11 @@
display: none;
}
#testimonials p.attribution {
margin-top: -1em;
}
/* "Quick Search" should be not be shown for now. */
div#searchbox h3 {
+35
View File
@@ -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
+41 -170
View File
@@ -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
@@ -47,179 +44,87 @@ pre-installed, jinja2 templating (without additional imports), and a
production webserver based on uvloop, serving up requests with gzip
compression automatically.
Features
--------
- A pleasant API, with a single import statement.
- Class-based views without inheritence.
- ASGI framework, the future of Python web services.
- The ability to mount any ASGI / WSGI app at a subroute.
- *f-string syntax* route declaration.
- 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.
- Single-page webapp support!
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`_
"ASGI is going to enable all sorts of new high-performance web services. It's awesome to see Responder starting to take advantage of that."
“I love that you are exploring new patterns. Go go go!” — Danny
Greenfield, author of `Two Scoops of Django`_
—Tom Christie, author of `Django REST Framework`_
..
“Love what I have seen while its in progress! Many features of
Responder are from my wishlist for Flask, and its 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):
--------------------
.. code-block:: shell
$ pipenv install git+https://github.com/kennethreitz/responder.git#egg=responder
$ pipenv install responder
✨🍰✨
Or, install from the development branch:
.. code-block:: shell
$ pipenv install -e git+https://github.com/kennethreitz/responder.git#egg=responder
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 +145,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
==================
+126
View File
@@ -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 req.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}``.
+153
View File
@@ -0,0 +1,153 @@
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))
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
----------------------
Responder comes with built-in support for OpenAPI / marshmallow::
import responder
from marshmallow import Schema, fields
api = responder.API(title="Web Service", version="1.0", openapi="3.0")
@api.schema("Pet")
class PetSchema(Schema):
name = fields.Str()
@api.route("/")
def route(req, resp):
"""A cute furry animal endpoint.
---
get:
description: Get a random pet
responses:
200:
description: A pet to be returned
schema:
$ref = "#/components/schemas/Pet"
"""
resp.media = PetSchema().dump({"name": "little orange"})
::
>>> r = api.session().get("http://;/schema.yml")
>>> print(r.text)
components:
parameters: {}
schemas:
Pet:
properties:
name: {type: string}
type: object
info: {title: Web Service, version: 1.0}
openapi: '3.0'
paths:
/:
get:
description: Get a random pet
responses:
200: {description: A pet to be returned, schema: $ref = "#/components/schemas/Pet"}
tags: []
Mount a WSGI App (e.g. Flask)
-----------------------------
Responder gives you the ability to mount another ASGI / WSGI app at a subroute::
import responder
from flask import Flask
api = responder.API()
flask = Flask(__name__)
@flask.route('/')
def hello():
return 'hello'
api.mount('/flask', flask)
That's it!
HSTS (Redirect to HTTPS)
------------------------
Want HSTS?
::
api = responder.API(enable_hsts=True)
Boom.
+4
View File
@@ -0,0 +1,4 @@
[pytest]
; addopts= -rsxX -s -v --strict
filterwarnings =
error::UserWarning
+1 -1
View File
@@ -1 +1 @@
__version__ = "0.0.2"
__version__ = "0.1.0"
+162 -67
View File
@@ -11,25 +11,17 @@ from graphql_server import encode_execution_results, json_encode, default_format
from starlette.routing import Router
from starlette.staticfiles import StaticFiles
from starlette.testclient import TestClient
from apispec import APISpec
from apispec.ext.marshmallow import MarshmallowPlugin
from apispec import yaml_utils
from asgiref.wsgi import WsgiToAsgi
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
from .templates import GRAPHIQL
# TODO: consider moving status codes here
class API:
@@ -43,15 +35,32 @@ class API:
status_codes = status_codes
def __init__(
self, static_dir="static", templates_dir="templates", enable_hsts=False
self,
*,
title=None,
version=None,
openapi=None,
openapi_route="/schema.yml",
static_dir="static",
static_route="/static",
templates_dir="templates",
enable_hsts=False,
):
self.title = title
self.version = version
self.openapi_version = openapi
self.static_dir = Path(os.path.abspath(static_dir))
self.static_route = static_route
self.templates_dir = Path(os.path.abspath(templates_dir))
self.built_in_templates_dir = Path(
os.path.abspath(os.path.dirname(__file__) + "/templates")
)
self.routes = {}
self.schemas = {}
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()
@@ -63,6 +72,36 @@ class API:
self._session = None
self.background = BackgroundQueue()
if self.openapi_version:
self.add_route(openapi_route, self.schema_response)
self.static_enabled = False
@property
def _apispec(self):
spec = APISpec(
title=self.title,
version=self.version,
openapi_version=self.openapi_version,
plugins=[MarshmallowPlugin()],
)
for route in self.routes:
if self.routes[route].description:
operations = yaml_utils.load_operations_from_docstring(
self.routes[route].description
)
spec.add_path(path=route, operations=operations)
for name, schema in self.schemas.items():
spec.definition(name, schema=schema)
return spec
@property
def openapi(self):
return self._apispec.to_yaml()
def __call__(self, scope):
path = scope["path"]
root_path = scope.get("root_path", "")
@@ -72,7 +111,11 @@ class API:
if path.startswith(path_prefix):
scope["path"] = path[len(path_prefix) :]
scope["root_path"] = root_path + path_prefix
return app(scope)
try:
return app(scope)
except TypeError:
app = WsgiToAsgi(app)
return app(scope)
# Call the main dispatcher.
async def asgi(receive, send):
@@ -84,6 +127,32 @@ class API:
return asgi
def add_schema(self, name, schema, check_existing=True):
"""Adds a mashmallow schema to the API specification."""
if check_existing:
assert name not in self.schemas
self.schemas[name] = schema
def schema(self, name, **options):
"""Decorator for creating new routes around function and class definitions.
Usage::
from marshmallow import Schema, fields
@api.schema("Pet")
class PetSchema(Schema):
name = fields.Str()
"""
def decorator(f):
self.add_schema(name=name, schema=f, **options)
return f
return decorator
def path_matches_route(self, path):
"""Given a path portion of a URL, tests that it matches against any registered route.
@@ -117,30 +186,34 @@ class API:
view = self.routes[route].endpoint(**params)
except TypeError:
view = self.routes[route].endpoint
try:
# GraphQL Schema.
assert hasattr(view, "execute")
self.graphql_response(req, resp, schema=view)
except AssertionError:
if self.routes[route].is_graphql:
await self.graphql_response(req, resp, schema=view)
else:
# 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:
getattr(view, "on_request")(req, resp)
r = getattr(view, "on_request")(req, resp)
if hasattr(r, "send"):
await r
except AttributeError:
pass
# Then on_get.
method = req.method.lower()
method = req.method
try:
getattr(view, f"on_{method}")(req, resp)
r = getattr(view, f"on_{method}")(req, resp)
if hasattr(r, "send"):
await r
except AttributeError:
pass
else:
@@ -148,23 +221,43 @@ class API:
return resp
def add_route(self, route, endpoint, *, check_existing=True):
# TODO: add graphiql
def add_route(self, route, endpoint=None, *, static=True, check_existing=True):
"""Add a route to the API.
:param route: A string representation of the route.
:param endpoint: The endpoint for the route -- can be a callable, a class, a WSGI application, or graphene schema (GraphQL).
:param endpoint: The endpoint for the route -- can be a callable, a class, or graphene schema (GraphQL).
:param static: if True, and no endpoint was passed, render "static/index.html", and forward all undefined routes to this view.
:param check_existing: If ``True``, an AssertionError will be raised, if the route is already defined.
"""
if check_existing:
assert route not in self.routes
# TODO: Support grpahiql.
if not endpoint and static:
self.static_enabled = True
endpoint = self.static_response
self.routes[route] = Route(route, endpoint)
# TODO: A better datastructer or sort it once the app is loaded
self.routes = dict(
sorted(self.routes.items(), key=lambda item: item[1]._weight())
)
def default_response(self, req, resp):
resp.status_code = HTTP_404
resp.text = "Not found."
if self.static_enabled:
self.static_response(req, resp)
else:
resp.status_code = status_codes.HTTP_404
resp.text = "Not found."
def static_response(self, req, resp):
index = (self.static_dir / "index.html").resolve()
if os.path.exists(index):
with open(index, "r") as f:
resp.text = f.read()
def schema_response(self, req, resp):
resp.status_code = status_codes.HTTP_200
resp.headers["Content-Type"] = "application/x-yaml"
resp.content = self.openapi
def redirect(
self, resp, location, *, set_text=True, status_code=status_codes.HTTP_301
@@ -185,9 +278,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 +299,14 @@ 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):
show_graphiql = req.method == "get" and req.accepts("text/html")
if show_graphiql:
resp.content = self.template_string(GRAPHIQL, endpoint=req.url.path)
return
query = await self._resolve_graphql_query(req)
result = schema.execute(query)
result, status_code = encode_execution_results(
[result],
@@ -219,13 +318,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!"
"""
@@ -235,16 +334,16 @@ class API:
return decorator
def mount(self, route, asgi_app):
"""Mounts a WSGI application at a given route.
def mount(self, route, app):
"""Mounts an WSGI / ASGI application at a given route.
:param route: String representation of the route to be used (shouldn't be parameterized).
:param wsgi_app: The other WSGI app (e.g. a Flask app).
:param app: The other WSGI / ASGI app.
"""
self.apps.update({route: asgi_app})
self.apps.update({route: app})
def session(self, base_url="http://;"):
"""Testing HTTP client. Returns a Requests session object, able to send HTTP requests to the WSGI application.
"""Testing HTTP client. Returns a Requests session object, able to send HTTP requests to the Responder application.
:param base_url: The URL to mount the connection adaptor to.
"""
@@ -253,7 +352,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 +361,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.
@@ -278,20 +380,13 @@ class API:
# Give reference to self.
values.update(api=self)
if auto_escape:
env = jinja2.Environment(
loader=jinja2.FileSystemLoader(
str(self.templates_dir), followlinks=True
),
autoescape=jinja2.select_autoescape(["html", "xml"]),
)
else:
env = jinja2.Environment(
loader=jinja2.FileSystemLoader(
str(self.templates_dir), followlinks=True
),
autoescape=jinja2.select_autoescape([]),
)
env = jinja2.Environment(
loader=jinja2.FileSystemLoader(
[str(self.templates_dir), str(self.built_in_templates_dir)],
followlinks=True,
),
autoescape=jinja2.select_autoescape(["html", "xml"] if auto_escape else []),
)
template = env.get_template(name)
return template.render(**values)
@@ -322,18 +417,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
View File
@@ -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
View File
@@ -1 +0,0 @@
import docopt
+5 -5
View File
@@ -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)
return yaml.safe_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():
+87 -42
View File
@@ -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):
@@ -89,53 +91,96 @@ class Request:
__slots__ = [
"_starlette",
"formats",
"headers",
"mimetype",
"method",
"full_url",
"url",
"params",
"_headers",
"_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():
headers[header] = value
self.headers = (
headers
) #: A case-insensitive dictionary, containg all headers sent in the Request.
self._headers = headers
self.mimetype = self.headers.get("Content-Type", "")
@property
def headers(self):
"""A case-insensitive dictionary, containing all headers sent in the Request."""
return self._headers
self.method = (
self._starlette.method.lower()
) #: The incoming HTTP method used for the request, lower-cased.
@property
def mimetype(self):
return self.headers.get("Content-Type", "")
self.full_url = str(
self._starlette.url
) #: The full URL of the Request, query parameters and all.
@property
def method(self):
"""The incoming HTTP method used for the request, lower-cased."""
return self._starlette.method.lower()
self.url = rfc3986.urlparse(self.full_url) #: The parsed URL of the Request
@property
def full_url(self):
"""The full URL of the Request, query parameters and all."""
return str(self._starlette.url)
@property
def url(self):
"""The parsed URL of the Request."""
return rfc3986.urlparse(self.full_url)
@property
def params(self):
"""A dictionary of the parsed query parameters used for the Request."""
try:
self.params = QueryDict(
self.url.query
) #: A dictionary of the parsed query paramaters used for the Request.
return QueryDict(self.url.query)
except AttributeError:
self.params = {}
return QueryDict({})
@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 +188,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 +198,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 +223,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 +233,7 @@ class Response:
self.formats = formats
@property
def body(self):
async def body(self):
if self.content:
return (self.content, {})
@@ -196,19 +242,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 +276,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)
+23 -8
View File
@@ -1,13 +1,13 @@
import re
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 +16,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}>"
@@ -28,6 +29,10 @@ class Route:
# Strings.
return self.does_match(other)
@property
def description(self):
return self.endpoint.__doc__
@property
def has_parameters(self):
return all([("{" in self.route), ("}" in self.route)])
@@ -45,7 +50,17 @@ 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
def _weight(self):
params_count = -len(set(re.findall(r"{([a-zA-Z]\w*)}", self.route)))
return params_count != 0, params_count
@property
def is_graphql(self):
return hasattr(self.endpoint, "get_graphql_type")
+1
View File
@@ -0,0 +1 @@
DEFAULT_ENCODING = "utf-8"
+145
View File
@@ -0,0 +1,145 @@
GRAPHIQL = """
{% set GRAPHIQL_VERSION = '0.12.0' %}
<!--
* Copyright (c) Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the license found in the
* LICENSE file in the root directory of this source tree.
-->
<!DOCTYPE html>
<html>
<head>
<style>
body {
height: 100%;
margin: 0;
width: 100%;
overflow: hidden;
}
#graphiql {
height: 100vh;
}
</style>
<!--
This GraphiQL example depends on Promise and fetch, which are available in
modern browsers, but can be "polyfilled" for older browsers.
GraphiQL itself depends on React DOM.
If you do not want to rely on a CDN, you can host these files locally or
include them directly in your favored resource bunder.
-->
<link href="//cdn.jsdelivr.net/npm/graphiql@{{ GRAPHIQL_VERSION }}/graphiql.css" rel="stylesheet"/>
<script src="//cdn.jsdelivr.net/npm/whatwg-fetch@2.0.3/fetch.min.js"></script>
<script src="//cdn.jsdelivr.net/npm/react@16.2.0/umd/react.production.min.js"></script>
<script src="//cdn.jsdelivr.net/npm/react-dom@16.2.0/umd/react-dom.production.min.js"></script>
<script src="//cdn.jsdelivr.net/npm/graphiql@{{ GRAPHIQL_VERSION }}/graphiql.min.js"></script>
</head>
<body>
<div id="graphiql">Loading...</div>
<script>
/**
* This GraphiQL example illustrates how to use some of GraphiQL's props
* in order to enable reading and updating the URL parameters, making
* link sharing of queries a little bit easier.
*
* This is only one example of this kind of feature, GraphiQL exposes
* various React params to enable interesting integrations.
*/
// Parse the search string to get url parameters.
var search = window.location.search;
var parameters = {};
search.substr(1).split('&').forEach(function (entry) {
var eq = entry.indexOf('=');
if (eq >= 0) {
parameters[decodeURIComponent(entry.slice(0, eq))] =
decodeURIComponent(entry.slice(eq + 1));
}
});
// if variables was provided, try to format it.
if (parameters.variables) {
try {
parameters.variables =
JSON.stringify(JSON.parse(parameters.variables), null, 2);
} catch (e) {
// Do nothing, we want to display the invalid JSON as a string, rather
// than present an error.
}
}
// When the query and variables string is edited, update the URL bar so
// that it can be easily shared
function onEditQuery(newQuery) {
parameters.query = newQuery;
updateURL();
}
function onEditVariables(newVariables) {
parameters.variables = newVariables;
updateURL();
}
function onEditOperationName(newOperationName) {
parameters.operationName = newOperationName;
updateURL();
}
function updateURL() {
var newSearch = '?' + Object.keys(parameters).filter(function (key) {
return Boolean(parameters[key]);
}).map(function (key) {
return encodeURIComponent(key) + '=' +
encodeURIComponent(parameters[key]);
}).join('&');
history.replaceState(null, null, newSearch);
}
// Defines a GraphQL fetcher using the fetch API. You're not required to
// use fetch, and could instead implement graphQLFetcher however you like,
// as long as it returns a Promise or Observable.
function graphQLFetcher(graphQLParams) {
// This example expects a GraphQL server at the path /graphql.
// Change this to point wherever you host your GraphQL server.
return fetch('{{ endpoint }}', {
method: 'post',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify(graphQLParams),
credentials: 'include',
}).then(function (response) {
return response.text();
}).then(function (responseBody) {
try {
return JSON.parse(responseBody);
} catch (error) {
return responseBody;
}
});
}
// Render <GraphiQL /> into the body.
// See the README in the top level of this module to learn more about
// how you can customize GraphiQL by providing different values or
// additional child elements.
ReactDOM.render(
React.createElement(GraphiQL, {
fetcher: graphQLFetcher,
query: parameters.query,
variables: parameters.variables,
operationName: parameters.operationName,
onEditQuery: onEditQuery,
onEditVariables: onEditVariables,
onEditOperationName: onEditOperationName
}),
document.getElementById('graphiql')
);
</script>
</body>
</html>
""".strip()
+5 -1
View File
@@ -34,6 +34,10 @@ required = [
"uvloop ; sys_platform != 'win32'",
"rfc3986",
"python-multipart",
"chardet",
"apispec>=1.0.0b1",
"marshmallow",
"asgiref",
]
@@ -114,7 +118,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
View File
@@ -0,0 +1 @@
lorem
+1 -94
View File
@@ -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') }}
-226
View File
@@ -1,226 +0,0 @@
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)
def test_api_basic_route(api):
@api.route("/")
def home(req, resp):
resp.text = "hello world!"
def test_api_basic_route_overlap(api):
@api.route("/")
def home(req, resp):
resp.text = "hello world!"
with pytest.raises(AssertionError):
@api.route("/")
def home2(req, resp):
resp.text = "hello world!"
def test_api_basic_route_overlap_alternative(api):
@api.route("/")
def home(req, resp):
resp.text = "hello world!"
def home2(req, resp):
resp.text = "hello world!"
with pytest.raises(AssertionError):
api.add_route("/", home2)
def test_api_basic_route_overlap_allowed(api):
@api.route("/")
def home(req, resp):
resp.text = "hello world!"
def home2(req, resp):
resp.text = "hello world!"
api.add_route("/", home2, check_existing=False)
def test_api_basic_route_overlap_allowed_alternative(api):
@api.route("/")
def home(req, resp):
resp.text = "hello world!"
@api.route("/", check_existing=False)
def home2(req, resp):
resp.text = "hello world!"
def test_class_based_view_registration(api):
@api.route("/")
class ThingsResource:
def on_request(req, resp):
resp.text = "42"
def test_requests_session(api):
assert api.session()
def test_requests_session_works(api, session, url):
TEXT = "spiral out"
@api.route("/")
def hello(req, resp):
resp.text = TEXT
assert session.get(url("/")).text == TEXT
def test_status_code(api):
@api.route("/")
def hello(req, resp):
resp.text = "keep going"
resp.status_code = responder.status_codes.HTTP_416
assert api.session().get("http://;/").status_code == responder.status_codes.HTTP_416
def test_json_media(api):
dump = {"life": 42}
@api.route("/")
def media(req, resp):
resp.media = dump
r = api.session().get("http://;/")
assert "json" in r.headers["Content-Type"]
assert r.json() == dump
def test_yaml_media(api):
dump = {"life": 42}
@api.route("/")
def media(req, resp):
resp.media = dump
r = api.session().get("http://;/", headers={"Accept": "yaml"})
assert "yaml" in r.headers["Content-Type"]
assert yaml.load(r.content) == dump
def test_graphql_schema_query_querying(api, schema):
api.add_route("/", schema)
r = api.session().get("http://;/?q={ hello }", headers={"Accept": "json"})
assert r.json() == {"data": {"hello": "Hello stranger"}}
def test_argumented_routing(api):
@api.route("/{name}")
def hello(req, resp, *, name):
resp.text = f"Hello, {name}."
r = api.session().get("http://;/sean")
assert r.text == "Hello, sean."
def test_mote_argumented_routing(api):
@api.route("/{greeting}/{name}")
def hello(req, resp, *, greeting, name):
resp.text = f"{greeting}, {name}."
r = api.session().get("http://;/hello/lyndsy")
assert r.text == "hello, lyndsy."
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):
resp.headers.update({"LIFE": "42"})
r = api.session().get("http://;/")
assert "DEATH" in r.headers
assert "LIFE" in r.headers
def test_query_params(api):
@api.route("/")
def route(req, resp):
resp.media = {"params": req.params}
r = api.session().get("http://;/?q=q")
assert r.json()["params"] == {"q": "q"}
r = api.session().get("http://;/?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):
@api.route("/")
async def route(req, resp):
resp.media = {"form": await req.media("form")}
dump = {"q": "q"}
r = api.session().get("http://;/", data=dump)
assert r.json()["form"] == dump
def test_async_function(api, session, url):
content = "The Emerald Tablet of Hermes"
@api.route("/")
async def route(req, resp):
resp.text = content
r = session.get(url("/"))
assert r.text == content
View File
+56
View File
@@ -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)
+36
View File
@@ -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)
+60
View File
@@ -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"}
+361
View File
@@ -0,0 +1,361 @@
import pytest
import yaml
import responder
def test_api_basic_route(api):
@api.route("/")
def home(req, resp):
resp.text = "hello world!"
def test_api_basic_route_overlap(api):
@api.route("/")
def home(req, resp):
resp.text = "hello world!"
with pytest.raises(AssertionError):
@api.route("/")
def home2(req, resp):
resp.text = "hello world!"
def test_api_basic_route_overlap_alternative(api):
@api.route("/")
def home(req, resp):
resp.text = "hello world!"
def home2(req, resp):
resp.text = "hello world!"
with pytest.raises(AssertionError):
api.add_route("/", home2)
def test_api_basic_route_overlap_allowed(api):
@api.route("/")
def home(req, resp):
resp.text = "hello world!"
def home2(req, resp):
resp.text = "hello world!"
api.add_route("/", home2, check_existing=False)
def test_api_basic_route_overlap_allowed_alternative(api):
@api.route("/")
def home(req, resp):
resp.text = "hello world!"
@api.route("/", check_existing=False)
def home2(req, resp):
resp.text = "hello world!"
def test_class_based_view_registration(api):
@api.route("/")
class ThingsResource:
def on_request(req, resp):
resp.text = "42"
def test_requests_session(api):
assert api.session()
def test_requests_session_works(api, session, url):
TEXT = "spiral out"
@api.route("/")
def hello(req, resp):
resp.text = TEXT
assert session.get(url("/")).text == TEXT
def test_status_code(api):
@api.route("/")
def hello(req, resp):
resp.text = "keep going"
resp.status_code = responder.status_codes.HTTP_416
assert api.session().get("http://;/").status_code == responder.status_codes.HTTP_416
def test_json_media(api):
dump = {"life": 42}
@api.route("/")
def media(req, resp):
resp.media = dump
r = api.session().get("http://;/")
assert "json" in r.headers["Content-Type"]
assert r.json() == dump
def test_yaml_media(api):
dump = {"life": 42}
@api.route("/")
def media(req, resp):
resp.media = dump
r = api.session().get("http://;/", headers={"Accept": "yaml"})
assert "yaml" in r.headers["Content-Type"]
assert yaml.load(r.content) == dump
def test_graphql_schema_query_querying(api, schema):
api.add_route("/", schema)
r = api.session().get("http://;/?q={ hello }", headers={"Accept": "json"})
assert r.json() == {"data": {"hello": "Hello stranger"}}
def test_argumented_routing(api, session):
@api.route("/{name}")
def hello(req, resp, *, name):
resp.text = f"Hello, {name}."
r = session.get(api.url_for(hello, name="sean"))
assert r.text == "Hello, sean."
def test_mote_argumented_routing(api, session):
@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"))
assert r.text == "hello, lyndsy."
def test_request_and_get(api, session):
@api.route("/")
class ThingsResource:
def on_request(self, req, resp):
resp.headers.update({"DEATH": "666"})
def on_get(self, request, resp):
resp.headers.update({"LIFE": "42"})
r = session.get(api.url_for(ThingsResource))
assert "DEATH" in r.headers
assert "LIFE" in r.headers
def test_class_based_view_status_code(api):
@api.route("/")
class ThingsResource:
def on_request(self, req, resp):
resp.status_code = responder.status_codes.HTTP_416
assert api.session().get("http://;/").status_code == responder.status_codes.HTTP_416
def test_query_params(api, url, session):
@api.route("/")
def route(req, resp):
resp.media = {"params": req.params}
r = session.get(api.url_for(route), params={"q": "q"})
assert r.json()["params"] == {"q": "q"}
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, session):
@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)
assert r.json()["form"] == dump
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(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
def test_graphiql(api, schema):
api.add_route("/", schema)
r = api.session().get("http://;/", headers={"Accept": "text/html"})
assert r.ok
assert 'GraphiQL' in r.text
def test_json_uploads(api, session):
@api.route("/")
async def route(req, resp):
resp.media = await req.media()
dump = {"complicated": "times"}
r = session.post(api.url_for(route), json=dump)
assert r.json() == dump
def test_yaml_uploads(api, session):
@api.route("/")
async def route(req, resp):
resp.media = await req.media()
dump = {"complicated": "times"}
r = session.post(
api.url_for(route),
data=yaml.dump(dump),
headers={"Content-Type": "application/x-yaml"},
)
assert r.json() == dump
def test_form_uploads(api, session):
@api.route("/")
async def route(req, resp):
resp.media = await req.media()
dump = {"complicated": "times"}
r = session.post(api.url_for(route), data=dump)
assert r.json() == dump
def test_json_downloads(api, session):
dump = {"testing": "123"}
@api.route("/")
def route(req, resp):
resp.media = dump
r = session.get(api.url_for(route), headers={"Content-Type": "application/json"})
assert r.json() == dump
def test_yaml_downloads(api, session):
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"})
assert yaml.safe_load(r.content) == dump
def test_schema_generation():
import responder
from marshmallow import Schema, fields
api = responder.API(title="Web Service", openapi="3.0")
@api.schema("Pet")
class PetSchema(Schema):
name = fields.Str()
@api.route("/")
def route(req, resp):
"""A cute furry animal endpoint.
---
get:
description: Get a random pet
responses:
200:
description: A pet to be returned
schema:
$ref = "#/components/schemas/Pet"
"""
resp.media = PetSchema().dump({"name": "little orange"})
r = api.session().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):
@api.route("/")
def hello(req, resp):
resp.text = "hello"
api.mount("/flask", flask)
r = session.get("http://;/flask")
assert r.ok
def test_async_class_based_views(api, session):
@api.route("/")
class Resource:
async def on_post(self, req, resp):
resp.text = await req.text
data = "frame"
r = session.post(api.url_for(Resource), data=data)
assert r.text == data
+83
View File
@@ -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