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