Compare commits

..

136 Commits

Author SHA1 Message Date
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
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
20 changed files with 705 additions and 324 deletions
+34 -2
View File
@@ -1,3 +1,35 @@
# 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 +43,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 +64,7 @@
- Safe load/dump yaml.
# v0.0.4:
- Asyncronous support for data uploads.
- Asynchronous support for data uploads.
- Bug fixes.
# v0.0.3:
Generated
+57 -105
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",
@@ -114,9 +126,10 @@
},
"itsdangerous": {
"hashes": [
"sha256:cbb3fcf8d3e33df861709ecaf89d9e6629cff0a217bc2848f1b41cd30d360519"
"sha256:a7de3201740a857380421ef286166134e10fe58846bcefbc9d6424a69a0b99ec",
"sha256:aca4fc561b7671115a2156f625f2eaa5e0e3527e0adf2870340e7968c0a81f85"
],
"version": "==0.24"
"version": "==1.0.0"
},
"jinja2": {
"hashes": [
@@ -174,6 +187,13 @@
],
"version": "==2.20.0"
},
"requests-toolbelt": {
"hashes": [
"sha256:42c9c170abc2cacb78b8ab23ac957945c7716249206f90874651971a4acff237",
"sha256:f6a531936c6fa4c6cfce1b9c10d5c4f498d16528d2a54a22ca00011205a187b5"
],
"version": "==0.8.0"
},
"responder": {
"editable": true,
"path": "."
@@ -201,9 +221,9 @@
},
"starlette": {
"hashes": [
"sha256:ce5c684fad4edb2967cd491518cd3c2724e420508202c2d48f519ea68dcec9d6"
"sha256:eac0f6cab6b48846a0c1af16615430ae0e7a95f669ee0841a7e2f242d51d8935"
],
"version": "==0.5.4"
"version": "==0.5.5"
},
"urllib3": {
"hashes": [
@@ -214,9 +234,9 @@
},
"uvicorn": {
"hashes": [
"sha256:30096b58325cdca8e547a6f5f4300040d0b8763f573cb1843abfa96f81a49cf8"
"sha256:e2b742fdaa0b52f4aac92fd2c078e7f1f17d11322bb3efb09d341d5c6998b4b5"
],
"version": "==0.3.13"
"version": "==0.3.16"
},
"websockets": {
"hashes": [
@@ -243,6 +263,13 @@
"sha256:f1414e6cbcea8d22843e7eafdfdfae3dd1aba41d1945f6ca66e4806c07c4f454"
],
"version": "==6.0"
},
"whitenoise": {
"hashes": [
"sha256:133a92ff0ab8fb9509f77d4f7d0de493eca19c6fea973f4195d4184f888f2e02",
"sha256:32b57d193478908a48acb66bf73e7a3c18679263e3e64bfebcfac1144a430039"
],
"version": "==4.1"
}
},
"develop": {
@@ -303,43 +330,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 +344,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 +394,11 @@
},
"flake8": {
"hashes": [
"sha256:7253265f7abd8b313e3892944044a365e3f4ac3fcdcfb4298f55ee9ddf188ba0",
"sha256:c7841163e2b576d435799169b78703ad6ac1bbb0f199994fc05f700b2a90ea37"
"sha256:6a35f5b8761f45c5513e3405f110a86bea57982c3b75b766ce7b65217abe1670",
"sha256:c01f8a3963b3571a8e6bd7a4063359aff90749e160778e03817cd9b71c9e07d2"
],
"index": "pypi",
"version": "==3.5.0"
"version": "==3.6.0"
},
"flask": {
"hashes": [
@@ -473,9 +430,10 @@
},
"itsdangerous": {
"hashes": [
"sha256:cbb3fcf8d3e33df861709ecaf89d9e6629cff0a217bc2848f1b41cd30d360519"
"sha256:a7de3201740a857380421ef286166134e10fe58846bcefbc9d6424a69a0b99ec",
"sha256:aca4fc561b7671115a2156f625f2eaa5e0e3527e0adf2870340e7968c0a81f85"
],
"version": "==0.24"
"version": "==1.0.0"
},
"jinja2": {
"hashes": [
@@ -542,23 +500,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 +528,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 +544,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 +608,10 @@
},
"tqdm": {
"hashes": [
"sha256:a0be569511161220ff709a5b60d0890d47921f746f1c737a11d965e1b29e7b2e",
"sha256:e293e6d7a7f41a529a27f8d6624ab11544ccbfe82a205af6fad102545099fc21"
"sha256:3c4d4a5a41ef162dd61f1edb86b0e1c7859054ab656b2e7c7b77e7fbf6d9f392",
"sha256:5b4d5549984503050883bc126280b386f5f4ca87e6c023c5d015655ad75bdebb"
],
"version": "==4.27.0"
"version": "==4.28.1"
},
"twine": {
"hashes": [
+2 -2
View File
@@ -86,7 +86,7 @@ class Query(graphene.ObjectType):
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))
```
@@ -123,7 +123,7 @@ Boom.
Install the latest release:
$ pipenv install responder
$ pipenv install responder --pre
✨🍰✨
+4 -3
View File
@@ -39,7 +39,7 @@ spread some `Hacktoberfest <https://hacktoberfest.digitalocean.com/>`_ spirit ar
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 +55,7 @@ Features
- Mutable response object, passed into each view. No need to return anything.
- Background tasks, spawned off in a ``ThreadPoolExecutor``.
- GraphQL (with *GraphiQL*) support!
- OpenAPI schema generation.
- OpenAPI schema generation, with interactive documentation!
- Single-page webapp support!
Testimonials
@@ -105,6 +105,7 @@ User Guides
quickstart
tour
deployment
testing
api
@@ -113,7 +114,7 @@ Installing Responder
.. code-block:: shell
$ pipenv install responder
$ pipenv install responder --pre
✨🍰✨
Only **Python 3.6+** is supported.
+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.
+42 -21
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,13 @@ 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))
Visiting the endpoint will render a *GraphiQL* instance, in the browser.
Built-in Testing Client (Requests)
----------------------------------
We can then send a query to our service::
>>> requests = api.session()
>>> r = requests.get("http://;/graph", params={"query": "{ hello }"})
>>> r.json()
{'data': {'hello': 'Hello stranger'}}
Or, request YAML back::
>>> r = requests.get("http://;/graph", params={"query": "{ hello(name:\"john\") }"}, headers={"Accept": "application/x-yaml"})
>>> print(r.text)
data: {hello: Hello john}
OpenAPI Schema Support
----------------------
@@ -121,6 +104,15 @@ Responder comes with built-in support for OpenAPI / marshmallow::
tags: []
Interactive Documentation
-------------------------
Responder can automatically supply API Documentation for you. Using the example above::
api = responder.API(title="Web Service", version="1.0", openapi="3.0", docs_route="/docs")
This will make ``/docs`` render interactive documentation for your API.
Mount a WSGI App (e.g. Flask)
-----------------------------
@@ -169,13 +161,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)?
::
+1 -1
View File
@@ -1 +1 @@
__version__ = "0.1.3"
__version__ = "0.3.2"
+211 -93
View File
@@ -4,13 +4,13 @@ from functools import partial
from pathlib import Path
import uvicorn
import asyncio
import apistar
import yaml
import jinja2
import itsdangerous
from graphql_server import encode_execution_results, json_encode, default_format_error
from starlette.routing import Router
from starlette.staticfiles import StaticFiles
from starlette.websockets import WebSocket
from starlette.debug import DebugMiddleware
from starlette.testclient import TestClient
from starlette.middleware.gzip import GZipMiddleware
from starlette.middleware.httpsredirect import HTTPSRedirectMiddleware
@@ -18,6 +18,7 @@ from apispec import APISpec
from apispec.ext.marshmallow import MarshmallowPlugin
from apispec import yaml_utils
from asgiref.wsgi import WsgiToAsgi
from whitenoise import WhiteNoise
from . import models
from . import status_codes
@@ -32,6 +33,7 @@ class API:
: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 +42,7 @@ class API:
def __init__(
self,
*,
debug=False,
title=None,
version=None,
openapi=None,
@@ -47,8 +50,10 @@ class API:
static_dir="static",
static_route="/static",
templates_dir="templates",
auto_escape=True,
secret_key="NOTASECRET",
enable_hsts=False,
docs_route=None,
):
self.secret_key = secret_key
self.title = title
@@ -61,18 +66,33 @@ class API:
os.path.abspath(os.path.dirname(__file__) + "/templates")
)
self.routes = {}
self.docs_theme = "swaggerui"
self.docs_route = docs_route
self.schemas = {}
self.session_cookie = "Responder-Session"
self.hsts_enabled = enable_hsts
self.static_files = StaticFiles(directory=str(self.static_dir))
self.apps = {self.static_route: self.static_files}
self.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)
self.whitenoise = WhiteNoise(
application=self._default_wsgi_app, index_file=True
)
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,12 +100,34 @@ 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)
# 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):
spec = APISpec(
@@ -137,7 +179,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 +222,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,59 +234,85 @@ 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_graphql:
await self.graphql_response(req, resp, schema=route.endpoint)
elif 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)
elif route.is_class_based or cont:
try:
view = route.endpoint(**params)
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 as e:
self.default_response(req, resp, error=True)
# Then on_get.
method = req.method
# Run on_request first.
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)
@@ -251,7 +320,14 @@ class API:
return resp
def add_route(
self, route, endpoint=None, *, default=False, static=False, check_existing=True
self,
route,
endpoint=None,
*,
default=False,
static=False,
check_existing=True,
websocket=False,
):
"""Add a route to the API.
@@ -270,21 +346,40 @@ 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
# Can we remove it ?
try:
if callable(endpoint):
endpoint.is_routed = True
except AttributeError:
pass
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()
@@ -318,7 +413,11 @@ class API:
if "json" in req.mimetype:
json_media = await req.media("json")
return json_media["query"], json_media.get("variables"), json_media.get("operationName")
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
@@ -345,7 +444,9 @@ class API:
return
query, variables, operation_name = await self._resolve_graphql_query(req)
result = schema.execute(query, variables=variables, operation_name=operation_name)
result = schema.execute(
query, variables=variables, operation_name=operation_name
)
result, status_code = encode_execution_results(
[result],
is_batch=False,
@@ -390,79 +491,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 +591,4 @@ class API:
if port is None:
port = 5042
uvicorn.run(self, host=address, port=port, **options)
uvicorn.run(self, host=address, port=port, debug=debug, **options)
+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
+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,
}
+14 -10
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 {}
@@ -143,7 +144,6 @@ class Request:
cookies = RequestsCookieJar()
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):
+22 -9
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,26 @@ 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")
@property
def is_class_based(self):
return hasattr(self.endpoint, "__class__")
@property
def is_function(self):
# TODO: Should we remove is_routed ?
routed = hasattr(self.endpoint, "is_routed")
code = hasattr(self.endpoint, "__code__")
kwdefaults = hasattr(self.endpoint, "__kwdefaults__")
return all((callable(self.endpoint), code, kwdefaults))
+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') }}
+1 -1
View File
@@ -21,7 +21,7 @@ def api():
@pytest.fixture
def session(api):
return api.session()
return api.requests
@pytest.fixture
-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)
+137 -54
View File
@@ -1,6 +1,7 @@
import pytest
import yaml
import responder
import io
def test_api_basic_route(api):
@@ -64,7 +65,7 @@ def test_class_based_view_registration(api):
def test_class_based_view_parameters(api):
@api.route("/{greeting}")
class Greeting:
def on_request(req, resp, *, greeting):
def on_request(self, req, resp, *, greeting):
resp.text = f"{greeting}, world!"
assert api.session().get("http://;/Hello").ok
@@ -72,16 +73,17 @@ def test_class_based_view_parameters(api):
def test_requests_session(api):
assert api.session()
assert api.requests
def test_requests_session_works(api, session, url):
def test_requests_session_works(api, url):
TEXT = "spiral out"
@api.route("/")
def hello(req, resp):
resp.text = TEXT
assert session.get(url("/")).text == TEXT
assert api.requests.get(url("/")).text == TEXT
def test_status_code(api):
@@ -90,7 +92,7 @@ def test_status_code(api):
resp.text = "keep going"
resp.status_code = responder.status_codes.HTTP_416
assert api.session().get("http://;/").status_code == responder.status_codes.HTTP_416
assert api.requests.get("http://;/").status_code == responder.status_codes.HTTP_416
def test_json_media(api):
@@ -100,7 +102,7 @@ def test_json_media(api):
def media(req, resp):
resp.media = dump
r = api.session().get("http://;/")
r = api.requests.get("http://;/")
assert "json" in r.headers["Content-Type"]
assert r.json() == dump
@@ -113,7 +115,7 @@ def test_yaml_media(api):
def media(req, resp):
resp.media = dump
r = api.session().get("http://;/", headers={"Accept": "yaml"})
r = api.requests.get("http://;/", headers={"Accept": "yaml"})
assert "yaml" in r.headers["Content-Type"]
assert yaml.load(r.content) == dump
@@ -122,38 +124,38 @@ def test_yaml_media(api):
def test_graphql_schema_query_querying(api, schema):
api.add_route("/", schema)
r = api.session().get("http://;/?q={ hello }", headers={"Accept": "json"})
r = api.requests.get("http://;/?q={ hello }", headers={"Accept": "json"})
assert r.json() == {"data": {"hello": "Hello stranger"}}
def test_argumented_routing(api, session):
def test_argumented_routing(api):
@api.route("/{name}")
def hello(req, resp, *, name):
resp.text = f"Hello, {name}."
r = session.get(api.url_for(hello, name="sean"))
r = api.requests.get(api.url_for(hello, name="sean"))
assert r.text == "Hello, sean."
def test_mote_argumented_routing(api, session):
def test_mote_argumented_routing(api):
@api.route("/{greeting}/{name}")
def hello(req, resp, *, greeting, name):
resp.text = f"{greeting}, {name}."
r = session.get(api.url_for(hello, greeting="hello", name="lyndsy"))
r = api.requests.get(api.url_for(hello, greeting="hello", name="lyndsy"))
assert r.text == "hello, lyndsy."
def test_request_and_get(api, session):
def test_request_and_get(api):
@api.route("/")
class ThingsResource:
def on_request(self, req, resp):
resp.headers.update({"DEATH": "666"})
def on_get(self, request, resp):
def on_get(self, req, resp):
resp.headers.update({"LIFE": "42"})
r = session.get(api.url_for(ThingsResource))
r = api.requests.get(api.url_for(ThingsResource))
assert "DEATH" in r.headers
assert "LIFE" in r.headers
@@ -164,58 +166,58 @@ def test_class_based_view_status_code(api):
def on_request(self, req, resp):
resp.status_code = responder.status_codes.HTTP_416
assert api.session().get("http://;/").status_code == responder.status_codes.HTTP_416
assert api.requests.get("http://;/").status_code == responder.status_codes.HTTP_416
def test_query_params(api, url, session):
def test_query_params(api, url):
@api.route("/")
def route(req, resp):
resp.media = {"params": req.params}
r = session.get(api.url_for(route), params={"q": "q"})
r = api.requests.get(api.url_for(route), params={"q": "q"})
assert r.json()["params"] == {"q": "q"}
r = session.get(url("/?q=1&q=2&q=3"))
r = api.requests.get(url("/?q=1&q=2&q=3"))
assert r.json()["params"] == {"q": "3"}
# Requires https://github.com/encode/starlette/pull/102
def test_form_data(api, session):
def test_form_data(api):
@api.route("/")
async def route(req, resp):
resp.media = {"form": await req.media("form")}
dump = {"q": "q"}
r = session.get(api.url_for(route), data=dump)
r = api.requests.get(api.url_for(route), data=dump)
assert r.json()["form"] == dump
def test_async_function(api, session):
def test_async_function(api):
content = "The Emerald Tablet of Hermes"
@api.route("/")
async def route(req, resp):
resp.text = content
r = session.get(api.url_for(route))
r = api.requests.get(api.url_for(route))
assert r.text == content
def test_media_parsing(api, session):
def test_media_parsing(api):
dump = {"hello": "sam"}
@api.route("/")
def route(req, resp):
resp.media = dump
r = session.get(api.url_for(route))
r = api.requests.get(api.url_for(route))
assert r.json() == dump
r = session.get(api.url_for(route), headers={"Accept": "application/x-yaml"})
r = api.requests.get(api.url_for(route), headers={"Accept": "application/x-yaml"})
assert r.text == "{hello: sam}\n"
def test_background(api, session):
def test_background(api):
@api.route("/")
def route(req, resp):
@api.background.task
@@ -227,11 +229,11 @@ def test_background(api, session):
task()
api.text = "ok"
r = session.get(api.url_for(route))
r = api.requests.get(api.url_for(route))
assert r.ok
def test_multiple_routes(api, session):
def test_multiple_routes(api):
@api.route("/1")
def route1(req, resp):
resp.text = "1"
@@ -240,45 +242,45 @@ def test_multiple_routes(api, session):
def route2(req, resp):
resp.text = "2"
r = session.get(api.url_for(route1))
r = api.requests.get(api.url_for(route1))
assert r.text == "1"
r = session.get(api.url_for(route2))
r = api.requests.get(api.url_for(route2))
assert r.text == "2"
def test_graphql_schema_json_query(api, schema):
api.add_route("/", schema)
r = api.session().post("http://;/", json={"query": "{ hello }"})
r = api.requests.post("http://;/", json={"query": "{ hello }"})
assert r.ok
def test_graphiql(api, schema):
api.add_route("/", schema)
r = api.session().get("http://;/", headers={"Accept": "text/html"})
r = api.requests.get("http://;/", headers={"Accept": "text/html"})
assert r.ok
assert "GraphiQL" in r.text
def test_json_uploads(api, session):
def test_json_uploads(api):
@api.route("/")
async def route(req, resp):
resp.media = await req.media()
dump = {"complicated": "times"}
r = session.post(api.url_for(route), json=dump)
r = api.requests.post(api.url_for(route), json=dump)
assert r.json() == dump
def test_yaml_uploads(api, session):
def test_yaml_uploads(api):
@api.route("/")
async def route(req, resp):
resp.media = await req.media()
dump = {"complicated": "times"}
r = session.post(
r = api.requests.post(
api.url_for(route),
data=yaml.dump(dump),
headers={"Content-Type": "application/x-yaml"},
@@ -286,35 +288,39 @@ def test_yaml_uploads(api, session):
assert r.json() == dump
def test_form_uploads(api, session):
def test_form_uploads(api):
@api.route("/")
async def route(req, resp):
resp.media = await req.media()
dump = {"complicated": "times"}
r = session.post(api.url_for(route), data=dump)
r = api.requests.post(api.url_for(route), data=dump)
assert r.json() == dump
def test_json_downloads(api, session):
def test_json_downloads(api):
dump = {"testing": "123"}
@api.route("/")
def route(req, resp):
resp.media = dump
r = session.get(api.url_for(route), headers={"Content-Type": "application/json"})
r = api.requests.get(
api.url_for(route), headers={"Content-Type": "application/json"}
)
assert r.json() == dump
def test_yaml_downloads(api, session):
def test_yaml_downloads(api):
dump = {"testing": "123"}
@api.route("/")
def route(req, resp):
resp.media = dump
r = session.get(api.url_for(route), headers={"Content-Type": "application/x-yaml"})
r = api.requests.get(
api.url_for(route), headers={"Content-Type": "application/x-yaml"}
)
assert yaml.safe_load(r.content) == dump
@@ -342,58 +348,135 @@ 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):
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"}.lJVWJULPqR9kdao_oT4pUglV281bxHfGvcKQ7XF8qNqaiIZlRcMvqKNdA1-d5z7DycAx5eqmzJZoqWPP759-Cw'
)
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):
@api.route("/")
def view(req, resp):
raise ValueError
r = api.requests.get(api.url_for(view))
assert not r.ok
def test_404(api):
r = api.requests.get("/foo")
assert r.status_code == responder.status_codes.HTTP_404
def test_kinda_websockets(api):
@api.route("/ws", websocket=True)
async def websocket(ws):
await ws.accept()
await ws.send_text("Hello via websocket!")
await ws.close()