Compare commits

..

198 Commits

Author SHA1 Message Date
kennethreitz 855d3c4320 cookies 2018-10-27 06:21:19 -04:00
kennethreitz 4564862acc xfail 2018-10-27 06:11:47 -04:00
kennethreitz 176dd70073 fix 301 redirects 2018-10-27 06:07:50 -04:00
kennethreitz a5e6f0c196 version 2018-10-27 05:39:17 -04:00
kennethreitz 083bb5a96c Merge branch 'master' of github.com:kennethreitz/responder 2018-10-27 05:13:39 -04:00
kennethreitz 04522281be don't do whitenoise index file 2018-10-27 05:13:30 -04:00
kennethreitz 0e8bb49b59 Merge pull request #164 from taoufik07/patch-12
Fix typo
2018-10-27 04:51:34 -04:00
Taoufik 9abf6eea16 typo 2018-10-26 23:57:16 +01:00
Taoufik 1d7a04ce7b Fix typo 2018-10-26 23:15:13 +01:00
kennethreitz 49fb5792c3 Merge branch 'master' of github.com:kennethreitz/responder 2018-10-26 17:04:22 -04:00
kennethreitz 5eebba09c5 test redirects 2018-10-26 17:04:14 -04:00
kennethreitz b86974688e Merge pull request #163 from Hwesta/patch-1
Fix typo in tour.rst
2018-10-26 15:24:06 -04:00
Holly Becker 74afe2ed13 Fix typo in tour.rst 2018-10-26 12:19:33 -07:00
kennethreitz ed53a0b624 Merge pull request #161 from repodevs/fix-doc-quickstart
DOC: Fix quickstart response headers
2018-10-26 14:17:31 -04:00
kennethreitz 23e15d6459 Merge pull request #162 from sheb/patch-3
fix CI build failing since secret key has changed
2018-10-26 14:17:20 -04:00
Sébastien Geffroy 71ea19d1c1 fix CI build failing since secret key has changed 2018-10-26 20:15:05 +02:00
Edi Santoso fa621d076d DOC: Fix quickstart response headers 2018-10-27 00:53:11 +07:00
kennethreitz 4902f1328a Merge pull request #160 from frostming/graphql-doc
Fix doc about graphql usage.
2018-10-26 11:57:15 -04:00
kennethreitz 2ee8ff484d better 2018-10-26 11:09:24 -04:00
kennethreitz c872fe3c78 image 2018-10-26 11:08:37 -04:00
kennethreitz a08b275463 fix 2018-10-26 10:51:48 -04:00
kennethreitz 9717208dd4 v1.0.1 2018-10-26 10:51:35 -04:00
kennethreitz c9a233f5e5 api cleanup 2018-10-26 10:50:58 -04:00
kennethreitz 7389350ff9 fail 2018-10-26 10:48:12 -04:00
Frost Ming f46ac08cff Fix doc about graphql usage. 2018-10-26 22:03:43 +08:00
kennethreitz 296d5e7974 pipfile.lock 2018-10-26 09:19:31 -04:00
kennethreitz fe0bea686c simplify 2018-10-26 08:22:04 -04:00
kennethreitz 838d172512 v1.0.0 2018-10-26 08:10:49 -04:00
kennethreitz 2c02c51c37 fix docs 2018-10-26 08:09:56 -04:00
kennethreitz 67a4cbca2c move graphql to extension 2018-10-26 08:07:24 -04:00
kennethreitz a2f97e727f powered by starlette 2018-10-26 07:39:02 -04:00
kennethreitz 462506113e cleanup 2018-10-26 07:36:41 -04:00
kennethreitz 5f2a72203f cleanup things 2018-10-26 07:29:46 -04:00
kennethreitz d6febe2d02 test for startup 2018-10-26 07:01:28 -04:00
kennethreitz c2bd1e989a add_event_handler update 2018-10-26 06:55:33 -04:00
kennethreitz f886c2c050 cleanup 2018-10-26 06:54:15 -04:00
kennethreitz ae770e603a Merge branch 'master' into master 2018-10-26 06:48:42 -04:00
kennethreitz 7b79472d65 v0.3.3 2018-10-25 18:15:22 -04:00
kennethreitz 090a3a571b Merge pull request #156 from taoufik07/patch-11
Fix link formatting
2018-10-25 18:12:36 -04:00
kennethreitz f9d55fc425 Merge pull request #157 from steinnes/re-raise-exception-after-response
Improve exception handling
2018-10-25 18:12:24 -04:00
Steinn Eldjárn Sigurðarson 4f57e8a5d1 Improve exception handling
Re-raise exceptions caught in _dispatch_request.  Added starlette
ExceptionMiddleware to be able to test this gracefully.
2018-10-25 22:08:10 +00:00
Taoufik 1e6c9d935a Fix link formatting 2018-10-25 22:45:29 +01:00
kennethreitz 00cfde169b remove future ideas 2018-10-25 17:41:58 -04:00
kennethreitz 02733ac718 Merge pull request #155 from taoufik07/features/CORS
Custom CORS params and docs
2018-10-25 17:36:40 -04:00
Taoufik 55b55e62da Improvements 2018-10-25 20:58:01 +01:00
Taoufik 5fccedd4c4 Improvement 2018-10-25 20:55:14 +01:00
kennethreitz b9ad78ec79 Merge branch 'master' of github.com:kennethreitz/responder 2018-10-25 15:44:07 -04:00
taoufik07 64ac6bcd1f Add CORS docs 2018-10-25 20:44:04 +01:00
kennethreitz 45e4d80c4d --pre 2018-10-25 15:44:01 -04:00
taoufik07 a5b1652d15 Custom CORS params via cors_params 2018-10-25 20:43:50 +01:00
kennethreitz f954eb7d88 Merge pull request #154 from taoufik07/features/CORS
CORS
2018-10-25 15:30:19 -04:00
taoufik07 53216813e5 Add CORSMiddleware 2018-10-25 20:12:21 +01:00
kennethreitz 1618203930 Merge pull request #153 from metakermit/address_doc
document how to customise the address
2018-10-25 13:40:58 -04:00
Dražen Lučanin 237a2ed426 document how to customise the address 2018-10-25 16:43:19 +02:00
kennethreitz d33289503a remove is_routed 2018-10-25 08:24:45 -04:00
kennethreitz f5ff4c9725 clean up 2018-10-25 08:23:22 -04:00
kennethreitz 62f932dcfc default secret key 2018-10-25 08:22:14 -04:00
kennethreitz b66112d0ca improvements 2018-10-25 08:20:06 -04:00
kennethreitz b98354e63a simplify readme 2018-10-25 07:31:34 -04:00
kennethreitz 94b3625718 ideas 2018-10-25 07:29:05 -04:00
kennethreitz f7ee720281 subtle improvements 2018-10-25 07:26:05 -04:00
kennethreitz 4ab523bf01 fix static 2018-10-25 07:25:35 -04:00
kennethreitz 2d4f1bfd02 test static 2018-10-25 07:10:51 -04:00
kennethreitz 38426c9143 for tests 2018-10-25 07:10:42 -04:00
kennethreitz bdf151e0a7 test responder docs 2018-10-25 07:08:45 -04:00
kennethreitz 9768b7888d Merge branch 'master' of github.com:kennethreitz/responder 2018-10-25 06:59:05 -04:00
kennethreitz 740a48566f important stuff 2018-10-25 06:58:57 -04:00
kennethreitz 475cd1a106 removed 2018-10-25 06:47:58 -04:00
kennethreitz 38e7c39d69 Merge pull request #151 from tkamenoko/patch-2
fix: license mismatch in setup.py
2018-10-25 06:38:45 -04:00
T.Kameyama 774db6bead fix: license mismatch in setup.py 2018-10-25 18:52:00 +09:00
kennethreitz a1a3e0412a Merge pull request #148 from taoufik07/patch-10
Quick fix
2018-10-24 19:09:59 -04:00
Taoufik 0b39c89e60 Quick fix 2018-10-24 19:17:53 +01:00
kennethreitz 53be4d8954 index.rst 2018-10-24 10:32:28 -04:00
kennethreitz 03812cc7eb v0.3.1 2018-10-24 08:34:46 -04:00
kennethreitz aa12b24293 whitenoise 2018-10-24 08:34:06 -04:00
kennethreitz daf43009ba changelog 2018-10-24 08:21:08 -04:00
kennethreitz 955d777ca5 Merge pull request #146 from JayjeetAtGithub/master
Improve readability
2018-10-24 08:19:00 -04:00
kennethreitz cc9472aa2f Merge pull request #147 from taoufik07/fix/default_endpoint_and_route_found
Do not return default endpoint if the route is found
2018-10-24 08:18:38 -04:00
kennethreitz e527f3cb1f interactive documentation 2018-10-24 08:19:28 -04:00
kennethreitz 3a375a8975 documentation 2018-10-24 08:17:26 -04:00
kennethreitz 2698496592 documentation endpoint 2018-10-24 08:16:55 -04:00
kennethreitz 0155d854e3 v0.3.0 2018-10-24 08:16:22 -04:00
kennethreitz c74cc8586f improve imports 2018-10-24 08:15:21 -04:00
kennethreitz 8eb89da9a0 improve imports 2018-10-24 08:14:24 -04:00
kennethreitz dee6ee3cef whitenoise, apistar 2018-10-24 08:13:15 -04:00
kennethreitz beab89df09 apistar 2018-10-24 08:12:45 -04:00
kennethreitz 5164d4ec32 apistar 2018-10-24 08:12:39 -04:00
taoufik07 878db851af Return default endpoint if the route is not found 2018-10-24 12:47:47 +01:00
Jayjeet Chakraborty 686ff72ae0 Improve readability 2018-10-24 16:37:02 +05:30
kennethreitz 2710d7098f v0.2.3 2018-10-24 07:02:44 -04:00
kennethreitz 7f41ff4035 Merge pull request #138 from taoufik07/fix/cbv
Fix CBV
2018-10-24 06:59:51 -04:00
kennethreitz ed8d51014c Merge branch 'master' into fix/cbv 2018-10-24 06:57:28 -04:00
kennethreitz d09a51f47d Merge pull request #140 from taoufik07/patch-9
Typo
2018-10-24 06:56:53 -04:00
kennethreitz 59bae90454 Merge pull request #142 from taoufik07/fix/static_response
Fix static response
2018-10-24 06:56:42 -04:00
kennethreitz 13ee0ca94e Merge pull request #136 from taoufik07/fix/Route.is_function
Fix Route.is_function
2018-10-24 06:56:24 -04:00
kennethreitz 5abc095050 Merge pull request #139 from JayjeetAtGithub/master
Fix Typo in api.py
2018-10-24 06:56:02 -04:00
kennethreitz 7eb68c8388 Merge pull request #143 from frostming/patch-1
Typo in tour.rst
2018-10-24 06:55:50 -04:00
Frost Ming f69b644a77 Typo in tour.rst 2018-10-24 12:28:11 +08:00
Peder Bergebakken Sundt 6b93125ff2 Add support for "tick" in api.on_event 2018-10-24 06:26:58 +02:00
Peder Bergebakken Sundt 43faef4569 Add api.on_event decorator supporting startup and cleanup 2018-10-24 06:25:44 +02:00
taoufik07 fe41d4c863 Fix static response 2018-10-24 01:17:02 +01:00
Taoufik 29830455ed Typo 2018-10-24 00:11:27 +01:00
Jayjeet Chakraborty e50828093d Clean print statement 2018-10-24 02:56:43 +05:30
Jayjeet Chakraborty 880d29c5a9 Fix Typo in api.py 2018-10-24 02:46:33 +05:30
taoufik07 77b2e9ba7a tests 2018-10-23 21:20:09 +01:00
taoufik07 586fad7646 Fix CBV 2018-10-23 21:19:57 +01:00
kennethreitz fb636028fb improvements 2018-10-23 14:58:02 -04:00
taoufik07 a8c3f8fc46 Fix Route.is_function 2018-10-23 19:52:43 +01:00
kennethreitz 72f4227c5a remove redundancy in tour 2018-10-23 08:46:20 -04:00
kennethreitz 8ccace8ef9 testing 2018-10-23 08:36:20 -04:00
kennethreitz 6d40c6dfe5 assert 2018-10-23 08:34:32 -04:00
kennethreitz 0b5562cdec fix 2018-10-23 08:33:51 -04:00
kennethreitz eeff0816f3 doc updates 2018-10-23 08:29:02 -04:00
kennethreitz f1f16dea3f best practices for secret key 2018-10-23 08:14:00 -04:00
kennethreitz bfc6ef2049 test client docs 2018-10-23 08:12:40 -04:00
kennethreitz 5212de79d3 v0.2.2 2018-10-23 08:05:07 -04:00
kennethreitz b61c02e5df Merge pull request #132 from vuonghv/show-exception-background-task
Show traceback info when background tasks raise exceptions
2018-10-23 08:02:59 -04:00
kennethreitz f982954e8f versions 2018-10-23 08:03:28 -04:00
kennethreitz 3ba20e69ba requests session 2018-10-23 08:02:30 -04:00
kennethreitz aea01fd893 Revert "idk what's happening"
This reverts commit e34cb539d2.
2018-10-23 08:00:56 -04:00
kennethreitz 950be14eca Revert "Merge branch 'master' of github.com:kennethreitz/responder"
This reverts commit 446deffc17, reversing
changes made to e0863115ee.
2018-10-23 08:00:30 -04:00
kennethreitz 446deffc17 Merge branch 'master' of github.com:kennethreitz/responder 2018-10-23 07:59:07 -04:00
kennethreitz e0863115ee use api.requests 2018-10-23 07:59:00 -04:00
kennethreitz e34cb539d2 idk what's happening 2018-10-23 07:58:48 -04:00
kennethreitz d8ade8638a merge 2018-10-23 07:57:23 -04:00
Vuong Hoang 3067080474 Show traceback when background tasks raise exceptions 2018-10-23 18:23:22 +07:00
kennethreitz 886cc0f214 Merge pull request #131 from daikeren/master
Fix Route.is_function
2018-10-23 07:01:40 -04:00
Andy Dai 071d34b016 Fix Route.is_function 2018-10-23 17:07:42 +08:00
kennethreitz a1564ca003 Merge pull request #123 from taoufik07/patch-7
Quick fix
2018-10-22 17:25:15 -04:00
Taoufik 60f0e765c2 Quick fix 2018-10-22 22:14:18 +01:00
kennethreitz 3f0ecea4bf Merge pull request #120 from Pentusha/master
Depend on marshmallow>=3.0.0b7
2018-10-22 17:06:00 -04:00
kennethreitz 2c9e6572c5 Update tour.rst 2018-10-22 17:05:50 -04:00
Ivan Larin 371a83f20f Depend on marshmallow>=3.0.0b7 kennethreitz/responder#119 2018-10-22 19:46:55 +03:00
kennethreitz b8cff1655a websockets 2018-10-22 10:18:37 -04:00
kennethreitz 232856ca3a Merge branch 'master' of github.com:kennethreitz/responder 2018-10-22 10:07:38 -04:00
kennethreitz 3f168ac6fd slots 2018-10-22 10:06:55 -04:00
kennethreitz c59cb1d0d3 websocket 2018-10-22 10:06:03 -04:00
kennethreitz ec13df75d0 kinda test websocket support 2018-10-22 10:05:20 -04:00
kennethreitz 6fc02964ba cleanup 2018-10-22 09:59:38 -04:00
kennethreitz ed79e45680 Merge pull request #116 from tkamenoko/patch-1
doc: fix Class-based views
2018-10-22 09:30:14 -04:00
kennethreitz 1be983bf89 cleanup 2018-10-22 09:28:14 -04:00
T.Kameyama b09d6a9d04 doc: fix Class-based views
In Class-based views, each method needs `self` as 1st argument.
2018-10-22 14:37:55 +09:00
taoufik07 db143d845d cleanup 2018-10-21 18:17:56 +01:00
taoufik07 2e23501f9d Fix check_existing 2018-10-21 18:15:13 +01:00
taoufik07 bd6addcd3a Add websocket support 2018-10-21 18:00:25 +01:00
taoufik07 631e1fb604 Add WebSocket 2018-10-21 17:36:21 +01:00
kennethreitz 30ee6726a8 Merge pull request #113 from metakermit/extend-static-docs
Extend static=True docs
2018-10-21 06:25:56 -04:00
kennethreitz 1c397db9d8 cleanup 2018-10-21 06:23:02 -04:00
kennethreitz cc23ca80f4 Merge pull request #112 from Nitish18/feat/server_debug_mode
feat: added debug mode for uvicorn server
2018-10-21 06:20:16 -04:00
Dražen Lučanin 449379a0ed extend static=True docs 2018-10-21 11:58:54 +02:00
Nitish Chauhan b3208b1c5b feat: added debug mode for uvicorn server 2018-10-21 15:20:08 +05:30
kennethreitz 4df60b55a6 Merge pull request #110 from sheb/patch-2
fix an AttributeError when route does not exist
2018-10-20 13:54:58 -07:00
Sébastien Geffroy 379553a1a5 fix an AttributeError when route does not exist 2018-10-20 21:55:43 +02:00
kennethreitz a2eaa5c7b5 Merge branch 'master' of github.com:kennethreitz/responder 2018-10-20 14:46:21 -04:00
kennethreitz 175c46e68c __version__ 2018-10-20 14:46:10 -04:00
kennethreitz a58cc11079 500 support 2018-10-20 14:45:52 -04:00
kennethreitz 218a375c27 test 500s 2018-10-20 14:45:33 -04:00
kennethreitz 567b1577c6 Merge pull request #108 from shyamjos/patch-1
Fixed minor spelling mistakes in changelog
2018-10-20 11:21:11 -07:00
kennethreitz 3c3687d11f Merge pull request #109 from taoufik07/patch-6
clean up
2018-10-20 11:21:00 -07:00
Taoufik 19dfac8340 clean up 2018-10-20 18:37:04 +01:00
kennethreitz b61feafe5a 500 on errrors 2018-10-20 13:36:06 -04:00
Shyam Jos 0c342c8b3e Corrected Spelling
Corrected Spelling
2018-10-20 23:01:57 +05:30
Shyam Jos dbcba8fad7 Fixed minor spelling mistakes in changelog
Fixed minor spelling mistakes in changelog
2018-10-20 22:24:44 +05:30
kennethreitz b8053e20f2 fix 2018-10-20 12:10:59 -04:00
kennethreitz 1896901aa8 Merge branch 'master' of github.com:kennethreitz/responder 2018-10-20 12:10:19 -04:00
kennethreitz 383c9132ed improvement 2018-10-20 12:10:09 -04:00
kennethreitz 57b144c3e7 Merge pull request #107 from taoufik07/patch-5
Refactor Route._weight and f-strings everywhere
2018-10-20 07:08:51 -07:00
kennethreitz eed5365fe0 file upload support 2018-10-20 09:56:35 -04:00
kennethreitz f5905568c4 files support 2018-10-20 09:54:53 -04:00
kennethreitz 096099470e yay tests pass 2018-10-20 08:50:36 -04:00
kennethreitz e7ed7aca3c tests still pass 2018-10-20 08:23:10 -04:00
kennethreitz 6725b275b8 cleanup 2018-10-20 07:59:39 -04:00
kennethreitz 3447a7ef41 v0.1.5 2018-10-20 07:59:12 -04:00
kennethreitz 99f35fbea4 use querydict for form parsing 2018-10-20 07:57:27 -04:00
kennethreitz 5c9a3912a9 cached _content 2018-10-20 07:38:53 -04:00
Taoufik 5d43c0418c f-string 2018-10-19 23:13:21 +01:00
Taoufik 87c0076e12 use f-string
Every time I scroll through the README, it hurts me
2018-10-19 23:10:39 +01:00
Taoufik 95252ac697 Refactor 2018-10-19 23:06:55 +01:00
kennethreitz 5bb9f96701 cleanup 2018-10-19 05:11:07 -07:00
kennethreitz 750e9dfaa7 cleanup 2018-10-19 04:54:49 -07:00
kennethreitz f34f3c1661 v0.1.4 2018-10-19 04:17:06 -07:00
kennethreitz d4f83c978c improvements 2018-10-19 04:16:19 -07:00
kennethreitz 212f280c19 models 2018-10-19 03:10:39 -07:00
kennethreitz f3e2450636 models 2018-10-19 03:09:53 -07:00
kennethreitz d6d496018d fix 2018-10-19 03:08:15 -07:00
kennethreitz 78be7fc772 api 2018-10-19 03:00:41 -07:00
kennethreitz 6ebadd8469 new files 2018-10-19 02:19:38 -07:00
kennethreitz 557750c8d4 customizable cookie 2018-10-18 17:02:10 -07:00
kennethreitz e85ef27e6c Merge pull request #98 from pbsds/master
Store Jinja enviroment in between template render calls
2018-10-18 16:46:31 -07:00
kennethreitz 4ca961a1b4 Merge pull request #104 from metakermit/cli-build
CLI build command
2018-10-18 16:42:17 -07:00
kennethreitz 6a9110e9c1 Merge branch 'master' into cli-build 2018-10-18 16:40:20 -07:00
Dražen Lučanin 51ffce09ae fix cli setup 2018-10-18 23:48:09 +02:00
Peder Bergebakken Sundt 1c4e96b365 Add api.jinja_values_base:dict
This allows the user to add functions and values for use in all
templates, without needing to pass them on each render call.

