Merge with pyramid-cubicweb
authorYann Voté <yann.vote@logilab.fr>
Mon, 26 Sep 2016 14:52:12 +0200
changeset 11631 faf279e33298
parent 11478 1817f8946c22 (current diff)
parent 11630 1400aee10df4 (diff)
child 11681 b23d58050076
Merge with pyramid-cubicweb The following tasks have been done: - merge packaging files - merge documentation - move pyramid_cubicweb package at cubicweb/pyramid and update imports accordingly - rename tests directory into test - move pyramid-cubicweb README.rst into README.pyramid.rst until better idea - add a test dependency on unreleased cubicweb-pyramid to have both py27 and py34 tests pass Closes #14023058.
MANIFEST.in
README.pyramid.rst
cubicweb/__pkginfo__.py
cubicweb/pyramid/__init__.py
cubicweb/pyramid/auth.py
cubicweb/pyramid/bwcompat.py
cubicweb/pyramid/core.py
cubicweb/pyramid/defaults.py
cubicweb/pyramid/init_instance.py
cubicweb/pyramid/login.py
cubicweb/pyramid/predicates.py
cubicweb/pyramid/profile.py
cubicweb/pyramid/resources.py
cubicweb/pyramid/rest_api.py
cubicweb/pyramid/session.py
cubicweb/pyramid/test/__init__.py
cubicweb/pyramid/test/data/bootstrap_cubes
cubicweb/pyramid/test/test_bw_request.py
cubicweb/pyramid/test/test_core.py
cubicweb/pyramid/test/test_login.py
cubicweb/pyramid/test/test_rest_api.py
cubicweb/pyramid/test/test_tools.py
cubicweb/pyramid/tools.py
cubicweb/test/unittest_cwconfig.py
debian/control
debian/copyright
debian/rules
doc/api/pyramid.rst
doc/api/pyramid/auth.rst
doc/api/pyramid/authplugin.rst
doc/api/pyramid/bwcompat.rst
doc/api/pyramid/core.rst
doc/api/pyramid/defaults.rst
doc/api/pyramid/login.rst
doc/api/pyramid/profile.rst
doc/api/pyramid/session.rst
doc/api/pyramid/tools.rst
doc/book/pyramid/auth.rst
doc/book/pyramid/ctl.rst
doc/book/pyramid/index.rst
doc/book/pyramid/profiling.rst
doc/book/pyramid/quickstart.rst
doc/book/pyramid/settings.rst
doc/conf.py
doc/index.rst
requirements/test-misc.txt
tox.ini
--- a/MANIFEST.in	Fri Sep 23 16:04:32 2016 +0200
+++ b/MANIFEST.in	Mon Sep 26 14:52:12 2016 +0200
@@ -1,4 +1,5 @@
 include README
+include README.pyramid.rst
 include COPYING
 include COPYING.LESSER
 include pylintrc
@@ -13,7 +14,7 @@
 recursive-include doc/book *
 recursive-include doc/tools *.py
 recursive-include doc/tutorials *.rst *.py
