airbyte-cdk 6.34.1.dev0__py3-none-any.whl → 6.34.1.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.
Potentially problematic release.
This version of airbyte-cdk might be problematic. Click here for more details.
- airbyte_cdk/connector_builder/connector_builder_handler.py +16 -12
- airbyte_cdk/connector_builder/test_reader/__init__.py +7 -0
- airbyte_cdk/connector_builder/test_reader/helpers.py +591 -0
- airbyte_cdk/connector_builder/test_reader/message_grouper.py +160 -0
- airbyte_cdk/connector_builder/test_reader/reader.py +441 -0
- airbyte_cdk/connector_builder/test_reader/types.py +75 -0
- airbyte_cdk/sources/declarative/async_job/job_orchestrator.py +7 -7
- airbyte_cdk/sources/declarative/auth/jwt.py +17 -11
- airbyte_cdk/sources/declarative/auth/oauth.py +6 -1
- airbyte_cdk/sources/declarative/auth/token.py +3 -8
- airbyte_cdk/sources/declarative/concurrent_declarative_source.py +30 -79
- airbyte_cdk/sources/declarative/declarative_component_schema.yaml +203 -100
- airbyte_cdk/sources/declarative/declarative_stream.py +3 -1
- airbyte_cdk/sources/declarative/decoders/__init__.py +0 -4
- airbyte_cdk/sources/declarative/decoders/composite_raw_decoder.py +7 -2
- airbyte_cdk/sources/declarative/decoders/json_decoder.py +12 -58
- airbyte_cdk/sources/declarative/extractors/record_selector.py +12 -3
- airbyte_cdk/sources/declarative/incremental/concurrent_partition_cursor.py +56 -25
- airbyte_cdk/sources/declarative/incremental/datetime_based_cursor.py +12 -6
- airbyte_cdk/sources/declarative/incremental/global_substream_cursor.py +6 -2
- airbyte_cdk/sources/declarative/interpolation/jinja.py +13 -0
- airbyte_cdk/sources/declarative/manifest_declarative_source.py +9 -0
- airbyte_cdk/sources/declarative/models/declarative_component_schema.py +150 -41
- airbyte_cdk/sources/declarative/parsers/model_to_component_factory.py +234 -84
- airbyte_cdk/sources/declarative/partition_routers/async_job_partition_router.py +5 -5
- airbyte_cdk/sources/declarative/partition_routers/list_partition_router.py +4 -2
- airbyte_cdk/sources/declarative/partition_routers/substream_partition_router.py +26 -18
- airbyte_cdk/sources/declarative/requesters/http_requester.py +8 -2
- airbyte_cdk/sources/declarative/requesters/paginators/default_paginator.py +16 -5
- airbyte_cdk/sources/declarative/requesters/request_option.py +83 -4
- airbyte_cdk/sources/declarative/requesters/request_options/datetime_based_request_options_provider.py +7 -6
- airbyte_cdk/sources/declarative/requesters/request_options/interpolated_nested_request_input_provider.py +1 -4
- airbyte_cdk/sources/declarative/requesters/request_options/interpolated_request_input_provider.py +0 -3
- airbyte_cdk/sources/declarative/requesters/request_options/interpolated_request_options_provider.py +2 -47
- airbyte_cdk/sources/declarative/retrievers/async_retriever.py +6 -12
- airbyte_cdk/sources/declarative/retrievers/simple_retriever.py +4 -3
- airbyte_cdk/sources/declarative/transformations/add_fields.py +4 -4
- airbyte_cdk/sources/file_based/config/abstract_file_based_spec.py +2 -1
- airbyte_cdk/sources/file_based/config/validate_config_transfer_modes.py +81 -0
- airbyte_cdk/sources/file_based/file_based_source.py +70 -37
- airbyte_cdk/sources/file_based/file_based_stream_reader.py +107 -12
- airbyte_cdk/sources/file_based/stream/__init__.py +10 -1
- airbyte_cdk/sources/file_based/stream/identities_stream.py +47 -0
- airbyte_cdk/sources/file_based/stream/permissions_file_based_stream.py +85 -0
- airbyte_cdk/sources/specs/transfer_modes.py +26 -0
- airbyte_cdk/sources/streams/call_rate.py +185 -47
- airbyte_cdk/sources/streams/http/http.py +1 -2
- airbyte_cdk/sources/streams/http/requests_native_auth/abstract_oauth.py +217 -56
- airbyte_cdk/sources/streams/http/requests_native_auth/oauth.py +144 -73
- airbyte_cdk/sources/streams/permissions/identities_stream.py +75 -0
- airbyte_cdk/test/mock_http/mocker.py +9 -1
- airbyte_cdk/test/mock_http/response.py +6 -3
- airbyte_cdk/utils/datetime_helpers.py +48 -66
- airbyte_cdk/utils/mapping_helpers.py +126 -26
- {airbyte_cdk-6.34.1.dev0.dist-info → airbyte_cdk-6.34.1.dev1.dist-info}/METADATA +1 -1
- {airbyte_cdk-6.34.1.dev0.dist-info → airbyte_cdk-6.34.1.dev1.dist-info}/RECORD +60 -51
- airbyte_cdk/connector_builder/message_grouper.py +0 -448
- {airbyte_cdk-6.34.1.dev0.dist-info → airbyte_cdk-6.34.1.dev1.dist-info}/LICENSE.txt +0 -0
- {airbyte_cdk-6.34.1.dev0.dist-info → airbyte_cdk-6.34.1.dev1.dist-info}/LICENSE_SHORT +0 -0
- {airbyte_cdk-6.34.1.dev0.dist-info → airbyte_cdk-6.34.1.dev1.dist-info}/WHEEL +0 -0
- {airbyte_cdk-6.34.1.dev0.dist-info → airbyte_cdk-6.34.1.dev1.dist-info}/entry_points.txt +0 -0
| @@ -4,7 +4,7 @@ | |
| 4 4 | 
             
            import copy
         | 
| 5 5 | 
             
            import logging
         | 
| 6 6 | 
             
            from dataclasses import InitVar, dataclass
         | 
| 7 | 
            -
            from typing import TYPE_CHECKING, Any, Iterable, List, Mapping, Optional, Union
         | 
