Compare commits

...

51 Commits

Author SHA1 Message Date
Timo Furrer f3cdc99b29 release: 1.3.0 2019-02-22 10:36:57 +00:00
Taoufik eb70376438 v1.3.0 changelog 2019-02-22 10:34:19 +00:00
Timo Furrer dae1a4fa35 Add template for 1.3.0 CHANGELOG 2019-02-22 10:16:17 +00:00
Taoufik 2ad351197e Merge pull request #304 from taoufik07/content_type
Add resp.html property and make resp.text a property
2019-02-22 10:12:46 +00:00
Taoufik 3d9235c4bc Merge pull request #293 from taoufik07/multiple_cookies_and_directives
Multiple cookies and directives
2019-02-22 10:12:18 +00:00
taoufik07 2cd5596def lint 2019-02-22 10:40:27 +01:00
Taoufik d4191030d9 Merge branch 'master' into multiple_cookies_and_directives 2019-02-22 10:33:53 +01:00
Taoufik 447630a051 Merge branch 'master' into content_type 2019-02-22 10:30:38 +01:00
Timo Furrer f7b53a4895 Merge pull request #308 from taoufik07/feature/stream
Support stream response
2019-02-22 10:15:42 +01:00
taoufik07 21896aa171 Support stream response 2019-02-22 03:58:09 +01:00
Taoufik e8a15697d2 Merge pull request #306 from iancleary/242_modify_swagger_strings
implemented rest of OpenAPI Info Object
2019-02-22 02:05:23 +00:00
icleary 0030993631 api OpenAPI params match /docs display order, updated tour docs and docs test 2019-02-21 18:35:19 -07:00
taoufik07 13ba2f72f5 Update docs 2019-02-22 02:00:12 +01:00
taoufik07 9f39917895 Update docs and README 2019-02-22 01:12:25 +01:00
taoufik07 1b0859fdbb Only encode text 2019-02-22 00:59:17 +01:00
taoufik07 acd1561b1b Add tests 2019-02-22 00:01:49 +01:00
icleary 9f2182949d snake case for terms_of_service, is not None for if statements 2019-02-21 08:35:53 -07:00
icleary 6e5b3a4bf9 ran black on changed file 2019-02-20 22:38:57 -07:00
icleary d2ec323888 edited docstring to remove ->type 2019-02-20 22:29:49 -07:00
icleary 8b9645cf2d implemented rest of OpenAPI Info Object 2019-02-20 22:04:58 -07:00
Taoufik 4ecfef0ddf Merge branch 'master' into content_type 2019-02-21 02:57:06 +01:00
taoufik07 84fb7bd622 resp.html 2019-02-21 02:55:20 +01:00
taoufik07 0b261252e1 Add resp.html property and make resp.text a property 2019-02-21 02:49:47 +01:00
Taoufik d60b5ee39e Merge pull request #297 from taoufik07/whitnoise_notfound 2019-02-21 00:19:02 +00:00
taoufik07 e2f887ec5f Add tests 2019-02-21 00:51:31 +01:00
taoufik07 97da6a6694 Format static_route 2019-02-21 00:46:27 +01:00
taoufik07 c0e9a6778d Set static_response status if not found 2019-02-20 23:20:37 +01:00
Taoufik 5c327a2e0b Merge pull request #302 from taoufik07/lock_update
Pin starlette to 0.10.* and update the lock file
2019-02-20 17:52:00 +00:00
taoufik07 5ed45634cb Pin starlette to 0.10.* and update the lock file 2019-02-20 18:45:53 +01:00
Taoufik a50a373e84 Merge pull request #301 from taoufik07/starlette-0.10.5 2019-02-19 21:59:27 +00:00
taoufik07 86705d0c2f Pin starlette to 0.10.5 2019-02-19 22:35:23 +01:00
taoufik07 b9581444f9 Return 404 when static file is not found 2019-02-19 14:47:31 +01:00
Taoufik 2a60b094b8 Merge branch 'master' into multiple_cookies_and_directives 2019-02-19 13:49:21 +01:00
taoufik07 1ec567cabf Cleanup and black 2019-02-19 13:47:59 +01:00
Taoufik 4fd898b239 Merge pull request #296 from taoufik07/travis_black_check 2019-02-19 12:43:01 +00:00
taoufik07 03d6b72a00 Add linting checks to travis 2019-02-19 13:34:02 +01:00
taoufik07 4d0382d580 Lint 2019-02-19 13:33:17 +01:00
taoufik07 a0dd7481ec Add tests 2019-02-17 19:39:36 +01:00
taoufik07 1c91480b0c Multiple cookies and directives 2019-02-17 19:35:34 +01:00
Taoufik 85e5ec0a9a Merge pull request #288 from taoufik07/starlette>=0.10.2
Update Pipfile.lock and starlette==0.10.*
2019-02-14 13:49:57 +00:00
Taoufik 4ac04b0abc Merge pull request #290 from josegonzalez/patch-1 2019-02-14 13:49:07 +00:00
Taoufik d7e64a6e39 Merge pull request #289 from taoufik07/patch-20 2019-02-14 12:39:40 +00:00
Taoufik 17d526632e Merge pull request #285 from taoufik07/patch-19
Await for background task
2019-02-14 12:34:21 +00:00
Jose Diaz-Gonzalez 43da481df7 fix: always respect the configured session_cookie
The `session_cookie` was refactored to be set via a hardcoded `DEFAULT_SESSION_COOKIE` static variable, and this change will allow future cookie changes to trickle to the Request object.
2019-02-13 10:54:07 -05:00
Taoufik 5f5402833b Typos 2019-02-13 15:09:48 +00:00
taoufik07 d59c4333f2 starlette 0.10.* 2019-02-13 16:03:20 +01:00
taoufik07 49114f36ce Update starlette>=0.10.2 2019-02-13 12:14:13 +01:00
taoufik07 b2039d99f3 Update Pipfile.lock 2019-02-13 12:13:16 +01:00
Taoufik 94fd86fee0 Await for background task 2019-02-11 08:45:48 +00:00
kennethreitz d70fdd3301 todo 2019-02-09 06:45:25 -06:00
kennethreitz 05b75efb43 version 2019-01-12 07:07:50 -05:00
18 changed files with 558 additions and 157 deletions
+1
View File
@@ -10,3 +10,4 @@ install:
# command to run the dependencies
script:
- "pytest"
- "black responder tests setup.py --check"
+13
View File
@@ -1,3 +1,16 @@
# v1.3.0
## Fixed
- Multiple cookies.
- Whitenoise returns not found.
- Other bugfixes.
## Added
- Stream support via `resp.stream`.
- Cookie directives via `resp.set_cookie`.
- Add `resp.html` to send HTML.
- Other improvements.
# v1.1.1
- Run sync views in a threadpoolexecutor.
Generated
+125 -70
View File
@@ -32,10 +32,10 @@
},
"apispec": {
"hashes": [
"sha256:8072aaba54cb430787c3662512d5c9fe521eae1ec0b6d7d05b129814b6b48f69",
"sha256:93a6046bf692e8e4398101d447fffcf148b9dbed66d886073e05b491cd6835fd"
"sha256:57a7b81fd19fff0663a7e5ffd196eaea79b5364151ed2b65533be36d55e0229c",
"sha256:b45def53903516e67e8584ee41f34bc60c3e4acace6892b69340293ea20f3caa"
],
"version": "==1.0.0b6"
"version": "==1.0.0"
},
"apistar": {
"hashes": [
@@ -117,6 +117,12 @@
],
"version": "==0.8.1"
},
"httptools": {
"hashes": [
"sha256:04c7703bbef0e8ca28b09811547352b8c7c20549eab70dc24e536bb24fd2b7c5"
],
"version": "==0.0.11"
},
"idna": {
"hashes": [
"sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407",
@@ -173,16 +179,16 @@
},
"marshmallow": {
"hashes": [
"sha256:3133fb98afd627dcd8c06e4705f0ecea1b28003a53820d0266fa6c0ff7cf215c",
"sha256:6489e72ea75a30cb07686ce01e24bf65fc7f42edf429153a70abb9e38e56ef52"
"sha256:7f9aba737a59dd3c6c6c79846f1df2fbfe036c17f038bbc2c83911b7304a90e1",
"sha256:b41cc52fe0491bdb8aa3e2186ca57d478d9ef69dba87fe37d309aa8a08fd30dd"
],
"version": "==3.0.0rc2"
"version": "==3.0.0rc4"
},
"parse": {
"hashes": [
"sha256:9dd6048ea212cd032a342f9f6aa2b7bc222f7407c7e37bdc2777fecd36897437"
"sha256:870dd675c1ee8951db3e29b81ebe44fd131e3eb8c03a79483a58ea574f3145c2"
],
"version": "==1.9.0"
"version": "==1.11.1"
},
"promise": {
"hashes": [
@@ -216,10 +222,10 @@
},
"requests-toolbelt": {
"hashes": [
"sha256:42c9c170abc2cacb78b8ab23ac957945c7716249206f90874651971a4acff237",
"sha256:f6a531936c6fa4c6cfce1b9c10d5c4f498d16528d2a54a22ca00011205a187b5"
"sha256:380606e1d10dc85c3bd47bf5a6095f815ec007be7a8b69c878507068df059e6f",
"sha256:968089d4584ad4ad7c171454f0a5c6dac23971e9472521ea3b6d49d610aa6fc0"
],
"version": "==0.8.0"
"version": "==0.9.1"
},
"responder": {
"editable": true,
@@ -234,10 +240,10 @@
},
"rx": {
"hashes": [
"sha256:13a1d8d9e252625c173dc795471e614eadfe1cf40ffc684e08b8fff0d9748c23",
"sha256:7357592bc7e881a95e0c2013b73326f704953301ab551fbc8133a6fadab84105"
"sha256:7e6919a3159d6c6cee266fdeeb48783118bab20177e38fa5d6a8ebd24a132f72",
"sha256:e7ccf18bb8e76f8a44557febd5c149c2ad36df19442f678be529c710b0553d85"
],
"version": "==1.6.1"
"version": "==3.0.0a2"
},
"six": {
"hashes": [
@@ -248,9 +254,9 @@
},
"starlette": {
"hashes": [
"sha256:01f04283b49a8cb0c8921baa90dbafe47e953f0a265f6ebb38176038e4bd9bf8"
"sha256:8bc2e41f7638290379ae91450413796f92d6c97b88a6b754f3c1a7f8bc7a07d6"
],
"version": "==0.9.9"
"version": "==0.10.7"
},
"urllib3": {
"hashes": [
@@ -261,9 +267,24 @@
},
"uvicorn": {
"hashes": [
"sha256:ab570ef3b088ddf30a8a2bb97f624c4eabe246301c2f21e38a48c82bfa3d8f52"
"sha256:f27889a332ee5c55b4841b11b2392d00dac079f39063fabc1e13e18ada3eb7ba"
],
"version": "==0.3.24"
"version": "==0.4.5"
},
"uvloop": {
"hashes": [
"sha256:198fe0c196056930ec6c4a0a878e531a66d15467ca7c74a875aa90271f0c6e3f",
"sha256:1c175f47d34b84e33c0e312f4987c927ea004afc3a5f05d2f0f610d71d0e4c89",
"sha256:1c47f197be8f0a3c651dd20be1e1bd43268186246f246d4e86c91e95a89e4865",
"sha256:3fd4943570d20e8cd4d9f0a3190ebd5cf040e5610b685e05c878128a11f7ad14",
"sha256:435e232869923fd2248e4ca0ad73e24a5b4debf40bed9dcde133cfe1bef98a7a",
"sha256:9cfdb966ae804c46b96c92207dfd2174935ffc70e706e42e1c94c60d16dbe860",
"sha256:a585781443eeb2edb858f8c08c503aac237a5f1bebf0c84ea8340cc337afa408",
"sha256:b296493e033846e46488a6aa227a75c790091f5ee5456ec637bb0badad1e8851",
"sha256:c684047c6cf6d697ba37872fb1b4489012ea91f3f802c8fbb9c367c4902e88dc",
"sha256:da5a59d8812188b57b5783c7fb78891d14dd1050b6259680e0dbd4253d7d0f64"
],
"version": "==0.12.1"
},
"websockets": {
"hashes": [
@@ -316,10 +337,10 @@
},
"atomicwrites": {
"hashes": [
"sha256:0312ad34fcad8fac3704d441f7b317e50af620823353ec657a53e981f92920c0",
"sha256:ec9ae8adaae229e4f8446952d204a3e4b5fdd2d099f9be3aaf556120135fb3ee"
"sha256:03472c30eb2c5d1ba9227e4c2ca66ab8287fbfbbda3888aa93dc2e28fc6811b4",
"sha256:75a9445bac02d8d058d5e1fe689654ba5a6556a1dfd8ce6ec55a0ed79866cfa6"
],
"version": "==1.2.1"
"version": "==1.3.0"
},
"attrs": {
"hashes": [
@@ -371,14 +392,6 @@
],
"version": "==7.0"
},
"colorama": {
"hashes": [
"sha256:05eed71e2e327246ad6b38c540c4a3117230b19679b875190486ddd2d721422d",
"sha256:f8ac84de7840f5b9c4e3347b3c1eaa50f7e49c2b07596221daec5edaabbd7c48"
],
"markers": "sys_platform == 'win32'",
"version": "==0.4.1"
},
"coverage": {
"hashes": [
"sha256:029c69deaeeeae1b15bc6c59f0ffa28aa8473721c614a23f2c2976dec245cd12",
@@ -421,13 +434,20 @@
],
"version": "==0.14"
},
"entrypoints": {
"hashes": [
"sha256:589f874b313739ad35be6e0cd7efde2a4e9b6fea91edcc34e58ecbb8dbe56d19",
"sha256:c70dd71abe5a8c85e55e12c19bd91ccfeec11a6e99044204511f9ed547d48451"
],
"version": "==0.3"
},
"flake8": {
"hashes": [
"sha256:6a35f5b8761f45c5513e3405f110a86bea57982c3b75b766ce7b65217abe1670",
"sha256:c01f8a3963b3571a8e6bd7a4063359aff90749e160778e03817cd9b71c9e07d2"
"sha256:6d8c66a65635d46d54de59b027a1dda40abbe2275b3164b634835ac9c13fd048",
"sha256:6eab21c6e34df2c05416faa40d0c59963008fff29b6f0ccfe8fa28152ab3e383"
],
"index": "pypi",
"version": "==3.6.0"
"version": "==3.7.6"
},
"flask": {
"hashes": [
@@ -500,10 +520,10 @@
},
"marshmallow": {
"hashes": [
"sha256:3133fb98afd627dcd8c06e4705f0ecea1b28003a53820d0266fa6c0ff7cf215c",
"sha256:6489e72ea75a30cb07686ce01e24bf65fc7f42edf429153a70abb9e38e56ef52"
"sha256:7f9aba737a59dd3c6c6c79846f1df2fbfe036c17f038bbc2c83911b7304a90e1",
"sha256:b41cc52fe0491bdb8aa3e2186ca57d478d9ef69dba87fe37d309aa8a08fd30dd"
],
"version": "==3.0.0rc2"
"version": "==3.0.0rc4"
},
"mccabe": {
"hashes": [
@@ -514,18 +534,18 @@
},
"more-itertools": {
"hashes": [
"sha256:38a936c0a6d98a38bcc2d03fdaaedaba9f412879461dd2ceff8d37564d6522e4",
"sha256:c0a5785b1109a6bd7fac76d6837fd1feca158e54e521ccd2ae8bfe393cc9d4fc",
"sha256:fe7a7cae1ccb57d33952113ff4fa1bc5f879963600ed74918f1236e212ee50b9"
"sha256:0125e8f60e9e031347105eb1682cef932f5e97d7b9a1a28d9bf00c22a5daef40",
"sha256:590044e3942351a1bdb1de960b739ff4ce277960f2425ad4509446dbace8d9d1"
],
"version": "==5.0.0"
"markers": "python_version > '2.7'",
"version": "==6.0.0"
},
"packaging": {
"hashes": [
"sha256:0886227f54515e592aaa2e5a553332c73962917f2831f1b0f9b9f4380a4b9807",
"sha256:f95a1e147590f204328170981833854229bb2912ac3d5f89e2a8ccd2834800c9"
"sha256:0c98a5d0be38ed775798ece1b9727178c4469d9c3b4ada66e8e6b7849f8732af",
"sha256:9e1cbf8c12b1f1ce0bb5344b8d7ecf66a6f8a6e91bcb0c84593ed6d3ab5c4ab3"
],
"version": "==18.0"
"version": "==19.0"
},
"pkginfo": {
"hashes": [
@@ -550,17 +570,17 @@
},
"pycodestyle": {
"hashes": [
"sha256:cbc619d09254895b0d12c2c691e237b2e91e9b2ecf5e84c26b35400f93dcfb83",
"sha256:cbfca99bd594a10f674d0cd97a3d802a1fdef635d4361e1a2658de47ed261e3a"
"sha256:95a2219d12372f05704562a14ec30bc76b05a5b297b21a5dfe3f6fac3491ae56",
"sha256:e40a936c9a450ad81df37f549d676d127b1b66000a6c500caa2b085bc0ca976c"
],
"version": "==2.4.0"
"version": "==2.5.0"
},
"pyflakes": {
"hashes": [
"sha256:9a7662ec724d0120012f6e29d6248ae3727d821bba522a0e6b356eff19126a49",
"sha256:f661252913bc1dbe7fcfcbf0af0db3f42ab65aabd1a6ca68fe5d466bace94dae"
"sha256:5e8c00e30c464c99e0b501dc160b13a14af7f27d4dffb529c556e30a159e231d",
"sha256:f277f9ca3e55de669fba45b7393a1449009cff5a37d1af10ebb76c52765269cd"
],
"version": "==2.0.0"
"version": "==2.1.0"
},
"pygments": {
"hashes": [
@@ -571,18 +591,18 @@
},
"pyparsing": {
"hashes": [
"sha256:40856e74d4987de5d01761a22d1621ae1c7f8774585acae358aa5c5936c6c90b",
"sha256:f353aab21fd474459d97b709e527b5571314ee5f067441dc9f88e33eecd96592"
"sha256:66c9268862641abcac4a96ba74506e594c884e3f57690a696d21ad8210ed667a",
"sha256:f6c5ef0d7480ad048c054c37632c67fca55299990fff127850181659eea33fc3"
],
"version": "==2.3.0"
"version": "==2.3.1"
},
"pytest": {
"hashes": [
"sha256:3e65a22eb0d4f1bdbc1eacccf4a3198bf8d4049dea5112d70a0c61b00e748d02",
"sha256:5924060b374f62608a078494b909d341720a050b5224ff87e17e12377486a71d"
"sha256:067a1d4bf827ffdd56ad21bd46674703fce77c5957f6c1eef731f6146bfcef1c",
"sha256:9687049d53695ad45cf5fdc7bbd51f0c49f1ea3ecfc4b7f3fde7501b541f17f4"
],
"index": "pypi",
"version": "==4.1.0"
"version": "==4.3.0"
},
"pytest-cov": {
"hashes": [
@@ -615,10 +635,10 @@
},
"requests-toolbelt": {
"hashes": [
"sha256:42c9c170abc2cacb78b8ab23ac957945c7716249206f90874651971a4acff237",
"sha256:f6a531936c6fa4c6cfce1b9c10d5c4f498d16528d2a54a22ca00011205a187b5"
"sha256:380606e1d10dc85c3bd47bf5a6095f815ec007be7a8b69c878507068df059e6f",
"sha256:968089d4584ad4ad7c171454f0a5c6dac23971e9472521ea3b6d49d610aa6fc0"
],
"version": "==0.8.0"
"version": "==0.9.1"
},
"six": {
"hashes": [
@@ -636,18 +656,53 @@
},
"sphinx": {
"hashes": [
"sha256:429e3172466df289f0f742471d7e30ba3ee11f3b5aecd9a840480d03f14bcfe5",
"sha256:c4cb17ba44acffae3d3209646b6baec1e215cad3065e852c68cc569d4df1b9f8"
"sha256:230af939a2f678ab4f2a0a948c3b24a822a0d280821859caaefb750ef7413003",
"sha256:835c701420102a0a71ba2ed54a5bada2da6fd01263bf6dc8c5c80c798e27709c"
],
"index": "pypi",
"version": "==1.8.3"
"version": "==2.0.0b1"
},
"sphinxcontrib-websupport": {
"sphinxcontrib-applehelp": {
"hashes": [
"sha256:68ca7ff70785cbe1e7bccc71a48b5b6d965d79ca50629606c7861a21b206d9dd",
"sha256:9de47f375baf1ea07cdb3436ff39d7a9c76042c10a769c52353ec46e4e8fc3b9"
"sha256:edaa0ab2b2bc74403149cb0209d6775c96de797dfd5b5e2a71981309efab3897",
"sha256:fb8dee85af95e5c30c91f10e7eb3c8967308518e0f7488a2828ef7bc191d0d5d"
],
"version": "==1.1.0"
"version": "==1.0.1"
},
"sphinxcontrib-devhelp": {
"hashes": [
"sha256:6c64b077937330a9128a4da74586e8c2130262f014689b4b89e2d08ee7294a34",
"sha256:9512ecb00a2b0821a146736b39f7aeb90759834b07e81e8cc23a9c70bacb9981"
],
"version": "==1.0.1"
},
"sphinxcontrib-htmlhelp": {
"hashes": [
"sha256:0d691ca8edf5995fbacfe69b191914256071a94cbad03c3688dca47385c9206c",
"sha256:e31c8271f5a8f04b620a500c0442a7d5cfc1a732fa5c10ec363f90fe72af0cb8"
],
"version": "==1.0.1"
},
"sphinxcontrib-jsmath": {
"hashes": [
"sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178",
"sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"
],
"version": "==1.0.1"
},
"sphinxcontrib-qthelp": {
"hashes": [
"sha256:18ec9f74ea2c92fd512d5f3b532d6ab4ac2be76b12cc2490f7729842ba2a60c9",
"sha256:f39159b45de6d3d86c30874a3220be4f8e75ed12c71aff50cb8b2cac46e240f0"
],
"version": "==1.0.1"
},
"sphinxcontrib-serializinghtml": {
"hashes": [
"sha256:01d9b2617d7e8ddf7a00cae091f08f9fa4db587cc160b493141ee56710810932",
"sha256:392187ac558863b8aff0d76dc78e0731fed58f3b06e2b00e22995dcdb630f213"
],
"version": "==1.1.1"
},
"toml": {
"hashes": [
@@ -658,18 +713,18 @@
},
"tqdm": {
"hashes": [
"sha256:b856be5cb6cfaee3b2733655c7c5bbc7751291bb5d1a4f54f020af4727570b3e",
"sha256:c9b9b5eeba13994a4c266aae7eef7aeeb0ba2973e431027e942b4faea139ef49"
"sha256:d385c95361699e5cf7622485d9b9eae2d4864b21cd5a2374a9c381ffed701021",
"sha256:e22977e3ebe961f72362f6ddfb9197cc531c9737aaf5f607ef09740c849ecd05"
],
"version": "==4.29.1"
"version": "==4.31.1"
},
"twine": {
"hashes": [
"sha256:7d89bc6acafb31d124e6e5b295ef26ac77030bf098960c2a4c4e058335827c5c",
"sha256:fad6f1251195f7ddd1460cb76d6ea106c93adb4e56c41e0da79658e56e547d2c"
"sha256:0fb0bfa3df4f62076cab5def36b1a71a2e4acb4d1fa5c97475b048117b1a6446",
"sha256:d6c29c933ecfc74e9b1d9fa13aa1f87c5d5770e119f5a4ce032092f0ff5b14dc"
],
"index": "pypi",
"version": "==1.12.1"
"version": "==1.13.0"
},
"urllib3": {
"hashes": [
+4 -2
View File
@@ -49,11 +49,13 @@ Only **Python 3.6+** is supported.
The primary concept here is to bring the niceties that are brought forth from both Flask and Falcon and unify them into a single framework, along with some new ideas I have. I also wanted to take some of the API primitives that are instilled in the Requests library and put them into a web framework. So, you'll find a lot of parallels here with Requests.
- Setting `resp.text` sends back unicode, while setting `resp.content` sends back bytes.
- Setting `resp.media` sends back JSON/YAML (`.text`/`.content` override this).
- Setting `resp.content` sends back bytes.
- Setting `resp.text` sends back unicode, while setting `resp.html` sends back HTML.
- Setting `resp.media` sends back JSON/YAML (`.text`/`.html`/`.content` override this).
- Case-insensitive `req.headers` dict (from Requests directly).
- `resp.status_code`, `req.method`, `req.url`, and other familiar friends.
## Ideas
- Flask-style route expression, with new capabilities -- all while using Python 3.6+'s new f-string syntax.
+3 -2
View File
@@ -113,8 +113,9 @@ The Basic Idea
The primary concept here is to bring the niceties that are brought forth from both Flask and Falcon and unify them into a single framework, along with some new ideas I have. I also wanted to take some of the API primitives that are instilled in the Requests library and put them into a web framework. So, you'll find a lot of parallels here with Requests.
- Setting ``resp.text`` sends back unicode, while setting ``resp.content`` sends back bytes.
- Setting ``resp.media`` sends back JSON/YAML (``.text``/``.content`` override this).
- Setting ``resp.content`` sends back bytes.
- Setting ``resp.text`` sends back unicode, while setting ``resp.html`` sends back HTML.
- Setting ``resp.media`` sends back JSON/YAML (``.text``/``.html``/``.content`` override this).
- Case-insensitive ``req.headers`` dict (from Requests directly).
- ``resp.status_code``, ``req.method``, ``req.url``, and other familiar friends.
+1 -1
View File
@@ -69,7 +69,7 @@ If you want to render a template, simply use ``api.template``. No need for addit
@api.route("/hello/{who}/html")
def hello_html(req, resp, *, who):
resp.content = api.template('hello.html', who=who)
resp.html = api.template('hello.html', who=who)
The ``api`` instance is available as an object during template rendering.
+40 -1
View File
@@ -63,7 +63,28 @@ Responder comes with built-in support for OpenAPI / marshmallow::
import responder
from marshmallow import Schema, fields
api = responder.API(title="Web Service", version="1.0", openapi="3.0.0")
description = "This is a sample server for a pet store."
terms_of_service = "http://example.com/terms/"
contact = {
"name": "API Support",
"url": "http://www.example.com/support",
"email": "support@example.com",
}
license = {
"name": "Apache 2.0",
"url": "https://www.apache.org/licenses/LICENSE-2.0.html",
}
api = responder.API(
title="Web Service",
version="1.0",
openapi="3.0.2",
docs_route='/docs',
description=description,
terms_of_service=terms_of_service,
contact=contact,
license=license,
)
@api.schema("Pet")
@@ -157,6 +178,24 @@ Responder makes it very easy to interact with cookies from a Request, or add som
{"hello": "world"}
To set cookies directives, you should use `resp.set_cookie`::
>>> resp.set_cookie("hello", value="world", max_age=60)
Supported directives:
* ``key`` - **Reduired**
* ``value`` - [OPTIONAL] - Defaults to ``""``.
* ``expires`` - Defaults to ``None``.
* ``max_age`` - Defaults to ``None``.
* ``domain`` - Defaults to ``None``.
* ``path`` - Defaults to ``"/"``.
* ``secure`` - Defaults to ``False``.
* ``httponly`` - Defaults to ``True``.
For more information see `directives <https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#Directives>`_
Using Cookie-Based Sessions
---------------------------
View File
+1 -1
View File
@@ -1 +1 @@
__version__ = "1.1.2"
__version__ = "1.3.0"
+46 -22
View File
@@ -46,6 +46,12 @@ class API:
: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.
:param title: The title of the application (OpenAPI Info Object)
:param version: The version of the OpenAPI document (OpenAPI Info Object)
:param description: The description of the OpenAPI document (OpenAPI Info Object)
:param terms_of_service: A URL to the Terms of Service for the API (OpenAPI Info Object)
:param contact: The contact dictionary of the application (OpenAPI Contact Object)
:param license: The license information of the exposed API (OpenAPI License Object)
"""
status_codes = status_codes
@@ -56,6 +62,10 @@ class API:
debug=False,
title=None,
version=None,
description=None,
terms_of_service=None,
contact=None,
license=None,
openapi=None,
openapi_route="/schema.yml",
static_dir="static",
@@ -74,9 +84,13 @@ class API:
self.secret_key = secret_key
self.title = title
self.version = version
self.description = description
self.terms_of_service = terms_of_service
self.contact = contact
self.license = license
self.openapi_version = openapi
self.static_dir = Path(os.path.abspath(static_dir))
self.static_route = static_route
self.static_route = f"/{static_route.strip('/')}"
self.templates_dir = Path(os.path.abspath(templates_dir))
self.built_in_templates_dir = Path(
os.path.abspath(os.path.dirname(__file__) + "/templates")
@@ -104,7 +118,7 @@ class API:
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 = WhiteNoise(application=self._notfound_wsgi_app)
self.whitenoise.add_files(str(self.static_dir))
self.whitenoise.add_files(
@@ -156,9 +170,14 @@ class API:
) #: A Requests session that is connected to the ASGI app.
@staticmethod
def _default_wsgi_app(*args, **kwargs):
def _default_wsgi_app(environ, start_response):
pass
@staticmethod
def _notfound_wsgi_app(environ, start_response):
start_response("404 NOT FOUND", [("Content-Type", "text/plain")])
return [b"Not Found."]
@property
def before_requests(self):
def gen():
@@ -170,11 +189,23 @@ class API:
@property
def _apispec(self):
info = {}
if self.description is not None:
info["description"] = self.description
if self.terms_of_service is not None:
info["termsOfService"] = self.terms_of_service
if self.contact is not None:
info["contact"] = self.contact
if self.license is not None:
info["license"] = self.license
spec = APISpec(
title=self.title,
version=self.version,
openapi_version=self.openapi_version,
plugins=[MarshmallowPlugin()],
info=info,
)
for route in self.routes:
@@ -239,7 +270,7 @@ class API:
async def _dispatch_ws(self, ws):
route = self.path_matches_route(ws.url.path)
route = self.routes.get(route)
# await self._dispatch(route, ws=ws)
try:
try:
# Run the view.
@@ -250,10 +281,8 @@ class API:
except TypeError as e:
cont = True
except Exception:
self.background(
self.default_response,
websocket=route.uses_websocket,
error=True
await self.background(
self.default_response, websocket=route.uses_websocket, error=True
)
raise
@@ -292,11 +321,6 @@ class API:
if route_object.does_match(path):
return route
def _prepare_cookies(self, resp):
if resp.cookies:
header = " ".join([f"{k}={v};" for k, v in resp.cookies.items()])
resp.headers["Set-Cookie"] = header
@property
def _signer(self):
return itsdangerous.Signer(self.secret_key)
@@ -333,7 +357,6 @@ class API:
self.default_response(req=req, resp=resp)
self._prepare_session(resp)
self._prepare_cookies(resp)
return resp
@@ -354,7 +377,7 @@ class API:
except TypeError as e:
cont = True
except Exception:
self.background(self.default_response, req, resp, error=True)
await self.background(self.default_response, req, resp, error=True)
raise
if route.is_class_based or cont:
@@ -388,7 +411,7 @@ class API:
# If it's async, await it.
if hasattr(r, "send"):
await r
except Exception as e:
except Exception:
await self.background(self.default_response, req, resp, error=True)
raise
@@ -461,14 +484,16 @@ class API:
resp.text = "Application error."
def docs_response(self, req, resp):
resp.text = self.docs
resp.html = self.docs
def static_response(self, req, resp):
index = (self.static_dir / "index.html").resolve()
resp.content = None
if os.path.exists(index):
with open(index, "r") as f:
resp.text = f.read()
resp.html = f.read()
else:
resp.status_code = status_codes.HTTP_404
resp.text = "Not found."
def schema_response(self, req, resp):
resp.status_code = status_codes.HTTP_200
@@ -593,7 +618,6 @@ class API:
def static_url(asset):
return f"{self.static_route}/{asset}"
# return asset
return template.render(
document=document,
@@ -658,6 +682,6 @@ class API:
spawn()
def run(self, **kwargs):
if 'debug' not in kwargs:
kwargs.update({'debug': self.debug})
if "debug" not in kwargs:
kwargs.update({"debug": self.debug})
self.serve(**kwargs)
+1 -1
View File
@@ -40,4 +40,4 @@ def cli():
prop = "api"
app = __import__(module)
getattr(app, prop).run()
getattr(app, prop).run()
+103 -20
View File
@@ -1,4 +1,6 @@
import functools
import io
import inspect
import json
import gzip
from base64 import b64decode
@@ -13,7 +15,10 @@ from requests.structures import CaseInsensitiveDict
from requests.cookies import RequestsCookieJar
from starlette.datastructures import MutableHeaders
from starlette.requests import Request as StarletteRequest
from starlette.responses import Response as StarletteResponse
from starlette.responses import (
Response as StarletteResponse,
StreamingResponse as StarletteStreamingResponse,
)
from urllib.parse import parse_qs
@@ -89,9 +94,16 @@ class QueryDict(dict):
yield from super().items()
# TODO: add slots
class Request:
__slots__ = ["_starlette", "formats", "_headers", "_encoding", "api", "_content"]
__slots__ = [
"_starlette",
"formats",
"_headers",
"_encoding",
"api",
"_content",
"_cookies",
]
def __init__(self, scope, receive, api=None):
self._starlette = StarletteRequest(scope, receive)
@@ -105,11 +117,12 @@ class Request:
headers[key] = value
self._headers = headers
self._cookies = None
@property
def session(self):
"""The session data, in dict form, from the Request."""
if "Responder-Session" in self.cookies:
if self.api.session_cookie in self.cookies:
data = self.cookies[self.api.session_cookie]
@@ -145,14 +158,17 @@ class Request:
@property
def cookies(self):
"""The cookies sent in the Request, as a dictionary."""
cookies = RequestsCookieJar()
cookie_header = self.headers.get("Cookie", "")
if self._cookies is None:
cookies = RequestsCookieJar()
cookie_header = self.headers.get("Cookie", "")
bc = SimpleCookie(cookie_header)
for k, v in bc.items():
cookies[k] = v
bc = SimpleCookie(cookie_header)
for key, morsel in bc.items():
cookies[key] = morsel.value
return cookies.get_dict()
self._cookies = cookies.get_dict()
return self._cookies
@property
def params(self):
@@ -232,11 +248,21 @@ class Request:
return await format(self)
def content_setter(mimetype):
def getter(instance):
return instance.content
def setter(instance, value):
instance.content = value
instance.mimetype = mimetype
return property(fget=getter, fset=setter)
class Response:
__slots__ = [
"req",
"status_code",
"text",
"content",
"encoding",
"media",
@@ -244,33 +270,54 @@ class Response:
"formats",
"cookies",
"session",
"mimetype",
"_stream",
]
text = content_setter("text/plain")
html = content_setter("text/html")
def __init__(self, req, *, formats):
self.req = req
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.mimetype = None
self.encoding = DEFAULT_ENCODING
self.media = (
None
) #: A Python object that will be content-negotiated and sent back to the client. Typically, in JSON formatting.
self._stream = None
self.headers = (
{}
) #: 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.cookies = SimpleCookie() #: The cookies set in the Response
self.session = (
req.session.copy()
) #: The cookie-based session data, in dict form, to add to the Response.
# Property or func/dec
def stream(self, func, *args, **kwargs):
assert inspect.isasyncgenfunction(func)
self._stream = functools.partial(func, *args, **kwargs)
return func
@property
async def body(self):
if self.content is not None:
return (self.content, {})
if self._stream is not None:
return (self._stream(), {})
if self.text is not None:
return (self.text.encode(self.encoding), {"Encoding": self.encoding})
if self.content is not None:
headers = {}
content = self.content
if self.mimetype is not None:
headers["Content-Type"] = self.mimetype
if self.mimetype == "text/plain" and self.encoding is not None:
headers["Encoding"] = self.encoding
content = content.encode(self.encoding)
return (content, headers)
for format in self.formats:
if self.req.accepts(format):
@@ -282,12 +329,48 @@ class Response:
{"Content-Type": "application/json"},
)
def set_cookie(
self,
key,
value="",
expires=None,
path="/",
domain=None,
max_age=None,
secure=False,
httponly=True,
):
self.cookies[key] = value
morsel = self.cookies[key]
if expires is not None:
morsel["expires"] = expires
if path is not None:
morsel["path"] = path
if domain is not None:
morsel["domain"] = domain
if max_age is not None:
morsel["max-age"] = max_age
morsel["secure"] = secure
morsel["httponly"] = httponly
def _prepare_cookies(self, starlette_response):
cookie_header = (
(b"set-cookie", morsel.output(header="").lstrip().encode("latin-1"))
for morsel in self.cookies.values()
)
starlette_response.raw_headers.extend(cookie_header)
async def __call__(self, receive, send):
body, headers = await self.body
if self.headers:
headers.update(self.headers)
response = StarletteResponse(
body, status_code=self.status_code, headers=headers
)
if self._stream is not None:
response_cls = StarletteStreamingResponse
else:
response_cls = StarletteResponse
response = response_cls(body, status_code=self.status_code, headers=headers)
self._prepare_cookies(response)
await response(receive, send)
+1 -1
View File
@@ -55,7 +55,7 @@ class Route:
def _weight(self):
params = set(self._param_pattern.findall(self.route))
params_count = len(params)
w = len(self.route.rsplit('}', 1)[-1].strip('/'))
w = len(self.route.rsplit("}", 1)[-1].strip("/"))
return params_count != 0, w == 0, -params_count
@property
+7 -7
View File
@@ -4,11 +4,11 @@ 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,
"allow_origins": (),
"allow_methods": ("GET",),
"allow_headers": (),
"allow_credentials": False,
"allow_origin_regex": None,
"expose_headers": (),
"max_age": 600,
}
+1 -1
View File
@@ -22,7 +22,7 @@ if sys.argv[-1] == "publish":
sys.exit()
required = [
"starlette>=0.9,<0.10",
"starlette==0.10.*",
"uvicorn",
"aiofiles",
"pyyaml",
+2 -4
View File
@@ -18,10 +18,7 @@ def current_dir():
@pytest.fixture
def api():
return responder.API(
debug=False,
allowed_hosts=[";"]
)
return responder.API(debug=False, allowed_hosts=[";"])
@pytest.fixture
@@ -49,6 +46,7 @@ def flask():
return app
@pytest.fixture
def schema():
class Query(graphene.ObjectType):
+177 -8
View File
@@ -2,8 +2,10 @@ import concurrent
import pytest
import yaml
import random
import responder
import requests
import string
import io
from starlette.responses import PlainTextResponse
@@ -334,7 +336,7 @@ def test_schema_generation():
from marshmallow import Schema, fields
api = responder.API(
title="Web Service", openapi="3.0", allowed_hosts=["testserver", ";"]
title="Web Service", openapi="3.0.2", allowed_hosts=["testserver", ";"]
)
@api.schema("Pet")
@@ -359,17 +361,34 @@ def test_schema_generation():
dump = yaml.safe_load(r.content)
assert dump
assert dump["openapi"] == "3.0"
assert dump["openapi"] == "3.0.2"
def test_documentation():
import responder
from marshmallow import Schema, fields
description = "This is a sample server for a pet store."
terms_of_service = "http://example.com/terms/"
contact = {
"name": "API Support",
"url": "http://www.example.com/support",
"email": "support@example.com",
}
license = {
"name": "Apache 2.0",
"url": "https://www.apache.org/licenses/LICENSE-2.0.html",
}
api = responder.API(
title="Web Service",
openapi="3.0",
version="1.0",
openapi="3.0.2",
docs_route="/docs",
description=description,
terms_of_service=terms_of_service,
contact=contact,
license=license,
allowed_hosts=["testserver", ";"],
)
@@ -422,13 +441,23 @@ def test_cookies(api):
def cookies(req, resp):
resp.media = {"cookies": req.cookies}
resp.cookies["sent"] = "true"
resp.set_cookie(
"hello",
"world",
expires=123,
path="/",
max_age=123,
secure=False,
httponly=True,
)
r = api.requests.get(api.url_for(cookies), cookies={"hello": "universe"})
assert r.json() == {"cookies": {"hello": "universe"}}
assert "sent" in r.cookies
assert "hello" in r.cookies
r = api.requests.get(api.url_for(cookies))
assert r.json() == {"cookies": {"sent": "true"}}
assert r.json() == {"cookies": {"hello": "world", "sent": "true"}}
@pytest.mark.xfail
@@ -439,11 +468,11 @@ def test_sessions(api):
resp.media = resp.session
r = api.requests.get(api.url_for(view))
assert "Responder-Session" in r.cookies
assert api.session_cookie in r.cookies
r = api.requests.get(api.url_for(view))
assert (
r.cookies["Responder-Session"]
r.cookies[api.session_cookie]
== '{"hello": "world"}.r3EB04hEEyLYIJaAXCEq3d4YEbs'
)
assert r.json() == {"hello": "world"}
@@ -479,8 +508,9 @@ def test_500(api):
def view(req, resp):
raise ValueError
dumb_client = responder.api.TestClient(api, base_url="http://;",
raise_server_exceptions=False)
dumb_client = responder.api.TestClient(
api, base_url="http://;", raise_server_exceptions=False
)
r = dumb_client.get(api.url_for(view))
assert not r.ok
assert r.status_code == responder.status_codes.HTTP_500
@@ -602,3 +632,142 @@ def test_allowed_hosts():
api._session = None
r = api.session(base_url="http://tenant2.;").get(api.url_for(get))
assert r.status_code == 200
def create_asset(static_dir, name=None, parent_dir=None):
if name is None:
name = random.choices(string.ascii_letters, k=6)
# :3
ext = random.choices(string.ascii_letters, k=2)
name = f"{name}.{ext}"
if parent_dir is None:
parent_dir = static_dir
else:
parent_dir = static_dir.mkdir(parent_dir)
asset = parent_dir.join(name)
asset.write("body { color: blue; }")
return asset
def test_staticfiles(tmpdir):
static_dir = tmpdir.mkdir("static")
asset1 = create_asset(static_dir)
parent_dir = "css"
asset2 = create_asset(static_dir, name="asset2", parent_dir=parent_dir)
api = responder.API(static_dir=str(static_dir))
session = api.session()
static_route = api.static_route
# ok
r = session.get(f"{static_route}/{asset1.basename}")
assert r.status_code == api.status_codes.HTTP_200
r = session.get(f"{static_route}/{parent_dir}/{asset2.basename}")
assert r.status_code == api.status_codes.HTTP_200
# Asset not found
r = session.get(f"{static_route}/not_found.css")
assert r.status_code == api.status_codes.HTTP_404
# Not found on dir listing
r = session.get(f"{static_route}")
assert r.status_code == api.status_codes.HTTP_404
r = session.get(f"{static_route}/{parent_dir}")
assert r.status_code == api.status_codes.HTTP_404
def test_staticfiles_custom_route(tmpdir):
static_dir = tmpdir.mkdir("static")
static_route = "custom/static/route/"
asset = create_asset(static_dir)
api = responder.API(static_dir=str(static_dir), static_route=static_route)
session = api.session()
# Check
assert api.static_route == "/custom/static/route"
static_route = api.static_route
# ok
r = session.get(f"{static_route}/{asset.basename}")
assert r.status_code == api.status_codes.HTTP_200
# Asset not found
r = session.get(f"{static_route}/not_found.css")
assert r.status_code == api.status_codes.HTTP_404
# Not found on dir listing
r = session.get(f"{static_route}")
assert r.status_code == api.status_codes.HTTP_404
def test_response_html_property(api):
@api.route("/")
def view(req, resp):
resp.html = "<h1>Hello !</h1>"
assert resp.content == "<h1>Hello !</h1>"
assert resp.mimetype == "text/html"
r = api.requests.get(api.url_for(view))
assert r.content == b"<h1>Hello !</h1>"
assert r.headers["Content-Type"] == "text/html"
def test_response_text_property(api):
@api.route("/")
def view(req, resp):
resp.text = "<h1>Hello !</h1>"
assert resp.content == "<h1>Hello !</h1>"
assert resp.mimetype == "text/plain"
r = api.requests.get(api.url_for(view))
assert r.content == b"<h1>Hello !</h1>"
assert r.headers["Content-Type"] == "text/plain"
def test_stream(api, session):
async def shout_stream(who):
for c in who.upper():
yield c
@api.route("/{who}")
async def greeting(req, resp, *, who):
resp.stream(shout_stream, who)
r = session.get("/morocco")
assert r.text == "MOROCCO"
@api.route("/")
async def home(req, resp):
# Raise when it's not an async generator
with pytest.raises(AssertionError):
def foo():
pass
res.stream(foo)
with pytest.raises(AssertionError):
async def foo():
pass
res.stream(foo)
with pytest.raises(AssertionError):
def foo():
yield "oopsie"
res.stream(foo)
+32 -16
View File
@@ -89,27 +89,43 @@ def test_does_match_with_route(route, match, expected):
[
pytest.param("/{greetings}", (True, True, -1), id="with one param"),
pytest.param(
"/{greetings}.{name}", (True, True, -2), id="with 2 params and dot in the middle"
),
pytest.param("/{greetings}/{name}", (True, True, -2), id="with 2 param and subpath"),
pytest.param(
"/{greetings}/{name}/{hello}", (True, True, -3), id="with 3 param and subpath"
"/{greetings}.{name}",
(True, True, -2),
id="with 2 params and dot in the middle",
),
pytest.param(
"/{greetings}_{name}", (True, True, -2), id="with 2 param and underscore"
),
pytest.param("/{greetings}/9roda", (True, False, -1), id="with one param"),
pytest.param(
"/{greetings}.{name}/9roda", (True, False, -2), id="with 2 params and dot in the middle"
),
pytest.param("/{greetings}/{name}/9roda", (True, False, -2), id="with 2 param and subpath"),
pytest.param(
"/{greetings}/{name}/{hello}/9roda", (True, False, -3), id="with 3 param and subpath"
"/{greetings}/{name}", (True, True, -2), id="with 2 params and subpath"
),
pytest.param(
"/{greetings}_{name}/9roda", (True, False, -2), id="with 2 param and underscore"
"/{greetings}/{name}/{hello}",
(True, True, -3),
id="with 3 params and subpath",
),
pytest.param("/hello", (False, False, 0), id="with 2 param and underscore"),
pytest.param(
"/{greetings}_{name}", (True, True, -2), id="with 2 params and underscore"
),
pytest.param("/{greetings}/test", (True, False, -1), id="with one param"),
pytest.param(
"/{greetings}.{name}/test",
(True, False, -2),
id="with 2 params and dot in the middle",
),
pytest.param(
"/{greetings}/{name}/test",
(True, False, -2),
id="with 2 params and subpath",
),
pytest.param(
"/{greetings}/{name}/{hello}/test",
(True, False, -3),
id="with 3 params and subpath",
),
pytest.param(
"/{greetings}_{name}/test",
(True, False, -2),
id="with 2 params and underscore",
),
pytest.param("/hello", (False, False, 0), id="without params"),
],
)
def test_weight(path_param, expected_weight):