airbyte-cdk 6.37.0.dev0__py3-none-any.whl → 6.37.0.dev1__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 (22) hide show
  1. airbyte_cdk/connector_builder/models.py +14 -16
  2. airbyte_cdk/connector_builder/test_reader/helpers.py +22 -120
  3. airbyte_cdk/connector_builder/test_reader/message_grouper.py +3 -16
  4. airbyte_cdk/connector_builder/test_reader/types.py +1 -9
  5. airbyte_cdk/entrypoint.py +7 -7
  6. airbyte_cdk/sources/declarative/auth/token_provider.py +0 -1
  7. airbyte_cdk/sources/declarative/concurrent_declarative_source.py +0 -15
  8. airbyte_cdk/sources/declarative/declarative_component_schema.yaml +43 -5
  9. airbyte_cdk/sources/declarative/decoders/composite_raw_decoder.py +2 -13
  10. airbyte_cdk/sources/declarative/incremental/concurrent_partition_cursor.py +17 -83
  11. airbyte_cdk/sources/declarative/models/declarative_component_schema.py +42 -3
  12. airbyte_cdk/sources/declarative/parsers/model_to_component_factory.py +63 -52
  13. airbyte_cdk/sources/declarative/partition_routers/__init__.py +4 -0
  14. airbyte_cdk/sources/declarative/partition_routers/grouping_partition_router.py +136 -0
  15. airbyte_cdk/sources/http_logger.py +0 -3
  16. airbyte_cdk/sources/streams/http/requests_native_auth/abstract_oauth.py +0 -1
  17. {airbyte_cdk-6.37.0.dev0.dist-info → airbyte_cdk-6.37.0.dev1.dist-info}/METADATA +1 -1
  18. {airbyte_cdk-6.37.0.dev0.dist-info → airbyte_cdk-6.37.0.dev1.dist-info}/RECORD +22 -21
  19. {airbyte_cdk-6.37.0.dev0.dist-info → airbyte_cdk-6.37.0.dev1.dist-info}/LICENSE.txt +0 -0
  20. {airbyte_cdk-6.37.0.dev0.dist-info → airbyte_cdk-6.37.0.dev1.dist-info}/LICENSE_SHORT +0 -0
  21. {airbyte_cdk-6.37.0.dev0.dist-info → airbyte_cdk-6.37.0.dev1.dist-info}/WHEEL +0 -0
  22. {airbyte_cdk-6.37.0.dev0.dist-info → airbyte_cdk-6.37.0.dev1.dist-info}/entry_points.txt +0 -0
@@ -21,6 +21,20 @@ class HttpRequest:
21
21
  body: Optional[str] = None
22
22
 
23
23
 
24
+ @dataclass
25
+ class StreamReadPages:
26
+ records: List[object]
27
+ request: Optional[HttpRequest] = None
28
+ response: Optional[HttpResponse] = None
29
+
30
+
31
+ @dataclass
32
+ class StreamReadSlices:
33
+ pages: List[StreamReadPages]
34
+ slice_descriptor: Optional[Dict[str, Any]]
35
+ state: Optional[List[Dict[str, Any]]] = None
36
+
37
+
24
38
  @dataclass
25
39
  class LogMessage:
26
40
  message: str
@@ -32,27 +46,11 @@ class LogMessage:
32
46
  @dataclass
33
47
  class AuxiliaryRequest:
34
48
  title: str
35
- type: str
36
49
  description: str
37
50
  request: HttpRequest
38
51
  response: HttpResponse
39
52
 
40
53
 
41
- @dataclass
42
- class StreamReadPages:
43
- records: List[object]
44
- request: Optional[HttpRequest] = None
45
- response: Optional[HttpResponse] = None
46
-
47
-
48
- @dataclass
49
- class StreamReadSlices:
50
- pages: List[StreamReadPages]
51
- slice_descriptor: Optional[Dict[str, Any]]
52
- state: Optional[List[Dict[str, Any]]] = None
53
- auxiliary_requests: Optional[List[AuxiliaryRequest]] = None
54
-
55
-
56
54
  @dataclass
57
55
  class StreamRead(object):
58
56
  logs: List[LogMessage]
@@ -28,7 +28,7 @@ from airbyte_cdk.utils.schema_inferrer import (
28
28
  SchemaInferrer,
29
29
  )
30
30
 
31
- from .types import ASYNC_AUXILIARY_REQUEST_TYPES, LOG_MESSAGES_OUTPUT_TYPE
31
+ from .types import LOG_MESSAGES_OUTPUT_TYPE
32
32
 
33
33
  # -------
34
34
  # Parsers
@@ -226,8 +226,7 @@ def should_close_page(
226
226
  at_least_one_page_in_group
227
227
  and is_log_message(message)
228
228
  and (
229
- is_page_http_request(json_message)
230
- or message.log.message.startswith(SliceLogger.SLICE_LOG_PREFIX) # type: ignore[union-attr] # AirbyteMessage with MessageType.LOG has log.message
229
+ is_page_http_request(json_message) or message.log.message.startswith("slice:") # type: ignore[union-attr] # AirbyteMessage with MessageType.LOG has log.message
231
230
  )
232
231
  )
233
232
 
@@ -331,10 +330,6 @@ def is_auxiliary_http_request(message: Optional[Dict[str, Any]]) -> bool:
331
330
  return is_http_log(message) and message.get("http", {}).get("is_auxiliary", False)
332
331
 
333
332
 
334
- def is_async_auxiliary_request(message: AuxiliaryRequest) -> bool:
335
- return message.type in ASYNC_AUXILIARY_REQUEST_TYPES
336
-
337
-
338
333
  def is_log_message(message: AirbyteMessage) -> bool:
339
334
  """
340
335
  Determines whether the provided message is of type LOG.
@@ -418,7 +413,6 @@ def handle_current_slice(
418
413
  current_slice_pages: List[StreamReadPages],
419
414
  current_slice_descriptor: Optional[Dict[str, Any]] = None,
420
415
  latest_state_message: Optional[Dict[str, Any]] = None,
421
- auxiliary_requests: Optional[List[AuxiliaryRequest]] = None,
422
416
  ) -> StreamReadSlices:
423
417
  """
424
418
  Handles the current slice by packaging its pages, descriptor, and state into a StreamReadSlices instance.
