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