Prepare PithorRest testcase for implementation
[kamaki] / kamaki / clients / pithos / rest_api.py
1 # Copyright 2012-2013 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 kamaki.clients.storage import StorageClient
35 from kamaki.clients.utils import path4url
36
37
38 class PithosRestClient(StorageClient):
39
40     def account_head(
41             self,
42             until=None,
43             if_modified_since=None,
44             if_unmodified_since=None,
45             *args, **kwargs):
46         """ Full Pithos+ HEAD at account level
47
48         --- request parameters ---
49
50         :param until: (string) optional timestamp
51
52         --- request headers ---
53
54         :param if_modified_since: (string) Retrieve if account has changed
55             since provided timestamp
56
57         :param if_unmodified_since: (string) Retrieve if account has not
58             change since provided timestamp
59
60         :returns: ConnectionResponse
61         """
62
63         self._assert_account()
64         path = path4url(self.account)
65
66         self.set_param('until', until, iff=until)
67         self.set_header('If-Modified-Since', if_modified_since)
68         self.set_header('If-Unmodified-Since', if_unmodified_since)
69
70         success = kwargs.pop('success', 204)
71         return self.head(path, *args, success=success, **kwargs)
72
73     def account_get(
74             self,
75             limit=None,
76             marker=None,
77             format='json',
78             show_only_shared=False,
79             until=None,
80             if_modified_since=None,
81             if_unmodified_since=None,
82             *args, **kwargs):
83         """  Full Pithos+ GET at account level
84
85         --- request parameters ---
86
87         :param limit: (integer) The amount of results requested
88             (server will use default value if None)
89
90         :param marker: string Return containers with name
91             lexicographically after marker
92
93         :param format: (string) reply format can be json or xml
94             (default: json)
95
96         :param shared: (bool) If true, only shared containers will be
97             included in results
98
99         :param until: (string) optional timestamp
100
101         --- request headers ---
102
103         :param if_modified_since: (string) Retrieve if account has changed
104             since provided timestamp
105
106         :param if_unmodified_since: (string) Retrieve if account has not
107             changed since provided timestamp
108
109         :returns: ConnectionResponse
110         """
111
112         self._assert_account()
113
114         self.set_param('format', format, iff=format)
115         self.set_param('limit', limit, iff=limit)
116         self.set_param('marker', marker, iff=marker)
117         self.set_param('shared', iff=show_only_shared)
118         self.set_param('until', until, iff=until)
119
120         self.set_header('If-Modified-Since', if_modified_since)
121         self.set_header('If-Unmodified-Since', if_unmodified_since)
122
123         path = path4url(self.account)
124         success = kwargs.pop('success', (200, 204))
125         return self.get(path, *args, success=success, **kwargs)
126
127     def account_post(
128             self,
129             update=True,
130             groups={},
131             metadata=None,
132             quota=None,
133             versioning=None,
134             *args, **kwargs):
135         """ Full Pithos+ POST at account level
136
137         --- request parameters ---
138
139         :param update: (bool) if True, Do not replace metadata/groups
140
141         --- request headers ---
142
143         :param groups: (dict) Optional user defined groups in the form
144             { 'group1':['user1', 'user2', ...],
145             'group2':['userA', 'userB', ...], }
146
147         :param metadata: (dict) Optional user defined metadata in the form
148             { 'name1': 'value1', 'name2': 'value2', ... }
149
150         :param quota: (integer) If supported, sets the Account quota
151
152         :param versioning: (string) If supported, sets the Account versioning
153             to 'auto' or some other supported versioning string
154
155         :returns: ConnectionResponse
156         """
157
158         self._assert_account()
159
160         self.set_param('update', iff=update)
161
162         if groups:
163             for group, usernames in groups.items():
164                 userstr = ''
165                 dlm = ''
166                 for user in usernames:
167                     userstr = userstr + dlm + user
168                     dlm = ','
169                 self.set_header('X-Account-Group-' + group, userstr)
170         if metadata:
171             for metaname, metaval in metadata.items():
172                 self.set_header('X-Account-Meta-' + metaname, metaval)
173         self.set_header('X-Account-Policy-Quota', quota)
174         self.set_header('X-Account-Policy-Versioning', versioning)
175
176         path = path4url(self.account)
177         success = kwargs.pop('success', 202)
178         return self.post(path, *args, success=success, **kwargs)
179
180     def container_head(
181             self,
182             until=None,
183             if_modified_since=None,
184             if_unmodified_since=None,
185             *args, **kwargs):
186         """ Full Pithos+ HEAD at container level
187
188         --- request params ---
189
190         :param until: (string) optional timestamp
191
192         --- request headers ---
193
194         :param if_modified_since: (string) Retrieve if account has changed
195             since provided timestamp
196
197         :param if_unmodified_since: (string) Retrieve if account has not
198             changed since provided timestamp
199
200         :returns: ConnectionResponse
201         """
202
203         self._assert_container()
204
205         self.set_param('until', until, iff=until)
206
207         self.set_header('If-Modified-Since', if_modified_since)
208         self.set_header('If-Unmodified-Since', if_unmodified_since)
209
210         path = path4url(self.account, self.container)
211         success = kwargs.pop('success', 204)
212         return self.head(path, *args, success=success, **kwargs)
213
214     def container_get(
215             self,
216             limit=None,
217             marker=None,
218             prefix=None,
219             delimiter=None,
220             path=None,
221             format='json',
222             meta=[],
223             show_only_shared=False,
224             until=None,
225             if_modified_since=None,
226             if_unmodified_since=None,
227             *args, **kwargs):
228         """ Full Pithos+ GET at container level
229
230         --- request parameters ---
231
232         :param limit: (integer) The amount of results requested
233             (server will use default value if None)
234
235         :param marker: (string) Return containers with name lexicographically
236             after marker
237
238         :param prefix: (string) Return objects starting with prefix
239
240         :param delimiter: (string) Return objects up to the delimiter
241
242         :param path: (string) assume prefix = path and delimiter = /
243             (overwrites prefix and delimiter)
244
245         :param format: (string) reply format can be json or xml (default:json)
246
247         :param meta: (list) Return objects that satisfy the key queries in
248             the specified comma separated list (use <key>, !<key> for
249             existence queries, <key><op><value> for value queries, where <op>
250             can be one of =, !=, <=, >=, <, >)
251
252         :param shared: (bool) If true, only shared containers will be included
253             in results
254
255         :param until: (string) optional timestamp
256
257         --- request headers ---
258
259         :param if_modified_since: (string) Retrieve if account has changed
260             since provided timestamp
261
262         :param if_unmodified_since: (string) Retrieve if account has not
263             changed since provided timestamp
264
265         :returns: ConnectionResponse
266         """
267
268         self._assert_container()
269
270         self.set_param('format', format, iff=format)
271         self.set_param('limit', limit, iff=limit)
272         self.set_param('marker', marker, iff=marker)
273         if not path:
274             self.set_param('prefix', prefix, iff=prefix)
275             self.set_param('delimiter', delimiter, iff=delimiter)
276         else:
277             self.set_param('path', path)
278         self.set_param('shared', iff=show_only_shared)
279         if meta:
280             self.set_param('meta',  ','.join(meta))
281         self.set_param('until', until, iff=until)
282
283         self.set_header('If-Modified-Since', if_modified_since)
284         self.set_header('If-Unmodified-Since', if_unmodified_since)
285
286         path = path4url(self.account, self.container)
287         success = kwargs.pop('success', 200)
288         return self.get(path, *args, success=success, **kwargs)
289
290     def container_put(
291             self,
292             quota=None, versioning=None, metadata=None,
293             *args, **kwargs):
294         """ Full Pithos+ PUT at container level
295
296         --- request headers ---
297
298         :param quota: (integer) Size limit in KB
299
300         :param versioning: (string) 'auto' or other string supported by server
301
302         :param metadata: (dict) Optional user defined metadata in the form
303             { 'name1': 'value1', 'name2': 'value2', ... }
304
305         :returns: ConnectionResponse
306         """
307         self._assert_container()
308
309         if metadata:
310             for metaname, metaval in metadata.items():
311                 self.set_header('X-Container-Meta-' + metaname, metaval)
312         self.set_header('X-Container-Policy-Quota', quota)
313         self.set_header('X-Container-Policy-Versioning', versioning)
314
315         path = path4url(self.account, self.container)
316         success = kwargs.pop('success', (201, 202))
317         return self.put(path, *args, success=success, **kwargs)
318
319     def container_post(
320             self,
321             update=True,
322             format='json',
323             quota=None,
324             versioning=None,
325             metadata=None,
326             content_type=None,
327             content_length=None,
328             transfer_encoding=None,
329             *args, **kwargs):
330         """ Full Pithos+ POST at container level
331
332         --- request params ---
333
334         :param update: (bool)  if True, Do not replace metadata/groups
335
336         :param format: (string) json (default) or xml
337
338         --- request headers ---
339
340         :param quota: (integer) Size limit in KB
341
342         :param versioning: (string) 'auto' or other string supported by server
343
344         :param metadata: (dict) Optional user defined metadata in the form
345             { 'name1': 'value1', 'name2': 'value2', ... }
346
347         :param content_type: (string) set a custom content type
348
349         :param content_length: (string) set a custrom content length
350
351         :param transfer_encoding: (string) set a custrom transfer encoding
352
353         :returns: ConnectionResponse
354         """
355         self._assert_container()
356
357         self.set_param('format', format, iff=format)
358         self.set_param('update', iff=update)
359
360         if metadata:
361             for metaname, metaval in metadata.items():
362                 self.set_header('X-Container-Meta-' + metaname, metaval)
363         self.set_header('X-Container-Policy-Quota', quota)
364         self.set_header('X-Container-Policy-Versioning', versioning)
365         self.set_header('Content-Type', content_type)
366         self.set_header('Content-Length', content_length)
367         self.set_header('Transfer-Encoding', transfer_encoding)
368
369         path = path4url(self.account, self.container)
370         success = kwargs.pop('success', 202)
371         return self.post(path, *args, success=success, **kwargs)
372
373     def container_delete(self, until=None, delimiter=None, *args, **kwargs):
374         """ Full Pithos+ DELETE at container level
375
376         --- request parameters ---
377
378         :param until: (timestamp string) if defined, container is purged up to
379             that time
380
381         :returns: ConnectionResponse
382         """
383
384         self._assert_container()
385
386         self.set_param('until', until, iff=until)
387         self.set_param('delimiter', delimiter, iff=delimiter)
388
389         path = path4url(self.account, self.container)
390         success = kwargs.pop('success', 204)
391         return self.delete(path, success=success)
392
393     def object_head(
394             self, object,
395             version=None,
396             if_etag_match=None,
397             if_etag_not_match=None,
398             if_modified_since=None,
399             if_unmodified_since=None,
400             *args, **kwargs):
401         """ Full Pithos+ HEAD at object level
402
403         --- request parameters ---
404
405         :param version: (string) optional version identified
406
407         --- request headers ---
408
409         :param if_etag_match: (string) if provided, return only results
410             with etag matching with this
411
412         :param if_etag_not_match: (string) if provided, return only results
413             with etag not matching with this
414
415         :param if_modified_since: (string) Retrieve if account has changed
416             since provided timestamp
417
418         :param if_unmodified_since: (string) Retrieve if account has not
419             changed since provided timestamp
420
421         :returns: ConnectionResponse
422         """
423
424         self._assert_container()
425
426         self.set_param('version', version, iff=version)
427
428         self.set_header('If-Match', if_etag_match)
429         self.set_header('If-None-Match', if_etag_not_match)
430         self.set_header('If-Modified-Since', if_modified_since)
431         self.set_header('If-Unmodified-Since', if_unmodified_since)
432
433         path = path4url(self.account, self.container, object)
434         success = kwargs.pop('success', 200)
435         return self.head(path, *args, success=success, **kwargs)
436
437     def object_get(
438             self, object,
439             format='json',
440             hashmap=False,
441             version=None,
442             data_range=None,
443             if_range=False,
444             if_etag_match=None,
445             if_etag_not_match=None,
446             if_modified_since=None,
447             if_unmodified_since=None,
448             *args, **kwargs):
449         """ Full Pithos+ GET at object level
450
451         --- request parameters ---
452
453         :param format: (string) json (default) or xml
454
455         :param hashmap: (bool) Optional request for hashmap
456
457         :param version: (string) optional version identified
458
459         --- request headers ---
460
461         :param data_range: (string) Optional range of data to retrieve
462
463         :param if_range: (bool)
464
465         :param if_etag_match: (string) if provided, return only results
466             with etag matching with this
467
468         :param if_etag_not_match: (string) if provided, return only results
469             with etag not matching with this
470
471         :param if_modified_since: (string) Retrieve if account has changed
472             since provided timestamp
473
474         :param if_unmodified_since: (string) Retrieve if account has not
475             changed since provided timestamp
476
477         :returns: ConnectionResponse
478         """
479
480         self._assert_container()
481
482         self.set_param('format', format, iff=format)
483         self.set_param('version', version, iff=version)
484         self.set_param('hashmap', hashmap, iff=hashmap)
485
486         self.set_header('Range', data_range)
487         self.set_header('If-Range', '', if_range and data_range)
488         self.set_header('If-Match', if_etag_match, )
489         self.set_header('If-None-Match', if_etag_not_match)
490         self.set_header('If-Modified-Since', if_modified_since)
491         self.set_header('If-Unmodified-Since', if_unmodified_since)
492
493         path = path4url(self.account, self.container, object)
494         success = kwargs.pop('success', 200)
495         return self.get(path, *args, success=success, **kwargs)
496
497     def object_put(
498             self, object,
499             format='json',
500             hashmap=False,
501             delimiter=None,
502             if_etag_match=None,
503             if_etag_not_match=None,
504             etag=None,
505             content_length=None,
506             content_type=None,
507             transfer_encoding=None,
508             copy_from=None,
509             move_from=None,
510             source_account=None,
511             source_version=None,
512             content_encoding=None,
513             content_disposition=None,
514             manifest=None,
515             permissions=None,
516             public=None,
517             metadata=None,
518             *args, **kwargs):
519         """ Full Pithos+ PUT at object level
520
521         --- request parameters ---
522
523         :param format: (string) json (default) or xml
524
525         :param hashmap: (bool) Optional hashmap provided instead of data
526
527         --- request headers ---
528
529         :param if_etag_match: (string) if provided, return only results
530             with etag matching with this
531
532         :param if_etag_not_match: (string) if provided, return only results
533             with etag not matching with this
534
535         :param etag: (string) The MD5 hash of the object (optional to check
536             written data)
537
538         :param content_length: (integer) The size of the data written
539
540         :param content_type: (string) The MIME content type of the object
541
542         :param transfer_encoding: (string) Set to chunked to specify
543             incremental uploading (if used, Content-Length is ignored)
544
545         :param copy_from: (string) The source path in the form
546             /<container>/<object>
547
548         :param move_from: (string) The source path in the form
549             /<container>/<object>
550
551         :param source_account: (string) The source account to copy/move from
552
553         :param source_version: (string) The source version to copy from
554
555         :param conent_encoding: (string) The encoding of the object
556
557         :param content_disposition: (string) Presentation style of the object
558
559         :param manifest: (string) Object parts prefix in
560             /<container>/<object> form
561
562         :param permissions: (dict) Object permissions in the form (all fields
563             are optional)
564             { 'read':[user1, group1, user2, ...],
565             'write':['user3, group2, group3, ...] }
566
567         :param public: (bool) If true, Object is publicly accessible,
568             if false, not
569
570         :param metadata: (dict) Optional user defined metadata in the form
571             {'meta-key-1':'meta-value-1', 'meta-key-2':'meta-value-2', ...}
572
573         :returns: ConnectionResponse
574         """
575
576         self._assert_container()
577
578         self.set_param('format', format, iff=format)
579         self.set_param('hashmap', hashmap, iff=hashmap)
580         self.set_param('delimiter', delimiter, iff=delimiter)
581
582         self.set_header('If-Match', if_etag_match)
583         self.set_header('If-None-Match', if_etag_not_match)
584         self.set_header('ETag', etag)
585         self.set_header('Content-Length', content_length)
586         self.set_header('Content-Type', content_type)
587         self.set_header('Transfer-Encoding', transfer_encoding)
588         self.set_header('X-Copy-From', copy_from)
589         self.set_header('X-Move-From', move_from)
590         self.set_header('X-Source-Account', source_account)
591         self.set_header('X-Source-Version', source_version)
592         self.set_header('Content-Encoding', content_encoding)
593         self.set_header('Content-Disposition', content_disposition)
594         self.set_header('X-Object-Manifest', manifest)
595         if permissions:
596             perms = None
597             if permissions:
598                 for perm_type, perm_list in permissions.items():
599                     if not perms:
600                         perms = ''  # Remove permissions
601                     if perm_list:
602                         perms += ';' if perms else ''
603                         perms += '%s=%s' % (perm_type, ','.join(perm_list))
604             self.set_header('X-Object-Sharing', perms)
605         self.set_header('X-Object-Public', public)
606         if metadata:
607             for key, val in metadata.items():
608                 self.set_header('X-Object-Meta-' + key, val)
609
610         path = path4url(self.account, self.container, object)
611         success = kwargs.pop('success', 201)
612         return self.put(path, *args, success=success, **kwargs)
613
614     def object_copy(
615             self, object, destination,
616             format='json',
617             ignore_content_type=False,
618             if_etag_match=None,
619             if_etag_not_match=None,
620             destination_account=None,
621             content_type=None,
622             content_encoding=None,
623             content_disposition=None,
624             source_version=None,
625             permissions=None,
626             public=False,
627             metadata=None,
628             *args, **kwargs):
629         """ Full Pithos+ COPY at object level
630
631         --- request parameters ---
632
633         :param format: (string) json (default) or xml
634
635         :param ignore_content_type: (bool) Ignore the supplied Content-Type
636
637         --- request headers ---
638
639         :param if_etag_match: (string) if provided, copy only results
640             with etag matching with this
641
642         :param if_etag_not_match: (string) if provided, copy only results
643             with etag not matching with this
644
645         :param destination: (string) The destination path in the form
646             /<container>/<object>
647
648         :param destination_account: (string) The destination account to copy to
649
650         :param content_type: (string) The MIME content type of the object
651
652         :param content_encoding: (string) The encoding of the object
653
654         :param content_disposition: (string) Object resentation style
655
656         :param source_version: (string) The source version to copy from
657
658         :param permissions: (dict) Object permissions in the form
659             (all fields are optional)
660             { 'read':[user1, group1, user2, ...],
661             'write':['user3, group2, group3, ...] }
662
663         :param permissions: update permissions
664
665         :param public: (bool) If true, Object is publicly accessible
666
667         :param metadata: (dict) Optional user defined metadata in the form
668             {'meta-key-1':'meta-value-1', 'meta-key-2':'meta-value-2', ...}
669             Metadata are appended to the source metadata. In case of same
670             keys, they replace the old metadata
671
672         :returns: ConnectionResponse
673         """
674
675         self._assert_container()
676
677         self.set_param('format', format, iff=format)
678         self.set_param('ignore_content_type', iff=ignore_content_type)
679
680         self.set_header('If-Match', if_etag_match)
681         self.set_header('If-None-Match', if_etag_not_match)
682         self.set_header('Destination', destination)
683         self.set_header('Destination-Account', destination_account)
684         self.set_header('Content-Type', content_type)
685         self.set_header('Content-Encoding', content_encoding)
686         self.set_header('Content-Disposition', content_disposition)
687         self.set_header('X-Source-Version', source_version)
688         if permissions:
689             perms = ''
690             for perm_type, perm_list in permissions.items():
691                 if not perms:
692                     perms = ''  # Remove permissions
693                 if perm_list:
694                     perms += ';' if perms else ''
695                     perms += '%s=%s' % (perm_type, ','.join(perm_list))
696             self.set_header('X-Object-Sharing', perms)
697         self.set_header('X-Object-Public', public)
698         if metadata:
699             for key, val in metadata.items():
700                 self.set_header('X-Object-Meta-' + key, val)
701
702         path = path4url(self.account, self.container, object)
703         success = kwargs.pop('success', 201)
704         return self.copy(path, *args, success=success, **kwargs)
705
706     def object_move(
707             self, object,
708             format='json',
709             ignore_content_type=False,
710             if_etag_match=None,
711             if_etag_not_match=None,
712             destination=None,
713             destination_account=None,
714             content_type=None,
715             content_encoding=None,
716             content_disposition=None,
717             permissions={},
718             public=False,
719             metadata={},
720             *args, **kwargs):
721         """ Full Pithos+ COPY at object level
722
723         --- request parameters ---
724
725         :param format: (string) json (default) or xml
726
727         :param ignore_content_type: (bool) Ignore the supplied Content-Type
728
729         --- request headers ---
730
731         :param if_etag_match: (string) if provided, return only results
732             with etag matching with this
733
734         :param if_etag_not_match: (string) if provided, return only results
735             with etag not matching with this
736
737         :param destination: (string) The destination path in the form
738             /<container>/<object>
739
740         :param destination_account: (string) The destination account to copy to
741
742         :param content_type: (string) The MIME content type of the object
743
744         :param content_encoding: (string) The encoding of the object
745
746         :param content_disposition: (string) Object presentation style
747
748         :param source_version: (string) The source version to copy from
749
750         :param permissions: (dict) Object permissions in the form
751             (all fields are optional)
752             { 'read':[user1, group1, user2, ...],
753             'write':['user3, group2, group3, ...] }
754
755         :param public: (bool) If true, Object is publicly accessible
756
757         :param metadata: (dict) Optional user defined metadata in the form
758             {'meta-key-1':'meta-value-1', 'meta-key-2':'meta-value-2', ...}
759
760         :returns: ConnectionResponse
761         """
762
763         self._assert_container()
764
765         self.set_param('format', format, iff=format)
766         self.set_param('ignore_content_type', iff=ignore_content_type)
767
768         self.set_header('If-Match', if_etag_match)
769         self.set_header('If-None-Match', if_etag_not_match)
770         self.set_header('Destination', destination)
771         self.set_header('Destination-Account', destination_account)
772         self.set_header('Content-Type', content_type)
773         self.set_header('Content-Encoding', content_encoding)
774         self.set_header('Content-Disposition', content_disposition)
775         if permissions:
776             perms = ''
777             for perm_type, perm_list in permissions.items():
778                 if not perms:
779                     perms = ''  # Remove permissions
780                 if perm_list:
781                     perms += ';' if perms else ''
782                     perms += '%s=%s' % (perm_type, ','.join(perm_list))
783             self.set_header('X-Object-Sharing', perms)
784         self.set_header('X-Object-Public', public)
785         if metadata:
786             for key, val in metadata.items():
787                 self.set_header('X-Object-Meta-' + key, val)
788
789         path = path4url(self.account, self.container, object)
790         success = kwargs.pop('success', 201)
791         return self.move(path, *args, success=success, **kwargs)
792
793     def object_post(
794             self, object,
795             format='json',
796             update=True,
797             if_etag_match=None,
798             if_etag_not_match=None,
799             content_length=None,
800             content_type=None,
801             content_range=None,
802             transfer_encoding=None,
803             content_encoding=None,
804             content_disposition=None,
805             source_object=None,
806             source_account=None,
807             source_version=None,
808             object_bytes=None,
809             manifest=None,
810             permissions={},
811             public=False,
812             metadata={},
813             *args, **kwargs):
814         """ Full Pithos+ POST at object level
815
816         --- request parameters ---
817
818         :param format: (string) json (default) or xml
819
820         :param update: (bool) Do not replace metadata
821
822         --- request headers ---
823
824         :param if_etag_match: (string) if provided, return only results
825             with etag matching with this
826
827         :param if_etag_not_match: (string) if provided, return only results
828             with etag not matching with this
829
830         :param content_length: (string) The size of the data written
831
832         :param content_type: (string) The MIME content type of the object
833
834         :param content_range: (string) The range of data supplied
835
836         :param transfer_encoding: (string) Set to chunked to specify
837             incremental uploading (if used, Content-Length is ignored)
838
839         :param content_encoding: (string) The encoding of the object
840
841         :param content_disposition: (string) Object presentation style
842
843         :param source_object: (string) Update with data from the object at
844             path /<container>/<object>
845
846         :param source_account: (string) The source account to update from
847
848         :param source_version: (string) The source version to copy from
849
850         :param object_bytes: (integer) The updated objects final size
851
852         :param manifest: (string) Object parts prefix as /<container>/<object>
853
854         :param permissions: (dict) Object permissions in the form (all fields
855             are optional)
856             { 'read':[user1, group1, user2, ...],
857             'write':['user3, group2, group3, ...] }
858
859         :param public: (bool) If true, Object is publicly accessible
860
861         :param metadata: (dict) Optional user defined metadata in the form
862             {'meta-key-1':'meta-value-1', 'meta-key-2':'meta-value-2', ...}
863
864         :returns: ConnectionResponse
865         """
866
867         self._assert_container()
868
869         self.set_param('format', format, iff=format)
870         self.set_param('update', iff=update)
871
872         self.set_header('If-Match', if_etag_match)
873         self.set_header('If-None-Match', if_etag_not_match)
874         self.set_header(
875             'Content-Length',
876             content_length,
877             iff=not transfer_encoding)
878         self.set_header('Content-Type', content_type)
879         self.set_header('Content-Range', content_range)
880         self.set_header('Transfer-Encoding', transfer_encoding)
881         self.set_header('Content-Encoding', content_encoding)
882         self.set_header('Content-Disposition', content_disposition)
883         self.set_header('X-Source-Object', source_object)
884         self.set_header('X-Source-Account', source_account)
885         self.set_header('X-Source-Version', source_version)
886         self.set_header('X-Object-Bytes', object_bytes)
887         self.set_header('X-Object-Manifest', manifest)
888         if permissions:
889             perms = ''
890             for perm_type, perm_list in permissions.items():
891                 if not perms:
892                     perms = ''  # Remove permissions
893                 if perm_list:
894                     perms += ';' if perms else ''
895                     perms += '%s=%s' % (perm_type, ','.join(perm_list))
896                 self.set_header('X-Object-Sharing', perms)
897         self.set_header('X-Object-Public', public)
898         for key, val in metadata.items():
899             self.set_header('X-Object-Meta-' + key, val)
900
901         path = path4url(self.account, self.container, object)
902         success = kwargs.pop('success', (202, 204))
903         return self.post(path, *args, success=success, **kwargs)
904
905     def object_delete(
906             self, object,
907             until=None, delimiter=None,
908             *args, **kwargs):
909         """ Full Pithos+ DELETE at object level
910
911         --- request parameters ---
912
913         :param until: (string) Optional timestamp
914
915         :returns: ConnectionResponse
916         """
917         self._assert_container()
918
919         self.set_param('until', until, iff=until)
920         self.set_param('delimiter', delimiter, iff=delimiter)
921
922         path = path4url(self.account, self.container, object)
923         success = kwargs.pop('success', 204)
924         return self.delete(path, *args, success=success, **kwargs)