airbyte-cdk 6.34.0.dev2__py3-none-any.whl → 6.34.1.dev0__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 +12 -16
- airbyte_cdk/connector_builder/message_grouper.py +448 -0
- airbyte_cdk/sources/declarative/async_job/job_orchestrator.py +7 -7
- airbyte_cdk/sources/declarative/auth/jwt.py +11 -17
- airbyte_cdk/sources/declarative/auth/oauth.py +1 -6
- airbyte_cdk/sources/declarative/auth/token.py +8 -3
- airbyte_cdk/sources/declarative/concurrent_declarative_source.py +19 -30
- airbyte_cdk/sources/declarative/declarative_component_schema.yaml +85 -203
- airbyte_cdk/sources/declarative/declarative_stream.py +1 -3
- airbyte_cdk/sources/declarative/decoders/__init__.py +4 -0
- airbyte_cdk/sources/declarative/decoders/composite_raw_decoder.py +2 -7
- airbyte_cdk/sources/declarative/decoders/json_decoder.py +58 -12
- airbyte_cdk/sources/declarative/extractors/record_selector.py +3 -12
- airbyte_cdk/sources/declarative/incremental/concurrent_partition_cursor.py +38 -122
- airbyte_cdk/sources/declarative/incremental/datetime_based_cursor.py +6 -12
- airbyte_cdk/sources/declarative/manifest_declarative_source.py +0 -9
- airbyte_cdk/sources/declarative/models/declarative_component_schema.py +41 -150
- airbyte_cdk/sources/declarative/parsers/model_to_component_factory.py +84 -234
- airbyte_cdk/sources/declarative/partition_routers/async_job_partition_router.py +5 -5
- airbyte_cdk/sources/declarative/partition_routers/list_partition_router.py +2 -4
- airbyte_cdk/sources/declarative/partition_routers/substream_partition_router.py +18 -26
- airbyte_cdk/sources/declarative/requesters/http_requester.py +1 -8
- airbyte_cdk/sources/declarative/requesters/paginators/default_paginator.py +5 -16
- airbyte_cdk/sources/declarative/requesters/request_option.py +4 -83
- airbyte_cdk/sources/declarative/requesters/request_options/datetime_based_request_options_provider.py +6 -7
- airbyte_cdk/sources/declarative/retrievers/async_retriever.py +12 -6
- airbyte_cdk/sources/declarative/retrievers/simple_retriever.py +1 -4
- airbyte_cdk/sources/file_based/config/abstract_file_based_spec.py +1 -2
- airbyte_cdk/sources/file_based/file_based_source.py +37 -70
- airbyte_cdk/sources/file_based/file_based_stream_reader.py +12 -107
- airbyte_cdk/sources/file_based/stream/__init__.py +1 -10
- airbyte_cdk/sources/streams/call_rate.py +47 -185
- airbyte_cdk/sources/streams/http/http.py +2 -1
- airbyte_cdk/sources/streams/http/requests_native_auth/abstract_oauth.py +56 -217
- airbyte_cdk/sources/streams/http/requests_native_auth/oauth.py +73 -144
- airbyte_cdk/test/mock_http/mocker.py +1 -9
- airbyte_cdk/test/mock_http/response.py +3 -6
- airbyte_cdk/utils/datetime_helpers.py +66 -48
- airbyte_cdk/utils/mapping_helpers.py +26 -126
- {airbyte_cdk-6.34.0.dev2.dist-info → airbyte_cdk-6.34.1.dev0.dist-info}/METADATA +1 -1
- {airbyte_cdk-6.34.0.dev2.dist-info → airbyte_cdk-6.34.1.dev0.dist-info}/RECORD +45 -54
- airbyte_cdk/connector_builder/test_reader/__init__.py +0 -7
- airbyte_cdk/connector_builder/test_reader/helpers.py +0 -591
- airbyte_cdk/connector_builder/test_reader/message_grouper.py +0 -160
- airbyte_cdk/connector_builder/test_reader/reader.py +0 -441
- airbyte_cdk/connector_builder/test_reader/types.py +0 -75
- airbyte_cdk/sources/file_based/config/validate_config_transfer_modes.py +0 -81
- airbyte_cdk/sources/file_based/stream/identities_stream.py +0 -47
- airbyte_cdk/sources/file_based/stream/permissions_file_based_stream.py +0 -85
- airbyte_cdk/sources/specs/transfer_modes.py +0 -26
- airbyte_cdk/sources/streams/permissions/identities_stream.py +0 -75
- {airbyte_cdk-6.34.0.dev2.dist-info → airbyte_cdk-6.34.1.dev0.dist-info}/LICENSE.txt +0 -0
- {airbyte_cdk-6.34.0.dev2.dist-info → airbyte_cdk-6.34.1.dev0.dist-info}/LICENSE_SHORT +0 -0
- {airbyte_cdk-6.34.0.dev2.dist-info → airbyte_cdk-6.34.1.dev0.dist-info}/WHEEL +0 -0
- {airbyte_cdk-6.34.0.dev2.dist-info → airbyte_cdk-6.34.1.dev0.dist-info}/entry_points.txt +0 -0
@@ -25,13 +25,6 @@ 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
|
-
|
35
28
|
class AbstractOauth2Authenticator(AuthBase):
|
36
29
|
"""
|
37
30
|
Abstract class for an OAuth authenticators that implements the OAuth token refresh flow. The authenticator
|
@@ -60,31 +53,15 @@ class AbstractOauth2Authenticator(AuthBase):
|
|
60
53
|
request.headers.update(self.get_auth_header())
|
61
54
|
return request
|
62
55
|
|
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
|
-
|
83
56
|
def get_auth_header(self) -> Mapping[str, Any]:
|
84
57
|
"""HTTP header to set on the requests"""
|
85
58
|
token = self.access_token if self._is_access_token_flow else self.get_access_token()
|
86
59
|
return {"Authorization": f"Bearer {token}"}
|
87
60
|
|
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
|
+
|
88
65
|
def get_access_token(self) -> str:
|
89
66
|
"""Returns the access token"""
|
90
67
|
if self.token_has_expired():
|
@@ -130,39 +107,9 @@ class AbstractOauth2Authenticator(AuthBase):
|
|
130
107
|
headers = self.get_refresh_request_headers()
|
131
108
|
return headers if headers else None
|
132
109
|
|
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
|
-
|
151
110
|
def _wrap_refresh_token_exception(
|
152
111
|
self, exception: requests.exceptions.RequestException
|
153
112
|
) -> 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
|
-
"""
|
166
113
|
try:
|
167
114
|
if exception.response is not None:
|
168
115
|
exception_content = exception.response.json()
|
@@ -184,24 +131,7 @@ class AbstractOauth2Authenticator(AuthBase):
|
|
184
131
|
),
|
185
132
|
max_time=300,
|
186
133
|
)
|
187
|
-
def
|
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
|
-
"""
|
134
|
+
def _get_refresh_access_token_response(self) -> Any:
|
205
135
|
try:
|
206
136
|
response = requests.request(
|
207
137
|
method="POST",
|
@@ -209,10 +139,22 @@ class AbstractOauth2Authenticator(AuthBase):
|
|
209
139
|
data=self.build_refresh_request_body(),
|
210
140
|
headers=self.build_refresh_request_headers(),
|
211
141
|
)
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
142
|
+
if response.ok:
|
143
|
+
response_json = response.json()
|
144
|
+
# Add the access token to the list of secrets so it is replaced before logging the response
|
145
|
+
# 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...
|
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()
|
216
158
|
except requests.exceptions.RequestException as e:
|
217
159
|
if e.response is not None:
|
218
160
|
if e.response.status_code == 429 or e.response.status_code >= 500:
|
@@ -226,34 +168,17 @@ class AbstractOauth2Authenticator(AuthBase):
|
|
226
168
|
except Exception as e:
|
227
169
|
raise Exception(f"Error while refreshing access token: {e}") from e
|
228
170
|
|
229
|
-
def
|
171
|
+
def refresh_access_token(self) -> Tuple[str, Union[str, int]]:
|
230
172
|
"""
|
231
|
-
|
232
|
-
|
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.
|
173
|
+
Returns the refresh token and its expiration datetime
|
241
174
|
|
242
|
-
|
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.
|
175
|
+
:return: a tuple of (access_token, token_lifespan)
|
245
176
|
"""
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
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
|
177
|
+
response_json = self._get_refresh_access_token_response()
|
178
|
+
|
179
|
+
return response_json[self.get_access_token_name()], response_json[
|
180
|
+
self.get_expires_in_name()
|
181
|
+
]
|
257
182
|
|
258
183
|
def _parse_token_expiration_date(self, value: Union[str, int]) -> AirbyteDateTime:
|
259
184
|
"""
|
@@ -261,9 +186,6 @@ class AbstractOauth2Authenticator(AuthBase):
|
|
261
186
|
|
262
187
|
:return: expiration datetime
|
263
188
|
"""
|
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()
|
267
189
|
|
268
190
|
if self.token_expiry_is_time_of_expiration:
|
269
191
|
if not self.token_expiry_date_format:
|
@@ -284,124 +206,21 @@ class AbstractOauth2Authenticator(AuthBase):
|
|
284
206
|
f"Invalid expires_in value: {value}. Expected number of seconds when no format specified."
|
285
207
|
)
|
286
208
|
|
287
|
-
|
288
|
-
|
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.
|
296
|
-
"""
|
297
|
-
return self._find_and_get_value_from_response(response_data, self.get_access_token_name())
|
298
|
-
|
299
|
-
def _extract_refresh_token(self, response_data: Mapping[str, Any]) -> Any:
|
300
|
-
"""
|
301
|
-
Extracts the refresh token from the given response data.
|
302
|
-
|
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.
|
308
|
-
"""
|
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.
|
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:
|
209
|
+
@property
|
210
|
+
def token_expiry_is_time_of_expiration(self) -> bool:
|
330
211
|
"""
|
331
|
-
|
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.
|
212
|
+
Indicates that the Token Expiry returns the date until which the token will be valid, not the amount of time it will be valid.
|
344
213
|
"""
|
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
214
|
|
352
|
-
|
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
|
373
|
-
|
374
|
-
return None
|
215
|
+
return False
|
375
216
|
|
376
217
|
@property
|
377
|
-
def
|
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:
|
218
|
+
def token_expiry_date_format(self) -> Optional[str]:
|
384
219
|
"""
|
385
|
-
|
386
|
-
|
387
|
-
Args:
|
388
|
-
response (requests.Response): The HTTP response to log.
|
220
|
+
Format of the datetime; exists it if expires_in is returned as the expiration datetime instead of seconds until it expires
|
389
221
|
"""
|
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
222
|
|
402
|
-
|
403
|
-
# ABSTR METHODS
|
404
|
-
# ----------------
|
223
|
+
return None
|
405
224
|
|
406
225
|
@abstractmethod
|
407
226
|
def get_token_refresh_endpoint(self) -> Optional[str]:
|
@@ -476,3 +295,23 @@ class AbstractOauth2Authenticator(AuthBase):
|
|
476
295
|
@abstractmethod
|
477
296
|
def access_token(self, value: str) -> str:
|
478
297
|
"""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
|
+
):
|
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
|
+
):
|
179
179
|
"""
|
180
180
|
Args:
|
181
181
|
connector_config (Mapping[str, Any]): The full connector configuration
|
@@ -196,12 +196,18 @@ 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
|
-
|
199
|
+
self._client_id = (
|
200
|
+
client_id # type: ignore[assignment] # Incorrect type for assignment
|
201
|
+
if client_id is not None
|
202
|
+
else dpath.get(connector_config, ("credentials", "client_id")) # type: ignore[arg-type]
|
202
203
|
)
|
203
|
-
self._client_secret
|
204
|
-
|
204
|
+
self._client_secret = (
|
205
|
+
client_secret # type: ignore[assignment] # Incorrect type for assignment
|
206
|
+
if client_secret is not None
|
207
|
+
else dpath.get(
|
208
|
+
connector_config, # type: ignore[arg-type]
|
209
|
+
("credentials", "client_secret"),
|
210
|
+
)
|
205
211
|
)
|
206
212
|
self._client_id_name = client_id_name
|
207
213
|
self._client_secret_name = client_secret_name
|
@@ -216,9 +222,9 @@ class SingleUseRefreshTokenOauth2Authenticator(Oauth2Authenticator):
|
|
216
222
|
super().__init__(
|
217
223
|
token_refresh_endpoint=token_refresh_endpoint,
|
218
224
|
client_id_name=self._client_id_name,
|
219
|
-
client_id=self.
|
225
|
+
client_id=self.get_client_id(),
|
220
226
|
client_secret_name=self._client_secret_name,
|
221
|
-
client_secret=self.
|
227
|
+
client_secret=self.get_client_secret(),
|
222
228
|
refresh_token=self.get_refresh_token(),
|
223
229
|
refresh_token_name=self._refresh_token_name,
|
224
230
|
scopes=scopes,
|
@@ -236,62 +242,51 @@ class SingleUseRefreshTokenOauth2Authenticator(Oauth2Authenticator):
|
|
236
242
|
refresh_token_error_values=refresh_token_error_values,
|
237
243
|
)
|
238
244
|
|
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
|
+
|
239
254
|
@property
|
240
255
|
def access_token(self) -> str:
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
"""
|
247
|
-
return self._get_config_value_by_path(self._access_token_config_path) # type: ignore[return-value]
|
256
|
+
return dpath.get( # type: ignore[return-value]
|
257
|
+
self._connector_config, # type: ignore[arg-type]
|
258
|
+
self._access_token_config_path,
|
259
|
+
default="",
|
260
|
+
)
|
248
261
|
|
249
262
|
@access_token.setter
|
250
263
|
def access_token(self, new_access_token: str) -> None:
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
"""
|
257
|
-
self._set_config_value_by_path(self._access_token_config_path, new_access_token)
|
264
|
+
dpath.new(
|
265
|
+
self._connector_config, # type: ignore[arg-type]
|
266
|
+
self._access_token_config_path,
|
267
|
+
new_access_token,
|
268
|
+
)
|
258
269
|
|
259
270
|
def get_refresh_token(self) -> str:
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
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]
|
271
|
+
return dpath.get( # type: ignore[return-value]
|
272
|
+
self._connector_config, # type: ignore[arg-type]
|
273
|
+
self._refresh_token_config_path,
|
274
|
+
default="",
|
275
|
+
)
|
270
276
|
|
271
277
|
def set_refresh_token(self, new_refresh_token: str) -> None:
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
"""
|
278
|
-
self._set_config_value_by_path(self._refresh_token_config_path, new_refresh_token)
|
278
|
+
dpath.new(
|
279
|
+
self._connector_config, # type: ignore[arg-type]
|
280
|
+
self._refresh_token_config_path,
|
281
|
+
new_refresh_token,
|
282
|
+
)
|
279
283
|
|
280
284
|
def get_token_expiry_date(self) -> AirbyteDateTime:
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
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)
|
285
|
+
expiry_date = dpath.get(
|
286
|
+
self._connector_config, # type: ignore[arg-type]
|
287
|
+
self._token_expiry_date_config_path,
|
288
|
+
default="",
|
289
|
+
)
|
295
290
|
result = (
|
296
291
|
ab_datetime_now() - timedelta(days=1)
|
297
292
|
if expiry_date == ""
|
@@ -301,15 +296,14 @@ class SingleUseRefreshTokenOauth2Authenticator(Oauth2Authenticator):
|
|
301
296
|
return result
|
302
297
|
raise TypeError("Invalid datetime conversion")
|
303
298
|
|
304
|
-
def set_token_expiry_date(
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
self._token_expiry_date_config_path, str(new_token_expiry_date)
|
299
|
+
def set_token_expiry_date( # type: ignore[override]
|
300
|
+
self,
|
301
|
+
new_token_expiry_date: AirbyteDateTime,
|
302
|
+
) -> None:
|
303
|
+
dpath.new(
|
304
|
+
self._connector_config, # type: ignore[arg-type]
|
305
|
+
self._token_expiry_date_config_path,
|
306
|
+
str(new_token_expiry_date),
|
313
307
|
)
|
314
308
|
|
315
309
|
def token_has_expired(self) -> bool:
|
@@ -321,16 +315,6 @@ class SingleUseRefreshTokenOauth2Authenticator(Oauth2Authenticator):
|
|
321
315
|
access_token_expires_in: str,
|
322
316
|
token_expiry_date_format: str | None = None,
|
323
317
|
) -> 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
|
-
"""
|
334
318
|
if token_expiry_date_format:
|
335
319
|
return ab_datetime_parse(access_token_expires_in)
|
336
320
|
else:
|
@@ -352,82 +336,27 @@ class SingleUseRefreshTokenOauth2Authenticator(Oauth2Authenticator):
|
|
352
336
|
self.access_token = new_access_token
|
353
337
|
self.set_refresh_token(new_refresh_token)
|
354
338
|
self.set_token_expiry_date(new_token_expiry_date)
|
355
|
-
|
339
|
+
# FIXME emit_configuration_as_airbyte_control_message as been deprecated in favor of package airbyte_cdk.sources.message
|
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]
|
356
348
|
return self.access_token
|
357
349
|
|
358
|
-
def refresh_access_token(
|
359
|
-
|
360
|
-
|
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()
|
350
|
+
def refresh_access_token( # type: ignore[override] # Signature doesn't match base class
|
351
|
+
self,
|
352
|
+
) -> Tuple[str, str, str]:
|
353
|
+
response_json = self._get_refresh_access_token_response()
|
366
354
|
return (
|
367
|
-
self.
|
368
|
-
self.
|
369
|
-
self.
|
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 "",
|
355
|
+
response_json[self.get_access_token_name()],
|
356
|
+
response_json[self.get_expires_in_name()],
|
357
|
+
response_json[self.get_refresh_token_name()],
|
403
358
|
)
|
404
359
|
|
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
|
-
|
431
360
|
@property
|
432
361
|
def _message_repository(self) -> MessageRepository:
|
433
362
|
"""
|
@@ -17,7 +17,6 @@ class SupportedHttpMethods(str, Enum):
|
|
17
17
|
GET = "get"
|
18
18
|
PATCH = "patch"
|
19
19
|
POST = "post"
|
20
|
-
PUT = "put"
|
21
20
|
DELETE = "delete"
|
22
21
|
|
23
22
|
|
@@ -78,7 +77,7 @@ class HttpMocker(contextlib.ContextDecorator):
|
|
78
77
|
additional_matcher=self._matches_wrapper(matcher),
|
79
78
|
response_list=[
|
80
79
|
{
|
81
|
-
|
80
|
+
"text": response.body,
|
82
81
|
"status_code": response.status_code,
|
83
82
|
"headers": response.headers,
|
84
83
|
}
|
@@ -86,10 +85,6 @@ class HttpMocker(contextlib.ContextDecorator):
|
|
86
85
|
],
|
87
86
|
)
|
88
87
|
|
89
|
-
@staticmethod
|
90
|
-
def _get_body_field(response: HttpResponse) -> str:
|
91
|
-
return "text" if isinstance(response.body, str) else "content"
|
92
|
-
|
93
88
|
def get(self, request: HttpRequest, responses: Union[HttpResponse, List[HttpResponse]]) -> None:
|
94
89
|
self._mock_request_method(SupportedHttpMethods.GET, request, responses)
|
95
90
|
|
@@ -103,9 +98,6 @@ class HttpMocker(contextlib.ContextDecorator):
|
|
103
98
|
) -> None:
|
104
99
|
self._mock_request_method(SupportedHttpMethods.POST, request, responses)
|
105
100
|
|
106
|
-
def put(self, request: HttpRequest, responses: Union[HttpResponse, List[HttpResponse]]) -> None:
|
107
|
-
self._mock_request_method(SupportedHttpMethods.PUT, request, responses)
|
108
|
-
|
109
101
|
def delete(
|
110
102
|
self, request: HttpRequest, responses: Union[HttpResponse, List[HttpResponse]]
|
111
103
|
) -> None:
|