airbyte-cdk 6.21.1.dev0__py3-none-any.whl → 6.26.0.dev4103__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 (39) hide show
  1. airbyte_cdk/cli/source_declarative_manifest/_run.py +6 -0
  2. airbyte_cdk/connector_builder/connector_builder_handler.py +1 -0
  3. airbyte_cdk/sources/declarative/auth/oauth.py +68 -11
  4. airbyte_cdk/sources/declarative/concurrent_declarative_source.py +81 -16
  5. airbyte_cdk/sources/declarative/declarative_component_schema.yaml +58 -2
  6. airbyte_cdk/sources/declarative/decoders/__init__.py +9 -1
  7. airbyte_cdk/sources/declarative/decoders/zipfile_decoder.py +59 -0
  8. airbyte_cdk/sources/declarative/extractors/record_filter.py +3 -5
  9. airbyte_cdk/sources/declarative/incremental/__init__.py +6 -0
  10. airbyte_cdk/sources/declarative/incremental/concurrent_partition_cursor.py +334 -0
  11. airbyte_cdk/sources/declarative/incremental/global_substream_cursor.py +3 -0
  12. airbyte_cdk/sources/declarative/incremental/per_partition_cursor.py +35 -3
  13. airbyte_cdk/sources/declarative/manifest_declarative_source.py +15 -4
  14. airbyte_cdk/sources/declarative/models/declarative_component_schema.py +50 -14
  15. airbyte_cdk/sources/declarative/parsers/custom_code_compiler.py +143 -0
  16. airbyte_cdk/sources/declarative/parsers/model_to_component_factory.py +220 -22
  17. airbyte_cdk/sources/declarative/partition_routers/substream_partition_router.py +6 -2
  18. airbyte_cdk/sources/declarative/requesters/error_handlers/composite_error_handler.py +22 -0
  19. airbyte_cdk/sources/declarative/retrievers/simple_retriever.py +1 -1
  20. airbyte_cdk/sources/file_based/config/abstract_file_based_spec.py +15 -0
  21. airbyte_cdk/sources/file_based/config/identities_based_stream_config.py +8 -0
  22. airbyte_cdk/sources/file_based/config/permissions.py +34 -0
  23. airbyte_cdk/sources/file_based/file_based_source.py +65 -1
  24. airbyte_cdk/sources/file_based/file_based_stream_reader.py +33 -0
  25. airbyte_cdk/sources/file_based/schema_helpers.py +25 -0
  26. airbyte_cdk/sources/file_based/stream/__init__.py +2 -1
  27. airbyte_cdk/sources/file_based/stream/default_file_based_stream.py +29 -0
  28. airbyte_cdk/sources/file_based/stream/identities_stream.py +99 -0
  29. airbyte_cdk/sources/http_logger.py +1 -1
  30. airbyte_cdk/sources/streams/concurrent/clamping.py +99 -0
  31. airbyte_cdk/sources/streams/concurrent/cursor.py +51 -57
  32. airbyte_cdk/sources/streams/concurrent/cursor_types.py +32 -0
  33. airbyte_cdk/sources/streams/http/requests_native_auth/oauth.py +20 -20
  34. airbyte_cdk/test/utils/manifest_only_fixtures.py +1 -2
  35. {airbyte_cdk-6.21.1.dev0.dist-info → airbyte_cdk-6.26.0.dev4103.dist-info}/METADATA +3 -3
  36. {airbyte_cdk-6.21.1.dev0.dist-info → airbyte_cdk-6.26.0.dev4103.dist-info}/RECORD +39 -31
  37. {airbyte_cdk-6.21.1.dev0.dist-info → airbyte_cdk-6.26.0.dev4103.dist-info}/LICENSE.txt +0 -0
  38. {airbyte_cdk-6.21.1.dev0.dist-info → airbyte_cdk-6.26.0.dev4103.dist-info}/WHEEL +0 -0
  39. {airbyte_cdk-6.21.1.dev0.dist-info → airbyte_cdk-6.26.0.dev4103.dist-info}/entry_points.txt +0 -0
@@ -13,7 +13,6 @@ from typing import (
13
13
  Mapping,
14
14
  MutableMapping,
15
15
  Optional,
16
- Protocol,
17
16
  Tuple,
18
17
  Union,
19
18
  )
