airbyte-cdk 6.34.1.dev0__py3-none-any.whl → 6.35.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.
- airbyte_cdk/connector_builder/connector_builder_handler.py +16 -12
- airbyte_cdk/connector_builder/test_reader/__init__.py +7 -0
- airbyte_cdk/connector_builder/test_reader/helpers.py +591 -0
- airbyte_cdk/connector_builder/test_reader/message_grouper.py +160 -0
- airbyte_cdk/connector_builder/test_reader/reader.py +441 -0
- airbyte_cdk/connector_builder/test_reader/types.py +75 -0
- airbyte_cdk/sources/declarative/async_job/job_orchestrator.py +7 -7
- airbyte_cdk/sources/declarative/auth/jwt.py +17 -11
- airbyte_cdk/sources/declarative/auth/oauth.py +6 -1
- airbyte_cdk/sources/declarative/auth/token.py +3 -8
- airbyte_cdk/sources/declarative/concurrent_declarative_source.py +30 -79
- airbyte_cdk/sources/declarative/declarative_component_schema.yaml +213 -100
- airbyte_cdk/sources/declarative/declarative_stream.py +3 -1
- airbyte_cdk/sources/declarative/decoders/__init__.py +0 -4
- airbyte_cdk/sources/declarative/decoders/composite_raw_decoder.py +18 -3
- airbyte_cdk/sources/declarative/decoders/json_decoder.py +12 -58
- airbyte_cdk/sources/declarative/extractors/record_selector.py +12 -3
- airbyte_cdk/sources/declarative/incremental/concurrent_partition_cursor.py +56 -25
- airbyte_cdk/sources/declarative/incremental/datetime_based_cursor.py +12 -6
- airbyte_cdk/sources/declarative/incremental/global_substream_cursor.py +6 -2
- airbyte_cdk/sources/declarative/interpolation/__init__.py +1 -1
- airbyte_cdk/sources/declarative/interpolation/filters.py +2 -1
- airbyte_cdk/sources/declarative/interpolation/interpolated_boolean.py +1 -1
- airbyte_cdk/sources/declarative/interpolation/interpolated_mapping.py +1 -1
- airbyte_cdk/sources/declarative/interpolation/interpolated_nested_mapping.py +1 -1
- airbyte_cdk/sources/declarative/interpolation/interpolated_string.py +1 -1
- airbyte_cdk/sources/declarative/interpolation/interpolation.py +2 -1
- airbyte_cdk/sources/declarative/interpolation/jinja.py +14 -1
- airbyte_cdk/sources/declarative/interpolation/macros.py +19 -4
- airbyte_cdk/sources/declarative/manifest_declarative_source.py +9 -0
- airbyte_cdk/sources/declarative/models/declarative_component_schema.py +150 -41
- airbyte_cdk/sources/declarative/parsers/model_to_component_factory.py +234 -84
- airbyte_cdk/sources/declarative/partition_routers/async_job_partition_router.py +5 -5
- airbyte_cdk/sources/declarative/partition_routers/list_partition_router.py +4 -2
- airbyte_cdk/sources/declarative/partition_routers/substream_partition_router.py +26 -18
- airbyte_cdk/sources/declarative/requesters/http_requester.py +8 -2
- airbyte_cdk/sources/declarative/requesters/paginators/default_paginator.py +16 -5
- airbyte_cdk/sources/declarative/requesters/request_option.py +83 -4
- airbyte_cdk/sources/declarative/requesters/request_options/datetime_based_request_options_provider.py +7 -6
- airbyte_cdk/sources/declarative/requesters/request_options/interpolated_nested_request_input_provider.py +1 -4
- airbyte_cdk/sources/declarative/requesters/request_options/interpolated_request_input_provider.py +0 -3
- airbyte_cdk/sources/declarative/requesters/request_options/interpolated_request_options_provider.py +2 -47
- airbyte_cdk/sources/declarative/retrievers/async_retriever.py +6 -12
- airbyte_cdk/sources/declarative/retrievers/simple_retriever.py +4 -3
- airbyte_cdk/sources/declarative/transformations/add_fields.py +4 -4
- airbyte_cdk/sources/file_based/config/abstract_file_based_spec.py +2 -1
- airbyte_cdk/sources/file_based/config/validate_config_transfer_modes.py +81 -0
- airbyte_cdk/sources/file_based/file_based_source.py +70 -37
- airbyte_cdk/sources/file_based/file_based_stream_reader.py +107 -12
- airbyte_cdk/sources/file_based/stream/__init__.py +10 -1
- airbyte_cdk/sources/file_based/stream/identities_stream.py +47 -0
- airbyte_cdk/sources/file_based/stream/permissions_file_based_stream.py +85 -0
- airbyte_cdk/sources/specs/transfer_modes.py +26 -0
- airbyte_cdk/sources/streams/call_rate.py +185 -47
- airbyte_cdk/sources/streams/http/http.py +1 -2
- airbyte_cdk/sources/streams/http/requests_native_auth/abstract_oauth.py +217 -56
- airbyte_cdk/sources/streams/http/requests_native_auth/oauth.py +144 -73
- airbyte_cdk/sources/streams/permissions/identities_stream.py +75 -0
- airbyte_cdk/test/mock_http/mocker.py +9 -1
- airbyte_cdk/test/mock_http/response.py +6 -3
- airbyte_cdk/utils/datetime_helpers.py +48 -66
- airbyte_cdk/utils/mapping_helpers.py +126 -26
- {airbyte_cdk-6.34.1.dev0.dist-info → airbyte_cdk-6.35.0.dist-info}/METADATA +1 -1
- {airbyte_cdk-6.34.1.dev0.dist-info → airbyte_cdk-6.35.0.dist-info}/RECORD +68 -59
- airbyte_cdk/connector_builder/message_grouper.py +0 -448
- {airbyte_cdk-6.34.1.dev0.dist-info → airbyte_cdk-6.35.0.dist-info}/LICENSE.txt +0 -0
- {airbyte_cdk-6.34.1.dev0.dist-info → airbyte_cdk-6.35.0.dist-info}/LICENSE_SHORT +0 -0
- {airbyte_cdk-6.34.1.dev0.dist-info → airbyte_cdk-6.35.0.dist-info}/WHEEL +0 -0
- {airbyte_cdk-6.34.1.dev0.dist-info → airbyte_cdk-6.35.0.dist-info}/entry_points.txt +0 -0
@@ -25,6 +25,13 @@ logger = logging.getLogger("airbyte")
|
|
25
25
|
_NOOP_MESSAGE_REPOSITORY = NoopMessageRepository()
|
26
26
|
|
27
27
|
|
28
|
+
class ResponseKeysMaxRecurtionReached(AirbyteTracedException):
|
29
|
+
"""
|
30
|
+
Raised when the max level of recursion is reached, when trying to
|
31
|
+
find-and-get the target key, during the `_make_handled_request`
|
32
|
+
"""
|
33
|
+
|
34
|
+
|
28
35
|
class AbstractOauth2Authenticator(AuthBase):
|
29
36
|
"""
|
30
37
|
Abstract class for an OAuth authenticators that implements the OAuth token refresh flow. The authenticator
|
@@ -53,15 +60,31 @@ class AbstractOauth2Authenticator(AuthBase):
|
|
53
60
|
request.headers.update(self.get_auth_header())
|
54
61
|
return request
|
55
62
|
|
63
|
+
@property
|
64
|
+
def _is_access_token_flow(self) -> bool:
|
65
|
+
return self.get_token_refresh_endpoint() is None and self.access_token is not None
|
66
|
+
|
67
|
+
@property
|
68
|
+
def token_expiry_is_time_of_expiration(self) -> bool:
|
69
|
+
"""
|
70
|
+
Indicates that the Token Expiry returns the date until which the token will be valid, not the amount of time it will be valid.
|
71
|
+
"""
|
72
|
+
|
73
|
+
return False
|
74
|
+
|
75
|
+
@property
|
76
|
+
def token_expiry_date_format(self) -> Optional[str]:
|
77
|
+
"""
|
78
|
+
Format of the datetime; exists it if expires_in is returned as the expiration datetime instead of seconds until it expires
|
79
|
+
"""
|
80
|
+
|
81
|
+
return None
|
82
|
+
|
56
83
|
def get_auth_header(self) -> Mapping[str, Any]:
|
57
84
|
"""HTTP header to set on the requests"""
|
58
85
|
token = self.access_token if self._is_access_token_flow else self.get_access_token()
|
59
86
|
return {"Authorization": f"Bearer {token}"}
|
60
87
|
|
61
|
-
@property
|
62
|
-
def _is_access_token_flow(self) -> bool:
|
63
|
-
return self.get_token_refresh_endpoint() is None and self.access_token is not None
|
64
|
-
|
65
88
|
def get_access_token(self) -> str:
|
66
89
|
"""Returns the access token"""
|
67
90
|
if self.token_has_expired():
|
@@ -107,9 +130,39 @@ class AbstractOauth2Authenticator(AuthBase):
|
|
107
130
|
headers = self.get_refresh_request_headers()
|
108
131
|
return headers if headers else None
|
109
132
|
|
133
|
+
def refresh_access_token(self) -> Tuple[str, Union[str, int]]:
|
134
|
+
"""
|
135
|
+
Returns the refresh token and its expiration datetime
|
136
|
+
|
137
|
+
:return: a tuple of (access_token, token_lifespan)
|
138
|
+
"""
|
139
|
+
response_json = self._make_handled_request()
|
140
|
+
self._ensure_access_token_in_response(response_json)
|
141
|
+
|
142
|
+
return (
|
143
|
+
self._extract_access_token(response_json),
|
144
|
+
self._extract_token_expiry_date(response_json),
|
145
|
+
)
|
146
|
+
|
147
|
+
# ----------------
|
148
|
+
# PRIVATE METHODS
|
149
|
+
# ----------------
|
150
|
+
|
110
151
|
def _wrap_refresh_token_exception(
|
111
152
|
self, exception: requests.exceptions.RequestException
|
112
153
|
) -> bool:
|
154
|
+
"""
|
155
|
+
Wraps and handles exceptions that occur during the refresh token process.
|
156
|
+
|
157
|
+
This method checks if the provided exception is related to a refresh token error
|
158
|
+
by examining the response status code and specific error content.
|
159
|
+
|
160
|
+
Args:
|
161
|
+
exception (requests.exceptions.RequestException): The exception raised during the request.
|
162
|
+
|
163
|
+
Returns:
|
164
|
+
bool: True if the exception is related to a refresh token error, False otherwise.
|
165
|
+
"""
|
113
166
|
try:
|
114
167
|
if exception.response is not None:
|
115
168
|
exception_content = exception.response.json()
|
@@ -131,7 +184,24 @@ class AbstractOauth2Authenticator(AuthBase):
|
|
131
184
|
),
|
132
185
|
max_time=300,
|
133
186
|
)
|
134
|
-
def
|
187
|
+
def _make_handled_request(self) -> Any:
|
188
|
+
"""
|
189
|
+
Makes a handled HTTP request to refresh an OAuth token.
|
190
|
+
|
191
|
+
This method sends a POST request to the token refresh endpoint with the necessary
|
192
|
+
headers and body to obtain a new access token. It handles various exceptions that
|
193
|
+
may occur during the request and logs the response for troubleshooting purposes.
|
194
|
+
|
195
|
+
Returns:
|
196
|
+
Mapping[str, Any]: The JSON response from the token refresh endpoint.
|
197
|
+
|
198
|
+
Raises:
|
199
|
+
DefaultBackoffException: If the response status code is 429 (Too Many Requests)
|
200
|
+
or any 5xx server error.
|
201
|
+
AirbyteTracedException: If the refresh token is invalid or expired, prompting
|
202
|
+
re-authentication.
|
203
|
+
Exception: For any other exceptions that occur during the request.
|
204
|
+
"""
|
135
205
|
try:
|
136
206
|
response = requests.request(
|
137
207
|
method="POST",
|
@@ -139,22 +209,10 @@ class AbstractOauth2Authenticator(AuthBase):
|
|
139
209
|
data=self.build_refresh_request_body(),
|
140
210
|
headers=self.build_refresh_request_headers(),
|
141
211
|
)
|
142
|
-
if
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
access_key = response_json.get(self.get_access_token_name())
|
147
|
-
if not access_key:
|
148
|
-
raise Exception(
|
149
|
-
"Token refresh API response was missing access token {self.get_access_token_name()}"
|
150
|
-
)
|
151
|
-
add_to_secrets(access_key)
|
152
|
-
self._log_response(response)
|
153
|
-
return response_json
|
154
|
-
else:
|
155
|
-
# log the response even if the request failed for troubleshooting purposes
|
156
|
-
self._log_response(response)
|
157
|
-
response.raise_for_status()
|
212
|
+
# log the response even if the request failed for troubleshooting purposes
|
213
|
+
self._log_response(response)
|
214
|
+
response.raise_for_status()
|
215
|
+
return response.json()
|
158
216
|
except requests.exceptions.RequestException as e:
|
159
217
|
if e.response is not None:
|
160
218
|
if e.response.status_code == 429 or e.response.status_code >= 500:
|
@@ -168,17 +226,34 @@ class AbstractOauth2Authenticator(AuthBase):
|
|
168
226
|
except Exception as e:
|
169
227
|
raise Exception(f"Error while refreshing access token: {e}") from e
|
170
228
|
|
171
|
-
def
|
229
|
+
def _ensure_access_token_in_response(self, response_data: Mapping[str, Any]) -> None:
|
172
230
|
"""
|
173
|
-
|
231
|
+
Ensures that the access token is present in the response data.
|
174
232
|
|
175
|
-
|
176
|
-
|
177
|
-
|
233
|
+
This method attempts to extract the access token from the provided response data.
|
234
|
+
If the access token is not found, it raises an exception indicating that the token
|
235
|
+
refresh API response was missing the access token. If the access token is found,
|
236
|
+
it adds the token to the list of secrets to ensure it is replaced before logging
|
237
|
+
the response.
|
238
|
+
|
239
|
+
Args:
|
240
|
+
response_data (Mapping[str, Any]): The response data from which to extract the access token.
|
178
241
|
|
179
|
-
|
180
|
-
|
181
|
-
|
242
|
+
Raises:
|
243
|
+
Exception: If the access token is not found in the response data.
|
244
|
+
ResponseKeysMaxRecurtionReached: If the maximum recursion depth is reached while extracting the access token.
|
245
|
+
"""
|
246
|
+
try:
|
247
|
+
access_key = self._extract_access_token(response_data)
|
248
|
+
if not access_key:
|
249
|
+
raise Exception(
|
250
|
+
"Token refresh API response was missing access token {self.get_access_token_name()}"
|
251
|
+
)
|
252
|
+
# Add the access token to the list of secrets so it is replaced before logging the response
|
253
|
+
# An argument could be made to remove the prevous access key from the list of secrets, but unmasking values seems like a security incident waiting to happen...
|
254
|
+
add_to_secrets(access_key)
|
255
|
+
except ResponseKeysMaxRecurtionReached as e:
|
256
|
+
raise e
|
182
257
|
|
183
258
|
def _parse_token_expiration_date(self, value: Union[str, int]) -> AirbyteDateTime:
|
184
259
|
"""
|
@@ -186,6 +261,9 @@ class AbstractOauth2Authenticator(AuthBase):
|
|
186
261
|
|
187
262
|
:return: expiration datetime
|
188
263
|
"""
|
264
|
+
if not value and not self.token_has_expired():
|
265
|
+
# No expiry token was provided but the previous one is not expired so it's fine
|
266
|
+
return self.get_token_expiry_date()
|
189
267
|
|
190
268
|
if self.token_expiry_is_time_of_expiration:
|
191
269
|
if not self.token_expiry_date_format:
|
@@ -206,22 +284,125 @@ class AbstractOauth2Authenticator(AuthBase):
|
|
206
284
|
f"Invalid expires_in value: {value}. Expected number of seconds when no format specified."
|
207
285
|
)
|
208
286
|
|
209
|
-
|
210
|
-
def token_expiry_is_time_of_expiration(self) -> bool:
|
287
|
+
def _extract_access_token(self, response_data: Mapping[str, Any]) -> Any:
|
211
288
|
"""
|
212
|
-
|
289
|
+
Extracts the access token from the given response data.
|
290
|
+
|
291
|
+
Args:
|
292
|
+
response_data (Mapping[str, Any]): The response data from which to extract the access token.
|
293
|
+
|
294
|
+
Returns:
|
295
|
+
str: The extracted access token.
|
213
296
|
"""
|
297
|
+
return self._find_and_get_value_from_response(response_data, self.get_access_token_name())
|
214
298
|
|
215
|
-
|
299
|
+
def _extract_refresh_token(self, response_data: Mapping[str, Any]) -> Any:
|
300
|
+
"""
|
301
|
+
Extracts the refresh token from the given response data.
|
216
302
|
|
217
|
-
|
218
|
-
|
303
|
+
Args:
|
304
|
+
response_data (Mapping[str, Any]): The response data from which to extract the refresh token.
|
305
|
+
|
306
|
+
Returns:
|
307
|
+
str: The extracted refresh token.
|
219
308
|
"""
|
220
|
-
|
309
|
+
return self._find_and_get_value_from_response(response_data, self.get_refresh_token_name())
|
310
|
+
|
311
|
+
def _extract_token_expiry_date(self, response_data: Mapping[str, Any]) -> Any:
|
312
|
+
"""
|
313
|
+
Extracts the token_expiry_date, like `expires_in` or `expires_at`, etc from the given response data.
|
314
|
+
|
315
|
+
Args:
|
316
|
+
response_data (Mapping[str, Any]): The response data from which to extract the token_expiry_date.
|
317
|
+
|
318
|
+
Returns:
|
319
|
+
str: The extracted token_expiry_date.
|
221
320
|
"""
|
321
|
+
return self._find_and_get_value_from_response(response_data, self.get_expires_in_name())
|
322
|
+
|
323
|
+
def _find_and_get_value_from_response(
|
324
|
+
self,
|
325
|
+
response_data: Mapping[str, Any],
|
326
|
+
key_name: str,
|
327
|
+
max_depth: int = 5,
|
328
|
+
current_depth: int = 0,
|
329
|
+
) -> Any:
|
330
|
+
"""
|
331
|
+
Recursively searches for a specified key in a nested dictionary or list and returns its value if found.
|
332
|
+
|
333
|
+
Args:
|
334
|
+
response_data (Mapping[str, Any]): The response data to search through, which can be a dictionary or a list.
|
335
|
+
key_name (str): The key to search for in the response data.
|
336
|
+
max_depth (int, optional): The maximum depth to search for the key to avoid infinite recursion. Defaults to 5.
|
337
|
+
current_depth (int, optional): The current depth of the recursion. Defaults to 0.
|
338
|
+
|
339
|
+
Returns:
|
340
|
+
Any: The value associated with the specified key if found, otherwise None.
|
341
|
+
|
342
|
+
Raises:
|
343
|
+
AirbyteTracedException: If the maximum recursion depth is reached without finding the key.
|
344
|
+
"""
|
345
|
+
if current_depth > max_depth:
|
346
|
+
# this is needed to avoid an inf loop, possible with a very deep nesting observed.
|
347
|
+
message = f"The maximum level of recursion is reached. Couldn't find the speficied `{key_name}` in the response."
|
348
|
+
raise ResponseKeysMaxRecurtionReached(
|
349
|
+
internal_message=message, message=message, failure_type=FailureType.config_error
|
350
|
+
)
|
351
|
+
|
352
|
+
if isinstance(response_data, dict):
|
353
|
+
# get from the root level
|
354
|
+
if key_name in response_data:
|
355
|
+
return response_data[key_name]
|
356
|
+
|
357
|
+
# get from the nested object
|
358
|
+
for _, value in response_data.items():
|
359
|
+
result = self._find_and_get_value_from_response(
|
360
|
+
value, key_name, max_depth, current_depth + 1
|
361
|
+
)
|
362
|
+
if result is not None:
|
363
|
+
return result
|
364
|
+
|
365
|
+
# get from the nested array object
|
366
|
+
elif isinstance(response_data, list):
|
367
|
+
for item in response_data:
|
368
|
+
result = self._find_and_get_value_from_response(
|
369
|
+
item, key_name, max_depth, current_depth + 1
|
370
|
+
)
|
371
|
+
if result is not None:
|
372
|
+
return result
|
222
373
|
|
223
374
|
return None
|
224
375
|
|
376
|
+
@property
|
377
|
+
def _message_repository(self) -> Optional[MessageRepository]:
|
378
|
+
"""
|
379
|
+
The implementation can define a message_repository if it wants debugging logs for HTTP requests
|
380
|
+
"""
|
381
|
+
return _NOOP_MESSAGE_REPOSITORY
|
382
|
+
|
383
|
+
def _log_response(self, response: requests.Response) -> None:
|
384
|
+
"""
|
385
|
+
Logs the HTTP response using the message repository if it is available.
|
386
|
+
|
387
|
+
Args:
|
388
|
+
response (requests.Response): The HTTP response to log.
|
389
|
+
"""
|
390
|
+
if self._message_repository:
|
391
|
+
self._message_repository.log_message(
|
392
|
+
Level.DEBUG,
|
393
|
+
lambda: format_http_message(
|
394
|
+
response,
|
395
|
+
"Refresh token",
|
396
|
+
"Obtains access token",
|
397
|
+
self._NO_STREAM_NAME,
|
398
|
+
is_auxiliary=True,
|
399
|
+
),
|
400
|
+
)
|
401
|
+
|
402
|
+
# ----------------
|
403
|
+
# ABSTR METHODS
|
404
|
+
# ----------------
|
405
|
+
|
225
406
|
@abstractmethod
|
226
407
|
def get_token_refresh_endpoint(self) -> Optional[str]:
|
227
408
|
"""Returns the endpoint to refresh the access token"""
|
@@ -295,23 +476,3 @@ class AbstractOauth2Authenticator(AuthBase):
|
|
295
476
|
@abstractmethod
|
296
477
|
def access_token(self, value: str) -> str:
|
297
478
|
"""Setter for the access token"""
|
298
|
-
|
299
|
-
@property
|
300
|
-
def _message_repository(self) -> Optional[MessageRepository]:
|
301
|
-
"""
|
302
|
-
The implementation can define a message_repository if it wants debugging logs for HTTP requests
|
303
|
-
"""
|
304
|
-
return _NOOP_MESSAGE_REPOSITORY
|
305
|
-
|
306
|
-
def _log_response(self, response: requests.Response) -> None:
|
307
|
-
if self._message_repository:
|
308
|
-
self._message_repository.log_message(
|
309
|
-
Level.DEBUG,
|
310
|
-
lambda: format_http_message(
|
311
|
-
response,
|
312
|
-
"Refresh token",
|
313
|
-
"Obtains access token",
|
314
|
-
self._NO_STREAM_NAME,
|
315
|
-
is_auxiliary=True,
|
316
|
-
),
|
317
|
-
)
|
@@ -51,7 +51,7 @@ class Oauth2Authenticator(AbstractOauth2Authenticator):
|
|
51
51
|
refresh_token_error_status_codes: Tuple[int, ...] = (),
|
52
52
|
refresh_token_error_key: str = "",
|
53
53
|
refresh_token_error_values: Tuple[str, ...] = (),
|
54
|
-
):
|
54
|
+
) -> None:
|
55
55
|
self._token_refresh_endpoint = token_refresh_endpoint
|
56
56
|
self._client_secret_name = client_secret_name
|
57
57
|
self._client_secret = client_secret
|
@@ -175,7 +175,7 @@ class SingleUseRefreshTokenOauth2Authenticator(Oauth2Authenticator):
|
|
175
175
|
refresh_token_error_status_codes: Tuple[int, ...] = (),
|
176
176
|
refresh_token_error_key: str = "",
|
177
177
|
refresh_token_error_values: Tuple[str, ...] = (),
|
178
|
-
):
|
178
|
+
) -> None:
|
179
179
|
"""
|
180
180
|
Args:
|
181
181
|
connector_config (Mapping[str, Any]): The full connector configuration
|
@@ -196,18 +196,12 @@ class SingleUseRefreshTokenOauth2Authenticator(Oauth2Authenticator):
|
|
196
196
|
token_expiry_is_time_of_expiration bool: set True it if expires_in is returned as time of expiration instead of the number seconds until expiration
|
197
197
|
message_repository (MessageRepository): the message repository used to emit logs on HTTP requests and control message on config update
|
198
198
|
"""
|
199
|
-
self.
|
200
|
-
|
201
|
-
|
202
|
-
else dpath.get(connector_config, ("credentials", "client_id")) # type: ignore[arg-type]
|
199
|
+
self._connector_config = connector_config
|
200
|
+
self._client_id: str = self._get_config_value_by_path(
|
201
|
+
("credentials", "client_id"), client_id
|
203
202
|
)
|
204
|
-
self._client_secret = (
|
205
|
-
client_secret
|
206
|
-
if client_secret is not None
|
207
|
-
else dpath.get(
|
208
|
-
connector_config, # type: ignore[arg-type]
|
209
|
-
("credentials", "client_secret"),
|
210
|
-
)
|
203
|
+
self._client_secret: str = self._get_config_value_by_path(
|
204
|
+
("credentials", "client_secret"), client_secret
|
211
205
|
)
|
212
206
|
self._client_id_name = client_id_name
|
213
207
|
self._client_secret_name = client_secret_name
|
@@ -222,9 +216,9 @@ class SingleUseRefreshTokenOauth2Authenticator(Oauth2Authenticator):
|
|
222
216
|
super().__init__(
|
223
217
|
token_refresh_endpoint=token_refresh_endpoint,
|
224
218
|
client_id_name=self._client_id_name,
|
225
|
-
client_id=self.
|
219
|
+
client_id=self._client_id,
|
226
220
|
client_secret_name=self._client_secret_name,
|
227
|
-
client_secret=self.
|
221
|
+
client_secret=self._client_secret,
|
228
222
|
refresh_token=self.get_refresh_token(),
|
229
223
|
refresh_token_name=self._refresh_token_name,
|
230
224
|
scopes=scopes,
|
@@ -242,51 +236,62 @@ class SingleUseRefreshTokenOauth2Authenticator(Oauth2Authenticator):
|
|
242
236
|
refresh_token_error_values=refresh_token_error_values,
|
243
237
|
)
|
244
238
|
|
245
|
-
def get_refresh_token_name(self) -> str:
|
246
|
-
return self._refresh_token_name
|
247
|
-
|
248
|
-
def get_client_id(self) -> str:
|
249
|
-
return self._client_id
|
250
|
-
|
251
|
-
def get_client_secret(self) -> str:
|
252
|
-
return self._client_secret
|
253
|
-
|
254
239
|
@property
|
255
240
|
def access_token(self) -> str:
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
241
|
+
"""
|
242
|
+
Retrieve the access token from the configuration.
|
243
|
+
|
244
|
+
Returns:
|
245
|
+
str: The access token.
|
246
|
+
"""
|
247
|
+
return self._get_config_value_by_path(self._access_token_config_path) # type: ignore[return-value]
|
261
248
|
|
262
249
|
@access_token.setter
|
263
250
|
def access_token(self, new_access_token: str) -> None:
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
251
|
+
"""
|
252
|
+
Sets a new access token.
|
253
|
+
|
254
|
+
Args:
|
255
|
+
new_access_token (str): The new access token to be set.
|
256
|
+
"""
|
257
|
+
self._set_config_value_by_path(self._access_token_config_path, new_access_token)
|
269
258
|
|
270
259
|
def get_refresh_token(self) -> str:
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
260
|
+
"""
|
261
|
+
Retrieve the refresh token from the configuration.
|
262
|
+
|
263
|
+
This method fetches the refresh token using the configuration path specified
|
264
|
+
by `_refresh_token_config_path`.
|
265
|
+
|
266
|
+
Returns:
|
267
|
+
str: The refresh token as a string.
|
268
|
+
"""
|
269
|
+
return self._get_config_value_by_path(self._refresh_token_config_path) # type: ignore[return-value]
|
276
270
|
|
277
271
|
def set_refresh_token(self, new_refresh_token: str) -> None:
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
272
|
+
"""
|
273
|
+
Updates the refresh token in the configuration.
|
274
|
+
|
275
|
+
Args:
|
276
|
+
new_refresh_token (str): The new refresh token to be set.
|
277
|
+
"""
|
278
|
+
self._set_config_value_by_path(self._refresh_token_config_path, new_refresh_token)
|
283
279
|
|
284
280
|
def get_token_expiry_date(self) -> AirbyteDateTime:
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
281
|
+
"""
|
282
|
+
Retrieves the token expiry date from the configuration.
|
283
|
+
|
284
|
+
This method fetches the token expiry date from the configuration using the specified path.
|
285
|
+
If the expiry date is an empty string, it returns the current date and time minus one day.
|
286
|
+
Otherwise, it parses the expiry date string into an AirbyteDateTime object.
|
287
|
+
|
288
|
+
Returns:
|
289
|
+
AirbyteDateTime: The parsed or calculated token expiry date.
|
290
|
+
|
291
|
+
Raises:
|
292
|
+
TypeError: If the result is not an instance of AirbyteDateTime.
|
293
|
+
"""
|
294
|
+
expiry_date = self._get_config_value_by_path(self._token_expiry_date_config_path)
|
290
295
|
result = (
|
291
296
|
ab_datetime_now() - timedelta(days=1)
|
292
297
|
if expiry_date == ""
|
@@ -296,14 +301,15 @@ class SingleUseRefreshTokenOauth2Authenticator(Oauth2Authenticator):
|
|
296
301
|
return result
|
297
302
|
raise TypeError("Invalid datetime conversion")
|
298
303
|
|
299
|
-
def set_token_expiry_date( # type: ignore[override]
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
304
|
+
def set_token_expiry_date(self, new_token_expiry_date: AirbyteDateTime) -> None: # type: ignore[override]
|
305
|
+
"""
|
306
|
+
Sets the token expiry date in the configuration.
|
307
|
+
|
308
|
+
Args:
|
309
|
+
new_token_expiry_date (AirbyteDateTime): The new expiry date for the token.
|
310
|
+
"""
|
311
|
+
self._set_config_value_by_path(
|
312
|
+
self._token_expiry_date_config_path, str(new_token_expiry_date)
|
307
313
|
)
|
308
314
|
|
309
315
|
def token_has_expired(self) -> bool:
|
@@ -315,6 +321,16 @@ class SingleUseRefreshTokenOauth2Authenticator(Oauth2Authenticator):
|
|
315
321
|
access_token_expires_in: str,
|
316
322
|
token_expiry_date_format: str | None = None,
|
317
323
|
) -> AirbyteDateTime:
|
324
|
+
"""
|
325
|
+
Calculate the new token expiry date based on the provided expiration duration or format.
|
326
|
+
|
327
|
+
Args:
|
328
|
+
access_token_expires_in (str): The duration (in seconds) until the access token expires, or the expiry date in a specific format.
|
329
|
+
token_expiry_date_format (str | None, optional): The format of the expiry date if provided. Defaults to None.
|
330
|
+
|
331
|
+
Returns:
|
332
|
+
AirbyteDateTime: The calculated expiry date of the access token.
|
333
|
+
"""
|
318
334
|
if token_expiry_date_format:
|
319
335
|
return ab_datetime_parse(access_token_expires_in)
|
320
336
|
else:
|
@@ -336,27 +352,82 @@ class SingleUseRefreshTokenOauth2Authenticator(Oauth2Authenticator):
|
|
336
352
|
self.access_token = new_access_token
|
337
353
|
self.set_refresh_token(new_refresh_token)
|
338
354
|
self.set_token_expiry_date(new_token_expiry_date)
|
339
|
-
|
340
|
-
# Usually, a class shouldn't care about the implementation details but to keep backward compatibility where we print the
|
341
|
-
# message directly in the console, this is needed
|
342
|
-
if not isinstance(self._message_repository, NoopMessageRepository):
|
343
|
-
self._message_repository.emit_message(
|
344
|
-
create_connector_config_control_message(self._connector_config) # type: ignore[arg-type]
|
345
|
-
)
|
346
|
-
else:
|
347
|
-
emit_configuration_as_airbyte_control_message(self._connector_config) # type: ignore[arg-type]
|
355
|
+
self._emit_control_message()
|
348
356
|
return self.access_token
|
349
357
|
|
350
|
-
def refresh_access_token( # type: ignore[override]
|
351
|
-
|
352
|
-
|
353
|
-
|
358
|
+
def refresh_access_token(self) -> Tuple[str, str, str]: # type: ignore[override]
|
359
|
+
"""
|
360
|
+
Refreshes the access token by making a handled request and extracting the necessary token information.
|
361
|
+
|
362
|
+
Returns:
|
363
|
+
Tuple[str, str, str]: A tuple containing the new access token, token expiry date, and refresh token.
|
364
|
+
"""
|
365
|
+
response_json = self._make_handled_request()
|
354
366
|
return (
|
355
|
-
|
356
|
-
|
357
|
-
|
367
|
+
self._extract_access_token(response_json),
|
368
|
+
self._extract_token_expiry_date(response_json),
|
369
|
+
self._extract_refresh_token(response_json),
|
370
|
+
)
|
371
|
+
|
372
|
+
def _set_config_value_by_path(self, config_path: Union[str, Sequence[str]], value: Any) -> None:
|
373
|
+
"""
|
374
|
+
Set a value in the connector configuration at the specified path.
|
375
|
+
|
376
|
+
Args:
|
377
|
+
config_path (Union[str, Sequence[str]]): The path within the configuration where the value should be set.
|
378
|
+
This can be a string representing a single key or a sequence of strings representing a nested path.
|
379
|
+
value (Any): The value to set at the specified path in the configuration.
|
380
|
+
|
381
|
+
Returns:
|
382
|
+
None
|
383
|
+
"""
|
384
|
+
dpath.new(self._connector_config, config_path, value) # type: ignore[arg-type]
|
385
|
+
|
386
|
+
def _get_config_value_by_path(
|
387
|
+
self, config_path: Union[str, Sequence[str]], default: Optional[str] = None
|
388
|
+
) -> str | Any:
|
389
|
+
"""
|
390
|
+
Retrieve a value from the connector configuration using a specified path.
|
391
|
+
|
392
|
+
Args:
|
393
|
+
config_path (Union[str, Sequence[str]]): The path to the desired configuration value. This can be a string or a sequence of strings.
|
394
|
+
default (Optional[str], optional): The default value to return if the specified path does not exist in the configuration. Defaults to None.
|
395
|
+
|
396
|
+
Returns:
|
397
|
+
Any: The value from the configuration at the specified path, or the default value if the path does not exist.
|
398
|
+
"""
|
399
|
+
return dpath.get(
|
400
|
+
self._connector_config, # type: ignore[arg-type]
|
401
|
+
config_path,
|
402
|
+
default=default if default is not None else "",
|
358
403
|
)
|
359
404
|
|
405
|
+
def _emit_control_message(self) -> None:
|
406
|
+
"""
|
407
|
+
Emits a control message based on the connector configuration.
|
408
|
+
|
409
|
+
This method checks if the message repository is not a NoopMessageRepository.
|
410
|
+
If it is not, it emits a message using the message repository. Otherwise,
|
411
|
+
it falls back to emitting the configuration as an Airbyte control message
|
412
|
+
directly to the console for backward compatibility.
|
413
|
+
|
414
|
+
Note:
|
415
|
+
The function `emit_configuration_as_airbyte_control_message` has been deprecated
|
416
|
+
in favor of the package `airbyte_cdk.sources.message`.
|
417
|
+
|
418
|
+
Raises:
|
419
|
+
TypeError: If the argument types are incorrect.
|
420
|
+
"""
|
421
|
+
# FIXME emit_configuration_as_airbyte_control_message as been deprecated in favor of package airbyte_cdk.sources.message
|
422
|
+
# Usually, a class shouldn't care about the implementation details but to keep backward compatibility where we print the
|
423
|
+
# message directly in the console, this is needed
|
424
|
+
if not isinstance(self._message_repository, NoopMessageRepository):
|
425
|
+
self._message_repository.emit_message(
|
426
|
+
create_connector_config_control_message(self._connector_config) # type: ignore[arg-type]
|
427
|
+
)
|
428
|
+
else:
|
429
|
+
emit_configuration_as_airbyte_control_message(self._connector_config) # type: ignore[arg-type]
|
430
|
+
|
360
431
|
@property
|
361
432
|
def _message_repository(self) -> MessageRepository:
|
362
433
|
"""
|