8000 Refactored existing code, and added integration tests. · firebase/firebase-admin-python@af0fb76 · GitHub
[go: up one dir, main page]

Skip to content

Commit af0fb76

Browse files
author
Alexander Whatley
committed
Refactored existing code, and added integration tests.
1 parent 99b7b25 commit af0fb76

File tree

3 files changed

+93
-46
lines changed

3 files changed

+93
-46
lines changed

.gitignore

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,3 @@
77
*~
88
scripts/cert.json
99
scripts/apikey.txt
10-
serviceAccountCredentials.json
11-
__pycache__
12-
*.pyc

firebase_admin/db.py

Lines changed: 83 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
# limitations under the License.
1414

1515
"""Firebase Realtime Database module.
16+
1617
This module contains functions and classes that facilitate interacting with the Firebase Realtime
1718
Database. It supports basic data manipulation operations, as well as complex queries such as
1819
limit queries and range queries. However, it does not support realtime update notifications. This
@@ -42,12 +43,16 @@
4243

4344
def reference(path='/', app=None):
4445
"""Returns a database Reference representing the node at the specified path.
46+
4547
If no path is specified, this function returns a Reference that represents the database root.
48+
4649
Args:
4750
path: Path to a node in the Firebase realtime database (optional).
4851
app: An App instance (optional).
52+
4953
Returns:
5054
Reference: A newly initialized Reference.
55+
5156
Raises:
5257
ValueError: If the specified path or app is invalid.
5358
"""
@@ -69,6 +74,7 @@ class Reference(object):
6974

7075
def __init__(self, **kwargs):
7176
"""Creates a new Reference using the provided parameters.
77+
7278
This method is for internal use only. Use db.reference() to obtain an instance of
7379
Reference.
7480
"""
@@ -97,12 +103,16 @@ def parent(self):
97103

98104
def child(self, path):
99105
"""Returns a Reference to the specified child node.
106+
100107
The path may point to an immediate child of the current Reference, or a deeply nested
101108
child. Child paths must not begin with '/'.
109+
102110
Args:
103111
path: Path to the child node.
112+
104113
Returns:
105114
Reference: A database Reference representing the specified child node.
115+
106116
Raises:
107117
ValueError: If the child path is not a string, not well-formed or begins with '/'.
108118
"""
@@ -117,28 +127,32 @@ def child(self, path):
117127

118128
def get(self):
119129
"""Returns the value at the current location of the database.
130+
120131
Returns:
121132
object: Decoded JSON value of the current database Reference.
133+
122134
Raises:
123135
ApiCallError: If an error occurs while communicating with the remote database server.
124136
"""
125137
return self._client.request('get', self._add_suffix())
126138

127-
def get_with_etag(self):
139+
def _get_with_etag(self):
128140
"""Returns the value at the current location of the database, along with its ETag.
129-
Returns:
130-
object: Tuple of the ETag value corresponding to the Reference, and the
131-
Decoded JSON value of the current database Reference.
132-
Raises:
133-
ApiCallError: If an error occurs while communicating with the remote database server.
134141
"""
135-
return self._client.request('get', self._add_suffix(), headers={'X-Firebase-ETag': 'true'})
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
136147

137148
def set(self, value):
138149
"""Sets the data at this location to the given value.
150+
139151
The value must be JSON-serializable and not None.
152+
140153
Args:
141154
value: JSON-serialable value to be set at this location.
155+
142156
Raises:
143157
ValueError: If the value is None.
144158
TypeError: If the value is not JSON-serializable.
@@ -150,12 +164,16 @@ def set(self, value):
150164

151165
def push(self, value=''):
152166
"""Creates a new child node.
167+
153168
The optional value argument can be used to provide an initial value for the child node. If
154169
no value is provided, child node will have empty string as the default value.
170+
155171
Args:
156172
value: JSON-serializable initial value for the child node (optional).
173+
157174
Returns:
158175
Reference: A Reference representing the newly created child node.
176+
159177
Raises:
160178
ValueError: If the value is None.
161179
TypeError: If the value is not JSON-serializable.
@@ -169,8 +187,10 @@ def push(self, value=''):
169187

170188
def update(self, value):
171189
"""Updates the specified child keys of this Reference to the provided values.
190+
172191
Args:
173192
value: A dictionary containing the child keys to update, and their new values.
193+
174194
Raises:
175195
ValueError: If value is empty or not a dictionary.
176196
ApiCallError: If an error occurs while communicating with the remote database server.
@@ -181,17 +201,8 @@ def update(self, value):
181201
raise ValueError('Dictionary must not contain None keys or values.')
182202
self._client.request_oneway('patch', self._add_suffix(), json=value, params='print=silent')
183203

