Merge cubicweb-pyramid cube
authorYann Voté <yann.vote@logilab.fr>
Mon, 26 Sep 2016 16:45:30 +0200
changeset 11681 b23d58050076
parent 11631 faf279e33298 (current diff)
parent 11680 e1caf133b81c (diff)
child 11682 299640088c23
Merge cubicweb-pyramid cube Only keep the CWSession schema definition and the ctl command, now in cubicweb/pyramid/pyramidctl.py Related to #14023058.
MANIFEST.in
README.pyramid.rst
cubicweb/cwconfig.py
cubicweb/misc/migration/3.24.0_Any.py
cubicweb/pyramid/pyramidctl.py
cubicweb/pyramid/test/data/bootstrap_cubes
cubicweb/schema.py
cubicweb/schemas/base.py
cubicweb/server/test/unittest_querier.py
cubicweb/test/unittest_cwconfig.py
cubicweb/test/unittest_schema.py
cubicweb/web/test/test_views.py
requirements/test-misc.txt
tox.ini
--- a/MANIFEST.in	Mon Sep 26 14:52:12 2016 +0200
+++ b/MANIFEST.in	Mon Sep 26 16:45:30 2016 +0200
@@ -48,7 +48,6 @@
 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
--- a/README.pyramid.rst	Mon Sep 26 14:52:12 2016 +0200
+++ b/README.pyramid.rst	Mon Sep 26 16:45:30 2016 +0200
@@ -83,3 +83,88 @@
 .. _documentation: http://pyramid-cubicweb.readthedocs.org/
 .. _AuthTktAuthenticationPolicy: \
     http://docs.pylonsproject.org/projects/pyramid/en/latest/api/authentication.html#pyramid.authentication.AuthTktAuthenticationPolicy
