mirror of
https://github.com/kennethreitz/responder.git
synced 2026-06-05 23:00:17 +00:00
Compare commits
914 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| cc0fe78382 | |||
| 413028b636 | |||
| 3edf979a8c | |||
| cd75deeb4e | |||
| b71bb5ddb9 | |||
| 27a9459f22 | |||
| b39c539d57 | |||
| 718b53cce2 | |||
| 2e0b4975f7 | |||
| a118a5dc4b | |||
| 69c1d7f185 | |||
| fba2f135a3 | |||
| 4006de72cd | |||
| b3c7252197 | |||
| 398ac3343e | |||
| 8b197ba361 | |||
| e700aa2937 | |||
| 3894550642 | |||
| 43fd041138 | |||
| 363af5338d | |||
| 55430a4366 | |||
| f7c6a3ae97 | |||
| dcadba1425 | |||
| de08b15ae8 | |||
| 0cfca6d906 | |||
| a73e413a66 | |||
| 87931a25d0 | |||
| 1fd9a682dd | |||
| 5d3e650901 | |||
| 48d082e6a5 | |||
| 87e22481e8 | |||
| e48ce6c301 | |||
| e9613500da | |||
| c2943accd0 | |||
| 649a255657 | |||
| 7eaaaaafe1 | |||
| ae09b88978 | |||
| e3e307fd68 | |||
| 89f0724029 | |||
| bebe62adaf | |||
| eb9cddc8c2 | |||
| 7c19eca78a | |||
| ed28b11d21 | |||
| 46cdd4a245 | |||
| ac91b172e6 | |||
| ed0da6d462 | |||
| 555e9bff65 | |||
| bf43d9f202 | |||
| e239cc304d | |||
| 3285bd57c7 | |||
| 3090fb9e68 | |||
| e90bd24ebe | |||
| a0acc03a97 | |||
| 8a668e6efe | |||
| 4c75742e4d | |||
| 796fdc2ddf | |||
| a8caa3054b | |||
| 2ef9e133ad | |||
| 2ec570ad61 | |||
| 02aa338970 | |||
| 882250bd86 | |||
| 3809eda2f2 | |||
| b32eda70d2 | |||
| f1b2f46a10 | |||
| cf82dac4ad | |||
| a0913e3f63 | |||
| f90955a9b9 | |||
| 3736c9229d | |||
| a802853367 | |||
| 96ca88fe88 | |||
| a57570210a | |||
| 7682e94b35 | |||
| 8bbebe113c | |||
| 7c921f827b | |||
| 4cc055f93a | |||
| e596a8b457 | |||
| fd2da55880 | |||
| 975e9b5643 | |||
| c0036e0474 | |||
| 103816e27a | |||
| b7c1684ab4 | |||
| 16bd6ca266 | |||
| 20bae4712b | |||
| a7aa80c690 | |||
| df89d1d58b | |||
| 477cddd29c | |||
| 9b8cf3a1b1 | |||
| 2871a3c07f | |||
| 13763296dd | |||
| 783b22ab1c | |||
| 109937adf4 | |||
| 63ea9cc4e0 | |||
| ec40a0c4c3 | |||
| 0855d1a378 | |||
| 77fe17d350 | |||
| 0b8a031ccb | |||
| 0678daa880 | |||
| 6761e3bdd8 | |||
| ead213a506 | |||
| 75b5782eee | |||
| a80df809e4 | |||
| 7f3177f662 | |||
| 906cd2fbbf | |||
| 9d0129da56 | |||
| aedcf12d99 | |||
| 86361523e2 | |||
| a7110ef441 | |||
| d3e4968546 | |||
| 03e34d56ab | |||
| b470d10416 | |||
| b8aea89039 | |||
| 4a1e89af1b | |||
| 81fbc94d36 | |||
| 6487671559 | |||
| 838c7f29b5 | |||
| df85a4c214 | |||
| 1bdbea238e | |||
| 97dbef92d9 | |||
| 85c1c0036c | |||
| 0653ee2c6b | |||
| d1db913c7d | |||
| d24b921cdc | |||
| b31b742787 | |||
| d820f0277f | |||
| 7219856177 | |||
| e6d302aabb | |||
| d73243ab60 | |||
| 8101e7d7b0 | |||
| 784c7e72ae | |||
| 93156fd2f1 | |||
| e4f6898498 | |||
| ef330135f9 | |||
| 54cbbdface | |||
| f3c9320837 | |||
| 0529629ac8 | |||
| 22af42ead6 | |||
| bd2efb68e1 | |||
| 37ba3d2efc | |||
| ed8afeaa87 | |||
| ee6efe5aa4 | |||
| 3a8113d8b0 | |||
| 7afce42943 | |||
| 67a6c25256 | |||
| 38dea8311c | |||
| 555e1f7924 | |||
| 8b87f63609 | |||
| f01b1d493f | |||
| a802245bf0 | |||
| 6dbbad158a | |||
| 877fe144b4 | |||
| 70e6bc0466 | |||
| 63f2e833eb | |||
| fb71abe534 | |||
| 8ccb39560e | |||
| e6b880be62 | |||
| d0016ac7c9 | |||
| 05035e0171 | |||
| 78b5bef879 | |||
| a6955b5db5 | |||
| a1a0a1b71e | |||
| 0bdde6d5fe | |||
| cf5447d5bd | |||
| b2dd2c205d | |||
| e52c9277c8 | |||
| 712ec2410d | |||
| dea2ca41d2 | |||
| ca0f32c02b | |||
| f21b296fba | |||
| 3224479b99 | |||
| f95950eedc | |||
| 4467376d0a | |||
| ac65dc5361 | |||
| 4957793c80 | |||
| ff7f4b502d | |||
| 816cb7188b | |||
| 6456d435eb | |||
| 63e338ed6f | |||
| 00211c8f03 | |||
| ebed9fe3aa | |||
| 734b5e7303 | |||
| 1696d501e2 | |||
| e65d2f8c50 | |||
| 9ea705b2ea | |||
| 5a5a811dca | |||
| df7b9419c2 | |||
| 37318f1106 | |||
| 19e9f6ac5d | |||
| 658b51a449 | |||
| 485303c0f2 | |||
| 885d902b7d | |||
| a35f02fb64 | |||
| 28d1f16ad5 | |||
| a04d7c3a9a | |||
| b876f8484c | |||
| 854c6d3d65 | |||
| f9a850a8fe | |||
| e808662fe7 | |||
| 7bbb02126e | |||
| aa101059a7 | |||
| d1f7fe02e4 | |||
| 3e26dc1373 | |||
| 0a9d819555 | |||
| b31dfeefb7 | |||
| fc640ec331 | |||
| 3382723457 | |||
| 1fc0722ad6 | |||
| b21e308357 | |||
| 738105314b | |||
| 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 | |||
| be56e92d65 | |||
| 69eb843604 | |||
| 84a7f0e90b | |||
| d1e105a29a | |||
| 9f0a568fa3 | |||
| 05b46cbe34 | |||
| c045586997 | |||
| 8f0707f697 | |||
| 36929b265c | |||
| 734ba64965 | |||
| 148e6742df | |||
| bcb7e8f4f3 | |||
| f678112099 | |||
| 60b0c5f256 | |||
| c8627939de | |||
| 9144f0158a | |||
| d541aca80f | |||
| c73b2b8d34 | |||
| e2493b489d | |||
| 8dee28ac7c | |||
| cdd3885a0c | |||
| 1a28d528d0 | |||
| 3ba12b8cee | |||
| 5a29ab6917 | |||
| 694144a0c8 | |||
| 8bed8e8741 | |||
| a81a348bce | |||
| fd9e8c5cbc | |||
| 8030b1919d | |||
| 72c789fdd7 | |||
| 1113a9aa0d | |||
| a5532614a2 | |||
| 122023fb70 | |||
| b8fa923ec9 | |||
| ae06b3e01a | |||
| 5599ec2809 | |||
| e795cbddb6 | |||
| 0cb087c37b | |||
| 983cbcc711 | |||
| 6d154b0c78 | |||
| f3f36e28c4 | |||
| fdf4797726 | |||
| 67d8a3be98 | |||
| 4001a60f6c | |||
| d94db41271 | |||
| 8abb78bb58 | |||
| a80db99aa3 | |||
| 69a300f21a | |||
| 1b024b8092 | |||
| a622689597 | |||
| 9943e66c49 | |||
| 7233c08281 | |||
| 0845d92fda | |||
| 1cc02e5a83 | |||
| aa4cd7a144 | |||
| b42ae0dfd7 | |||
| a6bd179726 | |||
| aac7b5117b | |||
| 8e8e99ed2e | |||
| 078ac23b20 | |||
| 8e61df6b6a | |||
| cf7fb56653 | |||
| da20d13c49 | |||
| 7a250aa8fc | |||
| af28ecb82d | |||
| 39a0e52a2a | |||
| a4f5a111c7 | |||
| c65e585493 | |||
| c9e94561aa | |||
| 1daa4c202b | |||
| 83ff361672 | |||
| 2c686f107d | |||
| ac8ec3d5ef | |||
| 21e70ef913 | |||
| c4f5b0e7c2 | |||
| 45d6c1389d | |||
| 0c9bc5a3af | |||
| 5b67d5a04a | |||
| f3396a5573 | |||
| e9f48788a3 | |||
| 6993a1ea46 | |||
| 8bcfb4585b | |||
| db45251f7f | |||
| 5582667b4f | |||
| 2c898aaf23 | |||
| e0999ffcdd | |||
| 03811768bb | |||
| fbef577c9f | |||
| 9434510ce9 | |||
| 354130c151 | |||
| 3e3cba016a | |||
| f75e120bef | |||
| 1d0294e430 | |||
| f786dd8254 | |||
| cd9d09fd53 | |||
| 7471bbcd4e | |||
| 43b04eccbd | |||
| 6a5d0b5e9f | |||
| 359d366de4 | |||
| 556d9f3a7b | |||
| 2cab2dcec0 | |||
| 99d4e78dc9 | |||
| 9aa99869ae | |||
| 08e0d87347 | |||
| 3f9e4057d3 | |||
| a29e40353c | |||
| 778cb2dd0f | |||
| f7d5514b94 | |||
| 954637f7b3 | |||
| 1ab46104c8 | |||
| 815776d473 | |||
| 8db1a7be90 | |||
| 7b11fa24dd | |||
| 1f0f2318d5 | |||
| 029b3e2a52 | |||
| 4fff823def | |||
| cab78275f4 | |||
| 5f60e4fedb | |||
| 96971a33a7 | |||
| 9a7409f521 | |||
| 80aa7e305b | |||
| 27d513cb01 | |||
| 9bf5cc8c03 | |||
| 7994b210cd | |||
| 46555bbe3f | |||
| 4d15dbc465 | |||
| 855d3c4320 | |||
| 4564862acc | |||
| 176dd70073 | |||
| a5e6f0c196 | |||
| 083bb5a96c | |||
| 04522281be | |||
| 0e8bb49b59 | |||
| 9abf6eea16 | |||
| 1d7a04ce7b | |||
| 49fb5792c3 | |||
| 5eebba09c5 | |||
| b86974688e | |||
| 74afe2ed13 | |||
| ed53a0b624 | |||
| 23e15d6459 | |||
| 71ea19d1c1 | |||
| fa621d076d | |||
| 4902f1328a | |||
| 2ee8ff484d | |||
| c872fe3c78 | |||
| a08b275463 | |||
| 9717208dd4 | |||
| c9a233f5e5 | |||
| 7389350ff9 | |||
| f46ac08cff | |||
| 296d5e7974 | |||
| fe0bea686c | |||
| 838d172512 | |||
| 2c02c51c37 | |||
| 67a4cbca2c | |||
| a2f97e727f | |||
| 462506113e | |||
| 5f2a72203f | |||
| d6febe2d02 | |||
| c2bd1e989a | |||
| f886c2c050 | |||
| ae770e603a | |||
| 7b79472d65 | |||
| 090a3a571b | |||
| f9d55fc425 | |||
| 4f57e8a5d1 | |||
| 1e6c9d935a | |||
| 00cfde169b | |||
| 02733ac718 | |||
| 55b55e62da | |||
| 5fccedd4c4 | |||
| b9ad78ec79 | |||
| 64ac6bcd1f | |||
| 45e4d80c4d | |||
| a5b1652d15 | |||
| f954eb7d88 | |||
| 53216813e5 | |||
| 1618203930 | |||
| 237a2ed426 | |||
| d33289503a | |||
| f5ff4c9725 | |||
| 62f932dcfc | |||
| b66112d0ca | |||
| b98354e63a | |||
| 94b3625718 | |||
| f7ee720281 | |||
| 4ab523bf01 | |||
| 2d4f1bfd02 | |||
| 38426c9143 | |||
| bdf151e0a7 | |||
| 9768b7888d | |||
| 740a48566f | |||
| 475cd1a106 | |||
| 38e7c39d69 | |||
| 774db6bead | |||
| a1a3e0412a | |||
| 0b39c89e60 | |||
| 53be4d8954 | |||
| 03812cc7eb | |||
| aa12b24293 | |||
| daf43009ba | |||
| 955d777ca5 | |||
| cc9472aa2f | |||
| e527f3cb1f | |||
| 3a375a8975 | |||
| 2698496592 | |||
| 0155d854e3 | |||
| c74cc8586f | |||
| 8eb89da9a0 | |||
| dee6ee3cef | |||
| beab89df09 | |||
| 5164d4ec32 | |||
| 878db851af | |||
| 686ff72ae0 | |||
| 2710d7098f | |||
| 7f41ff4035 | |||
| ed8d51014c | |||
| d09a51f47d | |||
| 59bae90454 | |||
| 13ee0ca94e | |||
| 5abc095050 | |||
| 7eb68c8388 | |||
| f69b644a77 | |||
| 6b93125ff2 | |||
| 43faef4569 | |||
| fe41d4c863 | |||
| 29830455ed | |||
| e50828093d | |||
| 880d29c5a9 | |||
| 77b2e9ba7a | |||
| 586fad7646 | |||
| fb636028fb | |||
| a8c3f8fc46 | |||
| 72f4227c5a | |||
| 8ccace8ef9 | |||
| 6d40c6dfe5 | |||
| 0b5562cdec | |||
| eeff0816f3 | |||
| f1f16dea3f | |||
| bfc6ef2049 | |||
| 5212de79d3 | |||
| b61c02e5df | |||
| f982954e8f | |||
| 3ba20e69ba | |||
| aea01fd893 | |||
| 950be14eca | |||
| 446deffc17 | |||
| e0863115ee | |||
| e34cb539d2 | |||
| d8ade8638a | |||
| 3067080474 | |||
| 886cc0f214 | |||
| 071d34b016 | |||
| a1564ca003 | |||
| 60f0e765c2 | |||
| 3f0ecea4bf | |||
| 2c9e6572c5 | |||
| 371a83f20f | |||
| b8cff1655a | |||
| 232856ca3a | |||
| 3f168ac6fd | |||
| c59cb1d0d3 | |||
| ec13df75d0 | |||
| 6fc02964ba | |||
| ed79e45680 | |||
| 1be983bf89 | |||
| b09d6a9d04 | |||
| db143d845d | |||
| 2e23501f9d | |||
| bd6addcd3a | |||
| 631e1fb604 | |||
| 30ee6726a8 | |||
| 1c397db9d8 | |||
| cc23ca80f4 | |||
| 449379a0ed | |||
| b3208b1c5b | |||
| 4df60b55a6 | |||
| 379553a1a5 | |||
| a2eaa5c7b5 | |||
| 175c46e68c | |||
| a58cc11079 | |||
| 218a375c27 | |||
| 567b1577c6 | |||
| 3c3687d11f | |||
| 19dfac8340 | |||
| b61feafe5a | |||
| 0c342c8b3e | |||
| dbcba8fad7 | |||
| b8053e20f2 | |||
| 1896901aa8 | |||
| 383c9132ed | |||
| 57b144c3e7 | |||
| eed5365fe0 | |||
| f5905568c4 | |||
| 096099470e | |||
| e7ed7aca3c | |||
| 6725b275b8 | |||
| 3447a7ef41 | |||
| 99f35fbea4 | |||
| 5c9a3912a9 | |||
| 5d43c0418c | |||
| 87c0076e12 | |||
| 95252ac697 | |||
| 5bb9f96701 | |||
| 750e9dfaa7 | |||
| f34f3c1661 | |||
| d4f83c978c | |||
| 212f280c19 | |||
| f3e2450636 | |||
| d6d496018d | |||
| 78be7fc772 | |||
| 6ebadd8469 | |||
| 557750c8d4 | |||
| e85ef27e6c | |||
| 4ca961a1b4 | |||
| 6a9110e9c1 | |||
| 51ffce09ae | |||
| 1c4e96b365 | |||
| 0db70e8edd | |||
| e46b3a5e19 | |||
| fdd3d4d85a | |||
| 37c9cba42e | |||
| c1544f66bb | |||
| d37f41f6a5 | |||
| b245dd2d51 | |||
| a1fcf11399 | |||
| 8f876da245 | |||
| 23b8e5a2b3 | |||
| 3b7e7c7192 | |||
| b7ecf6e2e0 | |||
| 2ec6aaff03 | |||
| 19f8553f2d | |||
| 05a64ff095 | |||
| a8fc78fcda | |||
| e0e8b40fa2 | |||
| 00165cd6ca | |||
| cd799ddfcd | |||
| fffd6b7c86 | |||
| 439b008a34 | |||
| f38e538892 | |||
| 6aa87a073f | |||
| c38198ccba | |||
| 3be88c8cbf | |||
| 558ced1afb | |||
| 0149e6935d | |||
| d97fdfd7c4 | |||
| 8b85d8c6fb | |||
| 673779490c | |||
| 48154e7e2d | |||
| 20f72b3f63 | |||
| e82c958af2 | |||
| 60c311ab9f | |||
| fbac81c245 | |||
| 9ca67d9228 | |||
| 5ffa18221f | |||
| aceb1f0f61 | |||
| cee5ca8873 | |||
| d961d4ab43 | |||
| 5205150a89 | |||
| 48e58cde5d | |||
| 033e91f8df | |||
| aab3705897 | |||
| d02efa81f2 | |||
| 95a8240da7 | |||
| dd0ddab610 | |||
| d23ac10f90 | |||
| ec18290b8a | |||
| 2c4cd39dc9 | |||
| 830bad0b85 | |||
| f14ef6fa15 | |||
| 7400b1c83d | |||
| e7caf39fba | |||
| 09fd0fb0ca | |||
| 72adb13c0f | |||
| ea0e382f82 | |||
| e70cba5143 | |||
| 8aec244c31 | |||
| 60e163164f | |||
| 86b9b5f3fa | |||
| 401a208767 | |||
| a1bfbda05b | |||
| 7d1f991ce4 | |||
| 1b10378f58 | |||
| 2bbb379994 | |||
| a835f119e1 | |||
| 91d8bac680 | |||
| 3db10a4ce8 | |||
| 590640645b | |||
| 7f02bfdf0c | |||
| e5cef0d9c0 | |||
| 85f9c33b2b | |||
| 148a430da4 | |||
| f7657679ac | |||
| f0479019c3 | |||
| a9a4ceaa78 | |||
| c55c905621 | |||
| 4db2289b7e | |||
| 93172ea1d0 | |||
| 2d935542e1 | |||
| f309ad7746 | |||
| a7ec1364f4 | |||
| eb71ced092 | |||
| 712ad0a73b | |||
| 48c0b137d5 | |||
| dfccfcc3e5 | |||
| 6abe667efb | |||
| c2472215ab | |||
| ac3c1e149c | |||
| cdf989427a | |||
| ebf129edd3 | |||
| 08c30f4baf | |||
| cf6bdc20ef | |||
| 3ece644af8 | |||
| 3991c82c91 | |||
| 9b635253f0 | |||
| b62f41336e | |||
| f7b777c79e | |||
| d18fa8e42a | |||
| 525c62ad26 | |||
| 4000a6a48c | |||
| 5b173ed4c4 | |||
| f56ad73565 | |||
| 003991c8c6 | |||
| e2a32afb80 | |||
| f305a69bb3 | |||
| 84e8babd9e | |||
| aeb46d9b54 | |||
| fafe0bd8e4 | |||
| 9a2ab45957 | |||
| 66978a8cdc | |||
| 1636012700 | |||
| 09206ae1e4 | |||
| 9188475746 | |||
| 34d158a632 | |||
| c06e6aa5ca | |||
| f4f670f048 | |||
| 778d742b6e | |||
| c8392b65b6 | |||
| c0ace9c2e5 | |||
| dfcab7dcbf | |||
| eb0870deb1 | |||
| 5b7ef34523 | |||
| 6ec728e466 | |||
| f12a562a08 | |||
| 17c4c95593 | |||
| 9b72c90944 | |||
| ec34da60a1 | |||
| daa4b6368a | |||
| 931a7a1a6c | |||
| 69d5790078 | |||
| 7571c18a55 | |||
| ff7ce9bdd0 | |||
| e5fc801899 | |||
| b362aa6813 | |||
| 652b961ac8 | |||
| 652713aec4 | |||
| 387b2f166b | |||
| 164b4a056a | |||
| 29e514fea6 | |||
| 310fff78c6 | |||
| f2efdc007c | |||
| b3be767923 | |||
| e86f2f3873 | |||
| 13d84f73d4 | |||
| e31342d3ba | |||
| daf0538bf3 | |||
| 451ce8b0c7 | |||
| b8cce14705 | |||
| bf1c9c650e | |||
| 8f6387536c | |||
| 56535ece11 | |||
| f1767719cb | |||
| c925b06114 | |||
| 402426884d | |||
| df6c8a5a75 | |||
| 99f5ae7125 | |||
| d50a1b7d07 | |||
| fab3bb76f7 | |||
| 5025c66bb2 | |||
| 800c153e96 | |||
| 71bbda0fb7 | |||
| 6e6bac429a | |||
| 1ce091a4d9 | |||
| a8f889be74 | |||
| 5f33c6bfee | |||
| 6a290c49d8 | |||
| b304d5d784 | |||
| cfe83b97d9 | |||
| 2fec2bf560 | |||
| 73dc1a7839 | |||
| 66fe951831 | |||
| 7991bcbf1a | |||
| de9516563a | |||
| 27fefb821c | |||
| c195894db9 | |||
| 6777b4d370 | |||
| 09269c22a2 | |||
| 2e24a2f079 | |||
| 5d9932dd61 | |||
| 062064213a | |||
| a2ae3ffb2b | |||
| 6cb4a0a3eb | |||
| f17c49091f | |||
| c16afc07df | |||
| 1616a96b2c | |||
| 261601230a | |||
| 453a38df54 | |||
| 5b004a849f | |||
| 29d811d3fd | |||
| 36c5739318 | |||
| b3f9c67d34 | |||
| bc8eb802f7 | |||
| a138eead74 | |||
| a700a0e1b1 | |||
| 205a33a241 | |||
| c88fd94c8b | |||
| a2b4e2e87c | |||
| 4a8f1e95ba | |||
| 3a847d921e | |||
| 806fdb9541 | |||
| cf1adbdb01 | |||
| 349d08e799 | |||
| d680c7ed83 | |||
| d4cb7a711b | |||
| bb6e19e7cd | |||
| 1c3ea53e63 | |||
| 88e17029c5 | |||
| 588e91b19f | |||
| 8cc2e7b6f1 | |||
| 222353b532 | |||
| b88b266fd5 | |||
| 60e6fb99af | |||
| 65b60e57b2 | |||
| 16a8402bf4 | |||
| 5896411136 | |||
| 0bb74a7885 | |||
| 86dfb9231f | |||
| 7198ce3eb0 | |||
| 08fecf1eb2 | |||
| 3eda26ca94 | |||
| d907914c7c | |||
| 266ab48fed | |||
| 3325cffa91 | |||
| 43469ac62a | |||
| a5c953fdb6 | |||
| 627c46e458 | |||
| 205eb34adc | |||
| 125e14d377 | |||
| a51c8a700b | |||
| 94e0400ea1 | |||
| 47c5b84093 | |||
| 8b1fbfd16d | |||
| cceb698899 | |||
| 01741df10d | |||
| f91ebf8baa | |||
| 4dde076030 | |||
| 3491001b7f | |||
| 2acec68649 | |||
| 51dab27374 | |||
| 145f5041bf | |||
| 6034505380 | |||
| 8533d74906 | |||
| b2ae57b982 | |||
| 49ffe9bec9 | |||
| fe5d92674e | |||
| 197d28f5c7 | |||
| cd48bb0789 | |||
| 90fc411e9a | |||
| c22b6a84aa | |||
| 9b65642f05 | |||
| 83547dce9c | |||
| efeecceb54 | |||
| ba9b5a40d2 | |||
| 47b5bda277 | |||
| a343b6b1b6 | |||
| 0fe48d3003 | |||
| 23e3760b08 | |||
| 3d31905562 | |||
| 9638c5266b | |||
| ad7ce9f55a | |||
| b0baf3b85a | |||
| d4d3687882 | |||
| faf55ca191 | |||
| d5096a23fb | |||
| ed5841d201 | |||
| bbfc095a00 | |||
| 0fcb68a13d | |||
| f97744c098 | |||
| d1cfa8d27a | |||
| 218dcf25c1 | |||
| 06e06973a4 | |||
| 6f73cfc5f2 | |||
| 6db5bbeaee | |||
| 6ef5077164 | |||
| 45e1ed7022 | |||
| c14b4535a6 | |||
| 411631d2f8 | |||
| f4c3690bd8 | |||
| 56fdea6b5d | |||
| 8a5c053d39 | |||
| 42870cfa23 | |||
| 6cf256cc05 | |||
| 9fec915f62 | |||
| f1d5ab73cd | |||
| cd62972945 | |||
| 998d09170c | |||
| 4ba57181ec | |||
| 8b9d8bdc62 | |||
| 4291d42dc0 | |||
| 79fcc1ce40 | |||
| bfc6778dca | |||
| 701e57c264 | |||
| 163d025c0d | |||
| d9befc6d8c | |||
| 9e50a4c241 | |||
| 9b0cae3794 | |||
| 6160dfb2f7 | |||
| cd013cdb06 | |||
| 26cc7c90e9 | |||
| f28ac3cf22 | |||
| 58fec4b082 | |||
| b91805a5df | |||
| 0fa0df1bdf | |||
| 3f7cacee3e | |||
| 72637fd650 | |||
| aba1284f8e | |||
| 179e1dc9e5 | |||
| 75879a494e | |||
| 73b1ea4713 | |||
| 55dc991c13 | |||
| c30316588a | |||
| db5d6e7481 | |||
| f8d52f58d4 | |||
| 227ee499e4 | |||
| dcdaf6a674 | |||
| d524ba3a37 | |||
| da5e288476 | |||
| baad7cd60d | |||
| e9d6fc33fd | |||
| c2fa0899e9 | |||
| 2dc09ec1f2 | |||
| fba640976f | |||
| 8e7df61a73 | |||
| 41776cf2df | |||
| 23983f0b75 | |||
| 84b457ede5 | |||
| a906e0bf0c | |||
| 3db1aad96a | |||
| 9c909e7a2c | |||
| ad2ef7cb33 | |||
| c851510ca9 | |||
| 71a21c2059 | |||
| d90537eb8d | |||
| 25e9888438 |
@@ -0,0 +1,16 @@
|
||||
# To get started with Dependabot version updates, you'll need to specify which
|
||||
# package ecosystems to update and where the package manifests are located.
|
||||
# Please see the documentation for all configuration options:
|
||||
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
|
||||
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "pip"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "monthly"
|
||||
@@ -0,0 +1,41 @@
|
||||
name: "Tests"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
pull_request: ~
|
||||
workflow_dispatch:
|
||||
|
||||
# Cancel redundant in-progress jobs.
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: "Python ${{ matrix.python-version }} on ${{ matrix.os }}"
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [
|
||||
"ubuntu-latest",
|
||||
"macos-12",
|
||||
"macos-latest",
|
||||
]
|
||||
python-version: [
|
||||
"3.10",
|
||||
"3.11",
|
||||
"3.12",
|
||||
"3.13",
|
||||
"pypy3.10",
|
||||
]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- uses: yezz123/setup-uv@v4
|
||||
- run: uv pip install --editable '.[graphql,develop,test]' --system
|
||||
- run: poe check
|
||||
+14
@@ -1,4 +1,18 @@
|
||||
.venv*
|
||||
.vscode/
|
||||
.cache
|
||||
.idea
|
||||
.python-version
|
||||
.coverage
|
||||
.pytest_cache
|
||||
.DS_Store
|
||||
coverage.xml
|
||||
|
||||
__pycache__
|
||||
tests/__pycache__
|
||||
|
||||
build
|
||||
responder.egg-info/
|
||||
dist/
|
||||
app.py
|
||||
app2.py
|
||||
|
||||
-13
@@ -1,13 +0,0 @@
|
||||
language: python
|
||||
python:
|
||||
- "3.6"
|
||||
- "3.7"
|
||||
|
||||
# command to install dependencies
|
||||
install:
|
||||
- "pip install pipenv --upgrade-strategy=only-if-needed"
|
||||
- "pipenv install --dev"
|
||||
|
||||
# command to run the dependencies
|
||||
script:
|
||||
- "pytest"
|
||||
+381
@@ -0,0 +1,381 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and
|
||||
this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [v2.0.5] - 2019-12-15
|
||||
|
||||
### Added
|
||||
|
||||
- Update requirements to support python 3.8
|
||||
|
||||
## [v2.0.4] - 2019-11-19
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix static app resolving
|
||||
|
||||
## [v2.0.3] - 2019-09-20
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix template conflicts
|
||||
|
||||
## [v2.0.2] - 2019-09-20
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix template conflicts
|
||||
|
||||
## [v2.0.1] - 2019-09-20
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix template import
|
||||
|
||||
## [v2.0.0] - 2019-09-19
|
||||
|
||||
### Changed
|
||||
|
||||
- Refactor Router and Schema
|
||||
|
||||
## [v1.3.2] - 2019-08-15
|
||||
|
||||
### Added
|
||||
|
||||
- ASGI 3 support
|
||||
- CI tests for python 3.8-dev
|
||||
- Now requests have `state` a mapping object
|
||||
|
||||
### Deprecated
|
||||
|
||||
- ASGI 2
|
||||
|
||||
## [v1.3.1] - 2019-04-28
|
||||
|
||||
### Added
|
||||
|
||||
- Route params Converters
|
||||
- Add search for documentation pages
|
||||
|
||||
### Changed
|
||||
|
||||
- Bump dependencies
|
||||
|
||||
## [v1.3.0] - 2019-02-22
|
||||
|
||||
### Fixed
|
||||
|
||||
- Versioning issue
|
||||
- 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.3] - 2019-01-12
|
||||
|
||||
### Changed
|
||||
|
||||
- Refactor `_route_for`
|
||||
|
||||
### Fixed
|
||||
|
||||
- Resolve startup/shutdwown events
|
||||
|
||||
## [v1.2.0] - 2018-12-29
|
||||
|
||||
### Added
|
||||
|
||||
- Documentations
|
||||
|
||||
### Changed
|
||||
|
||||
- Use Starlette's LifeSpan middleware
|
||||
- Update denpendencies
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix route.is_class_based
|
||||
- Fix test_500
|
||||
- Typos
|
||||
|
||||
## [v1.1.2] - 2018-11-11
|
||||
|
||||
### Fixed
|
||||
|
||||
- Minor fixes for Open API
|
||||
- Typos
|
||||
|
||||
## [v1.1.1] - 2018-10-29
|
||||
|
||||
### Changed
|
||||
|
||||
- Run sync views in a threadpoolexecutor.
|
||||
|
||||
## [v1.1.0] - 2018-10-27
|
||||
|
||||
### Added
|
||||
|
||||
- Support for `before_request`.
|
||||
|
||||
## [v1.0.5]- 2018-10-27
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix sessions.
|
||||
|
||||
## [v1.0.4] - 2018-10-27
|
||||
|
||||
### Fixed
|
||||
|
||||
- Potential bufix for cookies.
|
||||
|
||||
## [v1.0.3] - 2018-10-27
|
||||
|
||||
### Fixed
|
||||
|
||||
- Bugfix for redirects.
|
||||
|
||||
## [v1.0.2] - 2018-10-27
|
||||
|
||||
### Changed
|
||||
|
||||
- Improvement for static file hosting.
|
||||
|
||||
## [v1.0.1] - 2018-10-26
|
||||
|
||||
### Changed
|
||||
|
||||
- Improve cors configuration settings.
|
||||
|
||||
## [v1.0.0] - 2018-10-26
|
||||
|
||||
### Changed
|
||||
|
||||
- Move GraphQL support into a built-in plugin.
|
||||
|
||||
## [v0.3.3] - 2018-10-25
|
||||
|
||||
### Added
|
||||
|
||||
- CORS support
|
||||
|
||||
### Changed
|
||||
|
||||
- Improved exceptions.
|
||||
|
||||
## [v0.3.2] - 2018-10-25
|
||||
|
||||
### Changed
|
||||
|
||||
- Subtle improvements.
|
||||
|
||||
## [v0.3.1] - 2018-10-24
|
||||
|
||||
### Fixed
|
||||
|
||||
- Packaging fix.
|
||||
|
||||
## [v0.3.0] - 2018-10-24
|
||||
|
||||
### Changed
|
||||
|
||||
- Interactive Documentation endpoint.
|
||||
- Minor improvements.
|
||||
|
||||
## [v0.2.3] - 2018-10-24
|
||||
|
||||
### Changed
|
||||
|
||||
- Overall improvements.
|
||||
|
||||
## [v0.2.2] - 2018-10-23
|
||||
|
||||
### Added
|
||||
|
||||
- Show traceback info when background tasks raise exceptions.
|
||||
|
||||
## [v0.2.1] - 2018-10-23
|
||||
|
||||
### Added
|
||||
|
||||
- api.requests.
|
||||
|
||||
## [v0.2.0] - 2018-10-22
|
||||
|
||||
### Added
|
||||
|
||||
- WebSocket support.
|
||||
|
||||
## [v0.1.6] - 2018-10-20
|
||||
|
||||
### Added
|
||||
|
||||
- 500 support.
|
||||
|
||||
## [v0.1.5] - 2018-10-20
|
||||
|
||||
### Added
|
||||
|
||||
- File upload support
|
||||
|
||||
### Changed
|
||||
|
||||
- Improvements to sequential media reading.
|
||||
|
||||
## [v0.1.4] - 2018-10-19
|
||||
|
||||
### Fixed
|
||||
|
||||
- Stability.
|
||||
|
||||
## [v0.1.3] - 2018-10-18
|
||||
|
||||
### Added
|
||||
|
||||
- Sessions support.
|
||||
|
||||
## [v0.1.2] - 2018-10-18
|
||||
|
||||
### Added
|
||||
|
||||
- Cookies support.
|
||||
|
||||
## [v0.1.1] - 2018-10-17
|
||||
|
||||
### Changed
|
||||
|
||||
- Default routes.
|
||||
|
||||
## [v0.1.0] - 2018-10-17
|
||||
|
||||
### Added
|
||||
|
||||
- Prototype of static application support.
|
||||
|
||||
## [v0.0.10] - 2018-10-17
|
||||
|
||||
### Fixed
|
||||
|
||||
- Bugfix for async class-based views.
|
||||
|
||||
## [v0.0.9] - 2018-10-17
|
||||
|
||||
### Fixed
|
||||
|
||||
- Bugfix for async class-based views.
|
||||
|
||||
## [v0.0.8] - 2018-10-17
|
||||
|
||||
### Added
|
||||
|
||||
- GraphiQL Support.
|
||||
|
||||
### Changed
|
||||
|
||||
- Improvement to route selection.
|
||||
|
||||
## [v0.0.7] - 2018-10-16
|
||||
|
||||
### Changed
|
||||
|
||||
- Immutable Request object.
|
||||
|
||||
## [v0.0.6] - 2018-10-16
|
||||
|
||||
### Added
|
||||
|
||||
- Ability to mount WSGI apps.
|
||||
- Supply content-type when serving up the schema.
|
||||
|
||||
## [v0.0.5] - 2018-10-15
|
||||
|
||||
### Added
|
||||
|
||||
- OpenAPI Schema support.
|
||||
- Safe load/dump yaml.
|
||||
|
||||
## [v0.0.4] - 2018-10-15
|
||||
|
||||
### Added
|
||||
|
||||
- Asynchronous support for data uploads.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Bug fixes.
|
||||
|
||||
## [v0.0.3] - 2018-10-13
|
||||
|
||||
### Fixed
|
||||
|
||||
- Bug fixes.
|
||||
|
||||
## [v0.0.2] - 2018-10-13
|
||||
|
||||
### Changed
|
||||
|
||||
- Switch to ASGI/Starlette.
|
||||
|
||||
## [v0.0.1] - 2018-10-12
|
||||
|
||||
### Added
|
||||
|
||||
- Conception!
|
||||
|
||||
[unreleased]: https://github.com/taoufik07/responder/compare/v2.0.5..HEAD
|
||||
[v2.0.5]: https://github.com/taoufik07/responder/compare/v2.0.4..v2.0.5
|
||||
[v2.0.4]: https://github.com/taoufik07/responder/compare/v2.0.3..v2.0.4
|
||||
[v2.0.3]: https://github.com/taoufik07/responder/compare/v2.0.2..v2.0.3
|
||||
[v2.0.2]: https://github.com/taoufik07/responder/compare/v2.0.1..v2.0.2
|
||||
[v2.0.1]: https://github.com/taoufik07/responder/compare/v2.0.0..v2.0.1
|
||||
[v2.0.0]: https://github.com/taoufik07/responder/compare/v1.3.2..v2.0.0
|
||||
[v1.3.2]: https://github.com/taoufik07/responder/compare/v1.3.1..v1.3.2
|
||||
[v1.3.1]: https://github.com/taoufik07/responder/compare/v1.3.0..v1.3.1
|
||||
[v1.3.0]: https://github.com/taoufik07/responder/compare/v1.2.0..v1.3.0
|
||||
[v1.2.0]: https://github.com/taoufik07/responder/compare/v1.1.3..v1.2.0
|
||||
[v1.1.3]: https://github.com/taoufik07/responder/compare/v1.1.2..v1.1.3
|
||||
[v1.1.2]: https://github.com/taoufik07/responder/compare/v1.1.1..v1.1.2
|
||||
[v1.1.1]: https://github.com/taoufik07/responder/compare/v1.1.0..v1.1.1
|
||||
[v1.1.0]: https://github.com/taoufik07/responder/compare/v1.0.5..v1.1.0
|
||||
[v1.0.5]: https://github.com/taoufik07/responder/compare/v1.0.4..v1.0.5
|
||||
[v1.0.4]: https://github.com/taoufik07/responder/compare/v1.0.3..v1.0.4
|
||||
[v1.0.3]: https://github.com/taoufik07/responder/compare/v1.0.2..v1.0.3
|
||||
[v1.0.2]: https://github.com/taoufik07/responder/compare/v1.0.1..v1.0.2
|
||||
[v1.0.1]: https://github.com/taoufik07/responder/compare/v1.0.0..v1.0.1
|
||||
[v1.0.0]: https://github.com/taoufik07/responder/compare/v0.3.3..v1.0.0
|
||||
[v0.3.3]: https://github.com/taoufik07/responder/compare/v0.3.2..v0.3.3
|
||||
[v0.3.2]: https://github.com/taoufik07/responder/compare/v0.3.1..v0.3.2
|
||||
[v0.3.1]: https://github.com/taoufik07/responder/compare/v0.3.0..v0.3.1
|
||||
[v0.3.0]: https://github.com/taoufik07/responder/compare/v0.2.3..v0.3.0
|
||||
[v0.2.3]: https://github.com/taoufik07/responder/compare/v0.2.2..v0.2.3
|
||||
[v0.2.2]: https://github.com/taoufik07/responder/compare/v0.2.1..v0.2.2
|
||||
[v0.2.1]: https://github.com/taoufik07/responder/compare/v0.2.0..v0.2.1
|
||||
[v0.2.0]: https://github.com/taoufik07/responder/compare/v0.1.6..v0.2.0
|
||||
[v0.1.6]: https://github.com/taoufik07/responder/compare/v0.1.5..v0.1.6
|
||||
[v0.1.5]: https://github.com/taoufik07/responder/compare/v0.1.4..v0.1.5
|
||||
[v0.1.4]: https://github.com/taoufik07/responder/compare/v0.1.3..v0.1.4
|
||||
[v0.1.3]: https://github.com/taoufik07/responder/compare/v0.1.2..v0.1.3
|
||||
[v0.1.2]: https://github.com/taoufik07/responder/compare/v0.1.1..v0.1.2
|
||||
[v0.1.1]: https://github.com/taoufik07/responder/compare/v0.1.0..v0.1.1
|
||||
[v0.1.0]: https://github.com/taoufik07/responder/compare/v0.0.10..v0.1.0
|
||||
[v0.0.10]: https://github.com/taoufik07/responder/compare/v0.0.9..v0.0.10
|
||||
[v0.0.9]: https://github.com/taoufik07/responder/compare/v0.0.8..v0.0.9
|
||||
[v0.0.8]: https://github.com/taoufik07/responder/compare/v0.0.7..v0.0.8
|
||||
[v0.0.7]: https://github.com/taoufik07/responder/compare/v0.0.6..v0.0.7
|
||||
[v0.0.6]: https://github.com/taoufik07/responder/compare/v0.0.5..v0.0.6
|
||||
[v0.0.5]: https://github.com/taoufik07/responder/compare/v0.0.4..v0.0.5
|
||||
[v0.0.4]: https://github.com/taoufik07/responder/compare/v0.0.3..v0.0.4
|
||||
[v0.0.3]: https://github.com/taoufik07/responder/compare/v0.0.2..v0.0.3
|
||||
[v0.0.2]: https://github.com/taoufik07/responder/compare/v0.0.1..v0.0.2
|
||||
[v0.0.1]: https://github.com/taoufik07/responder/compare/v0.0.0..v0.0.1
|
||||
@@ -1,4 +0,0 @@
|
||||
- Kenneth Reitz (primary)
|
||||
- Tom Christie
|
||||
- Bruno Oliveira
|
||||
- serhii73
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
# Development Sandbox
|
||||
|
||||
## Setup
|
||||
|
||||
Acquire sources and install project in editable mode.
|
||||
```shell
|
||||
git clone https://github.com/kennethreitz/responder
|
||||
cd responder
|
||||
python3 -m venv .venv
|
||||
source .venv/bin/activate
|
||||
pip install --editable '.[graphql,develop,release,test]'
|
||||
```
|
||||
|
||||
## Operations
|
||||
|
||||
Invoke linter and software tests.
|
||||
```shell
|
||||
poe check
|
||||
```
|
||||
|
||||
Format code.
|
||||
```shell
|
||||
poe format
|
||||
```
|
||||
|
||||
|
||||
## Release
|
||||
|
||||
```shell
|
||||
git tag v2.1.0
|
||||
git push --tags
|
||||
poe release
|
||||
```
|
||||
@@ -0,0 +1,178 @@
|
||||
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
@@ -0,0 +1 @@
|
||||
include LICENSE
|
||||
@@ -1,22 +0,0 @@
|
||||
[[source]]
|
||||
url = "https://pypi.org/simple"
|
||||
verify_ssl = true
|
||||
name = "pypi"
|
||||
|
||||
[packages]
|
||||
responder = {editable = true, path = "."}
|
||||
uvicorn = "*"
|
||||
|
||||
[dev-packages]
|
||||
pytest = "*"
|
||||
"flake8" = "*"
|
||||
black = "*"
|
||||
twine = "*"
|
||||
flask = "*"
|
||||
sphinx = "*"
|
||||
|
||||
[requires]
|
||||
python_version = "3.7"
|
||||
|
||||
[pipenv]
|
||||
allow_prereleases = true
|
||||
Generated
-615
@@ -1,615 +0,0 @@
|
||||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "76d2978ee90d2c028b13c9a5abdd2371d74d514045d50fb9b92aec44e72054b3"
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {
|
||||
"python_version": "3.7"
|
||||
},
|
||||
"sources": [
|
||||
{
|
||||
"name": "pypi",
|
||||
"url": "https://pypi.org/simple",
|
||||
"verify_ssl": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"default": {
|
||||
"aniso8601": {
|
||||
"hashes": [
|
||||
"sha256:7849749cf00ae0680ad2bdfe4419c7a662bef19c03691a19e008c8b9a5267802",
|
||||
"sha256:94f90871fcd314a458a3d4eca1c84448efbd200e86f55fe4c733c7a40149ef50"
|
||||
],
|
||||
"version": "==3.0.2"
|
||||
},
|
||||
"certifi": {
|
||||
"hashes": [
|
||||
"sha256:376690d6f16d32f9d1fe8932551d80b23e9d393a8578c5633a2ed39a64861638",
|
||||
"sha256:456048c7e371c089d0a77a5212fb37a2c2dce1e24146e3b7e0261736aaeaa22a"
|
||||
],
|
||||
"version": "==2018.8.24"
|
||||
},
|
||||
"chardet": {
|
||||
"hashes": [
|
||||
"sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae",
|
||||
"sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"
|
||||
],
|
||||
"version": "==3.0.4"
|
||||
},
|
||||
"click": {
|
||||
"hashes": [
|
||||
"sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13",
|
||||
"sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7"
|
||||
],
|
||||
"version": "==7.0"
|
||||
},
|
||||
"graphene": {
|
||||
"hashes": [
|
||||
"sha256:b8ec446d17fa68721636eaad3d6adc1a378cb6323e219814c8f98c9928fc9642",
|
||||
"sha256:faa26573b598b22ffd274e2fd7a4c52efa405dcca96e01a62239482246248aa3"
|
||||
],
|
||||
"version": "==2.1.3"
|
||||
},
|
||||
"graphql-core": {
|
||||
"hashes": [
|
||||
"sha256:889e869be5574d02af77baf1f30b5db9ca2959f1c9f5be7b2863ead5a3ec6181",
|
||||
"sha256:9462e22e32c7f03b667373ec0a84d95fba10e8ce2ead08f29fbddc63b671b0c1"
|
||||
],
|
||||
"version": "==2.1"
|
||||
},
|
||||
"graphql-relay": {
|
||||
"hashes": [
|
||||
"sha256:2716b7245d97091af21abf096fabafac576905096d21ba7118fba722596f65db"
|
||||
],
|
||||
"version": "==0.4.5"
|
||||
},
|
||||
"graphql-server-core": {
|
||||
"hashes": [
|
||||
"sha256:e5f82add4b3d5580aa1f1e7d9f00e944ad3abe1b65eb337e611d6a77cc20f231"
|
||||
],
|
||||
"version": "==1.1.1"
|
||||
},
|
||||
"h11": {
|
||||
"hashes": [
|
||||
"sha256:acca6a44cb52a32ab442b1779adf0875c443c689e9e028f8d831a3769f9c5208",
|
||||
"sha256:f2b1ca39bfed357d1f19ac732913d5f9faa54a5062eca7d2ec3a916cfb7ae4c7"
|
||||
],
|
||||
"version": "==0.8.1"
|
||||
},
|
||||
"idna": {
|
||||
"hashes": [
|
||||
"sha256:156a6814fb5ac1fc6850fb002e0852d56c0c8d2531923a51032d1b70760e186e",
|
||||
"sha256:684a38a6f903c1d71d6d5fac066b58d7768af4de2b832e426ec79c30daa94a16"
|
||||
],
|
||||
"version": "==2.7"
|
||||
},
|
||||
"jinja2": {
|
||||
"hashes": [
|
||||
"sha256:74c935a1b8bb9a3947c50a54766a969d4846290e1e788ea44c1392163723c3bd",
|
||||
"sha256:f84be1bb0040caca4cea721fcbbbbd61f9be9464ca236387158b0feea01914a4"
|
||||
],
|
||||
"version": "==2.10"
|
||||
},
|
||||
"markupsafe": {
|
||||
"hashes": [
|
||||
"sha256:a6be69091dac236ea9c6bc7d012beab42010fa914c459791d627dad4910eb665"
|
||||
],
|
||||
"version": "==1.0"
|
||||
},
|
||||
"parse": {
|
||||
"hashes": [
|
||||
"sha256:9dd6048ea212cd032a342f9f6aa2b7bc222f7407c7e37bdc2777fecd36897437"
|
||||
],
|
||||
"version": "==1.9.0"
|
||||
},
|
||||
"promise": {
|
||||
"hashes": [
|
||||
"sha256:2ebbfc10b7abf6354403ed785fe4f04b9dfd421eb1a474ac8d187022228332af",
|
||||
"sha256:348f5f6c3edd4fd47c9cd65aed03ac1b31136d375aa63871a57d3e444c85655c"
|
||||
],
|
||||
"version": "==2.2.1"
|
||||
},
|
||||
"pyyaml": {
|
||||
"hashes": [
|
||||
"sha256:254bf6fda2b7c651837acb2c718e213df29d531eebf00edb54743d10bcb694eb",
|
||||
"sha256:3108529b78577327d15eec243f0ff348a0640b0c3478d67ad7f5648f93bac3e2",
|
||||
"sha256:3c17fb92c8ba2f525e4b5f7941d850e7a48c3a59b32d331e2502a3cdc6648e76",
|
||||
"sha256:8d6d96001aa7f0a6a4a95e8143225b5d06e41b1131044913fecb8f85a125714b",
|
||||
"sha256:c8a88edd93ee29ede719080b2be6cb2333dfee1dccba213b422a9c8e97f2967b"
|
||||
],
|
||||
"version": "==4.2b4"
|
||||
},
|
||||
"requests": {
|
||||
"hashes": [
|
||||
"sha256:63b52e3c866428a224f97cab011de738c36aec0185aa91cfacd418b5d58911d1",
|
||||
"sha256:ec22d826a36ed72a7358ff3fe56cbd4ba69dd7a6718ffd450ff0e9df7a47ce6a"
|
||||
],
|
||||
"version": "==2.19.1"
|
||||
},
|
||||
"requests-wsgi-adapter": {
|
||||
"hashes": [
|
||||
"sha256:7080c98ae2614b8d0b7339b611d97a535470d2fb479731f7d588d5f8108ea134"
|
||||
],
|
||||
"version": "==0.4.0"
|
||||
},
|
||||
"responder": {
|
||||
"editable": true,
|
||||
"path": "."
|
||||
},
|
||||
"rx": {
|
||||
"hashes": [
|
||||
"sha256:13a1d8d9e252625c173dc795471e614eadfe1cf40ffc684e08b8fff0d9748c23",
|
||||
"sha256:7357592bc7e881a95e0c2013b73326f704953301ab551fbc8133a6fadab84105"
|
||||
],
|
||||
"version": "==1.6.1"
|
||||
},
|
||||
"six": {
|
||||
"hashes": [
|
||||
"sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9",
|
||||
"sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb"
|
||||
],
|
||||
"version": "==1.11.0"
|
||||
},
|
||||
"urllib3": {
|
||||
"hashes": [
|
||||
"sha256:a68ac5e15e76e7e5dd2b8f94007233e01effe3e50e8daddf69acfd81cb686baf",
|
||||
"sha256:b5725a0bd4ba422ab0e66e89e030c806576753ea3ee08554382c14e685d117b5"
|
||||
],
|
||||
"version": "==1.23"
|
||||
},
|
||||
"uvicorn": {
|
||||
"hashes": [
|
||||
"sha256:8de03999a936d8704f07cc3b1d3a3edb6922a068b64d84b4f5e49604c8b70a11"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.3.12"
|
||||
},
|
||||
"waitress": {
|
||||
"hashes": [
|
||||
"sha256:40b0f297a7f3af61fbfbdc67e59090c70dc150a1601c39ecc9f5f1d283fb931b",
|
||||
"sha256:d33cd3d62426c0f1b3cd84ee3d65779c7003aae3fc060dee60524d10a57f05a9"
|
||||
],
|
||||
"version": "==1.1.0"
|
||||
},
|
||||
"websockets": {
|
||||
"hashes": [
|
||||
"sha256:0e2f7d6567838369af074f0ef4d0b802d19fa1fee135d864acc656ceefa33136",
|
||||
"sha256:2a16dac282b2fdae75178d0ed3d5b9bc3258dabfae50196cbb30578d84b6f6a6",
|
||||
"sha256:5a1fa6072405648cb5b3688e9ed3b94be683ce4a4e5723e6f5d34859dee495c1",
|
||||
"sha256:5c1f55a1274df9d6a37553fef8cff2958515438c58920897675c9bc70f5a0538",
|
||||
"sha256:669d1e46f165e0ad152ed8197f7edead22854a6c90419f544e0f234cc9dac6c4",
|
||||
"sha256:695e34c4dbea18d09ab2c258994a8bf6a09564e762655408241f6a14592d2908",
|
||||
"sha256:6b2e03d69afa8d20253455e67b64de1a82ff8612db105113cccec35d3f8429f0",
|
||||
"sha256:79ca7cdda7ad4e3663ea3c43bfa8637fc5d5604c7737f19a8964781abbd1148d",
|
||||
"sha256:7fd2dd9a856f72e6ed06f82facfce01d119b88457cd4b47b7ae501e8e11eba9c",
|
||||
"sha256:82c0354ac39379d836719a77ee360ef865377aa6fdead87909d50248d0f05f4d",
|
||||
"sha256:8f3b956d11c5b301206382726210dc1d3bee1a9ccf7aadf895aaf31f71c3716c",
|
||||
"sha256:91ec98640220ae05b34b79ee88abf27f97ef7c61cf525eec57ea8fcea9f7dddb",
|
||||
"sha256:952be9540d83dba815569d5cb5f31708801e0bbfc3a8c5aef1890b57ed7e58bf",
|
||||
"sha256:99ac266af38ba1b1fe13975aea01ac0e14bb5f3a3200d2c69f05385768b8568e",
|
||||
"sha256:9fa122e7adb24232247f8a89f2d9070bf64b7869daf93ac5e19546b409e47e96",
|
||||
"sha256:a0873eadc4b8ca93e2e848d490809e0123eea154aa44ecd0109c4d0171869584",
|
||||
"sha256:cb998bd4d93af46b8b49ecf5a72c0a98e5cc6d57fdca6527ba78ad89d6606484",
|
||||
"sha256:e02e57346f6a68523e3c43bbdf35dde5c440318d1f827208ae455f6a2ace446d",
|
||||
"sha256:e79a5a896bcee7fff24a788d72e5c69f13e61369d055f28113e71945a7eb1559",
|
||||
"sha256:ee55eb6bcf23ecc975e6b47c127c201b913598f38b6a300075f84eeef2d3baff",
|
||||
"sha256:f1414e6cbcea8d22843e7eafdfdfae3dd1aba41d1945f6ca66e4806c07c4f454"
|
||||
],
|
||||
"version": "==6.0"
|
||||
},
|
||||
"werkzeug": {
|
||||
"hashes": [
|
||||
"sha256:c3fd7a7d41976d9f44db327260e263132466836cef6f91512889ed60ad26557c",
|
||||
"sha256:d5da73735293558eb1651ee2fddc4d0dedcfa06538b8813a2e20011583c9e49b"
|
||||
],
|
||||
"version": "==0.14.1"
|
||||
},
|
||||
"whitenoise": {
|
||||
"hashes": [
|
||||
"sha256:133a92ff0ab8fb9509f77d4f7d0de493eca19c6fea973f4195d4184f888f2e02",
|
||||
"sha256:32b57d193478908a48acb66bf73e7a3c18679263e3e64bfebcfac1144a430039"
|
||||
],
|
||||
"version": "==4.1"
|
||||
}
|
||||
},
|
||||
"develop": {
|
||||
"alabaster": {
|
||||
"hashes": [
|
||||
"sha256:446438bdcca0e05bd45ea2de1668c1d9b032e1a9154c2c259092d77031ddd359",
|
||||
"sha256:a661d72d58e6ea8a57f7a86e37d86716863ee5e92788398526d58b26a4e4dc02"
|
||||
],
|
||||
"version": "==0.7.12"
|
||||
},
|
||||
"appdirs": {
|
||||
"hashes": [
|
||||
"sha256:9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92",
|
||||
"sha256:d8b24664561d0d34ddfaec54636d502d7cea6e29c3eaf68f3df6180863e2166e"
|
||||
],
|
||||
"version": "==1.4.3"
|
||||
},
|
||||
"atomicwrites": {
|
||||
"hashes": [
|
||||
"sha256:0312ad34fcad8fac3704d441f7b317e50af620823353ec657a53e981f92920c0",
|
||||
"sha256:ec9ae8adaae229e4f8446952d204a3e4b5fdd2d099f9be3aaf556120135fb3ee"
|
||||
],
|
||||
"version": "==1.2.1"
|
||||
},
|
||||
"attrs": {
|
||||
"hashes": [
|
||||
"sha256:10cbf6e27dbce8c30807caf056c8eb50917e0eaafe86347671b57254006c3e69",
|
||||
"sha256:ca4be454458f9dec299268d472aaa5a11f67a4ff70093396e1ceae9c76cf4bbb"
|
||||
],
|
||||
"version": "==18.2.0"
|
||||
},
|
||||
"babel": {
|
||||
"hashes": [
|
||||
"sha256:6778d85147d5d85345c14a26aada5e478ab04e39b078b0745ee6870c2b5cf669",
|
||||
"sha256:8cba50f48c529ca3fa18cf81fa9403be176d374ac4d60738b839122dfaaa3d23"
|
||||
],
|
||||
"version": "==2.6.0"
|
||||
},
|
||||
"black": {
|
||||
"hashes": [
|
||||
"sha256:817243426042db1d36617910df579a54f1afd659adb96fc5032fcf4b36209739",
|
||||
"sha256:e030a9a28f542debc08acceb273f228ac422798e5215ba2a791a6ddeaaca22a5"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==18.9b0"
|
||||
},
|
||||
"bleach": {
|
||||
"hashes": [
|
||||
"sha256:48d39675b80a75f6d1c3bdbffec791cf0bbbab665cf01e20da701c77de278718",
|
||||
"sha256:73d26f018af5d5adcdabf5c1c974add4361a9c76af215fe32fdec8a6fc5fb9b9"
|
||||
],
|
||||
"version": "==3.0.2"
|
||||
},
|
||||
"certifi": {
|
||||
"hashes": [
|
||||
"sha256:376690d6f16d32f9d1fe8932551d80b23e9d393a8578c5633a2ed39a64861638",
|
||||
"sha256:456048c7e371c089d0a77a5212fb37a2c2dce1e24146e3b7e0261736aaeaa22a"
|
||||
],
|
||||
"version": "==2018.8.24"
|
||||
},
|
||||
"cffi": {
|
||||
"hashes": [
|
||||
"sha256:151b7eefd035c56b2b2e1eb9963c90c6302dc15fbd8c1c0a83a163ff2c7d7743",
|
||||
"sha256:1553d1e99f035ace1c0544050622b7bc963374a00c467edafac50ad7bd276aef",
|
||||
"sha256:1b0493c091a1898f1136e3f4f991a784437fac3673780ff9de3bcf46c80b6b50",
|
||||
"sha256:2ba8a45822b7aee805ab49abfe7eec16b90587f7f26df20c71dd89e45a97076f",
|
||||
"sha256:3bb6bd7266598f318063e584378b8e27c67de998a43362e8fce664c54ee52d30",
|
||||
"sha256:3c85641778460581c42924384f5e68076d724ceac0f267d66c757f7535069c93",
|
||||
"sha256:3eb6434197633b7748cea30bf0ba9f66727cdce45117a712b29a443943733257",
|
||||
"sha256:495c5c2d43bf6cebe0178eb3e88f9c4aa48d8934aa6e3cddb865c058da76756b",
|
||||
"sha256:4c91af6e967c2015729d3e69c2e51d92f9898c330d6a851bf8f121236f3defd3",
|
||||
"sha256:57b2533356cb2d8fac1555815929f7f5f14d68ac77b085d2326b571310f34f6e",
|
||||
"sha256:770f3782b31f50b68627e22f91cb182c48c47c02eb405fd689472aa7b7aa16dc",
|
||||
"sha256:79f9b6f7c46ae1f8ded75f68cf8ad50e5729ed4d590c74840471fc2823457d04",
|
||||
"sha256:7a33145e04d44ce95bcd71e522b478d282ad0eafaf34fe1ec5bbd73e662f22b6",
|
||||
"sha256:857959354ae3a6fa3da6651b966d13b0a8bed6bbc87a0de7b38a549db1d2a359",
|
||||
"sha256:87f37fe5130574ff76c17cab61e7d2538a16f843bb7bca8ebbc4b12de3078596",
|
||||
"sha256:95d5251e4b5ca00061f9d9f3d6fe537247e145a8524ae9fd30a2f8fbce993b5b",
|
||||
"sha256:9d1d3e63a4afdc29bd76ce6aa9d58c771cd1599fbba8cf5057e7860b203710dd",
|
||||
"sha256:a36c5c154f9d42ec176e6e620cb0dd275744aa1d804786a71ac37dc3661a5e95",
|
||||
"sha256:a6a5cb8809091ec9ac03edde9304b3ad82ad4466333432b16d78ef40e0cce0d5",
|
||||
"sha256:ae5e35a2c189d397b91034642cb0eab0e346f776ec2eb44a49a459e6615d6e2e",
|
||||
"sha256:b0f7d4a3df8f06cf49f9f121bead236e328074de6449866515cea4907bbc63d6",
|
||||
"sha256:b75110fb114fa366b29a027d0c9be3709579602ae111ff61674d28c93606acca",
|
||||
"sha256:ba5e697569f84b13640c9e193170e89c13c6244c24400fc57e88724ef610cd31",
|
||||
"sha256:be2a9b390f77fd7676d80bc3cdc4f8edb940d8c198ed2d8c0be1319018c778e1",
|
||||
"sha256:ca1bd81f40adc59011f58159e4aa6445fc585a32bb8ac9badf7a2c1aa23822f2",
|
||||
"sha256:d5d8555d9bfc3f02385c1c37e9f998e2011f0db4f90e250e5bc0c0a85a813085",
|
||||
"sha256:e55e22ac0a30023426564b1059b035973ec82186ddddbac867078435801c7801",
|
||||
"sha256:e90f17980e6ab0f3c2f3730e56d1fe9bcba1891eeea58966e89d352492cc74f4",
|
||||
"sha256:ecbb7b01409e9b782df5ded849c178a0aa7c906cf8c5a67368047daab282b184",
|
||||
"sha256:ed01918d545a38998bfa5902c7c00e0fee90e957ce036a4000a88e3fe2264917",
|
||||
"sha256:edabd457cd23a02965166026fd9bfd196f4324fe6032e866d0f3bd0301cd486f",
|
||||
"sha256:fdf1c1dc5bafc32bc5d08b054f94d659422b05aba244d6be4ddc1c72d9aa70fb"
|
||||
],
|
||||
"version": "==1.11.5"
|
||||
},
|
||||
"chardet": {
|
||||
"hashes": [
|
||||
"sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae",
|
||||
"sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"
|
||||
],
|
||||
"version": "==3.0.4"
|
||||
},
|
||||
"click": {
|
||||
"hashes": [
|
||||
"sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13",
|
||||
"sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7"
|
||||
],
|
||||
"version": "==7.0"
|
||||
},
|
||||
"cmarkgfm": {
|
||||
"hashes": [
|
||||
"sha256:0186dccca79483e3405217993b83b914ba4559fe9a8396efc4eea56561b74061",
|
||||
"sha256:1a625afc6f62da428df96ec325dc30866cc5781520cbd904ff4ec44cf018171c",
|
||||
"sha256:207b7673ff4e177374c572feeae0e4ef33be620ec9171c08fd22e2b796e03e3d",
|
||||
"sha256:275905bb371a99285c74931700db3f0c078e7603bed383e8cf1a09f3ee05a3de",
|
||||
"sha256:50098f1c4950722521f0671e54139e0edc1837d63c990cf0f3d2c49607bb51a2",
|
||||
"sha256:50ed116d0b60a07df0dc7b180c28569064b9d37d1578d4c9021cff04d725cb63",
|
||||
"sha256:61a72def110eed903cd1848245897bcb80d295cd9d13944d4f9f30cba5b76655",
|
||||
"sha256:64186fb75d973a06df0e6ea12879533b71f6e7ba1ab01ffee7fc3e7534758889",
|
||||
"sha256:665303d34d7f14f10d7b0651082f25ebf7107f29ef3d699490cac16cdc0fc8ce",
|
||||
"sha256:70b18f843aec58e4e64aadce48a897fe7c50426718b7753aaee399e72df64190",
|
||||
"sha256:761ee7b04d1caee2931344ac6bfebf37102ffb203b136b676b0a71a3f0ea3c87",
|
||||
"sha256:811527e9b7280b136734ed6cb6845e5fbccaeaa132ddf45f0246cbe544016957",
|
||||
"sha256:987b0e157f70c72a84f3c2f9ef2d7ab0f26c08f2bf326c12c087ff9eebcb3ff5",
|
||||
"sha256:9fc6a2183d0a9b0974ec7cdcdad42bd78a3be674cc3e65f87dd694419b3b0ab7",
|
||||
"sha256:a3d17ee4ae739fe16f7501a52255c2e287ac817cfd88565b9859f70520afffea",
|
||||
"sha256:ba5b5488719c0f2ced0aa1986376f7baff1a1653a8eb5fdfcf3f84c7ce46ef8d",
|
||||
"sha256:c573ea89dd95d41b6d8cf36799c34b6d5b1eac4aed0212dee0f0a11fb7b01e8f",
|
||||
"sha256:c5f1b9e8592d2c448c44e6bc0d91224b16ea5f8293908b1561de1f6d2d0658b1",
|
||||
"sha256:cbe581456357d8f0674d6a590b1aaf46c11d01dd0a23af147a51a798c3818034",
|
||||
"sha256:cf219bec69e601fe27e3974b7307d2f06082ab385d42752738ad2eb630a47d65",
|
||||
"sha256:cf5014eb214d814a83a7a47407272d5db10b719dbeaf4d3cfe5969309d0fcf4b",
|
||||
"sha256:d08bad67fa18f7e8ff738c090628ee0cbf0505d74a991c848d6d04abfe67b697",
|
||||
"sha256:d6f716d7b1182bf35862b5065112f933f43dd1aa4f8097c9bcfb246f71528a34",
|
||||
"sha256:e08e479102627641c7cb4ece421c6ed4124820b1758765db32201136762282d9",
|
||||
"sha256:e20ac21418af0298437d29599f7851915497ce9f2866bc8e86b084d8911ee061",
|
||||
"sha256:e25f53c37e319241b9a412382140dffac98ca756ba8f360ac7ab5e30cad9670a",
|
||||
"sha256:e8932bddf159064f04e946fbb64693753488de21586f20e840b3be51745c8c09",
|
||||
"sha256:f20900f16377f2109783ae9348d34bc80530808439591c3d3df73d5c7ef1a00c"
|
||||
],
|
||||
"version": "==0.4.2"
|
||||
},
|
||||
"colorama": {
|
||||
"hashes": [
|
||||
"sha256:a3d89af5db9e9806a779a50296b5fdb466e281147c2c235e8225ecc6dbf7bbf3",
|
||||
"sha256:c9b54bebe91a6a803e0772c8561d53f2926bfeb17cd141fbabcb08424086595c"
|
||||
],
|
||||
"markers": "sys_platform == 'win32'",
|
||||
"version": "==0.4.0"
|
||||
},
|
||||
"docutils": {
|
||||
"hashes": [
|
||||
"sha256:02aec4bd92ab067f6ff27a38a38a41173bf01bed8f89157768c1573f53e474a6",
|
||||
"sha256:51e64ef2ebfb29cae1faa133b3710143496eca21c530f3f71424d77687764274",
|
||||
"sha256:7a4bd47eaf6596e1295ecb11361139febe29b084a87bf005bf899f9a42edc3c6"
|
||||
],
|
||||
"version": "==0.14"
|
||||
},
|
||||
"flake8": {
|
||||
"hashes": [
|
||||
"sha256:7253265f7abd8b313e3892944044a365e3f4ac3fcdcfb4298f55ee9ddf188ba0",
|
||||
"sha256:c7841163e2b576d435799169b78703ad6ac1bbb0f199994fc05f700b2a90ea37"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==3.5.0"
|
||||
},
|
||||
"flask": {
|
||||
"hashes": [
|
||||
"sha256:2271c0070dbcb5275fad4a82e29f23ab92682dc45f9dfbc22c02ba9b9322ce48",
|
||||
"sha256:a080b744b7e345ccfcbc77954861cb05b3c63786e93f2b3875e0913d44b43f05"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.0.2"
|
||||
},
|
||||
"future": {
|
||||
"hashes": [
|
||||
"sha256:e39ced1ab767b5936646cedba8bcce582398233d6a627067d4c6a454c90cfedb"
|
||||
],
|
||||
"version": "==0.16.0"
|
||||
},
|
||||
"idna": {
|
||||
"hashes": [
|
||||
"sha256:156a6814fb5ac1fc6850fb002e0852d56c0c8d2531923a51032d1b70760e186e",
|
||||
"sha256:684a38a6f903c1d71d6d5fac066b58d7768af4de2b832e426ec79c30daa94a16"
|
||||
],
|
||||
"version": "==2.7"
|
||||
},
|
||||
"imagesize": {
|
||||
"hashes": [
|
||||
"sha256:3f349de3eb99145973fefb7dbe38554414e5c30abd0c8e4b970a7c9d09f3a1d8",
|
||||
"sha256:f3832918bc3c66617f92e35f5d70729187676313caa60c187eb0f28b8fe5e3b5"
|
||||
],
|
||||
"version": "==1.1.0"
|
||||
},
|
||||
"itsdangerous": {
|
||||
"hashes": [
|
||||
"sha256:cbb3fcf8d3e33df861709ecaf89d9e6629cff0a217bc2848f1b41cd30d360519"
|
||||
],
|
||||
"version": "==0.24"
|
||||
},
|
||||
"jinja2": {
|
||||
"hashes": [
|
||||
"sha256:74c935a1b8bb9a3947c50a54766a969d4846290e1e788ea44c1392163723c3bd",
|
||||
"sha256:f84be1bb0040caca4cea721fcbbbbd61f9be9464ca236387158b0feea01914a4"
|
||||
],
|
||||
"version": "==2.10"
|
||||
},
|
||||
"markupsafe": {
|
||||
"hashes": [
|
||||
"sha256:a6be69091dac236ea9c6bc7d012beab42010fa914c459791d627dad4910eb665"
|
||||
],
|
||||
"version": "==1.0"
|
||||
},
|
||||
"mccabe": {
|
||||
"hashes": [
|
||||
"sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42",
|
||||
"sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"
|
||||
],
|
||||
"version": "==0.6.1"
|
||||
},
|
||||
"more-itertools": {
|
||||
"hashes": [
|
||||
"sha256:c187a73da93e7a8acc0001572aebc7e3c69daf7bf6881a2cea10650bd4420092",
|
||||
"sha256:c476b5d3a34e12d40130bc2f935028b5f636df8f372dc2c1c01dc19681b2039e",
|
||||
"sha256:fcbfeaea0be121980e15bc97b3817b5202ca73d0eae185b4550cbfce2a3ebb3d"
|
||||
],
|
||||
"version": "==4.3.0"
|
||||
},
|
||||
"packaging": {
|
||||
"hashes": [
|
||||
"sha256:0886227f54515e592aaa2e5a553332c73962917f2831f1b0f9b9f4380a4b9807",
|
||||
"sha256:f95a1e147590f204328170981833854229bb2912ac3d5f89e2a8ccd2834800c9"
|
||||
],
|
||||
"version": "==18.0"
|
||||
},
|
||||
"pkginfo": {
|
||||
"hashes": [
|
||||
"sha256:5878d542a4b3f237e359926384f1dde4e099c9f5525d236b1840cf704fa8d474",
|
||||
"sha256:a39076cb3eb34c333a0dd390b568e9e1e881c7bf2cc0aee12120636816f55aee"
|
||||
],
|
||||
"version": "==1.4.2"
|
||||
},
|
||||
"pluggy": {
|
||||
"hashes": [
|
||||
"sha256:6e3836e39f4d36ae72840833db137f7b7d35105079aee6ec4a62d9f80d594dd1",
|
||||
"sha256:95eb8364a4708392bae89035f45341871286a333f749c3141c20573d2b3876e1"
|
||||
],
|
||||
"version": "==0.7.1"
|
||||
},
|
||||
"py": {
|
||||
"hashes": [
|
||||
"sha256:bf92637198836372b520efcba9e020c330123be8ce527e535d185ed4b6f45694",
|
||||
"sha256:e76826342cefe3c3d5f7e8ee4316b80d1dd8a300781612ddbc765c17ba25a6c6"
|
||||
],
|
||||
"version": "==1.7.0"
|
||||
},
|
||||
"pycodestyle": {
|
||||
"hashes": [
|
||||
"sha256:682256a5b318149ca0d2a9185d365d8864a768a28db66a84a2ea946bcc426766",
|
||||
"sha256:6c4245ade1edfad79c3446fadfc96b0de2759662dc29d07d80a6f27ad1ca6ba9"
|
||||
],
|
||||
"version": "==2.3.1"
|
||||
},
|
||||
"pycparser": {
|
||||
"hashes": [
|
||||
"sha256:a988718abfad80b6b157acce7bf130a30876d27603738ac39f140993246b25b3"
|
||||
],
|
||||
"version": "==2.19"
|
||||
},
|
||||
"pyflakes": {
|
||||
"hashes": [
|
||||
"sha256:08bd6a50edf8cffa9fa09a463063c425ecaaf10d1eb0335a7e8b1401aef89e6f",
|
||||
"sha256:8d616a382f243dbf19b54743f280b80198be0bca3a5396f1d2e1fca6223e8805"
|
||||
],
|
||||
"version": "==1.6.0"
|
||||
},
|
||||
"pygments": {
|
||||
"hashes": [
|
||||
"sha256:78f3f434bcc5d6ee09020f92ba487f95ba50f1e3ef83ae96b9d5ffa1bab25c5d",
|
||||
"sha256:dbae1046def0efb574852fab9e90209b23f556367b5a320c0bcb871c77c3e8cc"
|
||||
],
|
||||
"version": "==2.2.0"
|
||||
},
|
||||
"pyparsing": {
|
||||
"hashes": [
|
||||
"sha256:bc6c7146b91af3f567cf6daeaec360bc07d45ffec4cf5353f4d7a208ce7ca30a",
|
||||
"sha256:d29593d8ebe7b57d6967b62494f8c72b03ac0262b1eed63826c6f788b3606401"
|
||||
],
|
||||
"version": "==2.2.2"
|
||||
},
|
||||
"pytest": {
|
||||
"hashes": [
|
||||
"sha256:7e258ee50338f4e46957f9e09a0f10fb1c2d05493fa901d113a8dafd0790de4e",
|
||||
"sha256:9332147e9af2dcf46cd7ceb14d5acadb6564744ddff1fe8c17f0ce60ece7d9a2"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==3.8.2"
|
||||
},
|
||||
"pytz": {
|
||||
"hashes": [
|
||||
"sha256:a061aa0a9e06881eb8b3b2b43f05b9439d6583c206d0a6c340ff72a7b6669053",
|
||||
"sha256:ffb9ef1de172603304d9d2819af6f5ece76f2e85ec10692a524dd876e72bf277"
|
||||
],
|
||||
"version": "==2018.5"
|
||||
},
|
||||
"readme-renderer": {
|
||||
"hashes": [
|
||||
"sha256:237ca8705ffea849870de41101dba41543561da05c0ae45b2f1c547efa9843d2",
|
||||
"sha256:f75049a3a7afa57165551e030dd8f9882ebf688b9600535a3f7e23596651875d"
|
||||
],
|
||||
"version": "==22.0"
|
||||
},
|
||||
"requests": {
|
||||
"hashes": [
|
||||
"sha256:63b52e3c866428a224f97cab011de738c36aec0185aa91cfacd418b5d58911d1",
|
||||
"sha256:ec22d826a36ed72a7358ff3fe56cbd4ba69dd7a6718ffd450ff0e9df7a47ce6a"
|
||||
],
|
||||
"version": "==2.19.1"
|
||||
},
|
||||
"requests-toolbelt": {
|
||||
"hashes": [
|
||||
"sha256:42c9c170abc2cacb78b8ab23ac957945c7716249206f90874651971a4acff237",
|
||||
"sha256:f6a531936c6fa4c6cfce1b9c10d5c4f498d16528d2a54a22ca00011205a187b5"
|
||||
],
|
||||
"version": "==0.8.0"
|
||||
},
|
||||
"six": {
|
||||
"hashes": [
|
||||
"sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9",
|
||||
"sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb"
|
||||
],
|
||||
"version": "==1.11.0"
|
||||
},
|
||||
"snowballstemmer": {
|
||||
"hashes": [
|
||||
"sha256:919f26a68b2c17a7634da993d91339e288964f93c274f1343e3bbbe2096e1128",
|
||||
"sha256:9f3bcd3c401c3e862ec0ebe6d2c069ebc012ce142cce209c098ccb5b09136e89"
|
||||
],
|
||||
"version": "==1.2.1"
|
||||
},
|
||||
"sphinx": {
|
||||
"hashes": [
|
||||
"sha256:652eb8c566f18823a022bb4b6dbc868d366df332a11a0226b5bc3a798a479f17",
|
||||
"sha256:d222626d8356de702431e813a05c68a35967e3d66c6cd1c2c89539bb179a7464"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.8.1"
|
||||
},
|
||||
"sphinxcontrib-websupport": {
|
||||
"hashes": [
|
||||
"sha256:68ca7ff70785cbe1e7bccc71a48b5b6d965d79ca50629606c7861a21b206d9dd",
|
||||
"sha256:9de47f375baf1ea07cdb3436ff39d7a9c76042c10a769c52353ec46e4e8fc3b9"
|
||||
],
|
||||
"version": "==1.1.0"
|
||||
},
|
||||
"toml": {
|
||||
"hashes": [
|
||||
"sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c",
|
||||
"sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e"
|
||||
],
|
||||
"version": "==0.10.0"
|
||||
},
|
||||
"tqdm": {
|
||||
"hashes": [
|
||||
"sha256:18f1818ce951aeb9ea162ae1098b43f583f7d057b34d706f66939353d1208889",
|
||||
"sha256:df02c0650160986bac0218bb07952245fc6960d23654648b5d5526ad5a4128c9"
|
||||
],
|
||||
"version": "==4.26.0"
|
||||
},
|
||||
"twine": {
|
||||
"hashes": [
|
||||
"sha256:7d89bc6acafb31d124e6e5b295ef26ac77030bf098960c2a4c4e058335827c5c",
|
||||
"sha256:fad6f1251195f7ddd1460cb76d6ea106c93adb4e56c41e0da79658e56e547d2c"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.12.1"
|
||||
},
|
||||
"urllib3": {
|
||||
"hashes": [
|
||||
"sha256:a68ac5e15e76e7e5dd2b8f94007233e01effe3e50e8daddf69acfd81cb686baf",
|
||||
"sha256:b5725a0bd4ba422ab0e66e89e030c806576753ea3ee08554382c14e685d117b5"
|
||||
],
|
||||
"version": "==1.23"
|
||||
},
|
||||
"webencodings": {
|
||||
"hashes": [
|
||||
"sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78",
|
||||
"sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923"
|
||||
],
|
||||
"version": "==0.5.1"
|
||||
},
|
||||
"werkzeug": {
|
||||
"hashes": [
|
||||
"sha256:c3fd7a7d41976d9f44db327260e263132466836cef6f91512889ed60ad26557c",
|
||||
"sha256:d5da73735293558eb1651ee2fddc4d0dedcfa06538b8813a2e20011583c9e49b"
|
||||
],
|
||||
"version": "==0.14.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,125 +1,91 @@
|
||||
# Responder: a familiar HTTP Service Framework for Python
|
||||
|
||||

|
||||
[](https://github.com/kennethreitz/responder/actions/workflows/test.yaml)
|
||||
[](https://responder.kennethreitz.org/)
|
||||
[](https://pypi.org/project/responder/)
|
||||
[](https://pypi.org/project/responder/)
|
||||
[](https://pypi.org/project/responder/)
|
||||
[](https://github.com/kennethreitz/responder/graphs/contributors)
|
||||
[](https://pepy.tech/project/responder/)
|
||||
[](https://pypi.org/project/responder/)
|
||||
[](https://pypi.org/project/responder/)
|
||||
|
||||
The Python world certainly doesn't need more web frameworks. But, it does need more creativity, so I thought I'd bring some of my ideas to the table and see what I could come up with.
|
||||
[](https://responder.readthedocs.io)
|
||||
|
||||
## But will it blend?
|
||||
Powered by [Starlette](https://www.starlette.io/). That `async` declaration is optional.
|
||||
[View documentation](https://responder.readthedocs.io).
|
||||
|
||||
```python
|
||||
import responder
|
||||
This gets you a ASGI app, with a production static files server pre-installed, jinja2
|
||||
templating (without additional imports), and a production webserver based on uvloop,
|
||||
serving up requests with gzip compression automatically.
|
||||
|
||||
api = responder.API()
|
||||
## Testimonials
|
||||
|
||||
@api.route("/{greeting}")
|
||||
def greet_world(req, resp, *, greeting):
|
||||
resp.text = f"{greeting}, world!"
|
||||
> "Pleasantly very taken with python-responder.
|
||||
> [@kennethreitz](https://twitter.com/kennethreitz) at his absolute best." —Rudraksh
|
||||
> M.K.
|
||||
|
||||
if __name__ == '__main__':
|
||||
api.run()
|
||||
```
|
||||
> "ASGI is going to enable all sorts of new high-performance web services. It's awesome
|
||||
> to see Responder starting to take advantage of that." — Tom Christie author of
|
||||
> [Django REST Framework](https://www.django-rest-framework.org/)
|
||||
|
||||
This gets you a WSGI app, with WhiteNoise pre-installed, jinja2 templating (without additional imports), and a production webserver (ready for slowloris attacks), serving up requests with gzip compression automatically.
|
||||
> "I love that you are exploring new patterns. Go go go!" — Danny Greenfield, author of
|
||||
> [Two Scoops of Django]()
|
||||
|
||||
Class-based views (and setting some headers and stuff):
|
||||
## More Examples
|
||||
|
||||
```python
|
||||
@api.route("/{greeting}")
|
||||
class GreetingResource:
|
||||
def on_request(req, resp, *, greeting): # or on_get...
|
||||
resp.text = f"{greeting}, world!"
|
||||
resp.headers.update({'X-Life': '42'})
|
||||
resp.status_code = api.status_codes.HTTP_416
|
||||
```
|
||||
See
|
||||
[the documentation's feature tour](https://responder.readthedocs.io/en/latest/tour.html)
|
||||
for more details on features available in Responder.
|
||||
|
||||
Render a template, with arguments:
|
||||
# Installing Responder
|
||||
|
||||
```python
|
||||
@api.route("/{greeting}")
|
||||
def greet_world(req, resp, *, greeting):
|
||||
resp.content = api.template("index.html", greeting=greeting)
|
||||
```
|
||||
Install the most recent stable release:
|
||||
|
||||
The `api` instance is available as an object during template rendering.
|
||||
pip install --upgrade responder
|
||||
|
||||
Serve a GraphQL API:
|
||||
Or, install directly from the repository:
|
||||
|
||||
```python
|
||||
import graphene
|
||||
|
||||
class Query(graphene.ObjectType):
|
||||
hello = graphene.String(name=graphene.String(default_value="stranger"))
|
||||
|
||||
def resolve_hello(self, info, name):
|
||||
return "Hello " + name
|
||||
|
||||
api.add_route("/graph", graphene.Schema(query=Query))
|
||||
```
|
||||
|
||||
We can then send a query to our service:
|
||||
|
||||
```pycon
|
||||
>>> requests = api.session()
|
||||
>>> r = requests.get("http://;/graph", params={"query": "{ hello }"})
|
||||
>>> r.json()
|
||||
{'data': {'hello': 'Hello stranger'}}
|
||||
```
|
||||
|
||||
Or, request YAML back:
|
||||
|
||||
```pycon
|
||||
>>> r = requests.get("http://;/graph", params={"query": "{ hello(name:\"john\") }"}, headers={"Accept": "application/x-yaml"})
|
||||
>>> print(r.text)
|
||||
data: {hello: Hello john}
|
||||
|
||||
```
|
||||
|
||||
Want HSTS?
|
||||
|
||||
```
|
||||
api = responder.API(enable_hsts=True)
|
||||
```
|
||||
|
||||
Boom. ✨🍰✨
|
||||
pip install 'responder @ git+https://github.com/kennethreitz/responder.git'
|
||||
|
||||
Only **Python 3.6+** is supported.
|
||||
|
||||
# The Basic Idea
|
||||
|
||||
The primary concept here is to bring the nicities 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.
|
||||
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 familar friends.
|
||||
- `resp.status_code`, `req.method`, `req.url`, and other familiar friends.
|
||||
|
||||
## New Ideas
|
||||
## Ideas
|
||||
|
||||
- Flask-style route expression, with new capabilities -- all while using Python 3.6+'s
|
||||
new f-string syntax.
|
||||
- I love Falcon's "every request and response is passed into to each view and mutated"
|
||||
methodology, especially `response.media`, and have used it here. In addition to
|
||||
supporting JSON, I have decided to support YAML as well, as Kubernetes is slowly
|
||||
taking over the world, and it uses YAML for all the things. Content-negotiation and
|
||||
all that.
|
||||
- **A built in testing client that uses the actual Requests you know and love**.
|
||||
- The ability to mount other WSGI apps easily.
|
||||
- Automatic gzipped-responses (still working on that).
|
||||
- In addition to Falcon's `on_get`, `on_post`, etc methods, Responder features an `on_request` method, which gets called on every type of request, much like Requests.
|
||||
- WhiteNoise is built-in, for serving static files.
|
||||
- Waitress built-in as a production web server. I would have chosen Gunicorn, but it doesn't run on Windows. Plus, Waitress serves well to protect against slowloris attacks, making nginx unnecessary in production.
|
||||
- GraphQL support, via Graphene. The goal here is to have any GraphQL query exposable at any route, magically.
|
||||
- Automatic gzipped-responses.
|
||||
- In addition to Falcon's `on_get`, `on_post`, etc methods, Responder features an
|
||||
`on_request` method, which gets called on every type of request, much like Requests.
|
||||
- A production static file server is built-in.
|
||||
- Uvicorn built-in as a production web server. I would have chosen Gunicorn, but it
|
||||
doesn't run on Windows. Plus, Uvicorn serves well to protect against slowloris
|
||||
attacks, making nginx unnecessary in production.
|
||||
- GraphQL support, via Graphene. The goal here is to have any GraphQL query exposable at
|
||||
any route, magically.
|
||||
- Provide an official way to run webpack.
|
||||
|
||||
## Development
|
||||
|
||||
## Old Ideas
|
||||
|
||||
- Flask-style route expression, with new capabilities -- primarily, the ability to cast a parameter to integers as well as other types that are missing from Flask, all while using Python 3.6+'s new f-string syntax.
|
||||
|
||||
- I love Falcon's "every request and response is passed into to each view and mutated" methodology, especially `response.media`, and have used it here. In addition to supporting JSON, I have decided to support YAML as well, as Kubernetes is slowly taking over the world, and it uses YAML for all the things. Content-negotiation and all that.
|
||||
|
||||
## Future Ideas
|
||||
|
||||
- I want to be able to "mount" any WSGI app into a sub-route.
|
||||
- Cooke-based sessions are currently an afterthought, as this is an API framework, but websites are APIs too.
|
||||
- Potentially support ASGI instead of WSGI. Will the tradeoffs be worth it? This is a question to ask. Procedural code works well for 90% use cases.
|
||||
- If frontend websites are supported, provide an official way to run webpack.
|
||||
|
||||
# The Goal
|
||||
|
||||
The primary goal here is to learn, not to get adoption. Though, who knows how these things will pan out.
|
||||
|
||||
# When can I use it?
|
||||
|
||||
When it's ready. It's not. I started work on this a few days ago. It works surprisingly well, considering! :)
|
||||
See [Development Sandbox](DEVELOP.md).
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
import responder
|
||||
import graphene
|
||||
|
||||
|
||||
from flask import Flask
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
|
||||
@app.route("/")
|
||||
def hello_world():
|
||||
return "Hello, World from flask!"
|
||||
|
||||
|
||||
api = responder.API(enable_hsts=False)
|
||||
api.mount("/hello", app)
|
||||
|
||||
|
||||
@api.route("/")
|
||||
def hello(req, resp):
|
||||
# resp.status = responder.status.ok
|
||||
resp.content = api.template("test.html")
|
||||
|
||||
|
||||
class ThingsResource:
|
||||
def on_request(self, req, resp):
|
||||
resp.status = responder.status.HTTP_200
|
||||
resp.media = ["yolo"]
|
||||
|
||||
|
||||
class Query(graphene.ObjectType):
|
||||
hello = graphene.String(name=graphene.String(default_value="stranger"))
|
||||
|
||||
def resolve_hello(self, info, name):
|
||||
return "Hello " + name
|
||||
|
||||
|
||||
schema = graphene.Schema(query=Query)
|
||||
|
||||
# Alerntatively,
|
||||
api.add_route("/graph", schema, graphiql=True)
|
||||
|
||||
print(
|
||||
api.session()
|
||||
.get(
|
||||
"http://app/",
|
||||
# data="{ hello }",
|
||||
# headers={"Accept": "application/x-yaml"},
|
||||
# data="hello",
|
||||
)
|
||||
.text
|
||||
)
|
||||
|
||||
# print(
|
||||
# api.session()
|
||||
# .get(
|
||||
# "http://app/hello/",
|
||||
# data="{ hello }",
|
||||
# headers={"Accept": "application/x-yaml"},
|
||||
# # data="hello",
|
||||
# )
|
||||
# .text
|
||||
# )
|
||||
# {hello: Hello stranger}
|
||||
|
||||
api.run(port=5000)
|
||||
@@ -0,0 +1,37 @@
|
||||
# ruff: noqa: S605, S607
|
||||
"""
|
||||
Build and publish a .deb package.
|
||||
https://pypi.python.org/pypi/stdeb/0.8.5#quickstart-2-just-tell-me-the-fastest-way-to-make-a-deb
|
||||
"""
|
||||
|
||||
import os
|
||||
from shutil import rmtree
|
||||
|
||||
here = os.path.abspath(os.path.dirname(__file__))
|
||||
|
||||
|
||||
def get_version():
|
||||
import responder
|
||||
|
||||
return responder.__version__
|
||||
|
||||
|
||||
def run():
|
||||
version = get_version()
|
||||
try:
|
||||
print("Removing previous builds")
|
||||
rmtree(os.path.join(here, "deb_dist"))
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
print("Creating Debian package manifest")
|
||||
os.system(
|
||||
"python setup.py --command-packages=stdeb.command sdist_dsc "
|
||||
"-z artful --package3=pipenv --depends3=python3-virtualenv-clone"
|
||||
)
|
||||
print("Building .deb")
|
||||
os.chdir(f"deb_dist/pipenv-{version}")
|
||||
os.system("dpkg-buildpackage -rfakeroot -uc -us")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
run()
|
||||
+6
-48
@@ -1,48 +1,6 @@
|
||||
alabaster==0.7.12
|
||||
appdirs==1.4.3
|
||||
atomicwrites==1.2.1
|
||||
attrs==18.2.0
|
||||
babel==2.6.0
|
||||
black==18.9b0
|
||||
bleach==3.0.2
|
||||
certifi==2018.8.24
|
||||
cffi==1.11.5
|
||||
chardet==3.0.4
|
||||
click==7.0
|
||||
cmarkgfm==0.4.2
|
||||
colorama==0.4.0 ; sys_platform == 'win32'
|
||||
docutils==0.14
|
||||
flake8==3.5.0
|
||||
flask==1.0.2
|
||||
future==0.16.0
|
||||
idna==2.7
|
||||
imagesize==1.1.0
|
||||
itsdangerous==0.24
|
||||
jinja2==2.10
|
||||
markupsafe==1.0
|
||||
mccabe==0.6.1
|
||||
more-itertools==4.3.0
|
||||
packaging==18.0
|
||||
pkginfo==1.4.2
|
||||
pluggy==0.7.1
|
||||
py==1.7.0
|
||||
pycodestyle==2.3.1
|
||||
pycparser==2.19
|
||||
pyflakes==1.6.0
|
||||
pygments==2.2.0
|
||||
pyparsing==2.2.2
|
||||
pytest==3.8.2
|
||||
pytz==2018.5
|
||||
readme-renderer==22.0
|
||||
requests-toolbelt==0.8.0
|
||||
requests==2.19.1
|
||||
six==1.11.0
|
||||
snowballstemmer==1.2.1
|
||||
sphinx==1.8.1
|
||||
sphinxcontrib-websupport==1.1.0
|
||||
toml==0.10.0
|
||||
tqdm==4.26.0
|
||||
twine==1.12.1
|
||||
urllib3==1.23
|
||||
webencodings==0.5.1
|
||||
werkzeug==0.14.1
|
||||
alabaster<0.8
|
||||
jinja2<3.2
|
||||
markupsafe<4
|
||||
readme-renderer<45
|
||||
sphinx>=5,<9
|
||||
sphinxcontrib-websupport<2.1
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/* Hide module name and default value for environment variable section */
|
||||
div[id$='environment-variables'] code.descclassname {
|
||||
display: none;
|
||||
div[id$="environment-variables"] code.descclassname {
|
||||
display: none;
|
||||
}
|
||||
div[id$='environment-variables'] em.property {
|
||||
display: none;
|
||||
div[id$="environment-variables"] em.property {
|
||||
display: none;
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+142
-132
@@ -10,142 +10,152 @@
|
||||
*/
|
||||
|
||||
var Konami = function (callback) {
|
||||
var konami = {
|
||||
addEvent: function (obj, type, fn, ref_obj) {
|
||||
if (obj.addEventListener)
|
||||
obj.addEventListener(type, fn, false);
|
||||
else if (obj.attachEvent) {
|
||||
// IE
|
||||
obj["e" + type + fn] = fn;
|
||||
obj[type + fn] = function () {
|
||||
obj["e" + type + fn](window.event, ref_obj);
|
||||
}
|
||||
obj.attachEvent("on" + type, obj[type + fn]);
|
||||
}
|
||||
},
|
||||
removeEvent: function (obj, eventName, eventCallback) {
|
||||
if (obj.removeEventListener) {
|
||||
obj.removeEventListener(eventName, eventCallback);
|
||||
} else if (obj.attachEvent) {
|
||||
obj.detachEvent(eventName);
|
||||
}
|
||||
},
|
||||
input: "",
|
||||
pattern: "38384040373937396665",
|
||||
keydownHandler: function (e, ref_obj) {
|
||||
if (ref_obj) {
|
||||
konami = ref_obj;
|
||||
} // IE
|
||||
konami.input += e ? e.keyCode : event.keyCode;
|
||||
if (konami.input.length > konami.pattern.length) {
|
||||
konami.input = konami.input.substr((konami.input.length - konami.pattern.length));
|
||||
}
|
||||
if (konami.input === konami.pattern) {
|
||||
konami.code(konami._currentLink);
|
||||
konami.input = '';
|
||||
e.preventDefault();
|
||||
return false;
|
||||
}
|
||||
},
|
||||
load: function (link) {
|
||||
this._currentLink = link;
|
||||
this.addEvent(document, "keydown", this.keydownHandler, this);
|
||||
this.iphone.load(link);
|
||||
},
|
||||
unload: function () {
|
||||
this.removeEvent(document, 'keydown', this.keydownHandler);
|
||||
this.iphone.unload();
|
||||
},
|
||||
code: function (link) {
|
||||
window.location = link
|
||||
},
|
||||
iphone: {
|
||||
start_x: 0,
|
||||
start_y: 0,
|
||||
stop_x: 0,
|
||||
stop_y: 0,
|
||||
tap: false,
|
||||
capture: false,
|
||||
orig_keys: "",
|
||||
keys: ["UP", "UP", "DOWN", "DOWN", "LEFT", "RIGHT", "LEFT", "RIGHT", "TAP", "TAP"],
|
||||
input: [],
|
||||
code: function (link) {
|
||||
konami.code(link);
|
||||
},
|
||||
touchmoveHandler: function (e) {
|
||||
if (e.touches.length === 1 && konami.iphone.capture === true) {
|
||||
var touch = e.touches[0];
|
||||
konami.iphone.stop_x = touch.pageX;
|
||||
konami.iphone.stop_y = touch.pageY;
|
||||
konami.iphone.tap = false;
|
||||
konami.iphone.capture = false;
|
||||
konami.iphone.check_direction();
|
||||
}
|
||||
},
|
||||
touchendHandler: function () {
|
||||
konami.iphone.input.push(konami.iphone.check_direction());
|
||||
|
||||
if (konami.iphone.input.length > konami.iphone.keys.length) konami.iphone.input.shift();
|
||||
|
||||
if (konami.iphone.input.length === konami.iphone.keys.length) {
|
||||
var match = true;
|
||||
for (var i = 0; i < konami.iphone.keys.length; i++) {
|
||||
if (konami.iphone.input[i] !== konami.iphone.keys[i]) {
|
||||
match = false;
|
||||
}
|
||||
}
|
||||
if (match) {
|
||||
konami.iphone.code(konami._currentLink);
|
||||
}
|
||||
}
|
||||
},
|
||||
touchstartHandler: function (e) {
|
||||
konami.iphone.start_x = e.changedTouches[0].pageX;
|
||||
konami.iphone.start_y = e.changedTouches[0].pageY;
|
||||
konami.iphone.tap = true;
|
||||
konami.iphone.capture = true;
|
||||
},
|
||||
load: function (link) {
|
||||
this.orig_keys = this.keys;
|
||||
konami.addEvent(document, "touchmove", this.touchmoveHandler);
|
||||
konami.addEvent(document, "touchend", this.touchendHandler, false);
|
||||
konami.addEvent(document, "touchstart", this.touchstartHandler);
|
||||
},
|
||||
unload: function () {
|
||||
konami.removeEvent(document, 'touchmove', this.touchmoveHandler);
|
||||
konami.removeEvent(document, 'touchend', this.touchendHandler);
|
||||
konami.removeEvent(document, 'touchstart', this.touchstartHandler);
|
||||
},
|
||||
check_direction: function () {
|
||||
x_magnitude = Math.abs(this.start_x - this.stop_x);
|
||||
y_magnitude = Math.abs(this.start_y - this.stop_y);
|
||||
x = ((this.start_x - this.stop_x) < 0) ? "RIGHT" : "LEFT";
|
||||
y = ((this.start_y - this.stop_y) < 0) ? "DOWN" : "UP";
|
||||
result = (x_magnitude > y_magnitude) ? x : y;
|
||||
result = (this.tap === true) ? "TAP" : result;
|
||||
return result;
|
||||
}
|
||||
var konami = {
|
||||
addEvent: function (obj, type, fn, ref_obj) {
|
||||
if (obj.addEventListener) obj.addEventListener(type, fn, false);
|
||||
else if (obj.attachEvent) {
|
||||
// IE
|
||||
obj["e" + type + fn] = fn;
|
||||
obj[type + fn] = function () {
|
||||
obj["e" + type + fn](window.event, ref_obj);
|
||||
};
|
||||
obj.attachEvent("on" + type, obj[type + fn]);
|
||||
}
|
||||
},
|
||||
removeEvent: function (obj, eventName, eventCallback) {
|
||||
if (obj.removeEventListener) {
|
||||
obj.removeEventListener(eventName, eventCallback);
|
||||
} else if (obj.attachEvent) {
|
||||
obj.detachEvent(eventName);
|
||||
}
|
||||
},
|
||||
input: "",
|
||||
pattern: "38384040373937396665",
|
||||
keydownHandler: function (e, ref_obj) {
|
||||
if (ref_obj) {
|
||||
konami = ref_obj;
|
||||
} // IE
|
||||
konami.input += e ? e.keyCode : event.keyCode;
|
||||
if (konami.input.length > konami.pattern.length) {
|
||||
konami.input = konami.input.substr(konami.input.length - konami.pattern.length);
|
||||
}
|
||||
if (konami.input === konami.pattern) {
|
||||
konami.code(konami._currentLink);
|
||||
konami.input = "";
|
||||
e.preventDefault();
|
||||
return false;
|
||||
}
|
||||
},
|
||||
load: function (link) {
|
||||
this._currentLink = link;
|
||||
this.addEvent(document, "keydown", this.keydownHandler, this);
|
||||
this.iphone.load(link);
|
||||
},
|
||||
unload: function () {
|
||||
this.removeEvent(document, "keydown", this.keydownHandler);
|
||||
this.iphone.unload();
|
||||
},
|
||||
code: function (link) {
|
||||
window.location = link;
|
||||
},
|
||||
iphone: {
|
||||
start_x: 0,
|
||||
start_y: 0,
|
||||
stop_x: 0,
|
||||
stop_y: 0,
|
||||
tap: false,
|
||||
capture: false,
|
||||
orig_keys: "",
|
||||
keys: [
|
||||
"UP",
|
||||
"UP",
|
||||
"DOWN",
|
||||
"DOWN",
|
||||
"LEFT",
|
||||
"RIGHT",
|
||||
"LEFT",
|
||||
"RIGHT",
|
||||
"TAP",
|
||||
"TAP",
|
||||
],
|
||||
input: [],
|
||||
code: function (link) {
|
||||
konami.code(link);
|
||||
},
|
||||
touchmoveHandler: function (e) {
|
||||
if (e.touches.length === 1 && konami.iphone.capture === true) {
|
||||
var touch = e.touches[0];
|
||||
konami.iphone.stop_x = touch.pageX;
|
||||
konami.iphone.stop_y = touch.pageY;
|
||||
konami.iphone.tap = false;
|
||||
konami.iphone.capture = false;
|
||||
konami.iphone.check_direction();
|
||||
}
|
||||
}
|
||||
},
|
||||
touchendHandler: function () {
|
||||
konami.iphone.input.push(konami.iphone.check_direction());
|
||||
|
||||
typeof callback === "string" && konami.load(callback);
|
||||
if (typeof callback === "function") {
|
||||
konami.code = callback;
|
||||
konami.load();
|
||||
}
|
||||
if (konami.iphone.input.length > konami.iphone.keys.length)
|
||||
konami.iphone.input.shift();
|
||||
|
||||
return konami;
|
||||
if (konami.iphone.input.length === konami.iphone.keys.length) {
|
||||
var match = true;
|
||||
for (var i = 0; i < konami.iphone.keys.length; i++) {
|
||||
if (konami.iphone.input[i] !== konami.iphone.keys[i]) {
|
||||
match = false;
|
||||
}
|
||||
}
|
||||
if (match) {
|
||||
konami.iphone.code(konami._currentLink);
|
||||
}
|
||||
}
|
||||
},
|
||||
touchstartHandler: function (e) {
|
||||
konami.iphone.start_x = e.changedTouches[0].pageX;
|
||||
konami.iphone.start_y = e.changedTouches[0].pageY;
|
||||
konami.iphone.tap = true;
|
||||
konami.iphone.capture = true;
|
||||
},
|
||||
load: function (link) {
|
||||
this.orig_keys = this.keys;
|
||||
konami.addEvent(document, "touchmove", this.touchmoveHandler);
|
||||
konami.addEvent(document, "touchend", this.touchendHandler, false);
|
||||
konami.addEvent(document, "touchstart", this.touchstartHandler);
|
||||
},
|
||||
unload: function () {
|
||||
konami.removeEvent(document, "touchmove", this.touchmoveHandler);
|
||||
konami.removeEvent(document, "touchend", this.touchendHandler);
|
||||
konami.removeEvent(document, "touchstart", this.touchstartHandler);
|
||||
},
|
||||
check_direction: function () {
|
||||
x_magnitude = Math.abs(this.start_x - this.stop_x);
|
||||
y_magnitude = Math.abs(this.start_y - this.stop_y);
|
||||
x = this.start_x - this.stop_x < 0 ? "RIGHT" : "LEFT";
|
||||
y = this.start_y - this.stop_y < 0 ? "DOWN" : "UP";
|
||||
result = x_magnitude > y_magnitude ? x : y;
|
||||
result = this.tap === true ? "TAP" : result;
|
||||
return result;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
typeof callback === "string" && konami.load(callback);
|
||||
if (typeof callback === "function") {
|
||||
konami.code = callback;
|
||||
konami.load();
|
||||
}
|
||||
|
||||
return konami;
|
||||
};
|
||||
|
||||
|
||||
if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') {
|
||||
module.exports = Konami;
|
||||
if (typeof module !== "undefined" && typeof module.exports !== "undefined") {
|
||||
module.exports = Konami;
|
||||
} else {
|
||||
if (typeof define === 'function' && define.amd) {
|
||||
define([], function() {
|
||||
return Konami;
|
||||
});
|
||||
} else {
|
||||
window.Konami = Konami;
|
||||
}
|
||||
if (typeof define === "function" && define.amd) {
|
||||
define([], function () {
|
||||
return Konami;
|
||||
});
|
||||
} else {
|
||||
window.Konami = Konami;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
<link rel="stylesheet" type="text/css" href="https://cloud.typography.com/7584432/7586812/css/fonts.css" />
|
||||
<script type="text/javascript">$('#searchbox').hide(0);</script>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
type="text/css"
|
||||
href="https://cloud.typography.com/7584432/7586812/css/fonts.css"
|
||||
/>
|
||||
<script type="text/javascript">
|
||||
$("#searchbox").hide(0);
|
||||
</script>
|
||||
<!--Alabaster (krTheme++) Hacks -->
|
||||
|
||||
<!-- CSS Adjustments (I'm very picky.) -->
|
||||
@@ -22,10 +28,11 @@
|
||||
}
|
||||
|
||||
pre,
|
||||
.pre,
|
||||
.class em,
|
||||
.descname,
|
||||
.method em {
|
||||
font-family: "Operator Mono SSm A", "Operator Mono SSm B" !important;
|
||||
font-family: "Operator Mono SSm A", "Operator Mono SSm B", monospace !important;
|
||||
font-weight: 400 !important;
|
||||
}
|
||||
|
||||
@@ -38,9 +45,7 @@
|
||||
}
|
||||
|
||||
.method {
|
||||
|
||||
margin-bottom: 2em;
|
||||
|
||||
}
|
||||
|
||||
.si,
|
||||
@@ -49,9 +54,13 @@
|
||||
.method em,
|
||||
.class em {
|
||||
font-style: italic !important;
|
||||
color: grey;
|
||||
}
|
||||
|
||||
.method em,
|
||||
.class em {
|
||||
margin-left: 0.3em;
|
||||
margin-right: 0.3em;
|
||||
color: grey;
|
||||
}
|
||||
|
||||
.method p,
|
||||
@@ -71,6 +80,9 @@
|
||||
display: none;
|
||||
}
|
||||
|
||||
#testimonials p.attribution {
|
||||
margin-top: -1em;
|
||||
}
|
||||
|
||||
/* "Quick Search" should be not be shown for now. */
|
||||
div#searchbox h3 {
|
||||
@@ -105,19 +117,15 @@
|
||||
</style>
|
||||
|
||||
<!-- Analytics tracking for Kenneth. -->
|
||||
<script type="text/javascript">
|
||||
var _gauges = _gauges || [];
|
||||
(function () {
|
||||
var t = document.createElement('script');
|
||||
t.type = 'text/javascript';
|
||||
t.async = true;
|
||||
t.id = 'gauges-tracker';
|
||||
t.setAttribute('data-site-id', '588f8e99c88d9013e60fa373');
|
||||
t.setAttribute('data-track-path', 'https://track.gaug.es/track.gif');
|
||||
t.src = 'https://d36ee2fcip1434.cloudfront.net/track.js';
|
||||
var s = document.getElementsByTagName('script')[0];
|
||||
s.parentNode.insertBefore(t, s);
|
||||
})();
|
||||
<script async src="https://www.googletagmanager.com/gtag/js?id=UA-127383416-1"></script>
|
||||
<script>
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag() {
|
||||
dataLayer.push(arguments);
|
||||
}
|
||||
gtag("js", new Date());
|
||||
|
||||
gtag("config", "UA-127383416-1");
|
||||
</script>
|
||||
|
||||
<!-- There are no more hacks. -->
|
||||
@@ -126,7 +134,10 @@
|
||||
|
||||
<script src="{{ pathto('_static/', 1) }}/konami.js"></script>
|
||||
<script>
|
||||
var easter_egg = new Konami('https://www.myfortunecookie.co.uk/fortunes/' + (Math.floor(Math.random() * 152) + 1));
|
||||
var easter_egg = new Konami(
|
||||
"https://www.myfortunecookie.co.uk/fortunes/" +
|
||||
(Math.floor(Math.random() * 152) + 1)
|
||||
);
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@@ -136,67 +147,94 @@
|
||||
</style>
|
||||
|
||||
<!-- GitHub Logo -->
|
||||
<a href="https://github.com/kennethreitz/responder" class="github-corner" aria-label="View source on GitHub">
|
||||
<svg width="80" height="80" viewBox="0 0 250 250" style="fill:#151513; color:#fff; position: absolute; top: 0; border: 0; right: 0;"
|
||||
aria-hidden="true">
|
||||
<a
|
||||
href="https://github.com/kennethreitz/responder"
|
||||
class="github-corner"
|
||||
aria-label="View source on GitHub"
|
||||
>
|
||||
<svg
|
||||
width="80"
|
||||
height="80"
|
||||
viewBox="0 0 250 250"
|
||||
style="fill: #151513; color: #fff; position: absolute; top: 0; border: 0; right: 0"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z"></path>
|
||||
<path d="M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2"
|
||||
fill="currentColor" style="transform-origin: 130px 106px;" class="octo-arm"></path>
|
||||
<path d="M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z"
|
||||
fill="currentColor" class="octo-body"></path>
|
||||
<path
|
||||
d="M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2"
|
||||
fill="currentColor"
|
||||
style="transform-origin: 130px 106px"
|
||||
class="octo-arm"
|
||||
></path>
|
||||
<path
|
||||
d="M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z"
|
||||
fill="currentColor"
|
||||
class="octo-body"
|
||||
></path>
|
||||
</svg>
|
||||
</a>
|
||||
<style>
|
||||
.github-corner:hover .octo-arm {
|
||||
animation: octocat-wave 560ms ease-in-out
|
||||
animation: octocat-wave 560ms ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes octocat-wave {
|
||||
|
||||
0%,
|
||||
100% {
|
||||
transform: rotate(0)
|
||||
transform: rotate(0);
|
||||
}
|
||||
|
||||
20%,
|
||||
60% {
|
||||
transform: rotate(-25deg)
|
||||
transform: rotate(-25deg);
|
||||
}
|
||||
|
||||
40%,
|
||||
80% {
|
||||
transform: rotate(10deg)
|
||||
transform: rotate(10deg);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width:500px) {
|
||||
@media (max-width: 500px) {
|
||||
.github-corner:hover .octo-arm {
|
||||
animation: none
|
||||
animation: none;
|
||||
}
|
||||
|
||||
.github-corner .octo-arm {
|
||||
animation: octocat-wave 560ms ease-in-out
|
||||
animation: octocat-wave 560ms ease-in-out;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
<!-- That was not a hack. That was art.
|
||||
|
||||
<!-- UserVoice JavaScript SDK (only needed once on a page) -->
|
||||
<script>(function () { var uv = document.createElement('script'); uv.type = 'text/javascript'; uv.async = true; uv.src = '//widget.uservoice.com/f4AQraEfwInlMzkexfRLg.js'; var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(uv, s) })()</script>
|
||||
<script>
|
||||
(function () {
|
||||
var uv = document.createElement("script");
|
||||
uv.type = "text/javascript";
|
||||
uv.async = true;
|
||||
uv.src = "//widget.uservoice.com/f4AQraEfwInlMzkexfRLg.js";
|
||||
var s = document.getElementsByTagName("script")[0];
|
||||
s.parentNode.insertBefore(uv, s);
|
||||
})();
|
||||
</script>
|
||||
|
||||
<!-- A tab to launch the Classic Widget -->
|
||||
<script>
|
||||
UserVoice = window.UserVoice || [];
|
||||
UserVoice.push(['showTab', 'classic_widget', {
|
||||
mode: 'feedback',
|
||||
primary_color: '#fa8c28',
|
||||
link_color: '#0a8cc6',
|
||||
forum_id: 913660,
|
||||
tab_label: 'Got feedback?',
|
||||
tab_color: '#00994f',
|
||||
tab_position: 'bottom-left',
|
||||
tab_inverted: true
|
||||
}]);
|
||||
UserVoice.push([
|
||||
"showTab",
|
||||
"classic_widget",
|
||||
{
|
||||
mode: "feedback",
|
||||
primary_color: "#fa8c28",
|
||||
link_color: "#0a8cc6",
|
||||
forum_id: 913660,
|
||||
tab_label: "Got feedback?",
|
||||
tab_color: "#00994f",
|
||||
tab_position: "bottom-left",
|
||||
tab_inverted: true,
|
||||
},
|
||||
]);
|
||||
</script>
|
||||
|
||||
@@ -1,33 +1,93 @@
|
||||
<p class="logo">
|
||||
<a href="{{ pathto(master_doc) }}">
|
||||
<img class="logo" src="{{ pathto('_static/responder.png', 1) }}" title="https://kennethreitz.org/tattoos" />
|
||||
<img
|
||||
class="logo"
|
||||
src="{{ pathto('_static/responder.png', 1) }}"
|
||||
title="https://kennethreitz.org/tattoos"
|
||||
/>
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<iframe src="https://ghbtns.com/github-btn.html?user=kennethreitz&repo=responder&type=watch&count=true&size=large"
|
||||
allowtransparency="true" frameborder="0" scrolling="0" width="200px" height="35px"></iframe>
|
||||
<iframe
|
||||
src="https://ghbtns.com/github-btn.html?user=kennethreitz&repo=responder&type=watch&count=true&size=large"
|
||||
allowtransparency="true"
|
||||
frameborder="0"
|
||||
scrolling="0"
|
||||
width="200px"
|
||||
height="35px"
|
||||
></iframe>
|
||||
</p>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://cdn.jsdelivr.net/npm/docsearch.js@2/dist/cdn/docsearch.min.css"
|
||||
/>
|
||||
<style>
|
||||
.algolia-autocomplete {
|
||||
width: 100%;
|
||||
height: 1.5em;
|
||||
}
|
||||
.algolia-autocomplete a {
|
||||
border-bottom: none !important;
|
||||
}
|
||||
#doc_search {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
<input id="doc_search" placeholder="Search the doc" autofocus />
|
||||
<script
|
||||
type="text/javascript"
|
||||
src="https://cdn.jsdelivr.net/npm/docsearch.js@2/dist/cdn/docsearch.min.js"
|
||||
onload="docsearch({
|
||||
apiKey: 'ac965312db252e0496283c75c6f76f0b',
|
||||
indexName: 'python-responder',
|
||||
inputSelector: '#doc_search',
|
||||
debug: false // Set debug to true if you want to inspect the dropdown
|
||||
})"
|
||||
async
|
||||
></script>
|
||||
|
||||
<p>
|
||||
<strong>Responder</strong> is a web service framework, written for human beings.
|
||||
</p>
|
||||
<p><strong>Responder</strong> is a web service framework, written for human beings.</p>
|
||||
|
||||
<h3>Stay Informed</h3>
|
||||
<p>Receive updates on new releases and upcoming projects.</p>
|
||||
|
||||
<p><iframe src="https://ghbtns.com/github-btn.html?user=kennethreitz&type=follow&count=true" allowtransparency="true"
|
||||
frameborder="0" scrolling="0" width="200" height="20"></iframe></p>
|
||||
|
||||
<p><a href="https://twitter.com/kennethreitz" class="twitter-follow-button" data-show-count="false">Follow
|
||||
@kennethreitz</a>
|
||||
<script>!function (d, s, id) { var js, fjs = d.getElementsByTagName(s)[0], p = /^http:/.test(d.location) ? 'http' : 'https'; if (!d.getElementById(id)) { js = d.createElement(s); js.id = id; js.src = p + '://platform.twitter.com/widgets.js'; fjs.parentNode.insertBefore(js, fjs); } }(document, 'script', 'twitter-wjs');</script>
|
||||
<p>
|
||||
<iframe
|
||||
src="https://ghbtns.com/github-btn.html?user=kennethreitz&type=follow&count=true"
|
||||
allowtransparency="true"
|
||||
frameborder="0"
|
||||
scrolling="0"
|
||||
width="200"
|
||||
height="20"
|
||||
></iframe>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<a
|
||||
href="https://twitter.com/kennethreitz"
|
||||
class="twitter-follow-button"
|
||||
data-show-count="false"
|
||||
>Follow @kennethreitz</a
|
||||
>
|
||||
<script>
|
||||
!(function (d, s, id) {
|
||||
var js,
|
||||
fjs = d.getElementsByTagName(s)[0],
|
||||
p = /^http:/.test(d.location) ? "http" : "https";
|
||||
if (!d.getElementById(id)) {
|
||||
js = d.createElement(s);
|
||||
js.id = id;
|
||||
js.src = p + "://platform.twitter.com/widgets.js";
|
||||
fjs.parentNode.insertBefore(js, fjs);
|
||||
}
|
||||
})(document, "script", "twitter-wjs");
|
||||
</script>
|
||||
</p>
|
||||
|
||||
<h3>Useful Links</h3>
|
||||
<ul>
|
||||
|
||||
<li><a href="http://github.com/kennethreitz/responder">Responder @ GitHub</a></li>
|
||||
<li><a href="http://pypi.python.org/pypi/responder">Responder @ PyPI</a></li>
|
||||
<li><a href="http://github.com/kennethreitz/responder/issues">Issue Tracker</a></li>
|
||||
|
||||
@@ -1,33 +1,93 @@
|
||||
<p class="logo">
|
||||
<a href="{{ pathto(master_doc) }}">
|
||||
<img class="logo" src="{{ pathto('_static/responder.png', 1) }}" title="https://kennethreitz.org/tattoos" />
|
||||
<img
|
||||
class="logo"
|
||||
src="{{ pathto('_static/responder.png', 1) }}"
|
||||
title="https://kennethreitz.org/tattoos"
|
||||
/>
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<iframe src="https://ghbtns.com/github-btn.html?user=kennethreitz&repo=responder&type=watch&count=true&size=large"
|
||||
allowtransparency="true" frameborder="0" scrolling="0" width="200px" height="35px"></iframe>
|
||||
<iframe
|
||||
src="https://ghbtns.com/github-btn.html?user=kennethreitz&repo=responder&type=watch&count=true&size=large"
|
||||
allowtransparency="true"
|
||||
frameborder="0"
|
||||
scrolling="0"
|
||||
width="200px"
|
||||
height="35px"
|
||||
></iframe>
|
||||
</p>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://cdn.jsdelivr.net/npm/docsearch.js@2/dist/cdn/docsearch.min.css"
|
||||
/>
|
||||
<style>
|
||||
.algolia-autocomplete {
|
||||
width: 100%;
|
||||
height: 1.5em;
|
||||
}
|
||||
.algolia-autocomplete a {
|
||||
border-bottom: none !important;
|
||||
}
|
||||
#doc_search {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
<input id="doc_search" placeholder="Search the doc" autofocus />
|
||||
<script
|
||||
type="text/javascript"
|
||||
src="https://cdn.jsdelivr.net/npm/docsearch.js@2/dist/cdn/docsearch.min.js"
|
||||
onload="docsearch({
|
||||
apiKey: 'ac965312db252e0496283c75c6f76f0b',
|
||||
indexName: 'python-responder',
|
||||
inputSelector: '#doc_search',
|
||||
debug: false // Set debug to true if you want to inspect the dropdown
|
||||
})"
|
||||
async
|
||||
></script>
|
||||
|
||||
<p>
|
||||
<strong>Responder</strong> is a web service framework, written for human beings.
|
||||
</p>
|
||||
<p><strong>Responder</strong> is a web service framework, written for human beings.</p>
|
||||
|
||||
<h3>Stay Informed</h3>
|
||||
<p>Receive updates on new releases and upcoming projects.</p>
|
||||
|
||||
<p><iframe src="https://ghbtns.com/github-btn.html?user=kennethreitz&type=follow&count=true" allowtransparency="true"
|
||||
frameborder="0" scrolling="0" width="200" height="20"></iframe></p>
|
||||
|
||||
<p><a href="https://twitter.com/kennethreitz" class="twitter-follow-button" data-show-count="false">Follow
|
||||
@kennethreitz</a>
|
||||
<script>!function (d, s, id) { var js, fjs = d.getElementsByTagName(s)[0], p = /^http:/.test(d.location) ? 'http' : 'https'; if (!d.getElementById(id)) { js = d.createElement(s); js.id = id; js.src = p + '://platform.twitter.com/widgets.js'; fjs.parentNode.insertBefore(js, fjs); } }(document, 'script', 'twitter-wjs');</script>
|
||||
<p>
|
||||
<iframe
|
||||
src="https://ghbtns.com/github-btn.html?user=kennethreitz&type=follow&count=true"
|
||||
allowtransparency="true"
|
||||
frameborder="0"
|
||||
scrolling="0"
|
||||
width="200"
|
||||
height="20"
|
||||
></iframe>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<a
|
||||
href="https://twitter.com/kennethreitz"
|
||||
class="twitter-follow-button"
|
||||
data-show-count="false"
|
||||
>Follow @kennethreitz</a
|
||||
>
|
||||
<script>
|
||||
!(function (d, s, id) {
|
||||
var js,
|
||||
fjs = d.getElementsByTagName(s)[0],
|
||||
p = /^http:/.test(d.location) ? "http" : "https";
|
||||
if (!d.getElementById(id)) {
|
||||
js = d.createElement(s);
|
||||
js.id = id;
|
||||
js.src = p + "://platform.twitter.com/widgets.js";
|
||||
fjs.parentNode.insertBefore(js, fjs);
|
||||
}
|
||||
})(document, "script", "twitter-wjs");
|
||||
</script>
|
||||
</p>
|
||||
|
||||
<h3>Useful Links</h3>
|
||||
<ul>
|
||||
|
||||
<li><a href="http://github.com/kennethreitz/responder">Responder @ GitHub</a></li>
|
||||
<li><a href="http://pypi.python.org/pypi/responder">Responder @ PyPI</a></li>
|
||||
<li><a href="http://github.com/kennethreitz/responder/issues">Issue Tracker</a></li>
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
|
||||
API Documentation
|
||||
=================
|
||||
|
||||
|
||||
Web Service (API) Class
|
||||
-----------------------
|
||||
.. module:: responder
|
||||
|
||||
.. autoclass:: API
|
||||
:inherited-members:
|
||||
|
||||
Requests & Responses
|
||||
--------------------
|
||||
|
||||
|
||||
.. autoclass:: Request
|
||||
:inherited-members:
|
||||
|
||||
.. autoclass:: Response
|
||||
:inherited-members:
|
||||
|
||||
|
||||
Utility Functions
|
||||
-----------------
|
||||
|
||||
.. autofunction:: responder.API.status_codes.is_100
|
||||
|
||||
.. autofunction:: responder.API.status_codes.is_200
|
||||
|
||||
.. autofunction:: responder.API.status_codes.is_300
|
||||
|
||||
.. autofunction:: responder.API.status_codes.is_400
|
||||
|
||||
.. autofunction:: responder.API.status_codes.is_500
|
||||
+3
-15
@@ -20,23 +20,11 @@
|
||||
# -- Project information -----------------------------------------------------
|
||||
|
||||
project = "responder"
|
||||
copyright = "2018, A Kenneth Reitz project"
|
||||
copyright = "2024, A Kenneth Reitz project"
|
||||
author = "Kenneth Reitz"
|
||||
|
||||
# The short X.Y version
|
||||
import os
|
||||
|
||||
# Path hackery to get current version number.
|
||||
here = os.path.abspath(os.path.dirname(__file__))
|
||||
|
||||
about = {}
|
||||
with open(os.path.join(here, "..", "..", "responder", "__version__.py")) as f:
|
||||
exec(f.read(), about)
|
||||
|
||||
version = about["__version__"]
|
||||
# The full version, including alpha/beta/rc tags
|
||||
release = about["__version__"]
|
||||
|
||||
version = ""
|
||||
|
||||
# -- General configuration ---------------------------------------------------
|
||||
|
||||
@@ -76,7 +64,7 @@ master_doc = "index"
|
||||
#
|
||||
# This is also used if you do content translation via gettext catalogs.
|
||||
# Usually you set "language" from the command line for these cases.
|
||||
language = None
|
||||
language = "en"
|
||||
|
||||
# List of patterns, relative to source directory, that match files and
|
||||
# directories to ignore when looking for source files.
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
Deploying Responder
|
||||
===================
|
||||
|
||||
You can deploy Responder anywhere you can deploy a basic Python application.
|
||||
|
||||
Docker Deployment
|
||||
-----------------
|
||||
|
||||
Assuming existing ``api.py`` and ``Pipfile.lock`` containing ``responder``.
|
||||
|
||||
``Dockerfile``::
|
||||
|
||||
FROM kennethreitz/pipenv
|
||||
ENV PORT '80'
|
||||
COPY . /app
|
||||
CMD python3 api.py
|
||||
EXPOSE 80
|
||||
|
||||
That's it!
|
||||
|
||||
Heroku Deployment
|
||||
-----------------
|
||||
|
||||
The basics::
|
||||
|
||||
$ mkdir my-api
|
||||
$ cd my-api
|
||||
$ git init
|
||||
$ heroku create
|
||||
...
|
||||
|
||||
Install Responder::
|
||||
|
||||
$ pipenv install responder
|
||||
...
|
||||
|
||||
Write out an ``api.py``::
|
||||
|
||||
import responder
|
||||
|
||||
api = responder.API()
|
||||
|
||||
@api.route("/")
|
||||
async def hello(req, resp):
|
||||
resp.text = "hello, world!"
|
||||
|
||||
if __name__ == "__main__":
|
||||
api.run()
|
||||
|
||||
Write out a ``Procfile``::
|
||||
|
||||
web: python api.py
|
||||
|
||||
That's it! Next, we commit and push to Heroku::
|
||||
|
||||
$ git add -A
|
||||
$ git commit -m 'initial commit'
|
||||
$ git push heroku master
|
||||
+100
-137
@@ -3,130 +3,103 @@
|
||||
You can adapt this file completely to your liking, but it should at least
|
||||
contain the root `toctree` directive.
|
||||
|
||||
A familar HTTP Service Framework
|
||||
================================
|
||||
A familiar HTTP Service Framework
|
||||
=================================
|
||||
|
||||
The Python world certainly doesn't need more web frameworks. But, it does need more creativity, so I thought I'd bring some of my ideas to the table and see what I could come up with.
|
||||
|Build Status| |image1| |image2| |image3| |image4| |image5|
|
||||
|
||||
But will it blend?
|
||||
------------------
|
||||
.. |Build Status| image:: https://github.com/kennethreitz/responder/actions/workflows/test.yaml/badge.svg
|
||||
:target: https://github.com/kennethreitz/responder/actions/workflows/test.yaml
|
||||
.. |image1| image:: https://img.shields.io/pypi/v/responder.svg
|
||||
:target: https://pypi.org/project/responder/
|
||||
.. |image2| image:: https://img.shields.io/pypi/l/responder.svg
|
||||
:target: https://pypi.org/project/responder/
|
||||
.. |image3| image:: https://img.shields.io/pypi/pyversions/responder.svg
|
||||
:target: https://pypi.org/project/responder/
|
||||
.. |image4| image:: https://img.shields.io/github/contributors/kennethreitz/responder.svg
|
||||
:target: https://github.com/kennethreitz/responder/graphs/contributors
|
||||
.. |image5| image:: https://img.shields.io/badge/Say%20Thanks-!-1EAEDB.svg
|
||||
:target: https://saythanks.io/to/kennethreitz
|
||||
|
||||
::
|
||||
.. code:: python
|
||||
|
||||
import responder
|
||||
import responder
|
||||
|
||||
api = responder.API()
|
||||
api = responder.API()
|
||||
|
||||
@api.route("/{greeting}")
|
||||
def greet_world(req, resp, *, greeting):
|
||||
resp.text = f"{greeting}, world!"
|
||||
@api.route("/{greeting}")
|
||||
async def greet_world(req, resp, *, greeting):
|
||||
resp.text = f"{greeting}, world!"
|
||||
|
||||
if __name__ == '__main__':
|
||||
api.run()
|
||||
if __name__ == '__main__':
|
||||
api.run()
|
||||
|
||||
Powered by `Starlette <https://www.starlette.io/>`_. That ``async`` declaration is optional.
|
||||
|
||||
This gets you a WSGI app, with WhiteNoise pre-installed, jinja2 templating (without additional imports), and a production webserver (ready for slowloris attacks), serving up requests with gzip compression automatically.
|
||||
This gets you a ASGI app, with a production static files server
|
||||
(`WhiteNoise <http://whitenoise.evans.io/en/stable/>`_)
|
||||
pre-installed, jinja2 templating (without additional imports), and a
|
||||
production webserver based on uvloop, serving up requests with
|
||||
automatic gzip compression.
|
||||
|
||||
-------------
|
||||
Features
|
||||
--------
|
||||
|
||||
Class-based views (and setting some headers and stuff)::
|
||||
- A pleasant API, with a single import statement.
|
||||
- Class-based views without inheritance.
|
||||
- `ASGI <https://asgi.readthedocs.io>`_ framework, the future of Python web services.
|
||||
- WebSocket support!
|
||||
- The ability to mount any ASGI / WSGI app at a subroute.
|
||||
- `f-string syntax <https://docs.python.org/3/whatsnew/3.6.html#pep-498-formatted-string-literals>`_ route declaration.
|
||||
- Mutable response object, passed into each view. No need to return anything.
|
||||
- Background tasks, spawned off in a ``ThreadPoolExecutor``.
|
||||
- GraphQL (with *GraphiQL*) support!
|
||||
- OpenAPI schema generation, with interactive documentation!
|
||||
- Single-page webapp support!
|
||||
|
||||
@api.route("/{greeting}")
|
||||
class GreetingResource:
|
||||
def on_request(req, resp, *, greeting): # or on_get...
|
||||
resp.text = f"{greeting}, world!"
|
||||
resp.headers.update({'X-Life': '42'})
|
||||
resp.status_code = api.status_codes.HTTP_416
|
||||
|
||||
|
||||
Render a template, with arguments::
|
||||
|
||||
|
||||
@api.route("/{greeting}")
|
||||
def greet_world(req, resp, *, greeting):
|
||||
resp.content = api.template("index.html", greeting=greeting)
|
||||
|
||||
|
||||
The ``api`` instance is available as an object during template rendering.
|
||||
|
||||
Serve a GraphQL API::
|
||||
|
||||
import graphene
|
||||
|
||||
class Query(graphene.ObjectType):
|
||||
hello = graphene.String(name=graphene.String(default_value="stranger"))
|
||||
|
||||
def resolve_hello(self, info, name):
|
||||
return "Hello " + name
|
||||
|
||||
api.add_route("/graph", graphene.Schema(query=Query))
|
||||
|
||||
|
||||
We can then send a query to our service::
|
||||
>>> requests = api.session()
|
||||
>>> r = requests.get("http://;/graph", params={"query": "{ hello }"})
|
||||
>>> r.json()
|
||||
{'data': {'hello': 'Hello stranger'}}
|
||||
|
||||
|
||||
Or, request YAML back::
|
||||
|
||||
>>> r = requests.get("http://;/graph", params={"query": "{ hello(name:\"john\") }"}, headers={"Accept": "application/x-yaml"})
|
||||
>>> print(r.text)
|
||||
data: {hello: Hello john}
|
||||
|
||||
|
||||
|
||||
Want HSTS?
|
||||
|
||||
::
|
||||
|
||||
api = responder.API(enable_hsts=True)
|
||||
|
||||
|
||||
Boom. ✨🍰✨
|
||||
|
||||
|
||||
The Basic Idea
|
||||
--------------
|
||||
|
||||
The primary concept here is to bring the nicities 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).
|
||||
- Case-insensitive `req.headers` dict (from Requests directly).
|
||||
- `resp.status_code`, `req.method`, `req.url`, and other familar friends.
|
||||
|
||||
New Ideas
|
||||
---------
|
||||
|
||||
- **A built in testing client that uses the actual Requests you know and love**.
|
||||
- The ability to mount other WSGI apps easily.
|
||||
- Automatic gzipped-responses (still working on that).
|
||||
- In addition to Falcon's ``on_get``, ``on_post``, etc methods, Responder features an `on_request` method, which gets called on every type of request, much like Requests.
|
||||
- WhiteNoise is built-in, for serving static files.
|
||||
- Waitress built-in as a production web server. I would have chosen Gunicorn, but it doesn't run on Windows. Plus, Waitress serves well to protect against slowloris attacks, making nginx unneccessary in production.
|
||||
- GraphQL support, via Graphene. The goal here is to have any GraphQL query exposable at any route, magically.
|
||||
|
||||
|
||||
Old Ideas
|
||||
---------
|
||||
|
||||
- Flask-style route expression, with new capabilities -- primarily, the ability to cast a parameter to integers as well as other types that are missing from Flask, all while using Python 3.6+'s new f-string syntax.
|
||||
|
||||
- I love Falcon's "every request and response is passed into to each view and mutated" methodology, especially `response.media`, and have used it here. In addition to supporting JSON, I have decided to support YAML as well, as Kubernetes is slowly taking over the world, and it uses YAML for all the things. Content-negotiation and all that.
|
||||
|
||||
Future Ideas
|
||||
Testimonials
|
||||
------------
|
||||
|
||||
- I want to be able to "mount" any WSGI app into a sub-route.
|
||||
- Cooke-based sessions are currently an afterthrought, as this is an API framework, but websites are APIs too.
|
||||
- Potentially support ASGI instead of WSGI. Will the tradeoffs be worth it? This is a question to ask. Procedural code works well for 90% use cases.
|
||||
- If frontend websites are supported, provide an official way to run webpack.
|
||||
“Pleasantly very taken with python-responder.
|
||||
`@kennethreitz <https://twitter.com/kennethreitz>`_ at his absolute
|
||||
best.”
|
||||
|
||||
—Rudraksh M.K.
|
||||
|
||||
|
||||
Installation
|
||||
============
|
||||
|
||||
..
|
||||
|
||||
"ASGI is going to enable all sorts of new high-performance web services. It's awesome to see Responder starting to take advantage of that."
|
||||
|
||||
—Tom Christie, author of `Django REST Framework`_
|
||||
|
||||
..
|
||||
|
||||
|
||||
“I love that you are exploring new patterns. Go go go!”
|
||||
|
||||
— Danny Greenfield, author of `Two Scoops of Django`_
|
||||
|
||||
|
||||
.. _Django REST Framework: https://www.django-rest-framework.org/
|
||||
.. _Two Scoops of Django: https://www.feldroy.com/two-scoops-press#two-scoops-of-django
|
||||
|
||||
User Guides
|
||||
-----------
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
quickstart
|
||||
tour
|
||||
deployment
|
||||
testing
|
||||
api
|
||||
|
||||
|
||||
Installing Responder
|
||||
--------------------
|
||||
|
||||
.. code-block:: shell
|
||||
|
||||
@@ -136,41 +109,31 @@ Installation
|
||||
Only **Python 3.6+** is supported.
|
||||
|
||||
|
||||
API Documentation
|
||||
=================
|
||||
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.
|
||||
|
||||
Web Service (API) Class
|
||||
-----------------------
|
||||
.. module:: responder
|
||||
- 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.
|
||||
|
||||
.. autoclass:: API
|
||||
:inherited-members:
|
||||
Ideas
|
||||
-----
|
||||
|
||||
Requests & Responses
|
||||
--------------------
|
||||
- Flask-style route expression, with new capabilities -- all while using Python 3.6+'s new f-string syntax.
|
||||
- I love Falcon's "every request and response is passed into each view and mutated" methodology, especially ``response.media``, and have used it here. In addition to supporting JSON, I have decided to support YAML as well, as Kubernetes is slowly taking over the world, and it uses YAML for all the things. Content-negotiation and all that.
|
||||
- **A built in testing client that uses the actual Requests you know and love**.
|
||||
- The ability to mount other WSGI apps easily.
|
||||
- Automatic gzipped-responses.
|
||||
- In addition to Falcon's ``on_get``, ``on_post``, etc methods, Responder features an ``on_request`` method, which gets called on every type of request, much like Requests.
|
||||
- A production static files server is built-in.
|
||||
- `Uvicorn <https://www.uvicorn.org/>`_ is built-in as a production web server. I would have chosen Gunicorn, but it doesn't run on Windows. Plus, Uvicorn serves well to protect against `slowloris <https://en.wikipedia.org/wiki/Slowloris_(computer_security)>`_ attacks, making nginx unnecessary in production.
|
||||
- GraphQL support, via Graphene. The goal here is to have any GraphQL query exposable at any route, magically.
|
||||
|
||||
|
||||
.. autoclass:: Request
|
||||
:inherited-members:
|
||||
|
||||
.. autoclass:: Response
|
||||
:inherited-members:
|
||||
|
||||
|
||||
Utility Functions
|
||||
-----------------
|
||||
|
||||
.. autofunction:: responder.API.status_codes.is_100
|
||||
|
||||
.. autofunction:: responder.API.status_codes.is_200
|
||||
|
||||
.. autofunction:: responder.API.status_codes.is_300
|
||||
|
||||
.. autofunction:: responder.API.status_codes.is_400
|
||||
|
||||
.. autofunction:: responder.API.status_codes.is_500
|
||||
|
||||
Indices and tables
|
||||
==================
|
||||
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
Quick Start!
|
||||
============
|
||||
|
||||
This section of the documentation exists to provide an introduction to the Responder interface,
|
||||
as well as educate the user on basic functionality.
|
||||
|
||||
|
||||
Declare a Web Service
|
||||
---------------------
|
||||
|
||||
The first thing you need to do is declare a web service::
|
||||
|
||||
import responder
|
||||
|
||||
api = responder.API()
|
||||
|
||||
Hello World!
|
||||
------------
|
||||
|
||||
Then, you can add a view / route to it.
|
||||
|
||||
Here, we'll make the root URL say "hello world!"::
|
||||
|
||||
@api.route("/")
|
||||
def hello_world(req, resp):
|
||||
resp.text = "hello, world!"
|
||||
|
||||
Run the Server
|
||||
--------------
|
||||
|
||||
Next, we can run our web service easily, with ``api.run()``::
|
||||
|
||||
api.run()
|
||||
|
||||
This will spin up a production web server on port ``5042``, ready for incoming HTTP requests.
|
||||
|
||||
Note: you can pass ``port=5000`` if you want to customize the port. The ``PORT`` environment variable for established web service providers (e.g. Heroku) will automatically be honored and will set the listening address to ``0.0.0.0`` automatically (also configurable through the ``address`` keyword argument).
|
||||
|
||||
|
||||
Accept Route Arguments
|
||||
----------------------
|
||||
|
||||
If you want dynamic URLs, you can use Python's familiar *f-string syntax* to declare variables in your routes::
|
||||
|
||||
@api.route("/hello/{who}")
|
||||
def hello_to(req, resp, *, who):
|
||||
resp.text = f"hello, {who}!"
|
||||
|
||||
A ``GET`` request to ``/hello/brettcannon`` will result in a response of ``hello, brettcannon!``.
|
||||
|
||||
Type convertors are also available::
|
||||
|
||||
@api.route("/add/{a:int}/{b:int}")
|
||||
async def add(req, resp, *, a, b):
|
||||
resp.text = f"{a} + {b} = {a + b}"
|
||||
|
||||
Supported types: ``str``, ``int`` and ``float``.
|
||||
|
||||
Returning JSON / YAML
|
||||
---------------------
|
||||
|
||||
If you want your API to send back JSON, simply set the ``resp.media`` property to a JSON-serializable Python object::
|
||||
|
||||
|
||||
@api.route("/hello/{who}/json")
|
||||
def hello_to(req, resp, *, who):
|
||||
resp.media = {"hello": who}
|
||||
|
||||
A ``GET`` request to ``/hello/guido/json`` will result in a response of ``{'hello': 'guido'}``.
|
||||
|
||||
If the client requests YAML instead (with a header of ``Accept: application/x-yaml``), YAML will be sent.
|
||||
|
||||
Rendering a Template
|
||||
--------------------
|
||||
|
||||
Responder provides a built-in light `jinja2 <http://jinja.pocoo.org/docs/>`_ wrapper ``templates.Templates``
|
||||
|
||||
Usage::
|
||||
|
||||
from responder.templates import Templates
|
||||
|
||||
templates = Templates()
|
||||
|
||||
@api.route("/hello/{name}/html")
|
||||
def hello(req, resp, name):
|
||||
resp.html = templates.render("hello.html", name=name)
|
||||
|
||||
|
||||
Also a ``render_async`` is available::
|
||||
|
||||
templates = Templates(enable_async=True)
|
||||
resp.html = await templates.render_async("hello.html", who=who)
|
||||
|
||||
You can also use the existing ``api.template(filename, *args, **kwargs)`` to render templates::
|
||||
|
||||
@api.route("/hello/{who}/html")
|
||||
def hello_html(req, resp, *, who):
|
||||
resp.html = api.template('hello.html', who=who)
|
||||
|
||||
|
||||
Setting Response Status Code
|
||||
----------------------------
|
||||
|
||||
If you want to set the response status code, simply set ``resp.status_code``::
|
||||
|
||||
@api.route("/416")
|
||||
def teapot(req, resp):
|
||||
resp.status_code = api.status_codes.HTTP_416 # ...or 416
|
||||
|
||||
|
||||
Setting Response Headers
|
||||
------------------------
|
||||
|
||||
If you want to set a response header, like ``X-Pizza: 42``, simply modify the ``resp.headers`` dictionary::
|
||||
|
||||
@api.route("/pizza")
|
||||
def pizza_pizza(req, resp):
|
||||
resp.headers['X-Pizza'] = '42'
|
||||
|
||||
That's it!
|
||||
|
||||
|
||||
Receiving Data & Background Tasks
|
||||
---------------------------------
|
||||
|
||||
If you're expecting to read any request data, on the server, you need to declare your view as async and await the content.
|
||||
|
||||
Here, we'll process our data in the background, while responding immediately to the client::
|
||||
|
||||
import time
|
||||
|
||||
@api.route("/incoming")
|
||||
async def receive_incoming(req, resp):
|
||||
|
||||
@api.background.task
|
||||
def process_data(data):
|
||||
"""Just sleeps for three seconds, as a demo."""
|
||||
time.sleep(3)
|
||||
|
||||
|
||||
# Parse the incoming data as form-encoded.
|
||||
# Note: 'json' and 'yaml' formats are also automatically supported.
|
||||
data = await req.media()
|
||||
|
||||
# Process the data (in the background).
|
||||
process_data(data)
|
||||
|
||||
# Immediately respond that upload was successful.
|
||||
resp.media = {'success': True}
|
||||
|
||||
A ``POST`` request to ``/incoming`` will result in an immediate response of ``{'success': true}``.
|
||||
|
||||
|
||||
Here's a sample code to post a file with background::
|
||||
|
||||
@api.route("/")
|
||||
async def upload_file(req, resp):
|
||||
|
||||
@api.background.task
|
||||
def process_data(data):
|
||||
f = open('./{}'.format(data['file']['filename']), 'w')
|
||||
f.write(data['file']['content'].decode('utf-8'))
|
||||
f.close()
|
||||
|
||||
data = await req.media(format='files')
|
||||
process_data(data)
|
||||
|
||||
resp.media = {'success': 'ok'}
|
||||
|
||||
You can send a file easily with requests::
|
||||
|
||||
import requests
|
||||
|
||||
data = {'file': ('hello.txt', 'hello, world!', "text/plain")}
|
||||
r = requests.post('http://127.0.0.1:8210/file', files=data)
|
||||
|
||||
print(r.text)
|
||||
@@ -0,0 +1,81 @@
|
||||
Building and Testing with Responder
|
||||
===================================
|
||||
|
||||
Responder comes with a first-class, well supported test client for your ASGI web services: **Requests**.
|
||||
|
||||
Here, we'll go over the basics of setting up a proper Python package and adding testing to it.
|
||||
|
||||
The Basics
|
||||
----------
|
||||
|
||||
Your repository should look like this::
|
||||
|
||||
Pipfile Pipfile.lock api.py test_api.py
|
||||
|
||||
``$ cat api.py``::
|
||||
|
||||
import responder
|
||||
|
||||
api = responder.API()
|
||||
|
||||
@api.route("/")
|
||||
def hello_world(req, resp):
|
||||
resp.text = "hello, world!"
|
||||
|
||||
if __name__ == "__main__":
|
||||
api.run()
|
||||
|
||||
|
||||
``$ cat Pipfile``::
|
||||
|
||||
[[source]]
|
||||
url = "https://pypi.org/simple"
|
||||
verify_ssl = true
|
||||
name = "pypi"
|
||||
|
||||
[packages]
|
||||
responder = "*"
|
||||
|
||||
[dev-packages]
|
||||
pytest = "*"
|
||||
|
||||
[requires]
|
||||
python_version = "3.7"
|
||||
|
||||
[pipenv]
|
||||
allow_prereleases = true
|
||||
|
||||
Writing Tests
|
||||
-------------
|
||||
|
||||
``$ cat test_api.py``::
|
||||
|
||||
import pytest
|
||||
import api as service
|
||||
|
||||
@pytest.fixture
|
||||
def api():
|
||||
return service.api
|
||||
|
||||
|
||||
def test_hello_world(api):
|
||||
r = api.requests.get("/")
|
||||
assert r.text == "hello, world!"
|
||||
|
||||
``$ pytest``::
|
||||
|
||||
...
|
||||
========================== 1 passed in 0.10 seconds ==========================
|
||||
|
||||
|
||||
(Optional) Proper Python Package
|
||||
--------------------------------
|
||||
|
||||
Optionally, you can not rely on relative imports, and instead install your api as a proper package. This requires:
|
||||
|
||||
1. A `proper setup.py <https://github.com/kennethreitz/setup.py>`_ file.
|
||||
2. ``$ pipenv install -e . --dev``
|
||||
|
||||
This will allow you to only specify your dependencies once: in ``setup.py``. ``$ pipenv lock`` will automatically lock your transitive dependencies (e.g. Responder), even if it's not specified in the ``Pipfile``.
|
||||
|
||||
This will ensure that your application gets installed in every developer's environment, using Pipenv.
|
||||
@@ -0,0 +1,427 @@
|
||||
Feature Tour
|
||||
============
|
||||
|
||||
|
||||
Class-Based Views
|
||||
-----------------
|
||||
|
||||
Class-based views (and setting some headers and stuff)::
|
||||
|
||||
@api.route("/{greeting}")
|
||||
class GreetingResource:
|
||||
def on_request(self, req, resp, *, greeting): # or on_get...
|
||||
resp.text = f"{greeting}, world!"
|
||||
resp.headers.update({'X-Life': '42'})
|
||||
resp.status_code = api.status_codes.HTTP_416
|
||||
|
||||
|
||||
Background Tasks
|
||||
----------------
|
||||
|
||||
Here, you can spawn off a background thread to run any function, out-of-request::
|
||||
|
||||
@api.route("/")
|
||||
def hello(req, resp):
|
||||
|
||||
@api.background.task
|
||||
def sleep(s=10):
|
||||
time.sleep(s)
|
||||
print("slept!")
|
||||
|
||||
sleep()
|
||||
resp.content = "processing"
|
||||
|
||||
|
||||
GraphQL
|
||||
-------
|
||||
|
||||
Serve a GraphQL API::
|
||||
|
||||
import graphene
|
||||
|
||||
class Query(graphene.ObjectType):
|
||||
hello = graphene.String(name=graphene.String(default_value="stranger"))
|
||||
|
||||
def resolve_hello(self, info, name):
|
||||
return f"Hello {name}"
|
||||
|
||||
schema = graphene.Schema(query=Query)
|
||||
view = responder.ext.GraphQLView(api=api, schema=schema)
|
||||
|
||||
api.add_route("/graph", view)
|
||||
|
||||
Visiting the endpoint will render a *GraphiQL* instance, in the browser.
|
||||
|
||||
You can make use of Responder's Request and Response objects in your GraphQL resolvers through ``info.context['request']`` and ``info.context['response']``.
|
||||
|
||||
|
||||
OpenAPI Schema Support
|
||||
----------------------
|
||||
|
||||
Responder comes with built-in support for OpenAPI / marshmallow
|
||||
|
||||
New in Responder `1.4.0`::
|
||||
|
||||
import responder
|
||||
from responder.ext.schema import Schema as OpenAPISchema
|
||||
from marshmallow import Schema, fields
|
||||
|
||||
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()
|
||||
|
||||
schema = OpenAPISchema(
|
||||
app=api,
|
||||
title="Web Service",
|
||||
version="1.0",
|
||||
openapi="3.0.2",
|
||||
description="A simple pet store",
|
||||
terms_of_service="http://example.com/terms/",
|
||||
contact=contact,
|
||||
license=license,
|
||||
)
|
||||
|
||||
@schema.schema("Pet")
|
||||
class PetSchema(Schema):
|
||||
name = fields.Str()
|
||||
|
||||
|
||||
@api.route("/")
|
||||
def route(req, resp):
|
||||
"""A cute furry animal endpoint.
|
||||
---
|
||||
get:
|
||||
description: Get a random pet
|
||||
responses:
|
||||
200:
|
||||
description: A pet to be returned
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Pet'
|
||||
"""
|
||||
resp.media = PetSchema().dump({"name": "little orange"})
|
||||
|
||||
|
||||
Old way *It's recommended to use the code above* ::
|
||||
|
||||
import responder
|
||||
from marshmallow import Schema, fields
|
||||
|
||||
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",
|
||||
description="A simple pet store",
|
||||
terms_of_service="http://example.com/terms/",
|
||||
contact=contact,
|
||||
license=license,
|
||||
)
|
||||
|
||||
@api.schema("Pet")
|
||||
class PetSchema(Schema):
|
||||
name = fields.Str()
|
||||
|
||||
@api.route("/")
|
||||
def route(req, resp):
|
||||
"""A cute furry animal endpoint.
|
||||
---
|
||||
get:
|
||||
description: Get a random pet
|
||||
responses:
|
||||
200:
|
||||
description: A pet to be returned
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Pet'
|
||||
"""
|
||||
resp.media = PetSchema().dump({"name": "little orange"})
|
||||
|
||||
::
|
||||
|
||||
>>> r = api.session().get("http://;/schema.yml")
|
||||
|
||||
>>> print(r.text)
|
||||
components:
|
||||
parameters: {}
|
||||
responses: {}
|
||||
schemas:
|
||||
Pet:
|
||||
properties:
|
||||
name: {type: string}
|
||||
type: object
|
||||
securitySchemes: {}
|
||||
info:
|
||||
contact: {email: support@example.com, name: API Support, url: 'http://www.example.com/support'}
|
||||
description: This is a sample server for a pet store.
|
||||
license: {name: Apache 2.0, url: 'https://www.apache.org/licenses/LICENSE-2.0.html'}
|
||||
termsOfService: http://example.com/terms/
|
||||
title: Web Service
|
||||
version: 1.0
|
||||
openapi: 3.0.2
|
||||
paths:
|
||||
/:
|
||||
get:
|
||||
description: Get a random pet
|
||||
responses:
|
||||
200: {description: A pet to be returned, schema: $ref: "#/components/schemas/Pet"}
|
||||
tags: []
|
||||
|
||||
|
||||
Interactive Documentation
|
||||
-------------------------
|
||||
|
||||
Responder can automatically supply API Documentation for you. Using the example above
|
||||
|
||||
The new and recommended way::
|
||||
|
||||
...
|
||||
from responder.ext.schema import Schema
|
||||
...
|
||||
api = responder.API()
|
||||
|
||||
schema = Schema(
|
||||
app=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,
|
||||
)
|
||||
|
||||
|
||||
The old way ::
|
||||
|
||||
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,
|
||||
)
|
||||
|
||||
This will make ``/docs`` render interactive documentation for your API.
|
||||
|
||||
Mount a WSGI / ASGI Apps (e.g. Flask, Starlette,...)
|
||||
----------------------------------------------------
|
||||
|
||||
Responder gives you the ability to mount another ASGI / WSGI app at a subroute::
|
||||
|
||||
import responder
|
||||
from flask import Flask
|
||||
|
||||
api = responder.API()
|
||||
flask = Flask(__name__)
|
||||
|
||||
@flask.route('/')
|
||||
def hello():
|
||||
return 'hello'
|
||||
|
||||
api.mount('/flask', flask)
|
||||
|
||||
That's it!
|
||||
|
||||
Single-Page Web Apps
|
||||
--------------------
|
||||
|
||||
If you have a single-page webapp, you can tell Responder to serve up your ``static/index.html`` at a route, like so::
|
||||
|
||||
api.add_route("/", static=True)
|
||||
|
||||
This will make ``index.html`` the default response to all undefined routes.
|
||||
|
||||
Reading / Writing Cookies
|
||||
-------------------------
|
||||
|
||||
Responder makes it very easy to interact with cookies from a Request, or add some to a Response::
|
||||
|
||||
>>> resp.cookies["hello"] = "world"
|
||||
|
||||
>>> req.cookies
|
||||
{"hello": "world"}
|
||||
|
||||
|
||||
To set cookies directives, you should use `resp.set_cookie`::
|
||||
|
||||
>>> resp.set_cookie("hello", value="world", max_age=60)
|
||||
|
||||
Supported directives:
|
||||
|
||||
* ``key`` - **Required**
|
||||
* ``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>`_
|
||||
|
||||
|
||||
Using Cookie-Based Sessions
|
||||
---------------------------
|
||||
|
||||
Responder has built-in support for cookie-based sessions. To enable cookie-based sessions, simply add something to the ``resp.session`` dictionary::
|
||||
|
||||
>>> resp.session['username'] = 'kennethreitz'
|
||||
|
||||
A cookie called ``Responder-Session`` will be set, which contains all the data in ``resp.session``. It is signed, for verification purposes.
|
||||
|
||||
You can easily read a Request's session data, that can be trusted to have originated from the API::
|
||||
|
||||
>>> req.session
|
||||
{'username': 'kennethreitz'}
|
||||
|
||||
**Note**: if you are using this in production, you should pass the ``secret_key`` argument to ``API(...)``::
|
||||
|
||||
api = responder.API(secret_key=os.environ['SECRET_KEY'])
|
||||
|
||||
Using ``before_request``
|
||||
------------------------
|
||||
|
||||
If you'd like a view to be executed before every request, simply do the following::
|
||||
|
||||
@api.route(before_request=True)
|
||||
def prepare_response(req, resp):
|
||||
resp.headers["X-Pizza"] = "42"
|
||||
|
||||
Now all requests to your HTTP Service will include an ``X-Pizza`` header.
|
||||
|
||||
For ``websockets``::
|
||||
|
||||
@api.route(before_request=True, websocket=True)
|
||||
def prepare_response(ws):
|
||||
await ws.accept()
|
||||
|
||||
|
||||
WebSocket Support
|
||||
-----------------
|
||||
|
||||
Responder supports WebSockets::
|
||||
|
||||
@api.route('/ws', websocket=True)
|
||||
async def websocket(ws):
|
||||
await ws.accept()
|
||||
while True:
|
||||
name = await ws.receive_text()
|
||||
await ws.send_text(f"Hello {name}!")
|
||||
await ws.close()
|
||||
|
||||
Accepting the connection::
|
||||
|
||||
await websocket.accept()
|
||||
|
||||
Sending and receiving data::
|
||||
|
||||
await websocket.send_{format}(data)
|
||||
await websocket.receive_{format}(data)
|
||||
|
||||
Supported formats: ``text``, ``json``, ``bytes``.
|
||||
|
||||
Closing the connection::
|
||||
|
||||
await websocket.close()
|
||||
|
||||
Using Requests Test Client
|
||||
--------------------------
|
||||
|
||||
Responder comes with a first-class, well supported test client for your ASGI web services: **Requests**.
|
||||
|
||||
Here's an example of a test (written with pytest)::
|
||||
|
||||
import myapi
|
||||
|
||||
@pytest.fixture
|
||||
def api():
|
||||
return myapi.api
|
||||
|
||||
def test_response(api):
|
||||
hello = "hello, world!"
|
||||
|
||||
@api.route('/some-url')
|
||||
def some_view(req, resp):
|
||||
resp.text = hello
|
||||
|
||||
r = api.requests.get(url=api.url_for(some_view))
|
||||
assert r.text == hello
|
||||
|
||||
HSTS (Redirect to HTTPS)
|
||||
------------------------
|
||||
|
||||
Want HSTS (to redirect all traffic to HTTPS)?
|
||||
|
||||
::
|
||||
|
||||
api = responder.API(enable_hsts=True)
|
||||
|
||||
|
||||
Boom.
|
||||
|
||||
CORS
|
||||
----
|
||||
|
||||
Want `CORS <https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS/>`_ ?
|
||||
|
||||
::
|
||||
|
||||
api = responder.API(cors=True)
|
||||
|
||||
|
||||
The default parameters used by **Responder** are restrictive by default, so you'll need to explicitly enable particular origins, methods, or headers, in order for browsers to be permitted to use them in a Cross-Domain context.
|
||||
|
||||
In order to set custom parameters, you need to set the ``cors_params`` argument of ``api``, a dictionary containing the following entries:
|
||||
|
||||
* ``allow_origins`` - A list of origins that should be permitted to make cross-origin requests. eg. ``['https://example.org', 'https://www.example.org']``. You can use ``['*']`` to allow any origin.
|
||||
* ``allow_origin_regex`` - A regex string to match against origins that should be permitted to make cross-origin requests. eg. ``'https://.*\.example\.org'``.
|
||||
* ``allow_methods`` - A list of HTTP methods that should be allowed for cross-origin requests. Defaults to `['GET']`. You can use ``['*']`` to allow all standard methods.
|
||||
* ``allow_headers`` - A list of HTTP request headers that should be supported for cross-origin requests. Defaults to ``[]``. You can use ``['*']`` to allow all headers. The ``Accept``, ``Accept-Language``, ``Content-Language`` and ``Content-Type`` headers are always allowed for CORS requests.
|
||||
* ``allow_credentials`` - Indicate that cookies should be supported for cross-origin requests. Defaults to ``False``.
|
||||
* ``expose_headers`` - Indicate any response headers that should be made accessible to the browser. Defaults to ``[]``.
|
||||
* ``max_age`` - Sets a maximum time in seconds for browsers to cache CORS responses. Defaults to ``60``.
|
||||
|
||||
Trusted Hosts
|
||||
-------------
|
||||
|
||||
Make sure that all the incoming requests headers have a valid ``host``, that matches one of the provided patterns in the ``allowed_hosts`` attribute, in order to prevent HTTP Host Header attacks.
|
||||
|
||||
A 400 response will be raised, if a request does not match any of the provided patterns in the ``allowed_hosts`` attribute.
|
||||
|
||||
::
|
||||
|
||||
api = responder.API(allowed_hosts=['example.com', 'tenant.example.com'])
|
||||
|
||||
* ``allowed_hosts`` - A list of allowed hostnames.
|
||||
|
||||
Note:
|
||||
|
||||
* By default, all hostnames are allowed.
|
||||
* Wildcard domains such as ``*.example.com`` are supported.
|
||||
* To allow any hostname use ``allowed_hosts=["*"]``.
|
||||
@@ -0,0 +1,12 @@
|
||||
import responder
|
||||
|
||||
api = responder.API()
|
||||
|
||||
|
||||
@api.route("/{greeting}")
|
||||
async def greet_world(req, resp, *, greeting):
|
||||
resp.text = f"{greeting}, world!"
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
api.run()
|
||||
+121
@@ -0,0 +1,121 @@
|
||||
[build-system]
|
||||
build-backend = "setuptools.build_meta"
|
||||
requires = [
|
||||
"setuptools>=42", # At least v42 of setuptools required.
|
||||
"versioningit",
|
||||
]
|
||||
|
||||
[tool.ruff]
|
||||
line-length = 90
|
||||
|
||||
extend-exclude = [
|
||||
"bin/mkdeb.py",
|
||||
"docs/source/conf.py",
|
||||
"setup.py",
|
||||
]
|
||||
|
||||
lint.select = [
|
||||
# Builtins
|
||||
"A",
|
||||
# Bugbear
|
||||
"B",
|
||||
# comprehensions
|
||||
"C4",
|
||||
# Pycodestyle
|
||||
"E",
|
||||
# eradicate
|
||||
"ERA",
|
||||
# Pyflakes
|
||||
"F",
|
||||
# isort
|
||||
"I",
|
||||
# pandas-vet
|
||||
"PD",
|
||||
# return
|
||||
"RET",
|
||||
# Bandit
|
||||
"S",
|
||||
# print
|
||||
"T20",
|
||||
"W",
|
||||
# flake8-2020
|
||||
"YTT",
|
||||
]
|
||||
|
||||
lint.extend-ignore = [
|
||||
"S101", # Allow use of `assert`.
|
||||
]
|
||||
|
||||
lint.per-file-ignores."tests/*" = [
|
||||
"ERA001", # Found commented-out code.
|
||||
"S101", # Allow use of `assert`, and `print`.
|
||||
]
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
addopts = """
|
||||
-rfEXs -p pytester --strict-markers --verbosity=3
|
||||
--cov --cov-report=term-missing --cov-report=xml
|
||||
"""
|
||||
filterwarnings = [
|
||||
"error::UserWarning",
|
||||
]
|
||||
log_level = "DEBUG"
|
||||
log_cli_level = "DEBUG"
|
||||
log_format = "%(asctime)-15s [%(name)-36s] %(levelname)-8s: %(message)s"
|
||||
minversion = "2.0"
|
||||
testpaths = [
|
||||
"responder",
|
||||
"tests",
|
||||
]
|
||||
markers = [
|
||||
]
|
||||
xfail_strict = true
|
||||
|
||||
[tool.versioningit]
|
||||
|
||||
[tool.poe.tasks]
|
||||
|
||||
check = [
|
||||
"lint",
|
||||
"test",
|
||||
]
|
||||
|
||||
docs-autobuild = [
|
||||
{ cmd = "sphinx-autobuild --open-browser --watch docs/source docs/build" },
|
||||
]
|
||||
docs-html = [
|
||||
{ cmd = "sphinx-build -W --keep-going docs/source docs/build" },
|
||||
]
|
||||
docs-linkcheck = [
|
||||
{ cmd = "sphinx-build -W --keep-going -b linkcheck docs/source docs/build" },
|
||||
]
|
||||
|
||||
format = [
|
||||
{ cmd = "ruff format ." },
|
||||
# Configure Ruff not to auto-fix (remove!):
|
||||
# unused imports (F401), unused variables (F841), `print` statements (T201), and commented-out code (ERA001).
|
||||
{ cmd = "ruff check --fix --ignore=ERA --ignore=F401 --ignore=F841 --ignore=T20 --ignore=ERA001 ." },
|
||||
{ cmd = "pyproject-fmt --keep-full-version pyproject.toml" },
|
||||
]
|
||||
|
||||
lint = [
|
||||
{ cmd = "ruff format --check ." },
|
||||
{ cmd = "ruff check ." },
|
||||
{ cmd = "validate-pyproject pyproject.toml" },
|
||||
# { cmd = "mypy" },
|
||||
]
|
||||
|
||||
release = [
|
||||
{ cmd = "python -m build" },
|
||||
{ cmd = "twine upload --skip-existing dist/*" },
|
||||
]
|
||||
|
||||
[tool.poe.tasks.test]
|
||||
cmd = "pytest"
|
||||
help = "Invoke software tests"
|
||||
|
||||
[tool.poe.tasks.test.args.expression]
|
||||
options = [ "-k" ]
|
||||
|
||||
[tool.poe.tasks.test.args.marker]
|
||||
options = [ "-m" ]
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
build:
|
||||
image: latest
|
||||
image: latest
|
||||
|
||||
python:
|
||||
version: 3.6
|
||||
version: 3.6
|
||||
|
||||
+17
-1
@@ -1 +1,17 @@
|
||||
from .core import *
|
||||
from importlib.metadata import PackageNotFoundError, version
|
||||
|
||||
from . import ext
|
||||
from .core import API, Request, Response
|
||||
|
||||
try:
|
||||
__version__ = version("responder")
|
||||
except PackageNotFoundError: # pragma: no cover
|
||||
__version__ = "unknown"
|
||||
|
||||
__all__ = [
|
||||
"API",
|
||||
"Request",
|
||||
"Response",
|
||||
"ext",
|
||||
"__version__",
|
||||
]
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
from .cli import main
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1 +0,0 @@
|
||||
__version__ = "0.0.1"
|
||||
+258
-251
@@ -1,243 +1,277 @@
|
||||
import os
|
||||
import json
|
||||
from functools import partial
|
||||
from pathlib import Path
|
||||
|
||||
import waitress
|
||||
import uvicorn
|
||||
from starlette.exceptions import ExceptionMiddleware
|
||||
from starlette.middleware.cors import CORSMiddleware
|
||||
from starlette.middleware.errors import ServerErrorMiddleware
|
||||
from starlette.middleware.gzip import GZipMiddleware
|
||||
from starlette.middleware.httpsredirect import HTTPSRedirectMiddleware
|
||||
from starlette.middleware.sessions import SessionMiddleware
|
||||
from starlette.middleware.trustedhost import TrustedHostMiddleware
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
import jinja2
|
||||
from whitenoise import WhiteNoise
|
||||
from wsgiadapter import WSGIAdapter as RequestsWSGIAdapter
|
||||
from requests import Session as RequestsSession
|
||||
from werkzeug.wsgi import DispatcherMiddleware
|
||||
from graphql_server import encode_execution_results, json_encode, default_format_error
|
||||
|
||||
from . import models
|
||||
from .status_codes import HTTP_404
|
||||
from . import status_codes
|
||||
from .routes import Route
|
||||
from .background import BackgroundQueue
|
||||
from .ext.schema import OpenAPISchema as OpenAPISchema
|
||||
from .formats import get_formats
|
||||
from .routes import Router
|
||||
from .staticfiles import StaticFiles
|
||||
from .statics import DEFAULT_CORS_PARAMS, DEFAULT_OPENAPI_THEME, DEFAULT_SECRET_KEY
|
||||
from .templates import Templates
|
||||
|
||||
|
||||
# TODO: consider moving status codes here
|
||||
class API:
|
||||
"""The primary web-service class.
|
||||
|
||||
:param static_dir: The directory to use for static files. Will be created for you if it doesn't already exist.
|
||||
:param templates_dir: The directory to use for templates. Will be created for you if it doesn't already exist.
|
||||
:param enable_hsts: If ``True``, send all responses to HTTPS URLs.
|
||||
"""
|
||||
:param static_dir: The directory to use for static files. Will be created for you if it doesn't already exist.
|
||||
:param templates_dir: The directory to use for templates. Will be created for you if it doesn't already exist.
|
||||
:param auto_escape: If ``True``, HTML and XML templates will automatically be escaped.
|
||||
:param enable_hsts: If ``True``, send all responses to HTTPS URLs.
|
||||
:param openapi_theme: OpenAPI documentation theme, must be one of ``elements``, ``rapidoc``, ``redoc``, ``swagger_ui``
|
||||
""" # noqa: E501
|
||||
|
||||
status_codes = status_codes
|
||||
|
||||
def __init__(
|
||||
self, static_dir="static", templates_dir="templates", enable_hsts=False
|
||||
self,
|
||||
*,
|
||||
debug=False,
|
||||
title=None,
|
||||
version=None,
|
||||
description=None,
|
||||
terms_of_service=None,
|
||||
contact=None,
|
||||
license=None, # noqa: A002
|
||||
openapi=None,
|
||||
openapi_route="/schema.yml",
|
||||
static_dir="static",
|
||||
static_route="/static",
|
||||
templates_dir="templates",
|
||||
auto_escape=True,
|
||||
secret_key=DEFAULT_SECRET_KEY,
|
||||
enable_hsts=False,
|
||||
docs_route=None,
|
||||
cors=False,
|
||||
cors_params=DEFAULT_CORS_PARAMS,
|
||||
allowed_hosts=None,
|
||||
openapi_theme=DEFAULT_OPENAPI_THEME,
|
||||
):
|
||||
self.static_dir = Path(os.path.abspath(static_dir))
|
||||
self.templates_dir = Path(os.path.abspath(templates_dir))
|
||||
self.routes = {}
|
||||
self.background = BackgroundQueue()
|
||||
|
||||
self.secret_key = secret_key
|
||||
|
||||
self.router = Router()
|
||||
|
||||
if static_dir is not None:
|
||||
if static_route is None:
|
||||
static_route = static_dir
|
||||
static_dir = Path(os.path.abspath(static_dir))
|
||||
|
||||
self.static_dir = static_dir
|
||||
self.static_route = static_route
|
||||
|
||||
self.hsts_enabled = enable_hsts
|
||||
self.apps = {"/": self._wsgi_app}
|
||||
self.cors = cors
|
||||
self.cors_params = cors_params
|
||||
self.debug = debug
|
||||
|
||||
if not allowed_hosts:
|
||||
# if not debug:
|
||||
# raise RuntimeError(
|
||||
# "You need to specify `allowed_hosts` when debug is set to False"
|
||||
# ) # noqa: ERA001
|
||||
allowed_hosts = ["*"]
|
||||
self.allowed_hosts = allowed_hosts
|
||||
|
||||
if self.static_dir is not None:
|
||||
os.makedirs(self.static_dir, exist_ok=True)
|
||||
|
||||
if self.static_dir is not None:
|
||||
self.mount(self.static_route, self.static_app)
|
||||
|
||||
self.formats = get_formats()
|
||||
|
||||
# Make the static/templates directory if they don't exist.
|
||||
for _dir in (self.static_dir, self.templates_dir):
|
||||
os.makedirs(_dir, exist_ok=True)
|
||||
|
||||
# Mount the whitenoise application.
|
||||
self.whitenoise = WhiteNoise(self.__wsgi_app, root=str(self.static_dir))
|
||||
|
||||
# Cached requests session.
|
||||
self._session = None
|
||||
|
||||
def __wsgi_app(self, environ, start_response):
|
||||
# def wsgi_app(self, request):
|
||||
"""The actual WSGI application. This is not implemented in
|
||||
:meth:`__call__` so that middlewares can be applied without
|
||||
losing a reference to the app object. Instead of doing this::
|
||||
self.default_endpoint = None
|
||||
self.app = ExceptionMiddleware(self.router, debug=debug)
|
||||
self.add_middleware(GZipMiddleware)
|
||||
|
||||
app = MyMiddleware(app)
|
||||
if self.hsts_enabled:
|
||||
self.add_middleware(HTTPSRedirectMiddleware)
|
||||
|
||||
It's a better idea to do this instead::
|
||||
self.add_middleware(TrustedHostMiddleware, allowed_hosts=self.allowed_hosts)
|
||||
|
||||
app.wsgi_app = MyMiddleware(app.wsgi_app)
|
||||
if self.cors:
|
||||
self.add_middleware(CORSMiddleware, **self.cors_params)
|
||||
self.add_middleware(ServerErrorMiddleware, debug=debug)
|
||||
self.add_middleware(SessionMiddleware, secret_key=self.secret_key)
|
||||
|
||||
Then you still have the original application object around and
|
||||
can continue to call methods on it.
|
||||
if openapi or docs_route:
|
||||
self.openapi = OpenAPISchema(
|
||||
app=self,
|
||||
title=title,
|
||||
version=version,
|
||||
openapi=openapi,
|
||||
docs_route=docs_route,
|
||||
description=description,
|
||||
terms_of_service=terms_of_service,
|
||||
contact=contact,
|
||||
license=license,
|
||||
openapi_route=openapi_route,
|
||||
static_route=static_route,
|
||||
openapi_theme=openapi_theme,
|
||||
)
|
||||
|
||||
.. versionchanged:: 0.7
|
||||
Teardown events for the request and app contexts are called
|
||||
even if an unhandled error occurs. Other events may not be
|
||||
called depending on when an error occurs during dispatch.
|
||||
See :ref:`callbacks-and-errors`.
|
||||
# TODO: Update docs for templates
|
||||
self.templates = Templates(directory=templates_dir)
|
||||
self.requests = (
|
||||
self.session()
|
||||
) #: A Requests session that is connected to the ASGI app.
|
||||
|
||||
:param environ: A WSGI environment.
|
||||
:param start_response: A callable accepting a status code,
|
||||
a list of headers, and an optional exception context to
|
||||
start the response.
|
||||
@property
|
||||
def static_app(self):
|
||||
if not hasattr(self, "_static_app"):
|
||||
assert self.static_dir is not None
|
||||
self._static_app = StaticFiles(directory=self.static_dir)
|
||||
return self._static_app
|
||||
|
||||
def before_request(self, websocket=False):
|
||||
def decorator(f):
|
||||
self.router.before_request(f, websocket=websocket)
|
||||
return f
|
||||
|
||||
return decorator
|
||||
|
||||
def add_middleware(self, middleware_cls, **middleware_config):
|
||||
self.app = middleware_cls(self.app, **middleware_config)
|
||||
|
||||
def schema(self, name, **options):
|
||||
"""Decorator for creating new routes around function and class definitions.
|
||||
Usage::
|
||||
from marshmallow import Schema, fields
|
||||
@api.schema("Pet")
|
||||
class PetSchema(Schema):
|
||||
name = fields.Str()
|
||||
"""
|
||||
|
||||
req = models.Request(environ, start_response)
|
||||
# if not req.dispatched:
|
||||
resp = self._dispatch_request(req)
|
||||
return resp(environ, start_response)
|
||||
def decorator(f):
|
||||
self.openapi.add_schema(name=name, schema=f, **options)
|
||||
return f
|
||||
|
||||
def _wsgi_app(self, environ, start_response):
|
||||
return self.whitenoise(environ, start_response)
|
||||
|
||||
def wsgi_app(self, environ, start_response):
|
||||
"""Returns the WSGI app for this application (including all mounted WSGI apps)."""
|
||||
apps = self.apps.copy()
|
||||
main = apps.pop("/")
|
||||
|
||||
return DispatcherMiddleware(main, apps)(environ, start_response)
|
||||
|
||||
def __call__(self, environ, start_response=None):
|
||||
"""The WSGI server calls the Flask application object as the
|
||||
WSGI application. This calls :meth:`wsgi_app` which can be
|
||||
wrapped to applying middleware."""
|
||||
return self.wsgi_app(environ, start_response)
|
||||
return decorator
|
||||
|
||||
def path_matches_route(self, path):
|
||||
"""Given a path portion of a URL, tests that it matches against any registered route.
|
||||
|
||||
:param path: The path portion of a URL, to test all known routes against.
|
||||
"""
|
||||
for (route, route_object) in self.routes.items():
|
||||
if route_object.does_match(path):
|
||||
""" # noqa: E501 (Line too long)
|
||||
for route in self.router.routes:
|
||||
match, _ = route.matches(path)
|
||||
if match:
|
||||
return route
|
||||
return None
|
||||
|
||||
def _dispatch_request(self, req):
|
||||
# Set formats on Request object.
|
||||
req.formats = self.formats
|
||||
|
||||
route = self.path_matches_route(req.path)
|
||||
resp = models.Response(req=req, formats=self.formats)
|
||||
|
||||
if self.hsts_enabled:
|
||||
if req.url.startswith("http://"):
|
||||
url = req.url.replace("http://", "https://", 1)
|
||||
self.redirect(resp, location=url)
|
||||
|
||||
if route:
|
||||
try:
|
||||
params = self.routes[route].incoming_matches(req.path)
|
||||
self.routes[route].endpoint(req, resp, **params)
|
||||
# The request is using class-based views.
|
||||
except TypeError:
|
||||
try:
|
||||
view = self.routes[route].endpoint(**params)
|
||||
except TypeError:
|
||||
view = self.routes[route].endpoint
|
||||
try:
|
||||
# GraphQL Schema.
|
||||
assert hasattr(view, "execute")
|
||||
self.graphql_response(req, resp, schema=view)
|
||||
except AssertionError:
|
||||
# WSGI App.
|
||||
try:
|
||||
req.dispatched = True
|
||||
return view(
|
||||
environ=req._environ, start_response=req._start_response
|
||||
)
|
||||
except TypeError:
|
||||
pass
|
||||
|
||||
# Run on_request first.
|
||||
try:
|
||||
getattr(view, "on_request")(req, resp)
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
# Then on_get.
|
||||
method = req.method
|
||||
|
||||
try:
|
||||
getattr(view, f"on_{method}")(req, resp)
|
||||
except AttributeError:
|
||||
pass
|
||||
else:
|
||||
self.default_response(req, resp)
|
||||
|
||||
return resp
|
||||
|
||||
def add_route(self, route, endpoint, *, check_existing=True):
|
||||
# TODO: add graphiql
|
||||
"""Add a route to the API.
|
||||
def add_route(
|
||||
self,
|
||||
route=None,
|
||||
endpoint=None,
|
||||
*,
|
||||
default=False,
|
||||
static=True,
|
||||
check_existing=True,
|
||||
websocket=False,
|
||||
before_request=False,
|
||||
):
|
||||
"""Adds a route to the API.
|
||||
|
||||
:param route: A string representation of the route.
|
||||
:param endpoint: The endpoint for the route -- can be a callable, a class, a WSGI application, or graphene schema (GraphQL).
|
||||
:param check_existing: If ``True``, an AssertionError will be raised, if the route is already defined.
|
||||
"""
|
||||
if check_existing:
|
||||
assert route not in self.routes
|
||||
:param endpoint: The endpoint for the route -- can be a callable, or a class.
|
||||
:param default: If ``True``, all unknown requests will route to this view.
|
||||
:param static: If ``True``, and no endpoint was passed, render "static/index.html".
|
||||
Also, it will become a default route.
|
||||
""" # noqa: E501
|
||||
|
||||
# TODO: Support grpahiql.
|
||||
self.routes[route] = Route(route, endpoint)
|
||||
# Path
|
||||
if static:
|
||||
assert self.static_dir is not None
|
||||
if not endpoint:
|
||||
endpoint = self._static_response
|
||||
default = True
|
||||
|
||||
def default_response(self, req, resp):
|
||||
resp.status_code = HTTP_404
|
||||
resp.text = "Not found."
|
||||
self.router.add_route(
|
||||
route,
|
||||
endpoint,
|
||||
default=default,
|
||||
websocket=websocket,
|
||||
before_request=before_request,
|
||||
check_existing=check_existing,
|
||||
)
|
||||
|
||||
async def _static_response(self, req, resp):
|
||||
assert self.static_dir is not None
|
||||
|
||||
index = (self.static_dir / "index.html").resolve()
|
||||
if os.path.exists(index):
|
||||
with open(index, "r") as f:
|
||||
resp.html = f.read()
|
||||
else:
|
||||
resp.status_code = status_codes.HTTP_404
|
||||
resp.text = "Not found."
|
||||
|
||||
def redirect(
|
||||
self, resp, location, *, set_text=True, status_code=status_codes.HTTP_301
|
||||
):
|
||||
"""Redirects a given response to a given location.
|
||||
|
||||
:param resp: The Response to mutate.
|
||||
:param location: The location of the redirect.
|
||||
:param set_text: If ``True``, sets the Redirect body content automatically.
|
||||
:param status_code: an `API.status_codes` attribute, or an integer, representing the HTTP status code of the redirect.
|
||||
:param status_code: an `API.status_codes` attribute, or an integer,
|
||||
representing the HTTP status code of the redirect.
|
||||
"""
|
||||
resp.redirect(location, set_text=set_text, status_code=status_code)
|
||||
|
||||
def on_event(self, event_type: str, **args):
|
||||
"""Decorator for registering functions or coroutines to run at certain events
|
||||
Supported events: startup, shutdown
|
||||
|
||||
Usage::
|
||||
|
||||
@api.on_event('startup')
|
||||
async def open_database_connection_pool():
|
||||
...
|
||||
|
||||
@api.on_event('shutdown')
|
||||
async def close_database_connection_pool():
|
||||
...
|
||||
|
||||
"""
|
||||
|
||||
assert resp.status_code.is_300(status_code)
|
||||
def decorator(func):
|
||||
self.add_event_handler(event_type, func, **args)
|
||||
return func
|
||||
|
||||
resp.status_code = status_code
|
||||
if set_text:
|
||||
resp.text = f"Redirecting to: {location}"
|
||||
resp.headers.update({"Location": location})
|
||||
return decorator
|
||||
|
||||
@staticmethod
|
||||
def _resolve_graphql_query(req):
|
||||
if "json" in req.mimetype:
|
||||
return req.json()["query"]
|
||||
def add_event_handler(self, event_type, handler):
|
||||
"""Adds an event handler to the API.
|
||||
|
||||
# Support query/q in form data.
|
||||
if "query" in req.media("form"):
|
||||
return req.media("form")["query"]
|
||||
if "q" in req.media("form"):
|
||||
return req.media("form")["q"]
|
||||
:param event_type: A string in ("startup", "shutdown")
|
||||
:param handler: The function to run. Can be either a function or a coroutine.
|
||||
"""
|
||||
|
||||
# Support query/q in params.
|
||||
if "query" in req.params:
|
||||
return req.params["query"]
|
||||
if "q" in req.params:
|
||||
return req.params["q"]
|
||||
self.router.add_event_handler(event_type, handler)
|
||||
|
||||
# Otherwise, the request text is used (typical).
|
||||
# TODO: Make some assertions about content-type here.
|
||||
return req.text
|
||||
|
||||
def graphql_response(self, req, resp, schema):
|
||||
query = self._resolve_graphql_query(req)
|
||||
result = schema.execute(query)
|
||||
result, status_code = encode_execution_results(
|
||||
[result],
|
||||
is_batch=False,
|
||||
format_error=default_format_error,
|
||||
encode=partial(json_encode, pretty=False),
|
||||
)
|
||||
resp.media = json.loads(result)
|
||||
return (query, result, status_code)
|
||||
|
||||
def route(self, route, **options):
|
||||
"""Decorator for creating new routes around function and class defenitions.
|
||||
def route(self, route=None, **options):
|
||||
"""Decorator for creating new routes around function and class definitions.
|
||||
|
||||
Usage::
|
||||
|
||||
@api.route("/hello")
|
||||
def hello(req, resp):
|
||||
req.text = "hello, world!"
|
||||
resp.text = "hello, world!"
|
||||
|
||||
"""
|
||||
|
||||
@@ -247,112 +281,85 @@ class API:
|
||||
|
||||
return decorator
|
||||
|
||||
def mount(self, route, wsgi_app):
|
||||
"""Mounts a WSGI application at a given route.
|
||||
def mount(self, route, app):
|
||||
"""Mounts an WSGI / ASGI application at a given route.
|
||||
|
||||
:param route: String representation of the route to be used (shouldn't be parameterized).
|
||||
:param wsgi_app: The other WSGI app (e.g. a Flask app).
|
||||
:param route: String representation of the route to be used
|
||||
(shouldn't be parameterized).
|
||||
:param app: The other WSGI / ASGI app.
|
||||
"""
|
||||
self.apps.update({route: wsgi_app})
|
||||
self.router.apps.update({route: app})
|
||||
|
||||
def session(self, base_url="http://;"):
|
||||
"""Testing HTTP client. Returns a Requests session object, able to send HTTP requests to the WSGI application.
|
||||
"""Testing HTTP client. Returns a Requests session object,
|
||||
able to send HTTP requests to the Responder application.
|
||||
|
||||
:param base_url: The URL to mount the connection adaptor to.
|
||||
"""
|
||||
|
||||
if self._session is None:
|
||||
session = RequestsSession()
|
||||
session.mount(base_url, RequestsWSGIAdapter(self))
|
||||
self._session = session
|
||||
self._session = TestClient(self, base_url=base_url)
|
||||
return self._session
|
||||
|
||||
def url_for(self, endpoint, absolute_url=False, **params):
|
||||
def url_for(self, endpoint, **params):
|
||||
# TODO: Absolute_url
|
||||
"""Given an endpoint, returns a rendered URL for its route.
|
||||
|
||||
:param view: The route endpoint you're searching for.
|
||||
:param endpoint: The route endpoint you're searching for.
|
||||
:param params: Data to pass into the URL generator (for parameterized URLs).
|
||||
"""
|
||||
for (route, route_object) in self.routes.items():
|
||||
if route_object.endpoint == endpoint:
|
||||
return route_object.url(**params)
|
||||
raise ValueError
|
||||
return self.router.url_for(endpoint, **params)
|
||||
|
||||
def template(self, name, auto_escape=True, **values):
|
||||
def template(self, filename, *args, **kwargs):
|
||||
"""Renders the given `jinja2 <http://jinja.pocoo.org/docs/>`_ template, with provided values supplied.
|
||||
|
||||
Note: The current ``api`` instance is always passed into the view.
|
||||
Note: The current ``api`` instance is by default passed into the view. This is set in the dict ``api.jinja_values_base``.
|
||||
|
||||
:param name: The filename of the jinja2 template, in ``templates_dir``.
|
||||
:param auto_escape: If ``True``, HTML and XML will automatically be escaped.
|
||||
:param values: Data to pass into the template.
|
||||
"""
|
||||
# Give reference to self.
|
||||
values.update(api=self)
|
||||
:param filename: The filename of the jinja2 template, in ``templates_dir``.
|
||||
:param *args: Data to pass into the template.
|
||||
:param *kwargs: Date to pass into the template.
|
||||
""" # noqa: E501
|
||||
return self.templates.render(filename, *args, **kwargs)
|
||||
|
||||
if auto_escape:
|
||||
env = jinja2.Environment(
|
||||
loader=jinja2.FileSystemLoader(
|
||||
str(self.templates_dir), followlinks=True
|
||||
),
|
||||
autoescape=jinja2.select_autoescape(["html", "xml"]),
|
||||
)
|
||||
else:
|
||||
env = jinja2.Environment(
|
||||
loader=jinja2.FileSystemLoader(
|
||||
str(self.templates_dir), followlinks=True
|
||||
),
|
||||
autoescape=jinja2.select_autoescape([]),
|
||||
)
|
||||
|
||||
template = env.get_template(name)
|
||||
return template.render(**values)
|
||||
|
||||
def template_string(self, s, auto_escape=True, **values):
|
||||
def template_string(self, source, *args, **kwargs):
|
||||
"""Renders the given `jinja2 <http://jinja.pocoo.org/docs/>`_ template string, with provided values supplied.
|
||||
Note: The current ``api`` instance is by default passed into the view. This is set in the dict ``api.jinja_values_base``.
|
||||
:param source: The template to use.
|
||||
:param *args: Data to pass into the template.
|
||||
:param **kwargs: Data to pass into the template.
|
||||
""" # noqa: E501
|
||||
return self.templates.render_string(source, *args, **kwargs)
|
||||
|
||||
Note: The current ``api`` instance is always passed into the view.
|
||||
|
||||
:param s: The template to use.
|
||||
:param auto_escape: If ``True``, HTML and XML will automatically be escaped.
|
||||
:param values: Data to pass into the template.
|
||||
"""
|
||||
# Give reference to self.
|
||||
values.update(api=self)
|
||||
|
||||
if auto_escape:
|
||||
env = jinja2.Environment(
|
||||
loader=jinja2.BaseLoader,
|
||||
autoescape=jinja2.select_autoescape(["html", "xml"]),
|
||||
)
|
||||
else:
|
||||
env = jinja2.Environment(
|
||||
loader=jinja2.BaseLoader, autoescape=jinja2.select_autoescape([])
|
||||
)
|
||||
|
||||
template = env.from_string(s)
|
||||
return template.render(**values)
|
||||
|
||||
def run(self, address=None, port=None, **kwargs):
|
||||
"""Runs the application with Waitress. If the ``PORT`` environment
|
||||
def serve(self, *, address=None, port=None, debug=False, **options):
|
||||
"""Runs the application with uvicorn. If the ``PORT`` environment
|
||||
variable is set, requests will be served on that port automatically to all
|
||||
known hosts.
|
||||
|
||||
:param address: The address to bind to.
|
||||
:param port: The port to bind to. If none is provided, one will be selected at random.
|
||||
:param kwargs: Additional keyword arguments to send to ``waitress.serve()``.
|
||||
"""
|
||||
:param debug: Whether to run application in debug mode.
|
||||
:param options: Additional keyword arguments to send to ``uvicorn.run()``.
|
||||
""" # noqa: E501
|
||||
|
||||
if "PORT" in os.environ:
|
||||
if address is None:
|
||||
address = "0.0.0.0"
|
||||
port = os.environ["PORT"]
|
||||
address = "0.0.0.0" # noqa: S104
|
||||
port = int(os.environ["PORT"])
|
||||
|
||||
if address is None:
|
||||
address = "127.0.0.1"
|
||||
if port is None:
|
||||
port = 0
|
||||
port = 5042
|
||||
|
||||
bind_to = f"{address}:{port}"
|
||||
def spawn():
|
||||
uvicorn.run(self, host=address, port=port, debug=debug, **options)
|
||||
|
||||
waitress.serve(app=self, listen=bind_to, **kwargs)
|
||||
spawn()
|
||||
|
||||
def run(self, **kwargs):
|
||||
if "debug" not in kwargs:
|
||||
kwargs.update({"debug": self.debug})
|
||||
self.serve(**kwargs)
|
||||
|
||||
async def __call__(self, scope, receive, send):
|
||||
await self.app(scope, receive, send)
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import asyncio
|
||||
import concurrent.futures
|
||||
import multiprocessing
|
||||
import traceback
|
||||
|
||||
from starlette.concurrency import run_in_threadpool
|
||||
|
||||
|
||||
class BackgroundQueue:
|
||||
def __init__(self, n=None):
|
||||
if n is None:
|
||||
n = multiprocessing.cpu_count()
|
||||
|
||||
self.n = n
|
||||
self.pool = concurrent.futures.ThreadPoolExecutor(max_workers=n)
|
||||
self.results = []
|
||||
|
||||
def run(self, f, *args, **kwargs):
|
||||
self.pool._max_workers = self.n
|
||||
self.pool._adjust_thread_count()
|
||||
|
||||
f = self.pool.submit(f, *args, **kwargs)
|
||||
self.results.append(f)
|
||||
return f
|
||||
|
||||
def task(self, f):
|
||||
def on_future_done(fs):
|
||||
try:
|
||||
fs.result()
|
||||
except Exception:
|
||||
traceback.print_exc()
|
||||
|
||||
def do_task(*args, **kwargs):
|
||||
result = self.run(f, *args, **kwargs)
|
||||
result.add_done_callback(on_future_done)
|
||||
return result
|
||||
|
||||
return do_task
|
||||
|
||||
async def __call__(self, func, *args, **kwargs) -> None:
|
||||
if asyncio.iscoroutinefunction(func):
|
||||
return await asyncio.ensure_future(func(*args, **kwargs))
|
||||
return await run_in_threadpool(func, *args, **kwargs)
|
||||
@@ -1 +0,0 @@
|
||||
import docopt
|
||||
@@ -1,2 +1,8 @@
|
||||
from .api import API
|
||||
from .models import Request, Response
|
||||
|
||||
__all__ = [
|
||||
"API",
|
||||
"Request",
|
||||
"Response",
|
||||
]
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
from pathlib import Path
|
||||
|
||||
from apispec import APISpec, yaml_utils
|
||||
from apispec.ext.marshmallow import MarshmallowPlugin
|
||||
|
||||
from responder import status_codes
|
||||
from responder.statics import API_THEMES, DEFAULT_OPENAPI_THEME
|
||||
from responder.templates import Templates
|
||||
|
||||
|
||||
class OpenAPISchema:
|
||||
def __init__(
|
||||
self,
|
||||
app,
|
||||
title,
|
||||
version,
|
||||
plugins=None,
|
||||
description=None,
|
||||
terms_of_service=None,
|
||||
contact=None,
|
||||
license=None, # noqa: A002
|
||||
openapi=None,
|
||||
openapi_route="/schema.yml",
|
||||
docs_route="/docs/",
|
||||
static_route="/static",
|
||||
openapi_theme=DEFAULT_OPENAPI_THEME,
|
||||
):
|
||||
self.app = app
|
||||
self.schemas = {}
|
||||
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.openapi_route = openapi_route
|
||||
|
||||
self.docs_theme = (
|
||||
openapi_theme if openapi_theme in API_THEMES else DEFAULT_OPENAPI_THEME
|
||||
)
|
||||
self.docs_route = docs_route
|
||||
|
||||
self.plugins = [MarshmallowPlugin()] if plugins is None else plugins
|
||||
|
||||
if self.openapi_version is not None:
|
||||
self.app.add_route(self.openapi_route, self.schema_response)
|
||||
|
||||
if self.docs_route is not None:
|
||||
self.app.add_route(self.docs_route, self.docs_response)
|
||||
|
||||
theme_path = (Path(__file__).parent / "docs").resolve()
|
||||
self.templates = Templates(directory=theme_path)
|
||||
|
||||
self.static_route = static_route
|
||||
|
||||
@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=self.plugins,
|
||||
info=info,
|
||||
)
|
||||
|
||||
for route in self.app.router.routes:
|
||||
if route.description:
|
||||
operations = yaml_utils.load_operations_from_docstring(route.description)
|
||||
spec.path(path=route.route, operations=operations)
|
||||
|
||||
for name, schema in self.schemas.items():
|
||||
spec.components.schema(name, schema=schema)
|
||||
|
||||
return spec
|
||||
|
||||
@property
|
||||
def openapi(self):
|
||||
return self._apispec.to_yaml()
|
||||
|
||||
def add_schema(self, name, schema, check_existing=True):
|
||||
"""Adds a marshmallow schema to the API specification."""
|
||||
if check_existing:
|
||||
assert name not in self.schemas
|
||||
|
||||
self.schemas[name] = schema
|
||||
|
||||
def schema(self, name, **options):
|
||||
"""Decorator for creating new routes around function and class definitions.
|
||||
|
||||
Usage::
|
||||
|
||||
from marshmallow import Schema, fields
|
||||
|
||||
@api.schema("Pet")
|
||||
class PetSchema(Schema):
|
||||
name = fields.Str()
|
||||
|
||||
"""
|
||||
|
||||
def decorator(f):
|
||||
self.add_schema(name=name, schema=f, **options)
|
||||
return f
|
||||
|
||||
return decorator
|
||||
|
||||
@property
|
||||
def docs(self):
|
||||
return self.templates.render(
|
||||
f"{self.docs_theme}.html",
|
||||
title=self.title,
|
||||
version=self.version,
|
||||
schema_url="/schema.yml",
|
||||
)
|
||||
|
||||
def static_url(self, asset):
|
||||
"""Given a static asset, return its URL path."""
|
||||
assert self.static_route is not None
|
||||
return f"{self.static_route}/{str(asset)}"
|
||||
|
||||
def docs_response(self, req, resp):
|
||||
resp.html = self.docs
|
||||
|
||||
def schema_response(self, req, resp):
|
||||
resp.status_code = status_codes.HTTP_200
|
||||
resp.headers["Content-Type"] = "application/x-yaml"
|
||||
resp.content = self.openapi
|
||||
@@ -0,0 +1,24 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1, shrink-to-fit=no"
|
||||
/>
|
||||
<title>{{ title }} {{ version }}</title>
|
||||
<!-- Embed elements Elements via Web Component -->
|
||||
<script src="https://unpkg.com/@stoplight/elements/web-components.min.js"></script>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://unpkg.com/@stoplight/elements/styles.min.css"
|
||||
/>
|
||||
</head>
|
||||
<body>
|
||||
<elements-api
|
||||
apiDescriptionUrl="{{ schema_url }}"
|
||||
router="hash"
|
||||
layout="sidebar"
|
||||
/>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,16 @@
|
||||
<!DOCTYPE html>
|
||||
<!-- Important: must specify -->
|
||||
<html>
|
||||
<head>
|
||||
<title>{{ title }} {{ version }}</title>
|
||||
<meta charset="utf-8" />
|
||||
<!-- Important: rapi-doc uses utf8 characters -->
|
||||
<script
|
||||
type="module"
|
||||
src="https://unpkg.com/rapidoc/dist/rapidoc-min.js"
|
||||
></script>
|
||||
</head>
|
||||
<body>
|
||||
<rapi-doc spec-url="{{ schema_url }}" show-header="false"> </rapi-doc>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,23 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>{{ title }} {{ version }}</title>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css?family=Montserrat:300,400,700|Roboto:300,400,700"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<redoc spec-url="{{ schema_url }}"></redoc>
|
||||
<script src="https://cdn.jsdelivr.net/npm/redoc@next/bundles/redoc.standalone.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,49 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>{{ title }} {{ version }}</title>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
type="text/css"
|
||||
href="https://unpkg.com/swagger-ui-dist/swagger-ui.css"
|
||||
/>
|
||||
<style>
|
||||
html {
|
||||
box-sizing: border-box;
|
||||
overflow: -moz-scrollbars-vertical;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
*,
|
||||
*:before,
|
||||
*:after {
|
||||
box-sizing: inherit;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
background: #fafafa;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="swagger-ui"></div>
|
||||
|
||||
<script src="https://unpkg.com/swagger-ui-dist/swagger-ui-bundle.js"></script>
|
||||
<script src="https://unpkg.com/swagger-ui-dist/swagger-ui-standalone-preset.js"></script>
|
||||
<script>
|
||||
window.onload = function () {
|
||||
const ui = SwaggerUIBundle({
|
||||
url: "{{ schema_url }}",
|
||||
dom_id: "#swagger-ui",
|
||||
deepLinking: true,
|
||||
presets: [SwaggerUIBundle.presets.apis, SwaggerUIStandalonePreset],
|
||||
plugins: [SwaggerUIBundle.plugins.DownloadUrl],
|
||||
layout: "BaseLayout",
|
||||
});
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
+71
-12
@@ -1,27 +1,86 @@
|
||||
import yaml
|
||||
import json
|
||||
from urllib.parse import urlencode
|
||||
|
||||
import yaml
|
||||
from requests_toolbelt.multipart import decoder
|
||||
|
||||
from .models import QueryDict
|
||||
|
||||
|
||||
def format_form(r, encode=False):
|
||||
if not encode:
|
||||
return r._wz.form
|
||||
async def format_form(r, encode=False):
|
||||
if encode:
|
||||
return None
|
||||
if "multipart/form-data" in r.headers.get("Content-Type"):
|
||||
decode = decoder.MultipartDecoder(await r.content, r.mimetype)
|
||||
queries = []
|
||||
for part in decode.parts:
|
||||
header = part.headers.get(b"Content-Disposition").decode("utf-8")
|
||||
text = part.text
|
||||
|
||||
for section in [h.strip() for h in header.split(";")]:
|
||||
split = section.split("=")
|
||||
if len(split) > 1:
|
||||
key = split[1]
|
||||
key = key[1:-1]
|
||||
queries.append((key, text))
|
||||
|
||||
content = urlencode(queries)
|
||||
return QueryDict(content)
|
||||
return QueryDict(await r.text)
|
||||
|
||||
|
||||
def format_yaml(r, encode=False):
|
||||
async def format_yaml(r, encode=False):
|
||||
if encode:
|
||||
r.headers.update({"Content-Type": "application/x-yaml"})
|
||||
return yaml.dump(r.media)
|
||||
else:
|
||||
return yaml.load(r.content)
|
||||
return yaml.safe_dump(r.media)
|
||||
return yaml.safe_load(await r.content)
|
||||
|
||||
|
||||
def format_json(r, encode=False):
|
||||
async def format_json(r, encode=False):
|
||||
if encode:
|
||||
r.headers.update({"Content-Type": "application/json"})
|
||||
return json.dumps(r.media)
|
||||
else:
|
||||
return json.loads(r.content)
|
||||
return json.loads(await r.content)
|
||||
|
||||
|
||||
async def format_files(r, encode=False):
|
||||
if encode:
|
||||
return None
|
||||
decoded = decoder.MultipartDecoder(await r.content, r.mimetype)
|
||||
dump = {}
|
||||
for part in decoded.parts:
|
||||
header = part.headers[b"Content-Disposition"].decode("utf-8")
|
||||
mimetype = part.headers.get(b"Content-Type", None)
|
||||
filename = None
|
||||
|
||||
for section in [h.strip() for h in header.split(";")]:
|
||||
split = section.split("=")
|
||||
if len(split) > 1:
|
||||
key = split[0]
|
||||
value = split[1]
|
||||
|
||||
value = value[1:-1]
|
||||
|
||||
if key == "filename":
|
||||
filename = value
|
||||
elif key == "name":
|
||||
formname = value
|
||||
|
||||
if mimetype is None:
|
||||
dump[formname] = part.content
|
||||
else:
|
||||
dump[formname] = {
|
||||
"filename": filename,
|
||||
"content": part.content,
|
||||
"content-type": mimetype.decode("utf-8"),
|
||||
}
|
||||
return dump
|
||||
|
||||
|
||||
def get_formats():
|
||||
return {"json": format_json, "yaml": format_yaml, "form": format_form}
|
||||
return {
|
||||
"json": format_json,
|
||||
"yaml": format_yaml,
|
||||
"form": format_form,
|
||||
"files": format_files,
|
||||
}
|
||||
|
||||
+336
-124
@@ -1,168 +1,380 @@
|
||||
import io
|
||||
import json
|
||||
import gzip
|
||||
|
||||
import graphene
|
||||
import yaml
|
||||
from requests.models import Request as RequestsRequest
|
||||
from requests.structures import CaseInsensitiveDict
|
||||
from werkzeug.wrappers import Request as WerkzeugRequest
|
||||
from werkzeug.wrappers import BaseResponse as WerkzeugResponse
|
||||
|
||||
|
||||
import functools
|
||||
import inspect
|
||||
import typing as t
|
||||
from http.cookies import SimpleCookie
|
||||
from urllib.parse import parse_qs
|
||||
|
||||
from .status_codes import HTTP_200
|
||||
import chardet
|
||||
import rfc3986
|
||||
from requests.cookies import RequestsCookieJar
|
||||
from requests.structures import CaseInsensitiveDict
|
||||
from starlette.requests import Request as StarletteRequest
|
||||
from starlette.requests import State
|
||||
from starlette.responses import (
|
||||
Response as StarletteResponse,
|
||||
)
|
||||
from starlette.responses import (
|
||||
StreamingResponse as StarletteStreamingResponse,
|
||||
)
|
||||
|
||||
# @staticmethod
|
||||
# def funcname(parameter_list):
|
||||
# pass
|
||||
from .statics import DEFAULT_ENCODING
|
||||
from .status_codes import HTTP_301
|
||||
|
||||
|
||||
def flatten(d):
|
||||
for key, value in d.copy().items():
|
||||
if len(value) == 1:
|
||||
d[key] = value[0]
|
||||
class QueryDict(dict):
|
||||
def __init__(self, query_string):
|
||||
self.update(parse_qs(query_string))
|
||||
|
||||
return d
|
||||
def __getitem__(self, key):
|
||||
"""
|
||||
Return the last data value for this key, or [] if it's an empty list;
|
||||
raise KeyError if not found.
|
||||
"""
|
||||
list_ = super().__getitem__(key)
|
||||
try:
|
||||
return list_[-1]
|
||||
except IndexError:
|
||||
return []
|
||||
|
||||
def get(self, key, default=None):
|
||||
"""
|
||||
Return the last data value for the passed key. If key doesn't exist
|
||||
or value is an empty list, return `default`.
|
||||
"""
|
||||
try:
|
||||
val = self[key]
|
||||
except KeyError:
|
||||
return default
|
||||
if val == []:
|
||||
return default
|
||||
return val
|
||||
|
||||
def _get_list(self, key, default=None, force_list=False):
|
||||
"""
|
||||
Return a list of values for the key.
|
||||
|
||||
Used internally to manipulate values list. If force_list is True,
|
||||
return a new copy of values.
|
||||
"""
|
||||
try:
|
||||
values = super().__getitem__(key)
|
||||
except KeyError:
|
||||
if default is None:
|
||||
return []
|
||||
return default
|
||||
else:
|
||||
if force_list:
|
||||
values = list(values) if values is not None else None
|
||||
return values
|
||||
|
||||
def get_list(self, key, default=None):
|
||||
"""
|
||||
Return the list of values for the key. If key doesn't exist, return a
|
||||
default value.
|
||||
"""
|
||||
return self._get_list(key, default, force_list=True)
|
||||
|
||||
def items(self):
|
||||
"""
|
||||
Yield (key, value) pairs, where value is the last item in the list
|
||||
associated with the key.
|
||||
"""
|
||||
for key in self:
|
||||
yield key, self[key]
|
||||
|
||||
def items_list(self):
|
||||
"""
|
||||
Yield (key, value) pairs, where value is the the list.
|
||||
"""
|
||||
yield from super().items()
|
||||
|
||||
|
||||
# TODO: add slots
|
||||
class Request:
|
||||
def __init__(self, environ, start_response=None):
|
||||
self._wz = WerkzeugRequest(environ)
|
||||
self.start_response = start_response
|
||||
self.headers = CaseInsensitiveDict(
|
||||
self._wz.headers.to_wsgi_list()
|
||||
) #: A case-insensitive dictionary, containg all headers sent in the Request.
|
||||
self.method = (
|
||||
self._wz.method.lower()
|
||||
) #: The incoming HTTP method used for the request, lower-cased.
|
||||
self.full_url = (
|
||||
self._wz.url
|
||||
) #: The full URL of the Request, query parameters and all.
|
||||
self.url = (
|
||||
self._wz.base_url
|
||||
) #: The URL of the Request, without query parameters.
|
||||
self.full_path = (
|
||||
self._wz.full_path
|
||||
) #: The full path portion of the URL of the Request, query parameters and all.
|
||||
self.path = (
|
||||
self._wz.path
|
||||
) #: The path portion of the URL of the Request, without query parameters.
|
||||
self.params = flatten(
|
||||
parse_qs(self._wz.query_string.decode("utf-8"))
|
||||
) #: A dictionary of the parsed query paramaters used for the Request.
|
||||
self.query = self._wz.query_string.decode(
|
||||
"utf-8"
|
||||
) #: A string containing only the query paramaters of the Request.
|
||||
self.raw = self._wz.stream #: A raw file-like stream of the incoming Request.
|
||||
self.content = self._wz.get_data(
|
||||
cache=True, as_text=False
|
||||
) #: The Request body, as bytes.
|
||||
self.mimetype = self._wz.mimetype #: The mimetype of the incoming Request.
|
||||
# TODO: rip that out
|
||||
self.text = self._wz.get_data(
|
||||
cache=False, as_text=True
|
||||
) #: The Request body, as unicode.
|
||||
self.formats = None
|
||||
__slots__ = [
|
||||
"_starlette",
|
||||
"formats",
|
||||
"_headers",
|
||||
"_encoding",
|
||||
"api",
|
||||
"_content",
|
||||
"_cookies",
|
||||
]
|
||||
|
||||
def __init__(self, scope, receive, api=None, formats=None):
|
||||
self._starlette = StarletteRequest(scope, receive)
|
||||
self.formats = formats
|
||||
self._encoding = None
|
||||
self.api = api
|
||||
self._content = None
|
||||
|
||||
headers = CaseInsensitiveDict()
|
||||
for key, value in self._starlette.headers.items():
|
||||
headers[key] = value
|
||||
|
||||
self._headers = headers
|
||||
self._cookies = None
|
||||
|
||||
@property
|
||||
def session(self):
|
||||
"""The session data, in dict form, from the Request."""
|
||||
return self._starlette.session
|
||||
|
||||
@property
|
||||
def headers(self):
|
||||
"""A case-insensitive dictionary, containing all headers sent in the Request."""
|
||||
return self._headers
|
||||
|
||||
@property
|
||||
def mimetype(self):
|
||||
return self.headers.get("Content-Type", "")
|
||||
|
||||
@property
|
||||
def method(self):
|
||||
"""The incoming HTTP method used for the request, lower-cased."""
|
||||
return self._starlette.method.lower()
|
||||
|
||||
@property
|
||||
def full_url(self):
|
||||
"""The full URL of the Request, query parameters and all."""
|
||||
return str(self._starlette.url)
|
||||
|
||||
@property
|
||||
def url(self):
|
||||
"""The parsed URL of the Request."""
|
||||
return rfc3986.urlparse(self.full_url)
|
||||
|
||||
@property
|
||||
def cookies(self):
|
||||
"""The cookies sent in the Request, as a dictionary."""
|
||||
if self._cookies is None:
|
||||
cookies = RequestsCookieJar()
|
||||
cookie_header = self.headers.get("Cookie", "")
|
||||
|
||||
bc = SimpleCookie(cookie_header)
|
||||
for key, morsel in bc.items():
|
||||
cookies[key] = morsel.value
|
||||
|
||||
self._cookies = cookies.get_dict()
|
||||
|
||||
return self._cookies
|
||||
|
||||
@property
|
||||
def params(self):
|
||||
"""A dictionary of the parsed query parameters used for the Request."""
|
||||
try:
|
||||
return QueryDict(self.url.query)
|
||||
except AttributeError:
|
||||
return QueryDict({})
|
||||
|
||||
@property
|
||||
def state(self) -> State:
|
||||
"""
|
||||
Use the state to store additional information.
|
||||
|
||||
This can be a very helpful feature, if you want to hand over
|
||||
information from a middelware or a route decorator to the
|
||||
actual route handler.
|
||||
|
||||
Usage: ``request.state.time_started = time.time()``
|
||||
"""
|
||||
return self._starlette.state
|
||||
|
||||
@property
|
||||
async def encoding(self):
|
||||
"""The encoding of the Request's body. Can be set, manually. Must be awaited."""
|
||||
# Use the user-set encoding first.
|
||||
if self._encoding:
|
||||
return self._encoding
|
||||
|
||||
return await self.apparent_encoding
|
||||
|
||||
@encoding.setter
|
||||
def encoding(self, value):
|
||||
self._encoding = value
|
||||
|
||||
@property
|
||||
async def content(self):
|
||||
"""The Request body, as bytes. Must be awaited."""
|
||||
if not self._content:
|
||||
self._content = await self._starlette.body()
|
||||
return self._content
|
||||
|
||||
@property
|
||||
async def text(self):
|
||||
"""The Request body, as unicode. Must be awaited."""
|
||||
return (await self.content).decode(await self.encoding)
|
||||
|
||||
@property
|
||||
async def declared_encoding(self):
|
||||
if "Encoding" in self.headers:
|
||||
return self.headers["Encoding"]
|
||||
return None
|
||||
|
||||
@property
|
||||
async def apparent_encoding(self):
|
||||
"""The apparent encoding, provided by the chardet library. Must be awaited."""
|
||||
declared_encoding = await self.declared_encoding
|
||||
|
||||
if declared_encoding:
|
||||
return declared_encoding
|
||||
|
||||
return chardet.detect(await self.content)["encoding"] or DEFAULT_ENCODING
|
||||
|
||||
@property
|
||||
def is_secure(self):
|
||||
"""Returns ``True`` if the incoming Request was securely made."""
|
||||
return self._wz.is_secure
|
||||
return self.url.scheme == "https"
|
||||
|
||||
def accepts(self, content_type):
|
||||
"""Returns ``True`` if the incoming Request accepts the given ``content_type``."""
|
||||
return content_type in self.headers["Accept"]
|
||||
return content_type in self.headers.get("Accept", [])
|
||||
|
||||
def media(self, format=None):
|
||||
"""Renders incoming json/yaml/form data as Python objects.
|
||||
async def media(self, format: t.Union[str, t.Callable] = None): # noqa: A001, A002
|
||||
"""Renders incoming json/yaml/form data as Python objects. Must be awaited.
|
||||
|
||||
:param format: The name of the format being used. Alternatively accepts a custom callable for the format type.
|
||||
:param format: The name of the format being used.
|
||||
Alternatively, accepts a custom callable for the format type.
|
||||
"""
|
||||
|
||||
if format is None:
|
||||
format = "yaml" if "yaml" in self.mimetype or "" else "json"
|
||||
format = "yaml" if "yaml" in self.mimetype or "" else "json" # noqa: A001
|
||||
format = "form" if "form" in self.mimetype or "" else format # noqa: A001
|
||||
|
||||
if format in self.formats:
|
||||
return self.formats[format](self)
|
||||
else:
|
||||
return format(self)
|
||||
return await self.formats[format](self)
|
||||
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",
|
||||
"content",
|
||||
"encoding",
|
||||
"media",
|
||||
"headers",
|
||||
"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 = HTTP_200 #: The HTTP Status Code to use for the Response.
|
||||
self.text = None #: A unicode representation of the response body.
|
||||
self.status_code = None #: The HTTP Status Code to use for the Response.
|
||||
self.content = None #: A bytes representation of the response body.
|
||||
self.encoding = "utf-8"
|
||||
self.media = (
|
||||
None
|
||||
) #: A Python object that will be content-negotiated and sent back to the client. Typically, in JSON formatting.
|
||||
self.headers = (
|
||||
{}
|
||||
) #: A Python dictionary of {Key: value}, representing the headers of the response.
|
||||
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 = SimpleCookie() #: The cookies set in the Response
|
||||
self.session = (
|
||||
req.session
|
||||
) #: 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
|
||||
|
||||
def redirect(self, location, *, set_text=True, status_code=HTTP_301):
|
||||
self.status_code = status_code
|
||||
if set_text:
|
||||
self.text = f"Redirecting to: {location}"
|
||||
self.headers.update({"Location": location})
|
||||
|
||||
@property
|
||||
def body(self):
|
||||
if self.content:
|
||||
return (self.content, self.mimetype, {})
|
||||
async def body(self):
|
||||
if self._stream is not None:
|
||||
return (self._stream(), {})
|
||||
|
||||
if self.text:
|
||||
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):
|
||||
return self.formats[format](self, encode=True), {}
|
||||
for format_ in self.formats:
|
||||
if self.req.accepts(format_):
|
||||
return (await self.formats[format_](self, encode=True)), {}
|
||||
|
||||
# Default to JSON anyway.
|
||||
else:
|
||||
return (json.dumps(self.media), {"Content-Type": "application/json"})
|
||||
return (
|
||||
await self.formats["json"](self, encode=True),
|
||||
{"Content-Type": "application/json"},
|
||||
)
|
||||
|
||||
@property
|
||||
def gzipped_body(self):
|
||||
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
|
||||
|
||||
body, headers = self.body
|
||||
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)
|
||||
|
||||
if isinstance(body, str):
|
||||
body = body.encode(self.encoding)
|
||||
|
||||
if "gzip" in self.req.headers["Accept-Encoding"].lower():
|
||||
gzip_buffer = io.BytesIO()
|
||||
gzip_file = gzip.GzipFile(mode="wb", fileobj=gzip_buffer)
|
||||
gzip_file.write(body)
|
||||
gzip_file.close()
|
||||
|
||||
new_headers = {
|
||||
"Content-Encoding": "gzip",
|
||||
"Vary": "Accept-Encoding",
|
||||
"Content-Length": str(len(body)),
|
||||
}
|
||||
headers.update(new_headers)
|
||||
|
||||
return (gzip_buffer.getvalue(), headers)
|
||||
else:
|
||||
return (body, headers)
|
||||
|
||||
@property
|
||||
def _wz(self):
|
||||
body, headers = self.body
|
||||
if len(self.body) > 500:
|
||||
body, headers = self.gzipped_body
|
||||
async def __call__(self, scope, receive, send):
|
||||
body, headers = await self.body
|
||||
if self.headers:
|
||||
headers.update(self.headers)
|
||||
|
||||
r = WerkzeugResponse(body, status=self.status_code, direct_passthrough=False)
|
||||
r.headers = headers
|
||||
return r
|
||||
if self._stream is not None:
|
||||
response_cls = StarletteStreamingResponse
|
||||
else:
|
||||
response_cls = StarletteResponse
|
||||
|
||||
def __call__(self, environ, start_response):
|
||||
return self._wz(environ, start_response)
|
||||
response = response_cls(body, status_code=self.status_code, headers=headers)
|
||||
self._prepare_cookies(response)
|
||||
|
||||
await response(scope, receive, send)
|
||||
|
||||
class Schema(graphene.Schema):
|
||||
def on_request(self, req, resp):
|
||||
pass
|
||||
@property
|
||||
def ok(self):
|
||||
return 200 <= self.status_code < 300
|
||||
|
||||
+339
-21
@@ -1,38 +1,356 @@
|
||||
from parse import parse, search
|
||||
import asyncio
|
||||
import inspect
|
||||
import re
|
||||
import traceback
|
||||
from collections import defaultdict
|
||||
|
||||
from starlette.concurrency import run_in_threadpool
|
||||
from starlette.exceptions import HTTPException
|
||||
from starlette.middleware.wsgi import WSGIMiddleware
|
||||
from starlette.websockets import WebSocket, WebSocketClose
|
||||
|
||||
from . import status_codes
|
||||
from .formats import get_formats
|
||||
from .models import Request, Response
|
||||
|
||||
_CONVERTORS = {
|
||||
"int": (int, r"\d+"),
|
||||
"str": (str, r"[^/]+"),
|
||||
"float": (float, r"\d+(.\d+)?"),
|
||||
}
|
||||
|
||||
PARAM_RE = re.compile("{([a-zA-Z_][a-zA-Z0-9_]*)(:[a-zA-Z_][a-zA-Z0-9_]*)?}")
|
||||
|
||||
|
||||
class Route:
|
||||
def __init__(self, route, endpoint):
|
||||
def compile_path(path):
|
||||
path_re = "^"
|
||||
param_convertors = {}
|
||||
idx = 0
|
||||
|
||||
for match in PARAM_RE.finditer(path):
|
||||
param_name, convertor_type = match.groups(default="str")
|
||||
convertor_type = convertor_type.lstrip(":")
|
||||
assert (
|
||||
convertor_type in _CONVERTORS.keys()
|
||||
), f"Unknown path convertor '{convertor_type}'"
|
||||
convertor, convertor_re = _CONVERTORS[convertor_type]
|
||||
|
||||
path_re += path[idx : match.start()]
|
||||
path_re += rf"(?P<{param_name}>{convertor_re})"
|
||||
|
||||
param_convertors[param_name] = convertor
|
||||
|
||||
idx = match.end()
|
||||
|
||||
path_re += path[idx:] + "$"
|
||||
|
||||
return re.compile(path_re), param_convertors
|
||||
|
||||
|
||||
class BaseRoute:
|
||||
def matches(self, scope):
|
||||
raise NotImplementedError()
|
||||
|
||||
async def __call__(self, scope, receive, send):
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
class Route(BaseRoute):
|
||||
def __init__(self, route, endpoint, *, before_request=False):
|
||||
assert route.startswith("/"), "Route path must start with '/'"
|
||||
self.route = route
|
||||
self.endpoint = endpoint
|
||||
self.before_request = before_request
|
||||
|
||||
self.path_re, self.param_convertors = compile_path(route)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<Route {self.route!r}={self.endpoint!r}>"
|
||||
|
||||
def __eq__(self, other):
|
||||
if hasattr(other, "route"):
|
||||
# Being compared to other routes.
|
||||
return self.route == other.route
|
||||
else:
|
||||
# Strings.
|
||||
return self.does_match(other)
|
||||
def url(self, **params):
|
||||
return self.route.format(**params)
|
||||
|
||||
@property
|
||||
def has_parameters(self):
|
||||
return all([("{" in self.route), ("}" in self.route)])
|
||||
def endpoint_name(self):
|
||||
return self.endpoint.__name__
|
||||
|
||||
def does_match(self, s):
|
||||
if s == self.route:
|
||||
return True
|
||||
@property
|
||||
def description(self):
|
||||
return self.endpoint.__doc__
|
||||
|
||||
named = self.incoming_matches(s)
|
||||
return bool(len(named))
|
||||
def matches(self, scope):
|
||||
if scope["type"] != "http":
|
||||
return False, {}
|
||||
|
||||
def incoming_matches(self, s):
|
||||
results = parse(self.route, s)
|
||||
return results.named if results else {}
|
||||
path = scope["path"]
|
||||
match = self.path_re.match(path)
|
||||
|
||||
if match is None:
|
||||
return False, {}
|
||||
|
||||
matched_params = match.groupdict()
|
||||
for key, value in matched_params.items():
|
||||
matched_params[key] = self.param_convertors[key](value)
|
||||
|
||||
return True, {"path_params": {**matched_params}}
|
||||
|
||||
async def __call__(self, scope, receive, send):
|
||||
request = Request(scope, receive, formats=get_formats())
|
||||
response = Response(req=request, formats=get_formats())
|
||||
|
||||
path_params = scope.get("path_params", {})
|
||||
before_requests = scope.get("before_requests", [])
|
||||
|
||||
for before_request in before_requests.get("http", []):
|
||||
if asyncio.iscoroutinefunction(before_request):
|
||||
await before_request(request, response)
|
||||
else:
|
||||
await run_in_threadpool(before_request, request, response)
|
||||
|
||||
views = []
|
||||
|
||||
if inspect.isclass(self.endpoint):
|
||||
endpoint = self.endpoint()
|
||||
on_request = getattr(endpoint, "on_request", None)
|
||||
if on_request:
|
||||
views.append(on_request)
|
||||
|
||||
method_name = f"on_{request.method}"
|
||||
try:
|
||||
view = getattr(endpoint, method_name)
|
||||
views.append(view)
|
||||
except AttributeError as ex:
|
||||
if on_request is None:
|
||||
raise HTTPException(status_code=status_codes.HTTP_405) from ex
|
||||
else:
|
||||
views.append(self.endpoint)
|
||||
|
||||
for view in views:
|
||||
# "Monckey patch" for graphql: explicitly checking __call__
|
||||
if asyncio.iscoroutinefunction(view) or asyncio.iscoroutinefunction(
|
||||
view.__call__
|
||||
):
|
||||
await view(request, response, **path_params)
|
||||
else:
|
||||
await run_in_threadpool(view, request, response, **path_params)
|
||||
|
||||
if response.status_code is None:
|
||||
response.status_code = status_codes.HTTP_200
|
||||
|
||||
await response(scope, receive, send)
|
||||
|
||||
def __eq__(self, other):
|
||||
# [TODO] compare to str ?
|
||||
return self.route == other.route and self.endpoint == other.endpoint
|
||||
|
||||
def __hash__(self):
|
||||
return hash(self.route) ^ hash(self.endpoint) ^ hash(self.before_request)
|
||||
|
||||
|
||||
class WebSocketRoute(BaseRoute):
|
||||
def __init__(self, route, endpoint, *, before_request=False):
|
||||
assert route.startswith("/"), "Route path must start with '/'"
|
||||
self.route = route
|
||||
self.endpoint = endpoint
|
||||
self.before_request = before_request
|
||||
|
||||
self.path_re, self.param_convertors = compile_path(route)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<Route {self.route!r}={self.endpoint!r}>"
|
||||
|
||||
def url(self, **params):
|
||||
return self.route.format(**params)
|
||||
|
||||
# def is_graphql, is_wsgi
|
||||
@property
|
||||
def endpoint_name(self):
|
||||
return self.endpoint.__name__
|
||||
|
||||
@property
|
||||
def description(self):
|
||||
return self.endpoint.__doc__
|
||||
|
||||
def matches(self, scope):
|
||||
if scope["type"] != "websocket":
|
||||
return False, {}
|
||||
|
||||
path = scope["path"]
|
||||
match = self.path_re.match(path)
|
||||
|
||||
if match is None:
|
||||
return False, {}
|
||||
|
||||
matched_params = match.groupdict()
|
||||
for key, value in matched_params.items():
|
||||
matched_params[key] = self.param_convertors[key](value)
|
||||
|
||||
return True, {"path_params": {**matched_params}}
|
||||
|
||||
async def __call__(self, scope, receive, send):
|
||||
ws = WebSocket(scope, receive, send)
|
||||
|
||||
before_requests = scope.get("before_requests", [])
|
||||
for before_request in before_requests.get("ws", []):
|
||||
await before_request(ws)
|
||||
|
||||
await self.endpoint(ws)
|
||||
|
||||
def __eq__(self, other):
|
||||
# [TODO] compare to str ?
|
||||
return self.route == other.route and self.endpoint == other.endpoint
|
||||
|
||||
def __hash__(self):
|
||||
return hash(self.route) ^ hash(self.endpoint) ^ hash(self.before_request)
|
||||
|
||||
|
||||
class Router:
|
||||
def __init__(self, routes=None, default_response=None, before_requests=None):
|
||||
self.routes = [] if routes is None else list(routes)
|
||||
# [TODO] Make its own router
|
||||
self.apps = {}
|
||||
self.default_endpoint = (
|
||||
self.default_response if default_response is None else default_response
|
||||
)
|
||||
self.before_requests = (
|
||||
{"http": [], "ws": []} if before_requests is None else before_requests
|
||||
)
|
||||
self.events = defaultdict(list)
|
||||
|
||||
def add_route(
|
||||
self,
|
||||
route=None,
|
||||
endpoint=None,
|
||||
*,
|
||||
default=False,
|
||||
websocket=False,
|
||||
before_request=False,
|
||||
check_existing=False,
|
||||
):
|
||||
"""Adds a route to the router.
|
||||
:param route: A string representation of the route
|
||||
:param endpoint: The endpoint for the route -- can be callable, or class.
|
||||
:param default: If ``True``, all unknown requests will route to this view.
|
||||
"""
|
||||
if before_request:
|
||||
if websocket:
|
||||
self.before_requests.setdefault("ws", []).append(endpoint)
|
||||
else:
|
||||
self.before_requests.setdefault("http", []).append(endpoint)
|
||||
return
|
||||
|
||||
if check_existing:
|
||||
assert not self.routes or route not in (
|
||||
item.route for item in self.routes
|
||||
), f"Route '{route}' already exists"
|
||||
|
||||
if default:
|
||||
self.default_endpoint = endpoint
|
||||
|
||||
if websocket:
|
||||
route = WebSocketRoute(route, endpoint)
|
||||
else:
|
||||
route = Route(route, endpoint)
|
||||
|
||||
self.routes.append(route)
|
||||
|
||||
def mount(self, route, app):
|
||||
"""Mounts ASGI / WSGI applications at a given route"""
|
||||
self.apps.update(route, app)
|
||||
|
||||
def add_event_handler(self, event_type, handler):
|
||||
assert event_type in (
|
||||
"startup",
|
||||
"shutdown",
|
||||
), f"Only 'startup' and 'shutdown' events are supported, not {event_type}."
|
||||
self.events[event_type].append(handler)
|
||||
|
||||
async def trigger_event(self, event_type):
|
||||
for handler in self.events.get(event_type, []):
|
||||
if asyncio.iscoroutinefunction(handler):
|
||||
await handler()
|
||||
else:
|
||||
handler()
|
||||
|
||||
def before_request(self, endpoint, websocket=False):
|
||||
if websocket:
|
||||
self.before_requests.setdefault("ws", []).append(endpoint)
|
||||
else:
|
||||
self.before_requests.setdefault("http", []).append(endpoint)
|
||||
|
||||
def url_for(self, endpoint, **params):
|
||||
# TODO: Check for params
|
||||
for route in self.routes:
|
||||
if endpoint in (route.endpoint, route.endpoint.__name__):
|
||||
return route.url(**params)
|
||||
return None
|
||||
|
||||
async def default_response(self, scope, receive, send):
|
||||
if scope["type"] == "websocket":
|
||||
websocket_close = WebSocketClose()
|
||||
await websocket_close(receive, send)
|
||||
return
|
||||
|
||||
# FIXME: Please review!
|
||||
request = Request(scope, receive)
|
||||
response = Response(request, formats=get_formats()) # noqa: F841
|
||||
|
||||
raise HTTPException(status_code=status_codes.HTTP_404)
|
||||
|
||||
def _resolve_route(self, scope):
|
||||
for route in self.routes:
|
||||
matches, child_scope = route.matches(scope)
|
||||
if matches:
|
||||
scope.update(child_scope)
|
||||
return route
|
||||
return None
|
||||
|
||||
async def lifespan(self, scope, receive, send):
|
||||
message = await receive()
|
||||
assert message["type"] == "lifespan.startup"
|
||||
|
||||
try:
|
||||
await self.trigger_event("startup")
|
||||
except BaseException:
|
||||
msg = traceback.format_exc()
|
||||
await send({"type": "lifespan.startup.failed", "message": msg})
|
||||
raise
|
||||
|
||||
await send({"type": "lifespan.startup.complete"})
|
||||
message = await receive()
|
||||
assert message["type"] == "lifespan.shutdown"
|
||||
await self.trigger_event("shutdown")
|
||||
await send({"type": "lifespan.shutdown.complete"})
|
||||
|
||||
async def __call__(self, scope, receive, send):
|
||||
assert scope["type"] in ("http", "websocket", "lifespan")
|
||||
|
||||
if scope["type"] == "lifespan":
|
||||
await self.lifespan(scope, receive, send)
|
||||
return
|
||||
|
||||
path = scope["path"]
|
||||
root_path = scope.get("root_path", "")
|
||||
|
||||
# Check "primary" mounted routes first (before submounted apps)
|
||||
route = self._resolve_route(scope)
|
||||
|
||||
scope["before_requests"] = self.before_requests
|
||||
|
||||
if route is not None:
|
||||
await route(scope, receive, send)
|
||||
return
|
||||
|
||||
# Call into a submounted app, if one exists.
|
||||
for path_prefix, app in self.apps.items():
|
||||
if path.startswith(path_prefix):
|
||||
scope["path"] = path[len(path_prefix) :]
|
||||
scope["root_path"] = root_path + path_prefix
|
||||
try:
|
||||
await app(scope, receive, send)
|
||||
return
|
||||
except TypeError:
|
||||
app = WSGIMiddleware(app)
|
||||
await app(scope, receive, send)
|
||||
return
|
||||
|
||||
await self.default_endpoint(scope, receive, send)
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
from starlette.staticfiles import StaticFiles as StarletteStaticFiles
|
||||
|
||||
|
||||
class StaticFiles(StarletteStaticFiles):
|
||||
"""
|
||||
Extension to Starlette's `StaticFiles`.
|
||||
|
||||
I've created an issue to discuss allowing multiple directories in
|
||||
Starlette's `StaticFiles`.
|
||||
|
||||
https://github.com/encode/starlette/issues/625
|
||||
|
||||
I've also made a PR to add this method to Starlette StaticFiles
|
||||
Once accepted we will remove this.
|
||||
|
||||
https://github.com/encode/starlette/pull/626
|
||||
"""
|
||||
|
||||
def add_directory(self, directory: str) -> None:
|
||||
self.all_directories = [*self.all_directories, *self.get_directories(directory)]
|
||||
@@ -0,0 +1,15 @@
|
||||
API_THEMES = ["elements", "rapidoc", "redoc", "swagger_ui"]
|
||||
DEFAULT_ENCODING = "utf-8"
|
||||
DEFAULT_OPENAPI_THEME = "swagger_ui"
|
||||
DEFAULT_SESSION_COOKIE = "Responder-Session"
|
||||
DEFAULT_SECRET_KEY = "NOTASECRET" # noqa: S105
|
||||
|
||||
DEFAULT_CORS_PARAMS = {
|
||||
"allow_origins": (),
|
||||
"allow_methods": ("GET",),
|
||||
"allow_headers": (),
|
||||
"allow_credentials": False,
|
||||
"allow_origin_regex": None,
|
||||
"expose_headers": (),
|
||||
"max_age": 600,
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
from contextlib import contextmanager
|
||||
|
||||
import jinja2
|
||||
|
||||
|
||||
class Templates:
|
||||
def __init__(
|
||||
self, directory="templates", autoescape=True, context=None, enable_async=False
|
||||
):
|
||||
self.directory = directory
|
||||
self._env = jinja2.Environment(
|
||||
loader=jinja2.FileSystemLoader([str(self.directory)]),
|
||||
autoescape=autoescape, # noqa: S701
|
||||
enable_async=enable_async,
|
||||
)
|
||||
self.default_context = {} if context is None else {**context}
|
||||
self._env.globals.update(self.default_context)
|
||||
|
||||
@property
|
||||
def context(self):
|
||||
return self._env.globals
|
||||
|
||||
@context.setter
|
||||
def context(self, context):
|
||||
self._env.globals = {**self.default_context, **context}
|
||||
|
||||
def get_template(self, name):
|
||||
return self._env.get_template(name)
|
||||
|
||||
def render(self, template, *args, **kwargs):
|
||||
"""Renders the given `jinja2 <http://jinja.pocoo.org/docs/>`_ template, with provided values supplied.
|
||||
|
||||
:param template: The filename of the jinja2 template.
|
||||
:param **kwargs: Data to pass into the template.
|
||||
:param **kwargs: Data to pass into the template.
|
||||
""" # noqa: E501
|
||||
return self.get_template(template).render(*args, **kwargs)
|
||||
|
||||
@contextmanager
|
||||
def _async(self):
|
||||
self._env.is_async = True
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
self._env.is_async = False
|
||||
|
||||
async def render_async(self, template, *args, **kwargs):
|
||||
with self._async():
|
||||
return await self.get_template(template).render_async(*args, **kwargs)
|
||||
|
||||
def render_string(self, source, *args, **kwargs):
|
||||
"""Renders the given `jinja2 <http://jinja.pocoo.org/docs/>`_ template string, with provided values supplied.
|
||||
|
||||
:param source: The template to use.
|
||||
:param *args, **kwargs: Data to pass into the template.
|
||||
:param **kwargs: Data to pass into the template.
|
||||
""" # noqa: E501
|
||||
template = self._env.from_string(source)
|
||||
return template.render(*args, **kwargs)
|
||||
@@ -2,147 +2,65 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import codecs
|
||||
import os
|
||||
import sys
|
||||
from shutil import rmtree
|
||||
|
||||
from setuptools import find_packages, setup, Command
|
||||
from setuptools import find_packages, setup
|
||||
from versioningit import get_cmdclasses
|
||||
|
||||
here = os.path.abspath(os.path.dirname(__file__))
|
||||
|
||||
with codecs.open(os.path.join(here, "README.md"), encoding="utf-8") as f:
|
||||
long_description = "\n" + f.read()
|
||||
|
||||
about = {}
|
||||
|
||||
with open(os.path.join(here, "responder", "__version__.py")) as f:
|
||||
exec(f.read(), about)
|
||||
|
||||
if sys.argv[-1] == "publish":
|
||||
os.system("python setup.py sdist bdist_wheel upload")
|
||||
sys.exit()
|
||||
|
||||
required = [
|
||||
"waitress",
|
||||
"werkzeug",
|
||||
"pyyaml",
|
||||
"aiofiles",
|
||||
"apispec>=1.0.0b1",
|
||||
"chardet",
|
||||
"docopt-ng",
|
||||
"marshmallow",
|
||||
"requests",
|
||||
"requests-wsgi-adapter",
|
||||
"graphene",
|
||||
"graphql-server-core>=1.1",
|
||||
"requests-toolbelt",
|
||||
"rfc3986",
|
||||
"starlette[full]",
|
||||
"uvicorn[standard]",
|
||||
"whitenoise",
|
||||
"jinja2",
|
||||
"parse",
|
||||
]
|
||||
|
||||
|
||||
# https://pypi.python.org/pypi/stdeb/0.8.5#quickstart-2-just-tell-me-the-fastest-way-to-make-a-deb
|
||||
class DebCommand(Command):
|
||||
"""Support for setup.py deb"""
|
||||
|
||||
description = "Build and publish the .deb package."
|
||||
user_options = []
|
||||
|
||||
@staticmethod
|
||||
def status(s):
|
||||
"""Prints things in bold."""
|
||||
print("\033[1m{0}\033[0m".format(s))
|
||||
|
||||
def initialize_options(self):
|
||||
pass
|
||||
|
||||
def finalize_options(self):
|
||||
pass
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
self.status("Removing previous builds…")
|
||||
rmtree(os.path.join(here, "deb_dist"))
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
self.status(u"Creating debian mainfest…")
|
||||
os.system(
|
||||
"python setup.py --command-packages=stdeb.command sdist_dsc -z artful --package3=pipenv --depends3=python3-virtualenv-clone"
|
||||
)
|
||||
self.status(u"Building .deb…")
|
||||
os.chdir("deb_dist/pipenv-{0}".format(about["__version__"]))
|
||||
os.system("dpkg-buildpackage -rfakeroot -uc -us")
|
||||
|
||||
|
||||
class UploadCommand(Command):
|
||||
"""Support setup.py publish."""
|
||||
|
||||
description = "Build and publish the package."
|
||||
user_options = []
|
||||
|
||||
@staticmethod
|
||||
def status(s):
|
||||
"""Prints things in bold."""
|
||||
print("\033[1m{0}\033[0m".format(s))
|
||||
|
||||
def initialize_options(self):
|
||||
pass
|
||||
|
||||
def finalize_options(self):
|
||||
pass
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
self.status("Removing previous builds…")
|
||||
rmtree(os.path.join(here, "dist"))
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
self.status("Building Source distribution…")
|
||||
os.system("{0} setup.py sdist bdist_wheel".format(sys.executable))
|
||||
self.status("Uploading the package to PyPI via Twine…")
|
||||
os.system("twine upload dist/*")
|
||||
self.status("Pushing git tags…")
|
||||
os.system("git tag v{0}".format(about["__version__"]))
|
||||
os.system("git push --tags")
|
||||
sys.exit()
|
||||
|
||||
|
||||
setup(
|
||||
name="responder",
|
||||
version=about["__version__"],
|
||||
description="A sorta familiar HTTP framework.",
|
||||
description="A familiar HTTP Service Framework for Python.",
|
||||
long_description=long_description,
|
||||
long_description_content_type="text/markdown",
|
||||
author="Kenneth Reitz",
|
||||
author_email="me@kennethreitz.org",
|
||||
url="https://github.com/kennethreitz/responder",
|
||||
packages=find_packages(exclude=["tests"]),
|
||||
# entry_points={
|
||||
# "console_scripts": ["pipenv=pipenv:cli", "pipenv-resolver=pipenv.resolver:main"]
|
||||
# },
|
||||
package_data={
|
||||
# "": ["LICENSE", "NOTICES"],
|
||||
# "pipenv.vendor.requests": ["*.pem"],
|
||||
# "pipenv.vendor.certifi": ["*.pem"],
|
||||
# "pipenv.vendor.click_completion": ["*.j2"],
|
||||
# "pipenv.patched.notpip._vendor.certifi": ["*.pem"],
|
||||
# "pipenv.patched.notpip._vendor.requests": ["*.pem"],
|
||||
# "pipenv.patched.notpip._vendor.distlib._backport": ["sysconfig.cfg"],
|
||||
# "pipenv.patched.notpip._vendor.distlib": [
|
||||
# "t32.exe",
|
||||
# "t64.exe",
|
||||
# "w32.exe",
|
||||
# "w64.exe",
|
||||
# ],
|
||||
},
|
||||
python_requires=">=3.6",
|
||||
package_data={},
|
||||
python_requires=">=3.10",
|
||||
setup_requires=[],
|
||||
install_requires=required,
|
||||
extras_require={},
|
||||
extras_require={
|
||||
"develop": ["poethepoet", "pyproject-fmt", "ruff", "validate-pyproject"],
|
||||
"graphql": ["graphene"],
|
||||
"release": ["build", "twine"],
|
||||
"test": ["pytest", "pytest-cov", "pytest-mock", "flask"],
|
||||
},
|
||||
include_package_data=True,
|
||||
license="Apache 2.0",
|
||||
classifiers=[
|
||||
"License :: OSI Approved :: MIT License",
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
"Environment :: Web Environment",
|
||||
"Intended Audience :: Developers",
|
||||
"License :: OSI Approved :: Apache Software License",
|
||||
"Operating System :: OS Independent",
|
||||
"Programming Language :: Python",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.6",
|
||||
"Programming Language :: Python :: 3.7",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
"Programming Language :: Python :: 3.12",
|
||||
"Programming Language :: Python :: 3.13",
|
||||
"Programming Language :: Python :: Implementation :: CPython",
|
||||
"Programming Language :: Python :: Implementation :: PyPy",
|
||||
"Topic :: Internet :: WWW/HTTP",
|
||||
],
|
||||
cmdclass={"upload": UploadCommand, "deb": DebCommand},
|
||||
cmdclass=get_cmdclasses(),
|
||||
)
|
||||
|
||||
@@ -1,96 +0,0 @@
|
||||
this is a test
|
||||
|
||||
Lorem ipsum dolor, sit amet consectetur adipisicing elit. Maiores in, ea beatae praesentium quis enim exercitationem
|
||||
voluptate repellat possimus laborum provident voluptates numquam id atque tempora. Quidem et repudiandae aliquam?
|
||||
|
||||
Lorem ipsum dolor sit amet consectetur adipisicing elit. Libero reiciendis consequuntur deserunt iure nesciunt autem
|
||||
saepe magnam quas, debitis aliquam molestias possimus necessitatibus cumque enim modi fuga tenetur hic natus?
|
||||
|
||||
Lorem ipsum dolor sit amet consectetur adipisicing elit. Quia magni maxime, optio aliquid tempore dignissimos aperiam
|
||||
voluptatibus, quae sunt vel iste nesciunt. Commodi saepe ipsam architecto omnis neque sequi beatae.
|
||||
|
||||
Lorem ipsum, dolor sit amet consectetur adipisicing elit. Neque quam sequi quidem corporis repudiandae quo, fugiat
|
||||
ullam inventore, ratione cupiditate maiores nobis autem asperiores earum dolorum praesentium quod consequuntur nostrum!
|
||||
|
||||
Lorem ipsum dolor sit amet consectetur adipisicing elit. Dolorum, in? Officiis ratione veritatis distinctio quas illo
|
||||
voluptatibus quia velit corrupti. Tempora ipsam perspiciatis ullam sapiente itaque esse doloribus error culpa.this is a
|
||||
test
|
||||
|
||||
Lorem ipsum dolor, sit amet consectetur adipisicing elit. Maiores in, ea beatae praesentium quis enim exercitationem
|
||||
voluptate repellat possimus laborum provident voluptates numquam id atque tempora. Quidem et repudiandae aliquam?
|
||||
|
||||
Lorem ipsum dolor sit amet consectetur adipisicing elit. Libero reiciendis consequuntur deserunt iure nesciunt autem
|
||||
saepe magnam quas, debitis aliquam molestias possimus necessitatibus cumque enim modi fuga tenetur hic natus?
|
||||
|
||||
Lorem ipsum dolor sit amet consectetur adipisicing elit. Quia magni maxime, optio aliquid tempore dignissimos aperiam
|
||||
voluptatibus, quae sunt vel iste nesciunt. Commodi saepe ipsam architecto omnis neque sequi beatae.
|
||||
|
||||
Lorem ipsum, dolor sit amet consectetur adipisicing elit. Neque quam sequi quidem corporis repudiandae quo, fugiat
|
||||
ullam inventore, ratione cupiditate maiores nobis autem asperiores earum dolorum praesentium quod consequuntur nostrum!
|
||||
|
||||
Lorem ipsum dolor sit amet consectetur adipisicing elit. Dolorum, in? Officiis ratione veritatis distinctio quas illo
|
||||
voluptatibus quia velit corrupti. Tempora ipsam perspiciatis ullam sapiente itaque esse doloribus error culpa.this is a
|
||||
test
|
||||
|
||||
Lorem ipsum dolor, sit amet consectetur adipisicing elit. Maiores in, ea beatae praesentium quis enim exercitationem
|
||||
voluptate repellat possimus laborum provident voluptates numquam id atque tempora. Quidem et repudiandae aliquam?
|
||||
|
||||
Lorem ipsum dolor sit amet consectetur adipisicing elit. Libero reiciendis consequuntur deserunt iure nesciunt autem
|
||||
saepe magnam quas, debitis aliquam molestias possimus necessitatibus cumque enim modi fuga tenetur hic natus?
|
||||
|
||||
Lorem ipsum dolor sit amet consectetur adipisicing elit. Quia magni maxime, optio aliquid tempore dignissimos aperiam
|
||||
voluptatibus, quae sunt vel iste nesciunt. Commodi saepe ipsam architecto omnis neque sequi beatae.
|
||||
|
||||
Lorem ipsum, dolor sit amet consectetur adipisicing elit. Neque quam sequi quidem corporis repudiandae quo, fugiat
|
||||
ullam inventore, ratione cupiditate maiores nobis autem asperiores earum dolorum praesentium quod consequuntur nostrum!
|
||||
|
||||
Lorem ipsum dolor sit amet consectetur adipisicing elit. Dolorum, in? Officiis ratione veritatis distinctio quas illo
|
||||
voluptatibus quia velit corrupti. Tempora ipsam perspiciatis ullam sapiente itaque esse doloribus error culpa.this is a
|
||||
test
|
||||
|
||||
Lorem ipsum dolor, sit amet consectetur adipisicing elit. Maiores in, ea beatae praesentium quis enim exercitationem
|
||||
voluptate repellat possimus laborum provident voluptates numquam id atque tempora. Quidem et repudiandae aliquam?
|
||||
|
||||
Lorem ipsum dolor sit amet consectetur adipisicing elit. Libero reiciendis consequuntur deserunt iure nesciunt autem
|
||||
saepe magnam quas, debitis aliquam molestias possimus necessitatibus cumque enim modi fuga tenetur hic natus?
|
||||
|
||||
Lorem ipsum dolor sit amet consectetur adipisicing elit. Quia magni maxime, optio aliquid tempore dignissimos aperiam
|
||||
voluptatibus, quae sunt vel iste nesciunt. Commodi saepe ipsam architecto omnis neque sequi beatae.
|
||||
|
||||
Lorem ipsum, dolor sit amet consectetur adipisicing elit. Neque quam sequi quidem corporis repudiandae quo, fugiat
|
||||
ullam inventore, ratione cupiditate maiores nobis autem asperiores earum dolorum praesentium quod consequuntur nostrum!
|
||||
|
||||
Lorem ipsum dolor sit amet consectetur adipisicing elit. Dolorum, in? Officiis ratione veritatis distinctio quas illo
|
||||
voluptatibus quia velit corrupti. Tempora ipsam perspiciatis ullam sapiente itaque esse doloribus error culpa.this is a
|
||||
test
|
||||
|
||||
Lorem ipsum dolor, sit amet consectetur adipisicing elit. Maiores in, ea beatae praesentium quis enim exercitationem
|
||||
voluptate repellat possimus laborum provident voluptates numquam id atque tempora. Quidem et repudiandae aliquam?
|
||||
|
||||
Lorem ipsum dolor sit amet consectetur adipisicing elit. Libero reiciendis consequuntur deserunt iure nesciunt autem
|
||||
saepe magnam quas, debitis aliquam molestias possimus necessitatibus cumque enim modi fuga tenetur hic natus?
|
||||
|
||||
Lorem ipsum dolor sit amet consectetur adipisicing elit. Quia magni maxime, optio aliquid tempore dignissimos aperiam
|
||||
voluptatibus, quae sunt vel iste nesciunt. Commodi saepe ipsam architecto omnis neque sequi beatae.
|
||||
|
||||
Lorem ipsum, dolor sit amet consectetur adipisicing elit. Neque quam sequi quidem corporis repudiandae quo, fugiat
|
||||
ullam inventore, ratione cupiditate maiores nobis autem asperiores earum dolorum praesentium quod consequuntur nostrum!
|
||||
|
||||
Lorem ipsum dolor sit amet consectetur adipisicing elit. Dolorum, in? Officiis ratione veritatis distinctio quas illo
|
||||
voluptatibus quia velit corrupti. Tempora ipsam perspiciatis ullam sapiente itaque esse doloribus error culpa.this is a
|
||||
test
|
||||
|
||||
Lorem ipsum dolor, sit amet consectetur adipisicing elit. Maiores in, ea beatae praesentium quis enim exercitationem
|
||||
voluptate repellat possimus laborum provident voluptates numquam id atque tempora. Quidem et repudiandae aliquam?
|
||||
|
||||
Lorem ipsum dolor sit amet consectetur adipisicing elit. Libero reiciendis consequuntur deserunt iure nesciunt autem
|
||||
saepe magnam quas, debitis aliquam molestias possimus necessitatibus cumque enim modi fuga tenetur hic natus?
|
||||
|
||||
Lorem ipsum dolor sit amet consectetur adipisicing elit. Quia magni maxime, optio aliquid tempore dignissimos aperiam
|
||||
voluptatibus, quae sunt vel iste nesciunt. Commodi saepe ipsam architecto omnis neque sequi beatae.
|
||||
|
||||
Lorem ipsum, dolor sit amet consectetur adipisicing elit. Neque quam sequi quidem corporis repudiandae quo, fugiat
|
||||
ullam inventore, ratione cupiditate maiores nobis autem asperiores earum dolorum praesentium quod consequuntur nostrum!
|
||||
|
||||
Lorem ipsum dolor sit amet consectetur adipisicing elit. Dolorum, in? Officiis ratione veritatis distinctio quas illo
|
||||
voluptatibus quia velit corrupti. Tempora ipsam perspiciatis ullam sapiente itaque esse doloribus error culpa.
|
||||
@@ -1,198 +0,0 @@
|
||||
import graphene
|
||||
import pytest
|
||||
import responder
|
||||
import yaml
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def api():
|
||||
return responder.API()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def flask():
|
||||
import flask
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
@app.route("/")
|
||||
def hello():
|
||||
return "Hello World!"
|
||||
|
||||
return app
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def schema():
|
||||
class Query(graphene.ObjectType):
|
||||
hello = graphene.String(name=graphene.String(default_value="stranger"))
|
||||
|
||||
def resolve_hello(self, info, name):
|
||||
return "Hello " + name
|
||||
|
||||
return graphene.Schema(query=Query)
|
||||
|
||||
|
||||
def test_api_basic_route(api):
|
||||
@api.route("/")
|
||||
def home(req, resp):
|
||||
resp.text = "hello world!"
|
||||
|
||||
|
||||
def test_api_basic_route_overlap(api):
|
||||
@api.route("/")
|
||||
def home(req, resp):
|
||||
resp.text = "hello world!"
|
||||
|
||||
with pytest.raises(AssertionError):
|
||||
|
||||
@api.route("/")
|
||||
def home2(req, resp):
|
||||
resp.text = "hello world!"
|
||||
|
||||
|
||||
def test_api_basic_route_overlap_alternative(api):
|
||||
@api.route("/")
|
||||
def home(req, resp):
|
||||
resp.text = "hello world!"
|
||||
|
||||
def home2(req, resp):
|
||||
resp.text = "hello world!"
|
||||
|
||||
with pytest.raises(AssertionError):
|
||||
api.add_route("/", home2)
|
||||
|
||||
|
||||
def test_api_basic_route_overlap_allowed(api):
|
||||
@api.route("/")
|
||||
def home(req, resp):
|
||||
resp.text = "hello world!"
|
||||
|
||||
def home2(req, resp):
|
||||
resp.text = "hello world!"
|
||||
|
||||
api.add_route("/", home2, check_existing=False)
|
||||
|
||||
|
||||
def test_api_basic_route_overlap_allowed_alternative(api):
|
||||
@api.route("/")
|
||||
def home(req, resp):
|
||||
resp.text = "hello world!"
|
||||
|
||||
@api.route("/", check_existing=False)
|
||||
def home2(req, resp):
|
||||
resp.text = "hello world!"
|
||||
|
||||
|
||||
def test_class_based_view_registration(api):
|
||||
@api.route("/")
|
||||
class ThingsResource:
|
||||
def on_request(req, resp):
|
||||
resp.text = "42"
|
||||
|
||||
|
||||
def test_requests_session(api):
|
||||
assert api.session()
|
||||
|
||||
|
||||
def test_requests_session_works(api):
|
||||
TEXT = "spiral out"
|
||||
|
||||
@api.route("/")
|
||||
def hello(req, resp):
|
||||
resp.text = TEXT
|
||||
|
||||
assert api.session().get("http://;/").text == TEXT
|
||||
|
||||
|
||||
def test_status_code(api):
|
||||
@api.route("/")
|
||||
def hello(req, resp):
|
||||
resp.text = "keep going"
|
||||
resp.status_code = responder.status_codes.HTTP_416
|
||||
|
||||
assert api.session().get("http://;/").status_code == responder.status_codes.HTTP_416
|
||||
|
||||
|
||||
def test_json_media(api):
|
||||
dump = {"life": 42}
|
||||
|
||||
@api.route("/")
|
||||
def media(req, resp):
|
||||
resp.media = dump
|
||||
|
||||
r = api.session().get("http://;/")
|
||||
|
||||
assert "json" in r.headers["Content-Type"]
|
||||
assert r.json() == dump
|
||||
|
||||
|
||||
def test_yaml_media(api):
|
||||
dump = {"life": 42}
|
||||
|
||||
@api.route("/")
|
||||
def media(req, resp):
|
||||
resp.media = dump
|
||||
|
||||
r = api.session().get("http://;/", headers={"Accept": "yaml"})
|
||||
|
||||
assert "yaml" in r.headers["Content-Type"]
|
||||
assert yaml.load(r.content) == dump
|
||||
|
||||
|
||||
def test_graphql_schema_query_querying(api, schema):
|
||||
api.add_route("/", schema)
|
||||
|
||||
r = api.session().get("http://;/?q={ hello }", headers={"Accept": "json"})
|
||||
assert r.json() == {"data": {"hello": None}}
|
||||
|
||||
|
||||
def test_argumented_routing(api):
|
||||
@api.route("/{name}")
|
||||
def hello(req, resp, *, name):
|
||||
resp.text = f"Hello, {name}."
|
||||
|
||||
r = api.session().get("http://;/sean")
|
||||
assert r.text == "Hello, sean."
|
||||
|
||||
|
||||
def test_mote_argumented_routing(api):
|
||||
@api.route("/{greeting}/{name}")
|
||||
def hello(req, resp, *, greeting, name):
|
||||
resp.text = f"{greeting}, {name}."
|
||||
|
||||
r = api.session().get("http://;/hello/lyndsy")
|
||||
assert r.text == "hello, lyndsy."
|
||||
|
||||
|
||||
def test_request_and_get(api):
|
||||
@api.route("/")
|
||||
class ThingsResource:
|
||||
def on_request(self, req, resp):
|
||||
resp.headers.update({"DEATH": "666"})
|
||||
|
||||
def on_get(self, request, resp):
|
||||
resp.headers.update({"LIFE": "42"})
|
||||
|
||||
r = api.session().get("http://;/")
|
||||
assert "DEATH" in r.headers
|
||||
assert "LIFE" in r.headers
|
||||
|
||||
|
||||
def test_query_params(api):
|
||||
@api.route("/")
|
||||
def route(req, resp):
|
||||
resp.media = {"params": req.params}
|
||||
|
||||
r = api.session().get("http://;/?q=q")
|
||||
assert r.json()["params"] == {"q": "q"}
|
||||
|
||||
|
||||
def test_form_data(api):
|
||||
@api.route("/")
|
||||
def route(req, resp):
|
||||
resp.media = {"form": req.media("form")}
|
||||
|
||||
dump = {"q": "q"}
|
||||
r = api.session().get("http://;/", data=dump)
|
||||
assert r.json()["form"] == dump
|
||||
@@ -0,0 +1,55 @@
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
import responder
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def data_dir(current_dir):
|
||||
yield current_dir / "data"
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def current_dir():
|
||||
yield Path(__file__).parent
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def api():
|
||||
return responder.API(debug=False, allowed_hosts=[";"])
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def session(api):
|
||||
return api.requests
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def url():
|
||||
def url_for(s):
|
||||
return f"http://;{s}"
|
||||
|
||||
return url_for
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def flask():
|
||||
from flask import Flask
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
@app.route("/")
|
||||
def hello():
|
||||
return "Hello World!"
|
||||
|
||||
return app
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def template_path(tmpdir):
|
||||
# create a Jinja template file on the filesystem
|
||||
template_name = "test.html"
|
||||
template_file = tmpdir.mkdir("static").join(template_name)
|
||||
template_file.write("{{ var }}")
|
||||
return template_file
|
||||
@@ -0,0 +1,21 @@
|
||||
def test_custom_encoding(api, session):
|
||||
data = "hi alex!"
|
||||
|
||||
@api.route("/")
|
||||
async def route(req, resp):
|
||||
req.encoding = "ascii"
|
||||
resp.text = await req.text
|
||||
|
||||
r = session.post(api.url_for(route), data=data)
|
||||
assert r.text == data
|
||||
|
||||
|
||||
def test_bytes_encoding(api, session):
|
||||
data = b"hi lenny!"
|
||||
|
||||
@api.route("/")
|
||||
async def route(req, resp):
|
||||
resp.text = (await req.content).decode("utf-8")
|
||||
|
||||
r = session.post(api.url_for(route), data=data)
|
||||
assert r.content == data
|
||||
@@ -0,0 +1,60 @@
|
||||
import inspect
|
||||
|
||||
import pytest
|
||||
|
||||
from responder import models
|
||||
|
||||
_default_query = "q=%7b%20hello%20%7d&name=myname&user_name=test_user"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"query, expected",
|
||||
[
|
||||
pytest.param(
|
||||
_default_query,
|
||||
{"q": ["{ hello }"], "name": ["myname"], "user_name": ["test_user"]},
|
||||
id="parse query with unique keys",
|
||||
),
|
||||
pytest.param(
|
||||
"q=1&q=2&q=3", {"q": ["1", "2", "3"]}, id="parse query with the same key"
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_query_dict(query, expected):
|
||||
d = models.QueryDict(query)
|
||||
assert d == expected
|
||||
|
||||
|
||||
def test_query_dict_get():
|
||||
d = models.QueryDict(_default_query)
|
||||
|
||||
assert d["user_name"] == "test_user"
|
||||
assert d.get("key_none_exist") is None
|
||||
|
||||
|
||||
def test_query_dict_get_list():
|
||||
d = models.QueryDict(_default_query)
|
||||
|
||||
assert d.get_list("user_name") == ["test_user"]
|
||||
assert d.get_list("key_none_exist") == []
|
||||
assert d.get_list("key_none_exist", ["foo"]) == ["foo"]
|
||||
|
||||
|
||||
def test_query_dict_items_list():
|
||||
d = models.QueryDict(_default_query)
|
||||
|
||||
items_list = d.items_list()
|
||||
assert inspect.isgenerator(items_list)
|
||||
assert dict(items_list) == {
|
||||
"q": ["{ hello }"],
|
||||
"name": ["myname"],
|
||||
"user_name": ["test_user"],
|
||||
}
|
||||
|
||||
|
||||
def test_query_dict_items():
|
||||
d = models.QueryDict(_default_query)
|
||||
|
||||
items = d.items()
|
||||
assert inspect.isgenerator(items)
|
||||
assert dict(items) == {"q": "{ hello }", "name": "myname", "user_name": "test_user"}
|
||||
@@ -0,0 +1,992 @@
|
||||
import random
|
||||
import string
|
||||
|
||||
import pytest
|
||||
import yaml
|
||||
from starlette.middleware.base import BaseHTTPMiddleware
|
||||
from starlette.testclient import TestClient as StarletteTestClient
|
||||
|
||||
import responder
|
||||
from responder.routes import Route, WebSocketRoute
|
||||
from responder.templates import Templates
|
||||
|
||||
|
||||
def test_api_basic_route(api):
|
||||
@api.route("/")
|
||||
def home(req, resp):
|
||||
resp.text = "hello world!"
|
||||
|
||||
|
||||
def test_route_repr():
|
||||
def home(req, resp):
|
||||
"""Home page"""
|
||||
resp.text = "Hello !"
|
||||
|
||||
route = Route("/", home)
|
||||
|
||||
assert route.__repr__() == f"<Route '/'={home!r}>"
|
||||
|
||||
assert route.endpoint_name == home.__name__
|
||||
assert route.description == home.__doc__
|
||||
|
||||
|
||||
def test_websocket_route_repr():
|
||||
def chat_endpoint(ws):
|
||||
"""Chat"""
|
||||
pass
|
||||
|
||||
route = WebSocketRoute("/", chat_endpoint)
|
||||
|
||||
assert route.__repr__() == f"<Route '/'={chat_endpoint!r}>"
|
||||
|
||||
assert route.endpoint_name == chat_endpoint.__name__
|
||||
assert route.description == chat_endpoint.__doc__
|
||||
|
||||
|
||||
def test_route_eq():
|
||||
def home(req, resp):
|
||||
resp.text = "Hello !"
|
||||
|
||||
assert Route("/", home) == Route("/", home)
|
||||
|
||||
def chat(ws):
|
||||
pass
|
||||
|
||||
assert WebSocketRoute("/", home) == WebSocketRoute("/", home)
|
||||
|
||||
|
||||
"""
|
||||
def test_api_basic_route_overlap(api):
|
||||
@api.route("/")
|
||||
def home(req, resp):
|
||||
resp.text = "hello world!"
|
||||
|
||||
with pytest.raises(AssertionError):
|
||||
|
||||
@api.route("/")
|
||||
def home2(req, resp):
|
||||
resp.text = "hello world!"
|
||||
"""
|
||||
|
||||
|
||||
def test_class_based_view_registration(api):
|
||||
@api.route("/")
|
||||
class ThingsResource:
|
||||
def on_request(req, resp):
|
||||
resp.text = "42"
|
||||
|
||||
|
||||
def test_class_based_view_parameters(api):
|
||||
@api.route("/{greeting}")
|
||||
class Greeting:
|
||||
pass
|
||||
|
||||
resp = api.session().get("http://;/Hello")
|
||||
assert resp.status_code == api.status_codes.HTTP_405
|
||||
|
||||
|
||||
def test_requests_session(api):
|
||||
assert api.session()
|
||||
assert api.requests
|
||||
|
||||
|
||||
def test_requests_session_works(api):
|
||||
TEXT = "spiral out"
|
||||
|
||||
@api.route("/")
|
||||
def hello(req, resp):
|
||||
resp.text = TEXT
|
||||
|
||||
assert api.requests.get("/").text == TEXT
|
||||
|
||||
|
||||
def test_status_code(api):
|
||||
@api.route("/")
|
||||
def hello(req, resp):
|
||||
resp.text = "keep going"
|
||||
resp.status_code = responder.status_codes.HTTP_416
|
||||
|
||||
assert api.requests.get("http://;/").status_code == responder.status_codes.HTTP_416
|
||||
|
||||
|
||||
def test_json_media(api):
|
||||
dump = {"life": 42}
|
||||
|
||||
@api.route("/")
|
||||
def media(req, resp):
|
||||
resp.media = dump
|
||||
|
||||
r = api.requests.get("http://;/")
|
||||
|
||||
assert "json" in r.headers["Content-Type"]
|
||||
assert r.json() == dump
|
||||
|
||||
|
||||
def test_yaml_media(api):
|
||||
dump = {"life": 42}
|
||||
|
||||
@api.route("/")
|
||||
def media(req, resp):
|
||||
resp.media = dump
|
||||
|
||||
r = api.requests.get("http://;/", headers={"Accept": "yaml"})
|
||||
|
||||
assert "yaml" in r.headers["Content-Type"]
|
||||
assert yaml.load(r.content, Loader=yaml.FullLoader) == dump # noqa: S506
|
||||
|
||||
|
||||
def test_argumented_routing(api):
|
||||
@api.route("/{name}")
|
||||
def hello(req, resp, *, name):
|
||||
resp.text = f"Hello, {name}."
|
||||
|
||||
r = api.requests.get(api.url_for(hello, name="sean"))
|
||||
assert r.text == "Hello, sean."
|
||||
|
||||
|
||||
def test_mote_argumented_routing(api):
|
||||
@api.route("/{greeting}/{name}")
|
||||
def hello(req, resp, *, greeting, name):
|
||||
resp.text = f"{greeting}, {name}."
|
||||
|
||||
r = api.requests.get(api.url_for(hello, greeting="hello", name="lyndsy"))
|
||||
assert r.text == "hello, lyndsy."
|
||||
|
||||
|
||||
def test_request_and_get(api):
|
||||
@api.route("/")
|
||||
class ThingsResource:
|
||||
def on_request(self, req, resp):
|
||||
resp.headers.update({"DEATH": "666"})
|
||||
|
||||
def on_get(self, req, resp):
|
||||
resp.headers.update({"LIFE": "42"})
|
||||
|
||||
r = api.requests.get(api.url_for(ThingsResource))
|
||||
assert "DEATH" in r.headers
|
||||
assert "LIFE" in r.headers
|
||||
|
||||
|
||||
def test_class_based_view_status_code(api):
|
||||
@api.route("/")
|
||||
class ThingsResource:
|
||||
def on_request(self, req, resp):
|
||||
resp.status_code = responder.status_codes.HTTP_416
|
||||
|
||||
assert api.requests.get("http://;/").status_code == responder.status_codes.HTTP_416
|
||||
|
||||
|
||||
def test_query_params(api, url):
|
||||
@api.route("/")
|
||||
def route(req, resp):
|
||||
resp.media = {"params": req.params}
|
||||
|
||||
r = api.requests.get(api.url_for(route), params={"q": "q"})
|
||||
assert r.json()["params"] == {"q": "q"}
|
||||
|
||||
r = api.requests.get(url("/?q=1&q=2&q=3"))
|
||||
assert r.json()["params"] == {"q": "3"}
|
||||
|
||||
|
||||
# Requires https://github.com/encode/starlette/pull/102
|
||||
# def test_form_data(api):
|
||||
|
||||
# @api.route("/")
|
||||
# async def route(req, resp):
|
||||
# resp.media = {"form": await req.media("form")}
|
||||
|
||||
# dump = {"q": "q"}
|
||||
|
||||
# r = api.requests.get(api.url_for(route), params=dump)
|
||||
# assert r.json()["form"] == dump
|
||||
|
||||
|
||||
def test_async_function(api):
|
||||
content = "The Emerald Tablet of Hermes"
|
||||
|
||||
@api.route("/")
|
||||
async def route(req, resp):
|
||||
resp.text = content
|
||||
|
||||
r = api.requests.get(api.url_for(route))
|
||||
assert r.text == content
|
||||
|
||||
|
||||
def test_media_parsing(api):
|
||||
dump = {"hello": "sam"}
|
||||
|
||||
@api.route("/")
|
||||
def route(req, resp):
|
||||
resp.media = dump
|
||||
|
||||
r = api.requests.get(api.url_for(route))
|
||||
assert r.json() == dump
|
||||
|
||||
r = api.requests.get(api.url_for(route), headers={"Accept": "application/x-yaml"})
|
||||
assert r.text == "hello: sam\n"
|
||||
|
||||
|
||||
def test_background(api):
|
||||
@api.route("/")
|
||||
def route(req, resp):
|
||||
@api.background.task
|
||||
def task():
|
||||
import time
|
||||
|
||||
time.sleep(3)
|
||||
|
||||
task()
|
||||
api.text = "ok"
|
||||
|
||||
r = api.requests.get(api.url_for(route))
|
||||
assert r.status_code < 300
|
||||
|
||||
|
||||
def test_multiple_routes(api):
|
||||
@api.route("/1")
|
||||
def route1(req, resp):
|
||||
resp.text = "1"
|
||||
|
||||
@api.route("/2")
|
||||
def route2(req, resp):
|
||||
resp.text = "2"
|
||||
|
||||
r = api.requests.get(api.url_for(route1))
|
||||
assert r.text == "1"
|
||||
|
||||
r = api.requests.get(api.url_for(route2))
|
||||
assert r.text == "2"
|
||||
|
||||
|
||||
def test_json_uploads(api):
|
||||
@api.route("/")
|
||||
async def route(req, resp):
|
||||
resp.media = await req.media()
|
||||
|
||||
dump = {"complicated": "times"}
|
||||
r = api.requests.post(api.url_for(route), json=dump)
|
||||
assert r.json() == dump
|
||||
|
||||
|
||||
def test_yaml_uploads(api):
|
||||
@api.route("/")
|
||||
async def route(req, resp):
|
||||
resp.media = await req.media()
|
||||
|
||||
dump = {"complicated": "times"}
|
||||
r = api.requests.post(
|
||||
api.url_for(route),
|
||||
data=yaml.dump(dump),
|
||||
headers={"Content-Type": "application/x-yaml"},
|
||||
)
|
||||
assert r.json() == dump
|
||||
|
||||
|
||||
def test_form_uploads(api):
|
||||
@api.route("/")
|
||||
async def route(req, resp):
|
||||
resp.media = await req.media()
|
||||
|
||||
dump = {"complicated": "times"}
|
||||
r = api.requests.post(api.url_for(route), data=dump)
|
||||
assert r.json() == dump
|
||||
|
||||
# requests with boundary
|
||||
files = {"complicated": (None, "times")}
|
||||
r = api.requests.post(api.url_for(route), files=files)
|
||||
assert r.json() == {"complicated": "times"}
|
||||
|
||||
|
||||
def test_json_downloads(api):
|
||||
dump = {"testing": "123"}
|
||||
|
||||
@api.route("/")
|
||||
def route(req, resp):
|
||||
resp.media = dump
|
||||
|
||||
r = api.requests.get(api.url_for(route), headers={"Content-Type": "application/json"})
|
||||
assert r.json() == dump
|
||||
|
||||
|
||||
def test_yaml_downloads(api):
|
||||
dump = {"testing": "123"}
|
||||
|
||||
@api.route("/")
|
||||
def route(req, resp):
|
||||
resp.media = dump
|
||||
|
||||
r = api.requests.get(
|
||||
api.url_for(route), headers={"Content-Type": "application/x-yaml"}
|
||||
)
|
||||
assert yaml.safe_load(r.content) == dump
|
||||
|
||||
|
||||
def test_schema_generation_explicit():
|
||||
import marshmallow
|
||||
|
||||
import responder
|
||||
from responder.ext.schema import OpenAPISchema as OpenAPISchema
|
||||
|
||||
api = responder.API()
|
||||
|
||||
schema = OpenAPISchema(app=api, title="Web Service", version="1.0", openapi="3.0.2")
|
||||
|
||||
@schema.schema("Pet")
|
||||
class PetSchema(marshmallow.Schema):
|
||||
name = marshmallow.fields.Str()
|
||||
|
||||
@api.route("/")
|
||||
def route(req, resp):
|
||||
"""A cute furry animal endpoint.
|
||||
---
|
||||
get:
|
||||
description: Get a random pet
|
||||
responses:
|
||||
200:
|
||||
description: A pet to be returned
|
||||
schema:
|
||||
$ref: "#/components/schemas/Pet"
|
||||
"""
|
||||
resp.media = PetSchema().dump({"name": "little orange"})
|
||||
|
||||
r = api.requests.get("http://;/schema.yml")
|
||||
dump = yaml.safe_load(r.content)
|
||||
|
||||
assert dump
|
||||
assert dump["openapi"] == "3.0.2"
|
||||
|
||||
|
||||
def test_schema_generation():
|
||||
from marshmallow import Schema, fields
|
||||
|
||||
import responder
|
||||
|
||||
api = responder.API(title="Web Service", openapi="3.0.2")
|
||||
|
||||
@api.schema("Pet")
|
||||
class PetSchema(Schema):
|
||||
name = fields.Str()
|
||||
|
||||
@api.route("/")
|
||||
def route(req, resp):
|
||||
"""A cute furry animal endpoint.
|
||||
---
|
||||
get:
|
||||
description: Get a random pet
|
||||
responses:
|
||||
200:
|
||||
description: A pet to be returned
|
||||
schema:
|
||||
$ref: "#/components/schemas/Pet"
|
||||
"""
|
||||
resp.media = PetSchema().dump({"name": "little orange"})
|
||||
|
||||
r = api.requests.get("http://;/schema.yml")
|
||||
dump = yaml.safe_load(r.content)
|
||||
|
||||
assert dump
|
||||
assert dump["openapi"] == "3.0.2"
|
||||
|
||||
|
||||
def test_documentation_explicit():
|
||||
import marshmallow
|
||||
|
||||
import responder
|
||||
from responder.ext.schema import OpenAPISchema as OpenAPISchema
|
||||
|
||||
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(allowed_hosts=["testserver", ";"])
|
||||
|
||||
schema = OpenAPISchema(
|
||||
app=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_,
|
||||
)
|
||||
|
||||
@schema.schema("Pet")
|
||||
class PetSchema(marshmallow.Schema):
|
||||
name = marshmallow.fields.Str()
|
||||
|
||||
@api.route("/")
|
||||
def route(req, resp):
|
||||
"""A cute furry animal endpoint.
|
||||
---
|
||||
get:
|
||||
description: Get a random pet
|
||||
responses:
|
||||
200:
|
||||
description: A pet to be returned
|
||||
schema:
|
||||
$ref: "#/components/schemas/Pet"
|
||||
"""
|
||||
resp.media = PetSchema().dump({"name": "little orange"})
|
||||
|
||||
r = api.requests.get("/docs")
|
||||
assert "html" in r.text
|
||||
|
||||
|
||||
def test_documentation():
|
||||
from marshmallow import Schema, fields
|
||||
|
||||
import responder
|
||||
|
||||
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_,
|
||||
allowed_hosts=["testserver", ";"],
|
||||
)
|
||||
|
||||
@api.schema("Pet")
|
||||
class PetSchema(Schema):
|
||||
name = fields.Str()
|
||||
|
||||
@api.route("/")
|
||||
def route(req, resp):
|
||||
"""A cute furry animal endpoint.
|
||||
---
|
||||
get:
|
||||
description: Get a random pet
|
||||
responses:
|
||||
200:
|
||||
description: A pet to be returned
|
||||
schema:
|
||||
$ref: "#/components/schemas/Pet"
|
||||
"""
|
||||
resp.media = PetSchema().dump({"name": "little orange"})
|
||||
|
||||
r = api.requests.get("/docs")
|
||||
assert "html" in r.text
|
||||
|
||||
|
||||
def test_mount_wsgi_app(api, flask):
|
||||
@api.route("/")
|
||||
def hello(req, resp):
|
||||
resp.text = "hello"
|
||||
|
||||
api.mount("/flask", flask)
|
||||
|
||||
r = api.requests.get("http://;/flask")
|
||||
assert r.status_code < 300
|
||||
|
||||
|
||||
def test_async_class_based_views(api):
|
||||
@api.route("/")
|
||||
class Resource:
|
||||
async def on_post(self, req, resp):
|
||||
resp.text = await req.text
|
||||
|
||||
data = "frame"
|
||||
r = api.requests.post(api.url_for(Resource), data=data)
|
||||
assert r.text == data
|
||||
|
||||
|
||||
def test_cookies(api):
|
||||
@api.route("/")
|
||||
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": {"hello": "world", "sent": "true"}}
|
||||
|
||||
|
||||
@pytest.mark.xfail
|
||||
def test_sessions(api):
|
||||
@api.route("/")
|
||||
def view(req, resp):
|
||||
resp.session["hello"] = "world"
|
||||
resp.media = resp.session
|
||||
|
||||
r = api.requests.get(api.url_for(view))
|
||||
assert api.session_cookie in r.cookies
|
||||
|
||||
r = api.requests.get(api.url_for(view))
|
||||
assert (
|
||||
r.cookies[api.session_cookie] == '{"hello": "world"}.r3EB04hEEyLYIJaAXCEq3d4YEbs'
|
||||
)
|
||||
assert r.json() == {"hello": "world"}
|
||||
|
||||
|
||||
def test_template_string_rendering(api):
|
||||
@api.route("/")
|
||||
def view(req, resp):
|
||||
resp.content = api.template_string("{{ var }}", var="hello")
|
||||
|
||||
r = api.requests.get(api.url_for(view))
|
||||
assert r.text == "hello"
|
||||
|
||||
|
||||
def test_template_rendering(template_path):
|
||||
api = responder.API(templates_dir=template_path.dirpath())
|
||||
|
||||
@api.route("/")
|
||||
def view(req, resp):
|
||||
resp.content = api.template(template_path.basename, var="hello")
|
||||
|
||||
r = api.requests.get(api.url_for(view))
|
||||
assert r.text == "hello"
|
||||
|
||||
|
||||
def test_template(api, template_path):
|
||||
templates = Templates(directory=template_path.dirpath())
|
||||
|
||||
@api.route("/{var}/")
|
||||
def view(req, resp, var):
|
||||
resp.html = templates.render(template_path.basename, var=var)
|
||||
|
||||
r = api.requests.get("/test/")
|
||||
assert r.text == "test"
|
||||
|
||||
|
||||
def test_template_async(api, template_path):
|
||||
templates = Templates(directory=template_path.dirpath(), enable_async=True)
|
||||
|
||||
@api.route("/{var}/async")
|
||||
async def view_async(req, resp, var):
|
||||
resp.html = await templates.render_async(template_path.basename, var=var)
|
||||
|
||||
r = api.requests.get("/test/async")
|
||||
assert r.text == "test"
|
||||
|
||||
|
||||
def test_file_uploads(api):
|
||||
@api.route("/")
|
||||
async def upload(req, resp):
|
||||
files = await req.media("files")
|
||||
result = {}
|
||||
result["hello"] = files["hello"]["content"].decode("utf-8")
|
||||
# result["not-a-file"] = files["not-a-file"].decode("utf-8")
|
||||
resp.media = {"files": result}
|
||||
|
||||
# # world = io.StringIO("world")
|
||||
|
||||
# data = {"hello": ("hello.txt", world, "text/plain"), "not-a-file": b"data only"}
|
||||
# r = api.requests.post(api.url_for(upload), files=data)
|
||||
# assert r.json() == {"files": {"hello": "world", "not-a-file": "data only"}}
|
||||
|
||||
|
||||
def test_500(api):
|
||||
@api.route("/")
|
||||
def view(req, resp):
|
||||
raise ValueError
|
||||
|
||||
dumb_client = responder.api.TestClient(
|
||||
api, base_url="http://;", raise_server_exceptions=False
|
||||
)
|
||||
r = dumb_client.get(api.url_for(view))
|
||||
assert r.status_code >= 300
|
||||
assert r.status_code == responder.status_codes.HTTP_500
|
||||
|
||||
|
||||
def test_404(api):
|
||||
r = api.requests.get("/foo")
|
||||
|
||||
assert r.status_code == responder.status_codes.HTTP_404
|
||||
|
||||
|
||||
def test_websockets_text(api):
|
||||
payload = "Hello via websocket!"
|
||||
|
||||
@api.route("/ws", websocket=True)
|
||||
async def websocket(ws):
|
||||
await ws.accept()
|
||||
await ws.send_text(payload)
|
||||
await ws.close()
|
||||
|
||||
client = StarletteTestClient(api)
|
||||
with client.websocket_connect("ws://;/ws") as ws:
|
||||
data = ws.receive_text()
|
||||
assert data == payload
|
||||
|
||||
|
||||
def test_websockets_bytes(api):
|
||||
payload = b"Hello via websocket!"
|
||||
|
||||
@api.route("/ws", websocket=True)
|
||||
async def websocket(ws):
|
||||
await ws.accept()
|
||||
await ws.send_bytes(payload)
|
||||
await ws.close()
|
||||
|
||||
client = StarletteTestClient(api)
|
||||
with client.websocket_connect("ws://;/ws") as ws:
|
||||
data = ws.receive_bytes()
|
||||
assert data == payload
|
||||
|
||||
|
||||
def test_websockets_json(api):
|
||||
payload = {"Hello": "via websocket!"}
|
||||
|
||||
@api.route("/ws", websocket=True)
|
||||
async def websocket(ws):
|
||||
await ws.accept()
|
||||
await ws.send_json(payload)
|
||||
await ws.close()
|
||||
|
||||
client = StarletteTestClient(api)
|
||||
with client.websocket_connect("ws://;/ws") as ws:
|
||||
data = ws.receive_json()
|
||||
assert data == payload
|
||||
|
||||
|
||||
def test_before_websockets(api):
|
||||
payload = {"Hello": "via websocket!"}
|
||||
|
||||
@api.route("/ws", websocket=True)
|
||||
async def websocket(ws):
|
||||
await ws.send_json(payload)
|
||||
await ws.close()
|
||||
|
||||
@api.before_request(websocket=True)
|
||||
async def before_request(ws):
|
||||
await ws.accept()
|
||||
await ws.send_json({"before": "request"})
|
||||
|
||||
client = StarletteTestClient(api)
|
||||
with client.websocket_connect("ws://;/ws") as ws:
|
||||
data = ws.receive_json()
|
||||
assert data == {"before": "request"}
|
||||
data = ws.receive_json()
|
||||
assert data == payload
|
||||
|
||||
|
||||
def test_startup(api):
|
||||
who = [None]
|
||||
|
||||
@api.route("/{greeting}")
|
||||
async def greet_world(req, resp, *, greeting):
|
||||
resp.text = f"{greeting}, {who[0]}!"
|
||||
|
||||
@api.on_event("startup")
|
||||
async def run_startup():
|
||||
who[0] = "world"
|
||||
|
||||
with api.requests as session:
|
||||
r = session.get("http://;/hello")
|
||||
assert r.text == "hello, world!"
|
||||
|
||||
|
||||
def test_redirects(api, session):
|
||||
@api.route("/2")
|
||||
def two(req, resp):
|
||||
api.redirect(resp, location="/1")
|
||||
|
||||
@api.route("/1")
|
||||
def one(req, resp):
|
||||
resp.text = "redirected"
|
||||
|
||||
assert session.get("/2").url == "http://;/1"
|
||||
|
||||
|
||||
def test_session_thoroughly(api, session):
|
||||
@api.route("/set")
|
||||
def setter(req, resp):
|
||||
resp.session["hello"] = "world"
|
||||
api.redirect(resp, location="/get")
|
||||
|
||||
@api.route("/get")
|
||||
def getter(req, resp):
|
||||
resp.media = {"session": req.session}
|
||||
|
||||
r = session.get(api.url_for(setter))
|
||||
r = session.get(api.url_for(getter))
|
||||
assert r.json() == {"session": {"hello": "world"}}
|
||||
|
||||
|
||||
def test_before_response(api, session):
|
||||
@api.route("/get")
|
||||
def get(req, resp):
|
||||
resp.media = req.session
|
||||
|
||||
@api.route(before_request=True)
|
||||
async def async_before_request(req, resp):
|
||||
resp.headers["x-pizza"] = "1"
|
||||
|
||||
@api.route(before_request=True)
|
||||
def before_request(req, resp):
|
||||
resp.headers["x-pizza"] = "1"
|
||||
|
||||
r = session.get(api.url_for(get))
|
||||
assert "x-pizza" in r.headers
|
||||
|
||||
|
||||
@pytest.mark.parametrize("enable_hsts", [True, False])
|
||||
@pytest.mark.parametrize("cors", [True, False])
|
||||
def test_allowed_hosts(enable_hsts, cors):
|
||||
api = responder.API(
|
||||
allowed_hosts=[";", "tenant.;"], enable_hsts=enable_hsts, cors=cors
|
||||
)
|
||||
|
||||
@api.route("/")
|
||||
def get(req, resp):
|
||||
pass
|
||||
|
||||
# Exact match
|
||||
r = api.requests.get(api.url_for(get))
|
||||
assert r.status_code == 200
|
||||
|
||||
# Reset the session
|
||||
api._session = None
|
||||
r = api.session(base_url="http://tenant.;").get(api.url_for(get))
|
||||
assert r.status_code == 200
|
||||
|
||||
# Reset the session
|
||||
api._session = None
|
||||
r = api.session(base_url="http://unkownhost").get(api.url_for(get))
|
||||
assert r.status_code == 400
|
||||
|
||||
# Reset the session
|
||||
api._session = None
|
||||
r = api.session(base_url="http://unkown_tenant.;").get(api.url_for(get))
|
||||
assert r.status_code == 400
|
||||
|
||||
api = responder.API(allowed_hosts=["*.;"])
|
||||
|
||||
@api.route("/")
|
||||
def get(req, resp):
|
||||
pass
|
||||
|
||||
# Wildcard domains
|
||||
# Using http://;
|
||||
r = api.requests.get(api.url_for(get))
|
||||
assert r.status_code == 400
|
||||
|
||||
# Reset the session
|
||||
api._session = None
|
||||
r = api.session(base_url="http://tenant1.;").get(api.url_for(get))
|
||||
assert r.status_code == 200
|
||||
|
||||
# Reset the session
|
||||
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) # noqa: S311
|
||||
# :3
|
||||
ext = random.choices(string.ascii_letters, k=2) # noqa: S311
|
||||
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
|
||||
|
||||
|
||||
@pytest.mark.parametrize("static_route", [None, "/static", "/custom/static/route"])
|
||||
def test_staticfiles(tmpdir, static_route):
|
||||
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), static_route=static_route)
|
||||
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_none_dir(tmpdir):
|
||||
api = responder.API(static_dir=None)
|
||||
session = api.session()
|
||||
|
||||
static_dir = tmpdir.mkdir("static")
|
||||
|
||||
asset = create_asset(static_dir)
|
||||
|
||||
static_route = api.static_route
|
||||
|
||||
# ok
|
||||
r = session.get(f"{static_route}/{asset.basename}")
|
||||
assert r.status_code == api.status_codes.HTTP_404
|
||||
|
||||
# dir listing
|
||||
r = session.get(f"{static_route}")
|
||||
assert r.status_code == api.status_codes.HTTP_404
|
||||
|
||||
# SPA
|
||||
with pytest.raises(Exception): # noqa: B017
|
||||
api.add_route("/spa", static=True)
|
||||
|
||||
|
||||
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
|
||||
|
||||
resp.stream(foo)
|
||||
|
||||
with pytest.raises(AssertionError):
|
||||
|
||||
async def foo():
|
||||
pass
|
||||
|
||||
resp.stream(foo)
|
||||
|
||||
with pytest.raises(AssertionError):
|
||||
|
||||
def foo():
|
||||
yield "oopsie"
|
||||
|
||||
resp.stream(foo)
|
||||
|
||||
|
||||
def test_empty_req_text(api):
|
||||
content = "It's working"
|
||||
|
||||
@api.route("/")
|
||||
async def home(req, resp):
|
||||
await req.text
|
||||
resp.text = content
|
||||
|
||||
r = api.requests.post("/")
|
||||
assert r.text == content
|
||||
|
||||
def test_api_request_state(api, url):
|
||||
class StateMiddleware(BaseHTTPMiddleware):
|
||||
async def dispatch(self, request, call_next):
|
||||
request.state.test1 = 42
|
||||
request.state.test2 = "Foo"
|
||||
|
||||
return await call_next(request)
|
||||
|
||||
api.add_middleware(StateMiddleware)
|
||||
|
||||
@api.route("/")
|
||||
def home(req, resp):
|
||||
resp.text = "{}_{}".format(req.state.test2, req.state.test1)
|
||||
|
||||
assert api.requests.get(url("/")).text == "Foo_42"
|
||||
|
||||
|
||||
def test_path_matches_route(api):
|
||||
@api.route("/hello")
|
||||
def home(req, resp):
|
||||
resp.text = "hello world!"
|
||||
|
||||
route = api.path_matches_route({"type": "http", "path": "/hello"})
|
||||
assert route.endpoint_name == "home"
|
||||
|
||||
assert not api.path_matches_route({"type": "http", "path": "/foo"})
|
||||
|
||||
|
||||
def test_route_without_endpoint(api):
|
||||
# test that a route without endpoint gets a default static response
|
||||
api.add_route("/")
|
||||
route = api.router.routes[0]
|
||||
assert route.endpoint_name == "_static_response"
|
||||
@@ -0,0 +1,68 @@
|
||||
import pytest
|
||||
|
||||
from responder import status_codes
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"status_code, expected",
|
||||
[
|
||||
pytest.param(101, True, id="Normal 101"),
|
||||
pytest.param(199, True, id="Not actual status code but within 100"),
|
||||
pytest.param(0, False, id="Zero case (below 100)"),
|
||||
pytest.param(200, False, id="Above 100"),
|
||||
],
|
||||
)
|
||||
def test_is_100(status_code, expected):
|
||||
assert status_codes.is_100(status_code) is expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"status_code, expected",
|
||||
[
|
||||
pytest.param(201, True, id="Normal 201"),
|
||||
pytest.param(299, True, id="Not actual status code but within 200"),
|
||||
pytest.param(0, False, id="Zero case (below 200)"),
|
||||
pytest.param(300, False, id="Above 200"),
|
||||
],
|
||||
)
|
||||
def test_is_200(status_code, expected):
|
||||
assert status_codes.is_200(status_code) is expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"status_code, expected",
|
||||
[
|
||||
pytest.param(301, True, id="Normal 301"),
|
||||
pytest.param(399, True, id="Not actual status code but within 300"),
|
||||
pytest.param(0, False, id="Zero case (below 300)"),
|
||||
pytest.param(400, False, id="Above 300"),
|
||||
],
|
||||
)
|
||||
def test_is_300(status_code, expected):
|
||||
assert status_codes.is_300(status_code) is expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"status_code, expected",
|
||||
[
|
||||
pytest.param(401, True, id="Normal 401"),
|
||||
pytest.param(499, True, id="Not actual status code but within 400"),
|
||||
pytest.param(0, False, id="Zero case (below 400)"),
|
||||
pytest.param(500, False, id="Above 400"),
|
||||
],
|
||||
)
|
||||
def test_is_400(status_code, expected):
|
||||
assert status_codes.is_400(status_code) is expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"status_code, expected",
|
||||
[
|
||||
pytest.param(501, True, id="Normal 501"),
|
||||
pytest.param(599, True, id="Not actual status code but within 500"),
|
||||
pytest.param(0, False, id="Zero case (below 500)"),
|
||||
pytest.param(600, False, id="Above 500"),
|
||||
],
|
||||
)
|
||||
def test_is_500(status_code, expected):
|
||||
assert status_codes.is_500(status_code) is expected
|
||||
Reference in New Issue
Block a user