command line client:
[pithos] / tools / store
1 #!/usr/bin/env python
2
3 # Copyright 2011 GRNET S.A. All rights reserved.
4
5 # Redistribution and use in source and binary forms, with or
6 # without modification, are permitted provided that the following
7 # conditions are met:
8
9 #   1. Redistributions of source code must retain the above
10 #      copyright notice, this list of conditions and the following
11 #      disclaimer.
12
13 #   2. Redistributions in binary form must reproduce the above
14 #      copyright notice, this list of conditions and the following
15 #      disclaimer in the documentation and/or other materials
16 #      provided with the distribution.
17
18 # THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
19 # OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
20 # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
21 # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
22 # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
25 # USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
26 # AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
27 # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
28 # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
29 # POSSIBILITY OF SUCH DAMAGE.
30
31 # The views and conclusions contained in the software and
32 # documentation are those of the authors and should not be
33 # interpreted as representing official policies, either expressed
34 # or implied, of GRNET S.A.
35
36 from getpass import getuser
37 from optparse import OptionParser
38 from os import environ
39 from sys import argv, exit, stdin, stdout
40 from datetime import datetime
41 from lib.client import Pithos_Client, Fault
42 from lib.util import get_user, get_auth, get_server, get_api
43
44 import json
45 import logging
46 import types
47 import re
48 import time as _time
49 import os
50
51 _cli_commands = {}
52
53 def cli_command(*args):
54     def decorator(cls):
55         cls.commands = args
56         for name in args:
57             _cli_commands[name] = cls
58         return cls
59     return decorator
60
61 def class_for_cli_command(name):
62     return _cli_commands[name]
63
64 class Command(object):
65     syntax = ''
66     
67     def __init__(self, name, argv):
68         parser = OptionParser('%%prog %s [options] %s' % (name, self.syntax))
69         parser.add_option('--host', dest='host', metavar='HOST',
70                           default=get_server(), help='use server HOST')
71         parser.add_option('--user', dest='user', metavar='USERNAME',
72                           default=get_user(),
73                           help='use account USERNAME')
74         parser.add_option('--token', dest='token', metavar='AUTH',
75                           default=get_auth(),
76                           help='use account AUTH')
77         parser.add_option('--api', dest='api', metavar='API',
78                           default=get_api(), help='use api API')
79         parser.add_option('-v', action='store_true', dest='verbose',
80                           default=False, help='use verbose output')
81         parser.add_option('-d', action='store_true', dest='debug',
82                           default=False, help='use debug output')
83         self.add_options(parser)
84         options, args = parser.parse_args(argv)
85         
86         # Add options to self
87         for opt in parser.option_list:
88             key = opt.dest
89             if key:
90                 val = getattr(options, key)
91                 setattr(self, key, val)
92         
93         self.client = Pithos_Client(self.host, self.token, self.user, self.api, self.verbose,
94                              self.debug)
95         
96         self.parser = parser
97         self.args = args
98     
99     def _build_args(self, attrs):
100         args = {}
101         for a in [a for a in attrs if getattr(self, a)]:
102             args[a] = getattr(self, a)
103         return args
104
105     def add_options(self, parser):
106         pass
107     
108     def execute(self, *args):
109         pass
110
111 @cli_command('list', 'ls')
112 class List(Command):
113     syntax = '[<container>[/<object>]]'
114     description = 'list containers or objects'
115     
116     def add_options(self, parser):
117         parser.add_option('-l', action='store_true', dest='detail',
118                           default=False, help='show detailed output')
119         parser.add_option('-n', action='store', type='int', dest='limit',
120                           default=10000, help='show limited output')
121         parser.add_option('--marker', action='store', type='str',
122                           dest='marker', default=None,
123                           help='show output greater then marker')
124         parser.add_option('--prefix', action='store', type='str',
125                           dest='prefix', default=None,
126                           help='show output starting with prefix')
127         parser.add_option('--delimiter', action='store', type='str',
128                           dest='delimiter', default=None,
129                           help='show output up to the delimiter')
130         parser.add_option('--path', action='store', type='str',
131                           dest='path', default=None,
132                           help='show output starting with prefix up to /')
133         parser.add_option('--meta', action='store', type='str',
134                           dest='meta', default=None,
135                           help='show output having the specified meta keys')
136         parser.add_option('--if-modified-since', action='store', type='str',
137                           dest='if_modified_since', default=None,
138                           help='show output if modified since then')
139         parser.add_option('--if-unmodified-since', action='store', type='str',
140                           dest='if_unmodified_since', default=None,
141                           help='show output if not modified since then')
142         parser.add_option('--until', action='store', dest='until',
143                           default=None, help='show metadata until that date')
144         parser.add_option('--format', action='store', dest='format',
145                           default='%d/%m/%Y', help='format to parse until date')
146     
147     def execute(self, container=None):
148         if container:
149             self.list_objects(container)
150         else:
151             self.list_containers()
152     
153     def list_containers(self):
154         attrs = ['limit', 'marker', 'if_modified_since',
155                  'if_unmodified_since']
156         args = self._build_args(attrs)
157         args['format'] = 'json' if self.detail else 'text'
158         
159         if getattr(self, 'until'):
160             t = _time.strptime(self.until, self.format)
161             args['until'] = int(_time.mktime(t))
162         
163         l = self.client.list_containers(**args)
164         print_list(l)
165     
166     def list_objects(self, container):
167         #prepate params
168         params = {}
169         attrs = ['limit', 'marker', 'prefix', 'delimiter', 'path',
170                  'meta', 'if_modified_since', 'if_unmodified_since']
171         args = self._build_args(attrs)
172         args['format'] = 'json' if self.detail else 'text'
173         
174         if self.until:
175             t = _time.strptime(self.until, self.format)
176             args['until'] = int(_time.mktime(t))
177         
178         container, sep, object = container.partition('/')
179         if object:
180             return
181         
182         detail = 'json'
183         #if request with meta quering disable trash filtering
184         show_trashed = True if self.meta else False
185         l = self.client.list_objects(container, **args)
186         print_list(l, detail=self.detail)
187
188 @cli_command('meta')
189 class Meta(Command):
190     syntax = '[<container>[/<object>]]'
191     description = 'get account/container/object metadata'
192     
193     def add_options(self, parser):
194         parser.add_option('-r', action='store_true', dest='restricted',
195                           default=False, help='show only user defined metadata')
196         parser.add_option('--until', action='store', dest='until',
197                           default=None, help='show metadata until that date')
198         parser.add_option('--format', action='store', dest='format',
199                           default='%d/%m/%Y', help='format to parse until date')
200         parser.add_option('--version', action='store', dest='version',
201                           default=None, help='show specific version \
202                                   (applies only for objects)')
203     
204     def execute(self, path=''):
205         container, sep, object = path.partition('/')
206         args = {'restricted':self.restricted}
207         if getattr(self, 'until'):
208             t = _time.strptime(self.until, self.format)
209             args['until'] = int(_time.mktime(t))
210         
211         if object:
212             meta = self.client.retrieve_object_metadata(container, object,
213                                                         self.restricted,
214                                                         self.version)
215         elif container:
216             meta = self.client.retrieve_container_metadata(container, **args)
217         else:
218             meta = self.client.retrieve_account_metadata(**args)
219         if meta == None:
220             print 'Entity does not exist'
221         else:
222             print_dict(meta, header=None)
223
224 @cli_command('create')
225 class CreateContainer(Command):
226     syntax = '<container> [key=val] [...]'
227     description = 'create a container'
228     
229     def execute(self, container, *args):
230         meta = {}
231         for arg in args:
232             key, sep, val = arg.partition('=')
233             meta[key] = val
234         ret = self.client.create_container(container, **meta)
235         if not ret:
236             print 'Container already exists'
237
238 @cli_command('delete', 'rm')
239 class Delete(Command):
240     syntax = '<container>[/<object>]'
241     description = 'delete a container or an object'
242     
243     def add_options(self, parser):
244         parser.add_option('--until', action='store', dest='until',
245                           default=None, help='remove history until that date')
246         parser.add_option('--format', action='store', dest='format',
247                           default='%d/%m/%Y', help='format to parse until date')
248     
249     def execute(self, path):
250         container, sep, object = path.partition('/')
251         until = None
252         if getattr(self, 'until'):
253             t = _time.strptime(self.until, self.format)
254             until = int(_time.mktime(t))
255         
256         if object:
257             self.client.delete_object(container, object, until)
258         else:
259             self.client.delete_container(container, until)
260
261 @cli_command('get')
262 class GetObject(Command):
263     syntax = '<container>/<object>'
264     description = 'get the data of an object'
265     
266     def add_options(self, parser):
267         parser.add_option('-l', action='store_true', dest='detail',
268                           default=False, help='show detailed output')
269         parser.add_option('--range', action='store', dest='range',
270                           default=None, help='show range of data')
271         parser.add_option('--if-range', action='store', dest='if_range',
272                           default=None, help='show range of data')
273         parser.add_option('--if-match', action='store', dest='if_match',
274                           default=None, help='show output if ETags match')
275         parser.add_option('--if-none-match', action='store',
276                           dest='if_none_match', default=None,
277                           help='show output if ETags don\'t match')
278         parser.add_option('--if-modified-since', action='store', type='str',
279                           dest='if_modified_since', default=None,
280                           help='show output if modified since then')
281         parser.add_option('--if-unmodified-since', action='store', type='str',
282                           dest='if_unmodified_since', default=None,
283                           help='show output if not modified since then')
284         parser.add_option('-o', action='store', type='str',
285                           dest='file', default=None,
286                           help='save output in file')
287         parser.add_option('--version', action='store', type='str',
288                           dest='version', default=None,
289                           help='get the specific \
290                                version')
291         parser.add_option('--versionlist', action='store_true',
292                           dest='versionlist', default=False,
293                           help='get the full object version list')
294     
295     def execute(self, path):
296         attrs = ['if_match', 'if_none_match', 'if_modified_since',
297                  'if_unmodified_since']
298         args = self._build_args(attrs)
299         args['format'] = 'json' if self.detail else 'text'
300         if self.range:
301             args['range'] = 'bytes=%s' %self.range
302         if getattr(self, 'if_range'):
303             args['if-range'] = 'If-Range:%s' % getattr(self, 'if_range')
304         
305         container, sep, object = path.partition('/')
306         data = None
307         if self.versionlist:
308             if 'detail' in args.keys():
309                 args.pop('detail')
310             args.pop('format')
311             self.detail = True
312             data = self.client.retrieve_object_versionlist(container, object, **args)
313         elif self.version:
314             data = self.client.retrieve_object_version(container, object,
315                                                        self.version, **args)
316         else:
317             data = self.client.retrieve_object(container, object, **args)    
318         
319         f = self.file and open(self.file, 'w') or stdout
320         if self.detail:
321             if self.versionlist:
322                 print_versions(data, f=f)
323             else:
324                 print_dict(data, f=f)
325         else:
326             f.write(data)
327         f.close()
328
329 @cli_command('mkdir')
330 class PutMarker(Command):
331     syntax = '<container>/<directory marker>'
332     description = 'create a directory marker'
333     
334     def execute(self, path):
335         container, sep, object = path.partition('/')
336         self.client.create_directory_marker(container, object)
337
338 @cli_command('put')
339 class PutObject(Command):
340     syntax = '<container>/<object> [key=val] [...]'
341     description = 'create/override object'
342     
343     def add_options(self, parser):
344         parser.add_option('--use_hashes', action='store_true', dest='use_hashes',
345                           default=False, help='provide hashmap instead of data')
346         parser.add_option('--chunked', action='store_true', dest='chunked',
347                           default=False, help='set chunked transfer mode')
348         parser.add_option('--etag', action='store', dest='etag',
349                           default=None, help='check written data')
350         parser.add_option('--content-encoding', action='store',
351                           dest='content_encoding', default=None,
352                           help='provide the object MIME content type')
353         parser.add_option('--content-disposition', action='store', type='str',
354                           dest='content_disposition', default=None,
355                           help='provide the presentation style of the object')
356         #parser.add_option('-S', action='store',
357         #                  dest='segment_size', default=False,
358         #                  help='use for large file support')
359         parser.add_option('--manifest', action='store',
360                           dest='x_object_manifest', default=None,
361                           help='upload a manifestation file')
362         parser.add_option('--content-type', action='store',
363                           dest='content_type', default=None,
364                           help='create object with specific content type')
365         parser.add_option('--sharing', action='store',
366                           dest='x_object_sharing', default=None,
367                           help='define sharing object policy')
368         parser.add_option('-f', action='store',
369                           dest='srcpath', default=None,
370                           help='file descriptor to read from (pass - for standard input)')
371         parser.add_option('--public', action='store_true',
372                           dest='x_object_public', default=False,
373                           help='make object publicly accessible')
374     
375     def execute(self, path, *args):
376         if path.find('=') != -1:
377             raise Fault('Missing path argument')
378         
379         #prepare user defined meta
380         meta = {}
381         for arg in args:
382             key, sep, val = arg.partition('=')
383             meta[key] = val
384         
385         attrs = ['etag', 'content_encoding', 'content_disposition',
386                  'content_type', 'x_object_sharing', 'x_object_public']
387         args = self._build_args(attrs)
388         
389         container, sep, object = path.partition('/')
390         
391         f = None
392         if self.srcpath:
393             f = open(self.srcpath) if self.srcpath != '-' else stdin
394         
395         if self.use_hashes and not f:
396             raise Fault('Illegal option combination')
397         
398         if self.chunked:
399             self.client.create_object_using_chunks(container, object, f,
400                                                     meta=meta, **args)
401         elif self.use_hashes:
402             format = 'json' if detail else 'text'
403             self.client.create_object_by_hashmap(container, object, f, format,
404                                  meta=meta, **args)
405         elif self.x_object_manifest:
406             self.client.create_manifestation(container, object, self.x_object_manifest)
407         elif not f:
408             self.client.create_zero_length_object(container, object, meta=meta, **args)
409         else:
410             self.client.create_object(container, object, f, meta=meta, **args)
411         if f:
412             f.close()
413
414 @cli_command('copy', 'cp')
415 class CopyObject(Command):
416     syntax = '<src container>/<src object> [<dst container>/]<dst object> [key=val] [...]'
417     description = 'copy an object to a different location'
418     
419     def add_options(self, parser):
420         parser.add_option('--version', action='store',
421                           dest='version', default=False,
422                           help='copy specific version')
423         parser.add_option('--public', action='store_true',
424                           dest='public', default=False,
425                           help='make object publicly accessible')
426         parser.add_option('--content-type', action='store',
427                           dest='content_type', default=None,
428                           help='change object\'s content type')
429     
430     def execute(self, src, dst, *args):
431         src_container, sep, src_object = src.partition('/')
432         dst_container, sep, dst_object = dst.partition('/')
433         
434         #prepare user defined meta
435         meta = {}
436         for arg in args:
437             key, sep, val = arg.partition('=')
438             meta[key] = val
439         
440         if not sep:
441             dst_container = src_container
442             dst_object = dst
443         
444         args = {'content_type':self.content_type} if self.content_type else {}
445         self.client.copy_object(src_container, src_object, dst_container,
446                                 dst_object, meta, self.public, self.version,
447                                 **args)
448
449 @cli_command('set')
450 class SetMeta(Command):
451     syntax = '[<container>[/<object>]] key=val [key=val] [...]'
452     description = 'set account/container/object metadata'
453     
454     def execute(self, path, *args):
455         #in case of account fix the args
456         if path.find('=') != -1:
457             args = list(args)
458             args.append(path)
459             args = tuple(args)
460             path = ''
461         meta = {}
462         for arg in args:
463             key, sep, val = arg.partition('=')
464             meta[key.strip()] = val.strip()
465         container, sep, object = path.partition('/')
466         if object:
467             self.client.update_object_metadata(container, object, **meta)
468         elif container:
469             self.client.update_container_metadata(container, **meta)
470         else:
471             self.client.update_account_metadata(**meta)
472
473 @cli_command('update')
474 class UpdateObject(Command):
475     syntax = '<container>/<object> path [key=val] [...]'
476     description = 'update object metadata/data (default mode: append)'
477     
478     def add_options(self, parser):
479         parser.add_option('-a', action='store_true', dest='append',
480                           default=True, help='append data')
481         parser.add_option('--offset', action='store',
482                           dest='offset',
483                           default=None, help='starting offest to be updated')
484         parser.add_option('--range', action='store', dest='content-range',
485                           default=None, help='range of data to be updated')
486         parser.add_option('--chunked', action='store_true', dest='chunked',
487                           default=False, help='set chunked transfer mode')
488         parser.add_option('--content-encoding', action='store',
489                           dest='content_encoding', default=None,
490                           help='provide the object MIME content type')
491         parser.add_option('--content-disposition', action='store', type='str',
492                           dest='content_disposition', default=None,
493                           help='provide the presentation style of the object')
494         parser.add_option('--manifest', action='store', type='str',
495                           dest='x_object_manifest', default=None,
496                           help='use for large file support')        
497         parser.add_option('--sharing', action='store',
498                           dest='x_object_sharing', default=None,
499                           help='define sharing object policy')
500         parser.add_option('--nosharing', action='store_true',
501                           dest='no_sharing', default=None,
502                           help='clear object sharing policy')
503         parser.add_option('-f', action='store',
504                           dest='srcpath', default=None,
505                           help='file descriptor to read from: pass - for standard input')
506         parser.add_option('--public', action='store_true',
507                           dest='x_object_public', default=False,
508                           help='make object publicly accessible')
509     
510     def execute(self, path, *args):
511         if path.find('=') != -1:
512             raise Fault('Missing path argument')
513         
514         #prepare user defined meta
515         meta = {}
516         for arg in args:
517             key, sep, val = arg.partition('=')
518             meta[key] = val
519         
520         if self.no_sharing:
521             self.x_object_sharing = ''
522         
523         attrs = ['content_encoding', 'content_disposition', 'x_object_sharing',
524                  'x_object_public']
525         args = self._build_args(attrs)
526         
527         container, sep, object = path.partition('/')
528         
529         f = None
530         if self.srcpath:
531             f = open(self.srcpath) if self.srcpath != '-' else stdin
532         
533         if self.chunked:
534             self.client.update_object_using_chunks(container, object, f,
535                                                     meta=meta, **args)
536         else:
537             self.client.update_object(container, object, f, meta=meta, **args)
538         if f:
539             f.close()
540
541 @cli_command('move', 'mv')
542 class MoveObject(Command):
543     syntax = '<src container>/<src object> [<dst container>/]<dst object>'
544     description = 'move an object to a different location'
545     
546     def add_options(self, parser):
547         parser.add_option('--version', action='store',
548                           dest='version', default=None,
549                           help='move a specific object version')
550         parser.add_option('--public', action='store_true',
551                           dest='public', default=False,
552                           help='make object publicly accessible')
553         parser.add_option('--content-type', action='store',
554                           dest='content_type', default=None,
555                           help='change object\'s content type')
556     
557     def execute(self, src, dst, *args):
558         src_container, sep, src_object = src.partition('/')
559         dst_container, sep, dst_object = dst.partition('/')
560         if not sep:
561             dst_container = src_container
562             dst_object = dst
563         
564         #prepare user defined meta
565         meta = {}
566         for arg in args:
567             key, sep, val = arg.partition('=')
568             meta[key] = val
569         
570         args = {'content_type':self.content_type} if self.content_type else {}
571         self.client.move_object(src_container, src_object, dst_container,
572                                 dst_object, meta, self.public, self.version, **args)
573
574 @cli_command('unset')
575 class UnsetObject(Command):
576     syntax = '<container>/[<object>] key [key] [...]'
577     description = 'delete metadata info'
578     
579     def execute(self, path, *args):
580         #in case of account fix the args
581         if len(args) == 0:
582             args = list(args)
583             args.append(path)
584             args = tuple(args)
585             path = ''
586         meta = []
587         for key in args:
588             meta.append(key)
589         container, sep, object = path.partition('/')
590         if object:
591             self.client.delete_object_metadata(container, object, meta)
592         elif container:
593             self.client.delete_container_metadata(container, meta)
594         else:
595             self.client.delete_account_metadata(meta)
596
597 @cli_command('group')
598 class CreateGroup(Command):
599     syntax = 'key=val [key=val] [...]'
600     description = 'create account groups'
601     
602     def execute(self, *args):
603         groups = {}
604         for arg in args:
605             key, sep, val = arg.partition('=')
606             groups[key] = val
607         self.client.set_account_groups(**groups)
608
609 @cli_command('ungroup')
610 class DeleteGroup(Command):
611     syntax = 'key [key] [...]'
612     description = 'delete account groups'
613     
614     def execute(self, *args):
615         groups = []
616         for arg in args:
617             groups.append(arg)
618         self.client.unset_account_groups(groups)
619
620 @cli_command('policy')
621 class SetPolicy(Command):
622     syntax = 'container key=val [key=val] [...]'
623     description = 'set container policies'
624     
625     def execute(self, path, *args):
626         if path.find('=') != -1:
627             raise Fault('Missing container argument')
628         
629         container, sep, object = path.partition('/')
630         
631         if object:
632             raise Fault('Only containers have policies')
633         
634         policies = {}
635         for arg in args:
636             key, sep, val = arg.partition('=')
637             policies[key] = val
638         
639         self.client.set_container_policies(container, **policies)
640
641 @cli_command('publish')
642 class PublishObject(Command):
643     syntax = '<container>/<object>'
644     description = 'publish an object'
645     
646     def execute(self, src):
647         src_container, sep, src_object = src.partition('/')
648         
649         self.client.publish_object(src_container, src_object)
650
651 @cli_command('unpublish')
652 class UnpublishObject(Command):
653     syntax = '<container>/<object>'
654     description = 'unpublish an object'
655     
656     def execute(self, src):
657         src_container, sep, src_object = src.partition('/')
658         
659         self.client.unpublish_object(src_container, src_object)
660
661 @cli_command('sharing')
662 class SharingObject(Command):
663     syntax = 'list users sharing objects with the user'
664     description = 'list user accounts sharing objects with the user'
665     
666     def add_options(self, parser):
667         parser.add_option('-l', action='store_true', dest='detail',
668                           default=False, help='show detailed output')
669         parser.add_option('-n', action='store', type='int', dest='limit',
670                           default=10000, help='show limited output')
671         parser.add_option('--marker', action='store', type='str',
672                           dest='marker', default=None,
673                           help='show output greater then marker')
674         
675     
676     def execute(self):
677         attrs = ['limit', 'marker']
678         args = self._build_args(attrs)
679         args['format'] = 'json' if self.detail else 'text'
680         
681         print_list(self.client.list_shared_by_others(**args))
682
683 def print_usage():
684     cmd = Command('', [])
685     parser = cmd.parser
686     parser.usage = '%prog <command> [options]'
687     parser.print_help()
688     
689     commands = []
690     for cls in set(_cli_commands.values()):
691         name = ', '.join(cls.commands)
692         description = getattr(cls, 'description', '')
693         commands.append('  %s %s' % (name.ljust(12), description))
694     print '\nCommands:\n' + '\n'.join(sorted(commands))
695
696 def print_dict(d, header='name', f=stdout, detail=True):
697     header = header if header in d else 'subdir'
698     if header and header in d:
699         f.write('%s\n' %d.pop(header).encode('utf8'))
700     if detail:
701         patterns = ['^x_(account|container|object)_meta_(\w+)$']
702         patterns.append(patterns[0].replace('_', '-'))
703         for key, val in sorted(d.items()):
704             f.write('%s: %s\n' % (key.rjust(30), val))
705
706 def print_list(l, verbose=False, f=stdout, detail=True):
707     for elem in l:
708         #if it's empty string continue
709         if not elem:
710             continue
711         if type(elem) == types.DictionaryType:
712             print_dict(elem, f=f, detail=detail)
713         elif type(elem) == types.StringType:
714             if not verbose:
715                 elem = elem.split('Traceback')[0]
716             f.write('%s\n' % elem)
717         else:
718             f.write('%s\n' % elem)
719
720 def print_versions(data, f=stdout):
721     if 'versions' not in data:
722         f.write('%s\n' %data)
723         return
724     f.write('versions:\n')
725     for id, t in data['versions']:
726         f.write('%s @ %s\n' % (str(id).rjust(30), datetime.fromtimestamp(t)))
727
728 def main():
729     try:
730         name = argv[1]
731         cls = class_for_cli_command(name)
732     except (IndexError, KeyError):
733         print_usage()
734         exit(1)
735     
736     cmd = cls(name, argv[2:])
737     
738     try:
739         cmd.execute(*cmd.args)
740     except TypeError, e:
741         cmd.parser.print_help()
742         exit(1)
743     except Fault, f:
744         status = f.status and '%s ' % f.status or ''
745         print '%s%s' % (status, f.data)
746
747 if __name__ == '__main__':
748     main()