@@ -427,7 +421,6 @@ def handle_current_slice(
427
421
  current_slice_pages (List[StreamReadPages]): The pages to be included in the slice.
428
422
  current_slice_descriptor (Optional[Dict[str, Any]]): Descriptor for the current slice, optional.
429
423
  latest_state_message (Optional[Dict[str, Any]]): The latest state message, optional.
430
- auxiliary_requests (Optional[List[AuxiliaryRequest]]): The auxiliary requests to include, optional.
431
424
 
432
425
  Returns:
433
426
  StreamReadSlices: An object containing the current slice's pages, descriptor, and state.
@@ -436,7 +429,6 @@ def handle_current_slice(
436
429
  pages=current_slice_pages,
437
430
  slice_descriptor=current_slice_descriptor,
438
431
  state=[latest_state_message] if latest_state_message else [],
439
- auxiliary_requests=auxiliary_requests if auxiliary_requests else [],
440
432
  )
441
433
 
442
434
 
@@ -494,24 +486,29 @@ def handle_auxiliary_request(json_message: Dict[str, JsonType]) -> AuxiliaryRequ
494
486
  Raises:
495
487
  ValueError: If any of the "airbyte_cdk", "stream", or "http" fields is not a dictionary.
496
488
  """
489
+ airbyte_cdk = json_message.get("airbyte_cdk", {})
490
+
491
+ if not isinstance(airbyte_cdk, dict):
492
+ raise ValueError(
493
+ f"Expected airbyte_cdk to be a dict, got {airbyte_cdk} of type {type(airbyte_cdk)}"
494
+ )
495
+
496
+ stream = airbyte_cdk.get("stream", {})
497
497
 
498
- airbyte_cdk = get_airbyte_cdk_from_message(json_message)
499
- stream = get_stream_from_airbyte_cdk(airbyte_cdk)
500
- title_prefix = get_auxiliary_request_title_prefix(stream)
501
- http = get_http_property_from_message(json_message)
502
- request_type = get_auxiliary_request_type(stream, http)
498
+ if not isinstance(stream, dict):
499
+ raise ValueError(f"Expected stream to be a dict, got {stream} of type {type(stream)}")
503
500
 
504
- title = title_prefix + str(http.get("title", None))
505
- description = str(http.get("description", None))
506
- request = create_request_from_log_message(json_message)
507
- response = create_response_from_log_message(json_message)
501
+ title_prefix = "Parent stream: " if stream.get("is_substream", False) else ""
502
+ http = json_message.get("http", {})
503
+
504
+ if not isinstance(http, dict):
505
+ raise ValueError(f"Expected http to be a dict, got {http} of type {type(http)}")
508
506
 
509
507
  return AuxiliaryRequest(
510
- title=title,
511
- type=request_type,
512
- description=description,
513
- request=request,
514
- response=response,
508
+ title=title_prefix + str(http.get("title", None)),
509
+ description=str(http.get("description", None)),
510
+ request=create_request_from_log_message(json_message),
511
+ response=create_response_from_log_message(json_message),
515
512
  )
516
513
 
517
514
 
@@ -561,8 +558,7 @@ def handle_log_message(
561
558
  at_least_one_page_in_group,
562
559
  current_page_request,
563
560
  current_page_response,
564
- auxiliary_request,
565
- log_message,
561
+ auxiliary_request or log_message,
566
562
  )
567
563
 
568
564
 
@@ -593,97 +589,3 @@ def handle_record_message(
593
589
  datetime_format_inferrer.accumulate(message.record) # type: ignore
594
590
 
595
591
  return records_count
596
-
597
-
598
- # -------
599
- # Reusable Getters
600
- # -------
601
-
602
-
603
- def get_airbyte_cdk_from_message(json_message: Dict[str, JsonType]) -> dict: # type: ignore
604
- """
605
- Retrieves the "airbyte_cdk" dictionary from the provided JSON message.
606
-
607
- This function validates that the extracted "airbyte_cdk" is of type dict,
608
- raising a ValueError if the validation fails.
609
-
610
- Parameters:
611
- json_message (Dict[str, JsonType]): A dictionary representing the JSON message.
612
-
613
- Returns:
614
- dict: The "airbyte_cdk" dictionary extracted from the JSON message.
615
-
616
- Raises:
617
- ValueError: If the "airbyte_cdk" field is not a dictionary.
618
- """
619
- airbyte_cdk = json_message.get("airbyte_cdk", {})
620
-
621
- if not isinstance(airbyte_cdk, dict):
622
- raise ValueError(
623
- f"Expected airbyte_cdk to be a dict, got {airbyte_cdk} of type {type(airbyte_cdk)}"
624
- )
625
-
626
- return airbyte_cdk
627
-
628
-
629
- def get_stream_from_airbyte_cdk(airbyte_cdk: dict) -> dict: # type: ignore
630
- """
631
- Retrieves the "stream" dictionary from the provided "airbyte_cdk" dictionary.
632
-
633
- This function ensures that the extracted "stream" is of type dict,
634
- raising a ValueError if the validation fails.
635
-
636
- Parameters:
637
- airbyte_cdk (dict): The dictionary representing the Airbyte CDK data.
638
-
639
- Returns:
640
- dict: The "stream" dictionary extracted from the Airbyte CDK data.
641
-
642
- Raises:
643
- ValueError: If the "stream" field is not a dictionary.
644
- """
645
-
646
- stream = airbyte_cdk.get("stream", {})
647
-
648
- if not isinstance(stream, dict):
649
- raise ValueError(f"Expected stream to be a dict, got {stream} of type {type(stream)}")
650
-
651
- return stream
652
-
653
-
654
- def get_auxiliary_request_title_prefix(stream: dict) -> str: # type: ignore
655
- """
656
- Generates a title prefix based on the stream type.
657
- """
658
- return "Parent stream: " if stream.get("is_substream", False) else ""
659
-
660
-
661
- def get_http_property_from_message(json_message: Dict[str, JsonType]) -> dict: # type: ignore
662
- """
663
- Retrieves the "http" dictionary from the provided JSON message.
664
-
665
- This function validates that the extracted "http" is of type dict,
666
- raising a ValueError if the validation fails.
667
-
668
- Parameters:
669
- json_message (Dict[str, JsonType]): A dictionary representing the JSON message.
670
-
671
- Returns:
672
- dict: The "http" dictionary extracted from the JSON message.
673
-
674
- Raises:
675
- ValueError: If the "http" field is not a dictionary.
676
- """
677
- http = json_message.get("http", {})
678
-
679
- if not isinstance(http, dict):
680
- raise ValueError(f"Expected http to be a dict, got {http} of type {type(http)}")
681
-
682
- return http
683
-
684
-
685
- def get_auxiliary_request_type(stream: dict, http: dict) -> str: # type: ignore
686
- """
687
- Determines the type of the auxiliary request based on the stream and HTTP properties.
688
- """
689
- return "PARENT_STREAM" if stream.get("is_substream", False) else str(http.get("type", None))
@@ -6,7 +6,6 @@
6
6
  from typing import Any, Dict, Iterator, List, Mapping, Optional
7
7
 
8
8
  from airbyte_cdk.connector_builder.models import (
9
- AuxiliaryRequest,
10
9
  HttpRequest,
11
10
  HttpResponse,
12
11
  StreamReadPages,
@@ -25,7 +24,6 @@ from .helpers import (
25
24
  handle_current_slice,
26
25
  handle_log_message,
27
26
  handle_record_message,
28
- is_async_auxiliary_request,
29
27
  is_config_update_message,
30
28
  is_log_message,
31
29
  is_record_message,
@@ -91,7 +89,6 @@ def get_message_groups(
91
89
  current_page_request: Optional[HttpRequest] = None
92
90
  current_page_response: Optional[HttpResponse] = None
93
91
  latest_state_message: Optional[Dict[str, Any]] = None
94
- slice_auxiliary_requests: List[AuxiliaryRequest] = []
95
92
 
96
93
  while records_count < limit and (message := next(messages, None)):
97
94
  json_message = airbyte_message_to_json(message)
@@ -109,7 +106,6 @@ def get_message_groups(
109
106
  current_slice_pages,
110
107
  current_slice_descriptor,
111
108
  latest_state_message,
112
- slice_auxiliary_requests,
113
109
  )
114
110
  current_slice_descriptor = parse_slice_description(message.log.message) # type: ignore
115
111
  current_slice_pages = []
@@ -122,8 +118,7 @@ def get_message_groups(
122
118
  at_least_one_page_in_group,
123
119
  current_page_request,
124
120
  current_page_response,
125
- auxiliary_request,
126
- log_message,
121
+ log_or_auxiliary_request,
127
122
  ) = handle_log_message(
128
123
  message,
129
124
  json_message,
@@ -131,15 +126,8 @@ def get_message_groups(
131
126
  current_page_request,
132
127
  current_page_response,
133
128
  )
134
-
135
- if auxiliary_request:
136
- if is_async_auxiliary_request(auxiliary_request):
137
- slice_auxiliary_requests.append(auxiliary_request)
138
- else:
139
- yield auxiliary_request
140
-
141
- if log_message:
142
- yield log_message
129
+ if log_or_auxiliary_request:
130
+ yield log_or_auxiliary_request
143
131
  elif is_trace_with_error(message):
144
132
  if message.trace is not None:
145
133
  yield message.trace
@@ -169,5 +157,4 @@ def get_message_groups(
169
157
  current_slice_pages,
170
158
  current_slice_descriptor,
171
159
  latest_state_message,
172
- slice_auxiliary_requests,
173
160
  )
@@ -71,13 +71,5 @@ LOG_MESSAGES_OUTPUT_TYPE = tuple[
71
71
  bool,
72
72
  HttpRequest | None,
73
73
  HttpResponse | None,
74
- AuxiliaryRequest | None,
75
- AirbyteLogMessage | None,
76
- ]
77
-
78
- ASYNC_AUXILIARY_REQUEST_TYPES = [
79
- "ASYNC_CREATE",
80
- "ASYNC_POLL",
81
- "ASYNC_ABORT",
82
- "ASYNC_DELETE",
74
+ AuxiliaryRequest | AirbyteLogMessage | None,
83
75
  ]
airbyte_cdk/entrypoint.py CHANGED
@@ -37,8 +37,8 @@ from airbyte_cdk.sources import Source
37
37
  from airbyte_cdk.sources.connector_state_manager import HashableStreamDescriptor
38
38
  from airbyte_cdk.sources.utils.schema_helpers import check_config_against_spec_or_exit, split_config
39
39
 
40
- from airbyte_cdk.utils import PrintBuffer, is_cloud_environment, message_utils # add PrintBuffer back once fixed
41
- # from airbyte_cdk.utils import is_cloud_environment, message_utils
40
+ # from airbyte_cdk.utils import PrintBuffer, is_cloud_environment, message_utils # add PrintBuffer back once fixed
41
+ from airbyte_cdk.utils import is_cloud_environment, message_utils
42
42
  from airbyte_cdk.utils.airbyte_secrets_utils import get_secrets, update_secrets
43
43
  from airbyte_cdk.utils.constants import ENV_REQUEST_CACHE_PATH
44
44
  from airbyte_cdk.utils.traced_exception import AirbyteTracedException
@@ -337,11 +337,11 @@ def launch(source: Source, args: List[str]) -> None:
337
337
  parsed_args = source_entrypoint.parse_args(args)
338
338
  # temporarily removes the PrintBuffer because we're seeing weird print behavior for concurrent syncs
339
339
  # Refer to: https://github.com/airbytehq/oncall/issues/6235
340
- with PrintBuffer():
341
- for message in source_entrypoint.run(parsed_args):
342
- # simply printing is creating issues for concurrent CDK as Python uses different two instructions to print: one for the message and
343
- # the other for the break line. Adding `\n` to the message ensure that both are printed at the same time
344
- print(f"{message}\n", end="", flush=True)
340
+ # with PrintBuffer():
341
+ for message in source_entrypoint.run(parsed_args):
342
+ # simply printing is creating issues for concurrent CDK as Python uses different two instructions to print: one for the message and
343
+ # the other for the break line. Adding `\n` to the message ensure that both are printed at the same time
344
+ print(f"{message}\n", end="", flush=True)
345
345
 
346
346
 
347
347
  def _init_internal_request_filter() -> None:
@@ -58,7 +58,6 @@ class SessionTokenProvider(TokenProvider):
58
58
  "Obtains session token",
59
59
  None,
60
60
  is_auxiliary=True,
61
- type="AUTH",
62
61
  ),
63
62
  )
64
63
  if response is None:
@@ -44,7 +44,6 @@ from airbyte_cdk.sources.declarative.types import ConnectionDefinition
44
44
  from airbyte_cdk.sources.source import TState
45
45
  from airbyte_cdk.sources.streams import Stream
46
46
  from airbyte_cdk.sources.streams.concurrent.abstract_stream import AbstractStream
47
- from airbyte_cdk.sources.streams.concurrent.abstract_stream_facade import AbstractStreamFacade
48
47
  from airbyte_cdk.sources.streams.concurrent.availability_strategy import (
49
48
  AlwaysAvailableAvailabilityStrategy,
50
49
  )
@@ -119,12 +118,6 @@ class ConcurrentDeclarativeSource(ManifestDeclarativeSource, Generic[TState]):
119
118
  message_repository=self.message_repository,
120
119
  )
121
120
 
122
- # TODO: Remove this. This property is necessary to safely migrate Stripe during the transition state.
123
- @property
124
- def is_partially_declarative(self) -> bool:
125
- """This flag used to avoid unexpected AbstractStreamFacade processing as concurrent streams."""
126
- return False
127
-
128
121
  def read(
129
122
  self,
130
123
  logger: logging.Logger,
@@ -376,14 +369,6 @@ class ConcurrentDeclarativeSource(ManifestDeclarativeSource, Generic[TState]):
376
369
  )
377
370
  else:
378
371
  synchronous_streams.append(declarative_stream)
379
- # TODO: Remove this. This check is necessary to safely migrate Stripe during the transition state.
380
- # Condition below needs to ensure that concurrent support is not lost for sources that already support
381
- # it before migration, but now are only partially migrated to declarative implementation (e.g., Stripe).
382
- elif (
383
- isinstance(declarative_stream, AbstractStreamFacade)
384
- and self.is_partially_declarative
385
- ):
386
- concurrent_streams.append(declarative_stream.get_underlying_stream())
387
372
  else:
388
373
  synchronous_streams.append(declarative_stream)
389
374
 
@@ -1490,11 +1490,7 @@ definitions:
1490
1490
  limit:
1491
1491
  title: Limit
1492
1492
  description: The maximum number of calls allowed within the interval.
1493
- anyOf:
1494
- - type: integer
1495
- - type: string
1496
- interpolation_context:
1497
- - config
1493
+ type: integer
1498
1494
  interval:
1499
1495
  title: Interval
1500
1496
  description: The time interval for the rate limit.
@@ -3134,12 +3130,14 @@ definitions:
3134
3130
  - "$ref": "#/definitions/CustomPartitionRouter"
3135
3131
  - "$ref": "#/definitions/ListPartitionRouter"
3136
3132
  - "$ref": "#/definitions/SubstreamPartitionRouter"
3133
+ - "$ref": "#/definitions/GroupingPartitionRouter"
3137
3134
  - type: array
3138
3135
  items:
3139
3136
  anyOf:
3140
3137
  - "$ref": "#/definitions/CustomPartitionRouter"
3141
3138
  - "$ref": "#/definitions/ListPartitionRouter"
3142
3139
  - "$ref": "#/definitions/SubstreamPartitionRouter"
3140
+ - "$ref": "#/definitions/GroupingPartitionRouter"
3143
3141
  decoder:
3144
3142
  title: Decoder
3145
3143
  description: Component decoding the response so records can be extracted.
@@ -3294,12 +3292,14 @@ definitions:
3294
3292
  - "$ref": "#/definitions/CustomPartitionRouter"
3295
3293
  - "$ref": "#/definitions/ListPartitionRouter"
3296
3294
  - "$ref": "#/definitions/SubstreamPartitionRouter"
3295
+ - "$ref": "#/definitions/GroupingPartitionRouter"
3297
3296
  - type: array
3298
3297
  items:
3299
3298
  anyOf:
3300
3299
  - "$ref": "#/definitions/CustomPartitionRouter"
3301
3300
  - "$ref": "#/definitions/ListPartitionRouter"
3302
3301
  - "$ref": "#/definitions/SubstreamPartitionRouter"
3302
+ - "$ref": "#/definitions/GroupingPartitionRouter"
3303
3303
  decoder:
3304
3304
  title: Decoder
3305
3305
  description: Component decoding the response so records can be extracted.
@@ -3416,6 +3416,44 @@ definitions:
3416
3416
  $parameters:
3417
3417
  type: object
3418
3418
  additionalProperties: true
3419
+ GroupingPartitionRouter:
3420
+ title: Grouping Partition Router
3421
+ description: >
3422
+ A decorator on top of a partition router that groups partitions into batches of a specified size.
3423
+ This is useful for APIs that support filtering by multiple partition keys in a single request.
3424
+ Note that per-partition incremental syncs may not work as expected because the grouping
3425
+ of partitions might change between syncs, potentially leading to inconsistent state tracking.
3426
+ type: object
3427
+ required:
3428
+ - type
3429
+ - group_size
3430
+ - underlying_partition_router
3431
+ properties:
3432
+ type:
3433
+ type: string
3434
+ enum: [GroupingPartitionRouter]
3435
+ group_size:
3436
+ title: Group Size
3437
+ description: The number of partitions to include in each group. This determines how many partition values are batched together in a single slice.
3438
+ type: integer
3439
+ examples:
3440
+ - 10
3441
+ - 50
3442
+ underlying_partition_router:
3443
+ title: Underlying Partition Router
3444
+ description: The partition router whose output will be grouped. This can be any valid partition router component.
3445
+ anyOf:
3446
+ - "$ref": "#/definitions/CustomPartitionRouter"
3447
+ - "$ref": "#/definitions/ListPartitionRouter"
3448
+ - "$ref": "#/definitions/SubstreamPartitionRouter"
3449
+ deduplicate:
3450
+ title: Deduplicate Partitions
3451
+ description: If true, ensures that partitions are unique within each group by removing duplicates based on the partition key.
3452
+ type: boolean
3453
+ default: true
3454
+ $parameters:
3455
+ type: object
3456
+ additionalProperties: true
3419
3457
  WaitUntilTimeFromHeader:
3420
3458
  title: Wait Until Time Defined In Response Header
3421
3459
  description: Extract time at which we can retry the request from response header and wait for the difference between now and that time.
@@ -107,16 +107,6 @@ class CsvParser(Parser):
107
107
  encoding: Optional[str] = "utf-8"
108
108
  delimiter: Optional[str] = ","
109
109
 
110
- def _get_delimiter(self) -> Optional[str]:
111
- """
112
- Get delimiter from the configuration. Check for the escape character and decode it.
113
- """
114
- if self.delimiter is not None:
115
- if self.delimiter.startswith("\\"):
116
- self.delimiter = self.delimiter.encode("utf-8").decode("unicode_escape")
117
-
118
- return self.delimiter
119
-
120
110
  def parse(
121
111
  self,
122
112
  data: BufferedIOBase,
@@ -125,9 +115,8 @@ class CsvParser(Parser):
125
115
  Parse CSV data from decompressed bytes.
126
116
  """
127
117
  text_data = TextIOWrapper(data, encoding=self.encoding) # type: ignore
128
- reader = csv.DictReader(text_data, delimiter=self._get_delimiter() or ",")
129
- for row in reader:
130
- yield row
118
+ reader = csv.DictReader(text_data, delimiter=self.delimiter or ",")
119
+ yield from reader
131
120
 
132
121
 
133
122
  @dataclass
@@ -95,10 +95,6 @@ class ConcurrentPerPartitionCursor(Cursor):
95
95
  # the oldest partitions can be efficiently removed, maintaining the most recent partitions.
96
96
  self._cursor_per_partition: OrderedDict[str, ConcurrentCursor] = OrderedDict()
97
97
  self._semaphore_per_partition: OrderedDict[str, threading.Semaphore] = OrderedDict()
98
-
99
- # Parent-state tracking: store each partition’s parent state in creation order
100
- self._partition_parent_state_map: OrderedDict[str, Mapping[str, Any]] = OrderedDict()
101
-
102
98
  self._finished_partitions: set[str] = set()
103
99
  self._lock = threading.Lock()
104
100
  self._timer = Timer()
@@ -159,62 +155,11 @@ class ConcurrentPerPartitionCursor(Cursor):
159
155
  and self._semaphore_per_partition[partition_key]._value == 0
160
156
  ):
