Compare commits

...

125 Commits

Author SHA1 Message Date
kennethreitz a2eaa5c7b5 Merge branch 'master' of github.com:kennethreitz/responder 2018-10-20 14:46:21 -04:00
kennethreitz 175c46e68c __version__ 2018-10-20 14:46:10 -04:00
kennethreitz a58cc11079 500 support 2018-10-20 14:45:52 -04:00
kennethreitz 218a375c27 test 500s 2018-10-20 14:45:33 -04:00
kennethreitz 567b1577c6 Merge pull request #108 from shyamjos/patch-1
Fixed minor spelling mistakes in changelog
2018-10-20 11:21:11 -07:00
kennethreitz 3c3687d11f Merge pull request #109 from taoufik07/patch-6
clean up
2018-10-20 11:21:00 -07:00
Taoufik 19dfac8340 clean up 2018-10-20 18:37:04 +01:00
kennethreitz b61feafe5a 500 on errrors 2018-10-20 13:36:06 -04:00
Shyam Jos 0c342c8b3e Corrected Spelling
Corrected Spelling
2018-10-20 23:01:57 +05:30
Shyam Jos dbcba8fad7 Fixed minor spelling mistakes in changelog
Fixed minor spelling mistakes in changelog
2018-10-20 22:24:44 +05:30
kennethreitz b8053e20f2 fix 2018-10-20 12:10:59 -04:00
kennethreitz 1896901aa8 Merge branch 'master' of github.com:kennethreitz/responder 2018-10-20 12:10:19 -04:00
kennethreitz 383c9132ed improvement 2018-10-20 12:10:09 -04:00
kennethreitz 57b144c3e7 Merge pull request #107 from taoufik07/patch-5
Refactor Route._weight and f-strings everywhere
2018-10-20 07:08:51 -07:00
kennethreitz eed5365fe0 file upload support 2018-10-20 09:56:35 -04:00
kennethreitz f5905568c4 files support 2018-10-20 09:54:53 -04:00
kennethreitz 096099470e yay tests pass 2018-10-20 08:50:36 -04:00
kennethreitz e7ed7aca3c tests still pass 2018-10-20 08:23:10 -04:00
kennethreitz 6725b275b8 cleanup 2018-10-20 07:59:39 -04:00
kennethreitz 3447a7ef41 v0.1.5 2018-10-20 07:59:12 -04:00
kennethreitz 99f35fbea4 use querydict for form parsing 2018-10-20 07:57:27 -04:00
kennethreitz 5c9a3912a9 cached _content 2018-10-20 07:38:53 -04:00
Taoufik 5d43c0418c f-string 2018-10-19 23:13:21 +01:00
Taoufik 87c0076e12 use f-string
Every time I scroll through the README, it hurts me
2018-10-19 23:10:39 +01:00
Taoufik 95252ac697 Refactor 2018-10-19 23:06:55 +01:00
kennethreitz 5bb9f96701 cleanup 2018-10-19 05:11:07 -07:00
kennethreitz 750e9dfaa7 cleanup 2018-10-19 04:54:49 -07:00
kennethreitz f34f3c1661 v0.1.4 2018-10-19 04:17:06 -07:00
kennethreitz d4f83c978c improvements 2018-10-19 04:16:19 -07:00
kennethreitz 212f280c19 models 2018-10-19 03:10:39 -07:00
kennethreitz f3e2450636 models 2018-10-19 03:09:53 -07:00
kennethreitz d6d496018d fix 2018-10-19 03:08:15 -07:00
kennethreitz 78be7fc772 api 2018-10-19 03:00:41 -07:00
kennethreitz 6ebadd8469 new files 2018-10-19 02:19:38 -07:00
kennethreitz 557750c8d4 customizable cookie 2018-10-18 17:02:10 -07:00
kennethreitz e85ef27e6c Merge pull request #98 from pbsds/master
Store Jinja enviroment in between template render calls
2018-10-18 16:46:31 -07:00
kennethreitz 4ca961a1b4 Merge pull request #104 from metakermit/cli-build
CLI build command
2018-10-18 16:42:17 -07:00
kennethreitz 6a9110e9c1 Merge branch 'master' into cli-build 2018-10-18 16:40:20 -07:00
Dražen Lučanin 51ffce09ae fix cli setup 2018-10-18 23:48:09 +02:00
Peder Bergebakken Sundt 1c4e96b365 Add api.jinja_values_base:dict
This allows the user to add functions and values for use in all
templates, without needing to pass them on each render call.

As a side effect: The reference to `api` is still passed into the template view,
but this now yield to the values passed into api.template(), like one
would normally expect.
2018-10-18 20:47:59 +02:00
Peder Bergebakken Sundt 0db70e8edd Store jinja enviroment in between the template render calls
This allows the user to modify the jinja
enviroment, adding custom filters and such
2018-10-18 20:47:34 +02:00
Peder Bergebakken Sundt e46b3a5e19 Rename s to s_ in api.template_string()
Issue described in #76 applied here as well, however less propable.
Same fix as in a8fc78fcda
2018-10-18 20:47:14 +02:00
kennethreitz fdd3d4d85a sessions 2018-10-18 10:25:19 -07:00
kennethreitz 37c9cba42e version 2018-10-18 10:20:06 -07:00
kennethreitz c1544f66bb tour 2018-10-18 10:14:20 -07:00
kennethreitz d37f41f6a5 docstrings 2018-10-18 10:08:57 -07:00
kennethreitz b245dd2d51 Merge pull request #96 from kennethreitz/sessions
sessions
2018-10-18 10:05:16 -07:00
kennethreitz a1fcf11399 Merge pull request #95 from taoufik07/patch-4
Use HTTPSRedirectMiddleware
2018-10-18 10:04:41 -07:00
kennethreitz 8f876da245 sessions 2018-10-18 10:03:56 -07:00
Taoufik 23b8e5a2b3 Use HTTPSRedirectMiddleware 2018-10-18 18:01:41 +01:00
kennethreitz 3b7e7c7192 Merge pull request #94 from mathiasose/graphql-variables-and-operation-name
Find GraphQL variables, operation name from JSON
2018-10-18 07:49:34 -07:00
Mathias Ose b7ecf6e2e0 Find GraphQL variables, operation name from JSON
Make `_resolve_graphql_query` return *three* things from the JSON query: query (as before), variables and operation names. These values are all passed on to `schema.execute`.

