e3ce80c915b7fd95839d4a4d7b91bbd5487be7fe
[pithos] / pithos / 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[quote(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         return self.retrieve_object(container, object, format='json', **args)
414     
415     def create_directory_marker(self, container, object, account=None):
416         """creates a dierectory marker"""
417         account = account or self.account
418         if not object:
419             raise Fault('Directory markers have to be nested in a container')
420         h = {'content_type':'application/directory'}
421         return self.create_zero_length_object(container, object, account=account,
422                                               **h)
423     
424     def create_object(self, container, object, f=stdin, format='text', meta={},
425                       params={}, etag=None, content_type=None, content_encoding=None,
426                       content_disposition=None, account=None, **headers):
427         """creates a zero-length object"""
428         account = account or self.account
429         path = '/%s/%s/%s' % (account, container, object)
430         for k, v  in headers.items():
431             if v == None:
432                 headers.pop(k)
433         
434         l = ['etag', 'content_encoding', 'content_disposition', 'content_type']
435         l = [elem for elem in l if eval(elem)]
436         for elem in l:
437             headers.update({elem:eval(elem)})
438         headers.setdefault('content-type', 'application/octet-stream')
439         
440         for k,v in meta.items():
441             headers['x-object-meta-%s' %k.strip()] = v.strip()
442         data = f.read() if f else None
443         return self.put(path, data, format, headers=headers, params=params)
444     
445     def create_zero_length_object(self, container, object, meta={}, etag=None,
446                                   content_type=None, content_encoding=None,
447                                   content_disposition=None, account=None,
448                                   **headers):
449         account = account or self.account
450         args = locals().copy()
451         for elem in ['self', 'container', 'headers', 'account']:
452             args.pop(elem)
453         args.update(headers)
454         return self.create_object(container, account=account, f=None, **args)
455     
456     def update_object(self, container, object, f=stdin,
457                       offset=None, meta={}, params={}, content_length=None,
458                       content_type=None, content_encoding=None,
459                       content_disposition=None,  account=None, **headers):
460         account = account or self.account
461         path = '/%s/%s/%s' % (account, container, object)
462         for k, v  in headers.items():
463             if v == None:
464                 headers.pop(k)
465         
466         l = ['content_encoding', 'content_disposition', 'content_type',
467              'content_length']
468         l = [elem for elem in l if eval(elem)]
469         for elem in l:
470             headers.update({elem:eval(elem)})
471         
472         if 'content_range' not in headers.keys():
473             if offset != None:
474                 headers['content_range'] = 'bytes %s-/*' % offset
475             else:
476                 headers['content_range'] = 'bytes */*'
477             
478         for k,v in meta.items():
479             headers['x-object-meta-%s' %k.strip()] = v.strip()
480         data = f.read() if f else None
481         return self.post(path, data, headers=headers, params=params)
482     
483     def update_object_using_chunks(self, container, object, f=stdin,
484                                    blocksize=1024, offset=None, meta={},
485                                    params={}, content_type=None, content_encoding=None,
486                                    content_disposition=None, account=None, **headers):
487         """updates an object (incremental upload)"""
488         account = account or self.account
489         path = '/%s/%s/%s' % (account, container, object)
490         headers = headers if not headers else {}
491         l = ['content_type', 'content_encoding', 'content_disposition']
492         l = [elem for elem in l if eval(elem)]
493         for elem in l:
494             headers.update({elem:eval(elem)})
495         
496         if offset != None:
497             headers['content_range'] = 'bytes %s-/*' % offset
498         else:
499             headers['content_range'] = 'bytes */*'
500         
501         for k,v in meta.items():
502             v = v.strip()
503             headers['x-object-meta-%s' %k.strip()] = v
504         return self._chunked_transfer(path, 'POST', f, headers=headers,
505                                       blocksize=blocksize, params=params)
506     
507     def _change_obj_location(self, src_container, src_object, dst_container,
508                              dst_object, remove=False, meta={}, account=None,
509                              content_type=None, **headers):
510         account = account or self.account
511         path = '/%s/%s/%s' % (account, dst_container, dst_object)
512         headers = {} if not headers else headers
513         for k, v in meta.items():
514             headers['x-object-meta-%s' % k] = v
515         if remove:
516             headers['x-move-from'] = '/%s/%s' % (src_container, src_object)
517         else:
518             headers['x-copy-from'] = '/%s/%s' % (src_container, src_object)
519         headers['content_length'] = 0
520         if content_type:
521             headers['content_type'] = content_type 
522         return self.put(path, headers=headers)
523     
524     def copy_object(self, src_container, src_object, dst_container, dst_object,
525                    meta={}, account=None, content_type=None, **headers):
526         """copies an object"""
527         account = account or self.account
528         return self._change_obj_location(src_container, src_object,
529                                    dst_container, dst_object, account=account,
530                                    remove=False, meta=meta,
531                                    content_type=content_type, **headers)
532     
533     def move_object(self, src_container, src_object, dst_container,
534                              dst_object, meta={}, account=None,
535                              content_type=None, **headers):
536         """moves an object"""
537         account = account or self.account
538         return self._change_obj_location(src_container, src_object,
539                                          dst_container, dst_object,
540                                          account=account, remove=True,
541                                          meta=meta, content_type=content_type,
542                                          **headers)
543     
544     def delete_object(self, container, object, params={}, account=None):
545         """deletes an object"""
546         account = account or self.account
547         return self.delete('/%s/%s/%s' % (account, container, object),
548                            params=params)
549     
550     def retrieve_object_metadata(self, container, object, restricted=False,
551                                  version=None, account=None):
552         """
553         set restricted to True to get only user defined metadata
554         """
555         account = account or self.account
556         path = '/%s/%s/%s' % (account, container, object)
557         prefix = 'x-object-meta-' if restricted else None
558         params = {'version':version} if version else {}
559         return self._get_metadata(path, prefix, params=params)
560     
561     def update_object_metadata(self, container, object, account=None,
562                                **meta):
563         """
564         updates object's metadata
565         """
566         account = account or self.account
567         path = '/%s/%s/%s' % (account, container, object)
568         return self._update_metadata(path, 'object', **meta)
569     
570     def delete_object_metadata(self, container, object, meta=[], account=None):
571         """
572         deletes object's metadata
573         """
574         account = account or self.account
575         path = '/%s/%s' % (account, container, object)
576         return self._delete_metadata(path, 'object', meta)
577     
578 class Pithos_Client(OOS_Client):
579     """Pithos Storage Client. Extends OOS_Client"""
580     
581     def _update_metadata(self, path, entity, **meta):
582         """
583         adds new and updates the values of previously set metadata
584         """
585         params = {'update':None}
586         headers = {}
587         prefix = 'x-%s-meta-' % entity
588         for k,v in meta.items():
589             k = '%s%s' % (prefix, k)
590             headers[k] = v
591         return self.post(path, headers=headers, params=params)
592     
593     def _delete_metadata(self, path, entity, meta=[]):
594         """
595         delete previously set metadata
596         """
597         params = {'update':None}
598         headers = {}
599         prefix = 'x-%s-meta-' % entity
600         for m in meta:
601             headers['%s%s' % (prefix, m)] = ''
602         return self.post(path, headers=headers, params=params)
603     
604     # Storage Account Services
605     
606     def list_containers(self, format='text', if_modified_since=None,
607                         if_unmodified_since=None, limit=None, marker=None,
608                         shared=False, until=None, account=None):
609         """returns a list with the account containers"""
610         account = account or self.account
611         params = {'until':until} if until else {}
612         if shared:
613             params['shared'] = None
614         headers = {'if-modified-since':if_modified_since,
615                    'if-unmodified-since':if_unmodified_since}
616         return OOS_Client.list_containers(self, account=account, format=format,
617                                           limit=limit, marker=marker,
618                                           params=params, **headers)
619     
620     def retrieve_account_metadata(self, restricted=False, until=None,
621                                   account=None):
622         """returns the account metadata"""
623         account = account or self.account
624         params = {'until':until} if until else {}
625         return OOS_Client.retrieve_account_metadata(self, account=account,
626                                                     restricted=restricted,
627                                                     **params)
628     
629     def set_account_groups(self, account=None, **groups):
630         """create account groups"""
631         account = account or self.account
632         path = '/%s' % account
633         headers = {}
634         for k, v in groups.items():
635             headers['x-account-group-%s' % k] = v
636         params = {'update':None}
637         return self.post(path, headers=headers, params=params)
638     
639     def retrieve_account_groups(self, account=None):
640         """returns the account groups"""
641         account = account or self.account
642         meta = self.retrieve_account_metadata(account=account)
643         prefix = 'x-account-group-'
644         prefixlen = len(prefix)
645         groups = {}
646         for key, val in meta.items():
647             if prefix and not key.startswith(prefix):
648                 continue
649             elif prefix and key.startswith(prefix):
650                 key = key[prefixlen:]
651             groups[key] = val
652         return groups
653     
654     def unset_account_groups(self, groups=[], account=None):
655         """delete account groups"""
656         account = account or self.account
657         path = '/%s' % account
658         headers = {}
659         for elem in groups:
660             headers['x-account-group-%s' % elem] = ''
661         params = {'update':None}
662         return self.post(path, headers=headers, params=params)
663     
664     def reset_account_groups(self, account=None, **groups):
665         """overrides account groups"""
666         account = account or self.account
667         path = '/%s' % account
668         headers = {}
669         for k, v in groups.items():
670             v = v.strip()
671             headers['x-account-group-%s' % k] = v
672         meta = self.retrieve_account_metadata(restricted=True)
673         prefix = 'x-account-meta-'
674         for k,v in meta.items():
675             k = '%s%s' % (prefix, k)
676             headers[k] = v
677         return self.post(path, headers=headers)
678     
679     # Storage Container Services
680     
681     def list_objects(self, container, format='text',
682                      limit=None, marker=None, prefix=None, delimiter=None,
683                      path=None, shared=False, include_trashed=False, params={},
684                      if_modified_since=None, if_unmodified_since=None, meta='',
685                      until=None, account=None):
686         """returns a list with the container objects"""
687         account = account or self.account
688         params = {'until':until, 'meta':meta}
689         if shared:
690             params['shared'] = None
691         args = locals().copy()
692         for elem in ['self', 'container', 'params', 'until', 'meta']:
693             args.pop(elem)
694         return OOS_Client.list_objects(self, container, params=params, **args)
695     
696     def retrieve_container_metadata(self, container, restricted=False,
697                                     until=None, account=None):
698         """returns container's metadata"""
699         account = account or self.account
700         params = {'until':until} if until else {}
701         return OOS_Client.retrieve_container_metadata(self, container,
702                                                       account=account,
703                                                       restricted=restricted,
704                                                       **params)
705     
706     def set_container_policies(self, container, account=None,
707                                **policies):
708         """sets containers policies"""
709         account = account or self.account
710         path = '/%s/%s' % (account, container)
711         headers = {}
712         for key, val in policies.items():
713             headers['x-container-policy-%s' % key] = val
714         return self.post(path, headers=headers)
715     
716     def update_container_data(self, container, f=stdin):
717         """adds blocks of data to the container"""
718         account = self.account
719         path = '/%s/%s' % (account, container)
720         params = {'update': None}
721         headers = {'content_type': 'application/octet-stream'}
722         data = f.read() if f else None
723         headers['content_length'] = len(data)
724         return self.post(path, data, headers=headers, params=params)
725     
726     def delete_container(self, container, until=None, account=None):
727         """deletes a container or the container history until the date provided"""
728         account = account or self.account
729         params = {'until':until} if until else {}
730         return OOS_Client.delete_container(self, container, account=account,
731                                            params=params)
732     
733     # Storage Object Services
734     
735     def retrieve_object(self, container, object, params={}, format='text',
736                         range=None, if_range=None,
737                         if_match=None, if_none_match=None,
738                         if_modified_since=None, if_unmodified_since=None,
739                         account=None, **headers):
740         """returns an object"""
741         account = account or self.account
742         headers={}
743         l = ['range', 'if_range', 'if_match', 'if_none_match',
744              'if_modified_since', 'if_unmodified_since']
745         l = [elem for elem in l if eval(elem)]
746         for elem in l:
747             headers.update({elem:eval(elem)})
748         if format != 'text':
749             params['hashmap'] = None
750         return OOS_Client.retrieve_object(self, container, object,
751                                           account=account, format=format,
752                                           params=params, **headers)
753     
754     def retrieve_object_version(self, container, object, version,
755                                 format='text', range=None, if_range=None,
756                                 if_match=None, if_none_match=None,
757                                 if_modified_since=None, if_unmodified_since=None,
758                                 account=None):
759         """returns a specific object version"""
760         account = account or self.account
761         args = locals().copy()
762         l = ['self', 'container', 'object']
763         for elem in l:
764             args.pop(elem)
765         params = {'version':version}
766         return self.retrieve_object(container, object, params=params, **args)
767     
768     def retrieve_object_versionlist(self, container, object, range=None,
769                                     if_range=None, if_match=None,
770                                     if_none_match=None, if_modified_since=None,
771                                     if_unmodified_since=None, account=None):
772         """returns the object version list"""
773         account = account or self.account
774         args = locals().copy()
775         l = ['self', 'container', 'object']
776         for elem in l:
777             args.pop(elem)
778         
779         return self.retrieve_object_version(container, object, version='list',
780                                             format='json', **args)
781     
782     def create_zero_length_object(self, container, object,
783                                   meta={}, etag=None, content_type=None,
784                                   content_encoding=None,
785                                   content_disposition=None,
786                                   x_object_manifest=None, x_object_sharing=None,
787                                   x_object_public=None, account=None):
788         """createas a zero length object"""
789         account = account or self.account
790         args = locals().copy()
791         for elem in ['self', 'container', 'object']:
792             args.pop(elem)
793         return OOS_Client.create_zero_length_object(self, container, object,
794                                                     **args)
795     
796     def create_object(self, container, object, f=stdin, format='text',
797                       meta={}, params={}, etag=None, content_type=None,
798                       content_encoding=None, content_disposition=None,
799                       x_object_manifest=None, x_object_sharing=None,
800                       x_object_public=None, account=None):
801         """creates an object"""
802         account = account or self.account
803         args = locals().copy()
804         for elem in ['self', 'container', 'object']:
805             args.pop(elem)
806         if format != 'text':
807             params.update({'hashmap':None})
808         return OOS_Client.create_object(self, container, object, **args)
809         
810     def create_object_using_chunks(self, container, object,
811                                    f=stdin, blocksize=1024, meta={}, etag=None,
812                                    content_type=None, content_encoding=None,
813                                    content_disposition=None,
814                                    x_object_sharing=None, x_object_manifest=None,
815                                    x_object_public=None, account=None):
816         """creates an object (incremental upload)"""
817         account = account or self.account
818         path = '/%s/%s/%s' % (account, container, object)
819         headers = {}
820         l = ['etag', 'content_type', 'content_encoding', 'content_disposition', 
821              'x_object_sharing', 'x_object_manifest', 'x_object_public']
822         l = [elem for elem in l if eval(elem)]
823         for elem in l:
824             headers.update({elem:eval(elem)})
825         headers.setdefault('content-type', 'application/octet-stream')
826         
827         for k,v in meta.items():
828             v = v.strip()
829             headers['x-object-meta-%s' %k.strip()] = v
830         
831         return self._chunked_transfer(path, 'PUT', f, headers=headers,
832                                       blocksize=blocksize)
833     
834     def create_object_by_hashmap(self, container, object, hashmap={},
835                                  meta={}, etag=None, content_encoding=None,
836                                  content_disposition=None, content_type=None,
837                                  x_object_sharing=None, x_object_manifest=None,
838                                  x_object_public = None, account=None):
839         """creates an object by uploading hashes representing data instead of data"""
840         account = account or self.account
841         args = locals().copy()
842         for elem in ['self', 'container', 'object', 'hashmap']:
843             args.pop(elem)
844             
845         try:
846             data = json.dumps(hashmap)
847         except SyntaxError:
848             raise Fault('Invalid formatting')
849         args['params'] = {'hashmap':None}
850         args['format'] = 'json'
851         
852         return self.create_object(container, object, f=StringIO(data), **args)
853     
854     def create_manifestation(self, container, object, manifest, account=None):
855         """creates a manifestation"""
856         account = account or self.account
857         headers={'x_object_manifest':manifest}
858         return self.create_object(container, object, f=None, account=account,
859                                   **headers)
860     
861     def update_object(self, container, object, f=stdin,
862                       offset=None, meta={}, replace=False, content_length=None,
863                       content_type=None, content_range=None,
864                       content_encoding=None, content_disposition=None,
865                       x_object_bytes=None, x_object_manifest=None,
866                       x_object_sharing=None, x_object_public=None,
867                       x_source_object=None, account=None):
868         """updates an object"""
869         account = account or self.account
870         args = locals().copy()
871         for elem in ['self', 'container', 'object', 'replace']:
872             args.pop(elem)
873         if not replace:
874             args['params'] = {'update':None}
875         return OOS_Client.update_object(self, container, object, **args)
876     
877     def update_object_using_chunks(self, container, object, f=stdin,
878                                    blocksize=1024, offset=None, meta={},
879                                    replace=False, content_type=None, content_encoding=None,
880                                    content_disposition=None, x_object_bytes=None,
881                                    x_object_manifest=None, x_object_sharing=None,
882                                    x_object_public=None, account=None):
883         """updates an object (incremental upload)"""
884         account = account or self.account
885         args = locals().copy()
886         for elem in ['self', 'container', 'object', 'replace']:
887             args.pop(elem)
888         if not replace:
889             args['params'] = {'update':None}
890         return OOS_Client.update_object_using_chunks(self, container, object, **args)
891     
892     def update_from_other_source(self, container, object, source,
893                       offset=None, meta={}, content_range=None,
894                       content_encoding=None, content_disposition=None,
895                       x_object_bytes=None, x_object_manifest=None,
896                       x_object_sharing=None, x_object_public=None, account=None):
897         """updates an object"""
898         account = account or self.account
899         args = locals().copy()
900         for elem in ['self', 'container', 'object', 'source']:
901             args.pop(elem)
902         
903         args['x_source_object'] = source
904         return self.update_object(container, object, f=None, **args)
905     
906     def delete_object(self, container, object, until=None, account=None):
907         """deletes an object or the object history until the date provided"""
908         account = account or self.account
909         params = {'until':until} if until else {}
910         return OOS_Client.delete_object(self, container, object, params, account)
911     
912     def trash_object(self, container, object):
913         """trashes an object"""
914         account = account or self.account
915         path = '/%s/%s' % (container, object)
916         meta = {'trash':'true'}
917         return self._update_metadata(path, 'object', **meta)
918     
919     def restore_object(self, container, object, account=None):
920         """restores a trashed object"""
921         account = account or self.account
922         return self.delete_object_metadata(container, object, account, ['trash'])
923     
924     def publish_object(self, container, object, account=None):
925         """sets a previously created object publicly accessible"""
926         account = account or self.account
927         path = '/%s/%s/%s' % (account, container, object)
928         headers = {}
929         headers['x_object_public'] = True
930         params = {'update':None}
931         return self.post(path, headers=headers, params=params)
932     
933     def unpublish_object(self, container, object, account=None):
934         """unpublish an object"""
935         account = account or self.account
936         path = '/%s/%s/%s' % (account, container, object)
937         headers = {}
938         headers['x_object_public'] = False
939         params = {'update':None}
940         return self.post(path, headers=headers, params=params)
941     
942     def copy_object(self, src_container, src_object, dst_container, dst_object,
943                     meta={}, public=False, version=None, account=None,
944                     content_type=None):
945         """copies an object"""
946         account = account or self.account
947         headers = {}
948         headers['x_object_public'] = public
949         if version:
950             headers['x_source_version'] = version
951         return OOS_Client.copy_object(self, src_container, src_object,
952                                       dst_container, dst_object, meta=meta,
953                                       account=account, content_type=content_type,
954                                       **headers)
955     
956     def move_object(self, src_container, src_object, dst_container,
957                              dst_object, meta={}, public=False,
958                              account=None, content_type=None):
959         """moves an object"""
960         headers = {}
961         headers['x_object_public'] = public
962         return OOS_Client.move_object(self, src_container, src_object,
963                                       dst_container, dst_object, meta=meta,
964                                       account=account, content_type=content_type,
965                                       **headers)
966     
967     def list_shared_by_others(self, limit=None, marker=None, format='text'):
968         """lists other accounts that share objects to the user"""
969         l = ['limit', 'marker']
970         params = {}
971         for elem in [elem for elem in l if eval(elem)]:
972             params[elem] = eval(elem)
973         return self._list('', format, params)
974     
975     def share_object(self, container, object, l, read=True):
976         """gives access(read by default) to an object to a user/group list"""
977         action = 'read' if read else 'write'
978         sharing = '%s=%s' % (action, ','.join(l))
979         self.update_object(container, object, f=None, x_object_sharing=sharing)