hercule.py
changeset 0 b97547f5f1fa
child 1132 96752791c2b6
equal deleted inserted replaced
-1:000000000000 0:b97547f5f1fa
       
     1 """RQL client for cubicweb, connecting to application using pyro
       
     2 
       
     3 :organization: Logilab
       
     4 :copyright: 2001-2008 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
       
     5 :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
       
     6 """
       
     7 __docformat__ = "restructuredtext en"
       
     8 
       
     9 import os
       
    10 import sys
       
    11 
       
    12 from logilab.common import flatten
       
    13 from logilab.common.cli import CLIHelper
       
    14 from logilab.common.clcommands import BadCommandUsage, pop_arg
       
    15 from cubicweb.toolsutils import CONNECT_OPTIONS, Command, register_commands
       
    16  
       
    17 # result formatter ############################################################
       
    18 
       
    19 PAGER = os.environ.get('PAGER', 'less')
       
    20             
       
    21 def pager_format_results(writer, layout):
       
    22     """pipe results to a pager like more or less"""
       
    23     (r, w) = os.pipe()
       
    24     pid = os.fork()
       
    25     if pid == 0:
       
    26         os.dup2(r, 0)
       
    27         os.close(r)
       
    28         os.close(w)
       
    29         if PAGER == 'less':
       
    30             os.execlp(PAGER, PAGER, '-r')
       
    31         else:
       
    32             os.execlp(PAGER, PAGER)
       
    33         sys.exit(0)
       
    34     stream = os.fdopen(w, "w")
       
    35     os.close(r)
       
    36     try:
       
    37         format_results(writer, layout, stream)
       
    38     finally:
       
    39         stream.close()
       
    40         status = os.waitpid(pid, 0)
       
    41 
       
    42 def izip2(list1, list2):
       
    43     for i in xrange(len(list1)):
       
    44         yield list1[i] + tuple(list2[i])
       
    45         
       
    46 def format_results(writer, layout, stream=sys.stdout): 
       
    47     """format result as text into the given file like object"""
       
    48     writer.format(layout, stream)
       
    49 
       
    50 
       
    51 try:
       
    52     encoding = sys.stdout.encoding
       
    53 except AttributeError: # python < 2.3
       
    54     encoding = 'UTF-8'
       
    55 
       
    56 def to_string(value, encoding=encoding):
       
    57     """used to converte arbitrary values to encoded string"""
       
    58     if isinstance(value, unicode):
       
    59         return value.encode(encoding, 'replace')
       
    60     return str(value)
       
    61 
       
    62 # command line querier ########################################################
       
    63     
       
    64 class RQLCli(CLIHelper):
       
    65     """Interactive command line client for CubicWeb, allowing user to execute
       
    66     arbitrary RQL queries and to fetch schema information
       
    67     """
       
    68     # commands are prefixed by ":"
       
    69     CMD_PREFIX = ':'
       
    70     # map commands to folders
       
    71     CLIHelper.CMD_MAP.update({
       
    72         'connect' :      "CubicWeb",
       
    73         'schema'  :      "CubicWeb",
       
    74         'description'  : "CubicWeb",
       
    75         'commit' :       "CubicWeb",
       
    76         'rollback' :     "CubicWeb",
       
    77         'autocommit'  :  "Others", 
       
    78         'debug' :        "Others",
       
    79         })
       
    80     
       
    81     def __init__(self, application=None, user=None, password=None,
       
    82                  host=None, debug=0):
       
    83         CLIHelper.__init__(self, os.path.join(os.environ["HOME"], ".erqlhist"))
       
    84         self.cnx = None
       
    85         self.cursor = None
       
    86         # XXX give a Request like object, not None
       
    87         from cubicweb.schemaviewer import SchemaViewer
       
    88         self.schema_viewer = SchemaViewer(None, encoding=encoding)
       
    89         from logilab.common.ureports import TextWriter
       
    90         self.writer = TextWriter()
       
    91         self.autocommit = False
       
    92         self._last_result = None
       
    93         self._previous_lines = []
       
    94         if application is not None:
       
    95             self.do_connect(application, user, password, host)
       
    96         self.do_debug(debug)
       
    97         
       
    98     def do_connect(self, application, user=None, password=None, host=None):
       
    99         """connect to an cubicweb application"""
       
   100         from cubicweb.dbapi import connect
       
   101         if user is None:
       
   102             user = raw_input('login: ')
       
   103         if password is None:
       
   104             from getpass import getpass
       
   105             password = getpass('password: ')
       
   106         if self.cnx is not None:
       
   107             self.cnx.close()
       
   108         self.cnx = connect(user=user, password=password, host=host,
       
   109                            database=application)
       
   110         self.schema = self.cnx.get_schema()
       
   111         self.cursor = self.cnx.cursor()
       
   112         # add entities types to the completion commands
       
   113         self._completer.list = (self.commands.keys() +
       
   114                                 self.schema.entities() + ['Any'])
       
   115         print _('You are now connected to %s') % application
       
   116         
       
   117 
       
   118     help_do_connect = ('connect', "connect <application> [<user> [<password> [<host>]]]",
       
   119                        _(do_connect.__doc__))
       
   120 
       
   121     def do_debug(self, debug=1):
       
   122         """set debug level"""
       
   123         self._debug = debug
       
   124         if debug:
       
   125             self._format = format_results
       
   126         else:
       
   127             self._format = pager_format_results
       
   128         if self._debug:
       
   129             print _('Debug level set to %s'%debug)
       
   130         
       
   131     help_do_debug = ('debug', "debug [debug_level]", _(do_debug.__doc__))
       
   132     
       
   133     def do_description(self):
       
   134         """display the description of the latest result"""
       
   135         if self.cursor.description is None:
       
   136             print _('No query has been executed')
       
   137         else:
       
   138             print '\n'.join([', '.join(line_desc)
       
   139                              for line_desc in self.cursor.description])
       
   140 
       
   141     help_do_description = ('description', "description", _(do_description.__doc__))
       
   142     
       
   143     def do_schema(self, name=None):
       
   144         """display information about the application schema """
       
   145         if self.cnx is None:
       
   146             print _('You are not connected to an application !')
       
   147             return
       
   148         done = None
       
   149         if name is None:
       
   150             # display the full schema
       
   151             self.display_schema(self.schema)
       
   152             done = 1
       
   153         else:
       
   154             if self.schema.has_entity(name):
       
   155                 self.display_schema(self.schema.eschema(name))
       
   156                 done = 1
       
   157             if self.schema.has_relation(name):
       
   158                 self.display_schema(self.schema.rschema(name))
       
   159                 done = 1
       
   160         if done is None:
       
   161             print _('Unable to find anything named "%s" in the schema !') % name
       
   162             
       
   163     help_do_schema = ('schema', "schema [keyword]", _(do_schema.__doc__))
       
   164 
       
   165     
       
   166     def do_commit(self):
       
   167         """commit the current transaction"""
       
   168         self.cnx.commit()
       
   169 
       
   170     help_do_commit = ('commit', "commit", _(do_commit.__doc__))
       
   171     
       
   172     def do_rollback(self):
       
   173         """rollback the current transaction"""
       
   174         self.cnx.rollback()
       
   175 
       
   176     help_do_rollback = ('rollback', "rollback", _(do_rollback.__doc__))
       
   177     
       
   178     def do_autocommit(self):
       
   179         """toggle autocommit mode"""
       
   180         self.autocommit = not self.autocommit
       
   181 
       
   182     help_do_autocommit = ('autocommit', "autocommit", _(do_autocommit.__doc__))
       
   183     
       
   184 
       
   185     def handle_line(self, stripped_line):
       
   186         """handle non command line :
       
   187         if the query is complete, executes it and displays results (if any)
       
   188         else, stores the query line and waits for the suite
       
   189         """
       
   190         if self.cnx is None:
       
   191             print _('You are not connected to an application !')
       
   192             return
       
   193         # append line to buffer
       
   194         self._previous_lines.append(stripped_line)
       
   195         # query are ended by a ';'
       
   196         if stripped_line[-1] != ';':
       
   197             return
       
   198         # extract query from the buffer and flush it
       
   199         query = '\n'.join(self._previous_lines)
       
   200         self._previous_lines = []
       
   201         # search results
       
   202         try:
       
   203             self.cursor.execute(query)
       
   204         except:
       
   205             if self.autocommit:
       
   206                 self.cnx.rollback()
       
   207             raise
       
   208         else:
       
   209             if self.autocommit:
       
   210                 self.cnx.commit()
       
   211         self.handle_result(self.cursor.fetchall(), self.cursor.description)
       
   212 
       
   213     def handle_result(self, result, description):
       
   214         """display query results if any"""
       
   215         if not result:
       
   216             print _('No result matching query')
       
   217         else:
       
   218             from logilab.common.ureports import Table
       
   219             children = flatten(izip2(description, result), to_string)
       
   220             layout = Table(cols=2*len(result[0]), children=children, cheaders=1)
       
   221             self._format(self.writer, layout)
       
   222             print _('%s results matching query') % len(result)
       
   223 
       
   224     def display_schema(self, schema):
       
   225         """display a schema object"""
       
   226         attr = schema.__class__.__name__.lower().replace('cubicweb', '')
       
   227         layout = getattr(self.schema_viewer, 'visit_%s' % attr)(schema)
       
   228         self._format(self.writer, layout)
       
   229 
       
   230 
       
   231 class CubicWebClientCommand(Command):
       
   232     """A command line querier for CubicWeb, using the Relation Query Language.
       
   233 
       
   234     <application>
       
   235       identifier of the application to connect to 
       
   236     """
       
   237     name = 'client'
       
   238     arguments = '<application>'
       
   239     options = CONNECT_OPTIONS + (
       
   240         ("verbose",
       
   241          {'short': 'v', 'type' : 'int', 'metavar': '<level>',
       
   242           'default': 0,
       
   243           'help': 'ask confirmation to continue after an error.',
       
   244           }),
       
   245         ("batch",
       
   246          {'short': 'b', 'type' : 'string', 'metavar': '<file>',
       
   247           'help': 'file containing a batch of RQL statements to execute.',
       
   248           }),
       
   249         )
       
   250     
       
   251     def run(self, args):
       
   252         """run the command with its specific arguments"""
       
   253         appid = pop_arg(args, expected_size_after=None)
       
   254         batch_stream = None
       
   255         if args:
       
   256             if len(args) == 1 and args[0] == '-':
       
   257                 batch_stream = sys.stdin
       
   258             else:
       
   259                 raise BadCommandUsage('too many arguments')
       
   260         if self.config.batch:
       
   261             batch_stream = open(self.config.batch)
       
   262         cli = RQLCli(appid, self.config.user, self.config.password,
       
   263                      self.config.host, self.config.debug)
       
   264         if batch_stream:
       
   265             cli.autocommit = True
       
   266             for line in batch_stream:
       
   267                 line = line.strip()
       
   268                 if not line:
       
   269                     continue
       
   270                 print '>>>', line
       
   271                 cli.handle_line(line)
       
   272         else:
       
   273             cli.run()
       
   274         
       
   275 register_commands((CubicWebClientCommand,))