As a side effect: The reference to `api` is still passed into the template view,
but this now yield to the values passed into api.template(), like one
would normally expect.
2018-10-18 20:47:59 +02:00
Peder Bergebakken Sundt 0db70e8edd Store jinja enviroment in between the template render calls
This allows the user to modify the jinja
enviroment, adding custom filters and such
2018-10-18 20:47:34 +02:00
Peder Bergebakken Sundt e46b3a5e19 Rename s to s_ in api.template_string()
Issue described in #76 applied here as well, however less propable.
Same fix as in a8fc78fcda
2018-10-18 20:47:14 +02:00
kennethreitz fdd3d4d85a sessions 2018-10-18 10:25:19 -07:00
kennethreitz a1bfbda05b Merge branch 'master' into cli 2018-10-17 03:15:49 -07:00
kennethreitz f309ad7746 cli 2018-10-16 05:14:48 -07:00
26 changed files with 942 additions and 503 deletions
+53 -2
View File
@@ -1,3 +1,54 @@
# v1.0.4
- Potential bufix for cookies.
# v1.0.3
- Bugfix for redirects.
# v1.0.2
- Improvement for static file hosting.
# v1.0.1
- Improve cors configuration settings.
# v1.0.0
- Move GraphQL support into a built-in plugin.
# v0.3.3
- Improved exceptions.
- CORS support.
# v0.3.2
- Subtle improvements.
# v0.3.1
- Packaging fix.
# v0.3.0
- Interactive Documentation endpoint.
- Minor improvements.
# v0.2.3
- Overall improvements.
# v0.2.2
- Show traceback info when background tasks raise exceptions.
# v0.2.1
- api.requests.
# v0.2.0
- WebSocket support.
# v0.1.6
- 500 support.
# v0.1.5
- Improvements to sequential media reading.
- File upload support.
# v0.1.4
- Stability.
# v0.1.3
- Sessions support.
@@ -11,7 +62,7 @@
- Prototype of static application support.
# v0.0.10
- Bufgix for async class-based views.
- Bugfix for async class-based views.
# v0.0.9
- Bugfix for async class-based views.
@@ -32,7 +83,7 @@
- Safe load/dump yaml.
# v0.0.4:
- Asyncronous support for data uploads.
- Asynchronous support for data uploads.
- Bug fixes.
# v0.0.3:
Generated
+59 -109
View File
@@ -37,6 +37,12 @@
],
"version": "==1.0.0b3"
},
"apistar": {
"hashes": [
"sha256:4338b24468b49526ceac4a8f84046056081ee747f373ca8d0647bd6b2344c895"
],
"version": "==0.6.0"
},
"asgiref": {
"hashes": [
"sha256:9b05dcd41a6a89ca8c6e7f7e4089c3f3e76b5af60aebb81ae6d455ad81989c97",
@@ -72,6 +78,12 @@
],
"version": "==7.0"
},
"docopt": {
"hashes": [
"sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491"
],
"version": "==0.6.2"
},
"graphene": {
"hashes": [
"sha256:b8ec446d17fa68721636eaad3d6adc1a378cb6323e219814c8f98c9928fc9642",
@@ -133,10 +145,10 @@
},
"marshmallow": {
"hashes": [
"sha256:82b201ad767eb54de371c08cb1db6ca4ad2a728fa41b831e3781bf944815eb38",
"sha256:c250f37ac0e249a8287394a60d91f6240b674642ad999e66cd09463dbccd1d4f"
"sha256:5e0053c86e3abaa72a03bbe0021ec97270c13fd6400b682eb1aeaf24b871bc8a",
"sha256:81884e930c1db72d8b8e3d8d2d090f2f43427e5c11c37f703b29879980491ab6"
],
"version": "==3.0.0b18"
"version": "==3.0.0b19"
},
"parse": {
"hashes": [
@@ -174,6 +186,13 @@
],
"version": "==2.20.0"
},
"requests-toolbelt": {
"hashes": [
"sha256:42c9c170abc2cacb78b8ab23ac957945c7716249206f90874651971a4acff237",
"sha256:f6a531936c6fa4c6cfce1b9c10d5c4f498d16528d2a54a22ca00011205a187b5"
],
"version": "==0.8.0"
},
"responder": {
"editable": true,
"path": "."
@@ -201,9 +220,9 @@
},
"starlette": {
"hashes": [
"sha256:ce5c684fad4edb2967cd491518cd3c2724e420508202c2d48f519ea68dcec9d6"
"sha256:eac0f6cab6b48846a0c1af16615430ae0e7a95f669ee0841a7e2f242d51d8935"
],
"version": "==0.5.4"
"version": "==0.5.5"
},
"urllib3": {
"hashes": [
@@ -214,9 +233,9 @@
},
"uvicorn": {
"hashes": [
"sha256:30096b58325cdca8e547a6f5f4300040d0b8763f573cb1843abfa96f81a49cf8"
"sha256:e2b742fdaa0b52f4aac92fd2c078e7f1f17d11322bb3efb09d341d5c6998b4b5"
],
"version": "==0.3.13"
"version": "==0.3.16"
},
"websockets": {
"hashes": [
@@ -243,6 +262,13 @@
"sha256:f1414e6cbcea8d22843e7eafdfdfae3dd1aba41d1945f6ca66e4806c07c4f454"
],
"version": "==6.0"
},
"whitenoise": {
"hashes": [
"sha256:133a92ff0ab8fb9509f77d4f7d0de493eca19c6fea973f4195d4184f888f2e02",
"sha256:32b57d193478908a48acb66bf73e7a3c18679263e3e64bfebcfac1144a430039"
],
"version": "==4.1"
}
},
"develop": {
@@ -303,43 +329,6 @@
],
"version": "==2018.10.15"
},
"cffi": {
"hashes": [
"sha256:151b7eefd035c56b2b2e1eb9963c90c6302dc15fbd8c1c0a83a163ff2c7d7743",
"sha256:1553d1e99f035ace1c0544050622b7bc963374a00c467edafac50ad7bd276aef",
"sha256:1b0493c091a1898f1136e3f4f991a784437fac3673780ff9de3bcf46c80b6b50",
"sha256:2ba8a45822b7aee805ab49abfe7eec16b90587f7f26df20c71dd89e45a97076f",
"sha256:3bb6bd7266598f318063e584378b8e27c67de998a43362e8fce664c54ee52d30",
"sha256:3c85641778460581c42924384f5e68076d724ceac0f267d66c757f7535069c93",
"sha256:3eb6434197633b7748cea30bf0ba9f66727cdce45117a712b29a443943733257",
"sha256:495c5c2d43bf6cebe0178eb3e88f9c4aa48d8934aa6e3cddb865c058da76756b",
"sha256:4c91af6e967c2015729d3e69c2e51d92f9898c330d6a851bf8f121236f3defd3",
"sha256:57b2533356cb2d8fac1555815929f7f5f14d68ac77b085d2326b571310f34f6e",
"sha256:770f3782b31f50b68627e22f91cb182c48c47c02eb405fd689472aa7b7aa16dc",
"sha256:79f9b6f7c46ae1f8ded75f68cf8ad50e5729ed4d590c74840471fc2823457d04",
"sha256:7a33145e04d44ce95bcd71e522b478d282ad0eafaf34fe1ec5bbd73e662f22b6",
"sha256:857959354ae3a6fa3da6651b966d13b0a8bed6bbc87a0de7b38a549db1d2a359",
"sha256:87f37fe5130574ff76c17cab61e7d2538a16f843bb7bca8ebbc4b12de3078596",
"sha256:95d5251e4b5ca00061f9d9f3d6fe537247e145a8524ae9fd30a2f8fbce993b5b",
"sha256:9d1d3e63a4afdc29bd76ce6aa9d58c771cd1599fbba8cf5057e7860b203710dd",
"sha256:a36c5c154f9d42ec176e6e620cb0dd275744aa1d804786a71ac37dc3661a5e95",
"sha256:a6a5cb8809091ec9ac03edde9304b3ad82ad4466333432b16d78ef40e0cce0d5",
"sha256:ae5e35a2c189d397b91034642cb0eab0e346f776ec2eb44a49a459e6615d6e2e",
"sha256:b0f7d4a3df8f06cf49f9f121bead236e328074de6449866515cea4907bbc63d6",
"sha256:b75110fb114fa366b29a027d0c9be3709579602ae111ff61674d28c93606acca",
"sha256:ba5e697569f84b13640c9e193170e89c13c6244c24400fc57e88724ef610cd31",
"sha256:be2a9b390f77fd7676d80bc3cdc4f8edb940d8c198ed2d8c0be1319018c778e1",
"sha256:ca1bd81f40adc59011f58159e4aa6445fc585a32bb8ac9badf7a2c1aa23822f2",
"sha256:d5d8555d9bfc3f02385c1c37e9f998e2011f0db4f90e250e5bc0c0a85a813085",
"sha256:e55e22ac0a30023426564b1059b035973ec82186ddddbac867078435801c7801",
"sha256:e90f17980e6ab0f3c2f3730e56d1fe9bcba1891eeea58966e89d352492cc74f4",
"sha256:ecbb7b01409e9b782df5ded849c178a0aa7c906cf8c5a67368047daab282b184",
"sha256:ed01918d545a38998bfa5902c7c00e0fee90e957ce036a4000a88e3fe2264917",
"sha256:edabd457cd23a02965166026fd9bfd196f4324fe6032e866d0f3bd0301cd486f",
"sha256:fdf1c1dc5bafc32bc5d08b054f94d659422b05aba244d6be4ddc1c72d9aa70fb"
],
"version": "==1.11.5"
},
"chardet": {
"hashes": [
"sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae",
@@ -354,39 +343,6 @@
],
"version": "==7.0"
},
"cmarkgfm": {
"hashes": [
"sha256:0186dccca79483e3405217993b83b914ba4559fe9a8396efc4eea56561b74061",
"sha256:1a625afc6f62da428df96ec325dc30866cc5781520cbd904ff4ec44cf018171c",
"sha256:207b7673ff4e177374c572feeae0e4ef33be620ec9171c08fd22e2b796e03e3d",
"sha256:275905bb371a99285c74931700db3f0c078e7603bed383e8cf1a09f3ee05a3de",
"sha256:50098f1c4950722521f0671e54139e0edc1837d63c990cf0f3d2c49607bb51a2",
"sha256:50ed116d0b60a07df0dc7b180c28569064b9d37d1578d4c9021cff04d725cb63",
"sha256:61a72def110eed903cd1848245897bcb80d295cd9d13944d4f9f30cba5b76655",
"sha256:64186fb75d973a06df0e6ea12879533b71f6e7ba1ab01ffee7fc3e7534758889",
"sha256:665303d34d7f14f10d7b0651082f25ebf7107f29ef3d699490cac16cdc0fc8ce",
"sha256:70b18f843aec58e4e64aadce48a897fe7c50426718b7753aaee399e72df64190",
"sha256:761ee7b04d1caee2931344ac6bfebf37102ffb203b136b676b0a71a3f0ea3c87",
"sha256:811527e9b7280b136734ed6cb6845e5fbccaeaa132ddf45f0246cbe544016957",
"sha256:987b0e157f70c72a84f3c2f9ef2d7ab0f26c08f2bf326c12c087ff9eebcb3ff5",
"sha256:9fc6a2183d0a9b0974ec7cdcdad42bd78a3be674cc3e65f87dd694419b3b0ab7",
"sha256:a3d17ee4ae739fe16f7501a52255c2e287ac817cfd88565b9859f70520afffea",
"sha256:ba5b5488719c0f2ced0aa1986376f7baff1a1653a8eb5fdfcf3f84c7ce46ef8d",
"sha256:c573ea89dd95d41b6d8cf36799c34b6d5b1eac4aed0212dee0f0a11fb7b01e8f",
"sha256:c5f1b9e8592d2c448c44e6bc0d91224b16ea5f8293908b1561de1f6d2d0658b1",
"sha256:cbe581456357d8f0674d6a590b1aaf46c11d01dd0a23af147a51a798c3818034",
"sha256:cf219bec69e601fe27e3974b7307d2f06082ab385d42752738ad2eb630a47d65",
"sha256:cf5014eb214d814a83a7a47407272d5db10b719dbeaf4d3cfe5969309d0fcf4b",
"sha256:d08bad67fa18f7e8ff738c090628ee0cbf0505d74a991c848d6d04abfe67b697",
"sha256:d6f716d7b1182bf35862b5065112f933f43dd1aa4f8097c9bcfb246f71528a34",
"sha256:e08e479102627641c7cb4ece421c6ed4124820b1758765db32201136762282d9",
"sha256:e20ac21418af0298437d29599f7851915497ce9f2866bc8e86b084d8911ee061",
"sha256:e25f53c37e319241b9a412382140dffac98ca756ba8f360ac7ab5e30cad9670a",
"sha256:e8932bddf159064f04e946fbb64693753488de21586f20e840b3be51745c8c09",
"sha256:f20900f16377f2109783ae9348d34bc80530808439591c3d3df73d5c7ef1a00c"
],
"version": "==0.4.2"
},
"colorama": {
"hashes": [
"sha256:a3d89af5db9e9806a779a50296b5fdb466e281147c2c235e8225ecc6dbf7bbf3",
@@ -437,11 +393,11 @@
},
"flake8": {
"hashes": [
"sha256:7253265f7abd8b313e3892944044a365e3f4ac3fcdcfb4298f55ee9ddf188ba0",
"sha256:c7841163e2b576d435799169b78703ad6ac1bbb0f199994fc05f700b2a90ea37"
"sha256:6a35f5b8761f45c5513e3405f110a86bea57982c3b75b766ce7b65217abe1670",
"sha256:c01f8a3963b3571a8e6bd7a4063359aff90749e160778e03817cd9b71c9e07d2"
],
"index": "pypi",
"version": "==3.5.0"
"version": "==3.6.0"
},
"flask": {
"hashes": [
@@ -453,9 +409,9 @@
},
"future": {
"hashes": [
"sha256:e39ced1ab767b5936646cedba8bcce582398233d6a627067d4c6a454c90cfedb"
"sha256:eb6d4df04f1fb538c99f69c9a28b255d1ee4e825d479b9c62fc38c0cf38065a4"
],
"version": "==0.16.0"
"version": "==0.17.0"
},
"idna": {
"hashes": [
@@ -492,10 +448,10 @@
},
"marshmallow": {
"hashes": [
"sha256:82b201ad767eb54de371c08cb1db6ca4ad2a728fa41b831e3781bf944815eb38",
"sha256:c250f37ac0e249a8287394a60d91f6240b674642ad999e66cd09463dbccd1d4f"
"sha256:5e0053c86e3abaa72a03bbe0021ec97270c13fd6400b682eb1aeaf24b871bc8a",
"sha256:81884e930c1db72d8b8e3d8d2d090f2f43427e5c11c37f703b29879980491ab6"
],
"version": "==3.0.0b18"
"version": "==3.0.0b19"
},
"mccabe": {
"hashes": [
@@ -542,23 +498,17 @@
},
"pycodestyle": {
"hashes": [
"sha256:682256a5b318149ca0d2a9185d365d8864a768a28db66a84a2ea946bcc426766",
"sha256:6c4245ade1edfad79c3446fadfc96b0de2759662dc29d07d80a6f27ad1ca6ba9"
"sha256:cbc619d09254895b0d12c2c691e237b2e91e9b2ecf5e84c26b35400f93dcfb83",
"sha256:cbfca99bd594a10f674d0cd97a3d802a1fdef635d4361e1a2658de47ed261e3a"
],
"version": "==2.3.1"
},
"pycparser": {
"hashes": [
"sha256:a988718abfad80b6b157acce7bf130a30876d27603738ac39f140993246b25b3"
],
"version": "==2.19"
"version": "==2.4.0"
},
"pyflakes": {
"hashes": [
"sha256:08bd6a50edf8cffa9fa09a463063c425ecaaf10d1eb0335a7e8b1401aef89e6f",
"sha256:8d616a382f243dbf19b54743f280b80198be0bca3a5396f1d2e1fca6223e8805"
"sha256:9a7662ec724d0120012f6e29d6248ae3727d821bba522a0e6b356eff19126a49",
"sha256:f661252913bc1dbe7fcfcbf0af0db3f42ab65aabd1a6ca68fe5d466bace94dae"
],
"version": "==1.6.0"
"version": "==2.0.0"
},
"pygments": {
"hashes": [
@@ -576,11 +526,11 @@
},
"pytest": {
"hashes": [
"sha256:10e59f84267370ab20cec9305bafe7505ba4d6b93ecbf66a1cce86193ed511d5",
"sha256:8c827e7d4816dfe13e9329c8226aef8e6e75d65b939bc74fda894143b6d1df59"
"sha256:212be78a6fa5352c392738a49b18f74ae9aeec1040f47c81cadbfd8d1233c310",
"sha256:6f6c1efc8d0ccc21f8f6c34d8330baca883cf109b66b3df954b0a117e5528fb4"
],
"index": "pypi",
"version": "==3.9.1"
"version": "==3.9.2"
},
"pytest-cov": {
"hashes": [
@@ -592,17 +542,17 @@
},
"pytz": {
"hashes": [
"sha256:a061aa0a9e06881eb8b3b2b43f05b9439d6583c206d0a6c340ff72a7b6669053",
"sha256:ffb9ef1de172603304d9d2819af6f5ece76f2e85ec10692a524dd876e72bf277"
"sha256:642253af8eae734d1509fc6ac9c1aee5e5b69d76392660889979b9870610a46b",
"sha256:91e3ccf2c344ffaa6defba1ce7f38f97026943f675b7703f44789768e4cb0ece"
],
"version": "==2018.5"
"version": "==2018.6"
},
"readme-renderer": {
"hashes": [
"sha256:237ca8705ffea849870de41101dba41543561da05c0ae45b2f1c547efa9843d2",
"sha256:f75049a3a7afa57165551e030dd8f9882ebf688b9600535a3f7e23596651875d"
"sha256:219a02f5359b6631f5ab952f6906c6c105bdd8bc4bf19c1ec5ee8bd9ea2dc1eb",
"sha256:f8f122ad9fd6d138337531379575a01a0b6ca70aedca78f094cb833da38c8c0c"
],
"version": "==22.0"
"version": "==23.0"
},
"requests": {
"hashes": [
@@ -656,10 +606,10 @@
},
"tqdm": {
"hashes": [
"sha256:a0be569511161220ff709a5b60d0890d47921f746f1c737a11d965e1b29e7b2e",
"sha256:e293e6d7a7f41a529a27f8d6624ab11544ccbfe82a205af6fad102545099fc21"
"sha256:3c4d4a5a41ef162dd61f1edb86b0e1c7859054ab656b2e7c7b77e7fbf6d9f392",
"sha256:5b4d5549984503050883bc126280b386f5f4ca87e6c023c5d015655ad75bdebb"
],
"version": "==4.27.0"
"version": "==4.28.1"
},
"twine": {
"hashes": [
+5 -101
View File
@@ -7,24 +7,10 @@
[![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)
[![](https://github.com/kennethreitz/responder/raw/master/ext/small.jpg)](http://python-responder.org/)
[![](https://farm2.staticflickr.com/1959/43750081370_a4e20752de_o_d.png)](http://python-responder.org/)
The Python world certainly doesn't need more web frameworks. But, it does need more creativity, so I thought I'd spread some [Hacktoberfest](https://hacktoberfest.digitalocean.com/) spirit around, bring some of my ideas to the table, and see what I could come up with.
```python
import responder
api = responder.API()
@api.route("/{greeting}")
async def greet_world(req, resp, *, greeting):
resp.text = f"{greeting}, world!"
if __name__ == '__main__':
api.run()
```
That `async` declaration is optional. [View documentation](http://python-responder.org).
Powered by [Starlette](https://www.starlette.io/). That `async` declaration is optional. [View documentation](http://python-responder.org).
This gets you a ASGI app, with a production static files server pre-installed, jinja2 templating (without additional imports), and a production webserver based on uvloop, serving up requests with gzip compression automatically.
@@ -41,81 +27,7 @@ This gets you a ASGI app, with a production static files server pre-installed, j
## More Examples
Class-based views (and setting some headers and stuff):
```python
@api.route("/{greeting}")
class GreetingResource:
def on_request(req, resp, *, greeting): # or on_get...
resp.text = f"{greeting}, world!"
resp.headers.update({'X-Life': '42'})
resp.status_code = api.status_codes.HTTP_416
```
Render a template, with arguments:
```python
@api.route("/{greeting}")
def greet_world(req, resp, *, greeting):
resp.content = api.template("index.html", greeting=greeting)
```
The `api` instance is available as an object during template rendering.
Here, you can spawn off a background thread to run any function, out-of-request:
```python
@api.route("/")
def hello(req, resp):
@api.background.task
def sleep(s=10):
time.sleep(s)
print("slept!")
sleep()
resp.content = "processing"
```
And even serve a GraphQL API:
```python
import graphene
class Query(graphene.ObjectType):
hello = graphene.String(name=graphene.String(default_value="stranger"))
def resolve_hello(self, info, name):
return "Hello " + name
api.add_route("/graph", graphene.Schema(query=Query))
```
We can then send a query to our service:
```pycon
>>> requests = api.session()
>>> r = requests.get("http://;/graph", params={"query": "{ hello }"})
>>> r.json()
{'data': {'hello': 'Hello stranger'}}
```
Or, request YAML back:
```pycon
>>> r = requests.get("http://;/graph", params={"query": "{ hello(name:\"john\") }"}, headers={"Accept": "application/x-yaml"})
>>> print(r.text)
data: {hello: Hello john}
```
Want HSTS?
```
api = responder.API(enable_hsts=True)
```
Boom.
See [the documentation's feature tour](http://python-responder.org/en/latest/tour.html) for more details on features available in Responder.
# Installing Responder
@@ -123,7 +35,7 @@ Boom.
Install the latest release:
$ pipenv install responder
$ pipenv install responder --pre
✨🍰✨
@@ -154,15 +66,7 @@ The primary concept here is to bring the niceties that are brought forth from bo
- A production static file server is built-in.
- Uvicorn built-in as a production web server. I would have chosen Gunicorn, but it doesn't run on Windows. Plus, Uvicorn serves well to protect against slowloris attacks, making nginx unnecessary in production.
- GraphQL support, via Graphene. The goal here is to have any GraphQL query exposable at any route, magically.
## Future Ideas
- Cookie-based sessions are currently an afterthought, as this is an API framework, but websites are APIs too.
- If frontend websites are supported, provide an official way to run webpack.
# The Goal
The primary goal here is to learn, not to get adoption. Though, who knows how these things will pan out.
- Provide an official way to run webpack.
----------
+1 -1
View File
@@ -30,7 +30,7 @@ The basics::
Install Responder::
$ pipenv install responder
$ pipenv install responder --pre
...
Write out an ``api.py``::
+5 -15
View File
@@ -21,9 +21,6 @@ A familiar HTTP Service Framework
.. |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.
.. code:: python
import responder
@@ -37,9 +34,9 @@ spread some `Hacktoberfest <https://hacktoberfest.digitalocean.com/>`_ spirit ar
if __name__ == '__main__':
api.run()
That ``async`` declaration is optional.
Powered by `Starlette <https://www.starlette.io/>`_. That ``async`` declaration is optional.
This gets you a ASGI app, with a production static files server
This gets you a ASGI app, with a production static files server (WhiteNoise)
pre-installed, jinja2 templating (without additional imports), and a
production webserver based on uvloop, serving up requests with gzip
compression automatically.
@@ -55,7 +52,7 @@ Features
- Mutable response object, passed into each view. No need to return anything.
- Background tasks, spawned off in a ``ThreadPoolExecutor``.
- GraphQL (with *GraphiQL*) support!
- OpenAPI schema generation.
- OpenAPI schema generation, with interactive documentation!
- Single-page webapp support!
Testimonials
@@ -105,6 +102,7 @@ User Guides
quickstart
tour
deployment
testing
api
@@ -113,7 +111,7 @@ Installing Responder
.. code-block:: shell
$ pipenv install responder
$ pipenv install responder --pre
✨🍰✨
Only **Python 3.6+** is supported.
@@ -143,14 +141,6 @@ Ideas
- GraphQL support, via Graphene. The goal here is to have any GraphQL query exposable at any route, magically.
Future Ideas
------------
- Cookie-based sessions are currently an afterthought, as this is an API framework, but websites are APIs too.
- If frontend websites are supported, provide an official way to run webpack.
Indices and tables
==================
+2 -2
View File
@@ -34,7 +34,7 @@ Next, we can run our web service easily, with ``api.run()``::
This will spin up a production web server on port ``5042``, ready for incoming HTTP requests.
Note: you can pass ``port=5000`` if you want to customize the port. The ``PORT`` environment variable for established web service providers (e.g. Heroku) will automatically be honored.
Note: you can pass ``port=5000`` if you want to customize the port. The ``PORT`` environment variable for established web service providers (e.g. Heroku) will automatically be honored and will set the listening address to ``0.0.0.0`` automatically (also configurable through the ``address`` keyword argument).
Accept Route Arguments
@@ -90,7 +90,7 @@ If you want to set a response header, like ``X-Pizza: 42``, simply modify the ``
@api.route("/pizza")
def pizza_pizza(req, resp):
resp.headers['X-Pizza'] = 42
resp.headers['X-Pizza'] = '42'
That's it!
+81
View File
@@ -0,0 +1,81 @@
Building and Testing with Responder
===================================
Responder comes with a first-class, well supported test client for your ASGI web services: **Requests**.
Here, we'll go over the basics of setting up a proper Python package and adding testing to it.
The Basics
----------
Your repository should look like this::
Pipfile Pipfile.lock api.py test_api.py
``$ cat api.py``::
import responder
api = responder.API()
@api.route("/")
def hello_world(req, resp):
resp.text = "hello, world!"
if __name__ == "__main__":
api.run()
``$ cat Pipfile``::
[[source]]
url = "https://pypi.org/simple"
verify_ssl = true
name = "pypi"
[packages]
responder = "*"
[dev-packages]
pytest = "*"
[requires]
python_version = "3.7"
[pipenv]
allow_prereleases = true
Writing Tests
-------------
``$ cat test_api.py``::
import pytest
import api as service
@pytest.fixture
def api():
return service.api
def test_hello_world(api):
r = api.requests.get("/")
assert r.text == "hello, world!"
``$ pytest``::
...
========================== 1 passed in 0.10 seconds ==========================
(Optional) Proper Python Package
--------------------------------
Optionally, you can not rely on relative imports, and instead install your api as a proper package. This requires:
1. A `proper setup.py <https://github.com/kennethreitz/setup.py>`_ file.
2. ``$ pipenv install -e . --dev``
This will allow you to only specify your dependencies once: in ``setup.py``. ``$ pipenv lock`` will automatically lock your transitive dependencies (e.g. Responder), even if it's not specified in the ``Pipfile``.
This will ensure that your application gets installed in every developer's environment, using Pipenv.
+68 -22
View File
@@ -9,7 +9,7 @@ Class-based views (and setting some headers and stuff)::
@api.route("/{greeting}")
class GreetingResource:
def on_request(req, resp, *, greeting): # or on_get...
def on_request(self, 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
@@ -43,30 +43,16 @@ Serve a GraphQL API::
hello = graphene.String(name=graphene.String(default_value="stranger"))
def resolve_hello(self, info, name):
return "Hello " + name
return f"Hello {name}"
api.add_route("/graph", graphene.Schema(query=Query))
schema = graphene.Schema(query=Query)
view = responder.ext.GraphQLView(api=api, schema=schema)
api.add_route("/graph", view)
Visiting the endpoint will render a *GraphiQL* instance, in the browser.
Built-in Testing Client (Requests)
----------------------------------
We can then send a query to our service::
>>> requests = api.session()
>>> r = requests.get("http://;/graph", params={"query": "{ hello }"})
>>> r.json()
{'data': {'hello': 'Hello stranger'}}
Or, request YAML back::
>>> r = requests.get("http://;/graph", params={"query": "{ hello(name:\"john\") }"}, headers={"Accept": "application/x-yaml"})
>>> print(r.text)
data: {hello: Hello john}
OpenAPI Schema Support
----------------------
@@ -121,6 +107,15 @@ Responder comes with built-in support for OpenAPI / marshmallow::
tags: []
Interactive Documentation
-------------------------
Responder can automatically supply API Documentation for you. Using the example above::
api = responder.API(title="Web Service", version="1.0", openapi="3.0", docs_route="/docs")
This will make ``/docs`` render interactive documentation for your API.
Mount a WSGI App (e.g. Flask)
-----------------------------
@@ -169,13 +164,42 @@ Responder has built-in support for cookie-based sessions. To enable cookie-based
A cookie called ``Responder-Session`` will be set, which contains all the data in ``resp.session``. It is signed, for verification purposes.
**Note**: if you are using this in production, you should pass the ``secret_key`` argument to ``API(...)``.
You can easily read a Request's session data, that can be trusted to have originated from the API::
>>> req.session
{'username': 'kennethreitz'}
**Note**: if you are using this in production, you should pass the ``secret_key`` argument to ``API(...)``::
api = responder.API(secret_key=os.environ['SECRET_KEY'])
Using Requests Test Client
--------------------------
Responder comes with a first-class, well supported test client for your ASGI web services: **Requests**.
Here's an example of a test (written with pytest)::
import myapi
@pytest.fixture
def api():
return myapi.api
def test_response(api):
hello = "hello, world!"
@api.route('/some-url')
def some_view(req, resp):
resp.text = hello
r = api.requests.get(url=api.url_for(some_view))
assert r.text == hello
HSTS (Redirect to HTTPS)
------------------------
Want HSTS?
Want HSTS (to redirect all traffic to HTTPS)?
::
@@ -183,3 +207,25 @@ Want HSTS?
Boom.
CORS
----
Want `CORS <https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS/>`_ ?
::
api = responder.API(cors=True)
The default parameters used by **Responder** are restrictive by default, so you'll need to explicitly enable particular origins, methods, or headers, in order for browsers to be permitted to use them in a Cross-Domain context.
In order to set custom parameters, you need to set the ``cors_params`` argument of ``api``, a dictionary containing the following entries:
* ``allow_origins`` - A list of origins that should be permitted to make cross-origin requests. eg. ``['https://example.org', 'https://www.example.org']``. You can use ``['*']`` to allow any origin.
* ``allow_origin_regex`` - A regex string to match against origins that should be permitted to make cross-origin requests. eg. ``'https://.*\.example\.org'``.
* ``allow_methods`` - A list of HTTP methods that should be allowed for cross-origin requests. Defaults to `['GET']`. You can use ``['*']`` to allow all standard methods.
* ``allow_headers`` - A list of HTTP request headers that should be supported for cross-origin requests. Defaults to ``[]``. You can use ``['*']`` to allow all headers. The ``Accept``, ``Accept-Language``, ``Content-Language`` and ``Content-Type`` headers are always allowed for CORS requests.
* ``allow_credentials`` - Indicate that cookies should be supported for cross-origin requests. Defaults to ``False``.
* ``expose_headers`` - Indicate any response headers that should be made accessible to the browser. Defaults to ``[]``.
* ``max_age`` - Sets a maximum time in seconds for browsers to cache CORS responses. Defaults to ``60``.
+1
View File
@@ -1 +1,2 @@
from .core import *
from . import ext
+1 -1
View File
@@ -1 +1 @@
__version__ = "0.1.3"
__version__ = "1.0.4"
+265 -146
View File
@@ -1,37 +1,48 @@
import os
import json
from functools import partial
import os
from pathlib import Path
import uvicorn
import asyncio
import jinja2
import apistar
import itsdangerous
from graphql_server import encode_execution_results, json_encode, default_format_error
import jinja2
import uvicorn
import yaml
from apispec import APISpec, yaml_utils
from apispec.ext.marshmallow import MarshmallowPlugin
from asgiref.wsgi import WsgiToAsgi
from starlette.debug import DebugMiddleware
from starlette.exceptions import ExceptionMiddleware
from starlette.lifespan import LifespanHandler
from starlette.middleware.cors import CORSMiddleware
from starlette.middleware.gzip import GZipMiddleware
from starlette.middleware.httpsredirect import HTTPSRedirectMiddleware
from starlette.routing import Router
from starlette.staticfiles import StaticFiles
from starlette.testclient import TestClient
from starlette.middleware.gzip import GZipMiddleware
from starlette.middleware.httpsredirect import HTTPSRedirectMiddleware
from apispec import APISpec
from apispec.ext.marshmallow import MarshmallowPlugin
from apispec import yaml_utils
from asgiref.wsgi import WsgiToAsgi
from starlette.websockets import WebSocket
from whitenoise import WhiteNoise
from . import models
from . import status_codes
from .routes import Route
from .formats import get_formats
from . import models, status_codes
from .background import BackgroundQueue
from .formats import get_formats
from .routes import Route
from .statics import (
DEFAULT_API_THEME,
DEFAULT_CORS_PARAMS,
DEFAULT_SECRET_KEY,
DEFAULT_SESSION_COOKIE,
)
from .templates import GRAPHIQL
# TODO: consider moving status codes here
class API:
"""The primary web-service class.
:param static_dir: The directory to use for static files. Will be created for you if it doesn't already exist.
:param templates_dir: The directory to use for templates. Will be created for you if it doesn't already exist.
:param auto_escape: If ``True``, HTML and XML templates will automatically be escaped.
:param enable_hsts: If ``True``, send all responses to HTTPS URLs.
"""
@@ -40,6 +51,7 @@ class API:
def __init__(
self,
*,
debug=False,
title=None,
version=None,
openapi=None,
@@ -47,9 +59,13 @@ class API:
static_dir="static",
static_route="/static",
templates_dir="templates",
secret_key="NOTASECRET",
auto_escape=True,
secret_key=DEFAULT_SECRET_KEY,
enable_hsts=False,
docs_route=None,
cors=False,
):
self.secret_key = secret_key
self.title = title
self.version = version
@@ -61,18 +77,32 @@ class API:
os.path.abspath(os.path.dirname(__file__) + "/templates")
)
self.routes = {}
self.docs_theme = DEFAULT_API_THEME
self.docs_route = docs_route
self.schemas = {}
self.session_cookie = DEFAULT_SESSION_COOKIE
self.hsts_enabled = enable_hsts
self.static_files = StaticFiles(directory=str(self.static_dir))
self.apps = {self.static_route: self.static_files}
self.formats = get_formats()
self.cors = cors
self.cors_params = DEFAULT_CORS_PARAMS
# Make the static/templates directory if they don't exist.
for _dir in (self.static_dir, self.templates_dir):
os.makedirs(_dir, exist_ok=True)
self.whitenoise = WhiteNoise(application=self._default_wsgi_app)
self.whitenoise.add_files(str(self.static_dir))
self.whitenoise.add_files(
(
Path(apistar.__file__).parent / "themes" / self.docs_theme / "static"
).resolve()
)
self.apps = {}
self.mount(self.static_route, self.whitenoise)
self.formats = get_formats()
# Cached requests session.
self._session = None
self.background = BackgroundQueue()
@@ -80,11 +110,39 @@ class API:
if self.openapi_version:
self.add_route(openapi_route, self.schema_response)
if self.docs_route:
self.add_route(self.docs_route, self.docs_response)
self.default_endpoint = None
self.app = self.dispatch
self.add_middleware(GZipMiddleware)
if debug:
self.add_middleware(DebugMiddleware)
if self.hsts_enabled:
self.add_middleware(HTTPSRedirectMiddleware)
self.lifespan_handler = LifespanHandler()
if self.cors:
self.add_middleware(CORSMiddleware, **self.cors_params)
self.add_middleware(ExceptionMiddleware, debug=debug)
# Jinja enviroment
self.jinja_env = jinja2.Environment(
loader=jinja2.FileSystemLoader(
[str(self.templates_dir), str(self.built_in_templates_dir)],
followlinks=True,
),
autoescape=jinja2.select_autoescape(["html", "xml"] if auto_escape else []),
)
self.jinja_values_base = {"api": self} # Give reference to self.
self.requests = (
self.session()
) #: A Requests session that is connected to the ASGI app.
@staticmethod
def _default_wsgi_app(*args, **kwargs):
pass
@property
def _apispec(self):
@@ -115,6 +173,9 @@ class API:
self.app = middleware_cls(self.app, **middleware_config)
def __call__(self, scope):
if scope["type"] == "lifespan":
return self.lifespan_handler(scope)
path = scope["path"]
root_path = scope.get("root_path", "")
@@ -137,7 +198,9 @@ class API:
nonlocal scope, self
req = models.Request(scope, receive=receive, api=self)
resp = await self._dispatch_request(req)
resp = await self._dispatch_request(
req, scope=scope, send=send, receive=receive
)
await resp(receive, send)
return asgi
@@ -178,7 +241,6 @@ class API:
return route
def _prepare_cookies(self, resp):
# print(resp.cookies)
if resp.cookies:
header = " ".join([f"{k}={v}" for k, v in resp.cookies.items()])
resp.headers["Set-Cookie"] = header
@@ -191,72 +253,114 @@ class API:
if resp.session:
data = self._signer.sign(json.dumps(resp.session).encode("utf-8"))
resp.cookies["Responder-Session"] = data.decode("utf-8")
resp.cookies[self.session_cookie] = data.decode("utf-8")
async def _dispatch_request(self, req):
@staticmethod
def no_response(req, resp, **params):
pass
async def _dispatch_request(self, req, **options):
# Set formats on Request object.
req.formats = self.formats
# Get the route.
route = self.path_matches_route(req.url.path)
resp = models.Response(req=req, formats=self.formats)
route = self.routes.get(route)
# Create the response object.
cont = False
if route:
try:
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 as e:
try:
view = self.routes[route].endpoint(**params)
except TypeError:
view = self.routes[route].endpoint
if route.uses_websocket:
resp = WebSocket(**options)
if self.routes[route].is_graphql:
await self.graphql_response(req, resp, schema=view)
else:
# WSGI App.
# try:
# return view(
# environ=req._environ, start_response=req._start_response
# )
# except TypeError:
# pass
pass
else:
resp = models.Response(req=req, formats=self.formats)
params = route.incoming_matches(req.url.path)
if route.is_function:
try:
try:
# Run the view.
r = route.endpoint(req, resp, **params)
# If it's async, await it.
if hasattr(r, "cr_running"):
await r
except TypeError as e:
cont = True
except Exception:
self.default_response(req, resp, error=True)
raise
elif route.is_class_based or cont:
try:
view = route.endpoint(**params)
except TypeError:
try:
view = route.endpoint()
except TypeError:
view = route.endpoint
# Run on_request first.
try:
r = getattr(view, "on_request")(req, resp, **params)
# Run the view.
r = getattr(view, "on_request", self.no_response)(
req, resp, **params
)
# If it's async, await it.
if hasattr(r, "send"):
await r
except AttributeError:
pass
except Exception:
self.default_response(req, resp, error=True)
raise
# Then on_get.
# Then run on_method.
method = req.method
try:
r = getattr(view, f"on_{method}")(req, resp, **params)
# Run the view.
r = getattr(view, f"on_{method}", self.no_response)(
req, resp, **params
)
# If it's async, await it.
if hasattr(r, "send"):
await r
except AttributeError:
pass
except Exception as e:
self.default_response(req, resp, error=True)
else:
self.default_response(req, resp)
resp = models.Response(req=req, formats=self.formats)
self.default_response(req, resp, notfound=True)
self.default_response(req, resp)
self._prepare_session(resp)
self._prepare_cookies(resp)
return resp
def add_event_handler(self, event_type, handler):
"""Adds an event handler to the API.
:param event_type: A string in ("startup", "shutdown")
:param handler: The function to run. Can be either a function or a coroutine.
"""
self.lifespan_handler.add_event_handler(event_type, handler)
def add_route(
self, route, endpoint=None, *, default=False, static=False, check_existing=True
self,
route,
endpoint=None,
*,
default=False,
static=False,
check_existing=True,
websocket=False,
):
"""Add a route to the API.
"""Adds a route to the API.
:param route: A string representation of the route.
:param endpoint: The endpoint for the route -- can be a callable, a class, or graphene schema (GraphQL).
:param endpoint: The endpoint for the route -- can be a callable, or a class.
:param default: If ``True``, all unknown requests will route to this view.
:param static: If ``True``, and no endpoint was passed, render "static/index.html", and it will become a default route.
:param check_existing: If ``True``, an AssertionError will be raised, if the route is already defined.
@@ -270,21 +374,33 @@ class API:
if default:
self.default_endpoint = endpoint
self.routes[route] = Route(route, endpoint)
# TODO: A better datastructer or sort it once the app is loaded
self.routes[route] = Route(route, endpoint, websocket=websocket)
# TODO: A better data structure or sort it once the app is loaded
self.routes = dict(
sorted(self.routes.items(), key=lambda item: item[1]._weight())
)
def default_response(self, req, resp):
if self.default_endpoint:
def default_response(self, req, resp, notfound=False, error=False):
if resp.status_code is None:
resp.status_code = 200
if self.default_endpoint and notfound:
self.default_endpoint(req, resp)
else:
resp.status_code = status_codes.HTTP_404
resp.text = "Not found."
if notfound:
resp.status_code = status_codes.HTTP_404
resp.text = "Not found."
if error:
resp.status_code = status_codes.HTTP_500
resp.text = "Application error."
def docs_response(self, req, resp):
resp.text = self.docs
def static_response(self, req, resp):
index = (self.static_dir / "index.html").resolve()
resp.content = ""
if os.path.exists(index):
with open(index, "r") as f:
resp.text = f.read()
@@ -305,55 +421,38 @@ class API:
:param status_code: an `API.status_codes` attribute, or an integer, representing the HTTP status code of the redirect.
"""
assert resp.status_code.is_300(status_code)
# assert resp.status_code.is_300(status_code)
resp.status_code = status_code
if set_text:
resp.text = f"Redirecting to: {location}"
resp.headers.update({"Location": location})
@staticmethod
async def _resolve_graphql_query(req):
# TODO: Get variables and operation_name from form data, params, request text?
def on_event(self, event_type: str, **args):
"""Decorator for registering functions or coroutines to run at certain events
Supported events: startup, cleanup, shutdown, tick
if "json" in req.mimetype:
json_media = await req.media("json")
return json_media["query"], json_media.get("variables"), json_media.get("operationName")
Usage::
# Support query/q in form data.
# Form data is awaiting https://github.com/encode/starlette/pull/102
# if "query" in req.media("form"):
# return req.media("form")["query"], None, None
# if "q" in req.media("form"):
# return req.media("form")["q"], None, None
@api.on_event('startup')
async def open_database_connection_pool():
...
# Support query/q in params.
if "query" in req.params:
return req.params["query"], None, None
if "q" in req.params:
return req.params["q"], None, None
@api.on_event('tick', seconds=10)
async def do_stuff():
...
# Otherwise, the request text is used (typical).
# TODO: Make some assertions about content-type here.
return req.text, None, None
@api.on_event('cleanup')
async def close_database_connection_pool():
...
async def graphql_response(self, req, resp, schema):
show_graphiql = req.method == "get" and req.accepts("text/html")
"""
if show_graphiql:
resp.content = self.template_string(GRAPHIQL, endpoint=req.url.path)
return
def decorator(func):
self.add_event_handler(event_type, func, **args)
return func
query, variables, operation_name = await self._resolve_graphql_query(req)
result = schema.execute(query, variables=variables, operation_name=operation_name)
result, status_code = encode_execution_results(
[result],
is_batch=False,
format_error=default_format_error,
encode=partial(json_encode, pretty=False),
)
resp.media = json.loads(result)
return (query, result, status_code)
return decorator
def route(self, route, **options):
"""Decorator for creating new routes around function and class definitions.
@@ -390,79 +489,96 @@ class API:
self._session = TestClient(self)
return self._session
def url_for(self, endpoint, testing=False, **params):
def _route_for(self, endpoint):
for (route, route_object) in self.routes.items():
if route_object.endpoint == endpoint:
return route_object
elif route_object.endpoint_name == endpoint:
return route_object
def url_for(self, endpoint, **params):
# TODO: Absolute_url
"""Given an endpoint, returns a rendered URL for its route.
:param view: The route endpoint you're searching for.
:param params: Data to pass into the URL generator (for parameterized URLs).
"""
for (route, route_object) in self.routes.items():
if route_object.endpoint == endpoint:
return route_object.url(testing=testing, **params)
route_object = self._route_for(endpoint)
if route_object:
return route_object.url(**params)
raise ValueError
def static_url(self, asset):
"""Given a static asset, return its URL path."""
return f"{self.static_route}/{str(asset)}"
def template(self, name_, auto_escape=True, **values):
"""Renders the given `jinja2 <http://jinja.pocoo.org/docs/>`_ template, with provided values supplied.
@property
def docs(self):
Note: The current ``api`` instance is always passed into the view.
loader = jinja2.PrefixLoader(
{
self.docs_theme: jinja2.PackageLoader(
"apistar", os.path.join("themes", self.docs_theme, "templates")
)
}
)
env = jinja2.Environment(autoescape=True, loader=loader)
document = apistar.document.Document()
document.content = yaml.safe_load(self.openapi)
:param name_: The filename of the jinja2 template, in ``templates_dir``.
:param auto_escape: If ``True``, HTML and XML will automatically be escaped.
:param values: Data to pass into the template.
"""
# Give reference to self.
values.update(api=self)
template = env.get_template("/".join([self.docs_theme, "index.html"]))
env = jinja2.Environment(
loader=jinja2.FileSystemLoader(
[str(self.templates_dir), str(self.built_in_templates_dir)],
followlinks=True,
),
autoescape=jinja2.select_autoescape(["html", "xml"] if auto_escape else []),
def static_url(asset):
return f"{self.static_route}/{asset}"
# return asset
return template.render(
document=document,
langs=["javascript", "python"],
code_style=None,
static_url=static_url,
schema_url="/schema.yml",
)
template = env.get_template(name_)
return template.render(**values)
def template(self, name_, **values):
"""Renders the given `jinja2 <http://jinja.pocoo.org/docs/>`_ template, with provided values supplied.
def template_string(self, s, auto_escape=True, **values):
"""Renders the given `jinja2 <http://jinja.pocoo.org/docs/>`_ template string, with provided values supplied.
Note: The current ``api`` instance is by default passed into the view. This is set in the dict ``api.jinja_values_base``.
Note: The current ``api`` instance is always passed into the view.
:param s: The template to use.
:param auto_escape: If ``True``, HTML and XML will automatically be escaped.
:param name_: The filename of the jinja2 template, in ``templates_dir``.
:param values: Data to pass into the template.
"""
# Give reference to self.
values.update(api=self)
# Prepopulate values with base
values = {**self.jinja_values_base, **values}
if auto_escape:
env = jinja2.Environment(
loader=jinja2.BaseLoader,
autoescape=jinja2.select_autoescape(["html", "xml"]),
)
else:
env = jinja2.Environment(
loader=jinja2.BaseLoader, autoescape=jinja2.select_autoescape([])
)
template = env.from_string(s)
template = self.jinja_env.get_template(name_)
return template.render(**values)
def run(self, address=None, port=None, **options):
def template_string(self, s_, **values):
"""Renders the given `jinja2 <http://jinja.pocoo.org/docs/>`_ template string, with provided values supplied.
Note: The current ``api`` instance is by default passed into the view. This is set in the dict ``api.jinja_values_base``.
:param s_: The template to use.
:param values: Data to pass into the template.
"""
# Prepopulate values with base
values = {**self.jinja_values_base, **values}
template = self.jinja_env.from_string(s_)
return template.render(**values)
def run(self, address=None, port=None, debug=False, **options):
"""Runs the application with uvicorn. If the ``PORT`` environment
variable is set, requests will be served on that port automatically to all
known hosts.
:param address: The address to bind to.
:param port: The port to bind to. If none is provided, one will be selected at random.
:param debug: Run uvicorn server in debug mode.
:param options: Additional keyword arguments to send to ``uvicorn.run()``.
"""
if "PORT" in os.environ:
if address is None:
address = "0.0.0.0"
@@ -473,4 +589,7 @@ class API:
if port is None:
port = 5042
uvicorn.run(self, host=address, port=port, **options)
def spawn():
uvicorn.run(self, host=address, port=port, debug=debug, **options)
spawn()
+8
View File
@@ -1,3 +1,4 @@
import traceback
import multiprocessing
import concurrent.futures
@@ -20,8 +21,15 @@ class BackgroundQueue:
return f
def task(self, f):
def on_future_done(fs):
try:
fs.result()
except:
traceback.print_exc()
def do_task(*args, **kwargs):
result = self.run(f, *args, **kwargs)
result.add_done_callback(on_future_done)
return result
return do_task
+43
View File
@@ -0,0 +1,43 @@
"""Responder.
Usage:
responder
responder run [--build] [--debug] <module>
responder build
responder --version
Options:
-h --help Show this screen.
-v --version Show version.
"""
import os
import docopt
from .__version__ import __version__
def cli():
args = docopt.docopt(
__doc__, argv=None, help=True, version=__version__, options_first=False
)
module = args["<module>"]
build = args["build"] or args["--build"]
run = args["run"]
if build:
os.system("npm run build")
if run:
split_module = module.split(":")
if len(split_module) > 1:
module = split_module[0]
prop = split_module[1]
else:
prop = "api"
app = __import__(module)
getattr(app, prop).run()
+1
View File
@@ -1,2 +1,3 @@
from .api import API
from .models import Request, Response
from .cli import cli
+1
View File
@@ -0,0 +1 @@
from .graphql import GraphQLView
+64
View File
@@ -0,0 +1,64 @@
import json
from functools import partial
from graphql_server import default_format_error, encode_execution_results, json_encode
from ..templates import GRAPHIQL
class GraphQLView:
def __init__(self, *, api, schema):
self.api = api
self.schema = schema
@staticmethod
async def _resolve_graphql_query(req):
# TODO: Get variables and operation_name from form data, params, request text?
if "json" in req.mimetype:
json_media = await req.media("json")
return (
json_media["query"],
json_media.get("variables"),
json_media.get("operationName"),
)
# Support query/q in form data.
# Form data is awaiting https://github.com/encode/starlette/pull/102
# if "query" in req.media("form"):
# return req.media("form")["query"], None, None
# if "q" in req.media("form"):
# return req.media("form")["q"], None, None
# Support query/q in params.
if "query" in req.params:
return req.params["query"], None, None
if "q" in req.params:
return req.params["q"], None, None
# Otherwise, the request text is used (typical).
# TODO: Make some assertions about content-type here.
return req.text, None, None
async def graphql_response(self, req, resp, schema):
show_graphiql = req.method == "get" and req.accepts("text/html")
if show_graphiql:
resp.content = self.api.template_string(GRAPHIQL, endpoint=req.url.path)
return
query, variables, operation_name = await self._resolve_graphql_query(req)
result = schema.execute(
query, variables=variables, operation_name=operation_name
)
result, status_code = encode_execution_results(
[result],
is_batch=False,
format_error=default_format_error,
encode=partial(json_encode, pretty=False),
)
resp.media = json.loads(result)
return (query, result, status_code)
async def on_request(self, req, resp):
await self.graphql_response(req, resp, self.schema)
+41 -3
View File
@@ -1,10 +1,17 @@
from urllib.parse import parse_qs
import yaml
import json
from parse import findall
from .models import QueryDict
from requests_toolbelt.multipart import decoder
async def format_form(r, encode=False):
if not encode:
return await r._starlette.form()
if encode:
pass
else:
return QueryDict(await r.text)
async def format_yaml(r, encode=False):
@@ -23,5 +30,36 @@ async def format_json(r, encode=False):
return json.loads(await r.content)
async def format_files(r, encode=False):
if encode:
pass
else:
decoded = decoder.MultipartDecoder(await r.content, r.mimetype)
dump = {}
for part in decoded.parts:
header = part.headers[b"Content-Disposition"].decode("utf-8")
filename = None
for section in [h.strip() for h in header.split(";")]:
split = section.split("=")
if len(split) > 1:
key = split[0]
value = split[1]
value = value[1:-1]
if key == "filename":
filename = value
if filename:
dump[filename] = part.content
return dump
def get_formats():
return {"json": format_json, "yaml": format_yaml, "form": format_form}
return {
"json": format_json,
"yaml": format_yaml,
"form": format_form,
"files": format_files,
}
+15 -11
View File
@@ -90,17 +90,18 @@ class QueryDict(dict):
# TODO: add slots
class Request:
__slots__ = ["_starlette", "formats", "_headers", "_encoding", "api"]
__slots__ = ["_starlette", "formats", "_headers", "_encoding", "api", "_content"]
def __init__(self, scope, receive, api=None):
self._starlette = StarletteRequest(scope, receive)
self.formats = None
self._encoding = None
self.api = api
self._content = None
headers = CaseInsensitiveDict()
for header, value in self._starlette.headers.items():
headers[header] = value
for key, value in self._starlette.headers.items():
headers[key] = value
self._headers = headers
@@ -108,7 +109,7 @@ class Request:
def session(self):
"""The session data, in dict form, from the Request."""
if "Responder-Session" in self.cookies:
data = self.cookies["Responder-Session"]
data = self.cookies[self.api.session_cookie]
data = self.api._signer.unsign(data)
return json.loads(data)
return {}
@@ -141,9 +142,8 @@ class Request:
def cookies(self):
"""The cookies sent in the Request, as a dictionary."""
cookies = RequestsCookieJar()
cookie_header = self.headers.get("cookie", "")
cookie_header = self.headers.get("Cookie", "")
# if cookie_header:
bc = SimpleCookie(cookie_header)
for k, v in bc.items():
cookies[k] = v
@@ -180,12 +180,14 @@ class Request:
@property
async def content(self):
"""The Request body, as bytes. Must be awaited."""
return await self._starlette.body()
if not self._content:
self._content = await self._starlette.body()
return self._content
@property
async def text(self):
"""The Request body, as unicode. Must be awaited."""
return (await self._starlette.body()).decode(await self.encoding)
return (await self.content).decode(await self.encoding)
@property
async def declared_encoding(self):
@@ -242,7 +244,7 @@ class Response:
def __init__(self, req, *, formats):
self.req = req
self.status_code = HTTP_200 #: The HTTP Status Code to use for the Response.
self.status_code = None #: 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 = DEFAULT_ENCODING
@@ -251,10 +253,12 @@ class Response:
) #: A Python object that will be content-negotiated and sent back to the client. Typically, in JSON formatting.
self.headers = (
{}
) #: A Python dictionary of {Key: value}, representing the headers of the response.
) #: A Python dictionary of ``{key: value}``, representing the headers of the response.
self.formats = formats
self.cookies = {} #: The cookies set in the Response, as a dictionary
self.session = req.session.copy() #: """The *cookie-based* session data, in dict form, to add to the Response."""
self.session = (
req.session.copy()
) #: The cookie-based session data, in dict form, to add to the Response.
@property
async def body(self):
+18 -11
View File
@@ -15,9 +15,10 @@ def memoize(f):
class Route:
_param_pattern = re.compile(r"{([^{}]*)}")
def __init__(self, route, endpoint):
def __init__(self, route, endpoint, *, websocket=False):
self.route = route
self.endpoint = endpoint
self.uses_websocket = websocket
self._memo = {}
def __repr__(self):
@@ -31,6 +32,10 @@ class Route:
# Strings.
return self.does_match(other)
@property
def endpoint_name(self):
return self.endpoint.__name__
@property
def description(self):
return self.endpoint.__doc__
@@ -52,18 +57,20 @@ class Route:
results = parse(self.route, s)
return results.named if results else {}
def url(self, testing=False, **params):
url = self.route.format(**params)
if testing:
url = f"http://;{url}"
return url
def url(self, **params):
return self.route.format(**params)
def _weight(self):
params = set(self._param_pattern.findall(self.route))
params_count = -len(params) or 0
return params_count != 0, params_count
params_count = len(params)
return params_count != 0, -params_count
@property
def is_graphql(self):
return hasattr(self.endpoint, "get_graphql_type")
def is_class_based(self):
return hasattr(self.endpoint, "__class__")
@property
def is_function(self):
code = hasattr(self.endpoint, "__code__")
kwdefaults = hasattr(self.endpoint, "__kwdefaults__")
return all((callable(self.endpoint), code, kwdefaults))
+13
View File
@@ -1 +1,14 @@
DEFAULT_ENCODING = "utf-8"
DEFAULT_API_THEME = "swaggerui"
DEFAULT_SESSION_COOKIE = "Responder-Session"
DEFAULT_SECRET_KEY = "NOTASECRET"
DEFAULT_CORS_PARAMS = {
"allow_origins": (),
"allow_methods": ("GET",),
"allow_headers": (),
"allow_credentials": False,
"allow_origin_regex": None,
"expose_headers": (),
"max_age": 600,
}
+6 -4
View File
@@ -38,7 +38,11 @@ required = [
"apispec>=1.0.0b1",
"marshmallow",
"asgiref",
"whitenoise",
"docopt",
"itsdangerous",
"requests-toolbelt",
"apistar",
]
@@ -118,9 +122,7 @@ setup(
author_email="me@kennethreitz.org",
url="https://github.com/kennethreitz/responder",
packages=find_packages(exclude=["tests"]),
# entry_points={
# "console_scripts": ["responder=responder:cli"]
# },
entry_points={"console_scripts": ["responder=responder.cli:cli"]},
package_data={
# "": ["LICENSE", "NOTICES"],
# "pipenv.vendor.requests": ["*.pem"],
@@ -143,7 +145,7 @@ setup(
include_package_data=True,
license="Apache 2.0",
classifiers=[
"License :: OSI Approved :: MIT License",
"License :: OSI Approved :: Apache Software License",
"Programming Language :: Python",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.6",
-1
View File
@@ -1 +0,0 @@
lorem
-3
View File
@@ -1,3 +0,0 @@
this is a test
{{ api.static_url('test') }}
+3 -2
View File
@@ -2,6 +2,8 @@ import graphene
import responder
from pathlib import Path
import pytest
import multiprocessing
import concurrent.futures
@pytest.fixture
@@ -21,7 +23,7 @@ def api():
@pytest.fixture
def session(api):
return api.session()
return api.requests
@pytest.fixture
@@ -44,7 +46,6 @@ def flask():
return app
@pytest.fixture
def schema():
class Query(graphene.ObjectType):
-12
View File
@@ -22,15 +22,3 @@ def test_bytes_encoding(api, session):
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)
+188 -57
View File
@@ -1,6 +1,12 @@
import concurrent
import pytest
import yaml
import responder
import requests
import io
from starlette.responses import PlainTextResponse
def test_api_basic_route(api):
@@ -64,7 +70,7 @@ def test_class_based_view_registration(api):
def test_class_based_view_parameters(api):
@api.route("/{greeting}")
class Greeting:
def on_request(req, resp, *, greeting):
def on_request(self, req, resp, *, greeting):
resp.text = f"{greeting}, world!"
assert api.session().get("http://;/Hello").ok
@@ -72,16 +78,17 @@ def test_class_based_view_parameters(api):
def test_requests_session(api):
assert api.session()
assert api.requests
def test_requests_session_works(api, session, url):
def test_requests_session_works(api, url):
TEXT = "spiral out"
@api.route("/")
def hello(req, resp):
resp.text = TEXT
assert session.get(url("/")).text == TEXT
assert api.requests.get(url("/")).text == TEXT
def test_status_code(api):
@@ -90,7 +97,7 @@ def test_status_code(api):
resp.text = "keep going"
resp.status_code = responder.status_codes.HTTP_416
assert api.session().get("http://;/").status_code == responder.status_codes.HTTP_416
assert api.requests.get("http://;/").status_code == responder.status_codes.HTTP_416
def test_json_media(api):
@@ -100,7 +107,7 @@ def test_json_media(api):
def media(req, resp):
resp.media = dump
r = api.session().get("http://;/")
r = api.requests.get("http://;/")
assert "json" in r.headers["Content-Type"]
assert r.json() == dump
@@ -113,47 +120,47 @@ def test_yaml_media(api):
def media(req, resp):
resp.media = dump
r = api.session().get("http://;/", headers={"Accept": "yaml"})
r = api.requests.get("http://;/", headers={"Accept": "yaml"})
assert "yaml" in r.headers["Content-Type"]
assert yaml.load(r.content) == dump
def test_graphql_schema_query_querying(api, schema):
api.add_route("/", schema)
api.add_route("/", responder.ext.GraphQLView(schema=schema, api=api))
r = api.session().get("http://;/?q={ hello }", headers={"Accept": "json"})
r = api.requests.get("http://;/?q={ hello }", headers={"Accept": "json"})
assert r.json() == {"data": {"hello": "Hello stranger"}}
def test_argumented_routing(api, session):
def test_argumented_routing(api):
@api.route("/{name}")
def hello(req, resp, *, name):
resp.text = f"Hello, {name}."
r = session.get(api.url_for(hello, name="sean"))
r = api.requests.get(api.url_for(hello, name="sean"))
assert r.text == "Hello, sean."
def test_mote_argumented_routing(api, session):
def test_mote_argumented_routing(api):
@api.route("/{greeting}/{name}")
def hello(req, resp, *, greeting, name):
resp.text = f"{greeting}, {name}."
r = session.get(api.url_for(hello, greeting="hello", name="lyndsy"))
r = api.requests.get(api.url_for(hello, greeting="hello", name="lyndsy"))
assert r.text == "hello, lyndsy."
def test_request_and_get(api, session):
def test_request_and_get(api):
@api.route("/")
class ThingsResource:
def on_request(self, req, resp):
resp.headers.update({"DEATH": "666"})
def on_get(self, request, resp):
def on_get(self, req, resp):
resp.headers.update({"LIFE": "42"})
r = session.get(api.url_for(ThingsResource))
r = api.requests.get(api.url_for(ThingsResource))
assert "DEATH" in r.headers
assert "LIFE" in r.headers
@@ -164,58 +171,58 @@ def test_class_based_view_status_code(api):
def on_request(self, req, resp):
resp.status_code = responder.status_codes.HTTP_416
assert api.session().get("http://;/").status_code == responder.status_codes.HTTP_416
assert api.requests.get("http://;/").status_code == responder.status_codes.HTTP_416
def test_query_params(api, url, session):
def test_query_params(api, url):
@api.route("/")
def route(req, resp):
resp.media = {"params": req.params}
r = session.get(api.url_for(route), params={"q": "q"})
r = api.requests.get(api.url_for(route), params={"q": "q"})
assert r.json()["params"] == {"q": "q"}
r = session.get(url("/?q=1&q=2&q=3"))
r = api.requests.get(url("/?q=1&q=2&q=3"))
assert r.json()["params"] == {"q": "3"}
# Requires https://github.com/encode/starlette/pull/102
def test_form_data(api, session):
def test_form_data(api):
@api.route("/")
async def route(req, resp):
resp.media = {"form": await req.media("form")}
dump = {"q": "q"}
r = session.get(api.url_for(route), data=dump)
r = api.requests.get(api.url_for(route), data=dump)
assert r.json()["form"] == dump
def test_async_function(api, session):
def test_async_function(api):
content = "The Emerald Tablet of Hermes"
@api.route("/")
async def route(req, resp):
resp.text = content
r = session.get(api.url_for(route))
r = api.requests.get(api.url_for(route))
assert r.text == content
def test_media_parsing(api, session):
def test_media_parsing(api):
dump = {"hello": "sam"}
@api.route("/")
def route(req, resp):
resp.media = dump
r = session.get(api.url_for(route))
r = api.requests.get(api.url_for(route))
assert r.json() == dump
r = session.get(api.url_for(route), headers={"Accept": "application/x-yaml"})
r = api.requests.get(api.url_for(route), headers={"Accept": "application/x-yaml"})
assert r.text == "{hello: sam}\n"
def test_background(api, session):
def test_background(api):
@api.route("/")
def route(req, resp):
@api.background.task
@@ -227,11 +234,11 @@ def test_background(api, session):
task()
api.text = "ok"
r = session.get(api.url_for(route))
r = api.requests.get(api.url_for(route))
assert r.ok
def test_multiple_routes(api, session):
def test_multiple_routes(api):
@api.route("/1")
def route1(req, resp):
resp.text = "1"
@@ -240,45 +247,45 @@ def test_multiple_routes(api, session):
def route2(req, resp):
resp.text = "2"
r = session.get(api.url_for(route1))
r = api.requests.get(api.url_for(route1))
assert r.text == "1"
r = session.get(api.url_for(route2))
r = api.requests.get(api.url_for(route2))
assert r.text == "2"
def test_graphql_schema_json_query(api, schema):
api.add_route("/", schema)
api.add_route("/", responder.ext.GraphQLView(schema=schema, api=api))
r = api.session().post("http://;/", json={"query": "{ hello }"})
r = api.requests.post("http://;/", json={"query": "{ hello }"})
assert r.ok
def test_graphiql(api, schema):
api.add_route("/", schema)
api.add_route("/", responder.ext.GraphQLView(schema=schema, api=api))
r = api.session().get("http://;/", headers={"Accept": "text/html"})
r = api.requests.get("http://;/", headers={"Accept": "text/html"})
assert r.ok
assert "GraphiQL" in r.text
def test_json_uploads(api, session):
def test_json_uploads(api):
@api.route("/")
async def route(req, resp):
resp.media = await req.media()
dump = {"complicated": "times"}
r = session.post(api.url_for(route), json=dump)
r = api.requests.post(api.url_for(route), json=dump)
assert r.json() == dump
def test_yaml_uploads(api, session):
def test_yaml_uploads(api):
@api.route("/")
async def route(req, resp):
resp.media = await req.media()
dump = {"complicated": "times"}
r = session.post(
r = api.requests.post(
api.url_for(route),
data=yaml.dump(dump),
headers={"Content-Type": "application/x-yaml"},
@@ -286,35 +293,39 @@ def test_yaml_uploads(api, session):
assert r.json() == dump
def test_form_uploads(api, session):
def test_form_uploads(api):
@api.route("/")
async def route(req, resp):
resp.media = await req.media()
dump = {"complicated": "times"}
r = session.post(api.url_for(route), data=dump)
r = api.requests.post(api.url_for(route), data=dump)
assert r.json() == dump
def test_json_downloads(api, session):
def test_json_downloads(api):
dump = {"testing": "123"}
@api.route("/")
def route(req, resp):
resp.media = dump
r = session.get(api.url_for(route), headers={"Content-Type": "application/json"})
r = api.requests.get(
api.url_for(route), headers={"Content-Type": "application/json"}
)
assert r.json() == dump
def test_yaml_downloads(api, session):
def test_yaml_downloads(api):
dump = {"testing": "123"}
@api.route("/")
def route(req, resp):
resp.media = dump
r = session.get(api.url_for(route), headers={"Content-Type": "application/x-yaml"})
r = api.requests.get(
api.url_for(route), headers={"Content-Type": "application/x-yaml"}
)
assert yaml.safe_load(r.content) == dump
@@ -342,58 +353,178 @@ def test_schema_generation():
"""
resp.media = PetSchema().dump({"name": "little orange"})
r = api.session().get("http://;/schema.yml")
r = api.requests.get("http://;/schema.yml")
dump = yaml.safe_load(r.content)
assert dump
assert dump["openapi"] == "3.0"
def test_mount_wsgi_app(api, flask, session):
def test_documentation():
import responder
from marshmallow import Schema, fields
api = responder.API(title="Web Service", openapi="3.0", docs_route="/docs")
@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.requests.get("/docs")
assert "html" in r.text
def test_mount_wsgi_app(api, flask):
@api.route("/")
def hello(req, resp):
resp.text = "hello"
api.mount("/flask", flask)
r = session.get("http://;/flask")
r = api.requests.get("http://;/flask")
assert r.ok
def test_async_class_based_views(api, session):
def test_async_class_based_views(api):
@api.route("/")
class Resource:
async def on_post(self, req, resp):
resp.text = await req.text
data = "frame"
r = session.post(api.url_for(Resource), data=data)
r = api.requests.post(api.url_for(Resource), data=data)
assert r.text == data
def test_cookies(api, session):
def test_cookies(api):
@api.route("/")
def cookies(req, resp):
resp.media = {"cookies": req.cookies}
resp.cookies["sent"] = "true"
r = session.get(api.url_for(cookies), cookies={"hello": "universe"})
r = api.requests.get(api.url_for(cookies), cookies={"hello": "universe"})
assert r.json() == {"cookies": {"hello": "universe"}}
assert "sent" in r.cookies
r = session.get(api.url_for(cookies))
r = api.requests.get(api.url_for(cookies))
assert r.json() == {"cookies": {"sent": "true"}}
def test_sessions(api, session):
@pytest.mark.xfail
def test_sessions(api):
@api.route("/")
def view(req, resp):
resp.session["hello"] = "world"
resp.media = resp.session
r = session.get(api.url_for(view))
r = api.requests.get(api.url_for(view))
assert "Responder-Session" in r.cookies
r = session.get(api.url_for(view))
assert r.cookies['Responder-Session'] == '{"hello": "world"}.r3EB04hEEyLYIJaAXCEq3d4YEbs'
r = api.requests.get(api.url_for(view))
assert (
r.cookies["Responder-Session"]
== '{"hello": "world"}.r3EB04hEEyLYIJaAXCEq3d4YEbs'
)
assert r.json() == {"hello": "world"}
def test_template_rendering(api):
@api.route("/")
def view(req, resp):
resp.content = api.template_string("{{ var }}", var="hello")
r = api.requests.get(api.url_for(view))
assert r.text == "hello"
def test_file_uploads(api):
@api.route("/")
async def upload(req, resp):
files = await req.media("files")
files["hello"] = files["hello"].decode("utf-8")
resp.media = {"files": files}
world = io.StringIO("world")
data = {"hello": world}
r = api.requests.post(api.url_for(upload), files=data)
assert r.json() == {"files": {"hello": "world"}}
def test_500(api):
def catcher(request, exc):
return PlainTextResponse("Suppressed error", 500)
api.app.add_exception_handler(ValueError, catcher)
@api.route("/")
def view(req, resp):
raise ValueError
r = api.requests.get(api.url_for(view))
assert not r.ok
assert r.content == b"Suppressed error"
def test_404(api):
r = api.requests.get("/foo")
assert r.status_code == responder.status_codes.HTTP_404
def test_kinda_websockets(api):
@api.route("/ws", websocket=True)
async def websocket(ws):
await ws.accept()
await ws.send_text("Hello via websocket!")
await ws.close()
@pytest.mark.xfail
def test_startup(api, session):
who = [None]
@api.route("/{greeting}")
async def greet_world(req, resp, *, greeting):
resp.text = f"{greeting}, {who[0]}!"
@api.on_event("startup")
async def asd():
who[0] = "world"
print("startup")
@api.on_event("cleanup")
async def asd():
print("cleanup")
pool = concurrent.futures.ThreadPoolExecutor(max_workers=2)
f = pool.submit(api.run)
r = requests.get(f"http://localhost:5042/hello")
assert r.text == "hello, world!"
def test_redirects(api, session):
@api.route("/2")
def two(req, resp):
api.redirect(resp, location="/1")
@api.route("/1")
def one(req, resp):
resp.text = "redirected"
assert session.get("/1").url == "http://testserver/1"