client library: display objects with space for name
[pithos] / tools / lib / client.py
1 # Copyright 2011 GRNET S.A. All rights reserved.
2
3 # Redistribution and use in source and binary forms, with or
4 # without modification, are permitted provided that the following
5 # conditions are met:
6
7 #   1. Redistributions of source code must retain the above
8 #      copyright notice, this list of conditions and the following
9 #      disclaimer.
10
11 #   2. Redistributions in binary form must reproduce the above
12 #      copyright notice, this list of conditions and the following
13 #      disclaimer in the documentation and/or other materials
14 #      provided with the distribution.
15
16 # THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
17 # OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18 # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
19 # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
20 # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
23 # USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
24 # AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
25 # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
26 # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27 # POSSIBILITY OF SUCH DAMAGE.
28
29 # The views and conclusions contained in the software and
30 # documentation are those of the authors and should not be
31 # interpreted as representing official policies, either expressed
32 # or implied, of GRNET S.A.
33
34 from httplib import HTTPConnection, HTTP
35 from sys import stdin
36 from xml.dom import minidom
37 from StringIO import StringIO
38
39 import json
40 import types
41 import socket
42 import urllib
43 import datetime
44
45 ERROR_CODES = {304:'Not Modified',
46                400:'Bad Request',
47                401:'Unauthorized',
48                404:'Not Found',
49                409:'Conflict',
50                411:'Length Required',
51                412:'Precondition Failed',
52                416:'Range Not Satisfiable',
53                422:'Unprocessable Entity',
54                503:'Service Unavailable'}
55
56 class Fault(Exception):
57     def __init__(self, data='', status=None):
58         if data == '' and status in ERROR_CODES.keys():
59             data = ERROR_CODES[status]
60         Exception.__init__(self, data)
61         self.data = data
62         self.status = status
63
64 class Client(object):
65     def __init__(self, host, token, account, api='v1', verbose=False, debug=False):
66         """`host` can also include a port, e.g '127.0.0.1:8000'."""
67         
68         self.host = host
69         self.account = account
70         self.api = api
71         self.verbose = verbose or debug
72         self.debug = debug
73         self.token = token
74     
75     def _req(self, method, path, body=None, headers={}, format='text', params={}):
76         slash = '/' if self.api else ''
77         full_path = '%s%s%s?format=%s' % (slash, self.api, path, format)
78         
79         for k,v in params.items():
80             if v:
81                 full_path = '%s&%s=%s' %(full_path, k, v)
82             else:
83                 full_path = '%s&%s=' %(full_path, k)
84         conn = HTTPConnection(self.host)
85         
86         full_path = urllib.quote(full_path, '?&:=/')
87         
88         kwargs = {}
89         for k,v in headers.items():
90             headers.pop(k)
91             k = k.replace('_', '-')
92             headers[k] = v
93         
94         kwargs['headers'] = headers
95         kwargs['headers']['X-Auth-Token'] = self.token
96         if body:
97             kwargs['body'] = body
98             kwargs['headers'].setdefault('content-type', 'application/octet-stream')
99         kwargs['headers'].setdefault('content-length', len(body) if body else 0)
100         
101         #print '#', method, full_path, kwargs
102         t1 = datetime.datetime.utcnow()
103         conn.request(method, full_path, **kwargs)
104         
105         resp = conn.getresponse()
106         t2 = datetime.datetime.utcnow()
107         #print 'response time:', str(t2-t1)
108         headers = dict(resp.getheaders())
109         
110         if self.verbose:
111             print '%d %s' % (resp.status, resp.reason)
112             for key, val in headers.items():
113                 print '%s: %s' % (key.capitalize(), val)
114             print
115         
116         length = resp.getheader('content-length', None)
117         data = resp.read(length)
118         if self.debug:
119             print data
120             print
121         
122         if int(resp.status) in ERROR_CODES.keys():
123             raise Fault(data, int(resp.status))
124         
125         #print '**',  resp.status, headers, data, '\n'
126         return resp.status, headers, data
127     
128     def _chunked_transfer(self, path, method='PUT', f=stdin, headers=None,
129                           blocksize=1024, params={}):
130         """perfomrs a chunked request"""
131         http = HTTPConnection(self.host)
132         
133         # write header
134         full_path = '/%s%s?' % (self.api, path)
135         
136         for k,v in params.items():
137             if v:
138                 full_path = '%s&%s=%s' %(full_path, k, v)
139             else:
140                 full_path = '%s&%s=' %(full_path, k)
141         
142         full_path = urllib.quote(full_path, '?&:=/')
143         
144         http.putrequest(method, full_path)
145         http.putheader('x-auth-token', self.token)
146         http.putheader('content-type', 'application/octet-stream')
147         http.putheader('transfer-encoding', 'chunked')
148         if headers:
149             for header,value in headers.items():
150                 http.putheader(header, value)
151         http.endheaders()
152         
153         # write body
154         data = ''
155         while True:
156             if f.closed:
157                 break
158             block = f.read(blocksize)
159             if block == '':
160                 break
161             data = '%x\r\n%s\r\n' % (len(block), block)
162             try:
163                 http.send(data)
164             except:
165                 #retry
166                 http.send(data)
167         data = '0\r\n\r\n'
168         try:
169             http.send(data)
170         except:
171             #retry
172             http.send(data)
173         
174         # get response
175         resp = http.getresponse()
176         
177         headers = dict(resp.getheaders())
178         
179         if self.verbose:
180             print '%d %s' % (resp.status, resp.reason)
181             for key, val in headers.items():
182                 print '%s: %s' % (key.capitalize(), val)
183             print
184         
185         length = resp.getheader('Content-length', None)
186         data = resp.read(length)
187         if self.debug:
188             print data
189             print
190         
191         if int(resp.status) in ERROR_CODES.keys():
192             raise Fault(data, int(resp.status))
193         
194         #print '*',  resp.status, headers, data
195         return resp.status, headers, data
196     
197     def delete(self, path, format='text', params={}):
198         return self._req('DELETE', path, format=format, params=params)
199     
200     def get(self, path, format='text', headers={}, params={}):
201         return self._req('GET', path, headers=headers, format=format,
202                         params=params)
203     
204     def head(self, path, format='text', params={}):
205          return self._req('HEAD', path, format=format, params=params)
206     
207     def post(self, path, body=None, format='text', headers=None, params={}):
208         return self._req('POST', path, body, headers=headers, format=format,
209                         params=params)
210     
211     def put(self, path, body=None, format='text', headers=None, params={}):
212         return self._req('PUT', path, body, headers=headers, format=format,
213                          params=params)
214     
215     def _list(self, path, format='text', params={}, **headers):
216         status, headers, data = self.get(path, format=format, headers=headers,
217                                          params=params)
218         if format == 'json':
219             data = json.loads(data) if data else ''
220         elif format == 'xml':
221             data = minidom.parseString(data)
222         else:
223             data = data.split('\n')[:-1] if data else ''
224         return data
225     
226     def _get_metadata(self, path, prefix=None, params={}):
227         status, headers, data = self.head(path, params=params)
228         prefixlen = len(prefix) if prefix else 0
229         meta = {}
230         for key, val in headers.items():
231             if prefix and not key.startswith(prefix):
232                 continue
233             elif prefix and key.startswith(prefix):
234                 key = key[prefixlen:]
235             meta[key] = val
236         return meta
237     
238     def _filter(self, l, d):
239         """
240         filter out from l elements having the metadata values provided
241         """
242         ll = l
243         for elem in l:
244             if type(elem) == types.DictionaryType:
245                 for key in d.keys():
246                     k = 'x_object_meta_%s' % key
247                     if k in elem.keys() and elem[k] == d[key]:
248                         ll.remove(elem)
249                         break
250         return ll
251     
252 class OOS_Client(Client):
253     """Openstack Object Storage Client"""
254     
255     def _update_metadata(self, path, entity, **meta):
256         """adds new and updates the values of previously set metadata"""
257         ex_meta = self.retrieve_account_metadata(restricted=True)
258         ex_meta.update(meta)
259         headers = {}
260         prefix = 'x-%s-meta-' % entity
261         for k,v in ex_meta.items():
262             k = '%s%s' % (prefix, k)
263             headers[k] = v
264         return self.post(path, headers=headers)
265     
266     def _reset_metadata(self, path, entity, **meta):
267         """
268         overwrites all user defined metadata
269         """
270         headers = {}
271         prefix = 'x-%s-meta-' % entity
272         for k,v in meta.items():
273             k = '%s%s' % (prefix, k)
274             headers[k] = v
275         return self.post(path, headers=headers)
276     
277     def _delete_metadata(self, path, entity, meta=[]):
278         """delete previously set metadata"""
279         ex_meta = self.retrieve_account_metadata(restricted=True)
280         headers = {}
281         prefix = 'x-%s-meta-' % entity
282         for k in ex_meta.keys():
283             if k in meta:
284                 headers['%s%s' % (prefix, k)] = ex_meta[k]
285         return self.post(path, headers=headers)
286     
287     # Storage Account Services
288     
289     def list_containers(self, format='text', limit=None,
290                         marker=None, params={}, account=None, **headers):
291         """lists containers"""
292         account = account or self.account
293         path = '/%s' % account
294         params.update({'limit':limit, 'marker':marker})
295         return self._list(path, format, params, **headers)
296     
297     def retrieve_account_metadata(self, restricted=False, account=None, **params):
298         """returns the account metadata"""
299         account = account or self.account
300         path = '/%s' % account
301         prefix = 'x-account-meta-' if restricted else None
302         return self._get_metadata(path, prefix, params)
303     
304     def update_account_metadata(self, account=None, **meta):
305         """updates the account metadata"""
306         account = account or self.account
307         path = '/%s' % account
308         return self._update_metadata(path, 'account', **meta)
309         
310     def delete_account_metadata(self, meta=[], account=None):
311         """deletes the account metadata"""
312         account = account or self.account
313         path = '/%s' % account
314         return self._delete_metadata(path, 'account', meta)
315     
316     def reset_account_metadata(self, account=None, **meta):
317         """resets account metadata"""
318         account = account or self.account
319         path = '/%s' % account
320         return self._reset_metadata(path, 'account', **meta)
321     
322     # Storage Container Services
323     
324     def _filter_trashed(self, l):
325         return self._filter(l, {'trash':'true'})
326     
327     def list_objects(self, container, format='text',
328                      limit=None, marker=None, prefix=None, delimiter=None,
329                      path=None, include_trashed=False, params={}, account=None,
330                      **headers):
331         """returns a list with the container objects"""
332         account = account or self.account
333         params.update({'limit':limit, 'marker':marker, 'prefix':prefix,
334                        'delimiter':delimiter, 'path':path})
335         l = self._list('/%s/%s' % (account, container), format, params,
336                        **headers)
337         #TODO support filter trashed with xml also
338         if format != 'xml' and not include_trashed:
339             l = self._filter_trashed(l)
340         return l
341     
342     def create_container(self, container, account=None, **meta):
343         """creates a container"""
344         account = account or self.account
345         headers = {}
346         for k,v in meta.items():
347             headers['x-container-meta-%s' %k.strip().upper()] = v.strip()
348         status, header, data = self.put('/%s/%s' % (account, container),
349                                         headers=headers)
350         if status == 202:
351             return False
352         elif status != 201:
353             raise Fault(data, int(status))
354         return True
355     
356     def delete_container(self, container, params={}, account=None):
357         """deletes a container"""
358         account = account or self.account
359         return self.delete('/%s/%s' % (account, container), params=params)
360     
361     def retrieve_container_metadata(self, container, restricted=False,
362                                     account=None, **params):
363         """returns the container metadata"""
364         account = account or self.account
365         prefix = 'x-container-meta-' if restricted else None
366         return self._get_metadata('/%s/%s' % (account, container), prefix,
367                                   params)
368     
369     def update_container_metadata(self, container, account=None, **meta):
370         """unpdates the container metadata"""
371         account = account or self.account
372         return self._update_metadata('/%s/%s' % (account, container),
373                                      'container', **meta)
374         
375     def delete_container_metadata(self, container, meta=[], account=None):
376         """deletes the container metadata"""
377         account = account or self.account
378         path = '/%s/%s' % (account, container)
379         return self._delete_metadata(path, 'container', meta)
380     
381     # Storage Object Services
382     
383     def request_object(self, container, object, format='text', params={},
384                        account=None, **headers):
385         """returns tuple containing the status, headers and data response for an object request"""
386         account = account or self.account
387         path = '/%s/%s/%s' % (account, container, object)
388         status, headers, data = self.get(path, format, headers, params)
389         return status, headers, data
390     
391     def retrieve_object(self, container, object, format='text', params={},
392                         account=None, **headers):
393         """returns an object's data"""
394         account = account or self.account
395         t = self.request_object(container, object, format, params, account,
396                                 **headers)
397         data = t[2]
398         if format == 'json':
399             data = json.loads(data) if data else ''
400         elif format == 'xml':
401             data = minidom.parseString(data)
402         return data
403     
404     def retrieve_object_hashmap(self, container, object, params={},
405                         account=None, **headers):
406         """returns the hashmap representing object's data"""
407         args = locals().copy()
408         for elem in ['self', 'container', 'object']:
409             args.pop(elem)
410         data = self.retrieve_object(container, object, format='json', **args)
411         return data['hashes']
412     
413     def create_directory_marker(self, container, object, account=None):
414         """creates a dierectory marker"""
415         account = account or self.account
416         if not object:
417             raise Fault('Directory markers have to be nested in a container')
418         h = {'content_type':'application/directory'}
419         return self.create_zero_length_object(container, object, account=account,
420                                               **h)
421     
422     def create_object(self, container, object, f=stdin, format='text', meta={},
423                       params={}, etag=None, content_type=None, content_encoding=None,
424                       content_disposition=None, account=None, **headers):
425         """creates a zero-length object"""
426         account = account or self.account
427         path = '/%s/%s/%s' % (account, container, object)
428         for k, v  in headers.items():
429             if v == None:
430                 headers.pop(k)
431         
432         l = ['etag', 'content_encoding', 'content_disposition', 'content_type']
433         l = [elem for elem in l if eval(elem)]
434         for elem in l:
435             headers.update({elem:eval(elem)})
436         headers.setdefault('content-type', 'application/octet-stream')
437         
438         for k,v in meta.items():
439             headers['x-object-meta-%s' %k.strip()] = v.strip()
440         data = f.read() if f else None
441         return self.put(path, data, format, headers=headers, params=params)
442     
443     def create_zero_length_object(self, container, object, meta={}, etag=None,
444                                   content_type=None, content_encoding=None,
445                                   content_disposition=None, account=None,
446                                   **headers):
447         account = account or self.account
448         args = locals().copy()
449         for elem in ['self', 'container', 'headers', 'account']:
450             args.pop(elem)
451         args.update(headers)
452         return self.create_object(container, account=account, f=None, **args)
453     
454     def update_object(self, container, object, f=stdin,
455                       offset=None, meta={}, params={}, content_length=None,
456                       content_type=None, content_encoding=None,
457                       content_disposition=None,  account=None, **headers):
458         account = account or self.account
459         path = '/%s/%s/%s' % (account, container, object)
460         for k, v  in headers.items():
461             if v == None:
462                 headers.pop(k)
463         
464         l = ['content_encoding', 'content_disposition', 'content_type',
465              'content_length']
466         l = [elem for elem in l if eval(elem)]
467         for elem in l:
468             headers.update({elem:eval(elem)})
469         
470         if 'content_range' not in headers.keys():
471             if offset != None:
472                 headers['content_range'] = 'bytes %s-/*' % offset
473             else:
474                 headers['content_range'] = 'bytes */*'
475             
476         for k,v in meta.items():
477             headers['x-object-meta-%s' %k.strip()] = v.strip()
478         data = f.read() if f else None
479         return self.post(path, data, headers=headers, params=params)
480     
481     def update_object_using_chunks(self, container, object, f=stdin,
482                                    blocksize=1024, offset=None, meta={},
483                                    params={}, content_type=None, content_encoding=None,
484                                    content_disposition=None, account=None, **headers):
485         """updates an object (incremental upload)"""
486         account = account or self.account
487         path = '/%s/%s/%s' % (account, container, object)
488         headers = headers if not headers else {}
489         l = ['content_type', 'content_encoding', 'content_disposition']
490         l = [elem for elem in l if eval(elem)]
491         for elem in l:
492             headers.update({elem:eval(elem)})
493         
494         if offset != None:
495             headers['content_range'] = 'bytes %s-/*' % offset
496         else:
497             headers['content_range'] = 'bytes */*'
498         
499         for k,v in meta.items():
500             v = v.strip()
501             headers['x-object-meta-%s' %k.strip()] = v
502         return self._chunked_transfer(path, 'POST', f, headers=headers,
503                                       blocksize=blocksize, params=params)
504     
505     def _change_obj_location(self, src_container, src_object, dst_container,
506                              dst_object, remove=False, meta={}, account=None,
507                              content_type=None, **headers):
508         account = account or self.account
509         path = '/%s/%s/%s' % (account, dst_container, dst_object)
510         headers = {} if not headers else headers
511         for k, v in meta.items():
512             headers['x-object-meta-%s' % k] = v
513         if remove:
514             headers['x-move-from'] = '/%s/%s' % (src_container, src_object)
515         else:
516             headers['x-copy-from'] = '/%s/%s' % (src_container, src_object)
517         headers['content_length'] = 0
518         if content_type:
519             headers['content_type'] = content_type 
520         return self.put(path, headers=headers)
521     
522     def copy_object(self, src_container, src_object, dst_container, dst_object,
523                    meta={}, account=None, content_type=None, **headers):
524         """copies an object"""
525         account = account or self.account
526         return self._change_obj_location(src_container, src_object,
527                                    dst_container, dst_object, account=account,
528                                    remove=False, meta=meta,
529                                    content_type=content_type, **headers)
530     
531     def move_object(self, src_container, src_object, dst_container,
532                              dst_object, meta={}, account=None,
533                              content_type=None, **headers):
534         """moves an object"""
535         account = account or self.account
536         return self._change_obj_location(src_container, src_object,
537                                          dst_container, dst_object,
538                                          account=account, remove=True,
539                                          meta=meta, content_type=content_type,
540                                          **headers)
541     
542     def delete_object(self, container, object, params={}, account=None):
543         """deletes an object"""
544         account = account or self.account
545         return self.delete('/%s/%s/%s' % (account, container, object),
546                            params=params)
547     
548     def retrieve_object_metadata(self, container, object, restricted=False,
549                                  version=None, account=None):
550         """
551         set restricted to True to get only user defined metadata
552         """
553         account = account or self.account
554         path = '/%s/%s/%s' % (account, container, object)
555         prefix = 'x-object-meta-' if restricted else None
556         params = {'version':version} if version else {}
557         return self._get_metadata(path, prefix, params=params)
558     
559     def update_object_metadata(self, container, object, account=None,
560                                **meta):
561         """
562         updates object's metadata
563         """
564         account = account or self.account
565         path = '/%s/%s/%s' % (account, container, object)
566         return self._update_metadata(path, 'object', **meta)
567     
568     def delete_object_metadata(self, container, object, meta=[], account=None):
569         """
570         deletes object's metadata
571         """
572         account = account or self.account
573         path = '/%s/%s' % (account, container, object)
574         return self._delete_metadata(path, 'object', meta)
575     
576 class Pithos_Client(OOS_Client):
577     """Pithos Storage Client. Extends OOS_Client"""
578     
579     def _update_metadata(self, path, entity, **meta):
580         """
581         adds new and updates the values of previously set metadata
582         """
583         params = {'update':None}
584         headers = {}
585         prefix = 'x-%s-meta-' % entity
586         for k,v in meta.items():
587             k = '%s%s' % (prefix, k)
588             headers[k] = v
589         return self.post(path, headers=headers, params=params)
590     
591     def _delete_metadata(self, path, entity, meta=[]):
592         """
593         delete previously set metadata
594         """
595         params = {'update':None}
596         headers = {}
597         prefix = 'x-%s-meta-' % entity
598         for m in meta:
599             headers['%s%s' % (prefix, m)] = ''
600         return self.post(path, headers=headers, params=params)
601     
602     # Storage Account Services
603     
604     def list_containers(self, format='text', if_modified_since=None,
605                         if_unmodified_since=None, limit=None, marker=None,
606                         shared=False, until=None, account=None):
607         """returns a list with the account containers"""
608         account = account or self.account
609         params = {'until':until} if until else {}
610         if shared:
611             params['shared'] = None
612         headers = {'if-modified-since':if_modified_since,
613                    'if-unmodified-since':if_unmodified_since}
614         return OOS_Client.list_containers(self, account=account, format=format,
615                                           limit=limit, marker=marker,
616                                           params=params, **headers)
617     
618     def retrieve_account_metadata(self, restricted=False, until=None,
619                                   account=None):
620         """returns the account metadata"""
621         account = account or self.account
622         params = {'until':until} if until else {}
623         return OOS_Client.retrieve_account_metadata(self, account=account,
624                                                     restricted=restricted,
625                                                     **params)
626     
627     def set_account_groups(self, account=None, **groups):
628         """create account groups"""
629         account = account or self.account
630         path = '/%s' % account
631         headers = {}
632         for k, v in groups.items():
633             headers['x-account-group-%s' % k] = v
634         params = {'update':None}
635         return self.post(path, headers=headers, params=params)
636     
637     def retrieve_account_groups(self, account=None):
638         """returns the account groups"""
639         account = account or self.account
640         meta = self.retrieve_account_metadata(account=account)
641         prefix = 'x-account-group-'
642         prefixlen = len(prefix)
643         groups = {}
644         for key, val in meta.items():
645             if prefix and not key.startswith(prefix):
646                 continue
647             elif prefix and key.startswith(prefix):
648                 key = key[prefixlen:]
649             groups[key] = val
650         return groups
651     
652     def unset_account_groups(self, groups=[], account=None):
653         """delete account groups"""
654         account = account or self.account
655         path = '/%s' % account
656         headers = {}
657         for elem in groups:
658             headers['x-account-group-%s' % elem] = ''
659         params = {'update':None}
660         return self.post(path, headers=headers, params=params)
661     
662     def reset_account_groups(self, account=None, **groups):
663         """overrides account groups"""
664         account = account or self.account
665         path = '/%s' % account
666         headers = {}
667         for k, v in groups.items():
668             v = v.strip()
669             headers['x-account-group-%s' % k] = v
670         meta = self.retrieve_account_metadata(restricted=True)
671         prefix = 'x-account-meta-'
672         for k,v in meta.items():
673             k = '%s%s' % (prefix, k)
674             headers[k] = v
675         return self.post(path, headers=headers)
676     
677     # Storage Container Services
678     
679     def list_objects(self, container, format='text',
680                      limit=None, marker=None, prefix=None, delimiter=None,
681                      path=None, shared=False, include_trashed=False, params={},
682                      if_modified_since=None, if_unmodified_since=None, meta='',
683                      until=None, account=None):
684         """returns a list with the container objects"""
685         account = account or self.account
686         params = {'until':until, 'meta':meta}
687         if shared:
688             params['shared'] = None
689         args = locals().copy()
690         for elem in ['self', 'container', 'params', 'until', 'meta']:
691             args.pop(elem)
692         return OOS_Client.list_objects(self, container, params=params, **args)
693     
694     def retrieve_container_metadata(self, container, restricted=False,
695                                     until=None, account=None):
696         """returns container's metadata"""
697         account = account or self.account
698         params = {'until':until} if until else {}
699         return OOS_Client.retrieve_container_metadata(self, container,
700                                                       account=account,
701                                                       restricted=restricted,
702                                                       **params)
703     
704     def set_container_policies(self, container, account=None,
705                                **policies):
706         """sets containers policies"""
707         account = account or self.account
708         path = '/%s/%s' % (account, container)
709         headers = {}
710         for key, val in policies.items():
711             headers['x-container-policy-%s' % key] = val
712         return self.post(path, headers=headers)
713     
714     def delete_container(self, container, until=None, account=None):
715         """deletes a container or the container history until the date provided"""
716         account = account or self.account
717         params = {'until':until} if until else {}
718         return OOS_Client.delete_container(self, container, account=account,
719                                            params=params)
720     
721     # Storage Object Services
722     
723     def retrieve_object(self, container, object, params={}, format='text',
724                         range=None, if_range=None,
725                         if_match=None, if_none_match=None,
726                         if_modified_since=None, if_unmodified_since=None,
727                         account=None, **headers):
728         """returns an object"""
729         account = account or self.account
730         headers={}
731         l = ['range', 'if_range', 'if_match', 'if_none_match',
732              'if_modified_since', 'if_unmodified_since']
733         l = [elem for elem in l if eval(elem)]
734         for elem in l:
735             headers.update({elem:eval(elem)})
736         if format != 'text':
737             params['hashmap'] = None
738         return OOS_Client.retrieve_object(self, container, object,
739                                           account=account, format=format,
740                                           params=params, **headers)
741     
742     def retrieve_object_version(self, container, object, version,
743                                 format='text', range=None, if_range=None,
744                                 if_match=None, if_none_match=None,
745                                 if_modified_since=None, if_unmodified_since=None,
746                                 account=None):
747         """returns a specific object version"""
748         account = account or self.account
749         args = locals().copy()
750         l = ['self', 'container', 'object']
751         for elem in l:
752             args.pop(elem)
753         params = {'version':version}
754         return self.retrieve_object(container, object, params=params, **args)
755     
756     def retrieve_object_versionlist(self, container, object, range=None,
757                                     if_range=None, if_match=None,
758                                     if_none_match=None, if_modified_since=None,
759                                     if_unmodified_since=None, account=None):
760         """returns the object version list"""
761         account = account or self.account
762         args = locals().copy()
763         l = ['self', 'container', 'object']
764         for elem in l:
765             args.pop(elem)
766         
767         return self.retrieve_object_version(container, object, version='list',
768                                             format='json', **args)
769     
770     def create_zero_length_object(self, container, object,
771                                   meta={}, etag=None, content_type=None,
772                                   content_encoding=None,
773                                   content_disposition=None,
774                                   x_object_manifest=None, x_object_sharing=None,
775                                   x_object_public=None, account=None):
776         """createas a zero length object"""
777         account = account or self.account
778         args = locals().copy()
779         for elem in ['self', 'container', 'object']:
780             args.pop(elem)
781         return OOS_Client.create_zero_length_object(self, container, object,
782                                                     **args)
783     
784     def create_object(self, container, object, f=stdin, format='text',
785                       meta={}, params={}, etag=None, content_type=None,
786                       content_encoding=None, content_disposition=None,
787                       x_object_manifest=None, x_object_sharing=None,
788                       x_object_public=None, account=None):
789         """creates an object"""
790         account = account or self.account
791         args = locals().copy()
792         for elem in ['self', 'container', 'object']:
793             args.pop(elem)
794         if format != 'text':
795             params.update({'hashmap':None})
796         return OOS_Client.create_object(self, container, object, **args)
797         
798     def create_object_using_chunks(self, container, object,
799                                    f=stdin, blocksize=1024, meta={}, etag=None,
800                                    content_type=None, content_encoding=None,
801                                    content_disposition=None,
802                                    x_object_sharing=None, x_object_manifest=None,
803                                    x_object_public=None, account=None):
804         """creates an object (incremental upload)"""
805         account = account or self.account
806         path = '/%s/%s/%s' % (account, container, object)
807         headers = {}
808         l = ['etag', 'content_type', 'content_encoding', 'content_disposition', 
809              'x_object_sharing', 'x_object_manifest', 'x_object_public']
810         l = [elem for elem in l if eval(elem)]
811         for elem in l:
812             headers.update({elem:eval(elem)})
813         headers.setdefault('content-type', 'application/octet-stream')
814         
815         for k,v in meta.items():
816             v = v.strip()
817             headers['x-object-meta-%s' %k.strip()] = v
818         
819         return self._chunked_transfer(path, 'PUT', f, headers=headers,
820                                       blocksize=blocksize)
821     
822     def create_object_by_hashmap(self, container, object, hashmap={},
823                                  meta={}, etag=None, content_encoding=None,
824                                  content_disposition=None, content_type=None,
825                                  x_object_sharing=None, x_object_manifest=None,
826                                  x_object_public = None, account=None):
827         """creates an object by uploading hashes representing data instead of data"""
828         account = account or self.account
829         args = locals().copy()
830         for elem in ['self', 'container', 'object', 'hashmap']:
831             args.pop(elem)
832             
833         try:
834             data = json.dumps(hashmap)
835         except SyntaxError:
836             raise Fault('Invalid formatting')
837         args['params'] = {'hashmap':None}
838         args['format'] = 'json'
839         
840         return self.create_object(container, object, f=StringIO(data), **args)
841     
842     def create_manifestation(self, container, object, manifest, account=None):
843         """creates a manifestation"""
844         account = account or self.account
845         headers={'x_object_manifest':manifest}
846         return self.create_object(container, object, f=None, account=account,
847                                   **headers)
848     
849     def update_object(self, container, object, f=stdin,
850                       offset=None, meta={}, replace=False, content_length=None,
851                       content_type=None, content_range=None,
852                       content_encoding=None, content_disposition=None,
853                       x_object_bytes=None, x_object_manifest=None,
854                       x_object_sharing=None, x_object_public=None,
855                       x_source_object=None, account=None):
856         """updates an object"""
857         account = account or self.account
858         args = locals().copy()
859         for elem in ['self', 'container', 'object', 'replace']:
860             args.pop(elem)
861         if not replace:
862             args['params'] = {'update':None}
863         return OOS_Client.update_object(self, container, object, **args)
864     
865     def update_object_using_chunks(self, container, object, f=stdin,
866                                    blocksize=1024, offset=None, meta={},
867                                    replace=False, content_type=None, content_encoding=None,
868                                    content_disposition=None, x_object_bytes=None,
869                                    x_object_manifest=None, x_object_sharing=None,
870                                    x_object_public=None, account=None):
871         """updates an object (incremental upload)"""
872         account = account or self.account
873         args = locals().copy()
874         for elem in ['self', 'container', 'object', 'replace']:
875             args.pop(elem)
876         if not replace:
877             args['params'] = {'update':None}
878         return OOS_Client.update_object_using_chunks(self, container, object, **args)
879     
880     def update_from_other_source(self, container, object, source,
881                       offset=None, meta={}, content_range=None,
882                       content_encoding=None, content_disposition=None,
883                       x_object_bytes=None, x_object_manifest=None,
884                       x_object_sharing=None, x_object_public=None, account=None):
885         """updates an object"""
886         account = account or self.account
887         args = locals().copy()
888         for elem in ['self', 'container', 'object', 'source']:
889             args.pop(elem)
890         
891         args['x_source_object'] = source
892         return self.update_object(container, object, f=None, **args)
893     
894     def delete_object(self, container, object, until=None, account=None):
895         """deletes an object or the object history until the date provided"""
896         account = account or self.account
897         params = {'until':until} if until else {}
898         return OOS_Client.delete_object(self, container, object, params, account)
899     
900     def trash_object(self, container, object):
901         """trashes an object"""
902         account = account or self.account
903         path = '/%s/%s' % (container, object)
904         meta = {'trash':'true'}
905         return self._update_metadata(path, 'object', **meta)
906     
907     def restore_object(self, container, object, account=None):
908         """restores a trashed object"""
909         account = account or self.account
910         return self.delete_object_metadata(container, object, account, ['trash'])
911     
912     def publish_object(self, container, object, account=None):
913         """sets a previously created object publicly accessible"""
914         account = account or self.account
915         path = '/%s/%s/%s' % (account, container, object)
916         headers = {}
917         headers['x_object_public'] = True
918         params = {'update':None}
919         return self.post(path, headers=headers, params=params)
920     
921     def unpublish_object(self, container, object, account=None):
922         """unpublish an object"""
923         account = account or self.account
924         path = '/%s/%s/%s' % (account, container, object)
925         headers = {}
926         headers['x_object_public'] = False
927         params = {'update':None}
928         return self.post(path, headers=headers, params=params)
929     
930     def copy_object(self, src_container, src_object, dst_container, dst_object,
931                     meta={}, public=False, version=None, account=None,
932                     content_type=None):
933         """copies an object"""
934         account = account or self.account
935         headers = {}
936         headers['x_object_public'] = public
937         if version:
938             headers['x_source_version'] = version
939         return OOS_Client.copy_object(self, src_container, src_object,
940                                       dst_container, dst_object, meta=meta,
941                                       account=account, content_type=content_type,
942                                       **headers)
943     
944     def move_object(self, src_container, src_object, dst_container,
945                              dst_object, meta={}, public=False,
946                              account=None, content_type=None):
947         """moves an object"""
948         headers = {}
949         headers['x_object_public'] = public
950         return OOS_Client.move_object(self, src_container, src_object,
951                                       dst_container, dst_object, meta=meta,
952                                       account=account, content_type=content_type,
953                                       **headers)
954     
955     def list_shared_by_others(self, limit=None, marker=None, format='text'):
956         """lists other accounts that share objects to the user"""
957         l = ['limit', 'marker']
958         params = {}
959         for elem in [elem for elem in l if eval(elem)]:
960             params[elem] = eval(elem)
961         return self._list('', format, params)
962     
963     def share_object(self, container, object, l, read=True):
964         """gives access(read by default) to an object to a user/group list"""
965         action = 'read' if read else 'write'
966         sharing = '%s=%s' % (action, ','.join(l))
967         self.update_object(container, object, f=None, x_object_sharing=sharing)
968
969 def _encode_headers(headers):
970     h = {}
971     for k, v in headers.items():
972         k = urllib.quote(k)
973         if v and type(v) == types.StringType:
974             v = urllib.quote(v, '/=,-* :"')
975         h[k] = v
976     return h