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