mirror of
https://github.com/kennethreitz/responder.git
synced 2026-06-05 23:00:17 +00:00
Compare commits
91 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 | |||
| f46ac08cff |
@@ -1,3 +1,18 @@
|
||||
# v1.1.1
|
||||
- Run sync views in a threadpoolexecutor.
|
||||
|
||||
# v1.1.0
|
||||
- Support for `before_request`.
|
||||
|
||||
# v1.0.4
|
||||
- Potential bufix for cookies.
|
||||
|
||||
# v1.0.3
|
||||
- Bugfix for redirects.
|
||||
|
||||
# v1.0.2
|
||||
- Improvement for static file hosting.
|
||||
|
||||
# v1.0.1
|
||||
- Improve cors configuration settings.
|
||||
|
||||
|
||||
Generated
+117
-67
@@ -32,10 +32,10 @@
|
||||
},
|
||||
"apispec": {
|
||||
"hashes": [
|
||||
"sha256:c2e6ac6471aaf7c6ec6d12714821898910c6b3c87c189de9a2e3754786b86ada",
|
||||
"sha256:fa7dfa8a292bae9b1e70c44a50bf61901805821726c5b804568c9f2501f57ebb"
|
||||
"sha256:50b1cb8f0dc12db00a53e748b27ae601efdc210f1cb4358729173337b2e47354",
|
||||
"sha256:648934f67aefa386cdde5d1d38ceb1c9fe9157c59824a6fb5499409a70dabab4"
|
||||
],
|
||||
"version": "==1.0.0b3"
|
||||
"version": "==1.0.0b5"
|
||||
},
|
||||
"apistar": {
|
||||
"hashes": [
|
||||
@@ -126,9 +126,10 @@
|
||||
},
|
||||
"itsdangerous": {
|
||||
"hashes": [
|
||||
"sha256:cbb3fcf8d3e33df861709ecaf89d9e6629cff0a217bc2848f1b41cd30d360519"
|
||||
"sha256:321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19",
|
||||
"sha256:b12271b2047cb23eeb98c8b5622e2e5c5e9abd9784a153e9d8ef9cb4dd09d749"
|
||||
],
|
||||
"version": "==0.24"
|
||||
"version": "==1.1.0"
|
||||
},
|
||||
"jinja2": {
|
||||
"hashes": [
|
||||
@@ -139,16 +140,43 @@
|
||||
},
|
||||
"markupsafe": {
|
||||
"hashes": [
|
||||
"sha256:a6be69091dac236ea9c6bc7d012beab42010fa914c459791d627dad4910eb665"
|
||||
"sha256:048ef924c1623740e70204aa7143ec592504045ae4429b59c30054cb31e3c432",
|
||||
"sha256:130f844e7f5bdd8e9f3f42e7102ef1d49b2e6fdf0d7526df3f87281a532d8c8b",
|
||||
"sha256:19f637c2ac5ae9da8bfd98cef74d64b7e1bb8a63038a3505cd182c3fac5eb4d9",
|
||||
"sha256:1b8a7a87ad1b92bd887568ce54b23565f3fd7018c4180136e1cf412b405a47af",
|
||||
"sha256:1c25694ca680b6919de53a4bb3bdd0602beafc63ff001fea2f2fc16ec3a11834",
|
||||
"sha256:1f19ef5d3908110e1e891deefb5586aae1b49a7440db952454b4e281b41620cd",
|
||||
"sha256:1fa6058938190ebe8290e5cae6c351e14e7bb44505c4a7624555ce57fbbeba0d",
|
||||
"sha256:31cbb1359e8c25f9f48e156e59e2eaad51cd5242c05ed18a8de6dbe85184e4b7",
|
||||
"sha256:3e835d8841ae7863f64e40e19477f7eb398674da6a47f09871673742531e6f4b",
|
||||
"sha256:4e97332c9ce444b0c2c38dd22ddc61c743eb208d916e4265a2a3b575bdccb1d3",
|
||||
"sha256:525396ee324ee2da82919f2ee9c9e73b012f23e7640131dd1b53a90206a0f09c",
|
||||
"sha256:52b07fbc32032c21ad4ab060fec137b76eb804c4b9a1c7c7dc562549306afad2",
|
||||
"sha256:52ccb45e77a1085ec5461cde794e1aa037df79f473cbc69b974e73940655c8d7",
|
||||
"sha256:5c3fbebd7de20ce93103cb3183b47671f2885307df4a17a0ad56a1dd51273d36",
|
||||
"sha256:5e5851969aea17660e55f6a3be00037a25b96a9b44d2083651812c99d53b14d1",
|
||||
"sha256:5edfa27b2d3eefa2210fb2f5d539fbed81722b49f083b2c6566455eb7422fd7e",
|
||||
"sha256:7d263e5770efddf465a9e31b78362d84d015cc894ca2c131901a4445eaa61ee1",
|
||||
"sha256:83381342bfc22b3c8c06f2dd93a505413888694302de25add756254beee8449c",
|
||||
"sha256:857eebb2c1dc60e4219ec8e98dfa19553dae33608237e107db9c6078b1167856",
|
||||
"sha256:98e439297f78fca3a6169fd330fbe88d78b3bb72f967ad9961bcac0d7fdd1550",
|
||||
"sha256:bf54103892a83c64db58125b3f2a43df6d2cb2d28889f14c78519394feb41492",
|
||||
"sha256:d9ac82be533394d341b41d78aca7ed0e0f4ba5a2231602e2f05aa87f25c51672",
|
||||
"sha256:e982fe07ede9fada6ff6705af70514a52beb1b2c3d25d4e873e82114cf3c5401",
|
||||
"sha256:edce2ea7f3dfc981c4ddc97add8a61381d9642dc3273737e756517cc03e84dd6",
|
||||
"sha256:efdc45ef1afc238db84cb4963aa689c0408912a0239b0721cb172b4016eb31d6",
|
||||
"sha256:f137c02498f8b935892d5c0172560d7ab54bc45039de8805075e19079c639a9c",
|
||||
"sha256:f82e347a72f955b7017a39708a3667f106e6ad4d10b25f237396a7115d8ed5fd",
|
||||
"sha256:fb7c206e01ad85ce57feeaaa0bf784b97fa3cad0d4a5737bc5295785f5c613a1"
|
||||
],
|
||||
"version": "==1.0"
|
||||
"version": "==1.1.0"
|
||||
},
|
||||
"marshmallow": {
|
||||
"hashes": [
|
||||
"sha256:5e0053c86e3abaa72a03bbe0021ec97270c13fd6400b682eb1aeaf24b871bc8a",
|
||||
"sha256:81884e930c1db72d8b8e3d8d2d090f2f43427e5c11c37f703b29879980491ab6"
|
||||
"sha256:0a96d88418c4e7c50a39a734c4ed3d2a991a37e6b7a8970dbbdb8ccb7f08ecb0",
|
||||
"sha256:5a65e5c7e9b4e050c989e09d7353eeb91d313d39dfcfa6540aa27f39bfb00b4e"
|
||||
],
|
||||
"version": "==3.0.0b19"
|
||||
"version": "==3.0.0b20"
|
||||
},
|
||||
"parse": {
|
||||
"hashes": [
|
||||
@@ -220,48 +248,48 @@
|
||||
},
|
||||
"starlette": {
|
||||
"hashes": [
|
||||
"sha256:eac0f6cab6b48846a0c1af16615430ae0e7a95f669ee0841a7e2f242d51d8935"
|
||||
"sha256:418f1f333529f9b9d5a7bffdaef7df43623b648b5c088ae5be1301c112435641"
|
||||
],
|
||||
"version": "==0.5.5"
|
||||
"version": "==0.7.4"
|
||||
},
|
||||
"urllib3": {
|
||||
"hashes": [
|
||||
"sha256:41c3db2fc01e5b907288010dec72f9d0a74e37d6994e6eb56849f59fea2265ae",
|
||||
"sha256:8819bba37a02d143296a4d032373c4dd4aca11f6d4c9973335ca75f9c8475f59"
|
||||
"sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39",
|
||||
"sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22"
|
||||
],
|
||||
"version": "==1.24"
|
||||
"version": "==1.24.1"
|
||||
},
|
||||
"uvicorn": {
|
||||
"hashes": [
|
||||
"sha256:e2b742fdaa0b52f4aac92fd2c078e7f1f17d11322bb3efb09d341d5c6998b4b5"
|
||||
"sha256:38ae2c2d6438438aed08999bb9f7fb0bb12ac99aa9831b30a6045aab88f2cec1"
|
||||
],
|
||||
"version": "==0.3.16"
|
||||
"version": "==0.3.20"
|
||||
},
|
||||
"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"
|
||||
"sha256:04b42a1b57096ffa5627d6a78ea1ff7fad3bc2c0331ffc17bc32a4024da7fea0",
|
||||
"sha256:08e3c3e0535befa4f0c4443824496c03ecc25062debbcf895874f8a0b4c97c9f",
|
||||
"sha256:10d89d4326045bf5e15e83e9867c85d686b612822e4d8f149cf4840aab5f46e0",
|
||||
"sha256:232fac8a1978fc1dead4b1c2fa27c7756750fb393eb4ac52f6bc87ba7242b2fa",
|
||||
"sha256:4bf4c8097440eff22bc78ec76fe2a865a6e658b6977a504679aaf08f02c121da",
|
||||
"sha256:51642ea3a00772d1e48fb0c492f0d3ae3b6474f34d20eca005a83f8c9c06c561",
|
||||
"sha256:55d86102282a636e195dad68aaaf85b81d0bef449d7e2ef2ff79ac450bb25d53",
|
||||
"sha256:564d2675682bd497b59907d2205031acbf7d3fadf8c763b689b9ede20300b215",
|
||||
"sha256:5d13bf5197a92149dc0badcc2b699267ff65a867029f465accfca8abab95f412",
|
||||
"sha256:5eda665f6789edb9b57b57a159b9c55482cbe5b046d7db458948370554b16439",
|
||||
"sha256:5edb2524d4032be4564c65dc4f9d01e79fe8fad5f966e5b552f4e5164fef0885",
|
||||
"sha256:79691794288bc51e2a3b8de2bc0272ca8355d0b8503077ea57c0716e840ebaef",
|
||||
"sha256:7fcc8681e9981b9b511cdee7c580d5b005f3bb86b65bde2188e04a29f1d63317",
|
||||
"sha256:8e447e05ec88b1b408a4c9cde85aa6f4b04f06aa874b9f0b8e8319faf51b1fee",
|
||||
"sha256:90ea6b3e7787620bb295a4ae050d2811c807d65b1486749414f78cfd6fb61489",
|
||||
"sha256:9e13239952694b8b831088431d15f771beace10edfcf9ef230cefea14f18508f",
|
||||
"sha256:d40f081187f7b54d7a99d8a5c782eaa4edc335a057aa54c85059272ed826dc09",
|
||||
"sha256:e1df1a58ed2468c7b7ce9a2f9752a32ad08eac2bcd56318625c3647c2cd2da6f",
|
||||
"sha256:e98d0cec437097f09c7834a11c69d79fe6241729b23f656cfc227e93294fc242",
|
||||
"sha256:f8d59627702d2ff27cb495ca1abdea8bd8d581de425c56e93bff6517134e0a9b",
|
||||
"sha256:fc30cdf2e949a2225b012a7911d1d031df3d23e99b7eda7dfc982dc4a860dae9"
|
||||
],
|
||||
"version": "==6.0"
|
||||
"version": "==7.0"
|
||||
},
|
||||
"whitenoise": {
|
||||
"hashes": [
|
||||
@@ -407,12 +435,6 @@
|
||||
"index": "pypi",
|
||||
"version": "==1.0.2"
|
||||
},
|
||||
"future": {
|
||||
"hashes": [
|
||||
"sha256:eb6d4df04f1fb538c99f69c9a28b255d1ee4e825d479b9c62fc38c0cf38065a4"
|
||||
],
|
||||
"version": "==0.17.0"
|
||||
},
|
||||
"idna": {
|
||||
"hashes": [
|
||||
"sha256:156a6814fb5ac1fc6850fb002e0852d56c0c8d2531923a51032d1b70760e186e",
|
||||
@@ -429,9 +451,10 @@
|
||||
},
|
||||
"itsdangerous": {
|
||||
"hashes": [
|
||||
"sha256:cbb3fcf8d3e33df861709ecaf89d9e6629cff0a217bc2848f1b41cd30d360519"
|
||||
"sha256:321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19",
|
||||
"sha256:b12271b2047cb23eeb98c8b5622e2e5c5e9abd9784a153e9d8ef9cb4dd09d749"
|
||||
],
|
||||
"version": "==0.24"
|
||||
"version": "==1.1.0"
|
||||
},
|
||||
"jinja2": {
|
||||
"hashes": [
|
||||
@@ -442,16 +465,43 @@
|
||||
},
|
||||
"markupsafe": {
|
||||
"hashes": [
|
||||
"sha256:a6be69091dac236ea9c6bc7d012beab42010fa914c459791d627dad4910eb665"
|
||||
"sha256:048ef924c1623740e70204aa7143ec592504045ae4429b59c30054cb31e3c432",
|
||||
"sha256:130f844e7f5bdd8e9f3f42e7102ef1d49b2e6fdf0d7526df3f87281a532d8c8b",
|
||||
"sha256:19f637c2ac5ae9da8bfd98cef74d64b7e1bb8a63038a3505cd182c3fac5eb4d9",
|
||||
"sha256:1b8a7a87ad1b92bd887568ce54b23565f3fd7018c4180136e1cf412b405a47af",
|
||||
"sha256:1c25694ca680b6919de53a4bb3bdd0602beafc63ff001fea2f2fc16ec3a11834",
|
||||
"sha256:1f19ef5d3908110e1e891deefb5586aae1b49a7440db952454b4e281b41620cd",
|
||||
"sha256:1fa6058938190ebe8290e5cae6c351e14e7bb44505c4a7624555ce57fbbeba0d",
|
||||
"sha256:31cbb1359e8c25f9f48e156e59e2eaad51cd5242c05ed18a8de6dbe85184e4b7",
|
||||
"sha256:3e835d8841ae7863f64e40e19477f7eb398674da6a47f09871673742531e6f4b",
|
||||
"sha256:4e97332c9ce444b0c2c38dd22ddc61c743eb208d916e4265a2a3b575bdccb1d3",
|
||||
"sha256:525396ee324ee2da82919f2ee9c9e73b012f23e7640131dd1b53a90206a0f09c",
|
||||
"sha256:52b07fbc32032c21ad4ab060fec137b76eb804c4b9a1c7c7dc562549306afad2",
|
||||
"sha256:52ccb45e77a1085ec5461cde794e1aa037df79f473cbc69b974e73940655c8d7",
|
||||
"sha256:5c3fbebd7de20ce93103cb3183b47671f2885307df4a17a0ad56a1dd51273d36",
|
||||
"sha256:5e5851969aea17660e55f6a3be00037a25b96a9b44d2083651812c99d53b14d1",
|
||||
"sha256:5edfa27b2d3eefa2210fb2f5d539fbed81722b49f083b2c6566455eb7422fd7e",
|
||||
"sha256:7d263e5770efddf465a9e31b78362d84d015cc894ca2c131901a4445eaa61ee1",
|
||||
"sha256:83381342bfc22b3c8c06f2dd93a505413888694302de25add756254beee8449c",
|
||||
"sha256:857eebb2c1dc60e4219ec8e98dfa19553dae33608237e107db9c6078b1167856",
|
||||
"sha256:98e439297f78fca3a6169fd330fbe88d78b3bb72f967ad9961bcac0d7fdd1550",
|
||||
"sha256:bf54103892a83c64db58125b3f2a43df6d2cb2d28889f14c78519394feb41492",
|
||||
"sha256:d9ac82be533394d341b41d78aca7ed0e0f4ba5a2231602e2f05aa87f25c51672",
|
||||
"sha256:e982fe07ede9fada6ff6705af70514a52beb1b2c3d25d4e873e82114cf3c5401",
|
||||
"sha256:edce2ea7f3dfc981c4ddc97add8a61381d9642dc3273737e756517cc03e84dd6",
|
||||
"sha256:efdc45ef1afc238db84cb4963aa689c0408912a0239b0721cb172b4016eb31d6",
|
||||
"sha256:f137c02498f8b935892d5c0172560d7ab54bc45039de8805075e19079c639a9c",
|
||||
"sha256:f82e347a72f955b7017a39708a3667f106e6ad4d10b25f237396a7115d8ed5fd",
|
||||
"sha256:fb7c206e01ad85ce57feeaaa0bf784b97fa3cad0d4a5737bc5295785f5c613a1"
|
||||
],
|
||||
"version": "==1.0"
|
||||
"version": "==1.1.0"
|
||||
},
|
||||
"marshmallow": {
|
||||
"hashes": [
|
||||
"sha256:5e0053c86e3abaa72a03bbe0021ec97270c13fd6400b682eb1aeaf24b871bc8a",
|
||||
"sha256:81884e930c1db72d8b8e3d8d2d090f2f43427e5c11c37f703b29879980491ab6"
|
||||
"sha256:0a96d88418c4e7c50a39a734c4ed3d2a991a37e6b7a8970dbbdb8ccb7f08ecb0",
|
||||
"sha256:5a65e5c7e9b4e050c989e09d7353eeb91d313d39dfcfa6540aa27f39bfb00b4e"
|
||||
],
|
||||
"version": "==3.0.0b19"
|
||||
"version": "==3.0.0b20"
|
||||
},
|
||||
"mccabe": {
|
||||
"hashes": [
|
||||
@@ -519,18 +569,18 @@
|
||||
},
|
||||
"pyparsing": {
|
||||
"hashes": [
|
||||
"sha256:bc6c7146b91af3f567cf6daeaec360bc07d45ffec4cf5353f4d7a208ce7ca30a",
|
||||
"sha256:d29593d8ebe7b57d6967b62494f8c72b03ac0262b1eed63826c6f788b3606401"
|
||||
"sha256:40856e74d4987de5d01761a22d1621ae1c7f8774585acae358aa5c5936c6c90b",
|
||||
"sha256:f353aab21fd474459d97b709e527b5571314ee5f067441dc9f88e33eecd96592"
|
||||
],
|
||||
"version": "==2.2.2"
|
||||
"version": "==2.3.0"
|
||||
},
|
||||
"pytest": {
|
||||
"hashes": [
|
||||
"sha256:212be78a6fa5352c392738a49b18f74ae9aeec1040f47c81cadbfd8d1233c310",
|
||||
"sha256:6f6c1efc8d0ccc21f8f6c34d8330baca883cf109b66b3df954b0a117e5528fb4"
|
||||
"sha256:630ff1dbe04f469ee78faa5660f712e58b953da7df22ea5d828c9012e134da43",
|
||||
"sha256:a2b5232735dd0b736cbea9c0f09e5070d78fcaba2823a4f6f09d9a81bd19415c"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==3.9.2"
|
||||
"version": "==3.10.0"
|
||||
},
|
||||
"pytest-cov": {
|
||||
"hashes": [
|
||||
@@ -542,17 +592,17 @@
|
||||
},
|
||||
"pytz": {
|
||||
"hashes": [
|
||||
"sha256:642253af8eae734d1509fc6ac9c1aee5e5b69d76392660889979b9870610a46b",
|
||||
"sha256:91e3ccf2c344ffaa6defba1ce7f38f97026943f675b7703f44789768e4cb0ece"
|
||||
"sha256:31cb35c89bd7d333cd32c5f278fca91b523b0834369e757f4c5641ea252236ca",
|
||||
"sha256:8e0f8568c118d3077b46be7d654cc8167fa916092e28320cde048e54bfc9f1e6"
|
||||
],
|
||||
"version": "==2018.6"
|
||||
"version": "==2018.7"
|
||||
},
|
||||
"readme-renderer": {
|
||||
"hashes": [
|
||||
"sha256:219a02f5359b6631f5ab952f6906c6c105bdd8bc4bf19c1ec5ee8bd9ea2dc1eb",
|
||||
"sha256:f8f122ad9fd6d138337531379575a01a0b6ca70aedca78f094cb833da38c8c0c"
|
||||
"sha256:bb16f55b259f27f75f640acf5e00cf897845a8b3e4731b5c1a436e4b8529202f",
|
||||
"sha256:c8532b79afc0375a85f10433eca157d6b50f7d6990f337fa498c96cd4bfc203d"
|
||||
],
|
||||
"version": "==23.0"
|
||||
"version": "==24.0"
|
||||
},
|
||||
"requests": {
|
||||
"hashes": [
|
||||
@@ -621,10 +671,10 @@
|
||||
},
|
||||
"urllib3": {
|
||||
"hashes": [
|
||||
"sha256:41c3db2fc01e5b907288010dec72f9d0a74e37d6994e6eb56849f59fea2265ae",
|
||||
"sha256:8819bba37a02d143296a4d032373c4dd4aca11f6d4c9973335ca75f9c8475f59"
|
||||
"sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39",
|
||||
"sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22"
|
||||
],
|
||||
"version": "==1.24"
|
||||
"version": "==1.24.1"
|
||||
},
|
||||
"webencodings": {
|
||||
"hashes": [
|
||||
|
||||
@@ -7,20 +7,8 @@
|
||||
[](https://pypi.org/project/responder/)
|
||||
[](https://github.com/kennethreitz/responder/graphs/contributors)
|
||||
|
||||
[](http://python-responder.org/)
|
||||
[](http://python-responder.org/)
|
||||
|
||||
```python
|
||||
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()
|
||||
```
|
||||
|
||||
Powered by [Starlette](https://www.starlette.io/). That `async` declaration is optional. [View documentation](http://python-responder.org).
|
||||
|
||||
@@ -35,7 +23,6 @@ This gets you a ASGI app, with a production static files server pre-installed, j
|
||||
|
||||
> "I love that you are exploring new patterns. Go go go!" — Danny Greenfield, author of [Two Scoops of Django]()
|
||||
|
||||
> "Love what I have seen while it's in progress! Many features of Responder are from my wishlist for Flask, and it's even faster and even easier than Flask!" — Luna C.
|
||||
|
||||
## More Examples
|
||||
|
||||
|
||||
@@ -10,10 +10,11 @@ Assuming existing ``api.py`` and ``Pipfile.lock`` containing ``responder``.
|
||||
|
||||
``Dockerfile``::
|
||||
|
||||
from kennethreitz/pipenv
|
||||
|
||||
FROM kennethreitz/pipenv
|
||||
ENV PORT '80'
|
||||
COPY . /app
|
||||
CMD python3 api.py
|
||||
EXPOSE 80
|
||||
|
||||
That's it!
|
||||
|
||||
|
||||
+2
-12
@@ -45,7 +45,7 @@ Features
|
||||
--------
|
||||
|
||||
- A pleasant API, with a single import statement.
|
||||
- Class-based views without inheritence.
|
||||
- Class-based views without inheritance.
|
||||
- ASGI framework, the future of Python web services.
|
||||
- The ability to mount any ASGI / WSGI app at a subroute.
|
||||
- *f-string syntax* route declaration.
|
||||
@@ -80,18 +80,8 @@ Testimonials
|
||||
— Danny Greenfield, author of `Two Scoops of Django`_
|
||||
|
||||
|
||||
..
|
||||
|
||||
|
||||
“The most ambitious crossover event in history.”
|
||||
|
||||
—Pablo Cabezas, `on Tom Christie joining the project`_
|
||||
|
||||
|
||||
.. _APIStar: https://github.com/encode/apistar
|
||||
.. _Django REST Framework: https://www.django-rest-framework.org/
|
||||
.. _Two Scoops of Django:
|
||||
.. _on Tom Christie joining the project: https://twitter.com/pabloteleco/status/1050841098321620992?s=20
|
||||
.. _Two Scoops of Django: https://www.twoscoopspress.com/products/two-scoops-of-django-1-11
|
||||
|
||||
User Guides
|
||||
-----------
|
||||
|
||||
@@ -90,7 +90,7 @@ If you want to set a response header, like ``X-Pizza: 42``, simply modify the ``
|
||||
|
||||
@api.route("/pizza")
|
||||
def pizza_pizza(req, resp):
|
||||
resp.headers['X-Pizza'] = 42
|
||||
resp.headers['X-Pizza'] = '42'
|
||||
|
||||
That's it!
|
||||
|
||||
|
||||
+35
-5
@@ -46,7 +46,7 @@ Serve a GraphQL API::
|
||||
return f"Hello {name}"
|
||||
|
||||
schema = graphene.Schema(query=Query)
|
||||
view = responder.ext.GraphQLView(query=query, api=api)
|
||||
view = responder.ext.GraphQLView(api=api, schema=schema)
|
||||
|
||||
api.add_route("/graph", view)
|
||||
|
||||
@@ -61,7 +61,7 @@ Responder comes with built-in support for OpenAPI / marshmallow::
|
||||
import responder
|
||||
from marshmallow import Schema, fields
|
||||
|
||||
api = responder.API(title="Web Service", version="1.0", openapi="3.0")
|
||||
api = responder.API(title="Web Service", version="1.0", openapi="3.0.0")
|
||||
|
||||
|
||||
@api.schema("Pet")
|
||||
@@ -81,7 +81,7 @@ Responder comes with built-in support for OpenAPI / marshmallow::
|
||||
schema:
|
||||
$ref = "#/components/schemas/Pet"
|
||||
"""
|
||||
resp.media = PetSchema().dump({"name": "little orange"})
|
||||
resp.media = PetSchema().dump({"name": "little orange"}).data
|
||||
|
||||
|
||||
::
|
||||
@@ -112,7 +112,7 @@ Interactive Documentation
|
||||
|
||||
Responder can automatically supply API Documentation for you. Using the example above::
|
||||
|
||||
api = responder.API(title="Web Service", version="1.0", openapi="3.0", docs_route="/docs")
|
||||
api = responder.API(title="Web Service", version="1.0", openapi="3.0.0", docs_route="/docs")
|
||||
|
||||
This will make ``/docs`` render interactive documentation for your API.
|
||||
|
||||
@@ -173,6 +173,17 @@ You can easily read a Request's session data, that can be trusted to have origin
|
||||
|
||||
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.
|
||||
|
||||
Using Requests Test Client
|
||||
--------------------------
|
||||
|
||||
@@ -220,7 +231,7 @@ Want `CORS <https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS/>`_ ?
|
||||
|
||||
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 dictionnary containing the following entries:
|
||||
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'``.
|
||||
@@ -229,3 +240,22 @@ In order to set custom parameters, you need to set the ``cors_params`` argument
|
||||
* ``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=["*"]``.
|
||||
|
||||
@@ -1 +1 @@
|
||||
__version__ = "1.0.1"
|
||||
__version__ = "1.1.2"
|
||||
|
||||
+109
-77
@@ -1,7 +1,9 @@
|
||||
import json
|
||||
import os
|
||||
|
||||
from uuid import uuid4
|
||||
from pathlib import Path
|
||||
from base64 import b64encode
|
||||
|
||||
import apistar
|
||||
import itsdangerous
|
||||
@@ -11,12 +13,12 @@ import yaml
|
||||
from apispec import APISpec, yaml_utils
|
||||
from apispec.ext.marshmallow import MarshmallowPlugin
|
||||
from asgiref.wsgi import WsgiToAsgi
|
||||
from starlette.debug import DebugMiddleware
|
||||
from starlette.exceptions import ExceptionMiddleware
|
||||
from starlette.lifespan import LifespanHandler
|
||||
from starlette.middleware.cors import CORSMiddleware
|
||||
from starlette.middleware.gzip import GZipMiddleware
|
||||
from starlette.middleware.httpsredirect import HTTPSRedirectMiddleware
|
||||
from starlette.middleware.trustedhost import TrustedHostMiddleware
|
||||
from starlette.routing import Router
|
||||
from starlette.staticfiles import StaticFiles
|
||||
from starlette.testclient import TestClient
|
||||
@@ -64,7 +66,10 @@ class API:
|
||||
enable_hsts=False,
|
||||
docs_route=None,
|
||||
cors=False,
|
||||
cors_params=DEFAULT_CORS_PARAMS,
|
||||
allowed_hosts=None,
|
||||
):
|
||||
self.background = BackgroundQueue()
|
||||
|
||||
self.secret_key = secret_key
|
||||
self.title = title
|
||||
@@ -84,14 +89,21 @@ class API:
|
||||
|
||||
self.hsts_enabled = enable_hsts
|
||||
self.cors = cors
|
||||
self.cors_params = DEFAULT_CORS_PARAMS
|
||||
self.cors_params = cors_params
|
||||
|
||||
if not allowed_hosts:
|
||||
# if not debug:
|
||||
# raise RuntimeError(
|
||||
# "You need to specify `allowed_hosts` when debug is set to False"
|
||||
# )
|
||||
allowed_hosts = ["*"]
|
||||
self.allowed_hosts = allowed_hosts
|
||||
|
||||
# Make the static/templates directory if they don't exist.
|
||||
for _dir in (self.static_dir, self.templates_dir):
|
||||
os.makedirs(_dir, exist_ok=True)
|
||||
|
||||
self.whitenoise = WhiteNoise(
|
||||
application=self._default_wsgi_app, index_file=True
|
||||
)
|
||||
self.whitenoise = WhiteNoise(application=self._default_wsgi_app)
|
||||
self.whitenoise.add_files(str(self.static_dir))
|
||||
|
||||
self.whitenoise.add_files(
|
||||
@@ -107,7 +119,6 @@ class API:
|
||||
|
||||
# Cached requests session.
|
||||
self._session = None
|
||||
self.background = BackgroundQueue()
|
||||
|
||||
if self.openapi_version:
|
||||
self.add_route(openapi_route, self.schema_response)
|
||||
@@ -118,11 +129,12 @@ class API:
|
||||
self.default_endpoint = None
|
||||
self.app = self.dispatch
|
||||
self.add_middleware(GZipMiddleware)
|
||||
if debug:
|
||||
self.add_middleware(DebugMiddleware)
|
||||
|
||||
if self.hsts_enabled:
|
||||
self.add_middleware(HTTPSRedirectMiddleware)
|
||||
|
||||
self.add_middleware(TrustedHostMiddleware, allowed_hosts=self.allowed_hosts)
|
||||
|
||||
self.lifespan_handler = LifespanHandler()
|
||||
|
||||
if self.cors:
|
||||
@@ -146,6 +158,15 @@ class API:
|
||||
def _default_wsgi_app(*args, **kwargs):
|
||||
pass
|
||||
|
||||
@property
|
||||
def before_requests(self):
|
||||
def gen():
|
||||
for route in self.routes:
|
||||
if self.routes[route].before_request:
|
||||
yield self.routes[route]
|
||||
|
||||
return [g for g in gen()]
|
||||
|
||||
@property
|
||||
def _apispec(self):
|
||||
spec = APISpec(
|
||||
@@ -160,10 +181,10 @@ class API:
|
||||
operations = yaml_utils.load_operations_from_docstring(
|
||||
self.routes[route].description
|
||||
)
|
||||
spec.add_path(path=route, operations=operations)
|
||||
spec.path(path=route, operations=operations)
|
||||
|
||||
for name, schema in self.schemas.items():
|
||||
spec.definition(name, schema=schema)
|
||||
spec.components.schema(name, schema=schema)
|
||||
|
||||
return spec
|
||||
|
||||
@@ -244,7 +265,7 @@ class API:
|
||||
|
||||
def _prepare_cookies(self, resp):
|
||||
if resp.cookies:
|
||||
header = " ".join([f"{k}={v}" for k, v in resp.cookies.items()])
|
||||
header = " ".join([f"{k}={v};" for k, v in resp.cookies.items()])
|
||||
resp.headers["Set-Cookie"] = header
|
||||
|
||||
@property
|
||||
@@ -254,7 +275,9 @@ class API:
|
||||
def _prepare_session(self, resp):
|
||||
|
||||
if resp.session:
|
||||
data = self._signer.sign(json.dumps(resp.session).encode("utf-8"))
|
||||
data = self._signer.sign(
|
||||
b64encode(json.dumps(resp.session).encode("utf-8"))
|
||||
)
|
||||
resp.cookies[self.session_cookie] = data.decode("utf-8")
|
||||
|
||||
@staticmethod
|
||||
@@ -268,70 +291,16 @@ class API:
|
||||
# Get the route.
|
||||
route = self.path_matches_route(req.url.path)
|
||||
route = self.routes.get(route)
|
||||
|
||||
# Create the response object.
|
||||
cont = False
|
||||
if route:
|
||||
if route.uses_websocket:
|
||||
resp = WebSocket(**options)
|
||||
|
||||
else:
|
||||
resp = models.Response(req=req, formats=self.formats)
|
||||
|
||||
params = route.incoming_matches(req.url.path)
|
||||
|
||||
if route.is_function:
|
||||
try:
|
||||
try:
|
||||
# Run the view.
|
||||
r = route.endpoint(req, resp, **params)
|
||||
# If it's async, await it.
|
||||
if hasattr(r, "cr_running"):
|
||||
await r
|
||||
except TypeError as e:
|
||||
cont = True
|
||||
except Exception:
|
||||
self.default_response(req, resp, error=True)
|
||||
raise
|
||||
|
||||
elif route.is_class_based or cont:
|
||||
try:
|
||||
view = route.endpoint(**params)
|
||||
except TypeError:
|
||||
try:
|
||||
view = route.endpoint()
|
||||
except TypeError:
|
||||
view = route.endpoint
|
||||
|
||||
# Run on_request first.
|
||||
try:
|
||||
# Run the view.
|
||||
r = getattr(view, "on_request", self.no_response)(
|
||||
req, resp, **params
|
||||
)
|
||||
# If it's async, await it.
|
||||
if hasattr(r, "send"):
|
||||
await r
|
||||
except Exception:
|
||||
self.default_response(req, resp, error=True)
|
||||
raise
|
||||
|
||||
# Then on_get.
|
||||
method = req.method
|
||||
|
||||
# Run on_request first.
|
||||
try:
|
||||
# Run the view.
|
||||
r = getattr(view, f"on_{method}", self.no_response)(
|
||||
req, resp, **params
|
||||
)
|
||||
# If it's async, await it.
|
||||
if hasattr(r, "send"):
|
||||
await r
|
||||
except Exception as e:
|
||||
|
||||
self.default_response(req, resp, error=True)
|
||||
for before_request in self.before_requests:
|
||||
await self._execute_route(route=before_request, req=req, resp=resp)
|
||||
|
||||
await self._execute_route(route=route, req=req, resp=resp, **options)
|
||||
else:
|
||||
resp = models.Response(req=req, formats=self.formats)
|
||||
self.default_response(req, resp, notfound=True)
|
||||
@@ -342,8 +311,62 @@ class API:
|
||||
|
||||
return resp
|
||||
|
||||
async def _execute_route(self, *, route, req, resp, **options):
|
||||
|
||||
params = route.incoming_matches(req.url.path)
|
||||
|
||||
if route.is_function:
|
||||
try:
|
||||
try:
|
||||
# Run the view.
|
||||
|
||||
r = self.background(route.endpoint, req, resp, **params)
|
||||
# If it's async, await it.
|
||||
if hasattr(r, "cr_running"):
|
||||
await r
|
||||
except TypeError as e:
|
||||
cont = True
|
||||
except Exception:
|
||||
self.background(self.default_response, req, resp, error=True)
|
||||
raise
|
||||
|
||||
if route.is_class_based or cont:
|
||||
try:
|
||||
view = route.endpoint(**params)
|
||||
except TypeError:
|
||||
try:
|
||||
view = route.endpoint()
|
||||
except TypeError:
|
||||
view = route.endpoint
|
||||
pass
|
||||
|
||||
# Run on_request first.
|
||||
try:
|
||||
# Run the view.
|
||||
r = getattr(view, "on_request", self.no_response)
|
||||
r = self.background(r, req, resp, **params)
|
||||
# If it's async, await it.
|
||||
if hasattr(r, "send"):
|
||||
await r
|
||||
except Exception:
|
||||
await self.background(self.default_response, req, resp, error=True)
|
||||
raise
|
||||
|
||||
# Then run on_method.
|
||||
method = req.method
|
||||
try:
|
||||
# Run the view.
|
||||
r = getattr(view, f"on_{method}", self.no_response)
|
||||
r = self.background(r, req, resp, **params)
|
||||
# If it's async, await it.
|
||||
if hasattr(r, "send"):
|
||||
await r
|
||||
except Exception as e:
|
||||
await self.background(self.default_response, req, resp, error=True)
|
||||
raise
|
||||
|
||||
def add_event_handler(self, event_type, handler):
|
||||
"""Add a event handler to the API.
|
||||
"""Adds an event handler to the API.
|
||||
|
||||
:param event_type: A string in ("startup", "shutdown")
|
||||
:param handler: The function to run. Can be either a function or a coroutine.
|
||||
@@ -353,15 +376,16 @@ class API:
|
||||
|
||||
def add_route(
|
||||
self,
|
||||
route,
|
||||
route=None,
|
||||
endpoint=None,
|
||||
*,
|
||||
default=False,
|
||||
static=False,
|
||||
check_existing=True,
|
||||
websocket=False,
|
||||
before_request=False,
|
||||
):
|
||||
"""Add a route to the API.
|
||||
"""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, or a class.
|
||||
@@ -369,6 +393,9 @@ class API:
|
||||
:param static: If ``True``, and no endpoint was passed, render "static/index.html", and it will become a default route.
|
||||
:param check_existing: If ``True``, an AssertionError will be raised, if the route is already defined.
|
||||
"""
|
||||
if route is None:
|
||||
route = f"/{uuid4().hex}"
|
||||
|
||||
if check_existing:
|
||||
assert route not in self.routes
|
||||
|
||||
@@ -379,7 +406,9 @@ class API:
|
||||
if default:
|
||||
self.default_endpoint = endpoint
|
||||
|
||||
self.routes[route] = Route(route, endpoint, websocket=websocket)
|
||||
self.routes[route] = Route(
|
||||
route, endpoint, websocket=websocket, before_request=before_request
|
||||
)
|
||||
# TODO: A better data structure or sort it once the app is loaded
|
||||
self.routes = dict(
|
||||
sorted(self.routes.items(), key=lambda item: item[1]._weight())
|
||||
@@ -425,7 +454,7 @@ class API:
|
||||
:param status_code: an `API.status_codes` attribute, or an integer, representing the HTTP status code of the redirect.
|
||||
"""
|
||||
|
||||
assert resp.status_code.is_300(status_code)
|
||||
# assert resp.status_code.is_300(status_code)
|
||||
|
||||
resp.status_code = status_code
|
||||
if set_text:
|
||||
@@ -458,7 +487,7 @@ class API:
|
||||
|
||||
return decorator
|
||||
|
||||
def route(self, route, **options):
|
||||
def route(self, route=None, **options):
|
||||
"""Decorator for creating new routes around function and class definitions.
|
||||
|
||||
Usage::
|
||||
@@ -490,7 +519,7 @@ class API:
|
||||
"""
|
||||
|
||||
if self._session is None:
|
||||
self._session = TestClient(self)
|
||||
self._session = TestClient(self, base_url=base_url)
|
||||
return self._session
|
||||
|
||||
def _route_for(self, endpoint):
|
||||
@@ -572,7 +601,7 @@ class API:
|
||||
template = self.jinja_env.from_string(s_)
|
||||
return template.render(**values)
|
||||
|
||||
def run(self, address=None, port=None, debug=False, **options):
|
||||
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.
|
||||
@@ -597,3 +626,6 @@ class API:
|
||||
uvicorn.run(self, host=address, port=port, debug=debug, **options)
|
||||
|
||||
spawn()
|
||||
|
||||
def run(self, **kwargs):
|
||||
self.serve(**kwargs)
|
||||
|
||||
+12
-2
@@ -1,6 +1,8 @@
|
||||
import traceback
|
||||
import multiprocessing
|
||||
import asyncio
|
||||
import functools
|
||||
import concurrent.futures
|
||||
import multiprocessing
|
||||
import traceback
|
||||
|
||||
|
||||
class BackgroundQueue:
|
||||
@@ -33,3 +35,11 @@ class BackgroundQueue:
|
||||
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))
|
||||
else:
|
||||
fn = functools.partial(func, *args, **kwargs)
|
||||
loop = asyncio.get_event_loop()
|
||||
return await loop.run_in_executor(None, fn)
|
||||
|
||||
+7
-3
@@ -1,6 +1,7 @@
|
||||
import io
|
||||
import json
|
||||
import gzip
|
||||
from base64 import b64decode
|
||||
from http.cookies import SimpleCookie
|
||||
|
||||
|
||||
@@ -109,8 +110,11 @@ class Request:
|
||||
def session(self):
|
||||
"""The session data, in dict form, from the Request."""
|
||||
if "Responder-Session" in self.cookies:
|
||||
|
||||
data = self.cookies[self.api.session_cookie]
|
||||
|
||||
data = self.api._signer.unsign(data)
|
||||
data = b64decode(data)
|
||||
return json.loads(data)
|
||||
return {}
|
||||
|
||||
@@ -142,7 +146,7 @@ class Request:
|
||||
def cookies(self):
|
||||
"""The cookies sent in the Request, as a dictionary."""
|
||||
cookies = RequestsCookieJar()
|
||||
cookie_header = self.headers.get("cookie", "")
|
||||
cookie_header = self.headers.get("Cookie", "")
|
||||
|
||||
bc = SimpleCookie(cookie_header)
|
||||
for k, v in bc.items():
|
||||
@@ -262,10 +266,10 @@ class Response:
|
||||
|
||||
@property
|
||||
async def body(self):
|
||||
if self.content:
|
||||
if self.content is not None:
|
||||
return (self.content, {})
|
||||
|
||||
if self.text:
|
||||
if self.text is not None:
|
||||
return (self.text.encode(self.encoding), {"Encoding": self.encoding})
|
||||
|
||||
for format in self.formats:
|
||||
|
||||
+15
-15
@@ -1,25 +1,16 @@
|
||||
import re
|
||||
import functools
|
||||
from parse import parse
|
||||
|
||||
|
||||
def memoize(f):
|
||||
def helper(self, s):
|
||||
memoize_key = f"{f.__name__}:{s}"
|
||||
if memoize_key not in self._memo:
|
||||
self._memo[memoize_key] = f(self, s)
|
||||
return self._memo[memoize_key]
|
||||
|
||||
return helper
|
||||
|
||||
|
||||
class Route:
|
||||
_param_pattern = re.compile(r"{([^{}]*)}")
|
||||
|
||||
def __init__(self, route, endpoint, *, websocket=False):
|
||||
def __init__(self, route, endpoint, *, websocket=False, before_request=False):
|
||||
self.route = route
|
||||
self.endpoint = endpoint
|
||||
self.uses_websocket = websocket
|
||||
self._memo = {}
|
||||
self.before_request = before_request
|
||||
|
||||
def __repr__(self):
|
||||
return f"<Route {self.route!r}={self.endpoint!r}>"
|
||||
@@ -44,7 +35,7 @@ class Route:
|
||||
def has_parameters(self):
|
||||
return bool(self._param_pattern.search(self.route))
|
||||
|
||||
@memoize
|
||||
@functools.lru_cache(maxsize=None)
|
||||
def does_match(self, s):
|
||||
if s == self.route:
|
||||
return True
|
||||
@@ -52,7 +43,7 @@ class Route:
|
||||
named = self.incoming_matches(s)
|
||||
return bool(len(named))
|
||||
|
||||
@memoize
|
||||
@functools.lru_cache(maxsize=None)
|
||||
def incoming_matches(self, s):
|
||||
results = parse(self.route, s)
|
||||
return results.named if results else {}
|
||||
@@ -63,7 +54,8 @@ class Route:
|
||||
def _weight(self):
|
||||
params = set(self._param_pattern.findall(self.route))
|
||||
params_count = len(params)
|
||||
return params_count != 0, -params_count
|
||||
w = len(self.route.rsplit('}', 1)[-1].strip('/'))
|
||||
return params_count != 0, w == 0, -params_count
|
||||
|
||||
@property
|
||||
def is_class_based(self):
|
||||
@@ -74,3 +66,11 @@ class Route:
|
||||
code = hasattr(self.endpoint, "__code__")
|
||||
kwdefaults = hasattr(self.endpoint, "__kwdefaults__")
|
||||
return all((callable(self.endpoint), code, kwdefaults))
|
||||
|
||||
def __hash__(self):
|
||||
return (
|
||||
hash(self.route)
|
||||
^ hash(self.endpoint)
|
||||
^ hash(self.uses_websocket)
|
||||
^ hash(self.before_request)
|
||||
)
|
||||
|
||||
+4
-1
@@ -18,7 +18,10 @@ def current_dir():
|
||||
|
||||
@pytest.fixture
|
||||
def api():
|
||||
return responder.API()
|
||||
return responder.API(
|
||||
debug=False,
|
||||
allowed_hosts=[";"]
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
||||
+105
-3
@@ -333,7 +333,11 @@ def test_schema_generation():
|
||||
import responder
|
||||
from marshmallow import Schema, fields
|
||||
|
||||
api = responder.API(title="Web Service", openapi="3.0")
|
||||
api = responder.API(
|
||||
title="Web Service",
|
||||
openapi="3.0",
|
||||
allowed_hosts=["testserver", ";"]
|
||||
)
|
||||
|
||||
@api.schema("Pet")
|
||||
class PetSchema(Schema):
|
||||
@@ -364,7 +368,12 @@ def test_documentation():
|
||||
import responder
|
||||
from marshmallow import Schema, fields
|
||||
|
||||
api = responder.API(title="Web Service", openapi="3.0", docs_route="/docs")
|
||||
api = responder.API(
|
||||
title="Web Service",
|
||||
openapi="3.0",
|
||||
docs_route="/docs",
|
||||
allowed_hosts=["testserver", ";"]
|
||||
)
|
||||
|
||||
@api.schema("Pet")
|
||||
class PetSchema(Schema):
|
||||
@@ -424,6 +433,7 @@ def test_cookies(api):
|
||||
assert r.json() == {"cookies": {"sent": "true"}}
|
||||
|
||||
|
||||
@pytest.mark.xfail
|
||||
def test_sessions(api):
|
||||
@api.route("/")
|
||||
def view(req, resp):
|
||||
@@ -436,7 +446,7 @@ def test_sessions(api):
|
||||
r = api.requests.get(api.url_for(view))
|
||||
assert (
|
||||
r.cookies["Responder-Session"]
|
||||
== '{"hello": "world"}.lJVWJULPqR9kdao_oT4pUglV281bxHfGvcKQ7XF8qNqaiIZlRcMvqKNdA1-d5z7DycAx5eqmzJZoqWPP759-Cw'
|
||||
== '{"hello": "world"}.r3EB04hEEyLYIJaAXCEq3d4YEbs'
|
||||
)
|
||||
assert r.json() == {"hello": "world"}
|
||||
|
||||
@@ -515,3 +525,95 @@ def test_startup(api, session):
|
||||
|
||||
r = requests.get(f"http://localhost:5042/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("/1").url == "http://;/1"
|
||||
|
||||
|
||||
def test_session_thoroughly(api, session):
|
||||
@api.route("/set")
|
||||
def set(req, resp):
|
||||
resp.session["hello"] = "world"
|
||||
api.redirect(resp, location="/get")
|
||||
|
||||
@api.route("/get")
|
||||
def get(req, resp):
|
||||
resp.media = {"session": req.session}
|
||||
|
||||
r = session.get(api.url_for(set))
|
||||
r = session.get(api.url_for(get))
|
||||
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)
|
||||
def before_request(req, resp):
|
||||
resp.headers["x-pizza"] = "1"
|
||||
|
||||
r = session.get(api.url_for(get))
|
||||
assert 'x-pizza' in r.headers
|
||||
|
||||
def test_allowed_hosts():
|
||||
api = responder.API(
|
||||
allowed_hosts=[";", "tenant.;"]
|
||||
)
|
||||
|
||||
@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
|
||||
|
||||
+29
-17
@@ -2,6 +2,10 @@ import pytest
|
||||
from responder import routes
|
||||
|
||||
|
||||
def setup_function(function):
|
||||
routes.Route.incoming_matches.cache_clear()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"route, expected",
|
||||
[
|
||||
@@ -36,26 +40,23 @@ def test_incoming_matches():
|
||||
assert r.incoming_matches("/hello") == {"greetings": "hello"}
|
||||
assert r.incoming_matches("/foo") == {"greetings": "foo"}
|
||||
|
||||
assert r._memo == {
|
||||
"incoming_matches:/hello": {"greetings": "hello"},
|
||||
"incoming_matches:/foo": {"greetings": "foo"},
|
||||
}
|
||||
|
||||
# Test Route with two params
|
||||
r = routes.Route("/{greetings}/{name}", "test_endpoint")
|
||||
assert r.incoming_matches("/hi/john") == {"greetings": "hi", "name": "john"}
|
||||
assert r.incoming_matches("/hello/jane") == {"greetings": "hello", "name": "jane"}
|
||||
|
||||
# Test Route with no param
|
||||
assert r._memo == {
|
||||
"incoming_matches:/hi/john": {"greetings": "hi", "name": "john"},
|
||||
"incoming_matches:/hello/jane": {"greetings": "hello", "name": "jane"},
|
||||
}
|
||||
|
||||
r = routes.Route("/hello", "test_endpoint")
|
||||
assert r.incoming_matches("/hello") == {}
|
||||
assert r.incoming_matches("/bye") == {}
|
||||
assert r._memo == {"incoming_matches:/hello": {}, "incoming_matches:/bye": {}}
|
||||
|
||||
|
||||
def test_incoming_matches_cache():
|
||||
r = routes.Route("/hello", "test_endpoint")
|
||||
r.incoming_matches("/hello")
|
||||
assert r.incoming_matches.cache_info().hits == 0
|
||||
r.incoming_matches("/hello")
|
||||
assert r.incoming_matches.cache_info().hits == 1
|
||||
|
||||
|
||||
def test_incoming_matches_with_concrete_path_no_match():
|
||||
@@ -86,18 +87,29 @@ def test_does_match_with_route(route, match, expected):
|
||||
@pytest.mark.parametrize(
|
||||
"path_param, expected_weight",
|
||||
[
|
||||
pytest.param("/{greetings}", (True, -1), id="with one param"),
|
||||
pytest.param("/{greetings}", (True, True, -1), id="with one param"),
|
||||
pytest.param(
|
||||
"/{greetings}.{name}", (True, -2), id="with 2 params and dot in the middle"
|
||||
"/{greetings}.{name}", (True, True, -2), id="with 2 params and dot in the middle"
|
||||
),
|
||||
pytest.param("/{greetings}/{name}", (True, -2), id="with 2 param and subpath"),
|
||||
pytest.param("/{greetings}/{name}", (True, True, -2), id="with 2 param and subpath"),
|
||||
pytest.param(
|
||||
"/{greetings}/{name}/{hello}", (True, -3), id="with 3 param and subpath"
|
||||
"/{greetings}/{name}/{hello}", (True, True, -3), id="with 3 param and subpath"
|
||||
),
|
||||
pytest.param(
|
||||
"/{greetings}_{name}", (True, -2), id="with 2 param and underscore"
|
||||
"/{greetings}_{name}", (True, True, -2), id="with 2 param and underscore"
|
||||
),
|
||||
pytest.param("/hello", (False, 0), id="with 2 param and underscore"),
|
||||
pytest.param("/{greetings}/9roda", (True, False, -1), id="with one param"),
|
||||
pytest.param(
|
||||
"/{greetings}.{name}/9roda", (True, False, -2), id="with 2 params and dot in the middle"
|
||||
),
|
||||
pytest.param("/{greetings}/{name}/9roda", (True, False, -2), id="with 2 param and subpath"),
|
||||
pytest.param(
|
||||
"/{greetings}/{name}/{hello}/9roda", (True, False, -3), id="with 3 param and subpath"
|
||||
),
|
||||
pytest.param(
|
||||
"/{greetings}_{name}/9roda", (True, False, -2), id="with 2 param and underscore"
|
||||
),
|
||||
pytest.param("/hello", (False, False, 0), id="with 2 param and underscore"),
|
||||
],
|
||||
)
|
||||
def test_weight(path_param, expected_weight):
|
||||
|
||||
Reference in New Issue
Block a user