--- /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)