Revision 2715ade4 snf-pithos-tools/pithos/tools/lib/client.py

b/snf-pithos-tools/pithos/tools/lib/client.py
1 1
# Copyright 2011-2012 GRNET S.A. All rights reserved.
2
# 
2
#
3 3
# Redistribution and use in source and binary forms, with or
4 4
# without modification, are permitted provided that the following
5 5
# conditions are met:
6
# 
6
#
7 7
#   1. Redistributions of source code must retain the above
8 8
#      copyright notice, this list of conditions and the following
9 9
#      disclaimer.
10
# 
10
#
11 11
#   2. Redistributions in binary form must reproduce the above
12 12
#      copyright notice, this list of conditions and the following
13 13
#      disclaimer in the documentation and/or other materials
14 14
#      provided with the distribution.
15
# 
15
#
16 16
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
17 17
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18 18
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
......
25 25
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
26 26
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27 27
# POSSIBILITY OF SUCH DAMAGE.
28
# 
28
#
29 29
# The views and conclusions contained in the software and
30 30
# documentation are those of the authors and should not be
31 31
# interpreted as representing official policies, either expressed
......
44 44
import urllib
45 45
import datetime
46 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'}
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

  
60 61

  
61 62
class Fault(Exception):
62 63
    def __init__(self, data='', status=None):
......
66 67
        self.data = data
67 68
        self.status = status
68 69

  
70

  
69 71
class Client(object):
70 72
    def __init__(self, url, token, account, verbose=False, debug=False):
71 73
        """`url` can also include a port, e.g '127.0.0.1:8000'."""
72
        
74

  
73 75
        self.url = url
74 76
        self.account = account
75 77
        self.verbose = verbose or debug
76 78
        self.debug = debug
77 79
        self.token = token
78
    
80

  
79 81
    def _req(self, method, path, body=None, headers={}, format='text', params={}):
80 82
        p = urlparse(self.url)
81 83
        if p.scheme == 'http':
......
84 86
            conn = HTTPSConnection(p.netloc)
85 87
        else:
86 88
            raise Exception('Unknown URL scheme')
87
        
89

  
88 90
        full_path = _prepare_path(p.path + path, format, params)
89
        
91

  
90 92
        kwargs = {}
91 93
        kwargs['headers'] = _prepare_headers(headers)
92 94
        kwargs['headers']['X-Auth-Token'] = self.token
93 95
        if body:
94 96
            kwargs['body'] = body
95
            kwargs['headers'].setdefault('content-type', 'application/octet-stream')
96
        kwargs['headers'].setdefault('content-length', len(body) if body else 0)
97
        
97
            kwargs['headers'].setdefault(
98
                'content-type', 'application/octet-stream')
99
        kwargs['headers'].setdefault('content-length', len(body)
100
                                     if body else 0)
101

  
98 102
        #print '#', method, full_path, kwargs
99 103
        #t1 = datetime.datetime.utcnow()
100 104
        conn.request(method, full_path, **kwargs)
101
        
105

  
102 106
        resp = conn.getresponse()
103 107
        #t2 = datetime.datetime.utcnow()
104 108
        #print 'response time:', str(t2-t1)
105 109
        return _handle_response(resp, self.verbose, self.debug)
106
    
110

  
107 111
    def _chunked_transfer(self, path, method='PUT', f=stdin, headers=None,
108 112
                          blocksize=1024, params={}):
109 113
        """perfomrs a chunked request"""
......
114 118
            conn = HTTPSConnection(p.netloc)
115 119
        else:
116 120
            raise Exception('Unknown URL scheme')
117
        
121

  
118 122
        full_path = _prepare_path(p.path + path, params=params)
119
        
123

  
120 124
        headers.setdefault('content-type', 'application/octet-stream')
121
        
125

  
122 126
        conn.putrequest(method, full_path)
123 127
        conn.putheader('x-auth-token', self.token)
124 128
        conn.putheader('transfer-encoding', 'chunked')
125
        for k,v in _prepare_headers(headers).items():
129
        for k, v in _prepare_headers(headers).items():
126 130
            conn.putheader(k, v)
127 131
        conn.endheaders()
128
        
132

  
129 133
        # write body
130 134
        data = ''
131 135
        while True:
......
146 150
        except:
147 151
            #retry
148 152
            conn.send(data)
149
        
153

  
150 154
        resp = conn.getresponse()
151 155
        return _handle_response(resp, self.verbose, self.debug)
152
    
156

  
153 157
    def delete(self, path, format='text', params={}):
154 158
        return self._req('DELETE', path, format=format, params=params)
155
    
159

  
156 160
    def get(self, path, format='text', headers={}, params={}):
157 161
        return self._req('GET', path, headers=headers, format=format,
158
                        params=params)
159
    
162
                         params=params)
163

  
160 164
    def head(self, path, format='text', params={}):
161
         return self._req('HEAD', path, format=format, params=params)
162
    
165
        return self._req('HEAD', path, format=format, params=params)
166

  
163 167
    def post(self, path, body=None, format='text', headers=None, params={}):
164 168
        return self._req('POST', path, body, headers=headers, format=format,
165
                        params=params)
