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