cubicweb/devtools/devctl.py
changeset 11057 0b59724cb3f2
parent 10967 3f620fd1ed18
child 11129 97095348b3ee
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/devtools/devctl.py	Sat Jan 16 13:48:51 2016 +0100
@@ -0,0 +1,851 @@
+# copyright 2003-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
+#
+# This file is part of CubicWeb.
+#
+# CubicWeb is free software: you can redistribute it and/or modify it under the
+# terms of the GNU Lesser General Public License as published by the Free
+# Software Foundation, either version 2.1 of the License, or (at your option)
+# any later version.
+#
+# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
+# details.
+#
+# You should have received a copy of the GNU Lesser General Public License along
+# with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
+"""additional cubicweb-ctl commands and command handlers for cubicweb and
+cubicweb's cubes development
+"""
+from __future__ import print_function
+
+__docformat__ = "restructuredtext en"
+
+# *ctl module should limit the number of import to be imported as quickly as
+# possible (for cubicweb-ctl reactivity, necessary for instance for usable bash
+# completion). So import locally in command helpers.
+import sys
+from datetime import datetime, date
+from os import mkdir, chdir, path as osp
+from warnings import warn
+
+from six.moves import input
+
+from logilab.common import STD_BLACKLIST
+
+from cubicweb.__pkginfo__ import version as cubicwebversion
+from cubicweb import CW_SOFTWARE_ROOT as BASEDIR, BadCommandUsage, ExecutionError
+from cubicweb.cwctl import CWCTL
+from cubicweb.cwconfig import CubicWebNoAppConfiguration
+from cubicweb.toolsutils import (SKEL_EXCLUDE, Command, copy_skeleton,
+                                 underline_title)
+from cubicweb.web.webconfig import WebConfiguration
+from cubicweb.server.serverconfig import ServerConfiguration
+
+
+class DevConfiguration(ServerConfiguration, WebConfiguration):
+    """dummy config to get full library schema and appobjects for
+    a cube or for cubicweb (without a home)
+    """
+    creating = True
+    cleanup_unused_appobjects = False
+
+    cubicweb_appobject_path = (ServerConfiguration.cubicweb_appobject_path
+                               | WebConfiguration.cubicweb_appobject_path)
+    cube_appobject_path = (ServerConfiguration.cube_appobject_path
+                           | WebConfiguration.cube_appobject_path)
+
+    def __init__(self, *cubes):
+        super(DevConfiguration, self).__init__(cubes and cubes[0] or None)
+        if cubes:
+            self._cubes = self.reorder_cubes(
+                self.expand_cubes(cubes, with_recommends=True))
+            self.load_site_cubicweb()
+        else:
+            self._cubes = ()
+
+    @property
+    def apphome(self):
+        return None
+
+    def available_languages(self):
+        return self.cw_languages()
+
+    def main_config_file(self):
+        return None
+    def init_log(self):
+        pass
+    def load_configuration(self, **kw):
+        pass
+    def default_log_file(self):
+        return None
+    def default_stats_file(self):
+        return None
+
+
+def cleanup_sys_modules(config):
+    # cleanup sys.modules, required when we're updating multiple cubes
+    for name, mod in list(sys.modules.items()):
+        if mod is None:
+            # duh ? logilab.common.os for instance
+            del sys.modules[name]
+            continue
+        if not hasattr(mod, '__file__'):
+            continue
+        if mod.__file__ is None:
+            # odd/rare but real
+            continue
+        for path in config.appobjects_path():
+            if mod.__file__.startswith(path):
+                del sys.modules[name]
+                break
+
+def generate_schema_pot(w, cubedir=None):
+    """generate a pot file with schema specific i18n messages
+
+    notice that relation definitions description and static vocabulary
+    should be marked using '_' and extracted using xgettext
+    """
+    from cubicweb.cwvreg import CWRegistryStore
+    if cubedir:
+        cube = osp.split(cubedir)[-1]
+        config = DevConfiguration(cube)
+        depcubes = list(config._cubes)
+        depcubes.remove(cube)
+        libconfig = DevConfiguration(*depcubes)
+    else:
+        config = DevConfiguration()
+        cube = libconfig = None
+    cleanup_sys_modules(config)
+    schema = config.load_schema(remove_unused_rtypes=False)
+    vreg = CWRegistryStore(config)
+    # set_schema triggers objects registrations
+    vreg.set_schema(schema)
+    w(DEFAULT_POT_HEAD)
+    _generate_schema_pot(w, vreg, schema, libconfig=libconfig)
+
+
+def _generate_schema_pot(w, vreg, schema, libconfig=None):
+    from cubicweb.i18n import add_msg
+    from cubicweb.schema import NO_I18NCONTEXT, CONSTRAINTS
+    w('# schema pot file, generated on %s\n'
+      % datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S'))
+    w('# \n')
+    w('# singular and plural forms for each entity type\n')
+    w('\n')
+    vregdone = set()
+    afss = vreg['uicfg']['autoform_section']
+    aiams = vreg['uicfg']['actionbox_appearsin_addmenu']
+    if libconfig is not None:
+        # processing a cube, libconfig being a config with all its dependencies
+        # (cubicweb incl.)
+        from cubicweb.cwvreg import CWRegistryStore
+        libschema = libconfig.load_schema(remove_unused_rtypes=False)
+        cleanup_sys_modules(libconfig)
+        libvreg = CWRegistryStore(libconfig)
+        libvreg.set_schema(libschema) # trigger objects registration
+        libafss = libvreg['uicfg']['autoform_section']
+        libaiams = libvreg['uicfg']['actionbox_appearsin_addmenu']
+        # prefill vregdone set
+        list(_iter_vreg_objids(libvreg, vregdone))
+
+        def is_in_lib(rtags, eschema, rschema, role, tschema, predicate=bool):
+            return any(predicate(rtag.etype_get(eschema, rschema, role, tschema))
+                       for rtag in rtags)
+    else:
+        # processing cubicweb itself
+        libschema = {}
+        for cstrtype in CONSTRAINTS:
+            add_msg(w, cstrtype)
+        libafss = libaiams = None
+        is_in_lib = lambda *args: False
+    done = set()
+    for eschema in sorted(schema.entities()):
+        if eschema.type in libschema:
+            done.add(eschema.description)
+    for eschema in sorted(schema.entities()):
+        etype = eschema.type
+        if etype not in libschema:
+            add_msg(w, etype)
+            add_msg(w, '%s_plural' % etype)
+            if not eschema.final:
+                add_msg(w, 'This %s:' % etype)
+                add_msg(w, 'New %s' % etype)
+                add_msg(w, 'add a %s' % etype) # AddNewAction
+                if libconfig is not None:  # processing a cube
+                    # As of 3.20.3 we no longer use it, but keeping this string
+                    # allows developers to run i18ncube with new cubicweb and still
+                    # have the right translations at runtime for older versions
+                    add_msg(w, 'This %s' % etype)
+            if eschema.description and not eschema.description in done:
+                done.add(eschema.description)
+                add_msg(w, eschema.description)
+        if eschema.final:
+            continue
+        for rschema, targetschemas, role in eschema.relation_definitions(True):
+            if rschema.final:
+                continue
+            for tschema in targetschemas:
+
+                for afs in afss:
+                    fsections = afs.etype_get(eschema, rschema, role, tschema)
+                    if 'main_inlined' in fsections and not \
+                            is_in_lib(libafss, eschema, rschema, role, tschema,
+                                      lambda x: 'main_inlined' in x):
+                        add_msg(w, 'add a %s' % tschema,
+                                'inlined:%s.%s.%s' % (etype, rschema, role))
+                        add_msg(w, str(tschema),
+                                'inlined:%s.%s.%s' % (etype, rschema, role))
+                        break
+
+                for aiam in aiams:
+                    if aiam.etype_get(eschema, rschema, role, tschema) and not \
+                            is_in_lib(libaiams, eschema, rschema, role, tschema):
+                        if role == 'subject':
+                            label = 'add %s %s %s %s' % (eschema, rschema,
+                                                         tschema, role)
+                            label2 = "creating %s (%s %%(linkto)s %s %s)" % (
+                                tschema, eschema, rschema, tschema)
+                        else:
+                            label = 'add %s %s %s %s' % (tschema, rschema,
+                                                         eschema, role)
+                            label2 = "creating %s (%s %s %s %%(linkto)s)" % (
+                                tschema, tschema, rschema, eschema)
+                        add_msg(w, label)
+                        add_msg(w, label2)
+                        break
+            # XXX also generate "creating ...' messages for actions in the
+            # addrelated submenu
+    w('# subject and object forms for each relation type\n')
+    w('# (no object form for final or symmetric relation types)\n')
+    w('\n')
+    for rschema in sorted(schema.relations()):
+        if rschema.type in libschema:
+            done.add(rschema.type)
+            done.add(rschema.description)
+    for rschema in sorted(schema.relations()):
+        rtype = rschema.type
+        if rtype not in libschema:
+            # bw compat, necessary until all translation of relation are done
+            # properly...
+            add_msg(w, rtype)
+            done.add(rtype)
+            if rschema.description and rschema.description not in done:
+                add_msg(w, rschema.description)
+            done.add(rschema.description)
+            librschema = None
+        else:
+            librschema = libschema.rschema(rtype)
+        # add context information only for non-metadata rtypes
+        if rschema not in NO_I18NCONTEXT:
+            libsubjects = librschema and librschema.subjects() or ()
+            for subjschema in rschema.subjects():
+                if not subjschema in libsubjects:
+                    add_msg(w, rtype, subjschema.type)
+        if not (rschema.final or rschema.symmetric):
+            if rschema not in NO_I18NCONTEXT:
+                libobjects = librschema and librschema.objects() or ()
+                for objschema in rschema.objects():
+                    if not objschema in libobjects:
+                        add_msg(w, '%s_object' % rtype, objschema.type)
+            if rtype not in libschema:
+                # bw compat, necessary until all translation of relation are
+                # done properly...
+                add_msg(w, '%s_object' % rtype)
+        for rdef in rschema.rdefs.values():
+            if not rdef.description or rdef.description in done:
+                continue
+            if (librschema is None or
+                (rdef.subject, rdef.object) not in librschema.rdefs or
+                librschema.rdefs[(rdef.subject, rdef.object)].description != rdef.description):
+                add_msg(w, rdef.description)
+            done.add(rdef.description)
+    for objid in _iter_vreg_objids(vreg, vregdone):
+        add_msg(w, '%s_description' % objid)
+        add_msg(w, objid)
+
+
+def _iter_vreg_objids(vreg, done):
+    for reg, objdict in vreg.items():
+        if reg in ('boxes', 'contentnavigation'):
+            continue
+        for objects in objdict.values():
+            for obj in objects:
+                objid = '%s_%s' % (reg, obj.__regid__)
+                if objid in done:
+                    break
+                pdefs = getattr(obj, 'cw_property_defs', {})
+                if pdefs:
+                    yield objid
+                    done.add(objid)
+                    break
+
+
+DEFAULT_POT_HEAD = r'''msgid ""
+msgstr ""
+"Project-Id-Version: cubicweb %s\n"
+"PO-Revision-Date: 2008-03-28 18:14+0100\n"
+"Last-Translator: Logilab Team <contact@logilab.fr>\n"
+"Language-Team: fr <contact@logilab.fr>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Generated-By: cubicweb-devtools\n"
+"Plural-Forms: nplurals=2; plural=(n > 1);\n"
+
+''' % cubicwebversion
+
+
+class UpdateCubicWebCatalogCommand(Command):
+    """Update i18n catalogs for cubicweb library.
+
+    It will regenerate cubicweb/i18n/xx.po files. You'll have then to edit those
+    files to add translations of newly added messages.
+    """
+    name = 'i18ncubicweb'
+    min_args = max_args = 0
+
+    def run(self, args):
+        """run the command with its specific arguments"""
+        import shutil
+        import tempfile
+        import yams
+        from logilab.common.fileutils import ensure_fs_mode
+        from logilab.common.shellutils import globfind, find, rm
+        from logilab.common.modutils import get_module_files
+        from cubicweb.i18n import extract_from_tal, execute2
+        tempdir = tempfile.mkdtemp(prefix='cw-')
+        cwi18ndir = WebConfiguration.i18n_lib_dir()
+        print('-> extract messages:', end=' ')
+        print('schema', end=' ')
+        schemapot = osp.join(tempdir, 'schema.pot')
+        potfiles = [schemapot]
+        potfiles.append(schemapot)
+        # explicit close necessary else the file may not be yet flushed when
+        # we'll using it below
+        schemapotstream = open(schemapot, 'w')
+        generate_schema_pot(schemapotstream.write, cubedir=None)
+        schemapotstream.close()
+        print('TAL', end=' ')
+        tali18nfile = osp.join(tempdir, 'tali18n.py')
+        extract_from_tal(find(osp.join(BASEDIR, 'web'), ('.py', '.pt')),
+                         tali18nfile)
+        print('-> generate .pot files.')
+        pyfiles = get_module_files(BASEDIR)
+        pyfiles += globfind(osp.join(BASEDIR, 'misc', 'migration'), '*.py')
+        schemafiles = globfind(osp.join(BASEDIR, 'schemas'), '*.py')
+        jsfiles = globfind(osp.join(BASEDIR, 'web'), 'cub*.js')
+        for id, files, lang in [('pycubicweb', pyfiles, None),
+                                ('schemadescr', schemafiles, None),
+                                ('yams', get_module_files(yams.__path__[0]), None),
+                                ('tal', [tali18nfile], None),
+                                ('js', jsfiles, 'java'),
+                                ]:
+            potfile = osp.join(tempdir, '%s.pot' % id)
+            cmd = ['xgettext', '--no-location', '--omit-header', '-k_']
+            if lang is not None:
+                cmd.extend(['-L', lang])
+            cmd.extend(['-o', potfile])
+            cmd.extend(files)
+            execute2(cmd)
+            if osp.exists(potfile):
+                potfiles.append(potfile)
+            else:
+                print('-> WARNING: %s file was not generated' % potfile)
+        print('-> merging %i .pot files' % len(potfiles))
+        cubicwebpot = osp.join(tempdir, 'cubicweb.pot')
+        cmd = ['msgcat', '-o', cubicwebpot] + potfiles
+        execute2(cmd)
+        print('-> merging main pot file with existing translations.')
+        chdir(cwi18ndir)
+        toedit = []
+        for lang in CubicWebNoAppConfiguration.cw_languages():
+            target = '%s.po' % lang
+            cmd = ['msgmerge', '-N', '--sort-output', '-o',
+                   target+'new', target, cubicwebpot]
+            execute2(cmd)
+            ensure_fs_mode(target)
+            shutil.move('%snew' % target, target)
+            toedit.append(osp.abspath(target))
+        # cleanup
+        rm(tempdir)
+        # instructions pour la suite
+        print('-> regenerated CubicWeb\'s .po catalogs.')
+        print('\nYou can now edit the following files:')
+        print('* ' + '\n* '.join(toedit))
+        print('when you are done, run "cubicweb-ctl i18ncube yourcube".')
+
+
+class UpdateCubeCatalogCommand(Command):
+    """Update i18n catalogs for cubes. If no cube is specified, update
+    catalogs of all registered cubes.
+    """
+    name = 'i18ncube'
+    arguments = '[<cube>...]'
+
+    def run(self, args):
+        """run the command with its specific arguments"""
+        if args:
+            cubes = [DevConfiguration.cube_dir(cube) for cube in args]
+        else:
+            cubes = [DevConfiguration.cube_dir(cube)
+                     for cube in DevConfiguration.available_cubes()]
+            cubes = [cubepath for cubepath in cubes
+                     if osp.exists(osp.join(cubepath, 'i18n'))]
+        if not update_cubes_catalogs(cubes):
+            raise ExecutionError("update cubes i18n catalog failed")
+
+
+def update_cubes_catalogs(cubes):
+    from subprocess import CalledProcessError
+    for cubedir in cubes:
+        if not osp.isdir(cubedir):
+            print('-> ignoring %s that is not a directory.' % cubedir)
+            continue
+        try:
+            toedit = update_cube_catalogs(cubedir)
+        except CalledProcessError as exc:
+            print('\n*** error while updating catalogs for cube', cubedir)
+            print('cmd:\n%s' % exc.cmd)
+            print('stdout:\n%s\nstderr:\n%s' % exc.data)
+        except Exception:
+            import traceback
+            traceback.print_exc()
+            print('*** error while updating catalogs for cube', cubedir)
+            return False
+        else:
+            # instructions pour la suite
+            if toedit:
+                print('-> regenerated .po catalogs for cube %s.' % cubedir)
+                print('\nYou can now edit the following files:')
+                print('* ' + '\n* '.join(toedit))
+                print ('When you are done, run "cubicweb-ctl i18ninstance '
+                       '<yourinstance>" to see changes in your instances.')
+            return True
+
+def update_cube_catalogs(cubedir):
+    import shutil
+    import tempfile
+    from logilab.common.fileutils import ensure_fs_mode
+    from logilab.common.shellutils import find, rm
+    from cubicweb.i18n import extract_from_tal, execute2
+    cube = osp.basename(osp.normpath(cubedir))
+    tempdir = tempfile.mkdtemp()
+    print(underline_title('Updating i18n catalogs for cube %s' % cube))
+    chdir(cubedir)
+    if osp.exists(osp.join('i18n', 'entities.pot')):
+        warn('entities.pot is deprecated, rename file to static-messages.pot (%s)'
+             % osp.join('i18n', 'entities.pot'), DeprecationWarning)
+        potfiles = [osp.join('i18n', 'entities.pot')]
+    elif osp.exists(osp.join('i18n', 'static-messages.pot')):
+        potfiles = [osp.join('i18n', 'static-messages.pot')]
+    else:
+        potfiles = []
+    print('-> extracting messages:', end=' ')
+    print('schema', end=' ')
+    schemapot = osp.join(tempdir, 'schema.pot')
+    potfiles.append(schemapot)
+    # explicit close necessary else the file may not be yet flushed when
+    # we'll using it below
+    schemapotstream = open(schemapot, 'w')
+    generate_schema_pot(schemapotstream.write, cubedir)
+    schemapotstream.close()
+    print('TAL', end=' ')
+    tali18nfile = osp.join(tempdir, 'tali18n.py')
+    ptfiles = find('.', ('.py', '.pt'), blacklist=STD_BLACKLIST+('test',))
+    extract_from_tal(ptfiles, tali18nfile)
+    print('Javascript')
+    jsfiles =  [jsfile for jsfile in find('.', '.js')
+                if osp.basename(jsfile).startswith('cub')]
+    if jsfiles:
+        tmppotfile = osp.join(tempdir, 'js.pot')
+        cmd = ['xgettext', '--no-location', '--omit-header', '-k_', '-L', 'java',
+               '--from-code=utf-8', '-o', tmppotfile] + jsfiles
+        execute2(cmd)
+        # no pot file created if there are no string to translate
+        if osp.exists(tmppotfile):
+            potfiles.append(tmppotfile)
+    print('-> creating cube-specific catalog')
+    tmppotfile = osp.join(tempdir, 'generated.pot')
+    cubefiles = find('.', '.py', blacklist=STD_BLACKLIST+('test',))
+    cubefiles.append(tali18nfile)
+    cmd = ['xgettext', '--no-location', '--omit-header', '-k_', '-o', tmppotfile]
+    cmd.extend(cubefiles)
+    execute2(cmd)
+    if osp.exists(tmppotfile): # doesn't exists of no translation string found
+        potfiles.append(tmppotfile)
+    potfile = osp.join(tempdir, 'cube.pot')
+    print('-> merging %i .pot files' % len(potfiles))
+    cmd = ['msgcat', '-o', potfile]
+    cmd.extend(potfiles)
+    execute2(cmd)
+    if not osp.exists(potfile):
+        print('no message catalog for cube', cube, 'nothing to translate')
+        # cleanup
+        rm(tempdir)
+        return ()
+    print('-> merging main pot file with existing translations:', end=' ')
+    chdir('i18n')
+    toedit = []
+    for lang in CubicWebNoAppConfiguration.cw_languages():
+        print(lang, end=' ')
+        cubepo = '%s.po' % lang
+        if not osp.exists(cubepo):
+            shutil.copy(potfile, cubepo)
+        else:
+            cmd = ['msgmerge','-N','-s','-o', cubepo+'new', cubepo, potfile]
+            execute2(cmd)
+            ensure_fs_mode(cubepo)
+            shutil.move('%snew' % cubepo, cubepo)
+        toedit.append(osp.abspath(cubepo))
+    print()
+    # cleanup
+    rm(tempdir)
+    return toedit
+
+
+# XXX totally broken, fix it
+# class LiveServerCommand(Command):
+#     """Run a server from within a cube directory.
+#     """
+#     name = 'live-server'
+#     arguments = ''
+#     options = ()
+
+#     def run(self, args):
+#         """run the command with its specific arguments"""
+#         from cubicweb.devtools.livetest import runserver
+#         runserver()
+
+
+class NewCubeCommand(Command):
+    """Create a new cube.
+
+    <cubename>
+      the name of the new cube. It should be a valid python module name.
+    """
+    name = 'newcube'
+    arguments = '<cubename>'
+    min_args = max_args = 1
+    options = (
+        ("layout",
+         {'short': 'L', 'type' : 'choice', 'metavar': '<cube layout>',
+          'default': 'simple', 'choices': ('simple', 'full'),
+          'help': 'cube layout. You\'ll get a minimal cube with the "simple" \
+layout, and a full featured cube with "full" layout.',
+          }
+         ),
+        ("directory",
+         {'short': 'd', 'type' : 'string', 'metavar': '<cubes directory>',
+          'help': 'directory where the new cube should be created',
+          }
+         ),
+        ("verbose",
+         {'short': 'v', 'type' : 'yn', 'metavar': '<verbose>',
+          'default': 'n',
+          'help': 'verbose mode: will ask all possible configuration questions',
+          }
+         ),
+        ("author",
+         {'short': 'a', 'type' : 'string', 'metavar': '<author>',
+          'default': 'LOGILAB S.A. (Paris, FRANCE)',
+          'help': 'cube author',
+          }
+         ),
+        ("author-email",
+         {'short': 'e', 'type' : 'string', 'metavar': '<email>',
+          'default': 'contact@logilab.fr',
+          'help': 'cube author\'s email',
+          }
+         ),
+        ("author-web-site",
+         {'short': 'w', 'type' : 'string', 'metavar': '<web site>',
+          'default': 'http://www.logilab.fr',
+          'help': 'cube author\'s web site',
+          }
+         ),
+        ("license",
+         {'short': 'l', 'type' : 'choice', 'metavar': '<license>',
+          'default': 'LGPL', 'choices': ('GPL', 'LGPL', ''),
+          'help': 'cube license',
+          }
+         ),
+        )
+
+    LICENSES = {
+        'LGPL': '''\
+# This program is free software: you can redistribute it and/or modify it under
+# the terms of the GNU Lesser General Public License as published by the Free
+# Software Foundation, either version 2.1 of the License, or (at your option)
+# any later version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
+# details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+''',
+
+        'GPL': '''\
+# This program is free software: you can redistribute it and/or modify it under
+# the terms of the GNU General Public License as published by the Free Software
+# Foundation, either version 2.1 of the License, or (at your option) any later
+# version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+# details.
+#
+# You should have received a copy of the GNU General Public License along with
+# this program. If not, see <http://www.gnu.org/licenses/>.
+''',
+        '': '# INSERT LICENSE HERE'
+        }
+
+    def run(self, args):
+        import re
+        from logilab.common.shellutils import ASK
+        cubename = args[0]
+        if not re.match('[_A-Za-z][_A-Za-z0-9]*$', cubename):
+            raise BadCommandUsage(
+                'cube name must be a valid python module name')
+        verbose = self.get('verbose')
+        cubesdir = self.get('directory')
+        if not cubesdir:
+            cubespath = ServerConfiguration.cubes_search_path()
+            if len(cubespath) > 1:
+                raise BadCommandUsage(
+                    "can't guess directory where to put the new cube."
+                    " Please specify it using the --directory option")
+            cubesdir = cubespath[0]
+        if not osp.isdir(cubesdir):
+            print("-> creating cubes directory", cubesdir)
+            try:
+                mkdir(cubesdir)
+            except OSError as err:
+                self.fail("failed to create directory %r\n(%s)"
+                          % (cubesdir, err))
+        cubedir = osp.join(cubesdir, cubename)
+        if osp.exists(cubedir):
+            self.fail("%s already exists!" % cubedir)
+        skeldir = osp.join(BASEDIR, 'skeleton')
+        default_name = 'cubicweb-%s' % cubename.lower().replace('_', '-')
+        if verbose:
+            distname = input('Debian name for your cube ? [%s]): '
+                                 % default_name).strip()
+            if not distname:
+                distname = default_name
+            elif not distname.startswith('cubicweb-'):
+                if ASK.confirm('Do you mean cubicweb-%s ?' % distname):
+                    distname = 'cubicweb-' + distname
+        else:
+            distname = default_name
+        if not re.match('[a-z][-a-z0-9]*$', distname):
+            raise BadCommandUsage(
+                'cube distname should be a valid debian package name')
+        longdesc = shortdesc = input(
+            'Enter a short description for your cube: ')
+        if verbose:
+            longdesc = input(
+                'Enter a long description (leave empty to reuse the short one): ')
+        dependencies = {'cubicweb': '>= %s' % cubicwebversion,
+                        'six': '>= 1.4.0',}
+        if verbose:
+            dependencies.update(self._ask_for_dependencies())
+        context = {'cubename' : cubename,
+                   'distname' : distname,
+                   'shortdesc' : shortdesc,
+                   'longdesc' : longdesc or shortdesc,
+                   'dependencies' : dependencies,
+                   'version'  : cubicwebversion,
+                   'year'  : str(date.today().year),
+                   'author': self['author'],
+                   'author-email': self['author-email'],
+                   'author-web-site': self['author-web-site'],
+                   'license': self['license'],
+                   'long-license': self.LICENSES[self['license']],
+                   }
+        exclude = SKEL_EXCLUDE
+        if self['layout'] == 'simple':
+            exclude += ('sobjects.py*', 'precreate.py*', 'realdb_test*',
+                        'cubes.*', 'uiprops.py*')
+        copy_skeleton(skeldir, cubedir, context, exclude=exclude)
+
+    def _ask_for_dependencies(self):
+        from logilab.common.shellutils import ASK
+        from logilab.common.textutils import splitstrip
+        depcubes = []
+        for cube in ServerConfiguration.available_cubes():
+            answer = ASK.ask("Depends on cube %s? " % cube,
+                             ('N','y','skip','type'), 'N')
+            if answer == 'y':
+                depcubes.append(cube)
+            if answer == 'type':
+                depcubes = splitstrip(input('type dependencies: '))
+                break
+            elif answer == 'skip':
+                break
+        return dict(('cubicweb-' + cube, ServerConfiguration.cube_version(cube))
+                    for cube in depcubes)
+
+
+class ExamineLogCommand(Command):
+    """Examine a rql log file.
+
+    Will print out the following table
+
+      Percentage; Cumulative Time (clock); Cumulative Time (CPU); Occurences; Query
+
+    sorted by descending cumulative time (clock). Time are expressed in seconds.
+
+    Chances are the lines at the top are the ones that will bring the higher
+    benefit after optimisation. Start there.
+    """
+    arguments = 'rql.log'
+    name = 'exlog'
+    options = ()
+
+    def run(self, args):
+        import re
+        requests = {}
+        for filepath in args:
+            try:
+                stream = open(filepath)
+            except OSError as ex:
+                raise BadCommandUsage("can't open rql log file %s: %s"
+                                      % (filepath, ex))
+            for lineno, line in enumerate(stream):
+                if not ' WHERE ' in line:
+                    continue
+                try:
+                    rql, time = line.split('--')
+                    rql = re.sub("(\'\w+': \d*)", '', rql)
+                    if '{' in rql:
+                        rql = rql[:rql.index('{')]
+                    req = requests.setdefault(rql, [])
+                    time.strip()
+                    chunks = time.split()
+                    clocktime = float(chunks[0][1:])
+                    cputime = float(chunks[-3])
+                    req.append( (clocktime, cputime) )
+                except Exception as exc:
+                    sys.stderr.write('Line %s: %s (%s)\n' % (lineno, exc, line))
+        stat = []
+        for rql, times in requests.items():
+            stat.append( (sum(time[0] for time in times),
+                          sum(time[1] for time in times),
+                          len(times), rql) )
+        stat.sort()
+        stat.reverse()
+        total_time = sum(clocktime for clocktime, cputime, occ, rql in stat) * 0.01
+        print('Percentage;Cumulative Time (clock);Cumulative Time (CPU);Occurences;Query')
+        for clocktime, cputime, occ, rql in stat:
+            print('%.2f;%.2f;%.2f;%s;%s' % (clocktime/total_time, clocktime,
+                                            cputime, occ, rql))
+
+
+class GenerateSchema(Command):
+    """Generate schema image for the given cube"""
+    name = "schema"
+    arguments = '<cube>'
+    min_args = max_args = 1
+    options = [
+        ('output-file',
+         {'type':'string', 'default': None,
+          'metavar': '<file>', 'short':'o', 'help':'output image file',
+          'input':False,
+          }),
+        ('viewer',
+         {'type': 'string', 'default':None,
+          'short': "d", 'metavar':'<cmd>',
+          'help':'command use to view the generated file (empty for none)',
+          }),
+        ('show-meta',
+         {'action': 'store_true', 'default':False,
+          'short': "m", 'metavar': "<yN>",
+          'help':'include meta and internal entities in schema',
+          }),
+        ('show-workflow',
+         {'action': 'store_true', 'default':False,
+          'short': "w", 'metavar': "<yN>",
+          'help':'include workflow entities in schema',
+          }),
+        ('show-cw-user',
+         {'action': 'store_true', 'default':False,
+          'metavar': "<yN>",
+          'help':'include cubicweb user entities in schema',
+          }),
+        ('exclude-type',
+         {'type':'string', 'default':'',
+          'short': "x", 'metavar': "<types>",
+          'help':'coma separated list of entity types to remove from view',
+          }),
+        ('include-type',
+         {'type':'string', 'default':'',
+          'short': "i", 'metavar': "<types>",
+          'help':'coma separated list of entity types to include in view',
+          }),
+        ('show-etype',
+         {'type':'string', 'default':'',
+          'metavar': '<etype>',
+          'help':'show graph of this etype and its neighbours'
+          }),
+        ]
+
+    def run(self, args):
+        from subprocess import Popen
+        from tempfile import NamedTemporaryFile
+        from logilab.common.textutils import splitstrip
+        from logilab.common.graph import GraphGenerator, DotBackend
+        from yams import schema2dot as s2d, BASE_TYPES
+        from cubicweb.schema import (META_RTYPES, SCHEMA_TYPES, SYSTEM_RTYPES,
+                                     WORKFLOW_TYPES, INTERNAL_TYPES)
+        cubes = splitstrip(args[0])
+        dev_conf = DevConfiguration(*cubes)
+        schema = dev_conf.load_schema()
+        out, viewer = self['output-file'], self['viewer']
+        if out is None:
+            tmp_file = NamedTemporaryFile(suffix=".svg")
+            out = tmp_file.name
+        skiptypes = BASE_TYPES | SCHEMA_TYPES
+        if not self['show-meta']:
+            skiptypes |=  META_RTYPES | SYSTEM_RTYPES | INTERNAL_TYPES
+        if not self['show-workflow']:
+            skiptypes |= WORKFLOW_TYPES
+        if not self['show-cw-user']:
+            skiptypes |= set(('CWUser', 'CWGroup', 'EmailAddress'))
+        skiptypes |= set(self['exclude-type'].split(','))
+        skiptypes -= set(self['include-type'].split(','))
+
+        if not self['show-etype']:
+            s2d.schema2dot(schema, out, skiptypes=skiptypes)
+        else:
+            etype = self['show-etype']
+            visitor = s2d.OneHopESchemaVisitor(schema[etype], skiptypes=skiptypes)
+            propshdlr = s2d.SchemaDotPropsHandler(visitor)
+            backend = DotBackend('schema', 'BT',
+                                 ratio='compress',size=None,
+                                 renderer='dot',
+                                 additionnal_param={'overlap' : 'false',
+                                                    'splines' : 'true',
+                                                    'sep' : '0.2'})
+            generator = s2d.GraphGenerator(backend)
+            generator.generate(visitor, propshdlr, out)
+
+        if viewer:
+            p = Popen((viewer, out))
+            p.wait()
+
+
+for cmdcls in (UpdateCubicWebCatalogCommand,
+               UpdateCubeCatalogCommand,
+               #LiveServerCommand,
+               NewCubeCommand,
+               ExamineLogCommand,
+               GenerateSchema,
+               ):
+    CWCTL.register(cmdcls)