166
    
169
                         params=params)
170

  
167 171
    def put(self, path, body=None, format='text', headers=None, params={}):
168 172
        return self._req('PUT', path, body, headers=headers, format=format,
169 173
                         params=params)
170
    
174

  
171 175
    def _list(self, path, format='text', params={}, **headers):
172 176
        status, headers, data = self.get(path, format=format, headers=headers,
173 177
                                         params=params)
......
178 182
        else:
179 183
            data = data.split('\n')[:-1] if data else ''
180 184
        return data
181
    
185

  
182 186
    def _get_metadata(self, path, prefix=None, params={}):
183 187
        status, headers, data = self.head(path, params=params)
184 188
        prefixlen = len(prefix) if prefix else 0
......
190 194
                key = key[prefixlen:]
191 195
            meta[key] = val
192 196
        return meta
193
    
197

  
194 198
    def _filter(self, l, d):
195 199
        """
196 200
        filter out from l elements having the metadata values provided
197 201
        """
198 202
        ll = l
199 203
        for elem in l:
200
            if type(elem) == types.DictionaryType:
204
            if isinstance(elem, types.DictionaryType):
201 205
                for key in d.keys():
202 206
                    k = 'x_object_meta_%s' % key
203 207
                    if k in elem.keys() and elem[k] == d[key]:
204 208
                        ll.remove(elem)
205 209
                        break
206 210
        return ll
207
    
211

  
212

  
208 213
class OOS_Client(Client):
209 214
    """Openstack Object Storage Client"""
210
    
215

  
211 216
    def _update_metadata(self, path, entity, **meta):
212 217
        """adds new and updates the values of previously set metadata"""
213 218
        ex_meta = self.retrieve_account_metadata(restricted=True)
214 219
        ex_meta.update(meta)
215 220
        headers = {}
216 221
        prefix = 'x-%s-meta-' % entity
217
        for k,v in ex_meta.items():
222
        for k, v in ex_meta.items():
218 223
            k = '%s%s' % (prefix, k)
219 224
            headers[k] = v
220 225
        return self.post(path, headers=headers)
221
    
226

  
222 227
    def _reset_metadata(self, path, entity, **meta):
223 228
        """
224 229
        overwrites all user defined metadata
225 230
        """
226 231
        headers = {}
227 232
        prefix = 'x-%s-meta-' % entity
228
        for k,v in meta.items():
233
        for k, v in meta.items():
229 234
            k = '%s%s' % (prefix, k)
230 235
            headers[k] = v
231 236
        return self.post(path, headers=headers)
232
    
237

  
233 238
    def _delete_metadata(self, path, entity, meta=[]):
234 239
        """delete previously set metadata"""
235 240
        ex_meta = self.retrieve_account_metadata(restricted=True)
......
239 244
            if k in meta:
240 245
                headers['%s%s' % (prefix, k)] = ex_meta[k]
241 246
        return self.post(path, headers=headers)
242
    
247

  
243 248
    # Storage Account Services
244
    
249

  
245 250
    def list_containers(self, format='text', limit=None,
246 251
                        marker=None, params={}, account=None, **headers):
247 252
        """lists containers"""
248 253
        account = account or self.account
249 254
        path = '/%s' % account
250
        params.update({'limit':limit, 'marker':marker})
255
        params.update({'limit': limit, 'marker': marker})
251 256
        return self._list(path, format, params, **headers)
252
    
257

  
253 258
    def retrieve_account_metadata(self, restricted=False, account=None, **params):
254 259
        """returns the account metadata"""
255 260
        account = account or self.account
256 261
        path = '/%s' % account
257 262
        prefix = 'x-account-meta-' if restricted else None
258 263
        return self._get_metadata(path, prefix, params)
259
    
264

  
260 265
    def update_account_metadata(self, account=None, **meta):
261 266
        """updates the account metadata"""
262 267
        account = account or self.account
263 268
        path = '/%s' % account
264 269
        return self._update_metadata(path, 'account', **meta)
265
        
270

  
266 271
    def delete_account_metadata(self, meta=[], account=None):
267 272
        """deletes the account metadata"""
268 273
        account = account or self.account
269 274
        path = '/%s' % account
270 275
        return self._delete_metadata(path, 'account', meta)
271
    
276

  
272 277
    def reset_account_metadata(self, account=None, **meta):
273 278
        """resets account metadata"""
274 279
        account = account or self.account
275 280
        path = '/%s' % account
276 281
        return self._reset_metadata(path, 'account', **meta)
277
    
282

  
278 283
    # Storage Container Services
279
    
284

  
280 285
    def _filter_trashed(self, l):
281
        return self._filter(l, {'trash':'true'})
282
    
286
        return self._filter(l, {'trash': 'true'})
287

  
283 288
    def list_objects(self, container, format='text',
284 289
                     limit=None, marker=None, prefix=None, delimiter=None,
285 290
                     path=None, include_trashed=False, params={}, account=None,
286 291
                     **headers):
287 292
        """returns a list with the container objects"""
288 293
        account = account or self.account
289
        params.update({'limit':limit, 'marker':marker, 'prefix':prefix,
290
                       'delimiter':delimiter, 'path':path})
