├── aiopyramid ├── gunicorn │ ├── __init__.py │ └── worker.py ├── websocket │ ├── __init__.py │ ├── exceptions.py │ ├── config │ │ ├── __init__.py │ │ ├── uwsgi.py │ │ └── gunicorn.py │ ├── helpers.py │ └── view.py ├── scaffolds │ ├── aio_starter │ │ ├── README.rst_tmpl │ │ ├── CHANGES.rst_tmpl │ │ ├── MANIFEST.in_tmpl │ │ ├── +package+ │ │ │ ├── views.py_tmpl │ │ │ ├── tests.py_tmpl │ │ │ └── __init__.py_tmpl │ │ ├── setup.py_tmpl │ │ └── development.ini_tmpl │ ├── aio_websocket │ │ ├── README.rst_tmpl │ │ ├── CHANGES.rst_tmpl │ │ ├── MANIFEST.in_tmpl │ │ ├── +package+ │ │ │ ├── __init__.py_tmpl │ │ │ ├── views.py_tmpl │ │ │ ├── tests.py_tmpl │ │ │ └── templates │ │ │ │ └── home.jinja2 │ │ ├── development.ini_tmpl │ │ └── setup.py_tmpl │ └── __init__.py ├── exceptions.py ├── __init__.py ├── tweens.py ├── auth.py ├── config.py ├── traversal.py └── helpers.py ├── COPYRIGHT ├── docs ├── modules.rst ├── tables.rst ├── requirements.txt ├── aiopyramid.scaffolds.rst ├── tests.rst ├── aiopyramid.gunicorn.rst ├── aiopyramid.websocket.config.rst ├── glossary.rst ├── aiopyramid.websocket.rst ├── aiopyramid.rst ├── index.rst ├── approach.rst ├── Makefile ├── conf.py ├── tutorial.rst └── features.rst ├── MANIFEST.in ├── tox.ini ├── .gitignore ├── README.rst ├── LICENSE ├── setup.py ├── tests ├── test_traversal.py ├── test_auth.py ├── test_tweens.py └── test_helpers.py └── CHANGES.rst /aiopyramid/gunicorn/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /aiopyramid/websocket/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /aiopyramid/scaffolds/aio_starter/README.rst_tmpl: -------------------------------------------------------------------------------- 1 | {{project}} 2 | 3 | -------------------------------------------------------------------------------- /aiopyramid/scaffolds/aio_websocket/README.rst_tmpl: -------------------------------------------------------------------------------- 1 | {{project}} 2 | 3 | -------------------------------------------------------------------------------- /COPYRIGHT: -------------------------------------------------------------------------------- 1 | Copyright © 2014, Guillaume Gauvrit 2 | Copyright © 2014, Jason Housley 3 | 4 | -------------------------------------------------------------------------------- /docs/modules.rst: -------------------------------------------------------------------------------- 1 | API 2 | === 3 | 4 | .. toctree:: 5 | :maxdepth: 4 6 | 7 | aiopyramid 8 | -------------------------------------------------------------------------------- /aiopyramid/scaffolds/aio_starter/CHANGES.rst_tmpl: -------------------------------------------------------------------------------- 1 | Changelog 2 | ========= 3 | 4 | 0.0 5 | --- 6 | 7 | * Initial release 8 | -------------------------------------------------------------------------------- /aiopyramid/scaffolds/aio_websocket/CHANGES.rst_tmpl: -------------------------------------------------------------------------------- 1 | Changelog 2 | ========= 3 | 4 | 0.0 5 | --- 6 | 7 | * Initial release 8 | -------------------------------------------------------------------------------- /aiopyramid/websocket/exceptions.py: -------------------------------------------------------------------------------- 1 | 2 | import greenlet 3 | 4 | 5 | class WebsocketClosed(greenlet.GreenletExit): 6 | pass 7 | -------------------------------------------------------------------------------- /aiopyramid/exceptions.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | class ScopeError(Exception): 4 | """ 5 | Error indicating execution in the wrong greenlet. 6 | """ 7 | -------------------------------------------------------------------------------- /docs/tables.rst: -------------------------------------------------------------------------------- 1 | Indices and Tables 2 | ================== 3 | 4 | * :ref:`genindex` 5 | * :ref:`modindex` 6 | * :ref:`search` 7 | * :doc:`glossary` 8 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include *.txt *.ini *.cfg *.rst LICENSE COPYRIGHT 2 | graft aiopyramid/scaffolds 3 | graft aiopyramid/gunicorn 4 | graft aiopyramid/websocket 5 | graft tests 6 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | aiohttp==3.7.4 2 | aiopyramid>=0.1.0 3 | greenlet==0.4.2 4 | gunicorn==19.5.0 5 | pyramid==1.5.1 6 | uWSGI>=2.0.7 7 | websockets>=2.2 8 | alabaster==0.7.4 9 | -------------------------------------------------------------------------------- /aiopyramid/scaffolds/aio_starter/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 -------------------------------------------------------------------------------- /aiopyramid/scaffolds/aio_websocket/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 -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py33,py34,py35,py36 3 | 4 | [testenv] 5 | deps = 6 | pytest 7 | pytest-pep8 8 | commands = py.test --pep8 9 | 10 | [pytest] 11 | pep8ignore = 12 | docs/* ALL 13 | -------------------------------------------------------------------------------- /docs/aiopyramid.scaffolds.rst: -------------------------------------------------------------------------------- 1 | aiopyramid.scaffolds package 2 | ============================ 3 | 4 | Module contents 5 | --------------- 6 | 7 | .. automodule:: aiopyramid.scaffolds 8 | :members: 9 | :undoc-members: 10 | :show-inheritance: 11 | -------------------------------------------------------------------------------- /aiopyramid/websocket/config/__init__.py: -------------------------------------------------------------------------------- 1 | __all__ = [] 2 | 3 | try: 4 | from .uwsgi import * 5 | __all__.append('UWSGIWebsocketMapper') 6 | except ImportError: 7 | pass 8 | 9 | try: 10 | from .gunicorn import * 11 | __all__.append('WebsocketMapper') 12 | except ImportError: 13 | pass 14 | -------------------------------------------------------------------------------- /aiopyramid/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Run pyramid app using asyncio 3 | """ 4 | 5 | from .config import CoroutineOrExecutorMapper 6 | 7 | 8 | def includeme(config): 9 | """ 10 | Setup the basic configuration to run :ref:`Pyramid ` 11 | with :mod:`asyncio`. 12 | """ 13 | 14 | config.set_view_mapper(CoroutineOrExecutorMapper) 15 | -------------------------------------------------------------------------------- /aiopyramid/scaffolds/__init__.py: -------------------------------------------------------------------------------- 1 | from pyramid.scaffolds import PyramidTemplate 2 | 3 | 4 | class AioStarterTemplate(PyramidTemplate): 5 | _template_dir = 'aio_starter' 6 | summary = 'Pyramid project using asyncio' 7 | 8 | 9 | class AioWebsocketTemplate(PyramidTemplate): 10 | _template_dir = 'aio_websocket' 11 | summary = 'Aiopyramid project with websocket-based view' 12 | -------------------------------------------------------------------------------- /aiopyramid/scaffolds/aio_starter/+package+/views.py_tmpl: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | from pyramid.view import view_config 4 | 5 | 6 | @view_config(route_name='say_hello', renderer='string') 7 | @asyncio.coroutine 8 | def say_hello(request): 9 | wait_time = float(request.params.get('sleep', 0.1)) 10 | yield from asyncio.sleep(wait_time) 11 | return "Welcome to Pyramid with Asyncio." 12 | -------------------------------------------------------------------------------- /docs/tests.rst: -------------------------------------------------------------------------------- 1 | Tests 2 | ===== 3 | 4 | Core functionality is backed by tests. The ``Aiopyramid`` requires `pytest`_. To run the 5 | tests, grab the code on `github`_, install `pytest`_, and run it like so: 6 | 7 | :: 8 | 9 | git clone https://github.com/housleyjk/aiopyramid 10 | cd aiopyramid 11 | pip install pytest 12 | py.test 13 | 14 | .. _pytest: http://pytest.org 15 | .. _github: https://github.com/housleyjk/aiopyramid 16 | -------------------------------------------------------------------------------- /aiopyramid/scaffolds/aio_starter/+package+/tests.py_tmpl: -------------------------------------------------------------------------------- 1 | import unittest 2 | import asyncio 3 | 4 | from pyramid import testing 5 | 6 | 7 | class HelloTestCase(unittest.TestCase): 8 | 9 | def test_demo_view(self): 10 | from .views import say_hello 11 | 12 | request = testing.DummyRequest() 13 | info = asyncio.get_event_loop().run_until_complete(say_hello(request)) 14 | self.assertEqual(info, 'Welcome to Pyramid with Asyncio.') 15 | -------------------------------------------------------------------------------- /aiopyramid/websocket/helpers.py: -------------------------------------------------------------------------------- 1 | 2 | from .exceptions import WebsocketClosed 3 | 4 | 5 | def ignore_websocket_closed(app): 6 | """ Wrapper for ignoring closed websockets. """ 7 | 8 | def _call_app_ignoring_ws_closed(environ, start_response): 9 | try: 10 | return app(environ, start_response) 11 | except WebsocketClosed as e: 12 | if e.__cause__: 13 | raise 14 | return ('') 15 | return _call_app_ignoring_ws_closed 16 | -------------------------------------------------------------------------------- /docs/aiopyramid.gunicorn.rst: -------------------------------------------------------------------------------- 1 | aiopyramid.gunicorn package 2 | =========================== 3 | 4 | Submodules 5 | ---------- 6 | 7 | aiopyramid.gunicorn.worker module 8 | --------------------------------- 9 | 10 | .. automodule:: aiopyramid.gunicorn.worker 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | 16 | Module contents 17 | --------------- 18 | 19 | .. automodule:: aiopyramid.gunicorn 20 | :members: 21 | :undoc-members: 22 | :show-inheritance: 23 | -------------------------------------------------------------------------------- /aiopyramid/scaffolds/aio_starter/+package+/__init__.py_tmpl: -------------------------------------------------------------------------------- 1 | import logging.config 2 | 3 | from pyramid.config import Configurator 4 | 5 | 6 | def main(global_config, **settings): 7 | """ This function returns a Pyramid WSGI application. 8 | """ 9 | 10 | # support logging in python3 11 | logging.config.fileConfig( 12 | settings['logging.config'], 13 | disable_existing_loggers=False 14 | ) 15 | 16 | config = Configurator(settings=settings) 17 | config.add_route('say_hello', '/') 18 | config.scan() 19 | return config.make_wsgi_app() 20 | -------------------------------------------------------------------------------- /aiopyramid/scaffolds/aio_websocket/+package+/__init__.py_tmpl: -------------------------------------------------------------------------------- 1 | import logging.config 2 | 3 | from pyramid.config import Configurator 4 | 5 | 6 | def main(global_config, **settings): 7 | """ This function returns a Pyramid WSGI application. 8 | """ 9 | 10 | # support logging in python3 11 | logging.config.fileConfig( 12 | settings['logging.config'], 13 | disable_existing_loggers=False 14 | ) 15 | 16 | config = Configurator(settings=settings) 17 | config.add_route('home', '/') 18 | config.add_route('echo', '/echo') 19 | config.scan() 20 | return config.make_wsgi_app() 21 | -------------------------------------------------------------------------------- /aiopyramid/scaffolds/aio_websocket/+package+/views.py_tmpl: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | from pyramid.view import view_config 4 | from aiopyramid.websocket.config import WebsocketMapper 5 | 6 | 7 | @view_config(route_name='home', renderer='{{project}}:templates/home.jinja2') 8 | @asyncio.coroutine 9 | def home(request): 10 | wait_time = float(request.params.get('sleep', 0.1)) 11 | yield from asyncio.sleep(wait_time) 12 | return {'title': '{{project}} websocket test', 'wait_time': wait_time} 13 | 14 | 15 | @view_config(route_name='echo', mapper=WebsocketMapper) 16 | @asyncio.coroutine 17 | def echo(ws): 18 | while True: 19 | message = yield from ws.recv() 20 | if message is None: 21 | break 22 | yield from ws.send(message) 23 | -------------------------------------------------------------------------------- /docs/aiopyramid.websocket.config.rst: -------------------------------------------------------------------------------- 1 | aiopyramid.websocket.config package 2 | =================================== 3 | 4 | Submodules 5 | ---------- 6 | 7 | aiopyramid.websocket.config.gunicorn module 8 | ------------------------------------------- 9 | 10 | .. automodule:: aiopyramid.websocket.config.gunicorn 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | aiopyramid.websocket.config.uwsgi module 16 | ---------------------------------------- 17 | 18 | .. automodule:: aiopyramid.websocket.config.uwsgi 19 | :members: 20 | :undoc-members: 21 | :show-inheritance: 22 | 23 | 24 | Module contents 25 | --------------- 26 | 27 | .. automodule:: aiopyramid.websocket.config 28 | :members: 29 | :undoc-members: 30 | :show-inheritance: 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | bin/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | eggs/ 16 | lib/ 17 | lib64/ 18 | parts/ 19 | sdist/ 20 | var/ 21 | *.egg-info/ 22 | .installed.cfg 23 | *.egg 24 | 25 | # Installer logs 26 | pip-log.txt 27 | pip-delete-this-directory.txt 28 | 29 | # Unit test / coverage reports 30 | htmlcov/ 31 | .tox/ 32 | .coverage 33 | .cache 34 | nosetests.xml 35 | coverage.xml 36 | 37 | # Translations 38 | *.mo 39 | 40 | # Mr Developer 41 | .mr.developer.cfg 42 | .project 43 | .pydevproject 44 | 45 | # Rope 46 | .ropeproject 47 | 48 | # Django stuff: 49 | *.log 50 | *.pot 51 | 52 | # Sphinx documentation 53 | docs/_build/ 54 | 55 | # vim 56 | *.swp 57 | *.swo 58 | -------------------------------------------------------------------------------- /docs/glossary.rst: -------------------------------------------------------------------------------- 1 | Glossary 2 | ======== 3 | 4 | .. glossary:: 5 | :sorted: 6 | 7 | websocket 8 | WebSocket is a protocol providing full-duplex communications channels over a single TCP connection. 9 | See `websockets`_ for a simple python library to get started. 10 | 11 | coroutine 12 | A coroutine is a generator that follows certain conventions in :mod:`asyncio`. See `asyncio docs`_. 13 | 14 | synchronized coroutine 15 | A coroutine that has been wrapped or decorated by :func:`~aiopyramid.helpers.synchronize` so that 16 | it can be executed without using ``yield from`` in a child :term:`greenlet`. Synchronized coroutines are 17 | used to bridge the gap between framework code which expects normal Python functions and application 18 | code that uses coroutines. 19 | 20 | .. _websockets: http://aaugustin.github.io/websockets/ 21 | .. _asyncio docs: https://docs.python.org/3/library/asyncio-task.html#coroutine 22 | -------------------------------------------------------------------------------- /docs/aiopyramid.websocket.rst: -------------------------------------------------------------------------------- 1 | aiopyramid.websocket package 2 | ============================ 3 | 4 | Subpackages 5 | ----------- 6 | 7 | .. toctree:: 8 | 9 | aiopyramid.websocket.config 10 | 11 | Submodules 12 | ---------- 13 | 14 | aiopyramid.websocket.exceptions module 15 | -------------------------------------- 16 | 17 | .. automodule:: aiopyramid.websocket.exceptions 18 | :members: 19 | :undoc-members: 20 | :show-inheritance: 21 | 22 | aiopyramid.websocket.helpers module 23 | ----------------------------------- 24 | 25 | .. automodule:: aiopyramid.websocket.helpers 26 | :members: 27 | :undoc-members: 28 | :show-inheritance: 29 | 30 | aiopyramid.websocket.view module 31 | -------------------------------- 32 | 33 | .. automodule:: aiopyramid.websocket.view 34 | :members: 35 | :undoc-members: 36 | :show-inheritance: 37 | 38 | 39 | Module contents 40 | --------------- 41 | 42 | .. automodule:: aiopyramid.websocket 43 | :members: 44 | :undoc-members: 45 | :show-inheritance: 46 | -------------------------------------------------------------------------------- /docs/aiopyramid.rst: -------------------------------------------------------------------------------- 1 | aiopyramid 2 | ========== 3 | 4 | Subpackages 5 | ----------- 6 | 7 | .. toctree:: 8 | 9 | aiopyramid.gunicorn 10 | aiopyramid.websocket 11 | 12 | Module contents 13 | --------------- 14 | 15 | .. automodule:: aiopyramid 16 | :members: 17 | :undoc-members: 18 | :show-inheritance: 19 | 20 | Submodules 21 | ---------- 22 | 23 | aiopyramid.config module 24 | ------------------------ 25 | 26 | .. automodule:: aiopyramid.config 27 | :members: 28 | :undoc-members: 29 | :show-inheritance: 30 | 31 | aiopyramid.exceptions module 32 | ---------------------------- 33 | 34 | .. automodule:: aiopyramid.exceptions 35 | :members: 36 | :undoc-members: 37 | :show-inheritance: 38 | 39 | aiopyramid.helpers module 40 | ------------------------- 41 | 42 | .. automodule:: aiopyramid.helpers 43 | :members: 44 | :undoc-members: 45 | :show-inheritance: 46 | 47 | aiopyramid.traversal module 48 | --------------------------- 49 | 50 | .. automodule:: aiopyramid.traversal 51 | 52 | aiopyramid.tweens module 53 | ------------------------ 54 | 55 | .. automodule:: aiopyramid.tweens 56 | :members: 57 | :undoc-members: 58 | :show-inheritance: 59 | 60 | 61 | -------------------------------------------------------------------------------- /aiopyramid/websocket/view.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | 4 | class WebsocketConnectionView: 5 | """ :term:`view callable` for websocket connections. """ 6 | 7 | def __init__(self, context, request): 8 | self.context = context 9 | self.request = request 10 | 11 | @asyncio.coroutine 12 | def __call__(self, ws): 13 | self.ws = ws 14 | yield from self.on_open() 15 | while True: 16 | message = yield from self.ws.recv() 17 | if message is None: 18 | yield from self.on_close() 19 | break 20 | yield from self.on_message(message) 21 | 22 | @asyncio.coroutine 23 | def send(self, message): 24 | yield from self.ws.send(message) 25 | 26 | @asyncio.coroutine 27 | def on_message(self, message): 28 | """ 29 | Callback called when a message is received. 30 | Default is a noop. 31 | """ 32 | pass 33 | 34 | @asyncio.coroutine 35 | def on_open(self): 36 | """ 37 | Callback called when the connection is first established. 38 | Default is a noop. 39 | """ 40 | 41 | @asyncio.coroutine 42 | def on_close(self): 43 | """ 44 | Callback called when the connection is closed. 45 | Default is a noop. 46 | """ 47 | pass 48 | -------------------------------------------------------------------------------- /aiopyramid/scaffolds/aio_websocket/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.includes = 10 | aiopyramid 11 | pyramid_jinja2 12 | 13 | # for py3 14 | logging.config = %(here)s/development.ini 15 | 16 | [uwsgi] 17 | http-socket = 0.0.0.0:6543 18 | workers = 1 19 | plugins = 20 | asyncio = 50 ;number of workers 21 | greenlet 22 | 23 | [server:main] 24 | use = egg:gunicorn#main 25 | host = 0.0.0.0 26 | port = 6543 27 | worker_class = aiopyramid.gunicorn.worker.AsyncGunicornWorker 28 | 29 | [loggers] 30 | keys = root, asyncio, {{project}}, gunicorn 31 | 32 | [handlers] 33 | keys = console 34 | 35 | [formatters] 36 | keys = generic 37 | 38 | [logger_root] 39 | level = INFO 40 | handlers = console 41 | 42 | [logger_asyncio] 43 | level = WARN 44 | handlers = 45 | qualname = asyncio 46 | 47 | [logger_gunicorn] 48 | level = INFO 49 | handlers = 50 | qualname = gunicorn 51 | 52 | [logger_{{project}}] 53 | level = DEBUG 54 | handlers = 55 | qualname = {{project}} 56 | 57 | 58 | [handler_console] 59 | class = StreamHandler 60 | args = (sys.stderr,) 61 | level = NOTSET 62 | formatter = generic 63 | 64 | [formatter_generic] 65 | format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s 66 | -------------------------------------------------------------------------------- /aiopyramid/scaffolds/aio_starter/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("aiopyramid 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 | 'aiopyramid[gunicorn]', 20 | ] 21 | 22 | setup( 23 | name=NAME, 24 | version='0.0', 25 | description='{{project}}', 26 | long_description=README + '\n\n' + CHANGES, 27 | classifiers=[ 28 | "Programming Language :: Python", 29 | "Programming Language :: Python :: 3.3", 30 | "Programming Language :: Python :: 3.4", 31 | "Framework :: Pyramid", 32 | "Topic :: Internet :: WWW/HTTP", 33 | "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", 34 | ], 35 | author='', 36 | author_email='', 37 | url='', 38 | keywords='aiopyramid asyncio web wsgi pylons pyramid', 39 | packages=find_packages(), 40 | include_package_data=True, 41 | zip_safe=False, 42 | test_suite=NAME, 43 | install_requires=requires, 44 | entry_points="""\ 45 | [paste.app_factory] 46 | main = {{package}}:main 47 | """, 48 | ) 49 | -------------------------------------------------------------------------------- /aiopyramid/scaffolds/aio_websocket/+package+/tests.py_tmpl: -------------------------------------------------------------------------------- 1 | import unittest 2 | import asyncio 3 | 4 | import websockets 5 | from pyramid import testing 6 | 7 | 8 | class HomeTestCase(unittest.TestCase): 9 | 10 | def test_home_view(self): 11 | from .views import home 12 | 13 | request = testing.DummyRequest() 14 | info = asyncio.get_event_loop().run_until_complete(home(request)) 15 | self.assertEqual(info['title'], 'aiotutorial websocket test') 16 | 17 | 18 | class WSTest(unittest.TestCase): 19 | """ Test aiopyramid websocket view. """ 20 | 21 | def setUp(self): 22 | self.loop = asyncio.get_event_loop() 23 | 24 | def test_echo_view(self): 25 | 26 | @asyncio.coroutine 27 | def _websockets_compat_wrapper(ws, path): 28 | """ wrapper to ignore the path argument used witn websockets.serve """ 29 | from .views import echo 30 | yield from echo(ws) 31 | 32 | self.loop.run_until_complete(websockets.serve(_websockets_compat_wrapper, 'localhost', 8765)) 33 | 34 | @asyncio.coroutine 35 | def _echo_view_client(): 36 | ws = yield from websockets.connect('ws://localhost:8765/') 37 | for x in range(20): 38 | yield from ws.send(str(x)) 39 | y = yield from ws.recv() 40 | int(y) == x 41 | self.assertEqual(int(y), x) 42 | self.loop.run_until_complete(_echo_view_client()) 43 | -------------------------------------------------------------------------------- /aiopyramid/scaffolds/aio_websocket/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("aiopyramid 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 | 'aiopyramid[gunicorn]', 20 | 'pyramid_jinja2', 21 | ] 22 | 23 | setup( 24 | name=NAME, 25 | version='0.0', 26 | description='{{project}}', 27 | long_description=README + '\n\n' + CHANGES, 28 | classifiers=[ 29 | "Programming Language :: Python", 30 | "Programming Language :: Python :: 3.3", 31 | "Programming Language :: Python :: 3.4", 32 | "Framework :: Pyramid", 33 | "Topic :: Internet :: WWW/HTTP", 34 | "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", 35 | ], 36 | author='', 37 | author_email='', 38 | url='', 39 | keywords='aiopyramid asyncio web wsgi pylons pyramid', 40 | packages=find_packages(), 41 | include_package_data=True, 42 | zip_safe=False, 43 | test_suite=NAME, 44 | install_requires=requires, 45 | entry_points="""\ 46 | [paste.app_factory] 47 | main = {{package}}:main 48 | """, 49 | ) 50 | -------------------------------------------------------------------------------- /aiopyramid/scaffolds/aio_starter/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 | aiopyramid 16 | 17 | # for py3 18 | logging.config = %(here)s/development.ini 19 | 20 | [uwsgi] 21 | http-socket = 0.0.0.0:6543 22 | workers = 1 23 | plugins = 24 | asyncio = 50 ;number of workers 25 | greenlet 26 | 27 | [server:main] 28 | use = egg:gunicorn#main 29 | host = 0.0.0.0 30 | port = 6543 31 | worker_class = aiopyramid.gunicorn.worker.AsyncGunicornWorker 32 | 33 | [loggers] 34 | keys = root, asyncio, {{project}} 35 | 36 | [handlers] 37 | keys = console 38 | 39 | [formatters] 40 | keys = generic 41 | 42 | [logger_root] 43 | level = INFO 44 | handlers = console 45 | 46 | [logger_asyncio] 47 | level = WARN 48 | handlers = 49 | qualname = asyncio 50 | 51 | [logger_gunicorn] 52 | level = INFO 53 | handlers = 54 | qualname = gunicorn 55 | 56 | [logger_{{project}}] 57 | level = DEBUG 58 | handlers = 59 | qualname = {{project}} 60 | 61 | 62 | [handler_console] 63 | class = StreamHandler 64 | args = (sys.stderr,) 65 | level = NOTSET 66 | formatter = generic 67 | 68 | [formatter_generic] 69 | format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s 70 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Introduction 2 | ============ 3 | 4 | A library for leveraging pyramid infrastructure asynchronously using the new ``asyncio``. 5 | 6 | ``Aiopyramid`` provides tools for making web applications with ``Pyramid`` and ``asyncio``. 7 | It will not necessarily make your application run faster. Instead, it gives you some tools 8 | and patterns to build an application on asynchronous servers. 9 | Bear in mind that you will need to use asynchronous libraries for io where appropriate. 10 | 11 | Since this library is built on relatively new technology, it is not intended for production use. 12 | 13 | Getting Started 14 | --------------- 15 | 16 | ``Aiopyramid`` includes a scaffold that creates a "hello world" application, 17 | check it out. The scaffold is designed to work with either `gunicorn`_ 18 | via a custom worker or `uWSGI`_ via the `uWSGI asyncio plugin`_. We will be 19 | be using gunicorn and installing aiopyramid along with its defined gunicorn 20 | extras for this example: 21 | 22 | :: 23 | 24 | pip install aiopyramid[gunicorn] gunicorn 25 | pcreate -s aio_starter 26 | cd 27 | pip install -e . 28 | gunicorn --paste development.ini 29 | 30 | There is also a ``websocket`` scaffold `aio_websocket` for those who basic tools for setting up 31 | a ``websocket`` server. 32 | 33 | Documentation 34 | ------------- 35 | 36 | Full documentation for ``Aiopyramid`` can be found `here`_. 37 | 38 | .. _gunicorn: http://gunicorn.org 39 | .. _uWSGI: https://github.com/unbit/uwsgi 40 | .. _uWSGI asyncio plugin: http://uwsgi-docs.readthedocs.org/en/latest/asyncio.html 41 | .. _here: http://aiopyramid.readthedocs.io/ 42 | -------------------------------------------------------------------------------- /aiopyramid/scaffolds/aio_websocket/+package+/templates/home.jinja2: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{title}} 5 | 62 | 63 |

{{title}}

64 |
65 | -------------------------------------------------------------------------------- /aiopyramid/tweens.py: -------------------------------------------------------------------------------- 1 | """ 2 | The aiopyramid.tweens module is deprecated. See example in the docs: 3 | http://aiopyramid.readthedocs.io/features.html#tweens. 4 | """ 5 | 6 | import asyncio 7 | import warnings 8 | 9 | from .helpers import synchronize 10 | 11 | warnings.warn(__doc__, DeprecationWarning) 12 | 13 | 14 | def coroutine_logger_tween_factory(handler, registry): 15 | """ 16 | Example of an asynchronous tween that delegates a synchronous function to 17 | a child thread. This tween asynchronously logs all requests and responses. 18 | """ 19 | 20 | # We use the synchronize decorator because we will call this 21 | # coroutine from a normal python context 22 | @synchronize 23 | # this is a coroutine 24 | @asyncio.coroutine 25 | def _async_print(content): 26 | # print doesn't really need to be run in a separate thread 27 | # but it works for demonstration purposes 28 | 29 | yield from asyncio.get_event_loop().run_in_executor( 30 | None, 31 | print, 32 | content 33 | ) 34 | 35 | def coroutine_logger_tween(request): 36 | # The following calls are guaranteed to happen in order but they do not 37 | # block the event loop 38 | 39 | # print the request on the aio event loop without needing to say yield 40 | # at this point, other coroutines and requests can be handled 41 | _async_print(request) 42 | 43 | # get response, this should be done in this greenlet 44 | # and not as a coroutine because this will call 45 | # the next tween and subsequently yield if necessary 46 | response = handler(request) 47 | 48 | # print the response on the aio event loop 49 | _async_print(request) 50 | 51 | # return response after logging is done 52 | return response 53 | 54 | return coroutine_logger_tween 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 | 42 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | from setuptools import setup, find_packages 4 | 5 | py_version = sys.version_info[:2] 6 | if py_version < (3, 3): 7 | raise Exception("aiopyramid requires Python >= 3.3.") 8 | 9 | here = os.path.abspath(os.path.dirname(__file__)) 10 | 11 | with open(os.path.join(here, 'README.rst')) as readme: 12 | README = readme.read() 13 | with open(os.path.join(here, 'CHANGES.rst')) as changes: 14 | CHANGES = changes.read() 15 | 16 | 17 | requires = [ 18 | 'pyramid', 19 | 'greenlet', 20 | ] 21 | 22 | if py_version < (3, 4): 23 | requires.append('asyncio') 24 | 25 | setup( 26 | name='aiopyramid', 27 | version='0.4.2', 28 | description='Tools for running pyramid using asyncio.', 29 | long_description=README + '\n\n\n\n' + CHANGES, 30 | classifiers=[ 31 | "Programming Language :: Python", 32 | "Programming Language :: Python :: 3.3", 33 | "Programming Language :: Python :: 3.4", 34 | "Programming Language :: Python :: 3.5", 35 | "Framework :: Pyramid", 36 | "Topic :: Internet :: WWW/HTTP", 37 | "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", 38 | "Intended Audience :: Developers", 39 | "License :: Repoze Public License", 40 | ], 41 | author='Jason Housley', 42 | author_email='housleyjk@gmail.com', 43 | url='https://github.com/housleyjk/aiopyramid', 44 | keywords='pyramid asyncio greenlet wsgi', 45 | packages=find_packages(), 46 | include_package_data=True, 47 | zip_safe=False, 48 | install_requires=requires, 49 | extras_require={ 50 | 'gunicorn': ['gunicorn>=19.1.1', 'aiohttp>=2.0.0,<3', 'aiohttp_wsgi>=0.7.0,<=0.7.1', 'websockets'], 51 | }, 52 | license="BSD-derived (http://www.repoze.org/LICENSE.txt)", 53 | entry_points="""\ 54 | [pyramid.scaffold] 55 | aio_starter=aiopyramid.scaffolds:AioStarterTemplate 56 | aio_websocket=aiopyramid.scaffolds:AioWebsocketTemplate 57 | """ 58 | ) 59 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | Aiopyramid 2 | ========== 3 | 4 | A library for leveraging pyramid infrastructure asynchronously using the new :mod:`asyncio`. 5 | 6 | ``Aiopyramid`` provides tools for making web applications with :ref:`Pyramid ` and :mod:`asyncio`. 7 | It will not necessarily make your application run faster. Instead, it gives you some tools 8 | and patterns to build an application on asynchronous servers that handle many active connections. 9 | 10 | This is not a fork of :ref:`Pyramid ` and it does not rewrite 11 | any :ref:`Pyramid ` code to run asynchronously! 12 | :ref:`Pyramid ` is just that flexible. 13 | 14 | Getting Started 15 | --------------- 16 | 17 | ``Aiopyramid`` includes a scaffold that creates a "hello world" application, 18 | check it out! The scaffold is designed to work with either `gunicorn`_ 19 | via a custom worker or `uWSGI`_ via the `uWSGI asyncio plugin`_. 20 | 21 | For example: 22 | 23 | :: 24 | 25 | pip install aiopyramid gunicorn 26 | pcreate -s aio_starter 27 | cd 28 | python setup.py develop 29 | gunicorn --paste development.ini 30 | 31 | There is also a :term:`websocket` scaffold `aio_websocket` with basic tools for setting up 32 | a :term:`websocket` server. 33 | 34 | For a more detailed walkthrough of how to setup ``Aiopyramid`` see the :doc:`tutorial`. 35 | 36 | 37 | Contents 38 | -------- 39 | 40 | .. toctree:: 41 | :maxdepth: 3 42 | 43 | features 44 | tutorial 45 | approach 46 | tests 47 | Index 48 | 49 | 50 | Contributors 51 | ------------ 52 | 53 | - Jason Housley 54 | - Guillaume Gauvrit 55 | - Tiago Requeijo 56 | - Ander Ustarroz 57 | - Ramon Navarro Bosch 58 | - Rickert Mulder 59 | 60 | Indices and Tables 61 | ================== 62 | 63 | * :ref:`genindex` 64 | * :ref:`modindex` 65 | * :ref:`search` 66 | * :doc:`glossary` 67 | 68 | .. _gunicorn: http://gunicorn.org 69 | .. _uWSGI: https://github.com/unbit/uwsgi 70 | .. _uWSGI asyncio plugin: http://uwsgi-docs.readthedocs.org/en/latest/asyncio.html 71 | -------------------------------------------------------------------------------- /tests/test_traversal.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import asyncio 3 | 4 | from pyramid.traversal import traverse 5 | 6 | from aiopyramid.helpers import spawn_greenlet, synchronize 7 | 8 | 9 | class DummyResource: 10 | """ Dummy resource for testing async traversal. """ 11 | 12 | def __init__(self, name, parent): 13 | self.__name__ = name 14 | self.__parent__ = parent 15 | self._dict = {} 16 | 17 | @synchronize 18 | @asyncio.coroutine 19 | def __getitem__(self, key): 20 | yield from asyncio.sleep(0.1) 21 | return self._dict[key] 22 | 23 | def __setitem__(self, key, value): 24 | self._dict[key] = value 25 | 26 | def add_child(self, name, klass): 27 | resource = klass(name=name, parent=self) 28 | self[name] = resource 29 | 30 | 31 | class TestTraversal(unittest.TestCase): 32 | 33 | def setUp(self): 34 | self.loop = asyncio.get_event_loop() 35 | 36 | def test_async_traversed_length(self): 37 | resource = DummyResource('root', None) 38 | resource.add_child('cat', DummyResource) 39 | out = self.loop.run_until_complete( 40 | spawn_greenlet(traverse, resource, ['cat']), 41 | ) 42 | self.assertEqual(len(out['traversed']), 1) 43 | 44 | def test_async_root(self): 45 | resource = DummyResource('root', None) 46 | resource.add_child('cat', DummyResource) 47 | out = self.loop.run_until_complete( 48 | spawn_greenlet(traverse, resource, ['']), 49 | ) 50 | self.assertTrue(out.get('root') == out.get('context')) 51 | 52 | def test_async_depth(self): 53 | resource = DummyResource('root', None) 54 | resource.add_child('cat', DummyResource) 55 | out = self.loop.run_until_complete( 56 | spawn_greenlet(traverse, resource, ['cat']), 57 | ) 58 | out['context'].add_child('dog', DummyResource) 59 | out = self.loop.run_until_complete( 60 | spawn_greenlet(traverse, resource, ['cat', 'dog']), 61 | ) 62 | self.assertListEqual(list(out['traversed']), ['cat', 'dog']) 63 | 64 | def test_async_view_name(self): 65 | resource = DummyResource('root', None) 66 | resource.add_child('cat', DummyResource) 67 | out = self.loop.run_until_complete( 68 | spawn_greenlet(traverse, resource, ['cat', 'mouse']), 69 | ) 70 | self.assertListEqual(list(out['traversed']), ['cat']) 71 | self.assertEqual(out['view_name'], 'mouse') 72 | -------------------------------------------------------------------------------- /tests/test_auth.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | import pytest 4 | from pyramid import testing 5 | from aiopyramid.helpers import spawn_greenlet, synchronize 6 | 7 | 8 | @pytest.yield_fixture 9 | def web_request(): 10 | request = testing.DummyRequest() 11 | yield request 12 | 13 | 14 | class TestAuthentication: 15 | 16 | @pytest.yield_fixture 17 | def wrapped_policy(self): 18 | from pyramid.authentication import CallbackAuthenticationPolicy 19 | from aiopyramid.auth import authn_policy_factory 20 | 21 | @asyncio.coroutine 22 | def callback(userid, request): 23 | yield from asyncio.sleep(0.1) 24 | return ['test_user'] 25 | 26 | class TestAuthenticationPolicy(CallbackAuthenticationPolicy): 27 | def __init__(self, callback): 28 | self.callback = callback 29 | self.debug = True 30 | 31 | def unauthenticated_userid(self, request): 32 | return 'theone' 33 | 34 | yield authn_policy_factory(TestAuthenticationPolicy, callback) 35 | 36 | def call_authn_policy_methods(self, policy, request): 37 | assert policy.unauthenticated_userid(request) == 'theone' 38 | assert policy.authenticated_userid(request) == 'theone' 39 | assert policy.effective_principals(request) == [ 40 | 'system.Everyone', 41 | 'system.Authenticated', 42 | 'theone', 43 | 'test_user', 44 | ] 45 | 46 | @asyncio.coroutine 47 | def yield_from_authn_policy_methods(self, policy, request): 48 | assert (yield from policy.unauthenticated_userid(request)) == 'theone' 49 | assert (yield from policy.authenticated_userid(request)) == 'theone' 50 | assert (yield from policy.effective_principals(request)) == [ 51 | 'system.Everyone', 52 | 'system.Authenticated', 53 | 'theone', 54 | 'test_user', 55 | ] 56 | 57 | def test_wrapper_in_sync(self, wrapped_policy, web_request): 58 | loop = asyncio.get_event_loop() 59 | loop.run_until_complete(spawn_greenlet( 60 | self.call_authn_policy_methods, 61 | wrapped_policy, 62 | web_request, 63 | )) 64 | 65 | def test_wrapper_in_coroutine(self, wrapped_policy, web_request): 66 | loop = asyncio.get_event_loop() 67 | loop.run_until_complete(spawn_greenlet( 68 | synchronize(self.yield_from_authn_policy_methods), 69 | wrapped_policy, 70 | web_request, 71 | )) 72 | -------------------------------------------------------------------------------- /aiopyramid/auth.py: -------------------------------------------------------------------------------- 1 | """ 2 | Utilities for making :ref:`Pyramid ` authentication 3 | and authorization work with Aiopyramid. 4 | """ 5 | 6 | 7 | from .helpers import spawn_greenlet_on_scope_error, synchronize 8 | 9 | 10 | def coroutine_callback_authentication_policy_factory( 11 | policy_class, 12 | coroutine=None, 13 | *args, 14 | **kwargs 15 | ): 16 | """ 17 | Factory function for creating an AuthenticationPolicy instance that uses 18 | a :term:`coroutine` as a callback. 19 | 20 | :param policy_class: The AuthenticationPolicy to wrap. 21 | :param coroutine coroutine: If provided this is passed to 22 | the AuthenticationPolicy as the callback argument. 23 | 24 | Extra arguments and keyword arguments are passed to 25 | the AuthenticationPolicy, so if the AuthenticationPolicy expects 26 | a callback under another name, it is necessary to pass 27 | a :term:`synchronized coroutine` as an argument or keyword argument 28 | to this factory or use 29 | :class:`~aiopyramid.auth.CoroutineAuthenticationPolicyProxy` directly. 30 | 31 | This function is also aliased as 32 | :func:`aiopyramid.auth.authn_policy_factory`. 33 | """ 34 | 35 | if coroutine: 36 | coroutine = synchronize(coroutine) 37 | policy = policy_class(callback=coroutine, *args, **kwargs) 38 | else: 39 | policy = policy_class(*args, **kwargs) 40 | return CoroutineAuthenticationPolicyProxy(policy) 41 | 42 | 43 | authn_policy_factory = coroutine_callback_authentication_policy_factory 44 | 45 | 46 | class CoroutineAuthenticationPolicyProxy: 47 | """ 48 | This authentication policy proxies calls to another policy that uses 49 | a callback to retrieve principals. Because this callback may be a 50 | :term:`synchronized coroutine`, this class handles the case where the 51 | callback fails due to a :class:`~aiopyramid.exceptions.ScopeError` and 52 | generates the appropriate ``Aiopyramid`` architecture. 53 | """ 54 | 55 | def __init__(self, policy): 56 | """ 57 | :param class policy: The authentication policy to wrap. 58 | """ 59 | 60 | self._policy = policy 61 | 62 | @spawn_greenlet_on_scope_error 63 | def remember(self, request, principal, **kwargs): 64 | return self._policy.remember(request, principal, **kwargs) 65 | 66 | @spawn_greenlet_on_scope_error 67 | def forget(self, request): 68 | return self._policy.forget(request) 69 | 70 | @spawn_greenlet_on_scope_error 71 | def unauthenticated_userid(self, request): 72 | return self._policy.unauthenticated_userid(request) 73 | 74 | @spawn_greenlet_on_scope_error 75 | def authenticated_userid(self, request): 76 | return self._policy.authenticated_userid(request) 77 | 78 | @spawn_greenlet_on_scope_error 79 | def effective_principals(self, request): 80 | return self._policy.effective_principals(request) 81 | -------------------------------------------------------------------------------- /tests/test_tweens.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import asyncio 3 | 4 | import greenlet 5 | 6 | from aiopyramid.helpers import spawn_greenlet, run_in_greenlet 7 | 8 | 9 | class TestTweens(unittest.TestCase): 10 | 11 | def setUp(self): 12 | self.loop = asyncio.get_event_loop() 13 | 14 | def _make_tweens(self): 15 | from pyramid.config.tweens import Tweens 16 | return Tweens() 17 | 18 | def _async_tween_factory(self, handler, registry): 19 | 20 | @asyncio.coroutine 21 | def _async_action(): 22 | yield from asyncio.sleep(0.2) 23 | return 12 24 | 25 | def async_tween(request): 26 | this = greenlet.getcurrent() 27 | future = asyncio.Future() 28 | sub_task = asyncio.ensure_future( 29 | run_in_greenlet(this, future, _async_action), 30 | ) 31 | self.assertIsInstance(sub_task, asyncio.Future) 32 | this.parent.switch(sub_task) 33 | self.assertEqual(future.result(), 12) 34 | return future 35 | 36 | return async_tween 37 | 38 | def _dummy_tween_factory(self, handler, registry): 39 | return handler 40 | 41 | def test_async_tween(self): 42 | out = self.loop.run_until_complete( 43 | spawn_greenlet(self._async_tween_factory(None, None), None), 44 | ) 45 | self.assertEqual(out, 12) 46 | 47 | def test_example_tween(self): 48 | from aiopyramid.tweens import coroutine_logger_tween_factory 49 | out = self.loop.run_until_complete( 50 | spawn_greenlet( 51 | coroutine_logger_tween_factory( 52 | lambda x: x, 53 | None, 54 | ), 55 | None, 56 | ) 57 | ) 58 | self.assertEqual(None, out) 59 | 60 | def test_sync_tween_above(self): 61 | tweens = self._make_tweens() 62 | tweens.add_implicit('async', self._async_tween_factory) 63 | tweens.add_implicit('sync', self._dummy_tween_factory) 64 | chain = tweens(None, None) 65 | out = self.loop.run_until_complete(spawn_greenlet(chain, None)) 66 | self.assertEqual(out, 12) 67 | 68 | def test_sync_tween_below(self): 69 | tweens = self._make_tweens() 70 | tweens.add_implicit('sync', self._dummy_tween_factory) 71 | tweens.add_implicit('async', self._async_tween_factory) 72 | chain = tweens(None, None) 73 | out = self.loop.run_until_complete(spawn_greenlet(chain, None)) 74 | self.assertEqual(out, 12) 75 | 76 | def test_sync_both(self): 77 | tweens = self._make_tweens() 78 | tweens.add_implicit('sync', self._dummy_tween_factory) 79 | tweens.add_implicit('async', self._async_tween_factory) 80 | tweens.add_implicit('sync', self._dummy_tween_factory) 81 | chain = tweens(None, None) 82 | out = self.loop.run_until_complete(spawn_greenlet(chain, None)) 83 | self.assertEqual(out, 12) 84 | 85 | 86 | class TestTweensGunicorn(unittest.TestCase): 87 | 88 | """ Test aiopyramid tweens gunicorn style. """ 89 | # TODO write tests that rely on subtasks 90 | -------------------------------------------------------------------------------- /CHANGES.rst: -------------------------------------------------------------------------------- 1 | Changes 2 | ======= 3 | 4 | .. :changelog: 5 | 6 | 0.4.2 (2019-06-18) 7 | ------------------ 8 | - Add class methods support into view mappers 9 | - Fix synchronous function call from executor view 10 | 11 | 0.4.1 (2016-06-04) 12 | ------------------ 13 | - Fix dependency mismatch for cases of aiohttp > 1.0 but < 2.0 14 | 15 | 0.4.0 (2016-05-29) 16 | ------------------ 17 | - Refactor to support latests aiohttp 18 | 19 | 0.3.7 (2017-05-07) 20 | ------------------ 21 | - Peg aiohttp dependency 22 | 23 | 0.3.6 (2016-09-22) 24 | ------------------ 25 | - Fix header normalization for Gunicorn 26 | 27 | 0.3.5 (2016-02-18) 28 | ------------------ 29 | - Fix Gunicorn logging support 30 | 31 | 0.3.4 (2016-02-03) 32 | ------------------ 33 | - Fix compatiblity with websockets 3+ 34 | 35 | 0.3.3 (2015-11-21) 36 | ------------------ 37 | - Merge fix for `ignore_websocket_closed` to allow chained exceptions 38 | - Add option to coerce bytes to str for uwsgi websockets 39 | 40 | 0.3.2 (2015-09-24) 41 | ------------------ 42 | - Support Python3.5 43 | 44 | 0.3.1 (2015-01-31) 45 | ------------------- 46 | - Fix issues related to POST requests 47 | - Fix issues related to coroutine mappers 48 | - Sync with Gunicorn settings a la issue #917 49 | 50 | 0.3.0 (2014-12-06) 51 | ------------------ 52 | - Add sphinx 53 | - Migrate README to sphinx docs 54 | - Add helpers for authentication 55 | - Deprecated aiopyramid.traversal, use aiopyramid.helpers.synchronize 56 | - Deprecated aiopyramid.tweens, moved examples to docs 57 | 58 | 0.2.4 (2014-10-06) 59 | ------------------ 60 | - Fix issue with gunicorn websockets 61 | - Fix issue with class-based view mappers 62 | 63 | 0.2.3 (2014-10-01) 64 | ------------------ 65 | - Fix issue with `synchronize` 66 | 67 | 0.2.2 (2014-09-30) 68 | ------------------ 69 | - Update example tween to work with gunicorn 70 | - Add kwargs support to helpers 71 | - Add tox for testing 72 | - Add decorator `synchronize` for wrapping coroutines 73 | - Refactored mappers and tween example to use `synchronize` 74 | - Bug fixes 75 | 76 | 0.2.1 (2014-09-15) 77 | ------------------ 78 | - Update scaffold example tests 79 | - Add test suite 80 | - Update README 81 | 82 | 0.2.0 (2014-09-01) 83 | ------------------ 84 | - Update README 85 | - added websocket mappers for uwsgi and gunicorn 86 | - added websocket view class 87 | 88 | 0.1.2 (2014-08-02) 89 | ------------------ 90 | - Update MANIFEST.in 91 | 92 | 0.1.0 (2014-08-01) 93 | ------------------ 94 | - Update README ready for release 95 | - Added asyncio traverser (patched from `ResourceTreeTraverser`) 96 | - Added custom gunicorn worker 97 | - Fix issue with uwsgi and executor threads 98 | - Update starter scaffold 99 | 100 | 0.0.3 (2014-07-30) 101 | ------------------ 102 | - Moving to an extension-based rather than patched-based approach 103 | - removed most code based on pyramid_asyncio except testing and scaffolds 104 | - added view mappers for running views in asyncio 105 | - added example tween that can come before or after synchronous tweens 106 | 107 | 0.0.2 (2014-07-22) 108 | ------------------ 109 | - Removed Gunicorn specific code 110 | - disabled excview_tween_factory 111 | - made viewresult_to_response a coroutine 112 | - added dummy code for testing with uwsgi 113 | 114 | 0.0.1 (2014-07-22) 115 | ------------------ 116 | - Migrated from pyramid_asyncio (Thank you Guillaume) 117 | - Removed worker.py and Gunicorn dependency 118 | - Added greenlet dependency 119 | - Changed contact information in setup.py 120 | -------------------------------------------------------------------------------- /aiopyramid/websocket/config/uwsgi.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | import asyncio 3 | from contextlib import suppress 4 | 5 | import greenlet 6 | 7 | from aiopyramid.config import AsyncioMapperBase 8 | from aiopyramid.helpers import run_in_greenlet 9 | from aiopyramid.websocket.exceptions import WebsocketClosed 10 | 11 | try: 12 | import uwsgi 13 | except ImportError: 14 | pass 15 | 16 | 17 | def uwsgi_recv_msg(g): 18 | g.has_message = True 19 | g.switch() 20 | 21 | 22 | class UWSGIWebsocket: 23 | 24 | def __init__(self, back, q_in, q_out): 25 | self.back = back 26 | self.q_in = q_in 27 | self.q_out = q_out 28 | self.open = True 29 | 30 | @asyncio.coroutine 31 | def recv(self): 32 | return (yield from self.q_in.get()) 33 | 34 | @asyncio.coroutine 35 | def send(self, message): 36 | yield from self.q_out.put(message) 37 | self.back.switch() 38 | 39 | @asyncio.coroutine 40 | def close(self): 41 | yield from self.q_in.put(None) 42 | self.back.throw(WebsocketClosed) 43 | 44 | 45 | class UWSGIWebsocketMapper(AsyncioMapperBase): 46 | 47 | use_str = True 48 | 49 | def launch_websocket_view(self, view): 50 | 51 | def websocket_view(context, request): 52 | uwsgi.websocket_handshake() 53 | this = greenlet.getcurrent() 54 | this.has_message = False 55 | q_in = asyncio.Queue() 56 | q_out = asyncio.Queue() 57 | 58 | # make socket proxy 59 | if inspect.isclass(view): 60 | view_callable = view(context, request) 61 | else: 62 | view_callable = view 63 | ws = UWSGIWebsocket(this, q_in, q_out) 64 | 65 | # start monitoring websocket events 66 | asyncio.get_event_loop().add_reader( 67 | uwsgi.connection_fd(), 68 | uwsgi_recv_msg, 69 | this 70 | ) 71 | 72 | # NOTE: don't use synchronize because we aren't waiting 73 | # for this future, instead we are using the reader to return 74 | # to the child greenlet. 75 | 76 | future = asyncio.Future() 77 | asyncio.ensure_future( 78 | run_in_greenlet(this, future, view_callable, ws) 79 | ) 80 | 81 | # switch to open 82 | this.parent.switch() 83 | 84 | while True: 85 | if future.done(): 86 | if future.exception() is not None: 87 | raise WebsocketClosed from future.exception() 88 | raise WebsocketClosed 89 | 90 | # message in 91 | if this.has_message: 92 | this.has_message = False 93 | try: 94 | msg = uwsgi.websocket_recv_nb() 95 | except OSError: 96 | msg = None 97 | 98 | if UWSGIWebsocketMapper.use_str: 99 | with suppress(Exception): 100 | print('howdy') 101 | msg = bytes.decode(msg) 102 | 103 | if msg or msg is None: 104 | q_in.put_nowait(msg) 105 | 106 | # message out 107 | if not q_out.empty(): 108 | msg = q_out.get_nowait() 109 | try: 110 | uwsgi.websocket_send(msg) 111 | except OSError: 112 | q_in.put_nowait(None) 113 | 114 | this.parent.switch() 115 | 116 | return websocket_view 117 | 118 | def __call__(self, view): 119 | """ Accepts a view_callable class. """ 120 | return self.launch_websocket_view(view) 121 | 122 | UWSGIWebsocketMapper.use_str = False 123 | -------------------------------------------------------------------------------- /aiopyramid/config.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module provides view mappers for running views in asyncio. 3 | """ 4 | import asyncio 5 | import inspect 6 | 7 | from pyramid.config.views import DefaultViewMapper 8 | from pyramid.exceptions import ConfigurationError 9 | 10 | from .helpers import synchronize, is_generator 11 | 12 | 13 | class AsyncioMapperBase(DefaultViewMapper): 14 | """ 15 | Base class for asyncio view mappers. 16 | """ 17 | 18 | def run_in_coroutine_view(self, view): 19 | 20 | view = synchronize(view) 21 | 22 | def coroutine_view(context, request): 23 | 24 | # Trigger loading of post data to avoid synchronization problems 25 | # This must be done in a non-async context 26 | request.params.__getitem__ = request.params.__getitem__ 27 | 28 | return view(context, request) 29 | 30 | return coroutine_view 31 | 32 | def run_in_executor_view(self, view): 33 | 34 | synchronizer = synchronize(strict=False) 35 | 36 | def executor_view(context, request): 37 | 38 | # Trigger loading of post data to avoid synchronization problems 39 | # This must be done in a non-async context 40 | request.params.__getitem__ = request.params.__getitem__ 41 | 42 | # since we are running in a new thread, 43 | # remove the old wsgi.file_wrapper for uwsgi 44 | request.environ.pop('wsgi.file_wrapper', None) 45 | 46 | if not asyncio.iscoroutinefunction(view): 47 | return view(context, request) 48 | 49 | exe = synchronizer(asyncio.get_event_loop().run_in_executor) 50 | return exe(None, view, context, request) 51 | 52 | return executor_view 53 | 54 | def is_class_method_coroutine(self, view): 55 | return inspect.isclass(view) and asyncio.iscoroutinefunction(getattr(view, self.attr)) 56 | 57 | class CoroutineMapper(AsyncioMapperBase): 58 | 59 | def __call__(self, view): 60 | original = view 61 | view = super().__call__(view) 62 | 63 | if ( 64 | is_generator(original) 65 | or is_generator(getattr(original, '__call__', None)) 66 | or (self.is_class_method_coroutine(original)) 67 | ): 68 | view = asyncio.coroutine(view) 69 | elif not asyncio.iscoroutinefunction(original): 70 | raise ConfigurationError( 71 | 'Non-coroutine {} mapped to coroutine.'.format(original) 72 | ) 73 | 74 | return self.run_in_coroutine_view(view) 75 | 76 | 77 | class ExecutorMapper(AsyncioMapperBase): 78 | 79 | def __call__(self, view): 80 | if ( 81 | asyncio.iscoroutinefunction(view) 82 | or asyncio.iscoroutinefunction(getattr(view, '__call__', None)) 83 | or self.is_class_method_coroutine(view) 84 | ): 85 | raise ConfigurationError( 86 | 'Coroutine {} mapped to executor.'.format(view) 87 | ) 88 | view = super().__call__(view) 89 | return self.run_in_executor_view(view) 90 | 91 | 92 | class CoroutineOrExecutorMapper(AsyncioMapperBase): 93 | 94 | def __call__(self, view): 95 | original = view 96 | while asyncio.iscoroutinefunction(view): 97 | try: 98 | view = view.__wrapped__ # unwrap coroutine 99 | except AttributeError: 100 | break 101 | 102 | view = super().__call__(view) 103 | 104 | if ( 105 | asyncio.iscoroutinefunction(original) 106 | or is_generator(original) 107 | or is_generator(getattr(original, '__call__', None)) 108 | or self.is_class_method_coroutine(original) 109 | ): 110 | view = asyncio.coroutine(view) 111 | return self.run_in_coroutine_view(view) 112 | else: 113 | return self.run_in_executor_view(view) 114 | -------------------------------------------------------------------------------- /aiopyramid/gunicorn/worker.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | from aiohttp_wsgi.wsgi import WSGIHandler, ReadBuffer 4 | from aiohttp.worker import GunicornWebWorker 5 | from aiohttp.web import Application, Response, HTTPRequestEntityTooLarge 6 | 7 | from aiopyramid.helpers import ( 8 | spawn_greenlet, 9 | ) 10 | 11 | 12 | def _run_application(application, environ): 13 | # Simple start_response callable. 14 | def start_response(status, headers, exc_info=None): 15 | nonlocal response_status, response_reason 16 | nonlocal response_headers, response_body 17 | status_code, reason = status.split(None, 1) 18 | status_code = int(status_code) 19 | # Start the response. 20 | response_status = status_code 21 | response_reason = reason 22 | response_headers = headers 23 | del response_body[:] 24 | return response_body.append 25 | # Response data. 26 | response_status = None 27 | response_reason = None 28 | response_headers = None 29 | response_body = [] 30 | # Run the application. 31 | body_iterable = application(environ, start_response) 32 | try: 33 | response_body.extend(body_iterable) 34 | assert response_status is not None, "application did not call start_response()" # noqa 35 | return ( 36 | response_status, 37 | response_reason, 38 | response_headers, 39 | b"".join(response_body), 40 | ) 41 | finally: 42 | # Close the body. 43 | if hasattr(body_iterable, "close"): 44 | body_iterable.close() 45 | 46 | 47 | class AiopyramidWSGIHandler(WSGIHandler): 48 | 49 | def _get_environ(self, request, body, content_length): 50 | environ = super(AiopyramidWSGIHandler, self)._get_environ( 51 | request, 52 | body, 53 | content_length) 54 | # restore hop headers for websockets 55 | for header_name in request.headers: 56 | header_name = header_name.upper() 57 | if header_name not in ("CONTENT-LENGTH", "CONTENT-TYPE"): 58 | header_value = ",".join(request.headers.getall(header_name)) 59 | environ["HTTP_" + header_name.replace("-", "_")] = header_value 60 | return environ 61 | 62 | @asyncio.coroutine 63 | def handle_request(self, request): 64 | # Check for body size overflow. 65 | if ( 66 | request.content_length is not None and 67 | request.content_length > self._max_request_body_size): 68 | raise HTTPRequestEntityTooLarge() 69 | # Buffer the body. 70 | body_buffer = ReadBuffer( 71 | self._inbuf_overflow, 72 | self._max_request_body_size, 73 | self._loop, 74 | self._executor) 75 | 76 | try: 77 | while True: 78 | block = yield from request.content.readany() 79 | if not block: 80 | break 81 | yield from body_buffer.write(block) 82 | # Seek the body. 83 | body, content_length = yield from body_buffer.get_body() 84 | # Get the environ. 85 | environ = self._get_environ(request, body, content_length) 86 | environ['async.writer'] = request.writer 87 | environ['async.protocol'] = request.protocol 88 | status, reason, headers, body = yield from spawn_greenlet( 89 | _run_application, 90 | self._application, 91 | environ, 92 | ) 93 | # All done! 94 | return Response( 95 | status=status, 96 | reason=reason, 97 | headers=headers, 98 | body=body, 99 | ) 100 | 101 | finally: 102 | yield from body_buffer.close() 103 | 104 | 105 | class AsyncGunicornWorker(GunicornWebWorker): 106 | 107 | def make_handler(self, app): 108 | aio_app = Application() 109 | aio_app.router.add_route( 110 | "*", 111 | "/{path_info:.*}", 112 | AiopyramidWSGIHandler( 113 | app, 114 | loop=self.loop, 115 | ), 116 | ) 117 | access_log = self.log.access_log if self.cfg.accesslog else None 118 | return aio_app.make_handler( 119 | loop=self.loop, 120 | logger=self.log, 121 | slow_request_timeout=self.cfg.timeout, 122 | keepalive_timeout=self.cfg.keepalive, 123 | access_log=access_log, 124 | access_log_format=self._get_valid_log_format( 125 | self.cfg.access_log_format)) 126 | -------------------------------------------------------------------------------- /aiopyramid/websocket/config/gunicorn.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import inspect 3 | import functools 4 | 5 | import websockets 6 | import gunicorn # noqa 7 | 8 | from pyramid.response import Response 9 | 10 | from aiopyramid.config import AsyncioMapperBase 11 | 12 | 13 | def _connection_closed_to_none(func): 14 | """ 15 | A backwards compatibility shim for websockets 3+. We need to 16 | still return `None` rather than throwing an exception in order 17 | to unite the interface with uWSGI even though the exception is 18 | more Pythonic. 19 | """ 20 | 21 | @asyncio.coroutine 22 | @functools.wraps(func) 23 | def _connection_closed_to_none_inner(*args, **kwargs): 24 | try: 25 | msg = yield from func(*args, **kwargs) 26 | except websockets.exceptions.ConnectionClosed: 27 | msg = None 28 | 29 | return msg 30 | 31 | return _connection_closed_to_none_inner 32 | 33 | 34 | def _use_bytes(func): 35 | """ 36 | Encodes strings received from websockets to bytes to 37 | provide consistency with uwsgi since we don't have access 38 | to the raw WebsocketFrame. 39 | """ 40 | 41 | @asyncio.coroutine 42 | @functools.wraps(func) 43 | def _use_bytes_inner(*args, **kwargs): 44 | data = yield from func(*args, **kwargs) 45 | if isinstance(data, str): 46 | return str.encode(data) 47 | else: 48 | return data 49 | 50 | return _use_bytes_inner 51 | 52 | 53 | class HandshakeInterator: 54 | 55 | def __init__(self, app_iter): 56 | self.content = list(app_iter) 57 | self.index = 0 58 | 59 | def __iter__(self): 60 | return self 61 | 62 | def __next__(self): 63 | try: 64 | return self.content[self.index] 65 | except IndexError: 66 | raise StopIteration 67 | finally: 68 | self.index += 1 69 | 70 | 71 | class SwitchProtocolsResponse(Response): 72 | """Upgrade from a WSGI connection with the WebSocket handshake.""" 73 | 74 | def __init__(self, environ, switch_protocols): 75 | super().__init__() 76 | self.status_int = 101 77 | 78 | http_1_1 = environ['SERVER_PROTOCOL'] == 'HTTP/1.1' 79 | 80 | def get_header(k): 81 | key_map = {k.upper(): k for k in environ} 82 | return environ[key_map['HTTP_' + k.upper().replace('-', '_')]] 83 | 84 | key = websockets.handshake.check_request(get_header) 85 | 86 | if not http_1_1 or key is None: 87 | self.status_int = 400 88 | self.content = "Invalid WebSocket handshake.\n" 89 | else: 90 | set_header = self.headers.__setitem__ 91 | websockets.handshake.build_response(set_header, key) 92 | self.app_iter = HandshakeInterator(self.app_iter) 93 | self.app_iter.close = switch_protocols 94 | 95 | 96 | class WebsocketMapper(AsyncioMapperBase): 97 | 98 | use_bytes = False 99 | 100 | def launch_websocket_view(self, view): 101 | 102 | def websocket_view(context, request): 103 | 104 | if inspect.isclass(view): 105 | view_callable = view(context, request) 106 | else: 107 | view_callable = view 108 | 109 | @asyncio.coroutine 110 | def _ensure_ws_close(ws): 111 | if WebsocketMapper.use_bytes: 112 | ws.recv = _use_bytes(ws.recv) 113 | 114 | ws.recv = _connection_closed_to_none(ws.recv) 115 | 116 | yield from view_callable(ws) 117 | yield from ws.close() 118 | 119 | def switch_protocols(): 120 | # TODO: Determine if there is a more standard way to do this 121 | ws_protocol = websockets.WebSocketCommonProtocol() 122 | transport = request.environ['async.writer']._transport 123 | 124 | http_protocol = request.environ['async.protocol'] 125 | http_protocol.connection_lost(None) 126 | 127 | transport._protocol = ws_protocol 128 | ws_protocol.connection_made(transport) 129 | asyncio.ensure_future(_ensure_ws_close(ws_protocol)) 130 | 131 | response = SwitchProtocolsResponse( 132 | request.environ, 133 | switch_protocols, 134 | ) 135 | # convert iterator to avoid eof issues 136 | response.body = response.body 137 | 138 | return response 139 | 140 | return websocket_view 141 | 142 | def __call__(self, view): 143 | """ Accepts a view_callable class. """ 144 | return self.launch_websocket_view(view) 145 | -------------------------------------------------------------------------------- /aiopyramid/traversal.py: -------------------------------------------------------------------------------- 1 | """ 2 | The aiopyramid.traversal module is deprecated, use aiopyramid.helpers.synchronize instead. 3 | See http://aiopyramid.readthedocs.io/features.html#traversal. 4 | """ # NOQA 5 | 6 | import asyncio 7 | import warnings 8 | 9 | from pyramid.traversal import ( 10 | ResourceTreeTraverser as TraverserBase, 11 | is_nonstr_iter, 12 | split_path_info, 13 | ) 14 | from pyramid.exceptions import URLDecodeError 15 | from pyramid.interfaces import VH_ROOT_KEY 16 | from pyramid.compat import decode_path_info 17 | 18 | from .helpers import synchronize 19 | 20 | SLASH = "/" 21 | 22 | warnings.warn(__doc__, DeprecationWarning) 23 | 24 | 25 | @synchronize 26 | @asyncio.coroutine 27 | def traverse( 28 | i, 29 | ob, 30 | view_selector, 31 | vpath_tuple, 32 | vroot_idx, 33 | vroot, 34 | vroot_tuple, 35 | root, 36 | subpath, 37 | ): 38 | """ 39 | A version of :func:`pyramid.traversal.traverse` that expects `__getitem__` 40 | to be a :term:`coroutine`. 41 | """ 42 | 43 | for segment in vpath_tuple: 44 | if segment[:2] == view_selector: 45 | return { 46 | 'context': ob, 47 | 'view_name': segment[2:], 48 | 'subpath': vpath_tuple[i + 1:], 49 | 'traversed': vpath_tuple[:vroot_idx + i + 1], 50 | 'virtual_root': vroot, 51 | 'virtual_root_path': vroot_tuple, 52 | 'root': root, 53 | } 54 | try: 55 | getitem = ob.__getitem__ 56 | except AttributeError: 57 | return { 58 | 'context': ob, 59 | 'view_name': segment, 60 | 'subpath': vpath_tuple[i + 1:], 61 | 'traversed': vpath_tuple[:vroot_idx + i + 1], 62 | 'virtual_root': vroot, 63 | 'virtual_root_path': vroot_tuple, 64 | 'root': root, 65 | } 66 | 67 | try: 68 | tsugi = yield from getitem(segment) 69 | except KeyError: 70 | return { 71 | 'context': ob, 72 | 'view_name': segment, 73 | 'subpath': vpath_tuple[i + 1:], 74 | 'traversed': vpath_tuple[:vroot_idx + i + 1], 75 | 'virtual_root': vroot, 76 | 'virtual_root_path': vroot_tuple, 77 | 'root': root, 78 | } 79 | if i == vroot_idx: 80 | vroot = tsugi 81 | ob = tsugi 82 | i += 1 83 | 84 | return { 85 | 'context': ob, 86 | 'view_name': "", 87 | 'subpath': subpath, 88 | 'traversed': vpath_tuple, 89 | 'virtual_root': vroot, 90 | 'virtual_root_path': vroot_tuple, 91 | 'root': root 92 | } 93 | 94 | 95 | class AsyncioTraverser(TraverserBase): 96 | """ 97 | Traversal algorithm patched from the default traverser to execute 98 | __getitem__ as a coroutine. 99 | """ 100 | 101 | def __call__(self, request): 102 | environ = request.environ 103 | matchdict = request.matchdict 104 | 105 | if matchdict is not None: 106 | 107 | path = matchdict.get('traverse', SLASH) or SLASH 108 | if is_nonstr_iter(path): 109 | # this is a *traverse stararg (not a {traverse}) 110 | # routing has already decoded these elements, so we just 111 | # need to join them 112 | path = '/' + SLASH.join(path) or SLASH 113 | 114 | subpath = matchdict.get('subpath', ()) 115 | if not is_nonstr_iter(subpath): 116 | # this is not a *subpath stararg (just a {subpath}) 117 | # routing has already decoded this string, so we just need 118 | # to split it 119 | subpath = split_path_info(subpath) 120 | 121 | else: 122 | # this request did not match a route 123 | subpath = () 124 | try: 125 | # empty if mounted under a path in mod_wsgi, for example 126 | path = request.path_info or SLASH 127 | except KeyError: 128 | # if environ['PATH_INFO'] is just not there 129 | path = SLASH 130 | except UnicodeDecodeError as e: 131 | raise URLDecodeError(e.encoding, e.object, e.start, e.end, 132 | e.reason) 133 | 134 | if VH_ROOT_KEY in environ: 135 | # HTTP_X_VHM_ROOT 136 | vroot_path = decode_path_info(environ[VH_ROOT_KEY]) 137 | vroot_tuple = split_path_info(vroot_path) 138 | vpath = vroot_path + path 139 | vroot_idx = len(vroot_tuple) - 1 140 | else: 141 | vroot_tuple = () 142 | vpath = path 143 | vroot_idx = - 1 144 | 145 | root = self.root 146 | ob = vroot = root 147 | 148 | if vpath == SLASH: 149 | vpath_tuple = () 150 | else: 151 | i = 0 152 | view_selector = self.VIEW_SELECTOR 153 | vpath_tuple = split_path_info(vpath) 154 | return traverse( 155 | i, 156 | ob, 157 | view_selector, 158 | vpath_tuple, 159 | vroot_idx, 160 | vroot, 161 | vroot_tuple, 162 | root, 163 | subpath, 164 | ) 165 | 166 | return { 167 | 'context': ob, 168 | 'view_name': "", 169 | 'subpath': subpath, 170 | 'traversed': vpath_tuple, 171 | 'virtual_root': vroot, 172 | 'virtual_root_path': vroot_tuple, 173 | 'root': root 174 | } 175 | -------------------------------------------------------------------------------- /aiopyramid/helpers.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import inspect 3 | import functools 4 | import logging 5 | 6 | import greenlet 7 | from pyramid.exceptions import ConfigurationError 8 | 9 | from .exceptions import ScopeError 10 | 11 | SCOPE_ERROR_MESSAGE = ''' 12 | Synchronized coroutine {} called in the parent 13 | greenlet. 14 | 15 | This is most likely because you called the synchronized 16 | coroutine inside of another coroutine. You need to 17 | yield from the coroutine directly without wrapping 18 | it in aiopyramid.helpers.synchronize. 19 | 20 | If you are calling this coroutine indirectly from 21 | a regular function and therefore cannot yield from it, 22 | then you need to run the first caller inside a new 23 | greenlet using aiopyramid.helpers.spawn_greenlet. 24 | ''' 25 | 26 | log = logging.getLogger(__name__) 27 | 28 | 29 | def is_generator(func): 30 | """ Tests whether `func` is capable of becoming an `asyncio.coroutine`. """ 31 | return ( 32 | inspect.isgeneratorfunction(func) or 33 | isinstance(func, asyncio.Future) or 34 | inspect.isgenerator(func) 35 | ) 36 | 37 | 38 | @asyncio.coroutine 39 | def spawn_greenlet(func, *args, **kwargs): 40 | """ 41 | Spawns a new greenlet and waits on any `asyncio.Future` objects returned. 42 | 43 | This is used by the Gunicorn worker to proxy a greenlet within an `asyncio` 44 | event loop. 45 | """ 46 | 47 | g = greenlet.greenlet(func) 48 | result = g.switch(*args, **kwargs) 49 | while True: 50 | if isinstance(result, asyncio.Future): 51 | result = yield from result 52 | else: 53 | break 54 | return result 55 | 56 | 57 | @asyncio.coroutine 58 | def run_in_greenlet(back, future, func, *args, **kwargs): 59 | """ 60 | Wait for :term:`coroutine` func and switch back to the request greenlet 61 | setting any result in the future or an Exception where appropriate. 62 | 63 | func is often a :term:`view callable` 64 | """ 65 | try: 66 | result = yield from func(*args, **kwargs) 67 | except Exception as ex: 68 | future.set_exception(ex) 69 | else: 70 | future.set_result(result) 71 | finally: 72 | return back.switch() 73 | 74 | 75 | def synchronize(*args, strict=True): 76 | """ 77 | Decorator for transforming an async coroutine function into a regular 78 | function relying on the `aiopyramid` architecture to schedule 79 | the coroutine and obtain the result. 80 | 81 | .. code-block:: python 82 | 83 | @synchronize 84 | @asyncio.coroutine 85 | def my_coroutine(): 86 | ... code that yields 87 | """ 88 | 89 | def _wrapper(coroutine_func): 90 | if strict and not asyncio.iscoroutinefunction(coroutine_func): 91 | raise ConfigurationError( 92 | 'Attempted to synchronize a non-coroutine {}.'.format( 93 | coroutine_func 94 | ) 95 | ) 96 | 97 | @functools.wraps(coroutine_func) 98 | def _wrapped_coroutine(*args, **kwargs): 99 | 100 | this = greenlet.getcurrent() 101 | if this.parent is None: 102 | if strict: 103 | raise ScopeError( 104 | SCOPE_ERROR_MESSAGE.format(coroutine_func) 105 | ) 106 | else: 107 | return coroutine_func(*args, **kwargs) 108 | else: 109 | future = asyncio.Future() 110 | sub_task = asyncio.ensure_future( 111 | run_in_greenlet( 112 | this, 113 | future, 114 | coroutine_func, 115 | *args, 116 | **kwargs 117 | ) 118 | ) 119 | while not future.done(): 120 | this.parent.switch(sub_task) 121 | return future.result() 122 | 123 | return _wrapped_coroutine 124 | 125 | try: 126 | coroutine_func = args[0] 127 | return _wrapper(coroutine_func) 128 | except IndexError: 129 | return _wrapper 130 | 131 | 132 | def spawn_greenlet_on_scope_error(func): 133 | """ 134 | Wraps a callable handling any 135 | :class:`ScopeErrors <~aiopyramid.exceptions.ScopeError>` that may 136 | occur because the callable is called from inside of a :term:`coroutine`. 137 | 138 | If no :class:`~aiopyramid.exceptions.ScopeError` occurs, the callable is 139 | executed normally and return arguments are passed through, otherwise, when 140 | a :class:`~aiopyramid.exceptions.ScopeError` does occur, a coroutine to 141 | retrieve the result of the callable is returned instead. 142 | """ 143 | 144 | @functools.wraps(func) 145 | def _run_or_return_future(*args, **kwargs): 146 | this = greenlet.getcurrent() 147 | # Check if we should see a ScopeError 148 | if this.parent is None: 149 | return spawn_greenlet(func, *args, **kwargs) 150 | else: 151 | try: 152 | return func(*args, **kwargs) 153 | except ScopeError: 154 | # ScopeError generated in multiple levels of indirection 155 | log.warn('Unexpected ScopeError encountered.') 156 | return spawn_greenlet(func, *args, **kwargs) 157 | 158 | return _run_or_return_future 159 | 160 | 161 | def use_executor(*args, executor=None): 162 | """ 163 | A decorator for running a callback in the executor. 164 | 165 | This is useful to provide a declarative style for converting some 166 | thread-based code to a :term:`coroutine`. It creates a :term:`coroutine` 167 | by running the wrapped code in a separate thread. 168 | 169 | """ 170 | def _wrapper(callback): 171 | @functools.wraps(callback) 172 | @asyncio.coroutine 173 | def _wrapped_function(*args, **kwargs): 174 | loop = asyncio.get_event_loop() 175 | r = yield from loop.run_in_executor( 176 | executor, 177 | functools.partial( 178 | callback, 179 | *args, 180 | **kwargs 181 | ) 182 | ) 183 | return r 184 | return _wrapped_function 185 | 186 | try: 187 | func = args[0] 188 | return _wrapper(func) 189 | except IndexError: 190 | return _wrapper 191 | -------------------------------------------------------------------------------- /tests/test_helpers.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import unittest 3 | 4 | import greenlet 5 | from pyramid.exceptions import ConfigurationError 6 | 7 | 8 | class TestIsGenerator(unittest.TestCase): 9 | 10 | def test_regular_yield(self): 11 | from aiopyramid.helpers import is_generator 12 | 13 | def _sample(): 14 | yield 1 15 | yield 2 16 | 17 | self.assertTrue(is_generator(_sample)) 18 | 19 | def test_yield_from(self): 20 | from aiopyramid.helpers import is_generator 21 | 22 | def _placeholder(): 23 | yield 6 24 | return 7 25 | 26 | def _sample(): 27 | yield from _placeholder 28 | 29 | self.assertTrue(is_generator(_sample)) 30 | 31 | def test_coroutine(self): 32 | from aiopyramid.helpers import is_generator 33 | 34 | @asyncio.coroutine 35 | def _sample(): 36 | return 5 37 | 38 | self.assertTrue(is_generator(_sample)) 39 | 40 | def test_false(self): 41 | from aiopyramid.helpers import is_generator 42 | 43 | def _sample(): 44 | return "plain old function" 45 | 46 | self.assertFalse(is_generator(_sample)) 47 | 48 | 49 | class TestSpawnGreenlet(unittest.TestCase): 50 | 51 | def test_return_direct_result(self): 52 | from aiopyramid.helpers import spawn_greenlet 53 | 54 | def _return_4(): 55 | return 4 56 | 57 | out = asyncio.get_event_loop().run_until_complete( 58 | spawn_greenlet(_return_4), 59 | ) 60 | self.assertEqual(out, 4) 61 | 62 | def test_switch_direct_result(self): 63 | from aiopyramid.helpers import spawn_greenlet 64 | 65 | def _switch_4_return_5(): 66 | this = greenlet.getcurrent() 67 | this.parent.switch(4) 68 | return 5 69 | 70 | out = asyncio.get_event_loop().run_until_complete( 71 | spawn_greenlet(_switch_4_return_5), 72 | ) 73 | self.assertEqual(out, 4) 74 | 75 | def test_wait_on_future(self): 76 | from aiopyramid.helpers import spawn_greenlet 77 | 78 | future = asyncio.Future() 79 | 80 | def _switch_future(): 81 | this = greenlet.getcurrent() 82 | this.parent.switch(future) 83 | return 5 # This should never get returned 84 | 85 | # the result is already set, but 86 | # spawn_greenlet will still need to yield from it 87 | future.set_result(4) 88 | out = asyncio.get_event_loop().run_until_complete( 89 | spawn_greenlet(_switch_future), 90 | ) 91 | self.assertEqual(out, 4) 92 | 93 | 94 | class TestRunInGreenlet(unittest.TestCase): 95 | 96 | def test_result(self): 97 | from aiopyramid.helpers import spawn_greenlet 98 | from aiopyramid.helpers import run_in_greenlet 99 | 100 | @asyncio.coroutine 101 | def _sample(pass_back): 102 | return pass_back 103 | 104 | def _greenlet(): 105 | this = greenlet.getcurrent() 106 | future = asyncio.Future() 107 | message = 12 108 | sub_task = asyncio.ensure_future( 109 | run_in_greenlet(this, future, _sample, message), 110 | ) 111 | this.parent.switch(sub_task) 112 | self.assertEqual(future.result(), message) 113 | return message + 1 114 | 115 | out = asyncio.get_event_loop().run_until_complete( 116 | spawn_greenlet(_greenlet), 117 | ) 118 | self.assertEqual(13, out) 119 | 120 | def test_result_chain(self): 121 | from aiopyramid.helpers import spawn_greenlet 122 | from aiopyramid.helpers import run_in_greenlet 123 | 124 | @asyncio.coroutine 125 | def _sample(pass_back): 126 | return pass_back 127 | 128 | @asyncio.coroutine 129 | def _chain(pass_back): 130 | out = yield from _sample(pass_back) 131 | self.assertEqual(out, pass_back) 132 | return out - 1 133 | 134 | def _greenlet(): 135 | this = greenlet.getcurrent() 136 | future = asyncio.Future() 137 | message = 12 138 | sub_task = asyncio.ensure_future(run_in_greenlet( 139 | this, 140 | future, 141 | _chain, 142 | message, 143 | )) 144 | this.parent.switch(sub_task) 145 | self.assertEqual(future.result(), message - 1) 146 | return message + 1 147 | 148 | out = asyncio.get_event_loop().run_until_complete( 149 | spawn_greenlet(_greenlet), 150 | ) 151 | self.assertEqual(13, out) 152 | 153 | def test_exception(self): 154 | from aiopyramid.helpers import spawn_greenlet 155 | from aiopyramid.helpers import run_in_greenlet 156 | 157 | @asyncio.coroutine 158 | def _sample(): 159 | raise KeyError 160 | 161 | def _greenlet(): 162 | this = greenlet.getcurrent() 163 | future = asyncio.Future() 164 | sub_task = asyncio.ensure_future( 165 | run_in_greenlet(this, future, _sample), 166 | ) 167 | this.parent.switch(sub_task) 168 | self.assertRaises(KeyError, future.result) 169 | 170 | asyncio.get_event_loop().run_until_complete( 171 | spawn_greenlet(_greenlet), 172 | ) 173 | 174 | 175 | class TestSynchronize(unittest.TestCase): 176 | 177 | @asyncio.coroutine 178 | def _sample(self, pass_back): 179 | return pass_back 180 | 181 | def _simple(self, pass_back): 182 | return pass_back 183 | 184 | def test_conversion(self): 185 | from aiopyramid.helpers import synchronize 186 | from aiopyramid.helpers import is_generator 187 | 188 | syncer = synchronize(strict=True) 189 | self.assertRaises(ConfigurationError, syncer, self._simple) 190 | self.assertFalse(is_generator(syncer(self._sample))) 191 | 192 | def test_scope_error(self): 193 | from aiopyramid.exceptions import ScopeError 194 | from aiopyramid.helpers import synchronize, spawn_greenlet 195 | 196 | synced = synchronize(self._sample) 197 | self.assertRaises(ScopeError, synced, 'val') 198 | 199 | five = asyncio.get_event_loop().run_until_complete( 200 | spawn_greenlet(synced, 5), 201 | ) 202 | self.assertEqual(five, 5) 203 | 204 | synced = synchronize(self._sample, strict=False) 205 | self.assertTrue(asyncio.iscoroutine(synced('val'))) 206 | 207 | def test_as_decorator(self): 208 | from aiopyramid.helpers import synchronize, spawn_greenlet 209 | from aiopyramid.exceptions import ScopeError 210 | 211 | @synchronize 212 | @asyncio.coroutine 213 | def _synced(pass_back): 214 | yield 215 | return pass_back 216 | 217 | self.assertRaises(ScopeError, _synced, 'val') 218 | twelve = asyncio.get_event_loop().run_until_complete( 219 | spawn_greenlet(_synced, 12), 220 | ) 221 | self.assertEqual(twelve, 12) 222 | -------------------------------------------------------------------------------- /docs/approach.rst: -------------------------------------------------------------------------------- 1 | .. _architecture: 2 | 3 | Architecture 4 | ============ 5 | 6 | ``Aiopyramid`` uses a design similar to the `uWSGI asyncio plugin`_. The :mod:`asyncio` event loop runs in a 7 | parent greenlet, while wsgi callables run in child greenlets. Because the callables are running in greenlets, 8 | it is possible to suspend a callable and switch to parent to run :term:`coroutines ` all on one event loop. 9 | Each task tracks which child greenlet it belongs to and switches back to the appropriate callable when it is done. 10 | 11 | The greenlet model makes it possible to have any Python code wait for a coroutine even when that code is unaware of 12 | :mod:`asyncio`. The `uWSGI asyncio plugin`_ sets up the architecture by itself, but it is also possible to setup this 13 | architecture whenever we have a running :mod:`asyncio` event loop using :func:`~aiopyramid.helpers.spawn_greenlet`. 14 | 15 | For example, there may be times when a :term:`coroutine` would need to call some function ``a`` that later calls 16 | a :term:`coroutine` ``b``. Since :term:`coroutines ` run in the parent greenlet (i.e. on the event loop) and the function ``a`` 17 | cannot ``yield from`` ``b`` because it is not a :term:`coroutine` itself, the parent :term:`coroutine` will need to 18 | set up the ``Aiopyramid`` architecture so that ``b`` can be synchronized with :func:`~aiopyramid.helpers.synchronize` and 19 | called like a normal function from inside ``a``. 20 | 21 | The following code demonstrates this usage without needing to setup a server. 22 | 23 | .. doctest:: 24 | 25 | 26 | >>> import asyncio 27 | >>> from aiopyramid.helpers import synchronize, spawn_greenlet 28 | >>> 29 | >>> @synchronize 30 | ... @asyncio.coroutine 31 | ... def some_async_task(): 32 | ... print('I am a synchronized coroutine.') 33 | ... yield from asyncio.sleep(0.2) 34 | ... print('Synchronized task done.') 35 | ... 36 | >>> def normal_function(): 37 | ... print('I am normal function that needs to call some_async_task') 38 | ... some_async_task() 39 | ... print('I (normal_function) called it, and it is done now like I expect.') 40 | ... 41 | >>> @asyncio.coroutine 42 | ... def parent(): 43 | ... print('I am a traditional coroutine that needs to call the naive normal_function') 44 | ... yield from spawn_greenlet(normal_function) 45 | ... print('All is done.') 46 | ... 47 | >>> loop = asyncio.get_event_loop() 48 | >>> loop.run_until_complete(parent()) 49 | I am a traditional coroutine that needs to call the naive normal_function 50 | I am normal function that needs to call some_async_task 51 | I am a synchronized coroutine. 52 | Synchronized task done. 53 | I (normal_function) called it, and it is done now like I expect. 54 | All is done. 55 | 56 | Please feel free to use this in other :mod:`asyncio` projects that don't use :ref:`Pyramid ` 57 | because it's awesome. 58 | 59 | To avoid confusion, it is worth making explicit the fact that this approach is for incorporating code that is 60 | fast and non-blocking itself but needs to call a coroutine to do some io. Don't try to use this to 61 | call long-running or blocking Python functions. Instead, use `run_in_executor`_, which is what ``Aiopyramid`` 62 | does by default with :term:`view callables ` that don't appear to be :term:`coroutines `. 63 | 64 | 65 | History 66 | ------- 67 | 68 | ``Aiopyramid`` was originally based on `pyramid_asyncio`_, but I chose a different approach 69 | for the following reasons: 70 | 71 | - The `pyramid_asyncio`_ library depends on patches made to the :ref:`Pyramid ` router that prevent it 72 | from working with the `uWSGI asyncio plugin`_. 73 | - The `pyramid_asyncio`_ rewrites various parts of :ref:`Pyramid `, 74 | including tweens, to expect :term:`coroutines ` from :ref:`Pyramid ` internals. 75 | 76 | On the other hand ``Aiopyramid`` is designed to follow these principles: 77 | 78 | - ``Aiopyramid`` should extend :ref:`Pyramid ` through existing :ref:`Pyramid ` mechanisms where possible. 79 | - Asynchronous code should be wrapped so that existing callers can treat it as synchronous code. 80 | - Ultimately, no framework can guarantee that all io calls are non-blocking because it is always possible for a programmer 81 | to call out to some function that blocks (in other words, the programmer forgets to wrap long-running calls in `run_in_executor`_). 82 | So, frameworks should leave the determination of what code is safe to the programmer and instead provide tools for 83 | programmers to make educated decisions about what Python libraries can be used on an asynchronous server. Following the 84 | :ref:`Pyramid ` philosophy, frameworks should not get in the way. 85 | 86 | The first principle is one of the reasons why I used :term:`view mappers ` rather than patching the router. 87 | :term:`View mappers ` are a mechanism already in place to handle how views are called. We don't need to rewrite 88 | vast parts of :ref:`Pyramid ` to run a view in the :mod:`asyncio` event loop. 89 | Yes, :ref:`Pyramid ` is that awesome. 90 | 91 | The second principle is what allows ``Aiopyramid`` to support existing extensions. The goal is to isolate 92 | asynchronous code from code that expects a synchronous response. Those methods that already exist in :ref:`Pyramid ` 93 | should not be rewritten as :term:`coroutines ` because we don't know who will try to call them as regular methods. 94 | 95 | Most of the :ref:`Pyramid ` framework does not run io blocking code. So, it is not actually necessary to change the 96 | framework itself. Instead we need tools for making application code asynchronous. It should be possible 97 | to run an existing simple url dispatch application asynchronously without modification. Blocking code will naturally end 98 | up being run in a separate thread via the `run_in_executor`_ method. This allows you to optimize 99 | only those highly concurrent views in your application or add in websocket support without needing to refactor 100 | all of the code. 101 | 102 | It is easy to simulate a multithreaded server by increasing the number of threads available to the executor. 103 | 104 | For example, include the following in your application's constructor: 105 | 106 | .. code-block:: python 107 | 108 | import asyncio 109 | from concurrent.futures import ThreadPoolExecutor 110 | ... 111 | asyncio.get_event_loop().set_default_executor(ThreadPoolExecutor(max_workers=150)) 112 | 113 | .. note:: 114 | It should be noted that ``Aiopyramid`` is not thread-safe by nature. You will need to ensure that in memory 115 | resources are not modified by multiple non-coroutine :term:`view callables `. For most existing applications, this 116 | should not be a problem. 117 | 118 | .. _uWSGI: https://github.com/unbit/uwsgi 119 | .. _pyramid_debugtoolbar: https://github.com/Pylons/pyramid_debugtoolbar 120 | .. _pyramid_asyncio: https://github.com/mardiros/pyramid_asyncio 121 | .. _uWSGI asyncio plugin: http://uwsgi-docs.readthedocs.org/en/latest/asyncio.html 122 | .. _run_in_executor: https://docs.python.org/3/library/asyncio-eventloop.html#asyncio.BaseEventLoop.run_in_executor 123 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 21 | 22 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 23 | 24 | help: 25 | @echo "Please use \`make ' where is one of" 26 | @echo " html to make standalone HTML files" 27 | @echo " dirhtml to make HTML files named index.html in directories" 28 | @echo " singlehtml to make a single large HTML file" 29 | @echo " pickle to make pickle files" 30 | @echo " json to make JSON files" 31 | @echo " htmlhelp to make HTML files and a HTML help project" 32 | @echo " qthelp to make HTML files and a qthelp project" 33 | @echo " devhelp to make HTML files and a Devhelp project" 34 | @echo " epub to make an epub" 35 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 36 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 37 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 38 | @echo " text to make text files" 39 | @echo " man to make manual pages" 40 | @echo " texinfo to make Texinfo files" 41 | @echo " info to make Texinfo files and run them through makeinfo" 42 | @echo " gettext to make PO message catalogs" 43 | @echo " changes to make an overview of all changed/added/deprecated items" 44 | @echo " xml to make Docutils-native XML files" 45 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 46 | @echo " linkcheck to check all external links for integrity" 47 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 48 | 49 | clean: 50 | rm -rf $(BUILDDIR)/* 51 | 52 | html: 53 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 54 | @echo 55 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 56 | 57 | dirhtml: 58 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 59 | @echo 60 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 61 | 62 | singlehtml: 63 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 64 | @echo 65 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 66 | 67 | pickle: 68 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 69 | @echo 70 | @echo "Build finished; now you can process the pickle files." 71 | 72 | json: 73 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 74 | @echo 75 | @echo "Build finished; now you can process the JSON files." 76 | 77 | htmlhelp: 78 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 79 | @echo 80 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 81 | ".hhp project file in $(BUILDDIR)/htmlhelp." 82 | 83 | qthelp: 84 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 85 | @echo 86 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 87 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 88 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/aiopyramid.qhcp" 89 | @echo "To view the help file:" 90 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/aiopyramid.qhc" 91 | 92 | devhelp: 93 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 94 | @echo 95 | @echo "Build finished." 96 | @echo "To view the help file:" 97 | @echo "# mkdir -p $$HOME/.local/share/devhelp/aiopyramid" 98 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/aiopyramid" 99 | @echo "# devhelp" 100 | 101 | epub: 102 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 103 | @echo 104 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 105 | 106 | latex: 107 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 108 | @echo 109 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 110 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 111 | "(use \`make latexpdf' here to do that automatically)." 112 | 113 | latexpdf: 114 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 115 | @echo "Running LaTeX files through pdflatex..." 116 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 117 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 118 | 119 | latexpdfja: 120 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 121 | @echo "Running LaTeX files through platex and dvipdfmx..." 122 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 123 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 124 | 125 | text: 126 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 127 | @echo 128 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 129 | 130 | man: 131 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 132 | @echo 133 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 134 | 135 | texinfo: 136 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 137 | @echo 138 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 139 | @echo "Run \`make' in that directory to run these through makeinfo" \ 140 | "(use \`make info' here to do that automatically)." 141 | 142 | info: 143 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 144 | @echo "Running Texinfo files through makeinfo..." 145 | make -C $(BUILDDIR)/texinfo info 146 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 147 | 148 | gettext: 149 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 150 | @echo 151 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 152 | 153 | changes: 154 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 155 | @echo 156 | @echo "The overview file is in $(BUILDDIR)/changes." 157 | 158 | linkcheck: 159 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 160 | @echo 161 | @echo "Link check complete; look for any errors in the above output " \ 162 | "or in $(BUILDDIR)/linkcheck/output.txt." 163 | 164 | doctest: 165 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 166 | @echo "Testing of doctests in the sources finished, look at the " \ 167 | "results in $(BUILDDIR)/doctest/output.txt." 168 | 169 | xml: 170 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 171 | @echo 172 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 173 | 174 | pseudoxml: 175 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 176 | @echo 177 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 178 | livehtml: 179 | sphinx-autobuild -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 180 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # aiopyramid documentation build configuration file, created by 5 | # sphinx-quickstart on Mon Dec 1 14:33:38 2014. 6 | # 7 | # This file is execfile()d with the current directory set to its 8 | # containing dir. 9 | # 10 | # Note that not all possible configuration values are present in this 11 | # autogenerated file. 12 | # 13 | # All configuration values have a default; values that are commented out 14 | # serve to show the default. 15 | 16 | import sys 17 | import os 18 | 19 | import alabaster 20 | 21 | # If extensions (or modules to document with autodoc) are in another directory, 22 | # add these directories to sys.path here. If the directory is relative to the 23 | # documentation root, use os.path.abspath to make it absolute, like shown here. 24 | #sys.path.insert(0, os.path.abspath('.')) 25 | 26 | # -- General configuration ------------------------------------------------ 27 | 28 | # If your documentation needs a minimal Sphinx version, state it here. 29 | #needs_sphinx = '1.0' 30 | 31 | # Add any Sphinx extension module names here, as strings. They can be 32 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 33 | # ones. 34 | extensions = [ 35 | 'sphinx.ext.autodoc', 36 | 'sphinx.ext.intersphinx', 37 | 'sphinx.ext.todo', 38 | 'sphinx.ext.coverage', 39 | 'sphinx.ext.pngmath', 40 | 'sphinx.ext.viewcode', 41 | 'sphinx.ext.doctest', 42 | ] 43 | 44 | # Add any paths that contain templates here, relative to this directory. 45 | templates_path = ['_templates'] 46 | 47 | # The suffix of source filenames. 48 | source_suffix = '.rst' 49 | 50 | # The encoding of source files. 51 | #source_encoding = 'utf-8-sig' 52 | 53 | # The master toctree document. 54 | master_doc = 'index' 55 | 56 | # General information about the project. 57 | project = 'aiopyramid' 58 | copyright = '2015, Jason Housley' 59 | 60 | # The version info for the project you're documenting, acts as replacement for 61 | # |version| and |release|, also used in various other places throughout the 62 | # built documents. 63 | # 64 | # The short X.Y version. 65 | version = '0.3' 66 | # The full version, including alpha/beta/rc tags. 67 | release = '0.3.1' 68 | 69 | # The language for content autogenerated by Sphinx. Refer to documentation 70 | # for a list of supported languages. 71 | #language = None 72 | 73 | # There are two options for replacing |today|: either, you set today to some 74 | # non-false value, then it is used: 75 | #today = '' 76 | # Else, today_fmt is used as the format for a strftime call. 77 | #today_fmt = '%B %d, %Y' 78 | 79 | # List of patterns, relative to source directory, that match files and 80 | # directories to ignore when looking for source files. 81 | exclude_patterns = ['_build'] 82 | 83 | # The reST default role (used for this markup: `text`) to use for all 84 | # documents. 85 | #default_role = None 86 | 87 | # If true, '()' will be appended to :func: etc. cross-reference text. 88 | #add_function_parentheses = True 89 | 90 | # If true, the current module name will be prepended to all description 91 | # unit titles (such as .. function::). 92 | #add_module_names = True 93 | 94 | # If true, sectionauthor and moduleauthor directives will be shown in the 95 | # output. They are ignored by default. 96 | #show_authors = False 97 | 98 | # The name of the Pygments (syntax highlighting) style to use. 99 | pygments_style = 'sphinx' 100 | 101 | # A list of ignored prefixes for module index sorting. 102 | #modindex_common_prefix = [] 103 | 104 | # If true, keep warnings as "system message" paragraphs in the built documents. 105 | #keep_warnings = False 106 | 107 | 108 | # -- Options for HTML output ---------------------------------------------- 109 | 110 | # The theme to use for HTML and HTML Help pages. See the documentation for 111 | # a list of builtin themes. 112 | # html_theme = 'pyramid' 113 | html_theme = 'alabaster' 114 | 115 | # Theme options are theme-specific and customize the look and feel of a theme 116 | # further. For a list of options available for each theme, see the 117 | # documentation. 118 | html_theme_options = { 119 | 'github_user': 'housleyjk', 120 | 'github_repo': 'aiopyramid', 121 | 'gratipay_user': 'housleyjk', 122 | } 123 | 124 | # Add any paths that contain custom themes here, relative to this directory. 125 | html_theme_path = [alabaster.get_path()] 126 | 127 | # The name for this set of Sphinx documents. If None, it defaults to 128 | # " v documentation". 129 | #html_title = None 130 | 131 | # A shorter title for the navigation bar. Default is the same as html_title. 132 | #html_short_title = None 133 | 134 | # The name of an image file (relative to this directory) to place at the top 135 | # of the sidebar. 136 | #html_logo = None 137 | 138 | # The name of an image file (within the static path) to use as favicon of the 139 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 140 | # pixels large. 141 | #html_favicon = None 142 | 143 | # Add any paths that contain custom static files (such as style sheets) here, 144 | # relative to this directory. They are copied after the builtin static files, 145 | # so a file named "default.css" will overwrite the builtin "default.css". 146 | html_static_path = ['_static'] 147 | 148 | # Add any extra paths that contain custom files (such as robots.txt or 149 | # .htaccess) here, relative to this directory. These files are copied 150 | # directly to the root of the documentation. 151 | #html_extra_path = [] 152 | 153 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 154 | # using the given strftime format. 155 | #html_last_updated_fmt = '%b %d, %Y' 156 | 157 | # If true, SmartyPants will be used to convert quotes and dashes to 158 | # typographically correct entities. 159 | #html_use_smartypants = True 160 | 161 | # Custom sidebar templates, maps document names to template names. 162 | html_sidebars = { 163 | '**': [ 164 | 'about.html', 165 | 'navigation.html', 166 | 'searchbox.html', 167 | 'donate.html', 168 | ] 169 | } 170 | 171 | # Additional templates that should be rendered to pages, maps page names to 172 | # template names. 173 | #html_additional_pages = {} 174 | 175 | # If false, no module index is generated. 176 | #html_domain_indices = True 177 | 178 | # If false, no index is generated. 179 | #html_use_index = True 180 | 181 | # If true, the index is split into individual pages for each letter. 182 | #html_split_index = False 183 | 184 | # If true, links to the reST sources are added to the pages. 185 | #html_show_sourcelink = True 186 | 187 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 188 | #html_show_sphinx = True 189 | 190 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 191 | #html_show_copyright = True 192 | 193 | # If true, an OpenSearch description file will be output, and all pages will 194 | # contain a tag referring to it. The value of this option must be the 195 | # base URL from which the finished HTML is served. 196 | #html_use_opensearch = '' 197 | 198 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 199 | #html_file_suffix = None 200 | 201 | # Output file base name for HTML help builder. 202 | htmlhelp_basename = 'aiopyramiddoc' 203 | 204 | 205 | # -- Options for LaTeX output --------------------------------------------- 206 | 207 | latex_elements = { 208 | # The paper size ('letterpaper' or 'a4paper'). 209 | #'papersize': 'letterpaper', 210 | 211 | # The font size ('10pt', '11pt' or '12pt'). 212 | #'pointsize': '10pt', 213 | 214 | # Additional stuff for the LaTeX preamble. 215 | #'preamble': '', 216 | } 217 | 218 | # Grouping the document tree into LaTeX files. List of tuples 219 | # (source start file, target name, title, 220 | # author, documentclass [howto, manual, or own class]). 221 | latex_documents = [ 222 | ('index', 'aiopyramid.tex', 'aiopyramid Documentation', 223 | 'Jason Housley', 'manual'), 224 | ] 225 | 226 | # The name of an image file (relative to this directory) to place at the top of 227 | # the title page. 228 | #latex_logo = None 229 | 230 | # For "manual" documents, if this is true, then toplevel headings are parts, 231 | # not chapters. 232 | #latex_use_parts = False 233 | 234 | # If true, show page references after internal links. 235 | #latex_show_pagerefs = False 236 | 237 | # If true, show URL addresses after external links. 238 | #latex_show_urls = False 239 | 240 | # Documents to append as an appendix to all manuals. 241 | #latex_appendices = [] 242 | 243 | # If false, no module index is generated. 244 | #latex_domain_indices = True 245 | 246 | 247 | # -- Options for manual page output --------------------------------------- 248 | 249 | # One entry per manual page. List of tuples 250 | # (source start file, name, description, authors, manual section). 251 | man_pages = [ 252 | ('index', 'aiopyramid', 'aiopyramid Documentation', 253 | ['Jason Housley'], 1) 254 | ] 255 | 256 | # If true, show URL addresses after external links. 257 | #man_show_urls = False 258 | 259 | 260 | # -- Options for Texinfo output ------------------------------------------- 261 | 262 | # Grouping the document tree into Texinfo files. List of tuples 263 | # (source start file, target name, title, author, 264 | # dir menu entry, description, category) 265 | texinfo_documents = [ 266 | ('index', 'aiopyramid', 'aiopyramid Documentation', 267 | 'Jason Housley', 'aiopyramid', 'One line description of project.', 268 | 'Miscellaneous'), 269 | ] 270 | 271 | # Documents to append as an appendix to all manuals. 272 | #texinfo_appendices = [] 273 | 274 | # If false, no module index is generated. 275 | #texinfo_domain_indices = True 276 | 277 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 278 | #texinfo_show_urls = 'footnote' 279 | 280 | # If true, do not generate a @detailmenu in the "Top" node's menu. 281 | #texinfo_no_detailmenu = False 282 | 283 | 284 | # Example configuration for intersphinx: refer to the Python standard library. 285 | intersphinx_mapping = { 286 | 'python': ('http://docs.python.org/3.4', None), 287 | 'pyramid': ('http://docs.pylonsproject.org/projects/pyramid/en/latest/', None), 288 | 'gunicorn': ('http://docs.gunicorn.org/en/latest/', None), 289 | 'uwsgi': ('https://uwsgi-docs.readthedocs.org/en/latest/', None), 290 | 'websockets': ('http://aaugustin.github.io/websockets/', None), 291 | } 292 | -------------------------------------------------------------------------------- /docs/tutorial.rst: -------------------------------------------------------------------------------- 1 | Tutorial 2 | ======== 3 | 4 | This is a basic tutorial for setting up a new project with `Aiopyramid`. 5 | 6 | Install Aiopyramid and Initialize Project 7 | ......................................... 8 | 9 | It is highly recommended that you use a virtual environment for your project. The 10 | tutorial will assume that you are using `virtualenvwrapper`_ with a virtualenv 11 | created like so:: 12 | 13 | mkvirtualenv aiotutorial --python=/path/to/python3.4/interpreter 14 | 15 | Once you have your tutorial environment active, install ``Aiopyramid``:: 16 | 17 | pip install aiopyramid 18 | 19 | This will also install the :ref:`Pyramid ` framework. Now create 20 | a new project using the ``aio_websocket`` scaffold. 21 | 22 | pcreate -s aio_websocket aiotutorial 23 | 24 | This will make an ``aiotutorial`` directory with the following structure:: 25 | 26 | . 27 | ├── aiotutorial << Our Python package 28 | │   ├── __init__.py << main file, contains the app constructor 29 | │   ├── templates << directory for storing jinja templates 30 | │   │   └── home.jinja2 << template for the example homepage, contains a websocket test 31 | │   ├── tests.py << tests module, contains tests for each of our existing views 32 | │   └── views.py << views module, contains view callables 33 | ├── CHANGES.rst << file for tracking changes to the library 34 | ├── development.ini << config file, contains project and server settings 35 | ├── MANIFEST.in << manifest file for distributing the project 36 | ├── README.rst << readme for bragging about the project 37 | └── setup.py << Python module for distributing the package and managing dependencies 38 | 39 | Let's look at some of these files a little closer. 40 | 41 | App Constructor 42 | ............... 43 | 44 | The ``aiotutorial/__init__.py`` file contains the constructor for our app. It loads the logging 45 | config from the ``development.ini`` config file and sets up Python logging. This is necessary 46 | because the logging configuration won't be automatically detected when using Python3. Then, it 47 | sets up two routes ``home`` and ``echo`` that we can tie into with our views. Finally, 48 | the constructor scans the project for configuration decorators and builds the wsgi callable. 49 | 50 | The app constructor is the place where we will connect Python libraries to our application and 51 | perform other configuration tasks. 52 | 53 | .. code-block:: python 54 | :linenos: 55 | 56 | import logging.config 57 | 58 | from pyramid.config import Configurator 59 | 60 | 61 | def main(global_config, **settings): 62 | """ This function returns a Pyramid WSGI application. 63 | """ 64 | 65 | # support logging in python3 66 | logging.config.fileConfig( 67 | settings['logging.config'], 68 | disable_existing_loggers=False 69 | ) 70 | 71 | config = Configurator(settings=settings) 72 | config.add_route('home', '/') 73 | config.add_route('echo', '/echo') 74 | config.scan() 75 | return config.make_wsgi_app() 76 | 77 | .. note:: *Thinking Asynchronously* 78 | 79 | The app constructor is called once to setup the application, which means that it is 80 | a synchronous context. The app is constructed before any requests are served, so it 81 | is safe to call blocking code here. 82 | 83 | 84 | Tests 85 | ..... 86 | 87 | The ``aiotutorial/tests.py`` file is a Python module with unittests for each of our views. 88 | Let's look at the test case for the home page: 89 | 90 | .. code-block:: python 91 | :linenos: 92 | 93 | class HomeTestCase(unittest.TestCase): 94 | 95 | def test_home_view(self): 96 | from .views import home 97 | 98 | request = testing.DummyRequest() 99 | info = asyncio.get_event_loop().run_until_complete(home(request)) 100 | self.assertEqual(info['title'], 'aiotutorial websocket test') 101 | 102 | Since test runners for unittest expect tests, such as ``test_home_view``, to run synchronously 103 | but our home view is a :term:`coroutine`, we need to manually obtain an :mod:`asyncio` event 104 | loop and run our view. Line 6 obtains a dummy request from :mod:`pyramid.testing`. We then pass 105 | that request to our view and run it on line 7. Finally, line 8 makes assertions about the kind 106 | of output we expect from our view. 107 | 108 | 109 | Views 110 | ..... 111 | 112 | This is the brains of our application, the place where decisions about how to respond to a particular 113 | :term:`request` are made, and as such this is the place where you will most often start `chaining together 114 | coroutines`_ to perform asynchronous tasks. Let's look at each of the example 115 | views in turn: 116 | 117 | .. code-block:: python 118 | :linenos: 119 | :emphasize-lines: 2,5 120 | 121 | @view_config(route_name='home', renderer='aiotutorial:templates/home.jinja2') 122 | @asyncio.coroutine 123 | def home(request): 124 | wait_time = float(request.params.get('sleep', 0.1)) 125 | yield from asyncio.sleep(wait_time) 126 | return {'title': 'aiotutorial websocket test', 'wait_time': wait_time} 127 | 128 | For those already familiar with :ref:`Pyramid ` most of this view should require 129 | no explanation. The important parts for running asynchronously are lines 2 and 5. 130 | 131 | The :func:`~pyramid.view.view_config` decorator on line 1 ties this view to the 'home' 132 | route declared in the app constructor. It also assigns a :term:`renderer` to the view that will 133 | render the data returned into the ``template/home.jinja`` template and return a response 134 | to the user. Line 2 wraps the view in a coroutine which differentiates it from a generator 135 | or native coroutine. Line 3 is the signature for the coroutine. ``Aiopyramid`` view mappers 136 | do not change the two default signatures for views, i.e. views that accept a request 137 | and views that accept a context and a request. On line 4, we retrieve a sleep parameter, 138 | from the request (the parameter can be either part of the querystring or the body). If 139 | the request doesn't include a sleep parameter, the view defaults to 0.1. We don't need to 140 | use ``yield from`` because ``request.params.get`` doesn't return a :term:`coroutine` or future. 141 | The data for the request exists in memory so retrieving the parameter should be very fast. 142 | Line 5 simulates performing some asynchronous task by suspending the coroutine and delegating to 143 | another coroutine, :func:`asyncio.sleep`, which uses events to wait for ``wait_time`` seconds. 144 | Using ``yield from`` is very important, without it the coroutine would 145 | continue without sleeping. Line 6 returns a Python dictionary that will be passed to the 146 | jinja2 renderer. 147 | 148 | The second view accepts a websocket connection: 149 | 150 | .. code-block:: python 151 | :linenos: 152 | 153 | @view_config(route_name='echo', mapper=WebsocketMapper) 154 | @asyncio.coroutine 155 | def echo(ws): 156 | while True: 157 | message = yield from ws.recv() 158 | if message is None: 159 | break 160 | yield from ws.send(message) 161 | 162 | This view is tied to the 'echo' route from the app constructor. Note that we use a special view mapper 163 | for websocket connections. The :class:`aiopyramid.websocket.config.WebsocketMapper` changes the signature 164 | of the view to accept a single websocket connection instead of a request. The connection object has three methods 165 | for communicating with the :term:`websocket` :meth:`recv`, :meth:`send`, and :meth:`close` that 166 | correspond to similar methods in the `websockets`_ library. 167 | 168 | This websocket view will run echoing the data it receives until the connection is closed. On line 5 we use 169 | ``yield from`` to wait until a message is received. If the message is None, then we know that the websocket 170 | has closed and we break the loop to complete the echo coroutine. Otherwise, line 7 simply returns the same 171 | message back to the websocket. Very simple. In both cases when we need to perform some io we use ``yield from`` 172 | to suspend our coroutine and delegate to another. 173 | 174 | This kind of explicit yielding is a nice advantage for readability in Python code. It shows us exactly where 175 | we are calling asynchronous code. 176 | 177 | Development.ini 178 | ............... 179 | 180 | The ``development.ini`` file contains the config for the project. Most of these settings could be specified in 181 | the app constructor but it makes sense to separate out these values from procedural code. Here is an overview 182 | of the two most important sections:: 183 | 184 | [app:main] 185 | use = egg:aiotutorial 186 | 187 | pyramid.includes = 188 | aiopyramid 189 | pyramid_jinja2 190 | 191 | # for py3 192 | logging.config = %(here)s/development.ini 193 | 194 | The ``[app:main]`` section contains the settings that will be passed to the app constructor as ``settings``. 195 | This is where we include extensions for :ref:`Pyramid ` such as ``Aiopyramid`` and the ``jinja`` 196 | templating library. 197 | 198 | The ``[server:main]`` configures the default server for the project, which in this case is :mod:`gunicorn`:: 199 | 200 | [server:main] 201 | use = egg:gunicorn#main 202 | host = 0.0.0.0 203 | port = 6543 204 | worker_class = aiopyramid.gunicorn.worker.AsyncGunicornWorker 205 | 206 | The ``port`` setting here is the port that we will use to access the application, such as in a browser. The 207 | ``worker_class`` is set to the :class:`aiopyramid.gunicorn.worker.AsyncGunicornWorker` because we need to have 208 | :mod:`gunicorn` setup the :doc:`Aiopyramid Architecture ` for us. 209 | 210 | Setup 211 | ..... 212 | 213 | The ``setup.py`` file makes the ``aiotutorial`` package easy to distribute, and it is also a good way, although 214 | not the only good way, to manage dependencies for our project. Lines 18-21 list the Python packages that we need 215 | for this project. 216 | 217 | .. code-block:: python 218 | 219 | requires = [ 220 | 'aiopyramid[gunicorn]', 221 | 'pyramid_jinja2', 222 | ] 223 | 224 | Note about View Mappers 225 | ....................... 226 | 227 | The default view mapper that ``Aiopyramid`` sets up when it is included by the application tries to be as 228 | robust as possible. It will inspect all of the views that we configure and try to guess whether or not 229 | they are :term:`coroutines `. If the view looks like a :term:`coroutine`, in other words if it has 230 | a ``yield from`` in it, the framework will treat it as a :term:`coroutine`, otherwise it will assume it is 231 | legacy code and will run it in a separate thread to avoid blocking the event loop. This is very important. 232 | 233 | When using ``Aiopyramid`` view mappers, it is actually not necessary to explicitly decorate :term:`view callables ` 234 | with :func:`asyncio.coroutine` as in the examples because the mapper will wrap views that appear to be :term:`coroutines ` 235 | for you. It is still good practice to explicitly wrap your views because it facilitates using them in places where a 236 | view mapper may not be active, but if you are annoyed by the repetition, then you can skip writing ``@asyncio.coroutine`` before 237 | every view as long as you remember what is a :term:`coroutine`. 238 | 239 | Making Sure it Works 240 | .................... 241 | 242 | The last step in initializing the project is to install out dependencies and test out that the scaffold works as we expect:: 243 | 244 | python setup.py develop 245 | 246 | You can also use ``setup.py`` to run unittests:: 247 | 248 | python setup.py test 249 | 250 | You should see the following at the end of the output:: 251 | 252 | 253 | test_home_view (aiotutorial.tests.HomeTestCase) ... ok 254 | test_echo_view (aiotutorial.tests.WSTest) ... ok 255 | 256 | ---------------------------------------------------------------------- 257 | Ran 2 tests in 1.709s 258 | 259 | OK 260 | 261 | If you don't like the test output from ``setup.py``, consider using a test runner like `pytest`_. 262 | 263 | Now try running the server and visiting the homepage:: 264 | 265 | gunicorn --paste development.ini 266 | 267 | Open your browser to http://127.0.0.1:6543 to see the JavaScript test of the our echo websocket. 268 | You should see the following output:: 269 | 270 | aiotutorial websocket test 271 | 272 | CONNECTED 273 | 274 | SENT: Aiopyramid echo test. 275 | 276 | RESPONSE: Aiopyramid echo test. 277 | 278 | DISCONNECTED 279 | 280 | This shows that the websocket is working. If you want to verify that the server is able to handle 281 | multiple requests on a single thread, simply open a different browser (to avoid browser connection 282 | limitations) and go to http://127.0.0.1:6543?sleep=10. The new browser should take roughly ten seconds 283 | to load the page because our view is waiting for the value of ``sleep``. However, while that request is 284 | ongoing, you can refresh your first browser and see that the server is still able to fulfill requests. 285 | 286 | Congratulations! You have successfully setup a highly configurable asynchronous server using ``Aiopyramid``! 287 | 288 | .. note:: *Extra Credit* 289 | 290 | If you really want to see the power of asynchronous programming in Python, obtain a copy of `slowloris`_ 291 | and run it against your knew ``Aiopyramid`` server and some non-asynchronous server. For example, 292 | you could run a simple ``Django`` application with gunicorn. You should see that the ``Aiopyramid`` server 293 | is still able to respond to requests whereas the ``Django`` server is bogged down. You could also use a simple 294 | PHP application using Apache to see this difference. 295 | 296 | .. _pytest: http://pytest.org 297 | .. _virtualenvwrapper: https://virtualenvwrapper.readthedocs.org/en/latest/ 298 | .. _chaining together coroutines: https://docs.python.org/3/library/asyncio-task.html#example-chain-coroutines 299 | .. _websockets: http://aaugustin.github.io/websockets/ 300 | .. _slowloris: http://ha.ckers.org/slowloris/ 301 | -------------------------------------------------------------------------------- /docs/features.rst: -------------------------------------------------------------------------------- 1 | Features 2 | ======== 3 | 4 | Rather than trying to rewrite :ref:`Pyramid `, ``Aiopyramid`` 5 | provides a set of features that will allow you to run existing code asynchronously 6 | where possible. 7 | 8 | Views 9 | ----- 10 | ``Aiopyramid`` provides three view mappers for calling :term:`view callables `: 11 | 12 | * :class:`~aiopyramid.config.CoroutineOrExecutorMapper` maps views to :term:`coroutines ` or separate threads 13 | * :class:`~aiopyramid.config.CoroutineMapper` maps views to :term:`coroutines ` 14 | * :class:`~aiopyramid.config.ExecutorMapper` maps views to separate threads 15 | 16 | When you include ``Aiopyramid``, 17 | the default view mapper is replaced with the :class:`~aiopyramid.config.CoroutineOrExecutorMapper` 18 | which detects whether your :term:`view callable` is a coroutine and does a ``yield from`` to 19 | call it asynchronously. If your :term:`view callable` is not a :term:`coroutine`, it will run it in a 20 | separate thread to avoid blocking the thread with the main loop. :mod:`asyncio` is not thread-safe, 21 | so you will need to guarantee that either in memory resources are not shared between 22 | :term:`view callables ` running in the executor or that such resources are synchronized. 23 | 24 | This means that you should not necessarily have to change existing views. Also, 25 | it is possible to restore the default view mapper, but note that this will mean that 26 | coroutine views that do not specify :class:`~aiopyramid.config.CoroutineMapper` as their 27 | view mapper will fail. 28 | 29 | If most of your view needs to be a :term:`coroutine` but you want to call out to code that blocks, you can 30 | always use `run_in_executor`_. `Aiopyramid` also provides a decorator, :func:`~aiopyramid.helpers.use_executor`, 31 | for specifying declaratively that a particular routine should run in a separate thread. 32 | 33 | For example: 34 | 35 | .. code-block:: python 36 | 37 | import asyncio 38 | from aiopyramid.helpers import use_executor 39 | 40 | class DatabaseUtilies: 41 | 42 | @use_executor # query_it is now a coroutine 43 | def query_it(): 44 | # some code that blocks 45 | 46 | 47 | Authorization 48 | ------------- 49 | 50 | If you are using the default authorization policy, then you will generally not need to make any modifications 51 | to authorize users with ``Aiopyramid``. The exception is if you want to use a callable that performs 52 | some io for your __acl__. In that case you will simply need to use a :term:`synchronized coroutine` so 53 | that the authorization policy can call your :term:`coroutine` like a normal Python function during view lookup. 54 | 55 | For example: 56 | 57 | .. code-block:: python 58 | 59 | import asyncio 60 | 61 | from aiopyramid.helpers import synchronize 62 | 63 | 64 | class MyResource: 65 | """ 66 | This resource uses a callable for it's 67 | __acl__ that accesses the db. 68 | """ 69 | 70 | # this 71 | __acl__ = synchronize(my_coroutine) 72 | 73 | # or this 74 | 75 | @synchronize 76 | @asyncio.coroutine 77 | def __acl__(self): 78 | ... 79 | 80 | # will work 81 | 82 | If you are using a custom authorization policy, most likely it will work with ``Aiopyramid`` in the same 83 | fashion, but it is up to you to guarantee that it does. 84 | 85 | Authentication 86 | -------------- 87 | 88 | Authentication poses a problem because the interface for 89 | :term:`authentication policies ` uses normal Python methods that the framework expects 90 | to call normally but at the same time it is usually necessary to perform some io to retrieve relevant information. 91 | The built-in :term:`authentication policies ` generally accept a callback function that 92 | delegates retrieving :term:`principals ` to the application, but this callback function is also expected 93 | to be called in the regular fashion. So, it is necessary to use a :term:`synchronized coroutine` as a callback 94 | function. 95 | 96 | The final problem is that :term:`synchronized coroutines ` are expected 97 | to be called from within a child :term:`greenlet`, or in other words from within framework code (see :ref:`architecture`). 98 | However, it is often the case that we will want to access the policy through :attr:`pyramid.request.Request.authenticated_userid` 99 | or by calling :func:`~pyramid.security.remember`, etc. from within another coroutine such as a :term:`view callable`. 100 | 101 | To handle both situations, ``Aiopyramid`` provides tools for wrapping a callback-based :term:`authentication policy` to 102 | work asynchronously. For example, the following code in your app constructor will allow you to use a :term:`coroutine` as 103 | a callback. 104 | 105 | .. code-block:: python 106 | 107 | from pyramid.authentication import AuthTktAuthenticationPolicy 108 | from aiopyramid.auth import authn_policy_factory 109 | 110 | from .myauth import get_principals 111 | 112 | ... 113 | 114 | # In the includeme or constructor 115 | authentication = authn_policy_factory( 116 | AuthTktAuthenticationPolicy, 117 | get_principals, 118 | 'sosecret', 119 | hashalg='sha512' 120 | ) 121 | config.set_authentication_policy(authentication) 122 | 123 | 124 | Relevant authentication tools will now return a :term:`coroutine` when called from another :term:`coroutine`, so you 125 | would access the :term:`authentication policy` using ``yield from`` in your :term:`view callable` since it performs io. 126 | 127 | .. code-block:: python 128 | 129 | from pyramid.security import remember, forget 130 | 131 | ... 132 | 133 | # in some coroutine 134 | 135 | maybe = yield from request.unauthenticated_userid 136 | checked = yield from request.authenticated_userid 137 | principals = yield from request.effective_principals 138 | headers = yield from remember(request, 'george') 139 | fheaders = yield from forget(request) 140 | 141 | 142 | .. note:: 143 | 144 | If you don't perform asynchronous io or wrap the :term:`authentication policy` as above, 145 | then don't use ``yield from`` in your view. This approach only works for :term:`coroutine` 146 | views. If you have both :term:`coroutine` views and legacy views running in an executor, 147 | you will probably need to write a custom :term:`authentication policy`. 148 | 149 | Tweens 150 | ------ 151 | :ref:`Pyramid ` allows you to write :term:`tweens ` which wrap the request/response chain. Most 152 | existing :term:`tweens ` expect those :term:`tweens ` above and below them to run synchronously. Therefore, 153 | if you have a :term:`tween` that needs to run asynchronously (e.g. it looks up some data from a 154 | database for each request), then you will need to write that `tween` so that it can wait 155 | without other :term:`tweens ` needing to explicitly ``yield from`` it. For example: 156 | 157 | .. code-block:: python 158 | 159 | import asyncio 160 | 161 | from aiopyramid.helpers import synchronize 162 | 163 | 164 | def coroutine_logger_tween_factory(handler, registry): 165 | """ 166 | Example of an asynchronous tween that delegates 167 | a synchronous function to a child thread. 168 | This tween asynchronously logs all requests and responses. 169 | """ 170 | 171 | # We use the synchronize decorator because we will call this 172 | # coroutine from a normal python context 173 | @synchronize 174 | # this is a coroutine 175 | @asyncio.coroutine 176 | def _async_print(content): 177 | # print doesn't really need to be run in a separate thread 178 | # but it works for demonstration purposes 179 | 180 | yield from asyncio.get_event_loop().run_in_executor( 181 | None, 182 | print, 183 | content 184 | ) 185 | 186 | def coroutine_logger_tween(request): 187 | # The following calls are guaranteed to happen in order 188 | # but they do not block the event loop 189 | 190 | # print the request on the aio event loop 191 | # without needing to say yield 192 | # at this point, 193 | # other coroutines and requests can be handled 194 | _async_print(request) 195 | 196 | # get response, this should be done in this greenlet 197 | # and not as a coroutine because this will call 198 | # the next tween and subsequently yield if necessary 199 | response = handler(request) 200 | 201 | # print the response on the aio event loop 202 | _async_print(request) 203 | 204 | # return response after logging is done 205 | return response 206 | 207 | return coroutine_logger_tween 208 | 209 | Traversal 210 | --------- 211 | When using :ref:`Pyramid's ` :term:`traversal` view lookup, 212 | it is often the case that you will want to 213 | make some io calls to a database or storage when traversing via `__getitem__`. When using the default 214 | traverser, :ref:`Pyramid ` will call `__getitem__` as a normal Python function. Therefore, 215 | it is necessary to synchronize `__getitem__` on any asynchronous resources like so: 216 | 217 | .. code-block:: python 218 | 219 | import asyncio 220 | 221 | from aiopyramid.helpers import synchronize 222 | 223 | 224 | class MyResource: 225 | """ This resource performs some asynchronous io. """ 226 | 227 | __name__ = "example" 228 | __parent__ = None 229 | 230 | @synchronize 231 | @asyncio.coroutine 232 | def __getitem__(self, key): 233 | yield from self.example_coroutine() 234 | return self # no matter the path, this is the context 235 | 236 | @asyncio.coroutine 237 | def example_coroutine(self): 238 | yield from asyncio.sleep(0.1) 239 | print('I am some async task.') 240 | 241 | Servers 242 | ------- 243 | 244 | ``Aiopyramid`` supports both asynchronous `gunicorn`_ and the `uWSGI asyncio plugin`_. 245 | 246 | Example `gunicorn`_ config: 247 | 248 | .. code-block:: ini 249 | 250 | [server:main] 251 | use = egg:gunicorn#main 252 | host = 0.0.0.0 253 | port = 6543 254 | worker_class = aiopyramid.gunicorn.worker.AsyncGunicornWorker 255 | 256 | Example `uWSGI`_ config: 257 | 258 | .. code-block:: ini 259 | 260 | [uwsgi] 261 | http-socket = 0.0.0.0:6543 262 | workers = 1 263 | plugins = 264 | asyncio = 50 265 | greenlet 266 | 267 | For those setting up ``Aiopyramid`` on a Mac, Ander Ustarroz's `tutorial`_ may prove useful. 268 | Rickert Mulder has also provided a fork of `uWSGI`_ that allows for quick installation by running 269 | `pip install git+git://github.com/circlingthesun/uwsgi.git` in a virtualenv. 270 | 271 | Websockets 272 | ---------- 273 | 274 | ``Aiopyramid`` provides additional view mappers for handling websocket connections with either 275 | `gunicorn`_ or `uWSGI`_. Websockets with `gunicorn`_ use the `websockets`_ library whereas 276 | `uWSGI`_ has native :term:`websocket` support. In either case, the interface is the same. 277 | 278 | A function :term:`view callable` for a :term:`websocket` connection follows this pattern: 279 | 280 | .. code-block:: python 281 | 282 | @view_config(mapper=) 283 | def websocket_callable(ws): 284 | # do stuff with ws 285 | 286 | 287 | The ``ws`` argument passed to the callable has three methods for communicating with the :term:`websocket` 288 | :meth:`recv`, :meth:`send`, and :meth:`close` methods, which correspond to similar methods in the `websockets`_ library. 289 | A :term:`websocket` connection that echoes all messages using `gunicorn`_ would be: 290 | 291 | .. code-block:: python 292 | 293 | from pyramid.view import view_config 294 | from aiopyramid.websocket.config import WebsocketMapper 295 | 296 | @view_config(route_name="ws", mapper=WebsocketMapper) 297 | def echo(ws): 298 | while True: 299 | message = yield from ws.recv() 300 | if message is None: 301 | break 302 | yield from ws.send(message) 303 | 304 | ``Aiopyramid`` also provides a :term:`view callable` class :class:`~aiopyramid.websocket.view.WebsocketConnectionView` 305 | that has :meth:`~aiopyramid.websocket.view.WebsocketConnectionView.on_message`, 306 | :meth:`~aiopyramid.websocket.view.WebsocketConnectionView.on_open`, 307 | and :meth:`~aiopyramid.websocket.view.WebsocketConnectionView.on_close` callbacks. 308 | Class-based websocket views also have a :meth:`~aiopyramid.websocket.view.WebsocketConnectionView.send` convenience method, 309 | otherwise the underlying ``ws`` may be accessed as :attr:`self.ws`. 310 | Simply extend :class:`~aiopyramid.websocket.view.WebsocketConnectionView` 311 | specifying the correct :term:`view mapper` for your server either via the :attr:`__view_mapper__` attribute or the 312 | :func:`view_config ` decorator. The above example could be rewritten in a larger project, this time using `uWSGI`_, 313 | as follows: 314 | 315 | .. code-block:: python 316 | 317 | from pyramid.view import view_config 318 | from aiopyramid.websocket.view import WebsocketConnectionView 319 | from aiopyramid.websocket.config import UWSGIWebsocketMapper 320 | 321 | from myproject.resources import MyWebsocketContext 322 | 323 | class MyWebsocket(WebsocketConnectionView): 324 | __view_mapper__ = UWSGIWebsocketMapper 325 | 326 | 327 | @view_config(context=MyWebsocketContext) 328 | class EchoWebsocket(MyWebsocket): 329 | 330 | def on_message(self, message): 331 | yield from self.send(message) 332 | 333 | 334 | The underlying websocket implementations of `uWSGI`_ and `websockets`_ differ in how they pass on 335 | the WebSocket message. `uWSGI`_ always sends `bytes` even when the WebSocket frame indicates that 336 | the message is text, whereas `websockets`_ decodes text messages to `str`. 337 | `Aiopyramid` attempts to match the behavior of `websockets`_ by default, which means 338 | that it coerces messages from `uWSGI`_ to `str` where possible. To adjust this behavior, you can set the 339 | :attr:`~aiopyramid.websocket.config.UWSGIWebsocketMapper.use_str` flag to `False`, or alternatively to coerce 340 | `websockets`_ messages back to bytes, set the :attr:`~aiopyramid.websocket.config.WebsocketMapper.use_bytes` 341 | flag to True: 342 | 343 | .. code-block:: python 344 | 345 | # In your app constructor 346 | from aiopyramid.websocket.config import WebsocketMapper 347 | 348 | WebsocketMapper.use_bytes = True 349 | 350 | 351 | uWSGI Special Note 352 | .................. 353 | 354 | ``Aiopyramid`` uses a special :class:`~aiopyramid.websocket.exceptions.WebsocketClosed` exception 355 | to disconnect a :term:`greenlet` after a :term:`websocket` 356 | has been closed. This exception will be visible in log output when using `uWSGI`_. In order to squelch this 357 | message, wrap the wsgi application in the :func:`~aiopyramid.websocket.helpers.ignore_websocket_closed` middleware 358 | in your application's constructor like so: 359 | 360 | .. code-block:: python 361 | 362 | from aiopyramid.websocket.helpers import ignore_websocket_closed 363 | 364 | ... 365 | app = config.make_wsgi_app() 366 | return ignore_websocket_closed(app) 367 | 368 | 369 | .. _gunicorn: http://gunicorn.org 370 | .. _uWSGI: https://github.com/unbit/uwsgi 371 | .. _uWSGI asyncio plugin: http://uwsgi-docs.readthedocs.org/en/latest/asyncio.html 372 | .. _websockets: http://aaugustin.github.io/websockets/ 373 | .. _tutorial: http://www.developerfiles.com/installing-uwsgi-with-asyncio-on-mac-os-x-10-10-yosemite/ 374 | .. _run_in_executor: https://docs.python.org/3/library/asyncio-eventloop.html#asyncio.BaseEventLoop.run_in_executor 375 | --------------------------------------------------------------------------------