161
157
  self._update_global_cursor(cursor.state[self.cursor_field.cursor_field_key])
162
-
163
- self._check_and_update_parent_state()
164
-
165
- self._emit_state_message()
166
-
167
- def _check_and_update_parent_state(self) -> None:
168
- """
169
- Pop the leftmost partition state from _partition_parent_state_map only if
170
- *all partitions* up to (and including) that partition key in _semaphore_per_partition
171
- are fully finished (i.e. in _finished_partitions and semaphore._value == 0).
172
- Additionally, delete finished semaphores with a value of 0 to free up memory,
173
- as they are only needed to track errors and completion status.
174
- """
175
- last_closed_state = None
176
-
177
- while self._partition_parent_state_map:
178
- # Look at the earliest partition key in creation order
179
- earliest_key = next(iter(self._partition_parent_state_map))
180
-
181
- # Verify ALL partitions from the left up to earliest_key are finished
182
- all_left_finished = True
183
- for p_key, sem in list(
184
- self._semaphore_per_partition.items()
185
- ): # Use list to allow modification during iteration
186
- # If any earlier partition is still not finished, we must stop
187
- if p_key not in self._finished_partitions or sem._value != 0:
188
- all_left_finished = False
189
- break
190
- # Once we've reached earliest_key in the semaphore order, we can stop checking
191
- if p_key == earliest_key:
192
- break
193
-
194
- # If the partitions up to earliest_key are not all finished, break the while-loop
195
- if not all_left_finished:
196
- break
197
-
198
- # Pop the leftmost entry from parent-state map
199
- _, closed_parent_state = self._partition_parent_state_map.popitem(last=False)
200
- last_closed_state = closed_parent_state
201
-
202
- # Clean up finished semaphores with value 0 up to and including earliest_key
203
- for p_key in list(self._semaphore_per_partition.keys()):
204
- sem = self._semaphore_per_partition[p_key]
205
- if p_key in self._finished_partitions and sem._value == 0:
206
- del self._semaphore_per_partition[p_key]
207
- logger.debug(f"Deleted finished semaphore for partition {p_key} with value 0")
208
- if p_key == earliest_key:
209
- break
210
-
211
- # Update _parent_state if we popped at least one partition
212
- if last_closed_state is not None:
213
- self._parent_state = last_closed_state
158
+ self._emit_state_message()
214
159
 
