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 +1 -0
- empyrebase/empyrebase.py +51 -0
- empyrebase/services/__init__.py +4 -0
- empyrebase/services/auth.py +131 -0
- empyrebase/services/database.py +254 -0
- empyrebase/services/firestore.py +174 -0
- empyrebase/services/storage.py +106 -0
- empyrebase/types/__init__.py +5 -0
- empyrebase/types/closable_sseclient.py +20 -0
- empyrebase/types/keep_auth_session.py +9 -0
- empyrebase/types/pyre.py +9 -0
- empyrebase/types/pyre_response.py +33 -0
- empyrebase/types/sseclient.py +167 -0
- empyrebase/types/stream.py +50 -0
- empyrebase/utils/__init__.py +32 -0
- empyrebase-1.0.0.dist-info/METADATA +19 -0
- empyrebase-1.0.0.dist-info/RECORD +19 -0
- empyrebase-1.0.0.dist-info/WHEEL +5 -0
- empyrebase-1.0.0.dist-info/top_level.txt +1 -0
empyrebase/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .pyrebase import initialize_app
|
empyrebase/empyrebase.py
ADDED
|
@@ -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,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,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()
|
empyrebase/types/pyre.py
ADDED
|
@@ -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 @@
|
|
|
1
|
+
empyrebase
|