Skip to content

Commit

Permalink
Add authentication support for coroutines.
Browse files Browse the repository at this point in the history
  • Loading branch information
housleyjk committed Dec 6, 2014
1 parent 0203a77 commit 1ee06de
Show file tree
Hide file tree
Showing 16 changed files with 461 additions and 87 deletions.
5 changes: 4 additions & 1 deletion CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,13 @@ Changes

.. :changelog:
0.2.5 (2014-12-01)
0.3.0 (2014-12-06)
------------------
- Add sphinx
- Migrate README to sphinx docs
- Add helpers for authentication
- Deprecated aiopyramid.traversal, use aiopyramid.helpers.synchronize
- Deprecated aiopyramid.tweens, moved examples to docs

0.2.4 (2014-10-06)
------------------
Expand Down
80 changes: 80 additions & 0 deletions aiopyramid/auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
"""
Utilities for making :ref:`Pyramid <pyramid:index>` authentication
and authorization work with Aiopyramid.
"""


from .helpers import spawn_greenlet_on_scope_error, synchronize


def coroutine_callback_authentication_policy_factory(
policy_class,
coroutine=None,
*args,
**kwargs
):
"""
Factory function for creating an AuthenticationPolicy instance that uses
a :term:`coroutine` as a callback.
:param policy_class: The AuthenticationPolicy to wrap.
:param coroutine coroutine: If provided this is passed to
the AuthenticationPolicy as the callback argument.
Extra arguments and keyword arguments are passed to
the AuthenticationPolicy, so if the AuthenticationPolicy expects
a callback under another name, it is necessary to pass
a :term:`synchronized coroutine` as an argument or keyword argument
to this factory or use
:class:`~aiopyramid.auth.CoroutineAuthenticationPolicyProxy` directly.
This function is also aliased as
:func:`aiopyramid.auth.authn_policy_factory`.
"""

if coroutine:
coroutine = synchronize(coroutine)
policy = policy_class(callback=coroutine, *args, **kwargs)
else:
policy = policy_class(*args, **kwargs)
return CoroutineAuthenticationPolicyProxy(policy)


authn_policy_factory = coroutine_callback_authentication_policy_factory


class CoroutineAuthenticationPolicyProxy:
"""
This authentication policy proxies calls to another policy that uses
a callback to retrieve principals. Because this callback may be a
:term:`synchronized coroutine`, this class handles the case where the
callback fails due to a :class:`~aiopyramid.exceptions.ScopeError` and
generates the appropriate ``Aiopyramid`` architecture.
"""

def __init__(self, policy):
"""
:param class policy: The authentication policy to wrap.
"""

self._policy = policy

@spawn_greenlet_on_scope_error
def remember(self, request, principal, **kwargs):
return self._policy.remember(request, principal, **kwargs)

@spawn_greenlet_on_scope_error
def forget(self, request):
return self._policy.forget(request)

@spawn_greenlet_on_scope_error
def unauthenticated_userid(self, request):
return self._policy.unauthenticated_userid(request)

@spawn_greenlet_on_scope_error
def authenticated_userid(self, request):
return self._policy.authenticated_userid(request)

@spawn_greenlet_on_scope_error
def effective_principals(self, request):
return self._policy.effective_principals(request)
62 changes: 48 additions & 14 deletions aiopyramid/helpers.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,30 @@
import asyncio
import inspect
import functools
import logging

import greenlet
from pyramid.exceptions import ConfigurationError

from .exceptions import ScopeError

SCOPE_ERROR_MESSAGE = '''
Synchronized coroutine {} called in the parent
greenlet.
This is most likely because you called the synchronized
coroutine inside of another coroutine. You need to
yield from the coroutine directly without wrapping
it in aiopyramid.helpers.synchronize.
If you are calling this coroutine indirectly from
a regular function and therefore cannot yield from it,
then you need to run the first caller inside a new
greenlet using aiopyramid.helpers.spawn_greenlet.
'''

log = logging.getLogger(__name__)


def is_generator(func):
""" Tests whether `func` is capable of becoming an `asyncio.coroutine`. """
Expand Down Expand Up @@ -83,20 +101,7 @@ def _wrapped_coroutine(*args, **kwargs):
if this.parent is None:
if strict:
raise ScopeError(
'''
Synchronized coroutine {} called in the parent
greenlet.
This is most likely because you called the synchronized
coroutine inside of another coroutine. You need to
yield from the coroutine directly without wrapping
it in aiopyramid.helpers.synchronize.
If you are calling this coroutine indirectly from
a regular function and therefore cannot yield from it,
then you need to run the first caller inside a new
greenlet using aiopyramid.helpers.spawn_greenlet.
'''
SCOPE_ERROR_MESSAGE.format(coroutine_func)
)
else:
return coroutine_func(*args, **kwargs)
Expand All @@ -122,3 +127,32 @@ def _wrapped_coroutine(*args, **kwargs):
return _wrapper(coroutine_func)
except IndexError:
return _wrapper


def spawn_greenlet_on_scope_error(func):
"""
Wraps a callable handling any
:class:`ScopeErrors <~aiopyramid.exceptions.ScopeError>` that may
occur because the callable is called from inside of a :term:`coroutine`.
If no :class:`~aiopyramid.exceptions.ScopeError` occurs, the callable is
executed normally and return arguments are passed through, otherwise, when
a :class:`~aiopyramid.exceptions.ScopeError` does occur, a coroutine to
retrieve the result of the callable is returned instead.
"""

