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