| 7 | 
            +
            from typing import TYPE_CHECKING, Any, Iterable, List, Mapping, MutableMapping, Optional, Union
         | 
| 8 8 |  | 
| 9 9 | 
             
            import dpath
         | 
| 10 10 |  | 
| @@ -118,7 +118,7 @@ class SubstreamPartitionRouter(PartitionRouter): | |
| 118 118 | 
             
                def _get_request_option(
         | 
| 119 119 | 
             
                    self, option_type: RequestOptionType, stream_slice: Optional[StreamSlice]
         | 
| 120 120 | 
             
                ) -> Mapping[str, Any]:
         | 
| 121 | 
            -
                    params = {}
         | 
| 121 | 
            +
                    params: MutableMapping[str, Any] = {}
         | 
| 122 122 | 
             
                    if stream_slice:
         | 
| 123 123 | 
             
                        for parent_config in self.parent_stream_configs:
         | 
| 124 124 | 
             
                            if (
         | 
| @@ -128,13 +128,7 @@ class SubstreamPartitionRouter(PartitionRouter): | |
| 128 128 | 
             
                                key = parent_config.partition_field.eval(self.config)  # type: ignore # partition_field is always casted to an interpolated string
         | 
| 129 129 | 
             
                                value = stream_slice.get(key)
         | 
| 130 130 | 
             
                                if value:
         | 
| 131 | 
            -
                                    params. | 
| 132 | 
            -
                                        {
         | 
| 133 | 
            -
                                            parent_config.request_option.field_name.eval(  # type: ignore [union-attr]
         | 
| 134 | 
            -
                                                config=self.config
         | 
| 135 | 
            -
                                            ): value
         | 
| 136 | 
            -
                                        }
         | 
| 137 | 
            -
                                    )
         | 
| 131 | 
            +
                                    parent_config.request_option.inject_into_request(params, value, self.config)
         | 
| 138 132 | 
             
                    return params
         | 
| 139 133 |  | 
| 140 134 | 
             
                def stream_slices(self) -> Iterable[StreamSlice]:
         | 
| @@ -305,23 +299,33 @@ class SubstreamPartitionRouter(PartitionRouter): | |
| 305 299 |  | 
| 306 300 | 
             
                def _migrate_child_state_to_parent_state(self, stream_state: StreamState) -> StreamState:
         | 
| 307 301 | 
             
                    """
         | 
| 308 | 
            -
                    Migrate the child stream state  | 
| 302 | 
            +
                    Migrate the child or global stream state into the parent stream's state format.
         | 
| 303 | 
            +
             | 
| 304 | 
            +
                    This method converts the child stream state—or, if present, the global state—into a format that is
         | 
| 305 | 
            +
                    compatible with parent streams that use incremental synchronization. The migration occurs only for
         | 
| 306 | 
            +
                    parent streams with incremental dependencies. It filters out per-partition states and retains only the
         | 
| 307 | 
            +
                    global state in the form {cursor_field: cursor_value}.
         | 
| 309 308 |  | 
| 310 | 
            -
                     | 
| 311 | 
            -
             | 
| 312 | 
            -
             | 
| 313 | 
            -
             | 
| 309 | 
            +
                    The method supports multiple input formats:
         | 
| 310 | 
            +
                      - A simple global state, e.g.:
         | 
| 311 | 
            +
                            {"updated_at": "2023-05-27T00:00:00Z"}
         | 
| 312 | 
            +
                      - A state object that contains a "state" key (which is assumed to hold the global state), e.g.:
         | 
| 313 | 
            +
                            {"state": {"updated_at": "2023-05-27T00:00:00Z"}, ...}
         | 
| 314 | 
            +
                        In this case, the migration uses the first value from the "state" dictionary.
         | 
| 315 | 
            +
                      - Any per-partition state formats or other non-simple structures are ignored during migration.
         | 
| 314 316 |  | 
| 315 317 | 
             
                    Args:
         | 
| 316 318 | 
             
                        stream_state (StreamState): The state to migrate. Expected formats include:
         | 
| 317 319 | 
             
                            - {"updated_at": "2023-05-27T00:00:00Z"}
         | 
| 318 | 
            -
                            - {" | 
| 320 | 
            +
                            - {"state": {"updated_at": "2023-05-27T00:00:00Z"}, ...}
         | 
| 321 | 
            +
                              (In this format, only the first global state value is used, and per-partition states are ignored.)
         | 
| 319 322 |  | 
| 320 323 | 
             
                    Returns:
         | 
| 321 324 | 
             
                        StreamState: A migrated state for parent streams in the format:
         | 
| 322 325 | 
             
                            {
         | 
| 323 326 | 
             
                                "parent_stream_name": {"parent_stream_cursor": "2023-05-27T00:00:00Z"}
         | 
| 324 327 | 
             
                            }
         | 
| 328 | 
            +
                        where each parent stream with an incremental dependency is assigned its corresponding cursor value.
         | 
| 325 329 |  | 
| 326 330 | 
             
                    Example:
         | 
| 327 331 | 
             
                        Input: {"updated_at": "2023-05-27T00:00:00Z"}
         | 
| @@ -332,11 +336,15 @@ class SubstreamPartitionRouter(PartitionRouter): | |
| 332 336 | 
             
                    substream_state_values = list(stream_state.values())
         | 
| 333 337 | 
             
                    substream_state = substream_state_values[0] if substream_state_values else {}
         | 
| 334 338 |  | 
| 335 | 
            -
                    # Ignore per-partition states or invalid formats
         | 
| 339 | 
            +
                    # Ignore per-partition states or invalid formats.
         | 
| 336 340 | 
             
                    if isinstance(substream_state, (list, dict)) or len(substream_state_values) != 1:
         | 
| 337 | 
            -
                         | 
| 341 | 
            +
                        # If a global state is present under the key "state", use its first value.
         | 
| 342 | 
            +
                        if "state" in stream_state and isinstance(stream_state["state"], dict):
         | 
| 343 | 
            +
                            substream_state = list(stream_state["state"].values())[0]
         | 
| 344 | 
            +
                        else:
         | 
| 345 | 
            +
                            return {}
         | 
| 338 346 |  | 
| 339 | 
            -
                    #  | 
| 347 | 
            +
                    # Build the parent state for all parent streams with incremental dependencies.
         | 
| 340 348 | 
             
                    parent_state = {}
         | 
| 341 349 | 
             
                    if substream_state:
         | 
| 342 350 | 
             
                        for parent_config in self.parent_stream_configs:
         | 
| @@ -22,6 +22,7 @@ from airbyte_cdk.sources.declarative.requesters.request_options.interpolated_req | |
| 22 22 | 
             
            )
         | 
| 23 23 | 
             
            from airbyte_cdk.sources.declarative.requesters.requester import HttpMethod, Requester
         | 
| 24 24 | 
             
            from airbyte_cdk.sources.message import MessageRepository, NoopMessageRepository
         | 
| 25 | 
            +
            from airbyte_cdk.sources.streams.call_rate import APIBudget
         | 
| 25 26 | 
             
            from airbyte_cdk.sources.streams.http import HttpClient
         | 
| 26 27 | 
             
            from airbyte_cdk.sources.streams.http.error_handlers import ErrorHandler
         | 
| 27 28 | 
             
            from airbyte_cdk.sources.types import Config, StreamSlice, StreamState
         | 
| @@ -55,6 +56,7 @@ class HttpRequester(Requester): | |
| 55 56 | 
             
                http_method: Union[str, HttpMethod] = HttpMethod.GET
         | 
| 56 57 | 
             
                request_options_provider: Optional[InterpolatedRequestOptionsProvider] = None
         | 
| 57 58 | 
             
                error_handler: Optional[ErrorHandler] = None
         | 
| 59 | 
            +
                api_budget: Optional[APIBudget] = None
         | 
| 58 60 | 
             
                disable_retries: bool = False
         | 
| 59 61 | 
             
                message_repository: MessageRepository = NoopMessageRepository()
         | 
| 60 62 | 
             
                use_cache: bool = False
         | 
| @@ -91,6 +93,7 @@ class HttpRequester(Requester): | |
| 91 93 | 
             
                        name=self.name,
         | 
| 92 94 | 
             
                        logger=self.logger,
         | 
| 93 95 | 
             
                        error_handler=self.error_handler,
         | 
| 96 | 
            +
                        api_budget=self.api_budget,
         | 
| 94 97 | 
             
                        authenticator=self._authenticator,
         | 
| 95 98 | 
             
                        use_cache=self.use_cache,
         | 
| 96 99 | 
             
                        backoff_strategy=backoff_strategies,
         | 
| @@ -120,7 +123,6 @@ class HttpRequester(Requester): | |
| 120 123 | 
             
                    next_page_token: Optional[Mapping[str, Any]],
         | 
| 121 124 | 
             
                ) -> str:
         | 
| 122 125 | 
             
                    kwargs = {
         | 
| 123 | 
            -
                        "stream_state": stream_state,
         | 
| 124 126 | 
             
                        "stream_slice": stream_slice,
         | 
| 125 127 | 
             
                        "next_page_token": next_page_token,
         | 
| 126 128 | 
             
                    }
         | 
| @@ -199,6 +201,9 @@ class HttpRequester(Requester): | |
| 199 201 | 
             
                    Raise a ValueError if there's a key collision
         | 
| 200 202 | 
             
                    Returned merged mapping otherwise
         | 
| 201 203 | 
             
                    """
         | 
