Skip to content
This repository was archived by the owner on May 10, 2024. It is now read-only.

Commit c6d5af3

Browse files
committed
Merge branch 'ryansydnor-s3' into develop
* ryansydnor-s3: Allow s3 bucket lifecycle policies with multiple transitions
2 parents d1973a4 + 196cef9 commit c6d5af3

File tree

3 files changed

+213
-42
lines changed

3 files changed

+213
-42
lines changed

boto/s3/lifecycle.py

Lines changed: 91 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -54,14 +54,21 @@ def __init__(self, id=None, prefix=None, status=None, expiration=None,
5454
else:
5555
# None or object
5656
self.expiration = expiration
57-
self.transition = transition
57+
58+
# retain backwards compatibility
59+
if isinstance(transition, Transition):
60+
self.transition = Transitions()
61+
self.transition.append(transition)
62+
elif transition:
63+
self.transition = transition
64+
else:
65+
self.transition = Transitions()
5866

5967
def __repr__(self):
6068
return '<Rule: %s>' % self.id
6169

6270
def startElement(self, name, attrs, connection):
6371
if name == 'Transition':
64-
self.transition = Transition()
6572
return self.transition
6673
elif name == 'Expiration':
6774
self.expiration = Expiration()
@@ -139,25 +146,13 @@ class Transition(object):
139146
in ISO 8601 format.
140147
141148
:ivar storage_class: The storage class to transition to. Valid
142-
values are GLACIER.
143-
149+
values are GLACIER, STANDARD_IA.
144150
"""
145151
def __init__(self, days=None, date=None, storage_class=None):
146152
self.days = days
147153
self.date = date
148154
self.storage_class = storage_class
149155

150-
def startElement(self, name, attrs, connection):
151-
return None
152-
153-
def endElement(self, name, value, connection):
154-
if name == 'Days':
155-
self.days = int(value)
156-
elif name == 'Date':
157-
self.date = value
158-
elif name == 'StorageClass':
159-
self.storage_class = value
160-
161156
def __repr__(self):
162157
if self.days is None:
163158
how_long = "on: %s" % self.date
@@ -175,6 +170,86 @@ def to_xml(self):
175170
s += '</Transition>'
176171
return s
177172

173+
class Transitions(list):
174+
"""
175+
A container for the transitions associated with a Lifecycle's Rule configuration.
176+
"""
177+
def __init__(self):
178+
self.transition_properties = 3
179+
self.current_transition_property = 1
180+
self.temp_days = None
181+
self.temp_date = None
182+
self.temp_storage_class = None
183+
184+
def startElement(self, name, attrs, connection):
185+
return None
186+
187+
def endElement(self, name, value, connection):
188+
if name == 'Days':
189+
self.temp_days = int(value)
190+
elif name == 'Date':
191+
self.temp_date = value
192+
elif name == 'StorageClass':
193+
self.temp_storage_class = value
194+
195+
# the XML does not contain a <Transitions> tag
196+
# but rather N number of <Transition> tags not
197+
# structured in any sort of hierarchy.
198+
if self.current_transition_property == self.transition_properties:
199+
self.append(Transition(self.temp_days, self.temp_date, self.temp_storage_class))
200+
self.temp_days = self.temp_date = self.temp_storage_class = None
201+
self.current_transition_property = 1
202+
else:
203+
self.current_transition_property += 1
204+
205+
def to_xml(self):
206+
"""
207+
Returns a string containing the XML version of the Lifecycle
208+
configuration as defined by S3.
209+
"""
210+
s = ''
211+
for transition in self:
212+
s += transition.to_xml()
213+
return s
214+
215+
def add_transition(self, days=None, date=None, storage_class=None):
216+
"""
217+
Add a transition to this Lifecycle configuration. This only adds
218+
the rule to the local copy. To install the new rule(s) on
219+
the bucket, you need to pass this Lifecycle config object
220+
to the configure_lifecycle method of the Bucket object.
221+
222+
:ivar days: The number of days until the object should be moved.
223+
224+
:ivar date: The date when the object should be moved. Should be
225+
in ISO 8601 format.
226+
227+
:ivar storage_class: The storage class to transition to. Valid
228+
values are GLACIER, STANDARD_IA.
229+
"""
230+
transition = Transition(days, date, storage_class)
231+
self.append(transition)
232+
233+
def __first_or_default(self, prop):
234+
for transition in self:
235+
return getattr(transition, prop)
236+
return None
237+
238+
# maintain backwards compatibility so that we can continue utilizing
239+
# 'rule.transition.days' syntax
240+
@property
241+
def days(self):
242+
return self.__first_or_default('days')
243+
244+
@property
245+
def date(self):
246+
return self.__first_or_default('date')
247+
248+
@property
249+
def storage_class(self):
250+
return self.__first_or_default('storage_class')
251+
252+
178253
class Lifecycle(list):
179254
"""
180255
A container for the rules associated with a Lifecycle configuration.
@@ -228,7 +303,7 @@ def add_rule(self, id=None, prefix='', status='Enabled',
228303
that are subject to the rule. The value must be a non-zero
229304
positive integer. A Expiration object instance is also perfect.
230305
231-
:type transition: Transition
306+
:type transition: Transitions
232307
:param transition: Indicates when an object transitions to a
233308
different storage class.
234309
"""

docs/source/s3_tut.rst

Lines changed: 29 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -442,33 +442,38 @@ And, finally, to delete all CORS configurations from a bucket::
442442

443443
>>> bucket.delete_cors()
444444

445-
Transitioning Objects to Glacier
445+
Transitioning Objects
446446
--------------------------------
447447

448-
You can configure objects in S3 to transition to Glacier after a period of
449-
time. This is done using lifecycle policies. A lifecycle policy can also
450-
specify that an object should be deleted after a period of time. Lifecycle
451-
configurations are assigned to buckets and require these parameters:
448+
S3 buckets support transitioning objects to various storage classes. This is
449+
done using lifecycle policies. You can currently transitions objects to
450+
Infrequent Access, Glacier, or just plain Expire. All of these options are
451+
capable of being applied after a number of days or after a given date.
452+
Lifecycle configurations are assigned to buckets and require these parameters:
452453

453-
* The object prefix that identifies the objects you are targeting.
454+
* The object prefix that identifies the objects you are targeting. (or none)
454455
* The action you want S3 to perform on the identified objects.
455-
* The date (or time period) when you want S3 to perform these actions.
456+
* The date or number of days when you want S3 to perform these actions.
456457

457-
For example, given a bucket ``s3-glacier-boto-demo``, we can first retrieve the
458+
For example, given a bucket ``s3-lifecycle-boto-demo``, we can first retrieve the
458459
bucket::
459460

460461
>>> import boto
461462
>>> c = boto.connect_s3()
462-
>>> bucket = c.get_bucket('s3-glacier-boto-demo')
463+
>>> bucket = c.get_bucket('s3-lifecycle-boto-demo')
463464

464465
Then we can create a lifecycle object. In our example, we want all objects
465-
under ``logs/*`` to transition to Glacier 30 days after the object is created.
466+
under ``logs/*`` to transition to Standard IA 30 days after the object is created,
467+
glacier 90 days after creation, and be deleted 120 days after creation.
466468

467469
::
468470

469-
>>> from boto.s3.lifecycle import Lifecycle, Transition, Rule
470-
>>> to_glacier = Transition(days=30, storage_class='GLACIER')
471-
>>> rule = Rule('ruleid', 'logs/', 'Enabled', transition=to_glacier)
471+
>>> from boto.s3.lifecycle import Lifecycle, Transitions, Rule
472+
>>> transitions = Transitions()
473+
>>> transitions.add_transition(days=30, storage_class='STANDARD_IA')
474+
>>> transitions.add_transition(days=90, storage_class='GLACIER')
475+
>>> expiration = Expiration(days=120)
476+
>>> rule = Rule(id='ruleid', prefix='logs/', status='Enabled', expiration=expiration, transition=transitions)
472477
>>> lifecycle = Lifecycle()
473478
>>> lifecycle.append(rule)
474479

@@ -485,19 +490,27 @@ You can also retrieve the current lifecycle policy for the bucket::
485490

486491
>>> current = bucket.get_lifecycle_config()
487492
>>> print current[0].transition
488-
<Transition: in: 30 days, GLACIER>
493+
>>> print current[0].expiration
494+
[<Transition: in: 90 days, GLACIER>, <Transition: in: 30 days, STANDARD_IA>]
495+
<Expiration: in: 120 days>
496+
497+
Note: We have deprecated directly accessing transition properties from the lifecycle
498+
object. You must index into the transition array first.
489499

490-
When an object transitions to Glacier, the storage class will be
500+
When an object transitions, the storage class will be
491501
updated. This can be seen when you **list** the objects in a bucket::
492502

493503
>>> for key in bucket.list():
494504
... print key, key.storage_class
495505
...
496-
<Key: s3-glacier-boto-demo,logs/testlog1.log> GLACIER
506+
<Key: s3-lifecycle-boto-demo,logs/testlog1.log> STANDARD_IA
507+
<Key: s3-lifecycle-boto-demo,logs/testlog2.log> GLACIER
497508

498509
You can also use the prefix argument to the ``bucket.list`` method::
499510

500511
>>> print list(b.list(prefix='logs/testlog1.log'))[0].storage_class
512+
>>> print list(b.list(prefix='logs/testlog2.log'))[0].storage_class
513+
u'STANDARD_IA'
501514
u'GLACIER'
502515

503516

tests/unit/s3/test_lifecycle.py

Lines changed: 93 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -50,27 +50,103 @@ def default_body(self):
5050
<Status>Disabled</Status>
5151
<Transition>
5252
<Date>2012-12-31T00:00:000Z</Date>
53-
<StorageClass>GLACIER</StorageClass>
53+
<StorageClass>STANDARD_IA</StorageClass>
54+
</Transition>
55+
<Expiration>
56+
<Date>2012-12-31T00:00:000Z</Date>
57+
</Expiration>
58+
</Rule>
59+
<Rule>
60+
<ID>multiple-transitions</ID>
61+
<Prefix></Prefix>
62+
<Status>Enabled</Status>
63+
<Transition>
64+
<Days>30</Days>
65+
<StorageClass>STANDARD_IA</StorageClass>
66+
</Transition>
67+
<Transition>
68+
<Days>90</Days>
69+
<StorageClass>GLACIER</StorageClass>
5470
</Transition>
5571
</Rule>
5672
</LifecycleConfiguration>
5773
"""
5874

59-
def test_parse_lifecycle_response(self):
75+
def _get_bucket_lifecycle_config(self):
6076
self.set_http_response(status_code=200)
6177
bucket = Bucket(self.service_connection, 'mybucket')
62-
response = bucket.get_lifecycle_config()
63-
self.assertEqual(len(response), 2)
64-
rule = response[0]
78+
return bucket.get_lifecycle_config()
79+
80+
def test_lifecycle_response_contains_all_rules(self):
81+
self.assertEqual(len(self._get_bucket_lifecycle_config()), 3)
82+
83+
def test_parse_lifecycle_id(self):
84+
rule = self._get_bucket_lifecycle_config()[0]
6585
self.assertEqual(rule.id, 'rule-1')
86+
87+
def test_parse_lifecycle_prefix(self):
88+
rule = self._get_bucket_lifecycle_config()[0]
6689
self.assertEqual(rule.prefix, 'prefix/foo')
90+
91+
def test_parse_lifecycle_no_prefix(self):
92+
rule = self._get_bucket_lifecycle_config()[2]
93+
self.assertEquals(rule.prefix, '')
94+
95+
def test_parse_lifecycle_enabled(self):
96+
rule = self._get_bucket_lifecycle_config()[0]
6797
self.assertEqual(rule.status, 'Enabled')
98+
99+
def test_parse_lifecycle_disabled(self):
100+
rule = self._get_bucket_lifecycle_config()[1]
101+
self.assertEqual(rule.status, 'Disabled')
102+
103+
def test_parse_expiration_days(self):
104+
rule = self._get_bucket_lifecycle_config()[0]
68105
self.assertEqual(rule.expiration.days, 365)
69-
self.assertIsNone(rule.expiration.date)
70-
transition = rule.transition
71-
self.assertEqual(transition.days, 30)
106+
107+
def test_parse_expiration_date(self):
108+
rule = self._get_bucket_lifecycle_config()[1]
109+
self.assertEqual(rule.expiration.date, '2012-12-31T00:00:000Z')
110+
111+
def test_parse_expiration_not_required(self):
112+
rule = self._get_bucket_lifecycle_config()[2]
113+
self.assertIsNone(rule.expiration)
114+
115+
def test_parse_transition_days(self):
116+
transition = self._get_bucket_lifecycle_config()[0].transition[0]
117+
self.assertEquals(transition.days, 30)
118+
self.assertIsNone(transition.date)
119+
120+
def test_parse_transition_days_deprecated(self):
121+
transition = self._get_bucket_lifecycle_config()[0].transition
122+
self.assertEquals(transition.days, 30)
123+
self.assertIsNone(transition.date)
124+
125+
def test_parse_transition_date(self):
126+
transition = self._get_bucket_lifecycle_config()[1].transition[0]
127+
self.assertEquals(transition.date, '2012-12-31T00:00:000Z')
128+
self.assertIsNone(transition.days)
129+
130+
def test_parse_transition_date_deprecated(self):
131+
transition = self._get_bucket_lifecycle_config()[1].transition
132+
self.assertEquals(transition.date, '2012-12-31T00:00:000Z')
133+
self.assertIsNone(transition.days)
134+
135+
def test_parse_storage_class_standard_ia(self):
136+
transition = self._get_bucket_lifecycle_config()[1].transition[0]
137+
self.assertEqual(transition.storage_class, 'STANDARD_IA')
138+
139+
def test_parse_storage_class_glacier(self):
140+
transition = self._get_bucket_lifecycle_config()[0].transition[0]
72141
self.assertEqual(transition.storage_class, 'GLACIER')
73-
self.assertEqual(response[1].transition.date, '2012-12-31T00:00:000Z')
142+
143+
def test_parse_storage_class_deprecated(self):
144+
transition = self._get_bucket_lifecycle_config()[1].transition
145+
self.assertEqual(transition.storage_class, 'STANDARD_IA')
146+
147+
def test_parse_multiple_lifecycle_rules(self):
148+
transition = self._get_bucket_lifecycle_config()[2].transition
149+
self.assertEqual(len(transition), 2)
74150

75151
def test_expiration_with_no_transition(self):
76152
lifecycle = Lifecycle()
@@ -87,7 +163,14 @@ def test_expiration_is_optional(self):
87163
'<Transition><StorageClass>GLACIER</StorageClass><Days>30</Days>',
88164
xml)
89165

90-
def test_expiration_with_expiration_and_transition(self):
166+
def test_transition_is_optional(self):
167+
r = Rule('myid', 'prefix', 'Enabled')
168+
xml = r.to_xml()
169+
self.assertEqual(
170+
'<Rule><ID>myid</ID><Prefix>prefix</Prefix><Status>Enabled</Status></Rule>',
171+
xml)
172+
173+
def test_expiration_and_transition(self):
91174
t = Transition(date='2012-11-30T00:00:000Z', storage_class='GLACIER')
92175
r = Rule('myid', 'prefix', 'Enabled', expiration=30, transition=t)
93176
xml = r.to_xml()

0 commit comments

Comments
 (0)