Add a ``schema`` command to cmd ctrl to generate schema image.
This changeset add the new commande and do some refactoring in cwconfig and
schema.py to allow the use of CubicWebNoAppConfiguration with
CubicWebSchemaLoader.
"""RQL client for cubicweb, connecting to instance using pyro
:organization: Logilab
:copyright: 2001-2010 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2.
:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
:license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses
"""
__docformat__ = "restructuredtext en"
import os
import sys
from logilab.common import flatten
from logilab.common.cli import CLIHelper
from logilab.common.clcommands import BadCommandUsage, pop_arg, register_commands
from cubicweb.toolsutils import CONNECT_OPTIONS, Command
# result formatter ############################################################
PAGER = os.environ.get('PAGER', 'less')
def pager_format_results(writer, layout):
"""pipe results to a pager like more or less"""
(r, w) = os.pipe()
pid = os.fork()
if pid == 0:
os.dup2(r, 0)
os.close(r)
os.close(w)
if PAGER == 'less':
os.execlp(PAGER, PAGER, '-r')
else:
os.execlp(PAGER, PAGER)
sys.exit(0)
stream = os.fdopen(w, "w")
os.close(r)
try:
format_results(writer, layout, stream)
finally:
stream.close()
os.waitpid(pid, 0)
def izip2(list1, list2):
for i in xrange(len(list1)):
yield list1[i] + tuple(list2[i])
def format_results(writer, layout, stream=sys.stdout):
"""format result as text into the given file like object"""
writer.format(layout, stream)
try:
encoding = sys.stdout.encoding
except AttributeError: # python < 2.3
encoding = 'UTF-8'
def to_string(value, encoding=encoding):
"""used to converte arbitrary values to encoded string"""
if isinstance(value, unicode):
return value.encode(encoding, 'replace')
return str(value)
# command line querier ########################################################
class RQLCli(CLIHelper):
"""Interactive command line client for CubicWeb, allowing user to execute
arbitrary RQL queries and to fetch schema information
"""
# commands are prefixed by ":"
CMD_PREFIX = ':'
# map commands to folders
CLIHelper.CMD_MAP.update({
'connect' : "CubicWeb",
'schema' : "CubicWeb",
'description' : "CubicWeb",
'commit' : "CubicWeb",
'rollback' : "CubicWeb",
'autocommit' : "Others",
'debug' : "Others",
})
def __init__(self, instance=None, user=None, password=None,
host=None, debug=0):
CLIHelper.__init__(self, os.path.join(os.environ["HOME"], ".erqlhist"))
self.cnx = None
self.cursor = None
# XXX give a Request like object, not None
from cubicweb.schemaviewer import SchemaViewer
self.schema_viewer = SchemaViewer(None, encoding=encoding)
from logilab.common.ureports import TextWriter
self.writer = TextWriter()
self.autocommit = False
self._last_result = None
self._previous_lines = []
if instance is not None:
self.do_connect(instance, user, password, host)
self.do_debug(debug)
def do_connect(self, instance, user=None, password=None, host=None):
"""connect to an cubicweb instance"""
from cubicweb.dbapi import connect
if user is None:
user = raw_input('login: ')
if password is None:
from getpass import getpass
password = getpass('password: ')
if self.cnx is not None:
self.cnx.close()
self.cnx = connect(login=user, password=password, host=host,
database=instance)
self.schema = self.cnx.get_schema()
self.cursor = self.cnx.cursor()
# add entities types to the completion commands
self._completer.list = (self.commands.keys() +
self.schema.entities() + ['Any'])
print _('You are now connected to %s') % instance
help_do_connect = ('connect', "connect <instance> [<user> [<password> [<host>]]]",
_(do_connect.__doc__))
def do_debug(self, debug=1):
"""set debug level"""
self._debug = debug
if debug:
self._format = format_results
else:
self._format = pager_format_results
if self._debug:
print _('Debug level set to %s'%debug)
help_do_debug = ('debug', "debug [debug_level]", _(do_debug.__doc__))
def do_description(self):
"""display the description of the latest result"""
if self.rset.description is None:
print _('No query has been executed')
else:
print '\n'.join([', '.join(line_desc)
for line_desc in self.rset.description])
help_do_description = ('description', "description", _(do_description.__doc__))
def do_schema(self, name=None):
"""display information about the instance schema """
if self.cnx is None:
print _('You are not connected to an instance !')
return
done = None
if name is None:
# display the full schema
self.display_schema(self.schema)
done = 1
else:
if self.schema.has_entity(name):
self.display_schema(self.schema.eschema(name))
done = 1
if self.schema.has_relation(name):
self.display_schema(self.schema.rschema(name))
done = 1
if done is None:
print _('Unable to find anything named "%s" in the schema !') % name
help_do_schema = ('schema', "schema [keyword]", _(do_schema.__doc__))
def do_commit(self):
"""commit the current transaction"""
self.cnx.commit()
help_do_commit = ('commit', "commit", _(do_commit.__doc__))
def do_rollback(self):
"""rollback the current transaction"""
self.cnx.rollback()
help_do_rollback = ('rollback', "rollback", _(do_rollback.__doc__))
def do_autocommit(self):
"""toggle autocommit mode"""
self.autocommit = not self.autocommit
help_do_autocommit = ('autocommit', "autocommit", _(do_autocommit.__doc__))
def handle_line(self, stripped_line):
"""handle non command line :
if the query is complete, executes it and displays results (if any)
else, stores the query line and waits for the suite
"""
if self.cnx is None:
print _('You are not connected to an instance !')
return
# append line to buffer
self._previous_lines.append(stripped_line)
# query are ended by a ';'
if stripped_line[-1] != ';':
return
# extract query from the buffer and flush it
query = '\n'.join(self._previous_lines)
self._previous_lines = []
# search results
try:
self.rset = rset = self.cursor.execute(query)
except:
if self.autocommit:
self.cnx.rollback()
raise
else:
if self.autocommit:
self.cnx.commit()
self.handle_result(rset)
def handle_result(self, rset):
"""display query results if any"""
if not rset:
print _('No result matching query')
else:
from logilab.common.ureports import Table
children = flatten(izip2(rset.description, rset.rows), to_string)
layout = Table(cols=2*len(rset.rows[0]), children=children, cheaders=1)
self._format(self.writer, layout)
print _('%s results matching query') % rset.rowcount
def display_schema(self, schema):
"""display a schema object"""
attr = schema.__class__.__name__.lower().replace('cubicweb', '')
layout = getattr(self.schema_viewer, 'visit_%s' % attr)(schema)
self._format(self.writer, layout)
class CubicWebClientCommand(Command):
"""A command line querier for CubicWeb, using the Relation Query Language.
<instance>
identifier of the instance to connect to
"""
name = 'client'
arguments = '<instance>'
options = CONNECT_OPTIONS + (
("verbose",
{'short': 'v', 'type' : 'int', 'metavar': '<level>',
'default': 0,
'help': 'ask confirmation to continue after an error.',
}),
("batch",
{'short': 'b', 'type' : 'string', 'metavar': '<file>',
'help': 'file containing a batch of RQL statements to execute.',
}),
)
def run(self, args):
"""run the command with its specific arguments"""
appid = pop_arg(args, expected_size_after=None)
batch_stream = None
if args:
if len(args) == 1 and args[0] == '-':
batch_stream = sys.stdin
else:
raise BadCommandUsage('too many arguments')
if self.config.batch:
batch_stream = open(self.config.batch)
cli = RQLCli(appid, self.config.user, self.config.password,
self.config.host, self.config.debug)
if batch_stream:
cli.autocommit = True
for line in batch_stream:
line = line.strip()
if not line:
continue
print '>>>', line
cli.handle_line(line)
else:
cli.run()
register_commands((CubicWebClientCommand,))