215
160
  def ensure_at_least_one_state_emitted(self) -> None:
216
161
  """
217
- The platform expects at least one state message on successful syncs. Hence, whatever happens, we expect this method to be
162
+ The platform expect to have at least one state message on successful syncs. Hence, whatever happens, we expect this method to be
218
163
  called.
219
164
  """
220
165
  if not any(
@@ -256,19 +201,13 @@ class ConcurrentPerPartitionCursor(Cursor):
256
201
 
257
202
  slices = self._partition_router.stream_slices()
258
203
  self._timer.start()
259
- for partition, last, parent_state in iterate_with_last_flag_and_state(
260
- slices, self._partition_router.get_stream_state
261
- ):
262
- yield from self._generate_slices_from_partition(partition, parent_state)
204
+ for partition in slices:
205
+ yield from self._generate_slices_from_partition(partition)
263
206
 
264
- def _generate_slices_from_partition(
265
- self, partition: StreamSlice, parent_state: Mapping[str, Any]
266
- ) -> Iterable[StreamSlice]:
207
+ def _generate_slices_from_partition(self, partition: StreamSlice) -> Iterable[StreamSlice]:
267
208
  # Ensure the maximum number of partitions is not exceeded
268
209
  self._ensure_partition_limit()
269
210
 
270
- partition_key = self._to_partition_key(partition.partition)
271
-
272
211
  cursor = self._cursor_per_partition.get(self._to_partition_key(partition.partition))
273
212
  if not cursor:
274
213
  cursor = self._create_cursor(
@@ -277,26 +216,18 @@ class ConcurrentPerPartitionCursor(Cursor):
277
216
  )
278
217
  with self._lock:
279
218
  self._number_of_partitions += 1
280
- self._cursor_per_partition[partition_key] = cursor
281
- self._semaphore_per_partition[partition_key] = threading.Semaphore(0)
282
-
283
- with self._lock:
284
- if (
285
- len(self._partition_parent_state_map) == 0
286
- or self._partition_parent_state_map[
287
- next(reversed(self._partition_parent_state_map))
288
- ]
289
- != parent_state
290
- ):
291
- self._partition_parent_state_map[partition_key] = deepcopy(parent_state)
219
+ self._cursor_per_partition[self._to_partition_key(partition.partition)] = cursor
220
+ self._semaphore_per_partition[self._to_partition_key(partition.partition)] = (
221
+ threading.Semaphore(0)
222
+ )
292
223
 
293
224
  for cursor_slice, is_last_slice, _ in iterate_with_last_flag_and_state(
294
225
  cursor.stream_slices(),
295
226
  lambda: None,
296
227
  ):
297
- self._semaphore_per_partition[partition_key].release()
228
+ self._semaphore_per_partition[self._to_partition_key(partition.partition)].release()
298
229
  if is_last_slice:
299
- self._finished_partitions.add(partition_key)
230
+ self._finished_partitions.add(self._to_partition_key(partition.partition))
300
231
  yield StreamSlice(
301
232
  partition=partition, cursor_slice=cursor_slice, extra_fields=partition.extra_fields
302
233
  )
@@ -326,9 +257,9 @@ class ConcurrentPerPartitionCursor(Cursor):
326
257
  while len(self._cursor_per_partition) > self.DEFAULT_MAX_PARTITIONS_NUMBER - 1:
327
258
  # Try removing finished partitions first
328
259
  for partition_key in list(self._cursor_per_partition.keys()):
329
- if partition_key in self._finished_partitions and (
330
- partition_key not in self._semaphore_per_partition
331
- or self._semaphore_per_partition[partition_key]._value == 0
260
+ if (
261
+ partition_key in self._finished_partitions
262
+ and self._semaphore_per_partition[partition_key]._value == 0
332
263
  ):
333
264
  oldest_partition = self._cursor_per_partition.pop(
334
265
  partition_key
@@ -407,6 +338,9 @@ class ConcurrentPerPartitionCursor(Cursor):
407
338
  self._cursor_per_partition[self._to_partition_key(state["partition"])] = (
408
339
  self._create_cursor(state["cursor"])
409
340
  )
341
+ self._semaphore_per_partition[self._to_partition_key(state["partition"])] = (
342
+ threading.Semaphore(0)
343
+ )
410
344
 
411
345
  # set default state for missing partitions if it is per partition with fallback to global
412
346
  if self._GLOBAL_STATE_KEY in stream_state:
@@ -646,7 +646,7 @@ class Rate(BaseModel):
646
646
  class Config:
647
647
  extra = Extra.allow
648
648
 
649
- limit: Union[int, str] = Field(
649
+ limit: int = Field(
650
650
  ...,
651
651
  description="The maximum number of calls allowed within the interval.",
652
652
  title="Limit",
@@ -2225,7 +2225,15 @@ class SimpleRetriever(BaseModel):
2225
2225
  CustomPartitionRouter,
2226
2226
  ListPartitionRouter,
2227
2227
  SubstreamPartitionRouter,
2228
- List[Union[CustomPartitionRouter, ListPartitionRouter, SubstreamPartitionRouter]],
2228
+ GroupingPartitionRouter,
2229
+ List[
2230
+ Union[
2231
+ CustomPartitionRouter,
2232
+ ListPartitionRouter,
2233
+ SubstreamPartitionRouter,
2234
+ GroupingPartitionRouter,
2235
+ ]
2236
+ ],
2229
2237
  ]
2230
2238
  ] = Field(
2231
2239
  [],
@@ -2303,7 +2311,15 @@ class AsyncRetriever(BaseModel):
2303
2311
  CustomPartitionRouter,
2304
2312
  ListPartitionRouter,
2305
2313
  SubstreamPartitionRouter,
2306
- List[Union[CustomPartitionRouter, ListPartitionRouter, SubstreamPartitionRouter]],
2314
+ GroupingPartitionRouter,
2315
+ List[
2316
+ Union[
2317
+ CustomPartitionRouter,
2318
+ ListPartitionRouter,
2319
+ SubstreamPartitionRouter,
2320
+ GroupingPartitionRouter,
2321
+ ]
2322
+ ],
2307
2323
  ]
2308
2324
  ] = Field(
2309
2325
  [],
@@ -2355,6 +2371,29 @@ class SubstreamPartitionRouter(BaseModel):
2355
2371
  parameters: Optional[Dict[str, Any]] = Field(None, alias="$parameters")
2356
2372
 
2357
2373
 
2374
+ class GroupingPartitionRouter(BaseModel):
2375
+ type: Literal["GroupingPartitionRouter"]
2376
+ group_size: int = Field(
2377
+ ...,
2378
+ description="The number of partitions to include in each group. This determines how many partition values are batched together in a single slice.",
2379
+ examples=[10, 50],
2380
+ title="Group Size",
2381
+ )
2382
+ underlying_partition_router: Union[
2383
+ CustomPartitionRouter, ListPartitionRouter, SubstreamPartitionRouter
2384
+ ] = Field(
2385
+ ...,
2386
+ description="The partition router whose output will be grouped. This can be any valid partition router component.",
2387
+ title="Underlying Partition Router",
2388
+ )
2389
+ deduplicate: Optional[bool] = Field(
2390
+ True,
2391
+ description="If true, ensures that partitions are unique within each group by removing duplicates based on the partition key.",
2392
+ title="Deduplicate Partitions",
2393
+ )
2394
+ parameters: Optional[Dict[str, Any]] = Field(None, alias="$parameters")
2395
+
2396
+
2358
2397
  class HttpComponentsResolver(BaseModel):
2359
2398
  type: Literal["HttpComponentsResolver"]
2360
2399
  retriever: Union[AsyncRetriever, CustomRetriever, SimpleRetriever] = Field(
@@ -227,6 +227,9 @@ from airbyte_cdk.sources.declarative.models.declarative_component_schema import
227
227
  from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
228
228
  FlattenFields as FlattenFieldsModel,
229
229
  )
230
+ from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
231
+ GroupingPartitionRouter as GroupingPartitionRouterModel,
232
+ )
230
233
  from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
231
234
  GzipDecoder as GzipDecoderModel,
232
235
  )
@@ -379,6 +382,7 @@ from airbyte_cdk.sources.declarative.parsers.custom_code_compiler import (
379
382
  )
380
383
  from airbyte_cdk.sources.declarative.partition_routers import (
381
384
  CartesianProductStreamSlicer,
385
+ GroupingPartitionRouter,
382
386
  ListPartitionRouter,
383
387
  PartitionRouter,
384
388
  SinglePartitionRouter,
@@ -624,6 +628,7 @@ class ModelToComponentFactory:
624
628
  UnlimitedCallRatePolicyModel: self.create_unlimited_call_rate_policy,
625
629
  RateModel: self.create_rate,
626
630
  HttpRequestRegexMatcherModel: self.create_http_request_matcher,
631
+ GroupingPartitionRouterModel: self.create_grouping_partition_router,
627
632
  }
628
633
 
629
634
  # Needed for the case where we need to perform a second parse on the fields of a custom component
@@ -2091,10 +2096,10 @@ class ModelToComponentFactory:
2091
2096
  def create_json_decoder(model: JsonDecoderModel, config: Config, **kwargs: Any) -> Decoder:
2092
2097
  return JsonDecoder(parameters={})
2093
2098
 
2094
- def create_csv_decoder(self, model: CsvDecoderModel, config: Config, **kwargs: Any) -> Decoder:
2099
+ @staticmethod
2100
+ def create_csv_decoder(model: CsvDecoderModel, config: Config, **kwargs: Any) -> Decoder:
2095
2101
  return CompositeRawDecoder(
2096
- parser=ModelToComponentFactory._get_parser(model, config),
2097
- stream_response=False if self._emit_connector_builder_messages else True,
2102
+ parser=ModelToComponentFactory._get_parser(model, config), stream_response=True
2098
2103
  )
2099
2104
 
2100
2105
  @staticmethod
@@ -2103,12 +2108,10 @@ class ModelToComponentFactory:
2103
2108
  parser=ModelToComponentFactory._get_parser(model, config), stream_response=True
2104
2109
  )
