hercule.py
changeset 0 b97547f5f1fa
child 1132 96752791c2b6
--- /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,))