TODO:
- Get variables and operation names from other requests types than JSON.
- Write tests.
- _Possibly_ refactor `_resolve_graphql_query` to return something a bit more structured than a 3-tuple.
2018-10-18 16:09:49 +02:00
kennethreitz 2ec6aaff03 docstrings 2018-10-18 06:42:26 -07:00
kennethreitz 19f8553f2d fix 2018-10-18 04:31:38 -07:00
kennethreitz 05a64ff095 cookies 2018-10-18 04:31:22 -07:00
kennethreitz a8fc78fcda fixes #76 2018-10-18 04:26:25 -07:00
kennethreitz e0e8b40fa2 Merge pull request #91 from kennethreitz/cookies
Cookies
2018-10-18 04:23:16 -07:00
kennethreitz 00165cd6ca tests for cookies 2018-10-18 04:16:44 -07:00
kennethreitz cd799ddfcd cookies 2018-10-18 04:07:13 -07:00
kennethreitz fffd6b7c86 Merge pull request #83 from kennethreitz/bnm_tests
added more tests to routes, and changed some bits in routes regarding regex
2018-10-18 02:51:55 -07:00
kennethreitz 439b008a34 Merge pull request #85 from condemil/patch-1
Add .python-version to .gitignore
2018-10-18 02:51:42 -07:00
kennethreitz f38e538892 Merge pull request #89 from pyasi/tests_for_status_codes
Add basic tests for the status codes file
2018-10-18 02:50:57 -07:00
Peter Yasi 6aa87a073f Add basic tests for the status codes file 2018-10-17 21:25:28 -04:00
Dmitry c38198ccba Add .python-version to .gitignore
.python-version allows to specify separate pyenv virtual environment
2018-10-17 22:58:16 +02:00
Luna 3be88c8cbf removed redundant import in routes.py 2018-10-17 21:17:14 +01:00
Luna 558ced1afb recommented pytest.ini addopts 2018-10-17 21:07:39 +01:00
Luna 0149e6935d added more tests to routes, and changed some bits in routes regarding regex 2018-10-17 21:05:38 +01:00
kennethreitz d97fdfd7c4 Merge pull request #75 from tomchristie/asgi-middleware
Support ASGI middleware
2018-10-17 12:03:15 -07:00
kennethreitz 8b85d8c6fb Merge pull request #80 from taoufik07/fix-CBV-missing-prams
Fix CBV missing params
2018-10-17 12:02:23 -07:00
kennethreitz 673779490c Merge pull request #82 from squiddy/patch-1
Fix docker image typo in deployment documentation
2018-10-17 12:01:43 -07:00
Reiner Gerecke 48154e7e2d Fix docker image typo in deployment documentation 2018-10-17 19:59:40 +02:00
taoufik07 20f72b3f63 Add tests 2018-10-17 18:43:24 +01:00
taoufik07 e82c958af2 Add missing params to on_method 2018-10-17 18:20:44 +01:00
taoufik07 60c311ab9f Add missing params to on_request 2018-10-17 18:20:16 +01:00
Tom Christie fbac81c245 Drop commented out gzip code 2018-10-17 15:13:09 +01:00
Tom Christie 9ca67d9228 Support ASGI middleware 2018-10-17 15:11:16 +01:00
kennethreitz 5ffa18221f an 2018-10-17 06:20:06 -07:00
kennethreitz aceb1f0f61 Must be awaited. 2018-10-17 06:17:21 -07:00
kennethreitz cee5ca8873 v0.1.1 2018-10-17 06:01:41 -07:00
kennethreitz d961d4ab43 default routes 2018-10-17 06:01:27 -07:00
kennethreitz 5205150a89 default route 2018-10-17 05:53:23 -07:00
kennethreitz 48e58cde5d docker 2018-10-17 05:19:22 -07:00
kennethreitz 033e91f8df name 2018-10-17 05:15:25 -07:00
kennethreitz aab3705897 myapi 2018-10-17 05:14:19 -07:00
kennethreitz d02efa81f2 deployment 2018-10-17 05:12:11 -07:00
kennethreitz 95a8240da7 single-page webapps 2018-10-17 04:58:11 -07:00
kennethreitz dd0ddab610 single page 2018-10-17 04:52:02 -07:00
kennethreitz d23ac10f90 version 2018-10-17 04:49:00 -07:00
kennethreitz ec18290b8a changelog 2018-10-17 04:48:38 -07:00
kennethreitz 2c4cd39dc9 static application support 2018-10-17 04:48:33 -07:00
kennethreitz 830bad0b85 docs for #53 2018-10-17 04:47:39 -07:00
kennethreitz f14ef6fa15 #53 2018-10-17 04:45:12 -07:00
kennethreitz 7400b1c83d static support #53 2018-10-17 04:38:51 -07:00
kennethreitz e7caf39fba static_route 2018-10-17 04:25:09 -07:00
kennethreitz 09fd0fb0ca version 2018-10-17 04:19:38 -07:00
kennethreitz 72adb13c0f version 2018-10-17 04:16:22 -07:00
kennethreitz ea0e382f82 test for #71 2018-10-17 04:15:36 -07:00
kennethreitz e70cba5143 Fix for #71 2018-10-17 04:12:13 -07:00
kennethreitz 8aec244c31 openapi 2018-10-17 04:12:03 -07:00
kennethreitz 60e163164f v0.0.8 2018-10-17 03:58:23 -07:00
kennethreitz 86b9b5f3fa graphiql 2018-10-17 03:57:39 -07:00
kennethreitz 401a208767 changelog 2018-10-17 03:27:26 -07:00
kennethreitz a1bfbda05b Merge branch 'master' into cli 2018-10-17 03:15:49 -07:00
kennethreitz 7d1f991ce4 changelog 2018-10-17 02:52:22 -07:00
kennethreitz 1b10378f58 merge 2018-10-17 02:33:49 -07:00
kennethreitz 2bbb379994 Merge pull request #70 from rpost/patch-1
Fix typo
2018-10-17 05:27:59 -04:00
kennethreitz a835f119e1 Merge pull request #67 from goodbadwolf/patch-1
Fix typo in quickstart example
2018-10-17 05:27:47 -04:00
kennethreitz 91d8bac680 Merge pull request #65 from taoufik07/routes_matching
Routes matching for humans
2018-10-17 05:27:32 -04:00
kennethreitz 3db10a4ce8 Merge pull request #63 from pesap/fix-typo
Fix typo in docs
2018-10-17 05:26:19 -04:00
kennethreitz 590640645b Merge pull request #62 from ybv/master
Add status code test for class based view
2018-10-17 05:26:03 -04:00
kennethreitz 7f02bfdf0c Merge pull request #61 from ewjoachim/patch-1
Fix typo
2018-10-17 05:25:49 -04:00
Radek Postołowicz e5cef0d9c0 Fix typo 2018-10-17 10:08:59 +02:00
ArtemGordinsky 85f9c33b2b Integrate GraphiQL 2018-10-17 08:00:03 +02:00
Manish P Mathai 148a430da4 Fix typo in quickstart example 2018-10-16 22:36:54 -07:00
Taoufik f7657679ac A verbose name 2018-10-17 05:07:29 +01:00
taoufik07 f0479019c3 Order the routes based on the weight 2018-10-17 04:38:08 +01:00
taoufik07 a9a4ceaa78 Add weight to Route 2018-10-17 04:37:31 +01:00
pesap c55c905621 Fix typo 2018-10-16 17:23:47 -07:00
ybv 4db2289b7e Add status code rest for class based view 2018-10-16 22:39:09 +05:30
Joachim Jablon 93172ea1d0 Fix typo 2018-10-16 14:41:30 +02:00
kennethreitz 2d935542e1 v0.0.7, immutable response object 2018-10-16 05:24:20 -07:00
kennethreitz f309ad7746 cli 2018-10-16 05:14:48 -07:00
kennethreitz a7ec1364f4 features 2018-10-16 04:42:44 -07:00
kennethreitz eb71ced092 graphql 2018-10-16 04:42:25 -07:00
kennethreitz 712ad0a73b mount a wsgi app 2018-10-16 04:35:30 -07:00
24 changed files with 988 additions and 236 deletions
+1
View File
@@ -1,6 +1,7 @@
.vscode/
.cache
.idea
.python-version
.coverage
.pytest_cache
.DS_Store
+36 -1
View File
@@ -1,3 +1,38 @@
# v0.1.6
- 500 support.
# v0.1.5
- Improvements to sequential media reading.
- File upload support.
# v0.1.4
- Stability.
# v0.1.3
- Sessions support.
# v0.1.2
- Cookies support.
# v0.1.1
- Default routes.
# v0.1.0
- Prototype of static application support.
# v0.0.10
- Bugfix for async class-based views.
# v0.0.9
- Bugfix for async class-based views.
# 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.
@@ -7,7 +42,7 @@
- Safe load/dump yaml.
# v0.0.4:
- Asyncronous support for data uploads.
- Asynchronous support for data uploads.
- Bug fixes.
# v0.0.3:
+1
View File
@@ -14,6 +14,7 @@ twine = "*"
flask = "*"
sphinx = "*"
marshmallow = "*"
pytest-cov = "*"
[requires]
python_version = "3.7"
Generated
+86 -25
View File
@@ -1,7 +1,7 @@
{
"_meta": {
"hash": {
"sha256": "9b959d9507c521f6088646507633207db03afec6ac31aeab07adf0d737dbb45b"
"sha256": "7bbe1f0addd73250027de73d6fb749aa2be3149af9744b107820c5e10498428e"
},
"pipfile-spec": 6,
"requires": {
@@ -72,6 +72,12 @@
],
"version": "==7.0"
},
"docopt": {
"hashes": [
"sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491"
],
"version": "==0.6.2"
},
"graphene": {
"hashes": [
"sha256:b8ec446d17fa68721636eaad3d6adc1a378cb6323e219814c8f98c9928fc9642",
@@ -112,6 +118,13 @@
],
"version": "==2.7"
},
"itsdangerous": {
"hashes": [
"sha256:a7de3201740a857380421ef286166134e10fe58846bcefbc9d6424a69a0b99ec",
"sha256:aca4fc561b7671115a2156f625f2eaa5e0e3527e0adf2870340e7968c0a81f85"
],
"version": "==1.0.0"
},
"jinja2": {
"hashes": [
"sha256:74c935a1b8bb9a3947c50a54766a969d4846290e1e788ea44c1392163723c3bd",
@@ -163,10 +176,17 @@
},
"requests": {
"hashes": [
"sha256:63b52e3c866428a224f97cab011de738c36aec0185aa91cfacd418b5d58911d1",
"sha256:ec22d826a36ed72a7358ff3fe56cbd4ba69dd7a6718ffd450ff0e9df7a47ce6a"
"sha256:99dcfdaaeb17caf6e526f32b6a7b780461512ab3f1d992187801694cba42770c",
"sha256:a84b8c9ab6239b578f22d1c21d51b696dcfe004032bb80ea832398d6909d7279"
],
"version": "==2.19.1"
"version": "==2.20.0"
},
"requests-toolbelt": {
"hashes": [
"sha256:42c9c170abc2cacb78b8ab23ac957945c7716249206f90874651971a4acff237",
"sha256:f6a531936c6fa4c6cfce1b9c10d5c4f498d16528d2a54a22ca00011205a187b5"
],
"version": "==0.8.0"
},
"responder": {
"editable": true,
@@ -195,22 +215,22 @@
},
"starlette": {
"hashes": [
"sha256:2c7ec085440fce7146a9be2b6d53b7110c3866ce6fa03d901efdc1fbe97e0f36"
"sha256:ce5c684fad4edb2967cd491518cd3c2724e420508202c2d48f519ea68dcec9d6"
],
"version": "==0.4.2"
"version": "==0.5.4"
},
"urllib3": {
"hashes": [
"sha256:a68ac5e15e76e7e5dd2b8f94007233e01effe3e50e8daddf69acfd81cb686baf",
"sha256:b5725a0bd4ba422ab0e66e89e030c806576753ea3ee08554382c14e685d117b5"
"sha256:41c3db2fc01e5b907288010dec72f9d0a74e37d6994e6eb56849f59fea2265ae",
"sha256:8819bba37a02d143296a4d032373c4dd4aca11f6d4c9973335ca75f9c8475f59"
],
"version": "==1.23"
"version": "==1.24"
},
"uvicorn": {
"hashes": [
"sha256:8de03999a936d8704f07cc3b1d3a3edb6922a068b64d84b4f5e49604c8b70a11"
"sha256:7c4550c7e6f7c8727fa5ccd5200baf62c9e055895e058933ee88f5d0c246ca0c"
],
"version": "==0.3.12"
"version": "==0.3.14"
},
"websockets": {
"hashes": [
@@ -389,6 +409,38 @@
"markers": "sys_platform == 'win32'",
"version": "==0.4.0"
},
"coverage": {
"hashes": [
"sha256:043d55226aec1d2baf4b2fcab5c204561ccf184a388096f41e396c1c092aff38",
"sha256:10bfd0b80b01d0684f968abbe1186bc19962e07b4b7601bb43b175b617cf689d",
"sha256:17e59864f19b3233032edb0566f26c25cc7f599503fb34d2645b5ce1fd6c2c3c",
"sha256:2105ee183c51fed27e2b6801029b3903f5c2774c78e3f53bd920ca468d0f5679",
"sha256:236505d15af6c7b7bfe2a9485db4b2bdea21d9239351483326184314418c79a8",
"sha256:237284425271db4f30d458b355decf388ab20b05278bdf8dc9a65de0973726c6",
"sha256:26d8eea4c840b73c61a1081d68bceb57b21a2d4f7afda6cac8ac38cb05226b00",
"sha256:39a3740f7721155f4269aedf67b211101c07bd2111b334dfd69b807156ab15d9",
"sha256:4bd0c42db8efc8a60965769796d43a5570906a870bc819f7388860aa72779d1b",
"sha256:4dcddadea47ac30b696956bd18365cd3a86724821656601151e263b86d34798f",
"sha256:51ea341289ac4456db946a25bd644f5635e5ae3793df262813cde875887d25c8",
"sha256:5415cafb082dad78935b3045c2e5d8907f436d15ad24c3fdb8e1839e084e4961",
"sha256:5631f1983074b33c35dbb84607f337b9d7e9808116d7f0f2cb7b9d6d4381d50e",
"sha256:5e9249bc361cd22565fd98590a53fd25a3dd666b74791ed7237fa99de938bbed",
"sha256:6a48746154f1331f28ef9e889c625b5b15a36cb86dd8021b4bdd1180a2186aa5",
"sha256:71d376dbac64855ed693bc1ca121794570fe603e8783cdfa304ec6825d4e768f",
"sha256:749ebd8a615337747592bd1523dfc4af7199b2bf6403b55f96c728668aeff91f",
"sha256:8ec528b585b95234e9c0c31dcd0a89152d8ed82b4567aa62dbcb3e9a0600deee",
"sha256:a1a9ccd879811437ca0307c914f136d6edb85bd0470e6d4966c6397927bcabd9",
"sha256:abd956c334752776230b779537d911a5a12fcb69d8fd3fe332ae63a140301ae6",
"sha256:ad18f836017f2e8881145795f483636564807aaed54223459915a0d4735300cf",
"sha256:b07ac0b1533298ddbc54c9bf3464664895f22899fec027b8d6c8d3ac59023283",
"sha256:d9385f1445e30e8e42b75a36a7899ea1fd0f5784233a626625d70f9b087de404",
"sha256:db2d1fcd32dbeeb914b2660af1838e9c178b75173f95fd221b1f9410b5d3ef1d",
"sha256:e1dec211147f1fd7cb7a0f9a96aeeca467a5af02d38911307b3b8c2324f9917e",
"sha256:e96dffc1fa57bb8c1c238f3d989341a97302492d09cb11f77df031112621c35c",
"sha256:ed4d97eb0ecdee29d0748acd84e6380729f78ce5ba0c7fe3401801634c25a1c5"
],
"version": "==5.0a3"
},
"docutils": {
"hashes": [
"sha256:02aec4bd92ab067f6ff27a38a38a41173bf01bed8f89157768c1573f53e474a6",
@@ -435,9 +487,10 @@
},
"itsdangerous": {
"hashes": [
"sha256:cbb3fcf8d3e33df861709ecaf89d9e6629cff0a217bc2848f1b41cd30d360519"
"sha256:a7de3201740a857380421ef286166134e10fe58846bcefbc9d6424a69a0b99ec",
"sha256:aca4fc561b7671115a2156f625f2eaa5e0e3527e0adf2870340e7968c0a81f85"
],
"version": "==0.24"
"version": "==1.0.0"
},
"jinja2": {
"hashes": [
@@ -490,10 +543,10 @@
},
"pluggy": {
"hashes": [
"sha256:6e3836e39f4d36ae72840833db137f7b7d35105079aee6ec4a62d9f80d594dd1",
"sha256:95eb8364a4708392bae89035f45341871286a333f749c3141c20573d2b3876e1"
"sha256:447ba94990e8014ee25ec853339faf7b0fc8050cdc3289d4d71f7f410fb90095",
"sha256:bde19360a8ec4dfd8a20dcb811780a30998101f078fc7ded6162f0076f50508f"
],
"version": "==0.7.1"
"version": "==0.8.0"
},
"py": {
"hashes": [
@@ -538,11 +591,19 @@
},
"pytest": {
"hashes": [
"sha256:7e258ee50338f4e46957f9e09a0f10fb1c2d05493fa901d113a8dafd0790de4e",
"sha256:9332147e9af2dcf46cd7ceb14d5acadb6564744ddff1fe8c17f0ce60ece7d9a2"
"sha256:10e59f84267370ab20cec9305bafe7505ba4d6b93ecbf66a1cce86193ed511d5",
"sha256:8c827e7d4816dfe13e9329c8226aef8e6e75d65b939bc74fda894143b6d1df59"
],
"index": "pypi",
"version": "==3.8.2"
"version": "==3.9.1"
},
"pytest-cov": {
"hashes": [
"sha256:513c425e931a0344944f84ea47f3956be0e416d95acbd897a44970c8d926d5d7",
"sha256:e360f048b7dae3f2f2a9a4d067b2dd6b6a015d384d1577c994a43f3f7cbad762"
],
"index": "pypi",
"version": "==2.6.0"
},
"pytz": {
"hashes": [
@@ -560,10 +621,10 @@
},
"requests": {
"hashes": [
"sha256:63b52e3c866428a224f97cab011de738c36aec0185aa91cfacd418b5d58911d1",
"sha256:ec22d826a36ed72a7358ff3fe56cbd4ba69dd7a6718ffd450ff0e9df7a47ce6a"
"sha256:99dcfdaaeb17caf6e526f32b6a7b780461512ab3f1d992187801694cba42770c",
"sha256:a84b8c9ab6239b578f22d1c21d51b696dcfe004032bb80ea832398d6909d7279"
],
"version": "==2.19.1"
"version": "==2.20.0"
},
"requests-toolbelt": {
"hashes": [
@@ -625,10 +686,10 @@
},
"urllib3": {
"hashes": [
"sha256:a68ac5e15e76e7e5dd2b8f94007233e01effe3e50e8daddf69acfd81cb686baf",
"sha256:b5725a0bd4ba422ab0e66e89e030c806576753ea3ee08554382c14e685d117b5"
"sha256:41c3db2fc01e5b907288010dec72f9d0a74e37d6994e6eb56849f59fea2265ae",
"sha256:8819bba37a02d143296a4d032373c4dd4aca11f6d4c9973335ca75f9c8475f59"
],
"version": "==1.23"
"version": "==1.24"
},
"webencodings": {
"hashes": [
+1 -1
View File
@@ -86,7 +86,7 @@ class Query(graphene.ObjectType):
hello = graphene.String(name=graphene.String(default_value="stranger"))
def resolve_hello(self, info, name):
return "Hello " + name
return f"Hello {name}"
api.add_route("/graph", graphene.Schema(query=Query))
```
+57
View File
@@ -0,0 +1,57 @@
Deploying Responder
===================
You can deploy Responder anywhere you can deploy a basic Python application.
Docker Deployment
-----------------
Assuming existing ``api.py`` and ``Pipfile.lock`` containing ``responder``.
``Dockerfile``::
from kennethreitz/pipenv
COPY . /app
CMD python3 api.py
That's it!
Heroku Deployment
-----------------
The basics::
$ mkdir my-api
$ cd my-api
$ git init
$ heroku create
...
Install Responder::
$ pipenv install responder
...
Write out an ``api.py``::
import responder
api = responder.API()
@api.route("/")
async def hello(req, resp):
resp.text = "hello, world!"
if __name__ == "__main__":
api.run()
Write out a ``Procfile``::
web: python api.py
That's it! Next, we commit and push to Heroku::
$ git add -A
$ git commit -m 'initial commit'
$ git push heroku master
+6 -1
View File
@@ -48,11 +48,15 @@ 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 declration.
- *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!
- OpenAPI schema generation.
- Single-page webapp support!
Testimonials
------------
@@ -100,6 +104,7 @@ User Guides
quickstart
tour
deployment
api
+1 -1
View File
@@ -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)
+60 -1
View File
@@ -43,10 +43,12 @@ Serve a GraphQL API::
hello = graphene.String(name=graphene.String(default_value="stranger"))
def resolve_hello(self, info, name):
return "Hello " + name
return f"Hello {name}"
api.add_route("/graph", graphene.Schema(query=Query))
Visiting the endpoint will render a *GraphiQL* instance, in the browser.
Built-in Testing Client (Requests)
----------------------------------
@@ -118,6 +120,63 @@ Responder comes with built-in support for OpenAPI / marshmallow::
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!
Single-Page Web Apps
--------------------
If you have a single-page webapp, you can tell Responder to serve up your ``static/index.html`` at a route, like so::
api.add_route("/", static=True)
This will make ``index.html`` the default response to all undefined routes.
Reading / Writing Cookies
-------------------------
Responder makes it very easy to interact with cookies from a Request, or add some to a Response::
>>> resp.cookies["hello"] = "world"
>>> req.cookies
{"hello": "world"}
Using Cookie-Based Sessions
---------------------------
Responder has built-in support for cookie-based sessions. To enable cookie-based sessions, simply add something to the ``resp.session`` dictionary::
>>> resp.session['username'] = 'kennethreitz'
A cookie called ``Responder-Session`` will be set, which contains all the data in ``resp.session``. It is signed, for verification purposes.
You can easily read a Request's session data, that can be trusted to have originated from the API::
>>> req.session
{'username': 'kennethreitz'}
**Note**: if you are using this in production, you should pass the ``secret_key`` argument to ``API(...)``.
HSTS (Redirect to HTTPS)
------------------------
+1 -1
View File
@@ -1,4 +1,4 @@
[pytest]
; addopts= -rsxX -s -v --strict
;addopts= -rsxX -s -v --strict
filterwarnings =
error::UserWarning
+1 -1
View File
@@ -1 +1 @@
__version__ = "0.0.6"
__version__ = "0.1.6"
+190 -93
View File
@@ -7,10 +7,13 @@ import uvicorn
import asyncio
import jinja2
import itsdangerous
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 starlette.middleware.gzip import GZipMiddleware
from starlette.middleware.httpsredirect import HTTPSRedirectMiddleware
from apispec import APISpec
from apispec.ext.marshmallow import MarshmallowPlugin
from apispec import yaml_utils
@@ -21,7 +24,7 @@ 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:
@@ -29,6 +32,7 @@ class API:
:param static_dir: The directory to use for static files. Will be created for you if it doesn't already exist.
:param templates_dir: The directory to use for templates. Will be created for you if it doesn't already exist.
:param auto_escape: If ``True``, HTML and XML templates will automatically be escaped.
:param enable_hsts: If ``True``, send all responses to HTTPS URLs.
"""
@@ -42,17 +46,25 @@ class API:
openapi=None,
openapi_route="/schema.yml",
static_dir="static",
static_route="/static",
templates_dir="templates",
auto_escape=True,
secret_key="NOTASECRET",
enable_hsts=False,
):
self.secret_key = secret_key
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.static_route = static_route
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.session_cookie = "Responder-Session"
self.hsts_enabled = enable_hsts
self.static_files = StaticFiles(directory=str(self.static_dir))
@@ -71,6 +83,22 @@ class API:
if self.openapi_version:
self.add_route(openapi_route, self.schema_response)
self.default_endpoint = None
self.app = self.dispatch
self.add_middleware(GZipMiddleware)
if self.hsts_enabled:
self.add_middleware(HTTPSRedirectMiddleware)
# Jinja enviroment
self.jinja_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 []),
)
self.jinja_values_base = {"api": self} # Give reference to self.
@property
def _apispec(self):
spec = APISpec(
@@ -96,6 +124,9 @@ class API:
def openapi(self):
return self._apispec.to_yaml()
def add_middleware(self, middleware_cls, **middleware_config):
self.app = middleware_cls(self.app, **middleware_config)
def __call__(self, scope):
path = scope["path"]
root_path = scope.get("root_path", "")
@@ -111,11 +142,14 @@ class API:
app = WsgiToAsgi(app)
return app(scope)
return self.app(scope)
def dispatch(self, scope):
# Call the main dispatcher.
async def asgi(receive, send):
nonlocal scope, self
req = models.Request(scope, receive=receive)
req = models.Request(scope, receive=receive, api=self)
resp = await self._dispatch_request(req)
await resp(receive, send)
@@ -156,79 +190,152 @@ class API:
if route_object.does_match(path):
return route
def _prepare_cookies(self, resp):
# print(resp.cookies)
if resp.cookies:
header = " ".join([f"{k}={v}" for k, v in resp.cookies.items()])
resp.headers["Set-Cookie"] = header
@property
def _signer(self):
return itsdangerous.Signer(self.secret_key)
def _prepare_session(self, resp):
if resp.session:
data = self._signer.sign(json.dumps(resp.session).encode("utf-8"))
resp.cookies[self.session_cookie] = data.decode("utf-8")
@staticmethod
def no_response(req, resp, **params):
pass
async def _dispatch_request(self, req):
# Set formats on Request object.
req.formats = self.formats
# Get the route.
route = self.path_matches_route(req.url.path)
resp = models.Response(req=req, formats=self.formats)
route = self.routes.get(route)
params = route.incoming_matches(req.url.path)
if self.hsts_enabled:
if req.url.startswith("http://"):
url = req.url.replace("http://", "https://", 1)
self.redirect(resp, location=url)
# Create the response object.
resp = models.Response(req=req, formats=self.formats)
cont = False
if route:
try:
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 as e:
if route.is_graphql:
await self.graphql_response(req, resp, schema=route.endpoint)
elif route.is_function:
try:
view = self.routes[route].endpoint(**params)
except TypeError:
view = self.routes[route].endpoint
try:
# GraphQL Schema.
assert hasattr(view, "execute")
await self.graphql_response(req, resp, schema=view)
except AssertionError:
# WSGI App.
# try:
# return view(
# environ=req._environ, start_response=req._start_response
# )
# except TypeError:
# pass
pass
# Run the view.
r = route.endpoint(req, resp, **params)
# If it's async, await it.
if hasattr(r, "cr_running"):
await r
except TypeError as e:
cont = True
except Exception:
self.default_response(req, resp, error=True)
if route.is_class_based or cont:
try:
view = route.endpoint(**params)
except TypeError:
view = route.endpoint
# Run on_request first.
try:
getattr(view, "on_request")(req, resp)
except AttributeError:
pass
# Run the view.
r = getattr(view, "on_request", self.no_response)(
req, resp, **params
)
# If it's async, await it.
if hasattr(r, "send"):
await r
except Exception as e:
self.default_response(req, resp, error=True)
# Then on_get.
method = req.method.lower()
method = req.method
# Run on_request first.
try:
getattr(view, f"on_{method}")(req, resp)
except AttributeError:
pass
# Run the view.
r = getattr(view, f"on_{method}", self.no_response)(
req, resp, **params
)
# If it's async, await it.
if hasattr(r, "send"):
await r
except Exception as e:
self.default_response(req, resp, error=True)
else:
self.default_response(req, resp)
self.default_response(req, resp, notfound=True)
self.default_response(req, resp)
self._prepare_session(resp)
self._prepare_cookies(resp)
return resp
def add_route(self, route, endpoint, *, check_existing=True):
# TODO: add graphiql
def add_route(
self, route, endpoint=None, *, default=False, static=False, check_existing=True
):
"""Add a route to the API.
:param route: A string representation of the route.
:param endpoint: The endpoint for the route -- can be a callable, a class, a WSGI application, or graphene schema (GraphQL).
:param endpoint: The endpoint for the route -- can be a callable, a class, or graphene schema (GraphQL).
:param default: If ``True``, all unknown requests will route to this view.
:param static: If ``True``, and no endpoint was passed, render "static/index.html", and it will become a default route.
:param check_existing: If ``True``, an AssertionError will be raised, if the route is already defined.
"""
if check_existing:
assert route not in self.routes
# TODO: Support grpahiql.
self.routes[route] = Route(route, endpoint)
if not endpoint and static:
endpoint = self.static_response
default = True
def default_response(self, req, resp):
resp.status_code = status_codes.HTTP_404
resp.text = "Not found."
if default:
self.default_endpoint = endpoint
try:
if callable(endpoint):
endpoint.is_routed = True
except AttributeError:
pass
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, notfound=False, error=False):
if resp.status_code is None:
resp.status_code = 200
if self.default_endpoint:
self.default_endpoint(req, resp)
else:
if notfound:
resp.status_code = status_codes.HTTP_404
resp.text = "Not found."
if error:
resp.status_code = status_codes.HTTP_500
resp.text = "Application error."
def static_response(self, req, resp):
index = (self.static_dir / "index.html").resolve()
if os.path.exists(index):
with open(index, "r") as f:
resp.text = f.read()
def schema_response(self, req, resp):
resp.status_code = status_codes.HTTP_200
@@ -255,29 +362,44 @@ class API:
@staticmethod
async def _resolve_graphql_query(req):
# TODO: Get variables and operation_name from form data, params, request text?
if "json" in req.mimetype:
return (await req.media("json"))["query"]
json_media = await req.media("json")
return (
json_media["query"],
json_media.get("variables"),
json_media.get("operationName"),
)
# Support query/q in form data.
# Form data is awaiting https://github.com/encode/starlette/pull/102
# if "query" in req.media("form"):
# return req.media("form")["query"]
# return req.media("form")["query"], None, None
# if "q" in req.media("form"):
# return req.media("form")["q"]
# return req.media("form")["q"], None, None
# Support query/q in params.
if "query" in req.params:
return req.params["query"]
return req.params["query"], None, None
if "q" in req.params:
return req.params["q"]
return req.params["q"], None, None
# Otherwise, the request text is used (typical).
# TODO: Make some assertions about content-type here.
return req.text
return req.text, None, None
async def graphql_response(self, req, resp, schema):
query = await self._resolve_graphql_query(req)
result = schema.execute(query)
show_graphiql = req.method == "get" and req.accepts("text/html")
if show_graphiql:
resp.content = self.template_string(GRAPHIQL, endpoint=req.url.path)
return
query, variables, operation_name = await self._resolve_graphql_query(req)
result = schema.execute(
query, variables=variables, operation_name=operation_name
)
result, status_code = encode_execution_results(
[result],
is_batch=False,
@@ -332,65 +454,40 @@ class API:
for (route, route_object) in self.routes.items():
if route_object.endpoint == endpoint:
return route_object.url(testing=testing, **params)
elif route_object.endpoint_name == endpoint:
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)}"
def template(self, name, auto_escape=True, **values):
def template(self, name_, **values):
"""Renders the given `jinja2 <http://jinja.pocoo.org/docs/>`_ template, with provided values supplied.
Note: The current ``api`` instance is always passed into the view.
Note: The current ``api`` instance is by default passed into the view. This is set in the dict ``api.jinja_values_base``.
:param name: The filename of the jinja2 template, in ``templates_dir``.
:param auto_escape: If ``True``, HTML and XML will automatically be escaped.
:param name_: The filename of the jinja2 template, in ``templates_dir``.
:param values: Data to pass into the template.
"""
# Give reference to self.
values.update(api=self)
# Prepopulate values with base
values = {**self.jinja_values_base, **values}
if auto_escape:
env = jinja2.Environment(
loader=jinja2.FileSystemLoader(
str(self.templates_dir), followlinks=True
),
autoescape=jinja2.select_autoescape(["html", "xml"]),
)
else:
env = jinja2.Environment(
loader=jinja2.FileSystemLoader(
str(self.templates_dir), followlinks=True
),
autoescape=jinja2.select_autoescape([]),
)
template = env.get_template(name)
template = self.jinja_env.get_template(name_)
return template.render(**values)
def template_string(self, s, auto_escape=True, **values):
def template_string(self, s_, **values):
"""Renders the given `jinja2 <http://jinja.pocoo.org/docs/>`_ template string, with provided values supplied.
Note: The current ``api`` instance is always passed into the view.
Note: The current ``api`` instance is by default passed into the view. This is set in the dict ``api.jinja_values_base``.
:param s: The template to use.
:param auto_escape: If ``True``, HTML and XML will automatically be escaped.
:param s_: The template to use.
:param values: Data to pass into the template.
"""
# Give reference to self.
values.update(api=self)
# Prepopulate values with base
values = {**self.jinja_values_base, **values}
if auto_escape:
env = jinja2.Environment(
loader=jinja2.BaseLoader,
autoescape=jinja2.select_autoescape(["html", "xml"]),
)
else:
env = jinja2.Environment(
loader=jinja2.BaseLoader, autoescape=jinja2.select_autoescape([])
)
template = env.from_string(s)
template = self.jinja_env.from_string(s_)
return template.render(**values)
def run(self, address=None, port=None, **options):
+43
View File
@@ -0,0 +1,43 @@
"""Responder.
Usage:
responder
responder run [--build] [--debug] <module>
responder build
responder --version
Options:
-h --help Show this screen.
-v --version Show version.
"""
import os
import docopt
from .__version__ import __version__
def cli():
args = docopt.docopt(
__doc__, argv=None, help=True, version=__version__, options_first=False
)
module = args["<module>"]
build = args["build"] or args["--build"]
run = args["run"]
if build:
os.system("npm run build")
if run:
split_module = module.split(":")
if len(split_module) > 1:
module = split_module[0]
prop = split_module[1]
else:
prop = "api"
app = __import__(module)
getattr(app, prop).run()
+1
View File
@@ -1,2 +1,3 @@
from .api import API
from .models import Request, Response
from .cli import cli
+41 -3
View File
@@ -1,10 +1,17 @@
from urllib.parse import parse_qs
import yaml
import json
from parse import findall
from .models import QueryDict
from requests_toolbelt.multipart import decoder
async def format_form(r, encode=False):
if not encode:
return await r._starlette.form()
if encode:
pass
else:
return QueryDict(await r.text)
async def format_yaml(r, encode=False):
@@ -23,5 +30,36 @@ async def format_json(r, encode=False):
return json.loads(await r.content)
async def format_files(r, encode=False):
if encode:
pass
else:
decoded = decoder.MultipartDecoder(await r.content, r.headers["Content-Type"])
dump = {}
for part in decoded.parts:
header = part.headers[b"Content-Disposition"].decode("utf-8")
filename = None
for section in [h.strip() for h in header.split(";")]:
split = section.split("=")
if len(split) > 1:
key = split[0]
value = split[1]
value = value[1:-1]
if key == "filename":
filename = value
if filename:
dump[filename] = part.content
return dump
def get_formats():
return {"json": format_json, "yaml": format_yaml, "form": format_form}
return {
"json": format_json,
"yaml": format_yaml,
"form": format_form,
"files": format_files,
}
+68 -65
View File
@@ -1,17 +1,19 @@
import io
import json
import gzip
from http.cookies import SimpleCookie
import chardet
import rfc3986
import graphene
import yaml
from requests.structures import CaseInsensitiveDict
from requests.cookies import RequestsCookieJar
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
@@ -88,48 +90,73 @@ class QueryDict(dict):
# TODO: add slots
class Request:
__slots__ = [
"_starlette",
"formats",
"headers",
"mimetype",
"method",
"full_url",
"url",
"params",
"_encoding",
]
__slots__ = ["_starlette", "formats", "_headers", "_encoding", "api", "_content"]
def __init__(self, scope, receive):
def __init__(self, scope, receive, api=None):
self._starlette = StarletteRequest(scope, receive)
self.formats = None
self._encoding = None
self.api = api
self._content = None
headers = CaseInsensitiveDict()
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 session(self):
"""The session data, in dict form, from the Request."""
if "Responder-Session" in self.cookies:
data = self.cookies[self.api.session_cookie]
data = self.api._signer.unsign(data)
return json.loads(data)
return {}
self.method = (
self._starlette.method.lower()
) #: The incoming HTTP method used for the request, lower-cased.
@property
def headers(self):
"""A case-insensitive dictionary, containing all headers sent in the Request."""
return self._headers
self.full_url = str(
self._starlette.url
) #: The full URL of the Request, query parameters and all.
@property
def mimetype(self):
return self.headers.get("Content-Type", "")
self.url = rfc3986.urlparse(self.full_url) #: The parsed URL of the Request
@property
def method(self):
"""The incoming HTTP method used for the request, lower-cased."""
return self._starlette.method.lower()
@property
def full_url(self):
"""The full URL of the Request, query parameters and all."""
return str(self._starlette.url)
@property
def url(self):
"""The parsed URL of the Request."""
return rfc3986.urlparse(self.full_url)
@property
def cookies(self):
"""The cookies sent in the Request, as a dictionary."""
cookies = RequestsCookieJar()
cookie_header = self.headers.get("cookie", "")
bc = SimpleCookie(cookie_header)
for k, v in bc.items():
cookies[k] = v
return cookies.get_dict()
@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):
@@ -153,12 +180,14 @@ class Request:
@property
async def content(self):
"""The Request body, as bytes. Must be awaited."""
return await self._starlette.body()
if not self._content:
self._content = await self._starlette.body()
return self._content
@property
async def text(self):
"""The Request body, as unicode. Must be awaited."""
return (await self._starlette.body()).decode(await self.encoding)
return (await self.content).decode(await self.encoding)
@property
async def declared_encoding(self):
@@ -184,7 +213,7 @@ class Request:
return content_type in self.headers.get("Accept", [])
async def media(self, format=None):
"""Renders incoming json/yaml/form data as Python objects.
"""Renders incoming json/yaml/form data as Python objects. Must be awaited.
:param format: The name of the format being used. Alternatively accepts a custom callable for the format type.
"""
@@ -209,11 +238,13 @@ class Response:
"media",
"headers",
"formats",
"cookies",
"session",
]
def __init__(self, req, *, formats):
self.req = req
self.status_code = HTTP_200 #: The HTTP Status Code to use for the Response.
self.status_code = None #: The HTTP Status Code to use for the Response.
self.text = None #: A unicode representation of the response body.
self.content = None #: A bytes representation of the response body.
self.encoding = DEFAULT_ENCODING
@@ -222,8 +253,12 @@ class Response:
) #: A Python object that will be content-negotiated and sent back to the client. Typically, in JSON formatting.
self.headers = (
{}
) #: A Python dictionary of {Key: value}, representing the headers of the response.
) #: A Python dictionary of ``{key: value}``, representing the headers of the response.
self.formats = formats
self.cookies = {} #: The cookies set in the Response, as a dictionary
self.session = (
req.session.copy()
) #: The cookie-based session data, in dict form, to add to the Response.
@property
async def body(self):
@@ -243,35 +278,8 @@ class Response:
{"Content-Type": "application/json"},
)
@property
async def gzipped_body(self):
body, headers = await self.body
if isinstance(body, str):
body = body.encode(self.encoding)
if "gzip" in self.req.headers["Accept-Encoding"].lower():
gzip_buffer = io.BytesIO()
gzip_file = gzip.GzipFile(mode="wb", fileobj=gzip_buffer)
gzip_file.write(body)
gzip_file.close()
new_headers = {
"Content-Encoding": "gzip",
"Vary": "Accept-Encoding",
"Content-Length": str(len(body)),
}
headers.update(new_headers)
return (gzip_buffer.getvalue(), headers)
else:
return (body, headers)
async def __call__(self, receive, send):
body, headers = await self.body
if len(await self.body) > 500:
body, headers = await self.gzipped_body
if self.headers:
headers.update(self.headers)
@@ -279,8 +287,3 @@ class Response:
body, status_code=self.status_code, headers=headers
)
await response(receive, send)
class Schema(graphene.Schema):
def on_request(self, req, resp):
pass
+28 -2
View File
@@ -1,4 +1,5 @@
from parse import parse, search
import re
from parse import parse
def memoize(f):
@@ -12,6 +13,8 @@ def memoize(f):
class Route:
_param_pattern = re.compile(r"{([^{}]*)}")
def __init__(self, route, endpoint):
self.route = route
self.endpoint = endpoint
@@ -28,13 +31,17 @@ class Route:
# Strings.
return self.does_match(other)
@property
def endpoint_name(self):
return self.endpoint.__name__
@property
def description(self):
return self.endpoint.__doc__
@property
def has_parameters(self):
return all([("{" in self.route), ("}" in self.route)])
return bool(self._param_pattern.search(self.route))
@memoize
def does_match(self, s):
@@ -55,3 +62,22 @@ class Route:
url = f"http://;{url}"
return url
def _weight(self):
params = set(self._param_pattern.findall(self.route))
params_count = len(params)
return params_count != 0, -params_count
@property
def is_graphql(self):
return hasattr(self.endpoint, "get_graphql_type")
@property
def is_class_based(self):
return hasattr(self.endpoint, "__class__")
def is_function(self):
routed = hasattr(self.endpoint, "is_routed")
code = hasattr(self.endpoint, "__code__")
kwdefaults = hasattr(self.endpoint, "__kwdefaults__")
return all((routed, code, kwdefaults))
+145
View File
@@ -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()
+4 -3
View File
@@ -38,6 +38,9 @@ required = [
"apispec>=1.0.0b1",
"marshmallow",
"asgiref",
"docopt",
"itsdangerous",
"requests-toolbelt",
]
@@ -117,9 +120,7 @@ setup(
author_email="me@kennethreitz.org",
url="https://github.com/kennethreitz/responder",
packages=find_packages(exclude=["tests"]),
# entry_points={
# "console_scripts": ["responder=responder:cli"]
# },
entry_points={"console_scripts": ["responder=responder.cli:cli"]},
package_data={
# "": ["LICENSE", "NOTICES"],
# "pipenv.vendor.requests": ["*.pem"],
+1
View File
@@ -0,0 +1 @@
lorem
-12
View File
@@ -22,15 +22,3 @@ def test_bytes_encoding(api, session):
r = session.get(api.url_for(route), data=data)
assert r.content == data
def test_false_encoding_raises(api, session):
data = "hi mom!"
@api.route("/")
async def route(req, resp):
req.encoding = "non-existient"
resp.text = await req.text
with pytest.raises(LookupError):
session.get(api.url_for(route), data=data)
+101
View File
@@ -1,6 +1,7 @@
import pytest
import yaml
import responder
import io
def test_api_basic_route(api):
@@ -61,6 +62,15 @@ def test_class_based_view_registration(api):
resp.text = "42"
def test_class_based_view_parameters(api):
@api.route("/{greeting}")
class Greeting:
def on_request(req, resp, *, greeting):
resp.text = f"{greeting}, world!"
assert api.session().get("http://;/Hello").ok
def test_requests_session(api):
assert api.session()
@@ -149,6 +159,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):
@@ -236,6 +255,14 @@ def test_graphql_schema_json_query(api, schema):
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):
@@ -332,3 +359,77 @@ def test_mount_wsgi_app(api, flask, session):
r = session.get("http://;/flask")
assert r.ok
def test_async_class_based_views(api, session):
@api.route("/")
class Resource:
async def on_post(self, req, resp):
resp.text = await req.text
data = "frame"
r = session.post(api.url_for(Resource), data=data)
assert r.text == data
def test_cookies(api, session):
@api.route("/")
def cookies(req, resp):
resp.media = {"cookies": req.cookies}
resp.cookies["sent"] = "true"
r = session.get(api.url_for(cookies), cookies={"hello": "universe"})
assert r.json() == {"cookies": {"hello": "universe"}}
assert "sent" in r.cookies
r = session.get(api.url_for(cookies))
assert r.json() == {"cookies": {"sent": "true"}}
def test_sessions(api, session):
@api.route("/")
def view(req, resp):
resp.session["hello"] = "world"
resp.media = resp.session
r = session.get(api.url_for(view))
assert "Responder-Session" in r.cookies
r = session.get(api.url_for(view))
assert (
r.cookies["Responder-Session"]
== '{"hello": "world"}.lJVWJULPqR9kdao_oT4pUglV281bxHfGvcKQ7XF8qNqaiIZlRcMvqKNdA1-d5z7DycAx5eqmzJZoqWPP759-Cw'
)
assert r.json() == {"hello": "world"}
def test_template_rendering(api, session):
@api.route("/")
def view(req, resp):
resp.content = api.template_string("{{ var }}", var="hello")
r = session.get(api.url_for(view))
assert r.text == "hello"
def test_file_uploads(api, session):
@api.route("/")
async def upload(req, resp):
files = await req.media("files")
files["hello"] = files["hello"].decode("utf-8")
resp.media = {"files": files}
world = io.StringIO("world")
data = {"hello": world}
r = session.post(api.url_for(upload), files=data)
assert r.json() == {"files": {"hello": "world"}}
def test_500(api, session):
@api.route("/")
def view(rea, resp):
raise ValueError
r = session.get(api.url_for(view))
assert not r.ok
+48 -26
View File
@@ -30,32 +30,32 @@ def test_equal():
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"
),
],
)
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():
# Test Route with one param
r = routes.Route("/{greetings}", "test_endpoint")
assert r.incoming_matches("/hello") == {"greetings": "hello"}
assert r.incoming_matches("/foo") == {"greetings": "foo"}
assert r._memo == {
"incoming_matches:/hello": {"greetings": "hello"},
"incoming_matches:/foo": {"greetings": "foo"},
}
# Test Route with two params
r = routes.Route("/{greetings}/{name}", "test_endpoint")
assert r.incoming_matches("/hi/john") == {"greetings": "hi", "name": "john"}
assert r.incoming_matches("/hello/jane") == {"greetings": "hello", "name": "jane"}
# Test Route with no param
assert r._memo == {
"incoming_matches:/hi/john": {"greetings": "hi", "name": "john"},
"incoming_matches:/hello/jane": {"greetings": "hello", "name": "jane"},
}
r = routes.Route("/hello", "test_endpoint")
assert r.incoming_matches("/hello") == {}
assert r.incoming_matches("/bye") == {}
assert r._memo == {"incoming_matches:/hello": {}, "incoming_matches:/bye": {}}
def test_incoming_matches_with_concrete_path_no_match():
@@ -81,3 +81,25 @@ def test_incoming_matches_with_concrete_path_no_match():
def test_does_match_with_route(route, match, expected):
r = routes.Route(route, "test_endpoint")
assert r.does_match(match) == expected
@pytest.mark.parametrize(
"path_param, expected_weight",
[
pytest.param("/{greetings}", (True, -1), id="with one param"),
pytest.param(
"/{greetings}.{name}", (True, -2), id="with 2 params and dot in the middle"
),
pytest.param("/{greetings}/{name}", (True, -2), id="with 2 param and subpath"),
pytest.param(
"/{greetings}/{name}/{hello}", (True, -3), id="with 3 param and subpath"
),
pytest.param(
"/{greetings}_{name}", (True, -2), id="with 2 param and underscore"
),
pytest.param("/hello", (False, 0), id="with 2 param and underscore"),
],
)
def test_weight(path_param, expected_weight):
r = routes.Route(path_param, "test_endpoint")
assert r._weight() == expected_weight
+67
View File
@@ -0,0 +1,67 @@
import pytest
from responder import status_codes
@pytest.mark.parametrize(
"status_code, expected",
[
pytest.param(101, True, id="Normal 101"),
pytest.param(199, True, id="Not actual status code but within 100"),
pytest.param(0, False, id="Zero case (below 100)"),
pytest.param(200, False, id="Above 100")
],
)
def test_is_100(status_code, expected):
assert status_codes.is_100(status_code) is expected
@pytest.mark.parametrize(
"status_code, expected",
[
pytest.param(201, True, id="Normal 201"),
pytest.param(299, True, id="Not actual status code but within 200"),
pytest.param(0, False, id="Zero case (below 200)"),
pytest.param(300, False, id="Above 200")
],
)
def test_is_200(status_code, expected):
assert status_codes.is_200(status_code) is expected
@pytest.mark.parametrize(
"status_code, expected",
[
pytest.param(301, True, id="Normal 301"),
pytest.param(399, True, id="Not actual status code but within 300"),
pytest.param(0, False, id="Zero case (below 300)"),
pytest.param(400, False, id="Above 300")
],
)
def test_is_300(status_code, expected):
assert status_codes.is_300(status_code) is expected
@pytest.mark.parametrize(
"status_code, expected",
[
pytest.param(401, True, id="Normal 401"),
pytest.param(499, True, id="Not actual status code but within 400"),
pytest.param(0, False, id="Zero case (below 400)"),
pytest.param(500, False, id="Above 400")
],
)
def test_is_400(status_code, expected):
assert status_codes.is_400(status_code) is expected
@pytest.mark.parametrize(
"status_code, expected",
[
pytest.param(501, True, id="Normal 401"),
pytest.param(599, True, id="Not actual status code but within 400"),
pytest.param(0, False, id="Zero case (below 400)"),
pytest.param(600, False, id="Above 500")
],
)
def test_is_500(status_code, expected):
assert status_codes.is_500(status_code) is expected