294
        params.update({'limit': limit, 'marker': marker, 'prefix': prefix,
295
                       'delimiter': delimiter, 'path': path})
291 296
        l = self._list('/%s/%s' % (account, container), format, params,
292 297
                       **headers)
293 298
        #TODO support filter trashed with xml also
294 299
        if format != 'xml' and not include_trashed:
295 300
            l = self._filter_trashed(l)
296 301
        return l
297
    
302

  
298 303
    def create_container(self, container, account=None, meta={}, **headers):
299 304
        """creates a container"""
300 305
        account = account or self.account
301 306
        if not headers:
302 307
            headers = {}
303
        for k,v in meta.items():
304
            headers['x-container-meta-%s' %k.strip().upper()] = v.strip()
308
        for k, v in meta.items():
309
            headers['x-container-meta-%s' % k.strip().upper()] = v.strip()
305 310
        status, header, data = self.put('/%s/%s' % (account, container),
306 311
                                        headers=headers)
307 312
        if status == 202:
......
309 314
        elif status != 201:
310 315
            raise Fault(data, int(status))
311 316
        return True
312
    
317

  
313 318
    def delete_container(self, container, params={}, account=None):
314 319
        """deletes a container"""
315 320
        account = account or self.account
316 321
        return self.delete('/%s/%s' % (account, container), params=params)
317
    
322

  
318 323
    def retrieve_container_metadata(self, container, restricted=False,
319 324
                                    account=None, **params):
320 325
        """returns the container metadata"""
......
322 327
        prefix = 'x-container-meta-' if restricted else None
323 328
        return self._get_metadata('/%s/%s' % (account, container), prefix,
324 329
                                  params)
325
    
330

  
326 331
    def update_container_metadata(self, container, account=None, **meta):
327 332
        """unpdates the container metadata"""
328 333
        account = account or self.account
329 334
        return self._update_metadata('/%s/%s' % (account, container),
330 335
                                     'container', **meta)
331
        
336

  
332 337
    def delete_container_metadata(self, container, meta=[], account=None):
333 338
        """deletes the container metadata"""
334 339
        account = account or self.account
335 340
        path = '/%s/%s' % (account, container)
336 341
        return self._delete_metadata(path, 'container', meta)
337
    
342

  
338 343
    # Storage Object Services
339
    
344

  
340 345
    def request_object(self, container, object, format='text', params={},
341 346
                       account=None, **headers):
342 347
        """returns tuple containing the status, headers and data response for an object request"""
......
344 349
        path = '/%s/%s/%s' % (account, container, object)
345 350
        status, headers, data = self.get(path, format, headers, params)
346 351
        return status, headers, data
347
    
352

  
348 353
    def retrieve_object(self, container, object, format='text', params={},
349 354
                        account=None, **headers):
350 355
        """returns an object's data"""
......
357 362
        elif format == 'xml':
358 363
            data = minidom.parseString(data)
359 364
        return data
360
    
361
    def retrieve_object_hashmap(self, container, object, format='json', params={},
362
                        account=None, **headers):
365

  
366
    def retrieve_object_hashmap(
367
        self, container, object, format='json', params={},
368
            account=None, **headers):
363 369
        """returns the hashmap representing object's data"""
364 370
        if not params:
365 371
            params = {}
366
        params.update({'hashmap':None})
372
        params.update({'hashmap': None})
367 373
        return self.retrieve_object(container, object, params, format, account, **headers)
368
    
374

  
369 375
    def create_directory_marker(self, container, object, account=None):
370 376
        """creates a dierectory marker"""
371 377
        account = account or self.account
372 378
        if not object:
373 379
            raise Fault('Directory markers have to be nested in a container')
374
        h = {'content_type':'application/directory'}
375
        return self.create_zero_length_object(container, object, account=account,
376
                                              **h)
377
    
380
        h = {'content_type': 'application/directory'}
381
        return self.create_zero_length_object(
382
            container, object, account=account,
383
            **h)
384

  
378 385
    def create_object(self, container, object, f=stdin, format='text', meta={},
379 386
                      params={}, etag=None, content_type=None, content_encoding=None,
380 387
                      content_disposition=None, account=None, **headers):
381 388
        """creates a zero-length object"""
382 389
        account = account or self.account
383 390
        path = '/%s/%s/%s' % (account, container, object)
384
        for k, v  in headers.items():
385
            if v == None:
391
        for k, v in headers.items():
392
            if v is None:
386 393
                headers.pop(k)
387
        
394

  
388 395
        l = ['etag', 'content_encoding', 'content_disposition', 'content_type']
389 396
        l = [elem for elem in l if eval(elem)]
390 397
        for elem in l:
391
            headers.update({elem:eval(elem)})
398
            headers.update({elem: eval(elem)})
392 399
        headers.setdefault('content-type', 'application/octet-stream')
393
        
394
        for k,v in meta.items():
395
            headers['x-object-meta-%s' %k.strip()] = v.strip()
400

  
401
        for k, v in meta.items():
402
            headers['x-object-meta-%s' % k.strip()] = v.strip()