-include doc/api/*.rst
+recursive-include doc/api *.rst
 recursive-include doc/_themes *
 recursive-include doc/_static *
 include doc/_templates/*.html
@@ -47,6 +48,7 @@
 recursive-include cubicweb/ext/test/data *.py
 recursive-include cubicweb/hooks/test/data-computed *.py
 recursive-include cubicweb/hooks/test/data bootstrap_cubes *.py
+recursive-include cubicweb/pyramid/test/data bootstrap_cubes
 recursive-include cubicweb/sobjects/test/data bootstrap_cubes *.py
 recursive-include cubicweb/server/test/data bootstrap_cubes *.py source* *.conf.in *.ldif
 recursive-include cubicweb/server/test/data-cwep002 *.py
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/README.pyramid.rst	Mon Sep 26 14:52:12 2016 +0200
@@ -0,0 +1,85 @@
+
+pyramid_cubicweb_ is one specific way of integrating CubicWeb_ with a
+Pyramid_ web application.
+
+Features
+========
+
+* provides a default route that let a cubicweb instance handle the request.
+
+Usage
+=====
+
+To use, install ``pyramid_cubicweb`` in your python environment, and
+then include_ the package::
+
+    config.include('pyramid_cubicweb')
+
+    
+Configuration
+=============
+
+Requires the following `INI setting / environment variable`_:
+
+* `cubicweb.instance` / `CW_INSTANCE`: the cubicweb instance name
+
+Authentication cookies
+----------------------
+
+When using the `pyramid_cubicweb.auth` (CubicWeb AuthTkt
+authentication policy), which is the default in most cases, you may
+have to configure the behaviour of these authentication policies using
+standard's Pyramid configuration. You may want to configure in your
+``pyramid.ini``:
+
+:Session Authentication:
+
+    This is a `AuthTktAuthenticationPolicy`_ so you may overwrite default
+    configuration values by adding configuration entries using the prefix
+    ``cubicweb.auth.authtkt.session``. Default values are:
+
+    ::
+
+        cubicweb.auth.authtkt.session.hashalg = sha512
+        cubicweb.auth.authtkt.session.cookie_name = auth_tkt
+        cubicweb.auth.authtkt.session.timeout = 1200
+        cubicweb.auth.authtkt.session.reissue_time = 120
+        cubicweb.auth.authtkt.session.http_only = True
+        cubicweb.auth.authtkt.session.secure = True
+
+
+:Persistent Authentication:
+
+    This is also a `AuthTktAuthenticationPolicy`_. It is used when persistent
+    sessions are activated (typically when using the cubicweb-rememberme_
+    cube). You may overwrite default configuration values by adding
+    configuration entries using the prefix
+    ``cubicweb.auth.authtkt.persistent``. Default values are:
+
+    ::
+
+        cubicweb.auth.authtkt.persistent.hashalg = sha512
+        cubicweb.auth.authtkt.persistent.cookie_name = pauth_tkt
+        cubicweb.auth.authtkt.persistent.max_age = 3600*24*30
+        cubicweb.auth.authtkt.persistent.reissue_time = 3600*24
+        cubicweb.auth.authtkt.persistent.http_only = True
+        cubicweb.auth.authtkt.persistent.secure = True
+
+
+.. Warning:: Legacy timeout values from the instance's
+             ``all-in-one.conf`` are **not** used at all (``
+             http-session-time`` and ``cleanup-session-time``)
+
+Please refer to the documentation_ for more details (available in the
+``docs`` directory of the source code).
+
+.. _pyramid_cubicweb: https://www.cubicweb.org/project/pyramid-cubicweb
+.. _CubicWeb: https://www.cubicweb.org/
+.. _`cubicweb-rememberme`: \
+    https://www.cubicweb.org/project/cubicweb-rememberme
+.. _Pyramid: http://pypi.python.org/pypi/pyramid
+.. _include: http://docs.pylonsproject.org/projects/pyramid/en/latest/api/config.html#pyramid.config.Configurator.include
+.. _`INI setting / environment variable`: http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html#adding-a-custom-setting
+.. _documentation: http://pyramid-cubicweb.readthedocs.org/
+.. _AuthTktAuthenticationPolicy: \
+    http://docs.pylonsproject.org/projects/pyramid/en/latest/api/authentication.html#pyramid.authentication.AuthTktAuthenticationPolicy
--- a/cubicweb/__pkginfo__.py	Fri Sep 23 16:04:32 2016 +0200
+++ b/cubicweb/__pkginfo__.py	Mon Sep 26 14:52:12 2016 +0200
@@ -59,6 +59,11 @@
     'pytz': '',
     'Markdown': '',
     'unittest2': '>= 0.7.0',
+    # pyramid dependencies
+    'pyramid': '>= 1.5.0',
+    'waitress': '>= 0.8.9',
+    'wsgicors': '>= 0.3',
+    'pyramid_multiauth': '',
     }
 
 __recommends__ = {
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/pyramid/__init__.py	Mon Sep 26 14:52:12 2016 +0200
@@ -0,0 +1,193 @@
+import os
+from warnings import warn
+import wsgicors
+
+from cubicweb.cwconfig import CubicWebConfiguration as cwcfg
+from pyramid.config import Configurator
+from pyramid.settings import asbool, aslist
+
+try:
+    from configparser import SafeConfigParser
+except ImportError:
+    from ConfigParser import SafeConfigParser
+
+
+def make_cubicweb_application(cwconfig, settings=None):
+    """
+    Create a pyramid-based CubicWeb instance from a cubicweb configuration.
+
+    It is initialy meant to be used by the 'pyramid' command of cubicweb-ctl.
+
+    :param cwconfig: A CubicWeb configuration
+    :returns: A Pyramid config object
+    """
+    settings = dict(settings) if settings else {}
+    settings.update(settings_from_cwconfig(cwconfig))
+    config = Configurator(settings=settings)
+    config.registry['cubicweb.config'] = cwconfig
+    config.include('cubicweb.pyramid')
+    return config
+
+def settings_from_cwconfig(cwconfig):
+    '''
+    Extract settings from pyramid.ini and pyramid-debug.ini (if in debug)
+
+    Can be used to configure middleware WSGI with settings from pyramid.ini files
+
+    :param cwconfig: A CubicWeb configuration
+    :returns: A settings dictionnary
+    '''
+    settings_filenames = [os.path.join(cwconfig.apphome, 'pyramid.ini')]
+    settings = {}
+    if cwconfig.debugmode:
+        settings_filenames.insert(
+            0, os.path.join(cwconfig.apphome, 'pyramid-debug.ini'))
+    
+        settings.update({
+            'pyramid.debug_authorization': True,
+            'pyramid.debug_notfound': True,
+            'pyramid.debug_routematch': True,
+            'pyramid.reload_templates': True,
+        })
+    
+    for fname in settings_filenames:
+        if os.path.exists(fname):
+            cp = SafeConfigParser()
+            cp.read(fname)
+            settings.update(cp.items('main'))
+            break
+    
+    return settings
+
+
+def wsgi_application_from_cwconfig(
+        cwconfig,
+        profile=False, profile_output=None, profile_dump_every=None):
+    """ Build a WSGI application from a cubicweb configuration
+
+    :param cwconfig: A CubicWeb configuration
+    :param profile: Enable profiling. See :ref:`profiling`.
+    :param profile_output: Profiling output filename. See :ref:`profiling`.
+    :param profile_dump_every: Profiling number of requests before dumping the
+                               stats. See :ref:`profiling`.
+
+    :returns: A fully operationnal WSGI application
+    """
+    config = make_cubicweb_application(cwconfig)
+    profile = profile or asbool(config.registry.settings.get(
+        'cubicweb.profile.enable', False))
+    if profile:
+        config.add_route('profile_ping', '_profile/ping')
+        config.add_route('profile_cnx', '_profile/cnx')
+        config.scan('cubicweb.pyramid.profile')
+    app = config.make_wsgi_app()
+    # This replaces completely web/cors.py, which is not used by
+    # cubicweb.pyramid anymore
+    app = wsgicors.CORS(
+        app,
+        origin=' '.join(cwconfig['access-control-allow-origin']),
+        headers=', '.join(cwconfig['access-control-allow-headers']),
+        methods=', '.join(cwconfig['access-control-allow-methods']),
+        credentials='true')
+
+    if profile:
+        from cubicweb.pyramid.profile import wsgi_profile
+        filename = profile_output or config.registry.settings.get(
+            'cubicweb.profile.output', 'program.prof')
+        dump_every = profile_dump_every or config.registry.settings.get(
+            'cubicweb.profile.dump_every', 100)
+        app = wsgi_profile(app, filename=filename, dump_every=dump_every)
+    return app
+
+
+def wsgi_application(instance_name=None, debug=None):
+    """ Build a WSGI application from a cubicweb instance name
+
+    :param instance_name: Name of the cubicweb instance (optional). If not
+                          provided, :envvar:`CW_INSTANCE` must exists.
+    :param debug: Enable/disable the debug mode. If defined to True or False,
+                  overrides :envvar:`CW_DEBUG`.
+
+    The following environment variables are used if they exist:
+
+    .. envvar:: CW_INSTANCE
+
+        A CubicWeb instance name.
+
+    .. envvar:: CW_DEBUG
+
+        If defined, the debugmode is enabled.
+
+    The function can be used as an entry-point for third-party wsgi containers.
+    Below is a sample uswgi configuration file:
+
+    .. code-block:: ini
+
+        [uwsgi]
+        http = 127.0.1.1:8080
+        env = CW_INSTANCE=myinstance
+        env = CW_DEBUG=1
+        module = cubicweb.pyramid:wsgi_application()
+        virtualenv = /home/user/.virtualenvs/myvirtualenv
+        processes = 1
+        threads = 8
+        stats = 127.0.0.1:9191
+        plugins = http,python
+
+    """
+    if instance_name is None:
+        instance_name = os.environ['CW_INSTANCE']
+    if debug is None:
+        debug = 'CW_DEBUG' in os.environ
+
+    cwconfig = cwcfg.config_for(instance_name, debugmode=debug)
+
+    return wsgi_application_from_cwconfig(cwconfig)
+
+
+def includeme(config):
+    """Set-up a CubicWeb instance.
+
+    The CubicWeb instance can be set in several ways:
+
+    -   Provide an already loaded CubicWeb config instance in the registry:
+
+        .. code-block:: python
+
+            config.registry['cubicweb.config'] = your_config_instance
+
+    -   Provide an instance name in the pyramid settings with
+        :confval:`cubicweb.instance`.
+
+    """
+    cwconfig = config.registry.get('cubicweb.config')
+
+    if cwconfig is None:
+        debugmode = asbool(
+            config.registry.settings.get('cubicweb.debug', False))
+        cwconfig = cwcfg.config_for(
+            config.registry.settings['cubicweb.instance'], debugmode=debugmode)
+        config.registry['cubicweb.config'] = cwconfig
+
+    if cwconfig.debugmode:
+        try:
+            config.include('pyramid_debugtoolbar')
+        except ImportError:
+            warn('pyramid_debugtoolbar package not available, install it to '
+                 'get UI debug features', RuntimeWarning)
+
+    config.registry['cubicweb.repository'] = repo = cwconfig.repository()
+    config.registry['cubicweb.registry'] = repo.vreg
+
+    if asbool(config.registry.settings.get('cubicweb.defaults', True)):
+        config.include('cubicweb.pyramid.defaults')
+
+    for name in aslist(config.registry.settings.get('cubicweb.includes', [])):
+        config.include(name)
+
+    config.include('cubicweb.pyramid.tools')
+    config.include('cubicweb.pyramid.predicates')
+    config.include('cubicweb.pyramid.core')
+
+    if asbool(config.registry.settings.get('cubicweb.bwcompat', True)):
+        config.include('cubicweb.pyramid.bwcompat')
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/pyramid/auth.py	Mon Sep 26 14:52:12 2016 +0200
@@ -0,0 +1,180 @@
+import datetime
+import logging
+import warnings
+
+from zope.interface import implementer
+
+from pyramid.settings import asbool
+from pyramid.authorization import ACLAuthorizationPolicy
+from cubicweb.pyramid.core import get_principals
+from pyramid_multiauth import MultiAuthenticationPolicy
+
+from pyramid.authentication import AuthTktAuthenticationPolicy
+
+from pyramid.interfaces import IAuthenticationPolicy
+
+log = logging.getLogger(__name__)
+
+
+@implementer(IAuthenticationPolicy)
+class UpdateLoginTimeAuthenticationPolicy(object):
+    """An authentication policy that update the user last_login_time.
+
+    The update is done in the 'remember' method, which is called by the login
+    views login,
+
+    Usually used via :func:`includeme`.
+    """
+
+    def authenticated_userid(self, request):
+        pass
+
+    def effective_principals(self, request):
+        return ()
+
+    def remember(self, request, principal, **kw):
+        try:
+            repo = request.registry['cubicweb.repository']
+            with repo.internal_cnx() as cnx:
+                cnx.execute(
+                    "SET U last_login_time %(now)s WHERE U eid %(user)s", {
+                        'now': datetime.datetime.now(),
+                        'user': principal})
+                cnx.commit()
+        except:
+            log.exception("Failed to update last_login_time")
+        return ()
+
+    def forget(self, request):
+        return ()
+
+
+class CWAuthTktAuthenticationPolicy(AuthTktAuthenticationPolicy):
+    """
+    An authentication policy that inhibate the call the 'remember' if a
+    'persistent' argument is passed to it, and is equal to the value that
+    was passed to the constructor.
+
+    This allow to combine two policies with different settings and select them
+    by just setting this argument.
+    """
+    def __init__(self, secret, persistent, defaults={}, prefix='', **settings):
+        self.persistent = persistent
+        unset = object()
+        kw = {}
+        # load string settings
+        for name in ('cookie_name', 'path', 'domain', 'hashalg'):
+            value = settings.get(prefix + name, defaults.get(name, unset))
+            if value is not unset:
+                kw[name] = value
+        # load boolean settings
+        for name in ('secure', 'include_ip', 'http_only', 'wild_domain',
+                     'parent_domain', 'debug'):
+            value = settings.get(prefix + name, defaults.get(name, unset))
+            if value is not unset:
+                kw[name] = asbool(value)
+        # load int settings
+        for name in ('timeout', 'reissue_time', 'max_age'):
+            value = settings.get(prefix + name, defaults.get(name, unset))
+            if value is not unset:
+                kw[name] = int(value)
+        super(CWAuthTktAuthenticationPolicy, self).__init__(secret, **kw)
+
+    def remember(self, request, principals, **kw):
+        if 'persistent' not in kw or kw.pop('persistent') == self.persistent:
+            return super(CWAuthTktAuthenticationPolicy, self).remember(
+                request, principals, **kw)
+        else:
+            return ()
+
+
+def includeme(config):
+    """ Activate the CubicWeb AuthTkt authentication policy.
+
+    Usually called via ``config.include('cubicweb.pyramid.auth')``.
+
+    See also :ref:`defaults_module`
+    """
+    settings = config.registry.settings
+
+    policies = []
+
+    if asbool(settings.get('cubicweb.auth.update_login_time', True)):
+        policies.append(UpdateLoginTimeAuthenticationPolicy())
+
+    if asbool(settings.get('cubicweb.auth.authtkt', True)):
+        session_prefix = 'cubicweb.auth.authtkt.session.'
+        persistent_prefix = 'cubicweb.auth.authtkt.persistent.'
+
+        try:
+            secret = config.registry['cubicweb.config']['pyramid-auth-secret']
+            warnings.warn(
+                "pyramid-auth-secret from all-in-one is now "
+                "cubicweb.auth.authtkt.[session|persistent].secret",
+                DeprecationWarning)
+        except:
+            secret = 'notsosecret'
+
+        session_secret = settings.get(
+            session_prefix + 'secret', secret)
+        persistent_secret = settings.get(
+            persistent_prefix + 'secret', secret)
+
+        if 'notsosecret' in (session_secret, persistent_secret):
+            warnings.warn('''
+
+                !! SECURITY WARNING !!
+
+                The authentication cookies are signed with a static secret key.
+
+                Configure the following options in your pyramid.ini file:
+
+                - cubicweb.auth.authtkt.session.secret
+                - cubicweb.auth.authtkt.persistent.secret
+
+                YOU SHOULD STOP THIS INSTANCE unless your really know what you
+                are doing !!
+
+            ''')
+
+        policies.append(
+            CWAuthTktAuthenticationPolicy(
+                session_secret, False,
+                defaults={
+                    'hashalg': 'sha512',
+                    'cookie_name': 'auth_tkt',
+                    'timeout': 1200,
+                    'reissue_time': 120,
+                    'http_only': True,
+                    'secure': True
+                },
+                prefix=session_prefix,
+                **settings
+            )
+        )
+
+        policies.append(
+            CWAuthTktAuthenticationPolicy(
+                persistent_secret, True,
+                defaults={
+                    'hashalg': 'sha512',
+                    'cookie_name': 'pauth_tkt',
+                    'max_age': 3600*24*30,
+                    'reissue_time': 3600*24,
+                    'http_only': True,
+                    'secure': True
+                },
+                prefix=persistent_prefix,
+                **settings
+            )
+        )
+
+    kw = {}
+    if asbool(settings.get('cubicweb.auth.groups_principals', True)):
+        kw['callback'] = get_principals
+
+    authpolicy = MultiAuthenticationPolicy(policies, **kw)
+    config.registry['cubicweb.authpolicy'] = authpolicy
+
+    config.set_authentication_policy(authpolicy)
+    config.set_authorization_policy(ACLAuthorizationPolicy())
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/pyramid/bwcompat.py	Mon Sep 26 14:52:12 2016 +0200
@@ -0,0 +1,213 @@
+import sys
+import logging
+
+from pyramid import security
+from pyramid import tweens
+from pyramid.httpexceptions import HTTPSeeOther
+from pyramid import httpexceptions
+from pyramid.settings import asbool
+
+import cubicweb
+import cubicweb.web
+
+from cubicweb.web.application import CubicWebPublisher
+
+from cubicweb.web import LogOut, PublishException
+
+from cubicweb.pyramid.core import cw_to_pyramid
+
+
+log = logging.getLogger(__name__)
+
+
+class PyramidSessionHandler(object):
+    """A CW Session handler that rely on the pyramid API to fetch the needed
+    informations.
+
+    It implements the :class:`cubicweb.web.application.CookieSessionHandler`
+    API.
+    """
+
+    def __init__(self, appli):
+        self.appli = appli
+
+    def get_session(self, req):
+        return req._request.cw_session
+
+    def logout(self, req, goto_url):
+        raise LogOut(url=goto_url)
+
+
+class CubicWebPyramidHandler(object):
+    """ A Pyramid request handler that rely on a cubicweb instance to do the
+    whole job
+
+    :param appli: A CubicWeb 'Application' object.
+    """
+    def __init__(self, appli):
+        self.appli = appli
+
+    def __call__(self, request):
+        """
+        Handler that mimics what CubicWebPublisher.main_handle_request and
+        CubicWebPublisher.core_handle do
+        """
+
+        # XXX The main handler of CW forbid anonymous https connections
+        # I guess we can drop this "feature" but in doubt I leave this comment
+        # so we don't forget about it. (cdevienne)
+
+        req = request.cw_request
+        vreg = request.registry['cubicweb.registry']
+
+        try:
+            content = None
+            try:
+                with cw_to_pyramid(request):
+                    ctrlid, rset = self.appli.url_resolver.process(req,
+                                                                   req.path)
+
+                    try:
+                        controller = vreg['controllers'].select(
+                            ctrlid, req, appli=self.appli)
+                    except cubicweb.NoSelectableObject:
+                        raise httpexceptions.HTTPUnauthorized(
+                            req._('not authorized'))
+
+                    req.update_search_state()
+                    content = controller.publish(rset=rset)
+
+                    # XXX this auto-commit should be handled by the cw_request
+                    # cleanup or the pyramid transaction manager.
+                    # It is kept here to have the ValidationError handling bw
+                    # compatible
+                    if req.cnx:
+                        txuuid = req.cnx.commit()
+                        # commited = True
+                        if txuuid is not None:
+                            req.data['last_undoable_transaction'] = txuuid
+            except cubicweb.web.ValidationError as ex:
+                # XXX The validation_error_handler implementation is light, we
+                # should redo it better in cw_to_pyramid, so it can be properly
+                # handled when raised from a cubicweb view.
+                # BUT the real handling of validation errors should be done
+                # earlier in the controllers, not here. In the end, the
+                # ValidationError should never by handled here.
+                content = self.appli.validation_error_handler(req, ex)
+            except cubicweb.web.RemoteCallFailed as ex:
+                # XXX The default pyramid error handler (or one that we provide
+                # for this exception) should be enough
+                # content = self.appli.ajax_error_handler(req, ex)
+                raise
+
+            if content is not None:
+                request.response.body = content
+
+
+        except LogOut as ex:
+            # The actual 'logging out' logic should be in separated function
+            # that is accessible by the pyramid views
+            headers = security.forget(request)
+            raise HTTPSeeOther(ex.url, headers=headers)
+        except cubicweb.AuthenticationError:
+            # Will occur upon access to req.cnx which is a
+            # cubicweb.dbapi._NeedAuthAccessMock.
+            if not content:
+                content = vreg['views'].main_template(req, 'login')
+                request.response.status_code = 403
+                request.response.body = content
+        finally:
+            # XXX CubicWebPyramidRequest.headers_out should
+            # access directly the pyramid response headers.
+            request.response.headers.clear()
+            for k, v in req.headers_out.getAllRawHeaders():
+                for item in v:
+                    request.response.headers.add(k, item)
+
+        return request.response
+
+    def error_handler(self, exc, request):
+        req = request.cw_request
+        if isinstance(exc, httpexceptions.HTTPException):
+            request.response = exc
+        elif isinstance(exc, PublishException) and exc.status is not None:
+            request.response = httpexceptions.exception_response(exc.status)
+        else:
+            request.response = httpexceptions.HTTPInternalServerError()
+        request.response.cache_control = 'no-cache'
+        vreg = request.registry['cubicweb.registry']
+        excinfo = sys.exc_info()
+        req.reset_message()
+        if req.ajax_request:
+            content = self.appli.ajax_error_handler(req, exc)
+        else:
+            try:
+                req.data['ex'] = exc
+                req.data['excinfo'] = excinfo
+                errview = vreg['views'].select('error', req)
+                template = self.appli.main_template_id(req)
+                content = vreg['views'].main_template(req, template, view=errview)
+            except Exception:
+                content = vreg['views'].main_template(req, 'error-template')
+        log.exception(exc)
+        request.response.body = content
+        return request.response
+
+
+class TweenHandler(object):
+    """ A Pyramid tween handler that submit unhandled requests to a Cubicweb
+    handler.
+
+    The CubicWeb handler to use is expected to be in the pyramid registry, at
+    key ``'cubicweb.handler'``.
+    """
+    def __init__(self, handler, registry):
+        self.handler = handler
+        self.cwhandler = registry['cubicweb.handler']
+
+    def __call__(self, request):
+        if request.path.startswith('/https/'):
+            request.environ['PATH_INFO'] = request.environ['PATH_INFO'][6:]
+            assert not request.path.startswith('/https/')
+            request.scheme = 'https'
+        try:
+            response = self.handler(request)
+        except httpexceptions.HTTPNotFound:
+            response = self.cwhandler(request)
+        return response
+
+
+def includeme(config):
+    """ Set up a tween app that will handle the request if the main application
+    raises a HTTPNotFound exception.
+
+    This is to keep legacy compatibility for cubes that makes use of the
+    cubicweb urlresolvers.
+
+    It provides, for now, support for cubicweb controllers, but this feature
+    will be reimplemented separatly in a less compatible way.
+
+    It is automatically included by the configuration system, but can be
+    disabled in the :ref:`pyramid_settings`:
+
+    .. code-block:: ini
+
+        cubicweb.bwcompat = no
+    """
+    cwconfig = config.registry['cubicweb.config']
+    repository = config.registry['cubicweb.repository']
+    cwappli = CubicWebPublisher(
+        repository, cwconfig,
+        session_handler_fact=PyramidSessionHandler)
+    cwhandler = CubicWebPyramidHandler(cwappli)
+
+    config.registry['cubicweb.appli'] = cwappli
+    config.registry['cubicweb.handler'] = cwhandler
+
+    config.add_tween(
+        'cubicweb.pyramid.bwcompat.TweenHandler', under=tweens.EXCVIEW)
+    if asbool(config.registry.settings.get(
+            'cubicweb.bwcompat.errorhandler', True)):
+        config.add_view(cwhandler.error_handler, context=Exception)
+        # XXX why do i need this?
+        config.add_view(cwhandler.error_handler, context=httpexceptions.HTTPForbidden)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/pyramid/core.py	Mon Sep 26 14:52:12 2016 +0200
@@ -0,0 +1,400 @@
+import itertools
+
+from contextlib import contextmanager
+from warnings import warn
+from cgi import FieldStorage
+
+import rql
+
+from cubicweb.web.request import CubicWebRequestBase
+from cubicweb import repoapi
+
+import cubicweb
+import cubicweb.web
+from cubicweb.server import session as cwsession
+
+from pyramid import httpexceptions
+
+from cubicweb.pyramid import tools
+
+import logging
+
+log = logging.getLogger(__name__)
+
+
+CW_321 = cubicweb.__pkginfo__.numversion >= (3, 21, 0)
+
+
+class Connection(cwsession.Connection):
+    """ A specialised Connection that access the session data through a
+    property.
+
+    This behavior makes sure the actual session data is not loaded until
+    actually accessed.
+    """
+    def __init__(self, session, *args, **kw):
+        super(Connection, self).__init__(session, *args, **kw)
+        self._session = session
+
+    def _get_session_data(self):
+        return self._session.data
+
+    def _set_session_data(self, data):
+        pass
+
+    _session_data = property(_get_session_data, _set_session_data)
+
+
+class Session(cwsession.Session):
+    """ A Session that access the session data through a property.
+
+    Along with :class:`Connection`, it avoid any load of the pyramid session
+    data until it is actually accessed.
+    """
+    def __init__(self, pyramid_request, user, repo):
+        super(Session, self).__init__(user, repo)
+        self._pyramid_request = pyramid_request
+
+    def get_data(self):
+        if not getattr(self, '_protect_data_access', False):
+            self._data_accessed = True
+            return self._pyramid_request.session
+
+    def set_data(self, data):
+        if getattr(self, '_data_accessed', False):
+            self._pyramid_request.session.clear()
+            self._pyramid_request.session.update(data)
+
+    data = property(get_data, set_data)
+
+    def new_cnx(self):
+        self._protect_data_access = True
+        try:
+            return Connection(self)
+        finally:
+            self._protect_data_access = False
+
+
+def cw_headers(request):
+    return itertools.chain(
+        *[[(k, item) for item in v]
+          for k, v in request.cw_request.headers_out.getAllRawHeaders()])
+
+
+@contextmanager
+def cw_to_pyramid(request):
+    """ Context manager to wrap a call to the cubicweb API.
+
+    All CW exceptions will be transformed into their pyramid equivalent.
+    When needed, some CW reponse bits may be converted too (mainly headers)"""
+    try:
+        yield
+    except cubicweb.web.Redirect as ex:
+        assert 300 <= ex.status < 400
+        raise httpexceptions.status_map[ex.status](
+            ex.location, headers=cw_headers(request))
+    except cubicweb.web.StatusResponse as ex:
+        warn('[3.16] StatusResponse is deprecated use req.status_out',
+             DeprecationWarning, stacklevel=2)
+        request.body = ex.content
+        request.status_int = ex.status
+    except cubicweb.web.Unauthorized as ex:
+        raise httpexceptions.HTTPForbidden(
+            request.cw_request._(
+                'You\'re not authorized to access this page. '
+                'If you think you should, please contact the site '
+                'administrator.'),
+            headers=cw_headers(request))
+    except cubicweb.web.Forbidden:
+        raise httpexceptions.HTTPForbidden(
+            request.cw_request._(
+                'This action is forbidden. '
+                'If you think it should be allowed, please contact the site '
+                'administrator.'),
+            headers=cw_headers(request))
+    except (rql.BadRQLQuery, cubicweb.web.RequestError) as ex:
+        raise
+
+
+class CubicWebPyramidRequest(CubicWebRequestBase):
+    """ A CubicWeb request that only wraps a pyramid request.
+
+    :param request: A pyramid request
+
+    """
+    def __init__(self, request):
+        self._request = request
+
+        self.path = request.upath_info
+
+        vreg = request.registry['cubicweb.registry']
+        https = request.scheme == 'https'
+
+        post = request.params.mixed()
+        headers_in = request.headers
+
+        super(CubicWebPyramidRequest, self).__init__(vreg, https, post,
+                                                     headers=headers_in)
+
+        self.content = request.body_file_seekable
+
+    def setup_params(self, params):
+        self.form = {}
+        for param, val in params.items():
+            if param in self.no_script_form_params and val:
+                val = self.no_script_form_param(param, val)
+            if isinstance(val, FieldStorage) and val.file:
+                val = (val.filename, val.file)
+            if param == '_cwmsgid':
+                self.set_message_id(val)
+            elif param == '__message':
+                warn('[3.13] __message in request parameter is deprecated '
+                     '(may only be given to .build_url). Seeing this message '
+                     'usualy means your application hold some <form> where '
+                     'you should replace use of __message hidden input by '
+                     'form.set_message, so new _cwmsgid mechanism is properly '
+                     'used',
+                     DeprecationWarning)
+                self.set_message(val)
+            else:
+                self.form[param] = val
+
+    def is_secure(self):
+        return self._request.scheme == 'https'
+
+    def relative_path(self, includeparams=True):
+        path = self._request.path[1:]
+        if includeparams and self._request.query_string:
+            return '%s?%s' % (path, self._request.query_string)
+        return path
+
+    def instance_uri(self):
+        return self._request.application_url
+
+    def get_full_path(self):
+        path = self._request.path
+        if self._request.query_string:
+            return '%s?%s' % (path, self._request.query_string)
+        return path
+
+    def http_method(self):
+        return self._request.method
+
+    def _set_status_out(self, value):
+        self._request.response.status_int = value
+
+    def _get_status_out(self):
+        return self._request.response.status_int
+
+    status_out = property(_get_status_out, _set_status_out)
+
+    @property
+    def message(self):
+        """Returns a '<br>' joined list of the cubicweb current message and the
+        default pyramid flash queue messages.
+        """
+        return u'\n<br>\n'.join(
+            self._request.session.pop_flash()
+            + self._request.session.pop_flash('cubicweb'))
+
+    def set_message(self, msg):
+        self.reset_message()
+        self._request.session.flash(msg, 'cubicweb')
+
+    def set_message_id(self, msgid):
+        self.reset_message()
+        self.set_message(
+            self._request.session.pop(msgid, u''))
+
+    def reset_message(self):
+        self._request.session.pop_flash('cubicweb')
+
+
+def render_view(request, vid, **kwargs):
+    """ Helper function to render a CubicWeb view.
+
+    :param request: A pyramid request
+    :param vid: A CubicWeb view id
+    :param **kwargs: Keyword arguments to select and instanciate the view
+    :returns: The rendered view content
+    """
+    vreg = request.registry['cubicweb.registry']
+    # XXX The select() function could, know how to handle a pyramid
+    # request, and feed it directly to the views that supports it.
+    # On the other hand, we could refine the View concept and decide it works
+    # with a cnx, and never with a WebRequest
+
+    with cw_to_pyramid(request):
+        view = vreg['views'].select(vid, request.cw_request, **kwargs)
+        view.set_stream()
+        view.render()
+        return view._stream.getvalue()
+
+
+def _cw_cnx(request):
+    """ Obtains a cw session from a pyramid request
+
+    The connection will be commited or rolled-back in a request finish
+    callback (this is temporary, we should make use of the transaction manager
+    in a later version).
+
+    Not meant for direct use, use ``request.cw_cnx`` instead.
+
+    :param request: A pyramid request
+    :returns type: :class:`cubicweb.server.session.Connection`
+    """
+    session = request.cw_session
+    if session is None:
+        return None
+
+    if CW_321:
+        cnx = session.new_cnx()
+
+        def commit_state(cnx):
+            return cnx.commit_state
+    else:
+        cnx = repoapi.ClientConnection(session)
+
+        def commit_state(cnx):
+            return cnx._cnx.commit_state
+
+    def cleanup(request):
+        try:
+            if (request.exception is not None and not isinstance(
+                request.exception, (
+                    httpexceptions.HTTPSuccessful,
+                    httpexceptions.HTTPRedirection))):
+                cnx.rollback()
+            elif commit_state(cnx) == 'uncommitable':
+                cnx.rollback()
+            else:
+                cnx.commit()
+        finally:
+            cnx.__exit__(None, None, None)
+
+    request.add_finished_callback(cleanup)
+    cnx.__enter__()
+    return cnx
+
+
+def repo_connect(request, repo, eid):
+    """A lightweight version of
+    :meth:`cubicweb.server.repository.Repository.connect` that does not keep
+    track of opened sessions, removing the need of closing them"""
+    user = tools.cached_build_user(repo, eid)
+    session = Session(request, user, repo)
+    tools.cnx_attach_entity(session, user)
+    # Calling the hooks should be done only once, disabling it completely for
+    # now
+    #with session.new_cnx() as cnx:
+        #repo.hm.call_hooks('session_open', cnx)
+        #cnx.commit()
+    # repo._sessions[session.sessionid] = session
+    return session
+
+
+def _cw_session(request):
+    """Obtains a cw session from a pyramid request
+
+    :param request: A pyramid request
+    :returns type: :class:`cubicweb.server.session.Session`
+
+    Not meant for direct use, use ``request.cw_session`` instead.
+    """
+    repo = request.registry['cubicweb.repository']
+
+    if not request.authenticated_userid:
+        eid = request.registry.get('cubicweb.anonymous_eid')
+        if eid is None:
+            return None
+        session = repo_connect(request, repo, eid=eid)
+    else:
+        session = request._cw_cached_session
+
+    return session
+
+
+def _cw_request(request):
+    """ Obtains a CubicWeb request wrapper for the pyramid request.
+
+    :param request: A pyramid request
+    :return: A CubicWeb request
+    :returns type: :class:`CubicWebPyramidRequest`
+
+    Not meant for direct use, use ``request.cw_request`` instead.
+
+    """
+    req = CubicWebPyramidRequest(request)
+    cnx = request.cw_cnx
+    if cnx is not None:
+        req.set_cnx(request.cw_cnx)
+    return req
+
+
+def get_principals(login, request):
+    """ Returns the group names of the authenticated user.
+
+    This function is meant to be used as an authentication policy callback.
+
+    It also pre-open the cubicweb session and put it in
+    request._cw_cached_session for later usage by :func:`_cw_session`.
+
+    .. note::
+
+        If the default authentication policy is not used, make sure this
+        function gets called by the active authentication policy.
+
+    :param login: A cubicweb user eid
+    :param request: A pyramid request
+    :returns: A list of group names
+    """
+    repo = request.registry['cubicweb.repository']
+
+    try:
+        session = repo_connect(request, repo, eid=login)
+        request._cw_cached_session = session
+    except:
+        log.exception("Failed")
+        raise
+
+    return session.user.groups
+
+
+def includeme(config):
+    """ Enables the core features of Pyramid CubicWeb.
+
+    Automatically called by the 'pyramid' command, or via
+    ``config.include('cubicweb.pyramid.code')``. In the later case,
+    the following registry entries must be defined first:
+
+    'cubicweb.config'
+        A cubicweb 'config' instance.
+
+    'cubicweb.repository'
+        The correponding cubicweb repository.
+
+    'cubicweb.registry'
+        The vreg.
+    """
+    repo = config.registry['cubicweb.repository']
+
+    with repo.internal_cnx() as cnx:
+        login = config.registry['cubicweb.config'].anonymous_user()[0]
+        if login is not None:
+            config.registry['cubicweb.anonymous_eid'] = cnx.find(
+                'CWUser', login=login).one().eid
+
+    config.add_request_method(
+        _cw_session, name='cw_session', property=True, reify=True)
+    config.add_request_method(
+        _cw_cnx, name='cw_cnx', property=True, reify=True)
+    config.add_request_method(
+        _cw_request, name='cw_request', property=True, reify=True)
+
+    cwcfg = config.registry['cubicweb.config']
+    for cube in cwcfg.cubes():
+        pkgname = 'cubes.' + cube
+        mod = __import__(pkgname)
+        mod = getattr(mod, cube)
+        if hasattr(mod, 'includeme'):
+            config.include('cubes.' + cube)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/pyramid/defaults.py	Mon Sep 26 14:52:12 2016 +0200
@@ -0,0 +1,24 @@
+""" Defaults for a classical CubicWeb instance. """
+
+
+def includeme(config):
+    """ Enable the defaults that make the application behave like a classical
+    CubicWeb instance.
+
+    The following modules get included:
+
+    -   :func:`cubicweb.pyramid.session <cubicweb.pyramid.session.includeme>`
+    -   :func:`cubicweb.pyramid.auth <cubicweb.pyramid.auth.includeme>`
+    -   :func:`cubicweb.pyramid.login <cubicweb.pyramid.login.includeme>`
+
+    It is automatically included by the configuration system, unless the
+    following entry is added to the :ref:`pyramid_settings`:
+
+    .. code-block:: ini
+
+        cubicweb.defaults = no
+
+    """
+    config.include('cubicweb.pyramid.session')
+    config.include('cubicweb.pyramid.auth')
+    config.include('cubicweb.pyramid.login')
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/pyramid/init_instance.py	Mon Sep 26 14:52:12 2016 +0200
@@ -0,0 +1,10 @@
+from cubicweb.cwconfig import CubicWebConfiguration
+
+
+def includeme(config):
+    appid = config.registry.settings['cubicweb.instance']
+    cwconfig = CubicWebConfiguration.config_for(appid)
+
+    config.registry['cubicweb.config'] = cwconfig
+    config.registry['cubicweb.repository'] = repo = cwconfig.repository()
+    config.registry['cubicweb.registry'] = repo.vreg
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/pyramid/login.py	Mon Sep 26 14:52:12 2016 +0200
@@ -0,0 +1,81 @@
+""" Provide login views that reproduce a classical CubicWeb behavior"""
+from pyramid import security
+from pyramid.httpexceptions import HTTPSeeOther
+from pyramid.view import view_config
+from pyramid.settings import asbool
+
+import cubicweb
+
+from cubicweb.pyramid.core import render_view
+
+
+@view_config(route_name='login')
+def login_form(request):
+    """ Default view for the 'login' route.
+
+    Display the 'login' CubicWeb view, which is should be a login form"""
+    request.response.text = render_view(request, 'login')
+    return request.response
+
+
+@view_config(route_name='login', request_param=('__login', '__password'))
+def login_password_login(request):
+    """ Handle GET/POST of __login/__password on the 'login' route.
+
+    The authentication itself is delegated to the CubicWeb repository.
+
+    Request parameters:
+
+    :param __login: The user login (or email if :confval:`allow-email-login` is
+                    on.
+    :param __password: The user password
+    :param __setauthcookie: (optional) If defined and equal to '1', set the
+                            authentication cookie maxage to 1 week.
+
+                            If not, the authentication cookie is a session
+                            cookie.
+    """
+    repo = request.registry['cubicweb.repository']
+
+    user_eid = None
+
+    login = request.params['__login']
+    password = request.params['__password']
+
+    try:
+        with repo.internal_cnx() as cnx:
+            user = repo.authenticate_user(cnx, login, password=password)
+            user_eid = user.eid
+    except cubicweb.AuthenticationError:
+        request.cw_request.set_message(request.cw_request._(
+            "Authentication failed. Please check your credentials."))
+        request.cw_request.post = dict(request.params)
+        del request.cw_request.post['__password']
+        request.response.status_code = 403
+        return login_form(request)
+
+    headers = security.remember(
+        request, user_eid,
+        persistent=asbool(request.params.get('__setauthcookie', False)))
+
+    new_path = request.params.get('postlogin_path', '')
+
+    if new_path == 'login':
+        new_path = ''
+
+    url = request.cw_request.build_url(new_path)
+    raise HTTPSeeOther(url, headers=headers)
+
+
+@view_config(route_name='login', effective_principals=security.Authenticated)
+def login_already_loggedin(request):
+    """ 'login' route view for Authenticated users.
+
+    Simply redirect the user to '/'."""
+    raise HTTPSeeOther('/')
+
+
+def includeme(config):
+    """ Create the 'login' route ('/login') and load this module views"""
+    config.add_route('login', '/login')
+    config.scan('cubicweb.pyramid.login')
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/pyramid/predicates.py	Mon Sep 26 14:52:12 2016 +0200
@@ -0,0 +1,22 @@
+"""Contains predicates used in Pyramid views.
+"""
+
+
+class MatchIsETypePredicate(object):
+    """A predicate that match if a given etype exist in schema.
+    """
+    def __init__(self, matchname, config):
+        self.matchname = matchname
+
+    def text(self):
+        return 'match_is_etype = %s' % self.matchname
+
+    phash = text
+
+    def __call__(self, info, request):
+        return info['match'][self.matchname].lower() in \
+            request.registry['cubicweb.registry'].case_insensitive_etypes
+
+
+def includeme(config):
+    config.add_route_predicate('match_is_etype', MatchIsETypePredicate)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/pyramid/profile.py	Mon Sep 26 14:52:12 2016 +0200
@@ -0,0 +1,63 @@
+""" Tools for profiling.
+
+See :ref:`profiling`."""
+from __future__ import print_function
+
+import cProfile
+import itertools
+
+from pyramid.view import view_config
+
+
+@view_config(route_name='profile_ping')
+def ping(request):
+    """ View that handle '/_profile/ping'
+
+    It simply reply 'ping', without requiring connection to the repository.
+    It is a useful as a comparison point to evaluate the actual overhead of
+    more costly views.
+    """
+    request.response.text = u'pong'
+    return request.response
+
+
+@view_config(route_name='profile_cnx')
+def cnx(request):
+    """ View that handle '/_profile/cnx'
+
+    Same as :func:`ping`, but it first ask for a connection to the repository.
+    Useful to evaluate the overhead of opening a connection.
+    """
+    request.cw_cnx
+    request.response.text = u'pong'
+    return request.response
+
+
+def wsgi_profile(app, filename='program.prof', dump_every=50):
+    """ A WSGI middleware for profiling
+
+    It enable the profiler before passing the request to the underlying
+    application, and disable it just after.
+
+    The stats will be dumped after ``dump_every`` requests
+
+    :param filename: The filename to dump the stats to.
+    :param dump_every: Number of requests after which to dump the stats.
+    """
+
+    profile = cProfile.Profile()
+
+    counter = itertools.count(1)
+
+    def application(environ, start_response):
+        profile.enable()
+        try:
+            return app(environ, start_response)
+        finally:
+            profile.disable()
+            if not counter.next() % dump_every:
+                print("Dump profile stats to %s" % filename)
+                profile.create_stats()
+                profile.dump_stats(filename)
+
+    return application
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/pyramid/resources.py	Mon Sep 26 14:52:12 2016 +0200
@@ -0,0 +1,74 @@
+"""Contains resources classes.
+"""
+from six import text_type
+
+from rql import TypeResolverException
+
+from pyramid.decorator import reify
+from pyramid.httpexceptions import HTTPNotFound
+
+
+class EntityResource(object):
+
+    """A resource class for an entity. It provide method to retrieve an entity
+    by eid.
+    """
+
+    @classmethod
+    def from_eid(cls):
+        def factory(request):
+            return cls(request, None, None, request.matchdict['eid'])
+        return factory
+
+    def __init__(self, request, cls, attrname, value):
+        self.request = request
+        self.cls = cls
+        self.attrname = attrname
+        self.value = value
+
+    @reify
+    def rset(self):
+        req = self.request.cw_request
+        if self.cls is None:
+            return req.execute('Any X WHERE X eid %(x)s',
+                               {'x': int(self.value)})
+        st = self.cls.fetch_rqlst(self.request.cw_cnx.user, ordermethod=None)
+        st.add_constant_restriction(st.get_variable('X'), self.attrname,
+                                    'x', 'Substitute')
+        if self.attrname == 'eid':
+            try:
+                rset = req.execute(st.as_string(), {'x': int(self.value)})
+            except (ValueError, TypeResolverException):
+                # conflicting eid/type
+                raise HTTPNotFound()
+        else:
+            rset = req.execute(st.as_string(), {'x': text_type(self.value)})
+        return rset
+
+
+class ETypeResource(object):
+
+    """A resource for etype.
+    """
+    @classmethod
+    def from_match(cls, matchname):
+        def factory(request):
+            return cls(request, request.matchdict[matchname])
+        return factory
+
+    def __init__(self, request, etype):
+        vreg = request.registry['cubicweb.registry']
+
+        self.request = request
+        self.etype = vreg.case_insensitive_etypes[etype.lower()]
+        self.cls = vreg['etypes'].etype_class(self.etype)
+
+    def __getitem__(self, value):
+        attrname = self.cls.cw_rest_attr_info()[0]
+        return EntityResource(self.request, self.cls, attrname, value)
+
+    @reify
+    def rset(self):
+        rql = self.cls.fetch_rql(self.request.cw_cnx.user)
+        rset = self.request.cw_request.execute(rql)
+        return rset
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/pyramid/rest_api.py	Mon Sep 26 14:52:12 2016 +0200
@@ -0,0 +1,24 @@
+from __future__ import absolute_import
+
+
+from pyramid.httpexceptions import HTTPNotFound
+from pyramid.view import view_config
+from cubicweb.pyramid.resources import EntityResource, ETypeResource
+from cubicweb.pyramid.predicates import MatchIsETypePredicate
+
+
+@view_config(
+    route_name='cwentities',
+    context=EntityResource,
+    request_method='DELETE')
+def delete_entity(context, request):
+    context.rset.one().cw_delete()
+    request.response.status_int = 204
+    return request.response
+
+
+def includeme(config):
+    config.add_route(
+        'cwentities', '/{etype}/*traverse',
+        factory=ETypeResource.from_match('etype'), match_is_etype='etype')
+    config.scan(__name__)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/pyramid/session.py	Mon Sep 26 14:52:12 2016 +0200
@@ -0,0 +1,189 @@
+import warnings
+import logging
+from contextlib import contextmanager
+
+from pyramid.compat import pickle
+from pyramid.session import SignedCookieSessionFactory
+
+from cubicweb import Binary
+
+
+log = logging.getLogger(__name__)
+
+
+def logerrors(logger):
+    def wrap(fn):
+        def newfn(*args, **kw):
+            try:
+                return fn(*args, **kw)
+            except:
+                logger.exception("Error in %s" % fn.__name__)
+        return newfn
+    return wrap
+
+
+@contextmanager
+def unsafe_cnx_context_manager(request):
+    """Return a connection for use as a context manager, with security disabled
+
+    If request has an attached connection, its security will be deactived in the context manager's
+    scope, else a new internal connection is returned.
+    """
+    cnx = request.cw_cnx
+    if cnx is None:
+        with request.registry['cubicweb.repository'].internal_cnx() as cnx:
+            yield cnx
+    else:
+        with cnx.security_enabled(read=False, write=False):
+            yield cnx
+
+
+def CWSessionFactory(
+        secret,
+        cookie_name='session',
+        max_age=None,
+        path='/',
+        domain=None,
+        secure=False,
+        httponly=True,
+        set_on_exception=True,
+        timeout=1200,
+        reissue_time=120,
+        hashalg='sha512',
+        salt='pyramid.session.',
+        serializer=None):
+    """ A pyramid session factory that store session data in the CubicWeb
+    database.
+
+    Storage is done with the 'CWSession' entity, which is provided by the
+    'pyramid' cube.
+
+    .. warning::
+
+        Although it provides a sane default behavior, this session storage has
+        a serious overhead because it uses RQL to access the database.
+
+        Using pure SQL would improve a bit (it is roughly twice faster), but it
+        is still pretty slow and thus not an immediate priority.
+
+        It is recommended to use faster session factory
+        (pyramid_redis_sessions_ for example) if you need speed.
+
+    .. _pyramid_redis_sessions: http://pyramid-redis-sessions.readthedocs.org/
+                                en/latest/index.html
+    """
+
+    SignedCookieSession = SignedCookieSessionFactory(
+        secret,
+        cookie_name=cookie_name,
+        max_age=max_age,
+        path=path,
+        domain=domain,
+        secure=secure,
+        httponly=httponly,
+        set_on_exception=set_on_exception,
+        timeout=timeout,
+        reissue_time=reissue_time,
+        hashalg=hashalg,
+        salt=salt,
+        serializer=serializer)
+
+    class CWSession(SignedCookieSession):
+        def __init__(self, request):
+            # _set_accessed will be called by the super __init__.
+            # Setting _loaded to True inhibates it.
+            self._loaded = True
+
+            # the super __init__ will load a single value in the dictionnary,
+            # the session id.
+            super(CWSession, self).__init__(request)
+
+            # Remove the session id from the dict
+            self.sessioneid = self.pop('sessioneid', None)
+            self.repo = request.registry['cubicweb.repository']
+
+            # We need to lazy-load only for existing sessions
+            self._loaded = self.sessioneid is None
+
+        @logerrors(log)
+        def _set_accessed(self, value):
+            self._accessed = value
+
+            if self._loaded:
+                return
+
+            with unsafe_cnx_context_manager(self.request) as cnx:
+                value_rset = cnx.execute('Any D WHERE X eid %(x)s, X cwsessiondata D',
+                                         {'x': self.sessioneid})
+                value = value_rset[0][0]
+                if value:
+                    # Use directly dict.update to avoir _set_accessed to be
+                    # recursively called
+                    dict.update(self, pickle.load(value))
+
+            self._loaded = True
+
+        def _get_accessed(self):
+            return self._accessed
+
+        accessed = property(_get_accessed, _set_accessed)
+
+        @logerrors(log)
+        def _set_cookie(self, response):
+            # Save the value in the database
+            data = Binary(pickle.dumps(dict(self)))
+            sessioneid = self.sessioneid
+
+            with unsafe_cnx_context_manager(self.request) as cnx:
+                if not sessioneid:
+                    session = cnx.create_entity(
+                        'CWSession', cwsessiondata=data)
+                    sessioneid = session.eid
+                else:
+                    session = cnx.entity_from_eid(sessioneid)
+                    session.cw_set(cwsessiondata=data)
+                cnx.commit()
+
+            # Only if needed actually set the cookie
+            if self.new or self.accessed - self.renewed > self._reissue_time:
+                dict.clear(self)
+                dict.__setitem__(self, 'sessioneid', sessioneid)
+                return super(CWSession, self)._set_cookie(response)
+
+            return True
+
+    return CWSession
+
+
+def includeme(config):
+    """ Activate the CubicWeb session factory.
+
+    Usually called via ``config.include('cubicweb.pyramid.auth')``.
+
+    See also :ref:`defaults_module`
+    """
+    settings = config.registry.settings
+    secret = settings.get('cubicweb.session.secret', '')
+    if not secret:
+        secret = config.registry['cubicweb.config'].get('pyramid-session-secret')
+        warnings.warn('''
+        Please migrate pyramid-session-secret from
+        all-in-one.conf to cubicweb.session.secret config entry in
+        your pyramid.ini file.
+        ''')
+    if not secret:
+        secret = 'notsosecret'
+        warnings.warn('''
+
+            !! WARNING !! !! WARNING !!
+
+            The session cookies are signed with a static secret key.
+            To put your own secret key, edit your pyramid.ini file
+            and set the 'cubicweb.session.secret' key.
+
+            YOU SHOULD STOP THIS INSTANCE unless your really know what you
+            are doing !!
+
+        ''')
+    session_factory = CWSessionFactory(secret)
+    config.set_session_factory(session_factory)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/pyramid/test/__init__.py	Mon Sep 26 14:52:12 2016 +0200
@@ -0,0 +1,30 @@
+import webtest
+
+from cubicweb.devtools.webtest import CubicWebTestTC
+
+from cubicweb.pyramid import make_cubicweb_application
+
+
+class PyramidCWTest(CubicWebTestTC):
+    settings = {}
+
+    @classmethod
+    def init_config(cls, config):
+        super(PyramidCWTest, cls).init_config(config)
+        config.global_set_option('https-url', 'https://localhost.local/')
+        config.global_set_option('anonymous-user', 'anon')
+        config.https_uiprops = None
+        config.https_datadir_url = None
+
+    def setUp(self):
+        # Skip CubicWebTestTC setUp
+        super(CubicWebTestTC, self).setUp()
+        config = make_cubicweb_application(self.config, self.settings)
+        self.includeme(config)
+        self.pyr_registry = config.registry
+        self.webapp = webtest.TestApp(
+            config.make_wsgi_app(),
+            extra_environ={'wsgi.url_scheme': 'https'})
+
+    def includeme(self, config):
+        pass
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/pyramid/test/data/bootstrap_cubes	Mon Sep 26 14:52:12 2016 +0200
@@ -0,0 +1,1 @@
+pyramid
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/pyramid/test/test_bw_request.py	Mon Sep 26 14:52:12 2016 +0200
@@ -0,0 +1,103 @@
+# -*- coding: utf-8 -*-
+from io import BytesIO
+
+import webtest
+
+import pyramid.request
+
+from cubicweb.pyramid.core import CubicWebPyramidRequest
+from cubicweb.pyramid.test import PyramidCWTest
+
+
+class WSGIAppTest(PyramidCWTest):
+    def make_request(self, path, environ=None, **kw):
+        r = webtest.app.TestRequest.blank(path, environ, **kw)
+
+        request = pyramid.request.Request(r.environ)
+        request.registry = self.pyr_registry
+
+        return request
+
+    def test_content_type(self):
+        req = CubicWebPyramidRequest(
+            self.make_request('/', {'CONTENT_TYPE': 'text/plain'}))
+
+        self.assertEqual('text/plain', req.get_header('Content-Type'))
+
+    def test_content_body(self):
+        req = CubicWebPyramidRequest(
+            self.make_request('/', {
+                'CONTENT_LENGTH': 12,
+                'CONTENT_TYPE': 'text/plain',
+                'wsgi.input': BytesIO(b'some content')}))
+
+        self.assertEqual(b'some content', req.content.read())
+
+    def test_http_scheme(self):
+        req = CubicWebPyramidRequest(
+            self.make_request('/', {
+                'wsgi.url_scheme': 'http'}))
+
+        self.assertFalse(req.https)
+
+    def test_https_scheme(self):
+        req = CubicWebPyramidRequest(
+            self.make_request('/', {
+                'wsgi.url_scheme': 'https'}))
+
+        self.assertTrue(req.https)
+
+    def test_https_prefix(self):
+        r = self.webapp.get('/https/')
+        self.assertIn('https://', r.text)
+
+    def test_big_content(self):
+        content = b'x'*100001
+
+        req = CubicWebPyramidRequest(
+            self.make_request('/', {
+                'CONTENT_LENGTH': len(content),
+                'CONTENT_TYPE': 'text/plain',
+                'wsgi.input': BytesIO(content)}))
+
+        self.assertEqual(content, req.content.read())
+
+    def test_post(self):
+        self.webapp.post(
+            '/',
+            params={'__login': self.admlogin, '__password': self.admpassword})
+
+    def test_get_multiple_variables(self):
+        req = CubicWebPyramidRequest(
+            self.make_request('/?arg=1&arg=2'))
+
+        self.assertEqual([u'1', u'2'], req.form['arg'])
+
+    def test_post_multiple_variables(self):
+        req = CubicWebPyramidRequest(
+            self.make_request('/', POST='arg=1&arg=2'))
+
+        self.assertEqual([u'1', u'2'], req.form['arg'])
+
+    def test_post_files(self):
+        content_type, params = self.webapp.encode_multipart(
+            (), (('filefield', 'aname', b'acontent'),))
+        req = CubicWebPyramidRequest(
+            self.make_request('/', POST=params, content_type=content_type))
+        self.assertIn('filefield', req.form)
+        fieldvalue = req.form['filefield']
+        self.assertEqual(u'aname', fieldvalue[0])
+        self.assertEqual(b'acontent', fieldvalue[1].read())
+
+    def test_post_unicode_urlencoded(self):
+        params = 'arg=%C3%A9'
+        req = CubicWebPyramidRequest(
+            self.make_request(
+                '/', POST=params,
+                content_type='application/x-www-form-urlencoded'))
+        self.assertEqual(u"é", req.form['arg'])
+
+
+if __name__ == '__main__':
+    from unittest import main
+    main()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/pyramid/test/test_core.py	Mon Sep 26 14:52:12 2016 +0200
@@ -0,0 +1,49 @@
+from cubicweb.pyramid.test import PyramidCWTest
+
+from cubicweb.view import View
+from cubicweb.web import Redirect
+from cubicweb import ValidationError
+
+
+class Redirector(View):
+    __regid__ = 'redirector'
+
+    def call(self, rset=None):
+        self._cw.set_header('Cache-Control', 'no-cache')
+        raise Redirect('http://example.org')
+
+
+def put_in_uncommitable_state(request):
+    try:
+        request.cw_cnx.execute('SET U login NULL WHERE U login "anon"')
+    except ValidationError:
+        pass
+    request.response.body = b'OK'
+    return request.response
+
+
+class CoreTest(PyramidCWTest):
+    anonymous_allowed = True
+
+    def includeme(self, config):
+        config.add_route('uncommitable', '/uncommitable')
+        config.add_view(put_in_uncommitable_state, route_name='uncommitable')
+
+    def test_cw_to_pyramid_copy_headers_on_redirect(self):
+        self.vreg.register(Redirector)
+        try:
+            res = self.webapp.get('/?vid=redirector', expect_errors=True)
+            self.assertEqual(res.status_int, 303)
+            self.assertEqual(res.headers['Cache-Control'], 'no-cache')
+        finally:
+            self.vreg.unregister(Redirector)
+
+    def test_uncommitable_cnx(self):
+        res = self.webapp.get('/uncommitable')
+        self.assertEqual(res.text, 'OK')
+        self.assertEqual(res.status_int, 200)
+
+
+if __name__ == '__main__':
+    from unittest import main
+    main()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/pyramid/test/test_login.py	Mon Sep 26 14:52:12 2016 +0200
@@ -0,0 +1,51 @@
+from cubicweb.pyramid.test import PyramidCWTest
+
+
+class LoginTest(PyramidCWTest):
+    def test_login_form(self):
+        res = self.webapp.get('/login')
+        self.assertIn('__login', res.text)
+
+    def test_login_password_login(self):
+        res = self.webapp.post('/login', {
+            '__login': self.admlogin, '__password': self.admpassword})
+        self.assertEqual(res.status_int, 303)
+
+        res = self.webapp.get('/login')
+        self.assertEqual(res.status_int, 303)
+
+    def test_login_password_login_cookie_expires(self):
+        res = self.webapp.post('/login', {
+            '__login': self.admlogin, '__password': self.admpassword})
+        self.assertEqual(res.status_int, 303)
+
+        cookies = self.webapp.cookiejar._cookies['localhost.local']['/']
+        self.assertNotIn('pauth_tkt', cookies)
+        self.assertIn('auth_tkt', cookies)
+        self.assertIsNone(cookies['auth_tkt'].expires)
+
+        res = self.webapp.get('/logout')
+        self.assertEqual(res.status_int, 303)
+
+        self.assertNotIn('auth_tkt', cookies)
+        self.assertNotIn('pauth_tkt', cookies)
+
+        res = self.webapp.post('/login', {
+            '__login': self.admlogin, '__password': self.admpassword,
+            '__setauthcookie': 1})
+        self.assertEqual(res.status_int, 303)
+
+        cookies = self.webapp.cookiejar._cookies['localhost.local']['/']
+        self.assertNotIn('auth_tkt', cookies)
+        self.assertIn('pauth_tkt', cookies)
+        self.assertIsNotNone(cookies['pauth_tkt'].expires)
+
+    def test_login_bad_password(self):
+        res = self.webapp.post('/login', {
+            '__login': self.admlogin, '__password': 'empty'}, status=403)
+        self.assertIn('Authentication failed', res.text)
+
+
+if __name__ == '__main__':
+    from unittest import main
+    main()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/pyramid/test/test_rest_api.py	Mon Sep 26 14:52:12 2016 +0200
@@ -0,0 +1,59 @@
+from __future__ import absolute_import
+
+from cubicweb.pyramid.rest_api import EntityResource
+from cubicweb.pyramid.core import CubicWebPyramidRequest
+from pyramid.view import view_config
+
+from cubicweb.pyramid.test import PyramidCWTest
+
+
+class RestApiTest(PyramidCWTest):
+    def includeme(self, config):
+        config.include('cubicweb.pyramid.rest_api')
+        config.include('cubicweb.pyramid.test.test_rest_api')
+
+    def test_delete(self):
+        with self.admin_access.repo_cnx() as cnx:
+            cnx.create_entity('CWGroup', name=u'tmp')
+            cnx.commit()
+
+        self.login()
+        res = self.webapp.delete('/cwgroup/tmp')
+        self.assertEqual(res.status_int, 204)
+
+        with self.admin_access.repo_cnx() as cnx:
+            self.assertEqual(cnx.find('CWGroup', name=u'tmp').rowcount, 0)
+
+    def test_rql_execute(self):
+        with self.admin_access.repo_cnx() as cnx:
+            cnx.create_entity('CWGroup', name=u'tmp')
+            cnx.commit()
+        self.login()
+        params = {'test_rql_execute': 'test'}
+        self.webapp.get('/cwgroup/tmp', params=params)
+
+
+@view_config(
+    route_name='cwentities',
+    context=EntityResource,
+    request_method='GET',
+    request_param=('test_rql_execute',)
+)
+def rql_execute_view(context, request):
+    """Return 500 response if rset.req is not a CubicWeb request.
+    """
+    if isinstance(context.rset.req, CubicWebPyramidRequest):
+        request.response.status_int = 204
+    else:
+        request.response.status_int = 500
+        request.response.text = 'rset.req is not a CubicWeb request'
+    return request.response
+
+
+def includeme(config):
+    config.scan(__name__)
+
+
+if __name__ == '__main__':
+    from unittest import main
+    main()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/pyramid/test/test_tools.py	Mon Sep 26 14:52:12 2016 +0200
@@ -0,0 +1,31 @@
+from cubicweb.pyramid.test import PyramidCWTest
+from cubicweb.pyramid import tools
+
+
+class ToolsTest(PyramidCWTest):
+    anonymous_allowed = True
+
+    def test_clone_user(self):
+        with self.admin_access.repo_cnx() as cnx:
+            user = cnx.find('CWUser', login='anon').one()
+            user.login  # fill the cache
+            clone = tools.clone_user(self.repo, user)
+
+            self.assertEqual(clone.eid, user.eid)
+            self.assertEqual(clone.login, user.login)
+
+            self.assertEqual(clone.cw_rset.rows, user.cw_rset.rows)
+            self.assertEqual(clone.cw_rset.rql, user.cw_rset.rql)
+
+    def test_cnx_attach_entity(self):
+        with self.admin_access.repo_cnx() as cnx:
+            user = cnx.find('CWUser', login='anon').one()
+
+        with self.admin_access.repo_cnx() as cnx:
+            tools.cnx_attach_entity(cnx, user)
+            self.assertEqual(user.login, 'anon')
+
+
+if __name__ == '__main__':
+    from unittest import main
+    main()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/pyramid/tools.py	Mon Sep 26 14:52:12 2016 +0200
@@ -0,0 +1,79 @@
+"""Various tools.
+
+.. warning::
+
+    This module should be considered as internal implementation details. Use
+    with caution, as the API may change without notice.
+"""
+
+#: A short-term cache for user clones.
+#: used by cached_build_user to speed-up repetitive calls to build_user
+#: The expiration is handled in a dumb and brutal way: the whole cache is
+#: cleared every 5 minutes.
+_user_cache = {}
+
+
+def clone_user(repo, user):
+    """Clone a CWUser instance.
+
+    .. warning::
+
+        The returned clone is detached from any cnx.
+        Before using it in any way, it should be attached to a cnx that has not
+        this user already loaded.
+    """
+    CWUser = repo.vreg['etypes'].etype_class('CWUser')
+    clone = CWUser(
+        None,
+        rset=user.cw_rset.copy(),
+        row=user.cw_row,
+        col=user.cw_col,
+        groups=set(user._groups) if hasattr(user, '_groups') else None,
+        properties=dict(user._properties)
+        if hasattr(user, '_properties') else None)
+    clone.cw_attr_cache = dict(user.cw_attr_cache)
+    return clone
+
+
+def cnx_attach_entity(cnx, entity):
+    """Attach an entity to a cnx."""
+    entity._cw = cnx
+    if entity.cw_rset:
+        entity.cw_rset.req = cnx
+
+
+def cached_build_user(repo, eid):
+    """Cached version of
+    :meth:`cubicweb.server.repository.Repository._build_user`
+    """
+    with repo.internal_cnx() as cnx:
+        if eid in _user_cache:
+            entity = clone_user(repo, _user_cache[eid])
+            # XXX the cnx is needed here so that the CWUser instance has an
+            # access to the vreg, which it needs when its 'prefered_language'
+            # property is accessed.
+            # If this property did not need a cnx to access a vreg, we could
+            # avoid the internal_cnx() and save more time.
+            cnx_attach_entity(cnx, entity)
+            return entity
+
+        user = repo._build_user(cnx, eid)
+        user.cw_clear_relation_cache()
+        _user_cache[eid] = clone_user(repo, user)
+        return user
+
+
+def clear_cache():
+    """Clear the user cache"""
+    _user_cache.clear()
+
+
+def includeme(config):
+    """Start the cache maintenance loop task.
+
+    Automatically included by :func:`cubicweb.pyramid.make_cubicweb_application`.
+    """
+    repo = config.registry['cubicweb.repository']
+    interval = int(config.registry.settings.get(
+        'cubicweb.usercache.expiration_time', 60*5))
+    repo.looping_task(interval, clear_cache)
--- a/cubicweb/test/unittest_cwconfig.py	Fri Sep 23 16:04:32 2016 +0200
+++ b/cubicweb/test/unittest_cwconfig.py	Mon Sep 26 14:52:12 2016 +0200
@@ -81,7 +81,7 @@
         expected_cubes = [
             'card', 'comment', 'cubicweb_comment', 'cubicweb_email', 'file',
             'cubicweb_file', 'cubicweb_forge', 'localperms',
-            'cubicweb_mycube', 'tag',
+            'cubicweb_mycube', 'pyramid', 'tag',
         ]
         self._test_available_cubes(expected_cubes)
         mock_iter_entry_points.assert_called_once_with(
@@ -168,7 +168,7 @@
             # local cubes
             'comment', 'email', 'file', 'forge', 'mycube',
             # test dependencies
-            'card', 'file', 'localperms', 'tag',
+            'card', 'file', 'localperms', 'pyramid', 'tag',
         ]))
         self._test_available_cubes(expected_cubes)
 
--- a/debian/control	Fri Sep 23 16:04:32 2016 +0200
+++ b/debian/control	Mon Sep 26 14:52:12 2016 +0200
@@ -18,6 +18,9 @@
  python-rql (>= 0.34.0),
  python-yams (>= 0.44.0),
  python-lxml,
+ python-setuptools,
+ python-pyramid,
+ python-waitress,
 Standards-Version: 3.9.1
 Homepage: https://www.cubicweb.org
 X-Python-Version: >= 2.6
@@ -120,6 +123,26 @@
  This package provides only the twisted server part of the library.
 
 
+Package: cubicweb-pyramid
+Architecture: all
+Depends:
+ ${misc:Depends},
+ ${python:Depends},
+ cubicweb-web (= ${source:Version}),
+ cubicweb-ctl (= ${source:Version}),
+ python-pyramid (>= 1.5.0),
+ python-pyramid-multiauth,
+ python-waitress (>= 0.8.9),
+ python-wsgicors,
+Recommends:
+ python-pyramid-debugtoolbar
+Description: Integrate CubicWeb with a Pyramid application
+ Provides pyramid extensions to load a CubicWeb instance and serve it through
+ the pyramid stack.
+ .
+ It prefigures what CubicWeb 4.0 will be.
+
+
 Package: cubicweb-web
 Architecture: all
 Depends:
--- a/debian/copyright	Fri Sep 23 16:04:32 2016 +0200
+++ b/debian/copyright	Mon Sep 26 14:52:12 2016 +0200
@@ -5,11 +5,13 @@
 Upstream Author:
 
     Logilab <contact@logilab.fr>
+    Christophe de Vienne
 
 Copyright:
 
     Copyright (c) 2003-2014 LOGILAB S.A. (Paris, FRANCE).
     http://www.logilab.fr/ -- mailto:contact@logilab.fr
+    Copyright (c) 2014 Unlish
 
 License:
 
@@ -43,4 +45,3 @@
 
     The rights to each pictogram in the social extension are either
     trademarked or copyrighted by the respective company.
-
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/debian/pydist-overrides	Mon Sep 26 14:52:12 2016 +0200
@@ -0,0 +1,1 @@
+cubicweb cubicweb-common
--- a/debian/rules	Fri Sep 23 16:04:32 2016 +0200
+++ b/debian/rules	Mon Sep 26 14:52:12 2016 +0200
@@ -55,6 +55,7 @@
 	rm -rf debian/cubicweb-twisted/usr/lib/python2*/*-packages/cubicweb/etwist/test
 	rm -rf debian/cubicweb-common/usr/lib/python2*/*-packages/cubicweb/ext/test
 	rm -rf debian/cubicweb-common/usr/lib/python2*/*-packages/cubicweb/entities/test
