--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/hercule.py Wed Nov 05 15:52:50 2008 +0100
@@ -0,0 +1,275 @@
+"""RQL client for cubicweb, connecting to application using pyro
+
+:organization: Logilab
+:copyright: 2001-2008 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
+"""
+__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
+from cubicweb.toolsutils import CONNECT_OPTIONS, Command, register_commands
+
+# 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()
+ status = 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, application=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 application is not None:
+ self.do_connect(application, user, password, host)
+ self.do_debug(debug)
+
+ def do_connect(self, application, user=None, password=None, host=None):
+ """connect to an cubicweb application"""
+ 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(user=user, password=password, host=host,
+ database=application)
+ 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') % application
+
+
+ help_do_connect = ('connect', "connect <application> [<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.cursor.description is None:
+ print _('No query has been executed')
+ else:
+ print '\n'.join([', '.join(line_desc)
+ for line_desc in self.cursor.description])
+
+ help_do_description = ('description', "description", _(do_description.__doc__))
+
+ def do_schema(self, name=None):
+ """display information about the application schema """
+ if self.cnx is None:
+ print _('You are not connected to an application !')
+ 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 application !')
+ 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.cursor.execute(query)
+ except:
+ if self.autocommit:
+ self.cnx.rollback()
+ raise
+ else:
+ if self.autocommit:
+ self.cnx.commit()
+ self.handle_result(self.cursor.fetchall(), self.cursor.description)
+
+ def handle_result(self, result, description):
+ """display query results if any"""
+ if not result:
+ print _('No result matching query')
+ else:
+ from logilab.common.ureports import Table
+ children = flatten(izip2(description, result), to_string)
+ layout = Table(cols=2*len(result[0]), children=children, cheaders=1)
+ self._format(self.writer, layout)
+ print _('%s results matching query') % len(result)
+
+ 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.
+
+ <application>
+ identifier of the application to connect to
+ """
+ name = 'client'
+ arguments = '<application>'
+ 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,))