@@ -21,6 +20,8 @@ from typing import (
21
20
  from airbyte_cdk.sources.connector_state_manager import ConnectorStateManager
22
21
  from airbyte_cdk.sources.message import MessageRepository
23
22
  from airbyte_cdk.sources.streams import NO_CURSOR_STATE_KEY
23
+ from airbyte_cdk.sources.streams.concurrent.clamping import ClampingStrategy, NoClamping
24
+ from airbyte_cdk.sources.streams.concurrent.cursor_types import CursorValueType, GapType
24
25
  from airbyte_cdk.sources.streams.concurrent.partitions.partition import Partition
25
26
  from airbyte_cdk.sources.streams.concurrent.partitions.stream_slicer import StreamSlicer
26
27
  from airbyte_cdk.sources.streams.concurrent.state_converters.abstract_stream_state_converter import (
@@ -35,36 +36,6 @@ def _extract_value(mapping: Mapping[str, Any], path: List[str]) -> Any:
35
36
  return functools.reduce(lambda a, b: a[b], path, mapping)
36
37
 
37
38
 
38
- class GapType(Protocol):
39
- """
40
- This is the representation of gaps between two cursor values. Examples:
41
- * if cursor values are datetimes, GapType is timedelta
42
- * if cursor values are integer, GapType will also be integer
43
- """
44
-
45
- pass
46
-
47
-
48
- class CursorValueType(Protocol):
49
- """Protocol for annotating comparable types."""
50
-
51
- @abstractmethod
52
- def __lt__(self: "CursorValueType", other: "CursorValueType") -> bool:
53
- pass
54
-
55
- @abstractmethod
56
- def __ge__(self: "CursorValueType", other: "CursorValueType") -> bool:
57
- pass
58
-
59
- @abstractmethod
60
- def __add__(self: "CursorValueType", other: GapType) -> "CursorValueType":
61
- pass
62
-
63
- @abstractmethod
64
- def __sub__(self: "CursorValueType", other: GapType) -> "CursorValueType":
65
- pass
66
-
67
-
68
39
  class CursorField:
69
40
  def __init__(self, cursor_field_key: str) -> None:
70
41
  self.cursor_field_key = cursor_field_key
@@ -172,6 +143,7 @@ class ConcurrentCursor(Cursor):
172
143
  lookback_window: Optional[GapType] = None,
173
144
  slice_range: Optional[GapType] = None,
174
145
  cursor_granularity: Optional[GapType] = None,
146
+ clamping_strategy: ClampingStrategy = NoClamping(),
175
147
  ) -> None:
176
148
  self._stream_name = stream_name
177
149
  self._stream_namespace = stream_namespace
@@ -193,10 +165,13 @@ class ConcurrentCursor(Cursor):
193
165
  self._cursor_granularity = cursor_granularity
194
166
  # Flag to track if the logger has been triggered (per stream)
195
167
  self._should_be_synced_logger_triggered = False
168
+ self._clamping_strategy = clamping_strategy
196
169
 
197
170
  @property
198
171
  def state(self) -> MutableMapping[str, Any]:
199
- return self._concurrent_state
172
+ return self._connector_state_converter.convert_to_state_message(
173
+ self.cursor_field, self._concurrent_state
174
+ )
200
175
 
201
176
  @property
202
177
  def cursor_field(self) -> CursorField:
@@ -241,10 +216,10 @@ class ConcurrentCursor(Cursor):
241
216
  return self._connector_state_converter.parse_value(self._cursor_field.extract_value(record))
242
217
 
243
218
  def close_partition(self, partition: Partition) -> None:
244
- slice_count_before = len(self.state.get("slices", []))
219
+ slice_count_before = len(self._concurrent_state.get("slices", []))
245
220
  self._add_slice_to_state(partition)
246
221
  if slice_count_before < len(
247
- self.state["slices"]
222
+ self._concurrent_state["slices"]
248
223
  ): # only emit if at least one slice has been processed
249
224
  self._merge_partitions()
250
225
  self._emit_state_message()
@@ -256,11 +231,11 @@ class ConcurrentCursor(Cursor):
256
231
  )
257
232
 
258
233
  if self._slice_boundary_fields:
