airbyte-cdk 6.37.0.dev1__py3-none-any.whl → 6.37.2__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 +16 -14
- airbyte_cdk/connector_builder/test_reader/helpers.py +120 -22
- airbyte_cdk/connector_builder/test_reader/message_grouper.py +16 -3
- airbyte_cdk/connector_builder/test_reader/types.py +9 -1
- airbyte_cdk/sources/declarative/auth/token_provider.py +1 -0
- airbyte_cdk/sources/declarative/concurrent_declarative_source.py +43 -7
- airbyte_cdk/sources/declarative/datetime/datetime_parser.py +7 -1
- airbyte_cdk/sources/declarative/declarative_component_schema.yaml +77 -48
- airbyte_cdk/sources/declarative/decoders/composite_raw_decoder.py +13 -2
- airbyte_cdk/sources/declarative/extractors/response_to_file_extractor.py +1 -0
- airbyte_cdk/sources/declarative/incremental/concurrent_partition_cursor.py +83 -17
- airbyte_cdk/sources/declarative/interpolation/macros.py +2 -0
- airbyte_cdk/sources/declarative/models/declarative_component_schema.py +37 -50
- airbyte_cdk/sources/declarative/parsers/custom_code_compiler.py +18 -4
- airbyte_cdk/sources/declarative/parsers/model_to_component_factory.py +171 -70
- airbyte_cdk/sources/declarative/partition_routers/__init__.py +0 -4
- airbyte_cdk/sources/declarative/requesters/README.md +5 -5
- airbyte_cdk/sources/declarative/requesters/http_job_repository.py +60 -17
- airbyte_cdk/sources/declarative/requesters/http_requester.py +49 -17
- airbyte_cdk/sources/declarative/requesters/paginators/default_paginator.py +25 -4
- airbyte_cdk/sources/declarative/requesters/paginators/no_pagination.py +6 -1
- airbyte_cdk/sources/declarative/requesters/paginators/paginator.py +7 -2
- airbyte_cdk/sources/declarative/requesters/requester.py +7 -1
- airbyte_cdk/sources/declarative/retrievers/async_retriever.py +10 -3
- airbyte_cdk/sources/declarative/retrievers/simple_retriever.py +21 -4
- airbyte_cdk/sources/declarative/transformations/keys_to_snake_transformation.py +2 -2
- airbyte_cdk/sources/http_logger.py +3 -0
- airbyte_cdk/sources/streams/concurrent/state_converters/abstract_stream_state_converter.py +2 -1
- airbyte_cdk/sources/streams/concurrent/state_converters/incrementing_count_stream_state_converter.py +92 -0
- airbyte_cdk/sources/streams/http/error_handlers/default_error_mapping.py +3 -3
- airbyte_cdk/sources/streams/http/requests_native_auth/abstract_oauth.py +1 -0
- airbyte_cdk/sources/types.py +1 -0
- airbyte_cdk/utils/mapping_helpers.py +18 -1
- {airbyte_cdk-6.37.0.dev1.dist-info → airbyte_cdk-6.37.2.dist-info}/METADATA +4 -4
- {airbyte_cdk-6.37.0.dev1.dist-info → airbyte_cdk-6.37.2.dist-info}/RECORD +39 -44
- airbyte_cdk/sources/declarative/partition_routers/grouping_partition_router.py +0 -136
- airbyte_cdk/sources/embedded/__init__.py +0 -3
- airbyte_cdk/sources/embedded/base_integration.py +0 -61
- airbyte_cdk/sources/embedded/catalog.py +0 -57
- airbyte_cdk/sources/embedded/runner.py +0 -57
- airbyte_cdk/sources/embedded/tools.py +0 -27
- {airbyte_cdk-6.37.0.dev1.dist-info → airbyte_cdk-6.37.2.dist-info}/LICENSE.txt +0 -0
- {airbyte_cdk-6.37.0.dev1.dist-info → airbyte_cdk-6.37.2.dist-info}/LICENSE_SHORT +0 -0
- {airbyte_cdk-6.37.0.dev1.dist-info → airbyte_cdk-6.37.2.dist-info}/WHEEL +0 -0
- {airbyte_cdk-6.37.0.dev1.dist-info → airbyte_cdk-6.37.2.dist-info}/entry_points.txt +0 -0
@@ -21,20 +21,6 @@ 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
|
-
|
38
24
|
@dataclass
|
39
25
|
class LogMessage:
|
40
26
|
message: str
|
@@ -46,11 +32,27 @@ class LogMessage:
|
|
46
32
|
@dataclass
|
47
33
|
class AuxiliaryRequest:
|
48
34
|
title: str
|
35
|
+
type: str
|
49
36
|
description: str
|
50
37
|
request: HttpRequest
|
51
38
|
response: HttpResponse
|
52
39
|
|
53
40
|
|
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
|
+
|
54
56
|
@dataclass
|
55
57
|
class StreamRead(object):
|
56
58
|
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 LOG_MESSAGES_OUTPUT_TYPE
|
31
|
+
from .types import ASYNC_AUXILIARY_REQUEST_TYPES, LOG_MESSAGES_OUTPUT_TYPE
|
32
32
|
|
33
33
|
# -------
|
34
34
|
# Parsers
|
@@ -226,7 +226,8 @@ 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)
|
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
|
230
231
|
)
|
231
232
|
)
|
232
233
|
|
@@ -330,6 +331,10 @@ def is_auxiliary_http_request(message: Optional[Dict[str, Any]]) -> bool:
|
|
330
331
|
return is_http_log(message) and message.get("http", {}).get("is_auxiliary", False)
|
331
332
|
|
332
333
|
|
334
|
+
def is_async_auxiliary_request(message: AuxiliaryRequest) -> bool:
|
335
|
+
return message.type in ASYNC_AUXILIARY_REQUEST_TYPES
|
336
|
+
|
337
|
+
|
333
338
|
def is_log_message(message: AirbyteMessage) -> bool:
|
334
339
|
"""
|
335
340
|
Determines whether the provided message is of type LOG.
|
@@ -413,6 +418,7 @@ def handle_current_slice(
|
|
413
418
|
current_slice_pages: List[StreamReadPages],
|
414
419
|
current_slice_descriptor: Optional[Dict[str, Any]] = None,
|
415
420
|
latest_state_message: Optional[Dict[str, Any]] = None,
|
421
|
+
auxiliary_requests: Optional[List[AuxiliaryRequest]] = None,
|
416
422
|
) -> StreamReadSlices:
|
417
423
|
"""
|
418
424
|
Handles the current slice by packaging its pages, descriptor, and state into a StreamReadSlices instance.
|
@@ -421,6 +427,7 @@ def handle_current_slice(
|
|
421
427
|
current_slice_pages (List[StreamReadPages]): The pages to be included in the slice.
|
422
428
|
current_slice_descriptor (Optional[Dict[str, Any]]): Descriptor for the current slice, optional.
|
423
429
|
latest_state_message (Optional[Dict[str, Any]]): The latest state message, optional.
|
430
|
+
auxiliary_requests (Optional[List[AuxiliaryRequest]]): The auxiliary requests to include, optional.
|
424
431
|
|
425
432
|
Returns:
|
426
433
|
StreamReadSlices: An object containing the current slice's pages, descriptor, and state.
|
@@ -429,6 +436,7 @@ def handle_current_slice(
|
|
429
436
|
pages=current_slice_pages,
|
430
437
|
slice_descriptor=current_slice_descriptor,
|
431
438
|
state=[latest_state_message] if latest_state_message else [],
|
439
|
+
auxiliary_requests=auxiliary_requests if auxiliary_requests else [],
|
432
440
|
)
|
433
441
|
|
434
442
|
|
@@ -486,29 +494,24 @@ def handle_auxiliary_request(json_message: Dict[str, JsonType]) -> AuxiliaryRequ
|
|
486
494
|
Raises:
|
487
495
|
ValueError: If any of the "airbyte_cdk", "stream", or "http" fields is not a dictionary.
|
488
496
|
"""
|
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
|
-
|
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)
|
500
503
|
|
501
|
-
|
502
|
-
|
503
|
-
|
504
|
-
|
505
|
-
raise ValueError(f"Expected http to be a dict, got {http} of type {type(http)}")
|
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)
|
506
508
|
|
507
509
|
return AuxiliaryRequest(
|
508
|
-
title=
|
509
|
-
|
510
|
-
|
511
|
-
|
510
|
+
title=title,
|
511
|
+
type=request_type,
|
512
|
+
description=description,
|
513
|
+
request=request,
|
514
|
+
response=response,
|
512
515
|
)
|
513
516
|
|
514
517
|
|
@@ -558,7 +561,8 @@ def handle_log_message(
|
|
558
561
|
at_least_one_page_in_group,
|
559
562
|
current_page_request,
|
560
563
|
current_page_response,
|
561
|
-
auxiliary_request
|
564
|
+
auxiliary_request,
|
565
|
+
log_message,
|
562
566
|
)
|
563
567
|
|
564
568
|
|
@@ -589,3 +593,97 @@ def handle_record_message(
|
|
589
593
|
datetime_format_inferrer.accumulate(message.record) # type: ignore
|
590
594
|
|
591
595
|
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,6 +6,7 @@
|
|
6
6
|
from typing import Any, Dict, Iterator, List, Mapping, Optional
|
7
7
|
|
8
8
|
from airbyte_cdk.connector_builder.models import (
|
9
|
+
AuxiliaryRequest,
|
9
10
|
HttpRequest,
|
10
11
|
HttpResponse,
|
11
12
|
StreamReadPages,
|
@@ -24,6 +25,7 @@ from .helpers import (
|
|
24
25
|
handle_current_slice,
|
25
26
|
handle_log_message,
|
26
27
|
handle_record_message,
|
28
|
+
is_async_auxiliary_request,
|
27
29
|
is_config_update_message,
|
28
30
|
is_log_message,
|
29
31
|
is_record_message,
|
@@ -89,6 +91,7 @@ def get_message_groups(
|
|
89
91
|
current_page_request: Optional[HttpRequest] = None
|
90
92
|
current_page_response: Optional[HttpResponse] = None
|
91
93
|
latest_state_message: Optional[Dict[str, Any]] = None
|
94
|
+
slice_auxiliary_requests: List[AuxiliaryRequest] = []
|
92
95
|
|
93
96
|
while records_count < limit and (message := next(messages, None)):
|
94
97
|
json_message = airbyte_message_to_json(message)
|
@@ -106,6 +109,7 @@ def get_message_groups(
|
|
106
109
|
current_slice_pages,
|
107
110
|
current_slice_descriptor,
|
108
111
|
latest_state_message,
|
112
|
+
slice_auxiliary_requests,
|
109
113
|
)
|
110
114
|
current_slice_descriptor = parse_slice_description(message.log.message) # type: ignore
|
111
115
|
current_slice_pages = []
|
@@ -118,7 +122,8 @@ def get_message_groups(
|
|
118
122
|
at_least_one_page_in_group,
|
119
123
|
current_page_request,
|
120
124
|
current_page_response,
|
121
|
-
|
125
|
+
auxiliary_request,
|
126
|
+
log_message,
|
122
127
|
) = handle_log_message(
|
123
128
|
message,
|
124
129
|
json_message,
|
@@ -126,8 +131,15 @@ def get_message_groups(
|
|
126
131
|
current_page_request,
|
127
132
|
current_page_response,
|
128
133
|
)
|
129
|
-
|
130
|
-
|
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
|
131
143
|
elif is_trace_with_error(message):
|
132
144
|
if message.trace is not None:
|
133
145
|
yield message.trace
|
@@ -157,4 +169,5 @@ def get_message_groups(
|
|
157
169
|
current_slice_pages,
|
158
170
|
current_slice_descriptor,
|
159
171
|
latest_state_message,
|
172
|
+
slice_auxiliary_requests,
|
160
173
|
)
|
@@ -71,5 +71,13 @@ LOG_MESSAGES_OUTPUT_TYPE = tuple[
|
|
71
71
|
bool,
|
72
72
|
HttpRequest | None,
|
73
73
|
HttpResponse | None,
|
74
|
-
AuxiliaryRequest |
|
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",
|
75
83
|
]
|
@@ -31,6 +31,9 @@ from airbyte_cdk.sources.declarative.models.declarative_component_schema import
|
|
31
31
|
from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
|
32
32
|
DatetimeBasedCursor as DatetimeBasedCursorModel,
|
33
33
|
)
|
34
|
+
from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
|
35
|
+
IncrementingCountCursor as IncrementingCountCursorModel,
|
36
|
+
)
|
34
37
|
from airbyte_cdk.sources.declarative.parsers.model_to_component_factory import (
|
35
38
|
ModelToComponentFactory,
|
36
39
|
)
|
@@ -44,6 +47,7 @@ from airbyte_cdk.sources.declarative.types import ConnectionDefinition
|
|
44
47
|
from airbyte_cdk.sources.source import TState
|
45
48
|
from airbyte_cdk.sources.streams import Stream
|
46
49
|
from airbyte_cdk.sources.streams.concurrent.abstract_stream import AbstractStream
|
50
|
+
from airbyte_cdk.sources.streams.concurrent.abstract_stream_facade import AbstractStreamFacade
|
47
51
|
from airbyte_cdk.sources.streams.concurrent.availability_strategy import (
|
48
52
|
AlwaysAvailableAvailabilityStrategy,
|
49
53
|
)
|
@@ -118,6 +122,12 @@ class ConcurrentDeclarativeSource(ManifestDeclarativeSource, Generic[TState]):
|
|
118
122
|
message_repository=self.message_repository,
|
119
123
|
)
|
120
124
|
|
125
|
+
# TODO: Remove this. This property is necessary to safely migrate Stripe during the transition state.
|
126
|
+
@property
|
127
|
+
def is_partially_declarative(self) -> bool:
|
128
|
+
"""This flag used to avoid unexpected AbstractStreamFacade processing as concurrent streams."""
|
129
|
+
return False
|
130
|
+
|
121
131
|
def read(
|
122
132
|
self,
|
123
133
|
logger: logging.Logger,
|
@@ -215,7 +225,7 @@ class ConcurrentDeclarativeSource(ManifestDeclarativeSource, Generic[TState]):
|
|
215
225
|
and not incremental_sync_component_definition
|
216
226
|
)
|
217
227
|
|
218
|
-
if self.
|
228
|
+
if self._is_concurrent_cursor_incremental_without_partition_routing(
|
219
229
|
declarative_stream, incremental_sync_component_definition
|
220
230
|
):
|
221
231
|
stream_state = self._connector_state_manager.get_stream_state(
|
@@ -247,15 +257,26 @@ class ConcurrentDeclarativeSource(ManifestDeclarativeSource, Generic[TState]):
|
|
247
257
|
stream_slicer=declarative_stream.retriever.stream_slicer,
|
248
258
|
)
|
249
259
|
else:
|
250
|
-
|
251
|
-
|
260
|
+
if (
|
261
|
+
incremental_sync_component_definition
|
262
|
+
and incremental_sync_component_definition.get("type")
|
263
|
+
== IncrementingCountCursorModel.__name__
|
264
|
+
):
|
265
|
+
cursor = self._constructor.create_concurrent_cursor_from_incrementing_count_cursor(
|
266
|
+
model_type=IncrementingCountCursorModel,
|
267
|
+
component_definition=incremental_sync_component_definition, # type: ignore # Not None because of the if condition above
|
268
|
+
stream_name=declarative_stream.name,
|
269
|
+
stream_namespace=declarative_stream.namespace,
|
270
|
+
config=config or {},
|
271
|
+
)
|
272
|
+
else:
|
273
|
+
cursor = self._constructor.create_concurrent_cursor_from_datetime_based_cursor(
|
252
274
|
model_type=DatetimeBasedCursorModel,
|
253
275
|
component_definition=incremental_sync_component_definition, # type: ignore # Not None because of the if condition above
|
254
276
|
stream_name=declarative_stream.name,
|
255
277
|
stream_namespace=declarative_stream.namespace,
|
256
278
|
config=config or {},
|
257
279
|
)
|
258
|
-
)
|
259
280
|
partition_generator = StreamSlicerPartitionGenerator(
|
260
281
|
partition_factory=DeclarativePartitionFactory(
|
261
282
|
declarative_stream.name,
|
@@ -369,12 +390,20 @@ class ConcurrentDeclarativeSource(ManifestDeclarativeSource, Generic[TState]):
|
|
369
390
|
)
|
370
391
|
else:
|
371
392
|
synchronous_streams.append(declarative_stream)
|
393
|
+
# TODO: Remove this. This check is necessary to safely migrate Stripe during the transition state.
|
394
|
+
# Condition below needs to ensure that concurrent support is not lost for sources that already support
|
395
|
+
# it before migration, but now are only partially migrated to declarative implementation (e.g., Stripe).
|
396
|
+
elif (
|
397
|
+
isinstance(declarative_stream, AbstractStreamFacade)
|
398
|
+
and self.is_partially_declarative
|
399
|
+
):
|
400
|
+
concurrent_streams.append(declarative_stream.get_underlying_stream())
|
372
401
|
else:
|
373
402
|
synchronous_streams.append(declarative_stream)
|
374
403
|
|
375
404
|
return concurrent_streams, synchronous_streams
|
376
405
|
|
377
|
-
def
|
406
|
+
def _is_concurrent_cursor_incremental_without_partition_routing(
|
378
407
|
self,
|
379
408
|
declarative_stream: DeclarativeStream,
|
380
409
|
incremental_sync_component_definition: Mapping[str, Any] | None,
|
@@ -382,11 +411,18 @@ class ConcurrentDeclarativeSource(ManifestDeclarativeSource, Generic[TState]):
|
|
382
411
|
return (
|
383
412
|
incremental_sync_component_definition is not None
|
384
413
|
and bool(incremental_sync_component_definition)
|
385
|
-
and
|
386
|
-
|
414
|
+
and (
|
415
|
+
incremental_sync_component_definition.get("type", "")
|
416
|
+
in (DatetimeBasedCursorModel.__name__, IncrementingCountCursorModel.__name__)
|
417
|
+
)
|
387
418
|
and hasattr(declarative_stream.retriever, "stream_slicer")
|
388
419
|
and (
|
389
420
|
isinstance(declarative_stream.retriever.stream_slicer, DatetimeBasedCursor)
|
421
|
+
# IncrementingCountCursorModel is hardcoded to be of type DatetimeBasedCursor
|
422
|
+
# add isintance check here if we want to create a Declarative IncrementingCountCursor
|
423
|
+
# or isinstance(
|
424
|
+
# declarative_stream.retriever.stream_slicer, IncrementingCountCursor
|
425
|
+
# )
|
390
426
|
or isinstance(declarative_stream.retriever.stream_slicer, AsyncJobPartitionRouter)
|
391
427
|
)
|
392
428
|
)
|
@@ -31,7 +31,8 @@ class DatetimeParser:
|
|
31
31
|
return datetime.datetime.fromtimestamp(float(date), tz=datetime.timezone.utc)
|
32
32
|
elif format == "%ms":
|
33
33
|
return self._UNIX_EPOCH + datetime.timedelta(milliseconds=int(date))
|
34
|
-
|
34
|
+
elif "%_ms" in format:
|
35
|
+
format = format.replace("%_ms", "%f")
|
35
36
|
parsed_datetime = datetime.datetime.strptime(str(date), format)
|
36
37
|
if self._is_naive(parsed_datetime):
|
37
38
|
return parsed_datetime.replace(tzinfo=datetime.timezone.utc)
|
@@ -48,6 +49,11 @@ class DatetimeParser:
|
|
48
49
|
if format == "%ms":
|
49
50
|
# timstamp() returns a float representing the number of seconds since the unix epoch
|
50
51
|
return str(int(dt.timestamp() * 1000))
|
52
|
+
if "%_ms" in format:
|
53
|
+
_format = format.replace("%_ms", "%f")
|
54
|
+
milliseconds = int(dt.microsecond / 1000)
|
55
|
+
formatted_dt = dt.strftime(_format).replace(dt.strftime("%f"), "%03d" % milliseconds)
|
56
|
+
return formatted_dt
|
51
57
|
else:
|
52
58
|
return dt.strftime(format)
|
53
59
|
|