├── COPYRIGHT
├── pyramid_asyncio
├── scaffolds
│ ├── aio_jinja2
│ │ ├── README.rst_tmpl
│ │ ├── CHANGES.rst_tmpl
│ │ ├── +package+
│ │ │ ├── templates
│ │ │ │ ├── hello.jinja2
│ │ │ │ └── layout.jinja2
│ │ │ ├── assets
│ │ │ │ └── media
│ │ │ │ │ └── pyramid-16x16.png
│ │ │ ├── views.py_tmpl
│ │ │ ├── __init__.py_tmpl
│ │ │ └── tests.py_tmpl
│ │ ├── MANIFEST.in_tmpl
│ │ ├── setup.py_tmpl
│ │ └── development.ini_tmpl
│ └── __init__.py
├── __init__.py
├── aioinspect.py
├── testing.py
├── worker.py
├── tweens.py
├── view.py
├── cache.py
├── kvs.py
├── session.py
├── request.py
├── authorization.py
├── router.py
└── config.py
├── MANIFEST.in
├── CHANGES.rst
├── .gitignore
├── README.rst
├── LICENSE
└── setup.py
/COPYRIGHT:
--------------------------------------------------------------------------------
1 | Copyright © 2014, Guillaume Gauvrit
2 |
--------------------------------------------------------------------------------
/pyramid_asyncio/scaffolds/aio_jinja2/README.rst_tmpl:
--------------------------------------------------------------------------------
1 | {{project}}
2 |
3 |
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include *.txt *.ini *.cfg *.rst LICENSE COPYRIGHT
2 | graft pyramid_asyncio/scaffolds
3 |
--------------------------------------------------------------------------------
/pyramid_asyncio/scaffolds/aio_jinja2/CHANGES.rst_tmpl:
--------------------------------------------------------------------------------
1 | Changelog
2 | =========
3 |
4 | 0.0
5 | ---
6 |
7 | * Initial release
8 |
--------------------------------------------------------------------------------
/pyramid_asyncio/scaffolds/aio_jinja2/+package+/templates/hello.jinja2:
--------------------------------------------------------------------------------
1 | {% extends "layout.jinja2" %}
2 |
3 | {% block body %}
4 |
5 | Hello {{name}}!
6 |
7 | {% endblock %}
8 |
--------------------------------------------------------------------------------
/pyramid_asyncio/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | Get pyramid working with asyncio
3 | """
4 | __version__ = '0.2'
5 |
6 | from .config import includeme
7 | from .view import coroutine_view_config
8 |
--------------------------------------------------------------------------------
/pyramid_asyncio/aioinspect.py:
--------------------------------------------------------------------------------
1 |
2 | import inspect
3 | import asyncio
4 |
5 |
6 | def is_generator(func):
7 | return isinstance(func, asyncio.Future) or inspect.isgenerator(func)
8 |
--------------------------------------------------------------------------------
/pyramid_asyncio/scaffolds/aio_jinja2/MANIFEST.in_tmpl:
--------------------------------------------------------------------------------
1 | include *.txt *.ini *.cfg *.rst LICENSE
2 | recursive-include {{package}} *.ico *.png *.css *.gif *.jpg *.pt *.txt *.jinja2 *.js *.html *.xml
--------------------------------------------------------------------------------
/pyramid_asyncio/scaffolds/aio_jinja2/+package+/assets/media/pyramid-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mardiros/pyramid_asyncio/HEAD/pyramid_asyncio/scaffolds/aio_jinja2/+package+/assets/media/pyramid-16x16.png
--------------------------------------------------------------------------------
/pyramid_asyncio/scaffolds/__init__.py:
--------------------------------------------------------------------------------
1 | from pyramid.scaffolds import PyramidTemplate
2 |
3 |
4 | class AioJinja2Template(PyramidTemplate):
5 | _template_dir = 'aio_jinja2'
6 | summary = 'Pyramid project using asyncio and jinja2'
7 |
--------------------------------------------------------------------------------
/pyramid_asyncio/testing.py:
--------------------------------------------------------------------------------
1 | """
2 | Helper for tests
3 | """
4 |
5 | import asyncio
6 |
7 | def async_test(func):
8 |
9 | def wrapper(*args, **kwargs):
10 | coro = asyncio.coroutine(func)
11 | future = coro(*args, **kwargs)
12 | loop = asyncio.get_event_loop()
13 | loop.run_until_complete(future)
14 | return wrapper
15 |
--------------------------------------------------------------------------------
/pyramid_asyncio/scaffolds/aio_jinja2/+package+/views.py_tmpl:
--------------------------------------------------------------------------------
1 | import asyncio
2 | from pyramid.response import Response
3 | from pyramid_asyncio.view import coroutine_view_config
4 |
5 |
6 | @coroutine_view_config(route_name='say_hello', renderer='hello.jinja2')
7 | def say_hello(request):
8 | yield from asyncio.sleep(1)
9 | return {'name': 'asyncio'}
10 |
--------------------------------------------------------------------------------
/pyramid_asyncio/scaffolds/aio_jinja2/+package+/templates/layout.jinja2:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{project}}
5 |
6 |
7 |
8 |
9 | {% block body %}
10 | {% endblock %}
11 |
12 |
--------------------------------------------------------------------------------
/pyramid_asyncio/scaffolds/aio_jinja2/+package+/__init__.py_tmpl:
--------------------------------------------------------------------------------
1 | from pyramid.config import Configurator
2 |
3 |
4 | def main(global_config, **settings):
5 | """ This function returns a Pyramid WSGI application.
6 | """
7 | config = Configurator(settings=settings)
8 | config.add_static_view('assets', 'assets', cache_max_age=3600)
9 | config.add_route('say_hello', '/')
10 | config.scan()
11 | return config.make_asyncio_app()
12 |
--------------------------------------------------------------------------------
/pyramid_asyncio/scaffolds/aio_jinja2/+package+/tests.py_tmpl:
--------------------------------------------------------------------------------
1 | import unittest
2 |
3 | from pyramid import testing
4 | from pyramid_asyncio.testing import async_test
5 |
6 | class HelloTestCase(unittest.TestCase):
7 |
8 | @async_test
9 | def test_passing_view(self):
10 | from .views import say_hello
11 | request = testing.DummyRequest()
12 | info = yield from say_hello(request)
13 | self.assertEqual(info['name'], 'asyncio')
14 |
--------------------------------------------------------------------------------
/pyramid_asyncio/worker.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 |
3 | from gunicorn.workers.gaiohttp import AiohttpWorker as BaseWorker
4 |
5 | class AiohttpWorker(BaseWorker):
6 |
7 | @asyncio.coroutine
8 | def _run(self):
9 |
10 | if hasattr(self.wsgi, 'open'):
11 | yield from self.wsgi.open()
12 |
13 | yield from super()._run()
14 |
15 | def handle_quit(self, sig, frame):
16 | self.alive = False
17 |
18 | handle_exit = handle_quit
19 |
--------------------------------------------------------------------------------
/CHANGES.rst:
--------------------------------------------------------------------------------
1 | Changelog
2 | =========
3 |
4 | 0.2 Released on 2017-05-07
5 | --------------------------
6 |
7 | - Pin version of Pyramid 1.5
8 |
9 | The ViewDeriver has evolved and is now an interface IViewDeriver.
10 | Unfortunately, this make things harder for replacing the default ViewDeriver
11 | installed.
12 |
13 | - Pin version of aiohttp.
14 |
15 | WSGI/Gunicorn support has been dropped in version 2.
16 |
17 |
18 | 0.1 Released on 2015-03-30
19 | --------------------------
20 |
21 | - Initial Release
22 |
23 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.py[cod]
2 | __pycache__
3 |
4 | # C extensions
5 | *.so
6 |
7 | # Packages
8 | *.egg
9 | *.egg-info
10 | dist
11 | build
12 | eggs
13 | parts
14 | var
15 | sdist
16 | develop-eggs
17 | .installed.cfg
18 | lib
19 | lib64
20 |
21 | # Installer logs
22 | pip-log.txt
23 |
24 | # Unit test / coverage reports
25 | .coverage
26 | .tox
27 | nosetests.xml
28 |
29 | # Translations
30 | *.mo
31 |
32 | # Mr Developer
33 | .mr.developer.cfg
34 | .project
35 | .pydevproject
36 | .settings
37 | venv*
38 | contrib
39 |
40 | # project files
41 | sample
42 |
--------------------------------------------------------------------------------
/pyramid_asyncio/tweens.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | import sys
3 |
4 | from pyramid.interfaces import (
5 | IExceptionViewClassifier,
6 | IRequest,
7 | IView,
8 | )
9 |
10 | from zope.interface import providedBy
11 |
12 | from pyramid.config import tweens
13 |
14 | from .aioinspect import is_generator
15 |
16 |
17 | class Tweens(tweens.Tweens):
18 |
19 | def __call__(self, handler, registry):
20 |
21 | def handle_handler(handler):
22 | @asyncio.coroutine
23 | def handle_request(request):
24 | response = handler(request)
25 | if is_generator(response):
26 | response = yield from response
27 | return response
28 | return handle_request
29 |
30 | if self.explicit:
31 | use = self.explicit
32 | else:
33 | use = self.implicit()
34 |
35 | for name, factory in use[::-1]:
36 | handler = factory(handler, registry)
37 | return handler
38 |
39 |
--------------------------------------------------------------------------------
/README.rst:
--------------------------------------------------------------------------------
1 | ===============
2 | pyramid-asyncio
3 | ===============
4 |
5 | A lib that override pyramid to build asyncio web application.
6 |
7 | Basically, it change views to asyncio coroutine.
8 |
9 |
10 |
11 | .. important ::
12 |
13 | This library is a proof of concept and has not been ported to recent version
14 | of Pyramid and aiohttp.
15 |
16 |
17 | Getting Started
18 | ---------------
19 |
20 | pyramid_asyncio add two directives to treat views as coroutine.
21 |
22 | * config.add_coroutine_view()
23 |
24 | This is a coroutine version of the ``config.add_view``.
25 | pyramid_asyncio provide also a decorator ``coroutine_view_config`` which
26 | is the view_config version for coroutine view.
27 |
28 | * config.make_asyncio_app()
29 |
30 | This create the wsgi app that work with the aiohttp gunicorn worker.
31 | aiohttp.worker.AsyncGunicornWorker
32 |
33 | config.make_wsgi_app() could not be used because the pyramid router
34 | must be changed.
35 |
36 |
37 | The simple way to create the pyramid app with asyncio is to use the
38 | scaffold.
39 |
40 | pyramid_asyncio comme with a scaffold that create a "hello world" application,
41 | check it
42 |
43 | ::
44 |
45 | pcreate -s aio_jinja2 helloworld
46 |
47 |
--------------------------------------------------------------------------------
/pyramid_asyncio/view.py:
--------------------------------------------------------------------------------
1 | import venusian
2 |
3 |
4 | class coroutine_view_config(object):
5 | """
6 | A patched version of view_config that use coroutine for views
7 | """
8 | venusian = venusian # for testing injection
9 | def __init__(self, **settings):
10 | if 'for_' in settings:
11 | if settings.get('context') is None:
12 | settings['context'] = settings['for_']
13 | self.__dict__.update(settings)
14 |
15 | def __call__(self, wrapped):
16 | settings = self.__dict__.copy()
17 | depth = settings.pop('_depth', 0)
18 |
19 | def callback(context, name, ob):
20 | config = context.config.with_package(info.module)
21 | config.add_coroutine_view(view=ob, **settings)
22 |
23 | info = self.venusian.attach(wrapped, callback, category='pyramid',
24 | depth=depth + 1)
25 |
26 | if info.scope == 'class':
27 | # if the decorator was attached to a method in a class, or
28 | # otherwise executed at class scope, we need to set an
29 | # 'attr' into the settings if one isn't already in there
30 | if settings.get('attr') is None:
31 | settings['attr'] = wrapped.__name__
32 |
33 | settings['_info'] = info.codeinfo # fbo "action_method"
34 | return wrapped
35 |
--------------------------------------------------------------------------------
/pyramid_asyncio/scaffolds/aio_jinja2/setup.py_tmpl:
--------------------------------------------------------------------------------
1 | import os
2 | import sys
3 |
4 | from setuptools import setup, find_packages
5 |
6 | py_version = sys.version_info[:2]
7 | if py_version < (3, 3):
8 | raise Exception("websockets requires Python >= 3.3.")
9 |
10 |
11 | here = os.path.abspath(os.path.dirname(__file__))
12 | NAME = '{{project}}'
13 | with open(os.path.join(here, 'README.rst')) as readme:
14 | README = readme.read()
15 | with open(os.path.join(here, 'CHANGES.rst')) as changes:
16 | CHANGES = changes.read()
17 |
18 | requires = [
19 | 'pyramid',
20 | 'gunicorn',
21 | 'aiohttp',
22 | 'pyramid_jinja2',
23 | ]
24 |
25 | setup(name=NAME,
26 | version='0.0',
27 | description='{{project}}',
28 | long_description=README + '\n\n' + CHANGES,
29 | classifiers=[
30 | "Programming Language :: Python",
31 | "Programming Language :: Python :: 3.3",
32 | "Programming Language :: Python :: 3.4",
33 | "Framework :: Pyramid",
34 | "Topic :: Internet :: WWW/HTTP",
35 | "Topic :: Internet :: WWW/HTTP :: WSGI :: Application",
36 | ],
37 | author='',
38 | author_email='',
39 | url='',
40 | keywords='web wsgi bfg pylons pyramid',
41 | packages=find_packages(),
42 | include_package_data=True,
43 | zip_safe=False,
44 | test_suite=NAME,
45 | install_requires=requires,
46 | entry_points="""\
47 | [paste.app_factory]
48 | main = {{package}}:main
49 | """,
50 | )
51 |
--------------------------------------------------------------------------------
/pyramid_asyncio/cache.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 |
3 | import simplejson as json
4 | from pyramid.events import NewRequest
5 |
6 | from .kvs import cache, serializer
7 |
8 |
9 | class ApplicationCache(cache.ApplicationCache):
10 |
11 | @classmethod
12 | @asyncio.coroutine
13 | def connect(cls, settings):
14 | """ Call that method in the pyramid configuration phase.
15 | """
16 |
17 | kvs_cache = json.loads(settings['kvs.cache'])
18 | kvs_cache['kvs'] = 'aioredis'
19 | settings['kvs.cache'] = json.dumps(kvs_cache)
20 | super().connect(settings)
21 | cls.client._client = yield from cls.client._client
22 |
23 | @asyncio.coroutine
24 | def get(self, key, default=None):
25 | return (yield from self.client.get(key, default))
26 |
27 | @asyncio.coroutine
28 | def set(self, key, value, ttl=None):
29 | yield from self.client.set(key, value, ttl=None)
30 |
31 | @asyncio.coroutine
32 | def pop(self, key, default=None):
33 | try:
34 | ret = yield from self.get(key)
35 | yield from self.client.delete(key)
36 | return ret
37 | except KeyError:
38 | return default
39 |
40 |
41 | def subscribe_cache(event):
42 | request = event.request
43 | request.set_property(ApplicationCache(request), 'cache', reify=True)
44 |
45 |
46 | @asyncio.coroutine
47 | def includeme(config):
48 | settings = config.registry.settings
49 | if 'kvs.cache' in settings:
50 | yield from ApplicationCache.connect(settings)
51 | config.add_subscriber(subscribe_cache, NewRequest)
52 |
--------------------------------------------------------------------------------
/pyramid_asyncio/kvs.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 |
3 | try:
4 | from asyncio_redis import Connection
5 | from pyramid_kvs import serializer, kvs, session, cache
6 | from pyramid_kvs.serializer import serializer
7 | except ImportError:
8 | raise Exception("Packages asyncio_redis and pyramid_kvs required")
9 |
10 |
11 | class AIORedis(kvs.KVS):
12 |
13 | @asyncio.coroutine
14 | def get(self, key, default=None):
15 | if key is None:
16 | return default
17 | ret = yield from self.raw_get(key)
18 | if ret is None:
19 | return default
20 | return self._serializer.loads(ret)
21 |
22 | @asyncio.coroutine
23 | def set(self, key, value, ttl=None):
24 | value = self._serializer.dumps(value)
25 | return (yield from self.raw_set(key, value, ttl or self.ttl))
26 |
27 | @asyncio.coroutine
28 | def delete(self, key):
29 | yield from self._client.delete([self._get_key(key)])
30 |
31 | @asyncio.coroutine
32 | def _create_client(self, **kwargs):
33 | return (yield from Connection.create(**kwargs))
34 |
35 | def _get_key(self, key):
36 | return super()._get_key(key).decode('utf-8')
37 |
38 | @asyncio.coroutine
39 | def raw_get(self, key, default=None):
40 | ret = yield from self._client.get(self._get_key(key))
41 | return default if ret is None else ret
42 |
43 | @asyncio.coroutine
44 | def raw_set(self, key, value, ttl):
45 | yield from self._client.setex(self._get_key(key), ttl,
46 | value)
47 |
48 | @asyncio.coroutine
49 | def incr(self, key):
50 | return (yield from self._client.incr(self._get_key(key)))
51 |
52 |
53 | kvs._implementations['aioredis'] = AIORedis # XXX Private access
54 |
55 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | License
2 |
3 | A copyright notice accompanies this license document that identifies
4 | the copyright holders.
5 |
6 | Redistribution and use in source and binary forms, with or without
7 | modification, are permitted provided that the following conditions are
8 | met:
9 |
10 | 1. Redistributions in source code must retain the accompanying
11 | copyright notice, this list of conditions, and the following
12 | disclaimer.
13 |
14 | 2. Redistributions in binary form must reproduce the accompanying
15 | copyright notice, this list of conditions, and the following
16 | disclaimer in the documentation and/or other materials provided
17 | with the distribution.
18 |
19 | 3. Names of the copyright holders must not be used to endorse or
20 | promote products derived from this software without prior
21 | written permission from the copyright holders.
22 |
23 | 4. If any files are modified, you must cause the modified files to
24 | carry prominent notices stating that you changed the files and
25 | the date of any change.
26 |
27 | Disclaimer
28 |
29 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND
30 | ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
31 | TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
32 | PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
33 | HOLDERS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
34 | EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
35 | TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
36 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
37 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
38 | TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
39 | THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
40 | SUCH DAMAGE.
41 |
--------------------------------------------------------------------------------
/pyramid_asyncio/scaffolds/aio_jinja2/development.ini_tmpl:
--------------------------------------------------------------------------------
1 | ###
2 | # app configuration
3 | # http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html
4 | ###
5 |
6 | [app:main]
7 | use = egg:{{project}}
8 |
9 | pyramid.reload_templates = true
10 | pyramid.debug_authorization = false
11 | pyramid.debug_notfound = false
12 | pyramid.debug_routematch = false
13 | pyramid.default_locale_name = en
14 | pyramid.includes =
15 | pyramid_asyncio
16 | pyramid_jinja2
17 |
18 |
19 | jinja2.directories = {{project}}:templates/
20 | jinja2.i18n.domain = {{project}}
21 | jinja2.filters =
22 | model_url = pyramid_jinja2.filters:model_url_filter
23 | route_url = pyramid_jinja2.filters:route_url_filter
24 | static_url = pyramid_jinja2.filters:static_url_filter
25 | route_path = pyramid_jinja2.filters:route_path_filter
26 | static_path = pyramid_jinja2.filters:static_path_filter
27 |
28 | # By default, the toolbar only appears for clients from IP addresses
29 | # '127.0.0.1' and '::1'.
30 | # debugtoolbar.hosts = 127.0.0.1 ::1
31 |
32 | ###
33 | # server configuration (This is not exactly wsgi)
34 | ###
35 |
36 | [server:main]
37 | use = egg:gunicorn#main
38 | host = 0.0.0.0
39 | port = 6543
40 | worker_class = pyramid_asyncio.worker.AiohttpWorker
41 |
42 | ###
43 | # logging configuration
44 | # http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html
45 | ###
46 |
47 | [loggers]
48 | keys = root, asyncio, {{project}}
49 |
50 | [handlers]
51 | keys = console
52 |
53 | [formatters]
54 | keys = generic
55 |
56 | [logger_root]
57 | level = INFO
58 | handlers = console
59 |
60 | [logger_asyncio]
61 | level = WARN
62 | handlers =
63 | qualname = asyncio
64 |
65 | [logger_{{project}}]
66 | level = DEBUG
67 | handlers =
68 | qualname = {{project}}
69 |
70 |
71 | [handler_console]
72 | class = StreamHandler
73 | args = (sys.stderr,)
74 | level = NOTSET
75 | formatter = generic
76 |
77 | [formatter_generic]
78 | format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s
79 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | import os
2 | import sys
3 | import re
4 |
5 | from setuptools import setup, find_packages
6 |
7 | NAME = 'pyramid-asyncio'
8 |
9 | py_version = sys.version_info[:2]
10 | if py_version < (3, 3):
11 | raise Exception("{name} requires Python >= 3.3.".format(name=NAME))
12 |
13 | here = os.path.abspath(os.path.dirname(__file__))
14 |
15 | with open(os.path.join(here, 'README.rst')) as readme:
16 | README = readme.read()
17 | with open(os.path.join(here, 'CHANGES.rst')) as changes:
18 | CHANGES = changes.read()
19 |
20 | with open(os.path.join(here, NAME.replace('-', '_'),
21 | '__init__.py')) as version:
22 | VERSION = re.compile(r".*__version__ = '(.*?)'",
23 | re.S).match(version.read()).group(1)
24 |
25 |
26 | requires = [
27 | 'pyramid < 1.7a1',
28 | 'gunicorn >= 19.0',
29 | 'aiohttp < 2.0',
30 | ]
31 |
32 | extras_require = {
33 | 'session': ['pyramid-kvs >= 0.2', # XXX unreleased
34 | 'asyncio-redis',
35 | 'simplejson'
36 | ]
37 | }
38 |
39 | if py_version < (3, 4):
40 | requires.append('asyncio')
41 |
42 |
43 | setup(name=NAME,
44 | version=VERSION,
45 | description='Pyramid Asyncio Glue',
46 | long_description=README + '\n\n' + CHANGES,
47 | classifiers=[
48 | "Programming Language :: Python",
49 | "Programming Language :: Python :: 3.3",
50 | "Programming Language :: Python :: 3.4",
51 | "Framework :: Pyramid",
52 | "Topic :: Internet :: WWW/HTTP",
53 | "Topic :: Internet :: WWW/HTTP :: WSGI :: Application",
54 | "Intended Audience :: Developers",
55 | "License :: Repoze Public License",
56 | ],
57 | author='Guillaume Gauvrit',
58 | author_email='guillaume@gauvr.it',
59 | url='https://github.com/mardiros/pyramid_asyncio',
60 | keywords='pyramid asyncio',
61 | packages=find_packages(),
62 | include_package_data=True,
63 | zip_safe=False,
64 | test_suite='{name}.tests'.format(name=NAME),
65 | install_requires=requires,
66 | license="BSD-derived (http://www.repoze.org/LICENSE.txt)",
67 | extras_require=extras_require,
68 | entry_points = """\
69 | [pyramid.scaffold]
70 | aio_jinja2=pyramid_asyncio.scaffolds:AioJinja2Template
71 | """
72 | )
73 |
--------------------------------------------------------------------------------
/pyramid_asyncio/session.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | import logging
3 | from collections import defaultdict
4 |
5 | from zope.interface import implementer
6 | from pyramid.interfaces import ISession, ISessionFactory
7 |
8 | from .kvs import session, serializer
9 |
10 |
11 | log = logging.getLogger(__name__)
12 |
13 | @implementer(ISession)
14 | class CookieSession(session.CookieSession):
15 |
16 | def __init__(self, request, client, key_name):
17 | self._dirty = False
18 | self.key_name = key_name
19 | self.client = client
20 | self.request = request
21 |
22 | self._session_key = self.get_session_key()
23 | self._session_data = None
24 |
25 | @asyncio.coroutine
26 | def load_session_data(self):
27 | self._session_data = defaultdict(defaultdict)
28 |
29 | if not self._session_key:
30 | log.warn('No session found')
31 | return
32 |
33 | stored_data = yield from self.client.get(self._session_key)
34 | if stored_data:
35 | self._session_data.update(stored_data)
36 | else:
37 | self.changed()
38 |
39 | def get_session_key(self):
40 | session_key = self.request.cookies.get(self.key_name)
41 | if not session_key:
42 | session_key = session._create_token() # XXX private method called
43 | return session_key
44 |
45 | @asyncio.coroutine
46 | def save_session(self, request, response):
47 | if self._session_data is None: # session invalidated
48 | self.client.delete(self._session_key)
49 | response.delete_cookie(self.key_name)
50 | return
51 | response.set_cookie(self.key_name, self._session_key,
52 | self.client.ttl)
53 | yield from self.client.set(self._session_key, self._session_data)
54 |
55 |
56 | @implementer(ISessionFactory)
57 | class SessionFactory(object):
58 | session_class = CookieSession
59 |
60 | def __init__(self, settings):
61 | self.config = serializer('json').loads(settings['asyncio.session'])
62 | self.config.setdefault('key_prefix', 'session::')
63 | self.key_name = self.config.pop('key_name', 'session_id')
64 | self._client = None
65 |
66 | @asyncio.coroutine
67 | def __call__(self, request):
68 | if self._client is None:
69 | self.config['kvs'] = 'aioredis'
70 | self._client = kvs.KVS(**self.config)
71 | self._client._client = yield from self._client._client
72 | session = CookieSession(request, self._client, self.key_name)
73 | yield from session.load_session_data()
74 | return session
75 |
--------------------------------------------------------------------------------
/pyramid_asyncio/request.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | from io import BytesIO
3 |
4 | from zope.interface import implementer
5 | from pyramid.interfaces import (
6 | IRequestFactory,
7 | IRequest,
8 | IAuthenticationPolicy,
9 | IAuthorizationPolicy,
10 | )
11 |
12 | from pyramid.request import Request as BaseRequest
13 | from pyramid.security import Allowed
14 | from .aioinspect import is_generator
15 |
16 |
17 | @implementer(IRequest)
18 | class Request(BaseRequest):
19 |
20 | @asyncio.coroutine
21 | def _process_response_callbacks(self, response):
22 | callbacks = self.response_callbacks
23 | while callbacks:
24 | callback = callbacks.pop(0)
25 | callback_resp = callback(self, response)
26 | if is_generator(callback_resp):
27 | yield from callback_resp
28 |
29 | @asyncio.coroutine
30 | def _process_finished_callbacks(self):
31 | callbacks = self.finished_callbacks
32 | while callbacks:
33 | callback = callbacks.pop(0)
34 | callback_resp = callback(self)
35 | if is_generator(callback_resp):
36 | yield from callback_resp
37 |
38 | @asyncio.coroutine
39 | def has_permission(self, permission, context=None):
40 | if context is None:
41 | context = self.context
42 | reg = self.registry
43 | authn_policy = reg.queryUtility(IAuthenticationPolicy)
44 | if authn_policy is None:
45 | return Allowed('No authentication policy in use.')
46 | authz_policy = reg.queryUtility(IAuthorizationPolicy)
47 | if authz_policy is None:
48 | raise ValueError('Authentication policy registered without '
49 | 'authorization policy') # should never happen
50 | principals = authn_policy.effective_principals(self)
51 | if is_generator(principals):
52 | principals = yield from principals
53 | permits = authz_policy.permits(context, principals, permission)
54 | if is_generator(permits):
55 | permits = yield from permits
56 | return permits
57 |
58 |
59 | @implementer(IRequestFactory)
60 | class RequestFactory:
61 | """ A utility which generates a request """
62 |
63 | @asyncio.coroutine
64 | def __call__(self, environ):
65 | """ Return an object implementing IRequest, e.g. an instance
66 | of ``pyramid.request.Request``"""
67 |
68 | body = environ['wsgi.input'].read()
69 | if is_generator(body):
70 | # wsgi.input can be a generator depending of
71 | # the configuration of aiohttp
72 | body = yield from body
73 | environ['wsgi.input'] = BytesIO(body)
74 | return Request(environ)
75 |
76 | @classmethod
77 | def blank(cls, path):
78 | """ Return an empty request object (see
79 | :meth:`pyramid.request.Request.blank`)"""
80 | return Request.blank(path)
81 |
--------------------------------------------------------------------------------
/pyramid_asyncio/authorization.py:
--------------------------------------------------------------------------------
1 | """
2 | Adaptation of pyramid.authorisation that support async permission loaded.
3 | """
4 | import asyncio
5 | from zope.interface import implementer
6 |
7 | from pyramid.interfaces import IAuthorizationPolicy
8 |
9 | from pyramid.location import lineage
10 |
11 | from pyramid.compat import is_nonstr_iter
12 |
13 | from pyramid.security import (
14 | ACLAllowed,
15 | ACLDenied,
16 | Allow,
17 | Deny,
18 | Everyone,
19 | )
20 |
21 | from .aioinspect import is_generator
22 |
23 |
24 | @implementer(IAuthorizationPolicy)
25 | class ACLAuthorizationPolicy(object):
26 | """ An :term:`authorization policy` which consults an :term:`ACL`
27 | object attached to a :term:`context` to determine authorization
28 | information about a :term:`principal` or multiple principals.
29 | If the context is part of a :term:`lineage`, the context's parents
30 | are consulted for ACL information too. The following is true
31 | about this security policy.
32 |
33 | - When checking whether the 'current' user is permitted (via the
34 | ``permits`` method), the security policy consults the
35 | ``context`` for an ACL first. If no ACL exists on the context,
36 | or one does exist but the ACL does not explicitly allow or deny
37 | access for any of the effective principals, consult the
38 | context's parent ACL, and so on, until the lineage is exhausted
39 | or we determine that the policy permits or denies.
40 |
41 | During this processing, if any :data:`pyramid.security.Deny`
42 | ACE is found matching any principal in ``principals``, stop
43 | processing by returning an
44 | :class:`pyramid.security.ACLDenied` instance (equals
45 | ``False``) immediately. If any
46 | :data:`pyramid.security.Allow` ACE is found matching any
47 | principal, stop processing by returning an
48 | :class:`pyramid.security.ACLAllowed` instance (equals
49 | ``True``) immediately. If we exhaust the context's
50 | :term:`lineage`, and no ACE has explicitly permitted or denied
51 | access, return an instance of
52 | :class:`pyramid.security.ACLDenied` (equals ``False``).
53 |
54 | - When computing principals allowed by a permission via the
55 | :func:`pyramid.security.principals_allowed_by_permission`
56 | method, we compute the set of principals that are explicitly
57 | granted the ``permission`` in the provided ``context``. We do
58 | this by walking 'up' the object graph *from the root* to the
59 | context. During this walking process, if we find an explicit
60 | :data:`pyramid.security.Allow` ACE for a principal that
61 | matches the ``permission``, the principal is included in the
62 | allow list. However, if later in the walking process that
63 | principal is mentioned in any :data:`pyramid.security.Deny`
64 | ACE for the permission, the principal is removed from the allow
65 | list. If a :data:`pyramid.security.Deny` to the principal
66 | :data:`pyramid.security.Everyone` is encountered during the
67 | walking process that matches the ``permission``, the allow list
68 | is cleared for all principals encountered in previous ACLs. The
69 | walking process ends after we've processed the any ACL directly
70 | attached to ``context``; a set of principals is returned.
71 |
72 | Objects of this class implement the
73 | :class:`pyramid.interfaces.IAuthorizationPolicy` interface.
74 | """
75 |
76 | @asyncio.coroutine
77 | def permits(self, context, principals, permission):
78 | """ Return an instance of
79 | :class:`pyramid.security.ACLAllowed` instance if the policy
80 | permits access, return an instance of
81 | :class:`pyramid.security.ACLDenied` if not."""
82 |
83 | acl = ''
84 |
85 | for location in lineage(context):
86 | try:
87 | acl = location.__acl__
88 | except AttributeError:
89 | continue
90 |
91 | if acl and callable(acl):
92 | acl = acl()
93 |
94 | if is_generator(acl):
95 | acl = yield from acl
96 |
97 | for ace in acl:
98 | ace_action, ace_principal, ace_permissions = ace
99 | if ace_principal in principals:
100 | if not is_nonstr_iter(ace_permissions):
101 | ace_permissions = [ace_permissions]
102 | if permission in ace_permissions:
103 | if ace_action == Allow:
104 | return ACLAllowed(ace, acl, permission,
105 | principals, location)
106 | else:
107 | return ACLDenied(ace, acl, permission,
108 | principals, location)
109 |
110 | # default deny (if no ACL in lineage at all, or if none of the
111 | # principals were mentioned in any ACE we found)
112 | return ACLDenied(
113 | '',
114 | acl,
115 | permission,
116 | principals,
117 | context)
118 |
119 | @asyncio.coroutine
120 | def principals_allowed_by_permission(self, context, permission):
121 | """ Return the set of principals explicitly granted the
122 | permission named ``permission`` according to the ACL directly
123 | attached to the ``context`` as well as inherited ACLs based on
124 | the :term:`lineage`."""
125 | allowed = set()
126 |
127 | for location in reversed(list(lineage(context))):
128 | # NB: we're walking *up* the object graph from the root
129 | try:
130 | acl = location.__acl__
131 | except AttributeError:
132 | continue
133 |
134 | allowed_here = set()
135 | denied_here = set()
136 |
137 | if acl and callable(acl):
138 | acl = acl()
139 |
140 | if is_generator(acl):
141 | acl = yield from acl
142 |
143 | for ace_action, ace_principal, ace_permissions in acl:
144 | if not is_nonstr_iter(ace_permissions):
145 | ace_permissions = [ace_permissions]
146 | if (ace_action == Allow) and (permission in ace_permissions):
147 | if not ace_principal in denied_here:
148 | allowed_here.add(ace_principal)
149 | if (ace_action == Deny) and (permission in ace_permissions):
150 | denied_here.add(ace_principal)
151 | if ace_principal == Everyone:
152 | # clear the entire allowed set, as we've hit a
153 | # deny of Everyone ala (Deny, Everyone, ALL)
154 | allowed = set()
155 | break
156 | elif ace_principal in allowed:
157 | allowed.remove(ace_principal)
158 |
159 | allowed.update(allowed_here)
160 |
161 | return allowed
162 |
--------------------------------------------------------------------------------
/pyramid_asyncio/router.py:
--------------------------------------------------------------------------------
1 | """
2 | Override the Pyramid Router in order to get request handler as
3 | asyncio coroutine.
4 | """
5 |
6 | import sys
7 | import asyncio
8 | import logging
9 |
10 | from zope.interface import (
11 | implementer,
12 | providedBy,
13 | )
14 |
15 | from pyramid.interfaces import (
16 | IRequest,
17 | IRouteRequest,
18 | IRouter,
19 | ISessionFactory,
20 | ITraverser,
21 | IView,
22 | IViewClassifier,
23 | IExceptionViewClassifier,
24 | ITweens,
25 | )
26 |
27 | from pyramid.events import (
28 | ContextFound,
29 | NewRequest,
30 | NewResponse,
31 | )
32 |
33 | from pyramid.exceptions import PredicateMismatch, ConfigurationError
34 | from pyramid.httpexceptions import HTTPException, HTTPNotFound
35 | from pyramid.settings import aslist
36 |
37 | from pyramid.traversal import (
38 | ResourceTreeTraverser,
39 | )
40 |
41 | from pyramid.router import Router as RouterBase
42 | from .tweens import Tweens
43 | from pyramid_asyncio.aioinspect import is_generator
44 |
45 | log = logging.getLogger(__name__)
46 |
47 |
48 | @implementer(IRouter)
49 | class Router(RouterBase):
50 |
51 | def __init__(self, config):
52 | self.first_route = True
53 | self.config = config
54 | super().__init__(config.registry)
55 |
56 | @asyncio.coroutine
57 | def handle_request(self, request):
58 |
59 | attrs = request.__dict__
60 | registry = attrs['registry']
61 |
62 | request.request_iface = IRequest
63 | context = None
64 | routes_mapper = self.routes_mapper
65 | debug_routematch = self.debug_routematch
66 | adapters = registry.adapters
67 | has_listeners = registry.has_listeners
68 | notify = registry.notify
69 | logger = self.logger
70 | if request.registry.queryUtility(ISessionFactory) is not None and is_generator(request.session):
71 | request.session = yield from request.session
72 |
73 | has_listeners and notify(NewRequest(request))
74 | # find the root object
75 | root_factory = self.root_factory
76 | if routes_mapper is not None:
77 | info = routes_mapper(request)
78 | match, route = info['match'], info['route']
79 | if route is None:
80 | if debug_routematch:
81 | msg = ('no route matched for url %s' %
82 | request.url)
83 | logger and logger.debug(msg)
84 | else:
85 | attrs['matchdict'] = match
86 | attrs['matched_route'] = route
87 |
88 | if debug_routematch:
89 | msg = (
90 | 'route matched for url %s; '
91 | 'route_name: %r, '
92 | 'path_info: %r, '
93 | 'pattern: %r, '
94 | 'matchdict: %r, '
95 | 'predicates: %r' % (
96 | request.url,
97 | route.name,
98 | request.path_info,
99 | route.pattern,
100 | match,
101 | ', '.join([p.text() for p in route.predicates]))
102 | )
103 | logger and logger.debug(msg)
104 |
105 | request.request_iface = registry.queryUtility(
106 | IRouteRequest,
107 | name=route.name,
108 | default=IRequest)
109 |
110 | root_factory = route.factory or self.root_factory
111 |
112 | root = root_factory(request)
113 | attrs['root'] = root
114 |
115 | # find a context
116 | traverser = adapters.queryAdapter(root, ITraverser)
117 | if traverser is None:
118 | traverser = ResourceTreeTraverser(root)
119 | tdict = traverser(request)
120 |
121 | context, view_name, subpath, traversed, vroot, vroot_path = (
122 | tdict['context'],
123 | tdict['view_name'],
124 | tdict['subpath'],
125 | tdict['traversed'],
126 | tdict['virtual_root'],
127 | tdict['virtual_root_path']
128 | )
129 |
130 | attrs.update(tdict)
131 | has_listeners and notify(ContextFound(request))
132 |
133 | # find a view callable
134 | context_iface = providedBy(context)
135 | view_callable = adapters.lookup(
136 | (IViewClassifier, request.request_iface, context_iface),
137 | IView, name=view_name, default=None)
138 |
139 | # invoke the view callable
140 | if view_callable is None:
141 | if self.debug_notfound:
142 | msg = (
143 | 'debug_notfound of url %s; path_info: %r, '
144 | 'context: %r, view_name: %r, subpath: %r, '
145 | 'traversed: %r, root: %r, vroot: %r, '
146 | 'vroot_path: %r' % (
147 | request.url, request.path_info, context,
148 | view_name, subpath, traversed, root, vroot,
149 | vroot_path)
150 | )
151 | logger and logger.debug(msg)
152 | else:
153 | msg = request.path_info
154 | raise HTTPNotFound(msg)
155 | else:
156 | try:
157 | if asyncio.iscoroutinefunction(view_callable):
158 | response = yield from view_callable(context, request)
159 | else:
160 | response = view_callable(context, request)
161 | while is_generator(response):
162 | response = yield from response
163 | except PredicateMismatch:
164 | # look for other views that meet the predicate
165 | # criteria
166 | for iface in context_iface.__sro__[1:]:
167 | previous_view_callable = view_callable
168 | view_callable = adapters.lookup(
169 | (IViewClassifier, request.request_iface, iface),
170 | IView, name=view_name, default=None)
171 | # intermediate bases may lookup same view_callable
172 | if view_callable is previous_view_callable:
173 | continue
174 | if view_callable is not None:
175 | try:
176 | response = yield from view_callable(context,
177 | request)
178 | break
179 | except PredicateMismatch:
180 | pass
181 | else:
182 | raise
183 | return response
184 |
185 | @asyncio.coroutine
186 | def invoke_subrequest(self, request, use_tweens=False):
187 | """Obtain a response object from the Pyramid application based on
188 | information in the ``request`` object provided. The ``request``
189 | object must be an object that implements the Pyramid request
190 | interface (such as a :class:`pyramid.request.Request` instance). If
191 | ``use_tweens`` is ``True``, the request will be sent to the
192 | :term:`tween` in the tween stack closest to the request ingress. If
193 | ``use_tweens`` is ``False``, the request will be sent to the main
194 | router handler, and no tweens will be invoked.
195 |
196 | See the API for pyramid.request for complete documentation.
197 | """
198 | registry = self.registry
199 | has_listeners = self.registry.has_listeners
200 | notify = self.registry.notify
201 | threadlocals = {'registry': registry, 'request': request}
202 | manager = self.threadlocal_manager
203 | manager.push(threadlocals)
204 | request.registry = registry
205 | request.invoke_subrequest = self.invoke_subrequest
206 |
207 | if use_tweens:
208 | # XXX Recopy tweens state, registered my own ITweens does not
209 | # save the registred handler. Should invest more
210 | tween = Tweens()
211 | registred_tweens = registry.queryUtility(ITweens)
212 | if registred_tweens is not None:
213 | tween.explicit = registred_tweens.explicit
214 | tween.implicit = registred_tweens.implicit
215 | handle_request = tween(self.orig_handle_request, registry)
216 | else:
217 | handle_request = self.orig_handle_request
218 | else:
219 | handle_request = self.orig_handle_request
220 |
221 | try:
222 |
223 | try:
224 | extensions = self.request_extensions
225 | if extensions is not None:
226 | request._set_extensions(extensions)
227 |
228 | response = yield from handle_request(request)
229 |
230 | if request.response_callbacks:
231 | yield from request._process_response_callbacks(response)
232 |
233 | has_listeners and notify(NewResponse(request, response))
234 |
235 | # XXX excview_tween_factory is not a generator
236 | # Move a part of its code here
237 | except Exception as exc:
238 | # WARNING: do not assign the result of sys.exc_info() to a local
239 | # var here, doing so will cause a leak. We used to actually
240 | # explicitly delete both "exception" and "exc_info" from ``attrs``
241 | # in a ``finally:`` clause below, but now we do not because these
242 | # attributes are useful to upstream tweens. This actually still
243 | # apparently causes a reference cycle, but it is broken
244 | # successfully by the garbage collector (see
245 | # https://github.com/Pylons/pyramid/issues/1223).
246 | attrs= request.__dict__
247 | attrs['exc_info'] = sys.exc_info()
248 | attrs['exception'] = exc
249 | adapters = request.registry.adapters
250 |
251 | # clear old generated request.response, if any; it may
252 | # have been mutated by the view, and its state is not
253 | # sane (e.g. caching headers)
254 | if 'response' in attrs:
255 | del attrs['response']
256 | # we use .get instead of .__getitem__ below due to
257 | # https://github.com/Pylons/pyramid/issues/700
258 | request_iface = attrs.get('request_iface', IRequest)
259 | provides = providedBy(exc)
260 | for_ = (IExceptionViewClassifier, request_iface.combined, provides)
261 | view_callable = adapters.lookup(for_, IView, default=None)
262 | if view_callable is None:
263 | raise
264 | response = view_callable(exc, request)
265 | while is_generator(response):
266 | response = yield from response
267 |
268 | finally:
269 | if request.finished_callbacks:
270 | yield from request._process_finished_callbacks()
271 |
272 | finally:
273 | manager.pop()
274 |
275 | return response
276 |
277 |
278 | @asyncio.coroutine
279 | def __call__(self, environ, start_response):
280 | """
281 | Accept ``environ`` and ``start_response``; create a
282 | :term:`request` and route the request to a :app:`Pyramid`
283 | view based on introspection of :term:`view configuration`
284 | within the application registry; call ``start_response`` and
285 | return an iterable.
286 | """
287 | request = yield from self.request_factory(environ)
288 | try:
289 | response = yield from self.invoke_subrequest(request,
290 | use_tweens=True)
291 | except HTTPException as exc:
292 | response = exc
293 | response = response(request.environ, start_response)
294 | return response
295 |
296 | exit_handlers = []
297 |
298 | @asyncio.coroutine
299 | def open(self):
300 | settings = self.config.get_settings()
301 | aioincludes = aslist(settings.get('asyncio.includes', ''))
302 |
303 | for callable in aioincludes:
304 | try:
305 | module = self.config.maybe_dotted(callable)
306 | try:
307 | includeme = getattr(module, 'includeme')
308 | except AttributeError:
309 | raise ConfigurationError(
310 | "module %r has no attribute 'includeme'" % (module.__name__)
311 | )
312 |
313 | yield from includeme(self.config)
314 | except Exception:
315 | log.exception('{} raise an exception'.format(callable))
316 | self.config.commit()
317 |
318 | @asyncio.coroutine
319 | def close(self):
320 | for handler in self.exit_handlers:
321 | yield from handler()
322 |
323 |
324 | def add_exit_handler(config, handler):
325 | Router.exit_handlers.append(handler)
326 |
--------------------------------------------------------------------------------
/pyramid_asyncio/config.py:
--------------------------------------------------------------------------------
1 | """
2 | This code is a big copy/paste of code from pyramid and change the
3 | view in order to handle it as a coroutine
4 | """
5 | import warnings
6 | import asyncio
7 | import inspect
8 |
9 | from zope.interface import Interface, implementedBy, implementer
10 | from zope.interface.interfaces import IInterface
11 | from pyramid import renderers
12 | from pyramid.config import Configurator as ConfiguratorBase, global_registries
13 | from pyramid.config.views import (ViewDeriver as ViewDeriverBase,
14 | StaticURLInfo as StaticURLInfoBase,
15 | isexception,
16 | wraps_view,
17 | view_description)
18 | from pyramid.config.util import DEFAULT_PHASH, MAX_ORDER
19 | from pyramid.events import ApplicationCreated
20 | from pyramid.util import action_method, viewdefaults
21 | from pyramid.config.views import MultiView
22 | from pyramid.security import NO_PERMISSION_REQUIRED
23 | from pyramid.response import Response
24 | from pyramid.compat import string_types, is_nonstr_iter
25 | from pyramid.registry import predvalseq, Deferred
26 | from pyramid.exceptions import ConfigurationError
27 | from pyramid.interfaces import (IDefaultPermission, IRequest, IRouteRequest,
28 | IView, ISecuredView, IMultiView,
29 | IResponse,
30 | IRendererFactory, IViewClassifier,
31 | IExceptionResponse, IExceptionViewClassifier,
32 | )
33 | from pyramid.httpexceptions import HTTPForbidden
34 | from .router import Router, add_exit_handler
35 | from .request import RequestFactory
36 | from .aioinspect import is_generator
37 |
38 |
39 | @viewdefaults
40 | @action_method
41 | def add_coroutine_view(
42 | config,
43 | view=None,
44 | name="",
45 | for_=None,
46 | permission=None,
47 | request_type=None,
48 | route_name=None,
49 | request_method=None,
50 | request_param=None,
51 | containment=None,
52 | attr=None,
53 | renderer=None,
54 | wrapper=None,
55 | xhr=None,
56 | accept=None,
57 | header=None,
58 | path_info=None,
59 | custom_predicates=(),
60 | context=None,
61 | decorator=None,
62 | mapper=None,
63 | http_cache=None,
64 | match_param=None,
65 | check_csrf=None,
66 | **predicates):
67 | """ patched version of pyramid add_view that use asyncio coroutine """
68 | self = config
69 | if custom_predicates:
70 | warnings.warn(
71 | ('The "custom_predicates" argument to Configurator.add_view '
72 | 'is deprecated as of Pyramid 1.5. Use '
73 | '"config.add_view_predicate" and use the registered '
74 | 'view predicate as a predicate argument to add_view instead. '
75 | 'See "Adding A Third Party View, Route, or Subscriber '
76 | 'Predicate" in the "Hooks" chapter of the documentation '
77 | 'for more information.'),
78 | DeprecationWarning,
79 | stacklevel=4
80 | )
81 |
82 | view = self.maybe_dotted(view)
83 | # transform the view to a coroutine only in case it's really a coroutine
84 | if not asyncio.iscoroutinefunction(view) and is_generator(view):
85 | view = asyncio.coroutine(view)
86 |
87 | context = self.maybe_dotted(context)
88 | for_ = self.maybe_dotted(for_)
89 | containment = self.maybe_dotted(containment)
90 | mapper = self.maybe_dotted(mapper)
91 |
92 | def combine(*decorators):
93 | def decorated(view_callable):
94 | # reversed() is allows a more natural ordering in the api
95 | for decorator in reversed(decorators):
96 | view_callable = decorator(view_callable)
97 | return view_callable
98 | return decorated
99 |
100 | if is_nonstr_iter(decorator):
101 | decorator = combine(*map(self.maybe_dotted, decorator))
102 | else:
103 | decorator = self.maybe_dotted(decorator)
104 |
105 | if not view:
106 | if renderer:
107 | def view(context, request):
108 | return {}
109 | else:
110 | raise ConfigurationError('"view" was not specified and '
111 | 'no "renderer" specified')
112 |
113 | if request_type is not None:
114 | request_type = self.maybe_dotted(request_type)
115 | if not IInterface.providedBy(request_type):
116 | raise ConfigurationError(
117 | 'request_type must be an interface, not %s' % request_type)
118 |
119 | if context is None:
120 | context = for_
121 |
122 | r_context = context
123 | if r_context is None:
124 | r_context = Interface
125 | if not IInterface.providedBy(r_context):
126 | r_context = implementedBy(r_context)
127 |
128 | if isinstance(renderer, string_types):
129 | renderer = renderers.RendererHelper(
130 | name=renderer, package=self.package,
131 | registry = self.registry)
132 |
133 | if accept is not None:
134 | accept = accept.lower()
135 |
136 | introspectables = []
137 | pvals = predicates.copy()
138 | pvals.update(
139 | dict(
140 | xhr=xhr,
141 | request_method=request_method,
142 | path_info=path_info,
143 | request_param=request_param,
144 | header=header,
145 | accept=accept,
146 | containment=containment,
147 | request_type=request_type,
148 | match_param=match_param,
149 | check_csrf=check_csrf,
150 | custom=predvalseq(custom_predicates),
151 | )
152 | )
153 |
154 | def discrim_func():
155 | # We need to defer the discriminator until we know what the phash
156 | # is. It can't be computed any sooner because thirdparty
157 | # predicates may not yet exist when add_view is called.
158 | order, preds, phash = predlist.make(self, **pvals)
159 | view_intr.update({'phash':phash, 'order':order, 'predicates':preds})
160 | return ('view', context, name, route_name, phash)
161 |
162 | discriminator = Deferred(discrim_func)
163 |
164 | if inspect.isclass(view) and attr:
165 | view_desc = 'method %r of %s' % (
166 | attr, self.object_description(view))
167 | else:
168 | view_desc = self.object_description(view)
169 |
170 | tmpl_intr = None
171 |
172 | view_intr = self.introspectable('views',
173 | discriminator,
174 | view_desc,
175 | 'view')
176 | view_intr.update(
177 | dict(name=name,
178 | context=context,
179 | containment=containment,
180 | request_param=request_param,
181 | request_methods=request_method,
182 | route_name=route_name,
183 | attr=attr,
184 | xhr=xhr,
185 | accept=accept,
186 | header=header,
187 | path_info=path_info,
188 | match_param=match_param,
189 | check_csrf=check_csrf,
190 | callable=view,
191 | mapper=mapper,
192 | decorator=decorator,
193 | )
194 | )
195 | view_intr.update(**predicates)
196 | introspectables.append(view_intr)
197 | predlist = self.get_predlist('view')
198 |
199 | def register(permission=permission, renderer=renderer):
200 | # the discrim_func above is guaranteed to have been called already
201 | order = view_intr['order']
202 | preds = view_intr['predicates']
203 | phash = view_intr['phash']
204 | request_iface = IRequest
205 | if route_name is not None:
206 | request_iface = self.registry.queryUtility(IRouteRequest,
207 | name=route_name)
208 | if request_iface is None:
209 | # route configuration should have already happened in
210 | # phase 2
211 | raise ConfigurationError(
212 | 'No route named %s found for view registration' %
213 | route_name)
214 |
215 | if renderer is None:
216 | # use default renderer if one exists (reg'd in phase 1)
217 | if self.registry.queryUtility(IRendererFactory) is not None:
218 | renderer = renderers.RendererHelper(
219 | name=None,
220 | package=self.package,
221 | registry=self.registry
222 | )
223 |
224 | if permission is None:
225 | # intent: will be None if no default permission is registered
226 | # (reg'd in phase 1)
227 | permission = self.registry.queryUtility(IDefaultPermission)
228 |
229 | # added by discrim_func above during conflict resolving
230 | preds = view_intr['predicates']
231 | order = view_intr['order']
232 | phash = view_intr['phash']
233 |
234 | # __no_permission_required__ handled by _secure_view
235 | deriver = ViewDeriver(
236 | registry=self.registry,
237 | permission=permission,
238 | predicates=preds,
239 | attr=attr,
240 | renderer=renderer,
241 | wrapper_viewname=wrapper,
242 | viewname=name,
243 | accept=accept,
244 | order=order,
245 | phash=phash,
246 | package=self.package,
247 | mapper=mapper,
248 | decorator=decorator,
249 | http_cache=http_cache,
250 | )
251 | derived_view = deriver(view)
252 | derived_view.__discriminator__ = lambda *arg: discriminator
253 | # __discriminator__ is used by superdynamic systems
254 | # that require it for introspection after manual view lookup;
255 | # see also MultiView.__discriminator__
256 | view_intr['derived_callable'] = derived_view
257 |
258 | registered = self.registry.adapters.registered
259 |
260 | # A multiviews is a set of views which are registered for
261 | # exactly the same context type/request type/name triad. Each
262 | # consituent view in a multiview differs only by the
263 | # predicates which it possesses.
264 |
265 | # To find a previously registered view for a context
266 | # type/request type/name triad, we need to use the
267 | # ``registered`` method of the adapter registry rather than
268 | # ``lookup``. ``registered`` ignores interface inheritance
269 | # for the required and provided arguments, returning only a
270 | # view registered previously with the *exact* triad we pass
271 | # in.
272 |
273 | # We need to do this three times, because we use three
274 | # different interfaces as the ``provided`` interface while
275 | # doing registrations, and ``registered`` performs exact
276 | # matches on all the arguments it receives.
277 |
278 | old_view = None
279 |
280 | for view_type in (IView, ISecuredView, IMultiView):
281 | old_view = registered((IViewClassifier, request_iface,
282 | r_context), view_type, name)
283 | if old_view is not None:
284 | break
285 |
286 | isexc = isexception(context)
287 |
288 | def regclosure():
289 | if hasattr(derived_view, '__call_permissive__'):
290 | view_iface = ISecuredView
291 | else:
292 | view_iface = IView
293 | self.registry.registerAdapter(
294 | derived_view,
295 | (IViewClassifier, request_iface, context), view_iface, name
296 | )
297 | if isexc:
298 | self.registry.registerAdapter(
299 | derived_view,
300 | (IExceptionViewClassifier, request_iface, context),
301 | view_iface, name)
302 |
303 | is_multiview = IMultiView.providedBy(old_view)
304 | old_phash = getattr(old_view, '__phash__', DEFAULT_PHASH)
305 |
306 | if old_view is None:
307 | # - No component was yet registered for any of our I*View
308 | # interfaces exactly; this is the first view for this
309 | # triad.
310 | regclosure()
311 |
312 | elif (not is_multiview) and (old_phash == phash):
313 | # - A single view component was previously registered with
314 | # the same predicate hash as this view; this registration
315 | # is therefore an override.
316 | regclosure()
317 |
318 | else:
319 | # - A view or multiview was already registered for this
320 | # triad, and the new view is not an override.
321 |
322 | # XXX we could try to be more efficient here and register
323 | # a non-secured view for a multiview if none of the
324 | # multiview's consituent views have a permission
325 | # associated with them, but this code is getting pretty
326 | # rough already
327 | if is_multiview:
328 | multiview = old_view
329 | else:
330 | multiview = MultiView(name)
331 | old_accept = getattr(old_view, '__accept__', None)
332 | old_order = getattr(old_view, '__order__', MAX_ORDER)
333 | multiview.add(old_view, old_order, old_accept, old_phash)
334 | multiview.add(derived_view, order, accept, phash)
335 | for view_type in (IView, ISecuredView):
336 | # unregister any existing views
337 | self.registry.adapters.unregister(
338 | (IViewClassifier, request_iface, r_context),
339 | view_type, name=name)
340 | if isexc:
341 | self.registry.adapters.unregister(
342 | (IExceptionViewClassifier, request_iface,
343 | r_context), view_type, name=name)
344 | self.registry.registerAdapter(
345 | multiview,
346 | (IViewClassifier, request_iface, context),
347 | IMultiView, name=name)
348 | if isexc:
349 | self.registry.registerAdapter(
350 | multiview,
351 | (IExceptionViewClassifier, request_iface, context),
352 | IMultiView, name=name)
353 | renderer_type = getattr(renderer, 'type', None) # gard against None
354 | intrspc = self.introspector
355 | if (
356 | renderer_type is not None and
357 | tmpl_intr is not None and
358 | intrspc is not None and
359 | intrspc.get('renderer factories', renderer_type) is not None
360 | ):
361 | # allow failure of registered template factories to be deferred
362 | # until view execution, like other bad renderer factories; if
363 | # we tried to relate this to an existing renderer factory
364 | # without checking if it the factory actually existed, we'd end
365 | # up with a KeyError at startup time, which is inconsistent
366 | # with how other bad renderer registrations behave (they throw
367 | # a ValueError at view execution time)
368 | tmpl_intr.relate('renderer factories', renderer.type)
369 |
370 | if mapper:
371 | mapper_intr = self.introspectable(
372 | 'view mappers',
373 | discriminator,
374 | 'view mapper for %s' % view_desc,
375 | 'view mapper'
376 | )
377 | mapper_intr['mapper'] = mapper
378 | mapper_intr.relate('views', discriminator)
379 | introspectables.append(mapper_intr)
380 | if route_name:
381 | view_intr.relate('routes', route_name) # see add_route
382 | if renderer is not None and renderer.name and '.' in renderer.name:
383 | # the renderer is a template
384 | tmpl_intr = self.introspectable(
385 | 'templates',
386 | discriminator,
387 | renderer.name,
388 | 'template'
389 | )
390 | tmpl_intr.relate('views', discriminator)
391 | tmpl_intr['name'] = renderer.name
392 | tmpl_intr['type'] = renderer.type
393 | tmpl_intr['renderer'] = renderer
394 | introspectables.append(tmpl_intr)
395 | if permission is not None:
396 | # if a permission exists, register a permission introspectable
397 | perm_intr = self.introspectable(
398 | 'permissions',
399 | permission,
400 | permission,
401 | 'permission'
402 | )
403 | perm_intr['value'] = permission
404 | perm_intr.relate('views', discriminator)
405 | introspectables.append(perm_intr)
406 | self.action(discriminator, register, introspectables=introspectables)
407 |
408 |
409 | def make_asyncio_app(config):
410 | self = config
411 | self.set_request_factory(RequestFactory())
412 | self.commit()
413 | app = Router(self)
414 |
415 | # Allow tools like "pshell development.ini" to find the 'last'
416 | # registry configured.
417 | global_registries.add(self.registry)
418 |
419 | # Push the registry onto the stack in case any code that depends on
420 | # the registry threadlocal APIs used in listeners subscribed to the
421 | # IApplicationCreated event.
422 | self.manager.push({'registry':self.registry, 'request':None})
423 | try:
424 | self.registry.notify(ApplicationCreated(app))
425 | finally:
426 | self.manager.pop()
427 |
428 | return app
429 |
430 |
431 |
432 | class ViewDeriver(ViewDeriverBase):
433 |
434 | @wraps_view
435 | def secured_view(self, view):
436 | permission = self.kw.get('permission')
437 | if permission == NO_PERMISSION_REQUIRED:
438 | # allow views registered within configurations that have a
439 | # default permission to explicitly override the default
440 | # permission, replacing it with no permission at all
441 | permission = None
442 |
443 | wrapped_view = view
444 | if self.authn_policy and self.authz_policy and (permission is not None):
445 | @asyncio.coroutine
446 | def _permitted(context, request):
447 | principals = self.authn_policy.effective_principals(request)
448 | if is_generator(principals):
449 | principals = yield from principals
450 | return self.authz_policy.permits(context, principals,
451 | permission)
452 | @asyncio.coroutine
453 | def _secured_view(context, request):
454 | result = yield from _permitted(context, request)
455 | if result:
456 | return view(context, request)
457 | view_name = getattr(view, '__name__', view)
458 | msg = getattr(
459 | request, 'authdebug_message',
460 | 'Unauthorized: %s failed permission check' % view_name)
461 | raise HTTPForbidden(msg, result=result)
462 | _secured_view.__call_permissive__ = view
463 | _secured_view.__permitted__ = _permitted
464 | _secured_view.__permission__ = permission
465 | wrapped_view = _secured_view
466 |
467 | return wrapped_view
468 |
469 | def _rendered_view(self, view, view_renderer):
470 | """ Render an async view """
471 |
472 | @asyncio.coroutine
473 | def rendered_view(context, request):
474 |
475 | renderer = view_renderer
476 | result = view(context, request)
477 | if is_generator(result):
478 | result = yield from result
479 |
480 | if result.__class__ is Response: # potential common case
481 | response = result
482 | else:
483 | registry = self.registry
484 | # this must adapt, it can't do a simple interface check
485 | # (avoid trying to render webob responses)
486 | response = registry.queryAdapterOrSelf(result, IResponse)
487 | if response is None:
488 | attrs = getattr(request, '__dict__', {})
489 | if 'override_renderer' in attrs:
490 | # renderer overridden by newrequest event or other
491 | renderer_name = attrs.pop('override_renderer')
492 | renderer = renderers.RendererHelper(
493 | name=renderer_name,
494 | package=self.kw.get('package'),
495 | registry=registry)
496 | if '__view__' in attrs:
497 | view_inst = attrs.pop('__view__')
498 | else:
499 | view_inst = getattr(view, '__original_view__', view)
500 | response = renderer.render_view(request, result, view_inst,
501 | context)
502 | return response
503 |
504 | return rendered_view
505 |
506 | def _response_resolved_view(self, view):
507 | registry = self.registry
508 |
509 | def viewresult_to_response(context, request):
510 |
511 | result = yield from view(context, request)
512 | if result.__class__ is Response: # common case
513 | response = result
514 | else:
515 | response = registry.queryAdapterOrSelf(result, IResponse)
516 | if response is None:
517 | if result is None:
518 | append = (' You may have forgotten to return a value '
519 | 'from the view callable.')
520 | elif isinstance(result, dict):
521 | append = (' You may have forgotten to define a '
522 | 'renderer in the view configuration.')
523 | else:
524 | append = ''
525 |
526 | msg = ('Could not convert return value of the view '
527 | 'callable %s into a response object. '
528 | 'The value returned was %r.' + append)
529 |
530 | raise ValueError(msg % (view_description(view), result))
531 |
532 | return response
533 |
534 | return viewresult_to_response
535 |
536 |
537 | def includeme(config):
538 | # Does not work as expected :/
539 | # config.registry.registerUtility(Tweens(), ITweens)
540 |
541 | config.add_directive('add_coroutine_view', add_coroutine_view)
542 | config.add_directive('make_asyncio_app', make_asyncio_app)
543 | config.add_directive('add_exit_handler', add_exit_handler)
544 |
--------------------------------------------------------------------------------