empyrebase 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
empyrebase/__init__.py ADDED
@@ -0,0 +1 @@
1
+ from .pyrebase import initialize_app
@@ -0,0 +1,51 @@
1
+ import requests
2
+ from requests.adapters import HTTPAdapter
3
+
4
+ from oauth2client.service_account import ServiceAccountCredentials
5
+
6
+ from .services import Auth, Database, Firestore, Storage
7
+
8
+
9
+ def initialize_app(config):
10
+ return Firebase(config)
11
+
12
+
13
+ class Firebase:
14
+ """ Firebase Interface """
15
+ def __init__(self, config):
16
+ self.api_key = config["apiKey"]
17
+ self.auth_domain = config["authDomain"]
18
+ self.database_url = config["databaseURL"]
19
+ self.storage_bucket = config["storageBucket"]
20
+ self.project_id = config["projectId"]
21
+ self.credentials = None
22
+ self.requests = requests.Session()
23
+ if config.get("serviceAccount"):
24
+ scopes = [
25
+ 'https://www.googleapis.com/auth/firebase.database',
26
+ 'https://www.googleapis.com/auth/userinfo.email',
27
+ "https://www.googleapis.com/auth/cloud-platform",
28
+ "https://firebasestorage.googleapis.com/",
29
+ ]
30
+ service_account_type = type(config["serviceAccount"])
31
+ if service_account_type is str:
32
+ self.credentials = ServiceAccountCredentials.from_json_keyfile_name(config["serviceAccount"], scopes)
33
+ if service_account_type is dict:
34
+ self.credentials = ServiceAccountCredentials.from_json_keyfile_dict(config["serviceAccount"], scopes)
35
+
36
+ adapter = HTTPAdapter()
37
+
38
+ for scheme in ('http://', 'https://'):
39
+ self.requests.mount(scheme, adapter)
40
+
41
+ def auth(self):
42
+ return Auth(self.api_key, self.requests, self.credentials)
43
+
44
+ def database(self):
45
+ return Database(self.credentials, self.api_key, self.database_url, self.requests)
46
+
47
+ def firestore(self, firebase_path: str="/", database_name="(default)", auth_id: str | None=None):
48
+ return Firestore(self.requests, self.project_id, firebase_path, database_name, auth_id)
49
+
50
+ def storage(self):
51
+ return Storage(self.credentials, self.storage_bucket, self.requests)
@@ -0,0 +1,4 @@
1
+ from .auth import Auth
2
+ from .database import Database
3
+ from .firestore import Firestore
4
+ from .storage import Storage
@@ -0,0 +1,131 @@
1
+ import datetime
2
+ import json
3
+ import jwt as jwt
4
+ from pyrebase.utils import raise_detailed_error
5
+ import requests
6
+ from Crypto.PublicKey import RSA
7
+
8
+
9
+ class Auth:
10
+ """ Authentication Service """
11
+ def __init__(self, api_key, requests, credentials):
12
+ self.api_key = api_key
13
+ self.current_user = None
14
+ self.requests = requests
15
+ self.credentials = credentials
16
+
17
+ def sign_in_with_email_and_password(self, email, password):
18
+ request_ref = "https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyPassword?key={0}".format(self.api_key)
19
+ headers = {"content-type": "application/json; charset=UTF-8"}
20
+ data = json.dumps({"email": email, "password": password, "returnSecureToken": True})
21
+ request_object = requests.post(request_ref, headers=headers, data=data)
22
+ raise_detailed_error(request_object)
23
+ self.current_user = request_object.json()
24
+ return request_object.json()
25
+
26
+ def sign_in_anonymous(self):
27
+ request_ref = "https://www.googleapis.com/identitytoolkit/v3/relyingparty/signupNewUser?key={0}".format(self.api_key)
28
+ headers = {"content-type": "application/json; charset=UTF-8" }
29
+ data = json.dumps({"returnSecureToken": True})
30
+ request_object = requests.post(request_ref, headers=headers, data=data)
31
+ raise_detailed_error(request_object)
32
+ self.current_user = request_object.json()
33
+ return request_object.json()
34
+
35
+ def create_custom_token(self, uid, additional_claims=None, expiry_minutes=60):
36
+ service_account_email = self.credentials.service_account_email
37
+ private_key = self.credentials._private_key_pkcs8_pem
38
+ payload = {
39
+ "iss": service_account_email,
40
+ "sub": service_account_email,
41
+ "aud": "https://identitytoolkit.googleapis.com/google.identity.identitytoolkit.v1.IdentityToolkit",
42
+ "uid": uid,
43
+ "iat": datetime.datetime.now(datetime.timezone.utc), # issued at time
44
+ "exp": datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(minutes=expiry_minutes)
45
+ }
46
+ if additional_claims:
47
+ payload.update(additional_claims)
48
+ token = jwt.encode(payload, private_key, algorithm="RS256") #create token
49
+ return token
50
+
51
+ def sign_in_with_custom_token(self, token):
52
+ request_ref = "https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyCustomToken?key={0}".format(self.api_key)
53
+ headers = {"content-type": "application/json; charset=UTF-8"}
54
+ data = json.dumps({"returnSecureToken": True, "token": token})
55
+ request_object = requests.post(request_ref, headers=headers, data=data)
56
+ raise_detailed_error(request_object)
57
+ return request_object.json()
58
+
59
+ def refresh(self, refresh_token):
60
+ request_ref = "https://securetoken.googleapis.com/v1/token?key={0}".format(self.api_key)
61
+ headers = {"content-type": "application/json; charset=UTF-8"}
62
+ data = json.dumps({"grantType": "refresh_token", "refreshToken": refresh_token})
63
+ request_object = requests.post(request_ref, headers=headers, data=data)
64
+ raise_detailed_error(request_object)
65
+ request_object_json = request_object.json()
66
+ # handle weirdly formatted response
67
+ user = {
68
+ "userId": request_object_json["user_id"],
69
+ "idToken": request_object_json["id_token"],
70
+ "refreshToken": request_object_json["refresh_token"]
71
+ }
72
+ return user
73
+
74
+ def get_account_info(self, id_token):
75
+ request_ref = "https://www.googleapis.com/identitytoolkit/v3/relyingparty/getAccountInfo?key={0}".format(self.api_key)
76
+ headers = {"content-type": "application/json; charset=UTF-8"}
77
+ data = json.dumps({"idToken": id_token})
78
+ request_object = requests.post(request_ref, headers=headers, data=data)
79
+ raise_detailed_error(request_object)
80
+ return request_object.json()
81
+
82
+ def send_email_verification(self, id_token):
83
+ request_ref = "https://www.googleapis.com/identitytoolkit/v3/relyingparty/getOobConfirmationCode?key={0}".format(self.api_key)
84
+ headers = {"content-type": "application/json; charset=UTF-8"}
85
+ data = json.dumps({"requestType": "VERIFY_EMAIL", "idToken": id_token})
86
+ request_object = requests.post(request_ref, headers=headers, data=data)
87
+ raise_detailed_error(request_object)
88
+ return request_object.json()
89
+
90
+ def send_password_reset_email(self, email):
91
+ request_ref = "https://www.googleapis.com/identitytoolkit/v3/relyingparty/getOobConfirmationCode?key={0}".format(self.api_key)
92
+ headers = {"content-type": "application/json; charset=UTF-8"}
93
+ data = json.dumps({"requestType": "PASSWORD_RESET", "email": email})
94
+ request_object = requests.post(request_ref, headers=headers, data=data)
95
+ raise_detailed_error(request_object)
96
+ return request_object.json()
97
+
98
+ def verify_password_reset_code(self, reset_code, new_password):
99
+ request_ref = "https://www.googleapis.com/identitytoolkit/v3/relyingparty/resetPassword?key={0}".format(self.api_key)
100
+ headers = {"content-type": "application/json; charset=UTF-8"}
101
+ data = json.dumps({"oobCode": reset_code, "newPassword": new_password})
102
+ request_object = requests.post(request_ref, headers=headers, data=data)
103
+ raise_detailed_error(request_object)
104
+ return request_object.json()
105
+
106
+ def create_user_with_email_and_password(self, email, password):
107
+ request_ref = "https://www.googleapis.com/identitytoolkit/v3/relyingparty/signupNewUser?key={0}".format(self.api_key)
108
+ headers = {"content-type": "application/json; charset=UTF-8" }
109
+ data = json.dumps({"email": email, "password": password, "returnSecureToken": True})
110
+ request_object = requests.post(request_ref, headers=headers, data=data)
111
+ raise_detailed_error(request_object)
112
+ return request_object.json()
113
+
114
+ def delete_user_account(self, id_token):
115
+ request_ref = "https://www.googleapis.com/identitytoolkit/v3/relyingparty/deleteAccount?key={0}".format(self.api_key)
116
+ headers = {"content-type": "application/json; charset=UTF-8"}
117
+ data = json.dumps({"idToken": id_token})
118
+ request_object = requests.post(request_ref, headers=headers, data=data)
119
+ raise_detailed_error(request_object)
120
+ return request_object.json()
121
+
122
+ def update_profile(self, id_token, display_name = None, photo_url = None, delete_attribute = None):
123
+ """
124
+ https://firebase.google.com/docs/reference/rest/auth#section-update-profile
125
+ """
126
+ request_ref = "https://identitytoolkit.googleapis.com/v1/accounts:update?key={0}".format(self.api_key)
127
+ headers = {"content-type": "application/json; charset=UTF-8"}
128
+ data = json.dumps({"idToken": id_token, "displayName": display_name, "photoURL": photo_url, "deleteAttribute": delete_attribute, "returnSecureToken": True})
129
+ request_object = requests.post(request_ref, headers=headers, data=data)
130
+ raise_detailed_error(request_object)
131
+ return request_object.json()
@@ -0,0 +1,254 @@
1
+ import json
2
+ import math
3
+ from random import randrange
4
+ import time
5
+ from urllib.parse import urlencode
6
+ from pyrebase.utils import convert_list_to_pyre, convert_to_pyre, raise_detailed_error
7
+ from pyrebase.types import PyreResponse, Stream
8
+
9
+
10
+ class Database:
11
+ """ Database Service """
12
+ def __init__(self, credentials, api_key, database_url, requests):
13
+
14
+ if not database_url.endswith('/'):
15
+ url = ''.join([database_url, '/'])
16
+ else:
17
+ url = database_url
18
+
19
+ self.credentials = credentials
20
+ self.api_key = api_key
21
+ self.database_url = url
22
+ self.requests = requests
23
+
24
+ self.path = ""
25
+ self.build_query = {}
26
+ self.last_push_time = 0
27
+ self.last_rand_chars = []
28
+
29
+ def order_by_key(self):
30
+ self.build_query["orderBy"] = "$key"
31
+ return self
32
+
33
+ def order_by_value(self):
34
+ self.build_query["orderBy"] = "$value"
35
+ return self
36
+
37
+ def order_by_child(self, order):
38
+ self.build_query["orderBy"] = order
39
+ return self
40
+
41
+ def start_at(self, start):
42
+ self.build_query["startAt"] = start
43
+ return self
44
+
45
+ def end_at(self, end):
46
+ self.build_query["endAt"] = end
47
+ return self
48
+
49
+ def equal_to(self, equal):
50
+ self.build_query["equalTo"] = equal
51
+ return self
52
+
53
+ def limit_to_first(self, limit_first):
54
+ self.build_query["limitToFirst"] = limit_first
55
+ return self
56
+
57
+ def limit_to_last(self, limit_last):
58
+ self.build_query["limitToLast"] = limit_last
59
+ return self
60
+
61
+ def shallow(self):
62
+ self.build_query["shallow"] = True
63
+ return self
64
+
65
+ def child(self, *args):
66
+ new_path = "/".join([str(arg) for arg in args])
67
+ if self.path:
68
+ self.path += "/{}".format(new_path)
69
+ else:
70
+ if new_path.startswith("/"):
71
+ new_path = new_path[1:]
72
+ self.path = new_path
73
+ return self
74
+
75
+ def build_request_url(self, token):
76
+ parameters = {}
77
+ if token:
78
+ parameters['auth'] = token
79
+ for param in list(self.build_query):
80
+ if type(self.build_query[param]) is str:
81
+ parameters[param] = '"' + self.build_query[param] + '"'
82
+ elif type(self.build_query[param]) is bool:
83
+ parameters[param] = "true" if self.build_query[param] else "false"
84
+ else:
85
+ parameters[param] = self.build_query[param]
86
+ # reset path and build_query for next query
87
+ request_ref = '{0}{1}.json?{2}'.format(self.database_url, self.path, urlencode(parameters))
88
+ self.path = ""
89
+ self.build_query = {}
90
+ return request_ref
91
+
92
+ def build_headers(self, token=None):
93
+ headers = {"content-type": "application/json; charset=UTF-8"}
94
+ if not token and self.credentials:
95
+ access_token = self.credentials.get_access_token().access_token
96
+ headers['Authorization'] = 'Bearer ' + access_token
97
+ return headers
98
+
99
+ def get(self, token=None, json_kwargs=None):
100
+ json_kwargs = json_kwargs or {}
101
+ build_query = self.build_query
102
+ query_key = self.path.split("/")[-1]
103
+ request_ref = self.build_request_url(token)
104
+ # headers
105
+ headers = self.build_headers(token)
106
+ # do request
107
+ request_object = self.requests.get(request_ref, headers=headers)
108
+ raise_detailed_error(request_object)
109
+ request_dict = request_object.json(**json_kwargs)
110
+
111
+ # if primitive or simple query return
112
+ if isinstance(request_dict, list):
113
+ return PyreResponse(convert_list_to_pyre(request_dict), query_key)
114
+ if not isinstance(request_dict, dict):
115
+ return PyreResponse(request_dict, query_key)
116
+ if not build_query:
117
+ return PyreResponse(convert_to_pyre(request_dict.items()), query_key)
118
+ # return keys if shallow
119
+ if build_query.get("shallow"):
120
+ return PyreResponse(request_dict.keys(), query_key)
121
+ # otherwise sort
122
+ sorted_response = None
123
+ if build_query.get("orderBy"):
124
+ if build_query["orderBy"] == "$key":
125
+ sorted_response = sorted(request_dict.items(), key=lambda item: item[0])
126
+ elif build_query["orderBy"] == "$value":
127
+ sorted_response = sorted(request_dict.items(), key=lambda item: item[1])
128
+ else:
129
+ sorted_response = sorted(request_dict.items(), key=lambda item: (build_query["orderBy"] in item[1], item[1].get(build_query["orderBy"], "")))
130
+ return PyreResponse(convert_to_pyre(sorted_response), query_key)
131
+
132
+ def push(self, data, token=None, json_kwargs=None):
133
+ json_kwargs = json_kwargs or {}
134
+ request_ref = self.check_token(self.database_url, self.path, token)
135
+ self.path = ""
136
+ headers = self.build_headers(token)
137
+ request_object = self.requests.post(request_ref, headers=headers, data=json.dumps(data, **json_kwargs).encode("utf-8"))
138
+ raise_detailed_error(request_object)
139
+ return request_object.json()
140
+
141
+ def set(self, data, token=None, json_kwargs=None):
142
+ json_kwargs = json_kwargs or {}
143
+ request_ref = self.check_token(self.database_url, self.path, token)
144
+ self.path = ""
145
+ headers = self.build_headers(token)
146
+ request_object = self.requests.put(request_ref, headers=headers, data=json.dumps(data, **json_kwargs).encode("utf-8"))
147
+ raise_detailed_error(request_object)
148
+ return request_object.json()
149
+
150
+ def update(self, data, token=None, json_kwargs=None):
151
+ json_kwargs = json_kwargs or {}
152
+ request_ref = self.check_token(self.database_url, self.path, token)
153
+ self.path = ""
154
+ headers = self.build_headers(token)
155
+ request_object = self.requests.patch(request_ref, headers=headers, data=json.dumps(data, **json_kwargs).encode("utf-8"))
156
+ raise_detailed_error(request_object)
157
+ return request_object.json()
158
+
159
+ def remove(self, token=None):
160
+ request_ref = self.check_token(self.database_url, self.path, token)
161
+ self.path = ""
162
+ headers = self.build_headers(token)
163
+ request_object = self.requests.delete(request_ref, headers=headers)
164
+ raise_detailed_error(request_object)
165
+ return request_object.json()
166
+
167
+ def stream(self, stream_handler, token=None, stream_id=None, is_async=True):
168
+ request_ref = self.build_request_url(token)
169
+ return Stream(request_ref, stream_handler, self.build_headers, stream_id, is_async)
170
+
171
+ def check_token(self, database_url, path, token):
172
+ if token:
173
+ return '{0}{1}.json?auth={2}'.format(database_url, path, token)
174
+ else:
175
+ return '{0}{1}.json'.format(database_url, path)
176
+
177
+ def generate_key(self):
178
+ push_chars = '-0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz'
179
+ now = int(time.time() * 1000)
180
+ duplicate_time = now == self.last_push_time
181
+ self.last_push_time = now
182
+ time_stamp_chars = [0] * 8
183
+ for i in reversed(range(0, 8)):
184
+ time_stamp_chars[i] = push_chars[now % 64]
185
+ now = int(math.floor(now / 64))
186
+ new_id = "".join(time_stamp_chars)
187
+ if not duplicate_time:
188
+ self.last_rand_chars = [randrange(64) for _ in range(12)]
189
+ else:
190
+ for i in range(0, 11):
191
+ if self.last_rand_chars[i] == 63:
192
+ self.last_rand_chars[i] = 0
193
+ self.last_rand_chars[i] += 1
194
+ for i in range(0, 12):
195
+ new_id += push_chars[self.last_rand_chars[i]]
196
+ return new_id
197
+
198
+ def sort(self, origin, by_key, reverse=False):
199
+ # unpack pyre objects
200
+ pyres = origin.each()
201
+ new_list = []
202
+ for pyre in pyres:
203
+ new_list.append(pyre.item)
204
+ # sort
205
+ data = sorted(dict(new_list).items(), key=lambda item: item[1][by_key], reverse=reverse)
206
+ return PyreResponse(convert_to_pyre(data), origin.key())
207
+
208
+ def get_etag(self, token=None, json_kwargs=None):
209
+ json_kwargs = json_kwargs or {}
210
+ request_ref = self.build_request_url(token)
211
+ headers = self.build_headers(token)
212
+ # extra header to get ETag
213
+ headers['X-Firebase-ETag'] = 'true'
214
+ request_object = self.requests.get(request_ref, headers=headers)
215
+ raise_detailed_error(request_object)
216
+ return {
217
+ 'ETag': request_object.headers['ETag'],
218
+ 'value': request_object.json()
219
+ }
220
+
221
+ def conditional_set(self, data, etag, token=None, json_kwargs=None):
222
+ json_kwargs = json_kwargs or {}
223
+ request_ref = self.check_token(self.database_url, self.path, token)
224
+ self.path = ""
225
+ headers = self.build_headers(token)
226
+ headers['if-match'] = etag
227
+ request_object = self.requests.put(request_ref, headers=headers, data=json.dumps(data, **json_kwargs).encode("utf-8"))
228
+
229
+ # ETag didn't match, so we should return the correct one for the user to try again
230
+ if request_object.status_code == 412:
231
+ return {
232
+ 'ETag': request_object.headers['ETag'],
233
+ 'value': request_object.json()
234
+ }
235
+
236
+ raise_detailed_error(request_object)
237
+ return request_object.json()
238
+
239
+ def conditional_remove(self, etag, token=None):
240
+ request_ref = self.check_token(self.database_url, self.path, token)
241
+ self.path = ""
242
+ headers = self.build_headers(token)
243
+ headers['if-match'] = etag
244
+ request_object = self.requests.delete(request_ref, headers=headers)
245
+
246
+ # ETag didn't match, so we should return the correct one for the user to try again
247
+ if request_object.status_code == 412:
248
+ return {
249
+ 'ETag': request_object.headers['ETag'],
250
+ 'value': request_object.json()
251
+ }
252
+
253
+ raise_detailed_error(request_object)
254
+ return request_object.json()
@@ -0,0 +1,174 @@
1
+ from datetime import datetime
2
+ from pyrebase.utils import raise_detailed_error, replace_all
3
+
4
+ class Firestore:
5
+ """Firebase Firestore"""
6
+
7
+ def __init__(self, requests, project_id, firebase_path, database_name="(default)", auth_id=None):
8
+ self.project_id = project_id
9
+ self.firebase_path = firebase_path
10
+ self.database_name = database_name
11
+ self.base_path = f"firestore.googleapis.com/v1/projects/{
12
+ project_id}/databases/{database_name}/documents/{firebase_path}"
13
+ self.headers = {}
14
+ self.requests = requests
15
+ if auth_id:
16
+ self.headers["Authorization"] = f"Bearer {auth_id}"
17
+
18
+ def authorize(self, auth_id: str):
19
+ self.headers["Authorization"] = f"Bearer {auth_id}"
20
+
21
+ def create_document(self, document, data={}):
22
+ """
23
+ Alias for `update_document`.
24
+ Creates a new document in the Firestore database.
25
+
26
+ Args:
27
+ document (str): Document path relative to the base path passed on initialization
28
+ data (dict): Data to be stored in the document
29
+ """
30
+
31
+ return self.update_document(document, data)
32
+
33
+ def get_document(self, document: str):
34
+ """Fetches the document from firestore database
35
+
36
+ Args:
37
+ document (str): document path relative to the base path passed on initialization
38
+ """
39
+ request_url = f"{self.base_path}/{document}"
40
+ request_url = replace_all(request_url, '//', '/')
41
+ request_url = "https://" + request_url
42
+
43
+ response = self.requests.get(request_url, headers=self.headers)
44
+
45
+ if response.status_code == 200:
46
+ data = response.json().get('fields', {})
47
+ cleaned = self._doc_to_dict(data)
48
+ return cleaned
49
+ else:
50
+ raise_detailed_error(response)
51
+
52
+ def batch_get_documents(self, documents: list):
53
+ """Fetches multiple documents in a batch
54
+
55
+ Args:
56
+ documents (list): List of document paths relative to the base path passed on initialization
57
+ """
58
+ request_url = f"{self.base_path}:batchGet"
59
+ request_url = replace_all(request_url, '//', '/')
60
+ request_url = "https://" + request_url
61
+
62
+ response = self.requests.post(request_url, headers=self.headers, json={
63
+ "documents": [
64
+ f"projects/{self.project_id}/databases/{
65
+ self.database_name}/documents/{document.lstrip('/')}"
66
+ for document in documents
67
+ ]
68
+ })
69
+ if response.status_code == 200:
70
+ results = response.json()
71
+ return [self._doc_to_dict(result['found']['fields']) for result in results if 'found' in result]
72
+ else:
73
+ raise_detailed_error(response)
74
+
75
+ def run_query(self, collection: str, structured_query: dict):
76
+ """Runs a structured query against the collection
77
+
78
+ Args:
79
+ collection (str): Collection path relative to the base path passed on initialization
80
+ structured_query (dict): Firestore structured query object
81
+ """
82
+ request_url = f"{self.base_path}/{collection}:runQuery"
83
+ request_url = replace_all(request_url, '//', '/')
84
+ response = self.requests.post(
85
+ request_url, headers=self.headers, json=structured_query)
86
+ if response.status_code == 200:
87
+ results = response.json()
88
+ return [self._doc_to_dict(result['document']['fields']) for result in results if 'document' in result]
89
+ else:
90
+ raise_detailed_error(response)
91
+
92
+ def update_document(self, document, data):
93
+ existing_data = self.get_document(document)
94
+ data = {**existing_data, **data}
95
+ firestore_data = self._dict_to_doc(data)
96
+
97
+ firestore_data = {k: v for k, v in firestore_data.items() if v is not None}
98
+
99
+ request_url = f"{self.base_path}/{document}"
100
+ request_url = replace_all(request_url, '//', '/')
101
+ request_url = "https://" + request_url
102
+
103
+ response = self.requests.patch(
104
+ request_url, headers=self.headers, json={"fields": firestore_data})
105
+
106
+ if response.status_code != 200:
107
+ raise_detailed_error(response)
108
+
109
+ def delete_document(self, document):
110
+ """Deletes the document from the Firestore database
111
+
112
+ Args:
113
+ document (str): Document path relative to the base path passed on initialization
114
+ """
115
+ request_url = f"{self.base_path}/{document}"
116
+ request_url = replace_all(request_url, '//', '/')
117
+ request_url = "https://" + request_url
118
+
119
+ response = self.requests.delete(request_url, headers=self.headers)
120
+ if response.status_code != 200:
121
+ raise_detailed_error(response)
122
+
123
+ def list_documents(self, collection: str):
124
+ """Lists all documents in a collection
125
+
126
+ Args:
127
+ collection (str): Collection path relative to the base path passed on initialization
128
+ """
129
+ request_url = f"{self.base_path}/{collection}"
130
+ request_url = replace_all(request_url, '//', '/')
131
+ request_url = "https://" + request_url
132
+
133
+ response = self.requests.get(request_url, headers=self.headers)
134
+ if response.status_code == 200:
135
+ documents = response.json().get('documents', [])
136
+ return {doc['name']: self._doc_to_dict(doc['fields']) if doc.get('fields') else {} for doc in documents}
137
+ else:
138
+ raise_detailed_error(response)
139
+
140
+ def __process_value(self, dtype, value):
141
+ processed = None
142
+ match dtype:
143
+ case 'stringValue':
144
+ processed = str(value)
145
+ case 'integerValue':
146
+ processed = int(value)
147
+ case 'booleanValue':
148
+ processed = bool(value)
149
+ case 'mapValue':
150
+ processed = self._doc_to_dict(value['fields'])
151
+ case 'timestampValue':
152
+ processed = datetime.fromisoformat(value.replace("Z", "+00:00"))
153
+ case 'arrayValue':
154
+ processed = [self.__process_value(
155
+ d, v) for val in value["values"] for d, v in val.items()]
156
+
157
+ return processed
158
+
159
+ def _dict_to_doc(self, data: dict):
160
+ return {key: {"stringValue": value} if isinstance(value, str)
161
+ else {"integerValue": value} if isinstance(value, int)
162
+ else {"booleanValue": value} if isinstance(value, bool)
163
+ else {"timestampValue": value.isoformat()} if isinstance(value, datetime)
164
+ else {"mapValue": {"fields": self._dict_to_doc(value)}} if isinstance(value, dict)
165
+ else None
166
+ for key, value in data.items()}
167
+
168
+ def _doc_to_dict(self, data: dict) -> dict:
169
+ clean = {}
170
+ for key in data:
171
+ dtype = list(data[key].keys())[0]
172
+ clean[key] = self.__process_value(dtype, data[key][dtype])
173
+
174
+ return clean
@@ -0,0 +1,106 @@
1
+ from uuid import uuid4
2
+ from google.cloud import storage
3
+ from urllib.parse import quote
4
+ from pyrebase.utils import raise_detailed_error
5
+ import requests
6
+
7
+
8
+ class Storage:
9
+ """ Storage Service """
10
+ def __init__(self, credentials, storage_bucket, requests):
11
+ self.storage_bucket = "https://firebasestorage.googleapis.com/v0/b/" + storage_bucket
12
+ self.credentials = credentials
13
+ self.requests = requests
14
+ self.path = ""
15
+ if credentials:
16
+ client = storage.Client(credentials=credentials, project=storage_bucket)
17
+ self.bucket = client.get_bucket(storage_bucket)
18
+
19
+ def child(self, *args):
20
+ new_path = "/".join(args)
21
+ if self.path:
22
+ self.path += "/{}".format(new_path)
23
+ else:
24
+ if new_path.startswith("/"):
25
+ new_path = new_path[1:]
26
+ self.path = new_path
27
+ return self
28
+
29
+ def put(self, file, token=None, content_type=None):
30
+ # reset path
31
+ path = self.path
32
+ self.path = None
33
+ if isinstance(file, str):
34
+ file_object = open(file, 'rb')
35
+ else:
36
+ file_object = file
37
+ request_ref = self.storage_bucket + "/o?name={0}".format(path)
38
+ if token:
39
+ headers = {"Authorization": "Firebase " + token}
40
+ request_object = self.requests.post(request_ref, headers=headers, data=file_object)
41
+ raise_detailed_error(request_object)
42
+ return request_object.json()
43
+ elif self.credentials:
44
+ blob = self.bucket.blob(path)
45
+
46
+ # Add metadata to enable file previews in console
47
+ blob.metadata = {"firebaseStorageDownloadTokens": str(uuid4())}
48
+ if isinstance(file, str):
49
+ return blob.upload_from_filename(filename=file)
50
+ else:
51
+ # If the file is not a string we need to patch the blob after upload to set the content type
52
+ blob.upload_from_string(file)
53
+ if content_type:
54
+ blob.content_type = content_type
55
+ blob.patch()
56
+ else:
57
+ request_object = self.requests.post(request_ref, data=file_object)
58
+ raise_detailed_error(request_object)
59
+ return request_object.json()
60
+
61
+ def delete(self, name, token):
62
+ if self.credentials:
63
+ self.bucket.delete_blob(name)
64
+ else:
65
+ request_ref = self.storage_bucket + "/o/?name={0}".format(name)
66
+ if token:
67
+ headers = {"Authorization": "Firebase " + token}
68
+ request_object = self.requests.delete(request_ref, headers=headers)
69
+ else:
70
+ request_object = self.requests.delete(request_ref)
71
+ raise_detailed_error(request_object)
72
+
73
+ def download(self, path, filename, token=None):
74
+ # remove leading backlash
75
+ url = self.get_url(token)
76
+ if path.startswith('/'):
77
+ path = path.lstrip('/')
78
+ if self.credentials:
79
+ blob = self.bucket.get_blob(path)
80
+ if not blob is None:
81
+ blob.download_to_filename(filename)
82
+ elif token:
83
+ headers = {"Authorization": "Firebase " + token}
84
+ r = requests.get(url, stream=True, headers=headers)
85
+ if r.status_code == 200:
86
+ with open(filename, 'wb') as f:
87
+ for chunk in r:
88
+ f.write(chunk)
89
+ else:
90
+ r = requests.get(url, stream=True)
91
+ if r.status_code == 200:
92
+ with open(filename, 'wb') as f:
93
+ for chunk in r:
94
+ f.write(chunk)
95
+
96
+ def get_url(self, token):
97
+ path = self.path if self.path else ''
98
+ self.path = None
99
+ if path.startswith('/'):
100
+ path = path.lstrip('/')
101
+ if token:
102
+ return "{0}/o/{1}?alt=media&token={2}".format(self.storage_bucket, quote(path, safe=''), token)
103
+ return "{0}/o/{1}?alt=media".format(self.storage_bucket, quote(path, safe=''))
104
+
105
+ def list_files(self):
106
+ return self.bucket.list_blobs()
@@ -0,0 +1,5 @@
1
+ from .closable_sseclient import ClosableSSEClient
2
+ from .pyre import Pyre
3
+ from .pyre_response import PyreResponse
4
+ from .sseclient import SSEClient
5
+ from .stream import Stream
@@ -0,0 +1,20 @@
1
+ import socket
2
+ from .sseclient import SSEClient
3
+
4
+
5
+ class ClosableSSEClient(SSEClient):
6
+ def __init__(self, *args, **kwargs):
7
+ self.should_connect = True
8
+ super(ClosableSSEClient, self).__init__(*args, **kwargs)
9
+
10
+ def _connect(self):
11
+ if self.should_connect:
12
+ super(ClosableSSEClient, self)._connect()
13
+ else:
14
+ raise StopIteration()
15
+
16
+ def close(self):
17
+ self.should_connect = False
18
+ self.retry = 0
19
+ self.resp.raw._fp.fp.raw._sock.shutdown(socket.SHUT_RDWR)
20
+ self.resp.raw._fp.fp.raw._sock.close()
@@ -0,0 +1,9 @@
1
+ from requests import Session
2
+
3
+ class KeepAuthSession(Session):
4
+ """
5
+ A session that doesn't drop Authentication on redirects between domains.
6
+ """
7
+
8
+ def rebuild_auth(self, prepared_request, response):
9
+ pass
@@ -0,0 +1,9 @@
1
+ class Pyre:
2
+ def __init__(self, item):
3
+ self.item = item
4
+
5
+ def val(self):
6
+ return self.item[1]
7
+
8
+ def key(self):
9
+ return self.item[0]
@@ -0,0 +1,33 @@
1
+ from collections import OrderedDict
2
+
3
+ class PyreResponse:
4
+ def __init__(self, pyres, query_key):
5
+ self.pyres = pyres
6
+ self.query_key = query_key
7
+
8
+ def __getitem__(self,index):
9
+ return self.pyres[index]
10
+
11
+ def val(self):
12
+ if isinstance(self.pyres, list) and self.pyres:
13
+ # unpack pyres into OrderedDict
14
+ pyre_list = []
15
+ # if firebase response was a list
16
+ if isinstance(self.pyres[0].key(), int):
17
+ for pyre in self.pyres:
18
+ pyre_list.append(pyre.val())
19
+ return pyre_list
20
+ # if firebase response was a dict with keys
21
+ for pyre in self.pyres:
22
+ pyre_list.append((pyre.key(), pyre.val()))
23
+ return OrderedDict(pyre_list)
24
+ else:
25
+ # return primitive or simple query results
26
+ return self.pyres
27
+
28
+ def key(self):
29
+ return self.query_key
30
+
31
+ def each(self):
32
+ if isinstance(self.pyres, list):
33
+ return self.pyres
@@ -0,0 +1,167 @@
1
+ import re
2
+ import time
3
+ import warnings
4
+ import six
5
+
6
+ import requests
7
+
8
+
9
+ # Technically, we should support streams that mix line endings. This regex,
10
+ # however, assumes that a system will provide consistent line endings.
11
+ end_of_field = re.compile(r'\r\n\r\n|\r\r|\n\n')
12
+
13
+ class SSEClient(object):
14
+ def __init__(self, url, session, build_headers, last_id=None, retry=3000, **kwargs):
15
+ self.url = url
16
+ self.last_id = last_id
17
+ self.retry = retry
18
+ self.running = True
19
+ # Optional support for passing in a requests.Session()
20
+ self.session = session
21
+ # function for building auth header when token expires
22
+ self.build_headers = build_headers
23
+ self.start_time = None
24
+ # Any extra kwargs will be fed into the requests.get call later.
25
+ self.requests_kwargs = kwargs
26
+
27
+ # The SSE spec requires making requests with Cache-Control: nocache
28
+ if 'headers' not in self.requests_kwargs:
29
+ self.requests_kwargs['headers'] = {}
30
+ self.requests_kwargs['headers']['Cache-Control'] = 'no-cache'
31
+
32
+ # The 'Accept' header is not required, but explicit > implicit
33
+ self.requests_kwargs['headers']['Accept'] = 'text/event-stream'
34
+
35
+ # Keep data here as it streams in
36
+ self.buf = u''
37
+
38
+ self._connect()
39
+
40
+ def _connect(self):
41
+ if self.last_id:
42
+ self.requests_kwargs['headers']['Last-Event-ID'] = self.last_id
43
+ headers = self.build_headers()
44
+ self.requests_kwargs['headers'].update(headers)
45
+ # Use session if set. Otherwise fall back to requests module.
46
+ self.requester = self.session or requests
47
+ self.resp = self.requester.get(self.url, stream=True, **self.requests_kwargs)
48
+
49
+ self.resp_iterator = self.resp.iter_content(decode_unicode=True)
50
+
51
+ # TODO: Ensure we're handling redirects. Might also stick the 'origin'
52
+ # attribute on Events like the Javascript spec requires.
53
+ self.resp.raise_for_status()
54
+
55
+ def _event_complete(self):
56
+ return re.search(end_of_field, self.buf) is not None
57
+
58
+ def __iter__(self):
59
+ return self
60
+
61
+ def __next__(self):
62
+ while not self._event_complete():
63
+ try:
64
+ nextchar = next(self.resp_iterator)
65
+ self.buf += nextchar
66
+ except (StopIteration, requests.RequestException):
67
+ time.sleep(self.retry / 1000.0)
68
+ self._connect()
69
+
70
+ # The SSE spec only supports resuming from a whole message, so
71
+ # if we have half a message we should throw it out.
72
+ head, sep, tail = self.buf.rpartition('\n')
73
+ self.buf = head + sep
74
+ continue
75
+
76
+ split = re.split(end_of_field, self.buf)
77
+ head = split[0]
78
+ tail = "".join(split[1:])
79
+
80
+ self.buf = tail
81
+ msg = Event.parse(head)
82
+
83
+ if msg.data == "credential is no longer valid":
84
+ self._connect()
85
+ return None
86
+
87
+ if msg.data == 'null':
88
+ return None
89
+
90
+ # If the server requests a specific retry delay, we need to honor it.
91
+ if msg.retry:
92
+ self.retry = msg.retry
93
+
94
+ # last_id should only be set if included in the message. It's not
95
+ # forgotten if a message omits it.
96
+ if msg.id:
97
+ self.last_id = msg.id
98
+
99
+ return msg
100
+
101
+ if six.PY2:
102
+ next = __next__
103
+
104
+
105
+ class Event(object):
106
+
107
+ sse_line_pattern = re.compile('(?P<name>[^:]*):?( ?(?P<value>.*))?')
108
+
109
+ def __init__(self, data='', event='message', id=None, retry=None):
110
+ self.data = data
111
+ self.event = event
112
+ self.id = id
113
+ self.retry = retry
114
+
115
+ def dump(self):
116
+ lines = []
117
+ if self.id:
118
+ lines.append('id: %s' % self.id)
119
+
120
+ # Only include an event line if it's not the default already.
121
+ if self.event != 'message':
122
+ lines.append('event: %s' % self.event)
123
+
124
+ if self.retry:
125
+ lines.append('retry: %s' % self.retry)
126
+
127
+ lines.extend('data: %s' % d for d in self.data.split('\n'))
128
+ return '\n'.join(lines) + '\n\n'
129
+
130
+ @classmethod
131
+ def parse(cls, raw):
132
+ """
133
+ Given a possibly-multiline string representing an SSE message, parse it
134
+ and return a Event object.
135
+ """
136
+ msg = cls()
137
+ for line in raw.split('\n'):
138
+ m = cls.sse_line_pattern.match(line)
139
+ if m is None:
140
+ # Malformed line. Discard but warn.
141
+ warnings.warn('Invalid SSE line: "%s"' % line, SyntaxWarning)
142
+ continue
143
+
144
+ name = m.groupdict()['name']
145
+ value = m.groupdict()['value']
146
+ if name == '':
147
+ # line began with a ":", so is a comment. Ignore
148
+ continue
149
+
150
+ if name == 'data':
151
+ # If we already have some data, then join to it with a newline.
152
+ # Else this is it.
153
+ if msg.data:
154
+ msg.data = '%s\n%s' % (msg.data, value)
155
+ else:
156
+ msg.data = value
157
+ elif name == 'event':
158
+ msg.event = value
159
+ elif name == 'id':
160
+ msg.id = value
161
+ elif name == 'retry':
162
+ msg.retry = int(value)
163
+
164
+ return msg
165
+
166
+ def __str__(self):
167
+ return self.data
@@ -0,0 +1,50 @@
1
+ import json
2
+ import threading
3
+ import time
4
+ from .closable_sseclient import ClosableSSEClient
5
+ from .keep_auth_session import KeepAuthSession
6
+
7
+
8
+ class Stream:
9
+ def __init__(self, url, stream_handler, build_headers, stream_id, is_async):
10
+ self.build_headers = build_headers
11
+ self.url = url
12
+ self.stream_handler = stream_handler
13
+ self.stream_id = stream_id
14
+ self.sse = None
15
+ self.thread = None
16
+
17
+ if is_async:
18
+ self.start()
19
+ else:
20
+ self.start_stream()
21
+
22
+ def make_session(self):
23
+ """
24
+ Return a custom session object to be passed to the ClosableSSEClient.
25
+ """
26
+ session = KeepAuthSession()
27
+ return session
28
+
29
+ def start(self):
30
+ self.thread = threading.Thread(target=self.start_stream)
31
+ self.thread.start()
32
+ return self
33
+
34
+ def start_stream(self):
35
+ self.sse = ClosableSSEClient(self.url, session=self.make_session(), build_headers=self.build_headers)
36
+ for msg in self.sse:
37
+ if msg:
38
+ msg_data = json.loads(msg.data)
39
+ msg_data["event"] = msg.event
40
+ if self.stream_id:
41
+ msg_data["stream_id"] = self.stream_id
42
+ self.stream_handler(msg_data)
43
+
44
+ def close(self):
45
+ while not self.sse and not hasattr(self.sse, 'resp'):
46
+ time.sleep(0.001)
47
+ self.sse.running = False
48
+ self.sse.close()
49
+ self.thread.join()
50
+ return self
@@ -0,0 +1,32 @@
1
+ from pyrebase.types import Pyre
2
+ from requests import HTTPError
3
+
4
+
5
+ def raise_detailed_error(request_object):
6
+ try:
7
+ request_object.raise_for_status()
8
+ except HTTPError as e:
9
+ # raise detailed error message
10
+ # TODO: Check if we get a { "error" : "Permission denied." } and handle automatically
11
+ raise HTTPError(e, request_object.text)
12
+
13
+
14
+ def convert_to_pyre(items):
15
+ pyre_list = []
16
+ for item in items:
17
+ pyre_list.append(Pyre(item))
18
+ return pyre_list
19
+
20
+
21
+ def convert_list_to_pyre(items):
22
+ pyre_list = []
23
+ for item in items:
24
+ pyre_list.append(Pyre([items.index(item), item]))
25
+ return pyre_list
26
+
27
+
28
+ def replace_all(og: str, a: str, b: str):
29
+ while a in og:
30
+ og = og.replace(a, b)
31
+
32
+ return og
@@ -0,0 +1,19 @@
1
+ Metadata-Version: 2.1
2
+ Name: empyrebase
3
+ Version: 1.0.0
4
+ Summary: A simple python wrapper for the Firebase API with current deps
5
+ Home-page: https://github.com/emrothenberg/empyrebase
6
+ Author: emrothenberg
7
+ License: MIT
8
+ Keywords: Firebase,Pyrebase,Google,Empyrebase
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Programming Language :: Python :: 3.4
13
+ Requires-Dist: requests-toolbelt>=1.0.0
14
+ Requires-Dist: requests>=2.31
15
+ Requires-Dist: urllib3<2,>=1.21.1
16
+ Requires-Dist: google-cloud-storage>=2.18.2
17
+ Requires-Dist: oauth2client>=4.1.2
18
+ Requires-Dist: pyjwt>=2.8.0
19
+ Requires-Dist: pycryptodome>=3.6.4
@@ -0,0 +1,19 @@
1
+ empyrebase/__init__.py,sha256=iYN3xUwMAE9MTxM8SvL3jNZIwBgrsMf9YhoCiMZZoZM,36
2
+ empyrebase/empyrebase.py,sha256=-vZf6ykEI7MRxNOiuzKipTa_rHIUbmiPRSDVC6ZIVOA,2010
3
+ empyrebase/services/__init__.py,sha256=_i0ot8btz9qZ5S6BH4FlcdVzodwyo0AYmikqTWvIpY0,115
4
+ empyrebase/services/auth.py,sha256=6RwyZcl9HVxuvwbuSJ6wE9bMomP-mJFxsaXRfMU2y4M,7154
5
+ empyrebase/services/database.py,sha256=-rvYz6U6-zJpiN-_iFCeNS5EJ7uHZurONtEfvcyeaZU,9951
6
+ empyrebase/services/firestore.py,sha256=GKJeKrx9ZcNqyh8h_UtZ2GCUcpDPnZ1tgAq-XICB2Iw,7011
7
+ empyrebase/services/storage.py,sha256=bsotFMkFMrA7oBkxRy3SYzfm5TSLypOcsOki9XpQ6Yg,4103
8
+ empyrebase/types/__init__.py,sha256=_CLe58gbs2RlNi6oHKBBgN67p4Rjrs0qMB8UcZLm7PE,172
9
+ empyrebase/types/closable_sseclient.py,sha256=8V-Dse_b460KRBLN7c1EUF2Uv5uefKA9DS198gVgG9s,578
10
+ empyrebase/types/keep_auth_session.py,sha256=Z6AaRHiOoAJb_cEae0uu3KljDaW7zJWp207bgjee4Kw,225
11
+ empyrebase/types/pyre.py,sha256=XwTRUJjL8NdYNa-3Lb9JmJnk7Wn5tyHUxyUOic63z74,163
12
+ empyrebase/types/pyre_response.py,sha256=Ej8TLteGNRYs6yFmMNdrdWsrp_bdKbiJWYrNm8JdQ_w,1028
13
+ empyrebase/types/sseclient.py,sha256=pVb6-F5gAvZH5Z8KAbaVtSfFukl4MTuflTeya1MW8O4,5378
14
+ empyrebase/types/stream.py,sha256=BRvX63XgI8pYOYpxg4dlqPcy89KsTeJnSw6bF1aQEc8,1491
15
+ empyrebase/utils/__init__.py,sha256=ppPQXci83abz17gSf6ZG5H6HdonAPgV9lWqHjn9uYiA,760
16
+ empyrebase-1.0.0.dist-info/METADATA,sha256=Exa1sIZgCfhCf5IzxgFkecDgqtFlAL4L_JnVCfbsqn4,695
17
+ empyrebase-1.0.0.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
18
+ empyrebase-1.0.0.dist-info/top_level.txt,sha256=sT4vtemalBChgezzmOwXa_Mf6cTm--SmuJ8XucPMi8U,11
19
+ empyrebase-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (75.6.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ empyrebase