airbyte-cdk 6.37.0.dev1__py3-none-any.whl → 6.37.1__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.
Potentially problematic release.
This version of airbyte-cdk might be problematic. Click here for more details.
- 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 +67 -46
- 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 +30 -45
- 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 +7 -1
- airbyte_cdk/sources/declarative/retrievers/async_retriever.py +10 -3
- 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/requests_native_auth/abstract_oauth.py +1 -0
- {airbyte_cdk-6.37.0.dev1.dist-info → airbyte_cdk-6.37.1.dist-info}/METADATA +2 -2
- {airbyte_cdk-6.37.0.dev1.dist-info → airbyte_cdk-6.37.1.dist-info}/RECORD +31 -31
- airbyte_cdk/sources/declarative/partition_routers/grouping_partition_router.py +0 -136
- {airbyte_cdk-6.37.0.dev1.dist-info → airbyte_cdk-6.37.1.dist-info}/LICENSE.txt +0 -0
- {airbyte_cdk-6.37.0.dev1.dist-info → airbyte_cdk-6.37.1.dist-info}/LICENSE_SHORT +0 -0
- {airbyte_cdk-6.37.0.dev1.dist-info → airbyte_cdk-6.37.1.dist-info}/WHEEL +0 -0
- {airbyte_cdk-6.37.0.dev1.dist-info → airbyte_cdk-6.37.1.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 |  |