| 204 | 
            +
             | 
| 205 | 
            +
                    is_body_json = requester_method.__name__ == "get_request_body_json"
         | 
| 206 | 
            +
             | 
| 202 207 | 
             
                    return combine_mappings(
         | 
| 203 208 | 
             
                        [
         | 
| 204 209 | 
             
                            requester_method(
         | 
| @@ -208,7 +213,8 @@ class HttpRequester(Requester): | |
| 208 213 | 
             
                            ),
         | 
| 209 214 | 
             
                            auth_options_method(),
         | 
| 210 215 | 
             
                            extra_options,
         | 
| 211 | 
            -
                        ]
         | 
| 216 | 
            +
                        ],
         | 
| 217 | 
            +
                        allow_same_value_merge=is_body_json,
         | 
| 212 218 | 
             
                    )
         | 
| 213 219 |  | 
| 214 220 | 
             
                def _request_headers(
         | 
| @@ -23,6 +23,9 @@ from airbyte_cdk.sources.declarative.requesters.request_option import ( | |
| 23 23 | 
             
            )
         | 
| 24 24 | 
             
            from airbyte_cdk.sources.declarative.requesters.request_path import RequestPath
         | 
| 25 25 | 
             
            from airbyte_cdk.sources.types import Config, Record, StreamSlice, StreamState
         | 
| 26 | 
            +
            from airbyte_cdk.utils.mapping_helpers import (
         | 
| 27 | 
            +
                _validate_component_request_option_paths,
         | 
| 28 | 
            +
            )
         | 
| 26 29 |  | 
| 27 30 |  | 
| 28 31 | 
             
            @dataclass
         | 
| @@ -113,6 +116,13 @@ class DefaultPaginator(Paginator): | |
| 113 116 | 
             
                    if isinstance(self.url_base, str):
         | 
| 114 117 | 
             
                        self.url_base = InterpolatedString(string=self.url_base, parameters=parameters)
         | 
| 115 118 |  | 
| 119 | 
            +
                    if self.page_token_option and not isinstance(self.page_token_option, RequestPath):
         | 
| 120 | 
            +
                        _validate_component_request_option_paths(
         | 
| 121 | 
            +
                            self.config,
         | 
| 122 | 
            +
                            self.page_size_option,
         | 
| 123 | 
            +
                            self.page_token_option,
         | 
| 124 | 
            +
                        )
         | 
| 125 | 
            +
             | 
| 116 126 | 
             
                def get_initial_token(self) -> Optional[Any]:
         | 
| 117 127 | 
             
                    """
         | 
| 118 128 | 
             
                    Return the page token that should be used for the first request of a stream
         | 
| @@ -187,7 +197,7 @@ class DefaultPaginator(Paginator): | |
| 187 197 | 
             
                def _get_request_options(
         | 
| 188 198 | 
             
                    self, option_type: RequestOptionType, next_page_token: Optional[Mapping[str, Any]]
         | 
| 189 199 | 
             
                ) -> MutableMapping[str, Any]:
         | 
| 190 | 
            -
                    options = {}
         | 
| 200 | 
            +
                    options: MutableMapping[str, Any] = {}
         | 
| 191 201 |  | 
| 192 202 | 
             
                    token = next_page_token.get("next_page_token") if next_page_token else None
         | 
| 193 203 | 
             
                    if (
         | 
| @@ -196,15 +206,16 @@ class DefaultPaginator(Paginator): | |
| 196 206 | 
             
                        and isinstance(self.page_token_option, RequestOption)
         | 
| 197 207 | 
             
                        and self.page_token_option.inject_into == option_type
         | 
| 198 208 | 
             
                    ):
         | 
| 199 | 
            -
                         | 
| 209 | 
            +
                        self.page_token_option.inject_into_request(options, token, self.config)
         | 
| 210 | 
            +
             | 
| 200 211 | 
             
                    if (
         | 
| 201 212 | 
             
                        self.page_size_option
         | 
| 202 213 | 
             
                        and self.pagination_strategy.get_page_size()
         | 
| 203 214 | 
             
                        and self.page_size_option.inject_into == option_type
         | 
| 204 215 | 
             
                    ):
         | 
| 205 | 
            -
                         | 
| 206 | 
            -
             | 
| 207 | 
            -
             | 
| 216 | 
            +
                        page_size = self.pagination_strategy.get_page_size()
         | 
| 217 | 
            +
                        self.page_size_option.inject_into_request(options, page_size, self.config)
         | 
| 218 | 
            +
             | 
| 208 219 | 
             
                    return options
         | 
| 209 220 |  | 
| 210 221 |  | 
| @@ -4,9 +4,10 @@ | |
| 4 4 |  | 
| 5 5 | 
             
            from dataclasses import InitVar, dataclass
         | 
| 6 6 | 
             
            from enum import Enum
         | 
| 7 | 
            -
            from typing import Any, Mapping, Union
         | 
| 7 | 
            +
            from typing import Any, List, Literal, Mapping, MutableMapping, Optional, Union
         | 
| 8 8 |  | 
| 9 9 | 
             
            from airbyte_cdk.sources.declarative.interpolation.interpolated_string import InterpolatedString
         | 
| 10 | 
            +
            from airbyte_cdk.sources.types import Config
         | 
| 10 11 |  | 
| 11 12 |  | 
| 12 13 | 
             
            class RequestOptionType(Enum):
         | 
| @@ -26,13 +27,91 @@ class RequestOption: | |
| 26 27 | 
             
                Describes an option to set on a request
         | 
| 27 28 |  | 
| 28 29 | 
             
                Attributes:
         | 
| 29 | 
            -
                    field_name (str): Describes the name of the parameter to inject
         | 
| 30 | 
            +
                    field_name (str): Describes the name of the parameter to inject. Mutually exclusive with field_path.
         | 
| 31 | 
            +
                    field_path (list(str)): Describes the path to a nested field as a list of field names.
         | 
| 32 | 
            +
                      Only valid for body_json injection type, and mutually exclusive with field_name.
         | 
| 30 33 | 
             
                    inject_into (RequestOptionType): Describes where in the HTTP request to inject the parameter
         | 
| 31 34 | 
             
                """
         | 
| 32 35 |  | 
| 33 | 
            -
                field_name: Union[InterpolatedString, str]
         | 
| 34 36 | 
             
                inject_into: RequestOptionType
         | 
| 35 37 | 
             
                parameters: InitVar[Mapping[str, Any]]
         | 
| 38 | 
            +
                field_name: Optional[Union[InterpolatedString, str]] = None
         | 
| 39 | 
            +
                field_path: Optional[List[Union[InterpolatedString, str]]] = None
         | 
| 36 40 |  | 
| 37 41 | 
             
                def __post_init__(self, parameters: Mapping[str, Any]) -> None:
         | 
| 38 | 
            -
                     | 
| 42 | 
            +
                    # Validate inputs. We should expect either field_name or field_path, but not both
         | 
| 43 | 
            +
                    if self.field_name is None and self.field_path is None:
         | 
| 44 | 
            +
                        raise ValueError("RequestOption requires either a field_name or field_path")
         | 
| 45 | 
            +
             | 
| 46 | 
            +
                    if self.field_name is not None and self.field_path is not None:
         | 
| 47 | 
            +
                        raise ValueError(
         | 
| 48 | 
            +
                            "Only one of field_name or field_path can be provided to RequestOption"
         | 
| 49 | 
            +
                        )
         | 
| 50 | 
            +
             | 
| 51 | 
            +
                    # Nested field injection is only supported for body JSON injection
         | 
| 52 | 
            +
                    if self.field_path is not None and self.inject_into != RequestOptionType.body_json:
         | 
| 53 | 
            +
                        raise ValueError(
         | 
| 54 | 
            +
                            "Nested field injection is only supported for body JSON injection. Please use a top-level field_name for other injection types."
         | 
| 55 | 
            +
                        )
         | 
| 56 | 
            +
             | 
| 57 | 
            +
                    # Convert field_name and field_path into InterpolatedString objects if they are strings
         | 
| 58 | 
            +
                    if self.field_name is not None:
         | 
| 59 | 
            +
                        self.field_name = InterpolatedString.create(self.field_name, parameters=parameters)
         | 
| 60 | 
            +
                    elif self.field_path is not None:
         | 
| 61 | 
            +
                        self.field_path = [
         | 
| 62 | 
            +
                            InterpolatedString.create(segment, parameters=parameters)
         | 
| 63 | 
            +
                            for segment in self.field_path
         | 
| 64 | 
            +
                        ]
         | 
| 65 | 
            +
             | 
| 66 | 
            +
                @property
         | 
| 67 | 
            +
                def _is_field_path(self) -> bool:
         | 
| 68 | 
            +
                    """Returns whether this option is a field path (ie, a nested field)"""
         | 
| 69 | 
            +
                    return self.field_path is not None
         | 
| 70 | 
            +
             | 
| 71 | 
            +
                def inject_into_request(
         | 
| 72 | 
            +
                    self,
         | 
| 73 | 
            +
                    target: MutableMapping[str, Any],
         | 
| 74 | 
            +
                    value: Any,
         | 
| 75 | 
            +
                    config: Config,
         | 
| 76 | 
            +
                ) -> None:
         | 
| 77 | 
            +
                    """
         | 
| 78 | 
            +
                    Inject a request option value into a target request structure using either field_name or field_path.
         | 
| 79 | 
            +
                    For non-body-json injection, only top-level field names are supported.
         | 
| 80 | 
            +
                    For body-json injection, both field names and nested field paths are supported.
         | 
| 81 | 
            +
             | 
| 82 | 
            +
                    Args:
         | 
| 83 | 
            +
                        target: The request structure to inject the value into
         | 
| 84 | 
            +
                        value: The value to inject
         | 
| 85 | 
            +
                        config: The config object to use for interpolation
         | 
| 86 | 
            +
                    """
         | 
| 87 | 
            +
                    if self._is_field_path:
         | 
| 88 | 
            +
                        if self.inject_into != RequestOptionType.body_json:
         | 
| 89 | 
            +
                            raise ValueError(
         | 
| 90 | 
            +
                                "Nested field injection is only supported for body JSON injection. Please use a top-level field_name for other injection types."
         | 
| 91 | 
            +
                            )
         | 
| 92 | 
            +
             | 
| 93 | 
            +
                        assert self.field_path is not None  # for type checker
         | 
| 94 | 
            +
                        current = target
         | 
| 95 | 
            +
                        # Convert path segments into strings, evaluating any interpolated segments
         | 
| 96 | 
            +
                        # Example: ["data", "{{ config[user_type] }}", "id"] -> ["data", "admin", "id"]
         | 
| 97 | 
            +
                        *path_parts, final_key = [
         | 
| 98 | 
            +
                            str(
         | 
| 99 | 
            +
                                segment.eval(config=config)
         | 
| 100 | 
            +
                                if isinstance(segment, InterpolatedString)
         | 
| 101 | 
            +
                                else segment
         | 
| 102 | 
            +
                            )
         | 
| 103 | 
            +
                            for segment in self.field_path
         | 
| 104 | 
            +
                        ]
         | 
| 105 | 
            +
             | 
| 106 | 
            +
                        # Build a nested dictionary structure and set the final value at the deepest level
         | 
| 107 | 
            +
                        for part in path_parts:
         | 
| 108 | 
            +
                            current = current.setdefault(part, {})
         | 
| 109 | 
            +
                        current[final_key] = value
         | 
| 110 | 
            +
                    else:
         | 
| 111 | 
            +
                        # For non-nested fields, evaluate the field name if it's an interpolated string
         | 
| 112 | 
            +
                        key = (
         | 
| 113 | 
            +
                            self.field_name.eval(config=config)
         | 
| 114 | 
            +
                            if isinstance(self.field_name, InterpolatedString)
         | 
| 115 | 
            +
                            else self.field_name
         | 
| 116 | 
            +
                        )
         | 
| 117 | 
            +
                        target[str(key)] = value
         | 
| @@ -80,12 +80,13 @@ class DatetimeBasedRequestOptionsProvider(RequestOptionsProvider): | |
| 80 80 | 
             
                    options: MutableMapping[str, Any] = {}
         | 
| 81 81 | 
             
                    if not stream_slice:
         | 
| 82 82 | 
             
                        return options
         | 
| 83 | 
            +
             | 
| 83 84 | 
             
                    if self.start_time_option and self.start_time_option.inject_into == option_type:
         | 
| 84 | 
            -
                         | 
| 85 | 
            -
             | 
| 86 | 
            -
             | 
| 85 | 
            +
                        start_time_value = stream_slice.get(self._partition_field_start.eval(self.config))
         | 
| 86 | 
            +
                        self.start_time_option.inject_into_request(options, start_time_value, self.config)
         | 
| 87 | 
            +
             | 
| 87 88 | 
             
                    if self.end_time_option and self.end_time_option.inject_into == option_type:
         | 
| 88 | 
            -
                         | 
| 89 | 
            -
             | 
| 90 | 
            -
             | 
| 89 | 
            +
                        end_time_value = stream_slice.get(self._partition_field_end.eval(self.config))
         | 
| 90 | 
            +
                        self.end_time_option.inject_into_request(options, end_time_value, self.config)
         | 
| 91 | 
            +
             | 
| 91 92 | 
             
                    return options
         | 
| @@ -10,7 +10,7 @@ from airbyte_cdk.sources.declarative.interpolation.interpolated_nested_mapping i | |
| 10 10 | 
             
                NestedMapping,
         | 
| 11 11 | 
             
            )
         | 