184-
def update_with_etag(self, value, etag):
185-
"""Updates the specified child keys of this Reference to the provided values
186-
and uses ETag to make sure data is up to date.
187-
Args:
188-
value: A dictionary containing the child keys to update, and their new values.
189-
etag: ETag value for the Reference.
190-
Returns:
191-
value: None if the update is successful, otherwise the current ETag of the reference
192-
and a snapshot of the data in the database.
193-
Raises:
194-
ValueError: If value is empty or not a dictionary, or if etag is not a string.
204+
def _update_with_etag(self, value, etag):
205+
"""Sets the data at this location to the specified value, if the etag matches.
195206
"""
196207
if not value or not isinstance(value, dict):
197208
raise ValueError('Value argument must be a non-empty dictionary.')
@@ -200,26 +211,33 @@ def update_with_etag(self, value, etag):
200211
if not isinstance(etag, str):
201212
raise ValueError('ETag must be a string.')
202213

214+
success = True
215+
snapshot = value
203216
try:
204217
self._client.request_oneway('put', self._add_suffix(), json=value,
205218
headers={'if-match': etag})
206219
except ApiCallError as error:
207220
detail = error.detail
208-
snapshot = detail.response.json()
221+
success = False
209222
etag = detail.response.headers['ETag']
210-
return etag, snapshot
223+
snapshot = detail.response.json()
224+
225+
return success, etag, snapshot
211226

212227
def delete(self):
213228
"""Deleted this node from the database.
229+
214230
Raises:
215231
ApiCallError: If an error occurs while communicating with the remote database server.
216232
"""
217233
self._client.request_oneway('delete', self._add_suffix())
218234

219235
def transaction(self, transaction_update):
220236
"""Write to database using a transaction.
237+
221238
Args:
222239
transaction_update: function that takes in current database data as a parameter.
240+
223241
Raises:
224242
ValueError: If transaction_update is not a function.
225243
@@ -228,25 +246,28 @@ def transaction(self, transaction_update):
228246
raise ValueError('transaction_update must be a function.')
229247

230248
tries = 0
231-
etag, data = self.get_with_etag()
249+
etag, data = self._get_with_etag()
232250
val = transaction_update(data)
233251
while tries < _TRANSACTION_MAX_RETRIES:
234-
resp = self.update_with_etag(val, etag)
235-
if resp is None:
252+
success, etag, snapshot = self._update_with_etag(val, etag)
253+
if success:
236254
break
237255
else:
238-
etag, data = resp
239-
val = transaction_update(data)
256+
val = transaction_update(snapshot)
240257
tries += 1
241258

242259
def order_by_child(self, path):
243260
"""Returns a Query that orders data by child values.
261+
244262
Returned Query can be used to set additional parameters, and execute complex database
245263
queries (e.g. limit queries, range queries).
264+
246265
Args:
247266
path: Path to a valid child of the current Reference.
267+
248268
Returns:
249269
Query: A database Query instance.
270+
250271
Raises:
251272
ValueError: If the child path is not a string, not well-formed or None.
252273
"""
@@ -256,17 +277,21 @@ def order_by_child(self, path):
256277

257278
def order_by_key(self):
258279
"""Creates a Query that orderes data by key.
280+
259281
Returned Query can be used to set additional parameters, and execute complex database
260282
queries (e.g. limit queries, range queries).
283+
261284
Returns:
262285
Query: A database Query instance.
263286
"""
264287
return Query(order_by='$key', client=self._client, pathurl=self._add_suffix())
265288

266289
def order_by_value(self):
267290
"""Creates a Query that orderes data by value.
291+
268292
Returned Query can be used to set additional parameters, and execute complex database
269293
queries (e.g. limit queries, range queries).
294+
270295
Returns:
271296
Query: A database Query instance.
272297
"""
@@ -287,6 +312,7 @@ def _check_priority(cls, priority):
287312

