Compare commits
35 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| dc71a0b725 | |||
| 969c0b408d | |||
| 6cd596b2b9 | |||
| 5cfbe13ddf | |||
| 9d31549fe8 | |||
| 7ae8062fc5 | |||
| 03ee860ceb | |||
| 9dd9792204 | |||
| 60b4516894 | |||
| 8442a0af85 | |||
| 42d5aea168 | |||
| feb473e952 | |||
| 7dccbc7e19 | |||
| 6e5ce5b42a | |||
| 1d3ec74e09 | |||
| b3fe28f522 | |||
| bfd9f875b7 | |||
| d00d5ec391 | |||
| b68d349afe | |||
| abdf9d7b86 | |||
| a8f9bc2bc1 | |||
| fb4cec76d8 | |||
| 0aee74f03a | |||
| 0b0b366804 | |||
| 6ef0a4cb91 | |||
| e70b638271 | |||
| f99530d723 | |||
| f28cd7001a | |||
| 5c5d0bbd74 | |||
| 4c00c90c49 | |||
| 093819b249 | |||
| 1193bdcb6c | |||
| cc6f10f9bd | |||
| 4ad2f00c1c | |||
| a491f9a3fe |
@@ -0,0 +1,4 @@
|
||||
# Sphinx build info version 1
|
||||
# This file records the configuration used when building these files. When it is not found, a full rebuild will be done.
|
||||
config: 5ce83980cbbb1ff8c0566b9128c91144
|
||||
tags: 645f666f9bcd5a90fca523b33c5a78b7
|
||||
@@ -1,16 +0,0 @@
|
||||
.vscode/
|
||||
.cache
|
||||
.idea
|
||||
.coverage
|
||||
.pytest_cache
|
||||
.DS_Store
|
||||
coverage.xml
|
||||
|
||||
__pycache__
|
||||
tests/__pycache__
|
||||
|
||||
build
|
||||
responder.egg-info/
|
||||
dist/
|
||||
app.py
|
||||
app2.py
|
||||
@@ -1,12 +0,0 @@
|
||||
language: python
|
||||
python:
|
||||
- "3.6"
|
||||
|
||||
# command to install dependencies
|
||||
install:
|
||||
- "pip install pipenv --upgrade-strategy=only-if-needed"
|
||||
- "pipenv install --dev"
|
||||
|
||||
# command to run the dependencies
|
||||
script:
|
||||
- "pytest"
|
||||
@@ -1,23 +0,0 @@
|
||||
# v0.0.7
|
||||
- Immutable Request object.
|
||||
|
||||
# v0.0.6:
|
||||
- Ability to mount WSGI apps.
|
||||
- Supply content-type when serving up the schema.
|
||||
|
||||
# v0.0.5:
|
||||
- OpenAPI Schema support.
|
||||
- Safe load/dump yaml.
|
||||
|
||||
# v0.0.4:
|
||||
- Asyncronous support for data uploads.
|
||||
- Bug fixes.
|
||||
|
||||
# v0.0.3:
|
||||
- Bug fixes.
|
||||
|
||||
# v0.0.2
|
||||
- Switch to ASGI/Starlette.
|
||||
|
||||
# v0.0.1
|
||||
- Conception!
|
||||
@@ -1,13 +0,0 @@
|
||||
Copyright 2018 Kenneth Reitz
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
@@ -1,22 +0,0 @@
|
||||
[[source]]
|
||||
url = "https://pypi.org/simple"
|
||||
verify_ssl = true
|
||||
name = "pypi"
|
||||
|
||||
[packages]
|
||||
responder = {editable = true, path = "."}
|
||||
|
||||
[dev-packages]
|
||||
pytest = "*"
|
||||
"flake8" = "*"
|
||||
black = "*"
|
||||
twine = "*"
|
||||
flask = "*"
|
||||
sphinx = "*"
|
||||
marshmallow = "*"
|
||||
|
||||
[requires]
|
||||
python_version = "3.7"
|
||||
|
||||
[pipenv]
|
||||
allow_prereleases = true
|
||||
@@ -1,648 +0,0 @@
|
||||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "9b959d9507c521f6088646507633207db03afec6ac31aeab07adf0d737dbb45b"
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {
|
||||
"python_version": "3.7"
|
||||
},
|
||||
"sources": [
|
||||
{
|
||||
"name": "pypi",
|
||||
"url": "https://pypi.org/simple",
|
||||
"verify_ssl": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"default": {
|
||||
"aiofiles": {
|
||||
"hashes": [
|
||||
"sha256:021ea0ba314a86027c166ecc4b4c07f2d40fc0f4b3a950d1868a0f2571c2bbee",
|
||||
"sha256:1e644c2573f953664368de28d2aa4c89dfd64550429d0c27c4680ccd3aa4985d"
|
||||
],
|
||||
"version": "==0.4.0"
|
||||
},
|
||||
"aniso8601": {
|
||||
"hashes": [
|
||||
"sha256:7849749cf00ae0680ad2bdfe4419c7a662bef19c03691a19e008c8b9a5267802",
|
||||
"sha256:94f90871fcd314a458a3d4eca1c84448efbd200e86f55fe4c733c7a40149ef50"
|
||||
],
|
||||
"version": "==3.0.2"
|
||||
},
|
||||
"apispec": {
|
||||
"hashes": [
|
||||
"sha256:c2e6ac6471aaf7c6ec6d12714821898910c6b3c87c189de9a2e3754786b86ada",
|
||||
"sha256:fa7dfa8a292bae9b1e70c44a50bf61901805821726c5b804568c9f2501f57ebb"
|
||||
],
|
||||
"version": "==1.0.0b3"
|
||||
},
|
||||
"asgiref": {
|
||||
"hashes": [
|
||||
"sha256:9b05dcd41a6a89ca8c6e7f7e4089c3f3e76b5af60aebb81ae6d455ad81989c97",
|
||||
"sha256:b21dc4c43d7aba5a844f4c48b8f49d56277bc34937fd9f9cb93ec97fde7e3082"
|
||||
],
|
||||
"version": "==2.3.2"
|
||||
},
|
||||
"async-timeout": {
|
||||
"hashes": [
|
||||
"sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f",
|
||||
"sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3"
|
||||
],
|
||||
"version": "==3.0.1"
|
||||
},
|
||||
"certifi": {
|
||||
"hashes": [
|
||||
"sha256:339dc09518b07e2fa7eda5450740925974815557727d6bd35d319c1524a04a4c",
|
||||
"sha256:6d58c986d22b038c8c0df30d639f23a3e6d172a05c3583e766f4c0b785c0986a"
|
||||
],
|
||||
"version": "==2018.10.15"
|
||||
},
|
||||
"chardet": {
|
||||
"hashes": [
|
||||
"sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae",
|
||||
"sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"
|
||||
],
|
||||
"version": "==3.0.4"
|
||||
},
|
||||
"click": {
|
||||
"hashes": [
|
||||
"sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13",
|
||||
"sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7"
|
||||
],
|
||||
"version": "==7.0"
|
||||
},
|
||||
"graphene": {
|
||||
"hashes": [
|
||||
"sha256:b8ec446d17fa68721636eaad3d6adc1a378cb6323e219814c8f98c9928fc9642",
|
||||
"sha256:faa26573b598b22ffd274e2fd7a4c52efa405dcca96e01a62239482246248aa3"
|
||||
],
|
||||
"version": "==2.1.3"
|
||||
},
|
||||
"graphql-core": {
|
||||
"hashes": [
|
||||
"sha256:889e869be5574d02af77baf1f30b5db9ca2959f1c9f5be7b2863ead5a3ec6181",
|
||||
"sha256:9462e22e32c7f03b667373ec0a84d95fba10e8ce2ead08f29fbddc63b671b0c1"
|
||||
],
|
||||
"version": "==2.1"
|
||||
},
|
||||
"graphql-relay": {
|
||||
"hashes": [
|
||||
"sha256:2716b7245d97091af21abf096fabafac576905096d21ba7118fba722596f65db"
|
||||
],
|
||||
"version": "==0.4.5"
|
||||
},
|
||||
"graphql-server-core": {
|
||||
"hashes": [
|
||||
"sha256:e5f82add4b3d5580aa1f1e7d9f00e944ad3abe1b65eb337e611d6a77cc20f231"
|
||||
],
|
||||
"version": "==1.1.1"
|
||||
},
|
||||
"h11": {
|
||||
"hashes": [
|
||||
"sha256:acca6a44cb52a32ab442b1779adf0875c443c689e9e028f8d831a3769f9c5208",
|
||||
"sha256:f2b1ca39bfed357d1f19ac732913d5f9faa54a5062eca7d2ec3a916cfb7ae4c7"
|
||||
],
|
||||
"version": "==0.8.1"
|
||||
},
|
||||
"idna": {
|
||||
"hashes": [
|
||||
"sha256:156a6814fb5ac1fc6850fb002e0852d56c0c8d2531923a51032d1b70760e186e",
|
||||
"sha256:684a38a6f903c1d71d6d5fac066b58d7768af4de2b832e426ec79c30daa94a16"
|
||||
],
|
||||
"version": "==2.7"
|
||||
},
|
||||
"jinja2": {
|
||||
"hashes": [
|
||||
"sha256:74c935a1b8bb9a3947c50a54766a969d4846290e1e788ea44c1392163723c3bd",
|
||||
"sha256:f84be1bb0040caca4cea721fcbbbbd61f9be9464ca236387158b0feea01914a4"
|
||||
],
|
||||
"version": "==2.10"
|
||||
},
|
||||
"markupsafe": {
|
||||
"hashes": [
|
||||
"sha256:a6be69091dac236ea9c6bc7d012beab42010fa914c459791d627dad4910eb665"
|
||||
],
|
||||
"version": "==1.0"
|
||||
},
|
||||
"marshmallow": {
|
||||
"hashes": [
|
||||
"sha256:82b201ad767eb54de371c08cb1db6ca4ad2a728fa41b831e3781bf944815eb38",
|
||||
"sha256:c250f37ac0e249a8287394a60d91f6240b674642ad999e66cd09463dbccd1d4f"
|
||||
],
|
||||
"version": "==3.0.0b18"
|
||||
},
|
||||
"parse": {
|
||||
"hashes": [
|
||||
"sha256:9dd6048ea212cd032a342f9f6aa2b7bc222f7407c7e37bdc2777fecd36897437"
|
||||
],
|
||||
"version": "==1.9.0"
|
||||
},
|
||||
"promise": {
|
||||
"hashes": [
|
||||
"sha256:2ebbfc10b7abf6354403ed785fe4f04b9dfd421eb1a474ac8d187022228332af",
|
||||
"sha256:348f5f6c3edd4fd47c9cd65aed03ac1b31136d375aa63871a57d3e444c85655c"
|
||||
],
|
||||
"version": "==2.2.1"
|
||||
},
|
||||
"python-multipart": {
|
||||
"hashes": [
|
||||
"sha256:f7bb5f611fc600d15fa47b3974c8aa16e93724513b49b5f95c81e6624c83fa43"
|
||||
],
|
||||
"version": "==0.0.5"
|
||||
},
|
||||
"pyyaml": {
|
||||
"hashes": [
|
||||
"sha256:254bf6fda2b7c651837acb2c718e213df29d531eebf00edb54743d10bcb694eb",
|
||||
"sha256:3108529b78577327d15eec243f0ff348a0640b0c3478d67ad7f5648f93bac3e2",
|
||||
"sha256:3c17fb92c8ba2f525e4b5f7941d850e7a48c3a59b32d331e2502a3cdc6648e76",
|
||||
"sha256:8d6d96001aa7f0a6a4a95e8143225b5d06e41b1131044913fecb8f85a125714b",
|
||||
"sha256:c8a88edd93ee29ede719080b2be6cb2333dfee1dccba213b422a9c8e97f2967b"
|
||||
],
|
||||
"version": "==4.2b4"
|
||||
},
|
||||
"requests": {
|
||||
"hashes": [
|
||||
"sha256:63b52e3c866428a224f97cab011de738c36aec0185aa91cfacd418b5d58911d1",
|
||||
"sha256:ec22d826a36ed72a7358ff3fe56cbd4ba69dd7a6718ffd450ff0e9df7a47ce6a"
|
||||
],
|
||||
"version": "==2.19.1"
|
||||
},
|
||||
"responder": {
|
||||
"editable": true,
|
||||
"path": "."
|
||||
},
|
||||
"rfc3986": {
|
||||
"hashes": [
|
||||
"sha256:632b8fcd2ac37f24334316227f909be4f9d0738cbf409404cff6fa5f69a24093",
|
||||
"sha256:8458571c4c57e1cf23593ad860bb601b6a604df6217f829c2bc70dc4b5af941b"
|
||||
],
|
||||
"version": "==1.1.0"
|
||||
},
|
||||
"rx": {
|
||||
"hashes": [
|
||||
"sha256:13a1d8d9e252625c173dc795471e614eadfe1cf40ffc684e08b8fff0d9748c23",
|
||||
"sha256:7357592bc7e881a95e0c2013b73326f704953301ab551fbc8133a6fadab84105"
|
||||
],
|
||||
"version": "==1.6.1"
|
||||
},
|
||||
"six": {
|
||||
"hashes": [
|
||||
"sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9",
|
||||
"sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb"
|
||||
],
|
||||
"version": "==1.11.0"
|
||||
},
|
||||
"starlette": {
|
||||
"hashes": [
|
||||
"sha256:2c7ec085440fce7146a9be2b6d53b7110c3866ce6fa03d901efdc1fbe97e0f36"
|
||||
],
|
||||
"version": "==0.4.2"
|
||||
},
|
||||
"urllib3": {
|
||||
"hashes": [
|
||||
"sha256:a68ac5e15e76e7e5dd2b8f94007233e01effe3e50e8daddf69acfd81cb686baf",
|
||||
"sha256:b5725a0bd4ba422ab0e66e89e030c806576753ea3ee08554382c14e685d117b5"
|
||||
],
|
||||
"version": "==1.23"
|
||||
},
|
||||
"uvicorn": {
|
||||
"hashes": [
|
||||
"sha256:8de03999a936d8704f07cc3b1d3a3edb6922a068b64d84b4f5e49604c8b70a11"
|
||||
],
|
||||
"version": "==0.3.12"
|
||||
},
|
||||
"websockets": {
|
||||
"hashes": [
|
||||
"sha256:0e2f7d6567838369af074f0ef4d0b802d19fa1fee135d864acc656ceefa33136",
|
||||
"sha256:2a16dac282b2fdae75178d0ed3d5b9bc3258dabfae50196cbb30578d84b6f6a6",
|
||||
"sha256:5a1fa6072405648cb5b3688e9ed3b94be683ce4a4e5723e6f5d34859dee495c1",
|
||||
"sha256:5c1f55a1274df9d6a37553fef8cff2958515438c58920897675c9bc70f5a0538",
|
||||
"sha256:669d1e46f165e0ad152ed8197f7edead22854a6c90419f544e0f234cc9dac6c4",
|
||||
"sha256:695e34c4dbea18d09ab2c258994a8bf6a09564e762655408241f6a14592d2908",
|
||||
"sha256:6b2e03d69afa8d20253455e67b64de1a82ff8612db105113cccec35d3f8429f0",
|
||||
"sha256:79ca7cdda7ad4e3663ea3c43bfa8637fc5d5604c7737f19a8964781abbd1148d",
|
||||
"sha256:7fd2dd9a856f72e6ed06f82facfce01d119b88457cd4b47b7ae501e8e11eba9c",
|
||||
"sha256:82c0354ac39379d836719a77ee360ef865377aa6fdead87909d50248d0f05f4d",
|
||||
"sha256:8f3b956d11c5b301206382726210dc1d3bee1a9ccf7aadf895aaf31f71c3716c",
|
||||
"sha256:91ec98640220ae05b34b79ee88abf27f97ef7c61cf525eec57ea8fcea9f7dddb",
|
||||
"sha256:952be9540d83dba815569d5cb5f31708801e0bbfc3a8c5aef1890b57ed7e58bf",
|
||||
"sha256:99ac266af38ba1b1fe13975aea01ac0e14bb5f3a3200d2c69f05385768b8568e",
|
||||
"sha256:9fa122e7adb24232247f8a89f2d9070bf64b7869daf93ac5e19546b409e47e96",
|
||||
"sha256:a0873eadc4b8ca93e2e848d490809e0123eea154aa44ecd0109c4d0171869584",
|
||||
"sha256:cb998bd4d93af46b8b49ecf5a72c0a98e5cc6d57fdca6527ba78ad89d6606484",
|
||||
"sha256:e02e57346f6a68523e3c43bbdf35dde5c440318d1f827208ae455f6a2ace446d",
|
||||
"sha256:e79a5a896bcee7fff24a788d72e5c69f13e61369d055f28113e71945a7eb1559",
|
||||
"sha256:ee55eb6bcf23ecc975e6b47c127c201b913598f38b6a300075f84eeef2d3baff",
|
||||
"sha256:f1414e6cbcea8d22843e7eafdfdfae3dd1aba41d1945f6ca66e4806c07c4f454"
|
||||
],
|
||||
"version": "==6.0"
|
||||
}
|
||||
},
|
||||
"develop": {
|
||||
"alabaster": {
|
||||
"hashes": [
|
||||
"sha256:446438bdcca0e05bd45ea2de1668c1d9b032e1a9154c2c259092d77031ddd359",
|
||||
"sha256:a661d72d58e6ea8a57f7a86e37d86716863ee5e92788398526d58b26a4e4dc02"
|
||||
],
|
||||
"version": "==0.7.12"
|
||||
},
|
||||
"appdirs": {
|
||||
"hashes": [
|
||||
"sha256:9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92",
|
||||
"sha256:d8b24664561d0d34ddfaec54636d502d7cea6e29c3eaf68f3df6180863e2166e"
|
||||
],
|
||||
"version": "==1.4.3"
|
||||
},
|
||||
"atomicwrites": {
|
||||
"hashes": [
|
||||
"sha256:0312ad34fcad8fac3704d441f7b317e50af620823353ec657a53e981f92920c0",
|
||||
"sha256:ec9ae8adaae229e4f8446952d204a3e4b5fdd2d099f9be3aaf556120135fb3ee"
|
||||
],
|
||||
"version": "==1.2.1"
|
||||
},
|
||||
"attrs": {
|
||||
"hashes": [
|
||||
"sha256:10cbf6e27dbce8c30807caf056c8eb50917e0eaafe86347671b57254006c3e69",
|
||||
"sha256:ca4be454458f9dec299268d472aaa5a11f67a4ff70093396e1ceae9c76cf4bbb"
|
||||
],
|
||||
"version": "==18.2.0"
|
||||
},
|
||||
"babel": {
|
||||
"hashes": [
|
||||
"sha256:6778d85147d5d85345c14a26aada5e478ab04e39b078b0745ee6870c2b5cf669",
|
||||
"sha256:8cba50f48c529ca3fa18cf81fa9403be176d374ac4d60738b839122dfaaa3d23"
|
||||
],
|
||||
"version": "==2.6.0"
|
||||
},
|
||||
"black": {
|
||||
"hashes": [
|
||||
"sha256:817243426042db1d36617910df579a54f1afd659adb96fc5032fcf4b36209739",
|
||||
"sha256:e030a9a28f542debc08acceb273f228ac422798e5215ba2a791a6ddeaaca22a5"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==18.9b0"
|
||||
},
|
||||
"bleach": {
|
||||
"hashes": [
|
||||
"sha256:48d39675b80a75f6d1c3bdbffec791cf0bbbab665cf01e20da701c77de278718",
|
||||
"sha256:73d26f018af5d5adcdabf5c1c974add4361a9c76af215fe32fdec8a6fc5fb9b9"
|
||||
],
|
||||
"version": "==3.0.2"
|
||||
},
|
||||
"certifi": {
|
||||
"hashes": [
|
||||
"sha256:339dc09518b07e2fa7eda5450740925974815557727d6bd35d319c1524a04a4c",
|
||||
"sha256:6d58c986d22b038c8c0df30d639f23a3e6d172a05c3583e766f4c0b785c0986a"
|
||||
],
|
||||
"version": "==2018.10.15"
|
||||
},
|
||||
"cffi": {
|
||||
"hashes": [
|
||||
"sha256:151b7eefd035c56b2b2e1eb9963c90c6302dc15fbd8c1c0a83a163ff2c7d7743",
|
||||
"sha256:1553d1e99f035ace1c0544050622b7bc963374a00c467edafac50ad7bd276aef",
|
||||
"sha256:1b0493c091a1898f1136e3f4f991a784437fac3673780ff9de3bcf46c80b6b50",
|
||||
"sha256:2ba8a45822b7aee805ab49abfe7eec16b90587f7f26df20c71dd89e45a97076f",
|
||||
"sha256:3bb6bd7266598f318063e584378b8e27c67de998a43362e8fce664c54ee52d30",
|
||||
"sha256:3c85641778460581c42924384f5e68076d724ceac0f267d66c757f7535069c93",
|
||||
"sha256:3eb6434197633b7748cea30bf0ba9f66727cdce45117a712b29a443943733257",
|
||||
"sha256:495c5c2d43bf6cebe0178eb3e88f9c4aa48d8934aa6e3cddb865c058da76756b",
|
||||
"sha256:4c91af6e967c2015729d3e69c2e51d92f9898c330d6a851bf8f121236f3defd3",
|
||||
"sha256:57b2533356cb2d8fac1555815929f7f5f14d68ac77b085d2326b571310f34f6e",
|
||||
"sha256:770f3782b31f50b68627e22f91cb182c48c47c02eb405fd689472aa7b7aa16dc",
|
||||
"sha256:79f9b6f7c46ae1f8ded75f68cf8ad50e5729ed4d590c74840471fc2823457d04",
|
||||
"sha256:7a33145e04d44ce95bcd71e522b478d282ad0eafaf34fe1ec5bbd73e662f22b6",
|
||||
"sha256:857959354ae3a6fa3da6651b966d13b0a8bed6bbc87a0de7b38a549db1d2a359",
|
||||
"sha256:87f37fe5130574ff76c17cab61e7d2538a16f843bb7bca8ebbc4b12de3078596",
|
||||
"sha256:95d5251e4b5ca00061f9d9f3d6fe537247e145a8524ae9fd30a2f8fbce993b5b",
|
||||
"sha256:9d1d3e63a4afdc29bd76ce6aa9d58c771cd1599fbba8cf5057e7860b203710dd",
|
||||
"sha256:a36c5c154f9d42ec176e6e620cb0dd275744aa1d804786a71ac37dc3661a5e95",
|
||||
"sha256:a6a5cb8809091ec9ac03edde9304b3ad82ad4466333432b16d78ef40e0cce0d5",
|
||||
"sha256:ae5e35a2c189d397b91034642cb0eab0e346f776ec2eb44a49a459e6615d6e2e",
|
||||
"sha256:b0f7d4a3df8f06cf49f9f121bead236e328074de6449866515cea4907bbc63d6",
|
||||
"sha256:b75110fb114fa366b29a027d0c9be3709579602ae111ff61674d28c93606acca",
|
||||
"sha256:ba5e697569f84b13640c9e193170e89c13c6244c24400fc57e88724ef610cd31",
|
||||
"sha256:be2a9b390f77fd7676d80bc3cdc4f8edb940d8c198ed2d8c0be1319018c778e1",
|
||||
"sha256:ca1bd81f40adc59011f58159e4aa6445fc585a32bb8ac9badf7a2c1aa23822f2",
|
||||
"sha256:d5d8555d9bfc3f02385c1c37e9f998e2011f0db4f90e250e5bc0c0a85a813085",
|
||||
"sha256:e55e22ac0a30023426564b1059b035973ec82186ddddbac867078435801c7801",
|
||||
"sha256:e90f17980e6ab0f3c2f3730e56d1fe9bcba1891eeea58966e89d352492cc74f4",
|
||||
"sha256:ecbb7b01409e9b782df5ded849c178a0aa7c906cf8c5a67368047daab282b184",
|
||||
"sha256:ed01918d545a38998bfa5902c7c00e0fee90e957ce036a4000a88e3fe2264917",
|
||||
"sha256:edabd457cd23a02965166026fd9bfd196f4324fe6032e866d0f3bd0301cd486f",
|
||||
"sha256:fdf1c1dc5bafc32bc5d08b054f94d659422b05aba244d6be4ddc1c72d9aa70fb"
|
||||
],
|
||||
"version": "==1.11.5"
|
||||
},
|
||||
"chardet": {
|
||||
"hashes": [
|
||||
"sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae",
|
||||
"sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"
|
||||
],
|
||||
"version": "==3.0.4"
|
||||
},
|
||||
"click": {
|
||||
"hashes": [
|
||||
"sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13",
|
||||
"sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7"
|
||||
],
|
||||
"version": "==7.0"
|
||||
},
|
||||
"cmarkgfm": {
|
||||
"hashes": [
|
||||
"sha256:0186dccca79483e3405217993b83b914ba4559fe9a8396efc4eea56561b74061",
|
||||
"sha256:1a625afc6f62da428df96ec325dc30866cc5781520cbd904ff4ec44cf018171c",
|
||||
"sha256:207b7673ff4e177374c572feeae0e4ef33be620ec9171c08fd22e2b796e03e3d",
|
||||
"sha256:275905bb371a99285c74931700db3f0c078e7603bed383e8cf1a09f3ee05a3de",
|
||||
"sha256:50098f1c4950722521f0671e54139e0edc1837d63c990cf0f3d2c49607bb51a2",
|
||||
"sha256:50ed116d0b60a07df0dc7b180c28569064b9d37d1578d4c9021cff04d725cb63",
|
||||
"sha256:61a72def110eed903cd1848245897bcb80d295cd9d13944d4f9f30cba5b76655",
|
||||
"sha256:64186fb75d973a06df0e6ea12879533b71f6e7ba1ab01ffee7fc3e7534758889",
|
||||
"sha256:665303d34d7f14f10d7b0651082f25ebf7107f29ef3d699490cac16cdc0fc8ce",
|
||||
"sha256:70b18f843aec58e4e64aadce48a897fe7c50426718b7753aaee399e72df64190",
|
||||
"sha256:761ee7b04d1caee2931344ac6bfebf37102ffb203b136b676b0a71a3f0ea3c87",
|
||||
"sha256:811527e9b7280b136734ed6cb6845e5fbccaeaa132ddf45f0246cbe544016957",
|
||||
"sha256:987b0e157f70c72a84f3c2f9ef2d7ab0f26c08f2bf326c12c087ff9eebcb3ff5",
|
||||
"sha256:9fc6a2183d0a9b0974ec7cdcdad42bd78a3be674cc3e65f87dd694419b3b0ab7",
|
||||
"sha256:a3d17ee4ae739fe16f7501a52255c2e287ac817cfd88565b9859f70520afffea",
|
||||
"sha256:ba5b5488719c0f2ced0aa1986376f7baff1a1653a8eb5fdfcf3f84c7ce46ef8d",
|
||||
"sha256:c573ea89dd95d41b6d8cf36799c34b6d5b1eac4aed0212dee0f0a11fb7b01e8f",
|
||||
"sha256:c5f1b9e8592d2c448c44e6bc0d91224b16ea5f8293908b1561de1f6d2d0658b1",
|
||||
"sha256:cbe581456357d8f0674d6a590b1aaf46c11d01dd0a23af147a51a798c3818034",
|
||||
"sha256:cf219bec69e601fe27e3974b7307d2f06082ab385d42752738ad2eb630a47d65",
|
||||
"sha256:cf5014eb214d814a83a7a47407272d5db10b719dbeaf4d3cfe5969309d0fcf4b",
|
||||
"sha256:d08bad67fa18f7e8ff738c090628ee0cbf0505d74a991c848d6d04abfe67b697",
|
||||
"sha256:d6f716d7b1182bf35862b5065112f933f43dd1aa4f8097c9bcfb246f71528a34",
|
||||
"sha256:e08e479102627641c7cb4ece421c6ed4124820b1758765db32201136762282d9",
|
||||
"sha256:e20ac21418af0298437d29599f7851915497ce9f2866bc8e86b084d8911ee061",
|
||||
"sha256:e25f53c37e319241b9a412382140dffac98ca756ba8f360ac7ab5e30cad9670a",
|
||||
"sha256:e8932bddf159064f04e946fbb64693753488de21586f20e840b3be51745c8c09",
|
||||
"sha256:f20900f16377f2109783ae9348d34bc80530808439591c3d3df73d5c7ef1a00c"
|
||||
],
|
||||
"version": "==0.4.2"
|
||||
},
|
||||
"colorama": {
|
||||
"hashes": [
|
||||
"sha256:a3d89af5db9e9806a779a50296b5fdb466e281147c2c235e8225ecc6dbf7bbf3",
|
||||
"sha256:c9b54bebe91a6a803e0772c8561d53f2926bfeb17cd141fbabcb08424086595c"
|
||||
],
|
||||
"markers": "sys_platform == 'win32'",
|
||||
"version": "==0.4.0"
|
||||
},
|
||||
"docutils": {
|
||||
"hashes": [
|
||||
"sha256:02aec4bd92ab067f6ff27a38a38a41173bf01bed8f89157768c1573f53e474a6",
|
||||
"sha256:51e64ef2ebfb29cae1faa133b3710143496eca21c530f3f71424d77687764274",
|
||||
"sha256:7a4bd47eaf6596e1295ecb11361139febe29b084a87bf005bf899f9a42edc3c6"
|
||||
],
|
||||
"version": "==0.14"
|
||||
},
|
||||
"flake8": {
|
||||
"hashes": [
|
||||
"sha256:7253265f7abd8b313e3892944044a365e3f4ac3fcdcfb4298f55ee9ddf188ba0",
|
||||
"sha256:c7841163e2b576d435799169b78703ad6ac1bbb0f199994fc05f700b2a90ea37"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==3.5.0"
|
||||
},
|
||||
"flask": {
|
||||
"hashes": [
|
||||
"sha256:2271c0070dbcb5275fad4a82e29f23ab92682dc45f9dfbc22c02ba9b9322ce48",
|
||||
"sha256:a080b744b7e345ccfcbc77954861cb05b3c63786e93f2b3875e0913d44b43f05"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.0.2"
|
||||
},
|
||||
"future": {
|
||||
"hashes": [
|
||||
"sha256:e39ced1ab767b5936646cedba8bcce582398233d6a627067d4c6a454c90cfedb"
|
||||
],
|
||||
"version": "==0.16.0"
|
||||
},
|
||||
"idna": {
|
||||
"hashes": [
|
||||
"sha256:156a6814fb5ac1fc6850fb002e0852d56c0c8d2531923a51032d1b70760e186e",
|
||||
"sha256:684a38a6f903c1d71d6d5fac066b58d7768af4de2b832e426ec79c30daa94a16"
|
||||
],
|
||||
"version": "==2.7"
|
||||
},
|
||||
"imagesize": {
|
||||
"hashes": [
|
||||
"sha256:3f349de3eb99145973fefb7dbe38554414e5c30abd0c8e4b970a7c9d09f3a1d8",
|
||||
"sha256:f3832918bc3c66617f92e35f5d70729187676313caa60c187eb0f28b8fe5e3b5"
|
||||
],
|
||||
"version": "==1.1.0"
|
||||
},
|
||||
"itsdangerous": {
|
||||
"hashes": [
|
||||
"sha256:cbb3fcf8d3e33df861709ecaf89d9e6629cff0a217bc2848f1b41cd30d360519"
|
||||
],
|
||||
"version": "==0.24"
|
||||
},
|
||||
"jinja2": {
|
||||
"hashes": [
|
||||
"sha256:74c935a1b8bb9a3947c50a54766a969d4846290e1e788ea44c1392163723c3bd",
|
||||
"sha256:f84be1bb0040caca4cea721fcbbbbd61f9be9464ca236387158b0feea01914a4"
|
||||
],
|
||||
"version": "==2.10"
|
||||
},
|
||||
"markupsafe": {
|
||||
"hashes": [
|
||||
"sha256:a6be69091dac236ea9c6bc7d012beab42010fa914c459791d627dad4910eb665"
|
||||
],
|
||||
"version": "==1.0"
|
||||
},
|
||||
"marshmallow": {
|
||||
"hashes": [
|
||||
"sha256:82b201ad767eb54de371c08cb1db6ca4ad2a728fa41b831e3781bf944815eb38",
|
||||
"sha256:c250f37ac0e249a8287394a60d91f6240b674642ad999e66cd09463dbccd1d4f"
|
||||
],
|
||||
"version": "==3.0.0b18"
|
||||
},
|
||||
"mccabe": {
|
||||
"hashes": [
|
||||
"sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42",
|
||||
"sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"
|
||||
],
|
||||
"version": "==0.6.1"
|
||||
},
|
||||
"more-itertools": {
|
||||
"hashes": [
|
||||
"sha256:c187a73da93e7a8acc0001572aebc7e3c69daf7bf6881a2cea10650bd4420092",
|
||||
"sha256:c476b5d3a34e12d40130bc2f935028b5f636df8f372dc2c1c01dc19681b2039e",
|
||||
"sha256:fcbfeaea0be121980e15bc97b3817b5202ca73d0eae185b4550cbfce2a3ebb3d"
|
||||
],
|
||||
"version": "==4.3.0"
|
||||
},
|
||||
"packaging": {
|
||||
"hashes": [
|
||||
"sha256:0886227f54515e592aaa2e5a553332c73962917f2831f1b0f9b9f4380a4b9807",
|
||||
"sha256:f95a1e147590f204328170981833854229bb2912ac3d5f89e2a8ccd2834800c9"
|
||||
],
|
||||
"version": "==18.0"
|
||||
},
|
||||
"pkginfo": {
|
||||
"hashes": [
|
||||
"sha256:5878d542a4b3f237e359926384f1dde4e099c9f5525d236b1840cf704fa8d474",
|
||||
"sha256:a39076cb3eb34c333a0dd390b568e9e1e881c7bf2cc0aee12120636816f55aee"
|
||||
],
|
||||
"version": "==1.4.2"
|
||||
},
|
||||
"pluggy": {
|
||||
"hashes": [
|
||||
"sha256:6e3836e39f4d36ae72840833db137f7b7d35105079aee6ec4a62d9f80d594dd1",
|
||||
"sha256:95eb8364a4708392bae89035f45341871286a333f749c3141c20573d2b3876e1"
|
||||
],
|
||||
"version": "==0.7.1"
|
||||
},
|
||||
"py": {
|
||||
"hashes": [
|
||||
"sha256:bf92637198836372b520efcba9e020c330123be8ce527e535d185ed4b6f45694",
|
||||
"sha256:e76826342cefe3c3d5f7e8ee4316b80d1dd8a300781612ddbc765c17ba25a6c6"
|
||||
],
|
||||
"version": "==1.7.0"
|
||||
},
|
||||
"pycodestyle": {
|
||||
"hashes": [
|
||||
"sha256:682256a5b318149ca0d2a9185d365d8864a768a28db66a84a2ea946bcc426766",
|
||||
"sha256:6c4245ade1edfad79c3446fadfc96b0de2759662dc29d07d80a6f27ad1ca6ba9"
|
||||
],
|
||||
"version": "==2.3.1"
|
||||
},
|
||||
"pycparser": {
|
||||
"hashes": [
|
||||
"sha256:a988718abfad80b6b157acce7bf130a30876d27603738ac39f140993246b25b3"
|
||||
],
|
||||
"version": "==2.19"
|
||||
},
|
||||
"pyflakes": {
|
||||
"hashes": [
|
||||
"sha256:08bd6a50edf8cffa9fa09a463063c425ecaaf10d1eb0335a7e8b1401aef89e6f",
|
||||
"sha256:8d616a382f243dbf19b54743f280b80198be0bca3a5396f1d2e1fca6223e8805"
|
||||
],
|
||||
"version": "==1.6.0"
|
||||
},
|
||||
"pygments": {
|
||||
"hashes": [
|
||||
"sha256:78f3f434bcc5d6ee09020f92ba487f95ba50f1e3ef83ae96b9d5ffa1bab25c5d",
|
||||
"sha256:dbae1046def0efb574852fab9e90209b23f556367b5a320c0bcb871c77c3e8cc"
|
||||
],
|
||||
"version": "==2.2.0"
|
||||
},
|
||||
"pyparsing": {
|
||||
"hashes": [
|
||||
"sha256:bc6c7146b91af3f567cf6daeaec360bc07d45ffec4cf5353f4d7a208ce7ca30a",
|
||||
"sha256:d29593d8ebe7b57d6967b62494f8c72b03ac0262b1eed63826c6f788b3606401"
|
||||
],
|
||||
"version": "==2.2.2"
|
||||
},
|
||||
"pytest": {
|
||||
"hashes": [
|
||||
"sha256:7e258ee50338f4e46957f9e09a0f10fb1c2d05493fa901d113a8dafd0790de4e",
|
||||
"sha256:9332147e9af2dcf46cd7ceb14d5acadb6564744ddff1fe8c17f0ce60ece7d9a2"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==3.8.2"
|
||||
},
|
||||
"pytz": {
|
||||
"hashes": [
|
||||
"sha256:a061aa0a9e06881eb8b3b2b43f05b9439d6583c206d0a6c340ff72a7b6669053",
|
||||
"sha256:ffb9ef1de172603304d9d2819af6f5ece76f2e85ec10692a524dd876e72bf277"
|
||||
],
|
||||
"version": "==2018.5"
|
||||
},
|
||||
"readme-renderer": {
|
||||
"hashes": [
|
||||
"sha256:237ca8705ffea849870de41101dba41543561da05c0ae45b2f1c547efa9843d2",
|
||||
"sha256:f75049a3a7afa57165551e030dd8f9882ebf688b9600535a3f7e23596651875d"
|
||||
],
|
||||
"version": "==22.0"
|
||||
},
|
||||
"requests": {
|
||||
"hashes": [
|
||||
"sha256:63b52e3c866428a224f97cab011de738c36aec0185aa91cfacd418b5d58911d1",
|
||||
"sha256:ec22d826a36ed72a7358ff3fe56cbd4ba69dd7a6718ffd450ff0e9df7a47ce6a"
|
||||
],
|
||||
"version": "==2.19.1"
|
||||
},
|
||||
"requests-toolbelt": {
|
||||
"hashes": [
|
||||
"sha256:42c9c170abc2cacb78b8ab23ac957945c7716249206f90874651971a4acff237",
|
||||
"sha256:f6a531936c6fa4c6cfce1b9c10d5c4f498d16528d2a54a22ca00011205a187b5"
|
||||
],
|
||||
"version": "==0.8.0"
|
||||
},
|
||||
"six": {
|
||||
"hashes": [
|
||||
"sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9",
|
||||
"sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb"
|
||||
],
|
||||
"version": "==1.11.0"
|
||||
},
|
||||
"snowballstemmer": {
|
||||
"hashes": [
|
||||
"sha256:919f26a68b2c17a7634da993d91339e288964f93c274f1343e3bbbe2096e1128",
|
||||
"sha256:9f3bcd3c401c3e862ec0ebe6d2c069ebc012ce142cce209c098ccb5b09136e89"
|
||||
],
|
||||
"version": "==1.2.1"
|
||||
},
|
||||
"sphinx": {
|
||||
"hashes": [
|
||||
"sha256:652eb8c566f18823a022bb4b6dbc868d366df332a11a0226b5bc3a798a479f17",
|
||||
"sha256:d222626d8356de702431e813a05c68a35967e3d66c6cd1c2c89539bb179a7464"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.8.1"
|
||||
},
|
||||
"sphinxcontrib-websupport": {
|
||||
"hashes": [
|
||||
"sha256:68ca7ff70785cbe1e7bccc71a48b5b6d965d79ca50629606c7861a21b206d9dd",
|
||||
"sha256:9de47f375baf1ea07cdb3436ff39d7a9c76042c10a769c52353ec46e4e8fc3b9"
|
||||
],
|
||||
"version": "==1.1.0"
|
||||
},
|
||||
"toml": {
|
||||
"hashes": [
|
||||
"sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c",
|
||||
"sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e"
|
||||
],
|
||||
"version": "==0.10.0"
|
||||
},
|
||||
"tqdm": {
|
||||
"hashes": [
|
||||
"sha256:a0be569511161220ff709a5b60d0890d47921f746f1c737a11d965e1b29e7b2e",
|
||||
"sha256:e293e6d7a7f41a529a27f8d6624ab11544ccbfe82a205af6fad102545099fc21"
|
||||
],
|
||||
"version": "==4.27.0"
|
||||
},
|
||||
"twine": {
|
||||
"hashes": [
|
||||
"sha256:7d89bc6acafb31d124e6e5b295ef26ac77030bf098960c2a4c4e058335827c5c",
|
||||
"sha256:fad6f1251195f7ddd1460cb76d6ea106c93adb4e56c41e0da79658e56e547d2c"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.12.1"
|
||||
},
|
||||
"urllib3": {
|
||||
"hashes": [
|
||||
"sha256:a68ac5e15e76e7e5dd2b8f94007233e01effe3e50e8daddf69acfd81cb686baf",
|
||||
"sha256:b5725a0bd4ba422ab0e66e89e030c806576753ea3ee08554382c14e685d117b5"
|
||||
],
|
||||
"version": "==1.23"
|
||||
},
|
||||
"webencodings": {
|
||||
"hashes": [
|
||||
"sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78",
|
||||
"sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923"
|
||||
],
|
||||
"version": "==0.5.1"
|
||||
},
|
||||
"werkzeug": {
|
||||
"hashes": [
|
||||
"sha256:c3fd7a7d41976d9f44db327260e263132466836cef6f91512889ed60ad26557c",
|
||||
"sha256:d5da73735293558eb1651ee2fddc4d0dedcfa06538b8813a2e20011583c9e49b"
|
||||
],
|
||||
"version": "==0.14.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,170 +0,0 @@
|
||||
# Responder: a familiar HTTP Service Framework for Python
|
||||
|
||||
[](https://travis-ci.org/kennethreitz/responder)
|
||||
[](https://responder.readthedocs.io/en/latest/)
|
||||
[](https://pypi.org/project/responder/)
|
||||
[](https://pypi.org/project/responder/)
|
||||
[](https://pypi.org/project/responder/)
|
||||
[](https://github.com/kennethreitz/responder/graphs/contributors)
|
||||
|
||||
[](http://python-responder.org/)
|
||||
|
||||
The Python world certainly doesn't need more web frameworks. But, it does need more creativity, so I thought I'd spread some [Hacktoberfest](https://hacktoberfest.digitalocean.com/) spirit around, bring some of my ideas to the table, and see what I could come up with.
|
||||
|
||||
```python
|
||||
import responder
|
||||
|
||||
api = responder.API()
|
||||
|
||||
@api.route("/{greeting}")
|
||||
async def greet_world(req, resp, *, greeting):
|
||||
resp.text = f"{greeting}, world!"
|
||||
|
||||
if __name__ == '__main__':
|
||||
api.run()
|
||||
```
|
||||
|
||||
That `async` declaration is optional. [View documentation](http://python-responder.org).
|
||||
|
||||
This gets you a ASGI app, with a production static files server pre-installed, jinja2 templating (without additional imports), and a production webserver based on uvloop, serving up requests with gzip compression automatically.
|
||||
|
||||
|
||||
## Testimonials
|
||||
|
||||
> "Pleasantly very taken with python-responder. [@kennethreitz](https://twitter.com/kennethreitz) at his absolute best." —Rudraksh M.K.
|
||||
|
||||
> "ASGI is going to enable all sorts of new high-performance web services. It's awesome to see Responder starting to take advantage of that." — Tom Christie author of [Django REST Framework](https://www.django-rest-framework.org/)
|
||||
|
||||
> "I love that you are exploring new patterns. Go go go!" — Danny Greenfield, author of [Two Scoops of Django]()
|
||||
|
||||
> "Love what I have seen while it's in progress! Many features of Responder are from my wishlist for Flask, and it's even faster and even easier than Flask!" — Luna C.
|
||||
|
||||
## More Examples
|
||||
|
||||
Class-based views (and setting some headers and stuff):
|
||||
|
||||
```python
|
||||
@api.route("/{greeting}")
|
||||
class GreetingResource:
|
||||
def on_request(req, resp, *, greeting): # or on_get...
|
||||
resp.text = f"{greeting}, world!"
|
||||
resp.headers.update({'X-Life': '42'})
|
||||
resp.status_code = api.status_codes.HTTP_416
|
||||
```
|
||||
|
||||
Render a template, with arguments:
|
||||
|
||||
```python
|
||||
@api.route("/{greeting}")
|
||||
def greet_world(req, resp, *, greeting):
|
||||
resp.content = api.template("index.html", greeting=greeting)
|
||||
```
|
||||
|
||||
The `api` instance is available as an object during template rendering.
|
||||
|
||||
Here, you can spawn off a background thread to run any function, out-of-request:
|
||||
|
||||
```python
|
||||
@api.route("/")
|
||||
def hello(req, resp):
|
||||
|
||||
@api.background.task
|
||||
def sleep(s=10):
|
||||
time.sleep(s)
|
||||
print("slept!")
|
||||
|
||||
sleep()
|
||||
resp.content = "processing"
|
||||
```
|
||||
|
||||
And even serve a GraphQL API:
|
||||
|
||||
```python
|
||||
import graphene
|
||||
|
||||
class Query(graphene.ObjectType):
|
||||
hello = graphene.String(name=graphene.String(default_value="stranger"))
|
||||
|
||||
def resolve_hello(self, info, name):
|
||||
return "Hello " + name
|
||||
|
||||
api.add_route("/graph", graphene.Schema(query=Query))
|
||||
```
|
||||
|
||||
We can then send a query to our service:
|
||||
|
||||
```pycon
|
||||
>>> requests = api.session()
|
||||
>>> r = requests.get("http://;/graph", params={"query": "{ hello }"})
|
||||
>>> r.json()
|
||||
{'data': {'hello': 'Hello stranger'}}
|
||||
```
|
||||
|
||||
Or, request YAML back:
|
||||
|
||||
```pycon
|
||||
>>> r = requests.get("http://;/graph", params={"query": "{ hello(name:\"john\") }"}, headers={"Accept": "application/x-yaml"})
|
||||
>>> print(r.text)
|
||||
data: {hello: Hello john}
|
||||
|
||||
```
|
||||
|
||||
Want HSTS?
|
||||
|
||||
```
|
||||
api = responder.API(enable_hsts=True)
|
||||
```
|
||||
|
||||
Boom.
|
||||
|
||||
|
||||
# Installing Responder
|
||||
|
||||
Install the latest release:
|
||||
|
||||
|
||||
$ pipenv install responder
|
||||
✨🍰✨
|
||||
|
||||
|
||||
Or, install from the development branch:
|
||||
|
||||
$ pipenv install -e git+https://github.com/kennethreitz/responder.git#egg=responder
|
||||
|
||||
Only **Python 3.6+** is supported.
|
||||
|
||||
|
||||
# The Basic Idea
|
||||
|
||||
The primary concept here is to bring the niceties that are brought forth from both Flask and Falcon and unify them into a single framework, along with some new ideas I have. I also wanted to take some of the API primitives that are instilled in the Requests library and put them into a web framework. So, you'll find a lot of parallels here with Requests.
|
||||
|
||||
- Setting `resp.text` sends back unicode, while setting `resp.content` sends back bytes.
|
||||
- Setting `resp.media` sends back JSON/YAML (`.text`/`.content` override this).
|
||||
- Case-insensitive `req.headers` dict (from Requests directly).
|
||||
- `resp.status_code`, `req.method`, `req.url`, and other familiar friends.
|
||||
|
||||
## Ideas
|
||||
|
||||
- Flask-style route expression, with new capabilities -- all while using Python 3.6+'s new f-string syntax.
|
||||
- I love Falcon's "every request and response is passed into to each view and mutated" methodology, especially `response.media`, and have used it here. In addition to supporting JSON, I have decided to support YAML as well, as Kubernetes is slowly taking over the world, and it uses YAML for all the things. Content-negotiation and all that.
|
||||
- **A built in testing client that uses the actual Requests you know and love**.
|
||||
- The ability to mount other WSGI apps easily.
|
||||
- Automatic gzipped-responses.
|
||||
- In addition to Falcon's `on_get`, `on_post`, etc methods, Responder features an `on_request` method, which gets called on every type of request, much like Requests.
|
||||
- A production static file server is built-in.
|
||||
- Uvicorn built-in as a production web server. I would have chosen Gunicorn, but it doesn't run on Windows. Plus, Uvicorn serves well to protect against slowloris attacks, making nginx unnecessary in production.
|
||||
- GraphQL support, via Graphene. The goal here is to have any GraphQL query exposable at any route, magically.
|
||||
|
||||
## Future Ideas
|
||||
|
||||
- Cookie-based sessions are currently an afterthought, as this is an API framework, but websites are APIs too.
|
||||
- If frontend websites are supported, provide an official way to run webpack.
|
||||
|
||||
# The Goal
|
||||
|
||||
The primary goal here is to learn, not to get adoption. Though, who knows how these things will pan out.
|
||||
|
||||
|
||||
----------
|
||||
|
||||
[](https://hacktoberfest.digitalocean.com/)
|
||||
@@ -0,0 +1,92 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html lang="en" data-content_root="../">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Overview: module code — responder 3.6.2 documentation</title>
|
||||
<link rel="stylesheet" type="text/css" href="../_static/pygments.css?v=5ecbeea2" />
|
||||
<link rel="stylesheet" type="text/css" href="../_static/basic.css?v=b08954a9" />
|
||||
<link rel="stylesheet" type="text/css" href="../_static/alabaster.css?v=27fed22d" />
|
||||
<link rel="stylesheet" type="text/css" href="../_static/copybutton.css?v=76b2166b" />
|
||||
<link rel="stylesheet" type="text/css" href="../_static/design-elements.e5416f61bae5d36adc6d722a2b6f8cff.css?v=452a8e97" />
|
||||
<script src="../_static/documentation_options.js?v=c0c9fa11"></script>
|
||||
<script src="../_static/doctools.js?v=9bcbadda"></script>
|
||||
<script src="../_static/sphinx_highlight.js?v=dc90522c"></script>
|
||||
<script src="../_static/clipboard.min.js?v=a7894cd8"></script>
|
||||
<script src="../_static/copybutton.js?v=fd10adb8"></script>
|
||||
<script>
|
||||
</script>
|
||||
<script src="../_static/design-elements.bbdccc18c4abea9397628f9fea3d48c2.js?v=03c7770e"></script>
|
||||
<link rel="index" title="Index" href="../genindex.html" />
|
||||
<link rel="search" title="Search" href="../search.html" />
|
||||
|
||||
<link rel="stylesheet" href="../_static/custom.css" type="text/css" />
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</head><body>
|
||||
|
||||
|
||||
<div class="document">
|
||||
<div class="documentwrapper">
|
||||
<div class="bodywrapper">
|
||||
|
||||
|
||||
<div class="body" role="main">
|
||||
|
||||
<h1>All modules for which code is available</h1>
|
||||
<ul><li><a href="responder/api.html">responder.api</a></li>
|
||||
<li><a href="responder/background.html">responder.background</a></li>
|
||||
<li><a href="responder/ext/ratelimit.html">responder.ext.ratelimit</a></li>
|
||||
<li><a href="responder/models.html">responder.models</a></li>
|
||||
<li><a href="responder/status_codes.html">responder.status_codes</a></li>
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="sphinxsidebar" role="navigation" aria-label="Main">
|
||||
<div class="sphinxsidebarwrapper"><p class="logo">
|
||||
<a href="../index.html">
|
||||
<img class="logo" src="../_static/responder.png" />
|
||||
</a>
|
||||
</p>
|
||||
<p>
|
||||
<strong>Responder</strong> — a familiar HTTP service framework for Python.
|
||||
<br />
|
||||
<small>v3.6.2</small>
|
||||
</p>
|
||||
<h3>Useful Links</h3>
|
||||
<ul>
|
||||
<li><a href="https://github.com/kennethreitz/responder">Responder @ GitHub</a></li>
|
||||
<li><a href="https://pypi.org/project/responder/">Responder @ PyPI</a></li>
|
||||
<li><a href="https://github.com/kennethreitz/responder/issues">Issue Tracker</a></li>
|
||||
</ul>
|
||||
<search id="searchbox" style="display: none" role="search">
|
||||
<h3 id="searchlabel">Quick search</h3>
|
||||
<div class="searchformwrapper">
|
||||
<form class="search" action="../search.html" method="get">
|
||||
<input type="text" name="q" aria-labelledby="searchlabel" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"/>
|
||||
<input type="submit" value="Go" />
|
||||
</form>
|
||||
</div>
|
||||
</search>
|
||||
<script>document.getElementById('searchbox').style.display = "block"</script>
|
||||
</div>
|
||||
</div>
|
||||
<div class="clearer"></div>
|
||||
</div>
|
||||
<div class="footer">
|
||||
©2018-2026, Kenneth Reitz.
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,777 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html lang="en" data-content_root="../../">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>responder.api — responder 3.6.2 documentation</title>
|
||||
<link rel="stylesheet" type="text/css" href="../../_static/pygments.css?v=5ecbeea2" />
|
||||
<link rel="stylesheet" type="text/css" href="../../_static/basic.css?v=b08954a9" />
|
||||
<link rel="stylesheet" type="text/css" href="../../_static/alabaster.css?v=27fed22d" />
|
||||
<link rel="stylesheet" type="text/css" href="../../_static/copybutton.css?v=76b2166b" />
|
||||
<link rel="stylesheet" type="text/css" href="../../_static/design-elements.e5416f61bae5d36adc6d722a2b6f8cff.css?v=452a8e97" />
|
||||
<script src="../../_static/documentation_options.js?v=c0c9fa11"></script>
|
||||
<script src="../../_static/doctools.js?v=9bcbadda"></script>
|
||||
<script src="../../_static/sphinx_highlight.js?v=dc90522c"></script>
|
||||
<script src="../../_static/clipboard.min.js?v=a7894cd8"></script>
|
||||
<script src="../../_static/copybutton.js?v=fd10adb8"></script>
|
||||
<script>
|
||||
</script>
|
||||
<script src="../../_static/design-elements.bbdccc18c4abea9397628f9fea3d48c2.js?v=03c7770e"></script>
|
||||
<link rel="index" title="Index" href="../../genindex.html" />
|
||||
<link rel="search" title="Search" href="../../search.html" />
|
||||
|
||||
<link rel="stylesheet" href="../../_static/custom.css" type="text/css" />
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</head><body>
|
||||
|
||||
|
||||
<div class="document">
|
||||
<div class="documentwrapper">
|
||||
<div class="bodywrapper">
|
||||
|
||||
|
||||
<div class="body" role="main">
|
||||
|
||||
<h1>Source code for responder.api</h1><div class="highlight"><pre>
|
||||
<span></span><span class="kn">import</span><span class="w"> </span><span class="nn">asyncio</span>
|
||||
<span class="kn">import</span><span class="w"> </span><span class="nn">inspect</span>
|
||||
<span class="kn">import</span><span class="w"> </span><span class="nn">os</span>
|
||||
<span class="kn">from</span><span class="w"> </span><span class="nn">pathlib</span><span class="w"> </span><span class="kn">import</span> <span class="n">Path</span>
|
||||
|
||||
<span class="n">__all__</span> <span class="o">=</span> <span class="p">[</span><span class="s2">"API"</span><span class="p">]</span>
|
||||
|
||||
<span class="kn">import</span><span class="w"> </span><span class="nn">uvicorn</span>
|
||||
<span class="kn">from</span><span class="w"> </span><span class="nn">starlette.middleware.cors</span><span class="w"> </span><span class="kn">import</span> <span class="n">CORSMiddleware</span>
|
||||
<span class="kn">from</span><span class="w"> </span><span class="nn">starlette.middleware.errors</span><span class="w"> </span><span class="kn">import</span> <span class="n">ServerErrorMiddleware</span>
|
||||
<span class="kn">from</span><span class="w"> </span><span class="nn">starlette.middleware.exceptions</span><span class="w"> </span><span class="kn">import</span> <span class="n">ExceptionMiddleware</span>
|
||||
<span class="kn">from</span><span class="w"> </span><span class="nn">starlette.middleware.gzip</span><span class="w"> </span><span class="kn">import</span> <span class="n">GZipMiddleware</span>
|
||||
<span class="kn">from</span><span class="w"> </span><span class="nn">starlette.middleware.httpsredirect</span><span class="w"> </span><span class="kn">import</span> <span class="n">HTTPSRedirectMiddleware</span>
|
||||
<span class="kn">from</span><span class="w"> </span><span class="nn">starlette.middleware.sessions</span><span class="w"> </span><span class="kn">import</span> <span class="n">SessionMiddleware</span>
|
||||
<span class="kn">from</span><span class="w"> </span><span class="nn">starlette.middleware.trustedhost</span><span class="w"> </span><span class="kn">import</span> <span class="n">TrustedHostMiddleware</span>
|
||||
|
||||
<span class="kn">from</span><span class="w"> </span><span class="nn">.</span><span class="w"> </span><span class="kn">import</span> <span class="n">status_codes</span>
|
||||
<span class="kn">from</span><span class="w"> </span><span class="nn">.background</span><span class="w"> </span><span class="kn">import</span> <span class="n">BackgroundQueue</span>
|
||||
<span class="kn">from</span><span class="w"> </span><span class="nn">.formats</span><span class="w"> </span><span class="kn">import</span> <span class="n">get_formats</span>
|
||||
<span class="kn">from</span><span class="w"> </span><span class="nn">.models</span><span class="w"> </span><span class="kn">import</span> <span class="n">Request</span><span class="p">,</span> <span class="n">Response</span>
|
||||
<span class="kn">from</span><span class="w"> </span><span class="nn">.routes</span><span class="w"> </span><span class="kn">import</span> <span class="n">Router</span>
|
||||
<span class="kn">from</span><span class="w"> </span><span class="nn">.staticfiles</span><span class="w"> </span><span class="kn">import</span> <span class="n">StaticFiles</span>
|
||||
<span class="kn">from</span><span class="w"> </span><span class="nn">.statics</span><span class="w"> </span><span class="kn">import</span> <span class="n">DEFAULT_CORS_PARAMS</span><span class="p">,</span> <span class="n">DEFAULT_OPENAPI_THEME</span><span class="p">,</span> <span class="n">DEFAULT_SECRET_KEY</span>
|
||||
<span class="kn">from</span><span class="w"> </span><span class="nn">.templates</span><span class="w"> </span><span class="kn">import</span> <span class="n">Templates</span>
|
||||
|
||||
|
||||
<div class="viewcode-block" id="API">
|
||||
<a class="viewcode-back" href="../../api.html#responder.API">[docs]</a>
|
||||
<span class="k">class</span><span class="w"> </span><span class="nc">API</span><span class="p">:</span>
|
||||
<span class="w"> </span><span class="sd">"""The primary web-service class.</span>
|
||||
|
||||
<span class="sd"> :param static_dir: The directory to use for static files. Will be created for you if it doesn't already exist.</span>
|
||||
<span class="sd"> :param templates_dir: The directory to use for templates. Will be created for you if it doesn't already exist.</span>
|
||||
<span class="sd"> :param auto_escape: If ``True``, HTML and XML templates will automatically be escaped.</span>
|
||||
<span class="sd"> :param enable_hsts: If ``True``, send all responses to HTTPS URLs.</span>
|
||||
<span class="sd"> :param gzip: If ``True`` (the default), compress responses with GZip.</span>
|
||||
<span class="sd"> :param openapi_theme: OpenAPI documentation theme, must be one of ``elements``, ``rapidoc``, ``redoc``, ``swagger_ui``</span>
|
||||
<span class="sd"> """</span> <span class="c1"># noqa: E501</span>
|
||||
|
||||
<span class="n">status_codes</span> <span class="o">=</span> <span class="n">status_codes</span>
|
||||
|
||||
<span class="k">def</span><span class="w"> </span><span class="fm">__init__</span><span class="p">(</span>
|
||||
<span class="bp">self</span><span class="p">,</span>
|
||||
<span class="o">*</span><span class="p">,</span>
|
||||
<span class="n">debug</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span>
|
||||
<span class="n">title</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span>
|
||||
<span class="n">version</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span>
|
||||
<span class="n">description</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span>
|
||||
<span class="n">terms_of_service</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span>
|
||||
<span class="n">contact</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span>
|
||||
<span class="n">license</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span> <span class="c1"># noqa: A002</span>
|
||||
<span class="n">openapi</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span>
|
||||
<span class="n">openapi_route</span><span class="o">=</span><span class="s2">"/schema.yml"</span><span class="p">,</span>
|
||||
<span class="n">static_dir</span><span class="o">=</span><span class="s2">"static"</span><span class="p">,</span>
|
||||
<span class="n">static_route</span><span class="o">=</span><span class="s2">"/static"</span><span class="p">,</span>
|
||||
<span class="n">templates_dir</span><span class="o">=</span><span class="s2">"templates"</span><span class="p">,</span>
|
||||
<span class="n">auto_escape</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span>
|
||||
<span class="n">secret_key</span><span class="o">=</span><span class="n">DEFAULT_SECRET_KEY</span><span class="p">,</span>
|
||||
<span class="n">enable_hsts</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span>
|
||||
<span class="n">docs_route</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span>
|
||||
<span class="n">cors</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span>
|
||||
<span class="n">cors_params</span><span class="o">=</span><span class="n">DEFAULT_CORS_PARAMS</span><span class="p">,</span>
|
||||
<span class="n">allowed_hosts</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span>
|
||||
<span class="n">openapi_theme</span><span class="o">=</span><span class="n">DEFAULT_OPENAPI_THEME</span><span class="p">,</span>
|
||||
<span class="n">lifespan</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span>
|
||||
<span class="n">gzip</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span>
|
||||
<span class="n">request_id</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span>
|
||||
<span class="n">enable_logging</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span>
|
||||
<span class="p">):</span>
|
||||
<span class="w"> </span><span class="sd">"""Create a new Responder API instance.</span>
|
||||
|
||||
<span class="sd"> :param debug: If ``True``, enable debug mode with verbose error pages.</span>
|
||||
<span class="sd"> :param title: The title of the API, used in OpenAPI documentation.</span>
|
||||
<span class="sd"> :param version: The version string for the API (e.g. ``"1.0"``).</span>
|
||||
<span class="sd"> :param description: A longer description of the API for OpenAPI docs.</span>
|
||||
<span class="sd"> :param terms_of_service: URL to the API's terms of service.</span>
|
||||
<span class="sd"> :param contact: Contact information dict for the API (``name``, ``url``, ``email``).</span>
|
||||
<span class="sd"> :param license: License information dict (``name``, ``url``).</span>
|
||||
<span class="sd"> :param openapi: The OpenAPI version string (e.g. ``"3.0.2"``). Enables OpenAPI schema generation.</span>
|
||||
<span class="sd"> :param openapi_route: The URL path for the OpenAPI schema (default ``"/schema.yml"``).</span>
|
||||
<span class="sd"> :param static_dir: Directory for static files. Set to ``None`` to disable. Created automatically if missing.</span>
|
||||
<span class="sd"> :param static_route: URL prefix for serving static files (default ``"/static"``).</span>
|
||||
<span class="sd"> :param templates_dir: Directory for Jinja2 templates (default ``"templates"``).</span>
|
||||
<span class="sd"> :param auto_escape: If ``True``, auto-escape HTML/XML in templates.</span>
|
||||
<span class="sd"> :param secret_key: Secret key for signing cookie-based sessions. **Always set this in production.**</span>
|
||||
<span class="sd"> :param enable_hsts: If ``True``, redirect all HTTP requests to HTTPS.</span>
|
||||
<span class="sd"> :param docs_route: URL path for interactive API docs (e.g. ``"/docs"``). Enables OpenAPI if not already set.</span>
|
||||
<span class="sd"> :param cors: If ``True``, enable CORS middleware.</span>
|
||||
<span class="sd"> :param cors_params: Dict of CORS configuration (``allow_origins``, ``allow_methods``, etc.).</span>
|
||||
<span class="sd"> :param allowed_hosts: List of allowed hostnames (e.g. ``["example.com"]``). Defaults to ``["*"]``.</span>
|
||||
<span class="sd"> :param openapi_theme: Documentation UI theme: ``"swagger_ui"``, ``"redoc"``, ``"rapidoc"``, or ``"elements"``.</span>
|
||||
<span class="sd"> :param lifespan: An async context manager for startup/shutdown logic.</span>
|
||||
<span class="sd"> :param gzip: If ``True`` (the default), compress responses with GZip.</span>
|
||||
<span class="sd"> :param request_id: If ``True``, add ``X-Request-ID`` headers to all responses.</span>
|
||||
<span class="sd"> :param enable_logging: If ``True``, enable structured logging with per-request context (request ID, method, path, client IP).</span>
|
||||
<span class="sd"> """</span> <span class="c1"># noqa: E501</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">background</span> <span class="o">=</span> <span class="n">BackgroundQueue</span><span class="p">()</span>
|
||||
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">secret_key</span> <span class="o">=</span> <span class="n">secret_key</span>
|
||||
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">router</span> <span class="o">=</span> <span class="n">Router</span><span class="p">(</span><span class="n">lifespan</span><span class="o">=</span><span class="n">lifespan</span><span class="p">)</span>
|
||||
|
||||
<span class="k">if</span> <span class="n">static_dir</span> <span class="ow">is</span> <span class="ow">not</span> <span class="kc">None</span><span class="p">:</span>
|
||||
<span class="k">if</span> <span class="n">static_route</span> <span class="ow">is</span> <span class="kc">None</span><span class="p">:</span>
|
||||
<span class="n">static_route</span> <span class="o">=</span> <span class="s2">""</span>
|
||||
<span class="n">static_dir</span> <span class="o">=</span> <span class="n">Path</span><span class="p">(</span><span class="n">static_dir</span><span class="p">)</span><span class="o">.</span><span class="n">resolve</span><span class="p">()</span>
|
||||
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">static_dir</span> <span class="o">=</span> <span class="n">static_dir</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">static_route</span> <span class="o">=</span> <span class="n">static_route</span>
|
||||
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">hsts_enabled</span> <span class="o">=</span> <span class="n">enable_hsts</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">cors</span> <span class="o">=</span> <span class="n">cors</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">cors_params</span> <span class="o">=</span> <span class="n">cors_params</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">debug</span> <span class="o">=</span> <span class="n">debug</span>
|
||||
|
||||
<span class="k">if</span> <span class="ow">not</span> <span class="n">allowed_hosts</span><span class="p">:</span>
|
||||
<span class="n">allowed_hosts</span> <span class="o">=</span> <span class="p">[</span><span class="s2">"*"</span><span class="p">]</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">allowed_hosts</span> <span class="o">=</span> <span class="n">allowed_hosts</span>
|
||||
|
||||
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">static_dir</span> <span class="ow">is</span> <span class="ow">not</span> <span class="kc">None</span><span class="p">:</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">static_dir</span><span class="o">.</span><span class="n">mkdir</span><span class="p">(</span><span class="n">parents</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span> <span class="n">exist_ok</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">mount</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">static_route</span><span class="p">,</span> <span class="bp">self</span><span class="o">.</span><span class="n">static_app</span><span class="p">)</span>
|
||||
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">formats</span> <span class="o">=</span> <span class="n">get_formats</span><span class="p">()</span>
|
||||
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_session</span> <span class="o">=</span> <span class="kc">None</span>
|
||||
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">default_endpoint</span> <span class="o">=</span> <span class="kc">None</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">app</span> <span class="o">=</span> <span class="n">ExceptionMiddleware</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">router</span><span class="p">,</span> <span class="n">debug</span><span class="o">=</span><span class="n">debug</span><span class="p">)</span>
|
||||
|
||||
<span class="k">if</span> <span class="n">gzip</span><span class="p">:</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">add_middleware</span><span class="p">(</span><span class="n">GZipMiddleware</span><span class="p">)</span>
|
||||
|
||||
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">hsts_enabled</span><span class="p">:</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">add_middleware</span><span class="p">(</span><span class="n">HTTPSRedirectMiddleware</span><span class="p">)</span>
|
||||
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">add_middleware</span><span class="p">(</span><span class="n">TrustedHostMiddleware</span><span class="p">,</span> <span class="n">allowed_hosts</span><span class="o">=</span><span class="bp">self</span><span class="o">.</span><span class="n">allowed_hosts</span><span class="p">)</span>
|
||||
|
||||
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">cors</span><span class="p">:</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">add_middleware</span><span class="p">(</span><span class="n">CORSMiddleware</span><span class="p">,</span> <span class="o">**</span><span class="bp">self</span><span class="o">.</span><span class="n">cors_params</span><span class="p">)</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">add_middleware</span><span class="p">(</span><span class="n">ServerErrorMiddleware</span><span class="p">,</span> <span class="n">debug</span><span class="o">=</span><span class="n">debug</span><span class="p">)</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">add_middleware</span><span class="p">(</span><span class="n">SessionMiddleware</span><span class="p">,</span> <span class="n">secret_key</span><span class="o">=</span><span class="bp">self</span><span class="o">.</span><span class="n">secret_key</span><span class="p">)</span>
|
||||
|
||||
<span class="k">if</span> <span class="n">openapi</span> <span class="ow">or</span> <span class="n">docs_route</span><span class="p">:</span>
|
||||
<span class="k">try</span><span class="p">:</span>
|
||||
<span class="kn">from</span><span class="w"> </span><span class="nn">.ext.openapi</span><span class="w"> </span><span class="kn">import</span> <span class="n">OpenAPISchema</span>
|
||||
<span class="k">except</span> <span class="ne">ImportError</span> <span class="k">as</span> <span class="n">ex</span><span class="p">:</span>
|
||||
<span class="k">raise</span> <span class="ne">ImportError</span><span class="p">(</span>
|
||||
<span class="s2">"The dependencies for the OpenAPI extension are not installed. "</span>
|
||||
<span class="s2">"Install them using: pip install responder"</span>
|
||||
<span class="p">)</span> <span class="kn">from</span><span class="w"> </span><span class="nn">ex</span>
|
||||
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">openapi</span> <span class="o">=</span> <span class="n">OpenAPISchema</span><span class="p">(</span>
|
||||
<span class="n">app</span><span class="o">=</span><span class="bp">self</span><span class="p">,</span>
|
||||
<span class="n">title</span><span class="o">=</span><span class="n">title</span><span class="p">,</span>
|
||||
<span class="n">version</span><span class="o">=</span><span class="n">version</span><span class="p">,</span>
|
||||
<span class="n">openapi</span><span class="o">=</span><span class="n">openapi</span><span class="p">,</span>
|
||||
<span class="n">docs_route</span><span class="o">=</span><span class="n">docs_route</span><span class="p">,</span>
|
||||
<span class="n">description</span><span class="o">=</span><span class="n">description</span><span class="p">,</span>
|
||||
<span class="n">terms_of_service</span><span class="o">=</span><span class="n">terms_of_service</span><span class="p">,</span>
|
||||
<span class="n">contact</span><span class="o">=</span><span class="n">contact</span><span class="p">,</span>
|
||||
<span class="n">license</span><span class="o">=</span><span class="n">license</span><span class="p">,</span>
|
||||
<span class="n">openapi_route</span><span class="o">=</span><span class="n">openapi_route</span><span class="p">,</span>
|
||||
<span class="n">static_route</span><span class="o">=</span><span class="n">static_route</span><span class="p">,</span>
|
||||
<span class="n">openapi_theme</span><span class="o">=</span><span class="n">openapi_theme</span><span class="p">,</span>
|
||||
<span class="p">)</span>
|
||||
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">templates</span> <span class="o">=</span> <span class="n">Templates</span><span class="p">(</span><span class="n">directory</span><span class="o">=</span><span class="n">templates_dir</span><span class="p">)</span>
|
||||
|
||||
<span class="k">if</span> <span class="n">request_id</span> <span class="ow">and</span> <span class="ow">not</span> <span class="n">enable_logging</span><span class="p">:</span>
|
||||
<span class="kn">import</span><span class="w"> </span><span class="nn">uuid</span><span class="w"> </span><span class="k">as</span><span class="w"> </span><span class="nn">_uuid</span>
|
||||
|
||||
<span class="k">def</span><span class="w"> </span><span class="nf">_add_request_id</span><span class="p">(</span><span class="n">req</span><span class="p">,</span> <span class="n">resp</span><span class="p">):</span>
|
||||
<span class="n">rid</span> <span class="o">=</span> <span class="n">req</span><span class="o">.</span><span class="n">headers</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">"X-Request-ID"</span><span class="p">,</span> <span class="nb">str</span><span class="p">(</span><span class="n">_uuid</span><span class="o">.</span><span class="n">uuid4</span><span class="p">()))</span>
|
||||
<span class="n">resp</span><span class="o">.</span><span class="n">headers</span><span class="p">[</span><span class="s2">"X-Request-ID"</span><span class="p">]</span> <span class="o">=</span> <span class="n">rid</span>
|
||||
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">router</span><span class="o">.</span><span class="n">after_request</span><span class="p">(</span><span class="n">_add_request_id</span><span class="p">)</span>
|
||||
|
||||
<span class="k">if</span> <span class="n">enable_logging</span><span class="p">:</span>
|
||||
<span class="kn">import</span><span class="w"> </span><span class="nn">logging</span><span class="w"> </span><span class="k">as</span><span class="w"> </span><span class="nn">_logging</span>
|
||||
|
||||
<span class="kn">from</span><span class="w"> </span><span class="nn">.ext.logging</span><span class="w"> </span><span class="kn">import</span> <span class="n">LoggingMiddleware</span><span class="p">,</span> <span class="n">get_logger</span><span class="p">,</span> <span class="n">setup_logging</span>
|
||||
|
||||
<span class="n">log_level</span> <span class="o">=</span> <span class="n">_logging</span><span class="o">.</span><span class="n">DEBUG</span> <span class="k">if</span> <span class="n">debug</span> <span class="k">else</span> <span class="n">_logging</span><span class="o">.</span><span class="n">INFO</span>
|
||||
<span class="n">setup_logging</span><span class="p">(</span><span class="n">level</span><span class="o">=</span><span class="n">log_level</span><span class="p">)</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">add_middleware</span><span class="p">(</span><span class="n">LoggingMiddleware</span><span class="p">)</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">log</span> <span class="o">=</span> <span class="n">get_logger</span><span class="p">(</span><span class="s2">"responder.app"</span><span class="p">)</span>
|
||||
<span class="k">else</span><span class="p">:</span>
|
||||
<span class="kn">import</span><span class="w"> </span><span class="nn">logging</span><span class="w"> </span><span class="k">as</span><span class="w"> </span><span class="nn">_logging</span>
|
||||
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">log</span> <span class="o">=</span> <span class="n">_logging</span><span class="o">.</span><span class="n">getLogger</span><span class="p">(</span><span class="s2">"responder.app"</span><span class="p">)</span>
|
||||
|
||||
<span class="nd">@property</span>
|
||||
<span class="k">def</span><span class="w"> </span><span class="nf">requests</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||
<span class="w"> </span><span class="sd">"""A test client connected to the ASGI app. Lazily initialized."""</span>
|
||||
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">session</span><span class="p">()</span>
|
||||
|
||||
<span class="nd">@property</span>
|
||||
<span class="k">def</span><span class="w"> </span><span class="nf">static_app</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||
<span class="w"> </span><span class="sd">"""The Starlette ``StaticFiles`` application for serving static assets."""</span>
|
||||
<span class="k">if</span> <span class="ow">not</span> <span class="nb">hasattr</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="s2">"_static_app"</span><span class="p">):</span>
|
||||
<span class="k">assert</span> <span class="bp">self</span><span class="o">.</span><span class="n">static_dir</span> <span class="ow">is</span> <span class="ow">not</span> <span class="kc">None</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_static_app</span> <span class="o">=</span> <span class="n">StaticFiles</span><span class="p">(</span><span class="n">directory</span><span class="o">=</span><span class="bp">self</span><span class="o">.</span><span class="n">static_dir</span><span class="p">)</span>
|
||||
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_static_app</span>
|
||||
|
||||
<div class="viewcode-block" id="API.before_request">
|
||||
<a class="viewcode-back" href="../../api.html#responder.API.before_request">[docs]</a>
|
||||
<span class="k">def</span><span class="w"> </span><span class="nf">before_request</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">websocket</span><span class="o">=</span><span class="kc">False</span><span class="p">):</span>
|
||||
<span class="w"> </span><span class="sd">"""Register a function to run before every request.</span>
|
||||
|
||||
<span class="sd"> If the hook sets ``resp.status_code``, the route handler is skipped</span>
|
||||
<span class="sd"> and the response is sent immediately (short-circuiting).</span>
|
||||
|
||||
<span class="sd"> :param websocket: If ``True``, register as a WebSocket before-request hook instead of HTTP.</span>
|
||||
|
||||
<span class="sd"> Usage::</span>
|
||||
|
||||
<span class="sd"> @api.before_request()</span>
|
||||
<span class="sd"> def check_auth(req, resp):</span>
|
||||
<span class="sd"> if "Authorization" not in req.headers:</span>
|
||||
<span class="sd"> resp.status_code = 401</span>
|
||||
<span class="sd"> resp.media = {"error": "unauthorized"}</span>
|
||||
|
||||
<span class="sd"> """</span> <span class="c1"># noqa: E501</span>
|
||||
|
||||
<span class="k">def</span><span class="w"> </span><span class="nf">decorator</span><span class="p">(</span><span class="n">f</span><span class="p">):</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">router</span><span class="o">.</span><span class="n">before_request</span><span class="p">(</span><span class="n">f</span><span class="p">,</span> <span class="n">websocket</span><span class="o">=</span><span class="n">websocket</span><span class="p">)</span>
|
||||
<span class="k">return</span> <span class="n">f</span>
|
||||
|
||||
<span class="k">return</span> <span class="n">decorator</span></div>
|
||||
|
||||
|
||||
<div class="viewcode-block" id="API.after_request">
|
||||
<a class="viewcode-back" href="../../api.html#responder.API.after_request">[docs]</a>
|
||||
<span class="k">def</span><span class="w"> </span><span class="nf">after_request</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||
<span class="w"> </span><span class="sd">"""Register a function to run after every request.</span>
|
||||
|
||||
<span class="sd"> Usage::</span>
|
||||
|
||||
<span class="sd"> @api.after_request()</span>
|
||||
<span class="sd"> def add_request_id(req, resp):</span>
|
||||
<span class="sd"> resp.headers["X-Request-ID"] = str(uuid.uuid4())</span>
|
||||
|
||||
<span class="sd"> """</span>
|
||||
|
||||
<span class="k">def</span><span class="w"> </span><span class="nf">decorator</span><span class="p">(</span><span class="n">f</span><span class="p">):</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">router</span><span class="o">.</span><span class="n">after_request</span><span class="p">(</span><span class="n">f</span><span class="p">)</span>
|
||||
<span class="k">return</span> <span class="n">f</span>
|
||||
|
||||
<span class="k">return</span> <span class="n">decorator</span></div>
|
||||
|
||||
|
||||
<div class="viewcode-block" id="API.add_middleware">
|
||||
<a class="viewcode-back" href="../../api.html#responder.API.add_middleware">[docs]</a>
|
||||
<span class="k">def</span><span class="w"> </span><span class="nf">add_middleware</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">middleware_cls</span><span class="p">,</span> <span class="o">**</span><span class="n">middleware_config</span><span class="p">):</span>
|
||||
<span class="w"> </span><span class="sd">"""Add ASGI middleware to the application.</span>
|
||||
|
||||
<span class="sd"> Middleware wraps the entire application and can inspect or modify</span>
|
||||
<span class="sd"> every request and response. Middleware is applied in reverse order —</span>
|
||||
<span class="sd"> the last middleware added runs first.</span>
|
||||
|
||||
<span class="sd"> :param middleware_cls: A Starlette-compatible middleware class.</span>
|
||||
<span class="sd"> :param middleware_config: Keyword arguments passed to the middleware constructor.</span>
|
||||
|
||||
<span class="sd"> Usage::</span>
|
||||
|
||||
<span class="sd"> from starlette.middleware.httpsredirect import HTTPSRedirectMiddleware</span>
|
||||
<span class="sd"> api.add_middleware(HTTPSRedirectMiddleware)</span>
|
||||
|
||||
<span class="sd"> """</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">app</span> <span class="o">=</span> <span class="n">middleware_cls</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">app</span><span class="p">,</span> <span class="o">**</span><span class="n">middleware_config</span><span class="p">)</span></div>
|
||||
|
||||
|
||||
<div class="viewcode-block" id="API.exception_handler">
|
||||
<a class="viewcode-back" href="../../api.html#responder.API.exception_handler">[docs]</a>
|
||||
<span class="k">def</span><span class="w"> </span><span class="nf">exception_handler</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">exception_cls</span><span class="p">):</span>
|
||||
<span class="w"> </span><span class="sd">"""Register a handler for a specific exception type.</span>
|
||||
|
||||
<span class="sd"> Usage::</span>
|
||||
|
||||
<span class="sd"> @api.exception_handler(ValueError)</span>
|
||||
<span class="sd"> async def handle_value_error(req, resp, exc):</span>
|
||||
<span class="sd"> resp.status_code = 400</span>
|
||||
<span class="sd"> resp.media = {"error": str(exc)}</span>
|
||||
|
||||
<span class="sd"> """</span>
|
||||
|
||||
<span class="k">def</span><span class="w"> </span><span class="nf">decorator</span><span class="p">(</span><span class="n">func</span><span class="p">):</span>
|
||||
<span class="k">async</span> <span class="k">def</span><span class="w"> </span><span class="nf">_handler</span><span class="p">(</span><span class="n">request</span><span class="p">,</span> <span class="n">exc</span><span class="p">):</span>
|
||||
<span class="kn">from</span><span class="w"> </span><span class="nn">starlette.responses</span><span class="w"> </span><span class="kn">import</span> <span class="n">Response</span> <span class="k">as</span> <span class="n">StarletteResp</span>
|
||||
|
||||
<span class="n">req</span> <span class="o">=</span> <span class="n">Request</span><span class="p">(</span><span class="n">request</span><span class="o">.</span><span class="n">scope</span><span class="p">,</span> <span class="n">request</span><span class="o">.</span><span class="n">receive</span><span class="p">,</span> <span class="n">formats</span><span class="o">=</span><span class="n">get_formats</span><span class="p">())</span>
|
||||
<span class="n">resp</span> <span class="o">=</span> <span class="n">Response</span><span class="p">(</span><span class="n">req</span><span class="o">=</span><span class="n">req</span><span class="p">,</span> <span class="n">formats</span><span class="o">=</span><span class="n">get_formats</span><span class="p">())</span>
|
||||
<span class="k">if</span> <span class="n">inspect</span><span class="o">.</span><span class="n">iscoroutinefunction</span><span class="p">(</span><span class="n">func</span><span class="p">):</span>
|
||||
<span class="k">await</span> <span class="n">func</span><span class="p">(</span><span class="n">req</span><span class="p">,</span> <span class="n">resp</span><span class="p">,</span> <span class="n">exc</span><span class="p">)</span>
|
||||
<span class="k">else</span><span class="p">:</span>
|
||||
<span class="n">func</span><span class="p">(</span><span class="n">req</span><span class="p">,</span> <span class="n">resp</span><span class="p">,</span> <span class="n">exc</span><span class="p">)</span>
|
||||
<span class="k">if</span> <span class="n">resp</span><span class="o">.</span><span class="n">status_code</span> <span class="ow">is</span> <span class="kc">None</span><span class="p">:</span>
|
||||
<span class="n">resp</span><span class="o">.</span><span class="n">status_code</span> <span class="o">=</span> <span class="mi">500</span>
|
||||
<span class="n">body</span><span class="p">,</span> <span class="n">headers</span> <span class="o">=</span> <span class="k">await</span> <span class="n">resp</span><span class="o">.</span><span class="n">body</span>
|
||||
<span class="k">return</span> <span class="n">StarletteResp</span><span class="p">(</span>
|
||||
<span class="n">content</span><span class="o">=</span><span class="n">body</span><span class="p">,</span> <span class="n">status_code</span><span class="o">=</span><span class="n">resp</span><span class="o">.</span><span class="n">status_code</span><span class="p">,</span> <span class="n">headers</span><span class="o">=</span><span class="n">headers</span>
|
||||
<span class="p">)</span>
|
||||
|
||||
<span class="c1"># Register with the ExceptionMiddleware</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">router</span><span class="o">.</span><span class="n">_exception_handlers</span> <span class="o">=</span> <span class="nb">getattr</span><span class="p">(</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">router</span><span class="p">,</span> <span class="s2">"_exception_handlers"</span><span class="p">,</span> <span class="p">{}</span>
|
||||
<span class="p">)</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">router</span><span class="o">.</span><span class="n">_exception_handlers</span><span class="p">[</span><span class="n">exception_cls</span><span class="p">]</span> <span class="o">=</span> <span class="n">_handler</span>
|
||||
<span class="c1"># Also register on the ASGI app chain</span>
|
||||
<span class="kn">from</span><span class="w"> </span><span class="nn">starlette.middleware.exceptions</span><span class="w"> </span><span class="kn">import</span> <span class="n">ExceptionMiddleware</span> <span class="k">as</span> <span class="n">EM</span>
|
||||
|
||||
<span class="n">app</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">app</span>
|
||||
<span class="k">while</span> <span class="n">app</span> <span class="ow">is</span> <span class="ow">not</span> <span class="kc">None</span><span class="p">:</span>
|
||||
<span class="k">if</span> <span class="nb">isinstance</span><span class="p">(</span><span class="n">app</span><span class="p">,</span> <span class="n">EM</span><span class="p">):</span>
|
||||
<span class="n">app</span><span class="o">.</span><span class="n">add_exception_handler</span><span class="p">(</span><span class="n">exception_cls</span><span class="p">,</span> <span class="n">_handler</span><span class="p">)</span>
|
||||
<span class="k">break</span>
|
||||
<span class="n">app</span> <span class="o">=</span> <span class="nb">getattr</span><span class="p">(</span><span class="n">app</span><span class="p">,</span> <span class="s2">"app"</span><span class="p">,</span> <span class="kc">None</span><span class="p">)</span>
|
||||
<span class="k">return</span> <span class="n">func</span>
|
||||
|
||||
<span class="k">return</span> <span class="n">decorator</span></div>
|
||||
|
||||
|
||||
<div class="viewcode-block" id="API.schema">
|
||||
<a class="viewcode-back" href="../../api.html#responder.API.schema">[docs]</a>
|
||||
<span class="k">def</span><span class="w"> </span><span class="nf">schema</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">name</span><span class="p">,</span> <span class="o">**</span><span class="n">options</span><span class="p">):</span>
|
||||
<span class="w"> </span><span class="sd">"""</span>
|
||||
<span class="sd"> Decorator for creating new routes around function and class definitions.</span>
|
||||
|
||||
<span class="sd"> Usage::</span>
|
||||
|
||||
<span class="sd"> from marshmallow import Schema, fields</span>
|
||||
<span class="sd"> @api.schema("Pet")</span>
|
||||
<span class="sd"> class PetSchema(Schema):</span>
|
||||
<span class="sd"> name = fields.Str()</span>
|
||||
<span class="sd"> """</span>
|
||||
|
||||
<span class="k">def</span><span class="w"> </span><span class="nf">decorator</span><span class="p">(</span><span class="n">f</span><span class="p">):</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">openapi</span><span class="o">.</span><span class="n">add_schema</span><span class="p">(</span><span class="n">name</span><span class="o">=</span><span class="n">name</span><span class="p">,</span> <span class="n">schema</span><span class="o">=</span><span class="n">f</span><span class="p">,</span> <span class="o">**</span><span class="n">options</span><span class="p">)</span>
|
||||
<span class="k">return</span> <span class="n">f</span>
|
||||
|
||||
<span class="k">return</span> <span class="n">decorator</span></div>
|
||||
|
||||
|
||||
<div class="viewcode-block" id="API.path_matches_route">
|
||||
<a class="viewcode-back" href="../../api.html#responder.API.path_matches_route">[docs]</a>
|
||||
<span class="k">def</span><span class="w"> </span><span class="nf">path_matches_route</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">path</span><span class="p">):</span>
|
||||
<span class="w"> </span><span class="sd">"""Given a path portion of a URL, tests that it matches against any registered route.</span>
|
||||
|
||||
<span class="sd"> :param path: The path portion of a URL, to test all known routes against.</span>
|
||||
<span class="sd"> """</span> <span class="c1"># noqa: E501 (Line too long)</span>
|
||||
<span class="k">for</span> <span class="n">route</span> <span class="ow">in</span> <span class="bp">self</span><span class="o">.</span><span class="n">router</span><span class="o">.</span><span class="n">routes</span><span class="p">:</span>
|
||||
<span class="n">match</span><span class="p">,</span> <span class="n">_</span> <span class="o">=</span> <span class="n">route</span><span class="o">.</span><span class="n">matches</span><span class="p">(</span><span class="n">path</span><span class="p">)</span>
|
||||
<span class="k">if</span> <span class="n">match</span><span class="p">:</span>
|
||||
<span class="k">return</span> <span class="n">route</span>
|
||||
<span class="k">return</span> <span class="kc">None</span></div>
|
||||
|
||||
|
||||
<div class="viewcode-block" id="API.add_route">
|
||||
<a class="viewcode-back" href="../../api.html#responder.API.add_route">[docs]</a>
|
||||
<span class="k">def</span><span class="w"> </span><span class="nf">add_route</span><span class="p">(</span>
|
||||
<span class="bp">self</span><span class="p">,</span>
|
||||
<span class="n">route</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span>
|
||||
<span class="n">endpoint</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span>
|
||||
<span class="o">*</span><span class="p">,</span>
|
||||
<span class="n">default</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span>
|
||||
<span class="n">static</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span>
|
||||
<span class="n">check_existing</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span>
|
||||
<span class="n">websocket</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span>
|
||||
<span class="n">before_request</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span>
|
||||
<span class="n">methods</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span>
|
||||
<span class="p">):</span>
|
||||
<span class="w"> </span><span class="sd">"""Adds a route to the API.</span>
|
||||
|
||||
<span class="sd"> :param route: A string representation of the route.</span>
|
||||
<span class="sd"> :param endpoint: The endpoint for the route -- can be a callable, or a class.</span>
|
||||
<span class="sd"> :param default: If ``True``, all unknown requests will route to this view.</span>
|
||||
<span class="sd"> :param static: If ``True``, and no endpoint was passed, render "static/index.html".</span>
|
||||
<span class="sd"> Also, it will become a default route.</span>
|
||||
<span class="sd"> :param methods: Optional list of HTTP methods (e.g. ``["GET", "POST"]``).</span>
|
||||
<span class="sd"> """</span> <span class="c1"># noqa: E501</span>
|
||||
|
||||
<span class="k">if</span> <span class="n">static</span><span class="p">:</span>
|
||||
<span class="k">assert</span> <span class="bp">self</span><span class="o">.</span><span class="n">static_dir</span> <span class="ow">is</span> <span class="ow">not</span> <span class="kc">None</span>
|
||||
<span class="k">if</span> <span class="ow">not</span> <span class="n">endpoint</span><span class="p">:</span>
|
||||
<span class="n">endpoint</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_static_response</span>
|
||||
<span class="n">default</span> <span class="o">=</span> <span class="kc">True</span>
|
||||
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">router</span><span class="o">.</span><span class="n">add_route</span><span class="p">(</span>
|
||||
<span class="n">route</span><span class="p">,</span>
|
||||
<span class="n">endpoint</span><span class="p">,</span>
|
||||
<span class="n">default</span><span class="o">=</span><span class="n">default</span><span class="p">,</span>
|
||||
<span class="n">websocket</span><span class="o">=</span><span class="n">websocket</span><span class="p">,</span>
|
||||
<span class="n">before_request</span><span class="o">=</span><span class="n">before_request</span><span class="p">,</span>
|
||||
<span class="n">check_existing</span><span class="o">=</span><span class="n">check_existing</span><span class="p">,</span>
|
||||
<span class="n">methods</span><span class="o">=</span><span class="n">methods</span><span class="p">,</span>
|
||||
<span class="p">)</span></div>
|
||||
|
||||
|
||||
<span class="k">async</span> <span class="k">def</span><span class="w"> </span><span class="nf">_static_response</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">req</span><span class="p">,</span> <span class="n">resp</span><span class="p">):</span>
|
||||
<span class="k">assert</span> <span class="bp">self</span><span class="o">.</span><span class="n">static_dir</span> <span class="ow">is</span> <span class="ow">not</span> <span class="kc">None</span>
|
||||
|
||||
<span class="n">index</span> <span class="o">=</span> <span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">static_dir</span> <span class="o">/</span> <span class="s2">"index.html"</span><span class="p">)</span><span class="o">.</span><span class="n">resolve</span><span class="p">()</span>
|
||||
<span class="k">if</span> <span class="n">index</span><span class="o">.</span><span class="n">exists</span><span class="p">():</span>
|
||||
<span class="n">resp</span><span class="o">.</span><span class="n">html</span> <span class="o">=</span> <span class="n">index</span><span class="o">.</span><span class="n">read_text</span><span class="p">()</span>
|
||||
<span class="k">else</span><span class="p">:</span>
|
||||
<span class="n">resp</span><span class="o">.</span><span class="n">status_code</span> <span class="o">=</span> <span class="n">status_codes</span><span class="o">.</span><span class="n">HTTP_404</span> <span class="c1"># type: ignore[attr-defined]</span>
|
||||
<span class="n">resp</span><span class="o">.</span><span class="n">text</span> <span class="o">=</span> <span class="s2">"Not found."</span>
|
||||
|
||||
<div class="viewcode-block" id="API.redirect">
|
||||
<a class="viewcode-back" href="../../api.html#responder.API.redirect">[docs]</a>
|
||||
<span class="k">def</span><span class="w"> </span><span class="nf">redirect</span><span class="p">(</span>
|
||||
<span class="bp">self</span><span class="p">,</span>
|
||||
<span class="n">resp</span><span class="p">,</span>
|
||||
<span class="n">location</span><span class="p">,</span>
|
||||
<span class="o">*</span><span class="p">,</span>
|
||||
<span class="n">set_text</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span>
|
||||
<span class="n">status_code</span><span class="o">=</span><span class="n">status_codes</span><span class="o">.</span><span class="n">HTTP_301</span><span class="p">,</span> <span class="c1"># type: ignore[attr-defined]</span>
|
||||
<span class="p">):</span>
|
||||
<span class="w"> </span><span class="sd">"""</span>
|
||||
<span class="sd"> Redirects a given response to a given location.</span>
|
||||
|
||||
<span class="sd"> :param resp: The Response to mutate.</span>
|
||||
<span class="sd"> :param location: The location of the redirect.</span>
|
||||
<span class="sd"> :param set_text: If ``True``, sets the Redirect body content automatically.</span>
|
||||
<span class="sd"> :param status_code: an `API.status_codes` attribute, or an integer,</span>
|
||||
<span class="sd"> representing the HTTP status code of the redirect.</span>
|
||||
<span class="sd"> """</span>
|
||||
<span class="n">resp</span><span class="o">.</span><span class="n">redirect</span><span class="p">(</span><span class="n">location</span><span class="p">,</span> <span class="n">set_text</span><span class="o">=</span><span class="n">set_text</span><span class="p">,</span> <span class="n">status_code</span><span class="o">=</span><span class="n">status_code</span><span class="p">)</span></div>
|
||||
|
||||
|
||||
<div class="viewcode-block" id="API.on_event">
|
||||
<a class="viewcode-back" href="../../api.html#responder.API.on_event">[docs]</a>
|
||||
<span class="k">def</span><span class="w"> </span><span class="nf">on_event</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">event_type</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="o">**</span><span class="n">args</span><span class="p">):</span>
|
||||
<span class="w"> </span><span class="sd">"""Decorator for registering functions or coroutines to run at certain events</span>
|
||||
<span class="sd"> Supported events: startup, shutdown</span>
|
||||
|
||||
<span class="sd"> Usage::</span>
|
||||
|
||||
<span class="sd"> @api.on_event('startup')</span>
|
||||
<span class="sd"> async def open_database_connection_pool():</span>
|
||||
<span class="sd"> ...</span>
|
||||
|
||||
<span class="sd"> @api.on_event('shutdown')</span>
|
||||
<span class="sd"> async def close_database_connection_pool():</span>
|
||||
<span class="sd"> ...</span>
|
||||
|
||||
<span class="sd"> """</span>
|
||||
|
||||
<span class="k">def</span><span class="w"> </span><span class="nf">decorator</span><span class="p">(</span><span class="n">func</span><span class="p">):</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">add_event_handler</span><span class="p">(</span><span class="n">event_type</span><span class="p">,</span> <span class="n">func</span><span class="p">,</span> <span class="o">**</span><span class="n">args</span><span class="p">)</span>
|
||||
<span class="k">return</span> <span class="n">func</span>
|
||||
|
||||
<span class="k">return</span> <span class="n">decorator</span></div>
|
||||
|
||||
|
||||
<div class="viewcode-block" id="API.add_event_handler">
|
||||
<a class="viewcode-back" href="../../api.html#responder.API.add_event_handler">[docs]</a>
|
||||
<span class="k">def</span><span class="w"> </span><span class="nf">add_event_handler</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">event_type</span><span class="p">,</span> <span class="n">handler</span><span class="p">):</span>
|
||||
<span class="w"> </span><span class="sd">"""Adds an event handler to the API.</span>
|
||||
|
||||
<span class="sd"> :param event_type: A string in ("startup", "shutdown")</span>
|
||||
<span class="sd"> :param handler: The function to run. Can be either a function or a coroutine.</span>
|
||||
<span class="sd"> """</span>
|
||||
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">router</span><span class="o">.</span><span class="n">add_event_handler</span><span class="p">(</span><span class="n">event_type</span><span class="p">,</span> <span class="n">handler</span><span class="p">)</span></div>
|
||||
|
||||
|
||||
<div class="viewcode-block" id="API.route">
|
||||
<a class="viewcode-back" href="../../api.html#responder.API.route">[docs]</a>
|
||||
<span class="k">def</span><span class="w"> </span><span class="nf">route</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">route</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span> <span class="o">*</span><span class="p">,</span> <span class="n">request_model</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span> <span class="n">response_model</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span> <span class="o">**</span><span class="n">options</span><span class="p">):</span>
|
||||
<span class="w"> </span><span class="sd">"""Decorator for creating new routes around function and class definitions.</span>
|
||||
|
||||
<span class="sd"> Usage::</span>
|
||||
|
||||
<span class="sd"> @api.route("/hello")</span>
|
||||
<span class="sd"> def hello(req, resp):</span>
|
||||
<span class="sd"> resp.text = "hello, world!"</span>
|
||||
|
||||
<span class="sd"> With Pydantic models for OpenAPI documentation::</span>
|
||||
|
||||
<span class="sd"> from pydantic import BaseModel</span>
|
||||
|
||||
<span class="sd"> class ItemIn(BaseModel):</span>
|
||||
<span class="sd"> name: str</span>
|
||||
<span class="sd"> price: float</span>
|
||||
|
||||
<span class="sd"> class ItemOut(BaseModel):</span>
|
||||
<span class="sd"> id: int</span>
|
||||
<span class="sd"> name: str</span>
|
||||
<span class="sd"> price: float</span>
|
||||
|
||||
<span class="sd"> @api.route("/items", methods=["POST"],</span>
|
||||
<span class="sd"> request_model=ItemIn, response_model=ItemOut)</span>
|
||||
<span class="sd"> async def create_item(req, resp):</span>
|
||||
<span class="sd"> data = await req.media()</span>
|
||||
<span class="sd"> resp.media = {"id": 1, **data}</span>
|
||||
|
||||
<span class="sd"> """</span>
|
||||
|
||||
<span class="k">def</span><span class="w"> </span><span class="nf">decorator</span><span class="p">(</span><span class="n">f</span><span class="p">):</span>
|
||||
<span class="k">if</span> <span class="n">request_model</span> <span class="ow">is</span> <span class="ow">not</span> <span class="kc">None</span><span class="p">:</span>
|
||||
<span class="n">f</span><span class="o">.</span><span class="n">_request_model</span> <span class="o">=</span> <span class="n">request_model</span>
|
||||
<span class="k">if</span> <span class="nb">hasattr</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="s2">"openapi"</span><span class="p">):</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">openapi</span><span class="o">.</span><span class="n">add_schema</span><span class="p">(</span>
|
||||
<span class="n">request_model</span><span class="o">.</span><span class="vm">__name__</span><span class="p">,</span> <span class="n">request_model</span><span class="p">,</span> <span class="n">check_existing</span><span class="o">=</span><span class="kc">False</span>
|
||||
<span class="p">)</span>
|
||||
<span class="k">if</span> <span class="n">response_model</span> <span class="ow">is</span> <span class="ow">not</span> <span class="kc">None</span><span class="p">:</span>
|
||||
<span class="n">f</span><span class="o">.</span><span class="n">_response_model</span> <span class="o">=</span> <span class="n">response_model</span>
|
||||
<span class="k">if</span> <span class="nb">hasattr</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="s2">"openapi"</span><span class="p">):</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">openapi</span><span class="o">.</span><span class="n">add_schema</span><span class="p">(</span>
|
||||
<span class="n">response_model</span><span class="o">.</span><span class="vm">__name__</span><span class="p">,</span> <span class="n">response_model</span><span class="p">,</span> <span class="n">check_existing</span><span class="o">=</span><span class="kc">False</span>
|
||||
<span class="p">)</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">add_route</span><span class="p">(</span><span class="n">route</span><span class="p">,</span> <span class="n">f</span><span class="p">,</span> <span class="o">**</span><span class="n">options</span><span class="p">)</span>
|
||||
<span class="k">return</span> <span class="n">f</span>
|
||||
|
||||
<span class="k">return</span> <span class="n">decorator</span></div>
|
||||
|
||||
|
||||
<div class="viewcode-block" id="API.graphql">
|
||||
<a class="viewcode-back" href="../../api.html#responder.API.graphql">[docs]</a>
|
||||
<span class="k">def</span><span class="w"> </span><span class="nf">graphql</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">route</span><span class="o">=</span><span class="s2">"/graphql"</span><span class="p">,</span> <span class="o">*</span><span class="p">,</span> <span class="n">schema</span><span class="p">):</span>
|
||||
<span class="w"> </span><span class="sd">"""Mount a GraphQL API at the given route.</span>
|
||||
|
||||
<span class="sd"> Usage::</span>
|
||||
|
||||
<span class="sd"> import graphene</span>
|
||||
|
||||
<span class="sd"> class Query(graphene.ObjectType):</span>
|
||||
<span class="sd"> hello = graphene.String(name=graphene.String(default_value="stranger"))</span>
|
||||
<span class="sd"> def resolve_hello(self, info, name):</span>
|
||||
<span class="sd"> return f"Hello {name}"</span>
|
||||
|
||||
<span class="sd"> api.graphql("/graphql", schema=graphene.Schema(query=Query))</span>
|
||||
|
||||
<span class="sd"> :param route: The URL path for the GraphQL endpoint.</span>
|
||||
<span class="sd"> :param schema: A Graphene schema instance.</span>
|
||||
<span class="sd"> """</span>
|
||||
<span class="kn">from</span><span class="w"> </span><span class="nn">.ext.graphql</span><span class="w"> </span><span class="kn">import</span> <span class="n">GraphQLView</span>
|
||||
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">add_route</span><span class="p">(</span><span class="n">route</span><span class="p">,</span> <span class="n">GraphQLView</span><span class="p">(</span><span class="n">api</span><span class="o">=</span><span class="bp">self</span><span class="p">,</span> <span class="n">schema</span><span class="o">=</span><span class="n">schema</span><span class="p">))</span></div>
|
||||
|
||||
|
||||
<div class="viewcode-block" id="API.mount">
|
||||
<a class="viewcode-back" href="../../api.html#responder.API.mount">[docs]</a>
|
||||
<span class="k">def</span><span class="w"> </span><span class="nf">mount</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">route</span><span class="p">,</span> <span class="n">app</span><span class="p">):</span>
|
||||
<span class="w"> </span><span class="sd">"""Mounts an WSGI / ASGI application at a given route.</span>
|
||||
|
||||
<span class="sd"> :param route: String representation of the route to be used</span>
|
||||
<span class="sd"> (shouldn't be parameterized).</span>
|
||||
<span class="sd"> :param app: The other WSGI / ASGI app.</span>
|
||||
<span class="sd"> """</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">router</span><span class="o">.</span><span class="n">apps</span><span class="o">.</span><span class="n">update</span><span class="p">({</span><span class="n">route</span><span class="p">:</span> <span class="n">app</span><span class="p">})</span></div>
|
||||
|
||||
|
||||
<div class="viewcode-block" id="API.session">
|
||||
<a class="viewcode-back" href="../../api.html#responder.API.session">[docs]</a>
|
||||
<span class="k">def</span><span class="w"> </span><span class="nf">session</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">base_url</span><span class="o">=</span><span class="s2">"http://;"</span><span class="p">):</span>
|
||||
<span class="w"> </span><span class="sd">"""Testing HTTP client. Returns a Starlette TestClient instance,</span>
|
||||
<span class="sd"> able to send HTTP requests to the Responder application.</span>
|
||||
|
||||
<span class="sd"> :param base_url: The base URL for the test client.</span>
|
||||
<span class="sd"> """</span>
|
||||
|
||||
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">_session</span> <span class="ow">is</span> <span class="kc">None</span><span class="p">:</span>
|
||||
<span class="kn">from</span><span class="w"> </span><span class="nn">starlette.testclient</span><span class="w"> </span><span class="kn">import</span> <span class="n">TestClient</span>
|
||||
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_session</span> <span class="o">=</span> <span class="n">TestClient</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">base_url</span><span class="o">=</span><span class="n">base_url</span><span class="p">)</span>
|
||||
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_session</span></div>
|
||||
|
||||
|
||||
<div class="viewcode-block" id="API.url_for">
|
||||
<a class="viewcode-back" href="../../api.html#responder.API.url_for">[docs]</a>
|
||||
<span class="k">def</span><span class="w"> </span><span class="nf">url_for</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">endpoint</span><span class="p">,</span> <span class="o">**</span><span class="n">params</span><span class="p">):</span>
|
||||
<span class="w"> </span><span class="sd">"""Given an endpoint, returns a rendered URL for its route.</span>
|
||||
|
||||
<span class="sd"> :param endpoint: The route endpoint you're searching for.</span>
|
||||
<span class="sd"> :param params: Data to pass into the URL generator (for parameterized URLs).</span>
|
||||
<span class="sd"> """</span>
|
||||
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">router</span><span class="o">.</span><span class="n">url_for</span><span class="p">(</span><span class="n">endpoint</span><span class="p">,</span> <span class="o">**</span><span class="n">params</span><span class="p">)</span></div>
|
||||
|
||||
|
||||
<div class="viewcode-block" id="API.template">
|
||||
<a class="viewcode-back" href="../../api.html#responder.API.template">[docs]</a>
|
||||
<span class="k">def</span><span class="w"> </span><span class="nf">template</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">filename</span><span class="p">,</span> <span class="o">*</span><span class="n">args</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">):</span>
|
||||
<span class="w"> </span><span class="sa">r</span><span class="sd">"""Render a Jinja2 template file with the provided values.</span>
|
||||
|
||||
<span class="sd"> :param filename: The filename of the jinja2 template, in ``templates_dir``.</span>
|
||||
<span class="sd"> :param \*args: Data to pass into the template.</span>
|
||||
<span class="sd"> :param \*\*kwargs: Data to pass into the template.</span>
|
||||
<span class="sd"> """</span>
|
||||
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">templates</span><span class="o">.</span><span class="n">render</span><span class="p">(</span><span class="n">filename</span><span class="p">,</span> <span class="o">*</span><span class="n">args</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">)</span></div>
|
||||
|
||||
|
||||
<div class="viewcode-block" id="API.template_string">
|
||||
<a class="viewcode-back" href="../../api.html#responder.API.template_string">[docs]</a>
|
||||
<span class="k">def</span><span class="w"> </span><span class="nf">template_string</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">source</span><span class="p">,</span> <span class="o">*</span><span class="n">args</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">):</span>
|
||||
<span class="w"> </span><span class="sa">r</span><span class="sd">"""Render a Jinja2 template string with the provided values.</span>
|
||||
|
||||
<span class="sd"> :param source: The template to use, a Jinja2 template string.</span>
|
||||
<span class="sd"> :param \*args: Data to pass into the template.</span>
|
||||
<span class="sd"> :param \*\*kwargs: Data to pass into the template.</span>
|
||||
<span class="sd"> """</span>
|
||||
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">templates</span><span class="o">.</span><span class="n">render_string</span><span class="p">(</span><span class="n">source</span><span class="p">,</span> <span class="o">*</span><span class="n">args</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">)</span></div>
|
||||
|
||||
|
||||
<div class="viewcode-block" id="API.serve">
|
||||
<a class="viewcode-back" href="../../api.html#responder.API.serve">[docs]</a>
|
||||
<span class="k">def</span><span class="w"> </span><span class="nf">serve</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="o">*</span><span class="p">,</span> <span class="n">address</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span> <span class="n">port</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span> <span class="n">debug</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span> <span class="o">**</span><span class="n">options</span><span class="p">):</span>
|
||||
<span class="w"> </span><span class="sd">"""</span>
|
||||
<span class="sd"> Run the application with uvicorn.</span>
|
||||
|
||||
<span class="sd"> If the ``PORT`` environment variable is set, requests will be served on that port</span>
|
||||
<span class="sd"> automatically to all known hosts.</span>
|
||||
|
||||
<span class="sd"> :param address: The address to bind to.</span>
|
||||
<span class="sd"> :param port: The port to bind to. If none is provided, one will be selected at random.</span>
|
||||
<span class="sd"> :param debug: Whether to run application in debug mode.</span>
|
||||
<span class="sd"> :param options: Additional keyword arguments to send to ``uvicorn.run()``.</span>
|
||||
<span class="sd"> """</span> <span class="c1"># noqa: E501</span>
|
||||
|
||||
<span class="k">if</span> <span class="s2">"PORT"</span> <span class="ow">in</span> <span class="n">os</span><span class="o">.</span><span class="n">environ</span><span class="p">:</span>
|
||||
<span class="k">if</span> <span class="n">address</span> <span class="ow">is</span> <span class="kc">None</span><span class="p">:</span>
|
||||
<span class="n">address</span> <span class="o">=</span> <span class="s2">"0.0.0.0"</span> <span class="c1"># noqa: S104</span>
|
||||
<span class="n">port</span> <span class="o">=</span> <span class="nb">int</span><span class="p">(</span><span class="n">os</span><span class="o">.</span><span class="n">environ</span><span class="p">[</span><span class="s2">"PORT"</span><span class="p">])</span>
|
||||
|
||||
<span class="k">if</span> <span class="n">address</span> <span class="ow">is</span> <span class="kc">None</span><span class="p">:</span>
|
||||
<span class="n">address</span> <span class="o">=</span> <span class="s2">"127.0.0.1"</span>
|
||||
<span class="k">if</span> <span class="n">port</span> <span class="ow">is</span> <span class="kc">None</span><span class="p">:</span>
|
||||
<span class="n">port</span> <span class="o">=</span> <span class="mi">5042</span>
|
||||
<span class="k">if</span> <span class="n">debug</span><span class="p">:</span>
|
||||
<span class="n">options</span><span class="p">[</span><span class="s2">"log_level"</span><span class="p">]</span> <span class="o">=</span> <span class="s2">"debug"</span>
|
||||
|
||||
<span class="n">uvicorn</span><span class="o">.</span><span class="n">run</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">host</span><span class="o">=</span><span class="n">address</span><span class="p">,</span> <span class="n">port</span><span class="o">=</span><span class="n">port</span><span class="p">,</span> <span class="o">**</span><span class="n">options</span><span class="p">)</span></div>
|
||||
|
||||
|
||||
<div class="viewcode-block" id="API.run">
|
||||
<a class="viewcode-back" href="../../api.html#responder.API.run">[docs]</a>
|
||||
<span class="k">def</span><span class="w"> </span><span class="nf">run</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">):</span>
|
||||
<span class="w"> </span><span class="sd">"""Run the application. Shorthand for :meth:`serve` that inherits the ``debug`` setting.</span>
|
||||
|
||||
<span class="sd"> :param kwargs: Keyword arguments passed through to :meth:`serve`.</span>
|
||||
<span class="sd"> """</span> <span class="c1"># noqa: E501</span>
|
||||
<span class="k">if</span> <span class="s2">"debug"</span> <span class="ow">not</span> <span class="ow">in</span> <span class="n">kwargs</span><span class="p">:</span>
|
||||
<span class="n">kwargs</span><span class="o">.</span><span class="n">update</span><span class="p">({</span><span class="s2">"debug"</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">debug</span><span class="p">})</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">serve</span><span class="p">(</span><span class="o">**</span><span class="n">kwargs</span><span class="p">)</span></div>
|
||||
|
||||
|
||||
<div class="viewcode-block" id="API.group">
|
||||
<a class="viewcode-back" href="../../api.html#responder.API.group">[docs]</a>
|
||||
<span class="k">def</span><span class="w"> </span><span class="nf">group</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">prefix</span><span class="p">):</span>
|
||||
<span class="w"> </span><span class="sd">"""Create a route group with a shared URL prefix.</span>
|
||||
|
||||
<span class="sd"> Usage::</span>
|
||||
|
||||
<span class="sd"> v1 = api.group("/v1")</span>
|
||||
|
||||
<span class="sd"> @v1.route("/users")</span>
|
||||
<span class="sd"> def list_users(req, resp):</span>
|
||||
<span class="sd"> resp.media = []</span>
|
||||
|
||||
<span class="sd"> @v1.route("/users/{id:int}")</span>
|
||||
<span class="sd"> def get_user(req, resp, *, id):</span>
|
||||
<span class="sd"> resp.media = {"id": id}</span>
|
||||
|
||||
<span class="sd"> """</span>
|
||||
<span class="k">return</span> <span class="n">RouteGroup</span><span class="p">(</span><span class="n">api</span><span class="o">=</span><span class="bp">self</span><span class="p">,</span> <span class="n">prefix</span><span class="o">=</span><span class="n">prefix</span><span class="p">)</span></div>
|
||||
|
||||
|
||||
<span class="k">async</span> <span class="k">def</span><span class="w"> </span><span class="fm">__call__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">scope</span><span class="p">,</span> <span class="n">receive</span><span class="p">,</span> <span class="n">send</span><span class="p">):</span>
|
||||
<span class="k">await</span> <span class="bp">self</span><span class="o">.</span><span class="n">app</span><span class="p">(</span><span class="n">scope</span><span class="p">,</span> <span class="n">receive</span><span class="p">,</span> <span class="n">send</span><span class="p">)</span></div>
|
||||
|
||||
|
||||
|
||||
<div class="viewcode-block" id="RouteGroup">
|
||||
<a class="viewcode-back" href="../../api.html#responder.RouteGroup">[docs]</a>
|
||||
<span class="k">class</span><span class="w"> </span><span class="nc">RouteGroup</span><span class="p">:</span>
|
||||
<span class="w"> </span><span class="sd">"""A group of routes with a shared URL prefix."""</span>
|
||||
|
||||
<span class="k">def</span><span class="w"> </span><span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">api</span><span class="p">,</span> <span class="n">prefix</span><span class="p">):</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">api</span> <span class="o">=</span> <span class="n">api</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">prefix</span> <span class="o">=</span> <span class="n">prefix</span><span class="o">.</span><span class="n">rstrip</span><span class="p">(</span><span class="s2">"/"</span><span class="p">)</span>
|
||||
|
||||
<span class="k">def</span><span class="w"> </span><span class="nf">route</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">route</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span> <span class="o">**</span><span class="n">options</span><span class="p">):</span>
|
||||
<span class="n">full_route</span> <span class="o">=</span> <span class="sa">f</span><span class="s2">"</span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">prefix</span><span class="si">}{</span><span class="n">route</span><span class="si">}</span><span class="s2">"</span>
|
||||
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">api</span><span class="o">.</span><span class="n">route</span><span class="p">(</span><span class="n">full_route</span><span class="p">,</span> <span class="o">**</span><span class="n">options</span><span class="p">)</span>
|
||||
|
||||
<span class="k">def</span><span class="w"> </span><span class="nf">before_request</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">):</span>
|
||||
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">api</span><span class="o">.</span><span class="n">before_request</span><span class="p">(</span><span class="o">**</span><span class="n">kwargs</span><span class="p">)</span></div>
|
||||
|
||||
</pre></div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="sphinxsidebar" role="navigation" aria-label="Main">
|
||||
<div class="sphinxsidebarwrapper"><p class="logo">
|
||||
<a href="../../index.html">
|
||||
<img class="logo" src="../../_static/responder.png" />
|
||||
</a>
|
||||
</p>
|
||||
<p>
|
||||
<strong>Responder</strong> — a familiar HTTP service framework for Python.
|
||||
<br />
|
||||
<small>v3.6.2</small>
|
||||
</p>
|
||||
<h3>Useful Links</h3>
|
||||
<ul>
|
||||
<li><a href="https://github.com/kennethreitz/responder">Responder @ GitHub</a></li>
|
||||
<li><a href="https://pypi.org/project/responder/">Responder @ PyPI</a></li>
|
||||
<li><a href="https://github.com/kennethreitz/responder/issues">Issue Tracker</a></li>
|
||||
</ul>
|
||||
<search id="searchbox" style="display: none" role="search">
|
||||
<h3 id="searchlabel">Quick search</h3>
|
||||
<div class="searchformwrapper">
|
||||
<form class="search" action="../../search.html" method="get">
|
||||
<input type="text" name="q" aria-labelledby="searchlabel" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"/>
|
||||
<input type="submit" value="Go" />
|
||||
</form>
|
||||
</div>
|
||||
</search>
|
||||
<script>document.getElementById('searchbox').style.display = "block"</script>
|
||||
</div>
|
||||
</div>
|
||||
<div class="clearer"></div>
|
||||
</div>
|
||||
<div class="footer">
|
||||
©2018-2026, Kenneth Reitz.
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,178 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html lang="en" data-content_root="../../">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>responder.background — responder 3.6.2 documentation</title>
|
||||
<link rel="stylesheet" type="text/css" href="../../_static/pygments.css?v=5ecbeea2" />
|
||||
<link rel="stylesheet" type="text/css" href="../../_static/basic.css?v=b08954a9" />
|
||||
<link rel="stylesheet" type="text/css" href="../../_static/alabaster.css?v=27fed22d" />
|
||||
<link rel="stylesheet" type="text/css" href="../../_static/copybutton.css?v=76b2166b" />
|
||||
<link rel="stylesheet" type="text/css" href="../../_static/design-elements.e5416f61bae5d36adc6d722a2b6f8cff.css?v=452a8e97" />
|
||||
<script src="../../_static/documentation_options.js?v=c0c9fa11"></script>
|
||||
<script src="../../_static/doctools.js?v=9bcbadda"></script>
|
||||
<script src="../../_static/sphinx_highlight.js?v=dc90522c"></script>
|
||||
<script src="../../_static/clipboard.min.js?v=a7894cd8"></script>
|
||||
<script src="../../_static/copybutton.js?v=fd10adb8"></script>
|
||||
<script>
|
||||
</script>
|
||||
<script src="../../_static/design-elements.bbdccc18c4abea9397628f9fea3d48c2.js?v=03c7770e"></script>
|
||||
<link rel="index" title="Index" href="../../genindex.html" />
|
||||
<link rel="search" title="Search" href="../../search.html" />
|
||||
|
||||
<link rel="stylesheet" href="../../_static/custom.css" type="text/css" />
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</head><body>
|
||||
|
||||
|
||||
<div class="document">
|
||||
<div class="documentwrapper">
|
||||
<div class="bodywrapper">
|
||||
|
||||
|
||||
<div class="body" role="main">
|
||||
|
||||
<h1>Source code for responder.background</h1><div class="highlight"><pre>
|
||||
<span></span><span class="kn">import</span><span class="w"> </span><span class="nn">asyncio</span>
|
||||
<span class="kn">import</span><span class="w"> </span><span class="nn">concurrent.futures</span>
|
||||
<span class="kn">import</span><span class="w"> </span><span class="nn">inspect</span>
|
||||
<span class="kn">import</span><span class="w"> </span><span class="nn">multiprocessing</span>
|
||||
<span class="kn">import</span><span class="w"> </span><span class="nn">traceback</span>
|
||||
|
||||
<span class="kn">from</span><span class="w"> </span><span class="nn">starlette.concurrency</span><span class="w"> </span><span class="kn">import</span> <span class="n">run_in_threadpool</span>
|
||||
|
||||
<span class="n">__all__</span> <span class="o">=</span> <span class="p">[</span><span class="s2">"BackgroundQueue"</span><span class="p">]</span>
|
||||
|
||||
|
||||
<div class="viewcode-block" id="BackgroundQueue">
|
||||
<a class="viewcode-back" href="../../api.html#responder.background.BackgroundQueue">[docs]</a>
|
||||
<span class="k">class</span><span class="w"> </span><span class="nc">BackgroundQueue</span><span class="p">:</span>
|
||||
<span class="w"> </span><span class="sd">"""A queue for running tasks in background threads.</span>
|
||||
|
||||
<span class="sd"> Uses a ``ThreadPoolExecutor`` sized to the number of CPUs. Access it</span>
|
||||
<span class="sd"> via ``api.background``.</span>
|
||||
|
||||
<span class="sd"> Usage::</span>
|
||||
|
||||
<span class="sd"> # As a decorator — fire and forget</span>
|
||||
<span class="sd"> @api.background.task</span>
|
||||
<span class="sd"> def send_email(to, subject):</span>
|
||||
<span class="sd"> ...</span>
|
||||
|
||||
<span class="sd"> send_email("user@example.com", "Hello")</span>
|
||||
|
||||
<span class="sd"> # Direct submission</span>
|
||||
<span class="sd"> future = api.background.run(send_email, "user@example.com", "Hello")</span>
|
||||
|
||||
<span class="sd"> # As a callable (supports async functions)</span>
|
||||
<span class="sd"> await api.background(send_email, "user@example.com", "Hello")</span>
|
||||
|
||||
<span class="sd"> """</span>
|
||||
|
||||
<span class="k">def</span><span class="w"> </span><span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">n</span><span class="o">=</span><span class="kc">None</span><span class="p">):</span>
|
||||
<span class="w"> </span><span class="sd">"""Create a new background queue.</span>
|
||||
|
||||
<span class="sd"> :param n: Number of worker threads. Defaults to CPU count.</span>
|
||||
<span class="sd"> """</span>
|
||||
<span class="k">if</span> <span class="n">n</span> <span class="ow">is</span> <span class="kc">None</span><span class="p">:</span>
|
||||
<span class="n">n</span> <span class="o">=</span> <span class="n">multiprocessing</span><span class="o">.</span><span class="n">cpu_count</span><span class="p">()</span>
|
||||
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">n</span> <span class="o">=</span> <span class="n">n</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">pool</span> <span class="o">=</span> <span class="n">concurrent</span><span class="o">.</span><span class="n">futures</span><span class="o">.</span><span class="n">ThreadPoolExecutor</span><span class="p">(</span><span class="n">max_workers</span><span class="o">=</span><span class="n">n</span><span class="p">)</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">results</span> <span class="o">=</span> <span class="p">[]</span>
|
||||
|
||||
<div class="viewcode-block" id="BackgroundQueue.run">
|
||||
<a class="viewcode-back" href="../../api.html#responder.background.BackgroundQueue.run">[docs]</a>
|
||||
<span class="k">def</span><span class="w"> </span><span class="nf">run</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">f</span><span class="p">,</span> <span class="o">*</span><span class="n">args</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">):</span>
|
||||
<span class="w"> </span><span class="sd">"""Submit a function to run in a background thread.</span>
|
||||
|
||||
<span class="sd"> :param f: The function to run.</span>
|
||||
<span class="sd"> :returns: A ``concurrent.futures.Future`` for the result.</span>
|
||||
<span class="sd"> """</span>
|
||||
<span class="n">f</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">pool</span><span class="o">.</span><span class="n">submit</span><span class="p">(</span><span class="n">f</span><span class="p">,</span> <span class="o">*</span><span class="n">args</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">)</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">results</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">f</span><span class="p">)</span>
|
||||
<span class="k">return</span> <span class="n">f</span></div>
|
||||
|
||||
|
||||
<div class="viewcode-block" id="BackgroundQueue.task">
|
||||
<a class="viewcode-back" href="../../api.html#responder.background.BackgroundQueue.task">[docs]</a>
|
||||
<span class="k">def</span><span class="w"> </span><span class="nf">task</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">f</span><span class="p">):</span>
|
||||
<span class="w"> </span><span class="sd">"""Decorator that wraps a function to run in the background thread pool.</span>
|
||||
|
||||
<span class="sd"> The decorated function returns a ``Future`` instead of blocking.</span>
|
||||
<span class="sd"> Exceptions are printed to stderr via traceback.</span>
|
||||
|
||||
<span class="sd"> :param f: The function to wrap.</span>
|
||||
<span class="sd"> """</span>
|
||||
|
||||
<span class="k">def</span><span class="w"> </span><span class="nf">on_future_done</span><span class="p">(</span><span class="n">fs</span><span class="p">):</span>
|
||||
<span class="k">try</span><span class="p">:</span>
|
||||
<span class="n">fs</span><span class="o">.</span><span class="n">result</span><span class="p">()</span>
|
||||
<span class="k">except</span> <span class="ne">Exception</span><span class="p">:</span>
|
||||
<span class="n">traceback</span><span class="o">.</span><span class="n">print_exc</span><span class="p">()</span>
|
||||
|
||||
<span class="k">def</span><span class="w"> </span><span class="nf">do_task</span><span class="p">(</span><span class="o">*</span><span class="n">args</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">):</span>
|
||||
<span class="n">result</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">run</span><span class="p">(</span><span class="n">f</span><span class="p">,</span> <span class="o">*</span><span class="n">args</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">)</span>
|
||||
<span class="n">result</span><span class="o">.</span><span class="n">add_done_callback</span><span class="p">(</span><span class="n">on_future_done</span><span class="p">)</span>
|
||||
<span class="k">return</span> <span class="n">result</span>
|
||||
|
||||
<span class="k">return</span> <span class="n">do_task</span></div>
|
||||
|
||||
|
||||
<span class="k">async</span> <span class="k">def</span><span class="w"> </span><span class="fm">__call__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">func</span><span class="p">,</span> <span class="o">*</span><span class="n">args</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">)</span> <span class="o">-></span> <span class="kc">None</span><span class="p">:</span>
|
||||
<span class="k">if</span> <span class="n">inspect</span><span class="o">.</span><span class="n">iscoroutinefunction</span><span class="p">(</span><span class="n">func</span><span class="p">):</span>
|
||||
<span class="k">return</span> <span class="k">await</span> <span class="n">asyncio</span><span class="o">.</span><span class="n">create_task</span><span class="p">(</span><span class="n">func</span><span class="p">(</span><span class="o">*</span><span class="n">args</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">))</span>
|
||||
<span class="k">return</span> <span class="k">await</span> <span class="n">run_in_threadpool</span><span class="p">(</span><span class="n">func</span><span class="p">,</span> <span class="o">*</span><span class="n">args</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">)</span></div>
|
||||
|
||||
</pre></div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="sphinxsidebar" role="navigation" aria-label="Main">
|
||||
<div class="sphinxsidebarwrapper"><p class="logo">
|
||||
<a href="../../index.html">
|
||||
<img class="logo" src="../../_static/responder.png" />
|
||||
</a>
|
||||
</p>
|
||||
<p>
|
||||
<strong>Responder</strong> — a familiar HTTP service framework for Python.
|
||||
<br />
|
||||
<small>v3.6.2</small>
|
||||
</p>
|
||||
<h3>Useful Links</h3>
|
||||
<ul>
|
||||
<li><a href="https://github.com/kennethreitz/responder">Responder @ GitHub</a></li>
|
||||
<li><a href="https://pypi.org/project/responder/">Responder @ PyPI</a></li>
|
||||
<li><a href="https://github.com/kennethreitz/responder/issues">Issue Tracker</a></li>
|
||||
</ul>
|
||||
<search id="searchbox" style="display: none" role="search">
|
||||
<h3 id="searchlabel">Quick search</h3>
|
||||
<div class="searchformwrapper">
|
||||
<form class="search" action="../../search.html" method="get">
|
||||
<input type="text" name="q" aria-labelledby="searchlabel" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"/>
|
||||
<input type="submit" value="Go" />
|
||||
</form>
|
||||
</div>
|
||||
</search>
|
||||
<script>document.getElementById('searchbox').style.display = "block"</script>
|
||||
</div>
|
||||
</div>
|
||||
<div class="clearer"></div>
|
||||
</div>
|
||||
<div class="footer">
|
||||
©2018-2026, Kenneth Reitz.
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,168 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html lang="en" data-content_root="../../../">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>responder.ext.ratelimit — responder 3.6.2 documentation</title>
|
||||
<link rel="stylesheet" type="text/css" href="../../../_static/pygments.css?v=5ecbeea2" />
|
||||
<link rel="stylesheet" type="text/css" href="../../../_static/basic.css?v=b08954a9" />
|
||||
<link rel="stylesheet" type="text/css" href="../../../_static/alabaster.css?v=27fed22d" />
|
||||
<link rel="stylesheet" type="text/css" href="../../../_static/copybutton.css?v=76b2166b" />
|
||||
<link rel="stylesheet" type="text/css" href="../../../_static/design-elements.e5416f61bae5d36adc6d722a2b6f8cff.css?v=452a8e97" />
|
||||
<script src="../../../_static/documentation_options.js?v=c0c9fa11"></script>
|
||||
<script src="../../../_static/doctools.js?v=9bcbadda"></script>
|
||||
<script src="../../../_static/sphinx_highlight.js?v=dc90522c"></script>
|
||||
<script src="../../../_static/clipboard.min.js?v=a7894cd8"></script>
|
||||
<script src="../../../_static/copybutton.js?v=fd10adb8"></script>
|
||||
<script>
|
||||
</script>
|
||||
<script src="../../../_static/design-elements.bbdccc18c4abea9397628f9fea3d48c2.js?v=03c7770e"></script>
|
||||
<link rel="index" title="Index" href="../../../genindex.html" />
|
||||
<link rel="search" title="Search" href="../../../search.html" />
|
||||
|
||||
<link rel="stylesheet" href="../../../_static/custom.css" type="text/css" />
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</head><body>
|
||||
|
||||
|
||||
<div class="document">
|
||||
<div class="documentwrapper">
|
||||
<div class="bodywrapper">
|
||||
|
||||
|
||||
<div class="body" role="main">
|
||||
|
||||
<h1>Source code for responder.ext.ratelimit</h1><div class="highlight"><pre>
|
||||
<span></span><span class="sd">"""Simple in-memory rate limiter for Responder."""</span>
|
||||
|
||||
<span class="kn">import</span><span class="w"> </span><span class="nn">threading</span>
|
||||
<span class="kn">import</span><span class="w"> </span><span class="nn">time</span>
|
||||
<span class="kn">from</span><span class="w"> </span><span class="nn">collections</span><span class="w"> </span><span class="kn">import</span> <span class="n">defaultdict</span>
|
||||
|
||||
|
||||
<div class="viewcode-block" id="RateLimiter">
|
||||
<a class="viewcode-back" href="../../../api.html#responder.ext.ratelimit.RateLimiter">[docs]</a>
|
||||
<span class="k">class</span><span class="w"> </span><span class="nc">RateLimiter</span><span class="p">:</span>
|
||||
<span class="w"> </span><span class="sd">"""Token bucket rate limiter.</span>
|
||||
|
||||
<span class="sd"> Usage::</span>
|
||||
|
||||
<span class="sd"> from responder.ext.ratelimit import RateLimiter</span>
|
||||
|
||||
<span class="sd"> limiter = RateLimiter(requests=100, period=60) # 100 req/min</span>
|
||||
|
||||
<span class="sd"> @api.route(before_request=True)</span>
|
||||
<span class="sd"> def rate_limit(req, resp):</span>
|
||||
<span class="sd"> limiter.check(req, resp)</span>
|
||||
|
||||
<span class="sd"> Or use the shorthand::</span>
|
||||
|
||||
<span class="sd"> limiter = RateLimiter(requests=100, period=60)</span>
|
||||
<span class="sd"> limiter.install(api)</span>
|
||||
|
||||
<span class="sd"> """</span>
|
||||
|
||||
<span class="k">def</span><span class="w"> </span><span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">requests</span><span class="o">=</span><span class="mi">100</span><span class="p">,</span> <span class="n">period</span><span class="o">=</span><span class="mi">60</span><span class="p">):</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">max_requests</span> <span class="o">=</span> <span class="n">requests</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">period</span> <span class="o">=</span> <span class="n">period</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_buckets</span><span class="p">:</span> <span class="nb">dict</span><span class="p">[</span><span class="nb">str</span><span class="p">,</span> <span class="nb">list</span><span class="p">[</span><span class="nb">float</span><span class="p">]]</span> <span class="o">=</span> <span class="n">defaultdict</span><span class="p">(</span><span class="nb">list</span><span class="p">)</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_lock</span> <span class="o">=</span> <span class="n">threading</span><span class="o">.</span><span class="n">Lock</span><span class="p">()</span>
|
||||
|
||||
<span class="k">def</span><span class="w"> </span><span class="nf">_client_key</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">req</span><span class="p">):</span>
|
||||
<span class="n">client</span> <span class="o">=</span> <span class="n">req</span><span class="o">.</span><span class="n">client</span>
|
||||
<span class="k">if</span> <span class="n">client</span><span class="p">:</span>
|
||||
<span class="k">return</span> <span class="n">client</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span>
|
||||
<span class="k">return</span> <span class="n">req</span><span class="o">.</span><span class="n">headers</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">"X-Forwarded-For"</span><span class="p">,</span> <span class="s2">"unknown"</span><span class="p">)</span>
|
||||
|
||||
<span class="k">def</span><span class="w"> </span><span class="nf">_cleanup</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">key</span><span class="p">):</span>
|
||||
<span class="n">now</span> <span class="o">=</span> <span class="n">time</span><span class="o">.</span><span class="n">time</span><span class="p">()</span>
|
||||
<span class="n">cutoff</span> <span class="o">=</span> <span class="n">now</span> <span class="o">-</span> <span class="bp">self</span><span class="o">.</span><span class="n">period</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_buckets</span><span class="p">[</span><span class="n">key</span><span class="p">]</span> <span class="o">=</span> <span class="p">[</span><span class="n">t</span> <span class="k">for</span> <span class="n">t</span> <span class="ow">in</span> <span class="bp">self</span><span class="o">.</span><span class="n">_buckets</span><span class="p">[</span><span class="n">key</span><span class="p">]</span> <span class="k">if</span> <span class="n">t</span> <span class="o">></span> <span class="n">cutoff</span><span class="p">]</span>
|
||||
<span class="k">if</span> <span class="ow">not</span> <span class="bp">self</span><span class="o">.</span><span class="n">_buckets</span><span class="p">[</span><span class="n">key</span><span class="p">]:</span>
|
||||
<span class="k">del</span> <span class="bp">self</span><span class="o">.</span><span class="n">_buckets</span><span class="p">[</span><span class="n">key</span><span class="p">]</span>
|
||||
|
||||
<div class="viewcode-block" id="RateLimiter.check">
|
||||
<a class="viewcode-back" href="../../../api.html#responder.ext.ratelimit.RateLimiter.check">[docs]</a>
|
||||
<span class="k">def</span><span class="w"> </span><span class="nf">check</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">req</span><span class="p">,</span> <span class="n">resp</span><span class="p">):</span>
|
||||
<span class="w"> </span><span class="sd">"""Check rate limit. Sets 429 status if exceeded."""</span>
|
||||
<span class="n">key</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_client_key</span><span class="p">(</span><span class="n">req</span><span class="p">)</span>
|
||||
|
||||
<span class="k">with</span> <span class="bp">self</span><span class="o">.</span><span class="n">_lock</span><span class="p">:</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_cleanup</span><span class="p">(</span><span class="n">key</span><span class="p">)</span>
|
||||
|
||||
<span class="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">_buckets</span><span class="p">[</span><span class="n">key</span><span class="p">])</span> <span class="o">>=</span> <span class="bp">self</span><span class="o">.</span><span class="n">max_requests</span><span class="p">:</span>
|
||||
<span class="n">resp</span><span class="o">.</span><span class="n">status_code</span> <span class="o">=</span> <span class="mi">429</span>
|
||||
<span class="n">resp</span><span class="o">.</span><span class="n">media</span> <span class="o">=</span> <span class="p">{</span><span class="s2">"error"</span><span class="p">:</span> <span class="s2">"rate limit exceeded"</span><span class="p">}</span>
|
||||
<span class="n">resp</span><span class="o">.</span><span class="n">headers</span><span class="p">[</span><span class="s2">"Retry-After"</span><span class="p">]</span> <span class="o">=</span> <span class="nb">str</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">period</span><span class="p">)</span>
|
||||
<span class="k">return</span> <span class="kc">False</span>
|
||||
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_buckets</span><span class="p">[</span><span class="n">key</span><span class="p">]</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">time</span><span class="o">.</span><span class="n">time</span><span class="p">())</span>
|
||||
<span class="n">remaining</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">max_requests</span> <span class="o">-</span> <span class="nb">len</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">_buckets</span><span class="p">[</span><span class="n">key</span><span class="p">])</span>
|
||||
|
||||
<span class="n">resp</span><span class="o">.</span><span class="n">headers</span><span class="p">[</span><span class="s2">"X-RateLimit-Limit"</span><span class="p">]</span> <span class="o">=</span> <span class="nb">str</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">max_requests</span><span class="p">)</span>
|
||||
<span class="n">resp</span><span class="o">.</span><span class="n">headers</span><span class="p">[</span><span class="s2">"X-RateLimit-Remaining"</span><span class="p">]</span> <span class="o">=</span> <span class="nb">str</span><span class="p">(</span><span class="n">remaining</span><span class="p">)</span>
|
||||
<span class="k">return</span> <span class="kc">True</span></div>
|
||||
|
||||
|
||||
<div class="viewcode-block" id="RateLimiter.install">
|
||||
<a class="viewcode-back" href="../../../api.html#responder.ext.ratelimit.RateLimiter.install">[docs]</a>
|
||||
<span class="k">def</span><span class="w"> </span><span class="nf">install</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">api</span><span class="p">):</span>
|
||||
<span class="w"> </span><span class="sd">"""Install as a before_request hook on the API."""</span>
|
||||
|
||||
<span class="nd">@api</span><span class="o">.</span><span class="n">route</span><span class="p">(</span><span class="n">before_request</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
|
||||
<span class="k">def</span><span class="w"> </span><span class="nf">_rate_limit</span><span class="p">(</span><span class="n">req</span><span class="p">,</span> <span class="n">resp</span><span class="p">):</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">check</span><span class="p">(</span><span class="n">req</span><span class="p">,</span> <span class="n">resp</span><span class="p">)</span></div>
|
||||
</div>
|
||||
|
||||
</pre></div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="sphinxsidebar" role="navigation" aria-label="Main">
|
||||
<div class="sphinxsidebarwrapper"><p class="logo">
|
||||
<a href="../../../index.html">
|
||||
<img class="logo" src="../../../_static/responder.png" />
|
||||
</a>
|
||||
</p>
|
||||
<p>
|
||||
<strong>Responder</strong> — a familiar HTTP service framework for Python.
|
||||
<br />
|
||||
<small>v3.6.2</small>
|
||||
</p>
|
||||
<h3>Useful Links</h3>
|
||||
<ul>
|
||||
<li><a href="https://github.com/kennethreitz/responder">Responder @ GitHub</a></li>
|
||||
<li><a href="https://pypi.org/project/responder/">Responder @ PyPI</a></li>
|
||||
<li><a href="https://github.com/kennethreitz/responder/issues">Issue Tracker</a></li>
|
||||
</ul>
|
||||
<search id="searchbox" style="display: none" role="search">
|
||||
<h3 id="searchlabel">Quick search</h3>
|
||||
<div class="searchformwrapper">
|
||||
<form class="search" action="../../../search.html" method="get">
|
||||
<input type="text" name="q" aria-labelledby="searchlabel" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"/>
|
||||
<input type="submit" value="Go" />
|
||||
</form>
|
||||
</div>
|
||||
</search>
|
||||
<script>document.getElementById('searchbox').style.display = "block"</script>
|
||||
</div>
|
||||
</div>
|
||||
<div class="clearer"></div>
|
||||
</div>
|
||||
<div class="footer">
|
||||
©2018-2026, Kenneth Reitz.
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,755 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html lang="en" data-content_root="../../">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>responder.models — responder 3.6.2 documentation</title>
|
||||
<link rel="stylesheet" type="text/css" href="../../_static/pygments.css?v=5ecbeea2" />
|
||||
<link rel="stylesheet" type="text/css" href="../../_static/basic.css?v=b08954a9" />
|
||||
<link rel="stylesheet" type="text/css" href="../../_static/alabaster.css?v=27fed22d" />
|
||||
<link rel="stylesheet" type="text/css" href="../../_static/copybutton.css?v=76b2166b" />
|
||||
<link rel="stylesheet" type="text/css" href="../../_static/design-elements.e5416f61bae5d36adc6d722a2b6f8cff.css?v=452a8e97" />
|
||||
<script src="../../_static/documentation_options.js?v=c0c9fa11"></script>
|
||||
<script src="../../_static/doctools.js?v=9bcbadda"></script>
|
||||
<script src="../../_static/sphinx_highlight.js?v=dc90522c"></script>
|
||||
<script src="../../_static/clipboard.min.js?v=a7894cd8"></script>
|
||||
<script src="../../_static/copybutton.js?v=fd10adb8"></script>
|
||||
<script>
|
||||
</script>
|
||||
<script src="../../_static/design-elements.bbdccc18c4abea9397628f9fea3d48c2.js?v=03c7770e"></script>
|
||||
<link rel="index" title="Index" href="../../genindex.html" />
|
||||
<link rel="search" title="Search" href="../../search.html" />
|
||||
|
||||
<link rel="stylesheet" href="../../_static/custom.css" type="text/css" />
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</head><body>
|
||||
|
||||
|
||||
<div class="document">
|
||||
<div class="documentwrapper">
|
||||
<div class="bodywrapper">
|
||||
|
||||
|
||||
<div class="body" role="main">
|
||||
|
||||
<h1>Source code for responder.models</h1><div class="highlight"><pre>
|
||||
<span></span><span class="kn">from</span><span class="w"> </span><span class="nn">__future__</span><span class="w"> </span><span class="kn">import</span> <span class="n">annotations</span>
|
||||
|
||||
<span class="kn">import</span><span class="w"> </span><span class="nn">functools</span>
|
||||
<span class="kn">import</span><span class="w"> </span><span class="nn">inspect</span>
|
||||
<span class="kn">from</span><span class="w"> </span><span class="nn">collections.abc</span><span class="w"> </span><span class="kn">import</span> <span class="n">Callable</span>
|
||||
<span class="kn">from</span><span class="w"> </span><span class="nn">http.cookies</span><span class="w"> </span><span class="kn">import</span> <span class="n">SimpleCookie</span>
|
||||
<span class="kn">from</span><span class="w"> </span><span class="nn">urllib.parse</span><span class="w"> </span><span class="kn">import</span> <span class="n">parse_qs</span><span class="p">,</span> <span class="n">urlparse</span>
|
||||
|
||||
<span class="n">__all__</span> <span class="o">=</span> <span class="p">[</span><span class="s2">"Request"</span><span class="p">,</span> <span class="s2">"Response"</span><span class="p">,</span> <span class="s2">"QueryDict"</span><span class="p">]</span>
|
||||
|
||||
<span class="k">try</span><span class="p">:</span>
|
||||
<span class="kn">import</span><span class="w"> </span><span class="nn">chardet</span>
|
||||
<span class="k">except</span> <span class="ne">ImportError</span><span class="p">:</span>
|
||||
<span class="n">chardet</span> <span class="o">=</span> <span class="kc">None</span> <span class="c1"># type: ignore[assignment]</span>
|
||||
<span class="kn">from</span><span class="w"> </span><span class="nn">starlette.requests</span><span class="w"> </span><span class="kn">import</span> <span class="n">Request</span> <span class="k">as</span> <span class="n">StarletteRequest</span>
|
||||
<span class="kn">from</span><span class="w"> </span><span class="nn">starlette.requests</span><span class="w"> </span><span class="kn">import</span> <span class="n">State</span>
|
||||
<span class="kn">from</span><span class="w"> </span><span class="nn">starlette.responses</span><span class="w"> </span><span class="kn">import</span> <span class="p">(</span>
|
||||
<span class="n">Response</span> <span class="k">as</span> <span class="n">StarletteResponse</span><span class="p">,</span>
|
||||
<span class="p">)</span>
|
||||
<span class="kn">from</span><span class="w"> </span><span class="nn">starlette.responses</span><span class="w"> </span><span class="kn">import</span> <span class="p">(</span>
|
||||
<span class="n">StreamingResponse</span> <span class="k">as</span> <span class="n">StarletteStreamingResponse</span><span class="p">,</span>
|
||||
<span class="p">)</span>
|
||||
|
||||
<span class="kn">from</span><span class="w"> </span><span class="nn">.statics</span><span class="w"> </span><span class="kn">import</span> <span class="n">DEFAULT_ENCODING</span>
|
||||
<span class="kn">from</span><span class="w"> </span><span class="nn">.status_codes</span><span class="w"> </span><span class="kn">import</span> <span class="n">HTTP_301</span> <span class="c1"># type: ignore[attr-defined]</span>
|
||||
|
||||
|
||||
<span class="k">class</span><span class="w"> </span><span class="nc">CaseInsensitiveDict</span><span class="p">(</span><span class="nb">dict</span><span class="p">):</span>
|
||||
<span class="w"> </span><span class="sd">"""A case-insensitive dict for HTTP headers."""</span>
|
||||
|
||||
<span class="k">def</span><span class="w"> </span><span class="fm">__setitem__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">key</span><span class="p">,</span> <span class="n">value</span><span class="p">):</span>
|
||||
<span class="nb">super</span><span class="p">()</span><span class="o">.</span><span class="fm">__setitem__</span><span class="p">(</span><span class="n">key</span><span class="o">.</span><span class="n">lower</span><span class="p">(),</span> <span class="n">value</span><span class="p">)</span>
|
||||
|
||||
<span class="k">def</span><span class="w"> </span><span class="fm">__getitem__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">key</span><span class="p">):</span>
|
||||
<span class="k">return</span> <span class="nb">super</span><span class="p">()</span><span class="o">.</span><span class="fm">__getitem__</span><span class="p">(</span><span class="n">key</span><span class="o">.</span><span class="n">lower</span><span class="p">())</span>
|
||||
|
||||
<span class="k">def</span><span class="w"> </span><span class="fm">__delitem__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">key</span><span class="p">):</span>
|
||||
<span class="nb">super</span><span class="p">()</span><span class="o">.</span><span class="fm">__delitem__</span><span class="p">(</span><span class="n">key</span><span class="o">.</span><span class="n">lower</span><span class="p">())</span>
|
||||
|
||||
<span class="k">def</span><span class="w"> </span><span class="fm">__contains__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">key</span><span class="p">):</span>
|
||||
<span class="k">return</span> <span class="nb">super</span><span class="p">()</span><span class="o">.</span><span class="fm">__contains__</span><span class="p">(</span><span class="n">key</span><span class="o">.</span><span class="n">lower</span><span class="p">())</span>
|
||||
|
||||
<span class="k">def</span><span class="w"> </span><span class="nf">get</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">key</span><span class="p">,</span> <span class="n">default</span><span class="o">=</span><span class="kc">None</span><span class="p">):</span>
|
||||
<span class="k">return</span> <span class="nb">super</span><span class="p">()</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">key</span><span class="o">.</span><span class="n">lower</span><span class="p">(),</span> <span class="n">default</span><span class="p">)</span>
|
||||
|
||||
<span class="k">def</span><span class="w"> </span><span class="nf">pop</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">key</span><span class="p">,</span> <span class="o">*</span><span class="n">args</span><span class="p">):</span>
|
||||
<span class="k">return</span> <span class="nb">super</span><span class="p">()</span><span class="o">.</span><span class="n">pop</span><span class="p">(</span><span class="n">key</span><span class="o">.</span><span class="n">lower</span><span class="p">(),</span> <span class="o">*</span><span class="n">args</span><span class="p">)</span>
|
||||
|
||||
<span class="k">def</span><span class="w"> </span><span class="nf">setdefault</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">key</span><span class="p">,</span> <span class="n">default</span><span class="o">=</span><span class="kc">None</span><span class="p">):</span>
|
||||
<span class="k">return</span> <span class="nb">super</span><span class="p">()</span><span class="o">.</span><span class="n">setdefault</span><span class="p">(</span><span class="n">key</span><span class="o">.</span><span class="n">lower</span><span class="p">(),</span> <span class="n">default</span><span class="p">)</span>
|
||||
|
||||
<span class="k">def</span><span class="w"> </span><span class="nf">update</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">other</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">):</span>
|
||||
<span class="k">if</span> <span class="n">other</span><span class="p">:</span>
|
||||
<span class="k">for</span> <span class="n">key</span><span class="p">,</span> <span class="n">value</span> <span class="ow">in</span> <span class="n">other</span><span class="o">.</span><span class="n">items</span><span class="p">():</span>
|
||||
<span class="bp">self</span><span class="p">[</span><span class="n">key</span><span class="p">]</span> <span class="o">=</span> <span class="n">value</span>
|
||||
<span class="k">for</span> <span class="n">key</span><span class="p">,</span> <span class="n">value</span> <span class="ow">in</span> <span class="n">kwargs</span><span class="o">.</span><span class="n">items</span><span class="p">():</span>
|
||||
<span class="bp">self</span><span class="p">[</span><span class="n">key</span><span class="p">]</span> <span class="o">=</span> <span class="n">value</span>
|
||||
|
||||
|
||||
<div class="viewcode-block" id="QueryDict">
|
||||
<a class="viewcode-back" href="../../api.html#responder.QueryDict">[docs]</a>
|
||||
<span class="k">class</span><span class="w"> </span><span class="nc">QueryDict</span><span class="p">(</span><span class="nb">dict</span><span class="p">):</span>
|
||||
<span class="w"> </span><span class="sd">"""A dictionary for query string parameters that handles multi-value keys.</span>
|
||||
|
||||
<span class="sd"> Single-value access returns the last value for a key. Use :meth:`get_list`</span>
|
||||
<span class="sd"> to retrieve all values for a multi-value parameter.</span>
|
||||
<span class="sd"> """</span>
|
||||
|
||||
<span class="k">def</span><span class="w"> </span><span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">query_string</span><span class="p">):</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">update</span><span class="p">(</span><span class="n">parse_qs</span><span class="p">(</span><span class="n">query_string</span><span class="p">))</span>
|
||||
|
||||
<span class="k">def</span><span class="w"> </span><span class="fm">__getitem__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">key</span><span class="p">):</span>
|
||||
<span class="w"> </span><span class="sd">"""</span>
|
||||
<span class="sd"> Return the last data value for this key, or [] if it's an empty list;</span>
|
||||
<span class="sd"> raise KeyError if not found.</span>
|
||||
<span class="sd"> """</span>
|
||||
<span class="n">list_</span> <span class="o">=</span> <span class="nb">super</span><span class="p">()</span><span class="o">.</span><span class="fm">__getitem__</span><span class="p">(</span><span class="n">key</span><span class="p">)</span>
|
||||
<span class="k">try</span><span class="p">:</span>
|
||||
<span class="k">return</span> <span class="n">list_</span><span class="p">[</span><span class="o">-</span><span class="mi">1</span><span class="p">]</span>
|
||||
<span class="k">except</span> <span class="ne">IndexError</span><span class="p">:</span>
|
||||
<span class="k">return</span> <span class="p">[]</span>
|
||||
|
||||
<div class="viewcode-block" id="QueryDict.get">
|
||||
<a class="viewcode-back" href="../../api.html#responder.QueryDict.get">[docs]</a>
|
||||
<span class="k">def</span><span class="w"> </span><span class="nf">get</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">key</span><span class="p">,</span> <span class="n">default</span><span class="o">=</span><span class="kc">None</span><span class="p">):</span>
|
||||
<span class="w"> </span><span class="sd">"""</span>
|
||||
<span class="sd"> Return the last data value for the passed key. If key doesn't exist</span>
|
||||
<span class="sd"> or value is an empty list, return `default`.</span>
|
||||
<span class="sd"> """</span>
|
||||
<span class="k">try</span><span class="p">:</span>
|
||||
<span class="n">val</span> <span class="o">=</span> <span class="bp">self</span><span class="p">[</span><span class="n">key</span><span class="p">]</span>
|
||||
<span class="k">except</span> <span class="ne">KeyError</span><span class="p">:</span>
|
||||
<span class="k">return</span> <span class="n">default</span>
|
||||
<span class="k">if</span> <span class="n">val</span> <span class="o">==</span> <span class="p">[]:</span>
|
||||
<span class="k">return</span> <span class="n">default</span>
|
||||
<span class="k">return</span> <span class="n">val</span></div>
|
||||
|
||||
|
||||
<span class="k">def</span><span class="w"> </span><span class="nf">_get_list</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">key</span><span class="p">,</span> <span class="n">default</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span> <span class="n">force_list</span><span class="o">=</span><span class="kc">False</span><span class="p">):</span>
|
||||
<span class="w"> </span><span class="sd">"""</span>
|
||||
<span class="sd"> Return a list of values for the key.</span>
|
||||
|
||||
<span class="sd"> Used internally to manipulate values list. If force_list is True,</span>
|
||||
<span class="sd"> return a new copy of values.</span>
|
||||
<span class="sd"> """</span>
|
||||
<span class="k">try</span><span class="p">:</span>
|
||||
<span class="n">values</span> <span class="o">=</span> <span class="nb">super</span><span class="p">()</span><span class="o">.</span><span class="fm">__getitem__</span><span class="p">(</span><span class="n">key</span><span class="p">)</span>
|
||||
<span class="k">except</span> <span class="ne">KeyError</span><span class="p">:</span>
|
||||
<span class="k">if</span> <span class="n">default</span> <span class="ow">is</span> <span class="kc">None</span><span class="p">:</span>
|
||||
<span class="k">return</span> <span class="p">[]</span>
|
||||
<span class="k">return</span> <span class="n">default</span>
|
||||
<span class="k">else</span><span class="p">:</span>
|
||||
<span class="k">if</span> <span class="n">force_list</span><span class="p">:</span>
|
||||
<span class="n">values</span> <span class="o">=</span> <span class="nb">list</span><span class="p">(</span><span class="n">values</span><span class="p">)</span> <span class="k">if</span> <span class="n">values</span> <span class="ow">is</span> <span class="ow">not</span> <span class="kc">None</span> <span class="k">else</span> <span class="kc">None</span>
|
||||
<span class="k">return</span> <span class="n">values</span>
|
||||
|
||||
<div class="viewcode-block" id="QueryDict.get_list">
|
||||
<a class="viewcode-back" href="../../api.html#responder.QueryDict.get_list">[docs]</a>
|
||||
<span class="k">def</span><span class="w"> </span><span class="nf">get_list</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">key</span><span class="p">,</span> <span class="n">default</span><span class="o">=</span><span class="kc">None</span><span class="p">):</span>
|
||||
<span class="w"> </span><span class="sd">"""</span>
|
||||
<span class="sd"> Return the list of values for the key. If key doesn't exist, return a</span>
|
||||
<span class="sd"> default value.</span>
|
||||
<span class="sd"> """</span>
|
||||
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_get_list</span><span class="p">(</span><span class="n">key</span><span class="p">,</span> <span class="n">default</span><span class="p">,</span> <span class="n">force_list</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span></div>
|
||||
|
||||
|
||||
<div class="viewcode-block" id="QueryDict.items">
|
||||
<a class="viewcode-back" href="../../api.html#responder.QueryDict.items">[docs]</a>
|
||||
<span class="k">def</span><span class="w"> </span><span class="nf">items</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||
<span class="w"> </span><span class="sd">"""</span>
|
||||
<span class="sd"> Yield (key, value) pairs, where value is the last item in the list</span>
|
||||
<span class="sd"> associated with the key.</span>
|
||||
<span class="sd"> """</span>
|
||||
<span class="k">for</span> <span class="n">key</span> <span class="ow">in</span> <span class="bp">self</span><span class="p">:</span>
|
||||
<span class="k">yield</span> <span class="n">key</span><span class="p">,</span> <span class="bp">self</span><span class="p">[</span><span class="n">key</span><span class="p">]</span></div>
|
||||
|
||||
|
||||
<div class="viewcode-block" id="QueryDict.items_list">
|
||||
<a class="viewcode-back" href="../../api.html#responder.QueryDict.items_list">[docs]</a>
|
||||
<span class="k">def</span><span class="w"> </span><span class="nf">items_list</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||
<span class="w"> </span><span class="sd">"""</span>
|
||||
<span class="sd"> Yield (key, value) pairs, where value is the the list.</span>
|
||||
<span class="sd"> """</span>
|
||||
<span class="k">yield from</span> <span class="nb">super</span><span class="p">()</span><span class="o">.</span><span class="n">items</span><span class="p">()</span></div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="viewcode-block" id="Request">
|
||||
<a class="viewcode-back" href="../../api.html#responder.Request">[docs]</a>
|
||||
<span class="k">class</span><span class="w"> </span><span class="nc">Request</span><span class="p">:</span>
|
||||
<span class="w"> </span><span class="sd">"""An HTTP request, passed to each view as the first argument.</span>
|
||||
|
||||
<span class="sd"> Provides access to headers, cookies, query parameters, the request body,</span>
|
||||
<span class="sd"> session data, and more. Most properties are synchronous; reading the body</span>
|
||||
<span class="sd"> (via :attr:`content`, :attr:`text`, or :meth:`media`) requires ``await``.</span>
|
||||
<span class="sd"> """</span>
|
||||
|
||||
<span class="vm">__slots__</span> <span class="o">=</span> <span class="p">[</span>
|
||||
<span class="s2">"_starlette"</span><span class="p">,</span>
|
||||
<span class="s2">"formats"</span><span class="p">,</span>
|
||||
<span class="s2">"_headers"</span><span class="p">,</span>
|
||||
<span class="s2">"_encoding"</span><span class="p">,</span>
|
||||
<span class="s2">"api"</span><span class="p">,</span>
|
||||
<span class="s2">"_content"</span><span class="p">,</span>
|
||||
<span class="s2">"_cookies"</span><span class="p">,</span>
|
||||
<span class="p">]</span>
|
||||
|
||||
<span class="k">def</span><span class="w"> </span><span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">scope</span><span class="p">,</span> <span class="n">receive</span><span class="p">,</span> <span class="n">api</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span> <span class="n">formats</span><span class="o">=</span><span class="kc">None</span><span class="p">):</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_starlette</span> <span class="o">=</span> <span class="n">StarletteRequest</span><span class="p">(</span><span class="n">scope</span><span class="p">,</span> <span class="n">receive</span><span class="p">)</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">formats</span> <span class="o">=</span> <span class="n">formats</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_encoding</span> <span class="o">=</span> <span class="kc">None</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">api</span> <span class="o">=</span> <span class="n">api</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_content</span> <span class="o">=</span> <span class="kc">None</span>
|
||||
|
||||
<span class="n">headers</span><span class="p">:</span> <span class="n">CaseInsensitiveDict</span> <span class="o">=</span> <span class="n">CaseInsensitiveDict</span><span class="p">()</span>
|
||||
<span class="k">for</span> <span class="n">key</span><span class="p">,</span> <span class="n">value</span> <span class="ow">in</span> <span class="bp">self</span><span class="o">.</span><span class="n">_starlette</span><span class="o">.</span><span class="n">headers</span><span class="o">.</span><span class="n">items</span><span class="p">():</span>
|
||||
<span class="n">headers</span><span class="p">[</span><span class="n">key</span><span class="p">]</span> <span class="o">=</span> <span class="n">value</span>
|
||||
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_headers</span> <span class="o">=</span> <span class="n">headers</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_cookies</span> <span class="o">=</span> <span class="kc">None</span>
|
||||
|
||||
<span class="nd">@property</span>
|
||||
<span class="k">def</span><span class="w"> </span><span class="nf">session</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||
<span class="w"> </span><span class="sd">"""The session data, in dict form, from the Request."""</span>
|
||||
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_starlette</span><span class="o">.</span><span class="n">session</span>
|
||||
|
||||
<span class="nd">@property</span>
|
||||
<span class="k">def</span><span class="w"> </span><span class="nf">headers</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||
<span class="w"> </span><span class="sd">"""A case-insensitive dictionary, containing all headers sent in the Request."""</span>
|
||||
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_headers</span>
|
||||
|
||||
<span class="nd">@property</span>
|
||||
<span class="k">def</span><span class="w"> </span><span class="nf">mimetype</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||
<span class="w"> </span><span class="sd">"""The MIME type of the request body, from the ``Content-Type`` header."""</span>
|
||||
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">headers</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">"Content-Type"</span><span class="p">,</span> <span class="s2">""</span><span class="p">)</span>
|
||||
|
||||
<span class="nd">@property</span>
|
||||
<span class="k">def</span><span class="w"> </span><span class="nf">is_json</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||
<span class="w"> </span><span class="sd">"""Returns ``True`` if the request content type is JSON."""</span>
|
||||
<span class="k">return</span> <span class="s2">"json"</span> <span class="ow">in</span> <span class="bp">self</span><span class="o">.</span><span class="n">mimetype</span>
|
||||
|
||||
<span class="nd">@property</span>
|
||||
<span class="k">def</span><span class="w"> </span><span class="nf">method</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||
<span class="w"> </span><span class="sd">"""The incoming HTTP method used for the request, lower-cased."""</span>
|
||||
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_starlette</span><span class="o">.</span><span class="n">method</span><span class="o">.</span><span class="n">lower</span><span class="p">()</span>
|
||||
|
||||
<span class="nd">@property</span>
|
||||
<span class="k">def</span><span class="w"> </span><span class="nf">full_url</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||
<span class="w"> </span><span class="sd">"""The full URL of the Request, query parameters and all."""</span>
|
||||
<span class="k">return</span> <span class="nb">str</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">_starlette</span><span class="o">.</span><span class="n">url</span><span class="p">)</span>
|
||||
|
||||
<span class="nd">@property</span>
|
||||
<span class="k">def</span><span class="w"> </span><span class="nf">url</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||
<span class="w"> </span><span class="sd">"""The parsed URL of the Request."""</span>
|
||||
<span class="k">return</span> <span class="n">urlparse</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">full_url</span><span class="p">)</span>
|
||||
|
||||
<span class="nd">@property</span>
|
||||
<span class="k">def</span><span class="w"> </span><span class="nf">cookies</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||
<span class="w"> </span><span class="sd">"""The cookies sent in the Request, as a dictionary."""</span>
|
||||
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">_cookies</span> <span class="ow">is</span> <span class="kc">None</span><span class="p">:</span>
|
||||
<span class="n">cookies</span> <span class="o">=</span> <span class="p">{}</span>
|
||||
<span class="n">cookie_header</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">headers</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">"Cookie"</span><span class="p">,</span> <span class="s2">""</span><span class="p">)</span>
|
||||
|
||||
<span class="n">bc</span><span class="p">:</span> <span class="n">SimpleCookie</span> <span class="o">=</span> <span class="n">SimpleCookie</span><span class="p">(</span><span class="n">cookie_header</span><span class="p">)</span>
|
||||
<span class="k">for</span> <span class="n">key</span><span class="p">,</span> <span class="n">morsel</span> <span class="ow">in</span> <span class="n">bc</span><span class="o">.</span><span class="n">items</span><span class="p">():</span>
|
||||
<span class="n">cookies</span><span class="p">[</span><span class="n">key</span><span class="p">]</span> <span class="o">=</span> <span class="n">morsel</span><span class="o">.</span><span class="n">value</span>
|
||||
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_cookies</span> <span class="o">=</span> <span class="n">cookies</span>
|
||||
|
||||
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_cookies</span>
|
||||
|
||||
<span class="nd">@property</span>
|
||||
<span class="k">def</span><span class="w"> </span><span class="nf">params</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||
<span class="w"> </span><span class="sd">"""A dictionary of the parsed query parameters used for the Request."""</span>
|
||||
<span class="k">try</span><span class="p">:</span>
|
||||
<span class="k">return</span> <span class="n">QueryDict</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">url</span><span class="o">.</span><span class="n">query</span><span class="p">)</span>
|
||||
<span class="k">except</span> <span class="ne">AttributeError</span><span class="p">:</span>
|
||||
<span class="k">return</span> <span class="n">QueryDict</span><span class="p">({})</span>
|
||||
|
||||
<span class="nd">@property</span>
|
||||
<span class="k">def</span><span class="w"> </span><span class="nf">path_params</span><span class="p">(</span><span class="bp">self</span><span class="p">)</span> <span class="o">-></span> <span class="nb">dict</span><span class="p">:</span>
|
||||
<span class="w"> </span><span class="sd">"""The path parameters extracted from the URL route."""</span>
|
||||
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_starlette</span><span class="o">.</span><span class="n">path_params</span>
|
||||
|
||||
<span class="nd">@property</span>
|
||||
<span class="k">def</span><span class="w"> </span><span class="nf">client</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||
<span class="w"> </span><span class="sd">"""The client's address as a (host, port) named tuple, or None."""</span>
|
||||
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_starlette</span><span class="o">.</span><span class="n">client</span>
|
||||
|
||||
<span class="nd">@property</span>
|
||||
<span class="k">def</span><span class="w"> </span><span class="nf">state</span><span class="p">(</span><span class="bp">self</span><span class="p">)</span> <span class="o">-></span> <span class="n">State</span><span class="p">:</span>
|
||||
<span class="w"> </span><span class="sd">"""</span>
|
||||
<span class="sd"> Use the state to store additional information.</span>
|
||||
|
||||
<span class="sd"> This can be a very helpful feature, if you want to hand over</span>
|
||||
<span class="sd"> information from a middelware or a route decorator to the</span>
|
||||
<span class="sd"> actual route handler.</span>
|
||||
|
||||
<span class="sd"> Usage: ``request.state.time_started = time.time()``</span>
|
||||
<span class="sd"> """</span>
|
||||
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_starlette</span><span class="o">.</span><span class="n">state</span>
|
||||
|
||||
<span class="nd">@property</span>
|
||||
<span class="k">async</span> <span class="k">def</span><span class="w"> </span><span class="nf">encoding</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||
<span class="w"> </span><span class="sd">"""The encoding of the Request's body. Can be set, manually. Must be awaited."""</span>
|
||||
<span class="c1"># Use the user-set encoding first.</span>
|
||||
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">_encoding</span><span class="p">:</span>
|
||||
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_encoding</span>
|
||||
|
||||
<span class="k">return</span> <span class="k">await</span> <span class="bp">self</span><span class="o">.</span><span class="n">apparent_encoding</span>
|
||||
|
||||
<span class="nd">@encoding</span><span class="o">.</span><span class="n">setter</span>
|
||||
<span class="k">def</span><span class="w"> </span><span class="nf">encoding</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">value</span><span class="p">):</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_encoding</span> <span class="o">=</span> <span class="n">value</span>
|
||||
|
||||
<span class="nd">@property</span>
|
||||
<span class="k">async</span> <span class="k">def</span><span class="w"> </span><span class="nf">content</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||
<span class="w"> </span><span class="sd">"""The Request body, as bytes. Must be awaited."""</span>
|
||||
<span class="k">if</span> <span class="ow">not</span> <span class="bp">self</span><span class="o">.</span><span class="n">_content</span><span class="p">:</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_content</span> <span class="o">=</span> <span class="k">await</span> <span class="bp">self</span><span class="o">.</span><span class="n">_starlette</span><span class="o">.</span><span class="n">body</span><span class="p">()</span>
|
||||
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_content</span>
|
||||
|
||||
<span class="nd">@property</span>
|
||||
<span class="k">async</span> <span class="k">def</span><span class="w"> </span><span class="nf">text</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||
<span class="w"> </span><span class="sd">"""The Request body, as unicode. Must be awaited."""</span>
|
||||
<span class="k">return</span> <span class="p">(</span><span class="k">await</span> <span class="bp">self</span><span class="o">.</span><span class="n">content</span><span class="p">)</span><span class="o">.</span><span class="n">decode</span><span class="p">(</span><span class="k">await</span> <span class="bp">self</span><span class="o">.</span><span class="n">encoding</span><span class="p">)</span>
|
||||
|
||||
<span class="nd">@property</span>
|
||||
<span class="k">async</span> <span class="k">def</span><span class="w"> </span><span class="nf">declared_encoding</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||
<span class="k">if</span> <span class="s2">"Encoding"</span> <span class="ow">in</span> <span class="bp">self</span><span class="o">.</span><span class="n">headers</span><span class="p">:</span>
|
||||
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">headers</span><span class="p">[</span><span class="s2">"Encoding"</span><span class="p">]</span>
|
||||
<span class="k">return</span> <span class="kc">None</span>
|
||||
|
||||
<span class="nd">@property</span>
|
||||
<span class="k">async</span> <span class="k">def</span><span class="w"> </span><span class="nf">apparent_encoding</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||
<span class="w"> </span><span class="sd">"""The apparent encoding, detected automatically. Must be awaited.</span>
|
||||
|
||||
<span class="sd"> Uses chardet for detection if installed, otherwise falls back to UTF-8.</span>
|
||||
<span class="sd"> """</span>
|
||||
<span class="n">declared_encoding</span> <span class="o">=</span> <span class="k">await</span> <span class="bp">self</span><span class="o">.</span><span class="n">declared_encoding</span>
|
||||
|
||||
<span class="k">if</span> <span class="n">declared_encoding</span><span class="p">:</span>
|
||||
<span class="k">return</span> <span class="n">declared_encoding</span>
|
||||
|
||||
<span class="k">if</span> <span class="n">chardet</span> <span class="ow">is</span> <span class="ow">not</span> <span class="kc">None</span><span class="p">:</span>
|
||||
<span class="k">return</span> <span class="n">chardet</span><span class="o">.</span><span class="n">detect</span><span class="p">(</span><span class="k">await</span> <span class="bp">self</span><span class="o">.</span><span class="n">content</span><span class="p">)[</span><span class="s2">"encoding"</span><span class="p">]</span> <span class="ow">or</span> <span class="n">DEFAULT_ENCODING</span>
|
||||
|
||||
<span class="k">return</span> <span class="n">DEFAULT_ENCODING</span>
|
||||
|
||||
<span class="nd">@property</span>
|
||||
<span class="k">def</span><span class="w"> </span><span class="nf">is_secure</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||
<span class="w"> </span><span class="sd">"""``True`` if the request was made over HTTPS."""</span>
|
||||
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">url</span><span class="o">.</span><span class="n">scheme</span> <span class="o">==</span> <span class="s2">"https"</span>
|
||||
|
||||
<div class="viewcode-block" id="Request.accepts">
|
||||
<a class="viewcode-back" href="../../api.html#responder.Request.accepts">[docs]</a>
|
||||
<span class="k">def</span><span class="w"> </span><span class="nf">accepts</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">content_type</span><span class="p">):</span>
|
||||
<span class="w"> </span><span class="sd">"""Returns ``True`` if the incoming Request accepts the given ``content_type``."""</span>
|
||||
<span class="k">return</span> <span class="n">content_type</span> <span class="ow">in</span> <span class="bp">self</span><span class="o">.</span><span class="n">headers</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">"Accept"</span><span class="p">,</span> <span class="p">[])</span></div>
|
||||
|
||||
|
||||
<div class="viewcode-block" id="Request.media">
|
||||
<a class="viewcode-back" href="../../api.html#responder.Request.media">[docs]</a>
|
||||
<span class="k">async</span> <span class="k">def</span><span class="w"> </span><span class="nf">media</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="nb">format</span><span class="p">:</span> <span class="nb">str</span> <span class="o">|</span> <span class="n">Callable</span> <span class="o">=</span> <span class="kc">None</span><span class="p">):</span> <span class="c1"># noqa: A002</span>
|
||||
<span class="w"> </span><span class="sd">"""Renders incoming json/yaml/form data as Python objects. Must be awaited.</span>
|
||||
|
||||
<span class="sd"> :param format: The name of the format being used.</span>
|
||||
<span class="sd"> Alternatively, accepts a custom callable for the format type.</span>
|
||||
<span class="sd"> """</span>
|
||||
|
||||
<span class="k">if</span> <span class="nb">format</span> <span class="ow">is</span> <span class="kc">None</span><span class="p">:</span>
|
||||
<span class="nb">format</span> <span class="o">=</span> <span class="s2">"yaml"</span> <span class="k">if</span> <span class="s2">"yaml"</span> <span class="ow">in</span> <span class="bp">self</span><span class="o">.</span><span class="n">mimetype</span> <span class="k">else</span> <span class="s2">"json"</span> <span class="c1"># noqa: A001</span>
|
||||
<span class="nb">format</span> <span class="o">=</span> <span class="s2">"form"</span> <span class="k">if</span> <span class="s2">"form"</span> <span class="ow">in</span> <span class="bp">self</span><span class="o">.</span><span class="n">mimetype</span> <span class="k">else</span> <span class="nb">format</span> <span class="c1"># noqa: A001</span>
|
||||
|
||||
<span class="n">formatter</span><span class="p">:</span> <span class="n">Callable</span>
|
||||
<span class="k">if</span> <span class="nb">isinstance</span><span class="p">(</span><span class="nb">format</span><span class="p">,</span> <span class="nb">str</span><span class="p">):</span>
|
||||
<span class="k">try</span><span class="p">:</span>
|
||||
<span class="n">formatter</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">formats</span><span class="p">[</span><span class="nb">format</span><span class="p">]</span>
|
||||
<span class="k">except</span> <span class="ne">KeyError</span> <span class="k">as</span> <span class="n">ex</span><span class="p">:</span>
|
||||
<span class="k">raise</span> <span class="ne">ValueError</span><span class="p">(</span><span class="sa">f</span><span class="s2">"Unable to process data in '</span><span class="si">{</span><span class="nb">format</span><span class="si">}</span><span class="s2">' format"</span><span class="p">)</span> <span class="kn">from</span><span class="w"> </span><span class="nn">ex</span>
|
||||
|
||||
<span class="k">elif</span> <span class="nb">callable</span><span class="p">(</span><span class="nb">format</span><span class="p">):</span>
|
||||
<span class="n">formatter</span> <span class="o">=</span> <span class="nb">format</span>
|
||||
|
||||
<span class="k">else</span><span class="p">:</span>
|
||||
<span class="k">raise</span> <span class="ne">TypeError</span><span class="p">(</span><span class="sa">f</span><span class="s2">"Invalid 'format' argument: </span><span class="si">{</span><span class="nb">format</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
|
||||
|
||||
<span class="k">return</span> <span class="k">await</span> <span class="n">formatter</span><span class="p">(</span><span class="bp">self</span><span class="p">)</span></div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<span class="k">def</span><span class="w"> </span><span class="nf">content_setter</span><span class="p">(</span><span class="n">mimetype</span><span class="p">):</span>
|
||||
<span class="k">def</span><span class="w"> </span><span class="nf">getter</span><span class="p">(</span><span class="n">instance</span><span class="p">):</span>
|
||||
<span class="k">return</span> <span class="n">instance</span><span class="o">.</span><span class="n">content</span>
|
||||
|
||||
<span class="k">def</span><span class="w"> </span><span class="nf">setter</span><span class="p">(</span><span class="n">instance</span><span class="p">,</span> <span class="n">value</span><span class="p">):</span>
|
||||
<span class="n">instance</span><span class="o">.</span><span class="n">content</span> <span class="o">=</span> <span class="n">value</span>
|
||||
<span class="n">instance</span><span class="o">.</span><span class="n">mimetype</span> <span class="o">=</span> <span class="n">mimetype</span>
|
||||
|
||||
<span class="k">return</span> <span class="nb">property</span><span class="p">(</span><span class="n">fget</span><span class="o">=</span><span class="n">getter</span><span class="p">,</span> <span class="n">fset</span><span class="o">=</span><span class="n">setter</span><span class="p">)</span>
|
||||
|
||||
|
||||
<div class="viewcode-block" id="Response">
|
||||
<a class="viewcode-back" href="../../api.html#responder.Response">[docs]</a>
|
||||
<span class="k">class</span><span class="w"> </span><span class="nc">Response</span><span class="p">:</span>
|
||||
<span class="w"> </span><span class="sd">"""An HTTP response, passed to each view as the second argument.</span>
|
||||
|
||||
<span class="sd"> Mutate this object to control what gets sent back to the client. Set</span>
|
||||
<span class="sd"> :attr:`text`, :attr:`html`, :attr:`media`, or :attr:`content` to define</span>
|
||||
<span class="sd"> the body. Use :attr:`headers` and :meth:`set_cookie` to control metadata.</span>
|
||||
|
||||
<span class="sd"> :var text: Set the response body as plain text (sets ``Content-Type: text/plain``).</span>
|
||||
<span class="sd"> :var html: Set the response body as HTML (sets ``Content-Type: text/html``).</span>
|
||||
<span class="sd"> :var media: Set a Python object (dict, list) to be serialized as JSON (or negotiated format).</span>
|
||||
<span class="sd"> :var content: Set the raw response body as bytes.</span>
|
||||
<span class="sd"> :var status_code: The HTTP status code (e.g. ``200``, ``404``). Defaults to ``200`` if not set.</span>
|
||||
<span class="sd"> :var headers: A dict of response headers.</span>
|
||||
<span class="sd"> :var cookies: A ``SimpleCookie`` holding cookies to set on the response.</span>
|
||||
<span class="sd"> :var session: A dict of session data. Changes are persisted in a signed cookie.</span>
|
||||
<span class="sd"> """</span> <span class="c1"># noqa: E501</span>
|
||||
|
||||
<span class="vm">__slots__</span> <span class="o">=</span> <span class="p">[</span>
|
||||
<span class="s2">"req"</span><span class="p">,</span>
|
||||
<span class="s2">"status_code"</span><span class="p">,</span>
|
||||
<span class="s2">"content"</span><span class="p">,</span>
|
||||
<span class="s2">"encoding"</span><span class="p">,</span>
|
||||
<span class="s2">"media"</span><span class="p">,</span>
|
||||
<span class="s2">"headers"</span><span class="p">,</span>
|
||||
<span class="s2">"formats"</span><span class="p">,</span>
|
||||
<span class="s2">"cookies"</span><span class="p">,</span>
|
||||
<span class="s2">"session"</span><span class="p">,</span>
|
||||
<span class="s2">"mimetype"</span><span class="p">,</span>
|
||||
<span class="s2">"_stream"</span><span class="p">,</span>
|
||||
<span class="p">]</span>
|
||||
|
||||
<span class="n">text</span> <span class="o">=</span> <span class="n">content_setter</span><span class="p">(</span><span class="s2">"text/plain"</span><span class="p">)</span>
|
||||
<span class="n">html</span> <span class="o">=</span> <span class="n">content_setter</span><span class="p">(</span><span class="s2">"text/html"</span><span class="p">)</span>
|
||||
|
||||
<span class="k">def</span><span class="w"> </span><span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">req</span><span class="p">,</span> <span class="o">*</span><span class="p">,</span> <span class="n">formats</span><span class="p">):</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">req</span> <span class="o">=</span> <span class="n">req</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">status_code</span><span class="p">:</span> <span class="nb">int</span> <span class="o">|</span> <span class="kc">None</span> <span class="o">=</span> <span class="kc">None</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">content</span> <span class="o">=</span> <span class="kc">None</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">mimetype</span> <span class="o">=</span> <span class="kc">None</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">encoding</span> <span class="o">=</span> <span class="n">DEFAULT_ENCODING</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">media</span> <span class="o">=</span> <span class="kc">None</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_stream</span> <span class="o">=</span> <span class="kc">None</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">headers</span> <span class="o">=</span> <span class="p">{}</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">formats</span> <span class="o">=</span> <span class="n">formats</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">cookies</span><span class="p">:</span> <span class="n">SimpleCookie</span> <span class="o">=</span> <span class="n">SimpleCookie</span><span class="p">()</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">session</span> <span class="o">=</span> <span class="n">req</span><span class="o">.</span><span class="n">session</span>
|
||||
|
||||
<div class="viewcode-block" id="Response.stream">
|
||||
<a class="viewcode-back" href="../../api.html#responder.Response.stream">[docs]</a>
|
||||
<span class="k">def</span><span class="w"> </span><span class="nf">stream</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">func</span><span class="p">,</span> <span class="o">*</span><span class="n">args</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">):</span>
|
||||
<span class="w"> </span><span class="sd">"""Set up a streaming response from an async generator function.</span>
|
||||
|
||||
<span class="sd"> The generator yields chunks of bytes that are sent to the client</span>
|
||||
<span class="sd"> as they are produced, without buffering the full response in memory.</span>
|
||||
|
||||
<span class="sd"> Usage::</span>
|
||||
|
||||
<span class="sd"> @api.route("/stream")</span>
|
||||
<span class="sd"> async def stream_data(req, resp):</span>
|
||||
<span class="sd"> @resp.stream</span>
|
||||
<span class="sd"> async def body():</span>
|
||||
<span class="sd"> for i in range(10):</span>
|
||||
<span class="sd"> yield f"chunk {i}\\n".encode()</span>
|
||||
|
||||
<span class="sd"> :param func: An async generator function that yields response chunks.</span>
|
||||
<span class="sd"> """</span>
|
||||
<span class="k">assert</span> <span class="n">inspect</span><span class="o">.</span><span class="n">isasyncgenfunction</span><span class="p">(</span><span class="n">func</span><span class="p">)</span>
|
||||
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_stream</span> <span class="o">=</span> <span class="n">functools</span><span class="o">.</span><span class="n">partial</span><span class="p">(</span><span class="n">func</span><span class="p">,</span> <span class="o">*</span><span class="n">args</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">)</span>
|
||||
|
||||
<span class="k">return</span> <span class="n">func</span></div>
|
||||
|
||||
|
||||
<div class="viewcode-block" id="Response.sse">
|
||||
<a class="viewcode-back" href="../../api.html#responder.Response.sse">[docs]</a>
|
||||
<span class="k">def</span><span class="w"> </span><span class="nf">sse</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">func</span><span class="p">,</span> <span class="o">*</span><span class="n">args</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">):</span>
|
||||
<span class="w"> </span><span class="sd">"""Set up Server-Sent Events streaming.</span>
|
||||
|
||||
<span class="sd"> Usage::</span>
|
||||
|
||||
<span class="sd"> @api.route("/events")</span>
|
||||
<span class="sd"> async def events(req, resp):</span>
|
||||
<span class="sd"> @resp.sse</span>
|
||||
<span class="sd"> async def stream():</span>
|
||||
<span class="sd"> for i in range(10):</span>
|
||||
<span class="sd"> yield {"data": f"message {i}"}</span>
|
||||
|
||||
<span class="sd"> Each yielded dict can have: data, event, id, retry.</span>
|
||||
<span class="sd"> Yielding a string is treated as data.</span>
|
||||
<span class="sd"> """</span>
|
||||
<span class="k">assert</span> <span class="n">inspect</span><span class="o">.</span><span class="n">isasyncgenfunction</span><span class="p">(</span><span class="n">func</span><span class="p">)</span>
|
||||
|
||||
<span class="k">async</span> <span class="k">def</span><span class="w"> </span><span class="nf">sse_generator</span><span class="p">():</span>
|
||||
<span class="k">async</span> <span class="k">for</span> <span class="n">event</span> <span class="ow">in</span> <span class="n">func</span><span class="p">(</span><span class="o">*</span><span class="n">args</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">):</span>
|
||||
<span class="k">if</span> <span class="nb">isinstance</span><span class="p">(</span><span class="n">event</span><span class="p">,</span> <span class="nb">str</span><span class="p">):</span>
|
||||
<span class="k">yield</span> <span class="sa">f</span><span class="s2">"data: </span><span class="si">{</span><span class="n">event</span><span class="si">}</span><span class="se">\n\n</span><span class="s2">"</span><span class="o">.</span><span class="n">encode</span><span class="p">()</span>
|
||||
<span class="k">elif</span> <span class="nb">isinstance</span><span class="p">(</span><span class="n">event</span><span class="p">,</span> <span class="nb">dict</span><span class="p">):</span>
|
||||
<span class="n">parts</span> <span class="o">=</span> <span class="p">[]</span>
|
||||
<span class="k">if</span> <span class="s2">"event"</span> <span class="ow">in</span> <span class="n">event</span><span class="p">:</span>
|
||||
<span class="n">parts</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="sa">f</span><span class="s2">"event: </span><span class="si">{</span><span class="n">event</span><span class="p">[</span><span class="s1">'event'</span><span class="p">]</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
|
||||
<span class="k">if</span> <span class="s2">"id"</span> <span class="ow">in</span> <span class="n">event</span><span class="p">:</span>
|
||||
<span class="n">parts</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="sa">f</span><span class="s2">"id: </span><span class="si">{</span><span class="n">event</span><span class="p">[</span><span class="s1">'id'</span><span class="p">]</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
|
||||
<span class="k">if</span> <span class="s2">"retry"</span> <span class="ow">in</span> <span class="n">event</span><span class="p">:</span>
|
||||
<span class="n">parts</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="sa">f</span><span class="s2">"retry: </span><span class="si">{</span><span class="n">event</span><span class="p">[</span><span class="s1">'retry'</span><span class="p">]</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
|
||||
<span class="n">data</span> <span class="o">=</span> <span class="n">event</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">"data"</span><span class="p">,</span> <span class="s2">""</span><span class="p">)</span>
|
||||
<span class="k">for</span> <span class="n">line</span> <span class="ow">in</span> <span class="nb">str</span><span class="p">(</span><span class="n">data</span><span class="p">)</span><span class="o">.</span><span class="n">split</span><span class="p">(</span><span class="s2">"</span><span class="se">\n</span><span class="s2">"</span><span class="p">):</span>
|
||||
<span class="n">parts</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="sa">f</span><span class="s2">"data: </span><span class="si">{</span><span class="n">line</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
|
||||
<span class="n">parts</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="s2">""</span><span class="p">)</span>
|
||||
<span class="n">parts</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="s2">""</span><span class="p">)</span>
|
||||
<span class="k">yield</span> <span class="s2">"</span><span class="se">\n</span><span class="s2">"</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="n">parts</span><span class="p">)</span><span class="o">.</span><span class="n">encode</span><span class="p">()</span>
|
||||
<span class="k">else</span><span class="p">:</span>
|
||||
<span class="k">yield</span> <span class="sa">f</span><span class="s2">"data: </span><span class="si">{</span><span class="n">event</span><span class="si">}</span><span class="se">\n\n</span><span class="s2">"</span><span class="o">.</span><span class="n">encode</span><span class="p">()</span>
|
||||
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_stream</span> <span class="o">=</span> <span class="n">sse_generator</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">mimetype</span> <span class="o">=</span> <span class="s2">"text/event-stream"</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">headers</span><span class="p">[</span><span class="s2">"Cache-Control"</span><span class="p">]</span> <span class="o">=</span> <span class="s2">"no-cache"</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">headers</span><span class="p">[</span><span class="s2">"Connection"</span><span class="p">]</span> <span class="o">=</span> <span class="s2">"keep-alive"</span>
|
||||
|
||||
<span class="k">return</span> <span class="n">func</span></div>
|
||||
|
||||
|
||||
<div class="viewcode-block" id="Response.stream_file">
|
||||
<a class="viewcode-back" href="../../api.html#responder.Response.stream_file">[docs]</a>
|
||||
<span class="k">def</span><span class="w"> </span><span class="nf">stream_file</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">path</span><span class="p">,</span> <span class="o">*</span><span class="p">,</span> <span class="n">content_type</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span> <span class="n">chunk_size</span><span class="o">=</span><span class="mi">8192</span><span class="p">):</span>
|
||||
<span class="w"> </span><span class="sd">"""Stream a file without loading it entirely into memory.</span>
|
||||
|
||||
<span class="sd"> :param path: Path to the file.</span>
|
||||
<span class="sd"> :param content_type: Optional MIME type override.</span>
|
||||
<span class="sd"> :param chunk_size: Size of chunks to read (default 8192 bytes).</span>
|
||||
<span class="sd"> """</span>
|
||||
<span class="kn">from</span><span class="w"> </span><span class="nn">pathlib</span><span class="w"> </span><span class="kn">import</span> <span class="n">Path</span> <span class="k">as</span> <span class="n">PathType</span>
|
||||
|
||||
<span class="n">path</span> <span class="o">=</span> <span class="n">PathType</span><span class="p">(</span><span class="n">path</span><span class="p">)</span>
|
||||
|
||||
<span class="k">if</span> <span class="n">content_type</span><span class="p">:</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">mimetype</span> <span class="o">=</span> <span class="n">content_type</span>
|
||||
<span class="k">else</span><span class="p">:</span>
|
||||
<span class="kn">import</span><span class="w"> </span><span class="nn">mimetypes</span>
|
||||
|
||||
<span class="n">guessed</span> <span class="o">=</span> <span class="n">mimetypes</span><span class="o">.</span><span class="n">guess_type</span><span class="p">(</span><span class="nb">str</span><span class="p">(</span><span class="n">path</span><span class="p">))[</span><span class="mi">0</span><span class="p">]</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">mimetype</span> <span class="o">=</span> <span class="n">guessed</span> <span class="ow">or</span> <span class="s2">"application/octet-stream"</span>
|
||||
|
||||
<span class="k">async</span> <span class="k">def</span><span class="w"> </span><span class="nf">file_generator</span><span class="p">():</span>
|
||||
<span class="kn">import</span><span class="w"> </span><span class="nn">anyio</span>
|
||||
|
||||
<span class="k">async</span> <span class="k">with</span> <span class="k">await</span> <span class="n">anyio</span><span class="o">.</span><span class="n">open_file</span><span class="p">(</span><span class="n">path</span><span class="p">,</span> <span class="s2">"rb"</span><span class="p">)</span> <span class="k">as</span> <span class="n">f</span><span class="p">:</span>
|
||||
<span class="k">while</span> <span class="kc">True</span><span class="p">:</span>
|
||||
<span class="n">chunk</span> <span class="o">=</span> <span class="k">await</span> <span class="n">f</span><span class="o">.</span><span class="n">read</span><span class="p">(</span><span class="n">chunk_size</span><span class="p">)</span>
|
||||
<span class="k">if</span> <span class="ow">not</span> <span class="n">chunk</span><span class="p">:</span>
|
||||
<span class="k">break</span>
|
||||
<span class="k">yield</span> <span class="n">chunk</span>
|
||||
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_stream</span> <span class="o">=</span> <span class="n">file_generator</span></div>
|
||||
|
||||
|
||||
<div class="viewcode-block" id="Response.file">
|
||||
<a class="viewcode-back" href="../../api.html#responder.Response.file">[docs]</a>
|
||||
<span class="k">def</span><span class="w"> </span><span class="nf">file</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">path</span><span class="p">,</span> <span class="o">*</span><span class="p">,</span> <span class="n">content_type</span><span class="o">=</span><span class="kc">None</span><span class="p">):</span>
|
||||
<span class="w"> </span><span class="sd">"""Serve a file from disk as the response.</span>
|
||||
|
||||
<span class="sd"> :param path: Path to the file to serve.</span>
|
||||
<span class="sd"> :param content_type: Optional MIME type override.</span>
|
||||
<span class="sd"> """</span>
|
||||
<span class="kn">from</span><span class="w"> </span><span class="nn">pathlib</span><span class="w"> </span><span class="kn">import</span> <span class="n">Path</span>
|
||||
|
||||
<span class="n">path</span> <span class="o">=</span> <span class="n">Path</span><span class="p">(</span><span class="n">path</span><span class="p">)</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">content</span> <span class="o">=</span> <span class="n">path</span><span class="o">.</span><span class="n">read_bytes</span><span class="p">()</span>
|
||||
|
||||
<span class="k">if</span> <span class="n">content_type</span><span class="p">:</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">mimetype</span> <span class="o">=</span> <span class="n">content_type</span>
|
||||
<span class="k">else</span><span class="p">:</span>
|
||||
<span class="kn">import</span><span class="w"> </span><span class="nn">mimetypes</span>
|
||||
|
||||
<span class="n">guessed</span> <span class="o">=</span> <span class="n">mimetypes</span><span class="o">.</span><span class="n">guess_type</span><span class="p">(</span><span class="nb">str</span><span class="p">(</span><span class="n">path</span><span class="p">))[</span><span class="mi">0</span><span class="p">]</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">mimetype</span> <span class="o">=</span> <span class="n">guessed</span> <span class="ow">or</span> <span class="s2">"application/octet-stream"</span></div>
|
||||
|
||||
|
||||
<div class="viewcode-block" id="Response.redirect">
|
||||
<a class="viewcode-back" href="../../api.html#responder.Response.redirect">[docs]</a>
|
||||
<span class="k">def</span><span class="w"> </span><span class="nf">redirect</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">location</span><span class="p">,</span> <span class="o">*</span><span class="p">,</span> <span class="n">set_text</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span> <span class="n">status_code</span><span class="o">=</span><span class="n">HTTP_301</span><span class="p">):</span>
|
||||
<span class="w"> </span><span class="sd">"""Redirect the client to a different URL.</span>
|
||||
|
||||
<span class="sd"> :param location: The URL to redirect to.</span>
|
||||
<span class="sd"> :param set_text: If ``True``, set a default redirect message as the body.</span>
|
||||
<span class="sd"> :param status_code: The HTTP status code (default ``301``).</span>
|
||||
<span class="sd"> """</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">status_code</span> <span class="o">=</span> <span class="n">status_code</span>
|
||||
<span class="k">if</span> <span class="n">set_text</span><span class="p">:</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">text</span> <span class="o">=</span> <span class="sa">f</span><span class="s2">"Redirecting to: </span><span class="si">{</span><span class="n">location</span><span class="si">}</span><span class="s2">"</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">headers</span><span class="o">.</span><span class="n">update</span><span class="p">({</span><span class="s2">"Location"</span><span class="p">:</span> <span class="n">location</span><span class="p">})</span></div>
|
||||
|
||||
|
||||
<span class="nd">@property</span>
|
||||
<span class="k">async</span> <span class="k">def</span><span class="w"> </span><span class="nf">body</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">_stream</span> <span class="ow">is</span> <span class="ow">not</span> <span class="kc">None</span><span class="p">:</span>
|
||||
<span class="n">headers</span> <span class="o">=</span> <span class="p">{}</span>
|
||||
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">mimetype</span> <span class="ow">is</span> <span class="ow">not</span> <span class="kc">None</span><span class="p">:</span>
|
||||
<span class="n">headers</span><span class="p">[</span><span class="s2">"Content-Type"</span><span class="p">]</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">mimetype</span>
|
||||
<span class="k">return</span> <span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">_stream</span><span class="p">(),</span> <span class="n">headers</span><span class="p">)</span>
|
||||
|
||||
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">content</span> <span class="ow">is</span> <span class="ow">not</span> <span class="kc">None</span><span class="p">:</span>
|
||||
<span class="n">headers</span> <span class="o">=</span> <span class="p">{}</span>
|
||||
<span class="n">content</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">content</span>
|
||||
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">mimetype</span> <span class="ow">is</span> <span class="ow">not</span> <span class="kc">None</span><span class="p">:</span>
|
||||
<span class="n">headers</span><span class="p">[</span><span class="s2">"Content-Type"</span><span class="p">]</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">mimetype</span>
|
||||
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">mimetype</span> <span class="o">==</span> <span class="s2">"text/plain"</span> <span class="ow">and</span> <span class="bp">self</span><span class="o">.</span><span class="n">encoding</span> <span class="ow">is</span> <span class="ow">not</span> <span class="kc">None</span><span class="p">:</span>
|
||||
<span class="n">headers</span><span class="p">[</span><span class="s2">"Encoding"</span><span class="p">]</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">encoding</span>
|
||||
<span class="k">if</span> <span class="nb">isinstance</span><span class="p">(</span><span class="n">content</span><span class="p">,</span> <span class="nb">str</span><span class="p">):</span>
|
||||
<span class="n">content</span> <span class="o">=</span> <span class="n">content</span><span class="o">.</span><span class="n">encode</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">encoding</span><span class="p">)</span>
|
||||
<span class="k">return</span> <span class="p">(</span><span class="n">content</span><span class="p">,</span> <span class="n">headers</span><span class="p">)</span>
|
||||
|
||||
<span class="k">for</span> <span class="n">format_</span> <span class="ow">in</span> <span class="bp">self</span><span class="o">.</span><span class="n">formats</span><span class="p">:</span>
|
||||
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">req</span><span class="o">.</span><span class="n">accepts</span><span class="p">(</span><span class="n">format_</span><span class="p">):</span>
|
||||
<span class="k">return</span> <span class="p">(</span><span class="k">await</span> <span class="bp">self</span><span class="o">.</span><span class="n">formats</span><span class="p">[</span><span class="n">format_</span><span class="p">](</span><span class="bp">self</span><span class="p">,</span> <span class="n">encode</span><span class="o">=</span><span class="kc">True</span><span class="p">)),</span> <span class="p">{}</span>
|
||||
|
||||
<span class="c1"># Default to JSON anyway.</span>
|
||||
<span class="k">return</span> <span class="p">(</span>
|
||||
<span class="k">await</span> <span class="bp">self</span><span class="o">.</span><span class="n">formats</span><span class="p">[</span><span class="s2">"json"</span><span class="p">](</span><span class="bp">self</span><span class="p">,</span> <span class="n">encode</span><span class="o">=</span><span class="kc">True</span><span class="p">),</span>
|
||||
<span class="p">{</span><span class="s2">"Content-Type"</span><span class="p">:</span> <span class="s2">"application/json"</span><span class="p">},</span>
|
||||
<span class="p">)</span>
|
||||
|
||||
<div class="viewcode-block" id="Response.set_cookie">
|
||||
<a class="viewcode-back" href="../../api.html#responder.Response.set_cookie">[docs]</a>
|
||||
<span class="k">def</span><span class="w"> </span><span class="nf">set_cookie</span><span class="p">(</span>
|
||||
<span class="bp">self</span><span class="p">,</span>
|
||||
<span class="n">key</span><span class="p">,</span>
|
||||
<span class="n">value</span><span class="o">=</span><span class="s2">""</span><span class="p">,</span>
|
||||
<span class="n">expires</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span>
|
||||
<span class="n">path</span><span class="o">=</span><span class="s2">"/"</span><span class="p">,</span>
|
||||
<span class="n">domain</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span>
|
||||
<span class="n">max_age</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span>
|
||||
<span class="n">secure</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span>
|
||||
<span class="n">httponly</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span>
|
||||
<span class="p">):</span>
|
||||
<span class="w"> </span><span class="sd">"""Set a cookie on the response with full control over directives.</span>
|
||||
|
||||
<span class="sd"> :param key: The cookie name.</span>
|
||||
<span class="sd"> :param value: The cookie value.</span>
|
||||
<span class="sd"> :param expires: Expiration date string (e.g. ``"Thu, 01 Jan 2026 00:00:00 GMT"``).</span>
|
||||
<span class="sd"> :param path: URL path the cookie applies to (default ``"/"``).</span>
|
||||
<span class="sd"> :param domain: Domain the cookie is valid for.</span>
|
||||
<span class="sd"> :param max_age: Maximum age in seconds before the cookie expires.</span>
|
||||
<span class="sd"> :param secure: If ``True``, cookie is only sent over HTTPS.</span>
|
||||
<span class="sd"> :param httponly: If ``True`` (default), cookie is inaccessible to JavaScript.</span>
|
||||
|
||||
<span class="sd"> Usage::</span>
|
||||
|
||||
<span class="sd"> resp.set_cookie(</span>
|
||||
<span class="sd"> "token", value="abc123",</span>
|
||||
<span class="sd"> max_age=3600, secure=True, httponly=True,</span>
|
||||
<span class="sd"> )</span>
|
||||
|
||||
<span class="sd"> """</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">cookies</span><span class="p">[</span><span class="n">key</span><span class="p">]</span> <span class="o">=</span> <span class="n">value</span>
|
||||
<span class="n">morsel</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">cookies</span><span class="p">[</span><span class="n">key</span><span class="p">]</span>
|
||||
<span class="k">if</span> <span class="n">expires</span> <span class="ow">is</span> <span class="ow">not</span> <span class="kc">None</span><span class="p">:</span>
|
||||
<span class="n">morsel</span><span class="p">[</span><span class="s2">"expires"</span><span class="p">]</span> <span class="o">=</span> <span class="n">expires</span>
|
||||
<span class="k">if</span> <span class="n">path</span> <span class="ow">is</span> <span class="ow">not</span> <span class="kc">None</span><span class="p">:</span>
|
||||
<span class="n">morsel</span><span class="p">[</span><span class="s2">"path"</span><span class="p">]</span> <span class="o">=</span> <span class="n">path</span>
|
||||
<span class="k">if</span> <span class="n">domain</span> <span class="ow">is</span> <span class="ow">not</span> <span class="kc">None</span><span class="p">:</span>
|
||||
<span class="n">morsel</span><span class="p">[</span><span class="s2">"domain"</span><span class="p">]</span> <span class="o">=</span> <span class="n">domain</span>
|
||||
<span class="k">if</span> <span class="n">max_age</span> <span class="ow">is</span> <span class="ow">not</span> <span class="kc">None</span><span class="p">:</span>
|
||||
<span class="n">morsel</span><span class="p">[</span><span class="s2">"max-age"</span><span class="p">]</span> <span class="o">=</span> <span class="n">max_age</span>
|
||||
<span class="n">morsel</span><span class="p">[</span><span class="s2">"secure"</span><span class="p">]</span> <span class="o">=</span> <span class="n">secure</span>
|
||||
<span class="n">morsel</span><span class="p">[</span><span class="s2">"httponly"</span><span class="p">]</span> <span class="o">=</span> <span class="n">httponly</span></div>
|
||||
|
||||
|
||||
<span class="k">def</span><span class="w"> </span><span class="nf">_prepare_cookies</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">starlette_response</span><span class="p">):</span>
|
||||
<span class="n">cookie_header</span> <span class="o">=</span> <span class="p">(</span>
|
||||
<span class="p">(</span><span class="sa">b</span><span class="s2">"set-cookie"</span><span class="p">,</span> <span class="n">morsel</span><span class="o">.</span><span class="n">output</span><span class="p">(</span><span class="n">header</span><span class="o">=</span><span class="s2">""</span><span class="p">)</span><span class="o">.</span><span class="n">lstrip</span><span class="p">()</span><span class="o">.</span><span class="n">encode</span><span class="p">(</span><span class="s2">"latin-1"</span><span class="p">))</span>
|
||||
<span class="k">for</span> <span class="n">morsel</span> <span class="ow">in</span> <span class="bp">self</span><span class="o">.</span><span class="n">cookies</span><span class="o">.</span><span class="n">values</span><span class="p">()</span>
|
||||
<span class="p">)</span>
|
||||
<span class="n">starlette_response</span><span class="o">.</span><span class="n">raw_headers</span><span class="o">.</span><span class="n">extend</span><span class="p">(</span><span class="n">cookie_header</span><span class="p">)</span>
|
||||
|
||||
<span class="k">async</span> <span class="k">def</span><span class="w"> </span><span class="fm">__call__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">scope</span><span class="p">,</span> <span class="n">receive</span><span class="p">,</span> <span class="n">send</span><span class="p">):</span>
|
||||
<span class="n">body</span><span class="p">,</span> <span class="n">headers</span> <span class="o">=</span> <span class="k">await</span> <span class="bp">self</span><span class="o">.</span><span class="n">body</span>
|
||||
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">headers</span><span class="p">:</span>
|
||||
<span class="n">headers</span><span class="o">.</span><span class="n">update</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">headers</span><span class="p">)</span>
|
||||
|
||||
<span class="n">response_cls</span><span class="p">:</span> <span class="nb">type</span><span class="p">[</span><span class="n">StarletteResponse</span><span class="p">]</span> <span class="o">|</span> <span class="nb">type</span><span class="p">[</span><span class="n">StarletteStreamingResponse</span><span class="p">]</span>
|
||||
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">_stream</span> <span class="ow">is</span> <span class="ow">not</span> <span class="kc">None</span><span class="p">:</span>
|
||||
<span class="n">response_cls</span> <span class="o">=</span> <span class="n">StarletteStreamingResponse</span>
|
||||
<span class="k">else</span><span class="p">:</span>
|
||||
<span class="n">response_cls</span> <span class="o">=</span> <span class="n">StarletteResponse</span>
|
||||
|
||||
<span class="n">response</span> <span class="o">=</span> <span class="n">response_cls</span><span class="p">(</span><span class="n">body</span><span class="p">,</span> <span class="n">status_code</span><span class="o">=</span><span class="bp">self</span><span class="o">.</span><span class="n">status_code_safe</span><span class="p">,</span> <span class="n">headers</span><span class="o">=</span><span class="n">headers</span><span class="p">)</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_prepare_cookies</span><span class="p">(</span><span class="n">response</span><span class="p">)</span>
|
||||
|
||||
<span class="k">await</span> <span class="n">response</span><span class="p">(</span><span class="n">scope</span><span class="p">,</span> <span class="n">receive</span><span class="p">,</span> <span class="n">send</span><span class="p">)</span>
|
||||
|
||||
<span class="nd">@property</span>
|
||||
<span class="k">def</span><span class="w"> </span><span class="nf">ok</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||
<span class="w"> </span><span class="sd">"""``True`` if the status code is in the 2xx range (success)."""</span>
|
||||
<span class="k">return</span> <span class="mi">200</span> <span class="o"><=</span> <span class="bp">self</span><span class="o">.</span><span class="n">status_code_safe</span> <span class="o"><</span> <span class="mi">300</span>
|
||||
|
||||
<span class="nd">@property</span>
|
||||
<span class="k">def</span><span class="w"> </span><span class="nf">status_code_safe</span><span class="p">(</span><span class="bp">self</span><span class="p">)</span> <span class="o">-></span> <span class="nb">int</span><span class="p">:</span>
|
||||
<span class="w"> </span><span class="sd">"""Return the status code, raising ``RuntimeError`` if it hasn't been set."""</span>
|
||||
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">status_code</span> <span class="ow">is</span> <span class="kc">None</span><span class="p">:</span>
|
||||
<span class="k">raise</span> <span class="ne">RuntimeError</span><span class="p">(</span><span class="s2">"HTTP status code has not been defined"</span><span class="p">)</span>
|
||||
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">status_code</span></div>
|
||||
|
||||
</pre></div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="sphinxsidebar" role="navigation" aria-label="Main">
|
||||
<div class="sphinxsidebarwrapper"><p class="logo">
|
||||
<a href="../../index.html">
|
||||
<img class="logo" src="../../_static/responder.png" />
|
||||
</a>
|
||||
</p>
|
||||
<p>
|
||||
<strong>Responder</strong> — a familiar HTTP service framework for Python.
|
||||
<br />
|
||||
<small>v3.6.2</small>
|
||||
</p>
|
||||
<h3>Useful Links</h3>
|
||||
<ul>
|
||||
<li><a href="https://github.com/kennethreitz/responder">Responder @ GitHub</a></li>
|
||||
<li><a href="https://pypi.org/project/responder/">Responder @ PyPI</a></li>
|
||||
<li><a href="https://github.com/kennethreitz/responder/issues">Issue Tracker</a></li>
|
||||
</ul>
|
||||
<search id="searchbox" style="display: none" role="search">
|
||||
<h3 id="searchlabel">Quick search</h3>
|
||||
<div class="searchformwrapper">
|
||||
<form class="search" action="../../search.html" method="get">
|
||||
<input type="text" name="q" aria-labelledby="searchlabel" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"/>
|
||||
<input type="submit" value="Go" />
|
||||
</form>
|
||||
</div>
|
||||
</search>
|
||||
<script>document.getElementById('searchbox').style.display = "block"</script>
|
||||
</div>
|
||||
</div>
|
||||
<div class="clearer"></div>
|
||||
</div>
|
||||
<div class="footer">
|
||||
©2018-2026, Kenneth Reitz.
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,210 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html lang="en" data-content_root="../../">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>responder.status_codes — responder 3.6.2 documentation</title>
|
||||
<link rel="stylesheet" type="text/css" href="../../_static/pygments.css?v=5ecbeea2" />
|
||||
<link rel="stylesheet" type="text/css" href="../../_static/basic.css?v=b08954a9" />
|
||||
<link rel="stylesheet" type="text/css" href="../../_static/alabaster.css?v=27fed22d" />
|
||||
<link rel="stylesheet" type="text/css" href="../../_static/copybutton.css?v=76b2166b" />
|
||||
<link rel="stylesheet" type="text/css" href="../../_static/design-elements.e5416f61bae5d36adc6d722a2b6f8cff.css?v=452a8e97" />
|
||||
<script src="../../_static/documentation_options.js?v=c0c9fa11"></script>
|
||||
<script src="../../_static/doctools.js?v=9bcbadda"></script>
|
||||
<script src="../../_static/sphinx_highlight.js?v=dc90522c"></script>
|
||||
<script src="../../_static/clipboard.min.js?v=a7894cd8"></script>
|
||||
<script src="../../_static/copybutton.js?v=fd10adb8"></script>
|
||||
<script>
|
||||
</script>
|
||||
<script src="../../_static/design-elements.bbdccc18c4abea9397628f9fea3d48c2.js?v=03c7770e"></script>
|
||||
<link rel="index" title="Index" href="../../genindex.html" />
|
||||
<link rel="search" title="Search" href="../../search.html" />
|
||||
|
||||
<link rel="stylesheet" href="../../_static/custom.css" type="text/css" />
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</head><body>
|
||||
|
||||
|
||||
<div class="document">
|
||||
<div class="documentwrapper">
|
||||
<div class="bodywrapper">
|
||||
|
||||
|
||||
<div class="body" role="main">
|
||||
|
||||
<h1>Source code for responder.status_codes</h1><div class="highlight"><pre>
|
||||
<span></span><span class="n">codes</span> <span class="o">=</span> <span class="p">{</span>
|
||||
<span class="c1"># Informational.</span>
|
||||
<span class="mi">100</span><span class="p">:</span> <span class="p">(</span><span class="s2">"continue"</span><span class="p">,),</span>
|
||||
<span class="mi">101</span><span class="p">:</span> <span class="p">(</span><span class="s2">"switching_protocols"</span><span class="p">,),</span>
|
||||
<span class="mi">102</span><span class="p">:</span> <span class="p">(</span><span class="s2">"processing"</span><span class="p">,),</span>
|
||||
<span class="mi">103</span><span class="p">:</span> <span class="p">(</span><span class="s2">"checkpoint"</span><span class="p">,),</span>
|
||||
<span class="mi">122</span><span class="p">:</span> <span class="p">(</span><span class="s2">"uri_too_long"</span><span class="p">,</span> <span class="s2">"request_uri_too_long"</span><span class="p">),</span>
|
||||
<span class="mi">200</span><span class="p">:</span> <span class="p">(</span><span class="s2">"ok"</span><span class="p">,</span> <span class="s2">"okay"</span><span class="p">,</span> <span class="s2">"all_ok"</span><span class="p">,</span> <span class="s2">"all_okay"</span><span class="p">,</span> <span class="s2">"all_good"</span><span class="p">,</span> <span class="s2">"</span><span class="se">\\</span><span class="s2">o/"</span><span class="p">,</span> <span class="s2">"✓"</span><span class="p">),</span>
|
||||
<span class="mi">201</span><span class="p">:</span> <span class="p">(</span><span class="s2">"created"</span><span class="p">,),</span>
|
||||
<span class="mi">202</span><span class="p">:</span> <span class="p">(</span><span class="s2">"accepted"</span><span class="p">,),</span>
|
||||
<span class="mi">203</span><span class="p">:</span> <span class="p">(</span><span class="s2">"non_authoritative_info"</span><span class="p">,</span> <span class="s2">"non_authoritative_information"</span><span class="p">),</span>
|
||||
<span class="mi">204</span><span class="p">:</span> <span class="p">(</span><span class="s2">"no_content"</span><span class="p">,),</span>
|
||||
<span class="mi">205</span><span class="p">:</span> <span class="p">(</span><span class="s2">"reset_content"</span><span class="p">,</span> <span class="s2">"reset"</span><span class="p">),</span>
|
||||
<span class="mi">206</span><span class="p">:</span> <span class="p">(</span><span class="s2">"partial_content"</span><span class="p">,</span> <span class="s2">"partial"</span><span class="p">),</span>
|
||||
<span class="mi">207</span><span class="p">:</span> <span class="p">(</span><span class="s2">"multi_status"</span><span class="p">,</span> <span class="s2">"multiple_status"</span><span class="p">,</span> <span class="s2">"multi_stati"</span><span class="p">,</span> <span class="s2">"multiple_stati"</span><span class="p">),</span>
|
||||
<span class="mi">208</span><span class="p">:</span> <span class="p">(</span><span class="s2">"already_reported"</span><span class="p">,),</span>
|
||||
<span class="mi">226</span><span class="p">:</span> <span class="p">(</span><span class="s2">"im_used"</span><span class="p">,),</span>
|
||||
<span class="c1"># Redirection.</span>
|
||||
<span class="mi">300</span><span class="p">:</span> <span class="p">(</span><span class="s2">"multiple_choices"</span><span class="p">,),</span>
|
||||
<span class="mi">301</span><span class="p">:</span> <span class="p">(</span><span class="s2">"moved_permanently"</span><span class="p">,</span> <span class="s2">"moved"</span><span class="p">,</span> <span class="s2">"</span><span class="se">\\</span><span class="s2">o-"</span><span class="p">),</span>
|
||||
<span class="mi">302</span><span class="p">:</span> <span class="p">(</span><span class="s2">"found"</span><span class="p">,),</span>
|
||||
<span class="mi">303</span><span class="p">:</span> <span class="p">(</span><span class="s2">"see_other"</span><span class="p">,</span> <span class="s2">"other"</span><span class="p">),</span>
|
||||
<span class="mi">304</span><span class="p">:</span> <span class="p">(</span><span class="s2">"not_modified"</span><span class="p">,),</span>
|
||||
<span class="mi">305</span><span class="p">:</span> <span class="p">(</span><span class="s2">"use_proxy"</span><span class="p">,),</span>
|
||||
<span class="mi">306</span><span class="p">:</span> <span class="p">(</span><span class="s2">"switch_proxy"</span><span class="p">,),</span>
|
||||
<span class="mi">307</span><span class="p">:</span> <span class="p">(</span><span class="s2">"temporary_redirect"</span><span class="p">,</span> <span class="s2">"temporary_moved"</span><span class="p">,</span> <span class="s2">"temporary"</span><span class="p">),</span>
|
||||
<span class="mi">308</span><span class="p">:</span> <span class="p">(</span><span class="s2">"permanent_redirect"</span><span class="p">,),</span>
|
||||
<span class="c1"># Client Error.</span>
|
||||
<span class="mi">400</span><span class="p">:</span> <span class="p">(</span><span class="s2">"bad_request"</span><span class="p">,</span> <span class="s2">"bad"</span><span class="p">),</span>
|
||||
<span class="mi">401</span><span class="p">:</span> <span class="p">(</span><span class="s2">"unauthorized"</span><span class="p">,),</span>
|
||||
<span class="mi">402</span><span class="p">:</span> <span class="p">(</span><span class="s2">"payment_required"</span><span class="p">,</span> <span class="s2">"payment"</span><span class="p">),</span>
|
||||
<span class="mi">403</span><span class="p">:</span> <span class="p">(</span><span class="s2">"forbidden"</span><span class="p">,),</span>
|
||||
<span class="mi">404</span><span class="p">:</span> <span class="p">(</span><span class="s2">"not_found"</span><span class="p">,</span> <span class="s2">"-o-"</span><span class="p">),</span>
|
||||
<span class="mi">405</span><span class="p">:</span> <span class="p">(</span><span class="s2">"method_not_allowed"</span><span class="p">,</span> <span class="s2">"not_allowed"</span><span class="p">),</span>
|
||||
<span class="mi">406</span><span class="p">:</span> <span class="p">(</span><span class="s2">"not_acceptable"</span><span class="p">,),</span>
|
||||
<span class="mi">407</span><span class="p">:</span> <span class="p">(</span><span class="s2">"proxy_authentication_required"</span><span class="p">,</span> <span class="s2">"proxy_auth"</span><span class="p">,</span> <span class="s2">"proxy_authentication"</span><span class="p">),</span>
|
||||
<span class="mi">408</span><span class="p">:</span> <span class="p">(</span><span class="s2">"request_timeout"</span><span class="p">,</span> <span class="s2">"timeout"</span><span class="p">),</span>
|
||||
<span class="mi">409</span><span class="p">:</span> <span class="p">(</span><span class="s2">"conflict"</span><span class="p">,),</span>
|
||||
<span class="mi">410</span><span class="p">:</span> <span class="p">(</span><span class="s2">"gone"</span><span class="p">,),</span>
|
||||
<span class="mi">411</span><span class="p">:</span> <span class="p">(</span><span class="s2">"length_required"</span><span class="p">,),</span>
|
||||
<span class="mi">412</span><span class="p">:</span> <span class="p">(</span><span class="s2">"precondition_failed"</span><span class="p">,</span> <span class="s2">"precondition"</span><span class="p">),</span>
|
||||
<span class="mi">413</span><span class="p">:</span> <span class="p">(</span><span class="s2">"request_entity_too_large"</span><span class="p">,),</span>
|
||||
<span class="mi">414</span><span class="p">:</span> <span class="p">(</span><span class="s2">"request_uri_too_large"</span><span class="p">,),</span>
|
||||
<span class="mi">415</span><span class="p">:</span> <span class="p">(</span><span class="s2">"unsupported_media_type"</span><span class="p">,</span> <span class="s2">"unsupported_media"</span><span class="p">,</span> <span class="s2">"media_type"</span><span class="p">),</span>
|
||||
<span class="mi">416</span><span class="p">:</span> <span class="p">(</span>
|
||||
<span class="s2">"requested_range_not_satisfiable"</span><span class="p">,</span>
|
||||
<span class="s2">"requested_range"</span><span class="p">,</span>
|
||||
<span class="s2">"range_not_satisfiable"</span><span class="p">,</span>
|
||||
<span class="p">),</span>
|
||||
<span class="mi">417</span><span class="p">:</span> <span class="p">(</span><span class="s2">"expectation_failed"</span><span class="p">,),</span>
|
||||
<span class="mi">418</span><span class="p">:</span> <span class="p">(</span><span class="s2">"im_a_teapot"</span><span class="p">,</span> <span class="s2">"teapot"</span><span class="p">,</span> <span class="s2">"i_am_a_teapot"</span><span class="p">),</span>
|
||||
<span class="mi">421</span><span class="p">:</span> <span class="p">(</span><span class="s2">"misdirected_request"</span><span class="p">,),</span>
|
||||
<span class="mi">422</span><span class="p">:</span> <span class="p">(</span><span class="s2">"unprocessable_entity"</span><span class="p">,</span> <span class="s2">"unprocessable"</span><span class="p">),</span>
|
||||
<span class="mi">423</span><span class="p">:</span> <span class="p">(</span><span class="s2">"locked"</span><span class="p">,),</span>
|
||||
<span class="mi">424</span><span class="p">:</span> <span class="p">(</span><span class="s2">"failed_dependency"</span><span class="p">,</span> <span class="s2">"dependency"</span><span class="p">),</span>
|
||||
<span class="mi">425</span><span class="p">:</span> <span class="p">(</span><span class="s2">"unordered_collection"</span><span class="p">,</span> <span class="s2">"unordered"</span><span class="p">),</span>
|
||||
<span class="mi">426</span><span class="p">:</span> <span class="p">(</span><span class="s2">"upgrade_required"</span><span class="p">,</span> <span class="s2">"upgrade"</span><span class="p">),</span>
|
||||
<span class="mi">428</span><span class="p">:</span> <span class="p">(</span><span class="s2">"precondition_required"</span><span class="p">,</span> <span class="s2">"precondition"</span><span class="p">),</span>
|
||||
<span class="mi">429</span><span class="p">:</span> <span class="p">(</span><span class="s2">"too_many_requests"</span><span class="p">,</span> <span class="s2">"too_many"</span><span class="p">),</span>
|
||||
<span class="mi">431</span><span class="p">:</span> <span class="p">(</span><span class="s2">"header_fields_too_large"</span><span class="p">,</span> <span class="s2">"fields_too_large"</span><span class="p">),</span>
|
||||
<span class="mi">444</span><span class="p">:</span> <span class="p">(</span><span class="s2">"no_response"</span><span class="p">,</span> <span class="s2">"none"</span><span class="p">),</span>
|
||||
<span class="mi">449</span><span class="p">:</span> <span class="p">(</span><span class="s2">"retry_with"</span><span class="p">,</span> <span class="s2">"retry"</span><span class="p">),</span>
|
||||
<span class="mi">450</span><span class="p">:</span> <span class="p">(</span><span class="s2">"blocked_by_windows_parental_controls"</span><span class="p">,</span> <span class="s2">"parental_controls"</span><span class="p">),</span>
|
||||
<span class="mi">451</span><span class="p">:</span> <span class="p">(</span><span class="s2">"unavailable_for_legal_reasons"</span><span class="p">,</span> <span class="s2">"legal_reasons"</span><span class="p">),</span>
|
||||
<span class="mi">499</span><span class="p">:</span> <span class="p">(</span><span class="s2">"client_closed_request"</span><span class="p">,),</span>
|
||||
<span class="c1"># Server Error.</span>
|
||||
<span class="mi">500</span><span class="p">:</span> <span class="p">(</span><span class="s2">"internal_server_error"</span><span class="p">,</span> <span class="s2">"server_error"</span><span class="p">,</span> <span class="s2">"/o</span><span class="se">\\</span><span class="s2">"</span><span class="p">,</span> <span class="s2">"✗"</span><span class="p">),</span>
|
||||
<span class="mi">501</span><span class="p">:</span> <span class="p">(</span><span class="s2">"not_implemented"</span><span class="p">,),</span>
|
||||
<span class="mi">502</span><span class="p">:</span> <span class="p">(</span><span class="s2">"bad_gateway"</span><span class="p">,),</span>
|
||||
<span class="mi">503</span><span class="p">:</span> <span class="p">(</span><span class="s2">"service_unavailable"</span><span class="p">,</span> <span class="s2">"unavailable"</span><span class="p">),</span>
|
||||
<span class="mi">504</span><span class="p">:</span> <span class="p">(</span><span class="s2">"gateway_timeout"</span><span class="p">,),</span>
|
||||
<span class="mi">505</span><span class="p">:</span> <span class="p">(</span><span class="s2">"http_version_not_supported"</span><span class="p">,</span> <span class="s2">"http_version"</span><span class="p">),</span>
|
||||
<span class="mi">506</span><span class="p">:</span> <span class="p">(</span><span class="s2">"variant_also_negotiates"</span><span class="p">,),</span>
|
||||
<span class="mi">507</span><span class="p">:</span> <span class="p">(</span><span class="s2">"insufficient_storage"</span><span class="p">,),</span>
|
||||
<span class="mi">509</span><span class="p">:</span> <span class="p">(</span><span class="s2">"bandwidth_limit_exceeded"</span><span class="p">,</span> <span class="s2">"bandwidth"</span><span class="p">),</span>
|
||||
<span class="mi">510</span><span class="p">:</span> <span class="p">(</span><span class="s2">"not_extended"</span><span class="p">,),</span>
|
||||
<span class="mi">511</span><span class="p">:</span> <span class="p">(</span><span class="s2">"network_authentication_required"</span><span class="p">,</span> <span class="s2">"network_auth"</span><span class="p">,</span> <span class="s2">"network_authentication"</span><span class="p">),</span>
|
||||
<span class="p">}</span>
|
||||
|
||||
<span class="k">for</span> <span class="n">number</span> <span class="ow">in</span> <span class="n">codes</span><span class="p">:</span>
|
||||
<span class="nb">locals</span><span class="p">()[</span><span class="sa">f</span><span class="s2">"HTTP_</span><span class="si">{</span><span class="n">number</span><span class="si">}</span><span class="s2">"</span><span class="p">]</span> <span class="o">=</span> <span class="n">number</span>
|
||||
|
||||
<span class="k">for</span> <span class="n">label</span> <span class="ow">in</span> <span class="n">codes</span><span class="p">[</span><span class="n">number</span><span class="p">]:</span>
|
||||
<span class="nb">locals</span><span class="p">()[</span><span class="n">label</span><span class="p">]</span> <span class="o">=</span> <span class="n">number</span>
|
||||
|
||||
|
||||
<span class="k">def</span><span class="w"> </span><span class="nf">_is_category</span><span class="p">(</span><span class="n">category</span><span class="p">,</span> <span class="n">status_code</span><span class="p">):</span>
|
||||
<span class="k">return</span> <span class="n">category</span> <span class="o"><=</span> <span class="n">status_code</span> <span class="o"><</span> <span class="n">category</span> <span class="o">+</span> <span class="mi">100</span>
|
||||
|
||||
|
||||
<div class="viewcode-block" id="is_100">
|
||||
<a class="viewcode-back" href="../../api.html#responder.status_codes.is_100">[docs]</a>
|
||||
<span class="k">def</span><span class="w"> </span><span class="nf">is_100</span><span class="p">(</span><span class="n">status_code</span><span class="p">):</span>
|
||||
<span class="k">return</span> <span class="n">_is_category</span><span class="p">(</span><span class="mi">100</span><span class="p">,</span> <span class="n">status_code</span><span class="p">)</span></div>
|
||||
|
||||
|
||||
|
||||
<div class="viewcode-block" id="is_200">
|
||||
<a class="viewcode-back" href="../../api.html#responder.status_codes.is_200">[docs]</a>
|
||||
<span class="k">def</span><span class="w"> </span><span class="nf">is_200</span><span class="p">(</span><span class="n">status_code</span><span class="p">):</span>
|
||||
<span class="k">return</span> <span class="n">_is_category</span><span class="p">(</span><span class="mi">200</span><span class="p">,</span> <span class="n">status_code</span><span class="p">)</span></div>
|
||||
|
||||
|
||||
|
||||
<div class="viewcode-block" id="is_300">
|
||||
<a class="viewcode-back" href="../../api.html#responder.status_codes.is_300">[docs]</a>
|
||||
<span class="k">def</span><span class="w"> </span><span class="nf">is_300</span><span class="p">(</span><span class="n">status_code</span><span class="p">):</span>
|
||||
<span class="k">return</span> <span class="n">_is_category</span><span class="p">(</span><span class="mi">300</span><span class="p">,</span> <span class="n">status_code</span><span class="p">)</span></div>
|
||||
|
||||
|
||||
|
||||
<div class="viewcode-block" id="is_400">
|
||||
<a class="viewcode-back" href="../../api.html#responder.status_codes.is_400">[docs]</a>
|
||||
<span class="k">def</span><span class="w"> </span><span class="nf">is_400</span><span class="p">(</span><span class="n">status_code</span><span class="p">):</span>
|
||||
<span class="k">return</span> <span class="n">_is_category</span><span class="p">(</span><span class="mi">400</span><span class="p">,</span> <span class="n">status_code</span><span class="p">)</span></div>
|
||||
|
||||
|
||||
|
||||
<div class="viewcode-block" id="is_500">
|
||||
<a class="viewcode-back" href="../../api.html#responder.status_codes.is_500">[docs]</a>
|
||||
<span class="k">def</span><span class="w"> </span><span class="nf">is_500</span><span class="p">(</span><span class="n">status_code</span><span class="p">):</span>
|
||||
<span class="k">return</span> <span class="n">_is_category</span><span class="p">(</span><span class="mi">500</span><span class="p">,</span> <span class="n">status_code</span><span class="p">)</span></div>
|
||||
|
||||
</pre></div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="sphinxsidebar" role="navigation" aria-label="Main">
|
||||
<div class="sphinxsidebarwrapper"><p class="logo">
|
||||
<a href="../../index.html">
|
||||
<img class="logo" src="../../_static/responder.png" />
|
||||
</a>
|
||||
</p>
|
||||
<p>
|
||||
<strong>Responder</strong> — a familiar HTTP service framework for Python.
|
||||
<br />
|
||||
<small>v3.6.2</small>
|
||||
</p>
|
||||
<h3>Useful Links</h3>
|
||||
<ul>
|
||||
<li><a href="https://github.com/kennethreitz/responder">Responder @ GitHub</a></li>
|
||||
<li><a href="https://pypi.org/project/responder/">Responder @ PyPI</a></li>
|
||||
<li><a href="https://github.com/kennethreitz/responder/issues">Issue Tracker</a></li>
|
||||
</ul>
|
||||
<search id="searchbox" style="display: none" role="search">
|
||||
<h3 id="searchlabel">Quick search</h3>
|
||||
<div class="searchformwrapper">
|
||||
<form class="search" action="../../search.html" method="get">
|
||||
<input type="text" name="q" aria-labelledby="searchlabel" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"/>
|
||||
<input type="submit" value="Go" />
|
||||
</form>
|
||||
</div>
|
||||
</search>
|
||||
<script>document.getElementById('searchbox').style.display = "block"</script>
|
||||
</div>
|
||||
</div>
|
||||
<div class="clearer"></div>
|
||||
</div>
|
||||
<div class="footer">
|
||||
©2018-2026, Kenneth Reitz.
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,182 @@
|
||||
API Reference
|
||||
=============
|
||||
|
||||
This page documents Responder's public Python API. For usage examples
|
||||
and explanations, see the :doc:`quickstart` and :doc:`tour`.
|
||||
|
||||
|
||||
The API Class
|
||||
-------------
|
||||
|
||||
The central object of every Responder application. It holds your routes,
|
||||
middleware, templates, and configuration. Create one at the top of your
|
||||
module and use it to define your entire web service.
|
||||
|
||||
Quick example::
|
||||
|
||||
import responder
|
||||
|
||||
api = responder.API(
|
||||
title="My Service", # OpenAPI title
|
||||
version="1.0", # OpenAPI version
|
||||
openapi="3.0.2", # enable OpenAPI
|
||||
docs_route="/docs", # Swagger UI at /docs
|
||||
cors=True, # enable CORS
|
||||
secret_key="change-me", # session signing key
|
||||
allowed_hosts=["example.com"],
|
||||
)
|
||||
|
||||
.. module:: responder
|
||||
|
||||
.. autoclass:: API
|
||||
:inherited-members:
|
||||
|
||||
|
||||
Request
|
||||
-------
|
||||
|
||||
The request object is passed into every view as the first argument. It
|
||||
gives you access to everything the client sent — headers, query
|
||||
parameters, the request body, cookies, and more.
|
||||
|
||||
Most properties are synchronous, but reading the body requires ``await``
|
||||
because it involves I/O.
|
||||
|
||||
Common patterns::
|
||||
|
||||
# Headers (case-insensitive)
|
||||
token = req.headers.get("Authorization")
|
||||
|
||||
# Query parameters: /search?q=python&page=2
|
||||
query = req.params["q"]
|
||||
|
||||
# JSON body
|
||||
data = await req.media()
|
||||
|
||||
# Form data
|
||||
form = await req.media("form")
|
||||
|
||||
# File uploads
|
||||
files = await req.media("files")
|
||||
|
||||
# Client info
|
||||
ip, port = req.client
|
||||
is_https = req.is_secure
|
||||
|
||||
.. autoclass:: Request
|
||||
:inherited-members:
|
||||
|
||||
|
||||
Response
|
||||
--------
|
||||
|
||||
The response object is passed into every view as the second argument.
|
||||
Mutate it to control what gets sent back to the client — the body,
|
||||
status code, headers, and cookies.
|
||||
|
||||
Common patterns::
|
||||
|
||||
resp.text = "plain text" # text/plain
|
||||
resp.html = "<h1>Hello</h1>" # text/html
|
||||
resp.media = {"key": "value"} # application/json
|
||||
resp.content = b"raw bytes" # application/octet-stream
|
||||
resp.file("path/to/file.pdf") # auto content-type
|
||||
resp.stream_file("large/export.csv") # streamed
|
||||
|
||||
resp.status_code = 201
|
||||
resp.headers["X-Custom"] = "value"
|
||||
resp.cookies["session"] = "abc123"
|
||||
|
||||
.. autoclass:: Response
|
||||
:inherited-members:
|
||||
|
||||
|
||||
Route Groups
|
||||
------------
|
||||
|
||||
Group related routes under a shared URL prefix — useful for API versioning
|
||||
and organizing large applications::
|
||||
|
||||
v1 = api.group("/v1")
|
||||
|
||||
@v1.route("/users")
|
||||
def list_users(req, resp):
|
||||
resp.media = []
|
||||
|
||||
.. autoclass:: responder.api.RouteGroup
|
||||
:members:
|
||||
|
||||
|
||||
Background Queue
|
||||
----------------
|
||||
|
||||
Run tasks in background threads without blocking the response. Available
|
||||
as ``api.background``::
|
||||
|
||||
@api.route("/submit")
|
||||
async def submit(req, resp):
|
||||
data = await req.media()
|
||||
|
||||
@api.background.task
|
||||
def process(data):
|
||||
# runs in a thread pool
|
||||
...
|
||||
|
||||
process(data)
|
||||
resp.media = {"status": "accepted"}
|
||||
|
||||
.. autoclass:: responder.background.BackgroundQueue
|
||||
:members:
|
||||
|
||||
|
||||
Query Dict
|
||||
----------
|
||||
|
||||
A dictionary subclass for query string parameters with multi-value support.
|
||||
Behaves like a normal dict for single values, but supports ``getlist()``
|
||||
for parameters that appear multiple times (e.g. ``?tag=a&tag=b``).
|
||||
|
||||
.. autoclass:: responder.models.QueryDict
|
||||
:members:
|
||||
|
||||
|
||||
Rate Limiter
|
||||
------------
|
||||
|
||||
In-memory token bucket rate limiter. Limits requests per client IP address
|
||||
and returns ``429 Too Many Requests`` when exceeded::
|
||||
|
||||
from responder.ext.ratelimit import RateLimiter
|
||||
|
||||
limiter = RateLimiter(requests=100, period=60) # 100 req/min
|
||||
limiter.install(api)
|
||||
|
||||
Response headers: ``X-RateLimit-Limit``, ``X-RateLimit-Remaining``,
|
||||
and ``Retry-After`` (when limited).
|
||||
|
||||
.. autoclass:: responder.ext.ratelimit.RateLimiter
|
||||
:members:
|
||||
|
||||
|
||||
Status Code Helpers
|
||||
-------------------
|
||||
|
||||
Convenience functions for checking which category a status code falls
|
||||
into. Useful in middleware and after-request hooks::
|
||||
|
||||
from responder.status_codes import is_200, is_400, is_500
|
||||
|
||||
@api.after_request()
|
||||
def log_errors(req, resp):
|
||||
if is_400(resp.status_code) or is_500(resp.status_code):
|
||||
print(f"Error: {req.method} {req.url.path} -> {resp.status_code}")
|
||||
|
||||
.. autofunction:: responder.status_codes.is_100
|
||||
|
||||
.. autofunction:: responder.status_codes.is_200
|
||||
|
||||
.. autofunction:: responder.status_codes.is_300
|
||||
|
||||
.. autofunction:: responder.status_codes.is_400
|
||||
|
||||
.. autofunction:: responder.status_codes.is_500
|
||||
@@ -0,0 +1,8 @@
|
||||
# Backlog
|
||||
|
||||
## Future Ideas
|
||||
- WebSocket before_request short-circuit support (reject before accept)
|
||||
- Per-route rate limiting (different limits for different endpoints)
|
||||
- Built-in structured logging with request context
|
||||
- OpenAPI 3.1 support
|
||||
- Dependency injection for route handlers
|
||||
@@ -0,0 +1,580 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
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).
|
||||
|
||||
## [v3.6.2] - 2026-04-12
|
||||
|
||||
### Fixed
|
||||
|
||||
- GraphQL error responses now correctly return 400 status instead of always 200
|
||||
- OpenAPI docs UI now respects custom `openapi_route` instead of hardcoding `/schema.yml`
|
||||
- `before_requests` default type mismatch that could crash routes called outside the router
|
||||
- Blocking synchronous file I/O in `Response.stream_file()` — now uses async I/O via anyio
|
||||
- Memory leak in rate limiter (empty bucket keys never cleaned up)
|
||||
- Race condition in rate limiter `check()` — added thread-safe locking
|
||||
- WSGI fallback catching all `TypeError`s instead of just call-signature mismatches
|
||||
- Pydantic request/response model validation crashing on non-dict bodies
|
||||
- Test assertions that could never fail (`or True`, `< 500` patterns)
|
||||
- `CaseInsensitiveDict` missing `__delitem__`, `pop`, and `setdefault` overrides
|
||||
- `assert` used for input validation in OpenAPI extension (stripped by `python -O`)
|
||||
- Potential XSS in GraphiQL template endpoint injection
|
||||
- Dead `or ""` in media format detection logic
|
||||
|
||||
### Changed
|
||||
|
||||
- `DELETE` requests now participate in Pydantic request body validation
|
||||
- Simplified status code category check to use chained comparison
|
||||
|
||||
### Removed
|
||||
|
||||
- Unused `method` parameter from `load_target()`
|
||||
- Unused Node.js setup step from CI test workflow
|
||||
|
||||
## [v3.6.1] - 2026-04-12
|
||||
|
||||
### Added
|
||||
|
||||
- Configurable GZip compression via `gzip` parameter on `API()` (defaults to `True`)
|
||||
|
||||
## [v3.6.0] - 2026-03-24
|
||||
|
||||
### Added
|
||||
|
||||
- Built-in structured logging with per-request context (`enable_logging=True`)
|
||||
- `api.log` — always-available logger, enriched with request context when logging is enabled
|
||||
- Automatic access logging with timing: `GET /path → 200 (1.2ms)`
|
||||
- Request ID generation/forwarding via `X-Request-ID` header
|
||||
- `contextvars`-based request context (ID, method, path, client IP) on every log record
|
||||
- `responder.ext.logging` module: `get_logger()`, `RequestContext`, `RequestContextFilter`
|
||||
- CLAUDE.md project guide and `/release` command
|
||||
- Version number in docs sidebar
|
||||
|
||||
### Changed
|
||||
|
||||
- Comprehensive documentation improvements across all pages
|
||||
- Deployment: health checks, Docker Compose, Caddy, Procfile, production checklist
|
||||
- API reference: usage examples for every class
|
||||
- Feature tour: Pydantic validation, content negotiation, structured logging sections
|
||||
- Tutorials: modernized SQLAlchemy to `mapped_column()`, fixed deprecated `datetime.utcnow()`,
|
||||
WebSocket `WebSocketDisconnect` handling, role-based auth, auth strategy guide
|
||||
- Testing: rate limiting and WSGI mount examples
|
||||
- Middleware: pure ASGI middleware example
|
||||
- Quickstart: links to all tutorials
|
||||
- Sandbox: full rewrite with project layout
|
||||
- Docker example uses `uv` instead of pip
|
||||
- Backlog updated: removed implemented features, replaced HTTP/2 server push with dependency injection
|
||||
|
||||
### Removed
|
||||
|
||||
- `uv.lock` — this is a library, not an application
|
||||
|
||||
## [v3.5.0] - 2026-03-24
|
||||
|
||||
### Added
|
||||
|
||||
- CI validation for Python 3.14, 3.14 free-threaded, and PyPy 3.11
|
||||
- Marimo notebook mounting docs and example
|
||||
- Type annotations for `routes.py`
|
||||
|
||||
### Changed
|
||||
|
||||
- Replaced deprecated `asyncio.iscoroutinefunction` with `inspect.iscoroutinefunction` ahead of Python 3.16 removal
|
||||
- Narrowed broad `except Exception` to specific exceptions in response model serialization and websocket chat example
|
||||
- Improved GraphQL API interface with expanded test coverage
|
||||
- Code formatting cleanup via pyproject-fmt and ruff
|
||||
- Dropped Python 3.9 from CI
|
||||
|
||||
### Fixed
|
||||
|
||||
- WSGI mount returning 400 when requesting the exact mount root path
|
||||
- Werkzeug 3.1.7 compatibility for trusted host validation in tests
|
||||
- `future.result` bare property access in background task test (now properly calls `future.result()`)
|
||||
- OpenAPI template packaging and static file serving
|
||||
- RST title underline warning breaking docs CI
|
||||
|
||||
### Removed
|
||||
|
||||
- Read the Docs configuration (docs hosted on GitHub Pages)
|
||||
|
||||
## [v3.4.0] - 2026-03-22
|
||||
|
||||
### Changed
|
||||
|
||||
- Upgraded to Starlette 1.0
|
||||
- Added comprehensive docstrings across the codebase
|
||||
- Expanded API reference documentation
|
||||
|
||||
## [v3.3.0] - 2026-03-22
|
||||
|
||||
### Added
|
||||
|
||||
- Full documentation rewrite: tutorials for REST APIs, SQLAlchemy, Flask migration
|
||||
- Auth, WebSocket, middleware, and configuration guides
|
||||
- Testing docs with prose, examples, and tips
|
||||
- GitHub Pages deployment for docs
|
||||
|
||||
### Changed
|
||||
|
||||
- Reworked homepage prose
|
||||
- Rewrote CLI and API reference docs
|
||||
|
||||
## [v3.2.0] - 2026-03-22
|
||||
|
||||
### Added
|
||||
|
||||
- Pydantic auto-validation: `request_model` validates input, returns 422 on failure
|
||||
- Pydantic auto-serialization: `response_model` strips extra fields from responses
|
||||
- Server-Sent Events: `@resp.sse` for real-time streaming
|
||||
- `resp.stream_file()` for streaming large files without loading into memory
|
||||
- `@api.after_request()` hooks
|
||||
- `api.group("/prefix")` for route groups and API versioning
|
||||
- `api.graphql("/path", schema=schema)` one-liner GraphQL setup
|
||||
- `api = responder.API(request_id=True)` for automatic request ID generation
|
||||
- Built-in rate limiter: `RateLimiter(requests=100, period=60).install(api)`
|
||||
- MessagePack format support: `await req.media("msgpack")`
|
||||
- `req.is_json`, `req.path_params`, `req.client` properties
|
||||
- `api.exception_handler()` decorator for custom error handling
|
||||
- Lifespan context manager support
|
||||
- `uuid` and `path` route convertors
|
||||
- PEP 561 `py.typed` marker
|
||||
- Pydantic support for OpenAPI schema generation
|
||||
|
||||
### Changed
|
||||
|
||||
- Dependencies flattened: `pip install responder` gets everything
|
||||
- Core deps reduced to starlette + uvicorn
|
||||
- TestClient lazy-loaded (no httpx import in production)
|
||||
- Before-request hooks can short-circuit by setting status code
|
||||
- Removed poethepoet task runner
|
||||
|
||||
### Fixed
|
||||
|
||||
- Multipart parser losing headers when parts have multiple headers
|
||||
- `url_for()` with typed route params (`{id:int}`)
|
||||
- `resp.body` encoding crash on bytes content
|
||||
- GraphQL text query missing `await`
|
||||
- Streaming responses not sending Content-Type headers
|
||||
- Python 3.9 compatibility for union type syntax
|
||||
|
||||
## [v3.0.0] - 2026-03-22
|
||||
|
||||
### Added
|
||||
|
||||
- Platform: Added support for Python 3.10 - Python 3.13
|
||||
- CLI: `responder run` now also accepts a filesystem path on its `<target>`
|
||||
argument, enabling usage on single-file applications.
|
||||
- CLI: `responder run` now also accepts URLs.
|
||||
|
||||
### Changed
|
||||
|
||||
- Platform: Minimum Python version is now 3.9 (dropped 3.6, 3.7, 3.8)
|
||||
- Dependencies: Dramatically reduced core dependency count (10 → 5)
|
||||
- Removed `requests`, `requests-toolbelt`, `rfc3986`, `whitenoise`
|
||||
- Moved `apispec` and `marshmallow` to `openapi` optional extra
|
||||
- Replaced `rfc3986` with stdlib `urllib.parse`
|
||||
- Replaced `requests-toolbelt` multipart decoder with `python-multipart`
|
||||
- Replaced deprecated `starlette.middleware.wsgi` with `a2wsgi`
|
||||
- Switched from WhiteNoise to ServeStatic
|
||||
- Dependencies: Pinned `starlette[full]>=0.40` (was unpinned)
|
||||
- GraphQL: Upgraded to `graphene>=3` and `graphql-core>=3.1`
|
||||
(from `graphene<3` and `graphql-server-core`, which is unmaintained)
|
||||
- GraphQL: Updated GraphiQL UI from 0.12.0 (2018) to 3.0.6 with React 18
|
||||
- Extensions: All of CLI-, GraphQL-, and OpenAPI-Support modules are
|
||||
extensions now, found within the `responder.ext` module namespace.
|
||||
- Packaging: Migrated from `setup.py` to declarative `pyproject.toml`
|
||||
|
||||
### Removed
|
||||
|
||||
- Platform: Removed support for EOL Python 3.6, 3.7, 3.8
|
||||
- Status codes: Removed deprecated `resume_incomplete` and `resume`
|
||||
aliases for HTTP 308 (marked for removal in 3.0)
|
||||
- CLI: `responder run --build` ceased to exist
|
||||
|
||||
### Fixed
|
||||
|
||||
- Routing: Fixed dispatching `static_route=None` on Windows
|
||||
- uvicorn: `--debug` now maps to uvicorn's `log_level = "debug"`
|
||||
- Tests: Fixed deprecated httpx TestClient usage
|
||||
|
||||
## [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
|
||||
|
||||
- ASGI 3 support
|
||||
- CI tests for python 3.8-dev
|
||||
- Now requests have `state` a mapping object
|
||||
|
||||
### Deprecated
|
||||
|
||||
- ASGI 2
|
||||
|
||||
## [v1.3.1] - 2019-04-28
|
||||
|
||||
### Added
|
||||
|
||||
- Route params Converters
|
||||
- Add search for documentation pages
|
||||
|
||||
### Changed
|
||||
|
||||
- Bump dependencies
|
||||
|
||||
## [v1.3.0] - 2019-02-22
|
||||
|
||||
### Fixed
|
||||
|
||||
- Versioning issue
|
||||
- Multiple cookies.
|
||||
- Whitenoise returns not found.
|
||||
- Other bugfixes.
|
||||
|
||||
### Added
|
||||
|
||||
- Stream support via `resp.stream`.
|
||||
- Cookie directives via `resp.set_cookie`.
|
||||
- Add `resp.html` to send HTML.
|
||||
- Other improvements.
|
||||
|
||||
## [v1.1.3] - 2019-01-12
|
||||
|
||||
### Changed
|
||||
|
||||
- Refactor `_route_for`
|
||||
|
||||
### Fixed
|
||||
|
||||
- Resolve startup/shutdwown events
|
||||
|
||||
## [v1.2.0] - 2018-12-29
|
||||
|
||||
### Added
|
||||
|
||||
- Documentations
|
||||
|
||||
### Changed
|
||||
|
||||
- Use Starlette's LifeSpan middleware
|
||||
- Update denpendencies
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix route.is_class_based
|
||||
- Fix test_500
|
||||
- Typos
|
||||
|
||||
## [v1.1.2] - 2018-11-11
|
||||
|
||||
### Fixed
|
||||
|
||||
- Minor fixes for Open API
|
||||
- Typos
|
||||
|
||||
## [v1.1.1] - 2018-10-29
|
||||
|
||||
### Changed
|
||||
|
||||
- Run sync views in a threadpoolexecutor.
|
||||
|
||||
## [v1.1.0] - 2018-10-27
|
||||
|
||||
### Added
|
||||
|
||||
- Support for `before_request`.
|
||||
|
||||
## [v1.0.5]- 2018-10-27
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix sessions.
|
||||
|
||||
## [v1.0.4] - 2018-10-27
|
||||
|
||||
### Fixed
|
||||
|
||||
- Potential bufix for cookies.
|
||||
|
||||
## [v1.0.3] - 2018-10-27
|
||||
|
||||
### Fixed
|
||||
|
||||
- Bugfix for redirects.
|
||||
|
||||
## [v1.0.2] - 2018-10-27
|
||||
|
||||
### Changed
|
||||
|
||||
- Improvement for static file hosting.
|
||||
|
||||
## [v1.0.1] - 2018-10-26
|
||||
|
||||
### Changed
|
||||
|
||||
- Improve cors configuration settings.
|
||||
|
||||
## [v1.0.0] - 2018-10-26
|
||||
|
||||
### Changed
|
||||
|
||||
- Move GraphQL support into a built-in plugin.
|
||||
|
||||
## [v0.3.3] - 2018-10-25
|
||||
|
||||
### Added
|
||||
|
||||
- CORS support
|
||||
|
||||
### Changed
|
||||
|
||||
- Improved exceptions.
|
||||
|
||||
## [v0.3.2] - 2018-10-25
|
||||
|
||||
### Changed
|
||||
|
||||
- Subtle improvements.
|
||||
|
||||
## [v0.3.1] - 2018-10-24
|
||||
|
||||
### Fixed
|
||||
|
||||
- Packaging fix.
|
||||
|
||||
## [v0.3.0] - 2018-10-24
|
||||
|
||||
### Changed
|
||||
|
||||
- Interactive Documentation endpoint.
|
||||
- Minor improvements.
|
||||
|
||||
## [v0.2.3] - 2018-10-24
|
||||
|
||||
### Changed
|
||||
|
||||
- Overall improvements.
|
||||
|
||||
## [v0.2.2] - 2018-10-23
|
||||
|
||||
### Added
|
||||
|
||||
- Show traceback info when background tasks raise exceptions.
|
||||
|
||||
## [v0.2.1] - 2018-10-23
|
||||
|
||||
### Added
|
||||
|
||||
- api.requests.
|
||||
|
||||
## [v0.2.0] - 2018-10-22
|
||||
|
||||
### Added
|
||||
|
||||
- WebSocket support.
|
||||
|
||||
## [v0.1.6] - 2018-10-20
|
||||
|
||||
### Added
|
||||
|
||||
- 500 support.
|
||||
|
||||
## [v0.1.5] - 2018-10-20
|
||||
|
||||
### Added
|
||||
|
||||
- File upload support
|
||||
|
||||
### Changed
|
||||
|
||||
- Improvements to sequential media reading.
|
||||
|
||||
## [v0.1.4] - 2018-10-19
|
||||
|
||||
### Fixed
|
||||
|
||||
- Stability.
|
||||
|
||||
## [v0.1.3] - 2018-10-18
|
||||
|
||||
### Added
|
||||
|
||||
- Sessions support.
|
||||
|
||||
## [v0.1.2] - 2018-10-18
|
||||
|
||||
### Added
|
||||
|
||||
- Cookies support.
|
||||
|
||||
## [v0.1.1] - 2018-10-17
|
||||
|
||||
### Changed
|
||||
|
||||
- Default routes.
|
||||
|
||||
## [v0.1.0] - 2018-10-17
|
||||
|
||||
### Added
|
||||
|
||||
- Prototype of static application support.
|
||||
|
||||
## [v0.0.10] - 2018-10-17
|
||||
|
||||
### Fixed
|
||||
|
||||
- Bugfix for async class-based views.
|
||||
|
||||
## [v0.0.9] - 2018-10-17
|
||||
|
||||
### Fixed
|
||||
|
||||
- Bugfix for async class-based views.
|
||||
|
||||
## [v0.0.8] - 2018-10-17
|
||||
|
||||
### Added
|
||||
|
||||
- GraphiQL Support.
|
||||
|
||||
### Changed
|
||||
|
||||
- Improvement to route selection.
|
||||
|
||||
## [v0.0.7] - 2018-10-16
|
||||
|
||||
### Changed
|
||||
|
||||
- Immutable Request object.
|
||||
|
||||
## [v0.0.6] - 2018-10-16
|
||||
|
||||
### Added
|
||||
|
||||
- Ability to mount WSGI apps.
|
||||
- Supply content-type when serving up the schema.
|
||||
|
||||
## [v0.0.5] - 2018-10-15
|
||||
|
||||
### Added
|
||||
|
||||
- OpenAPI Schema support.
|
||||
- Safe load/dump yaml.
|
||||
|
||||
## [v0.0.4] - 2018-10-15
|
||||
|
||||
### Added
|
||||
|
||||
- Asynchronous support for data uploads.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Bug fixes.
|
||||
|
||||
## [v0.0.3] - 2018-10-13
|
||||
|
||||
### Fixed
|
||||
|
||||
- Bug fixes.
|
||||
|
||||
## [v0.0.2] - 2018-10-13
|
||||
|
||||
### Changed
|
||||
|
||||
- Switch to ASGI/Starlette.
|
||||
|
||||
## [v0.0.1] - 2018-10-12
|
||||
|
||||
### Added
|
||||
|
||||
- Conception!
|
||||
|
||||
[v3.6.2]: https://github.com/kennethreitz/responder/compare/v3.6.1..v3.6.2
|
||||
[v3.6.1]: https://github.com/kennethreitz/responder/compare/v3.6.0..v3.6.1
|
||||
[v3.6.0]: https://github.com/kennethreitz/responder/compare/v3.5.0..v3.6.0
|
||||
[v3.5.0]: https://github.com/kennethreitz/responder/compare/v3.4.0..v3.5.0
|
||||
[v3.4.0]: https://github.com/kennethreitz/responder/compare/v3.3.0..v3.4.0
|
||||
[v3.3.0]: https://github.com/kennethreitz/responder/compare/v3.2.0..v3.3.0
|
||||
[v3.2.0]: https://github.com/kennethreitz/responder/compare/v3.0.0..v3.2.0
|
||||
[v3.0.0]: https://github.com/kennethreitz/responder/compare/v2.0.5..v3.0.0
|
||||
[v2.0.5]: https://github.com/kennethreitz/responder/compare/v2.0.4..v2.0.5
|
||||
[v2.0.4]: https://github.com/kennethreitz/responder/compare/v2.0.3..v2.0.4
|
||||
[v2.0.3]: https://github.com/kennethreitz/responder/compare/v2.0.2..v2.0.3
|
||||
[v2.0.2]: https://github.com/kennethreitz/responder/compare/v2.0.1..v2.0.2
|
||||
[v2.0.1]: https://github.com/kennethreitz/responder/compare/v2.0.0..v2.0.1
|
||||
[v2.0.0]: https://github.com/kennethreitz/responder/compare/v1.3.2..v2.0.0
|
||||
[v1.3.2]: https://github.com/kennethreitz/responder/compare/v1.3.1..v1.3.2
|
||||
[v1.3.1]: https://github.com/kennethreitz/responder/compare/v1.3.0..v1.3.1
|
||||
[v1.3.0]: https://github.com/kennethreitz/responder/compare/v1.2.0..v1.3.0
|
||||
[v1.2.0]: https://github.com/kennethreitz/responder/compare/v1.1.3..v1.2.0
|
||||
[v1.1.3]: https://github.com/kennethreitz/responder/compare/v1.1.2..v1.1.3
|
||||
[v1.1.2]: https://github.com/kennethreitz/responder/compare/v1.1.1..v1.1.2
|
||||
[v1.1.1]: https://github.com/kennethreitz/responder/compare/v1.1.0..v1.1.1
|
||||
[v1.1.0]: https://github.com/kennethreitz/responder/compare/v1.0.5..v1.1.0
|
||||
[v1.0.5]: https://github.com/kennethreitz/responder/compare/v1.0.4..v1.0.5
|
||||
[v1.0.4]: https://github.com/kennethreitz/responder/compare/v1.0.3..v1.0.4
|
||||
[v1.0.3]: https://github.com/kennethreitz/responder/compare/v1.0.2..v1.0.3
|
||||
[v1.0.2]: https://github.com/kennethreitz/responder/compare/v1.0.1..v1.0.2
|
||||
[v1.0.1]: https://github.com/kennethreitz/responder/compare/v1.0.0..v1.0.1
|
||||
[v1.0.0]: https://github.com/kennethreitz/responder/compare/v0.3.3..v1.0.0
|
||||
[v0.3.3]: https://github.com/kennethreitz/responder/compare/v0.3.2..v0.3.3
|
||||
[v0.3.2]: https://github.com/kennethreitz/responder/compare/v0.3.1..v0.3.2
|
||||
[v0.3.1]: https://github.com/kennethreitz/responder/compare/v0.3.0..v0.3.1
|
||||
[v0.3.0]: https://github.com/kennethreitz/responder/compare/v0.2.3..v0.3.0
|
||||
[v0.2.3]: https://github.com/kennethreitz/responder/compare/v0.2.2..v0.2.3
|
||||
[v0.2.2]: https://github.com/kennethreitz/responder/compare/v0.2.1..v0.2.2
|
||||
[v0.2.1]: https://github.com/kennethreitz/responder/compare/v0.2.0..v0.2.1
|
||||
[v0.2.0]: https://github.com/kennethreitz/responder/compare/v0.1.6..v0.2.0
|
||||
[v0.1.6]: https://github.com/kennethreitz/responder/compare/v0.1.5..v0.1.6
|
||||
[v0.1.5]: https://github.com/kennethreitz/responder/compare/v0.1.4..v0.1.5
|
||||
[v0.1.4]: https://github.com/kennethreitz/responder/compare/v0.1.3..v0.1.4
|
||||
[v0.1.3]: https://github.com/kennethreitz/responder/compare/v0.1.2..v0.1.3
|
||||
[v0.1.2]: https://github.com/kennethreitz/responder/compare/v0.1.1..v0.1.2
|
||||
[v0.1.1]: https://github.com/kennethreitz/responder/compare/v0.1.0..v0.1.1
|
||||
[v0.1.0]: https://github.com/kennethreitz/responder/compare/v0.0.10..v0.1.0
|
||||
[v0.0.10]: https://github.com/kennethreitz/responder/compare/v0.0.9..v0.0.10
|
||||
[v0.0.9]: https://github.com/kennethreitz/responder/compare/v0.0.8..v0.0.9
|
||||
[v0.0.8]: https://github.com/kennethreitz/responder/compare/v0.0.7..v0.0.8
|
||||
[v0.0.7]: https://github.com/kennethreitz/responder/compare/v0.0.6..v0.0.7
|
||||
[v0.0.6]: https://github.com/kennethreitz/responder/compare/v0.0.5..v0.0.6
|
||||
[v0.0.5]: https://github.com/kennethreitz/responder/compare/v0.0.4..v0.0.5
|
||||
[v0.0.4]: https://github.com/kennethreitz/responder/compare/v0.0.3..v0.0.4
|
||||
[v0.0.3]: https://github.com/kennethreitz/responder/compare/v0.0.2..v0.0.3
|
||||
[v0.0.2]: https://github.com/kennethreitz/responder/compare/v0.0.1..v0.0.2
|
||||
[v0.0.1]: https://github.com/kennethreitz/responder/compare/v0.0.0..v0.0.1
|
||||
@@ -0,0 +1,100 @@
|
||||
Command Line Interface
|
||||
======================
|
||||
|
||||
Responder installs a ``responder`` command that lets you launch
|
||||
applications from the terminal. You can point it at a Python module,
|
||||
a local file, or even a URL — and it will find your ``API`` instance
|
||||
and start serving.
|
||||
|
||||
|
||||
Launching from a Module
|
||||
-----------------------
|
||||
|
||||
The most common way to run a Responder application in production. Use
|
||||
Python's standard dotted module path::
|
||||
|
||||
$ responder run acme.app
|
||||
|
||||
This imports ``acme.app`` and looks for an attribute called ``api``
|
||||
(a ``responder.API`` instance). It's the same import system Python
|
||||
uses everywhere — your ``PYTHONPATH`` and virtual environment are
|
||||
respected.
|
||||
|
||||
|
||||
Launching from a File
|
||||
---------------------
|
||||
|
||||
During development, you often have a single file you want to run::
|
||||
|
||||
$ responder run helloworld.py
|
||||
|
||||
This loads the file directly and starts the server. Quick and easy for
|
||||
prototyping and single-file applications.
|
||||
|
||||
You can test it with a simple HTTP request::
|
||||
|
||||
$ curl http://127.0.0.1:5042/hello
|
||||
hello, world!
|
||||
|
||||
|
||||
Launching from a URL
|
||||
--------------------
|
||||
|
||||
Responder can fetch and run a Python file from any URL — great for
|
||||
demos, sharing examples, and running code from GitHub::
|
||||
|
||||
$ responder run https://github.com/kennethreitz/responder/raw/refs/heads/main/examples/helloworld.py
|
||||
|
||||
This also works with ``github://`` URLs and any filesystem protocol
|
||||
supported by `fsspec <https://filesystem-spec.readthedocs.io/>`_::
|
||||
|
||||
$ responder run github://kennethreitz:responder@/examples/helloworld.py
|
||||
|
||||
Cloud storage is supported too — Azure Blob Storage, Google Cloud
|
||||
Storage, S3, HDFS, SFTP, and more. Install ``fsspec[full]`` for all
|
||||
protocols::
|
||||
|
||||
$ uv pip install 'fsspec[full]'
|
||||
|
||||
|
||||
Custom Instance Names
|
||||
---------------------
|
||||
|
||||
By default, Responder looks for an attribute called ``api``. If your
|
||||
application uses a different name, specify it with a colon::
|
||||
|
||||
$ responder run acme.app:service
|
||||
$ responder run myapp.py:application
|
||||
|
||||
For URLs, use a fragment::
|
||||
|
||||
$ responder run https://example.com/app.py#service
|
||||
|
||||
|
||||
Environment Variables
|
||||
---------------------
|
||||
|
||||
Responder automatically reads the ``PORT`` environment variable at
|
||||
runtime:
|
||||
|
||||
- ``PORT`` — bind to ``0.0.0.0`` on this port (cloud platform convention)
|
||||
|
||||
When ``PORT`` is set, the server binds to all interfaces automatically.
|
||||
This is how cloud platforms like Fly.io, Railway, and Heroku inject the
|
||||
listen port.
|
||||
|
||||
For other settings like ``SECRET_KEY``, read them in your application
|
||||
code and pass them to ``responder.API()``::
|
||||
|
||||
import os
|
||||
api = responder.API(secret_key=os.environ["SECRET_KEY"])
|
||||
|
||||
|
||||
Building Frontend Assets
|
||||
-------------------------
|
||||
|
||||
If your project includes a JavaScript frontend with a ``package.json``,
|
||||
the ``build`` subcommand runs ``npm run build``::
|
||||
|
||||
$ responder build
|
||||
$ responder build /path/to/frontend
|
||||
@@ -0,0 +1,187 @@
|
||||
Deployment
|
||||
==========
|
||||
|
||||
Responder applications are standard `ASGI <https://asgi.readthedocs.io/>`_
|
||||
apps. ASGI (Asynchronous Server Gateway Interface) is the modern successor
|
||||
to WSGI — it supports async, WebSockets, and HTTP/2. This means you can
|
||||
deploy a Responder app anywhere that runs Python, using any ASGI server.
|
||||
|
||||
|
||||
Running Locally
|
||||
---------------
|
||||
|
||||
During development, ``api.run()`` is all you need::
|
||||
|
||||
if __name__ == "__main__":
|
||||
api.run()
|
||||
|
||||
This starts a `uvicorn <https://www.uvicorn.org/>`_ server on
|
||||
``127.0.0.1:5042``. Uvicorn is a lightning-fast ASGI server built on
|
||||
`uvloop <https://uvloop.readthedocs.io/>`_ — it handles thousands of
|
||||
concurrent connections efficiently and protects against slowloris attacks,
|
||||
making a reverse proxy like nginx optional for many deployments.
|
||||
|
||||
|
||||
Docker
|
||||
------
|
||||
|
||||
Docker is the most common way to package and deploy web applications.
|
||||
Here's a minimal Dockerfile::
|
||||
|
||||
FROM python:3.13-slim
|
||||
WORKDIR /app
|
||||
COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
|
||||
COPY . .
|
||||
RUN uv pip install --system responder
|
||||
ENV PORT=80
|
||||
EXPOSE 80
|
||||
CMD ["python", "api.py"]
|
||||
|
||||
Build and run::
|
||||
|
||||
$ docker build -t myapi .
|
||||
$ docker run -p 8000:80 myapi
|
||||
|
||||
The ``python:3.13-slim`` image is about 150MB — small enough for fast
|
||||
deploys but includes everything you need. Using ``uv`` for installs
|
||||
is significantly faster than pip. For even smaller images, you can use
|
||||
``python:3.13-alpine``, though some packages may need extra build
|
||||
dependencies.
|
||||
|
||||
|
||||
Cloud Platforms
|
||||
---------------
|
||||
|
||||
Responder automatically honors the ``PORT`` environment variable. When
|
||||
``PORT`` is set, the server binds to ``0.0.0.0`` on that port — this is
|
||||
the convention that virtually every cloud platform uses.
|
||||
|
||||
This means zero configuration on:
|
||||
|
||||
- **Fly.io** — ``fly launch`` and you're done
|
||||
- **Railway** — push your code, Railway sets ``PORT``
|
||||
- **Render** — set start command to ``python api.py``
|
||||
- **Google Cloud Run** — containerize and deploy
|
||||
- **Azure Container Apps** — same pattern
|
||||
- **AWS App Runner** — and here too
|
||||
|
||||
The pattern is always the same: deploy your code, set the start command
|
||||
to ``python api.py``, and the platform handles the rest.
|
||||
|
||||
|
||||
Health Check Endpoint
|
||||
---------------------
|
||||
|
||||
Every production deployment needs a health check — a lightweight endpoint
|
||||
that monitoring tools, load balancers, and orchestrators can poll to verify
|
||||
your service is running::
|
||||
|
||||
@api.route("/health")
|
||||
def health(req, resp):
|
||||
resp.media = {"status": "healthy"}
|
||||
|
||||
Keep it simple. Don't query the database or do expensive work — the health
|
||||
check should return instantly. Cloud platforms, Docker, and Kubernetes all
|
||||
look for an HTTP 200 to confirm your service is alive.
|
||||
|
||||
For Docker, add a ``HEALTHCHECK`` instruction::
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=3s \
|
||||
CMD curl -f http://localhost/health || exit 1
|
||||
|
||||
|
||||
Uvicorn Directly
|
||||
----------------
|
||||
|
||||
For production deployments where you want more control, bypass
|
||||
``api.run()`` and use uvicorn directly::
|
||||
|
||||
$ uvicorn api:api --host 0.0.0.0 --port 8000 --workers 4
|
||||
|
||||
The ``--workers`` flag spawns multiple processes, each handling requests
|
||||
independently. A good starting point is 2-4 workers per CPU core.
|
||||
|
||||
Uvicorn supports many options — SSL certificates, access logging, graceful
|
||||
shutdown timeouts, and more. See the
|
||||
`uvicorn documentation <https://www.uvicorn.org/deployment/>`_ for details.
|
||||
|
||||
For platforms like Heroku or Railway that use a ``Procfile``::
|
||||
|
||||
web: uvicorn api:api --host 0.0.0.0 --port $PORT --workers 4
|
||||
|
||||
|
||||
Docker Compose
|
||||
--------------
|
||||
|
||||
For local development with databases and other services, Docker Compose
|
||||
ties everything together::
|
||||
|
||||
# docker-compose.yml
|
||||
services:
|
||||
api:
|
||||
build: .
|
||||
ports:
|
||||
- "5042:80"
|
||||
environment:
|
||||
- PORT=80
|
||||
- DATABASE_URL=postgresql+asyncpg://user:pass@db/myapp
|
||||
- SECRET_KEY=dev-secret
|
||||
depends_on:
|
||||
- db
|
||||
|
||||
db:
|
||||
image: docker.io/postgres:16-alpine
|
||||
environment:
|
||||
POSTGRES_USER: user
|
||||
POSTGRES_PASSWORD: pass
|
||||
POSTGRES_DB: myapp
|
||||
volumes:
|
||||
- pgdata:/var/lib/postgresql/data
|
||||
|
||||
volumes:
|
||||
pgdata:
|
||||
|
||||
Run with ``docker compose up``. The API waits for ``db`` to start, then
|
||||
connects using the ``DATABASE_URL`` environment variable.
|
||||
|
||||
|
||||
Reverse Proxy
|
||||
-------------
|
||||
|
||||
For high-traffic production deployments, you may want a reverse proxy like
|
||||
`nginx <https://nginx.org/>`_ or `Caddy <https://caddyserver.com/>`_ in
|
||||
front of your application for:
|
||||
|
||||
- **SSL/TLS termination** — let the proxy handle HTTPS certificates
|
||||
- **Load balancing** — distribute traffic across multiple app instances
|
||||
- **Static asset serving** — offload static files to the proxy
|
||||
- **Rate limiting** — at the infrastructure level
|
||||
|
||||
A minimal Caddy config that handles HTTPS automatically::
|
||||
|
||||
# Caddyfile
|
||||
example.com {
|
||||
reverse_proxy localhost:5042
|
||||
}
|
||||
|
||||
Responder's ``TrustedHostMiddleware`` and ``HTTPSRedirectMiddleware`` work
|
||||
correctly behind proxies that set standard forwarding headers
|
||||
(``X-Forwarded-For``, ``X-Forwarded-Proto``).
|
||||
|
||||
That said, uvicorn is production-ready on its own. Many applications run
|
||||
uvicorn directly without a reverse proxy and do just fine.
|
||||
|
||||
|
||||
Production Checklist
|
||||
--------------------
|
||||
|
||||
Before going live:
|
||||
|
||||
- **Set a secret key** — ``SECRET_KEY`` env var, never the default
|
||||
- **Disable debug mode** — ``DEBUG=false`` or omit it entirely
|
||||
- **Set allowed hosts** — restrict to your actual domain names
|
||||
- **Use multiple workers** — ``--workers 4`` or more, depending on CPU cores
|
||||
- **Add a health check** — ``/health`` endpoint for monitoring
|
||||
- **Enable HTTPS** — via your proxy, cloud platform, or uvicorn's ``--ssl-*`` flags
|
||||
- **Set up logging** — uvicorn logs requests by default; pipe them to your log aggregator
|
||||
- **Pin your dependencies** — use a lock file or pinned requirements for reproducible deploys
|
||||
@@ -0,0 +1,172 @@
|
||||
Configuration
|
||||
=============
|
||||
|
||||
Every application needs different settings for different environments —
|
||||
debug mode in development, real secrets in production, different database
|
||||
URLs for testing. This guide covers how to manage configuration cleanly.
|
||||
|
||||
|
||||
Environment Variables
|
||||
---------------------
|
||||
|
||||
The simplest and most universal approach. Environment variables work
|
||||
everywhere — locally, in Docker, on cloud platforms — and keep secrets
|
||||
out of your source code::
|
||||
|
||||
import os
|
||||
import responder
|
||||
|
||||
api = responder.API(
|
||||
debug=os.getenv("DEBUG", "false").lower() == "true",
|
||||
secret_key=os.environ["SECRET_KEY"],
|
||||
cors=os.getenv("CORS_ENABLED", "false").lower() == "true",
|
||||
)
|
||||
|
||||
Some variables Responder handles automatically:
|
||||
|
||||
- ``PORT`` — when set, the server binds to ``0.0.0.0`` on this port
|
||||
|
||||
Set variables in your shell::
|
||||
|
||||
$ export SECRET_KEY="your-secret-here"
|
||||
$ export DEBUG=true
|
||||
$ python app.py
|
||||
|
||||
Or in a ``.env`` file (don't commit this to git)::
|
||||
|
||||
SECRET_KEY=your-secret-here
|
||||
DEBUG=true
|
||||
|
||||
|
||||
Using .env Files
|
||||
----------------
|
||||
|
||||
For local development, a ``.env`` file is convenient. Install
|
||||
``python-dotenv`` and load it at the top of your app::
|
||||
|
||||
$ uv pip install python-dotenv
|
||||
|
||||
::
|
||||
|
||||
from dotenv import load_dotenv
|
||||
load_dotenv()
|
||||
|
||||
import os
|
||||
import responder
|
||||
|
||||
api = responder.API(
|
||||
secret_key=os.environ["SECRET_KEY"],
|
||||
)
|
||||
|
||||
Add ``.env`` to your ``.gitignore`` — never commit secrets.
|
||||
|
||||
|
||||
Configuration Class Pattern
|
||||
----------------------------
|
||||
|
||||
For larger applications, a configuration class keeps things organized::
|
||||
|
||||
import os
|
||||
|
||||
class Config:
|
||||
SECRET_KEY = os.environ.get("SECRET_KEY", "dev-secret")
|
||||
DEBUG = os.environ.get("DEBUG", "false").lower() == "true"
|
||||
DATABASE_URL = os.environ.get("DATABASE_URL", "sqlite:///dev.db")
|
||||
CORS_ORIGINS = os.environ.get("CORS_ORIGINS", "").split(",")
|
||||
|
||||
config = Config()
|
||||
|
||||
api = responder.API(
|
||||
debug=config.DEBUG,
|
||||
secret_key=config.SECRET_KEY,
|
||||
cors=bool(config.CORS_ORIGINS[0]),
|
||||
cors_params={"allow_origins": config.CORS_ORIGINS},
|
||||
)
|
||||
|
||||
This makes it easy to see all your settings in one place.
|
||||
|
||||
|
||||
Secret Key
|
||||
----------
|
||||
|
||||
The ``secret_key`` is used to sign session cookies. If someone knows your
|
||||
secret key, they can forge session data and impersonate any user.
|
||||
|
||||
Rules:
|
||||
|
||||
- **Never use the default** in production
|
||||
- **Generate a random key**: ``python -c "import secrets; print(secrets.token_hex(32))"``
|
||||
- **Store it in an environment variable**, not in code
|
||||
- **Rotate it** if it's ever compromised (this invalidates all sessions)
|
||||
|
||||
::
|
||||
|
||||
api = responder.API(secret_key=os.environ["SECRET_KEY"])
|
||||
|
||||
|
||||
Debug Mode
|
||||
----------
|
||||
|
||||
Debug mode controls error page behavior:
|
||||
|
||||
- **On** (``debug=True``): detailed error pages with tracebacks. Never
|
||||
use this in production — it exposes your source code.
|
||||
- **Off** (``debug=False``): generic error pages. This is the default.
|
||||
|
||||
::
|
||||
|
||||
api = responder.API(debug=True) # development only
|
||||
|
||||
A common pattern is to read it from the environment::
|
||||
|
||||
api = responder.API(debug=os.getenv("DEBUG") == "true")
|
||||
|
||||
|
||||
Allowed Hosts
|
||||
-------------
|
||||
|
||||
In production, always set ``allowed_hosts`` to prevent Host header
|
||||
attacks. This should match the domain names your application serves::
|
||||
|
||||
api = responder.API(
|
||||
allowed_hosts=["example.com", "www.example.com"],
|
||||
)
|
||||
|
||||
In development, you can use ``["*"]`` (the default) or specific local
|
||||
addresses::
|
||||
|
||||
api = responder.API(allowed_hosts=["localhost", "127.0.0.1"])
|
||||
|
||||
|
||||
Putting It All Together
|
||||
-----------------------
|
||||
|
||||
A production-ready configuration setup::
|
||||
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
import responder
|
||||
|
||||
api = responder.API(
|
||||
debug=os.getenv("DEBUG", "false") == "true",
|
||||
secret_key=os.environ["SECRET_KEY"],
|
||||
allowed_hosts=os.getenv("ALLOWED_HOSTS", "*").split(","),
|
||||
cors=bool(os.getenv("CORS_ORIGINS")),
|
||||
cors_params={
|
||||
"allow_origins": os.getenv("CORS_ORIGINS", "").split(","),
|
||||
"allow_methods": ["GET", "POST", "PUT", "DELETE"],
|
||||
},
|
||||
)
|
||||
|
||||
With a ``.env`` file for local development::
|
||||
|
||||
SECRET_KEY=dev-secret-do-not-use-in-prod
|
||||
DEBUG=true
|
||||
ALLOWED_HOSTS=localhost,127.0.0.1
|
||||
CORS_ORIGINS=http://localhost:3000
|
||||
|
||||
And environment variables set properly in production (via your cloud
|
||||
platform's dashboard, Docker secrets, or a secrets manager).
|
||||
@@ -0,0 +1,135 @@
|
||||
Responder
|
||||
=========
|
||||
|
||||
A familiar HTTP Service Framework for Python.
|
||||
|
||||
.. code:: python
|
||||
|
||||
import responder
|
||||
|
||||
api = responder.API()
|
||||
|
||||
@api.route("/{greeting}")
|
||||
async def greet_world(req, resp, *, greeting):
|
||||
resp.text = f"{greeting}, world!"
|
||||
|
||||
if __name__ == '__main__':
|
||||
api.run()
|
||||
|
||||
Powered by `Starlette`_, `uvicorn`_, and good intentions. The ``async`` is optional.
|
||||
|
||||
|
||||
The Idea
|
||||
--------
|
||||
|
||||
If you've ever used `Flask`_, the routing will look familiar. If you've
|
||||
used `Falcon`_, the request/response pattern will click immediately. And
|
||||
if you've used `Requests`_ — well, you'll feel right at home.
|
||||
|
||||
Responder takes these ideas and brings them together. Every view receives
|
||||
a request and a response. You read from one and write to the other. No
|
||||
return values, no special response classes, no boilerplate.
|
||||
|
||||
- ``resp.text`` sends text. ``resp.html`` sends HTML. ``resp.media`` sends JSON.
|
||||
- ``resp.file("path")`` serves a file. ``resp.content`` sends raw bytes.
|
||||
- ``req.headers`` is case-insensitive. ``req.params`` holds query parameters.
|
||||
- ``resp.status_code``, ``req.method``, ``req.url`` — the familiar ones.
|
||||
|
||||
Set ``resp.media`` to a dict and the right thing happens. If the client
|
||||
asks for YAML, it gets YAML. Content negotiation is automatic.
|
||||
|
||||
Responder and `FastAPI`_ are siblings — both built on Starlette, both
|
||||
born around the same time, both part of the push that made ASGI the
|
||||
future of Python web services. FastAPI went deep on type annotations
|
||||
and automatic validation. Responder went for simplicity and a mutable
|
||||
request/response pattern. Both projects are better for the other
|
||||
existing. Use whichever feels right.
|
||||
|
||||
This is a passion project. It exists because building a web framework
|
||||
from scratch is one of the best ways to understand how the web works.
|
||||
It's a great fit for personal projects, prototyping, teaching, research,
|
||||
and anyone who values a clean API over a sprawling ecosystem. If you
|
||||
need battle-tested infrastructure at scale, FastAPI and Django will
|
||||
serve you well. If you want something small, expressive, and fun to
|
||||
work with — welcome.
|
||||
|
||||
|
||||
What You Get
|
||||
------------
|
||||
|
||||
One ``pip install``, batteries included:
|
||||
|
||||
- Pydantic request validation and response serialization.
|
||||
- Mount Flask, Django, or any WSGI/ASGI app at a subroute.
|
||||
- Gzip compression, HSTS, CORS, and trusted host validation.
|
||||
- Before-request and after-request hooks for auth and logging.
|
||||
- A test client for fast, in-process testing with pytest.
|
||||
- Route parameters with f-string syntax and type convertors.
|
||||
- Lifespan context managers for startup and shutdown logic.
|
||||
- Custom exception handlers for clean error responses.
|
||||
- `GraphQL`_ with Graphene and a built-in GraphiQL IDE.
|
||||
- Server-Sent Events for real-time streaming.
|
||||
- File serving with automatic content-type detection.
|
||||
- Sync and async views — ``async`` is always optional.
|
||||
- Class-based views with ``on_get``, ``on_post``, ``on_request``.
|
||||
- Built-in rate limiting with ``X-RateLimit`` headers.
|
||||
- Structured logging with per-request context.
|
||||
- Content negotiation: JSON, YAML, and MessagePack.
|
||||
- A pleasant API with a single import statement.
|
||||
- OpenAPI schema generation with Swagger UI.
|
||||
- A production `uvicorn`_ server, ready to deploy.
|
||||
- Route groups for API versioning.
|
||||
- Signed cookie-based sessions.
|
||||
- Background tasks in a thread pool.
|
||||
- WebSocket support.
|
||||
|
||||
|
||||
Installation
|
||||
------------
|
||||
|
||||
.. code-block:: shell
|
||||
|
||||
$ uv pip install responder
|
||||
|
||||
Python 3.10 and above. That's it.
|
||||
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
:caption: User Guide
|
||||
|
||||
quickstart
|
||||
tour
|
||||
deployment
|
||||
testing
|
||||
api
|
||||
cli
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
:caption: Tutorials
|
||||
|
||||
tutorial-rest
|
||||
tutorial-sqlalchemy
|
||||
tutorial-auth
|
||||
tutorial-websockets
|
||||
tutorial-middleware
|
||||
tutorial-flask
|
||||
guide-config
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
:caption: Project
|
||||
|
||||
changes
|
||||
Sandbox <sandbox>
|
||||
backlog
|
||||
|
||||
|
||||
.. _Starlette: https://www.starlette.io/
|
||||
.. _uvicorn: https://www.uvicorn.org/
|
||||
.. _Flask: https://flask.palletsprojects.com/
|
||||
.. _Falcon: https://falconframework.org/
|
||||
.. _FastAPI: https://fastapi.tiangolo.com/
|
||||
.. _GraphQL: https://graphql.org/
|
||||
.. _Requests: https://requests.readthedocs.io/
|
||||
@@ -0,0 +1,384 @@
|
||||
Quick Start
|
||||
===========
|
||||
|
||||
This guide will walk you through the basics of building a web service with
|
||||
Responder. By the end, you'll understand how HTTP requests and responses
|
||||
work, how to define routes, read data from clients, send data back, render
|
||||
HTML templates, and process work in the background.
|
||||
|
||||
|
||||
Create a Web Service
|
||||
--------------------
|
||||
|
||||
Every web application starts with a single object — the application
|
||||
instance. In Responder, this is the ``API`` class. It holds your routes,
|
||||
middleware, templates, and configuration. Think of it as the central
|
||||
nervous system of your web service::
|
||||
|
||||
import responder
|
||||
|
||||
api = responder.API()
|
||||
|
||||
That's it. One import, one line. You now have a fully functional ASGI
|
||||
application with gzip compression, static file serving, session support,
|
||||
and a production-ready server — all wired up and ready to go.
|
||||
|
||||
|
||||
Hello World
|
||||
-----------
|
||||
|
||||
A web service isn't very useful until it can respond to requests. In HTTP,
|
||||
a *route* maps a URL path to a function that handles it. When a client
|
||||
(like a browser or ``curl``) sends a request to that path, your function
|
||||
runs and produces a response.
|
||||
|
||||
Here's the simplest possible route::
|
||||
|
||||
@api.route("/")
|
||||
def hello_world(req, resp):
|
||||
resp.text = "hello, world!"
|
||||
|
||||
Two things to notice:
|
||||
|
||||
1. Every view function receives two arguments: ``req`` (the incoming
|
||||
request) and ``resp`` (the outgoing response).
|
||||
2. You don't return anything. Instead, you *mutate* the response object
|
||||
directly. This is a deliberate design choice — it keeps the API
|
||||
consistent whether you're setting text, JSON, headers, cookies, or
|
||||
status codes.
|
||||
|
||||
|
||||
Run the Server
|
||||
--------------
|
||||
|
||||
Start your web service with a single call::
|
||||
|
||||
api.run()
|
||||
|
||||
This spins up a production-grade `uvicorn <https://www.uvicorn.org/>`_
|
||||
server on port ``5042``, ready for incoming HTTP requests. Open
|
||||
``http://localhost:5042`` in your browser and you'll see your hello world
|
||||
response.
|
||||
|
||||
You can customize the port with ``api.run(port=8000)``. The ``PORT``
|
||||
environment variable is also honored automatically — when set, Responder
|
||||
binds to ``0.0.0.0`` on that port, which is what cloud platforms expect.
|
||||
|
||||
.. note::
|
||||
|
||||
Both sync and async views are supported. The ``async`` keyword is always
|
||||
optional — use it when you need to ``await`` something, like reading a
|
||||
request body or querying a database.
|
||||
|
||||
|
||||
Route Parameters
|
||||
----------------
|
||||
|
||||
Static URLs like ``/about`` are useful, but most applications need dynamic
|
||||
routes — URLs that contain variable data, like a user ID or a product slug.
|
||||
|
||||
In Responder, you declare route parameters using Python's f-string syntax::
|
||||
|
||||
@api.route("/hello/{who}")
|
||||
def hello_to(req, resp, *, who):
|
||||
resp.text = f"hello, {who}!"
|
||||
|
||||
A ``GET`` request to ``/hello/world`` will respond with ``hello, world!``.
|
||||
A request to ``/hello/guido`` will respond with ``hello, guido!``.
|
||||
|
||||
Route parameters are passed as *keyword-only* arguments (after the ``*``
|
||||
in the function signature). This is a Python feature that makes the
|
||||
interface explicit — you always know which arguments come from the URL.
|
||||
|
||||
|
||||
Type Convertors
|
||||
^^^^^^^^^^^^^^^
|
||||
|
||||
By default, route parameters are strings. But often you want them as
|
||||
integers, UUIDs, or other types. Responder can convert them automatically
|
||||
using type annotations in the route pattern::
|
||||
|
||||
@api.route("/add/{a:int}/{b:int}")
|
||||
async def add(req, resp, *, a, b):
|
||||
resp.text = f"{a} + {b} = {a + b}"
|
||||
|
||||
Here, ``a`` and ``b`` will arrive as Python ``int`` objects, not strings.
|
||||
If someone requests ``/add/3/hello``, they'll get a 404 — the route won't
|
||||
match because ``hello`` isn't a valid integer.
|
||||
|
||||
Supported types:
|
||||
|
||||
- ``str`` — matches any string without slashes (this is the default)
|
||||
- ``int`` — matches digits and converts to ``int``
|
||||
- ``float`` — matches decimal numbers and converts to ``float``
|
||||
- ``uuid`` — matches UUID strings like ``550e8400-e29b-41d4-a716-446655440000``
|
||||
- ``path`` — matches any string *including* slashes, useful for file paths
|
||||
like ``/files/{filepath:path}``
|
||||
|
||||
|
||||
Sending Responses
|
||||
-----------------
|
||||
|
||||
When an HTTP server receives a request, it must send back a response. Every
|
||||
HTTP response has three parts: a status code (like ``200 OK`` or ``404 Not
|
||||
Found``), headers (metadata like ``Content-Type``), and a body (the actual
|
||||
data).
|
||||
|
||||
Responder lets you set all three by mutating the response object.
|
||||
|
||||
**Text and HTML** — the simplest response types. ``resp.text`` sets the
|
||||
``Content-Type`` to ``text/plain``, while ``resp.html`` sets it to
|
||||
``text/html``::
|
||||
|
||||
resp.text = "plain text response"
|
||||
resp.html = "<h1>HTML response</h1>"
|
||||
|
||||
**JSON** — the lingua franca of web APIs. Set ``resp.media`` to any
|
||||
JSON-serializable Python object — a dict, a list, whatever — and Responder
|
||||
will serialize it to JSON and set the right headers::
|
||||
|
||||
@api.route("/hello/{who}/json")
|
||||
def hello_json(req, resp, *, who):
|
||||
resp.media = {"hello": who}
|
||||
|
||||
If the client sends an ``Accept: application/x-yaml`` header, the same data
|
||||
will be returned as YAML instead. This is called *content negotiation* —
|
||||
the server and client agree on a format. It happens automatically.
|
||||
|
||||
**Files** — serve a file from disk. Responder uses Python's ``mimetypes``
|
||||
module to figure out the ``Content-Type`` from the file extension::
|
||||
|
||||
resp.file("reports/annual.pdf")
|
||||
|
||||
**Raw bytes** — for binary data like images or protocol buffers::
|
||||
|
||||
resp.content = b"\x89PNG\r\n..."
|
||||
|
||||
**Status codes** — HTTP status codes tell the client what happened. ``200``
|
||||
means success, ``201`` means something was created, ``404`` means not found,
|
||||
``500`` means the server broke. Set it directly::
|
||||
|
||||
resp.status_code = 201
|
||||
|
||||
**Headers** — HTTP headers carry metadata. Common ones include
|
||||
``Content-Type``, ``Cache-Control``, ``Authorization``, and custom
|
||||
application headers::
|
||||
|
||||
resp.headers["X-Custom"] = "value"
|
||||
|
||||
**Redirects** — tell the client to go somewhere else::
|
||||
|
||||
api.redirect(resp, location="/new-url")
|
||||
|
||||
This sends a ``301 Moved Permanently`` response by default. The client's
|
||||
browser will automatically follow the redirect.
|
||||
|
||||
|
||||
Reading Requests
|
||||
----------------
|
||||
|
||||
The other half of HTTP is the request — the data the client sends to your
|
||||
server. This includes the HTTP method (GET, POST, PUT, DELETE), the URL,
|
||||
headers, query parameters, cookies, and optionally a body.
|
||||
|
||||
Responder wraps all of this in the ``req`` object.
|
||||
|
||||
**Method and URL** — every HTTP request has a method (what the client wants
|
||||
to do) and a URL (what resource it's about)::
|
||||
|
||||
req.method # "get", "post", etc. (lowercase)
|
||||
req.full_url # "http://example.com/path?q=1"
|
||||
req.url # parsed URL object
|
||||
|
||||
**Headers** — HTTP headers carry metadata from the client, like what
|
||||
content types it accepts, authentication tokens, and more. Responder's
|
||||
headers dict is case-insensitive, because the HTTP spec says header names
|
||||
are case-insensitive::
|
||||
|
||||
req.headers["Content-Type"]
|
||||
req.headers["content-type"] # same thing
|
||||
|
||||
**Query parameters** — the part of the URL after the ``?``. These are
|
||||
commonly used for search, filtering, and pagination::
|
||||
|
||||
# GET /search?q=python&page=2
|
||||
req.params["q"] # "python"
|
||||
req.params["page"] # "2"
|
||||
|
||||
Note that query parameters are always strings. If you need an integer,
|
||||
you'll need to convert it yourself: ``int(req.params["page"])``.
|
||||
|
||||
**Path parameters** — the dynamic parts of the URL that matched your route
|
||||
pattern. These are also available on the request object, which is useful
|
||||
in before-request hooks where they aren't passed as function arguments::
|
||||
|
||||
req.path_params["user_id"] # same as the keyword argument
|
||||
|
||||
**Request body** — for POST, PUT, and PATCH requests, the client sends
|
||||
data in the body. Since reading the body is an I/O operation, you need to
|
||||
``await`` it::
|
||||
|
||||
# JSON body (the most common format for APIs)
|
||||
data = await req.media()
|
||||
|
||||
# Form data (from HTML forms)
|
||||
data = await req.media("form")
|
||||
|
||||
# File uploads (multipart)
|
||||
files = await req.media("files")
|
||||
|
||||
# Raw bytes
|
||||
body = await req.content
|
||||
|
||||
# Raw text
|
||||
text = await req.text
|
||||
|
||||
**Other useful properties**::
|
||||
|
||||
req.is_json # True if the content type is JSON
|
||||
req.cookies # dict of cookies sent by the client
|
||||
req.session # session data (a signed, server-side dict)
|
||||
req.client # (host, port) tuple — the client's IP address
|
||||
req.is_secure # True if the request came over HTTPS
|
||||
|
||||
|
||||
Rendering Templates
|
||||
-------------------
|
||||
|
||||
While APIs typically return JSON, many web applications need to render
|
||||
HTML pages. Responder includes built-in support for
|
||||
`Jinja2 <https://jinja.palletsprojects.com/>`_, one of the most popular
|
||||
templating engines in the Python ecosystem.
|
||||
|
||||
Templates let you write HTML with placeholders that get filled in with
|
||||
dynamic data. This keeps your presentation logic (HTML) separate from
|
||||
your application logic (Python) — a pattern called
|
||||
*separation of concerns*.
|
||||
|
||||
The simplest way to render a template is ``api.template()``. Templates
|
||||
are loaded from the ``templates/`` directory by default::
|
||||
|
||||
@api.route("/hello/{name}/html")
|
||||
def hello_html(req, resp, *, name):
|
||||
resp.html = api.template("hello.html", name=name)
|
||||
|
||||
The template file ``templates/hello.html`` might look like::
|
||||
|
||||
<h1>Hello, {{ name }}!</h1>
|
||||
|
||||
The ``{{ name }}`` part is a Jinja2 expression — it gets replaced with
|
||||
the value you passed in.
|
||||
|
||||
You can also use the ``Templates`` class directly for more control over
|
||||
the template directory and configuration::
|
||||
|
||||
from responder.templates import Templates
|
||||
|
||||
templates = Templates(directory="my_templates")
|
||||
|
||||
@api.route("/page")
|
||||
def page(req, resp):
|
||||
resp.html = templates.render("page.html", title="Hello")
|
||||
|
||||
For applications that need non-blocking template rendering (rare, but
|
||||
useful under extreme load), async rendering is supported::
|
||||
|
||||
templates = Templates(directory="templates", enable_async=True)
|
||||
resp.html = await templates.render_async("page.html", title="Hello")
|
||||
|
||||
And for quick one-off templates, you can render a string directly without
|
||||
a file::
|
||||
|
||||
resp.html = api.template_string("Hello, {{ name }}!", name="world")
|
||||
|
||||
|
||||
Background Tasks
|
||||
----------------
|
||||
|
||||
Sometimes you want to accept a request, respond immediately, and do the
|
||||
actual processing later. This is a common pattern for operations that take
|
||||
a long time — sending emails, processing images, updating caches, or
|
||||
calling slow external APIs.
|
||||
|
||||
Responder makes this easy with background tasks. Decorate any function
|
||||
with ``@api.background.task`` and it will run in a thread pool, separate
|
||||
from the request/response cycle::
|
||||
|
||||
@api.route("/incoming")
|
||||
async def receive_incoming(req, resp):
|
||||
data = await req.media()
|
||||
|
||||
@api.background.task
|
||||
def process_data(data):
|
||||
"""This runs in a background thread."""
|
||||
import time
|
||||
time.sleep(10) # simulate heavy work
|
||||
|
||||
process_data(data)
|
||||
|
||||
# This response is sent immediately, while process_data
|
||||
# continues running in the background.
|
||||
resp.media = {"status": "accepted"}
|
||||
|
||||
The client gets an instant response — the heavy lifting happens after.
|
||||
This is the same pattern used by task queues like Celery, but much simpler
|
||||
for lightweight use cases where you don't need a full message broker.
|
||||
|
||||
.. note::
|
||||
|
||||
Background tasks run in threads, not processes. They share memory with
|
||||
your application, which makes them fast to start but means CPU-intensive
|
||||
work will block the event loop. For heavy computation, consider a proper
|
||||
task queue.
|
||||
|
||||
|
||||
Putting It All Together
|
||||
-----------------------
|
||||
|
||||
Here's a complete, working Responder application that combines everything
|
||||
from this guide::
|
||||
|
||||
import responder
|
||||
|
||||
api = responder.API()
|
||||
|
||||
@api.route("/")
|
||||
def index(req, resp):
|
||||
resp.text = "Welcome to the API"
|
||||
|
||||
@api.route("/hello/{name}")
|
||||
def greet(req, resp, *, name):
|
||||
resp.media = {"message": f"hello, {name}!"}
|
||||
|
||||
@api.route("/add/{a:int}/{b:int}")
|
||||
def add(req, resp, *, a, b):
|
||||
resp.media = {"result": a + b}
|
||||
|
||||
@api.route("/echo", methods=["POST"])
|
||||
async def echo(req, resp):
|
||||
data = await req.media()
|
||||
resp.media = {"received": data}
|
||||
|
||||
if __name__ == "__main__":
|
||||
api.run()
|
||||
|
||||
Save this as ``app.py``, run it with ``python app.py``, and try::
|
||||
|
||||
$ curl http://localhost:5042/
|
||||
$ curl http://localhost:5042/hello/world
|
||||
$ curl http://localhost:5042/add/3/4
|
||||
$ curl -X POST http://localhost:5042/echo \
|
||||
-H "Content-Type: application/json" -d '{"key": "value"}'
|
||||
|
||||
From here, explore the :doc:`tour` for the full range of features, or
|
||||
jump into the tutorials:
|
||||
|
||||
- :doc:`tutorial-rest` — build a full CRUD API with validation
|
||||
- :doc:`tutorial-sqlalchemy` — connect to a database
|
||||
- :doc:`tutorial-auth` — add authentication
|
||||
- :doc:`tutorial-websockets` — real-time communication
|
||||
- :doc:`tutorial-middleware` — hooks and middleware
|
||||
- :doc:`tutorial-flask` — migrating from Flask
|
||||
- :doc:`guide-config` — environment variables and secrets
|
||||
- :doc:`deployment` — Docker, cloud platforms, and production
|
||||
- :doc:`testing` — writing tests with pytest
|
||||
@@ -0,0 +1,62 @@
|
||||
(sandbox)=
|
||||
# Development Sandbox
|
||||
|
||||
## Setup
|
||||
|
||||
Clone the repo and install all dependencies:
|
||||
```shell
|
||||
git clone https://github.com/kennethreitz/responder.git
|
||||
cd responder
|
||||
uv venv && source .venv/bin/activate
|
||||
uv pip install --upgrade --editable '.[develop,docs,release,test]'
|
||||
```
|
||||
|
||||
## Running Tests
|
||||
```shell
|
||||
pytest # full suite with coverage
|
||||
pytest tests/test_responder.py -xvs # single file, stop on first failure
|
||||
pytest -k "test_mount" # run tests matching a pattern
|
||||
```
|
||||
|
||||
## Code Formatting
|
||||
```shell
|
||||
ruff format . # auto-format
|
||||
ruff check --fix . # lint and auto-fix
|
||||
```
|
||||
|
||||
## Type Checking
|
||||
```shell
|
||||
mypy
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
Live-reloading doc server (opens in browser):
|
||||
```shell
|
||||
cd docs
|
||||
sphinx-autobuild --open-browser --watch source source build
|
||||
```
|
||||
|
||||
Or build once:
|
||||
```shell
|
||||
cd docs
|
||||
make html
|
||||
# open build/html/index.html
|
||||
```
|
||||
|
||||
## Project Layout
|
||||
|
||||
```
|
||||
responder/
|
||||
├── responder/ # main package
|
||||
│ ├── api.py # API class — the entry point
|
||||
│ ├── routes.py # Router, Route, WebSocketRoute
|
||||
│ ├── models.py # Request and Response wrappers
|
||||
│ ├── ext/ # extensions (CLI, GraphQL, OpenAPI, rate limiting)
|
||||
│ ├── background.py # background task queue
|
||||
│ └── formats.py # content negotiation (JSON, YAML, msgpack)
|
||||
├── tests/ # pytest test suite
|
||||
├── examples/ # runnable example apps
|
||||
├── docs/source/ # Sphinx documentation
|
||||
└── pyproject.toml # project metadata and tool config
|
||||
```
|
||||
@@ -0,0 +1,340 @@
|
||||
Testing
|
||||
=======
|
||||
|
||||
Responder includes a built-in test client powered by Starlette's
|
||||
``TestClient``. You don't need to start a server — tests run in-process,
|
||||
making them fast and reliable. There's no separate test server to manage,
|
||||
no ports to allocate, and no race conditions to worry about. Just import
|
||||
your app and start making requests.
|
||||
|
||||
|
||||
Getting Started
|
||||
---------------
|
||||
|
||||
Given a simple application in ``api.py``::
|
||||
|
||||
import responder
|
||||
|
||||
api = responder.API()
|
||||
|
||||
@api.route("/")
|
||||
def hello(req, resp):
|
||||
resp.text = "hello, world!"
|
||||
|
||||
if __name__ == "__main__":
|
||||
api.run()
|
||||
|
||||
You can test it with pytest. Every Responder ``API`` instance has a
|
||||
``requests`` property that gives you a test client — use it exactly like
|
||||
you'd use ``requests`` or ``httpx``::
|
||||
|
||||
# test_api.py
|
||||
import api as service
|
||||
|
||||
def test_hello():
|
||||
r = service.api.requests.get("/")
|
||||
assert r.text == "hello, world!"
|
||||
|
||||
Run your tests::
|
||||
|
||||
$ pytest
|
||||
|
||||
That's really all there is to it. No configuration, no test server setup.
|
||||
|
||||
|
||||
Using Fixtures
|
||||
--------------
|
||||
|
||||
For larger test suites, pytest fixtures keep things organized. Create a
|
||||
fixture that returns your API instance, and every test gets a fresh
|
||||
reference to it::
|
||||
|
||||
import pytest
|
||||
import api as service
|
||||
|
||||
@pytest.fixture
|
||||
def api():
|
||||
return service.api
|
||||
|
||||
def test_hello(api):
|
||||
r = api.requests.get("/")
|
||||
assert r.text == "hello, world!"
|
||||
|
||||
def test_json(api):
|
||||
@api.route("/data")
|
||||
def data(req, resp):
|
||||
resp.media = {"key": "value"}
|
||||
|
||||
r = api.requests.get(api.url_for(data))
|
||||
assert r.json() == {"key": "value"}
|
||||
|
||||
The ``api.url_for()`` method generates a URL for a given route endpoint,
|
||||
so you don't have to hard-code paths in your tests. If you rename a route
|
||||
later, your tests won't break.
|
||||
|
||||
|
||||
Testing JSON APIs
|
||||
-----------------
|
||||
|
||||
Most APIs send and receive JSON. The test client makes this natural — pass
|
||||
``json=`` to send a JSON body, and call ``.json()`` on the response to
|
||||
parse it::
|
||||
|
||||
def test_create_item(api):
|
||||
@api.route("/items")
|
||||
async def create(req, resp):
|
||||
data = await req.media()
|
||||
resp.media = {"created": data}
|
||||
resp.status_code = 201
|
||||
|
||||
r = api.requests.post(api.url_for(create), json={"name": "widget"})
|
||||
assert r.status_code == 201
|
||||
assert r.json() == {"created": {"name": "widget"}}
|
||||
|
||||
You can also test content negotiation by setting the ``Accept`` header::
|
||||
|
||||
r = api.requests.get("/data", headers={"Accept": "application/x-yaml"})
|
||||
assert "key: value" in r.text
|
||||
|
||||
|
||||
Testing Request Validation
|
||||
--------------------------
|
||||
|
||||
If you're using Pydantic models for request validation, you can test
|
||||
that invalid inputs are properly rejected::
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
class Item(BaseModel):
|
||||
name: str
|
||||
price: float
|
||||
|
||||
def test_validation(api):
|
||||
@api.route("/items", methods=["POST"], request_model=Item)
|
||||
async def create(req, resp):
|
||||
data = await req.media()
|
||||
resp.media = data
|
||||
|
||||
# Valid request
|
||||
r = api.requests.post("/items", json={"name": "thing", "price": 9.99})
|
||||
assert r.status_code == 200
|
||||
|
||||
# Missing required field
|
||||
r = api.requests.post("/items", json={"name": "thing"})
|
||||
assert r.status_code == 422
|
||||
assert "errors" in r.json()
|
||||
|
||||
|
||||
Testing File Uploads
|
||||
--------------------
|
||||
|
||||
File uploads use the ``files`` parameter, just like the ``requests``
|
||||
library. Each file is a tuple of ``(filename, content, content_type)``::
|
||||
|
||||
def test_upload(api):
|
||||
@api.route("/upload")
|
||||
async def upload(req, resp):
|
||||
files = await req.media("files")
|
||||
resp.media = {"received": list(files.keys())}
|
||||
|
||||
files = {"doc": ("report.pdf", b"content", "application/pdf")}
|
||||
r = api.requests.post(api.url_for(upload), files=files)
|
||||
assert r.json() == {"received": ["doc"]}
|
||||
|
||||
|
||||
Testing Headers and Cookies
|
||||
----------------------------
|
||||
|
||||
Check response headers and cookies just like you would with any HTTP
|
||||
client::
|
||||
|
||||
def test_headers(api):
|
||||
@api.route("/")
|
||||
def view(req, resp):
|
||||
resp.headers["X-Custom"] = "hello"
|
||||
resp.cookies["session"] = "abc123"
|
||||
|
||||
r = api.requests.get("/")
|
||||
assert r.headers["X-Custom"] == "hello"
|
||||
assert "session" in r.cookies
|
||||
|
||||
|
||||
Testing WebSockets
|
||||
------------------
|
||||
|
||||
WebSocket tests use Starlette's ``TestClient`` directly, since WebSocket
|
||||
connections require a different protocol. The ``websocket_connect`` context
|
||||
manager gives you a connection you can send and receive on::
|
||||
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
def test_websocket(api):
|
||||
@api.route("/ws", websocket=True)
|
||||
async def ws(ws):
|
||||
await ws.accept()
|
||||
name = await ws.receive_text()
|
||||
await ws.send_text(f"hello, {name}!")
|
||||
await ws.close()
|
||||
|
||||
client = TestClient(api)
|
||||
with client.websocket_connect("/ws") as ws:
|
||||
ws.send_text("world")
|
||||
assert ws.receive_text() == "hello, world!"
|
||||
|
||||
|
||||
Testing Error Handling
|
||||
----------------------
|
||||
|
||||
By default, the test client raises exceptions from your route handlers,
|
||||
which is usually what you want — it makes bugs obvious. But when you're
|
||||
testing error handling specifically, you want to see the error response
|
||||
instead. Disable exception propagation with ``raise_server_exceptions``::
|
||||
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
def test_500(api):
|
||||
@api.route("/fail")
|
||||
def fail(req, resp):
|
||||
raise ValueError("something broke")
|
||||
|
||||
client = TestClient(api, raise_server_exceptions=False)
|
||||
r = client.get(api.url_for(fail))
|
||||
assert r.status_code == 500
|
||||
|
||||
If you've registered a custom exception handler, you can test that too::
|
||||
|
||||
def test_custom_error(api):
|
||||
@api.exception_handler(ValueError)
|
||||
async def handle(req, resp, exc):
|
||||
resp.status_code = 400
|
||||
resp.media = {"error": str(exc)}
|
||||
|
||||
@api.route("/fail")
|
||||
def fail(req, resp):
|
||||
raise ValueError("bad input")
|
||||
|
||||
client = TestClient(api, raise_server_exceptions=False)
|
||||
r = client.get(api.url_for(fail))
|
||||
assert r.status_code == 400
|
||||
assert r.json() == {"error": "bad input"}
|
||||
|
||||
|
||||
Testing Lifespan Events
|
||||
-----------------------
|
||||
|
||||
If your app uses startup and shutdown events (for database connections,
|
||||
caches, etc.), you need the test client to trigger them. Wrap the client
|
||||
in a ``with`` block — startup runs on enter, shutdown runs on exit::
|
||||
|
||||
def test_with_lifespan(api):
|
||||
started = {"value": False}
|
||||
|
||||
@api.on_event("startup")
|
||||
async def on_startup():
|
||||
started["value"] = True
|
||||
|
||||
@api.route("/")
|
||||
def check(req, resp):
|
||||
resp.media = {"started": started["value"]}
|
||||
|
||||
with api.requests as session:
|
||||
r = session.get("http://;/")
|
||||
assert r.json() == {"started": True}
|
||||
|
||||
Without the ``with`` block, lifespan events won't fire, which can lead to
|
||||
confusing test failures if your routes depend on startup initialization.
|
||||
|
||||
|
||||
Testing Before and After Hooks
|
||||
------------------------------
|
||||
|
||||
Before-request and after-request hooks run automatically during tests,
|
||||
just like in production. You can verify their effects on the response::
|
||||
|
||||
def test_hooks(api):
|
||||
@api.route(before_request=True)
|
||||
def add_version(req, resp):
|
||||
resp.headers["X-Version"] = "3.2"
|
||||
|
||||
@api.after_request()
|
||||
def add_timing(req, resp):
|
||||
resp.headers["X-Served-By"] = "responder"
|
||||
|
||||
@api.route("/")
|
||||
def view(req, resp):
|
||||
resp.text = "ok"
|
||||
|
||||
r = api.requests.get("/")
|
||||
assert r.headers["X-Version"] == "3.2"
|
||||
assert r.headers["X-Served-By"] == "responder"
|
||||
|
||||
|
||||
Testing Rate Limiting
|
||||
---------------------
|
||||
|
||||
Rate limiters are just hooks — they run automatically during tests.
|
||||
Verify the headers and the 429 response::
|
||||
|
||||
from responder.ext.ratelimit import RateLimiter
|
||||
|
||||
def test_rate_limiting():
|
||||
api = responder.API(allowed_hosts=["localhost"])
|
||||
limiter = RateLimiter(requests=2, period=60)
|
||||
limiter.install(api)
|
||||
|
||||
@api.route("/")
|
||||
def view(req, resp):
|
||||
resp.text = "ok"
|
||||
|
||||
# First two requests succeed
|
||||
for _ in range(2):
|
||||
r = api.requests.get("http://localhost/")
|
||||
assert r.status_code == 200
|
||||
assert "X-RateLimit-Remaining" in r.headers
|
||||
|
||||
# Third request is rate limited
|
||||
r = api.requests.get("http://localhost/")
|
||||
assert r.status_code == 429
|
||||
|
||||
|
||||
Testing Mounted Apps
|
||||
--------------------
|
||||
|
||||
When testing WSGI apps mounted at a subroute, use ``localhost`` as the
|
||||
host to avoid Werkzeug's trusted host validation::
|
||||
|
||||
from flask import Flask
|
||||
|
||||
def test_flask_mount():
|
||||
api = responder.API(allowed_hosts=["localhost"])
|
||||
|
||||
flask_app = Flask(__name__)
|
||||
@flask_app.route("/")
|
||||
def hello():
|
||||
return "Hello from Flask!"
|
||||
|
||||
api.mount("/flask", flask_app)
|
||||
|
||||
r = api.requests.get("http://localhost/flask")
|
||||
assert r.status_code == 200
|
||||
assert "Hello from Flask" in r.text
|
||||
|
||||
|
||||
Tips
|
||||
----
|
||||
|
||||
- **Keep tests fast.** The in-process test client is already fast — no
|
||||
network overhead. Avoid ``time.sleep()`` in tests.
|
||||
|
||||
- **One API per test** when testing configuration. If you need a specific
|
||||
``API()`` configuration (like ``cors=True``), create a new instance
|
||||
in the test rather than sharing the fixture.
|
||||
|
||||
- Use ``api.url_for()`` instead of hard-coded paths. It's a small
|
||||
thing, but it makes refactoring painless.
|
||||
|
||||
- **Test the contract, not the implementation.** Assert on status codes,
|
||||
response bodies, and headers — not on internal state.
|
||||
|
||||
- **Use ``localhost`` for mounted WSGI apps.** Werkzeug 3.1.7+ validates
|
||||
the ``Host`` header, so avoid synthetic hosts like ``;`` in tests.
|
||||
@@ -0,0 +1,738 @@
|
||||
Feature Tour
|
||||
============
|
||||
|
||||
This section walks through Responder's features in depth. Each section
|
||||
explains the concept, shows working code, and explains the design choices
|
||||
behind it. If you're new to web development, this is a good place to learn
|
||||
how modern web frameworks work under the hood.
|
||||
|
||||
|
||||
Method Filtering
|
||||
----------------
|
||||
|
||||
HTTP defines several *methods* (also called verbs) that describe what a
|
||||
client wants to do with a resource. The most common are:
|
||||
|
||||
- ``GET`` — retrieve data
|
||||
- ``POST`` — create something new
|
||||
- ``PUT`` — replace something entirely
|
||||
- ``PATCH`` — update part of something
|
||||
- ``DELETE`` — remove something
|
||||
|
||||
By default, a Responder route matches all methods. This is fine for simple
|
||||
endpoints, but REST APIs typically map different methods to different
|
||||
operations. Use the ``methods`` parameter to restrict a route::
|
||||
|
||||
@api.route("/items", methods=["GET"])
|
||||
def list_items(req, resp):
|
||||
resp.media = {"items": []}
|
||||
|
||||
@api.route("/items", methods=["POST"], check_existing=False)
|
||||
async def create_item(req, resp):
|
||||
data = await req.media()
|
||||
resp.media = {"created": data}
|
||||
|
||||
Note the ``check_existing=False`` — Responder normally prevents you from
|
||||
registering two routes with the same path (to catch typos). When you
|
||||
intentionally want multiple handlers for the same path with different
|
||||
methods, you need to opt in.
|
||||
|
||||
|
||||
Class-Based Views
|
||||
-----------------
|
||||
|
||||
Function-based views are great for simple endpoints, but sometimes you want
|
||||
to group related HTTP methods together into a single resource. This is
|
||||
where class-based views come in — a pattern popularized by
|
||||
`Falcon <https://falconframework.org/>`_.
|
||||
|
||||
Responder dispatches to the appropriate method handler based on the HTTP
|
||||
method::
|
||||
|
||||
@api.route("/{greeting}")
|
||||
class GreetingResource:
|
||||
def on_get(self, req, resp, *, greeting):
|
||||
resp.text = f"{greeting}, world!"
|
||||
|
||||
def on_post(self, req, resp, *, greeting):
|
||||
resp.media = {"received": greeting}
|
||||
|
||||
def on_request(self, req, resp, *, greeting):
|
||||
"""Called on EVERY request, before the method-specific handler."""
|
||||
resp.headers["X-Greeting"] = greeting
|
||||
|
||||
The ``on_request`` method is called for all HTTP methods, much like
|
||||
middleware scoped to a single route. Method-specific handlers (``on_get``,
|
||||
``on_post``, ``on_put``, ``on_delete``, etc.) are called after.
|
||||
|
||||
No inheritance required — just define a class with the right method names.
|
||||
This is simpler than Django's ``View`` classes and more Pythonic than
|
||||
framework-specific base classes.
|
||||
|
||||
|
||||
Lifespan Events
|
||||
---------------
|
||||
|
||||
Real applications need to set up resources when they start (database
|
||||
connection pools, ML models, caches) and tear them down when they stop.
|
||||
This is called the application *lifespan*.
|
||||
|
||||
The modern approach is the *context manager* pattern, where startup and
|
||||
shutdown are two halves of the same block::
|
||||
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app):
|
||||
# Startup — runs before the first request
|
||||
print("connecting to database...")
|
||||
yield
|
||||
# Shutdown — runs after the server stops
|
||||
print("closing connections...")
|
||||
|
||||
api = responder.API(lifespan=lifespan)
|
||||
|
||||
Everything before ``yield`` runs at startup. Everything after runs at
|
||||
shutdown. If startup fails, the server won't start. If shutdown raises,
|
||||
it's logged but the server still exits.
|
||||
|
||||
The traditional event decorator style also works::
|
||||
|
||||
@api.on_event("startup")
|
||||
async def startup():
|
||||
print("starting up")
|
||||
|
||||
@api.on_event("shutdown")
|
||||
async def shutdown():
|
||||
print("shutting down")
|
||||
|
||||
The context manager is preferred for new code — it keeps related startup
|
||||
and shutdown logic together and makes resource cleanup more explicit.
|
||||
|
||||
|
||||
Serving Files
|
||||
-------------
|
||||
|
||||
Web applications often need to serve files — downloads, reports, images.
|
||||
Responder makes this simple with ``resp.file()``, which reads a file from
|
||||
disk and sets the ``Content-Type`` header automatically using Python's
|
||||
``mimetypes`` module::
|
||||
|
||||
@api.route("/download")
|
||||
def download(req, resp):
|
||||
resp.file("reports/annual.pdf")
|
||||
|
||||
You can override the content type if the automatic detection isn't right::
|
||||
|
||||
@api.route("/image")
|
||||
def image(req, resp):
|
||||
resp.file("photos/cat.jpg", content_type="image/jpeg")
|
||||
|
||||
For large files, use ``resp.stream_file()`` to avoid loading the entire
|
||||
file into memory. This streams the file in chunks::
|
||||
|
||||
@api.route("/export")
|
||||
def export(req, resp):
|
||||
resp.stream_file("data/export.csv")
|
||||
|
||||
|
||||
Custom Error Handling
|
||||
---------------------
|
||||
|
||||
In production, you don't want your users to see raw Python tracebacks.
|
||||
Responder lets you register custom handlers for specific exception types,
|
||||
so you can return clean, structured error responses::
|
||||
|
||||
@api.exception_handler(ValueError)
|
||||
async def handle_value_error(req, resp, exc):
|
||||
resp.status_code = 400
|
||||
resp.media = {"error": str(exc)}
|
||||
|
||||
Now, any route that raises a ``ValueError`` will return a clean JSON
|
||||
response with a 400 status code instead of a generic 500 error page.
|
||||
|
||||
This is a common pattern in API development — you define your own exception
|
||||
classes for different error conditions, register handlers for each, and
|
||||
your API always returns consistent, machine-readable error responses.
|
||||
|
||||
|
||||
Before-Request Hooks
|
||||
--------------------
|
||||
|
||||
Sometimes you need to run the same code before every request —
|
||||
authentication checks, request logging, adding common headers, or setting
|
||||
up per-request state. Before-request hooks let you do this without
|
||||
duplicating code in every route::
|
||||
|
||||
@api.route(before_request=True)
|
||||
def add_headers(req, resp):
|
||||
resp.headers["X-API-Version"] = "3.2"
|
||||
|
||||
**Short-circuiting** is the really powerful part. If your hook sets
|
||||
``resp.status_code``, the route handler is skipped entirely and the
|
||||
response is sent immediately. This is the pattern for authentication::
|
||||
|
||||
@api.route(before_request=True)
|
||||
def auth_check(req, resp):
|
||||
if "Authorization" not in req.headers:
|
||||
resp.status_code = 401
|
||||
resp.media = {"error": "unauthorized"}
|
||||
|
||||
If the ``Authorization`` header is missing, the client gets a 401 response
|
||||
and the actual route handler never runs. This is cleaner than adding
|
||||
auth checks to every individual route.
|
||||
|
||||
|
||||
After-Request Hooks
|
||||
-------------------
|
||||
|
||||
The complement to before-request hooks. After-request hooks run after the
|
||||
route handler completes but before the response is sent. They're useful
|
||||
for logging, adding response headers, or any post-processing::
|
||||
|
||||
@api.after_request()
|
||||
def log_response(req, resp):
|
||||
print(f"{req.method} {req.full_url} -> {resp.status_code}")
|
||||
|
||||
@api.after_request()
|
||||
async def add_timing(req, resp):
|
||||
resp.headers["X-Served-By"] = "responder"
|
||||
|
||||
|
||||
WebSocket Support
|
||||
-----------------
|
||||
|
||||
HTTP is a request-response protocol — the client asks, the server answers.
|
||||
But some applications need real-time, bidirectional communication: chat
|
||||
apps, live dashboards, multiplayer games, collaborative editors.
|
||||
|
||||
`WebSockets <https://en.wikipedia.org/wiki/WebSocket>`_ solve this by
|
||||
upgrading an HTTP connection into a persistent, full-duplex channel where
|
||||
both sides can send messages at any time::
|
||||
|
||||
@api.route("/ws", websocket=True)
|
||||
async def websocket(ws):
|
||||
await ws.accept()
|
||||
while True:
|
||||
name = await ws.receive_text()
|
||||
await ws.send_text(f"Hello {name}!")
|
||||
await ws.close()
|
||||
|
||||
You can send and receive in multiple formats:
|
||||
|
||||
- ``send_text`` / ``receive_text`` — plain text strings
|
||||
- ``send_json`` / ``receive_json`` — JSON objects (auto-serialized)
|
||||
- ``send_bytes`` / ``receive_bytes`` — raw binary data
|
||||
|
||||
WebSocket routes are marked with ``websocket=True`` in the route decorator.
|
||||
They receive a ``ws`` object instead of ``req`` and ``resp``.
|
||||
|
||||
|
||||
Server-Sent Events (SSE)
|
||||
-------------------------
|
||||
|
||||
SSE is a simpler alternative to WebSockets for *one-way* real-time
|
||||
communication — the server pushes events to the client, but the client
|
||||
can't send messages back. This is perfect for live feeds, progress bars,
|
||||
notification streams, and AI response streaming.
|
||||
|
||||
Unlike WebSockets, SSE works over plain HTTP, is automatically reconnected
|
||||
by the browser, and doesn't require any special client-side libraries::
|
||||
|
||||
@api.route("/events")
|
||||
async def events(req, resp):
|
||||
@resp.sse
|
||||
async def stream():
|
||||
for i in range(10):
|
||||
yield {"data": f"message {i}"}
|
||||
|
||||
On the client side, you consume SSE events with JavaScript's built-in
|
||||
``EventSource`` API::
|
||||
|
||||
const source = new EventSource("/events");
|
||||
source.onmessage = (event) => {
|
||||
console.log(event.data);
|
||||
};
|
||||
|
||||
Each yielded value can be a string (treated as data) or a dict with the
|
||||
standard SSE fields::
|
||||
|
||||
yield {"event": "update", "data": "hello", "id": "1", "retry": "5000"}
|
||||
yield "simple string message"
|
||||
|
||||
|
||||
GraphQL
|
||||
-------
|
||||
|
||||
`GraphQL <https://graphql.org/>`_ is a query language for APIs that lets
|
||||
clients request exactly the data they need — no more, no less. Instead of
|
||||
multiple REST endpoints, you define a schema and let clients query it.
|
||||
|
||||
Responder includes built-in GraphQL support via
|
||||
`Graphene <https://graphene-python.org/>`_. Set up a full GraphQL endpoint
|
||||
with a single method call::
|
||||
|
||||
import graphene
|
||||
|
||||
class Query(graphene.ObjectType):
|
||||
hello = graphene.String(name=graphene.String(default_value="stranger"))
|
||||
|
||||
def resolve_hello(self, info, name):
|
||||
return f"Hello {name}"
|
||||
|
||||
api.graphql("/graphql", schema=graphene.Schema(query=Query))
|
||||
|
||||
Visiting ``/graphql`` in a browser renders the
|
||||
`GraphiQL <https://github.com/graphql/graphiql>`_ interactive IDE, where
|
||||
you can explore your schema, write queries, and see results in real-time.
|
||||
Programmatic clients can POST JSON queries to the same endpoint.
|
||||
|
||||
You can access the Responder request and response objects in your resolvers
|
||||
through ``info.context["request"]`` and ``info.context["response"]``.
|
||||
|
||||
|
||||
OpenAPI Documentation
|
||||
---------------------
|
||||
|
||||
`OpenAPI <https://www.openapis.org/>`_ (formerly Swagger) is the industry
|
||||
standard for describing REST APIs. An OpenAPI specification lets you
|
||||
auto-generate interactive documentation, client libraries, and validation
|
||||
logic.
|
||||
|
||||
Responder generates OpenAPI specs from your code::
|
||||
|
||||
api = responder.API(
|
||||
title="Pet Store",
|
||||
version="1.0",
|
||||
openapi="3.0.2",
|
||||
docs_route="/docs",
|
||||
)
|
||||
|
||||
This gives you:
|
||||
|
||||
- An OpenAPI schema at ``/schema.yml``
|
||||
- Interactive Swagger UI documentation at ``/docs``
|
||||
|
||||
There are three ways to document your endpoints.
|
||||
|
||||
**Pydantic models** — the recommended approach. Use ``request_model`` and
|
||||
``response_model`` to annotate your routes, and Responder generates the
|
||||
schema automatically. When ``request_model`` is set, request bodies are
|
||||
also validated automatically — invalid inputs get a ``422`` response with
|
||||
detailed error messages::
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
class PetIn(BaseModel):
|
||||
name: str
|
||||
age: int = 0
|
||||
|
||||
class PetOut(BaseModel):
|
||||
id: int
|
||||
name: str
|
||||
age: int
|
||||
|
||||
@api.route("/pets", methods=["POST"],
|
||||
request_model=PetIn, response_model=PetOut)
|
||||
async def create_pet(req, resp):
|
||||
data = await req.media()
|
||||
resp.media = {"id": 1, **data}
|
||||
|
||||
When ``response_model`` is set, the response is serialized through the
|
||||
model — extra fields are stripped and types are enforced.
|
||||
|
||||
**YAML docstrings** — for full control, embed OpenAPI YAML in the
|
||||
docstring::
|
||||
|
||||
@api.route("/pets")
|
||||
def list_pets(req, resp):
|
||||
"""A list of pets.
|
||||
---
|
||||
get:
|
||||
description: Get all pets
|
||||
responses:
|
||||
200:
|
||||
description: A list of pets
|
||||
"""
|
||||
resp.media = [{"name": "Fido"}]
|
||||
|
||||
**Marshmallow schemas** — if you're already using marshmallow::
|
||||
|
||||
from marshmallow import Schema, fields
|
||||
|
||||
@api.schema("Pet")
|
||||
class PetSchema(Schema):
|
||||
name = fields.Str()
|
||||
|
||||
All three approaches can be mixed in the same API. You can choose from
|
||||
multiple documentation themes: ``swagger_ui`` (default), ``redoc``,
|
||||
``rapidoc``, or ``elements``.
|
||||
|
||||
|
||||
Route Groups
|
||||
------------
|
||||
|
||||
As your application grows, you'll want to organize routes logically.
|
||||
Route groups let you share a URL prefix across related endpoints — a
|
||||
common pattern for API versioning::
|
||||
|
||||
v1 = api.group("/v1")
|
||||
|
||||
@v1.route("/users")
|
||||
def list_users(req, resp):
|
||||
resp.media = []
|
||||
|
||||
@v1.route("/users/{user_id:int}")
|
||||
def get_user(req, resp, *, user_id):
|
||||
resp.media = {"id": user_id}
|
||||
|
||||
v2 = api.group("/v2")
|
||||
|
||||
@v2.route("/users")
|
||||
def list_users_v2(req, resp):
|
||||
resp.media = {"users": [], "total": 0}
|
||||
|
||||
This keeps your code organized without affecting the routing logic.
|
||||
|
||||
|
||||
Mounting Other Apps
|
||||
-------------------
|
||||
|
||||
Responder can mount any WSGI or ASGI application at a subroute. This is
|
||||
incredibly useful for gradual migrations — you can run Flask and Responder
|
||||
side by side, moving routes over one at a time::
|
||||
|
||||
from flask import Flask
|
||||
|
||||
flask_app = Flask(__name__)
|
||||
|
||||
@flask_app.route("/")
|
||||
def hello():
|
||||
return "Hello from Flask!"
|
||||
|
||||
api.mount("/flask", flask_app)
|
||||
|
||||
Requests to ``/flask/`` will be handled by Flask. Everything else goes
|
||||
through Responder. Both WSGI and ASGI apps are supported — Responder
|
||||
wraps WSGI apps in an ASGI adapter automatically.
|
||||
|
||||
You can also mount `marimo <https://marimo.io/>`_ notebooks as
|
||||
interactive dashboards within your API::
|
||||
|
||||
import marimo
|
||||
|
||||
server = (
|
||||
marimo.create_asgi_app()
|
||||
.with_app(path="", root="./notebooks/dashboard.py")
|
||||
.with_app(path="/analysis", root="./notebooks/analysis.py")
|
||||
)
|
||||
|
||||
api.mount("/notebooks", server.build())
|
||||
|
||||
Notebooks are served at ``/notebooks/`` and ``/notebooks/analysis``,
|
||||
with full interactivity — reactive cells, widgets, plots, and all.
|
||||
|
||||
|
||||
Cookies
|
||||
-------
|
||||
|
||||
`Cookies <https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies>`_ are
|
||||
small pieces of data that the server asks the browser to store and send
|
||||
back with every subsequent request. They're the foundation of sessions,
|
||||
authentication tokens, and user preferences on the web.
|
||||
|
||||
Reading and writing cookies is straightforward::
|
||||
|
||||
# Read cookies from the request
|
||||
session_id = req.cookies.get("session_id")
|
||||
|
||||
# Set a cookie on the response
|
||||
resp.cookies["hello"] = "world"
|
||||
|
||||
For production use, you'll want to set security directives. The
|
||||
``httponly`` flag prevents JavaScript from reading the cookie (defending
|
||||
against XSS attacks), and ``secure`` ensures it's only sent over HTTPS::
|
||||
|
||||
resp.set_cookie(
|
||||
"token",
|
||||
value="abc123",
|
||||
max_age=3600, # expires in 1 hour
|
||||
secure=True, # HTTPS only
|
||||
httponly=True, # no JavaScript access
|
||||
path="/",
|
||||
)
|
||||
|
||||
|
||||
Cookie-Based Sessions
|
||||
---------------------
|
||||
|
||||
Sessions let you store per-user data across multiple requests. Responder's
|
||||
built-in sessions are cookie-based — the session data is serialized, signed
|
||||
with your secret key, and stored in a cookie. The signature prevents
|
||||
tampering: if someone modifies the cookie, the signature won't match and
|
||||
the data will be rejected::
|
||||
|
||||
@api.route("/login")
|
||||
def login(req, resp):
|
||||
resp.session["username"] = "alice"
|
||||
|
||||
@api.route("/profile")
|
||||
def profile(req, resp):
|
||||
resp.media = {"user": req.session.get("username")}
|
||||
|
||||
.. warning::
|
||||
|
||||
Always set a secret key in production. The default key is not secret::
|
||||
|
||||
api = responder.API(secret_key="your-secret-key-here")
|
||||
|
||||
|
||||
Static Files
|
||||
------------
|
||||
|
||||
Most web applications serve static assets — CSS stylesheets, JavaScript
|
||||
files, images, fonts. Responder serves these from the ``static/`` directory
|
||||
by default::
|
||||
|
||||
api = responder.API(static_dir="static", static_route="/static")
|
||||
|
||||
Place your assets in the ``static/`` directory and they'll be served
|
||||
automatically at ``/static/style.css``, ``/static/app.js``, etc.
|
||||
|
||||
For single-page applications (React, Vue, Angular), you can serve
|
||||
``index.html`` as the default response for all unmatched routes::
|
||||
|
||||
api.add_route("/", static=True)
|
||||
|
||||
|
||||
CORS
|
||||
----
|
||||
|
||||
`CORS <https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS>`_ (Cross-
|
||||
Origin Resource Sharing) is a security mechanism that controls which
|
||||
websites can make requests to your API. Browsers enforce this — if your
|
||||
API is at ``api.example.com`` and your frontend is at ``app.example.com``,
|
||||
the browser will block requests unless your API explicitly allows it.
|
||||
|
||||
Enable CORS and configure which origins are allowed::
|
||||
|
||||
api = responder.API(cors=True, cors_params={
|
||||
"allow_origins": ["https://app.example.com"],
|
||||
"allow_methods": ["GET", "POST"],
|
||||
"allow_headers": ["*"],
|
||||
"allow_credentials": True,
|
||||
"max_age": 600,
|
||||
})
|
||||
|
||||
The default policy is restrictive — you must explicitly allow each origin.
|
||||
Using ``["*"]`` for allow_origins permits any website to call your API,
|
||||
which is fine for public APIs but not for private ones.
|
||||
|
||||
|
||||
HSTS
|
||||
----
|
||||
|
||||
`HSTS <https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Strict-Transport-Security>`_
|
||||
(HTTP Strict Transport Security) tells browsers to always use HTTPS when
|
||||
communicating with your server. Once a browser sees the HSTS header, it
|
||||
will refuse to connect over plain HTTP, even if the user types ``http://``
|
||||
in the address bar::
|
||||
|
||||
api = responder.API(enable_hsts=True)
|
||||
|
||||
|
||||
Trusted Hosts
|
||||
-------------
|
||||
|
||||
The ``Host`` header in an HTTP request tells the server which domain name
|
||||
the client used. Attackers can forge this header to trick your application
|
||||
into generating URLs to malicious domains (a class of attack called *Host
|
||||
header injection*).
|
||||
|
||||
Restrict which hostnames your application accepts::
|
||||
|
||||
api = responder.API(allowed_hosts=["example.com", "*.example.com"])
|
||||
|
||||
Requests with unrecognized hosts get a ``400 Bad Request``. Wildcard
|
||||
patterns are supported. By default, all hostnames are allowed.
|
||||
|
||||
|
||||
Request ID
|
||||
----------
|
||||
|
||||
In distributed systems, tracing a single request across multiple services
|
||||
is essential for debugging. Request IDs are unique identifiers attached to
|
||||
each request — if something goes wrong, you can search your logs for that
|
||||
ID and find every related event.
|
||||
|
||||
Responder can auto-generate request IDs. If the client sends an
|
||||
``X-Request-ID`` header (common in microservice architectures), it's
|
||||
forwarded. Otherwise, a new UUID is generated::
|
||||
|
||||
api = responder.API(request_id=True)
|
||||
|
||||
The ID appears in the ``X-Request-ID`` response header.
|
||||
|
||||
|
||||
Rate Limiting
|
||||
-------------
|
||||
|
||||
Rate limiting prevents individual clients from overwhelming your API with
|
||||
too many requests. It's essential for public APIs, and good practice even
|
||||
for internal ones.
|
||||
|
||||
Responder includes a built-in token bucket rate limiter::
|
||||
|
||||
from responder.ext.ratelimit import RateLimiter
|
||||
|
||||
limiter = RateLimiter(requests=100, period=60) # 100 req/min
|
||||
limiter.install(api)
|
||||
|
||||
When the limit is exceeded, clients receive a ``429 Too Many Requests``
|
||||
response with a ``Retry-After`` header. Every response includes
|
||||
``X-RateLimit-Limit`` and ``X-RateLimit-Remaining`` headers so clients
|
||||
can pace themselves.
|
||||
|
||||
The rate limiter is per-client, keyed by IP address.
|
||||
|
||||
|
||||
Structured Logging
|
||||
------------------
|
||||
|
||||
Production applications need structured, searchable logs. Responder
|
||||
includes built-in logging that automatically attaches request context
|
||||
— request ID, HTTP method, path, and client IP — to every log message
|
||||
emitted during request handling::
|
||||
|
||||
api = responder.API(enable_logging=True)
|
||||
|
||||
This gives you:
|
||||
|
||||
- **Access logging** with timing for every request::
|
||||
|
||||
2026-03-24 12:00:00 [INFO] responder.access — GET /users → 200 (1.2ms)
|
||||
|
||||
- **A logger on the API instance** — use ``api.log`` anywhere in
|
||||
your routes. Request context (ID, method, path, client IP) is
|
||||
attached automatically::
|
||||
|
||||
@api.route("/users/{user_id:int}")
|
||||
def get_user(req, resp, *, user_id):
|
||||
api.log.info("fetching user %d", user_id)
|
||||
# => [INFO] responder.app -- fetching user 42 [GET /users/42] [req:a1b2c3] [client:10.0.0.1]
|
||||
resp.media = {"id": user_id}
|
||||
|
||||
- **Request IDs** generated automatically (or forwarded from the
|
||||
``X-Request-ID`` header) and included in responses.
|
||||
|
||||
The logging uses Python's standard ``logging`` module, so it works with
|
||||
any handler — files, syslog, JSON formatters, Datadog, Sentry, whatever
|
||||
you already use.
|
||||
|
||||
For additional loggers (e.g. in helper modules), use ``get_logger``::
|
||||
|
||||
from responder.ext.logging import get_logger
|
||||
logger = get_logger("myapp.db")
|
||||
|
||||
You can also access the current request context directly::
|
||||
|
||||
from responder.ext.logging import RequestContext
|
||||
|
||||
@api.route("/debug")
|
||||
def debug(req, resp):
|
||||
resp.media = {
|
||||
"request_id": RequestContext.get_request_id(),
|
||||
"client_ip": RequestContext.get_client_ip(),
|
||||
}
|
||||
|
||||
When ``enable_logging=True`` is set, it supersedes ``request_id=True``
|
||||
— the logging middleware handles request IDs itself, so you don't get
|
||||
duplicate headers.
|
||||
|
||||
|
||||
Pydantic Validation
|
||||
-------------------
|
||||
|
||||
`Pydantic <https://docs.pydantic.dev/>`_ models integrate directly with
|
||||
Responder's routing. Set ``request_model`` to validate incoming data and
|
||||
``response_model`` to control the shape of outgoing data::
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
class ItemIn(BaseModel):
|
||||
name: str
|
||||
price: float
|
||||
|
||||
class ItemOut(BaseModel):
|
||||
id: int
|
||||
name: str
|
||||
price: float
|
||||
|
||||
@api.route("/items", methods=["POST"],
|
||||
request_model=ItemIn, response_model=ItemOut)
|
||||
async def create_item(req, resp):
|
||||
data = await req.media()
|
||||
resp.media = {"id": 1, **data}
|
||||
|
||||
When ``request_model`` is set:
|
||||
|
||||
- Valid requests are parsed and the data is available via ``await req.media()``
|
||||
- Invalid requests get an automatic ``422 Unprocessable Entity`` response
|
||||
with detailed error messages — you don't write any validation code
|
||||
|
||||
When ``response_model`` is set:
|
||||
|
||||
- The response is serialized through the model before being sent
|
||||
- Extra fields are stripped automatically
|
||||
- Type coercion happens at the boundary
|
||||
|
||||
This is the recommended way to build validated REST APIs with Responder.
|
||||
See the :doc:`tutorial-rest` for a complete walkthrough.
|
||||
|
||||
|
||||
Content Negotiation
|
||||
-------------------
|
||||
|
||||
Responder automatically negotiates the response format based on the
|
||||
client's ``Accept`` header. Set ``resp.media`` to a Python object and
|
||||
the right thing happens:
|
||||
|
||||
- ``Accept: application/json`` (default) → JSON
|
||||
- ``Accept: application/x-yaml`` → YAML
|
||||
- ``Accept: application/x-msgpack`` → MessagePack
|
||||
|
||||
This means a single endpoint serves multiple formats without any
|
||||
conditional logic in your code::
|
||||
|
||||
@api.route("/data")
|
||||
def data(req, resp):
|
||||
resp.media = {"key": "value"}
|
||||
|
||||
Clients get the format they ask for::
|
||||
|
||||
$ curl http://localhost:5042/data
|
||||
{"key": "value"}
|
||||
|
||||
$ curl -H "Accept: application/x-yaml" http://localhost:5042/data
|
||||
key: value
|
||||
|
||||
|
||||
MessagePack
|
||||
-----------
|
||||
|
||||
`MessagePack <https://msgpack.org/>`_ is a binary serialization format
|
||||
that's more compact and faster to parse than JSON. It's useful for
|
||||
high-throughput APIs, IoT devices, and anywhere bandwidth matters.
|
||||
|
||||
Responder supports MessagePack alongside JSON and YAML::
|
||||
|
||||
# Decode a MessagePack request body
|
||||
data = await req.media("msgpack")
|
||||
|
||||
# Respond with MessagePack
|
||||
resp.media = {"result": [1, 2, 3]}
|
||||
|
||||
Content negotiation works automatically — clients can send
|
||||
``Accept: application/x-msgpack`` to receive MessagePack responses
|
||||
instead of JSON. You can also explicitly decode MessagePack request
|
||||
bodies by passing ``"msgpack"`` to ``req.media()``.
|
||||
@@ -0,0 +1,244 @@
|
||||
Authentication
|
||||
==============
|
||||
|
||||
Every API that handles user data needs authentication — a way to verify
|
||||
who is making a request. This guide covers the most common patterns:
|
||||
API keys, JWT tokens, and how to build reusable auth guards with
|
||||
Responder's before-request hooks.
|
||||
|
||||
|
||||
API Key Authentication
|
||||
----------------------
|
||||
|
||||
The simplest approach. The client sends a secret key in a header, and
|
||||
your server checks it against a known value. This is common for
|
||||
server-to-server communication and simple APIs::
|
||||
|
||||
API_KEYS = {"sk-abc123", "sk-def456"}
|
||||
|
||||
@api.route(before_request=True)
|
||||
def check_api_key(req, resp):
|
||||
key = req.headers.get("X-API-Key")
|
||||
if key not in API_KEYS:
|
||||
resp.status_code = 401
|
||||
resp.media = {"error": "Invalid or missing API key"}
|
||||
|
||||
Because the before-request hook sets ``resp.status_code``, the route
|
||||
handler is skipped entirely for unauthorized requests. The client never
|
||||
reaches your endpoint — the guard catches them first.
|
||||
|
||||
The client sends the key like this::
|
||||
|
||||
$ curl -H "X-API-Key: sk-abc123" http://localhost:5042/protected
|
||||
|
||||
|
||||
Bearer Token Authentication
|
||||
----------------------------
|
||||
|
||||
Bearer tokens are the standard for modern APIs. The client sends a token
|
||||
in the ``Authorization`` header, and the server validates it. The most
|
||||
common format is `JWT <https://jwt.io/>`_ (JSON Web Tokens).
|
||||
|
||||
Install PyJWT::
|
||||
|
||||
$ uv pip install pyjwt
|
||||
|
||||
Create a helper to encode and decode tokens::
|
||||
|
||||
import jwt
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
SECRET = "your-secret-key"
|
||||
|
||||
def create_token(user_id: int) -> str:
|
||||
payload = {
|
||||
"sub": user_id,
|
||||
"exp": datetime.now(timezone.utc) + timedelta(hours=24),
|
||||
}
|
||||
return jwt.encode(payload, SECRET, algorithm="HS256")
|
||||
|
||||
def verify_token(token: str) -> dict | None:
|
||||
try:
|
||||
return jwt.decode(token, SECRET, algorithms=["HS256"])
|
||||
except jwt.InvalidTokenError:
|
||||
return None
|
||||
|
||||
Add a login endpoint that issues tokens, and a before-request hook that
|
||||
verifies them::
|
||||
|
||||
@api.route("/login", methods=["POST"])
|
||||
async def login(req, resp):
|
||||
data = await req.media()
|
||||
# In a real app, check credentials against a database
|
||||
if data.get("username") == "admin" and data.get("password") == "secret":
|
||||
token = create_token(user_id=1)
|
||||
resp.media = {"token": token}
|
||||
else:
|
||||
resp.status_code = 401
|
||||
resp.media = {"error": "Invalid credentials"}
|
||||
|
||||
@api.route(before_request=True)
|
||||
def auth_guard(req, resp):
|
||||
# Skip auth for the login endpoint itself
|
||||
if req.url.path == "/login":
|
||||
return
|
||||
|
||||
auth = req.headers.get("Authorization", "")
|
||||
if not auth.startswith("Bearer "):
|
||||
resp.status_code = 401
|
||||
resp.media = {"error": "Missing bearer token"}
|
||||
return
|
||||
|
||||
token = auth[7:] # Strip "Bearer "
|
||||
payload = verify_token(token)
|
||||
if payload is None:
|
||||
resp.status_code = 401
|
||||
resp.media = {"error": "Invalid or expired token"}
|
||||
return
|
||||
|
||||
# Store the authenticated user on the request state
|
||||
req.state.user_id = payload["sub"]
|
||||
|
||||
Now any route can access the authenticated user::
|
||||
|
||||
@api.route("/me")
|
||||
def get_me(req, resp):
|
||||
resp.media = {"user_id": req.state.user_id}
|
||||
|
||||
The client flow:
|
||||
|
||||
1. ``POST /login`` with credentials → receive a token
|
||||
2. Include ``Authorization: Bearer <token>`` on every subsequent request
|
||||
3. The token expires after 24 hours — the client must log in again
|
||||
|
||||
|
||||
Skipping Auth for Public Routes
|
||||
--------------------------------
|
||||
|
||||
The example above skips auth for ``/login`` by checking the path. For
|
||||
more control, you can use a set of public paths::
|
||||
|
||||
PUBLIC_PATHS = {"/login", "/signup", "/health", "/docs", "/schema.yml"}
|
||||
|
||||
@api.route(before_request=True)
|
||||
def auth_guard(req, resp):
|
||||
if req.url.path in PUBLIC_PATHS:
|
||||
return
|
||||
# ... check token
|
||||
|
||||
|
||||
Custom Exception for Auth Errors
|
||||
---------------------------------
|
||||
|
||||
For cleaner code, define a custom exception and register a handler::
|
||||
|
||||
class AuthError(Exception):
|
||||
def __init__(self, message="Unauthorized", status_code=401):
|
||||
self.message = message
|
||||
self.status_code = status_code
|
||||
|
||||
@api.exception_handler(AuthError)
|
||||
async def handle_auth_error(req, resp, exc):
|
||||
resp.status_code = exc.status_code
|
||||
resp.media = {"error": exc.message}
|
||||
|
||||
Now your auth guard can simply raise::
|
||||
|
||||
@api.route(before_request=True)
|
||||
def auth_guard(req, resp):
|
||||
if req.url.path in PUBLIC_PATHS:
|
||||
return
|
||||
if "Authorization" not in req.headers:
|
||||
raise AuthError("Missing authorization header")
|
||||
|
||||
|
||||
Using Sessions for Web Apps
|
||||
----------------------------
|
||||
|
||||
For traditional web applications (with HTML pages and forms), cookie-based
|
||||
sessions are simpler than tokens. The browser handles cookies automatically
|
||||
— no client-side token management needed::
|
||||
|
||||
@api.route("/login", methods=["POST"])
|
||||
async def login(req, resp):
|
||||
data = await req.media("form")
|
||||
if data["username"] == "admin" and data["password"] == "secret":
|
||||
resp.session["user"] = data["username"]
|
||||
api.redirect(resp, location="/dashboard")
|
||||
else:
|
||||
resp.status_code = 401
|
||||
resp.html = "<p>Invalid credentials</p>"
|
||||
|
||||
@api.route("/dashboard")
|
||||
def dashboard(req, resp):
|
||||
user = req.session.get("user")
|
||||
if not user:
|
||||
api.redirect(resp, location="/login")
|
||||
return
|
||||
resp.html = f"<h1>Welcome, {user}!</h1>"
|
||||
|
||||
@api.route("/logout")
|
||||
def logout(req, resp):
|
||||
resp.session.clear()
|
||||
api.redirect(resp, location="/login")
|
||||
|
||||
Remember to set a proper secret key::
|
||||
|
||||
api = responder.API(secret_key="your-production-secret-key")
|
||||
|
||||
The session data is signed (not encrypted) — users can read it but
|
||||
can't tamper with it. Don't store sensitive data like passwords in
|
||||
sessions.
|
||||
|
||||
|
||||
Role-Based Access Control
|
||||
--------------------------
|
||||
|
||||
For APIs where different users have different permissions, embed the
|
||||
role in the token and check it in route-specific guards::
|
||||
|
||||
def create_token(user_id: int, role: str = "user") -> str:
|
||||
payload = {
|
||||
"sub": user_id,
|
||||
"role": role,
|
||||
"exp": datetime.now(timezone.utc) + timedelta(hours=24),
|
||||
}
|
||||
return jwt.encode(payload, SECRET, algorithm="HS256")
|
||||
|
||||
Create a helper that checks for a specific role::
|
||||
|
||||
def require_role(*roles):
|
||||
"""Before-request hook factory that restricts by role."""
|
||||
def check(req, resp):
|
||||
user_role = getattr(req.state, "role", None)
|
||||
if user_role not in roles:
|
||||
resp.status_code = 403
|
||||
resp.media = {"error": "Insufficient permissions"}
|
||||
return check
|
||||
|
||||
Use it on specific routes::
|
||||
|
||||
@api.route("/admin/users", before_request=require_role("admin"))
|
||||
def list_all_users(req, resp):
|
||||
resp.media = {"users": [...]}
|
||||
|
||||
And store the role during token verification::
|
||||
|
||||
# In your auth_guard:
|
||||
req.state.user_id = payload["sub"]
|
||||
req.state.role = payload.get("role", "user")
|
||||
|
||||
|
||||
Choosing an Auth Strategy
|
||||
--------------------------
|
||||
|
||||
- **API keys** — simplest. Good for server-to-server, CLI tools, and
|
||||
internal services. No expiration unless you build it.
|
||||
- **JWT tokens** — standard for SPAs and mobile apps. Stateless, so
|
||||
they scale well. Downside: you can't revoke them without a blocklist.
|
||||
- **Sessions** — best for traditional web apps with HTML forms. The
|
||||
browser manages cookies automatically. Stateful — the server controls
|
||||
the session lifecycle.
|
||||
|
||||
Start with API keys for internal tools, JWT for public APIs, and
|
||||
sessions for web apps with login pages.
|
||||
@@ -0,0 +1,192 @@
|
||||
Migrating from Flask
|
||||
====================
|
||||
|
||||
If you're coming from Flask, you'll find Responder familiar but different
|
||||
in a few key ways. This guide maps Flask concepts to their Responder
|
||||
equivalents and shows you how to translate common patterns.
|
||||
|
||||
|
||||
The Big Differences
|
||||
-------------------
|
||||
|
||||
**No return values.** In Flask, you return a response. In Responder, you
|
||||
mutate it. This is the single biggest difference:
|
||||
|
||||
Flask::
|
||||
|
||||
@app.route("/")
|
||||
def hello():
|
||||
return "hello, world!"
|
||||
|
||||
Responder::
|
||||
|
||||
@api.route("/")
|
||||
def hello(req, resp):
|
||||
resp.text = "hello, world!"
|
||||
|
||||
**Explicit request and response.** Flask uses a global ``request`` object
|
||||
(via thread-local magic). Responder passes ``req`` and ``resp`` explicitly.
|
||||
No magic, no import needed — they're right there in the function signature.
|
||||
|
||||
**ASGI, not WSGI.** Flask runs on WSGI, which is synchronous. Responder
|
||||
runs on ASGI, which supports async natively. You can still write sync
|
||||
views — Responder runs them in a thread pool automatically.
|
||||
|
||||
|
||||
Quick Reference
|
||||
---------------
|
||||
|
||||
.. list-table::
|
||||
:header-rows: 1
|
||||
:widths: 40 60
|
||||
|
||||
* - Flask
|
||||
- Responder
|
||||
* - ``Flask(__name__)``
|
||||
- ``responder.API()``
|
||||
* - ``return "text"``
|
||||
- ``resp.text = "text"``
|
||||
* - ``return jsonify(data)``
|
||||
- ``resp.media = data``
|
||||
* - ``return render_template("t.html", x=1)``
|
||||
- ``resp.html = api.template("t.html", x=1)``
|
||||
* - ``request.args["q"]``
|
||||
- ``req.params["q"]``
|
||||
* - ``request.json``
|
||||
- ``await req.media()``
|
||||
* - ``request.form``
|
||||
- ``await req.media("form")``
|
||||
* - ``request.headers["X"]``
|
||||
- ``req.headers["X"]``
|
||||
* - ``request.method``
|
||||
- ``req.method``
|
||||
* - ``request.cookies["x"]``
|
||||
- ``req.cookies["x"]``
|
||||
* - ``session["x"] = 1``
|
||||
- ``resp.session["x"] = 1``
|
||||
* - ``abort(404)``
|
||||
- ``resp.status_code = 404``
|
||||
* - ``redirect("/new")``
|
||||
- ``api.redirect(resp, location="/new")``
|
||||
* - ``@app.before_request``
|
||||
- ``@api.route(before_request=True)``
|
||||
* - ``@app.errorhandler(404)``
|
||||
- ``@api.exception_handler(ValueError)``
|
||||
* - ``app.run(debug=True)``
|
||||
- ``api.run(debug=True)``
|
||||
|
||||
|
||||
Route Parameters
|
||||
----------------
|
||||
|
||||
Flask uses ``<angle_brackets>``. Responder uses ``{curly_braces}``
|
||||
with the same type convertor idea:
|
||||
|
||||
Flask::
|
||||
|
||||
@app.route("/users/<int:user_id>")
|
||||
def get_user(user_id):
|
||||
return jsonify({"id": user_id})
|
||||
|
||||
Responder::
|
||||
|
||||
@api.route("/users/{user_id:int}")
|
||||
def get_user(req, resp, *, user_id):
|
||||
resp.media = {"id": user_id}
|
||||
|
||||
Note the ``*`` — route parameters are keyword-only arguments in
|
||||
Responder. This makes the interface explicit about which arguments
|
||||
come from the URL.
|
||||
|
||||
|
||||
JSON APIs
|
||||
---------
|
||||
|
||||
Flask::
|
||||
|
||||
@app.route("/api/items", methods=["POST"])
|
||||
def create_item():
|
||||
data = request.json
|
||||
# ... create item
|
||||
return jsonify(item), 201
|
||||
|
||||
Responder::
|
||||
|
||||
@api.route("/api/items", methods=["POST"])
|
||||
async def create_item(req, resp):
|
||||
data = await req.media()
|
||||
# ... create item
|
||||
resp.media = item
|
||||
resp.status_code = 201
|
||||
|
||||
The ``await`` is needed because reading the request body is an async
|
||||
I/O operation. This is more explicit than Flask's approach, and it
|
||||
means the event loop isn't blocked while waiting for the body to arrive.
|
||||
|
||||
|
||||
Templates
|
||||
---------
|
||||
|
||||
Both use Jinja2. The syntax is nearly identical:
|
||||
|
||||
Flask::
|
||||
|
||||
@app.route("/hello/<name>")
|
||||
def hello(name):
|
||||
return render_template("hello.html", name=name)
|
||||
|
||||
Responder::
|
||||
|
||||
@api.route("/hello/{name}")
|
||||
def hello(req, resp, *, name):
|
||||
resp.html = api.template("hello.html", name=name)
|
||||
|
||||
|
||||
Blueprints → Route Groups
|
||||
--------------------------
|
||||
|
||||
Flask uses Blueprints to organize routes. Responder has route groups:
|
||||
|
||||
Flask::
|
||||
|
||||
bp = Blueprint("api", __name__, url_prefix="/api")
|
||||
|
||||
@bp.route("/users")
|
||||
def list_users():
|
||||
return jsonify([])
|
||||
|
||||
app.register_blueprint(bp)
|
||||
|
||||
Responder::
|
||||
|
||||
api_v1 = api.group("/api")
|
||||
|
||||
@api_v1.route("/users")
|
||||
def list_users(req, resp):
|
||||
resp.media = []
|
||||
|
||||
|
||||
Gradual Migration
|
||||
-----------------
|
||||
|
||||
You don't have to migrate all at once. Responder can mount your existing
|
||||
Flask app at a subroute, so you can move endpoints over one at a time::
|
||||
|
||||
from flask import Flask
|
||||
|
||||
flask_app = Flask(__name__)
|
||||
|
||||
# Your existing Flask routes stay here
|
||||
@flask_app.route("/legacy")
|
||||
def legacy():
|
||||
return "old endpoint"
|
||||
|
||||
# Mount Flask under /old, new routes go on Responder
|
||||
api.mount("/old", flask_app)
|
||||
|
||||
@api.route("/new")
|
||||
def new_endpoint(req, resp):
|
||||
resp.media = {"modern": True}
|
||||
|
||||
Requests to ``/old/legacy`` go to Flask. Everything else goes to
|
||||
Responder. When you've moved everything over, remove the mount.
|
||||
@@ -0,0 +1,170 @@
|
||||
Writing Middleware
|
||||
==================
|
||||
|
||||
Middleware sits between the server and your route handlers, processing
|
||||
every request and response that flows through your application. It's the
|
||||
right tool for cross-cutting concerns — things that apply to *all*
|
||||
requests, not just specific routes.
|
||||
|
||||
Common middleware use cases:
|
||||
|
||||
- Request logging and timing
|
||||
- Authentication and authorization
|
||||
- Adding security headers
|
||||
- Request ID generation
|
||||
- Rate limiting
|
||||
- Response compression (built-in)
|
||||
|
||||
|
||||
Hooks vs. Middleware
|
||||
--------------------
|
||||
|
||||
Responder gives you two levels of request processing:
|
||||
|
||||
**Hooks** (``before_request`` / ``after_request``) run inside Responder's
|
||||
routing layer. They receive Responder's ``req`` and ``resp`` objects and
|
||||
are the simplest way to add behavior::
|
||||
|
||||
@api.route(before_request=True)
|
||||
def add_header(req, resp):
|
||||
resp.headers["X-Powered-By"] = "Responder"
|
||||
|
||||
@api.after_request()
|
||||
def log_request(req, resp):
|
||||
print(f"{req.method} {req.url.path} -> {resp.status_code}")
|
||||
|
||||
**Middleware** runs at the ASGI level, wrapping the entire application.
|
||||
It's more powerful but more complex — you work with raw ASGI scopes
|
||||
instead of Responder objects. Use middleware when you need to process
|
||||
requests *before* they reach Responder's routing, or when you need to
|
||||
integrate with Starlette middleware.
|
||||
|
||||
|
||||
Using Starlette Middleware
|
||||
--------------------------
|
||||
|
||||
Responder is built on Starlette, so any Starlette middleware works
|
||||
out of the box::
|
||||
|
||||
from starlette.middleware.base import BaseHTTPMiddleware
|
||||
|
||||
class TimingMiddleware(BaseHTTPMiddleware):
|
||||
async def dispatch(self, request, call_next):
|
||||
import time
|
||||
start = time.time()
|
||||
response = await call_next(request)
|
||||
duration = time.time() - start
|
||||
response.headers["X-Response-Time"] = f"{duration:.3f}s"
|
||||
return response
|
||||
|
||||
api.add_middleware(TimingMiddleware)
|
||||
|
||||
The ``dispatch`` method receives a Starlette ``Request`` and a
|
||||
``call_next`` function. Call ``call_next(request)`` to pass the request
|
||||
to the next middleware (or to your route handler). The return value is
|
||||
a Starlette ``Response`` that you can modify before it's sent.
|
||||
|
||||
|
||||
Built-in Middleware
|
||||
-------------------
|
||||
|
||||
Responder configures several middleware components automatically:
|
||||
|
||||
- **GZipMiddleware** — compresses responses larger than 500 bytes
|
||||
- **TrustedHostMiddleware** — validates the ``Host`` header
|
||||
- **ServerErrorMiddleware** — catches unhandled exceptions
|
||||
- **ExceptionMiddleware** — routes exceptions to your handlers
|
||||
- **SessionMiddleware** — manages signed cookie sessions
|
||||
|
||||
Optional middleware you can enable:
|
||||
|
||||
- **CORSMiddleware** — ``api = responder.API(cors=True)``
|
||||
- **HTTPSRedirectMiddleware** — ``api = responder.API(enable_hsts=True)``
|
||||
|
||||
|
||||
Adding Third-Party Middleware
|
||||
-----------------------------
|
||||
|
||||
Any ASGI middleware can be added with ``api.add_middleware()``::
|
||||
|
||||
from some_package import SomeMiddleware
|
||||
|
||||
api.add_middleware(SomeMiddleware, option1="value", option2=True)
|
||||
|
||||
Keyword arguments are passed to the middleware's constructor.
|
||||
|
||||
|
||||
Middleware Order
|
||||
----------------
|
||||
|
||||
Middleware wraps your application like layers of an onion. The *last*
|
||||
middleware added is the *outermost* layer — it sees the request first
|
||||
and the response last.
|
||||
|
||||
Responder's built-in middleware stack (from outermost to innermost):
|
||||
|
||||
1. SessionMiddleware
|
||||
2. ServerErrorMiddleware
|
||||
3. CORSMiddleware (if enabled)
|
||||
4. TrustedHostMiddleware
|
||||
5. HTTPSRedirectMiddleware (if enabled)
|
||||
6. GZipMiddleware
|
||||
7. ExceptionMiddleware
|
||||
8. Your routes
|
||||
|
||||
When you call ``api.add_middleware()``, your middleware is added *outside*
|
||||
the existing stack. Keep this in mind for ordering dependencies — if
|
||||
middleware A depends on middleware B having run first, add B before A.
|
||||
|
||||
|
||||
Writing Pure ASGI Middleware
|
||||
----------------------------
|
||||
|
||||
For maximum performance and control, you can write middleware as a plain
|
||||
ASGI application. This bypasses Starlette's ``BaseHTTPMiddleware``
|
||||
abstraction — it's faster and gives you direct access to the ASGI
|
||||
protocol::
|
||||
|
||||
class SecurityHeadersMiddleware:
|
||||
def __init__(self, app):
|
||||
self.app = app
|
||||
|
||||
async def __call__(self, scope, receive, send):
|
||||
if scope["type"] != "http":
|
||||
await self.app(scope, receive, send)
|
||||
return
|
||||
|
||||
async def send_with_headers(message):
|
||||
if message["type"] == "http.response.start":
|
||||
extra = [
|
||||
(b"x-content-type-options", b"nosniff"),
|
||||
(b"x-frame-options", b"DENY"),
|
||||
(b"referrer-policy", b"strict-origin-when-cross-origin"),
|
||||
]
|
||||
message["headers"] = list(message["headers"]) + extra
|
||||
await send(message)
|
||||
|
||||
await self.app(scope, receive, send_with_headers)
|
||||
|
||||
api.add_middleware(SecurityHeadersMiddleware)
|
||||
|
||||
This is the same pattern used internally by Starlette and uvicorn. The
|
||||
middleware receives the ASGI ``scope``, ``receive``, and ``send`` callables,
|
||||
and wraps ``send`` to inject headers into the response.
|
||||
|
||||
For most cases, ``BaseHTTPMiddleware`` is simpler and perfectly fine.
|
||||
Use the pure ASGI approach when you need to handle WebSocket connections,
|
||||
streaming responses, or want to avoid the overhead of request/response
|
||||
object creation.
|
||||
|
||||
|
||||
When to Use What
|
||||
-----------------
|
||||
|
||||
- **Simple header additions, logging, auth checks** → use hooks
|
||||
- **Response transformation, timing, third-party integrations** → use middleware
|
||||
- **Rate limiting** → use the built-in ``RateLimiter`` (it uses hooks internally)
|
||||
- **Request ID** → use ``api = responder.API(request_id=True)``
|
||||
|
||||
Start with hooks. They're simpler and cover most cases. Graduate to
|
||||
middleware when hooks aren't enough.
|
||||
@@ -0,0 +1,219 @@
|
||||
Building a REST API
|
||||
===================
|
||||
|
||||
This tutorial walks you through building a complete REST API from scratch.
|
||||
By the end, you'll have a working API with CRUD operations, request
|
||||
validation, error handling, and interactive documentation.
|
||||
|
||||
We'll build a simple book catalog — a service that lets you create, read,
|
||||
update, and delete books.
|
||||
|
||||
|
||||
Project Setup
|
||||
-------------
|
||||
|
||||
Create a new file called ``app.py``::
|
||||
|
||||
import responder
|
||||
|
||||
api = responder.API(
|
||||
title="Book Catalog",
|
||||
version="1.0",
|
||||
openapi="3.0.2",
|
||||
docs_route="/docs",
|
||||
)
|
||||
|
||||
We're enabling OpenAPI documentation from the start. Visit ``/docs`` at
|
||||
any point to see interactive Swagger UI for your API.
|
||||
|
||||
|
||||
Define Your Models
|
||||
------------------
|
||||
|
||||
We'll use `Pydantic <https://docs.pydantic.dev/>`_ to define our data
|
||||
models. Pydantic models serve double duty — they validate incoming data
|
||||
*and* generate OpenAPI schemas automatically::
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
class BookIn(BaseModel):
|
||||
"""What the client sends when creating a book."""
|
||||
title: str
|
||||
author: str
|
||||
year: int
|
||||
isbn: str | None = None
|
||||
|
||||
class Book(BaseModel):
|
||||
"""What the API returns."""
|
||||
id: int
|
||||
title: str
|
||||
author: str
|
||||
year: int
|
||||
isbn: str | None = None
|
||||
|
||||
``BookIn`` is the *input* model — it doesn't have an ``id`` because the
|
||||
server assigns that. ``Book`` is the *output* model — it includes
|
||||
everything. This input/output separation is a common REST API pattern.
|
||||
|
||||
|
||||
In-Memory Storage
|
||||
-----------------
|
||||
|
||||
For this tutorial, we'll store books in a simple dict. In a real
|
||||
application, you'd use a database (see :doc:`tutorial-sqlalchemy`)::
|
||||
|
||||
books_db: dict[int, dict] = {}
|
||||
next_id = 1
|
||||
|
||||
|
||||
List All Books
|
||||
--------------
|
||||
|
||||
The first endpoint — list all books. This is a ``GET`` request to
|
||||
``/books``::
|
||||
|
||||
@api.route("/books", methods=["GET"], response_model=list)
|
||||
def list_books(req, resp):
|
||||
resp.media = list(books_db.values())
|
||||
|
||||
In REST API design, ``GET`` requests should never modify data. They're
|
||||
*safe* and *idempotent* — calling them multiple times has the same effect
|
||||
as calling them once.
|
||||
|
||||
|
||||
Create a Book
|
||||
-------------
|
||||
|
||||
To create a book, the client sends a ``POST`` request with a JSON body.
|
||||
We use ``request_model=BookIn`` to validate the input automatically — if
|
||||
the client sends bad data, they get a ``422`` response with error details::
|
||||
|
||||
@api.route("/books", methods=["POST"], check_existing=False,
|
||||
request_model=BookIn, response_model=Book)
|
||||
async def create_book(req, resp):
|
||||
global next_id
|
||||
data = await req.media()
|
||||
|
||||
book = {"id": next_id, **data}
|
||||
books_db[next_id] = book
|
||||
next_id += 1
|
||||
|
||||
resp.media = book
|
||||
resp.status_code = 201
|
||||
|
||||
Note ``resp.status_code = 201`` — the HTTP ``201 Created`` status code
|
||||
tells the client that a new resource was successfully created. This is
|
||||
more informative than a generic ``200 OK``.
|
||||
|
||||
|
||||
Get a Single Book
|
||||
-----------------
|
||||
|
||||
Retrieve a specific book by its ID. The ``{book_id:int}`` route parameter
|
||||
ensures only integer IDs match — requests like ``/books/abc`` will 404::
|
||||
|
||||
@api.route("/books/{book_id:int}", methods=["GET"], response_model=Book)
|
||||
def get_book(req, resp, *, book_id):
|
||||
if book_id not in books_db:
|
||||
resp.status_code = 404
|
||||
resp.media = {"error": f"Book {book_id} not found"}
|
||||
return
|
||||
|
||||
resp.media = books_db[book_id]
|
||||
|
||||
|
||||
Update a Book
|
||||
-------------
|
||||
|
||||
``PUT`` replaces a resource entirely. The client must send all fields::
|
||||
|
||||
@api.route("/books/{book_id:int}", methods=["PUT"], check_existing=False,
|
||||
request_model=BookIn, response_model=Book)
|
||||
async def update_book(req, resp, *, book_id):
|
||||
if book_id not in books_db:
|
||||
resp.status_code = 404
|
||||
resp.media = {"error": f"Book {book_id} not found"}
|
||||
return
|
||||
|
||||
data = await req.media()
|
||||
book = {"id": book_id, **data}
|
||||
books_db[book_id] = book
|
||||
resp.media = book
|
||||
|
||||
|
||||
Delete a Book
|
||||
-------------
|
||||
|
||||
``DELETE`` removes a resource. The convention is to return ``204 No Content``
|
||||
with an empty body on success::
|
||||
|
||||
@api.route("/books/{book_id:int}", methods=["DELETE"], check_existing=False)
|
||||
def delete_book(req, resp, *, book_id):
|
||||
if book_id not in books_db:
|
||||
resp.status_code = 404
|
||||
resp.media = {"error": f"Book {book_id} not found"}
|
||||
return
|
||||
|
||||
del books_db[book_id]
|
||||
resp.status_code = 204
|
||||
|
||||
|
||||
Error Handling
|
||||
--------------
|
||||
|
||||
Let's add a custom error handler so any ``ValueError`` in our code returns
|
||||
a clean JSON response instead of a 500 error::
|
||||
|
||||
@api.exception_handler(ValueError)
|
||||
async def handle_value_error(req, resp, exc):
|
||||
resp.status_code = 400
|
||||
resp.media = {"error": str(exc)}
|
||||
|
||||
|
||||
Run It
|
||||
------
|
||||
|
||||
Add the standard entry point at the bottom of your file::
|
||||
|
||||
if __name__ == "__main__":
|
||||
api.run()
|
||||
|
||||
Start the server::
|
||||
|
||||
$ python app.py
|
||||
|
||||
Visit ``http://localhost:5042/docs`` to see your interactive API
|
||||
documentation. You can test every endpoint directly from the browser.
|
||||
|
||||
|
||||
Try It Out
|
||||
----------
|
||||
|
||||
Using ``curl``::
|
||||
|
||||
# Create a book
|
||||
$ curl -X POST http://localhost:5042/books \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"title": "Dune", "author": "Frank Herbert", "year": 1965}'
|
||||
|
||||
# List all books
|
||||
$ curl http://localhost:5042/books
|
||||
|
||||
# Get a specific book
|
||||
$ curl http://localhost:5042/books/1
|
||||
|
||||
# Update a book
|
||||
$ curl -X PUT http://localhost:5042/books/1 \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"title": "Dune", "author": "Frank Herbert", "year": 1965, "isbn": "978-0441172719"}'
|
||||
|
||||
# Delete a book
|
||||
$ curl -X DELETE http://localhost:5042/books/1
|
||||
|
||||
|
||||
What's Next
|
||||
-----------
|
||||
|
||||
This tutorial used in-memory storage. For a real application, you'll want
|
||||
a database. See :doc:`tutorial-sqlalchemy` for how to integrate SQLAlchemy
|
||||
with Responder using the lifespan pattern.
|
||||
@@ -0,0 +1,255 @@
|
||||
Using SQLAlchemy
|
||||
================
|
||||
|
||||
Most real web applications need a database. This guide shows how to
|
||||
integrate `SQLAlchemy <https://www.sqlalchemy.org/>`_ with Responder,
|
||||
using async support and the lifespan pattern for connection management.
|
||||
|
||||
SQLAlchemy is the most popular Python database toolkit. It gives you an
|
||||
ORM (Object-Relational Mapper) for working with databases using Python
|
||||
classes instead of raw SQL, plus a powerful query builder for when you
|
||||
need fine-grained control.
|
||||
|
||||
|
||||
Installation
|
||||
------------
|
||||
|
||||
Install SQLAlchemy with async support and an async database driver.
|
||||
We'll use SQLite for simplicity, but the pattern works with PostgreSQL,
|
||||
MySQL, and any other database SQLAlchemy supports::
|
||||
|
||||
$ uv pip install 'sqlalchemy[asyncio]' aiosqlite
|
||||
|
||||
|
||||
Define Your Models
|
||||
------------------
|
||||
|
||||
SQLAlchemy models map Python classes to database tables. Each attribute
|
||||
becomes a column::
|
||||
|
||||
# models.py
|
||||
from sqlalchemy import String
|
||||
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
|
||||
|
||||
class Base(DeclarativeBase):
|
||||
pass
|
||||
|
||||
class Book(Base):
|
||||
__tablename__ = "books"
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
|
||||
title: Mapped[str] = mapped_column(String, nullable=False)
|
||||
author: Mapped[str] = mapped_column(String, nullable=False)
|
||||
year: Mapped[int] = mapped_column(nullable=False)
|
||||
isbn: Mapped[str | None] = mapped_column(String, nullable=True)
|
||||
|
||||
This uses SQLAlchemy 2.0's ``Mapped`` type annotations and
|
||||
``mapped_column()``, which give you type checker support and cleaner
|
||||
syntax than the older ``Column()`` style. Each model class corresponds
|
||||
to a table, and each ``mapped_column()`` corresponds to a column.
|
||||
|
||||
|
||||
Database Setup
|
||||
--------------
|
||||
|
||||
Create the async engine and session factory. The *engine* manages
|
||||
the connection pool. The *session* is your unit of work — you use it to
|
||||
query and modify data within a transaction::
|
||||
|
||||
# database.py
|
||||
from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker
|
||||
|
||||
DATABASE_URL = "sqlite+aiosqlite:///./books.db"
|
||||
|
||||
engine = create_async_engine(DATABASE_URL, echo=True)
|
||||
async_session = async_sessionmaker(engine, expire_on_commit=False)
|
||||
|
||||
The ``echo=True`` flag prints all SQL queries to the console — very
|
||||
helpful during development, but you'll want to disable it in production.
|
||||
|
||||
The ``expire_on_commit=False`` flag keeps model attributes accessible
|
||||
after a commit, which is convenient for returning created objects in
|
||||
API responses.
|
||||
|
||||
|
||||
Lifespan for Startup and Shutdown
|
||||
----------------------------------
|
||||
|
||||
Use Responder's lifespan context manager to create the database tables
|
||||
on startup and dispose of connections on shutdown::
|
||||
|
||||
# app.py
|
||||
from contextlib import asynccontextmanager
|
||||
import responder
|
||||
from database import engine
|
||||
from models import Base
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app):
|
||||
# Startup: create tables
|
||||
async with engine.begin() as conn:
|
||||
await conn.run_sync(Base.metadata.create_all)
|
||||
yield
|
||||
# Shutdown: close all connections
|
||||
await engine.dispose()
|
||||
|
||||
api = responder.API(lifespan=lifespan)
|
||||
|
||||
This is the proper way to manage database connections in an async
|
||||
application. The lifespan context manager ensures that:
|
||||
|
||||
1. Tables are created before the first request
|
||||
2. The connection pool is properly closed when the server shuts down
|
||||
3. If table creation fails, the server won't start
|
||||
|
||||
|
||||
CRUD Endpoints
|
||||
--------------
|
||||
|
||||
Now let's build the API endpoints. Each one opens a database session,
|
||||
does its work, and commits or rolls back::
|
||||
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy import select
|
||||
from database import async_session
|
||||
from models import Book
|
||||
|
||||
# Pydantic models for request/response validation
|
||||
class BookIn(BaseModel):
|
||||
title: str
|
||||
author: str
|
||||
year: int
|
||||
isbn: str | None = None
|
||||
|
||||
class BookOut(BaseModel):
|
||||
id: int
|
||||
title: str
|
||||
author: str
|
||||
year: int
|
||||
isbn: str | None = None
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
The ``from_attributes = True`` config tells Pydantic to read data from
|
||||
SQLAlchemy model attributes (not just dicts). This lets you pass a
|
||||
SQLAlchemy ``Book`` object directly to ``BookOut``.
|
||||
|
||||
**List all books**::
|
||||
|
||||
@api.route("/books", methods=["GET"])
|
||||
async def list_books(req, resp):
|
||||
async with async_session() as session:
|
||||
result = await session.execute(select(Book))
|
||||
books = result.scalars().all()
|
||||
resp.media = [BookOut.model_validate(b).model_dump() for b in books]
|
||||
|
||||
**Create a book**::
|
||||
|
||||
@api.route("/books", methods=["POST"], check_existing=False,
|
||||
request_model=BookIn, response_model=BookOut)
|
||||
async def create_book(req, resp):
|
||||
data = await req.media()
|
||||
|
||||
async with async_session() as session:
|
||||
book = Book(**data)
|
||||
session.add(book)
|
||||
await session.commit()
|
||||
await session.refresh(book)
|
||||
resp.media = BookOut.model_validate(book).model_dump()
|
||||
resp.status_code = 201
|
||||
|
||||
**Get a single book**::
|
||||
|
||||
@api.route("/books/{book_id:int}", methods=["GET"])
|
||||
async def get_book(req, resp, *, book_id):
|
||||
async with async_session() as session:
|
||||
book = await session.get(Book, book_id)
|
||||
if book is None:
|
||||
resp.status_code = 404
|
||||
resp.media = {"error": "Book not found"}
|
||||
return
|
||||
resp.media = BookOut.model_validate(book).model_dump()
|
||||
|
||||
**Update a book**::
|
||||
|
||||
@api.route("/books/{book_id:int}", methods=["PUT"], check_existing=False,
|
||||
request_model=BookIn)
|
||||
async def update_book(req, resp, *, book_id):
|
||||
data = await req.media()
|
||||
|
||||
async with async_session() as session:
|
||||
book = await session.get(Book, book_id)
|
||||
if book is None:
|
||||
resp.status_code = 404
|
||||
resp.media = {"error": "Book not found"}
|
||||
return
|
||||
|
||||
for key, value in data.items():
|
||||
setattr(book, key, value)
|
||||
|
||||
await session.commit()
|
||||
await session.refresh(book)
|
||||
resp.media = BookOut.model_validate(book).model_dump()
|
||||
|
||||
**Delete a book**::
|
||||
|
||||
@api.route("/books/{book_id:int}", methods=["DELETE"], check_existing=False)
|
||||
async def delete_book(req, resp, *, book_id):
|
||||
async with async_session() as session:
|
||||
book = await session.get(Book, book_id)
|
||||
if book is None:
|
||||
resp.status_code = 404
|
||||
resp.media = {"error": "Book not found"}
|
||||
return
|
||||
|
||||
await session.delete(book)
|
||||
await session.commit()
|
||||
resp.status_code = 204
|
||||
|
||||
|
||||
Run It
|
||||
------
|
||||
|
||||
::
|
||||
|
||||
if __name__ == "__main__":
|
||||
api.run()
|
||||
|
||||
Start the server and you'll see SQLAlchemy's SQL echo in the console.
|
||||
The SQLite database file ``books.db`` is created automatically on first
|
||||
startup.
|
||||
|
||||
|
||||
Using PostgreSQL
|
||||
----------------
|
||||
|
||||
To switch to PostgreSQL, just change the connection URL and driver::
|
||||
|
||||
$ uv pip install asyncpg
|
||||
|
||||
::
|
||||
|
||||
DATABASE_URL = "postgresql+asyncpg://user:pass@localhost/mydb"
|
||||
|
||||
Everything else stays the same. SQLAlchemy abstracts the database
|
||||
differences so your application code doesn't need to change.
|
||||
|
||||
|
||||
Tips
|
||||
----
|
||||
|
||||
- Use ``async with async_session() as session`` for every request.
|
||||
Don't share sessions across requests — each request should get its
|
||||
own session and transaction.
|
||||
|
||||
- For complex queries, use SQLAlchemy's ``select()`` with ``.where()``,
|
||||
``.order_by()``, ``.limit()``, and ``.offset()`` — it composes
|
||||
naturally.
|
||||
|
||||
- In production, use connection pooling (SQLAlchemy does this by
|
||||
default) and set pool size limits appropriate for your database.
|
||||
|
||||
- Consider `Alembic <https://alembic.sqlalchemy.org/>`_ for database
|
||||
migrations — it tracks schema changes over time so you can evolve
|
||||
your database without losing data.
|
||||
@@ -0,0 +1,219 @@
|
||||
WebSocket Tutorial
|
||||
==================
|
||||
|
||||
HTTP is request-response — the client asks, the server answers, and the
|
||||
connection closes. WebSockets upgrade that into a persistent, bidirectional
|
||||
channel where both sides can send messages at any time. This is what powers
|
||||
chat apps, live dashboards, multiplayer games, and collaborative editors.
|
||||
|
||||
This tutorial builds a simple chat room to show how WebSockets work in
|
||||
Responder.
|
||||
|
||||
|
||||
How WebSockets Work
|
||||
-------------------
|
||||
|
||||
1. The client sends a normal HTTP request with an ``Upgrade: websocket``
|
||||
header.
|
||||
2. The server accepts the upgrade and the connection switches protocols.
|
||||
3. Both sides can now send messages freely — no more request/response.
|
||||
4. Either side can close the connection at any time.
|
||||
|
||||
In Responder, WebSocket routes receive a ``ws`` object instead of
|
||||
``req`` and ``resp``. The ``ws`` object has methods for accepting the
|
||||
connection, sending and receiving data, and closing.
|
||||
|
||||
|
||||
Echo Server
|
||||
-----------
|
||||
|
||||
The simplest WebSocket — echoes everything back::
|
||||
|
||||
@api.route("/ws", websocket=True)
|
||||
async def echo(ws):
|
||||
await ws.accept()
|
||||
while True:
|
||||
data = await ws.receive_text()
|
||||
await ws.send_text(f"Echo: {data}")
|
||||
|
||||
The ``await ws.accept()`` call completes the WebSocket handshake. After
|
||||
that, you're in a loop — receive a message, send a response.
|
||||
|
||||
Test it with a WebSocket client::
|
||||
|
||||
$ pip install websocket-client
|
||||
$ python -c "
|
||||
import websocket
|
||||
ws = websocket.create_connection('ws://localhost:5042/ws')
|
||||
ws.send('hello')
|
||||
print(ws.recv()) # Echo: hello
|
||||
ws.close()
|
||||
"
|
||||
|
||||
|
||||
Chat Room
|
||||
---------
|
||||
|
||||
A chat room needs to broadcast messages to all connected clients. We keep
|
||||
a set of active connections and iterate through them when someone sends
|
||||
a message::
|
||||
|
||||
from starlette.websockets import WebSocketDisconnect
|
||||
|
||||
connected = set()
|
||||
|
||||
@api.route("/chat", websocket=True)
|
||||
async def chat(ws):
|
||||
await ws.accept()
|
||||
connected.add(ws)
|
||||
try:
|
||||
while True:
|
||||
message = await ws.receive_text()
|
||||
# Broadcast to all connected clients
|
||||
for client in connected:
|
||||
await client.send_text(message)
|
||||
except WebSocketDisconnect:
|
||||
pass
|
||||
finally:
|
||||
connected.discard(ws)
|
||||
|
||||
The ``try/finally`` block ensures we remove disconnected clients from
|
||||
the set, even if the connection drops unexpectedly. Catching
|
||||
``WebSocketDisconnect`` specifically (rather than bare ``Exception``)
|
||||
makes the intent clear and avoids swallowing real bugs.
|
||||
|
||||
|
||||
Data Formats
|
||||
------------
|
||||
|
||||
WebSockets support three data formats:
|
||||
|
||||
**Text** — plain strings::
|
||||
|
||||
await ws.send_text("hello")
|
||||
message = await ws.receive_text()
|
||||
|
||||
**JSON** — auto-serialized Python objects::
|
||||
|
||||
await ws.send_json({"type": "update", "data": [1, 2, 3]})
|
||||
message = await ws.receive_json()
|
||||
|
||||
**Binary** — raw bytes, useful for images, audio, or custom protocols::
|
||||
|
||||
await ws.send_bytes(b"\x00\x01\x02")
|
||||
data = await ws.receive_bytes()
|
||||
|
||||
|
||||
HTML Client
|
||||
-----------
|
||||
|
||||
Here's a minimal HTML page that connects to the chat room. The browser's
|
||||
built-in ``WebSocket`` API handles everything — no libraries needed:
|
||||
|
||||
.. code-block:: html
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<body>
|
||||
<div id="messages"></div>
|
||||
<input id="input" placeholder="Type a message..." />
|
||||
<script>
|
||||
const ws = new WebSocket("ws://localhost:5042/chat");
|
||||
const messages = document.getElementById("messages");
|
||||
const input = document.getElementById("input");
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
const p = document.createElement("p");
|
||||
p.textContent = event.data;
|
||||
messages.appendChild(p);
|
||||
};
|
||||
|
||||
input.addEventListener("keypress", (e) => {
|
||||
if (e.key === "Enter") {
|
||||
ws.send(input.value);
|
||||
input.value = "";
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Save this as ``static/index.html`` and serve it with Responder's
|
||||
built-in static file support.
|
||||
|
||||
|
||||
Before-Request Hooks for WebSockets
|
||||
------------------------------------
|
||||
|
||||
You can run code before a WebSocket connection is established, just like
|
||||
HTTP before-request hooks. This is useful for authentication::
|
||||
|
||||
@api.before_request(websocket=True)
|
||||
async def ws_auth(ws):
|
||||
# Check for a token in the query string
|
||||
# (WebSocket headers are limited in browsers)
|
||||
await ws.accept()
|
||||
|
||||
WebSocket before-request hooks receive the ``ws`` object and must call
|
||||
``await ws.accept()`` if they want the connection to proceed.
|
||||
|
||||
|
||||
Connection Lifecycle
|
||||
--------------------
|
||||
|
||||
WebSocket connections go through several states:
|
||||
|
||||
1. **Connecting** — the client sends an upgrade request
|
||||
2. **Open** — after ``await ws.accept()``, both sides can send messages
|
||||
3. **Closing** — either side initiates a close handshake
|
||||
4. **Closed** — the connection is fully terminated
|
||||
|
||||
When a client disconnects (closes the tab, loses network), the next
|
||||
``await ws.receive_text()`` raises ``WebSocketDisconnect``. Always
|
||||
handle this — otherwise your server accumulates dead connections::
|
||||
|
||||
from starlette.websockets import WebSocketDisconnect
|
||||
|
||||
@api.route("/ws", websocket=True)
|
||||
async def handler(ws):
|
||||
await ws.accept()
|
||||
try:
|
||||
while True:
|
||||
data = await ws.receive_text()
|
||||
await ws.send_text(f"Got: {data}")
|
||||
except WebSocketDisconnect:
|
||||
print("Client disconnected")
|
||||
|
||||
You can also close connections from the server side::
|
||||
|
||||
await ws.close(code=1000) # 1000 = normal closure
|
||||
|
||||
Common close codes: ``1000`` (normal), ``1001`` (going away),
|
||||
``1008`` (policy violation), ``1011`` (server error).
|
||||
|
||||
|
||||
Testing WebSockets
|
||||
------------------
|
||||
|
||||
Use Starlette's ``TestClient`` for WebSocket tests::
|
||||
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
def test_echo():
|
||||
client = TestClient(api)
|
||||
with client.websocket_connect("/ws") as ws:
|
||||
ws.send_text("hello")
|
||||
assert ws.receive_text() == "Echo: hello"
|
||||
|
||||
The ``websocket_connect`` context manager handles the connection
|
||||
lifecycle — it connects on enter and disconnects on exit.
|
||||
|
||||
You can also test that connections are properly rejected::
|
||||
|
||||
from starlette.websockets import WebSocketDisconnect
|
||||
|
||||
def test_websocket_404():
|
||||
client = TestClient(api)
|
||||
with pytest.raises(WebSocketDisconnect):
|
||||
with client.websocket_connect("/nonexistent"):
|
||||
pass
|
||||
@@ -0,0 +1,26 @@
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
console.info("Initializing sphinx-design-elements");
|
||||
setup_dropdown_group();
|
||||
});
|
||||
|
||||
function setup_dropdown_group() {
|
||||
|
||||
// Select all relevant detail elements nested within container elements using the `dropdown-group` class.
|
||||
const dropdown_details = document.querySelectorAll(".dropdown-group details");
|
||||
|
||||
// Add event listener for special toggling.
|
||||
dropdown_details.forEach((details) => {
|
||||
details.addEventListener("toggle", toggleOpenGroup);
|
||||
});
|
||||
|
||||
// When toggling elements, exclusively open one element, and close all others.
|
||||
function toggleOpenGroup(e) {
|
||||
if (this.open) {
|
||||
dropdown_details.forEach((details) => {
|
||||
if (details !== this && details.open) {
|
||||
details.open = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
/**
|
||||
* Appearance shortcuts.
|
||||
*/
|
||||
|
||||
|
||||
/* General */
|
||||
|
||||
/* Display paragraphs inline (side by side) */
|
||||
.inline p {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
/* No borders */
|
||||
.no-margin {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
hr.docutils {
|
||||
margin-top: 0.75rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
/* Very small text size */
|
||||
.text-small {
|
||||
font-size: small;
|
||||
}
|
||||
|
||||
/* Smaller text size */
|
||||
.text-smaller {
|
||||
font-size: smaller;
|
||||
}
|
||||
|
||||
/* Medium text size */
|
||||
.text-medium {
|
||||
font-size: medium;
|
||||
}
|
||||
|
||||
/* Large text size */
|
||||
.text-large {
|
||||
font-size: large;
|
||||
}
|
||||
|
||||
/* Very large text size */
|
||||
.text-larger {
|
||||
font-size: larger;
|
||||
}
|
||||
|
||||
|
||||
/* Grid table: Light-weight table-row outline styling. */
|
||||
.top-border {
|
||||
border-top-width: thin;
|
||||
border-top-style: solid;
|
||||
border-top-color: lightgray;
|
||||
}
|
||||
.bottom-border {
|
||||
border-bottom-width: thin;
|
||||
border-bottom-style: solid;
|
||||
border-top-color: lightgray;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Appearance bugfixes.
|
||||
*/
|
||||
|
||||
/* Grid table: Fix margins of child elements inside table items. */
|
||||
.sd-col > .highlight-markdown {
|
||||
margin: unset;
|
||||
}
|
||||
.sd-col > .highlight-restructuredtext {
|
||||
margin: unset;
|
||||
}
|
||||
|
||||
/* Shield: Inside a grid table, code blocks in neighbouring cells did not have any margins to each other. */
|
||||
.col-compact * {
|
||||
margin-bottom: 0.3em;
|
||||
}
|
||||
.sd-col .highlight {
|
||||
margin-bottom: 0.3em !important;
|
||||
}
|
||||
|
||||
/* Info card: Fix grid item formatting. */
|
||||
.sd-col > div.line-block {
|
||||
margin-top: unset;
|
||||
margin-bottom: unset;
|
||||
}
|
||||
|
||||
.bottom-margin-generous {
|
||||
margin-bottom: 2em !important;
|
||||
}
|
||||
@@ -0,0 +1,663 @@
|
||||
/* -- page layout ----------------------------------------------------------- */
|
||||
|
||||
body {
|
||||
font-family: Georgia, serif;
|
||||
font-size: 17px;
|
||||
background-color: #fff;
|
||||
color: #000;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
|
||||
div.document {
|
||||
width: 940px;
|
||||
margin: 30px auto 0 auto;
|
||||
}
|
||||
|
||||
div.documentwrapper {
|
||||
float: left;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
div.bodywrapper {
|
||||
margin: 0 0 0 220px;
|
||||
}
|
||||
|
||||
div.sphinxsidebar {
|
||||
width: 220px;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
hr {
|
||||
border: 1px solid #B1B4B6;
|
||||
}
|
||||
|
||||
div.body {
|
||||
background-color: #fff;
|
||||
color: #3E4349;
|
||||
padding: 0 30px 0 30px;
|
||||
}
|
||||
|
||||
div.body > .section {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
div.footer {
|
||||
width: 940px;
|
||||
margin: 20px auto 30px auto;
|
||||
font-size: 14px;
|
||||
color: #888;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
div.footer a {
|
||||
color: #888;
|
||||
}
|
||||
|
||||
p.caption {
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
}
|
||||
|
||||
|
||||
div.relations {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
||||
div.sphinxsidebar {
|
||||
max-height: 100%;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
div.sphinxsidebar a {
|
||||
color: #444;
|
||||
text-decoration: none;
|
||||
border-bottom: 1px dotted #999;
|
||||
}
|
||||
|
||||
div.sphinxsidebar a:hover {
|
||||
border-bottom: 1px solid #999;
|
||||
}
|
||||
|
||||
div.sphinxsidebarwrapper {
|
||||
padding: 18px 10px;
|
||||
}
|
||||
|
||||
div.sphinxsidebarwrapper p.logo {
|
||||
padding: 0;
|
||||
margin: -10px 0 0 0px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
div.sphinxsidebarwrapper h1.logo {
|
||||
margin-top: -10px;
|
||||
text-align: center;
|
||||
margin-bottom: 5px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
div.sphinxsidebarwrapper h1.logo-name {
|
||||
margin-top: 0px;
|
||||
}
|
||||
|
||||
div.sphinxsidebarwrapper p.blurb {
|
||||
margin-top: 0;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
div.sphinxsidebar h3,
|
||||
div.sphinxsidebar h4 {
|
||||
font-family: Georgia, serif;
|
||||
color: #444;
|
||||
font-size: 24px;
|
||||
font-weight: normal;
|
||||
margin: 0 0 5px 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
div.sphinxsidebar h4 {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
div.sphinxsidebar h3 a {
|
||||
color: #444;
|
||||
}
|
||||
|
||||
div.sphinxsidebar p.logo a,
|
||||
div.sphinxsidebar h3 a,
|
||||
div.sphinxsidebar p.logo a:hover,
|
||||
div.sphinxsidebar h3 a:hover {
|
||||
border: none;
|
||||
}
|
||||
|
||||
div.sphinxsidebar p {
|
||||
color: #555;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
div.sphinxsidebar ul {
|
||||
margin: 10px 0;
|
||||
padding: 0;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
div.sphinxsidebar ul li.toctree-l1 > a {
|
||||
font-size: 120%;
|
||||
}
|
||||
|
||||
div.sphinxsidebar ul li.toctree-l2 > a {
|
||||
font-size: 110%;
|
||||
}
|
||||
|
||||
div.sphinxsidebar input {
|
||||
border: 1px solid #CCC;
|
||||
font-family: Georgia, serif;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
div.sphinxsidebar #searchbox {
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
div.sphinxsidebar .search > div {
|
||||
display: table-cell;
|
||||
}
|
||||
|
||||
div.sphinxsidebar hr {
|
||||
border: none;
|
||||
height: 1px;
|
||||
color: #AAA;
|
||||
background: #AAA;
|
||||
|
||||
text-align: left;
|
||||
margin-left: 0;
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
div.sphinxsidebar .badge {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
div.sphinxsidebar .badge:hover {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
/* To address an issue with donation coming after search */
|
||||
div.sphinxsidebar h3.donation {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
/* -- body styles ----------------------------------------------------------- */
|
||||
|
||||
a {
|
||||
color: #004B6B;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: #6D4100;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
div.body h1,
|
||||
div.body h2,
|
||||
div.body h3,
|
||||
div.body h4,
|
||||
div.body h5,
|
||||
div.body h6 {
|
||||
font-family: Georgia, serif;
|
||||
font-weight: normal;
|
||||
margin: 30px 0px 10px 0px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
div.body h1 { margin-top: 0; padding-top: 0; font-size: 240%; }
|
||||
div.body h2 { font-size: 180%; }
|
||||
div.body h3 { font-size: 150%; }
|
||||
div.body h4 { font-size: 130%; }
|
||||
div.body h5 { font-size: 100%; }
|
||||
div.body h6 { font-size: 100%; }
|
||||
|
||||
a.headerlink {
|
||||
color: #DDD;
|
||||
padding: 0 4px;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a.headerlink:hover {
|
||||
color: #444;
|
||||
background: #EAEAEA;
|
||||
}
|
||||
|
||||
div.body p, div.body dd, div.body li {
|
||||
line-height: 1.4em;
|
||||
}
|
||||
|
||||
div.admonition {
|
||||
margin: 20px 0px;
|
||||
padding: 10px 30px;
|
||||
background-color: #EEE;
|
||||
border: 1px solid #CCC;
|
||||
}
|
||||
|
||||
div.admonition tt.xref, div.admonition code.xref, div.admonition a tt {
|
||||
background-color: #FBFBFB;
|
||||
border-bottom: 1px solid #fafafa;
|
||||
}
|
||||
|
||||
div.admonition p.admonition-title {
|
||||
font-family: Georgia, serif;
|
||||
font-weight: normal;
|
||||
font-size: 24px;
|
||||
margin: 0 0 10px 0;
|
||||
padding: 0;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
div.admonition p.last {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
dt:target, .highlight {
|
||||
background: #FAF3E8;
|
||||
}
|
||||
|
||||
div.warning {
|
||||
background-color: #FCC;
|
||||
border: 1px solid #FAA;
|
||||
}
|
||||
|
||||
div.danger {
|
||||
background-color: #FCC;
|
||||
border: 1px solid #FAA;
|
||||
-moz-box-shadow: 2px 2px 4px #D52C2C;
|
||||
-webkit-box-shadow: 2px 2px 4px #D52C2C;
|
||||
box-shadow: 2px 2px 4px #D52C2C;
|
||||
}
|
||||
|
||||
div.error {
|
||||
background-color: #FCC;
|
||||
border: 1px solid #FAA;
|
||||
-moz-box-shadow: 2px 2px 4px #D52C2C;
|
||||
-webkit-box-shadow: 2px 2px 4px #D52C2C;
|
||||
box-shadow: 2px 2px 4px #D52C2C;
|
||||
}
|
||||
|
||||
div.caution {
|
||||
background-color: #FCC;
|
||||
border: 1px solid #FAA;
|
||||
}
|
||||
|
||||
div.attention {
|
||||
background-color: #FCC;
|
||||
border: 1px solid #FAA;
|
||||
}
|
||||
|
||||
div.important {
|
||||
background-color: #EEE;
|
||||
border: 1px solid #CCC;
|
||||
}
|
||||
|
||||
div.note {
|
||||
background-color: #EEE;
|
||||
border: 1px solid #CCC;
|
||||
}
|
||||
|
||||
div.tip {
|
||||
background-color: #EEE;
|
||||
border: 1px solid #CCC;
|
||||
}
|
||||
|
||||
div.hint {
|
||||
background-color: #EEE;
|
||||
border: 1px solid #CCC;
|
||||
}
|
||||
|
||||
div.seealso {
|
||||
background-color: #EEE;
|
||||
border: 1px solid #CCC;
|
||||
}
|
||||
|
||||
div.topic {
|
||||
background-color: #EEE;
|
||||
}
|
||||
|
||||
p.admonition-title {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
p.admonition-title:after {
|
||||
content: ":";
|
||||
}
|
||||
|
||||
pre, tt, code {
|
||||
font-family: 'Consolas', 'Menlo', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', monospace;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.hll {
|
||||
background-color: #FFC;
|
||||
margin: 0 -12px;
|
||||
padding: 0 12px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
img.screenshot {
|
||||
}
|
||||
|
||||
tt.descname, tt.descclassname, code.descname, code.descclassname {
|
||||
font-size: 0.95em;
|
||||
}
|
||||
|
||||
tt.descname, code.descname {
|
||||
padding-right: 0.08em;
|
||||
}
|
||||
|
||||
img.screenshot {
|
||||
-moz-box-shadow: 2px 2px 4px #EEE;
|
||||
-webkit-box-shadow: 2px 2px 4px #EEE;
|
||||
box-shadow: 2px 2px 4px #EEE;
|
||||
}
|
||||
|
||||
table.docutils {
|
||||
border: 1px solid #888;
|
||||
-moz-box-shadow: 2px 2px 4px #EEE;
|
||||
-webkit-box-shadow: 2px 2px 4px #EEE;
|
||||
box-shadow: 2px 2px 4px #EEE;
|
||||
}
|
||||
|
||||
table.docutils td, table.docutils th {
|
||||
border: 1px solid #888;
|
||||
padding: 0.25em 0.7em;
|
||||
}
|
||||
|
||||
table.field-list, table.footnote {
|
||||
border: none;
|
||||
-moz-box-shadow: none;
|
||||
-webkit-box-shadow: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
table.footnote {
|
||||
margin: 15px 0;
|
||||
width: 100%;
|
||||
border: 1px solid #EEE;
|
||||
background: #FDFDFD;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
table.footnote + table.footnote {
|
||||
margin-top: -15px;
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
table.field-list th {
|
||||
padding: 0 0.8em 0 0;
|
||||
}
|
||||
|
||||
table.field-list td {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
table.field-list p {
|
||||
margin-bottom: 0.8em;
|
||||
}
|
||||
|
||||
/* Cloned from
|
||||
* https://github.com/sphinx-doc/sphinx/commit/ef60dbfce09286b20b7385333d63a60321784e68
|
||||
*/
|
||||
.field-name {
|
||||
-moz-hyphens: manual;
|
||||
-ms-hyphens: manual;
|
||||
-webkit-hyphens: manual;
|
||||
hyphens: manual;
|
||||
}
|
||||
|
||||
table.footnote td.label {
|
||||
width: .1px;
|
||||
padding: 0.3em 0 0.3em 0.5em;
|
||||
}
|
||||
|
||||
table.footnote td {
|
||||
padding: 0.3em 0.5em;
|
||||
}
|
||||
|
||||
dl {
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
margin-top: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
dl dd {
|
||||
margin-left: 30px;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
margin: 0 0 0 30px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
ul, ol {
|
||||
/* Matches the 30px from the narrow-screen "li > ul" selector below */
|
||||
margin: 10px 0 10px 30px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
pre {
|
||||
background: unset;
|
||||
padding: 7px 30px;
|
||||
margin: 15px 0px;
|
||||
line-height: 1.3em;
|
||||
}
|
||||
|
||||
div.viewcode-block:target {
|
||||
background: #ffd;
|
||||
}
|
||||
|
||||
dl pre, blockquote pre, li pre {
|
||||
margin-left: 0;
|
||||
padding-left: 30px;
|
||||
}
|
||||
|
||||
tt, code {
|
||||
background-color: #ecf0f3;
|
||||
color: #222;
|
||||
/* padding: 1px 2px; */
|
||||
}
|
||||
|
||||
tt.xref, code.xref, a tt {
|
||||
background-color: #FBFBFB;
|
||||
border-bottom: 1px solid #fff;
|
||||
}
|
||||
|
||||
a.reference {
|
||||
text-decoration: none;
|
||||
border-bottom: 1px dotted #004B6B;
|
||||
}
|
||||
|
||||
a.reference:hover {
|
||||
border-bottom: 1px solid #6D4100;
|
||||
}
|
||||
|
||||
/* Don't put an underline on images */
|
||||
a.image-reference, a.image-reference:hover {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
a.footnote-reference {
|
||||
text-decoration: none;
|
||||
font-size: 0.7em;
|
||||
vertical-align: top;
|
||||
border-bottom: 1px dotted #004B6B;
|
||||
}
|
||||
|
||||
a.footnote-reference:hover {
|
||||
border-bottom: 1px solid #6D4100;
|
||||
}
|
||||
|
||||
a:hover tt, a:hover code {
|
||||
background: #EEE;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 940px) {
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 20px 30px;
|
||||
}
|
||||
|
||||
div.documentwrapper {
|
||||
float: none;
|
||||
background: #fff;
|
||||
margin-left: 0;
|
||||
margin-top: 0;
|
||||
margin-right: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
div.sphinxsidebar {
|
||||
display: block;
|
||||
float: none;
|
||||
width: unset;
|
||||
margin: 50px -30px -20px -30px;
|
||||
padding: 10px 20px;
|
||||
background: #333;
|
||||
color: #FFF;
|
||||
}
|
||||
|
||||
div.sphinxsidebar h3, div.sphinxsidebar h4, div.sphinxsidebar p,
|
||||
div.sphinxsidebar h3 a {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
div.sphinxsidebar a {
|
||||
color: #AAA;
|
||||
}
|
||||
|
||||
div.sphinxsidebar p.logo {
|
||||
display: none;
|
||||
}
|
||||
|
||||
div.document {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
div.footer {
|
||||
display: none;
|
||||
}
|
||||
|
||||
div.bodywrapper {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
div.body {
|
||||
min-height: 0;
|
||||
min-width: auto; /* fixes width on small screens, breaks .hll */
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.hll {
|
||||
/* "fixes" the breakage */
|
||||
width: max-content;
|
||||
}
|
||||
|
||||
.rtd_doc_footer {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.document {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.footer {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.github {
|
||||
display: none;
|
||||
}
|
||||
|
||||
ul {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
li > ul {
|
||||
/* Matches the 30px from the "ul, ol" selector above */
|
||||
margin-left: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* misc. */
|
||||
|
||||
.revsys-inline {
|
||||
display: none!important;
|
||||
}
|
||||
|
||||
/* Hide ugly table cell borders in ..bibliography:: directive output */
|
||||
table.docutils.citation, table.docutils.citation td, table.docutils.citation th {
|
||||
border: none;
|
||||
/* Below needed in some edge cases; if not applied, bottom shadows appear */
|
||||
-moz-box-shadow: none;
|
||||
-webkit-box-shadow: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
|
||||
/* relbar */
|
||||
|
||||
.related {
|
||||
line-height: 30px;
|
||||
width: 100%;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.related.top {
|
||||
border-bottom: 1px solid #EEE;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.related.bottom {
|
||||
border-top: 1px solid #EEE;
|
||||
}
|
||||
|
||||
.related ul {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.related li {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
nav#rellinks {
|
||||
float: right;
|
||||
}
|
||||
|
||||
nav#rellinks li+li:before {
|
||||
content: "|";
|
||||
}
|
||||
|
||||
nav#breadcrumbs li+li:before {
|
||||
content: "\00BB";
|
||||
}
|
||||
|
||||
/* Hide certain items when printing */
|
||||
@media print {
|
||||
div.related {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
img.github {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
border: 0;
|
||||
right: 0;
|
||||
}
|
||||
@@ -0,0 +1,906 @@
|
||||
/*
|
||||
* Sphinx stylesheet -- basic theme.
|
||||
*/
|
||||
|
||||
/* -- main layout ----------------------------------------------------------- */
|
||||
|
||||
div.clearer {
|
||||
clear: both;
|
||||
}
|
||||
|
||||
div.section::after {
|
||||
display: block;
|
||||
content: '';
|
||||
clear: left;
|
||||
}
|
||||
|
||||
/* -- relbar ---------------------------------------------------------------- */
|
||||
|
||||
div.related {
|
||||
width: 100%;
|
||||
font-size: 90%;
|
||||
}
|
||||
|
||||
div.related h3 {
|
||||
display: none;
|
||||
}
|
||||
|
||||
div.related ul {
|
||||
margin: 0;
|
||||
padding: 0 0 0 10px;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
div.related li {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
div.related li.right {
|
||||
float: right;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
/* -- sidebar --------------------------------------------------------------- */
|
||||
|
||||
div.sphinxsidebarwrapper {
|
||||
padding: 10px 5px 0 10px;
|
||||
}
|
||||
|
||||
div.sphinxsidebar {
|
||||
float: left;
|
||||
width: 230px;
|
||||
margin-left: -100%;
|
||||
font-size: 90%;
|
||||
word-wrap: break-word;
|
||||
overflow-wrap : break-word;
|
||||
}
|
||||
|
||||
div.sphinxsidebar ul {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
div.sphinxsidebar ul ul,
|
||||
div.sphinxsidebar ul.want-points {
|
||||
margin-left: 20px;
|
||||
list-style: square;
|
||||
}
|
||||
|
||||
div.sphinxsidebar ul ul {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
div.sphinxsidebar form {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
div.sphinxsidebar input {
|
||||
border: 1px solid #98dbcc;
|
||||
font-family: sans-serif;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
div.sphinxsidebar #searchbox form.search {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
div.sphinxsidebar #searchbox input[type="text"] {
|
||||
float: left;
|
||||
width: 80%;
|
||||
padding: 0.25em;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
div.sphinxsidebar #searchbox input[type="submit"] {
|
||||
float: left;
|
||||
width: 20%;
|
||||
border-left: none;
|
||||
padding: 0.25em;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
|
||||
img {
|
||||
border: 0;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
/* -- search page ----------------------------------------------------------- */
|
||||
|
||||
ul.search {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
ul.search li {
|
||||
padding: 5px 0;
|
||||
}
|
||||
|
||||
ul.search li a {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
ul.search li p.context {
|
||||
color: #888;
|
||||
margin: 2px 0 0 30px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
ul.keywordmatches li.goodmatch a {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* -- index page ------------------------------------------------------------ */
|
||||
|
||||
table.contentstable {
|
||||
width: 90%;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
table.contentstable p.biglink {
|
||||
line-height: 150%;
|
||||
}
|
||||
|
||||
a.biglink {
|
||||
font-size: 1.3em;
|
||||
}
|
||||
|
||||
span.linkdescr {
|
||||
font-style: italic;
|
||||
padding-top: 5px;
|
||||
font-size: 90%;
|
||||
}
|
||||
|
||||
/* -- general index --------------------------------------------------------- */
|
||||
|
||||
table.indextable {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
table.indextable td {
|
||||
text-align: left;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
table.indextable ul {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
list-style-type: none;
|
||||
}
|
||||
|
||||
table.indextable > tbody > tr > td > ul {
|
||||
padding-left: 0em;
|
||||
}
|
||||
|
||||
table.indextable tr.pcap {
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
table.indextable tr.cap {
|
||||
margin-top: 10px;
|
||||
background-color: #f2f2f2;
|
||||
}
|
||||
|
||||
img.toggler {
|
||||
margin-right: 3px;
|
||||
margin-top: 3px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
div.modindex-jumpbox {
|
||||
border-top: 1px solid #ddd;
|
||||
border-bottom: 1px solid #ddd;
|
||||
margin: 1em 0 1em 0;
|
||||
padding: 0.4em;
|
||||
}
|
||||
|
||||
div.genindex-jumpbox {
|
||||
border-top: 1px solid #ddd;
|
||||
border-bottom: 1px solid #ddd;
|
||||
margin: 1em 0 1em 0;
|
||||
padding: 0.4em;
|
||||
}
|
||||
|
||||
/* -- domain module index --------------------------------------------------- */
|
||||
|
||||
table.modindextable td {
|
||||
padding: 2px;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
/* -- general body styles --------------------------------------------------- */
|
||||
|
||||
div.body {
|
||||
min-width: inherit;
|
||||
max-width: 800px;
|
||||
}
|
||||
|
||||
div.body p, div.body dd, div.body li, div.body blockquote {
|
||||
-moz-hyphens: auto;
|
||||
-ms-hyphens: auto;
|
||||
-webkit-hyphens: auto;
|
||||
hyphens: auto;
|
||||
}
|
||||
|
||||
a.headerlink {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
a:visited {
|
||||
color: #551A8B;
|
||||
}
|
||||
|
||||
h1:hover > a.headerlink,
|
||||
h2:hover > a.headerlink,
|
||||
h3:hover > a.headerlink,
|
||||
h4:hover > a.headerlink,
|
||||
h5:hover > a.headerlink,
|
||||
h6:hover > a.headerlink,
|
||||
dt:hover > a.headerlink,
|
||||
caption:hover > a.headerlink,
|
||||
p.caption:hover > a.headerlink,
|
||||
div.code-block-caption:hover > a.headerlink {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
div.body p.caption {
|
||||
text-align: inherit;
|
||||
}
|
||||
|
||||
div.body td {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.first {
|
||||
margin-top: 0 !important;
|
||||
}
|
||||
|
||||
p.rubric {
|
||||
margin-top: 30px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
img.align-left, figure.align-left, .figure.align-left, object.align-left {
|
||||
clear: left;
|
||||
float: left;
|
||||
margin-right: 1em;
|
||||
}
|
||||
|
||||
img.align-right, figure.align-right, .figure.align-right, object.align-right {
|
||||
clear: right;
|
||||
float: right;
|
||||
margin-left: 1em;
|
||||
}
|
||||
|
||||
img.align-center, figure.align-center, .figure.align-center, object.align-center {
|
||||
display: block;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
img.align-default, figure.align-default, .figure.align-default {
|
||||
display: block;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.align-left {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.align-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.align-default {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.align-right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
/* -- sidebars -------------------------------------------------------------- */
|
||||
|
||||
div.sidebar,
|
||||
aside.sidebar {
|
||||
margin: 0 0 0.5em 1em;
|
||||
border: 1px solid #ddb;
|
||||
padding: 7px;
|
||||
background-color: #ffe;
|
||||
width: 40%;
|
||||
float: right;
|
||||
clear: right;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
p.sidebar-title {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
nav.contents,
|
||||
aside.topic,
|
||||
div.admonition, div.topic, blockquote {
|
||||
clear: left;
|
||||
}
|
||||
|
||||
/* -- topics ---------------------------------------------------------------- */
|
||||
|
||||
nav.contents,
|
||||
aside.topic,
|
||||
div.topic {
|
||||
border: 1px solid #ccc;
|
||||
padding: 7px;
|
||||
margin: 10px 0 10px 0;
|
||||
}
|
||||
|
||||
p.topic-title {
|
||||
font-size: 1.1em;
|
||||
font-weight: bold;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
/* -- admonitions ----------------------------------------------------------- */
|
||||
|
||||
div.admonition {
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
padding: 7px;
|
||||
}
|
||||
|
||||
div.admonition dt {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
p.admonition-title {
|
||||
margin: 0px 10px 5px 0px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
div.body p.centered {
|
||||
text-align: center;
|
||||
margin-top: 25px;
|
||||
}
|
||||
|
||||
/* -- content of sidebars/topics/admonitions -------------------------------- */
|
||||
|
||||
div.sidebar > :last-child,
|
||||
aside.sidebar > :last-child,
|
||||
nav.contents > :last-child,
|
||||
aside.topic > :last-child,
|
||||
div.topic > :last-child,
|
||||
div.admonition > :last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
div.sidebar::after,
|
||||
aside.sidebar::after,
|
||||
nav.contents::after,
|
||||
aside.topic::after,
|
||||
div.topic::after,
|
||||
div.admonition::after,
|
||||
blockquote::after {
|
||||
display: block;
|
||||
content: '';
|
||||
clear: both;
|
||||
}
|
||||
|
||||
/* -- tables ---------------------------------------------------------------- */
|
||||
|
||||
table.docutils {
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
border: 0;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
table.align-center {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
table.align-default {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
table caption span.caption-number {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
table caption span.caption-text {
|
||||
}
|
||||
|
||||
table.docutils td, table.docutils th {
|
||||
padding: 1px 8px 1px 5px;
|
||||
border-top: 0;
|
||||
border-left: 0;
|
||||
border-right: 0;
|
||||
border-bottom: 1px solid #aaa;
|
||||
}
|
||||
|
||||
th {
|
||||
text-align: left;
|
||||
padding-right: 5px;
|
||||
}
|
||||
|
||||
table.citation {
|
||||
border-left: solid 1px gray;
|
||||
margin-left: 1px;
|
||||
}
|
||||
|
||||
table.citation td {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
th > :first-child,
|
||||
td > :first-child {
|
||||
margin-top: 0px;
|
||||
}
|
||||
|
||||
th > :last-child,
|
||||
td > :last-child {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
/* -- figures --------------------------------------------------------------- */
|
||||
|
||||
div.figure, figure {
|
||||
margin: 0.5em;
|
||||
padding: 0.5em;
|
||||
}
|
||||
|
||||
div.figure p.caption, figcaption {
|
||||
padding: 0.3em;
|
||||
}
|
||||
|
||||
div.figure p.caption span.caption-number,
|
||||
figcaption span.caption-number {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
div.figure p.caption span.caption-text,
|
||||
figcaption span.caption-text {
|
||||
}
|
||||
|
||||
/* -- field list styles ----------------------------------------------------- */
|
||||
|
||||
table.field-list td, table.field-list th {
|
||||
border: 0 !important;
|
||||
}
|
||||
|
||||
.field-list ul {
|
||||
margin: 0;
|
||||
padding-left: 1em;
|
||||
}
|
||||
|
||||
.field-list p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.field-name {
|
||||
-moz-hyphens: manual;
|
||||
-ms-hyphens: manual;
|
||||
-webkit-hyphens: manual;
|
||||
hyphens: manual;
|
||||
}
|
||||
|
||||
/* -- hlist styles ---------------------------------------------------------- */
|
||||
|
||||
table.hlist {
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
table.hlist td {
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
/* -- object description styles --------------------------------------------- */
|
||||
|
||||
.sig {
|
||||
font-family: 'Consolas', 'Menlo', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', monospace;
|
||||
}
|
||||
|
||||
.sig-name, code.descname {
|
||||
background-color: transparent;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.sig-name {
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
code.descname {
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
.sig-prename, code.descclassname {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.optional {
|
||||
font-size: 1.3em;
|
||||
}
|
||||
|
||||
.sig-paren {
|
||||
font-size: larger;
|
||||
}
|
||||
|
||||
.sig-param.n {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* C++ specific styling */
|
||||
|
||||
.sig-inline.c-texpr,
|
||||
.sig-inline.cpp-texpr {
|
||||
font-family: unset;
|
||||
}
|
||||
|
||||
.sig.c .k, .sig.c .kt,
|
||||
.sig.cpp .k, .sig.cpp .kt {
|
||||
color: #0033B3;
|
||||
}
|
||||
|
||||
.sig.c .m,
|
||||
.sig.cpp .m {
|
||||
color: #1750EB;
|
||||
}
|
||||
|
||||
.sig.c .s, .sig.c .sc,
|
||||
.sig.cpp .s, .sig.cpp .sc {
|
||||
color: #067D17;
|
||||
}
|
||||
|
||||
|
||||
/* -- other body styles ----------------------------------------------------- */
|
||||
|
||||
ol.arabic {
|
||||
list-style: decimal;
|
||||
}
|
||||
|
||||
ol.loweralpha {
|
||||
list-style: lower-alpha;
|
||||
}
|
||||
|
||||
ol.upperalpha {
|
||||
list-style: upper-alpha;
|
||||
}
|
||||
|
||||
ol.lowerroman {
|
||||
list-style: lower-roman;
|
||||
}
|
||||
|
||||
ol.upperroman {
|
||||
list-style: upper-roman;
|
||||
}
|
||||
|
||||
:not(li) > ol > li:first-child > :first-child,
|
||||
:not(li) > ul > li:first-child > :first-child {
|
||||
margin-top: 0px;
|
||||
}
|
||||
|
||||
:not(li) > ol > li:last-child > :last-child,
|
||||
:not(li) > ul > li:last-child > :last-child {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
ol.simple ol p,
|
||||
ol.simple ul p,
|
||||
ul.simple ol p,
|
||||
ul.simple ul p {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
ol.simple > li:not(:first-child) > p,
|
||||
ul.simple > li:not(:first-child) > p {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
ol.simple p,
|
||||
ul.simple p {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
aside.footnote > span,
|
||||
div.citation > span {
|
||||
float: left;
|
||||
}
|
||||
aside.footnote > span:last-of-type,
|
||||
div.citation > span:last-of-type {
|
||||
padding-right: 0.5em;
|
||||
}
|
||||
aside.footnote > p {
|
||||
margin-left: 2em;
|
||||
}
|
||||
div.citation > p {
|
||||
margin-left: 4em;
|
||||
}
|
||||
aside.footnote > p:last-of-type,
|
||||
div.citation > p:last-of-type {
|
||||
margin-bottom: 0em;
|
||||
}
|
||||
aside.footnote > p:last-of-type:after,
|
||||
div.citation > p:last-of-type:after {
|
||||
content: "";
|
||||
clear: both;
|
||||
}
|
||||
|
||||
dl.field-list {
|
||||
display: grid;
|
||||
grid-template-columns: fit-content(30%) auto;
|
||||
}
|
||||
|
||||
dl.field-list > dt {
|
||||
font-weight: bold;
|
||||
word-break: break-word;
|
||||
padding-left: 0.5em;
|
||||
padding-right: 5px;
|
||||
}
|
||||
|
||||
dl.field-list > dd {
|
||||
padding-left: 0.5em;
|
||||
margin-top: 0em;
|
||||
margin-left: 0em;
|
||||
margin-bottom: 0em;
|
||||
}
|
||||
|
||||
dl {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
dd > :first-child {
|
||||
margin-top: 0px;
|
||||
}
|
||||
|
||||
dd ul, dd table {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
dd {
|
||||
margin-top: 3px;
|
||||
margin-bottom: 10px;
|
||||
margin-left: 30px;
|
||||
}
|
||||
|
||||
.sig dd {
|
||||
margin-top: 0px;
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
.sig dl {
|
||||
margin-top: 0px;
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
dl > dd:last-child,
|
||||
dl > dd:last-child > :last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
dt:target, span.highlighted {
|
||||
background-color: #fbe54e;
|
||||
}
|
||||
|
||||
rect.highlighted {
|
||||
fill: #fbe54e;
|
||||
}
|
||||
|
||||
dl.glossary dt {
|
||||
font-weight: bold;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
.versionmodified {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.system-message {
|
||||
background-color: #fda;
|
||||
padding: 5px;
|
||||
border: 3px solid red;
|
||||
}
|
||||
|
||||
.footnote:target {
|
||||
background-color: #ffa;
|
||||
}
|
||||
|
||||
.line-block {
|
||||
display: block;
|
||||
margin-top: 1em;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.line-block .line-block {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
margin-left: 1.5em;
|
||||
}
|
||||
|
||||
.guilabel, .menuselection {
|
||||
font-family: sans-serif;
|
||||
}
|
||||
|
||||
.accelerator {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.classifier {
|
||||
font-style: oblique;
|
||||
}
|
||||
|
||||
.classifier:before {
|
||||
font-style: normal;
|
||||
margin: 0 0.5em;
|
||||
content: ":";
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
abbr, acronym {
|
||||
border-bottom: dotted 1px;
|
||||
cursor: help;
|
||||
}
|
||||
|
||||
/* -- code displays --------------------------------------------------------- */
|
||||
|
||||
pre {
|
||||
overflow: auto;
|
||||
overflow-y: hidden; /* fixes display issues on Chrome browsers */
|
||||
}
|
||||
|
||||
pre, div[class*="highlight-"] {
|
||||
clear: both;
|
||||
}
|
||||
|
||||
span.pre {
|
||||
-moz-hyphens: none;
|
||||
-ms-hyphens: none;
|
||||
-webkit-hyphens: none;
|
||||
hyphens: none;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
div[class*="highlight-"] {
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
td.linenos pre {
|
||||
border: 0;
|
||||
background-color: transparent;
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
table.highlighttable {
|
||||
display: block;
|
||||
}
|
||||
|
||||
table.highlighttable tbody {
|
||||
display: block;
|
||||
}
|
||||
|
||||
table.highlighttable tr {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
table.highlighttable td {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
table.highlighttable td.linenos {
|
||||
padding-right: 0.5em;
|
||||
}
|
||||
|
||||
table.highlighttable td.code {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.highlight .hll {
|
||||
display: block;
|
||||
}
|
||||
|
||||
div.highlight pre,
|
||||
table.highlighttable pre {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
div.code-block-caption + div {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
div.code-block-caption {
|
||||
margin-top: 1em;
|
||||
padding: 2px 5px;
|
||||
font-size: small;
|
||||
}
|
||||
|
||||
div.code-block-caption code {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
table.highlighttable td.linenos,
|
||||
span.linenos,
|
||||
div.highlight span.gp { /* gp: Generic.Prompt */
|
||||
user-select: none;
|
||||
-webkit-user-select: text; /* Safari fallback only */
|
||||
-webkit-user-select: none; /* Chrome/Safari */
|
||||
-moz-user-select: none; /* Firefox */
|
||||
-ms-user-select: none; /* IE10+ */
|
||||
}
|
||||
|
||||
div.code-block-caption span.caption-number {
|
||||
padding: 0.1em 0.3em;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
div.code-block-caption span.caption-text {
|
||||
}
|
||||
|
||||
div.literal-block-wrapper {
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
code.xref, a code {
|
||||
background-color: transparent;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
h1 code, h2 code, h3 code, h4 code, h5 code, h6 code {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.viewcode-link {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.viewcode-back {
|
||||
float: right;
|
||||
font-family: sans-serif;
|
||||
}
|
||||
|
||||
div.viewcode-block:target {
|
||||
margin: -1px -10px;
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
/* -- math display ---------------------------------------------------------- */
|
||||
|
||||
img.math {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
div.body div.math p {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
span.eqno {
|
||||
float: right;
|
||||
}
|
||||
|
||||
span.eqno a.headerlink {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
div.math:hover a.headerlink {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
/* -- printout stylesheet --------------------------------------------------- */
|
||||
|
||||
@media print {
|
||||
div.document,
|
||||
div.documentwrapper,
|
||||
div.bodywrapper {
|
||||
margin: 0 !important;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
div.sphinxsidebar,
|
||||
div.related,
|
||||
div.footer,
|
||||
#top-link {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-check" width="44" height="44" viewBox="0 0 24 24" stroke-width="2" stroke="#22863a" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M5 12l5 5l10 -10" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 313 B |
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-copy" width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="#000000" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<rect x="8" y="8" width="12" height="12" rx="2" />
|
||||
<path d="M16 8v-2a2 2 0 0 0 -2 -2h-8a2 2 0 0 0 -2 2v8a2 2 0 0 0 2 2h2" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 411 B |
@@ -0,0 +1,94 @@
|
||||
/* Copy buttons */
|
||||
button.copybtn {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
top: .3em;
|
||||
right: .3em;
|
||||
width: 1.7em;
|
||||
height: 1.7em;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s, border .3s, background-color .3s;
|
||||
user-select: none;
|
||||
padding: 0;
|
||||
border: none;
|
||||
outline: none;
|
||||
border-radius: 0.4em;
|
||||
/* The colors that GitHub uses */
|
||||
border: #1b1f2426 1px solid;
|
||||
background-color: #f6f8fa;
|
||||
color: #57606a;
|
||||
}
|
||||
|
||||
button.copybtn.success {
|
||||
border-color: #22863a;
|
||||
color: #22863a;
|
||||
}
|
||||
|
||||
button.copybtn svg {
|
||||
stroke: currentColor;
|
||||
width: 1.5em;
|
||||
height: 1.5em;
|
||||
padding: 0.1em;
|
||||
}
|
||||
|
||||
div.highlight {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Show the copybutton */
|
||||
.highlight:hover button.copybtn, button.copybtn.success {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.highlight button.copybtn:hover {
|
||||
background-color: rgb(235, 235, 235);
|
||||
}
|
||||
|
||||
.highlight button.copybtn:active {
|
||||
background-color: rgb(187, 187, 187);
|
||||
}
|
||||
|
||||
/**
|
||||
* A minimal CSS-only tooltip copied from:
|
||||
* https://codepen.io/mildrenben/pen/rVBrpK
|
||||
*
|
||||
* To use, write HTML like the following:
|
||||
*
|
||||
* <p class="o-tooltip--left" data-tooltip="Hey">Short</p>
|
||||
*/
|
||||
.o-tooltip--left {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.o-tooltip--left:after {
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
position: absolute;
|
||||
content: attr(data-tooltip);
|
||||
padding: .2em;
|
||||
font-size: .8em;
|
||||
left: -.2em;
|
||||
background: grey;
|
||||
color: white;
|
||||
white-space: nowrap;
|
||||
z-index: 2;
|
||||
border-radius: 2px;
|
||||
transform: translateX(-102%) translateY(0);
|
||||
transition: opacity 0.2s cubic-bezier(0.64, 0.09, 0.08, 1), transform 0.2s cubic-bezier(0.64, 0.09, 0.08, 1);
|
||||
}
|
||||
|
||||
.o-tooltip--left:hover:after {
|
||||
display: block;
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
transform: translateX(-100%) translateY(0);
|
||||
transition: opacity 0.2s cubic-bezier(0.64, 0.09, 0.08, 1), transform 0.2s cubic-bezier(0.64, 0.09, 0.08, 1);
|
||||
transition-delay: .5s;
|
||||
}
|
||||
|
||||
/* By default the copy button shouldn't show up when printing a page */
|
||||
@media print {
|
||||
button.copybtn {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,248 @@
|
||||
// Localization support
|
||||
const messages = {
|
||||
'en': {
|
||||
'copy': 'Copy',
|
||||
'copy_to_clipboard': 'Copy to clipboard',
|
||||
'copy_success': 'Copied!',
|
||||
'copy_failure': 'Failed to copy',
|
||||
},
|
||||
'es' : {
|
||||
'copy': 'Copiar',
|
||||
'copy_to_clipboard': 'Copiar al portapapeles',
|
||||
'copy_success': '¡Copiado!',
|
||||
'copy_failure': 'Error al copiar',
|
||||
},
|
||||
'de' : {
|
||||
'copy': 'Kopieren',
|
||||
'copy_to_clipboard': 'In die Zwischenablage kopieren',
|
||||
'copy_success': 'Kopiert!',
|
||||
'copy_failure': 'Fehler beim Kopieren',
|
||||
},
|
||||
'fr' : {
|
||||
'copy': 'Copier',
|
||||
'copy_to_clipboard': 'Copier dans le presse-papier',
|
||||
'copy_success': 'Copié !',
|
||||
'copy_failure': 'Échec de la copie',
|
||||
},
|
||||
'ru': {
|
||||
'copy': 'Скопировать',
|
||||
'copy_to_clipboard': 'Скопировать в буфер',
|
||||
'copy_success': 'Скопировано!',
|
||||
'copy_failure': 'Не удалось скопировать',
|
||||
},
|
||||
'zh-CN': {
|
||||
'copy': '复制',
|
||||
'copy_to_clipboard': '复制到剪贴板',
|
||||
'copy_success': '复制成功!',
|
||||
'copy_failure': '复制失败',
|
||||
},
|
||||
'it' : {
|
||||
'copy': 'Copiare',
|
||||
'copy_to_clipboard': 'Copiato negli appunti',
|
||||
'copy_success': 'Copiato!',
|
||||
'copy_failure': 'Errore durante la copia',
|
||||
}
|
||||
}
|
||||
|
||||
let locale = 'en'
|
||||
if( document.documentElement.lang !== undefined
|
||||
&& messages[document.documentElement.lang] !== undefined ) {
|
||||
locale = document.documentElement.lang
|
||||
}
|
||||
|
||||
let doc_url_root = DOCUMENTATION_OPTIONS.URL_ROOT;
|
||||
if (doc_url_root == '#') {
|
||||
doc_url_root = '';
|
||||
}
|
||||
|
||||
/**
|
||||
* SVG files for our copy buttons
|
||||
*/
|
||||
let iconCheck = `<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-check" width="44" height="44" viewBox="0 0 24 24" stroke-width="2" stroke="#22863a" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<title>${messages[locale]['copy_success']}</title>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M5 12l5 5l10 -10" />
|
||||
</svg>`
|
||||
|
||||
// If the user specified their own SVG use that, otherwise use the default
|
||||
let iconCopy = ``;
|
||||
if (!iconCopy) {
|
||||
iconCopy = `<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-copy" width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="#000000" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<title>${messages[locale]['copy_to_clipboard']}</title>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<rect x="8" y="8" width="12" height="12" rx="2" />
|
||||
<path d="M16 8v-2a2 2 0 0 0 -2 -2h-8a2 2 0 0 0 -2 2v8a2 2 0 0 0 2 2h2" />
|
||||
</svg>`
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up copy/paste for code blocks
|
||||
*/
|
||||
|
||||
const runWhenDOMLoaded = cb => {
|
||||
if (document.readyState != 'loading') {
|
||||
cb()
|
||||
} else if (document.addEventListener) {
|
||||
document.addEventListener('DOMContentLoaded', cb)
|
||||
} else {
|
||||
document.attachEvent('onreadystatechange', function() {
|
||||
if (document.readyState == 'complete') cb()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const codeCellId = index => `codecell${index}`
|
||||
|
||||
// Clears selected text since ClipboardJS will select the text when copying
|
||||
const clearSelection = () => {
|
||||
if (window.getSelection) {
|
||||
window.getSelection().removeAllRanges()
|
||||
} else if (document.selection) {
|
||||
document.selection.empty()
|
||||
}
|
||||
}
|
||||
|
||||
// Changes tooltip text for a moment, then changes it back
|
||||
// We want the timeout of our `success` class to be a bit shorter than the
|
||||
// tooltip and icon change, so that we can hide the icon before changing back.
|
||||
var timeoutIcon = 2000;
|
||||
var timeoutSuccessClass = 1500;
|
||||
|
||||
const temporarilyChangeTooltip = (el, oldText, newText) => {
|
||||
el.setAttribute('data-tooltip', newText)
|
||||
el.classList.add('success')
|
||||
// Remove success a little bit sooner than we change the tooltip
|
||||
// So that we can use CSS to hide the copybutton first
|
||||
setTimeout(() => el.classList.remove('success'), timeoutSuccessClass)
|
||||
setTimeout(() => el.setAttribute('data-tooltip', oldText), timeoutIcon)
|
||||
}
|
||||
|
||||
// Changes the copy button icon for two seconds, then changes it back
|
||||
const temporarilyChangeIcon = (el) => {
|
||||
el.innerHTML = iconCheck;
|
||||
setTimeout(() => {el.innerHTML = iconCopy}, timeoutIcon)
|
||||
}
|
||||
|
||||
const addCopyButtonToCodeCells = () => {
|
||||
// If ClipboardJS hasn't loaded, wait a bit and try again. This
|
||||
// happens because we load ClipboardJS asynchronously.
|
||||
if (window.ClipboardJS === undefined) {
|
||||
setTimeout(addCopyButtonToCodeCells, 250)
|
||||
return
|
||||
}
|
||||
|
||||
// Add copybuttons to all of our code cells
|
||||
const COPYBUTTON_SELECTOR = 'div.highlight pre';
|
||||
const codeCells = document.querySelectorAll(COPYBUTTON_SELECTOR)
|
||||
codeCells.forEach((codeCell, index) => {
|
||||
const id = codeCellId(index)
|
||||
codeCell.setAttribute('id', id)
|
||||
|
||||
const clipboardButton = id =>
|
||||
`<button class="copybtn o-tooltip--left" data-tooltip="${messages[locale]['copy']}" data-clipboard-target="#${id}">
|
||||
${iconCopy}
|
||||
</button>`
|
||||
codeCell.insertAdjacentHTML('afterend', clipboardButton(id))
|
||||
})
|
||||
|
||||
function escapeRegExp(string) {
|
||||
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes excluded text from a Node.
|
||||
*
|
||||
* @param {Node} target Node to filter.
|
||||
* @param {string} exclude CSS selector of nodes to exclude.
|
||||
* @returns {DOMString} Text from `target` with text removed.
|
||||
*/
|
||||
function filterText(target, exclude) {
|
||||
const clone = target.cloneNode(true); // clone as to not modify the live DOM
|
||||
if (exclude) {
|
||||
// remove excluded nodes
|
||||
clone.querySelectorAll(exclude).forEach(node => node.remove());
|
||||
}
|
||||
return clone.innerText;
|
||||
}
|
||||
|
||||
// Callback when a copy button is clicked. Will be passed the node that was clicked
|
||||
// should then grab the text and replace pieces of text that shouldn't be used in output
|
||||
function formatCopyText(textContent, copybuttonPromptText, isRegexp = false, onlyCopyPromptLines = true, removePrompts = true, copyEmptyLines = true, lineContinuationChar = "", hereDocDelim = "") {
|
||||
var regexp;
|
||||
var match;
|
||||
|
||||
// Do we check for line continuation characters and "HERE-documents"?
|
||||
var useLineCont = !!lineContinuationChar
|
||||
var useHereDoc = !!hereDocDelim
|
||||
|
||||
// create regexp to capture prompt and remaining line
|
||||
if (isRegexp) {
|
||||
regexp = new RegExp('^(' + copybuttonPromptText + ')(.*)')
|
||||
} else {
|
||||
regexp = new RegExp('^(' + escapeRegExp(copybuttonPromptText) + ')(.*)')
|
||||
}
|
||||
|
||||
const outputLines = [];
|
||||
var promptFound = false;
|
||||
var gotLineCont = false;
|
||||
var gotHereDoc = false;
|
||||
const lineGotPrompt = [];
|
||||
for (const line of textContent.split('\n')) {
|
||||
match = line.match(regexp)
|
||||
if (match || gotLineCont || gotHereDoc) {
|
||||
promptFound = regexp.test(line)
|
||||
lineGotPrompt.push(promptFound)
|
||||
if (removePrompts && promptFound) {
|
||||
outputLines.push(match[2])
|
||||
} else {
|
||||
outputLines.push(line)
|
||||
}
|
||||
gotLineCont = line.endsWith(lineContinuationChar) & useLineCont
|
||||
if (line.includes(hereDocDelim) & useHereDoc)
|
||||
gotHereDoc = !gotHereDoc
|
||||
} else if (!onlyCopyPromptLines) {
|
||||
outputLines.push(line)
|
||||
} else if (copyEmptyLines && line.trim() === '') {
|
||||
outputLines.push(line)
|
||||
}
|
||||
}
|
||||
|
||||
// If no lines with the prompt were found then just use original lines
|
||||
if (lineGotPrompt.some(v => v === true)) {
|
||||
textContent = outputLines.join('\n');
|
||||
}
|
||||
|
||||
// Remove a trailing newline to avoid auto-running when pasting
|
||||
if (textContent.endsWith("\n")) {
|
||||
textContent = textContent.slice(0, -1)
|
||||
}
|
||||
return textContent
|
||||
}
|
||||
|
||||
|
||||
var copyTargetText = (trigger) => {
|
||||
var target = document.querySelector(trigger.attributes['data-clipboard-target'].value);
|
||||
|
||||
// get filtered text
|
||||
let exclude = '.linenos';
|
||||
|
||||
let text = filterText(target, exclude);
|
||||
return formatCopyText(text, '>>> |\\.\\.\\. |\\$ ', true, true, true, true, '', '')
|
||||
}
|
||||
|
||||
// Initialize with a callback so we can modify the text before copy
|
||||
const clipboard = new ClipboardJS('.copybtn', {text: copyTargetText})
|
||||
|
||||
// Update UI with error/success messages
|
||||
clipboard.on('success', event => {
|
||||
clearSelection()
|
||||
temporarilyChangeTooltip(event.trigger, messages[locale]['copy'], messages[locale]['copy_success'])
|
||||
temporarilyChangeIcon(event.trigger)
|
||||
})
|
||||
|
||||
clipboard.on('error', event => {
|
||||
temporarilyChangeTooltip(event.trigger, messages[locale]['copy'], messages[locale]['copy_failure'])
|
||||
})
|
||||
}
|
||||
|
||||
runWhenDOMLoaded(addCopyButtonToCodeCells)
|
||||
@@ -0,0 +1,73 @@
|
||||
function escapeRegExp(string) {
|
||||
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes excluded text from a Node.
|
||||
*
|
||||
* @param {Node} target Node to filter.
|
||||
* @param {string} exclude CSS selector of nodes to exclude.
|
||||
* @returns {DOMString} Text from `target` with text removed.
|
||||
*/
|
||||
export function filterText(target, exclude) {
|
||||
const clone = target.cloneNode(true); // clone as to not modify the live DOM
|
||||
if (exclude) {
|
||||
// remove excluded nodes
|
||||
clone.querySelectorAll(exclude).forEach(node => node.remove());
|
||||
}
|
||||
return clone.innerText;
|
||||
}
|
||||
|
||||
// Callback when a copy button is clicked. Will be passed the node that was clicked
|
||||
// should then grab the text and replace pieces of text that shouldn't be used in output
|
||||
export function formatCopyText(textContent, copybuttonPromptText, isRegexp = false, onlyCopyPromptLines = true, removePrompts = true, copyEmptyLines = true, lineContinuationChar = "", hereDocDelim = "") {
|
||||
var regexp;
|
||||
var match;
|
||||
|
||||
// Do we check for line continuation characters and "HERE-documents"?
|
||||
var useLineCont = !!lineContinuationChar
|
||||
var useHereDoc = !!hereDocDelim
|
||||
|
||||
// create regexp to capture prompt and remaining line
|
||||
if (isRegexp) {
|
||||
regexp = new RegExp('^(' + copybuttonPromptText + ')(.*)')
|
||||
} else {
|
||||
regexp = new RegExp('^(' + escapeRegExp(copybuttonPromptText) + ')(.*)')
|
||||
}
|
||||
|
||||
const outputLines = [];
|
||||
var promptFound = false;
|
||||
var gotLineCont = false;
|
||||
var gotHereDoc = false;
|
||||
const lineGotPrompt = [];
|
||||
for (const line of textContent.split('\n')) {
|
||||
match = line.match(regexp)
|
||||
if (match || gotLineCont || gotHereDoc) {
|
||||
promptFound = regexp.test(line)
|
||||
lineGotPrompt.push(promptFound)
|
||||
if (removePrompts && promptFound) {
|
||||
outputLines.push(match[2])
|
||||
} else {
|
||||
outputLines.push(line)
|
||||
}
|
||||
gotLineCont = line.endsWith(lineContinuationChar) & useLineCont
|
||||
if (line.includes(hereDocDelim) & useHereDoc)
|
||||
gotHereDoc = !gotHereDoc
|
||||
} else if (!onlyCopyPromptLines) {
|
||||
outputLines.push(line)
|
||||
} else if (copyEmptyLines && line.trim() === '') {
|
||||
outputLines.push(line)
|
||||
}
|
||||
}
|
||||
|
||||
// If no lines with the prompt were found then just use original lines
|
||||
if (lineGotPrompt.some(v => v === true)) {
|
||||
textContent = outputLines.join('\n');
|
||||
}
|
||||
|
||||
// Remove a trailing newline to avoid auto-running when pasting
|
||||
if (textContent.endsWith("\n")) {
|
||||
textContent = textContent.slice(0, -1)
|
||||
}
|
||||
return textContent
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
/* This file intentionally left blank. */
|
||||
@@ -0,0 +1,26 @@
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
console.info("Initializing sphinx-design-elements");
|
||||
setup_dropdown_group();
|
||||
});
|
||||
|
||||
function setup_dropdown_group() {
|
||||
|
||||
// Select all relevant detail elements nested within container elements using the `dropdown-group` class.
|
||||
const dropdown_details = document.querySelectorAll(".dropdown-group details");
|
||||
|
||||
// Add event listener for special toggling.
|
||||
dropdown_details.forEach((details) => {
|
||||
details.addEventListener("toggle", toggleOpenGroup);
|
||||
});
|
||||
|
||||
// When toggling elements, exclusively open one element, and close all others.
|
||||
function toggleOpenGroup(e) {
|
||||
if (this.open) {
|
||||
dropdown_details.forEach((details) => {
|
||||
if (details !== this && details.open) {
|
||||
details.open = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
/**
|
||||
* Appearance shortcuts.
|
||||
*/
|
||||
|
||||
|
||||
/* General */
|
||||
|
||||
/* Display paragraphs inline (side by side) */
|
||||
.inline p {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
/* No borders */
|
||||
.no-margin {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
hr.docutils {
|
||||
margin-top: 0.75rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
/* Very small text size */
|
||||
.text-small {
|
||||
font-size: small;
|
||||
}
|
||||
|
||||
/* Smaller text size */
|
||||
.text-smaller {
|
||||
font-size: smaller;
|
||||
}
|
||||
|
||||
/* Medium text size */
|
||||
.text-medium {
|
||||
font-size: medium;
|
||||
}
|
||||
|
||||
/* Large text size */
|
||||
.text-large {
|
||||
font-size: large;
|
||||
}
|
||||
|
||||
/* Very large text size */
|
||||
.text-larger {
|
||||
font-size: larger;
|
||||
}
|
||||
|
||||
|
||||
/* Grid table: Light-weight table-row outline styling. */
|
||||
.top-border {
|
||||
border-top-width: thin;
|
||||
border-top-style: solid;
|
||||
border-top-color: lightgray;
|
||||
}
|
||||
.bottom-border {
|
||||
border-bottom-width: thin;
|
||||
border-bottom-style: solid;
|
||||
border-top-color: lightgray;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Appearance bugfixes.
|
||||
*/
|
||||
|
||||
/* Grid table: Fix margins of child elements inside table items. */
|
||||
.sd-col > .highlight-markdown {
|
||||
margin: unset;
|
||||
}
|
||||
.sd-col > .highlight-restructuredtext {
|
||||
margin: unset;
|
||||
}
|
||||
|
||||
/* Shield: Inside a grid table, code blocks in neighbouring cells did not have any margins to each other. */
|
||||
.col-compact * {
|
||||
margin-bottom: 0.3em;
|
||||
}
|
||||
.sd-col .highlight {
|
||||
margin-bottom: 0.3em !important;
|
||||
}
|
||||
|
||||
/* Info card: Fix grid item formatting. */
|
||||
.sd-col > div.line-block {
|
||||
margin-top: unset;
|
||||
margin-bottom: unset;
|
||||
}
|
||||
|
||||
.bottom-margin-generous {
|
||||
margin-bottom: 2em !important;
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
/*
|
||||
* Base JavaScript utilities for all Sphinx HTML documentation.
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
const BLACKLISTED_KEY_CONTROL_ELEMENTS = new Set([
|
||||
"TEXTAREA",
|
||||
"INPUT",
|
||||
"SELECT",
|
||||
"BUTTON",
|
||||
]);
|
||||
|
||||
const _ready = (callback) => {
|
||||
if (document.readyState !== "loading") {
|
||||
callback();
|
||||
} else {
|
||||
document.addEventListener("DOMContentLoaded", callback);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Small JavaScript module for the documentation.
|
||||
*/
|
||||
const Documentation = {
|
||||
init: () => {
|
||||
Documentation.initDomainIndexTable();
|
||||
Documentation.initOnKeyListeners();
|
||||
},
|
||||
|
||||
/**
|
||||
* i18n support
|
||||
*/
|
||||
TRANSLATIONS: {},
|
||||
PLURAL_EXPR: (n) => (n === 1 ? 0 : 1),
|
||||
LOCALE: "unknown",
|
||||
|
||||
// gettext and ngettext don't access this so that the functions
|
||||
// can safely bound to a different name (_ = Documentation.gettext)
|
||||
gettext: (string) => {
|
||||
const translated = Documentation.TRANSLATIONS[string];
|
||||
switch (typeof translated) {
|
||||
case "undefined":
|
||||
return string; // no translation
|
||||
case "string":
|
||||
return translated; // translation exists
|
||||
default:
|
||||
return translated[0]; // (singular, plural) translation tuple exists
|
||||
}
|
||||
},
|
||||
|
||||
ngettext: (singular, plural, n) => {
|
||||
const translated = Documentation.TRANSLATIONS[singular];
|
||||
if (typeof translated !== "undefined")
|
||||
return translated[Documentation.PLURAL_EXPR(n)];
|
||||
return n === 1 ? singular : plural;
|
||||
},
|
||||
|
||||
addTranslations: (catalog) => {
|
||||
Object.assign(Documentation.TRANSLATIONS, catalog.messages);
|
||||
Documentation.PLURAL_EXPR = new Function(
|
||||
"n",
|
||||
`return (${catalog.plural_expr})`
|
||||
);
|
||||
Documentation.LOCALE = catalog.locale;
|
||||
},
|
||||
|
||||
/**
|
||||
* helper function to focus on search bar
|
||||
*/
|
||||
focusSearchBar: () => {
|
||||
document.querySelectorAll("input[name=q]")[0]?.focus();
|
||||
},
|
||||
|
||||
/**
|
||||
* Initialise the domain index toggle buttons
|
||||
*/
|
||||
initDomainIndexTable: () => {
|
||||
const toggler = (el) => {
|
||||
const idNumber = el.id.substr(7);
|
||||
const toggledRows = document.querySelectorAll(`tr.cg-${idNumber}`);
|
||||
if (el.src.substr(-9) === "minus.png") {
|
||||
el.src = `${el.src.substr(0, el.src.length - 9)}plus.png`;
|
||||
toggledRows.forEach((el) => (el.style.display = "none"));
|
||||
} else {
|
||||
el.src = `${el.src.substr(0, el.src.length - 8)}minus.png`;
|
||||
toggledRows.forEach((el) => (el.style.display = ""));
|
||||
}
|
||||
};
|
||||
|
||||
const togglerElements = document.querySelectorAll("img.toggler");
|
||||
togglerElements.forEach((el) =>
|
||||
el.addEventListener("click", (event) => toggler(event.currentTarget))
|
||||
);
|
||||
togglerElements.forEach((el) => (el.style.display = ""));
|
||||
if (DOCUMENTATION_OPTIONS.COLLAPSE_INDEX) togglerElements.forEach(toggler);
|
||||
},
|
||||
|
||||
initOnKeyListeners: () => {
|
||||
// only install a listener if it is really needed
|
||||
if (
|
||||
!DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS &&
|
||||
!DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS
|
||||
)
|
||||
return;
|
||||
|
||||
document.addEventListener("keydown", (event) => {
|
||||
// bail for input elements
|
||||
if (BLACKLISTED_KEY_CONTROL_ELEMENTS.has(document.activeElement.tagName)) return;
|
||||
// bail with special keys
|
||||
if (event.altKey || event.ctrlKey || event.metaKey) return;
|
||||
|
||||
if (!event.shiftKey) {
|
||||
switch (event.key) {
|
||||
case "ArrowLeft":
|
||||
if (!DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS) break;
|
||||
|
||||
const prevLink = document.querySelector('link[rel="prev"]');
|
||||
if (prevLink && prevLink.href) {
|
||||
window.location.href = prevLink.href;
|
||||
event.preventDefault();
|
||||
}
|
||||
break;
|
||||
case "ArrowRight":
|
||||
if (!DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS) break;
|
||||
|
||||
const nextLink = document.querySelector('link[rel="next"]');
|
||||
if (nextLink && nextLink.href) {
|
||||
window.location.href = nextLink.href;
|
||||
event.preventDefault();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// some keyboard layouts may need Shift to get /
|
||||
switch (event.key) {
|
||||
case "/":
|
||||
if (!DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS) break;
|
||||
Documentation.focusSearchBar();
|
||||
event.preventDefault();
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
// quick alias for translations
|
||||
const _ = Documentation.gettext;
|
||||
|
||||
_ready(Documentation.init);
|
||||
@@ -0,0 +1,13 @@
|
||||
const DOCUMENTATION_OPTIONS = {
|
||||
VERSION: '3.6.2',
|
||||
LANGUAGE: 'en',
|
||||
COLLAPSE_INDEX: false,
|
||||
BUILDER: 'html',
|
||||
FILE_SUFFIX: '.html',
|
||||
LINK_SUFFIX: '.html',
|
||||
HAS_SOURCE: true,
|
||||
SOURCELINK_SUFFIX: '.txt',
|
||||
NAVIGATION_WITH_KEYS: false,
|
||||
SHOW_SEARCH_SUMMARY: true,
|
||||
ENABLE_SEARCH_SHORTCUTS: true,
|
||||
};
|
||||
|
After Width: | Height: | Size: 286 B |
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="80" height="80" viewBox="0 0 250 250" fill="#fff">
|
||||
<path d="M0 0l115 115h15l12 27 108 108V0z" fill="#151513"/>
|
||||
<path d="M128 109c-15-9-9-19-9-19 3-7 2-11 2-11-1-7 3-2 3-2 4 5 2 11 2 11-3 10 5 15 9 16"/>
|
||||
<path d="M115 115s4 2 5 0l14-14c3-2 6-3 8-3-8-11-15-24 2-41 5-5 10-7 16-7 1-2 3-7 12-11 0 0 5 3 7 16 4 2 8 5 12 9s7 8 9 12c14 3 17 7 17 7-4 8-9 11-11 11 0 6-2 11-7 16-16 16-30 10-41 2 0 3-1 7-5 11l-12 11c-1 1 1 5 1 5z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 490 B |
@@ -0,0 +1,192 @@
|
||||
/*
|
||||
* This script contains the language-specific data used by searchtools.js,
|
||||
* namely the list of stopwords, stemmer, scorer and splitter.
|
||||
*/
|
||||
|
||||
var stopwords = ["a", "and", "are", "as", "at", "be", "but", "by", "for", "if", "in", "into", "is", "it", "near", "no", "not", "of", "on", "or", "such", "that", "the", "their", "then", "there", "these", "they", "this", "to", "was", "will", "with"];
|
||||
|
||||
|
||||
/* Non-minified version is copied as a separate JS file, if available */
|
||||
|
||||
/**
|
||||
* Porter Stemmer
|
||||
*/
|
||||
var Stemmer = function() {
|
||||
|
||||
var step2list = {
|
||||
ational: 'ate',
|
||||
tional: 'tion',
|
||||
enci: 'ence',
|
||||
anci: 'ance',
|
||||
izer: 'ize',
|
||||
bli: 'ble',
|
||||
alli: 'al',
|
||||
entli: 'ent',
|
||||
eli: 'e',
|
||||
ousli: 'ous',
|
||||
ization: 'ize',
|
||||
ation: 'ate',
|
||||
ator: 'ate',
|
||||
alism: 'al',
|
||||
iveness: 'ive',
|
||||
fulness: 'ful',
|
||||
ousness: 'ous',
|
||||
aliti: 'al',
|
||||
iviti: 'ive',
|
||||
biliti: 'ble',
|
||||
logi: 'log'
|
||||
};
|
||||
|
||||
var step3list = {
|
||||
icate: 'ic',
|
||||
ative: '',
|
||||
alize: 'al',
|
||||
iciti: 'ic',
|
||||
ical: 'ic',
|
||||
ful: '',
|
||||
ness: ''
|
||||
};
|
||||
|
||||
var c = "[^aeiou]"; // consonant
|
||||
var v = "[aeiouy]"; // vowel
|
||||
var C = c + "[^aeiouy]*"; // consonant sequence
|
||||
var V = v + "[aeiou]*"; // vowel sequence
|
||||
|
||||
var mgr0 = "^(" + C + ")?" + V + C; // [C]VC... is m>0
|
||||
var meq1 = "^(" + C + ")?" + V + C + "(" + V + ")?$"; // [C]VC[V] is m=1
|
||||
var mgr1 = "^(" + C + ")?" + V + C + V + C; // [C]VCVC... is m>1
|
||||
var s_v = "^(" + C + ")?" + v; // vowel in stem
|
||||
|
||||
this.stemWord = function (w) {
|
||||
var stem;
|
||||
var suffix;
|
||||
var firstch;
|
||||
var origword = w;
|
||||
|
||||
if (w.length < 3)
|
||||
return w;
|
||||
|
||||
var re;
|
||||
var re2;
|
||||
var re3;
|
||||
var re4;
|
||||
|
||||
firstch = w.substr(0,1);
|
||||
if (firstch == "y")
|
||||
w = firstch.toUpperCase() + w.substr(1);
|
||||
|
||||
// Step 1a
|
||||
re = /^(.+?)(ss|i)es$/;
|
||||
re2 = /^(.+?)([^s])s$/;
|
||||
|
||||
if (re.test(w))
|
||||
w = w.replace(re,"$1$2");
|
||||
else if (re2.test(w))
|
||||
w = w.replace(re2,"$1$2");
|
||||
|
||||
// Step 1b
|
||||
re = /^(.+?)eed$/;
|
||||
re2 = /^(.+?)(ed|ing)$/;
|
||||
if (re.test(w)) {
|
||||
var fp = re.exec(w);
|
||||
re = new RegExp(mgr0);
|
||||
if (re.test(fp[1])) {
|
||||
re = /.$/;
|
||||
w = w.replace(re,"");
|
||||
}
|
||||
}
|
||||
else if (re2.test(w)) {
|
||||
var fp = re2.exec(w);
|
||||
stem = fp[1];
|
||||
re2 = new RegExp(s_v);
|
||||
if (re2.test(stem)) {
|
||||
w = stem;
|
||||
re2 = /(at|bl|iz)$/;
|
||||
re3 = new RegExp("([^aeiouylsz])\\1$");
|
||||
re4 = new RegExp("^" + C + v + "[^aeiouwxy]$");
|
||||
if (re2.test(w))
|
||||
w = w + "e";
|
||||
else if (re3.test(w)) {
|
||||
re = /.$/;
|
||||
w = w.replace(re,"");
|
||||
}
|
||||
else if (re4.test(w))
|
||||
w = w + "e";
|
||||
}
|
||||
}
|
||||
|
||||
// Step 1c
|
||||
re = /^(.+?)y$/;
|
||||
if (re.test(w)) {
|
||||
var fp = re.exec(w);
|
||||
stem = fp[1];
|
||||
re = new RegExp(s_v);
|
||||
if (re.test(stem))
|
||||
w = stem + "i";
|
||||
}
|
||||
|
||||
// Step 2
|
||||
re = /^(.+?)(ational|tional|enci|anci|izer|bli|alli|entli|eli|ousli|ization|ation|ator|alism|iveness|fulness|ousness|aliti|iviti|biliti|logi)$/;
|
||||
if (re.test(w)) {
|
||||
var fp = re.exec(w);
|
||||
stem = fp[1];
|
||||
suffix = fp[2];
|
||||
re = new RegExp(mgr0);
|
||||
if (re.test(stem))
|
||||
w = stem + step2list[suffix];
|
||||
}
|
||||
|
||||
// Step 3
|
||||
re = /^(.+?)(icate|ative|alize|iciti|ical|ful|ness)$/;
|
||||
if (re.test(w)) {
|
||||
var fp = re.exec(w);
|
||||
stem = fp[1];
|
||||
suffix = fp[2];
|
||||
re = new RegExp(mgr0);
|
||||
if (re.test(stem))
|
||||
w = stem + step3list[suffix];
|
||||
}
|
||||
|
||||
// Step 4
|
||||
re = /^(.+?)(al|ance|ence|er|ic|able|ible|ant|ement|ment|ent|ou|ism|ate|iti|ous|ive|ize)$/;
|
||||
re2 = /^(.+?)(s|t)(ion)$/;
|
||||
if (re.test(w)) {
|
||||
var fp = re.exec(w);
|
||||
stem = fp[1];
|
||||
re = new RegExp(mgr1);
|
||||
if (re.test(stem))
|
||||
w = stem;
|
||||
}
|
||||
else if (re2.test(w)) {
|
||||
var fp = re2.exec(w);
|
||||
stem = fp[1] + fp[2];
|
||||
re2 = new RegExp(mgr1);
|
||||
if (re2.test(stem))
|
||||
w = stem;
|
||||
}
|
||||
|
||||
// Step 5
|
||||
re = /^(.+?)e$/;
|
||||
if (re.test(w)) {
|
||||
var fp = re.exec(w);
|
||||
stem = fp[1];
|
||||
re = new RegExp(mgr1);
|
||||
re2 = new RegExp(meq1);
|
||||
re3 = new RegExp("^" + C + v + "[^aeiouwxy]$");
|
||||
if (re.test(stem) || (re2.test(stem) && !(re3.test(stem))))
|
||||
w = stem;
|
||||
}
|
||||
re = /ll$/;
|
||||
re2 = new RegExp(mgr1);
|
||||
if (re.test(w) && re2.test(w)) {
|
||||
re = /.$/;
|
||||
w = w.replace(re,"");
|
||||
}
|
||||
|
||||
// and turn initial Y back to y
|
||||
if (firstch == "y")
|
||||
w = firstch.toLowerCase() + w.substr(1);
|
||||
return w;
|
||||
}
|
||||
}
|
||||
|
||||
|
After Width: | Height: | Size: 90 B |
|
After Width: | Height: | Size: 90 B |
@@ -0,0 +1,84 @@
|
||||
pre { line-height: 125%; }
|
||||
td.linenos .normal { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; }
|
||||
span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; }
|
||||
td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
|
||||
span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
|
||||
.highlight .hll { background-color: #ffffcc }
|
||||
.highlight { background: #f8f8f8; }
|
||||
.highlight .c { color: #8F5902; font-style: italic } /* Comment */
|
||||
.highlight .err { color: #A40000; border: 1px solid #EF2929 } /* Error */
|
||||
.highlight .g { color: #000 } /* Generic */
|
||||
.highlight .k { color: #004461; font-weight: bold } /* Keyword */
|
||||
.highlight .l { color: #000 } /* Literal */
|
||||
.highlight .n { color: #000 } /* Name */
|
||||
.highlight .o { color: #582800 } /* Operator */
|
||||
.highlight .x { color: #000 } /* Other */
|
||||
.highlight .p { color: #000; font-weight: bold } /* Punctuation */
|
||||
.highlight .ch { color: #8F5902; font-style: italic } /* Comment.Hashbang */
|
||||
.highlight .cm { color: #8F5902; font-style: italic } /* Comment.Multiline */
|
||||
.highlight .cp { color: #8F5902 } /* Comment.Preproc */
|
||||
.highlight .cpf { color: #8F5902; font-style: italic } /* Comment.PreprocFile */
|
||||
.highlight .c1 { color: #8F5902; font-style: italic } /* Comment.Single */
|
||||
.highlight .cs { color: #8F5902; font-style: italic } /* Comment.Special */
|
||||
.highlight .gd { color: #A40000 } /* Generic.Deleted */
|
||||
.highlight .ge { color: #000; font-style: italic } /* Generic.Emph */
|
||||
.highlight .ges { color: #000 } /* Generic.EmphStrong */
|
||||
.highlight .gr { color: #EF2929 } /* Generic.Error */
|
||||
.highlight .gh { color: #000080; font-weight: bold } /* Generic.Heading */
|
||||
.highlight .gi { color: #00A000 } /* Generic.Inserted */
|
||||
.highlight .go { color: #888 } /* Generic.Output */
|
||||
.highlight .gp { color: #745334 } /* Generic.Prompt */
|
||||
.highlight .gs { color: #000; font-weight: bold } /* Generic.Strong */
|
||||
.highlight .gu { color: #800080; font-weight: bold } /* Generic.Subheading */
|
||||
.highlight .gt { color: #A40000; font-weight: bold } /* Generic.Traceback */
|
||||
.highlight .kc { color: #004461; font-weight: bold } /* Keyword.Constant */
|
||||
.highlight .kd { color: #004461; font-weight: bold } /* Keyword.Declaration */
|
||||
.highlight .kn { color: #004461; font-weight: bold } /* Keyword.Namespace */
|
||||
.highlight .kp { color: #004461; font-weight: bold } /* Keyword.Pseudo */
|
||||
.highlight .kr { color: #004461; font-weight: bold } /* Keyword.Reserved */
|
||||
.highlight .kt { color: #004461; font-weight: bold } /* Keyword.Type */
|
||||
.highlight .ld { color: #000 } /* Literal.Date */
|
||||
.highlight .m { color: #900 } /* Literal.Number */
|
||||
.highlight .s { color: #4E9A06 } /* Literal.String */
|
||||
.highlight .na { color: #C4A000 } /* Name.Attribute */
|
||||
.highlight .nb { color: #004461 } /* Name.Builtin */
|
||||
.highlight .nc { color: #000 } /* Name.Class */
|
||||
.highlight .no { color: #000 } /* Name.Constant */
|
||||
.highlight .nd { color: #888 } /* Name.Decorator */
|
||||
.highlight .ni { color: #CE5C00 } /* Name.Entity */
|
||||
.highlight .ne { color: #C00; font-weight: bold } /* Name.Exception */
|
||||
.highlight .nf { color: #000 } /* Name.Function */
|
||||
.highlight .nl { color: #F57900 } /* Name.Label */
|
||||
.highlight .nn { color: #000 } /* Name.Namespace */
|
||||
.highlight .nx { color: #000 } /* Name.Other */
|
||||
.highlight .py { color: #000 } /* Name.Property */
|
||||
.highlight .nt { color: #004461; font-weight: bold } /* Name.Tag */
|
||||
.highlight .nv { color: #000 } /* Name.Variable */
|
||||
.highlight .ow { color: #004461; font-weight: bold } /* Operator.Word */
|
||||
.highlight .pm { color: #000; font-weight: bold } /* Punctuation.Marker */
|
||||
.highlight .w { color: #F8F8F8 } /* Text.Whitespace */
|
||||
.highlight .mb { color: #900 } /* Literal.Number.Bin */
|
||||
.highlight .mf { color: #900 } /* Literal.Number.Float */
|
||||
.highlight .mh { color: #900 } /* Literal.Number.Hex */
|
||||
.highlight .mi { color: #900 } /* Literal.Number.Integer */
|
||||
.highlight .mo { color: #900 } /* Literal.Number.Oct */
|
||||
.highlight .sa { color: #4E9A06 } /* Literal.String.Affix */
|
||||
.highlight .sb { color: #4E9A06 } /* Literal.String.Backtick */
|
||||
.highlight .sc { color: #4E9A06 } /* Literal.String.Char */
|
||||
.highlight .dl { color: #4E9A06 } /* Literal.String.Delimiter */
|
||||
.highlight .sd { color: #8F5902; font-style: italic } /* Literal.String.Doc */
|
||||
.highlight .s2 { color: #4E9A06 } /* Literal.String.Double */
|
||||
.highlight .se { color: #4E9A06 } /* Literal.String.Escape */
|
||||
.highlight .sh { color: #4E9A06 } /* Literal.String.Heredoc */
|
||||
.highlight .si { color: #4E9A06 } /* Literal.String.Interpol */
|
||||
.highlight .sx { color: #4E9A06 } /* Literal.String.Other */
|
||||
.highlight .sr { color: #4E9A06 } /* Literal.String.Regex */
|
||||
.highlight .s1 { color: #4E9A06 } /* Literal.String.Single */
|
||||
.highlight .ss { color: #4E9A06 } /* Literal.String.Symbol */
|
||||
.highlight .bp { color: #3465A4 } /* Name.Builtin.Pseudo */
|
||||
.highlight .fm { color: #000 } /* Name.Function.Magic */
|
||||
.highlight .vc { color: #000 } /* Name.Variable.Class */
|
||||
.highlight .vg { color: #000 } /* Name.Variable.Global */
|
||||
.highlight .vi { color: #000 } /* Name.Variable.Instance */
|
||||
.highlight .vm { color: #000 } /* Name.Variable.Magic */
|
||||
.highlight .il { color: #900 } /* Literal.Number.Integer.Long */
|
||||
|
Before Width: | Height: | Size: 762 KiB After Width: | Height: | Size: 762 KiB |
@@ -0,0 +1,635 @@
|
||||
/*
|
||||
* Sphinx JavaScript utilities for the full-text search.
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* Simple result scoring code.
|
||||
*/
|
||||
if (typeof Scorer === "undefined") {
|
||||
var Scorer = {
|
||||
// Implement the following function to further tweak the score for each result
|
||||
// The function takes a result array [docname, title, anchor, descr, score, filename]
|
||||
// and returns the new score.
|
||||
/*
|
||||
score: result => {
|
||||
const [docname, title, anchor, descr, score, filename, kind] = result
|
||||
return score
|
||||
},
|
||||
*/
|
||||
|
||||
// query matches the full name of an object
|
||||
objNameMatch: 11,
|
||||
// or matches in the last dotted part of the object name
|
||||
objPartialMatch: 6,
|
||||
// Additive scores depending on the priority of the object
|
||||
objPrio: {
|
||||
0: 15, // used to be importantResults
|
||||
1: 5, // used to be objectResults
|
||||
2: -5, // used to be unimportantResults
|
||||
},
|
||||
// Used when the priority is not in the mapping.
|
||||
objPrioDefault: 0,
|
||||
|
||||
// query found in title
|
||||
title: 15,
|
||||
partialTitle: 7,
|
||||
// query found in terms
|
||||
term: 5,
|
||||
partialTerm: 2,
|
||||
};
|
||||
}
|
||||
|
||||
// Global search result kind enum, used by themes to style search results.
|
||||
class SearchResultKind {
|
||||
static get index() { return "index"; }
|
||||
static get object() { return "object"; }
|
||||
static get text() { return "text"; }
|
||||
static get title() { return "title"; }
|
||||
}
|
||||
|
||||
const _removeChildren = (element) => {
|
||||
while (element && element.lastChild) element.removeChild(element.lastChild);
|
||||
};
|
||||
|
||||
/**
|
||||
* See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#escaping
|
||||
*/
|
||||
const _escapeRegExp = (string) =>
|
||||
string.replace(/[.*+\-?^${}()|[\]\\]/g, "\\$&"); // $& means the whole matched string
|
||||
|
||||
const _displayItem = (item, searchTerms, highlightTerms) => {
|
||||
const docBuilder = DOCUMENTATION_OPTIONS.BUILDER;
|
||||
const docFileSuffix = DOCUMENTATION_OPTIONS.FILE_SUFFIX;
|
||||
const docLinkSuffix = DOCUMENTATION_OPTIONS.LINK_SUFFIX;
|
||||
const showSearchSummary = DOCUMENTATION_OPTIONS.SHOW_SEARCH_SUMMARY;
|
||||
const contentRoot = document.documentElement.dataset.content_root;
|
||||
|
||||
const [docName, title, anchor, descr, score, _filename, kind] = item;
|
||||
|
||||
let listItem = document.createElement("li");
|
||||
// Add a class representing the item's type:
|
||||
// can be used by a theme's CSS selector for styling
|
||||
// See SearchResultKind for the class names.
|
||||
listItem.classList.add(`kind-${kind}`);
|
||||
let requestUrl;
|
||||
let linkUrl;
|
||||
if (docBuilder === "dirhtml") {
|
||||
// dirhtml builder
|
||||
let dirname = docName + "/";
|
||||
if (dirname.match(/\/index\/$/))
|
||||
dirname = dirname.substring(0, dirname.length - 6);
|
||||
else if (dirname === "index/") dirname = "";
|
||||
requestUrl = contentRoot + dirname;
|
||||
linkUrl = requestUrl;
|
||||
} else {
|
||||
// normal html builders
|
||||
requestUrl = contentRoot + docName + docFileSuffix;
|
||||
linkUrl = docName + docLinkSuffix;
|
||||
}
|
||||
let linkEl = listItem.appendChild(document.createElement("a"));
|
||||
linkEl.href = linkUrl + anchor;
|
||||
linkEl.dataset.score = score;
|
||||
linkEl.innerHTML = title;
|
||||
if (descr) {
|
||||
listItem.appendChild(document.createElement("span")).innerHTML =
|
||||
" (" + descr + ")";
|
||||
// highlight search terms in the description
|
||||
if (SPHINX_HIGHLIGHT_ENABLED) // set in sphinx_highlight.js
|
||||
highlightTerms.forEach((term) => _highlightText(listItem, term, "highlighted"));
|
||||
}
|
||||
else if (showSearchSummary)
|
||||
fetch(requestUrl)
|
||||
.then((responseData) => responseData.text())
|
||||
.then((data) => {
|
||||
if (data)
|
||||
listItem.appendChild(
|
||||
Search.makeSearchSummary(data, searchTerms, anchor)
|
||||
);
|
||||
// highlight search terms in the summary
|
||||
if (SPHINX_HIGHLIGHT_ENABLED) // set in sphinx_highlight.js
|
||||
highlightTerms.forEach((term) => _highlightText(listItem, term, "highlighted"));
|
||||
});
|
||||
Search.output.appendChild(listItem);
|
||||
};
|
||||
const _finishSearch = (resultCount) => {
|
||||
Search.stopPulse();
|
||||
Search.title.innerText = _("Search Results");
|
||||
if (!resultCount)
|
||||
Search.status.innerText = Documentation.gettext(
|
||||
"Your search did not match any documents. Please make sure that all words are spelled correctly and that you've selected enough categories."
|
||||
);
|
||||
else
|
||||
Search.status.innerText = Documentation.ngettext(
|
||||
"Search finished, found one page matching the search query.",
|
||||
"Search finished, found ${resultCount} pages matching the search query.",
|
||||
resultCount,
|
||||
).replace('${resultCount}', resultCount);
|
||||
};
|
||||
const _displayNextItem = (
|
||||
results,
|
||||
resultCount,
|
||||
searchTerms,
|
||||
highlightTerms,
|
||||
) => {
|
||||
// results left, load the summary and display it
|
||||
// this is intended to be dynamic (don't sub resultsCount)
|
||||
if (results.length) {
|
||||
_displayItem(results.pop(), searchTerms, highlightTerms);
|
||||
setTimeout(
|
||||
() => _displayNextItem(results, resultCount, searchTerms, highlightTerms),
|
||||
5
|
||||
);
|
||||
}
|
||||
// search finished, update title and status message
|
||||
else _finishSearch(resultCount);
|
||||
};
|
||||
// Helper function used by query() to order search results.
|
||||
// Each input is an array of [docname, title, anchor, descr, score, filename, kind].
|
||||
// Order the results by score (in opposite order of appearance, since the
|
||||
// `_displayNextItem` function uses pop() to retrieve items) and then alphabetically.
|
||||
const _orderResultsByScoreThenName = (a, b) => {
|
||||
const leftScore = a[4];
|
||||
const rightScore = b[4];
|
||||
if (leftScore === rightScore) {
|
||||
// same score: sort alphabetically
|
||||
const leftTitle = a[1].toLowerCase();
|
||||
const rightTitle = b[1].toLowerCase();
|
||||
if (leftTitle === rightTitle) return 0;
|
||||
return leftTitle > rightTitle ? -1 : 1; // inverted is intentional
|
||||
}
|
||||
return leftScore > rightScore ? 1 : -1;
|
||||
};
|
||||
|
||||
/**
|
||||
* Default splitQuery function. Can be overridden in ``sphinx.search`` with a
|
||||
* custom function per language.
|
||||
*
|
||||
* The regular expression works by splitting the string on consecutive characters
|
||||
* that are not Unicode letters, numbers, underscores, or emoji characters.
|
||||
* This is the same as ``\W+`` in Python, preserving the surrogate pair area.
|
||||
*/
|
||||
if (typeof splitQuery === "undefined") {
|
||||
var splitQuery = (query) => query
|
||||
.split(/[^\p{Letter}\p{Number}_\p{Emoji_Presentation}]+/gu)
|
||||
.filter(term => term) // remove remaining empty strings
|
||||
}
|
||||
|
||||
/**
|
||||
* Search Module
|
||||
*/
|
||||
const Search = {
|
||||
_index: null,
|
||||
_queued_query: null,
|
||||
_pulse_status: -1,
|
||||
|
||||
htmlToText: (htmlString, anchor) => {
|
||||
const htmlElement = new DOMParser().parseFromString(htmlString, 'text/html');
|
||||
for (const removalQuery of [".headerlink", "script", "style"]) {
|
||||
htmlElement.querySelectorAll(removalQuery).forEach((el) => { el.remove() });
|
||||
}
|
||||
if (anchor) {
|
||||
const anchorContent = htmlElement.querySelector(`[role="main"] ${anchor}`);
|
||||
if (anchorContent) return anchorContent.textContent;
|
||||
|
||||
console.warn(
|
||||
`Anchored content block not found. Sphinx search tries to obtain it via DOM query '[role=main] ${anchor}'. Check your theme or template.`
|
||||
);
|
||||
}
|
||||
|
||||
// if anchor not specified or not found, fall back to main content
|
||||
const docContent = htmlElement.querySelector('[role="main"]');
|
||||
if (docContent) return docContent.textContent;
|
||||
|
||||
console.warn(
|
||||
"Content block not found. Sphinx search tries to obtain it via DOM query '[role=main]'. Check your theme or template."
|
||||
);
|
||||
return "";
|
||||
},
|
||||
|
||||
init: () => {
|
||||
const query = new URLSearchParams(window.location.search).get("q");
|
||||
document
|
||||
.querySelectorAll('input[name="q"]')
|
||||
.forEach((el) => (el.value = query));
|
||||
if (query) Search.performSearch(query);
|
||||
},
|
||||
|
||||
loadIndex: (url) =>
|
||||
(document.body.appendChild(document.createElement("script")).src = url),
|
||||
|
||||
setIndex: (index) => {
|
||||
Search._index = index;
|
||||
if (Search._queued_query !== null) {
|
||||
const query = Search._queued_query;
|
||||
Search._queued_query = null;
|
||||
Search.query(query);
|
||||
}
|
||||
},
|
||||
|
||||
hasIndex: () => Search._index !== null,
|
||||
|
||||
deferQuery: (query) => (Search._queued_query = query),
|
||||
|
||||
stopPulse: () => (Search._pulse_status = -1),
|
||||
|
||||
startPulse: () => {
|
||||
if (Search._pulse_status >= 0) return;
|
||||
|
||||
const pulse = () => {
|
||||
Search._pulse_status = (Search._pulse_status + 1) % 4;
|
||||
Search.dots.innerText = ".".repeat(Search._pulse_status);
|
||||
if (Search._pulse_status >= 0) window.setTimeout(pulse, 500);
|
||||
};
|
||||
pulse();
|
||||
},
|
||||
|
||||
/**
|
||||
* perform a search for something (or wait until index is loaded)
|
||||
*/
|
||||
performSearch: (query) => {
|
||||
// create the required interface elements
|
||||
const searchText = document.createElement("h2");
|
||||
searchText.textContent = _("Searching");
|
||||
const searchSummary = document.createElement("p");
|
||||
searchSummary.classList.add("search-summary");
|
||||
searchSummary.innerText = "";
|
||||
const searchList = document.createElement("ul");
|
||||
searchList.setAttribute("role", "list");
|
||||
searchList.classList.add("search");
|
||||
|
||||
const out = document.getElementById("search-results");
|
||||
Search.title = out.appendChild(searchText);
|
||||
Search.dots = Search.title.appendChild(document.createElement("span"));
|
||||
Search.status = out.appendChild(searchSummary);
|
||||
Search.output = out.appendChild(searchList);
|
||||
|
||||
const searchProgress = document.getElementById("search-progress");
|
||||
// Some themes don't use the search progress node
|
||||
if (searchProgress) {
|
||||
searchProgress.innerText = _("Preparing search...");
|
||||
}
|
||||
Search.startPulse();
|
||||
|
||||
// index already loaded, the browser was quick!
|
||||
if (Search.hasIndex()) Search.query(query);
|
||||
else Search.deferQuery(query);
|
||||
},
|
||||
|
||||
_parseQuery: (query) => {
|
||||
// stem the search terms and add them to the correct list
|
||||
const stemmer = new Stemmer();
|
||||
const searchTerms = new Set();
|
||||
const excludedTerms = new Set();
|
||||
const highlightTerms = new Set();
|
||||
const objectTerms = new Set(splitQuery(query.toLowerCase().trim()));
|
||||
splitQuery(query.trim()).forEach((queryTerm) => {
|
||||
const queryTermLower = queryTerm.toLowerCase();
|
||||
|
||||
// maybe skip this "word"
|
||||
// stopwords array is from language_data.js
|
||||
if (
|
||||
stopwords.indexOf(queryTermLower) !== -1 ||
|
||||
queryTerm.match(/^\d+$/)
|
||||
)
|
||||
return;
|
||||
|
||||
// stem the word
|
||||
let word = stemmer.stemWord(queryTermLower);
|
||||
// select the correct list
|
||||
if (word[0] === "-") excludedTerms.add(word.substr(1));
|
||||
else {
|
||||
searchTerms.add(word);
|
||||
highlightTerms.add(queryTermLower);
|
||||
}
|
||||
});
|
||||
|
||||
if (SPHINX_HIGHLIGHT_ENABLED) { // set in sphinx_highlight.js
|
||||
localStorage.setItem("sphinx_highlight_terms", [...highlightTerms].join(" "))
|
||||
}
|
||||
|
||||
// console.debug("SEARCH: searching for:");
|
||||
// console.info("required: ", [...searchTerms]);
|
||||
// console.info("excluded: ", [...excludedTerms]);
|
||||
|
||||
return [query, searchTerms, excludedTerms, highlightTerms, objectTerms];
|
||||
},
|
||||
|
||||
/**
|
||||
* execute search (requires search index to be loaded)
|
||||
*/
|
||||
_performSearch: (query, searchTerms, excludedTerms, highlightTerms, objectTerms) => {
|
||||
const filenames = Search._index.filenames;
|
||||
const docNames = Search._index.docnames;
|
||||
const titles = Search._index.titles;
|
||||
const allTitles = Search._index.alltitles;
|
||||
const indexEntries = Search._index.indexentries;
|
||||
|
||||
// Collect multiple result groups to be sorted separately and then ordered.
|
||||
// Each is an array of [docname, title, anchor, descr, score, filename, kind].
|
||||
const normalResults = [];
|
||||
const nonMainIndexResults = [];
|
||||
|
||||
_removeChildren(document.getElementById("search-progress"));
|
||||
|
||||
const queryLower = query.toLowerCase().trim();
|
||||
for (const [title, foundTitles] of Object.entries(allTitles)) {
|
||||
if (title.toLowerCase().trim().includes(queryLower) && (queryLower.length >= title.length/2)) {
|
||||
for (const [file, id] of foundTitles) {
|
||||
const score = Math.round(Scorer.title * queryLower.length / title.length);
|
||||
const boost = titles[file] === title ? 1 : 0; // add a boost for document titles
|
||||
normalResults.push([
|
||||
docNames[file],
|
||||
titles[file] !== title ? `${titles[file]} > ${title}` : title,
|
||||
id !== null ? "#" + id : "",
|
||||
null,
|
||||
score + boost,
|
||||
filenames[file],
|
||||
SearchResultKind.title,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// search for explicit entries in index directives
|
||||
for (const [entry, foundEntries] of Object.entries(indexEntries)) {
|
||||
if (entry.includes(queryLower) && (queryLower.length >= entry.length/2)) {
|
||||
for (const [file, id, isMain] of foundEntries) {
|
||||
const score = Math.round(100 * queryLower.length / entry.length);
|
||||
const result = [
|
||||
docNames[file],
|
||||
titles[file],
|
||||
id ? "#" + id : "",
|
||||
null,
|
||||
score,
|
||||
filenames[file],
|
||||
SearchResultKind.index,
|
||||
];
|
||||
if (isMain) {
|
||||
normalResults.push(result);
|
||||
} else {
|
||||
nonMainIndexResults.push(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// lookup as object
|
||||
objectTerms.forEach((term) =>
|
||||
normalResults.push(...Search.performObjectSearch(term, objectTerms))
|
||||
);
|
||||
|
||||
// lookup as search terms in fulltext
|
||||
normalResults.push(...Search.performTermsSearch(searchTerms, excludedTerms));
|
||||
|
||||
// let the scorer override scores with a custom scoring function
|
||||
if (Scorer.score) {
|
||||
normalResults.forEach((item) => (item[4] = Scorer.score(item)));
|
||||
nonMainIndexResults.forEach((item) => (item[4] = Scorer.score(item)));
|
||||
}
|
||||
|
||||
// Sort each group of results by score and then alphabetically by name.
|
||||
normalResults.sort(_orderResultsByScoreThenName);
|
||||
nonMainIndexResults.sort(_orderResultsByScoreThenName);
|
||||
|
||||
// Combine the result groups in (reverse) order.
|
||||
// Non-main index entries are typically arbitrary cross-references,
|
||||
// so display them after other results.
|
||||
let results = [...nonMainIndexResults, ...normalResults];
|
||||
|
||||
// remove duplicate search results
|
||||
// note the reversing of results, so that in the case of duplicates, the highest-scoring entry is kept
|
||||
let seen = new Set();
|
||||
results = results.reverse().reduce((acc, result) => {
|
||||
let resultStr = result.slice(0, 4).concat([result[5]]).map(v => String(v)).join(',');
|
||||
if (!seen.has(resultStr)) {
|
||||
acc.push(result);
|
||||
seen.add(resultStr);
|
||||
}
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
return results.reverse();
|
||||
},
|
||||
|
||||
query: (query) => {
|
||||
const [searchQuery, searchTerms, excludedTerms, highlightTerms, objectTerms] = Search._parseQuery(query);
|
||||
const results = Search._performSearch(searchQuery, searchTerms, excludedTerms, highlightTerms, objectTerms);
|
||||
|
||||
// for debugging
|
||||
//Search.lastresults = results.slice(); // a copy
|
||||
// console.info("search results:", Search.lastresults);
|
||||
|
||||
// print the results
|
||||
_displayNextItem(results, results.length, searchTerms, highlightTerms);
|
||||
},
|
||||
|
||||
/**
|
||||
* search for object names
|
||||
*/
|
||||
performObjectSearch: (object, objectTerms) => {
|
||||
const filenames = Search._index.filenames;
|
||||
const docNames = Search._index.docnames;
|
||||
const objects = Search._index.objects;
|
||||
const objNames = Search._index.objnames;
|
||||
const titles = Search._index.titles;
|
||||
|
||||
const results = [];
|
||||
|
||||
const objectSearchCallback = (prefix, match) => {
|
||||
const name = match[4]
|
||||
const fullname = (prefix ? prefix + "." : "") + name;
|
||||
const fullnameLower = fullname.toLowerCase();
|
||||
if (fullnameLower.indexOf(object) < 0) return;
|
||||
|
||||
let score = 0;
|
||||
const parts = fullnameLower.split(".");
|
||||
|
||||
// check for different match types: exact matches of full name or
|
||||
// "last name" (i.e. last dotted part)
|
||||
if (fullnameLower === object || parts.slice(-1)[0] === object)
|
||||
score += Scorer.objNameMatch;
|
||||
else if (parts.slice(-1)[0].indexOf(object) > -1)
|
||||
score += Scorer.objPartialMatch; // matches in last name
|
||||
|
||||
const objName = objNames[match[1]][2];
|
||||
const title = titles[match[0]];
|
||||
|
||||
// If more than one term searched for, we require other words to be
|
||||
// found in the name/title/description
|
||||
const otherTerms = new Set(objectTerms);
|
||||
otherTerms.delete(object);
|
||||
if (otherTerms.size > 0) {
|
||||
const haystack = `${prefix} ${name} ${objName} ${title}`.toLowerCase();
|
||||
if (
|
||||
[...otherTerms].some((otherTerm) => haystack.indexOf(otherTerm) < 0)
|
||||
)
|
||||
return;
|
||||
}
|
||||
|
||||
let anchor = match[3];
|
||||
if (anchor === "") anchor = fullname;
|
||||
else if (anchor === "-") anchor = objNames[match[1]][1] + "-" + fullname;
|
||||
|
||||
const descr = objName + _(", in ") + title;
|
||||
|
||||
// add custom score for some objects according to scorer
|
||||
if (Scorer.objPrio.hasOwnProperty(match[2]))
|
||||
score += Scorer.objPrio[match[2]];
|
||||
else score += Scorer.objPrioDefault;
|
||||
|
||||
results.push([
|
||||
docNames[match[0]],
|
||||
fullname,
|
||||
"#" + anchor,
|
||||
descr,
|
||||
score,
|
||||
filenames[match[0]],
|
||||
SearchResultKind.object,
|
||||
]);
|
||||
};
|
||||
Object.keys(objects).forEach((prefix) =>
|
||||
objects[prefix].forEach((array) =>
|
||||
objectSearchCallback(prefix, array)
|
||||
)
|
||||
);
|
||||
return results;
|
||||
},
|
||||
|
||||
/**
|
||||
* search for full-text terms in the index
|
||||
*/
|
||||
performTermsSearch: (searchTerms, excludedTerms) => {
|
||||
// prepare search
|
||||
const terms = Search._index.terms;
|
||||
const titleTerms = Search._index.titleterms;
|
||||
const filenames = Search._index.filenames;
|
||||
const docNames = Search._index.docnames;
|
||||
const titles = Search._index.titles;
|
||||
|
||||
const scoreMap = new Map();
|
||||
const fileMap = new Map();
|
||||
|
||||
// perform the search on the required terms
|
||||
searchTerms.forEach((word) => {
|
||||
const files = [];
|
||||
// find documents, if any, containing the query word in their text/title term indices
|
||||
// use Object.hasOwnProperty to avoid mismatching against prototype properties
|
||||
const arr = [
|
||||
{ files: terms.hasOwnProperty(word) ? terms[word] : undefined, score: Scorer.term },
|
||||
{ files: titleTerms.hasOwnProperty(word) ? titleTerms[word] : undefined, score: Scorer.title },
|
||||
];
|
||||
// add support for partial matches
|
||||
if (word.length > 2) {
|
||||
const escapedWord = _escapeRegExp(word);
|
||||
if (!terms.hasOwnProperty(word)) {
|
||||
Object.keys(terms).forEach((term) => {
|
||||
if (term.match(escapedWord))
|
||||
arr.push({ files: terms[term], score: Scorer.partialTerm });
|
||||
});
|
||||
}
|
||||
if (!titleTerms.hasOwnProperty(word)) {
|
||||
Object.keys(titleTerms).forEach((term) => {
|
||||
if (term.match(escapedWord))
|
||||
arr.push({ files: titleTerms[term], score: Scorer.partialTitle });
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// no match but word was a required one
|
||||
if (arr.every((record) => record.files === undefined)) return;
|
||||
|
||||
// found search word in contents
|
||||
arr.forEach((record) => {
|
||||
if (record.files === undefined) return;
|
||||
|
||||
let recordFiles = record.files;
|
||||
if (recordFiles.length === undefined) recordFiles = [recordFiles];
|
||||
files.push(...recordFiles);
|
||||
|
||||
// set score for the word in each file
|
||||
recordFiles.forEach((file) => {
|
||||
if (!scoreMap.has(file)) scoreMap.set(file, new Map());
|
||||
const fileScores = scoreMap.get(file);
|
||||
fileScores.set(word, record.score);
|
||||
});
|
||||
});
|
||||
|
||||
// create the mapping
|
||||
files.forEach((file) => {
|
||||
if (!fileMap.has(file)) fileMap.set(file, [word]);
|
||||
else if (fileMap.get(file).indexOf(word) === -1) fileMap.get(file).push(word);
|
||||
});
|
||||
});
|
||||
|
||||
// now check if the files don't contain excluded terms
|
||||
const results = [];
|
||||
for (const [file, wordList] of fileMap) {
|
||||
// check if all requirements are matched
|
||||
|
||||
// as search terms with length < 3 are discarded
|
||||
const filteredTermCount = [...searchTerms].filter(
|
||||
(term) => term.length > 2
|
||||
).length;
|
||||
if (
|
||||
wordList.length !== searchTerms.size &&
|
||||
wordList.length !== filteredTermCount
|
||||
)
|
||||
continue;
|
||||
|
||||
// ensure that none of the excluded terms is in the search result
|
||||
if (
|
||||
[...excludedTerms].some(
|
||||
(term) =>
|
||||
terms[term] === file ||
|
||||
titleTerms[term] === file ||
|
||||
(terms[term] || []).includes(file) ||
|
||||
(titleTerms[term] || []).includes(file)
|
||||
)
|
||||
)
|
||||
break;
|
||||
|
||||
// select one (max) score for the file.
|
||||
const score = Math.max(...wordList.map((w) => scoreMap.get(file).get(w)));
|
||||
// add result to the result list
|
||||
results.push([
|
||||
docNames[file],
|
||||
titles[file],
|
||||
"",
|
||||
null,
|
||||
score,
|
||||
filenames[file],
|
||||
SearchResultKind.text,
|
||||
]);
|
||||
}
|
||||
return results;
|
||||
},
|
||||
|
||||
/**
|
||||
* helper function to return a node containing the
|
||||
* search summary for a given text. keywords is a list
|
||||
* of stemmed words.
|
||||
*/
|
||||
makeSearchSummary: (htmlText, keywords, anchor) => {
|
||||
const text = Search.htmlToText(htmlText, anchor);
|
||||
if (text === "") return null;
|
||||
|
||||
const textLower = text.toLowerCase();
|
||||
const actualStartPosition = [...keywords]
|
||||
.map((k) => textLower.indexOf(k.toLowerCase()))
|
||||
.filter((i) => i > -1)
|
||||
.slice(-1)[0];
|
||||
const startWithContext = Math.max(actualStartPosition - 120, 0);
|
||||
|
||||
const top = startWithContext === 0 ? "" : "...";
|
||||
const tail = startWithContext + 240 < text.length ? "..." : "";
|
||||
|
||||
let summary = document.createElement("p");
|
||||
summary.classList.add("context");
|
||||
summary.textContent = top + text.substr(startWithContext, 240).trim() + tail;
|
||||
|
||||
return summary;
|
||||
},
|
||||
};
|
||||
|
||||
_ready(Search.init);
|
||||
@@ -0,0 +1,154 @@
|
||||
/* Highlighting utilities for Sphinx HTML documentation. */
|
||||
"use strict";
|
||||
|
||||
const SPHINX_HIGHLIGHT_ENABLED = true
|
||||
|
||||
/**
|
||||
* highlight a given string on a node by wrapping it in
|
||||
* span elements with the given class name.
|
||||
*/
|
||||
const _highlight = (node, addItems, text, className) => {
|
||||
if (node.nodeType === Node.TEXT_NODE) {
|
||||
const val = node.nodeValue;
|
||||
const parent = node.parentNode;
|
||||
const pos = val.toLowerCase().indexOf(text);
|
||||
if (
|
||||
pos >= 0 &&
|
||||
!parent.classList.contains(className) &&
|
||||
!parent.classList.contains("nohighlight")
|
||||
) {
|
||||
let span;
|
||||
|
||||
const closestNode = parent.closest("body, svg, foreignObject");
|
||||
const isInSVG = closestNode && closestNode.matches("svg");
|
||||
if (isInSVG) {
|
||||
span = document.createElementNS("http://www.w3.org/2000/svg", "tspan");
|
||||
} else {
|
||||
span = document.createElement("span");
|
||||
span.classList.add(className);
|
||||
}
|
||||
|
||||
span.appendChild(document.createTextNode(val.substr(pos, text.length)));
|
||||
const rest = document.createTextNode(val.substr(pos + text.length));
|
||||
parent.insertBefore(
|
||||
span,
|
||||
parent.insertBefore(
|
||||
rest,
|
||||
node.nextSibling
|
||||
)
|
||||
);
|
||||
node.nodeValue = val.substr(0, pos);
|
||||
/* There may be more occurrences of search term in this node. So call this
|
||||
* function recursively on the remaining fragment.
|
||||
*/
|
||||
_highlight(rest, addItems, text, className);
|
||||
|
||||
if (isInSVG) {
|
||||
const rect = document.createElementNS(
|
||||
"http://www.w3.org/2000/svg",
|
||||
"rect"
|
||||
);
|
||||
const bbox = parent.getBBox();
|
||||
rect.x.baseVal.value = bbox.x;
|
||||
rect.y.baseVal.value = bbox.y;
|
||||
rect.width.baseVal.value = bbox.width;
|
||||
rect.height.baseVal.value = bbox.height;
|
||||
rect.setAttribute("class", className);
|
||||
addItems.push({ parent: parent, target: rect });
|
||||
}
|
||||
}
|
||||
} else if (node.matches && !node.matches("button, select, textarea")) {
|
||||
node.childNodes.forEach((el) => _highlight(el, addItems, text, className));
|
||||
}
|
||||
};
|
||||
const _highlightText = (thisNode, text, className) => {
|
||||
let addItems = [];
|
||||
_highlight(thisNode, addItems, text, className);
|
||||
addItems.forEach((obj) =>
|
||||
obj.parent.insertAdjacentElement("beforebegin", obj.target)
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Small JavaScript module for the documentation.
|
||||
*/
|
||||
const SphinxHighlight = {
|
||||
|
||||
/**
|
||||
* highlight the search words provided in localstorage in the text
|
||||
*/
|
||||
highlightSearchWords: () => {
|
||||
if (!SPHINX_HIGHLIGHT_ENABLED) return; // bail if no highlight
|
||||
|
||||
// get and clear terms from localstorage
|
||||
const url = new URL(window.location);
|
||||
const highlight =
|
||||
localStorage.getItem("sphinx_highlight_terms")
|
||||
|| url.searchParams.get("highlight")
|
||||
|| "";
|
||||
localStorage.removeItem("sphinx_highlight_terms")
|
||||
url.searchParams.delete("highlight");
|
||||
window.history.replaceState({}, "", url);
|
||||
|
||||
// get individual terms from highlight string
|
||||
const terms = highlight.toLowerCase().split(/\s+/).filter(x => x);
|
||||
if (terms.length === 0) return; // nothing to do
|
||||
|
||||
// There should never be more than one element matching "div.body"
|
||||
const divBody = document.querySelectorAll("div.body");
|
||||
const body = divBody.length ? divBody[0] : document.querySelector("body");
|
||||
window.setTimeout(() => {
|
||||
terms.forEach((term) => _highlightText(body, term, "highlighted"));
|
||||
}, 10);
|
||||
|
||||
const searchBox = document.getElementById("searchbox");
|
||||
if (searchBox === null) return;
|
||||
searchBox.appendChild(
|
||||
document
|
||||
.createRange()
|
||||
.createContextualFragment(
|
||||
'<p class="highlight-link">' +
|
||||
'<a href="javascript:SphinxHighlight.hideSearchWords()">' +
|
||||
_("Hide Search Matches") +
|
||||
"</a></p>"
|
||||
)
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* helper function to hide the search marks again
|
||||
*/
|
||||
hideSearchWords: () => {
|
||||
document
|
||||
.querySelectorAll("#searchbox .highlight-link")
|
||||
.forEach((el) => el.remove());
|
||||
document
|
||||
.querySelectorAll("span.highlighted")
|
||||
.forEach((el) => el.classList.remove("highlighted"));
|
||||
localStorage.removeItem("sphinx_highlight_terms")
|
||||
},
|
||||
|
||||
initEscapeListener: () => {
|
||||
// only install a listener if it is really needed
|
||||
if (!DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS) return;
|
||||
|
||||
document.addEventListener("keydown", (event) => {
|
||||
// bail for input elements
|
||||
if (BLACKLISTED_KEY_CONTROL_ELEMENTS.has(document.activeElement.tagName)) return;
|
||||
// bail with special keys
|
||||
if (event.shiftKey || event.altKey || event.ctrlKey || event.metaKey) return;
|
||||
if (DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS && (event.key === "Escape")) {
|
||||
SphinxHighlight.hideSearchWords();
|
||||
event.preventDefault();
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
_ready(() => {
|
||||
/* Do not call highlightSearchWords() when we are on the search page.
|
||||
* It will highlight words from the *previous* search query.
|
||||
*/
|
||||
if (typeof Search === "undefined") SphinxHighlight.highlightSearchWords();
|
||||
SphinxHighlight.initEscapeListener();
|
||||
});
|
||||
@@ -0,0 +1,114 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html lang="en" data-content_root="./">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" /><meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
|
||||
<title>Backlog — responder 3.6.2 documentation</title>
|
||||
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=5ecbeea2" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/basic.css?v=b08954a9" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/alabaster.css?v=27fed22d" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/copybutton.css?v=76b2166b" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/design-elements.e5416f61bae5d36adc6d722a2b6f8cff.css?v=452a8e97" />
|
||||
<script src="_static/documentation_options.js?v=c0c9fa11"></script>
|
||||
<script src="_static/doctools.js?v=9bcbadda"></script>
|
||||
<script src="_static/sphinx_highlight.js?v=dc90522c"></script>
|
||||
<script src="_static/clipboard.min.js?v=a7894cd8"></script>
|
||||
<script src="_static/copybutton.js?v=fd10adb8"></script>
|
||||
<script>
|
||||
</script>
|
||||
<script src="_static/design-elements.bbdccc18c4abea9397628f9fea3d48c2.js?v=03c7770e"></script>
|
||||
<link rel="index" title="Index" href="genindex.html" />
|
||||
<link rel="search" title="Search" href="search.html" />
|
||||
<link rel="prev" title="Development Sandbox" href="sandbox.html" />
|
||||
|
||||
<link rel="stylesheet" href="_static/custom.css" type="text/css" />
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</head><body>
|
||||
|
||||
|
||||
<div class="document">
|
||||
<div class="documentwrapper">
|
||||
<div class="bodywrapper">
|
||||
|
||||
|
||||
<div class="body" role="main">
|
||||
|
||||
<section id="backlog">
|
||||
<h1>Backlog<a class="headerlink" href="#backlog" title="Link to this heading">¶</a></h1>
|
||||
<section id="future-ideas">
|
||||
<h2>Future Ideas<a class="headerlink" href="#future-ideas" title="Link to this heading">¶</a></h2>
|
||||
<ul class="simple">
|
||||
<li><p>WebSocket before_request short-circuit support (reject before accept)</p></li>
|
||||
<li><p>Per-route rate limiting (different limits for different endpoints)</p></li>
|
||||
<li><p>Built-in structured logging with request context</p></li>
|
||||
<li><p>OpenAPI 3.1 support</p></li>
|
||||
<li><p>Dependency injection for route handlers</p></li>
|
||||
</ul>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="sphinxsidebar" role="navigation" aria-label="Main">
|
||||
<div class="sphinxsidebarwrapper"><p class="logo">
|
||||
<a href="index.html">
|
||||
<img class="logo" src="_static/responder.png" />
|
||||
</a>
|
||||
</p>
|
||||
<p>
|
||||
<strong>Responder</strong> — a familiar HTTP service framework for Python.
|
||||
<br />
|
||||
<small>v3.6.2</small>
|
||||
</p>
|
||||
<h3>Useful Links</h3>
|
||||
<ul>
|
||||
<li><a href="https://github.com/kennethreitz/responder">Responder @ GitHub</a></li>
|
||||
<li><a href="https://pypi.org/project/responder/">Responder @ PyPI</a></li>
|
||||
<li><a href="https://github.com/kennethreitz/responder/issues">Issue Tracker</a></li>
|
||||
</ul>
|
||||
<div>
|
||||
<h3><a href="index.html">Table of Contents</a></h3>
|
||||
<ul>
|
||||
<li><a class="reference internal" href="#">Backlog</a><ul>
|
||||
<li><a class="reference internal" href="#future-ideas">Future Ideas</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
<search id="searchbox" style="display: none" role="search">
|
||||
<h3 id="searchlabel">Quick search</h3>
|
||||
<div class="searchformwrapper">
|
||||
<form class="search" action="search.html" method="get">
|
||||
<input type="text" name="q" aria-labelledby="searchlabel" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"/>
|
||||
<input type="submit" value="Go" />
|
||||
</form>
|
||||
</div>
|
||||
</search>
|
||||
<script>document.getElementById('searchbox').style.display = "block"</script>
|
||||
</div>
|
||||
</div>
|
||||
<div class="clearer"></div>
|
||||
</div>
|
||||
<div class="footer">
|
||||
©2018-2026, Kenneth Reitz.
|
||||
|
||||
|
|
||||
<a href="_sources/backlog.md.txt"
|
||||
rel="nofollow">Page source</a>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,198 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html lang="en" data-content_root="./">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" /><meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
|
||||
<title>Command Line Interface — responder 3.6.2 documentation</title>
|
||||
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=5ecbeea2" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/basic.css?v=b08954a9" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/alabaster.css?v=27fed22d" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/copybutton.css?v=76b2166b" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/design-elements.e5416f61bae5d36adc6d722a2b6f8cff.css?v=452a8e97" />
|
||||
<script src="_static/documentation_options.js?v=c0c9fa11"></script>
|
||||
<script src="_static/doctools.js?v=9bcbadda"></script>
|
||||
<script src="_static/sphinx_highlight.js?v=dc90522c"></script>
|
||||
<script src="_static/clipboard.min.js?v=a7894cd8"></script>
|
||||
<script src="_static/copybutton.js?v=fd10adb8"></script>
|
||||
<script>
|
||||
</script>
|
||||
<script src="_static/design-elements.bbdccc18c4abea9397628f9fea3d48c2.js?v=03c7770e"></script>
|
||||
<link rel="index" title="Index" href="genindex.html" />
|
||||
<link rel="search" title="Search" href="search.html" />
|
||||
<link rel="next" title="Building a REST API" href="tutorial-rest.html" />
|
||||
<link rel="prev" title="API Reference" href="api.html" />
|
||||
|
||||
<link rel="stylesheet" href="_static/custom.css" type="text/css" />
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</head><body>
|
||||
|
||||
|
||||
<div class="document">
|
||||
<div class="documentwrapper">
|
||||
<div class="bodywrapper">
|
||||
|
||||
|
||||
<div class="body" role="main">
|
||||
|
||||
<section id="command-line-interface">
|
||||
<h1>Command Line Interface<a class="headerlink" href="#command-line-interface" title="Link to this heading">¶</a></h1>
|
||||
<p>Responder installs a <code class="docutils literal notranslate"><span class="pre">responder</span></code> command that lets you launch
|
||||
applications from the terminal. You can point it at a Python module,
|
||||
a local file, or even a URL — and it will find your <code class="docutils literal notranslate"><span class="pre">API</span></code> instance
|
||||
and start serving.</p>
|
||||
<section id="launching-from-a-module">
|
||||
<h2>Launching from a Module<a class="headerlink" href="#launching-from-a-module" title="Link to this heading">¶</a></h2>
|
||||
<p>The most common way to run a Responder application in production. Use
|
||||
Python’s standard dotted module path:</p>
|
||||
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span>$ responder run acme.app
|
||||
</pre></div>
|
||||
</div>
|
||||
<p>This imports <code class="docutils literal notranslate"><span class="pre">acme.app</span></code> and looks for an attribute called <code class="docutils literal notranslate"><span class="pre">api</span></code>
|
||||
(a <code class="docutils literal notranslate"><span class="pre">responder.API</span></code> instance). It’s the same import system Python
|
||||
uses everywhere — your <code class="docutils literal notranslate"><span class="pre">PYTHONPATH</span></code> and virtual environment are
|
||||
respected.</p>
|
||||
</section>
|
||||
<section id="launching-from-a-file">
|
||||
<h2>Launching from a File<a class="headerlink" href="#launching-from-a-file" title="Link to this heading">¶</a></h2>
|
||||
<p>During development, you often have a single file you want to run:</p>
|
||||
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span>$ responder run helloworld.py
|
||||
</pre></div>
|
||||
</div>
|
||||
<p>This loads the file directly and starts the server. Quick and easy for
|
||||
prototyping and single-file applications.</p>
|
||||
<p>You can test it with a simple HTTP request:</p>
|
||||
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span>$ curl http://127.0.0.1:5042/hello
|
||||
hello, world!
|
||||
</pre></div>
|
||||
</div>
|
||||
</section>
|
||||
<section id="launching-from-a-url">
|
||||
<h2>Launching from a URL<a class="headerlink" href="#launching-from-a-url" title="Link to this heading">¶</a></h2>
|
||||
<p>Responder can fetch and run a Python file from any URL — great for
|
||||
demos, sharing examples, and running code from GitHub:</p>
|
||||
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span>$ responder run https://github.com/kennethreitz/responder/raw/refs/heads/main/examples/helloworld.py
|
||||
</pre></div>
|
||||
</div>
|
||||
<p>This also works with <code class="docutils literal notranslate"><span class="pre">github://</span></code> URLs and any filesystem protocol
|
||||
supported by <a class="reference external" href="https://filesystem-spec.readthedocs.io/">fsspec</a>:</p>
|
||||
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span>$ responder run github://kennethreitz:responder@/examples/helloworld.py
|
||||
</pre></div>
|
||||
</div>
|
||||
<p>Cloud storage is supported too — Azure Blob Storage, Google Cloud
|
||||
Storage, S3, HDFS, SFTP, and more. Install <code class="docutils literal notranslate"><span class="pre">fsspec[full]</span></code> for all
|
||||
protocols:</p>
|
||||
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span>$ uv pip install 'fsspec[full]'
|
||||
</pre></div>
|
||||
</div>
|
||||
</section>
|
||||
<section id="custom-instance-names">
|
||||
<h2>Custom Instance Names<a class="headerlink" href="#custom-instance-names" title="Link to this heading">¶</a></h2>
|
||||
<p>By default, Responder looks for an attribute called <code class="docutils literal notranslate"><span class="pre">api</span></code>. If your
|
||||
application uses a different name, specify it with a colon:</p>
|
||||
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span>$ responder run acme.app:service
|
||||
$ responder run myapp.py:application
|
||||
</pre></div>
|
||||
</div>
|
||||
<p>For URLs, use a fragment:</p>
|
||||
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span>$ responder run https://example.com/app.py#service
|
||||
</pre></div>
|
||||
</div>
|
||||
</section>
|
||||
<section id="environment-variables">
|
||||
<h2>Environment Variables<a class="headerlink" href="#environment-variables" title="Link to this heading">¶</a></h2>
|
||||
<p>Responder automatically reads the <code class="docutils literal notranslate"><span class="pre">PORT</span></code> environment variable at
|
||||
runtime:</p>
|
||||
<ul class="simple">
|
||||
<li><p><code class="docutils literal notranslate"><span class="pre">PORT</span></code> — bind to <code class="docutils literal notranslate"><span class="pre">0.0.0.0</span></code> on this port (cloud platform convention)</p></li>
|
||||
</ul>
|
||||
<p>When <code class="docutils literal notranslate"><span class="pre">PORT</span></code> is set, the server binds to all interfaces automatically.
|
||||
This is how cloud platforms like Fly.io, Railway, and Heroku inject the
|
||||
listen port.</p>
|
||||
<p>For other settings like <code class="docutils literal notranslate"><span class="pre">SECRET_KEY</span></code>, read them in your application
|
||||
code and pass them to <code class="docutils literal notranslate"><span class="pre">responder.API()</span></code>:</p>
|
||||
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="kn">import</span><span class="w"> </span><span class="nn">os</span>
|
||||
<span class="n">api</span> <span class="o">=</span> <span class="n">responder</span><span class="o">.</span><span class="n">API</span><span class="p">(</span><span class="n">secret_key</span><span class="o">=</span><span class="n">os</span><span class="o">.</span><span class="n">environ</span><span class="p">[</span><span class="s2">"SECRET_KEY"</span><span class="p">])</span>
|
||||
</pre></div>
|
||||
</div>
|
||||
</section>
|
||||
<section id="building-frontend-assets">
|
||||
<h2>Building Frontend Assets<a class="headerlink" href="#building-frontend-assets" title="Link to this heading">¶</a></h2>
|
||||
<p>If your project includes a JavaScript frontend with a <code class="docutils literal notranslate"><span class="pre">package.json</span></code>,
|
||||
the <code class="docutils literal notranslate"><span class="pre">build</span></code> subcommand runs <code class="docutils literal notranslate"><span class="pre">npm</span> <span class="pre">run</span> <span class="pre">build</span></code>:</p>
|
||||
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span>$ responder build
|
||||
$ responder build /path/to/frontend
|
||||
</pre></div>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="sphinxsidebar" role="navigation" aria-label="Main">
|
||||
<div class="sphinxsidebarwrapper"><p class="logo">
|
||||
<a href="index.html">
|
||||
<img class="logo" src="_static/responder.png" />
|
||||
</a>
|
||||
</p>
|
||||
<p>
|
||||
<strong>Responder</strong> — a familiar HTTP service framework for Python.
|
||||
<br />
|
||||
<small>v3.6.2</small>
|
||||
</p>
|
||||
<h3>Useful Links</h3>
|
||||
<ul>
|
||||
<li><a href="https://github.com/kennethreitz/responder">Responder @ GitHub</a></li>
|
||||
<li><a href="https://pypi.org/project/responder/">Responder @ PyPI</a></li>
|
||||
<li><a href="https://github.com/kennethreitz/responder/issues">Issue Tracker</a></li>
|
||||
</ul>
|
||||
<div>
|
||||
<h3><a href="index.html">Table of Contents</a></h3>
|
||||
<ul>
|
||||
<li><a class="reference internal" href="#">Command Line Interface</a><ul>
|
||||
<li><a class="reference internal" href="#launching-from-a-module">Launching from a Module</a></li>
|
||||
<li><a class="reference internal" href="#launching-from-a-file">Launching from a File</a></li>
|
||||
<li><a class="reference internal" href="#launching-from-a-url">Launching from a URL</a></li>
|
||||
<li><a class="reference internal" href="#custom-instance-names">Custom Instance Names</a></li>
|
||||
<li><a class="reference internal" href="#environment-variables">Environment Variables</a></li>
|
||||
<li><a class="reference internal" href="#building-frontend-assets">Building Frontend Assets</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
<search id="searchbox" style="display: none" role="search">
|
||||
<h3 id="searchlabel">Quick search</h3>
|
||||
<div class="searchformwrapper">
|
||||
<form class="search" action="search.html" method="get">
|
||||
<input type="text" name="q" aria-labelledby="searchlabel" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"/>
|
||||
<input type="submit" value="Go" />
|
||||
</form>
|
||||
</div>
|
||||
</search>
|
||||
<script>document.getElementById('searchbox').style.display = "block"</script>
|
||||
</div>
|
||||
</div>
|
||||
<div class="clearer"></div>
|
||||
</div>
|
||||
<div class="footer">
|
||||
©2018-2026, Kenneth Reitz.
|
||||
|
||||
|
|
||||
<a href="_sources/cli.rst.txt"
|
||||
rel="nofollow">Page source</a>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,278 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html lang="en" data-content_root="./">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" /><meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
|
||||
<title>Deployment — responder 3.6.2 documentation</title>
|
||||
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=5ecbeea2" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/basic.css?v=b08954a9" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/alabaster.css?v=27fed22d" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/copybutton.css?v=76b2166b" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/design-elements.e5416f61bae5d36adc6d722a2b6f8cff.css?v=452a8e97" />
|
||||
<script src="_static/documentation_options.js?v=c0c9fa11"></script>
|
||||
<script src="_static/doctools.js?v=9bcbadda"></script>
|
||||
<script src="_static/sphinx_highlight.js?v=dc90522c"></script>
|
||||
<script src="_static/clipboard.min.js?v=a7894cd8"></script>
|
||||
<script src="_static/copybutton.js?v=fd10adb8"></script>
|
||||
<script>
|
||||
</script>
|
||||
<script src="_static/design-elements.bbdccc18c4abea9397628f9fea3d48c2.js?v=03c7770e"></script>
|
||||
<link rel="index" title="Index" href="genindex.html" />
|
||||
<link rel="search" title="Search" href="search.html" />
|
||||
<link rel="next" title="Testing" href="testing.html" />
|
||||
<link rel="prev" title="Feature Tour" href="tour.html" />
|
||||
|
||||
<link rel="stylesheet" href="_static/custom.css" type="text/css" />
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</head><body>
|
||||
|
||||
|
||||
<div class="document">
|
||||
<div class="documentwrapper">
|
||||
<div class="bodywrapper">
|
||||
|
||||
|
||||
<div class="body" role="main">
|
||||
|
||||
<section id="deployment">
|
||||
<h1>Deployment<a class="headerlink" href="#deployment" title="Link to this heading">¶</a></h1>
|
||||
<p>Responder applications are standard <a class="reference external" href="https://asgi.readthedocs.io/">ASGI</a>
|
||||
apps. ASGI (Asynchronous Server Gateway Interface) is the modern successor
|
||||
to WSGI — it supports async, WebSockets, and HTTP/2. This means you can
|
||||
deploy a Responder app anywhere that runs Python, using any ASGI server.</p>
|
||||
<section id="running-locally">
|
||||
<h2>Running Locally<a class="headerlink" href="#running-locally" title="Link to this heading">¶</a></h2>
|
||||
<p>During development, <code class="docutils literal notranslate"><span class="pre">api.run()</span></code> is all you need:</p>
|
||||
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="k">if</span> <span class="vm">__name__</span> <span class="o">==</span> <span class="s2">"__main__"</span><span class="p">:</span>
|
||||
<span class="n">api</span><span class="o">.</span><span class="n">run</span><span class="p">()</span>
|
||||
</pre></div>
|
||||
</div>
|
||||
<p>This starts a <a class="reference external" href="https://www.uvicorn.org/">uvicorn</a> server on
|
||||
<code class="docutils literal notranslate"><span class="pre">127.0.0.1:5042</span></code>. Uvicorn is a lightning-fast ASGI server built on
|
||||
<a class="reference external" href="https://uvloop.readthedocs.io/">uvloop</a> — it handles thousands of
|
||||
concurrent connections efficiently and protects against slowloris attacks,
|
||||
making a reverse proxy like nginx optional for many deployments.</p>
|
||||
</section>
|
||||
<section id="docker">
|
||||
<h2>Docker<a class="headerlink" href="#docker" title="Link to this heading">¶</a></h2>
|
||||
<p>Docker is the most common way to package and deploy web applications.
|
||||
Here’s a minimal Dockerfile:</p>
|
||||
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="n">FROM</span> <span class="n">python</span><span class="p">:</span><span class="mf">3.13</span><span class="o">-</span><span class="n">slim</span>
|
||||
<span class="n">WORKDIR</span> <span class="o">/</span><span class="n">app</span>
|
||||
<span class="n">COPY</span> <span class="o">--</span><span class="n">from</span><span class="o">=</span><span class="n">ghcr</span><span class="o">.</span><span class="n">io</span><span class="o">/</span><span class="n">astral</span><span class="o">-</span><span class="n">sh</span><span class="o">/</span><span class="n">uv</span><span class="p">:</span><span class="n">latest</span> <span class="o">/</span><span class="n">uv</span> <span class="o">/</span><span class="n">usr</span><span class="o">/</span><span class="n">local</span><span class="o">/</span><span class="nb">bin</span><span class="o">/</span><span class="n">uv</span>
|
||||
<span class="n">COPY</span> <span class="o">.</span> <span class="o">.</span>
|
||||
<span class="n">RUN</span> <span class="n">uv</span> <span class="n">pip</span> <span class="n">install</span> <span class="o">--</span><span class="n">system</span> <span class="n">responder</span>
|
||||
<span class="n">ENV</span> <span class="n">PORT</span><span class="o">=</span><span class="mi">80</span>
|
||||
<span class="n">EXPOSE</span> <span class="mi">80</span>
|
||||
<span class="n">CMD</span> <span class="p">[</span><span class="s2">"python"</span><span class="p">,</span> <span class="s2">"api.py"</span><span class="p">]</span>
|
||||
</pre></div>
|
||||
</div>
|
||||
<p>Build and run:</p>
|
||||
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span>$ docker build -t myapi .
|
||||
$ docker run -p 8000:80 myapi
|
||||
</pre></div>
|
||||
</div>
|
||||
<p>The <code class="docutils literal notranslate"><span class="pre">python:3.13-slim</span></code> image is about 150MB — small enough for fast
|
||||
deploys but includes everything you need. Using <code class="docutils literal notranslate"><span class="pre">uv</span></code> for installs
|
||||
is significantly faster than pip. For even smaller images, you can use
|
||||
<code class="docutils literal notranslate"><span class="pre">python:3.13-alpine</span></code>, though some packages may need extra build
|
||||
dependencies.</p>
|
||||
</section>
|
||||
<section id="cloud-platforms">
|
||||
<h2>Cloud Platforms<a class="headerlink" href="#cloud-platforms" title="Link to this heading">¶</a></h2>
|
||||
<p>Responder automatically honors the <code class="docutils literal notranslate"><span class="pre">PORT</span></code> environment variable. When
|
||||
<code class="docutils literal notranslate"><span class="pre">PORT</span></code> is set, the server binds to <code class="docutils literal notranslate"><span class="pre">0.0.0.0</span></code> on that port — this is
|
||||
the convention that virtually every cloud platform uses.</p>
|
||||
<p>This means zero configuration on:</p>
|
||||
<ul class="simple">
|
||||
<li><p><strong>Fly.io</strong> — <code class="docutils literal notranslate"><span class="pre">fly</span> <span class="pre">launch</span></code> and you’re done</p></li>
|
||||
<li><p><strong>Railway</strong> — push your code, Railway sets <code class="docutils literal notranslate"><span class="pre">PORT</span></code></p></li>
|
||||
<li><p><strong>Render</strong> — set start command to <code class="docutils literal notranslate"><span class="pre">python</span> <span class="pre">api.py</span></code></p></li>
|
||||
<li><p><strong>Google Cloud Run</strong> — containerize and deploy</p></li>
|
||||
<li><p><strong>Azure Container Apps</strong> — same pattern</p></li>
|
||||
<li><p><strong>AWS App Runner</strong> — and here too</p></li>
|
||||
</ul>
|
||||
<p>The pattern is always the same: deploy your code, set the start command
|
||||
to <code class="docutils literal notranslate"><span class="pre">python</span> <span class="pre">api.py</span></code>, and the platform handles the rest.</p>
|
||||
</section>
|
||||
<section id="health-check-endpoint">
|
||||
<h2>Health Check Endpoint<a class="headerlink" href="#health-check-endpoint" title="Link to this heading">¶</a></h2>
|
||||
<p>Every production deployment needs a health check — a lightweight endpoint
|
||||
that monitoring tools, load balancers, and orchestrators can poll to verify
|
||||
your service is running:</p>
|
||||
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="nd">@api</span><span class="o">.</span><span class="n">route</span><span class="p">(</span><span class="s2">"/health"</span><span class="p">)</span>
|
||||
<span class="k">def</span><span class="w"> </span><span class="nf">health</span><span class="p">(</span><span class="n">req</span><span class="p">,</span> <span class="n">resp</span><span class="p">):</span>
|
||||
<span class="n">resp</span><span class="o">.</span><span class="n">media</span> <span class="o">=</span> <span class="p">{</span><span class="s2">"status"</span><span class="p">:</span> <span class="s2">"healthy"</span><span class="p">}</span>
|
||||
</pre></div>
|
||||
</div>
|
||||
<p>Keep it simple. Don’t query the database or do expensive work — the health
|
||||
check should return instantly. Cloud platforms, Docker, and Kubernetes all
|
||||
look for an HTTP 200 to confirm your service is alive.</p>
|
||||
<p>For Docker, add a <code class="docutils literal notranslate"><span class="pre">HEALTHCHECK</span></code> instruction:</p>
|
||||
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="n">HEALTHCHECK</span> <span class="o">--</span><span class="n">interval</span><span class="o">=</span><span class="mi">30</span><span class="n">s</span> <span class="o">--</span><span class="n">timeout</span><span class="o">=</span><span class="mi">3</span><span class="n">s</span> \
|
||||
<span class="n">CMD</span> <span class="n">curl</span> <span class="o">-</span><span class="n">f</span> <span class="n">http</span><span class="p">:</span><span class="o">//</span><span class="n">localhost</span><span class="o">/</span><span class="n">health</span> <span class="o">||</span> <span class="n">exit</span> <span class="mi">1</span>
|
||||
</pre></div>
|
||||
</div>
|
||||
</section>
|
||||
<section id="uvicorn-directly">
|
||||
<h2>Uvicorn Directly<a class="headerlink" href="#uvicorn-directly" title="Link to this heading">¶</a></h2>
|
||||
<p>For production deployments where you want more control, bypass
|
||||
<code class="docutils literal notranslate"><span class="pre">api.run()</span></code> and use uvicorn directly:</p>
|
||||
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span>$ uvicorn api:api --host 0.0.0.0 --port 8000 --workers 4
|
||||
</pre></div>
|
||||
</div>
|
||||
<p>The <code class="docutils literal notranslate"><span class="pre">--workers</span></code> flag spawns multiple processes, each handling requests
|
||||
independently. A good starting point is 2-4 workers per CPU core.</p>
|
||||
<p>Uvicorn supports many options — SSL certificates, access logging, graceful
|
||||
shutdown timeouts, and more. See the
|
||||
<a class="reference external" href="https://www.uvicorn.org/deployment/">uvicorn documentation</a> for details.</p>
|
||||
<p>For platforms like Heroku or Railway that use a <code class="docutils literal notranslate"><span class="pre">Procfile</span></code>:</p>
|
||||
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span>web: uvicorn api:api --host 0.0.0.0 --port $PORT --workers 4
|
||||
</pre></div>
|
||||
</div>
|
||||
</section>
|
||||
<section id="docker-compose">
|
||||
<h2>Docker Compose<a class="headerlink" href="#docker-compose" title="Link to this heading">¶</a></h2>
|
||||
<p>For local development with databases and other services, Docker Compose
|
||||
ties everything together:</p>
|
||||
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="c1"># docker-compose.yml</span>
|
||||
<span class="n">services</span><span class="p">:</span>
|
||||
<span class="n">api</span><span class="p">:</span>
|
||||
<span class="n">build</span><span class="p">:</span> <span class="o">.</span>
|
||||
<span class="n">ports</span><span class="p">:</span>
|
||||
<span class="o">-</span> <span class="s2">"5042:80"</span>
|
||||
<span class="n">environment</span><span class="p">:</span>
|
||||
<span class="o">-</span> <span class="n">PORT</span><span class="o">=</span><span class="mi">80</span>
|
||||
<span class="o">-</span> <span class="n">DATABASE_URL</span><span class="o">=</span><span class="n">postgresql</span><span class="o">+</span><span class="n">asyncpg</span><span class="p">:</span><span class="o">//</span><span class="n">user</span><span class="p">:</span><span class="k">pass</span><span class="nd">@db</span><span class="o">/</span><span class="n">myapp</span>
|
||||
<span class="o">-</span> <span class="n">SECRET_KEY</span><span class="o">=</span><span class="n">dev</span><span class="o">-</span><span class="n">secret</span>
|
||||
<span class="n">depends_on</span><span class="p">:</span>
|
||||
<span class="o">-</span> <span class="n">db</span>
|
||||
|
||||
<span class="n">db</span><span class="p">:</span>
|
||||
<span class="n">image</span><span class="p">:</span> <span class="n">docker</span><span class="o">.</span><span class="n">io</span><span class="o">/</span><span class="n">postgres</span><span class="p">:</span><span class="mi">16</span><span class="o">-</span><span class="n">alpine</span>
|
||||
<span class="n">environment</span><span class="p">:</span>
|
||||
<span class="n">POSTGRES_USER</span><span class="p">:</span> <span class="n">user</span>
|
||||
<span class="n">POSTGRES_PASSWORD</span><span class="p">:</span> <span class="k">pass</span>
|
||||
<span class="n">POSTGRES_DB</span><span class="p">:</span> <span class="n">myapp</span>
|
||||
<span class="n">volumes</span><span class="p">:</span>
|
||||
<span class="o">-</span> <span class="n">pgdata</span><span class="p">:</span><span class="o">/</span><span class="n">var</span><span class="o">/</span><span class="n">lib</span><span class="o">/</span><span class="n">postgresql</span><span class="o">/</span><span class="n">data</span>
|
||||
|
||||
<span class="n">volumes</span><span class="p">:</span>
|
||||
<span class="n">pgdata</span><span class="p">:</span>
|
||||
</pre></div>
|
||||
</div>
|
||||
<p>Run with <code class="docutils literal notranslate"><span class="pre">docker</span> <span class="pre">compose</span> <span class="pre">up</span></code>. The API waits for <code class="docutils literal notranslate"><span class="pre">db</span></code> to start, then
|
||||
connects using the <code class="docutils literal notranslate"><span class="pre">DATABASE_URL</span></code> environment variable.</p>
|
||||
</section>
|
||||
<section id="reverse-proxy">
|
||||
<h2>Reverse Proxy<a class="headerlink" href="#reverse-proxy" title="Link to this heading">¶</a></h2>
|
||||
<p>For high-traffic production deployments, you may want a reverse proxy like
|
||||
<a class="reference external" href="https://nginx.org/">nginx</a> or <a class="reference external" href="https://caddyserver.com/">Caddy</a> in
|
||||
front of your application for:</p>
|
||||
<ul class="simple">
|
||||
<li><p><strong>SSL/TLS termination</strong> — let the proxy handle HTTPS certificates</p></li>
|
||||
<li><p><strong>Load balancing</strong> — distribute traffic across multiple app instances</p></li>
|
||||
<li><p><strong>Static asset serving</strong> — offload static files to the proxy</p></li>
|
||||
<li><p><strong>Rate limiting</strong> — at the infrastructure level</p></li>
|
||||
</ul>
|
||||
<p>A minimal Caddy config that handles HTTPS automatically:</p>
|
||||
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="c1"># Caddyfile</span>
|
||||
<span class="n">example</span><span class="o">.</span><span class="n">com</span> <span class="p">{</span>
|
||||
<span class="n">reverse_proxy</span> <span class="n">localhost</span><span class="p">:</span><span class="mi">5042</span>
|
||||
<span class="p">}</span>
|
||||
</pre></div>
|
||||
</div>
|
||||
<p>Responder’s <code class="docutils literal notranslate"><span class="pre">TrustedHostMiddleware</span></code> and <code class="docutils literal notranslate"><span class="pre">HTTPSRedirectMiddleware</span></code> work
|
||||
correctly behind proxies that set standard forwarding headers
|
||||
(<code class="docutils literal notranslate"><span class="pre">X-Forwarded-For</span></code>, <code class="docutils literal notranslate"><span class="pre">X-Forwarded-Proto</span></code>).</p>
|
||||
<p>That said, uvicorn is production-ready on its own. Many applications run
|
||||
uvicorn directly without a reverse proxy and do just fine.</p>
|
||||
</section>
|
||||
<section id="production-checklist">
|
||||
<h2>Production Checklist<a class="headerlink" href="#production-checklist" title="Link to this heading">¶</a></h2>
|
||||
<p>Before going live:</p>
|
||||
<ul class="simple">
|
||||
<li><p><strong>Set a secret key</strong> — <code class="docutils literal notranslate"><span class="pre">SECRET_KEY</span></code> env var, never the default</p></li>
|
||||
<li><p><strong>Disable debug mode</strong> — <code class="docutils literal notranslate"><span class="pre">DEBUG=false</span></code> or omit it entirely</p></li>
|
||||
<li><p><strong>Set allowed hosts</strong> — restrict to your actual domain names</p></li>
|
||||
<li><p><strong>Use multiple workers</strong> — <code class="docutils literal notranslate"><span class="pre">--workers</span> <span class="pre">4</span></code> or more, depending on CPU cores</p></li>
|
||||
<li><p><strong>Add a health check</strong> — <code class="docutils literal notranslate"><span class="pre">/health</span></code> endpoint for monitoring</p></li>
|
||||
<li><p><strong>Enable HTTPS</strong> — via your proxy, cloud platform, or uvicorn’s <code class="docutils literal notranslate"><span class="pre">--ssl-*</span></code> flags</p></li>
|
||||
<li><p><strong>Set up logging</strong> — uvicorn logs requests by default; pipe them to your log aggregator</p></li>
|
||||
<li><p><strong>Pin your dependencies</strong> — use a lock file or pinned requirements for reproducible deploys</p></li>
|
||||
</ul>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="sphinxsidebar" role="navigation" aria-label="Main">
|
||||
<div class="sphinxsidebarwrapper"><p class="logo">
|
||||
<a href="index.html">
|
||||
<img class="logo" src="_static/responder.png" />
|
||||
</a>
|
||||
</p>
|
||||
<p>
|
||||
<strong>Responder</strong> — a familiar HTTP service framework for Python.
|
||||
<br />
|
||||
<small>v3.6.2</small>
|
||||
</p>
|
||||
<h3>Useful Links</h3>
|
||||
<ul>
|
||||
<li><a href="https://github.com/kennethreitz/responder">Responder @ GitHub</a></li>
|
||||
<li><a href="https://pypi.org/project/responder/">Responder @ PyPI</a></li>
|
||||
<li><a href="https://github.com/kennethreitz/responder/issues">Issue Tracker</a></li>
|
||||
</ul>
|
||||
<div>
|
||||
<h3><a href="index.html">Table of Contents</a></h3>
|
||||
<ul>
|
||||
<li><a class="reference internal" href="#">Deployment</a><ul>
|
||||
<li><a class="reference internal" href="#running-locally">Running Locally</a></li>
|
||||
<li><a class="reference internal" href="#docker">Docker</a></li>
|
||||
<li><a class="reference internal" href="#cloud-platforms">Cloud Platforms</a></li>
|
||||
<li><a class="reference internal" href="#health-check-endpoint">Health Check Endpoint</a></li>
|
||||
<li><a class="reference internal" href="#uvicorn-directly">Uvicorn Directly</a></li>
|
||||
<li><a class="reference internal" href="#docker-compose">Docker Compose</a></li>
|
||||
<li><a class="reference internal" href="#reverse-proxy">Reverse Proxy</a></li>
|
||||
<li><a class="reference internal" href="#production-checklist">Production Checklist</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
<search id="searchbox" style="display: none" role="search">
|
||||
<h3 id="searchlabel">Quick search</h3>
|
||||
<div class="searchformwrapper">
|
||||
<form class="search" action="search.html" method="get">
|
||||
<input type="text" name="q" aria-labelledby="searchlabel" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"/>
|
||||
<input type="submit" value="Go" />
|
||||
</form>
|
||||
</div>
|
||||
</search>
|
||||
<script>document.getElementById('searchbox').style.display = "block"</script>
|
||||
</div>
|
||||
</div>
|
||||
<div class="clearer"></div>
|
||||
</div>
|
||||
<div class="footer">
|
||||
©2018-2026, Kenneth Reitz.
|
||||
|
||||
|
|
||||
<a href="_sources/deployment.rst.txt"
|
||||
rel="nofollow">Page source</a>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,19 +0,0 @@
|
||||
# Minimal makefile for Sphinx documentation
|
||||
#
|
||||
|
||||
# You can set these variables from the command line.
|
||||
SPHINXOPTS =
|
||||
SPHINXBUILD = sphinx-build
|
||||
SOURCEDIR = source
|
||||
BUILDDIR = build
|
||||
|
||||
# Put it first so that "make" without argument is like "make help".
|
||||
help:
|
||||
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
||||
|
||||
.PHONY: help Makefile
|
||||
|
||||
# Catch-all target: route all unknown targets to Sphinx using the new
|
||||
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
|
||||
%: Makefile
|
||||
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
||||
@@ -1,35 +0,0 @@
|
||||
@ECHO OFF
|
||||
|
||||
pushd %~dp0
|
||||
|
||||
REM Command file for Sphinx documentation
|
||||
|
||||
if "%SPHINXBUILD%" == "" (
|
||||
set SPHINXBUILD=sphinx-build
|
||||
)
|
||||
set SOURCEDIR=source
|
||||
set BUILDDIR=build
|
||||
|
||||
if "%1" == "" goto help
|
||||
|
||||
%SPHINXBUILD% >NUL 2>NUL
|
||||
if errorlevel 9009 (
|
||||
echo.
|
||||
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
|
||||
echo.installed, then set the SPHINXBUILD environment variable to point
|
||||
echo.to the full path of the 'sphinx-build' executable. Alternatively you
|
||||
echo.may add the Sphinx directory to PATH.
|
||||
echo.
|
||||
echo.If you don't have Sphinx installed, grab it from
|
||||
echo.http://sphinx-doc.org/
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
|
||||
goto end
|
||||
|
||||
:help
|
||||
%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
|
||||
|
||||
:end
|
||||
popd
|
||||
@@ -1,48 +0,0 @@
|
||||
alabaster==0.7.12
|
||||
appdirs==1.4.3
|
||||
atomicwrites==1.2.1
|
||||
attrs==18.2.0
|
||||
babel==2.6.0
|
||||
black==18.9b0
|
||||
bleach==3.0.2
|
||||
certifi==2018.8.24
|
||||
cffi==1.11.5
|
||||
chardet==3.0.4
|
||||
click==7.0
|
||||
cmarkgfm==0.4.2
|
||||
colorama==0.4.0 ; sys_platform == 'win32'
|
||||
docutils==0.14
|
||||
flake8==3.5.0
|
||||
flask==1.0.2
|
||||
future==0.16.0
|
||||
idna==2.7
|
||||
imagesize==1.1.0
|
||||
itsdangerous==0.24
|
||||
jinja2==2.10
|
||||
markupsafe==1.0
|
||||
mccabe==0.6.1
|
||||
more-itertools==4.3.0
|
||||
packaging==18.0
|
||||
pkginfo==1.4.2
|
||||
pluggy==0.7.1
|
||||
py==1.7.0
|
||||
pycodestyle==2.3.1
|
||||
pycparser==2.19
|
||||
pyflakes==1.6.0
|
||||
pygments==2.2.0
|
||||
pyparsing==2.2.2
|
||||
pytest==3.8.2
|
||||
pytz==2018.5
|
||||
readme-renderer==22.0
|
||||
requests-toolbelt==0.8.0
|
||||
requests==2.19.1
|
||||
six==1.11.0
|
||||
snowballstemmer==1.2.1
|
||||
sphinx==1.8.1
|
||||
sphinxcontrib-websupport==1.1.0
|
||||
toml==0.10.0
|
||||
tqdm==4.26.0
|
||||
twine==1.12.1
|
||||
urllib3==1.23
|
||||
webencodings==0.5.1
|
||||
werkzeug==0.14.1
|
||||
@@ -1,7 +0,0 @@
|
||||
/* Hide module name and default value for environment variable section */
|
||||
div[id$='environment-variables'] code.descclassname {
|
||||
display: none;
|
||||
}
|
||||
div[id$='environment-variables'] em.property {
|
||||
display: none;
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
|
||||
/*
|
||||
Copyright (C) 2011-2018 Hoefler & Co.
|
||||
This software is the property of Hoefler & Co. (H&Co).
|
||||
Your right to access and use this software is subject to the
|
||||
applicable License Agreement, or Terms of Service, that exists
|
||||
between you and H&Co. If no such agreement exists, you may not
|
||||
access or use this software for any purpose.
|
||||
This software may only be hosted at the locations specified in
|
||||
the applicable License Agreement or Terms of Service, and only
|
||||
for the purposes expressly set forth therein. You may not copy,
|
||||
modify, convert, create derivative works from or distribute this
|
||||
software in any way, or make it accessible to any third party,
|
||||
without first obtaining the written permission of H&Co.
|
||||
For more information, please visit us at http://typography.com.
|
||||
148887-130097-20181011
|
||||
*/
|
||||
|
||||
<!-- sorry your browser is not supported. -->
|
||||