Compare commits

...

129 Commits

Author SHA1 Message Date
kennethreitz be56e92d65 Merge pull request #277 from amikrop/master
Make `_route_for` shorter
2019-01-12 07:06:31 -05:00
kennethreitz 69eb843604 fixes 2019-01-12 06:57:28 -05:00
Aristotelis Mikropoulos 84a7f0e90b Make _route_for shorter 2019-01-11 03:46:35 +02:00
kennethreitz d1e105a29a Merge pull request #276 from tomchristie/resolve-startup-and-shutdown-events
Resolve startup/shutdown events
2019-01-09 17:49:49 -05:00
Tom Christie 9f0a568fa3 Resolve startup/shutdwown events 2019-01-09 12:42:15 +00:00
kennethreitz 05b46cbe34 Merge pull request #269 from erm/lifespan-handler
Return lifespan handler in dispatch method
2018-12-29 07:05:41 -05:00
kennethreitz c045586997 Merge pull request #267 from valtyr/graphql-context
Add Request and Response to GraphQL context
2018-12-29 07:05:26 -05:00
kennethreitz 8f0707f697 Merge pull request #262 from carlodri/carlodri-patch-1
include LICENSE il sdist
2018-12-29 07:05:12 -05:00
kennethreitz 36929b265c Merge pull request #263 from tkamenoko/patch-3
fix docstring of `API.url_for`
2018-12-29 07:05:05 -05:00
kennethreitz 734ba64965 Merge pull request #264 from sangheestyle/master
fix typo
2018-12-29 07:04:53 -05:00
kennethreitz 148e6742df Merge pull request #268 from taoufik07/patch-18
Websocket docs
2018-12-29 07:04:45 -05:00
Jordan bcb7e8f4f3 Update lock file 2018-12-28 18:10:27 +11:00
Jordan f678112099 Return lifespan handler in dispatch method 2018-12-28 17:46:27 +11:00
Taoufik 60b0c5f256 Update 2018-12-25 01:32:38 +00:00
Taoufik c8627939de Update 2018-12-25 01:18:44 +00:00
Taoufik 9144f0158a Websocket docs 2018-12-25 01:15:54 +00:00
Valtýr Örn Kjartansson d541aca80f Mention the GraphQL context object in docs 2018-12-23 13:28:50 +00:00
Valtýr Örn Kjartansson c73b2b8d34 Add request and response to GraphQL context
This allows the GraphQL resolvers access to things like session, headers etc.
Also enables creation of powerful GraphQL middleware.
2018-12-23 13:14:15 +00:00
Kim, Sanghee e2493b489d fix typo 2018-12-20 17:54:15 +02:00
T.Kameyama 8dee28ac7c fix docstring of API.url_for 2018-12-17 19:23:33 +09:00
Carlo cdd3885a0c include LICENSE il sdist 2018-12-12 22:35:41 +01:00
kennethreitz 1a28d528d0 Merge pull request #233 from taoufik07/websocket-x.x
WebSocket returns
2018-12-12 03:59:24 -05:00
kennethreitz 3ba12b8cee Merge pull request #230 from tkamenoko/file-form
formats: format_file returns filename, data, and content-type [Breaking]
2018-12-12 03:59:10 -05:00
kennethreitz 5a29ab6917 Merge pull request #257 from ibnesayeed/lifespan
Import lifespan as a middleware as per the change in starlette package
2018-12-12 03:58:53 -05:00
kennethreitz 694144a0c8 Merge pull request #244 from barrust/route-isclass-fix
fix for route.is_class_based
2018-12-12 03:58:41 -05:00
kennethreitz 8bed8e8741 Merge pull request #249 from abstiles/fix-broken-test-500
Fix broken exception handling in test_500
2018-12-12 03:58:06 -05:00
kennethreitz a81a348bce Merge pull request #245 from cdfuller/add-monospace-default
Add monospace fallback for preformatted text in docs
2018-12-12 03:57:55 -05:00
kennethreitz fd9e8c5cbc Merge pull request #240 from barrust/persist-debug
persist debug from API to run
2018-12-12 03:57:19 -05:00
kennethreitz 8030b1919d Merge pull request #253 from TomFaulkner/patch-1
Grammatical fix
2018-12-12 03:56:52 -05:00
Sawood Alam 72c789fdd7 Update Pipfile.lock to reflect version changes 2018-11-30 12:30:28 -05:00
Sawood Alam 1113a9aa0d Import lifespan as a middleware as per the change in starlette package 2018-11-30 11:52:43 -05:00
Tom Faulkner a5532614a2 Grammatical fix
Grammar: `into to` to `into`
2018-11-26 19:30:20 -06:00
Andrew Stiles 122023fb70 Restore the removal of ExceptionMiddleware
Back out the changes made to API and fix the test with a non-raising
instance of TestClient.
2018-11-21 13:05:51 -06:00
Andrew Stiles b8fa923ec9 Fix broken exception handling in test_500 2018-11-21 11:30:31 -06:00
Tyler Barrus ae06b3e01a default cont to False 2018-11-20 10:38:15 -05:00
Cody Fuller 5599ec2809 Add monospace fallback for preformatted text 2018-11-20 09:37:30 -06:00
Tyler Barrus e795cbddb6 fix for route.is_class_based 2018-11-20 10:26:29 -05:00
Tyler Barrus 0cb087c37b persist debug from API to run 2018-11-19 23:37:04 -05:00
taoufik07 983cbcc711 cleanup 2018-11-17 21:40:17 +01:00
taoufik07 6d154b0c78 WIP websocket 2018-11-17 20:49:26 +01:00
Tessei Kameyama f3f36e28c4 [formats] format-files detects file or simple-data
if `part` has `content-type`, `format_files` returns
`{"name" : filedata}` . `filedata` is `dict`, the keys are
`filename, content, content-type` .

Otherwise, `format_files` just returns `{"name" : b"data"}` .
2018-11-16 23:50:00 +09:00
Tessei Kameyama fdf4797726 [test] update file-upload [wip]
if form-data don't have content-type,
`req.media("files")` returns `{"name" : b"data"}` , else
`{"name" :
{"filename" : "foo.txt",
"content" : b"data",
"content-type" : "some/type"} }` .

not implemented yet.
2018-11-16 22:13:05 +09:00
Tessei Kameyama 67d8a3be98 [fix] typo 2018-11-16 20:23:09 +09:00
kennethreitz 4001a60f6c Merge pull request #229 from tomchristie/run-in-threadpool
Use Starlette's run_in_threadpool
2018-11-15 06:24:34 -05:00
kennethreitz d94db41271 Merge branch 'master' of https://github.com/kennethreitz/responder 2018-11-15 06:25:42 -05:00
kennethreitz 8abb78bb58 fix for #215 2018-11-15 06:22:04 -05:00
kennethreitz a80db99aa3 Merge pull request #228 from mmanhertz/patch-2
Fix issue in OpenAPI Schema Support example
2018-11-15 05:49:07 -05:00
Tom Christie 69a300f21a Use Starlette's run_in_threadpool 2018-11-15 10:38:05 +00:00
Matthias Manhertz 1b024b8092 Fix issue in OpenAPI Schema Support example
`PetSchema().dump()` returns a dict. Calling `.data` on it causes an AttributeError.
2018-11-15 11:19:11 +01:00
kennethreitz a622689597 Merge pull request #225 from peerster/patch-1
Update README
2018-11-14 06:18:13 -05:00
Pär 9943e66c49 Updates all of the python-responder.org links 2018-11-14 11:10:29 +01:00
Pär 7233c08281 Update README
Update link to python-responder.org to use https
2018-11-14 11:07:18 +01:00
kennethreitz 0845d92fda Merge pull request #223 from jkermes/master
Fix static response
2018-11-13 09:33:34 -05:00
Julien Kermes 1cc02e5a83 Fix static response 2018-11-13 15:18:30 +01:00
kennethreitz aa4cd7a144 Merge pull request #222 from MRSharff/debian-manifest-typofix
Fixed a typo in setup.py: mainfest -> manifest
2018-11-13 07:10:34 -05:00
Mathew Sharff b42ae0dfd7 Fixed a typo in setup.py: mainfest -> manifest 2018-11-12 21:33:12 -08:00
kennethreitz a6bd179726 version bump 2018-11-11 16:58:07 -05:00
kennethreitz aac7b5117b Merge pull request #218 from tomchristie/patch-1
Bring testominials in line with README
2018-11-10 17:48:29 -05:00
Tom Christie 8e8e99ed2e Bring testominials in line with README 2018-11-09 18:25:53 +00:00
kennethreitz 078ac23b20 Merge pull request #213 from mmanhertz/fix-212
fix for #212
2018-11-07 17:25:14 -05:00
kennethreitz 8e61df6b6a Merge pull request #207 from taoufik07/await_backgroundtask_raise
Await for background task and raise
2018-11-07 17:25:02 -05:00
mmanhertz cf7fb56653 fix for #212 2018-11-07 17:05:18 +01:00
taoufik07 da20d13c49 Await for background task and raise 2018-11-06 17:27:04 +01:00
kennethreitz 7a250aa8fc Merge branch 'master' of github.com:kennethreitz/responder 2018-11-06 05:46:25 -05:00
kennethreitz af28ecb82d fix for #200 2018-11-06 05:46:17 -05:00
kennethreitz 39a0e52a2a Merge pull request #201 from mmanhertz/patch-1
Fix OpenAPI version in examples
2018-11-06 05:12:51 -05:00
Matthias Manhertz a4f5a111c7 Fix OpenAPI version in examples
The example for "OpenAPI Schema Support" and "Interactive Documentation had `openapi="3.0"` which would cause the following error when swagger UI tried to render the schema:
```
Unable to render this definition
The provided definition does not specify a valid version field.

Please indicate a valid Swagger or OpenAPI version field. Supported version fields are swagger: "2.0" and those that match  openapi: 3.0.n (for example, openapi: 3.0.0).
```
2018-11-06 10:46:30 +01:00
kennethreitz c65e585493 Merge pull request #192 from vlcinsky/fix_doc_typo
Fixed missing `.data` on Marshmallow dump
2018-11-05 17:40:06 -05:00
kennethreitz c9e94561aa Merge pull request #196 from xyb/patch-1
fix Dockerfile
2018-11-05 14:09:13 -05:00
Xie Yanbo 1daa4c202b fix Dockerfile 2018-11-06 00:47:09 +08:00
kennethreitz 83ff361672 Merge pull request #195 from taoufik07/fix/routes_conflict
Fix/routes conflict
2018-11-05 09:01:15 -05:00
taoufik07 2c686f107d Tests 2018-11-05 14:52:08 +01:00
taoufik07 ac8ec3d5ef cleanup 2018-11-05 14:51:56 +01:00
taoufik07 21e70ef913 Fix tests 2018-11-05 14:48:34 +01:00
taoufik07 c4f5b0e7c2 Fix routes conflict 2018-11-05 14:47:46 +01:00
Jan Vlcinsky 45d6c1389d Fixed missing .data on Marshmallow dump 2018-11-04 22:06:54 +01:00
kennethreitz 0c9bc5a3af fix 2018-11-04 07:22:03 -05:00
kennethreitz 5b67d5a04a Merge branch 'master' of github.com:kennethreitz/responder 2018-11-04 07:20:41 -05:00
kennethreitz f3396a5573 fix for #188 2018-11-04 07:20:30 -05:00
kennethreitz e9f48788a3 Merge pull request #184 from rparrapy/change-route-cache
Replace memoize decorator by functools.lru_cache
2018-11-02 07:09:08 -04:00
Rodrigo Parra 6993a1ea46 Replace memoize decorator by functools.lru_cache 2018-11-02 06:32:01 -03:00
kennethreitz 8bcfb4585b cleanup testimonials 2018-11-01 05:38:22 -04:00
kennethreitz db45251f7f Merge pull request #182 from rparrapy/patch-1
Fix typo
2018-10-31 17:39:02 -04:00
kennethreitz 5582667b4f deployment 2018-10-31 17:37:13 -04:00
Rodrigo Parra 2c898aaf23 Fix typo 2018-10-31 14:36:40 -03:00
kennethreitz e0999ffcdd Merge branch 'master' of github.com:kennethreitz/responder 2018-10-31 06:10:49 -04:00
kennethreitz 03811768bb depoloyment 2018-10-31 06:10:40 -04:00
kennethreitz fbef577c9f Merge pull request #179 from taoufik07/patch-16
Typo :3
2018-10-30 21:56:18 -04:00
Taoufik 9434510ce9 Typo :3 2018-10-31 01:04:25 +01:00
kennethreitz 354130c151 .serve 2018-10-30 19:08:57 -04:00
kennethreitz 3e3cba016a Merge pull request #177 from tomchristie/patch-1
Update api.py
2018-10-30 10:37:38 -04:00
Tom Christie f75e120bef Update api.py
Refs https://github.com/kennethreitz/responder/issues/174