396 403
        data = f.read() if f else None
397 404
        return self.put(path, data, format, headers=headers, params=params)
398
    
405

  
399 406
    def create_zero_length_object(self, container, object, meta={}, etag=None,
400 407
                                  content_type=None, content_encoding=None,
401 408
                                  content_disposition=None, account=None,
......
406 413
            args.pop(elem)
407 414
        args.update(headers)
408 415
        return self.create_object(container, account=account, f=None, **args)
409
    
416

  
410 417
    def update_object(self, container, object, f=stdin,
411 418
                      offset=None, meta={}, params={}, content_length=None,
412 419
                      content_type=None, content_encoding=None,
413
                      content_disposition=None,  account=None, **headers):
420
                      content_disposition=None, account=None, **headers):
414 421
        account = account or self.account
415 422
        path = '/%s/%s/%s' % (account, container, object)
416
        for k, v  in headers.items():
417
            if v == None:
423
        for k, v in headers.items():
424
            if v is None:
418 425
                headers.pop(k)
419
        
426

  
420 427
        l = ['content_encoding', 'content_disposition', 'content_type',
421 428
             'content_length']
422 429
        l = [elem for elem in l if eval(elem)]
423 430
        for elem in l:
424
            headers.update({elem:eval(elem)})
425
        
431
            headers.update({elem: eval(elem)})
432

  
426 433
        if 'content_range' not in headers.keys():
427
            if offset != None:
434
            if offset is not None:
428 435
                headers['content_range'] = 'bytes %s-/*' % offset
429 436
            else:
430 437
                headers['content_range'] = 'bytes */*'
431
            
432
        for k,v in meta.items():
433
            headers['x-object-meta-%s' %k.strip()] = v.strip()
438

  
439
        for k, v in meta.items():
440
            headers['x-object-meta-%s' % k.strip()] = v.strip()
434 441
        data = f.read() if f else None
435 442
        return self.post(path, data, headers=headers, params=params)
436
    
443

  
437 444
    def update_object_using_chunks(self, container, object, f=stdin,
438 445
                                   blocksize=1024, offset=None, meta={},
439 446
                                   params={}, content_type=None, content_encoding=None,
......
445 452
        l = ['content_type', 'content_encoding', 'content_disposition']
446 453
        l = [elem for elem in l if eval(elem)]
447 454
        for elem in l:
448
            headers.update({elem:eval(elem)})
449
        
450
        if offset != None:
455
            headers.update({elem: eval(elem)})
456

  
457
        if offset is not None:
451 458
            headers['content_range'] = 'bytes %s-/*' % offset
452 459
        else:
453 460
            headers['content_range'] = 'bytes */*'
454
        
455
        for k,v in meta.items():
461

  
462
        for k, v in meta.items():
456 463
            v = v.strip()
457
            headers['x-object-meta-%s' %k.strip()] = v
464
            headers['x-object-meta-%s' % k.strip()] = v
458 465
        return self._chunked_transfer(path, 'POST', f, headers=headers,
459 466
                                      blocksize=blocksize, params=params)
460
    
467

  
461 468
    def _change_obj_location(self, src_container, src_object, dst_container,
462 469
                             dst_object, remove=False, meta={}, account=None,
463 470
                             content_type=None, delimiter=None, **headers):
......
477 484
        else:
478 485
            params['ignore_content_type'] = ''
479 486
        if delimiter:
480
        	params['delimiter'] = delimiter
487
            params['delimiter'] = delimiter
481 488
        return self.put(path, headers=headers, params=params)
482
    
489

  
483 490
    def copy_object(self, src_container, src_object, dst_container, dst_object,
484
                   meta={}, account=None, content_type=None, delimiter=None, **headers):
491
                    meta={}, account=None, content_type=None, delimiter=None, **headers):
485 492
        """copies an object"""
486 493
        account = account or self.account
487 494
        return self._change_obj_location(src_container, src_object,
488
                                   dst_container, dst_object, account=account,
489
                                   remove=False, meta=meta,
490
                                   content_type=content_type, delimiter=delimiter, **headers)
491
    
495
                                         dst_container, dst_object, account=account,
496
                                         remove=False, meta=meta,
497
                                         content_type=content_type, delimiter=delimiter, **headers)
498

  
492 499
    def move_object(self, src_container, src_object, dst_container,
493
                             dst_object, meta={}, account=None,
494
                             content_type=None, **headers):
500
                    dst_object, meta={}, account=None,
501
                    content_type=None, **headers):
495 502
        """moves an object"""
496 503
        account = account or self.account
497 504
        return self._change_obj_location(src_container, src_object,
......
499 506
                                         account=account, remove=True,
500 507
                                         meta=meta, content_type=content_type,
501 508
                                         **headers)
502
    
509

  
503 510
    def delete_object(self, container, object, params={}, account=None):
504 511
        """deletes an object"""
505 512
        account = account or self.account
506 513
        return self.delete('/%s/%s/%s' % (account, container, object),
507 514
                           params=params)
508
    
515

  
509 516
    def retrieve_object_metadata(self, container, object, restricted=False,
510 517
                                 version=None, account=None):