259
- if "slices" not in self.state:
234
+ if "slices" not in self._concurrent_state:
260
235
  raise RuntimeError(
261
236
  f"The state for stream {self._stream_name} should have at least one slice to delineate the sync start time, but no slices are present. This is unexpected. Please contact Support."
262
237
  )
263
- self.state["slices"].append(
238
+ self._concurrent_state["slices"].append(
264
239
  {
265
240
  self._connector_state_converter.START_KEY: self._extract_from_slice(
266
241
  partition, self._slice_boundary_fields[self._START_BOUNDARY]
@@ -288,7 +263,7 @@ class ConcurrentCursor(Cursor):
288
263
  "expected. Please contact the Airbyte team."
289
264
  )
290
265
 
291
- self.state["slices"].append(
266
+ self._concurrent_state["slices"].append(
292
267
  {
293
268
  self._connector_state_converter.START_KEY: self.start,
294
269
  self._connector_state_converter.END_KEY: most_recent_cursor_value,
@@ -300,9 +275,7 @@ class ConcurrentCursor(Cursor):
300
275
  self._connector_state_manager.update_state_for_stream(
301
276
  self._stream_name,
302
277
  self._stream_namespace,
303
- self._connector_state_converter.convert_to_state_message(
304
- self._cursor_field, self.state
305
- ),
278
+ self.state,
306
279
  )
307
280
  state_message = self._connector_state_manager.create_state_message(
308
281
  self._stream_name, self._stream_namespace
@@ -310,7 +283,9 @@ class ConcurrentCursor(Cursor):
310
283
  self._message_repository.emit_message(state_message)
311
284
 
312
285
  def _merge_partitions(self) -> None:
313
- self.state["slices"] = self._connector_state_converter.merge_intervals(self.state["slices"])
286
+ self._concurrent_state["slices"] = self._connector_state_converter.merge_intervals(
287
+ self._concurrent_state["slices"]
288
+ )
314
289
 
315
290
  def _extract_from_slice(self, partition: Partition, key: str) -> CursorValueType:
316
291
  try:
@@ -347,36 +322,42 @@ class ConcurrentCursor(Cursor):
347
322
  if self._start is not None and self._is_start_before_first_slice():
348
323
  yield from self._split_per_slice_range(
349
324
  self._start,
350
- self.state["slices"][0][self._connector_state_converter.START_KEY],
325
+ self._concurrent_state["slices"][0][self._connector_state_converter.START_KEY],
351
326
  False,
352
327
  )
353
328
 
354
- if len(self.state["slices"]) == 1:
329
+ if len(self._concurrent_state["slices"]) == 1:
355
330
  yield from self._split_per_slice_range(
356
331
  self._calculate_lower_boundary_of_last_slice(
357
- self.state["slices"][0][self._connector_state_converter.END_KEY]
332
+ self._concurrent_state["slices"][0][self._connector_state_converter.END_KEY]
358
333
  ),
359
334
  self._end_provider(),
360
335
  True,
361
336
  )
362
- elif len(self.state["slices"]) > 1:
363
- for i in range(len(self.state["slices"]) - 1):
337
+ elif len(self._concurrent_state["slices"]) > 1:
338
+ for i in range(len(self._concurrent_state["slices"]) - 1):
364
339
  if self._cursor_granularity:
365
340
  yield from self._split_per_slice_range(
366
- self.state["slices"][i][self._connector_state_converter.END_KEY]
341
+ self._concurrent_state["slices"][i][self._connector_state_converter.END_KEY]
367
342
  + self._cursor_granularity,
368
- self.state["slices"][i + 1][self._connector_state_converter.START_KEY],
343
+ self._concurrent_state["slices"][i + 1][
344
+ self._connector_state_converter.START_KEY
345
+ ],
369
346
  False,
370
347
  )
371
348
  else:
372
349
  yield from self._split_per_slice_range(
373
- self.state["slices"][i][self._connector_state_converter.END_KEY],
374
- self.state["slices"][i + 1][self._connector_state_converter.START_KEY],
350
+ self._concurrent_state["slices"][i][
351
+ self._connector_state_converter.END_KEY
352
+ ],
353
+ self._concurrent_state["slices"][i + 1][
354
+ self._connector_state_converter.START_KEY
355
+ ],
375
356
  False,
376
357
  )
377
358
  yield from self._split_per_slice_range(
378
359
  self._calculate_lower_boundary_of_last_slice(
379
- self.state["slices"][-1][self._connector_state_converter.END_KEY]
360
+ self._concurrent_state["slices"][-1][self._connector_state_converter.END_KEY]
380
361
  ),
381
362
  self._end_provider(),
382
363
  True,
@@ -387,7 +368,8 @@ class ConcurrentCursor(Cursor):
387
368
  def _is_start_before_first_slice(self) -> bool:
388
369
  return (
389
370
  self._start is not None
390
- and self._start < self.state["slices"][0][self._connector_state_converter.START_KEY]
371
+ and self._start
372
+ < self._concurrent_state["slices"][0][self._connector_state_converter.START_KEY]
391
373
  )
392
374
 
393
375
  def _calculate_lower_boundary_of_last_slice(
@@ -408,10 +390,12 @@ class ConcurrentCursor(Cursor):
408
390
 
409
391
  lower = max(lower, self._start) if self._start else lower
410
392
  if not self._slice_range or self._evaluate_upper_safely(lower, self._slice_range) >= upper:
393
+ clamped_lower = self._clamping_strategy.clamp(lower)
394
+ clamped_upper = self._clamping_strategy.clamp(upper)
411
395
  start_value, end_value = (
412
- (lower, upper - self._cursor_granularity)
396
+ (clamped_lower, clamped_upper - self._cursor_granularity)
413
397
  if self._cursor_granularity and not upper_is_end
414
- else (lower, upper)
398
+ else (clamped_lower, clamped_upper)
415
399
  )
416
400
  yield StreamSlice(
417
401
  partition={},
@@ -433,11 +417,21 @@ class ConcurrentCursor(Cursor):
433
417
  )
434
418
  has_reached_upper_boundary = current_upper_boundary >= upper
435
419
 
420
+ clamped_upper = (
421
+ self._clamping_strategy.clamp(current_upper_boundary)
422
+ if current_upper_boundary != upper
423
+ else current_upper_boundary
424
+ )
425
+ clamped_lower = self._clamping_strategy.clamp(current_lower_boundary)
426
+ if clamped_lower >= clamped_upper:
427
+ # clamping collapsed both values which means that it is time to stop processing
428
+ # FIXME should this be replace by proper end_provider
429
+ break
436
430
  start_value, end_value = (
437
- (current_lower_boundary, current_upper_boundary - self._cursor_granularity)
431
+ (clamped_lower, clamped_upper - self._cursor_granularity)
438
432
  if self._cursor_granularity
439
433
  and (not upper_is_end or not has_reached_upper_boundary)
440
- else (current_lower_boundary, current_upper_boundary)
434
+ else (clamped_lower, clamped_upper)
441
435
  )
442
436
  yield StreamSlice(
443
437
  partition={},
@@ -450,7 +444,7 @@ class ConcurrentCursor(Cursor):
450
444
  ]: self._connector_state_converter.output_format(end_value),
451
445
  },
452
446
  )
453
- current_lower_boundary = current_upper_boundary
447
+ current_lower_boundary = clamped_upper
454
448
  if current_upper_boundary >= upper:
455
449
  stop_processing = True
456
450
 
@@ -0,0 +1,32 @@
1
+ from abc import abstractmethod
2
+ from typing import Protocol
3
+
4
+
5
+ class GapType(Protocol):
6
+ """
7
+ This is the representation of gaps between two cursor values. Examples:
8
+ * if cursor values are datetimes, GapType is timedelta
9
+ * if cursor values are integer, GapType will also be integer
10
+ """
11
+
12
+ pass
13
+
14
+
15
+ class CursorValueType(Protocol):
16
+ """Protocol for annotating comparable types."""
17
+
18
+ @abstractmethod
19
+ def __lt__(self: "CursorValueType", other: "CursorValueType") -> bool:
20
+ pass
21
+
22
+ @abstractmethod
23
+ def __ge__(self: "CursorValueType", other: "CursorValueType") -> bool:
24
+ pass
25
+
26
+ @abstractmethod
27
+ def __add__(self: "CursorValueType", other: GapType) -> "CursorValueType":
28
+ pass
29
+
30
+ @abstractmethod
31
+ def __sub__(self: "CursorValueType", other: GapType) -> "CursorValueType":
32
+ pass
@@ -95,16 +95,16 @@ class Oauth2Authenticator(AbstractOauth2Authenticator):
95
95
  return self._access_token_name
96
96
 
97
97
  def get_scopes(self) -> list[str]:
98
- return self._scopes # type: ignore [return-value]
98
+ return self._scopes # type: ignore[return-value]
99
99
 
100
100
  def get_expires_in_name(self) -> str:
101
101
  return self._expires_in_name
102
102
 
103
103
  def get_refresh_request_body(self) -> Mapping[str, Any]:
104
- return self._refresh_request_body # type: ignore [return-value]
104
+ return self._refresh_request_body # type: ignore[return-value]
105
105
 
106
106
  def get_refresh_request_headers(self) -> Mapping[str, Any]:
107
- return self._refresh_request_headers # type: ignore [return-value]
107
+ return self._refresh_request_headers # type: ignore[return-value]
108
108
 
109
109
  def get_grant_type_name(self) -> str:
110
110
  return self._grant_type_name
@@ -128,11 +128,11 @@ class Oauth2Authenticator(AbstractOauth2Authenticator):
128
128
 
129
129
  @property
130
130
  def access_token(self) -> str:
131
- return self._access_token # type: ignore [return-value]
131
+ return self._access_token # type: ignore[return-value]
132
132
 
133
133
  @access_token.setter
134
134
  def access_token(self, value: str) -> None:
135
- self._access_token = value # type: ignore [assignment] # Incorrect type for assignment
135
+ self._access_token = value # type: ignore[assignment] # Incorrect type for assignment
136
136
 
137
137
 
138
138
  class SingleUseRefreshTokenOauth2Authenticator(Oauth2Authenticator):
@@ -192,15 +192,15 @@ class SingleUseRefreshTokenOauth2Authenticator(Oauth2Authenticator):
192
192
  message_repository (MessageRepository): the message repository used to emit logs on HTTP requests and control message on config update
193
193
  """
194
194
  self._client_id = (
195
- client_id # type: ignore [assignment] # Incorrect type for assignment
195
+ client_id # type: ignore[assignment] # Incorrect type for assignment
196
196
  if client_id is not None
197
- else dpath.get(connector_config, ("credentials", "client_id")) # type: ignore [arg-type]
197
+ else dpath.get(connector_config, ("credentials", "client_id")) # type: ignore[arg-type]
198
198
  )
199
199
  self._client_secret = (
200
- client_secret # type: ignore [assignment] # Incorrect type for assignment
200
+ client_secret # type: ignore[assignment] # Incorrect type for assignment
201
201
  if client_secret is not None
202
202
  else dpath.get(
203
- connector_config, # type: ignore [arg-type]
203
+ connector_config, # type: ignore[arg-type]
204
204
  ("credentials", "client_secret"),
205
205
  )
206
206
  )
@@ -248,8 +248,8 @@ class SingleUseRefreshTokenOauth2Authenticator(Oauth2Authenticator):
248
248
 
249
249
  @property
250
250
  def access_token(self) -> str:
251
- return dpath.get( # type: ignore [return-value]
252
- self._connector_config, # type: ignore [arg-type]
251
+ return dpath.get( # type: ignore[return-value]
252
+ self._connector_config, # type: ignore[arg-type]
253
253
  self._access_token_config_path,
254
254
  default="",
255
255
  )
@@ -257,39 +257,39 @@ class SingleUseRefreshTokenOauth2Authenticator(Oauth2Authenticator):
257
257
  @access_token.setter
258
258
  def access_token(self, new_access_token: str) -> None:
259
259
  dpath.new(
260
- self._connector_config, # type: ignore [arg-type]
260
+ self._connector_config, # type: ignore[arg-type]
261
261
  self._access_token_config_path,
262
262
  new_access_token,
263
263
  )
264
264
 
265
265
  def get_refresh_token(self) -> str:
266
- return dpath.get( # type: ignore [return-value]
267
- self._connector_config, # type: ignore [arg-type]
266
+ return dpath.get( # type: ignore[return-value]
267
+ self._connector_config, # type: ignore[arg-type]
268
268
  self._refresh_token_config_path,
269
269
  default="",
270
270
  )
271
271
 
272
272
  def set_refresh_token(self, new_refresh_token: str) -> None:
273
273
  dpath.new(
274
- self._connector_config, # type: ignore [arg-type]
274
+ self._connector_config, # type: ignore[arg-type]
275
275
  self._refresh_token_config_path,
276
276
  new_refresh_token,
277
277
  )
278
278
 
279
279
  def get_token_expiry_date(self) -> pendulum.DateTime:
280
280
  expiry_date = dpath.get(
281
- self._connector_config, # type: ignore [arg-type]
281
+ self._connector_config, # type: ignore[arg-type]
282
282
  self._token_expiry_date_config_path,
283
283
  default="",
284
284
  )
285
- return pendulum.now().subtract(days=1) if expiry_date == "" else pendulum.parse(expiry_date) # type: ignore [arg-type, return-value, no-untyped-call]
285
+ return pendulum.now().subtract(days=1) if expiry_date == "" else pendulum.parse(expiry_date) # type: ignore[arg-type, return-value, no-untyped-call]
286
286
 
287
287
  def set_token_expiry_date( # type: ignore[override]
288
288
  self,
289
289
  new_token_expiry_date: pendulum.DateTime,
290
290
  ) -> None:
291
291
  dpath.new(
292
- self._connector_config, # type: ignore [arg-type]
292
+ self._connector_config, # type: ignore[arg-type]
293
293
  self._token_expiry_date_config_path,
294
294
  str(new_token_expiry_date),
295
295
  )
@@ -329,10 +329,10 @@ class SingleUseRefreshTokenOauth2Authenticator(Oauth2Authenticator):
329
329
  # message directly in the console, this is needed
330
330
  if not isinstance(self._message_repository, NoopMessageRepository):
331
331
  self._message_repository.emit_message(
332
- create_connector_config_control_message(self._connector_config) # type: ignore [arg-type]
332
+ create_connector_config_control_message(self._connector_config) # type: ignore[arg-type]
333
333
  )
334
334
  else:
335
- emit_configuration_as_airbyte_control_message(self._connector_config) # type: ignore [arg-type]
335
+ emit_configuration_as_airbyte_control_message(self._connector_config) # type: ignore[arg-type]
336
336
  return self.access_token
337
337
 
338
338
  def refresh_access_token( # type: ignore[override] # Signature doesn't match base class
@@ -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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: airbyte-cdk
3
- Version: 6.21.1.dev0
3
+ Version: 6.26.0.dev4103
4
4
  Summary: A framework for writing Airbyte Connectors.
5
5
  License: MIT
6
6
  Keywords: airbyte,connector-development-kit,cdk
@@ -23,7 +23,7 @@ Requires-Dist: Jinja2 (>=3.1.2,<3.2.0)
23
23
  Requires-Dist: PyYAML (>=6.0.1,<7.0.0)
24
24
  Requires-Dist: Unidecode (>=1.3,<2.0)
25
25
  Requires-Dist: airbyte-protocol-models-dataclasses (>=0.14,<0.15)
26
- Requires-Dist: avro (>=1.11.2,<1.12.0) ; extra == "file-based"
26
+ Requires-Dist: avro (>=1.11.2,<1.13.0) ; extra == "file-based"
27
27
  Requires-Dist: backoff
28
28
  Requires-Dist: cachetools
29
29
  Requires-Dist: cohere (==4.21) ; extra == "vector-db-based"
@@ -66,7 +66,7 @@ Requires-Dist: tiktoken (==0.8.0) ; extra == "vector-db-based"
66
66
  Requires-Dist: unstructured.pytesseract (>=0.3.12) ; extra == "file-based"
67
67
  Requires-Dist: unstructured[docx,pptx] (==0.10.27) ; extra == "file-based"
68
68
  Requires-Dist: wcmatch (==10.0)
69
- Requires-Dist: xmltodict (>=0.13.0,<0.14.0)
69
+ Requires-Dist: xmltodict (>=0.13,<0.15)
70
70
  Project-URL: Documentation, https://docs.airbyte.io/
71
71
  Project-URL: Homepage, https://airbyte.com
72
72
  Project-URL: Repository, https://github.com/airbytehq/airbyte-python-cdk