Compare commits

..

56 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
kennethreitz be56e92d65 Merge pull request #277 from amikrop/master
Make `_route_for` shorter
2019-01-12 07:06:31 -05:00
kennethreitz 69eb843604 fixes 2019-01-12 06:57:28 -05:00
Aristotelis Mikropoulos 84a7f0e90b Make _route_for shorter 2019-01-11 03:46:35 +02:00
kennethreitz d1e105a29a Merge pull request #276 from tomchristie/resolve-startup-and-shutdown-events
Resolve startup/shutdown events
2019-01-09 17:49:49 -05:00
Tom Christie 9f0a568fa3 Resolve startup/shutdwown events 2019-01-09 12:42:15 +00:00
18 changed files with 574 additions and 197 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
+131 -90
View File
@@ -1,7 +1,7 @@
{
"_meta": {
"hash": {
"sha256": "c0cbfe79ef8c8aa085ee976408a6b934cda63343b8951502b0cc4f0dc3453a3b"
"sha256": "7bbe1f0addd73250027de73d6fb749aa2be3149af9744b107820c5e10498428e"
},
"pipfile-spec": 6,
"requires": {
@@ -32,10 +32,10 @@
},
"apispec": {
"hashes": [
"sha256:8072aaba54cb430787c3662512d5c9fe521eae1ec0b6d7d05b129814b6b48f69",
"sha256:93a6046bf692e8e4398101d447fffcf148b9dbed66d886073e05b491cd6835fd"
"sha256:57a7b81fd19fff0663a7e5ffd196eaea79b5364151ed2b65533be36d55e0229c",
"sha256:b45def53903516e67e8584ee41f34bc60c3e4acace6892b69340293ea20f3caa"
],
"version": "==1.0.0b6"
"version": "==1.0.0"
},
"apistar": {
"hashes": [
@@ -179,16 +179,16 @@
},
"marshmallow": {
"hashes": [
"sha256:7adba78acbce1a812185ab8139d2c80223387d751f8c558d53eceb8aecf7cae5",
"sha256:9aa50624253e654ae97a22854e37287042911c15fb23932be357e56df33c2d51"
"sha256:7f9aba737a59dd3c6c6c79846f1df2fbfe036c17f038bbc2c83911b7304a90e1",
"sha256:b41cc52fe0491bdb8aa3e2186ca57d478d9ef69dba87fe37d309aa8a08fd30dd"
],
"version": "==3.0.0rc1"
"version": "==3.0.0rc4"
},
"parse": {
"hashes": [
"sha256:9dd6048ea212cd032a342f9f6aa2b7bc222f7407c7e37bdc2777fecd36897437"
"sha256:870dd675c1ee8951db3e29b81ebe44fd131e3eb8c03a79483a58ea574f3145c2"
],
"version": "==1.9.0"
"version": "==1.11.1"
},
"promise": {
"hashes": [
@@ -222,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,
@@ -240,10 +240,10 @@
},
"rx": {
"hashes": [
"sha256:13a1d8d9e252625c173dc795471e614eadfe1cf40ffc684e08b8fff0d9748c23",
"sha256:7357592bc7e881a95e0c2013b73326f704953301ab551fbc8133a6fadab84105"
"sha256:7e6919a3159d6c6cee266fdeeb48783118bab20177e38fa5d6a8ebd24a132f72",
"sha256:e7ccf18bb8e76f8a44557febd5c149c2ad36df19442f678be529c710b0553d85"
],
"version": "==1.6.1"
"version": "==3.0.0a2"
},
"six": {
"hashes": [
@@ -254,10 +254,9 @@
},
"starlette": {
"hashes": [
"sha256:01f04283b49a8cb0c8921baa90dbafe47e953f0a265f6ebb38176038e4bd9bf8"
"sha256:8bc2e41f7638290379ae91450413796f92d6c97b88a6b754f3c1a7f8bc7a07d6"
],
"index": "pypi",
"version": "==0.9.9"
"version": "==0.10.7"
},
"urllib3": {
"hashes": [
@@ -268,24 +267,24 @@
},
"uvicorn": {
"hashes": [
"sha256:9c7b384046305982c658d812de862d31c95e7e1c19dc4f0f432d060d921c968a"
"sha256:f27889a332ee5c55b4841b11b2392d00dac079f39063fabc1e13e18ada3eb7ba"
],
"version": "==0.3.23"
"version": "==0.4.5"
},
"uvloop": {
"hashes": [
"sha256:0e4ed2bd0e207bc284c3dfe3aafc9e9c96184f78a0f4881f8d5f9ed82eb08ef4",
"sha256:145931364fa88c9be5e7960729678677ea8d205ceebff3fbf0751e3463907f10",
"sha256:347785a64715f5aa361e01d9414be78c61218fc96fe137d554831c555c3bfe15",
"sha256:40b11baef9d36d92a786ab87c59164df8ca49945b0eb6bfcbdd3985c86864d39",
"sha256:63b6d876f5b7c1f1e1a31b950bb6eabab9b2490c0ba6df06ecaa86c7dac2dbe6",
"sha256:85a63f5b485c756b0390800579b4f22cb3a279795bf52e7698942980fb993793",
"sha256:85ce7aed6481f078c4157e7049bc02b13abdaa3f1adc814e234b6262fab3c808",
"sha256:975a0b29dfd378493b8be47a0599ea9f284ca9e39b4532ab280beaf7cf50d00f",
"sha256:dfe83e6bb90892b0c2440b8e425f83b31c9f23195dd189bd59b2fb3fb12a7080",
"sha256:ea97302d8fa9d7b6fb1dd079774edcc581ebd8561e5ea71e1fd95c803904d38a"
"sha256:198fe0c196056930ec6c4a0a878e531a66d15467ca7c74a875aa90271f0c6e3f",
"sha256:1c175f47d34b84e33c0e312f4987c927ea004afc3a5f05d2f0f610d71d0e4c89",
"sha256:1c47f197be8f0a3c651dd20be1e1bd43268186246f246d4e86c91e95a89e4865",
"sha256:3fd4943570d20e8cd4d9f0a3190ebd5cf040e5610b685e05c878128a11f7ad14",
"sha256:435e232869923fd2248e4ca0ad73e24a5b4debf40bed9dcde133cfe1bef98a7a",
"sha256:9cfdb966ae804c46b96c92207dfd2174935ffc70e706e42e1c94c60d16dbe860",
"sha256:a585781443eeb2edb858f8c08c503aac237a5f1bebf0c84ea8340cc337afa408",
"sha256:b296493e033846e46488a6aa227a75c790091f5ee5456ec637bb0badad1e8851",
"sha256:c684047c6cf6d697ba37872fb1b4489012ea91f3f802c8fbb9c367c4902e88dc",
"sha256:da5a59d8812188b57b5783c7fb78891d14dd1050b6259680e0dbd4253d7d0f64"
],
"version": "==0.12.0rc1"
"version": "==0.12.1"
},
"websockets": {
"hashes": [
@@ -338,10 +337,10 @@
},
"atomicwrites": {
"hashes": [
"sha256:0312ad34fcad8fac3704d441f7b317e50af620823353ec657a53e981f92920c0",
"sha256:ec9ae8adaae229e4f8446952d204a3e4b5fdd2d099f9be3aaf556120135fb3ee"
"sha256:03472c30eb2c5d1ba9227e4c2ca66ab8287fbfbbda3888aa93dc2e28fc6811b4",
"sha256:75a9445bac02d8d058d5e1fe689654ba5a6556a1dfd8ce6ec55a0ed79866cfa6"
],
"version": "==1.2.1"
"version": "==1.3.0"
},
"attrs": {
"hashes": [
@@ -367,10 +366,10 @@
},
"bleach": {
"hashes": [
"sha256:48d39675b80a75f6d1c3bdbffec791cf0bbbab665cf01e20da701c77de278718",
"sha256:73d26f018af5d5adcdabf5c1c974add4361a9c76af215fe32fdec8a6fc5fb9b9"
"sha256:213336e49e102af26d9cde77dd2d0397afabc5a6bf2fed985dc35b5d1e285a16",
"sha256:3fdf7f77adcf649c9911387df51254b813185e32b2c6619f690b593a617e19fa"
],
"version": "==3.0.2"
"version": "==3.1.0"
},
"certifi": {
"hashes": [
@@ -435,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": [
@@ -514,10 +520,10 @@
},
"marshmallow": {
"hashes": [
"sha256:7adba78acbce1a812185ab8139d2c80223387d751f8c558d53eceb8aecf7cae5",
"sha256:9aa50624253e654ae97a22854e37287042911c15fb23932be357e56df33c2d51"
"sha256:7f9aba737a59dd3c6c6c79846f1df2fbfe036c17f038bbc2c83911b7304a90e1",
"sha256:b41cc52fe0491bdb8aa3e2186ca57d478d9ef69dba87fe37d309aa8a08fd30dd"
],
"version": "==3.0.0rc1"
"version": "==3.0.0rc4"
},
"mccabe": {
"hashes": [
@@ -528,32 +534,32 @@
},
"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": [
"sha256:5878d542a4b3f237e359926384f1dde4e099c9f5525d236b1840cf704fa8d474",
"sha256:a39076cb3eb34c333a0dd390b568e9e1e881c7bf2cc0aee12120636816f55aee"
"sha256:7424f2c8511c186cd5424bbf31045b77435b37a8d604990b79d4e70d741148bb",
"sha256:a6d9e40ca61ad3ebd0b72fbadd4fba16e4c0e4df0428c041e01e06eb6ee71f32"
],
"version": "==1.4.2"
"version": "==1.5.0.1"
},
"pluggy": {
"hashes": [
"sha256:447ba94990e8014ee25ec853339faf7b0fc8050cdc3289d4d71f7f410fb90095",
"sha256:bde19360a8ec4dfd8a20dcb811780a30998101f078fc7ded6162f0076f50508f"
"sha256:8ddc32f03971bfdf900a81961a48ccf2fb677cf7715108f85295c67405798616",
"sha256:980710797ff6a041e9a73a5787804f848996ecaa6f8a1b1e08224a5894f2074a"
],
"version": "==0.8.0"
"version": "==0.8.1"
},
"py": {
"hashes": [
@@ -564,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": [
@@ -585,33 +591,33 @@
},
"pyparsing": {
"hashes": [
"sha256:40856e74d4987de5d01761a22d1621ae1c7f8774585acae358aa5c5936c6c90b",
"sha256:f353aab21fd474459d97b709e527b5571314ee5f067441dc9f88e33eecd96592"
"sha256:66c9268862641abcac4a96ba74506e594c884e3f57690a696d21ad8210ed667a",
"sha256:f6c5ef0d7480ad048c054c37632c67fca55299990fff127850181659eea33fc3"
],
"version": "==2.3.0"
"version": "==2.3.1"
},
"pytest": {
"hashes": [
"sha256:f689bf2fc18c4585403348dd56f47d87780bf217c53ed9ae7a3e2d7faa45f8e9",
"sha256:f812ea39a0153566be53d88f8de94839db1e8a05352ed8a49525d7d7f37861e9"
"sha256:067a1d4bf827ffdd56ad21bd46674703fce77c5957f6c1eef731f6146bfcef1c",
"sha256:9687049d53695ad45cf5fdc7bbd51f0c49f1ea3ecfc4b7f3fde7501b541f17f4"
],
"index": "pypi",
"version": "==4.0.2"
"version": "==4.3.0"
},
"pytest-cov": {
"hashes": [
"sha256:513c425e931a0344944f84ea47f3956be0e416d95acbd897a44970c8d926d5d7",
"sha256:e360f048b7dae3f2f2a9a4d067b2dd6b6a015d384d1577c994a43f3f7cbad762"
"sha256:0ab664b25c6aa9716cbf203b17ddb301932383046082c081b9848a0edf5add33",
"sha256:230ef817450ab0699c6cc3c9c8f7a829c34674456f2ed8df1fe1d39780f7c87f"
],
"index": "pypi",
"version": "==2.6.0"
"version": "==2.6.1"
},
"pytz": {
"hashes": [
"sha256:31cb35c89bd7d333cd32c5f278fca91b523b0834369e757f4c5641ea252236ca",
"sha256:8e0f8568c118d3077b46be7d654cc8167fa916092e28320cde048e54bfc9f1e6"
"sha256:32b0891edff07e28efe91284ed9c31e123d84bea3fd98e1f72be2508f43ef8d9",
"sha256:d5f05e487007e29e03409f9398d074e158d920d36eb82eaf66fb1136b0c5374c"
],
"version": "==2018.7"
"version": "==2018.9"
},
"readme-renderer": {
"hashes": [
@@ -629,10 +635,10 @@
},
"requests-toolbelt": {
"hashes": [
"sha256:42c9c170abc2cacb78b8ab23ac957945c7716249206f90874651971a4acff237",
"sha256:f6a531936c6fa4c6cfce1b9c10d5c4f498d16528d2a54a22ca00011205a187b5"
"sha256:380606e1d10dc85c3bd47bf5a6095f815ec007be7a8b69c878507068df059e6f",
"sha256:968089d4584ad4ad7c171454f0a5c6dac23971e9472521ea3b6d49d610aa6fc0"
],
"version": "==0.8.0"
"version": "==0.9.1"
},
"six": {
"hashes": [
@@ -650,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": [
@@ -672,18 +713,18 @@
},
"tqdm": {
"hashes": [
"sha256:3c4d4a5a41ef162dd61f1edb86b0e1c7859054ab656b2e7c7b77e7fbf6d9f392",
"sha256:5b4d5549984503050883bc126280b386f5f4ca87e6c023c5d015655ad75bdebb"
"sha256:d385c95361699e5cf7622485d9b9eae2d4864b21cd5a2374a9c381ffed701021",
"sha256:e22977e3ebe961f72362f6ddfb9197cc531c9737aaf5f607ef09740c849ecd05"
],
"version": "==4.28.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"
+50 -28
View File
@@ -19,7 +19,7 @@ from starlette.middleware.gzip import GZipMiddleware
from starlette.middleware.httpsredirect import HTTPSRedirectMiddleware
from starlette.middleware.lifespan import LifespanMiddleware
from starlette.middleware.trustedhost import TrustedHostMiddleware
from starlette.routing import Router
from starlette.routing import Router, LifespanHandler
from starlette.staticfiles import StaticFiles
from starlette.testclient import TestClient
from starlette.websockets import WebSocket
@@ -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(
@@ -136,7 +150,7 @@ class API:
self.add_middleware(TrustedHostMiddleware, allowed_hosts=self.allowed_hosts)
self.lifespan_handler = LifespanMiddleware(self.app)
self.lifespan_handler = LifespanMiddleware(LifespanHandler)
if self.cors:
self.add_middleware(CORSMiddleware, **self.cors_params)
@@ -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
@@ -555,10 +580,8 @@ class API:
return self._session
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:
for route_object in self.routes.values():
if endpoint in (route_object.endpoint, route_object.endpoint_name):
return route_object
def url_for(self, endpoint, **params):
@@ -595,7 +618,6 @@ class API:
def static_url(asset):
return f"{self.static_route}/{asset}"
# return asset
return template.render(
document=document,
@@ -660,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,
}
+2 -2
View File
@@ -22,7 +22,7 @@ if sys.argv[-1] == "publish":
sys.exit()
required = [
"starlette",
"starlette==0.10.*",
"uvicorn",
"aiofiles",
"pyyaml",
@@ -31,7 +31,7 @@ required = [
"graphql-server-core>=1.1",
"jinja2",
"parse",
"uvloop ; sys_platform != 'win32'",
"uvloop; sys_platform != 'win32'",
"rfc3986",
"python-multipart",
"chardet",
+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):
+182 -21
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
@@ -500,8 +530,7 @@ def test_kinda_websockets(api):
await ws.close()
@pytest.mark.xfail
def test_startup(api, session):
def test_startup(api):
who = [None]
@api.route("/{greeting}")
@@ -509,19 +538,12 @@ def test_startup(api, session):
resp.text = f"{greeting}, {who[0]}!"
@api.on_event("startup")
async def asd():
async def run_startup():
who[0] = "world"
print("startup")
@api.on_event("cleanup")
async def asd():
print("cleanup")
pool = concurrent.futures.ThreadPoolExecutor(max_workers=2)
f = pool.submit(api.run)
r = requests.get(f"http://localhost:5042/hello")
assert r.text == "hello, world!"
with api.requests as session:
r = session.get(f"http://;/hello")
assert r.text == "hello, world!"
def test_redirects(api, session):
@@ -610,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):