@@ -125,31 +125,58 @@ def child(self, path):
125125 full_path = self ._pathurl + '/' + path
126126 return Reference (client = self ._client , path = full_path )
127127
128- def get (self ):
129- """Returns the value at the current location of the database.
128+ def get (self , etag = False ):
129+ """Returns the value, and possibly the ETag, at the current location of the database.
130130
131131 Returns:
132- object: Decoded JSON value of the current database Reference.
132+ object: Decoded JSON value of the current database Reference if etag=False, otherwise
133+ the decoded JSON value and the corresponding ETag.
133134
134135 Raises:
135136 ApiCallError: If an error occurs while communicating with the remote database server.
136137 """
137- return self ._client .request ('get' , self ._add_suffix ())
138+ if etag :
139+ data , headers = self ._client .request ('get' , self ._add_suffix (),
140+ headers = {'X-Firebase-ETag' : 'true' },
141+ resp_headers = True )
142+ etag = headers .get ('ETag' )
143+ return data , etag
144+ else :
145+ return self ._client .request ('get' , self ._add_suffix ())
146+
147+ def get_if_changed (self , etag ):
148+ """Get data in this location if the ETag no longer matches.
149+
150+ Args:
151+ etag: The ETag value we want to check against the ETag in the current location.
152+
153+ Returns:
154+ object: Tuple of boolean of whether the request was successful, current location's etag,
155+ and snapshot of location's data if passed in etag does not match.
156+
157+ Raises:
158+ ValueError: If the ETag is not a string.
159+ """
160+ #pylint: disable=protected-access
161+ if not isinstance (etag , six .string_types ):
162+ raise ValueError ('ETag must be a string.' )
138163
139- def _get_with_etag (self ):
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 )
143- etag = headers .get ('ETag' )
144- return etag , data
164+ resp = self ._client ._do_request ('get' , self ._add_suffix (),
165+ headers = {'if-none-match' : etag })
166+ if resp .status_code == 200 :
167+ value , headers = resp .json (), resp .headers
168+ new_etag = headers .get ('ETag' )
169+ return True , new_etag , value
170+ elif resp .status_code == 304 :
171+ return False , None , None
145172
146173 def set (self , value ):
147174 """Sets the data at this location to the given value.
148175
149176 The value must be JSON-serializable and not None.
150177
151178 Args:
152- value: JSON-serialable value to be set at this location.
179+ value: JSON-serializable value to be set at this location.
153180
154181 Raises:
155182 ValueError: If the value is None.
@@ -160,6 +187,41 @@ def set(self, value):
160187 raise ValueError ('Value must not be None.' )
161188 self ._client .request_oneway ('put' , self ._add_suffix (), json = value , params = 'print=silent' )
162189
190+ def set_if_unchanged (self , expected_etag , value ):
191+ """Sets the data at this location to the given value, if expected_etag is the same as the
192+ correct ETag value.
193+
194+ Args:
195+ expected_etag: Value of ETag we want to check.
196+ value: JSON-serializable value to be set at this location.
197+
198+ Returns:
199+ object: Tuple of boolean of whether the request was successful, current location's etag,
200+ and snapshot of location's data if passed in etag does not match.
201+
202+ Raises:
203+ ValueError: If the value is None, or if expected_etag is not a string.
204+ ApiCallError: If an error occurs while communicating with the remote database server.
205+ """
206+ # pylint: disable=missing-raises-doc
207+ if not isinstance (expected_etag , six .string_types ):
208+ raise ValueError ('Expected ETag must be a string.' )
209+ if value is None :
210+ raise ValueError ('Value must not be none.' )
211+
212+ try :
213+ self ._client .request_oneway ('put' , self ._add_suffix (),
214+ json = value , headers = {'if-match' : expected_etag })
215+ return True , expected_etag , value
216+ except ApiCallError as error :
217+ detail = error .detail
218+ if detail .response is not None and 'ETag' in detail .response .headers :
219+ etag = detail .response .headers ['ETag' ]
220+ snapshot = detail .response .json ()
221+ return False , etag , snapshot
222+ else :
223+ raise error
224+
163225 def push (self , value = '' ):
164226 """Creates a new child node.
165227
@@ -197,25 +259,8 @@ def update(self, value):
197259 raise ValueError ('Value argument must be a non-empty dictionary.' )
198260 if None in value .keys () or None in value .values ():
199261 raise ValueError ('Dictionary must not contain None keys or values.' )
200- self ._client .request_oneway ('patch' , self ._add_suffix (), json = value , params = 'print=silent' )
201-
202- def _update_with_etag (self , value , etag ):
203- """Sets the data at this location to the specified value, if the etag matches."""
204- if not isinstance (etag , six .string_types ):
205- raise ValueError ('ETag must be a string.' )
206-
207- try :
208- self ._client .request_oneway (
209- 'put' , self ._add_suffix (), json = value , headers = {'if-match' : etag })
210- return True , etag , value
211- except ApiCallError as error :
212- detail = error .detail
213- if detail .response is not None and 'ETag' in detail .response .headers :
214- etag = detail .response .headers ['ETag' ]
215- snapshot = detail .response .json ()
216- return False , etag , snapshot
217- else :
218- raise error
262+ self ._client .request_oneway ('patch' , self ._add_suffix (), json = value ,
263+ params = 'print=silent' )
219264
220265 def delete (self ):
221266 """Deletes this node from the database.
@@ -253,16 +298,15 @@ def transaction(self, transaction_update):
253298 Raises:
254299 TransactionError: If the transaction aborts after exhausting all retry attempts.
255300 ValueError: If transaction_update is not a function.
256-
257301 """
258302 if not callable (transaction_update ):
259303 raise ValueError ('transaction_update must be a function.' )
260304
261305 tries = 0
262- etag , data = self ._get_with_etag ( )
306+ data , etag = self .get ( etag = True )
263307 while tries < _TRANSACTION_MAX_RETRIES :
264308 new_data = transaction_update (data )
265- success , etag , data = self ._update_with_etag ( new_data , etag )
309+ success , etag , data = self .set_if_unchanged ( etag , new_data )
266310 if success :
267311 return new_data
268312 tries += 1
@@ -480,14 +524,14 @@ def __init__(self, message, error):
480524 Exception .__init__ (self , message )
481525 self .detail = error
482526
483-
484527class TransactionError (Exception ):
485528 """Represents an Exception encountered while performing a transaction."""
486529
487530 def __init__ (self , message ):
488531 Exception .__init__ (self , message )
489532
490533
534+
491535class _Sorter (object ):
492536 """Helper class for sorting query results."""
493537
@@ -674,8 +718,11 @@ def from_app(cls, app):
674718
675719 def request (self , method , urlpath , ** kwargs ):
676720 resp_headers = kwargs .pop ('resp_headers' , False )
721+ params = kwargs .get ('params' , None )
677722 resp = self ._do_request (method , urlpath , ** kwargs )
678- if resp_headers :
723+ if resp_headers and params == 'print=silent' :
724+ return resp .headers
725+ elif resp_headers :
679726 return resp .json (), resp .headers
680727 else :
681728 return resp .json ()
0 commit comments