2105
2110
 
2106
- def create_gzip_decoder(
2107
- self, model: GzipDecoderModel, config: Config, **kwargs: Any
2108
- ) -> Decoder:
2111
+ @staticmethod
2112
+ def create_gzip_decoder(model: GzipDecoderModel, config: Config, **kwargs: Any) -> Decoder:
2109
2113
  return CompositeRawDecoder(
2110
- parser=ModelToComponentFactory._get_parser(model, config),
2111
- stream_response=False if self._emit_connector_builder_messages else True,
2114
+ parser=ModelToComponentFactory._get_parser(model, config), stream_response=True
2112
2115
  )
2113
2116
 
2114
2117
  @staticmethod
@@ -2629,47 +2632,6 @@ class ModelToComponentFactory:
2629
2632
  transformations: List[RecordTransformation],
2630
2633
  **kwargs: Any,
2631
2634
  ) -> AsyncRetriever:
2632
- def _get_download_retriever() -> SimpleRetrieverTestReadDecorator | SimpleRetriever:
2633
- record_selector = RecordSelector(
2634
- extractor=download_extractor,
2635
- name=name,
2636
- record_filter=None,
2637
- transformations=transformations,
2638
- schema_normalization=TypeTransformer(TransformConfig.NoTransform),
2639
- config=config,
2640
- parameters={},
2641
- )
2642
- paginator = (
2643
- self._create_component_from_model(
2644
- model=model.download_paginator, decoder=decoder, config=config, url_base=""
2645
- )
2646
- if model.download_paginator
2647
- else NoPagination(parameters={})
2648
- )
2649
- maximum_number_of_slices = self._limit_slices_fetched or 5
2650
-
2651
- if self._limit_slices_fetched or self._emit_connector_builder_messages:
2652
- return SimpleRetrieverTestReadDecorator(
2653
- requester=download_requester,
2654
- record_selector=record_selector,
2655
- primary_key=None,
2656
- name=job_download_components_name,
2657
- paginator=paginator,
2658
- config=config,
2659
- parameters={},
2660
- maximum_number_of_slices=maximum_number_of_slices,
2661
- )
2662
-
2663
- return SimpleRetriever(
2664
- requester=download_requester,
2665
- record_selector=record_selector,
2666
- primary_key=None,
2667
- name=job_download_components_name,
2668
- paginator=paginator,
2669
- config=config,
2670
- parameters={},
2671
- )
2672
-
2673
2635
  decoder = (
2674
2636
  self._create_component_from_model(model=model.decoder, config=config)
2675
2637
  if model.decoder
@@ -2723,7 +2685,29 @@ class ModelToComponentFactory:
2723
2685
  config=config,
2724
2686
  name=job_download_components_name,
2725
2687
  )
