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.
Files changed (61) hide show
  1. airbyte_cdk/connector_builder/connector_builder_handler.py +16 -12
  2. airbyte_cdk/connector_builder/test_reader/__init__.py +7 -0
  3. airbyte_cdk/connector_builder/test_reader/helpers.py +591 -0
  4. airbyte_cdk/connector_builder/test_reader/message_grouper.py +160 -0
  5. airbyte_cdk/connector_builder/test_reader/reader.py +441 -0
  6. airbyte_cdk/connector_builder/test_reader/types.py +75 -0
  7. airbyte_cdk/sources/declarative/async_job/job_orchestrator.py +7 -7
  8. airbyte_cdk/sources/declarative/auth/jwt.py +17 -11
  9. airbyte_cdk/sources/declarative/auth/oauth.py +6 -1
  10. airbyte_cdk/sources/declarative/auth/token.py +3 -8
  11. airbyte_cdk/sources/declarative/concurrent_declarative_source.py +30 -79
  12. airbyte_cdk/sources/declarative/declarative_component_schema.yaml +203 -100
  13. airbyte_cdk/sources/declarative/declarative_stream.py +3 -1
  14. airbyte_cdk/sources/declarative/decoders/__init__.py +0 -4
  15. airbyte_cdk/sources/declarative/decoders/composite_raw_decoder.py +7 -2
  16. airbyte_cdk/sources/declarative/decoders/json_decoder.py +12 -58
  17. airbyte_cdk/sources/declarative/extractors/record_selector.py +12 -3
  18. airbyte_cdk/sources/declarative/incremental/concurrent_partition_cursor.py +56 -25
  19. airbyte_cdk/sources/declarative/incremental/datetime_based_cursor.py +12 -6
  20. airbyte_cdk/sources/declarative/incremental/global_substream_cursor.py +6 -2
  21. airbyte_cdk/sources/declarative/interpolation/jinja.py +13 -0
  22. airbyte_cdk/sources/declarative/manifest_declarative_source.py +9 -0
  23. airbyte_cdk/sources/declarative/models/declarative_component_schema.py +150 -41
  24. airbyte_cdk/sources/declarative/parsers/model_to_component_factory.py +234 -84
  25. airbyte_cdk/sources/declarative/partition_routers/async_job_partition_router.py +5 -5
  26. airbyte_cdk/sources/declarative/partition_routers/list_partition_router.py +4 -2
  27. airbyte_cdk/sources/declarative/partition_routers/substream_partition_router.py +26 -18
  28. airbyte_cdk/sources/declarative/requesters/http_requester.py +8 -2
  29. airbyte_cdk/sources/declarative/requesters/paginators/default_paginator.py +16 -5
  30. airbyte_cdk/sources/declarative/requesters/request_option.py +83 -4
  31. airbyte_cdk/sources/declarative/requesters/request_options/datetime_based_request_options_provider.py +7 -6
  32. airbyte_cdk/sources/declarative/requesters/request_options/interpolated_nested_request_input_provider.py +1 -4
  33. airbyte_cdk/sources/declarative/requesters/request_options/interpolated_request_input_provider.py +0 -3
  34. airbyte_cdk/sources/declarative/requesters/request_options/interpolated_request_options_provider.py +2 -47
  35. airbyte_cdk/sources/declarative/retrievers/async_retriever.py +6 -12
  36. airbyte_cdk/sources/declarative/retrievers/simple_retriever.py +4 -3
  37. airbyte_cdk/sources/declarative/transformations/add_fields.py +4 -4
  38. airbyte_cdk/sources/file_based/config/abstract_file_based_spec.py +2 -1
  39. airbyte_cdk/sources/file_based/config/validate_config_transfer_modes.py +81 -0
  40. airbyte_cdk/sources/file_based/file_based_source.py +70 -37
  41. airbyte_cdk/sources/file_based/file_based_stream_reader.py +107 -12
  42. airbyte_cdk/sources/file_based/stream/__init__.py +10 -1
  43. airbyte_cdk/sources/file_based/stream/identities_stream.py +47 -0
  44. airbyte_cdk/sources/file_based/stream/permissions_file_based_stream.py +85 -0
  45. airbyte_cdk/sources/specs/transfer_modes.py +26 -0
  46. airbyte_cdk/sources/streams/call_rate.py +185 -47
  47. airbyte_cdk/sources/streams/http/http.py +1 -2
  48. airbyte_cdk/sources/streams/http/requests_native_auth/abstract_oauth.py +217 -56
  49. airbyte_cdk/sources/streams/http/requests_native_auth/oauth.py +144 -73
  50. airbyte_cdk/sources/streams/permissions/identities_stream.py +75 -0
  51. airbyte_cdk/test/mock_http/mocker.py +9 -1
  52. airbyte_cdk/test/mock_http/response.py +6 -3
  53. airbyte_cdk/utils/datetime_helpers.py +48 -66
  54. airbyte_cdk/utils/mapping_helpers.py +126 -26
  55. {airbyte_cdk-6.34.1.dev0.dist-info → airbyte_cdk-6.34.1.dev1.dist-info}/METADATA +1 -1
  56. {airbyte_cdk-6.34.1.dev0.dist-info → airbyte_cdk-6.34.1.dev1.dist-info}/RECORD +60 -51
  57. airbyte_cdk/connector_builder/message_grouper.py +0 -448
  58. {airbyte_cdk-6.34.1.dev0.dist-info → airbyte_cdk-6.34.1.dev1.dist-info}/LICENSE.txt +0 -0
  59. {airbyte_cdk-6.34.1.dev0.dist-info → airbyte_cdk-6.34.1.dev1.dist-info}/LICENSE_SHORT +0 -0
  60. {airbyte_cdk-6.34.1.dev0.dist-info → airbyte_cdk-6.34.1.dev1.dist-info}/WHEEL +0 -0
  61. {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.update(
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 to the parent stream's state format.
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
- This method converts the global or child state into a format compatible with parent
311
- streams. The migration occurs only for parent streams with incremental dependencies.
312
- The method filters out per-partition states and retains only the global state in the
313
- format `{cursor_field: cursor_value}`.
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
- - {"states": [...] } (ignored during migration)
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
- return {}
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
- # Copy child state to parent streams with incremental dependencies
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
- options[self.page_token_option.field_name.eval(config=self.config)] = token # type: ignore # field_name is always cast to an interpolated string
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
- options[self.page_size_option.field_name.eval(config=self.config)] = ( # type: ignore [union-attr]
206
- self.pagination_strategy.get_page_size()
207
- ) # type: ignore # field_name is always cast to an interpolated string
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
- self.field_name = InterpolatedString.create(self.field_name, parameters=parameters)
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
- options[self.start_time_option.field_name.eval(config=self.config)] = stream_slice.get( # type: ignore # field_name is always casted to an interpolated string
85
- self._partition_field_start.eval(self.config)
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
- options[self.end_time_option.field_name.eval(config=self.config)] = stream_slice.get( # type: ignore [union-attr]
89
- self._partition_field_end.eval(self.config)
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, StreamState
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
  }
@@ -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
  }
@@ -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.models import FailureType
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 _validate_and_get_stream_slice_partition(
59
+ def _validate_and_get_stream_slice_jobs(
61
60
  self, stream_slice: Optional[StreamSlice] = None
62
- ) -> AsyncPartition:
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
- if not isinstance(stream_slice, StreamSlice) or "partition" not in stream_slice.partition:
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
- partition: AsyncPartition = self._validate_and_get_stream_slice_partition(stream_slice)
93
- records: Iterable[Mapping[str, Any]] = self.stream_slicer.fetch_records(partition)
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 state
68
- - path: ["current_state"]
69
- value: "{{ stream_state.cursor_field }}" # Or {{ stream_state['cursor_field'] }}
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, "stream_state": stream_state, "stream_slice": stream_slice}
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",