@@ -137,11 +137,9 @@ def get(self):
137
137
return self ._client .request ('get' , self ._add_suffix ())
138
138
139
139
def _get_with_etag (self ):
140
- """Returns the value at the current location of the database, along with its ETag.
141
- """
142
- data , headers = self ._client .request ('get' , self ._add_suffix (),
143
- headers = {'X-Firebase-ETag' : 'true' },
144
- resp_headers = True )
140
+ """Returns the value at the current location of the database, along with its ETag."""
141
+ data , headers = self ._client .request (
142
+ 'get' , self ._add_suffix (), headers = {'X-Firebase-ETag' : 'true' }, resp_headers = True )
145
143
etag = headers .get ('ETag' )
146
144
return etag , data
147
145
@@ -202,49 +200,62 @@ def update(self, value):
202
200
self ._client .request_oneway ('patch' , self ._add_suffix (), json = value , params = 'print=silent' )
203
201
204
202
def _update_with_etag (self , value , etag ):
205
- """Sets the data at this location to the specified value, if the etag matches.
206
- """
203
+ """Sets the data at this location to the specified value, if the etag matches."""
207
204
if not value or not isinstance (value , dict ):
208
205
raise ValueError ('Value argument must be a non-empty dictionary.' )
209
206
if None in value .keys () or None in value .values ():
210
207
raise ValueError ('Dictionary must not contain None keys or values.' )
211
208
if not isinstance (etag , six .string_types ):
212
209
raise ValueError ('ETag must be a string.' )
213
210
214
- success = True
215
- snapshot = value
216
211
try :
217
- self ._client .request_oneway ('put' , self ._add_suffix (), json = value ,
218
- headers = {'if-match' : etag })
212
+ self ._client .request_oneway (
213
+ 'put' , self ._add_suffix (), json = value , headers = {'if-match' : etag })
214
+ return True , etag , value
219
215
except ApiCallError as error :
220
216
detail = error .detail
221
- if detail .response . headers and 'ETag' in detail .response .headers :
217
+ if detail .response is not None and 'ETag' in detail .response .headers :
222
218
etag = detail .response .headers ['ETag' ]
223
219
snapshot = detail .response .json ()
224
220
return False , etag , snapshot
225
221
else :
226
222
raise error
227
223
228
- return success , etag , snapshot
229
-
230
224
def delete (self ):
231
- """Deleted this node from the database.
225
+ """Deletes this node from the database.
232
226
233
227
Raises:
234
228
ApiCallError: If an error occurs while communicating with the remote database server.
235
229
"""
236
230
self ._client .request_oneway ('delete' , self ._add_suffix ())
237
231
238
232
def transaction (self , transaction_update ):
239
- """Write to database using a transaction.
233
+ """Atomically modifies the data at this location.
234
+
235
+ Unlike a normal `set()`, which just overwrites the data regardless of its previous state,
236
+ `transaction()` is used to modify the existing value to a new value, ensuring there are
237
+ no conflicts with other clients simultaneously writing to the same location.
238
+
239
+ This is accomplished by passing an update function which is used to transform the current
240
+ value of this reference into a new value. If another client writes to this location before
241
+ the new value is successfully saved, the update function is called again with the new
242
+ current value, and the write will be retried. In case of repeated failures, this method
243
+ will retry the transaction up to 25 times before giving up and raising a TransactionError.
244
+ The update function may also force an early abort by raising an exception instead of
245
+ returning a value.
240
246
241
247
Args:
242
- transaction_update: function that takes in current database data as a parameter.
248
+ transaction_update: A function which will be passed the current data stored at this
249
+ location. The function should return the new value it would like written. If
250
+ an exception is raised, the transaction will be aborted, and the data at this
251
+ location will not be modified. The exceptions raised by this function are
252
+ propagated to the caller of the transaction method.
243
253
244
254
Returns:
245
- bool: True if transaction is successful, otherwise False .
255
+ object: New value of the current database Reference (only if the transaction commits) .
246
256
247
257
Raises:
258
+ TransactionError: If the transaction aborts after exhausting all retry attempts.
248
259
ValueError: If transaction_update is not a function.
249
260
250
261
"""
@@ -253,16 +264,13 @@ def transaction(self, transaction_update):
253
264
254
265
tries = 0
255
266
etag , data = self ._get_with_etag ()
256
- val = transaction_update (data )
257
267
while tries < _TRANSACTION_MAX_RETRIES :
258
- success , etag , snapshot = self ._update_with_etag (val , etag )
268
+ new_data = transaction_update (data )
269
+ success , etag , data = self ._update_with_etag (new_data , etag )
259
270
if success :
260
- return True
261
- else :
262
- val = transaction_update (snapshot )
263
- tries += 1
264
-
265
- return False
271
+ return new_data
272
+ tries += 1
273
+ raise TransactionError ('Transaction aborted after failed retries.' )
266
274
267
275
def order_by_child (self , path ):
268
276
"""Returns a Query that orders data by child values.
@@ -477,6 +485,13 @@ def __init__(self, message, error):
477
485
self .detail = error
478
486
479
487
488
+ class TransactionError (Exception ):
489
+ """Represents an Exception encountered while performing a transaction."""
490
+
491
+ def __init__ (self , message ):
492
+ Exception .__init__ (self , message )
493
+
494
+
480
495
class _Sorter (object ):
481
496
"""Helper class for sorting query results."""
482
497
0 commit comments