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.
- airbyte_cdk/connector_builder/models.py +14 -16
- airbyte_cdk/connector_builder/test_reader/helpers.py +22 -120
- airbyte_cdk/connector_builder/test_reader/message_grouper.py +3 -16
- airbyte_cdk/connector_builder/test_reader/types.py +1 -9
- airbyte_cdk/entrypoint.py +7 -7
- airbyte_cdk/sources/declarative/auth/token_provider.py +0 -1
- airbyte_cdk/sources/declarative/concurrent_declarative_source.py +0 -15
- airbyte_cdk/sources/declarative/declarative_component_schema.yaml +43 -5
- airbyte_cdk/sources/declarative/decoders/composite_raw_decoder.py +2 -13
- airbyte_cdk/sources/declarative/incremental/concurrent_partition_cursor.py +17 -83
- airbyte_cdk/sources/declarative/models/declarative_component_schema.py +42 -3
- airbyte_cdk/sources/declarative/parsers/model_to_component_factory.py +63 -52
- airbyte_cdk/sources/declarative/partition_routers/__init__.py +4 -0
- airbyte_cdk/sources/declarative/partition_routers/grouping_partition_router.py +136 -0
- airbyte_cdk/sources/http_logger.py +0 -3
- airbyte_cdk/sources/streams/http/requests_native_auth/abstract_oauth.py +0 -1
- {airbyte_cdk-6.37.0.dev0.dist-info → airbyte_cdk-6.37.0.dev1.dist-info}/METADATA +1 -1
- {airbyte_cdk-6.37.0.dev0.dist-info → airbyte_cdk-6.37.0.dev1.dist-info}/RECORD +22 -21
- {airbyte_cdk-6.37.0.dev0.dist-info → airbyte_cdk-6.37.0.dev1.dist-info}/LICENSE.txt +0 -0
- {airbyte_cdk-6.37.0.dev0.dist-info → airbyte_cdk-6.37.0.dev1.dist-info}/LICENSE_SHORT +0 -0
- {airbyte_cdk-6.37.0.dev0.dist-info → airbyte_cdk-6.37.0.dev1.dist-info}/WHEEL +0 -0
- {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
|
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
|
-
|
499
|
-
|
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
|
-
|
505
|
-
|
506
|
-
|
507
|
-
|
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
|
-
|
512
|
-
|
513
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
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:
|
@@ -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
|
-
|
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.
|
129
|
-
|
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
|
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
|
260
|
-
|
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[
|
281
|
-
|
282
|
-
|
283
|
-
|
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[
|
228
|
+
self._semaphore_per_partition[self._to_partition_key(partition.partition)].release()
|
298
229
|
if is_last_slice:
|
299
|
-
self._finished_partitions.add(
|
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
|
330
|
-
partition_key
|
331
|
-
|
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:
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
2107
|
-
|
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 =
|
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=
|
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,
|
@@ -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=
|
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=
|
15
|
-
airbyte_cdk/connector_builder/test_reader/message_grouper.py,sha256=
|
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=
|
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=
|
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=
|
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=
|
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=
|
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=
|
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=
|
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=
|
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=
|
123
|
-
airbyte_cdk/sources/declarative/partition_routers/__init__.py,sha256=
|
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=
|
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=
|
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.
|
364
|
-
airbyte_cdk-6.37.0.
|
365
|
-
airbyte_cdk-6.37.0.
|
366
|
-
airbyte_cdk-6.37.0.
|
367
|
-
airbyte_cdk-6.37.0.
|
368
|
-
airbyte_cdk-6.37.0.
|
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,,
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|