From 407b335ed9954c042377fd2e060c36edcd07cf60 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Thu, 27 Feb 2014 23:45:24 -0500 Subject: add support for using an absolute path to override an asset fixes #1229 --- CHANGES.txt | 4 + docs/narr/assets.rst | 3 + pyramid/config/assets.py | 216 +++++++--- .../pkgs/asset/subpackage/templates/bar.pt | 0 pyramid/tests/test_config/test_assets.py | 439 +++++++++++++++------ 5 files changed, 496 insertions(+), 166 deletions(-) create mode 100644 pyramid/tests/test_config/pkgs/asset/subpackage/templates/bar.pt diff --git a/CHANGES.txt b/CHANGES.txt index 434eab898..2350bb3de 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,3 +1,7 @@ +- Assets can now be overidden by an absolute path on the filesystem when using + the ``config.override_asset`` API. + See https://github.com/Pylons/pyramid/issues/1229 + Unreleased ========== diff --git a/docs/narr/assets.rst b/docs/narr/assets.rst index b0a8d18b0..fec55ce7c 100644 --- a/docs/narr/assets.rst +++ b/docs/narr/assets.rst @@ -526,3 +526,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/pyramid/config/assets.py b/pyramid/config/assets.py index 0616e6cda..9da092f08 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,180 @@ 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 + 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.package, path): + return pkg_resources.resource_filename(self.package, path) + + def get_stream(self, resource_name): + path = self.get_path(resource_name) + if pkg_resources.resource_exists(self.package, path): + return pkg_resources.resource_stream(self.package, path) + + def get_string(self, resource_name): + path = self.get_path(resource_name) + if pkg_resources.resource_exists(self.package, path): + return pkg_resources.resource_string(self.package, path) + + def exists(self, resource_name): + path = self.get_path(resource_name) + if pkg_resources.resource_exists(self.package, path): + return True + + def isdir(self, resource_name): + path = self.get_path(resource_name) + if pkg_resources.resource_exists(self.package, path): + return pkg_resources.resource_isdir(self.package, path) + + def listdir(self, resource_name): + path = self.get_path(resource_name) + if pkg_resources.resource_exists(self.package, path): + return pkg_resources.resource_listdir(self.package, 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 +369,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/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 diff --git a/pyramid/tests/test_config/test_assets.py b/pyramid/tests/test_config/test_assets.py index 345e7f8d6..b605a602d 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,13 @@ 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') 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 +66,13 @@ 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, '') 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 +83,13 @@ 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/') 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 +100,13 @@ 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, '') 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 +117,105 @@ 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/') + + 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) + + 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) + + 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) 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 +241,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 +289,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 +299,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 +459,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 +469,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 +480,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 +656,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 +782,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 +819,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 +832,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 +877,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: -- cgit v1.2.3 From 5cf18393fbe9084e4b079a1136ed5de46ad89969 Mon Sep 17 00:00:00 2001 From: "Karl O. Pinc" Date: Tue, 12 Aug 2014 20:49:11 -0500 Subject: Docs: Introduce the concept of "userid" into the glossary. --- docs/glossary.rst | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/docs/glossary.rst b/docs/glossary.rst index deb4c1c8b..eb57f3d0d 100644 --- a/docs/glossary.rst +++ b/docs/glossary.rst @@ -286,13 +286,23 @@ 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, having zero or more + :term:`permissions `. Principals are 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". + + userid + A *userid* is a a string or unicode object used to identify and + authenticate a real-world user, often a person. A userid is + supplied to an :term:`authentication policy` in order to discover + the user's :term:`principals `. The default behavior + of the authentication policies :app:`Pyramid` provides is to + return the user's userid as one of the user's principals, but a + userid need not be a principal. authorization policy An authorization policy in :app:`Pyramid` terms is a bit of -- cgit v1.2.3 From 81719b800cfea1c6fd68427ea1d9c0a2f3e6c1dd Mon Sep 17 00:00:00 2001 From: "Karl O. Pinc" Date: Tue, 12 Aug 2014 21:56:26 -0500 Subject: Docs: Make clear that a userid need not be a principal. --- docs/api/request.rst | 10 ++++++---- docs/narr/security.rst | 6 +++--- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/docs/api/request.rst b/docs/api/request.rst index 77d80f6d6..3a32fd938 100644 --- a/docs/api/request.rst +++ b/docs/api/request.rst @@ -194,10 +194,12 @@ .. 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) diff --git a/docs/narr/security.rst b/docs/narr/security.rst index 8db23a33b..57d7ac38f 100644 --- a/docs/narr/security.rst +++ b/docs/narr/security.rst @@ -611,9 +611,9 @@ that implements the following interface: 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 + typically including the 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): -- cgit v1.2.3 From c7afe4e43ab19a5e8274988fe8dd004c04c160a1 Mon Sep 17 00:00:00 2001 From: "Karl O. Pinc" Date: Tue, 12 Aug 2014 22:10:03 -0500 Subject: Security: Change "principal" argument in security.remember() to "userid". Make the change througout the authentication policies, etc. as well. --- docs/narr/security.rst | 14 +++++++------- pyramid/authentication.py | 24 ++++++++++++------------ pyramid/interfaces.py | 4 ++-- pyramid/security.py | 6 +++--- pyramid/testing.py | 4 ++-- pyramid/tests/test_security.py | 4 ++-- 6 files changed, 28 insertions(+), 28 deletions(-) diff --git a/docs/narr/security.rst b/docs/narr/security.rst index 57d7ac38f..16718cfa4 100644 --- a/docs/narr/security.rst +++ b/docs/narr/security.rst @@ -104,9 +104,9 @@ 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 -:term:`ACL` found in the resource tree when attempting to call some -:term:`view`. +to a single :term:`userid` and matches that userid's principals against the +principals present in any :term:`ACL` found in the resource tree when +attempting to call some :term:`view`. While it is possible to mix and match different authentication and authorization policies, it is an error to configure a Pyramid application @@ -616,11 +616,11 @@ that implements the following interface: 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. """ + 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 diff --git a/pyramid/authentication.py b/pyramid/authentication.py index b84981bbc..f4c211ffa 100644 --- a/pyramid/authentication.py +++ b/pyramid/authentication.py @@ -335,11 +335,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 +348,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 +404,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 +652,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=, ``tokens=``. @@ -660,7 +660,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.""" @@ -1061,13 +1061,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 +1132,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/interfaces.py b/pyramid/interfaces.py index c5a70dbfd..bba818c8a 100644 --- a/pyramid/interfaces.py +++ b/pyramid/interfaces.py @@ -460,9 +460,9 @@ class IAuthenticationPolicy(Interface): user, including 'system' groups such as Everyone and 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 + userid named ``userid`` when set in a response. An individual authentication policy and its consumers can decide on the composition and meaning of ``**kw.`` """ diff --git a/pyramid/security.py b/pyramid/security.py index 041155563..3cef7ee5a 100644 --- a/pyramid/security.py +++ b/pyramid/security.py @@ -115,12 +115,12 @@ deprecated( '"effective_principals" attribute of the Pyramid request instead.' ) -def remember(request, principal, **kw): +def remember(request, userid, **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 @@ -142,7 +142,7 @@ def remember(request, principal, **kw): 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): """ diff --git a/pyramid/testing.py b/pyramid/testing.py index 8cbd8b82b..f77889e72 100644 --- a/pyramid/testing.py +++ b/pyramid/testing.py @@ -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): diff --git a/pyramid/tests/test_security.py b/pyramid/tests/test_security.py index 6f08a100c..027f9cda0 100644 --- a/pyramid/tests/test_security.py +++ b/pyramid/tests/test_security.py @@ -462,8 +462,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 -- cgit v1.2.3 From dc324784193a577bc039dcddb0651ef5ec9e6f57 Mon Sep 17 00:00:00 2001 From: "Karl O. Pinc" Date: Tue, 12 Aug 2014 22:12:25 -0500 Subject: Docs: Make "userid" link to the glossary term. --- docs/api/request.rst | 31 ++++++++++++++++--------------- docs/narr/security.rst | 24 +++++++++++++----------- docs/tutorials/wiki/design.rst | 4 ++-- docs/tutorials/wiki2/design.rst | 3 ++- 4 files changed, 33 insertions(+), 29 deletions(-) diff --git a/docs/api/request.rst b/docs/api/request.rst index 3a32fd938..4f93fa34f 100644 --- a/docs/api/request.rst +++ b/docs/api/request.rst @@ -167,27 +167,28 @@ .. 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 diff --git a/docs/narr/security.rst b/docs/narr/security.rst index 16718cfa4..f3879d0ba 100644 --- a/docs/narr/security.rst +++ b/docs/narr/security.rst @@ -595,19 +595,21 @@ 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.""" def effective_principals(self, request): """ Return a sequence representing the effective principals 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 ` to their corresponding passwords. -- GROUPS, a dictionary mapping usernames to a +- GROUPS, a dictionary mapping :term:`userids ` 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 + `) to their corresponding passwords. - GROUPS, a dictionary mapping user names to a list of groups they belong to. -- cgit v1.2.3 From a0cba72fb9925a1476ebf0848fa6ae07bbea5840 Mon Sep 17 00:00:00 2001 From: "Karl O. Pinc" Date: Tue, 12 Aug 2014 22:33:48 -0500 Subject: Docs: Include the concept of credentials in the high level security overview. --- docs/narr/security.rst | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/docs/narr/security.rst b/docs/narr/security.rst index f3879d0ba..29c62d9f3 100644 --- a/docs/narr/security.rst +++ b/docs/narr/security.rst @@ -13,6 +13,11 @@ 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 +30,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. Based on the request and the remembered (or lack of) + :term:`userid` and related credentials it returns some number of + :term:`principal` identifiers. - If an :term:`authorization policy` is in effect and the :term:`view configuration` associated with the view callable that was found has -- cgit v1.2.3 From 6bedf31e5275c2f2a33051a547aa1dc722aafa97 Mon Sep 17 00:00:00 2001 From: "Karl O. Pinc" Date: Tue, 12 Aug 2014 23:05:35 -0500 Subject: Docs: Add resource tree into security overview. --- docs/narr/security.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/narr/security.rst b/docs/narr/security.rst index 29c62d9f3..e6bbff44e 100644 --- a/docs/narr/security.rst +++ b/docs/narr/security.rst @@ -20,6 +20,12 @@ allowed. Here's how it works at a high level: - A :term:`request` is generated when a user visits the application. +- If an :term:`authorization policy` is in effect the application uses + the request and it's :term:`root factory` to create a :ref:`resource tree + ` of :term:`contexts `. The resource + tree maps contexts to URLs and within the contexts the application + puts declarations which authorize access. + - Based on the request, a :term:`context` resource is located through :term:`resource location`. A context is located differently depending on whether the application uses :term:`traversal` or :term:`URL dispatch`, but -- cgit v1.2.3 From 03e95958a9c2b9042e55bc55e4cdb193649857ef Mon Sep 17 00:00:00 2001 From: "Karl O. Pinc" Date: Tue, 12 Aug 2014 23:42:20 -0500 Subject: Docs: Switched first 2 paragraphs of security overview. --- docs/narr/security.rst | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/docs/narr/security.rst b/docs/narr/security.rst index e6bbff44e..203962751 100644 --- a/docs/narr/security.rst +++ b/docs/narr/security.rst @@ -6,8 +6,18 @@ 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`, 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. + +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 @@ -54,14 +64,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`. -- cgit v1.2.3 From fe83c6bfdab16818cb434d95a09bd6510b43aa24 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Wed, 13 Aug 2014 10:48:22 -0500 Subject: some tweaks to the usage of userid in the docs --- docs/api/request.rst | 4 +-- docs/glossary.rst | 15 +++++------ docs/narr/security.rst | 69 ++++++++++++++++++++++++++++---------------------- pyramid/interfaces.py | 57 +++++++++++++++++++++++++++-------------- 4 files changed, 86 insertions(+), 59 deletions(-) diff --git a/docs/api/request.rst b/docs/api/request.rst index 4f93fa34f..dd68fa09c 100644 --- a/docs/api/request.rst +++ b/docs/api/request.rst @@ -172,8 +172,8 @@ 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``. + record associated with the :term:`userid` exists in persistent storage; + if it has not, this value will be ``None``. .. attribute:: unauthenticated_userid diff --git a/docs/glossary.rst b/docs/glossary.rst index eb57f3d0d..ef207a4bb 100644 --- a/docs/glossary.rst +++ b/docs/glossary.rst @@ -287,22 +287,21 @@ Glossary principal A *principal* is a string or unicode object representing an - entity, typically a user or group, having zero or more - :term:`permissions `. Principals are provided by an + entity, typically a user or group. Principals are 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" + :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". + principals: `"bob"`, `"group foo"` and `"group bar"`. userid - A *userid* is a a string or unicode object used to identify and - authenticate a real-world user, often a person. A userid is + 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 `. The default behavior of the authentication policies :app:`Pyramid` provides is to - return the user's userid as one of the user's principals, but a - userid need not be a principal. + 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 diff --git a/docs/narr/security.rst b/docs/narr/security.rst index 203962751..2dc0c76af 100644 --- a/docs/narr/security.rst +++ b/docs/narr/security.rst @@ -7,14 +7,14 @@ Security ======== :app:`Pyramid` provides an optional, declarative, security system. -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. +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 @@ -30,12 +30,6 @@ allowed. Here's how it works at a high level: - A :term:`request` is generated when a user visits the application. -- If an :term:`authorization policy` is in effect the application uses - the request and it's :term:`root factory` to create a :ref:`resource tree - ` of :term:`contexts `. The resource - tree maps contexts to URLs and within the contexts the application - puts declarations which authorize access. - - Based on the request, a :term:`context` resource is located through :term:`resource location`. A context is located differently depending on whether the application uses :term:`traversal` or :term:`URL dispatch`, but @@ -46,9 +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. Based on the request and the remembered (or lack of) - :term:`userid` and related credentials 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 @@ -64,7 +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. - 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 @@ -119,9 +112,10 @@ 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:`userid` and matches that userid's principals against the -principals present in any :term:`ACL` found in the resource tree when -attempting to call some :term:`view`. +to a single :term:`userid` and matches that userid's +:term:`principals ` against the principals present in any +:term:`ACL` found in the resource tree when attempting to call some +:term:`view`. While it is possible to mix and match different authentication and authorization policies, it is an error to configure a Pyramid application @@ -616,7 +610,9 @@ that implements the following interface: 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``.""" + should return ``None``. + + """ def unauthenticated_userid(self, request): """ Return the *unauthenticated* userid. This method @@ -624,24 +620,37 @@ that implements the following interface: 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.""" + 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 - typically including the userid and any groups belonged to - by the current user, always including 'system' groups such + 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``. """ + ``pyramid.security.Authenticated``. + + """ def remember(self, request, userid, **kw): """ Return a set of headers suitable for 'remembering' the - userid named ``userid`` when set in a response. An + :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. """ - + 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 diff --git a/pyramid/interfaces.py b/pyramid/interfaces.py index bba818c8a..e03704b7f 100644 --- a/pyramid/interfaces.py +++ b/pyramid/interfaces.py @@ -437,38 +437,57 @@ class IViewMapperFactory(Interface): invocation signatures and response values. """ -class IAuthenticationPolicy(Interface): +class IAuthenticationPolicy(object): """ 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, userid, **kw): """ Return a set of headers suitable for 'remembering' the - userid named ``userid`` 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. """ -- cgit v1.2.3 From a9fb5255cedeb02e5c4210b90fe9942c9dbc781a Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Mon, 10 Nov 2014 01:33:22 -0600 Subject: re-add missing Interface parent --- pyramid/interfaces.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyramid/interfaces.py b/pyramid/interfaces.py index e03704b7f..2b56262c0 100644 --- a/pyramid/interfaces.py +++ b/pyramid/interfaces.py @@ -437,7 +437,7 @@ class IViewMapperFactory(Interface): invocation signatures and response values. """ -class IAuthenticationPolicy(object): +class IAuthenticationPolicy(Interface): """ An object representing a Pyramid authentication policy. """ def authenticated_userid(request): -- cgit v1.2.3 From 7a2b72c2ba018d6b75ee151843e37da67bbfc2bb Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Mon, 10 Nov 2014 01:34:38 -0600 Subject: update the public api for remember --- docs/api/security.rst | 2 +- pyramid/security.py | 21 ++++++++++++++++++++- pyramid/tests/test_security.py | 17 +++++++++++++++-- 3 files changed, 36 insertions(+), 4 deletions(-) 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/pyramid/security.py b/pyramid/security.py index 3cef7ee5a..cbb4b895f 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,7 +117,7 @@ deprecated( '"effective_principals" attribute of the Pyramid request instead.' ) -def remember(request, userid, **kw): +def remember(request, userid=_marker, **kw): """ Returns a sequence of header tuples (e.g. ``[('Set-Cookie', 'foo=abc')]``) on this request's response. @@ -138,7 +140,24 @@ def remember(request, userid, **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 [] diff --git a/pyramid/tests/test_security.py b/pyramid/tests/test_security.py index 027f9cda0..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() -- cgit v1.2.3 From 3ffd40c295a1e37ec94b6123fff8fb4dd5f5abf5 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Mon, 10 Nov 2014 01:39:53 -0600 Subject: update changelog --- CHANGES.txt | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CHANGES.txt b/CHANGES.txt index c1b729b3f..8083113ef 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -55,6 +55,13 @@ Bug Fixes add another callback to the list. See https://github.com/Pylons/pyramid/pull/1373 +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 ---- @@ -65,6 +72,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 --------- -- cgit v1.2.3 From f3a5679992c51ed3067bb6f5b577dad9fe4274ff Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Tue, 11 Nov 2014 01:34:37 -0600 Subject: enable PYTHONSTARTUP support in pshell Fixes #1299 --- CHANGES.txt | 4 ++++ pyramid/scripts/pshell.py | 10 ++++++++++ pyramid/tests/test_scripts/pystartup.py | 1 + pyramid/tests/test_scripts/test_pshell.py | 28 ++++++++++++++++++++++++++++ 4 files changed, 43 insertions(+) create mode 100644 pyramid/tests/test_scripts/pystartup.py diff --git a/CHANGES.txt b/CHANGES.txt index cf2cced51..4bd438bd7 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -24,6 +24,10 @@ Features ``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/1299 + Bug Fixes --------- diff --git a/pyramid/scripts/pshell.py b/pyramid/scripts/pshell.py index 12b078677..ef462239b 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,10 +53,12 @@ class PShellCommand(object): loaded_objects = {} object_help = {} setup = None + pystartup = None def __init__(self, argv, quiet=False): self.quiet = quiet self.options, self.args = self.parser.parse_args(argv[1:]) + self.pystartup = os.environ.get('PYTHONSTARTUP') def pshell_file_config(self, filename): config = self.ConfigParser() @@ -144,6 +148,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/tests/test_scripts/pystartup.py b/pyramid/tests/test_scripts/pystartup.py new file mode 100644 index 000000000..c4e5bcc80 --- /dev/null +++ b/pyramid/tests/test_scripts/pystartup.py @@ -0,0 +1 @@ +foo = 1 diff --git a/pyramid/tests/test_scripts/test_pshell.py b/pyramid/tests/test_scripts/test_pshell.py index 7cb130c41..86858a709 100644 --- a/pyramid/tests/test_scripts/test_pshell.py +++ b/pyramid/tests/test_scripts/test_pshell.py @@ -369,6 +369,34 @@ class TestPShellCommand(unittest.TestCase): self.assertTrue(self.bootstrap.closer.called) self.assertTrue(shell.help) + def test_command_loads_pythonstartup(self): + import os + marker = object() + old_pystartup = os.environ.get('PYTHONSTARTUP', marker) + os.environ['PYTHONSTARTUP'] = ( + os.path.abspath( + os.path.join( + os.path.dirname(__file__), + 'pystartup.py'))) + try: + command = self._makeOne() + 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) + finally: # pragma: no cover + if old_pystartup is not marker: + os.environ['PYTHONSTARTUP'] = old_pystartup + else: + del os.environ['PYTHONSTARTUP'] class Test_main(unittest.TestCase): def _callFUT(self, argv): -- cgit v1.2.3 From 940a7a3e3a254ba3b5db333f2a07ab43f5018d98 Mon Sep 17 00:00:00 2001 From: Randall Leeds Date: Thu, 10 Jul 2014 16:29:29 -0700 Subject: add failing test for package root spec static view --- pyramid/tests/test_config/test_views.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pyramid/tests/test_config/test_views.py b/pyramid/tests/test_config/test_views.py index 39b8ba70d..a82f7f257 100644 --- a/pyramid/tests/test_config/test_views.py +++ b/pyramid/tests/test_config/test_views.py @@ -3898,6 +3898,13 @@ class TestStaticURLInfo(unittest.TestCase): ('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)] + self._assertRegistrations(config, expected) + def test_add_url_withendslash(self): inst = self._makeOne() config = self._makeConfig() -- cgit v1.2.3 From e7745ac72ff5c5c499722a8cfcc589a77201fc9a Mon Sep 17 00:00:00 2001 From: Randall Leeds Date: Thu, 10 Jul 2014 16:31:31 -0700 Subject: Fix static views with package root spec patterns --- CHANGES.txt | 3 +++ pyramid/config/views.py | 2 +- pyramid/tests/test_config/test_views.py | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index b5d08c8ff..5a0edc566 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -67,6 +67,9 @@ Bug Fixes - 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 + Docs ---- diff --git a/pyramid/config/views.py b/pyramid/config/views.py index e4171b0c5..ba3981388 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -1955,7 +1955,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 diff --git a/pyramid/tests/test_config/test_views.py b/pyramid/tests/test_config/test_views.py index a82f7f257..b0d03fb72 100644 --- a/pyramid/tests/test_config/test_views.py +++ b/pyramid/tests/test_config/test_views.py @@ -3902,7 +3902,7 @@ class TestStaticURLInfo(unittest.TestCase): inst = self._makeOne() config = self._makeConfig() inst.add(config, 'http://example.com', 'package:') - expected = [('http://example.com/', 'package:', None)] + expected = [('http://example.com/', 'package:', None, None)] self._assertRegistrations(config, expected) def test_add_url_withendslash(self): -- cgit v1.2.3 From 0b0ea0a6fff1d238bcc419c7a4feb72ad4969175 Mon Sep 17 00:00:00 2001 From: Randall Leeds Date: Tue, 11 Nov 2014 00:42:43 -0800 Subject: Add myself to contributors --- CONTRIBUTORS.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index c77d3e92c..66f029cb7 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -232,3 +232,5 @@ Contributors - Amit Mane, 2014/01/23 - Fenton Travers, 2014/05/06 + +- Randall Leeds, 2014/11/11 -- cgit v1.2.3 From e51dd09eb1ba4c873f7dec763a1e51c5779801b7 Mon Sep 17 00:00:00 2001 From: John Anderson Date: Tue, 11 Nov 2014 08:17:02 -0800 Subject: Format proutes output and include module instead of repr of view --- pyramid/scripts/proutes.py | 66 ++++++++++++++++++++++++++---- pyramid/tests/test_scripts/test_proutes.py | 10 +++-- 2 files changed, 65 insertions(+), 11 deletions(-) diff --git a/pyramid/scripts/proutes.py b/pyramid/scripts/proutes.py index 5784026bb..792030a74 100644 --- a/pyramid/scripts/proutes.py +++ b/pyramid/scripts/proutes.py @@ -4,11 +4,17 @@ import textwrap from pyramid.paster import bootstrap from pyramid.scripts.common import parse_vars +from pyramid.config.views import MultiView + + +PAD = 3 + def main(argv=sys.argv, quiet=False): command = PRoutesCommand(argv, quiet) return command.run() + class PRoutesCommand(object): description = """\ Print all URL dispatch routes used by a Pyramid application in the @@ -43,7 +49,7 @@ class PRoutesCommand(object): 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') @@ -59,13 +65,22 @@ class PRoutesCommand(object): registry = env['registry'] mapper = self._get_mapper(registry) if mapper is not None: + mapped_routes = [('Name', 'Pattern', 'View')] + + max_name = len('Name') + max_pattern = len('Pattern') + max_view = len('View') + 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'))) + + mapped_routes.append(( + '-' * max_name, + '-' * max_pattern, + '-' * max_view, + )) + for route in routes: pattern = route.pattern if not pattern.startswith('/'): @@ -73,13 +88,50 @@ class PRoutesCommand(object): 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, '')) + view_callable = '' else: view_callable = registry.adapters.lookup( (IViewClassifier, request_iface, Interface), IView, name='', default=None) - self.out(fmt % (route.name, pattern, view_callable)) + + if view_callable is not None: + if isinstance(view_callable, MultiView): + view_callables = [ + x[1] for x in view_callable.views + ] + else: + view_callables = [view_callable] + + for view_func in view_callables: + view_callable = '%s.%s' % ( + view_func.__module__, + view_func.__name__, + ) + else: + view_callable = str(None) + + if len(route.name) > max_name: + max_name = len(route.name) + + if len(pattern) > max_pattern: + max_pattern = len(pattern) + + if len(view_callable) > max_view: + max_view = len(view_callable) + + mapped_routes.append((route.name, pattern, view_callable)) + + fmt = '%-{0}s %-{1}s %-{2}s'.format( + max_name + PAD, + max_pattern + PAD, + max_view + PAD, + ) + + for route_data in mapped_routes: + self.out(fmt % route_data) + return 0 if __name__ == '__main__': # pragma: no cover diff --git a/pyramid/tests/test_scripts/test_proutes.py b/pyramid/tests/test_scripts/test_proutes.py index 25a3cd2e3..45ab57d3a 100644 --- a/pyramid/tests/test_scripts/test_proutes.py +++ b/pyramid/tests/test_scripts/test_proutes.py @@ -123,8 +123,11 @@ 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', ' Date: Tue, 11 Nov 2014 12:01:10 -0600 Subject: adjust tests to work even when someone has defined PYTHONSTARTUP in their shell --- CHANGES.txt | 2 +- pyramid/scripts/pshell.py | 3 +-- pyramid/tests/test_scripts/test_pshell.py | 41 ++++++++++++++----------------- 3 files changed, 20 insertions(+), 26 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 4bd438bd7..f72a793a5 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -26,7 +26,7 @@ Features - 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/1299 + See https://github.com/Pylons/pyramid/pull/1448 Bug Fixes --------- diff --git a/pyramid/scripts/pshell.py b/pyramid/scripts/pshell.py index ef462239b..1168ba78a 100644 --- a/pyramid/scripts/pshell.py +++ b/pyramid/scripts/pshell.py @@ -53,12 +53,11 @@ class PShellCommand(object): loaded_objects = {} object_help = {} setup = None - pystartup = None + pystartup = os.environ.get('PYTHONSTARTUP') def __init__(self, argv, quiet=False): self.quiet = quiet self.options, self.args = self.parser.parse_args(argv[1:]) - self.pystartup = os.environ.get('PYTHONSTARTUP') def pshell_file_config(self, filename): config = self.ConfigParser() diff --git a/pyramid/tests/test_scripts/test_pshell.py b/pyramid/tests/test_scripts/test_pshell.py index 86858a709..a6ba2eaea 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): @@ -370,33 +374,24 @@ class TestPShellCommand(unittest.TestCase): self.assertTrue(shell.help) def test_command_loads_pythonstartup(self): - import os - marker = object() - old_pystartup = os.environ.get('PYTHONSTARTUP', marker) - os.environ['PYTHONSTARTUP'] = ( + command = self._makeOne() + command.pystartup = ( os.path.abspath( os.path.join( os.path.dirname(__file__), 'pystartup.py'))) - try: - command = self._makeOne() - 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) - finally: # pragma: no cover - if old_pystartup is not marker: - os.environ['PYTHONSTARTUP'] = old_pystartup - else: - del os.environ['PYTHONSTARTUP'] + 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): -- cgit v1.2.3 From 0a50d16f44885ec3aee3044981ebb5c6081c3657 Mon Sep 17 00:00:00 2001 From: Christophe de Vienne Date: Mon, 4 Aug 2014 15:08:29 +0200 Subject: Remove duplicate code --- pyramid/config/views.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/pyramid/config/views.py b/pyramid/config/views.py index e4171b0c5..db67d2582 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -1186,10 +1186,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, -- cgit v1.2.3 From c617b7df97a326ca010ddb196978169e2a178c4a Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Thu, 13 Nov 2014 15:47:21 -0600 Subject: update changelog for fixes from #1453 --- CHANGES.txt | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index b5d08c8ff..76f9bc84e 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -12,8 +12,8 @@ Features 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 natural for a user to define things relative - to their own packages. + 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 @@ -30,6 +30,9 @@ Features ``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 + Bug Fixes --------- -- cgit v1.2.3 From 716a20fc79c98e250c90a3d3e9f2218bec181a8d Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Sun, 16 Nov 2014 23:11:15 -0600 Subject: use hmac.compare_digest if available --- CHANGES.txt | 5 +++++ pyramid/tests/test_util.py | 43 +++++++++++++++++++++++++++++++++++++++++++ pyramid/util.py | 32 ++++++++++++++++++++++++-------- 3 files changed, 72 insertions(+), 8 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index a893ebae4..bbaa6739e 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -33,6 +33,11 @@ Features - 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 + Bug Fixes --------- diff --git a/pyramid/tests/test_util.py b/pyramid/tests/test_util.py index 2ca4c4a66..a18fa8d16 100644 --- a/pyramid/tests/test_util.py +++ b/pyramid/tests/test_util.py @@ -217,6 +217,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 diff --git a/pyramid/util.py b/pyramid/util.py index 6b92f17fc..6de53d559 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 @@ -227,7 +232,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 +242,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): -- cgit v1.2.3 From 8d535290147bc943045e1feaee9326cf3b996bed Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Sun, 16 Nov 2014 23:31:11 -0600 Subject: attempt to clarify the benefit of these changes --- CHANGES.txt | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/CHANGES.txt b/CHANGES.txt index 7d79ddd18..2e2707f3f 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -8,7 +8,17 @@ Features argument to ``pyramid.config.Configurator.add_static_view``: ``cachebust``. - Assets can now be overidden by an absolute path on the filesystem when using - the ``config.override_asset`` API. + 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/1229 Bug Fixes -- cgit v1.2.3 From e0c09c151ffb9bce0fdc71fb351745e3c282bb18 Mon Sep 17 00:00:00 2001 From: John Anderson Date: Sun, 16 Nov 2014 22:15:15 -0800 Subject: Make sure tox fails the cover build if it isn't at 100% --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 2bf213ca4..9a9c5a983 100644 --- a/tox.ini +++ b/tox.ini @@ -12,7 +12,7 @@ basepython = python2.6 commands = python setup.py dev - python setup.py nosetests --with-xunit --with-xcoverage + python setup.py nosetests --with-xunit --with-xcoverage --cover-min-percentage=100 deps = nosexcover -- cgit v1.2.3 From 36046388d5cbe99b8d972853efba03b2fb5aa203 Mon Sep 17 00:00:00 2001 From: John Anderson Date: Sun, 16 Nov 2014 22:58:22 -0800 Subject: Added coverage for MultiView and long names in proutes --- pyramid/scripts/proutes.py | 6 ++- pyramid/tests/test_scripts/test_proutes.py | 83 ++++++++++++++++++++++++++++++ 2 files changed, 87 insertions(+), 2 deletions(-) diff --git a/pyramid/scripts/proutes.py b/pyramid/scripts/proutes.py index 792030a74..d0c1aa13e 100644 --- a/pyramid/scripts/proutes.py +++ b/pyramid/scripts/proutes.py @@ -4,7 +4,6 @@ import textwrap from pyramid.paster import bootstrap from pyramid.scripts.common import parse_vars -from pyramid.config.views import MultiView PAD = 3 @@ -58,6 +57,8 @@ class PRoutesCommand(object): from pyramid.interfaces import IRouteRequest from pyramid.interfaces import IViewClassifier from pyramid.interfaces import IView + from pyramid.interfaces import IMultiView + from zope.interface import Interface config_uri = self.args[0] @@ -72,6 +73,7 @@ class PRoutesCommand(object): max_view = len('View') routes = mapper.get_routes() + if not routes: return 0 @@ -97,7 +99,7 @@ class PRoutesCommand(object): IView, name='', default=None) if view_callable is not None: - if isinstance(view_callable, MultiView): + if IMultiView.providedBy(view_callable): view_callables = [ x[1] for x in view_callable.views ] diff --git a/pyramid/tests/test_scripts/test_proutes.py b/pyramid/tests/test_scripts/test_proutes.py index 45ab57d3a..32202af4b 100644 --- a/pyramid/tests/test_scripts/test_proutes.py +++ b/pyramid/tests/test_scripts/test_proutes.py @@ -128,6 +128,48 @@ class TestPRoutesCommand(unittest.TestCase): ['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.registry import Registry + from pyramid.interfaces import IRouteRequest + from pyramid.interfaces import IViewClassifier + from pyramid.interfaces import IView + registry = Registry() + 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 @@ -157,6 +199,47 @@ class TestPRoutesCommand(unittest.TestCase): self.assertEqual(len(L), 3) self.assertEqual(L[-1].split()[:3], ['a', '/a', '']) + def test_single_route_multiview_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 IMultiView + + registry = Registry() + + 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] + self.assertEqual( + compare_to, + ['a', '/a', 'pyramid.tests.test_scripts.test_proutes.view'] + ) + def test__get_mapper(self): from pyramid.registry import Registry from pyramid.urldispatch import RoutesMapper -- cgit v1.2.3 From 6bdda352153a277fb2812746dce5522f441a49f2 Mon Sep 17 00:00:00 2001 From: John Anderson Date: Sun, 16 Nov 2014 23:08:25 -0800 Subject: Switch to using tox for travis so coverage is ran --- .travis.yml | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/.travis.yml b/.travis.yml index 4ca998c42..2d54a2b36 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,18 +1,20 @@ # Wire up travis language: python -python: - - 2.6 - - 2.7 - - pypy - - 3.2 - - 3.3 - - 3.4 - - pypy3 +env: + - TOX_ENV=py26 + - TOX_ENV=py27 + - TOX_ENV=py32 + - TOX_ENV=py33 + - TOX_ENV=py34 + - TOX_ENV=pypy + - TOX_ENV=cover -install: python setup.py dev +install: + - pip install tox -script: python setup.py test -q +script: + - tox -e $TOX_ENV notifications: email: -- cgit v1.2.3 From b542a4d723e5e994f693884618878186b94fa51c Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Mon, 17 Nov 2014 01:10:11 -0600 Subject: improve the docs for absolute path overrides --- docs/narr/assets.rst | 65 ++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 56 insertions(+), 9 deletions(-) diff --git a/docs/narr/assets.rst b/docs/narr/assets.rst index 74708ff3e..fc908c2b4 100644 --- a/docs/narr/assets.rst +++ b/docs/narr/assets.rst @@ -276,15 +276,62 @@ 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 ` +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 -- cgit v1.2.3 From d965c4fa42aa04888e5a829d9975ffec26037c9b Mon Sep 17 00:00:00 2001 From: John Anderson Date: Sun, 16 Nov 2014 23:21:37 -0800 Subject: Use travis_retry in case of timeouts, remove -e $OTX_ENV --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 2d54a2b36..dddeb1df7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,10 +11,10 @@ env: - TOX_ENV=cover install: - - pip install tox + - travis_retry pip install tox script: - - tox -e $TOX_ENV + - travis_retry tox notifications: email: -- cgit v1.2.3 From 650d3d5fa383d89a3b28029162d6ef4d58be3da1 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Mon, 17 Nov 2014 01:25:58 -0600 Subject: reference appropriate PR --- CHANGES.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.txt b/CHANGES.txt index 2e2707f3f..06852b885 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -19,7 +19,7 @@ Features 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/1229 + See https://github.com/Pylons/pyramid/issues/1252 Bug Fixes --------- -- cgit v1.2.3 From 6beffc41634844f3ea3b6152f292d3dbe6b5500c Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Mon, 17 Nov 2014 01:29:25 -0600 Subject: note the deprecation in the todo --- TODO.txt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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 ------------------ -- cgit v1.2.3 From 1d298deae192918a994423c3fc4ee9cd4bf7e7ca Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Mon, 17 Nov 2014 01:50:09 -0600 Subject: fix travis.yml to use the correct TOXENV var --- .travis.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.travis.yml b/.travis.yml index dddeb1df7..4ff4939d9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,13 +2,13 @@ language: python env: - - TOX_ENV=py26 - - TOX_ENV=py27 - - TOX_ENV=py32 - - TOX_ENV=py33 - - TOX_ENV=py34 - - TOX_ENV=pypy - - TOX_ENV=cover + - TOXENV=py26 + - TOXENV=py27 + - TOXENV=py32 + - TOXENV=py33 + - TOXENV=py34 + - TOXENV=pypy + - TOXENV=cover install: - travis_retry pip install tox -- cgit v1.2.3 From d10b97733618b92e1b6cfe7cbb8802f90b58dcde Mon Sep 17 00:00:00 2001 From: John Anderson Date: Mon, 17 Nov 2014 00:37:58 -0800 Subject: Add RTD and Travis badges in the README --- README.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.rst b/README.rst index a3458028b..73709319c 100644 --- a/README.rst +++ b/README.rst @@ -1,6 +1,13 @@ 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=latest + :target: https://readthedocs.org/projects/pyramid/?badge=latest + :alt: 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. -- cgit v1.2.3 From 39a18291c45fa7b7591dec42ba22a5ad1957a86f Mon Sep 17 00:00:00 2001 From: John Anderson Date: Mon, 17 Nov 2014 00:39:25 -0800 Subject: Use direct link to docs --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 73709319c..c6f174adf 100644 --- a/README.rst +++ b/README.rst @@ -5,7 +5,7 @@ Pyramid :target: https://travis-ci.org/Pylons/pyramid .. image:: https://readthedocs.org/projects/pyramid/badge/?version=latest - :target: https://readthedocs.org/projects/pyramid/?badge=latest + :target: http://pyramid.readthedocs.org/en/latest/ :alt: Documentation Status Pyramid is a small, fast, down-to-earth, open source Python web framework. -- cgit v1.2.3 From 0760606f7b6af51a4431e336f46541485387efd5 Mon Sep 17 00:00:00 2001 From: Tres Seaver Date: Mon, 17 Nov 2014 08:36:59 -0500 Subject: Don't fail coverage check on systems w/ locale set. See: http://jenkins.pylonsproject.org/job/pyramid/1617/console. --- pyramid/tests/test_integration.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 -- cgit v1.2.3 From 5f75af8e760559dc321836f6f7bec4c147b1de42 Mon Sep 17 00:00:00 2001 From: John Anderson Date: Mon, 17 Nov 2014 06:58:34 -0800 Subject: Use pylonsproject.org link for docs, point to master branch --- README.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index c6f174adf..adf7eea5e 100644 --- a/README.rst +++ b/README.rst @@ -4,8 +4,8 @@ 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=latest - :target: http://pyramid.readthedocs.org/en/latest/ +.. image:: https://readthedocs.org/projects/pyramid/badge/?version=master + :target: http://docs.pylonsproject.org/projects/pyramid/en/master/ :alt: Documentation Status Pyramid is a small, fast, down-to-earth, open source Python web framework. -- cgit v1.2.3 From bbbcfb35a55685b3b90dcfdfb32853b81639d126 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Mon, 17 Nov 2014 10:50:54 -0800 Subject: - add step for badges in README.rst --- RELEASING.txt | 3 +++ 1 file changed, 3 insertions(+) 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 -- cgit v1.2.3 From c62f7dc452f262ed747e0038d3962e8a198ee1d5 Mon Sep 17 00:00:00 2001 From: Matt Russell Date: Wed, 19 Nov 2014 19:32:25 +0000 Subject: Fixes breaking docs build due to unquoted asterisks in doc string. --- pyramid/interfaces.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyramid/interfaces.py b/pyramid/interfaces.py index 2b56262c0..b21c6b9cc 100644 --- a/pyramid/interfaces.py +++ b/pyramid/interfaces.py @@ -479,7 +479,7 @@ class IAuthenticationPolicy(Interface): """ Return a set of headers suitable for 'remembering' the :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. + decide on the composition and meaning of ``**kw``. """ -- cgit v1.2.3 From 39c7e4fec6265355b3eefb77a5dfbf9d6fc26ded Mon Sep 17 00:00:00 2001 From: Matt Russell Date: Wed, 19 Nov 2014 23:20:01 +0000 Subject: Correct url in git remote command. --- HACKING.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/HACKING.txt b/HACKING.txt index e3afbf241..16c17699c 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 -- cgit v1.2.3 From bc8e4d2acf583b9c581202c0f0efee3ee1da710a Mon Sep 17 00:00:00 2001 From: Tres Seaver Date: Fri, 21 Nov 2014 14:26:10 -0500 Subject: Add support for testing 'pypy3' under Tox / Travis. --- .travis.yml | 1 + CHANGES.txt | 2 ++ tox.ini | 2 +- 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 4ff4939d9..5a205b268 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,6 +8,7 @@ env: - TOXENV=py33 - TOXENV=py34 - TOXENV=pypy + - TOXENV=pypy3 - TOXENV=cover install: diff --git a/CHANGES.txt b/CHANGES.txt index c7c829fb6..ea3323aa0 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -4,6 +4,8 @@ Next release Features -------- +- Added support / testing for 'pypy3' under Tox and Travis. + - Cache busting for static resources has been added and is available via a new argument to ``pyramid.config.Configurator.add_static_view``: ``cachebust``. See https://github.com/Pylons/pyramid/pull/1380 diff --git a/tox.ini b/tox.ini index 9a9c5a983..3f32dbc3f 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] envlist = - py26,py27,py32,py33,py34,pypy,cover + py26,py27,py32,py33,py34,pypy,pypy3,cover [testenv] commands = -- cgit v1.2.3 From 782eb470cf4b31c2cab75f3cc14a5f9c42eeb9f0 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Fri, 21 Nov 2014 19:03:44 -0600 Subject: update changelog for #1469 --- CHANGES.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGES.txt b/CHANGES.txt index ea3323aa0..46c331268 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -5,6 +5,7 @@ Features -------- - Added support / testing for 'pypy3' under Tox and Travis. + See https://github.com/Pylons/pyramid/pull/1469 - Cache busting for static resources has been added and is available via a new argument to ``pyramid.config.Configurator.add_static_view``: ``cachebust``. -- cgit v1.2.3 From 370862eb748f74dacee6b2bb1a5a2e35f865018a Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Sat, 22 Nov 2014 23:31:50 -0800 Subject: add request processing diagram to docs/narr/router.rst --- docs/_static/pyramid_router.svg | 3 +++ docs/narr/router.rst | 3 +++ 2 files changed, 6 insertions(+) create mode 100644 docs/_static/pyramid_router.svg diff --git a/docs/_static/pyramid_router.svg b/docs/_static/pyramid_router.svg new file mode 100644 index 000000000..21bbcb532 --- /dev/null +++ b/docs/_static/pyramid_router.svg @@ -0,0 +1,3 @@ + + +2014-11-23 07:19ZRequest Processingno exceptionsmiddleware ingress tween ingresstraversalContextFoundtween egressresponse callbacksfinished callbacksmiddleware egressBeforeRenderRequest ProcessingLegendeventcallbackviewexternal process (middleware, tween)internal processview pipelinepredicatesview lookuproute predicatesURL dispatchNewRequestNewResponseview mapper ingressviewview mapper egressresponse adapterdecorators ingressdecorators egressauthorization diff --git a/docs/narr/router.rst b/docs/narr/router.rst index ac3deefdc..745c2faa1 100644 --- a/docs/narr/router.rst +++ b/docs/narr/router.rst @@ -9,6 +9,9 @@ Request Processing ================== +.. image:: ../_static/pyramid_router.svg + :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 -- cgit v1.2.3 From 39846565d0d98a6a9ef2ef34faad3eb620b3e9fb Mon Sep 17 00:00:00 2001 From: Hugo Branquinho Date: Tue, 25 Nov 2014 16:31:04 +0000 Subject: Shortcut for package name on registry --- pyramid/registry.py | 5 +++++ pyramid/tests/test_registry.py | 5 +++++ 2 files changed, 10 insertions(+) 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/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) -- cgit v1.2.3 From d89c5f76b3032a1447f19dc87a7a6ceb7508c3cb Mon Sep 17 00:00:00 2001 From: Hugo Branquinho Date: Tue, 25 Nov 2014 19:38:24 +0000 Subject: Documentation added --- CONTRIBUTORS.txt | 2 ++ docs/api/registry.rst | 12 ++++++++++++ 2 files changed, 14 insertions(+) diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index 66f029cb7..9c2191f3b 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -234,3 +234,5 @@ Contributors - Fenton Travers, 2014/05/06 - Randall Leeds, 2014/11/11 + +- Hugo Branquinho, 2014/11/25 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 -- cgit v1.2.3 From 138706a24bd8e7051c60942c2789d8c16b4ca2ed Mon Sep 17 00:00:00 2001 From: Matt Russell Date: Wed, 19 Nov 2014 23:06:17 +0000 Subject: Include code examples for integration and functional tests in docs #1001 Wrap lines as per convention. --- docs/narr/MyProject/myproject/tests.py | 37 +++++++++ docs/narr/MyProject/setup.py | 45 +++++++---- docs/narr/testing.rst | 142 +++++++++++++++------------------ 3 files changed, 131 insertions(+), 93 deletions(-) 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/testing.rst b/docs/narr/testing.rst index e001ad81c..3620f5e11 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 more information about :app:`Pyramid`'s ``includme`` function. - 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. -- cgit v1.2.3 From 9c94e129f1bbb753317deba7ea5f790db13e0709 Mon Sep 17 00:00:00 2001 From: Matt Russell Date: Tue, 25 Nov 2014 20:59:18 +0000 Subject: Tweak seealso for the includeme function. --- docs/narr/testing.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/narr/testing.rst b/docs/narr/testing.rst index 3620f5e11..ecda57489 100644 --- a/docs/narr/testing.rst +++ b/docs/narr/testing.rst @@ -351,7 +351,7 @@ code's integration with the rest of :app:`Pyramid`. .. seealso:: - See more information about :app:`Pyramid`'s ``includme`` function. + See also :ref:`including_configuration` Let's demonstrate this by showing an integration test for a view. -- cgit v1.2.3 From bc56b88f9d306e510044dc9fede0d36d8e88d8eb Mon Sep 17 00:00:00 2001 From: Tres Seaver Date: Wed, 26 Nov 2014 15:05:50 -0500 Subject: Unused import. --- pyramid/authentication.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pyramid/authentication.py b/pyramid/authentication.py index f4c211ffa..aefa180b5 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 -- cgit v1.2.3 From ea58f249f89bb9f48b926baf1e4fcc04832db672 Mon Sep 17 00:00:00 2001 From: Tres Seaver Date: Wed, 26 Nov 2014 15:06:02 -0500 Subject: 79 columns. --- pyramid/authentication.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyramid/authentication.py b/pyramid/authentication.py index aefa180b5..09c8a2d3a 100644 --- a/pyramid/authentication.py +++ b/pyramid/authentication.py @@ -928,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) -- cgit v1.2.3 From ec5226745f8f5161f89636e036e2b8efed216b74 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Thu, 27 Nov 2014 10:49:33 -0600 Subject: fix issue in auth_tkt parsing with the cookie type being unicode In webob the cookies are always unicode but the auth_tkt tests were expecting them to be a native string. This didn't manifest itself until we started using the ``hmac.compare_digest`` which fails if the types are not the same. Fixes #1477 --- pyramid/authentication.py | 2 +- pyramid/tests/test_authentication.py | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/pyramid/authentication.py b/pyramid/authentication.py index 09c8a2d3a..e0e241e52 100644 --- a/pyramid/authentication.py +++ b/pyramid/authentication.py @@ -740,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: diff --git a/pyramid/tests/test_authentication.py b/pyramid/tests/test_authentication.py index e25e9faa1..920a7e65d 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 = u'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 = u'7d947cdef99bad55f8e3382a8bd089bb9dd0547f7925b7d189adc1160ca'\ + 'b0ec0e6888faa41eba641a18522b26f19109f3ffafb769767ba8a26d02aa'\ + 'eae56599a0000000auserid!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 = u'b3e7156db4f8abde4439c4a6499a0668f9e7ffd7fa27b798400ecdade8d'\ + '76c530000000auserid!' result = self._callFUT('secret', ticket, '198.51.100.1', 'sha256') self.assertEqual(result, (10, 'userid', [''], '')) def test_ipv6(self): - ticket = 'd025b601a0f12ca6d008aa35ff3a22b7d8f3d1c1456c85becf8760cd7a2f'\ - 'a4910000000auserid!' + ticket = u'd025b601a0f12ca6d008aa35ff3a22b7d8f3d1c1456c85becf8760cd7a2'\ + 'fa4910000000auserid!' result = self._callFUT('secret', ticket, '2001:db8::1', 'sha256') self.assertEqual(result, (10, 'userid', [''], '')) pass -- cgit v1.2.3 From eb2bc87f663d2e056c57a9bdbb233cf574a5a1a9 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Sat, 29 Nov 2014 11:45:46 -0600 Subject: fix build on py3.2 missing u-prefix introduced in ec5226745f8f5161f89636e036e2b8efed216b74 --- pyramid/tests/test_authentication.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/pyramid/tests/test_authentication.py b/pyramid/tests/test_authentication.py index 920a7e65d..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 = u'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 = u'7d947cdef99bad55f8e3382a8bd089bb9dd0547f7925b7d189adc1160ca'\ - 'b0ec0e6888faa41eba641a18522b26f19109f3ffafb769767ba8a26d02aa'\ - 'eae56599a0000000auserid!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 = u'b3e7156db4f8abde4439c4a6499a0668f9e7ffd7fa27b798400ecdade8d'\ - '76c530000000auserid!' + ticket = text_('b3e7156db4f8abde4439c4a6499a0668f9e7ffd7fa27b798400ecd' + 'ade8d76c530000000auserid!') result = self._callFUT('secret', ticket, '198.51.100.1', 'sha256') self.assertEqual(result, (10, 'userid', [''], '')) def test_ipv6(self): - ticket = u'd025b601a0f12ca6d008aa35ff3a22b7d8f3d1c1456c85becf8760cd7a2'\ - 'fa4910000000auserid!' + ticket = text_('d025b601a0f12ca6d008aa35ff3a22b7d8f3d1c1456c85becf8760' + 'cd7a2fa4910000000auserid!') result = self._callFUT('secret', ticket, '2001:db8::1', 'sha256') self.assertEqual(result, (10, 'userid', [''], '')) pass -- cgit v1.2.3 From 3408269bd291b771efef8e54f039038fc5b59a26 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Mon, 1 Dec 2014 13:40:39 -0800 Subject: - rename pyramid_router.svg to pyramid_request_processing.svg to be consistent with its content - add source files for future modifications --- docs/_static/pyramid_request_processing.graffle | 9748 +++++++++++++++++++++++ docs/_static/pyramid_request_processing.png | Bin 0 -> 122854 bytes docs/_static/pyramid_request_processing.svg | 3 + docs/_static/pyramid_router.svg | 3 - docs/narr/router.rst | 2 +- 5 files changed, 9752 insertions(+), 4 deletions(-) create mode 100644 docs/_static/pyramid_request_processing.graffle create mode 100644 docs/_static/pyramid_request_processing.png create mode 100644 docs/_static/pyramid_request_processing.svg delete mode 100644 docs/_static/pyramid_router.svg 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 @@ + + + + + ActiveLayerIndex + 0 + ApplicationVersion + + com.omnigroup.OmniGrafflePro + 139.18.0.187838 + + AutoAdjust + + BackgroundGraphic + + Bounds + {{0, 0}, {576, 733}} + Class + SolidGraphic + FontInfo + + Font + Helvetica + Size + 12 + + ID + 2 + Style + + shadow + + Draws + NO + + stroke + + Draws + NO + + + + BaseZoom + 0 + CanvasOrigin + {0, 0} + ColumnAlign + 1 + ColumnSpacing + 36 + CreationDate + 2014-11-18 08:33:33 +0000 + Creator + Steve Piercy + DisplayScale + 1 0/72 in = 1 0/72 in + GraphDocumentVersion + 8 + GraphicsList + + + Class + LineGraphic + FontInfo + + Color + + w + 0 + + Font + Helvetica + Size + 12 + + Head + + ID + 169389 + + ID + 169504 + Layer + 0 + Points + + {344.41667175292969, 402.88506673894034} + {375.5, 402.27232108797347} + + Style + + stroke + + Color + + b + 0.0980392 + g + 0.0980392 + r + 0.0980392 + + HeadArrow + 0 + Legacy + + Pattern + 2 + TailArrow + 0 + + + Tail + + ID + 169428 + + + + Class + LineGraphic + FontInfo + + Color + + w + 0 + + Font + Helvetica + Size + 12 + + Head + + ID + 169382 + + ID + 169433 + Layer + 0 + Points + + {155.00000254313238, 459.27667544230695} + {238.5002713470962, 456.52468399152298} + + Style + + stroke + + Color + + b + 0.0980392 + g + 0.0980392 + r + 0.0980392 + + HeadArrow + 0 + Legacy + + Pattern + 2 + TailArrow + 0 + + + Tail + + ID + 169370 + Position + 0.28820157051086426 + + + + Class + LineGraphic + FontInfo + + Color + + w + 0 + + Font + Helvetica + Size + 12 + + Head + + ID + 169383 + + ID + 169432 + Layer + 0 + Points + + {155.00000254313238, 482.12574895537085} + {238.52297468463752, 508.35839132916635} + + Style + + stroke + + Color + + b + 0.0980392 + g + 0.0980392 + r + 0.0980392 + + HeadArrow + 0 + Legacy + + Pattern + 2 + TailArrow + 0 + + + Tail + + ID + 169370 + Position + 0.5668826699256897 + + + + Class + Group + Graphics + + + Bounds + {{238.8333613077798, 284.99999999999994}, {105.66668701171875, 18.656048080136394}} + Class + ShapedGraphic + ID + 169425 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + {0.50000000000000089, -0.49999999999999645} + {-0.49526813868737474, -0.4689979626999552} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.637876 + g + 1 + r + 1 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\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} + VerticalPad + 0 + + + + Bounds + {{238.75000762939453, 412.15071036499205}, {105.66666412353516, 18.656048080136394}} + Class + ShapedGraphic + ID + 169426 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + {0.50000000000000089, 0.5} + {-0.49999999999999911, 0.49999999999999289} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.637876 + g + 1 + r + 1 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\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} + VerticalPad + 0 + + + + Bounds + {{238.75000762939453, 303.65604172230951}, {105.66666412353516, 18.656048080136394}} + Class + ShapedGraphic + ID + 169427 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + {0.50000000000000089, -0.49999999999999645} + {-0.49526813868737474, -0.4689979626999552} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.637876 + g + 1 + r + 1 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\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} + VerticalPad + 0 + + + + Bounds + {{238.75000762939453, 393.55704269887212}, {105.66666412353516, 18.656048080136394}} + Class + ShapedGraphic + ID + 169428 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.637876 + g + 1 + r + 1 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\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} + VerticalPad + 0 + + + + Bounds + {{238.75000762939453, 374.90099016834085}, {105.66666412353516, 18.656048080136394}} + Class + ShapedGraphic + ID + 169429 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.637876 + g + 1 + r + 1 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\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} + VerticalPad + 0 + + + + Bounds + {{238.75000762939453, 341.36561209044055}, {105.66666412353516, 33.089282989501953}} + Class + ShapedGraphic + ID + 169430 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.422927 + g + 1 + r + 1 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\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} + VerticalPad + 0 + + + + Bounds + {{238.75000762939453, 322.26348241170439}, {105.66666412353516, 18.656048080136394}} + Class + ShapedGraphic + ID + 169431 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.637876 + g + 1 + r + 1 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\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} + VerticalPad + 0 + + + + ID + 169424 + Layer + 0 + + + Class + LineGraphic + FontInfo + + Color + + w + 0 + + Font + Helvetica + Size + 12 + + Head + + ID + 169422 + Info + 4 + + ID + 169423 + Layer + 0 + Points + + {155.00000254313238, 470.25295298442387} + {238.33861159880226, 482.4262543949045} + + Style + + stroke + + Color + + b + 0.0980392 + g + 0.0980392 + r + 0.0980392 + + HeadArrow + 0 + Legacy + + Pattern + 2 + TailArrow + 0 + + + Tail + + ID + 169370 + Position + 0.42701038718223572 + + + + Bounds + {{238.83336130777977, 471.22620192028251}, {105.66666412353516, 22.544641494750977}} + Class + ShapedGraphic + ID + 169422 + Layer + 0 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.999449 + g + 0.743511 + r + 0.872276 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\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} + VerticalPad + 0 + + + + Class + LineGraphic + FontInfo + + Color + + w + 0 + + Font + Helvetica + Size + 12 + + Head + + ID + 169420 + Info + 4 + + ID + 169421 + Layer + 0 + Points + + {154.99998733539806, 128.68025330008533} + {239.83340199788393, 128.59152244387357} + + Style + + stroke + + Color + + b + 0.0980392 + g + 0.0980392 + r + 0.0980392 + + HeadArrow + 0 + Legacy + + Pattern + 2 + TailArrow + 0 + + + Tail + + ID + 169386 + Position + 0.35945424437522888 + + + + Bounds + {{239.83340199788395, 117.31920169649808}, {105.66666412353516, 22.544641494750977}} + Class + ShapedGraphic + ID + 169420 + Layer + 0 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.999449 + g + 0.743511 + r + 0.872276 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\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} + VerticalPad + 0 + + + + Class + TableGroup + Graphics + + + Bounds + {{102.1666056315114, 148.28868579864499}, {105.66669464111328, 33.08929443359375}} + Class + ShapedGraphic + ID + 169418 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.815377 + g + 1 + r + 0.820561 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\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} + VerticalPad + 0 + + + + Bounds + {{102.1666056315114, 181.37798023223874}, {105.66669464111328, 17.244049072265625}} + Class + ShapedGraphic + ID + 169419 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.815377 + g + 1 + r + 0.820561 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\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} + VerticalPad + 0 + + + + GridH + + 169418 + 169419 + + + ID + 169417 + Layer + 0 + + + Class + TableGroup + Graphics + + + Bounds + {{102.16666158040482, 272}, {105.66666412353516, 33.08929443359375}} + Class + ShapedGraphic + ID + 169412 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.815377 + g + 1 + r + 0.820561 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\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} + VerticalPad + 0 + + + + Bounds + {{102.16666158040482, 305.08929443359375}, {105.66666412353516, 17.244049072265625}} + Class + ShapedGraphic + ID + 169413 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.815377 + g + 1 + r + 0.820561 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\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} + VerticalPad + 0 + + + + GridH + + 169412 + 169413 + + + ID + 169411 + Layer + 0 + + + Class + LineGraphic + Head + + ID + 169407 + Info + 7 + + ID + 169410 + Layer + 0 + Points + + {238.75000762939462, 430.80675844512831} + {207.66666666666765, 385.656005859375} + + Style + + stroke + + Color + + b + 0.755269 + g + 0.755239 + r + 0.75529 + + HeadArrow + 0 + Legacy + + Pattern + 11 + TailArrow + 0 + + + Tail + + ID + 169426 + Info + 6 + + + + Class + LineGraphic + Head + + ID + 169407 + Info + 8 + + ID + 169409 + Layer + 0 + Points + + {239.33336141608385, 285.57837549845181} + {207.66666666666777, 353.07514659563753} + + Style + + stroke + + Color + + b + 0.755269 + g + 0.755239 + r + 0.75529 + + HeadArrow + 0 + Legacy + + Pattern + 11 + TailArrow + 0 + + + Tail + + ID + 169425 + Info + 6 + + + + Class + LineGraphic + ControlPoints + + {0, 6.9840087890625} + {0, -8.9999999999999432} + + FontInfo + + Color + + w + 0 + + Font + Helvetica + Size + 12 + + Head + + ID + 169381 + + ID + 169408 + Layer + 0 + Points + + {155.00000254313238, 386.66442959065108} + {155.00000254313238, 422.21209462483216} + + Style + + stroke + + Bezier + + Color + + b + 0.0980392 + g + 0.0980392 + r + 0.0980392 + + HeadArrow + SharpArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 169407 + + + + Bounds + {{102.16667048136482, 353.07514659563753}, {105.66666412353516, 33.089282989501953}} + Class + ShapedGraphic + ID + 169407 + Layer + 0 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + {-0.49211360058019871, -0.49251945318722434} + {-0.49211360058019871, 0.49470854679786669} + {0.4984227008620481, 0.48463479169597612} + {0.49842270086204898, -0.5} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.422927 + g + 1 + r + 1 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\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} + VerticalPad + 0 + + + + Class + LineGraphic + FontInfo + + Color + + w + 0 + + Font + Helvetica + Size + 12 + + Head + + ID + 169380 + Info + 4 + + ID + 169399 + Layer + 0 + Points + + {154.9999936421724, 258.44082431579938} + {238.8333613077798, 258.45536063967575} + + Style + + stroke + + Color + + b + 0.0980392 + g + 0.0980392 + r + 0.0980392 + + HeadArrow + 0 + Legacy + + Pattern + 2 + TailArrow + 0 + + + Tail + + ID + 169372 + Position + 0.51973581314086914 + + + + Class + Group + Graphics + + + Bounds + {{383.66662216186666, 130.51770718892479}, {105.66666412353516, 22.544641494750977}} + Class + ShapedGraphic + ID + 169393 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.815377 + g + 1 + r + 0.820561 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\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} + VerticalPad + 0 + + + + Bounds + {{383.66662216186666, 91.940789540609359}, {105.66666412353516, 33.089282989501953}} + Class + ShapedGraphic + ID + 169394 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.999208 + g + 0.811343 + r + 0.644457 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\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)} + VerticalPad + 0 + + + + Bounds + {{383.66662216186666, 158.54998334248924}, {105.66666412353516, 22.544641494750977}} + Class + ShapedGraphic + ID + 169395 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.422927 + g + 1 + r + 1 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\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} + VerticalPad + 0 + + + + Bounds + {{383.66662216186666, 186.58225949605369}, {105.66666412353516, 22.544641494750977}} + Class + ShapedGraphic + ID + 169396 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.327428 + g + 0.81823 + r + 0.995566 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\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} + VerticalPad + 0 + + + + Bounds + {{383.66662216186666, 63.908513387045019}, {105.66666412353516, 22.544641494750977}} + Class + ShapedGraphic + ID + 169397 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.999449 + g + 0.743511 + r + 0.872276 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\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} + VerticalPad + 0 + + + + Bounds + {{370.9999504089372, 42.910746256511771}, {132.66667175292969, 184.08924865722656}} + Class + ShapedGraphic + ID + 169398 + Magnets + + {1, 0.5} + {1, -0.5} + {-1, 0.5} + {-1, -0.5} + {0.5, 1} + {-0.5, 1} + {0.5, -1} + {-0.5, -1} + + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + CornerRadius + 5 + + + Text + + Align + 0 + Text + {\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} + VerticalPad + 0 + + TextPlacement + 0 + + + ID + 169391 + Layer + 0 + + + Bounds + {{233.5000012715667, 20.000000000000934}, {116, 14}} + Class + ShapedGraphic + FitText + YES + Flow + Resize + FontInfo + + Font + Helvetica + Size + 12 + + ID + 169390 + Layer + 0 + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0 + Text + {\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 <%Canvas%>} + VerticalPad + 0 + + Wrap + NO + + + Bounds + {{375.5, 391}, {105.66666412353516, 22.544642175946908}} + Class + ShapedGraphic + ID + 169389 + Layer + 0 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.999449 + g + 0.743511 + r + 0.872276 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\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} + VerticalPad + 0 + + + + Class + LineGraphic + ControlPoints + + {0, 7.05596923828125} + {0, -9} + + FontInfo + + Color + + w + 0 + + Font + Helvetica + Size + 12 + + Head + + ID + 169418 + Info + 2 + + ID + 169386 + Layer + 0 + Points + + {155.00000170434049, 119.22767858295661} + {154.99995295206804, 148.28868579864499} + + Style + + stroke + + Bezier + + Color + + b + 0.0980392 + g + 0.0980392 + r + 0.0980392 + + HeadArrow + SharpArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 169378 + + + + Class + LineGraphic + ControlPoints + + {0, 6.9840087890625} + {0, -9} + + FontInfo + + Color + + w + 0 + + Font + Helvetica + Size + 12 + + Head + + ID + 169378 + + ID + 169385 + Layer + 0 + Points + + {155.00000254313238, 67.727678571434836} + {155.00000254313238, 96.18303707668386} + + Style + + stroke + + Bezier + + Color + + b + 0.0980392 + g + 0.0980392 + r + 0.0980392 + + HeadArrow + SharpArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 169377 + Info + 1 + + + + Bounds + {{102.16667048136482, 509.6179466247504}, {105.66666412353516, 22.544641494750977}} + Class + ShapedGraphic + ID + 169384 + Layer + 0 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.999208 + g + 0.811343 + r + 0.644457 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\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} + VerticalPad + 0 + + + + Bounds + {{239, 497.23589324949899}, {105.66666412353516, 22.544641494750977}} + Class + ShapedGraphic + ID + 169383 + Layer + 0 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.327428 + g + 0.81823 + r + 0.995566 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\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} + VerticalPad + 0 + + + + Bounds + {{239, 445.23589324949717}, {105.66666412353516, 22.544641494750977}} + Class + ShapedGraphic + ID + 169382 + Layer + 0 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.327428 + g + 0.81823 + r + 0.995566 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\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} + VerticalPad + 0 + + + + Bounds + {{102.16667048136482, 422.21209462483216}, {105.66666412353516, 22.544641494750977}} + Class + ShapedGraphic + ID + 169381 + Layer + 0 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.999208 + g + 0.811343 + r + 0.644457 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\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} + VerticalPad + 0 + + + + Bounds + {{238.83336130777977, 247.18303989230026}, {105.66666412353516, 22.544641494750977}} + Class + ShapedGraphic + ID + 169380 + Layer + 0 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.999449 + g + 0.743511 + r + 0.872276 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\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} + VerticalPad + 0 + + + + Bounds + {{102.16667048136482, 222.18303707668389}, {105.66666412353516, 22.544641494750977}} + Class + ShapedGraphic + ID + 169379 + Layer + 0 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.815377 + g + 1 + r + 0.820561 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\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} + VerticalPad + 0 + + + + Bounds + {{102.16667048136482, 96.18303707668386}, {105.66666412353516, 22.544641494750977}} + Class + ShapedGraphic + ID + 169378 + Layer + 0 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.999208 + g + 0.811343 + r + 0.644457 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\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} + VerticalPad + 0 + + + + Bounds + {{102.16667048136482, 45.18303707668386}, {105.66666412353516, 22.544641494750977}} + Class + ShapedGraphic + ID + 169377 + Layer + 0 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.999208 + g + 0.811343 + r + 0.644457 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\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 } + VerticalPad + 0 + + + + Class + LineGraphic + ControlPoints + + {0, 6.9840087890625} + {0, -9} + + FontInfo + + Color + + w + 0 + + Font + Helvetica + Size + 12 + + Head + + ID + 169379 + + ID + 169373 + Layer + 0 + Points + + {154.99995295206804, 198.62202930450437} + {155.00000254313238, 222.18303707668389} + + Style + + stroke + + Bezier + + Color + + b + 0.0980392 + g + 0.0980392 + r + 0.0980392 + + HeadArrow + SharpArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 169419 + Info + 1 + + + + Class + LineGraphic + ControlPoints + + {0, 7.05596923828125} + {0, -9} + + FontInfo + + Color + + w + 0 + + Font + Helvetica + Size + 12 + + Head + + ID + 169412 + Info + 2 + + ID + 169372 + Layer + 0 + Points + + {154.9999936421724, 245.22767856643924} + {154.9999936421724, 272} + + Style + + stroke + + Bezier + + Color + + b + 0.0980392 + g + 0.0980392 + r + 0.0980392 + + HeadArrow + SharpArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 169379 + + + + Class + LineGraphic + ControlPoints + + {0, 6.9840087890625} + {0, -8.9999999999999432} + + FontInfo + + Color + + w + 0 + + Font + Helvetica + Size + 12 + + Head + + ID + 169407 + + ID + 169371 + Layer + 0 + Points + + {154.9999936421724, 322.33334350585938} + {155.00000254313238, 353.07514659563753} + + Style + + stroke + + Bezier + + Color + + b + 0.0980392 + g + 0.0980392 + r + 0.0980392 + + HeadArrow + SharpArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 169413 + Info + 1 + + + + Class + LineGraphic + ControlPoints + + {0, 6.9839935302734375} + {0, -9} + + FontInfo + + Color + + w + 0 + + Font + Helvetica + Size + 12 + + Head + + ID + 169384 + Info + 2 + + ID + 169370 + Layer + 0 + Points + + {155.00000254313238, 444.75673611958314} + {155.00000254313238, 509.6179466247504} + + Style + + stroke + + Bezier + + Color + + b + 0.0980392 + g + 0.0980392 + r + 0.0980392 + + HeadArrow + SharpArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 169381 + + + + Class + LineGraphic + Head + + ID + 169444 + Info + 6 + + ID + 169503 + Layer + 1 + Points + + {272.4166717529298, 537.32234122436705} + {420.4999504089364, 515.08928491955714} + + Style + + stroke + + Color + + b + 0.755269 + g + 0.755239 + r + 0.75529 + + HeadArrow + 0 + Legacy + + Pattern + 11 + TailArrow + 0 + + + Tail + + ID + 169494 + Info + 5 + + + + Class + LineGraphic + Head + + ID + 169444 + + ID + 169502 + Layer + 1 + Points + + {272.50004831949906, 391.51558277923863} + {420.4999504089364, 472.78869058972316} + + Style + + stroke + + Color + + b + 0.755269 + g + 0.755239 + r + 0.75529 + + HeadArrow + 0 + Legacy + + Pattern + 11 + TailArrow + 0 + + + Tail + + ID + 169493 + Info + 5 + + + + Class + LineGraphic + FontInfo + + Color + + w + 0 + + Font + Helvetica + Size + 12 + + Head + + ID + 169450 + + ID + 169501 + Layer + 1 + Points + + {83.000002543132396, 592.81693102013151} + {239, 583.78422005970799} + + Style + + stroke + + Color + + b + 0.0980392 + g + 0.0980392 + r + 0.0980392 + + HeadArrow + 0 + Legacy + + Pattern + 2 + TailArrow + 0 + + + Tail + + ID + 169438 + Position + 0.28820157051086426 + + + + Class + LineGraphic + FontInfo + + Color + + w + 0 + + Font + Helvetica + Size + 12 + + Head + + ID + 169451 + + ID + 169500 + Layer + 1 + Points + + {83.000002543132396, 629.80996162681686} + {239, 640.78422005970981} + + Style + + stroke + + Color + + b + 0.0980392 + g + 0.0980392 + r + 0.0980392 + + HeadArrow + 0 + Legacy + + Pattern + 2 + TailArrow + 0 + + + Tail + + ID + 169438 + Position + 0.5668826699256897 + + + + Class + Group + Graphics + + + Bounds + {{166.8333613077798, 391.51558277923863}, {105.66668701171875, 18.656048080136394}} + Class + ShapedGraphic + ID + 169493 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + {0.50000000000000089, -0.49999999999999645} + {-0.49526813868737474, -0.4689979626999552} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.637876 + g + 1 + r + 1 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\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} + VerticalPad + 0 + + + + Bounds + {{166.75000762939453, 518.66629314423074}, {105.66666412353516, 18.656048080136394}} + Class + ShapedGraphic + ID + 169494 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + {0.50000000000000089, 0.5} + {-0.49999999999999911, 0.49999999999999289} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.637876 + g + 1 + r + 1 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\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} + VerticalPad + 0 + + + + Bounds + {{166.75000762939453, 410.17162450154819}, {105.66666412353516, 18.656048080136394}} + Class + ShapedGraphic + ID + 169495 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + {0.50000000000000089, -0.49999999999999645} + {-0.49526813868737474, -0.4689979626999552} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.637876 + g + 1 + r + 1 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\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} + VerticalPad + 0 + + + + Bounds + {{166.75000762939453, 500.07262547811081}, {105.66666412353516, 18.656048080136394}} + Class + ShapedGraphic + ID + 169496 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.637876 + g + 1 + r + 1 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\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} + VerticalPad + 0 + + + + Bounds + {{166.75000762939453, 481.41657294757954}, {105.66666412353516, 18.656048080136394}} + Class + ShapedGraphic + ID + 169497 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.637876 + g + 1 + r + 1 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\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} + VerticalPad + 0 + + + + Bounds + {{166.75000762939453, 447.88119486967923}, {105.66666412353516, 33.089282989501953}} + Class + ShapedGraphic + ID + 169498 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.422927 + g + 1 + r + 1 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\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} + VerticalPad + 0 + + + + Bounds + {{166.75000762939453, 428.77906519094307}, {105.66666412353516, 18.656048080136394}} + Class + ShapedGraphic + ID + 169499 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.637876 + g + 1 + r + 1 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\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} + VerticalPad + 0 + + + + ID + 169492 + Layer + 1 + + + Class + LineGraphic + FontInfo + + Color + + w + 0 + + Font + Helvetica + Size + 12 + + Head + + ID + 169490 + Info + 4 + + ID + 169491 + Layer + 1 + Points + + {83.166643778483959, 611.77452873049333} + {238.8333613077798, 611.77452873049333} + + Style + + stroke + + Color + + b + 0.0980392 + g + 0.0980392 + r + 0.0980392 + + HeadArrow + 0 + Legacy + + Pattern + 2 + TailArrow + 0 + + + + + Bounds + {{238.83336130777977, 600.50220798311784}, {105.66666412353516, 22.544641494750977}} + Class + ShapedGraphic + ID + 169490 + Layer + 1 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.999449 + g + 0.743511 + r + 0.872276 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\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} + VerticalPad + 0 + + + + Class + LineGraphic + FontInfo + + Color + + w + 0 + + Font + Helvetica + Size + 12 + + Head + + ID + 169488 + Info + 4 + + ID + 169489 + Layer + 1 + Points + + {82.999986314263907, 140.3328574622312} + {239.83340199788393, 141.59152244387357} + + Style + + stroke + + Color + + b + 0.0980392 + g + 0.0980392 + r + 0.0980392 + + HeadArrow + 0 + Legacy + + Pattern + 2 + TailArrow + 0 + + + Tail + + ID + 169454 + Position + 0.35945424437522888 + + + + Bounds + {{239.83340199788395, 130.31920169649808}, {105.66666412353516, 22.544641494750977}} + Class + ShapedGraphic + ID + 169488 + Layer + 1 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.999449 + g + 0.743511 + r + 0.872276 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\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} + VerticalPad + 0 + + + + Class + TableGroup + Graphics + + + Bounds + {{30.166605631511416, 166.28868579864499}, {105.66668701171875, 33.08929443359375}} + Class + ShapedGraphic + ID + 169486 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.815377 + g + 1 + r + 0.820561 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\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} + VerticalPad + 0 + + + + Bounds + {{30.166605631511416, 199.37798023223874}, {105.66668701171875, 17.244049072265625}} + Class + ShapedGraphic + ID + 169487 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.815377 + g + 1 + r + 0.820561 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\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} + VerticalPad + 0 + + + + GridH + + 169486 + 169487 + + + ID + 169485 + Layer + 1 + + + Class + TableGroup + Graphics + + + Bounds + {{420.5000406901047, 338.15028762817326}, {105.66668701171875, 33.08929443359375}} + Class + ShapedGraphic + ID + 169483 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.815377 + g + 1 + r + 0.820561 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\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} + VerticalPad + 0 + + + + Bounds + {{420.5000406901047, 371.23958206176701}, {105.66668701171875, 17.244049072265625}} + Class + ShapedGraphic + ID + 169484 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.815377 + g + 1 + r + 0.820561 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\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} + VerticalPad + 0 + + + + GridH + + 169483 + 169484 + + + ID + 169482 + Layer + 1 + + + Class + TableGroup + Graphics + + + Bounds + {{30.166661580404835, 335}, {105.66667175292969, 33.08929443359375}} + Class + ShapedGraphic + ID + 169480 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.815377 + g + 1 + r + 0.820561 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\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} + VerticalPad + 0 + + + + Bounds + {{30.166661580404835, 368.08929443359375}, {105.66667175292969, 17.244049072265625}} + Class + ShapedGraphic + ID + 169481 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.815377 + g + 1 + r + 0.820561 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\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} + VerticalPad + 0 + + + + GridH + + 169480 + 169481 + + + ID + 169479 + Layer + 1 + + + Class + LineGraphic + Head + + ID + 169475 + Info + 7 + + ID + 169478 + Layer + 1 + Points + + {166.75000762939462, 537.32234122436694} + {135.66666666666765, 485} + + Style + + stroke + + Color + + b + 0.755269 + g + 0.755239 + r + 0.75529 + + HeadArrow + 0 + Legacy + + Pattern + 11 + TailArrow + 0 + + + Tail + + ID + 169494 + Info + 6 + + + + Class + LineGraphic + Head + + ID + 169475 + Info + 8 + + ID + 169477 + Layer + 1 + Points + + {167.33336141608385, 392.09395827769049} + {135.66666666666777, 452.41914073626253} + + Style + + stroke + + Color + + b + 0.755269 + g + 0.755239 + r + 0.75529 + + HeadArrow + 0 + Legacy + + Pattern + 11 + TailArrow + 0 + + + Tail + + ID + 169493 + Info + 6 + + + + Class + LineGraphic + ControlPoints + + {0, 6.9840087890625} + {0, -8.9999999999999432} + + FontInfo + + Color + + w + 0 + + Font + Helvetica + Size + 12 + + Head + + ID + 169449 + + ID + 169476 + Layer + 1 + Points + + {83.000002543132396, 485.50842372576449} + {83.000002543132396, 548.10604731241608} + + Style + + stroke + + Bezier + + Color + + b + 0.0980392 + g + 0.0980392 + r + 0.0980392 + + HeadArrow + SharpArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 169475 + + + + Bounds + {{30.166670481364818, 452.41914073626253}, {105.66666412353516, 33.089282989501953}} + Class + ShapedGraphic + ID + 169475 + Layer + 1 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + {-0.49211360058019871, -0.49251945318722434} + {-0.49211360058019871, 0.49470854679786669} + {0.4984227008620481, 0.48463479169597612} + {0.49842270086204898, -0.5} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.422927 + g + 1 + r + 1 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\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} + VerticalPad + 0 + + + + Class + LineGraphic + ControlPoints + + {51.333333333333314, 0} + {-0.66666666666662877, 58.666666666666686} + {0.66673293066804717, -58.666850540458825} + {-16.306719354194399, 0.26652623861849634} + + Head + + ID + 169443 + Info + 4 + + ID + 169474 + Layer + 1 + Points + + {369.66666666666669, 541} + {404.00000000000023, 362} + {420.36749776329049, 302.42112495959606} + + Style + + stroke + + Bezier + + Color + + b + 0.75663 + g + 0.756618 + r + 0.75664 + + HeadArrow + 0 + Legacy + + LineType + 1 + Pattern + 1 + TailArrow + 0 + + + + + Class + LineGraphic + ControlPoints + + {69.833333333332462, -0.72767857143483639} + {-0.66690523835279691, -51.044605218028948} + {0.66666666666674246, 51.044637362162291} + {-24.333271383961971, -0.13425428344572765} + + Head + + ID + 169443 + Info + 4 + + ID + 169473 + Layer + 1 + Points + + {310.66666666666754, 118.72767857143484} + {399.33333333333417, 216.62202930450439} + {420.37955338188368, 301.45369961823752} + + Style + + stroke + + Bezier + + Color + + b + 0.75663 + g + 0.756618 + r + 0.75664 + + HeadArrow + 0 + Legacy + + LineType + 1 + Pattern + 1 + TailArrow + 0 + + + + + Class + LineGraphic + ControlPoints + + {-3.9999491373696401, 78.910715080442856} + {92.666683130060392, 0.22547126950667007} + + Head + + ID + 169490 + Info + 3 + + ID + 169472 + Layer + 1 + Points + + {473.33328247070392, 515.08928491955714} + {344.50002543131501, 611.77452873049333} + + Style + + stroke + + Bezier + + HeadArrow + SharpArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 169444 + Info + 1 + + + + Class + LineGraphic + ControlPoints + + {31.999987284342428, -14.081351280212308} + {-32.166667938232536, 10.244050343831077} + + ID + 169471 + Layer + 1 + Points + + {344.96346869509995, 346.26317428240412} + {389.8333346048999, 328.08928298950207} + + Style + + stroke + + Bezier + + HeadArrow + SharpArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 169456 + Info + 3 + + + + Class + LineGraphic + ControlPoints + + {31.999987284342428, -14.081351280212308} + {-28.500001271565793, 8.3333333333333144} + + ID + 169470 + Layer + 1 + Points + + {344.98861594084059, 323.71068461220347} + {391.1666679382335, 313.6666666666664} + + Style + + stroke + + Bezier + + HeadArrow + SharpArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 169455 + + + + Class + LineGraphic + ControlPoints + + {31.999987284342428, -14.081351280212308} + {-40, 1.5446373167492311} + + ID + 169469 + Layer + 1 + Points + + {344.9995533451783, 301.1612218744512} + {394.50000127156665, 299.00000000000045} + + Style + + stroke + + Bezier + + HeadArrow + SharpArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 169442 + Info + 3 + + + + Class + LineGraphic + ControlPoints + + {8.5833282470703125, -10.244596987647753} + {0, 0} + + FontInfo + + Color + + w + 0 + + Font + Helvetica + Size + 12 + + Head + + ID + 169457 + Info + 4 + + ID + 169468 + Layer + 1 + Points + + {272.41667175292969, 509.40064951817902} + {285.5, 503.81699882234847} + + Style + + stroke + + Bezier + + Color + + b + 0.0980392 + g + 0.0980392 + r + 0.0980392 + + HeadArrow + 0 + Legacy + + LineType + 1 + Pattern + 1 + TailArrow + 0 + + + Tail + + ID + 169496 + + + + Class + LineGraphic + FontInfo + + Color + + w + 0 + + Font + Helvetica + Size + 12 + + Head + + ID + 169448 + Info + 4 + + ID + 169467 + Layer + 1 + Points + + {82.999997456869679, 300.27229985501288} + {238.34892458824362, 260.57913893040109} + + Style + + stroke + + Color + + b + 0.0980392 + g + 0.0980392 + r + 0.0980392 + + HeadArrow + 0 + Legacy + + Pattern + 2 + TailArrow + 0 + + + Tail + + ID + 169440 + Position + 0.51973581314086914 + + + + Class + Group + Graphics + + + Bounds + {{419.66662216186666, 214.61452811107179}, {105.66666412353516, 22.544642175946908}} + Class + ShapedGraphic + ID + 169460 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.756045 + g + 0.75004 + r + 0.994455 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\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} + VerticalPad + 0 + + + + Bounds + {{419.66662216186666, 130.51770718892479}, {105.66666412353516, 22.544641494750977}} + Class + ShapedGraphic + ID + 169461 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.815377 + g + 1 + r + 0.820561 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\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} + VerticalPad + 0 + + + + Bounds + {{419.66662216186666, 91.940789540609359}, {105.66666412353516, 33.089282989501953}} + Class + ShapedGraphic + ID + 169462 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.999208 + g + 0.811343 + r + 0.644457 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\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)} + VerticalPad + 0 + + + + Bounds + {{419.66662216186666, 158.54998334248924}, {105.66666412353516, 22.544641494750977}} + Class + ShapedGraphic + ID + 169463 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.422927 + g + 1 + r + 1 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\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} + VerticalPad + 0 + + + + Bounds + {{419.66662216186666, 186.58225949605369}, {105.66666412353516, 22.544641494750977}} + Class + ShapedGraphic + ID + 169464 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.327428 + g + 0.81823 + r + 0.995566 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\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} + VerticalPad + 0 + + + + Bounds + {{419.66662216186666, 63.908513387045019}, {105.66666412353516, 22.544641494750977}} + Class + ShapedGraphic + ID + 169465 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.999449 + g + 0.743511 + r + 0.872276 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\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} + VerticalPad + 0 + + + + Bounds + {{406.9999504089372, 42.910746256511771}, {132.66667175292969, 207.81692504882812}} + Class + ShapedGraphic + ID + 169466 + Magnets + + {1, 0.5} + {1, -0.5} + {-1, 0.5} + {-1, -0.5} + {0.5, 1} + {-0.5, 1} + {0.5, -1} + {-0.5, -1} + + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + CornerRadius + 5 + + + Text + + Align + 0 + Text + {\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} + VerticalPad + 0 + + TextPlacement + 0 + + + ID + 169459 + Layer + 1 + + + Bounds + {{233.5000012715667, 20.000000000000934}, {116, 14}} + Class + ShapedGraphic + FitText + YES + Flow + Resize + FontInfo + + Font + Helvetica + Size + 12 + + ID + 169458 + Layer + 1 + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0 + Text + {\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 <%Canvas%>} + VerticalPad + 0 + + Wrap + NO + + + Bounds + {{285.5, 492.544677734375}, {105.66666412353516, 22.544642175946908}} + Class + ShapedGraphic + ID + 169457 + Layer + 1 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.999449 + g + 0.743511 + r + 0.872276 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\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} + VerticalPad + 0 + + + + Bounds + {{238.83337529500417, 335.17855853126167}, {105.66666412353516, 22.544642175946908}} + Class + ShapedGraphic + ID + 169456 + Layer + 1 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\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} + VerticalPad + 0 + + + + Bounds + {{238.83337529500417, 312.54463200342093}, {105.66666412353516, 22.544642175946908}} + Class + ShapedGraphic + ID + 169455 + Layer + 1 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\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} + VerticalPad + 0 + + + + Class + LineGraphic + ControlPoints + + {0, 7.05596923828125} + {0, -9} + + FontInfo + + Color + + w + 0 + + Font + Helvetica + Size + 12 + + Head + + ID + 169486 + Info + 2 + + ID + 169454 + Layer + 1 + Points + + {83.000001850648417, 128.2276785555359} + {82.999949137370791, 166.28868579864502} + + Style + + stroke + + Bezier + + Color + + b + 0.0980392 + g + 0.0980392 + r + 0.0980392 + + HeadArrow + SharpArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 169446 + + + + Class + LineGraphic + ControlPoints + + {0, 6.9840087890625} + {0, -9} + + FontInfo + + Color + + w + 0 + + Font + Helvetica + Size + 12 + + Head + + ID + 169446 + + ID + 169453 + Layer + 1 + Points + + {83.000002543132396, 67.727678571434836} + {83.000002543132396, 105.18303707668386} + + Style + + stroke + + Bezier + + Color + + b + 0.0980392 + g + 0.0980392 + r + 0.0980392 + + HeadArrow + SharpArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 169445 + Info + 1 + + + + Bounds + {{30.166670481364818, 671.51189931233432}, {105.66666412353516, 22.544641494750977}} + Class + ShapedGraphic + ID + 169452 + Layer + 1 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.999208 + g + 0.811343 + r + 0.644457 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\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} + VerticalPad + 0 + + + + Bounds + {{239, 629.51189931233432}, {105.66666412353516, 22.544641494750977}} + Class + ShapedGraphic + ID + 169451 + Layer + 1 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.327428 + g + 0.81823 + r + 0.995566 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\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} + VerticalPad + 0 + + + + Bounds + {{239, 572.5118993123325}, {105.66666412353516, 22.544641494750977}} + Class + ShapedGraphic + ID + 169450 + Layer + 1 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.327428 + g + 0.81823 + r + 0.995566 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\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} + VerticalPad + 0 + + + + Bounds + {{30.166670481364818, 548.10604731241608}, {105.66666412353516, 22.544641494750977}} + Class + ShapedGraphic + ID + 169449 + Layer + 1 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.999208 + g + 0.811343 + r + 0.644457 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\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} + VerticalPad + 0 + + + + Bounds + {{238.83336130777977, 249.18303989230026}, {105.66666412353516, 22.544641494750977}} + Class + ShapedGraphic + ID + 169448 + Layer + 1 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.999449 + g + 0.743511 + r + 0.872276 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\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} + VerticalPad + 0 + + + + Bounds + {{30.166670481364818, 240.18303707668389}, {105.66666412353516, 22.544641494750977}} + Class + ShapedGraphic + ID + 169447 + Layer + 1 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.815377 + g + 1 + r + 0.820561 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\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} + VerticalPad + 0 + + + + Bounds + {{30.166670481364818, 105.18303707668386}, {105.66666412353516, 22.544641494750977}} + Class + ShapedGraphic + ID + 169446 + Layer + 1 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.999208 + g + 0.811343 + r + 0.644457 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\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} + VerticalPad + 0 + + + + Bounds + {{30.166670481364818, 45.18303707668386}, {105.66666412353516, 22.544641494750977}} + Class + ShapedGraphic + ID + 169445 + Layer + 1 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.999208 + g + 0.811343 + r + 0.644457 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\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 } + VerticalPad + 0 + + + + Bounds + {{420.49995040893634, 472.78869058972316}, {105.66666412353516, 42.300594329833984}} + Class + ShapedGraphic + ID + 169444 + Layer + 1 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + {-0.49999999999999956, -0.5} + {-0.49999999999999956, 0.5} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.422927 + g + 1 + r + 1 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\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} + VerticalPad + 0 + + + + Bounds + {{420.49995040893634, 290.66666666666691}, {105.66666412353516, 22.544642175946908}} + Class + ShapedGraphic + ID + 169443 + Layer + 1 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.756045 + g + 0.75004 + r + 0.994455 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\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} + VerticalPad + 0 + + + + Bounds + {{238.83336512247806, 289.91071033477789}, {105.66666412353516, 22.544642175946908}} + Class + ShapedGraphic + ID + 169442 + Layer + 1 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\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} + VerticalPad + 0 + + + + Class + LineGraphic + ControlPoints + + {0, 6.9840087890625} + {0, -9} + + FontInfo + + Color + + w + 0 + + Font + Helvetica + Size + 12 + + Head + + ID + 169447 + + ID + 169441 + Layer + 1 + Points + + {82.999949137370791, 216.62202930450437} + {83.000002543132396, 240.18303707668389} + + Style + + stroke + + Bezier + + Color + + b + 0.0980392 + g + 0.0980392 + r + 0.0980392 + + HeadArrow + SharpArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 169487 + Info + 1 + + + + Class + LineGraphic + ControlPoints + + {0, 7.05596923828125} + {0, -9} + + FontInfo + + Color + + w + 0 + + Font + Helvetica + Size + 12 + + Head + + ID + 169480 + Info + 2 + + ID + 169440 + Layer + 1 + Points + + {82.999997456869679, 263.22767855635425} + {82.999997456869679, 335} + + Style + + stroke + + Bezier + + Color + + b + 0.0980392 + g + 0.0980392 + r + 0.0980392 + + HeadArrow + SharpArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 169447 + + + + Class + LineGraphic + ControlPoints + + {0, 6.9840087890625} + {0, -8.9999999999999432} + + FontInfo + + Color + + w + 0 + + Font + Helvetica + Size + 12 + + Head + + ID + 169475 + + ID + 169439 + Layer + 1 + Points + + {82.999997456869679, 385.33334350585938} + {83.000002543132396, 452.41914073626253} + + Style + + stroke + + Bezier + + Color + + b + 0.0980392 + g + 0.0980392 + r + 0.0980392 + + HeadArrow + SharpArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 169481 + Info + 1 + + + + Class + LineGraphic + ControlPoints + + {0, 6.9839935302734375} + {0, -9} + + FontInfo + + Color + + w + 0 + + Font + Helvetica + Size + 12 + + Head + + ID + 169452 + Info + 2 + + ID + 169438 + Layer + 1 + Points + + {83.000002543132396, 571.15068879140836} + {83.000002543132396, 671.51189931233421} + + Style + + stroke + + Bezier + + Color + + b + 0.0980392 + g + 0.0980392 + r + 0.0980392 + + HeadArrow + SharpArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 169449 + + + + Class + LineGraphic + ControlPoints + + {0, 7.055999755859375} + {0, -9} + + FontInfo + + Color + + w + 0 + + Font + Helvetica + Size + 12 + + Head + + ID + 169483 + Info + 2 + + ID + 169437 + Layer + 1 + Points + + {473.33328247070392, 313.2113088426139} + {473.33338419596407, 338.15028762817326} + + Style + + stroke + + Bezier + + Color + + b + 0.0980392 + g + 0.0980392 + r + 0.0980392 + + HeadArrow + SharpArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 169443 + + + + Class + LineGraphic + ControlPoints + + {0, 6.9840011596679688} + {0, -9} + + FontInfo + + Color + + w + 0 + + Font + Helvetica + Size + 12 + + Head + + ID + 169444 + + ID + 169436 + Layer + 1 + Points + + {473.33338359264764, 388.98363112285512} + {473.33328247070392, 472.78869058972316} + + Style + + stroke + + Bezier + + Color + + b + 0.0980392 + g + 0.0980392 + r + 0.0980392 + + HeadArrow + SharpArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 169484 + + + + Class + LineGraphic + Head + + ID + 169358 + Info + 6 + + ID + 169359 + Layer + 2 + Points + + {272.4166717529298, 537.32234122436705} + {420.4999504089364, 515.08928491955714} + + Style + + stroke + + Color + + b + 0.755269 + g + 0.755239 + r + 0.75529 + + HeadArrow + 0 + Legacy + + Pattern + 11 + TailArrow + 0 + + + Tail + + ID + 169206 + Info + 5 + + + + Class + LineGraphic + Head + + ID + 169358 + + ID + 169360 + Layer + 2 + Points + + {272.50004831949906, 391.51558277923863} + {420.4999504089364, 472.78869058972316} + + Style + + stroke + + Color + + b + 0.755269 + g + 0.755239 + r + 0.75529 + + HeadArrow + 0 + Legacy + + Pattern + 11 + TailArrow + 0 + + + Tail + + ID + 169205 + Info + 5 + + + + Class + LineGraphic + FontInfo + + Color + + w + 0 + + Font + Helvetica + Size + 12 + + Head + + ID + 169044 + + ID + 169130 + Layer + 2 + Points + + {83.000002543132396, 592.81693102013151} + {239, 583.78422005970799} + + Style + + stroke + + Color + + b + 0.0980392 + g + 0.0980392 + r + 0.0980392 + + HeadArrow + 0 + Legacy + + Pattern + 2 + TailArrow + 0 + + + Tail + + ID + 169128 + Position + 0.28820157051086426 + + + + Class + LineGraphic + FontInfo + + Color + + w + 0 + + Font + Helvetica + Size + 12 + + Head + + ID + 169045 + + ID + 169129 + Layer + 2 + Points + + {83.000002543132396, 629.80996162681686} + {239, 640.78422005970981} + + Style + + stroke + + Color + + b + 0.0980392 + g + 0.0980392 + r + 0.0980392 + + HeadArrow + 0 + Legacy + + Pattern + 2 + TailArrow + 0 + + + Tail + + ID + 169128 + Position + 0.5668826699256897 + + + + Class + Group + Graphics + + + Bounds + {{166.8333613077798, 391.51558277923863}, {105.66668701171875, 18.656048080136394}} + Class + ShapedGraphic + ID + 169205 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + {0.50000000000000089, -0.49999999999999645} + {-0.49526813868737474, -0.4689979626999552} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.637876 + g + 1 + r + 1 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\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} + VerticalPad + 0 + + + + Bounds + {{166.75000762939453, 518.66629314423074}, {105.66666412353516, 18.656048080136394}} + Class + ShapedGraphic + ID + 169206 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + {0.50000000000000089, 0.5} + {-0.49999999999999911, 0.49999999999999289} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.637876 + g + 1 + r + 1 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\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} + VerticalPad + 0 + + + + Bounds + {{166.75000762939453, 410.17162450154819}, {105.66666412353516, 18.656048080136394}} + Class + ShapedGraphic + ID + 169207 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + {0.50000000000000089, -0.49999999999999645} + {-0.49526813868737474, -0.4689979626999552} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.637876 + g + 1 + r + 1 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\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} + VerticalPad + 0 + + + + Bounds + {{166.75000762939453, 500.07262547811081}, {105.66666412353516, 18.656048080136394}} + Class + ShapedGraphic + ID + 169208 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.637876 + g + 1 + r + 1 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\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} + VerticalPad + 0 + + + + Bounds + {{166.75000762939453, 481.41657294757954}, {105.66666412353516, 18.656048080136394}} + Class + ShapedGraphic + ID + 169209 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.637876 + g + 1 + r + 1 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\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} + VerticalPad + 0 + + + + Bounds + {{166.75000762939453, 447.88119486967923}, {105.66666412353516, 33.089282989501953}} + Class + ShapedGraphic + ID + 169210 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.422927 + g + 1 + r + 1 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\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} + VerticalPad + 0 + + + + Bounds + {{166.75000762939453, 428.77906519094307}, {105.66666412353516, 18.656048080136394}} + Class + ShapedGraphic + ID + 169211 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.637876 + g + 1 + r + 1 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\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} + VerticalPad + 0 + + + + ID + 169204 + Layer + 2 + + + Class + LineGraphic + FontInfo + + Color + + w + 0 + + Font + Helvetica + Size + 12 + + Head + + ID + 169085 + Info + 4 + + ID + 169086 + Layer + 2 + Points + + {83.166643778483959, 611.77452873049333} + {238.8333613077798, 611.77452873049333} + + Style + + stroke + + Color + + b + 0.0980392 + g + 0.0980392 + r + 0.0980392 + + HeadArrow + 0 + Legacy + + Pattern + 2 + TailArrow + 0 + + + + + Bounds + {{238.83336130777977, 600.50220798311784}, {105.66666412353516, 22.544641494750977}} + Class + ShapedGraphic + ID + 169085 + Layer + 2 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.999449 + g + 0.743511 + r + 0.872276 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\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} + VerticalPad + 0 + + + + Class + LineGraphic + FontInfo + + Color + + w + 0 + + Font + Helvetica + Size + 12 + + Head + + ID + 169083 + Info + 4 + + ID + 169084 + Layer + 2 + Points + + {82.999986314263907, 140.3328574622312} + {239.83340199788393, 141.59152244387357} + + Style + + stroke + + Color + + b + 0.0980392 + g + 0.0980392 + r + 0.0980392 + + HeadArrow + 0 + Legacy + + Pattern + 2 + TailArrow + 0 + + + Tail + + ID + 169048 + Position + 0.35945424437522888 + + + + Bounds + {{239.83340199788395, 130.31920169649808}, {105.66666412353516, 22.544641494750977}} + Class + ShapedGraphic + ID + 169083 + Layer + 2 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.999449 + g + 0.743511 + r + 0.872276 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\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} + VerticalPad + 0 + + + + Class + TableGroup + Graphics + + + Bounds + {{30.166605631511416, 166.28868579864499}, {105.66668701171875, 33.08929443359375}} + Class + ShapedGraphic + ID + 169081 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.815377 + g + 1 + r + 0.820561 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\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} + VerticalPad + 0 + + + + Bounds + {{30.166605631511416, 199.37798023223874}, {105.66668701171875, 17.244049072265625}} + Class + ShapedGraphic + ID + 169082 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.815377 + g + 1 + r + 0.820561 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\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} + VerticalPad + 0 + + + + GridH + + 169081 + 169082 + + + ID + 169080 + Layer + 2 + + + Class + TableGroup + Graphics + + + Bounds + {{420.5000406901047, 338.15028762817326}, {105.66668701171875, 33.08929443359375}} + Class + ShapedGraphic + ID + 169355 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.815377 + g + 1 + r + 0.820561 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\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} + VerticalPad + 0 + + + + Bounds + {{420.5000406901047, 371.23958206176701}, {105.66668701171875, 17.244049072265625}} + Class + ShapedGraphic + ID + 169356 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.815377 + g + 1 + r + 0.820561 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\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} + VerticalPad + 0 + + + + GridH + + 169355 + 169356 + + + ID + 169354 + Layer + 2 + + + Class + TableGroup + Graphics + + + Bounds + {{30.166661580404835, 335}, {105.66667175292969, 33.08929443359375}} + Class + ShapedGraphic + ID + 169075 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.815377 + g + 1 + r + 0.820561 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\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} + VerticalPad + 0 + + + + Bounds + {{30.166661580404835, 368.08929443359375}, {105.66667175292969, 17.244049072265625}} + Class + ShapedGraphic + ID + 169076 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.815377 + g + 1 + r + 0.820561 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\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} + VerticalPad + 0 + + + + GridH + + 169075 + 169076 + + + ID + 169074 + Layer + 2 + + + Class + LineGraphic + Head + + ID + 169070 + Info + 7 + + ID + 169073 + Layer + 2 + Points + + {166.75000762939462, 537.32234122436694} + {135.66666666666765, 485} + + Style + + stroke + + Color + + b + 0.755269 + g + 0.755239 + r + 0.75529 + + HeadArrow + 0 + Legacy + + Pattern + 11 + TailArrow + 0 + + + Tail + + ID + 169206 + Info + 6 + + + + Class + LineGraphic + Head + + ID + 169070 + Info + 8 + + ID + 169072 + Layer + 2 + Points + + {167.33336141608385, 392.09395827769049} + {135.66666666666777, 452.41914073626253} + + Style + + stroke + + Color + + b + 0.755269 + g + 0.755239 + r + 0.75529 + + HeadArrow + 0 + Legacy + + Pattern + 11 + TailArrow + 0 + + + Tail + + ID + 169205 + Info + 6 + + + + Class + LineGraphic + ControlPoints + + {0, 6.9840087890625} + {0, -8.9999999999999432} + + FontInfo + + Color + + w + 0 + + Font + Helvetica + Size + 12 + + Head + + ID + 169043 + + ID + 169071 + Layer + 2 + Points + + {83.000002543132396, 485.50842372576449} + {83.000002543132396, 548.10604731241608} + + Style + + stroke + + Bezier + + Color + + b + 0.0980392 + g + 0.0980392 + r + 0.0980392 + + HeadArrow + SharpArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 169070 + + + + Bounds + {{30.166670481364818, 452.41914073626253}, {105.66666412353516, 33.089282989501953}} + Class + ShapedGraphic + ID + 169070 + Layer + 2 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + {-0.49211360058019871, -0.49251945318722434} + {-0.49211360058019871, 0.49470854679786669} + {0.4984227008620481, 0.48463479169597612} + {0.49842270086204898, -0.5} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.422927 + g + 1 + r + 1 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\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} + VerticalPad + 0 + + + + Class + LineGraphic + ControlPoints + + {51.333333333333314, 0} + {-0.66666666666662877, 58.666666666666686} + {0.66673293066804717, -58.666850540458825} + {-16.306719354194399, 0.26652623861849634} + + Head + + ID + 169344 + Info + 4 + + ID + 169345 + Layer + 2 + Points + + {369.66666666666669, 541} + {404.00000000000023, 362} + {420.36749776329049, 302.42112495959606} + + Style + + stroke + + Bezier + + Color + + b + 0.75663 + g + 0.756618 + r + 0.75664 + + HeadArrow + 0 + Legacy + + LineType + 1 + Pattern + 1 + TailArrow + 0 + + + + + Class + LineGraphic + ControlPoints + + {69.833333333332462, -0.72767857143483639} + {-0.66690523835279691, -51.044605218028948} + {0.66666666666674246, 51.044637362162291} + {-24.333271383961971, -0.13425428344572765} + + Head + + ID + 169344 + Info + 4 + + ID + 169346 + Layer + 2 + Points + + {310.66666666666754, 118.72767857143484} + {399.33333333333417, 216.62202930450439} + {420.37955338188368, 301.45369961823752} + + Style + + stroke + + Bezier + + Color + + b + 0.75663 + g + 0.756618 + r + 0.75664 + + HeadArrow + 0 + Legacy + + LineType + 1 + Pattern + 1 + TailArrow + 0 + + + + + Class + LineGraphic + ControlPoints + + {-3.9999491373696401, 78.910715080442856} + {92.666683130060392, 0.22547126950667007} + + Head + + ID + 169085 + Info + 3 + + ID + 169361 + Layer + 2 + Points + + {473.33328247070392, 515.08928491955714} + {344.50002543131501, 611.77452873049333} + + Style + + stroke + + Bezier + + HeadArrow + SharpArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 169358 + Info + 1 + + + + Class + LineGraphic + ControlPoints + + {31.999987284342428, -14.081351280212308} + {-32.166667938232536, 10.244050343831077} + + ID + 169341 + Layer + 2 + Points + + {344.96346869509995, 346.26317428240412} + {389.8333346048999, 328.08928298950207} + + Style + + stroke + + Bezier + + HeadArrow + SharpArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 169340 + Info + 3 + + + + Class + LineGraphic + ControlPoints + + {31.999987284342428, -14.081351280212308} + {-28.500001271565793, 8.3333333333333144} + + ID + 169337 + Layer + 2 + Points + + {344.98861594084059, 323.71068461220347} + {391.1666679382335, 313.6666666666664} + + Style + + stroke + + Bezier + + HeadArrow + SharpArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 169336 + + + + Class + LineGraphic + ControlPoints + + {31.999987284342428, -14.081351280212308} + {-40, 1.5446373167492311} + + ID + 169333 + Layer + 2 + Points + + {344.9995533451783, 301.1612218744512} + {394.50000127156665, 299.00000000000045} + + Style + + stroke + + Bezier + + HeadArrow + SharpArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 169332 + Info + 3 + + + + Class + LineGraphic + ControlPoints + + {8.5833282470703125, -10.244596987647753} + {0, 0} + + FontInfo + + Color + + w + 0 + + Font + Helvetica + Size + 12 + + Head + + ID + 169051 + Info + 4 + + ID + 169062 + Layer + 2 + Points + + {272.41667175292969, 509.40064951817902} + {285.5, 503.81699882234847} + + Style + + stroke + + Bezier + + Color + + b + 0.0980392 + g + 0.0980392 + r + 0.0980392 + + HeadArrow + 0 + Legacy + + LineType + 1 + Pattern + 1 + TailArrow + 0 + + + Tail + + ID + 169208 + + + + Class + LineGraphic + FontInfo + + Color + + w + 0 + + Font + Helvetica + Size + 12 + + Head + + ID + 169042 + Info + 4 + + ID + 169061 + Layer + 2 + Points + + {82.999997456869679, 300.27229985501288} + {238.34892458824362, 260.57913893040109} + + Style + + stroke + + Color + + b + 0.0980392 + g + 0.0980392 + r + 0.0980392 + + HeadArrow + 0 + Legacy + + Pattern + 2 + TailArrow + 0 + + + Tail + + ID + 169034 + Position + 0.51973581314086914 + + + + Class + Group + Graphics + + + Bounds + {{419.66662216186666, 214.61452811107179}, {105.66666412353516, 22.544642175946908}} + Class + ShapedGraphic + ID + 169054 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.756045 + g + 0.75004 + r + 0.994455 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\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} + VerticalPad + 0 + + + + Bounds + {{419.66662216186666, 130.51770718892479}, {105.66666412353516, 22.544641494750977}} + Class + ShapedGraphic + ID + 169055 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.815377 + g + 1 + r + 0.820561 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\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} + VerticalPad + 0 + + + + Bounds + {{419.66662216186666, 91.940789540609359}, {105.66666412353516, 33.089282989501953}} + Class + ShapedGraphic + ID + 169056 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.999208 + g + 0.811343 + r + 0.644457 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\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)} + VerticalPad + 0 + + + + Bounds + {{419.66662216186666, 158.54998334248924}, {105.66666412353516, 22.544641494750977}} + Class + ShapedGraphic + ID + 169057 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.422927 + g + 1 + r + 1 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\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} + VerticalPad + 0 + + + + Bounds + {{419.66662216186666, 186.58225949605369}, {105.66666412353516, 22.544641494750977}} + Class + ShapedGraphic + ID + 169058 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.327428 + g + 0.81823 + r + 0.995566 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\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} + VerticalPad + 0 + + + + Bounds + {{419.66662216186666, 63.908513387045019}, {105.66666412353516, 22.544641494750977}} + Class + ShapedGraphic + ID + 169059 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.999449 + g + 0.743511 + r + 0.872276 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\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} + VerticalPad + 0 + + + + Bounds + {{406.9999504089372, 42.910746256511771}, {132.66667175292969, 207.81692504882812}} + Class + ShapedGraphic + ID + 169060 + Magnets + + {1, 0.5} + {1, -0.5} + {-1, 0.5} + {-1, -0.5} + {0.5, 1} + {-0.5, 1} + {0.5, -1} + {-0.5, -1} + + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + CornerRadius + 5 + + + Text + + Align + 0 + Text + {\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} + VerticalPad + 0 + + TextPlacement + 0 + + + ID + 169053 + Layer + 2 + + + Bounds + {{233.5000012715667, 20.000000000000934}, {116, 14}} + Class + ShapedGraphic + FitText + YES + Flow + Resize + FontInfo + + Font + Helvetica + Size + 12 + + ID + 169052 + Layer + 2 + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0 + Text + {\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 <%Canvas%>} + VerticalPad + 0 + + Wrap + NO + + + Bounds + {{285.5, 492.544677734375}, {105.66666412353516, 22.544642175946908}} + Class + ShapedGraphic + ID + 169051 + Layer + 2 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.999449 + g + 0.743511 + r + 0.872276 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\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} + VerticalPad + 0 + + + + Bounds + {{238.83337529500417, 335.17855853126167}, {105.66666412353516, 22.544642175946908}} + Class + ShapedGraphic + ID + 169340 + Layer + 2 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\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} + VerticalPad + 0 + + + + Bounds + {{238.83337529500417, 312.54463200342093}, {105.66666412353516, 22.544642175946908}} + Class + ShapedGraphic + ID + 169336 + Layer + 2 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\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} + VerticalPad + 0 + + + + Class + LineGraphic + ControlPoints + + {0, 7.05596923828125} + {0, -9} + + FontInfo + + Color + + w + 0 + + Font + Helvetica + Size + 12 + + Head + + ID + 169081 + Info + 2 + + ID + 169048 + Layer + 2 + Points + + {83.000001850648417, 128.2276785555359} + {82.999949137370791, 166.28868579864502} + + Style + + stroke + + Bezier + + Color + + b + 0.0980392 + g + 0.0980392 + r + 0.0980392 + + HeadArrow + SharpArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 169040 + + + + Class + LineGraphic + ControlPoints + + {0, 6.9840087890625} + {0, -9} + + FontInfo + + Color + + w + 0 + + Font + Helvetica + Size + 12 + + Head + + ID + 169040 + + ID + 169047 + Layer + 2 + Points + + {83.000002543132396, 67.727678571434836} + {83.000002543132396, 105.18303707668386} + + Style + + stroke + + Bezier + + Color + + b + 0.0980392 + g + 0.0980392 + r + 0.0980392 + + HeadArrow + SharpArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 169039 + Info + 1 + + + + Bounds + {{30.166670481364818, 671.51189931233432}, {105.66666412353516, 22.544641494750977}} + Class + ShapedGraphic + ID + 169046 + Layer + 2 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.999208 + g + 0.811343 + r + 0.644457 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\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} + VerticalPad + 0 + + + + Bounds + {{239, 629.51189931233432}, {105.66666412353516, 22.544641494750977}} + Class + ShapedGraphic + ID + 169045 + Layer + 2 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.327428 + g + 0.81823 + r + 0.995566 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\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} + VerticalPad + 0 + + + + Bounds + {{239, 572.5118993123325}, {105.66666412353516, 22.544641494750977}} + Class + ShapedGraphic + ID + 169044 + Layer + 2 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.327428 + g + 0.81823 + r + 0.995566 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\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} + VerticalPad + 0 + + + + Bounds + {{30.166670481364818, 548.10604731241608}, {105.66666412353516, 22.544641494750977}} + Class + ShapedGraphic + ID + 169043 + Layer + 2 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.999208 + g + 0.811343 + r + 0.644457 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\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} + VerticalPad + 0 + + + + Bounds + {{238.83336130777977, 249.18303989230026}, {105.66666412353516, 22.544641494750977}} + Class + ShapedGraphic + ID + 169042 + Layer + 2 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.999449 + g + 0.743511 + r + 0.872276 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\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} + VerticalPad + 0 + + + + Bounds + {{30.166670481364818, 240.18303707668389}, {105.66666412353516, 22.544641494750977}} + Class + ShapedGraphic + ID + 169041 + Layer + 2 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.815377 + g + 1 + r + 0.820561 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\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} + VerticalPad + 0 + + + + Bounds + {{30.166670481364818, 105.18303707668386}, {105.66666412353516, 22.544641494750977}} + Class + ShapedGraphic + ID + 169040 + Layer + 2 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.999208 + g + 0.811343 + r + 0.644457 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\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} + VerticalPad + 0 + + + + Bounds + {{30.166670481364818, 45.18303707668386}, {105.66666412353516, 22.544641494750977}} + Class + ShapedGraphic + ID + 169039 + Layer + 2 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.999208 + g + 0.811343 + r + 0.644457 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\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 } + VerticalPad + 0 + + + + Bounds + {{420.49995040893634, 472.78869058972316}, {105.66666412353516, 42.300594329833984}} + Class + ShapedGraphic + ID + 169358 + Layer + 2 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + {-0.49999999999999956, -0.5} + {-0.49999999999999956, 0.5} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.422927 + g + 1 + r + 1 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\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} + VerticalPad + 0 + + + + Bounds + {{420.49995040893634, 290.66666666666691}, {105.66666412353516, 22.544642175946908}} + Class + ShapedGraphic + ID + 169344 + Layer + 2 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.756045 + g + 0.75004 + r + 0.994455 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\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} + VerticalPad + 0 + + + + Bounds + {{238.83336512247806, 289.91071033477789}, {105.66666412353516, 22.544642175946908}} + Class + ShapedGraphic + ID + 169332 + Layer + 2 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\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} + VerticalPad + 0 + + + + Class + LineGraphic + ControlPoints + + {0, 6.9840087890625} + {0, -9} + + FontInfo + + Color + + w + 0 + + Font + Helvetica + Size + 12 + + Head + + ID + 169041 + + ID + 169035 + Layer + 2 + Points + + {82.999949137370791, 216.62202930450437} + {83.000002543132396, 240.18303707668389} + + Style + + stroke + + Bezier + + Color + + b + 0.0980392 + g + 0.0980392 + r + 0.0980392 + + HeadArrow + SharpArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 169082 + Info + 1 + + + + Class + LineGraphic + ControlPoints + + {0, 7.05596923828125} + {0, -9} + + FontInfo + + Color + + w + 0 + + Font + Helvetica + Size + 12 + + Head + + ID + 169075 + Info + 2 + + ID + 169034 + Layer + 2 + Points + + {82.999997456869679, 263.22767855635425} + {82.999997456869679, 335} + + Style + + stroke + + Bezier + + Color + + b + 0.0980392 + g + 0.0980392 + r + 0.0980392 + + HeadArrow + SharpArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 169041 + + + + Class + LineGraphic + ControlPoints + + {0, 6.9840087890625} + {0, -8.9999999999999432} + + FontInfo + + Color + + w + 0 + + Font + Helvetica + Size + 12 + + Head + + ID + 169070 + + ID + 169033 + Layer + 2 + Points + + {82.999997456869679, 385.33334350585938} + {83.000002543132396, 452.41914073626253} + + Style + + stroke + + Bezier + + Color + + b + 0.0980392 + g + 0.0980392 + r + 0.0980392 + + HeadArrow + SharpArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 169076 + Info + 1 + + + + Class + LineGraphic + ControlPoints + + {0, 6.9839935302734375} + {0, -9} + + FontInfo + + Color + + w + 0 + + Font + Helvetica + Size + 12 + + Head + + ID + 169046 + Info + 2 + + ID + 169128 + Layer + 2 + Points + + {83.000002543132396, 571.15068879140836} + {83.000002543132396, 671.51189931233421} + + Style + + stroke + + Bezier + + Color + + b + 0.0980392 + g + 0.0980392 + r + 0.0980392 + + HeadArrow + SharpArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 169043 + + + + Class + LineGraphic + ControlPoints + + {0, 7.055999755859375} + {0, -9} + + FontInfo + + Color + + w + 0 + + Font + Helvetica + Size + 12 + + Head + + ID + 169355 + Info + 2 + + ID + 169357 + Layer + 2 + Points + + {473.33328247070392, 313.2113088426139} + {473.33338419596407, 338.15028762817326} + + Style + + stroke + + Bezier + + Color + + b + 0.0980392 + g + 0.0980392 + r + 0.0980392 + + HeadArrow + SharpArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 169344 + + + + Class + LineGraphic + ControlPoints + + {0, 6.9840011596679688} + {0, -9} + + FontInfo + + Color + + w + 0 + + Font + Helvetica + Size + 12 + + Head + + ID + 169358 + + ID + 169362 + Layer + 2 + Points + + {473.33338359264764, 388.98363112285512} + {473.33328247070392, 472.78869058972316} + + Style + + stroke + + Bezier + + Color + + b + 0.0980392 + g + 0.0980392 + r + 0.0980392 + + HeadArrow + SharpArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 169356 + + + + GridInfo + + GuidesLocked + NO + GuidesVisible + YES + HPages + 1 + ImageCounter + 3 + KeepToScale + + Layers + + + Lock + NO + Name + no exceptions + Print + YES + View + YES + + + Lock + NO + Name + exceptions only + Print + YES + View + NO + + + Lock + NO + Name + all + Print + YES + View + NO + + + LayoutInfo + + Animate + NO + circoMinDist + 18 + circoSeparation + 0.0 + layoutEngine + dot + neatoSeparation + 0.0 + twopiSeparation + 0.0 + + LinksVisible + NO + MagnetsVisible + NO + MasterSheets + + ModificationDate + 2014-11-23 07:19:11 +0000 + Modifier + Steve Piercy + NotesVisible + NO + Orientation + 2 + OriginVisible + NO + PageBreaks + YES + PrintInfo + + NSBottomMargin + + float + 41 + + NSHorizonalPagination + + coded + BAtzdHJlYW10eXBlZIHoA4QBQISEhAhOU051bWJlcgCEhAdOU1ZhbHVlAISECE5TT2JqZWN0AIWEASqEhAFxlwCG + + NSLeftMargin + + float + 18 + + NSPaperSize + + size + {612, 792} + + NSPrintReverseOrientation + + int + 0 + + NSRightMargin + + float + 18 + + NSTopMargin + + float + 18 + + + PrintOnePage + + ReadOnly + NO + RowAlign + 1 + RowSpacing + 36 + SheetTitle + Request Processing + SmartAlignmentGuidesActive + YES + SmartDistanceGuidesActive + YES + UniqueID + 1 + UseEntirePage + + VPages + 1 + WindowInfo + + CurrentSheet + 0 + ExpandedCanvases + + + name + Request Processing + + + Frame + {{35, 93}, {1394, 1325}} + ListView + + OutlineWidth + 178 + RightSidebar + + ShowRuler + + Sidebar + + SidebarWidth + 163 + VisibleRegion + {{-231, -226}, {1037, 1186}} + Zoom + 1 + ZoomValues + + + Request Processing + 1 + 2 + + + + + diff --git a/docs/_static/pyramid_request_processing.png b/docs/_static/pyramid_request_processing.png new file mode 100644 index 000000000..2fbb1e164 Binary files /dev/null and b/docs/_static/pyramid_request_processing.png 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 @@ + + +2014-11-23 07:19ZRequest Processingno exceptionsmiddleware ingress tween ingresstraversalContextFoundtween egressresponse callbacksfinished callbacksmiddleware egressBeforeRenderRequest ProcessingLegendeventcallbackviewexternal process (middleware, tween)internal processview pipelinepredicatesview lookuproute predicatesURL dispatchNewRequestNewResponseview mapper ingressviewview mapper egressresponse adapterdecorators ingressdecorators egressauthorization diff --git a/docs/_static/pyramid_router.svg b/docs/_static/pyramid_router.svg deleted file mode 100644 index 21bbcb532..000000000 --- a/docs/_static/pyramid_router.svg +++ /dev/null @@ -1,3 +0,0 @@ - - -2014-11-23 07:19ZRequest Processingno exceptionsmiddleware ingress tween ingresstraversalContextFoundtween egressresponse callbacksfinished callbacksmiddleware egressBeforeRenderRequest ProcessingLegendeventcallbackviewexternal process (middleware, tween)internal processview pipelinepredicatesview lookuproute predicatesURL dispatchNewRequestNewResponseview mapper ingressviewview mapper egressresponse adapterdecorators ingressdecorators egressauthorization diff --git a/docs/narr/router.rst b/docs/narr/router.rst index 745c2faa1..e82b66801 100644 --- a/docs/narr/router.rst +++ b/docs/narr/router.rst @@ -9,7 +9,7 @@ Request Processing ================== -.. image:: ../_static/pyramid_router.svg +.. image:: ../_static/pyramid_request_processing.svg :alt: Request Processing Once a :app:`Pyramid` application is up and running, it is ready to accept -- cgit v1.2.3 From cc15bbf7de74f4cdfc676e34fa429d2658d1ddf6 Mon Sep 17 00:00:00 2001 From: Tres Seaver Date: Tue, 9 Dec 2014 11:00:41 -0500 Subject: Move coverage floor pct into 'setup.cfg'. --- setup.cfg | 1 + tox.ini | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index a877ffb7f..9633b6980 100644 --- a/setup.cfg +++ b/setup.cfg @@ -7,6 +7,7 @@ where=pyramid nocapture=1 cover-package=pyramid cover-erase=1 +cover-min-percentage=100 [aliases] dev = develop easy_install pyramid[testing] diff --git a/tox.ini b/tox.ini index 3f32dbc3f..714c5b6d3 100644 --- a/tox.ini +++ b/tox.ini @@ -12,7 +12,7 @@ basepython = python2.6 commands = python setup.py dev - python setup.py nosetests --with-xunit --with-xcoverage --cover-min-percentage=100 + python setup.py nosetests --with-xunit --with-xcoverage deps = nosexcover -- cgit v1.2.3