| 12 12 | 
             
            from airbyte_cdk.sources.declarative.interpolation.interpolated_string import InterpolatedString
         | 
| 13 | 
            -
            from airbyte_cdk.sources.types import Config, StreamSlice | 
| 13 | 
            +
            from airbyte_cdk.sources.types import Config, StreamSlice
         | 
| 14 14 |  | 
| 15 15 |  | 
| 16 16 | 
             
            @dataclass
         | 
| @@ -42,20 +42,17 @@ class InterpolatedNestedRequestInputProvider: | |
| 42 42 |  | 
| 43 43 | 
             
                def eval_request_inputs(
         | 
| 44 44 | 
             
                    self,
         | 
| 45 | 
            -
                    stream_state: Optional[StreamState] = None,
         | 
| 46 45 | 
             
                    stream_slice: Optional[StreamSlice] = None,
         | 
| 47 46 | 
             
                    next_page_token: Optional[Mapping[str, Any]] = None,
         | 
| 48 47 | 
             
                ) -> Mapping[str, Any]:
         | 
| 49 48 | 
             
                    """
         | 
| 50 49 | 
             
                    Returns the request inputs to set on an outgoing HTTP request
         | 
| 51 50 |  | 
| 52 | 
            -
                    :param stream_state: The stream state
         | 
| 53 51 | 
             
                    :param stream_slice: The stream slice
         | 
| 54 52 | 
             
                    :param next_page_token: The pagination token
         | 
| 55 53 | 
             
                    :return: The request inputs to set on an outgoing HTTP request
         | 
| 56 54 | 
             
                    """
         | 