+
+Command
+=======
+
+Summary
+-------
+
+Add the 'pyramid' command to cubicweb-ctl".
+
+This cube also add a ``CWSession`` entity type so that sessions can be
+stored in the database, which allows to run a Cubicweb instance
+without having to set up a session storage (like redis or memcache)
+solution.
+
+However, for production systems, it is greatly advised to use such a
+storage solution for the sessions.
+
+The handling of the sessions is made by pyramid (see the
+`pyramid's documentation on sessions`_ for more details).
+
+For example, to set up a redis based session storage, you need the
+`pyramid-redis-session`_ package, then you must configure pyramid to
+use this backend, by configuring the ``pyramid.ini`` file in the instance's
+config directory (near the ``all-in-one.conf`` file):
+
+
+.. code-block:: ini
+
+   [main]
+   cubicweb.defaults = no # we do not want to load the default cw session handling
+
+   cubicweb.auth.authtkt.session.secret = <secret1>
+   cubicweb.auth.authtkt.persistent.secret = <secret2>
+   cubicweb.auth.authtkt.session.secure = yes
+   cubicweb.auth.authtkt.persistent.secure = yes
+
+   redis.sessions.secret = <secret3>
+   redis.sessions.prefix = <my-app>:
+
+   redis.sessions.url = redis://localhost:6379/0
+
+   pyramid.includes =
+           pyramid_redis_sessions
+           pyramid_cubicweb.auth
+           pyramid_cubicweb.login
+
+
+See the documentation of `Pyramid Cubicweb`_ for more details.
+
+.. Warning:: If you want to be able to log in a CubicWeb application
+             served by pyramid on a unsecured stream (typically when
+             you start an instance in dev mode using a simple
+             ``cubicweb-ctl pyramid -D -linfo myinstance``), you
+             **must** set ``cubicweb.auth.authtkt.session.secure`` to
+             ``no``.
+
+Secrets
+~~~~~~~
+
+There are a number of secrets to configure in ``pyramid.ini``. They
+should be different one from each other, as explained in `Pyramid's
+documentation`_.
+
+For the record:
+
+:cubicweb.session.secret: This secret is used to encrypt the session's
+   data ID (data themselved are stored in the backend, database or
+   redis) when using the integrated (``CWSession`` based) session data
+   storage.
+
+:redis.session.secret: This secret is used to encrypt the session's
+   data ID (data themselved are stored in the backend, database or
+   redis) when using redis as backend.
+
+:cubicweb.auth.authtkt.session.secret: This secret is used to encrypt
+   the authentication cookie.
+
+:cubicweb.auth.authtkt.persistent.secret: This secret is used to
+   encrypt the persistent authentication cookie.
+
+
+.. _`Pyramid Cubicweb`: http://pyramid-cubicweb.readthedocs.org/
+.. _`pyramid's documentation on sessions`: http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/sessions.html
+.. _`pyramid-redis-session`: http://pyramid-redis-sessions.readthedocs.org/en/latest/index.html
+.. _`Pyramid's documentation`: http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/security.html#admonishment-against-secret-sharing
--- a/cubicweb/cwconfig.py	Mon Sep 26 14:52:12 2016 +0200
+++ b/cubicweb/cwconfig.py	Mon Sep 26 16:45:30 2016 +0200
@@ -681,7 +681,7 @@
     def load_cwctl_plugins(cls):
         cls.cls_adjust_sys_path()
         for ctlmod in ('web.webctl',  'etwist.twctl', 'server.serverctl',
-                       'devtools.devctl'):
+                       'devtools.devctl', 'pyramid.pyramidctl'):
             try:
                 __import__('cubicweb.%s' % ctlmod)
             except ImportError:
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/misc/migration/3.24.0_Any.py	Mon Sep 26 16:45:30 2016 +0200
@@ -0,0 +1,2 @@
+# Check the CW versions and add the entity only if needed ?
+add_entity_type('CWSession')
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/pyramid/pyramidctl.py	Mon Sep 26 16:45:30 2016 +0200
@@ -0,0 +1,460 @@
+"""
+Provides a 'pyramid' command as a replacement to the 'start' command.
+
+The reloading strategy is heavily inspired by (and partially copied from)
+the pyramid script 'pserve'.
+"""
+from __future__ import print_function
+
+import atexit
+import errno
+import os
+import signal
+import sys
+import tempfile
+import time
+import threading
+import subprocess
+
+from cubicweb import BadCommandUsage, ExecutionError
+from cubicweb.__pkginfo__ import numversion as cwversion
+from cubicweb.cwconfig import CubicWebConfiguration as cwcfg
+from cubicweb.cwctl import CWCTL, InstanceCommand, init_cmdline_log_threshold
+from cubicweb.pyramid import wsgi_application_from_cwconfig
+from cubicweb.server import set_debug
+
+import waitress
+
+MAXFD = 1024
+
+DBG_FLAGS = ('RQL', 'SQL', 'REPO', 'HOOKS', 'OPS', 'SEC', 'MORE')
+LOG_LEVELS = ('debug', 'info', 'warning', 'error')
+
+
+class PyramidStartHandler(InstanceCommand):
+    """Start an interactive pyramid server.
+
+    This command requires http://hg.logilab.org/review/pyramid_cubicweb/
+
+    <instance>
+      identifier of the instance to configure.
+    """
+    name = 'pyramid'
+
+    options = (
+        ('no-daemon',
+         {'action': 'store_true',
+          'help': 'Run the server in the foreground.'}),
+        ('debug-mode',
+         {'action': 'store_true',
+          'help': 'Activate the repository debug mode ('
+                  'logs in the console and the debug toolbar).'
+                  ' Implies --no-daemon'}),
+        ('debug',
+         {'short': 'D', 'action': 'store_true',
+          'help': 'Equals to "--debug-mode --no-daemon --reload"'}),
+        ('reload',
+         {'action': 'store_true',
+          'help': 'Restart the server if any source file is changed'}),
+        ('reload-interval',
+         {'type': 'int', 'default': 1,
+          'help': 'Interval, in seconds, between file modifications checks'}),
+        ('loglevel',
+         {'short': 'l', 'type': 'choice', 'metavar': '<log level>',
+          'default': None, 'choices': LOG_LEVELS,
+          'help': 'debug if -D is set, error otherwise; '
+                  'one of %s' % (LOG_LEVELS,),
+          }),
+        ('dbglevel',
+         {'type': 'multiple_choice', 'metavar': '<dbg level>',
+          'default': None,
+          'choices': DBG_FLAGS,
+          'help': ('Set the server debugging flags; you may choose several '
+                   'values in %s; imply "debug" loglevel' % (DBG_FLAGS,)),
+          }),
+        ('profile',
+         {'action': 'store_true',
+          'default': False,
+          'help': 'Enable profiling'}),
+        ('profile-output',
+         {'type': 'string',
+          'default': None,
+          'help': 'Profiling output file (default: "program.prof")'}),
+        ('profile-dump-every',
+         {'type': 'int',
+          'default': None,
+          'metavar': 'N',
+          'help': 'Dump profile stats to ouput every N requests '
+                  '(default: 100)'}),
+    )
+    if cwversion >= (3, 21, 0):
+        options = options + (
+            ('param',
+             {'short': 'p',
+              'type': 'named',
+              'metavar': 'key1:value1,key2:value2',
+              'default': {},
+              'help': 'override <key> configuration file option with <value>.',
+              }),
+        )
+
+    _reloader_environ_key = 'CW_RELOADER_SHOULD_RUN'
+    _reloader_filelist_environ_key = 'CW_RELOADER_FILELIST'
+
+    def debug(self, msg):
+        print('DEBUG - %s' % msg)
+
+    def info(self, msg):
+        print('INFO - %s' % msg)
+
+    def ordered_instances(self):
+        instances = super(PyramidStartHandler, self).ordered_instances()
+        if (self['debug-mode'] or self['debug'] or self['reload']) \
+                and len(instances) > 1:
+            raise BadCommandUsage(
+                '--debug-mode, --debug and --reload can be used on a single '
+                'instance only')
+        return instances
+
+    def quote_first_command_arg(self, arg):
+        """
+        There's a bug in Windows when running an executable that's
+        located inside a path with a space in it.  This method handles
+        that case, or on non-Windows systems or an executable with no
+        spaces, it just leaves well enough alone.
+        """
+        if (sys.platform != 'win32' or ' ' not in arg):
+            # Problem does not apply:
+            return arg
+        try:
+            import win32api
+        except ImportError:
+            raise ValueError(
+                "The executable %r contains a space, and in order to "
+                "handle this issue you must have the win32api module "
+                "installed" % arg)
+        arg = win32api.GetShortPathName(arg)
+        return arg
+
+    def _remove_pid_file(self, written_pid, filename):
+        current_pid = os.getpid()
+        if written_pid != current_pid:
+            # A forked process must be exiting, not the process that
+            # wrote the PID file
+            return
+        if not os.path.exists(filename):
+            return
+        with open(filename) as f:
+            content = f.read().strip()
+        try:
+            pid_in_file = int(content)
+        except ValueError:
+            pass
+        else:
+            if pid_in_file != current_pid:
+                msg = "PID file %s contains %s, not expected PID %s"
+                self.out(msg % (filename, pid_in_file, current_pid))
+                return
+        self.info("Removing PID file %s" % filename)
+        try:
+            os.unlink(filename)
+            return
+        except OSError as e:
+            # Record, but don't give traceback
+            self.out("Cannot remove PID file: (%s)" % e)
+        # well, at least lets not leave the invalid PID around...
+        try:
+            with open(filename, 'w') as f:
+                f.write('')
+        except OSError as e:
+            self.out('Stale PID left in file: %s (%s)' % (filename, e))
+        else:
+            self.out('Stale PID removed')
+
+    def record_pid(self, pid_file):
+        pid = os.getpid()
+        self.debug('Writing PID %s to %s' % (pid, pid_file))
+        with open(pid_file, 'w') as f:
+            f.write(str(pid))
+        atexit.register(
+            self._remove_pid_file, pid, pid_file)
+
+    def daemonize(self, pid_file):
+        pid = live_pidfile(pid_file)
+        if pid:
+            raise ExecutionError(
+                "Daemon is already running (PID: %s from PID file %s)"
+                % (pid, pid_file))
+
+        self.debug('Entering daemon mode')
+        pid = os.fork()
+        if pid:
+            # The forked process also has a handle on resources, so we
+            # *don't* want proper termination of the process, we just
+            # want to exit quick (which os._exit() does)
+            os._exit(0)
+        # Make this the session leader
+        os.setsid()
+        # Fork again for good measure!
+        pid = os.fork()
+        if pid:
+            os._exit(0)
+
+        # @@: Should we set the umask and cwd now?
+
+        import resource  # Resource usage information.
+        maxfd = resource.getrlimit(resource.RLIMIT_NOFILE)[1]
+        if (maxfd == resource.RLIM_INFINITY):
+            maxfd = MAXFD
+        # Iterate through and close all file descriptors.
+        for fd in range(0, maxfd):
+            try:
+                os.close(fd)
+            except OSError:  # ERROR, fd wasn't open to begin with (ignored)
+                pass
+
+        if (hasattr(os, "devnull")):
+            REDIRECT_TO = os.devnull
+        else:
+            REDIRECT_TO = "/dev/null"
+        os.open(REDIRECT_TO, os.O_RDWR)  # standard input (0)
+        # Duplicate standard input to standard output and standard error.
+        os.dup2(0, 1)  # standard output (1)
+        os.dup2(0, 2)  # standard error (2)
+
+    def restart_with_reloader(self):
+        self.debug('Starting subprocess with file monitor')
+
+        with tempfile.NamedTemporaryFile(delete=False) as f:
+            filelist_path = f.name
+
+        while True:
+            args = [self.quote_first_command_arg(sys.executable)] + sys.argv
+            new_environ = os.environ.copy()
+            new_environ[self._reloader_environ_key] = 'true'
+            new_environ[self._reloader_filelist_environ_key] = filelist_path
+            proc = None
+            try:
+                try:
+                    proc = subprocess.Popen(args, env=new_environ)
+                    exit_code = proc.wait()
+                    proc = None
+                    print("Process exited with", exit_code)
+                except KeyboardInterrupt:
+                    self.info('^C caught in monitor process')
+                    return 1
+            finally:
+                if proc is not None:
+                    proc.terminate()
+                    self.info(
+                        'Waiting for the server to stop. Hit CTRL-C to exit')
+                    exit_code = proc.wait()
+
+            if exit_code != 3:
+                with open(filelist_path) as f:
+                    filelist = [line.strip() for line in f]
+                if filelist:
+                    self.info("Reloading failed. Waiting for a file to change")
+                    mon = Monitor(extra_files=filelist, nomodules=True)
+                    while mon.check_reload():
+                        time.sleep(1)
+                else:
+                    return exit_code
+
+            self.info('%s %s %s' % ('-' * 20, 'Restarting', '-' * 20))
+
+    def set_needreload(self):
+        self._needreload = True
+
+    def install_reloader(self, poll_interval, extra_files, filelist_path):
+        mon = Monitor(
+            poll_interval=poll_interval, extra_files=extra_files,
+            atexit=self.set_needreload, filelist_path=filelist_path)
+        mon_thread = threading.Thread(target=mon.periodic_reload)
+        mon_thread.daemon = True
+        mon_thread.start()
+
+    def configfiles(self, cwconfig):
+        """Generate instance configuration filenames"""
+        yield cwconfig.main_config_file()
+        for f in (
+                'sources', 'logging.conf', 'pyramid.ini', 'pyramid-debug.ini'):
+            f = os.path.join(cwconfig.apphome, f)
+            if os.path.exists(f):
+                yield f
+
+    def i18nfiles(self, cwconfig):
+        """Generate instance i18n files"""
+        i18ndir = os.path.join(cwconfig.apphome, 'i18n')
+        if os.path.exists(i18ndir):
+            for lang in cwconfig.available_languages():
+                f = os.path.join(i18ndir, lang, 'LC_MESSAGES', 'cubicweb.mo')
+                if os.path.exists(f):
+                    yield f
+
+    def pyramid_instance(self, appid):
+        self._needreload = False
+
+        debugmode = self['debug-mode'] or self['debug']
+        autoreload = self['reload'] or self['debug']
+        daemonize = not (self['no-daemon'] or debugmode or autoreload)
+
+        if autoreload and not os.environ.get(self._reloader_environ_key):
+            return self.restart_with_reloader()
+
+        cwconfig = cwcfg.config_for(appid, debugmode=debugmode)
+        if cwversion >= (3, 21, 0):
+            cwconfig.cmdline_options = self.config.param
+        if autoreload:
+            _turn_sigterm_into_systemexit()
+            self.debug('Running reloading file monitor')
+            extra_files = [sys.argv[0]]
+            extra_files.extend(self.configfiles(cwconfig))
+            extra_files.extend(self.i18nfiles(cwconfig))
+            self.install_reloader(
+                self['reload-interval'], extra_files,
+                filelist_path=os.environ.get(
+                    self._reloader_filelist_environ_key))
+
+        if daemonize:
+            self.daemonize(cwconfig['pid-file'])
+            self.record_pid(cwconfig['pid-file'])
+
+        if self['dbglevel']:
+            self['loglevel'] = 'debug'
+            set_debug('|'.join('DBG_' + x.upper() for x in self['dbglevel']))
+        init_cmdline_log_threshold(cwconfig, self['loglevel'])
+
+        app = wsgi_application_from_cwconfig(
+            cwconfig, profile=self['profile'],
+            profile_output=self['profile-output'],
+            profile_dump_every=self['profile-dump-every']
+        )
+
+        host = cwconfig['interface']
+        port = cwconfig['port'] or 8080
+        repo = app.application.registry['cubicweb.repository']
+        try:
+            repo.start_looping_tasks()
+            waitress.serve(app, host=host, port=port)
+        finally:
+            repo.shutdown()
+        if self._needreload:
+            return 3
+        return 0
+
+CWCTL.register(PyramidStartHandler)
+
+
+def live_pidfile(pidfile):  # pragma: no cover
+    """(pidfile:str) -> int | None
+    Returns an int found in the named file, if there is one,
+    and if there is a running process with that process id.
+    Return None if no such process exists.
+    """
+    pid = read_pidfile(pidfile)
+    if pid:
+        try:
+            os.kill(int(pid), 0)
+            return pid
+        except OSError as e:
+            if e.errno == errno.EPERM:
+                return pid
+    return None
+
+
+def read_pidfile(filename):
+    if os.path.exists(filename):
+        try:
+            with open(filename) as f:
+                content = f.read()
+            return int(content.strip())
+        except (ValueError, IOError):
+            return None
+    else:
+        return None
+
+
+def _turn_sigterm_into_systemexit():
+    """Attempts to turn a SIGTERM exception into a SystemExit exception."""
+    try:
+        import signal
+    except ImportError:
+        return
+
+    def handle_term(signo, frame):
+        raise SystemExit
+    signal.signal(signal.SIGTERM, handle_term)
+
+
+class Monitor(object):
+    """A file monitor and server stopper.
+
+    It is a simplified version of pyramid pserve.Monitor, with little changes:
+
+    -   The constructor takes extra_files, atexit, nomodules and filelist_path
+    -   The process is stopped by auto-kill with signal SIGTERM
+    """
+
+    def __init__(self, poll_interval=1, extra_files=[], atexit=None,
+                 nomodules=False, filelist_path=None):
+        self.module_mtimes = {}
+        self.keep_running = True
+        self.poll_interval = poll_interval
+        self.extra_files = extra_files
+        self.atexit = atexit
+        self.nomodules = nomodules
+        self.filelist_path = filelist_path
+
+    def _exit(self):
+        if self.atexit:
+            self.atexit()
+        os.kill(os.getpid(), signal.SIGTERM)
+
+    def periodic_reload(self):
+        while True:
+            if not self.check_reload():
+                self._exit()
+                break
+            time.sleep(self.poll_interval)
+
+    def check_reload(self):
+        filenames = list(self.extra_files)
+
+        if not self.nomodules:
+            for module in list(sys.modules.values()):
+                try:
+                    filename = module.__file__
+                except (AttributeError, ImportError):
+                    continue
+                if filename is not None:
+                    filenames.append(filename)
+
+        for filename in filenames:
+            try:
+                stat = os.stat(filename)
+                if stat:
+                    mtime = stat.st_mtime
+                else:
+                    mtime = 0
+            except (OSError, IOError):
+                continue
+            if filename.endswith('.pyc') and os.path.exists(filename[:-1]):
+                mtime = max(os.stat(filename[:-1]).st_mtime, mtime)
+            if filename not in self.module_mtimes:
+                self.module_mtimes[filename] = mtime
+            elif self.module_mtimes[filename] < mtime:
+                print('%s changed; reloading...' % filename)
+                return False
+
+        if self.filelist_path:
+            with open(self.filelist_path) as f:
+                filelist = set((line.strip() for line in f))
+
+            filelist.update(filenames)
+
+            with open(self.filelist_path, 'w') as f:
+                for filename in filelist:
+                    f.write('%s\n' % filename)
+
+        return True
--- a/cubicweb/pyramid/test/data/bootstrap_cubes	Mon Sep 26 14:52:12 2016 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,1 +0,0 @@
-pyramid
--- a/cubicweb/schema.py	Mon Sep 26 14:52:12 2016 +0200
+++ b/cubicweb/schema.py	Mon Sep 26 16:45:30 2016 +0200
@@ -97,7 +97,8 @@
                       'SubWorkflowExitPoint'))
 
 INTERNAL_TYPES = set(('CWProperty', 'CWCache', 'ExternalUri', 'CWDataImport',
-                      'CWSource', 'CWSourceHostConfig', 'CWSourceSchemaConfig'))
+                      'CWSource', 'CWSourceHostConfig', 'CWSourceSchemaConfig',
+                      'CWSession'))
 
 UNIQUE_CONSTRAINTS = ('SizeConstraint', 'FormatConstraint',
                       'StaticVocabularyConstraint',
--- a/cubicweb/schemas/base.py	Mon Sep 26 14:52:12 2016 +0200
+++ b/cubicweb/schemas/base.py	Mon Sep 26 16:45:30 2016 +0200
@@ -23,7 +23,7 @@
 from yams.buildobjs import (EntityType, RelationType, RelationDefinition,
                             SubjectRelation,
                             String, TZDatetime, Datetime, Password, Interval,
-                            Boolean, UniqueConstraint)
+                            Boolean, Bytes, UniqueConstraint)
 from cubicweb.schema import (
     RQLConstraint, WorkflowableEntityType, ERQLExpression, RRQLExpression,
     PUB_SYSTEM_ENTITY_PERMS, PUB_SYSTEM_REL_PERMS, PUB_SYSTEM_ATTR_PERMS,
@@ -381,3 +381,17 @@
         'add':    ('managers', RRQLExpression('U has_update_permission S'),),
         'delete': ('managers', RRQLExpression('U has_update_permission S'),),
         }