288313
class Query(object):
289314
"""Represents a complex query that can be executed on a Reference.
315+
290316
Complex queries can consist of up to 2 components: a required ordering constraint, and an
291317
optional filtering constraint. At the server, data is first sorted according to the given
292318
ordering constraint (e.g. order by child). Then the filtering constraint (e.g. limit, range)
@@ -316,10 +342,13 @@ def __init__(self, **kwargs):
316342

317343
def limit_to_first(self, limit):
318344
"""Creates a query with limit, and anchors it to the start of the window.
345+
319346
Args:
320347
limit: The maximum number of child nodes to return.
348+
321349
Returns:
322350
Query: The updated Query instance.
351+
323352
Raises:
324353
ValueError: If the value is not an integer, or set_limit_last() was called previously.
325354
"""
@@ -332,10 +361,13 @@ def limit_to_first(self, limit):
332361

333362
def limit_to_last(self, limit):
334363
"""Creates a query with limit, and anchors it to the end of the window.
364+
335365
Args:
336366
limit: The maximum number of child nodes to return.
367+
337368
Returns:
338369
Query: The updated Query instance.
370+
339371
Raises:
340372
ValueError: If the value is not an integer, or set_limit_first() was called previously.
341373
"""
@@ -348,12 +380,16 @@ def limit_to_last(self, limit):
348380

349381
def start_at(self, start):
350382
"""Sets the lower bound for a range query.
383+
351384
The Query will only return child nodes with a value greater than or equal to the specified
352385
value.
386+
353387
Args:
354388
start: JSON-serializable value to start at, inclusive.
389+
355390
Returns:
356391
Query: The updated Query instance.
392+
357393
Raises:
358394
ValueError: If the value is empty or None.
359395
"""
@@ -364,12 +400,16 @@ def start_at(self, start):
364400

365401
def end_at(self, end):
366402
"""Sets the upper bound for a range query.
403+
367404
The Query will only return child nodes with a value less than or equal to the specified
368405
value.
406+
369407
Args:
370408
end: JSON-serializable value to end at, inclusive.
409+
371410
Returns:
372411
Query: The updated Query instance.
412+
373413
Raises:
374414
ValueError: If the value is empty or None.
375415
"""
@@ -380,11 +420,15 @@ def end_at(self, end):
380420

381421
def equal_to(self, value):
382422
"""Sets an equals constraint on the Query.
423+
383424
The Query will only return child nodes whose value is equal to the specified value.
425+
384426
Args:
385427
value: JSON-serializable value to query for.
428+
386429
Returns:
387430
Query: The updated Query instance.
431+
388432
Raises:
389433
ValueError: If the value is empty or None.
390434
"""
@@ -402,9 +446,12 @@ def _querystr(self):
402446

403447
def get(self):
404448
"""Executes this Query and returns the results.
449+
405450
The results will be returned as a sorted list or an OrderedDict.
451+
406452
Returns:
407453
object: Decoded JSON result of the Query.
454+
408455
Raises:
409456
ApiCallError: If an error occurs while communicating with the remote database server.
410457
"""
@@ -483,6 +530,7 @@ def value(self):
483530
@classmethod
484531
def _get_index_type(cls, index):
485532
"""Assigns an integer code to the type of the index.
533+
486534
The index type determines how differently typed values are sorted. This ordering is based
487535
on https://firebase.google.com/docs/database/rest/retrieve-data#section-rest-ordered-data
488536
"""
@@ -512,6 +560,7 @@ def _extract_child(cls, value, path):
512560

513561
def _compare(self, other):
514562
"""Compares two _SortEntry instances.
563+
515564
If the indices have the same numeric or string type, compare them directly. Ties are
516565
broken by comparing the keys. If the indices have the same type, but are neither numeric
517566
nor string, compare the keys. In all other cases compare based on the ordering provided
@@ -549,14 +598,17 @@ def __eq__(self, other):
549598

550599
class _Client(object):
551600
"""HTTP client used to make REST calls.
601+
552602
_Client maintains an HTTP session, and handles authenticating HTTP requests along with
553603
marshalling and unmarshalling of JSON data.
554604
"""
555605

556606
def __init__(self, **kwargs):
557607
"""Creates a new _Client from the given parameters.
608+
558609
This exists primarily to enable testing. For regular use, obtain _Client instances by
559610
calling the from_app() class method.
611+
560612
Keyword Args:
561613
url: Firebase Realtime Database URL.
562614
session: An HTTP session created using the requests module.
@@ -602,9 +654,10 @@ def from_app(cls, app):
602654
session=session, auth_override=auth_override)
603655

604656
def request(self, method, urlpath, **kwargs):
657+
resp_headers = kwargs.pop('resp_headers', False)
605658
resp = self._do_request(method, urlpath, **kwargs)
606-
if 'headers' in kwargs and kwargs['headers'].get('X-Firebase-ETag') == 'true':
607-
return resp.headers['ETag'], resp.json()
659+
if resp_headers:
660+
return resp.json(), resp.headers
608661
else:
609662
return resp.json()
610663

@@ -619,10 +672,9 @@ def _do_request(self, method, urlpath, **kwargs):
619672
620673
Args:
621674
method: HTTP method name as a string (e.g. get, post).
622-
urlpath: URL path of the remote endpoint. This will be appended to the server's
623-
base URL.
624-
kwargs: An additional set of keyword arguments to be passed into requests
625-
(e.g. json, params).
675+
urlpath: URL path of the remote endpoint. This will be appended to the server's base URL.
676+
kwargs: An additional set of keyword arguments to be passed into requests API
677+
(e.g. json, params).
626678
627679
Returns:
628680
Response: An HTTP response object.

0 commit comments

Comments
 (0)
0