Compare commits

...

255 Commits

Author SHA1 Message Date
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
kennethreitz 94e0400ea1 v0.0.2 2018-10-13 08:42:20 -04:00
kennethreitz 47c5b84093 form parsing working 2018-10-13 08:41:28 -04:00
kennethreitz 8b1fbfd16d better 2018-10-13 08:24:08 -04:00
kennethreitz cceb698899 installing 2018-10-13 08:23:28 -04:00
kennethreitz 01741df10d the cake is a lie 2018-10-13 08:20:38 -04:00
kennethreitz f91ebf8baa Merge branch 'master' of github.com:kennethreitz/responder 2018-10-13 08:19:21 -04:00
kennethreitz 4dde076030 move installation up 2018-10-13 08:19:12 -04:00
kennethreitz 3491001b7f Update README.md 2018-10-13 08:15:00 -04:00
kennethreitz 2acec68649 __slots__ 2018-10-13 08:13:32 -04:00
kennethreitz 51dab27374 index.rst 2018-10-13 08:08:00 -04:00
kennethreitz 145f5041bf options 2018-10-13 08:02:17 -04:00
kennethreitz 6034505380 Merge branch 'master' of github.com:kennethreitz/responder 2018-10-13 08:01:18 -04:00
kennethreitz 8533d74906 5042 2018-10-13 08:01:10 -04:00
kennethreitz b2ae57b982 port 2018-10-13 07:59:16 -04:00
kennethreitz 49ffe9bec9 Merge pull request #20 from aitoehigie/master
Fleshed out the benchmarks section of the README.md file
2018-10-13 07:57:16 -04:00
kennethreitz fe5d92674e google analytics 2018-10-13 07:53:13 -04:00
kennethreitz 197d28f5c7 index 2018-10-13 07:50:29 -04:00
kennethreitz cd48bb0789 update readme 2018-10-13 07:50:09 -04:00
kennethreitz 90fc411e9a : 2018-10-13 07:46:36 -04:00
kennethreitz c22b6a84aa an 2018-10-13 07:45:42 -04:00
kennethreitz 9b65642f05 async 2018-10-13 07:43:57 -04:00
kennethreitz 83547dce9c continuation? 2018-10-13 07:43:09 -04:00
kennethreitz efeecceb54 fix? 2018-10-13 07:41:35 -04:00
kennethreitz ba9b5a40d2 simplify 2018-10-13 07:37:16 -04:00
kennethreitz 47b5bda277 fix 2018-10-13 07:36:43 -04:00
kennethreitz a343b6b1b6 fix 2018-10-13 07:36:30 -04:00
kennethreitz 0fe48d3003 index 2018-10-13 07:33:19 -04:00
kennethreitz 23e3760b08 Merge branch 'master' of github.com:kennethreitz/responder 2018-10-13 07:31:50 -04:00
kennethreitz 3d31905562 improve formats for json 2018-10-13 07:31:45 -04:00
kennethreitz 9638c5266b index.rst 2018-10-13 07:30:56 -04:00
kennethreitz ad7ce9f55a Merge pull request #23 from aaqaishtyaq/docs-badge
add documentation badge in Readme
2018-10-13 07:26:55 -04:00
kennethreitz b0baf3b85a readme 2018-10-13 07:26:18 -04:00
kennethreitz d4d3687882 readme 2018-10-13 07:24:19 -04:00
kennethreitz faf55ca191 readme 2018-10-13 07:21:52 -04:00
kennethreitz d5096a23fb async def 2018-10-13 07:06:55 -04:00
kennethreitz ed5841d201 test async function 2018-10-13 07:06:14 -04:00
kennethreitz bbfc095a00 gitignore 2018-10-13 07:00:53 -04:00
kennethreitz 0fcb68a13d bunk 2018-10-13 07:00:42 -04:00
kennethreitz f97744c098 async defenitions work 2018-10-13 06:59:23 -04:00
kennethreitz d1cfa8d27a safe_load 2018-10-13 05:55:46 -04:00
Aaqa Ishtyaq 218dcf25c1 add documentation badge in Readme 2018-10-13 12:31:18 +05:30
aitoehigie 06e06973a4 format the readme file 2018-10-13 03:24:57 +01:00
aitoehigie 6f73cfc5f2 format the readme file 2018-10-13 03:23:47 +01:00
aitoehigie 6db5bbeaee format the readme file 2018-10-13 03:22:55 +01:00
aitoehigie 6ef5077164 format the readme file 2018-10-13 03:21:53 +01:00
aitoehigie 45e1ed7022 format the readme file 2018-10-13 03:20:36 +01:00
aitoehigie c14b4535a6 format the readme file 2018-10-13 03:19:21 +01:00
aitoehigie 411631d2f8 format the readme file 2018-10-13 03:17:42 +01:00
aitoehigie f4c3690bd8 format the readme file 2018-10-13 03:15:49 +01:00
aitoehigie 56fdea6b5d update the benchmarks section 2018-10-13 03:05:12 +01:00
aitoehigie 8a5c053d39 Add a concise benchmarks section to the README.md file 2018-10-13 02:34:58 +01:00
kennethreitz 42870cfa23 memoize template rendering 2018-10-12 16:56:21 -04:00
kennethreitz 6cf256cc05 memoize routes 2018-10-12 16:48:41 -04:00
kennethreitz 9fec915f62 Merge branch 'master' of github.com:kennethreitz/responder 2018-10-12 16:42:21 -04:00
kennethreitz f1d5ab73cd readme 2018-10-12 16:42:13 -04:00
kennethreitz cd62972945 Merge pull request #14 from taoufik07/feature/query_dict
Add a QueryDict to manage query params
2018-10-12 15:26:05 -04:00
kennethreitz 998d09170c Update README.md 2018-10-12 14:36:03 -04:00
kennethreitz 4ba57181ec Update README.md 2018-10-12 14:31:27 -04:00
kennethreitz 8b9d8bdc62 Update README.md 2018-10-12 14:30:44 -04:00
taoufik07 4291d42dc0 Merge branch 'master' into feature/query_dict 2018-10-12 18:55:17 +01:00
kennethreitz 79fcc1ce40 uvloop 2018-10-12 13:28:07 -04:00
kennethreitz bfc6778dca readme 2018-10-12 13:27:02 -04:00
kennethreitz 701e57c264 fix all the things 2018-10-12 13:25:38 -04:00
kennethreitz 163d025c0d fix tests 2018-10-12 13:15:53 -04:00
kennethreitz d9befc6d8c test 2018-10-12 13:13:13 -04:00
kennethreitz 9e50a4c241 background 2018-10-12 12:28:14 -04:00
kennethreitz 9b0cae3794 background 2018-10-12 12:26:04 -04:00
taoufik07 6160dfb2f7 Add a simple test 2018-10-12 17:01:23 +01:00
taoufik07 cd013cdb06 Add QueryDict 2018-10-12 17:01:06 +01:00
kennethreitz 26cc7c90e9 Merge branch 'asgi' of https://github.com/tomchristie/responder into asgi 2018-10-12 10:29:13 -04:00
kennethreitz f28ac3cf22 Merge pull request #12 from taoufik07/patch-1
Typo : nicities -> niceties
2018-10-12 10:27:37 -04:00
kennethreitz 58fec4b082 Merge pull request #13 from taoufik07/patch-2
Fix resolve_hello indentation
2018-10-12 10:27:26 -04:00
Taoufik b91805a5df Fix graphql test 2018-10-12 15:27:04 +01:00
Tom Christie 0fa0df1bdf Add Pipfile 2018-10-12 15:18:13 +01:00
Tom Christie 3f7cacee3e Updated Pipfile.lock with latest pipenv 2018-10-12 15:12:44 +01:00
Taoufik 72637fd650 FIx resolve_hello indentation and use f-string 2018-10-12 15:12:17 +01:00
Taoufik aba1284f8e Typo : nicities -> niceties 2018-10-12 15:03:49 +01:00
Tom Christie 179e1dc9e5 Update Pipfile.lock 2018-10-12 15:02:08 +01:00
kennethreitz 75879a494e Merge pull request #7 from CianciuStyles/fix-familar-typo
Change "familar" into "familiar"
2018-10-12 09:54:46 -04:00
kennethreitz 73b1ea4713 Merge pull request #10 from OdinTech3/patch-1
Fixed a mispelt word
2018-10-12 09:54:30 -04:00
kennethreitz 55dc991c13 Merge pull request #11 from MichaelPereira/patch-1
Small typo : cooke -> cookie
2018-10-12 09:54:20 -04:00
Tom Christie c30316588a Form parsing requires Starlette 102 2018-10-12 14:53:55 +01:00
Tom Christie db5d6e7481 Lowercase method for on_{method} 2018-10-12 14:47:20 +01:00
Tom Christie f8d52f58d4 Drop form parsing support until Starlette PR #102 is in 2018-10-12 14:36:44 +01:00
Tom Christie 227ee499e4 request.params for API compatability with existing codebase/tests 2018-10-12 14:35:31 +01:00
Michael Pereira dcdaf6a674 Small typo : cooke -> cookie 2018-10-12 09:32:38 -04:00
Tom Christie d524ba3a37 Merge remote-tracking branch 'upstream/master' into asgi 2018-10-12 14:28:33 +01:00
Nick Spirit da5e288476 Fixed a mispelt word 2018-10-12 09:27:52 -04:00
kennethreitz baad7cd60d fix rst 2018-10-12 09:15:03 -04:00
kennethreitz e9d6fc33fd rst 2018-10-12 09:13:53 -04:00
kennethreitz c2fa0899e9 responder 2018-10-12 09:12:34 -04:00
kennethreitz 2dc09ec1f2 build status 2018-10-12 09:09:42 -04:00
kennethreitz fba640976f gzip 2018-10-12 09:04:33 -04:00
kennethreitz 8e7df61a73 fixes 2018-10-12 09:00:52 -04:00
Tom Christie 41776cf2df Initial pass at ASGI support 2018-10-12 13:57:22 +01:00
kennethreitz 23983f0b75 hacktoberfest 2018-10-12 08:55:27 -04:00
kennethreitz 84b457ede5 fix 2018-10-12 08:31:46 -04:00
kennethreitz a906e0bf0c Merge branch 'master' of github.com:kennethreitz/responder 2018-10-12 08:26:28 -04:00
kennethreitz 3db1aad96a hacks 2018-10-12 08:26:20 -04:00
kennethreitz 9c909e7a2c Update README.md 2018-10-12 08:19:38 -04:00
kennethreitz ad2ef7cb33 readme improvements 2018-10-12 08:17:56 -04:00
kennethreitz c851510ca9 fix travis 2018-10-12 08:13:35 -04:00
kennethreitz 71a21c2059 license 2018-10-12 08:12:54 -04:00
kennethreitz d90537eb8d README 2018-10-12 08:12:02 -04:00
Stefano Cianciulli 25e9888438 Change "familar" into "familiar" 2018-10-12 11:00:59 +01:00
32 changed files with 1623 additions and 759 deletions
+12
View File
@@ -1,4 +1,16 @@
.vscode/
.cache
.idea
.coverage
.pytest_cache
.DS_Store
coverage.xml
__pycache__
tests/__pycache__
build
responder.egg-info/
dist/
app.py
app2.py
-1
View File
@@ -1,7 +1,6 @@
language: python
python:
- "3.6"
- "3.7"
# command to install dependencies
install:
+23
View File
@@ -0,0 +1,23 @@
# 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
+13
View File
@@ -0,0 +1,13 @@
Copyright 2018 Kenneth Reitz
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+1 -1
View File
@@ -5,7 +5,6 @@ name = "pypi"
[packages]
responder = {editable = true, path = "."}
uvicorn = "*"
[dev-packages]
pytest = "*"
@@ -14,6 +13,7 @@ black = "*"
twine = "*"
flask = "*"
sphinx = "*"
marshmallow = "*"
[requires]
python_version = "3.7"
Generated
+71 -38
View File
@@ -1,7 +1,7 @@
{
"_meta": {
"hash": {
"sha256": "76d2978ee90d2c028b13c9a5abdd2371d74d514045d50fb9b92aec44e72054b3"
"sha256": "9b959d9507c521f6088646507633207db03afec6ac31aeab07adf0d737dbb45b"
},
"pipfile-spec": 6,
"requires": {
@@ -16,6 +16,13 @@
]
},
"default": {
"aiofiles": {
"hashes": [
"sha256:021ea0ba314a86027c166ecc4b4c07f2d40fc0f4b3a950d1868a0f2571c2bbee",
"sha256:1e644c2573f953664368de28d2aa4c89dfd64550429d0c27c4680ccd3aa4985d"
],
"version": "==0.4.0"
},
"aniso8601": {
"hashes": [
"sha256:7849749cf00ae0680ad2bdfe4419c7a662bef19c03691a19e008c8b9a5267802",
@@ -23,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": [
@@ -97,6 +125,13 @@
],
"version": "==1.0"
},
"marshmallow": {
"hashes": [
"sha256:82b201ad767eb54de371c08cb1db6ca4ad2a728fa41b831e3781bf944815eb38",
"sha256:c250f37ac0e249a8287394a60d91f6240b674642ad999e66cd09463dbccd1d4f"
],
"version": "==3.0.0b18"
},
"parse": {
"hashes": [
"sha256:9dd6048ea212cd032a342f9f6aa2b7bc222f7407c7e37bdc2777fecd36897437"
@@ -110,6 +145,12 @@
],
"version": "==2.2.1"
},
"python-multipart": {
"hashes": [
"sha256:f7bb5f611fc600d15fa47b3974c8aa16e93724513b49b5f95c81e6624c83fa43"
],
"version": "==0.0.5"
},
"pyyaml": {
"hashes": [
"sha256:254bf6fda2b7c651837acb2c718e213df29d531eebf00edb54743d10bcb694eb",
@@ -127,16 +168,17 @@
],
"version": "==2.19.1"
},
"requests-wsgi-adapter": {
"hashes": [
"sha256:7080c98ae2614b8d0b7339b611d97a535470d2fb479731f7d588d5f8108ea134"
],
"version": "==0.4.0"
},
"responder": {
"editable": true,
"path": "."
},
"rfc3986": {
"hashes": [
"sha256:632b8fcd2ac37f24334316227f909be4f9d0738cbf409404cff6fa5f69a24093",
"sha256:8458571c4c57e1cf23593ad860bb601b6a604df6217f829c2bc70dc4b5af941b"
],
"version": "==1.1.0"
},
"rx": {
"hashes": [
"sha256:13a1d8d9e252625c173dc795471e614eadfe1cf40ffc684e08b8fff0d9748c23",
@@ -151,6 +193,12 @@
],
"version": "==1.11.0"
},
"starlette": {
"hashes": [
"sha256:2c7ec085440fce7146a9be2b6d53b7110c3866ce6fa03d901efdc1fbe97e0f36"
],
"version": "==0.4.2"
},
"urllib3": {
"hashes": [
"sha256:a68ac5e15e76e7e5dd2b8f94007233e01effe3e50e8daddf69acfd81cb686baf",
@@ -162,16 +210,8 @@
"hashes": [
"sha256:8de03999a936d8704f07cc3b1d3a3edb6922a068b64d84b4f5e49604c8b70a11"
],
"index": "pypi",
"version": "==0.3.12"
},
"waitress": {
"hashes": [
"sha256:40b0f297a7f3af61fbfbdc67e59090c70dc150a1601c39ecc9f5f1d283fb931b",
"sha256:d33cd3d62426c0f1b3cd84ee3d65779c7003aae3fc060dee60524d10a57f05a9"
],
"version": "==1.1.0"
},
"websockets": {
"hashes": [
"sha256:0e2f7d6567838369af074f0ef4d0b802d19fa1fee135d864acc656ceefa33136",
@@ -197,20 +237,6 @@
"sha256:f1414e6cbcea8d22843e7eafdfdfae3dd1aba41d1945f6ca66e4806c07c4f454"
],
"version": "==6.0"
},
"werkzeug": {
"hashes": [
"sha256:c3fd7a7d41976d9f44db327260e263132466836cef6f91512889ed60ad26557c",
"sha256:d5da73735293558eb1651ee2fddc4d0dedcfa06538b8813a2e20011583c9e49b"
],
"version": "==0.14.1"
},
"whitenoise": {
"hashes": [
"sha256:133a92ff0ab8fb9509f77d4f7d0de493eca19c6fea973f4195d4184f888f2e02",
"sha256:32b57d193478908a48acb66bf73e7a3c18679263e3e64bfebcfac1144a430039"
],
"version": "==4.1"
}
},
"develop": {
@@ -266,10 +292,10 @@
},
"certifi": {
"hashes": [
"sha256:376690d6f16d32f9d1fe8932551d80b23e9d393a8578c5633a2ed39a64861638",
"sha256:456048c7e371c089d0a77a5212fb37a2c2dce1e24146e3b7e0261736aaeaa22a"
"sha256:339dc09518b07e2fa7eda5450740925974815557727d6bd35d319c1524a04a4c",
"sha256:6d58c986d22b038c8c0df30d639f23a3e6d172a05c3583e766f4c0b785c0986a"
],
"version": "==2018.8.24"
"version": "==2018.10.15"
},
"cffi": {
"hashes": [
@@ -426,6 +452,13 @@
],
"version": "==1.0"
},
"marshmallow": {
"hashes": [
"sha256:82b201ad767eb54de371c08cb1db6ca4ad2a728fa41b831e3781bf944815eb38",
"sha256:c250f37ac0e249a8287394a60d91f6240b674642ad999e66cd09463dbccd1d4f"
],
"version": "==3.0.0b18"
},
"mccabe": {
"hashes": [
"sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42",
@@ -577,10 +610,10 @@
},
"tqdm": {
"hashes": [
"sha256:18f1818ce951aeb9ea162ae1098b43f583f7d057b34d706f66939353d1208889",
"sha256:df02c0650160986bac0218bb07952245fc6960d23654648b5d5526ad5a4128c9"
"sha256:a0be569511161220ff709a5b60d0890d47921f746f1c737a11d965e1b29e7b2e",
"sha256:e293e6d7a7f41a529a27f8d6624ab11544ccbfe82a205af6fad102545099fc21"
],
"version": "==4.26.0"
"version": "==4.27.0"
},
"twine": {
"hashes": [
+70 -25
View File
@@ -1,10 +1,15 @@
# Responder: a familiar HTTP Service Framework for Python
![](https://github.com/kennethreitz/responder/raw/master/ext/Artboard%201%402x.png)
[![Build Status](https://travis-ci.org/kennethreitz/responder.svg?branch=master)](https://travis-ci.org/kennethreitz/responder)
[![Documentation Status](https://readthedocs.org/projects/mybinder/badge/?version=latest)](https://responder.readthedocs.io/en/latest/)
[![image](https://img.shields.io/pypi/v/responder.svg)](https://pypi.org/project/responder/)
[![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)
The Python world certainly doesn't need more web frameworks. But, it does need more creativity, so I thought I'd bring some of my ideas to the table and see what I could come up with.
[![](https://github.com/kennethreitz/responder/raw/master/ext/small.jpg)](http://python-responder.org/)
## But will it blend?
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.
```python
import responder
@@ -12,14 +17,29 @@ import responder
api = responder.API()
@api.route("/{greeting}")
def greet_world(req, resp, *, greeting):
async def greet_world(req, resp, *, greeting):
resp.text = f"{greeting}, world!"
if __name__ == '__main__':
api.run()
```
This gets you a WSGI app, with WhiteNoise pre-installed, jinja2 templating (without additional imports), and a production webserver (ready for slowloris attacks), serving up requests with gzip compression automatically.
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.
## Testimonials
> "Pleasantly very taken with python-responder. [@kennethreitz](https://twitter.com/kennethreitz) at his absolute best." —Rudraksh M.K.
> "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.
## More Examples
Class-based views (and setting some headers and stuff):
@@ -42,7 +62,22 @@ def greet_world(req, resp, *, greeting):
The `api` instance is available as an object during template rendering.
Serve a GraphQL API:
Here, you can spawn off a background thread to run any function, out-of-request:
```python
@api.route("/")
def hello(req, resp):
@api.background.task
def sleep(s=10):
time.sleep(s)
print("slept!")
sleep()
resp.content = "processing"
```
And even serve a GraphQL API:
```python
import graphene
@@ -80,46 +115,56 @@ Want HSTS?
api = responder.API(enable_hsts=True)
```
Boom. ✨🍰✨
Boom.
# Installing Responder
Install the latest release:
$ pipenv install responder
✨🍰✨
Or, install from the development branch:
$ pipenv install -e git+https://github.com/kennethreitz/responder.git#egg=responder
Only **Python 3.6+** is supported.
# 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).
- Case-insensitive `req.headers` dict (from Requests directly).
- `resp.status_code`, `req.method`, `req.url`, and other familar friends.
- `resp.status_code`, `req.method`, `req.url`, and other familiar friends.
## New Ideas
## Ideas
- Flask-style route expression, with new capabilities -- all while using Python 3.6+'s new f-string syntax.
- I love Falcon's "every request and response is passed into to each view and mutated" methodology, especially `response.media`, and have used it here. In addition to supporting JSON, I have decided to support YAML as well, as Kubernetes is slowly taking over the world, and it uses YAML for all the things. Content-negotiation and all that.
- **A built in testing client that uses the actual Requests you know and love**.
- The ability to mount other WSGI apps easily.
- Automatic gzipped-responses (still working on that).
- Automatic gzipped-responses.
- In addition to Falcon's `on_get`, `on_post`, etc methods, Responder features an `on_request` method, which gets called on every type of request, much like Requests.
- WhiteNoise is built-in, for serving static files.
- Waitress built-in as a production web server. I would have chosen Gunicorn, but it doesn't run on Windows. Plus, Waitress serves well to protect against slowloris attacks, making nginx unnecessary in production.
- A production static file server is built-in.
- Uvicorn built-in as a production web server. I would have chosen Gunicorn, but it doesn't run on Windows. Plus, Uvicorn serves well to protect against slowloris attacks, making nginx unnecessary in production.
- GraphQL support, via Graphene. The goal here is to have any GraphQL query exposable at any route, magically.
## Old Ideas
- Flask-style route expression, with new capabilities -- primarily, the ability to cast a parameter to integers as well as other types that are missing from Flask, all while using Python 3.6+'s new f-string syntax.
- I love Falcon's "every request and response is passed into to each view and mutated" methodology, especially `response.media`, and have used it here. In addition to supporting JSON, I have decided to support YAML as well, as Kubernetes is slowly taking over the world, and it uses YAML for all the things. Content-negotiation and all that.
## Future Ideas
- I want to be able to "mount" any WSGI app into a sub-route.
- Cooke-based sessions are currently an afterthought, as this is an API framework, but websites are APIs too.
- Potentially support ASGI instead of WSGI. Will the tradeoffs be worth it? This is a question to ask. Procedural code works well for 90% use cases.
- Cookie-based sessions are currently an afterthought, as this is an API framework, but websites are APIs too.
- If frontend websites are supported, provide an official way to run webpack.
# The Goal
The primary goal here is to learn, not to get adoption. Though, who knows how these things will pan out.
# When can I use it?
When it's ready. It's not. I started work on this a few days ago. It works surprisingly well, considering! :)
----------
[![hacktoberfest](https://hacktoberfest.digitalocean.com/assets/hacktoberfest-2018-social-card-c8d2e1489f647f2e0a26e6f598adeb760872818905b34cd437afc7ac2857ceab.png)](https://hacktoberfest.digitalocean.com/)
-66
View File
@@ -1,66 +0,0 @@
import responder
import graphene
from flask import Flask
app = Flask(__name__)
@app.route("/")
def hello_world():
return "Hello, World from flask!"
api = responder.API(enable_hsts=False)
api.mount("/hello", app)
@api.route("/")
def hello(req, resp):
# resp.status = responder.status.ok
resp.content = api.template("test.html")
class ThingsResource:
def on_request(self, req, resp):
resp.status = responder.status.HTTP_200
resp.media = ["yolo"]
class Query(graphene.ObjectType):
hello = graphene.String(name=graphene.String(default_value="stranger"))
def resolve_hello(self, info, name):
return "Hello " + name
schema = graphene.Schema(query=Query)
# Alerntatively,
api.add_route("/graph", schema, graphiql=True)
print(
api.session()
.get(
"http://app/",
# data="{ hello }",
# headers={"Accept": "application/x-yaml"},
# data="hello",
)
.text
)
# print(
# api.session()
# .get(
# "http://app/hello/",
# data="{ hello }",
# headers={"Accept": "application/x-yaml"},
# # data="hello",
# )
# .text
# )
# {hello: Hello stranger}
api.run(port=5000)
+18 -14
View File
@@ -22,6 +22,7 @@
}
pre,
.pre,
.class em,
.descname,
.method em {
@@ -49,9 +50,13 @@
.method em,
.class em {
font-style: italic !important;
color: grey;
}
.method em,
.class em {
margin-left: 0.3em;
margin-right: 0.3em;
color: grey;
}
.method p,
@@ -71,6 +76,11 @@
display: none;
}
#testimonials p.attribution {
margin-top: -1em;
}
/* "Quick Search" should be not be shown for now. */
div#searchbox h3 {
@@ -105,19 +115,13 @@
</style>
<!-- Analytics tracking for Kenneth. -->
<script type="text/javascript">
var _gauges = _gauges || [];
(function () {
var t = document.createElement('script');
t.type = 'text/javascript';
t.async = true;
t.id = 'gauges-tracker';
t.setAttribute('data-site-id', '588f8e99c88d9013e60fa373');
t.setAttribute('data-track-path', 'https://track.gaug.es/track.gif');
t.src = 'https://d36ee2fcip1434.cloudfront.net/track.js';
var s = document.getElementsByTagName('script')[0];
s.parentNode.insertBefore(t, s);
})();
<script async src="https://www.googletagmanager.com/gtag/js?id=UA-127383416-1"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag() { dataLayer.push(arguments); }
gtag('js', new Date());
gtag('config', 'UA-127383416-1');
</script>
<!-- There are no more hacks. -->
+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
+113 -136
View File
@@ -3,130 +3,110 @@
You can adapt this file completely to your liking, but it should at least
contain the root `toctree` directive.
A familar HTTP Service Framework
================================
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 bring some of my ideas to the table and see what I could come up with.
|Build Status| |image1| |image2| |image3| |image4| |image5|
But will it blend?
------------------
.. |Build Status| image:: https://travis-ci.org/kennethreitz/responder.svg?branch=master
:target: https://travis-ci.org/kennethreitz/responder
.. |image1| image:: https://img.shields.io/pypi/v/responder.svg
:target: https://pypi.org/project/responder/
.. |image2| image:: https://img.shields.io/pypi/l/responder.svg
:target: https://pypi.org/project/responder/
.. |image3| image:: https://img.shields.io/pypi/pyversions/responder.svg
:target: https://pypi.org/project/responder/
.. |image4| image:: https://img.shields.io/github/contributors/kennethreitz/responder.svg
:target: https://github.com/kennethreitz/responder/graphs/contributors
.. |image5| image:: https://img.shields.io/badge/Say%20Thanks-!-1EAEDB.svg
:target: https://saythanks.io/to/kennethreitz
::
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.
import responder
.. code:: python
api = responder.API()
import responder
@api.route("/{greeting}")
def greet_world(req, resp, *, greeting):
resp.text = f"{greeting}, world!"
api = responder.API()
if __name__ == '__main__':
api.run()
@api.route("/{greeting}")
async def greet_world(req, resp, *, greeting):
resp.text = f"{greeting}, world!"
if __name__ == '__main__':
api.run()
This gets you a WSGI app, with WhiteNoise pre-installed, jinja2 templating (without additional imports), and a production webserver (ready for slowloris attacks), serving up requests with gzip compression automatically.
That ``async`` declaration is optional.
-------------
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.
Class-based views (and setting some headers and stuff)::
Features
--------
@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
- 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 declration.
- Mutable response object, passed into each view. No need to return anything.
- Background tasks, spawned off in a ``ThreadPoolExecutor``.
- GraphQL support!
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.
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. ✨🍰✨
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.
- Setting `resp.text` sends back unicode, while setting `resp.content` sends back bytes.
- 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 familar friends.
New Ideas
---------
- **A built in testing client that uses the actual Requests you know and love**.
- The ability to mount other WSGI apps easily.
- Automatic gzipped-responses (still working on that).
- In addition to Falcon's ``on_get``, ``on_post``, etc methods, Responder features an `on_request` method, which gets called on every type of request, much like Requests.
- WhiteNoise is built-in, for serving static files.
- Waitress built-in as a production web server. I would have chosen Gunicorn, but it doesn't run on Windows. Plus, Waitress serves well to protect against slowloris attacks, making nginx unneccessary in production.
- GraphQL support, via Graphene. The goal here is to have any GraphQL query exposable at any route, magically.
Old Ideas
---------
- Flask-style route expression, with new capabilities -- primarily, the ability to cast a parameter to integers as well as other types that are missing from Flask, all while using Python 3.6+'s new f-string syntax.
- I love Falcon's "every request and response is passed into to each view and mutated" methodology, especially `response.media`, and have used it here. In addition to supporting JSON, I have decided to support YAML as well, as Kubernetes is slowly taking over the world, and it uses YAML for all the things. Content-negotiation and all that.
Future Ideas
Testimonials
------------
- I want to be able to "mount" any WSGI app into a sub-route.
- Cooke-based sessions are currently an afterthrought, as this is an API framework, but websites are APIs too.
- Potentially support ASGI instead of WSGI. Will the tradeoffs be worth it? This is a question to ask. Procedural code works well for 90% use cases.
- If frontend websites are supported, provide an official way to run webpack.
“Pleasantly very taken with python-responder.
`@kennethreitz <https://twitter.com/kennethreitz>`_ at his absolute
best.”
—Rudraksh M.K.
Installation
============
..
"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`_
..
“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`_
.. _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
User Guides
-----------
.. toctree::
:maxdepth: 2
quickstart
tour
api
Installing Responder
--------------------
.. code-block:: shell
@@ -136,40 +116,37 @@ Installation
Only **Python 3.6+** is supported.
API Documentation
=================
The Basic Idea
--------------
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).
- Case-insensitive ``req.headers`` dict (from Requests directly).
- ``resp.status_code``, ``req.method``, ``req.url``, and other familiar friends.
Ideas
-----
- Flask-style route expression, with new capabilities -- all while using Python 3.6+'s new f-string syntax.
- I love Falcon's "every request and response is passed into to each view and mutated" methodology, especially ``response.media``, and have used it here. In addition to supporting JSON, I have decided to support YAML as well, as Kubernetes is slowly taking over the world, and it uses YAML for all the things. Content-negotiation and all that.
- **A built in testing client that uses the actual Requests you know and love**.
- The ability to mount other WSGI apps easily.
- Automatic gzipped-responses.
- In addition to Falcon's ``on_get``, ``on_post``, etc methods, Responder features an ``on_request`` method, which gets called on every type of request, much like Requests.
- A production static files server is built-in.
- Uvicorn built-in as a production web server. I would have chosen Gunicorn, but it doesn't run on Windows. Plus, Uvicorn serves well to protect against slowloris attacks, making nginx unneccessary in production.
- GraphQL support, via Graphene. The goal here is to have any GraphQL query exposable at any route, magically.
Web Service (API) Class
-----------------------
.. module:: responder
Future Ideas
------------
.. autoclass:: API
:inherited-members:
Requests & Responses
--------------------
- 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.
.. 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 resp.media()
# Process the data (in the background).
process_data(data)
# Immediately respond that upload was successful.
resp.media = {'success': True}
A ``POST`` request to ``/incoming`` will result in an immediate response of ``{'success': true}``.
+151
View File
@@ -0,0 +1,151 @@
Feature Tour
============
Class-Based Views
-----------------
Class-based views (and setting some headers and stuff)::
@api.route("/{greeting}")
class GreetingResource:
def on_request(req, resp, *, greeting): # or on_get...
resp.text = f"{greeting}, world!"
resp.headers.update({'X-Life': '42'})
resp.status_code = api.status_codes.HTTP_416
Background Tasks
----------------
Here, you can spawn off a background thread to run any function, out-of-request::
@api.route("/")
def hello(req, resp):
@api.background.task
def sleep(s=10):
time.sleep(s)
print("slept!")
sleep()
resp.content = "processing"
GraphQL
-------
Serve a GraphQL API::
import graphene
class Query(graphene.ObjectType):
hello = graphene.String(name=graphene.String(default_value="stranger"))
def resolve_hello(self, info, name):
return "Hello " + name
api.add_route("/graph", graphene.Schema(query=Query))
Built-in Testing Client (Requests)
----------------------------------
We can then send a query to our service::
>>> requests = api.session()
>>> r = requests.get("http://;/graph", params={"query": "{ hello }"})
>>> r.json()
{'data': {'hello': 'Hello stranger'}}
Or, request YAML back::
>>> r = requests.get("http://;/graph", params={"query": "{ hello(name:\"john\") }"}, headers={"Accept": "application/x-yaml"})
>>> print(r.text)
data: {hello: Hello john}
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.1"
__version__ = "0.0.7"
+149 -92
View File
@@ -3,20 +3,24 @@ import json
from functools import partial
from pathlib import Path
import waitress
import uvicorn
import asyncio
import jinja2
from whitenoise import WhiteNoise
from wsgiadapter import WSGIAdapter as RequestsWSGIAdapter
from requests import Session as RequestsSession
from werkzeug.wsgi import DispatcherMiddleware
from graphql_server import encode_execution_results, json_encode, default_format_error
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
# TODO: consider moving status codes here
@@ -31,72 +35,117 @@ 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",
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 = f"/{static_dir}"
self.templates_dir = Path(os.path.abspath(templates_dir))
self.routes = {}
self.schemas = {}
self.hsts_enabled = enable_hsts
self.apps = {"/": self._wsgi_app}
self.static_files = StaticFiles(directory=str(self.static_dir))
self.apps = {self.static_route: self.static_files}
self.formats = get_formats()
# Make the static/templates directory if they don't exist.
for _dir in (self.static_dir, self.templates_dir):
os.makedirs(_dir, exist_ok=True)
# Mount the whitenoise application.
self.whitenoise = WhiteNoise(self.__wsgi_app, root=str(self.static_dir))
# Cached requests session.
self._session = None
self.background = BackgroundQueue()
def __wsgi_app(self, environ, start_response):
# def wsgi_app(self, request):
"""The actual WSGI application. This is not implemented in
:meth:`__call__` so that middlewares can be applied without
losing a reference to the app object. Instead of doing this::
if self.openapi_version:
self.add_route(openapi_route, self.schema_response)
app = MyMiddleware(app)
@property
def _apispec(self):
spec = APISpec(
title=self.title,
version=self.version,
openapi_version=self.openapi_version,
plugins=[MarshmallowPlugin()],
)
It's a better idea to do this instead::
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)
app.wsgi_app = MyMiddleware(app.wsgi_app)
for name, schema in self.schemas.items():
spec.definition(name, schema=schema)
Then you still have the original application object around and
can continue to call methods on it.
return spec
.. versionchanged:: 0.7
Teardown events for the request and app contexts are called
even if an unhandled error occurs. Other events may not be
called depending on when an error occurs during dispatch.
See :ref:`callbacks-and-errors`.
@property
def openapi(self):
return self._apispec.to_yaml()
def __call__(self, scope):
path = scope["path"]
root_path = scope.get("root_path", "")
# Call into a submounted app, if one exists.
for path_prefix, app in self.apps.items():
if path.startswith(path_prefix):
scope["path"] = path[len(path_prefix) :]
scope["root_path"] = root_path + path_prefix
try:
return app(scope)
except TypeError:
app = WsgiToAsgi(app)
return app(scope)
# Call the main dispatcher.
async def asgi(receive, send):
nonlocal scope, self
req = models.Request(scope, receive=receive)
resp = await self._dispatch_request(req)
await resp(receive, send)
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()
:param environ: A WSGI environment.
:param start_response: A callable accepting a status code,
a list of headers, and an optional exception context to
start the response.
"""
req = models.Request(environ, start_response)
# if not req.dispatched:
resp = self._dispatch_request(req)
return resp(environ, start_response)
def decorator(f):
self.add_schema(name=name, schema=f, **options)
return f
def _wsgi_app(self, environ, start_response):
return self.whitenoise(environ, start_response)
def wsgi_app(self, environ, start_response):
"""Returns the WSGI app for this application (including all mounted WSGI apps)."""
apps = self.apps.copy()
main = apps.pop("/")
return DispatcherMiddleware(main, apps)(environ, start_response)
def __call__(self, environ, start_response=None):
"""The WSGI server calls the Flask application object as the
WSGI application. This calls :meth:`wsgi_app` which can be
wrapped to applying middleware."""
return self.wsgi_app(environ, start_response)
return decorator
def path_matches_route(self, path):
"""Given a path portion of a URL, tests that it matches against any registered route.
@@ -107,11 +156,11 @@ class API:
if route_object.does_match(path):
return route
def _dispatch_request(self, req):
async def _dispatch_request(self, req):
# Set formats on Request object.
req.formats = self.formats
route = self.path_matches_route(req.path)
route = self.path_matches_route(req.url.path)
resp = models.Response(req=req, formats=self.formats)
if self.hsts_enabled:
@@ -121,10 +170,12 @@ class API:
if route:
try:
params = self.routes[route].incoming_matches(req.path)
self.routes[route].endpoint(req, resp, **params)
params = self.routes[route].incoming_matches(req.url.path)
result = self.routes[route].endpoint(req, resp, **params)
if hasattr(result, "cr_running"):
await result
# The request is using class-based views.
except TypeError:
except TypeError as e:
try:
view = self.routes[route].endpoint(**params)
except TypeError:
@@ -132,16 +183,16 @@ class API:
try:
# GraphQL Schema.
assert hasattr(view, "execute")
self.graphql_response(req, resp, schema=view)
await self.graphql_response(req, resp, schema=view)
except AssertionError:
# WSGI App.
try:
req.dispatched = True
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:
@@ -150,7 +201,7 @@ class API:
pass
# Then on_get.
method = req.method
method = req.method.lower()
try:
getattr(view, f"on_{method}")(req, resp)
@@ -176,9 +227,14 @@ class API:
self.routes[route] = Route(route, endpoint)
def default_response(self, req, resp):
resp.status_code = HTTP_404
resp.status_code = status_codes.HTTP_404
resp.text = "Not found."
def 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
):
@@ -198,15 +254,16 @@ 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.
if "query" in req.media("form"):
return req.media("form")["query"]
if "q" in req.media("form"):
return req.media("form")["q"]
# Form data is awaiting https://github.com/encode/starlette/pull/102
# if "query" in req.media("form"):
# return req.media("form")["query"]
# if "q" in req.media("form"):
# return req.media("form")["q"]
# Support query/q in params.
if "query" in req.params:
@@ -218,8 +275,8 @@ class API:
# TODO: Make some assertions about content-type here.
return req.text
def graphql_response(self, req, resp, schema):
query = self._resolve_graphql_query(req)
async def graphql_response(self, req, resp, schema):
query = await self._resolve_graphql_query(req)
result = schema.execute(query)
result, status_code = encode_execution_results(
[result],
@@ -231,13 +288,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!"
"""
@@ -247,27 +304,25 @@ class API:
return decorator
def mount(self, route, wsgi_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: wsgi_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.
"""
if self._session is None:
session = RequestsSession()
session.mount(base_url, RequestsWSGIAdapter(self))
self._session = session
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.
@@ -276,9 +331,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
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.
@@ -334,25 +393,23 @@ class API:
template = env.from_string(s)
return template.render(**values)
def run(self, address=None, port=None, **kwargs):
"""Runs the application with Waitress. If the ``PORT`` environment
def run(self, address=None, port=None, **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 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"
if port is None:
port = 0
port = 5042
bind_to = f"{address}:{port}"
waitress.serve(app=self, listen=bind_to, **kwargs)
uvicorn.run(self, host=address, port=port, **options)
+27
View File
@@ -0,0 +1,27 @@
import multiprocessing
import concurrent.futures
class BackgroundQueue:
def __init__(self, n=None):
if n is None:
n = multiprocessing.cpu_count()
self.n = n
self.pool = concurrent.futures.ThreadPoolExecutor(max_workers=n)
self.results = []
def run(self, f, *args, **kwargs):
self.pool._max_workers = self.n
self.pool._adjust_thread_count()
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)
return result
return do_task
-1
View File
@@ -1 +0,0 @@
import docopt
+7 -7
View File
@@ -2,25 +2,25 @@ import yaml
import json
def format_form(r, encode=False):
async def format_form(r, encode=False):
if not encode:
return r._wz.form
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.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():
+198 -73
View File
@@ -2,81 +2,195 @@ import io
import json
import gzip
import chardet
import rfc3986
import graphene
import yaml
from requests.models import Request as RequestsRequest
from requests.structures import CaseInsensitiveDict
from werkzeug.wrappers import Request as WerkzeugRequest
from werkzeug.wrappers import BaseResponse as WerkzeugResponse
from starlette.datastructures import MutableHeaders
from starlette.requests import Request as StarletteRequest
from starlette.responses import Response as StarletteResponse
from urllib.parse import parse_qs
from .status_codes import HTTP_200
# @staticmethod
# def funcname(parameter_list):
# pass
from .statics import DEFAULT_ENCODING
def flatten(d):
for key, value in d.copy().items():
if len(value) == 1:
d[key] = value[0]
class QueryDict(dict):
def __init__(self, query_string):
self.update(parse_qs(query_string))
return d
def __getitem__(self, key):
"""
Return the last data value for this key, or [] if it's an empty list;
raise KeyError if not found.
"""
list_ = super().__getitem__(key)
try:
return list_[-1]
except IndexError:
return []
def get(self, key, default=None):
"""
Return the last data value for the passed key. If key doesn't exist
or value is an empty list, return `default`.
"""
try:
val = self[key]
except KeyError:
return default
if val == []:
return default
return val
def _get_list(self, key, default=None, force_list=False):
"""
Return a list of values for the key.
Used internally to manipulate values list. If force_list is True,
return a new copy of values.
"""
try:
values = super().__getitem__(key)
except KeyError:
if default is None:
return []
return default
else:
if force_list:
values = list(values) if values is not None else None
return values
def get_list(self, key, default=None):
"""
Return the list of values for the key. If key doesn't exist, return a
default value.
"""
return self._get_list(key, default, force_list=True)
def items(self):
"""
Yield (key, value) pairs, where value is the last item in the list
associated with the key.
"""
for key in self:
yield key, self[key]
def items_list(self):
"""
Yield (key, value) pairs, where value is the the list.
"""
yield from super().items()
# TODO: add slots
class Request:
def __init__(self, environ, start_response=None):
self._wz = WerkzeugRequest(environ)
self.start_response = start_response
self.headers = CaseInsensitiveDict(
self._wz.headers.to_wsgi_list()
) #: A case-insensitive dictionary, containg all headers sent in the Request.
self.method = (
self._wz.method.lower()
) #: The incoming HTTP method used for the request, lower-cased.
self.full_url = (
self._wz.url
) #: The full URL of the Request, query parameters and all.
self.url = (
self._wz.base_url
) #: The URL of the Request, without query parameters.
self.full_path = (
self._wz.full_path
) #: The full path portion of the URL of the Request, query parameters and all.
self.path = (
self._wz.path
) #: The path portion of the URL of the Request, without query parameters.
self.params = flatten(
parse_qs(self._wz.query_string.decode("utf-8"))
) #: A dictionary of the parsed query paramaters used for the Request.
self.query = self._wz.query_string.decode(
"utf-8"
) #: A string containing only the query paramaters of the Request.
self.raw = self._wz.stream #: A raw file-like stream of the incoming Request.
self.content = self._wz.get_data(
cache=True, as_text=False
) #: The Request body, as bytes.
self.mimetype = self._wz.mimetype #: The mimetype of the incoming Request.
# TODO: rip that out
self.text = self._wz.get_data(
cache=False, as_text=True
) #: The Request body, as unicode.
__slots__ = [
"_starlette",
"formats",
"_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
@property
def headers(self):
"""A case-insensitive dictionary, containing all headers sent in the Request."""
return self._headers
@property
def mimetype(self):
return self.headers.get("Content-Type", "")
@property
def method(self):
"""The incoming HTTP method used for the request, lower-cased."""
return self._starlette.method.lower()
@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:
return QueryDict(self.url.query)
except AttributeError:
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. Must be awaited."""
return await self._starlette.body()
@property
async def text(self):
"""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):
"""Returns ``True`` if the incoming Request was securely made."""
return self._wz.is_secure
return self.url.scheme == "https"
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.
@@ -84,20 +198,32 @@ 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:
__slots__ = [
"req",
"status_code",
"text",
"content",
"encoding",
"media",
"headers",
"formats",
]
def __init__(self, req, *, formats):
self.req = req
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.
@@ -107,25 +233,27 @@ class Response:
self.formats = formats
@property
def body(self):
async def body(self):
if self.content:
return (self.content, self.mimetype, {})
return (self.content, {})
if self.text:
return (self.text.encode(self.encoding), {"Encoding": self.encoding})
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 (json.dumps(self.media), {"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)
@@ -147,20 +275,17 @@ class Response:
else:
return (body, headers)
@property
def _wz(self):
body, headers = self.body
if len(self.body) > 500:
body, headers = self.gzipped_body
async def __call__(self, receive, send):
body, headers = await self.body
if len(await self.body) > 500:
body, headers = await self.gzipped_body
if self.headers:
headers.update(self.headers)
r = WerkzeugResponse(body, status=self.status_code, direct_passthrough=False)
r.headers = headers
return r
def __call__(self, environ, start_response):
return self._wz(environ, start_response)
response = StarletteResponse(
body, status_code=self.status_code, headers=headers
)
await response(receive, send)
class Schema(graphene.Schema):
+22 -3
View File
@@ -1,10 +1,21 @@
from parse import parse, search
def memoize(f):
def helper(self, 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
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}>"
@@ -17,10 +28,15 @@ 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)])
@memoize
def does_match(self, s):
if s == self.route:
return True
@@ -28,11 +44,14 @@ class Route:
named = self.incoming_matches(s)
return bool(len(named))
@memoize
def incoming_matches(self, s):
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
+1
View File
@@ -0,0 +1 @@
DEFAULT_ENCODING = "utf-8"
+11 -5
View File
@@ -22,16 +22,22 @@ if sys.argv[-1] == "publish":
sys.exit()
required = [
"waitress",
"werkzeug",
"starlette",
"uvicorn",
"aiofiles",
"pyyaml",
"requests",
"requests-wsgi-adapter",
"graphene",
"graphql-server-core>=1.1",
"whitenoise",
"jinja2",
"parse",
"uvloop ; sys_platform != 'win32'",
"rfc3986",
"python-multipart",
"chardet",
"apispec>=1.0.0b1",
"marshmallow",
"asgiref",
]
@@ -112,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 -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') }}
-198
View File
@@ -1,198 +0,0 @@
import graphene
import pytest
import responder
import yaml
@pytest.fixture
def api():
return responder.API()
@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 "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):
TEXT = "spiral out"
@api.route("/")
def hello(req, resp):
resp.text = TEXT
assert api.session().get("http://;/").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": None}}
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"}
def test_form_data(api):
@api.route("/")
def route(req, resp):
resp.media = {"form": req.media("form")}
dump = {"q": "q"}
r = api.session().get("http://;/", data=dump)
assert r.json()["form"] == dump
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"}
+334
View File
@@ -0,0 +1,334 @@
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_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_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
+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