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