511 518
        """
......
514 521
        account = account or self.account
515 522
        path = '/%s/%s/%s' % (account, container, object)
516 523
        prefix = 'x-object-meta-' if restricted else None
517
        params = {'version':version} if version else {}
524
        params = {'version': version} if version else {}
518 525
        return self._get_metadata(path, prefix, params=params)
519
    
526

  
520 527
    def update_object_metadata(self, container, object, account=None,
521 528
                               **meta):
522 529
        """
......
525 532
        account = account or self.account
526 533
        path = '/%s/%s/%s' % (account, container, object)
527 534
        return self._update_metadata(path, 'object', **meta)
528
    
535

  
529 536
    def delete_object_metadata(self, container, object, meta=[], account=None):
530 537
        """
531 538
        deletes object's metadata
......
533 540
        account = account or self.account
534 541
        path = '/%s/%s' % (account, container, object)
535 542
        return self._delete_metadata(path, 'object', meta)
536
    
543

  
544

  
537 545
class Pithos_Client(OOS_Client):
538 546
    """Pithos Storage Client. Extends OOS_Client"""
539
    
547

  
540 548
    def _update_metadata(self, path, entity, **meta):
541 549
        """
542 550
        adds new and updates the values of previously set metadata
543 551
        """
544
        params = {'update':None}
552
        params = {'update': None}
545 553
        headers = {}
546 554
        prefix = 'x-%s-meta-' % entity
547
        for k,v in meta.items():
555
        for k, v in meta.items():
548 556
            k = '%s%s' % (prefix, k)
549 557
            headers[k] = v
550 558
        return self.post(path, headers=headers, params=params)
551
    
559

  
552 560
    def _delete_metadata(self, path, entity, meta=[]):
553 561
        """
554 562
        delete previously set metadata
555 563
        """
556
        params = {'update':None}
564
        params = {'update': None}
557 565
        headers = {}
558 566
        prefix = 'x-%s-meta-' % entity
559 567
        for m in meta:
560 568
            headers['%s%s' % (prefix, m)] = ''
561 569
        return self.post(path, headers=headers, params=params)
562
    
570

  
563 571
    # Storage Account Services
564
    
572

  
565 573
    def list_containers(self, format='text', if_modified_since=None,
566 574
                        if_unmodified_since=None, limit=None, marker=None,
567 575
                        shared=False, until=None, account=None, public=False):
568 576
        """returns a list with the account containers"""
569 577
        account = account or self.account
570
        params = {'until':until} if until else {}
578
        params = {'until': until} if until else {}
571 579
        if shared:
572 580
            params['shared'] = None
573 581
        if public:
574 582
            params['public'] = None
575
        headers = {'if-modified-since':if_modified_since,
576
                   'if-unmodified-since':if_unmodified_since}
583
        headers = {'if-modified-since': if_modified_since,
584
                   'if-unmodified-since': if_unmodified_since}
577 585
        return OOS_Client.list_containers(self, account=account, format=format,
578 586
                                          limit=limit, marker=marker,
579 587
                                          params=params, **headers)
580
    
588

  
581 589
    def retrieve_account_metadata(self, restricted=False, until=None,
582 590
                                  account=None):
583 591
        """returns the account metadata"""
584 592
        account = account or self.account
585
        params = {'until':until} if until else {}
593
        params = {'until': until} if until else {}
586 594
        return OOS_Client.retrieve_account_metadata(self, account=account,
587 595
                                                    restricted=restricted,
588 596
                                                    **params)
589
    
597

  
590 598
    def set_account_groups(self, account=None, **groups):
591 599
        """create account groups"""
592 600
        account = account or self.account
......
594 602
        headers = {}
595 603
        for k, v in groups.items():
596 604
            headers['x-account-group-%s' % k] = v
597
        params = {'update':None}
605
        params = {'update': None}
598 606
        return self.post(path, headers=headers, params=params)
599
    
607

  
600 608
    def retrieve_account_groups(self, account=None):
601 609
        """returns the account groups"""
602 610
        account = account or self.account
......
611 619
                key = key[prefixlen:]
612 620
            groups[key] = val
613 621
        return groups
614
    
622

  
615 623
    def unset_account_groups(self, groups=[], account=None):
616 624
        """delete account groups"""
617 625
        account = account or self.account
......
619 627
        headers = {}
620 628
        for elem in groups:
621 629
            headers['x-account-group-%s' % elem] = ''
622
        params = {'update':None}
630
        params = {'update': None}
623 631
        return self.post(path, headers=headers, params=params)
624
    
632

  
625 633
    def reset_account_groups(self, account=None, **groups):
626 634
        """overrides account groups"""
627 635
        account = account or self.account
......
632 640
            headers['x-account-group-%s' % k] = v
633 641
        meta = self.retrieve_account_metadata(restricted=True)
634 642
        prefix = 'x-account-meta-'
635
        for k,v in meta.items():
643
        for k, v in meta.items():
636 644
            k = '%s%s' % (prefix, k)
637 645
            headers[k] = v
638 646
        return self.post(path, headers=headers)
639
    
647

  
640 648
    # Storage Container Services
641 649
    def create_container(self, container, account=None, meta={}, policies={}):
642 650
        """creates a container"""
......
644 652
        for k, v in policies.items():
645 653
            args['X-Container-Policy-%s' % k.capitalize()] = v
646 654
        return OOS_Client.create_container(self, container, account, meta, **args)
647
    
655

  
648 656
    def list_objects(self, container, format='text',
649 657
                     limit=None, marker=None, prefix=None, delimiter=None,
650 658
                     path=None, shared=False, include_trashed=False, params={},
......
652 660
                     until=None, account=None, public=False):
653 661
        """returns a list with the container objects"""
654 662
        account = account or self.account
655
        params = {'until':until, 'meta':meta}
663
        params = {'until': until, 'meta': meta}
656 664
        if shared:
657 665
            params['shared'] = None
658 666
        if public:
......
661 669
        for elem in ['self', 'container', 'params', 'until', 'meta']:
662 670
            args.pop(elem)
663 671
        return OOS_Client.list_objects(self, container, params=params, **args)
664
    
672

  
665 673
    def retrieve_container_metadata(self, container, restricted=False,
666 674
                                    until=None, account=None):
667 675
        """returns container's metadata"""
