summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChris McDonough <chrism@plope.com>2015-04-02 13:57:30 -0400
committerChris McDonough <chrism@plope.com>2015-04-02 13:57:30 -0400
commit45ddb5a5b744aebeac6004e9dba1c03d5bc8c50f (patch)
tree9efe59281dbc0b20b9a94b69ee0a18bcb124aa93
parentb6498fce8c1418f3c7b33d31aa9c151a86bc4166 (diff)
parent575ff8050b0cbdbf424947361f419b803568e122 (diff)
downloadpyramid-45ddb5a5b744aebeac6004e9dba1c03d5bc8c50f.tar.gz
pyramid-45ddb5a5b744aebeac6004e9dba1c03d5bc8c50f.tar.bz2
pyramid-45ddb5a5b744aebeac6004e9dba1c03d5bc8c50f.zip
Merge branch 'master' of github.com:Pylons/pyramid
-rw-r--r--.gitignore4
-rw-r--r--.travis.yml24
-rw-r--r--CHANGES.txt184
-rw-r--r--CONTRIBUTORS.txt14
-rw-r--r--HACKING.txt5
-rw-r--r--HISTORY.txt2
-rw-r--r--README.rst11
-rw-r--r--RELEASING.txt3
-rw-r--r--TODO.txt5
-rw-r--r--docs/_static/pyramid_request_processing.graffle9748
-rw-r--r--docs/_static/pyramid_request_processing.pngbin0 -> 122854 bytes
-rw-r--r--docs/_static/pyramid_request_processing.svg3
-rw-r--r--docs/_static/pyramid_router.graffle1621
-rw-r--r--docs/_static/pyramid_router.pngbin0 -> 120643 bytes
-rw-r--r--docs/_static/pyramid_router.svg3
m---------docs/_themes0
-rw-r--r--docs/api/config.rst5
-rw-r--r--docs/api/exceptions.rst12
-rw-r--r--docs/api/httpexceptions.rst94
-rw-r--r--docs/api/index.rst12
-rw-r--r--docs/api/interfaces.rst5
-rw-r--r--docs/api/registry.rst12
-rw-r--r--docs/api/request.rst42
-rw-r--r--docs/api/security.rst2
-rw-r--r--docs/api/static.rst14
-rw-r--r--docs/conf.py2
-rw-r--r--docs/glossary.rst37
-rw-r--r--docs/index.rst3
-rw-r--r--docs/narr/MyProject/myproject/tests.py37
-rw-r--r--docs/narr/MyProject/setup.py45
-rw-r--r--docs/narr/assets.rst244
-rw-r--r--docs/narr/commandline.rst58
-rw-r--r--docs/narr/configuration.rst4
-rw-r--r--docs/narr/environment.rst170
-rw-r--r--docs/narr/extconfig.rst104
-rw-r--r--docs/narr/hooks.rst51
-rw-r--r--docs/narr/hybrid.rst2
-rw-r--r--docs/narr/i18n.rst18
-rw-r--r--docs/narr/introspector.rst16
-rw-r--r--docs/narr/logging.rst68
-rw-r--r--docs/narr/router.rst6
-rw-r--r--docs/narr/security.rst154
-rw-r--r--docs/narr/sessions.rst4
-rw-r--r--docs/narr/startup.rst8
-rw-r--r--docs/narr/templates.rst3
-rw-r--r--docs/narr/testing.rst142
-rw-r--r--docs/narr/urldispatch.rst62
-rw-r--r--docs/narr/viewconfig.rst6
-rw-r--r--docs/narr/webob.rst8
-rw-r--r--docs/quick_tour.rst2
-rw-r--r--docs/quick_tutorial/debugtoolbar.rst52
-rw-r--r--docs/quick_tutorial/forms.rst2
-rw-r--r--docs/quick_tutorial/functional_testing.rst7
-rw-r--r--docs/quick_tutorial/hello_world.rst2
-rw-r--r--docs/quick_tutorial/ini.rst6
-rw-r--r--docs/quick_tutorial/jinja2.rst13
-rw-r--r--docs/quick_tutorial/jinja2/tutorial/tests.py8
-rw-r--r--docs/quick_tutorial/logging.rst2
-rw-r--r--docs/quick_tutorial/more_view_classes/tutorial/views.py6
-rw-r--r--docs/tutorials/wiki/authorization.rst4
-rw-r--r--docs/tutorials/wiki/design.rst4
-rw-r--r--docs/tutorials/wiki2/design.rst3
-rw-r--r--pyramid/authentication.py29
-rw-r--r--pyramid/compat.py113
-rw-r--r--pyramid/config/__init__.py135
-rw-r--r--pyramid/config/adapters.py2
-rw-r--r--pyramid/config/assets.py220
-rw-r--r--pyramid/config/factories.py52
-rw-r--r--pyramid/config/routes.py31
-rw-r--r--pyramid/config/settings.py9
-rw-r--r--pyramid/config/util.py3
-rw-r--r--pyramid/config/views.py163
-rw-r--r--pyramid/decorator.py8
-rw-r--r--pyramid/httpexceptions.py69
-rw-r--r--pyramid/i18n.py8
-rw-r--r--pyramid/interfaces.py134
-rw-r--r--pyramid/path.py10
-rw-r--r--pyramid/registry.py5
-rw-r--r--pyramid/renderers.py113
-rw-r--r--pyramid/request.py52
-rw-r--r--pyramid/response.py17
-rw-r--r--pyramid/router.py3
-rw-r--r--pyramid/scaffolds/alchemy/development.ini_tmpl2
-rw-r--r--pyramid/scaffolds/alchemy/production.ini_tmpl2
-rw-r--r--pyramid/scaffolds/starter/development.ini_tmpl2
-rw-r--r--pyramid/scaffolds/starter/production.ini_tmpl2
-rw-r--r--pyramid/scaffolds/tests.py4
-rw-r--r--pyramid/scaffolds/zodb/development.ini_tmpl2
-rw-r--r--pyramid/scaffolds/zodb/production.ini_tmpl2
-rw-r--r--pyramid/scripting.py8
-rw-r--r--pyramid/scripts/pcreate.py15
-rw-r--r--pyramid/scripts/prequest.py8
-rw-r--r--pyramid/scripts/proutes.py380
-rw-r--r--pyramid/scripts/pserve.py37
-rw-r--r--pyramid/scripts/pshell.py9
-rw-r--r--pyramid/security.py39
-rw-r--r--pyramid/session.py4
-rw-r--r--pyramid/static.py134
-rw-r--r--pyramid/testing.py12
-rw-r--r--pyramid/tests/test_authentication.py16
-rw-r--r--pyramid/tests/test_compat.py26
-rw-r--r--pyramid/tests/test_config/pkgs/asset/models.py8
-rw-r--r--pyramid/tests/test_config/pkgs/asset/subpackage/templates/bar.pt0
-rw-r--r--pyramid/tests/test_config/pkgs/asset/views.py22
-rw-r--r--pyramid/tests/test_config/test_adapters.py2
-rw-r--r--pyramid/tests/test_config/test_assets.py487
-rw-r--r--pyramid/tests/test_config/test_factories.py34
-rw-r--r--pyramid/tests/test_config/test_init.py91
-rw-r--r--pyramid/tests/test_config/test_settings.py31
-rw-r--r--pyramid/tests/test_config/test_util.py7
-rw-r--r--pyramid/tests/test_config/test_views.py190
-rw-r--r--pyramid/tests/test_decorator.py18
-rw-r--r--pyramid/tests/test_integration.py2
-rw-r--r--pyramid/tests/test_path.py2
-rw-r--r--pyramid/tests/test_registry.py5
-rw-r--r--pyramid/tests/test_renderers.py91
-rw-r--r--pyramid/tests/test_request.py93
-rw-r--r--pyramid/tests/test_response.py28
-rw-r--r--pyramid/tests/test_router.py22
-rw-r--r--pyramid/tests/test_scripting.py16
-rw-r--r--pyramid/tests/test_scripts/dummy.py2
-rw-r--r--pyramid/tests/test_scripts/pystartup.txt3
-rw-r--r--pyramid/tests/test_scripts/test_pcreate.py28
-rw-r--r--pyramid/tests/test_scripts/test_prequest.py13
-rw-r--r--pyramid/tests/test_scripts/test_proutes.py618
-rw-r--r--pyramid/tests/test_scripts/test_pserve.py2
-rw-r--r--pyramid/tests/test_scripts/test_pshell.py23
-rw-r--r--pyramid/tests/test_security.py21
-rw-r--r--pyramid/tests/test_session.py2
-rw-r--r--pyramid/tests/test_static.py163
-rw-r--r--pyramid/tests/test_testing.py6
-rw-r--r--pyramid/tests/test_traversal.py2
-rw-r--r--pyramid/tests/test_urldispatch.py2
-rw-r--r--pyramid/tests/test_util.py301
-rw-r--r--pyramid/traversal.py2
-rw-r--r--pyramid/url.py4
-rw-r--r--pyramid/urldispatch.py13
-rw-r--r--pyramid/util.py128
-rw-r--r--pyramid/view.py32
-rw-r--r--rtd.txt2
-rw-r--r--setup.cfg2
-rw-r--r--setup.py4
-rw-r--r--tox.ini73
143 files changed, 16398 insertions, 1265 deletions
diff --git a/.gitignore b/.gitignore
index 8dca2069c..fe132412a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,6 @@
*.egg
*.egg-info
+.eggs/
*.pyc
*$py.class
*.pt.py
@@ -7,9 +8,12 @@
*~
.*.swp
.coverage
+.coverage.*
.tox/
nosetests.xml
coverage.xml
+nosetests-*.xml
+coverage-*.xml
tutorial.db
build/
dist/
diff --git a/.travis.yml b/.travis.yml
index 29e499e76..42b3073c7 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,16 +1,24 @@
# Wire up travis
language: python
+sudo: false
-python:
- - 2.6
- - 2.7
- - pypy
- - 3.2
- - 3.3
+env:
+ - TOXENV=py26
+ - TOXENV=py27
+ - TOXENV=py32
+ - TOXENV=py33
+ - TOXENV=py34
+ - TOXENV=pypy
+ - TOXENV=pypy3
+ - TOXENV=py2-docs
+ - TOXENV=py3-docs
+ - TOXENV=py2-cover,py3-cover,coverage
-install: python setup.py dev
+install:
+ - travis_retry pip install tox
-script: python setup.py test -q
+script:
+ - travis_retry tox
notifications:
email:
diff --git a/CHANGES.txt b/CHANGES.txt
index 51af8ee01..639b9b802 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,21 +1,152 @@
Next release
============
+Features
+--------
+
+- The ``pyramid.config.Configurator`` has grown the ability to allow
+ actions to call other actions during a commit-cycle. This enables much more
+ logic to be placed into actions, such as the ability to invoke other actions
+ or group them for improved conflict detection. We have also exposed and
+ documented the config phases that Pyramid uses in order to further assist
+ in building conforming addons.
+ See https://github.com/Pylons/pyramid/pull/1513
+
+- Add ``pyramid.request.apply_request_extensions`` function which can be
+ used in testing to apply any request extensions configured via
+ ``config.add_request_method``. Previously it was only possible to test
+ the extensions by going through Pyramid's router.
+ See https://github.com/Pylons/pyramid/pull/1581
+
+- pcreate when run without a scaffold argument will now print information on
+ the missing flag, as well as a list of available scaffolds.
+ See https://github.com/Pylons/pyramid/pull/1566 and
+ https://github.com/Pylons/pyramid/issues/1297
+
+- Added support / testing for 'pypy3' under Tox and Travis.
+ See https://github.com/Pylons/pyramid/pull/1469
+
+- Automate code coverage metrics across py2 and py3 instead of just py2.
+ See https://github.com/Pylons/pyramid/pull/1471
+
+- Cache busting for static resources has been added and is available via a new
+ argument to ``pyramid.config.Configurator.add_static_view``: ``cachebust``.
+ Core APIs are shipped for both cache busting via query strings and
+ path segments and may be extended to fit into custom asset pipelines.
+ See https://github.com/Pylons/pyramid/pull/1380 and
+ https://github.com/Pylons/pyramid/pull/1583
+
+- Add ``pyramid.config.Configurator.root_package`` attribute and init
+ parameter to assist with includeable packages that wish to resolve
+ resources relative to the package in which the ``Configurator`` was created.
+ This is especially useful for addons that need to load asset specs from
+ settings, in which case it is may be natural for a developer to define
+ imports or assets relative to the top-level package.
+ See https://github.com/Pylons/pyramid/pull/1337
+
+- Added line numbers to the log formatters in the scaffolds to assist with
+ debugging. See https://github.com/Pylons/pyramid/pull/1326
+
+- Add new HTTP exception objects for status codes
+ ``428 Precondition Required``, ``429 Too Many Requests`` and
+ ``431 Request Header Fields Too Large`` in ``pyramid.httpexceptions``.
+ See https://github.com/Pylons/pyramid/pull/1372/files
+
+- The ``pshell`` script will now load a ``PYTHONSTARTUP`` file if one is
+ defined in the environment prior to launching the interpreter.
+ See https://github.com/Pylons/pyramid/pull/1448
+
+- Make it simple to define notfound and forbidden views that wish to use
+ the default exception-response view but with altered predicates and other
+ configuration options. The ``view`` argument is now optional in
+ ``config.add_notfound_view`` and ``config.add_forbidden_view``..
+ See https://github.com/Pylons/pyramid/issues/494
+
+- Greatly improve the readability of the ``pcreate`` shell script output.
+ See https://github.com/Pylons/pyramid/pull/1453
+
+- Improve robustness to timing attacks in the ``AuthTktCookieHelper`` and
+ the ``SignedCookieSessionFactory`` classes by using the stdlib's
+ ``hmac.compare_digest`` if it is available (such as Python 2.7.7+ and 3.3+).
+ See https://github.com/Pylons/pyramid/pull/1457
+
+- Assets can now be overidden by an absolute path on the filesystem when using
+ the ``config.override_asset`` API. This makes it possible to fully support
+ serving up static content from a mutable directory while still being able
+ to use the ``request.static_url`` API and ``config.add_static_view``.
+ Previously it was not possible to use ``config.add_static_view`` with an
+ absolute path **and** generate urls to the content. This change replaces
+ the call, ``config.add_static_view('/abs/path', 'static')``, with
+ ``config.add_static_view('myapp:static', 'static')`` and
+ ``config.override_asset(to_override='myapp:static/',
+ override_with='/abs/path/')``. The ``myapp:static`` asset spec is completely
+ made up and does not need to exist - it is used for generating urls
+ via ``request.static_url('myapp:static/foo.png')``.
+ See https://github.com/Pylons/pyramid/issues/1252
+
+- Added ``pyramid.config.Configurator.set_response_factory`` and the
+ ``response_factory`` keyword argument to the ``Configurator`` for defining
+ a factory that will return a custom ``Response`` class.
+ See https://github.com/Pylons/pyramid/pull/1499
+
+- Allow an iterator to be returned from a renderer. Previously it was only
+ possible to return bytes or unicode.
+ See https://github.com/Pylons/pyramid/pull/1417
+
+- ``pserve`` can now take a ``-b`` or ``--browser`` option to open the server
+ URL in a web browser. See https://github.com/Pylons/pyramid/pull/1533
+
+- Overall improvments for the ``proutes`` command. Added ``--format`` and
+ ``--glob`` arguments to the command, introduced the ``method``
+ column for displaying available request methods, and improved the ``view``
+ output by showing the module instead of just ``__repr__``.
+ See https://github.com/Pylons/pyramid/pull/1488
+
+- Support keyword-only arguments and function annotations in views in
+ Python 3. See https://github.com/Pylons/pyramid/pull/1556
+
+- ``request.response`` will no longer be mutated when using the
+ ``pyramid.renderers.render_to_response()`` API. It is now necessary to
+ pass in a ``response=`` argument to ``render_to_response`` if you wish to
+ supply the renderer with a custom response object for it to use. If you
+ do not pass one then a response object will be created using the
+ application's ``IResponseFactory``. Almost all renderers
+ mutate the ``request.response`` response object (for example, the JSON
+ renderer sets ``request.response.content_type`` to ``application/json``).
+ However, when invoking ``render_to_response`` it is not expected that the
+ response object being returned would be the same one used later in the
+ request. The response object returned from ``render_to_response`` is now
+ explicitly different from ``request.response``. This does not change the
+ API of a renderer. See https://github.com/Pylons/pyramid/pull/1563
+
+- The ``append_slash`` argument of ```Configurator().add_notfound_view()`` will
+ now accept anything that implements the ``IResponse`` interface and will use
+ that as the response class instead of the default ``HTTPFound``. See
+ https://github.com/Pylons/pyramid/pull/1610
+
Bug Fixes
---------
+- Work around an issue where ``pserve --reload`` would leave terminal echo
+ disabled if it reloaded during a pdb session.
+ See https://github.com/Pylons/pyramid/pull/1577,
+ https://github.com/Pylons/pyramid/pull/1592
+
- ``pyramid.wsgi.wsgiapp`` and ``pyramid.wsgi.wsgiapp2`` now raise
``ValueError`` when accidentally passed ``None``.
+ See https://github.com/Pylons/pyramid/pull/1320
- Fix an issue whereby predicates would be resolved as maybe_dotted in the
introspectable but not when passed for registration. This would mean that
- add_route_predicate for example can not take a string and turn it into the
- actual callable function.
+ ``add_route_predicate`` for example can not take a string and turn it into
+ the actual callable function.
+ See https://github.com/Pylons/pyramid/pull/1306
- Fix ``pyramid.testing.setUp`` to return a ``Configurator`` with a proper
package. Previously it was not possible to do package-relative includes
using the returned ``Configurator`` during testing. There is now a
``package`` argument that can override this behavior as well.
+ See https://github.com/Pylons/pyramid/pull/1322
- Fix an issue where a ``pyramid.response.FileResponse`` may apply a charset
where it does not belong. See https://github.com/Pylons/pyramid/pull/1251
@@ -25,9 +156,54 @@ Bug Fixes
type, unlike any previous version of Python. See
https://github.com/Pylons/pyramid/issues/1360 for more information.
+- ``pcreate`` now normalizes the package name by converting hyphens to
+ underscores. See https://github.com/Pylons/pyramid/pull/1376
+
+- Fix an issue with the final response/finished callback being unable to
+ add another callback to the list. See
+ https://github.com/Pylons/pyramid/pull/1373
+
+- Fix a failing unittest caused by differing mimetypes across various OSs.
+ See https://github.com/Pylons/pyramid/issues/1405
+
+- Fix route generation for static view asset specifications having no path.
+ See https://github.com/Pylons/pyramid/pull/1377
+
+- Allow the ``pyramid.renderers.JSONP`` renderer to work even if there is no
+ valid request object. In this case it will not wrap the object in a
+ callback and thus behave just like the ``pyramid.renderers.JSON`` renderer.
+ See https://github.com/Pylons/pyramid/pull/1561
+
+- Prevent "parameters to load are deprecated" ``DeprecationWarning``
+ from setuptools>=11.3. See https://github.com/Pylons/pyramid/pull/1541
+
+- Avoiding sharing the ``IRenderer`` objects across threads when attached to
+ a view using the `renderer=` argument. These renderers were instantiated
+ at time of first render and shared between requests, causing potentially
+ subtle effects like `pyramid.reload_templates = true` failing to work
+ in `pyramid_mako`. See https://github.com/Pylons/pyramid/pull/1575
+ and https://github.com/Pylons/pyramid/issues/1268
+
+- Avoiding timing attacks against CSRF tokens.
+ See https://github.com/Pylons/pyramid/pull/1574
+
+Deprecations
+------------
+
+- Renamed the ``principal`` argument to ``pyramid.security.remember()`` to
+ ``userid`` in order to clarify its intended purpose.
+ See https://github.com/Pylons/pyramid/pull/1399
+
Docs
----
+- Moved the documentation for ``accept`` on ``Configurator.add_view`` to no
+ longer be part of the predicate list. See
+ https://github.com/Pylons/pyramid/issues/1391 for a bug report stating
+ ``not_`` was failing on ``accept``. Discussion with @mcdonc led to the
+ conclusion that it should not be documented as a predicate.
+ See https://github.com/Pylons/pyramid/pull/1487 for this PR
+
- Removed logging configuration from Quick Tutorial ini files except for
scaffolding- and logging-related chapters to avoid needing to explain it too
early.
@@ -35,6 +211,10 @@ Docs
- Clarify a previously-implied detail of the ``ISession.invalidate`` API
documentation.
+- Improve and clarify the documentation on what Pyramid defines as a
+ ``principal`` and a ``userid`` in its security APIs.
+ See https://github.com/Pylons/pyramid/pull/1399
+
Scaffolds
---------
diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt
index c77d3e92c..3d574f99d 100644
--- a/CONTRIBUTORS.txt
+++ b/CONTRIBUTORS.txt
@@ -232,3 +232,17 @@ Contributors
- Amit Mane, 2014/01/23
- Fenton Travers, 2014/05/06
+
+- Randall Leeds, 2014/11/11
+
+- Hugo Branquinho, 2014/11/25
+
+- Adrian Teng, 2014/12/17
+
+- Ilja Everila, 2015/02/05
+
+- Geoffrey T. Dairiki, 2015/02/06
+
+- David Glick, 2015/02/12
+
+- Donald Stufft, 2015/03/15
diff --git a/HACKING.txt b/HACKING.txt
index 1386be3af..e104869ec 100644
--- a/HACKING.txt
+++ b/HACKING.txt
@@ -31,7 +31,7 @@ By Hand
$ cd hack-on-pyramid
# Configure remotes such that you can pull changes from the Pyramid
# repository into your local repository.
- $ git remote add upstream https://github.com:Pylons/pyramid.git
+ $ git remote add upstream https://github.com/Pylons/pyramid.git
# fetch and merge changes from upstream into master
$ git fetch upstream
$ git merge upstream/master
@@ -113,6 +113,7 @@ for this use case) and inside that a simple pyramid application named
``hacking`` that you can then fire up like so:
cd env27/hacking
+ ../bin/python setup.py develop
../bin/pserve development.ini
Adding Features
@@ -194,7 +195,7 @@ Test Coverage
-------------
- The codebase *must* have 100% test statement coverage after each commit.
- You can test coverage via ``tox -e coverage``, or alternately by installing
+ You can test coverage via ``tox -e cover``, or alternately by installing
``nose`` and ``coverage`` into your virtualenv (easiest via ``setup.py
dev``) , and running ``setup.py nosetests --with-coverage``.
diff --git a/HISTORY.txt b/HISTORY.txt
index 6aad221a8..242568e98 100644
--- a/HISTORY.txt
+++ b/HISTORY.txt
@@ -1327,7 +1327,7 @@ Bug Fixes
- Make test suite pass on 32-bit systems; closes #286. closes #306.
See also https://github.com/Pylons/pyramid/issues/286
-- The ``pryamid.view.view_config`` decorator did not accept a ``match_params``
+- The ``pyramid.view.view_config`` decorator did not accept a ``match_params``
predicate argument. See https://github.com/Pylons/pyramid/pull/308
- The AuthTktCookieHelper could potentially generate Unicode headers
diff --git a/README.rst b/README.rst
index a3458028b..6de42ea40 100644
--- a/README.rst
+++ b/README.rst
@@ -1,6 +1,17 @@
Pyramid
=======
+.. image:: https://travis-ci.org/Pylons/pyramid.png?branch=master
+ :target: https://travis-ci.org/Pylons/pyramid
+
+.. image:: https://readthedocs.org/projects/pyramid/badge/?version=master
+ :target: http://docs.pylonsproject.org/projects/pyramid/en/master/
+ :alt: Master Documentation Status
+
+.. image:: https://readthedocs.org/projects/pyramid/badge/?version=latest
+ :target: http://docs.pylonsproject.org/projects/pyramid/en/latest/
+ :alt: Latest Documentation Status
+
Pyramid is a small, fast, down-to-earth, open source Python web framework.
It makes real-world web application development and
deployment more fun, more predictable, and more productive.
diff --git a/RELEASING.txt b/RELEASING.txt
index 553d2dcf2..0adef552c 100644
--- a/RELEASING.txt
+++ b/RELEASING.txt
@@ -26,6 +26,9 @@ Releasing Pyramid
- Copy relevant changes (delta bug fixes) from CHANGES.txt to
docs/whatsnew-X.X (if it's a major release).
+- update README.rst to use correct versions of badges and URLs according to
+ each branch and context, i.e., RTD "latest" == GitHub/Travis "1.x-branch".
+
- Make sure docs render OK::
$ cd docs
diff --git a/TODO.txt b/TODO.txt
index 62b8c39f4..e738b58d8 100644
--- a/TODO.txt
+++ b/TODO.txt
@@ -125,7 +125,10 @@ Future
- 1.7: Change ``pyramid.authentication.AuthTktAuthenticationPolicy`` default
``hashalg`` to ``sha512``.
-- 1.8 Remove set_request_property.
+- 1.8: Remove set_request_property.
+
+- 1.9: Remove extra code enabling ``pyramid.security.remember(principal=...)``
+ and force use of ``userid``.
Probably Bad Ideas
------------------
diff --git a/docs/_static/pyramid_request_processing.graffle b/docs/_static/pyramid_request_processing.graffle
new file mode 100644
index 000000000..71319610b
--- /dev/null
+++ b/docs/_static/pyramid_request_processing.graffle
@@ -0,0 +1,9748 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>ActiveLayerIndex</key>
+ <integer>0</integer>
+ <key>ApplicationVersion</key>
+ <array>
+ <string>com.omnigroup.OmniGrafflePro</string>
+ <string>139.18.0.187838</string>
+ </array>
+ <key>AutoAdjust</key>
+ <true/>
+ <key>BackgroundGraphic</key>
+ <dict>
+ <key>Bounds</key>
+ <string>{{0, 0}, {576, 733}}</string>
+ <key>Class</key>
+ <string>SolidGraphic</string>
+ <key>FontInfo</key>
+ <dict>
+ <key>Font</key>
+ <string>Helvetica</string>
+ <key>Size</key>
+ <real>12</real>
+ </dict>
+ <key>ID</key>
+ <integer>2</integer>
+ <key>Style</key>
+ <dict>
+ <key>shadow</key>
+ <dict>
+ <key>Draws</key>
+ <string>NO</string>
+ </dict>
+ <key>stroke</key>
+ <dict>
+ <key>Draws</key>
+ <string>NO</string>
+ </dict>
+ </dict>
+ </dict>
+ <key>BaseZoom</key>
+ <integer>0</integer>
+ <key>CanvasOrigin</key>
+ <string>{0, 0}</string>
+ <key>ColumnAlign</key>
+ <integer>1</integer>
+ <key>ColumnSpacing</key>
+ <real>36</real>
+ <key>CreationDate</key>
+ <string>2014-11-18 08:33:33 +0000</string>
+ <key>Creator</key>
+ <string>Steve Piercy</string>
+ <key>DisplayScale</key>
+ <string>1 0/72 in = 1 0/72 in</string>
+ <key>GraphDocumentVersion</key>
+ <integer>8</integer>
+ <key>GraphicsList</key>
+ <array>
+ <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>169389</integer>
+ </dict>
+ <key>ID</key>
+ <integer>169504</integer>
+ <key>Layer</key>
+ <integer>0</integer>
+ <key>Points</key>
+ <array>
+ <string>{344.41667175292969, 402.88506673894034}</string>
+ <string>{375.5, 402.27232108797347}</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>169428</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>169382</integer>
+ </dict>
+ <key>ID</key>
+ <integer>169433</integer>
+ <key>Layer</key>
+ <integer>0</integer>
+ <key>Points</key>
+ <array>
+ <string>{155.00000254313238, 459.27667544230695}</string>
+ <string>{238.5002713470962, 456.52468399152298}</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>169370</integer>
+ <key>Position</key>
+ <real>0.28820157051086426</real>
+ </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>169383</integer>
+ </dict>
+ <key>ID</key>
+ <integer>169432</integer>
+ <key>Layer</key>
+ <integer>0</integer>
+ <key>Points</key>
+ <array>
+ <string>{155.00000254313238, 482.12574895537085}</string>
+ <string>{238.52297468463752, 508.35839132916635}</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>169370</integer>
+ <key>Position</key>
+ <real>0.5668826699256897</real>
+ </dict>
+ </dict>
+ <dict>
+ <key>Class</key>
+ <string>Group</string>
+ <key>Graphics</key>
+ <array>
+ <dict>
+ <key>Bounds</key>
+ <string>{{238.8333613077798, 284.99999999999994}, {105.66668701171875, 18.656048080136394}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>169425</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 authorization}</string>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{238.75000762939453, 412.15071036499205}, {105.66666412353516, 18.656048080136394}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>169426</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>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 decorators egress}</string>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{238.75000762939453, 303.65604172230951}, {105.66666412353516, 18.656048080136394}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>169427</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 decorators ingress}</string>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{238.75000762939453, 393.55704269887212}, {105.66666412353516, 18.656048080136394}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>169428</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.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 response adapter}</string>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{238.75000762939453, 374.90099016834085}, {105.66666412353516, 18.656048080136394}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>169429</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.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 view mapper egress}</string>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{238.75000762939453, 341.36561209044055}, {105.66666412353516, 33.089282989501953}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>169430</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.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>
+ </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>
+ <dict>
+ <key>Bounds</key>
+ <string>{{238.75000762939453, 322.26348241170439}, {105.66666412353516, 18.656048080136394}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>169431</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.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 view mapper ingress}</string>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ </dict>
+ </array>
+ <key>ID</key>
+ <integer>169424</integer>
+ <key>Layer</key>
+ <integer>0</integer>
+ </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>169422</integer>
+ <key>Info</key>
+ <integer>4</integer>
+ </dict>
+ <key>ID</key>
+ <integer>169423</integer>
+ <key>Layer</key>
+ <integer>0</integer>
+ <key>Points</key>
+ <array>
+ <string>{155.00000254313238, 470.25295298442387}</string>
+ <string>{238.33861159880226, 482.4262543949045}</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>169370</integer>
+ <key>Position</key>
+ <real>0.42701038718223572</real>
+ </dict>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{238.83336130777977, 471.22620192028251}, {105.66666412353516, 22.544641494750977}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>169422</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 NewResponse}</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>169420</integer>
+ <key>Info</key>
+ <integer>4</integer>
+ </dict>
+ <key>ID</key>
+ <integer>169421</integer>
+ <key>Layer</key>
+ <integer>0</integer>
+ <key>Points</key>
+ <array>
+ <string>{154.99998733539806, 128.68025330008533}</string>
+ <string>{239.83340199788393, 128.59152244387357}</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>169386</integer>
+ <key>Position</key>
+ <real>0.35945424437522888</real>
+ </dict>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{239.83340199788395, 117.31920169649808}, {105.66666412353516, 22.544641494750977}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>169420</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 NewRequest}</string>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Class</key>
+ <string>TableGroup</string>
+ <key>Graphics</key>
+ <array>
+ <dict>
+ <key>Bounds</key>
+ <string>{{102.1666056315114, 148.28868579864499}, {105.66669464111328, 33.08929443359375}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>169418</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.815377</string>
+ <key>g</key>
+ <string>1</string>
+ <key>r</key>
+ <string>0.820561</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 URL dispatch}</string>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{102.1666056315114, 181.37798023223874}, {105.66669464111328, 17.244049072265625}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>169419</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.815377</string>
+ <key>g</key>
+ <string>1</string>
+ <key>r</key>
+ <string>0.820561</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 route predicates}</string>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ </dict>
+ </array>
+ <key>GridH</key>
+ <array>
+ <integer>169418</integer>
+ <integer>169419</integer>
+ <array/>
+ </array>
+ <key>ID</key>
+ <integer>169417</integer>
+ <key>Layer</key>
+ <integer>0</integer>
+ </dict>
+ <dict>
+ <key>Class</key>
+ <string>TableGroup</string>
+ <key>Graphics</key>
+ <array>
+ <dict>
+ <key>Bounds</key>
+ <string>{{102.16666158040482, 272}, {105.66666412353516, 33.08929443359375}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>169412</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.815377</string>
+ <key>g</key>
+ <string>1</string>
+ <key>r</key>
+ <string>0.820561</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 view lookup}</string>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{102.16666158040482, 305.08929443359375}, {105.66666412353516, 17.244049072265625}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>169413</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.815377</string>
+ <key>g</key>
+ <string>1</string>
+ <key>r</key>
+ <string>0.820561</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 predicates}</string>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ </dict>
+ </array>
+ <key>GridH</key>
+ <array>
+ <integer>169412</integer>
+ <integer>169413</integer>
+ <array/>
+ </array>
+ <key>ID</key>
+ <integer>169411</integer>
+ <key>Layer</key>
+ <integer>0</integer>
+ </dict>
+ <dict>
+ <key>Class</key>
+ <string>LineGraphic</string>
+ <key>Head</key>
+ <dict>
+ <key>ID</key>
+ <integer>169407</integer>
+ <key>Info</key>
+ <integer>7</integer>
+ </dict>
+ <key>ID</key>
+ <integer>169410</integer>
+ <key>Layer</key>
+ <integer>0</integer>
+ <key>Points</key>
+ <array>
+ <string>{238.75000762939462, 430.80675844512831}</string>
+ <string>{207.66666666666765, 385.656005859375}</string>
+ </array>
+ <key>Style</key>
+ <dict>
+ <key>stroke</key>
+ <dict>
+ <key>Color</key>
+ <dict>
+ <key>b</key>
+ <string>0.755269</string>
+ <key>g</key>
+ <string>0.755239</string>
+ <key>r</key>
+ <string>0.75529</string>
+ </dict>
+ <key>HeadArrow</key>
+ <string>0</string>
+ <key>Legacy</key>
+ <true/>
+ <key>Pattern</key>
+ <integer>11</integer>
+ <key>TailArrow</key>
+ <string>0</string>
+ </dict>
+ </dict>
+ <key>Tail</key>
+ <dict>
+ <key>ID</key>
+ <integer>169426</integer>
+ <key>Info</key>
+ <integer>6</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Class</key>
+ <string>LineGraphic</string>
+ <key>Head</key>
+ <dict>
+ <key>ID</key>
+ <integer>169407</integer>
+ <key>Info</key>
+ <integer>8</integer>
+ </dict>
+ <key>ID</key>
+ <integer>169409</integer>
+ <key>Layer</key>
+ <integer>0</integer>
+ <key>Points</key>
+ <array>
+ <string>{239.33336141608385, 285.57837549845181}</string>
+ <string>{207.66666666666777, 353.07514659563753}</string>
+ </array>
+ <key>Style</key>
+ <dict>
+ <key>stroke</key>
+ <dict>
+ <key>Color</key>
+ <dict>
+ <key>b</key>
+ <string>0.755269</string>
+ <key>g</key>
+ <string>0.755239</string>
+ <key>r</key>
+ <string>0.75529</string>
+ </dict>
+ <key>HeadArrow</key>
+ <string>0</string>
+ <key>Legacy</key>
+ <true/>
+ <key>Pattern</key>
+ <integer>11</integer>
+ <key>TailArrow</key>
+ <string>0</string>
+ </dict>
+ </dict>
+ <key>Tail</key>
+ <dict>
+ <key>ID</key>
+ <integer>169425</integer>
+ <key>Info</key>
+ <integer>6</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Class</key>
+ <string>LineGraphic</string>
+ <key>ControlPoints</key>
+ <array>
+ <string>{0, 6.9840087890625}</string>
+ <string>{0, -8.9999999999999432}</string>
+ </array>
+ <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>169381</integer>
+ </dict>
+ <key>ID</key>
+ <integer>169408</integer>
+ <key>Layer</key>
+ <integer>0</integer>
+ <key>Points</key>
+ <array>
+ <string>{155.00000254313238, 386.66442959065108}</string>
+ <string>{155.00000254313238, 422.21209462483216}</string>
+ </array>
+ <key>Style</key>
+ <dict>
+ <key>stroke</key>
+ <dict>
+ <key>Bezier</key>
+ <true/>
+ <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>SharpArrow</string>
+ <key>Legacy</key>
+ <true/>
+ <key>LineType</key>
+ <integer>1</integer>
+ <key>TailArrow</key>
+ <string>0</string>
+ </dict>
+ </dict>
+ <key>Tail</key>
+ <dict>
+ <key>ID</key>
+ <integer>169407</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{102.16667048136482, 353.07514659563753}, {105.66666412353516, 33.089282989501953}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>169407</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.49211360058019871, -0.49251945318722434}</string>
+ <string>{-0.49211360058019871, 0.49470854679786669}</string>
+ <string>{0.4984227008620481, 0.48463479169597612}</string>
+ <string>{0.49842270086204898, -0.5}</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.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>
+ </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 pipeline}</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>169380</integer>
+ <key>Info</key>
+ <integer>4</integer>
+ </dict>
+ <key>ID</key>
+ <integer>169399</integer>
+ <key>Layer</key>
+ <integer>0</integer>
+ <key>Points</key>
+ <array>
+ <string>{154.9999936421724, 258.44082431579938}</string>
+ <string>{238.8333613077798, 258.45536063967575}</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>169372</integer>
+ <key>Position</key>
+ <real>0.51973581314086914</real>
+ </dict>
+ </dict>
+ <dict>
+ <key>Class</key>
+ <string>Group</string>
+ <key>Graphics</key>
+ <array>
+ <dict>
+ <key>Bounds</key>
+ <string>{{383.66662216186666, 130.51770718892479}, {105.66666412353516, 22.544641494750977}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>169393</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.815377</string>
+ <key>g</key>
+ <string>1</string>
+ <key>r</key>
+ <string>0.820561</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 internal process}</string>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{383.66662216186666, 91.940789540609359}, {105.66666412353516, 33.089282989501953}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>169394</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.999208</string>
+ <key>g</key>
+ <string>0.811343</string>
+ <key>r</key>
+ <string>0.644457</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 external process (middleware, tween)}</string>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{383.66662216186666, 158.54998334248924}, {105.66666412353516, 22.544641494750977}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>169395</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.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>
+ </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>
+ <dict>
+ <key>Bounds</key>
+ <string>{{383.66662216186666, 186.58225949605369}, {105.66666412353516, 22.544641494750977}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>169396</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.327428</string>
+ <key>g</key>
+ <string>0.81823</string>
+ <key>r</key>
+ <string>0.995566</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 callback}</string>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{383.66662216186666, 63.908513387045019}, {105.66666412353516, 22.544641494750977}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>169397</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 event}</string>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{370.9999504089372, 42.910746256511771}, {132.66667175292969, 184.08924865722656}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>169398</integer>
+ <key>Magnets</key>
+ <array>
+ <string>{1, 0.5}</string>
+ <string>{1, -0.5}</string>
+ <string>{-1, 0.5}</string>
+ <string>{-1, -0.5}</string>
+ <string>{0.5, 1}</string>
+ <string>{-0.5, 1}</string>
+ <string>{0.5, -1}</string>
+ <string>{-0.5, -1}</string>
+ </array>
+ <key>Shape</key>
+ <string>Rectangle</string>
+ <key>Style</key>
+ <dict>
+ <key>fill</key>
+ <dict>
+ <key>Draws</key>
+ <string>NO</string>
+ </dict>
+ <key>shadow</key>
+ <dict>
+ <key>Draws</key>
+ <string>NO</string>
+ </dict>
+ <key>stroke</key>
+ <dict>
+ <key>CornerRadius</key>
+ <real>5</real>
+ </dict>
+ </dict>
+ <key>Text</key>
+ <dict>
+ <key>Align</key>
+ <integer>0</integer>
+ <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
+
+\f0\b\fs20 \cf0 Legend}</string>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ <key>TextPlacement</key>
+ <integer>0</integer>
+ </dict>
+ </array>
+ <key>ID</key>
+ <integer>169391</integer>
+ <key>Layer</key>
+ <integer>0</integer>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{233.5000012715667, 20.000000000000934}, {116, 14}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>FitText</key>
+ <string>YES</string>
+ <key>Flow</key>
+ <string>Resize</string>
+ <key>FontInfo</key>
+ <dict>
+ <key>Font</key>
+ <string>Helvetica</string>
+ <key>Size</key>
+ <real>12</real>
+ </dict>
+ <key>ID</key>
+ <integer>169390</integer>
+ <key>Layer</key>
+ <integer>0</integer>
+ <key>Shape</key>
+ <string>Rectangle</string>
+ <key>Style</key>
+ <dict>
+ <key>fill</key>
+ <dict>
+ <key>Draws</key>
+ <string>NO</string>
+ </dict>
+ <key>shadow</key>
+ <dict>
+ <key>Draws</key>
+ <string>NO</string>
+ </dict>
+ <key>stroke</key>
+ <dict>
+ <key>Draws</key>
+ <string>NO</string>
+ </dict>
+ </dict>
+ <key>Text</key>
+ <dict>
+ <key>Pad</key>
+ <integer>0</integer>
+ <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\pardirnatural\qc
+
+\f0\b\fs24 \cf0 &lt;%Canvas%&gt;}</string>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ <key>Wrap</key>
+ <string>NO</string>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{375.5, 391}, {105.66666412353516, 22.544642175946908}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>169389</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 BeforeRender}</string>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Class</key>
+ <string>LineGraphic</string>
+ <key>ControlPoints</key>
+ <array>
+ <string>{0, 7.05596923828125}</string>
+ <string>{0, -9}</string>
+ </array>
+ <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>169418</integer>
+ <key>Info</key>
+ <integer>2</integer>
+ </dict>
+ <key>ID</key>
+ <integer>169386</integer>
+ <key>Layer</key>
+ <integer>0</integer>
+ <key>Points</key>
+ <array>
+ <string>{155.00000170434049, 119.22767858295661}</string>
+ <string>{154.99995295206804, 148.28868579864499}</string>
+ </array>
+ <key>Style</key>
+ <dict>
+ <key>stroke</key>
+ <dict>
+ <key>Bezier</key>
+ <true/>
+ <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>SharpArrow</string>
+ <key>Legacy</key>
+ <true/>
+ <key>LineType</key>
+ <integer>1</integer>
+ <key>TailArrow</key>
+ <string>0</string>
+ </dict>
+ </dict>
+ <key>Tail</key>
+ <dict>
+ <key>ID</key>
+ <integer>169378</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Class</key>
+ <string>LineGraphic</string>
+ <key>ControlPoints</key>
+ <array>
+ <string>{0, 6.9840087890625}</string>
+ <string>{0, -9}</string>
+ </array>
+ <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>169378</integer>
+ </dict>
+ <key>ID</key>
+ <integer>169385</integer>
+ <key>Layer</key>
+ <integer>0</integer>
+ <key>Points</key>
+ <array>
+ <string>{155.00000254313238, 67.727678571434836}</string>
+ <string>{155.00000254313238, 96.18303707668386}</string>
+ </array>
+ <key>Style</key>
+ <dict>
+ <key>stroke</key>
+ <dict>
+ <key>Bezier</key>
+ <true/>
+ <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>SharpArrow</string>
+ <key>Legacy</key>
+ <true/>
+ <key>LineType</key>
+ <integer>1</integer>
+ <key>TailArrow</key>
+ <string>0</string>
+ </dict>
+ </dict>
+ <key>Tail</key>
+ <dict>
+ <key>ID</key>
+ <integer>169377</integer>
+ <key>Info</key>
+ <integer>1</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{102.16667048136482, 509.6179466247504}, {105.66666412353516, 22.544641494750977}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>169384</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.999208</string>
+ <key>g</key>
+ <string>0.811343</string>
+ <key>r</key>
+ <string>0.644457</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 middleware egress}</string>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{239, 497.23589324949899}, {105.66666412353516, 22.544641494750977}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>169383</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.327428</string>
+ <key>g</key>
+ <string>0.81823</string>
+ <key>r</key>
+ <string>0.995566</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 finished callbacks}</string>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{239, 445.23589324949717}, {105.66666412353516, 22.544641494750977}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>169382</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.327428</string>
+ <key>g</key>
+ <string>0.81823</string>
+ <key>r</key>
+ <string>0.995566</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 response callbacks}</string>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{102.16667048136482, 422.21209462483216}, {105.66666412353516, 22.544641494750977}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>169381</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.999208</string>
+ <key>g</key>
+ <string>0.811343</string>
+ <key>r</key>
+ <string>0.644457</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 tween egress}</string>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{238.83336130777977, 247.18303989230026}, {105.66666412353516, 22.544641494750977}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>169380</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 ContextFound}</string>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{102.16667048136482, 222.18303707668389}, {105.66666412353516, 22.544641494750977}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>169379</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.815377</string>
+ <key>g</key>
+ <string>1</string>
+ <key>r</key>
+ <string>0.820561</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 traversal}</string>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{102.16667048136482, 96.18303707668386}, {105.66666412353516, 22.544641494750977}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>169378</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.999208</string>
+ <key>g</key>
+ <string>0.811343</string>
+ <key>r</key>
+ <string>0.644457</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 tween ingress}</string>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{102.16667048136482, 45.18303707668386}, {105.66666412353516, 22.544641494750977}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>169377</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.999208</string>
+ <key>g</key>
+ <string>0.811343</string>
+ <key>r</key>
+ <string>0.644457</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 middleware ingress }</string>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Class</key>
+ <string>LineGraphic</string>
+ <key>ControlPoints</key>
+ <array>
+ <string>{0, 6.9840087890625}</string>
+ <string>{0, -9}</string>
+ </array>
+ <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>169379</integer>
+ </dict>
+ <key>ID</key>
+ <integer>169373</integer>
+ <key>Layer</key>
+ <integer>0</integer>
+ <key>Points</key>
+ <array>
+ <string>{154.99995295206804, 198.62202930450437}</string>
+ <string>{155.00000254313238, 222.18303707668389}</string>
+ </array>
+ <key>Style</key>
+ <dict>
+ <key>stroke</key>
+ <dict>
+ <key>Bezier</key>
+ <true/>
+ <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>SharpArrow</string>
+ <key>Legacy</key>
+ <true/>
+ <key>LineType</key>
+ <integer>1</integer>
+ <key>TailArrow</key>
+ <string>0</string>
+ </dict>
+ </dict>
+ <key>Tail</key>
+ <dict>
+ <key>ID</key>
+ <integer>169419</integer>
+ <key>Info</key>
+ <integer>1</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Class</key>
+ <string>LineGraphic</string>
+ <key>ControlPoints</key>
+ <array>
+ <string>{0, 7.05596923828125}</string>
+ <string>{0, -9}</string>
+ </array>
+ <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>169412</integer>
+ <key>Info</key>
+ <integer>2</integer>
+ </dict>
+ <key>ID</key>
+ <integer>169372</integer>
+ <key>Layer</key>
+ <integer>0</integer>
+ <key>Points</key>
+ <array>
+ <string>{154.9999936421724, 245.22767856643924}</string>
+ <string>{154.9999936421724, 272}</string>
+ </array>
+ <key>Style</key>
+ <dict>
+ <key>stroke</key>
+ <dict>
+ <key>Bezier</key>
+ <true/>
+ <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>SharpArrow</string>
+ <key>Legacy</key>
+ <true/>
+ <key>LineType</key>
+ <integer>1</integer>
+ <key>TailArrow</key>
+ <string>0</string>
+ </dict>
+ </dict>
+ <key>Tail</key>
+ <dict>
+ <key>ID</key>
+ <integer>169379</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Class</key>
+ <string>LineGraphic</string>
+ <key>ControlPoints</key>
+ <array>
+ <string>{0, 6.9840087890625}</string>
+ <string>{0, -8.9999999999999432}</string>
+ </array>
+ <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>169407</integer>
+ </dict>
+ <key>ID</key>
+ <integer>169371</integer>
+ <key>Layer</key>
+ <integer>0</integer>
+ <key>Points</key>
+ <array>
+ <string>{154.9999936421724, 322.33334350585938}</string>
+ <string>{155.00000254313238, 353.07514659563753}</string>
+ </array>
+ <key>Style</key>
+ <dict>
+ <key>stroke</key>
+ <dict>
+ <key>Bezier</key>
+ <true/>
+ <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>SharpArrow</string>
+ <key>Legacy</key>
+ <true/>
+ <key>LineType</key>
+ <integer>1</integer>
+ <key>TailArrow</key>
+ <string>0</string>
+ </dict>
+ </dict>
+ <key>Tail</key>
+ <dict>
+ <key>ID</key>
+ <integer>169413</integer>
+ <key>Info</key>
+ <integer>1</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Class</key>
+ <string>LineGraphic</string>
+ <key>ControlPoints</key>
+ <array>
+ <string>{0, 6.9839935302734375}</string>
+ <string>{0, -9}</string>
+ </array>
+ <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>169384</integer>
+ <key>Info</key>
+ <integer>2</integer>
+ </dict>
+ <key>ID</key>
+ <integer>169370</integer>
+ <key>Layer</key>
+ <integer>0</integer>
+ <key>Points</key>
+ <array>
+ <string>{155.00000254313238, 444.75673611958314}</string>
+ <string>{155.00000254313238, 509.6179466247504}</string>
+ </array>
+ <key>Style</key>
+ <dict>
+ <key>stroke</key>
+ <dict>
+ <key>Bezier</key>
+ <true/>
+ <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>SharpArrow</string>
+ <key>Legacy</key>
+ <true/>
+ <key>LineType</key>
+ <integer>1</integer>
+ <key>TailArrow</key>
+ <string>0</string>
+ </dict>
+ </dict>
+ <key>Tail</key>
+ <dict>
+ <key>ID</key>
+ <integer>169381</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Class</key>
+ <string>LineGraphic</string>
+ <key>Head</key>
+ <dict>
+ <key>ID</key>
+ <integer>169444</integer>
+ <key>Info</key>
+ <integer>6</integer>
+ </dict>
+ <key>ID</key>
+ <integer>169503</integer>
+ <key>Layer</key>
+ <integer>1</integer>
+ <key>Points</key>
+ <array>
+ <string>{272.4166717529298, 537.32234122436705}</string>
+ <string>{420.4999504089364, 515.08928491955714}</string>
+ </array>
+ <key>Style</key>
+ <dict>
+ <key>stroke</key>
+ <dict>
+ <key>Color</key>
+ <dict>
+ <key>b</key>
+ <string>0.755269</string>
+ <key>g</key>
+ <string>0.755239</string>
+ <key>r</key>
+ <string>0.75529</string>
+ </dict>
+ <key>HeadArrow</key>
+ <string>0</string>
+ <key>Legacy</key>
+ <true/>
+ <key>Pattern</key>
+ <integer>11</integer>
+ <key>TailArrow</key>
+ <string>0</string>
+ </dict>
+ </dict>
+ <key>Tail</key>
+ <dict>
+ <key>ID</key>
+ <integer>169494</integer>
+ <key>Info</key>
+ <integer>5</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Class</key>
+ <string>LineGraphic</string>
+ <key>Head</key>
+ <dict>
+ <key>ID</key>
+ <integer>169444</integer>
+ </dict>
+ <key>ID</key>
+ <integer>169502</integer>
+ <key>Layer</key>
+ <integer>1</integer>
+ <key>Points</key>
+ <array>
+ <string>{272.50004831949906, 391.51558277923863}</string>
+ <string>{420.4999504089364, 472.78869058972316}</string>
+ </array>
+ <key>Style</key>
+ <dict>
+ <key>stroke</key>
+ <dict>
+ <key>Color</key>
+ <dict>
+ <key>b</key>
+ <string>0.755269</string>
+ <key>g</key>
+ <string>0.755239</string>
+ <key>r</key>
+ <string>0.75529</string>
+ </dict>
+ <key>HeadArrow</key>
+ <string>0</string>
+ <key>Legacy</key>
+ <true/>
+ <key>Pattern</key>
+ <integer>11</integer>
+ <key>TailArrow</key>
+ <string>0</string>
+ </dict>
+ </dict>
+ <key>Tail</key>
+ <dict>
+ <key>ID</key>
+ <integer>169493</integer>
+ <key>Info</key>
+ <integer>5</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>169450</integer>
+ </dict>
+ <key>ID</key>
+ <integer>169501</integer>
+ <key>Layer</key>
+ <integer>1</integer>
+ <key>Points</key>
+ <array>
+ <string>{83.000002543132396, 592.81693102013151}</string>
+ <string>{239, 583.78422005970799}</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>169438</integer>
+ <key>Position</key>
+ <real>0.28820157051086426</real>
+ </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>169451</integer>
+ </dict>
+ <key>ID</key>
+ <integer>169500</integer>
+ <key>Layer</key>
+ <integer>1</integer>
+ <key>Points</key>
+ <array>
+ <string>{83.000002543132396, 629.80996162681686}</string>
+ <string>{239, 640.78422005970981}</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>169438</integer>
+ <key>Position</key>
+ <real>0.5668826699256897</real>
+ </dict>
+ </dict>
+ <dict>
+ <key>Class</key>
+ <string>Group</string>
+ <key>Graphics</key>
+ <array>
+ <dict>
+ <key>Bounds</key>
+ <string>{{166.8333613077798, 391.51558277923863}, {105.66668701171875, 18.656048080136394}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>169493</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 authorization}</string>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{166.75000762939453, 518.66629314423074}, {105.66666412353516, 18.656048080136394}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>169494</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>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 decorators egress}</string>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{166.75000762939453, 410.17162450154819}, {105.66666412353516, 18.656048080136394}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>169495</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 decorators ingress}</string>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{166.75000762939453, 500.07262547811081}, {105.66666412353516, 18.656048080136394}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>169496</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.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 response adapter}</string>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{166.75000762939453, 481.41657294757954}, {105.66666412353516, 18.656048080136394}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>169497</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.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 view mapper egress}</string>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{166.75000762939453, 447.88119486967923}, {105.66666412353516, 33.089282989501953}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>169498</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.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>
+ </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>
+ <dict>
+ <key>Bounds</key>
+ <string>{{166.75000762939453, 428.77906519094307}, {105.66666412353516, 18.656048080136394}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>169499</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.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 view mapper ingress}</string>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ </dict>
+ </array>
+ <key>ID</key>
+ <integer>169492</integer>
+ <key>Layer</key>
+ <integer>1</integer>
+ </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>169490</integer>
+ <key>Info</key>
+ <integer>4</integer>
+ </dict>
+ <key>ID</key>
+ <integer>169491</integer>
+ <key>Layer</key>
+ <integer>1</integer>
+ <key>Points</key>
+ <array>
+ <string>{83.166643778483959, 611.77452873049333}</string>
+ <string>{238.8333613077798, 611.77452873049333}</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>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{238.83336130777977, 600.50220798311784}, {105.66666412353516, 22.544641494750977}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>169490</integer>
+ <key>Layer</key>
+ <integer>1</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 NewResponse}</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>169488</integer>
+ <key>Info</key>
+ <integer>4</integer>
+ </dict>
+ <key>ID</key>
+ <integer>169489</integer>
+ <key>Layer</key>
+ <integer>1</integer>
+ <key>Points</key>
+ <array>
+ <string>{82.999986314263907, 140.3328574622312}</string>
+ <string>{239.83340199788393, 141.59152244387357}</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>169454</integer>
+ <key>Position</key>
+ <real>0.35945424437522888</real>
+ </dict>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{239.83340199788395, 130.31920169649808}, {105.66666412353516, 22.544641494750977}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>169488</integer>
+ <key>Layer</key>
+ <integer>1</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 NewRequest}</string>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Class</key>
+ <string>TableGroup</string>
+ <key>Graphics</key>
+ <array>
+ <dict>
+ <key>Bounds</key>
+ <string>{{30.166605631511416, 166.28868579864499}, {105.66668701171875, 33.08929443359375}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>169486</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.815377</string>
+ <key>g</key>
+ <string>1</string>
+ <key>r</key>
+ <string>0.820561</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 URL dispatch}</string>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{30.166605631511416, 199.37798023223874}, {105.66668701171875, 17.244049072265625}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>169487</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.815377</string>
+ <key>g</key>
+ <string>1</string>
+ <key>r</key>
+ <string>0.820561</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 route predicates}</string>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ </dict>
+ </array>
+ <key>GridH</key>
+ <array>
+ <integer>169486</integer>
+ <integer>169487</integer>
+ <array/>
+ </array>
+ <key>ID</key>
+ <integer>169485</integer>
+ <key>Layer</key>
+ <integer>1</integer>
+ </dict>
+ <dict>
+ <key>Class</key>
+ <string>TableGroup</string>
+ <key>Graphics</key>
+ <array>
+ <dict>
+ <key>Bounds</key>
+ <string>{{420.5000406901047, 338.15028762817326}, {105.66668701171875, 33.08929443359375}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>169483</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.815377</string>
+ <key>g</key>
+ <string>1</string>
+ <key>r</key>
+ <string>0.820561</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 view lookup}</string>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{420.5000406901047, 371.23958206176701}, {105.66668701171875, 17.244049072265625}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>169484</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.815377</string>
+ <key>g</key>
+ <string>1</string>
+ <key>r</key>
+ <string>0.820561</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 predicates}</string>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ </dict>
+ </array>
+ <key>GridH</key>
+ <array>
+ <integer>169483</integer>
+ <integer>169484</integer>
+ <array/>
+ </array>
+ <key>ID</key>
+ <integer>169482</integer>
+ <key>Layer</key>
+ <integer>1</integer>
+ </dict>
+ <dict>
+ <key>Class</key>
+ <string>TableGroup</string>
+ <key>Graphics</key>
+ <array>
+ <dict>
+ <key>Bounds</key>
+ <string>{{30.166661580404835, 335}, {105.66667175292969, 33.08929443359375}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>169480</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.815377</string>
+ <key>g</key>
+ <string>1</string>
+ <key>r</key>
+ <string>0.820561</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 view lookup}</string>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{30.166661580404835, 368.08929443359375}, {105.66667175292969, 17.244049072265625}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>169481</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.815377</string>
+ <key>g</key>
+ <string>1</string>
+ <key>r</key>
+ <string>0.820561</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 predicates}</string>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ </dict>
+ </array>
+ <key>GridH</key>
+ <array>
+ <integer>169480</integer>
+ <integer>169481</integer>
+ <array/>
+ </array>
+ <key>ID</key>
+ <integer>169479</integer>
+ <key>Layer</key>
+ <integer>1</integer>
+ </dict>
+ <dict>
+ <key>Class</key>
+ <string>LineGraphic</string>
+ <key>Head</key>
+ <dict>
+ <key>ID</key>
+ <integer>169475</integer>
+ <key>Info</key>
+ <integer>7</integer>
+ </dict>
+ <key>ID</key>
+ <integer>169478</integer>
+ <key>Layer</key>
+ <integer>1</integer>
+ <key>Points</key>
+ <array>
+ <string>{166.75000762939462, 537.32234122436694}</string>
+ <string>{135.66666666666765, 485}</string>
+ </array>
+ <key>Style</key>
+ <dict>
+ <key>stroke</key>
+ <dict>
+ <key>Color</key>
+ <dict>
+ <key>b</key>
+ <string>0.755269</string>
+ <key>g</key>
+ <string>0.755239</string>
+ <key>r</key>
+ <string>0.75529</string>
+ </dict>
+ <key>HeadArrow</key>
+ <string>0</string>
+ <key>Legacy</key>
+ <true/>
+ <key>Pattern</key>
+ <integer>11</integer>
+ <key>TailArrow</key>
+ <string>0</string>
+ </dict>
+ </dict>
+ <key>Tail</key>
+ <dict>
+ <key>ID</key>
+ <integer>169494</integer>
+ <key>Info</key>
+ <integer>6</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Class</key>
+ <string>LineGraphic</string>
+ <key>Head</key>
+ <dict>
+ <key>ID</key>
+ <integer>169475</integer>
+ <key>Info</key>
+ <integer>8</integer>
+ </dict>
+ <key>ID</key>
+ <integer>169477</integer>
+ <key>Layer</key>
+ <integer>1</integer>
+ <key>Points</key>
+ <array>
+ <string>{167.33336141608385, 392.09395827769049}</string>
+ <string>{135.66666666666777, 452.41914073626253}</string>
+ </array>
+ <key>Style</key>
+ <dict>
+ <key>stroke</key>
+ <dict>
+ <key>Color</key>
+ <dict>
+ <key>b</key>
+ <string>0.755269</string>
+ <key>g</key>
+ <string>0.755239</string>
+ <key>r</key>
+ <string>0.75529</string>
+ </dict>
+ <key>HeadArrow</key>
+ <string>0</string>
+ <key>Legacy</key>
+ <true/>
+ <key>Pattern</key>
+ <integer>11</integer>
+ <key>TailArrow</key>
+ <string>0</string>
+ </dict>
+ </dict>
+ <key>Tail</key>
+ <dict>
+ <key>ID</key>
+ <integer>169493</integer>
+ <key>Info</key>
+ <integer>6</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Class</key>
+ <string>LineGraphic</string>
+ <key>ControlPoints</key>
+ <array>
+ <string>{0, 6.9840087890625}</string>
+ <string>{0, -8.9999999999999432}</string>
+ </array>
+ <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>169449</integer>
+ </dict>
+ <key>ID</key>
+ <integer>169476</integer>
+ <key>Layer</key>
+ <integer>1</integer>
+ <key>Points</key>
+ <array>
+ <string>{83.000002543132396, 485.50842372576449}</string>
+ <string>{83.000002543132396, 548.10604731241608}</string>
+ </array>
+ <key>Style</key>
+ <dict>
+ <key>stroke</key>
+ <dict>
+ <key>Bezier</key>
+ <true/>
+ <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>SharpArrow</string>
+ <key>Legacy</key>
+ <true/>
+ <key>LineType</key>
+ <integer>1</integer>
+ <key>TailArrow</key>
+ <string>0</string>
+ </dict>
+ </dict>
+ <key>Tail</key>
+ <dict>
+ <key>ID</key>
+ <integer>169475</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{30.166670481364818, 452.41914073626253}, {105.66666412353516, 33.089282989501953}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>169475</integer>
+ <key>Layer</key>
+ <integer>1</integer>
+ <key>Magnets</key>
+ <array>
+ <string>{0, 1}</string>
+ <string>{0, -1}</string>
+ <string>{1, 0}</string>
+ <string>{-1, 0}</string>
+ <string>{-0.49211360058019871, -0.49251945318722434}</string>
+ <string>{-0.49211360058019871, 0.49470854679786669}</string>
+ <string>{0.4984227008620481, 0.48463479169597612}</string>
+ <string>{0.49842270086204898, -0.5}</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.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>
+ </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 pipeline}</string>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Class</key>
+ <string>LineGraphic</string>
+ <key>ControlPoints</key>
+ <array>
+ <string>{51.333333333333314, 0}</string>
+ <string>{-0.66666666666662877, 58.666666666666686}</string>
+ <string>{0.66673293066804717, -58.666850540458825}</string>
+ <string>{-16.306719354194399, 0.26652623861849634}</string>
+ </array>
+ <key>Head</key>
+ <dict>
+ <key>ID</key>
+ <integer>169443</integer>
+ <key>Info</key>
+ <integer>4</integer>
+ </dict>
+ <key>ID</key>
+ <integer>169474</integer>
+ <key>Layer</key>
+ <integer>1</integer>
+ <key>Points</key>
+ <array>
+ <string>{369.66666666666669, 541}</string>
+ <string>{404.00000000000023, 362}</string>
+ <string>{420.36749776329049, 302.42112495959606}</string>
+ </array>
+ <key>Style</key>
+ <dict>
+ <key>stroke</key>
+ <dict>
+ <key>Bezier</key>
+ <true/>
+ <key>Color</key>
+ <dict>
+ <key>b</key>
+ <string>0.75663</string>
+ <key>g</key>
+ <string>0.756618</string>
+ <key>r</key>
+ <string>0.75664</string>
+ </dict>
+ <key>HeadArrow</key>
+ <string>0</string>
+ <key>Legacy</key>
+ <true/>
+ <key>LineType</key>
+ <integer>1</integer>
+ <key>Pattern</key>
+ <integer>1</integer>
+ <key>TailArrow</key>
+ <string>0</string>
+ </dict>
+ </dict>
+ </dict>
+ <dict>
+ <key>Class</key>
+ <string>LineGraphic</string>
+ <key>ControlPoints</key>
+ <array>
+ <string>{69.833333333332462, -0.72767857143483639}</string>
+ <string>{-0.66690523835279691, -51.044605218028948}</string>
+ <string>{0.66666666666674246, 51.044637362162291}</string>
+ <string>{-24.333271383961971, -0.13425428344572765}</string>
+ </array>
+ <key>Head</key>
+ <dict>
+ <key>ID</key>
+ <integer>169443</integer>
+ <key>Info</key>
+ <integer>4</integer>
+ </dict>
+ <key>ID</key>
+ <integer>169473</integer>
+ <key>Layer</key>
+ <integer>1</integer>
+ <key>Points</key>
+ <array>
+ <string>{310.66666666666754, 118.72767857143484}</string>
+ <string>{399.33333333333417, 216.62202930450439}</string>
+ <string>{420.37955338188368, 301.45369961823752}</string>
+ </array>
+ <key>Style</key>
+ <dict>
+ <key>stroke</key>
+ <dict>
+ <key>Bezier</key>
+ <true/>
+ <key>Color</key>
+ <dict>
+ <key>b</key>
+ <string>0.75663</string>
+ <key>g</key>
+ <string>0.756618</string>
+ <key>r</key>
+ <string>0.75664</string>
+ </dict>
+ <key>HeadArrow</key>
+ <string>0</string>
+ <key>Legacy</key>
+ <true/>
+ <key>LineType</key>
+ <integer>1</integer>
+ <key>Pattern</key>
+ <integer>1</integer>
+ <key>TailArrow</key>
+ <string>0</string>
+ </dict>
+ </dict>
+ </dict>
+ <dict>
+ <key>Class</key>
+ <string>LineGraphic</string>
+ <key>ControlPoints</key>
+ <array>
+ <string>{-3.9999491373696401, 78.910715080442856}</string>
+ <string>{92.666683130060392, 0.22547126950667007}</string>
+ </array>
+ <key>Head</key>
+ <dict>
+ <key>ID</key>
+ <integer>169490</integer>
+ <key>Info</key>
+ <integer>3</integer>
+ </dict>
+ <key>ID</key>
+ <integer>169472</integer>
+ <key>Layer</key>
+ <integer>1</integer>
+ <key>Points</key>
+ <array>
+ <string>{473.33328247070392, 515.08928491955714}</string>
+ <string>{344.50002543131501, 611.77452873049333}</string>
+ </array>
+ <key>Style</key>
+ <dict>
+ <key>stroke</key>
+ <dict>
+ <key>Bezier</key>
+ <true/>
+ <key>HeadArrow</key>
+ <string>SharpArrow</string>
+ <key>Legacy</key>
+ <true/>
+ <key>LineType</key>
+ <integer>1</integer>
+ <key>TailArrow</key>
+ <string>0</string>
+ </dict>
+ </dict>
+ <key>Tail</key>
+ <dict>
+ <key>ID</key>
+ <integer>169444</integer>
+ <key>Info</key>
+ <integer>1</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Class</key>
+ <string>LineGraphic</string>
+ <key>ControlPoints</key>
+ <array>
+ <string>{31.999987284342428, -14.081351280212308}</string>
+ <string>{-32.166667938232536, 10.244050343831077}</string>
+ </array>
+ <key>ID</key>
+ <integer>169471</integer>
+ <key>Layer</key>
+ <integer>1</integer>
+ <key>Points</key>
+ <array>
+ <string>{344.96346869509995, 346.26317428240412}</string>
+ <string>{389.8333346048999, 328.08928298950207}</string>
+ </array>
+ <key>Style</key>
+ <dict>
+ <key>stroke</key>
+ <dict>
+ <key>Bezier</key>
+ <true/>
+ <key>HeadArrow</key>
+ <string>SharpArrow</string>
+ <key>Legacy</key>
+ <true/>
+ <key>LineType</key>
+ <integer>1</integer>
+ <key>TailArrow</key>
+ <string>0</string>
+ </dict>
+ </dict>
+ <key>Tail</key>
+ <dict>
+ <key>ID</key>
+ <integer>169456</integer>
+ <key>Info</key>
+ <integer>3</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Class</key>
+ <string>LineGraphic</string>
+ <key>ControlPoints</key>
+ <array>
+ <string>{31.999987284342428, -14.081351280212308}</string>
+ <string>{-28.500001271565793, 8.3333333333333144}</string>
+ </array>
+ <key>ID</key>
+ <integer>169470</integer>
+ <key>Layer</key>
+ <integer>1</integer>
+ <key>Points</key>
+ <array>
+ <string>{344.98861594084059, 323.71068461220347}</string>
+ <string>{391.1666679382335, 313.6666666666664}</string>
+ </array>
+ <key>Style</key>
+ <dict>
+ <key>stroke</key>
+ <dict>
+ <key>Bezier</key>
+ <true/>
+ <key>HeadArrow</key>
+ <string>SharpArrow</string>
+ <key>Legacy</key>
+ <true/>
+ <key>LineType</key>
+ <integer>1</integer>
+ <key>TailArrow</key>
+ <string>0</string>
+ </dict>
+ </dict>
+ <key>Tail</key>
+ <dict>
+ <key>ID</key>
+ <integer>169455</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Class</key>
+ <string>LineGraphic</string>
+ <key>ControlPoints</key>
+ <array>
+ <string>{31.999987284342428, -14.081351280212308}</string>
+ <string>{-40, 1.5446373167492311}</string>
+ </array>
+ <key>ID</key>
+ <integer>169469</integer>
+ <key>Layer</key>
+ <integer>1</integer>
+ <key>Points</key>
+ <array>
+ <string>{344.9995533451783, 301.1612218744512}</string>
+ <string>{394.50000127156665, 299.00000000000045}</string>
+ </array>
+ <key>Style</key>
+ <dict>
+ <key>stroke</key>
+ <dict>
+ <key>Bezier</key>
+ <true/>
+ <key>HeadArrow</key>
+ <string>SharpArrow</string>
+ <key>Legacy</key>
+ <true/>
+ <key>LineType</key>
+ <integer>1</integer>
+ <key>TailArrow</key>
+ <string>0</string>
+ </dict>
+ </dict>
+ <key>Tail</key>
+ <dict>
+ <key>ID</key>
+ <integer>169442</integer>
+ <key>Info</key>
+ <integer>3</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Class</key>
+ <string>LineGraphic</string>
+ <key>ControlPoints</key>
+ <array>
+ <string>{8.5833282470703125, -10.244596987647753}</string>
+ <string>{0, 0}</string>
+ </array>
+ <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>169457</integer>
+ <key>Info</key>
+ <integer>4</integer>
+ </dict>
+ <key>ID</key>
+ <integer>169468</integer>
+ <key>Layer</key>
+ <integer>1</integer>
+ <key>Points</key>
+ <array>
+ <string>{272.41667175292969, 509.40064951817902}</string>
+ <string>{285.5, 503.81699882234847}</string>
+ </array>
+ <key>Style</key>
+ <dict>
+ <key>stroke</key>
+ <dict>
+ <key>Bezier</key>
+ <true/>
+ <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>LineType</key>
+ <integer>1</integer>
+ <key>Pattern</key>
+ <integer>1</integer>
+ <key>TailArrow</key>
+ <string>0</string>
+ </dict>
+ </dict>
+ <key>Tail</key>
+ <dict>
+ <key>ID</key>
+ <integer>169496</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>169448</integer>
+ <key>Info</key>
+ <integer>4</integer>
+ </dict>
+ <key>ID</key>
+ <integer>169467</integer>
+ <key>Layer</key>
+ <integer>1</integer>
+ <key>Points</key>
+ <array>
+ <string>{82.999997456869679, 300.27229985501288}</string>
+ <string>{238.34892458824362, 260.57913893040109}</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>169440</integer>
+ <key>Position</key>
+ <real>0.51973581314086914</real>
+ </dict>
+ </dict>
+ <dict>
+ <key>Class</key>
+ <string>Group</string>
+ <key>Graphics</key>
+ <array>
+ <dict>
+ <key>Bounds</key>
+ <string>{{419.66662216186666, 214.61452811107179}, {105.66666412353516, 22.544642175946908}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>169460</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.756045</string>
+ <key>g</key>
+ <string>0.75004</string>
+ <key>r</key>
+ <string>0.994455</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 exception}</string>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{419.66662216186666, 130.51770718892479}, {105.66666412353516, 22.544641494750977}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>169461</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.815377</string>
+ <key>g</key>
+ <string>1</string>
+ <key>r</key>
+ <string>0.820561</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 internal process}</string>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{419.66662216186666, 91.940789540609359}, {105.66666412353516, 33.089282989501953}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>169462</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.999208</string>
+ <key>g</key>
+ <string>0.811343</string>
+ <key>r</key>
+ <string>0.644457</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 external process (middleware, tween)}</string>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{419.66662216186666, 158.54998334248924}, {105.66666412353516, 22.544641494750977}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>169463</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.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>
+ </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>
+ <dict>
+ <key>Bounds</key>
+ <string>{{419.66662216186666, 186.58225949605369}, {105.66666412353516, 22.544641494750977}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>169464</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.327428</string>
+ <key>g</key>
+ <string>0.81823</string>
+ <key>r</key>
+ <string>0.995566</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 callback}</string>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{419.66662216186666, 63.908513387045019}, {105.66666412353516, 22.544641494750977}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>169465</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 event}</string>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{406.9999504089372, 42.910746256511771}, {132.66667175292969, 207.81692504882812}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>169466</integer>
+ <key>Magnets</key>
+ <array>
+ <string>{1, 0.5}</string>
+ <string>{1, -0.5}</string>
+ <string>{-1, 0.5}</string>
+ <string>{-1, -0.5}</string>
+ <string>{0.5, 1}</string>
+ <string>{-0.5, 1}</string>
+ <string>{0.5, -1}</string>
+ <string>{-0.5, -1}</string>
+ </array>
+ <key>Shape</key>
+ <string>Rectangle</string>
+ <key>Style</key>
+ <dict>
+ <key>fill</key>
+ <dict>
+ <key>Draws</key>
+ <string>NO</string>
+ </dict>
+ <key>shadow</key>
+ <dict>
+ <key>Draws</key>
+ <string>NO</string>
+ </dict>
+ <key>stroke</key>
+ <dict>
+ <key>CornerRadius</key>
+ <real>5</real>
+ </dict>
+ </dict>
+ <key>Text</key>
+ <dict>
+ <key>Align</key>
+ <integer>0</integer>
+ <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
+
+\f0\b\fs20 \cf0 Legend}</string>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ <key>TextPlacement</key>
+ <integer>0</integer>
+ </dict>
+ </array>
+ <key>ID</key>
+ <integer>169459</integer>
+ <key>Layer</key>
+ <integer>1</integer>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{233.5000012715667, 20.000000000000934}, {116, 14}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>FitText</key>
+ <string>YES</string>
+ <key>Flow</key>
+ <string>Resize</string>
+ <key>FontInfo</key>
+ <dict>
+ <key>Font</key>
+ <string>Helvetica</string>
+ <key>Size</key>
+ <real>12</real>
+ </dict>
+ <key>ID</key>
+ <integer>169458</integer>
+ <key>Layer</key>
+ <integer>1</integer>
+ <key>Shape</key>
+ <string>Rectangle</string>
+ <key>Style</key>
+ <dict>
+ <key>fill</key>
+ <dict>
+ <key>Draws</key>
+ <string>NO</string>
+ </dict>
+ <key>shadow</key>
+ <dict>
+ <key>Draws</key>
+ <string>NO</string>
+ </dict>
+ <key>stroke</key>
+ <dict>
+ <key>Draws</key>
+ <string>NO</string>
+ </dict>
+ </dict>
+ <key>Text</key>
+ <dict>
+ <key>Pad</key>
+ <integer>0</integer>
+ <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\pardirnatural\qc
+
+\f0\b\fs24 \cf0 &lt;%Canvas%&gt;}</string>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ <key>Wrap</key>
+ <string>NO</string>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{285.5, 492.544677734375}, {105.66666412353516, 22.544642175946908}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>169457</integer>
+ <key>Layer</key>
+ <integer>1</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 BeforeRender}</string>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{238.83337529500417, 335.17855853126167}, {105.66666412353516, 22.544642175946908}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>169456</integer>
+ <key>Layer</key>
+ <integer>1</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>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 HTTPForbidden}</string>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{238.83337529500417, 312.54463200342093}, {105.66666412353516, 22.544642175946908}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>169455</integer>
+ <key>Layer</key>
+ <integer>1</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>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 PredicateMismatch}</string>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Class</key>
+ <string>LineGraphic</string>
+ <key>ControlPoints</key>
+ <array>
+ <string>{0, 7.05596923828125}</string>
+ <string>{0, -9}</string>
+ </array>
+ <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>169486</integer>
+ <key>Info</key>
+ <integer>2</integer>
+ </dict>
+ <key>ID</key>
+ <integer>169454</integer>
+ <key>Layer</key>
+ <integer>1</integer>
+ <key>Points</key>
+ <array>
+ <string>{83.000001850648417, 128.2276785555359}</string>
+ <string>{82.999949137370791, 166.28868579864502}</string>
+ </array>
+ <key>Style</key>
+ <dict>
+ <key>stroke</key>
+ <dict>
+ <key>Bezier</key>
+ <true/>
+ <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>SharpArrow</string>
+ <key>Legacy</key>
+ <true/>
+ <key>LineType</key>
+ <integer>1</integer>
+ <key>TailArrow</key>
+ <string>0</string>
+ </dict>
+ </dict>
+ <key>Tail</key>
+ <dict>
+ <key>ID</key>
+ <integer>169446</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Class</key>
+ <string>LineGraphic</string>
+ <key>ControlPoints</key>
+ <array>
+ <string>{0, 6.9840087890625}</string>
+ <string>{0, -9}</string>
+ </array>
+ <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>169446</integer>
+ </dict>
+ <key>ID</key>
+ <integer>169453</integer>
+ <key>Layer</key>
+ <integer>1</integer>
+ <key>Points</key>
+ <array>
+ <string>{83.000002543132396, 67.727678571434836}</string>
+ <string>{83.000002543132396, 105.18303707668386}</string>
+ </array>
+ <key>Style</key>
+ <dict>
+ <key>stroke</key>
+ <dict>
+ <key>Bezier</key>
+ <true/>
+ <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>SharpArrow</string>
+ <key>Legacy</key>
+ <true/>
+ <key>LineType</key>
+ <integer>1</integer>
+ <key>TailArrow</key>
+ <string>0</string>
+ </dict>
+ </dict>
+ <key>Tail</key>
+ <dict>
+ <key>ID</key>
+ <integer>169445</integer>
+ <key>Info</key>
+ <integer>1</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{30.166670481364818, 671.51189931233432}, {105.66666412353516, 22.544641494750977}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>169452</integer>
+ <key>Layer</key>
+ <integer>1</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.999208</string>
+ <key>g</key>
+ <string>0.811343</string>
+ <key>r</key>
+ <string>0.644457</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 middleware egress}</string>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{239, 629.51189931233432}, {105.66666412353516, 22.544641494750977}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>169451</integer>
+ <key>Layer</key>
+ <integer>1</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.327428</string>
+ <key>g</key>
+ <string>0.81823</string>
+ <key>r</key>
+ <string>0.995566</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 finished callbacks}</string>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{239, 572.5118993123325}, {105.66666412353516, 22.544641494750977}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>169450</integer>
+ <key>Layer</key>
+ <integer>1</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.327428</string>
+ <key>g</key>
+ <string>0.81823</string>
+ <key>r</key>
+ <string>0.995566</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 response callbacks}</string>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{30.166670481364818, 548.10604731241608}, {105.66666412353516, 22.544641494750977}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>169449</integer>
+ <key>Layer</key>
+ <integer>1</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.999208</string>
+ <key>g</key>
+ <string>0.811343</string>
+ <key>r</key>
+ <string>0.644457</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 tween egress}</string>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{238.83336130777977, 249.18303989230026}, {105.66666412353516, 22.544641494750977}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>169448</integer>
+ <key>Layer</key>
+ <integer>1</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 ContextFound}</string>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{30.166670481364818, 240.18303707668389}, {105.66666412353516, 22.544641494750977}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>169447</integer>
+ <key>Layer</key>
+ <integer>1</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.815377</string>
+ <key>g</key>
+ <string>1</string>
+ <key>r</key>
+ <string>0.820561</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 traversal}</string>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{30.166670481364818, 105.18303707668386}, {105.66666412353516, 22.544641494750977}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>169446</integer>
+ <key>Layer</key>
+ <integer>1</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.999208</string>
+ <key>g</key>
+ <string>0.811343</string>
+ <key>r</key>
+ <string>0.644457</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 tween ingress}</string>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{30.166670481364818, 45.18303707668386}, {105.66666412353516, 22.544641494750977}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>169445</integer>
+ <key>Layer</key>
+ <integer>1</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.999208</string>
+ <key>g</key>
+ <string>0.811343</string>
+ <key>r</key>
+ <string>0.644457</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 middleware ingress }</string>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{420.49995040893634, 472.78869058972316}, {105.66666412353516, 42.300594329833984}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>169444</integer>
+ <key>Layer</key>
+ <integer>1</integer>
+ <key>Magnets</key>
+ <array>
+ <string>{0, 1}</string>
+ <string>{0, -1}</string>
+ <string>{1, 0}</string>
+ <string>{-1, 0}</string>
+ <string>{-0.49999999999999956, -0.5}</string>
+ <string>{-0.49999999999999956, 0.5}</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.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>
+ </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 notfound_view / forbidden_view / exception_view}</string>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{420.49995040893634, 290.66666666666691}, {105.66666412353516, 22.544642175946908}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>169443</integer>
+ <key>Layer</key>
+ <integer>1</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.756045</string>
+ <key>g</key>
+ <string>0.75004</string>
+ <key>r</key>
+ <string>0.994455</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 exception}</string>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{238.83336512247806, 289.91071033477789}, {105.66666412353516, 22.544642175946908}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>169442</integer>
+ <key>Layer</key>
+ <integer>1</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>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 HTTPNotFound}</string>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Class</key>
+ <string>LineGraphic</string>
+ <key>ControlPoints</key>
+ <array>
+ <string>{0, 6.9840087890625}</string>
+ <string>{0, -9}</string>
+ </array>
+ <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>169447</integer>
+ </dict>
+ <key>ID</key>
+ <integer>169441</integer>
+ <key>Layer</key>
+ <integer>1</integer>
+ <key>Points</key>
+ <array>
+ <string>{82.999949137370791, 216.62202930450437}</string>
+ <string>{83.000002543132396, 240.18303707668389}</string>
+ </array>
+ <key>Style</key>
+ <dict>
+ <key>stroke</key>
+ <dict>
+ <key>Bezier</key>
+ <true/>
+ <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>SharpArrow</string>
+ <key>Legacy</key>
+ <true/>
+ <key>LineType</key>
+ <integer>1</integer>
+ <key>TailArrow</key>
+ <string>0</string>
+ </dict>
+ </dict>
+ <key>Tail</key>
+ <dict>
+ <key>ID</key>
+ <integer>169487</integer>
+ <key>Info</key>
+ <integer>1</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Class</key>
+ <string>LineGraphic</string>
+ <key>ControlPoints</key>
+ <array>
+ <string>{0, 7.05596923828125}</string>
+ <string>{0, -9}</string>
+ </array>
+ <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>169480</integer>
+ <key>Info</key>
+ <integer>2</integer>
+ </dict>
+ <key>ID</key>
+ <integer>169440</integer>
+ <key>Layer</key>
+ <integer>1</integer>
+ <key>Points</key>
+ <array>
+ <string>{82.999997456869679, 263.22767855635425}</string>
+ <string>{82.999997456869679, 335}</string>
+ </array>
+ <key>Style</key>
+ <dict>
+ <key>stroke</key>
+ <dict>
+ <key>Bezier</key>
+ <true/>
+ <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>SharpArrow</string>
+ <key>Legacy</key>
+ <true/>
+ <key>LineType</key>
+ <integer>1</integer>
+ <key>TailArrow</key>
+ <string>0</string>
+ </dict>
+ </dict>
+ <key>Tail</key>
+ <dict>
+ <key>ID</key>
+ <integer>169447</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Class</key>
+ <string>LineGraphic</string>
+ <key>ControlPoints</key>
+ <array>
+ <string>{0, 6.9840087890625}</string>
+ <string>{0, -8.9999999999999432}</string>
+ </array>
+ <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>169475</integer>
+ </dict>
+ <key>ID</key>
+ <integer>169439</integer>
+ <key>Layer</key>
+ <integer>1</integer>
+ <key>Points</key>
+ <array>
+ <string>{82.999997456869679, 385.33334350585938}</string>
+ <string>{83.000002543132396, 452.41914073626253}</string>
+ </array>
+ <key>Style</key>
+ <dict>
+ <key>stroke</key>
+ <dict>
+ <key>Bezier</key>
+ <true/>
+ <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>SharpArrow</string>
+ <key>Legacy</key>
+ <true/>
+ <key>LineType</key>
+ <integer>1</integer>
+ <key>TailArrow</key>
+ <string>0</string>
+ </dict>
+ </dict>
+ <key>Tail</key>
+ <dict>
+ <key>ID</key>
+ <integer>169481</integer>
+ <key>Info</key>
+ <integer>1</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Class</key>
+ <string>LineGraphic</string>
+ <key>ControlPoints</key>
+ <array>
+ <string>{0, 6.9839935302734375}</string>
+ <string>{0, -9}</string>
+ </array>
+ <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>169452</integer>
+ <key>Info</key>
+ <integer>2</integer>
+ </dict>
+ <key>ID</key>
+ <integer>169438</integer>
+ <key>Layer</key>
+ <integer>1</integer>
+ <key>Points</key>
+ <array>
+ <string>{83.000002543132396, 571.15068879140836}</string>
+ <string>{83.000002543132396, 671.51189931233421}</string>
+ </array>
+ <key>Style</key>
+ <dict>
+ <key>stroke</key>
+ <dict>
+ <key>Bezier</key>
+ <true/>
+ <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>SharpArrow</string>
+ <key>Legacy</key>
+ <true/>
+ <key>LineType</key>
+ <integer>1</integer>
+ <key>TailArrow</key>
+ <string>0</string>
+ </dict>
+ </dict>
+ <key>Tail</key>
+ <dict>
+ <key>ID</key>
+ <integer>169449</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Class</key>
+ <string>LineGraphic</string>
+ <key>ControlPoints</key>
+ <array>
+ <string>{0, 7.055999755859375}</string>
+ <string>{0, -9}</string>
+ </array>
+ <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>169483</integer>
+ <key>Info</key>
+ <integer>2</integer>
+ </dict>
+ <key>ID</key>
+ <integer>169437</integer>
+ <key>Layer</key>
+ <integer>1</integer>
+ <key>Points</key>
+ <array>
+ <string>{473.33328247070392, 313.2113088426139}</string>
+ <string>{473.33338419596407, 338.15028762817326}</string>
+ </array>
+ <key>Style</key>
+ <dict>
+ <key>stroke</key>
+ <dict>
+ <key>Bezier</key>
+ <true/>
+ <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>SharpArrow</string>
+ <key>Legacy</key>
+ <true/>
+ <key>LineType</key>
+ <integer>1</integer>
+ <key>TailArrow</key>
+ <string>0</string>
+ </dict>
+ </dict>
+ <key>Tail</key>
+ <dict>
+ <key>ID</key>
+ <integer>169443</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Class</key>
+ <string>LineGraphic</string>
+ <key>ControlPoints</key>
+ <array>
+ <string>{0, 6.9840011596679688}</string>
+ <string>{0, -9}</string>
+ </array>
+ <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>169444</integer>
+ </dict>
+ <key>ID</key>
+ <integer>169436</integer>
+ <key>Layer</key>
+ <integer>1</integer>
+ <key>Points</key>
+ <array>
+ <string>{473.33338359264764, 388.98363112285512}</string>
+ <string>{473.33328247070392, 472.78869058972316}</string>
+ </array>
+ <key>Style</key>
+ <dict>
+ <key>stroke</key>
+ <dict>
+ <key>Bezier</key>
+ <true/>
+ <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>SharpArrow</string>
+ <key>Legacy</key>
+ <true/>
+ <key>LineType</key>
+ <integer>1</integer>
+ <key>TailArrow</key>
+ <string>0</string>
+ </dict>
+ </dict>
+ <key>Tail</key>
+ <dict>
+ <key>ID</key>
+ <integer>169484</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Class</key>
+ <string>LineGraphic</string>
+ <key>Head</key>
+ <dict>
+ <key>ID</key>
+ <integer>169358</integer>
+ <key>Info</key>
+ <integer>6</integer>
+ </dict>
+ <key>ID</key>
+ <integer>169359</integer>
+ <key>Layer</key>
+ <integer>2</integer>
+ <key>Points</key>
+ <array>
+ <string>{272.4166717529298, 537.32234122436705}</string>
+ <string>{420.4999504089364, 515.08928491955714}</string>
+ </array>
+ <key>Style</key>
+ <dict>
+ <key>stroke</key>
+ <dict>
+ <key>Color</key>
+ <dict>
+ <key>b</key>
+ <string>0.755269</string>
+ <key>g</key>
+ <string>0.755239</string>
+ <key>r</key>
+ <string>0.75529</string>
+ </dict>
+ <key>HeadArrow</key>
+ <string>0</string>
+ <key>Legacy</key>
+ <true/>
+ <key>Pattern</key>
+ <integer>11</integer>
+ <key>TailArrow</key>
+ <string>0</string>
+ </dict>
+ </dict>
+ <key>Tail</key>
+ <dict>
+ <key>ID</key>
+ <integer>169206</integer>
+ <key>Info</key>
+ <integer>5</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Class</key>
+ <string>LineGraphic</string>
+ <key>Head</key>
+ <dict>
+ <key>ID</key>
+ <integer>169358</integer>
+ </dict>
+ <key>ID</key>
+ <integer>169360</integer>
+ <key>Layer</key>
+ <integer>2</integer>
+ <key>Points</key>
+ <array>
+ <string>{272.50004831949906, 391.51558277923863}</string>
+ <string>{420.4999504089364, 472.78869058972316}</string>
+ </array>
+ <key>Style</key>
+ <dict>
+ <key>stroke</key>
+ <dict>
+ <key>Color</key>
+ <dict>
+ <key>b</key>
+ <string>0.755269</string>
+ <key>g</key>
+ <string>0.755239</string>
+ <key>r</key>
+ <string>0.75529</string>
+ </dict>
+ <key>HeadArrow</key>
+ <string>0</string>
+ <key>Legacy</key>
+ <true/>
+ <key>Pattern</key>
+ <integer>11</integer>
+ <key>TailArrow</key>
+ <string>0</string>
+ </dict>
+ </dict>
+ <key>Tail</key>
+ <dict>
+ <key>ID</key>
+ <integer>169205</integer>
+ <key>Info</key>
+ <integer>5</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>169044</integer>
+ </dict>
+ <key>ID</key>
+ <integer>169130</integer>
+ <key>Layer</key>
+ <integer>2</integer>
+ <key>Points</key>
+ <array>
+ <string>{83.000002543132396, 592.81693102013151}</string>
+ <string>{239, 583.78422005970799}</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>169128</integer>
+ <key>Position</key>
+ <real>0.28820157051086426</real>
+ </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>169045</integer>
+ </dict>
+ <key>ID</key>
+ <integer>169129</integer>
+ <key>Layer</key>
+ <integer>2</integer>
+ <key>Points</key>
+ <array>
+ <string>{83.000002543132396, 629.80996162681686}</string>
+ <string>{239, 640.78422005970981}</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>169128</integer>
+ <key>Position</key>
+ <real>0.5668826699256897</real>
+ </dict>
+ </dict>
+ <dict>
+ <key>Class</key>
+ <string>Group</string>
+ <key>Graphics</key>
+ <array>
+ <dict>
+ <key>Bounds</key>
+ <string>{{166.8333613077798, 391.51558277923863}, {105.66668701171875, 18.656048080136394}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>169205</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 authorization}</string>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{166.75000762939453, 518.66629314423074}, {105.66666412353516, 18.656048080136394}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>169206</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>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 decorators egress}</string>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{166.75000762939453, 410.17162450154819}, {105.66666412353516, 18.656048080136394}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>169207</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 decorators ingress}</string>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{166.75000762939453, 500.07262547811081}, {105.66666412353516, 18.656048080136394}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>169208</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.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 response adapter}</string>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{166.75000762939453, 481.41657294757954}, {105.66666412353516, 18.656048080136394}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>169209</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.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 view mapper egress}</string>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{166.75000762939453, 447.88119486967923}, {105.66666412353516, 33.089282989501953}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>169210</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.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>
+ </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>
+ <dict>
+ <key>Bounds</key>
+ <string>{{166.75000762939453, 428.77906519094307}, {105.66666412353516, 18.656048080136394}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>169211</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.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 view mapper ingress}</string>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ </dict>
+ </array>
+ <key>ID</key>
+ <integer>169204</integer>
+ <key>Layer</key>
+ <integer>2</integer>
+ </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>169085</integer>
+ <key>Info</key>
+ <integer>4</integer>
+ </dict>
+ <key>ID</key>
+ <integer>169086</integer>
+ <key>Layer</key>
+ <integer>2</integer>
+ <key>Points</key>
+ <array>
+ <string>{83.166643778483959, 611.77452873049333}</string>
+ <string>{238.8333613077798, 611.77452873049333}</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>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{238.83336130777977, 600.50220798311784}, {105.66666412353516, 22.544641494750977}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>169085</integer>
+ <key>Layer</key>
+ <integer>2</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 NewResponse}</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>169083</integer>
+ <key>Info</key>
+ <integer>4</integer>
+ </dict>
+ <key>ID</key>
+ <integer>169084</integer>
+ <key>Layer</key>
+ <integer>2</integer>
+ <key>Points</key>
+ <array>
+ <string>{82.999986314263907, 140.3328574622312}</string>
+ <string>{239.83340199788393, 141.59152244387357}</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>169048</integer>
+ <key>Position</key>
+ <real>0.35945424437522888</real>
+ </dict>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{239.83340199788395, 130.31920169649808}, {105.66666412353516, 22.544641494750977}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>169083</integer>
+ <key>Layer</key>
+ <integer>2</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 NewRequest}</string>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Class</key>
+ <string>TableGroup</string>
+ <key>Graphics</key>
+ <array>
+ <dict>
+ <key>Bounds</key>
+ <string>{{30.166605631511416, 166.28868579864499}, {105.66668701171875, 33.08929443359375}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>169081</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.815377</string>
+ <key>g</key>
+ <string>1</string>
+ <key>r</key>
+ <string>0.820561</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 URL dispatch}</string>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{30.166605631511416, 199.37798023223874}, {105.66668701171875, 17.244049072265625}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>169082</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.815377</string>
+ <key>g</key>
+ <string>1</string>
+ <key>r</key>
+ <string>0.820561</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 route predicates}</string>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ </dict>
+ </array>
+ <key>GridH</key>
+ <array>
+ <integer>169081</integer>
+ <integer>169082</integer>
+ <array/>
+ </array>
+ <key>ID</key>
+ <integer>169080</integer>
+ <key>Layer</key>
+ <integer>2</integer>
+ </dict>
+ <dict>
+ <key>Class</key>
+ <string>TableGroup</string>
+ <key>Graphics</key>
+ <array>
+ <dict>
+ <key>Bounds</key>
+ <string>{{420.5000406901047, 338.15028762817326}, {105.66668701171875, 33.08929443359375}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>169355</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.815377</string>
+ <key>g</key>
+ <string>1</string>
+ <key>r</key>
+ <string>0.820561</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 view lookup}</string>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{420.5000406901047, 371.23958206176701}, {105.66668701171875, 17.244049072265625}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>169356</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.815377</string>
+ <key>g</key>
+ <string>1</string>
+ <key>r</key>
+ <string>0.820561</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 predicates}</string>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ </dict>
+ </array>
+ <key>GridH</key>
+ <array>
+ <integer>169355</integer>
+ <integer>169356</integer>
+ <array/>
+ </array>
+ <key>ID</key>
+ <integer>169354</integer>
+ <key>Layer</key>
+ <integer>2</integer>
+ </dict>
+ <dict>
+ <key>Class</key>
+ <string>TableGroup</string>
+ <key>Graphics</key>
+ <array>
+ <dict>
+ <key>Bounds</key>
+ <string>{{30.166661580404835, 335}, {105.66667175292969, 33.08929443359375}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>169075</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.815377</string>
+ <key>g</key>
+ <string>1</string>
+ <key>r</key>
+ <string>0.820561</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 view lookup}</string>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{30.166661580404835, 368.08929443359375}, {105.66667175292969, 17.244049072265625}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>169076</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.815377</string>
+ <key>g</key>
+ <string>1</string>
+ <key>r</key>
+ <string>0.820561</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 predicates}</string>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ </dict>
+ </array>
+ <key>GridH</key>
+ <array>
+ <integer>169075</integer>
+ <integer>169076</integer>
+ <array/>
+ </array>
+ <key>ID</key>
+ <integer>169074</integer>
+ <key>Layer</key>
+ <integer>2</integer>
+ </dict>
+ <dict>
+ <key>Class</key>
+ <string>LineGraphic</string>
+ <key>Head</key>
+ <dict>
+ <key>ID</key>
+ <integer>169070</integer>
+ <key>Info</key>
+ <integer>7</integer>
+ </dict>
+ <key>ID</key>
+ <integer>169073</integer>
+ <key>Layer</key>
+ <integer>2</integer>
+ <key>Points</key>
+ <array>
+ <string>{166.75000762939462, 537.32234122436694}</string>
+ <string>{135.66666666666765, 485}</string>
+ </array>
+ <key>Style</key>
+ <dict>
+ <key>stroke</key>
+ <dict>
+ <key>Color</key>
+ <dict>
+ <key>b</key>
+ <string>0.755269</string>
+ <key>g</key>
+ <string>0.755239</string>
+ <key>r</key>
+ <string>0.75529</string>
+ </dict>
+ <key>HeadArrow</key>
+ <string>0</string>
+ <key>Legacy</key>
+ <true/>
+ <key>Pattern</key>
+ <integer>11</integer>
+ <key>TailArrow</key>
+ <string>0</string>
+ </dict>
+ </dict>
+ <key>Tail</key>
+ <dict>
+ <key>ID</key>
+ <integer>169206</integer>
+ <key>Info</key>
+ <integer>6</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Class</key>
+ <string>LineGraphic</string>
+ <key>Head</key>
+ <dict>
+ <key>ID</key>
+ <integer>169070</integer>
+ <key>Info</key>
+ <integer>8</integer>
+ </dict>
+ <key>ID</key>
+ <integer>169072</integer>
+ <key>Layer</key>
+ <integer>2</integer>
+ <key>Points</key>
+ <array>
+ <string>{167.33336141608385, 392.09395827769049}</string>
+ <string>{135.66666666666777, 452.41914073626253}</string>
+ </array>
+ <key>Style</key>
+ <dict>
+ <key>stroke</key>
+ <dict>
+ <key>Color</key>
+ <dict>
+ <key>b</key>
+ <string>0.755269</string>
+ <key>g</key>
+ <string>0.755239</string>
+ <key>r</key>
+ <string>0.75529</string>
+ </dict>
+ <key>HeadArrow</key>
+ <string>0</string>
+ <key>Legacy</key>
+ <true/>
+ <key>Pattern</key>
+ <integer>11</integer>
+ <key>TailArrow</key>
+ <string>0</string>
+ </dict>
+ </dict>
+ <key>Tail</key>
+ <dict>
+ <key>ID</key>
+ <integer>169205</integer>
+ <key>Info</key>
+ <integer>6</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Class</key>
+ <string>LineGraphic</string>
+ <key>ControlPoints</key>
+ <array>
+ <string>{0, 6.9840087890625}</string>
+ <string>{0, -8.9999999999999432}</string>
+ </array>
+ <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>169043</integer>
+ </dict>
+ <key>ID</key>
+ <integer>169071</integer>
+ <key>Layer</key>
+ <integer>2</integer>
+ <key>Points</key>
+ <array>
+ <string>{83.000002543132396, 485.50842372576449}</string>
+ <string>{83.000002543132396, 548.10604731241608}</string>
+ </array>
+ <key>Style</key>
+ <dict>
+ <key>stroke</key>
+ <dict>
+ <key>Bezier</key>
+ <true/>
+ <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>SharpArrow</string>
+ <key>Legacy</key>
+ <true/>
+ <key>LineType</key>
+ <integer>1</integer>
+ <key>TailArrow</key>
+ <string>0</string>
+ </dict>
+ </dict>
+ <key>Tail</key>
+ <dict>
+ <key>ID</key>
+ <integer>169070</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{30.166670481364818, 452.41914073626253}, {105.66666412353516, 33.089282989501953}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>169070</integer>
+ <key>Layer</key>
+ <integer>2</integer>
+ <key>Magnets</key>
+ <array>
+ <string>{0, 1}</string>
+ <string>{0, -1}</string>
+ <string>{1, 0}</string>
+ <string>{-1, 0}</string>
+ <string>{-0.49211360058019871, -0.49251945318722434}</string>
+ <string>{-0.49211360058019871, 0.49470854679786669}</string>
+ <string>{0.4984227008620481, 0.48463479169597612}</string>
+ <string>{0.49842270086204898, -0.5}</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.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>
+ </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 pipeline}</string>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Class</key>
+ <string>LineGraphic</string>
+ <key>ControlPoints</key>
+ <array>
+ <string>{51.333333333333314, 0}</string>
+ <string>{-0.66666666666662877, 58.666666666666686}</string>
+ <string>{0.66673293066804717, -58.666850540458825}</string>
+ <string>{-16.306719354194399, 0.26652623861849634}</string>
+ </array>
+ <key>Head</key>
+ <dict>
+ <key>ID</key>
+ <integer>169344</integer>
+ <key>Info</key>
+ <integer>4</integer>
+ </dict>
+ <key>ID</key>
+ <integer>169345</integer>
+ <key>Layer</key>
+ <integer>2</integer>
+ <key>Points</key>
+ <array>
+ <string>{369.66666666666669, 541}</string>
+ <string>{404.00000000000023, 362}</string>
+ <string>{420.36749776329049, 302.42112495959606}</string>
+ </array>
+ <key>Style</key>
+ <dict>
+ <key>stroke</key>
+ <dict>
+ <key>Bezier</key>
+ <true/>
+ <key>Color</key>
+ <dict>
+ <key>b</key>
+ <string>0.75663</string>
+ <key>g</key>
+ <string>0.756618</string>
+ <key>r</key>
+ <string>0.75664</string>
+ </dict>
+ <key>HeadArrow</key>
+ <string>0</string>
+ <key>Legacy</key>
+ <true/>
+ <key>LineType</key>
+ <integer>1</integer>
+ <key>Pattern</key>
+ <integer>1</integer>
+ <key>TailArrow</key>
+ <string>0</string>
+ </dict>
+ </dict>
+ </dict>
+ <dict>
+ <key>Class</key>
+ <string>LineGraphic</string>
+ <key>ControlPoints</key>
+ <array>
+ <string>{69.833333333332462, -0.72767857143483639}</string>
+ <string>{-0.66690523835279691, -51.044605218028948}</string>
+ <string>{0.66666666666674246, 51.044637362162291}</string>
+ <string>{-24.333271383961971, -0.13425428344572765}</string>
+ </array>
+ <key>Head</key>
+ <dict>
+ <key>ID</key>
+ <integer>169344</integer>
+ <key>Info</key>
+ <integer>4</integer>
+ </dict>
+ <key>ID</key>
+ <integer>169346</integer>
+ <key>Layer</key>
+ <integer>2</integer>
+ <key>Points</key>
+ <array>
+ <string>{310.66666666666754, 118.72767857143484}</string>
+ <string>{399.33333333333417, 216.62202930450439}</string>
+ <string>{420.37955338188368, 301.45369961823752}</string>
+ </array>
+ <key>Style</key>
+ <dict>
+ <key>stroke</key>
+ <dict>
+ <key>Bezier</key>
+ <true/>
+ <key>Color</key>
+ <dict>
+ <key>b</key>
+ <string>0.75663</string>
+ <key>g</key>
+ <string>0.756618</string>
+ <key>r</key>
+ <string>0.75664</string>
+ </dict>
+ <key>HeadArrow</key>
+ <string>0</string>
+ <key>Legacy</key>
+ <true/>
+ <key>LineType</key>
+ <integer>1</integer>
+ <key>Pattern</key>
+ <integer>1</integer>
+ <key>TailArrow</key>
+ <string>0</string>
+ </dict>
+ </dict>
+ </dict>
+ <dict>
+ <key>Class</key>
+ <string>LineGraphic</string>
+ <key>ControlPoints</key>
+ <array>
+ <string>{-3.9999491373696401, 78.910715080442856}</string>
+ <string>{92.666683130060392, 0.22547126950667007}</string>
+ </array>
+ <key>Head</key>
+ <dict>
+ <key>ID</key>
+ <integer>169085</integer>
+ <key>Info</key>
+ <integer>3</integer>
+ </dict>
+ <key>ID</key>
+ <integer>169361</integer>
+ <key>Layer</key>
+ <integer>2</integer>
+ <key>Points</key>
+ <array>
+ <string>{473.33328247070392, 515.08928491955714}</string>
+ <string>{344.50002543131501, 611.77452873049333}</string>
+ </array>
+ <key>Style</key>
+ <dict>
+ <key>stroke</key>
+ <dict>
+ <key>Bezier</key>
+ <true/>
+ <key>HeadArrow</key>
+ <string>SharpArrow</string>
+ <key>Legacy</key>
+ <true/>
+ <key>LineType</key>
+ <integer>1</integer>
+ <key>TailArrow</key>
+ <string>0</string>
+ </dict>
+ </dict>
+ <key>Tail</key>
+ <dict>
+ <key>ID</key>
+ <integer>169358</integer>
+ <key>Info</key>
+ <integer>1</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Class</key>
+ <string>LineGraphic</string>
+ <key>ControlPoints</key>
+ <array>
+ <string>{31.999987284342428, -14.081351280212308}</string>
+ <string>{-32.166667938232536, 10.244050343831077}</string>
+ </array>
+ <key>ID</key>
+ <integer>169341</integer>
+ <key>Layer</key>
+ <integer>2</integer>
+ <key>Points</key>
+ <array>
+ <string>{344.96346869509995, 346.26317428240412}</string>
+ <string>{389.8333346048999, 328.08928298950207}</string>
+ </array>
+ <key>Style</key>
+ <dict>
+ <key>stroke</key>
+ <dict>
+ <key>Bezier</key>
+ <true/>
+ <key>HeadArrow</key>
+ <string>SharpArrow</string>
+ <key>Legacy</key>
+ <true/>
+ <key>LineType</key>
+ <integer>1</integer>
+ <key>TailArrow</key>
+ <string>0</string>
+ </dict>
+ </dict>
+ <key>Tail</key>
+ <dict>
+ <key>ID</key>
+ <integer>169340</integer>
+ <key>Info</key>
+ <integer>3</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Class</key>
+ <string>LineGraphic</string>
+ <key>ControlPoints</key>
+ <array>
+ <string>{31.999987284342428, -14.081351280212308}</string>
+ <string>{-28.500001271565793, 8.3333333333333144}</string>
+ </array>
+ <key>ID</key>
+ <integer>169337</integer>
+ <key>Layer</key>
+ <integer>2</integer>
+ <key>Points</key>
+ <array>
+ <string>{344.98861594084059, 323.71068461220347}</string>
+ <string>{391.1666679382335, 313.6666666666664}</string>
+ </array>
+ <key>Style</key>
+ <dict>
+ <key>stroke</key>
+ <dict>
+ <key>Bezier</key>
+ <true/>
+ <key>HeadArrow</key>
+ <string>SharpArrow</string>
+ <key>Legacy</key>
+ <true/>
+ <key>LineType</key>
+ <integer>1</integer>
+ <key>TailArrow</key>
+ <string>0</string>
+ </dict>
+ </dict>
+ <key>Tail</key>
+ <dict>
+ <key>ID</key>
+ <integer>169336</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Class</key>
+ <string>LineGraphic</string>
+ <key>ControlPoints</key>
+ <array>
+ <string>{31.999987284342428, -14.081351280212308}</string>
+ <string>{-40, 1.5446373167492311}</string>
+ </array>
+ <key>ID</key>
+ <integer>169333</integer>
+ <key>Layer</key>
+ <integer>2</integer>
+ <key>Points</key>
+ <array>
+ <string>{344.9995533451783, 301.1612218744512}</string>
+ <string>{394.50000127156665, 299.00000000000045}</string>
+ </array>
+ <key>Style</key>
+ <dict>
+ <key>stroke</key>
+ <dict>
+ <key>Bezier</key>
+ <true/>
+ <key>HeadArrow</key>
+ <string>SharpArrow</string>
+ <key>Legacy</key>
+ <true/>
+ <key>LineType</key>
+ <integer>1</integer>
+ <key>TailArrow</key>
+ <string>0</string>
+ </dict>
+ </dict>
+ <key>Tail</key>
+ <dict>
+ <key>ID</key>
+ <integer>169332</integer>
+ <key>Info</key>
+ <integer>3</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Class</key>
+ <string>LineGraphic</string>
+ <key>ControlPoints</key>
+ <array>
+ <string>{8.5833282470703125, -10.244596987647753}</string>
+ <string>{0, 0}</string>
+ </array>
+ <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>169051</integer>
+ <key>Info</key>
+ <integer>4</integer>
+ </dict>
+ <key>ID</key>
+ <integer>169062</integer>
+ <key>Layer</key>
+ <integer>2</integer>
+ <key>Points</key>
+ <array>
+ <string>{272.41667175292969, 509.40064951817902}</string>
+ <string>{285.5, 503.81699882234847}</string>
+ </array>
+ <key>Style</key>
+ <dict>
+ <key>stroke</key>
+ <dict>
+ <key>Bezier</key>
+ <true/>
+ <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>LineType</key>
+ <integer>1</integer>
+ <key>Pattern</key>
+ <integer>1</integer>
+ <key>TailArrow</key>
+ <string>0</string>
+ </dict>
+ </dict>
+ <key>Tail</key>
+ <dict>
+ <key>ID</key>
+ <integer>169208</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>169042</integer>
+ <key>Info</key>
+ <integer>4</integer>
+ </dict>
+ <key>ID</key>
+ <integer>169061</integer>
+ <key>Layer</key>
+ <integer>2</integer>
+ <key>Points</key>
+ <array>
+ <string>{82.999997456869679, 300.27229985501288}</string>
+ <string>{238.34892458824362, 260.57913893040109}</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>169034</integer>
+ <key>Position</key>
+ <real>0.51973581314086914</real>
+ </dict>
+ </dict>
+ <dict>
+ <key>Class</key>
+ <string>Group</string>
+ <key>Graphics</key>
+ <array>
+ <dict>
+ <key>Bounds</key>
+ <string>{{419.66662216186666, 214.61452811107179}, {105.66666412353516, 22.544642175946908}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>169054</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.756045</string>
+ <key>g</key>
+ <string>0.75004</string>
+ <key>r</key>
+ <string>0.994455</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 exception}</string>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{419.66662216186666, 130.51770718892479}, {105.66666412353516, 22.544641494750977}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>169055</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.815377</string>
+ <key>g</key>
+ <string>1</string>
+ <key>r</key>
+ <string>0.820561</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 internal process}</string>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{419.66662216186666, 91.940789540609359}, {105.66666412353516, 33.089282989501953}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>169056</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.999208</string>
+ <key>g</key>
+ <string>0.811343</string>
+ <key>r</key>
+ <string>0.644457</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 external process (middleware, tween)}</string>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{419.66662216186666, 158.54998334248924}, {105.66666412353516, 22.544641494750977}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>169057</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.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>
+ </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>
+ <dict>
+ <key>Bounds</key>
+ <string>{{419.66662216186666, 186.58225949605369}, {105.66666412353516, 22.544641494750977}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>169058</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.327428</string>
+ <key>g</key>
+ <string>0.81823</string>
+ <key>r</key>
+ <string>0.995566</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 callback}</string>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{419.66662216186666, 63.908513387045019}, {105.66666412353516, 22.544641494750977}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>169059</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 event}</string>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{406.9999504089372, 42.910746256511771}, {132.66667175292969, 207.81692504882812}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>169060</integer>
+ <key>Magnets</key>
+ <array>
+ <string>{1, 0.5}</string>
+ <string>{1, -0.5}</string>
+ <string>{-1, 0.5}</string>
+ <string>{-1, -0.5}</string>
+ <string>{0.5, 1}</string>
+ <string>{-0.5, 1}</string>
+ <string>{0.5, -1}</string>
+ <string>{-0.5, -1}</string>
+ </array>
+ <key>Shape</key>
+ <string>Rectangle</string>
+ <key>Style</key>
+ <dict>
+ <key>fill</key>
+ <dict>
+ <key>Draws</key>
+ <string>NO</string>
+ </dict>
+ <key>shadow</key>
+ <dict>
+ <key>Draws</key>
+ <string>NO</string>
+ </dict>
+ <key>stroke</key>
+ <dict>
+ <key>CornerRadius</key>
+ <real>5</real>
+ </dict>
+ </dict>
+ <key>Text</key>
+ <dict>
+ <key>Align</key>
+ <integer>0</integer>
+ <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
+
+\f0\b\fs20 \cf0 Legend}</string>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ <key>TextPlacement</key>
+ <integer>0</integer>
+ </dict>
+ </array>
+ <key>ID</key>
+ <integer>169053</integer>
+ <key>Layer</key>
+ <integer>2</integer>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{233.5000012715667, 20.000000000000934}, {116, 14}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>FitText</key>
+ <string>YES</string>
+ <key>Flow</key>
+ <string>Resize</string>
+ <key>FontInfo</key>
+ <dict>
+ <key>Font</key>
+ <string>Helvetica</string>
+ <key>Size</key>
+ <real>12</real>
+ </dict>
+ <key>ID</key>
+ <integer>169052</integer>
+ <key>Layer</key>
+ <integer>2</integer>
+ <key>Shape</key>
+ <string>Rectangle</string>
+ <key>Style</key>
+ <dict>
+ <key>fill</key>
+ <dict>
+ <key>Draws</key>
+ <string>NO</string>
+ </dict>
+ <key>shadow</key>
+ <dict>
+ <key>Draws</key>
+ <string>NO</string>
+ </dict>
+ <key>stroke</key>
+ <dict>
+ <key>Draws</key>
+ <string>NO</string>
+ </dict>
+ </dict>
+ <key>Text</key>
+ <dict>
+ <key>Pad</key>
+ <integer>0</integer>
+ <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\pardirnatural\qc
+
+\f0\b\fs24 \cf0 &lt;%Canvas%&gt;}</string>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ <key>Wrap</key>
+ <string>NO</string>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{285.5, 492.544677734375}, {105.66666412353516, 22.544642175946908}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>169051</integer>
+ <key>Layer</key>
+ <integer>2</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 BeforeRender}</string>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{238.83337529500417, 335.17855853126167}, {105.66666412353516, 22.544642175946908}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>169340</integer>
+ <key>Layer</key>
+ <integer>2</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>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 HTTPForbidden}</string>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{238.83337529500417, 312.54463200342093}, {105.66666412353516, 22.544642175946908}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>169336</integer>
+ <key>Layer</key>
+ <integer>2</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>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 PredicateMismatch}</string>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Class</key>
+ <string>LineGraphic</string>
+ <key>ControlPoints</key>
+ <array>
+ <string>{0, 7.05596923828125}</string>
+ <string>{0, -9}</string>
+ </array>
+ <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>169081</integer>
+ <key>Info</key>
+ <integer>2</integer>
+ </dict>
+ <key>ID</key>
+ <integer>169048</integer>
+ <key>Layer</key>
+ <integer>2</integer>
+ <key>Points</key>
+ <array>
+ <string>{83.000001850648417, 128.2276785555359}</string>
+ <string>{82.999949137370791, 166.28868579864502}</string>
+ </array>
+ <key>Style</key>
+ <dict>
+ <key>stroke</key>
+ <dict>
+ <key>Bezier</key>
+ <true/>
+ <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>SharpArrow</string>
+ <key>Legacy</key>
+ <true/>
+ <key>LineType</key>
+ <integer>1</integer>
+ <key>TailArrow</key>
+ <string>0</string>
+ </dict>
+ </dict>
+ <key>Tail</key>
+ <dict>
+ <key>ID</key>
+ <integer>169040</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Class</key>
+ <string>LineGraphic</string>
+ <key>ControlPoints</key>
+ <array>
+ <string>{0, 6.9840087890625}</string>
+ <string>{0, -9}</string>
+ </array>
+ <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>169040</integer>
+ </dict>
+ <key>ID</key>
+ <integer>169047</integer>
+ <key>Layer</key>
+ <integer>2</integer>
+ <key>Points</key>
+ <array>
+ <string>{83.000002543132396, 67.727678571434836}</string>
+ <string>{83.000002543132396, 105.18303707668386}</string>
+ </array>
+ <key>Style</key>
+ <dict>
+ <key>stroke</key>
+ <dict>
+ <key>Bezier</key>
+ <true/>
+ <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>SharpArrow</string>
+ <key>Legacy</key>
+ <true/>
+ <key>LineType</key>
+ <integer>1</integer>
+ <key>TailArrow</key>
+ <string>0</string>
+ </dict>
+ </dict>
+ <key>Tail</key>
+ <dict>
+ <key>ID</key>
+ <integer>169039</integer>
+ <key>Info</key>
+ <integer>1</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{30.166670481364818, 671.51189931233432}, {105.66666412353516, 22.544641494750977}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>169046</integer>
+ <key>Layer</key>
+ <integer>2</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.999208</string>
+ <key>g</key>
+ <string>0.811343</string>
+ <key>r</key>
+ <string>0.644457</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 middleware egress}</string>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{239, 629.51189931233432}, {105.66666412353516, 22.544641494750977}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>169045</integer>
+ <key>Layer</key>
+ <integer>2</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.327428</string>
+ <key>g</key>
+ <string>0.81823</string>
+ <key>r</key>
+ <string>0.995566</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 finished callbacks}</string>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{239, 572.5118993123325}, {105.66666412353516, 22.544641494750977}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>169044</integer>
+ <key>Layer</key>
+ <integer>2</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.327428</string>
+ <key>g</key>
+ <string>0.81823</string>
+ <key>r</key>
+ <string>0.995566</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 response callbacks}</string>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{30.166670481364818, 548.10604731241608}, {105.66666412353516, 22.544641494750977}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>169043</integer>
+ <key>Layer</key>
+ <integer>2</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.999208</string>
+ <key>g</key>
+ <string>0.811343</string>
+ <key>r</key>
+ <string>0.644457</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 tween egress}</string>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{238.83336130777977, 249.18303989230026}, {105.66666412353516, 22.544641494750977}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>169042</integer>
+ <key>Layer</key>
+ <integer>2</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 ContextFound}</string>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{30.166670481364818, 240.18303707668389}, {105.66666412353516, 22.544641494750977}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>169041</integer>
+ <key>Layer</key>
+ <integer>2</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.815377</string>
+ <key>g</key>
+ <string>1</string>
+ <key>r</key>
+ <string>0.820561</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 traversal}</string>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{30.166670481364818, 105.18303707668386}, {105.66666412353516, 22.544641494750977}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>169040</integer>
+ <key>Layer</key>
+ <integer>2</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.999208</string>
+ <key>g</key>
+ <string>0.811343</string>
+ <key>r</key>
+ <string>0.644457</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 tween ingress}</string>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{30.166670481364818, 45.18303707668386}, {105.66666412353516, 22.544641494750977}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>169039</integer>
+ <key>Layer</key>
+ <integer>2</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.999208</string>
+ <key>g</key>
+ <string>0.811343</string>
+ <key>r</key>
+ <string>0.644457</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 middleware ingress }</string>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{420.49995040893634, 472.78869058972316}, {105.66666412353516, 42.300594329833984}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>169358</integer>
+ <key>Layer</key>
+ <integer>2</integer>
+ <key>Magnets</key>
+ <array>
+ <string>{0, 1}</string>
+ <string>{0, -1}</string>
+ <string>{1, 0}</string>
+ <string>{-1, 0}</string>
+ <string>{-0.49999999999999956, -0.5}</string>
+ <string>{-0.49999999999999956, 0.5}</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.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>
+ </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 notfound_view / forbidden_view / exception_view}</string>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{420.49995040893634, 290.66666666666691}, {105.66666412353516, 22.544642175946908}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>169344</integer>
+ <key>Layer</key>
+ <integer>2</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.756045</string>
+ <key>g</key>
+ <string>0.75004</string>
+ <key>r</key>
+ <string>0.994455</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 exception}</string>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{238.83336512247806, 289.91071033477789}, {105.66666412353516, 22.544642175946908}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>169332</integer>
+ <key>Layer</key>
+ <integer>2</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>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 HTTPNotFound}</string>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Class</key>
+ <string>LineGraphic</string>
+ <key>ControlPoints</key>
+ <array>
+ <string>{0, 6.9840087890625}</string>
+ <string>{0, -9}</string>
+ </array>
+ <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>169041</integer>
+ </dict>
+ <key>ID</key>
+ <integer>169035</integer>
+ <key>Layer</key>
+ <integer>2</integer>
+ <key>Points</key>
+ <array>
+ <string>{82.999949137370791, 216.62202930450437}</string>
+ <string>{83.000002543132396, 240.18303707668389}</string>
+ </array>
+ <key>Style</key>
+ <dict>
+ <key>stroke</key>
+ <dict>
+ <key>Bezier</key>
+ <true/>
+ <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>SharpArrow</string>
+ <key>Legacy</key>
+ <true/>
+ <key>LineType</key>
+ <integer>1</integer>
+ <key>TailArrow</key>
+ <string>0</string>
+ </dict>
+ </dict>
+ <key>Tail</key>
+ <dict>
+ <key>ID</key>
+ <integer>169082</integer>
+ <key>Info</key>
+ <integer>1</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Class</key>
+ <string>LineGraphic</string>
+ <key>ControlPoints</key>
+ <array>
+ <string>{0, 7.05596923828125}</string>
+ <string>{0, -9}</string>
+ </array>
+ <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>169075</integer>
+ <key>Info</key>
+ <integer>2</integer>
+ </dict>
+ <key>ID</key>
+ <integer>169034</integer>
+ <key>Layer</key>
+ <integer>2</integer>
+ <key>Points</key>
+ <array>
+ <string>{82.999997456869679, 263.22767855635425}</string>
+ <string>{82.999997456869679, 335}</string>
+ </array>
+ <key>Style</key>
+ <dict>
+ <key>stroke</key>
+ <dict>
+ <key>Bezier</key>
+ <true/>
+ <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>SharpArrow</string>
+ <key>Legacy</key>
+ <true/>
+ <key>LineType</key>
+ <integer>1</integer>
+ <key>TailArrow</key>
+ <string>0</string>
+ </dict>
+ </dict>
+ <key>Tail</key>
+ <dict>
+ <key>ID</key>
+ <integer>169041</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Class</key>
+ <string>LineGraphic</string>
+ <key>ControlPoints</key>
+ <array>
+ <string>{0, 6.9840087890625}</string>
+ <string>{0, -8.9999999999999432}</string>
+ </array>
+ <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>169070</integer>
+ </dict>
+ <key>ID</key>
+ <integer>169033</integer>
+ <key>Layer</key>
+ <integer>2</integer>
+ <key>Points</key>
+ <array>
+ <string>{82.999997456869679, 385.33334350585938}</string>
+ <string>{83.000002543132396, 452.41914073626253}</string>
+ </array>
+ <key>Style</key>
+ <dict>
+ <key>stroke</key>
+ <dict>
+ <key>Bezier</key>
+ <true/>
+ <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>SharpArrow</string>
+ <key>Legacy</key>
+ <true/>
+ <key>LineType</key>
+ <integer>1</integer>
+ <key>TailArrow</key>
+ <string>0</string>
+ </dict>
+ </dict>
+ <key>Tail</key>
+ <dict>
+ <key>ID</key>
+ <integer>169076</integer>
+ <key>Info</key>
+ <integer>1</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Class</key>
+ <string>LineGraphic</string>
+ <key>ControlPoints</key>
+ <array>
+ <string>{0, 6.9839935302734375}</string>
+ <string>{0, -9}</string>
+ </array>
+ <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>169046</integer>
+ <key>Info</key>
+ <integer>2</integer>
+ </dict>
+ <key>ID</key>
+ <integer>169128</integer>
+ <key>Layer</key>
+ <integer>2</integer>
+ <key>Points</key>
+ <array>
+ <string>{83.000002543132396, 571.15068879140836}</string>
+ <string>{83.000002543132396, 671.51189931233421}</string>
+ </array>
+ <key>Style</key>
+ <dict>
+ <key>stroke</key>
+ <dict>
+ <key>Bezier</key>
+ <true/>
+ <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>SharpArrow</string>
+ <key>Legacy</key>
+ <true/>
+ <key>LineType</key>
+ <integer>1</integer>
+ <key>TailArrow</key>
+ <string>0</string>
+ </dict>
+ </dict>
+ <key>Tail</key>
+ <dict>
+ <key>ID</key>
+ <integer>169043</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Class</key>
+ <string>LineGraphic</string>
+ <key>ControlPoints</key>
+ <array>
+ <string>{0, 7.055999755859375}</string>
+ <string>{0, -9}</string>
+ </array>
+ <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>169355</integer>
+ <key>Info</key>
+ <integer>2</integer>
+ </dict>
+ <key>ID</key>
+ <integer>169357</integer>
+ <key>Layer</key>
+ <integer>2</integer>
+ <key>Points</key>
+ <array>
+ <string>{473.33328247070392, 313.2113088426139}</string>
+ <string>{473.33338419596407, 338.15028762817326}</string>
+ </array>
+ <key>Style</key>
+ <dict>
+ <key>stroke</key>
+ <dict>
+ <key>Bezier</key>
+ <true/>
+ <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>SharpArrow</string>
+ <key>Legacy</key>
+ <true/>
+ <key>LineType</key>
+ <integer>1</integer>
+ <key>TailArrow</key>
+ <string>0</string>
+ </dict>
+ </dict>
+ <key>Tail</key>
+ <dict>
+ <key>ID</key>
+ <integer>169344</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Class</key>
+ <string>LineGraphic</string>
+ <key>ControlPoints</key>
+ <array>
+ <string>{0, 6.9840011596679688}</string>
+ <string>{0, -9}</string>
+ </array>
+ <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>169358</integer>
+ </dict>
+ <key>ID</key>
+ <integer>169362</integer>
+ <key>Layer</key>
+ <integer>2</integer>
+ <key>Points</key>
+ <array>
+ <string>{473.33338359264764, 388.98363112285512}</string>
+ <string>{473.33328247070392, 472.78869058972316}</string>
+ </array>
+ <key>Style</key>
+ <dict>
+ <key>stroke</key>
+ <dict>
+ <key>Bezier</key>
+ <true/>
+ <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>SharpArrow</string>
+ <key>Legacy</key>
+ <true/>
+ <key>LineType</key>
+ <integer>1</integer>
+ <key>TailArrow</key>
+ <string>0</string>
+ </dict>
+ </dict>
+ <key>Tail</key>
+ <dict>
+ <key>ID</key>
+ <integer>169356</integer>
+ </dict>
+ </dict>
+ </array>
+ <key>GridInfo</key>
+ <dict/>
+ <key>GuidesLocked</key>
+ <string>NO</string>
+ <key>GuidesVisible</key>
+ <string>YES</string>
+ <key>HPages</key>
+ <integer>1</integer>
+ <key>ImageCounter</key>
+ <integer>3</integer>
+ <key>KeepToScale</key>
+ <false/>
+ <key>Layers</key>
+ <array>
+ <dict>
+ <key>Lock</key>
+ <string>NO</string>
+ <key>Name</key>
+ <string>no exceptions</string>
+ <key>Print</key>
+ <string>YES</string>
+ <key>View</key>
+ <string>YES</string>
+ </dict>
+ <dict>
+ <key>Lock</key>
+ <string>NO</string>
+ <key>Name</key>
+ <string>exceptions only</string>
+ <key>Print</key>
+ <string>YES</string>
+ <key>View</key>
+ <string>NO</string>
+ </dict>
+ <dict>
+ <key>Lock</key>
+ <string>NO</string>
+ <key>Name</key>
+ <string>all</string>
+ <key>Print</key>
+ <string>YES</string>
+ <key>View</key>
+ <string>NO</string>
+ </dict>
+ </array>
+ <key>LayoutInfo</key>
+ <dict>
+ <key>Animate</key>
+ <string>NO</string>
+ <key>circoMinDist</key>
+ <real>18</real>
+ <key>circoSeparation</key>
+ <real>0.0</real>
+ <key>layoutEngine</key>
+ <string>dot</string>
+ <key>neatoSeparation</key>
+ <real>0.0</real>
+ <key>twopiSeparation</key>
+ <real>0.0</real>
+ </dict>
+ <key>LinksVisible</key>
+ <string>NO</string>
+ <key>MagnetsVisible</key>
+ <string>NO</string>
+ <key>MasterSheets</key>
+ <array/>
+ <key>ModificationDate</key>
+ <string>2014-11-23 07:19:11 +0000</string>
+ <key>Modifier</key>
+ <string>Steve Piercy</string>
+ <key>NotesVisible</key>
+ <string>NO</string>
+ <key>Orientation</key>
+ <integer>2</integer>
+ <key>OriginVisible</key>
+ <string>NO</string>
+ <key>PageBreaks</key>
+ <string>YES</string>
+ <key>PrintInfo</key>
+ <dict>
+ <key>NSBottomMargin</key>
+ <array>
+ <string>float</string>
+ <string>41</string>
+ </array>
+ <key>NSHorizonalPagination</key>
+ <array>
+ <string>coded</string>
+ <string>BAtzdHJlYW10eXBlZIHoA4QBQISEhAhOU051bWJlcgCEhAdOU1ZhbHVlAISECE5TT2JqZWN0AIWEASqEhAFxlwCG</string>
+ </array>
+ <key>NSLeftMargin</key>
+ <array>
+ <string>float</string>
+ <string>18</string>
+ </array>
+ <key>NSPaperSize</key>
+ <array>
+ <string>size</string>
+ <string>{612, 792}</string>
+ </array>
+ <key>NSPrintReverseOrientation</key>
+ <array>
+ <string>int</string>
+ <string>0</string>
+ </array>
+ <key>NSRightMargin</key>
+ <array>
+ <string>float</string>
+ <string>18</string>
+ </array>
+ <key>NSTopMargin</key>
+ <array>
+ <string>float</string>
+ <string>18</string>
+ </array>
+ </dict>
+ <key>PrintOnePage</key>
+ <false/>
+ <key>ReadOnly</key>
+ <string>NO</string>
+ <key>RowAlign</key>
+ <integer>1</integer>
+ <key>RowSpacing</key>
+ <real>36</real>
+ <key>SheetTitle</key>
+ <string>Request Processing</string>
+ <key>SmartAlignmentGuidesActive</key>
+ <string>YES</string>
+ <key>SmartDistanceGuidesActive</key>
+ <string>YES</string>
+ <key>UniqueID</key>
+ <integer>1</integer>
+ <key>UseEntirePage</key>
+ <false/>
+ <key>VPages</key>
+ <integer>1</integer>
+ <key>WindowInfo</key>
+ <dict>
+ <key>CurrentSheet</key>
+ <integer>0</integer>
+ <key>ExpandedCanvases</key>
+ <array>
+ <dict>
+ <key>name</key>
+ <string>Request Processing</string>
+ </dict>
+ </array>
+ <key>Frame</key>
+ <string>{{35, 93}, {1394, 1325}}</string>
+ <key>ListView</key>
+ <true/>
+ <key>OutlineWidth</key>
+ <integer>178</integer>
+ <key>RightSidebar</key>
+ <true/>
+ <key>ShowRuler</key>
+ <true/>
+ <key>Sidebar</key>
+ <true/>
+ <key>SidebarWidth</key>
+ <integer>163</integer>
+ <key>VisibleRegion</key>
+ <string>{{-231, -226}, {1037, 1186}}</string>
+ <key>Zoom</key>
+ <real>1</real>
+ <key>ZoomValues</key>
+ <array>
+ <array>
+ <string>Request Processing</string>
+ <real>1</real>
+ <real>2</real>
+ </array>
+ </array>
+ </dict>
+</dict>
+</plist>
diff --git a/docs/_static/pyramid_request_processing.png b/docs/_static/pyramid_request_processing.png
new file mode 100644
index 000000000..2fbb1e164
--- /dev/null
+++ 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
new file mode 100644
index 000000000..21bbcb532
--- /dev/null
+++ b/docs/_static/pyramid_request_processing.svg
@@ -0,0 +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>2014-11-23 07:19Z</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" width="105.666664" height="22.544642" fill="#dfbeff"/><rect x="375.5" y="391" width="105.666664" height="22.544642" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(380.5 396.27232)" 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="37.83089" y="10" textLength="20.004883">view</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.33336" 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.75001" 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.75001" y="322.26348" width="105.666664" height="18.656048" fill="#ffffa3"/><rect x="238.75001" y="322.26348" width="105.666664" height="18.656048" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(243.75001 325.5915)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="1.9812813" y="10" textLength="91.7041">view mapper ingress</tspan></text><rect x="238.75001" y="341.36561" width="105.666664" height="33.089283" fill="#ffff6c"/><rect x="238.75001" y="341.36561" width="105.666664" height="33.089283" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(243.75001 351.91025)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="37.83089" y="10" textLength="20.004883">view</tspan></text><rect x="238.75001" y="374.901" width="105.666664" height="18.656048" fill="#ffffa3"/><rect x="238.75001" y="374.901" width="105.666664" height="18.656048" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(243.75001 378.22901)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="3.0921211" y="10" textLength="89.48242">view mapper egress</tspan></text><rect x="238.75001" y="393.55704" width="105.666664" height="18.656048" fill="#ffffa3"/><rect x="238.75001" y="393.55704" width="105.666664" height="18.656048" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(243.75001 396.88507)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="8.9173164" y="10" textLength="77.83203">response adapter</tspan></text><rect x="238.75001" y="303.65604" width="105.666664" height="18.656048" fill="#ffffa3"/><rect x="238.75001" y="303.65604" width="105.666664" height="18.656048" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(243.75001 306.98407)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="6.702961" y="10" textLength="82.26074">decorators ingress</tspan></text><rect x="238.75001" y="412.1507" width="105.666664" height="18.656048" fill="#ffffa3"/><rect x="238.75001" y="412.1507" width="105.666664" height="18.656048" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(243.75001 415.47873)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="7.813801" y="10" textLength="80.039062">decorators egress</tspan></text><rect x="238.83336" y="285" width="105.66669" height="18.656048" fill="#ffffa3"/><rect x="238.83336" y="285" width="105.66669" height="18.656048" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(243.83336 288.32802)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="19.202972" 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.41667" y1="402.88507" x2="375.5" y2="402.27232" stroke="#191919" stroke-linecap="round" stroke-linejoin="round" stroke-width="1" stroke-dasharray="1,4"/></g></g></svg>
diff --git a/docs/_static/pyramid_router.graffle b/docs/_static/pyramid_router.graffle
new file mode 100644
index 000000000..217878426
--- /dev/null
+++ b/docs/_static/pyramid_router.graffle
@@ -0,0 +1,1621 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>ActiveLayerIndex</key>
+ <integer>0</integer>
+ <key>ApplicationVersion</key>
+ <array>
+ <string>com.omnigroup.OmniGrafflePro</string>
+ <string>139.18.0.187838</string>
+ </array>
+ <key>AutoAdjust</key>
+ <true/>
+ <key>BackgroundGraphic</key>
+ <dict>
+ <key>Bounds</key>
+ <string>{{0, 0}, {576, 733}}</string>
+ <key>Class</key>
+ <string>SolidGraphic</string>
+ <key>ID</key>
+ <integer>2</integer>
+ <key>Style</key>
+ <dict>
+ <key>shadow</key>
+ <dict>
+ <key>Draws</key>
+ <string>NO</string>
+ </dict>
+ <key>stroke</key>
+ <dict>
+ <key>Draws</key>
+ <string>NO</string>
+ </dict>
+ </dict>
+ </dict>
+ <key>BaseZoom</key>
+ <integer>0</integer>
+ <key>CanvasOrigin</key>
+ <string>{0, 0}</string>
+ <key>ColumnAlign</key>
+ <integer>1</integer>
+ <key>ColumnSpacing</key>
+ <real>36</real>
+ <key>CreationDate</key>
+ <string>2014-12-01 08:25:13 +0000</string>
+ <key>Creator</key>
+ <string>Steve Piercy</string>
+ <key>DisplayScale</key>
+ <string>1 0/72 in = 1 0/72 in</string>
+ <key>GraphDocumentVersion</key>
+ <integer>8</integer>
+ <key>GraphicsList</key>
+ <array>
+ <dict>
+ <key>Class</key>
+ <string>LineGraphic</string>
+ <key>ControlPoints</key>
+ <array>
+ <string>{0, 6.9840087890625}</string>
+ <string>{0, -9}</string>
+ </array>
+ <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>169413</integer>
+ </dict>
+ <key>ID</key>
+ <integer>169414</integer>
+ <key>Points</key>
+ <array>
+ <string>{202.04165903727232, 501.05557886759294}</string>
+ <string>{202.04165903727232, 528.77776209513161}</string>
+ </array>
+ <key>Style</key>
+ <dict>
+ <key>stroke</key>
+ <dict>
+ <key>Bezier</key>
+ <true/>
+ <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>SharpArrow</string>
+ <key>Legacy</key>
+ <true/>
+ <key>LineType</key>
+ <integer>1</integer>
+ <key>TailArrow</key>
+ <string>0</string>
+ </dict>
+ </dict>
+ <key>Tail</key>
+ <dict>
+ <key>ID</key>
+ <integer>169412</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{104.41666666666686, 528.77776209513161}, {195.24998474121094, 29}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>169413</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>
+ </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 Return the
+\b response}</string>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{104.41666666666657, 471.55557886759294}, {195.24998474121094, 29}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>169412</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.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>
+ </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 Invoke the
+\b view callable
+\b0 ,\
+which returns a
+\b response}</string>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{291.21562524160186, 379.55555343627816}, {26, 24}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>FitText</key>
+ <string>YES</string>
+ <key>Flow</key>
+ <string>Resize</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>ID</key>
+ <integer>169411</integer>
+ <key>Line</key>
+ <dict>
+ <key>ID</key>
+ <integer>169410</integer>
+ <key>Offset</key>
+ <real>7.3333320617675781</real>
+ <key>Position</key>
+ <real>0.4865129292011261</real>
+ <key>RotationType</key>
+ <integer>0</integer>
+ </dict>
+ <key>Shape</key>
+ <string>Rectangle</string>
+ <key>Style</key>
+ <dict>
+ <key>fill</key>
+ <dict>
+ <key>Draws</key>
+ <string>NO</string>
+ </dict>
+ <key>shadow</key>
+ <dict>
+ <key>Draws</key>
+ <string>NO</string>
+ </dict>
+ <key>stroke</key>
+ <dict>
+ <key>Draws</key>
+ <string>NO</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\pardirnatural\qc
+
+\f0\fs24 \cf0 No}</string>
+ </dict>
+ <key>Wrap</key>
+ <string>NO</string>
+ </dict>
+ <dict>
+ <key>Class</key>
+ <string>LineGraphic</string>
+ <key>ControlPoints</key>
+ <array>
+ <string>{34.791667904111534, 0}</string>
+ <string>{-33.999994913736998, 0}</string>
+ </array>
+ <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>169409</integer>
+ </dict>
+ <key>ID</key>
+ <integer>169410</integer>
+ <key>Points</key>
+ <array>
+ <string>{280.85416589389337, 398.88888549804574}</string>
+ <string>{327.47912214508739, 398.88888549804574}</string>
+ </array>
+ <key>Style</key>
+ <dict>
+ <key>stroke</key>
+ <dict>
+ <key>Bezier</key>
+ <true/>
+ <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>SharpArrow</string>
+ <key>Legacy</key>
+ <true/>
+ <key>LineType</key>
+ <integer>1</integer>
+ <key>TailArrow</key>
+ <string>0</string>
+ </dict>
+ </dict>
+ <key>Tail</key>
+ <dict>
+ <key>ID</key>
+ <integer>169404</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{327.47912214508739, 384.38888549804574}, {156.62496948242188, 29}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>169409</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.756045</string>
+ <key>g</key>
+ <string>0.75004</string>
+ <key>r</key>
+ <string>0.994455</string>
+ </dict>
+ </dict>
+ <key>shadow</key>
+ <dict>
+ <key>Draws</key>
+ <string>NO</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 Return the
+\b Forbidden View}</string>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{175.11595161998204, 438.9999954213917}, {30, 24}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>FitText</key>
+ <string>YES</string>
+ <key>Flow</key>
+ <string>Resize</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>ID</key>
+ <integer>169408</integer>
+ <key>Shape</key>
+ <string>Rectangle</string>
+ <key>Style</key>
+ <dict>
+ <key>fill</key>
+ <dict>
+ <key>Draws</key>
+ <string>NO</string>
+ </dict>
+ <key>shadow</key>
+ <dict>
+ <key>Draws</key>
+ <string>NO</string>
+ </dict>
+ <key>stroke</key>
+ <dict>
+ <key>Draws</key>
+ <string>NO</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\pardirnatural\qc
+
+\f0\fs24 \cf0 Yes}</string>
+ </dict>
+ <key>Wrap</key>
+ <string>NO</string>
+ </dict>
+ <dict>
+ <key>Class</key>
+ <string>LineGraphic</string>
+ <key>ControlPoints</key>
+ <array>
+ <string>{0, 6.9840087890625}</string>
+ <string>{0, -9}</string>
+ </array>
+ <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>169412</integer>
+ <key>Info</key>
+ <integer>2</integer>
+ </dict>
+ <key>ID</key>
+ <integer>169407</integer>
+ <key>Points</key>
+ <array>
+ <string>{202.04165267944353, 437.33333079020139}</string>
+ <string>{202.04165903727204, 471.55557886759294}</string>
+ </array>
+ <key>Style</key>
+ <dict>
+ <key>stroke</key>
+ <dict>
+ <key>Bezier</key>
+ <true/>
+ <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>SharpArrow</string>
+ <key>Legacy</key>
+ <true/>
+ <key>LineType</key>
+ <integer>1</integer>
+ <key>TailArrow</key>
+ <string>0</string>
+ </dict>
+ </dict>
+ <key>Tail</key>
+ <dict>
+ <key>ID</key>
+ <integer>169404</integer>
+ <key>Info</key>
+ <integer>1</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{171.708317756653, 329.24978243601743}, {30, 24}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>FitText</key>
+ <string>YES</string>
+ <key>Flow</key>
+ <string>Resize</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>ID</key>
+ <integer>169406</integer>
+ <key>Line</key>
+ <dict>
+ <key>ID</key>
+ <integer>169405</integer>
+ <key>Offset</key>
+ <real>-15.333334922790527</real>
+ <key>Position</key>
+ <real>0.45895844697952271</real>
+ <key>RotationType</key>
+ <integer>0</integer>
+ </dict>
+ <key>Shape</key>
+ <string>Rectangle</string>
+ <key>Style</key>
+ <dict>
+ <key>fill</key>
+ <dict>
+ <key>Draws</key>
+ <string>NO</string>
+ </dict>
+ <key>shadow</key>
+ <dict>
+ <key>Draws</key>
+ <string>NO</string>
+ </dict>
+ <key>stroke</key>
+ <dict>
+ <key>Draws</key>
+ <string>NO</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\pardirnatural\qc
+
+\f0\fs24 \cf0 Yes}</string>
+ </dict>
+ <key>Wrap</key>
+ <string>NO</string>
+ </dict>
+ <dict>
+ <key>Class</key>
+ <string>LineGraphic</string>
+ <key>ControlPoints</key>
+ <array>
+ <string>{0, 6.9840087890625}</string>
+ <string>{0, -9}</string>
+ </array>
+ <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>169404</integer>
+ <key>Info</key>
+ <integer>2</integer>
+ </dict>
+ <key>ID</key>
+ <integer>169405</integer>
+ <key>Points</key>
+ <array>
+ <string>{202.04165267944353, 326.72223360222029}</string>
+ <string>{202.04165267944353, 360.44446818033811}</string>
+ </array>
+ <key>Style</key>
+ <dict>
+ <key>stroke</key>
+ <dict>
+ <key>Bezier</key>
+ <true/>
+ <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>SharpArrow</string>
+ <key>Legacy</key>
+ <true/>
+ <key>LineType</key>
+ <integer>1</integer>
+ <key>TailArrow</key>
+ <string>0</string>
+ </dict>
+ </dict>
+ <key>Tail</key>
+ <dict>
+ <key>ID</key>
+ <integer>3</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{123.72916793823259, 360.44446818033811}, {156.62496948242188, 76.888862609863281}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>169404</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>Diamond</string>
+ <key>Style</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>
+ </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 Current user has
+\b authorization
+\b0 to invoke the view callable?}</string>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{283.07625736262997, 281.88889694213805}, {26, 24}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>FitText</key>
+ <string>YES</string>
+ <key>Flow</key>
+ <string>Resize</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>ID</key>
+ <integer>169403</integer>
+ <key>Line</key>
+ <dict>
+ <key>ID</key>
+ <integer>169402</integer>
+ <key>Offset</key>
+ <real>7.3333320617675781</real>
+ <key>Position</key>
+ <real>0.4865129292011261</real>
+ <key>RotationType</key>
+ <integer>0</integer>
+ </dict>
+ <key>Shape</key>
+ <string>Rectangle</string>
+ <key>Style</key>
+ <dict>
+ <key>fill</key>
+ <dict>
+ <key>Draws</key>
+ <string>NO</string>
+ </dict>
+ <key>shadow</key>
+ <dict>
+ <key>Draws</key>
+ <string>NO</string>
+ </dict>
+ <key>stroke</key>
+ <dict>
+ <key>Draws</key>
+ <string>NO</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\pardirnatural\qc
+
+\f0\fs24 \cf0 No}</string>
+ </dict>
+ <key>Wrap</key>
+ <string>NO</string>
+ </dict>
+ <dict>
+ <key>Class</key>
+ <string>LineGraphic</string>
+ <key>ControlPoints</key>
+ <array>
+ <string>{34.791667904111534, 0}</string>
+ <string>{-33.999994913736998, 0}</string>
+ </array>
+ <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>169401</integer>
+ </dict>
+ <key>ID</key>
+ <integer>169402</integer>
+ <key>Points</key>
+ <array>
+ <string>{265.20833208871704, 301.22222900390562}</string>
+ <string>{327.47911580403627, 301.22222900390562}</string>
+ </array>
+ <key>Style</key>
+ <dict>
+ <key>stroke</key>
+ <dict>
+ <key>Bezier</key>
+ <true/>
+ <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>SharpArrow</string>
+ <key>Legacy</key>
+ <true/>
+ <key>LineType</key>
+ <integer>1</integer>
+ <key>TailArrow</key>
+ <string>0</string>
+ </dict>
+ </dict>
+ <key>Tail</key>
+ <dict>
+ <key>ID</key>
+ <integer>3</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{327.47911580403627, 286.72222900390562}, {156.62496948242188, 29}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>169401</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.756045</string>
+ <key>g</key>
+ <string>0.75004</string>
+ <key>r</key>
+ <string>0.994455</string>
+ </dict>
+ </dict>
+ <key>shadow</key>
+ <dict>
+ <key>Draws</key>
+ <string>NO</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 Return the
+\b Not Found View}</string>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Class</key>
+ <string>LineGraphic</string>
+ <key>ControlPoints</key>
+ <array>
+ <string>{0, 6.9840087890625}</string>
+ <string>{0, -9}</string>
+ </array>
+ <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>3</integer>
+ <key>Info</key>
+ <integer>2</integer>
+ </dict>
+ <key>ID</key>
+ <integer>169400</integer>
+ <key>Points</key>
+ <array>
+ <string>{202.04165903727255, 251}</string>
+ <string>{202.04165776570633, 276.22223154703772}</string>
+ </array>
+ <key>Style</key>
+ <dict>
+ <key>stroke</key>
+ <dict>
+ <key>Bezier</key>
+ <true/>
+ <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>SharpArrow</string>
+ <key>Legacy</key>
+ <true/>
+ <key>LineType</key>
+ <integer>1</integer>
+ <key>TailArrow</key>
+ <string>0</string>
+ </dict>
+ </dict>
+ <key>Tail</key>
+ <dict>
+ <key>ID</key>
+ <integer>169393</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{139.37498982747391, 276.22223154703778}, {125.33333587646484, 50}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>3</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>Diamond</string>
+ <key>Style</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>
+ </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 callable found?}</string>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Class</key>
+ <string>LineGraphic</string>
+ <key>ControlPoints</key>
+ <array>
+ <string>{0, 6.9840087890625}</string>
+ <string>{0, -9}</string>
+ </array>
+ <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>169393</integer>
+ <key>Info</key>
+ <integer>2</integer>
+ </dict>
+ <key>ID</key>
+ <integer>169396</integer>
+ <key>Points</key>
+ <array>
+ <string>{202.04165903727255, 196.77777862548834}</string>
+ <string>{202.04165903727255, 222}</string>
+ </array>
+ <key>Style</key>
+ <dict>
+ <key>stroke</key>
+ <dict>
+ <key>Bezier</key>
+ <true/>
+ <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>SharpArrow</string>
+ <key>Legacy</key>
+ <true/>
+ <key>LineType</key>
+ <integer>1</integer>
+ <key>TailArrow</key>
+ <string>0</string>
+ </dict>
+ </dict>
+ <key>Tail</key>
+ <dict>
+ <key>ID</key>
+ <integer>169392</integer>
+ <key>Info</key>
+ <integer>1</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Class</key>
+ <string>LineGraphic</string>
+ <key>ControlPoints</key>
+ <array>
+ <string>{0, 6.9840087890625}</string>
+ <string>{0, -9}</string>
+ </array>
+ <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>169392</integer>
+ </dict>
+ <key>ID</key>
+ <integer>169395</integer>
+ <key>Points</key>
+ <array>
+ <string>{202.04165903727255, 142.55555725097662}</string>
+ <string>{202.04165903727255, 167.77777862548834}</string>
+ </array>
+ <key>Style</key>
+ <dict>
+ <key>stroke</key>
+ <dict>
+ <key>Bezier</key>
+ <true/>
+ <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>SharpArrow</string>
+ <key>Legacy</key>
+ <true/>
+ <key>LineType</key>
+ <integer>1</integer>
+ <key>TailArrow</key>
+ <string>0</string>
+ </dict>
+ </dict>
+ <key>Tail</key>
+ <dict>
+ <key>ID</key>
+ <integer>169391</integer>
+ <key>Info</key>
+ <integer>1</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Class</key>
+ <string>LineGraphic</string>
+ <key>ControlPoints</key>
+ <array>
+ <string>{0, 6.9840087890625}</string>
+ <string>{0, -9}</string>
+ </array>
+ <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>169391</integer>
+ </dict>
+ <key>ID</key>
+ <integer>169385</integer>
+ <key>Points</key>
+ <array>
+ <string>{202.04165903727255, 82.666667938232479}</string>
+ <string>{202.04165903727255, 107.88888931274418}</string>
+ </array>
+ <key>Style</key>
+ <dict>
+ <key>stroke</key>
+ <dict>
+ <key>Bezier</key>
+ <true/>
+ <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>SharpArrow</string>
+ <key>Legacy</key>
+ <true/>
+ <key>LineType</key>
+ <integer>1</integer>
+ <key>TailArrow</key>
+ <string>0</string>
+ </dict>
+ </dict>
+ <key>Tail</key>
+ <dict>
+ <key>ID</key>
+ <integer>19</integer>
+ <key>Info</key>
+ <integer>1</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{104.41666666666708, 222}, {195.24998474121094, 29}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>169393</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.815377</string>
+ <key>g</key>
+ <string>1</string>
+ <key>r</key>
+ <string>0.820561</string>
+ </dict>
+ </dict>
+ <key>shadow</key>
+ <dict>
+ <key>Draws</key>
+ <string>NO</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 Look up a
+\b view callable
+\b0 in the
+\b registry
+\b0 using the
+\b context
+\b0 and
+\b view name}</string>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{104.41666666666708, 167.77777862548834}, {195.24998474121094, 29}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>169392</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.815377</string>
+ <key>g</key>
+ <string>1</string>
+ <key>r</key>
+ <string>0.820561</string>
+ </dict>
+ </dict>
+ <key>shadow</key>
+ <dict>
+ <key>Draws</key>
+ <string>NO</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\b\fs20 \cf0 Traversal
+\b0 locates\
+the
+\b context
+\b0 and
+\b view name}</string>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{104.41666666666708, 107.88888931274418}, {195.24998474121094, 34.666667938232422}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>169391</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.815377</string>
+ <key>g</key>
+ <string>1</string>
+ <key>r</key>
+ <string>0.820561</string>
+ </dict>
+ </dict>
+ <key>shadow</key>
+ <dict>
+ <key>Draws</key>
+ <string>NO</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 Traverse the model graph\
+from the
+\b root
+\b0 using the
+\b path}</string>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{104.41666666666708, 48.000000000000043}, {195.24998474121094, 34.666667938232422}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>19</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.815377</string>
+ <key>g</key>
+ <string>1</string>
+ <key>r</key>
+ <string>0.820561</string>
+ </dict>
+ </dict>
+ <key>shadow</key>
+ <dict>
+ <key>Draws</key>
+ <string>NO</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 Obtain a root object from the
+\b root factory}</string>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{229.04165903727255, 20.000000000000934}, {90, 14}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>FitText</key>
+ <string>YES</string>
+ <key>Flow</key>
+ <string>Resize</string>
+ <key>FontInfo</key>
+ <dict>
+ <key>Font</key>
+ <string>Helvetica</string>
+ <key>Size</key>
+ <real>12</real>
+ </dict>
+ <key>ID</key>
+ <integer>169390</integer>
+ <key>Shape</key>
+ <string>Rectangle</string>
+ <key>Style</key>
+ <dict>
+ <key>fill</key>
+ <dict>
+ <key>Draws</key>
+ <string>NO</string>
+ </dict>
+ <key>shadow</key>
+ <dict>
+ <key>Draws</key>
+ <string>NO</string>
+ </dict>
+ <key>stroke</key>
+ <dict>
+ <key>Draws</key>
+ <string>NO</string>
+ </dict>
+ </dict>
+ <key>Text</key>
+ <dict>
+ <key>Pad</key>
+ <integer>0</integer>
+ <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\pardirnatural\qc
+
+\f0\b\fs24 \cf0 &lt;%Canvas%&gt;}</string>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ <key>Wrap</key>
+ <string>NO</string>
+ </dict>
+ </array>
+ <key>GridInfo</key>
+ <dict/>
+ <key>GuidesLocked</key>
+ <string>NO</string>
+ <key>GuidesVisible</key>
+ <string>YES</string>
+ <key>HPages</key>
+ <integer>1</integer>
+ <key>ImageCounter</key>
+ <integer>1</integer>
+ <key>KeepToScale</key>
+ <false/>
+ <key>Layers</key>
+ <array>
+ <dict>
+ <key>Lock</key>
+ <string>NO</string>
+ <key>Name</key>
+ <string>Layer 1</string>
+ <key>Print</key>
+ <string>YES</string>
+ <key>View</key>
+ <string>YES</string>
+ </dict>
+ </array>
+ <key>LayoutInfo</key>
+ <dict>
+ <key>Animate</key>
+ <string>NO</string>
+ <key>circoMinDist</key>
+ <real>18</real>
+ <key>circoSeparation</key>
+ <real>0.0</real>
+ <key>layoutEngine</key>
+ <string>dot</string>
+ <key>neatoSeparation</key>
+ <real>0.0</real>
+ <key>twopiSeparation</key>
+ <real>0.0</real>
+ </dict>
+ <key>LinksVisible</key>
+ <string>NO</string>
+ <key>MagnetsVisible</key>
+ <string>NO</string>
+ <key>MasterSheets</key>
+ <array/>
+ <key>ModificationDate</key>
+ <string>2014-12-01 09:19:51 +0000</string>
+ <key>Modifier</key>
+ <string>Steve Piercy</string>
+ <key>NotesVisible</key>
+ <string>NO</string>
+ <key>Orientation</key>
+ <integer>2</integer>
+ <key>OriginVisible</key>
+ <string>NO</string>
+ <key>PageBreaks</key>
+ <string>YES</string>
+ <key>PrintInfo</key>
+ <dict>
+ <key>NSBottomMargin</key>
+ <array>
+ <string>float</string>
+ <string>41</string>
+ </array>
+ <key>NSHorizonalPagination</key>
+ <array>
+ <string>coded</string>
+ <string>BAtzdHJlYW10eXBlZIHoA4QBQISEhAhOU051bWJlcgCEhAdOU1ZhbHVlAISECE5TT2JqZWN0AIWEASqEhAFxlwCG</string>
+ </array>
+ <key>NSLeftMargin</key>
+ <array>
+ <string>float</string>
+ <string>18</string>
+ </array>
+ <key>NSPaperSize</key>
+ <array>
+ <string>size</string>
+ <string>{612, 792}</string>
+ </array>
+ <key>NSPrintReverseOrientation</key>
+ <array>
+ <string>int</string>
+ <string>0</string>
+ </array>
+ <key>NSRightMargin</key>
+ <array>
+ <string>float</string>
+ <string>18</string>
+ </array>
+ <key>NSTopMargin</key>
+ <array>
+ <string>float</string>
+ <string>18</string>
+ </array>
+ </dict>
+ <key>PrintOnePage</key>
+ <false/>
+ <key>ReadOnly</key>
+ <string>NO</string>
+ <key>RowAlign</key>
+ <integer>1</integer>
+ <key>RowSpacing</key>
+ <real>36</real>
+ <key>SheetTitle</key>
+ <string>Pyramid Router</string>
+ <key>SmartAlignmentGuidesActive</key>
+ <string>YES</string>
+ <key>SmartDistanceGuidesActive</key>
+ <string>YES</string>
+ <key>UniqueID</key>
+ <integer>1</integer>
+ <key>UseEntirePage</key>
+ <false/>
+ <key>VPages</key>
+ <integer>1</integer>
+ <key>WindowInfo</key>
+ <dict>
+ <key>CurrentSheet</key>
+ <integer>0</integer>
+ <key>ExpandedCanvases</key>
+ <array>
+ <dict>
+ <key>name</key>
+ <string>Pyramid Router</string>
+ </dict>
+ </array>
+ <key>Frame</key>
+ <string>{{96, 20}, {1076, 1286}}</string>
+ <key>ListView</key>
+ <false/>
+ <key>OutlineWidth</key>
+ <integer>142</integer>
+ <key>RightSidebar</key>
+ <true/>
+ <key>ShowRuler</key>
+ <true/>
+ <key>Sidebar</key>
+ <true/>
+ <key>SidebarWidth</key>
+ <integer>120</integer>
+ <key>VisibleRegion</key>
+ <string>{{8, -10}, {532, 754.66666666666663}}</string>
+ <key>Zoom</key>
+ <real>1.5</real>
+ <key>ZoomValues</key>
+ <array>
+ <array>
+ <string>Pyramid Router</string>
+ <real>1.5</real>
+ <real>1</real>
+ </array>
+ </array>
+ </dict>
+</dict>
+</plist>
diff --git a/docs/_static/pyramid_router.png b/docs/_static/pyramid_router.png
new file mode 100644
index 000000000..3c9f81158
--- /dev/null
+++ b/docs/_static/pyramid_router.png
Binary files differ
diff --git a/docs/_static/pyramid_router.svg b/docs/_static/pyramid_router.svg
new file mode 100644
index 000000000..1537777c9
--- /dev/null
+++ b/docs/_static/pyramid_router.svg
@@ -0,0 +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="93 11 403 558" width="403pt" height="558pt"><metadata xmlns:dc="http://purl.org/dc/elements/1.1/"><dc:date>2014-12-01 09:19Z</dc:date><!-- Produced by OmniGraffle Professional 5.4.4 --></metadata><defs><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="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="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><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="12" 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></defs><g stroke="none" stroke-opacity="1" stroke-dasharray="none" fill="none" fill-opacity="1"><title>Pyramid Router</title><rect fill="white" width="576" height="733"/><g><title>Layer 1</title><text transform="translate(229.04166 20)" fill="black"><tspan font-family="Helvetica" font-size="12" font-weight="bold" x=".32226562" y="11" textLength="89.35547">Pyramid Router</tspan></text><rect x="104.416667" y="48" width="195.24998" height="34.666668" fill="#d2ffd0"/><rect x="104.416667" y="48" width="195.24998" height="34.666668" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(109.416667 59.333334)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x=".088371277" y="10" textLength="129.51172">Obtain a root object from the </tspan><tspan font-family="Helvetica" font-size="10" font-weight="bold" x="129.60009" y="10" textLength="55.561523">root factory</tspan></text><rect x="104.416667" y="107.88889" width="195.24998" height="34.666668" fill="#d2ffd0"/><rect x="104.416667" y="107.88889" width="195.24998" height="34.666668" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(109.416667 113.22222)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="35.557121" y="10" textLength="6.1083984">T</tspan><tspan font-family="Helvetica" font-size="10" font-weight="500" x="41.29931" y="10" textLength="108.393555">raverse the model graph</tspan><tspan font-family="Helvetica" font-size="10" font-weight="500" x="29.551262" y="22" textLength="39.458008">from the </tspan><tspan font-family="Helvetica" font-size="10" font-weight="bold" x="69.00927" y="22" textLength="19.438477">root</tspan><tspan font-family="Helvetica" font-size="10" font-weight="500" x="88.447746" y="22" textLength="46.142578"> using the </tspan><tspan font-family="Helvetica" font-size="10" font-weight="bold" x="134.590324" y="22" textLength="21.108398">path</tspan></text><rect x="104.416667" y="167.77778" width="195.24998" height="29" fill="#d2ffd0"/><rect x="104.416667" y="167.77778" width="195.24998" height="29" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(109.416667 170.27778)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="bold" x="53.428215" y="10" textLength="6.1083984">T</tspan><tspan font-family="Helvetica" font-size="10" font-weight="bold" x="58.98974" y="10" textLength="38.36914">raversal</tspan><tspan font-family="Helvetica" font-size="10" font-weight="500" x="97.35888" y="10" textLength="34.46289"> locates</tspan><tspan font-family="Helvetica" font-size="10" font-weight="500" x="30.093254" y="22" textLength="16.6796875">the </tspan><tspan font-family="Helvetica" font-size="10" font-weight="bold" x="46.772942" y="22" textLength="35.561523">context</tspan><tspan font-family="Helvetica" font-size="10" font-weight="500" x="82.334465" y="22" textLength="22.241211"> and </tspan><tspan font-family="Helvetica" font-size="10" font-weight="bold" x="104.575676" y="22" textLength="50.581055">view name</tspan></text><rect x="104.416667" y="222" width="195.24998" height="29" fill="#d2ffd0"/><rect x="104.416667" y="222" width="195.24998" height="29" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(109.416667 224.5)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="5.3471603" y="10" textLength="46.704102">Look up a </tspan><tspan font-family="Helvetica" font-size="10" font-weight="bold" x="52.051262" y="10" textLength="61.14746">view callable</tspan><tspan font-family="Helvetica" font-size="10" font-weight="500" x="113.19872" y="10" textLength="30.019531"> in the </tspan><tspan font-family="Helvetica" font-size="10" font-weight="bold" x="143.21825" y="10" textLength="36.68457">registry</tspan><tspan font-family="Helvetica" font-size="10" font-weight="500" x="179.90282" y="10" textLength="2.7783203"> </tspan><tspan font-family="Helvetica" font-size="10" font-weight="500" x="16.750969" y="22" textLength="43.364258">using the </tspan><tspan font-family="Helvetica" font-size="10" font-weight="bold" x="60.115227" y="22" textLength="35.561523">context</tspan><tspan font-family="Helvetica" font-size="10" font-weight="500" x="95.67675" y="22" textLength="22.241211"> and </tspan><tspan font-family="Helvetica" font-size="10" font-weight="bold" x="117.91796" y="22" textLength="50.581055">view name</tspan></text><path d="M 202.04166 82.66667 C 202.04166 87.86648 202.04166 94.31586 202.04166 100.98615" marker-end="url(#SharpArrow_Marker)" stroke="#191919" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><path d="M 202.04166 142.55556 C 202.04166 147.75537 202.04166 154.20475 202.04166 160.87504" marker-end="url(#SharpArrow_Marker)" stroke="#191919" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><path d="M 202.04166 196.77778 C 202.04166 201.97759 202.04166 208.42697 202.04166 215.09726" marker-end="url(#SharpArrow_Marker)" stroke="#191919" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><path d="M 202.04166 276.22223 L 264.70833 301.22223 L 202.04166 326.22223 L 139.37499 301.22223 Z" fill="#ffff6c"/><path d="M 202.04166 276.22223 L 264.70833 301.22223 L 202.04166 326.22223 L 139.37499 301.22223 Z" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(161.29499 288.72223)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="12.905763" y="10" textLength="6.669922">V</tspan><tspan font-family="Helvetica" font-size="10" font-weight="500" x="19.399903" y="10" textLength="54.472656">iew callable </tspan><tspan font-family="Helvetica" font-size="10" font-weight="500" x="26.707032" y="22" textLength="30.585938">found?</tspan></text><path d="M 202.04166 251 C 202.04166 256.19981 202.04166 262.6492 202.04166 269.31949" marker-end="url(#SharpArrow_Marker)" stroke="#191919" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><rect x="327.47912" y="286.72223" width="156.62497" height="29" fill="#fec0c1"/><rect x="327.47912" y="286.72223" width="156.62497" height="29" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(332.47912 295.22223)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="10.89061" y="10" textLength="49.472656">Return the </tspan><tspan font-family="Helvetica" font-size="10" font-weight="bold" x="60.363266" y="10" textLength="59.42871">Not Found V</tspan><tspan font-family="Helvetica" font-size="10" font-weight="bold" x="119.616196" y="10" textLength="16.118164">iew</tspan></text><path d="M 265.20833 301.22223 C 297.4314 301.22223 294.2168 301.22223 320.5783 301.22223" marker-end="url(#SharpArrow_Marker)" stroke="#191919" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(288.07626 286.8889)" fill="black"><tspan font-family="Helvetica" font-size="12" font-weight="500" fill="black" x=".33007812" y="11" textLength="15.339844">No</tspan></text><path d="M 202.04165 360.44447 L 280.35414 398.8889 L 202.04165 437.33333 L 123.72917 398.8889 Z" fill="#ffff6c"/><path d="M 202.04165 360.44447 L 280.35414 398.8889 L 202.04165 437.33333 L 123.72917 398.8889 Z" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(149.87354 380.12)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="16.495594" y="10" textLength="77.25586">Current user has </tspan><tspan font-family="Helvetica" font-size="10" font-weight="bold" x=".9462776" y="22" textLength="62.773438">authorization</tspan><tspan font-family="Helvetica" font-size="10" font-weight="500" x="63.719715" y="22" textLength="45.581055"> to invoke </tspan><tspan font-family="Helvetica" font-size="10" font-weight="500" x="14.26659" y="34" textLength="78.935547">the view callable?</tspan></text><path d="M 202.04165 326.72223 C 202.04165 332.1894 202.04165 344.2467 202.04165 353.54354" marker-end="url(#SharpArrow_Marker)" stroke="#191919" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(176.70832 334.24978)" fill="black"><tspan font-family="Helvetica" font-size="12" font-weight="500" fill="black" x=".21191406" y="11" textLength="8.0039062">Y</tspan><tspan font-family="Helvetica" font-size="12" font-weight="500" fill="black" x="7.114258" y="11" textLength="12.673828">es</tspan></text><path d="M 202.04165 437.33333 C 202.04165 442.81278 202.04166 455.21977 202.04166 464.65762" marker-end="url(#SharpArrow_Marker)" stroke="#191919" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(180.11595 444)" fill="black"><tspan font-family="Helvetica" font-size="12" font-weight="500" x=".21191406" y="11" textLength="8.0039062">Y</tspan><tspan font-family="Helvetica" font-size="12" font-weight="500" x="7.114258" y="11" textLength="12.673828">es</tspan></text><rect x="327.47912" y="384.38889" width="156.62497" height="29" fill="#fec0c1"/><rect x="327.47912" y="384.38889" width="156.62497" height="29" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(332.47912 392.88889)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="11.439926" y="10" textLength="49.472656">Return the </tspan><tspan font-family="Helvetica" font-size="10" font-weight="bold" x="60.912582" y="10" textLength="58.330078">Forbidden V</tspan><tspan font-family="Helvetica" font-size="10" font-weight="bold" x="119.06688" y="10" textLength="16.118164">iew</tspan></text><path d="M 280.85417 398.88889 C 312.9685 398.88889 296.55343 398.88889 320.57617 398.88889" marker-end="url(#SharpArrow_Marker)" stroke="#191919" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(296.21563 384.55555)" fill="black"><tspan font-family="Helvetica" font-size="12" font-weight="500" x=".33007812" y="11" textLength="15.339844">No</tspan></text><rect x="104.416667" y="471.55558" width="195.24998" height="29" fill="#ffff6c"/><rect x="104.416667" y="471.55558" width="195.24998" height="29" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(109.416667 474.05558)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="36.201653" y="10" textLength="48.920898">Invoke the </tspan><tspan font-family="Helvetica" font-size="10" font-weight="bold" x="85.12255" y="10" textLength="61.14746">view callable</tspan><tspan font-family="Helvetica" font-size="10" font-weight="500" x="146.27001" y="10" textLength="2.7783203">,</tspan><tspan font-family="Helvetica" font-size="10" font-weight="500" x="35.100578" y="22" textLength="70.585938">which returns a </tspan><tspan font-family="Helvetica" font-size="10" font-weight="bold" x="105.686516" y="22" textLength="44.46289">response</tspan></text><rect x="104.416667" y="528.77776" width="195.24998" height="29" fill="#dfbeff"/><rect x="104.416667" y="528.77776" width="195.24998" height="29" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(109.416667 537.27776)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="45.65722" y="10" textLength="49.472656">Return the </tspan><tspan font-family="Helvetica" font-size="10" font-weight="bold" x="95.129875" y="10" textLength="44.46289">response</tspan></text><path d="M 202.04166 501.05558 C 202.04166 506.35088 202.04166 514.3792 202.04166 521.8749" marker-end="url(#SharpArrow_Marker)" stroke="#191919" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/></g></g></svg>
diff --git a/docs/_themes b/docs/_themes
-Subproject 3bec9280a6cedb15e97e5899021aa8d723c2538
+Subproject 382cba80fbd6a7424818d17ec63ca520e485f10
diff --git a/docs/api/config.rst b/docs/api/config.rst
index 48dd2f0b9..ae913d32c 100644
--- a/docs/api/config.rst
+++ b/docs/api/config.rst
@@ -132,3 +132,8 @@
are being used.
.. autoclass:: not_
+
+.. attribute:: PHASE0_CONFIG
+.. attribute:: PHASE1_CONFIG
+.. attribute:: PHASE2_CONFIG
+.. attribute:: PHASE3_CONFIG
diff --git a/docs/api/exceptions.rst b/docs/api/exceptions.rst
index 0c630571f..faca0fbb6 100644
--- a/docs/api/exceptions.rst
+++ b/docs/api/exceptions.rst
@@ -5,14 +5,14 @@
.. automodule:: pyramid.exceptions
- .. autoclass:: BadCSRFToken
+ .. autoexception:: BadCSRFToken
- .. autoclass:: PredicateMismatch
+ .. autoexception:: PredicateMismatch
- .. autoclass:: Forbidden
+ .. autoexception:: Forbidden
- .. autoclass:: NotFound
+ .. autoexception:: NotFound
- .. autoclass:: ConfigurationError
+ .. autoexception:: ConfigurationError
- .. autoclass:: URLDecodeError
+ .. autoexception:: URLDecodeError
diff --git a/docs/api/httpexceptions.rst b/docs/api/httpexceptions.rst
index b50f10beb..d4cf97f1d 100644
--- a/docs/api/httpexceptions.rst
+++ b/docs/api/httpexceptions.rst
@@ -13,96 +13,96 @@
.. autofunction:: exception_response
- .. autoclass:: HTTPException
+ .. autoexception:: HTTPException
- .. autoclass:: HTTPOk
+ .. autoexception:: HTTPOk
- .. autoclass:: HTTPRedirection
+ .. autoexception:: HTTPRedirection
- .. autoclass:: HTTPError
+ .. autoexception:: HTTPError
- .. autoclass:: HTTPClientError
+ .. autoexception:: HTTPClientError
- .. autoclass:: HTTPServerError
+ .. autoexception:: HTTPServerError
- .. autoclass:: HTTPCreated
+ .. autoexception:: HTTPCreated
- .. autoclass:: HTTPAccepted
+ .. autoexception:: HTTPAccepted
- .. autoclass:: HTTPNonAuthoritativeInformation
+ .. autoexception:: HTTPNonAuthoritativeInformation
- .. autoclass:: HTTPNoContent
+ .. autoexception:: HTTPNoContent
- .. autoclass:: HTTPResetContent
+ .. autoexception:: HTTPResetContent
- .. autoclass:: HTTPPartialContent
+ .. autoexception:: HTTPPartialContent
- .. autoclass:: HTTPMultipleChoices
+ .. autoexception:: HTTPMultipleChoices
- .. autoclass:: HTTPMovedPermanently
+ .. autoexception:: HTTPMovedPermanently
- .. autoclass:: HTTPFound
+ .. autoexception:: HTTPFound
- .. autoclass:: HTTPSeeOther
+ .. autoexception:: HTTPSeeOther
- .. autoclass:: HTTPNotModified
+ .. autoexception:: HTTPNotModified
- .. autoclass:: HTTPUseProxy
+ .. autoexception:: HTTPUseProxy
- .. autoclass:: HTTPTemporaryRedirect
+ .. autoexception:: HTTPTemporaryRedirect
- .. autoclass:: HTTPBadRequest
+ .. autoexception:: HTTPBadRequest
- .. autoclass:: HTTPUnauthorized
+ .. autoexception:: HTTPUnauthorized
- .. autoclass:: HTTPPaymentRequired
+ .. autoexception:: HTTPPaymentRequired
- .. autoclass:: HTTPForbidden
+ .. autoexception:: HTTPForbidden
- .. autoclass:: HTTPNotFound
+ .. autoexception:: HTTPNotFound
- .. autoclass:: HTTPMethodNotAllowed
+ .. autoexception:: HTTPMethodNotAllowed
- .. autoclass:: HTTPNotAcceptable
+ .. autoexception:: HTTPNotAcceptable
- .. autoclass:: HTTPProxyAuthenticationRequired
+ .. autoexception:: HTTPProxyAuthenticationRequired
- .. autoclass:: HTTPRequestTimeout
+ .. autoexception:: HTTPRequestTimeout
- .. autoclass:: HTTPConflict
+ .. autoexception:: HTTPConflict
- .. autoclass:: HTTPGone
+ .. autoexception:: HTTPGone
- .. autoclass:: HTTPLengthRequired
+ .. autoexception:: HTTPLengthRequired
- .. autoclass:: HTTPPreconditionFailed
+ .. autoexception:: HTTPPreconditionFailed
- .. autoclass:: HTTPRequestEntityTooLarge
+ .. autoexception:: HTTPRequestEntityTooLarge
- .. autoclass:: HTTPRequestURITooLong
+ .. autoexception:: HTTPRequestURITooLong
- .. autoclass:: HTTPUnsupportedMediaType
+ .. autoexception:: HTTPUnsupportedMediaType
- .. autoclass:: HTTPRequestRangeNotSatisfiable
+ .. autoexception:: HTTPRequestRangeNotSatisfiable
- .. autoclass:: HTTPExpectationFailed
+ .. autoexception:: HTTPExpectationFailed
- .. autoclass:: HTTPUnprocessableEntity
+ .. autoexception:: HTTPUnprocessableEntity
- .. autoclass:: HTTPLocked
+ .. autoexception:: HTTPLocked
- .. autoclass:: HTTPFailedDependency
+ .. autoexception:: HTTPFailedDependency
- .. autoclass:: HTTPInternalServerError
+ .. autoexception:: HTTPInternalServerError
- .. autoclass:: HTTPNotImplemented
+ .. autoexception:: HTTPNotImplemented
- .. autoclass:: HTTPBadGateway
+ .. autoexception:: HTTPBadGateway
- .. autoclass:: HTTPServiceUnavailable
+ .. autoexception:: HTTPServiceUnavailable
- .. autoclass:: HTTPGatewayTimeout
+ .. autoexception:: HTTPGatewayTimeout
- .. autoclass:: HTTPVersionNotSupported
+ .. autoexception:: HTTPVersionNotSupported
- .. autoclass:: HTTPInsufficientStorage
+ .. autoexception:: HTTPInsufficientStorage
diff --git a/docs/api/index.rst b/docs/api/index.rst
new file mode 100644
index 000000000..cb38aa0b2
--- /dev/null
+++ b/docs/api/index.rst
@@ -0,0 +1,12 @@
+.. _html_api_documentation:
+
+API Documentation
+=================
+
+Comprehensive reference material for every public API exposed by :app:`Pyramid`:
+
+.. toctree::
+ :maxdepth: 1
+ :glob:
+
+ *
diff --git a/docs/api/interfaces.rst b/docs/api/interfaces.rst
index d8d935afd..de2a664a4 100644
--- a/docs/api/interfaces.rst
+++ b/docs/api/interfaces.rst
@@ -56,6 +56,9 @@ Other Interfaces
.. autointerface:: IRenderer
:members:
+ .. autointerface:: IResponseFactory
+ :members:
+
.. autointerface:: IViewMapperFactory
:members:
@@ -86,3 +89,5 @@ Other Interfaces
.. autointerface:: IResourceURL
:members:
+ .. autointerface:: ICacheBuster
+ :members:
diff --git a/docs/api/registry.rst b/docs/api/registry.rst
index bab3e26ba..57a80b3f5 100644
--- a/docs/api/registry.rst
+++ b/docs/api/registry.rst
@@ -14,6 +14,18 @@
accessed as ``request.registry.settings`` or
``config.registry.settings`` in a typical Pyramid application.
+ .. attribute:: package_name
+
+ .. versionadded:: 1.6
+
+ When a registry is set up (or created) by a :term:`Configurator`, this
+ attribute will be the shortcut for
+ :attr:`pyramid.config.Configurator.package_name`.
+
+ This attribute is often accessed as ``request.registry.package_name`` or
+ ``config.registry.package_name`` or ``config.package_name``
+ in a typical Pyramid application.
+
.. attribute:: introspector
.. versionadded:: 1.3
diff --git a/docs/api/request.rst b/docs/api/request.rst
index 77d80f6d6..b325ad076 100644
--- a/docs/api/request.rst
+++ b/docs/api/request.rst
@@ -167,37 +167,40 @@
.. versionadded:: 1.5
- A property which returns the userid of the currently authenticated user
- or ``None`` if there is no :term:`authentication policy` in effect or
- there is no currently authenticated user. This differs from
- :attr:`~pyramid.request.Request.unauthenticated_userid`, because the
- effective authentication policy will have ensured that a record
- associated with the userid exists in persistent storage; if it has
- not, this value will be ``None``.
+ A property which returns the :term:`userid` of the currently
+ authenticated user or ``None`` if there is no :term:`authentication
+ policy` in effect or there is no currently authenticated user. This
+ differs from :attr:`~pyramid.request.Request.unauthenticated_userid`,
+ because the effective authentication policy will have ensured that a
+ record associated with the :term:`userid` exists in persistent storage;
+ if it has not, this value will be ``None``.
.. attribute:: unauthenticated_userid
.. versionadded:: 1.5
A property which returns a value which represents the *claimed* (not
- verified) user id of the credentials present in the request. ``None`` if
- there is no :term:`authentication policy` in effect or there is no user
- data associated with the current request. This differs from
- :attr:`~pyramid.request.Request.authenticated_userid`, because the
- effective authentication policy will not ensure that a record associated
- with the userid exists in persistent storage. Even if the userid
- does not exist in persistent storage, this value will be the value
- of the userid *claimed* by the request data.
+ verified) :term:`userid` of the credentials present in the
+ request. ``None`` if there is no :term:`authentication policy` in effect
+ or there is no user data associated with the current request. This
+ differs from :attr:`~pyramid.request.Request.authenticated_userid`,
+ because the effective authentication policy will not ensure that a
+ record associated with the :term:`userid` exists in persistent storage.
+ Even if the :term:`userid` does not exist in persistent storage, this
+ value will be the value of the :term:`userid` *claimed* by the request
+ data.
.. attribute:: effective_principals
.. versionadded:: 1.5
A property which returns the list of 'effective' :term:`principal`
- identifiers for this request. This will include the userid of the
- currently authenticated user if a user is currently authenticated. If no
- :term:`authentication policy` is in effect, this will return a sequence
- containing only the :attr:`pyramid.security.Everyone` principal.
+ identifiers for this request. This list typically includes the
+ :term:`userid` of the currently authenticated user if a user is
+ currently authenticated, but this depends on the
+ :term:`authentication policy` in effect. If no :term:`authentication
+ policy` is in effect, this will return a sequence containing only the
+ :attr:`pyramid.security.Everyone` principal.
.. method:: invoke_subrequest(request, use_tweens=False)
@@ -366,3 +369,4 @@
that used as ``request.GET``, ``request.POST``, and ``request.params``),
see :class:`pyramid.interfaces.IMultiDict`.
+.. autofunction:: apply_request_extensions(request)
diff --git a/docs/api/security.rst b/docs/api/security.rst
index 814b68e5a..88086dbbf 100644
--- a/docs/api/security.rst
+++ b/docs/api/security.rst
@@ -16,7 +16,7 @@ Authentication API Functions
.. autofunction:: forget
-.. autofunction:: remember
+.. autofunction:: remember(request, userid, **kwargs)
Authorization API Functions
---------------------------
diff --git a/docs/api/static.rst b/docs/api/static.rst
index c28473584..b6b279139 100644
--- a/docs/api/static.rst
+++ b/docs/api/static.rst
@@ -9,3 +9,17 @@
:members:
:inherited-members:
+ .. autoclass:: PathSegmentCacheBuster
+ :members:
+
+ .. autoclass:: QueryStringCacheBuster
+ :members:
+
+ .. autoclass:: PathSegmentMd5CacheBuster
+ :members:
+
+ .. autoclass:: QueryStringMd5CacheBuster
+ :members:
+
+ .. autoclass:: QueryStringConstantCacheBuster
+ :members:
diff --git a/docs/conf.py b/docs/conf.py
index 4bc8e2172..fa4578275 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -74,7 +74,7 @@ intersphinx_mapping = {
'http://docs.pylonsproject.org/projects/deform/en/latest',
None),
'sqla': ('http://docs.sqlalchemy.org/en/latest', None),
- 'who': ('http://docs.repoze.org/who/latest', None),
+ 'who': ('http://repozewho.readthedocs.org/en/latest', None),
'python': ('http://docs.python.org', None),
'python3': ('http://docs.python.org/3', None),
'tstring':
diff --git a/docs/glossary.rst b/docs/glossary.rst
index deb4c1c8b..9c0ea8598 100644
--- a/docs/glossary.rst
+++ b/docs/glossary.rst
@@ -16,6 +16,11 @@ Glossary
An object which, provided a :term:`WSGI` environment as a single
positional argument, returns a Pyramid-compatible request.
+ response factory
+ An object which, provided a :term:`request` as a single positional
+ argument, returns a Pyramid-compatible response. See
+ :class:`pyramid.interfaces.IResponseFactory`.
+
response
An object returned by a :term:`view callable` that represents response
data returned to the requesting user agent. It must implement the
@@ -286,13 +291,22 @@ Glossary
:term:`authorization policy`.
principal
- A *principal* is a string or unicode object representing a userid
- or a group id. It is provided by an :term:`authentication
- policy`. For example, if a user had the user id "bob", and Bob
- was part of two groups named "group foo" and "group bar", the
- request might have information attached to it that would
- indicate that Bob was represented by three principals: "bob",
- "group foo" and "group bar".
+ A *principal* is a string or unicode object representing an
+ entity, typically a user or group. Principals are provided by an
+ :term:`authentication policy`. For example, if a user had the
+ :term:`userid` `"bob"`, and was part of two groups named `"group foo"`
+ and "group bar", the request might have information attached to
+ it that would indicate that Bob was represented by three
+ principals: `"bob"`, `"group foo"` and `"group bar"`.
+
+ userid
+ A *userid* is a string or unicode object used to identify and
+ authenticate a real-world user (or client). A userid is
+ supplied to an :term:`authentication policy` in order to discover
+ the user's :term:`principals <principal>`. The default behavior
+ of the authentication policies :app:`Pyramid` provides is to
+ return the user's userid as a principal, but this is not strictly
+ necessary in custom policies that define their principals differently.
authorization policy
An authorization policy in :app:`Pyramid` terms is a bit of
@@ -749,9 +763,16 @@ Glossary
made. For example the word "java" might be translated
differently if the translation domain is "programming-languages"
than would be if the translation domain was "coffee". A
- translation domain is represnted by a collection of ``.mo`` files
+ translation domain is represented by a collection of ``.mo`` files
within one or more :term:`translation directory` directories.
+ Translation Context
+ A string representing the "context" in which a translation was
+ made within a given :term:`translation domain`. See the gettext
+ documentation, `11.2.5 Using contexts for solving ambiguities
+ <https://www.gnu.org/software/gettext/manual/gettext.html#Contexts>`_
+ for more information.
+
Translator
A callable which receives a :term:`translation string` and returns a
translated Unicode object for the purposes of internationalization. A
diff --git a/docs/index.rst b/docs/index.rst
index ac16ff237..fc7560e8f 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -135,8 +135,6 @@ platforms.
tutorials/wiki/index.rst
tutorials/modwsgi/index.rst
-.. _html_api_documentation:
-
API Documentation
=================
@@ -146,6 +144,7 @@ Comprehensive reference material for every public API exposed by :app:`Pyramid`:
:maxdepth: 1
:glob:
+ api/index
api/*
Change History
diff --git a/docs/narr/MyProject/myproject/tests.py b/docs/narr/MyProject/myproject/tests.py
index 64dcab1d5..8c60407e5 100644
--- a/docs/narr/MyProject/myproject/tests.py
+++ b/docs/narr/MyProject/myproject/tests.py
@@ -15,3 +15,40 @@ class ViewTests(unittest.TestCase):
request = testing.DummyRequest()
info = my_view(request)
self.assertEqual(info['project'], 'MyProject')
+
+class ViewIntegrationTests(unittest.TestCase):
+ def setUp(self):
+ """ This sets up the application registry with the
+ registrations your application declares in its ``includeme``
+ function.
+ """
+ self.config = testing.setUp()
+ self.config.include('myproject')
+
+ def tearDown(self):
+ """ Clear out the application registry """
+ testing.tearDown()
+
+ def test_my_view(self):
+ from myproject.views import my_view
+ request = testing.DummyRequest()
+ result = my_view(request)
+ self.assertEqual(result.status, '200 OK')
+ body = result.app_iter[0]
+ self.assertTrue('Welcome to' in body)
+ self.assertEqual(len(result.headerlist), 2)
+ self.assertEqual(result.headerlist[0],
+ ('Content-Type', 'text/html; charset=UTF-8'))
+ self.assertEqual(result.headerlist[1], ('Content-Length',
+ str(len(body))))
+
+class FunctionalTests(unittest.TestCase):
+ def setUp(self):
+ from myproject import main
+ app = main({})
+ from webtest import TestApp
+ self.testapp = TestApp(app)
+
+ def test_root(self):
+ res = self.testapp.get('/', status=200)
+ self.assertTrue('Pyramid' in res.body)
diff --git a/docs/narr/MyProject/setup.py b/docs/narr/MyProject/setup.py
index 8c019af51..9f34540a7 100644
--- a/docs/narr/MyProject/setup.py
+++ b/docs/narr/MyProject/setup.py
@@ -1,30 +1,42 @@
-import os
+"""Setup for the MyProject package.
+"""
+import os
from setuptools import setup, find_packages
-here = os.path.abspath(os.path.dirname(__file__))
-with open(os.path.join(here, 'README.txt')) as f:
- README = f.read()
-with open(os.path.join(here, 'CHANGES.txt')) as f:
- CHANGES = f.read()
-requires = [
+HERE = os.path.abspath(os.path.dirname(__file__))
+
+
+with open(os.path.join(HERE, 'README.txt')) as fp:
+ README = fp.read()
+
+
+with open(os.path.join(HERE, 'CHANGES.txt')) as fp:
+ CHANGES = fp.read()
+
+
+REQUIRES = [
'pyramid',
'pyramid_chameleon',
'pyramid_debugtoolbar',
'waitress',
]
+TESTS_REQUIRE = [
+ 'webtest'
+ ]
+
setup(name='MyProject',
version='0.0',
description='MyProject',
long_description=README + '\n\n' + CHANGES,
classifiers=[
- "Programming Language :: Python",
- "Framework :: Pyramid",
- "Topic :: Internet :: WWW/HTTP",
- "Topic :: Internet :: WWW/HTTP :: WSGI :: Application",
- ],
+ 'Programming Language :: Python',
+ 'Framework :: Pyramid',
+ 'Topic :: Internet :: WWW/HTTP',
+ 'Topic :: Internet :: WWW/HTTP :: WSGI :: Application',
+ ],
author='',
author_email='',
url='',
@@ -32,11 +44,10 @@ setup(name='MyProject',
packages=find_packages(),
include_package_data=True,
zip_safe=False,
- install_requires=requires,
- tests_require=requires,
- test_suite="myproject",
+ install_requires=REQUIRES,
+ tests_require=TESTS_REQUIRE,
+ test_suite='myproject',
entry_points="""\
[paste.app_factory]
main = myproject:main
- """,
- )
+ """)
diff --git a/docs/narr/assets.rst b/docs/narr/assets.rst
index b0a8d18b0..d6bc8cbb8 100644
--- a/docs/narr/assets.rst
+++ b/docs/narr/assets.rst
@@ -276,15 +276,238 @@ to put static media on a separate webserver during production (if the
``name`` argument to :meth:`~pyramid.config.Configurator.add_static_view` is
a URL), while keeping static media package-internal and served by the
development webserver during development (if the ``name`` argument to
-:meth:`~pyramid.config.Configurator.add_static_view` is a URL prefix). To
-create such a circumstance, we suggest using the
-:attr:`pyramid.registry.Registry.settings` API in conjunction with a setting
-in the application ``.ini`` file named ``media_location``. Then set the
-value of ``media_location`` to either a prefix or a URL depending on whether
-the application is being run in development or in production (use a different
-``.ini`` file for production than you do for development). This is just a
-suggestion for a pattern; any setting name other than ``media_location``
-could be used.
+:meth:`~pyramid.config.Configurator.add_static_view` is a URL prefix).
+
+For example, we may define a :ref:`custom setting <adding_a_custom_setting>`
+named ``media_location`` which we can set to an external URL in production
+when our assets are hosted on a CDN.
+
+.. code-block:: python
+ :linenos:
+
+ media_location = settings.get('media_location', 'static')
+
+ config = Configurator(settings=settings)
+ config.add_static_view(path='myapp:static', name=media_location)
+
+Now we can optionally define the setting in our ini file:
+
+.. code-block:: ini
+ :linenos:
+
+ # production.ini
+ [app:main]
+ use = egg:myapp#main
+
+ media_location = http://static.example.com/
+
+It is also possible to serve assets that live outside of the source by
+referring to an absolute path on the filesystem. There are two ways to
+accomplish this.
+
+First, :meth:`~pyramid.config.Configurator.add_static_view`
+supports taking an absolute path directly instead of an asset spec. This works
+as expected, looking in the file or folder of files and serving them up at
+some URL within your application or externally. Unfortunately, this technique
+has a drawback that it is not possible to use the
+:meth:`~pyramid.request.Request.static_url` method to generate URLs, since it
+works based on an asset spec.
+
+The second approach, available in Pyramid 1.6+, uses the asset overriding
+APIs described in the :ref:`overriding_assets_section` section. It is then
+possible to configure a "dummy" package which then serves its file or folder
+from an absolute path.
+
+.. code-block:: python
+
+ config.add_static_view(path='myapp:static_images', name='static')
+ config.override_asset(to_override='myapp:static_images/',
+ override_with='/abs/path/to/images/')
+
+From this configuration it is now possible to use
+:meth:`~pyramid.request.Request.static_url` to generate URLs to the data
+in the folder by doing something like
+``request.static_url('myapp:static_images/foo.png')``. While it is not
+necessary that the ``static_images`` file or folder actually exist in the
+``myapp`` package, it is important that the ``myapp`` portion points to a
+valid package. If the folder does exist then the overriden folder is given
+priority if the file's name exists in both locations.
+
+.. index::
+ single: Cache Busting
+
+.. _cache_busting:
+
+Cache Busting
+-------------
+
+.. versionadded:: 1.6
+
+In order to maximize performance of a web application, you generally want to
+limit the number of times a particular client requests the same static asset.
+Ideally a client would cache a particular static asset "forever", requiring
+it to be sent to the client a single time. The HTTP protocol allows you to
+send headers with an HTTP response that can instruct a client to cache a
+particular asset for an amount of time. As long as the client has a copy of
+the asset in its cache and that cache hasn't expired, the client will use the
+cached copy rather than request a new copy from the server. The drawback to
+sending cache headers to the client for a static asset is that at some point
+the static asset may change, and then you'll want the client to load a new copy
+of the asset. Under normal circumstances you'd just need to wait for the
+client's cached copy to expire before they get the new version of the static
+resource.
+
+A commonly used workaround to this problem is a technique known as "cache
+busting". Cache busting schemes generally involve generating a URL for a
+static asset that changes when the static asset changes. This way headers can
+be sent along with the static asset instructing the client to cache the asset
+for a very long time. When a static asset is changed, the URL used to refer to
+it in a web page also changes, so the client sees it as a new resource and
+requests a copy, regardless of any caching policy set for the resource's old
+URL.
+
+:app:`Pyramid` can be configured to produce cache busting URLs for static
+assets by passing the optional argument, ``cachebust`` to
+:meth:`~pyramid.config.Configurator.add_static_view`:
+
+.. code-block:: python
+ :linenos:
+
+ # config is an instance of pyramid.config.Configurator
+ config.add_static_view(name='static', path='mypackage:folder/static',
+ cachebust=True)
+
+Setting the ``cachebust`` argument instructs :app:`Pyramid` to use a cache
+busting scheme which adds the md5 checksum for a static asset as a path segment
+in the asset's URL:
+
+.. code-block:: python
+ :linenos:
+
+ js_url = request.static_url('mypackage:folder/static/js/myapp.js')
+ # Returns: 'http://www.example.com/static/c9658b3c0a314a1ca21e5988e662a09e/js/myapp.js`
+
+When the asset changes, so will its md5 checksum, and therefore so will its
+URL. Supplying the ``cachebust`` argument also causes the static view to set
+headers instructing clients to cache the asset for ten years, unless the
+``max_cache_age`` argument is also passed, in which case that value is used.
+
+.. note::
+
+ md5 checksums are cached in RAM so if you change a static resource without
+ restarting your application, you may still generate URLs with a stale md5
+ checksum.
+
+Disabling the Cache Buster
+~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+It can be useful in some situations (e.g. development) to globally disable all
+configured cache busters without changing calls to
+:meth:`~pyramid.config.Configurator.add_static_view`. To do this set the
+``PYRAMID_PREVENT_CACHEBUST`` environment variable or the
+``pyramid.prevent_cachebust`` configuration value to a true value.
+
+Customizing the Cache Buster
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Revisiting from the previous section:
+
+.. code-block:: python
+ :linenos:
+
+ # config is an instance of pyramid.config.Configurator
+ config.add_static_view(name='static', path='mypackage:folder/static',
+ cachebust=True)
+
+Setting ``cachebust`` to ``True`` instructs :app:`Pyramid` to use a default
+cache busting implementation that should work for many situations. The
+``cachebust`` may be set to any object that implements the interface,
+:class:`~pyramid.interfaces.ICacheBuster`. The above configuration is exactly
+equivalent to:
+
+.. code-block:: python
+ :linenos:
+
+ from pyramid.static import PathSegmentMd5CacheBuster
+
+ # config is an instance of pyramid.config.Configurator
+ config.add_static_view(name='static', path='mypackage:folder/static',
+ cachebust=PathSegmentMd5CacheBuster())
+
+:app:`Pyramid` includes a handful of ready to use cache buster implementations:
+:class:`~pyramid.static.PathSegmentMd5CacheBuster`, which inserts an md5
+checksum token in the path portion of the asset's URL,
+:class:`~pyramid.static.QueryStringMd5CacheBuster`, which adds an md5 checksum
+token to the query string of the asset's URL, and
+:class:`~pyramid.static.QueryStringConstantCacheBuster`, which adds an
+arbitrary token you provide to the query string of the asset's URL.
+
+In order to implement your own cache buster, you can write your own class from
+scratch which implements the :class:`~pyramid.interfaces.ICacheBuster`
+interface. Alternatively you may choose to subclass one of the existing
+implementations. One of the most likely scenarios is you'd want to change the
+way the asset token is generated. To do this just subclass either
+:class:`~pyramid.static.PathSegmentCacheBuster` or
+:class:`~pyramid.static.QueryStringCacheBuster` and define a
+``tokenize(pathspec)`` method. Here is an example which just uses Git to get
+the hash of the currently checked out code:
+
+.. code-block:: python
+ :linenos:
+
+ import os
+ import subprocess
+ from pyramid.static import PathSegmentCacheBuster
+
+ class GitCacheBuster(PathSegmentCacheBuster):
+ """
+ Assuming your code is installed as a Git checkout, as opposed to as an
+ egg from an egg repository like PYPI, you can use this cachebuster to
+ get the current commit's SHA1 to use as the cache bust token.
+ """
+ def __init__(self):
+ here = os.path.dirname(os.path.abspath(__file__))
+ self.sha1 = subprocess.check_output(
+ ['git', 'rev-parse', 'HEAD'],
+ cwd=here).strip()
+
+ def tokenize(self, pathspec):
+ return self.sha1
+
+Choosing a Cache Buster
+~~~~~~~~~~~~~~~~~~~~~~~
+
+The default cache buster implementation,
+:class:`~pyramid.static.PathSegmentMd5CacheBuster`, works very well assuming
+that you're using :app:`Pyramid` to serve your static assets. The md5 checksum
+is fine grained enough that browsers should only request new versions of
+specific assets that have changed. Many caching HTTP proxies will fail to
+cache a resource if the URL contains a query string. In general, therefore,
+you should prefer a cache busting strategy which modifies the path segment to
+a strategy which adds a query string.
+
+It is possible, however, that your static assets are being served by another
+web server or externally on a CDN. In these cases modifying the path segment
+for a static asset URL would cause the external service to fail to find the
+asset, causing your customer to get a 404. In these cases you would need to
+fall back to a cache buster which adds a query string. It is even possible
+that there isn't a copy of your static assets available to the :app:`Pyramid`
+application, so a cache busting implementation that generates md5 checksums
+would fail since it can't access the assets. In such a case,
+:class:`~pyramid.static.QueryStringConstantCacheBuster` is a reasonable
+fallback. The following code would set up a cachebuster that just uses the
+time at start up as a cachebust token:
+
+.. code-block:: python
+ :linenos:
+
+ import time
+ from pyramid.static import QueryStringConstantCacheBuster
+
+ config.add_static_view(
+ name='http://mycdn.example.com/',
+ path='mypackage:static',
+ cachebust=QueryStringConstantCacheBuster(str(time.time())))
.. index::
single: static assets view
@@ -526,3 +749,6 @@ files. Any software which uses the
:func:`pkg_resources.get_resource_string` APIs will obtain an overridden file
when an override is used.
+As of Pyramid 1.6, it is also possible to override an asset by supplying an
+absolute path to a file or directory. This may be useful if the assets are
+not distributed as part of a Python package.
diff --git a/docs/narr/commandline.rst b/docs/narr/commandline.rst
index 3cabbd8f4..1fe2d9278 100644
--- a/docs/narr/commandline.rst
+++ b/docs/narr/commandline.rst
@@ -146,7 +146,7 @@ name ``main`` as a section name:
.. code-block:: text
- $ $VENV/bin starter/development.ini#main
+ $ $VENV/bin/pshell starter/development.ini#main
Python 2.6.5 (r265:79063, Apr 29 2010, 00:31:32)
[GCC 4.4.3] on linux2
Type "help" for more information.
@@ -312,24 +312,60 @@ For example:
:linenos:
$ $VENV/bin/proutes development.ini
- Name Pattern View
- ---- ------- ----
- home / <function my_view>
- home2 / <function my_view>
- another /another None
- static/ static/*subpath <static_view object>
- catchall /*subpath <function static_view>
-
-``proutes`` generates a table with three columns: *Name*, *Pattern*,
+ Name Pattern View
+ ---- ------- ----
+ debugtoolbar /_debug_toolbar/*subpath <wsgiapp> *
+ __static/ /static/*subpath dummy_starter:static/ *
+ __static2/ /static2/*subpath /var/www/static/ *
+ __pdt_images/ /pdt_images/*subpath pyramid_debugtoolbar:static/img/ *
+ a / <unknown> *
+ no_view_attached / <unknown> *
+ route_and_view_attached / app1.standard_views.route_and_view_attached *
+ method_conflicts /conflicts app1.standard_conflicts <route mismatch>
+ multiview /multiview app1.standard_views.multiview GET,PATCH
+ not_post /not_post app1.standard_views.multview !POST,*
+
+``proutes`` generates a table with four columns: *Name*, *Pattern*, *Method*,
and *View*. The items listed in the
Name column are route names, the items listed in the Pattern column are route
patterns, and the items listed in the View column are representations of the
view callable that will be invoked when a request matches the associated
-route pattern. The view column may show ``None`` if no associated view
+route pattern. The view column may show ``<unknown>`` if no associated view
callable could be found. If no routes are configured within your
application, nothing will be printed to the console when ``proutes``
is executed.
+It is convenient when using the ``proutes`` often to configure which columns
+and the order you would like to view them. To facilitate this, ``proutes`` will
+look for a special ``[proutes]`` section in your INI file and use those as
+defaults.
+
+For example you may remove request method and place the view first:
+
+.. code-block:: text
+ :linenos:
+
+ [proutes]
+ format = view
+ name
+ pattern
+
+You can also separate the formats with commas or spaces:
+
+.. code-block:: text
+ :linenos:
+
+ [proutes]
+ format = view name pattern
+
+ [proutes]
+ format = view, name, pattern
+
+If you want to temporarily configure the columns and order there is the
+``--format`` which is a comma separated list of columns you want to include. The
+current available formats are ``name``, ``pattern``, ``view``, and ``method``.
+
+
.. index::
pair: tweens; printing
single: ptweens
diff --git a/docs/narr/configuration.rst b/docs/narr/configuration.rst
index 52615533d..f7fa94daf 100644
--- a/docs/narr/configuration.rst
+++ b/docs/narr/configuration.rst
@@ -17,6 +17,10 @@ plugging application code that you've written into :app:`Pyramid` is also
referred to within this documentation as "configuration"; you are configuring
:app:`Pyramid` to call the code that makes up your application.
+.. seealso::
+ For information on ``.ini`` files for Pyramid applications see the
+ :ref:`startup_chapter` chapter.
+
There are two ways to configure a :app:`Pyramid` application:
:term:`imperative configuration` and :term:`declarative configuration`. Both
are described below.
diff --git a/docs/narr/environment.rst b/docs/narr/environment.rst
index 412635f08..0b06fb80b 100644
--- a/docs/narr/environment.rst
+++ b/docs/narr/environment.rst
@@ -13,7 +13,6 @@
single: reload settings
single: default_locale_name
single: environment variables
- single: Mako environment settings
single: ini file settings
single: PasteDeploy settings
@@ -158,6 +157,28 @@ feature when this is true.
| | |
+---------------------------------+----------------------------------+
+Preventing Cache Busting
+------------------------
+
+Prevent the ``cachebust`` static view configuration argument from having any
+effect globally in this process when this value is true. No cache buster will
+be configured or used when this is true.
+
+.. versionadded:: 1.6
+
+.. seealso::
+
+ See also :ref:`cache_busting`.
+
++---------------------------------+----------------------------------+
+| Environment Variable Name | Config File Setting Name |
++=================================+==================================+
+| ``PYRAMID_PREVENT_CACHEBUST`` | ``pyramid.prevent_cachebust`` |
+| | or ``prevent_cachebust`` |
+| | |
+| | |
++---------------------------------+----------------------------------+
+
Debugging All
-------------
@@ -396,153 +417,6 @@ Is equivalent to using the following statements in your configuration code:
It is fine to use both or either form.
-.. _mako_template_renderer_settings:
-
-Mako Template Render Settings
------------------------------
-
-Mako derives additional settings to configure its template renderer that
-should be set when using it. Many of these settings are optional and only need
-to be set if they should be different from the default. The Mako Template
-Renderer uses a subclass of Mako's `template lookup
-<http://www.makotemplates.org/docs/usage.html#usage_lookup>`_ and accepts
-several arguments to configure it.
-
-Mako Directories
-~~~~~~~~~~~~~~~~
-
-The value(s) supplied here are passed in as the template directories. They
-should be in :term:`asset specification` format, for example:
-``my.package:templates``.
-
-+-----------------------------+
-| Config File Setting Name |
-+=============================+
-| ``mako.directories`` |
-| |
-| |
-| |
-+-----------------------------+
-
-Mako Module Directory
-~~~~~~~~~~~~~~~~~~~~~
-
-The value supplied here tells Mako where to store compiled Mako templates. If
-omitted, compiled templates will be stored in memory. This value should be an
-absolute path, for example: ``%(here)s/data/templates`` would use a directory
-called ``data/templates`` in the same parent directory as the INI file.
-
-+-----------------------------+
-| Config File Setting Name |
-+=============================+
-| ``mako.module_directory`` |
-| |
-| |
-| |
-+-----------------------------+
-
-Mako Input Encoding
-~~~~~~~~~~~~~~~~~~~
-
-The encoding that Mako templates are assumed to have. By default this is set
-to ``utf-8``. If you wish to use a different template encoding, this value
-should be changed accordingly.
-
-+-----------------------------+
-| Config File Setting Name |
-+=============================+
-| ``mako.input_encoding`` |
-| |
-| |
-| |
-+-----------------------------+
-
-Mako Error Handler
-~~~~~~~~~~~~~~~~~~
-
-A callable (or a :term:`dotted Python name` which names a callable) which is
-called whenever Mako compile or runtime exceptions occur. The callable is
-passed the current context as well as the exception. If the callable returns
-True, the exception is considered to be handled, else it is re-raised after
-the function completes. Is used to provide custom error-rendering functions.
-
-+-----------------------------+
-| Config File Setting Name |
-+=============================+
-| ``mako.error_handler`` |
-| |
-| |
-| |
-+-----------------------------+
-
-Mako Default Filters
-~~~~~~~~~~~~~~~~~~~~
-
-List of string filter names that will be applied to all Mako expressions.
-
-+-----------------------------+
-| Config File Setting Name |
-+=============================+
-| ``mako.default_filters`` |
-| |
-| |
-| |
-+-----------------------------+
-
-Mako Import
-~~~~~~~~~~~
-
-String list of Python statements, typically individual "import" lines, which
-will be placed into the module level preamble of all generated Python modules.
-
-
-+-----------------------------+
-| Config File Setting Name |
-+=============================+
-| ``mako.imports`` |
-| |
-| |
-| |
-+-----------------------------+
-
-
-Mako Strict Undefined
-~~~~~~~~~~~~~~~~~~~~~
-
-``true`` or ``false``, representing the "strict undefined" behavior of Mako
-(see `Mako Context Variables
-<http://www.makotemplates.org/docs/runtime.html#context-variables>`_). By
-default, this is ``false``.
-
-+-----------------------------+
-| Config File Setting Name |
-+=============================+
-| ``mako.strict_undefined`` |
-| |
-| |
-| |
-+-----------------------------+
-
-Mako Preprocessor
-~~~~~~~~~~~~~~~~~
-
-.. versionadded:: 1.1
-
-A callable (or a :term:`dotted Python name` which names a callable) which is
-called to preprocess the source before the template is called. The callable
-will be passed the full template source before it is parsed. The return
-result of the callable will be used as the template source code.
-
-
-+-----------------------------+
-| Config File Setting Name |
-+=============================+
-| ``mako.preprocessor`` |
-| |
-| |
-| |
-+-----------------------------+
-
Examples
--------
diff --git a/docs/narr/extconfig.rst b/docs/narr/extconfig.rst
index 6587aef92..a61eca7b7 100644
--- a/docs/narr/extconfig.rst
+++ b/docs/narr/extconfig.rst
@@ -215,13 +215,115 @@ registers an action with a higher order than the
passed to it, that a route by this name was already registered by
``add_route``, and if such a route has not already been registered, it's a
configuration error (a view that names a nonexistent route via its
-``route_name`` parameter will never be called).
+``route_name`` parameter will never be called). As of Pyramid 1.6 it is
+possible for one action to invoke another. See :ref:`ordering_actions` for
+more information.
``introspectables`` is a sequence of :term:`introspectable` objects. You can
pass a sequence of introspectables to the
:meth:`~pyramid.config.Configurator.action` method, which allows you to
augment Pyramid's configuration introspection system.
+.. _ordering_actions:
+
+Ordering Actions
+----------------
+
+In Pyramid every :term:`action` has an inherent ordering relative to other
+actions. The logic within actions is deferred until a call to
+:meth:`pyramid.config.Configurator.commit` (which is automatically invoked by
+:meth:`pyramid.config.Configurator.make_wsgi_app`). This means you may call
+``config.add_view(route_name='foo')`` **before**
+``config.add_route('foo', '/foo')`` because nothing actually happens until
+commit-time. During a commit cycle conflicts are resolved, actions are ordered
+and executed.
+
+By default, almost every action in Pyramid has an ``order`` of
+:const:`pyramid.config.PHASE3_CONFIG`. Every action within the same order-level
+will be executed in the order it was called.
+This means that if an action must be reliably executed before or after another
+action, the ``order`` must be defined explicitly to make this work. For
+example, views are dependent on routes being defined. Thus the action created
+by :meth:`pyramid.config.Configurator.add_route` has an ``order`` of
+:const:`pyramid.config.PHASE2_CONFIG`.
+
+Pre-defined Phases
+~~~~~~~~~~~~~~~~~~
+
+:const:`pyramid.config.PHASE0_CONFIG`
+
+- This phase is reserved for developers who want to execute actions prior
+ to Pyramid's core directives.
+
+:const:`pyramid.config.PHASE1_CONFIG`
+
+- :meth:`pyramid.config.Configurator.add_renderer`
+- :meth:`pyramid.config.Configurator.add_route_predicate`
+- :meth:`pyramid.config.Configurator.add_subscriber_predicate`
+- :meth:`pyramid.config.Configurator.add_view_predicate`
+- :meth:`pyramid.config.Configurator.set_authorization_policy`
+- :meth:`pyramid.config.Configurator.set_default_permission`
+- :meth:`pyramid.config.Configurator.set_view_mapper`
+
+:const:`pyramid.config.PHASE2_CONFIG`
+
+- :meth:`pyramid.config.Configurator.add_route`
+- :meth:`pyramid.config.Configurator.set_authentication_policy`
+
+:const:`pyramid.config.PHASE3_CONFIG`
+
+- The default for all builtin or custom directives unless otherwise specified.
+
+Calling Actions From Actions
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. versionadded:: 1.6
+
+Pyramid's configurator allows actions to be added during a commit-cycle as
+long as they are added to the current or a later ``order`` phase. This means
+that your custom action can defer decisions until commit-time and then do
+things like invoke :meth:`pyramid.config.Configurator.add_route`. It can also
+provide better conflict detection if your addon needs to call more than one
+other action.
+
+For example, let's make an addon that invokes ``add_route`` and ``add_view``,
+but we want it to conflict with any other call to our addon:
+
+.. code-block:: python
+ :linenos:
+
+ from pyramid.config import PHASE0_CONFIG
+
+ def includeme(config):
+ config.add_directive('add_auto_route', add_auto_route)
+
+ def add_auto_route(config, name, view):
+ def register():
+ config.add_view(route_name=name, view=view)
+ config.add_route(name, '/' + name)
+ config.action(('auto route', name), register, order=PHASE0_CONFIG)
+
+Now someone else can use your addon and be informed if there is a conflict
+between this route and another, or two calls to ``add_auto_route``.
+Notice how we had to invoke our action **before** ``add_view`` or
+``add_route``. If we tried to invoke this afterward, the subsequent calls to
+``add_view`` and ``add_route`` would cause conflicts because that phase had
+already been executed, and the configurator cannot go back in time to add more
+views during that commit-cycle.
+
+.. code-block:: python
+ :linenos:
+
+ from pyramid.config import Configurator
+
+ def main(global_config, **settings):
+ config = Configurator()
+ config.include('auto_route_addon')
+ config.add_auto_route('foo', my_view)
+
+ def my_view(request):
+ return request.response
+
.. _introspection:
Adding Configuration Introspection
diff --git a/docs/narr/hooks.rst b/docs/narr/hooks.rst
index 4da36e730..4fd7670b9 100644
--- a/docs/narr/hooks.rst
+++ b/docs/narr/hooks.rst
@@ -349,6 +349,55 @@ We attach and cache an object named ``extra`` to the ``request`` object.
the property
.. index::
+ single: response factory
+
+.. _changing_the_response_factory:
+
+Changing the Response Factory
+-------------------------------
+
+.. versionadded:: 1.6
+
+Whenever :app:`Pyramid` returns a response from a view it creates a
+:term:`response` object. By default, an instance of the
+:class:`pyramid.response.Response` class is created to represent the response
+object.
+
+The factory that :app:`Pyramid` uses to create a response object instance can be
+changed by passing a :class:`pyramid.interfaces.IResponseFactory` argument to
+the constructor of the :term:`configurator`. This argument can be either a
+callable or a :term:`dotted Python name` representing a callable.
+
+The factory takes a single positional argument, which is a :term:`Request`
+object. The argument may be ``None``.
+
+.. code-block:: python
+ :linenos:
+
+ from pyramid.response import Response
+
+ class MyResponse(Response):
+ pass
+
+ config = Configurator(response_factory=lambda r: MyResponse())
+
+If you're doing imperative configuration, and you'd rather do it after you've
+already constructed a :term:`configurator` it can also be registered via the
+:meth:`pyramid.config.Configurator.set_response_factory` method:
+
+.. code-block:: python
+ :linenos:
+
+ from pyramid.config import Configurator
+ from pyramid.response import Response
+
+ class MyResponse(Response):
+ pass
+
+ config = Configurator()
+ config.set_response_factory(lambda r: MyResponse())
+
+.. index::
single: before render event
single: adding renderer globals
@@ -730,7 +779,7 @@ If you want to implement your own Response object instead of using the
:class:`pyramid.response.Response` object in any capacity at all, you'll have
to make sure the object implements every attribute and method outlined in
:class:`pyramid.interfaces.IResponse` and you'll have to ensure that it uses
-``zope.interface.implementer(IResponse)`` as a class decoratoror.
+``zope.interface.implementer(IResponse)`` as a class decorator.
.. code-block:: python
:linenos:
diff --git a/docs/narr/hybrid.rst b/docs/narr/hybrid.rst
index 4a3258d35..1c324d22b 100644
--- a/docs/narr/hybrid.rst
+++ b/docs/narr/hybrid.rst
@@ -453,7 +453,7 @@ commonly in route declarations that look like this:
.. code-block:: python
:linenos:
- from pryamid.static import static_view
+ from pyramid.static import static_view
www = static_view('mypackage:static', use_subpath=True)
diff --git a/docs/narr/i18n.rst b/docs/narr/i18n.rst
index 1de2c8941..3c804a158 100644
--- a/docs/narr/i18n.rst
+++ b/docs/narr/i18n.rst
@@ -326,7 +326,7 @@ application. You run a ``pot-create`` command to extract the messages:
$ cd /place/where/myapplication/setup.py/lives
$ mkdir -p myapplication/locale
- $ $VENV/bin/pot-create src > myapplication/locale/myapplication.pot
+ $ $VENV/bin/pot-create -o myapplication/locale/myapplication.pot src
The message catalog ``.pot`` template will end up in:
@@ -352,9 +352,9 @@ command from Gettext:
$ cd /place/where/myapplication/setup.py/lives
$ cd myapplication/locale
$ mkdir -p es/LC_MESSAGES
- $ msginit -l es es/LC_MESSAGES/myapplication.po
+ $ msginit -l es -o es/LC_MESSAGES/myapplication.po
-This will create a new the message catalog ``.po`` file will in:
+This will create a new message catalog ``.po`` file in:
``myapplication/locale/es/LC_MESSAGES/myapplication.po``.
@@ -402,11 +402,11 @@ command from Gettext:
.. code-block:: text
$ cd /place/where/myapplication/setup.py/lives
- $ msgfmt myapplication/locale/*/LC_MESSAGES/*.po
+ $ msgfmt -o myapplication/locale/es/LC_MESSAGES/myapplication.mo myapplication/locale/es/LC_MESSAGES/myapplication.po
This will create a ``.mo`` file for each ``.po`` file in your
application. As long as the :term:`translation directory` in which
-the ``.mo`` file ends up in is configured into your application, these
+the ``.mo`` file ends up in is configured into your application (see :ref:`adding_a_translation_directory`), these
translations will be available to :app:`Pyramid`.
.. index::
@@ -792,9 +792,11 @@ Then as a part of the code of a custom :term:`locale negotiator`:
.. code-block:: python
:linenos:
- from pyramid.threadlocal import get_current_registry
- settings = get_current_registry().settings
- languages = settings['available_languages'].split()
+ from pyramid.settings import aslist
+
+ def my_locale_negotiator(request):
+ languages = aslist(request.registry.settings['available_languages'])
+ # ...
This is only a suggestion. You can create your own "available
languages" configuration scheme as necessary.
diff --git a/docs/narr/introspector.rst b/docs/narr/introspector.rst
index a7bde4cf7..8caba522c 100644
--- a/docs/narr/introspector.rst
+++ b/docs/narr/introspector.rst
@@ -121,7 +121,7 @@ introspectables in categories not described here.
``subscriber``
The subscriber callable object (the resolution of the ``subscriber``
- argument passed to ``add_susbcriber``).
+ argument passed to ``add_subscriber``).
``interfaces``
@@ -137,12 +137,12 @@ introspectables in categories not described here.
``predicates``
The predicate objects created as the result of passing predicate arguments
- to ``add_susbcriber``
+ to ``add_subscriber``
``derived_predicates``
Wrappers around the predicate objects created as the result of passing
- predicate arguments to ``add_susbcriber`` (to be used when predicates take
+ predicate arguments to ``add_subscriber`` (to be used when predicates take
only one value but must be passed more than one).
``response adapters``
@@ -450,9 +450,9 @@ introspectables in categories not described here.
The :class:`pyramid.interfaces.IRendererInfo` object which represents
this template's renderer.
-``view mapper``
+``view mappers``
- Each introspectable in the ``permissions`` category represents a call to
+ Each introspectable in the ``view mappers`` category represents a call to
:meth:`pyramid.config.Configurator.add_view` that has an explicit
``mapper`` argument to *or* a call to
:meth:`pyramid.config.Configurator.set_view_mapper`; each will have
@@ -481,8 +481,8 @@ introspectables in categories not described here.
``translation directories``
- Each introspectable in the ``asset overrides`` category represents an
- individual element in a ``specs`` argument passed to
+ Each introspectable in the ``translation directories`` category represents
+ an individual element in a ``specs`` argument passed to
:meth:`pyramid.config.Configurator.add_translation_dirs`; each will have
the following data.
@@ -511,7 +511,7 @@ introspectables in categories not described here.
``type``
- ``implict`` or ``explicit`` as a string.
+ ``implicit`` or ``explicit`` as a string.
``under``
diff --git a/docs/narr/logging.rst b/docs/narr/logging.rst
index 75428d513..921883091 100644
--- a/docs/narr/logging.rst
+++ b/docs/narr/logging.rst
@@ -16,6 +16,11 @@ how to send log messages to loggers that you've configured.
a third-party scaffold which does not create these files, the
configuration information in this chapter may not be applicable.
+.. index:
+ pair: settings; logging
+ pair: .ini; logging
+ pair: logging; configuration
+
.. _logging_config:
Logging Configuration
@@ -242,23 +247,22 @@ level is set to ``INFO``, whereas the application's log level is set to
[logger_myapp]
level = DEBUG
handlers =
- qualname = helloworld
+ qualname = myapp
All of the child loggers of the ``myapp`` logger will inherit the ``DEBUG``
level unless they're explicitly set differently. Meaning the ``myapp.views``,
``myapp.models`` (and all your app's modules') loggers by default have an
effective level of ``DEBUG`` too.
-For more advanced filtering, the logging module provides a `Filter
-<http://docs.python.org/lib/node423.html>`_ object; however it cannot be used
-directly from the configuration file.
+For more advanced filtering, the logging module provides a
+:class:`logging.Filter` object; however it cannot be used directly from the
+configuration file.
-Advanced Configuration
+Advanced Configuration
----------------------
-To capture log output to a separate file, use a `FileHandler
-<http://docs.python.org/lib/node412.html>`_ (or a `RotatingFileHandler
-<http://docs.python.org/lib/node413.html>`_):
+To capture log output to a separate file, use :class:`logging.FileHandler` (or
+:class:`logging.handlers.RotatingFileHandler`):
.. code-block:: ini
@@ -294,15 +298,27 @@ use the :term:`pyramid_exclog` package. Details about its configuration are
in its `documentation
<http://docs.pylonsproject.org/projects/pyramid_exclog/dev/>`_.
+.. index::
+ single: TransLogger
+ single: middleware; TransLogger
+ pair: configuration; middleware
+ single: settings; middleware
+ pair: .ini; middleware
+
+.. _request_logging_with_pastes_translogger:
+
Request Logging with Paste's TransLogger
----------------------------------------
-Paste provides the `TransLogger
-<http://pythonpaste.org/modules/translogger.html>`_ :term:`middleware` for
-logging requests using the `Apache Combined Log Format
-<http://httpd.apache.org/docs/2.2/logs.html#combined>`_. TransLogger combined
-with a FileHandler can be used to create an ``access.log`` file similar to
-Apache's.
+The term:`WSGI` design is modular. Waitress logs error conditions, debugging
+output, etc., but not web traffic. For web traffic logging Paste provides the
+`TransLogger <http://pythonpaste.org/modules/translogger.html>`_
+:term:`middleware`. TransLogger produces logs in the `Apache Combined Log
+Format <http://httpd.apache.org/docs/2.2/logs.html#combined>`_. But
+TransLogger does not write to files, the Python logging system must be
+configured to do this. The Python :class:`logging.FileHandler` logging
+handler can be used alongside TransLogger to create an ``access.log`` file
+similar to Apache's.
Like any standard :term:`middleware` with a Paste entry point, TransLogger can
be configured to wrap your application using ``.ini`` file syntax. First,
@@ -343,10 +359,12 @@ function of your project's ``__init__`` file:
app = TransLogger(app, setup_console_handler=False)
return app
-TransLogger will automatically setup a logging handler to the console when
-called with no arguments, so it 'just works' in environments that don't
-configure logging. Since we've configured our own logging handlers, we need
-to disable that option via ``setup_console_handler = False``.
+
+.. note::
+ TransLogger will automatically setup a logging handler to the console when
+ called with no arguments, so it 'just works' in environments that don't
+ configure logging. Since our logging handlers are configured we disable
+ the automation via ``setup_console_handler = False``.
With the filter in place, TransLogger's logger (named the ``wsgi`` logger) will
propagate its log messages to the parent logger (the root logger), sending
@@ -361,9 +379,9 @@ its output to the console when we request a page:
"Mozilla/5.0 (Macintosh; U; Intel Mac OS X; en-US; rv:1.8.1.6) Gecko/20070725
Firefox/2.0.0.6"
-To direct TransLogger to an ``access.log`` FileHandler, we need to add that
-FileHandler to the list of handlers (named ``accesslog``), and ensure that the
-``wsgi`` logger is configured and uses this handler accordingly:
+To direct TransLogger to an ``access.log`` FileHandler, we need the following
+to add a FileHandler (named ``accesslog``) to the list of handlers, and ensure
+that the ``wsgi`` logger is configured and uses this handler accordingly:
.. code-block:: ini
@@ -377,7 +395,7 @@ FileHandler to the list of handlers (named ``accesslog``), and ensure that the
[logger_wsgi]
level = INFO
- handlers = handler_accesslog
+ handlers = accesslog
qualname = wsgi
propagate = 0
@@ -395,7 +413,7 @@ directs its records only to the ``accesslog`` handler.
Finally, there's no need to use the ``generic`` formatter with TransLogger as
TransLogger itself provides all the information we need. We'll use a
formatter that passes-through the log messages as is. Add a new formatter
-called ``accesslog`` by include the following in your configuration file:
+called ``accesslog`` by including the following in your configuration file:
.. code-block:: ini
@@ -405,7 +423,9 @@ called ``accesslog`` by include the following in your configuration file:
[formatter_accesslog]
format = %(message)s
-Then wire this new ``accesslog`` formatter into the FileHandler:
+
+Finally alter the existing configuration to wire this new
+``accesslog`` formatter into the FileHandler:
.. code-block:: ini
diff --git a/docs/narr/router.rst b/docs/narr/router.rst
index ac3deefdc..6f90c70cc 100644
--- a/docs/narr/router.rst
+++ b/docs/narr/router.rst
@@ -9,6 +9,9 @@
Request Processing
==================
+.. image:: ../_static/pyramid_request_processing.*
+ :alt: Request Processing
+
Once a :app:`Pyramid` application is up and running, it is ready to accept
requests and return responses. What happens from the time a :term:`WSGI`
request enters a :app:`Pyramid` application through to the point that
@@ -116,7 +119,8 @@ request enters a :app:`Pyramid` application through to the point that
#. The :term:`thread local` stack is popped.
-.. image:: router.png
+.. image:: ../_static/pyramid_router.*
+ :alt: Pyramid Router
This is a very high-level overview that leaves out various details. For more
detail about subsystems invoked by the :app:`Pyramid` router such as
diff --git a/docs/narr/security.rst b/docs/narr/security.rst
index 8db23a33b..75f4dc7c5 100644
--- a/docs/narr/security.rst
+++ b/docs/narr/security.rst
@@ -6,13 +6,28 @@
Security
========
-:app:`Pyramid` provides an optional declarative authorization system
-that can prevent a :term:`view` from being invoked based on an
+:app:`Pyramid` provides an optional, declarative, security system.
+Security in :app:`Pyramid` is separated into authentication and
+authorization. The two systems communicate via :term:`principal`
+identifiers. Authentication is merely the mechanism by which credentials
+provided in the :term:`request` are resolved to one or more
+:term:`principal` identifiers. These identifiers represent the users and
+groups that are in effect during the request. Authorization then determines
+access based on the :term:`principal` identifiers, the requested
+:term:`permission`, and a :term:`context`.
+
+The :app:`Pyramid` authorization system
+can prevent a :term:`view` from being invoked based on an
:term:`authorization policy`. Before a view is invoked, the
authorization system can use the credentials in the :term:`request`
along with the :term:`context` resource to determine if access will be
allowed. Here's how it works at a high level:
+- A user may or may not have previously visited the application and
+ supplied authentication credentials, including a :term:`userid`. If
+ so, the application may have called
+ :func:`pyramid.security.remember` to remember these.
+
- A :term:`request` is generated when a user visits the application.
- Based on the request, a :term:`context` resource is located through
@@ -25,7 +40,9 @@ allowed. Here's how it works at a high level:
context as well as other attributes of the request.
- If an :term:`authentication policy` is in effect, it is passed the
- request; it returns some number of :term:`principal` identifiers.
+ request. It will return some number of :term:`principal` identifiers.
+ To do this, the policy would need to determine the authenticated
+ :term:`userid` present in the request.
- If an :term:`authorization policy` is in effect and the :term:`view
configuration` associated with the view callable that was found has
@@ -41,15 +58,6 @@ allowed. Here's how it works at a high level:
- If the authorization policy denies access, the view callable is not
invoked; instead the :term:`forbidden view` is invoked.
-Security in :app:`Pyramid`, unlike many systems, cleanly and explicitly
-separates authentication and authorization. Authentication is merely the
-mechanism by which credentials provided in the :term:`request` are
-resolved to one or more :term:`principal` identifiers. These identifiers
-represent the users and groups in effect during the request.
-Authorization then determines access based on the :term:`principal`
-identifiers, the :term:`view callable` being invoked, and the
-:term:`context` resource.
-
Authorization is enabled by modifying your application to include an
:term:`authentication policy` and :term:`authorization policy`.
:app:`Pyramid` comes with a variety of implementations of these
@@ -104,7 +112,8 @@ For example:
The above configuration enables a policy which compares the value of an "auth
ticket" cookie passed in the request's environment which contains a reference
-to a single :term:`principal` against the principals present in any
+to a single :term:`userid` and matches that userid's
+:term:`principals <principal>` against the principals present in any
:term:`ACL` found in the resource tree when attempting to call some
:term:`view`.
@@ -332,9 +341,7 @@ third argument is a permission or sequence of permission names.
A principal is usually a user id, however it also may be a group id if your
authentication system provides group information and the effective
:term:`authentication policy` policy is written to respect group information.
-For example, the
-:class:`pyramid.authentication.RepozeWho1AuthenticationPolicy` respects group
-information if you configure it with a ``callback``.
+See :ref:`extending_default_authentication_policies`.
Each ACE in an ACL is processed by an authorization policy *in the
order dictated by the ACL*. So if you have an ACL like this:
@@ -574,6 +581,60 @@ via print statements when a call to
:meth:`~pyramid.request.Request.has_permission` fails is often useful.
.. index::
+ single: authentication policy (extending)
+
+.. _extending_default_authentication_policies:
+
+Extending Default Authentication Policies
+-----------------------------------------
+
+Pyramid ships with some builtin authentication policies for use in your
+applications. See :mod:`pyramid.authentication` for the available
+policies. They differ on their mechanisms for tracking authentication
+credentials between requests, however they all interface with your
+application in mostly the same way.
+
+Above you learned about :ref:`assigning_acls`. Each :term:`principal` used
+in the :term:`ACL` is matched against the list returned from
+:meth:`pyramid.interfaces.IAuthenticationPolicy.effective_principals`.
+Similarly, :meth:`pyramid.request.Request.authenticated_userid` maps to
+:meth:`pyramid.interfaces.IAuthenticationPolicy.authenticated_userid`.
+
+You may control these values by subclassing the default authentication
+policies. For example, below we subclass the
+:class:`pyramid.authentication.AuthTktAuthenticationPolicy` and define
+extra functionality to query our database before confirming that the
+:term:`userid` is valid in order to avoid blindly trusting the value in the
+cookie (what if the cookie is still valid but the user has deleted their
+account?). We then use that :term:`userid` to augment the
+``effective_principals`` with information about groups and other state for
+that user.
+
+.. code-block:: python
+ :linenos:
+
+ from pyramid.authentication import AuthTktAuthenticationPolicy
+
+ class MyAuthenticationPolicy(AuthTktAuthenticationPolicy):
+ def authenticated_userid(self, request):
+ userid = self.unauthenticated_userid(request)
+ if userid:
+ if request.verify_userid_is_still_valid(userid):
+ return userid
+
+ def effective_principals(self, request):
+ principals = [Everyone]
+ userid = self.authenticated_userid(request)
+ if userid:
+ principals += [Authenticated, str(userid)]
+ return principals
+
+In most instances ``authenticated_userid`` and ``effective_principals`` are
+application-specific whereas ``unauthenticated_userid``, ``remember`` and
+``forget`` are generic and focused on transport/serialization of data
+between consecutive requests.
+
+.. index::
single: authentication policy (creating)
.. _creating_an_authentication_policy:
@@ -595,39 +656,56 @@ that implements the following interface:
""" An object representing a Pyramid authentication policy. """
def authenticated_userid(self, request):
- """ Return the authenticated userid or ``None`` if no
- authenticated userid can be found. This method of the policy
- should ensure that a record exists in whatever persistent store is
- used related to the user (the user should not have been deleted);
- if a record associated with the current id does not exist in a
- persistent store, it should return ``None``."""
+ """ Return the authenticated :term:`userid` or ``None`` if
+ no authenticated userid can be found. This method of the
+ policy should ensure that a record exists in whatever
+ persistent store is used related to the user (the user
+ should not have been deleted); if a record associated with
+ the current id does not exist in a persistent store, it
+ should return ``None``.
+
+ """
def unauthenticated_userid(self, request):
- """ Return the *unauthenticated* userid. This method performs the
- same duty as ``authenticated_userid`` but is permitted to return the
- userid based only on data present in the request; it needn't (and
- shouldn't) check any persistent store to ensure that the user record
- related to the request userid exists."""
+ """ Return the *unauthenticated* userid. This method
+ performs the same duty as ``authenticated_userid`` but is
+ permitted to return the userid based only on data present
+ in the request; it needn't (and shouldn't) check any
+ persistent store to ensure that the user record related to
+ the request userid exists.
+
+ This method is intended primarily a helper to assist the
+ ``authenticated_userid`` method in pulling credentials out
+ of the request data, abstracting away the specific headers,
+ query strings, etc that are used to authenticate the request.
+
+ """
def effective_principals(self, request):
""" Return a sequence representing the effective principals
- including the userid and any groups belonged to by the current
- user, including 'system' groups such as
- ``pyramid.security.Everyone`` and
- ``pyramid.security.Authenticated``. """
+ typically including the :term:`userid` and any groups belonged
+ to by the current user, always including 'system' groups such
+ as ``pyramid.security.Everyone`` and
+ ``pyramid.security.Authenticated``.
+
+ """
- def remember(self, request, principal, **kw):
+ def remember(self, request, userid, **kw):
""" Return a set of headers suitable for 'remembering' the
- principal named ``principal`` when set in a response. An
- individual authentication policy and its consumers can decide
- on the composition and meaning of **kw. """
-
+ :term:`userid` named ``userid`` when set in a response. An
+ individual authentication policy and its consumers can
+ decide on the composition and meaning of **kw.
+
+ """
+
def forget(self, request):
""" Return a set of headers suitable for 'forgetting' the
- current user on subsequent requests. """
+ current user on subsequent requests.
+
+ """
After you do so, you can pass an instance of such a class into the
-:class:`~pyramid.config.Configurator.set_authentication_policy` method
+:class:`~pyramid.config.Configurator.set_authentication_policy` method at
configuration time to use it.
.. index::
diff --git a/docs/narr/sessions.rst b/docs/narr/sessions.rst
index 8da743a01..5c103405a 100644
--- a/docs/narr/sessions.rst
+++ b/docs/narr/sessions.rst
@@ -44,7 +44,7 @@ It is digitally signed, however, and thus its data cannot easily be
tampered with.
You can configure this session factory in your :app:`Pyramid` application
-by using the :meth:`pyramid.config.Configurator.set_session_factory`` method.
+by using the :meth:`pyramid.config.Configurator.set_session_factory` method.
.. code-block:: python
:linenos:
@@ -380,7 +380,7 @@ Checking CSRF Tokens Manually
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
In request handling code, you can check the presence and validity of a CSRF
-token with :func:`pyramid.session.check_csrf_token(request)``. If the token is
+token with :func:`pyramid.session.check_csrf_token`. If the token is
valid, it will return ``True``, otherwise it will raise ``HTTPBadRequest``.
Optionally, you can specify ``raises=False`` to have the check return ``False``
instead of raising an exception.
diff --git a/docs/narr/startup.rst b/docs/narr/startup.rst
index 7b4a7ea08..a1a23ed52 100644
--- a/docs/narr/startup.rst
+++ b/docs/narr/startup.rst
@@ -19,6 +19,7 @@ console.
.. index::
single: startup process
+ pair: settings; .ini
The Startup Process
-------------------
@@ -139,6 +140,13 @@ Here's a high-level time-ordered overview of what happens when you press
The server serves the application, and the application is running, waiting
to receive requests.
+.. seealso::
+ Logging configuration is described in the :ref:`logging_chapter`
+ chapter. There, in :ref:`request_logging_with_pastes_translogger`,
+ you will also find an example of how to configure
+ :term:`middleware` to add pre-packaged functionality to your
+ application.
+
.. index::
pair: settings; deployment
single: custom settings
diff --git a/docs/narr/templates.rst b/docs/narr/templates.rst
index 460cda8ee..4c1364493 100644
--- a/docs/narr/templates.rst
+++ b/docs/narr/templates.rst
@@ -316,8 +316,7 @@ template renderer:
we're using a Chameleon renderer, it means "relative to the directory in
which the file which defines the view configuration lives". In this case,
this is the directory containing the file that defines the ``my_view``
- function. View-configuration-relative asset specifications work only
- in Chameleon, not in Mako templates.
+ function.
Similar renderer configuration can be done imperatively. See
:ref:`views_which_use_a_renderer`.
diff --git a/docs/narr/testing.rst b/docs/narr/testing.rst
index e001ad81c..ecda57489 100644
--- a/docs/narr/testing.rst
+++ b/docs/narr/testing.rst
@@ -128,8 +128,9 @@ functions accepts various arguments that influence the environment of the
test. See the :ref:`testing_module` API for information about the extra
arguments supported by these functions.
-If you also want to make :func:`~pyramid.threadlocal.get_current_request` return something
-other than ``None`` during the course of a single test, you can pass a
+If you also want to make :func:`~pyramid.threadlocal.get_current_request`
+return something other than ``None`` during the course of a single test, you
+can pass a
:term:`request` object into the :func:`pyramid.testing.setUp` within the
``setUp`` method of your test:
@@ -333,66 +334,49 @@ Creating Integration Tests
--------------------------
In :app:`Pyramid`, a *unit test* typically relies on "mock" or "dummy"
-implementations to give the code under test only enough context to run.
+implementations to give the code under test enough context to run.
"Integration testing" implies another sort of testing. In the context of a
-:app:`Pyramid` integration test, the test logic tests the functionality of
-some code *and* its integration with the rest of the :app:`Pyramid`
+:app:`Pyramid` integration test, the test logic exercises the functionality of
+the code under test *and* its integration with the rest of the :app:`Pyramid`
framework.
-In :app:`Pyramid` applications that are plugins to Pyramid, you can create an
-integration test by including its ``includeme`` function via
-:meth:`pyramid.config.Configurator.include` in the test's setup code. This
-causes the entire :app:`Pyramid` environment to be set up and torn down as if
-your application was running "for real". This is a heavy-hammer way of
-making sure that your tests have enough context to run properly, and it tests
-your code's integration with the rest of :app:`Pyramid`.
+Creating an integration test for a :app:`Pyramid` application usually means
+invoking the application's ``includeme`` function via
+:meth:`pyramid.config.Configurator.include` within the test's setup code. This
+causes the entire :app:`Pyramid` environment to be set up, simulating what
+happens when your application is run "for real". This is a heavy-hammer way of
+making sure that your tests have enough context to run properly, and tests your
+code's integration with the rest of :app:`Pyramid`.
-Let's demonstrate this by showing an integration test for a view. The below
-test assumes that your application's package name is ``myapp``, and that
-there is a ``views`` module in the app with a function with the name
-``my_view`` in it that returns the response 'Welcome to this application'
-after accessing some values that require a fully set up environment.
+.. seealso::
-.. code-block:: python
- :linenos:
+ See also :ref:`including_configuration`
- import unittest
+Let's demonstrate this by showing an integration test for a view.
- from pyramid import testing
+Given the following view definition, which assumes that your application's
+:term:`package` name is ``myproject``, and within that :term:`package` there
+exists a module ``views``, which in turn contains a :term:`view` function named
+``my_view``:
- class ViewIntegrationTests(unittest.TestCase):
- def setUp(self):
- """ This sets up the application registry with the
- registrations your application declares in its ``includeme``
- function.
- """
- import myapp
- self.config = testing.setUp()
- self.config.include('myapp')
+ .. literalinclude:: MyProject/myproject/views.py
+ :linenos:
+ :lines: 1-6
+ :language: python
- def tearDown(self):
- """ Clear out the application registry """
- testing.tearDown()
+You'd then create a ``tests`` module within your ``myproject`` package,
+containing the following test code:
- def test_my_view(self):
- from myapp.views import my_view
- request = testing.DummyRequest()
- result = my_view(request)
- self.assertEqual(result.status, '200 OK')
- body = result.app_iter[0]
- self.assertTrue('Welcome to' in body)
- self.assertEqual(len(result.headerlist), 2)
- self.assertEqual(result.headerlist[0],
- ('Content-Type', 'text/html; charset=UTF-8'))
- self.assertEqual(result.headerlist[1], ('Content-Length',
- str(len(body))))
-
-Unless you cannot avoid it, you should prefer writing unit tests that use the
-:class:`~pyramid.config.Configurator` API to set up the right "mock"
-registrations rather than creating an integration test. Unit tests will run
-faster (because they do less for each test) and the result of a unit test is
-usually easier to make assertions about.
+ .. literalinclude:: MyProject/myproject/tests.py
+ :linenos:
+ :pyobject: ViewIntegrationTests
+ :language: python
+
+Writing unit tests that use the :class:`~pyramid.config.Configurator` API to
+set up the right "mock" registrations is often preferred to creating
+integration tests. Unit tests will run faster (because they do less for each
+test) and are usually easier to reason about.
.. index::
single: functional tests
@@ -404,34 +388,40 @@ Creating Functional Tests
Functional tests test your literal application.
-The below test assumes that your application's package name is ``myapp``, and
-that there is a view that returns an HTML body when the root URL is invoked.
-It further assumes that you've added a ``tests_require`` dependency on the
-``WebTest`` package within your ``setup.py`` file. :term:`WebTest` is a
-functional testing package written by Ian Bicking.
+In Pyramid, functional tests are typically written using the :term:`WebTest`
+package, which provides APIs for invoking HTTP(S) requests to your application.
-.. code-block:: python
- :linenos:
+Regardless of which testing :term:`package` you use, ensure to add a
+``tests_require`` dependency on that package to to your application's
+``setup.py`` file:
- import unittest
+ .. literalinclude:: MyProject/setup.py
+ :linenos:
+ :emphasize-lines: 26-28,48
+ :language: python
- class FunctionalTests(unittest.TestCase):
- def setUp(self):
- from myapp import main
- app = main({})
- from webtest import TestApp
- self.testapp = TestApp(app)
-
- def test_root(self):
- res = self.testapp.get('/', status=200)
- self.assertTrue('Pyramid' in res.body)
-
-When this test is run, each test creates a "real" WSGI application using the
-``main`` function in your ``myapp.__init__`` module and uses :term:`WebTest`
-to wrap that WSGI application. It assigns the result to ``self.testapp``.
-In the test named ``test_root``, we use the testapp's ``get`` method to
-invoke the root URL. We then assert that the returned HTML has the string
-``Pyramid`` in it.
+Assuming your :term:`package` is named ``myproject``, which contains a
+``views`` module, which in turn contains a :term:`view` function ``my_view``
+that returns a HTML body when the root URL is invoked:
+
+ .. literalinclude:: MyProject/myproject/views.py
+ :linenos:
+ :language: python
+
+Then the following example functional test (shown below) demonstrates invoking
+the :term:`view` shown above:
+
+ .. literalinclude:: MyProject/myproject/tests.py
+ :linenos:
+ :pyobject: FunctionalTests
+ :language: python
+
+When this test is run, each test method creates a "real" :term:`WSGI`
+application using the ``main`` function in your ``myproject.__init__`` module,
+using :term:`WebTest` to wrap that WSGI application. It assigns the result to
+``self.testapp``. In the test named ``test_root``. The ``TestApp``'s ``get``
+method is used to invoke the root URL. Finally, an assertion is made that the
+returned HTML contains the text ``MyProject``.
See the :term:`WebTest` documentation for further information about the
methods available to a :class:`webtest.app.TestApp` instance.
diff --git a/docs/narr/urldispatch.rst b/docs/narr/urldispatch.rst
index 87a962a9a..fa3e734fe 100644
--- a/docs/narr/urldispatch.rst
+++ b/docs/narr/urldispatch.rst
@@ -495,17 +495,21 @@ result in a particular view callable being invoked:
:linenos:
config.add_route('idea', 'site/{id}')
- config.add_view('mypackage.views.site_view', route_name='idea')
+ config.scan()
When a route configuration with a ``view`` attribute is added to the system,
and an incoming request matches the *pattern* of the route configuration, the
:term:`view callable` named as the ``view`` attribute of the route
configuration will be invoked.
-In the case of the above example, when the URL of a request matches
-``/site/{id}``, the view callable at the Python dotted path name
-``mypackage.views.site_view`` will be called with the request. In other
-words, we've associated a view callable directly with a route pattern.
+Recall that the ``@view_config`` is equivalent to calling ``config.add_view``,
+because the ``config.scan()`` call will import ``mypackage.views``, shown
+below, and execute ``config.add_view`` under the hood. Each view then maps the
+route name to the matching view callable. In the case of the above
+example, when the URL of a request matches ``/site/{id}``, the view callable at
+the Python dotted path name ``mypackage.views.site_view`` will be called with
+the request. In other words, we've associated a view callable directly with a
+route pattern.
When the ``/site/{id}`` route pattern matches during a request, the
``site_view`` view callable is invoked with that request as its sole
@@ -519,8 +523,10 @@ The ``mypackage.views`` module referred to above might look like so:
.. code-block:: python
:linenos:
+ from pyramid.view import view_config
from pyramid.response import Response
+ @view_config(route_name='idea')
def site_view(request):
return Response(request.matchdict['id'])
@@ -542,11 +548,30 @@ add to your application:
config.add_route('idea', 'ideas/{idea}')
config.add_route('user', 'users/{user}')
config.add_route('tag', 'tags/{tag}')
+ config.scan()
+
+Here is an example of a corresponding ``mypackage.views`` module:
- config.add_view('mypackage.views.idea_view', route_name='idea')
- config.add_view('mypackage.views.user_view', route_name='user')
- config.add_view('mypackage.views.tag_view', route_name='tag')
+.. code-block:: python
+ :linenos:
+
+ from pyramid.view import view_config
+ from pyramid.response import Response
+ @view_config(route_name='idea')
+ def idea_view(request):
+ return Response(request.matchdict['id'])
+
+ @view_config(route_name='user')
+ def user_view(request):
+ user = request.matchdict['user']
+ return Response(u'The user is {}.'.format(user))
+
+ @view_config(route_name='tag')
+ def tag_view(request):
+ tag = request.matchdict['tag']
+ return Response(u'The tag is {}.'.format(tag))
+
The above configuration will allow :app:`Pyramid` to service URLs in these
forms:
@@ -596,7 +621,7 @@ An example of using a route with a factory:
:linenos:
config.add_route('idea', 'ideas/{idea}', factory='myproject.resources.Idea')
- config.add_view('myproject.views.idea_view', route_name='idea')
+ config.scan()
The above route will manufacture an ``Idea`` resource as a :term:`context`,
assuming that ``mypackage.resources.Idea`` resolves to a class that accepts a
@@ -610,7 +635,20 @@ request in its ``__init__``. For example:
pass
In a more complicated application, this root factory might be a class
-representing a :term:`SQLAlchemy` model.
+representing a :term:`SQLAlchemy` model. The view ``mypackage.views.idea_view``
+might look like this:
+
+.. code-block:: python
+ :linenos:
+
+ @view_config(route_name='idea')
+ def idea_view(request):
+ idea = request.context
+ return Response(idea)
+
+Here, ``request.context`` is an instance of ``Idea``. If indeed the resource
+object is a SQLAlchemy model, you do not even have to perform a query in the
+view callable, since you have access to the resource via ``request.context``.
See :ref:`route_factories` for more details about how to use route factories.
@@ -804,7 +842,9 @@ route. When configured, along with at least one other route in your
application, this view will be invoked if the value of ``PATH_INFO`` does not
already end in a slash, and if the value of ``PATH_INFO`` *plus* a slash
matches any route's pattern. In this case it does an HTTP redirect to the
-slash-appended ``PATH_INFO``.
+slash-appended ``PATH_INFO``. In addition you may pass anything that implements
+:class:`pyramid.interfaces.IResponse` which will then be used in place of the
+default class (:class:`pyramid.httpexceptions.HTTPFound`).
Let's use an example. If the following routes are configured in your
application:
diff --git a/docs/narr/viewconfig.rst b/docs/narr/viewconfig.rst
index a0feef8d7..d5203c6ba 100644
--- a/docs/narr/viewconfig.rst
+++ b/docs/narr/viewconfig.rst
@@ -325,7 +325,7 @@ configured view.
``match_param``
This param may be either a single string of the format "key=value" or a
- dict of key/value pairs.
+ tuple containing one or more of these strings.
This argument ensures that the view will only be called when the
:term:`request` has key/value pairs in its :term:`matchdict` that equal
@@ -334,8 +334,8 @@ configured view.
hand side of the expression (``edit``) for the view to "match" the current
request.
- If the ``match_param`` is a dict, every key/value pair must match for the
- predicate to pass.
+ If the ``match_param`` is a tuple, every key/value pair must match
+ for the predicate to pass.
If ``match_param`` is not supplied, the view will be invoked without
consideration of the keys and values in ``request.matchdict``.
diff --git a/docs/narr/webob.rst b/docs/narr/webob.rst
index 6a331e4bf..0eb070b06 100644
--- a/docs/narr/webob.rst
+++ b/docs/narr/webob.rst
@@ -310,6 +310,14 @@ Python's ``urllib2`` instead of a Javascript AJAX request:
req = urllib2.Request('http://localhost:6543/', json_payload, headers)
resp = urllib2.urlopen(req)
+If you are doing Cross-origin resource sharing (CORS), then the standard
+requires the browser to do a pre-flight HTTP OPTIONS request. The easiest way
+to handling this is adding an extra ``view_config`` for the same route, with
+``request_method`` set to ``OPTIONS``, and setting the desired response header
+before returning. You can find examples of response headers here_.
+
+.. _here: https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS#Preflighted_requests
+
.. index::
single: cleaning up after request
diff --git a/docs/quick_tour.rst b/docs/quick_tour.rst
index 4ab39bb11..41a0dc8c0 100644
--- a/docs/quick_tour.rst
+++ b/docs/quick_tour.rst
@@ -700,7 +700,7 @@ we might need to detect situations when other people use the site. We
need *logging*.
Fortunately Pyramid uses the normal Python approach to logging. The
-scaffold generated in your ``development.ini`` a number of lines that
+scaffold generated in your ``development.ini`` has a number of lines that
configure the logging for you to some reasonable defaults. You then see
messages sent by Pyramid (for example, when a new request comes in).
diff --git a/docs/quick_tutorial/debugtoolbar.rst b/docs/quick_tutorial/debugtoolbar.rst
index 90750c633..d138eb760 100644
--- a/docs/quick_tutorial/debugtoolbar.rst
+++ b/docs/quick_tutorial/debugtoolbar.rst
@@ -58,33 +58,31 @@ Steps
Analysis
========
-``pyramid_debugtoolbar`` is a full-fledged Python package,
-available on PyPI just like thousands of other Python packages. Thus we
-start by installing the ``pyramid_debugtoolbar`` package into our
-virtual environment using normal Python package installation commands.
-
-The ``pyramid_debugtoolbar`` Python package is also a Pyramid add-on,
-which means we need to include its add-on configuration into our web
-application. We could do this with imperative configuration in
-``tutorial/__init__.py`` by using ``config.include``. Pyramid also
-supports wiring in add-on configuration via our ``development.ini``
-using ``pyramid.includes``. We use this to load the configuration for
-the debugtoolbar.
-
-You'll now see an attractive button on the right side of
-your browser, which you may click to provide introspective access to debugging
-information in a new browser tab. Even better, if your web application
-generates an error,
-you will see a nice traceback on the screen. When you want to disable
-this toolbar, no need to change code: you can remove it from
-``pyramid.includes`` in the relevant ``.ini`` configuration file (thus
-showing why configuration files are handy.)
-
-Note injects a small amount of html/css into your app just before the closing
-``</body>`` tag in order to display itself. If you
-start to experience otherwise inexplicable client-side weirdness, you can shut
-it off by commenting out the ``pyramid_debugtoolbar`` line in
-``pyramid.includes`` temporarily.
+``pyramid_debugtoolbar`` is a full-fledged Python package, available on PyPI
+just like thousands of other Python packages. Thus we start by installing the
+``pyramid_debugtoolbar`` package into our virtual environment using normal
+Python package installation commands.
+
+The ``pyramid_debugtoolbar`` Python package is also a Pyramid add-on, which
+means we need to include its add-on configuration into our web application. We
+could do this with imperative configuration in ``tutorial/__init__.py`` by
+using ``config.include``. Pyramid also supports wiring in add-on configuration
+via our ``development.ini`` using ``pyramid.includes``. We use this to load
+the configuration for the debugtoolbar.
+
+You'll now see an attractive button on the right side of your browser, which
+you may click to provide introspective access to debugging information in a
+new browser tab. Even better, if your web application generates an error, you
+will see a nice traceback on the screen. When you want to disable this
+toolbar, no need to change code: you can remove it from ``pyramid.includes``
+in the relevant ``.ini`` configuration file (thus showing why configuration
+files are handy.)
+
+Note that the toolbar injects a small amount of html/css into your app just
+before the closing ``</body>`` tag in order to display itself. If you start to
+experience otherwise inexplicable client-side weirdness, you can shut it off
+by commenting out the ``pyramid_debugtoolbar`` line in ``pyramid.includes``
+temporarily.
.. seealso:: See also :ref:`pyramid_debugtoolbar <toolbar:overview>`.
diff --git a/docs/quick_tutorial/forms.rst b/docs/quick_tutorial/forms.rst
index e8bc0c8b4..b08167edc 100644
--- a/docs/quick_tutorial/forms.rst
+++ b/docs/quick_tutorial/forms.rst
@@ -104,7 +104,7 @@ assets which need to be published. We don't have to know where on disk
it is located. We point at the package, then the path inside the package.
We just need to include a call to ``add_static_view`` to make that
-directory available at a URL. For Pyramid-specific pages,
+directory available at a URL. For Pyramid-specific packages,
Pyramid provides a facility (``config.include()``) which even makes
that unnecessary for consumers of a package. (Deform is not specific to
Pyramid.)
diff --git a/docs/quick_tutorial/functional_testing.rst b/docs/quick_tutorial/functional_testing.rst
index 205ddf5cb..09b05b0bc 100644
--- a/docs/quick_tutorial/functional_testing.rst
+++ b/docs/quick_tutorial/functional_testing.rst
@@ -37,12 +37,15 @@ Steps
$ $VENV/bin/python setup.py develop
$ $VENV/bin/easy_install webtest
-#. Let's extend ``unit_testing/tutorial/tests.py`` to include a
+#. Let's extend ``functional_testing/tutorial/tests.py`` to include a
functional test:
.. literalinclude:: functional_testing/tutorial/tests.py
:linenos:
+ Be sure this file is not executable, or ``nosetests`` may not
+ include your tests.
+
#. Now run the tests:
.. code-block:: bash
@@ -67,4 +70,4 @@ execution time of our tests.
Extra Credit
============
-#. Why do our functional tests use ``b''``? \ No newline at end of file
+#. Why do our functional tests use ``b''``?
diff --git a/docs/quick_tutorial/hello_world.rst b/docs/quick_tutorial/hello_world.rst
index 1a9ba4c9d..4ae80ca87 100644
--- a/docs/quick_tutorial/hello_world.rst
+++ b/docs/quick_tutorial/hello_world.rst
@@ -77,7 +77,7 @@ explanation:
#. *Lines 12-14*. Use Pyramid's :term:`configurator` to connect
:term:`view` code to a particular URL :term:`route`.
-#. *Lines 6-7*. Implement the view code that generates the
+#. *Lines 6-8*. Implement the view code that generates the
:term:`response`.
#. *Lines 15-17*. Publish a :term:`WSGI` app using an HTTP
diff --git a/docs/quick_tutorial/ini.rst b/docs/quick_tutorial/ini.rst
index 3402c50e8..b8720711b 100644
--- a/docs/quick_tutorial/ini.rst
+++ b/docs/quick_tutorial/ini.rst
@@ -14,9 +14,9 @@ Pyramid has a first-class concept of
:ref:`configuration <configuration_narr>` distinct from code.
This approach is optional, but its presence makes it distinct from
other Python web frameworks. It taps into Python's ``setuptools``
-library, which establishes conventions for how Python projects can be
-installed and provide "entry points". Pyramid uses an entry point to
-let a Pyramid application it where to find the WSGI app.
+library, which establishes conventions for installing and providing
+"entry points" for Python projects. Pyramid uses an entry point to
+let a Pyramid application know where to find the WSGI app.
Objectives
==========
diff --git a/docs/quick_tutorial/jinja2.rst b/docs/quick_tutorial/jinja2.rst
index 2f1e295dd..613542349 100644
--- a/docs/quick_tutorial/jinja2.rst
+++ b/docs/quick_tutorial/jinja2.rst
@@ -20,8 +20,8 @@ Objectives
Steps
=====
-#. In this step let's start by installing the ``pyramid_jinja2``
- add-on, the copying the ``view_class`` step's directory:
+#. In this step let's start by copying the ``view_class`` step's
+ directory, and then installing the ``pyramid_jinja2`` add-on.
.. code-block:: bash
@@ -45,12 +45,6 @@ Steps
.. literalinclude:: jinja2/tutorial/home.jinja2
:language: html
-#. Get the ``pyramid.includes`` into the functional test setup in
- ``jinja2/tutorial/tests.py``:
-
- .. literalinclude:: jinja2/tutorial/tests.py
- :linenos:
-
#. Now run the tests:
.. code-block:: bash
@@ -78,9 +72,6 @@ Our view code stayed largely the same. We simply changed the file
extension on the renderer. For the template, the syntax for Chameleon
and Jinja2's basic variable insertion is very similar.
-Our functional tests don't have ``development.ini`` so they needed the
-``pyramid.includes`` to be setup in the test setup.
-
Extra Credit
============
diff --git a/docs/quick_tutorial/jinja2/tutorial/tests.py b/docs/quick_tutorial/jinja2/tutorial/tests.py
index 0b22946f3..4381235ec 100644
--- a/docs/quick_tutorial/jinja2/tutorial/tests.py
+++ b/docs/quick_tutorial/jinja2/tutorial/tests.py
@@ -30,13 +30,7 @@ class TutorialViewTests(unittest.TestCase):
class TutorialFunctionalTests(unittest.TestCase):
def setUp(self):
from tutorial import main
-
- settings = {
- 'pyramid.includes': [
- 'pyramid_jinja2'
- ]
- }
- app = main({}, **settings)
+ app = main({})
from webtest import TestApp
self.testapp = TestApp(app)
diff --git a/docs/quick_tutorial/logging.rst b/docs/quick_tutorial/logging.rst
index 855ded59f..e07d23d6d 100644
--- a/docs/quick_tutorial/logging.rst
+++ b/docs/quick_tutorial/logging.rst
@@ -16,7 +16,7 @@ we might need to detect problems when other people use the site. We
need *logging*.
Fortunately Pyramid uses the normal Python approach to logging. The
-scaffold generated, in your ``development.ini``, a number of lines that
+scaffold generated, in your ``development.ini``, has a number of lines that
configure the logging for you to some reasonable defaults. You then see
messages sent by Pyramid (for example, when a new request comes in.)
diff --git a/docs/quick_tutorial/more_view_classes/tutorial/views.py b/docs/quick_tutorial/more_view_classes/tutorial/views.py
index 635de0520..156e468a9 100644
--- a/docs/quick_tutorial/more_view_classes/tutorial/views.py
+++ b/docs/quick_tutorial/more_view_classes/tutorial/views.py
@@ -5,7 +5,7 @@ from pyramid.view import (
@view_defaults(route_name='hello')
-class TutorialViews:
+class TutorialViews(object):
def __init__(self, request):
self.request = request
self.view_name = 'TutorialViews'
@@ -25,13 +25,13 @@ class TutorialViews:
def hello(self):
return {'page_title': 'Hello View'}
- # Posting to /home via the "Edit" submit button
+ # Posting to /howdy/first/last via the "Edit" submit button
@view_config(request_method='POST', renderer='edit.pt')
def edit(self):
new_name = self.request.params['new_name']
return {'page_title': 'Edit View', 'new_name': new_name}
- # Posting to /home via the "Delete" submit button
+ # Posting to /howdy/first/last via the "Delete" submit button
@view_config(request_method='POST', request_param='form.delete',
renderer='delete.pt')
def delete(self):
diff --git a/docs/tutorials/wiki/authorization.rst b/docs/tutorials/wiki/authorization.rst
index 93cd0c18e..6c98b6f3a 100644
--- a/docs/tutorials/wiki/authorization.rst
+++ b/docs/tutorials/wiki/authorization.rst
@@ -197,9 +197,9 @@ Add the following import statements to the
head of ``tutorial/tutorial/views.py``:
.. literalinclude:: src/authorization/tutorial/views.py
- :lines: 6-13,15-17
+ :lines: 6-17
:linenos:
- :emphasize-lines: 3,6-9,11
+ :emphasize-lines: 3,6-11
:language: python
(Only the highlighted lines, with other necessary modifications,
diff --git a/docs/tutorials/wiki/design.rst b/docs/tutorials/wiki/design.rst
index eb785dd1c..28380bd66 100644
--- a/docs/tutorials/wiki/design.rst
+++ b/docs/tutorials/wiki/design.rst
@@ -53,10 +53,10 @@ Security
We'll eventually be adding security to our application. The components we'll
use to do this are below.
-- USERS, a dictionary mapping usernames to their
+- USERS, a dictionary mapping :term:`userids <userid>` to their
corresponding passwords.
-- GROUPS, a dictionary mapping usernames to a
+- GROUPS, a dictionary mapping :term:`userids <userid>` to a
list of groups to which they belong to.
- ``groupfinder``, an *authorization callback* that looks up
diff --git a/docs/tutorials/wiki2/design.rst b/docs/tutorials/wiki2/design.rst
index df2c83398..ff7413668 100644
--- a/docs/tutorials/wiki2/design.rst
+++ b/docs/tutorials/wiki2/design.rst
@@ -53,7 +53,8 @@ Security
We'll eventually be adding security to our application. The components we'll
use to do this are below.
-- USERS, a dictionary mapping users names to their corresponding passwords.
+- USERS, a dictionary mapping users names (the user's :term:`userids
+ <userid>`) to their corresponding passwords.
- GROUPS, a dictionary mapping user names to a list of groups they belong to.
diff --git a/pyramid/authentication.py b/pyramid/authentication.py
index b84981bbc..e0e241e52 100644
--- a/pyramid/authentication.py
+++ b/pyramid/authentication.py
@@ -3,7 +3,6 @@ from codecs import utf_8_decode
from codecs import utf_8_encode
import hashlib
import base64
-import datetime
import re
import time as time_mod
import warnings
@@ -335,11 +334,11 @@ class RepozeWho1AuthenticationPolicy(CallbackAuthenticationPolicy):
effective_principals.extend(groups)
return effective_principals
- def remember(self, request, principal, **kw):
- """ Store the ``principal`` as ``repoze.who.userid``.
+ def remember(self, request, userid, **kw):
+ """ Store the ``userid`` as ``repoze.who.userid``.
The identity to authenticated to :mod:`repoze.who`
- will contain the given principal as ``userid``, and
+ will contain the given userid as ``userid``, and
provide all keyword arguments as additional identity
keys. Useful keys could be ``max_age`` or ``userdata``.
"""
@@ -348,7 +347,7 @@ class RepozeWho1AuthenticationPolicy(CallbackAuthenticationPolicy):
return []
environ = request.environ
identity = kw
- identity['repoze.who.userid'] = principal
+ identity['repoze.who.userid'] = userid
return identifier.remember(environ, identity)
def forget(self, request):
@@ -404,7 +403,7 @@ class RemoteUserAuthenticationPolicy(CallbackAuthenticationPolicy):
""" The ``REMOTE_USER`` value found within the ``environ``."""
return request.environ.get(self.environ_key)
- def remember(self, request, principal, **kw):
+ def remember(self, request, userid, **kw):
""" A no-op. The ``REMOTE_USER`` does not provide a protocol for
remembering the user. This will be application-specific and can
be done somewhere else or in a subclass."""
@@ -652,7 +651,7 @@ class AuthTktAuthenticationPolicy(CallbackAuthenticationPolicy):
if result:
return result['userid']
- def remember(self, request, principal, **kw):
+ def remember(self, request, userid, **kw):
""" Accepts the following kw args: ``max_age=<int-seconds>,
``tokens=<sequence-of-ascii-strings>``.
@@ -660,7 +659,7 @@ class AuthTktAuthenticationPolicy(CallbackAuthenticationPolicy):
the response.
"""
- return self.cookie.remember(request, principal, **kw)
+ return self.cookie.remember(request, userid, **kw)
def forget(self, request):
""" A list of headers which will delete appropriate cookies."""
@@ -741,7 +740,7 @@ def parse_ticket(secret, ticket, ip, hashalg='md5'):
If the ticket cannot be parsed, a ``BadTicket`` exception will be raised
with an explanation.
"""
- ticket = ticket.strip('"')
+ ticket = native_(ticket).strip('"')
digest_size = hashlib.new(hashalg).digest_size * 2
digest = ticket[:digest_size]
try:
@@ -929,7 +928,7 @@ class AuthTktCookieHelper(object):
if reissue and not hasattr(request, '_authtkt_reissued'):
if ( (now - timestamp) > self.reissue_time ):
- # work around https://github.com/Pylons/pyramid/issues#issue/108
+ # See https://github.com/Pylons/pyramid/issues#issue/108
tokens = list(filter(None, tokens))
headers = self.remember(request, userid, max_age=self.max_age,
tokens=tokens)
@@ -1061,13 +1060,13 @@ class SessionAuthenticationPolicy(CallbackAuthenticationPolicy):
self.userid_key = prefix + 'userid'
self.debug = debug
- def remember(self, request, principal, **kw):
- """ Store a principal in the session."""
- request.session[self.userid_key] = principal
+ def remember(self, request, userid, **kw):
+ """ Store a userid in the session."""
+ request.session[self.userid_key] = userid
return []
def forget(self, request):
- """ Remove the stored principal from the session."""
+ """ Remove the stored userid from the session."""
if self.userid_key in request.session:
del request.session[self.userid_key]
return []
@@ -1132,7 +1131,7 @@ class BasicAuthAuthenticationPolicy(CallbackAuthenticationPolicy):
if credentials:
return credentials[0]
- def remember(self, request, principal, **kw):
+ def remember(self, request, userid, **kw):
""" A no-op. Basic authentication does not provide a protocol for
remembering the user. Credentials are sent on every request.
diff --git a/pyramid/compat.py b/pyramid/compat.py
index bfa345b88..e9edda359 100644
--- a/pyramid/compat.py
+++ b/pyramid/compat.py
@@ -3,27 +3,27 @@ import platform
import sys
import types
-if platform.system() == 'Windows': # pragma: no cover
+if platform.system() == 'Windows': # pragma: no cover
WIN = True
-else: # pragma: no cover
+else: # pragma: no cover
WIN = False
-try: # pragma: no cover
+try: # pragma: no cover
import __pypy__
PYPY = True
-except: # pragma: no cover
+except: # pragma: no cover
__pypy__ = None
PYPY = False
try:
import cPickle as pickle
-except ImportError: # pragma: no cover
+except ImportError: # pragma: no cover
import pickle
# True if we are running on Python 3.
PY3 = sys.version_info[0] == 3
-if PY3: # pragma: no cover
+if PY3:
string_types = str,
integer_types = int,
class_types = type,
@@ -43,16 +43,16 @@ def text_(s, encoding='latin-1', errors='strict'):
``s.decode(encoding, errors)``, otherwise return ``s``"""
if isinstance(s, binary_type):
return s.decode(encoding, errors)
- return s # pragma: no cover
+ return s
def bytes_(s, encoding='latin-1', errors='strict'):
""" If ``s`` is an instance of ``text_type``, return
``s.encode(encoding, errors)``, otherwise return ``s``"""
- if isinstance(s, text_type): # pragma: no cover
+ if isinstance(s, text_type):
return s.encode(encoding, errors)
return s
-if PY3: # pragma: no cover
+if PY3:
def ascii_native_(s):
if isinstance(s, text_type):
s = s.encode('ascii')
@@ -72,7 +72,7 @@ Python 2: If ``s`` is an instance of ``text_type``, return
"""
-if PY3: # pragma: no cover
+if PY3:
def native_(s, encoding='latin-1', errors='strict'):
""" If ``s`` is an instance of ``text_type``, return
``s``, otherwise return ``str(s, encoding, errors)``"""
@@ -95,7 +95,7 @@ Python 2: If ``s`` is an instance of ``text_type``, return
``s.encode(encoding, errors)``, otherwise return ``str(s)``
"""
-if PY3: # pragma: no cover
+if PY3:
from urllib import parse
urlparse = parse
from urllib.parse import quote as url_quote
@@ -112,18 +112,19 @@ else:
from urllib import unquote as url_unquote
from urllib import urlencode as url_encode
from urllib2 import urlopen as url_open
+
def url_unquote_text(v, encoding='utf-8', errors='replace'): # pragma: no cover
v = url_unquote(v)
return v.decode(encoding, errors)
+
def url_unquote_native(v, encoding='utf-8', errors='replace'): # pragma: no cover
return native_(url_unquote_text(v, encoding, errors))
-
-if PY3: # pragma: no cover
+
+if PY3: # pragma: no cover
import builtins
exec_ = getattr(builtins, "exec")
-
def reraise(tp, value, tb=None):
if value is None:
value = tp
@@ -131,10 +132,9 @@ if PY3: # pragma: no cover
raise value.with_traceback(tb)
raise value
-
del builtins
-else: # pragma: no cover
+else: # pragma: no cover
def exec_(code, globs=None, locs=None):
"""Execute code in a namespace."""
if globs is None:
@@ -147,35 +147,38 @@ else: # pragma: no cover
locs = globs
exec("""exec code in globs, locs""")
-
exec_("""def reraise(tp, value, tb=None):
raise tp, value, tb
""")
-if PY3: # pragma: no cover
+if PY3: # pragma: no cover
def iteritems_(d):
return d.items()
+
def itervalues_(d):
return d.values()
+
def iterkeys_(d):
return d.keys()
-else: # pragma: no cover
+else: # pragma: no cover
def iteritems_(d):
return d.iteritems()
+
def itervalues_(d):
return d.itervalues()
+
def iterkeys_(d):
return d.iterkeys()
-if PY3: # pragma: no cover
+if PY3:
def map_(*arg):
return list(map(*arg))
else:
map_ = map
-
-if PY3: # pragma: no cover
+
+if PY3:
def is_nonstr_iter(v):
if isinstance(v, str):
return False
@@ -183,46 +186,49 @@ if PY3: # pragma: no cover
else:
def is_nonstr_iter(v):
return hasattr(v, '__iter__')
-
-if PY3: # pragma: no cover
+
+if PY3:
im_func = '__func__'
im_self = '__self__'
else:
im_func = 'im_func'
im_self = 'im_self'
-try: # pragma: no cover
+try:
import configparser
-except ImportError: # pragma: no cover
+except ImportError:
import ConfigParser as configparser
try:
- from Cookie import SimpleCookie
-except ImportError: # pragma: no cover
from http.cookies import SimpleCookie
+except ImportError:
+ from Cookie import SimpleCookie
-if PY3: # pragma: no cover
+if PY3:
from html import escape
else:
from cgi import escape
-try: # pragma: no cover
- input_ = raw_input
-except NameError: # pragma: no cover
+if PY3:
input_ = input
+else:
+ input_ = raw_input
+if PY3:
+ from inspect import getfullargspec as getargspec
+else:
+ from inspect import getargspec
-try:
- from StringIO import StringIO as NativeIO
-except ImportError: # pragma: no cover
+if PY3:
from io import StringIO as NativeIO
+else:
+ from io import BytesIO as NativeIO
# "json" is not an API; it's here to support older pyramid_debugtoolbar
# versions which attempt to import it
import json
-
-if PY3: # pragma: no cover
+if PY3:
# see PEP 3333 for why we encode WSGI PATH_INFO to latin-1 before
# decoding it to utf-8
def decode_path_info(path):
@@ -231,16 +237,47 @@ else:
def decode_path_info(path):
return path.decode('utf-8')
-if PY3: # pragma: no cover
+if PY3:
# see PEP 3333 for why we decode the path to latin-1
from urllib.parse import unquote_to_bytes
+
def unquote_bytes_to_wsgi(bytestring):
return unquote_to_bytes(bytestring).decode('latin-1')
else:
from urlparse import unquote as unquote_to_bytes
+
def unquote_bytes_to_wsgi(bytestring):
return unquote_to_bytes(bytestring)
+
def is_bound_method(ob):
return inspect.ismethod(ob) and getattr(ob, im_self, None) is not None
+# support annotations and keyword-only arguments in PY3
+if PY3: # pragma: no cover
+ from inspect import getfullargspec as getargspec
+else:
+ from inspect import getargspec
+
+if PY3: # pragma: no cover
+ from itertools import zip_longest
+else:
+ from itertools import izip_longest as zip_longest
+
+def is_unbound_method(fn):
+ """
+ This consistently verifies that the callable is bound to a
+ class.
+ """
+ is_bound = is_bound_method(fn)
+
+ if not is_bound and inspect.isroutine(fn):
+ spec = getargspec(fn)
+ has_self = len(spec.args) > 0 and spec.args[0] == 'self'
+
+ if PY3 and inspect.isfunction(fn) and has_self: # pragma: no cover
+ return True
+ elif inspect.ismethod(fn):
+ return True
+
+ return False
diff --git a/pyramid/config/__init__.py b/pyramid/config/__init__.py
index ebaae38a9..401def208 100644
--- a/pyramid/config/__init__.py
+++ b/pyramid/config/__init__.py
@@ -12,7 +12,10 @@ from pyramid.interfaces import (
IDebugLogger,
IExceptionResponse,
IPredicateList,
+ PHASE0_CONFIG,
PHASE1_CONFIG,
+ PHASE2_CONFIG,
+ PHASE3_CONFIG,
)
from pyramid.asset import resolve_asset_spec
@@ -23,6 +26,7 @@ from pyramid.compat import (
text_,
reraise,
string_types,
+ zip_longest,
)
from pyramid.events import ApplicationCreated
@@ -54,7 +58,9 @@ from pyramid.settings import aslist
from pyramid.threadlocal import manager
from pyramid.util import (
+ ActionInfo,
WeakOrderedSet,
+ action_method,
object_description,
)
@@ -74,11 +80,6 @@ from pyramid.config.zca import ZCAConfiguratorMixin
from pyramid.path import DottedNameResolver
-from pyramid.util import (
- action_method,
- ActionInfo,
- )
-
empty = text_('')
_marker = object()
@@ -86,6 +87,10 @@ ConfigurationError = ConfigurationError # pyflakes
not_ = not_ # pyflakes, this is an API
+PHASE0_CONFIG = PHASE0_CONFIG # api
+PHASE1_CONFIG = PHASE1_CONFIG # api
+PHASE2_CONFIG = PHASE2_CONFIG # api
+PHASE3_CONFIG = PHASE3_CONFIG # api
class Configurator(
TestingConfiguratorMixin,
@@ -125,6 +130,14 @@ class Configurator(
is passed (the default), the package is assumed to be the Python package
in which the *caller* of the ``Configurator`` constructor lives.
+ If the ``root_package`` is passed, it will propagate through the
+ configuration hierarchy as a way for included packages to locate
+ resources relative to the package in which the main ``Configurator`` was
+ created. If ``None`` is passed (the default), the ``root_package`` will
+ be derived from the ``package`` argument. The ``package`` attribute is
+ always pointing at the package being included when using :meth:`.include`,
+ whereas the ``root_package`` does not change.
+
If the ``settings`` argument is passed, it should be a Python dictionary
representing the :term:`deployment settings` for this application. These
are later retrievable using the
@@ -171,6 +184,11 @@ class Configurator(
See :ref:`changing_the_request_factory`. By default it is ``None``,
which means use the default request factory.
+ If ``response_factory`` is passed, it should be a :term:`response
+ factory` implementation or a :term:`dotted Python name` to the same.
+ See :ref:`changing_the_response_factory`. By default it is ``None``,
+ which means use the default response factory.
+
If ``default_permission`` is passed, it should be a
:term:`permission` string to be used as the default permission for
all view configuration registrations performed against this
@@ -182,7 +200,7 @@ class Configurator(
configurations which do not explicitly declare a permission will
always be executable by entirely anonymous users (any
authorization policy in effect is ignored).
-
+
.. seealso::
See also :ref:`setting_a_default_permission`.
@@ -243,6 +261,10 @@ class Configurator(
.. versionadded:: 1.3
The ``introspection`` argument.
+
+ .. versionadded:: 1.6
+ The ``root_package`` argument.
+ The ``response_factory`` argument.
"""
manager = manager # for testing injection
venusian = venusian # for testing injection
@@ -265,6 +287,7 @@ class Configurator(
debug_logger=None,
locale_negotiator=None,
request_factory=None,
+ response_factory=None,
default_permission=None,
session_factory=None,
default_view_mapper=None,
@@ -272,13 +295,17 @@ class Configurator(
exceptionresponse_view=default_exceptionresponse_view,
route_prefix=None,
introspection=True,
+ root_package=None,
):
if package is None:
package = caller_package()
+ if root_package is None:
+ root_package = package
name_resolver = DottedNameResolver(package)
self.name_resolver = name_resolver
self.package_name = name_resolver.get_package_name()
self.package = name_resolver.get_package()
+ self.root_package = root_package
self.registry = registry
self.autocommit = autocommit
self.route_prefix = route_prefix
@@ -295,6 +322,7 @@ class Configurator(
debug_logger=debug_logger,
locale_negotiator=locale_negotiator,
request_factory=request_factory,
+ response_factory=response_factory,
default_permission=default_permission,
session_factory=session_factory,
default_view_mapper=default_view_mapper,
@@ -310,6 +338,7 @@ class Configurator(
debug_logger=None,
locale_negotiator=None,
request_factory=None,
+ response_factory=None,
default_permission=None,
session_factory=None,
default_view_mapper=None,
@@ -397,6 +426,9 @@ class Configurator(
if request_factory:
self.set_request_factory(request_factory)
+ if response_factory:
+ self.set_response_factory(response_factory)
+
if default_permission:
self.set_default_permission(default_permission)
@@ -454,7 +486,7 @@ class Configurator(
_registry.registerSelfAdapter = registerSelfAdapter
# API
-
+
def _get_introspector(self):
introspector = getattr(self.registry, 'introspector', _marker)
if introspector is _marker:
@@ -747,6 +779,7 @@ class Configurator(
configurator = self.__class__(
registry=self.registry,
package=package_of(module),
+ root_package=self.root_package,
autocommit=self.autocommit,
route_prefix=route_prefix,
)
@@ -806,6 +839,7 @@ class Configurator(
configurator = self.__class__(
registry=self.registry,
package=package,
+ root_package=self.root_package,
autocommit=self.autocommit,
route_prefix=self.route_prefix,
introspection=self.introspection,
@@ -958,7 +992,7 @@ class Configurator(
class ActionState(object):
def __init__(self):
# NB "actions" is an API, dep'd upon by pyramid_zcml's load_zcml func
- self.actions = []
+ self.actions = []
self._seen_files = set()
def processSpec(self, spec):
@@ -1042,10 +1076,82 @@ class ActionState(object):
>>> output
[('f', (1,), {}), ('f', (2,), {})]
- """
+ The execution is re-entrant such that actions may be added by other
+ actions with the one caveat that the order of any added actions must
+ be equal to or larger than the current action.
+ >>> output = []
+ >>> def f(*a, **k):
+ ... output.append(('f', a, k))
+ ... context.actions.append((3, g, (8,), {}))
+ >>> def g(*a, **k):
+ ... output.append(('g', a, k))
+ >>> context.actions = [
+ ... (1, f, (1,)),
+ ... ]
+ >>> context.execute_actions()
+ >>> output
+ [('f', (1,), {}), ('g', (8,), {})]
+
+ """
try:
- for action in resolveConflicts(self.actions):
+ all_actions = []
+ executed_actions = []
+ pending_actions = iter([])
+
+ # resolve the new action list against what we have already
+ # executed -- if a new action appears intertwined in the list
+ # of already-executed actions then someone wrote a broken
+ # re-entrant action because it scheduled the action *after* it
+ # should have been executed (as defined by the action order)
+ def resume(actions):
+ for a, b in zip_longest(actions, executed_actions):
+ if b is None and a is not None:
+ # common case is that we are executing every action
+ yield a
+ elif b is not None and a != b:
+ raise ConfigurationError(
+ 'During execution a re-entrant action was added '
+ 'that modified the planned execution order in a '
+ 'way that is incompatible with what has already '
+ 'been executed.')
+ else:
+ # resolved action is in the same location as before,
+ # so we are in good shape, but the action is already
+ # executed so we skip it
+ assert b is not None and a == b
+
+ while True:
+ # We clear the actions list prior to execution so if there
+ # are some new actions then we add them to the mix and resolve
+ # conflicts again. This orders the new actions as well as
+ # ensures that the previously executed actions have no new
+ # conflicts.
+ if self.actions:
+ # Only resolve the new actions against executed_actions
+ # and pending_actions instead of everything to avoid
+ # redundant checks.
+ # Assume ``actions = resolveConflicts([A, B, C])`` which
+ # after conflict checks, resulted in ``actions == [A]``
+ # then we know action A won out or a conflict would have
+ # been raised. Thus, when action D is added later, we only
+ # need to check the new action against A.
+ # ``actions = resolveConflicts([A, D]) should drop the
+ # number of redundant checks down from O(n^2) closer to
+ # O(n lg n).
+ all_actions.extend(self.actions)
+ pending_actions = resume(resolveConflicts(
+ executed_actions
+ + list(pending_actions)
+ + self.actions
+ ))
+ self.actions = []
+
+ action = next(pending_actions, None)
+ if action is None:
+ # we are done!
+ break
+
callable = action['callable']
args = action['args']
kw = action['kw']
@@ -1066,15 +1172,19 @@ class ActionState(object):
ConfigurationExecutionError(t, v, info),
tb)
finally:
- del t, v, tb
+ del t, v, tb
if introspector is not None:
for introspectable in introspectables:
introspectable.register(introspector, info)
-
+
+ executed_actions.append(action)
+
finally:
if clear:
del self.actions[:]
+ else:
+ self.actions = all_actions
# this function is licensed under the ZPL (stolen from Zope)
def resolveConflicts(actions):
@@ -1195,4 +1305,3 @@ def expand_action(discriminator, callable=None, args=(), kw=None,
)
global_registries = WeakOrderedSet()
-
diff --git a/pyramid/config/adapters.py b/pyramid/config/adapters.py
index f6a652e3d..3d11980da 100644
--- a/pyramid/config/adapters.py
+++ b/pyramid/config/adapters.py
@@ -143,7 +143,7 @@ class AdaptersConfiguratorMixin(object):
Adds a subscriber predicate factory. The associated subscriber
predicate can later be named as a keyword argument to
:meth:`pyramid.config.Configurator.add_subscriber` in the
- ``**predicates`` anonyous keyword argument dictionary.
+ ``**predicates`` anonymous keyword argument dictionary.
``name`` should be the name of the predicate. It must be a valid
Python identifier (it will be used as a ``**predicates`` keyword
diff --git a/pyramid/config/assets.py b/pyramid/config/assets.py
index 0616e6cda..6dabea358 100644
--- a/pyramid/config/assets.py
+++ b/pyramid/config/assets.py
@@ -1,3 +1,4 @@
+import os
import pkg_resources
import sys
@@ -79,7 +80,8 @@ class OverrideProvider(pkg_resources.DefaultProvider):
return result
return pkg_resources.DefaultProvider.resource_listdir(
self, resource_name)
-
+
+
@implementer(IPackageOverrides)
class PackageOverrides(object):
# pkg_resources arg in kw args below for testing
@@ -97,57 +99,61 @@ class PackageOverrides(object):
# optional)...
# A __loader__ attribute is basically metadata, and setuptools
# uses it as such.
- package.__loader__ = self
+ package.__loader__ = self
# we call register_loader_type for every instantiation of this
# class; that's OK, it's idempotent to do it more than once.
pkg_resources.register_loader_type(self.__class__, OverrideProvider)
self.overrides = []
self.overridden_package_name = package.__name__
- def insert(self, path, package, prefix):
+ def insert(self, path, source):
if not path or path.endswith('/'):
- override = DirectoryOverride(path, package, prefix)
+ override = DirectoryOverride(path, source)
else:
- override = FileOverride(path, package, prefix)
+ override = FileOverride(path, source)
self.overrides.insert(0, override)
return override
- def search_path(self, resource_name):
+ def filtered_sources(self, resource_name):
for override in self.overrides:
o = override(resource_name)
if o is not None:
- package, name = o
- yield package, name
+ yield o
def get_filename(self, resource_name):
- for package, rname in self.search_path(resource_name):
- if pkg_resources.resource_exists(package, rname):
- return pkg_resources.resource_filename(package, rname)
+ for source, path in self.filtered_sources(resource_name):
+ result = source.get_filename(path)
+ if result is not None:
+ return result
def get_stream(self, resource_name):
- for package, rname in self.search_path(resource_name):
- if pkg_resources.resource_exists(package, rname):
- return pkg_resources.resource_stream(package, rname)
+ for source, path in self.filtered_sources(resource_name):
+ result = source.get_stream(path)
+ if result is not None:
+ return result
def get_string(self, resource_name):
- for package, rname in self.search_path(resource_name):
- if pkg_resources.resource_exists(package, rname):
- return pkg_resources.resource_string(package, rname)
+ for source, path in self.filtered_sources(resource_name):
+ result = source.get_string(path)
+ if result is not None:
+ return result
def has_resource(self, resource_name):
- for package, rname in self.search_path(resource_name):
- if pkg_resources.resource_exists(package, rname):
+ for source, path in self.filtered_sources(resource_name):
+ if source.exists(path):
return True
def isdir(self, resource_name):
- for package, rname in self.search_path(resource_name):
- if pkg_resources.resource_exists(package, rname):
- return pkg_resources.resource_isdir(package, rname)
+ for source, path in self.filtered_sources(resource_name):
+ result = source.isdir(path)
+ if result is not None:
+ return result
def listdir(self, resource_name):
- for package, rname in self.search_path(resource_name):
- if pkg_resources.resource_exists(package, rname):
- return pkg_resources.resource_listdir(package, rname)
+ for source, path in self.filtered_sources(resource_name):
+ result = source.listdir(path)
+ if result is not None:
+ return result
@property
def real_loader(self):
@@ -174,72 +180,184 @@ class PackageOverrides(object):
""" See IPEP302Loader.
"""
return self.real_loader.get_source(fullname)
-
+
class DirectoryOverride:
- def __init__(self, path, package, prefix):
+ def __init__(self, path, source):
self.path = path
- self.package = package
- self.prefix = prefix
self.pathlen = len(self.path)
+ self.source = source
def __call__(self, resource_name):
if resource_name.startswith(self.path):
- name = '%s%s' % (self.prefix, resource_name[self.pathlen:])
- return self.package, name
+ new_path = resource_name[self.pathlen:]
+ return self.source, new_path
class FileOverride:
- def __init__(self, path, package, prefix):
+ def __init__(self, path, source):
self.path = path
- self.package = package
- self.prefix = prefix
+ self.source = source
def __call__(self, resource_name):
if resource_name == self.path:
- return self.package, self.prefix
+ return self.source, ''
+
+
+class PackageAssetSource(object):
+ """
+ An asset source relative to a package.
+
+ If this asset source is a file, then we expect the ``prefix`` to point
+ to the new name of the file, and the incoming ``resource_name`` will be
+ the empty string, as returned by the ``FileOverride``.
+
+ """
+ def __init__(self, package, prefix):
+ self.package = package
+ if hasattr(package, '__name__'):
+ self.pkg_name = package.__name__
+ else:
+ self.pkg_name = package
+ self.prefix = prefix
+
+ def get_path(self, resource_name):
+ return '%s%s' % (self.prefix, resource_name)
+
+ def get_filename(self, resource_name):
+ path = self.get_path(resource_name)
+ if pkg_resources.resource_exists(self.pkg_name, path):
+ return pkg_resources.resource_filename(self.pkg_name, path)
+
+ def get_stream(self, resource_name):
+ path = self.get_path(resource_name)
+ if pkg_resources.resource_exists(self.pkg_name, path):
+ return pkg_resources.resource_stream(self.pkg_name, path)
+
+ def get_string(self, resource_name):
+ path = self.get_path(resource_name)
+ if pkg_resources.resource_exists(self.pkg_name, path):
+ return pkg_resources.resource_string(self.pkg_name, path)
+
+ def exists(self, resource_name):
+ path = self.get_path(resource_name)
+ if pkg_resources.resource_exists(self.pkg_name, path):
+ return True
+
+ def isdir(self, resource_name):
+ path = self.get_path(resource_name)
+ if pkg_resources.resource_exists(self.pkg_name, path):
+ return pkg_resources.resource_isdir(self.pkg_name, path)
+
+ def listdir(self, resource_name):
+ path = self.get_path(resource_name)
+ if pkg_resources.resource_exists(self.pkg_name, path):
+ return pkg_resources.resource_listdir(self.pkg_name, path)
+
+
+class FSAssetSource(object):
+ """
+ An asset source relative to a path in the filesystem.
+
+ """
+ def __init__(self, prefix):
+ self.prefix = prefix
+
+ def get_filename(self, resource_name):
+ if resource_name:
+ path = os.path.join(self.prefix, resource_name)
+ else:
+ path = self.prefix
+
+ if os.path.exists(path):
+ return path
+
+ def get_stream(self, resource_name):
+ path = self.get_filename(resource_name)
+ if path is not None:
+ return open(path, 'rb')
+
+ def get_string(self, resource_name):
+ stream = self.get_stream(resource_name)
+ if stream is not None:
+ with stream:
+ return stream.read()
+
+ def exists(self, resource_name):
+ path = self.get_filename(resource_name)
+ if path is not None:
+ return True
+
+ def isdir(self, resource_name):
+ path = self.get_filename(resource_name)
+ if path is not None:
+ return os.path.isdir(path)
+
+ def listdir(self, resource_name):
+ path = self.get_filename(resource_name)
+ if path is not None:
+ return os.listdir(path)
class AssetsConfiguratorMixin(object):
- def _override(self, package, path, override_package, override_prefix,
+ def _override(self, package, path, override_source,
PackageOverrides=PackageOverrides):
pkg_name = package.__name__
- override_pkg_name = override_package.__name__
override = self.registry.queryUtility(IPackageOverrides, name=pkg_name)
if override is None:
override = PackageOverrides(package)
self.registry.registerUtility(override, IPackageOverrides,
name=pkg_name)
- override.insert(path, override_pkg_name, override_prefix)
+ override.insert(path, override_source)
@action_method
def override_asset(self, to_override, override_with, _override=None):
""" Add a :app:`Pyramid` asset override to the current
configuration state.
- ``to_override`` is a :term:`asset specification` to the
+ ``to_override`` is an :term:`asset specification` to the
asset being overridden.
- ``override_with`` is a :term:`asset specification` to the
- asset that is performing the override.
+ ``override_with`` is an :term:`asset specification` to the
+ asset that is performing the override. This may also be an absolute
+ path.
See :ref:`assets_chapter` for more
information about asset overrides."""
if to_override == override_with:
- raise ConfigurationError('You cannot override an asset with itself')
+ raise ConfigurationError(
+ 'You cannot override an asset with itself')
package = to_override
path = ''
if ':' in to_override:
package, path = to_override.split(':', 1)
- override_package = override_with
- override_prefix = ''
- if ':' in override_with:
- override_package, override_prefix = override_with.split(':', 1)
-
# *_isdir = override is package or directory
- overridden_isdir = path=='' or path.endswith('/')
- override_isdir = override_prefix=='' or override_prefix.endswith('/')
+ overridden_isdir = path == '' or path.endswith('/')
+
+ if os.path.isabs(override_with):
+ override_source = FSAssetSource(override_with)
+ if not os.path.exists(override_with):
+ raise ConfigurationError(
+ 'Cannot override asset with an absolute path that does '
+ 'not exist')
+ override_isdir = os.path.isdir(override_with)
+ override_package = None
+ override_prefix = override_with
+ else:
+ override_package = override_with
+ override_prefix = ''
+ if ':' in override_with:
+ override_package, override_prefix = override_with.split(':', 1)
+
+ __import__(override_package)
+ to_package = sys.modules[override_package]
+ override_source = PackageAssetSource(to_package, override_prefix)
+
+ override_isdir = (
+ override_prefix == '' or
+ override_with.endswith('/')
+ )
if overridden_isdir and (not override_isdir):
raise ConfigurationError(
@@ -255,10 +373,8 @@ class AssetsConfiguratorMixin(object):
def register():
__import__(package)
- __import__(override_package)
from_package = sys.modules[package]
- to_package = sys.modules[override_package]
- override(from_package, path, to_package, override_prefix)
+ override(from_package, path, override_source)
intr = self.introspectable(
'asset overrides',
diff --git a/pyramid/config/factories.py b/pyramid/config/factories.py
index 1990c377a..f0b6252ae 100644
--- a/pyramid/config/factories.py
+++ b/pyramid/config/factories.py
@@ -1,9 +1,10 @@
-from zope.deprecation import deprecate
+from zope.deprecation import deprecated
from zope.interface import implementer
from pyramid.interfaces import (
IDefaultRootFactory,
IRequestFactory,
+ IResponseFactory,
IRequestExtensions,
IRootFactory,
ISessionFactory,
@@ -13,9 +14,11 @@ from pyramid.traversal import DefaultRootFactory
from pyramid.util import (
action_method,
- InstancePropertyMixin,
+ get_callable_name,
+ InstancePropertyHelper,
)
+
class FactoriesConfiguratorMixin(object):
@action_method
def set_root_factory(self, factory):
@@ -32,9 +35,10 @@ class FactoriesConfiguratorMixin(object):
factory = self.maybe_dotted(factory)
if factory is None:
factory = DefaultRootFactory
+
def register():
self.registry.registerUtility(factory, IRootFactory)
- self.registry.registerUtility(factory, IDefaultRootFactory) # b/c
+ self.registry.registerUtility(factory, IDefaultRootFactory) # b/c
intr = self.introspectable('root factories',
None,
@@ -43,7 +47,7 @@ class FactoriesConfiguratorMixin(object):
intr['factory'] = factory
self.action(IRootFactory, register, introspectables=(intr,))
- _set_root_factory = set_root_factory # bw compat
+ _set_root_factory = set_root_factory # bw compat
@action_method
def set_session_factory(self, factory):
@@ -59,6 +63,7 @@ class FactoriesConfiguratorMixin(object):
achieve the same purpose.
"""
factory = self.maybe_dotted(factory)
+
def register():
self.registry.registerUtility(factory, ISessionFactory)
intr = self.introspectable('session factory', None,
@@ -88,6 +93,7 @@ class FactoriesConfiguratorMixin(object):
can be used to achieve the same purpose.
"""
factory = self.maybe_dotted(factory)
+
def register():
self.registry.registerUtility(factory, IRequestFactory)
intr = self.introspectable('request factory', None,
@@ -97,6 +103,31 @@ class FactoriesConfiguratorMixin(object):
self.action(IRequestFactory, register, introspectables=(intr,))
@action_method
+ def set_response_factory(self, factory):
+ """ The object passed as ``factory`` should be an object (or a
+ :term:`dotted Python name` which refers to an object) which
+ will be used by the :app:`Pyramid` as the default response
+ objects. The factory should conform to the
+ :class:`pyramid.interfaces.IResponseFactory` interface.
+
+ .. note::
+
+ Using the ``response_factory`` argument to the
+ :class:`pyramid.config.Configurator` constructor
+ can be used to achieve the same purpose.
+ """
+ factory = self.maybe_dotted(factory)
+
+ def register():
+ self.registry.registerUtility(factory, IResponseFactory)
+
+ intr = self.introspectable('response factory', None,
+ self.object_description(factory),
+ 'response factory')
+ intr['factory'] = factory
+ self.action(IResponseFactory, register, introspectables=(intr,))
+
+ @action_method
def add_request_method(self,
callable=None,
name=None,
@@ -143,10 +174,12 @@ class FactoriesConfiguratorMixin(object):
property = property or reify
if property:
- name, callable = InstancePropertyMixin._make_property(
+ name, callable = InstancePropertyHelper.make_property(
callable, name=name, reify=reify)
elif name is None:
name = callable.__name__
+ else:
+ name = get_callable_name(name)
def register():
exts = self.registry.queryUtility(IRequestExtensions)
@@ -180,8 +213,6 @@ class FactoriesConfiguratorMixin(object):
introspectables=(intr,))
@action_method
- @deprecate('set_request_propery() is deprecated as of Pyramid 1.5; use '
- 'add_request_method() with the property=True argument instead')
def set_request_property(self, callable, name=None, reify=False):
""" Add a property to the request object.
@@ -195,9 +226,14 @@ class FactoriesConfiguratorMixin(object):
self.add_request_method(
callable, name=name, property=not reify, reify=reify)
+ deprecated(
+ set_request_property,
+ 'set_request_propery() is deprecated as of Pyramid 1.5; use '
+ 'add_request_method() with the property=True argument instead')
+
+
@implementer(IRequestExtensions)
class _RequestExtensions(object):
def __init__(self):
self.descriptors = {}
self.methods = {}
-
diff --git a/pyramid/config/routes.py b/pyramid/config/routes.py
index f1463b50b..24f38a4fd 100644
--- a/pyramid/config/routes.py
+++ b/pyramid/config/routes.py
@@ -138,6 +138,18 @@ class RoutesConfiguratorMixin(object):
.. versionadded:: 1.1
+ accept
+
+ This value represents a match query for one or more mimetypes in the
+ ``Accept`` HTTP request header. If this value is specified, it must
+ be in one of the following forms: a mimetype match token in the form
+ ``text/plain``, a wildcard mimetype match token in the form
+ ``text/*`` or a match-all wildcard mimetype match token in the form
+ ``*/*``. If any of the forms matches the ``Accept`` header of the
+ request, or if the ``Accept`` header isn't set at all in the request,
+ this will match the current route. If this does not match the
+ ``Accept`` header of the request, route matching continues.
+
Predicate Arguments
pattern
@@ -220,19 +232,6 @@ class RoutesConfiguratorMixin(object):
case of the header name is not significant. If this
predicate returns ``False``, route matching continues.
- accept
-
- This value represents a match query for one or more
- mimetypes in the ``Accept`` HTTP request header. If this
- value is specified, it must be in one of the following
- forms: a mimetype match token in the form ``text/plain``, a
- wildcard mimetype match token in the form ``text/*`` or a
- match-all wildcard mimetype match token in the form ``*/*``.
- If any of the forms matches the ``Accept`` header of the
- request, or if the ``Accept`` header isn't set at all in the
- request, this predicate will be true. If this predicate
- returns ``False``, route matching continues.
-
effective_principals
If specified, this value should be a :term:`principal` identifier or
@@ -303,6 +302,8 @@ class RoutesConfiguratorMixin(object):
# check for an external route; an external route is one which is
# is a full url (e.g. 'http://example.com/{id}')
parsed = urlparse.urlparse(pattern)
+ external_url = pattern
+
if parsed.hostname:
pattern = parsed.path
@@ -357,6 +358,10 @@ class RoutesConfiguratorMixin(object):
intr['pregenerator'] = pregenerator
intr['static'] = static
intr['use_global_views'] = use_global_views
+
+ if static is True:
+ intr['external_url'] = external_url
+
introspectables.append(intr)
if factory:
diff --git a/pyramid/config/settings.py b/pyramid/config/settings.py
index 565a6699c..492b7d524 100644
--- a/pyramid/config/settings.py
+++ b/pyramid/config/settings.py
@@ -17,7 +17,7 @@ class SettingsConfiguratorMixin(object):
def add_settings(self, settings=None, **kw):
"""Augment the :term:`deployment settings` with one or more
- key/value pairs.
+ key/value pairs.
You may pass a dictionary::
@@ -117,6 +117,11 @@ class Settings(dict):
config_prevent_http_cache)
eff_prevent_http_cache = asbool(eget('PYRAMID_PREVENT_HTTP_CACHE',
config_prevent_http_cache))
+ config_prevent_cachebust = self.get('prevent_cachebust', '')
+ config_prevent_cachebust = self.get('pyramid.prevent_cachebust',
+ config_prevent_cachebust)
+ eff_prevent_cachebust = asbool(eget('PYRAMID_PREVENT_CACHEBUST',
+ config_prevent_cachebust))
update = {
'debug_authorization': eff_debug_all or eff_debug_auth,
@@ -128,6 +133,7 @@ class Settings(dict):
'reload_assets':eff_reload_all or eff_reload_assets,
'default_locale_name':eff_locale_name,
'prevent_http_cache':eff_prevent_http_cache,
+ 'prevent_cachebust':eff_prevent_cachebust,
'pyramid.debug_authorization': eff_debug_all or eff_debug_auth,
'pyramid.debug_notfound': eff_debug_all or eff_debug_notfound,
@@ -138,6 +144,7 @@ class Settings(dict):
'pyramid.reload_assets':eff_reload_all or eff_reload_assets,
'pyramid.default_locale_name':eff_locale_name,
'pyramid.prevent_http_cache':eff_prevent_http_cache,
+ 'pyramid.prevent_cachebust':eff_prevent_cachebust,
}
self.update(update)
diff --git a/pyramid/config/util.py b/pyramid/config/util.py
index 892592196..23cdc6be8 100644
--- a/pyramid/config/util.py
+++ b/pyramid/config/util.py
@@ -3,6 +3,7 @@ import inspect
from pyramid.compat import (
bytes_,
+ getargspec,
is_nonstr_iter,
)
@@ -201,7 +202,7 @@ def takes_one_arg(callee, attr=None, argname=None):
return False
try:
- argspec = inspect.getargspec(fn)
+ argspec = getargspec(fn)
except TypeError:
return False
diff --git a/pyramid/config/views.py b/pyramid/config/views.py
index 7a6157ec8..6be81163f 100644
--- a/pyramid/config/views.py
+++ b/pyramid/config/views.py
@@ -34,6 +34,7 @@ from pyramid.interfaces import (
)
from pyramid import renderers
+from pyramid.static import PathSegmentMd5CacheBuster
from pyramid.compat import (
string_types,
@@ -41,14 +42,10 @@ from pyramid.compat import (
url_quote,
WIN,
is_bound_method,
- is_nonstr_iter
+ is_unbound_method,
+ is_nonstr_iter,
)
-from pyramid.encode import (
- quote_plus,
- urlencode,
-)
-
from pyramid.exceptions import (
ConfigurationError,
PredicateMismatch,
@@ -57,6 +54,7 @@ from pyramid.exceptions import (
from pyramid.httpexceptions import (
HTTPForbidden,
HTTPNotFound,
+ default_exceptionresponse_view,
)
from pyramid.registry import (
@@ -302,7 +300,7 @@ class ViewDeriver(object):
raise PredicateMismatch(
'predicate mismatch for view %s (%s)' % (
view_name, predicate.text()))
- return view(context, request)
+ return view(context, request)
def checker(context, request):
return all((predicate(context, request) for predicate in
preds))
@@ -351,7 +349,6 @@ class ViewDeriver(object):
def _rendered_view(self, view, view_renderer):
def rendered_view(context, request):
- renderer = view_renderer
result = view(context, request)
if result.__class__ is Response: # potential common case
response = result
@@ -369,6 +366,8 @@ class ViewDeriver(object):
name=renderer_name,
package=self.kw.get('package'),
registry = registry)
+ else:
+ renderer = view_renderer.clone()
if '__view__' in attrs:
view_inst = attrs.pop('__view__')
else:
@@ -421,6 +420,12 @@ class DefaultViewMapper(object):
self.attr = kw.get('attr')
def __call__(self, view):
+ if is_unbound_method(view) and self.attr is None:
+ raise ConfigurationError((
+ 'Unbound method calls are not supported, please set the class '
+ 'as your `view` and the method as your `attr`'
+ ))
+
if inspect.isclass(view):
view = self.map_class(view)
else:
@@ -844,6 +849,18 @@ class ViewsConfiguratorMixin(object):
very useful for 'civilians' who are just developing stock Pyramid
applications. Pay no attention to the man behind the curtain.
+ accept
+
+ This value represents a match query for one or more mimetypes in the
+ ``Accept`` HTTP request header. If this value is specified, it must
+ be in one of the following forms: a mimetype match token in the form
+ ``text/plain``, a wildcard mimetype match token in the form
+ ``text/*`` or a match-all wildcard mimetype match token in the form
+ ``*/*``. If any of the forms matches the ``Accept`` header of the
+ request, or if the ``Accept`` header isn't set at all in the request,
+ this will match the current view. If this does not match the
+ ``Accept`` header of the request, view matching continues.
+
Predicate Arguments
name
@@ -894,8 +911,8 @@ class ViewsConfiguratorMixin(object):
request_param
- This value can be any string or any sequence of strings. A view
- declaration with this argument ensures that the view will only be
+ This value can be any string or any sequence of strings. A view
+ declaration with this argument ensures that the view will only be
called when the :term:`request` has a key in the ``request.params``
dictionary (an HTTP ``GET`` or ``POST`` variable) that has a
name which matches the supplied value (if the value is a string)
@@ -944,17 +961,6 @@ class ViewsConfiguratorMixin(object):
This is useful for detecting AJAX requests issued from
jQuery, Prototype and other Javascript libraries.
- accept
-
- The value of this argument represents a match query for one
- or more mimetypes in the ``Accept`` HTTP request header. If
- this value is specified, it must be in one of the following
- forms: a mimetype match token in the form ``text/plain``, a
- wildcard mimetype match token in the form ``text/*`` or a
- match-all wildcard mimetype match token in the form ``*/*``.
- If any of the forms matches the ``Accept`` header of the
- request, this predicate will be true.
-
header
This value represents an HTTP header name or a header
@@ -1001,7 +1007,7 @@ class ViewsConfiguratorMixin(object):
Note that using this feature requires a :term:`session factory` to
have been configured.
-
+
.. versionadded:: 1.4a2
physical_path
@@ -1039,7 +1045,7 @@ class ViewsConfiguratorMixin(object):
This value should be a sequence of references to custom
predicate callables. Use custom predicates when no set of
predefined predicates do what you need. Custom predicates
- can be combined with predefined predicates as necessary.
+ can be combined with predefined predicates as necessary.
Each custom predicate callable should accept two arguments:
``context`` and ``request`` and should return either
``True`` or ``False`` after doing arbitrary evaluation of
@@ -1074,7 +1080,7 @@ class ViewsConfiguratorMixin(object):
DeprecationWarning,
stacklevel=4
)
-
+
view = self.maybe_dotted(view)
context = self.maybe_dotted(context)
for_ = self.maybe_dotted(for_)
@@ -1160,7 +1166,7 @@ class ViewsConfiguratorMixin(object):
view_desc = self.object_description(view)
tmpl_intr = None
-
+
view_intr = self.introspectable('views',
discriminator,
view_desc,
@@ -1189,10 +1195,6 @@ class ViewsConfiguratorMixin(object):
predlist = self.get_predlist('view')
def register(permission=permission, renderer=renderer):
- # the discrim_func above is guaranteed to have been called already
- order = view_intr['order']
- preds = view_intr['predicates']
- phash = view_intr['phash']
request_iface = IRequest
if route_name is not None:
request_iface = self.registry.queryUtility(IRouteRequest,
@@ -1569,7 +1571,7 @@ class ViewsConfiguratorMixin(object):
wrapper=None,
route_name=None,
request_type=None,
- request_method=None,
+ request_method=None,
request_param=None,
containment=None,
xhr=None,
@@ -1595,9 +1597,12 @@ class ViewsConfiguratorMixin(object):
config.add_forbidden_view(forbidden)
+ If ``view`` argument is not provided, the view callable defaults to
+ :func:`~pyramid.httpexceptions.default_exceptionresponse_view`.
+
All arguments have the same meaning as
:meth:`pyramid.config.Configurator.add_view` and each predicate
- argument restricts the set of circumstances under which this notfound
+ argument restricts the set of circumstances under which this forbidden
view will be invoked. Unlike
:meth:`pyramid.config.Configurator.add_view`, this method will raise
an exception if passed ``name``, ``permission``, ``context``,
@@ -1612,7 +1617,10 @@ class ViewsConfiguratorMixin(object):
'%s may not be used as an argument to add_forbidden_view'
% arg
)
-
+
+ if view is None:
+ view = default_exceptionresponse_view
+
settings = dict(
view=view,
context=HTTPForbidden,
@@ -1623,7 +1631,7 @@ class ViewsConfiguratorMixin(object):
containment=containment,
xhr=xhr,
accept=accept,
- header=header,
+ header=header,
path_info=path_info,
custom_predicates=custom_predicates,
decorator=decorator,
@@ -1638,7 +1646,7 @@ class ViewsConfiguratorMixin(object):
return self.add_view(**settings)
set_forbidden_view = add_forbidden_view # deprecated sorta-bw-compat alias
-
+
@viewdefaults
@action_method
def add_notfound_view(
@@ -1649,7 +1657,7 @@ class ViewsConfiguratorMixin(object):
wrapper=None,
route_name=None,
request_type=None,
- request_method=None,
+ request_method=None,
request_param=None,
containment=None,
xhr=None,
@@ -1675,6 +1683,9 @@ class ViewsConfiguratorMixin(object):
config.add_notfound_view(notfound)
+ If ``view`` argument is not provided, the view callable defaults to
+ :func:`~pyramid.httpexceptions.default_exceptionresponse_view`.
+
All arguments except ``append_slash`` have the same meaning as
:meth:`pyramid.config.Configurator.add_view` and each predicate
argument restricts the set of circumstances under which this notfound
@@ -1692,6 +1703,24 @@ class ViewsConfiguratorMixin(object):
Pyramid will return the result of the view callable provided as
``view``, as normal.
+ If the argument provided as ``append_slash`` is not a boolean but
+ instead implements :class:`~pyramid.interfaces.IResponse`, the
+ append_slash logic will behave as if ``append_slash=True`` was passed,
+ but the provided class will be used as the response class instead of
+ the default :class:`~pyramid.httpexceptions.HTTPFound` response class
+ when a redirect is performed. For example:
+
+ .. code-block:: python
+
+ from pyramid.httpexceptions import HTTPMovedPermanently
+ config.add_notfound_view(append_slash=HTTPMovedPermanently)
+
+ The above means that a redirect to a slash-appended route will be
+ attempted, but instead of :class:`~pyramid.httpexceptions.HTTPFound`
+ being used, :class:`~pyramid.httpexceptions.HTTPMovedPermanently will
+ be used` for the redirect response if a slash-appended route is found.
+
+ .. versionchanged:: 1.6
.. versionadded:: 1.3
"""
for arg in ('name', 'permission', 'context', 'for_', 'http_cache'):
@@ -1700,7 +1729,10 @@ class ViewsConfiguratorMixin(object):
'%s may not be used as an argument to add_notfound_view'
% arg
)
-
+
+ if view is None:
+ view = default_exceptionresponse_view
+
settings = dict(
view=view,
context=HTTPNotFound,
@@ -1711,7 +1743,7 @@ class ViewsConfiguratorMixin(object):
containment=containment,
xhr=xhr,
accept=accept,
- header=header,
+ header=header,
path_info=path_info,
custom_predicates=custom_predicates,
decorator=decorator,
@@ -1723,7 +1755,12 @@ class ViewsConfiguratorMixin(object):
settings.update(predicates)
if append_slash:
view = self._derive_view(view, attr=attr, renderer=renderer)
- view = AppendSlashNotFoundViewFactory(view)
+ if IResponse.implementedBy(append_slash):
+ view = AppendSlashNotFoundViewFactory(
+ view, redirect_class=append_slash,
+ )
+ else:
+ view = AppendSlashNotFoundViewFactory(view)
settings['view'] = view
else:
settings['attr'] = attr
@@ -1786,7 +1823,20 @@ class ViewsConfiguratorMixin(object):
``Expires`` and ``Cache-Control`` headers for static assets served.
Note that this argument has no effect when the ``name`` is a *url
prefix*. By default, this argument is ``None``, meaning that no
- particular Expires or Cache-Control headers are set in the response.
+ particular Expires or Cache-Control headers are set in the response,
+ unless ``cachebust`` is specified.
+
+ The ``cachebust`` keyword argument may be set to cause
+ :meth:`~pyramid.request.Request.static_url` to use cache busting when
+ generating URLs. See :ref:`cache_busting` for general information
+ about cache busting. The value of the ``cachebust`` argument may be
+ ``True``, in which case a default cache busting implementation is used.
+ The value of the ``cachebust`` argument may also be an object which
+ implements :class:`~pyramid.interfaces.ICacheBuster`. See the
+ :mod:`~pyramid.static` module for some implementations. If the
+ ``cachebust`` argument is provided, the default for ``cache_max_age``
+ is modified to be ten years. ``cache_max_age`` may still be explicitly
+ provided to override this default.
The ``permission`` keyword argument is used to specify the
:term:`permission` required by a user to execute the static view. By
@@ -1884,6 +1934,8 @@ def isexception(o):
@implementer(IStaticURLInfo)
class StaticURLInfo(object):
+ # Indirection for testing
+ _default_cachebust = PathSegmentMd5CacheBuster
def _get_registrations(self, registry):
try:
@@ -1897,11 +1949,14 @@ class StaticURLInfo(object):
registry = request.registry
except AttributeError: # bw compat (for tests)
registry = get_current_registry()
- for (url, spec, route_name) in self._get_registrations(registry):
+ registrations = self._get_registrations(registry)
+ for (url, spec, route_name, cachebust) in registrations:
if path.startswith(spec):
subpath = path[len(spec):]
if WIN: # pragma: no cover
subpath = subpath.replace('\\', '/') # windows
+ if cachebust:
+ subpath, kw = cachebust(subpath, kw)
if url is None:
kw['subpath'] = subpath
return request.route_url(route_name, **kw)
@@ -1928,7 +1983,7 @@ class StaticURLInfo(object):
sep = os.sep
else:
sep = '/'
- if not spec.endswith(sep):
+ if not spec.endswith(sep) and not spec.endswith(':'):
spec = spec + sep
# we also make sure the name ends with a slash, purely as a
@@ -1941,6 +1996,21 @@ class StaticURLInfo(object):
# make sure it ends with a slash
name = name + '/'
+ if config.registry.settings.get('pyramid.prevent_cachebust'):
+ cb = None
+ else:
+ cb = extra.pop('cachebust', None)
+ if cb is True:
+ cb = self._default_cachebust()
+ if cb:
+ def cachebust(subpath, kw):
+ subpath_tuple = tuple(subpath.split('/'))
+ subpath_tuple, kw = cb.pregenerate(
+ spec + subpath, subpath_tuple, kw)
+ return '/'.join(subpath_tuple), kw
+ else:
+ cachebust = None
+
if url_parse(name).netloc:
# it's a URL
# url, spec, route_name
@@ -1949,10 +2019,14 @@ class StaticURLInfo(object):
else:
# it's a view name
url = None
- cache_max_age = extra.pop('cache_max_age', None)
+ ten_years = 10 * 365 * 24 * 60 * 60 # more or less
+ default = ten_years if cb else None
+ cache_max_age = extra.pop('cache_max_age', default)
+
# create a view
+ cb_match = getattr(cb, 'match', None)
view = static_view(spec, cache_max_age=cache_max_age,
- use_subpath=True)
+ use_subpath=True, cachebust_match=cb_match)
# Mutate extra to allow factory, etc to be passed through here.
# Treat permission specially because we'd like to default to
@@ -1993,7 +2067,7 @@ class StaticURLInfo(object):
registrations.pop(idx)
# url, spec, route_name
- registrations.append((url, spec, route_name))
+ registrations.append((url, spec, route_name, cachebust))
intr = config.introspectable('static views',
name,
@@ -2004,4 +2078,3 @@ class StaticURLInfo(object):
config.action(None, callable=register, introspectables=(intr,))
-
diff --git a/pyramid/decorator.py b/pyramid/decorator.py
index 0d17bc398..df30c5e10 100644
--- a/pyramid/decorator.py
+++ b/pyramid/decorator.py
@@ -1,3 +1,6 @@
+import functools
+
+
class reify(object):
""" Use as a class method decorator. It operates almost exactly like the
Python ``@property`` decorator, but it puts the result of the method it
@@ -26,10 +29,7 @@ class reify(object):
"""
def __init__(self, wrapped):
self.wrapped = wrapped
- try:
- self.__doc__ = wrapped.__doc__
- except: # pragma: no cover
- pass
+ functools.update_wrapper(self, wrapped)
def __get__(self, inst, objtype=None):
if inst is None:
diff --git a/pyramid/httpexceptions.py b/pyramid/httpexceptions.py
index ebee39ada..a30129e16 100644
--- a/pyramid/httpexceptions.py
+++ b/pyramid/httpexceptions.py
@@ -52,6 +52,9 @@ Exception
* 422 - HTTPUnprocessableEntity
* 423 - HTTPLocked
* 424 - HTTPFailedDependency
+ * 428 - HTTPPreconditionRequired
+ * 429 - HTTPTooManyRequests
+ * 431 - HTTPRequestHeaderFieldsTooLarge
HTTPServerError
* 500 - HTTPInternalServerError
* 501 - HTTPNotImplemented
@@ -868,7 +871,12 @@ class HTTPUnprocessableEntity(HTTPClientError):
subclass of :class:`~HTTPClientError`
This indicates that the server is unable to process the contained
- instructions. Only for WebDAV.
+ instructions.
+
+ May be used to notify the client that their JSON/XML is well formed, but
+ not correct for the current request.
+
+ See RFC4918 section 11 for more information.
code: 422, title: Unprocessable Entity
"""
@@ -881,7 +889,7 @@ class HTTPLocked(HTTPClientError):
"""
subclass of :class:`~HTTPClientError`
- This indicates that the resource is locked. Only for WebDAV
+ This indicates that the resource is locked.
code: 423, title: Locked
"""
@@ -896,7 +904,6 @@ class HTTPFailedDependency(HTTPClientError):
This indicates that the method could not be performed because the
requested action depended on another action and that action failed.
- Only for WebDAV.
code: 424, title: Failed Dependency
"""
@@ -907,6 +914,62 @@ class HTTPFailedDependency(HTTPClientError):
'The method could not be performed because the requested '
'action dependended on another action and that action failed')
+class HTTPPreconditionRequired(HTTPClientError):
+ """
+ subclass of :class:`~HTTPClientError`
+
+ This indicates that the origin server requires the
+ request to be conditional.
+
+ Its typical use is to avoid the "lost update" problem, where a client
+ GETs a resource's state, modifies it, and PUTs it back to the server,
+ when meanwhile a third party has modified the state on the server,
+ leading to a conflict. By requiring requests to be conditional, the
+ server can assure that clients are working with the correct copies.
+
+ RFC 6585.3
+
+ code: 428, title: Precondition Required
+ """
+ code = 428
+ title = 'Precondition Required'
+ explanation = (
+ 'The origin server requires the request to be conditional.')
+
+class HTTPTooManyRequests(HTTPClientError):
+ """
+ subclass of :class:`~HTTPClientError`
+
+ This indicates that the user has sent too many
+ requests in a given amount of time ("rate limiting").
+
+ RFC 6585.4
+
+ code: 429, title: Too Many Requests
+ """
+ code = 429
+ title = 'Too Many Requests'
+ explanation = (
+ 'The action could not be performed because there were too '
+ 'many requests by the client.')
+
+class HTTPRequestHeaderFieldsTooLarge(HTTPClientError):
+ """
+ subclass of :class:`~HTTPClientError`
+
+ This indicates that the server is unwilling to process
+ the request because its header fields are too large. The request MAY
+ be resubmitted after reducing the size of the request header fields.
+
+ RFC 6585.5
+
+ code: 431, title: Request Header Fields Too Large
+ """
+ code = 431
+ title = 'Request Header Fields Too Large'
+ explanation = (
+ 'The requests header fields were too large.')
+
############################################################
## 5xx Server Error
############################################################
diff --git a/pyramid/i18n.py b/pyramid/i18n.py
index 4c8f4b55d..c30351f7a 100644
--- a/pyramid/i18n.py
+++ b/pyramid/i18n.py
@@ -331,9 +331,9 @@ class Translations(gettext.GNUTranslations, object):
"""Like ``ugettext()``, but look the message up in the specified
domain.
"""
- if PY3: # pragma: no cover
+ if PY3:
return self._domains.get(domain, self).gettext(message)
- else: # pragma: no cover
+ else:
return self._domains.get(domain, self).ugettext(message)
def dngettext(self, domain, singular, plural, num):
@@ -352,10 +352,10 @@ class Translations(gettext.GNUTranslations, object):
"""Like ``ungettext()`` but look the message up in the specified
domain.
"""
- if PY3: # pragma: no cover
+ if PY3:
return self._domains.get(domain, self).ngettext(
singular, plural, num)
- else: # pragma: no cover
+ else:
return self._domains.get(domain, self).ungettext(
singular, plural, num)
diff --git a/pyramid/interfaces.py b/pyramid/interfaces.py
index aa2dbdafd..bab91b0ee 100644
--- a/pyramid/interfaces.py
+++ b/pyramid/interfaces.py
@@ -382,6 +382,9 @@ class IRendererInfo(Interface):
settings = Attribute('The deployment settings dictionary related '
'to the current application')
+ def clone():
+ """ Return a shallow copy that does not share any mutable state."""
+
class IRendererFactory(Interface):
def __call__(info):
""" Return an object that implements
@@ -439,36 +442,55 @@ class IViewMapperFactory(Interface):
class IAuthenticationPolicy(Interface):
""" An object representing a Pyramid authentication policy. """
+
def authenticated_userid(request):
- """ Return the authenticated userid or ``None`` if no authenticated
- userid can be found. This method of the policy should ensure that a
- record exists in whatever persistent store is used related to the
- user (the user should not have been deleted); if a record associated
- with the current id does not exist in a persistent store, it should
- return ``None``."""
+ """ Return the authenticated :term:`userid` or ``None`` if
+ no authenticated userid can be found. This method of the
+ policy should ensure that a record exists in whatever
+ persistent store is used related to the user (the user
+ should not have been deleted); if a record associated with
+ the current id does not exist in a persistent store, it
+ should return ``None``.
+
+ """
def unauthenticated_userid(request):
- """ Return the *unauthenticated* userid. This method performs the
- same duty as ``authenticated_userid`` but is permitted to return the
- userid based only on data present in the request; it needn't (and
- shouldn't) check any persistent store to ensure that the user record
- related to the request userid exists."""
+ """ Return the *unauthenticated* userid. This method
+ performs the same duty as ``authenticated_userid`` but is
+ permitted to return the userid based only on data present
+ in the request; it needn't (and shouldn't) check any
+ persistent store to ensure that the user record related to
+ the request userid exists.
+
+ This method is intended primarily a helper to assist the
+ ``authenticated_userid`` method in pulling credentials out
+ of the request data, abstracting away the specific headers,
+ query strings, etc that are used to authenticate the request.
+
+ """
def effective_principals(request):
""" Return a sequence representing the effective principals
- including the userid and any groups belonged to by the current
- user, including 'system' groups such as Everyone and
- Authenticated. """
+ typically including the :term:`userid` and any groups belonged
+ to by the current user, always including 'system' groups such
+ as ``pyramid.security.Everyone`` and
+ ``pyramid.security.Authenticated``.
+
+ """
- def remember(request, principal, **kw):
+ def remember(request, userid, **kw):
""" Return a set of headers suitable for 'remembering' the
- principal named ``principal`` when set in a response. An
- individual authentication policy and its consumers can decide
- on the composition and meaning of ``**kw.`` """
+ :term:`userid` named ``userid`` when set in a response. An
+ individual authentication policy and its consumers can
+ decide on the composition and meaning of ``**kw``.
+
+ """
def forget(request):
""" Return a set of headers suitable for 'forgetting' the
- current user on subsequent requests. """
+ current user on subsequent requests.
+
+ """
class IAuthorizationPolicy(Interface):
""" An object representing a Pyramid authorization policy. """
@@ -563,18 +585,16 @@ class IStaticURLInfo(Interface):
""" Generate a URL for the given path """
class IResponseFactory(Interface):
- """ A utility which generates a response factory """
- def __call__():
- """ Return a response factory (e.g. a callable that returns an object
- implementing IResponse, e.g. :class:`pyramid.response.Response`). It
- should accept all the arguments that the Pyramid Response class
- accepts."""
+ """ A utility which generates a response """
+ def __call__(request):
+ """ Return a response object implementing IResponse,
+ e.g. :class:`pyramid.response.Response`). It should handle the
+ case when ``request`` is ``None``."""
class IRequestFactory(Interface):
""" A utility which generates a request """
def __call__(environ):
- """ Return an object implementing IRequest, e.g. an instance
- of ``pyramid.request.Request``"""
+ """ Return an instance of ``pyramid.request.Request``"""
def blank(path):
""" Return an empty request object (see
@@ -708,7 +728,7 @@ class IRoute(Interface):
pregenerator = Attribute('This attribute should either be ``None`` or '
'a callable object implementing the '
'``IRoutePregenerator`` interface')
-
+
def match(path):
"""
If the ``path`` passed to this function can be matched by the
@@ -803,7 +823,7 @@ class IContextURL(IResourceURL):
# <__main__.Fudge object at 0x1cda890>
# <object object at 0x7fa678f3e2a0> <object object at 0x7fa678f3e2a0>
# <__main__.Another object at 0x1cda850>
-
+
def virtual_root():
""" Return the virtual root related to a request and the
current context"""
@@ -837,9 +857,9 @@ class IPEP302Loader(Interface):
def get_code(fullname):
""" Return the code object for the module identified by 'fullname'.
-
+
Return 'None' if it's a built-in or extension module.
-
+
If the loader doesn't have the code object but it does have the source
code, return the compiled source code.
@@ -848,16 +868,16 @@ class IPEP302Loader(Interface):
def get_source(fullname):
""" Return the source code for the module identified by 'fullname'.
-
+
Return a string, using newline characters for line endings, or None
if the source is not available.
-
+
Raise ImportError if the module can't be found by the importer at all.
"""
def get_filename(fullname):
""" Return the value of '__file__' if the named module was loaded.
-
+
If the module is not found, raise ImportError.
"""
@@ -1164,10 +1184,54 @@ class IJSONAdapter(Interface):
class IPredicateList(Interface):
""" Interface representing a predicate list """
+class ICacheBuster(Interface):
+ """
+ Instances of ``ICacheBuster`` may be provided as arguments to
+ :meth:`~pyramid.config.Configurator.add_static_view`. Instances of
+ ``ICacheBuster`` provide mechanisms for generating a cache bust token for
+ a static asset, modifying a static asset URL to include a cache bust token,
+ and, optionally, unmodifying a static asset URL in order to look up an
+ asset. See :ref:`cache_busting`.
+
+ .. versionadded:: 1.6
+ """
+ def pregenerate(pathspec, subpath, kw):
+ """
+ Modifies a subpath and/or keyword arguments from which a static asset
+ URL will be computed during URL generation. The ``pathspec`` argument
+ is the path specification for the resource to be cache busted.
+ The ``subpath`` argument is a tuple of path elements that represent the
+ portion of the asset URL which is used to find the asset. The ``kw``
+ argument is a dict of keywords that are to be passed eventually to
+ :meth:`~pyramid.request.Request.route_url` for URL generation. The
+ return value should be a two-tuple of ``(subpath, kw)`` which are
+ versions of the same arguments modified to include the cachebust token
+ in the generated URL.
+ """
+
+ def match(subpath):
+ """
+ Performs the logical inverse of
+ :meth:`~pyramid.interfaces.ICacheBuster.pregenerate` by taking a
+ subpath from a cache busted URL and removing the cache bust token, so
+ that :app:`Pyramid` can find the underlying asset.
+
+ ``subpath`` is the subpath portion of the URL for an incoming request
+ for a static asset. The return value should be the same tuple with the
+ cache busting token elided.
+
+ If the cache busting scheme in use doesn't specifically modify the path
+ portion of the generated URL (e.g. it adds a query string), a method
+ which implements this interface may not be necessary. It is
+ permissible for an instance of
+ :class:`~pyramid.interfaces.ICacheBuster` to omit this method.
+ """
+
# configuration phases: a lower phase number means the actions associated
# with this phase will be executed earlier than those with later phase
# numbers. The default phase number is 0, FTR.
+PHASE0_CONFIG = -30
PHASE1_CONFIG = -20
PHASE2_CONFIG = -10
-
+PHASE3_CONFIG = 0
diff --git a/pyramid/path.py b/pyramid/path.py
index 470e766f8..f2d8fff55 100644
--- a/pyramid/path.py
+++ b/pyramid/path.py
@@ -337,8 +337,14 @@ class DottedNameResolver(Resolver):
value = package.__name__
else:
value = package.__name__ + value
- return pkg_resources.EntryPoint.parse(
- 'x=%s' % value).load(False)
+ # Calling EntryPoint.load with an argument is deprecated.
+ # See https://pythonhosted.org/setuptools/history.html#id8
+ ep = pkg_resources.EntryPoint.parse('x=%s' % value)
+ if hasattr(ep, 'resolve'):
+ # setuptools>=10.2
+ return ep.resolve() # pragma: NO COVER
+ else:
+ return ep.load(False) # pragma: NO COVER
def _zope_dottedname_style(self, value, package):
""" package.module.attr style """
diff --git a/pyramid/registry.py b/pyramid/registry.py
index 606251a8d..8c05940b9 100644
--- a/pyramid/registry.py
+++ b/pyramid/registry.py
@@ -5,6 +5,7 @@ from zope.interface import implementer
from zope.interface.registry import Components
from pyramid.compat import text_
+from pyramid.decorator import reify
from pyramid.interfaces import (
ISettings,
@@ -42,6 +43,10 @@ class Registry(Components, dict):
# defeat bool determination via dict.__len__
return True
+ @reify
+ def package_name(self):
+ return self.__name__
+
def registerSubscriptionAdapter(self, *arg, **kw):
result = Components.registerSubscriptionAdapter(self, *arg, **kw)
self.has_listeners = True
diff --git a/pyramid/renderers.py b/pyramid/renderers.py
index 108255ee4..088d451bb 100644
--- a/pyramid/renderers.py
+++ b/pyramid/renderers.py
@@ -1,3 +1,4 @@
+import contextlib
import json
import os
@@ -10,7 +11,6 @@ from zope.interface.registry import Components
from pyramid.interfaces import (
IJSONAdapter,
IRendererFactory,
- IResponseFactory,
IRendererInfo,
)
@@ -25,7 +25,7 @@ from pyramid.events import BeforeRender
from pyramid.path import caller_package
-from pyramid.response import Response
+from pyramid.response import _get_response_factory
from pyramid.threadlocal import get_current_registry
# API
@@ -74,24 +74,16 @@ def render(renderer_name, value, request=None, package=None):
helper = RendererHelper(name=renderer_name, package=package,
registry=registry)
- saved_response = None
- # save the current response, preventing the renderer from affecting it
- attrs = request.__dict__ if request is not None else {}
- if 'response' in attrs:
- saved_response = attrs['response']
- del attrs['response']
-
- result = helper.render(value, None, request=request)
-
- # restore the original response, overwriting any changes
- if saved_response is not None:
- attrs['response'] = saved_response
- elif 'response' in attrs:
- del attrs['response']
+ with temporary_response(request):
+ result = helper.render(value, None, request=request)
return result
-def render_to_response(renderer_name, value, request=None, package=None):
+def render_to_response(renderer_name,
+ value,
+ request=None,
+ package=None,
+ response=None):
""" Using the renderer ``renderer_name`` (a template
or a static renderer), render the value (or set of values) using
the result of the renderer's ``__call__`` method (usually a string
@@ -122,9 +114,16 @@ def render_to_response(renderer_name, value, request=None, package=None):
Supply a ``request`` parameter in order to provide the renderer
with the most correct 'system' values (``request`` and ``context``
- in particular). Keep in mind that if the ``request`` parameter is
- not passed in, any changes to ``request.response`` attributes made
- before calling this function will be ignored.
+ in particular). Keep in mind that any changes made to ``request.response``
+ prior to calling this function will not be reflected in the resulting
+ response object. A new response object will be created for each call
+ unless one is passed as the ``response`` argument.
+
+ .. versionchanged:: 1.6
+ In previous versions, any changes made to ``request.response`` outside
+ of this function call would affect the returned response. This is no
+ longer the case. If you wish to send in a pre-initialized response
+ then you may pass one in the ``response`` argument.
"""
try:
@@ -135,7 +134,33 @@ def render_to_response(renderer_name, value, request=None, package=None):
package = caller_package()
helper = RendererHelper(name=renderer_name, package=package,
registry=registry)
- return helper.render_to_response(value, None, request=request)
+
+ with temporary_response(request):
+ if response is not None:
+ request.response = response
+ result = helper.render_to_response(value, None, request=request)
+
+ return result
+
+@contextlib.contextmanager
+def temporary_response(request):
+ """
+ Temporarily delete request.response and restore it afterward.
+ """
+ saved_response = None
+ # save the current response, preventing the renderer from affecting it
+ attrs = request.__dict__ if request is not None else {}
+ if 'response' in attrs:
+ saved_response = attrs['response']
+ del attrs['response']
+
+ yield
+
+ # restore the original response, overwriting any changes
+ if saved_response is not None:
+ attrs['response'] = saved_response
+ elif 'response' in attrs:
+ del attrs['response']
def get_renderer(renderer_name, package=None):
""" Return the renderer object for the renderer ``renderer_name``.
@@ -248,7 +273,7 @@ class JSON(object):
When you've done this, the JSON renderer will be able to serialize
instances of the ``Foo`` class when they're encountered in your view
results."""
-
+
self.components.registerAdapter(adapter, (type_or_iface,),
IJSONAdapter)
@@ -265,7 +290,7 @@ class JSON(object):
response.content_type = 'application/json'
default = self._make_default(request)
return self.serializer(value, default=default, **self.kw)
-
+
return _render
def _make_default(self, request):
@@ -286,7 +311,7 @@ json_renderer_factory = JSON() # bw compat
class JSONP(JSON):
""" `JSONP <http://en.wikipedia.org/wiki/JSONP>`_ renderer factory helper
which implements a hybrid json/jsonp renderer. JSONP is useful for
- making cross-domain AJAX requests.
+ making cross-domain AJAX requests.
Configure a JSONP renderer using the
:meth:`pyramid.config.Configurator.add_renderer` API at application
@@ -309,7 +334,7 @@ class JSONP(JSON):
config = Configurator()
config.add_renderer('jsonp', JSONP(param_name='callback', indent=4))
-
+
.. versionchanged:: 1.4
The ability of this class to accept a ``**kw`` in its constructor.
@@ -356,19 +381,19 @@ class JSONP(JSON):
``self.param_name`` is present in request.GET; otherwise returns
plain-JSON encoded string with content-type ``application/json``"""
def _render(value, system):
- request = system['request']
+ request = system.get('request')
default = self._make_default(request)
val = self.serializer(value, default=default, **self.kw)
- callback = request.GET.get(self.param_name)
- if callback is None:
- ct = 'application/json'
- body = val
- else:
- ct = 'application/javascript'
- body = '%s(%s);' % (callback, val)
- response = request.response
- if response.content_type == response.default_content_type:
- response.content_type = ct
+ ct = 'application/json'
+ body = val
+ if request is not None:
+ callback = request.GET.get(self.param_name)
+ if callback is not None:
+ ct = 'application/javascript'
+ body = '%s(%s);' % (callback, val)
+ response = request.response
+ if response.content_type == response.default_content_type:
+ response.content_type = ct
return body
return _render
@@ -448,14 +473,16 @@ class RendererHelper(object):
if response is None:
# request is None or request is not a pyramid.response.Response
registry = self.registry
- response_factory = registry.queryUtility(IResponseFactory,
- default=Response)
-
- response = response_factory()
+ response_factory = _get_response_factory(registry)
+ response = response_factory(request)
if result is not None:
if isinstance(result, text_type):
response.text = result
+ elif isinstance(result, bytes):
+ response.body = result
+ elif hasattr(result, '__iter__'):
+ response.app_iter = result
else:
response.body = result
@@ -487,18 +514,18 @@ class NullRendererHelper(RendererHelper):
@property
def settings(self):
- return get_current_registry().settings or {}
+ return {}
def render_view(self, request, value, view, context):
return value
def render(self, value, system_values, request=None):
return value
-
+
def render_to_response(self, value, system_values, request=None):
return value
def clone(self, name=None, package=None, registry=None):
return self
-
+
null_renderer = NullRendererHelper()
diff --git a/pyramid/request.py b/pyramid/request.py
index 6318049ee..3cbe5d9e3 100644
--- a/pyramid/request.py
+++ b/pyramid/request.py
@@ -1,3 +1,4 @@
+from collections import deque
import json
from zope.interface import implementer
@@ -7,33 +8,37 @@ from webob import BaseRequest
from pyramid.interfaces import (
IRequest,
+ IRequestExtensions,
IResponse,
ISessionFactory,
- IResponseFactory,
)
from pyramid.compat import (
text_,
bytes_,
native_,
+ iteritems_,
)
from pyramid.decorator import reify
from pyramid.i18n import LocalizerRequestMixin
-from pyramid.response import Response
+from pyramid.response import Response, _get_response_factory
from pyramid.security import (
AuthenticationAPIMixin,
AuthorizationAPIMixin,
)
from pyramid.url import URLMethodsMixin
-from pyramid.util import InstancePropertyMixin
+from pyramid.util import (
+ InstancePropertyHelper,
+ InstancePropertyMixin,
+)
class TemplateContext(object):
pass
class CallbackMethodsMixin(object):
- response_callbacks = ()
- finished_callbacks = ()
+ response_callbacks = None
+ finished_callbacks = None
def add_response_callback(self, callback):
"""
Add a callback to the set of callbacks to be called by the
@@ -72,15 +77,15 @@ class CallbackMethodsMixin(object):
"""
callbacks = self.response_callbacks
- if not callbacks:
- callbacks = []
+ if callbacks is None:
+ callbacks = deque()
callbacks.append(callback)
self.response_callbacks = callbacks
def _process_response_callbacks(self, response):
callbacks = self.response_callbacks
while callbacks:
- callback = callbacks.pop(0)
+ callback = callbacks.popleft()
callback(self, response)
def add_finished_callback(self, callback):
@@ -132,15 +137,15 @@ class CallbackMethodsMixin(object):
"""
callbacks = self.finished_callbacks
- if not callbacks:
- callbacks = []
+ if callbacks is None:
+ callbacks = deque()
callbacks.append(callback)
self.finished_callbacks = callbacks
def _process_finished_callbacks(self):
callbacks = self.finished_callbacks
while callbacks:
- callback = callbacks.pop(0)
+ callback = callbacks.popleft()
callback(self)
@implementer(IRequest)
@@ -213,10 +218,8 @@ class Request(
right" attributes (e.g. by calling ``request.response.set_cookie()``)
within a view that uses a renderer. Mutations to this response object
will be preserved in the response sent to the client."""
- registry = self.registry
- response_factory = registry.queryUtility(IResponseFactory,
- default=Response)
- return response_factory()
+ response_factory = _get_response_factory(self.registry)
+ return response_factory(self)
def is_response(self, ob):
""" Return ``True`` if the object passed as ``ob`` is a valid
@@ -309,3 +312,22 @@ def call_app_with_subpath_as_path_info(request, app):
new_request.environ['PATH_INFO'] = new_path_info
return new_request.get_response(app)
+
+def apply_request_extensions(request, extensions=None):
+ """Apply request extensions (methods and properties) to an instance of
+ :class:`pyramid.interfaces.IRequest`. This method is dependent on the
+ ``request`` containing a properly initialized registry.
+
+ After invoking this method, the ``request`` should have the methods
+ and properties that were defined using
+ :meth:`pyramid.config.Configurator.add_request_method`.
+ """
+ if extensions is None:
+ extensions = request.registry.queryUtility(IRequestExtensions)
+ if extensions is not None:
+ for name, fn in iteritems_(extensions.methods):
+ method = fn.__get__(request, request.__class__)
+ setattr(request, name, method)
+
+ InstancePropertyHelper.apply_properties(
+ request, extensions.descriptors)
diff --git a/pyramid/response.py b/pyramid/response.py
index d11fd0123..892e5dfff 100644
--- a/pyramid/response.py
+++ b/pyramid/response.py
@@ -8,7 +8,8 @@ import venusian
from webob import Response as _Response
from zope.interface import implementer
-from pyramid.interfaces import IResponse
+from pyramid.interfaces import IResponse, IResponseFactory
+
def init_mimetypes(mimetypes):
# this is a function so it can be unittested
@@ -143,7 +144,7 @@ class response_adapter(object):
@response_adapter(dict, list)
def myadapter(ob):
return Response(json.dumps(ob))
-
+
This method will have no effect until a :term:`scan` is performed
agains the package or module which contains it, ala:
@@ -167,3 +168,15 @@ class response_adapter(object):
def __call__(self, wrapped):
self.venusian.attach(wrapped, self.register, category='pyramid')
return wrapped
+
+
+def _get_response_factory(registry):
+ """ Obtain a :class: `pyramid.response.Response` using the
+ `pyramid.interfaces.IResponseFactory`.
+ """
+ response_factory = registry.queryUtility(
+ IResponseFactory,
+ default=lambda r: Response()
+ )
+
+ return response_factory
diff --git a/pyramid/router.py b/pyramid/router.py
index ba4f85b18..0b1ecade7 100644
--- a/pyramid/router.py
+++ b/pyramid/router.py
@@ -27,6 +27,7 @@ from pyramid.events import (
from pyramid.exceptions import PredicateMismatch
from pyramid.httpexceptions import HTTPNotFound
from pyramid.request import Request
+from pyramid.request import apply_request_extensions
from pyramid.threadlocal import manager
from pyramid.traversal import (
@@ -213,7 +214,7 @@ class Router(object):
try:
extensions = self.request_extensions
if extensions is not None:
- request._set_extensions(extensions)
+ apply_request_extensions(request, extensions=extensions)
response = handle_request(request)
if request.response_callbacks:
diff --git a/pyramid/scaffolds/alchemy/development.ini_tmpl b/pyramid/scaffolds/alchemy/development.ini_tmpl
index e54a8609c..448803c8f 100644
--- a/pyramid/scaffolds/alchemy/development.ini_tmpl
+++ b/pyramid/scaffolds/alchemy/development.ini_tmpl
@@ -68,4 +68,4 @@ level = NOTSET
formatter = generic
[formatter_generic]
-format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s
+format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s
diff --git a/pyramid/scaffolds/alchemy/production.ini_tmpl b/pyramid/scaffolds/alchemy/production.ini_tmpl
index b316ec9ca..022bc0b7b 100644
--- a/pyramid/scaffolds/alchemy/production.ini_tmpl
+++ b/pyramid/scaffolds/alchemy/production.ini_tmpl
@@ -59,4 +59,4 @@ level = NOTSET
formatter = generic
[formatter_generic]
-format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s
+format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s
diff --git a/pyramid/scaffolds/starter/development.ini_tmpl b/pyramid/scaffolds/starter/development.ini_tmpl
index 842cd61d9..c2a28e178 100644
--- a/pyramid/scaffolds/starter/development.ini_tmpl
+++ b/pyramid/scaffolds/starter/development.ini_tmpl
@@ -57,4 +57,4 @@ level = NOTSET
formatter = generic
[formatter_generic]
-format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s
+format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s
diff --git a/pyramid/scaffolds/starter/production.ini_tmpl b/pyramid/scaffolds/starter/production.ini_tmpl
index 6a123abf5..b2681c71d 100644
--- a/pyramid/scaffolds/starter/production.ini_tmpl
+++ b/pyramid/scaffolds/starter/production.ini_tmpl
@@ -51,4 +51,4 @@ level = NOTSET
formatter = generic
[formatter_generic]
-format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s
+format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s
diff --git a/pyramid/scaffolds/tests.py b/pyramid/scaffolds/tests.py
index dfbf9b6cf..db828759e 100644
--- a/pyramid/scaffolds/tests.py
+++ b/pyramid/scaffolds/tests.py
@@ -6,9 +6,9 @@ import tempfile
import time
try:
+ import http.client as httplib
+except ImportError:
import httplib
-except ImportError: # pragma: no cover
- import http.client as httplib #py3
class TemplateTest(object):
def make_venv(self, directory): # pragma: no cover
diff --git a/pyramid/scaffolds/zodb/development.ini_tmpl b/pyramid/scaffolds/zodb/development.ini_tmpl
index f57d559bf..199ddfab4 100644
--- a/pyramid/scaffolds/zodb/development.ini_tmpl
+++ b/pyramid/scaffolds/zodb/development.ini_tmpl
@@ -62,4 +62,4 @@ level = NOTSET
formatter = generic
[formatter_generic]
-format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s
+format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s
diff --git a/pyramid/scaffolds/zodb/production.ini_tmpl b/pyramid/scaffolds/zodb/production.ini_tmpl
index c231e159d..522ff7651 100644
--- a/pyramid/scaffolds/zodb/production.ini_tmpl
+++ b/pyramid/scaffolds/zodb/production.ini_tmpl
@@ -57,4 +57,4 @@ level = NOTSET
formatter = generic
[formatter_generic]
-format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s
+format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s
diff --git a/pyramid/scripting.py b/pyramid/scripting.py
index fdb4aa430..d9587338f 100644
--- a/pyramid/scripting.py
+++ b/pyramid/scripting.py
@@ -1,12 +1,12 @@
from pyramid.config import global_registries
from pyramid.exceptions import ConfigurationError
-from pyramid.request import Request
from pyramid.interfaces import (
- IRequestExtensions,
IRequestFactory,
IRootFactory,
)
+from pyramid.request import Request
+from pyramid.request import apply_request_extensions
from pyramid.threadlocal import manager as threadlocal_manager
from pyramid.traversal import DefaultRootFactory
@@ -77,9 +77,7 @@ def prepare(request=None, registry=None):
request.registry = registry
threadlocals = {'registry':registry, 'request':request}
threadlocal_manager.push(threadlocals)
- extensions = registry.queryUtility(IRequestExtensions)
- if extensions is not None:
- request._set_extensions(extensions)
+ apply_request_extensions(request)
def closer():
threadlocal_manager.pop()
root_factory = registry.queryUtility(IRootFactory,
diff --git a/pyramid/scripts/pcreate.py b/pyramid/scripts/pcreate.py
index 4c1f432fb..d2c5f8c27 100644
--- a/pyramid/scripts/pcreate.py
+++ b/pyramid/scripts/pcreate.py
@@ -18,7 +18,7 @@ def main(argv=sys.argv, quiet=False):
class PCreateCommand(object):
verbosity = 1 # required
description = "Render Pyramid scaffolding to an output directory"
- usage = "usage: %prog [options] output_directory"
+ usage = "usage: %prog [options] -s <scaffold> output_directory"
parser = optparse.OptionParser(usage, description=description)
parser.add_option('-s', '--scaffold',
dest='scaffold_name',
@@ -63,8 +63,16 @@ class PCreateCommand(object):
def run(self):
if self.options.list:
return self.show_scaffolds()
+ if not self.options.scaffold_name and not self.args:
+ if not self.quiet: # pragma: no cover
+ self.parser.print_help()
+ self.out('')
+ self.show_scaffolds()
+ return 2
if not self.options.scaffold_name:
- self.out('You must provide at least one scaffold name')
+ self.out('You must provide at least one scaffold name: -s <scaffold name>')
+ self.out('')
+ self.show_scaffolds()
return 2
if not self.args:
self.out('You must provide a project name')
@@ -81,7 +89,8 @@ class PCreateCommand(object):
args = self.args
output_dir = os.path.abspath(os.path.normpath(args[0]))
project_name = os.path.basename(os.path.split(output_dir)[1])
- pkg_name = _bad_chars_re.sub('', project_name.lower())
+ pkg_name = _bad_chars_re.sub(
+ '', project_name.lower().replace('-', '_'))
safe_name = pkg_resources.safe_name(project_name)
egg_name = pkg_resources.to_filename(safe_name)
diff --git a/pyramid/scripts/prequest.py b/pyramid/scripts/prequest.py
index 2ab3b8bb9..34eeadf32 100644
--- a/pyramid/scripts/prequest.py
+++ b/pyramid/scripts/prequest.py
@@ -5,7 +5,7 @@ import textwrap
from pyramid.compat import url_unquote
from pyramid.request import Request
-from pyramid.paster import get_app
+from pyramid.paster import get_app, setup_logging
from pyramid.scripts.common import parse_vars
def main(argv=sys.argv, quiet=False):
@@ -97,12 +97,18 @@ class PRequestCommand(object):
if not self.quiet:
print(msg)
+ def configure_logging(self, app_spec):
+ setup_logging(app_spec)
+
def run(self):
if not len(self.args) >= 2:
self.out('You must provide at least two arguments')
return 2
app_spec = self.args[0]
path = self.args[1]
+
+ self.configure_logging(app_spec)
+
if not path.startswith('/'):
path = '/' + path
diff --git a/pyramid/scripts/proutes.py b/pyramid/scripts/proutes.py
index 5784026bb..544947724 100644
--- a/pyramid/scripts/proutes.py
+++ b/pyramid/scripts/proutes.py
@@ -1,14 +1,233 @@
+import fnmatch
import optparse
import sys
import textwrap
+import re
from pyramid.paster import bootstrap
+from pyramid.compat import (string_types, configparser)
+from pyramid.interfaces import (
+ IRouteRequest,
+ IViewClassifier,
+ IView,
+)
+from pyramid.config import not_
+
from pyramid.scripts.common import parse_vars
+from pyramid.static import static_view
+from zope.interface import Interface
+
+
+PAD = 3
+ANY_KEY = '*'
+UNKNOWN_KEY = '<unknown>'
+
def main(argv=sys.argv, quiet=False):
command = PRoutesCommand(argv, quiet)
return command.run()
+
+def _get_pattern(route):
+ pattern = route.pattern
+
+ if not pattern.startswith('/'):
+ pattern = '/%s' % pattern
+ return pattern
+
+
+def _get_print_format(fmt, max_name, max_pattern, max_view, max_method):
+ print_fmt = ''
+ max_map = {
+ 'name': max_name,
+ 'pattern': max_pattern,
+ 'view': max_view,
+ 'method': max_method,
+ }
+ sizes = []
+
+ for index, col in enumerate(fmt):
+ size = max_map[col] + PAD
+ print_fmt += '{{%s: <{%s}}} ' % (col, index)
+ sizes.append(size)
+
+ return print_fmt.format(*sizes)
+
+
+def _get_request_methods(route_request_methods, view_request_methods):
+ excludes = set()
+
+ if route_request_methods:
+ route_request_methods = set(route_request_methods)
+
+ if view_request_methods:
+ view_request_methods = set(view_request_methods)
+
+ for method in view_request_methods.copy():
+ if method.startswith('!'):
+ view_request_methods.remove(method)
+ excludes.add(method[1:])
+
+ has_route_methods = route_request_methods is not None
+ has_view_methods = len(view_request_methods) > 0
+ has_methods = has_route_methods or has_view_methods
+
+ if has_route_methods is False and has_view_methods is False:
+ request_methods = [ANY_KEY]
+ elif has_route_methods is False and has_view_methods is True:
+ request_methods = view_request_methods
+ elif has_route_methods is True and has_view_methods is False:
+ request_methods = route_request_methods
+ else:
+ request_methods = route_request_methods.intersection(
+ view_request_methods
+ )
+
+ request_methods = set(request_methods).difference(excludes)
+
+ if has_methods and not request_methods:
+ request_methods = '<route mismatch>'
+ elif request_methods:
+ if excludes and request_methods == set([ANY_KEY]):
+ for exclude in excludes:
+ request_methods.add('!%s' % exclude)
+
+ request_methods = ','.join(sorted(request_methods))
+
+ return request_methods
+
+
+def _get_view_module(view_callable):
+ if view_callable is None:
+ return UNKNOWN_KEY
+
+ if hasattr(view_callable, '__name__'):
+ if hasattr(view_callable, '__original_view__'):
+ original_view = view_callable.__original_view__
+ else:
+ original_view = None
+
+ if isinstance(original_view, static_view):
+ if original_view.package_name is not None:
+ return '%s:%s' % (
+ original_view.package_name,
+ original_view.docroot
+ )
+ else:
+ return original_view.docroot
+ else:
+ view_name = view_callable.__name__
+ else:
+ # Currently only MultiView hits this,
+ # we could just not run _get_view_module
+ # for them and remove this logic
+ view_name = str(view_callable)
+
+ view_module = '%s.%s' % (
+ view_callable.__module__,
+ view_name,
+ )
+
+ # If pyramid wraps something in wsgiapp or wsgiapp2 decorators
+ # that is currently returned as pyramid.router.decorator, lets
+ # hack a nice name in:
+ if view_module == 'pyramid.router.decorator':
+ view_module = '<wsgiapp>'
+
+ return view_module
+
+
+def get_route_data(route, registry):
+ pattern = _get_pattern(route)
+
+ request_iface = registry.queryUtility(
+ IRouteRequest,
+ name=route.name
+ )
+
+ route_request_methods = None
+ view_request_methods_order = []
+ view_request_methods = {}
+ view_callable = None
+
+ route_intr = registry.introspector.get(
+ 'routes', route.name
+ )
+
+ if request_iface is None:
+ return [
+ (route.name, _get_pattern(route), UNKNOWN_KEY, ANY_KEY)
+ ]
+
+ view_callable = registry.adapters.lookup(
+ (IViewClassifier, request_iface, Interface),
+ IView,
+ name='',
+ default=None
+ )
+ view_module = _get_view_module(view_callable)
+
+ # Introspectables can be turned off, so there could be a chance
+ # that we have no `route_intr` but we do have a route + callable
+ if route_intr is None:
+ view_request_methods[view_module] = []
+ view_request_methods_order.append(view_module)
+ else:
+ if route_intr.get('static', False) is True:
+ return [
+ (route.name, route_intr['external_url'], UNKNOWN_KEY, ANY_KEY)
+ ]
+
+
+ route_request_methods = route_intr['request_methods']
+ view_intr = registry.introspector.related(route_intr)
+
+ if view_intr:
+ for view in view_intr:
+ request_method = view.get('request_methods')
+
+ if request_method is not None:
+ view_callable = view['callable']
+ view_module = _get_view_module(view_callable)
+
+ if view_module not in view_request_methods:
+ view_request_methods[view_module] = []
+ view_request_methods_order.append(view_module)
+
+ if isinstance(request_method, string_types):
+ request_method = (request_method,)
+ elif isinstance(request_method, not_):
+ request_method = ('!%s' % request_method.value,)
+
+ view_request_methods[view_module].extend(request_method)
+ else:
+ if view_module not in view_request_methods:
+ view_request_methods[view_module] = []
+ view_request_methods_order.append(view_module)
+
+ else:
+ view_request_methods[view_module] = []
+ view_request_methods_order.append(view_module)
+
+ final_routes = []
+
+ for view_module in view_request_methods_order:
+ methods = view_request_methods[view_module]
+ request_methods = _get_request_methods(
+ route_request_methods,
+ methods
+ )
+
+ final_routes.append((
+ route.name,
+ pattern,
+ view_module,
+ request_methods,
+ ))
+
+ return final_routes
+
+
class PRoutesCommand(object):
description = """\
Print all URL dispatch routes used by a Pyramid application in the
@@ -25,62 +244,153 @@ class PRoutesCommand(object):
bootstrap = (bootstrap,)
stdout = sys.stdout
usage = '%prog config_uri'
-
+ ConfigParser = configparser.ConfigParser # testing
parser = optparse.OptionParser(
usage,
description=textwrap.dedent(description)
- )
+ )
+ parser.add_option('-g', '--glob',
+ action='store', type='string', dest='glob',
+ default='', help='Display routes matching glob pattern')
+
+ parser.add_option('-f', '--format',
+ action='store', type='string', dest='format',
+ default='', help=('Choose which columns to display, this '
+ 'will override the format key in the '
+ '[proutes] ini section'))
def __init__(self, argv, quiet=False):
self.options, self.args = self.parser.parse_args(argv[1:])
self.quiet = quiet
+ self.available_formats = [
+ 'name', 'pattern', 'view', 'method'
+ ]
+ self.column_format = self.available_formats
+
+ def validate_formats(self, formats):
+ invalid_formats = []
+ for fmt in formats:
+ if fmt not in self.available_formats:
+ invalid_formats.append(fmt)
+
+ msg = (
+ 'You provided invalid formats %s, '
+ 'Available formats are %s'
+ )
+
+ if invalid_formats:
+ msg = msg % (invalid_formats, self.available_formats)
+ self.out(msg)
+ return False
+
+ return True
+
+ def proutes_file_config(self, filename):
+ config = self.ConfigParser()
+ config.read(filename)
+ try:
+ items = config.items('proutes')
+ for k, v in items:
+ if 'format' == k:
+ cols = re.split(r'[,|\s|\n]*', v)
+ self.column_format = [x.strip() for x in cols]
+
+ except configparser.NoSectionError:
+ return
+
+ def out(self, msg): # pragma: no cover
+ if not self.quiet:
+ print(msg)
def _get_mapper(self, registry):
from pyramid.config import Configurator
- config = Configurator(registry = registry)
+ config = Configurator(registry=registry)
return config.get_routes_mapper()
- def out(self, msg): # pragma: no cover
- if not self.quiet:
- print(msg)
-
def run(self, quiet=False):
if not self.args:
self.out('requires a config file argument')
return 2
- from pyramid.interfaces import IRouteRequest
- from pyramid.interfaces import IViewClassifier
- from pyramid.interfaces import IView
- from zope.interface import Interface
config_uri = self.args[0]
-
env = self.bootstrap[0](config_uri, options=parse_vars(self.args[1:]))
registry = env['registry']
mapper = self._get_mapper(registry)
- if mapper is not None:
- routes = mapper.get_routes()
- fmt = '%-15s %-30s %-25s'
- if not routes:
- return 0
- self.out(fmt % ('Name', 'Pattern', 'View'))
- self.out(
- fmt % ('-'*len('Name'), '-'*len('Pattern'), '-'*len('View')))
- for route in routes:
- pattern = route.pattern
- if not pattern.startswith('/'):
- pattern = '/' + pattern
- request_iface = registry.queryUtility(IRouteRequest,
- name=route.name)
- view_callable = None
- if (request_iface is None) or (route.factory is not None):
- self.out(fmt % (route.name, pattern, '<unknown>'))
- else:
- view_callable = registry.adapters.lookup(
- (IViewClassifier, request_iface, Interface),
- IView, name='', default=None)
- self.out(fmt % (route.name, pattern, view_callable))
+
+ self.proutes_file_config(config_uri)
+
+ if self.options.format:
+ columns = self.options.format.split(',')
+ self.column_format = [x.strip() for x in columns]
+
+ is_valid = self.validate_formats(self.column_format)
+
+ if is_valid is False:
+ return 2
+
+ if mapper is None:
+ return 0
+
+ max_name = len('Name')
+ max_pattern = len('Pattern')
+ max_view = len('View')
+ max_method = len('Method')
+
+ routes = mapper.get_routes(include_static=True)
+
+ if len(routes) == 0:
+ return 0
+
+ mapped_routes = [{
+ 'name': 'Name',
+ 'pattern': 'Pattern',
+ 'view': 'View',
+ 'method': 'Method'
+ },{
+ 'name': '----',
+ 'pattern': '-------',
+ 'view': '----',
+ 'method': '------'
+ }]
+
+ for route in routes:
+ route_data = get_route_data(route, registry)
+
+ for name, pattern, view, method in route_data:
+ if self.options.glob:
+ match = (fnmatch.fnmatch(name, self.options.glob) or
+ fnmatch.fnmatch(pattern, self.options.glob))
+ if not match:
+ continue
+
+ if len(name) > max_name:
+ max_name = len(name)
+
+ if len(pattern) > max_pattern:
+ max_pattern = len(pattern)
+
+ if len(view) > max_view:
+ max_view = len(view)
+
+ if len(method) > max_method:
+ max_method = len(method)
+
+ mapped_routes.append({
+ 'name': name,
+ 'pattern': pattern,
+ 'view': view,
+ 'method': method
+ })
+
+ fmt = _get_print_format(
+ self.column_format, max_name, max_pattern, max_view, max_method
+ )
+
+ for route in mapped_routes:
+ self.out(fmt.format(**route))
+
return 0
-if __name__ == '__main__': # pragma: no cover
+
+if __name__ == '__main__': # pragma: no cover
sys.exit(main() or 0)
diff --git a/pyramid/scripts/pserve.py b/pyramid/scripts/pserve.py
index ea125a0dd..57e4ab012 100644
--- a/pyramid/scripts/pserve.py
+++ b/pyramid/scripts/pserve.py
@@ -21,9 +21,11 @@ import textwrap
import threading
import time
import traceback
+import webbrowser
from paste.deploy import loadserver
from paste.deploy import loadapp
+from paste.deploy.loadwsgi import loadcontext, SERVER
from pyramid.compat import PY3
from pyramid.compat import WIN
@@ -34,6 +36,11 @@ from pyramid.scripts.common import parse_vars
MAXFD = 1024
+try:
+ import termios
+except ImportError: # pragma: no cover
+ termios = None
+
if WIN and not hasattr(os, 'kill'): # pragma: no cover
# py 2.6 on windows
def kill(pid, sig=None):
@@ -122,6 +129,11 @@ class PServeCommand(object):
action='store_true',
help="Auto-restart server if it dies")
parser.add_option(
+ '-b', '--browser',
+ dest='browser',
+ action='store_true',
+ help="Open a web browser to server url")
+ parser.add_option(
'--status',
action='store_true',
dest='show_status',
@@ -334,6 +346,17 @@ class PServeCommand(object):
msg = ''
self.out('Exiting%s (-v to see traceback)' % msg)
+ if self.options.browser:
+ def open_browser():
+ context = loadcontext(SERVER, app_spec, name=app_name, relative_to=base,
+ global_conf=vars)
+ url = 'http://127.0.0.1:{port}/'.format(**context.config())
+ time.sleep(1)
+ webbrowser.open(url)
+ t = threading.Thread(target=open_browser)
+ t.setDaemon(True)
+ t.start()
+
serve()
def loadapp(self, app_spec, name, relative_to, **kw): # pragma: no cover
@@ -691,15 +714,23 @@ def _turn_sigterm_into_systemexit(): # pragma: no cover
raise SystemExit
signal.signal(signal.SIGTERM, handle_term)
+def ensure_echo_on(): # pragma: no cover
+ if termios:
+ fd = sys.stdin
+ if fd.isatty():
+ attr_list = termios.tcgetattr(fd)
+ if not attr_list[3] & termios.ECHO:
+ attr_list[3] |= termios.ECHO
+ termios.tcsetattr(fd, termios.TCSANOW, attr_list)
+
def install_reloader(poll_interval=1, extra_files=None): # pragma: no cover
"""
Install the reloading monitor.
On some platforms server threads may not terminate when the main
- thread does, causing ports to remain open/locked. The
- ``raise_keyboard_interrupt`` option creates a unignorable signal
- which causes the whole application to shut-down (rudely).
+ thread does, causing ports to remain open/locked.
"""
+ ensure_echo_on()
mon = Monitor(poll_interval=poll_interval)
if extra_files is None:
extra_files = []
diff --git a/pyramid/scripts/pshell.py b/pyramid/scripts/pshell.py
index 12b078677..1168ba78a 100644
--- a/pyramid/scripts/pshell.py
+++ b/pyramid/scripts/pshell.py
@@ -1,9 +1,11 @@
from code import interact
import optparse
+import os
import sys
import textwrap
from pyramid.compat import configparser
+from pyramid.compat import exec_
from pyramid.util import DottedNameResolver
from pyramid.paster import bootstrap
@@ -51,6 +53,7 @@ class PShellCommand(object):
loaded_objects = {}
object_help = {}
setup = None
+ pystartup = os.environ.get('PYTHONSTARTUP')
def __init__(self, argv, quiet=False):
self.quiet = quiet
@@ -144,6 +147,12 @@ class PShellCommand(object):
if shell is None:
shell = self.make_shell()
+ if self.pystartup and os.path.isfile(self.pystartup):
+ with open(self.pystartup, 'rb') as fp:
+ exec_(fp.read().decode('utf-8'), env)
+ if '__builtins__' in env:
+ del env['__builtins__']
+
try:
shell(env, help)
finally:
diff --git a/pyramid/security.py b/pyramid/security.py
index 041155563..f993ef353 100644
--- a/pyramid/security.py
+++ b/pyramid/security.py
@@ -17,6 +17,8 @@ Authenticated = 'system.Authenticated'
Allow = 'Allow'
Deny = 'Deny'
+_marker = object()
+
class AllPermissionsList(object):
""" Stand in 'permission list' to represent all permissions """
def __iter__(self):
@@ -115,16 +117,16 @@ deprecated(
'"effective_principals" attribute of the Pyramid request instead.'
)
-def remember(request, principal, **kw):
+def remember(request, userid=_marker, **kw):
"""
Returns a sequence of header tuples (e.g. ``[('Set-Cookie', 'foo=abc')]``)
on this request's response.
These headers are suitable for 'remembering' a set of credentials
- implied by the data passed as ``principal`` and ``*kw`` using the
+ implied by the data passed as ``userid`` and ``*kw`` using the
current :term:`authentication policy`. Common usage might look
like so within the body of a view function (``response`` is
assumed to be a :term:`WebOb` -style :term:`response` object
- computed previously by the view code)::
+ computed previously by the view code):
.. code-block:: python
@@ -138,11 +140,28 @@ def remember(request, principal, **kw):
always return an empty sequence. If used, the composition and
meaning of ``**kw`` must be agreed upon by the calling code and
the effective authentication policy.
+
+ .. deprecated:: 1.6
+ Renamed the ``principal`` argument to ``userid`` to clarify its
+ purpose.
"""
+ if userid is _marker:
+ principal = kw.pop('principal', _marker)
+ if principal is _marker:
+ raise TypeError(
+ 'remember() missing 1 required positional argument: '
+ '\'userid\'')
+ else:
+ deprecated(
+ 'principal',
+ 'The "principal" argument was deprecated in Pyramid 1.6. '
+ 'It will be removed in Pyramid 1.9. Use the "userid" '
+ 'argument instead.')
+ userid = principal
policy = _get_authentication_policy(request)
if policy is None:
return []
- return policy.remember(request, principal, **kw)
+ return policy.remember(request, userid, **kw)
def forget(request):
"""
@@ -151,12 +170,14 @@ def forget(request):
possessed by the currently authenticated user. A common usage
might look like so within the body of a view function
(``response`` is assumed to be an :term:`WebOb` -style
- :term:`response` object computed previously by the view code)::
+ :term:`response` object computed previously by the view code):
+
+ .. code-block:: python
- from pyramid.security import forget
- headers = forget(request)
- response.headerlist.extend(headers)
- return response
+ from pyramid.security import forget
+ headers = forget(request)
+ response.headerlist.extend(headers)
+ return response
If no :term:`authentication policy` is in use, this function will
always return an empty sequence.
diff --git a/pyramid/session.py b/pyramid/session.py
index a95c3f258..c4cfc1949 100644
--- a/pyramid/session.py
+++ b/pyramid/session.py
@@ -125,8 +125,8 @@ def check_csrf_token(request,
.. versionadded:: 1.4a2
"""
- supplied_token = request.params.get(token, request.headers.get(header))
- if supplied_token != request.session.get_csrf_token():
+ supplied_token = request.params.get(token, request.headers.get(header, ""))
+ if strings_differ(request.session.get_csrf_token(), supplied_token):
if raises:
raise BadCSRFToken('check_csrf_token(): Invalid token')
return False
diff --git a/pyramid/static.py b/pyramid/static.py
index aa67568d3..4ff02f798 100644
--- a/pyramid/static.py
+++ b/pyramid/static.py
@@ -1,4 +1,5 @@
# -*- coding: utf-8 -*-
+import hashlib
import os
from os.path import (
@@ -26,7 +27,7 @@ from pyramid.httpexceptions import (
HTTPMovedPermanently,
)
-from pyramid.path import caller_package
+from pyramid.path import AssetResolver, caller_package
from pyramid.response import FileResponse
from pyramid.traversal import traversal_path_info
@@ -78,7 +79,7 @@ class static_view(object):
"""
def __init__(self, root_dir, cache_max_age=3600, package_name=None,
- use_subpath=False, index='index.html'):
+ use_subpath=False, index='index.html', cachebust_match=None):
# package_name is for bw compat; it is preferred to pass in a
# package-relative path as root_dir
# (e.g. ``anotherpackage:foo/static``).
@@ -91,13 +92,15 @@ class static_view(object):
self.docroot = docroot
self.norm_docroot = normcase(normpath(docroot))
self.index = index
+ self.cachebust_match = cachebust_match
def __call__(self, context, request):
if self.use_subpath:
path_tuple = request.subpath
else:
path_tuple = traversal_path_info(request.environ['PATH_INFO'])
-
+ if self.cachebust_match:
+ path_tuple = self.cachebust_match(path_tuple)
path = _secure_path(path_tuple)
if path is None:
@@ -154,3 +157,128 @@ def _secure_path(path_tuple):
encoded = slash.join(path_tuple) # will be unicode
return encoded
+def _generate_md5(spec):
+ asset = AssetResolver(None).resolve(spec)
+ md5 = hashlib.md5()
+ with asset.stream() as stream:
+ for block in iter(lambda: stream.read(4096), b''):
+ md5.update(block)
+ return md5.hexdigest()
+
+class Md5AssetTokenGenerator(object):
+ """
+ A mixin class which provides an implementation of
+ :meth:`~pyramid.interfaces.ICacheBuster.target` which generates an md5
+ checksum token for an asset, caching it for subsequent calls.
+ """
+ def __init__(self):
+ self.token_cache = {}
+
+ def tokenize(self, pathspec):
+ # An astute observer will notice that this use of token_cache doesn't
+ # look particularly thread safe. Basic read/write operations on Python
+ # dicts, however, are atomic, so simply accessing and writing values
+ # to the dict shouldn't cause a segfault or other catastrophic failure.
+ # (See: http://effbot.org/pyfaq/what-kinds-of-global-value-mutation-are-thread-safe.htm)
+ #
+ # We do have a race condition that could result in the same md5
+ # checksum getting computed twice or more times in parallel. Since
+ # the program would still function just fine if this were to occur,
+ # the extra overhead of using locks to serialize access to the dict
+ # seems an unnecessary burden.
+ #
+ token = self.token_cache.get(pathspec)
+ if not token:
+ self.token_cache[pathspec] = token = _generate_md5(pathspec)
+ return token
+
+class PathSegmentCacheBuster(object):
+ """
+ An implementation of :class:`~pyramid.interfaces.ICacheBuster` which
+ inserts a token for cache busting in the path portion of an asset URL.
+
+ To use this class, subclass it and provide a ``tokenize`` method which
+ accepts a ``pathspec`` and returns a token.
+
+ .. versionadded:: 1.6
+ """
+ def pregenerate(self, pathspec, subpath, kw):
+ token = self.tokenize(pathspec)
+ return (token,) + subpath, kw
+
+ def match(self, subpath):
+ return subpath[1:]
+
+class PathSegmentMd5CacheBuster(PathSegmentCacheBuster,
+ Md5AssetTokenGenerator):
+ """
+ An implementation of :class:`~pyramid.interfaces.ICacheBuster` which
+ inserts an md5 checksum token for cache busting in the path portion of an
+ asset URL. Generated md5 checksums are cached in order to speed up
+ subsequent calls.
+
+ .. versionadded:: 1.6
+ """
+ def __init__(self):
+ super(PathSegmentMd5CacheBuster, self).__init__()
+
+class QueryStringCacheBuster(object):
+ """
+ An implementation of :class:`~pyramid.interfaces.ICacheBuster` which adds
+ a token for cache busting in the query string of an asset URL.
+
+ The optional ``param`` argument determines the name of the parameter added
+ to the query string and defaults to ``'x'``.
+
+ To use this class, subclass it and provide a ``tokenize`` method which
+ accepts a ``pathspec`` and returns a token.
+
+ .. versionadded:: 1.6
+ """
+ def __init__(self, param='x'):
+ self.param = param
+
+ def pregenerate(self, pathspec, subpath, kw):
+ token = self.tokenize(pathspec)
+ query = kw.setdefault('_query', {})
+ if isinstance(query, dict):
+ query[self.param] = token
+ else:
+ kw['_query'] = tuple(query) + ((self.param, token),)
+ return subpath, kw
+
+class QueryStringMd5CacheBuster(QueryStringCacheBuster,
+ Md5AssetTokenGenerator):
+ """
+ An implementation of :class:`~pyramid.interfaces.ICacheBuster` which adds
+ an md5 checksum token for cache busting in the query string of an asset
+ URL. Generated md5 checksums are cached in order to speed up subsequent
+ calls.
+
+ The optional ``param`` argument determines the name of the parameter added
+ to the query string and defaults to ``'x'``.
+
+ .. versionadded:: 1.6
+ """
+ def __init__(self, param='x'):
+ super(QueryStringMd5CacheBuster, self).__init__(param=param)
+
+class QueryStringConstantCacheBuster(QueryStringCacheBuster):
+ """
+ An implementation of :class:`~pyramid.interfaces.ICacheBuster` which adds
+ an arbitrary token for cache busting in the query string of an asset URL.
+
+ The ``token`` parameter is the token string to use for cache busting and
+ will be the same for every request.
+
+ The optional ``param`` argument determines the name of the parameter added
+ to the query string and defaults to ``'x'``.
+
+ .. versionadded:: 1.6
+ """
+ def __init__(self, token, param='x'):
+ super(QueryStringConstantCacheBuster, self).__init__(param=param)
+ self._token = token
+
+ def tokenize(self, pathspec):
+ return self._token
diff --git a/pyramid/testing.py b/pyramid/testing.py
index 8cbd8b82b..667e6af4e 100644
--- a/pyramid/testing.py
+++ b/pyramid/testing.py
@@ -9,7 +9,6 @@ from zope.interface import (
from pyramid.interfaces import (
IRequest,
- IResponseFactory,
ISession,
)
@@ -22,7 +21,7 @@ from pyramid.compat import (
from pyramid.config import Configurator
from pyramid.decorator import reify
from pyramid.path import caller_package
-from pyramid.response import Response
+from pyramid.response import Response, _get_response_factory
from pyramid.registry import Registry
from pyramid.security import (
@@ -42,6 +41,7 @@ from pyramid.request import CallbackMethodsMixin
from pyramid.url import URLMethodsMixin
from pyramid.util import InstancePropertyMixin
+
_marker = object()
class DummyRootFactory(object):
@@ -79,8 +79,8 @@ class DummySecurityPolicy(object):
effective_principals.extend(self.groupids)
return effective_principals
- def remember(self, request, principal, **kw):
- self.remembered = principal
+ def remember(self, request, userid, **kw):
+ self.remembered = userid
return self.remember_result
def forget(self, request):
@@ -383,8 +383,8 @@ class DummyRequest(
@reify
def response(self):
- f = self.registry.queryUtility(IResponseFactory, default=Response)
- return f()
+ f = _get_response_factory(self.registry)
+ return f(self)
have_zca = True
diff --git a/pyramid/tests/test_authentication.py b/pyramid/tests/test_authentication.py
index e25e9faa1..c7fc1c211 100644
--- a/pyramid/tests/test_authentication.py
+++ b/pyramid/tests/test_authentication.py
@@ -1211,26 +1211,26 @@ class Test_parse_ticket(unittest.TestCase):
self._assertRaisesBadTicket('secret', ticket, '0.0.0.0')
def test_correct_with_user_data(self):
- ticket = '66f9cc3e423dc57c91df696cf3d1f0d80000000auserid!a,b!'
+ ticket = text_('66f9cc3e423dc57c91df696cf3d1f0d80000000auserid!a,b!')
result = self._callFUT('secret', ticket, '0.0.0.0')
self.assertEqual(result, (10, 'userid', ['a', 'b'], ''))
def test_correct_with_user_data_sha512(self):
- ticket = '7d947cdef99bad55f8e3382a8bd089bb9dd0547f7925b7d189adc1160cab'\
- '0ec0e6888faa41eba641a18522b26f19109f3ffafb769767ba8a26d02aae'\
- 'ae56599a0000000auserid!a,b!'
+ ticket = text_('7d947cdef99bad55f8e3382a8bd089bb9dd0547f7925b7d189adc1'
+ '160cab0ec0e6888faa41eba641a18522b26f19109f3ffafb769767'
+ 'ba8a26d02aaeae56599a0000000auserid!a,b!')
result = self._callFUT('secret', ticket, '0.0.0.0', 'sha512')
self.assertEqual(result, (10, 'userid', ['a', 'b'], ''))
def test_ipv4(self):
- ticket = 'b3e7156db4f8abde4439c4a6499a0668f9e7ffd7fa27b798400ecdade8d7'\
- '6c530000000auserid!'
+ ticket = text_('b3e7156db4f8abde4439c4a6499a0668f9e7ffd7fa27b798400ecd'
+ 'ade8d76c530000000auserid!')
result = self._callFUT('secret', ticket, '198.51.100.1', 'sha256')
self.assertEqual(result, (10, 'userid', [''], ''))
def test_ipv6(self):
- ticket = 'd025b601a0f12ca6d008aa35ff3a22b7d8f3d1c1456c85becf8760cd7a2f'\
- 'a4910000000auserid!'
+ ticket = text_('d025b601a0f12ca6d008aa35ff3a22b7d8f3d1c1456c85becf8760'
+ 'cd7a2fa4910000000auserid!')
result = self._callFUT('secret', ticket, '2001:db8::1', 'sha256')
self.assertEqual(result, (10, 'userid', [''], ''))
pass
diff --git a/pyramid/tests/test_compat.py b/pyramid/tests/test_compat.py
new file mode 100644
index 000000000..23ccce82e
--- /dev/null
+++ b/pyramid/tests/test_compat.py
@@ -0,0 +1,26 @@
+import unittest
+from pyramid.compat import is_unbound_method
+
+class TestUnboundMethods(unittest.TestCase):
+ def test_old_style_bound(self):
+ self.assertFalse(is_unbound_method(OldStyle().run))
+
+ def test_new_style_bound(self):
+ self.assertFalse(is_unbound_method(NewStyle().run))
+
+ def test_old_style_unbound(self):
+ self.assertTrue(is_unbound_method(OldStyle.run))
+
+ def test_new_style_unbound(self):
+ self.assertTrue(is_unbound_method(NewStyle.run))
+
+ def test_normal_func_unbound(self):
+ def func(): return 'OK'
+
+ self.assertFalse(is_unbound_method(func))
+
+class OldStyle:
+ def run(self): return 'OK'
+
+class NewStyle(object):
+ def run(self): return 'OK'
diff --git a/pyramid/tests/test_config/pkgs/asset/models.py b/pyramid/tests/test_config/pkgs/asset/models.py
deleted file mode 100644
index d80d14bb3..000000000
--- a/pyramid/tests/test_config/pkgs/asset/models.py
+++ /dev/null
@@ -1,8 +0,0 @@
-from zope.interface import Interface
-
-class IFixture(Interface):
- pass
-
-def fixture():
- """ """
-
diff --git a/pyramid/tests/test_config/pkgs/asset/subpackage/templates/bar.pt b/pyramid/tests/test_config/pkgs/asset/subpackage/templates/bar.pt
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/pyramid/tests/test_config/pkgs/asset/subpackage/templates/bar.pt
diff --git a/pyramid/tests/test_config/pkgs/asset/views.py b/pyramid/tests/test_config/pkgs/asset/views.py
deleted file mode 100644
index cbfc5a574..000000000
--- a/pyramid/tests/test_config/pkgs/asset/views.py
+++ /dev/null
@@ -1,22 +0,0 @@
-from zope.interface import Interface
-from webob import Response
-from pyramid.httpexceptions import HTTPForbidden
-
-def fixture_view(context, request):
- """ """
- return Response('fixture')
-
-def erroneous_view(context, request):
- """ """
- raise RuntimeError()
-
-def exception_view(context, request):
- """ """
- return Response('supressed')
-
-def protected_view(context, request):
- """ """
- raise HTTPForbidden()
-
-class IDummy(Interface):
- pass
diff --git a/pyramid/tests/test_config/test_adapters.py b/pyramid/tests/test_config/test_adapters.py
index 4cbb1bf80..b3b7576a3 100644
--- a/pyramid/tests/test_config/test_adapters.py
+++ b/pyramid/tests/test_config/test_adapters.py
@@ -219,7 +219,7 @@ class AdaptersConfiguratorMixinTests(unittest.TestCase):
def test_add_response_adapter_dottednames(self):
from pyramid.interfaces import IResponse
config = self._makeOne(autocommit=True)
- if PY3: # pragma: no cover
+ if PY3:
str_name = 'builtins.str'
else:
str_name = '__builtin__.str'
diff --git a/pyramid/tests/test_config/test_assets.py b/pyramid/tests/test_config/test_assets.py
index 345e7f8d6..842c73da6 100644
--- a/pyramid/tests/test_config/test_assets.py
+++ b/pyramid/tests/test_config/test_assets.py
@@ -1,6 +1,10 @@
+import os.path
import unittest
from pyramid.testing import cleanUp
+# we use this folder
+here = os.path.dirname(os.path.abspath(__file__))
+
class TestAssetsConfiguratorMixin(unittest.TestCase):
def _makeOne(self, *arg, **kw):
from pyramid.config import Configurator
@@ -10,27 +14,31 @@ class TestAssetsConfiguratorMixin(unittest.TestCase):
def test_override_asset_samename(self):
from pyramid.exceptions import ConfigurationError
config = self._makeOne()
- self.assertRaises(ConfigurationError, config.override_asset,'a', 'a')
+ self.assertRaises(ConfigurationError, config.override_asset, 'a', 'a')
def test_override_asset_directory_with_file(self):
from pyramid.exceptions import ConfigurationError
config = self._makeOne()
self.assertRaises(ConfigurationError, config.override_asset,
- 'a:foo/', 'a:foo.pt')
+ 'a:foo/',
+ 'pyramid.tests.test_config.pkgs.asset:foo.pt')
def test_override_asset_file_with_directory(self):
from pyramid.exceptions import ConfigurationError
config = self._makeOne()
self.assertRaises(ConfigurationError, config.override_asset,
- 'a:foo.pt', 'a:foo/')
+ 'a:foo.pt',
+ 'pyramid.tests.test_config.pkgs.asset:templates/')
def test_override_asset_file_with_package(self):
from pyramid.exceptions import ConfigurationError
config = self._makeOne()
self.assertRaises(ConfigurationError, config.override_asset,
- 'a:foo.pt', 'a')
+ 'a:foo.pt',
+ 'pyramid.tests.test_config.pkgs.asset')
def test_override_asset_file_with_file(self):
+ from pyramid.config.assets import PackageAssetSource
config = self._makeOne(autocommit=True)
override = DummyUnderOverride()
config.override_asset(
@@ -41,10 +49,19 @@ class TestAssetsConfiguratorMixin(unittest.TestCase):
from pyramid.tests.test_config.pkgs.asset import subpackage
self.assertEqual(override.package, asset)
self.assertEqual(override.path, 'templates/foo.pt')
- self.assertEqual(override.override_package, subpackage)
- self.assertEqual(override.override_prefix, 'templates/bar.pt')
+ source = override.source
+ self.assertTrue(isinstance(source, PackageAssetSource))
+ self.assertEqual(source.package, subpackage)
+ self.assertEqual(source.prefix, 'templates/bar.pt')
+
+ resource_name = ''
+ expected = os.path.join(here, 'pkgs', 'asset',
+ 'subpackage', 'templates', 'bar.pt')
+ self.assertEqual(override.source.get_filename(resource_name),
+ expected)
def test_override_asset_package_with_package(self):
+ from pyramid.config.assets import PackageAssetSource
config = self._makeOne(autocommit=True)
override = DummyUnderOverride()
config.override_asset(
@@ -55,10 +72,19 @@ class TestAssetsConfiguratorMixin(unittest.TestCase):
from pyramid.tests.test_config.pkgs.asset import subpackage
self.assertEqual(override.package, asset)
self.assertEqual(override.path, '')
- self.assertEqual(override.override_package, subpackage)
- self.assertEqual(override.override_prefix, '')
+ source = override.source
+ self.assertTrue(isinstance(source, PackageAssetSource))
+ self.assertEqual(source.package, subpackage)
+ self.assertEqual(source.prefix, '')
+
+ resource_name = 'templates/bar.pt'
+ expected = os.path.join(here, 'pkgs', 'asset',
+ 'subpackage', 'templates', 'bar.pt')
+ self.assertEqual(override.source.get_filename(resource_name),
+ expected)
def test_override_asset_directory_with_directory(self):
+ from pyramid.config.assets import PackageAssetSource
config = self._makeOne(autocommit=True)
override = DummyUnderOverride()
config.override_asset(
@@ -69,10 +95,19 @@ class TestAssetsConfiguratorMixin(unittest.TestCase):
from pyramid.tests.test_config.pkgs.asset import subpackage
self.assertEqual(override.package, asset)
self.assertEqual(override.path, 'templates/')
- self.assertEqual(override.override_package, subpackage)
- self.assertEqual(override.override_prefix, 'templates/')
+ source = override.source
+ self.assertTrue(isinstance(source, PackageAssetSource))
+ self.assertEqual(source.package, subpackage)
+ self.assertEqual(source.prefix, 'templates/')
+
+ resource_name = 'bar.pt'
+ expected = os.path.join(here, 'pkgs', 'asset',
+ 'subpackage', 'templates', 'bar.pt')
+ self.assertEqual(override.source.get_filename(resource_name),
+ expected)
def test_override_asset_directory_with_package(self):
+ from pyramid.config.assets import PackageAssetSource
config = self._makeOne(autocommit=True)
override = DummyUnderOverride()
config.override_asset(
@@ -83,10 +118,19 @@ class TestAssetsConfiguratorMixin(unittest.TestCase):
from pyramid.tests.test_config.pkgs.asset import subpackage
self.assertEqual(override.package, asset)
self.assertEqual(override.path, 'templates/')
- self.assertEqual(override.override_package, subpackage)
- self.assertEqual(override.override_prefix, '')
+ source = override.source
+ self.assertTrue(isinstance(source, PackageAssetSource))
+ self.assertEqual(source.package, subpackage)
+ self.assertEqual(source.prefix, '')
+
+ resource_name = 'templates/bar.pt'
+ expected = os.path.join(here, 'pkgs', 'asset',
+ 'subpackage', 'templates', 'bar.pt')
+ self.assertEqual(override.source.get_filename(resource_name),
+ expected)
def test_override_asset_package_with_directory(self):
+ from pyramid.config.assets import PackageAssetSource
config = self._makeOne(autocommit=True)
override = DummyUnderOverride()
config.override_asset(
@@ -97,32 +141,129 @@ class TestAssetsConfiguratorMixin(unittest.TestCase):
from pyramid.tests.test_config.pkgs.asset import subpackage
self.assertEqual(override.package, asset)
self.assertEqual(override.path, '')
- self.assertEqual(override.override_package, subpackage)
- self.assertEqual(override.override_prefix, 'templates/')
+ source = override.source
+ self.assertTrue(isinstance(source, PackageAssetSource))
+ self.assertEqual(source.package, subpackage)
+ self.assertEqual(source.prefix, 'templates/')
+
+ resource_name = 'bar.pt'
+ expected = os.path.join(here, 'pkgs', 'asset',
+ 'subpackage', 'templates', 'bar.pt')
+ self.assertEqual(override.source.get_filename(resource_name),
+ expected)
+
+ def test_override_asset_directory_with_absfile(self):
+ from pyramid.exceptions import ConfigurationError
+ config = self._makeOne()
+ self.assertRaises(ConfigurationError, config.override_asset,
+ 'a:foo/',
+ os.path.join(here, 'pkgs', 'asset', 'foo.pt'))
+
+ def test_override_asset_file_with_absdirectory(self):
+ from pyramid.exceptions import ConfigurationError
+ config = self._makeOne()
+ abspath = os.path.join(here, 'pkgs', 'asset', 'subpackage', 'templates')
+ self.assertRaises(ConfigurationError, config.override_asset,
+ 'a:foo.pt',
+ abspath)
+
+ def test_override_asset_file_with_missing_abspath(self):
+ from pyramid.exceptions import ConfigurationError
+ config = self._makeOne()
+ self.assertRaises(ConfigurationError, config.override_asset,
+ 'a:foo.pt',
+ os.path.join(here, 'wont_exist'))
+
+ def test_override_asset_file_with_absfile(self):
+ from pyramid.config.assets import FSAssetSource
+ config = self._makeOne(autocommit=True)
+ override = DummyUnderOverride()
+ abspath = os.path.join(here, 'pkgs', 'asset', 'subpackage',
+ 'templates', 'bar.pt')
+ config.override_asset(
+ 'pyramid.tests.test_config.pkgs.asset:templates/foo.pt',
+ abspath,
+ _override=override)
+ from pyramid.tests.test_config.pkgs import asset
+ self.assertEqual(override.package, asset)
+ self.assertEqual(override.path, 'templates/foo.pt')
+ source = override.source
+ self.assertTrue(isinstance(source, FSAssetSource))
+ self.assertEqual(source.prefix, abspath)
+
+ resource_name = ''
+ expected = os.path.join(here, 'pkgs', 'asset',
+ 'subpackage', 'templates', 'bar.pt')
+ self.assertEqual(override.source.get_filename(resource_name),
+ expected)
+
+ def test_override_asset_directory_with_absdirectory(self):
+ from pyramid.config.assets import FSAssetSource
+ config = self._makeOne(autocommit=True)
+ override = DummyUnderOverride()
+ abspath = os.path.join(here, 'pkgs', 'asset', 'subpackage', 'templates')
+ config.override_asset(
+ 'pyramid.tests.test_config.pkgs.asset:templates/',
+ abspath,
+ _override=override)
+ from pyramid.tests.test_config.pkgs import asset
+ self.assertEqual(override.package, asset)
+ self.assertEqual(override.path, 'templates/')
+ source = override.source
+ self.assertTrue(isinstance(source, FSAssetSource))
+ self.assertEqual(source.prefix, abspath)
+
+ resource_name = 'bar.pt'
+ expected = os.path.join(here, 'pkgs', 'asset',
+ 'subpackage', 'templates', 'bar.pt')
+ self.assertEqual(override.source.get_filename(resource_name),
+ expected)
+
+ def test_override_asset_package_with_absdirectory(self):
+ from pyramid.config.assets import FSAssetSource
+ config = self._makeOne(autocommit=True)
+ override = DummyUnderOverride()
+ abspath = os.path.join(here, 'pkgs', 'asset', 'subpackage', 'templates')
+ config.override_asset(
+ 'pyramid.tests.test_config.pkgs.asset',
+ abspath,
+ _override=override)
+ from pyramid.tests.test_config.pkgs import asset
+ self.assertEqual(override.package, asset)
+ self.assertEqual(override.path, '')
+ source = override.source
+ self.assertTrue(isinstance(source, FSAssetSource))
+ self.assertEqual(source.prefix, abspath)
+
+ resource_name = 'bar.pt'
+ expected = os.path.join(here, 'pkgs', 'asset',
+ 'subpackage', 'templates', 'bar.pt')
+ self.assertEqual(override.source.get_filename(resource_name),
+ expected)
def test__override_not_yet_registered(self):
from pyramid.interfaces import IPackageOverrides
package = DummyPackage('package')
- opackage = DummyPackage('opackage')
+ source = DummyAssetSource()
config = self._makeOne()
- config._override(package, 'path', opackage, 'oprefix',
+ config._override(package, 'path', source,
PackageOverrides=DummyPackageOverrides)
overrides = config.registry.queryUtility(IPackageOverrides,
name='package')
- self.assertEqual(overrides.inserted, [('path', 'opackage', 'oprefix')])
+ self.assertEqual(overrides.inserted, [('path', source)])
self.assertEqual(overrides.package, package)
def test__override_already_registered(self):
from pyramid.interfaces import IPackageOverrides
package = DummyPackage('package')
- opackage = DummyPackage('opackage')
+ source = DummyAssetSource()
overrides = DummyPackageOverrides(package)
config = self._makeOne()
config.registry.registerUtility(overrides, IPackageOverrides,
name='package')
- config._override(package, 'path', opackage, 'oprefix',
+ config._override(package, 'path', source,
PackageOverrides=DummyPackageOverrides)
- self.assertEqual(overrides.inserted, [('path', 'opackage', 'oprefix')])
+ self.assertEqual(overrides.inserted, [('path', source)])
self.assertEqual(overrides.package, package)
@@ -148,30 +289,24 @@ class TestOverrideProvider(unittest.TestCase):
reg.registerUtility(overrides, IPackageOverrides, name=name)
def test_get_resource_filename_no_overrides(self):
- import os
resource_name = 'test_assets.py'
import pyramid.tests.test_config
provider = self._makeOne(pyramid.tests.test_config)
- here = os.path.dirname(os.path.abspath(__file__))
expected = os.path.join(here, resource_name)
result = provider.get_resource_filename(None, resource_name)
self.assertEqual(result, expected)
def test_get_resource_stream_no_overrides(self):
- import os
resource_name = 'test_assets.py'
import pyramid.tests.test_config
provider = self._makeOne(pyramid.tests.test_config)
- here = os.path.dirname(os.path.abspath(__file__))
with provider.get_resource_stream(None, resource_name) as result:
_assertBody(result.read(), os.path.join(here, resource_name))
def test_get_resource_string_no_overrides(self):
- import os
resource_name = 'test_assets.py'
import pyramid.tests.test_config
provider = self._makeOne(pyramid.tests.test_config)
- here = os.path.dirname(os.path.abspath(__file__))
result = provider.get_resource_string(None, resource_name)
_assertBody(result, os.path.join(here, resource_name))
@@ -202,11 +337,9 @@ class TestOverrideProvider(unittest.TestCase):
def test_get_resource_filename_override_returns_None(self):
overrides = DummyOverrides(None)
self._registerOverrides(overrides)
- import os
resource_name = 'test_assets.py'
import pyramid.tests.test_config
provider = self._makeOne(pyramid.tests.test_config)
- here = os.path.dirname(os.path.abspath(__file__))
expected = os.path.join(here, resource_name)
result = provider.get_resource_filename(None, resource_name)
self.assertEqual(result, expected)
@@ -214,22 +347,18 @@ class TestOverrideProvider(unittest.TestCase):
def test_get_resource_stream_override_returns_None(self):
overrides = DummyOverrides(None)
self._registerOverrides(overrides)
- import os
resource_name = 'test_assets.py'
import pyramid.tests.test_config
provider = self._makeOne(pyramid.tests.test_config)
- here = os.path.dirname(os.path.abspath(__file__))
with provider.get_resource_stream(None, resource_name) as result:
_assertBody(result.read(), os.path.join(here, resource_name))
def test_get_resource_string_override_returns_None(self):
overrides = DummyOverrides(None)
self._registerOverrides(overrides)
- import os
resource_name = 'test_assets.py'
import pyramid.tests.test_config
provider = self._makeOne(pyramid.tests.test_config)
- here = os.path.dirname(os.path.abspath(__file__))
result = provider.get_resource_string(None, resource_name)
_assertBody(result, os.path.join(here, resource_name))
@@ -378,8 +507,8 @@ class TestPackageOverrides(unittest.TestCase):
from pyramid.config.assets import DirectoryOverride
package = DummyPackage('package')
po = self._makeOne(package)
- po.overrides= [None]
- po.insert('foo/', 'package', 'bar/')
+ po.overrides = [None]
+ po.insert('foo/', DummyAssetSource())
self.assertEqual(len(po.overrides), 2)
override = po.overrides[0]
self.assertEqual(override.__class__, DirectoryOverride)
@@ -388,8 +517,8 @@ class TestPackageOverrides(unittest.TestCase):
from pyramid.config.assets import FileOverride
package = DummyPackage('package')
po = self._makeOne(package)
- po.overrides= [None]
- po.insert('foo.pt', 'package', 'bar.pt')
+ po.overrides = [None]
+ po.insert('foo.pt', DummyAssetSource())
self.assertEqual(len(po.overrides), 2)
override = po.overrides[0]
self.assertEqual(override.__class__, FileOverride)
@@ -399,132 +528,137 @@ class TestPackageOverrides(unittest.TestCase):
from pyramid.config.assets import DirectoryOverride
package = DummyPackage('package')
po = self._makeOne(package)
- po.overrides= [None]
- po.insert('', 'package', 'bar/')
+ po.overrides = [None]
+ source = DummyAssetSource()
+ po.insert('', source)
self.assertEqual(len(po.overrides), 2)
override = po.overrides[0]
self.assertEqual(override.__class__, DirectoryOverride)
- def test_search_path(self):
- overrides = [ DummyOverride(None), DummyOverride(('package', 'name'))]
+ def test_filtered_sources(self):
+ overrides = [ DummyOverride(None), DummyOverride('foo')]
package = DummyPackage('package')
po = self._makeOne(package)
- po.overrides= overrides
- self.assertEqual(list(po.search_path('whatever')),
- [('package', 'name')])
+ po.overrides = overrides
+ self.assertEqual(list(po.filtered_sources('whatever')), ['foo'])
def test_get_filename(self):
- import os
- overrides = [ DummyOverride(None), DummyOverride(
- ('pyramid.tests.test_config', 'test_assets.py'))]
+ source = DummyAssetSource(filename='foo.pt')
+ overrides = [ DummyOverride(None), DummyOverride((source, ''))]
package = DummyPackage('package')
po = self._makeOne(package)
- po.overrides= overrides
- here = os.path.dirname(os.path.abspath(__file__))
- expected = os.path.join(here, 'test_assets.py')
- self.assertEqual(po.get_filename('whatever'), expected)
+ po.overrides = overrides
+ result = po.get_filename('whatever')
+ self.assertEqual(result, 'foo.pt')
+ self.assertEqual(source.resource_name, '')
def test_get_filename_file_doesnt_exist(self):
- overrides = [ DummyOverride(None), DummyOverride(
- ('pyramid.tests.test_config', 'wont_exist'))]
+ source = DummyAssetSource(filename=None)
+ overrides = [DummyOverride(None), DummyOverride((source, 'wont_exist'))]
package = DummyPackage('package')
po = self._makeOne(package)
- po.overrides= overrides
+ po.overrides = overrides
self.assertEqual(po.get_filename('whatever'), None)
-
+ self.assertEqual(source.resource_name, 'wont_exist')
+
def test_get_stream(self):
- import os
- overrides = [ DummyOverride(None), DummyOverride(
- ('pyramid.tests.test_config', 'test_assets.py'))]
+ source = DummyAssetSource(stream='a stream?')
+ overrides = [DummyOverride(None), DummyOverride((source, 'foo.pt'))]
package = DummyPackage('package')
po = self._makeOne(package)
- po.overrides= overrides
- here = os.path.dirname(os.path.abspath(__file__))
- with po.get_stream('whatever') as stream:
- _assertBody(stream.read(), os.path.join(here, 'test_assets.py'))
+ po.overrides = overrides
+ self.assertEqual(po.get_stream('whatever'), 'a stream?')
+ self.assertEqual(source.resource_name, 'foo.pt')
def test_get_stream_file_doesnt_exist(self):
- overrides = [ DummyOverride(None), DummyOverride(
- ('pyramid.tests.test_config', 'wont_exist'))]
+ source = DummyAssetSource(stream=None)
+ overrides = [DummyOverride(None), DummyOverride((source, 'wont_exist'))]
package = DummyPackage('package')
po = self._makeOne(package)
- po.overrides= overrides
+ po.overrides = overrides
self.assertEqual(po.get_stream('whatever'), None)
+ self.assertEqual(source.resource_name, 'wont_exist')
def test_get_string(self):
- import os
- overrides = [ DummyOverride(None), DummyOverride(
- ('pyramid.tests.test_config', 'test_assets.py'))]
+ source = DummyAssetSource(string='a string')
+ overrides = [DummyOverride(None), DummyOverride((source, 'foo.pt'))]
package = DummyPackage('package')
po = self._makeOne(package)
- po.overrides= overrides
- here = os.path.dirname(os.path.abspath(__file__))
- _assertBody(po.get_string('whatever'),
- os.path.join(here, 'test_assets.py'))
+ po.overrides = overrides
+ self.assertEqual(po.get_string('whatever'), 'a string')
+ self.assertEqual(source.resource_name, 'foo.pt')
def test_get_string_file_doesnt_exist(self):
- overrides = [ DummyOverride(None), DummyOverride(
- ('pyramid.tests.test_config', 'wont_exist'))]
+ source = DummyAssetSource(string=None)
+ overrides = [DummyOverride(None), DummyOverride((source, 'wont_exist'))]
package = DummyPackage('package')
po = self._makeOne(package)
- po.overrides= overrides
+ po.overrides = overrides
self.assertEqual(po.get_string('whatever'), None)
+ self.assertEqual(source.resource_name, 'wont_exist')
def test_has_resource(self):
- overrides = [ DummyOverride(None), DummyOverride(
- ('pyramid.tests.test_config', 'test_assets.py'))]
+ source = DummyAssetSource(exists=True)
+ overrides = [DummyOverride(None), DummyOverride((source, 'foo.pt'))]
package = DummyPackage('package')
po = self._makeOne(package)
- po.overrides= overrides
+ po.overrides = overrides
self.assertEqual(po.has_resource('whatever'), True)
+ self.assertEqual(source.resource_name, 'foo.pt')
def test_has_resource_file_doesnt_exist(self):
- overrides = [ DummyOverride(None), DummyOverride(
- ('pyramid.tests.test_config', 'wont_exist'))]
+ source = DummyAssetSource(exists=None)
+ overrides = [DummyOverride(None), DummyOverride((source, 'wont_exist'))]
package = DummyPackage('package')
po = self._makeOne(package)
- po.overrides= overrides
+ po.overrides = overrides
self.assertEqual(po.has_resource('whatever'), None)
+ self.assertEqual(source.resource_name, 'wont_exist')
def test_isdir_false(self):
- overrides = [ DummyOverride(
- ('pyramid.tests.test_config', 'test_assets.py'))]
+ source = DummyAssetSource(isdir=False)
+ overrides = [DummyOverride(None), DummyOverride((source, 'foo.pt'))]
package = DummyPackage('package')
po = self._makeOne(package)
- po.overrides= overrides
+ po.overrides = overrides
self.assertEqual(po.isdir('whatever'), False)
-
+ self.assertEqual(source.resource_name, 'foo.pt')
+
def test_isdir_true(self):
- overrides = [ DummyOverride(
- ('pyramid.tests.test_config', 'files'))]
+ source = DummyAssetSource(isdir=True)
+ overrides = [DummyOverride(None), DummyOverride((source, 'foo.pt'))]
package = DummyPackage('package')
po = self._makeOne(package)
- po.overrides= overrides
+ po.overrides = overrides
self.assertEqual(po.isdir('whatever'), True)
+ self.assertEqual(source.resource_name, 'foo.pt')
def test_isdir_doesnt_exist(self):
- overrides = [ DummyOverride(None), DummyOverride(
- ('pyramid.tests.test_config', 'wont_exist'))]
+ source = DummyAssetSource(isdir=None)
+ overrides = [DummyOverride(None), DummyOverride((source, 'wont_exist'))]
package = DummyPackage('package')
po = self._makeOne(package)
- po.overrides= overrides
+ po.overrides = overrides
self.assertEqual(po.isdir('whatever'), None)
+ self.assertEqual(source.resource_name, 'wont_exist')
def test_listdir(self):
- overrides = [ DummyOverride(
- ('pyramid.tests.test_config', 'files'))]
+ source = DummyAssetSource(listdir=True)
+ overrides = [DummyOverride(None), DummyOverride((source, 'foo.pt'))]
package = DummyPackage('package')
po = self._makeOne(package)
- po.overrides= overrides
- self.assertTrue(po.listdir('whatever'))
+ po.overrides = overrides
+ self.assertEqual(po.listdir('whatever'), True)
+ self.assertEqual(source.resource_name, 'foo.pt')
def test_listdir_doesnt_exist(self):
- overrides = [ DummyOverride(None), DummyOverride(
- ('pyramid.tests.test_config', 'wont_exist'))]
+ source = DummyAssetSource(listdir=None)
+ overrides = [DummyOverride(None), DummyOverride((source, 'wont_exist'))]
package = DummyPackage('package')
po = self._makeOne(package)
- po.overrides= overrides
+ po.overrides = overrides
self.assertEqual(po.listdir('whatever'), None)
+ self.assertEqual(source.resource_name, 'wont_exist')
# PEP 302 __loader__ extensions: use the "real" __loader__, if present.
def test_get_data_pkg_has_no___loader__(self):
@@ -570,27 +704,124 @@ class TestPackageOverrides(unittest.TestCase):
def test_get_source_pkg_has___loader__(self):
package = DummyPackage('package')
- loader = package.__loader__ = DummyLoader()
+ loader = package.__loader__ = DummyLoader()
po = self._makeOne(package)
self.assertEqual(po.get_source('whatever'), 'def foo():\n pass')
self.assertEqual(loader._got_source, 'whatever')
+class AssetSourceIntegrationTests(object):
+
+ def test_get_filename(self):
+ source = self._makeOne('')
+ self.assertEqual(source.get_filename('test_assets.py'),
+ os.path.join(here, 'test_assets.py'))
+
+ def test_get_filename_with_prefix(self):
+ source = self._makeOne('test_assets.py')
+ self.assertEqual(source.get_filename(''),
+ os.path.join(here, 'test_assets.py'))
+
+ def test_get_filename_file_doesnt_exist(self):
+ source = self._makeOne('')
+ self.assertEqual(source.get_filename('wont_exist'), None)
+
+ def test_get_stream(self):
+ source = self._makeOne('')
+ with source.get_stream('test_assets.py') as stream:
+ _assertBody(stream.read(), os.path.join(here, 'test_assets.py'))
+
+ def test_get_stream_with_prefix(self):
+ source = self._makeOne('test_assets.py')
+ with source.get_stream('') as stream:
+ _assertBody(stream.read(), os.path.join(here, 'test_assets.py'))
+
+ def test_get_stream_file_doesnt_exist(self):
+ source = self._makeOne('')
+ self.assertEqual(source.get_stream('wont_exist'), None)
+
+ def test_get_string(self):
+ source = self._makeOne('')
+ _assertBody(source.get_string('test_assets.py'),
+ os.path.join(here, 'test_assets.py'))
+
+ def test_get_string_with_prefix(self):
+ source = self._makeOne('test_assets.py')
+ _assertBody(source.get_string(''),
+ os.path.join(here, 'test_assets.py'))
+
+ def test_get_string_file_doesnt_exist(self):
+ source = self._makeOne('')
+ self.assertEqual(source.get_string('wont_exist'), None)
+
+ def test_exists(self):
+ source = self._makeOne('')
+ self.assertEqual(source.exists('test_assets.py'), True)
+
+ def test_exists_with_prefix(self):
+ source = self._makeOne('test_assets.py')
+ self.assertEqual(source.exists(''), True)
+
+ def test_exists_file_doesnt_exist(self):
+ source = self._makeOne('')
+ self.assertEqual(source.exists('wont_exist'), None)
+
+ def test_isdir_false(self):
+ source = self._makeOne('')
+ self.assertEqual(source.isdir('test_assets.py'), False)
+
+ def test_isdir_true(self):
+ source = self._makeOne('')
+ self.assertEqual(source.isdir('files'), True)
+
+ def test_isdir_doesnt_exist(self):
+ source = self._makeOne('')
+ self.assertEqual(source.isdir('wont_exist'), None)
+
+ def test_listdir(self):
+ source = self._makeOne('')
+ self.assertTrue(source.listdir('files'))
+
+ def test_listdir_doesnt_exist(self):
+ source = self._makeOne('')
+ self.assertEqual(source.listdir('wont_exist'), None)
+
+class TestPackageAssetSource(AssetSourceIntegrationTests, unittest.TestCase):
+
+ def _getTargetClass(self):
+ from pyramid.config.assets import PackageAssetSource
+ return PackageAssetSource
+
+ def _makeOne(self, prefix, package='pyramid.tests.test_config'):
+ klass = self._getTargetClass()
+ return klass(package, prefix)
+
+class TestFSAssetSource(AssetSourceIntegrationTests, unittest.TestCase):
+ def _getTargetClass(self):
+ from pyramid.config.assets import FSAssetSource
+ return FSAssetSource
+
+ def _makeOne(self, prefix, base_prefix=here):
+ klass = self._getTargetClass()
+ return klass(os.path.join(base_prefix, prefix))
+
class TestDirectoryOverride(unittest.TestCase):
def _getTargetClass(self):
from pyramid.config.assets import DirectoryOverride
return DirectoryOverride
- def _makeOne(self, path, package, prefix):
+ def _makeOne(self, path, source):
klass = self._getTargetClass()
- return klass(path, package, prefix)
+ return klass(path, source)
def test_it_match(self):
- o = self._makeOne('foo/', 'package', 'bar/')
+ source = DummyAssetSource()
+ o = self._makeOne('foo/', source)
result = o('foo/something.pt')
- self.assertEqual(result, ('package', 'bar/something.pt'))
+ self.assertEqual(result, (source, 'something.pt'))
def test_it_no_match(self):
- o = self._makeOne('foo/', 'package', 'bar/')
+ source = DummyAssetSource()
+ o = self._makeOne('foo/', source)
result = o('baz/notfound.pt')
self.assertEqual(result, None)
@@ -599,17 +830,19 @@ class TestFileOverride(unittest.TestCase):
from pyramid.config.assets import FileOverride
return FileOverride
- def _makeOne(self, path, package, prefix):
+ def _makeOne(self, path, source):
klass = self._getTargetClass()
- return klass(path, package, prefix)
+ return klass(path, source)
def test_it_match(self):
- o = self._makeOne('foo.pt', 'package', 'bar.pt')
+ source = DummyAssetSource()
+ o = self._makeOne('foo.pt', source)
result = o('foo.pt')
- self.assertEqual(result, ('package', 'bar.pt'))
+ self.assertEqual(result, (source, ''))
def test_it_no_match(self):
- o = self._makeOne('foo.pt', 'package', 'bar.pt')
+ source = DummyAssetSource()
+ o = self._makeOne('foo.pt', source)
result = o('notfound.pt')
self.assertEqual(result, None)
@@ -634,8 +867,8 @@ class DummyPackageOverrides:
self.package = package
self.inserted = []
- def insert(self, path, package, prefix):
- self.inserted.append((path, package, prefix))
+ def insert(self, path, source):
+ self.inserted.append((path, source))
class DummyPkgResources:
def __init__(self):
@@ -647,6 +880,34 @@ class DummyPkgResources:
class DummyPackage:
def __init__(self, name):
self.__name__ = name
+
+class DummyAssetSource:
+ def __init__(self, **kw):
+ self.kw = kw
+
+ def get_filename(self, resource_name):
+ self.resource_name = resource_name
+ return self.kw['filename']
+
+ def get_stream(self, resource_name):
+ self.resource_name = resource_name
+ return self.kw['stream']
+
+ def get_string(self, resource_name):
+ self.resource_name = resource_name
+ return self.kw['string']
+
+ def exists(self, resource_name):
+ self.resource_name = resource_name
+ return self.kw['exists']
+
+ def isdir(self, resource_name):
+ self.resource_name = resource_name
+ return self.kw['isdir']
+
+ def listdir(self, resource_name):
+ self.resource_name = resource_name
+ return self.kw['listdir']
class DummyLoader:
_got_data = _is_package = None
@@ -664,12 +925,10 @@ class DummyLoader:
return 'def foo():\n pass'
class DummyUnderOverride:
- def __call__(self, package, path, override_package, override_prefix,
- _info=''):
+ def __call__(self, package, path, source, _info=''):
self.package = package
self.path = path
- self.override_package = override_package
- self.override_prefix = override_prefix
+ self.source = source
def read_(src):
with open(src, 'rb') as f:
diff --git a/pyramid/tests/test_config/test_factories.py b/pyramid/tests/test_config/test_factories.py
index 6e679397f..42bb5accc 100644
--- a/pyramid/tests/test_config/test_factories.py
+++ b/pyramid/tests/test_config/test_factories.py
@@ -23,6 +23,21 @@ class TestFactoriesMixin(unittest.TestCase):
self.assertEqual(config.registry.getUtility(IRequestFactory),
dummyfactory)
+ def test_set_response_factory(self):
+ from pyramid.interfaces import IResponseFactory
+ config = self._makeOne(autocommit=True)
+ factory = lambda r: object()
+ config.set_response_factory(factory)
+ self.assertEqual(config.registry.getUtility(IResponseFactory), factory)
+
+ def test_set_response_factory_dottedname(self):
+ from pyramid.interfaces import IResponseFactory
+ config = self._makeOne(autocommit=True)
+ config.set_response_factory(
+ 'pyramid.tests.test_config.dummyfactory')
+ self.assertEqual(config.registry.getUtility(IResponseFactory),
+ dummyfactory)
+
def test_set_root_factory(self):
from pyramid.interfaces import IRootFactory
config = self._makeOne()
@@ -111,6 +126,23 @@ class TestFactoriesMixin(unittest.TestCase):
config = self._makeOne(autocommit=True)
self.assertRaises(AttributeError, config.add_request_method)
+ def test_add_request_method_with_text_type_name(self):
+ from pyramid.interfaces import IRequestExtensions
+ from pyramid.compat import text_, PY3
+ from pyramid.exceptions import ConfigurationError
+
+ config = self._makeOne(autocommit=True)
+ def boomshaka(r): pass
+
+ def get_bad_name():
+ if PY3: # pragma: nocover
+ name = b'La Pe\xc3\xb1a'
+ else: # pragma: nocover
+ name = text_(b'La Pe\xc3\xb1a', 'utf-8')
+
+ config.add_request_method(boomshaka, name=name)
+
+ self.assertRaises(ConfigurationError, get_bad_name)
class TestDeprecatedFactoriesMixinMethods(unittest.TestCase):
def setUp(self):
@@ -120,7 +152,7 @@ class TestDeprecatedFactoriesMixinMethods(unittest.TestCase):
def tearDown(self):
from zope.deprecation import __show__
__show__.on()
-
+
def _makeOne(self, *arg, **kw):
from pyramid.config import Configurator
config = Configurator(*arg, **kw)
diff --git a/pyramid/tests/test_config/test_init.py b/pyramid/tests/test_config/test_init.py
index d6dba17f6..0ed04eb06 100644
--- a/pyramid/tests/test_config/test_init.py
+++ b/pyramid/tests/test_config/test_init.py
@@ -546,6 +546,18 @@ class ConfiguratorTests(unittest.TestCase):
utility = reg.getUtility(IRequestFactory)
self.assertEqual(utility, factory)
+ def test_setup_registry_response_factory(self):
+ from pyramid.registry import Registry
+ from pyramid.interfaces import IResponseFactory
+ reg = Registry()
+ config = self._makeOne(reg)
+ factory = lambda r: object()
+ config.setup_registry(response_factory=factory)
+ self.assertEqual(reg.queryUtility(IResponseFactory), None)
+ config.commit()
+ utility = reg.getUtility(IResponseFactory)
+ self.assertEqual(utility, factory)
+
def test_setup_registry_request_factory_dottedname(self):
from pyramid.registry import Registry
from pyramid.interfaces import IRequestFactory
@@ -736,6 +748,18 @@ pyramid.tests.test_config.dummy_include2""",
else: # pragma: no cover
raise AssertionError
+ def test_include_constant_root_package(self):
+ from pyramid import tests
+ from pyramid.tests import test_config
+ config = self._makeOne(root_package=tests)
+ results = {}
+ def include(config):
+ results['package'] = config.package
+ results['root_package'] = config.root_package
+ config.include(include)
+ self.assertEqual(results['root_package'], tests)
+ self.assertEqual(results['package'], test_config)
+
def test_action_branching_kw_is_None(self):
config = self._makeOne(autocommit=True)
self.assertEqual(config.action('discrim'), None)
@@ -1491,6 +1515,73 @@ class TestActionState(unittest.TestCase):
self.assertRaises(ConfigurationExecutionError, c.execute_actions)
self.assertEqual(output, [('f', (1,), {}), ('f', (2,), {})])
+ def test_reentrant_action(self):
+ output = []
+ c = self._makeOne()
+ def f(*a, **k):
+ output.append(('f', a, k))
+ c.actions.append((3, g, (8,), {}))
+ def g(*a, **k):
+ output.append(('g', a, k))
+ c.actions = [
+ (1, f, (1,)),
+ ]
+ c.execute_actions()
+ self.assertEqual(output, [('f', (1,), {}), ('g', (8,), {})])
+
+ def test_reentrant_action_error(self):
+ from pyramid.exceptions import ConfigurationError
+ c = self._makeOne()
+ def f(*a, **k):
+ c.actions.append((3, g, (8,), {}, (), None, -1))
+ def g(*a, **k): pass
+ c.actions = [
+ (1, f, (1,)),
+ ]
+ self.assertRaises(ConfigurationError, c.execute_actions)
+
+ def test_reentrant_action_without_clear(self):
+ c = self._makeOne()
+ def f(*a, **k):
+ c.actions.append((3, g, (8,)))
+ def g(*a, **k): pass
+ c.actions = [
+ (1, f, (1,)),
+ ]
+ c.execute_actions(clear=False)
+ self.assertEqual(c.actions, [
+ (1, f, (1,)),
+ (3, g, (8,)),
+ ])
+
+class Test_reentrant_action_functional(unittest.TestCase):
+ def _makeConfigurator(self, *arg, **kw):
+ from pyramid.config import Configurator
+ config = Configurator(*arg, **kw)
+ return config
+
+ def test_functional(self):
+ def add_auto_route(config, name, view):
+ def register():
+ config.add_view(route_name=name, view=view)
+ config.add_route(name, '/' + name)
+ config.action(
+ ('auto route', name), register, order=-30
+ )
+ config = self._makeConfigurator()
+ config.add_directive('add_auto_route', add_auto_route)
+ def my_view(request): return request.response
+ config.add_auto_route('foo', my_view)
+ config.commit()
+ from pyramid.interfaces import IRoutesMapper
+ mapper = config.registry.getUtility(IRoutesMapper)
+ routes = mapper.get_routes()
+ route = routes[0]
+ self.assertEqual(len(routes), 1)
+ self.assertEqual(route.name, 'foo')
+ self.assertEqual(route.path, '/foo')
+
+
class Test_resolveConflicts(unittest.TestCase):
def _callFUT(self, actions):
from pyramid.config import resolveConflicts
diff --git a/pyramid/tests/test_config/test_settings.py b/pyramid/tests/test_config/test_settings.py
index c74f96375..d2a98b347 100644
--- a/pyramid/tests/test_config/test_settings.py
+++ b/pyramid/tests/test_config/test_settings.py
@@ -57,7 +57,7 @@ class TestSettingsConfiguratorMixin(unittest.TestCase):
self.assertEqual(settings['a'], 1)
class TestSettings(unittest.TestCase):
-
+
def _getTargetClass(self):
from pyramid.config.settings import Settings
return Settings
@@ -131,6 +131,35 @@ class TestSettings(unittest.TestCase):
self.assertEqual(result['prevent_http_cache'], True)
self.assertEqual(result['pyramid.prevent_http_cache'], True)
+ def test_prevent_cachebust(self):
+ settings = self._makeOne({})
+ self.assertEqual(settings['prevent_cachebust'], False)
+ self.assertEqual(settings['pyramid.prevent_cachebust'], False)
+ result = self._makeOne({'prevent_cachebust':'false'})
+ self.assertEqual(result['prevent_cachebust'], False)
+ self.assertEqual(result['pyramid.prevent_cachebust'], False)
+ result = self._makeOne({'prevent_cachebust':'t'})
+ self.assertEqual(result['prevent_cachebust'], True)
+ self.assertEqual(result['pyramid.prevent_cachebust'], True)
+ result = self._makeOne({'prevent_cachebust':'1'})
+ self.assertEqual(result['prevent_cachebust'], True)
+ self.assertEqual(result['pyramid.prevent_cachebust'], True)
+ result = self._makeOne({'pyramid.prevent_cachebust':'t'})
+ self.assertEqual(result['prevent_cachebust'], True)
+ self.assertEqual(result['pyramid.prevent_cachebust'], True)
+ result = self._makeOne({}, {'PYRAMID_PREVENT_CACHEBUST':'1'})
+ self.assertEqual(result['prevent_cachebust'], True)
+ self.assertEqual(result['pyramid.prevent_cachebust'], True)
+ result = self._makeOne({'prevent_cachebust':'false',
+ 'pyramid.prevent_cachebust':'1'})
+ self.assertEqual(result['prevent_cachebust'], True)
+ self.assertEqual(result['pyramid.prevent_cachebust'], True)
+ result = self._makeOne({'prevent_cachebust':'false',
+ 'pyramid.prevent_cachebust':'f'},
+ {'PYRAMID_PREVENT_CACHEBUST':'1'})
+ self.assertEqual(result['prevent_cachebust'], True)
+ self.assertEqual(result['pyramid.prevent_cachebust'], True)
+
def test_reload_templates(self):
settings = self._makeOne({})
self.assertEqual(settings['reload_templates'], False)
diff --git a/pyramid/tests/test_config/test_util.py b/pyramid/tests/test_config/test_util.py
index bb61714ae..ccf7fa260 100644
--- a/pyramid/tests/test_config/test_util.py
+++ b/pyramid/tests/test_config/test_util.py
@@ -568,6 +568,13 @@ class Test_takes_one_arg(unittest.TestCase):
foo = Foo()
self.assertTrue(self._callFUT(foo.method))
+ def test_function_annotations(self):
+ def foo(bar):
+ """ """
+ # avoid SyntaxErrors in python2, this if effectively nop
+ getattr(foo, '__annotations__', {}).update({'bar': 'baz'})
+ self.assertTrue(self._callFUT(foo))
+
class TestNotted(unittest.TestCase):
def _makeOne(self, predicate):
from pyramid.config.util import Notted
diff --git a/pyramid/tests/test_config/test_views.py b/pyramid/tests/test_config/test_views.py
index 57bb5e9d0..c5db121a0 100644
--- a/pyramid/tests/test_config/test_views.py
+++ b/pyramid/tests/test_config/test_views.py
@@ -113,7 +113,7 @@ class TestViewsConfigurationMixin(unittest.TestCase):
config.add_view(renderer='dummy.pt')
view = self._getViewCallable(config)
self.assertRaises(ValueError, view, None, None)
-
+
def test_add_view_with_tmpl_renderer_factory_no_renderer_factory(self):
config = self._makeOne(autocommit=True)
introspector = DummyIntrospector()
@@ -136,7 +136,7 @@ class TestViewsConfigurationMixin(unittest.TestCase):
('renderer factories', '.pt') in introspector.related[-1])
view = self._getViewCallable(config)
self.assertTrue(b'Hello!' in view(None, None).body)
-
+
def test_add_view_wrapped_view_is_decorated(self):
def view(request): # request-only wrapper
""" """
@@ -1666,6 +1666,20 @@ class TestViewsConfigurationMixin(unittest.TestCase):
renderer=null_renderer)
self.assertRaises(ConfigurationConflictError, config.commit)
+ def test_add_view_class_method_no_attr(self):
+ from pyramid.renderers import null_renderer
+ from zope.interface import directlyProvides
+ from pyramid.exceptions import ConfigurationError
+
+ config = self._makeOne(autocommit=True)
+ class DummyViewClass(object):
+ def run(self): pass
+
+ def configure_view():
+ config.add_view(view=DummyViewClass.run, renderer=null_renderer)
+
+ self.assertRaises(ConfigurationError, configure_view)
+
def test_derive_view_function(self):
from pyramid.renderers import null_renderer
def view(request):
@@ -1783,6 +1797,21 @@ class TestViewsConfigurationMixin(unittest.TestCase):
result = view(None, request)
self.assertEqual(result, 'OK')
+ def test_add_forbidden_view_no_view_argument(self):
+ from zope.interface import implementedBy
+ from pyramid.interfaces import IRequest
+ from pyramid.httpexceptions import HTTPForbidden
+ config = self._makeOne(autocommit=True)
+ config.setup_registry()
+ config.add_forbidden_view()
+ request = self._makeRequest(config)
+ view = self._getViewCallable(config,
+ ctx_iface=implementedBy(HTTPForbidden),
+ request_iface=IRequest)
+ context = HTTPForbidden()
+ result = view(context, request)
+ self.assertEqual(result, context)
+
def test_add_forbidden_view_allows_other_predicates(self):
from pyramid.renderers import null_renderer
config = self._makeOne(autocommit=True)
@@ -1860,6 +1889,21 @@ class TestViewsConfigurationMixin(unittest.TestCase):
result = view(None, request)
self.assertEqual(result, (None, request))
+ def test_add_notfound_view_no_view_argument(self):
+ from zope.interface import implementedBy
+ from pyramid.interfaces import IRequest
+ from pyramid.httpexceptions import HTTPNotFound
+ config = self._makeOne(autocommit=True)
+ config.setup_registry()
+ config.add_notfound_view()
+ request = self._makeRequest(config)
+ view = self._getViewCallable(config,
+ ctx_iface=implementedBy(HTTPNotFound),
+ request_iface=IRequest)
+ context = HTTPNotFound()
+ result = view(context, request)
+ self.assertEqual(result, context)
+
def test_add_notfound_view_allows_other_predicates(self):
from pyramid.renderers import null_renderer
config = self._makeOne(autocommit=True)
@@ -1897,7 +1941,7 @@ class TestViewsConfigurationMixin(unittest.TestCase):
from pyramid.renderers import null_renderer
from zope.interface import implementedBy
from pyramid.interfaces import IRequest
- from pyramid.httpexceptions import HTTPNotFound
+ from pyramid.httpexceptions import HTTPFound, HTTPNotFound
config = self._makeOne(autocommit=True)
config.add_route('foo', '/foo/')
def view(request): return Response('OK')
@@ -1910,6 +1954,30 @@ class TestViewsConfigurationMixin(unittest.TestCase):
ctx_iface=implementedBy(HTTPNotFound),
request_iface=IRequest)
result = view(None, request)
+ self.assertTrue(isinstance(result, HTTPFound))
+ self.assertEqual(result.location, '/scriptname/foo/?a=1&b=2')
+
+ def test_add_notfound_view_append_slash_custom_response(self):
+ from pyramid.response import Response
+ from pyramid.renderers import null_renderer
+ from zope.interface import implementedBy
+ from pyramid.interfaces import IRequest
+ from pyramid.httpexceptions import HTTPMovedPermanently, HTTPNotFound
+ config = self._makeOne(autocommit=True)
+ config.add_route('foo', '/foo/')
+ def view(request): return Response('OK')
+ config.add_notfound_view(
+ view, renderer=null_renderer,append_slash=HTTPMovedPermanently
+ )
+ request = self._makeRequest(config)
+ request.environ['PATH_INFO'] = '/foo'
+ request.query_string = 'a=1&b=2'
+ request.path = '/scriptname/foo'
+ view = self._getViewCallable(config,
+ ctx_iface=implementedBy(HTTPNotFound),
+ request_iface=IRequest)
+ result = view(None, request)
+ self.assertTrue(isinstance(result, HTTPMovedPermanently))
self.assertEqual(result.location, '/scriptname/foo/?a=1&b=2')
def test_add_notfound_view_with_view_defaults(self):
@@ -2504,6 +2572,8 @@ class TestViewDeriver(unittest.TestCase):
self.assertEqual(view_inst, view)
self.assertEqual(ctx, context)
return response
+ def clone(self):
+ return self
def view(request):
return 'OK'
deriver = self._makeOne(renderer=moo())
@@ -2541,6 +2611,8 @@ class TestViewDeriver(unittest.TestCase):
self.assertEqual(view_inst, 'view')
self.assertEqual(ctx, context)
return response
+ def clone(self):
+ return self
def view(request):
return 'OK'
deriver = self._makeOne(renderer=moo())
@@ -3135,6 +3207,8 @@ class TestViewDeriver(unittest.TestCase):
self.assertEqual(view_inst.__class__, View)
self.assertEqual(ctx, context)
return response
+ def clone(self):
+ return self
class View(object):
def __init__(self, context, request):
pass
@@ -3159,6 +3233,8 @@ class TestViewDeriver(unittest.TestCase):
self.assertEqual(view_inst.__class__, View)
self.assertEqual(ctx, context)
return response
+ def clone(self):
+ return self
class View(object):
def __init__(self, request):
pass
@@ -3183,6 +3259,8 @@ class TestViewDeriver(unittest.TestCase):
self.assertEqual(view_inst.__class__, View)
self.assertEqual(ctx, context)
return response
+ def clone(self):
+ return self
class View:
def __init__(self, context, request):
pass
@@ -3207,6 +3285,8 @@ class TestViewDeriver(unittest.TestCase):
self.assertEqual(view_inst.__class__, View)
self.assertEqual(ctx, context)
return response
+ def clone(self):
+ return self
class View:
def __init__(self, request):
pass
@@ -3231,6 +3311,8 @@ class TestViewDeriver(unittest.TestCase):
self.assertEqual(view_inst, view)
self.assertEqual(ctx, context)
return response
+ def clone(self):
+ return self
class View:
def index(self, context, request):
return {'a':'1'}
@@ -3253,6 +3335,8 @@ class TestViewDeriver(unittest.TestCase):
self.assertEqual(view_inst, view)
self.assertEqual(ctx, context)
return response
+ def clone(self):
+ return self
class View:
def index(self, request):
return {'a':'1'}
@@ -3742,8 +3826,9 @@ class TestStaticURLInfo(unittest.TestCase):
def test_generate_registration_miss(self):
inst = self._makeOne()
- registrations = [(None, 'spec', 'route_name'),
- ('http://example.com/foo/', 'package:path/', None)]
+ registrations = [
+ (None, 'spec', 'route_name', None),
+ ('http://example.com/foo/', 'package:path/', None, None)]
inst._get_registrations = lambda *x: registrations
request = self._makeRequest()
result = inst.generate('package:path/abc', request)
@@ -3751,7 +3836,8 @@ class TestStaticURLInfo(unittest.TestCase):
def test_generate_registration_no_registry_on_request(self):
inst = self._makeOne()
- registrations = [('http://example.com/foo/', 'package:path/', None)]
+ registrations = [
+ ('http://example.com/foo/', 'package:path/', None, None)]
inst._get_registrations = lambda *x: registrations
request = self._makeRequest()
del request.registry
@@ -3760,7 +3846,8 @@ class TestStaticURLInfo(unittest.TestCase):
def test_generate_slash_in_name1(self):
inst = self._makeOne()
- registrations = [('http://example.com/foo/', 'package:path/', None)]
+ registrations = [
+ ('http://example.com/foo/', 'package:path/', None, None)]
inst._get_registrations = lambda *x: registrations
request = self._makeRequest()
result = inst.generate('package:path/abc', request)
@@ -3768,7 +3855,8 @@ class TestStaticURLInfo(unittest.TestCase):
def test_generate_slash_in_name2(self):
inst = self._makeOne()
- registrations = [('http://example.com/foo/', 'package:path/', None)]
+ registrations = [
+ ('http://example.com/foo/', 'package:path/', None, None)]
inst._get_registrations = lambda *x: registrations
request = self._makeRequest()
result = inst.generate('package:path/', request)
@@ -3788,7 +3876,7 @@ class TestStaticURLInfo(unittest.TestCase):
def test_generate_route_url(self):
inst = self._makeOne()
- registrations = [(None, 'package:path/', '__viewname/')]
+ registrations = [(None, 'package:path/', '__viewname/', None)]
inst._get_registrations = lambda *x: registrations
def route_url(n, **kw):
self.assertEqual(n, '__viewname/')
@@ -3801,7 +3889,7 @@ class TestStaticURLInfo(unittest.TestCase):
def test_generate_url_unquoted_local(self):
inst = self._makeOne()
- registrations = [(None, 'package:path/', '__viewname/')]
+ registrations = [(None, 'package:path/', '__viewname/', None)]
inst._get_registrations = lambda *x: registrations
def route_url(n, **kw):
self.assertEqual(n, '__viewname/')
@@ -3814,7 +3902,7 @@ class TestStaticURLInfo(unittest.TestCase):
def test_generate_url_quoted_remote(self):
inst = self._makeOne()
- registrations = [('http://example.com/', 'package:path/', None)]
+ registrations = [('http://example.com/', 'package:path/', None, None)]
inst._get_registrations = lambda *x: registrations
request = self._makeRequest()
result = inst.generate('package:path/abc def', request, a=1)
@@ -3822,7 +3910,7 @@ class TestStaticURLInfo(unittest.TestCase):
def test_generate_url_with_custom_query(self):
inst = self._makeOne()
- registrations = [('http://example.com/', 'package:path/', None)]
+ registrations = [('http://example.com/', 'package:path/', None, None)]
inst._get_registrations = lambda *x: registrations
request = self._makeRequest()
result = inst.generate('package:path/abc def', request, a=1,
@@ -3832,7 +3920,7 @@ class TestStaticURLInfo(unittest.TestCase):
def test_generate_url_with_custom_anchor(self):
inst = self._makeOne()
- registrations = [('http://example.com/', 'package:path/', None)]
+ registrations = [('http://example.com/', 'package:path/', None, None)]
inst._get_registrations = lambda *x: registrations
request = self._makeRequest()
uc = text_(b'La Pe\xc3\xb1a', 'utf-8')
@@ -3841,33 +3929,57 @@ class TestStaticURLInfo(unittest.TestCase):
self.assertEqual(result,
'http://example.com/abc%20def#La%20Pe%C3%B1a')
+ def test_generate_url_cachebust(self):
+ def cachebust(subpath, kw):
+ kw['foo'] = 'bar'
+ return 'foo' + '/' + subpath, kw
+ inst = self._makeOne()
+ registrations = [(None, 'package:path/', '__viewname', cachebust)]
+ inst._get_registrations = lambda *x: registrations
+ request = self._makeRequest()
+ def route_url(n, **kw):
+ self.assertEqual(n, '__viewname')
+ self.assertEqual(kw, {'subpath':'foo/abc', 'foo':'bar'})
+ request.route_url = route_url
+ inst.generate('package:path/abc', request)
+
def test_add_already_exists(self):
inst = self._makeOne()
config = self._makeConfig(
[('http://example.com/', 'package:path/', None)])
inst.add(config, 'http://example.com', 'anotherpackage:path')
- expected = [('http://example.com/', 'anotherpackage:path/', None)]
+ expected = [
+ ('http://example.com/', 'anotherpackage:path/', None, None)]
+ self._assertRegistrations(config, expected)
+
+ def test_add_package_root(self):
+ inst = self._makeOne()
+ config = self._makeConfig()
+ inst.add(config, 'http://example.com', 'package:')
+ expected = [('http://example.com/', 'package:', None, None)]
self._assertRegistrations(config, expected)
def test_add_url_withendslash(self):
inst = self._makeOne()
config = self._makeConfig()
inst.add(config, 'http://example.com/', 'anotherpackage:path')
- expected = [('http://example.com/', 'anotherpackage:path/', None)]
+ expected = [
+ ('http://example.com/', 'anotherpackage:path/', None, None)]
self._assertRegistrations(config, expected)
def test_add_url_noendslash(self):
inst = self._makeOne()
config = self._makeConfig()
inst.add(config, 'http://example.com', 'anotherpackage:path')
- expected = [('http://example.com/', 'anotherpackage:path/', None)]
+ expected = [
+ ('http://example.com/', 'anotherpackage:path/', None, None)]
self._assertRegistrations(config, expected)
def test_add_url_noscheme(self):
inst = self._makeOne()
config = self._makeConfig()
inst.add(config, '//example.com', 'anotherpackage:path')
- expected = [('//example.com/', 'anotherpackage:path/', None)]
+ expected = [('//example.com/', 'anotherpackage:path/', None, None)]
self._assertRegistrations(config, expected)
def test_add_viewname(self):
@@ -3876,7 +3988,7 @@ class TestStaticURLInfo(unittest.TestCase):
config = self._makeConfig()
inst = self._makeOne()
inst.add(config, 'view', 'anotherpackage:path', cache_max_age=1)
- expected = [(None, 'anotherpackage:path/', '__view/')]
+ expected = [(None, 'anotherpackage:path/', '__view/', None)]
self._assertRegistrations(config, expected)
self.assertEqual(config.route_args, ('__view/', 'view/*subpath'))
self.assertEqual(config.view_kw['permission'], NO_PERMISSION_REQUIRED)
@@ -3887,7 +3999,7 @@ class TestStaticURLInfo(unittest.TestCase):
config.route_prefix = '/abc'
inst = self._makeOne()
inst.add(config, 'view', 'anotherpackage:path',)
- expected = [(None, 'anotherpackage:path/', '__/abc/view/')]
+ expected = [(None, 'anotherpackage:path/', '__/abc/view/', None)]
self._assertRegistrations(config, expected)
self.assertEqual(config.route_args, ('__/abc/view/', 'view/*subpath'))
@@ -3904,7 +4016,7 @@ class TestStaticURLInfo(unittest.TestCase):
inst.add(config, 'view', 'anotherpackage:path', cache_max_age=1,
context=DummyContext)
self.assertEqual(config.view_kw['context'], DummyContext)
-
+
def test_add_viewname_with_for_(self):
config = self._makeConfig()
inst = self._makeOne()
@@ -3920,6 +4032,34 @@ class TestStaticURLInfo(unittest.TestCase):
self.assertEqual(config.view_kw['renderer'],
'mypackage:templates/index.pt')
+ def test_add_cachebust_default(self):
+ config = self._makeConfig()
+ inst = self._makeOne()
+ inst._default_cachebust = lambda: DummyCacheBuster('foo')
+ inst.add(config, 'view', 'mypackage:path', cachebust=True)
+ cachebust = config.registry._static_url_registrations[0][3]
+ subpath, kw = cachebust('some/path', {})
+ self.assertEqual(subpath, 'some/path')
+ self.assertEqual(kw['x'], 'foo')
+
+ def test_add_cachebust_prevented(self):
+ config = self._makeConfig()
+ config.registry.settings['pyramid.prevent_cachebust'] = True
+ inst = self._makeOne()
+ inst.add(config, 'view', 'mypackage:path', cachebust=True)
+ cachebust = config.registry._static_url_registrations[0][3]
+ self.assertEqual(cachebust, None)
+
+ def test_add_cachebust_custom(self):
+ config = self._makeConfig()
+ inst = self._makeOne()
+ inst.add(config, 'view', 'mypackage:path',
+ cachebust=DummyCacheBuster('foo'))
+ cachebust = config.registry._static_url_registrations[0][3]
+ subpath, kw = cachebust('some/path', {})
+ self.assertEqual(subpath, 'some/path')
+ self.assertEqual(kw['x'], 'foo')
+
class Test_view_description(unittest.TestCase):
def _callFUT(self, view):
from pyramid.config.views import view_description
@@ -3939,7 +4079,8 @@ class Test_view_description(unittest.TestCase):
class DummyRegistry:
- pass
+ def __init__(self):
+ self.settings = {}
from zope.interface import implementer
from pyramid.interfaces import IResponse
@@ -4025,6 +4166,13 @@ class DummyMultiView:
def __permitted__(self, context, request):
""" """
+class DummyCacheBuster(object):
+ def __init__(self, token):
+ self.token = token
+ def pregenerate(self, pathspec, subpath, kw):
+ kw['x'] = self.token
+ return subpath, kw
+
def parse_httpdate(s):
import datetime
# cannot use %Z, must use literal GMT; Jython honors timezone
diff --git a/pyramid/tests/test_decorator.py b/pyramid/tests/test_decorator.py
index 9ab1b7229..0a98a512d 100644
--- a/pyramid/tests/test_decorator.py
+++ b/pyramid/tests/test_decorator.py
@@ -15,15 +15,19 @@ class TestReify(unittest.TestCase):
self.assertEqual(inst.__dict__['wrapped'], 'a')
def test___get__noinst(self):
- decorator = self._makeOne(None)
+ def wrapped(inst):
+ return 'a' # pragma: no cover
+ decorator = self._makeOne(wrapped)
result = decorator.__get__(None)
self.assertEqual(result, decorator)
- def test___doc__copied(self):
- def wrapped(inst):
- """My doc"""
- decorator = self._makeOne(wrapped)
- self.assertEqual(decorator.__doc__, "My doc")
-
+ def test_dunder_attrs_copied(self):
+ from pyramid.util import viewdefaults
+ decorator = self._makeOne(viewdefaults)
+ self.assertEqual(decorator.__doc__, viewdefaults.__doc__)
+ self.assertEqual(decorator.__name__, viewdefaults.__name__)
+ self.assertEqual(decorator.__module__, viewdefaults.__module__)
+
+
class Dummy(object):
pass
diff --git a/pyramid/tests/test_integration.py b/pyramid/tests/test_integration.py
index 35648ed38..c2786c391 100644
--- a/pyramid/tests/test_integration.py
+++ b/pyramid/tests/test_integration.py
@@ -81,7 +81,7 @@ class StaticAppBase(IntegrationBase):
res = self.testapp.get('/static/.hiddenfile', status=200)
_assertBody(res.body, os.path.join(here, 'fixtures/static/.hiddenfile'))
- if defaultlocale is not None:
+ if defaultlocale is not None: # pragma: no cover
# These tests are expected to fail on LANG=C systems due to decode
# errors and on non-Linux systems due to git highchar handling
# vagaries
diff --git a/pyramid/tests/test_path.py b/pyramid/tests/test_path.py
index fd927996a..f85373fd9 100644
--- a/pyramid/tests/test_path.py
+++ b/pyramid/tests/test_path.py
@@ -376,7 +376,7 @@ class TestDottedNameResolver(unittest.TestCase):
def test_zope_dottedname_style_resolve_builtin(self):
typ = self._makeOne()
- if PY3: # pragma: no cover
+ if PY3:
result = typ._zope_dottedname_style('builtins.str', None)
else:
result = typ._zope_dottedname_style('__builtin__.str', None)
diff --git a/pyramid/tests/test_registry.py b/pyramid/tests/test_registry.py
index 11019b852..50f49f24d 100644
--- a/pyramid/tests/test_registry.py
+++ b/pyramid/tests/test_registry.py
@@ -12,6 +12,11 @@ class TestRegistry(unittest.TestCase):
registry = self._makeOne()
self.assertEqual(registry.__nonzero__(), True)
+ def test_package_name(self):
+ package_name = 'testing'
+ registry = self._getTargetClass()(package_name)
+ self.assertEqual(registry.package_name, package_name)
+
def test_registerHandler_and_notify(self):
registry = self._makeOne()
self.assertEqual(registry.has_listeners, False)
diff --git a/pyramid/tests/test_renderers.py b/pyramid/tests/test_renderers.py
index 2bddd2318..ed6344a40 100644
--- a/pyramid/tests/test_renderers.py
+++ b/pyramid/tests/test_renderers.py
@@ -182,7 +182,10 @@ class TestRendererHelper(unittest.TestCase):
from pyramid.interfaces import IResponseFactory
class ResponseFactory(object):
pass
- self.config.registry.registerUtility(ResponseFactory, IResponseFactory)
+
+ self.config.registry.registerUtility(
+ lambda r: ResponseFactory(), IResponseFactory
+ )
def test_render_to_response(self):
self._registerRendererFactory()
@@ -191,8 +194,8 @@ class TestRendererHelper(unittest.TestCase):
helper = self._makeOne('loo.foo')
response = helper.render_to_response('values', {},
request=request)
- self.assertEqual(response.body[0], 'values')
- self.assertEqual(response.body[1], {})
+ self.assertEqual(response.app_iter[0], 'values')
+ self.assertEqual(response.app_iter[1], {})
def test_get_renderer(self):
factory = self._registerRendererFactory()
@@ -209,8 +212,8 @@ class TestRendererHelper(unittest.TestCase):
request = testing.DummyRequest()
response = 'response'
response = helper.render_view(request, response, view, context)
- self.assertEqual(response.body[0], 'response')
- self.assertEqual(response.body[1],
+ self.assertEqual(response.app_iter[0], 'response')
+ self.assertEqual(response.app_iter[1],
{'renderer_info': helper,
'renderer_name': 'loo.foo',
'request': request,
@@ -287,6 +290,23 @@ class TestRendererHelper(unittest.TestCase):
response = helper._make_response(la.encode('utf-8'), request)
self.assertEqual(response.body, la.encode('utf-8'))
+ def test__make_response_result_is_iterable(self):
+ from pyramid.response import Response
+ request = testing.DummyRequest()
+ request.response = Response()
+ helper = self._makeOne('loo.foo')
+ la = text_('/La Pe\xc3\xb1a', 'utf-8')
+ response = helper._make_response([la.encode('utf-8')], request)
+ self.assertEqual(response.body, la.encode('utf-8'))
+
+ def test__make_response_result_is_other(self):
+ self._registerResponseFactory()
+ request = None
+ helper = self._makeOne('loo.foo')
+ result = object()
+ response = helper._make_response(result, request)
+ self.assertEqual(response.body, result)
+
def test__make_response_result_is_None_no_body(self):
from pyramid.response import Response
request = testing.DummyRequest()
@@ -310,7 +330,9 @@ class TestRendererHelper(unittest.TestCase):
class ResponseFactory(object):
def __init__(self):
pass
- self.config.registry.registerUtility(ResponseFactory, IResponseFactory)
+ self.config.registry.registerUtility(
+ lambda r: ResponseFactory(), IResponseFactory
+ )
request = testing.DummyRequest()
helper = self._makeOne('loo.foo')
response = helper._make_response(b'abc', request)
@@ -495,10 +517,11 @@ class Test_render_to_response(unittest.TestCase):
def tearDown(self):
testing.tearDown()
- def _callFUT(self, renderer_name, value, request=None, package=None):
+ def _callFUT(self, renderer_name, value, request=None, package=None,
+ response=None):
from pyramid.renderers import render_to_response
return render_to_response(renderer_name, value, request=request,
- package=package)
+ package=package, response=response)
def test_it_no_request(self):
renderer = self.config.testing_add_renderer(
@@ -532,6 +555,43 @@ class Test_render_to_response(unittest.TestCase):
renderer.assert_(a=1)
renderer.assert_(request=request)
+ def test_response_preserved(self):
+ request = testing.DummyRequest()
+ response = object() # should error if mutated
+ request.response = response
+ # use a json renderer, which will mutate the response
+ result = self._callFUT('json', dict(a=1), request=request)
+ self.assertEqual(result.body, b'{"a": 1}')
+ self.assertNotEqual(request.response, result)
+ self.assertEqual(request.response, response)
+
+ def test_no_response_to_preserve(self):
+ from pyramid.decorator import reify
+ class DummyRequestWithClassResponse(object):
+ _response = DummyResponse()
+ _response.content_type = None
+ _response.default_content_type = None
+ @reify
+ def response(self):
+ return self._response
+ request = DummyRequestWithClassResponse()
+ # use a json renderer, which will mutate the response
+ result = self._callFUT('json', dict(a=1), request=request)
+ self.assertEqual(result.body, b'{"a": 1}')
+ self.assertFalse('response' in request.__dict__)
+
+ def test_custom_response_object(self):
+ class DummyRequestWithClassResponse(object):
+ pass
+ request = DummyRequestWithClassResponse()
+ response = DummyResponse()
+ # use a json renderer, which will mutate the response
+ result = self._callFUT('json', dict(a=1), request=request,
+ response=response)
+ self.assertTrue(result is response)
+ self.assertEqual(result.body, b'{"a": 1}')
+ self.assertFalse('response' in request.__dict__)
+
class Test_get_renderer(unittest.TestCase):
def setUp(self):
self.config = testing.setUp()
@@ -580,13 +640,26 @@ class TestJSONP(unittest.TestCase):
self.assertEqual(request.response.content_type,
'application/json')
+ def test_render_without_request(self):
+ renderer_factory = self._makeOne()
+ renderer = renderer_factory(None)
+ result = renderer({'a':'1'}, {})
+ self.assertEqual(result, '{"a": "1"}')
+
class Dummy:
pass
class DummyResponse:
status = '200 OK'
+ default_content_type = 'text/html'
+ content_type = default_content_type
headerlist = ()
app_iter = ()
- body = ''
+ body = b''
+
+ # compat for renderer that will set unicode on py3
+ def _set_text(self, val): # pragma: no cover
+ self.body = val.encode('utf8')
+ text = property(fset=_set_text)
diff --git a/pyramid/tests/test_request.py b/pyramid/tests/test_request.py
index ed41b62ff..79cf1abb8 100644
--- a/pyramid/tests/test_request.py
+++ b/pyramid/tests/test_request.py
@@ -1,3 +1,4 @@
+from collections import deque
import unittest
from pyramid import testing
@@ -119,13 +120,13 @@ class TestRequest(unittest.TestCase):
def test_add_response_callback(self):
inst = self._makeOne()
- self.assertEqual(inst.response_callbacks, ())
+ self.assertEqual(inst.response_callbacks, None)
def callback(request, response):
""" """
inst.add_response_callback(callback)
- self.assertEqual(inst.response_callbacks, [callback])
+ self.assertEqual(list(inst.response_callbacks), [callback])
inst.add_response_callback(callback)
- self.assertEqual(inst.response_callbacks, [callback, callback])
+ self.assertEqual(list(inst.response_callbacks), [callback, callback])
def test__process_response_callbacks(self):
inst = self._makeOne()
@@ -135,24 +136,48 @@ class TestRequest(unittest.TestCase):
def callback2(request, response):
request.called2 = True
response.called2 = True
- inst.response_callbacks = [callback1, callback2]
+ inst.add_response_callback(callback1)
+ inst.add_response_callback(callback2)
response = DummyResponse()
inst._process_response_callbacks(response)
self.assertEqual(inst.called1, True)
self.assertEqual(inst.called2, True)
self.assertEqual(response.called1, True)
self.assertEqual(response.called2, True)
- self.assertEqual(inst.response_callbacks, [])
+ self.assertEqual(len(inst.response_callbacks), 0)
+
+ def test__process_response_callback_adding_response_callback(self):
+ """
+ When a response callback adds another callback, that new callback should still be called.
+
+ See https://github.com/Pylons/pyramid/pull/1373
+ """
+ inst = self._makeOne()
+ def callback1(request, response):
+ request.called1 = True
+ response.called1 = True
+ request.add_response_callback(callback2)
+ def callback2(request, response):
+ request.called2 = True
+ response.called2 = True
+ inst.add_response_callback(callback1)
+ response = DummyResponse()
+ inst._process_response_callbacks(response)
+ self.assertEqual(inst.called1, True)
+ self.assertEqual(inst.called2, True)
+ self.assertEqual(response.called1, True)
+ self.assertEqual(response.called2, True)
+ self.assertEqual(len(inst.response_callbacks), 0)
def test_add_finished_callback(self):
inst = self._makeOne()
- self.assertEqual(inst.finished_callbacks, ())
+ self.assertEqual(inst.finished_callbacks, None)
def callback(request):
""" """
inst.add_finished_callback(callback)
- self.assertEqual(inst.finished_callbacks, [callback])
+ self.assertEqual(list(inst.finished_callbacks), [callback])
inst.add_finished_callback(callback)
- self.assertEqual(inst.finished_callbacks, [callback, callback])
+ self.assertEqual(list(inst.finished_callbacks), [callback, callback])
def test__process_finished_callbacks(self):
inst = self._makeOne()
@@ -160,11 +185,12 @@ class TestRequest(unittest.TestCase):
request.called1 = True
def callback2(request):
request.called2 = True
- inst.finished_callbacks = [callback1, callback2]
+ inst.add_finished_callback(callback1)
+ inst.add_finished_callback(callback2)
inst._process_finished_callbacks()
self.assertEqual(inst.called1, True)
self.assertEqual(inst.called2, True)
- self.assertEqual(inst.finished_callbacks, [])
+ self.assertEqual(len(inst.finished_callbacks), 0)
def test_resource_url(self):
self._registerResourceURL()
@@ -284,7 +310,7 @@ class TestRequest(unittest.TestCase):
b'/\xe6\xb5\x81\xe8\xa1\x8c\xe8\xb6\x8b\xe5\x8a\xbf',
'utf-8'
)
- if PY3: # pragma: no cover
+ if PY3:
body = bytes(json.dumps({'a':inp}), 'utf-16')
else:
body = json.dumps({'a':inp}).decode('utf-8').encode('utf-16')
@@ -409,7 +435,50 @@ class Test_call_app_with_subpath_as_path_info(unittest.TestCase):
self.assertEqual(request.environ['SCRIPT_NAME'], '/' + encoded)
self.assertEqual(request.environ['PATH_INFO'], '/' + encoded)
-class DummyRequest:
+class Test_apply_request_extensions(unittest.TestCase):
+ def setUp(self):
+ self.config = testing.setUp()
+
+ def tearDown(self):
+ testing.tearDown()
+
+ def _callFUT(self, request, extensions=None):
+ from pyramid.request import apply_request_extensions
+ return apply_request_extensions(request, extensions=extensions)
+
+ def test_it_with_registry(self):
+ from pyramid.interfaces import IRequestExtensions
+ extensions = Dummy()
+ extensions.methods = {'foo': lambda x, y: y}
+ extensions.descriptors = {'bar': property(lambda x: 'bar')}
+ self.config.registry.registerUtility(extensions, IRequestExtensions)
+ request = DummyRequest()
+ request.registry = self.config.registry
+ self._callFUT(request)
+ self.assertEqual(request.bar, 'bar')
+ self.assertEqual(request.foo('abc'), 'abc')
+
+ def test_it_override_extensions(self):
+ from pyramid.interfaces import IRequestExtensions
+ ignore = Dummy()
+ ignore.methods = {'x': lambda x, y, z: 'asdf'}
+ ignore.descriptors = {'bar': property(lambda x: 'asdf')}
+ self.config.registry.registerUtility(ignore, IRequestExtensions)
+ request = DummyRequest()
+ request.registry = self.config.registry
+
+ extensions = Dummy()
+ extensions.methods = {'foo': lambda x, y: y}
+ extensions.descriptors = {'bar': property(lambda x: 'bar')}
+ self._callFUT(request, extensions=extensions)
+ self.assertRaises(AttributeError, lambda: request.x)
+ self.assertEqual(request.bar, 'bar')
+ self.assertEqual(request.foo('abc'), 'abc')
+
+class Dummy(object):
+ pass
+
+class DummyRequest(object):
def __init__(self, environ=None):
if environ is None:
environ = {}
diff --git a/pyramid/tests/test_response.py b/pyramid/tests/test_response.py
index a16eb8d33..ad55882c9 100644
--- a/pyramid/tests/test_response.py
+++ b/pyramid/tests/test_response.py
@@ -1,4 +1,5 @@
import io
+import mimetypes
import os
import unittest
from pyramid import testing
@@ -7,7 +8,7 @@ class TestResponse(unittest.TestCase):
def _getTargetClass(self):
from pyramid.response import Response
return Response
-
+
def test_implements_IResponse(self):
from pyramid.interfaces import IResponse
cls = self._getTargetClass()
@@ -51,15 +52,11 @@ class TestFileResponse(unittest.TestCase):
r.app_iter.close()
def test_without_content_type(self):
- for suffix, content_type in (
- ('txt', 'text/plain; charset=UTF-8'),
- ('xml', 'application/xml; charset=UTF-8'),
- ('pdf', 'application/pdf')
- ):
+ for suffix in ('txt', 'xml', 'pdf'):
path = self._getPath(suffix)
r = self._makeOne(path)
- self.assertEqual(r.content_type, content_type.split(';')[0])
- self.assertEqual(r.headers['content-type'], content_type)
+ self.assertEqual(r.headers['content-type'].split(';')[0],
+ mimetypes.guess_type(path, strict=False)[0])
r.app_iter.close()
def test_python_277_bug_15207(self):
@@ -122,7 +119,7 @@ class Test_patch_mimetypes(unittest.TestCase):
result = self._callFUT(module)
self.assertEqual(result, True)
self.assertEqual(module.initted, True)
-
+
def test_missing_init(self):
class DummyMimetypes(object):
pass
@@ -177,6 +174,17 @@ class TestResponseAdapter(unittest.TestCase):
self.assertEqual(dummy_venusian.attached,
[(foo, dec.register, 'pyramid')])
+
+class TestGetResponseFactory(unittest.TestCase):
+ def test_get_factory(self):
+ from pyramid.registry import Registry
+ from pyramid.response import Response, _get_response_factory
+
+ registry = Registry()
+ response = _get_response_factory(registry)(None)
+ self.assertTrue(isinstance(response, Response))
+
+
class Dummy(object):
pass
@@ -193,5 +201,3 @@ class DummyVenusian(object):
def attach(self, wrapped, fn, category=None):
self.attached.append((wrapped, fn, category))
-
-
diff --git a/pyramid/tests/test_router.py b/pyramid/tests/test_router.py
index 838e52db0..b57c248d5 100644
--- a/pyramid/tests/test_router.py
+++ b/pyramid/tests/test_router.py
@@ -317,6 +317,7 @@ class TestRouter(unittest.TestCase):
from pyramid.interfaces import IRequestExtensions
from pyramid.interfaces import IRequest
from pyramid.request import Request
+ from pyramid.util import InstancePropertyHelper
context = DummyContext()
self._registerTraverserFactory(context)
class Extensions(object):
@@ -324,11 +325,12 @@ class TestRouter(unittest.TestCase):
self.methods = {}
self.descriptors = {}
extensions = Extensions()
- L = []
+ ext_method = lambda r: 'bar'
+ name, fn = InstancePropertyHelper.make_property(ext_method, name='foo')
+ extensions.descriptors[name] = fn
request = Request.blank('/')
request.request_iface = IRequest
request.registry = self.registry
- request._set_extensions = lambda *x: L.extend(x)
def request_factory(environ):
return request
self.registry.registerUtility(extensions, IRequestExtensions)
@@ -342,7 +344,7 @@ class TestRouter(unittest.TestCase):
router.request_factory = request_factory
start_response = DummyStartResponse()
router(environ, start_response)
- self.assertEqual(L, [extensions])
+ self.assertEqual(view.request.foo, 'bar')
def test_call_view_registered_nonspecific_default_path(self):
from pyramid.interfaces import IViewClassifier
@@ -522,7 +524,7 @@ class TestRouter(unittest.TestCase):
def view(context, request):
def callback(request, response):
response.called_back = True
- request.response_callbacks = [callback]
+ request.add_response_callback(callback)
return response
environ = self._makeEnviron()
self._registerView(view, '', IViewClassifier, IRequest, IContext)
@@ -545,7 +547,7 @@ class TestRouter(unittest.TestCase):
def view(context, request):
def callback(request):
request.environ['called_back'] = True
- request.finished_callbacks = [callback]
+ request.add_finished_callback(callback)
return response
environ = self._makeEnviron()
self._registerView(view, '', IViewClassifier, IRequest, IContext)
@@ -567,7 +569,7 @@ class TestRouter(unittest.TestCase):
def view(context, request):
def callback(request):
request.environ['called_back'] = True
- request.finished_callbacks = [callback]
+ request.add_finished_callback(callback)
raise NotImplementedError
environ = self._makeEnviron()
self._registerView(view, '', IViewClassifier, IRequest, IContext)
@@ -599,17 +601,19 @@ class TestRouter(unittest.TestCase):
environ = self._makeEnviron()
self._registerView(view, '', IViewClassifier, None, None)
request_events = self._registerEventListener(INewRequest)
- aftertraversal_events = self._registerEventListener(IContextFound)
+ context_found_events = self._registerEventListener(IContextFound)
response_events = self._registerEventListener(INewResponse)
router = self._makeOne()
start_response = DummyStartResponse()
result = router(environ, start_response)
self.assertEqual(len(request_events), 1)
self.assertEqual(request_events[0].request.environ, environ)
- self.assertEqual(len(aftertraversal_events), 1)
- self.assertEqual(aftertraversal_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)
self.assertEqual(len(response_events), 1)
self.assertEqual(response_events[0].response, response)
+ self.assertEqual(response_events[0].request.context, context)
self.assertEqual(result, response.app_iter)
def test_call_newrequest_evllist_exc_can_be_caught_by_exceptionview(self):
diff --git a/pyramid/tests/test_scripting.py b/pyramid/tests/test_scripting.py
index a36d1ed71..1e952062b 100644
--- a/pyramid/tests/test_scripting.py
+++ b/pyramid/tests/test_scripting.py
@@ -122,11 +122,15 @@ class Test_prepare(unittest.TestCase):
self.assertEqual(request.context, context)
def test_it_with_extensions(self):
- exts = Dummy()
+ from pyramid.util import InstancePropertyHelper
+ exts = DummyExtensions()
+ ext_method = lambda r: 'bar'
+ name, fn = InstancePropertyHelper.make_property(ext_method, 'foo')
+ exts.descriptors[name] = fn
request = DummyRequest({})
registry = request.registry = self._makeRegistry([exts, DummyFactory])
info = self._callFUT(request=request, registry=registry)
- self.assertEqual(request.extensions, exts)
+ self.assertEqual(request.foo, 'bar')
root, closer = info['root'], info['closer']
closer()
@@ -199,11 +203,13 @@ class DummyThreadLocalManager:
def pop(self):
self.popped.append(True)
-class DummyRequest:
+class DummyRequest(object):
matchdict = None
matched_route = None
def __init__(self, environ):
self.environ = environ
- def _set_extensions(self, exts):
- self.extensions = exts
+class DummyExtensions:
+ def __init__(self):
+ self.descriptors = {}
+ self.methods = {}
diff --git a/pyramid/tests/test_scripts/dummy.py b/pyramid/tests/test_scripts/dummy.py
index 366aa00b5..930b9ed64 100644
--- a/pyramid/tests/test_scripts/dummy.py
+++ b/pyramid/tests/test_scripts/dummy.py
@@ -60,7 +60,7 @@ class DummyMapper(object):
def __init__(self, *routes):
self.routes = routes
- def get_routes(self):
+ def get_routes(self, include_static=False):
return self.routes
class DummyRoute(object):
diff --git a/pyramid/tests/test_scripts/pystartup.txt b/pyramid/tests/test_scripts/pystartup.txt
new file mode 100644
index 000000000..c62c4ca74
--- /dev/null
+++ b/pyramid/tests/test_scripts/pystartup.txt
@@ -0,0 +1,3 @@
+# this file has a .txt extension to avoid coverage reports
+# since it is not imported but rather the contents are read and exec'd
+foo = 1
diff --git a/pyramid/tests/test_scripts/test_pcreate.py b/pyramid/tests/test_scripts/test_pcreate.py
index 2488e9595..63e5e6368 100644
--- a/pyramid/tests/test_scripts/test_pcreate.py
+++ b/pyramid/tests/test_scripts/test_pcreate.py
@@ -12,10 +12,10 @@ class TestPCreateCommand(unittest.TestCase):
from pyramid.scripts.pcreate import PCreateCommand
return PCreateCommand
- def _makeOne(self, *args):
+ def _makeOne(self, *args, **kw):
effargs = ['pcreate']
effargs.extend(args)
- cmd = self._getTargetClass()(effargs)
+ cmd = self._getTargetClass()(effargs, **kw)
cmd.out = self.out
return cmd
@@ -34,8 +34,13 @@ class TestPCreateCommand(unittest.TestCase):
out = self.out_.getvalue()
self.assertTrue(out.startswith('No scaffolds available'))
+ def test_run_no_scaffold_no_args(self):
+ cmd = self._makeOne(quiet=True)
+ result = cmd.run()
+ self.assertEqual(result, 2)
+
def test_run_no_scaffold_name(self):
- cmd = self._makeOne()
+ cmd = self._makeOne('dummy')
result = cmd.run()
self.assertEqual(result, 2)
out = self.out_.getvalue()
@@ -73,6 +78,23 @@ class TestPCreateCommand(unittest.TestCase):
{'project': 'Distro', 'egg': 'Distro', 'package': 'distro',
'pyramid_version': '0.1', 'pyramid_docs_branch':'0.1-branch'})
+ def test_scaffold_with_hyphen_in_project_name(self):
+ import os
+ cmd = self._makeOne('-s', 'dummy', 'Distro-')
+ scaffold = DummyScaffold('dummy')
+ cmd.scaffolds = [scaffold]
+ cmd.pyramid_dist = DummyDist("0.1")
+ result = cmd.run()
+ self.assertEqual(result, 0)
+ self.assertEqual(
+ scaffold.output_dir,
+ os.path.normpath(os.path.join(os.getcwd(), 'Distro-'))
+ )
+ self.assertEqual(
+ scaffold.vars,
+ {'project': 'Distro-', 'egg': 'Distro_', 'package': 'distro_',
+ 'pyramid_version': '0.1', 'pyramid_docs_branch':'0.1-branch'})
+
def test_known_scaffold_absolute_path(self):
import os
path = os.path.abspath('Distro')
diff --git a/pyramid/tests/test_scripts/test_prequest.py b/pyramid/tests/test_scripts/test_prequest.py
index 37f1d3c0f..95cec0518 100644
--- a/pyramid/tests/test_scripts/test_prequest.py
+++ b/pyramid/tests/test_scripts/test_prequest.py
@@ -210,8 +210,21 @@ class TestPRequestCommand(unittest.TestCase):
self.assertEqual(self._path_info, '/')
self.assertEqual(self._spec, 'development.ini')
self.assertEqual(self._app_name, None)
+
self.assertEqual(self._out, [b'abc'])
+ def test_command_method_configures_logging(self):
+ command = self._makeOne(['', 'development.ini', '/'])
+ called_args = []
+
+ def configure_logging(app_spec):
+ called_args.append(app_spec)
+
+ command.configure_logging = configure_logging
+ command.run()
+ self.assertEqual(called_args, ['development.ini'])
+
+
class Test_main(unittest.TestCase):
def _callFUT(self, argv):
from pyramid.scripts.prequest import main
diff --git a/pyramid/tests/test_scripts/test_proutes.py b/pyramid/tests/test_scripts/test_proutes.py
index 25a3cd2e3..e426eee73 100644
--- a/pyramid/tests/test_scripts/test_proutes.py
+++ b/pyramid/tests/test_scripts/test_proutes.py
@@ -1,6 +1,16 @@
import unittest
from pyramid.tests.test_scripts import dummy
+
+class DummyIntrospector(object):
+ def __init__(self):
+ self.relations = {}
+ self.introspectables = {}
+
+ def get(self, name, discrim):
+ pass
+
+
class TestPRoutesCommand(unittest.TestCase):
def _getTargetClass(self):
from pyramid.scripts.proutes import PRoutesCommand
@@ -10,8 +20,20 @@ class TestPRoutesCommand(unittest.TestCase):
cmd = self._getTargetClass()([])
cmd.bootstrap = (dummy.DummyBootstrap(),)
cmd.args = ('/foo/bar/myapp.ini#myapp',)
+
return cmd
+ def _makeRegistry(self):
+ from pyramid.registry import Registry
+ registry = Registry()
+ registry.introspector = DummyIntrospector()
+ return registry
+
+ def _makeConfig(self, *arg, **kw):
+ from pyramid.config import Configurator
+ config = Configurator(*arg, **kw)
+ return config
+
def test_good_args(self):
cmd = self._getTargetClass()([])
cmd.bootstrap = (dummy.DummyBootstrap(),)
@@ -19,6 +41,8 @@ class TestPRoutesCommand(unittest.TestCase):
route = dummy.DummyRoute('a', '/a')
mapper = dummy.DummyMapper(route)
cmd._get_mapper = lambda *arg: mapper
+ registry = self._makeRegistry()
+ cmd.bootstrap = (dummy.DummyBootstrap(registry=registry),)
L = []
cmd.out = lambda msg: L.append(msg)
cmd.run()
@@ -58,12 +82,15 @@ class TestPRoutesCommand(unittest.TestCase):
route = dummy.DummyRoute('a', '/a')
mapper = dummy.DummyMapper(route)
command._get_mapper = lambda *arg: mapper
+ registry = self._makeRegistry()
+ command.bootstrap = (dummy.DummyBootstrap(registry=registry),)
+
L = []
command.out = L.append
result = command.run()
self.assertEqual(result, 0)
self.assertEqual(len(L), 3)
- self.assertEqual(L[-1].split(), ['a', '/a', '<unknown>'])
+ self.assertEqual(L[-1].split(), ['a', '/a', '<unknown>', '*'])
def test_route_with_no_slash_prefix(self):
command = self._makeOne()
@@ -72,16 +99,18 @@ class TestPRoutesCommand(unittest.TestCase):
command._get_mapper = lambda *arg: mapper
L = []
command.out = L.append
+ registry = self._makeRegistry()
+ command.bootstrap = (dummy.DummyBootstrap(registry=registry),)
result = command.run()
self.assertEqual(result, 0)
self.assertEqual(len(L), 3)
- self.assertEqual(L[-1].split(), ['a', '/a', '<unknown>'])
+ self.assertEqual(L[-1].split(), ['a', '/a', '<unknown>', '*'])
def test_single_route_no_views_registered(self):
from zope.interface import Interface
- from pyramid.registry import Registry
from pyramid.interfaces import IRouteRequest
- registry = Registry()
+ registry = self._makeRegistry()
+
def view():pass
class IMyRoute(Interface):
pass
@@ -96,15 +125,15 @@ class TestPRoutesCommand(unittest.TestCase):
result = command.run()
self.assertEqual(result, 0)
self.assertEqual(len(L), 3)
- self.assertEqual(L[-1].split()[:3], ['a', '/a', 'None'])
+ self.assertEqual(L[-1].split()[:3], ['a', '/a', '<unknown>'])
def test_single_route_one_view_registered(self):
from zope.interface import Interface
- from pyramid.registry import Registry
from pyramid.interfaces import IRouteRequest
from pyramid.interfaces import IViewClassifier
from pyramid.interfaces import IView
- registry = Registry()
+ registry = self._makeRegistry()
+
def view():pass
class IMyRoute(Interface):
pass
@@ -123,15 +152,60 @@ class TestPRoutesCommand(unittest.TestCase):
self.assertEqual(result, 0)
self.assertEqual(len(L), 3)
compare_to = L[-1].split()[:3]
- self.assertEqual(compare_to, ['a', '/a', '<function'])
-
+ self.assertEqual(
+ compare_to,
+ ['a', '/a', 'pyramid.tests.test_scripts.test_proutes.view']
+ )
+
+ def test_one_route_with_long_name_one_view_registered(self):
+ from zope.interface import Interface
+ from pyramid.interfaces import IRouteRequest
+ from pyramid.interfaces import IViewClassifier
+ from pyramid.interfaces import IView
+ registry = self._makeRegistry()
+
+ def view():pass
+
+ class IMyRoute(Interface):
+ pass
+
+ registry.registerAdapter(
+ view,
+ (IViewClassifier, IMyRoute, Interface),
+ IView, ''
+ )
+
+ registry.registerUtility(IMyRoute, IRouteRequest,
+ name='very_long_name_123')
+
+ command = self._makeOne()
+ route = dummy.DummyRoute(
+ 'very_long_name_123',
+ '/and_very_long_pattern_as_well'
+ )
+ mapper = dummy.DummyMapper(route)
+ command._get_mapper = lambda *arg: mapper
+ L = []
+ command.out = L.append
+ command.bootstrap = (dummy.DummyBootstrap(registry=registry),)
+ result = command.run()
+ self.assertEqual(result, 0)
+ self.assertEqual(len(L), 3)
+ compare_to = L[-1].split()[:3]
+ self.assertEqual(
+ compare_to,
+ ['very_long_name_123',
+ '/and_very_long_pattern_as_well',
+ 'pyramid.tests.test_scripts.test_proutes.view']
+ )
+
def test_single_route_one_view_registered_with_factory(self):
from zope.interface import Interface
- from pyramid.registry import Registry
from pyramid.interfaces import IRouteRequest
from pyramid.interfaces import IViewClassifier
from pyramid.interfaces import IView
- registry = Registry()
+ registry = self._makeRegistry()
+
def view():pass
class IMyRoot(Interface):
pass
@@ -154,14 +228,529 @@ class TestPRoutesCommand(unittest.TestCase):
self.assertEqual(len(L), 3)
self.assertEqual(L[-1].split()[:3], ['a', '/a', '<unknown>'])
+ def test_single_route_multiview_registered(self):
+ from zope.interface import Interface
+ from pyramid.interfaces import IRouteRequest
+ from pyramid.interfaces import IViewClassifier
+ from pyramid.interfaces import IMultiView
+
+ registry = self._makeRegistry()
+
+ def view(): pass
+
+ class IMyRoute(Interface):
+ pass
+
+ multiview1 = dummy.DummyMultiView(
+ view, context='context',
+ view_name='a1'
+ )
+
+ registry.registerAdapter(
+ multiview1,
+ (IViewClassifier, IMyRoute, Interface),
+ IMultiView, ''
+ )
+ registry.registerUtility(IMyRoute, IRouteRequest, name='a')
+ command = self._makeOne()
+ route = dummy.DummyRoute('a', '/a')
+ mapper = dummy.DummyMapper(route)
+ command._get_mapper = lambda *arg: mapper
+ L = []
+ command.out = L.append
+ command.bootstrap = (dummy.DummyBootstrap(registry=registry),)
+ result = command.run()
+ self.assertEqual(result, 0)
+ self.assertEqual(len(L), 3)
+ compare_to = L[-1].split()[:3]
+ view_module = 'pyramid.tests.test_scripts.dummy'
+ view_str = '<pyramid.tests.test_scripts.dummy.DummyMultiView'
+ final = '%s.%s' % (view_module, view_str)
+
+ self.assertEqual(
+ compare_to,
+ ['a', '/a', final]
+ )
+
def test__get_mapper(self):
- from pyramid.registry import Registry
from pyramid.urldispatch import RoutesMapper
command = self._makeOne()
- registry = Registry()
+ registry = self._makeRegistry()
+
result = command._get_mapper(registry)
self.assertEqual(result.__class__, RoutesMapper)
-
+
+ def test_one_route_all_methods_view_only_post(self):
+ from pyramid.renderers import null_renderer as nr
+
+ def view1(context, request): return 'view1'
+
+ config = self._makeConfig(autocommit=True)
+ config.add_route('foo', '/a/b')
+ config.add_view(
+ route_name='foo',
+ view=view1,
+ renderer=nr,
+ request_method='POST'
+ )
+
+ command = self._makeOne()
+ L = []
+ command.out = L.append
+ command.bootstrap = (dummy.DummyBootstrap(registry=config.registry),)
+ result = command.run()
+ self.assertEqual(result, 0)
+ self.assertEqual(len(L), 3)
+ compare_to = L[-1].split()
+ expected = [
+ 'foo', '/a/b',
+ 'pyramid.tests.test_scripts.test_proutes.view1', 'POST'
+ ]
+ self.assertEqual(compare_to, expected)
+
+ def test_one_route_only_post_view_all_methods(self):
+ from pyramid.renderers import null_renderer as nr
+
+ def view1(context, request): return 'view1'
+
+ config = self._makeConfig(autocommit=True)
+ config.add_route('foo', '/a/b', request_method='POST')
+ config.add_view(
+ route_name='foo',
+ view=view1,
+ renderer=nr,
+ )
+
+ command = self._makeOne()
+ L = []
+ command.out = L.append
+ command.bootstrap = (dummy.DummyBootstrap(registry=config.registry),)
+ result = command.run()
+ self.assertEqual(result, 0)
+ self.assertEqual(len(L), 3)
+ compare_to = L[-1].split()
+ expected = [
+ 'foo', '/a/b',
+ 'pyramid.tests.test_scripts.test_proutes.view1', 'POST'
+ ]
+ self.assertEqual(compare_to, expected)
+
+ def test_one_route_only_post_view_post_and_get(self):
+ from pyramid.renderers import null_renderer as nr
+
+ def view1(context, request): return 'view1'
+
+ config = self._makeConfig(autocommit=True)
+ config.add_route('foo', '/a/b', request_method='POST')
+ config.add_view(
+ route_name='foo',
+ view=view1,
+ renderer=nr,
+ request_method=('POST', 'GET')
+ )
+
+ command = self._makeOne()
+ L = []
+ command.out = L.append
+ command.bootstrap = (dummy.DummyBootstrap(registry=config.registry),)
+ result = command.run()
+ self.assertEqual(result, 0)
+ self.assertEqual(len(L), 3)
+ compare_to = L[-1].split()
+ expected = [
+ 'foo', '/a/b',
+ 'pyramid.tests.test_scripts.test_proutes.view1', 'POST'
+ ]
+ self.assertEqual(compare_to, expected)
+
+ def test_route_request_method_mismatch(self):
+ from pyramid.renderers import null_renderer as nr
+
+ def view1(context, request): return 'view1'
+
+ config = self._makeConfig(autocommit=True)
+ config.add_route('foo', '/a/b', request_method='POST')
+ config.add_view(
+ route_name='foo',
+ view=view1,
+ renderer=nr,
+ request_method='GET'
+ )
+
+ command = self._makeOne()
+ L = []
+ command.out = L.append
+ command.bootstrap = (dummy.DummyBootstrap(registry=config.registry),)
+ result = command.run()
+ self.assertEqual(result, 0)
+ self.assertEqual(len(L), 3)
+ compare_to = L[-1].split()
+ expected = [
+ 'foo', '/a/b',
+ 'pyramid.tests.test_scripts.test_proutes.view1',
+ '<route', 'mismatch>'
+ ]
+ self.assertEqual(compare_to, expected)
+
+ def test_route_static_views(self):
+ from pyramid.renderers import null_renderer as nr
+ config = self._makeConfig(autocommit=True)
+ config.add_static_view('static', 'static', cache_max_age=3600)
+ config.add_static_view(name='static2', path='/var/www/static')
+ config.add_static_view(
+ name='pyramid_scaffold',
+ path='pyramid:scaffolds/starter/+package+/static'
+ )
+
+ command = self._makeOne()
+ L = []
+ command.out = L.append
+ command.bootstrap = (dummy.DummyBootstrap(registry=config.registry),)
+ result = command.run()
+ self.assertEqual(result, 0)
+ self.assertEqual(len(L), 5)
+
+ expected = [
+ ['__static/', '/static/*subpath',
+ 'pyramid.tests.test_scripts:static/', '*'],
+ ['__static2/', '/static2/*subpath', '/var/www/static/', '*'],
+ ['__pyramid_scaffold/', '/pyramid_scaffold/*subpath',
+ 'pyramid:scaffolds/starter/+package+/static/', '*'],
+ ]
+
+ for index, line in enumerate(L[2:]):
+ data = line.split()
+ self.assertEqual(data, expected[index])
+
+ def test_route_no_view(self):
+ from pyramid.renderers import null_renderer as nr
+ config = self._makeConfig(autocommit=True)
+ config.add_route('foo', '/a/b', request_method='POST')
+
+ command = self._makeOne()
+ L = []
+ command.out = L.append
+ command.bootstrap = (dummy.DummyBootstrap(registry=config.registry),)
+ result = command.run()
+ self.assertEqual(result, 0)
+ self.assertEqual(len(L), 3)
+ compare_to = L[-1].split()
+ expected = [
+ 'foo', '/a/b',
+ '<unknown>',
+ 'POST',
+ ]
+ self.assertEqual(compare_to, expected)
+
+ def test_route_as_wsgiapp(self):
+ from pyramid.wsgi import wsgiapp2
+
+ config1 = self._makeConfig(autocommit=True)
+ def view1(context, request): return 'view1'
+ config1.add_route('foo', '/a/b', request_method='POST')
+ config1.add_view(view=view1, route_name='foo')
+
+ config2 = self._makeConfig(autocommit=True)
+ config2.add_route('foo', '/a/b', request_method='POST')
+ config2.add_view(
+ wsgiapp2(config1.make_wsgi_app()),
+ route_name='foo',
+ )
+
+ command = self._makeOne()
+ L = []
+ command.out = L.append
+ command.bootstrap = (dummy.DummyBootstrap(registry=config2.registry),)
+ result = command.run()
+ self.assertEqual(result, 0)
+ self.assertEqual(len(L), 3)
+ compare_to = L[-1].split()
+ expected = [
+ 'foo', '/a/b',
+ '<wsgiapp>',
+ 'POST',
+ ]
+ self.assertEqual(compare_to, expected)
+
+ def test_route_is_get_view_request_method_not_post(self):
+ from pyramid.renderers import null_renderer as nr
+ from pyramid.config import not_
+
+ def view1(context, request): return 'view1'
+
+ config = self._makeConfig(autocommit=True)
+ config.add_route('foo', '/a/b', request_method='GET')
+ config.add_view(
+ route_name='foo',
+ view=view1,
+ renderer=nr,
+ request_method=not_('POST')
+ )
+
+ command = self._makeOne()
+ L = []
+ command.out = L.append
+ command.bootstrap = (dummy.DummyBootstrap(registry=config.registry),)
+ result = command.run()
+ self.assertEqual(result, 0)
+ self.assertEqual(len(L), 3)
+ compare_to = L[-1].split()
+ expected = [
+ 'foo', '/a/b',
+ 'pyramid.tests.test_scripts.test_proutes.view1',
+ 'GET'
+ ]
+ self.assertEqual(compare_to, expected)
+
+ def test_view_request_method_not_post(self):
+ from pyramid.renderers import null_renderer as nr
+ from pyramid.config import not_
+
+ def view1(context, request): return 'view1'
+
+ config = self._makeConfig(autocommit=True)
+ config.add_route('foo', '/a/b')
+ config.add_view(
+ route_name='foo',
+ view=view1,
+ renderer=nr,
+ request_method=not_('POST')
+ )
+
+ command = self._makeOne()
+ L = []
+ command.out = L.append
+ command.bootstrap = (dummy.DummyBootstrap(registry=config.registry),)
+ result = command.run()
+ self.assertEqual(result, 0)
+ self.assertEqual(len(L), 3)
+ compare_to = L[-1].split()
+ expected = [
+ 'foo', '/a/b',
+ 'pyramid.tests.test_scripts.test_proutes.view1',
+ '!POST,*'
+ ]
+ self.assertEqual(compare_to, expected)
+
+ def test_view_glob(self):
+ from pyramid.renderers import null_renderer as nr
+ from pyramid.config import not_
+
+ def view1(context, request): return 'view1'
+ def view2(context, request): return 'view2'
+
+ config = self._makeConfig(autocommit=True)
+ config.add_route('foo', '/a/b')
+ config.add_view(
+ route_name='foo',
+ view=view1,
+ renderer=nr,
+ request_method=not_('POST')
+ )
+
+ config.add_route('bar', '/b/a')
+ config.add_view(
+ route_name='bar',
+ view=view2,
+ renderer=nr,
+ request_method=not_('POST')
+ )
+
+ command = self._makeOne()
+ command.options.glob = '*foo*'
+
+ L = []
+ command.out = L.append
+ command.bootstrap = (dummy.DummyBootstrap(registry=config.registry),)
+ result = command.run()
+ self.assertEqual(result, 0)
+ self.assertEqual(len(L), 3)
+ compare_to = L[-1].split()
+ expected = [
+ 'foo', '/a/b',
+ 'pyramid.tests.test_scripts.test_proutes.view1',
+ '!POST,*'
+ ]
+ self.assertEqual(compare_to, expected)
+
+ def test_good_format(self):
+ from pyramid.renderers import null_renderer as nr
+ from pyramid.config import not_
+
+ def view1(context, request): return 'view1'
+
+ config = self._makeConfig(autocommit=True)
+ config.add_route('foo', '/a/b')
+ config.add_view(
+ route_name='foo',
+ view=view1,
+ renderer=nr,
+ request_method=not_('POST')
+ )
+
+ command = self._makeOne()
+ command.options.glob = '*foo*'
+ command.options.format = 'method,name'
+ L = []
+ command.out = L.append
+ command.bootstrap = (dummy.DummyBootstrap(registry=config.registry),)
+ result = command.run()
+ self.assertEqual(result, 0)
+ self.assertEqual(len(L), 3)
+ compare_to = L[-1].split()
+ expected = ['!POST,*', 'foo']
+
+ self.assertEqual(compare_to, expected)
+ self.assertEqual(L[0].split(), ['Method', 'Name'])
+
+ def test_bad_format(self):
+ from pyramid.renderers import null_renderer as nr
+ from pyramid.config import not_
+
+ def view1(context, request): return 'view1'
+
+ config = self._makeConfig(autocommit=True)
+ config.add_route('foo', '/a/b')
+ config.add_view(
+ route_name='foo',
+ view=view1,
+ renderer=nr,
+ request_method=not_('POST')
+ )
+
+ command = self._makeOne()
+ command.options.glob = '*foo*'
+ command.options.format = 'predicates,name,pattern'
+ L = []
+ command.out = L.append
+ command.bootstrap = (dummy.DummyBootstrap(registry=config.registry),)
+ expected = (
+ "You provided invalid formats ['predicates'], "
+ "Available formats are ['name', 'pattern', 'view', 'method']"
+ )
+ result = command.run()
+ self.assertEqual(result, 2)
+ self.assertEqual(L[0], expected)
+
+ def test_config_format_ini_newlines(self):
+ from pyramid.renderers import null_renderer as nr
+ from pyramid.config import not_
+
+ def view1(context, request): return 'view1'
+
+ config = self._makeConfig(autocommit=True)
+ config.add_route('foo', '/a/b')
+ config.add_view(
+ route_name='foo',
+ view=view1,
+ renderer=nr,
+ request_method=not_('POST')
+ )
+
+ command = self._makeOne()
+
+ L = []
+ command.out = L.append
+ command.bootstrap = (dummy.DummyBootstrap(registry=config.registry),)
+ config_factory = dummy.DummyConfigParserFactory()
+ command.ConfigParser = config_factory
+ config_factory.items = [('format', 'method\nname')]
+
+ result = command.run()
+ self.assertEqual(result, 0)
+ self.assertEqual(len(L), 3)
+ compare_to = L[-1].split()
+ expected = ['!POST,*', 'foo']
+
+ self.assertEqual(compare_to, expected)
+ self.assertEqual(L[0].split(), ['Method', 'Name'])
+
+ def test_config_format_ini_spaces(self):
+ from pyramid.renderers import null_renderer as nr
+ from pyramid.config import not_
+
+ def view1(context, request): return 'view1'
+
+ config = self._makeConfig(autocommit=True)
+ config.add_route('foo', '/a/b')
+ config.add_view(
+ route_name='foo',
+ view=view1,
+ renderer=nr,
+ request_method=not_('POST')
+ )
+
+ command = self._makeOne()
+
+ L = []
+ command.out = L.append
+ command.bootstrap = (dummy.DummyBootstrap(registry=config.registry),)
+ config_factory = dummy.DummyConfigParserFactory()
+ command.ConfigParser = config_factory
+ config_factory.items = [('format', 'method name')]
+
+ result = command.run()
+ self.assertEqual(result, 0)
+ self.assertEqual(len(L), 3)
+ compare_to = L[-1].split()
+ expected = ['!POST,*', 'foo']
+
+ self.assertEqual(compare_to, expected)
+ self.assertEqual(L[0].split(), ['Method', 'Name'])
+
+ def test_config_format_ini_commas(self):
+ from pyramid.renderers import null_renderer as nr
+ from pyramid.config import not_
+
+ def view1(context, request): return 'view1'
+
+ config = self._makeConfig(autocommit=True)
+ config.add_route('foo', '/a/b')
+ config.add_view(
+ route_name='foo',
+ view=view1,
+ renderer=nr,
+ request_method=not_('POST')
+ )
+
+ command = self._makeOne()
+
+ L = []
+ command.out = L.append
+ command.bootstrap = (dummy.DummyBootstrap(registry=config.registry),)
+ config_factory = dummy.DummyConfigParserFactory()
+ command.ConfigParser = config_factory
+ config_factory.items = [('format', 'method,name')]
+
+ result = command.run()
+ self.assertEqual(result, 0)
+ self.assertEqual(len(L), 3)
+ compare_to = L[-1].split()
+ expected = ['!POST,*', 'foo']
+
+ self.assertEqual(compare_to, expected)
+ self.assertEqual(L[0].split(), ['Method', 'Name'])
+
+ def test_static_routes_included_in_list(self):
+ from pyramid.renderers import null_renderer as nr
+
+ config = self._makeConfig(autocommit=True)
+ config.add_route('foo', 'http://example.com/bar.aspx', static=True)
+
+ command = self._makeOne()
+ L = []
+ command.out = L.append
+ command.bootstrap = (dummy.DummyBootstrap(registry=config.registry),)
+ result = command.run()
+ self.assertEqual(result, 0)
+ self.assertEqual(len(L), 3)
+ compare_to = L[-1].split()
+ expected = [
+ 'foo', 'http://example.com/bar.aspx',
+ '<unknown>', '*',
+ ]
+ self.assertEqual(compare_to, expected)
+
class Test_main(unittest.TestCase):
def _callFUT(self, argv):
from pyramid.scripts.proutes import main
@@ -170,4 +759,3 @@ class Test_main(unittest.TestCase):
def test_it(self):
result = self._callFUT(['proutes'])
self.assertEqual(result, 2)
-
diff --git a/pyramid/tests/test_scripts/test_pserve.py b/pyramid/tests/test_scripts/test_pserve.py
index 107ff4c0a..75d4f5bef 100644
--- a/pyramid/tests/test_scripts/test_pserve.py
+++ b/pyramid/tests/test_scripts/test_pserve.py
@@ -4,7 +4,7 @@ import tempfile
import unittest
from pyramid.compat import PY3
-if PY3: # pragma: no cover
+if PY3:
import builtins as __builtin__
else:
import __builtin__
diff --git a/pyramid/tests/test_scripts/test_pshell.py b/pyramid/tests/test_scripts/test_pshell.py
index 7cb130c41..dab32fecd 100644
--- a/pyramid/tests/test_scripts/test_pshell.py
+++ b/pyramid/tests/test_scripts/test_pshell.py
@@ -1,3 +1,4 @@
+import os
import unittest
from pyramid.tests.test_scripts import dummy
@@ -24,6 +25,9 @@ class TestPShellCommand(unittest.TestCase):
self.options.python_shell = ''
self.options.setup = None
cmd.options = self.options
+ # default to None to prevent side-effects from running tests in
+ # unknown environments
+ cmd.pystartup = None
return cmd
def test_make_default_shell(self):
@@ -369,6 +373,25 @@ class TestPShellCommand(unittest.TestCase):
self.assertTrue(self.bootstrap.closer.called)
self.assertTrue(shell.help)
+ def test_command_loads_pythonstartup(self):
+ command = self._makeOne()
+ command.pystartup = (
+ os.path.abspath(
+ os.path.join(
+ os.path.dirname(__file__),
+ 'pystartup.txt')))
+ shell = dummy.DummyShell()
+ command.run(shell)
+ self.assertEqual(self.bootstrap.a[0], '/foo/bar/myapp.ini#myapp')
+ self.assertEqual(shell.env, {
+ 'app':self.bootstrap.app, 'root':self.bootstrap.root,
+ 'registry':self.bootstrap.registry,
+ 'request':self.bootstrap.request,
+ 'root_factory':self.bootstrap.root_factory,
+ 'foo':1,
+ })
+ self.assertTrue(self.bootstrap.closer.called)
+ self.assertTrue(shell.help)
class Test_main(unittest.TestCase):
def _callFUT(self, argv):
diff --git a/pyramid/tests/test_security.py b/pyramid/tests/test_security.py
index 6f08a100c..6d75ac8e3 100644
--- a/pyramid/tests/test_security.py
+++ b/pyramid/tests/test_security.py
@@ -134,9 +134,9 @@ class TestRemember(unittest.TestCase):
def tearDown(self):
testing.tearDown()
- def _callFUT(self, *arg):
+ def _callFUT(self, *arg, **kwarg):
from pyramid.security import remember
- return remember(*arg)
+ return remember(*arg, **kwarg)
def test_no_authentication_policy(self):
request = _makeRequest()
@@ -159,6 +159,19 @@ class TestRemember(unittest.TestCase):
result = self._callFUT(request, 'me')
self.assertEqual(result, [('X-Pyramid-Test', 'me')])
+ def test_with_deprecated_principal_arg(self):
+ request = _makeRequest()
+ registry = request.registry
+ _registerAuthenticationPolicy(registry, 'yo')
+ result = self._callFUT(request, principal='me')
+ self.assertEqual(result, [('X-Pyramid-Test', 'me')])
+
+ def test_with_missing_arg(self):
+ request = _makeRequest()
+ registry = request.registry
+ _registerAuthenticationPolicy(registry, 'yo')
+ self.assertRaises(TypeError, lambda: self._callFUT(request))
+
class TestForget(unittest.TestCase):
def setUp(self):
testing.setUp()
@@ -462,8 +475,8 @@ class DummyAuthenticationPolicy:
def authenticated_userid(self, request):
return self.result
- def remember(self, request, principal, **kw):
- headers = [(_TEST_HEADER, principal)]
+ def remember(self, request, userid, **kw):
+ headers = [(_TEST_HEADER, userid)]
self._header_remembered = headers[0]
return headers
diff --git a/pyramid/tests/test_session.py b/pyramid/tests/test_session.py
index 35c234e99..b013ffa66 100644
--- a/pyramid/tests/test_session.py
+++ b/pyramid/tests/test_session.py
@@ -521,7 +521,7 @@ class Test_manage_accessed(unittest.TestCase):
result = wrapper(session, 'a')
self.assertEqual(result, 1)
callbacks = request.response_callbacks
- self.assertEqual(len(callbacks), 0)
+ if callbacks is not None: self.assertEqual(len(callbacks), 0)
class Test_manage_changed(unittest.TestCase):
def _makeOne(self, wrapped):
diff --git a/pyramid/tests/test_static.py b/pyramid/tests/test_static.py
index 94497d4f6..a3df74b44 100644
--- a/pyramid/tests/test_static.py
+++ b/pyramid/tests/test_static.py
@@ -26,7 +26,7 @@ class Test_static_view_use_subpath_False(unittest.TestCase):
if kw is not None:
environ.update(kw)
return Request(environ=environ)
-
+
def test_ctor_defaultargs(self):
inst = self._makeOne('package:resource_name')
self.assertEqual(inst.package_name, 'package')
@@ -110,6 +110,14 @@ class Test_static_view_use_subpath_False(unittest.TestCase):
response = inst(context, request)
self.assertTrue(b'<html>static</html>' in response.body)
+ def test_cachebust_match(self):
+ inst = self._makeOne('pyramid.tests:fixtures/static')
+ inst.cachebust_match = lambda subpath: subpath[1:]
+ request = self._makeRequest({'PATH_INFO':'/foo/index.html'})
+ context = DummyContext()
+ response = inst(context, request)
+ self.assertTrue(b'<html>static</html>' in response.body)
+
def test_resource_is_file_with_wsgi_file_wrapper(self):
from pyramid.response import _BLOCK_SIZE
inst = self._makeOne('pyramid.tests:fixtures/static')
@@ -218,7 +226,7 @@ class Test_static_view_use_subpath_True(unittest.TestCase):
if kw is not None:
environ.update(kw)
return Request(environ=environ)
-
+
def test_ctor_defaultargs(self):
inst = self._makeOne('package:resource_name')
self.assertEqual(inst.package_name, 'package')
@@ -273,7 +281,7 @@ class Test_static_view_use_subpath_True(unittest.TestCase):
context = DummyContext()
from pyramid.httpexceptions import HTTPNotFound
self.assertRaises(HTTPNotFound, inst, context, request)
-
+
def test_oob_os_sep(self):
import os
inst = self._makeOne('pyramid.tests:fixtures/static')
@@ -360,6 +368,155 @@ class Test_static_view_use_subpath_True(unittest.TestCase):
from pyramid.httpexceptions import HTTPNotFound
self.assertRaises(HTTPNotFound, inst, context, request)
+class TestMd5AssetTokenGenerator(unittest.TestCase):
+ _fspath = None
+ _tmp = None
+
+ @property
+ def fspath(self):
+ if self._fspath:
+ return self._fspath
+
+ import os
+ import tempfile
+ self._tmp = tmp = tempfile.mkdtemp()
+ self._fspath = os.path.join(tmp, 'test.txt')
+ return self._fspath
+
+ def tearDown(self):
+ import shutil
+ if self._tmp:
+ shutil.rmtree(self._tmp)
+
+ def _makeOne(self):
+ from pyramid.static import Md5AssetTokenGenerator as cls
+ return cls()
+
+ def test_package_resource(self):
+ fut = self._makeOne().tokenize
+ expected = '76d653a3a044e2f4b38bb001d283e3d9'
+ token = fut('pyramid.tests:fixtures/static/index.html')
+ self.assertEqual(token, expected)
+
+ def test_filesystem_resource(self):
+ fut = self._makeOne().tokenize
+ expected = 'd5155f250bef0e9923e894dbc713c5dd'
+ with open(self.fspath, 'w') as f:
+ f.write("Are we rich yet?")
+ token = fut(self.fspath)
+ self.assertEqual(token, expected)
+
+ def test_cache(self):
+ fut = self._makeOne().tokenize
+ expected = 'd5155f250bef0e9923e894dbc713c5dd'
+ with open(self.fspath, 'w') as f:
+ f.write("Are we rich yet?")
+ token = fut(self.fspath)
+ self.assertEqual(token, expected)
+
+ # md5 shouldn't change because we've cached it
+ with open(self.fspath, 'w') as f:
+ f.write("Sorry for the convenience.")
+ token = fut(self.fspath)
+ self.assertEqual(token, expected)
+
+class TestPathSegmentMd5CacheBuster(unittest.TestCase):
+
+ def _makeOne(self):
+ from pyramid.static import PathSegmentMd5CacheBuster as cls
+ inst = cls()
+ inst.tokenize = lambda pathspec: 'foo'
+ return inst
+
+ def test_token(self):
+ fut = self._makeOne().tokenize
+ self.assertEqual(fut('whatever'), 'foo')
+
+ def test_pregenerate(self):
+ fut = self._makeOne().pregenerate
+ self.assertEqual(fut('foo', ('bar',), 'kw'), (('foo', 'bar'), 'kw'))
+
+ def test_match(self):
+ fut = self._makeOne().match
+ self.assertEqual(fut(('foo', 'bar')), ('bar',))
+
+class TestQueryStringMd5CacheBuster(unittest.TestCase):
+
+ def _makeOne(self, param=None):
+ from pyramid.static import QueryStringMd5CacheBuster as cls
+ if param:
+ inst = cls(param)
+ else:
+ inst = cls()
+ inst.tokenize = lambda pathspec: 'foo'
+ return inst
+
+ def test_token(self):
+ fut = self._makeOne().tokenize
+ self.assertEqual(fut('whatever'), 'foo')
+
+ def test_pregenerate(self):
+ fut = self._makeOne().pregenerate
+ self.assertEqual(
+ fut('foo', ('bar',), {}),
+ (('bar',), {'_query': {'x': 'foo'}}))
+
+ def test_pregenerate_change_param(self):
+ fut = self._makeOne('y').pregenerate
+ self.assertEqual(
+ fut('foo', ('bar',), {}),
+ (('bar',), {'_query': {'y': 'foo'}}))
+
+ def test_pregenerate_query_is_already_tuples(self):
+ fut = self._makeOne().pregenerate
+ self.assertEqual(
+ fut('foo', ('bar',), {'_query': [('a', 'b')]}),
+ (('bar',), {'_query': (('a', 'b'), ('x', 'foo'))}))
+
+ def test_pregenerate_query_is_tuple_of_tuples(self):
+ fut = self._makeOne().pregenerate
+ self.assertEqual(
+ fut('foo', ('bar',), {'_query': (('a', 'b'),)}),
+ (('bar',), {'_query': (('a', 'b'), ('x', 'foo'))}))
+
+class TestQueryStringConstantCacheBuster(TestQueryStringMd5CacheBuster):
+
+ def _makeOne(self, param=None):
+ from pyramid.static import QueryStringConstantCacheBuster as cls
+ if param:
+ inst = cls('foo', param)
+ else:
+ inst = cls('foo')
+ return inst
+
+ def test_token(self):
+ fut = self._makeOne().tokenize
+ self.assertEqual(fut('whatever'), 'foo')
+
+ def test_pregenerate(self):
+ fut = self._makeOne().pregenerate
+ self.assertEqual(
+ fut('foo', ('bar',), {}),
+ (('bar',), {'_query': {'x': 'foo'}}))
+
+ def test_pregenerate_change_param(self):
+ fut = self._makeOne('y').pregenerate
+ self.assertEqual(
+ fut('foo', ('bar',), {}),
+ (('bar',), {'_query': {'y': 'foo'}}))
+
+ def test_pregenerate_query_is_already_tuples(self):
+ fut = self._makeOne().pregenerate
+ self.assertEqual(
+ fut('foo', ('bar',), {'_query': [('a', 'b')]}),
+ (('bar',), {'_query': (('a', 'b'), ('x', 'foo'))}))
+
+ def test_pregenerate_query_is_tuple_of_tuples(self):
+ fut = self._makeOne().pregenerate
+ self.assertEqual(
+ fut('foo', ('bar',), {'_query': (('a', 'b'),)}),
+ (('bar',), {'_query': (('a', 'b'), ('x', 'foo'))}))
+
class DummyContext:
pass
diff --git a/pyramid/tests/test_testing.py b/pyramid/tests/test_testing.py
index 2d0548b33..113f7e5f4 100644
--- a/pyramid/tests/test_testing.py
+++ b/pyramid/tests/test_testing.py
@@ -217,7 +217,7 @@ class TestDummyRequest(unittest.TestCase):
def test_add_response_callback(self):
request = self._makeOne()
request.add_response_callback(1)
- self.assertEqual(request.response_callbacks, [1])
+ self.assertEqual(list(request.response_callbacks), [1])
def test_registry_is_config_registry_when_setup_is_called_after_ctor(self):
# see https://github.com/Pylons/pyramid/issues/165
@@ -259,7 +259,9 @@ class TestDummyRequest(unittest.TestCase):
registry = Registry('this_test')
class ResponseFactory(object):
pass
- registry.registerUtility(ResponseFactory, IResponseFactory)
+ registry.registerUtility(
+ lambda r: ResponseFactory(), IResponseFactory
+ )
request = self._makeOne()
request.registry = registry
resp = request.response
diff --git a/pyramid/tests/test_traversal.py b/pyramid/tests/test_traversal.py
index 0dcc4a027..aa3f1ad16 100644
--- a/pyramid/tests/test_traversal.py
+++ b/pyramid/tests/test_traversal.py
@@ -335,7 +335,7 @@ class ResourceTreeTraverserTests(unittest.TestCase):
foo = DummyContext(bar, path)
root = DummyContext(foo, 'root')
policy = self._makeOne(root)
- if PY3: # pragma: no cover
+ if PY3:
vhm_root = b'/Qu\xc3\xa9bec'.decode('latin-1')
else:
vhm_root = b'/Qu\xc3\xa9bec'
diff --git a/pyramid/tests/test_urldispatch.py b/pyramid/tests/test_urldispatch.py
index 1755d9f47..20a3a4fc8 100644
--- a/pyramid/tests/test_urldispatch.py
+++ b/pyramid/tests/test_urldispatch.py
@@ -120,7 +120,7 @@ class RoutesMapperTests(unittest.TestCase):
def test___call__pathinfo_cant_be_decoded(self):
from pyramid.exceptions import URLDecodeError
mapper = self._makeOne()
- if PY3: # pragma: no cover
+ if PY3:
path_info = b'\xff\xfe\xe6\x00'.decode('latin-1')
else:
path_info = b'\xff\xfe\xe6\x00'
diff --git a/pyramid/tests/test_util.py b/pyramid/tests/test_util.py
index 2ca4c4a66..2bf6a710f 100644
--- a/pyramid/tests/test_util.py
+++ b/pyramid/tests/test_util.py
@@ -1,9 +1,193 @@
import unittest
from pyramid.compat import PY3
+
+class Test_InstancePropertyHelper(unittest.TestCase):
+ def _makeOne(self):
+ cls = self._getTargetClass()
+ return cls()
+
+ def _getTargetClass(self):
+ from pyramid.util import InstancePropertyHelper
+ return InstancePropertyHelper
+
+ def test_callable(self):
+ def worker(obj):
+ return obj.bar
+ foo = Dummy()
+ helper = self._getTargetClass()
+ helper.set_property(foo, worker)
+ foo.bar = 1
+ self.assertEqual(1, foo.worker)
+ foo.bar = 2
+ self.assertEqual(2, foo.worker)
+
+ def test_callable_with_name(self):
+ def worker(obj):
+ return obj.bar
+ foo = Dummy()
+ helper = self._getTargetClass()
+ helper.set_property(foo, worker, name='x')
+ foo.bar = 1
+ self.assertEqual(1, foo.x)
+ foo.bar = 2
+ self.assertEqual(2, foo.x)
+
+ def test_callable_with_reify(self):
+ def worker(obj):
+ return obj.bar
+ foo = Dummy()
+ helper = self._getTargetClass()
+ helper.set_property(foo, worker, reify=True)
+ foo.bar = 1
+ self.assertEqual(1, foo.worker)
+ foo.bar = 2
+ self.assertEqual(1, foo.worker)
+
+ def test_callable_with_name_reify(self):
+ def worker(obj):
+ return obj.bar
+ foo = Dummy()
+ helper = self._getTargetClass()
+ helper.set_property(foo, worker, name='x')
+ helper.set_property(foo, worker, name='y', reify=True)
+ foo.bar = 1
+ self.assertEqual(1, foo.y)
+ self.assertEqual(1, foo.x)
+ foo.bar = 2
+ self.assertEqual(2, foo.x)
+ self.assertEqual(1, foo.y)
+
+ def test_property_without_name(self):
+ def worker(obj): pass
+ foo = Dummy()
+ helper = self._getTargetClass()
+ self.assertRaises(ValueError, helper.set_property, foo, property(worker))
+
+ def test_property_with_name(self):
+ def worker(obj):
+ return obj.bar
+ foo = Dummy()
+ helper = self._getTargetClass()
+ helper.set_property(foo, property(worker), name='x')
+ foo.bar = 1
+ self.assertEqual(1, foo.x)
+ foo.bar = 2
+ self.assertEqual(2, foo.x)
+
+ def test_property_with_reify(self):
+ def worker(obj): pass
+ foo = Dummy()
+ helper = self._getTargetClass()
+ self.assertRaises(ValueError, helper.set_property,
+ foo, property(worker), name='x', reify=True)
+
+ def test_override_property(self):
+ def worker(obj): pass
+ foo = Dummy()
+ helper = self._getTargetClass()
+ helper.set_property(foo, worker, name='x')
+ def doit():
+ foo.x = 1
+ self.assertRaises(AttributeError, doit)
+
+ def test_override_reify(self):
+ def worker(obj): pass
+ foo = Dummy()
+ helper = self._getTargetClass()
+ helper.set_property(foo, worker, name='x', reify=True)
+ foo.x = 1
+ self.assertEqual(1, foo.x)
+ foo.x = 2
+ self.assertEqual(2, foo.x)
+
+ def test_reset_property(self):
+ foo = Dummy()
+ helper = self._getTargetClass()
+ helper.set_property(foo, lambda _: 1, name='x')
+ self.assertEqual(1, foo.x)
+ helper.set_property(foo, lambda _: 2, name='x')
+ self.assertEqual(2, foo.x)
+
+ def test_reset_reify(self):
+ """ This is questionable behavior, but may as well get notified
+ if it changes."""
+ foo = Dummy()
+ helper = self._getTargetClass()
+ helper.set_property(foo, lambda _: 1, name='x', reify=True)
+ self.assertEqual(1, foo.x)
+ helper.set_property(foo, lambda _: 2, name='x', reify=True)
+ self.assertEqual(1, foo.x)
+
+ def test_make_property(self):
+ from pyramid.decorator import reify
+ helper = self._getTargetClass()
+ name, fn = helper.make_property(lambda x: 1, name='x', reify=True)
+ self.assertEqual(name, 'x')
+ self.assertTrue(isinstance(fn, reify))
+
+ def test_apply_properties_with_iterable(self):
+ foo = Dummy()
+ helper = self._getTargetClass()
+ x = helper.make_property(lambda _: 1, name='x', reify=True)
+ y = helper.make_property(lambda _: 2, name='y')
+ helper.apply_properties(foo, [x, y])
+ self.assertEqual(1, foo.x)
+ self.assertEqual(2, foo.y)
+
+ def test_apply_properties_with_dict(self):
+ foo = Dummy()
+ helper = self._getTargetClass()
+ x_name, x_fn = helper.make_property(lambda _: 1, name='x', reify=True)
+ y_name, y_fn = helper.make_property(lambda _: 2, name='y')
+ helper.apply_properties(foo, {x_name: x_fn, y_name: y_fn})
+ self.assertEqual(1, foo.x)
+ self.assertEqual(2, foo.y)
+
+ def test_make_property_unicode(self):
+ from pyramid.compat import text_
+ from pyramid.exceptions import ConfigurationError
+
+ cls = self._getTargetClass()
+ if PY3: # pragma: nocover
+ name = b'La Pe\xc3\xb1a'
+ else: # pragma: nocover
+ name = text_(b'La Pe\xc3\xb1a', 'utf-8')
+
+ def make_bad_name():
+ cls.make_property(lambda x: 1, name=name, reify=True)
+
+ self.assertRaises(ConfigurationError, make_bad_name)
+
+ def test_add_property(self):
+ helper = self._makeOne()
+ helper.add_property(lambda obj: obj.bar, name='x', reify=True)
+ helper.add_property(lambda obj: obj.bar, name='y')
+ self.assertEqual(len(helper.properties), 2)
+ foo = Dummy()
+ helper.apply(foo)
+ foo.bar = 1
+ self.assertEqual(foo.x, 1)
+ self.assertEqual(foo.y, 1)
+ foo.bar = 2
+ self.assertEqual(foo.x, 1)
+ self.assertEqual(foo.y, 2)
+
+ def test_apply_multiple_times(self):
+ helper = self._makeOne()
+ helper.add_property(lambda obj: 1, name='x')
+ foo, bar = Dummy(), Dummy()
+ helper.apply(foo)
+ self.assertEqual(foo.x, 1)
+ helper.add_property(lambda obj: 2, name='x')
+ helper.apply(bar)
+ self.assertEqual(foo.x, 1)
+ self.assertEqual(bar.x, 2)
+
class Test_InstancePropertyMixin(unittest.TestCase):
def _makeOne(self):
cls = self._getTargetClass()
+
class Foo(cls):
pass
return Foo()
@@ -109,43 +293,6 @@ class Test_InstancePropertyMixin(unittest.TestCase):
foo.set_property(lambda _: 2, name='x', reify=True)
self.assertEqual(1, foo.x)
- def test__make_property(self):
- from pyramid.decorator import reify
- cls = self._getTargetClass()
- name, fn = cls._make_property(lambda x: 1, name='x', reify=True)
- self.assertEqual(name, 'x')
- self.assertTrue(isinstance(fn, reify))
-
- def test__set_properties_with_iterable(self):
- foo = self._makeOne()
- x = foo._make_property(lambda _: 1, name='x', reify=True)
- y = foo._make_property(lambda _: 2, name='y')
- foo._set_properties([x, y])
- self.assertEqual(1, foo.x)
- self.assertEqual(2, foo.y)
-
- def test__set_properties_with_dict(self):
- foo = self._makeOne()
- x_name, x_fn = foo._make_property(lambda _: 1, name='x', reify=True)
- y_name, y_fn = foo._make_property(lambda _: 2, name='y')
- foo._set_properties({x_name: x_fn, y_name: y_fn})
- self.assertEqual(1, foo.x)
- self.assertEqual(2, foo.y)
-
- def test__set_extensions(self):
- inst = self._makeOne()
- def foo(self, result):
- return result
- n, bar = inst._make_property(lambda _: 'bar', name='bar')
- class Extensions(object):
- def __init__(self):
- self.methods = {'foo':foo}
- self.descriptors = {'bar':bar}
- extensions = Extensions()
- inst._set_extensions(extensions)
- self.assertEqual(inst.bar, 'bar')
- self.assertEqual(inst.foo('abc'), 'abc')
-
class Test_WeakOrderedSet(unittest.TestCase):
def _makeOne(self):
from pyramid.config import WeakOrderedSet
@@ -217,6 +364,49 @@ class Test_WeakOrderedSet(unittest.TestCase):
self.assertEqual(list(wos), [])
self.assertEqual(wos.last, None)
+class Test_strings_differ(unittest.TestCase):
+ def _callFUT(self, *args, **kw):
+ from pyramid.util import strings_differ
+ return strings_differ(*args, **kw)
+
+ def test_it(self):
+ self.assertFalse(self._callFUT(b'foo', b'foo'))
+ self.assertTrue(self._callFUT(b'123', b'345'))
+ self.assertTrue(self._callFUT(b'1234', b'123'))
+ self.assertTrue(self._callFUT(b'123', b'1234'))
+
+ def test_it_with_internal_comparator(self):
+ result = self._callFUT(b'foo', b'foo', compare_digest=None)
+ self.assertFalse(result)
+
+ result = self._callFUT(b'123', b'abc', compare_digest=None)
+ self.assertTrue(result)
+
+ def test_it_with_external_comparator(self):
+ class DummyComparator(object):
+ called = False
+ def __init__(self, ret_val):
+ self.ret_val = ret_val
+
+ def __call__(self, a, b):
+ self.called = True
+ return self.ret_val
+
+ dummy_compare = DummyComparator(True)
+ result = self._callFUT(b'foo', b'foo', compare_digest=dummy_compare)
+ self.assertTrue(dummy_compare.called)
+ self.assertFalse(result)
+
+ dummy_compare = DummyComparator(False)
+ result = self._callFUT(b'123', b'345', compare_digest=dummy_compare)
+ self.assertTrue(dummy_compare.called)
+ self.assertTrue(result)
+
+ dummy_compare = DummyComparator(False)
+ result = self._callFUT(b'abc', b'abc', compare_digest=dummy_compare)
+ self.assertTrue(dummy_compare.called)
+ self.assertTrue(result)
+
class Test_object_description(unittest.TestCase):
def _callFUT(self, object):
from pyramid.util import object_description
@@ -241,9 +431,9 @@ class Test_object_description(unittest.TestCase):
self.assertEqual(self._callFUT(('a', 'b')), "('a', 'b')")
def test_set(self):
- if PY3: # pragma: no cover
+ if PY3:
self.assertEqual(self._callFUT(set(['a'])), "{'a'}")
- else: # pragma: no cover
+ else:
self.assertEqual(self._callFUT(set(['a'])), "set(['a'])")
def test_list(self):
@@ -281,7 +471,7 @@ class Test_object_description(unittest.TestCase):
self.assertEqual(
self._callFUT(inst),
"object %s" % str(inst))
-
+
def test_shortened_repr(self):
inst = ['1'] * 1000
self.assertEqual(
@@ -549,7 +739,7 @@ class TestActionInfo(unittest.TestCase):
def _getTargetClass(self):
from pyramid.util import ActionInfo
return ActionInfo
-
+
def _makeOne(self, filename, lineno, function, linerepr):
return self._getTargetClass()(filename, lineno, function, linerepr)
@@ -576,7 +766,36 @@ class TestActionInfo(unittest.TestCase):
"Line 0 of file filename:\n linerepr ")
+class TestCallableName(unittest.TestCase):
+ def test_valid_ascii(self):
+ from pyramid.util import get_callable_name
+ from pyramid.compat import text_, PY3
+
+ if PY3: # pragma: nocover
+ name = b'hello world'
+ else: # pragma: nocover
+ name = text_(b'hello world', 'utf-8')
+
+ self.assertEqual(get_callable_name(name), 'hello world')
+
+ def test_invalid_ascii(self):
+ from pyramid.util import get_callable_name
+ from pyramid.compat import text_, PY3
+ from pyramid.exceptions import ConfigurationError
+
+ def get_bad_name():
+ if PY3: # pragma: nocover
+ name = b'La Pe\xc3\xb1a'
+ else: # pragma: nocover
+ name = text_(b'La Pe\xc3\xb1a', 'utf-8')
+
+ get_callable_name(name)
+
+ self.assertRaises(ConfigurationError, get_bad_name)
+
+
def dummyfunc(): pass
+
class Dummy(object):
pass
diff --git a/pyramid/traversal.py b/pyramid/traversal.py
index 4c275c4c1..a38cf271e 100644
--- a/pyramid/traversal.py
+++ b/pyramid/traversal.py
@@ -575,7 +575,7 @@ the ``safe`` argument to this function. This corresponds to the
"""
-if PY3: # pragma: no cover
+if PY3:
# special-case on Python 2 for speed? unchecked
def quote_path_segment(segment, safe=''):
""" %s """ % quote_path_segment_doc
diff --git a/pyramid/url.py b/pyramid/url.py
index bf4d4ff48..a0f3d7f2f 100644
--- a/pyramid/url.py
+++ b/pyramid/url.py
@@ -223,7 +223,7 @@ class URLMethodsMixin(object):
named portion in the generated URL. For example, if you pass
``_host='foo.com'``, and the URL that would have been generated
without the host replacement is ``http://example.com/a``, the result
- will be ``https://foo.com/a``.
+ will be ``http://foo.com/a``.
Note that if ``_scheme`` is passed as ``https``, and ``_port`` is not
passed, the ``_port`` value is assumed to have been passed as
@@ -414,7 +414,7 @@ class URLMethodsMixin(object):
portion in the generated URL. For example, if you pass
``host='foo.com'``, and the URL that would have been generated
without the host replacement is ``http://example.com/a``, the result
- will be ``https://foo.com/a``.
+ will be ``http://foo.com/a``.
If ``scheme`` is passed as ``https``, and an explicit ``port`` is not
passed, the ``port`` value is assumed to have been passed as ``443``.
diff --git a/pyramid/urldispatch.py b/pyramid/urldispatch.py
index fe4d433c3..4a8828810 100644
--- a/pyramid/urldispatch.py
+++ b/pyramid/urldispatch.py
@@ -42,12 +42,17 @@ class Route(object):
class RoutesMapper(object):
def __init__(self):
self.routelist = []
+ self.static_routes = []
+
self.routes = {}
def has_routes(self):
return bool(self.routelist)
- def get_routes(self):
+ def get_routes(self, include_static=False):
+ if include_static is True:
+ return self.routelist + self.static_routes
+
return self.routelist
def get_route(self, name):
@@ -59,9 +64,13 @@ class RoutesMapper(object):
oldroute = self.routes[name]
if oldroute in self.routelist:
self.routelist.remove(oldroute)
+
route = Route(name, pattern, factory, predicates, pregenerator)
if not static:
self.routelist.append(route)
+ else:
+ self.static_routes.append(route)
+
self.routes[name] = route
return route
@@ -201,7 +210,7 @@ def _compile_route(route):
def generator(dict):
newdict = {}
for k, v in dict.items():
- if PY3: # pragma: no cover
+ if PY3:
if v.__class__ is binary_type:
# url_quote below needs a native string, not bytes on Py3
v = v.decode('utf-8')
diff --git a/pyramid/util.py b/pyramid/util.py
index 6b92f17fc..7a8af4899 100644
--- a/pyramid/util.py
+++ b/pyramid/util.py
@@ -1,4 +1,9 @@
import functools
+try:
+ # py2.7.7+ and py3.3+ have native comparison support
+ from hmac import compare_digest
+except ImportError: # pragma: nocover
+ compare_digest = None
import inspect
import traceback
import weakref
@@ -17,6 +22,7 @@ from pyramid.compat import (
string_types,
text_,
PY3,
+ native_
)
from pyramid.interfaces import IActionInfo
@@ -28,14 +34,21 @@ class DottedNameResolver(_DottedNameResolver):
_marker = object()
-class InstancePropertyMixin(object):
- """ Mixin that will allow an instance to add properties at
- run-time as if they had been defined via @property or @reify
- on the class itself.
+class InstancePropertyHelper(object):
+ """A helper object for assigning properties and descriptors to instances.
+ It is not normally possible to do this because descriptors must be
+ defined on the class itself.
+
+ This class is optimized for adding multiple properties at once to an
+ instance. This is done by calling :meth:`.add_property` once
+ per-property and then invoking :meth:`.apply` on target objects.
+
"""
+ def __init__(self):
+ self.properties = {}
@classmethod
- def _make_property(cls, callable, name=None, reify=False):
+ def make_property(cls, callable, name=None, reify=False):
""" Convert a callable into one suitable for adding to the
instance. This will return a 2-tuple containing the computed
(name, property) pair.
@@ -50,7 +63,7 @@ class InstancePropertyMixin(object):
raise ValueError('cannot reify a property')
elif name is not None:
fn = lambda this: callable(this)
- fn.__name__ = name
+ fn.__name__ = get_callable_name(name)
fn.__doc__ = callable.__doc__
else:
name = callable.__name__
@@ -63,25 +76,15 @@ class InstancePropertyMixin(object):
return name, fn
- def _set_properties(self, properties):
- """ Create several properties on the instance at once.
-
- This is a more efficient version of
- :meth:`pyramid.util.InstancePropertyMixin.set_property` which
- can accept multiple ``(name, property)`` pairs generated via
- :meth:`pyramid.util.InstancePropertyMixin._make_property`.
-
- ``properties`` is a sequence of two-tuples *or* a data structure
- with an ``.items()`` method which returns a sequence of two-tuples
- (presumably a dictionary). It will be used to add several
- properties to the instance in a manner that is more efficient
- than simply calling ``set_property`` repeatedly.
+ @classmethod
+ def apply_properties(cls, target, properties):
+ """Accept a list or dict of ``properties`` generated from
+ :meth:`.make_property` and apply them to a ``target`` object.
"""
attrs = dict(properties)
-
if attrs:
- parent = self.__class__
- cls = type(parent.__name__, (parent, object), attrs)
+ parent = target.__class__
+ newcls = type(parent.__name__, (parent, object), attrs)
# We assign __provides__, __implemented__ and __providedBy__ below
# to prevent a memory leak that results from from the usage of this
# instance's eventual use in an adapter lookup. Adapter lookup
@@ -100,14 +103,34 @@ class InstancePropertyMixin(object):
# attached to it
val = getattr(parent, name, _marker)
if val is not _marker:
- setattr(cls, name, val)
- self.__class__ = cls
+ setattr(newcls, name, val)
+ target.__class__ = newcls
+
+ @classmethod
+ def set_property(cls, target, callable, name=None, reify=False):
+ """A helper method to apply a single property to an instance."""
+ prop = cls.make_property(callable, name=name, reify=reify)
+ cls.apply_properties(target, [prop])
+
+ def add_property(self, callable, name=None, reify=False):
+ """Add a new property configuration.
- def _set_extensions(self, extensions):
- for name, fn in iteritems_(extensions.methods):
- method = fn.__get__(self, self.__class__)
- setattr(self, name, method)
- self._set_properties(extensions.descriptors)
+ This should be used in combination with :meth:`.apply` as a
+ more efficient version of :meth:`.set_property`.
+ """
+ name, fn = self.make_property(callable, name=name, reify=reify)
+ self.properties[name] = fn
+
+ def apply(self, target):
+ """ Apply all configured properties to the ``target`` instance."""
+ if self.properties:
+ self.apply_properties(target, self.properties)
+
+class InstancePropertyMixin(object):
+ """ Mixin that will allow an instance to add properties at
+ run-time as if they had been defined via @property or @reify
+ on the class itself.
+ """
def set_property(self, callable, name=None, reify=False):
""" Add a callable or a property descriptor to the instance.
@@ -161,8 +184,8 @@ class InstancePropertyMixin(object):
>>> foo.y # notice y keeps the original value
1
"""
- prop = self._make_property(callable, name=name, reify=reify)
- self._set_properties([prop])
+ InstancePropertyHelper.set_property(
+ self, callable, name=name, reify=reify)
class WeakOrderedSet(object):
""" Maintain a set of items.
@@ -227,7 +250,7 @@ class WeakOrderedSet(object):
oid = self._order[-1]
return self._items[oid]()
-def strings_differ(string1, string2):
+def strings_differ(string1, string2, compare_digest=compare_digest):
"""Check whether two strings differ while avoiding timing attacks.
This function returns True if the given strings differ and False
@@ -237,14 +260,25 @@ def strings_differ(string1, string2):
http://seb.dbzteam.org/crypto/python-oauth-timing-hmac.pdf
- """
- if len(string1) != len(string2):
- return True
-
- invalid_bits = 0
- for a, b in zip(string1, string2):
- invalid_bits += a != b
+ .. versionchanged:: 1.6
+ Support :func:`hmac.compare_digest` if it is available (Python 2.7.7+
+ and Python 3.3+).
+ """
+ len_eq = len(string1) == len(string2)
+ if len_eq:
+ invalid_bits = 0
+ left = string1
+ else:
+ invalid_bits = 1
+ left = string2
+ right = string2
+
+ if compare_digest is not None:
+ invalid_bits += not compare_digest(left, right)
+ else:
+ for a, b in zip(left, right):
+ invalid_bits += a != b
return invalid_bits != 0
def object_description(object):
@@ -275,7 +309,7 @@ def object_description(object):
if isinstance(object, (bool, float, type(None))):
return text_(str(object))
if isinstance(object, set):
- if PY3: # pragma: no cover
+ if PY3:
return shortrepr(object, '}')
else:
return shortrepr(object, ')')
@@ -535,3 +569,17 @@ def action_method(wrapped):
wrapper.__docobj__ = wrapped
return wrapper
+
+def get_callable_name(name):
+ """
+ Verifies that the ``name`` is ascii and will raise a ``ConfigurationError``
+ if it is not.
+ """
+ try:
+ return native_(name, 'ascii')
+ except (UnicodeEncodeError, UnicodeDecodeError):
+ msg = (
+ '`name="%s"` is invalid. `name` must be ascii because it is '
+ 'used on __name__ of the method'
+ )
+ raise ConfigurationError(msg % name)
diff --git a/pyramid/view.py b/pyramid/view.py
index 02ac8849f..b30383003 100644
--- a/pyramid/view.py
+++ b/pyramid/view.py
@@ -252,10 +252,11 @@ class AppendSlashNotFoundViewFactory(object):
.. deprecated:: 1.3
"""
- def __init__(self, notfound_view=None):
+ def __init__(self, notfound_view=None, redirect_class=HTTPFound):
if notfound_view is None:
notfound_view = default_exceptionresponse_view
self.notfound_view = notfound_view
+ self.redirect_class = redirect_class
def __call__(self, context, request):
path = decode_path_info(request.environ['PATH_INFO'] or '/')
@@ -268,7 +269,7 @@ class AppendSlashNotFoundViewFactory(object):
qs = request.query_string
if qs:
qs = '?' + qs
- return HTTPFound(location=request.path+'/'+qs)
+ return self.redirect_class(location=request.path+'/'+qs)
return self.notfound_view(context, request)
append_slash_notfound_view = AppendSlashNotFoundViewFactory()
@@ -331,6 +332,31 @@ class notfound_view_config(object):
redirect to the URL implied by the route; if it does not, Pyramid will
return the result of the view callable provided as ``view``, as normal.
+ If the argument provided as ``append_slash`` is not a boolean but
+ instead implements :class:`~pyramid.interfaces.IResponse`, the
+ append_slash logic will behave as if ``append_slash=True`` was passed,
+ but the provided class will be used as the response class instead of
+ the default :class:`~pyramid.httpexceptions.HTTPFound` response class
+ when a redirect is performed. For example:
+
+ .. code-block:: python
+
+ from pyramid.httpexceptions import (
+ HTTPMovedPermanently,
+ HTTPNotFound
+ )
+
+ @notfound_view_config(append_slash=HTTPMovedPermanently)
+ def aview(request):
+ return HTTPNotFound('not found')
+
+ The above means that a redirect to a slash-appended route will be
+ attempted, but instead of :class:`~pyramid.httpexceptions.HTTPFound`
+ being used, :class:`~pyramid.httpexceptions.HTTPMovedPermanently will
+ be used` for the redirect response if a slash-appended route is found.
+
+ .. versionchanged:: 1.6
+
See :ref:`changing_the_notfound_view` for detailed usage information.
"""
@@ -380,7 +406,7 @@ class forbidden_view_config(object):
@forbidden_view_config()
def forbidden(request):
- return Response('You are not allowed', status='401 Unauthorized')
+ return Response('You are not allowed', status='403 Forbidden')
All arguments passed to this function have the same meaning as
:meth:`pyramid.view.view_config` and each predicate argument restricts
diff --git a/rtd.txt b/rtd.txt
index b449ac73c..4aecd9933 100644
--- a/rtd.txt
+++ b/rtd.txt
@@ -1,4 +1,4 @@
+Sphinx >= 1.2.3
repoze.sphinx.autointerface
repoze.lru
pylons_sphinx_latesturl
-
diff --git a/setup.cfg b/setup.cfg
index a877ffb7f..875480594 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -5,8 +5,6 @@ zip_ok = false
match=^test
where=pyramid
nocapture=1
-cover-package=pyramid
-cover-erase=1
[aliases]
dev = develop easy_install pyramid[testing]
diff --git a/setup.py b/setup.py
index d736dc38d..3233193e7 100644
--- a/setup.py
+++ b/setup.py
@@ -56,7 +56,7 @@ if not PY3:
tests_require.append('zope.component>=3.11.0')
docs_extras = [
- 'Sphinx',
+ 'Sphinx >= 1.2.3',
'docutils',
'repoze.sphinx.autointerface',
]
@@ -68,7 +68,7 @@ testing_extras = tests_require + [
]
setup(name='pyramid',
- version='1.6dev',
+ version='1.6.dev0',
description='The Pyramid Web Framework, a Pylons project',
long_description=README + '\n\n' + CHANGES,
classifiers=[
diff --git a/tox.ini b/tox.ini
index 2bf213ca4..e0f99e7f6 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,22 +1,63 @@
[tox]
-envlist =
- py26,py27,py32,py33,py34,pypy,cover
+envlist =
+ py26,py27,py32,py33,py34,pypy,pypy3,
+ {py2,py3}-docs,
+ {py2,py3}-cover,coverage
[testenv]
-commands =
- python setup.py dev
- python setup.py test -q
-
-[testenv:cover]
+# Most of these are defaults but if you specify any you can't fall back
+# to defaults for others.
basepython =
- python2.6
-commands =
- python setup.py dev
- python setup.py nosetests --with-xunit --with-xcoverage
-deps =
- nosexcover
+ py26: python2.6
+ py27: python2.7
+ py32: python3.2
+ py33: python3.3
+ py34: python3.4
+ pypy: pypy
+ pypy3: pypy3
+ py2: python2.7
+ py3: python3.4
+
+commands =
+ pip install pyramid[testing]
+ nosetests --with-xunit --xunit-file=nosetests-{envname}.xml {posargs:}
-# we separate coverage into its own testenv because a) "last run wins" wrt
-# cobertura jenkins reporting and b) pypy and jython can't handle any
-# combination of versions of coverage and nosexcover that i can find.
+[testenv:py2-cover]
+commands =
+ pip install pyramid[testing]
+ coverage run --source=pyramid {envbindir}/nosetests
+ coverage xml -o coverage-py2.xml
+setenv =
+ COVERAGE_FILE=.coverage.py2
+[testenv:py3-cover]
+commands =
+ pip install pyramid[testing]
+ coverage run --source=pyramid {envbindir}/nosetests
+ coverage xml -o coverage-py3.xml
+setenv =
+ COVERAGE_FILE=.coverage.py3
+
+[testenv:py2-docs]
+whitelist_externals = make
+commands =
+ pip install pyramid[docs]
+ make -C docs html
+
+[testenv:py3-docs]
+whitelist_externals = make
+commands =
+ pip install pyramid[docs]
+ make -C docs html
+
+[testenv:coverage]
+basepython = python3.4
+commands =
+ coverage erase
+ coverage combine
+ coverage xml
+ coverage report --show-missing --fail-under=100
+deps =
+ coverage
+setenv =
+ COVERAGE_FILE=.coverage