1
- # Copyright 2022 Google LLC
1
+ # Copyright 2024 Google LLC
2
2
#
3
3
# Licensed under the Apache License, Version 2.0 (the "License");
4
4
# you may not use this file except in compliance with the License.
17
17
from datetime import datetime
18
18
import json
19
19
from typing import Any , Dict , Mapping , Type , TypeVar , Union
20
+ from io import BytesIO
20
21
21
22
import pytz
22
23
from dateutil .relativedelta import relativedelta
25
26
26
27
from auth import decorators
27
28
28
- from .abstract_datastore import AbstractDatastore
29
- from .credentials_helpers import encode_key
30
- from .exceptions import CredentialsError
29
+ from auth .abstract_datastore import AbstractDatastore
30
+ from auth .credentials_helpers import encode_key
31
+ from auth .exceptions import CredentialsError
31
32
32
33
33
34
@dataclass
34
35
class ProjectCredentials (object ):
36
+ """ProjectCredentials
37
+
38
+ A dataclass to hold the client is and secret to be passed around within the
39
+ functions.
40
+ """
35
41
client_id : str
36
42
client_secret : str
37
43
@@ -69,11 +75,33 @@ def datastore(self) -> AbstractDatastore:
69
75
70
76
@datastore .setter
71
77
def datastore (self , f : AbstractDatastore ) -> None :
78
+ """datastore setter
79
+
80
+ This will always raise an exception. Once the `Credentials` object has been
81
+ created, the datastore cannot be changed. If you need to move the object
82
+ from one datastore to another, then create a second `Credentials` object
83
+ pointing to the new datastore and pass in the OAuth Credentials, either
84
+ as the OAuth credentials object or as a `Mapping[str, Any]`.
85
+
86
+ Args:
87
+ f (AbstractDatastore): the datastore
88
+
89
+ Raises:
90
+ KeyError: the datastore should be immutable
91
+ """
72
92
raise KeyError ('Datastore can only be set on instantiation.' )
73
93
74
94
@decorators .lazy_property
75
95
def project_credentials (self ) -> ProjectCredentials :
76
- """The project credentials."""
96
+ """The project credentials.
97
+
98
+ This is lazy-evaluated using the decorator, so the project credentials
99
+ are only fetched once from the datastore for the life of the object,
100
+ reducing API calls and thus improving speed and reducing cost.
101
+
102
+ Returns:
103
+ ProjectCredentials: the project client id and secret as a dataclass
104
+ """
77
105
secrets = None
78
106
if secrets := self .datastore .get_document (id = 'client_id' ):
79
107
secrets |= self .datastore .get_document (id = 'client_secret' )
@@ -92,7 +120,7 @@ def project_credentials(self) -> ProjectCredentials:
92
120
93
121
@decorators .lazy_property
94
122
def token_details (self ) -> Dict [str , Any ]:
95
- """The users's refresh and access token."""
123
+ """The users's OAuth token."""
96
124
return self .datastore .get_document (id = encode_key (self ._email ))
97
125
98
126
def store_credentials (self ,
@@ -111,7 +139,8 @@ def store_credentials(self,
111
139
key = encode_key (self ._email )
112
140
113
141
if isinstance (creds , oauth .Credentials ):
114
- self .datastore .update_document (id = key , new_data = creds .to_json ())
142
+ self .datastore .update_document (id = key ,
143
+ new_data = self ._to_dict (creds ))
115
144
else :
116
145
self .datastore .update_document (id = key , new_data = creds )
117
146
@@ -125,23 +154,60 @@ def _refresh_credentials(self, creds: oauth.Credentials) -> None:
125
154
self .store_credentials (creds )
126
155
127
156
def _to_utc (self , last_date : datetime ) -> datetime :
157
+ """Convert a datetime to UTC
158
+
159
+ Args:
160
+ last_date (datetime): the date to convert
161
+
162
+ Returns:
163
+ datetime: the date in UTC
164
+ """
128
165
if (last_date .tzinfo is None or
129
166
last_date .tzinfo .utcoffset (last_date ) is None ):
130
167
last_date = pytz .UTC .localize (last_date )
131
168
132
169
return last_date
133
170
134
- @property
171
+ def _to_dict (self , credentials : oauth .Credentials ) -> Mapping [str , Any ]:
172
+ """Convert an OAuth token to a dict
173
+
174
+ Note the conversion of the expiry date (A `datetime` object) to the Zulu
175
+ format date string. This is because the primary function of the dict is to
176
+ be serialized to the `Datastore` and we have to ensure that all fields can
177
+ be turned to `json` for this purpose. When reinstantiated, the OAuth
178
+ Credentials object takes care of converting the expiry date back to a
179
+ `datetime` for us.
180
+
181
+ Args:
182
+ credentials (oauth.Credentials): the OAuth credentials
183
+
184
+ Returns:
185
+ Mapping[str, Any]: the credentials as a `dict[str, Any]`
186
+ """
187
+ return {'token' : credentials .token ,
188
+ 'refresh_token' : credentials .refresh_token ,
189
+ 'token_uri' : credentials .token_uri ,
190
+ 'client_id' : credentials .client_id ,
191
+ 'client_secret' : credentials .client_secret ,
192
+ 'scopes' : credentials .scopes ,
193
+ 'default_scopes' : credentials .default_scopes ,
194
+ 'expiry' : credentials .expiry .strftime ('%Y-%m-%dT%H:%M:%SZ' )}
195
+
196
+ @ property
135
197
def credentials (self ) -> oauth .Credentials :
136
198
"""Fetches the credentials.
137
199
138
200
Returns:
139
201
(google.oauth2.credentials.Credentials): the credentials
140
202
"""
141
203
expiry = self ._to_utc (
142
- datetime .now ().astimezone (pytz .utc ) + relativedelta (minutes = 30 ))
204
+ datetime .now ().astimezone (pytz .utc ) + relativedelta (minutes = 60 ))
205
+
143
206
if token := self .token_details :
144
- creds = oauth .Credentials .from_authorized_user_info (json .loads (token ))
207
+ if isinstance (token , str ):
208
+ token = json .loads (token )
209
+
210
+ creds = oauth .Credentials .from_authorized_user_info (token )
145
211
146
212
if creds .expired :
147
213
creds .expiry = expiry
@@ -153,7 +219,7 @@ def credentials(self) -> oauth.Credentials:
153
219
154
220
return creds
155
221
156
- @property
222
+ @ property
157
223
def auth_headers (self ) -> Dict [str , Any ]:
158
224
"""Returns authorized http headers.
159
225
0 commit comments