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