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.
- 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",
|