You don't need both to use both DebugMiddleware and ExceptionMiddleware, just one or the other.

Currently errors will cause a traceback 500 page, then re-raise, then attempt to generate another traceback 500 page, resulting in the behavior commented on in #174
2018-10-30 14:34:10 +00:00
kennethreitz 1d0294e430 merge 2018-10-30 05:01:25 -04:00
kennethreitz f786dd8254 apispec 2018-10-30 04:59:24 -04:00
kennethreitz cd9d09fd53 Merge pull request #175 from taoufik07/patch-15
Set debug to False in tests
2018-10-29 19:04:32 -04:00
kennethreitz 7471bbcd4e Merge pull request #173 from taoufik07/patch-14
Trusted hosts docs
2018-10-29 19:04:19 -04:00
Taoufik 43b04eccbd Set debug to False in tests 2018-10-29 22:53:56 +01:00
Taoufik 6a5d0b5e9f Trusted hosts docs 2018-10-29 21:56:54 +01:00
kennethreitz 359d366de4 Merge pull request #172 from taoufik07/trustedhost_starlette
starlette's TrustedhostMiddleware
2018-10-29 16:33:35 -04:00
taoufik07 556d9f3a7b Update starlette 2018-10-29 21:08:38 +01:00
taoufik07 2cab2dcec0 Tests 2018-10-29 20:54:38 +01:00
taoufik07 99d4e78dc9 Use starlette TrustedHostMiddleware 2018-10-29 20:48:51 +01:00
kennethreitz 9aa99869ae next version 2018-10-29 08:00:44 -04:00
kennethreitz 08e0d87347 Merge branch 'master' of github.com:kennethreitz/responder 2018-10-29 07:57:19 -04:00
kennethreitz 3f9e4057d3 run sync tasks in threadpoolexecutor 2018-10-29 07:54:59 -04:00
kennethreitz a29e40353c Merge pull request #170 from taoufik07/trusted_hosts
Trusted hosts support
2018-10-28 14:47:28 -04:00
taoufik07 778cb2dd0f Add Tests 2018-10-28 18:26:42 +00:00
taoufik07 f7d5514b94 Fix test base_url 2018-10-28 18:12:21 +00:00
taoufik07 954637f7b3 Pass base_url to the TestClient 2018-10-28 18:09:52 +00:00
taoufik07 1ab46104c8 Allow all hosts by default 2018-10-28 14:51:24 +00:00
kennethreitz 815776d473 Merge branch 'master' of github.com:kennethreitz/responder 2018-10-28 05:25:56 -04:00
kennethreitz 8db1a7be90 Merge pull request #169 from taoufik07/patch-13
typo
2018-10-28 05:23:56 -04:00
taoufik07 7b11fa24dd Silence for now 2018-10-28 01:38:17 +01:00
taoufik07 1f0f2318d5 cleanup 2018-10-28 01:34:26 +01:00
taoufik07 029b3e2a52 Tests 2018-10-28 00:46:50 +01:00
taoufik07 4fff823def Trusted host 2018-10-28 00:46:39 +01:00
Taoufik cab78275f4 typo 2018-10-27 22:25:11 +01:00
kennethreitz 5f60e4fedb before_request 2018-10-27 09:22:17 -04:00
kennethreitz 96971a33a7 tour 2018-10-27 09:20:18 -04:00
kennethreitz 9a7409f521 test for before_request 2018-10-27 09:18:07 -04:00
kennethreitz 80aa7e305b before_request=True 2018-10-27 09:15:52 -04:00
kennethreitz 27d513cb01 Merge branch 'master' of github.com:kennethreitz/responder 2018-10-27 09:04:29 -04:00
kennethreitz 9bf5cc8c03 before_request, v1 2018-10-27 09:04:19 -04:00
kennethreitz 7994b210cd Merge pull request #166 from repodevs/fix-deployment-example
DOC: change dockerfile instruction `from` to use UPPERCASE
2018-10-27 07:33:47 -04:00
Edi Santoso 46555bbe3f DOC: change dockerfile instruction from to use UPPERCASE 2018-10-27 18:29:43 +07:00
kennethreitz 4d15dbc465 fix for sessions 2018-10-27 07:10:05 -04:00
kennethreitz 855d3c4320 cookies 2018-10-27 06:21:19 -04:00
kennethreitz 4564862acc xfail 2018-10-27 06:11:47 -04:00
kennethreitz 176dd70073 fix 301 redirects 2018-10-27 06:07:50 -04:00
20 changed files with 653 additions and 355 deletions
+13
View File
@@ -1,5 +1,18 @@
# v1.1.1
- Run sync views in a threadpoolexecutor.
# v1.1.0
- Support for `before_request`.
# v1.0.4
- Potential bufix for cookies.
# v1.0.3
- Bugfix for redirects.
# v1.0.2
- Improvement for static file hosting.
# v1.0.1
- Improve cors configuration settings.
+1
View File
@@ -0,0 +1 @@
include LICENSE
Generated
+205 -153
View File
@@ -32,10 +32,10 @@
},
"apispec": {
"hashes": [
"sha256:c2e6ac6471aaf7c6ec6d12714821898910c6b3c87c189de9a2e3754786b86ada",
"sha256:fa7dfa8a292bae9b1e70c44a50bf61901805821726c5b804568c9f2501f57ebb"
"sha256:8072aaba54cb430787c3662512d5c9fe521eae1ec0b6d7d05b129814b6b48f69",
"sha256:93a6046bf692e8e4398101d447fffcf148b9dbed66d886073e05b491cd6835fd"
],
"version": "==1.0.0b3"
"version": "==1.0.0b6"
},
"apistar": {
"hashes": [
@@ -59,10 +59,10 @@
},
"certifi": {
"hashes": [
"sha256:339dc09518b07e2fa7eda5450740925974815557727d6bd35d319c1524a04a4c",
"sha256:6d58c986d22b038c8c0df30d639f23a3e6d172a05c3583e766f4c0b785c0986a"
"sha256:47f9c83ef4c0c621eaef743f133f09fa8a74a9b75f037e8624f83bd1b6626cb7",
"sha256:993f830721089fef441cdfeb4b2c8c9df86f0c63239f06bd025a76a7daddb033"
],
"version": "==2018.10.15"
"version": "==2018.11.29"
},
"chardet": {
"hashes": [
@@ -119,16 +119,17 @@
},
"idna": {
"hashes": [
"sha256:156a6814fb5ac1fc6850fb002e0852d56c0c8d2531923a51032d1b70760e186e",
"sha256:684a38a6f903c1d71d6d5fac066b58d7768af4de2b832e426ec79c30daa94a16"
"sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407",
"sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c"
],
"version": "==2.7"
"version": "==2.8"
},
"itsdangerous": {
"hashes": [
"sha256:cbb3fcf8d3e33df861709ecaf89d9e6629cff0a217bc2848f1b41cd30d360519"
"sha256:321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19",
"sha256:b12271b2047cb23eeb98c8b5622e2e5c5e9abd9784a153e9d8ef9cb4dd09d749"
],
"version": "==0.24"
"version": "==1.1.0"
},
"jinja2": {
"hashes": [
@@ -139,16 +140,43 @@
},
"markupsafe": {
"hashes": [
"sha256:a6be69091dac236ea9c6bc7d012beab42010fa914c459791d627dad4910eb665"
"sha256:048ef924c1623740e70204aa7143ec592504045ae4429b59c30054cb31e3c432",
"sha256:130f844e7f5bdd8e9f3f42e7102ef1d49b2e6fdf0d7526df3f87281a532d8c8b",
"sha256:19f637c2ac5ae9da8bfd98cef74d64b7e1bb8a63038a3505cd182c3fac5eb4d9",
"sha256:1b8a7a87ad1b92bd887568ce54b23565f3fd7018c4180136e1cf412b405a47af",
"sha256:1c25694ca680b6919de53a4bb3bdd0602beafc63ff001fea2f2fc16ec3a11834",
"sha256:1f19ef5d3908110e1e891deefb5586aae1b49a7440db952454b4e281b41620cd",
"sha256:1fa6058938190ebe8290e5cae6c351e14e7bb44505c4a7624555ce57fbbeba0d",
"sha256:31cbb1359e8c25f9f48e156e59e2eaad51cd5242c05ed18a8de6dbe85184e4b7",
"sha256:3e835d8841ae7863f64e40e19477f7eb398674da6a47f09871673742531e6f4b",
"sha256:4e97332c9ce444b0c2c38dd22ddc61c743eb208d916e4265a2a3b575bdccb1d3",
"sha256:525396ee324ee2da82919f2ee9c9e73b012f23e7640131dd1b53a90206a0f09c",
"sha256:52b07fbc32032c21ad4ab060fec137b76eb804c4b9a1c7c7dc562549306afad2",
"sha256:52ccb45e77a1085ec5461cde794e1aa037df79f473cbc69b974e73940655c8d7",
"sha256:5c3fbebd7de20ce93103cb3183b47671f2885307df4a17a0ad56a1dd51273d36",
"sha256:5e5851969aea17660e55f6a3be00037a25b96a9b44d2083651812c99d53b14d1",
"sha256:5edfa27b2d3eefa2210fb2f5d539fbed81722b49f083b2c6566455eb7422fd7e",
"sha256:7d263e5770efddf465a9e31b78362d84d015cc894ca2c131901a4445eaa61ee1",
"sha256:83381342bfc22b3c8c06f2dd93a505413888694302de25add756254beee8449c",
"sha256:857eebb2c1dc60e4219ec8e98dfa19553dae33608237e107db9c6078b1167856",
"sha256:98e439297f78fca3a6169fd330fbe88d78b3bb72f967ad9961bcac0d7fdd1550",
"sha256:bf54103892a83c64db58125b3f2a43df6d2cb2d28889f14c78519394feb41492",
"sha256:d9ac82be533394d341b41d78aca7ed0e0f4ba5a2231602e2f05aa87f25c51672",
"sha256:e982fe07ede9fada6ff6705af70514a52beb1b2c3d25d4e873e82114cf3c5401",
"sha256:edce2ea7f3dfc981c4ddc97add8a61381d9642dc3273737e756517cc03e84dd6",
"sha256:efdc45ef1afc238db84cb4963aa689c0408912a0239b0721cb172b4016eb31d6",
"sha256:f137c02498f8b935892d5c0172560d7ab54bc45039de8805075e19079c639a9c",
"sha256:f82e347a72f955b7017a39708a3667f106e6ad4d10b25f237396a7115d8ed5fd",
"sha256:fb7c206e01ad85ce57feeaaa0bf784b97fa3cad0d4a5737bc5295785f5c613a1"
],
"version": "==1.0"
"version": "==1.1.0"
},
"marshmallow": {
"hashes": [
"sha256:5e0053c86e3abaa72a03bbe0021ec97270c13fd6400b682eb1aeaf24b871bc8a",
"sha256:81884e930c1db72d8b8e3d8d2d090f2f43427e5c11c37f703b29879980491ab6"
"sha256:3133fb98afd627dcd8c06e4705f0ecea1b28003a53820d0266fa6c0ff7cf215c",
"sha256:6489e72ea75a30cb07686ce01e24bf65fc7f42edf429153a70abb9e38e56ef52"
],
"version": "==3.0.0b19"
"version": "==3.0.0rc2"
},
"parse": {
"hashes": [
@@ -181,10 +209,10 @@
},
"requests": {
"hashes": [
"sha256:99dcfdaaeb17caf6e526f32b6a7b780461512ab3f1d992187801694cba42770c",
"sha256:a84b8c9ab6239b578f22d1c21d51b696dcfe004032bb80ea832398d6909d7279"
"sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e",
"sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b"
],
"version": "==2.20.0"
"version": "==2.21.0"
},
"requests-toolbelt": {
"hashes": [
@@ -199,10 +227,10 @@
},
"rfc3986": {
"hashes": [
"sha256:632b8fcd2ac37f24334316227f909be4f9d0738cbf409404cff6fa5f69a24093",
"sha256:8458571c4c57e1cf23593ad860bb601b6a604df6217f829c2bc70dc4b5af941b"
"sha256:5ad82677b02b88c8d24f6511b4ee9baa5e7da675599b479fbbc5c9c578b5b737",
"sha256:bc3ae4b7cd88a99eff2d3900fcb858d44562fd7f273fc07aeef568b9bb6fc4e1"
],
"version": "==1.1.0"
"version": "==1.2.0"
},
"rx": {
"hashes": [
@@ -213,62 +241,62 @@
},
"six": {
"hashes": [
"sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9",
"sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb"
"sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c",
"sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73"
],
"version": "==1.11.0"
"version": "==1.12.0"
},
"starlette": {
"hashes": [
"sha256:eac0f6cab6b48846a0c1af16615430ae0e7a95f669ee0841a7e2f242d51d8935"
"sha256:01f04283b49a8cb0c8921baa90dbafe47e953f0a265f6ebb38176038e4bd9bf8"
],
"version": "==0.5.5"
"version": "==0.9.9"
},
"urllib3": {
"hashes": [
"sha256:41c3db2fc01e5b907288010dec72f9d0a74e37d6994e6eb56849f59fea2265ae",
"sha256:8819bba37a02d143296a4d032373c4dd4aca11f6d4c9973335ca75f9c8475f59"
"sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39",
"sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22"
],
"version": "==1.24"
"version": "==1.24.1"
},
"uvicorn": {
"hashes": [
"sha256:e2b742fdaa0b52f4aac92fd2c078e7f1f17d11322bb3efb09d341d5c6998b4b5"
"sha256:ab570ef3b088ddf30a8a2bb97f624c4eabe246301c2f21e38a48c82bfa3d8f52"
],
"version": "==0.3.16"
"version": "==0.3.24"
},
"websockets": {
"hashes": [
"sha256:0e2f7d6567838369af074f0ef4d0b802d19fa1fee135d864acc656ceefa33136",
"sha256:2a16dac282b2fdae75178d0ed3d5b9bc3258dabfae50196cbb30578d84b6f6a6",
"sha256:5a1fa6072405648cb5b3688e9ed3b94be683ce4a4e5723e6f5d34859dee495c1",
"sha256:5c1f55a1274df9d6a37553fef8cff2958515438c58920897675c9bc70f5a0538",
"sha256:669d1e46f165e0ad152ed8197f7edead22854a6c90419f544e0f234cc9dac6c4",
"sha256:695e34c4dbea18d09ab2c258994a8bf6a09564e762655408241f6a14592d2908",
"sha256:6b2e03d69afa8d20253455e67b64de1a82ff8612db105113cccec35d3f8429f0",
"sha256:79ca7cdda7ad4e3663ea3c43bfa8637fc5d5604c7737f19a8964781abbd1148d",
"sha256:7fd2dd9a856f72e6ed06f82facfce01d119b88457cd4b47b7ae501e8e11eba9c",
"sha256:82c0354ac39379d836719a77ee360ef865377aa6fdead87909d50248d0f05f4d",
"sha256:8f3b956d11c5b301206382726210dc1d3bee1a9ccf7aadf895aaf31f71c3716c",
"sha256:91ec98640220ae05b34b79ee88abf27f97ef7c61cf525eec57ea8fcea9f7dddb",
"sha256:952be9540d83dba815569d5cb5f31708801e0bbfc3a8c5aef1890b57ed7e58bf",
"sha256:99ac266af38ba1b1fe13975aea01ac0e14bb5f3a3200d2c69f05385768b8568e",
"sha256:9fa122e7adb24232247f8a89f2d9070bf64b7869daf93ac5e19546b409e47e96",
"sha256:a0873eadc4b8ca93e2e848d490809e0123eea154aa44ecd0109c4d0171869584",
"sha256:cb998bd4d93af46b8b49ecf5a72c0a98e5cc6d57fdca6527ba78ad89d6606484",
"sha256:e02e57346f6a68523e3c43bbdf35dde5c440318d1f827208ae455f6a2ace446d",
"sha256:e79a5a896bcee7fff24a788d72e5c69f13e61369d055f28113e71945a7eb1559",
"sha256:ee55eb6bcf23ecc975e6b47c127c201b913598f38b6a300075f84eeef2d3baff",
"sha256:f1414e6cbcea8d22843e7eafdfdfae3dd1aba41d1945f6ca66e4806c07c4f454"
"sha256:04b42a1b57096ffa5627d6a78ea1ff7fad3bc2c0331ffc17bc32a4024da7fea0",
"sha256:08e3c3e0535befa4f0c4443824496c03ecc25062debbcf895874f8a0b4c97c9f",
"sha256:10d89d4326045bf5e15e83e9867c85d686b612822e4d8f149cf4840aab5f46e0",
"sha256:232fac8a1978fc1dead4b1c2fa27c7756750fb393eb4ac52f6bc87ba7242b2fa",
"sha256:4bf4c8097440eff22bc78ec76fe2a865a6e658b6977a504679aaf08f02c121da",
"sha256:51642ea3a00772d1e48fb0c492f0d3ae3b6474f34d20eca005a83f8c9c06c561",
"sha256:55d86102282a636e195dad68aaaf85b81d0bef449d7e2ef2ff79ac450bb25d53",
"sha256:564d2675682bd497b59907d2205031acbf7d3fadf8c763b689b9ede20300b215",
"sha256:5d13bf5197a92149dc0badcc2b699267ff65a867029f465accfca8abab95f412",
"sha256:5eda665f6789edb9b57b57a159b9c55482cbe5b046d7db458948370554b16439",
"sha256:5edb2524d4032be4564c65dc4f9d01e79fe8fad5f966e5b552f4e5164fef0885",
"sha256:79691794288bc51e2a3b8de2bc0272ca8355d0b8503077ea57c0716e840ebaef",
"sha256:7fcc8681e9981b9b511cdee7c580d5b005f3bb86b65bde2188e04a29f1d63317",
"sha256:8e447e05ec88b1b408a4c9cde85aa6f4b04f06aa874b9f0b8e8319faf51b1fee",
"sha256:90ea6b3e7787620bb295a4ae050d2811c807d65b1486749414f78cfd6fb61489",
"sha256:9e13239952694b8b831088431d15f771beace10edfcf9ef230cefea14f18508f",
"sha256:d40f081187f7b54d7a99d8a5c782eaa4edc335a057aa54c85059272ed826dc09",
"sha256:e1df1a58ed2468c7b7ce9a2f9752a32ad08eac2bcd56318625c3647c2cd2da6f",
"sha256:e98d0cec437097f09c7834a11c69d79fe6241729b23f656cfc227e93294fc242",
"sha256:f8d59627702d2ff27cb495ca1abdea8bd8d581de425c56e93bff6517134e0a9b",
"sha256:fc30cdf2e949a2225b012a7911d1d031df3d23e99b7eda7dfc982dc4a860dae9"
],
"version": "==6.0"
"version": "==7.0"
},
"whitenoise": {
"hashes": [
"sha256:133a92ff0ab8fb9509f77d4f7d0de493eca19c6fea973f4195d4184f888f2e02",
"sha256:32b57d193478908a48acb66bf73e7a3c18679263e3e64bfebcfac1144a430039"
"sha256:118ab3e5f815d380171b100b05b76de2a07612f422368a201a9ffdeefb2251c1",
"sha256:42133ddd5229eeb6a0c9899496bdbe56c292394bf8666da77deeb27454c0456a"
],
"version": "==4.1"
"version": "==4.1.2"
}
},
"develop": {
@@ -317,17 +345,17 @@
},
"bleach": {
"hashes": [
"sha256:48d39675b80a75f6d1c3bdbffec791cf0bbbab665cf01e20da701c77de278718",
"sha256:73d26f018af5d5adcdabf5c1c974add4361a9c76af215fe32fdec8a6fc5fb9b9"
"sha256:213336e49e102af26d9cde77dd2d0397afabc5a6bf2fed985dc35b5d1e285a16",
"sha256:3fdf7f77adcf649c9911387df51254b813185e32b2c6619f690b593a617e19fa"
],
"version": "==3.0.2"
"version": "==3.1.0"
},
"certifi": {
"hashes": [
"sha256:339dc09518b07e2fa7eda5450740925974815557727d6bd35d319c1524a04a4c",
"sha256:6d58c986d22b038c8c0df30d639f23a3e6d172a05c3583e766f4c0b785c0986a"
"sha256:47f9c83ef4c0c621eaef743f133f09fa8a74a9b75f037e8624f83bd1b6626cb7",
"sha256:993f830721089fef441cdfeb4b2c8c9df86f0c63239f06bd025a76a7daddb033"
],
"version": "==2018.10.15"
"version": "==2018.11.29"
},
"chardet": {
"hashes": [
@@ -345,43 +373,45 @@
},
"colorama": {
"hashes": [
"sha256:a3d89af5db9e9806a779a50296b5fdb466e281147c2c235e8225ecc6dbf7bbf3",
"sha256:c9b54bebe91a6a803e0772c8561d53f2926bfeb17cd141fbabcb08424086595c"
"sha256:05eed71e2e327246ad6b38c540c4a3117230b19679b875190486ddd2d721422d",
"sha256:f8ac84de7840f5b9c4e3347b3c1eaa50f7e49c2b07596221daec5edaabbd7c48"
],
"markers": "sys_platform == 'win32'",
"version": "==0.4.0"
"version": "==0.4.1"
},
"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"
"sha256:029c69deaeeeae1b15bc6c59f0ffa28aa8473721c614a23f2c2976dec245cd12",
"sha256:02abbbebc6e9d5abe13cd28b5e963dedb6ffb51c146c916d17b18f141acd9947",
"sha256:1bbfe5b82a3921d285e999c6d256c1e16b31c554c29da62d326f86c173d30337",
"sha256:210c02f923df33a8d0e461c86fdcbbb17228ff4f6d92609fc06370a98d283c2d",
"sha256:2d0807ba935f540d20b49d5bf1c0237b90ce81e133402feda906e540003f2f7a",
"sha256:35d7a013874a7c927ce997350d314144ffc5465faf787bb4e46e6c4f381ef562",
"sha256:3636f9d0dcb01aed4180ef2e57a4e34bb4cac3ecd203c2a23db8526d86ab2fb4",
"sha256:42f4be770af2455a75e4640f033a82c62f3fb0d7a074123266e143269d7010ef",
"sha256:48440b25ba6cda72d4c638f3a9efa827b5b87b489c96ab5f4ff597d976413156",
"sha256:4dac8dfd1acf6a3ac657475dfdc66c621f291b1b7422a939cc33c13ac5356473",
"sha256:4e8474771c69c2991d5eab65764289a7dd450bbea050bc0ebb42b678d8222b42",
"sha256:551f10ddfeff56a1325e5a34eff304c5892aa981fd810babb98bfee77ee2fb17",
"sha256:5b104982f1809c1577912519eb249f17d9d7e66304ad026666cb60a5ef73309c",
"sha256:5c62aef73dfc87bfcca32cee149a1a7a602bc74bac72223236b0023543511c88",
"sha256:633151f8d1ad9467b9f7e90854a7f46ed8f2919e8bc7d98d737833e8938fc081",
"sha256:772207b9e2d5bf3f9d283b88915723e4e92d9a62c83f44ec92b9bd0cd685541b",
"sha256:7d5e02f647cd727afc2659ec14d4d1cc0508c47e6cfb07aea33d7aa9ca94d288",
"sha256:a9798a4111abb0f94584000ba2a2c74841f2cfe5f9254709756367aabbae0541",
"sha256:b38ea741ab9e35bfa7015c93c93bbd6a1623428f97a67083fc8ebd366238b91f",
"sha256:b6a5478c904236543c0347db8a05fac6fc0bd574c870e7970faa88e1d9890044",
"sha256:c6248bfc1de36a3844685a2e10ba17c18119ba6252547f921062a323fb31bff1",
"sha256:c705ab445936457359b1424ef25ccc0098b0491b26064677c39f1d14a539f056",
"sha256:d95a363d663ceee647291131dbd213af258df24f41350246842481ec3709bd33",
"sha256:e27265eb80cdc5dab55a40ef6f890e04ecc618649ad3da5265f128b141f93f78",
"sha256:ebc276c9cb5d917bd2ae959f84ffc279acafa9c9b50b0fa436ebb70bbe2166ea",
"sha256:f4d229866d030863d0fe3bf297d6d11e6133ca15bbb41ed2534a8b9a3d6bd061",
"sha256:f95675bd88b51474d4fe5165f3266f419ce754ffadfb97f10323931fa9ac95e5",
"sha256:f95bc54fb6d61b9f9ff09c4ae8ff6a3f5edc937cda3ca36fc937302a7c152bf1",
"sha256:fd0f6be53de40683584e5331c341e65a679dbe5ec489a0697cec7c2ef1a48cda"
],
"version": "==5.0a3"
"version": "==5.0a4"
},
"docutils": {
"hashes": [
@@ -407,18 +437,12 @@
"index": "pypi",
"version": "==1.0.2"
},
"future": {
"hashes": [
"sha256:eb6d4df04f1fb538c99f69c9a28b255d1ee4e825d479b9c62fc38c0cf38065a4"
],
"version": "==0.17.0"
},
"idna": {
"hashes": [
"sha256:156a6814fb5ac1fc6850fb002e0852d56c0c8d2531923a51032d1b70760e186e",
"sha256:684a38a6f903c1d71d6d5fac066b58d7768af4de2b832e426ec79c30daa94a16"
"sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407",
"sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c"
],
"version": "==2.7"
"version": "==2.8"
},
"imagesize": {
"hashes": [
@@ -429,9 +453,10 @@
},
"itsdangerous": {
"hashes": [
"sha256:cbb3fcf8d3e33df861709ecaf89d9e6629cff0a217bc2848f1b41cd30d360519"
"sha256:321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19",
"sha256:b12271b2047cb23eeb98c8b5622e2e5c5e9abd9784a153e9d8ef9cb4dd09d749"
],
"version": "==0.24"
"version": "==1.1.0"
},
"jinja2": {
"hashes": [
@@ -442,16 +467,43 @@
},
"markupsafe": {
"hashes": [
"sha256:a6be69091dac236ea9c6bc7d012beab42010fa914c459791d627dad4910eb665"
"sha256:048ef924c1623740e70204aa7143ec592504045ae4429b59c30054cb31e3c432",
"sha256:130f844e7f5bdd8e9f3f42e7102ef1d49b2e6fdf0d7526df3f87281a532d8c8b",
"sha256:19f637c2ac5ae9da8bfd98cef74d64b7e1bb8a63038a3505cd182c3fac5eb4d9",
"sha256:1b8a7a87ad1b92bd887568ce54b23565f3fd7018c4180136e1cf412b405a47af",
"sha256:1c25694ca680b6919de53a4bb3bdd0602beafc63ff001fea2f2fc16ec3a11834",
"sha256:1f19ef5d3908110e1e891deefb5586aae1b49a7440db952454b4e281b41620cd",
"sha256:1fa6058938190ebe8290e5cae6c351e14e7bb44505c4a7624555ce57fbbeba0d",
"sha256:31cbb1359e8c25f9f48e156e59e2eaad51cd5242c05ed18a8de6dbe85184e4b7",
"sha256:3e835d8841ae7863f64e40e19477f7eb398674da6a47f09871673742531e6f4b",
"sha256:4e97332c9ce444b0c2c38dd22ddc61c743eb208d916e4265a2a3b575bdccb1d3",
"sha256:525396ee324ee2da82919f2ee9c9e73b012f23e7640131dd1b53a90206a0f09c",
"sha256:52b07fbc32032c21ad4ab060fec137b76eb804c4b9a1c7c7dc562549306afad2",
"sha256:52ccb45e77a1085ec5461cde794e1aa037df79f473cbc69b974e73940655c8d7",
"sha256:5c3fbebd7de20ce93103cb3183b47671f2885307df4a17a0ad56a1dd51273d36",
"sha256:5e5851969aea17660e55f6a3be00037a25b96a9b44d2083651812c99d53b14d1",
"sha256:5edfa27b2d3eefa2210fb2f5d539fbed81722b49f083b2c6566455eb7422fd7e",
"sha256:7d263e5770efddf465a9e31b78362d84d015cc894ca2c131901a4445eaa61ee1",
"sha256:83381342bfc22b3c8c06f2dd93a505413888694302de25add756254beee8449c",
"sha256:857eebb2c1dc60e4219ec8e98dfa19553dae33608237e107db9c6078b1167856",
"sha256:98e439297f78fca3a6169fd330fbe88d78b3bb72f967ad9961bcac0d7fdd1550",
"sha256:bf54103892a83c64db58125b3f2a43df6d2cb2d28889f14c78519394feb41492",
"sha256:d9ac82be533394d341b41d78aca7ed0e0f4ba5a2231602e2f05aa87f25c51672",
"sha256:e982fe07ede9fada6ff6705af70514a52beb1b2c3d25d4e873e82114cf3c5401",
"sha256:edce2ea7f3dfc981c4ddc97add8a61381d9642dc3273737e756517cc03e84dd6",
"sha256:efdc45ef1afc238db84cb4963aa689c0408912a0239b0721cb172b4016eb31d6",
"sha256:f137c02498f8b935892d5c0172560d7ab54bc45039de8805075e19079c639a9c",
"sha256:f82e347a72f955b7017a39708a3667f106e6ad4d10b25f237396a7115d8ed5fd",
"sha256:fb7c206e01ad85ce57feeaaa0bf784b97fa3cad0d4a5737bc5295785f5c613a1"
],
"version": "==1.0"
"version": "==1.1.0"
},
"marshmallow": {
"hashes": [
"sha256:5e0053c86e3abaa72a03bbe0021ec97270c13fd6400b682eb1aeaf24b871bc8a",
"sha256:81884e930c1db72d8b8e3d8d2d090f2f43427e5c11c37f703b29879980491ab6"
"sha256:3133fb98afd627dcd8c06e4705f0ecea1b28003a53820d0266fa6c0ff7cf215c",
"sha256:6489e72ea75a30cb07686ce01e24bf65fc7f42edf429153a70abb9e38e56ef52"
],
"version": "==3.0.0b19"
"version": "==3.0.0rc2"
},
"mccabe": {
"hashes": [
@@ -462,11 +514,11 @@
},
"more-itertools": {
"hashes": [
"sha256:c187a73da93e7a8acc0001572aebc7e3c69daf7bf6881a2cea10650bd4420092",
"sha256:c476b5d3a34e12d40130bc2f935028b5f636df8f372dc2c1c01dc19681b2039e",
"sha256:fcbfeaea0be121980e15bc97b3817b5202ca73d0eae185b4550cbfce2a3ebb3d"
"sha256:38a936c0a6d98a38bcc2d03fdaaedaba9f412879461dd2ceff8d37564d6522e4",
"sha256:c0a5785b1109a6bd7fac76d6837fd1feca158e54e521ccd2ae8bfe393cc9d4fc",
"sha256:fe7a7cae1ccb57d33952113ff4fa1bc5f879963600ed74918f1236e212ee50b9"
],
"version": "==4.3.0"
"version": "==5.0.0"
},
"packaging": {
"hashes": [
@@ -477,17 +529,17 @@
},
"pkginfo": {
"hashes": [
"sha256:5878d542a4b3f237e359926384f1dde4e099c9f5525d236b1840cf704fa8d474",
"sha256:a39076cb3eb34c333a0dd390b568e9e1e881c7bf2cc0aee12120636816f55aee"
"sha256:7424f2c8511c186cd5424bbf31045b77435b37a8d604990b79d4e70d741148bb",
"sha256:a6d9e40ca61ad3ebd0b72fbadd4fba16e4c0e4df0428c041e01e06eb6ee71f32"
],
"version": "==1.4.2"
"version": "==1.5.0.1"
},
"pluggy": {
"hashes": [
"sha256:447ba94990e8014ee25ec853339faf7b0fc8050cdc3289d4d71f7f410fb90095",
"sha256:bde19360a8ec4dfd8a20dcb811780a30998101f078fc7ded6162f0076f50508f"
"sha256:8ddc32f03971bfdf900a81961a48ccf2fb677cf7715108f85295c67405798616",
"sha256:980710797ff6a041e9a73a5787804f848996ecaa6f8a1b1e08224a5894f2074a"
],
"version": "==0.8.0"
"version": "==0.8.1"
},
"py": {
"hashes": [
@@ -512,54 +564,54 @@
},
"pygments": {
"hashes": [
"sha256:78f3f434bcc5d6ee09020f92ba487f95ba50f1e3ef83ae96b9d5ffa1bab25c5d",
"sha256:dbae1046def0efb574852fab9e90209b23f556367b5a320c0bcb871c77c3e8cc"
"sha256:5ffada19f6203563680669ee7f53b64dabbeb100eb51b61996085e99c03b284a",
"sha256:e8218dd399a61674745138520d0d4cf2621d7e032439341bc3f647bff125818d"
],
"version": "==2.2.0"
"version": "==2.3.1"
},
"pyparsing": {
"hashes": [
"sha256:bc6c7146b91af3f567cf6daeaec360bc07d45ffec4cf5353f4d7a208ce7ca30a",
"sha256:d29593d8ebe7b57d6967b62494f8c72b03ac0262b1eed63826c6f788b3606401"
"sha256:40856e74d4987de5d01761a22d1621ae1c7f8774585acae358aa5c5936c6c90b",
"sha256:f353aab21fd474459d97b709e527b5571314ee5f067441dc9f88e33eecd96592"
],
"version": "==2.2.2"
"version": "==2.3.0"
},
"pytest": {
"hashes": [
"sha256:212be78a6fa5352c392738a49b18f74ae9aeec1040f47c81cadbfd8d1233c310",
"sha256:6f6c1efc8d0ccc21f8f6c34d8330baca883cf109b66b3df954b0a117e5528fb4"
"sha256:3e65a22eb0d4f1bdbc1eacccf4a3198bf8d4049dea5112d70a0c61b00e748d02",
"sha256:5924060b374f62608a078494b909d341720a050b5224ff87e17e12377486a71d"
],
"index": "pypi",
"version": "==3.9.2"
"version": "==4.1.0"
},
"pytest-cov": {
"hashes": [
"sha256:513c425e931a0344944f84ea47f3956be0e416d95acbd897a44970c8d926d5d7",
"sha256:e360f048b7dae3f2f2a9a4d067b2dd6b6a015d384d1577c994a43f3f7cbad762"
"sha256:0ab664b25c6aa9716cbf203b17ddb301932383046082c081b9848a0edf5add33",
"sha256:230ef817450ab0699c6cc3c9c8f7a829c34674456f2ed8df1fe1d39780f7c87f"
],
"index": "pypi",
"version": "==2.6.0"
"version": "==2.6.1"
},
"pytz": {
"hashes": [
"sha256:642253af8eae734d1509fc6ac9c1aee5e5b69d76392660889979b9870610a46b",
"sha256:91e3ccf2c344ffaa6defba1ce7f38f97026943f675b7703f44789768e4cb0ece"
"sha256:32b0891edff07e28efe91284ed9c31e123d84bea3fd98e1f72be2508f43ef8d9",
"sha256:d5f05e487007e29e03409f9398d074e158d920d36eb82eaf66fb1136b0c5374c"
],
"version": "==2018.6"
"version": "==2018.9"
},
"readme-renderer": {
"hashes": [
"sha256:219a02f5359b6631f5ab952f6906c6c105bdd8bc4bf19c1ec5ee8bd9ea2dc1eb",
"sha256:f8f122ad9fd6d138337531379575a01a0b6ca70aedca78f094cb833da38c8c0c"
"sha256:bb16f55b259f27f75f640acf5e00cf897845a8b3e4731b5c1a436e4b8529202f",
"sha256:c8532b79afc0375a85f10433eca157d6b50f7d6990f337fa498c96cd4bfc203d"
],
"version": "==23.0"
"version": "==24.0"
},
"requests": {
"hashes": [
"sha256:99dcfdaaeb17caf6e526f32b6a7b780461512ab3f1d992187801694cba42770c",
"sha256:a84b8c9ab6239b578f22d1c21d51b696dcfe004032bb80ea832398d6909d7279"
"sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e",
"sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b"
],
"version": "==2.20.0"
"version": "==2.21.0"
},
"requests-toolbelt": {
"hashes": [
@@ -570,10 +622,10 @@
},
"six": {
"hashes": [
"sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9",
"sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb"
"sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c",
"sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73"
],
"version": "==1.11.0"
"version": "==1.12.0"
},
"snowballstemmer": {
"hashes": [
@@ -584,11 +636,11 @@
},
"sphinx": {
"hashes": [
"sha256:652eb8c566f18823a022bb4b6dbc868d366df332a11a0226b5bc3a798a479f17",
"sha256:d222626d8356de702431e813a05c68a35967e3d66c6cd1c2c89539bb179a7464"
"sha256:429e3172466df289f0f742471d7e30ba3ee11f3b5aecd9a840480d03f14bcfe5",
"sha256:c4cb17ba44acffae3d3209646b6baec1e215cad3065e852c68cc569d4df1b9f8"
],
"index": "pypi",
"version": "==1.8.1"
"version": "==1.8.3"
},
"sphinxcontrib-websupport": {
"hashes": [
@@ -606,10 +658,10 @@
},
"tqdm": {
"hashes": [
"sha256:3c4d4a5a41ef162dd61f1edb86b0e1c7859054ab656b2e7c7b77e7fbf6d9f392",
"sha256:5b4d5549984503050883bc126280b386f5f4ca87e6c023c5d015655ad75bdebb"
"sha256:b856be5cb6cfaee3b2733655c7c5bbc7751291bb5d1a4f54f020af4727570b3e",
"sha256:c9b9b5eeba13994a4c266aae7eef7aeeb0ba2973e431027e942b4faea139ef49"
],
"version": "==4.28.1"
"version": "==4.29.1"
},
"twine": {
"hashes": [
@@ -621,10 +673,10 @@
},
"urllib3": {
"hashes": [
"sha256:41c3db2fc01e5b907288010dec72f9d0a74e37d6994e6eb56849f59fea2265ae",
"sha256:8819bba37a02d143296a4d032373c4dd4aca11f6d4c9973335ca75f9c8475f59"
"sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39",
"sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22"
],
"version": "==1.24"
"version": "==1.24.1"
},
"webencodings": {
"hashes": [
+3 -4
View File
@@ -7,10 +7,10 @@
[![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)
[![](https://farm2.staticflickr.com/1959/43750081370_a4e20752de_o_d.png)](http://python-responder.org/)
[![](https://farm2.staticflickr.com/1959/43750081370_a4e20752de_o_d.png)](https://python-responder.org/)
Powered by [Starlette](https://www.starlette.io/). That `async` declaration is optional. [View documentation](http://python-responder.org).
Powered by [Starlette](https://www.starlette.io/). That `async` declaration is optional. [View documentation](https://python-responder.org).
This gets you a ASGI app, with a production static files server pre-installed, jinja2 templating (without additional imports), and a production webserver based on uvloop, serving up requests with gzip compression automatically.
@@ -23,11 +23,10 @@ This gets you a ASGI app, with a production static files server pre-installed, j
> "I love that you are exploring new patterns. Go go go!" — Danny Greenfield, author of [Two Scoops of Django]()
> "Love what I have seen while it's in progress! Many features of Responder are from my wishlist for Flask, and it's even faster and even easier than Flask!" — Luna C.
## More Examples
See [the documentation's feature tour](http://python-responder.org/en/latest/tour.html) for more details on features available in Responder.
See [the documentation's feature tour](https://python-responder.org/en/latest/tour.html) for more details on features available in Responder.
# Installing Responder
+1 -1
View File
@@ -26,7 +26,7 @@
.class em,
.descname,
.method em {
font-family: "Operator Mono SSm A", "Operator Mono SSm B" !important;
font-family: "Operator Mono SSm A", "Operator Mono SSm B", monospace !important;
font-weight: 400 !important;
}
+3 -2
View File
@@ -10,10 +10,11 @@ Assuming existing ``api.py`` and ``Pipfile.lock`` containing ``responder``.
``Dockerfile``::
from kennethreitz/pipenv
FROM kennethreitz/pipenv
ENV PORT '80'
COPY . /app
CMD python3 api.py
EXPOSE 80
That's it!
+5 -14
View File
@@ -45,8 +45,9 @@ Features
--------
- A pleasant API, with a single import statement.
- Class-based views without inheritence.
- Class-based views without inheritance.
- ASGI framework, the future of Python web services.
- WebSocket support!
- The ability to mount any ASGI / WSGI app at a subroute.
- *f-string syntax* route declaration.
- Mutable response object, passed into each view. No need to return anything.
@@ -80,18 +81,8 @@ Testimonials
— Danny Greenfield, author of `Two Scoops of Django`_
..
“The most ambitious crossover event in history.”
—Pablo Cabezas, `on Tom Christie joining the project`_
.. _APIStar: https://github.com/encode/apistar
.. _Django REST Framework: https://www.django-rest-framework.org/
.. _Two Scoops of Django:
.. _on Tom Christie joining the project: https://twitter.com/pabloteleco/status/1050841098321620992?s=20
.. _Two Scoops of Django: https://www.twoscoopspress.com/products/two-scoops-of-django-1-11
User Guides
-----------
@@ -131,13 +122,13 @@ 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.
- I love Falcon's "every request and response is passed into 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.
- 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.
+62 -2
View File
@@ -52,6 +52,8 @@ Serve a GraphQL API::
Visiting the endpoint will render a *GraphiQL* instance, in the browser.
You can make use of Responder's Request and Response objects in your GraphQL resolvers through ``info.context['request']`` and ``info.context['response']``.
OpenAPI Schema Support
----------------------
@@ -61,7 +63,7 @@ Responder comes with built-in support for OpenAPI / marshmallow::
import responder
from marshmallow import Schema, fields
api = responder.API(title="Web Service", version="1.0", openapi="3.0")
api = responder.API(title="Web Service", version="1.0", openapi="3.0.0")
@api.schema("Pet")
@@ -112,7 +114,7 @@ Interactive Documentation
Responder can automatically supply API Documentation for you. Using the example above::
api = responder.API(title="Web Service", version="1.0", openapi="3.0", docs_route="/docs")
api = responder.API(title="Web Service", version="1.0", openapi="3.0.0", docs_route="/docs")
This will make ``/docs`` render interactive documentation for your API.
@@ -173,6 +175,45 @@ You can easily read a Request's session data, that can be trusted to have origin
api = responder.API(secret_key=os.environ['SECRET_KEY'])
Using ``before_request``
------------------------
If you'd like a view to be executed before every request, simply do the following::
@api.route(before_request=True)
def prepare_response(req, resp):
resp.headers["X-Pizza"] = "42"
Now all requests to your HTTP Service will include an ``X-Pizza`` header.
WebSocket Support
-----------------
Responder supports WebSockets::
@api.route('/ws', websocket=True)
async def websocket(ws):
await ws.accept()
while True:
name = await ws.receive_text()
await ws.send_text(f"Hello {name}!")
await ws.close()
Accepting the connection::
await websocket.accept()
Sending and receiving data::
await websocket.send_{format}(data)
await websocket.receive_{format}(data)
Supported formats: ``text``, ``json``, ``bytes``.
Closing the connection::
await websocket.close()
Using Requests Test Client
--------------------------
@@ -229,3 +270,22 @@ In order to set custom parameters, you need to set the ``cors_params`` argument
* ``allow_credentials`` - Indicate that cookies should be supported for cross-origin requests. Defaults to ``False``.
* ``expose_headers`` - Indicate any response headers that should be made accessible to the browser. Defaults to ``[]``.
* ``max_age`` - Sets a maximum time in seconds for browsers to cache CORS responses. Defaults to ``60``.
Trusted Hosts
-------------
Make sure that all the incoming requests headers have a valid ``host``, that matches one of the provided patterns in the ``allowed_hosts`` attribute, in order to prevent HTTP Host Header attacks.
A 400 response will be raised, if a request does not match any of the provided patterns in the ``allowed_hosts`` attribute.
::
api = responder.API(allowed_hosts=[example.com, tenant.example.com])
* ``allowed_hosts`` - A list of allowed hostnames.
Note:
* By default, all hostnames are allowed.
* Wildcard domains such as ``*.example.com`` are supported.
* To allow any hostname use ``allowed_hosts=["*"]``.
+1 -1
View File
@@ -1 +1 @@
__version__ = "1.0.2"
__version__ = "1.1.2"
+162 -94
View File
@@ -1,7 +1,9 @@
import json
import os
from uuid import uuid4
from pathlib import Path
from base64 import b64encode
import apistar
import itsdangerous
@@ -11,13 +13,13 @@ import yaml
from apispec import APISpec, yaml_utils
from apispec.ext.marshmallow import MarshmallowPlugin
from asgiref.wsgi import WsgiToAsgi
from starlette.debug import DebugMiddleware
from starlette.exceptions import ExceptionMiddleware
from starlette.lifespan import LifespanHandler
from starlette.middleware.errors import ServerErrorMiddleware
from starlette.middleware.cors import CORSMiddleware
from starlette.middleware.gzip import GZipMiddleware
from starlette.middleware.httpsredirect import HTTPSRedirectMiddleware
from starlette.routing import Router
from starlette.middleware.lifespan import LifespanMiddleware
from starlette.middleware.trustedhost import TrustedHostMiddleware
from starlette.routing import Router, LifespanHandler
from starlette.staticfiles import StaticFiles
from starlette.testclient import TestClient
from starlette.websockets import WebSocket
@@ -64,7 +66,10 @@ class API:
enable_hsts=False,
docs_route=None,
cors=False,
cors_params=DEFAULT_CORS_PARAMS,
allowed_hosts=None,
):
self.background = BackgroundQueue()
self.secret_key = secret_key
self.title = title
@@ -84,7 +89,17 @@ class API:
self.hsts_enabled = enable_hsts
self.cors = cors
self.cors_params = DEFAULT_CORS_PARAMS
self.cors_params = cors_params
self.debug = debug
if not allowed_hosts:
# if not debug:
# raise RuntimeError(
# "You need to specify `allowed_hosts` when debug is set to False"
# )
allowed_hosts = ["*"]
self.allowed_hosts = allowed_hosts
# Make the static/templates directory if they don't exist.
for _dir in (self.static_dir, self.templates_dir):
os.makedirs(_dir, exist_ok=True)
@@ -105,7 +120,6 @@ class API:
# Cached requests session.
self._session = None
self.background = BackgroundQueue()
if self.openapi_version:
self.add_route(openapi_route, self.schema_response)
@@ -116,16 +130,17 @@ class API:
self.default_endpoint = None
self.app = self.dispatch
self.add_middleware(GZipMiddleware)
if debug:
self.add_middleware(DebugMiddleware)
if self.hsts_enabled:
self.add_middleware(HTTPSRedirectMiddleware)
self.lifespan_handler = LifespanHandler()
self.add_middleware(TrustedHostMiddleware, allowed_hosts=self.allowed_hosts)
self.lifespan_handler = LifespanMiddleware(LifespanHandler)
if self.cors:
self.add_middleware(CORSMiddleware, **self.cors_params)
self.add_middleware(ExceptionMiddleware, debug=debug)
self.add_middleware(ServerErrorMiddleware, debug=debug)
# Jinja enviroment
self.jinja_env = jinja2.Environment(
@@ -144,6 +159,15 @@ class API:
def _default_wsgi_app(*args, **kwargs):
pass
@property
def before_requests(self):
def gen():
for route in self.routes:
if self.routes[route].before_request:
yield self.routes[route]
return [g for g in gen()]
@property
def _apispec(self):
spec = APISpec(
@@ -158,10 +182,10 @@ class API:
operations = yaml_utils.load_operations_from_docstring(
self.routes[route].description
)
spec.add_path(path=route, operations=operations)
spec.path(path=route, operations=operations)
for name, schema in self.schemas.items():
spec.definition(name, schema=schema)
spec.components.schema(name, schema=schema)
return spec
@@ -173,6 +197,7 @@ class API:
self.app = middleware_cls(self.app, **middleware_config)
def __call__(self, scope):
if scope["type"] == "lifespan":
return self.lifespan_handler(scope)
@@ -197,14 +222,41 @@ class API:
async def asgi(receive, send):
nonlocal scope, self
req = models.Request(scope, receive=receive, api=self)
resp = await self._dispatch_request(
req, scope=scope, send=send, receive=receive
)
await resp(receive, send)
if scope["type"] == "lifespan":
return self.lifespan_handler(scope)
elif scope["type"] == "websocket":
ws = WebSocket(scope=scope, receive=receive, send=send)
await self._dispatch_ws(ws)
else:
req = models.Request(scope, receive=receive, api=self)
resp = await self._dispatch_request(
req, scope=scope, send=send, receive=receive
)
await resp(receive, send)
return asgi
async def _dispatch_ws(self, ws):
route = self.path_matches_route(ws.url.path)
route = self.routes.get(route)
# await self._dispatch(route, ws=ws)
try:
try:
# Run the view.
r = self.background(route.endpoint, ws)
# If it's async, await it.
if hasattr(r, "cr_running"):
await r
except TypeError as e:
cont = True
except Exception:
self.background(
self.default_response,
websocket=route.uses_websocket,
error=True
)
raise
def add_schema(self, name, schema, check_existing=True):
"""Adds a mashmallow schema to the API specification."""
if check_existing:
@@ -242,7 +294,7 @@ class API:
def _prepare_cookies(self, resp):
if resp.cookies:
header = " ".join([f"{k}={v}" for k, v in resp.cookies.items()])
header = " ".join([f"{k}={v};" for k, v in resp.cookies.items()])
resp.headers["Set-Cookie"] = header
@property
@@ -252,7 +304,9 @@ class API:
def _prepare_session(self, resp):
if resp.session:
data = self._signer.sign(json.dumps(resp.session).encode("utf-8"))
data = self._signer.sign(
b64encode(json.dumps(resp.session).encode("utf-8"))
)
resp.cookies[self.session_cookie] = data.decode("utf-8")
@staticmethod
@@ -266,78 +320,78 @@ class API:
# Get the route.
route = self.path_matches_route(req.url.path)
route = self.routes.get(route)
# Create the response object.
cont = False
if route:
if route.uses_websocket:
resp = WebSocket(**options)
resp = models.Response(req=req, formats=self.formats)
else:
resp = models.Response(req=req, formats=self.formats)
params = route.incoming_matches(req.url.path)
if route.is_function:
try:
try:
# Run the view.
r = route.endpoint(req, resp, **params)
# If it's async, await it.
if hasattr(r, "cr_running"):
await r
except TypeError as e:
cont = True
except Exception:
self.default_response(req, resp, error=True)
raise
elif route.is_class_based or cont:
try:
view = route.endpoint(**params)
except TypeError:
try:
view = route.endpoint()
except TypeError:
view = route.endpoint
# Run on_request first.
try:
# Run the view.
r = getattr(view, "on_request", self.no_response)(
req, resp, **params
)
# If it's async, await it.
if hasattr(r, "send"):
await r
except Exception:
self.default_response(req, resp, error=True)
raise
# Then run on_method.
method = req.method
try:
# Run the view.
r = getattr(view, f"on_{method}", self.no_response)(
req, resp, **params
)
# If it's async, await it.
if hasattr(r, "send"):
await r
except Exception as e:
self.default_response(req, resp, error=True)
for before_request in self.before_requests:
await self._execute_route(route=before_request, req=req, resp=resp)
await self._execute_route(route=route, req=req, resp=resp, **options)
else:
resp = models.Response(req=req, formats=self.formats)
self.default_response(req, resp, notfound=True)
self.default_response(req, resp)
self.default_response(req=req, resp=resp, notfound=True)
self.default_response(req=req, resp=resp)
self._prepare_session(resp)
self._prepare_cookies(resp)
return resp
async def _execute_route(self, *, route, req, resp, **options):
params = route.incoming_matches(req.url.path)
cont = True
if route.is_function:
try:
try:
# Run the view.
r = self.background(route.endpoint, req, resp, **params)
# If it's async, await it.
if hasattr(r, "cr_running"):
await r
except TypeError as e:
cont = True
except Exception:
self.background(self.default_response, req, resp, error=True)
raise
if route.is_class_based or cont:
try:
view = route.endpoint(**params)
except TypeError:
try:
view = route.endpoint()
except TypeError:
view = route.endpoint
pass
# Run on_request first.
try:
# Run the view.
r = getattr(view, "on_request", self.no_response)
r = self.background(r, req, resp, **params)
# If it's async, await it.
if hasattr(r, "send"):
await r
except Exception:
await self.background(self.default_response, req, resp, error=True)
raise
# Then run on_method.
method = req.method
try:
# Run the view.
r = getattr(view, f"on_{method}", self.no_response)
r = self.background(r, req, resp, **params)
# If it's async, await it.
if hasattr(r, "send"):
await r
except Exception as e:
await self.background(self.default_response, req, resp, error=True)
raise
def add_event_handler(self, event_type, handler):
"""Adds an event handler to the API.
@@ -349,13 +403,14 @@ class API:
def add_route(
self,
route,
route=None,
endpoint=None,
*,
default=False,
static=False,
check_existing=True,
websocket=False,
before_request=False,
):
"""Adds a route to the API.
@@ -365,6 +420,9 @@ class API:
:param static: If ``True``, and no endpoint was passed, render "static/index.html", and it will become a default route.
:param check_existing: If ``True``, an AssertionError will be raised, if the route is already defined.
"""
if route is None:
route = f"/{uuid4().hex}"
if check_existing:
assert route not in self.routes
@@ -375,18 +433,25 @@ class API:
if default:
self.default_endpoint = endpoint
self.routes[route] = Route(route, endpoint, websocket=websocket)
self.routes[route] = Route(
route, endpoint, websocket=websocket, before_request=before_request
)
# TODO: A better data structure or sort it once the app is loaded
self.routes = dict(
sorted(self.routes.items(), key=lambda item: item[1]._weight())
)
def default_response(self, req, resp, notfound=False, error=False):
def default_response(
self, req=None, resp=None, websocket=False, notfound=False, error=False
):
if websocket:
return
if resp.status_code is None:
resp.status_code = 200
if self.default_endpoint and notfound:
self.default_endpoint(req, resp)
self.default_endpoint(req=req, resp=resp)
else:
if notfound:
resp.status_code = status_codes.HTTP_404
@@ -400,7 +465,7 @@ class API:
def static_response(self, req, resp):
index = (self.static_dir / "index.html").resolve()
resp.content = ""
resp.content = None
if os.path.exists(index):
with open(index, "r") as f:
resp.text = f.read()
@@ -421,7 +486,7 @@ class API:
:param status_code: an `API.status_codes` attribute, or an integer, representing the HTTP status code of the redirect.
"""
assert resp.status_code.is_300(status_code)
# assert resp.status_code.is_300(status_code)
resp.status_code = status_code
if set_text:
@@ -454,7 +519,7 @@ class API:
return decorator
def route(self, route, **options):
def route(self, route=None, **options):
"""Decorator for creating new routes around function and class definitions.
Usage::
@@ -486,21 +551,19 @@ class API:
"""
if self._session is None:
self._session = TestClient(self)
self._session = TestClient(self, base_url=base_url)
return self._session
def _route_for(self, endpoint):
for (route, route_object) in self.routes.items():
if route_object.endpoint == endpoint:
return route_object
elif route_object.endpoint_name == endpoint:
for route_object in self.routes.values():
if endpoint in (route_object.endpoint, route_object.endpoint_name):
return route_object
def url_for(self, endpoint, **params):
# TODO: Absolute_url
"""Given an endpoint, returns a rendered URL for its route.
:param view: The route endpoint you're searching for.
:param endpoint: The route endpoint you're searching for.
:param params: Data to pass into the URL generator (for parameterized URLs).
"""
route_object = self._route_for(endpoint)
@@ -568,7 +631,7 @@ class API:
template = self.jinja_env.from_string(s_)
return template.render(**values)
def run(self, address=None, port=None, debug=False, **options):
def serve(self, *, address=None, port=None, debug=False, **options):
"""Runs the application with uvicorn. If the ``PORT`` environment
variable is set, requests will be served on that port automatically to all
known hosts.
@@ -593,3 +656,8 @@ class API:
uvicorn.run(self, host=address, port=port, debug=debug, **options)
spawn()
def run(self, **kwargs):
if 'debug' not in kwargs:
kwargs.update({'debug': self.debug})
self.serve(**kwargs)
+11 -2
View File
@@ -1,6 +1,9 @@
import traceback
import multiprocessing
import asyncio
import functools
import concurrent.futures
import multiprocessing
import traceback
from starlette.concurrency import run_in_threadpool
class BackgroundQueue:
@@ -33,3 +36,9 @@ class BackgroundQueue:
return result
return do_task
async def __call__(self, func, *args, **kwargs) -> None:
if asyncio.iscoroutinefunction(func):
return await asyncio.ensure_future(func(*args, **kwargs))
else:
return await run_in_threadpool(func, *args, **kwargs)
+2 -1
View File
@@ -48,8 +48,9 @@ class GraphQLView:
return
query, variables, operation_name = await self._resolve_graphql_query(req)
context = {"request": req, "response": resp}
result = schema.execute(
query, variables=variables, operation_name=operation_name
query, variables=variables, operation_name=operation_name, context=context
)
result, status_code = encode_execution_results(
[result],
+14 -6
View File
@@ -1,11 +1,10 @@
from urllib.parse import parse_qs
import json
import yaml
import json
from parse import findall
from .models import QueryDict
from requests_toolbelt.multipart import decoder
from .models import QueryDict
async def format_form(r, encode=False):
if encode:
@@ -38,6 +37,7 @@ async def format_files(r, encode=False):
dump = {}
for part in decoded.parts:
header = part.headers[b"Content-Disposition"].decode("utf-8")
mimetype = part.headers.get(b"Content-Type", None)
filename = None
for section in [h.strip() for h in header.split(";")]:
@@ -50,9 +50,17 @@ async def format_files(r, encode=False):
if key == "filename":
filename = value
elif key == "name":
formname = value
if filename:
dump[filename] = part.content
if mimetype is None:
dump[formname] = part.content
else:
dump[formname] = {
"filename": filename,
"content": part.content,
"content-type": mimetype.decode("utf-8"),
}
return dump
+7 -3
View File
@@ -1,6 +1,7 @@
import io
import json
import gzip
from base64 import b64decode
from http.cookies import SimpleCookie
@@ -109,8 +110,11 @@ class Request:
def session(self):
"""The session data, in dict form, from the Request."""
if "Responder-Session" in self.cookies:
data = self.cookies[self.api.session_cookie]
data = self.api._signer.unsign(data)
data = b64decode(data)
return json.loads(data)
return {}
@@ -142,7 +146,7 @@ class Request:
def cookies(self):
"""The cookies sent in the Request, as a dictionary."""
cookies = RequestsCookieJar()
cookie_header = self.headers.get("cookie", "")
cookie_header = self.headers.get("Cookie", "")
bc = SimpleCookie(cookie_header)
for k, v in bc.items():
@@ -262,10 +266,10 @@ class Response:
@property
async def body(self):
if self.content:
if self.content is not None:
return (self.content, {})
if self.text:
if self.text is not None:
return (self.text.encode(self.encoding), {"Encoding": self.encoding})
for format in self.formats:
+17 -16
View File
@@ -1,25 +1,17 @@
import re
import functools
import inspect
from parse import parse
def memoize(f):
def helper(self, s):
memoize_key = f"{f.__name__}:{s}"
if memoize_key not in self._memo:
self._memo[memoize_key] = f(self, s)
return self._memo[memoize_key]
return helper
class Route:
_param_pattern = re.compile(r"{([^{}]*)}")
def __init__(self, route, endpoint, *, websocket=False):
def __init__(self, route, endpoint, *, websocket=False, before_request=False):
self.route = route
self.endpoint = endpoint
self.uses_websocket = websocket
self._memo = {}
self.before_request = before_request
def __repr__(self):
return f"<Route {self.route!r}={self.endpoint!r}>"
@@ -44,7 +36,7 @@ class Route:
def has_parameters(self):
return bool(self._param_pattern.search(self.route))
@memoize
@functools.lru_cache(maxsize=None)
def does_match(self, s):
if s == self.route:
return True
@@ -52,7 +44,7 @@ class Route:
named = self.incoming_matches(s)
return bool(len(named))
@memoize
@functools.lru_cache(maxsize=None)
def incoming_matches(self, s):
results = parse(self.route, s)
return results.named if results else {}
@@ -63,14 +55,23 @@ class Route:
def _weight(self):
params = set(self._param_pattern.findall(self.route))
params_count = len(params)
return params_count != 0, -params_count
w = len(self.route.rsplit('}', 1)[-1].strip('/'))
return params_count != 0, w == 0, -params_count
@property
def is_class_based(self):
return hasattr(self.endpoint, "__class__")
return inspect.isclass(self.endpoint)
@property
def is_function(self):
code = hasattr(self.endpoint, "__code__")
kwdefaults = hasattr(self.endpoint, "__kwdefaults__")
return all((callable(self.endpoint), code, kwdefaults))
def __hash__(self):
return (
hash(self.route)
^ hash(self.endpoint)
^ hash(self.uses_websocket)
^ hash(self.before_request)
)
+3 -3
View File
@@ -22,7 +22,7 @@ if sys.argv[-1] == "publish":
sys.exit()
required = [
"starlette",
"starlette>=0.9,<0.10",
"uvicorn",
"aiofiles",
"pyyaml",
@@ -31,7 +31,7 @@ required = [
"graphql-server-core>=1.1",
"jinja2",
"parse",
"uvloop ; sys_platform != 'win32'",
"uvloop; sys_platform != 'win32'",
"rfc3986",
"python-multipart",
"chardet",
@@ -70,7 +70,7 @@ class DebCommand(Command):
rmtree(os.path.join(here, "deb_dist"))
except FileNotFoundError:
pass
self.status(u"Creating debian mainfest…")
self.status(u"Creating debian manifest…")
os.system(
"python setup.py --command-packages=stdeb.command sdist_dsc -z artful --package3=pipenv --depends3=python3-virtualenv-clone"
)
+4 -1
View File
@@ -18,7 +18,10 @@ def current_dir():
@pytest.fixture
def api():
return responder.API()
return responder.API(
debug=False,
allowed_hosts=[";"]
)
@pytest.fixture
+102 -27
View File
@@ -333,7 +333,9 @@ def test_schema_generation():
import responder
from marshmallow import Schema, fields
api = responder.API(title="Web Service", openapi="3.0")
api = responder.API(
title="Web Service", openapi="3.0", allowed_hosts=["testserver", ";"]
)
@api.schema("Pet")
class PetSchema(Schema):
@@ -364,7 +366,12 @@ def test_documentation():
import responder
from marshmallow import Schema, fields
api = responder.API(title="Web Service", openapi="3.0", docs_route="/docs")
api = responder.API(
title="Web Service",
openapi="3.0",
docs_route="/docs",
allowed_hosts=["testserver", ";"],
)
@api.schema("Pet")
class PetSchema(Schema):
@@ -424,6 +431,7 @@ def test_cookies(api):
assert r.json() == {"cookies": {"sent": "true"}}
@pytest.mark.xfail
def test_sessions(api):
@api.route("/")
def view(req, resp):
@@ -455,28 +463,27 @@ def test_file_uploads(api):
async def upload(req, resp):
files = await req.media("files")
files["hello"] = files["hello"].decode("utf-8")
resp.media = {"files": files}
result = {}
result["hello"] = files["hello"]["content"].decode("utf-8")
result["not-a-file"] = files["not-a-file"].decode("utf-8")
resp.media = {"files": result}
world = io.StringIO("world")
data = {"hello": world}
data = {"hello": ("hello.txt", world, "text/plain"), "not-a-file": b"data only"}
r = api.requests.post(api.url_for(upload), files=data)
assert r.json() == {"files": {"hello": "world"}}
assert r.json() == {"files": {"hello": "world", "not-a-file": "data only"}}
def test_500(api):
def catcher(request, exc):
return PlainTextResponse("Suppressed error", 500)
api.app.add_exception_handler(ValueError, catcher)
@api.route("/")
def view(req, resp):
raise ValueError
r = api.requests.get(api.url_for(view))
dumb_client = responder.api.TestClient(api, base_url="http://;",
raise_server_exceptions=False)
r = dumb_client.get(api.url_for(view))
assert not r.ok
assert r.content == b"Suppressed error"
assert r.status_code == responder.status_codes.HTTP_500
def test_404(api):
@@ -493,8 +500,7 @@ def test_kinda_websockets(api):
await ws.close()
@pytest.mark.xfail
def test_startup(api, session):
def test_startup(api):
who = [None]
@api.route("/{greeting}")
@@ -502,19 +508,12 @@ def test_startup(api, session):
resp.text = f"{greeting}, {who[0]}!"
@api.on_event("startup")
async def asd():
async def run_startup():
who[0] = "world"
print("startup")
@api.on_event("cleanup")
async def asd():
print("cleanup")
pool = concurrent.futures.ThreadPoolExecutor(max_workers=2)
f = pool.submit(api.run)
r = requests.get(f"http://localhost:5042/hello")
assert r.text == "hello, world!"
with api.requests as session:
r = session.get(f"http://;/hello")
assert r.text == "hello, world!"
def test_redirects(api, session):
@@ -526,4 +525,80 @@ def test_redirects(api, session):
def one(req, resp):
resp.text = "redirected"
assert session.get("/1").url == "http://testserver/1"
assert session.get("/1").url == "http://;/1"
def test_session_thoroughly(api, session):
@api.route("/set")
def set(req, resp):
resp.session["hello"] = "world"
api.redirect(resp, location="/get")
@api.route("/get")
def get(req, resp):
resp.media = {"session": req.session}
r = session.get(api.url_for(set))
r = session.get(api.url_for(get))
assert r.json() == {"session": {"hello": "world"}}
def test_before_response(api, session):
@api.route("/get")
def get(req, resp):
resp.media = req.session
@api.route(before_request=True)
def before_request(req, resp):
resp.headers["x-pizza"] = "1"
r = session.get(api.url_for(get))
assert "x-pizza" in r.headers
def test_allowed_hosts():
api = responder.API(allowed_hosts=[";", "tenant.;"])
@api.route("/")
def get(req, resp):
pass
# Exact match
r = api.requests.get(api.url_for(get))
assert r.status_code == 200
# Reset the session
api._session = None
r = api.session(base_url="http://tenant.;").get(api.url_for(get))
assert r.status_code == 200
# Reset the session
api._session = None
r = api.session(base_url="http://unkownhost").get(api.url_for(get))
assert r.status_code == 400
# Reset the session
api._session = None
r = api.session(base_url="http://unkown_tenant.;").get(api.url_for(get))
assert r.status_code == 400
api = responder.API(allowed_hosts=["*.;"])
@api.route("/")
def get(req, resp):
pass
# Wildcard domains
# Using http://;
r = api.requests.get(api.url_for(get))
assert r.status_code == 400
# Reset the session
api._session = None
r = api.session(base_url="http://tenant1.;").get(api.url_for(get))
assert r.status_code == 200
# Reset the session
api._session = None
r = api.session(base_url="http://tenant2.;").get(api.url_for(get))
assert r.status_code == 200
+29 -17
View File
@@ -2,6 +2,10 @@ import pytest
from responder import routes
def setup_function(function):
routes.Route.incoming_matches.cache_clear()
@pytest.mark.parametrize(
"route, expected",
[
@@ -36,26 +40,23 @@ def test_incoming_matches():
assert r.incoming_matches("/hello") == {"greetings": "hello"}
assert r.incoming_matches("/foo") == {"greetings": "foo"}
assert r._memo == {
"incoming_matches:/hello": {"greetings": "hello"},
"incoming_matches:/foo": {"greetings": "foo"},
}
# Test Route with two params
r = routes.Route("/{greetings}/{name}", "test_endpoint")
assert r.incoming_matches("/hi/john") == {"greetings": "hi", "name": "john"}
assert r.incoming_matches("/hello/jane") == {"greetings": "hello", "name": "jane"}
# Test Route with no param
assert r._memo == {
"incoming_matches:/hi/john": {"greetings": "hi", "name": "john"},
"incoming_matches:/hello/jane": {"greetings": "hello", "name": "jane"},
}
r = routes.Route("/hello", "test_endpoint")
assert r.incoming_matches("/hello") == {}
assert r.incoming_matches("/bye") == {}
assert r._memo == {"incoming_matches:/hello": {}, "incoming_matches:/bye": {}}
def test_incoming_matches_cache():
r = routes.Route("/hello", "test_endpoint")
r.incoming_matches("/hello")
assert r.incoming_matches.cache_info().hits == 0
r.incoming_matches("/hello")
assert r.incoming_matches.cache_info().hits == 1
def test_incoming_matches_with_concrete_path_no_match():
@@ -86,18 +87,29 @@ def test_does_match_with_route(route, match, expected):
@pytest.mark.parametrize(
"path_param, expected_weight",
[
pytest.param("/{greetings}", (True, -1), id="with one param"),
pytest.param("/{greetings}", (True, True, -1), id="with one param"),
pytest.param(
"/{greetings}.{name}", (True, -2), id="with 2 params and dot in the middle"
"/{greetings}.{name}", (True, True, -2), id="with 2 params and dot in the middle"
),
pytest.param("/{greetings}/{name}", (True, -2), id="with 2 param and subpath"),
pytest.param("/{greetings}/{name}", (True, True, -2), id="with 2 param and subpath"),
pytest.param(
"/{greetings}/{name}/{hello}", (True, -3), id="with 3 param and subpath"
"/{greetings}/{name}/{hello}", (True, True, -3), id="with 3 param and subpath"
),
pytest.param(
"/{greetings}_{name}", (True, -2), id="with 2 param and underscore"
"/{greetings}_{name}", (True, True, -2), id="with 2 param and underscore"
),
pytest.param("/hello", (False, 0), id="with 2 param and underscore"),
pytest.param("/{greetings}/9roda", (True, False, -1), id="with one param"),
pytest.param(
"/{greetings}.{name}/9roda", (True, False, -2), id="with 2 params and dot in the middle"
),
pytest.param("/{greetings}/{name}/9roda", (True, False, -2), id="with 2 param and subpath"),
pytest.param(
"/{greetings}/{name}/{hello}/9roda", (True, False, -3), id="with 3 param and subpath"
),
pytest.param(
"/{greetings}_{name}/9roda", (True, False, -2), id="with 2 param and underscore"
),
pytest.param("/hello", (False, False, 0), id="with 2 param and underscore"),
],
)
def test_weight(path_param, expected_weight):
+8 -8
View File
@@ -8,7 +8,7 @@ from responder import status_codes
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")
pytest.param(200, False, id="Above 100"),
],
)
def test_is_100(status_code, expected):
@@ -21,7 +21,7 @@ def test_is_100(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")
pytest.param(300, False, id="Above 200"),
],
)
def test_is_200(status_code, expected):
@@ -34,7 +34,7 @@ def test_is_200(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")
pytest.param(400, False, id="Above 300"),
],
)
def test_is_300(status_code, expected):
@@ -47,7 +47,7 @@ def test_is_300(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")
pytest.param(500, False, id="Above 400"),
],
)
def test_is_400(status_code, expected):
@@ -57,10 +57,10 @@ def test_is_400(status_code, 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")
pytest.param(501, True, id="Normal 501"),
pytest.param(599, True, id="Not actual status code but within 500"),
pytest.param(0, False, id="Zero case (below 500)"),
pytest.param(600, False, id="Above 500"),
],
)
def test_is_500(status_code, expected):