+
+
+class CWSession(EntityType):
+    """Persistent session.
+
+    Used by cubicweb.pyramid to store the session data.
+    """
+    __permissions__ = {
+        'read':   ('managers',),
+        'add':    (),
+        'update': (),
+        'delete': (),
+    }
+    cwsessiondata = Bytes()
--- a/cubicweb/server/test/unittest_querier.py	Mon Sep 26 14:52:12 2016 +0200
+++ b/cubicweb/server/test/unittest_querier.py	Mon Sep 26 16:45:30 2016 +0200
@@ -608,15 +608,15 @@
                               [[u'description_format', 13],
                                [u'description', 14],
                                [u'name', 19],
-                               [u'created_by', 45],
-                               [u'creation_date', 45],
-                               [u'cw_source', 45],
-                               [u'cwuri', 45],
-                               [u'in_basket', 45],
-                               [u'is', 45],
-                               [u'is_instance_of', 45],
-                               [u'modification_date', 45],
-                               [u'owned_by', 45]])
+                               [u'created_by', 46],
+                               [u'creation_date', 46],
+                               [u'cw_source', 46],
+                               [u'cwuri', 46],
+                               [u'in_basket', 46],
+                               [u'is', 46],
+                               [u'is_instance_of', 46],
+                               [u'modification_date', 46],
+                               [u'owned_by', 46]])
 
     def test_select_aggregat_having_dumb(self):
         # dumb but should not raise an error
