├── 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 | --------------------------------------------------------------------------------