@functools.wraps(func)
def _run_or_return_future(*args, **kwargs):
this = greenlet.getcurrent()
# Check if we should see a ScopeError
if this.parent is None:
return spawn_greenlet(func, *args, **kwargs)
else:
try:
return func(*args, **kwargs)
except ScopeError:
# ScopeError generated in multiple levels of indirection
log.warn('Unexpected ScopeError encountered.')
return spawn_greenlet(func, *args, **kwargs)

return _run_or_return_future
9 changes: 9 additions & 0 deletions aiopyramid/traversal.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
"""
The aiopyramid.traversal module is deprecated, use aiopyramid.helpers.synchronize instead.
See http://aiopyramid.readthedocs.org/en/latest/features.html#traversal.
""" # noqa

import warnings
warnings.warn(__doc__, DeprecationWarning)


import asyncio

from pyramid.traversal import (
Expand Down
8 changes: 8 additions & 0 deletions aiopyramid/tweens.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
"""
The aiopyramid.tweens module is deprecated. See example in the docs:
http://aiopyramid.readthedocs.org/en/latest/features.html#tweens.
"""

import warnings
warnings.warn(__doc__, DeprecationWarning)

import asyncio

from .helpers import synchronize
Expand Down
3 changes: 0 additions & 3 deletions docs/aiopyramid.rst
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,6 @@ aiopyramid.traversal module
---------------------------

.. automodule:: aiopyramid.traversal
:members:
:undoc-members:
:show-inheritance:

aiopyramid.tweens module
------------------------
Expand Down
21 changes: 12 additions & 9 deletions docs/approach.rst
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
.. _architecture:

Architecture
============

Expand Down Expand Up @@ -55,7 +57,7 @@ Please feel free to use this in other :mod:`asyncio` projects that don't use :re
because it's awesome.

To avoid confusion, it is worth making explicit the fact that this approach is for incorporating code that is
fast and non-blocking itself but needs to call a coroutine to do some blocking task. Don't try to use this to
fast and non-blocking itself but needs to call a coroutine to do some io. Don't try to use this to
call long-running or blocking Python functions. Instead, use `run_in_executor`_, which is what ``Aiopyramid``
does by default with :term:`view callables <view callable>` that don't appear to be :term:`coroutines <coroutine>`.

Expand All @@ -69,7 +71,7 @@ for the following reasons:
- The `pyramid_asyncio`_ library depends on patches made to the :ref:`Pyramid <pyramid:index>` router that prevent it
from working with the `uWSGI asyncio plugin`_.
- The `pyramid_asyncio`_ rewrites various parts of :ref:`Pyramid <pyramid:index>`,
including tweens, to expect :term:`coroutins <coroutine>` from :ref:`Pyramid <pyramid:index>` internals.
including tweens, to expect :term:`coroutines <coroutine>` from :ref:`Pyramid <pyramid:index>` internals.

On the other hand ``Aiopyramid`` is designed to follow these principles:

Expand All @@ -79,10 +81,10 @@ On the other hand ``Aiopyramid`` is designed to follow these principles:
to call out to some function that blocks (in other words, the programmer forgets to wrap long-running calls in `run_in_executor`_).
So, frameworks should leave the determination of what code is safe to the programmer and instead provide tools for
programmers to make educated decisions about what Python libraries can be used on an asynchronous server. Following the
:ref:`Pyramid <pyramid:index>` philosophy, frameworks should get out of the way.
:ref:`Pyramid <pyramid:index>` philosophy, frameworks should not get in the way.

The first principle is one of the reasons why I used view mappers rather than patching the router.
View mappers are a mechanism already in place to handle how views are called. We don't need to rewrite
The first principle is one of the reasons why I used :term:`view mappers <view mapper>` rather than patching the router.
:term:`View mappers <view mapper>` are a mechanism already in place to handle how views are called. We don't need to rewrite
vast parts of :ref:`Pyramid <pyramid:index>` to run a view in the :mod:`asyncio` event loop.
Yes, :ref:`Pyramid <pyramid:index>` is that awesome.

Expand All @@ -92,7 +94,7 @@ should not be rewritten as :term:`coroutines <coroutine>` because we don't know

Most of the :ref:`Pyramid <pyramid:index>` framework does not run io blocking code. So, it is not actually necessary to change the
framework itself. Instead we need tools for making application code asynchronous. It should be possible
to run an existing url dispatch application asynchronously without modification. Blocking code will naturally end
to run an existing simple url dispatch application asynchronously without modification. Blocking code will naturally end
up being run in a separate thread via the `run_in_executor`_ method. This allows you to optimize
only those highly concurrent views in your application or add in websocket support without needing to refactor
all of the code.
Expand All @@ -108,9 +110,10 @@ For example, include the following in your application's constructor:
...
asyncio.get_event_loop().set_default_executor(ThreadPoolExecutor(max_workers=150))
It should be noted that ``Aiopyramid`` is not thread-safe by nature. You will need to ensure that in memory
resources are not modified by multiple non-coroutine :term:`view callables <view callable>`. For most existing applications, this
should not be a problem.
.. note::
It should be noted that ``Aiopyramid`` is not thread-safe by nature. You will need to ensure that in memory
resources are not modified by multiple non-coroutine :term:`view callables <view callable>`. For most existing applications, this
should not be a problem.

.. _uWSGI: https://github.com/unbit/uwsgi
.. _pyramid_debugtoolbar: https://github.com/Pylons/pyramid_debugtoolbar
Expand Down
4 changes: 2 additions & 2 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,9 @@
# built documents.
#
# The short X.Y version.
version = '0.2.4'
version = '0.3'
# The full version, including alpha/beta/rc tags.
release = '0.2'
release = '0.3.0'

# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
Expand Down
Loading

0 comments on commit 1ee06de

Please sign in to comment.