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.
Files changed (47) hide show
  1. airbyte_cdk/cli/source_declarative_manifest/_run.py +9 -3
  2. airbyte_cdk/connector_builder/connector_builder_handler.py +3 -2
  3. airbyte_cdk/sources/declarative/async_job/job_orchestrator.py +7 -7
  4. airbyte_cdk/sources/declarative/auth/jwt.py +17 -11
  5. airbyte_cdk/sources/declarative/auth/oauth.py +89 -23
  6. airbyte_cdk/sources/declarative/auth/token_provider.py +4 -5
  7. airbyte_cdk/sources/declarative/checks/check_dynamic_stream.py +19 -9
  8. airbyte_cdk/sources/declarative/concurrent_declarative_source.py +145 -43
  9. airbyte_cdk/sources/declarative/declarative_component_schema.yaml +51 -2
  10. airbyte_cdk/sources/declarative/declarative_stream.py +3 -1
  11. airbyte_cdk/sources/declarative/extractors/record_filter.py +3 -5
  12. airbyte_cdk/sources/declarative/incremental/__init__.py +6 -0
  13. airbyte_cdk/sources/declarative/incremental/concurrent_partition_cursor.py +400 -0
  14. airbyte_cdk/sources/declarative/incremental/global_substream_cursor.py +3 -0
  15. airbyte_cdk/sources/declarative/incremental/per_partition_cursor.py +35 -3
  16. airbyte_cdk/sources/declarative/manifest_declarative_source.py +20 -7
  17. airbyte_cdk/sources/declarative/models/declarative_component_schema.py +41 -5
  18. airbyte_cdk/sources/declarative/parsers/custom_code_compiler.py +143 -0
  19. airbyte_cdk/sources/declarative/parsers/model_to_component_factory.py +313 -30
  20. airbyte_cdk/sources/declarative/partition_routers/async_job_partition_router.py +5 -5
  21. airbyte_cdk/sources/declarative/partition_routers/substream_partition_router.py +46 -12
  22. airbyte_cdk/sources/declarative/requesters/error_handlers/composite_error_handler.py +22 -0
  23. airbyte_cdk/sources/declarative/requesters/error_handlers/http_response_filter.py +4 -4
  24. airbyte_cdk/sources/declarative/retrievers/async_retriever.py +6 -12
  25. airbyte_cdk/sources/declarative/retrievers/simple_retriever.py +1 -1
  26. airbyte_cdk/sources/declarative/schema/__init__.py +2 -0
  27. airbyte_cdk/sources/declarative/schema/dynamic_schema_loader.py +44 -5
  28. airbyte_cdk/sources/http_logger.py +1 -1
  29. airbyte_cdk/sources/streams/concurrent/clamping.py +99 -0
  30. airbyte_cdk/sources/streams/concurrent/cursor.py +51 -57
  31. airbyte_cdk/sources/streams/concurrent/cursor_types.py +32 -0
  32. airbyte_cdk/sources/streams/concurrent/state_converters/datetime_stream_state_converter.py +22 -13
  33. airbyte_cdk/sources/streams/core.py +6 -6
  34. airbyte_cdk/sources/streams/http/http.py +1 -2
  35. airbyte_cdk/sources/streams/http/requests_native_auth/abstract_oauth.py +231 -62
  36. airbyte_cdk/sources/streams/http/requests_native_auth/oauth.py +171 -88
  37. airbyte_cdk/sources/types.py +4 -2
  38. airbyte_cdk/sources/utils/transform.py +23 -2
  39. airbyte_cdk/test/utils/manifest_only_fixtures.py +1 -2
  40. airbyte_cdk/utils/datetime_helpers.py +499 -0
  41. airbyte_cdk/utils/slice_hasher.py +8 -1
  42. airbyte_cdk-6.33.0.dist-info/LICENSE_SHORT +1 -0
  43. {airbyte_cdk-6.31.2.dev0.dist-info → airbyte_cdk-6.33.0.dist-info}/METADATA +6 -6
  44. {airbyte_cdk-6.31.2.dev0.dist-info → airbyte_cdk-6.33.0.dist-info}/RECORD +47 -41
  45. {airbyte_cdk-6.31.2.dev0.dist-info → airbyte_cdk-6.33.0.dist-info}/WHEEL +1 -1
  46. {airbyte_cdk-6.31.2.dev0.dist-info → airbyte_cdk-6.33.0.dist-info}/LICENSE.txt +0 -0
  47. {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: pendulum.DateTime | None = None,
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 pendulum.now().subtract(days=1) # type: ignore [no-untyped-call]
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 [return-value]
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 [return-value]
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 [return-value]
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) -> pendulum.DateTime:
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 [return-value]
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 [assignment] # Incorrect type for assignment
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._client_id = (
195
- client_id # type: ignore [assignment] # Incorrect type for assignment
196
- if client_id is not None
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 # type: ignore [assignment] # Incorrect type for assignment
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.get_client_id(),
219
+ client_id=self._client_id,
221
220
  client_secret_name=self._client_secret_name,
222
- client_secret=self.get_client_secret(),
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
- return dpath.get( # type: ignore [return-value]
252
- self._connector_config, # type: ignore [arg-type]
253
- self._access_token_config_path,
254
- default="",
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
- dpath.new(
260
- self._connector_config, # type: ignore [arg-type]
261
- self._access_token_config_path,
262
- new_access_token,
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
- return dpath.get( # type: ignore [return-value]
267
- self._connector_config, # type: ignore [arg-type]
268
- self._refresh_token_config_path,
269
- default="",
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
- dpath.new(
274
- self._connector_config, # type: ignore [arg-type]
275
- self._refresh_token_config_path,
276
- new_refresh_token,
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
- def get_token_expiry_date(self) -> pendulum.DateTime:
280
- expiry_date = dpath.get(
281
- self._connector_config, # type: ignore [arg-type]
282
- self._token_expiry_date_config_path,
283
- default="",
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
- return pendulum.now().subtract(days=1) if expiry_date == "" else pendulum.parse(expiry_date) # type: ignore [arg-type, return-value, no-untyped-call]
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
- self,
289
- new_token_expiry_date: pendulum.DateTime,
290
- ) -> None:
291
- dpath.new(
292
- self._connector_config, # type: ignore [arg-type]
293
- self._token_expiry_date_config_path,
294
- str(new_token_expiry_date),
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 pendulum.now("UTC") > self.get_token_expiry_date()
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
- ) -> pendulum.DateTime:
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 pendulum.from_format(access_token_expires_in, token_expiry_date_format)
335
+ return ab_datetime_parse(access_token_expires_in)
308
336
  else:
309
- return pendulum.now("UTC").add(seconds=int(access_token_expires_in))
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: pendulum.DateTime = self.get_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
- # FIXME emit_configuration_as_airbyte_control_message as been deprecated in favor of package airbyte_cdk.sources.message
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] # Signature doesn't match base class
339
- self,
340
- ) -> Tuple[str, str, str]:
341
- response_json = self._get_refresh_access_token_response()
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
- response_json[self.get_access_token_name()],
344
- response_json[self.get_expires_in_name()],
345
- response_json[self.get_refresh_token_name()],
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
  """
@@ -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 orjson
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(orjson.dumps(self._stream_slice, option=orjson.OPT_SORT_KEYS))
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 strtobool(original_item) == 1
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) -> Optional[ModuleType]:
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.