--- a/cubicweb/test/unittest_cwconfig.py	Mon Sep 26 14:52:12 2016 +0200
+++ b/cubicweb/test/unittest_cwconfig.py	Mon Sep 26 16:45:30 2016 +0200
@@ -81,7 +81,7 @@
         expected_cubes = [
             'card', 'comment', 'cubicweb_comment', 'cubicweb_email', 'file',
             'cubicweb_file', 'cubicweb_forge', 'localperms',
-            'cubicweb_mycube', 'pyramid', 'tag',
+            'cubicweb_mycube', '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', 'pyramid', 'tag',
+            'card', 'file', 'localperms', 'tag',
         ]))
         self._test_available_cubes(expected_cubes)
 
--- a/cubicweb/test/unittest_schema.py	Mon Sep 26 14:52:12 2016 +0200
+++ b/cubicweb/test/unittest_schema.py	Mon Sep 26 16:45:30 2016 +0200
@@ -174,7 +174,7 @@
             'CWCache', 'CWComputedRType', 'CWConstraint',
             'CWConstraintType', 'CWDataImport', 'CWEType',
             'CWAttribute', 'CWGroup', 'EmailAddress',
-            'CWRelation', 'CWPermission', 'CWProperty', 'CWRType',
+            'CWRelation', 'CWPermission', 'CWProperty', 'CWRType', 'CWSession',
             'CWSource', 'CWSourceHostConfig', 'CWSourceSchemaConfig',
             'CWUniqueTogetherConstraint', 'CWUser',
             'ExternalUri', 'FakeFile', 'Float', 'Int', 'Interval', 'Note',