| 57 55 | 
             
                    kwargs = {
         | 
| 58 | 
            -
                        "stream_state": stream_state,
         | 
| 59 56 | 
             
                        "stream_slice": stream_slice,
         | 
| 60 57 | 
             
                        "next_page_token": next_page_token,
         | 
| 61 58 | 
             
                    }
         | 
    
        airbyte_cdk/sources/declarative/requesters/request_options/interpolated_request_input_provider.py
    CHANGED
    
    | @@ -37,7 +37,6 @@ class InterpolatedRequestInputProvider: | |
| 37 37 |  | 
| 38 38 | 
             
                def eval_request_inputs(
         | 
| 39 39 | 
             
                    self,
         | 
| 40 | 
            -
                    stream_state: Optional[StreamState] = None,
         | 
| 41 40 | 
             
                    stream_slice: Optional[StreamSlice] = None,
         | 
| 42 41 | 
             
                    next_page_token: Optional[Mapping[str, Any]] = None,
         | 
| 43 42 | 
             
                    valid_key_types: Optional[Tuple[Type[Any]]] = None,
         | 
| @@ -46,7 +45,6 @@ class InterpolatedRequestInputProvider: | |
| 46 45 | 
             
                    """
         | 
| 47 46 | 
             
                    Returns the request inputs to set on an outgoing HTTP request
         | 
| 48 47 |  | 
| 49 | 
            -
                    :param stream_state: The stream state
         | 
| 50 48 | 
             
                    :param stream_slice: The stream slice
         | 
| 51 49 | 
             
                    :param next_page_token: The pagination token
         | 
| 52 50 | 
             
                    :param valid_key_types: A tuple of types that the interpolator should allow
         | 
| @@ -54,7 +52,6 @@ class InterpolatedRequestInputProvider: | |
| 54 52 | 
             
                    :return: The request inputs to set on an outgoing HTTP request
         | 
| 55 53 | 
             
                    """
         | 
| 56 54 | 
             
                    kwargs = {
         | 
| 57 | 
            -
                        "stream_state": stream_state,
         | 
| 58 55 | 
             
                        "stream_slice": stream_slice,
         | 
| 59 56 | 
             
                        "next_page_token": next_page_token,
         | 
| 60 57 | 
             
                    }
         | 
    
        airbyte_cdk/sources/declarative/requesters/request_options/interpolated_request_options_provider.py
    CHANGED
    
    | @@ -5,8 +5,6 @@ | |
| 5 5 | 
             
            from dataclasses import InitVar, dataclass, field
         | 
| 6 6 | 
             
            from typing import Any, Mapping, MutableMapping, Optional, Union
         | 
| 7 7 |  | 
| 8 | 
            -
            from typing_extensions import deprecated
         | 
| 9 | 
            -
             | 
| 10 8 | 
             
            from airbyte_cdk.sources.declarative.interpolation.interpolated_nested_mapping import NestedMapping
         | 
| 11 9 | 
             
            from airbyte_cdk.sources.declarative.requesters.request_options.interpolated_nested_request_input_provider import (
         | 
| 12 10 | 
             
                InterpolatedNestedRequestInputProvider,
         | 
| @@ -17,7 +15,6 @@ from airbyte_cdk.sources.declarative.requesters.request_options.interpolated_req | |
| 17 15 | 
             
            from airbyte_cdk.sources.declarative.requesters.request_options.request_options_provider import (
         | 
| 18 16 | 
             
                RequestOptionsProvider,
         | 
| 19 17 | 
             
            )
         | 
| 20 | 
            -
            from airbyte_cdk.sources.source import ExperimentalClassWarning
         | 
| 21 18 | 
             
            from airbyte_cdk.sources.types import Config, StreamSlice, StreamState
         | 
| 22 19 |  | 
| 23 20 | 
             
            RequestInput = Union[str, Mapping[str, str]]
         | 
| @@ -80,7 +77,6 @@ class InterpolatedRequestOptionsProvider(RequestOptionsProvider): | |
| 80 77 | 
             
                    next_page_token: Optional[Mapping[str, Any]] = None,
         | 
| 81 78 | 
             
                ) -> MutableMapping[str, Any]:
         | 
| 82 79 | 
             
                    interpolated_value = self._parameter_interpolator.eval_request_inputs(
         | 
| 83 | 
            -
                        stream_state,
         | 
| 84 80 | 
             
                        stream_slice,
         | 
| 85 81 | 
             
                        next_page_token,
         | 
| 86 82 | 
             
                        valid_key_types=(str,),
         | 
| @@ -97,9 +93,7 @@ class InterpolatedRequestOptionsProvider(RequestOptionsProvider): | |
| 97 93 | 
             
                    stream_slice: Optional[StreamSlice] = None,
         | 
| 98 94 | 
             
                    next_page_token: Optional[Mapping[str, Any]] = None,
         | 
| 99 95 | 
             
                ) -> Mapping[str, Any]:
         | 
| 100 | 
            -
                    return self._headers_interpolator.eval_request_inputs(
         | 
| 101 | 
            -
                        stream_state, stream_slice, next_page_token
         | 
| 102 | 
            -
                    )
         | 
| 96 | 
            +
                    return self._headers_interpolator.eval_request_inputs(stream_slice, next_page_token)
         | 
| 103 97 |  | 
| 104 98 | 
             
                def get_request_body_data(
         | 
| 105 99 | 
             
                    self,
         | 
| @@ -109,7 +103,6 @@ class InterpolatedRequestOptionsProvider(RequestOptionsProvider): | |
| 109 103 | 
             
                    next_page_token: Optional[Mapping[str, Any]] = None,
         | 
| 110 104 | 
             
                ) -> Union[Mapping[str, Any], str]:
         | 
| 111 105 | 
             
                    return self._body_data_interpolator.eval_request_inputs(
         | 
| 112 | 
            -
                        stream_state,
         | 
| 113 106 | 
             
                        stream_slice,
         | 
| 114 107 | 
             
                        next_page_token,
         | 
| 115 108 | 
             
                        valid_key_types=(str,),
         | 
| @@ -123,42 +116,4 @@ class InterpolatedRequestOptionsProvider(RequestOptionsProvider): | |
| 123 116 | 
             
                    stream_slice: Optional[StreamSlice] = None,
         | 
| 124 117 | 
             
                    next_page_token: Optional[Mapping[str, Any]] = None,
         | 
| 125 118 | 
             
                ) -> Mapping[str, Any]:
         | 
| 126 | 
            -
                    return self._body_json_interpolator.eval_request_inputs(
         | 
| 127 | 
            -
                        stream_state, stream_slice, next_page_token
         | 
| 128 | 
            -
                    )
         | 
| 129 | 
            -
             | 
| 130 | 
            -
                @deprecated(
         | 
| 131 | 
            -
                    "This class is temporary and used to incrementally deliver low-code to concurrent",
         | 
| 132 | 
            -
                    category=ExperimentalClassWarning,
         | 
| 133 | 
            -
                )
         | 
| 134 | 
            -
                def request_options_contain_stream_state(self) -> bool:
         | 
| 135 | 
            -
                    """
         | 
| 136 | 
            -
                    Temporary helper method used as we move low-code streams to the concurrent framework. This method determines if
         | 
| 137 | 
            -
                    the InterpolatedRequestOptionsProvider has is a dependency on a non-thread safe interpolation context such as
         | 
| 138 | 
            -
                    stream_state.
         | 
| 139 | 
            -
                    """
         | 
| 140 | 
            -
             | 
| 141 | 
            -
                    return (
         | 
| 142 | 
            -
                        self._check_if_interpolation_uses_stream_state(self.request_parameters)
         | 
| 143 | 
            -
                        or self._check_if_interpolation_uses_stream_state(self.request_headers)
         | 
| 144 | 
            -
                        or self._check_if_interpolation_uses_stream_state(self.request_body_data)
         | 
| 145 | 
            -
                        or self._check_if_interpolation_uses_stream_state(self.request_body_json)
         | 
| 146 | 
            -
                    )
         | 
| 147 | 
            -
             | 
| 148 | 
            -
                @staticmethod
         | 
| 149 | 
            -
                def _check_if_interpolation_uses_stream_state(
         | 
| 150 | 
            -
                    request_input: Optional[Union[RequestInput, NestedMapping]],
         | 
| 151 | 
            -
                ) -> bool:
         | 
| 152 | 
            -
                    if not request_input:
         | 
| 153 | 
            -
                        return False
         | 
| 154 | 
            -
                    elif isinstance(request_input, str):
         | 
| 155 | 
            -
                        return "stream_state" in request_input
         | 
| 156 | 
            -
                    else:
         | 
| 157 | 
            -
                        for key, val in request_input.items():
         | 
| 158 | 
            -
                            # Covers the case of RequestInput in the form of a string or Mapping[str, str]. It also covers the case
         | 
| 159 | 
            -
                            # of a NestedMapping where the value is a string.
         | 
| 160 | 
            -
                            # Note: Doesn't account for nested mappings for request_body_json, but I don't see stream_state used in that way
         | 
| 161 | 
            -
                            # in our code
         | 
| 162 | 
            -
                            if "stream_state" in key or (isinstance(val, str) and "stream_state" in val):
         | 
| 163 | 
            -
                                return True
         | 
| 164 | 
            -
                    return False
         | 
| 119 | 
            +
                    return self._body_json_interpolator.eval_request_inputs(stream_slice, next_page_token)
         | 
| @@ -6,7 +6,7 @@ from typing import Any, Iterable, Mapping, Optional | |
| 6 6 |  | 
| 7 7 | 
             
            from typing_extensions import deprecated
         | 
| 8 8 |  | 
| 9 | 
            -
            from airbyte_cdk. | 
| 9 | 
            +
            from airbyte_cdk.sources.declarative.async_job.job import AsyncJob
         | 
| 10 10 | 
             
            from airbyte_cdk.sources.declarative.async_job.job_orchestrator import AsyncPartition
         | 
| 11 11 | 
             
            from airbyte_cdk.sources.declarative.extractors.record_selector import RecordSelector
         | 
| 12 12 | 
             
            from airbyte_cdk.sources.declarative.partition_routers.async_job_partition_router import (
         | 
| @@ -16,7 +16,6 @@ from airbyte_cdk.sources.declarative.retrievers.retriever import Retriever | |
| 16 16 | 
             
            from airbyte_cdk.sources.source import ExperimentalClassWarning
         | 
| 17 17 | 
             
            from airbyte_cdk.sources.streams.core import StreamData
         | 
| 18 18 | 
             
            from airbyte_cdk.sources.types import Config, StreamSlice, StreamState
         | 
| 19 | 
            -
            from airbyte_cdk.utils.traced_exception import AirbyteTracedException
         | 
| 20 19 |  | 
| 21 20 |  | 
| 22 21 | 
             
            @deprecated(
         | 
| @@ -57,9 +56,9 @@ class AsyncRetriever(Retriever): | |
| 57 56 |  | 
| 58 57 | 
             
                    return self.state
         | 
| 59 58 |  | 
| 60 | 
            -
                def  | 
| 59 | 
            +
                def _validate_and_get_stream_slice_jobs(
         | 
| 61 60 | 
             
                    self, stream_slice: Optional[StreamSlice] = None
         | 
| 62 | 
            -
                ) ->  | 
| 61 | 
            +
                ) -> Iterable[AsyncJob]:
         | 
| 63 62 | 
             
                    """
         | 
| 64 63 | 
             
                    Validates the stream_slice argument and returns the partition from it.
         | 
| 65 64 |  | 
| @@ -73,12 +72,7 @@ class AsyncRetriever(Retriever): | |
| 73 72 | 
             
                        AirbyteTracedException: If the stream_slice is not an instance of StreamSlice or if the partition is not present in the stream_slice.
         | 
| 74 73 |  | 
| 75 74 | 
             
                    """
         | 
| 76 | 
            -
                     | 
| 77 | 
            -
                        raise AirbyteTracedException(
         | 
| 78 | 
            -
                            message="Invalid arguments to AsyncRetriever.read_records: stream_slice is not optional. Please contact Airbyte Support",
         | 
| 79 | 
            -
                            failure_type=FailureType.system_error,
         | 
| 80 | 
            -
                        )
         | 
| 81 | 
            -
                    return stream_slice["partition"]  # type: ignore  # stream_slice["partition"] has been added as an AsyncPartition as part of stream_slices
         | 
| 75 | 
            +
                    return stream_slice.extra_fields.get("jobs", []) if stream_slice else []
         | 
| 82 76 |  | 
| 83 77 | 
             
                def stream_slices(self) -> Iterable[Optional[StreamSlice]]:
         | 
| 84 78 | 
             
                    return self.stream_slicer.stream_slices()
         | 
| @@ -89,8 +83,8 @@ class AsyncRetriever(Retriever): | |
| 89 83 | 
             
                    stream_slice: Optional[StreamSlice] = None,
         | 
| 90 84 | 
             
                ) -> Iterable[StreamData]:
         | 
| 91 85 | 
             
                    stream_state: StreamState = self._get_stream_state()
         | 
| 92 | 
            -
                     | 
| 93 | 
            -
                    records: Iterable[Mapping[str, Any]] = self.stream_slicer.fetch_records( | 
| 86 | 
            +
                    jobs: Iterable[AsyncJob] = self._validate_and_get_stream_slice_jobs(stream_slice)
         | 
| 87 | 
            +
                    records: Iterable[Mapping[str, Any]] = self.stream_slicer.fetch_records(jobs)
         | 
| 94 88 |  | 
| 95 89 | 
             
                    yield from self.record_selector.filter_and_transform(
         | 
| 96 90 | 
             
                        all_data=records,
         | 
| @@ -128,9 +128,11 @@ class SimpleRetriever(Retriever): | |
| 128 128 | 
             
                    Returned merged mapping otherwise
         | 
| 129 129 | 
             
                    """
         | 
| 130 130 | 
             
                    # FIXME we should eventually remove the usage of stream_state as part of the interpolation
         | 
| 131 | 
            +
             | 
| 132 | 
            +
                    is_body_json = paginator_method.__name__ == "get_request_body_json"
         | 
| 133 | 
            +
             | 
| 131 134 | 
             
                    mappings = [
         | 
| 132 135 | 
             
                        paginator_method(
         | 
| 133 | 
            -
                            stream_state=stream_state,
         | 
| 134 136 | 
             
                            stream_slice=stream_slice,
         | 
| 135 137 | 
             
                            next_page_token=next_page_token,
         | 
| 136 138 | 
             
                        ),
         | 
| @@ -138,12 +140,11 @@ class SimpleRetriever(Retriever): | |
| 138 140 | 
             
                    if not next_page_token or not self.ignore_stream_slicer_parameters_on_paginated_requests:
         | 
| 139 141 | 
             
                        mappings.append(
         | 
| 140 142 | 
             
                            stream_slicer_method(
         | 
| 141 | 
            -
                                stream_state=stream_state,
         | 
| 142 143 | 
             
                                stream_slice=stream_slice,
         | 
| 143 144 | 
             
                                next_page_token=next_page_token,
         | 
| 144 145 | 
             
                            )
         | 
| 145 146 | 
             
                        )
         | 
| 146 | 
            -
                    return combine_mappings(mappings)
         | 
| 147 | 
            +
                    return combine_mappings(mappings, allow_same_value_merge=is_body_json)
         | 
| 147 148 |  | 
| 148 149 | 
             
                def _request_headers(
         | 
| 149 150 | 
             
                    self,
         | 
| @@ -64,9 +64,9 @@ class AddFields(RecordTransformation): | |
| 64 64 | 
             
                    - path: ["shop_id"]
         | 
| 65 65 | 
             
                      value: "{{ config.shop_id }}"
         | 
| 66 66 |  | 
| 67 | 
            -
                    # from  | 
| 68 | 
            -
                    - path: [" | 
| 69 | 
            -
                      value: "{{  | 
| 67 | 
            +
                    # from stream_interval
         | 
| 68 | 
            +
                    - path: ["date"]
         | 
| 69 | 
            +
                      value: "{{ stream_interval.start_date }}"
         | 
| 70 70 |  | 
| 71 71 | 
             
                    # from record
         | 
| 72 72 | 
             
                    - path: ["unnested_value"]
         | 
| @@ -128,7 +128,7 @@ class AddFields(RecordTransformation): | |
| 128 128 | 
             
                ) -> None:
         | 
| 129 129 | 
             
                    if config is None:
         | 
| 130 130 | 
             
                        config = {}
         | 
| 131 | 
            -
                    kwargs = {"record": record, " | 
| 131 | 
            +
                    kwargs = {"record": record, "stream_slice": stream_slice}
         | 
| 132 132 | 
             
                    for parsed_field in self._parsed_fields:
         | 
| 133 133 | 
             
                        valid_types = (parsed_field.value_type,) if parsed_field.value_type else None
         | 
| 134 134 | 
             
                        value = parsed_field.value.eval(config, valid_types=valid_types, **kwargs)
         | 
| @@ -11,6 +11,7 @@ from pydantic.v1 import AnyUrl, BaseModel, Field | |
| 11 11 |  | 
| 12 12 | 
             
            from airbyte_cdk import OneOfOptionConfig
         | 
| 13 13 | 
             
            from airbyte_cdk.sources.file_based.config.file_based_stream_config import FileBasedStreamConfig
         | 
| 14 | 
            +
            from airbyte_cdk.sources.specs.transfer_modes import DeliverPermissions
         | 
| 14 15 | 
             
            from airbyte_cdk.sources.utils import schema_helpers
         | 
| 15 16 |  | 
| 16 17 |  | 
| @@ -65,7 +66,7 @@ class AbstractFileBasedSpec(BaseModel): | |
| 65 66 | 
             
                    order=10,
         | 
| 66 67 | 
             
                )
         | 
| 67 68 |  | 
| 68 | 
            -
                delivery_method: Union[DeliverRecords, DeliverRawFiles] = Field(
         | 
| 69 | 
            +
                delivery_method: Union[DeliverRecords, DeliverRawFiles, DeliverPermissions] = Field(
         | 
| 69 70 | 
             
                    title="Delivery Method",
         | 
| 70 71 | 
             
                    discriminator="delivery_type",
         | 
| 71 72 | 
             
                    type="object",
         |