668 676
        account = account or self.account
669
        params = {'until':until} if until else {}
677
        params = {'until': until} if until else {}
670 678
        return OOS_Client.retrieve_container_metadata(self, container,
671 679
                                                      account=account,
672 680
                                                      restricted=restricted,
673 681
                                                      **params)
674
    
682

  
675 683
    def set_container_policies(self, container, account=None,
676 684
                               **policies):
677 685
        """sets containers policies"""
......
681 689
        for key, val in policies.items():
682 690
            headers['x-container-policy-%s' % key] = val
683 691
        return self.post(path, headers=headers)
684
    
692

  
685 693
    def update_container_data(self, container, f=stdin):
686 694
        """adds blocks of data to the container"""
687 695
        account = self.account
......
691 699
        data = f.read() if f else None
692 700
        headers['content_length'] = len(data)
693 701
        return self.post(path, data, headers=headers, params=params)
694
    
702

  
695 703
    def delete_container(self, container, until=None, account=None, delimiter=None):
696 704
        """deletes a container or the container history until the date provided"""
697 705
        account = account or self.account
698
        params = {'until':until} if until else {}
706
        params = {'until': until} if until else {}
699 707
        if delimiter:
700
        	params['delimiter'] = delimiter
708
            params['delimiter'] = delimiter
701 709
        return OOS_Client.delete_container(self, container, account=account,
702 710
                                           params=params)
703
    
711

  
704 712
    # Storage Object Services
705
    
713

  
706 714
    def retrieve_object(self, container, object, params={}, format='text',
707 715
                        range=None, if_range=None,
708 716
                        if_match=None, if_none_match=None,
......
710 718
                        account=None, **headers):
711 719
        """returns an object"""
712 720
        account = account or self.account
713
        headers={}
721
        headers = {}
714 722
        l = ['range', 'if_range', 'if_match', 'if_none_match',
715 723
             'if_modified_since', 'if_unmodified_since']
716 724
        l = [elem for elem in l if eval(elem)]
717 725
        for elem in l:
718
            headers.update({elem:eval(elem)})
726
            headers.update({elem: eval(elem)})
719 727
        if format != 'text':
720 728
            params['hashmap'] = None
721 729
        return OOS_Client.retrieve_object(self, container, object,
722 730
                                          account=account, format=format,
723 731
                                          params=params, **headers)
724
    