2726
- download_retriever = _get_download_retriever()
2688
+ download_retriever = SimpleRetriever(
2689
+ requester=download_requester,
2690
+ record_selector=RecordSelector(
2691
+ extractor=download_extractor,
2692
+ name=name,
2693
+ record_filter=None,
2694
+ transformations=transformations,
2695
+ schema_normalization=TypeTransformer(TransformConfig.NoTransform),
2696
+ config=config,
2697
+ parameters={},
2698
+ ),
2699
+ primary_key=None,
2700
+ name=job_download_components_name,
2701
+ paginator=(
2702
+ self._create_component_from_model(
2703
+ model=model.download_paginator, decoder=decoder, config=config, url_base=""
2704
+ )
2705
+ if model.download_paginator
2706
+ else NoPagination(parameters={})
2707
+ ),
2708
+ config=config,
2709
+ parameters={},
2710
+ )
2727
2711
  abort_requester = (
2728
2712
  self._create_component_from_model(
2729
2713
  model=model.abort_requester,
@@ -3045,9 +3029,8 @@ class ModelToComponentFactory:
3045
3029
  )
3046
3030
 
3047
3031
  def create_rate(self, model: RateModel, config: Config, **kwargs: Any) -> Rate:
3048
- interpolated_limit = InterpolatedString.create(str(model.limit), parameters={})
3049
3032
  return Rate(
3050
- limit=int(interpolated_limit.eval(config=config)),
3033
+ limit=model.limit,
3051
3034
  interval=parse_duration(model.interval),
3052
3035
  )
3053
3036
 
@@ -3066,3 +3049,31 @@ class ModelToComponentFactory:
3066
3049
  self._api_budget = self.create_component(
3067
3050
  model_type=HTTPAPIBudgetModel, component_definition=component_definition, config=config
3068
3051
  )
3052
+
3053
+ def create_grouping_partition_router(
3054
+ self, model: GroupingPartitionRouterModel, config: Config, **kwargs: Any
3055
+ ) -> GroupingPartitionRouter:
3056
+ underlying_router = self._create_component_from_model(
3057
+ model=model.underlying_partition_router, config=config
3058
+ )
3059
+ if model.group_size < 1:
3060
+ raise ValueError(f"Group size must be greater than 0, got {model.group_size}")
3061
+
3062
+ if not isinstance(underlying_router, PartitionRouter):
3063
+ raise ValueError(
3064
+ f"Underlying partition router must be a PartitionRouter subclass, got {type(underlying_router)}"
3065
+ )
3066
+
3067
+ if isinstance(underlying_router, SubstreamPartitionRouter):
3068
+ if any(
3069
+ parent_config.request_option
3070
+ for parent_config in underlying_router.parent_stream_configs
3071
+ ):
3072
+ raise ValueError("Request options are not supported for GroupingPartitionRouter.")
3073
+
3074
+ return GroupingPartitionRouter(
3075
+ group_size=model.group_size,
3076
+ underlying_partition_router=underlying_router,
3077
+ deduplicate=model.deduplicate if model.deduplicate is not None else True,
3078
+ config=config,
3079
+ )
@@ -8,6 +8,9 @@ from airbyte_cdk.sources.declarative.partition_routers.async_job_partition_route
8
8
  from airbyte_cdk.sources.declarative.partition_routers.cartesian_product_stream_slicer import (
9
9
  CartesianProductStreamSlicer,
10
10
  )
11
+ from airbyte_cdk.sources.declarative.partition_routers.grouping_partition_router import (
12
+ GroupingPartitionRouter,
13
+ )
11
14
  from airbyte_cdk.sources.declarative.partition_routers.list_partition_router import (
12
15
  ListPartitionRouter,
13
16
  )
@@ -22,6 +25,7 @@ from airbyte_cdk.sources.declarative.partition_routers.substream_partition_route
22
25
  __all__ = [
23
26
  "AsyncJobPartitionRouter",
24
27
  "CartesianProductStreamSlicer",
28
+ "GroupingPartitionRouter",
25
29
  "ListPartitionRouter",
26
30
  "SinglePartitionRouter",
27
31
  "SubstreamPartitionRouter",
@@ -0,0 +1,136 @@
1
+ #
2
+ # Copyright (c) 2023 Airbyte, Inc., all rights reserved.
3
+ #
4
+
5
+ from dataclasses import dataclass
6
+ from typing import Any, Iterable, Mapping, Optional
7
+
8
+ from airbyte_cdk.sources.declarative.partition_routers.partition_router import PartitionRouter
9
+ from airbyte_cdk.sources.types import Config, StreamSlice, StreamState
10
+
11
+
12
+ @dataclass
13
+ class GroupingPartitionRouter(PartitionRouter):
14
+ """
15
+ A partition router that groups partitions from an underlying partition router into batches of a specified size.
16
+ This is useful for APIs that support filtering by multiple partition keys in a single request.
17
+
18
+ Attributes:
19
+ group_size (int): The number of partitions to include in each group.
20
+ underlying_partition_router (PartitionRouter): The partition router whose output will be grouped.
21
+ deduplicate (bool): If True, ensures unique partitions within each group by removing duplicates based on the partition key.
22
+ config (Config): The connector configuration.
23
+ parameters (Mapping[str, Any]): Additional parameters for interpolation and configuration.
24
+ """
25
+
26
+ group_size: int
27
+ underlying_partition_router: PartitionRouter
28
+ config: Config
29
+ deduplicate: bool = True
30
+
31
+ def stream_slices(self) -> Iterable[StreamSlice]:
32
+ """
33
+ Lazily groups partitions from the underlying partition router into batches of size `group_size`.
34
+
35
+ This method processes partitions one at a time from the underlying router, maintaining a batch buffer.
36
+ When the buffer reaches `group_size` or the underlying router is exhausted, it yields a grouped slice.
37
+ If deduplication is enabled, it tracks seen partition keys to ensure uniqueness within the current batch.
38
+
39
+ Yields:
40
+ Iterable[StreamSlice]: An iterable of StreamSlice objects, where each slice contains a batch of partition values.
41
+ """
42
+ batch = []
43
+ seen_keys = set()
44
+
45
+ # Iterate over partitions lazily from the underlying router
46
+ for partition in self.underlying_partition_router.stream_slices():
47
+ # Extract the partition key (assuming single key-value pair, e.g., {"board_ids": value})
48
+ key = next(iter(partition.partition.values()), None)
49
+
50
+ # Skip duplicates if deduplication is enabled
51
+ if self.deduplicate and key in seen_keys:
52
+ continue
53
+
54
+ # Add partition to the batch
55
+ batch.append(partition)
56
+ if self.deduplicate:
57
+ seen_keys.add(key)
58
+
59
+ # Yield the batch when it reaches the group_size
60
+ if len(batch) == self.group_size:
61
+ yield self._create_grouped_slice(batch)
62
+ batch = [] # Reset the batch
63
+
64
+ # Yield any remaining partitions if the batch isn't empty
65
+ if batch:
66
+ yield self._create_grouped_slice(batch)
67
+
68
+ def _create_grouped_slice(self, batch: list[StreamSlice]) -> StreamSlice:
69
+ """
70
+ Creates a grouped StreamSlice from a batch of partitions, aggregating extra fields into a dictionary with list values.
71
+
72
+ Args:
73
+ batch (list[StreamSlice]): A list of StreamSlice objects to group.
74
+
75
+ Returns:
76
+ StreamSlice: A single StreamSlice with combined partition and extra field values.
77
+ """
78
+ # Combine partition values into a single dict with lists
79
+ grouped_partition = {
80
+ key: [p.partition.get(key) for p in batch] for key in batch[0].partition.keys()
81
+ }
82
+
83
+ # Aggregate extra fields into a dict with list values
84
+ extra_fields_dict = (
85
+ {
86
+ key: [p.extra_fields.get(key) for p in batch]
87
+ for key in set().union(*(p.extra_fields.keys() for p in batch if p.extra_fields))
88
+ }
89
+ if any(p.extra_fields for p in batch)
90
+ else {}
91
+ )
92
+ return StreamSlice(
93
+ partition=grouped_partition,
94
+ cursor_slice={}, # Cursor is managed by the underlying router or incremental sync
95
+ extra_fields=extra_fields_dict,
96
+ )
97
+
98
+ def get_request_params(
99
+ self,
100
+ stream_state: Optional[StreamState] = None,
101
+ stream_slice: Optional[StreamSlice] = None,
102
+ next_page_token: Optional[Mapping[str, Any]] = None,
103
+ ) -> Mapping[str, Any]:
104
+ return {}
105
+
106
+ def get_request_headers(
107
+ self,
108
+ stream_state: Optional[StreamState] = None,
109
+ stream_slice: Optional[StreamSlice] = None,
110
+ next_page_token: Optional[Mapping[str, Any]] = None,
111
+ ) -> Mapping[str, Any]:
112
+ return {}
113
+
114
+ def get_request_body_data(
115
+ self,
116
+ stream_state: Optional[StreamState] = None,
117
+ stream_slice: Optional[StreamSlice] = None,
118
+ next_page_token: Optional[Mapping[str, Any]] = None,
119
+ ) -> Mapping[str, Any]:
120
+ return {}
121
+
122
+ def get_request_body_json(
123
+ self,
124
+ stream_state: Optional[StreamState] = None,
125
+ stream_slice: Optional[StreamSlice] = None,
126
+ next_page_token: Optional[Mapping[str, Any]] = None,
127
+ ) -> Mapping[str, Any]:
128
+ return {}
129
+
130
+ def set_initial_state(self, stream_state: StreamState) -> None:
131
+ """Delegate state initialization to the underlying partition router."""
132
+ self.underlying_partition_router.set_initial_state(stream_state)
133
+
134
+ def get_stream_state(self) -> Optional[Mapping[str, StreamState]]:
135
+ """Delegate state retrieval to the underlying partition router."""
136
+ return self.underlying_partition_router.get_stream_state()
@@ -15,14 +15,11 @@ def format_http_message(
15
15
  description: str,
16
16
  stream_name: Optional[str],
17
17
  is_auxiliary: bool | None = None,
18
- type: Optional[str] = None,
19
18
  ) -> LogMessage:
20
- request_type: str = type if type else "HTTP"
21
19
  request = response.request
22
20
  log_message = {
23
21
  "http": {
24
22
  "title": title,
25
- "type": request_type,
26
23
  "description": description,
27
24
  "request": {
28
25
  "method": request.method,
@@ -396,7 +396,6 @@ class AbstractOauth2Authenticator(AuthBase):
396
396
  "Obtains access token",
397
397
  self._NO_STREAM_NAME,
398
398
  is_auxiliary=True,
399
- type="AUTH",
400
399
  ),
401
400
  )
402
401
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: airbyte-cdk
3
- Version: 6.37.0.dev0
3
+ Version: 6.37.0.dev1
4
4
  Summary: A framework for writing Airbyte Connectors.
5
5
  Home-page: https://airbyte.com
6
6
  License: MIT
@@ -9,12 +9,12 @@ airbyte_cdk/connector_builder/README.md,sha256=Hw3wvVewuHG9-QgsAq1jDiKuLlStDxKBz
9
9
  airbyte_cdk/connector_builder/__init__.py,sha256=4Hw-PX1-VgESLF16cDdvuYCzGJtHntThLF4qIiULWeo,61
10
10
  airbyte_cdk/connector_builder/connector_builder_handler.py,sha256=BntqkP63RBPvGCtB3CrLLtYplfSlBR42kwXyyk4YGas,4268
11
11
  airbyte_cdk/connector_builder/main.py,sha256=ubAPE0Oo5gjZOa-KMtLLJQkc8_inUpFR3sIb2DEh2No,3722
12
- airbyte_cdk/connector_builder/models.py,sha256=9pIZ98LW_d6fRS39VdnUOf3cxGt4TkC5MJ0_OrzcCRk,1578
12
+ airbyte_cdk/connector_builder/models.py,sha256=uCHpOdJx2PyZtIqk-mt9eSVuFMQoEqrW-9sjCz0Z-AQ,1500
13
13
  airbyte_cdk/connector_builder/test_reader/__init__.py,sha256=iTwBMoI9vaJotEgpqZbFjlxRcbxXYypSVJ9YxeHk7wc,120
14
- airbyte_cdk/connector_builder/test_reader/helpers.py,sha256=Iczn-_iczS2CaIAunWwyFcX0uLTra8Wh9JVfzm1Gfxo,26765
15
- airbyte_cdk/connector_builder/test_reader/message_grouper.py,sha256=84BAEPIBHMq3WCfO14WNvh_q7OsjGgDt0q1FTu8eW-w,6918
14
+ airbyte_cdk/connector_builder/test_reader/helpers.py,sha256=niu_EhzwVXnvtzj2Rf_unrxqLRC4Twbe-t17HyNoRJY,23662
15
+ airbyte_cdk/connector_builder/test_reader/message_grouper.py,sha256=L0xKpWQhfRecKblI3uYO9twPTPJYJzlOhK2P8zHWXRU,6487
16
16
  airbyte_cdk/connector_builder/test_reader/reader.py,sha256=GurMB4ITO_PntvhIHSJkXbhynLilI4DObY5A2axavXo,20667
17
- airbyte_cdk/connector_builder/test_reader/types.py,sha256=hPZG3jO03kBaPyW94NI3JHRS1jxXGSNBcN1HFzOxo5Y,2528
17
+ airbyte_cdk/connector_builder/test_reader/types.py,sha256=jP28aOlCS8Q6V7jMksfJKsuAJ-m2dNYiXaUjYvb0DBA,2404
18
18
  airbyte_cdk/destinations/__init__.py,sha256=FyDp28PT_YceJD5HDFhA-mrGfX9AONIyMQ4d68CHNxQ,213
19
19
  airbyte_cdk/destinations/destination.py,sha256=CIq-yb8C_0QvcKCtmStaHfiqn53GEfRAIGGCkJhKP1Q,5880
20
20
  airbyte_cdk/destinations/vector_db_based/README.md,sha256=QAe8c_1Afme4r2TCE10cTSaxUE3zgCBuArSuRQqK8tA,2115
@@ -26,7 +26,7 @@ airbyte_cdk/destinations/vector_db_based/indexer.py,sha256=beiSi2Uu67EoTr7yQSaCJ
26
26
  airbyte_cdk/destinations/vector_db_based/test_utils.py,sha256=MkqLiOJ5QyKbV4rNiJhe-BHM7FD-ADHQ4bQGf4c5lRY,1932
27
27
  airbyte_cdk/destinations/vector_db_based/utils.py,sha256=FOyEo8Lc-fY8UyhpCivhZtIqBRyxf3cUt6anmK03fUY,1127
28
28
  airbyte_cdk/destinations/vector_db_based/writer.py,sha256=nZ00xPiohElJmYktEZZIhr0m5EDETCHGhg0Lb2S7A20,5095
29
- airbyte_cdk/entrypoint.py,sha256=2b8lsoJkMIVtQE0vKkpsjHYnIUJm9G6HDuEN_lc4SP0,18569
29
+ airbyte_cdk/entrypoint.py,sha256=xFLY2PV8mKXUaeBAknczbK6plrs4_B1WdWA6K3iaRJI,18555
30
30
  airbyte_cdk/exception_handler.py,sha256=D_doVl3Dt60ASXlJsfviOCswxGyKF2q0RL6rif3fNks,2013
31
31
  airbyte_cdk/logger.py,sha256=qi4UGuSYQQGaFaTVJlMD9lLppwqLXt1XBhwSXo-Q5IA,3660
32
32
  airbyte_cdk/models/__init__.py,sha256=MOTiuML2wShBaMSIwikdjyye2uUWBjo4J1QFSbnoiM4,2075
@@ -60,22 +60,22 @@ airbyte_cdk/sources/declarative/auth/jwt.py,sha256=SICqNsN2Cn_EgKadIgWuZpQxuMHyz
60
60
  airbyte_cdk/sources/declarative/auth/oauth.py,sha256=SUfib1oSzlyRRnOSg8Bui73mfyrcyr9OssdchbKdu4s,14162
61
61
  airbyte_cdk/sources/declarative/auth/selective_authenticator.py,sha256=qGwC6YsCldr1bIeKG6Qo-A9a5cTdHw-vcOn3OtQrS4c,1540
62
62
  airbyte_cdk/sources/declarative/auth/token.py,sha256=2EnE78EhBOY9hbeZnQJ9AuFaM-G7dccU-oKo_LThRQk,11070
63
- airbyte_cdk/sources/declarative/auth/token_provider.py,sha256=Jzuxlmt1_-_aFC_n0OmP8L1nDOacLzbEVVx3kjdX_W8,3104
63
+ airbyte_cdk/sources/declarative/auth/token_provider.py,sha256=9CuSsmOoHkvlc4k-oZ3Jx5luAgfTMm1I_5HOZxw7wMU,3075
64
64
  airbyte_cdk/sources/declarative/checks/__init__.py,sha256=nsVV5Bo0E_tBNd8A4Xdsdb-75PpcLo5RQu2RQ_Gv-ME,806
65
65
  airbyte_cdk/sources/declarative/checks/check_dynamic_stream.py,sha256=HUktywjI8pqOeED08UGqponUSwxs2TOAECTowlWlrRE,2138
66
66
  airbyte_cdk/sources/declarative/checks/check_stream.py,sha256=dAA-UhmMj0WLXCkRQrilWCfJmncBzXCZ18ptRNip3XA,2139
67
67
  airbyte_cdk/sources/declarative/checks/connection_checker.py,sha256=MBRJo6WJlZQHpIfOGaNOkkHUmgUl_4wDM6VPo41z5Ss,1383
68
68
  airbyte_cdk/sources/declarative/concurrency_level/__init__.py,sha256=5XUqrmlstYlMM0j6crktlKQwALek0uiz2D3WdM46MyA,191
69
69
  airbyte_cdk/sources/declarative/concurrency_level/concurrency_level.py,sha256=YIwCTCpOr_QSNW4ltQK0yUGWInI8PKNY216HOOegYLk,2101
70
- airbyte_cdk/sources/declarative/concurrent_declarative_source.py,sha256=KBF9wdPC5KauFwg9dv4pFHLz01ZMwbMvN5ZCcZgiBEE,25424
70
+ airbyte_cdk/sources/declarative/concurrent_declarative_source.py,sha256=MRnIdGeKPk1dO9-4eWRHa7mI6Ay_7szGo9H1RJSZDb8,24453
71
71
  airbyte_cdk/sources/declarative/datetime/__init__.py,sha256=l9LG7Qm6e5r_qgqfVKnx3mXYtg1I9MmMjomVIPfU4XA,177
72
72
  airbyte_cdk/sources/declarative/datetime/datetime_parser.py,sha256=SX9JjdesN1edN2WVUVMzU_ptqp2QB1OnsnjZ4mwcX7w,2579
73
73
  airbyte_cdk/sources/declarative/datetime/min_max_datetime.py,sha256=0BHBtDNQZfvwM45-tY5pNlTcKAFSGGNxemoi0Jic-0E,5785
74
- airbyte_cdk/sources/declarative/declarative_component_schema.yaml,sha256=5o5GsltzbVL2jyXvjWzUoV_r5xpwG_YdLSVUuG_d_34,144548
74
+ airbyte_cdk/sources/declarative/declarative_component_schema.yaml,sha256=d8jbuP86iBhEhtbtw5dGg9b8U93KkDACgAO4EYW2lzg,146344
75
75
  airbyte_cdk/sources/declarative/declarative_source.py,sha256=nF7wBqFd3AQmEKAm4CnIo29CJoQL562cJGSCeL8U8bA,1531
76
76
  airbyte_cdk/sources/declarative/declarative_stream.py,sha256=venZjfpvtqr3oFSuvMBWtn4h9ayLhD4L65ACuXCDZ64,10445
77
77
  airbyte_cdk/sources/declarative/decoders/__init__.py,sha256=JHb_0d3SE6kNY10mxA5YBEKPeSbsWYjByq1gUQxepoE,953
78
- airbyte_cdk/sources/declarative/decoders/composite_raw_decoder.py,sha256=DJbWaaJ5LHCBpyWz-4bEw8rqtJYqabEYZtxnfRtWFE0,4946
78
+ airbyte_cdk/sources/declarative/decoders/composite_raw_decoder.py,sha256=kg5_kNlhXj8p9GZFztlbG16pk1XcMtP9ezqulq1VNg4,4545
79
79
  airbyte_cdk/sources/declarative/decoders/decoder.py,sha256=sl-Gt8lXi7yD2Q-sD8je5QS2PbgrgsYjxRLWsay7DMc,826
80
80
  airbyte_cdk/sources/declarative/decoders/json_decoder.py,sha256=BdWpXXPhEGf_zknggJmhojLosmxuw51RBVTS0jvdCPc,2080
81
81
  airbyte_cdk/sources/declarative/decoders/noop_decoder.py,sha256=iZh0yKY_JzgBnJWiubEusf5c0o6Khd-8EWFWT-8EgFo,542
@@ -92,7 +92,7 @@ airbyte_cdk/sources/declarative/extractors/record_selector.py,sha256=HCqx7IyENM_
92
92
  airbyte_cdk/sources/declarative/extractors/response_to_file_extractor.py,sha256=LhqGDfX06_dDYLKsIVnwQ_nAWCln-v8PV7Wgt_QVeTI,6533
93
93
  airbyte_cdk/sources/declarative/extractors/type_transformer.py,sha256=d6Y2Rfg8pMVEEnHllfVksWZdNVOU55yk34O03dP9muY,1626
94
94
  airbyte_cdk/sources/declarative/incremental/__init__.py,sha256=U1oZKtBaEC6IACmvziY9Wzg7Z8EgF4ZuR7NwvjlB_Sk,1255
95
- airbyte_cdk/sources/declarative/incremental/concurrent_partition_cursor.py,sha256=MT5JbdEbnPzk3VWZGGvThe4opoX5dHhSXFrnTRYC6dg,22210
95
+ airbyte_cdk/sources/declarative/incremental/concurrent_partition_cursor.py,sha256=Pg2phEFT9T8AzUjK6hVhn0rgR3yY6JPF-Dfv0g1m5dQ,19191
96
96
  airbyte_cdk/sources/declarative/incremental/datetime_based_cursor.py,sha256=Rbe6lJLTtZ5en33MwZiB9-H9-AwDMNHgwBZs8EqhYqk,22172
97
97
  airbyte_cdk/sources/declarative/incremental/declarative_cursor.py,sha256=5Bhw9VRPyIuCaD0wmmq_L3DZsa-rJgtKSEUzSd8YYD0,536
98
98
  airbyte_cdk/sources/declarative/incremental/global_substream_cursor.py,sha256=2tsE6FgXzemf4fZZ4uGtd8QpRBl9GJ2CRqSNJE5p0EI,16077
@@ -113,16 +113,17 @@ airbyte_cdk/sources/declarative/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW
113
113
  airbyte_cdk/sources/declarative/migrations/legacy_to_per_partition_state_migration.py,sha256=iemy3fKLczcU0-Aor7tx5jcT6DRedKMqyK7kCOp01hg,3924
114
114
  airbyte_cdk/sources/declarative/migrations/state_migration.py,sha256=KWPjealMLKSMtajXgkdGgKg7EmTLR-CqqD7UIh0-eDU,794
115
115
  airbyte_cdk/sources/declarative/models/__init__.py,sha256=nUFxNCiKeYRVXuZEKA7GD-lTHxsiKcQ8FitZjKhPIvE,100
116
- airbyte_cdk/sources/declarative/models/declarative_component_schema.py,sha256=gNL9DqajD2A8UBnKAz7F7YQuYH7frQyHiPQPIMGq2xo,101958
116
+ airbyte_cdk/sources/declarative/models/declarative_component_schema.py,sha256=Uk8sJRXs763ym_UCBzW2YGDzLPwTvzMQt_J0a3M-5zA,103309
117
117
  airbyte_cdk/sources/declarative/parsers/__init__.py,sha256=ZnqYNxHsKCgO38IwB34RQyRMXTs4GTvlRi3ImKnIioo,61
118
118
  airbyte_cdk/sources/declarative/parsers/custom_code_compiler.py,sha256=958MMX6_ZOJUlDDdNr9Krosgi2bCKGx2Z765M2Woz18,5505
119
119
  airbyte_cdk/sources/declarative/parsers/custom_exceptions.py,sha256=Rir9_z3Kcd5Es0-LChrzk-0qubAsiK_RSEnLmK2OXm8,553
120
120
  airbyte_cdk/sources/declarative/parsers/manifest_component_transformer.py,sha256=CXwTfD3wSQq3okcqwigpprbHhSURUokh4GK2OmOyKC8,9132
121
121
  airbyte_cdk/sources/declarative/parsers/manifest_reference_resolver.py,sha256=IWUOdF03o-aQn0Occo1BJCxU0Pz-QILk5L67nzw2thw,6803
122
- airbyte_cdk/sources/declarative/parsers/model_to_component_factory.py,sha256=dj7b3s8jaDV7Tb7EXVSzkvC8QY-mxyOf48rxkSMws6A,134851
123
- airbyte_cdk/sources/declarative/partition_routers/__init__.py,sha256=HJ-Syp3p7RpyR_OK0X_a2kSyISfu3W-PKrRI16iY0a8,957
122
+ airbyte_cdk/sources/declarative/parsers/model_to_component_factory.py,sha256=nY_s3xidDFJTuxlxbLVfjlZIxMhaXtwAg7tnxdkgdXg,135237
123
+ airbyte_cdk/sources/declarative/partition_routers/__init__.py,sha256=TBC9AkGaUqHm2IKHMPN6punBIcY5tWGULowcLoAVkfw,1109
124
124
  airbyte_cdk/sources/declarative/partition_routers/async_job_partition_router.py,sha256=VelO7zKqKtzMJ35jyFeg0ypJLQC0plqqIBNXoBW1G2E,3001
125
125
  airbyte_cdk/sources/declarative/partition_routers/cartesian_product_stream_slicer.py,sha256=c5cuVFM6NFkuQqG8Z5IwkBuwDrvXZN1CunUOM_L0ezg,6892
126
+ airbyte_cdk/sources/declarative/partition_routers/grouping_partition_router.py,sha256=PFoY931wC1i7Elphrd7LCFUPYKOTPEovLXC-mvkQow0,5531
126
127
  airbyte_cdk/sources/declarative/partition_routers/list_partition_router.py,sha256=tmGGpMoOBmaMfhVZq53AEWxoHm2lmNVi6hA2_IVEnAA,4882
127
128
  airbyte_cdk/sources/declarative/partition_routers/partition_router.py,sha256=YyEIzdmLd1FjbVP3QbQ2VFCLW_P-OGbVh6VpZShp54k,2218
128
129
  airbyte_cdk/sources/declarative/partition_routers/single_partition_router.py,sha256=SKzKjSyfccq4dxGIh-J6ejrgkCHzaiTIazmbmeQiRD4,1942
@@ -250,7 +251,7 @@ airbyte_cdk/sources/file_based/stream/identities_stream.py,sha256=DwgNU-jDp5vZ_W
250
251
  airbyte_cdk/sources/file_based/stream/permissions_file_based_stream.py,sha256=i0Jn0zuAPomLa4pHSu9TQ3gAN5xXhNzPTYVwUDiDEyE,3523
251
252
  airbyte_cdk/sources/file_based/types.py,sha256=INxG7OPnkdUP69oYNKMAbwhvV1AGvLRHs1J6pIia2FI,218
252
253
  airbyte_cdk/sources/http_config.py,sha256=OBZeuyFilm6NlDlBhFQvHhTWabEvZww6OHDIlZujIS0,730
253
- airbyte_cdk/sources/http_logger.py,sha256=H93kPAujHhPmXNX0JSFG3D-SL6yEFA5PtKot9Hu3TYA,1690
254
+ airbyte_cdk/sources/http_logger.py,sha256=l_1fk5YwdonZ1wvAsTwjj6d36fj2WrVraIAMj5jTQdM,1575
254
255
  airbyte_cdk/sources/message/__init__.py,sha256=y98fzHsQBwXwp2zEa4K5mxGFqjnx9lDn9O0pTk-VS4U,395
255
256
  airbyte_cdk/sources/message/repository.py,sha256=SG7avgti_-dj8FcRHTTrhgLLGJbElv14_zIB0SH8AIc,4763
256
257
  airbyte_cdk/sources/source.py,sha256=KIBBH5VLEb8BZ8B9aROlfaI6OLoJqKDPMJ10jkAR7nk,3611
@@ -303,7 +304,7 @@ airbyte_cdk/sources/streams/http/http.py,sha256=0uariNq8OFnlX7iqOHwBhecxA-Hfd5hS
303
304
  airbyte_cdk/sources/streams/http/http_client.py,sha256=tDE0ROtxjGMVphvsw8INvGMtZ97hIF-v47pZ3jIyiwc,23011
304
305
  airbyte_cdk/sources/streams/http/rate_limiting.py,sha256=IwdjrHKUnU97XO4qONgYRv4YYW51xQ8SJm4WLafXDB8,6351
305
306
  airbyte_cdk/sources/streams/http/requests_native_auth/__init__.py,sha256=RN0D3nOX1xLgwEwKWu6pkGy3XqBFzKSNZ8Lf6umU2eY,413
306
- airbyte_cdk/sources/streams/http/requests_native_auth/abstract_oauth.py,sha256=P9U8vtcrZ3m0InSG2W0H4gTYTxjQxkIe6mhF9xvO8Ug,18824
307
+ airbyte_cdk/sources/streams/http/requests_native_auth/abstract_oauth.py,sha256=cM5CM1mnbTEMiY6gKHblGXr9KTS5VEziGoc-TXC302k,18791
307
308
  airbyte_cdk/sources/streams/http/requests_native_auth/abstract_token.py,sha256=Y3n7J-sk5yGjv_OxtY6Z6k0PEsFZmtIRi-x0KCbaHdA,1010
308
309
  airbyte_cdk/sources/streams/http/requests_native_auth/oauth.py,sha256=C2j2uVfi9d-3KgHO3NGxIiFdfASjHOtsd6g_LWPYOAs,20311
309
310
  airbyte_cdk/sources/streams/http/requests_native_auth/token.py,sha256=h5PTzcdH-RQLeCg7xZ45w_484OPUDSwNWl_iMJQmZoI,2526
@@ -360,9 +361,9 @@ airbyte_cdk/utils/slice_hasher.py,sha256=EDxgROHDbfG-QKQb59m7h_7crN1tRiawdf5uU7G
360
361
  airbyte_cdk/utils/spec_schema_transformations.py,sha256=-5HTuNsnDBAhj-oLeQXwpTGA0HdcjFOf2zTEMUTTg_Y,816
361
362
  airbyte_cdk/utils/stream_status_utils.py,sha256=ZmBoiy5HVbUEHAMrUONxZvxnvfV9CesmQJLDTAIWnWw,1171
362
363
  airbyte_cdk/utils/traced_exception.py,sha256=C8uIBuCL_E4WnBAOPSxBicD06JAldoN9fGsQDp463OY,6292
363
- airbyte_cdk-6.37.0.dev0.dist-info/LICENSE.txt,sha256=Wfe61S4BaGPj404v8lrAbvhjYR68SHlkzeYrg3_bbuM,1051
364
- airbyte_cdk-6.37.0.dev0.dist-info/LICENSE_SHORT,sha256=aqF6D1NcESmpn-cqsxBtszTEnHKnlsp8L4x9wAh3Nxg,55
365
- airbyte_cdk-6.37.0.dev0.dist-info/METADATA,sha256=kcKo6BoaqSRqsOMLq2fmnSa6iVkhtMy4YMEL4Dk1WYU,6015
366
- airbyte_cdk-6.37.0.dev0.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
367
- airbyte_cdk-6.37.0.dev0.dist-info/entry_points.txt,sha256=fj-e3PAQvsxsQzyyq8UkG1k8spunWnD4BAH2AwlR6NM,95
368
- airbyte_cdk-6.37.0.dev0.dist-info/RECORD,,
364
+ airbyte_cdk-6.37.0.dev1.dist-info/LICENSE.txt,sha256=Wfe61S4BaGPj404v8lrAbvhjYR68SHlkzeYrg3_bbuM,1051
365
+ airbyte_cdk-6.37.0.dev1.dist-info/LICENSE_SHORT,sha256=aqF6D1NcESmpn-cqsxBtszTEnHKnlsp8L4x9wAh3Nxg,55
366
+ airbyte_cdk-6.37.0.dev1.dist-info/METADATA,sha256=9grGzb0aDmQhE2MU2nXEKIYJFWcdpJpMTrSW5kTML3M,6015
367
+ airbyte_cdk-6.37.0.dev1.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
368
+ airbyte_cdk-6.37.0.dev1.dist-info/entry_points.txt,sha256=fj-e3PAQvsxsQzyyq8UkG1k8spunWnD4BAH2AwlR6NM,95
369
+ airbyte_cdk-6.37.0.dev1.dist-info/RECORD,,