mirror of
https://github.com/kennethreitz/responder.git
synced 2026-06-05 23:00:17 +00:00
Compare commits
58 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 60e163164f | |||
| 86b9b5f3fa | |||
| 401a208767 | |||
| 7d1f991ce4 | |||
| 1b10378f58 | |||
| 2bbb379994 | |||
| a835f119e1 | |||
| 91d8bac680 | |||
| 3db10a4ce8 | |||
| 590640645b | |||
| 7f02bfdf0c | |||
| e5cef0d9c0 | |||
| 85f9c33b2b | |||
| 148a430da4 | |||
| f7657679ac | |||
| f0479019c3 | |||
| a9a4ceaa78 | |||
| c55c905621 | |||
| 4db2289b7e | |||
| 93172ea1d0 | |||
| 2d935542e1 | |||
| a7ec1364f4 | |||
| eb71ced092 | |||
| 712ad0a73b | |||
| 48c0b137d5 | |||
| dfccfcc3e5 | |||
| 6abe667efb | |||
| c2472215ab | |||
| ac3c1e149c | |||
| cdf989427a | |||
| ebf129edd3 | |||
| 08c30f4baf | |||
| cf6bdc20ef | |||
| 3ece644af8 | |||
| 3991c82c91 | |||
| 9b635253f0 | |||
| b62f41336e | |||
| f7b777c79e | |||
| d18fa8e42a | |||
| 525c62ad26 | |||
| 4000a6a48c | |||
| 5b173ed4c4 | |||
| f56ad73565 | |||
| 003991c8c6 | |||
| e2a32afb80 | |||
| f305a69bb3 | |||
| 84e8babd9e | |||
| aeb46d9b54 | |||
| fafe0bd8e4 | |||
| 9a2ab45957 | |||
| 66978a8cdc | |||
| 1636012700 | |||
| 09206ae1e4 | |||
| 9188475746 | |||
| 34d158a632 | |||
| c06e6aa5ca | |||
| f4f670f048 | |||
| 778d742b6e |
@@ -13,3 +13,4 @@ build
|
||||
responder.egg-info/
|
||||
dist/
|
||||
app.py
|
||||
app2.py
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
# v0.0.8
|
||||
- GraphiQL Support.
|
||||
- Improvement to route selection.
|
||||
|
||||
# v0.0.7
|
||||
- Immutable Request object.
|
||||
|
||||
# v0.0.6:
|
||||
- Ability to mount WSGI apps.
|
||||
- Supply content-type when serving up the schema.
|
||||
|
||||
# v0.0.5:
|
||||
- OpenAPI Schema support.
|
||||
- Safe load/dump yaml.
|
||||
|
||||
# v0.0.4:
|
||||
- Asyncronous support for data uploads.
|
||||
- Bug fixes.
|
||||
|
||||
# v0.0.3:
|
||||
- Bug fixes.
|
||||
|
||||
# v0.0.2
|
||||
- Switch to ASGI/Starlette.
|
||||
|
||||
# v0.0.1
|
||||
- Conception!
|
||||
@@ -1,4 +0,0 @@
|
||||
- Kenneth Reitz (primary)
|
||||
- Tom Christie
|
||||
- Bruno Oliveira
|
||||
- serhii73
|
||||
@@ -5,10 +5,6 @@ name = "pypi"
|
||||
|
||||
[packages]
|
||||
responder = {editable = true, path = "."}
|
||||
uvicorn = "*"
|
||||
starlette = "*"
|
||||
aiofiles = "*"
|
||||
docopt = "*"
|
||||
|
||||
[dev-packages]
|
||||
pytest = "*"
|
||||
@@ -17,8 +13,7 @@ black = "*"
|
||||
twine = "*"
|
||||
flask = "*"
|
||||
sphinx = "*"
|
||||
locust = "*"
|
||||
locustio = "*"
|
||||
marshmallow = "*"
|
||||
|
||||
[requires]
|
||||
python_version = "3.7"
|
||||
|
||||
Generated
+47
-134
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "620c4b82d439a27e1afdbe54a6a77130cc52060846d4a6e322a5b064ff68e8fd"
|
||||
"sha256": "9b959d9507c521f6088646507633207db03afec6ac31aeab07adf0d737dbb45b"
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {
|
||||
@@ -21,7 +21,6 @@
|
||||
"sha256:021ea0ba314a86027c166ecc4b4c07f2d40fc0f4b3a950d1868a0f2571c2bbee",
|
||||
"sha256:1e644c2573f953664368de28d2aa4c89dfd64550429d0c27c4680ccd3aa4985d"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.4.0"
|
||||
},
|
||||
"aniso8601": {
|
||||
@@ -31,12 +30,33 @@
|
||||
],
|
||||
"version": "==3.0.2"
|
||||
},
|
||||
"apispec": {
|
||||
"hashes": [
|
||||
"sha256:c2e6ac6471aaf7c6ec6d12714821898910c6b3c87c189de9a2e3754786b86ada",
|
||||
"sha256:fa7dfa8a292bae9b1e70c44a50bf61901805821726c5b804568c9f2501f57ebb"
|
||||
],
|
||||
"version": "==1.0.0b3"
|
||||
},
|
||||
"asgiref": {
|
||||
"hashes": [
|
||||
"sha256:9b05dcd41a6a89ca8c6e7f7e4089c3f3e76b5af60aebb81ae6d455ad81989c97",
|
||||
"sha256:b21dc4c43d7aba5a844f4c48b8f49d56277bc34937fd9f9cb93ec97fde7e3082"
|
||||
],
|
||||
"version": "==2.3.2"
|
||||
},
|
||||
"async-timeout": {
|
||||
"hashes": [
|
||||
"sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f",
|
||||
"sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3"
|
||||
],
|
||||
"version": "==3.0.1"
|
||||
},
|
||||
"certifi": {
|
||||
"hashes": [
|
||||
"sha256:376690d6f16d32f9d1fe8932551d80b23e9d393a8578c5633a2ed39a64861638",
|
||||
"sha256:456048c7e371c089d0a77a5212fb37a2c2dce1e24146e3b7e0261736aaeaa22a"
|
||||
"sha256:339dc09518b07e2fa7eda5450740925974815557727d6bd35d319c1524a04a4c",
|
||||
"sha256:6d58c986d22b038c8c0df30d639f23a3e6d172a05c3583e766f4c0b785c0986a"
|
||||
],
|
||||
"version": "==2018.8.24"
|
||||
"version": "==2018.10.15"
|
||||
},
|
||||
"chardet": {
|
||||
"hashes": [
|
||||
@@ -105,6 +125,13 @@
|
||||
],
|
||||
"version": "==1.0"
|
||||
},
|
||||
"marshmallow": {
|
||||
"hashes": [
|
||||
"sha256:82b201ad767eb54de371c08cb1db6ca4ad2a728fa41b831e3781bf944815eb38",
|
||||
"sha256:c250f37ac0e249a8287394a60d91f6240b674642ad999e66cd09463dbccd1d4f"
|
||||
],
|
||||
"version": "==3.0.0b18"
|
||||
},
|
||||
"parse": {
|
||||
"hashes": [
|
||||
"sha256:9dd6048ea212cd032a342f9f6aa2b7bc222f7407c7e37bdc2777fecd36897437"
|
||||
@@ -168,10 +195,9 @@
|
||||
},
|
||||
"starlette": {
|
||||
"hashes": [
|
||||
"sha256:9f42bba2c3140402df7fe645b79aadc694cca80140d7bdd43b8a7175f84a8a70"
|
||||
"sha256:2c7ec085440fce7146a9be2b6d53b7110c3866ce6fa03d901efdc1fbe97e0f36"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.4.1"
|
||||
"version": "==0.4.2"
|
||||
},
|
||||
"urllib3": {
|
||||
"hashes": [
|
||||
@@ -184,7 +210,6 @@
|
||||
"hashes": [
|
||||
"sha256:8de03999a936d8704f07cc3b1d3a3edb6922a068b64d84b4f5e49604c8b70a11"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.3.12"
|
||||
},
|
||||
"websockets": {
|
||||
@@ -267,10 +292,10 @@
|
||||
},
|
||||
"certifi": {
|
||||
"hashes": [
|
||||
"sha256:376690d6f16d32f9d1fe8932551d80b23e9d393a8578c5633a2ed39a64861638",
|
||||
"sha256:456048c7e371c089d0a77a5212fb37a2c2dce1e24146e3b7e0261736aaeaa22a"
|
||||
"sha256:339dc09518b07e2fa7eda5450740925974815557727d6bd35d319c1524a04a4c",
|
||||
"sha256:6d58c986d22b038c8c0df30d639f23a3e6d172a05c3583e766f4c0b785c0986a"
|
||||
],
|
||||
"version": "==2018.8.24"
|
||||
"version": "==2018.10.15"
|
||||
},
|
||||
"cffi": {
|
||||
"hashes": [
|
||||
@@ -307,7 +332,6 @@
|
||||
"sha256:edabd457cd23a02965166026fd9bfd196f4324fe6032e866d0f3bd0301cd486f",
|
||||
"sha256:fdf1c1dc5bafc32bc5d08b054f94d659422b05aba244d6be4ddc1c72d9aa70fb"
|
||||
],
|
||||
"markers": "sys_platform == 'win32' and platform_python_implementation == 'CPython'",
|
||||
"version": "==1.11.5"
|
||||
},
|
||||
"chardet": {
|
||||
@@ -395,59 +419,6 @@
|
||||
],
|
||||
"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",
|
||||
@@ -475,27 +446,19 @@
|
||||
],
|
||||
"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"
|
||||
],
|
||||
"version": "==1.0"
|
||||
},
|
||||
"marshmallow": {
|
||||
"hashes": [
|
||||
"sha256:82b201ad767eb54de371c08cb1db6ca4ad2a728fa41b831e3781bf944815eb38",
|
||||
"sha256:c250f37ac0e249a8287394a60d91f6240b674642ad999e66cd09463dbccd1d4f"
|
||||
],
|
||||
"version": "==3.0.0b18"
|
||||
},
|
||||
"mccabe": {
|
||||
"hashes": [
|
||||
"sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42",
|
||||
@@ -511,26 +474,6 @@
|
||||
],
|
||||
"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",
|
||||
@@ -608,36 +551,6 @@
|
||||
],
|
||||
"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",
|
||||
@@ -697,10 +610,10 @@
|
||||
},
|
||||
"tqdm": {
|
||||
"hashes": [
|
||||
"sha256:18f1818ce951aeb9ea162ae1098b43f583f7d057b34d706f66939353d1208889",
|
||||
"sha256:df02c0650160986bac0218bb07952245fc6960d23654648b5d5526ad5a4128c9"
|
||||
"sha256:a0be569511161220ff709a5b60d0890d47921f746f1c737a11d965e1b29e7b2e",
|
||||
"sha256:e293e6d7a7f41a529a27f8d6624ab11544ccbfe82a205af6fad102545099fc21"
|
||||
],
|
||||
"version": "==4.26.0"
|
||||
"version": "==4.27.0"
|
||||
},
|
||||
"twine": {
|
||||
"hashes": [
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
[](https://pypi.org/project/responder/)
|
||||
[](https://pypi.org/project/responder/)
|
||||
[](https://github.com/kennethreitz/responder/graphs/contributors)
|
||||
[](https://saythanks.io/to/kennethreitz)
|
||||
|
||||
[](http://python-responder.org/)
|
||||
|
||||
@@ -25,7 +24,7 @@ if __name__ == '__main__':
|
||||
api.run()
|
||||
```
|
||||
|
||||
That `async` declaration is optional.
|
||||
That `async` declaration is optional. [View documentation](http://python-responder.org).
|
||||
|
||||
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.
|
||||
|
||||
@@ -34,15 +33,12 @@ This gets you a ASGI app, with a production static files server pre-installed, j
|
||||
|
||||
> "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/)
|
||||
> "ASGI is going to enable all sorts of new high-performance web services. It's awesome to see Responder starting to take advantage of that." — Tom Christie author of [Django REST Framework](https://www.django-rest-framework.org/)
|
||||
|
||||
> "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):
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
}
|
||||
|
||||
pre,
|
||||
.pre,
|
||||
.class em,
|
||||
.descname,
|
||||
.method em {
|
||||
|
||||
+14
-12
@@ -44,6 +44,18 @@ pre-installed, jinja2 templating (without additional imports), and a
|
||||
production webserver based on uvloop, serving up requests with gzip
|
||||
compression automatically.
|
||||
|
||||
Features
|
||||
--------
|
||||
|
||||
- A pleasant API, with a single import statement.
|
||||
- Class-based views without inheritence.
|
||||
- ASGI framework, the future of Python web services.
|
||||
- The ability to mount any ASGI / WSGI app at a subroute.
|
||||
- *f-string syntax* route declaration.
|
||||
- Mutable response object, passed into each view. No need to return anything.
|
||||
- Background tasks, spawned off in a ``ThreadPoolExecutor``.
|
||||
- GraphQL (with *GraphiQL*) support!
|
||||
|
||||
Testimonials
|
||||
------------
|
||||
|
||||
@@ -57,9 +69,9 @@ Testimonials
|
||||
|
||||
..
|
||||
|
||||
“Buckle up!”
|
||||
"ASGI is going to enable all sorts of new high-performance web services. It's awesome to see Responder starting to take advantage of that."
|
||||
|
||||
—Tom Christie of `APIStar`_ and `Django REST Framework`_
|
||||
—Tom Christie, author of `Django REST Framework`_
|
||||
|
||||
..
|
||||
|
||||
@@ -96,21 +108,11 @@ User Guides
|
||||
Installing Responder
|
||||
--------------------
|
||||
|
||||
Install the latest release:
|
||||
|
||||
.. code-block:: shell
|
||||
|
||||
$ pipenv install 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.
|
||||
|
||||
|
||||
|
||||
@@ -115,7 +115,7 @@ Here, we'll process our data in the background, while responding immediately to
|
||||
|
||||
# Parse the incoming data as form-encoded.
|
||||
# Note: 'json' and 'yaml' formats are also automatically supported.
|
||||
data = await resp.media()
|
||||
data = await req.media()
|
||||
|
||||
# Process the data (in the background).
|
||||
process_data(data)
|
||||
|
||||
@@ -47,6 +47,8 @@ Serve a GraphQL API::
|
||||
|
||||
api.add_route("/graph", graphene.Schema(query=Query))
|
||||
|
||||
Visiting the endpoint will render a *GraphiQL* instance, in the browser.
|
||||
|
||||
|
||||
Built-in Testing Client (Requests)
|
||||
----------------------------------
|
||||
@@ -65,6 +67,78 @@ Or, request YAML back::
|
||||
>>> print(r.text)
|
||||
data: {hello: Hello john}
|
||||
|
||||
OpenAPI Schema Support
|
||||
----------------------
|
||||
|
||||
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.schema("Pet")
|
||||
class PetSchema(Schema):
|
||||
name = fields.Str()
|
||||
|
||||
|
||||
@api.route("/")
|
||||
def route(req, resp):
|
||||
"""A cute furry animal endpoint.
|
||||
---
|
||||
get:
|
||||
description: Get a random pet
|
||||
responses:
|
||||
200:
|
||||
description: A pet to be returned
|
||||
schema:
|
||||
$ref = "#/components/schemas/Pet"
|
||||
"""
|
||||
resp.media = PetSchema().dump({"name": "little orange"})
|
||||
|
||||
|
||||
::
|
||||
|
||||
>>> r = api.session().get("http://;/schema.yml")
|
||||
|
||||
>>> print(r.text)
|
||||
components:
|
||||
parameters: {}
|
||||
schemas:
|
||||
Pet:
|
||||
properties:
|
||||
name: {type: string}
|
||||
type: object
|
||||
info: {title: Web Service, version: 1.0}
|
||||
openapi: '3.0'
|
||||
paths:
|
||||
/:
|
||||
get:
|
||||
description: Get a random pet
|
||||
responses:
|
||||
200: {description: A pet to be returned, schema: $ref = "#/components/schemas/Pet"}
|
||||
tags: []
|
||||
|
||||
|
||||
Mount a WSGI App (e.g. Flask)
|
||||
-----------------------------
|
||||
|
||||
Responder gives you the ability to mount another ASGI / WSGI app at a subroute::
|
||||
|
||||
import responder
|
||||
from flask import Flask
|
||||
|
||||
api = responder.API()
|
||||
flask = Flask(__name__)
|
||||
|
||||
@flask.route('/')
|
||||
def hello():
|
||||
return 'hello'
|
||||
|
||||
api.mount('/flask', flask)
|
||||
|
||||
That's it!
|
||||
|
||||
HSTS (Redirect to HTTPS)
|
||||
------------------------
|
||||
|
||||
@@ -1 +1 @@
|
||||
__version__ = "0.0.4"
|
||||
__version__ = "0.0.8"
|
||||
|
||||
+110
-29
@@ -11,13 +11,17 @@ from graphql_server import encode_execution_results, json_encode, default_format
|
||||
from starlette.routing import Router
|
||||
from starlette.staticfiles import StaticFiles
|
||||
from starlette.testclient import TestClient
|
||||
from apispec import APISpec
|
||||
from apispec.ext.marshmallow import MarshmallowPlugin
|
||||
from apispec import yaml_utils
|
||||
from asgiref.wsgi import WsgiToAsgi
|
||||
|
||||
from . import models
|
||||
from . import status_codes
|
||||
from .routes import Route
|
||||
from .formats import get_formats
|
||||
from .background import BackgroundQueue
|
||||
|
||||
from .templates import GRAPHIQL
|
||||
|
||||
# TODO: consider moving status codes here
|
||||
class API:
|
||||
@@ -31,12 +35,27 @@ class API:
|
||||
status_codes = status_codes
|
||||
|
||||
def __init__(
|
||||
self, static_dir="static", templates_dir="templates", enable_hsts=False
|
||||
self,
|
||||
*,
|
||||
title=None,
|
||||
version=None,
|
||||
openapi=None,
|
||||
openapi_route="/schema.yml",
|
||||
static_dir="static",
|
||||
templates_dir="templates",
|
||||
enable_hsts=False,
|
||||
):
|
||||
self.title = title
|
||||
self.version = version
|
||||
self.openapi_version = openapi
|
||||
self.static_dir = Path(os.path.abspath(static_dir))
|
||||
self.static_route = f"/{static_dir}"
|
||||
self.templates_dir = Path(os.path.abspath(templates_dir))
|
||||
self.built_in_templates_dir = Path(
|
||||
os.path.abspath(os.path.dirname(__file__) + "/templates")
|
||||
)
|
||||
self.routes = {}
|
||||
self.schemas = {}
|
||||
|
||||
self.hsts_enabled = enable_hsts
|
||||
self.static_files = StaticFiles(directory=str(self.static_dir))
|
||||
@@ -52,6 +71,34 @@ class API:
|
||||
self._session = None
|
||||
self.background = BackgroundQueue()
|
||||
|
||||
if self.openapi_version:
|
||||
self.add_route(openapi_route, self.schema_response)
|
||||
|
||||
@property
|
||||
def _apispec(self):
|
||||
spec = APISpec(
|
||||
title=self.title,
|
||||
version=self.version,
|
||||
openapi_version=self.openapi_version,
|
||||
plugins=[MarshmallowPlugin()],
|
||||
)
|
||||
|
||||
for route in self.routes:
|
||||
if self.routes[route].description:
|
||||
operations = yaml_utils.load_operations_from_docstring(
|
||||
self.routes[route].description
|
||||
)
|
||||
spec.add_path(path=route, operations=operations)
|
||||
|
||||
for name, schema in self.schemas.items():
|
||||
spec.definition(name, schema=schema)
|
||||
|
||||
return spec
|
||||
|
||||
@property
|
||||
def openapi(self):
|
||||
return self._apispec.to_yaml()
|
||||
|
||||
def __call__(self, scope):
|
||||
path = scope["path"]
|
||||
root_path = scope.get("root_path", "")
|
||||
@@ -61,7 +108,11 @@ class API:
|
||||
if path.startswith(path_prefix):
|
||||
scope["path"] = path[len(path_prefix) :]
|
||||
scope["root_path"] = root_path + path_prefix
|
||||
return app(scope)
|
||||
try:
|
||||
return app(scope)
|
||||
except TypeError:
|
||||
app = WsgiToAsgi(app)
|
||||
return app(scope)
|
||||
|
||||
# Call the main dispatcher.
|
||||
async def asgi(receive, send):
|
||||
@@ -73,6 +124,32 @@ class API:
|
||||
|
||||
return asgi
|
||||
|
||||
def add_schema(self, name, schema, check_existing=True):
|
||||
"""Adds a mashmallow schema to the API specification."""
|
||||
if check_existing:
|
||||
assert name not in self.schemas
|
||||
|
||||
self.schemas[name] = schema
|
||||
|
||||
def schema(self, name, **options):
|
||||
"""Decorator for creating new routes around function and class definitions.
|
||||
|
||||
Usage::
|
||||
|
||||
from marshmallow import Schema, fields
|
||||
|
||||
@api.schema("Pet")
|
||||
class PetSchema(Schema):
|
||||
name = fields.Str()
|
||||
|
||||
"""
|
||||
|
||||
def decorator(f):
|
||||
self.add_schema(name=name, schema=f, **options)
|
||||
return f
|
||||
|
||||
return decorator
|
||||
|
||||
def path_matches_route(self, path):
|
||||
"""Given a path portion of a URL, tests that it matches against any registered route.
|
||||
|
||||
@@ -106,11 +183,10 @@ class API:
|
||||
view = self.routes[route].endpoint(**params)
|
||||
except TypeError:
|
||||
view = self.routes[route].endpoint
|
||||
try:
|
||||
# GraphQL Schema.
|
||||
assert hasattr(view, "execute")
|
||||
|
||||
if self.routes[route].is_graphql:
|
||||
await self.graphql_response(req, resp, schema=view)
|
||||
except AssertionError:
|
||||
else:
|
||||
# WSGI App.
|
||||
# try:
|
||||
# return view(
|
||||
@@ -139,7 +215,6 @@ class API:
|
||||
return resp
|
||||
|
||||
def add_route(self, route, endpoint, *, check_existing=True):
|
||||
# TODO: add graphiql
|
||||
"""Add a route to the API.
|
||||
|
||||
:param route: A string representation of the route.
|
||||
@@ -148,14 +223,21 @@ class API:
|
||||
"""
|
||||
if check_existing:
|
||||
assert route not in self.routes
|
||||
|
||||
# TODO: Support grpahiql.
|
||||
self.routes[route] = Route(route, endpoint)
|
||||
# TODO: A better datastructer or sort it once the app is loaded
|
||||
self.routes = dict(
|
||||
sorted(self.routes.items(), key=lambda item: item[1]._weight())
|
||||
)
|
||||
|
||||
def default_response(self, req, resp):
|
||||
resp.status_code = status_codes.HTTP_404
|
||||
resp.text = "Not found."
|
||||
|
||||
def schema_response(self, req, resp):
|
||||
resp.status_code = status_codes.HTTP_200
|
||||
resp.headers["Content-Type"] = "application/x-yaml"
|
||||
resp.content = self.openapi
|
||||
|
||||
def redirect(
|
||||
self, resp, location, *, set_text=True, status_code=status_codes.HTTP_301
|
||||
):
|
||||
@@ -197,6 +279,12 @@ class API:
|
||||
return req.text
|
||||
|
||||
async def graphql_response(self, req, resp, schema):
|
||||
show_graphiql = req.method.lower() == "get" and req.accepts("text/html")
|
||||
|
||||
if show_graphiql:
|
||||
resp.content = self.template_string(GRAPHIQL, endpoint=req.url.path)
|
||||
return
|
||||
|
||||
query = await self._resolve_graphql_query(req)
|
||||
result = schema.execute(query)
|
||||
result, status_code = encode_execution_results(
|
||||
@@ -225,16 +313,16 @@ class API:
|
||||
|
||||
return decorator
|
||||
|
||||
def mount(self, route, asgi_app):
|
||||
"""Mounts a WSGI application at a given route.
|
||||
def mount(self, route, app):
|
||||
"""Mounts an WSGI / ASGI application at a given route.
|
||||
|
||||
:param route: String representation of the route to be used (shouldn't be parameterized).
|
||||
:param wsgi_app: The other WSGI app (e.g. a Flask app).
|
||||
:param app: The other WSGI / ASGI app.
|
||||
"""
|
||||
self.apps.update({route: asgi_app})
|
||||
self.apps.update({route: app})
|
||||
|
||||
def session(self, base_url="http://;"):
|
||||
"""Testing HTTP client. Returns a Requests session object, able to send HTTP requests to the WSGI application.
|
||||
"""Testing HTTP client. Returns a Requests session object, able to send HTTP requests to the Responder application.
|
||||
|
||||
:param base_url: The URL to mount the connection adaptor to.
|
||||
"""
|
||||
@@ -271,20 +359,13 @@ class API:
|
||||
# Give reference to self.
|
||||
values.update(api=self)
|
||||
|
||||
if auto_escape:
|
||||
env = jinja2.Environment(
|
||||
loader=jinja2.FileSystemLoader(
|
||||
str(self.templates_dir), followlinks=True
|
||||
),
|
||||
autoescape=jinja2.select_autoescape(["html", "xml"]),
|
||||
)
|
||||
else:
|
||||
env = jinja2.Environment(
|
||||
loader=jinja2.FileSystemLoader(
|
||||
str(self.templates_dir), followlinks=True
|
||||
),
|
||||
autoescape=jinja2.select_autoescape([]),
|
||||
)
|
||||
env = jinja2.Environment(
|
||||
loader=jinja2.FileSystemLoader(
|
||||
[str(self.templates_dir), str(self.built_in_templates_dir)],
|
||||
followlinks=True,
|
||||
),
|
||||
autoescape=jinja2.select_autoescape(["html", "xml"] if auto_escape else []),
|
||||
)
|
||||
|
||||
template = env.get_template(name)
|
||||
return template.render(**values)
|
||||
|
||||
@@ -10,7 +10,7 @@ async def format_form(r, encode=False):
|
||||
async def format_yaml(r, encode=False):
|
||||
if encode:
|
||||
r.headers.update({"Content-Type": "application/x-yaml"})
|
||||
return yaml.dump(r.media)
|
||||
return yaml.safe_dump(r.media)
|
||||
else:
|
||||
return yaml.safe_load(await r.content)
|
||||
|
||||
|
||||
+28
-21
@@ -91,12 +91,7 @@ class Request:
|
||||
__slots__ = [
|
||||
"_starlette",
|
||||
"formats",
|
||||
"headers",
|
||||
"mimetype",
|
||||
"method",
|
||||
"full_url",
|
||||
"url",
|
||||
"params",
|
||||
"_headers",
|
||||
"_encoding",
|
||||
]
|
||||
|
||||
@@ -109,27 +104,39 @@ class Request:
|
||||
for header, value in self._starlette.headers.items():
|
||||
headers[header] = value
|
||||
|
||||
self.headers = (
|
||||
headers
|
||||
) #: A case-insensitive dictionary, containing all headers sent in the Request.
|
||||
self._headers = headers
|
||||
|
||||
self.mimetype = self.headers.get("Content-Type", "")
|
||||
@property
|
||||
def headers(self):
|
||||
"""A case-insensitive dictionary, containing all headers sent in the Request."""
|
||||
return self._headers
|
||||
|
||||
self.method = (
|
||||
self._starlette.method.lower()
|
||||
) #: The incoming HTTP method used for the request, lower-cased.
|
||||
@property
|
||||
def mimetype(self):
|
||||
return self.headers.get("Content-Type", "")
|
||||
|
||||
self.full_url = str(
|
||||
self._starlette.url
|
||||
) #: The full URL of the Request, query parameters and all.
|
||||
@property
|
||||
def method(self):
|
||||
"""The incoming HTTP method used for the request, lower-cased."""
|
||||
return self._starlette.method.lower()
|
||||
|
||||
self.url = rfc3986.urlparse(self.full_url) #: The parsed URL of the Request
|
||||
@property
|
||||
def full_url(self):
|
||||
"""The full URL of the Request, query parameters and all."""
|
||||
return str(self._starlette.url)
|
||||
|
||||
@property
|
||||
def url(self):
|
||||
"""The parsed URL of the Request."""
|
||||
return rfc3986.urlparse(self.full_url)
|
||||
|
||||
@property
|
||||
def params(self):
|
||||
"""A dictionary of the parsed query parameters used for the Request."""
|
||||
try:
|
||||
self.params = QueryDict(
|
||||
self.url.query
|
||||
) #: A dictionary of the parsed query parameters used for the Request.
|
||||
return QueryDict(self.url.query)
|
||||
except AttributeError:
|
||||
self.params = {}
|
||||
return QueryDict({})
|
||||
|
||||
@property
|
||||
async def encoding(self):
|
||||
|
||||
+14
-1
@@ -1,9 +1,10 @@
|
||||
import re
|
||||
from parse import parse, search
|
||||
|
||||
|
||||
def memoize(f):
|
||||
def helper(self, s):
|
||||
memoize_key = f'{f.__name__}:{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]
|
||||
@@ -28,6 +29,10 @@ class Route:
|
||||
# Strings.
|
||||
return self.does_match(other)
|
||||
|
||||
@property
|
||||
def description(self):
|
||||
return self.endpoint.__doc__
|
||||
|
||||
@property
|
||||
def has_parameters(self):
|
||||
return all([("{" in self.route), ("}" in self.route)])
|
||||
@@ -51,3 +56,11 @@ class Route:
|
||||
url = f"http://;{url}"
|
||||
|
||||
return url
|
||||
|
||||
def _weight(self):
|
||||
params_count = -len(set(re.findall(r"{([a-zA-Z]\w*)}", self.route)))
|
||||
return params_count != 0, params_count
|
||||
|
||||
@property
|
||||
def is_graphql(self):
|
||||
return hasattr(self.endpoint, "get_graphql_type")
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
GRAPHIQL = """
|
||||
{% set GRAPHIQL_VERSION = '0.12.0' %}
|
||||
|
||||
<!--
|
||||
* Copyright (c) Facebook, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
-->
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<style>
|
||||
body {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
#graphiql {
|
||||
height: 100vh;
|
||||
}
|
||||
</style>
|
||||
|
||||
<!--
|
||||
This GraphiQL example depends on Promise and fetch, which are available in
|
||||
modern browsers, but can be "polyfilled" for older browsers.
|
||||
GraphiQL itself depends on React DOM.
|
||||
If you do not want to rely on a CDN, you can host these files locally or
|
||||
include them directly in your favored resource bunder.
|
||||
-->
|
||||
<link href="//cdn.jsdelivr.net/npm/graphiql@{{ GRAPHIQL_VERSION }}/graphiql.css" rel="stylesheet"/>
|
||||
<script src="//cdn.jsdelivr.net/npm/whatwg-fetch@2.0.3/fetch.min.js"></script>
|
||||
<script src="//cdn.jsdelivr.net/npm/react@16.2.0/umd/react.production.min.js"></script>
|
||||
<script src="//cdn.jsdelivr.net/npm/react-dom@16.2.0/umd/react-dom.production.min.js"></script>
|
||||
<script src="//cdn.jsdelivr.net/npm/graphiql@{{ GRAPHIQL_VERSION }}/graphiql.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="graphiql">Loading...</div>
|
||||
<script>
|
||||
|
||||
/**
|
||||
* This GraphiQL example illustrates how to use some of GraphiQL's props
|
||||
* in order to enable reading and updating the URL parameters, making
|
||||
* link sharing of queries a little bit easier.
|
||||
*
|
||||
* This is only one example of this kind of feature, GraphiQL exposes
|
||||
* various React params to enable interesting integrations.
|
||||
*/
|
||||
|
||||
// Parse the search string to get url parameters.
|
||||
var search = window.location.search;
|
||||
var parameters = {};
|
||||
search.substr(1).split('&').forEach(function (entry) {
|
||||
var eq = entry.indexOf('=');
|
||||
if (eq >= 0) {
|
||||
parameters[decodeURIComponent(entry.slice(0, eq))] =
|
||||
decodeURIComponent(entry.slice(eq + 1));
|
||||
}
|
||||
});
|
||||
|
||||
// if variables was provided, try to format it.
|
||||
if (parameters.variables) {
|
||||
try {
|
||||
parameters.variables =
|
||||
JSON.stringify(JSON.parse(parameters.variables), null, 2);
|
||||
} catch (e) {
|
||||
// Do nothing, we want to display the invalid JSON as a string, rather
|
||||
// than present an error.
|
||||
}
|
||||
}
|
||||
|
||||
// When the query and variables string is edited, update the URL bar so
|
||||
// that it can be easily shared
|
||||
function onEditQuery(newQuery) {
|
||||
parameters.query = newQuery;
|
||||
updateURL();
|
||||
}
|
||||
|
||||
function onEditVariables(newVariables) {
|
||||
parameters.variables = newVariables;
|
||||
updateURL();
|
||||
}
|
||||
|
||||
function onEditOperationName(newOperationName) {
|
||||
parameters.operationName = newOperationName;
|
||||
updateURL();
|
||||
}
|
||||
|
||||
function updateURL() {
|
||||
var newSearch = '?' + Object.keys(parameters).filter(function (key) {
|
||||
return Boolean(parameters[key]);
|
||||
}).map(function (key) {
|
||||
return encodeURIComponent(key) + '=' +
|
||||
encodeURIComponent(parameters[key]);
|
||||
}).join('&');
|
||||
history.replaceState(null, null, newSearch);
|
||||
}
|
||||
|
||||
// Defines a GraphQL fetcher using the fetch API. You're not required to
|
||||
// use fetch, and could instead implement graphQLFetcher however you like,
|
||||
// as long as it returns a Promise or Observable.
|
||||
function graphQLFetcher(graphQLParams) {
|
||||
// This example expects a GraphQL server at the path /graphql.
|
||||
// Change this to point wherever you host your GraphQL server.
|
||||
return fetch('{{ endpoint }}', {
|
||||
method: 'post',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(graphQLParams),
|
||||
credentials: 'include',
|
||||
}).then(function (response) {
|
||||
return response.text();
|
||||
}).then(function (responseBody) {
|
||||
try {
|
||||
return JSON.parse(responseBody);
|
||||
} catch (error) {
|
||||
return responseBody;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Render <GraphiQL /> into the body.
|
||||
// See the README in the top level of this module to learn more about
|
||||
// how you can customize GraphiQL by providing different values or
|
||||
// additional child elements.
|
||||
ReactDOM.render(
|
||||
React.createElement(GraphiQL, {
|
||||
fetcher: graphQLFetcher,
|
||||
query: parameters.query,
|
||||
variables: parameters.variables,
|
||||
operationName: parameters.operationName,
|
||||
onEditQuery: onEditQuery,
|
||||
onEditVariables: onEditVariables,
|
||||
onEditOperationName: onEditOperationName
|
||||
}),
|
||||
document.getElementById('graphiql')
|
||||
);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
""".strip()
|
||||
@@ -35,6 +35,9 @@ required = [
|
||||
"rfc3986",
|
||||
"python-multipart",
|
||||
"chardet",
|
||||
"apispec>=1.0.0b1",
|
||||
"marshmallow",
|
||||
"asgiref",
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -149,6 +149,15 @@ def test_request_and_get(api, session):
|
||||
assert "LIFE" in r.headers
|
||||
|
||||
|
||||
def test_class_based_view_status_code(api):
|
||||
@api.route("/")
|
||||
class ThingsResource:
|
||||
def on_request(self, req, resp):
|
||||
resp.status_code = responder.status_codes.HTTP_416
|
||||
|
||||
assert api.session().get("http://;/").status_code == responder.status_codes.HTTP_416
|
||||
|
||||
|
||||
def test_query_params(api, url, session):
|
||||
@api.route("/")
|
||||
def route(req, resp):
|
||||
@@ -234,3 +243,108 @@ def test_graphql_schema_json_query(api, schema):
|
||||
|
||||
r = api.session().post("http://;/", json={"query": "{ hello }"})
|
||||
assert r.ok
|
||||
|
||||
def test_graphiql(api, schema):
|
||||
api.add_route("/", schema)
|
||||
|
||||
r = api.session().get("http://;/", headers={"Accept": "text/html"})
|
||||
assert r.ok
|
||||
assert 'GraphiQL' in r.text
|
||||
|
||||
|
||||
def test_json_uploads(api, session):
|
||||
@api.route("/")
|
||||
async def route(req, resp):
|
||||
resp.media = await req.media()
|
||||
|
||||
dump = {"complicated": "times"}
|
||||
r = session.post(api.url_for(route), json=dump)
|
||||
assert r.json() == dump
|
||||
|
||||
|
||||
def test_yaml_uploads(api, session):
|
||||
@api.route("/")
|
||||
async def route(req, resp):
|
||||
resp.media = await req.media()
|
||||
|
||||
dump = {"complicated": "times"}
|
||||
r = session.post(
|
||||
api.url_for(route),
|
||||
data=yaml.dump(dump),
|
||||
headers={"Content-Type": "application/x-yaml"},
|
||||
)
|
||||
assert r.json() == dump
|
||||
|
||||
|
||||
def test_form_uploads(api, session):
|
||||
@api.route("/")
|
||||
async def route(req, resp):
|
||||
resp.media = await req.media()
|
||||
|
||||
dump = {"complicated": "times"}
|
||||
r = session.post(api.url_for(route), data=dump)
|
||||
assert r.json() == dump
|
||||
|
||||
|
||||
def test_json_downloads(api, session):
|
||||
dump = {"testing": "123"}
|
||||
|
||||
@api.route("/")
|
||||
def route(req, resp):
|
||||
resp.media = dump
|
||||
|
||||
r = session.get(api.url_for(route), headers={"Content-Type": "application/json"})
|
||||
assert r.json() == dump
|
||||
|
||||
|
||||
def test_yaml_downloads(api, session):
|
||||
dump = {"testing": "123"}
|
||||
|
||||
@api.route("/")
|
||||
def route(req, resp):
|
||||
resp.media = dump
|
||||
|
||||
r = session.get(api.url_for(route), headers={"Content-Type": "application/x-yaml"})
|
||||
assert yaml.safe_load(r.content) == dump
|
||||
|
||||
|
||||
def test_schema_generation():
|
||||
import responder
|
||||
from marshmallow import Schema, fields
|
||||
|
||||
api = responder.API(title="Web Service", openapi="3.0")
|
||||
|
||||
@api.schema("Pet")
|
||||
class PetSchema(Schema):
|
||||
name = fields.Str()
|
||||
|
||||
@api.route("/")
|
||||
def route(req, resp):
|
||||
"""A cute furry animal endpoint.
|
||||
---
|
||||
get:
|
||||
description: Get a random pet
|
||||
responses:
|
||||
200:
|
||||
description: A pet to be returned
|
||||
schema:
|
||||
$ref = "#/components/schemas/Pet"
|
||||
"""
|
||||
resp.media = PetSchema().dump({"name": "little orange"})
|
||||
|
||||
r = api.session().get("http://;/schema.yml")
|
||||
dump = yaml.safe_load(r.content)
|
||||
|
||||
assert dump
|
||||
assert dump["openapi"] == "3.0"
|
||||
|
||||
|
||||
def test_mount_wsgi_app(api, flask, session):
|
||||
@api.route("/")
|
||||
def hello(req, resp):
|
||||
resp.text = "hello"
|
||||
|
||||
api.mount("/flask", flask)
|
||||
|
||||
r = session.get("http://;/flask")
|
||||
assert r.ok
|
||||
|
||||
Reference in New Issue
Block a user