mirror of
https://github.com/kennethreitz/responder.git
synced 2026-06-05 23:00:17 +00:00
Compare commits
33 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 109937adf4 | |||
| 63ea9cc4e0 | |||
| ec40a0c4c3 | |||
| 0855d1a378 | |||
| 77fe17d350 | |||
| 0b8a031ccb | |||
| 0678daa880 | |||
| 6761e3bdd8 | |||
| ead213a506 | |||
| 75b5782eee | |||
| a80df809e4 | |||
| 7f3177f662 | |||
| 906cd2fbbf | |||
| 9d0129da56 | |||
| aedcf12d99 | |||
| 86361523e2 | |||
| a7110ef441 | |||
| d3e4968546 | |||
| 03e34d56ab | |||
| b470d10416 | |||
| b8aea89039 | |||
| 4a1e89af1b | |||
| 81fbc94d36 | |||
| 6487671559 | |||
| 838c7f29b5 | |||
| df85a4c214 | |||
| 1bdbea238e | |||
| 97dbef92d9 | |||
| 85c1c0036c | |||
| 0653ee2c6b | |||
| d1db913c7d | |||
| d24b921cdc | |||
| b31b742787 |
+30
-1
@@ -5,6 +5,29 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [Unreleased]
|
||||
## [v2.0.5] - 2019-12-15
|
||||
### Added
|
||||
- Update requirements to support python 3.8
|
||||
|
||||
## [v2.0.4] - 2019-11-19
|
||||
### Fixed
|
||||
- Fix static app resolving
|
||||
|
||||
## [v2.0.3] - 2019-09-20
|
||||
### Fixed
|
||||
- Fix template conflicts
|
||||
|
||||
## [v2.0.2] - 2019-09-20
|
||||
### Fixed
|
||||
- Fix template conflicts
|
||||
|
||||
## [v2.0.1] - 2019-09-20
|
||||
### Fixed
|
||||
- Fix template import
|
||||
|
||||
## [v2.0.0] - 2019-09-19
|
||||
### Changed
|
||||
- Refactor Router and Schema
|
||||
|
||||
## [v1.3.2] - 2019-08-15
|
||||
### Added
|
||||
@@ -208,7 +231,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
### Added
|
||||
- Conception!
|
||||
|
||||
[Unreleased]: https://github.com/taoufik07/responder/compare/v1.3.2..HEAD
|
||||
[Unreleased]: https://github.com/taoufik07/responder/compare/v2.0.5..HEAD
|
||||
[v2.0.5]: https://github.com/taoufik07/responder/compare/v2.0.4..v2.0.5
|
||||
[v2.0.4]: https://github.com/taoufik07/responder/compare/v2.0.3..v2.0.4
|
||||
[v2.0.3]: https://github.com/taoufik07/responder/compare/v2.0.2..v2.0.3
|
||||
[v2.0.2]: https://github.com/taoufik07/responder/compare/v2.0.1..v2.0.2
|
||||
[v2.0.1]: https://github.com/taoufik07/responder/compare/v2.0.0..v2.0.1
|
||||
[v2.0.0]: https://github.com/taoufik07/responder/compare/v1.3.2..v2.0.0
|
||||
[v1.3.2]: https://github.com/taoufik07/responder/compare/v1.3.1..v1.3.2
|
||||
[v1.3.1]: https://github.com/taoufik07/responder/compare/v1.3.0..v1.3.1
|
||||
[v1.3.0]: https://github.com/taoufik07/responder/compare/v1.2.0..v1.3.0
|
||||
|
||||
Generated
+325
-197
@@ -23,17 +23,17 @@
|
||||
},
|
||||
"aniso8601": {
|
||||
"hashes": [
|
||||
"sha256:b8a6a9b24611fc50cf2d9b45d371bfdc4fd0581d1cc52254f5502130a776d4af",
|
||||
"sha256:bb167645c79f7a438f9dfab6161af9bed75508c645b1f07d1158240841d22673"
|
||||
"sha256:513d2b6637b7853806ae79ffaca6f3e8754bdd547048f5ccc1420aec4b714f1e",
|
||||
"sha256:d10a4bf949f619f719b227ef5386e31f49a2b6d453004b21f02661ccc8670c7b"
|
||||
],
|
||||
"version": "==6.0.0"
|
||||
"version": "==7.0.0"
|
||||
},
|
||||
"apispec": {
|
||||
"hashes": [
|
||||
"sha256:11d1aaf620a80f67ded7688fcaf14fa4fd975d566876b5db69b067ffbfe4d1d9",
|
||||
"sha256:2f4734203e53f6ae6499d1f2168d19e62f04f071d6e4b86c6d6014ebf2ebdf6b"
|
||||
"sha256:cf8e1f3b56949710f8cf23797b7f40215e9dae8bac583789a3f2c13dc56349fa",
|
||||
"sha256:fe5cf5fc89b1c4a73acd5af3a10ede02b31ec116f215ed02271cb905d3172367"
|
||||
],
|
||||
"version": "==2.0.2"
|
||||
"version": "==3.1.0"
|
||||
},
|
||||
"apistar": {
|
||||
"hashes": [
|
||||
@@ -43,10 +43,10 @@
|
||||
},
|
||||
"certifi": {
|
||||
"hashes": [
|
||||
"sha256:046832c04d4e752f37383b628bc601a7ea7211496b4638f6514d0e5b9acc4939",
|
||||
"sha256:945e3ba63a0b9f577b1395204e13c3a231f9bc0223888be653286534e5873695"
|
||||
"sha256:017c25db2a153ce562900032d5bc68e9f191e44e9a0f762f373977de9df1fbb3",
|
||||
"sha256:25b64c7da4cd7479594d035c08c2d809eb4aab3a26e5a990ea98cc450c320f1f"
|
||||
],
|
||||
"version": "==2019.6.16"
|
||||
"version": "==2019.11.28"
|
||||
},
|
||||
"chardet": {
|
||||
"hashes": [
|
||||
@@ -70,10 +70,10 @@
|
||||
},
|
||||
"graphene": {
|
||||
"hashes": [
|
||||
"sha256:77d61618132ccd084c343e64c22d806cee18dce73cc86e0f427378dbdeeac287",
|
||||
"sha256:acf808d50d053b94f7958414d511489a9e490a7f9563b9be80f6875fc5723d2a"
|
||||
"sha256:09165f03e1591b76bf57b133482db9be6dac72c74b0a628d3c93182af9c5a896",
|
||||
"sha256:2cbe6d4ef15cfc7b7805e0760a0e5b80747161ce1b0f990dfdc0d2cf497c12f9"
|
||||
],
|
||||
"version": "==2.1.7"
|
||||
"version": "==2.1.8"
|
||||
},
|
||||
"graphql-core": {
|
||||
"hashes": [
|
||||
@@ -84,17 +84,16 @@
|
||||
},
|
||||
"graphql-relay": {
|
||||
"hashes": [
|
||||
"sha256:0e94201af4089e1f81f07d7bd8f84799768e39d70fa1ea16d1df505b46cc6335",
|
||||
"sha256:75aa0758971e252964cb94068a4decd472d2a8295229f02189e3cbca1f10dbb5",
|
||||
"sha256:7fa74661246e826ef939ee92e768f698df167a7617361ab399901eaebf80dce6"
|
||||
"sha256:870b6b5304123a38a0b215a79eace021acce5a466bf40cd39fa18cb8528afabb",
|
||||
"sha256:ac514cb86db9a43014d7e73511d521137ac12cf0101b2eaa5f0a3da2e10d913d"
|
||||
],
|
||||
"version": "==2.0.0"
|
||||
"version": "==2.0.1"
|
||||
},
|
||||
"graphql-server-core": {
|
||||
"hashes": [
|
||||
"sha256:e5f82add4b3d5580aa1f1e7d9f00e944ad3abe1b65eb337e611d6a77cc20f231"
|
||||
"sha256:fa810f811cd91dd938ac49819562c3a12f2b6bf141164f984b16359359133983"
|
||||
],
|
||||
"version": "==1.1.1"
|
||||
"version": "==1.1.3"
|
||||
},
|
||||
"h11": {
|
||||
"hashes": [
|
||||
@@ -125,10 +124,10 @@
|
||||
},
|
||||
"jinja2": {
|
||||
"hashes": [
|
||||
"sha256:065c4f02ebe7f7cf559e49ee5a95fb800a9e4528727aec6f24402a5374c65013",
|
||||
"sha256:14dd6caf1527abb21f08f86c784eac40853ba93edb79552aa1e4b8aef1b61c7b"
|
||||
"sha256:74320bb91f31270f9551d46522e33af46a80c3d619f4a4bf42b3164d30b5911f",
|
||||
"sha256:9fe95f19286cfefaa917656583d020be14e7859c6b0252588391e47db34527de"
|
||||
],
|
||||
"version": "==2.10.1"
|
||||
"version": "==2.10.3"
|
||||
},
|
||||
"markupsafe": {
|
||||
"hashes": [
|
||||
@@ -165,16 +164,10 @@
|
||||
},
|
||||
"marshmallow": {
|
||||
"hashes": [
|
||||
"sha256:7ea8540fc7e35be3b0af8b017313944b984d5acdb118b4ba3c270ac9611765c7",
|
||||
"sha256:bc91e3f90e86133241ac62ea0dd35217d631a207e8628430bc66c347dbe12f7d"
|
||||
"sha256:0ba81b6da4ae69eb229b74b3c741ff13fe04fb899824377b1aff5aaa1a9fd46e",
|
||||
"sha256:3e53dd9e9358977a3929e45cdbe4a671f9eff53a7d6a23f33ed3eab8c1890d8f"
|
||||
],
|
||||
"version": "==3.0.0rc9"
|
||||
},
|
||||
"parse": {
|
||||
"hashes": [
|
||||
"sha256:1b68657434d371e5156048ca4a0c5aea5afc6ca59a2fea4dd1a575354f617142"
|
||||
],
|
||||
"version": "==1.12.0"
|
||||
"version": "==3.3.0"
|
||||
},
|
||||
"promise": {
|
||||
"hashes": [
|
||||
@@ -191,21 +184,19 @@
|
||||
},
|
||||
"pyyaml": {
|
||||
"hashes": [
|
||||
"sha256:0113bc0ec2ad727182326b61326afa3d1d8280ae1122493553fd6f4397f33df9",
|
||||
"sha256:01adf0b6c6f61bd11af6e10ca52b7d4057dd0be0343eb9283c878cf3af56aee4",
|
||||
"sha256:5124373960b0b3f4aa7df1707e63e9f109b5263eca5976c66e08b1c552d4eaf8",
|
||||
"sha256:5ca4f10adbddae56d824b2c09668e91219bb178a1eee1faa56af6f99f11bf696",
|
||||
"sha256:7907be34ffa3c5a32b60b95f4d95ea25361c951383a894fec31be7252b2b6f34",
|
||||
"sha256:7ec9b2a4ed5cad025c2278a1e6a19c011c80a3caaac804fd2d329e9cc2c287c9",
|
||||
"sha256:87ae4c829bb25b9fe99cf71fbb2140c448f534e24c998cc60f39ae4f94396a73",
|
||||
"sha256:9de9919becc9cc2ff03637872a440195ac4241c80536632fffeb6a1e25a74299",
|
||||
"sha256:a5a85b10e450c66b49f98846937e8cfca1db3127a9d5d1e31ca45c3d0bef4c5b",
|
||||
"sha256:b0997827b4f6a7c286c01c5f60384d218dca4ed7d9efa945c3e1aa623d5709ae",
|
||||
"sha256:b631ef96d3222e62861443cc89d6563ba3eeb816eeb96b2629345ab795e53681",
|
||||
"sha256:bf47c0607522fdbca6c9e817a6e81b08491de50f3766a7a0e6a5be7905961b41",
|
||||
"sha256:f81025eddd0327c7d4cfe9b62cf33190e1e736cc6e97502b3ec425f574b3e7a8"
|
||||
"sha256:0e7f69397d53155e55d10ff68fdfb2cf630a35e6daf65cf0bdeaf04f127c09dc",
|
||||
"sha256:2e9f0b7c5914367b0916c3c104a024bb68f269a486b9d04a2e8ac6f6597b7803",
|
||||
"sha256:35ace9b4147848cafac3db142795ee42deebe9d0dad885ce643928e88daebdcc",
|
||||
"sha256:38a4f0d114101c58c0f3a88aeaa44d63efd588845c5a2df5290b73db8f246d15",
|
||||
"sha256:483eb6a33b671408c8529106df3707270bfacb2447bf8ad856a4b4f57f6e3075",
|
||||
"sha256:4b6be5edb9f6bb73680f5bf4ee08ff25416d1400fbd4535fe0069b2994da07cd",
|
||||
"sha256:7f38e35c00e160db592091751d385cd7b3046d6d51f578b29943225178257b31",
|
||||
"sha256:8100c896ecb361794d8bfdb9c11fce618c7cf83d624d73d5ab38aef3bc82d43f",
|
||||
"sha256:c0ee8eca2c582d29c3c2ec6e2c4f703d1b7f1fb10bc72317355a746057e7346c",
|
||||
"sha256:e4c015484ff0ff197564917b4b4246ca03f411b9bd7f16e02a2f586eb48b6d04",
|
||||
"sha256:ebc4ed52dcc93eeebeae5cf5deb2ae4347b3a81c3fa12b0b8c976544829396a4"
|
||||
],
|
||||
"version": "==5.1.2"
|
||||
"version": "==5.2"
|
||||
},
|
||||
"requests": {
|
||||
"hashes": [
|
||||
@@ -241,16 +232,16 @@
|
||||
},
|
||||
"six": {
|
||||
"hashes": [
|
||||
"sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c",
|
||||
"sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73"
|
||||
"sha256:1f1b7d42e254082a9db6279deae68afb421ceba6158efa6131de7b3003ee93fd",
|
||||
"sha256:30f610279e8b2578cab6db20741130331735c781b56053c59c4076da27f06b66"
|
||||
],
|
||||
"version": "==1.12.0"
|
||||
"version": "==1.13.0"
|
||||
},
|
||||
"starlette": {
|
||||
"hashes": [
|
||||
"sha256:849553e6dc8dd4faac5d4383219bdf49b199f3f09d60ce374c2a00a42ab7c77d"
|
||||
"sha256:9597bc28e3c4659107c1c4a45ec32dc45e947d78fe56230222be673b2c36454a"
|
||||
],
|
||||
"version": "==0.12.7"
|
||||
"version": "==0.12.13"
|
||||
},
|
||||
"typesystem": {
|
||||
"hashes": [
|
||||
@@ -260,64 +251,64 @@
|
||||
},
|
||||
"urllib3": {
|
||||
"hashes": [
|
||||
"sha256:b246607a25ac80bedac05c6f282e3cdaf3afb65420fd024ac94435cabe6e18d1",
|
||||
"sha256:dbe59173209418ae49d485b87d1681aefa36252ee85884c31346debd19463232"
|
||||
"sha256:a8a318824cc77d1fd4b2bec2ded92646630d7fe8619497b142c84a9e6f5a7293",
|
||||
"sha256:f3c5fd51747d450d4dcf6f923c81f78f811aab8205fda64b0aba34a4e48b0745"
|
||||
],
|
||||
"version": "==1.25.3"
|
||||
"version": "==1.25.7"
|
||||
},
|
||||
"uvicorn": {
|
||||
"hashes": [
|
||||
"sha256:8aa44f9d9c3082ef693950387ea25d376e32944df6d4071dbd8edc3c25a40c74"
|
||||
"sha256:f4c34642618449f55e2bab8c6b22ff7615b520d2e7e23275be2ca894254327a3"
|
||||
],
|
||||
"version": "==0.8.6"
|
||||
"version": "==0.10.8"
|
||||
},
|
||||
"uvloop": {
|
||||
"hashes": [
|
||||
"sha256:0fcd894f6fc3226a962ee7ad895c4f52e3f5c3c55098e21efb17c071849a0573",
|
||||
"sha256:2f31de1742c059c96cb76b91c5275b22b22b965c886ee1fced093fa27dde9e64",
|
||||
"sha256:459e4649fcd5ff719523de33964aa284898e55df62761e7773d088823ccbd3e0",
|
||||
"sha256:67867aafd6e0bc2c30a079603a85d83b94f23c5593b3cc08ec7e58ac18bf48e5",
|
||||
"sha256:8c200457e6847f28d8bb91c5e5039d301716f5f2fce25646f5fb3fd65eda4a26",
|
||||
"sha256:958906b9ca39eb158414fbb7d6b8ef1b7aee4db5c8e8e5d00fcbb69a1ce9dca7",
|
||||
"sha256:ac1dca3d8f3ef52806059e81042ee397ac939e5a86c8a3cea55d6b087db66115",
|
||||
"sha256:b284c22d8938866318e3b9d178142b8be316c52d16fcfe1560685a686718a021",
|
||||
"sha256:c48692bf4587ce281d641087658eca275a5ad3b63c78297bbded96570ae9ce8f",
|
||||
"sha256:fefc3b2b947c99737c348887db2c32e539160dcbeb7af9aa6b53db7a283538fe"
|
||||
"sha256:08b109f0213af392150e2fe6f81d33261bb5ce968a288eb698aad4f46eb711bd",
|
||||
"sha256:123ac9c0c7dd71464f58f1b4ee0bbd81285d96cdda8bc3519281b8973e3a461e",
|
||||
"sha256:4315d2ec3ca393dd5bc0b0089d23101276778c304d42faff5dc4579cb6caef09",
|
||||
"sha256:4544dcf77d74f3a84f03dd6278174575c44c67d7165d4c42c71db3fdc3860726",
|
||||
"sha256:afd5513c0ae414ec71d24f6f123614a80f3d27ca655a4fcf6cabe50994cc1891",
|
||||
"sha256:b4f591aa4b3fa7f32fb51e2ee9fea1b495eb75b0b3c8d0ca52514ad675ae63f7",
|
||||
"sha256:bcac356d62edd330080aed082e78d4b580ff260a677508718f88016333e2c9c5",
|
||||
"sha256:e7514d7a48c063226b7d06617cbb12a14278d4323a065a8d46a7962686ce2e95",
|
||||
"sha256:f07909cd9fc08c52d294b1570bba92186181ca01fe3dc9ffba68955273dd7362"
|
||||
],
|
||||
"version": "==0.12.2"
|
||||
"version": "==0.14.0"
|
||||
},
|
||||
"websockets": {
|
||||
"hashes": [
|
||||
"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"
|
||||
"sha256:0e4fb4de42701340bd2353bb2eee45314651caa6ccee80dbd5f5d5978888fed5",
|
||||
"sha256:1d3f1bf059d04a4e0eb4985a887d49195e15ebabc42364f4eb564b1d065793f5",
|
||||
"sha256:20891f0dddade307ffddf593c733a3fdb6b83e6f9eef85908113e628fa5a8308",
|
||||
"sha256:295359a2cc78736737dd88c343cd0747546b2174b5e1adc223824bcaf3e164cb",
|
||||
"sha256:2db62a9142e88535038a6bcfea70ef9447696ea77891aebb730a333a51ed559a",
|
||||
"sha256:3762791ab8b38948f0c4d281c8b2ddfa99b7e510e46bd8dfa942a5fff621068c",
|
||||
"sha256:3db87421956f1b0779a7564915875ba774295cc86e81bc671631379371af1170",
|
||||
"sha256:3ef56fcc7b1ff90de46ccd5a687bbd13a3180132268c4254fc0fa44ecf4fc422",
|
||||
"sha256:4f9f7d28ce1d8f1295717c2c25b732c2bc0645db3215cf757551c392177d7cb8",
|
||||
"sha256:5c01fd846263a75bc8a2b9542606927cfad57e7282965d96b93c387622487485",
|
||||
"sha256:5c65d2da8c6bce0fca2528f69f44b2f977e06954c8512a952222cea50dad430f",
|
||||
"sha256:751a556205d8245ff94aeef23546a1113b1dd4f6e4d102ded66c39b99c2ce6c8",
|
||||
"sha256:7ff46d441db78241f4c6c27b3868c9ae71473fe03341340d2dfdbe8d79310acc",
|
||||
"sha256:965889d9f0e2a75edd81a07592d0ced54daa5b0785f57dc429c378edbcffe779",
|
||||
"sha256:9b248ba3dd8a03b1a10b19efe7d4f7fa41d158fdaa95e2cf65af5a7b95a4f989",
|
||||
"sha256:9bef37ee224e104a413f0780e29adb3e514a5b698aabe0d969a6ba426b8435d1",
|
||||
"sha256:c1ec8db4fac31850286b7cd3b9c0e1b944204668b8eb721674916d4e28744092",
|
||||
"sha256:c8a116feafdb1f84607cb3b14aa1418424ae71fee131642fc568d21423b51824",
|
||||
"sha256:ce85b06a10fc65e6143518b96d3dca27b081a740bae261c2fb20375801a9d56d",
|
||||
"sha256:d705f8aeecdf3262379644e4b55107a3b55860eb812b673b28d0fbc347a60c55",
|
||||
"sha256:e898a0863421650f0bebac8ba40840fc02258ef4714cb7e1fd76b6a6354bda36",
|
||||
"sha256:f8a7bff6e8664afc4e6c28b983845c5bc14965030e3fb98789734d416af77c4b"
|
||||
],
|
||||
"version": "==7.0"
|
||||
"version": "==8.1"
|
||||
},
|
||||
"whitenoise": {
|
||||
"hashes": [
|
||||
"sha256:59d880d25d0e90bcc6554fe0504a11195bd2e59b3d690b6fb42a8040d4e67ef5",
|
||||
"sha256:c9b7c47fdc1dba4d37bf2787a01a844dc7a521e174fcd22a2d429e0be65e1782"
|
||||
"sha256:0f9137f74bd95fa54329ace88d8dc695fbe895369a632e35f7a136e003e41d73",
|
||||
"sha256:62556265ec1011bd87113fb81b7516f52688887b7a010ee899ff1fd18fd22700"
|
||||
],
|
||||
"version": "==4.1.3"
|
||||
"version": "==5.0.1"
|
||||
}
|
||||
},
|
||||
"develop": {
|
||||
@@ -335,19 +326,12 @@
|
||||
],
|
||||
"version": "==1.4.3"
|
||||
},
|
||||
"atomicwrites": {
|
||||
"hashes": [
|
||||
"sha256:03472c30eb2c5d1ba9227e4c2ca66ab8287fbfbbda3888aa93dc2e28fc6811b4",
|
||||
"sha256:75a9445bac02d8d058d5e1fe689654ba5a6556a1dfd8ce6ec55a0ed79866cfa6"
|
||||
],
|
||||
"version": "==1.3.0"
|
||||
},
|
||||
"attrs": {
|
||||
"hashes": [
|
||||
"sha256:69c0dbf2ed392de1cb5ec704444b08a5ef81680a61cb899dc08127123af36a79",
|
||||
"sha256:f0b870f674851ecbfbbbd364d6b5cbdff9dcedbc7f3f5e18a6891057f21fe399"
|
||||
"sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c",
|
||||
"sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72"
|
||||
],
|
||||
"version": "==19.1.0"
|
||||
"version": "==19.3.0"
|
||||
},
|
||||
"babel": {
|
||||
"hashes": [
|
||||
@@ -358,11 +342,11 @@
|
||||
},
|
||||
"black": {
|
||||
"hashes": [
|
||||
"sha256:09a9dcb7c46ed496a9850b76e4e825d6049ecd38b611f1224857a79bd985a8cf",
|
||||
"sha256:68950ffd4d9169716bcb8719a56c07a2f4485354fec061cdd5910aa07369731c"
|
||||
"sha256:1b30e59be925fafc1ee4565e5e08abef6b03fe455102883820fe5ee2e4734e0b",
|
||||
"sha256:c2edb73a08e9e0e6f65a0e6af18b059b8b1cdd5bef997d7a0b181df93dc81539"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==19.3b0"
|
||||
"version": "==19.10b0"
|
||||
},
|
||||
"bleach": {
|
||||
"hashes": [
|
||||
@@ -373,10 +357,48 @@
|
||||
},
|
||||
"certifi": {
|
||||
"hashes": [
|
||||
"sha256:046832c04d4e752f37383b628bc601a7ea7211496b4638f6514d0e5b9acc4939",
|
||||
"sha256:945e3ba63a0b9f577b1395204e13c3a231f9bc0223888be653286534e5873695"
|
||||
"sha256:017c25db2a153ce562900032d5bc68e9f191e44e9a0f762f373977de9df1fbb3",
|
||||
"sha256:25b64c7da4cd7479594d035c08c2d809eb4aab3a26e5a990ea98cc450c320f1f"
|
||||
],
|
||||
"version": "==2019.6.16"
|
||||
"version": "==2019.11.28"
|
||||
},
|
||||
"cffi": {
|
||||
"hashes": [
|
||||
"sha256:0b49274afc941c626b605fb59b59c3485c17dc776dc3cc7cc14aca74cc19cc42",
|
||||
"sha256:0e3ea92942cb1168e38c05c1d56b0527ce31f1a370f6117f1d490b8dcd6b3a04",
|
||||
"sha256:135f69aecbf4517d5b3d6429207b2dff49c876be724ac0c8bf8e1ea99df3d7e5",
|
||||
"sha256:19db0cdd6e516f13329cba4903368bff9bb5a9331d3410b1b448daaadc495e54",
|
||||
"sha256:2781e9ad0e9d47173c0093321bb5435a9dfae0ed6a762aabafa13108f5f7b2ba",
|
||||
"sha256:291f7c42e21d72144bb1c1b2e825ec60f46d0a7468f5346841860454c7aa8f57",
|
||||
"sha256:2c5e309ec482556397cb21ede0350c5e82f0eb2621de04b2633588d118da4396",
|
||||
"sha256:2e9c80a8c3344a92cb04661115898a9129c074f7ab82011ef4b612f645939f12",
|
||||
"sha256:32a262e2b90ffcfdd97c7a5e24a6012a43c61f1f5a57789ad80af1d26c6acd97",
|
||||
"sha256:3c9fff570f13480b201e9ab69453108f6d98244a7f495e91b6c654a47486ba43",
|
||||
"sha256:415bdc7ca8c1c634a6d7163d43fb0ea885a07e9618a64bda407e04b04333b7db",
|
||||
"sha256:42194f54c11abc8583417a7cf4eaff544ce0de8187abaf5d29029c91b1725ad3",
|
||||
"sha256:4424e42199e86b21fc4db83bd76909a6fc2a2aefb352cb5414833c030f6ed71b",
|
||||
"sha256:4a43c91840bda5f55249413037b7a9b79c90b1184ed504883b72c4df70778579",
|
||||
"sha256:599a1e8ff057ac530c9ad1778293c665cb81a791421f46922d80a86473c13346",
|
||||
"sha256:5c4fae4e9cdd18c82ba3a134be256e98dc0596af1e7285a3d2602c97dcfa5159",
|
||||
"sha256:5ecfa867dea6fabe2a58f03ac9186ea64da1386af2159196da51c4904e11d652",
|
||||
"sha256:62f2578358d3a92e4ab2d830cd1c2049c9c0d0e6d3c58322993cc341bdeac22e",
|
||||
"sha256:6471a82d5abea994e38d2c2abc77164b4f7fbaaf80261cb98394d5793f11b12a",
|
||||
"sha256:6d4f18483d040e18546108eb13b1dfa1000a089bcf8529e30346116ea6240506",
|
||||
"sha256:71a608532ab3bd26223c8d841dde43f3516aa5d2bf37b50ac410bb5e99053e8f",
|
||||
"sha256:74a1d8c85fb6ff0b30fbfa8ad0ac23cd601a138f7509dc617ebc65ef305bb98d",
|
||||
"sha256:7b93a885bb13073afb0aa73ad82059a4c41f4b7d8eb8368980448b52d4c7dc2c",
|
||||
"sha256:7d4751da932caaec419d514eaa4215eaf14b612cff66398dd51129ac22680b20",
|
||||
"sha256:7f627141a26b551bdebbc4855c1157feeef18241b4b8366ed22a5c7d672ef858",
|
||||
"sha256:8169cf44dd8f9071b2b9248c35fc35e8677451c52f795daa2bb4643f32a540bc",
|
||||
"sha256:aa00d66c0fab27373ae44ae26a66a9e43ff2a678bf63a9c7c1a9a4d61172827a",
|
||||
"sha256:ccb032fda0873254380aa2bfad2582aedc2959186cce61e3a17abc1a55ff89c3",
|
||||
"sha256:d754f39e0d1603b5b24a7f8484b22d2904fa551fe865fd0d4c3332f078d20d4e",
|
||||
"sha256:d75c461e20e29afc0aee7172a0950157c704ff0dd51613506bd7d82b718e7410",
|
||||
"sha256:dcd65317dd15bc0451f3e01c80da2216a31916bdcffd6221ca1202d96584aa25",
|
||||
"sha256:e570d3ab32e2c2861c4ebe6ffcad6a8abf9347432a37608fe1fbd157b3f0036b",
|
||||
"sha256:fd43a88e045cf992ed09fa724b5315b790525f2676883a6ea64e3263bae6549d"
|
||||
],
|
||||
"version": "==1.13.2"
|
||||
},
|
||||
"chardet": {
|
||||
"hashes": [
|
||||
@@ -394,43 +416,72 @@
|
||||
},
|
||||
"coverage": {
|
||||
"hashes": [
|
||||
"sha256:108efa19b676e62590a7a13084098e35183479c0d9608131c20b0921c5a72dc0",
|
||||
"sha256:16fe3ef881eff27bab287f91dadb4ff0ce4388b9e928d84cbf148a83cc70b3a1",
|
||||
"sha256:1d0bbc11421827d1100da82ac8dc929532b97ad464038475a0f6505cbf83d6ea",
|
||||
"sha256:23a8ca5b3c9673f775cc151e85a737f1a967df2ec02b09e8c5a3b606ff2050bf",
|
||||
"sha256:24b890e51455276762b55cb06fa1c922066e8fc18d1deb1a6399b4d24dfa8ea2",
|
||||
"sha256:2f0041757ca4801f3c6a74d1660862fdb18a25aea302dd0ce9b067ddbb06b667",
|
||||
"sha256:3169aba03baddfccdab7cc04cf0878dbf76fc06d300bc35639129a6b794d6484",
|
||||
"sha256:364fb1bf0f999af2e7f4b1a1e614b2af8c3e0017d11af716aad25f911b7cd0c7",
|
||||
"sha256:5256856d23f3e45959e7e3a8f9d4cbad3d1613e5660cb8117cd1417798efc395",
|
||||
"sha256:5b26daa1e1a1147455bf62cd682e504e68f1d1e04235374d50a5248a3c792b1c",
|
||||
"sha256:60247c8f0c756732e2cfe21f03e6847b923b9a9eaff61f04dc64d3047ec1b669",
|
||||
"sha256:6463d51507308eb3973340d903537f17ece2ee1e6513aa0c27548fc3a09b0471",
|
||||
"sha256:64cbadf7a884b299794238bc4391752130e74f71e919993b50c1c431786ef2a2",
|
||||
"sha256:6de85748ea39ce819ad6d90e660da43964457a1f5cd25262e962a7c7c87945b3",
|
||||
"sha256:6f95b4794bd84f64aeca25087d8e3abc416aad76842afcac34fa6c3a6f61c62e",
|
||||
"sha256:778fa184aa3079fa3cbd240e2f5b36771c3382db26bc7bf78aea9d06212c6c66",
|
||||
"sha256:790a9c5e2dbdf6c41eec9776ed663e99bd36c1604e3bf2e8ae3b123181bfee9f",
|
||||
"sha256:7d97c1aec0b68b4ea5e3c9edb9fc3f951e8a52360f4bad3aacab9a77defe5b17",
|
||||
"sha256:93cefddcc0b541d3c52981a232947bf085a38092b0812317f1adb56f02869bcb",
|
||||
"sha256:95e49867ac616ec63ecd69ea005e65e4b896a48b8db7f9f3ad69f37be29324b7",
|
||||
"sha256:aca423563eafba66a7c15125391b267befd1e45238de5e1a119ae1fb4ea83b5c",
|
||||
"sha256:baef7c35e7fce738d9637e9c7a6aa79cb79085e4de49c2ec517ce19239a660f6",
|
||||
"sha256:c10ccf0797ffce85e93a40aff3a96a3adb63c734f95b59384a7c9522ed25c9e2",
|
||||
"sha256:ca39704a05bba1886c384a4d7944fda72c53fe5e61979cd933d22084678ad4c1",
|
||||
"sha256:f6e96d5eee578187f5b7e9266bf646b73de29e2dd7adca8bd83e383680ce1f4c",
|
||||
"sha256:fc6524511fa664cb4e91401229eedd0dad4ba6ded9c4423fee2f698d78908d9c",
|
||||
"sha256:fdf2e7e5f074495ad6ea796ca0d245aa6a8b9e4c546ffbf8d30aaaee6601af0f"
|
||||
"sha256:0cd13a6e98c37b510a2d34c8281d5e1a226aaf9b65b7d770ef03c63169965351",
|
||||
"sha256:1a4b6b6a2a3a6612e6361130c2cc3dc4378d8c221752b96167ccbad94b47f3cd",
|
||||
"sha256:2ee55e6dba516ddf6f484aa83ccabbb0adf45a18892204c23486938d12258cde",
|
||||
"sha256:3be5338a2eb4ef03c57f20917e1d12a1fd10e3853fed060b6d6b677cb3745898",
|
||||
"sha256:44b783b02db03c4777d8cf71bae19eadc171a6f2a96777d916b2c30a1eb3d070",
|
||||
"sha256:475bf7c4252af0a56e1abba9606f1e54127cdf122063095c75ab04f6f99cf45e",
|
||||
"sha256:47c81ee687eafc2f1db7f03fbe99aab81330565ebc62fb3b61edfc2216a550c8",
|
||||
"sha256:4a7f8e72b18f2aca288ff02255ce32cc830bc04d993efbc87abf6beddc9e56c0",
|
||||
"sha256:50197163a22fd17f79086e087a787883b3ec9280a509807daf158dfc2a7ded02",
|
||||
"sha256:56b13000acf891f700f5067512b804d1ec8c301d627486c678b903859d07f798",
|
||||
"sha256:79388ae29c896299b3567965dbcd93255f175c17c6c7bca38614d12718c47466",
|
||||
"sha256:79fd5d3d62238c4f583b75d48d53cdae759fe04d4fb18fe8b371d88ad2b6f8be",
|
||||
"sha256:7fe3e2fde2bf1d7ce25ebcd2d3de3650b8d60d9a73ce6dcef36e20191291613d",
|
||||
"sha256:81042a24f67b96e4287774014fa27220d8a4d91af1043389e4d73892efc89ac6",
|
||||
"sha256:81326f1095c53111f8afc95da281e1414185f4a538609a77ca50bdfa39a6c207",
|
||||
"sha256:8873dc0d8f42142ea9f20c27bbdc485190fff93823c6795be661703369e5877d",
|
||||
"sha256:88d2cbcb0a112f47eef71eb95460b6995da18e6f8ca50c264585abc2c473154b",
|
||||
"sha256:91f2491aeab9599956c45a77c5666d323efdec790bfe23fcceafcd91105d585a",
|
||||
"sha256:979daa8655ae5a51e8e7a24e7d34e250ae8309fd9719490df92cbb2fe2b0422b",
|
||||
"sha256:9c871b006c878a890c6e44a5b2f3c6291335324b298c904dc0402ee92ee1f0be",
|
||||
"sha256:a6d092545e5af53e960465f652e00efbf5357adad177b2630d63978d85e46a72",
|
||||
"sha256:b5ed7837b923d1d71c4f587ae1539ccd96bfd6be9788f507dbe94dab5febbb5d",
|
||||
"sha256:ba259f68250f16d2444cbbfaddaa0bb20e1560a4fdaad50bece25c199e6af864",
|
||||
"sha256:be1d89614c6b6c36d7578496dc8625123bda2ff44f224cf8b1c45b810ee7383f",
|
||||
"sha256:c1b030a79749aa8d1f1486885040114ee56933b15ccfc90049ba266e4aa2139f",
|
||||
"sha256:c95bb147fab76f2ecde332d972d8f4138b8f2daee6c466af4ff3b4f29bd4c19e",
|
||||
"sha256:d52c1c2d7e856cecc05aa0526453cb14574f821b7f413cc279b9514750d795c1",
|
||||
"sha256:d609a6d564ad3d327e9509846c2c47f170456344521462b469e5cb39e48ba31c",
|
||||
"sha256:e1bad043c12fb58e8c7d92b3d7f2f49977dcb80a08a6d1e7a5114a11bf819fca",
|
||||
"sha256:e5a675f6829c53c87d79117a8eb656cc4a5f8918185a32fc93ba09778e90f6db",
|
||||
"sha256:fec32646b98baf4a22fdceb08703965bd16dea09051fbeb31a04b5b6e72b846c"
|
||||
],
|
||||
"version": "==5.0a6"
|
||||
"version": "==5.0"
|
||||
},
|
||||
"cryptography": {
|
||||
"hashes": [
|
||||
"sha256:02079a6addc7b5140ba0825f542c0869ff4df9a69c360e339ecead5baefa843c",
|
||||
"sha256:1df22371fbf2004c6f64e927668734070a8953362cd8370ddd336774d6743595",
|
||||
"sha256:369d2346db5934345787451504853ad9d342d7f721ae82d098083e1f49a582ad",
|
||||
"sha256:3cda1f0ed8747339bbdf71b9f38ca74c7b592f24f65cdb3ab3765e4b02871651",
|
||||
"sha256:44ff04138935882fef7c686878e1c8fd80a723161ad6a98da31e14b7553170c2",
|
||||
"sha256:4b1030728872c59687badcca1e225a9103440e467c17d6d1730ab3d2d64bfeff",
|
||||
"sha256:58363dbd966afb4f89b3b11dfb8ff200058fbc3b947507675c19ceb46104b48d",
|
||||
"sha256:6ec280fb24d27e3d97aa731e16207d58bd8ae94ef6eab97249a2afe4ba643d42",
|
||||
"sha256:7270a6c29199adc1297776937a05b59720e8a782531f1f122f2eb8467f9aab4d",
|
||||
"sha256:73fd30c57fa2d0a1d7a49c561c40c2f79c7d6c374cc7750e9ac7c99176f6428e",
|
||||
"sha256:7f09806ed4fbea8f51585231ba742b58cbcfbfe823ea197d8c89a5e433c7e912",
|
||||
"sha256:90df0cc93e1f8d2fba8365fb59a858f51a11a394d64dbf3ef844f783844cc793",
|
||||
"sha256:971221ed40f058f5662a604bd1ae6e4521d84e6cad0b7b170564cc34169c8f13",
|
||||
"sha256:a518c153a2b5ed6b8cc03f7ae79d5ffad7315ad4569b2d5333a13c38d64bd8d7",
|
||||
"sha256:b0de590a8b0979649ebeef8bb9f54394d3a41f66c5584fff4220901739b6b2f0",
|
||||
"sha256:b43f53f29816ba1db8525f006fa6f49292e9b029554b3eb56a189a70f2a40879",
|
||||
"sha256:d31402aad60ed889c7e57934a03477b572a03af7794fa8fb1780f21ea8f6551f",
|
||||
"sha256:de96157ec73458a7f14e3d26f17f8128c959084931e8997b9e655a39c8fde9f9",
|
||||
"sha256:df6b4dca2e11865e6cfbfb708e800efb18370f5a46fd601d3755bc7f85b3a8a2",
|
||||
"sha256:ecadccc7ba52193963c0475ac9f6fa28ac01e01349a2ca48509667ef41ffd2cf",
|
||||
"sha256:fb81c17e0ebe3358486cd8cc3ad78adbae58af12fc2bf2bc0bb84e8090fa5ce8"
|
||||
],
|
||||
"version": "==2.8"
|
||||
},
|
||||
"docutils": {
|
||||
"hashes": [
|
||||
"sha256:6c4f696463b79f1fb8ba0c594b63840ebd41f059e92b31957c46b74a4599b6d0",
|
||||
"sha256:9e4d7ecfc600058e07ba661411a2b7de2fd0fafa17d1a7f7361cd47b1175c827",
|
||||
"sha256:a2aeea129088da402665e92e0b25b04b073c04b2dce4ab65caaa38b7ce2e1a99"
|
||||
"sha256:7a6228589435302e421f5c473ce0180878b90f70227f7174cacde5efbd34275f",
|
||||
"sha256:f1bad547016f945f7b35b28d8bead307821822ca3f8d4f87a1bd2ad1a8faab51"
|
||||
],
|
||||
"version": "==0.15.2"
|
||||
"version": "==0.16b0.dev0"
|
||||
},
|
||||
"entrypoints": {
|
||||
"hashes": [
|
||||
@@ -441,11 +492,11 @@
|
||||
},
|
||||
"flake8": {
|
||||
"hashes": [
|
||||
"sha256:19241c1cbc971b9962473e4438a2ca19749a7dd002dd1a946eaba171b4114548",
|
||||
"sha256:8e9dfa3cecb2400b3738a42c54c3043e821682b9c840b0448c0503f781130696"
|
||||
"sha256:45681a117ecc81e870cbf1262835ae4af5e7a8b08e40b944a8a6e6b895914cfb",
|
||||
"sha256:49356e766643ad15072a789a20915d3c91dc89fd313ccd71802303fd67e4deca"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==3.7.8"
|
||||
"version": "==3.7.9"
|
||||
},
|
||||
"flask": {
|
||||
"hashes": [
|
||||
@@ -471,10 +522,11 @@
|
||||
},
|
||||
"importlib-metadata": {
|
||||
"hashes": [
|
||||
"sha256:23d3d873e008a513952355379d93cbcab874c58f4f034ff657c7a87422fa64e8",
|
||||
"sha256:80d2de76188eabfbfcf27e6a37342c2827801e59c4cc14b0371c56fed43820e3"
|
||||
"sha256:073a852570f92da5f744a3472af1b61e28e9f78ccf0c9117658dc32b15de7b45",
|
||||
"sha256:d95141fbfa7ef2ec65cfd945e2af7e5a6ddbd7c8d9a25e66ff3be8e3daf9f60f"
|
||||
],
|
||||
"version": "==0.19"
|
||||
"markers": "python_version < '3.8'",
|
||||
"version": "==1.3.0"
|
||||
},
|
||||
"itsdangerous": {
|
||||
"hashes": [
|
||||
@@ -483,12 +535,26 @@
|
||||
],
|
||||
"version": "==1.1.0"
|
||||
},
|
||||
"jeepney": {
|
||||
"hashes": [
|
||||
"sha256:13806f91a96e9b2623fd2a81b950d763ee471454aafd9eb6d75dbe7afce428fb",
|
||||
"sha256:f6a3f93464a0cf052f4e87da3c8b3ed1e27696758fb9739c63d3a74d9a1b6774"
|
||||
],
|
||||
"version": "==0.4.1"
|
||||
},
|
||||
"jinja2": {
|
||||
"hashes": [
|
||||
"sha256:065c4f02ebe7f7cf559e49ee5a95fb800a9e4528727aec6f24402a5374c65013",
|
||||
"sha256:14dd6caf1527abb21f08f86c784eac40853ba93edb79552aa1e4b8aef1b61c7b"
|
||||
"sha256:74320bb91f31270f9551d46522e33af46a80c3d619f4a4bf42b3164d30b5911f",
|
||||
"sha256:9fe95f19286cfefaa917656583d020be14e7859c6b0252588391e47db34527de"
|
||||
],
|
||||
"version": "==2.10.1"
|
||||
"version": "==2.10.3"
|
||||
},
|
||||
"keyring": {
|
||||
"hashes": [
|
||||
"sha256:a3f71fc0cf6b74e201e70532879ba1d15db25cb2c7407dce52fe52a6d5fc7b66",
|
||||
"sha256:fc9cadedae35b77141f670f84c10a657147d2e526348698c93dd77f039979729"
|
||||
],
|
||||
"version": "==20.0.0"
|
||||
},
|
||||
"markupsafe": {
|
||||
"hashes": [
|
||||
@@ -525,10 +591,10 @@
|
||||
},
|
||||
"marshmallow": {
|
||||
"hashes": [
|
||||
"sha256:7ea8540fc7e35be3b0af8b017313944b984d5acdb118b4ba3c270ac9611765c7",
|
||||
"sha256:bc91e3f90e86133241ac62ea0dd35217d631a207e8628430bc66c347dbe12f7d"
|
||||
"sha256:0ba81b6da4ae69eb229b74b3c741ff13fe04fb899824377b1aff5aaa1a9fd46e",
|
||||
"sha256:3e53dd9e9358977a3929e45cdbe4a671f9eff53a7d6a23f33ed3eab8c1890d8f"
|
||||
],
|
||||
"version": "==3.0.0rc9"
|
||||
"version": "==3.3.0"
|
||||
},
|
||||
"mccabe": {
|
||||
"hashes": [
|
||||
@@ -539,17 +605,23 @@
|
||||
},
|
||||
"more-itertools": {
|
||||
"hashes": [
|
||||
"sha256:409cd48d4db7052af495b09dec721011634af3753ae1ef92d2b32f73a745f832",
|
||||
"sha256:92b8c4b06dac4f0611c0729b2f2ede52b2e1bac1ab48f089c7ddc12e26bb60c4"
|
||||
"sha256:b84b238cce0d9adad5ed87e745778d20a3f8487d0f0cb8b8a586816c7496458d",
|
||||
"sha256:c833ef592a0324bcc6a60e48440da07645063c453880c9477ceb22490aec1564"
|
||||
],
|
||||
"version": "==7.2.0"
|
||||
"version": "==8.0.2"
|
||||
},
|
||||
"packaging": {
|
||||
"hashes": [
|
||||
"sha256:a7ac867b97fdc07ee80a8058fe4435ccd274ecc3b0ed61d852d7d53055528cf9",
|
||||
"sha256:c491ca87294da7cc01902edbe30a5bc6c4c28172b5138ab4e4aa1b9d7bfaeafe"
|
||||
"sha256:28b924174df7a2fa32c1953825ff29c61e2f5e082343165438812f00d3a7fc47",
|
||||
"sha256:d9551545c6d761f3def1677baf08ab2a3ca17c56879e70fecba2fc4dde4ed108"
|
||||
],
|
||||
"version": "==19.1"
|
||||
"version": "==19.2"
|
||||
},
|
||||
"pathspec": {
|
||||
"hashes": [
|
||||
"sha256:e285ccc8b0785beadd4c18e5708b12bb8fcf529a1e61215b3feff1d1e559ea5c"
|
||||
],
|
||||
"version": "==0.6.0"
|
||||
},
|
||||
"pkginfo": {
|
||||
"hashes": [
|
||||
@@ -560,10 +632,10 @@
|
||||
},
|
||||
"pluggy": {
|
||||
"hashes": [
|
||||
"sha256:0825a152ac059776623854c1543d65a4ad408eb3d33ee114dff91e57ec6ae6fc",
|
||||
"sha256:b9817417e95936bf75d85d3f8767f7df6cdde751fc40aed3bb3074cbcb77757c"
|
||||
"sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0",
|
||||
"sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"
|
||||
],
|
||||
"version": "==0.12.0"
|
||||
"version": "==0.13.1"
|
||||
},
|
||||
"py": {
|
||||
"hashes": [
|
||||
@@ -579,6 +651,12 @@
|
||||
],
|
||||
"version": "==2.5.0"
|
||||
},
|
||||
"pycparser": {
|
||||
"hashes": [
|
||||
"sha256:a988718abfad80b6b157acce7bf130a30876d27603738ac39f140993246b25b3"
|
||||
],
|
||||
"version": "==2.19"
|
||||
},
|
||||
"pyflakes": {
|
||||
"hashes": [
|
||||
"sha256:17dbeb2e3f4d772725c777fabc446d5634d1038f234e77343108ce445ea69ce0",
|
||||
@@ -588,40 +666,40 @@
|
||||
},
|
||||
"pygments": {
|
||||
"hashes": [
|
||||
"sha256:71e430bc85c88a430f000ac1d9b331d2407f681d6f6aec95e8bcfbc3df5b0127",
|
||||
"sha256:881c4c157e45f30af185c1ffe8d549d48ac9127433f2c380c24b84572ad66297"
|
||||
"sha256:2a3fe295e54a20164a9df49c75fa58526d3be48e14aceba6d6b1e8ac0bfd6f1b",
|
||||
"sha256:98c8aa5a9f778fcd1026a17361ddaf7330d1b7c62ae97c3bb0ae73e0b9b6b0fe"
|
||||
],
|
||||
"version": "==2.4.2"
|
||||
"version": "==2.5.2"
|
||||
},
|
||||
"pyparsing": {
|
||||
"hashes": [
|
||||
"sha256:6f98a7b9397e206d78cc01df10131398f1c8b8510a2f4d97d9abd82e1aacdd80",
|
||||
"sha256:d9338df12903bbf5d65a0e4e87c2161968b10d2e489652bb47001d82a9b028b4"
|
||||
"sha256:20f995ecd72f2a1f4bf6b072b63b22e2eb457836601e76d6e5dfcd75436acc1f",
|
||||
"sha256:4ca62001be367f01bd3e92ecbb79070272a9d4964dce6a48a82ff0b8bc7e683a"
|
||||
],
|
||||
"version": "==2.4.2"
|
||||
"version": "==2.4.5"
|
||||
},
|
||||
"pytest": {
|
||||
"hashes": [
|
||||
"sha256:6ef6d06de77ce2961156013e9dff62f1b2688aa04d0dc244299fe7d67e09370d",
|
||||
"sha256:a736fed91c12681a7b34617c8fcefe39ea04599ca72c608751c31d89579a3f77"
|
||||
"sha256:6b571215b5a790f9b41f19f3531c53a45cf6bb8ef2988bc1ff9afb38270b25fa",
|
||||
"sha256:e41d489ff43948babd0fad7ad5e49b8735d5d55e26628a58673c39ff61d95de4"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==5.0.1"
|
||||
"version": "==5.3.2"
|
||||
},
|
||||
"pytest-cov": {
|
||||
"hashes": [
|
||||
"sha256:2b097cde81a302e1047331b48cadacf23577e431b61e9c6f49a1170bbe3d3da6",
|
||||
"sha256:e00ea4fdde970725482f1f35630d12f074e121a23801aabf2ae154ec6bdd343a"
|
||||
"sha256:cc6742d8bac45070217169f5f72ceee1e0e55b0221f54bcf24845972d3a47f2b",
|
||||
"sha256:cdbdef4f870408ebdbfeb44e63e07eb18bb4619fae852f6e760645fa36172626"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==2.7.1"
|
||||
"version": "==2.8.1"
|
||||
},
|
||||
"pytz": {
|
||||
"hashes": [
|
||||
"sha256:26c0b32e437e54a18161324a2fca3c4b9846b74a8dccddd843113109e1116b32",
|
||||
"sha256:c894d57500a4cd2d5c71114aaab77dbab5eabd9022308ce5ac9bb93a60a6f0c7"
|
||||
"sha256:1c557d7d0e871de1f5ccd5833f60fb2550652da6be2693c1e02300743d21500d",
|
||||
"sha256:b02c06db6cf09c12dd25137e563b31700d3b80fcc4ad23abb7a315f2789819be"
|
||||
],
|
||||
"version": "==2019.2"
|
||||
"version": "==2019.3"
|
||||
},
|
||||
"readme-renderer": {
|
||||
"hashes": [
|
||||
@@ -630,6 +708,22 @@
|
||||
],
|
||||
"version": "==24.0"
|
||||
},
|
||||
"regex": {
|
||||
"hashes": [
|
||||
"sha256:3dbd8333fd2ebd50977ac8747385a73aa1f546eb6b16fcd83d274470fe11f243",
|
||||
"sha256:40b7d1291a56897927e08bb973f8c186c2feb14c7f708bfe7aaee09483e85a20",
|
||||
"sha256:719978a9145d59fc78509ea1d1bb74243f93583ef2a34dcc5623cf8118ae9726",
|
||||
"sha256:75cf3796f89f75f83207a5c6a6e14eaf57e0369ef0ffff8e22bf36bbcfa0f1de",
|
||||
"sha256:77396cf80be8b2a35db863cca4c1a902d88ceeb183adab328b81184e71a5eafe",
|
||||
"sha256:77a3799152951d6d14ae5720ca162c97c64f85d4755da585418eac216b736cad",
|
||||
"sha256:91235c98283d2bddf1a588f0fbc2da8afa37959294bbd18b76297bdf316ba4d6",
|
||||
"sha256:aaffd68c4c1ed891366d5c390081f4bf6337595e76a157baf453603d8e53fbcb",
|
||||
"sha256:ad9e3c7260809c0d1ded100269f78ea0217c0704f1eaaf40a382008461848b45",
|
||||
"sha256:c203c9ee755e9656d0af8fab82754d5a664ebaf707b3f883c7eff6a3dd5151cf",
|
||||
"sha256:e865bc508e316a3a09d36c8621596e6599a203bc54f1cd41020a127ccdac468a"
|
||||
],
|
||||
"version": "==2019.12.9"
|
||||
},
|
||||
"requests": {
|
||||
"hashes": [
|
||||
"sha256:11e007a8a2aa0323f5a921e9e6a2d7e4e67d9877e85773fba9ba6419025cbeb4",
|
||||
@@ -644,26 +738,35 @@
|
||||
],
|
||||
"version": "==0.9.1"
|
||||
},
|
||||
"secretstorage": {
|
||||
"hashes": [
|
||||
"sha256:20c797ae48a4419f66f8d28fc221623f11fc45b6828f96bdb1ad9990acb59f92",
|
||||
"sha256:7a119fb52a88e398dbb22a4b3eb39b779bfbace7e4153b7bc6e5954d86282a8a"
|
||||
],
|
||||
"markers": "sys_platform == 'linux'",
|
||||
"version": "==3.1.1"
|
||||
},
|
||||
"six": {
|
||||
"hashes": [
|
||||
"sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c",
|
||||
"sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73"
|
||||
"sha256:1f1b7d42e254082a9db6279deae68afb421ceba6158efa6131de7b3003ee93fd",
|
||||
"sha256:30f610279e8b2578cab6db20741130331735c781b56053c59c4076da27f06b66"
|
||||
],
|
||||
"version": "==1.12.0"
|
||||
"version": "==1.13.0"
|
||||
},
|
||||
"snowballstemmer": {
|
||||
"hashes": [
|
||||
"sha256:9f3b9ffe0809d174f7047e121431acf99c89a7040f0ca84f94ba53a498e6d0c9"
|
||||
"sha256:209f257d7533fdb3cb73bdbd24f436239ca3b2fa67d56f6ff88e86be08cc5ef0",
|
||||
"sha256:df3bac3df4c2c01363f3dd2cfa78cce2840a79b9f1c2d2de9ce8d31683992f52"
|
||||
],
|
||||
"version": "==1.9.0"
|
||||
"version": "==2.0.0"
|
||||
},
|
||||
"sphinx": {
|
||||
"hashes": [
|
||||
"sha256:22538e1bbe62b407cf5a8aabe1bb15848aa66bb79559f42f5202bbce6b757a69",
|
||||
"sha256:f9a79e746b87921cabc3baa375199c6076d1270cee53915dbd24fdbeaaacc427"
|
||||
"sha256:0a11e2fd31fe5c7e64b4fc53c2c022946512f021d603eb41ac6ae51d5fcbb574",
|
||||
"sha256:138e39aa10f28d52aa5759fc6d1cba2be6a4b750010974047fa7d0e31addcf63"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==2.1.2"
|
||||
"version": "==2.3.0"
|
||||
},
|
||||
"sphinxcontrib-applehelp": {
|
||||
"hashes": [
|
||||
@@ -716,25 +819,50 @@
|
||||
},
|
||||
"tqdm": {
|
||||
"hashes": [
|
||||
"sha256:14a285392c32b6f8222ecfbcd217838f88e11630affe9006cd0e94c7eff3cb61",
|
||||
"sha256:25d4c0ea02a305a688e7e9c2cdc8f862f989ef2a4701ab28ee963295f5b109ab"
|
||||
"sha256:7543892c59720e36e4212180274d8f58dde36803bc1f6370fd09afa20b8f5892",
|
||||
"sha256:f0ab01cf3ae5673d18f918700c0165e5fad0f26b5ebe4b34f62ead92686b5340"
|
||||
],
|
||||
"version": "==4.32.2"
|
||||
"version": "==4.40.2"
|
||||
},
|
||||
"twine": {
|
||||
"hashes": [
|
||||
"sha256:0fb0bfa3df4f62076cab5def36b1a71a2e4acb4d1fa5c97475b048117b1a6446",
|
||||
"sha256:d6c29c933ecfc74e9b1d9fa13aa1f87c5d5770e119f5a4ce032092f0ff5b14dc"
|
||||
"sha256:c1af8ca391e43b0a06bbc155f7f67db0bf0d19d284bfc88d1675da497a946124",
|
||||
"sha256:d561a5e511f70275e5a485a6275ff61851c16ffcb3a95a602189161112d9f160"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.13.0"
|
||||
"version": "==3.1.1"
|
||||
},
|
||||
"typed-ast": {
|
||||
"hashes": [
|
||||
"sha256:1170afa46a3799e18b4c977777ce137bb53c7485379d9706af8a59f2ea1aa161",
|
||||
"sha256:18511a0b3e7922276346bcb47e2ef9f38fb90fd31cb9223eed42c85d1312344e",
|
||||
"sha256:262c247a82d005e43b5b7f69aff746370538e176131c32dda9cb0f324d27141e",
|
||||
"sha256:2b907eb046d049bcd9892e3076c7a6456c93a25bebfe554e931620c90e6a25b0",
|
||||
"sha256:354c16e5babd09f5cb0ee000d54cfa38401d8b8891eefa878ac772f827181a3c",
|
||||
"sha256:48e5b1e71f25cfdef98b013263a88d7145879fbb2d5185f2a0c79fa7ebbeae47",
|
||||
"sha256:4e0b70c6fc4d010f8107726af5fd37921b666f5b31d9331f0bd24ad9a088e631",
|
||||
"sha256:630968c5cdee51a11c05a30453f8cd65e0cc1d2ad0d9192819df9978984529f4",
|
||||
"sha256:66480f95b8167c9c5c5c87f32cf437d585937970f3fc24386f313a4c97b44e34",
|
||||
"sha256:71211d26ffd12d63a83e079ff258ac9d56a1376a25bc80b1cdcdf601b855b90b",
|
||||
"sha256:7954560051331d003b4e2b3eb822d9dd2e376fa4f6d98fee32f452f52dd6ebb2",
|
||||
"sha256:838997f4310012cf2e1ad3803bce2f3402e9ffb71ded61b5ee22617b3a7f6b6e",
|
||||
"sha256:95bd11af7eafc16e829af2d3df510cecfd4387f6453355188342c3e79a2ec87a",
|
||||
"sha256:bc6c7d3fa1325a0c6613512a093bc2a2a15aeec350451cbdf9e1d4bffe3e3233",
|
||||
"sha256:cc34a6f5b426748a507dd5d1de4c1978f2eb5626d51326e43280941206c209e1",
|
||||
"sha256:d755f03c1e4a51e9b24d899561fec4ccaf51f210d52abdf8c07ee2849b212a36",
|
||||
"sha256:d7c45933b1bdfaf9f36c579671fec15d25b06c8398f113dab64c18ed1adda01d",
|
||||
"sha256:d896919306dd0aa22d0132f62a1b78d11aaf4c9fc5b3410d3c666b818191630a",
|
||||
"sha256:fdc1c9bbf79510b76408840e009ed65958feba92a88833cdceecff93ae8fff66",
|
||||
"sha256:ffde2fbfad571af120fcbfbbc61c72469e72f550d676c3342492a9dfdefb8f12"
|
||||
],
|
||||
"version": "==1.4.0"
|
||||
},
|
||||
"urllib3": {
|
||||
"hashes": [
|
||||
"sha256:b246607a25ac80bedac05c6f282e3cdaf3afb65420fd024ac94435cabe6e18d1",
|
||||
"sha256:dbe59173209418ae49d485b87d1681aefa36252ee85884c31346debd19463232"
|
||||
"sha256:a8a318824cc77d1fd4b2bec2ded92646630d7fe8619497b142c84a9e6f5a7293",
|
||||
"sha256:f3c5fd51747d450d4dcf6f923c81f78f811aab8205fda64b0aba34a4e48b0745"
|
||||
],
|
||||
"version": "==1.25.3"
|
||||
"version": "==1.25.7"
|
||||
},
|
||||
"wcwidth": {
|
||||
"hashes": [
|
||||
@@ -752,17 +880,17 @@
|
||||
},
|
||||
"werkzeug": {
|
||||
"hashes": [
|
||||
"sha256:87ae4e5b5366da2347eb3116c0e6c681a0e939a33b2805e2c0cbd282664932c4",
|
||||
"sha256:a13b74dd3c45f758d4ebdb224be8f1ab8ef58b3c0ffc1783a8c7d9f4f50227e6"
|
||||
"sha256:7280924747b5733b246fe23972186c6b348f9ae29724135a6dfc1e53cea433e7",
|
||||
"sha256:e5f4a1f98b52b18a93da705a7458e55afb26f32bff83ff5d19189f92462d65c4"
|
||||
],
|
||||
"version": "==0.15.5"
|
||||
"version": "==0.16.0"
|
||||
},
|
||||
"zipp": {
|
||||
"hashes": [
|
||||
"sha256:4970c3758f4e89a7857a973b1e2a5d75bcdc47794442f2e2dd4fe8e0466e809a",
|
||||
"sha256:8a5712cfd3bb4248015eb3b0b3c54a5f6ee3f2425963ef2a0125b8bc40aafaec"
|
||||
"sha256:3718b1cbcd963c7d4c5511a8240812904164b7f381b647143a89d3b98f9bcd8e",
|
||||
"sha256:f06903e9f1f43b12d371004b4ac7b06ab39a44adc747266928ae6debfa7b3335"
|
||||
],
|
||||
"version": "==0.5.2"
|
||||
"version": "==0.6.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,10 +7,10 @@
|
||||
[](https://pypi.org/project/responder/)
|
||||
[](https://github.com/taoufik07/responder/graphs/contributors)
|
||||
|
||||
[](https://python-responder.org/)
|
||||
[](https://responder.readthedocs.io)
|
||||
|
||||
|
||||
Powered by [Starlette](https://www.starlette.io/). That `async` declaration is optional. [View documentation](https://python-responder.org).
|
||||
Powered by [Starlette](https://www.starlette.io/). That `async` declaration is optional. [View documentation](https://responder.readthedocs.io).
|
||||
|
||||
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.
|
||||
|
||||
@@ -26,7 +26,7 @@ This gets you a ASGI app, with a production static files server pre-installed, j
|
||||
|
||||
## More Examples
|
||||
|
||||
See [the documentation's feature tour](https://python-responder.org/en/latest/tour.html) for more details on features available in Responder.
|
||||
See [the documentation's feature tour](https://responder.readthedocs.io/en/latest/tour.html) for more details on features available in Responder.
|
||||
|
||||
|
||||
# Installing Responder
|
||||
|
||||
@@ -45,4 +45,4 @@ tqdm==4.26.0
|
||||
twine==1.12.1
|
||||
urllib3==1.23
|
||||
webencodings==0.5.1
|
||||
werkzeug==0.14.1
|
||||
werkzeug==0.15.5
|
||||
|
||||
@@ -73,13 +73,30 @@ If the client requests YAML instead (with a header of ``Accept: application/x-ya
|
||||
Rendering a Template
|
||||
--------------------
|
||||
|
||||
If you want to render a template, simply use ``api.template``. No need for additional imports::
|
||||
Responder provides a built-in light `jinja2 <http://jinja.pocoo.org/docs/>`_ wrapper ``templates.Templates``
|
||||
|
||||
Usage::
|
||||
|
||||
from responder.templates import Templates
|
||||
|
||||
templates = Templates()
|
||||
|
||||
@api.route("/hello/{name}/html")
|
||||
def hello(req, resp, name):
|
||||
resp.html = templates.render("hello.html", name=name)
|
||||
|
||||
|
||||
Also a ``render_async`` is available::
|
||||
|
||||
templates = Templates(enable_async=True)
|
||||
resp.html = await templates.render_async("hello.html", who=who)
|
||||
|
||||
You can also use the existing ``api.template(filename, *args, **kwargs)`` to render templates::
|
||||
|
||||
@api.route("/hello/{who}/html")
|
||||
def hello_html(req, resp, *, who):
|
||||
resp.html = api.template('hello.html', who=who)
|
||||
|
||||
The ``api`` instance is available as an object during template rendering.
|
||||
|
||||
Setting Response Status Code
|
||||
----------------------------
|
||||
|
||||
+83
-11
@@ -58,13 +58,14 @@ You can make use of Responder's Request and Response objects in your GraphQL res
|
||||
OpenAPI Schema Support
|
||||
----------------------
|
||||
|
||||
Responder comes with built-in support for OpenAPI / marshmallow::
|
||||
Responder comes with built-in support for OpenAPI / marshmallow
|
||||
|
||||
New in Responder `1.4.0`::
|
||||
|
||||
import responder
|
||||
from responder.ext.schema import Schema as OpenAPISchema
|
||||
from marshmallow import Schema, fields
|
||||
|
||||
description = "This is a sample server for a pet store."
|
||||
terms_of_service = "http://example.com/terms/"
|
||||
contact = {
|
||||
"name": "API Support",
|
||||
"url": "http://www.example.com/support",
|
||||
@@ -74,19 +75,21 @@ Responder comes with built-in support for OpenAPI / marshmallow::
|
||||
"name": "Apache 2.0",
|
||||
"url": "https://www.apache.org/licenses/LICENSE-2.0.html",
|
||||
}
|
||||
|
||||
api = responder.API()
|
||||
|
||||
api = responder.API(
|
||||
schema = OpenAPISchema(
|
||||
app=api,
|
||||
title="Web Service",
|
||||
version="1.0",
|
||||
openapi="3.0.2",
|
||||
description=description,
|
||||
terms_of_service=terms_of_service,
|
||||
description="A simple pet store",
|
||||
terms_of_service="http://example.com/terms/",
|
||||
contact=contact,
|
||||
license=license,
|
||||
)
|
||||
|
||||
|
||||
@api.schema("Pet")
|
||||
@schema.schema("Pet")
|
||||
class PetSchema(Schema):
|
||||
name = fields.Str()
|
||||
|
||||
@@ -108,6 +111,51 @@ Responder comes with built-in support for OpenAPI / marshmallow::
|
||||
resp.media = PetSchema().dump({"name": "little orange"})
|
||||
|
||||
|
||||
Old way *It's recommended to use the code above* ::
|
||||
|
||||
import responder
|
||||
from marshmallow import Schema, fields
|
||||
|
||||
contact = {
|
||||
"name": "API Support",
|
||||
"url": "http://www.example.com/support",
|
||||
"email": "support@example.com",
|
||||
}
|
||||
license = {
|
||||
"name": "Apache 2.0",
|
||||
"url": "https://www.apache.org/licenses/LICENSE-2.0.html",
|
||||
}
|
||||
|
||||
api = responder.API(
|
||||
title="Web Service",
|
||||
version="1.0",
|
||||
openapi="3.0.2",
|
||||
description="A simple pet store",
|
||||
terms_of_service="http://example.com/terms/",
|
||||
contact=contact,
|
||||
license=license,
|
||||
)
|
||||
|
||||
@api.schema("Pet")
|
||||
class PetSchema(Schema):
|
||||
name = fields.Str()
|
||||
|
||||
@api.route("/")
|
||||
def route(req, resp):
|
||||
"""A cute furry animal endpoint.
|
||||
---
|
||||
get:
|
||||
description: Get a random pet
|
||||
responses:
|
||||
200:
|
||||
description: A pet to be returned
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Pet'
|
||||
"""
|
||||
resp.media = PetSchema().dump({"name": "little orange"})
|
||||
|
||||
::
|
||||
|
||||
>>> r = api.session().get("http://;/schema.yml")
|
||||
@@ -142,7 +190,31 @@ Responder comes with built-in support for OpenAPI / marshmallow::
|
||||
Interactive Documentation
|
||||
-------------------------
|
||||
|
||||
Responder can automatically supply API Documentation for you. Using the example above::
|
||||
Responder can automatically supply API Documentation for you. Using the example above
|
||||
|
||||
The new and recommended way::
|
||||
|
||||
...
|
||||
from responder.ext.schema import Schema
|
||||
...
|
||||
api = responder.API()
|
||||
|
||||
schema = Schema(
|
||||
app=api,
|
||||
title="Web Service",
|
||||
version="1.0",
|
||||
openapi="3.0.2",
|
||||
...
|
||||
docs_route='/docs',
|
||||
...
|
||||
description=description,
|
||||
terms_of_service=terms_of_service,
|
||||
contact=contact,
|
||||
license=license,
|
||||
)
|
||||
|
||||
|
||||
The old way ::
|
||||
|
||||
api = responder.API(
|
||||
title="Web Service",
|
||||
@@ -157,8 +229,8 @@ Responder can automatically supply API Documentation for you. Using the example
|
||||
|
||||
This will make ``/docs`` render interactive documentation for your API.
|
||||
|
||||
Mount a WSGI App (e.g. Flask)
|
||||
-----------------------------
|
||||
Mount a WSGI / ASGI Apps (e.g. Flask, Starlette,...)
|
||||
----------------------------------------------------
|
||||
|
||||
Responder gives you the ability to mount another ASGI / WSGI app at a subroute::
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
__version__ = "1.3.2"
|
||||
__version__ = "2.0.5"
|
||||
|
||||
+78
-422
@@ -1,43 +1,33 @@
|
||||
import json
|
||||
import os
|
||||
|
||||
from uuid import uuid4
|
||||
from pathlib import Path
|
||||
from base64 import b64encode
|
||||
|
||||
import apistar
|
||||
import itsdangerous
|
||||
import jinja2
|
||||
import uvicorn
|
||||
import yaml
|
||||
from apispec import APISpec, yaml_utils
|
||||
from apispec.ext.marshmallow import MarshmallowPlugin
|
||||
from starlette.exceptions import ExceptionMiddleware
|
||||
from starlette.middleware.wsgi import WSGIMiddleware
|
||||
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.middleware.trustedhost import TrustedHostMiddleware
|
||||
from starlette.middleware.sessions import SessionMiddleware
|
||||
from starlette.routing import Lifespan
|
||||
from starlette.staticfiles import StaticFiles
|
||||
from starlette.testclient import TestClient
|
||||
from starlette.websockets import WebSocket
|
||||
from whitenoise import WhiteNoise
|
||||
|
||||
from . import models, status_codes
|
||||
from .background import BackgroundQueue
|
||||
from .formats import get_formats
|
||||
from .routes import Route
|
||||
from .statics import (
|
||||
DEFAULT_API_THEME,
|
||||
DEFAULT_CORS_PARAMS,
|
||||
DEFAULT_SECRET_KEY,
|
||||
DEFAULT_SESSION_COOKIE,
|
||||
)
|
||||
from .templates import GRAPHIQL
|
||||
from .routes import Router
|
||||
from .statics import DEFAULT_API_THEME, DEFAULT_CORS_PARAMS, DEFAULT_SECRET_KEY
|
||||
from .ext.schema import Schema as OpenAPISchema
|
||||
from .staticfiles import StaticFiles
|
||||
from .templates import Templates
|
||||
|
||||
|
||||
# TODO: consider moving status codes here
|
||||
class API:
|
||||
"""The primary web-service class.
|
||||
|
||||
@@ -45,12 +35,6 @@ class API:
|
||||
: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.
|
||||
:param title: The title of the application (OpenAPI Info Object)
|
||||
:param version: The version of the OpenAPI document (OpenAPI Info Object)
|
||||
:param description: The description of the OpenAPI document (OpenAPI Info Object)
|
||||
:param terms_of_service: A URL to the Terms of Service for the API (OpenAPI Info Object)
|
||||
:param contact: The contact dictionary of the application (OpenAPI Contact Object)
|
||||
:param license: The license information of the exposed API (OpenAPI License Object)
|
||||
"""
|
||||
|
||||
status_codes = status_codes
|
||||
@@ -81,13 +65,8 @@ class API:
|
||||
self.background = BackgroundQueue()
|
||||
|
||||
self.secret_key = secret_key
|
||||
self.title = title
|
||||
self.version = version
|
||||
self.description = description
|
||||
self.terms_of_service = terms_of_service
|
||||
self.contact = contact
|
||||
self.license = license
|
||||
self.openapi_version = openapi
|
||||
|
||||
self.router = Router()
|
||||
|
||||
if static_dir is not None:
|
||||
if static_route is None:
|
||||
@@ -97,23 +76,6 @@ class API:
|
||||
self.static_dir = static_dir
|
||||
self.static_route = static_route
|
||||
|
||||
self.built_in_templates_dir = Path(
|
||||
os.path.abspath(os.path.dirname(__file__) + "/templates")
|
||||
)
|
||||
|
||||
if templates_dir is not None:
|
||||
templates_dir = Path(os.path.abspath(templates_dir))
|
||||
|
||||
self.templates_dir = templates_dir or self.built_in_templates_dir
|
||||
|
||||
self.apps = {}
|
||||
self.routes = {}
|
||||
self.before_requests = {"http": [], "ws": []}
|
||||
self.docs_theme = DEFAULT_API_THEME
|
||||
self.docs_route = docs_route
|
||||
self.schemas = {}
|
||||
self.session_cookie = DEFAULT_SESSION_COOKIE
|
||||
|
||||
self.hsts_enabled = enable_hsts
|
||||
self.cors = cors
|
||||
self.cors_params = cors_params
|
||||
@@ -127,39 +89,19 @@ class API:
|
||||
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):
|
||||
if _dir is not None:
|
||||
os.makedirs(_dir, exist_ok=True)
|
||||
if self.static_dir is not None:
|
||||
os.makedirs(self.static_dir, exist_ok=True)
|
||||
|
||||
if self.static_dir is not None:
|
||||
self.whitenoise = WhiteNoise(application=self._notfound_wsgi_app)
|
||||
self.whitenoise.add_files(str(self.static_dir))
|
||||
|
||||
self.whitenoise.add_files(
|
||||
(
|
||||
Path(apistar.__file__).parent
|
||||
/ "themes"
|
||||
/ self.docs_theme
|
||||
/ "static"
|
||||
).resolve()
|
||||
)
|
||||
|
||||
self.mount(self.static_route, self.whitenoise)
|
||||
self.mount(self.static_route, self.static_app)
|
||||
|
||||
self.formats = get_formats()
|
||||
|
||||
# Cached requests session.
|
||||
self._session = None
|
||||
|
||||
if self.openapi_version:
|
||||
self.add_route(openapi_route, self.schema_response)
|
||||
|
||||
if self.docs_route:
|
||||
self.add_route(self.docs_route, self.docs_response)
|
||||
|
||||
self.default_endpoint = None
|
||||
self.app = self.asgi
|
||||
self.app = ExceptionMiddleware(self.router, debug=debug)
|
||||
self.add_middleware(GZipMiddleware)
|
||||
|
||||
if self.hsts_enabled:
|
||||
@@ -167,185 +109,60 @@ class API:
|
||||
|
||||
self.add_middleware(TrustedHostMiddleware, allowed_hosts=self.allowed_hosts)
|
||||
|
||||
self.lifespan_handler = Lifespan()
|
||||
|
||||
if self.cors:
|
||||
self.add_middleware(CORSMiddleware, **self.cors_params)
|
||||
self.add_middleware(ServerErrorMiddleware, debug=debug)
|
||||
self.add_middleware(SessionMiddleware, secret_key=self.secret_key)
|
||||
|
||||
# Jinja environment
|
||||
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.
|
||||
if openapi or docs_route:
|
||||
self.openapi = OpenAPISchema(
|
||||
app=self,
|
||||
title=title,
|
||||
version=version,
|
||||
openapi=openapi,
|
||||
docs_route=docs_route,
|
||||
description=description,
|
||||
terms_of_service=terms_of_service,
|
||||
contact=contact,
|
||||
license=license,
|
||||
openapi_route=openapi_route,
|
||||
static_route=static_route,
|
||||
)
|
||||
|
||||
# TODO: Update docs for templates
|
||||
self.templates = Templates(directory=templates_dir)
|
||||
self.requests = (
|
||||
self.session()
|
||||
) #: A Requests session that is connected to the ASGI app.
|
||||
|
||||
@staticmethod
|
||||
def _default_wsgi_app(environ, start_response):
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def _notfound_wsgi_app(environ, start_response):
|
||||
start_response("404 NOT FOUND", [("Content-Type", "text/plain")])
|
||||
return [b"Not Found."]
|
||||
@property
|
||||
def static_app(self):
|
||||
if not hasattr(self, "_static_app"):
|
||||
assert self.static_dir is not None
|
||||
self._static_app = StaticFiles(directory=self.static_dir)
|
||||
return self._static_app
|
||||
|
||||
def before_request(self, websocket=False):
|
||||
def decorator(f):
|
||||
if websocket:
|
||||
self.before_requests.setdefault("ws", []).append(f)
|
||||
else:
|
||||
self.before_requests.setdefault("http", []).append(f)
|
||||
self.router.before_request(f, websocket=websocket)
|
||||
return f
|
||||
|
||||
return decorator
|
||||
|
||||
@property
|
||||
def before_http_requests(self):
|
||||
return self.before_requests.get("http", [])
|
||||
|
||||
@property
|
||||
def before_ws_requests(self):
|
||||
return self.before_requests.get("ws", [])
|
||||
|
||||
@property
|
||||
def _apispec(self):
|
||||
|
||||
info = {}
|
||||
if self.description is not None:
|
||||
info["description"] = self.description
|
||||
if self.terms_of_service is not None:
|
||||
info["termsOfService"] = self.terms_of_service
|
||||
if self.contact is not None:
|
||||
info["contact"] = self.contact
|
||||
if self.license is not None:
|
||||
info["license"] = self.license
|
||||
|
||||
spec = APISpec(
|
||||
title=self.title,
|
||||
version=self.version,
|
||||
openapi_version=self.openapi_version,
|
||||
plugins=[MarshmallowPlugin()],
|
||||
info=info,
|
||||
)
|
||||
|
||||
for route in self.routes:
|
||||
if self.routes[route].description:
|
||||
operations = yaml_utils.load_operations_from_docstring(
|
||||
self.routes[route].description
|
||||
)
|
||||
spec.path(path=route, operations=operations)
|
||||
|
||||
for name, schema in self.schemas.items():
|
||||
spec.components.schema(name, schema=schema)
|
||||
|
||||
return spec
|
||||
|
||||
@property
|
||||
def openapi(self):
|
||||
return self._apispec.to_yaml()
|
||||
|
||||
def add_middleware(self, middleware_cls, **middleware_config):
|
||||
self.app = middleware_cls(self.app, **middleware_config)
|
||||
|
||||
async def __call__(self, scope, receive, send):
|
||||
if scope["type"] == "lifespan":
|
||||
await self.lifespan_handler(scope, receive, send)
|
||||
return
|
||||
|
||||
path = scope["path"]
|
||||
root_path = scope.get("root_path", "")
|
||||
|
||||
# Call into a submounted app, if one exists.
|
||||
for path_prefix, app in self.apps.items():
|
||||
if path.startswith(path_prefix):
|
||||
scope["path"] = path[len(path_prefix) :]
|
||||
scope["root_path"] = root_path + path_prefix
|
||||
try:
|
||||
await app(scope, receive, send)
|
||||
return
|
||||
except TypeError:
|
||||
app = WSGIMiddleware(app)
|
||||
await app(scope, receive, send)
|
||||
return
|
||||
|
||||
await self.app(scope, receive, send)
|
||||
|
||||
async def asgi(self, scope, receive, send):
|
||||
assert scope["type"] in ("http", "websocket")
|
||||
|
||||
if scope["type"] == "websocket":
|
||||
await self._dispatch_ws(scope=scope, receive=receive, send=send)
|
||||
else:
|
||||
req = models.Request(scope, receive=receive, api=self)
|
||||
resp = await self._dispatch_http(
|
||||
req, scope=scope, send=send, receive=receive
|
||||
)
|
||||
await resp(scope, receive, send)
|
||||
|
||||
async def _dispatch_http(self, req, **options):
|
||||
# Set formats on Request object.
|
||||
req.formats = self.formats
|
||||
|
||||
# Get the route.
|
||||
route = self.path_matches_route(req.url.path)
|
||||
route = self.routes.get(route)
|
||||
if route:
|
||||
resp = models.Response(req=req, formats=self.formats)
|
||||
|
||||
for before_request in self.before_http_requests:
|
||||
await self.background(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=req, resp=resp, notfound=True)
|
||||
self.default_response(req=req, resp=resp)
|
||||
|
||||
self._prepare_session(resp)
|
||||
|
||||
return resp
|
||||
|
||||
async def _dispatch_ws(self, scope, receive, send):
|
||||
ws = WebSocket(scope=scope, receive=receive, send=send)
|
||||
|
||||
route = self.path_matches_route(ws.url.path)
|
||||
route = self.routes.get(route)
|
||||
|
||||
if route:
|
||||
for before_request in self.before_ws_requests:
|
||||
await self.background(before_request, ws=ws)
|
||||
await self.background(route.endpoint, ws)
|
||||
else:
|
||||
await send({"type": "websocket.close", "code": 1000})
|
||||
|
||||
def add_schema(self, name, schema, check_existing=True):
|
||||
"""Adds a mashmallow schema to the API specification."""
|
||||
if check_existing:
|
||||
assert name not in self.schemas
|
||||
|
||||
self.schemas[name] = schema
|
||||
|
||||
def schema(self, name, **options):
|
||||
"""Decorator for creating new routes around function and class definitions.
|
||||
|
||||
Usage::
|
||||
|
||||
from marshmallow import Schema, fields
|
||||
|
||||
@api.schema("Pet")
|
||||
class PetSchema(Schema):
|
||||
name = fields.Str()
|
||||
|
||||
"""
|
||||
|
||||
def decorator(f):
|
||||
self.add_schema(name=name, schema=f, **options)
|
||||
self.openapi.add_schema(name=name, schema=f, **options)
|
||||
return f
|
||||
|
||||
return decorator
|
||||
@@ -355,97 +172,18 @@ class API:
|
||||
|
||||
:param path: The path portion of a URL, to test all known routes against.
|
||||
"""
|
||||
for (route, route_object) in self.routes.items():
|
||||
if route_object.does_match(path):
|
||||
for route in self.router.routes:
|
||||
match, _ = route.matches(path)
|
||||
if match:
|
||||
return route
|
||||
|
||||
@property
|
||||
def _signer(self):
|
||||
return itsdangerous.Signer(self.secret_key)
|
||||
|
||||
def _prepare_session(self, resp):
|
||||
|
||||
if resp.session:
|
||||
data = self._signer.sign(
|
||||
b64encode(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 _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:
|
||||
await 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:
|
||||
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.
|
||||
|
||||
:param event_type: A string in ("startup", "shutdown")
|
||||
:param handler: The function to run. Can be either a function or a coroutine.
|
||||
"""
|
||||
|
||||
self.lifespan_handler.add_event_handler(event_type, handler)
|
||||
|
||||
def add_route(
|
||||
self,
|
||||
route=None,
|
||||
endpoint=None,
|
||||
*,
|
||||
default=False,
|
||||
static=False,
|
||||
static=True,
|
||||
check_existing=True,
|
||||
websocket=False,
|
||||
before_request=False,
|
||||
@@ -456,60 +194,25 @@ class API:
|
||||
:param endpoint: The endpoint for the route -- can be a callable, or a class.
|
||||
: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 before_request:
|
||||
if websocket:
|
||||
self.before_requests.setdefault("ws", []).append(endpoint)
|
||||
else:
|
||||
self.before_requests.setdefault("http", []).append(endpoint)
|
||||
return
|
||||
|
||||
if route is None:
|
||||
route = f"/{uuid4().hex}"
|
||||
|
||||
if check_existing:
|
||||
assert route not in self.routes
|
||||
|
||||
# Path
|
||||
if static:
|
||||
assert self.static_dir is not None
|
||||
if not endpoint:
|
||||
endpoint = self.static_response
|
||||
endpoint = self._static_response
|
||||
default = True
|
||||
|
||||
if default:
|
||||
self.default_endpoint = endpoint
|
||||
|
||||
self.routes[route] = Route(route, endpoint, websocket=websocket)
|
||||
# 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())
|
||||
self.router.add_route(
|
||||
route,
|
||||
endpoint,
|
||||
default=default,
|
||||
websocket=websocket,
|
||||
before_request=before_request,
|
||||
check_existing=check_existing,
|
||||
)
|
||||
|
||||
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=req, resp=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 docs_response(self, req, resp):
|
||||
resp.html = self.docs
|
||||
|
||||
def static_response(self, req, resp):
|
||||
|
||||
async def _static_response(self, req, resp):
|
||||
assert self.static_dir is not None
|
||||
|
||||
index = (self.static_dir / "index.html").resolve()
|
||||
@@ -520,28 +223,16 @@ class API:
|
||||
resp.status_code = status_codes.HTTP_404
|
||||
resp.text = "Not found."
|
||||
|
||||
def schema_response(self, req, resp):
|
||||
resp.status_code = status_codes.HTTP_200
|
||||
resp.headers["Content-Type"] = "application/x-yaml"
|
||||
resp.content = self.openapi
|
||||
|
||||
def redirect(
|
||||
self, resp, location, *, set_text=True, status_code=status_codes.HTTP_301
|
||||
):
|
||||
"""Redirects a given response to a given location.
|
||||
|
||||
:param resp: The Response to mutate.
|
||||
:param location: The location of the redirect.
|
||||
:param set_text: If ``True``, sets the Redirect body content automatically.
|
||||
: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)
|
||||
|
||||
resp.status_code = status_code
|
||||
if set_text:
|
||||
resp.text = f"Redirecting to: {location}"
|
||||
resp.headers.update({"Location": location})
|
||||
resp.redirect(location, set_text=set_text, status_code=status_code)
|
||||
|
||||
def on_event(self, event_type: str, **args):
|
||||
"""Decorator for registering functions or coroutines to run at certain events
|
||||
@@ -565,6 +256,15 @@ class API:
|
||||
|
||||
return decorator
|
||||
|
||||
def add_event_handler(self, event_type, handler):
|
||||
"""Adds an event handler to the API.
|
||||
|
||||
:param event_type: A string in ("startup", "shutdown")
|
||||
:param handler: The function to run. Can be either a function or a coroutine.
|
||||
"""
|
||||
|
||||
self.router.lifespan_handler.add_event_handler(event_type, handler)
|
||||
|
||||
def route(self, route=None, **options):
|
||||
"""Decorator for creating new routes around function and class definitions.
|
||||
|
||||
@@ -588,7 +288,7 @@ class API:
|
||||
:param route: String representation of the route to be used (shouldn't be parameterized).
|
||||
:param app: The other WSGI / ASGI app.
|
||||
"""
|
||||
self.apps.update({route: app})
|
||||
self.router.apps.update({route: app})
|
||||
|
||||
def session(self, base_url="http://;"):
|
||||
"""Testing HTTP client. Returns a Requests session object, able to send HTTP requests to the Responder application.
|
||||
@@ -600,11 +300,6 @@ class API:
|
||||
self._session = TestClient(self, base_url=base_url)
|
||||
return self._session
|
||||
|
||||
def _route_for(self, 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.
|
||||
@@ -612,67 +307,25 @@ class API:
|
||||
: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)
|
||||
if route_object:
|
||||
return route_object.url(**params)
|
||||
raise ValueError
|
||||
return self.router.url_for(endpoint, **params)
|
||||
|
||||
def static_url(self, asset):
|
||||
"""Given a static asset, return its URL path."""
|
||||
assert None not in (self.static_dir, self.static_route)
|
||||
return f"{self.static_route}/{str(asset)}"
|
||||
|
||||
@property
|
||||
def docs(self):
|
||||
|
||||
loader = jinja2.PrefixLoader(
|
||||
{
|
||||
self.docs_theme: jinja2.PackageLoader(
|
||||
"apistar", os.path.join("themes", self.docs_theme, "templates")
|
||||
)
|
||||
}
|
||||
)
|
||||
env = jinja2.Environment(autoescape=True, loader=loader)
|
||||
document = apistar.document.Document()
|
||||
document.content = yaml.safe_load(self.openapi)
|
||||
|
||||
template = env.get_template("/".join([self.docs_theme, "index.html"]))
|
||||
|
||||
return template.render(
|
||||
document=document,
|
||||
langs=["javascript", "python"],
|
||||
code_style=None,
|
||||
static_url=self.static_url,
|
||||
schema_url="/schema.yml",
|
||||
)
|
||||
|
||||
def template(self, name_, **values):
|
||||
def template(self, filename, *args, **kwargs):
|
||||
"""Renders the given `jinja2 <http://jinja.pocoo.org/docs/>`_ template, with provided values supplied.
|
||||
|
||||
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 values: Data to pass into the template.
|
||||
:param filename: The filename of the jinja2 template, in ``templates_dir``.
|
||||
:param *args: Data to pass into the template.
|
||||
:param *kwargs: Date to pass into the template.
|
||||
"""
|
||||
# Prepopulate values with base
|
||||
values = {**self.jinja_values_base, **values}
|
||||
return self.templates.render(filename, *args, **kwargs)
|
||||
|
||||
template = self.jinja_env.get_template(name_)
|
||||
return template.render(**values)
|
||||
|
||||
def template_string(self, s_, **values):
|
||||
def template_string(self, source, *args, **kwargs):
|
||||
"""Renders the given `jinja2 <http://jinja.pocoo.org/docs/>`_ template string, with provided values supplied.
|
||||
|
||||
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 values: Data to pass into the template.
|
||||
:param source: The template to use.
|
||||
:param *args: Data to pass into the template.
|
||||
:param **kwargs: Data to pass into the template.
|
||||
"""
|
||||
# Prepopulate values with base
|
||||
values = {**self.jinja_values_base, **values}
|
||||
|
||||
template = self.jinja_env.from_string(s_)
|
||||
return template.render(**values)
|
||||
return self.templates.render_string(source, *args, **kwargs)
|
||||
|
||||
def serve(self, *, address=None, port=None, debug=False, **options):
|
||||
"""Runs the application with uvicorn. If the ``PORT`` environment
|
||||
@@ -704,3 +357,6 @@ class API:
|
||||
if "debug" not in kwargs:
|
||||
kwargs.update({"debug": self.debug})
|
||||
self.serve(**kwargs)
|
||||
|
||||
async def __call__(self, scope, receive, send):
|
||||
await self.app(scope, receive, send)
|
||||
|
||||
@@ -3,7 +3,7 @@ from functools import partial
|
||||
|
||||
from graphql_server import default_format_error, encode_execution_results, json_encode
|
||||
|
||||
from ..templates import GRAPHIQL
|
||||
from .templates import GRAPHIQL
|
||||
|
||||
|
||||
class GraphQLView:
|
||||
@@ -44,7 +44,9 @@ class GraphQLView:
|
||||
show_graphiql = req.method == "get" and req.accepts("text/html")
|
||||
|
||||
if show_graphiql:
|
||||
resp.content = self.api.template_string(GRAPHIQL, endpoint=req.url.path)
|
||||
resp.content = self.api.templates.render_string(
|
||||
GRAPHIQL, endpoint=req.url.path
|
||||
)
|
||||
return
|
||||
|
||||
query, variables, operation_name = await self._resolve_graphql_query(req)
|
||||
@@ -63,3 +65,6 @@ class GraphQLView:
|
||||
|
||||
async def on_request(self, req, resp):
|
||||
await self.graphql_response(req, resp, self.schema)
|
||||
|
||||
async def __call__(self, req, resp):
|
||||
await self.on_request(req, resp)
|
||||
@@ -0,0 +1,160 @@
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
import apistar
|
||||
import jinja2
|
||||
import yaml
|
||||
from apispec import APISpec, yaml_utils
|
||||
from apispec.ext.marshmallow import MarshmallowPlugin
|
||||
|
||||
from responder.statics import DEFAULT_API_THEME
|
||||
from responder.staticfiles import StaticFiles
|
||||
from responder import status_codes
|
||||
|
||||
|
||||
class Schema:
|
||||
def __init__(
|
||||
self,
|
||||
app,
|
||||
title,
|
||||
version,
|
||||
plugins=None,
|
||||
description=None,
|
||||
terms_of_service=None,
|
||||
contact=None,
|
||||
license=None,
|
||||
openapi=None,
|
||||
openapi_route="/schema.yml",
|
||||
docs_route="/docs/",
|
||||
static_route="/static",
|
||||
):
|
||||
self.app = app
|
||||
self.schemas = {}
|
||||
self.title = title
|
||||
self.version = version
|
||||
self.description = description
|
||||
self.terms_of_service = terms_of_service
|
||||
self.contact = contact
|
||||
self.license = license
|
||||
|
||||
self.openapi_version = openapi
|
||||
self.openapi_route = openapi_route
|
||||
|
||||
self.docs_theme = DEFAULT_API_THEME
|
||||
self.docs_route = docs_route
|
||||
|
||||
self.plugins = [MarshmallowPlugin()] if plugins is None else plugins
|
||||
|
||||
if self.openapi_version is not None:
|
||||
self.app.add_route(self.openapi_route, self.schema_response)
|
||||
|
||||
if self.docs_route is not None:
|
||||
self.app.add_route(self.docs_route, self.docs_response)
|
||||
|
||||
theme_path = (
|
||||
Path(apistar.__file__).parent / "themes" / self.docs_theme / "static"
|
||||
).resolve()
|
||||
|
||||
self.static_route = static_route
|
||||
|
||||
self.app.static_app.add_directory(theme_path)
|
||||
|
||||
@property
|
||||
def _apispec(self):
|
||||
|
||||
info = {}
|
||||
if self.description is not None:
|
||||
info["description"] = self.description
|
||||
if self.terms_of_service is not None:
|
||||
info["termsOfService"] = self.terms_of_service
|
||||
if self.contact is not None:
|
||||
info["contact"] = self.contact
|
||||
if self.license is not None:
|
||||
info["license"] = self.license
|
||||
|
||||
spec = APISpec(
|
||||
title=self.title,
|
||||
version=self.version,
|
||||
openapi_version=self.openapi_version,
|
||||
plugins=self.plugins,
|
||||
info=info,
|
||||
)
|
||||
|
||||
for route in self.app.router.routes:
|
||||
if route.description:
|
||||
operations = yaml_utils.load_operations_from_docstring(
|
||||
route.description
|
||||
)
|
||||
spec.path(path=route.route, operations=operations)
|
||||
|
||||
for name, schema in self.schemas.items():
|
||||
spec.components.schema(name, schema=schema)
|
||||
|
||||
return spec
|
||||
|
||||
@property
|
||||
def openapi(self):
|
||||
return self._apispec.to_yaml()
|
||||
|
||||
def add_schema(self, name, schema, check_existing=True):
|
||||
"""Adds a mashmallow schema to the API specification."""
|
||||
if check_existing:
|
||||
assert name not in self.schemas
|
||||
|
||||
self.schemas[name] = schema
|
||||
|
||||
def schema(self, name, **options):
|
||||
"""Decorator for creating new routes around function and class definitions.
|
||||
|
||||
Usage::
|
||||
|
||||
from marshmallow import Schema, fields
|
||||
|
||||
@api.schema("Pet")
|
||||
class PetSchema(Schema):
|
||||
name = fields.Str()
|
||||
|
||||
"""
|
||||
|
||||
def decorator(f):
|
||||
self.add_schema(name=name, schema=f, **options)
|
||||
return f
|
||||
|
||||
return decorator
|
||||
|
||||
@property
|
||||
def docs(self):
|
||||
|
||||
loader = jinja2.PrefixLoader(
|
||||
{
|
||||
self.docs_theme: jinja2.PackageLoader(
|
||||
"apistar", os.path.join("themes", self.docs_theme, "templates")
|
||||
)
|
||||
}
|
||||
)
|
||||
env = jinja2.Environment(autoescape=True, loader=loader)
|
||||
document = apistar.document.Document()
|
||||
document.content = yaml.safe_load(self.openapi)
|
||||
|
||||
template = env.get_template("/".join([self.docs_theme, "index.html"]))
|
||||
|
||||
return template.render(
|
||||
document=document,
|
||||
langs=["javascript", "python"],
|
||||
code_style=None,
|
||||
static_url=self.static_url,
|
||||
schema_url="/schema.yml",
|
||||
)
|
||||
|
||||
def static_url(self, asset):
|
||||
"""Given a static asset, return its URL path."""
|
||||
assert self.static_route is not None
|
||||
return f"{self.static_route}/{str(asset)}"
|
||||
|
||||
def docs_response(self, req, resp):
|
||||
resp.html = self.docs
|
||||
|
||||
def schema_response(self, req, resp):
|
||||
resp.status_code = status_codes.HTTP_200
|
||||
resp.headers["Content-Type"] = "application/x-yaml"
|
||||
resp.content = self.openapi
|
||||
+17
-20
@@ -3,16 +3,18 @@ import io
|
||||
import inspect
|
||||
import json
|
||||
import gzip
|
||||
from urllib.parse import parse_qs
|
||||
from base64 import b64decode
|
||||
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, State
|
||||
from starlette.responses import (
|
||||
@@ -20,9 +22,7 @@ from starlette.responses import (
|
||||
StreamingResponse as StarletteStreamingResponse,
|
||||
)
|
||||
|
||||
from urllib.parse import parse_qs
|
||||
|
||||
from .status_codes import HTTP_200
|
||||
from .status_codes import HTTP_200, HTTP_301
|
||||
from .statics import DEFAULT_ENCODING
|
||||
|
||||
|
||||
@@ -105,9 +105,9 @@ class Request:
|
||||
"_cookies",
|
||||
]
|
||||
|
||||
def __init__(self, scope, receive, api=None):
|
||||
def __init__(self, scope, receive, api=None, formats=None):
|
||||
self._starlette = StarletteRequest(scope, receive)
|
||||
self.formats = None
|
||||
self.formats = formats
|
||||
self._encoding = None
|
||||
self.api = api
|
||||
self._content = None
|
||||
@@ -122,14 +122,7 @@ class Request:
|
||||
@property
|
||||
def session(self):
|
||||
"""The session data, in dict form, from the Request."""
|
||||
if self.api.session_cookie in self.cookies:
|
||||
|
||||
data = self.cookies[self.api.session_cookie]
|
||||
|
||||
data = self.api._signer.unsign(data)
|
||||
data = b64decode(data)
|
||||
return json.loads(data)
|
||||
return {}
|
||||
return self._starlette.session
|
||||
|
||||
@property
|
||||
def headers(self):
|
||||
@@ -182,12 +175,12 @@ class Request:
|
||||
def state(self) -> State:
|
||||
"""
|
||||
Use the state to store additional information.
|
||||
|
||||
|
||||
This can be a very helpful feature, if you want to hand over
|
||||
information from a middelware or a route decorator to the
|
||||
actual route handler.
|
||||
|
||||
For example: ``request.state.time_started = time.time()``
|
||||
Usage: ``request.state.time_started = time.time()``
|
||||
"""
|
||||
return self._starlette.state
|
||||
|
||||
@@ -290,9 +283,7 @@ class Response:
|
||||
self.content = None #: A bytes representation of the response body.
|
||||
self.mimetype = None
|
||||
self.encoding = DEFAULT_ENCODING
|
||||
self.media = (
|
||||
None
|
||||
) #: A Python object that will be content-negotiated and sent back to the client. Typically, in JSON formatting.
|
||||
self.media = None #: A Python object that will be content-negotiated and sent back to the client. Typically, in JSON formatting.
|
||||
self._stream = None
|
||||
self.headers = (
|
||||
{}
|
||||
@@ -300,7 +291,7 @@ class Response:
|
||||
self.formats = formats
|
||||
self.cookies = SimpleCookie() #: The cookies set in the Response
|
||||
self.session = (
|
||||
req.session.copy()
|
||||
req.session
|
||||
) #: The cookie-based session data, in dict form, to add to the Response.
|
||||
|
||||
# Property or func/dec
|
||||
@@ -311,6 +302,12 @@ class Response:
|
||||
|
||||
return func
|
||||
|
||||
def redirect(self, location, *, set_text=True, status_code=HTTP_301):
|
||||
self.status_code = status_code
|
||||
if set_text:
|
||||
self.text = f"Redirecting to: {location}"
|
||||
self.headers.update({"Location": location})
|
||||
|
||||
@property
|
||||
async def body(self):
|
||||
if self._stream is not None:
|
||||
|
||||
+290
-55
@@ -1,43 +1,76 @@
|
||||
import asyncio
|
||||
import json
|
||||
import re
|
||||
import functools
|
||||
import inspect
|
||||
from parse import parse, with_pattern
|
||||
|
||||
from starlette.routing import Lifespan
|
||||
from starlette.middleware.wsgi import WSGIMiddleware
|
||||
from starlette.websockets import WebSocket, WebSocketClose
|
||||
from starlette.concurrency import run_in_threadpool
|
||||
from starlette.exceptions import HTTPException
|
||||
|
||||
from .models import Request, Response
|
||||
from . import status_codes
|
||||
from .formats import get_formats
|
||||
from .statics import DEFAULT_SESSION_COOKIE
|
||||
|
||||
|
||||
def _make_convertor(type, pattern):
|
||||
@with_pattern(pattern)
|
||||
def inner(value):
|
||||
return type(value)
|
||||
|
||||
return inner
|
||||
|
||||
|
||||
_convertors = {
|
||||
"int": _make_convertor(int, r"\d+"),
|
||||
"str": _make_convertor(str, r"[^/]+"),
|
||||
"float": _make_convertor(float, r"\d+(.\d+)?"),
|
||||
_CONVERTORS = {
|
||||
"int": (int, r"\d+"),
|
||||
"str": (str, r"[^/]+"),
|
||||
"float": (float, r"\d+(.\d+)?"),
|
||||
}
|
||||
|
||||
PARAM_RE = re.compile("{([a-zA-Z_][a-zA-Z0-9_]*)(:[a-zA-Z_][a-zA-Z0-9_]*)?}")
|
||||
|
||||
class Route:
|
||||
_param_pattern = re.compile(r"{([^{}]*)}")
|
||||
|
||||
def __init__(self, route, endpoint, *, websocket=False, before_request=False):
|
||||
def compile_path(path):
|
||||
path_re = "^"
|
||||
param_convertors = {}
|
||||
idx = 0
|
||||
|
||||
for match in PARAM_RE.finditer(path):
|
||||
param_name, convertor_type = match.groups(default="str")
|
||||
convertor_type = convertor_type.lstrip(":")
|
||||
assert (
|
||||
convertor_type in _CONVERTORS.keys()
|
||||
), f"Unknown path convertor '{convertor_type}'"
|
||||
convertor, convertor_re = _CONVERTORS[convertor_type]
|
||||
|
||||
path_re += path[idx : match.start()]
|
||||
path_re += rf"(?P<{param_name}>{convertor_re})"
|
||||
|
||||
param_convertors[param_name] = convertor
|
||||
|
||||
idx = match.end()
|
||||
|
||||
path_re += path[idx:] + "$"
|
||||
|
||||
return re.compile(path_re), param_convertors
|
||||
|
||||
|
||||
class BaseRoute:
|
||||
def matches(self, scope):
|
||||
raise NotImplementedError()
|
||||
|
||||
async def __call__(self, scope, receive, send):
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
class Route(BaseRoute):
|
||||
def __init__(self, route, endpoint, *, before_request=False):
|
||||
assert route.startswith("/"), "Route path must start with '/'"
|
||||
self.route = route
|
||||
self.endpoint = endpoint
|
||||
self.uses_websocket = websocket
|
||||
self.before_request = before_request
|
||||
|
||||
self.path_re, self.param_convertors = compile_path(route)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<Route {self.route!r}={self.endpoint!r}>"
|
||||
|
||||
def __eq__(self, other):
|
||||
if hasattr(other, "route"):
|
||||
# Being compared to other routes.
|
||||
return self.route == other.route
|
||||
else:
|
||||
# Strings.
|
||||
return self.does_match(other)
|
||||
def url(self, **params):
|
||||
return self.route.format(**params)
|
||||
|
||||
@property
|
||||
def endpoint_name(self):
|
||||
@@ -47,46 +80,248 @@ class Route:
|
||||
def description(self):
|
||||
return self.endpoint.__doc__
|
||||
|
||||
@property
|
||||
def has_parameters(self):
|
||||
return bool(self._param_pattern.search(self.route))
|
||||
def matches(self, scope):
|
||||
if scope["type"] != "http":
|
||||
return False, {}
|
||||
|
||||
@functools.lru_cache(maxsize=None)
|
||||
def does_match(self, s):
|
||||
if s == self.route:
|
||||
return True
|
||||
path = scope["path"]
|
||||
match = self.path_re.match(path)
|
||||
|
||||
named = self.incoming_matches(s)
|
||||
return bool(len(named))
|
||||
if match is None:
|
||||
return False, {}
|
||||
|
||||
@functools.lru_cache(maxsize=None)
|
||||
def incoming_matches(self, s):
|
||||
results = parse(self.route, s, _convertors)
|
||||
return results.named if results else {}
|
||||
matched_params = match.groupdict()
|
||||
for key, value in matched_params.items():
|
||||
matched_params[key] = self.param_convertors[key](value)
|
||||
|
||||
return True, {"path_params": {**matched_params}}
|
||||
|
||||
async def __call__(self, scope, receive, send):
|
||||
request = Request(scope, receive, formats=get_formats())
|
||||
response = Response(req=request, formats=get_formats())
|
||||
|
||||
path_params = scope.get("path_params", {})
|
||||
before_requests = scope.get("before_requests", [])
|
||||
|
||||
for before_request in before_requests.get("http", []):
|
||||
if asyncio.iscoroutinefunction(before_request):
|
||||
await before_request(request, response)
|
||||
else:
|
||||
await run_in_threadpool(before_request, request, response)
|
||||
|
||||
views = []
|
||||
|
||||
if inspect.isclass(self.endpoint):
|
||||
endpoint = self.endpoint()
|
||||
on_request = getattr(endpoint, "on_request", None)
|
||||
if on_request:
|
||||
views.append(on_request)
|
||||
|
||||
method_name = f"on_{request.method}"
|
||||
try:
|
||||
view = getattr(endpoint, method_name)
|
||||
views.append(view)
|
||||
except AttributeError:
|
||||
if on_request is None:
|
||||
raise HTTPException(status_code=status_codes.HTTP_405)
|
||||
else:
|
||||
views.append(self.endpoint)
|
||||
|
||||
for view in views:
|
||||
# "Monckey patch" for graphql: explicitly checking __call__
|
||||
if asyncio.iscoroutinefunction(view) or asyncio.iscoroutinefunction(
|
||||
view.__call__
|
||||
):
|
||||
await view(request, response, **path_params)
|
||||
else:
|
||||
await run_in_threadpool(view, request, response, **path_params)
|
||||
|
||||
if response.status_code is None:
|
||||
response.status_code = status_codes.HTTP_200
|
||||
|
||||
await response(scope, receive, send)
|
||||
|
||||
def __eq__(self, other):
|
||||
# [TODO] compare to str ?
|
||||
return self.route == other.route and self.endpoint == other.endpoint
|
||||
|
||||
def __hash__(self):
|
||||
return hash(self.route) ^ hash(self.endpoint) ^ hash(self.before_request)
|
||||
|
||||
|
||||
class WebSocketRoute(BaseRoute):
|
||||
def __init__(self, route, endpoint, *, before_request=False):
|
||||
assert route.startswith("/"), "Route path must start with '/'"
|
||||
self.route = route
|
||||
self.endpoint = endpoint
|
||||
self.before_request = before_request
|
||||
|
||||
self.path_re, self.param_convertors = compile_path(route)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<Route {self.route!r}={self.endpoint!r}>"
|
||||
|
||||
def url(self, **params):
|
||||
return self.route.format(**params)
|
||||
|
||||
def _weight(self):
|
||||
params = set(self._param_pattern.findall(self.route))
|
||||
params_count = len(params)
|
||||
w = len(self.route.rsplit("}", 1)[-1].strip("/"))
|
||||
return params_count != 0, w == 0, -params_count
|
||||
@property
|
||||
def endpoint_name(self):
|
||||
return self.endpoint.__name__
|
||||
|
||||
@property
|
||||
def is_class_based(self):
|
||||
return inspect.isclass(self.endpoint)
|
||||
def description(self):
|
||||
return self.endpoint.__doc__
|
||||
|
||||
@property
|
||||
def is_function(self):
|
||||
code = hasattr(self.endpoint, "__code__")
|
||||
kwdefaults = hasattr(self.endpoint, "__kwdefaults__")
|
||||
return all((callable(self.endpoint), code, kwdefaults))
|
||||
def matches(self, scope):
|
||||
if scope["type"] != "websocket":
|
||||
return False, {}
|
||||
|
||||
path = scope["path"]
|
||||
match = self.path_re.match(path)
|
||||
|
||||
if match is None:
|
||||
return False, {}
|
||||
|
||||
matched_params = match.groupdict()
|
||||
for key, value in matched_params.items():
|
||||
matched_params[key] = self.param_convertors[key](value)
|
||||
|
||||
return True, {"path_params": {**matched_params}}
|
||||
|
||||
async def __call__(self, scope, receive, send):
|
||||
ws = WebSocket(scope, receive, send)
|
||||
|
||||
before_requests = scope.get("before_requests", [])
|
||||
for before_request in before_requests.get("ws", []):
|
||||
await before_request(ws)
|
||||
|
||||
await self.endpoint(ws)
|
||||
|
||||
def __eq__(self, other):
|
||||
# [TODO] compare to str ?
|
||||
return self.route == other.route and self.endpoint == other.endpoint
|
||||
|
||||
def __hash__(self):
|
||||
return (
|
||||
hash(self.route)
|
||||
^ hash(self.endpoint)
|
||||
^ hash(self.uses_websocket)
|
||||
^ hash(self.before_request)
|
||||
return hash(self.route) ^ hash(self.endpoint) ^ hash(self.before_request)
|
||||
|
||||
|
||||
class Router:
|
||||
def __init__(self, routes=None, default_response=None, before_requests=None):
|
||||
self.routes = [] if routes is None else list(routes)
|
||||
# [TODO] Make its own router
|
||||
self.apps = {}
|
||||
self.default_endpoint = (
|
||||
self.default_response if default_response is None else default_response
|
||||
)
|
||||
self.lifespan_handler = Lifespan()
|
||||
self.before_requests = (
|
||||
{"http": [], "ws": []} if before_requests is None else before_requests
|
||||
)
|
||||
|
||||
def add_route(
|
||||
self,
|
||||
route=None,
|
||||
endpoint=None,
|
||||
*,
|
||||
default=False,
|
||||
websocket=False,
|
||||
before_request=False,
|
||||
check_existing=False,
|
||||
):
|
||||
""" Adds a route to the router.
|
||||
:param route: A string representation of the route
|
||||
:param endpoint: The endpoint for the route -- can be callable, or class.
|
||||
:param default: If ``True``, all unknown requests will route to this view.
|
||||
"""
|
||||
if before_request:
|
||||
if websocket:
|
||||
self.before_requests.setdefault("ws", []).append(endpoint)
|
||||
else:
|
||||
self.before_requests.setdefault("http", []).append(endpoint)
|
||||
return
|
||||
|
||||
if check_existing:
|
||||
assert not self.routes or route not in (
|
||||
item.route for item in self.routes
|
||||
), f"Route '{route}' already exists"
|
||||
|
||||
if default:
|
||||
self.default_endpoint = endpoint
|
||||
|
||||
if websocket:
|
||||
route = WebSocketRoute(route, endpoint)
|
||||
else:
|
||||
route = Route(route, endpoint)
|
||||
|
||||
self.routes.append(route)
|
||||
|
||||
def mount(self, route, app):
|
||||
"""Mounts ASGI / WSGI applications at a given route
|
||||
"""
|
||||
self.apps.update(route, app)
|
||||
|
||||
def before_request(self, endpoint, websocket=False):
|
||||
if websocket:
|
||||
self.before_requests.setdefault("ws", []).append(endpoint)
|
||||
else:
|
||||
self.before_requests.setdefault("http", []).append(endpoint)
|
||||
|
||||
def url_for(self, endpoint, **params):
|
||||
# TODO: Check for params
|
||||
for route in self.routes:
|
||||
if endpoint in (route.endpoint, route.endpoint.__name__):
|
||||
return route.url(**params)
|
||||
return None
|
||||
|
||||
async def default_response(self, scope, receive, send):
|
||||
if scope["type"] == "websocket":
|
||||
websocket_close = WebSocketClose()
|
||||
await websocket_close(receive, send)
|
||||
return
|
||||
|
||||
request = Request(scope, receive)
|
||||
response = Response(request, formats=get_formats())
|
||||
|
||||
raise HTTPException(status_code=status_codes.HTTP_404)
|
||||
|
||||
def _resolve_route(self, scope):
|
||||
for route in self.routes:
|
||||
matches, child_scope = route.matches(scope)
|
||||
if matches:
|
||||
scope.update(child_scope)
|
||||
return route
|
||||
return None
|
||||
|
||||
async def __call__(self, scope, receive, send):
|
||||
assert scope["type"] in ("http", "websocket", "lifespan")
|
||||
|
||||
if scope["type"] == "lifespan":
|
||||
await self.lifespan_handler(scope, receive, send)
|
||||
return
|
||||
|
||||
path = scope["path"]
|
||||
root_path = scope.get("root_path", "")
|
||||
|
||||
# Check "primary" mounted routes first (before submounted apps)
|
||||
route = self._resolve_route(scope)
|
||||
|
||||
scope["before_requests"] = self.before_requests
|
||||
|
||||
if route is not None:
|
||||
await route(scope, receive, send)
|
||||
return
|
||||
|
||||
# Call into a submounted app, if one exists.
|
||||
for path_prefix, app in self.apps.items():
|
||||
if path.startswith(path_prefix):
|
||||
scope["path"] = path[len(path_prefix) :]
|
||||
scope["root_path"] = root_path + path_prefix
|
||||
try:
|
||||
await app(scope, receive, send)
|
||||
return
|
||||
except TypeError:
|
||||
app = WSGIMiddleware(app)
|
||||
await app(scope, receive, send)
|
||||
return
|
||||
|
||||
await self.default_response(scope, receive, send)
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import typing
|
||||
|
||||
from starlette.staticfiles import StaticFiles
|
||||
|
||||
|
||||
class StaticFiles(StaticFiles):
|
||||
"""I've created an issue to disccuss allowing multiple directories in starletter's `StaticFiles`.
|
||||
|
||||
https://github.com/encode/starlette/issues/625
|
||||
|
||||
I've also made a PR to add this method to starlette StaticFiles
|
||||
Once accepted we will remove this.
|
||||
|
||||
https://github.com/encode/starlette/pull/626
|
||||
"""
|
||||
|
||||
def add_directory(self, directory: str) -> None:
|
||||
self.all_directories = [*self.all_directories, *self.get_directories(directory)]
|
||||
@@ -0,0 +1,59 @@
|
||||
from contextlib import contextmanager
|
||||
|
||||
import jinja2
|
||||
|
||||
|
||||
class Templates:
|
||||
def __init__(
|
||||
self, directory="templates", autoescape=True, context=None, enable_async=False
|
||||
):
|
||||
self.directory = directory
|
||||
self._env = jinja2.Environment(
|
||||
loader=jinja2.FileSystemLoader([str(self.directory)]),
|
||||
autoescape=autoescape,
|
||||
enable_async=enable_async,
|
||||
)
|
||||
self.default_context = {} if context is None else {**context}
|
||||
self._env.globals.update(self.default_context)
|
||||
|
||||
@property
|
||||
def context(self):
|
||||
return self._env.globals
|
||||
|
||||
@context.setter
|
||||
def context(self, context):
|
||||
self._env.globals = {**self.default_context, **context}
|
||||
|
||||
def get_template(self, name):
|
||||
return self._env.get_template(name)
|
||||
|
||||
def render(self, template, *args, **kwargs):
|
||||
"""Renders the given `jinja2 <http://jinja.pocoo.org/docs/>`_ template, with provided values supplied.
|
||||
|
||||
:param template: The filename of the jinja2 template.
|
||||
:param **kwargs: Data to pass into the template.
|
||||
:param **kwargs: Data to pass into the template.
|
||||
"""
|
||||
return self.get_template(template).render(*args, **kwargs)
|
||||
|
||||
@contextmanager
|
||||
def _async(self):
|
||||
self._env.is_async = True
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
self._env.is_async = False
|
||||
|
||||
async def render_async(self, template, *args, **kwargs):
|
||||
with self._async():
|
||||
return await self.get_template(template).render_async(*args, **kwargs)
|
||||
|
||||
def render_string(self, source, *args, **kwargs):
|
||||
"""Renders the given `jinja2 <http://jinja.pocoo.org/docs/>`_ template string, with provided values supplied.
|
||||
|
||||
:param source: The template to use.
|
||||
:param *args, **kwargs: Data to pass into the template.
|
||||
:param **kwargs: Data to pass into the template.
|
||||
"""
|
||||
template = self._env.from_string(source)
|
||||
return template.render(*args, **kwargs)
|
||||
@@ -23,15 +23,13 @@ if sys.argv[-1] == "publish":
|
||||
|
||||
required = [
|
||||
"starlette==0.12.*",
|
||||
"uvicorn>=0.7, <0.9",
|
||||
"uvicorn==0.10.*",
|
||||
"aiofiles",
|
||||
"pyyaml",
|
||||
"requests",
|
||||
"graphene",
|
||||
"graphene<3.0",
|
||||
"graphql-server-core>=1.1",
|
||||
"jinja2",
|
||||
"parse",
|
||||
"uvloop; sys_platform != 'win32' and sys_platform != 'cygwin' and sys_platform != 'cli'",
|
||||
"rfc3986",
|
||||
"python-multipart",
|
||||
"chardet",
|
||||
@@ -39,9 +37,9 @@ required = [
|
||||
"marshmallow",
|
||||
"whitenoise",
|
||||
"docopt",
|
||||
"itsdangerous",
|
||||
"requests-toolbelt",
|
||||
"apistar",
|
||||
"itsdangerous",
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -56,3 +56,12 @@ def schema():
|
||||
return f"Hello {name}"
|
||||
|
||||
return graphene.Schema(query=Query)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def template_path(tmpdir):
|
||||
# create a Jinja template file on the filesystem
|
||||
template_name = "test.html"
|
||||
template_file = tmpdir.mkdir("static").join(template_name)
|
||||
template_file.write("{{ var }}")
|
||||
return template_file
|
||||
|
||||
+204
-90
@@ -7,6 +7,8 @@ import responder
|
||||
import requests
|
||||
import string
|
||||
import io
|
||||
from responder.routes import Router, Route, WebSocketRoute
|
||||
from responder.templates import Templates
|
||||
|
||||
from starlette.middleware.base import BaseHTTPMiddleware
|
||||
from starlette.responses import PlainTextResponse
|
||||
@@ -19,6 +21,47 @@ def test_api_basic_route(api):
|
||||
resp.text = "hello world!"
|
||||
|
||||
|
||||
def test_route_repr():
|
||||
def home(req, resp):
|
||||
"""Home page
|
||||
"""
|
||||
resp.text = "Hello !"
|
||||
|
||||
route = Route("/", home)
|
||||
|
||||
assert route.__repr__() == f"<Route '/'={home!r}>"
|
||||
|
||||
assert route.endpoint_name == home.__name__
|
||||
assert route.description == home.__doc__
|
||||
|
||||
|
||||
def test_websocket_route_repr():
|
||||
def chat_endpoint(ws):
|
||||
"""Chat
|
||||
"""
|
||||
pass
|
||||
|
||||
route = WebSocketRoute("/", chat_endpoint)
|
||||
|
||||
assert route.__repr__() == f"<Route '/'={chat_endpoint!r}>"
|
||||
|
||||
assert route.endpoint_name == chat_endpoint.__name__
|
||||
assert route.description == chat_endpoint.__doc__
|
||||
|
||||
|
||||
def test_route_eq():
|
||||
def home(req, resp):
|
||||
resp.text = "Hello !"
|
||||
|
||||
assert Route("/", home) == Route("/", home)
|
||||
|
||||
def chat(ws):
|
||||
pass
|
||||
|
||||
assert WebSocketRoute("/", home) == WebSocketRoute("/", home)
|
||||
|
||||
|
||||
"""
|
||||
def test_api_basic_route_overlap(api):
|
||||
@api.route("/")
|
||||
def home(req, resp):
|
||||
@@ -29,39 +72,7 @@ def test_api_basic_route_overlap(api):
|
||||
@api.route("/")
|
||||
def home2(req, resp):
|
||||
resp.text = "hello world!"
|
||||
|
||||
|
||||
def test_api_basic_route_overlap_alternative(api):
|
||||
@api.route("/")
|
||||
def home(req, resp):
|
||||
resp.text = "hello world!"
|
||||
|
||||
def home2(req, resp):
|
||||
resp.text = "hello world!"
|
||||
|
||||
with pytest.raises(AssertionError):
|
||||
api.add_route("/", home2)
|
||||
|
||||
|
||||
def test_api_basic_route_overlap_allowed(api):
|
||||
@api.route("/")
|
||||
def home(req, resp):
|
||||
resp.text = "hello world!"
|
||||
|
||||
def home2(req, resp):
|
||||
resp.text = "hello world!"
|
||||
|
||||
api.add_route("/", home2, check_existing=False)
|
||||
|
||||
|
||||
def test_api_basic_route_overlap_allowed_alternative(api):
|
||||
@api.route("/")
|
||||
def home(req, resp):
|
||||
resp.text = "hello world!"
|
||||
|
||||
@api.route("/", check_existing=False)
|
||||
def home2(req, resp):
|
||||
resp.text = "hello world!"
|
||||
"""
|
||||
|
||||
|
||||
def test_class_based_view_registration(api):
|
||||
@@ -74,10 +85,10 @@ def test_class_based_view_registration(api):
|
||||
def test_class_based_view_parameters(api):
|
||||
@api.route("/{greeting}")
|
||||
class Greeting:
|
||||
def on_request(self, req, resp, *, greeting):
|
||||
resp.text = f"{greeting}, world!"
|
||||
pass
|
||||
|
||||
assert api.session().get("http://;/Hello").ok
|
||||
resp = api.session().get("http://;/Hello")
|
||||
assert resp.status_code == api.status_codes.HTTP_405
|
||||
|
||||
|
||||
def test_requests_session(api):
|
||||
@@ -85,14 +96,14 @@ def test_requests_session(api):
|
||||
assert api.requests
|
||||
|
||||
|
||||
def test_requests_session_works(api, url):
|
||||
def test_requests_session_works(api):
|
||||
TEXT = "spiral out"
|
||||
|
||||
@api.route("/")
|
||||
def hello(req, resp):
|
||||
resp.text = TEXT
|
||||
|
||||
assert api.requests.get(url("/")).text == TEXT
|
||||
assert api.requests.get("/").text == TEXT
|
||||
|
||||
|
||||
def test_status_code(api):
|
||||
@@ -338,13 +349,45 @@ def test_yaml_downloads(api):
|
||||
assert yaml.safe_load(r.content) == dump
|
||||
|
||||
|
||||
def test_schema_generation_explicit():
|
||||
import responder
|
||||
from responder.ext.schema import Schema as OpenAPISchema
|
||||
import marshmallow
|
||||
|
||||
api = responder.API()
|
||||
|
||||
schema = OpenAPISchema(app=api, title="Web Service", version="1.0", openapi="3.0.2")
|
||||
|
||||
@schema.schema("Pet")
|
||||
class PetSchema(marshmallow.Schema):
|
||||
name = marshmallow.fields.Str()
|
||||
|
||||
@api.route("/")
|
||||
def route(req, resp):
|
||||
"""A cute furry animal endpoint.
|
||||
---
|
||||
get:
|
||||
description: Get a random pet
|
||||
responses:
|
||||
200:
|
||||
description: A pet to be returned
|
||||
schema:
|
||||
$ref: "#/components/schemas/Pet"
|
||||
"""
|
||||
resp.media = PetSchema().dump({"name": "little orange"})
|
||||
|
||||
r = api.requests.get("http://;/schema.yml")
|
||||
dump = yaml.safe_load(r.content)
|
||||
|
||||
assert dump
|
||||
assert dump["openapi"] == "3.0.2"
|
||||
|
||||
|
||||
def test_schema_generation():
|
||||
import responder
|
||||
from marshmallow import Schema, fields
|
||||
|
||||
api = responder.API(
|
||||
title="Web Service", openapi="3.0.2", allowed_hosts=["testserver", ";"]
|
||||
)
|
||||
api = responder.API(title="Web Service", openapi="3.0.2")
|
||||
|
||||
@api.schema("Pet")
|
||||
class PetSchema(Schema):
|
||||
@@ -371,6 +414,60 @@ def test_schema_generation():
|
||||
assert dump["openapi"] == "3.0.2"
|
||||
|
||||
|
||||
def test_documentation_explicit():
|
||||
import responder
|
||||
from responder.ext.schema import Schema as OpenAPISchema
|
||||
|
||||
import marshmallow
|
||||
|
||||
description = "This is a sample server for a pet store."
|
||||
terms_of_service = "http://example.com/terms/"
|
||||
contact = {
|
||||
"name": "API Support",
|
||||
"url": "http://www.example.com/support",
|
||||
"email": "support@example.com",
|
||||
}
|
||||
license = {
|
||||
"name": "Apache 2.0",
|
||||
"url": "https://www.apache.org/licenses/LICENSE-2.0.html",
|
||||
}
|
||||
|
||||
api = responder.API(allowed_hosts=["testserver", ";"])
|
||||
|
||||
schema = OpenAPISchema(
|
||||
app=api,
|
||||
title="Web Service",
|
||||
version="1.0",
|
||||
openapi="3.0.2",
|
||||
docs_route="/docs",
|
||||
description=description,
|
||||
terms_of_service=terms_of_service,
|
||||
contact=contact,
|
||||
license=license,
|
||||
)
|
||||
|
||||
@schema.schema("Pet")
|
||||
class PetSchema(marshmallow.Schema):
|
||||
name = marshmallow.fields.Str()
|
||||
|
||||
@api.route("/")
|
||||
def route(req, resp):
|
||||
"""A cute furry animal endpoint.
|
||||
---
|
||||
get:
|
||||
description: Get a random pet
|
||||
responses:
|
||||
200:
|
||||
description: A pet to be returned
|
||||
schema:
|
||||
$ref: "#/components/schemas/Pet"
|
||||
"""
|
||||
resp.media = PetSchema().dump({"name": "little orange"})
|
||||
|
||||
r = api.requests.get("/docs")
|
||||
assert "html" in r.text
|
||||
|
||||
|
||||
def test_documentation():
|
||||
import responder
|
||||
from marshmallow import Schema, fields
|
||||
@@ -485,7 +582,7 @@ def test_sessions(api):
|
||||
assert r.json() == {"hello": "world"}
|
||||
|
||||
|
||||
def test_template_rendering(api):
|
||||
def test_template_string_rendering(api):
|
||||
@api.route("/")
|
||||
def view(req, resp):
|
||||
resp.content = api.template_string("{{ var }}", var="hello")
|
||||
@@ -494,6 +591,39 @@ def test_template_rendering(api):
|
||||
assert r.text == "hello"
|
||||
|
||||
|
||||
def test_template_rendering(template_path):
|
||||
api = responder.API(templates_dir=template_path.dirpath())
|
||||
|
||||
@api.route("/")
|
||||
def view(req, resp):
|
||||
resp.content = api.template(template_path.basename, var="hello")
|
||||
|
||||
r = api.requests.get(api.url_for(view))
|
||||
assert r.text == "hello"
|
||||
|
||||
|
||||
def test_template(api, template_path):
|
||||
templates = Templates(directory=template_path.dirpath())
|
||||
|
||||
@api.route("/{var}/")
|
||||
def view(req, resp, var):
|
||||
resp.html = templates.render(template_path.basename, var=var)
|
||||
|
||||
r = api.requests.get("/test/")
|
||||
assert r.text == "test"
|
||||
|
||||
|
||||
def test_template_async(api, template_path):
|
||||
templates = Templates(directory=template_path.dirpath(), enable_async=True)
|
||||
|
||||
@api.route("/{var}/async")
|
||||
async def view_async(req, resp, var):
|
||||
resp.html = await templates.render_async(template_path.basename, var=var)
|
||||
|
||||
r = api.requests.get("/test/async")
|
||||
assert r.text == "test"
|
||||
|
||||
|
||||
def test_file_uploads(api):
|
||||
@api.route("/")
|
||||
async def upload(req, resp):
|
||||
@@ -582,7 +712,7 @@ def test_before_websockets(api):
|
||||
await ws.send_json(payload)
|
||||
await ws.close()
|
||||
|
||||
@api.route(before_request=True, websocket=True)
|
||||
@api.before_request(websocket=True)
|
||||
async def before_request(ws):
|
||||
await ws.accept()
|
||||
await ws.send_json({"before": "request"})
|
||||
@@ -643,6 +773,10 @@ def test_before_response(api, session):
|
||||
def get(req, resp):
|
||||
resp.media = req.session
|
||||
|
||||
@api.route(before_request=True)
|
||||
async def async_before_request(req, resp):
|
||||
resp.headers["x-pizza"] = "1"
|
||||
|
||||
@api.route(before_request=True)
|
||||
def before_request(req, resp):
|
||||
resp.headers["x-pizza"] = "1"
|
||||
@@ -651,8 +785,12 @@ def test_before_response(api, session):
|
||||
assert "x-pizza" in r.headers
|
||||
|
||||
|
||||
def test_allowed_hosts():
|
||||
api = responder.API(allowed_hosts=[";", "tenant.;"])
|
||||
@pytest.mark.parametrize("enable_hsts", [True, False])
|
||||
@pytest.mark.parametrize("cors", [True, False])
|
||||
def test_allowed_hosts(enable_hsts, cors):
|
||||
api = responder.API(
|
||||
allowed_hosts=[";", "tenant.;"], enable_hsts=enable_hsts, cors=cors
|
||||
)
|
||||
|
||||
@api.route("/")
|
||||
def get(req, resp):
|
||||
@@ -716,14 +854,15 @@ def create_asset(static_dir, name=None, parent_dir=None):
|
||||
return asset
|
||||
|
||||
|
||||
def test_staticfiles(tmpdir):
|
||||
@pytest.mark.parametrize("static_route", [None, "/static", "/custom/static/route"])
|
||||
def test_staticfiles(tmpdir, static_route):
|
||||
static_dir = tmpdir.mkdir("static")
|
||||
|
||||
asset1 = create_asset(static_dir)
|
||||
parent_dir = "css"
|
||||
asset2 = create_asset(static_dir, name="asset2", parent_dir=parent_dir)
|
||||
|
||||
api = responder.API(static_dir=str(static_dir))
|
||||
api = responder.API(static_dir=str(static_dir), static_route=static_route)
|
||||
session = api.session()
|
||||
|
||||
static_route = api.static_route
|
||||
@@ -747,30 +886,6 @@ def test_staticfiles(tmpdir):
|
||||
assert r.status_code == api.status_codes.HTTP_404
|
||||
|
||||
|
||||
def test_staticfiles_custom_route(tmpdir):
|
||||
static_dir = tmpdir.mkdir("static")
|
||||
static_route = "/custom/static/route"
|
||||
|
||||
asset = create_asset(static_dir)
|
||||
|
||||
api = responder.API(static_dir=str(static_dir), static_route=static_route)
|
||||
session = api.session()
|
||||
|
||||
static_route = api.static_route
|
||||
|
||||
# ok
|
||||
r = session.get(f"{static_route}/{asset.basename}")
|
||||
assert r.status_code == api.status_codes.HTTP_200
|
||||
|
||||
# Asset not found
|
||||
r = session.get(f"{static_route}/not_found.css")
|
||||
assert r.status_code == api.status_codes.HTTP_404
|
||||
|
||||
# Not found on dir listing
|
||||
r = session.get(f"{static_route}")
|
||||
assert r.status_code == api.status_codes.HTTP_404
|
||||
|
||||
|
||||
def test_staticfiles_none_dir(tmpdir):
|
||||
api = responder.API(static_dir=None)
|
||||
session = api.session()
|
||||
@@ -794,25 +909,6 @@ def test_staticfiles_none_dir(tmpdir):
|
||||
api.add_route("/spa", static=True)
|
||||
|
||||
|
||||
def test_staticfiles_none_dir_route(tmpdir):
|
||||
api = responder.API(static_dir=None, static_route=None)
|
||||
session = api.session()
|
||||
|
||||
static_dir = tmpdir.mkdir("static")
|
||||
|
||||
asset = create_asset(static_dir)
|
||||
|
||||
static_route = api.static_route
|
||||
|
||||
# ok
|
||||
r = session.get(f"{static_route}/{asset.basename}")
|
||||
assert r.status_code == api.status_codes.HTTP_404
|
||||
|
||||
# dir listing
|
||||
r = session.get(f"{static_route}")
|
||||
assert r.status_code == api.status_codes.HTTP_404
|
||||
|
||||
|
||||
def test_response_html_property(api):
|
||||
@api.route("/")
|
||||
def view(req, resp):
|
||||
@@ -904,3 +1000,21 @@ def test_empty_req_text(api):
|
||||
resp.text = "{}_{}".format(req.state.test2, req.state.test1)
|
||||
|
||||
assert api.requests.get(url("/")).text == "Foo_42"
|
||||
|
||||
|
||||
def test_path_matches_route(api):
|
||||
@api.route("/hello")
|
||||
def home(req, resp):
|
||||
resp.text = "hello world!"
|
||||
|
||||
route = api.path_matches_route({"type": "http", "path": "/hello"})
|
||||
assert route.endpoint_name == "home"
|
||||
|
||||
assert not api.path_matches_route({"type": "http", "path": "/foo"})
|
||||
|
||||
|
||||
def test_route_without_endpoint(api):
|
||||
# test that a route without endpoint gets a default static response
|
||||
api.add_route("/")
|
||||
route = api.router.routes[0]
|
||||
assert route.endpoint_name == "_static_response"
|
||||
|
||||
@@ -1,159 +0,0 @@
|
||||
import pytest
|
||||
from responder import routes
|
||||
|
||||
|
||||
def setup_function(function):
|
||||
routes.Route.incoming_matches.cache_clear()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"route, expected",
|
||||
[
|
||||
pytest.param("/", False, id="home path without params"),
|
||||
pytest.param("/test_path", False, id="sub path without params"),
|
||||
pytest.param("/{test_path}", True, id="path with params"),
|
||||
],
|
||||
)
|
||||
def test_parameter(route, expected):
|
||||
r = routes.Route(route, "test_endpoint")
|
||||
assert r.has_parameters is expected
|
||||
|
||||
|
||||
def test_url():
|
||||
r = routes.Route("/{my_path}", "test_endpoint")
|
||||
url = r.url(my_path="path")
|
||||
assert url == "/path"
|
||||
|
||||
|
||||
def test_equal():
|
||||
r = routes.Route("/{path_param}", "test_endpoint")
|
||||
r2 = routes.Route("/{path_param}", "test_endpoint")
|
||||
r3 = routes.Route("/test_path", "test_endpoint")
|
||||
|
||||
assert r == r2
|
||||
assert r != r3
|
||||
|
||||
|
||||
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"}
|
||||
|
||||
# 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
|
||||
r = routes.Route("/hello", "test_endpoint")
|
||||
assert r.incoming_matches("/hello") == {}
|
||||
assert r.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():
|
||||
r = routes.Route("/concrete_path", "test_endpoint")
|
||||
assert r.incoming_matches("hello") == {}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"route, match, expected",
|
||||
[
|
||||
pytest.param(
|
||||
"/{path_param}",
|
||||
"/{path_param}",
|
||||
True,
|
||||
id="with both parametrized path match",
|
||||
),
|
||||
pytest.param(
|
||||
"/concrete", "/concrete", True, id="with both concrete path match"
|
||||
),
|
||||
pytest.param("/concrete", "/no_match", False, id="with no match"),
|
||||
],
|
||||
)
|
||||
def test_does_match_with_route(route, match, expected):
|
||||
r = routes.Route(route, "test_endpoint")
|
||||
assert r.does_match(match) == expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path_param, expected_weight",
|
||||
[
|
||||
pytest.param("/{greetings}", (True, True, -1), id="with one param"),
|
||||
pytest.param(
|
||||
"/{greetings}.{name}",
|
||||
(True, True, -2),
|
||||
id="with 2 params and dot in the middle",
|
||||
),
|
||||
pytest.param(
|
||||
"/{greetings}/{name}", (True, True, -2), id="with 2 params and subpath"
|
||||
),
|
||||
pytest.param(
|
||||
"/{greetings}/{name}/{hello}",
|
||||
(True, True, -3),
|
||||
id="with 3 params and subpath",
|
||||
),
|
||||
pytest.param(
|
||||
"/{greetings}_{name}", (True, True, -2), id="with 2 params and underscore"
|
||||
),
|
||||
pytest.param("/{greetings}/test", (True, False, -1), id="with one param"),
|
||||
pytest.param(
|
||||
"/{greetings}.{name}/test",
|
||||
(True, False, -2),
|
||||
id="with 2 params and dot in the middle",
|
||||
),
|
||||
pytest.param(
|
||||
"/{greetings}/{name}/test",
|
||||
(True, False, -2),
|
||||
id="with 2 params and subpath",
|
||||
),
|
||||
pytest.param(
|
||||
"/{greetings}/{name}/{hello}/test",
|
||||
(True, False, -3),
|
||||
id="with 3 params and subpath",
|
||||
),
|
||||
pytest.param(
|
||||
"/{greetings}_{name}/test",
|
||||
(True, False, -2),
|
||||
id="with 2 params and underscore",
|
||||
),
|
||||
pytest.param("/hello", (False, False, 0), id="without params"),
|
||||
],
|
||||
)
|
||||
def test_weight(path_param, expected_weight):
|
||||
r = routes.Route(path_param, "test_endpoint")
|
||||
assert r._weight() == expected_weight
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"route, path, expected_result",
|
||||
[
|
||||
pytest.param("/{greetings:str}", "/hello", {"greetings": "hello"}),
|
||||
pytest.param(
|
||||
"/{greetings:str}/{who}",
|
||||
"/hello/Laidia",
|
||||
{"greetings": "hello", "who": "Laidia"},
|
||||
),
|
||||
pytest.param("/{birth_date:int}", "/1937", {"birth_date": 1937}),
|
||||
pytest.param(
|
||||
"/{name:str}/{age:int}", "/Fatna/80", {"name": "Fatna", "age": 80}
|
||||
),
|
||||
pytest.param(
|
||||
"/{x:float}/{y:float}", "/10.20/75", {"x": float(10.20), "y": float(75)}
|
||||
),
|
||||
pytest.param("/{name:str}/{age:int}", "/Fatna/eighty", {}),
|
||||
pytest.param("/{greetings:int}", "/hello", {}),
|
||||
pytest.param("/{name:float}", "/Fatna", {}),
|
||||
],
|
||||
)
|
||||
def test_custom_specifiers(route, path, expected_result):
|
||||
r = routes.Route(route, "test_endpoint")
|
||||
assert r.incoming_matches(path) == expected_result
|
||||
Reference in New Issue
Block a user