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