mirror of
https://github.com/kennethreitz/responder.git
synced 2026-06-05 23:00:17 +00:00
Compare commits
51 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f3cdc99b29 | |||
| eb70376438 | |||
| dae1a4fa35 | |||
| 2ad351197e | |||
| 3d9235c4bc | |||
| 2cd5596def | |||
| d4191030d9 | |||
| 447630a051 | |||
| f7b53a4895 | |||
| 21896aa171 | |||
| e8a15697d2 | |||
| 0030993631 | |||
| 13ba2f72f5 | |||
| 9f39917895 | |||
| 1b0859fdbb | |||
| acd1561b1b | |||
| 9f2182949d | |||
| 6e5b3a4bf9 | |||
| d2ec323888 | |||
| 8b9645cf2d | |||
| 4ecfef0ddf | |||
| 84fb7bd622 | |||
| 0b261252e1 | |||
| d60b5ee39e | |||
| e2f887ec5f | |||
| 97da6a6694 | |||
| c0e9a6778d | |||
| 5c327a2e0b | |||
| 5ed45634cb | |||
| a50a373e84 | |||
| 86705d0c2f | |||
| b9581444f9 | |||
| 2a60b094b8 | |||
| 1ec567cabf | |||
| 4fd898b239 | |||
| 03d6b72a00 | |||
| 4d0382d580 | |||
| a0dd7481ec | |||
| 1c91480b0c | |||
| 85e5ec0a9a | |||
| 4ac04b0abc | |||
| d7e64a6e39 | |||
| 17d526632e | |||
| 43da481df7 | |||
| 5f5402833b | |||
| d59c4333f2 | |||
| 49114f36ce | |||
| b2039d99f3 | |||
| 94fd86fee0 | |||
| d70fdd3301 | |||
| 05b75efb43 |
@@ -10,3 +10,4 @@ install:
|
||||
# command to run the dependencies
|
||||
script:
|
||||
- "pytest"
|
||||
- "black responder tests setup.py --check"
|
||||
|
||||
@@ -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
@@ -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": [
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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
@@ -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
|
||||
---------------------------
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
__version__ = "1.1.2"
|
||||
__version__ = "1.3.0"
|
||||
|
||||
+46
-22
@@ -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
@@ -40,4 +40,4 @@ def cli():
|
||||
prop = "api"
|
||||
|
||||
app = __import__(module)
|
||||
getattr(app, prop).run()
|
||||
getattr(app, prop).run()
|
||||
|
||||
+103
-20
@@ -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
@@ -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
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user