mirror of
https://github.com/kennethreitz/responder.git
synced 2026-06-05 23:00:17 +00:00
Compare commits
105 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 |
@@ -2,3 +2,4 @@
|
||||
build
|
||||
responder.egg-info/
|
||||
dist/
|
||||
app.py
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
language: python
|
||||
python:
|
||||
- "3.6"
|
||||
- "3.7"
|
||||
|
||||
# command to install dependencies
|
||||
install:
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
Copyright 2018 Kenneth Reitz
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
@@ -6,6 +6,8 @@ name = "pypi"
|
||||
[packages]
|
||||
responder = {editable = true, path = "."}
|
||||
uvicorn = "*"
|
||||
starlette = "*"
|
||||
aiofiles = "*"
|
||||
|
||||
[dev-packages]
|
||||
pytest = "*"
|
||||
@@ -14,6 +16,8 @@ black = "*"
|
||||
twine = "*"
|
||||
flask = "*"
|
||||
sphinx = "*"
|
||||
locust = "*"
|
||||
locustio = "*"
|
||||
|
||||
[requires]
|
||||
python_version = "3.7"
|
||||
|
||||
Generated
+148
-28
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "76d2978ee90d2c028b13c9a5abdd2371d74d514045d50fb9b92aec44e72054b3"
|
||||
"sha256": "620c4b82d439a27e1afdbe54a6a77130cc52060846d4a6e322a5b064ff68e8fd"
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {
|
||||
@@ -16,6 +16,14 @@
|
||||
]
|
||||
},
|
||||
"default": {
|
||||
"aiofiles": {
|
||||
"hashes": [
|
||||
"sha256:021ea0ba314a86027c166ecc4b4c07f2d40fc0f4b3a950d1868a0f2571c2bbee",
|
||||
"sha256:1e644c2573f953664368de28d2aa4c89dfd64550429d0c27c4680ccd3aa4985d"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.4.0"
|
||||
},
|
||||
"aniso8601": {
|
||||
"hashes": [
|
||||
"sha256:7849749cf00ae0680ad2bdfe4419c7a662bef19c03691a19e008c8b9a5267802",
|
||||
@@ -110,6 +118,12 @@
|
||||
],
|
||||
"version": "==2.2.1"
|
||||
},
|
||||
"python-multipart": {
|
||||
"hashes": [
|
||||
"sha256:f7bb5f611fc600d15fa47b3974c8aa16e93724513b49b5f95c81e6624c83fa43"
|
||||
],
|
||||
"version": "==0.0.5"
|
||||
},
|
||||
"pyyaml": {
|
||||
"hashes": [
|
||||
"sha256:254bf6fda2b7c651837acb2c718e213df29d531eebf00edb54743d10bcb694eb",
|
||||
@@ -127,16 +141,17 @@
|
||||
],
|
||||
"version": "==2.19.1"
|
||||
},
|
||||
"requests-wsgi-adapter": {
|
||||
"hashes": [
|
||||
"sha256:7080c98ae2614b8d0b7339b611d97a535470d2fb479731f7d588d5f8108ea134"
|
||||
],
|
||||
"version": "==0.4.0"
|
||||
},
|
||||
"responder": {
|
||||
"editable": true,
|
||||
"path": "."
|
||||
},
|
||||
"rfc3986": {
|
||||
"hashes": [
|
||||
"sha256:632b8fcd2ac37f24334316227f909be4f9d0738cbf409404cff6fa5f69a24093",
|
||||
"sha256:8458571c4c57e1cf23593ad860bb601b6a604df6217f829c2bc70dc4b5af941b"
|
||||
],
|
||||
"version": "==1.1.0"
|
||||
},
|
||||
"rx": {
|
||||
"hashes": [
|
||||
"sha256:13a1d8d9e252625c173dc795471e614eadfe1cf40ffc684e08b8fff0d9748c23",
|
||||
@@ -151,6 +166,13 @@
|
||||
],
|
||||
"version": "==1.11.0"
|
||||
},
|
||||
"starlette": {
|
||||
"hashes": [
|
||||
"sha256:9f42bba2c3140402df7fe645b79aadc694cca80140d7bdd43b8a7175f84a8a70"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.4.1"
|
||||
},
|
||||
"urllib3": {
|
||||
"hashes": [
|
||||
"sha256:a68ac5e15e76e7e5dd2b8f94007233e01effe3e50e8daddf69acfd81cb686baf",
|
||||
@@ -165,13 +187,6 @@
|
||||
"index": "pypi",
|
||||
"version": "==0.3.12"
|
||||
},
|
||||
"waitress": {
|
||||
"hashes": [
|
||||
"sha256:40b0f297a7f3af61fbfbdc67e59090c70dc150a1601c39ecc9f5f1d283fb931b",
|
||||
"sha256:d33cd3d62426c0f1b3cd84ee3d65779c7003aae3fc060dee60524d10a57f05a9"
|
||||
],
|
||||
"version": "==1.1.0"
|
||||
},
|
||||
"websockets": {
|
||||
"hashes": [
|
||||
"sha256:0e2f7d6567838369af074f0ef4d0b802d19fa1fee135d864acc656ceefa33136",
|
||||
@@ -197,20 +212,6 @@
|
||||
"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": {
|
||||
@@ -306,6 +307,7 @@
|
||||
"sha256:edabd457cd23a02965166026fd9bfd196f4324fe6032e866d0f3bd0301cd486f",
|
||||
"sha256:fdf1c1dc5bafc32bc5d08b054f94d659422b05aba244d6be4ddc1c72d9aa70fb"
|
||||
],
|
||||
"markers": "sys_platform == 'win32' and platform_python_implementation == 'CPython'",
|
||||
"version": "==1.11.5"
|
||||
},
|
||||
"chardet": {
|
||||
@@ -393,6 +395,59 @@
|
||||
],
|
||||
"version": "==0.16.0"
|
||||
},
|
||||
"gevent": {
|
||||
"hashes": [
|
||||
"sha256:1f277c5cf060b30313c5f9b91588f4c645e11839e14a63c83fcf6f24b1bc9b95",
|
||||
"sha256:298a04a334fb5e3dcd6f89d063866a09155da56041bc94756da59db412cb45b1",
|
||||
"sha256:30e9b2878d5b57c68a40b3a08d496bcdaefc79893948989bb9b9fab087b3f3c0",
|
||||
"sha256:33533bc5c6522883e4437e901059fe5afa3ea74287eeea27a130494ff130e731",
|
||||
"sha256:3f06f4802824c577272960df003a304ce95b3e82eea01dad2637cc8609c80e2c",
|
||||
"sha256:419fd562e4b94b91b58cccb3bd3f17e1a11f6162fca6c591a7822bc8a68f023d",
|
||||
"sha256:4ea938f44b882e02cca9583069d38eb5f257cc15a03e918980c307e7739b1038",
|
||||
"sha256:51143a479965e3e634252a0f4a1ea07e5307cf8dc773ef6bf9dfe6741785fb4c",
|
||||
"sha256:5bf9bd1dd4951552d9207af3168f420575e3049016b9c10fe0c96760ce3555b7",
|
||||
"sha256:6004512833707a1877cc1a5aea90fd182f569e089bc9ab22a81d480dad018f1b",
|
||||
"sha256:640b3b52121ab519e0980cb38b572df0d2bc76941103a697e828c13d76ac8836",
|
||||
"sha256:6951655cc18b8371d823e81c700883debb0f633aae76f425dfeb240f76b95a67",
|
||||
"sha256:71eeb8d9146e8131b65c3364bb760b097c21b7b9fdbec91bf120685a510f997a",
|
||||
"sha256:7c899e5a6f94d6310352716740f05e41eb8c52d995f27fc01e90380913aa8f22",
|
||||
"sha256:8465f84ba31aaf52a080837e9c5ddd592ab0a21dfda3212239ce1e1796f4d503",
|
||||
"sha256:99de2e38dde8669dd30a8a1261bdb39caee6bd00a5f928d01dfdb85ab0502562",
|
||||
"sha256:9fa4284b44bc42bef6e437488d000ae37499ccee0d239013465638504c4565b7",
|
||||
"sha256:a1beea0443d3404c03e069d4c4d9fc13d8ec001771c77f9a23f01911a41f0e49",
|
||||
"sha256:a66a26b78d90d7c4e9bf9efb2b2bd0af49234604ac52eaca03ea79ac411e3f6d",
|
||||
"sha256:a94e197bd9667834f7bb6bd8dff1736fab68619d0f8cd78a9c1cebe3c4944677",
|
||||
"sha256:ac0331d3a3289a3d16627742be9c8969f293740a31efdedcd9087dadd6b2da57",
|
||||
"sha256:d26b57c50bf52fb38dadf3df5bbecd2236f49d7ac98f3cf32d6b8a2d25afc27f",
|
||||
"sha256:fd23b27387d76410eb6a01ea13efc7d8b4b95974ba212c336e8b1d6ab45a9578"
|
||||
],
|
||||
"version": "==1.3.7"
|
||||
},
|
||||
"greenlet": {
|
||||
"hashes": [
|
||||
"sha256:000546ad01e6389e98626c1367be58efa613fa82a1be98b0c6fc24b563acc6d0",
|
||||
"sha256:0d48200bc50cbf498716712129eef819b1729339e34c3ae71656964dac907c28",
|
||||
"sha256:23d12eacffa9d0f290c0fe0c4e81ba6d5f3a5b7ac3c30a5eaf0126bf4deda5c8",
|
||||
"sha256:37c9ba82bd82eb6a23c2e5acc03055c0e45697253b2393c9a50cef76a3985304",
|
||||
"sha256:51503524dd6f152ab4ad1fbd168fc6c30b5795e8c70be4410a64940b3abb55c0",
|
||||
"sha256:8041e2de00e745c0e05a502d6e6db310db7faa7c979b3a5877123548a4c0b214",
|
||||
"sha256:81fcd96a275209ef117e9ec91f75c731fa18dcfd9ffaa1c0adbdaa3616a86043",
|
||||
"sha256:853da4f9563d982e4121fed8c92eea1a4594a2299037b3034c3c898cb8e933d6",
|
||||
"sha256:8b4572c334593d449113f9dc8d19b93b7b271bdbe90ba7509eb178923327b625",
|
||||
"sha256:9416443e219356e3c31f1f918a91badf2e37acf297e2fa13d24d1cc2380f8fbc",
|
||||
"sha256:9854f612e1b59ec66804931df5add3b2d5ef0067748ea29dc60f0efdcda9a638",
|
||||
"sha256:99a26afdb82ea83a265137a398f570402aa1f2b5dfb4ac3300c026931817b163",
|
||||
"sha256:a19bf883b3384957e4a4a13e6bd1ae3d85ae87f4beb5957e35b0be287f12f4e4",
|
||||
"sha256:a9f145660588187ff835c55a7d2ddf6abfc570c2651c276d3d4be8a2766db490",
|
||||
"sha256:ac57fcdcfb0b73bb3203b58a14501abb7e5ff9ea5e2edfa06bb03035f0cff248",
|
||||
"sha256:bcb530089ff24f6458a81ac3fa699e8c00194208a724b644ecc68422e1111939",
|
||||
"sha256:beeabe25c3b704f7d56b573f7d2ff88fc99f0138e43480cecdfcaa3b87fe4f87",
|
||||
"sha256:d634a7ea1fc3380ff96f9e44d8d22f38418c1c381d5fac680b272d7d90883720",
|
||||
"sha256:d97b0661e1aead761f0ded3b769044bb00ed5d33e1ec865e891a8b128bf7c656"
|
||||
],
|
||||
"markers": "platform_python_implementation == 'CPython'",
|
||||
"version": "==0.4.15"
|
||||
},
|
||||
"idna": {
|
||||
"hashes": [
|
||||
"sha256:156a6814fb5ac1fc6850fb002e0852d56c0c8d2531923a51032d1b70760e186e",
|
||||
@@ -420,6 +475,21 @@
|
||||
],
|
||||
"version": "==2.10"
|
||||
},
|
||||
"locust": {
|
||||
"hashes": [
|
||||
"sha256:aaa38b525795e9c1a35ac3620543cf4e62f82948714f60a32023ea8c9b8edc2e"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.0"
|
||||
},
|
||||
"locustio": {
|
||||
"hashes": [
|
||||
"sha256:be7b44468b8683def983e7451ab505cd85fff8d06f6b75ad7c899cedbbf789ac",
|
||||
"sha256:c77b471e0e08e215c93a7af9a95b79193268072873fbbc0effca40f3d9b58be4"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.9.0"
|
||||
},
|
||||
"markupsafe": {
|
||||
"hashes": [
|
||||
"sha256:a6be69091dac236ea9c6bc7d012beab42010fa914c459791d627dad4910eb665"
|
||||
@@ -441,6 +511,26 @@
|
||||
],
|
||||
"version": "==4.3.0"
|
||||
},
|
||||
"msgpack": {
|
||||
"hashes": [
|
||||
"sha256:0b3b1773d2693c70598585a34ca2715873ba899565f0a7c9a1545baef7e7fbdc",
|
||||
"sha256:0bae5d1538c5c6a75642f75a1781f3ac2275d744a92af1a453c150da3446138b",
|
||||
"sha256:0ee8c8c85aa651be3aa0cd005b5931769eaa658c948ce79428766f1bd46ae2c3",
|
||||
"sha256:1369f9edba9500c7a6489b70fdfac773e925342f4531f1e3d4c20ac3173b1ae0",
|
||||
"sha256:22d9c929d1d539f37da3d1b0e16270fa9d46107beab8c0d4d2bddffffe895cee",
|
||||
"sha256:2ff43e3247a1e11d544017bb26f580a68306cec7a6257d8818893c1fda665f42",
|
||||
"sha256:31a98047355d34d047fcdb55b09cb19f633cf214c705a765bd745456c142130c",
|
||||
"sha256:8767eb0032732c3a0da92cbec5ac186ef89a3258c6edca09161472ca0206c45f",
|
||||
"sha256:8acc8910218555044e23826980b950e96685dc48124a290c86f6f41a296ea172",
|
||||
"sha256:ab189a6365be1860a5ecf8159c248f12d33f79ea799ae9695fa6a29896dcf1d4",
|
||||
"sha256:cfd6535feb0f1cf1c7cdb25773e965cc9f92928244a8c3ef6f8f8a8e1f7ae5c4",
|
||||
"sha256:e274cd4480d8c76ec467a85a9c6635bbf2258f0649040560382ab58cabb44bcf",
|
||||
"sha256:f86642d60dca13e93260187d56c2bef2487aa4d574a669e8ceefcf9f4c26fd00",
|
||||
"sha256:f8a57cbda46a94ed0db55b73e6ab0c15e78b4ede8690fa491a0e55128d552bb0",
|
||||
"sha256:fcea97a352416afcbccd7af9625159d80704a25c519c251c734527329bb20d0e"
|
||||
],
|
||||
"version": "==0.5.6"
|
||||
},
|
||||
"packaging": {
|
||||
"hashes": [
|
||||
"sha256:0886227f54515e592aaa2e5a553332c73962917f2831f1b0f9b9f4380a4b9807",
|
||||
@@ -518,6 +608,36 @@
|
||||
],
|
||||
"version": "==2018.5"
|
||||
},
|
||||
"pyzmq": {
|
||||
"hashes": [
|
||||
"sha256:25a0715c8f69cf72f67cfe5a68a3f3ed391c67c063d2257bec0fe7fc2c7f08f8",
|
||||
"sha256:2bab63759632c6b9e0d5bf19cc63c3b01df267d660e0abcf230cf0afaa966349",
|
||||
"sha256:30ab49d99b24bf0908ebe1cdfa421720bfab6f93174e4883075b7ff38cc555ba",
|
||||
"sha256:32c7ca9fc547a91e3c26fc6080b6982e46e79819e706eb414dd78f635a65d946",
|
||||
"sha256:41219ae72b3cc86d97557fe5b1ef5d1adc1057292ec597b50050874a970a39cf",
|
||||
"sha256:4b8c48a9a13cea8f1f16622f9bd46127108af14cd26150461e3eab71e0de3e46",
|
||||
"sha256:55724997b4a929c0d01b43c95051318e26ddbae23565018e138ae2dc60187e59",
|
||||
"sha256:65f0a4afae59d4fc0aad54a917ab599162613a761b760ba167d66cc646ac3786",
|
||||
"sha256:6f88591a8b246f5c285ee6ce5c1bf4f6bd8464b7f090b1333a446b6240a68d40",
|
||||
"sha256:75022a4c60dcd8765bb9ca32f6de75a0ec83b0d96e0309dc479f4c7b21f26cb7",
|
||||
"sha256:76ea493bfab18dcb090d825f3662b5612e2def73dffc196d51a5194b0294a81d",
|
||||
"sha256:7b60c045b80709e4e3c085bab9b691e71761b44c2b42dbb047b8b498e7bc16b3",
|
||||
"sha256:8e6af2f736734aef8ed6f278f9f552ec7f37b1a6b98e59b887484a840757f67d",
|
||||
"sha256:9ac2298e486524331e26390eac14e4627effd3f8e001d4266ed9d8f1d2d31cce",
|
||||
"sha256:9ba650f493a9bc1f24feca1d90fce0e5dd41088a252ac9840131dfbdbf3815ca",
|
||||
"sha256:a02a4a385e394e46012dc83d2e8fd6523f039bb52997c1c34a2e0dd49ed839c1",
|
||||
"sha256:a3ceee84114d9f5711fa0f4db9c652af0e4636c89eabc9b7f03a3882569dd1ed",
|
||||
"sha256:a72b82ac1910f2cf61a49139f4974f994984475f771b0faa730839607eeedddf",
|
||||
"sha256:ab136ac51027e7c484c53138a0fab4a8a51e80d05162eb7b1585583bcfdbad27",
|
||||
"sha256:c095b224300bcac61e6c445e27f9046981b1ac20d891b2f1714da89d34c637c8",
|
||||
"sha256:c5cc52d16c06dc2521340d69adda78a8e1031705924e103c0eb8fc8af861d810",
|
||||
"sha256:d612e9833a89e8177f8c1dc68d7b4ff98d3186cd331acd616b01bbdab67d3a7b",
|
||||
"sha256:e828376a23c66c6fe90dcea24b4b72cd774f555a6ee94081670872918df87a19",
|
||||
"sha256:e9767c7ab2eb552796440168d5c6e23a99ecaade08dda16266d43ad461730192",
|
||||
"sha256:ebf8b800d42d217e4710d1582b0c8bff20cdcb4faad7c7213e52644034300924"
|
||||
],
|
||||
"version": "==17.1.2"
|
||||
},
|
||||
"readme-renderer": {
|
||||
"hashes": [
|
||||
"sha256:237ca8705ffea849870de41101dba41543561da05c0ae45b2f1c547efa9843d2",
|
||||
|
||||
@@ -1,10 +1,18 @@
|
||||
# Responder: a familiar HTTP Service Framework for Python
|
||||
|
||||

|
||||
[](https://travis-ci.org/kennethreitz/responder)
|
||||
[](https://responder.readthedocs.io/en/latest/)
|
||||
[](https://pypi.org/project/responder/)
|
||||
[](https://pypi.org/project/responder/)
|
||||
[](https://pypi.org/project/responder/)
|
||||
[](https://github.com/kennethreitz/responder/graphs/contributors)
|
||||
[](https://saythanks.io/to/kennethreitz)
|
||||
|
||||
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.
|
||||
[](http://python-responder.org/)
|
||||
|
||||
## But will it blend?
|
||||
The Python world certainly doesn't need more web frameworks. But, it does need more creativity, so I thought I'd spread some [Hacktoberfest](https://hacktoberfest.digitalocean.com/) spirit around, bring some of my ideas to the table, and see what I could come up with.
|
||||
|
||||
## An Example Web Service:
|
||||
|
||||
```python
|
||||
import responder
|
||||
@@ -12,14 +20,32 @@ import responder
|
||||
api = responder.API()
|
||||
|
||||
@api.route("/{greeting}")
|
||||
def greet_world(req, resp, *, greeting):
|
||||
async def greet_world(req, resp, *, greeting):
|
||||
resp.text = f"{greeting}, world!"
|
||||
|
||||
if __name__ == '__main__':
|
||||
api.run()
|
||||
```
|
||||
|
||||
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.
|
||||
That `async` declaration is optional.
|
||||
|
||||
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.
|
||||
|
||||
|
||||
## Testimonials
|
||||
|
||||
> "Pleasantly very taken with python-responder. [@kennethreitz](https://twitter.com/kennethreitz) at his absolute best." —Rudraksh M.K.
|
||||
|
||||
> "Buckle up!" —Tom Christie of [APIStar](https://github.com/encode/apistar) and [Django REST Framework](https://www.django-rest-framework.org/)
|
||||
|
||||
> "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.
|
||||
|
||||
> "The most ambitious crossover event in history." —Pablo Cabezas, [on Tom Christie joining the project](https://twitter.com/pabloteleco/status/1050841098321620992?s=20)
|
||||
|
||||
|
||||
## More Examples
|
||||
|
||||
Class-based views (and setting some headers and stuff):
|
||||
|
||||
@@ -42,7 +68,22 @@ def greet_world(req, resp, *, greeting):
|
||||
|
||||
The `api` instance is available as an object during template rendering.
|
||||
|
||||
Serve a GraphQL API:
|
||||
Here, you can spawn off a background thread to run any function, out-of-request:
|
||||
|
||||
```python
|
||||
@api.route("/")
|
||||
def hello(req, resp):
|
||||
|
||||
@api.background.task
|
||||
def sleep(s=10):
|
||||
time.sleep(s)
|
||||
print("slept!")
|
||||
|
||||
sleep()
|
||||
resp.content = "processing"
|
||||
```
|
||||
|
||||
And even serve a GraphQL API:
|
||||
|
||||
```python
|
||||
import graphene
|
||||
@@ -80,46 +121,97 @@ Want HSTS?
|
||||
api = responder.API(enable_hsts=True)
|
||||
```
|
||||
|
||||
Boom. ✨🍰✨
|
||||
Boom.
|
||||
|
||||
|
||||
# Installing Responder
|
||||
|
||||
Install the latest release (might be out of date with the docs a bit):
|
||||
|
||||
|
||||
$ pipenv install responder
|
||||
✨🍰✨
|
||||
|
||||
|
||||
Or, install from the development branch:
|
||||
|
||||
$ pipenv install -e git+https://github.com/kennethreitz/responder.git#egg=responder
|
||||
|
||||
Only **Python 3.6+** is supported.
|
||||
|
||||
# Web Service Performance Charecteristics
|
||||
The objective of these benchmark tests is not testing deployment (like uwsgi vs gunicorn vs uvicorn etc) but instead test the performance of python-response against other popular Python web frameworks.
|
||||
|
||||
### Methodology
|
||||
The results below were gotten running the performance tests on a Lenovo W530, Intel(R) Core(TM) i7-3740QM CPU @ 2.70GHz, MEM: 32GB, Linux Mint 19. I used Python 3.7.0 with the WRK utility with params:
|
||||
wrk -d20s -t10 -c200 (i.e. 10 threads and 200 connections).
|
||||
|
||||
1. #### Simple "Hello World" benchmark
|
||||
|
||||
python-responder v0.0.1 (Master branch)
|
||||
Requests/sec: 1368.23
|
||||
Transfer/sec: 163.01KB
|
||||
|
||||
|
||||
|
||||
|
||||
Django v2.1.2 (i18n == False)
|
||||
Requests/sec: 544.54
|
||||
Transfer/sec: 103.18KB
|
||||
|
||||
|
||||
|
||||
|
||||
Django v2.1.2 (i18n == True)
|
||||
Requests/sec: 535.12
|
||||
Transfer/sec: 101.38KB
|
||||
|
||||
|
||||
|
||||
Django v2.1.2 (Minimal 1 file Django Application)
|
||||
https://gist.github.com/aitoehigie/ebcc1d3e460e66cd51e5501fa2636798
|
||||
Requests/sec: 701.53
|
||||
Transfer/sec: 99.34KB
|
||||
|
||||
|
||||
|
||||
|
||||
Flask v1.0.2
|
||||
Requests/sec: 896.24
|
||||
Transfer/sec: 144.41KB
|
||||
|
||||
|
||||
# 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).
|
||||
- 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).
|
||||
- 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.
|
||||
- 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.
|
||||
- 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.
|
||||
|
||||
|
||||
## 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.
|
||||
- Cookie-based sessions are currently an afterthought, as this is an API framework, but websites are APIs too.
|
||||
- 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! :)
|
||||
----------
|
||||
|
||||
[](https://hacktoberfest.digitalocean.com/)
|
||||
|
||||
@@ -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)
|
||||
@@ -49,9 +49,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,
|
||||
@@ -105,19 +109,13 @@
|
||||
</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. -->
|
||||
|
||||
+154
-46
@@ -3,30 +3,83 @@
|
||||
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://travis-ci.org/kennethreitz/responder.svg?branch=master
|
||||
:target: https://travis-ci.org/kennethreitz/responder
|
||||
.. |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
|
||||
|
||||
::
|
||||
The Python world certainly doesn't need more web frameworks. But, it does need more creativity, so I thought I'd
|
||||
spread some `Hacktoberfest <https://hacktoberfest.digitalocean.com/>`_ spirit around, bring some of my ideas to the table, and see what I could come up with.
|
||||
|
||||
import responder
|
||||
An Example Web Service:
|
||||
-----------------------
|
||||
|
||||
api = responder.API()
|
||||
.. code:: python
|
||||
|
||||
@api.route("/{greeting}")
|
||||
def greet_world(req, resp, *, greeting):
|
||||
resp.text = f"{greeting}, world!"
|
||||
import responder
|
||||
|
||||
if __name__ == '__main__':
|
||||
api.run()
|
||||
api = responder.API()
|
||||
|
||||
@api.route("/{greeting}")
|
||||
async def greet_world(req, resp, *, greeting):
|
||||
resp.text = f"{greeting}, world!"
|
||||
|
||||
if __name__ == '__main__':
|
||||
api.run()
|
||||
|
||||
That ``async`` declaration is optional.
|
||||
|
||||
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.
|
||||
|
||||
Testimonials
|
||||
------------
|
||||
|
||||
“Pleasantly very taken with python-responder.
|
||||
`@kennethreitz <https://twitter.com/kennethreitz>`_ at his absolute
|
||||
best.” —Rudraksh M.K.
|
||||
|
||||
..
|
||||
|
||||
“Buckle up!” —Tom Christie of `APIStar`_ and `Django REST Framework`_
|
||||
|
||||
“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.
|
||||
|
||||
|
||||
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.
|
||||
..
|
||||
|
||||
|
||||
“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
|
||||
|
||||
More Examples
|
||||
-------------
|
||||
|
||||
Class-based views (and setting some headers and stuff)::
|
||||
@@ -49,6 +102,20 @@ Render a template, with arguments::
|
||||
|
||||
The ``api`` instance is available as an object during template rendering.
|
||||
|
||||
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"
|
||||
|
||||
|
||||
Serve a GraphQL API::
|
||||
|
||||
import graphene
|
||||
@@ -63,6 +130,7 @@ Serve a GraphQL API::
|
||||
|
||||
|
||||
We can then send a query to our service::
|
||||
|
||||
>>> requests = api.session()
|
||||
>>> r = requests.get("http://;/graph", params={"query": "{ hello }"})
|
||||
>>> r.json()
|
||||
@@ -84,58 +152,98 @@ Want HSTS?
|
||||
api = responder.API(enable_hsts=True)
|
||||
|
||||
|
||||
Boom. ✨🍰✨
|
||||
Boom.
|
||||
|
||||
|
||||
Installing Responder
|
||||
====================
|
||||
|
||||
Install the latest release (might be out of date with the docs a bit):
|
||||
|
||||
.. code-block:: shell
|
||||
|
||||
$ pipenv install git+https://github.com/kennethreitz/responder.git#egg=responder
|
||||
✨🍰✨
|
||||
|
||||
|
||||
Or, install from the development branch:
|
||||
|
||||
.. code-block:: shell
|
||||
|
||||
$ pipenv install -e git+https://github.com/kennethreitz/responder.git#egg=responder
|
||||
|
||||
|
||||
Only **Python 3.6+** is supported.
|
||||
|
||||
|
||||
|
||||
Web Service Performance Charecteristics
|
||||
---------------------------------------
|
||||
|
||||
The objective of these benchmark tests is not testing deployment (like uwsgi vs gunicorn vs uvicorn etc) but instead test the performance of python-response against other popular Python web frameworks.
|
||||
|
||||
Methodology
|
||||
~~~~~~~~~~~
|
||||
|
||||
The results below were gotten running the performance tests on a Lenovo
|
||||
W530, Intel(R) Core(TM) i7-3740QM CPU @ 2.70GHz, MEM: 32GB, Linux Mint
|
||||
19. I used Python 3.7.0 with the WRK utility with params: wrk -d20s -t10
|
||||
-c200 (i.e. 10 threads and 200 connections).
|
||||
|
||||
1. .. rubric:: Simple “Hello World” benchmark
|
||||
:name: simple-hello-world-benchmark
|
||||
|
||||
| python-responder v0.0.1 (Master branch)
|
||||
| Requests/sec: 1368.23
|
||||
| Transfer/sec: 163.01KB
|
||||
|
||||
| Django v2.1.2 (i18n == False)
|
||||
| Requests/sec: 544.54
|
||||
| Transfer/sec: 103.18KB
|
||||
|
||||
| Django v2.1.2 (i18n == True)
|
||||
| Requests/sec: 535.12
|
||||
| Transfer/sec: 101.38KB
|
||||
|
||||
| Django v2.1.2 (Minimal 1 file Django Application)
|
||||
| https://gist.github.com/aitoehigie/ebcc1d3e460e66cd51e5501fa2636798
|
||||
| Requests/sec: 701.53
|
||||
| Transfer/sec: 99.34KB
|
||||
|
||||
| Flask v1.0.2
|
||||
| Requests/sec: 896.24
|
||||
| Transfer/sec: 144.41KB
|
||||
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.
|
||||
- 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 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 unneccessary in production.
|
||||
- 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 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 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
|
||||
------------
|
||||
|
||||
- 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.
|
||||
- Cookie-based sessions are currently an afterthrought, as this is an API framework, but websites are APIs too.
|
||||
- If frontend websites are supported, provide an official way to run webpack.
|
||||
|
||||
|
||||
Installation
|
||||
============
|
||||
|
||||
.. code-block:: shell
|
||||
|
||||
$ pipenv install responder
|
||||
✨🍰✨
|
||||
|
||||
Only **Python 3.6+** is supported.
|
||||
|
||||
|
||||
API Documentation
|
||||
=================
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
__version__ = "0.0.1"
|
||||
__version__ = "0.0.2"
|
||||
|
||||
+58
-73
@@ -3,20 +3,32 @@ import json
|
||||
from functools import partial
|
||||
from pathlib import Path
|
||||
|
||||
import waitress
|
||||
import uvicorn
|
||||
|
||||
import asyncio
|
||||
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 starlette.routing import Router
|
||||
from starlette.staticfiles import StaticFiles
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
from . import models
|
||||
from .status_codes import HTTP_404
|
||||
from . import status_codes
|
||||
from .routes import Route
|
||||
from .formats import get_formats
|
||||
from .background import BackgroundQueue
|
||||
|
||||
|
||||
def memoize(f):
|
||||
memo = {}
|
||||
|
||||
def helper(self, name, auto_escape=True, **values):
|
||||
if repr(values) not in memo:
|
||||
memo[repr(values)] = f(self, name, auto_escape=True, **values)
|
||||
return memo[repr(values)]
|
||||
|
||||
return helper
|
||||
|
||||
|
||||
# TODO: consider moving status codes here
|
||||
@@ -36,67 +48,41 @@ class API:
|
||||
self.static_dir = Path(os.path.abspath(static_dir))
|
||||
self.templates_dir = Path(os.path.abspath(templates_dir))
|
||||
self.routes = {}
|
||||
|
||||
self.hsts_enabled = enable_hsts
|
||||
self.apps = {"/": self._wsgi_app}
|
||||
self.static_files = StaticFiles(directory=str(self.static_dir))
|
||||
self.apps = {"/static": self.static_files}
|
||||
|
||||
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
|
||||
self.background = BackgroundQueue()
|
||||
|
||||
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::
|
||||
def __call__(self, scope):
|
||||
path = scope["path"]
|
||||
root_path = scope.get("root_path", "")
|
||||
|
||||
app = MyMiddleware(app)
|
||||
# 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
|
||||
return app(scope)
|
||||
|
||||
It's a better idea to do this instead::
|
||||
# Call the main dispatcher.
|
||||
async def asgi(receive, send):
|
||||
nonlocal scope, self
|
||||
|
||||
app.wsgi_app = MyMiddleware(app.wsgi_app)
|
||||
req = models.Request(scope, receive=receive)
|
||||
resp = await self._dispatch_request(req)
|
||||
await resp(receive, send)
|
||||
|
||||
Then you still have the original application object around and
|
||||
can continue to call methods on it.
|
||||
|
||||
.. 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`.
|
||||
|
||||
: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.
|
||||
"""
|
||||
|
||||
req = models.Request(environ, start_response)
|
||||
# if not req.dispatched:
|
||||
resp = self._dispatch_request(req)
|
||||
return resp(environ, start_response)
|
||||
|
||||
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 asgi
|
||||
|
||||
def path_matches_route(self, path):
|
||||
"""Given a path portion of a URL, tests that it matches against any registered route.
|
||||
@@ -107,11 +93,11 @@ class API:
|
||||
if route_object.does_match(path):
|
||||
return route
|
||||
|
||||
def _dispatch_request(self, req):
|
||||
async def _dispatch_request(self, req):
|
||||
# Set formats on Request object.
|
||||
req.formats = self.formats
|
||||
|
||||
route = self.path_matches_route(req.path)
|
||||
route = self.path_matches_route(req.url.path)
|
||||
resp = models.Response(req=req, formats=self.formats)
|
||||
|
||||
if self.hsts_enabled:
|
||||
@@ -121,10 +107,12 @@ class API:
|
||||
|
||||
if route:
|
||||
try:
|
||||
params = self.routes[route].incoming_matches(req.path)
|
||||
self.routes[route].endpoint(req, resp, **params)
|
||||
params = self.routes[route].incoming_matches(req.url.path)
|
||||
result = self.routes[route].endpoint(req, resp, **params)
|
||||
if hasattr(result, "cr_running"):
|
||||
await result
|
||||
# The request is using class-based views.
|
||||
except TypeError:
|
||||
except TypeError as e:
|
||||
try:
|
||||
view = self.routes[route].endpoint(**params)
|
||||
except TypeError:
|
||||
@@ -136,7 +124,6 @@ class API:
|
||||
except AssertionError:
|
||||
# WSGI App.
|
||||
try:
|
||||
req.dispatched = True
|
||||
return view(
|
||||
environ=req._environ, start_response=req._start_response
|
||||
)
|
||||
@@ -150,7 +137,7 @@ class API:
|
||||
pass
|
||||
|
||||
# Then on_get.
|
||||
method = req.method
|
||||
method = req.method.lower()
|
||||
|
||||
try:
|
||||
getattr(view, f"on_{method}")(req, resp)
|
||||
@@ -203,10 +190,11 @@ class API:
|
||||
return req.json()["query"]
|
||||
|
||||
# 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"]
|
||||
# Form data is awaiting https://github.com/encode/starlette/pull/102
|
||||
# if "query" in req.media("form"):
|
||||
# return req.media("form")["query"]
|
||||
# if "q" in req.media("form"):
|
||||
# return req.media("form")["q"]
|
||||
|
||||
# Support query/q in params.
|
||||
if "query" in req.params:
|
||||
@@ -247,13 +235,13 @@ class API:
|
||||
|
||||
return decorator
|
||||
|
||||
def mount(self, route, wsgi_app):
|
||||
def mount(self, route, asgi_app):
|
||||
"""Mounts a WSGI 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).
|
||||
"""
|
||||
self.apps.update({route: wsgi_app})
|
||||
self.apps.update({route: asgi_app})
|
||||
|
||||
def session(self, base_url="http://;"):
|
||||
"""Testing HTTP client. Returns a Requests session object, able to send HTTP requests to the WSGI application.
|
||||
@@ -262,9 +250,7 @@ class API:
|
||||
"""
|
||||
|
||||
if self._session is None:
|
||||
session = RequestsSession()
|
||||
session.mount(base_url, RequestsWSGIAdapter(self))
|
||||
self._session = session
|
||||
self._session = TestClient(self)
|
||||
return self._session
|
||||
|
||||
def url_for(self, endpoint, absolute_url=False, **params):
|
||||
@@ -279,6 +265,7 @@ class API:
|
||||
return route_object.url(**params)
|
||||
raise ValueError
|
||||
|
||||
@memoize
|
||||
def template(self, name, auto_escape=True, **values):
|
||||
"""Renders the given `jinja2 <http://jinja.pocoo.org/docs/>`_ template, with provided values supplied.
|
||||
|
||||
@@ -334,7 +321,7 @@ class API:
|
||||
template = env.from_string(s)
|
||||
return template.render(**values)
|
||||
|
||||
def run(self, address=None, port=None, **kwargs):
|
||||
def run(self, address=None, port=None, **options):
|
||||
"""Runs the application with Waitress. If the ``PORT`` environment
|
||||
variable is set, requests will be served on that port automatically to all
|
||||
known hosts.
|
||||
@@ -351,8 +338,6 @@ class API:
|
||||
if address is None:
|
||||
address = "127.0.0.1"
|
||||
if port is None:
|
||||
port = 0
|
||||
port = 5042
|
||||
|
||||
bind_to = f"{address}:{port}"
|
||||
|
||||
waitress.serve(app=self, listen=bind_to, **kwargs)
|
||||
uvicorn.run(self, host=address, port=port, **options)
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import multiprocessing
|
||||
import concurrent.futures
|
||||
|
||||
|
||||
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 = []
|
||||
self.callbacks = []
|
||||
|
||||
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)
|
||||
|
||||
def task(self, f):
|
||||
def do_task(*args, **kwargs):
|
||||
result = self.run(f, *args, **kwargs)
|
||||
|
||||
for cb in self.callbacks:
|
||||
result.add_done_callback(cb)
|
||||
|
||||
return result
|
||||
|
||||
return do_task
|
||||
|
||||
def callback(self, f):
|
||||
self.callbacks.append(f)
|
||||
|
||||
def register_callback():
|
||||
f()
|
||||
|
||||
return register_callback
|
||||
@@ -2,9 +2,9 @@ import yaml
|
||||
import json
|
||||
|
||||
|
||||
def format_form(r, encode=False):
|
||||
async def format_form(r, encode=False):
|
||||
if not encode:
|
||||
return r._wz.form
|
||||
return await r._starlette.form()
|
||||
|
||||
|
||||
def format_yaml(r, encode=False):
|
||||
@@ -12,7 +12,7 @@ def format_yaml(r, encode=False):
|
||||
r.headers.update({"Content-Type": "application/x-yaml"})
|
||||
return yaml.dump(r.media)
|
||||
else:
|
||||
return yaml.load(r.content)
|
||||
return yaml.safe_load(r.content)
|
||||
|
||||
|
||||
def format_json(r, encode=False):
|
||||
|
||||
+139
-59
@@ -2,75 +2,144 @@ import io
|
||||
import json
|
||||
import gzip
|
||||
|
||||
import rfc3986
|
||||
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
|
||||
from starlette.datastructures import MutableHeaders
|
||||
from starlette.requests import Request as StarletteRequest
|
||||
from starlette.responses import Response as StarletteResponse
|
||||
|
||||
|
||||
from urllib.parse import parse_qs
|
||||
|
||||
from .status_codes import HTTP_200
|
||||
|
||||
# @staticmethod
|
||||
# def funcname(parameter_list):
|
||||
# pass
|
||||
|
||||
class QueryDict(dict):
|
||||
def __init__(self, query_string):
|
||||
self.update(parse_qs(query_string))
|
||||
|
||||
def flatten(d):
|
||||
for key, value in d.copy().items():
|
||||
if len(value) == 1:
|
||||
d[key] = value[0]
|
||||
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 []
|
||||
|
||||
return d
|
||||
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.
|
||||
__slots__ = [
|
||||
"_starlette",
|
||||
"formats",
|
||||
"headers",
|
||||
"mimetype",
|
||||
"method",
|
||||
"full_url",
|
||||
"url",
|
||||
"params",
|
||||
]
|
||||
|
||||
def __init__(self, scope, receive):
|
||||
self._starlette = StarletteRequest(scope, receive)
|
||||
self.formats = None
|
||||
|
||||
headers = CaseInsensitiveDict()
|
||||
for header, value in self._starlette.headers.items():
|
||||
headers[header] = value
|
||||
|
||||
self.headers = (
|
||||
headers
|
||||
) #: A case-insensitive dictionary, containg all headers sent in the Request.
|
||||
|
||||
self.mimetype = self.headers.get("Content-Type", "")
|
||||
|
||||
self.method = (
|
||||
self._starlette.method.lower()
|
||||
) #: The incoming HTTP method used for the request, lower-cased.
|
||||
|
||||
self.full_url = str(
|
||||
self._starlette.url
|
||||
) #: The full URL of the Request, query parameters and all.
|
||||
|
||||
self.url = rfc3986.urlparse(self.full_url) #: The parsed URL of the Request
|
||||
try:
|
||||
self.params = QueryDict(
|
||||
self.url.query
|
||||
) #: A dictionary of the parsed query paramaters used for the Request.
|
||||
except AttributeError:
|
||||
self.params = {}
|
||||
|
||||
@property
|
||||
async def content(self):
|
||||
"""The Request body, as bytes."""
|
||||
return (await self._starlette.body()).encode(self.encoding)
|
||||
|
||||
@property
|
||||
async def text(self):
|
||||
"""The Request body, as unicode."""
|
||||
return await self._starlette.body()
|
||||
|
||||
@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``."""
|
||||
@@ -92,6 +161,17 @@ class Request:
|
||||
|
||||
|
||||
class Response:
|
||||
__slots__ = [
|
||||
"req",
|
||||
"status_code",
|
||||
"text",
|
||||
"content",
|
||||
"encoding",
|
||||
"media",
|
||||
"headers",
|
||||
"formats",
|
||||
]
|
||||
|
||||
def __init__(self, req, *, formats):
|
||||
self.req = req
|
||||
self.status_code = HTTP_200 #: The HTTP Status Code to use for the Response.
|
||||
@@ -109,7 +189,7 @@ class Response:
|
||||
@property
|
||||
def body(self):
|
||||
if self.content:
|
||||
return (self.content, self.mimetype, {})
|
||||
return (self.content, {})
|
||||
|
||||
if self.text:
|
||||
return (self.text.encode(self.encoding), {"Encoding": self.encoding})
|
||||
@@ -120,7 +200,10 @@ class Response:
|
||||
|
||||
# Default to JSON anyway.
|
||||
else:
|
||||
return (json.dumps(self.media), {"Content-Type": "application/json"})
|
||||
return (
|
||||
self.formats["json"](self, encode=True),
|
||||
{"Content-Type": "application/json"},
|
||||
)
|
||||
|
||||
@property
|
||||
def gzipped_body(self):
|
||||
@@ -147,20 +230,17 @@ class Response:
|
||||
else:
|
||||
return (body, headers)
|
||||
|
||||
@property
|
||||
def _wz(self):
|
||||
async def __call__(self, receive, send):
|
||||
body, headers = self.body
|
||||
if len(self.body) > 500:
|
||||
body, headers = self.gzipped_body
|
||||
if self.headers:
|
||||
headers.update(self.headers)
|
||||
|
||||
r = WerkzeugResponse(body, status=self.status_code, direct_passthrough=False)
|
||||
r.headers = headers
|
||||
return r
|
||||
|
||||
def __call__(self, environ, start_response):
|
||||
return self._wz(environ, start_response)
|
||||
response = StarletteResponse(
|
||||
body, status_code=self.status_code, headers=headers
|
||||
)
|
||||
await response(receive, send)
|
||||
|
||||
|
||||
class Schema(graphene.Schema):
|
||||
|
||||
@@ -1,6 +1,17 @@
|
||||
from parse import parse, search
|
||||
|
||||
|
||||
def memoize(f):
|
||||
memo = {}
|
||||
|
||||
def helper(self, s):
|
||||
if s not in memo:
|
||||
memo[s] = f(self, s)
|
||||
return memo[s]
|
||||
|
||||
return helper
|
||||
|
||||
|
||||
class Route:
|
||||
def __init__(self, route, endpoint):
|
||||
self.route = route
|
||||
@@ -21,6 +32,7 @@ class Route:
|
||||
def has_parameters(self):
|
||||
return all([("{" in self.route), ("}" in self.route)])
|
||||
|
||||
@memoize
|
||||
def does_match(self, s):
|
||||
if s == self.route:
|
||||
return True
|
||||
@@ -28,6 +40,7 @@ class Route:
|
||||
named = self.incoming_matches(s)
|
||||
return bool(len(named))
|
||||
|
||||
@memoize
|
||||
def incoming_matches(self, s):
|
||||
results = parse(self.route, s)
|
||||
return results.named if results else {}
|
||||
|
||||
@@ -22,16 +22,18 @@ if sys.argv[-1] == "publish":
|
||||
sys.exit()
|
||||
|
||||
required = [
|
||||
"waitress",
|
||||
"werkzeug",
|
||||
"starlette",
|
||||
"uvicorn",
|
||||
"aiofiles",
|
||||
"pyyaml",
|
||||
"requests",
|
||||
"requests-wsgi-adapter",
|
||||
"graphene",
|
||||
"graphql-server-core>=1.1",
|
||||
"whitenoise",
|
||||
"jinja2",
|
||||
"parse",
|
||||
"uvloop ; sys_platform != 'win32'",
|
||||
"rfc3986",
|
||||
"python-multipart",
|
||||
]
|
||||
|
||||
|
||||
|
||||
+35
-7
@@ -9,6 +9,19 @@ def api():
|
||||
return responder.API()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def session(api):
|
||||
return api.session()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def url():
|
||||
def url_for(s):
|
||||
return f"http://;{s}"
|
||||
|
||||
return url_for
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def flask():
|
||||
import flask
|
||||
@@ -27,8 +40,8 @@ def schema():
|
||||
class Query(graphene.ObjectType):
|
||||
hello = graphene.String(name=graphene.String(default_value="stranger"))
|
||||
|
||||
def resolve_hello(self, info, name):
|
||||
return "Hello " + name
|
||||
def resolve_hello(self, info, name):
|
||||
return f"Hello {name}"
|
||||
|
||||
return graphene.Schema(query=Query)
|
||||
|
||||
@@ -95,14 +108,14 @@ def test_requests_session(api):
|
||||
assert api.session()
|
||||
|
||||
|
||||
def test_requests_session_works(api):
|
||||
def test_requests_session_works(api, session, url):
|
||||
TEXT = "spiral out"
|
||||
|
||||
@api.route("/")
|
||||
def hello(req, resp):
|
||||
resp.text = TEXT
|
||||
|
||||
assert api.session().get("http://;/").text == TEXT
|
||||
assert session.get(url("/")).text == TEXT
|
||||
|
||||
|
||||
def test_status_code(api):
|
||||
@@ -144,7 +157,7 @@ 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}}
|
||||
assert r.json() == {"data": {"hello": "Hello stranger"}}
|
||||
|
||||
|
||||
def test_argumented_routing(api):
|
||||
@@ -187,12 +200,27 @@ def test_query_params(api):
|
||||
r = api.session().get("http://;/?q=q")
|
||||
assert r.json()["params"] == {"q": "q"}
|
||||
|
||||
r = api.session().get("http://;/?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("/")
|
||||
def route(req, resp):
|
||||
resp.media = {"form": req.media("form")}
|
||||
async def route(req, resp):
|
||||
resp.media = {"form": await req.media("form")}
|
||||
|
||||
dump = {"q": "q"}
|
||||
r = api.session().get("http://;/", data=dump)
|
||||
assert r.json()["form"] == dump
|
||||
|
||||
|
||||
def test_async_function(api, session, url):
|
||||
content = "The Emerald Tablet of Hermes"
|
||||
|
||||
@api.route("/")
|
||||
async def route(req, resp):
|
||||
resp.text = content
|
||||
|
||||
r = session.get(url("/"))
|
||||
assert r.text == content
|
||||
|
||||
Reference in New Issue
Block a user