@@ -196,7 +196,8 @@
             'constrained_by', 'constraint_of',
             'content', 'content_format', 'contrat_exclusif',
             'created_by', 'creation_date', 'cstrtype', 'custom_workflow',
-            'cwuri', 'cw_for_source', 'cw_import_of', 'cw_host_config_of', 'cw_schema', 'cw_source',
+            'cwuri', 'cwsessiondata', 'cw_for_source', 'cw_import_of', 'cw_host_config_of',
+            'cw_schema', 'cw_source',
 
             'data', 'data_encoding', 'data_format', 'data_name', 'default_workflow', 'defaultval',
             'delete_permission', 'description', 'description_format', 'destination_state',
@@ -526,6 +527,7 @@
                      ('cw_source', 'CWProperty', 'CWSource', 'object'),
                      ('cw_source', 'CWRType', 'CWSource', 'object'),
                      ('cw_source', 'CWRelation', 'CWSource', 'object'),
+                     ('cw_source', 'CWSession', 'CWSource', 'object'),
                      ('cw_source', 'CWSource', 'CWSource', 'object'),
                      ('cw_source', 'CWSourceHostConfig', 'CWSource', 'object'),
                      ('cw_source', 'CWSourceSchemaConfig', 'CWSource', 'object'),
--- a/cubicweb/web/test/test_views.py	Mon Sep 26 14:52:12 2016 +0200
+++ b/cubicweb/web/test/test_views.py	Mon Sep 26 16:45:30 2016 +0200
@@ -31,7 +31,7 @@
         # some EntityType. The two Blog types below require the sioc cube that
         # we do not want to add as a dependency.
         etypes = super(AutomaticWebTest, self).to_test_etypes()
-        etypes -= set(('Blog', 'BlogEntry'))
+        etypes -= set(('Blog', 'BlogEntry', 'CWSession'))
         return etypes
 
 
--- a/requirements/test-misc.txt	Mon Sep 26 14:52:12 2016 +0200
+++ b/requirements/test-misc.txt	Mon Sep 26 16:45:30 2016 +0200
@@ -20,8 +20,5 @@
 ## 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	Mon Sep 26 14:52:12 2016 +0200
+++ b/tox.ini	Mon Sep 26 16:45:30 2016 +0200
@@ -158,6 +158,7 @@
   cubicweb/pyramid/test/test_login.py,
   cubicweb/pyramid/test/test_rest_api.py,
   cubicweb/pyramid/test/test_tools.py,
+  cubicweb/pyramid/pyramidctl.py,
 
 
 # vim: wrap sts=2 sw=2