3838_RESERVED_FILTERS = ('$key' , '$value' , '$priority' )
3939_USER_AGENT = 'Firebase/HTTP/{0}/{1}.{2}/AdminPython' .format (
4040 firebase_admin .__version__ , sys .version_info .major , sys .version_info .minor )
41+ _TRANSACTION_MAX_RETRIES = 25
4142
4243
4344def reference (path = '/' , app = None ):
@@ -135,6 +136,15 @@ def get(self):
135136 """
136137 return self ._client .request ('get' , self ._add_suffix ())
137138
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 )
145+ etag = headers .get ('ETag' )
146+ return etag , data
147+
138148 def set (self , value ):
139149 """Sets the data at this location to the given value.
140150
@@ -191,6 +201,32 @@ def update(self, value):
191201 raise ValueError ('Dictionary must not contain None keys or values.' )
192202 self ._client .request_oneway ('patch' , self ._add_suffix (), json = value , params = 'print=silent' )
193203
204+ def _update_with_etag (self , value , etag ):
205+ """Sets the data at this location to the specified value, if the etag matches.
206+ """
207+ if not value or not isinstance (value , dict ):
208+ raise ValueError ('Value argument must be a non-empty dictionary.' )
209+ if None in value .keys () or None in value .values ():
210+ raise ValueError ('Dictionary must not contain None keys or values.' )
211+ if not isinstance (etag , six .string_types ):
212+ raise ValueError ('ETag must be a string.' )
213+
214+ success = True
215+ snapshot = value
216+ try :
217+ self ._client .request_oneway ('put' , self ._add_suffix (), json = value ,
218+ headers = {'if-match' : etag })
219+ except ApiCallError as error :
220+ detail = error .detail
221+ if detail .response .headers and 'ETag' in detail .response .headers :
222+ etag = detail .response .headers ['ETag' ]
223+ snapshot = detail .response .json ()
224+ return False , etag , snapshot
225+ else :
226+ raise error
227+
228+ return success , etag , snapshot
229+
194230 def delete (self ):
195231 """Deleted this node from the database.
196232
@@ -199,6 +235,35 @@ def delete(self):
199235 """
200236 self ._client .request_oneway ('delete' , self ._add_suffix ())
201237
238+ def transaction (self , transaction_update ):
239+ """Write to database using a transaction.
240+
241+ Args:
242+ transaction_update: function that takes in current database data as a parameter.
243+
244+ Returns:
245+ bool: True if transaction is successful, otherwise False.
246+
247+ Raises:
248+ ValueError: If transaction_update is not a function.
249+
250+ """
251+ if not callable (transaction_update ):
252+ raise ValueError ('transaction_update must be a function.' )
253+
254+ tries = 0
255+ etag , data = self ._get_with_etag ()
256+ val = transaction_update (data )
257+ while tries < _TRANSACTION_MAX_RETRIES :
258+ success , etag , snapshot = self ._update_with_etag (val , etag )
259+ if success :
260+ return True
261+ else :
262+ val = transaction_update (snapshot )
263+ tries += 1
264+
265+ return False
266+
202267 def order_by_child (self , path ):
203268 """Returns a Query that orders data by child values.
204269
@@ -597,7 +662,12 @@ def from_app(cls, app):
597662 session = session , auth_override = auth_override )
598663
599664 def request (self , method , urlpath , ** kwargs ):
600- return self ._do_request (method , urlpath , ** kwargs ).json ()
665+ resp_headers = kwargs .pop ('resp_headers' , False )
666+ resp = self ._do_request (method , urlpath , ** kwargs )
667+ if resp_headers :
668+ return resp .json (), resp .headers
669+ else :
670+ return resp .json ()
601671
602672 def request_oneway (self , method , urlpath , ** kwargs ):
603673 self ._do_request (method , urlpath , ** kwargs )
0 commit comments