airbyte-cdk 6.31.2.dev0__py3-none-any.whl → 6.33.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/cli/source_declarative_manifest/_run.py +9 -3
- airbyte_cdk/connector_builder/connector_builder_handler.py +3 -2
- 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 +89 -23
- airbyte_cdk/sources/declarative/auth/token_provider.py +4 -5
- airbyte_cdk/sources/declarative/checks/check_dynamic_stream.py +19 -9
- airbyte_cdk/sources/declarative/concurrent_declarative_source.py +145 -43
- airbyte_cdk/sources/declarative/declarative_component_schema.yaml +51 -2
- airbyte_cdk/sources/declarative/declarative_stream.py +3 -1
- airbyte_cdk/sources/declarative/extractors/record_filter.py +3 -5
- airbyte_cdk/sources/declarative/incremental/__init__.py +6 -0
- airbyte_cdk/sources/declarative/incremental/concurrent_partition_cursor.py +400 -0
- airbyte_cdk/sources/declarative/incremental/global_substream_cursor.py +3 -0
- airbyte_cdk/sources/declarative/incremental/per_partition_cursor.py +35 -3
- airbyte_cdk/sources/declarative/manifest_declarative_source.py +20 -7
- airbyte_cdk/sources/declarative/models/declarative_component_schema.py +41 -5
- airbyte_cdk/sources/declarative/parsers/custom_code_compiler.py +143 -0
- airbyte_cdk/sources/declarative/parsers/model_to_component_factory.py +313 -30
- airbyte_cdk/sources/declarative/partition_routers/async_job_partition_router.py +5 -5
- airbyte_cdk/sources/declarative/partition_routers/substream_partition_router.py +46 -12
- airbyte_cdk/sources/declarative/requesters/error_handlers/composite_error_handler.py +22 -0
- airbyte_cdk/sources/declarative/requesters/error_handlers/http_response_filter.py +4 -4
- airbyte_cdk/sources/declarative/retrievers/async_retriever.py +6 -12
- airbyte_cdk/sources/declarative/retrievers/simple_retriever.py +1 -1
- airbyte_cdk/sources/declarative/schema/__init__.py +2 -0
- airbyte_cdk/sources/declarative/schema/dynamic_schema_loader.py +44 -5
- airbyte_cdk/sources/http_logger.py +1 -1
- airbyte_cdk/sources/streams/concurrent/clamping.py +99 -0
- airbyte_cdk/sources/streams/concurrent/cursor.py +51 -57
- airbyte_cdk/sources/streams/concurrent/cursor_types.py +32 -0
- airbyte_cdk/sources/streams/concurrent/state_converters/datetime_stream_state_converter.py +22 -13
- airbyte_cdk/sources/streams/core.py +6 -6
- airbyte_cdk/sources/streams/http/http.py +1 -2
- airbyte_cdk/sources/streams/http/requests_native_auth/abstract_oauth.py +231 -62
- airbyte_cdk/sources/streams/http/requests_native_auth/oauth.py +171 -88
- airbyte_cdk/sources/types.py +4 -2
- airbyte_cdk/sources/utils/transform.py +23 -2
- airbyte_cdk/test/utils/manifest_only_fixtures.py +1 -2
- airbyte_cdk/utils/datetime_helpers.py +499 -0
- airbyte_cdk/utils/slice_hasher.py +8 -1
- airbyte_cdk-6.33.0.dist-info/LICENSE_SHORT +1 -0
- {airbyte_cdk-6.31.2.dev0.dist-info → airbyte_cdk-6.33.0.dist-info}/METADATA +6 -6
- {airbyte_cdk-6.31.2.dev0.dist-info → airbyte_cdk-6.33.0.dist-info}/RECORD +47 -41
- {airbyte_cdk-6.31.2.dev0.dist-info → airbyte_cdk-6.33.0.dist-info}/WHEEL +1 -1
- {airbyte_cdk-6.31.2.dev0.dist-info → airbyte_cdk-6.33.0.dist-info}/LICENSE.txt +0 -0
- {airbyte_cdk-6.31.2.dev0.dist-info → airbyte_cdk-6.33.0.dist-info}/entry_points.txt +0 -0
@@ -2,10 +2,10 @@
|
|
2
2
|
# Copyright (c) 2023 Airbyte, Inc., all rights reserved.
|
3
3
|
#
|
4
4
|
|
5
|
+
from datetime import timedelta
|
5
6
|
from typing import Any, List, Mapping, Optional, Sequence, Tuple, Union
|
6
7
|
|
7
8
|
import dpath
|
8
|
-
import pendulum
|
9
9
|
|
10
10
|
from airbyte_cdk.config_observation import (
|
11
11
|
create_connector_config_control_message,
|
@@ -15,6 +15,11 @@ from airbyte_cdk.sources.message import MessageRepository, NoopMessageRepository
|
|
15
15
|
from airbyte_cdk.sources.streams.http.requests_native_auth.abstract_oauth import (
|
16
16
|
AbstractOauth2Authenticator,
|
17
17
|
)
|
18
|
+
from airbyte_cdk.utils.datetime_helpers import (
|
19
|
+
AirbyteDateTime,
|
20
|
+
ab_datetime_now,
|
21
|
+
ab_datetime_parse,
|
22
|
+
)
|
18
23
|
|
19
24
|
|
20
25
|
class Oauth2Authenticator(AbstractOauth2Authenticator):
|
@@ -34,7 +39,7 @@ class Oauth2Authenticator(AbstractOauth2Authenticator):
|
|
34
39
|
client_secret_name: str = "client_secret",
|
35
40
|
refresh_token_name: str = "refresh_token",
|
36
41
|
scopes: List[str] | None = None,
|
37
|
-
token_expiry_date:
|
42
|
+
token_expiry_date: AirbyteDateTime | None = None,
|
38
43
|
token_expiry_date_format: str | None = None,
|
39
44
|
access_token_name: str = "access_token",
|
40
45
|
expires_in_name: str = "expires_in",
|
@@ -46,7 +51,7 @@ class Oauth2Authenticator(AbstractOauth2Authenticator):
|
|
46
51
|
refresh_token_error_status_codes: Tuple[int, ...] = (),
|
47
52
|
refresh_token_error_key: str = "",
|
48
53
|
refresh_token_error_values: Tuple[str, ...] = (),
|
49
|
-
):
|
54
|
+
) -> None:
|
50
55
|
self._token_refresh_endpoint = token_refresh_endpoint
|
51
56
|
self._client_secret_name = client_secret_name
|
52
57
|
self._client_secret = client_secret
|
@@ -62,7 +67,7 @@ class Oauth2Authenticator(AbstractOauth2Authenticator):
|
|
62
67
|
self._grant_type_name = grant_type_name
|
63
68
|
self._grant_type = grant_type
|
64
69
|
|
65
|
-
self._token_expiry_date = token_expiry_date or
|
70
|
+
self._token_expiry_date = token_expiry_date or (ab_datetime_now() - timedelta(days=1))
|
66
71
|
self._token_expiry_date_format = token_expiry_date_format
|
67
72
|
self._token_expiry_is_time_of_expiration = token_expiry_is_time_of_expiration
|
68
73
|
self._access_token = None
|
@@ -95,16 +100,16 @@ class Oauth2Authenticator(AbstractOauth2Authenticator):
|
|
95
100
|
return self._access_token_name
|
96
101
|
|
97
102
|
def get_scopes(self) -> list[str]:
|
98
|
-
return self._scopes # type: ignore
|
103
|
+
return self._scopes # type: ignore[return-value]
|
99
104
|
|
100
105
|
def get_expires_in_name(self) -> str:
|
101
106
|
return self._expires_in_name
|
102
107
|
|
103
108
|
def get_refresh_request_body(self) -> Mapping[str, Any]:
|
104
|
-
return self._refresh_request_body # type: ignore
|
109
|
+
return self._refresh_request_body # type: ignore[return-value]
|
105
110
|
|
106
111
|
def get_refresh_request_headers(self) -> Mapping[str, Any]:
|
107
|
-
return self._refresh_request_headers # type: ignore
|
112
|
+
return self._refresh_request_headers # type: ignore[return-value]
|
108
113
|
|
109
114
|
def get_grant_type_name(self) -> str:
|
110
115
|
return self._grant_type_name
|
@@ -112,7 +117,7 @@ class Oauth2Authenticator(AbstractOauth2Authenticator):
|
|
112
117
|
def get_grant_type(self) -> str:
|
113
118
|
return self._grant_type
|
114
119
|
|
115
|
-
def get_token_expiry_date(self) ->
|
120
|
+
def get_token_expiry_date(self) -> AirbyteDateTime:
|
116
121
|
return self._token_expiry_date
|
117
122
|
|
118
123
|
def set_token_expiry_date(self, value: Union[str, int]) -> None:
|
@@ -128,11 +133,11 @@ class Oauth2Authenticator(AbstractOauth2Authenticator):
|
|
128
133
|
|
129
134
|
@property
|
130
135
|
def access_token(self) -> str:
|
131
|
-
return self._access_token # type: ignore
|
136
|
+
return self._access_token # type: ignore[return-value]
|
132
137
|
|
133
138
|
@access_token.setter
|
134
139
|
def access_token(self, value: str) -> None:
|
135
|
-
self._access_token = value # type: ignore
|
140
|
+
self._access_token = value # type: ignore[assignment] # Incorrect type for assignment
|
136
141
|
|
137
142
|
|
138
143
|
class SingleUseRefreshTokenOauth2Authenticator(Oauth2Authenticator):
|
@@ -170,7 +175,7 @@ class SingleUseRefreshTokenOauth2Authenticator(Oauth2Authenticator):
|
|
170
175
|
refresh_token_error_status_codes: Tuple[int, ...] = (),
|
171
176
|
refresh_token_error_key: str = "",
|
172
177
|
refresh_token_error_values: Tuple[str, ...] = (),
|
173
|
-
):
|
178
|
+
) -> None:
|
174
179
|
"""
|
175
180
|
Args:
|
176
181
|
connector_config (Mapping[str, Any]): The full connector configuration
|
@@ -191,18 +196,12 @@ class SingleUseRefreshTokenOauth2Authenticator(Oauth2Authenticator):
|
|
191
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
|
192
197
|
message_repository (MessageRepository): the message repository used to emit logs on HTTP requests and control message on config update
|
193
198
|
"""
|
194
|
-
self.
|
195
|
-
|
196
|
-
|
197
|
-
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
|
198
202
|
)
|
199
|
-
self._client_secret = (
|
200
|
-
client_secret
|
201
|
-
if client_secret is not None
|
202
|
-
else dpath.get(
|
203
|
-
connector_config, # type: ignore [arg-type]
|
204
|
-
("credentials", "client_secret"),
|
205
|
-
)
|
203
|
+
self._client_secret: str = self._get_config_value_by_path(
|
204
|
+
("credentials", "client_secret"), client_secret
|
206
205
|
)
|
207
206
|
self._client_id_name = client_id_name
|
208
207
|
self._client_secret_name = client_secret_name
|
@@ -217,9 +216,9 @@ class SingleUseRefreshTokenOauth2Authenticator(Oauth2Authenticator):
|
|
217
216
|
super().__init__(
|
218
217
|
token_refresh_endpoint=token_refresh_endpoint,
|
219
218
|
client_id_name=self._client_id_name,
|
220
|
-
client_id=self.
|
219
|
+
client_id=self._client_id,
|
221
220
|
client_secret_name=self._client_secret_name,
|
222
|
-
client_secret=self.
|
221
|
+
client_secret=self._client_secret,
|
223
222
|
refresh_token=self.get_refresh_token(),
|
224
223
|
refresh_token_name=self._refresh_token_name,
|
225
224
|
scopes=scopes,
|
@@ -237,76 +236,105 @@ class SingleUseRefreshTokenOauth2Authenticator(Oauth2Authenticator):
|
|
237
236
|
refresh_token_error_values=refresh_token_error_values,
|
238
237
|
)
|
239
238
|
|
240
|
-
def get_refresh_token_name(self) -> str:
|
241
|
-
return self._refresh_token_name
|
242
|
-
|
243
|
-
def get_client_id(self) -> str:
|
244
|
-
return self._client_id
|
245
|
-
|
246
|
-
def get_client_secret(self) -> str:
|
247
|
-
return self._client_secret
|
248
|
-
|
249
239
|
@property
|
250
240
|
def access_token(self) -> str:
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
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]
|
256
248
|
|
257
249
|
@access_token.setter
|
258
250
|
def access_token(self, new_access_token: str) -> None:
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
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)
|
264
258
|
|
265
259
|
def get_refresh_token(self) -> str:
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
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]
|
271
270
|
|
272
271
|
def set_refresh_token(self, new_refresh_token: str) -> None:
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
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)
|
279
|
+
|
280
|
+
def get_token_expiry_date(self) -> AirbyteDateTime:
|
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.
|
278
287
|
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
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)
|
295
|
+
result = (
|
296
|
+
ab_datetime_now() - timedelta(days=1)
|
297
|
+
if expiry_date == ""
|
298
|
+
else ab_datetime_parse(str(expiry_date))
|
284
299
|
)
|
285
|
-
|
300
|
+
if isinstance(result, AirbyteDateTime):
|
301
|
+
return result
|
302
|
+
raise TypeError("Invalid datetime conversion")
|
286
303
|
|
287
|
-
def set_token_expiry_date( # type: ignore[override]
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
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)
|
295
313
|
)
|
296
314
|
|
297
315
|
def token_has_expired(self) -> bool:
|
298
316
|
"""Returns True if the token is expired"""
|
299
|
-
return
|
317
|
+
return ab_datetime_now() > self.get_token_expiry_date()
|
300
318
|
|
301
319
|
@staticmethod
|
302
320
|
def get_new_token_expiry_date(
|
303
321
|
access_token_expires_in: str,
|
304
322
|
token_expiry_date_format: str | None = None,
|
305
|
-
) ->
|
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
|
+
"""
|
306
334
|
if token_expiry_date_format:
|
307
|
-
return
|
335
|
+
return ab_datetime_parse(access_token_expires_in)
|
308
336
|
else:
|
309
|
-
return
|
337
|
+
return ab_datetime_now() + timedelta(seconds=int(access_token_expires_in))
|
310
338
|
|
311
339
|
def get_access_token(self) -> str:
|
312
340
|
"""Retrieve new access and refresh token if the access token has expired.
|
@@ -318,33 +346,88 @@ class SingleUseRefreshTokenOauth2Authenticator(Oauth2Authenticator):
|
|
318
346
|
new_access_token, access_token_expires_in, new_refresh_token = (
|
319
347
|
self.refresh_access_token()
|
320
348
|
)
|
321
|
-
new_token_expiry_date:
|
349
|
+
new_token_expiry_date: AirbyteDateTime = self.get_new_token_expiry_date(
|
322
350
|
access_token_expires_in, self._token_expiry_date_format
|
323
351
|
)
|
324
352
|
self.access_token = new_access_token
|
325
353
|
self.set_refresh_token(new_refresh_token)
|
326
354
|
self.set_token_expiry_date(new_token_expiry_date)
|
327
|
-
|
328
|
-
# Usually, a class shouldn't care about the implementation details but to keep backward compatibility where we print the
|
329
|
-
# message directly in the console, this is needed
|
330
|
-
if not isinstance(self._message_repository, NoopMessageRepository):
|
331
|
-
self._message_repository.emit_message(
|
332
|
-
create_connector_config_control_message(self._connector_config) # type: ignore [arg-type]
|
333
|
-
)
|
334
|
-
else:
|
335
|
-
emit_configuration_as_airbyte_control_message(self._connector_config) # type: ignore [arg-type]
|
355
|
+
self._emit_control_message()
|
336
356
|
return self.access_token
|
337
357
|
|
338
|
-
def refresh_access_token( # type: ignore[override]
|
339
|
-
|
340
|
-
|
341
|
-
|
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()
|
342
366
|
return (
|
343
|
-
|
344
|
-
|
345
|
-
|
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 "",
|
346
403
|
)
|
347
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
|
+
|
348
431
|
@property
|
349
432
|
def _message_repository(self) -> MessageRepository:
|
350
433
|
"""
|
airbyte_cdk/sources/types.py
CHANGED
@@ -6,7 +6,7 @@ from __future__ import annotations
|
|
6
6
|
|
7
7
|
from typing import Any, ItemsView, Iterator, KeysView, List, Mapping, Optional, ValuesView
|
8
8
|
|
9
|
-
import
|
9
|
+
from airbyte_cdk.utils.slice_hasher import SliceHasher
|
10
10
|
|
11
11
|
# A FieldPointer designates a path to a field inside a mapping. For example, retrieving ["k1", "k1.2"] in the object {"k1" :{"k1.2":
|
12
12
|
# "hello"}] returns "hello"
|
@@ -151,7 +151,9 @@ class StreamSlice(Mapping[str, Any]):
|
|
151
151
|
return self._stream_slice
|
152
152
|
|
153
153
|
def __hash__(self) -> int:
|
154
|
-
return hash(
|
154
|
+
return SliceHasher.hash(
|
155
|
+
stream_slice=self._stream_slice
|
156
|
+
) # no need to provide stream_name here as this is used for slicing the cursor
|
155
157
|
|
156
158
|
def __bool__(self) -> bool:
|
157
159
|
return bool(self._stream_slice) or bool(self._extra_fields)
|
@@ -3,7 +3,6 @@
|
|
3
3
|
#
|
4
4
|
|
5
5
|
import logging
|
6
|
-
from distutils.util import strtobool
|
7
6
|
from enum import Flag, auto
|
8
7
|
from typing import Any, Callable, Dict, Generator, Mapping, Optional, cast
|
9
8
|
|
@@ -22,6 +21,28 @@ python_to_json = {v: k for k, v in json_to_python.items()}
|
|
22
21
|
|
23
22
|
logger = logging.getLogger("airbyte")
|
24
23
|
|
24
|
+
_TRUTHY_STRINGS = ("y", "yes", "t", "true", "on", "1")
|
25
|
+
_FALSEY_STRINGS = ("n", "no", "f", "false", "off", "0")
|
26
|
+
|
27
|
+
|
28
|
+
def _strtobool(value: str, /) -> int:
|
29
|
+
"""Mimic the behavior of distutils.util.strtobool.
|
30
|
+
|
31
|
+
From: https://docs.python.org/2/distutils/apiref.html#distutils.util.strtobool
|
32
|
+
|
33
|
+
> Convert a string representation of truth to true (1) or false (0).
|
34
|
+
> True values are y, yes, t, true, on and 1; false values are n, no, f, false, off and 0. Raises
|
35
|
+
> `ValueError` if val is anything else.
|
36
|
+
"""
|
37
|
+
normalized_str = value.lower().strip()
|
38
|
+
if normalized_str in _TRUTHY_STRINGS:
|
39
|
+
return 1
|
40
|
+
|
41
|
+
if normalized_str in _FALSEY_STRINGS:
|
42
|
+
return 0
|
43
|
+
|
44
|
+
raise ValueError(f"Invalid boolean value: {normalized_str}")
|
45
|
+
|
25
46
|
|
26
47
|
class TransformConfig(Flag):
|
27
48
|
"""
|
@@ -129,7 +150,7 @@ class TypeTransformer:
|
|
129
150
|
return int(original_item)
|
130
151
|
elif target_type == "boolean":
|
131
152
|
if isinstance(original_item, str):
|
132
|
-
return
|
153
|
+
return _strtobool(original_item) == 1
|
133
154
|
return bool(original_item)
|
134
155
|
elif target_type == "array":
|
135
156
|
item_types = set(subschema.get("items", {}).get("type", set()))
|
@@ -4,7 +4,6 @@
|
|
4
4
|
import importlib.util
|
5
5
|
from pathlib import Path
|
6
6
|
from types import ModuleType
|
7
|
-
from typing import Optional
|
8
7
|
|
9
8
|
import pytest
|
10
9
|
|
@@ -30,7 +29,7 @@ def connector_dir(request: pytest.FixtureRequest) -> Path:
|
|
30
29
|
|
31
30
|
|
32
31
|
@pytest.fixture(scope="session")
|
33
|
-
def components_module(connector_dir: Path) ->
|
32
|
+
def components_module(connector_dir: Path) -> ModuleType | None:
|
34
33
|
"""Load and return the components module from the connector directory.
|
35
34
|
|
36
35
|
This assumes the components module is located at <connector_dir>/components.py.
|