summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBert JW Regeer <bertjw@regeer.org>2016-04-14 10:47:00 -0600
committerBert JW Regeer <bertjw@regeer.org>2016-04-14 10:47:00 -0600
commitaac7a47497ad4a4cd1c5f0ca0d19bd1de460a281 (patch)
tree7663f087713be5487846663a0b64e086f60f8de3
parent4c6592734ffb5eace320d8e92e33c5866f111749 (diff)
parent88637857ca84eb74fd318ad1bf8c4464e50ae662 (diff)
downloadpyramid-aac7a47497ad4a4cd1c5f0ca0d19bd1de460a281.tar.gz
pyramid-aac7a47497ad4a4cd1c5f0ca0d19bd1de460a281.tar.bz2
pyramid-aac7a47497ad4a4cd1c5f0ca0d19bd1de460a281.zip
Merge branch 'master' into feature/json_exceptions
-rw-r--r--CHANGES.txt8
-rw-r--r--TODO.txt1
-rw-r--r--docs/_static/pyramid_request_processing.graffle881
-rw-r--r--docs/_static/pyramid_request_processing.pngbin123953 -> 130688 bytes
-rw-r--r--docs/_static/pyramid_request_processing.svg2
-rw-r--r--docs/api/events.rst2
-rw-r--r--docs/api/interfaces.rst3
-rw-r--r--docs/glossary.rst8
-rw-r--r--docs/narr/hooks.rst42
-rw-r--r--docs/narr/router.rst25
-rw-r--r--docs/narr/sessions.rst70
-rw-r--r--docs/narr/viewconfig.rst26
-rw-r--r--pyramid/config/settings.py6
-rw-r--r--pyramid/config/views.py51
-rw-r--r--pyramid/events.py24
-rw-r--r--pyramid/interfaces.py8
-rw-r--r--pyramid/router.py15
-rw-r--r--pyramid/session.py3
-rw-r--r--pyramid/settings.py7
-rw-r--r--pyramid/tests/test_config/test_views.py60
-rw-r--r--pyramid/tests/test_events.py35
-rw-r--r--pyramid/tests/test_router.py4
-rw-r--r--pyramid/tests/test_viewderivers.py152
-rw-r--r--pyramid/view.py3
-rw-r--r--pyramid/viewderivers.py41
25 files changed, 1073 insertions, 404 deletions
diff --git a/CHANGES.txt b/CHANGES.txt
index d5640ee0b..96d8d8236 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -8,6 +8,14 @@ unreleased
receive a valid JSON response. See:
https://github.com/Pylons/pyramid/pull/2489
+- (Deprecation) Support for Python 3.3 will be removed in Pyramid 1.8.
+ https://github.com/Pylons/pyramid/issues/2477
+
+- A new event and interface (BeforeTraversal) has been introduced that will
+ notify listeners before traversal starts in the router. See
+ https://github.com/Pylons/pyramid/pull/2469 and
+ https://github.com/Pylons/pyramid/pull/1876
+
- Python 2.6 is no longer supported by Pyramid. See
https://github.com/Pylons/pyramid/issues/2368
diff --git a/TODO.txt b/TODO.txt
index 837c9d681..ff567bca8 100644
--- a/TODO.txt
+++ b/TODO.txt
@@ -124,6 +124,7 @@ Future
``hashalg`` to ``sha512``.
- 1.8: Remove set_request_property.
+- 1.8: Drop Python 3.3 support.
- 1.9: Remove extra code enabling ``pyramid.security.remember(principal=...)``
and force use of ``userid``.
diff --git a/docs/_static/pyramid_request_processing.graffle b/docs/_static/pyramid_request_processing.graffle
index 16b360543..56e4e13f2 100644
--- a/docs/_static/pyramid_request_processing.graffle
+++ b/docs/_static/pyramid_request_processing.graffle
@@ -22,7 +22,7 @@
<key>Font</key>
<string>Helvetica</string>
<key>Size</key>
- <real>12</real>
+ <real>13</real>
</dict>
<key>ID</key>
<integer>2</integer>
@@ -59,6 +59,181 @@
<key>GraphicsList</key>
<array>
<dict>
+ <key>Bounds</key>
+ <string>{{238.74999618530273, 294.65604172230951}, {105.66668701171875, 18.656048080136394}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>FontInfo</key>
+ <dict>
+ <key>Font</key>
+ <string>Helvetica</string>
+ <key>Size</key>
+ <real>11</real>
+ </dict>
+ <key>ID</key>
+ <integer>169515</integer>
+ <key>Layer</key>
+ <integer>0</integer>
+ <key>Magnets</key>
+ <array>
+ <string>{0, 1}</string>
+ <string>{0, -1}</string>
+ <string>{1, 0}</string>
+ <string>{-1, 0}</string>
+ <string>{0.50000000000000089, -0.49999999999999645}</string>
+ <string>{-0.49526813868737474, -0.4689979626999552}</string>
+ </array>
+ <key>Shape</key>
+ <string>Rectangle</string>
+ <key>Style</key>
+ <dict>
+ <key>fill</key>
+ <dict>
+ <key>Color</key>
+ <dict>
+ <key>b</key>
+ <string>0.637876</string>
+ <key>g</key>
+ <string>1</string>
+ <key>r</key>
+ <string>1</string>
+ </dict>
+ </dict>
+ <key>shadow</key>
+ <dict>
+ <key>Draws</key>
+ <string>NO</string>
+ <key>ShadowVector</key>
+ <string>{2, 2}</string>
+ </dict>
+ </dict>
+ <key>Text</key>
+ <dict>
+ <key>Text</key>
+ <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400
+\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;}
+{\colortbl;\red255\green255\blue255;}
+\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc
+
+\f0\fs20 \cf0 CSRF checks}</string>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Class</key>
+ <string>LineGraphic</string>
+ <key>FontInfo</key>
+ <dict>
+ <key>Color</key>
+ <dict>
+ <key>w</key>
+ <string>0</string>
+ </dict>
+ <key>Font</key>
+ <string>Helvetica</string>
+ <key>Size</key>
+ <real>12</real>
+ </dict>
+ <key>Head</key>
+ <dict>
+ <key>ID</key>
+ <integer>169513</integer>
+ </dict>
+ <key>ID</key>
+ <integer>169514</integer>
+ <key>Layer</key>
+ <integer>0</integer>
+ <key>Points</key>
+ <array>
+ <string>{154.9999760464211, 209.11365574251681}</string>
+ <string>{239.8333613077798, 209.14732074737549}</string>
+ </array>
+ <key>Style</key>
+ <dict>
+ <key>stroke</key>
+ <dict>
+ <key>Color</key>
+ <dict>
+ <key>b</key>
+ <string>0.0980392</string>
+ <key>g</key>
+ <string>0.0980392</string>
+ <key>r</key>
+ <string>0.0980392</string>
+ </dict>
+ <key>HeadArrow</key>
+ <string>0</string>
+ <key>Legacy</key>
+ <true/>
+ <key>Pattern</key>
+ <integer>2</integer>
+ <key>TailArrow</key>
+ <string>0</string>
+ </dict>
+ </dict>
+ <key>Tail</key>
+ <dict>
+ <key>ID</key>
+ <integer>169373</integer>
+ <key>Position</key>
+ <real>0.47711458802223206</real>
+ </dict>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{239.83336130777977, 197.875}, {105.66666412353516, 22.544641494750977}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>169513</integer>
+ <key>Layer</key>
+ <integer>0</integer>
+ <key>Magnets</key>
+ <array>
+ <string>{0, 1}</string>
+ <string>{0, -1}</string>
+ <string>{1, 0}</string>
+ <string>{-1, 0}</string>
+ </array>
+ <key>Shape</key>
+ <string>Rectangle</string>
+ <key>Style</key>
+ <dict>
+ <key>fill</key>
+ <dict>
+ <key>Color</key>
+ <dict>
+ <key>b</key>
+ <string>0.999449</string>
+ <key>g</key>
+ <string>0.743511</string>
+ <key>r</key>
+ <string>0.872276</string>
+ </dict>
+ </dict>
+ <key>shadow</key>
+ <dict>
+ <key>Draws</key>
+ <string>NO</string>
+ <key>ShadowVector</key>
+ <string>{2, 2}</string>
+ </dict>
+ </dict>
+ <key>Text</key>
+ <dict>
+ <key>Text</key>
+ <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400
+\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;}
+{\colortbl;\red255\green255\blue255;}
+\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc
+
+\f0\fs20 \cf0 BeforeTraversal}</string>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ </dict>
+ <dict>
<key>Class</key>
<string>LineGraphic</string>
<key>FontInfo</key>
@@ -84,8 +259,8 @@
<integer>0</integer>
<key>Points</key>
<array>
- <string>{344.41668319702148, 402.88506673894034}</string>
- <string>{375.5, 402.77232108797347}</string>
+ <string>{344.41668319702148, 411.88506673894034}</string>
+ <string>{375.5, 411.77232108797347}</string>
</array>
<key>Style</key>
<dict>
@@ -237,378 +412,430 @@
</dict>
</dict>
<dict>
+ <key>Bounds</key>
+ <string>{{238.74999618530273, 275.99999999999994}, {105.75002924601222, 18.656048080136394}}</string>
<key>Class</key>
- <string>Group</string>
- <key>Graphics</key>
+ <string>ShapedGraphic</string>
+ <key>FontInfo</key>
+ <dict>
+ <key>Font</key>
+ <string>Helvetica</string>
+ <key>Size</key>
+ <real>11</real>
+ </dict>
+ <key>ID</key>
+ <integer>169506</integer>
+ <key>Layer</key>
+ <integer>0</integer>
+ <key>Magnets</key>
<array>
+ <string>{0, 1}</string>
+ <string>{0, -1}</string>
+ <string>{1, 0}</string>
+ <string>{-1, 0}</string>
+ <string>{0.50000000000000089, -0.49999999999999645}</string>
+ <string>{-0.49526813868737474, -0.4689979626999552}</string>
+ </array>
+ <key>Shape</key>
+ <string>Rectangle</string>
+ <key>Style</key>
+ <dict>
+ <key>fill</key>
<dict>
- <key>Bounds</key>
- <string>{{238.74999618530273, 284.99999999999994}, {105.75002924601222, 18.656048080136394}}</string>
- <key>Class</key>
- <string>ShapedGraphic</string>
- <key>ID</key>
- <integer>169506</integer>
- <key>Magnets</key>
- <array>
- <string>{0, 1}</string>
- <string>{0, -1}</string>
- <string>{1, 0}</string>
- <string>{-1, 0}</string>
- <string>{0.50000000000000089, -0.49999999999999645}</string>
- <string>{-0.49526813868737474, -0.4689979626999552}</string>
- </array>
- <key>Shape</key>
- <string>Rectangle</string>
- <key>Style</key>
+ <key>Color</key>
<dict>
- <key>fill</key>
- <dict>
- <key>Color</key>
- <dict>
- <key>b</key>
- <string>0.637876</string>
- <key>g</key>
- <string>1</string>
- <key>r</key>
- <string>1</string>
- </dict>
- </dict>
- <key>shadow</key>
- <dict>
- <key>Draws</key>
- <string>NO</string>
- <key>ShadowVector</key>
- <string>{2, 2}</string>
- </dict>
+ <key>b</key>
+ <string>0.637876</string>
+ <key>g</key>
+ <string>1</string>
+ <key>r</key>
+ <string>1</string>
</dict>
- <key>Text</key>
- <dict>
- <key>Text</key>
- <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400
+ </dict>
+ <key>shadow</key>
+ <dict>
+ <key>Draws</key>
+ <string>NO</string>
+ <key>ShadowVector</key>
+ <string>{2, 2}</string>
+ </dict>
+ </dict>
+ <key>Text</key>
+ <dict>
+ <key>Text</key>
+ <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400
\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;}
{\colortbl;\red255\green255\blue255;}
\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc
\f0\fs20 \cf0 authorization}</string>
- <key>VerticalPad</key>
- <integer>0</integer>
- </dict>
- </dict>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{238.74999618530273, 421.15071036499205}, {105.66668701171875, 18.656048080136394}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>FontInfo</key>
+ <dict>
+ <key>Font</key>
+ <string>Helvetica</string>
+ <key>Size</key>
+ <real>11</real>
+ </dict>
+ <key>ID</key>
+ <integer>169507</integer>
+ <key>Layer</key>
+ <integer>0</integer>
+ <key>Magnets</key>
+ <array>
+ <string>{0, 1}</string>
+ <string>{0, -1}</string>
+ <string>{1, 0}</string>
+ <string>{-1, 0}</string>
+ <string>{0.50000000000000089, 0.5}</string>
+ <string>{-0.49999999999999911, 0.49999999999999289}</string>
+ </array>
+ <key>Shape</key>
+ <string>Rectangle</string>
+ <key>Style</key>
+ <dict>
+ <key>fill</key>
<dict>
- <key>Bounds</key>
- <string>{{238.74999618530273, 412.15071036499205}, {105.66668701171875, 18.656048080136394}}</string>
- <key>Class</key>
- <string>ShapedGraphic</string>
- <key>ID</key>
- <integer>169507</integer>
- <key>Magnets</key>
- <array>
- <string>{0, 1}</string>
- <string>{0, -1}</string>
- <string>{1, 0}</string>
- <string>{-1, 0}</string>
- <string>{0.50000000000000089, 0.5}</string>
- <string>{-0.49999999999999911, 0.49999999999999289}</string>
- </array>
- <key>Shape</key>
- <string>Rectangle</string>
- <key>Style</key>
+ <key>Color</key>
<dict>
- <key>fill</key>
- <dict>
- <key>Color</key>
- <dict>
- <key>b</key>
- <string>0.637876</string>
- <key>g</key>
- <string>1</string>
- <key>r</key>
- <string>1</string>
- </dict>
- </dict>
- <key>shadow</key>
- <dict>
- <key>Draws</key>
- <string>NO</string>
- <key>ShadowVector</key>
- <string>{2, 2}</string>
- </dict>
+ <key>b</key>
+ <string>0.637876</string>
+ <key>g</key>
+ <string>1</string>
+ <key>r</key>
+ <string>1</string>
</dict>
- <key>Text</key>
- <dict>
- <key>Text</key>
- <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400
+ </dict>
+ <key>shadow</key>
+ <dict>
+ <key>Draws</key>
+ <string>NO</string>
+ <key>ShadowVector</key>
+ <string>{2, 2}</string>
+ </dict>
+ </dict>
+ <key>Text</key>
+ <dict>
+ <key>Text</key>
+ <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400
\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;}
{\colortbl;\red255\green255\blue255;}
\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc
\f0\fs20 \cf0 decorators egress}</string>
- <key>VerticalPad</key>
- <integer>0</integer>
- </dict>
- </dict>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{238.74999618530273, 312.65604172230951}, {105.66668701171875, 18.656048080136394}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>FontInfo</key>
+ <dict>
+ <key>Font</key>
+ <string>Helvetica</string>
+ <key>Size</key>
+ <real>11</real>
+ </dict>
+ <key>ID</key>
+ <integer>169508</integer>
+ <key>Layer</key>
+ <integer>0</integer>
+ <key>Magnets</key>
+ <array>
+ <string>{0, 1}</string>
+ <string>{0, -1}</string>
+ <string>{1, 0}</string>
+ <string>{-1, 0}</string>
+ <string>{0.50000000000000089, -0.49999999999999645}</string>
+ <string>{-0.49526813868737474, -0.4689979626999552}</string>
+ </array>
+ <key>Shape</key>
+ <string>Rectangle</string>
+ <key>Style</key>
+ <dict>
+ <key>fill</key>
<dict>
- <key>Bounds</key>
- <string>{{238.74999618530273, 303.65604172230951}, {105.66668701171875, 18.656048080136394}}</string>
- <key>Class</key>
- <string>ShapedGraphic</string>
- <key>ID</key>
- <integer>169508</integer>
- <key>Magnets</key>
- <array>
- <string>{0, 1}</string>
- <string>{0, -1}</string>
- <string>{1, 0}</string>
- <string>{-1, 0}</string>
- <string>{0.50000000000000089, -0.49999999999999645}</string>
- <string>{-0.49526813868737474, -0.4689979626999552}</string>
- </array>
- <key>Shape</key>
- <string>Rectangle</string>
- <key>Style</key>
+ <key>Color</key>
<dict>
- <key>fill</key>
- <dict>
- <key>Color</key>
- <dict>
- <key>b</key>
- <string>0.637876</string>
- <key>g</key>
- <string>1</string>
- <key>r</key>
- <string>1</string>
- </dict>
- </dict>
- <key>shadow</key>
- <dict>
- <key>Draws</key>
- <string>NO</string>
- <key>ShadowVector</key>
- <string>{2, 2}</string>
- </dict>
+ <key>b</key>
+ <string>0.637876</string>
+ <key>g</key>
+ <string>1</string>
+ <key>r</key>
+ <string>1</string>
</dict>
- <key>Text</key>
- <dict>
- <key>Text</key>
- <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400
+ </dict>
+ <key>shadow</key>
+ <dict>
+ <key>Draws</key>
+ <string>NO</string>
+ <key>ShadowVector</key>
+ <string>{2, 2}</string>
+ </dict>
+ </dict>
+ <key>Text</key>
+ <dict>
+ <key>Text</key>
+ <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400
\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;}
{\colortbl;\red255\green255\blue255;}
\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc
\f0\fs20 \cf0 decorators ingress}</string>
- <key>VerticalPad</key>
- <integer>0</integer>
- </dict>
- </dict>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{238.74999618530273, 402.55704269887212}, {105.66668701171875, 18.656048080136394}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>FontInfo</key>
+ <dict>
+ <key>Font</key>
+ <string>Helvetica</string>
+ <key>Size</key>
+ <real>11</real>
+ </dict>
+ <key>ID</key>
+ <integer>169509</integer>
+ <key>Layer</key>
+ <integer>0</integer>
+ <key>Magnets</key>
+ <array>
+ <string>{0, 1}</string>
+ <string>{0, -1}</string>
+ <string>{1, 0}</string>
+ <string>{-1, 0}</string>
+ </array>
+ <key>Shape</key>
+ <string>Rectangle</string>
+ <key>Style</key>
+ <dict>
+ <key>fill</key>
<dict>
- <key>Bounds</key>
- <string>{{238.74999618530273, 393.55704269887212}, {105.66668701171875, 18.656048080136394}}</string>
- <key>Class</key>
- <string>ShapedGraphic</string>
- <key>ID</key>
- <integer>169509</integer>
- <key>Magnets</key>
- <array>
- <string>{0, 1}</string>
- <string>{0, -1}</string>
- <string>{1, 0}</string>
- <string>{-1, 0}</string>
- </array>
- <key>Shape</key>
- <string>Rectangle</string>
- <key>Style</key>
+ <key>Color</key>
<dict>
- <key>fill</key>
- <dict>
- <key>Color</key>
- <dict>
- <key>b</key>
- <string>0.637876</string>
- <key>g</key>
- <string>1</string>
- <key>r</key>
- <string>1</string>
- </dict>
- </dict>
- <key>shadow</key>
- <dict>
- <key>Draws</key>
- <string>NO</string>
- <key>ShadowVector</key>
- <string>{2, 2}</string>
- </dict>
+ <key>b</key>
+ <string>0.637876</string>
+ <key>g</key>
+ <string>1</string>
+ <key>r</key>
+ <string>1</string>
</dict>
- <key>Text</key>
- <dict>
- <key>Text</key>
- <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400
+ </dict>
+ <key>shadow</key>
+ <dict>
+ <key>Draws</key>
+ <string>NO</string>
+ <key>ShadowVector</key>
+ <string>{2, 2}</string>
+ </dict>
+ </dict>
+ <key>Text</key>
+ <dict>
+ <key>Text</key>
+ <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400
\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;}
{\colortbl;\red255\green255\blue255;}
\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc
\f0\fs20 \cf0 response adapter}</string>
- <key>VerticalPad</key>
- <integer>0</integer>
- </dict>
- </dict>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{238.74999618530273, 383.90099016834085}, {105.66668701171875, 18.656048080136394}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>FontInfo</key>
+ <dict>
+ <key>Font</key>
+ <string>Helvetica</string>
+ <key>Size</key>
+ <real>11</real>
+ </dict>
+ <key>ID</key>
+ <integer>169510</integer>
+ <key>Layer</key>
+ <integer>0</integer>
+ <key>Magnets</key>
+ <array>
+ <string>{0, 1}</string>
+ <string>{0, -1}</string>
+ <string>{1, 0}</string>
+ <string>{-1, 0}</string>
+ </array>
+ <key>Shape</key>
+ <string>Rectangle</string>
+ <key>Style</key>
+ <dict>
+ <key>fill</key>
<dict>
- <key>Bounds</key>
- <string>{{238.74999618530273, 374.90099016834085}, {105.66668701171875, 18.656048080136394}}</string>
- <key>Class</key>
- <string>ShapedGraphic</string>
- <key>ID</key>
- <integer>169510</integer>
- <key>Magnets</key>
- <array>
- <string>{0, 1}</string>
- <string>{0, -1}</string>
- <string>{1, 0}</string>
- <string>{-1, 0}</string>
- </array>
- <key>Shape</key>
- <string>Rectangle</string>
- <key>Style</key>
+ <key>Color</key>
<dict>
- <key>fill</key>
- <dict>
- <key>Color</key>
- <dict>
- <key>b</key>
- <string>0.637876</string>
- <key>g</key>
- <string>1</string>
- <key>r</key>
- <string>1</string>
- </dict>
- </dict>
- <key>shadow</key>
- <dict>
- <key>Draws</key>
- <string>NO</string>
- <key>ShadowVector</key>
- <string>{2, 2}</string>
- </dict>
+ <key>b</key>
+ <string>0.637876</string>
+ <key>g</key>
+ <string>1</string>
+ <key>r</key>
+ <string>1</string>
</dict>
- <key>Text</key>
- <dict>
- <key>Text</key>
- <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400
+ </dict>
+ <key>shadow</key>
+ <dict>
+ <key>Draws</key>
+ <string>NO</string>
+ <key>ShadowVector</key>
+ <string>{2, 2}</string>
+ </dict>
+ </dict>
+ <key>Text</key>
+ <dict>
+ <key>Text</key>
+ <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400
\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;}
{\colortbl;\red255\green255\blue255;}
\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc
\f0\fs20 \cf0 view mapper egress}</string>
- <key>VerticalPad</key>
- <integer>0</integer>
- </dict>
- </dict>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{238.74999618530273, 350.36561209044055}, {105.66668701171875, 33.089282989501953}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>FontInfo</key>
+ <dict>
+ <key>Font</key>
+ <string>Helvetica</string>
+ <key>Size</key>
+ <real>11</real>
+ </dict>
+ <key>ID</key>
+ <integer>169511</integer>
+ <key>Layer</key>
+ <integer>0</integer>
+ <key>Magnets</key>
+ <array>
+ <string>{0, 1}</string>
+ <string>{0, -1}</string>
+ <string>{1, 0}</string>
+ <string>{-1, 0}</string>
+ </array>
+ <key>Shape</key>
+ <string>Rectangle</string>
+ <key>Style</key>
+ <dict>
+ <key>fill</key>
<dict>
- <key>Bounds</key>
- <string>{{238.74999618530273, 341.36561209044055}, {105.66668701171875, 33.089282989501953}}</string>
- <key>Class</key>
- <string>ShapedGraphic</string>
- <key>ID</key>
- <integer>169511</integer>
- <key>Magnets</key>
- <array>
- <string>{0, 1}</string>
- <string>{0, -1}</string>
- <string>{1, 0}</string>
- <string>{-1, 0}</string>
- </array>
- <key>Shape</key>
- <string>Rectangle</string>
- <key>Style</key>
+ <key>Color</key>
<dict>
- <key>fill</key>
- <dict>
- <key>Color</key>
- <dict>
- <key>b</key>
- <string>0.422927</string>
- <key>g</key>
- <string>1</string>
- <key>r</key>
- <string>1</string>
- </dict>
- </dict>
- <key>shadow</key>
- <dict>
- <key>Draws</key>
- <string>NO</string>
- <key>ShadowVector</key>
- <string>{2, 2}</string>
- </dict>
+ <key>b</key>
+ <string>0.422927</string>
+ <key>g</key>
+ <string>1</string>
+ <key>r</key>
+ <string>1</string>
</dict>
- <key>Text</key>
- <dict>
- <key>Text</key>
- <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400
+ </dict>
+ <key>shadow</key>
+ <dict>
+ <key>Draws</key>
+ <string>NO</string>
+ <key>ShadowVector</key>
+ <string>{2, 2}</string>
+ </dict>
+ </dict>
+ <key>Text</key>
+ <dict>
+ <key>Text</key>
+ <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400
\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;}
{\colortbl;\red255\green255\blue255;}
\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc
\f0\fs20 \cf0 view}</string>
- <key>VerticalPad</key>
- <integer>0</integer>
- </dict>
- </dict>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{238.74999618530273, 331.26348241170439}, {105.66668701171875, 18.656048080136394}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>FontInfo</key>
+ <dict>
+ <key>Font</key>
+ <string>Helvetica</string>
+ <key>Size</key>
+ <real>11</real>
+ </dict>
+ <key>ID</key>
+ <integer>169512</integer>
+ <key>Layer</key>
+ <integer>0</integer>
+ <key>Magnets</key>
+ <array>
+ <string>{0, 1}</string>
+ <string>{0, -1}</string>
+ <string>{1, 0}</string>
+ <string>{-1, 0}</string>
+ </array>
+ <key>Shape</key>
+ <string>Rectangle</string>
+ <key>Style</key>
+ <dict>
+ <key>fill</key>
<dict>
- <key>Bounds</key>
- <string>{{238.74999618530273, 322.26348241170439}, {105.66668701171875, 18.656048080136394}}</string>
- <key>Class</key>
- <string>ShapedGraphic</string>
- <key>ID</key>
- <integer>169512</integer>
- <key>Magnets</key>
- <array>
- <string>{0, 1}</string>
- <string>{0, -1}</string>
- <string>{1, 0}</string>
- <string>{-1, 0}</string>
- </array>
- <key>Shape</key>
- <string>Rectangle</string>
- <key>Style</key>
+ <key>Color</key>
<dict>
- <key>fill</key>
- <dict>
- <key>Color</key>
- <dict>
- <key>b</key>
- <string>0.637876</string>
- <key>g</key>
- <string>1</string>
- <key>r</key>
- <string>1</string>
- </dict>
- </dict>
- <key>shadow</key>
- <dict>
- <key>Draws</key>
- <string>NO</string>
- <key>ShadowVector</key>
- <string>{2, 2}</string>
- </dict>
+ <key>b</key>
+ <string>0.637876</string>
+ <key>g</key>
+ <string>1</string>
+ <key>r</key>
+ <string>1</string>
</dict>
- <key>Text</key>
- <dict>
- <key>Text</key>
- <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400
+ </dict>
+ <key>shadow</key>
+ <dict>
+ <key>Draws</key>
+ <string>NO</string>
+ <key>ShadowVector</key>
+ <string>{2, 2}</string>
+ </dict>
+ </dict>
+ <key>Text</key>
+ <dict>
+ <key>Text</key>
+ <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400
\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;}
{\colortbl;\red255\green255\blue255;}
\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc
\f0\fs20 \cf0 view mapper ingress}</string>
- <key>VerticalPad</key>
- <integer>0</integer>
- </dict>
- </dict>
- </array>
- <key>ID</key>
- <integer>169505</integer>
- <key>Layer</key>
- <integer>0</integer>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
</dict>
<dict>
<key>Class</key>
@@ -1094,7 +1321,7 @@
<integer>0</integer>
<key>Points</key>
<array>
- <string>{238.74999618530282, 430.80675844512831}</string>
+ <string>{238.74999618530282, 439.80675844512831}</string>
<string>{207.66666666666765, 385.656005859375}</string>
</array>
<key>Style</key>
@@ -1144,7 +1371,7 @@
<integer>0</integer>
<key>Points</key>
<array>
- <string>{239.25039065750093, 285.57837549845181}</string>
+ <string>{239.25039065750093, 276.57837549845181}</string>
<string>{207.66666666666777, 353.07514659563753}</string>
</array>
<key>Style</key>
@@ -1742,7 +1969,7 @@
</dict>
<dict>
<key>Bounds</key>
- <string>{{375.5, 391.5}, {105.66666412353516, 22.544642175946908}}</string>
+ <string>{{375.5, 400.5}, {105.66666412353516, 22.544642175946908}}</string>
<key>Class</key>
<string>ShapedGraphic</string>
<key>ID</key>
@@ -9578,6 +9805,10 @@
<string>YES</string>
<key>HPages</key>
<integer>1</integer>
+ <key>HorizontalGuides</key>
+ <array>
+ <real>209.875</real>
+ </array>
<key>ImageCounter</key>
<integer>3</integer>
<key>KeepToScale</key>
@@ -9637,7 +9868,7 @@
<key>MasterSheets</key>
<array/>
<key>ModificationDate</key>
- <string>2016-03-13 08:04:48 +0000</string>
+ <string>2016-04-13 08:32:47 +0000</string>
<key>Modifier</key>
<string>Steve Piercy</string>
<key>NotesVisible</key>
@@ -9718,7 +9949,7 @@
</dict>
</array>
<key>Frame</key>
- <string>{{35, 93}, {1394, 1325}}</string>
+ <string>{{35, 93}, {2284, 1325}}</string>
<key>ListView</key>
<true/>
<key>OutlineWidth</key>
@@ -9732,15 +9963,15 @@
<key>SidebarWidth</key>
<integer>163</integer>
<key>VisibleRegion</key>
- <string>{{152.25, 226.5}, {255.75, 292.75}}</string>
+ <string>{{110.125, 77.875}, {239.125, 146.375}}</string>
<key>Zoom</key>
- <real>4</real>
+ <real>8</real>
<key>ZoomValues</key>
<array>
<array>
<string>Request Processing</string>
- <real>4</real>
<real>8</real>
+ <real>4</real>
</array>
</array>
</dict>
diff --git a/docs/_static/pyramid_request_processing.png b/docs/_static/pyramid_request_processing.png
index c684255fa..2f44f4824 100644
--- a/docs/_static/pyramid_request_processing.png
+++ b/docs/_static/pyramid_request_processing.png
Binary files differ
diff --git a/docs/_static/pyramid_request_processing.svg b/docs/_static/pyramid_request_processing.svg
index d32d5c5bc..03f6d56fa 100644
--- a/docs/_static/pyramid_request_processing.svg
+++ b/docs/_static/pyramid_request_processing.svg
@@ -1,3 +1,3 @@
<?xml version="1.0"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
-<svg xmlns="http://www.w3.org/2000/svg" xmlns:xl="http://www.w3.org/1999/xlink" version="1.1" viewBox="91 11 424 533" width="424pt" height="533pt"><metadata xmlns:dc="http://purl.org/dc/elements/1.1/"><dc:date>2016-03-13 08:04Z</dc:date><!-- Produced by OmniGraffle Professional 5.4.4 --></metadata><defs><marker orient="auto" overflow="visible" markerUnits="strokeWidth" id="SharpArrow_Marker" viewBox="-4 -4 10 8" markerWidth="10" markerHeight="8" color="#191919"><g><path d="M 5 0 L -3 -3 L 0 0 L 0 0 L -3 3 Z" fill="currentColor" stroke="currentColor" stroke-width="1"/></g></marker><font-face font-family="Helvetica" font-size="10" units-per-em="1000" underline-position="-75.683594" underline-thickness="49.316406" slope="0" x-height="522.94922" cap-height="717.28516" ascent="770.01953" descent="-229.98047" font-weight="500"><font-face-src><font-face-name name="Helvetica"/></font-face-src></font-face><font-face font-family="Helvetica" font-size="12" units-per-em="1000" underline-position="-75.683594" underline-thickness="49.316406" slope="0" x-height="532.22656" cap-height="719.72656" ascent="770.01953" descent="-229.98047" font-weight="bold"><font-face-src><font-face-name name="Helvetica-Bold"/></font-face-src></font-face><font-face font-family="Helvetica" font-size="10" units-per-em="1000" underline-position="-75.683594" underline-thickness="49.316406" slope="0" x-height="532.22656" cap-height="719.72656" ascent="770.01953" descent="-229.98047" font-weight="bold"><font-face-src><font-face-name name="Helvetica-Bold"/></font-face-src></font-face></defs><g stroke="none" stroke-opacity="1" stroke-dasharray="none" fill="none" fill-opacity="1"><title>Request Processing</title><rect fill="white" width="576" height="733"/><g><title>no exceptions</title><path d="M 155 444.75674 C 155 450.64061 155 486.2592 155 502.71617" marker-end="url(#SharpArrow_Marker)" stroke="#191919" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><path d="M 154.99999 322.33334 C 154.99999 327.72413 155 337.74646 155 346.1775" marker-end="url(#SharpArrow_Marker)" stroke="#191919" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><path d="M 154.99999 245.22768 C 154.99999 250.5417 154.99999 257.93189 154.99999 265.10145" marker-end="url(#SharpArrow_Marker)" stroke="#191919" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><path d="M 154.99995 198.62203 C 154.99995 203.74682 154.99998 209.1909 154.99999 215.28222" marker-end="url(#SharpArrow_Marker)" stroke="#191919" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><rect x="102.16667" y="45.183037" width="105.666664" height="22.544641" fill="#a4cfff"/><rect x="102.16667" y="45.183037" width="105.666664" height="22.544641" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(107.16667 50.455358)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="4.7596016" y="10" textLength="88.92578">middleware ingress </tspan></text><rect x="102.16667" y="96.183037" width="105.666664" height="22.544641" fill="#a4cfff"/><rect x="102.16667" y="96.183037" width="105.666664" height="22.544641" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(107.16667 101.45536)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="16.983723" y="10" textLength="61.69922">tween ingress</tspan></text><rect x="102.16667" y="222.18304" width="105.666664" height="22.544641" fill="#d2ffd0"/><rect x="102.16667" y="222.18304" width="105.666664" height="22.544641" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(107.16667 227.45536)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="28.660969" y="10" textLength="38.344727">traversal</tspan></text><rect x="238.83336" y="247.18304" width="105.666664" height="22.544641" fill="#dfbeff"/><rect x="238.83336" y="247.18304" width="105.666664" height="22.544641" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(243.83336 252.45536)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="16.424641" y="10" textLength="62.817383">ContextFound</tspan></text><rect x="102.16667" y="422.2121" width="105.666664" height="22.544641" fill="#a4cfff"/><rect x="102.16667" y="422.2121" width="105.666664" height="22.544641" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(107.16667 427.48442)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="18.094563" y="10" textLength="59.47754">tween egress</tspan></text><rect x="239" y="445.2359" width="105.666664" height="22.544641" fill="#fed153"/><rect x="239" y="445.2359" width="105.666664" height="22.544641" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(244 450.50821)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="5.3113594" y="10" textLength="85.043945">response callbacks</tspan></text><rect x="239" y="497.2359" width="105.666664" height="22.544641" fill="#fed153"/><rect x="239" y="497.2359" width="105.666664" height="22.544641" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(244 502.5082)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="8.6463203" y="10" textLength="5">fi</tspan><tspan font-family="Helvetica" font-size="10" font-weight="500" x="13.64632" y="10" textLength="73.374023">nished callbacks</tspan></text><rect x="102.16667" y="509.61795" width="105.666664" height="22.544641" fill="#a4cfff"/><rect x="102.16667" y="509.61795" width="105.666664" height="22.544641" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(107.16667 514.89027)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="5.8704414" y="10" textLength="83.92578">middleware egress</tspan></text><path d="M 155 67.72768 C 155 73.048893 155 81.55558 155 89.2853" marker-end="url(#SharpArrow_Marker)" stroke="#191919" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><path d="M 155 119.22768 C 155 124.62026 154.99997 133.48763 154.99996 141.38632" marker-end="url(#SharpArrow_Marker)" stroke="#191919" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><rect x="375.5" y="391.5" width="105.666664" height="22.544642" fill="#dfbeff"/><rect x="375.5" y="391.5" width="105.666664" height="22.544642" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(380.5 396.77232)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="16.702961" y="10" textLength="62.260742">BeforeRender</tspan></text><text transform="translate(233.5 20)" fill="black"><tspan font-family="Helvetica" font-size="12" font-weight="bold" x=".31445312" y="11" textLength="115.371094">Request Processing</tspan></text><path d="M 375.99995 42.910746 L 498.66662 42.910746 C 501.42805 42.910746 503.66662 45.149323 503.66662 47.910746 L 503.66662 222 C 503.66662 224.76142 501.42805 227 498.66662 227 L 375.99995 227 C 373.23853 227 370.99995 224.76142 370.99995 222 L 370.99995 47.910746 C 370.99995 45.149323 373.23853 42.910746 375.99995 42.910746 Z" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(375.99995 42.910746)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="bold" x="0" y="10" textLength="35.55664">Legend</tspan></text><rect x="383.66662" y="63.908513" width="105.666664" height="22.544641" fill="#dfbeff"/><rect x="383.66662" y="63.908513" width="105.666664" height="22.544641" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(388.66662 69.180834)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="35.601887" y="10" textLength="24.46289">event</tspan></text><rect x="383.66662" y="186.58226" width="105.666664" height="22.544641" fill="#fed153"/><rect x="383.66662" y="186.58226" width="105.666664" height="22.544641" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(388.66662 191.85458)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="29.769367" y="10" textLength="36.12793">callback</tspan></text><rect x="383.66662" y="158.54998" width="105.666664" height="22.544641" fill="#ffff6c"/><rect x="383.66662" y="158.54998" width="105.666664" height="22.544641" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(388.66662 163.8223)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="21.158527" y="10" textLength="53.34961">view deriver</tspan></text><rect x="383.66662" y="91.94079" width="105.666664" height="33.089283" fill="#a4cfff"/><rect x="383.66662" y="91.94079" width="105.666664" height="33.089283" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(388.66662 96.48543)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="11.148762" y="10" textLength="76.14746">external process </tspan><tspan font-family="Helvetica" font-size="10" font-weight="500" x="2.8162422" y="22" textLength="90.03418">(middleware, tween)</tspan></text><rect x="383.66662" y="130.51771" width="105.666664" height="22.544641" fill="#d2ffd0"/><rect x="383.66662" y="130.51771" width="105.666664" height="22.544641" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(388.66662 135.79003)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="12.537922" y="10" textLength="70.59082">internal process</tspan></text><line x1="154.99999" y1="258.44082" x2="238.83336" y2="258.45536" stroke="#191919" stroke-linecap="round" stroke-linejoin="round" stroke-width="1" stroke-dasharray="1,4"/><rect x="102.16667" y="353.07515" width="105.666664" height="33.089283" fill="#ffff6c"/><rect x="102.16667" y="353.07515" width="105.666664" height="33.089283" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(107.16667 363.61979)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="19.205402" y="10" textLength="57.25586">view pipeline</tspan></text><path d="M 155 386.66443 C 155 392.17252 155 405.5052 155 415.30935" marker-end="url(#SharpArrow_Marker)" stroke="#191919" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><line x1="239.25039" y1="285.57838" x2="207.66667" y2="353.07515" stroke="#c1c1c1" stroke-linecap="round" stroke-linejoin="round" stroke-width="1" stroke-dasharray="1,3"/><line x1="238.75" y1="430.80676" x2="207.66667" y2="385.656" stroke="#c1c1c1" stroke-linecap="round" stroke-linejoin="round" stroke-width="1" stroke-dasharray="1,3"/><rect x="102.16666" y="305.0893" width="105.666664" height="17.244049" fill="#d2ffd0"/><rect x="102.16666" y="305.0893" width="105.666664" height="17.244049" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(107.16666 307.71132)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="24.764484" y="10" textLength="46.137695">predicates</tspan></text><rect x="102.16666" y="272" width="105.666664" height="33.089294" fill="#d2ffd0"/><rect x="102.16666" y="272" width="105.666664" height="33.089294" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(107.16666 282.54465)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="21.707844" y="10" textLength="52.250977">view lookup</tspan></text><rect x="102.166606" y="181.37798" width="105.666695" height="17.244049" fill="#d2ffd0"/><rect x="102.166606" y="181.37798" width="105.666695" height="17.244049" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(107.166606 184)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="11.978855" y="10" textLength="71.708984">route predicates</tspan></text><rect x="102.166606" y="148.28869" width="105.666695" height="33.089294" fill="#d2ffd0"/><rect x="102.166606" y="148.28869" width="105.666695" height="33.089294" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(107.166606 158.83333)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="18.001804" y="10" textLength="20.004883">URL</tspan><tspan font-family="Helvetica" font-size="10" font-weight="500" x="37.640476" y="10" textLength="40.024414"> dispatch</tspan></text><rect x="239.8334" y="117.3192" width="105.666664" height="22.544641" fill="#dfbeff"/><rect x="239.8334" y="117.3192" width="105.666664" height="22.544641" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(244.8334 122.59152)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="19.207844" y="10" textLength="57.250977">NewRequest</tspan></text><line x1="154.99999" y1="128.68025" x2="239.8334" y2="128.59152" stroke="#191919" stroke-linecap="round" stroke-linejoin="round" stroke-width="1" stroke-dasharray="1,4"/><rect x="238.83336" y="471.2262" width="105.666664" height="22.544641" fill="#dfbeff"/><rect x="238.83336" y="471.2262" width="105.666664" height="22.544641" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(243.83336 476.49852)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="15.316242" y="10" textLength="65.03418">NewResponse</tspan></text><line x1="155" y1="470.25295" x2="238.33861" y2="482.42625" stroke="#191919" stroke-linecap="round" stroke-linejoin="round" stroke-width="1" stroke-dasharray="1,4"/><rect x="238.75" y="322.26348" width="105.66669" height="18.656048" fill="#ffffa3"/><rect x="238.75" y="322.26348" width="105.66669" height="18.656048" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(243.75 325.5915)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="1.9812927" y="10" textLength="91.7041">view mapper ingress</tspan></text><rect x="238.75" y="341.36561" width="105.66669" height="33.089283" fill="#ffff6c"/><rect x="238.75" y="341.36561" width="105.66669" height="33.089283" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(243.75 351.91025)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="37.830902" y="10" textLength="20.004883">view</tspan></text><rect x="238.75" y="374.901" width="105.66669" height="18.656048" fill="#ffffa3"/><rect x="238.75" y="374.901" width="105.66669" height="18.656048" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(243.75 378.22901)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="3.0921326" y="10" textLength="89.48242">view mapper egress</tspan></text><rect x="238.75" y="393.55704" width="105.66669" height="18.656048" fill="#ffffa3"/><rect x="238.75" y="393.55704" width="105.66669" height="18.656048" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(243.75 396.88507)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="8.917328" y="10" textLength="77.83203">response adapter</tspan></text><rect x="238.75" y="303.65604" width="105.66669" height="18.656048" fill="#ffffa3"/><rect x="238.75" y="303.65604" width="105.66669" height="18.656048" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(243.75 306.98407)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="6.7029724" y="10" textLength="82.26074">decorators ingress</tspan></text><rect x="238.75" y="412.1507" width="105.66669" height="18.656048" fill="#ffffa3"/><rect x="238.75" y="412.1507" width="105.66669" height="18.656048" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(243.75 415.47873)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="7.8138123" y="10" textLength="80.039062">decorators egress</tspan></text><rect x="238.75" y="285" width="105.75003" height="18.656048" fill="#ffffa3"/><rect x="238.75" y="285" width="105.75003" height="18.656048" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(243.75 288.32802)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="19.244644" y="10" textLength="57.260742">authorization</tspan></text><line x1="155" y1="482.12575" x2="238.52297" y2="508.3584" stroke="#191919" stroke-linecap="round" stroke-linejoin="round" stroke-width="1" stroke-dasharray="1,4"/><line x1="155" y1="459.27668" x2="238.50027" y2="456.52468" stroke="#191919" stroke-linecap="round" stroke-linejoin="round" stroke-width="1" stroke-dasharray="1,4"/><line x1="344.41668" y1="402.88507" x2="375.5" y2="402.77232" stroke="#191919" stroke-linecap="round" stroke-linejoin="round" stroke-width="1" stroke-dasharray="1,4"/></g></g></svg>
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xl="http://www.w3.org/1999/xlink" version="1.1" viewBox="91 11 424 533" width="424pt" height="533pt"><metadata xmlns:dc="http://purl.org/dc/elements/1.1/"><dc:date>2016-04-13 08:32Z</dc:date><!-- Produced by OmniGraffle Professional 5.4.4 --></metadata><defs><marker orient="auto" overflow="visible" markerUnits="strokeWidth" id="SharpArrow_Marker" viewBox="-4 -4 10 8" markerWidth="10" markerHeight="8" color="#191919"><g><path d="M 5 0 L -3 -3 L 0 0 L 0 0 L -3 3 Z" fill="currentColor" stroke="currentColor" stroke-width="1"/></g></marker><font-face font-family="Helvetica" font-size="10" units-per-em="1000" underline-position="-75.683594" underline-thickness="49.316406" slope="0" x-height="522.94922" cap-height="717.28516" ascent="770.01953" descent="-229.98047" font-weight="500"><font-face-src><font-face-name name="Helvetica"/></font-face-src></font-face><font-face font-family="Helvetica" font-size="12" units-per-em="1000" underline-position="-75.683594" underline-thickness="49.316406" slope="0" x-height="532.22656" cap-height="719.72656" ascent="770.01953" descent="-229.98047" font-weight="bold"><font-face-src><font-face-name name="Helvetica-Bold"/></font-face-src></font-face><font-face font-family="Helvetica" font-size="10" units-per-em="1000" underline-position="-75.683594" underline-thickness="49.316406" slope="0" x-height="532.22656" cap-height="719.72656" ascent="770.01953" descent="-229.98047" font-weight="bold"><font-face-src><font-face-name name="Helvetica-Bold"/></font-face-src></font-face></defs><g stroke="none" stroke-opacity="1" stroke-dasharray="none" fill="none" fill-opacity="1"><title>Request Processing</title><rect fill="white" width="576" height="733"/><g><title>no exceptions</title><path d="M 155 444.75674 C 155 450.64061 155 486.2592 155 502.71617" marker-end="url(#SharpArrow_Marker)" stroke="#191919" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><path d="M 154.99999 322.33334 C 154.99999 327.72413 155 337.74646 155 346.1775" marker-end="url(#SharpArrow_Marker)" stroke="#191919" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><path d="M 154.99999 245.22768 C 154.99999 250.5417 154.99999 257.93189 154.99999 265.10145" marker-end="url(#SharpArrow_Marker)" stroke="#191919" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><path d="M 154.99995 198.62203 C 154.99995 203.74682 154.99998 209.1909 154.99999 215.28222" marker-end="url(#SharpArrow_Marker)" stroke="#191919" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><rect x="102.16667" y="45.183037" width="105.666664" height="22.544641" fill="#a4cfff"/><rect x="102.16667" y="45.183037" width="105.666664" height="22.544641" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(107.16667 50.455358)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="4.7596016" y="10" textLength="88.92578">middleware ingress </tspan></text><rect x="102.16667" y="96.183037" width="105.666664" height="22.544641" fill="#a4cfff"/><rect x="102.16667" y="96.183037" width="105.666664" height="22.544641" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(107.16667 101.45536)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="16.983723" y="10" textLength="61.69922">tween ingress</tspan></text><rect x="102.16667" y="222.18304" width="105.666664" height="22.544641" fill="#d2ffd0"/><rect x="102.16667" y="222.18304" width="105.666664" height="22.544641" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(107.16667 227.45536)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="28.660969" y="10" textLength="38.344727">traversal</tspan></text><rect x="238.83336" y="247.18304" width="105.666664" height="22.544641" fill="#dfbeff"/><rect x="238.83336" y="247.18304" width="105.666664" height="22.544641" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(243.83336 252.45536)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="16.424641" y="10" textLength="62.817383">ContextFound</tspan></text><rect x="102.16667" y="422.2121" width="105.666664" height="22.544641" fill="#a4cfff"/><rect x="102.16667" y="422.2121" width="105.666664" height="22.544641" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(107.16667 427.48442)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="18.094563" y="10" textLength="59.47754">tween egress</tspan></text><rect x="239" y="445.2359" width="105.666664" height="22.544641" fill="#fed153"/><rect x="239" y="445.2359" width="105.666664" height="22.544641" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(244 450.50821)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="5.3113594" y="10" textLength="85.043945">response callbacks</tspan></text><rect x="239" y="497.2359" width="105.666664" height="22.544641" fill="#fed153"/><rect x="239" y="497.2359" width="105.666664" height="22.544641" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(244 502.5082)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="8.6463203" y="10" textLength="5">fi</tspan><tspan font-family="Helvetica" font-size="10" font-weight="500" x="13.64632" y="10" textLength="73.374023">nished callbacks</tspan></text><rect x="102.16667" y="509.61795" width="105.666664" height="22.544641" fill="#a4cfff"/><rect x="102.16667" y="509.61795" width="105.666664" height="22.544641" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(107.16667 514.89027)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="5.8704414" y="10" textLength="83.92578">middleware egress</tspan></text><path d="M 155 67.72768 C 155 73.048893 155 81.55558 155 89.2853" marker-end="url(#SharpArrow_Marker)" stroke="#191919" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><path d="M 155 119.22768 C 155 124.62026 154.99997 133.48763 154.99996 141.38632" marker-end="url(#SharpArrow_Marker)" stroke="#191919" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><rect x="375.5" y="400.5" width="105.666664" height="22.544642" fill="#dfbeff"/><rect x="375.5" y="400.5" width="105.666664" height="22.544642" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(380.5 405.77232)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="16.702961" y="10" textLength="62.260742">BeforeRender</tspan></text><text transform="translate(233.5 20)" fill="black"><tspan font-family="Helvetica" font-size="12" font-weight="bold" x=".31445312" y="11" textLength="115.371094">Request Processing</tspan></text><path d="M 375.99995 42.910746 L 498.66662 42.910746 C 501.42805 42.910746 503.66662 45.149323 503.66662 47.910746 L 503.66662 222 C 503.66662 224.76142 501.42805 227 498.66662 227 L 375.99995 227 C 373.23853 227 370.99995 224.76142 370.99995 222 L 370.99995 47.910746 C 370.99995 45.149323 373.23853 42.910746 375.99995 42.910746 Z" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(375.99995 42.910746)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="bold" x="0" y="10" textLength="35.55664">Legend</tspan></text><rect x="383.66662" y="63.908513" width="105.666664" height="22.544641" fill="#dfbeff"/><rect x="383.66662" y="63.908513" width="105.666664" height="22.544641" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(388.66662 69.180834)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="35.601887" y="10" textLength="24.46289">event</tspan></text><rect x="383.66662" y="186.58226" width="105.666664" height="22.544641" fill="#fed153"/><rect x="383.66662" y="186.58226" width="105.666664" height="22.544641" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(388.66662 191.85458)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="29.769367" y="10" textLength="36.12793">callback</tspan></text><rect x="383.66662" y="158.54998" width="105.666664" height="22.544641" fill="#ffff6c"/><rect x="383.66662" y="158.54998" width="105.666664" height="22.544641" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(388.66662 163.8223)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="21.158527" y="10" textLength="53.34961">view deriver</tspan></text><rect x="383.66662" y="91.94079" width="105.666664" height="33.089283" fill="#a4cfff"/><rect x="383.66662" y="91.94079" width="105.666664" height="33.089283" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(388.66662 96.48543)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="11.148762" y="10" textLength="76.14746">external process </tspan><tspan font-family="Helvetica" font-size="10" font-weight="500" x="2.8162422" y="22" textLength="90.03418">(middleware, tween)</tspan></text><rect x="383.66662" y="130.51771" width="105.666664" height="22.544641" fill="#d2ffd0"/><rect x="383.66662" y="130.51771" width="105.666664" height="22.544641" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(388.66662 135.79003)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="12.537922" y="10" textLength="70.59082">internal process</tspan></text><line x1="154.99999" y1="258.44082" x2="238.83336" y2="258.45536" stroke="#191919" stroke-linecap="round" stroke-linejoin="round" stroke-width="1" stroke-dasharray="1,4"/><rect x="102.16667" y="353.07515" width="105.666664" height="33.089283" fill="#ffff6c"/><rect x="102.16667" y="353.07515" width="105.666664" height="33.089283" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(107.16667 363.61979)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="19.205402" y="10" textLength="57.25586">view pipeline</tspan></text><path d="M 155 386.66443 C 155 392.17252 155 405.5052 155 415.30935" marker-end="url(#SharpArrow_Marker)" stroke="#191919" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><line x1="239.25039" y1="276.57838" x2="207.66667" y2="353.07515" stroke="#c1c1c1" stroke-linecap="round" stroke-linejoin="round" stroke-width="1" stroke-dasharray="1,3"/><line x1="238.75" y1="439.80676" x2="207.66667" y2="385.656" stroke="#c1c1c1" stroke-linecap="round" stroke-linejoin="round" stroke-width="1" stroke-dasharray="1,3"/><rect x="102.16666" y="305.0893" width="105.666664" height="17.244049" fill="#d2ffd0"/><rect x="102.16666" y="305.0893" width="105.666664" height="17.244049" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(107.16666 307.71132)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="24.764484" y="10" textLength="46.137695">predicates</tspan></text><rect x="102.16666" y="272" width="105.666664" height="33.089294" fill="#d2ffd0"/><rect x="102.16666" y="272" width="105.666664" height="33.089294" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(107.16666 282.54465)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="21.707844" y="10" textLength="52.250977">view lookup</tspan></text><rect x="102.166606" y="181.37798" width="105.666695" height="17.244049" fill="#d2ffd0"/><rect x="102.166606" y="181.37798" width="105.666695" height="17.244049" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(107.166606 184)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="11.978855" y="10" textLength="71.708984">route predicates</tspan></text><rect x="102.166606" y="148.28869" width="105.666695" height="33.089294" fill="#d2ffd0"/><rect x="102.166606" y="148.28869" width="105.666695" height="33.089294" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(107.166606 158.83333)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="18.001804" y="10" textLength="20.004883">URL</tspan><tspan font-family="Helvetica" font-size="10" font-weight="500" x="37.640476" y="10" textLength="40.024414"> dispatch</tspan></text><rect x="239.8334" y="117.3192" width="105.666664" height="22.544641" fill="#dfbeff"/><rect x="239.8334" y="117.3192" width="105.666664" height="22.544641" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(244.8334 122.59152)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="19.207844" y="10" textLength="57.250977">NewRequest</tspan></text><line x1="154.99999" y1="128.68025" x2="239.8334" y2="128.59152" stroke="#191919" stroke-linecap="round" stroke-linejoin="round" stroke-width="1" stroke-dasharray="1,4"/><rect x="238.83336" y="471.2262" width="105.666664" height="22.544641" fill="#dfbeff"/><rect x="238.83336" y="471.2262" width="105.666664" height="22.544641" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(243.83336 476.49852)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="15.316242" y="10" textLength="65.03418">NewResponse</tspan></text><line x1="155" y1="470.25295" x2="238.33861" y2="482.42625" stroke="#191919" stroke-linecap="round" stroke-linejoin="round" stroke-width="1" stroke-dasharray="1,4"/><rect x="238.75" y="331.26348" width="105.66669" height="18.656048" fill="#ffffa3"/><rect x="238.75" y="331.26348" width="105.66669" height="18.656048" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(243.75 334.5915)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="1.9812927" y="10" textLength="91.7041">view mapper ingress</tspan></text><rect x="238.75" y="350.36561" width="105.66669" height="33.089283" fill="#ffff6c"/><rect x="238.75" y="350.36561" width="105.66669" height="33.089283" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(243.75 360.91025)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="37.830902" y="10" textLength="20.004883">view</tspan></text><rect x="238.75" y="383.901" width="105.66669" height="18.656048" fill="#ffffa3"/><rect x="238.75" y="383.901" width="105.66669" height="18.656048" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(243.75 387.22901)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="3.0921326" y="10" textLength="89.48242">view mapper egress</tspan></text><rect x="238.75" y="402.55704" width="105.66669" height="18.656048" fill="#ffffa3"/><rect x="238.75" y="402.55704" width="105.66669" height="18.656048" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(243.75 405.88507)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="8.917328" y="10" textLength="77.83203">response adapter</tspan></text><rect x="238.75" y="312.65604" width="105.66669" height="18.656048" fill="#ffffa3"/><rect x="238.75" y="312.65604" width="105.66669" height="18.656048" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(243.75 315.98407)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="6.7029724" y="10" textLength="82.26074">decorators ingress</tspan></text><rect x="238.75" y="421.1507" width="105.66669" height="18.656048" fill="#ffffa3"/><rect x="238.75" y="421.1507" width="105.66669" height="18.656048" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(243.75 424.47873)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="7.8138123" y="10" textLength="80.039062">decorators egress</tspan></text><rect x="238.75" y="276" width="105.75003" height="18.656048" fill="#ffffa3"/><rect x="238.75" y="276" width="105.75003" height="18.656048" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(243.75 279.32802)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="19.244644" y="10" textLength="57.260742">authorization</tspan></text><line x1="155" y1="482.12575" x2="238.52297" y2="508.3584" stroke="#191919" stroke-linecap="round" stroke-linejoin="round" stroke-width="1" stroke-dasharray="1,4"/><line x1="155" y1="459.27668" x2="238.50027" y2="456.52468" stroke="#191919" stroke-linecap="round" stroke-linejoin="round" stroke-width="1" stroke-dasharray="1,4"/><line x1="344.41668" y1="411.88507" x2="375.5" y2="411.77232" stroke="#191919" stroke-linecap="round" stroke-linejoin="round" stroke-width="1" stroke-dasharray="1,4"/><rect x="239.83336" y="197.875" width="105.666664" height="22.544641" fill="#dfbeff"/><rect x="239.83336" y="197.875" width="105.666664" height="22.544641" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(244.83336 203.14732)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="12.44759" y="10" textLength="35.57129">BeforeT</tspan><tspan font-family="Helvetica" font-size="10" font-weight="500" x="47.652668" y="10" textLength="35.566406">raversal</tspan></text><line x1="154.99998" y1="209.11366" x2="239.83336" y2="209.14732" stroke="#191919" stroke-linecap="round" stroke-linejoin="round" stroke-width="1" stroke-dasharray="1,4"/><rect x="238.75" y="294.65604" width="105.66669" height="18.656048" fill="#ffffa3"/><rect x="238.75" y="294.65604" width="105.66669" height="18.656048" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(243.75 297.98407)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="17.27182" y="10" textLength="61.123047">CSRF checks</tspan></text></g></g></svg>
diff --git a/docs/api/events.rst b/docs/api/events.rst
index 31a0e22c1..0a8463740 100644
--- a/docs/api/events.rst
+++ b/docs/api/events.rst
@@ -21,6 +21,8 @@ Event Types
.. autoclass:: ContextFound
+.. autoclass:: BeforeTraversal
+
.. autoclass:: NewResponse
.. autoclass:: BeforeRender
diff --git a/docs/api/interfaces.rst b/docs/api/interfaces.rst
index 635d3c5b6..272820a91 100644
--- a/docs/api/interfaces.rst
+++ b/docs/api/interfaces.rst
@@ -17,6 +17,9 @@ Event-Related Interfaces
.. autointerface:: IContextFound
:members:
+ .. autointerface:: IBeforeTraversal
+ :members:
+
.. autointerface:: INewResponse
:members:
diff --git a/docs/glossary.rst b/docs/glossary.rst
index 655301a5c..486e94848 100644
--- a/docs/glossary.rst
+++ b/docs/glossary.rst
@@ -1099,6 +1099,14 @@ Glossary
Examples of built-in derivers including view mapper, the permission
checker, and applying a renderer to a dictionary returned from the view.
+ truthy string
+ A string represeting a value of ``True``. Acceptable values are
+ ``t``, ``true``, ``y``, ``yes``, ``on`` and ``1``.
+
+ falsey string
+ A string represeting a value of ``False``. Acceptable values are
+ ``f``, ``false``, ``n``, ``no``, ``off`` and ``0``.
+
pip
The `Python Packaging Authority's <https://www.pypa.io/>`_ recommended
tool for installing Python packages.
diff --git a/docs/narr/hooks.rst b/docs/narr/hooks.rst
index 2c3782387..28d1e09d5 100644
--- a/docs/narr/hooks.rst
+++ b/docs/narr/hooks.rst
@@ -1590,6 +1590,12 @@ the user-defined :term:`view callable`:
This element will also output useful debugging information when
``pyramid.debug_authorization`` is enabled.
+``csrf_view``
+
+ Used to check the CSRF token provided in the request. This element is a
+ no-op if both the ``require_csrf`` view option and the
+ ``pyramid.require_default_csrf`` setting are disabled.
+
``owrapped_view``
Invokes the wrapped view defined by the ``wrapper`` option.
@@ -1656,42 +1662,6 @@ View derivers are unique in that they have access to most of the options
passed to :meth:`pyramid.config.Configurator.add_view` in order to decide what
to do, and they have a chance to affect every view in the application.
-Let's look at one more example which will protect views by requiring a CSRF
-token unless ``disable_csrf=True`` is passed to the view:
-
-.. code-block:: python
- :linenos:
-
- from pyramid.response import Response
- from pyramid.session import check_csrf_token
-
- def require_csrf_view(view, info):
- wrapper_view = view
- if not info.options.get('disable_csrf', False):
- def wrapper_view(context, request):
- if request.method == 'POST':
- check_csrf_token(request)
- return view(context, request)
- return wrapper_view
-
- require_csrf_view.options = ('disable_csrf',)
-
- config.add_view_deriver(require_csrf_view)
-
- def protected_view(request):
- return Response('protected')
-
- def unprotected_view(request):
- return Response('unprotected')
-
- config.add_view(protected_view, name='safe')
- config.add_view(unprotected_view, name='unsafe', disable_csrf=True)
-
-Navigating to ``/safe`` with a POST request will then fail when the call to
-:func:`pyramid.session.check_csrf_token` raises a
-:class:`pyramid.exceptions.BadCSRFToken` exception. However, ``/unsafe`` will
-not error.
-
Ordering View Derivers
~~~~~~~~~~~~~~~~~~~~~~
diff --git a/docs/narr/router.rst b/docs/narr/router.rst
index e02142e6e..e45e6f4a8 100644
--- a/docs/narr/router.rst
+++ b/docs/narr/router.rst
@@ -41,19 +41,24 @@ request enters a :app:`Pyramid` application through to the point that
user-defined :term:`route` matches the current WSGI environment. The
:term:`router` passes the request as an argument to the mapper.
-#. If any route matches, the route mapper adds attributes to the request:
- ``matchdict`` and ``matched_route`` attributes are added to the request
- object. The former contains a dictionary representing the matched dynamic
- elements of the request's ``PATH_INFO`` value, and the latter contains the
+#. If any route matches, the route mapper adds the attributes ``matchdict``
+ and ``matched_route`` to the request object. The former contains a
+ dictionary representing the matched dynamic elements of the request's
+ ``PATH_INFO`` value, and the latter contains the
:class:`~pyramid.interfaces.IRoute` object representing the route which
- matched. The root object associated with the route found is also generated:
- if the :term:`route configuration` which matched has an associated
- ``factory`` argument, this factory is used to generate the root object,
- otherwise a default :term:`root factory` is used.
+ matched.
-#. If a route match was *not* found, and a ``root_factory`` argument was passed
+#. A :class:`~pyramid.events.BeforeTraversal` :term:`event` is sent to any
+ subscribers.
+
+#. Continuing, if any route matches, the root object associated with the found
+ route is generated. If the :term:`route configuration` which matched has an
+ associated ``factory`` argument, then this factory is used to generate the
+ root object; otherwise a default :term:`root factory` is used.
+
+ However, if no route matches, and if a ``root_factory`` argument was passed
to the :term:`Configurator` constructor, that callable is used to generate
- the root object. If the ``root_factory`` argument passed to the
+ the root object. If the ``root_factory`` argument passed to the
Configurator constructor was ``None``, a default root factory is used to
generate a root object.
diff --git a/docs/narr/sessions.rst b/docs/narr/sessions.rst
index db554a93b..d66e86258 100644
--- a/docs/narr/sessions.rst
+++ b/docs/narr/sessions.rst
@@ -367,6 +367,21 @@ Or include it as a header in a jQuery AJAX request:
The handler for the URL that receives the request should then require that the
correct CSRF token is supplied.
+.. index::
+ single: session.new_csrf_token
+
+Using the ``session.new_csrf_token`` Method
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+To explicitly create a new CSRF token, use the ``session.new_csrf_token()``
+method. This differs only from ``session.get_csrf_token()`` inasmuch as it
+clears any existing CSRF token, creates a new CSRF token, sets the token into
+the session, and returns the token.
+
+.. code-block:: python
+
+ token = request.session.new_csrf_token()
+
Checking CSRF Tokens Manually
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -389,12 +404,51 @@ header named ``X-CSRF-Token``.
# ...
-.. index::
- single: session.new_csrf_token
+.. _auto_csrf_checking:
+
+Checking CSRF Tokens Automatically
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. versionadded:: 1.7
+
+:app:`Pyramid` supports automatically checking CSRF tokens on POST requests.
+Any other request may be checked manually. This feature can be turned on
+globally for an application using the ``pyramid.require_default_csrf`` setting.
+
+If the ``pyramid.required_default_csrf`` setting is a :term:`truthy string` or
+``True`` then the default CSRF token parameter will be ``csrf_token``. If a
+different token is desired, it may be passed as the value. Finally, a
+:term:`falsey string` or ``False`` will turn off automatic CSRF checking
+globally on every POST request.
+
+No matter what, CSRF checking may be explicitly enabled or disabled on a
+per-view basis using the ``require_csrf`` view option. This option is of the
+same format as the ``pyramid.require_default_csrf`` setting, accepting strings
+or boolean values.
+
+If ``require_csrf`` is ``True`` but does not explicitly define a token to
+check, then the token name is pulled from whatever was set in the
+``pyramid.require_default_csrf`` setting. Finally, if that setting does not
+explicitly define a token, then ``csrf_token`` is the token required. This token
+name will be required in ``request.params`` which is a combination of the
+query string and a submitted form body.
+
+It is always possible to pass the token in the ``X-CSRF-Token`` header as well.
+There is currently no way to define an alternate name for this header without
+performing CSRF checking manually.
+
+If CSRF checks fail then a :class:`pyramid.exceptions.BadCSRFToken` exception
+will be raised. This exception may be caught and handled by an
+:term:`exception view` but, by default, will result in a ``400 Bad Request``
+response being sent to the client.
Checking CSRF Tokens with a View Predicate
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+.. deprecated:: 1.7
+ Use the ``require_csrf`` option or read :ref:`auto_csrf_checking` instead
+ to have :class:`pyramid.exceptions.BadCSRFToken` exceptions raised.
+
A convenient way to require a valid CSRF token for a particular view is to
include ``check_csrf=True`` as a view predicate. See
:meth:`pyramid.config.Configurator.add_view`.
@@ -410,15 +464,3 @@ include ``check_csrf=True`` as a view predicate. See
predicate system, when it doesn't find a view, raises ``HTTPNotFound``
instead of ``HTTPBadRequest``, so ``check_csrf=True`` behavior is different
from calling :func:`pyramid.session.check_csrf_token`.
-
-Using the ``session.new_csrf_token`` Method
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-To explicitly create a new CSRF token, use the ``session.new_csrf_token()``
-method. This differs only from ``session.get_csrf_token()`` inasmuch as it
-clears any existing CSRF token, creates a new CSRF token, sets the token into
-the session, and returns the token.
-
-.. code-block:: python
-
- token = request.session.new_csrf_token()
diff --git a/docs/narr/viewconfig.rst b/docs/narr/viewconfig.rst
index 0bd52b6e2..e645185f5 100644
--- a/docs/narr/viewconfig.rst
+++ b/docs/narr/viewconfig.rst
@@ -192,6 +192,32 @@ Non-Predicate Arguments
only influence ``Cache-Control`` headers, pass a tuple as ``http_cache`` with
the first element of ``None``, i.e., ``(None, {'public':True})``.
+
+``require_csrf``
+
+ CSRF checks only affect POST requests. Any other request methods will pass
+ untouched. This option is used in combination with the
+ ``pyramid.require_default_csrf`` setting to control which request parameters
+ are checked for CSRF tokens.
+
+ This feature requires a configured :term:`session factory`.
+
+ If this option is set to ``True`` then CSRF checks will be enabled for POST
+ requests to this view. The required token will be whatever was specified by
+ the ``pyramid.require_default_csrf`` setting, or will fallback to
+ ``csrf_token``.
+
+ If this option is set to a string then CSRF checks will be enabled and it
+ will be used as the required token regardless of the
+ ``pyramid.require_default_csrf`` setting.
+
+ If this option is set to ``False`` then CSRF checks will be disabled
+ regardless of the ``pyramid.require_default_csrf`` setting.
+
+ See :ref:`auto_csrf_checking` for more information.
+
+ .. versionadded:: 1.7
+
``wrapper``
The :term:`view name` of a different :term:`view configuration` which will
receive the response body of this view as the ``request.wrapped_body``
diff --git a/pyramid/config/settings.py b/pyramid/config/settings.py
index 492b7d524..b66986327 100644
--- a/pyramid/config/settings.py
+++ b/pyramid/config/settings.py
@@ -122,6 +122,8 @@ class Settings(dict):
config_prevent_cachebust)
eff_prevent_cachebust = asbool(eget('PYRAMID_PREVENT_CACHEBUST',
config_prevent_cachebust))
+ require_default_csrf = self.get('pyramid.require_default_csrf')
+ eff_require_default_csrf = require_default_csrf
update = {
'debug_authorization': eff_debug_all or eff_debug_auth,
@@ -134,6 +136,7 @@ class Settings(dict):
'default_locale_name':eff_locale_name,
'prevent_http_cache':eff_prevent_http_cache,
'prevent_cachebust':eff_prevent_cachebust,
+ 'require_default_csrf':eff_require_default_csrf,
'pyramid.debug_authorization': eff_debug_all or eff_debug_auth,
'pyramid.debug_notfound': eff_debug_all or eff_debug_notfound,
@@ -145,7 +148,8 @@ class Settings(dict):
'pyramid.default_locale_name':eff_locale_name,
'pyramid.prevent_http_cache':eff_prevent_http_cache,
'pyramid.prevent_cachebust':eff_prevent_cachebust,
- }
+ 'pyramid.require_default_csrf':eff_require_default_csrf,
+ }
self.update(update)
diff --git a/pyramid/config/views.py b/pyramid/config/views.py
index 3f6a9080d..6fe31fd4a 100644
--- a/pyramid/config/views.py
+++ b/pyramid/config/views.py
@@ -213,6 +213,7 @@ class ViewsConfiguratorMixin(object):
http_cache=None,
match_param=None,
check_csrf=None,
+ require_csrf=None,
**view_options):
""" Add a :term:`view configuration` to the current
configuration state. Arguments to ``add_view`` are broken
@@ -366,6 +367,31 @@ class ViewsConfiguratorMixin(object):
before returning the response from the view. This effectively
disables any HTTP caching done by ``http_cache`` for that response.
+ require_csrf
+
+ .. versionadded:: 1.7
+
+ CSRF checks only affect POST requests. Any other request methods
+ will pass untouched. This option is used in combination with the
+ ``pyramid.require_default_csrf`` setting to control which
+ request parameters are checked for CSRF tokens.
+
+ This feature requires a configured :term:`session factory`.
+
+ If this option is set to ``True`` then CSRF checks will be enabled
+ for POST requests to this view. The required token will be whatever
+ was specified by the ``pyramid.require_default_csrf`` setting, or
+ will fallback to ``csrf_token``.
+
+ If this option is set to a string then CSRF checks will be enabled
+ and it will be used as the required token regardless of the
+ ``pyramid.require_default_csrf`` setting.
+
+ If this option is set to ``False`` then CSRF checks will be disabled
+ regardless of the ``pyramid.require_default_csrf`` setting.
+
+ See :ref:`auto_csrf_checking` for more information.
+
wrapper
The :term:`view name` of a different :term:`view
@@ -587,6 +613,11 @@ class ViewsConfiguratorMixin(object):
check_csrf
+ .. deprecated:: 1.7
+ Use the ``require_csrf`` option or see :ref:`auto_csrf_checking`
+ instead to have :class:`pyramid.exceptions.BadCSRFToken`
+ exceptions raised.
+
If specified, this value should be one of ``None``, ``True``,
``False``, or a string representing the 'check name'. If the value
is ``True`` or a string, CSRF checking will be performed. If the
@@ -682,7 +713,18 @@ class ViewsConfiguratorMixin(object):
'Predicate" in the "Hooks" chapter of the documentation '
'for more information.'),
DeprecationWarning,
- stacklevel=4
+ stacklevel=4,
+ )
+
+ if check_csrf is not None:
+ warnings.warn(
+ ('The "check_csrf" argument to Configurator.add_view is '
+ 'deprecated as of Pyramid 1.7. Use the "require_csrf" option '
+ 'instead or see "Checking CSRF Tokens Automatically" in the '
+ '"Sessions" chapter of the documentation for more '
+ 'information.'),
+ DeprecationWarning,
+ stacklevel=4,
)
view = self.maybe_dotted(view)
@@ -805,6 +847,8 @@ class ViewsConfiguratorMixin(object):
path_info=path_info,
match_param=match_param,
check_csrf=check_csrf,
+ http_cache=http_cache,
+ require_csrf=require_csrf,
callable=view,
mapper=mapper,
decorator=decorator,
@@ -860,6 +904,7 @@ class ViewsConfiguratorMixin(object):
decorator=decorator,
mapper=mapper,
http_cache=http_cache,
+ require_csrf=require_csrf,
extra_options=ovals,
)
derived_view.__discriminator__ = lambda *arg: discriminator
@@ -1184,6 +1229,7 @@ class ViewsConfiguratorMixin(object):
d = pyramid.viewderivers
derivers = [
('secured_view', d.secured_view),
+ ('csrf_view', d.csrf_view),
('owrapped_view', d.owrapped_view),
('http_cached_view', d.http_cached_view),
('decorated_view', d.decorated_view),
@@ -1284,7 +1330,7 @@ class ViewsConfiguratorMixin(object):
viewname=None, accept=None, order=MAX_ORDER,
phash=DEFAULT_PHASH, decorator=None,
mapper=None, http_cache=None, context=None,
- extra_options=None):
+ require_csrf=None, extra_options=None):
view = self.maybe_dotted(view)
mapper = self.maybe_dotted(mapper)
if isinstance(renderer, string_types):
@@ -1311,6 +1357,7 @@ class ViewsConfiguratorMixin(object):
mapper=mapper,
decorator=decorator,
http_cache=http_cache,
+ require_csrf=require_csrf,
)
if extra_options:
options.update(extra_options)
diff --git a/pyramid/events.py b/pyramid/events.py
index 97375638e..35da2fa6f 100644
--- a/pyramid/events.py
+++ b/pyramid/events.py
@@ -11,6 +11,7 @@ from pyramid.interfaces import (
INewResponse,
IApplicationCreated,
IBeforeRender,
+ IBeforeTraversal,
)
class subscriber(object):
@@ -129,6 +130,26 @@ class NewResponse(object):
self.request = request
self.response = response
+@implementer(IBeforeTraversal)
+class BeforeTraversal(object):
+ """
+ An instance of this class is emitted as an :term:`event` after the
+ :app:`Pyramid` :term:`router` has attempted to find a :term:`route` object
+ but before any traversal or view code is executed. The instance has an
+ attribute, ``request``, which is the request object generated by
+ :app:`Pyramid`.
+
+ Notably, the request object **may** have an attribute named
+ ``matched_route``, which is the matched route if found. If no route
+ matched, this attribute is not available.
+
+ This class implements the :class:`pyramid.interfaces.IBeforeTraversal`
+ interface.
+ """
+
+ def __init__(self, request):
+ self.request = request
+
@implementer(IContextFound)
class ContextFound(object):
""" An instance of this class is emitted as an :term:`event` after
@@ -156,7 +177,7 @@ class ContextFound(object):
AfterTraversal = ContextFound # b/c as of 1.0
@implementer(IApplicationCreated)
-class ApplicationCreated(object):
+class ApplicationCreated(object):
""" An instance of this class is emitted as an :term:`event` when
the :meth:`pyramid.config.Configurator.make_wsgi_app` is
called. The instance has an attribute, ``app``, which is an
@@ -243,4 +264,3 @@ class BeforeRender(dict):
dict.__init__(self, system)
self.rendering_val = rendering_val
-
diff --git a/pyramid/interfaces.py b/pyramid/interfaces.py
index 64bb4b50c..2b00752cf 100644
--- a/pyramid/interfaces.py
+++ b/pyramid/interfaces.py
@@ -25,6 +25,14 @@ class IContextFound(Interface):
IAfterTraversal = IContextFound
+class IBeforeTraversal(Interface):
+ """
+ An event type that is emitted after :app:`Pyramid` attempted to find a
+ route but before it calls any traversal or view code. See the documentation
+ attached to :class:`pyramid.events.Routefound` for more information.
+ """
+ request = Attribute('The request object')
+
class INewRequest(Interface):
""" An event type that is emitted whenever :app:`Pyramid`
begins to process a new request. See the documentation attached
diff --git a/pyramid/router.py b/pyramid/router.py
index 4054ef52e..19773cf62 100644
--- a/pyramid/router.py
+++ b/pyramid/router.py
@@ -20,6 +20,7 @@ from pyramid.events import (
ContextFound,
NewRequest,
NewResponse,
+ BeforeTraversal,
)
from pyramid.httpexceptions import HTTPNotFound
@@ -114,10 +115,19 @@ class Router(object):
root_factory = route.factory or self.root_factory
+ # Notify anyone listening that we are about to start traversal
+ #
+ # Notify before creating root_factory in case we want to do something
+ # special on a route we may have matched. See
+ # https://github.com/Pylons/pyramid/pull/1876 for ideas of what is
+ # possible.
+ has_listeners and notify(BeforeTraversal(request))
+
+ # Create the root factory
root = root_factory(request)
attrs['root'] = root
- # find a context
+ # We are about to traverse and find a context
traverser = adapters.queryAdapter(root, ITraverser)
if traverser is None:
traverser = ResourceTreeTraverser(root)
@@ -133,6 +143,9 @@ class Router(object):
)
attrs.update(tdict)
+
+ # Notify anyone listening that we have a context and traversal is
+ # complete
has_listeners and notify(ContextFound(request))
# find a view callable
diff --git a/pyramid/session.py b/pyramid/session.py
index a4cdf910d..fd7b5f8d5 100644
--- a/pyramid/session.py
+++ b/pyramid/session.py
@@ -123,6 +123,9 @@ def check_csrf_token(request,
Note that using this function requires that a :term:`session factory` is
configured.
+ See :ref:`auto_csrf_checking` for information about how to secure your
+ application automatically against CSRF attacks.
+
.. versionadded:: 1.4a2
"""
supplied_token = request.params.get(token, request.headers.get(header, ""))
diff --git a/pyramid/settings.py b/pyramid/settings.py
index e2cb3cb3c..8a498d572 100644
--- a/pyramid/settings.py
+++ b/pyramid/settings.py
@@ -1,13 +1,12 @@
from pyramid.compat import string_types
truthy = frozenset(('t', 'true', 'y', 'yes', 'on', '1'))
+falsey = frozenset(('f', 'false', 'n', 'no', 'off', '0'))
def asbool(s):
""" Return the boolean value ``True`` if the case-lowered value of string
- input ``s`` is any of ``t``, ``true``, ``y``, ``on``, or ``1``, otherwise
- return the boolean value ``False``. If ``s`` is the value ``None``,
- return ``False``. If ``s`` is already one of the boolean values ``True``
- or ``False``, return it."""
+ input ``s`` is a :term:`truthy string`. If ``s`` is already one of the
+ boolean values ``True`` or ``False``, return it."""
if s is None:
return False
if isinstance(s, bool):
diff --git a/pyramid/tests/test_config/test_views.py b/pyramid/tests/test_config/test_views.py
index b2513c42c..0bf0bd0b3 100644
--- a/pyramid/tests/test_config/test_views.py
+++ b/pyramid/tests/test_config/test_views.py
@@ -1491,6 +1491,22 @@ class TestViewsConfigurationMixin(unittest.TestCase):
request.upath_info = text_('/')
self._assertNotFound(wrapper, None, request)
+ def test_add_view_with_check_csrf_predicates_match(self):
+ import warnings
+ from pyramid.renderers import null_renderer
+ view = lambda *arg: 'OK'
+ config = self._makeOne(autocommit=True)
+ with warnings.catch_warnings(record=True) as w:
+ warnings.filterwarnings('always')
+ config.add_view(view=view, check_csrf=True, renderer=null_renderer)
+ self.assertEqual(len(w), 1)
+ wrapper = self._getViewCallable(config)
+ request = self._makeRequest(config)
+ request.session = DummySession({'csrf_token': 'foo'})
+ request.params = {'csrf_token': 'foo'}
+ request.headers = {}
+ self.assertEqual(wrapper(None, request), 'OK')
+
def test_add_view_with_custom_predicates_match(self):
import warnings
from pyramid.renderers import null_renderer
@@ -1570,6 +1586,46 @@ class TestViewsConfigurationMixin(unittest.TestCase):
config.add_view(view=view2)
self.assertRaises(ConfigurationConflictError, config.commit)
+ def test_add_view_with_csrf_param(self):
+ from pyramid.renderers import null_renderer
+ def view(request):
+ return 'OK'
+ config = self._makeOne(autocommit=True)
+ config.add_view(view, require_csrf='st', renderer=null_renderer)
+ view = self._getViewCallable(config)
+ request = self._makeRequest(config)
+ request.method = 'POST'
+ request.params = {'st': 'foo'}
+ request.headers = {}
+ request.session = DummySession({'csrf_token': 'foo'})
+ self.assertEqual(view(None, request), 'OK')
+
+ def test_add_view_with_csrf_header(self):
+ from pyramid.renderers import null_renderer
+ def view(request):
+ return 'OK'
+ config = self._makeOne(autocommit=True)
+ config.add_view(view, require_csrf=True, renderer=null_renderer)
+ view = self._getViewCallable(config)
+ request = self._makeRequest(config)
+ request.method = 'POST'
+ request.headers = {'X-CSRF-Token': 'foo'}
+ request.session = DummySession({'csrf_token': 'foo'})
+ self.assertEqual(view(None, request), 'OK')
+
+ def test_add_view_with_missing_csrf_header(self):
+ from pyramid.exceptions import BadCSRFToken
+ from pyramid.renderers import null_renderer
+ def view(request): return 'OK'
+ config = self._makeOne(autocommit=True)
+ config.add_view(view, require_csrf=True, renderer=null_renderer)
+ view = self._getViewCallable(config)
+ request = self._makeRequest(config)
+ request.method = 'POST'
+ request.headers = {}
+ request.session = DummySession({'csrf_token': 'foo'})
+ self.assertRaises(BadCSRFToken, lambda: view(None, request))
+
def test_add_view_with_permission(self):
from pyramid.renderers import null_renderer
view1 = lambda *arg: 'OK'
@@ -3233,3 +3289,7 @@ class DummyIntrospector(object):
return self.getval
def relate(self, a, b):
self.related.append((a, b))
+
+class DummySession(dict):
+ def get_csrf_token(self):
+ return self['csrf_token']
diff --git a/pyramid/tests/test_events.py b/pyramid/tests/test_events.py
index 2c72c07e8..52e53c34e 100644
--- a/pyramid/tests/test_events.py
+++ b/pyramid/tests/test_events.py
@@ -14,7 +14,7 @@ class NewRequestEventTests(unittest.TestCase):
from zope.interface.verify import verifyClass
klass = self._getTargetClass()
verifyClass(INewRequest, klass)
-
+
def test_instance_conforms_to_INewRequest(self):
from pyramid.interfaces import INewRequest
from zope.interface.verify import verifyObject
@@ -40,7 +40,7 @@ class NewResponseEventTests(unittest.TestCase):
from zope.interface.verify import verifyClass
klass = self._getTargetClass()
verifyClass(INewResponse, klass)
-
+
def test_instance_conforms_to_INewResponse(self):
from pyramid.interfaces import INewResponse
from zope.interface.verify import verifyObject
@@ -103,7 +103,7 @@ class ContextFoundEventTests(unittest.TestCase):
from zope.interface.verify import verifyClass
from pyramid.interfaces import IContextFound
verifyClass(IContextFound, self._getTargetClass())
-
+
def test_instance_conforms_to_IContextFound(self):
from zope.interface.verify import verifyObject
from pyramid.interfaces import IContextFound
@@ -118,12 +118,33 @@ class AfterTraversalEventTests(ContextFoundEventTests):
from zope.interface.verify import verifyClass
from pyramid.interfaces import IAfterTraversal
verifyClass(IAfterTraversal, self._getTargetClass())
-
+
def test_instance_conforms_to_IAfterTraversal(self):
from zope.interface.verify import verifyObject
from pyramid.interfaces import IAfterTraversal
verifyObject(IAfterTraversal, self._makeOne())
+class BeforeTraversalEventTests(unittest.TestCase):
+ def _getTargetClass(self):
+ from pyramid.events import BeforeTraversal
+ return BeforeTraversal
+
+ def _makeOne(self, request=None):
+ if request is None:
+ request = DummyRequest()
+ return self._getTargetClass()(request)
+
+ def test_class_conforms_to_IBeforeTraversal(self):
+ from zope.interface.verify import verifyClass
+ from pyramid.interfaces import IBeforeTraversal
+ verifyClass(IBeforeTraversal, self._getTargetClass())
+
+ def test_instance_conforms_to_IBeforeTraversal(self):
+ from zope.interface.verify import verifyObject
+ from pyramid.interfaces import IBeforeTraversal
+ verifyObject(IBeforeTraversal, self._makeOne())
+
+
class TestSubscriber(unittest.TestCase):
def setUp(self):
self.config = testing.setUp()
@@ -221,7 +242,7 @@ class TestBeforeRender(unittest.TestCase):
result = event.setdefault('a', 1)
self.assertEqual(result, 1)
self.assertEqual(event, {'a':1})
-
+
def test_setdefault_success(self):
event = self._makeOne({})
event['a'] = 1
@@ -282,7 +303,7 @@ class DummyConfigurator(object):
class DummyRegistry(object):
pass
-
+
class DummyVenusian(object):
def __init__(self):
self.attached = []
@@ -292,7 +313,7 @@ class DummyVenusian(object):
class Dummy:
pass
-
+
class DummyRequest:
pass
diff --git a/pyramid/tests/test_router.py b/pyramid/tests/test_router.py
index 1cdc4abaa..7aa42804c 100644
--- a/pyramid/tests/test_router.py
+++ b/pyramid/tests/test_router.py
@@ -591,6 +591,7 @@ class TestRouter(unittest.TestCase):
def test_call_eventsends(self):
from pyramid.interfaces import INewRequest
from pyramid.interfaces import INewResponse
+ from pyramid.interfaces import IBeforeTraversal
from pyramid.interfaces import IContextFound
from pyramid.interfaces import IViewClassifier
context = DummyContext()
@@ -601,6 +602,7 @@ class TestRouter(unittest.TestCase):
environ = self._makeEnviron()
self._registerView(view, '', IViewClassifier, None, None)
request_events = self._registerEventListener(INewRequest)
+ beforetraversal_events = self._registerEventListener(IBeforeTraversal)
context_found_events = self._registerEventListener(IContextFound)
response_events = self._registerEventListener(INewResponse)
router = self._makeOne()
@@ -608,6 +610,8 @@ class TestRouter(unittest.TestCase):
result = router(environ, start_response)
self.assertEqual(len(request_events), 1)
self.assertEqual(request_events[0].request.environ, environ)
+ self.assertEqual(len(beforetraversal_events), 1)
+ self.assertEqual(beforetraversal_events[0].request.environ, environ)
self.assertEqual(len(context_found_events), 1)
self.assertEqual(context_found_events[0].request.environ, environ)
self.assertEqual(context_found_events[0].request.context, context)
diff --git a/pyramid/tests/test_viewderivers.py b/pyramid/tests/test_viewderivers.py
index 1823beb4d..c8fbe6f36 100644
--- a/pyramid/tests/test_viewderivers.py
+++ b/pyramid/tests/test_viewderivers.py
@@ -1090,6 +1090,149 @@ class TestDeriveView(unittest.TestCase):
self.assertRaises(ConfigurationError, self.config._derive_view,
view, http_cache=(None,))
+ def test_csrf_view_requires_bool_or_str_in_require_csrf(self):
+ def view(request): pass
+ try:
+ self.config._derive_view(view, require_csrf=object())
+ except ConfigurationError as ex:
+ self.assertEqual(
+ 'View option "require_csrf" must be a string or boolean value',
+ ex.args[0])
+ else: # pragma: no cover
+ raise AssertionError
+
+ def test_csrf_view_requires_bool_or_str_in_config_setting(self):
+ def view(request): pass
+ self.config.add_settings({'pyramid.require_default_csrf': object()})
+ try:
+ self.config._derive_view(view)
+ except ConfigurationError as ex:
+ self.assertEqual(
+ 'Config setting "pyramid.require_csrf_default" must be a '
+ 'string or boolean value',
+ ex.args[0])
+ else: # pragma: no cover
+ raise AssertionError
+
+ def test_csrf_view_requires_header(self):
+ response = DummyResponse()
+ def inner_view(request):
+ return response
+ request = self._makeRequest()
+ request.method = 'POST'
+ request.session = DummySession({'csrf_token': 'foo'})
+ request.headers = {'X-CSRF-Token': 'foo'}
+ view = self.config._derive_view(inner_view, require_csrf=True)
+ result = view(None, request)
+ self.assertTrue(result is response)
+
+ def test_csrf_view_requires_param(self):
+ response = DummyResponse()
+ def inner_view(request):
+ return response
+ request = self._makeRequest()
+ request.method = 'POST'
+ request.session = DummySession({'csrf_token': 'foo'})
+ request.params['DUMMY'] = 'foo'
+ view = self.config._derive_view(inner_view, require_csrf='DUMMY')
+ result = view(None, request)
+ self.assertTrue(result is response)
+
+ def test_csrf_view_ignores_GET(self):
+ response = DummyResponse()
+ def inner_view(request):
+ return response
+ request = self._makeRequest()
+ request.method = 'GET'
+ view = self.config._derive_view(inner_view, require_csrf=True)
+ result = view(None, request)
+ self.assertTrue(result is response)
+
+ def test_csrf_view_fails_on_bad_POST_param(self):
+ from pyramid.exceptions import BadCSRFToken
+ def inner_view(request): pass
+ request = self._makeRequest()
+ request.method = 'POST'
+ request.session = DummySession({'csrf_token': 'foo'})
+ request.params['DUMMY'] = 'bar'
+ view = self.config._derive_view(inner_view, require_csrf='DUMMY')
+ self.assertRaises(BadCSRFToken, lambda: view(None, request))
+
+ def test_csrf_view_fails_on_bad_POST_header(self):
+ from pyramid.exceptions import BadCSRFToken
+ def inner_view(request): pass
+ request = self._makeRequest()
+ request.method = 'POST'
+ request.session = DummySession({'csrf_token': 'foo'})
+ request.headers = {'X-CSRF-Token': 'bar'}
+ view = self.config._derive_view(inner_view, require_csrf='DUMMY')
+ self.assertRaises(BadCSRFToken, lambda: view(None, request))
+
+ def test_csrf_view_uses_config_setting_truthy(self):
+ response = DummyResponse()
+ def inner_view(request):
+ return response
+ request = self._makeRequest()
+ request.method = 'POST'
+ request.session = DummySession({'csrf_token': 'foo'})
+ request.params['csrf_token'] = 'foo'
+ self.config.add_settings({'pyramid.require_default_csrf': 'yes'})
+ view = self.config._derive_view(inner_view)
+ result = view(None, request)
+ self.assertTrue(result is response)
+
+ def test_csrf_view_uses_config_setting_with_custom_token(self):
+ response = DummyResponse()
+ def inner_view(request):
+ return response
+ request = self._makeRequest()
+ request.method = 'POST'
+ request.session = DummySession({'csrf_token': 'foo'})
+ request.params['DUMMY'] = 'foo'
+ self.config.add_settings({'pyramid.require_default_csrf': 'DUMMY'})
+ view = self.config._derive_view(inner_view)
+ result = view(None, request)
+ self.assertTrue(result is response)
+
+ def test_csrf_view_uses_config_setting_falsey(self):
+ response = DummyResponse()
+ def inner_view(request):
+ return response
+ request = self._makeRequest()
+ request.method = 'POST'
+ request.session = DummySession({'csrf_token': 'foo'})
+ request.params['csrf_token'] = 'foo'
+ self.config.add_settings({'pyramid.require_default_csrf': 'no'})
+ view = self.config._derive_view(inner_view)
+ result = view(None, request)
+ self.assertTrue(result is response)
+
+ def test_csrf_view_uses_view_option_override(self):
+ response = DummyResponse()
+ def inner_view(request):
+ return response
+ request = self._makeRequest()
+ request.method = 'POST'
+ request.session = DummySession({'csrf_token': 'foo'})
+ request.params['DUMMY'] = 'foo'
+ self.config.add_settings({'pyramid.require_default_csrf': 'yes'})
+ view = self.config._derive_view(inner_view, require_csrf='DUMMY')
+ result = view(None, request)
+ self.assertTrue(result is response)
+
+ def test_csrf_view_uses_config_setting_when_view_option_is_true(self):
+ response = DummyResponse()
+ def inner_view(request):
+ return response
+ request = self._makeRequest()
+ request.method = 'POST'
+ request.session = DummySession({'csrf_token': 'foo'})
+ request.params['DUMMY'] = 'foo'
+ self.config.add_settings({'pyramid.require_default_csrf': 'DUMMY'})
+ view = self.config._derive_view(inner_view, require_csrf=True)
+ result = view(None, request)
+ self.assertTrue(result is response)
+
class TestDerivationOrder(unittest.TestCase):
def setUp(self):
@@ -1111,6 +1254,7 @@ class TestDerivationOrder(unittest.TestCase):
dlist = [d for (d, _) in derivers_sorted]
self.assertEqual([
'secured_view',
+ 'csrf_view',
'owrapped_view',
'http_cached_view',
'decorated_view',
@@ -1133,6 +1277,7 @@ class TestDerivationOrder(unittest.TestCase):
dlist = [d for (d, _) in derivers_sorted]
self.assertEqual([
'secured_view',
+ 'csrf_view',
'owrapped_view',
'http_cached_view',
'decorated_view',
@@ -1153,6 +1298,7 @@ class TestDerivationOrder(unittest.TestCase):
dlist = [d for (d, _) in derivers_sorted]
self.assertEqual([
'secured_view',
+ 'csrf_view',
'owrapped_view',
'http_cached_view',
'decorated_view',
@@ -1174,6 +1320,7 @@ class TestDerivationOrder(unittest.TestCase):
dlist = [d for (d, _) in derivers_sorted]
self.assertEqual([
'secured_view',
+ 'csrf_view',
'owrapped_view',
'http_cached_view',
'decorated_view',
@@ -1408,6 +1555,7 @@ class DummyRequest:
self.environ = environ
self.params = {}
self.cookies = {}
+ self.headers = {}
self.response = DummyResponse()
class DummyLogger:
@@ -1428,6 +1576,10 @@ class DummySecurityPolicy:
def permits(self, context, principals, permission):
return self.permitted
+class DummySession(dict):
+ def get_csrf_token(self):
+ return self['csrf_token']
+
def parse_httpdate(s):
import datetime
# cannot use %Z, must use literal GMT; Jython honors timezone
diff --git a/pyramid/view.py b/pyramid/view.py
index 0129526ce..62ac5310e 100644
--- a/pyramid/view.py
+++ b/pyramid/view.py
@@ -169,7 +169,8 @@ class view_config(object):
``request_type``, ``route_name``, ``request_method``, ``request_param``,
``containment``, ``xhr``, ``accept``, ``header``, ``path_info``,
``custom_predicates``, ``decorator``, ``mapper``, ``http_cache``,
- ``match_param``, ``check_csrf``, ``physical_path``, and ``predicates``.
+ ``require_csrf``, ``match_param``, ``check_csrf``, ``physical_path``, and
+ ``view_options``.
The meanings of these arguments are the same as the arguments passed to
:meth:`pyramid.config.Configurator.add_view`. If any argument is left
diff --git a/pyramid/viewderivers.py b/pyramid/viewderivers.py
index 8061e5d4a..41102319d 100644
--- a/pyramid/viewderivers.py
+++ b/pyramid/viewderivers.py
@@ -6,6 +6,7 @@ from zope.interface import (
)
from pyramid.security import NO_PERMISSION_REQUIRED
+from pyramid.session import check_csrf_token
from pyramid.response import Response
from pyramid.interfaces import (
@@ -18,6 +19,7 @@ from pyramid.interfaces import (
)
from pyramid.compat import (
+ string_types,
is_bound_method,
is_unbound_method,
)
@@ -33,6 +35,10 @@ from pyramid.exceptions import (
PredicateMismatch,
)
from pyramid.httpexceptions import HTTPForbidden
+from pyramid.settings import (
+ falsey,
+ truthy,
+)
from pyramid.util import object_description
from pyramid.view import render_view_to_response
from pyramid import renderers
@@ -455,5 +461,40 @@ def decorated_view(view, info):
decorated_view.options = ('decorator',)
+def _parse_csrf_setting(val, error_source):
+ if val:
+ if isinstance(val, string_types):
+ if val.lower() in truthy:
+ val = True
+ elif val.lower() in falsey:
+ val = False
+ elif not isinstance(val, bool):
+ raise ConfigurationError(
+ '{0} must be a string or boolean value'
+ .format(error_source))
+ return val
+
+def csrf_view(view, info):
+ default_val = _parse_csrf_setting(
+ info.settings.get('pyramid.require_default_csrf'),
+ 'Config setting "pyramid.require_csrf_default"')
+ val = _parse_csrf_setting(
+ info.options.get('require_csrf'),
+ 'View option "require_csrf"')
+ if (val is True and default_val) or val is None:
+ val = default_val
+ if val is True:
+ val = 'csrf_token'
+ wrapped_view = view
+ if val:
+ def csrf_view(context, request):
+ if request.method == 'POST':
+ check_csrf_token(request, val, raises=True)
+ return view(context, request)
+ wrapped_view = csrf_view
+ return wrapped_view
+
+csrf_view.options = ('require_csrf',)
+
VIEW = 'VIEW'
INGRESS = 'INGRESS'