+	rm -rf debian/cubicweb-pyramid/usr/lib/python2*/*-packages/cubicweb/pyramid/tests
 
 
 # Build architecture-independent files here.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/api/pyramid.rst	Mon Sep 26 14:52:12 2016 +0200
@@ -0,0 +1,17 @@
+:mod:`cubicweb.pyramid`
+=======================
+
+.. automodule:: cubicweb.pyramid
+
+    .. autofunction:: make_cubicweb_application
+
+    .. autofunction:: wsgi_application_from_cwconfig
+
+    .. autofunction:: wsgi_application
+
+.. toctree::
+    :maxdepth: 1
+    :glob:
+
+    pyramid/*
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/api/pyramid/auth.rst	Mon Sep 26 14:52:12 2016 +0200
@@ -0,0 +1,12 @@
+.. _auth_module:
+
+:mod:`cubicweb.pyramid.auth`
+----------------------------
+
+.. automodule:: cubicweb.pyramid.auth
+
+    .. autofunction:: includeme
+
+    .. autoclass:: UpdateLoginTimeAuthenticationPolicy
+        :show-inheritance:
+        :members:
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/api/pyramid/authplugin.rst	Mon Sep 26 14:52:12 2016 +0200
@@ -0,0 +1,10 @@
+.. _authplugin_module:
+
+:mod:`cubicweb.pyramid.authplugin`
+----------------------------------
+
+.. automodule:: cubicweb.pyramid.authplugin
+
+    .. autoclass:: DirectAuthentifier
+        :show-inheritance:
+        :members:
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/api/pyramid/bwcompat.rst	Mon Sep 26 14:52:12 2016 +0200
@@ -0,0 +1,19 @@
+.. _bwcompat_module:
+
+:mod:`cubicweb.pyramid.bwcompat`
+--------------------------------
+
+.. automodule:: cubicweb.pyramid.bwcompat
+
+    .. autofunction:: includeme
+
+    .. autoclass:: PyramidSessionHandler
+        :members:
+
+    .. autoclass:: CubicWebPyramidHandler
+        :members:
+
+        .. automethod:: __call__
+
+    .. autoclass:: TweenHandler
+        :members:
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/api/pyramid/core.rst	Mon Sep 26 14:52:12 2016 +0200
@@ -0,0 +1,23 @@
+.. _core_module:
+
+:mod:`cubicweb.pyramid.core`
+----------------------------
+
+.. automodule:: cubicweb.pyramid.core
+
+    .. autofunction:: includeme
+
+    .. autofunction:: cw_to_pyramid
+
+    .. autofunction:: render_view
+
+    .. autofunction:: repo_connect
+    .. autofunction:: get_principals
+
+    .. autoclass:: CubicWebPyramidRequest
+        :show-inheritance:
+        :members:
+
+    .. autofunction:: _cw_session
+    .. autofunction:: _cw_cnx
+    .. autofunction:: _cw_request
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/api/pyramid/defaults.rst	Mon Sep 26 14:52:12 2016 +0200
@@ -0,0 +1,8 @@
+.. _defaults_module:
+
+:mod:`cubicweb.pyramid.defaults`
+--------------------------------
+
+.. automodule:: cubicweb.pyramid.defaults
+
+    .. autofunction:: includeme
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/api/pyramid/login.rst	Mon Sep 26 14:52:12 2016 +0200
@@ -0,0 +1,16 @@
+.. _login_module:
+
+:mod:`cubicweb.pyramid.login`
+-----------------------------
+
+.. automodule:: cubicweb.pyramid.login
+
+    .. autofunction:: includeme
+
+
+    Views
+    -----
+
+    .. autofunction:: login_form
+    .. autofunction:: login_password_login
+    .. autofunction:: login_already_loggedin
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/api/pyramid/profile.rst	Mon Sep 26 14:52:12 2016 +0200
@@ -0,0 +1,19 @@
+.. _profile_module:
+
+:mod:`cubicweb.pyramid.profile`
+===============================
+
+.. automodule:: cubicweb.pyramid.profile
+
+    Views
+    -----
+
+    .. autofunction:: ping
+
+    .. autofunction:: cnx
+
+    WSGI
+    ----
+
+    .. autofunction:: wsgi_profile
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/api/pyramid/session.rst	Mon Sep 26 14:52:12 2016 +0200
@@ -0,0 +1,10 @@
+.. _session_module:
+
+:mod:`cubicweb.pyramid.session`
+-------------------------------
+
+.. automodule:: cubicweb.pyramid.session
+
+    .. autofunction:: includeme
+
+    .. autofunction:: CWSessionFactory
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/api/pyramid/tools.rst	Mon Sep 26 14:52:12 2016 +0200
@@ -0,0 +1,13 @@
+.. _tools_module:
+
+:mod:`cubicweb.pyramid.tools`
+----------------------------
+
+.. automodule:: cubicweb.pyramid.tools
+
+    .. autofunction:: includeme
+
+    .. autofunction:: clone_user
+    .. autofunction:: cnx_attach_entity
+    .. autofunction:: cached_build_user
+    .. autofunction:: clear_cache
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/pyramid/auth.rst	Mon Sep 26 14:52:12 2016 +0200
@@ -0,0 +1,40 @@
+Authentication
+==============
+
+Overview
+--------
+
+A default authentication stack is provided by the :mod:`cubicweb.pyramid.auth`
+module, which is included by :mod:`cubicweb.pyramid.default`.
+
+The authentication stack is built around `pyramid_multiauth`_, and provides a
+few default policies that reproduce the default cubicweb behavior.
+
+.. note::
+
+    Note that this module only provides an authentication policy, not the views
+    that handle the login form. See :ref:`login_module`
+
+Customize
+---------
+
+The default policies can be individually deactivated, as well as the default
+authentication callback that returns the current user groups as :term:`principals`.
+
+The following settings can be set to `False`:
+
+-   :confval:`cubicweb.auth.update_login_time`. Activate the policy that update
+    the user `login_time` when `remember` is called.
+-   :confval:`cubicweb.auth.authtkt` and all its subvalues.
+-   :confval:`cubicweb.auth.groups_principals`
+
+Additionnal policies can be added by accessing the MultiAuthenticationPolicy
+instance in the registry:
+
+.. code-block:: python
+
+    mypolicy = SomePolicy()
+    authpolicy = config.registry['cubicweb.authpolicy']
+    authpolicy._policies.append(mypolicy)
+
+.. _pyramid_multiauth: https://github.com/mozilla-services/pyramid_multiauth
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/pyramid/ctl.rst	Mon Sep 26 14:52:12 2016 +0200
@@ -0,0 +1,63 @@
+.. _cubicweb-ctl_pyramid:
+
+The 'pyramid' command
+=====================
+
+.. program:: cubicweb-ctl pyramid
+
+The 'pyramid' command is a replacement for the 'start' command of :ref:`cubicweb-ctl`.
+It provides the same options and a few other ones.
+
+.. note::
+
+    The 'pyramid' command is provided by the ``pyramid`` cube.
+
+Options
+-------
+
+
+.. option:: --no-daemon
+
+    Run the server in the foreground.
+
+.. option:: --debug-mode
+
+    Activate the repository debug mode (logs in the console and the debug
+    toolbar). Implies :option:`--no-daemon`.
+
+    Also force the following pyramid options:
+
+    .. code-block:: ini
+    
+        pyramid.debug_authorization = yes
+        pyramid.debug_notfound = yes
+        pyramid.debug_routematch = yes
+        pyramid.reload_templates = yes
+
+.. option:: -D, --debug
+
+    Equals to :option:`--debug-mode` :option:`--no-daemon` :option:`--reload`
+
+.. option:: --reload
+
+    Restart the server if any source file is changed
+
+.. option:: --reload-interval=RELOAD_INTERVAL
+
+    Interval, in seconds, between file modifications checks [current: 1]
+
+.. option:: -l <log level>, --loglevel=<log level>
+
+    Set the loglevel. debug if -D is set, error otherwise
+
+.. option:: -p, --profile
+
+    Enable profiling. See :ref:`profiling`.
+
+.. option:: --profile-output=PROFILE_OUTPUT
+
+    Profiling output file (default: "program.prof")
+
+.. option:: --profile-dump-every=N
+
+    Dump profile stats to ouput every N requests (default: 100)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/pyramid/index.rst	Mon Sep 26 14:52:12 2016 +0200
@@ -0,0 +1,36 @@
+================
+Pyramid Cubicweb
+================
+
+Pyramid Cubicweb is an attempt to rebase the CubicWeb framework on pyramid.
+
+It can be used in two different ways:
+
+-   Within CubicWeb, through the 'pyramid' cube and the
+    :ref:`pyramid command <cubicweb-ctl_pyramid>`.
+    In this mode, the Pyramid CubicWeb replaces some parts of
+    CubicWeb and make the pyramid api available to the cubes.
+
+-   Within a pyramid application, it provides easy access to a CubicWeb
+    instance and registry.
+
+Narrative Documentation
+=======================
+
+.. toctree::
+    :maxdepth: 2
+    
+    quickstart
+    ctl
+    settings
+    auth
+    profiling
+
+Api Documentation
+=================
+
+.. toctree::
+    :maxdepth: 2
+    :glob:
+
+    ../../api/pyramid
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/pyramid/profiling.rst	Mon Sep 26 14:52:12 2016 +0200
@@ -0,0 +1,52 @@
+.. _profiling:
+
+Profiling
+=========
+
+Profiling of requests by the pyramid debug toolbar can be a little restrictive
+when a specific url needs thin profiling that includes the whole pyramid
+dispatch.
+
+Pyramid CubicWeb provides facilities to profile requests as a
+:func:`wsgi middleware <cubicweb.pyramid.profile.wsgi_profile>`, and a few
+views that facilitate profiling of basic features.
+
+The views and the wsgi middleware are activated when the 'profile' option is
+given. This can be done on the command line
+(:option:`cubicweb-ctl pyramid --profile`) or in the :ref:`pyramid_settings`.
+
+Views
+-----
+
+The following routes and corresponding views are provided when profiling is on:
+
+-   ``/_profile/ping``: Reply 'ping' without doing anything else. See also
+    :func:`cubicweb.pyramid.profile.ping`.
+
+-   ``/_profile/cnx``: Reply 'ping' after getting a cnx. See also
+    :func:`cubicweb.pyramid.profile.cnx`.
+
+Typical Usage
+-------------
+
+Let's say we want to measure the cost of having a ``cnx``.
+
+-   Start the application with profile enabled:
+
+    .. code-block:: console
+
+        $ cubicweb-ctl pyramid --no-daemon --profile --profile-dump-every 100
+
+-   Use 'ab' or any other http benchmark tool to throw a lot of requests:
+
+    .. code-block:: console
+
+        $ ab -c 1 -n 100 http://localhost:8080/_profile/cnx
+
+-   Analyse the results. I personnaly fancy SnakeViz_:
+
+    .. code-block:: console
+
+        $ snakeviz program.prof
+
+.. _SnakeViz: http://jiffyclub.github.io/snakeviz/
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/pyramid/quickstart.rst	Mon Sep 26 14:52:12 2016 +0200
@@ -0,0 +1,59 @@
+Quick start
+===========
+
+.. highlight:: bash
+
+Prerequites
+-----------
+
+-   Install everything (here with pip, possibly in a virtualenv)::
+
+        pip install pyramid-cubicweb cubicweb-pyramid pyramid_debugtoolbar
+
+-   Have a working Cubicweb instance, for example:
+
+
+    -   Make sure CubicWeb is in user mode::
+
+            export CW_MODE=user
+
+    -   Create a CubicWeb instance, and install the 'pyramid' cube on it (see
+        :ref:`configenv` for more details on this step)::
+
+            cubicweb-ctl create pyramid myinstance
+
+-   Edit your ``~/etc/cubicweb.d/myinstance/all-in-one.conf`` and set values for
+    :confval:`pyramid-auth-secret` and :confval:`pyramid-session-secret`.
+    *required if cubicweb.pyramid.auth and pyramid_cubiweb.session get
+    included, which is the default*
+
+From CubicWeb
+-------------
+
+-   Start the instance with the :ref:`'pyramid' command <cubicweb-ctl_pyramid>`
+    instead of 'start'::
+
+        cubicweb-ctl pyramid --debug myinstance
+
+In a pyramid application
+------------------------
+
+-   Create a pyramid application
+
+-   Include cubicweb.pyramid:
+
+    .. code-block:: python
+
+        def includeme(config):
+            # ...
+            config.include('cubicweb.pyramid')
+            # ...
+
+-   Configure the instance name (in the .ini file):
+
+    .. code-block:: ini
+
+        cubicweb.instance = myinstance
+
+-   Configure the base-url and https-url in all-in-one.conf to match the ones
+    of the pyramid configuration (this is a temporary limitation).
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/pyramid/settings.rst	Mon Sep 26 14:52:12 2016 +0200
@@ -0,0 +1,146 @@
+Settings
+========
+
+.. _cubicweb_settings:
+
+Cubicweb Settings
+-----------------
+
+Pyramid CubicWeb will **not** make use of the configuration entries
+found in the cubicweb configuration (a.k.a. `all-in-one.conf`) for any
+pyramid related configuration value.
+
+
+.. _pyramid_settings:
+
+Pyramid Settings
+----------------
+
+If a ``pyramid.ini`` file is found in the instance home directory (where the
+``all-in-one.conf`` file is), its ``[main]`` section will be read and used as the
+``settings`` of the pyramid Configurator.
+
+This configuration file is almost the same as the one read by ``pserve``, which
+allow to easily add any pyramid extension and configure it.
+
+A typical ``pyramid.ini`` file is:
+
+.. code-block:: ini
+
+    [main]
+    pyramid.includes =
+        pyramid_redis_sessions
+
+    cubicweb.defaults = no
+    cubicweb.includes =
+        cubicweb.pyramid.auth
+        cubicweb.pyramid.login
+
+    cubicweb.profile = no
+
+    redis.sessions.secret = your_cookie_signing_secret
+    redis.sessions.timeout = 1200
+
+    redis.sessions.host = mywheezy
+
+The Pyramid CubicWeb specific configuration entries are:
+
+.. confval:: cubicweb.instance (string)
+
+    A CubicWeb instance name. Useful when the application is not run by
+    :ref:`cubicweb-ctl_pyramid`.
+
+.. confval:: cubicweb.debug (bool)
+
+    Enables the cubicweb debugmode. Works only if the instance is setup by
+    :confval:`cubicweb.instance`.
+
+    Unlike when the debugmode is set by the :option:`cubicweb-ctl pyramid --debug-mode`
+    command, the pyramid debug options are untouched.
+
+.. confval:: cubicweb.includes (list)
+
+    Same as ``pyramid.includes``, but the includes are done after the cubicweb
+    specific registry entries are initialized.
+
+    Useful to include extensions that requires these entries.
+
+.. confval:: cubicweb.bwcompat (bool)
+
+    (True) Enable/disable backward compatibility. See :ref:`bwcompat_module`.
+
+.. confval:: cubicweb.bwcompat.errorhandler (bool)
+
+    (True) Enable/disable the backward compatibility error handler.
+    Set to 'no' if you need to define your own error handlers.
+
+.. confval:: cubicweb.defaults (bool)
+
+    (True) Enable/disable defaults. See :ref:`defaults_module`.
+
+.. confval:: cubicweb.profile (bool)
+
+    (False) Enable/disable profiling. See :ref:`profiling`.
+
+.. confval:: cubicweb.auth.update_login_time (bool)
+
+    (True) Add a :class:`cubicweb.pyramid.auth.UpdateLoginTimeAuthenticationPolicy`
+    policy, that update the CWUser.login_time attribute when a user login.
+    
+.. confval:: cubicweb.auth.authtkt (bool)
+
+    (True) Enables the 2 cookie-base auth policies, which activate/deactivate
+    depending on the `persistent` argument passed to `remember`.
+
+    The default login views set persistent to True if a `__setauthcookie`
+    parameters is passed to them, and evals to True in
+    :func:`pyramid.settings.asbool`.
+
+    The configuration values of the policies are arguments for
+    :class:`pyramid.authentication.AuthTktAuthenticationPolicy`.
+
+    The first policy handles session authentication. It doesn't get
+    activated if `remember()` is called with `persistent=False`:
+
+    .. confval:: cubicweb.auth.authtkt.session.cookie_name (str)
+
+        ('auth_tkt') The cookie name. Must be different from the persistent
+        authentication cookie name.
+
+    .. confval:: cubicweb.auth.authtkt.session.timeout (int)
+
+        (1200) Cookie timeout.
+
+    .. confval:: cubicweb.auth.authtkt.session.reissue_time (int)
+
+        (120) Reissue time.
+
+    The second policy handles persistent authentication. It doesn't get
+    activated if `remember()` is called with `persistent=True`:
+
+    .. confval:: cubicweb.auth.authtkt.persistent.cookie_name (str)
+
+        ('auth_tkt') The cookie name. Must be different from the session
+        authentication cookie name.
+
+    .. confval:: cubicweb.auth.authtkt.persistent.max_age (int)
+
+        (30 days) Max age in seconds.
+
+    .. confval:: cubicweb.auth.authtkt.persistent.reissue_time (int)
+
+        (1 day) Reissue time in seconds.
+
+    Both policies set the ``secure`` flag to ``True`` by default, meaning that
+    cookies will only be sent back over a secure connection (see
+    `Authentication Policies documentation`_ for details). This can be
+    configured through :confval:`cubicweb.auth.authtkt.persistent.secure` and
+    :confval:`cubicweb.auth.authtkt.session.secure` configuration options.
+
+    .. _`Authentication Policies documentation`: \
+        http://docs.pylonsproject.org/projects/pyramid/en/latest/api/authentication.html
+
+.. confval:: cubicweb.auth.groups_principals (bool)
+
+    (True) Setup a callback on the authentication stack that inject the user
+    groups in the principals.
--- a/doc/conf.py	Fri Sep 23 16:04:32 2016 +0200
+++ b/doc/conf.py	Mon Sep 26 14:52:12 2016 +0200
@@ -224,3 +224,8 @@
 .. |yams| replace:: *Yams*
 .. |rql| replace:: *RQL*
 """
+
+def setup(app):
+    app.add_object_type('confval', 'confval',
+                        objname='configuration value',
+                        indextemplate='pair: %s; configuration value')
--- a/doc/index.rst	Fri Sep 23 16:04:32 2016 +0200
+++ b/doc/index.rst	Mon Sep 26 14:52:12 2016 +0200
@@ -73,6 +73,7 @@
 
    book/devrepo/index
    book/devweb/index
+   book/pyramid/index
 
 .. toctree::
    :maxdepth: 2
--- a/requirements/test-misc.txt	Fri Sep 23 16:04:32 2016 +0200
+++ b/requirements/test-misc.txt	Mon Sep 26 14:52:12 2016 +0200
@@ -20,5 +20,8 @@
 ## cubicweb/hooks/test
 psycopg2
 
+## cubicweb/pyramid/test
+http://hg.logilab.org/review/cubes/pyramid/archive/4808ab6b1c9c.tar.bz2
+
 ## cubicweb/sobject/test
 cubicweb-comment
--- a/tox.ini	Fri Sep 23 16:04:32 2016 +0200
+++ b/tox.ini	Mon Sep 26 14:52:12 2016 +0200
@@ -15,7 +15,7 @@
 commands =
   py34: touch {envdir}/share/cubicweb/cubes/__init__.py
   misc: {envpython} -m pip install --upgrade --no-deps --quiet git+git://github.com/logilab/yapps@master#egg=yapps
-  misc: {envpython} -m pytest {posargs} {toxinidir}/cubicweb/test {toxinidir}/cubicweb/dataimport/test {toxinidir}/cubicweb/devtools/test {toxinidir}/cubicweb/entities/test {toxinidir}/cubicweb/ext/test {toxinidir}/cubicweb/hooks/test {toxinidir}/cubicweb/sobjects/test {toxinidir}/cubicweb/wsgi/test
+  misc: {envpython} -m pytest {posargs} {toxinidir}/cubicweb/test {toxinidir}/cubicweb/dataimport/test {toxinidir}/cubicweb/devtools/test {toxinidir}/cubicweb/entities/test {toxinidir}/cubicweb/ext/test {toxinidir}/cubicweb/hooks/test {toxinidir}/cubicweb/sobjects/test {toxinidir}/cubicweb/wsgi/test {toxinidir}/cubicweb/pyramid/test
   py27-misc: {envpython} -m pytest {posargs} {toxinidir}/cubicweb/etwist/test
   server: {envpython} -m pytest {posargs} {toxinidir}/cubicweb/server/test
   web: {envpython} -m pytest {posargs} {toxinidir}/cubicweb/web/test
@@ -139,5 +139,25 @@
   cubicweb/web/views/json.py,
   cubicweb/web/views/searchrestriction.py,
   cubicweb/xy.py,
+  cubicweb/pyramid/auth.py,
+  cubicweb/pyramid/bwcompat.py,
+  cubicweb/pyramid/core.py,
+  cubicweb/pyramid/defaults.py,
+  cubicweb/pyramid/init_instance.py,
+  cubicweb/pyramid/__init__.py,
+  cubicweb/pyramid/login.py,
+  cubicweb/pyramid/predicates.py,
+  cubicweb/pyramid/profile.py,
+  cubicweb/pyramid/resources.py,
+  cubicweb/pyramid/rest_api.py,
+  cubicweb/pyramid/session.py,
+  cubicweb/pyramid/tools.py,
+  cubicweb/pyramid/test/__init__.py,
+  cubicweb/pyramid/test/test_bw_request.py,
+  cubicweb/pyramid/test/test_core.py,
+  cubicweb/pyramid/test/test_login.py,
+  cubicweb/pyramid/test/test_rest_api.py,
+  cubicweb/pyramid/test/test_tools.py,
+
 
 # vim: wrap sts=2 sw=2