Compare commits

..

125 Commits

Author SHA1 Message Date
kennethreitz 588e91b19f fix 2018-10-13 09:46:34 -04:00
kennethreitz 8cc2e7b6f1 fix 2018-10-13 09:46:29 -04:00
kennethreitz 222353b532 version 2018-10-13 09:46:14 -04:00
kennethreitz b88b266fd5 profile 2018-10-13 09:40:33 -04:00
kennethreitz 60e6fb99af fix 2018-10-13 09:27:11 -04:00
kennethreitz 16a8402bf4 api.static_url 2018-10-13 09:23:05 -04:00
kennethreitz 0bb74a7885 pytest.ini 2018-10-13 09:13:33 -04:00
kennethreitz 86dfb9231f oops 2018-10-13 09:10:51 -04:00
kennethreitz 7198ce3eb0 fomatting 2018-10-13 09:09:08 -04:00
kennethreitz 08fecf1eb2 Merge branch 'bnm_tests' 2018-10-13 09:08:16 -04:00
kennethreitz 3eda26ca94 Merge branch 'master' into bnm_tests 2018-10-13 09:07:44 -04:00
kennethreitz d907914c7c Merge pull request #25 from 0xflotus/patch-1
fixed Characteristics
2018-10-13 09:02:54 -04:00
kennethreitz 266ab48fed better tests 2018-10-13 09:03:20 -04:00
Luna 3325cffa91 added entry to gitignore 2018-10-13 14:02:17 +01:00
Luna 43469ac62a Merge branch 'master' of https://github.com/kennethreitz/responder into bnm_tests 2018-10-13 14:00:59 +01:00
0xflotus a5c953fdb6 fixed Characteristics 2018-10-13 15:00:38 +02:00
Luna 627c46e458 added test cases for routes.py 2018-10-13 13:59:46 +01:00
kennethreitz 205eb34adc cleanup 2018-10-13 08:51:01 -04:00
kennethreitz 125e14d377 responder 2018-10-13 08:50:23 -04:00
kennethreitz a51c8a700b fix 2018-10-13 08:49:15 -04:00
kennethreitz 94e0400ea1 v0.0.2 2018-10-13 08:42:20 -04:00
kennethreitz 47c5b84093 form parsing working 2018-10-13 08:41:28 -04:00
kennethreitz 8b1fbfd16d better 2018-10-13 08:24:08 -04:00
kennethreitz cceb698899 installing 2018-10-13 08:23:28 -04:00
kennethreitz 01741df10d the cake is a lie 2018-10-13 08:20:38 -04:00
kennethreitz f91ebf8baa Merge branch 'master' of github.com:kennethreitz/responder 2018-10-13 08:19:21 -04:00
kennethreitz 4dde076030 move installation up 2018-10-13 08:19:12 -04:00
kennethreitz 3491001b7f Update README.md 2018-10-13 08:15:00 -04:00
kennethreitz 2acec68649 __slots__ 2018-10-13 08:13:32 -04:00
kennethreitz 51dab27374 index.rst 2018-10-13 08:08:00 -04:00
kennethreitz 145f5041bf options 2018-10-13 08:02:17 -04:00
kennethreitz 6034505380 Merge branch 'master' of github.com:kennethreitz/responder 2018-10-13 08:01:18 -04:00
kennethreitz 8533d74906 5042 2018-10-13 08:01:10 -04:00
kennethreitz b2ae57b982 port 2018-10-13 07:59:16 -04:00
kennethreitz 49ffe9bec9 Merge pull request #20 from aitoehigie/master
Fleshed out the benchmarks section of the README.md file
2018-10-13 07:57:16 -04:00
kennethreitz fe5d92674e google analytics 2018-10-13 07:53:13 -04:00
kennethreitz 197d28f5c7 index 2018-10-13 07:50:29 -04:00
kennethreitz cd48bb0789 update readme 2018-10-13 07:50:09 -04:00
kennethreitz 90fc411e9a : 2018-10-13 07:46:36 -04:00
kennethreitz c22b6a84aa an 2018-10-13 07:45:42 -04:00
kennethreitz 9b65642f05 async 2018-10-13 07:43:57 -04:00
kennethreitz 83547dce9c continuation? 2018-10-13 07:43:09 -04:00
kennethreitz efeecceb54 fix? 2018-10-13 07:41:35 -04:00
kennethreitz ba9b5a40d2 simplify 2018-10-13 07:37:16 -04:00
kennethreitz 47b5bda277 fix 2018-10-13 07:36:43 -04:00
kennethreitz a343b6b1b6 fix 2018-10-13 07:36:30 -04:00
kennethreitz 0fe48d3003 index 2018-10-13 07:33:19 -04:00
kennethreitz 23e3760b08 Merge branch 'master' of github.com:kennethreitz/responder 2018-10-13 07:31:50 -04:00
kennethreitz 3d31905562 improve formats for json 2018-10-13 07:31:45 -04:00
kennethreitz 9638c5266b index.rst 2018-10-13 07:30:56 -04:00
kennethreitz ad7ce9f55a Merge pull request #23 from aaqaishtyaq/docs-badge
add documentation badge in Readme
2018-10-13 07:26:55 -04:00
kennethreitz b0baf3b85a readme 2018-10-13 07:26:18 -04:00
kennethreitz d4d3687882 readme 2018-10-13 07:24:19 -04:00
kennethreitz faf55ca191 readme 2018-10-13 07:21:52 -04:00
kennethreitz d5096a23fb async def 2018-10-13 07:06:55 -04:00
kennethreitz ed5841d201 test async function 2018-10-13 07:06:14 -04:00
kennethreitz bbfc095a00 gitignore 2018-10-13 07:00:53 -04:00
kennethreitz 0fcb68a13d bunk 2018-10-13 07:00:42 -04:00
kennethreitz f97744c098 async defenitions work 2018-10-13 06:59:23 -04:00
kennethreitz d1cfa8d27a safe_load 2018-10-13 05:55:46 -04:00
Aaqa Ishtyaq 218dcf25c1 add documentation badge in Readme 2018-10-13 12:31:18 +05:30
aitoehigie 06e06973a4 format the readme file 2018-10-13 03:24:57 +01:00
aitoehigie 6f73cfc5f2 format the readme file 2018-10-13 03:23:47 +01:00
aitoehigie 6db5bbeaee format the readme file 2018-10-13 03:22:55 +01:00
aitoehigie 6ef5077164 format the readme file 2018-10-13 03:21:53 +01:00
aitoehigie 45e1ed7022 format the readme file 2018-10-13 03:20:36 +01:00
aitoehigie c14b4535a6 format the readme file 2018-10-13 03:19:21 +01:00
aitoehigie 411631d2f8 format the readme file 2018-10-13 03:17:42 +01:00
aitoehigie f4c3690bd8 format the readme file 2018-10-13 03:15:49 +01:00
aitoehigie 56fdea6b5d update the benchmarks section 2018-10-13 03:05:12 +01:00
aitoehigie 8a5c053d39 Add a concise benchmarks section to the README.md file 2018-10-13 02:34:58 +01:00
kennethreitz 42870cfa23 memoize template rendering 2018-10-12 16:56:21 -04:00
kennethreitz 6cf256cc05 memoize routes 2018-10-12 16:48:41 -04:00
kennethreitz 9fec915f62 Merge branch 'master' of github.com:kennethreitz/responder 2018-10-12 16:42:21 -04:00
kennethreitz f1d5ab73cd readme 2018-10-12 16:42:13 -04:00
kennethreitz cd62972945 Merge pull request #14 from taoufik07/feature/query_dict
Add a QueryDict to manage query params
2018-10-12 15:26:05 -04:00
kennethreitz 998d09170c Update README.md 2018-10-12 14:36:03 -04:00
kennethreitz 4ba57181ec Update README.md 2018-10-12 14:31:27 -04:00
kennethreitz 8b9d8bdc62 Update README.md 2018-10-12 14:30:44 -04:00
taoufik07 4291d42dc0 Merge branch 'master' into feature/query_dict 2018-10-12 18:55:17 +01:00
kennethreitz 79fcc1ce40 uvloop 2018-10-12 13:28:07 -04:00
kennethreitz bfc6778dca readme 2018-10-12 13:27:02 -04:00
kennethreitz 701e57c264 fix all the things 2018-10-12 13:25:38 -04:00
kennethreitz 163d025c0d fix tests 2018-10-12 13:15:53 -04:00
kennethreitz d9befc6d8c test 2018-10-12 13:13:13 -04:00
kennethreitz 9e50a4c241 background 2018-10-12 12:28:14 -04:00
kennethreitz 9b0cae3794 background 2018-10-12 12:26:04 -04:00
taoufik07 6160dfb2f7 Add a simple test 2018-10-12 17:01:23 +01:00
taoufik07 cd013cdb06 Add QueryDict 2018-10-12 17:01:06 +01:00
kennethreitz 26cc7c90e9 Merge branch 'asgi' of https://github.com/tomchristie/responder into asgi 2018-10-12 10:29:13 -04:00
kennethreitz f28ac3cf22 Merge pull request #12 from taoufik07/patch-1
Typo : nicities -> niceties
2018-10-12 10:27:37 -04:00
kennethreitz 58fec4b082 Merge pull request #13 from taoufik07/patch-2
Fix resolve_hello indentation
2018-10-12 10:27:26 -04:00
Taoufik b91805a5df Fix graphql test 2018-10-12 15:27:04 +01:00
Tom Christie 0fa0df1bdf Add Pipfile 2018-10-12 15:18:13 +01:00
Tom Christie 3f7cacee3e Updated Pipfile.lock with latest pipenv 2018-10-12 15:12:44 +01:00
Taoufik 72637fd650 FIx resolve_hello indentation and use f-string 2018-10-12 15:12:17 +01:00
Taoufik aba1284f8e Typo : nicities -> niceties 2018-10-12 15:03:49 +01:00
Tom Christie 179e1dc9e5 Update Pipfile.lock 2018-10-12 15:02:08 +01:00
kennethreitz 75879a494e Merge pull request #7 from CianciuStyles/fix-familar-typo
Change "familar" into "familiar"
2018-10-12 09:54:46 -04:00
kennethreitz 73b1ea4713 Merge pull request #10 from OdinTech3/patch-1
Fixed a mispelt word
2018-10-12 09:54:30 -04:00
kennethreitz 55dc991c13 Merge pull request #11 from MichaelPereira/patch-1
Small typo : cooke -> cookie
2018-10-12 09:54:20 -04:00
Tom Christie c30316588a Form parsing requires Starlette 102 2018-10-12 14:53:55 +01:00
Tom Christie db5d6e7481 Lowercase method for on_{method} 2018-10-12 14:47:20 +01:00
Tom Christie f8d52f58d4 Drop form parsing support until Starlette PR #102 is in 2018-10-12 14:36:44 +01:00
Tom Christie 227ee499e4 request.params for API compatability with existing codebase/tests 2018-10-12 14:35:31 +01:00
Michael Pereira dcdaf6a674 Small typo : cooke -> cookie 2018-10-12 09:32:38 -04:00
Tom Christie d524ba3a37 Merge remote-tracking branch 'upstream/master' into asgi 2018-10-12 14:28:33 +01:00
Nick Spirit da5e288476 Fixed a mispelt word 2018-10-12 09:27:52 -04:00
kennethreitz baad7cd60d fix rst 2018-10-12 09:15:03 -04:00
kennethreitz e9d6fc33fd rst 2018-10-12 09:13:53 -04:00
kennethreitz c2fa0899e9 responder 2018-10-12 09:12:34 -04:00
kennethreitz 2dc09ec1f2 build status 2018-10-12 09:09:42 -04:00
kennethreitz fba640976f gzip 2018-10-12 09:04:33 -04:00
kennethreitz 8e7df61a73 fixes 2018-10-12 09:00:52 -04:00
Tom Christie 41776cf2df Initial pass at ASGI support 2018-10-12 13:57:22 +01:00
kennethreitz 23983f0b75 hacktoberfest 2018-10-12 08:55:27 -04:00
kennethreitz 84b457ede5 fix 2018-10-12 08:31:46 -04:00
kennethreitz a906e0bf0c Merge branch 'master' of github.com:kennethreitz/responder 2018-10-12 08:26:28 -04:00
kennethreitz 3db1aad96a hacks 2018-10-12 08:26:20 -04:00
kennethreitz 9c909e7a2c Update README.md 2018-10-12 08:19:38 -04:00
kennethreitz ad2ef7cb33 readme improvements 2018-10-12 08:17:56 -04:00
kennethreitz c851510ca9 fix travis 2018-10-12 08:13:35 -04:00
kennethreitz 71a21c2059 license 2018-10-12 08:12:54 -04:00
kennethreitz d90537eb8d README 2018-10-12 08:12:02 -04:00
Stefano Cianciulli 25e9888438 Change "familar" into "familiar" 2018-10-12 11:00:59 +01:00
26 changed files with 1130 additions and 445 deletions
+11
View File
@@ -1,4 +1,15 @@
.vscode/
.cache
.idea
.coverage
.pytest_cache
.DS_Store
coverage.xml
__pycache__
tests/__pycache__
build
responder.egg-info/
dist/
app.py
-1
View File
@@ -1,7 +1,6 @@
language: python
python:
- "3.6"
- "3.7"
# command to install dependencies
install:
+13
View File
@@ -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.
+4
View File
@@ -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
View File
@@ -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",
+117 -25
View File
@@ -1,10 +1,18 @@
# Responder: a familiar HTTP Service Framework for Python
![](https://github.com/kennethreitz/responder/raw/master/ext/Artboard%201%402x.png)
[![Build Status](https://travis-ci.org/kennethreitz/responder.svg?branch=master)](https://travis-ci.org/kennethreitz/responder)
[![Documentation Status](https://readthedocs.org/projects/mybinder/badge/?version=latest)](https://responder.readthedocs.io/en/latest/)
[![image](https://img.shields.io/pypi/v/responder.svg)](https://pypi.org/project/responder/)
[![image](https://img.shields.io/pypi/l/responder.svg)](https://pypi.org/project/responder/)
[![image](https://img.shields.io/pypi/pyversions/responder.svg)](https://pypi.org/project/responder/)
[![image](https://img.shields.io/github/contributors/kennethreitz/responder.svg)](https://github.com/kennethreitz/responder/graphs/contributors)
[![image](https://img.shields.io/badge/Say%20Thanks-!-1EAEDB.svg)](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.
[![](https://github.com/kennethreitz/responder/raw/master/ext/small.jpg)](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:
$ 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 Characteristics
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! :)
----------
[![hacktoberfest](https://hacktoberfest.digitalocean.com/assets/hacktoberfest-2018-social-card-c8d2e1489f647f2e0a26e6f598adeb760872818905b34cd437afc7ac2857ceab.png)](https://hacktoberfest.digitalocean.com/)
-66
View File
@@ -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)
+12 -14
View File
@@ -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. -->
+162 -54
View File
@@ -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 its in progress! Many features of
Responder are from my wishlist for Flask, and its 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.
The Basic Idea
--------------
Installing Responder
====================
The primary concept here is to bring the nicities that are brought forth from both Flask and Falcon and unify them into a single framework, along with some new ideas I have. I also wanted to take some of the API primitives that are instilled in the Requests library and put them into a web framework. So, you'll find a lot of parallels here with Requests.
- Setting `resp.text` sends back unicode, while setting `resp.content` sends back bytes.
- Setting `resp.media` sends back JSON/YAML (`.text`/`.content` override this).
- Case-insensitive `req.headers` dict (from Requests directly).
- `resp.status_code`, `req.method`, `req.url`, and other familar friends.
New Ideas
---------
- **A built in testing client that uses the actual Requests you know and love**.
- The ability to mount other WSGI apps easily.
- Automatic gzipped-responses (still working on that).
- In addition to Falcon's ``on_get``, ``on_post``, etc methods, Responder features an `on_request` method, which gets called on every type of request, much like Requests.
- WhiteNoise is built-in, for serving static files.
- Waitress built-in as a production web server. I would have chosen Gunicorn, but it doesn't run on Windows. Plus, Waitress serves well to protect against slowloris attacks, making nginx unneccessary in production.
- GraphQL support, via Graphene. The goal here is to have any GraphQL query exposable at any route, magically.
Old Ideas
---------
- Flask-style route expression, with new capabilities -- primarily, the ability to cast a parameter to integers as well as other types that are missing from Flask, all while using Python 3.6+'s new f-string syntax.
- I love Falcon's "every request and response is passed into to each view and mutated" methodology, especially `response.media`, and have used it here. In addition to supporting JSON, I have decided to support YAML as well, as Kubernetes is slowly taking over the world, and it uses YAML for all the things. Content-negotiation and all that.
Future Ideas
------------
- I want to be able to "mount" any WSGI app into a sub-route.
- Cooke-based sessions are currently an afterthrought, as this is an API framework, but websites are APIs too.
- Potentially support ASGI instead of WSGI. Will the tradeoffs be worth it? This is a question to ask. Procedural code works well for 90% use cases.
- If frontend websites are supported, provide an official way to run webpack.
Installation
============
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.
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 familiar friends.
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.
- 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.
Future Ideas
------------
- 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.
API Documentation
=================
+3
View File
@@ -0,0 +1,3 @@
[pytest]
filterwarnings =
error::UserWarning
+1 -1
View File
@@ -1 +1 @@
__version__ = "0.0.1"
__version__ = "0.0.3"
+66 -76
View File
@@ -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
@@ -34,69 +46,44 @@ class API:
self, static_dir="static", templates_dir="templates", enable_hsts=False
):
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.routes = {}
self.hsts_enabled = enable_hsts
self.apps = {"/": self._wsgi_app}
self.static_files = StaticFiles(directory=str(self.static_dir))
self.apps = {self.static_route: 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 +94,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 +108,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 +125,6 @@ class API:
except AssertionError:
# WSGI App.
try:
req.dispatched = True
return view(
environ=req._environ, start_response=req._start_response
)
@@ -150,7 +138,7 @@ class API:
pass
# Then on_get.
method = req.method
method = req.method.lower()
try:
getattr(view, f"on_{method}")(req, resp)
@@ -203,10 +191,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 +236,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,12 +251,10 @@ 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):
def url_for(self, endpoint, testing=False, **params):
# TODO: Absolute_url
"""Given an endpoint, returns a rendered URL for its route.
@@ -276,9 +263,14 @@ class API:
"""
for (route, route_object) in self.routes.items():
if route_object.endpoint == endpoint:
return route_object.url(**params)
return route_object.url(testing=testing, **params)
raise ValueError
def static_url(self, asset):
"""Given a static asset, return its URL path."""
return f"{self.static_route}/{str(asset)}"
@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 +326,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.
@@ -346,13 +338,11 @@ class API:
if "PORT" in os.environ:
if address is None:
address = "0.0.0.0"
port = os.environ["PORT"]
port = int(os.environ["PORT"])
if address is None:
address = "127.0.0.1"
if port is None:
port = 0
port = 5042
bind_to = f"{address}:{port}"
waitress.serve(app=self, listen=bind_to, **kwargs)
uvicorn.run(self, host=address, port=port, **options)
+39
View File
@@ -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
+3 -3
View File
@@ -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
View File
@@ -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):
+19 -2
View File
@@ -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,11 +40,16 @@ 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 {}
def url(self, **params):
return self.route.format(**params)
def url(self, testing=False, **params):
url = self.route.format(**params)
if testing:
url = f"http://;{url}"
return url
# def is_graphql, is_wsgi
+6 -4
View File
@@ -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",
]
+1 -94
View File
@@ -1,96 +1,3 @@
this is a test
Lorem ipsum dolor, sit amet consectetur adipisicing elit. Maiores in, ea beatae praesentium quis enim exercitationem
voluptate repellat possimus laborum provident voluptates numquam id atque tempora. Quidem et repudiandae aliquam?
Lorem ipsum dolor sit amet consectetur adipisicing elit. Libero reiciendis consequuntur deserunt iure nesciunt autem
saepe magnam quas, debitis aliquam molestias possimus necessitatibus cumque enim modi fuga tenetur hic natus?
Lorem ipsum dolor sit amet consectetur adipisicing elit. Quia magni maxime, optio aliquid tempore dignissimos aperiam
voluptatibus, quae sunt vel iste nesciunt. Commodi saepe ipsam architecto omnis neque sequi beatae.
Lorem ipsum, dolor sit amet consectetur adipisicing elit. Neque quam sequi quidem corporis repudiandae quo, fugiat
ullam inventore, ratione cupiditate maiores nobis autem asperiores earum dolorum praesentium quod consequuntur nostrum!
Lorem ipsum dolor sit amet consectetur adipisicing elit. Dolorum, in? Officiis ratione veritatis distinctio quas illo
voluptatibus quia velit corrupti. Tempora ipsam perspiciatis ullam sapiente itaque esse doloribus error culpa.this is a
test
Lorem ipsum dolor, sit amet consectetur adipisicing elit. Maiores in, ea beatae praesentium quis enim exercitationem
voluptate repellat possimus laborum provident voluptates numquam id atque tempora. Quidem et repudiandae aliquam?
Lorem ipsum dolor sit amet consectetur adipisicing elit. Libero reiciendis consequuntur deserunt iure nesciunt autem
saepe magnam quas, debitis aliquam molestias possimus necessitatibus cumque enim modi fuga tenetur hic natus?
Lorem ipsum dolor sit amet consectetur adipisicing elit. Quia magni maxime, optio aliquid tempore dignissimos aperiam
voluptatibus, quae sunt vel iste nesciunt. Commodi saepe ipsam architecto omnis neque sequi beatae.
Lorem ipsum, dolor sit amet consectetur adipisicing elit. Neque quam sequi quidem corporis repudiandae quo, fugiat
ullam inventore, ratione cupiditate maiores nobis autem asperiores earum dolorum praesentium quod consequuntur nostrum!
Lorem ipsum dolor sit amet consectetur adipisicing elit. Dolorum, in? Officiis ratione veritatis distinctio quas illo
voluptatibus quia velit corrupti. Tempora ipsam perspiciatis ullam sapiente itaque esse doloribus error culpa.this is a
test
Lorem ipsum dolor, sit amet consectetur adipisicing elit. Maiores in, ea beatae praesentium quis enim exercitationem
voluptate repellat possimus laborum provident voluptates numquam id atque tempora. Quidem et repudiandae aliquam?
Lorem ipsum dolor sit amet consectetur adipisicing elit. Libero reiciendis consequuntur deserunt iure nesciunt autem
saepe magnam quas, debitis aliquam molestias possimus necessitatibus cumque enim modi fuga tenetur hic natus?
Lorem ipsum dolor sit amet consectetur adipisicing elit. Quia magni maxime, optio aliquid tempore dignissimos aperiam
voluptatibus, quae sunt vel iste nesciunt. Commodi saepe ipsam architecto omnis neque sequi beatae.
Lorem ipsum, dolor sit amet consectetur adipisicing elit. Neque quam sequi quidem corporis repudiandae quo, fugiat
ullam inventore, ratione cupiditate maiores nobis autem asperiores earum dolorum praesentium quod consequuntur nostrum!
Lorem ipsum dolor sit amet consectetur adipisicing elit. Dolorum, in? Officiis ratione veritatis distinctio quas illo
voluptatibus quia velit corrupti. Tempora ipsam perspiciatis ullam sapiente itaque esse doloribus error culpa.this is a
test
Lorem ipsum dolor, sit amet consectetur adipisicing elit. Maiores in, ea beatae praesentium quis enim exercitationem
voluptate repellat possimus laborum provident voluptates numquam id atque tempora. Quidem et repudiandae aliquam?
Lorem ipsum dolor sit amet consectetur adipisicing elit. Libero reiciendis consequuntur deserunt iure nesciunt autem
saepe magnam quas, debitis aliquam molestias possimus necessitatibus cumque enim modi fuga tenetur hic natus?
Lorem ipsum dolor sit amet consectetur adipisicing elit. Quia magni maxime, optio aliquid tempore dignissimos aperiam
voluptatibus, quae sunt vel iste nesciunt. Commodi saepe ipsam architecto omnis neque sequi beatae.
Lorem ipsum, dolor sit amet consectetur adipisicing elit. Neque quam sequi quidem corporis repudiandae quo, fugiat
ullam inventore, ratione cupiditate maiores nobis autem asperiores earum dolorum praesentium quod consequuntur nostrum!
Lorem ipsum dolor sit amet consectetur adipisicing elit. Dolorum, in? Officiis ratione veritatis distinctio quas illo
voluptatibus quia velit corrupti. Tempora ipsam perspiciatis ullam sapiente itaque esse doloribus error culpa.this is a
test
Lorem ipsum dolor, sit amet consectetur adipisicing elit. Maiores in, ea beatae praesentium quis enim exercitationem
voluptate repellat possimus laborum provident voluptates numquam id atque tempora. Quidem et repudiandae aliquam?
Lorem ipsum dolor sit amet consectetur adipisicing elit. Libero reiciendis consequuntur deserunt iure nesciunt autem
saepe magnam quas, debitis aliquam molestias possimus necessitatibus cumque enim modi fuga tenetur hic natus?
Lorem ipsum dolor sit amet consectetur adipisicing elit. Quia magni maxime, optio aliquid tempore dignissimos aperiam
voluptatibus, quae sunt vel iste nesciunt. Commodi saepe ipsam architecto omnis neque sequi beatae.
Lorem ipsum, dolor sit amet consectetur adipisicing elit. Neque quam sequi quidem corporis repudiandae quo, fugiat
ullam inventore, ratione cupiditate maiores nobis autem asperiores earum dolorum praesentium quod consequuntur nostrum!
Lorem ipsum dolor sit amet consectetur adipisicing elit. Dolorum, in? Officiis ratione veritatis distinctio quas illo
voluptatibus quia velit corrupti. Tempora ipsam perspiciatis ullam sapiente itaque esse doloribus error culpa.this is a
test
Lorem ipsum dolor, sit amet consectetur adipisicing elit. Maiores in, ea beatae praesentium quis enim exercitationem
voluptate repellat possimus laborum provident voluptates numquam id atque tempora. Quidem et repudiandae aliquam?
Lorem ipsum dolor sit amet consectetur adipisicing elit. Libero reiciendis consequuntur deserunt iure nesciunt autem
saepe magnam quas, debitis aliquam molestias possimus necessitatibus cumque enim modi fuga tenetur hic natus?
Lorem ipsum dolor sit amet consectetur adipisicing elit. Quia magni maxime, optio aliquid tempore dignissimos aperiam
voluptatibus, quae sunt vel iste nesciunt. Commodi saepe ipsam architecto omnis neque sequi beatae.
Lorem ipsum, dolor sit amet consectetur adipisicing elit. Neque quam sequi quidem corporis repudiandae quo, fugiat
ullam inventore, ratione cupiditate maiores nobis autem asperiores earum dolorum praesentium quod consequuntur nostrum!
Lorem ipsum dolor sit amet consectetur adipisicing elit. Dolorum, in? Officiis ratione veritatis distinctio quas illo
voluptatibus quia velit corrupti. Tempora ipsam perspiciatis ullam sapiente itaque esse doloribus error culpa.
{{ api.static_url('test') }}
+12
View File
@@ -0,0 +1,12 @@
[[source]]
url = "https://pypi.org/simple"
verify_ssl = true
name = "pypi"
[packages]
responder = "*"
[dev-packages]
[requires]
python_version = "3.7"
+225
View File
@@ -0,0 +1,225 @@
{
"_meta": {
"hash": {
"sha256": "bb8daba96850f25605226c489daa0c0af3e0aaeca209896f0783d0576e42ca41"
},
"pipfile-spec": 6,
"requires": {
"python_version": "3.7"
},
"sources": [
{
"name": "pypi",
"url": "https://pypi.org/simple",
"verify_ssl": true
}
]
},
"default": {
"aiofiles": {
"hashes": [
"sha256:021ea0ba314a86027c166ecc4b4c07f2d40fc0f4b3a950d1868a0f2571c2bbee",
"sha256:1e644c2573f953664368de28d2aa4c89dfd64550429d0c27c4680ccd3aa4985d"
],
"version": "==0.4.0"
},
"aniso8601": {
"hashes": [
"sha256:7849749cf00ae0680ad2bdfe4419c7a662bef19c03691a19e008c8b9a5267802",
"sha256:94f90871fcd314a458a3d4eca1c84448efbd200e86f55fe4c733c7a40149ef50"
],
"version": "==3.0.2"
},
"certifi": {
"hashes": [
"sha256:376690d6f16d32f9d1fe8932551d80b23e9d393a8578c5633a2ed39a64861638",
"sha256:456048c7e371c089d0a77a5212fb37a2c2dce1e24146e3b7e0261736aaeaa22a"
],
"version": "==2018.8.24"
},
"chardet": {
"hashes": [
"sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae",
"sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"
],
"version": "==3.0.4"
},
"click": {
"hashes": [
"sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13",
"sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7"
],
"version": "==7.0"
},
"graphene": {
"hashes": [
"sha256:b8ec446d17fa68721636eaad3d6adc1a378cb6323e219814c8f98c9928fc9642",
"sha256:faa26573b598b22ffd274e2fd7a4c52efa405dcca96e01a62239482246248aa3"
],
"version": "==2.1.3"
},
"graphql-core": {
"hashes": [
"sha256:889e869be5574d02af77baf1f30b5db9ca2959f1c9f5be7b2863ead5a3ec6181",
"sha256:9462e22e32c7f03b667373ec0a84d95fba10e8ce2ead08f29fbddc63b671b0c1"
],
"version": "==2.1"
},
"graphql-relay": {
"hashes": [
"sha256:2716b7245d97091af21abf096fabafac576905096d21ba7118fba722596f65db"
],
"version": "==0.4.5"
},
"graphql-server-core": {
"hashes": [
"sha256:e5f82add4b3d5580aa1f1e7d9f00e944ad3abe1b65eb337e611d6a77cc20f231"
],
"version": "==1.1.1"
},
"h11": {
"hashes": [
"sha256:acca6a44cb52a32ab442b1779adf0875c443c689e9e028f8d831a3769f9c5208",
"sha256:f2b1ca39bfed357d1f19ac732913d5f9faa54a5062eca7d2ec3a916cfb7ae4c7"
],
"version": "==0.8.1"
},
"idna": {
"hashes": [
"sha256:156a6814fb5ac1fc6850fb002e0852d56c0c8d2531923a51032d1b70760e186e",
"sha256:684a38a6f903c1d71d6d5fac066b58d7768af4de2b832e426ec79c30daa94a16"
],
"version": "==2.7"
},
"jinja2": {
"hashes": [
"sha256:74c935a1b8bb9a3947c50a54766a969d4846290e1e788ea44c1392163723c3bd",
"sha256:f84be1bb0040caca4cea721fcbbbbd61f9be9464ca236387158b0feea01914a4"
],
"version": "==2.10"
},
"markupsafe": {
"hashes": [
"sha256:a6be69091dac236ea9c6bc7d012beab42010fa914c459791d627dad4910eb665"
],
"version": "==1.0"
},
"parse": {
"hashes": [
"sha256:9dd6048ea212cd032a342f9f6aa2b7bc222f7407c7e37bdc2777fecd36897437"
],
"version": "==1.9.0"
},
"promise": {
"hashes": [
"sha256:2ebbfc10b7abf6354403ed785fe4f04b9dfd421eb1a474ac8d187022228332af",
"sha256:348f5f6c3edd4fd47c9cd65aed03ac1b31136d375aa63871a57d3e444c85655c"
],
"version": "==2.2.1"
},
"python-multipart": {
"hashes": [
"sha256:f7bb5f611fc600d15fa47b3974c8aa16e93724513b49b5f95c81e6624c83fa43"
],
"version": "==0.0.5"
},
"pyyaml": {
"hashes": [
"sha256:3d7da3009c0f3e783b2c873687652d83b1bbfd5c88e9813fb7e5b03c0dd3108b",
"sha256:3ef3092145e9b70e3ddd2c7ad59bdd0252a94dfe3949721633e41344de00a6bf",
"sha256:40c71b8e076d0550b2e6380bada1f1cd1017b882f7e16f09a65be98e017f211a",
"sha256:558dd60b890ba8fd982e05941927a3911dc409a63dcb8b634feaa0cda69330d3",
"sha256:a7c28b45d9f99102fa092bb213aa12e0aaf9a6a1f5e395d36166639c1f96c3a1",
"sha256:aa7dd4a6a427aed7df6fb7f08a580d68d9b118d90310374716ae90b710280af1",
"sha256:bc558586e6045763782014934bfaf39d48b8ae85a2713117d16c39864085c613",
"sha256:d46d7982b62e0729ad0175a9bc7e10a566fc07b224d2c79fafb5e032727eaa04",
"sha256:d5eef459e30b09f5a098b9cea68bebfeb268697f78d647bd255a085371ac7f3f",
"sha256:e01d3203230e1786cd91ccfdc8f8454c8069c91bee3962ad93b87a4b2860f537",
"sha256:e170a9e6fcfd19021dd29845af83bb79236068bf5fd4df3327c1be18182b2531"
],
"version": "==3.13"
},
"requests": {
"hashes": [
"sha256:63b52e3c866428a224f97cab011de738c36aec0185aa91cfacd418b5d58911d1",
"sha256:ec22d826a36ed72a7358ff3fe56cbd4ba69dd7a6718ffd450ff0e9df7a47ce6a"
],
"version": "==2.19.1"
},
"responder": {
"hashes": [
"sha256:56bbc0246e113d2a6f98dbb3f884f305a238e268fda91aceeac3e6470c005fd7",
"sha256:e70820c27adee0aee5fde2a024c7e446bf1d6374863b1a80f6f62c16365b6341"
],
"index": "pypi",
"version": "==0.0.2"
},
"rfc3986": {
"hashes": [
"sha256:632b8fcd2ac37f24334316227f909be4f9d0738cbf409404cff6fa5f69a24093",
"sha256:8458571c4c57e1cf23593ad860bb601b6a604df6217f829c2bc70dc4b5af941b"
],
"version": "==1.1.0"
},
"rx": {
"hashes": [
"sha256:13a1d8d9e252625c173dc795471e614eadfe1cf40ffc684e08b8fff0d9748c23",
"sha256:7357592bc7e881a95e0c2013b73326f704953301ab551fbc8133a6fadab84105"
],
"version": "==1.6.1"
},
"six": {
"hashes": [
"sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9",
"sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb"
],
"version": "==1.11.0"
},
"starlette": {
"hashes": [
"sha256:9f42bba2c3140402df7fe645b79aadc694cca80140d7bdd43b8a7175f84a8a70"
],
"version": "==0.4.1"
},
"urllib3": {
"hashes": [
"sha256:a68ac5e15e76e7e5dd2b8f94007233e01effe3e50e8daddf69acfd81cb686baf",
"sha256:b5725a0bd4ba422ab0e66e89e030c806576753ea3ee08554382c14e685d117b5"
],
"version": "==1.23"
},
"uvicorn": {
"hashes": [
"sha256:8de03999a936d8704f07cc3b1d3a3edb6922a068b64d84b4f5e49604c8b70a11"
],
"version": "==0.3.12"
},
"websockets": {
"hashes": [
"sha256:0e2f7d6567838369af074f0ef4d0b802d19fa1fee135d864acc656ceefa33136",
"sha256:2a16dac282b2fdae75178d0ed3d5b9bc3258dabfae50196cbb30578d84b6f6a6",
"sha256:5a1fa6072405648cb5b3688e9ed3b94be683ce4a4e5723e6f5d34859dee495c1",
"sha256:5c1f55a1274df9d6a37553fef8cff2958515438c58920897675c9bc70f5a0538",
"sha256:669d1e46f165e0ad152ed8197f7edead22854a6c90419f544e0f234cc9dac6c4",
"sha256:695e34c4dbea18d09ab2c258994a8bf6a09564e762655408241f6a14592d2908",
"sha256:6b2e03d69afa8d20253455e67b64de1a82ff8612db105113cccec35d3f8429f0",
"sha256:79ca7cdda7ad4e3663ea3c43bfa8637fc5d5604c7737f19a8964781abbd1148d",
"sha256:7fd2dd9a856f72e6ed06f82facfce01d119b88457cd4b47b7ae501e8e11eba9c",
"sha256:82c0354ac39379d836719a77ee360ef865377aa6fdead87909d50248d0f05f4d",
"sha256:8f3b956d11c5b301206382726210dc1d3bee1a9ccf7aadf895aaf31f71c3716c",
"sha256:91ec98640220ae05b34b79ee88abf27f97ef7c61cf525eec57ea8fcea9f7dddb",
"sha256:952be9540d83dba815569d5cb5f31708801e0bbfc3a8c5aef1890b57ed7e58bf",
"sha256:99ac266af38ba1b1fe13975aea01ac0e14bb5f3a3200d2c69f05385768b8568e",
"sha256:9fa122e7adb24232247f8a89f2d9070bf64b7869daf93ac5e19546b409e47e96",
"sha256:a0873eadc4b8ca93e2e848d490809e0123eea154aa44ecd0109c4d0171869584",
"sha256:cb998bd4d93af46b8b49ecf5a72c0a98e5cc6d57fdca6527ba78ad89d6606484",
"sha256:e02e57346f6a68523e3c43bbdf35dde5c440318d1f827208ae455f6a2ace446d",
"sha256:e79a5a896bcee7fff24a788d72e5c69f13e61369d055f28113e71945a7eb1559",
"sha256:ee55eb6bcf23ecc975e6b47c127c201b913598f38b6a300075f84eeef2d3baff",
"sha256:f1414e6cbcea8d22843e7eafdfdfae3dd1aba41d1945f6ca66e4806c07c4f454"
],
"version": "==6.0"
}
},
"develop": {}
}
+1
View File
@@ -0,0 +1 @@
web: python server.py
+12
View File
@@ -0,0 +1,12 @@
import os
import responder
api = responder.API(enable_hsts=True)
@api.route("/")
def route(req, resp):
resp.text = "hello, world!"
api.run(port=int(os.environ["PORT"]))
View File
View File
+46 -18
View File
@@ -9,9 +9,22 @@ 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
from flask import Flask
app = Flask(__name__)
@@ -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,28 +157,28 @@ 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):
def test_argumented_routing(api, session):
@api.route("/{name}")
def hello(req, resp, *, name):
resp.text = f"Hello, {name}."
r = api.session().get("http://;/sean")
r = session.get(api.url_for(hello, name="sean"))
assert r.text == "Hello, sean."
def test_mote_argumented_routing(api):
def test_mote_argumented_routing(api, session):
@api.route("/{greeting}/{name}")
def hello(req, resp, *, greeting, name):
resp.text = f"{greeting}, {name}."
r = api.session().get("http://;/hello/lyndsy")
r = session.get(api.url_for(hello, greeting="hello", name="lyndsy"))
assert r.text == "hello, lyndsy."
def test_request_and_get(api):
def test_request_and_get(api, session):
@api.route("/")
class ThingsResource:
def on_request(self, req, resp):
@@ -174,25 +187,40 @@ def test_request_and_get(api):
def on_get(self, request, resp):
resp.headers.update({"LIFE": "42"})
r = api.session().get("http://;/")
r = session.get(api.url_for(ThingsResource))
assert "DEATH" in r.headers
assert "LIFE" in r.headers
def test_query_params(api):
def test_query_params(api, url, session):
@api.route("/")
def route(req, resp):
resp.media = {"params": req.params}
r = api.session().get("http://;/?q=q")
r = session.get(api.url_for(route), params={"q": "q"})
assert r.json()["params"] == {"q": "q"}
r = session.get(url("/?q=1&q=2&q=3"))
assert r.json()["params"] == {"q": "3"}
def test_form_data(api):
# Requires https://github.com/encode/starlette/pull/102
def test_form_data(api, session):
@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)
r = session.get(api.url_for(route), data=dump)
assert r.json()["form"] == dump
def test_async_function(api, session):
content = "The Emerald Tablet of Hermes"
@api.route("/")
async def route(req, resp):
resp.text = content
r = session.get(api.url_for(route))
assert r.text == content
+90
View File
@@ -0,0 +1,90 @@
import pytest
from responder import routes
@pytest.mark.parametrize(
"route, expected",
[
pytest.param("/", False, id="home path without params"),
pytest.param("/test_path", False, id="sub path without params"),
pytest.param("/{test_path}", True, id="path with params"),
],
)
def test_parameter(route, expected):
r = routes.Route(route, "test_endpoint")
assert r.has_parameters is expected
def test_url():
r = routes.Route("/{my_path}", "test_endpoint")
url = r.url(my_path="path")
assert url == "/path"
def test_equal():
r = routes.Route("/{path_param}", "test_endpoint")
r2 = routes.Route("/{path_param}", "test_endpoint")
r3 = routes.Route("/test_path", "test_endpoint")
assert r == r2
assert r != r3
@pytest.mark.parametrize(
"path_param, actual, match",
[
pytest.param(
"/{greetings}", "/hello", {"greetings": "hello"}, id="with one strformat"
),
pytest.param(
"/{greetings}.{name}",
"/hi.jane",
{"greetings": "hi", "name": "jane"},
id="with dot in url and two strformat",
),
pytest.param(
"/{greetings}/{name}",
"/hi/john",
{"greetings": "hi", "name": "john"},
id="with sub url and two strformat",
),
pytest.param(
"/concrete_path", "/foo", {}, id="test concrete path with no match"
),
pytest.param(
"/concrete_path",
"/hello",
{"greetings": "hello"},
id="test concrete path with previous match",
),
],
)
def test_incoming_matches(path_param, actual, match):
r = routes.Route(path_param, "test_endpoint")
assert r.incoming_matches(actual) == match
def test_incoming_matches_with_concrete_path_no_match():
r = routes.Route("/concrete_path", "test_endpoint")
assert r.incoming_matches("hello") == {}
@pytest.mark.parametrize(
"route, match, expected",
[
pytest.param(
"/{path_param}",
"/{path_param}",
True,
id="with both parametrized path match",
),
pytest.param(
"/concrete", "/concrete", True, id="with both concrete path match"
),
pytest.param("/concrete", "/{path_param}", True, id="with previous match"),
pytest.param("/concrete", "/no_match", False, id="with no match"),
],
)
def test_does_match_with_route(route, match, expected):
r = routes.Route(route, "test_endpoint")
assert r.does_match(match) == expected