732

  
725 733
    def retrieve_object_version(self, container, object, version,
726 734
                                format='text', range=None, if_range=None,
727 735
                                if_match=None, if_none_match=None,
......
733 741
        l = ['self', 'container', 'object']
734 742
        for elem in l:
735 743
            args.pop(elem)
736
        params = {'version':version}
744
        params = {'version': version}
737 745
        return self.retrieve_object(container, object, params=params, **args)
738
    
746

  
739 747
    def retrieve_object_versionlist(self, container, object, range=None,
740 748
                                    if_range=None, if_match=None,
741 749
                                    if_none_match=None, if_modified_since=None,
......
746 754
        l = ['self', 'container', 'object']
747 755
        for elem in l:
748 756
            args.pop(elem)
749
        
757

  
750 758
        return self.retrieve_object_version(container, object, version='list',
751 759
                                            format='json', **args)
752
    
760

  
753 761
    def create_zero_length_object(self, container, object,
754 762
                                  meta={}, etag=None, content_type=None,
755 763
                                  content_encoding=None,
......
763 771
            args.pop(elem)
764 772
        return OOS_Client.create_zero_length_object(self, container, object,
765 773
                                                    **args)
766
    
774

  
767 775
    def create_folder(self, container, name,
768
                          meta={}, etag=None, 
769
                          content_encoding=None,
770
                          content_disposition=None,
771
                          x_object_manifest=None, x_object_sharing=None,
772
                          x_object_public=None, account=None):
773
    	args = locals().copy()
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()
774 782
        for elem in ['self', 'container', 'name']:
775 783
            args.pop(elem)
776 784
        args['content_type'] = 'application/directory'
777 785
        return self.create_zero_length_object(container, name, **args)
778
    
786

  
779 787
    def create_object(self, container, object, f=stdin, format='text',
780 788
                      meta={}, params={}, etag=None, content_type=None,
781 789
                      content_encoding=None, content_disposition=None,
......
787 795
        for elem in ['self', 'container', 'object']:
788 796
            args.pop(elem)
789 797
        if format != 'text':
790
            params.update({'hashmap':None})
798
            params.update({'hashmap': None})
791 799
        return OOS_Client.create_object(self, container, object, **args)
792
        
800

  
793 801
    def create_object_using_chunks(self, container, object,
794 802
                                   f=stdin, blocksize=1024, meta={}, etag=None,
795 803
                                   content_type=None, content_encoding=None,
......
800 808
        account = account or self.account
801 809
        path = '/%s/%s/%s' % (account, container, object)
802 810
        headers = {}
803
        l = ['etag', 'content_type', 'content_encoding', 'content_disposition', 
811
        l = ['etag', 'content_type', 'content_encoding', 'content_disposition',
804 812
             'x_object_sharing', 'x_object_manifest', 'x_object_public']
805 813
        l = [elem for elem in l if eval(elem)]
806 814
        for elem in l:
807
            headers.update({elem:eval(elem)})
815
            headers.update({elem: eval(elem)})
808 816
        headers.setdefault('content-type', 'application/octet-stream')
809
        
810
        for k,v in meta.items():
817

  
818
        for k, v in meta.items():
811 819
            v = v.strip()
812
            headers['x-object-meta-%s' %k.strip()] = v
813
        
820
            headers['x-object-meta-%s' % k.strip()] = v
821

  
814 822
        return self._chunked_transfer(path, 'PUT', f, headers=headers,
815 823
                                      blocksize=blocksize)
816
    
824

  
817 825
    def create_object_by_hashmap(self, container, object, hashmap={},
818 826
                                 meta={}, etag=None, content_encoding=None,
819 827
                                 content_disposition=None, content_type=None,
820 828
                                 x_object_sharing=None, x_object_manifest=None,
821
                                 x_object_public = None, account=None):
829
                                 x_object_public=None, account=None):
822 830
        """creates an object by uploading hashes representing data instead of data"""
823 831
        account = account or self.account
824 832
        args = locals().copy()
825 833
        for elem in ['self', 'container', 'object', 'hashmap']:
826 834
            args.pop(elem)
827
            
835

  
828 836
        try:
829 837
            data = json.dumps(hashmap)
830 838
        except SyntaxError:
831 839
            raise Fault('Invalid formatting')
832
        args['params'] = {'hashmap':None}
840
        args['params'] = {'hashmap': None}
833 841
        args['format'] = 'json'
834
        
842

  
835 843
        return self.create_object(container, object, f=StringIO(data), **args)
836
    
844

  
837 845
    def create_manifestation(self, container, object, manifest, account=None):
838 846
        """creates a manifestation"""
839 847
        account = account or self.account
840
        headers={'x_object_manifest':manifest}
848
        headers = {'x_object_manifest': manifest}
841 849
        return self.create_object(container, object, f=None, account=account,
842 850
                                  **headers)
843
    
851

  
844 852
    def update_object(self, container, object, f=stdin,
845 853
                      offset=None, meta={}, replace=False, content_length=None,
846 854
                      content_type=None, content_range=None,
......
854 862
        for elem in ['self', 'container', 'object', 'replace']:
855 863
            args.pop(elem)
856 864
        if not replace:
857
            args['params'] = {'update':None}
865
            args['params'] = {'update': None}
858 866
        return OOS_Client.update_object(self, container, object, **args)
859
    
867

  
860 868
    def update_object_using_chunks(self, container, object, f=stdin,
861 869
                                   blocksize=1024, offset=None, meta={},
862 870
                                   replace=False, content_type=None, content_encoding=None,
......
869 877
        for elem in ['self', 'container', 'object', 'replace']:
870 878
            args.pop(elem)
871 879
        if not replace:
872
            args['params'] = {'update':None}
880
            args['params'] = {'update': None}
873 881
        return OOS_Client.update_object_using_chunks(self, container, object, **args)
874
    
882

  
875 883
    def update_from_other_source(self, container, object, source,
876
                      offset=None, meta={}, content_range=None,
877
                      content_encoding=None, content_disposition=None,
878
                      x_object_bytes=None, x_object_manifest=None,
879
                      x_object_sharing=None, x_object_public=None, account=None):
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):
880 888
        """updates an object"""
881 889
        account = account or self.account
882 890
        args = locals().copy()
883 891
        for elem in ['self', 'container', 'object', 'source']:
884 892
            args.pop(elem)
885
        
893

  
886 894
        args['x_source_object'] = source
887 895
        return self.update_object(container, object, f=None, **args)
888
    
896

  
889 897
    def delete_object(self, container, object, until=None, account=None, delimiter=None):
890 898
        """deletes an object or the object history until the date provided"""
891 899
        account = account or self.account
892
        params = {'until':until} if until else {}
900
        params = {'until': until} if until else {}
893 901
        if delimiter:
894
        	params['delimiter'] = delimiter
902
            params['delimiter'] = delimiter
895 903
        return OOS_Client.delete_object(self, container, object, params, account)
896
    
904

  
897 905
    def trash_object(self, container, object):
898 906
        """trashes an object"""
899 907
        account = account or self.account
900 908
        path = '/%s/%s' % (container, object)
901
        meta = {'trash':'true'}
909
        meta = {'trash': 'true'}
902 910
        return self._update_metadata(path, 'object', **meta)
903
    
911

  
904 912
    def restore_object(self, container, object, account=None):
905 913
        """restores a trashed object"""
906 914
        account = account or self.account
907 915
        return self.delete_object_metadata(container, object, account, ['trash'])
908
    
916

  
909 917
    def publish_object(self, container, object, account=None):
910 918
        """sets a previously created object publicly accessible"""
911 919
        account = account or self.account
912 920
        path = '/%s/%s/%s' % (account, container, object)
913 921
        headers = {}
914 922
        headers['x_object_public'] = True
915
        params = {'update':None}
923
        params = {'update': None}
916 924
        return self.post(path, headers=headers, params=params)
917
    
925

  
918 926
    def unpublish_object(self, container, object, account=None):
919 927
        """unpublish an object"""
920 928
        account = account or self.account
921 929
        path = '/%s/%s/%s' % (account, container, object)
922 930
        headers = {}
923 931
        headers['x_object_public'] = False
924
        params = {'update':None}
932
        params = {'update': None}
925 933
        return self.post(path, headers=headers, params=params)
926
    
934

  
927 935
    def copy_object(self, src_container, src_object, dst_container, dst_object,
928 936
                    meta={}, public=False, version=None, account=None,
929 937
                    content_type=None, delimiter=None):
......
938 946
                                      account=account, content_type=content_type,
939 947
                                      delimiter=delimiter,
940 948
                                      **headers)
941
    
949

  
942 950
    def move_object(self, src_container, src_object, dst_container,
943
                             dst_object, meta={}, public=False,
944
                             account=None, content_type=None, delimiter=None):
951
                    dst_object, meta={}, public=False,
952
                    account=None, content_type=None, delimiter=None):
945 953
        """moves an object"""
946 954
        headers = {}
947 955
        headers['x_object_public'] = public
......
950 958
                                      account=account, content_type=content_type,
951 959
                                      delimiter=delimiter,
952 960
                                      **headers)
953
    
961

  
954 962
    def list_shared_by_others(self, limit=None, marker=None, format='text'):
955 963
        """lists other accounts that share objects to the user"""
956 964
        l = ['limit', 'marker']
......
958 966
        for elem in [elem for elem in l if eval(elem)]:
959 967
            params[elem] = eval(elem)
960 968
        return self._list('', format, params)
961
    
969

  
962 970
    def share_object(self, container, object, l, read=True):
963 971
        """gives access(read by default) to an object to a user/group list"""
964 972
        action = 'read' if read else 'write'
965 973
        sharing = '%s=%s' % (action, ','.join(l))
966 974
        self.update_object(container, object, f=None, x_object_sharing=sharing)
967 975

  
976

  
968 977
def _prepare_path(path, format='text', params={}):
969 978
    full_path = '%s?format=%s' % (quote(path), format)
970
    
971
    for k,v in params.items():
979

  
980
    for k, v in params.items():
972 981
        value = quote(str(v)) if v else ''
973
        full_path = '%s&%s=%s' %(full_path, quote(k), value)
982
        full_path = '%s&%s=%s' % (full_path, quote(k), value)
974 983
    return full_path
975 984

  
985

  
976 986
def _prepare_headers(headers):
977
    for k,v in headers.items():
987
    for k, v in headers.items():
978 988
        headers.pop(k)
979 989
        k = k.replace('_', '-')
980
        headers[quote(k)] = quote(v, safe='/=,:@ *"') if type(v) == types.StringType else v
990
        headers[quote(k)] = quote(
991
            v, safe='/=,:@ *"') if isinstance(v, types.StringType) else v
981 992
    return headers
982 993

  
994

  
983 995
def _handle_response(response, verbose=False, debug=False):
984 996
    headers = response.getheaders()
985
    headers = dict((unquote(h), unquote(v)) for h,v in headers)
986
    
997
    headers = dict((unquote(h), unquote(v)) for h, v in headers)
998

  
987 999
    if verbose:
988 1000
        print '%d %s' % (response.status, response.reason)
989 1001
        for key, val in headers.items():
990 1002
            print '%s: %s' % (key.capitalize(), val)
991 1003
        print
992
    
1004

  
993 1005
    length = response.getheader('content-length', None)
994 1006
    data = response.read(length)
995 1007
    if debug:
996 1008
        print data
997 1009
        print
998
    
1010

  
999 1011
    if int(response.status) in ERROR_CODES.keys():
1000 1012
        raise Fault(data, int(response.status))
1001
    
1013

  
1002 1014
    #print '**',  response.status, headers, data, '\n'
1003 1015
    return response.status, headers, data

Also available in: Unified diff