Rewrite docs from scratch, remove 14MB of bundled fonts

Complete documentation rewrite:
- Clean, code-first index page (no badges, no testimonials)
- Rewritten quickstart: request/response reference, templates, background tasks
- Rewritten feature tour: method filtering, lifespan, file serving,
  error handling, hooks, WebSockets, GraphQL, OpenAPI, CORS, sessions
- Simplified testing and deployment guides
- Stripped conf.py to essential extensions only

Removed cruft:
- 14MB of paid font files (Mercury, Operator Mono)
- Google Analytics (deprecated Universal Analytics)
- UserVoice widget
- Konami code easter egg
- Algolia DocSearch (not configured)
- Twitter widgets
- Unused Sphinx extensions (mathjax, ifconfig, coverage, doctest, opengraph)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-22 12:17:22 -04:00
parent 30801557a3
commit 33ebc77f10
72 changed files with 328 additions and 6025 deletions
-7
View File
@@ -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;
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -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. -->
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,177 +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
*/
@font-face {
font-family: "Mercury Text G1 A";
src: url("http://python-responder.org/en/latest/_static/fonts/692185/AA83D0999C9464BC6.eot");
src: url("http://python-responder.org/en/latest/_static/fonts/692185/AA83D0999C9464BC6.eot?#hco")
format("embedded-opentype");
font-style: normal;
font-weight: 400;
}
@font-face {
font-family: "Mercury Text G1 4r";
src: url("http://python-responder.org/en/latest/_static/fonts/692185/AA83D0999C9464BC6.eot");
src: url("http://python-responder.org/en/latest/_static/fonts/692185/AA83D0999C9464BC6.eot?#hco")
format("embedded-opentype");
font-style: normal;
font-weight: 400;
}
@font-face {
font-family: "Mercury Text G1 A";
src: url("http://python-responder.org/en/latest/_static/fonts/692185/005CED86771E6F899.eot");
src: url("http://python-responder.org/en/latest/_static/fonts/692185/005CED86771E6F899.eot?#hco")
format("embedded-opentype");
font-style: italic;
font-weight: 400;
}
@font-face {
font-family: "Mercury Text G1 4i";
src: url("http://python-responder.org/en/latest/_static/fonts/692185/005CED86771E6F899.eot");
src: url("http://python-responder.org/en/latest/_static/fonts/692185/005CED86771E6F899.eot?#hco")
format("embedded-opentype");
font-style: italic;
font-weight: 400;
}
@font-face {
font-family: "Mercury Text G1 A";
src: url("http://python-responder.org/en/latest/_static/fonts/692185/40351B0A9DF3B9622.eot");
src: url("http://python-responder.org/en/latest/_static/fonts/692185/40351B0A9DF3B9622.eot?#hco")
format("embedded-opentype");
font-style: normal;
font-weight: 600;
}
@font-face {
font-family: "Mercury Text G1 6r";
src: url("http://python-responder.org/en/latest/_static/fonts/692185/40351B0A9DF3B9622.eot");
src: url("http://python-responder.org/en/latest/_static/fonts/692185/40351B0A9DF3B9622.eot?#hco")
format("embedded-opentype");
font-style: normal;
font-weight: 600;
}
@font-face {
font-family: "Mercury Text G1 A";
src: url("http://python-responder.org/en/latest/_static/fonts/692185/3A39878E22934F8AD.eot");
src: url("http://python-responder.org/en/latest/_static/fonts/692185/3A39878E22934F8AD.eot?#hco")
format("embedded-opentype");
font-style: italic;
font-weight: 600;
}
@font-face {
font-family: "Mercury Text G1 6i";
src: url("http://python-responder.org/en/latest/_static/fonts/692185/3A39878E22934F8AD.eot");
src: url("http://python-responder.org/en/latest/_static/fonts/692185/3A39878E22934F8AD.eot?#hco")
format("embedded-opentype");
font-style: italic;
font-weight: 600;
}
@font-face {
font-family: "Mercury Text G1 A";
src: url("http://python-responder.org/en/latest/_static/fonts/692185/DD7AD6D5FDE05ABCA.eot");
src: url("http://python-responder.org/en/latest/_static/fonts/692185/DD7AD6D5FDE05ABCA.eot?#hco")
format("embedded-opentype");
font-style: normal;
font-weight: 700;
}
@font-face {
font-family: "Mercury Text G1 7r";
src: url("http://python-responder.org/en/latest/_static/fonts/692185/DD7AD6D5FDE05ABCA.eot");
src: url("http://python-responder.org/en/latest/_static/fonts/692185/DD7AD6D5FDE05ABCA.eot?#hco")
format("embedded-opentype");
font-style: normal;
font-weight: 700;
}
@font-face {
font-family: "Mercury Text G1 A";
src: url("http://python-responder.org/en/latest/_static/fonts/692185/777143010DB6642D4.eot");
src: url("http://python-responder.org/en/latest/_static/fonts/692185/777143010DB6642D4.eot?#hco")
format("embedded-opentype");
font-style: italic;
font-weight: 700;
}
@font-face {
font-family: "Mercury Text G1 7i";
src: url("http://python-responder.org/en/latest/_static/fonts/692185/777143010DB6642D4.eot");
src: url("http://python-responder.org/en/latest/_static/fonts/692185/777143010DB6642D4.eot?#hco")
format("embedded-opentype");
font-style: italic;
font-weight: 700;
}
@font-face {
font-family: "Operator Mono SSm A";
src: url("http://python-responder.org/en/latest/_static/fonts/692185/4A801A74B6CEC6B76.eot");
src: url("http://python-responder.org/en/latest/_static/fonts/692185/4A801A74B6CEC6B76.eot?#hco")
format("embedded-opentype");
font-style: normal;
font-weight: 400;
}
@font-face {
font-family: "Operator Mono SSm 4r";
src: url("http://python-responder.org/en/latest/_static/fonts/692185/4A801A74B6CEC6B76.eot");
src: url("http://python-responder.org/en/latest/_static/fonts/692185/4A801A74B6CEC6B76.eot?#hco")
format("embedded-opentype");
font-style: normal;
font-weight: 400;
}
@font-face {
font-family: "Operator Mono SSm A";
src: url("http://python-responder.org/en/latest/_static/fonts/692185/7E9ADDBCA2C8BD433.eot");
src: url("http://python-responder.org/en/latest/_static/fonts/692185/7E9ADDBCA2C8BD433.eot?#hco")
format("embedded-opentype");
font-style: italic;
font-weight: 400;
}
@font-face {
font-family: "Operator Mono SSm 4i";
src: url("http://python-responder.org/en/latest/_static/fonts/692185/7E9ADDBCA2C8BD433.eot");
src: url("http://python-responder.org/en/latest/_static/fonts/692185/7E9ADDBCA2C8BD433.eot?#hco")
format("embedded-opentype");
font-style: italic;
font-weight: 400;
}
@font-face {
font-family: "Operator Mono SSm A";
src: url("http://python-responder.org/en/latest/_static/fonts/692185/13B223000FA5C8685.eot");
src: url("http://python-responder.org/en/latest/_static/fonts/692185/13B223000FA5C8685.eot?#hco")
format("embedded-opentype");
font-style: normal;
font-weight: 700;
}
@font-face {
font-family: "Operator Mono SSm 7r";
src: url("http://python-responder.org/en/latest/_static/fonts/692185/13B223000FA5C8685.eot");
src: url("http://python-responder.org/en/latest/_static/fonts/692185/13B223000FA5C8685.eot?#hco")
format("embedded-opentype");
font-style: normal;
font-weight: 700;
}
@font-face {
font-family: "Operator Mono SSm A";
src: url("http://python-responder.org/en/latest/_static/fonts/692185/0AC552691F872135E.eot");
src: url("http://python-responder.org/en/latest/_static/fonts/692185/0AC552691F872135E.eot?#hco")
format("embedded-opentype");
font-style: italic;
font-weight: 700;
}
@font-face {
font-family: "Operator Mono SSm 7i";
src: url("http://python-responder.org/en/latest/_static/fonts/692185/0AC552691F872135E.eot");
src: url("http://python-responder.org/en/latest/_static/fonts/692185/0AC552691F872135E.eot?#hco")
format("embedded-opentype");
font-style: italic;
font-weight: 700;
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,177 +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
*/
@font-face {
font-family: "Mercury Text G1 A";
src: url("http://python-responder.org/en/latest/_static/fonts/692185/7D0605C11BA3A93EF.eot");
src: url("http://python-responder.org/en/latest/_static/fonts/692185/7D0605C11BA3A93EF.eot?#hco")
format("embedded-opentype");
font-style: normal;
font-weight: 400;
}
@font-face {
font-family: "Mercury Text G1 4r";
src: url("http://python-responder.org/en/latest/_static/fonts/692185/7D0605C11BA3A93EF.eot");
src: url("http://python-responder.org/en/latest/_static/fonts/692185/7D0605C11BA3A93EF.eot?#hco")
format("embedded-opentype");
font-style: normal;
font-weight: 400;
}
@font-face {
font-family: "Mercury Text G1 A";
src: url("http://python-responder.org/en/latest/_static/fonts/692185/81A13EBFC10447CAC.eot");
src: url("http://python-responder.org/en/latest/_static/fonts/692185/81A13EBFC10447CAC.eot?#hco")
format("embedded-opentype");
font-style: italic;
font-weight: 400;
}
@font-face {
font-family: "Mercury Text G1 4i";
src: url("http://python-responder.org/en/latest/_static/fonts/692185/81A13EBFC10447CAC.eot");
src: url("http://python-responder.org/en/latest/_static/fonts/692185/81A13EBFC10447CAC.eot?#hco")
format("embedded-opentype");
font-style: italic;
font-weight: 400;
}
@font-face {
font-family: "Mercury Text G1 A";
src: url("http://python-responder.org/en/latest/_static/fonts/692185/25E3F5D50DDE1C555.eot");
src: url("http://python-responder.org/en/latest/_static/fonts/692185/25E3F5D50DDE1C555.eot?#hco")
format("embedded-opentype");
font-style: normal;
font-weight: 600;
}
@font-face {
font-family: "Mercury Text G1 6r";
src: url("http://python-responder.org/en/latest/_static/fonts/692185/25E3F5D50DDE1C555.eot");
src: url("http://python-responder.org/en/latest/_static/fonts/692185/25E3F5D50DDE1C555.eot?#hco")
format("embedded-opentype");
font-style: normal;
font-weight: 600;
}
@font-face {
font-family: "Mercury Text G1 A";
src: url("http://python-responder.org/en/latest/_static/fonts/692185/F526A6C670B9765E2.eot");
src: url("http://python-responder.org/en/latest/_static/fonts/692185/F526A6C670B9765E2.eot?#hco")
format("embedded-opentype");
font-style: italic;
font-weight: 600;
}
@font-face {
font-family: "Mercury Text G1 6i";
src: url("http://python-responder.org/en/latest/_static/fonts/692185/F526A6C670B9765E2.eot");
src: url("http://python-responder.org/en/latest/_static/fonts/692185/F526A6C670B9765E2.eot?#hco")
format("embedded-opentype");
font-style: italic;
font-weight: 600;
}
@font-face {
font-family: "Mercury Text G1 A";
src: url("http://python-responder.org/en/latest/_static/fonts/692185/7DCD4B5CCAEE3223E.eot");
src: url("http://python-responder.org/en/latest/_static/fonts/692185/7DCD4B5CCAEE3223E.eot?#hco")
format("embedded-opentype");
font-style: normal;
font-weight: 700;
}
@font-face {
font-family: "Mercury Text G1 7r";
src: url("http://python-responder.org/en/latest/_static/fonts/692185/7DCD4B5CCAEE3223E.eot");
src: url("http://python-responder.org/en/latest/_static/fonts/692185/7DCD4B5CCAEE3223E.eot?#hco")
format("embedded-opentype");
font-style: normal;
font-weight: 700;
}
@font-face {
font-family: "Mercury Text G1 A";
src: url("http://python-responder.org/en/latest/_static/fonts/692185/C08332ABD7F145352.eot");
src: url("http://python-responder.org/en/latest/_static/fonts/692185/C08332ABD7F145352.eot?#hco")
format("embedded-opentype");
font-style: italic;
font-weight: 700;
}
@font-face {
font-family: "Mercury Text G1 7i";
src: url("http://python-responder.org/en/latest/_static/fonts/692185/C08332ABD7F145352.eot");
src: url("http://python-responder.org/en/latest/_static/fonts/692185/C08332ABD7F145352.eot?#hco")
format("embedded-opentype");
font-style: italic;
font-weight: 700;
}
@font-face {
font-family: "Operator Mono SSm A";
src: url("http://python-responder.org/en/latest/_static/fonts/692185/C579DF5B35B145D49.eot");
src: url("http://python-responder.org/en/latest/_static/fonts/692185/C579DF5B35B145D49.eot?#hco")
format("embedded-opentype");
font-style: normal;
font-weight: 400;
}
@font-face {
font-family: "Operator Mono SSm 4r";
src: url("http://python-responder.org/en/latest/_static/fonts/692185/C579DF5B35B145D49.eot");
src: url("http://python-responder.org/en/latest/_static/fonts/692185/C579DF5B35B145D49.eot?#hco")
format("embedded-opentype");
font-style: normal;
font-weight: 400;
}
@font-face {
font-family: "Operator Mono SSm A";
src: url("http://python-responder.org/en/latest/_static/fonts/692185/E3597D43523236FD8.eot");
src: url("http://python-responder.org/en/latest/_static/fonts/692185/E3597D43523236FD8.eot?#hco")
format("embedded-opentype");
font-style: italic;
font-weight: 400;
}
@font-face {
font-family: "Operator Mono SSm 4i";
src: url("http://python-responder.org/en/latest/_static/fonts/692185/E3597D43523236FD8.eot");
src: url("http://python-responder.org/en/latest/_static/fonts/692185/E3597D43523236FD8.eot?#hco")
format("embedded-opentype");
font-style: italic;
font-weight: 400;
}
@font-face {
font-family: "Operator Mono SSm A";
src: url("http://python-responder.org/en/latest/_static/fonts/692185/3CBFC66855DD9B6EA.eot");
src: url("http://python-responder.org/en/latest/_static/fonts/692185/3CBFC66855DD9B6EA.eot?#hco")
format("embedded-opentype");
font-style: normal;
font-weight: 700;
}
@font-face {
font-family: "Operator Mono SSm 7r";
src: url("http://python-responder.org/en/latest/_static/fonts/692185/3CBFC66855DD9B6EA.eot");
src: url("http://python-responder.org/en/latest/_static/fonts/692185/3CBFC66855DD9B6EA.eot?#hco")
format("embedded-opentype");
font-style: normal;
font-weight: 700;
}
@font-face {
font-family: "Operator Mono SSm A";
src: url("http://python-responder.org/en/latest/_static/fonts/692185/09E151FC31ECEF374.eot");
src: url("http://python-responder.org/en/latest/_static/fonts/692185/09E151FC31ECEF374.eot?#hco")
format("embedded-opentype");
font-style: italic;
font-weight: 700;
}
@font-face {
font-family: "Operator Mono SSm 7i";
src: url("http://python-responder.org/en/latest/_static/fonts/692185/09E151FC31ECEF374.eot");
src: url("http://python-responder.org/en/latest/_static/fonts/692185/09E151FC31ECEF374.eot?#hco")
format("embedded-opentype");
font-style: italic;
font-weight: 700;
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -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. -->
File diff suppressed because one or more lines are too long
-161
View File
@@ -1,161 +0,0 @@
/*
* Konami-JS ~
* :: Now with support for touch events and multiple instances for
* :: those situations that call for multiple easter eggs!
* Code: https://github.com/snaptortoise/konami-js
* Copyright (c) 2009 George Mandis (georgemandis.com, snaptortoise.com)
* Version: 1.6.2 (7/17/2018)
* Licensed under the MIT License (http://opensource.org/licenses/MIT)
* Tested in: Safari 4+, Google Chrome 4+, Firefox 3+, IE7+, Mobile Safari 2.2.1+ and Android
*/
var Konami = function (callback) {
var konami = {
addEvent: function (obj, type, fn, ref_obj) {
if (obj.addEventListener) obj.addEventListener(type, fn, false);
else if (obj.attachEvent) {
// IE
obj["e" + type + fn] = fn;
obj[type + fn] = function () {
obj["e" + type + fn](window.event, ref_obj);
};
obj.attachEvent("on" + type, obj[type + fn]);
}
},
removeEvent: function (obj, eventName, eventCallback) {
if (obj.removeEventListener) {
obj.removeEventListener(eventName, eventCallback);
} else if (obj.attachEvent) {
obj.detachEvent(eventName);
}
},
input: "",
pattern: "38384040373937396665",
keydownHandler: function (e, ref_obj) {
if (ref_obj) {
konami = ref_obj;
} // IE
konami.input += e ? e.keyCode : event.keyCode;
if (konami.input.length > konami.pattern.length) {
konami.input = konami.input.substr(konami.input.length - konami.pattern.length);
}
if (konami.input === konami.pattern) {
konami.code(konami._currentLink);
konami.input = "";
e.preventDefault();
return false;
}
},
load: function (link) {
this._currentLink = link;
this.addEvent(document, "keydown", this.keydownHandler, this);
this.iphone.load(link);
},
unload: function () {
this.removeEvent(document, "keydown", this.keydownHandler);
this.iphone.unload();
},
code: function (link) {
window.location = link;
},
iphone: {
start_x: 0,
start_y: 0,
stop_x: 0,
stop_y: 0,
tap: false,
capture: false,
orig_keys: "",
keys: [
"UP",
"UP",
"DOWN",
"DOWN",
"LEFT",
"RIGHT",
"LEFT",
"RIGHT",
"TAP",
"TAP",
],
input: [],
code: function (link) {
konami.code(link);
},
touchmoveHandler: function (e) {
if (e.touches.length === 1 && konami.iphone.capture === true) {
var touch = e.touches[0];
konami.iphone.stop_x = touch.pageX;
konami.iphone.stop_y = touch.pageY;
konami.iphone.tap = false;
konami.iphone.capture = false;
konami.iphone.check_direction();
}
},
touchendHandler: function () {
konami.iphone.input.push(konami.iphone.check_direction());
if (konami.iphone.input.length > konami.iphone.keys.length)
konami.iphone.input.shift();
if (konami.iphone.input.length === konami.iphone.keys.length) {
var match = true;
for (var i = 0; i < konami.iphone.keys.length; i++) {
if (konami.iphone.input[i] !== konami.iphone.keys[i]) {
match = false;
}
}
if (match) {
konami.iphone.code(konami._currentLink);
}
}
},
touchstartHandler: function (e) {
konami.iphone.start_x = e.changedTouches[0].pageX;
konami.iphone.start_y = e.changedTouches[0].pageY;
konami.iphone.tap = true;
konami.iphone.capture = true;
},
load: function (link) {
this.orig_keys = this.keys;
konami.addEvent(document, "touchmove", this.touchmoveHandler);
konami.addEvent(document, "touchend", this.touchendHandler, false);
konami.addEvent(document, "touchstart", this.touchstartHandler);
},
unload: function () {
konami.removeEvent(document, "touchmove", this.touchmoveHandler);
konami.removeEvent(document, "touchend", this.touchendHandler);
konami.removeEvent(document, "touchstart", this.touchstartHandler);
},
check_direction: function () {
x_magnitude = Math.abs(this.start_x - this.stop_x);
y_magnitude = Math.abs(this.start_y - this.stop_y);
x = this.start_x - this.stop_x < 0 ? "RIGHT" : "LEFT";
y = this.start_y - this.stop_y < 0 ? "DOWN" : "UP";
result = x_magnitude > y_magnitude ? x : y;
result = this.tap === true ? "TAP" : result;
return result;
},
},
};
typeof callback === "string" && konami.load(callback);
if (typeof callback === "function") {
konami.code = callback;
konami.load();
}
return konami;
};
if (typeof module !== "undefined" && typeof module.exports !== "undefined") {
module.exports = Konami;
} else {
if (typeof define === "function" && define.amd) {
define([], function () {
return Konami;
});
} else {
window.Konami = Konami;
}
}
+3 -224
View File
@@ -1,242 +1,21 @@
<link
rel="stylesheet"
type="text/css"
href="https://cloud.typography.com/7584432/7586812/css/fonts.css"
/>
<script async defer src="https://buttons.github.io/buttons.js"></script>
<script type="text/javascript">
$("#searchbox").hide(0);
</script>
<!--Alabaster (krTheme++) Hacks -->
<!-- CSS Adjustments (I'm very picky.) -->
<style type="text/css">
/* Rezzy requires precise alignment. */
img.logo {
margin-left: -20px !important;
}
h1 {
font-family: "Mercury Text G1 A", "Mercury Text G1 B" !important;
font-style: normal !important;
font-weight: 600 !important;
}
.section {
font-family: "Mercury Text G1 A", "Mercury Text G1 B" !important;
font-style: normal !important;
font-weight: 400 !important;
}
pre,
.pre,
.class em,
.descname,
.method em {
font-family: "Operator Mono SSm A", "Operator Mono SSm B", monospace !important;
font-weight: 400 !important;
}
.property {
color: lightgrey !important;
}
.method .descname {
color: #220a54;
}
.method {
margin-bottom: 2em;
}
.si,
.s2,
.s1,
.method em,
.class em {
font-style: italic !important;
color: grey;
}
.method em,
.class em {
margin-left: 0.3em;
margin-right: 0.3em;
}
.method p,
.class p {
font-family: "Mercury Text G1 A", "Mercury Text G1 B";
font-style: italic !important;
font-weight: 400 !important;
font-size: 1.15em;
}
.method p:first,
.class p:first {
background: #fffcbf;
}
.class .property {
display: none;
}
#testimonials p.attribution {
margin-top: -1em;
}
/* "Quick Search" should be not be shown for now. */
div#searchbox h3 {
display: none;
}
/* Make the document a little wider, less code is cut-off. */
/* Make the document a little wider. */
div.document {
width: 1008px;
}
/* Much-improved spacing around code blocks. */
/* Better spacing around code blocks. */
div.highlight pre {
padding: 11px 14px;
}
/* Remain Responsive! */
/* Responsive layout. */
@media screen and (max-width: 1008px) {
div.sphinxsidebar {
display: none;
}
div.document {
width: 100% !important;
}
/* Have code blocks escape the document right-margin. */
div.highlight pre {
margin-right: -30px;
}
}
</style>
<!-- Analytics tracking for Kenneth. -->
<script async src="https://www.googletagmanager.com/gtag/js?id=UA-127383416-1"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag() {
dataLayer.push(arguments);
}
gtag("js", new Date());
gtag("config", "UA-127383416-1");
</script>
<!-- There are no more hacks. -->
<!-- இڿڰۣ-ڰۣ— -->
<!-- Love, Kenneth Reitz -->
<script src="{{ pathto('_static/', 1) }}/konami.js"></script>
<script>
var easter_egg = new Konami(
"https://www.myfortunecookie.co.uk/fortunes/" +
(Math.floor(Math.random() * 152) + 1)
);
</script>
<style>
.injected {
display: none !important;
}
</style>
<!-- GitHub Logo -->
<a
href="https://github.com/kennethreitz/responder"
class="github-corner"
aria-label="View source on GitHub"
>
<svg
width="80"
height="80"
viewBox="0 0 250 250"
style="fill: #151513; color: #fff; position: absolute; top: 0; border: 0; right: 0"
aria-hidden="true"
>
<path d="M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z"></path>
<path
d="M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2"
fill="currentColor"
style="transform-origin: 130px 106px"
class="octo-arm"
></path>
<path
d="M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z"
fill="currentColor"
class="octo-body"
></path>
</svg>
</a>
<style>
.github-corner:hover .octo-arm {
animation: octocat-wave 560ms ease-in-out;
}
@keyframes octocat-wave {
0%,
100% {
transform: rotate(0);
}
20%,
60% {
transform: rotate(-25deg);
}
40%,
80% {
transform: rotate(10deg);
}
}
@media (max-width: 500px) {
.github-corner:hover .octo-arm {
animation: none;
}
.github-corner .octo-arm {
animation: octocat-wave 560ms ease-in-out;
}
}
</style>
<!-- That was not a hack. That was art.
<!-- UserVoice JavaScript SDK (only needed once on a page) -->
<script>
(function () {
var uv = document.createElement("script");
uv.type = "text/javascript";
uv.async = true;
uv.src = "//widget.uservoice.com/f4AQraEfwInlMzkexfRLg.js";
var s = document.getElementsByTagName("script")[0];
s.parentNode.insertBefore(uv, s);
})();
</script>
<!-- A tab to launch the Classic Widget -->
<script>
UserVoice = window.UserVoice || [];
UserVoice.push([
"showTab",
"classic_widget",
{
mode: "feedback",
primary_color: "#fa8c28",
link_color: "#0a8cc6",
forum_id: 913660,
tab_label: "Got feedback?",
tab_color: "#00994f",
tab_position: "bottom-left",
tab_inverted: true,
},
]);
</script>
+4 -85
View File
@@ -1,89 +1,8 @@
<p class="logo">
<a href="{{ pathto(master_doc) }}">
<img
class="logo"
src="{{ pathto('_static/responder.png', 1) }}"
title="https://kennethreitz.org/tattoos"
/>
</a>
</p>
<p>
<a class="github-button"
href="https://github.com/kennethreitz/responder"
data-color-scheme="no-preference: light; light: light; dark: light;"
data-size="large"
data-show-count="true"
aria-label="Star kennethreitz/responder on GitHub">Star</a>
<strong>Responder</strong> — a familiar HTTP service framework for Python.
</p>
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/docsearch.js@2/dist/cdn/docsearch.min.css"
/>
<style>
.algolia-autocomplete {
width: 100%;
height: 1.5em;
}
.algolia-autocomplete a {
border-bottom: none !important;
}
#doc_search {
width: 100%;
height: 100%;
}
</style>
<input id="doc_search" placeholder="Search the doc" autofocus />
<script
type="text/javascript"
src="https://cdn.jsdelivr.net/npm/docsearch.js@2/dist/cdn/docsearch.min.js"
onload="docsearch({
apiKey: 'ac965312db252e0496283c75c6f76f0b',
indexName: 'python-responder',
inputSelector: '#doc_search',
debug: false // Set debug to true if you want to inspect the dropdown
})"
async
></script>
<p><strong>Responder</strong> is a web service framework, written for human beings.</p>
<h3>Stay Informed</h3>
<p>Receive updates on new releases and upcoming projects.</p>
<p>
<a class="github-button"
href="https://github.com/kennethreitz"
data-color-scheme="no-preference: light; light: light; dark: light;"
data-size="medium"
data-show-count="true"
aria-label="Follow @kennethreitz on GitHub">Follow @kennethreitz</a>
</p>
<p>
<a
href="https://x.com/kennethreitz42"
class="twitter-follow-button"
data-show-count="false"
>Follow @kennethreitz</a
>
<script>
!(function (d, s, id) {
var js,
fjs = d.getElementsByTagName(s)[0],
p = /^http:/.test(d.location) ? "http" : "https";
if (!d.getElementById(id)) {
js = d.createElement(s);
js.id = id;
js.src = p + "://platform.twitter.com/widgets.js";
fjs.parentNode.insertBefore(js, fjs);
}
})(document, "script", "twitter-wjs");
</script>
</p>
<h3>Useful Links</h3>
<ul>
<li><a href="http://github.com/kennethreitz/responder">Responder @ GitHub</a></li>
<li><a href="http://pypi.python.org/pypi/responder">Responder @ PyPI</a></li>
<li><a href="http://github.com/kennethreitz/responder/issues">Issue Tracker</a></li>
<li><a href="https://github.com/kennethreitz/responder">GitHub</a></li>
<li><a href="https://pypi.org/project/responder/">PyPI</a></li>
<li><a href="https://github.com/kennethreitz/responder/issues">Issues</a></li>
</ul>
-94
View File
@@ -1,94 +0,0 @@
<p class="logo">
<a href="{{ pathto(master_doc) }}">
<img
class="logo"
src="{{ pathto('_static/responder.png', 1) }}"
title="https://kennethreitz.org/tattoos"
/>
</a>
</p>
<p>
<iframe
src="https://ghbtns.com/github-btn.html?user=kennethreitz&repo=responder&type=watch&count=true&size=large"
allowtransparency="true"
frameborder="0"
scrolling="0"
width="200px"
height="35px"
></iframe>
</p>
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/docsearch.js@2/dist/cdn/docsearch.min.css"
/>
<style>
.algolia-autocomplete {
width: 100%;
height: 1.5em;
}
.algolia-autocomplete a {
border-bottom: none !important;
}
#doc_search {
width: 100%;
height: 100%;
}
</style>
<input id="doc_search" placeholder="Search the doc" autofocus />
<script
type="text/javascript"
src="https://cdn.jsdelivr.net/npm/docsearch.js@2/dist/cdn/docsearch.min.js"
onload="docsearch({
apiKey: 'ac965312db252e0496283c75c6f76f0b',
indexName: 'python-responder',
inputSelector: '#doc_search',
debug: false // Set debug to true if you want to inspect the dropdown
})"
async
></script>
<p><strong>Responder</strong> is a web service framework, written for human beings.</p>
<h3>Stay Informed</h3>
<p>Receive updates on new releases and upcoming projects.</p>
<p>
<iframe
src="https://ghbtns.com/github-btn.html?user=kennethreitz&type=follow&count=true"
allowtransparency="true"
frameborder="0"
scrolling="0"
width="200"
height="20"
></iframe>
</p>
<p>
<a
href="https://x.com/kennethreitz42"
class="twitter-follow-button"
data-show-count="false"
>Follow @kennethreitz</a
>
<script>
!(function (d, s, id) {
var js,
fjs = d.getElementsByTagName(s)[0],
p = /^http:/.test(d.location) ? "http" : "https";
if (!d.getElementById(id)) {
js = d.createElement(s);
js.id = id;
js.src = p + "://platform.twitter.com/widgets.js";
fjs.parentNode.insertBefore(js, fjs);
}
})(document, "script", "twitter-wjs");
</script>
</p>
<h3>Useful Links</h3>
<ul>
<li><a href="http://github.com/kennethreitz/responder">Responder @ GitHub</a></li>
<li><a href="http://pypi.python.org/pypi/responder">Responder @ PyPI</a></li>
<li><a href="http://github.com/kennethreitz/responder/issues">Issue Tracker</a></li>
</ul>
+11 -252
View File
@@ -1,108 +1,35 @@
# -*- coding: utf-8 -*-
#
# Configuration file for the Sphinx documentation builder.
#
# This file does only contain a selection of the most common options. For a
# full list see the documentation:
# http://www.sphinx-doc.org/en/master/config
# Sphinx configuration for Responder documentation.
# -- Path setup --------------------------------------------------------------
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#
# import os
# import sys
# sys.path.insert(0, os.path.abspath('.'))
# -- Project information -----------------------------------------------------
project = "responder"
copyright = "2018-2026, A Kenneth Reitz project"
author = "Kenneth Reitz"
# The short X.Y version
import os
# Path hackery to get current version number.
here = os.path.abspath(os.path.dirname(__file__))
project = "responder"
copyright = "2018-2026, Kenneth Reitz"
author = "Kenneth Reitz"
here = os.path.abspath(os.path.dirname(__file__))
about = {}
with open(os.path.join(here, "..", "..", "responder", "__version__.py")) as f:
exec(f.read(), about)
version = about["__version__"]
# The full version, including alpha/beta/rc tags
release = about["__version__"]
# -- General configuration ---------------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here.
#
# needs_sphinx = '1.0'
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [
"sphinx.ext.autodoc",
"sphinx.ext.doctest",
"sphinx.ext.intersphinx",
"sphinx.ext.todo",
"sphinx.ext.coverage",
"sphinx.ext.mathjax",
"sphinx.ext.ifconfig",
"sphinx.ext.viewcode",
"sphinx.ext.githubpages",
"myst_parser",
"sphinx_copybutton",
"sphinx_design",
"sphinx_design_elements",
"sphinxext.opengraph",
]
# Add any paths that contain templates here, relative to this directory.
templates_path = ["_templates"]
# The suffix(es) of source filenames.
# You can specify multiple suffix as a list of string:
#
# source_suffix = ['.rst', '.md']
source_suffix = {".rst": "restructuredtext"}
# The master toctree document.
master_doc = "index"
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
#
# This is also used if you do content translation via gettext catalogs.
# Usually you set "language" from the command line for these cases.
language = "en"
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# This pattern also affects html_static_path and html_extra_path.
exclude_patterns = []
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = None
# -- Options for HTML output -------------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
#
# Theme
html_theme = "alabaster"
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
# documentation.
#
html_theme_options = {
"show_powered_by": False,
"github_user": "kennethreitz",
@@ -110,183 +37,15 @@ html_theme_options = {
"github_banner": False,
"show_related": False,
}
html_sidebars = {
"index": ["sidebarintro.html", "sourcelink.html", "searchbox.html", "hacks.html"],
"**": [
"sidebarlogo.html",
"localtoc.html",
"relations.html",
"sourcelink.html",
"searchbox.html",
"hacks.html",
],
}
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ["_static"]
# Custom sidebar templates, must be a dictionary that maps document names
# to template names.
#
# The default sidebars (for documents that don't match any pattern) are
# defined by theme itself. Builtin themes are using these templates by
# default: ``['localtoc.html', 'relations.html', 'sourcelink.html',
# 'searchbox.html']``.
#
# html_sidebars = {}
# -- Options for HTMLHelp output ---------------------------------------------
# Output file base name for HTML help builder.
htmlhelp_basename = "responderdoc"
# -- Options for LaTeX output ------------------------------------------------
latex_elements = {
# The paper size ('letterpaper' or 'a4paper').
#
# 'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').
#
# 'pointsize': '10pt',
# Additional stuff for the LaTeX preamble.
#
# 'preamble': '',
# Latex figure (float) alignment
#
# 'figure_align': 'htbp',
html_sidebars = {
"**": ["localtoc.html", "searchbox.html"],
}
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title,
# author, documentclass [howto, manual, or own class]).
latex_documents = [
(master_doc, "responder.tex", "responder Documentation", "Kenneth Reitz", "manual")
]
# -- Options for manual page output ------------------------------------------
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [(master_doc, "responder", "responder Documentation", [author], 1)]
# -- Options for Texinfo output ----------------------------------------------
# Grouping the document tree into Texinfo files. List of tuples
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
(
master_doc,
"responder",
"responder Documentation",
author,
"responder",
"One line description of project.",
"Miscellaneous",
)
]
# -- Options for Epub output -------------------------------------------------
# Bibliographic Dublin Core info.
epub_title = project
# The unique identifier of the text. This can be a ISBN number
# or the project homepage.
#
# epub_identifier = ''
# A unique identification for the text.
#
# epub_uid = ''
# A list of files that should not be packed into the epub file.
epub_exclude_files = ["search.html"]
# -- Extension configuration -------------------------------------------------
# -- Options for link checker ----------------------------------------------
linkcheck_ignore = [
# Feldroy.com links are ignored because it blocks GHA.
r"https://www.feldroy.com/.*",
]
linkcheck_anchors_ignore_for_url = [
# Requires JavaScript.
# After opting-in to new GitHub issues, Sphinx can no longer grok the HTML anchor references.
r"https://github.com",
]
# -- Options for intersphinx extension ---------------------------------------
# Example configuration for intersphinx: refer to the Python standard library.
intersphinx_mapping = {"python": ("https://docs.python.org/3", None)}
# -- Options for todo extension ----------------------------------------------
# If true, `todo` and `todoList` produce output, else they produce nothing.
todo_include_todos = True
# -- Options for MyST --------------------------------------------------------
# MyST
myst_heading_anchors = 3
myst_enable_extensions = [
"attrs_block",
"attrs_inline",
"colon_fence",
"deflist",
"fieldlist",
"html_admonition",
"html_image",
"linkify",
"replacements",
"strikethrough",
"substitution",
"tasklist",
]
myst_substitutions = {}
# -- Options for OpenGraph ---------------------------------------------------
#
# When making changes, check them using the RTD PR preview URL on https://www.opengraph.xyz/.
#
# About text lengths
#
# Original documentation says:
# - ogp_description_length
# Configure the amount of characters taken from a page. The default of 200 is probably good
# for most people. If something other than a number is used, it defaults back to 200.
# -- https://sphinxext-opengraph.readthedocs.io/en/latest/#options
#
# Other people say:
# - og:title 40 chars
# - og:description has 2 max lengths:
# When the link is used in a Post, it's 300 chars. When a link is used in a Comment, it's 110 chars.
# So you can either treat it as 110, or, write your Descriptions to 300 but make sure the first 110
# is the critical part and still makes sense when it gets cut off.
# -- https://stackoverflow.com/questions/8914476/facebook-open-graph-meta-tags-maximum-content-length
ogp_site_url = "https://responder.kennethreitz.org/"
ogp_description_length = 300
ogp_site_name = "Responder Documentation"
ogp_image = "https://responder.kennethreitz.org/_static/responder.png"
ogp_image_alt = False
ogp_use_first_image = False
ogp_type = "website"
ogp_enable_meta_description = True
# -- Options for sphinx-copybutton -------------------------------------------
# Copybutton
copybutton_remove_prompts = True
copybutton_line_continuation_character = "\\"
copybutton_prompt_text = r">>> |\.\.\. |\$ |sh\$ |PS> |cr> |mysql> |In \[\d*\]: | {2,5}\.\.\.: | {5,8}: "
copybutton_prompt_text = r">>> |\.\.\. |\$ "
copybutton_prompt_is_regexp = True
+15 -39
View File
@@ -1,14 +1,10 @@
Deploying Responder
===================
Deployment
==========
You can deploy Responder anywhere you can deploy a basic Python application.
Docker
------
Docker Deployment
-----------------
Assuming an existing ``api.py`` containing your Responder application.
``Dockerfile``::
::
FROM python:3.13-slim
WORKDIR /app
@@ -18,38 +14,18 @@ Assuming an existing ``api.py`` containing your Responder application.
EXPOSE 80
CMD ["python", "api.py"]
That's it!
Cloud Deployment
Cloud Platforms
---------------
Responder honors the ``PORT`` environment variable automatically.
It works with any platform that sets ``PORT``: Fly.io, Railway, Render,
Google Cloud Run, etc.
Uvicorn Directly
----------------
Responder automatically honors the ``PORT`` environment variable, which is
set by most cloud platforms (Fly.io, Railway, Render, Google Cloud Run, etc.).
The basics::
$ mkdir my-api
$ cd my-api
Write out an ``api.py``::
import responder
api = responder.API()
@api.route("/")
async def hello(req, resp):
resp.text = "hello, world!"
if __name__ == "__main__":
api.run()
Deploy with your platform of choice. Responder will bind to ``0.0.0.0``
on the port specified by ``PORT`` automatically.
Running with Uvicorn Directly
-----------------------------
For production deployments, you can also run your app directly with uvicorn::
For more control, run with uvicorn::
uvicorn api:api --host 0.0.0.0 --port 8000 --workers 4
+7 -138
View File
@@ -1,29 +1,7 @@
.. responder documentation master file, created by
sphinx-quickstart on Thu Oct 11 12:58:34 2018.
You can adapt this file completely to your liking, but it should at least
contain the root `toctree` directive.
Responder
=========
A familiar HTTP Service Framework
=================================
|ci-tests| |version| |license| |python-versions| |downloads| |contributors| |say-thanks|
.. |ci-tests| image:: https://github.com/kennethreitz/responder/actions/workflows/test.yaml/badge.svg
:target: https://github.com/kennethreitz/responder/actions/workflows/test.yaml
.. |ci-docs| image:: https://github.com/kennethreitz/responder/actions/workflows/docs.yaml/badge.svg
:target: https://github.com/kennethreitz/responder/actions/workflows/docs.yaml
.. |version| image:: https://img.shields.io/pypi/v/responder.svg
:target: https://pypi.org/project/responder/
.. |license| image:: https://img.shields.io/pypi/l/responder.svg
:target: https://pypi.org/project/responder/
.. |python-versions| image:: https://img.shields.io/pypi/pyversions/responder.svg
:target: https://pypi.org/project/responder/
.. |downloads| image:: https://static.pepy.tech/badge/responder/month
:target: https://www.pepy.tech/projects/responder
.. |contributors| image:: https://img.shields.io/github/contributors/kennethreitz/responder.svg
:target: https://github.com/kennethreitz/responder/graphs/contributors
.. |say-thanks| image:: https://img.shields.io/badge/Say%20Thanks-!-1EAEDB.svg
:target: https://saythanks.io/to/kennethreitz
A familiar HTTP Service Framework for Python, powered by `Starlette`_.
.. code:: python
@@ -38,53 +16,11 @@ A familiar HTTP Service Framework
if __name__ == '__main__':
api.run()
Responder is powered by `Starlette`_.
Install it::
The example program demonstrates an `ASGI`_ application using `Responder`_,
including production-ready components like the `uvicorn`_ webserver, based
on `uvloop`_, and the `Jinja`_ templating library pre-installed.
The ``async`` declaration within the example program is optional.
pip install responder
Features
--------
- A pleasant API, with a single import statement.
- Class-based views without inheritance.
- `ASGI`_, the future of Python web services.
- Asynchronous Python frameworks and applications.
- Automatic gzip compression.
- WebSocket support!
- The ability to mount any ASGI / WSGI app at a subroute.
- `f-string syntax`_ route declaration.
- Mutable response object, passed into each view. No need to return anything.
- Background tasks, spawned off in a ``ThreadPoolExecutor``.
- GraphQL (with *GraphiQL*) support!
- OpenAPI schema generation, with interactive documentation!
- Single-page webapp support!
Testimonials
------------
“Pleasantly very taken with python-responder.
`@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`_
..
“I love that you are exploring new patterns. Go go go!”
— Danny Greenfield, author of `Two Scoops of Django`_
User Guides
-----------
Python 3.9+.
.. toctree::
:maxdepth: 2
@@ -96,80 +32,13 @@ User Guides
api
cli
Installing Responder
--------------------
Use ``uv`` for fast installation.
.. code-block:: shell
uv pip install --upgrade 'responder'
Or use standard pip where ``uv`` is not available.
.. code-block:: shell
pip install --upgrade 'responder'
Responder supports **Python 3.9+**.
Development
-----------
If you are looking at installing Responder
for hacking on it, please refer to the :ref:`sandbox` documentation.
.. toctree::
:maxdepth: 1
:caption: Project
changes
Sandbox <sandbox>
backlog
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.content`` sends back bytes.
- Setting ``resp.text`` sends back unicode, while setting ``resp.html`` sends back HTML.
- Setting ``resp.media`` sends back JSON/YAML (``.text``/``.html``/``.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 -- using Python's f-string syntax.
- I love Falcon's "every request and response is passed into each view and mutated" methodology, especially ``response.media``, and have used it here. In addition to supporting JSON, I have decided to support YAML as well, as Kubernetes is slowly taking over the world, and it uses YAML for all the things. Content-negotiation and all that.
- **A built in testing client** powered by Starlette's TestClient.
- The ability to mount other WSGI apps easily.
- Automatic gzipped-responses.
- In addition to Falcon's ``on_get``, ``on_post``, etc methods, Responder features an ``on_request`` method, which gets called on every type of request, much like Requests.
- A production static files server is built-in.
- `uvicorn`_ is 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.
Indices and tables
==================
* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`
.. _@kennethreitz: https://x.com/kennethreitz42
.. _ASGI: https://en.wikipedia.org/wiki/Asynchronous_Server_Gateway_Interface
.. _Django REST Framework: https://www.django-rest-framework.org/
.. _f-string syntax: https://docs.python.org/3/whatsnew/3.6.html#pep-498-formatted-string-literals
.. _Jinja: https://jinja.palletsprojects.com/en/stable/
.. _ServeStatic: https://archmonger.github.io/ServeStatic/latest/
.. _Slowloris: https://en.wikipedia.org/wiki/Slowloris_(computer_security)
.. _Starlette: https://www.starlette.io/
.. _Responder: https://responder.kennethreitz.org/
.. _Two Scoops of Django: https://www.feldroy.com/two-scoops-press#two-scoops-of-django
.. _uvicorn: https://www.uvicorn.org/
.. _uvloop: https://uvloop.readthedocs.io/
+87 -120
View File
@@ -1,176 +1,143 @@
Quick Start!
============
Quick Start
===========
This section of the documentation exists to provide an introduction to the Responder interface,
as well as educate the user on basic functionality.
Create an API
-------------
Declare a Web Service
---------------------
The first thing you need to do is declare a web service::
::
import responder
api = responder.API()
Hello World!
------------
Add a route
-----------
Then, you can add a view / route to it.
Here, we'll make the root URL say "hello world!"::
::
@api.route("/")
def hello_world(req, resp):
def hello(req, resp):
resp.text = "hello, world!"
Run the Server
--------------
Run it
------
Next, we can run our web service easily, with ``api.run()``::
::
api.run()
This will spin up a production web server on port ``5042``, ready for incoming HTTP requests.
Note: you can pass ``port=5000`` if you want to customize the port. The ``PORT`` environment variable for established web service providers (e.g. Heroku) will automatically be honored and will set the listening address to ``0.0.0.0`` automatically (also configurable through the ``address`` keyword argument).
This starts a production uvicorn server on port ``5042``. Customize with
``api.run(port=8000)`` or set the ``PORT`` environment variable.
Accept Route Arguments
----------------------
Route Parameters
----------------
If you want dynamic URLs, you can use Python's familiar *f-string syntax* to declare variables in your routes::
Use f-string syntax for dynamic URLs::
@api.route("/hello/{who}")
def hello_to(req, resp, *, who):
resp.text = f"hello, {who}!"
A ``GET`` request to ``/hello/brettcannon`` will result in a response of ``hello, brettcannon!``.
Type convertors are also available::
Type convertors are available::
@api.route("/add/{a:int}/{b:int}")
async def add(req, resp, *, a, b):
resp.text = f"{a} + {b} = {a + b}"
Supported types: ``str``, ``int``, ``float``, ``uuid``, and ``path``.
Returning JSON / YAML
---------------------
If you want your API to send back JSON, simply set the ``resp.media`` property to a JSON-serializable Python object::
Supported types: ``str``, ``int``, ``float``, ``uuid``, ``path``.
@api.route("/hello/{who}/json")
def hello_to(req, resp, *, who):
resp.media = {"hello": who}
Responses
---------
A ``GET`` request to ``/hello/guido/json`` will result in a response of ``{'hello': 'guido'}``.
::
If the client requests YAML instead (with a header of ``Accept: application/x-yaml``), YAML will be sent.
# Text
resp.text = "hello"
Rendering a Template
--------------------
# HTML
resp.html = "<h1>hello</h1>"
Responder provides a built-in light `Jinja`_ wrapper ``templates.Templates``
# JSON (default)
resp.media = {"hello": "world"}
Usage::
# Bytes
resp.content = b"\x00\x01\x02"
from responder.templates import Templates
# File
resp.file("report.pdf")
templates = Templates()
# Status code
resp.status_code = 201
@api.route("/hello/{name}/html")
def hello(req, resp, name):
resp.html = templates.render("hello.html", name=name)
# Headers
resp.headers["X-Custom"] = "value"
# Redirect
api.redirect(resp, location="/other")
Also a ``render_async`` is available::
Requests
--------
templates = Templates(enable_async=True)
resp.html = await templates.render_async("hello.html", who=who)
::
You can also use the existing ``api.template(filename, *args, **kwargs)`` to render templates::
# Method (lowercase)
req.method # "get", "post", etc.
@api.route("/hello/{who}/html")
def hello_html(req, resp, *, who):
resp.html = api.template('hello.html', who=who)
# Headers (case-insensitive)
req.headers["Content-Type"]
# Query parameters
req.params["q"]
# Path parameters
req.path_params["user_id"]
# JSON body (must await)
data = await req.media()
# Raw body
body = await req.content
# Check content type
req.is_json # True/False
# Client address
req.client # (host, port)
Setting Response Status Code
----------------------------
Templates
---------
If you want to set the response status code, simply set ``resp.status_code``::
Responder includes Jinja2 templating::
@api.route("/416")
def teapot(req, resp):
resp.status_code = api.status_codes.HTTP_416 # ...or 416
@api.route("/hello/{name}/html")
def hello_html(req, resp, *, name):
resp.html = api.template("hello.html", name=name)
Or use the ``Templates`` class directly::
from responder.templates import Templates
templates = Templates(directory="templates")
resp.html = templates.render("page.html", title="Hello")
Setting Response Headers
------------------------
Background Tasks
----------------
If you want to set a response header, like ``X-Pizza: 42``, simply modify the ``resp.headers`` dictionary::
Process work in the background while responding immediately::
@api.route("/pizza")
def pizza_pizza(req, resp):
resp.headers['X-Pizza'] = '42'
That's it!
Receiving Data & Background Tasks
---------------------------------
If you're expecting to read any request data, on the server, you need to declare your view as async and await the content.
Here, we'll process our data in the background, while responding immediately to the client::
import time
@api.route("/incoming")
async def receive_incoming(req, resp):
@api.background.task
def process_data(data):
"""Just sleeps for three seconds, as a demo."""
time.sleep(3)
# Parse the incoming data as form-encoded.
# Note: 'json' and 'yaml' formats are also automatically supported.
@api.route("/work")
async def work(req, resp):
data = await req.media()
# Process the data (in the background).
process_data(data)
# Immediately respond that upload was successful.
resp.media = {'success': True}
A ``POST`` request to ``/incoming`` will result in an immediate response of ``{'success': true}``.
Here's a sample code to post a file with background::
@api.route("/")
async def upload_file(req, resp):
@api.background.task
def process_data(data):
with open(f"./{data['file']['filename']}", 'wb') as f:
f.write(data['file']['content'])
def process(data):
import time
time.sleep(10)
data = await req.media(format='files')
process_data(data)
resp.media = {'success': 'ok'}
You can test file uploads using the built-in test client::
files = {'file': ('hello.txt', b'hello, world!', 'text/plain')}
r = api.requests.post(api.url_for(upload_file), files=files)
print(r.json())
.. _Jinja: https://jinja.palletsprojects.com/en/stable/
process(data)
resp.media = {"status": "processing"}
+64 -20
View File
@@ -1,34 +1,41 @@
Building and Testing with Responder
===================================
Testing
=======
Responder comes with a first-class, well supported test client for your ASGI web services (powered by Starlette's TestClient).
Responder includes a built-in test client powered by Starlette's TestClient.
Here, we'll go over the basics of setting up and testing a Responder application.
The Basics
Basic Test
----------
Your project should look like this::
api.py test_api.py
``$ cat api.py``::
``api.py``::
import responder
api = responder.API()
@api.route("/")
def hello_world(req, resp):
def hello(req, resp):
resp.text = "hello, world!"
if __name__ == "__main__":
api.run()
Writing Tests
-------------
``test_api.py``::
``$ cat test_api.py``::
import api as service
def test_hello():
r = service.api.requests.get("/")
assert r.text == "hello, world!"
Run with pytest::
$ pytest
Using Fixtures
--------------
::
import pytest
import api as service
@@ -37,12 +44,49 @@ Writing Tests
def api():
return service.api
def test_hello_world(api):
def test_hello(api):
r = api.requests.get("/")
assert r.text == "hello, world!"
``$ pytest``::
def test_json(api):
@api.route("/data")
def data(req, resp):
resp.media = {"key": "value"}
...
========================== 1 passed in 0.10 seconds ==========================
r = api.requests.get(api.url_for(data))
assert r.json() == {"key": "value"}
Testing WebSockets
------------------
::
from starlette.testclient import TestClient
def test_websocket(api):
@api.route("/ws", websocket=True)
async def ws(ws):
await ws.accept()
await ws.send_text("hello")
await ws.close()
client = TestClient(api)
with client.websocket_connect("/ws") as ws:
assert ws.receive_text() == "hello"
Testing File Uploads
--------------------
::
def test_upload(api):
@api.route("/upload")
async def upload(req, resp):
files = await req.media("files")
resp.media = {"name": list(files.keys())[0]}
files = {"doc": ("test.txt", b"content", "text/plain")}
r = api.requests.post(api.url_for(upload), files=files)
assert r.json() == {"name": "doc"}
+136 -399
View File
@@ -2,14 +2,14 @@ Feature Tour
============
Route Method Filtering
----------------------
Method Filtering
----------------
You can restrict routes to specific HTTP methods::
Restrict routes to specific HTTP methods::
@api.route("/items", methods=["GET"])
def list_items(req, resp):
resp.media = {"items": [...]}
resp.media = {"items": []}
@api.route("/items", methods=["POST"], check_existing=False)
async def create_item(req, resp):
@@ -20,62 +20,65 @@ You can restrict routes to specific HTTP methods::
Class-Based Views
-----------------
Class-based views (and setting some headers and stuff)::
::
@api.route("/{greeting}")
class GreetingResource:
def on_request(self, req, resp, *, greeting): # or on_get...
def on_get(self, req, resp, *, greeting):
resp.text = f"{greeting}, world!"
resp.headers.update({'X-Life': '42'})
resp.status_code = api.status_codes.HTTP_416
def on_post(self, req, resp, *, greeting):
resp.media = {"received": greeting}
def on_request(self, req, resp, *, greeting):
"""Called on every request method."""
resp.headers["X-Greeting"] = greeting
Lifespan Events
---------------
Use the lifespan context manager for startup and shutdown logic::
Use a context manager for startup and shutdown::
from contextlib import asynccontextmanager
@asynccontextmanager
async def lifespan(app):
# Startup: connect to database, etc.
print("Starting up...")
# Startup
print("connecting to database...")
yield
# Shutdown: clean up resources
print("Shutting down...")
# Shutdown
print("closing connections...")
api = responder.API(lifespan=lifespan)
You can also use the traditional event decorators::
Or use event decorators::
@api.on_event('startup')
@api.on_event("startup")
async def startup():
print("Starting up...")
print("starting up")
@api.on_event('shutdown')
@api.on_event("shutdown")
async def shutdown():
print("Shutting down...")
print("shutting down")
Serving Files
-------------
File Serving
------------
Serve files from disk with automatic content-type detection::
Serve files with automatic content-type detection::
@api.route("/download")
def download(req, resp):
resp.file("reports/annual.pdf")
You can also specify the content type explicitly::
@api.route("/image")
def image(req, resp):
resp.file("photos/cat.jpg", content_type="image/jpeg")
Custom Error Handling
---------------------
Error Handling
--------------
Register handlers for specific exception types::
@@ -85,329 +88,30 @@ Register handlers for specific exception types::
resp.media = {"error": str(exc)}
Background Tasks
----------------
Before-Request Hooks
--------------------
Here, you can spawn off a background thread to run any function, out-of-request::
Run code before every request::
@api.route("/")
def hello(req, resp):
@api.route(before_request=True)
def add_headers(req, resp):
resp.headers["X-API-Version"] = "3.1"
@api.background.task
def sleep(s=10):
time.sleep(s)
print("slept!")
Short-circuit by setting a status code — the route handler will be skipped::
sleep()
resp.content = "processing"
@api.route(before_request=True)
def auth_check(req, resp):
if "Authorization" not in req.headers:
resp.status_code = 401
resp.media = {"error": "unauthorized"}
GraphQL
-------
Responder supports GraphQL, a query language for APIs that enables clients to
request exactly the data they need.
For more information about GraphQL, visit https://graphql.org/.
Serve a GraphQL API::
import graphene
from responder.ext.graphql import GraphQLView
class Query(graphene.ObjectType):
hello = graphene.String(name=graphene.String(default_value="stranger"))
def resolve_hello(self, info, name):
return f"Hello {name}"
schema = graphene.Schema(query=Query)
view = GraphQLView(api=api, schema=schema)
api.add_route("/graph", view)
Visiting the endpoint will render a *GraphiQL* instance, in the browser.
You can make use of Responder's Request and Response objects in your GraphQL resolvers through ``info.context['request']`` and ``info.context['response']``.
OpenAPI Schema Support
----------------------
Responder comes with built-in support for OpenAPI / marshmallow.
.. note::
If you're upgrading from a previous version, note that the OpenAPI module
has been renamed from ``responder.ext.schema`` to ``responder.ext.openapi``.
Update your imports accordingly.
New in Responder 1.4.0::
import responder
from responder.ext.openapi import OpenAPISchema
from marshmallow import Schema, fields
contact = {
"name": "API Support",
"url": "http://www.example.com/support",
"email": "support@example.com",
}
license = {
"name": "Apache 2.0",
"url": "https://www.apache.org/licenses/LICENSE-2.0.html",
}
api = responder.API()
schema = OpenAPISchema(
app=api,
title="Web Service",
version="1.0",
openapi="3.0.2",
description="A simple pet store",
terms_of_service="http://example.com/terms/",
contact=contact,
license=license,
)
@schema.schema("Pet")
class PetSchema(Schema):
name = fields.Str()
@api.route("/")
def route(req, resp):
"""A cute furry animal endpoint.
---
get:
description: Get a random pet
responses:
200:
description: A pet to be returned
content:
application/json:
schema:
$ref: '#/components/schemas/Pet'
"""
resp.media = PetSchema().dump({"name": "little orange"})
Old way *It's recommended to use the code above* ::
import responder
from marshmallow import Schema, fields
contact = {
"name": "API Support",
"url": "http://www.example.com/support",
"email": "support@example.com",
}
license = {
"name": "Apache 2.0",
"url": "https://www.apache.org/licenses/LICENSE-2.0.html",
}
api = responder.API(
title="Web Service",
version="1.0",
openapi="3.0.2",
description="A simple pet store",
terms_of_service="http://example.com/terms/",
contact=contact,
license=license,
)
@api.schema("Pet")
class PetSchema(Schema):
name = fields.Str()
@api.route("/")
def route(req, resp):
"""A cute furry animal endpoint.
---
get:
description: Get a random pet
responses:
200:
description: A pet to be returned
content:
application/json:
schema:
$ref: '#/components/schemas/Pet'
"""
resp.media = PetSchema().dump({"name": "little orange"})
WebSockets
----------
::
>>> r = api.session().get("http://;/schema.yml")
>>> print(r.text)
components:
parameters: {}
responses: {}
schemas:
Pet:
properties:
name: {type: string}
type: object
securitySchemes: {}
info:
contact: {email: support@example.com, name: API Support, url: 'http://www.example.com/support'}
description: This is a sample server for a pet store.
license: {name: Apache 2.0, url: 'https://www.apache.org/licenses/LICENSE-2.0.html'}
termsOfService: http://example.com/terms/
title: Web Service
version: 1.0
openapi: 3.0.2
paths:
/:
get:
description: Get a random pet
responses:
200: {description: A pet to be returned, schema: $ref: "#/components/schemas/Pet"}
tags: []
Interactive Documentation
-------------------------
Responder can automatically supply API Documentation for you. Using the example above
The new and recommended way::
from responder.ext.openapi import OpenAPISchema
api = responder.API()
schema = OpenAPISchema(
app=api,
title="Web Service",
version="1.0",
openapi="3.0.2",
...
docs_route='/docs',
...
description=description,
terms_of_service=terms_of_service,
contact=contact,
license=license,
)
The old way::
api = responder.API(
title="Web Service",
version="1.0",
openapi="3.0.2",
docs_route='/docs',
description=description,
terms_of_service=terms_of_service,
contact=contact,
license=license,
)
This will make ``/docs`` render interactive documentation for your API.
Mount a WSGI / ASGI Apps (e.g. Flask, Starlette,...)
----------------------------------------------------
Responder gives you the ability to mount another ASGI / WSGI app at a subroute::
import responder
from flask import Flask
api = responder.API()
flask = Flask(__name__)
@flask.route('/')
def hello():
return 'hello'
api.mount('/flask', flask)
That's it!
Single-Page Web Apps
--------------------
If you have a single-page webapp, you can tell Responder to serve up your ``static/index.html`` at a route, like so::
api.add_route("/", static=True)
This will make ``index.html`` the default response to all undefined routes.
Reading / Writing Cookies
-------------------------
Responder makes it very easy to interact with cookies from a Request, or add some to a Response::
>>> resp.cookies["hello"] = "world"
>>> req.cookies
{"hello": "world"}
To set cookies directives, you should use `resp.set_cookie`::
>>> resp.set_cookie("hello", value="world", max_age=60)
Supported directives:
* ``key`` - **Required**
* ``value`` - [OPTIONAL] - Defaults to ``""``.
* ``expires`` - Defaults to ``None``.
* ``max_age`` - Defaults to ``None``.
* ``domain`` - Defaults to ``None``.
* ``path`` - Defaults to ``"/"``.
* ``secure`` - Defaults to ``False``.
* ``httponly`` - Defaults to ``True``.
For more information see `directives <https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie>`_
Using Cookie-Based Sessions
---------------------------
Responder has built-in support for cookie-based sessions. To enable cookie-based sessions, simply add something to the ``resp.session`` dictionary::
>>> resp.session['username'] = 'kennethreitz'
A cookie called ``Responder-Session`` will be set, which contains all the data in ``resp.session``. It is signed, for verification purposes.
You can easily read a Request's session data, that can be trusted to have originated from the API::
>>> req.session
{'username': 'kennethreitz'}
**Note**: if you are using this in production, you should pass the ``secret_key`` argument to ``API(...)``::
api = responder.API(secret_key=os.environ['SECRET_KEY'])
Using ``before_request``
------------------------
If you'd like a view to be executed before every request, simply do the following::
@api.route(before_request=True)
def prepare_response(req, resp):
resp.headers["X-Pizza"] = "42"
Now all requests to your HTTP Service will include an ``X-Pizza`` header.
For ``websockets``::
@api.route(before_request=True, websocket=True)
def prepare_response(ws):
await ws.accept()
WebSocket Support
-----------------
Responder supports WebSockets::
@api.route('/ws', websocket=True)
@api.route("/ws", websocket=True)
async def websocket(ws):
await ws.accept()
while True:
@@ -415,93 +119,126 @@ Responder supports WebSockets::
await ws.send_text(f"Hello {name}!")
await ws.close()
Accepting the connection::
Supported formats: ``send_text``, ``send_json``, ``send_bytes``.
await websocket.accept()
Sending and receiving data::
GraphQL
-------
await websocket.send_{format}(data)
await websocket.receive_{format}(data)
One-liner setup with `Graphene <https://graphene-python.org/>`_::
Supported formats: ``text``, ``json``, ``bytes``.
import graphene
Closing the connection::
class Query(graphene.ObjectType):
hello = graphene.String(name=graphene.String(default_value="stranger"))
def resolve_hello(self, info, name):
return f"Hello {name}"
await websocket.close()
api.graphql("/graphql", schema=graphene.Schema(query=Query))
Using Requests Test Client
--------------------------
Visiting ``/graphql`` in a browser renders the GraphiQL IDE.
Responder comes with a first-class, well supported test client for your ASGI web services (powered by Starlette's TestClient).
Here's an example of a test (written with pytest)::
import myapi
@pytest.fixture
def api():
return myapi.api
def test_response(api):
hello = "hello, world!"
@api.route('/some-url')
def some_view(req, resp):
resp.text = hello
r = api.requests.get(url=api.url_for(some_view))
assert r.text == hello
HSTS (Redirect to HTTPS)
------------------------
Want HSTS (to redirect all traffic to HTTPS)?
OpenAPI
-------
::
api = responder.API(enable_hsts=True)
api = responder.API(
title="My API",
version="1.0",
openapi="3.0.2",
docs_route="/docs",
)
Visit ``/docs`` for interactive Swagger UI documentation.
The schema is served at ``/schema.yml``.
Boom.
Mounting Apps
-------------
Mount any WSGI or ASGI application at a subroute::
from flask import Flask
flask_app = Flask(__name__)
@flask_app.route("/")
def hello():
return "Hello from Flask!"
api.mount("/flask", flask_app)
Cookies
-------
::
# Read cookies
req.cookies["session_id"]
# Set cookies
resp.cookies["hello"] = "world"
# With directives
resp.set_cookie("token", value="abc", max_age=3600, secure=True)
Sessions
--------
Built-in cookie-based sessions::
@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")}
Set a secret key for production::
api = responder.API(secret_key="your-secret-key")
Static Files
------------
Static files are served from the ``static/`` directory by default::
api = responder.API(static_dir="static", static_route="/static")
For single-page apps, serve ``index.html`` as the default::
api.add_route("/", static=True)
CORS
----
Want `CORS <https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS/>`_ ?
::
api = responder.API(cors=True)
api = responder.API(cors=True, cors_params={
"allow_origins": ["https://example.com"],
"allow_methods": ["GET", "POST"],
"allow_headers": ["*"],
})
The default parameters used by **Responder** are restrictive by default, so you'll need to explicitly enable particular origins, methods, or headers, in order for browsers to be permitted to use them in a Cross-Domain context.
HSTS
----
In order to set custom parameters, you need to set the ``cors_params`` argument of ``api``, a dictionary containing the following entries:
Redirect all traffic to HTTPS::
api = responder.API(enable_hsts=True)
* ``allow_origins`` - A list of origins that should be permitted to make cross-origin requests. eg. ``['https://example.org', 'https://www.example.org']``. You can use ``['*']`` to allow any origin.
* ``allow_origin_regex`` - A regex string to match against origins that should be permitted to make cross-origin requests. eg. ``'https://.*\.example\.org'``.
* ``allow_methods`` - A list of HTTP methods that should be allowed for cross-origin requests. Defaults to `['GET']`. You can use ``['*']`` to allow all standard methods.
* ``allow_headers`` - A list of HTTP request headers that should be supported for cross-origin requests. Defaults to ``[]``. You can use ``['*']`` to allow all headers. The ``Accept``, ``Accept-Language``, ``Content-Language`` and ``Content-Type`` headers are always allowed for CORS requests.
* ``allow_credentials`` - Indicate that cookies should be supported for cross-origin requests. Defaults to ``False``.
* ``expose_headers`` - Indicate any response headers that should be made accessible to the browser. Defaults to ``[]``.
* ``max_age`` - Sets a maximum time in seconds for browsers to cache CORS responses. Defaults to ``60``.
Trusted Hosts
-------------
Make sure that all the incoming requests headers have a valid ``host``, that matches one of the provided patterns in the ``allowed_hosts`` attribute, in order to prevent HTTP Host Header attacks.
A 400 response will be raised, if a request does not match any of the provided patterns in the ``allowed_hosts`` attribute.
::
api = responder.API(allowed_hosts=['example.com', 'tenant.example.com'])
* ``allowed_hosts`` - A list of allowed hostnames.
Note:
* By default, all hostnames are allowed.
* Wildcard domains such as ``*.example.com`` are supported.
* To allow any hostname use ``allowed_hosts=["*"]``.
api = responder.API(allowed_hosts=["example.com", "*.example.com"])
+1 -2
View File
@@ -52,12 +52,11 @@ develop = [
]
docs = [
"alabaster<1.1",
"myst-parser[linkify]",
"myst-parser",
"sphinx>=5,<9",
"sphinx-autobuild",
"sphinx-copybutton",
"sphinx-design-elements",
"sphinxext.opengraph",
]
release = ["build", "twine"]
test = [