@@ -125,31 +125,58 @@ def child(self, path):
125
125
full_path = self ._pathurl + '/' + path
126
126
return Reference (client = self ._client , path = full_path )
127
127
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.
130
130
131
131
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.
133
134
134
135
Raises:
135
136
ApiCallError: If an error occurs while communicating with the remote database server.
136
137
"""
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.' )
138
163
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
145
172
146
173
def set (self , value ):
147
174
"""Sets the data at this location to the given value.
148
175
149
176
The value must be JSON-serializable and not None.
150
177
151
178
Args:
152
- value: JSON-serialable value to be set at this location.
179
+ value: JSON-serializable value to be set at this location.
153
180
154
181
Raises:
155
182
ValueError: If the value is None.
@@ -160,6 +187,41 @@ def set(self, value):
160
187
raise ValueError ('Value must not be None.' )
161
188
self ._client .request_oneway ('put' , self ._add_suffix (), json = value , params = 'print=silent' )
162
189
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
+
163
225
def push (self , value = '' ):
164
226
"""Creates a new child node.
165
227
@@ -197,25 +259,8 @@ def update(self, value):
197
259
raise ValueError ('Value argument must be a non-empty dictionary.' )
198
260
if None in value .keys () or None in value .values ():
199
261
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' )
219
264
220
265
def delete (self ):
221
266
"""Deletes this node from the database.
@@ -253,16 +298,15 @@ def transaction(self, transaction_update):
253
298
Raises:
254
299
TransactionError: If the transaction aborts after exhausting all retry attempts.
255
300
ValueError: If transaction_update is not a function.
256
-
257
301
"""
258
302
if not callable (transaction_update ):
259
303
raise ValueError ('transaction_update must be a function.' )
260
304
261
305
tries = 0
262
- etag , data = self ._get_with_etag ( )
306
+ data , etag = self .get ( etag = True )
263
307
while tries < _TRANSACTION_MAX_RETRIES :
264
308
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 )
266
310
if success :
267
311
return new_data
268
312
tries += 1
@@ -480,14 +524,14 @@ def __init__(self, message, error):
480
524
Exception .__init__ (self , message )
481
525
self .detail = error
482
526
483
-
484
527
class TransactionError (Exception ):
485
528
"""Represents an Exception encountered while performing a transaction."""
486
529
487
530
def __init__ (self , message ):
488
531
Exception .__init__ (self , message )
489
532
490
533
534
+
491
535
class _Sorter (object ):
492
536
"""Helper class for sorting query results."""
493
537
@@ -674,8 +718,11 @@ def from_app(cls, app):
674
718
675
719
def request (self , method , urlpath , ** kwargs ):
676
720
resp_headers = kwargs .pop ('resp_headers' , False )
721
+ params = kwargs .get ('params' , None )
677
722
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 :
679
726
return resp .json (), resp .headers
680
727
else :
681
728
return resp .json ()
0 commit comments