Add link to login in htdocs.
[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         parser.add_option('--replace', action='store_true',
510                           dest='replace', default=False,
511                           help='override metadata')
512     
513     def execute(self, path, *args):
514         if path.find('=') != -1:
515             raise Fault('Missing path argument')
516         
517         #prepare user defined meta
518         meta = {}
519         for arg in args:
520             key, sep, val = arg.partition('=')
521             meta[key] = val
522         
523         if self.no_sharing:
524             self.x_object_sharing = ''
525         
526         attrs = ['content_encoding', 'content_disposition', 'x_object_sharing',
527                  'x_object_public', 'replace']
528         args = self._build_args(attrs)
529         
530         container, sep, object = path.partition('/')
531         
532         f = None
533         if self.srcpath:
534             f = open(self.srcpath) if self.srcpath != '-' else stdin
535         
536         if self.chunked:
537             self.client.update_object_using_chunks(container, object, f,
538                                                     meta=meta, **args)
539         else:
540             self.client.update_object(container, object, f, meta=meta, **args)
541         if f:
542             f.close()
543
544 @cli_command('move', 'mv')
545 class MoveObject(Command):
546     syntax = '<src container>/<src object> [<dst container>/]<dst object>'
547     description = 'move an object to a different location'
548     
549     def add_options(self, parser):
550         parser.add_option('--version', action='store',
551                           dest='version', default=None,
552                           help='move a specific object version')
553         parser.add_option('--public', action='store_true',
554                           dest='public', default=False,
555                           help='make object publicly accessible')
556         parser.add_option('--content-type', action='store',
557                           dest='content_type', default=None,
558                           help='change object\'s content type')
559     
560     def execute(self, src, dst, *args):
561         src_container, sep, src_object = src.partition('/')
562         dst_container, sep, dst_object = dst.partition('/')
563         if not sep:
564             dst_container = src_container
565             dst_object = dst
566         
567         #prepare user defined meta
568         meta = {}
569         for arg in args:
570             key, sep, val = arg.partition('=')
571             meta[key] = val
572         
573         args = {'content_type':self.content_type} if self.content_type else {}
574         self.client.move_object(src_container, src_object, dst_container,
575                                 dst_object, meta, self.public, self.version, **args)
576
577 @cli_command('unset')
578 class UnsetObject(Command):
579     syntax = '<container>/[<object>] key [key] [...]'
580     description = 'delete metadata info'
581     
582     def execute(self, path, *args):
583         #in case of account fix the args
584         if len(args) == 0:
585             args = list(args)
586             args.append(path)
587             args = tuple(args)
588             path = ''
589         meta = []
590         for key in args:
591             meta.append(key)
592         container, sep, object = path.partition('/')
593         if object:
594             self.client.delete_object_metadata(container, object, meta)
595         elif container:
596             self.client.delete_container_metadata(container, meta)
597         else:
598             self.client.delete_account_metadata(meta)
599
600 @cli_command('group')
601 class CreateGroup(Command):
602     syntax = 'key=val [key=val] [...]'
603     description = 'create account groups'
604     
605     def execute(self, *args):
606         groups = {}
607         for arg in args:
608             key, sep, val = arg.partition('=')
609             groups[key] = val
610         self.client.set_account_groups(**groups)
611
612 @cli_command('ungroup')
613 class DeleteGroup(Command):
614     syntax = 'key [key] [...]'
615     description = 'delete account groups'
616     
617     def execute(self, *args):
618         groups = []
619         for arg in args:
620             groups.append(arg)
621         self.client.unset_account_groups(groups)
622
623 @cli_command('policy')
624 class SetPolicy(Command):
625     syntax = 'container key=val [key=val] [...]'
626     description = 'set container policies'
627     
628     def execute(self, path, *args):
629         if path.find('=') != -1:
630             raise Fault('Missing container argument')
631         
632         container, sep, object = path.partition('/')
633         
634         if object:
635             raise Fault('Only containers have policies')
636         
637         policies = {}
638         for arg in args:
639             key, sep, val = arg.partition('=')
640             policies[key] = val
641         
642         self.client.set_container_policies(container, **policies)
643
644 @cli_command('publish')
645 class PublishObject(Command):
646     syntax = '<container>/<object>'
647     description = 'publish an object'
648     
649     def execute(self, src):
650         src_container, sep, src_object = src.partition('/')
651         
652         self.client.publish_object(src_container, src_object)
653
654 @cli_command('unpublish')
655 class UnpublishObject(Command):
656     syntax = '<container>/<object>'
657     description = 'unpublish an object'
658     
659     def execute(self, src):
660         src_container, sep, src_object = src.partition('/')
661         
662         self.client.unpublish_object(src_container, src_object)
663
664 @cli_command('sharing')
665 class SharingObject(Command):
666     syntax = 'list users sharing objects with the user'
667     description = 'list user accounts sharing objects with the user'
668     
669     def add_options(self, parser):
670         parser.add_option('-l', action='store_true', dest='detail',
671                           default=False, help='show detailed output')
672         parser.add_option('-n', action='store', type='int', dest='limit',
673                           default=10000, help='show limited output')
674         parser.add_option('--marker', action='store', type='str',
675                           dest='marker', default=None,
676                           help='show output greater then marker')
677         
678     
679     def execute(self):
680         attrs = ['limit', 'marker']
681         args = self._build_args(attrs)
682         args['format'] = 'json' if self.detail else 'text'
683         
684         print_list(self.client.list_shared_by_others(**args))
685
686 def print_usage():
687     cmd = Command('', [])
688     parser = cmd.parser
689     parser.usage = '%prog <command> [options]'
690     parser.print_help()
691     
692     commands = []
693     for cls in set(_cli_commands.values()):
694         name = ', '.join(cls.commands)
695         description = getattr(cls, 'description', '')
696         commands.append('  %s %s' % (name.ljust(12), description))
697     print '\nCommands:\n' + '\n'.join(sorted(commands))
698
699 def print_dict(d, header='name', f=stdout, detail=True):
700     header = header if header in d else 'subdir'
701     if header and header in d:
702         f.write('%s\n' %d.pop(header).encode('utf8'))
703     if detail:
704         patterns = ['^x_(account|container|object)_meta_(\w+)$']
705         patterns.append(patterns[0].replace('_', '-'))
706         for key, val in sorted(d.items()):
707             f.write('%s: %s\n' % (key.rjust(30), val))
708
709 def print_list(l, verbose=False, f=stdout, detail=True):
710     for elem in l:
711         #if it's empty string continue
712         if not elem:
713             continue
714         if type(elem) == types.DictionaryType:
715             print_dict(elem, f=f, detail=detail)
716         elif type(elem) == types.StringType:
717             if not verbose:
718                 elem = elem.split('Traceback')[0]
719             f.write('%s\n' % elem)
720         else:
721             f.write('%s\n' % elem)
722
723 def print_versions(data, f=stdout):
724     if 'versions' not in data:
725         f.write('%s\n' %data)
726         return
727     f.write('versions:\n')
728     for id, t in data['versions']:
729         f.write('%s @ %s\n' % (str(id).rjust(30), datetime.fromtimestamp(t)))
730
731 def main():
732     try:
733         name = argv[1]
734         cls = class_for_cli_command(name)
735     except (IndexError, KeyError):
736         print_usage()
737         exit(1)
738     
739     cmd = cls(name, argv[2:])
740     
741     try:
742         cmd.execute(*cmd.args)
743     except TypeError, e:
744         cmd.parser.print_help()
745         exit(1)
746     except Fault, f:
747         status = f.status and '%s ' % f.status or ''
748         print '%s%s' % (status, f.data)
749
750 if __name__ == '__main__':
751     main()