airbyte-cdk 6.8.1rc8__py3-none-any.whl → 6.8.1rc10__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 (74) hide show
  1. airbyte_cdk/cli/source_declarative_manifest/_run.py +11 -5
  2. airbyte_cdk/config_observation.py +1 -1
  3. airbyte_cdk/connector_builder/main.py +1 -1
  4. airbyte_cdk/connector_builder/message_grouper.py +10 -10
  5. airbyte_cdk/destinations/destination.py +1 -1
  6. airbyte_cdk/destinations/vector_db_based/embedder.py +2 -2
  7. airbyte_cdk/destinations/vector_db_based/writer.py +12 -4
  8. airbyte_cdk/entrypoint.py +7 -6
  9. airbyte_cdk/logger.py +2 -2
  10. airbyte_cdk/sources/abstract_source.py +1 -1
  11. airbyte_cdk/sources/config.py +1 -1
  12. airbyte_cdk/sources/connector_state_manager.py +9 -4
  13. airbyte_cdk/sources/declarative/auth/oauth.py +1 -1
  14. airbyte_cdk/sources/declarative/auth/selective_authenticator.py +6 -1
  15. airbyte_cdk/sources/declarative/concurrent_declarative_source.py +28 -42
  16. airbyte_cdk/sources/declarative/datetime/min_max_datetime.py +10 -4
  17. airbyte_cdk/sources/declarative/declarative_component_schema.yaml +116 -19
  18. airbyte_cdk/sources/declarative/decoders/noop_decoder.py +4 -1
  19. airbyte_cdk/sources/declarative/incremental/datetime_based_cursor.py +8 -6
  20. airbyte_cdk/sources/declarative/interpolation/jinja.py +35 -36
  21. airbyte_cdk/sources/declarative/interpolation/macros.py +1 -1
  22. airbyte_cdk/sources/declarative/manifest_declarative_source.py +53 -2
  23. airbyte_cdk/sources/declarative/models/declarative_component_schema.py +95 -2
  24. airbyte_cdk/sources/declarative/parsers/manifest_component_transformer.py +6 -0
  25. airbyte_cdk/sources/declarative/parsers/model_to_component_factory.py +100 -27
  26. airbyte_cdk/sources/declarative/partition_routers/__init__.py +2 -1
  27. airbyte_cdk/sources/declarative/partition_routers/substream_partition_router.py +13 -7
  28. airbyte_cdk/sources/declarative/requesters/error_handlers/default_error_handler.py +1 -1
  29. airbyte_cdk/sources/declarative/requesters/error_handlers/http_response_filter.py +8 -6
  30. airbyte_cdk/sources/declarative/requesters/paginators/default_paginator.py +1 -1
  31. airbyte_cdk/sources/declarative/requesters/request_options/datetime_based_request_options_provider.py +2 -2
  32. airbyte_cdk/sources/declarative/requesters/request_options/interpolated_request_options_provider.py +1 -1
  33. airbyte_cdk/sources/declarative/resolvers/__init__.py +13 -0
  34. airbyte_cdk/sources/declarative/resolvers/components_resolver.py +55 -0
  35. airbyte_cdk/sources/declarative/resolvers/http_components_resolver.py +106 -0
  36. airbyte_cdk/sources/declarative/retrievers/async_retriever.py +5 -2
  37. airbyte_cdk/sources/declarative/spec/spec.py +1 -1
  38. airbyte_cdk/sources/embedded/base_integration.py +3 -2
  39. airbyte_cdk/sources/file_based/availability_strategy/abstract_file_based_availability_strategy.py +12 -4
  40. airbyte_cdk/sources/file_based/availability_strategy/default_file_based_availability_strategy.py +18 -7
  41. airbyte_cdk/sources/file_based/file_types/avro_parser.py +14 -11
  42. airbyte_cdk/sources/file_based/file_types/csv_parser.py +3 -3
  43. airbyte_cdk/sources/file_based/file_types/excel_parser.py +11 -5
  44. airbyte_cdk/sources/file_based/file_types/jsonl_parser.py +1 -1
  45. airbyte_cdk/sources/file_based/stream/abstract_file_based_stream.py +2 -2
  46. airbyte_cdk/sources/file_based/stream/concurrent/adapters.py +6 -3
  47. airbyte_cdk/sources/file_based/stream/cursor/default_file_based_cursor.py +1 -1
  48. airbyte_cdk/sources/http_logger.py +3 -3
  49. airbyte_cdk/sources/streams/concurrent/abstract_stream.py +5 -2
  50. airbyte_cdk/sources/streams/concurrent/adapters.py +6 -3
  51. airbyte_cdk/sources/streams/concurrent/availability_strategy.py +9 -3
  52. airbyte_cdk/sources/streams/concurrent/cursor.py +1 -1
  53. airbyte_cdk/sources/streams/concurrent/state_converters/datetime_stream_state_converter.py +2 -2
  54. airbyte_cdk/sources/streams/core.py +17 -14
  55. airbyte_cdk/sources/streams/http/http.py +19 -19
  56. airbyte_cdk/sources/streams/http/http_client.py +4 -48
  57. airbyte_cdk/sources/streams/http/requests_native_auth/abstract_token.py +2 -1
  58. airbyte_cdk/sources/streams/http/requests_native_auth/oauth.py +62 -33
  59. airbyte_cdk/sources/utils/record_helper.py +1 -1
  60. airbyte_cdk/sources/utils/schema_helpers.py +1 -1
  61. airbyte_cdk/sources/utils/transform.py +34 -15
  62. airbyte_cdk/test/entrypoint_wrapper.py +11 -6
  63. airbyte_cdk/test/mock_http/response_builder.py +1 -1
  64. airbyte_cdk/utils/airbyte_secrets_utils.py +1 -1
  65. airbyte_cdk/utils/event_timing.py +10 -10
  66. airbyte_cdk/utils/message_utils.py +4 -3
  67. airbyte_cdk/utils/spec_schema_transformations.py +3 -2
  68. airbyte_cdk/utils/traced_exception.py +14 -12
  69. airbyte_cdk-6.8.1rc10.dist-info/METADATA +111 -0
  70. {airbyte_cdk-6.8.1rc8.dist-info → airbyte_cdk-6.8.1rc10.dist-info}/RECORD +73 -70
  71. airbyte_cdk-6.8.1rc8.dist-info/METADATA +0 -307
  72. {airbyte_cdk-6.8.1rc8.dist-info → airbyte_cdk-6.8.1rc10.dist-info}/LICENSE.txt +0 -0
  73. {airbyte_cdk-6.8.1rc8.dist-info → airbyte_cdk-6.8.1rc10.dist-info}/WHEEL +0 -0
  74. {airbyte_cdk-6.8.1rc8.dist-info → airbyte_cdk-6.8.1rc10.dist-info}/entry_points.txt +0 -0
@@ -7,8 +7,12 @@ version: 1.0.0
7
7
  required:
8
8
  - type
9
9
  - check
10
- - streams
11
10
  - version
11
+ anyOf:
12
+ - required:
13
+ - streams
14
+ - required:
15
+ - dynamic_streams
12
16
  properties:
13
17
  type:
14
18
  type: string
@@ -19,6 +23,10 @@ properties:
19
23
  type: array
20
24
  items:
21
25
  "$ref": "#/definitions/DeclarativeStream"
26
+ dynamic_streams:
27
+ type: array
28
+ items:
29
+ "$ref": "#/definitions/DynamicDeclarativeStream"
22
30
  version:
23
31
  type: string
24
32
  description: The version of the Airbyte CDK used to build and test the source.
@@ -1321,7 +1329,7 @@ definitions:
1321
1329
  type: array
1322
1330
  items:
1323
1331
  - type: string
1324
- interpolation_content:
1332
+ interpolation_context:
1325
1333
  - config
1326
1334
  examples:
1327
1335
  - ["data"]
@@ -2057,7 +2065,7 @@ definitions:
2057
2065
  The DeclarativeOAuth Specific URL templated string to obtain the `access_token`, `refresh_token` etc.
2058
2066
  The placeholders are replaced during the processing to provide neccessary values.
2059
2067
  examples:
2060
- - access_token_url: https://auth.host.com/oauth2/token?{client_id_key}={{client_id_key}}&{client_secret_key}={{client_secret_key}}&{auth_code_key}={{auth_code_key}}&{redirect_uri_key}={urlEncoder:{{redirect_uri_key}}}
2068
+ - access_token_url: https://auth.host.com/oauth2/token?{client_id_key}={{client_id_key}}&{client_secret_key}={{client_secret_key}}&{auth_code_key}={{auth_code_key}}&{redirect_uri_key}={urlEncoder:{{redirect_uri_key}}}
2061
2069
  access_token_headers:
2062
2070
  title: (Optional) DeclarativeOAuth Access Token Headers
2063
2071
  type: object
@@ -2065,9 +2073,10 @@ definitions:
2065
2073
  description: |-
2066
2074
  The DeclarativeOAuth Specific optional headers to inject while exchanging the `auth_code` to `access_token` during `completeOAuthFlow` step.
2067
2075
  examples:
2068
- - access_token_headers: {
2069
- "Authorization": "Basic {base64Encoder:{client_id}:{client_secret}}"
2070
- }
2076
+ - access_token_headers:
2077
+ {
2078
+ "Authorization": "Basic {base64Encoder:{client_id}:{client_secret}}",
2079
+ }
2071
2080
  access_token_params:
2072
2081
  title: (Optional) DeclarativeOAuth Access Token Query Params (Json Encoded)
2073
2082
  type: object
@@ -2076,18 +2085,19 @@ definitions:
2076
2085
  The DeclarativeOAuth Specific optional query parameters to inject while exchanging the `auth_code` to `access_token` during `completeOAuthFlow` step.
2077
2086
  When this property is provided, the query params will be encoded as `Json` and included in the outgoing API request.
2078
2087
  examples:
2079
- - access_token_params: {
2080
- "{auth_code_key}": "{{auth_code_key}}",
2081
- "{client_id_key}": "{{client_id_key}}",
2082
- "{client_secret_key}": "{{client_secret_key}}"
2083
- }
2088
+ - access_token_params:
2089
+ {
2090
+ "{auth_code_key}": "{{auth_code_key}}",
2091
+ "{client_id_key}": "{{client_id_key}}",
2092
+ "{client_secret_key}": "{{client_secret_key}}",
2093
+ }
2084
2094
  extract_output:
2085
2095
  title: DeclarativeOAuth Extract Output
2086
2096
  type: array
2087
2097
  items:
2088
2098
  type: string
2089
2099
  description: |-
2090
- The DeclarativeOAuth Specific list of strings to indicate which keys should be extracted and returned back to the input config.
2100
+ The DeclarativeOAuth Specific list of strings to indicate which keys should be extracted and returned back to the input config.
2091
2101
  examples:
2092
2102
  - extract_output: ["access_token", "refresh_token", "other_field"]
2093
2103
  state:
@@ -2099,17 +2109,14 @@ definitions:
2099
2109
  - max
2100
2110
  description: |-
2101
2111
  The DeclarativeOAuth Specific object to provide the criteria of how the `state` query param should be constructed,
2102
- including length and complexity.
2112
+ including length and complexity.
2103
2113
  properties:
2104
2114
  min:
2105
2115
  type: integer
2106
2116
  max:
2107
2117
  type: integer
2108
2118
  examples:
2109
- - state: {
2110
- "min": 7,
2111
- "max": 128,
2112
- }
2119
+ - state: { "min": 7, "max": 128 }
2113
2120
  client_id_key:
2114
2121
  title: (Optional) DeclarativeOAuth Client ID Key Override
2115
2122
  type: string
@@ -2135,14 +2142,14 @@ definitions:
2135
2142
  title: (Optional) DeclarativeOAuth State Key Override
2136
2143
  type: string
2137
2144
  description: |-
2138
- The DeclarativeOAuth Specific optional override to provide the custom `state` key name, if required by data-provider.
2145
+ The DeclarativeOAuth Specific optional override to provide the custom `state` key name, if required by data-provider.
2139
2146
  examples:
2140
2147
  - state_key: "my_custom_state_key_key_name"
2141
2148
  auth_code_key:
2142
2149
  title: (Optional) DeclarativeOAuth Auth Code Key Override
2143
2150
  type: string
2144
2151
  description: |-
2145
- The DeclarativeOAuth Specific optional override to provide the custom `code` key name to something like `auth_code` or `custom_auth_code`, if required by data-provider.
2152
+ The DeclarativeOAuth Specific optional override to provide the custom `code` key name to something like `auth_code` or `custom_auth_code`, if required by data-provider.
2146
2153
  examples:
2147
2154
  - auth_code_key: "my_custom_auth_code_key_name"
2148
2155
  redirect_uri_key:
@@ -2896,6 +2903,96 @@ definitions:
2896
2903
  $parameters:
2897
2904
  type: object
2898
2905
  additionalProperties: true
2906
+ ComponentMappingDefinition:
2907
+ title: Component Mapping Definition
2908
+ description: (This component is experimental. Use at your own risk.) Specifies a mapping definition to update or add fields in a record or configuration. This allows dynamic mapping of data by interpolating values into the template based on provided contexts.
2909
+ type: object
2910
+ required:
2911
+ - type
2912
+ - field_path
2913
+ - value
2914
+ properties:
2915
+ type:
2916
+ type: string
2917
+ enum: [ComponentMappingDefinition]
2918
+ field_path:
2919
+ title: Field Path
2920
+ description: A list of potentially nested fields indicating the full path where value will be added or updated.
2921
+ type: array
2922
+ items:
2923
+ - type: string
2924
+ interpolation_context:
2925
+ - config
2926
+ - components_values
2927
+ - stream_template_config
2928
+ examples:
2929
+ - ["data"]
2930
+ - ["data", "records"]
2931
+ - ["data", "{{ parameters.name }}"]
2932
+ - ["data", "*", "record"]
2933
+ value:
2934
+ title: Value
2935
+ description: The dynamic or static value to assign to the key. Interpolated values can be used to dynamically determine the value during runtime.
2936
+ type: string
2937
+ interpolation_context:
2938
+ - config
2939
+ - stream_template_config
2940
+ - components_values
2941
+ examples:
2942
+ - "{{ components_values['updates'] }}"
2943
+ - "{{ components_values['MetaData']['LastUpdatedTime'] }}"
2944
+ - "{{ config['segment_id'] }}"
2945
+ value_type:
2946
+ title: Value Type
2947
+ description: The expected data type of the value. If omitted, the type will be inferred from the value provided.
2948
+ "$ref": "#/definitions/ValueType"
2949
+ $parameters:
2950
+ type: object
2951
+ additionalProperties: true
2952
+ HttpComponentsResolver:
2953
+ type: object
2954
+ description: (This component is experimental. Use at your own risk.) Component resolve and populates stream templates with components fetched via an HTTP retriever.
2955
+ properties:
2956
+ type:
2957
+ type: string
2958
+ enum: [HttpComponentsResolver]
2959
+ retriever:
2960
+ title: Retriever
2961
+ description: Component used to coordinate how records are extracted across stream slices and request pages.
2962
+ anyOf:
2963
+ - "$ref": "#/definitions/AsyncRetriever"
2964
+ - "$ref": "#/definitions/CustomRetriever"
2965
+ - "$ref": "#/definitions/SimpleRetriever"
2966
+ components_mapping:
2967
+ type: array
2968
+ items:
2969
+ "$ref": "#/definitions/ComponentMappingDefinition"
2970
+ $parameters:
2971
+ type: object
2972
+ additionalProperties: true
2973
+ required:
2974
+ - type
2975
+ - retriever
2976
+ - components_mapping
2977
+ DynamicDeclarativeStream:
2978
+ type: object
2979
+ description: (This component is experimental. Use at your own risk.) A component that described how will be created declarative streams based on stream template.
2980
+ properties:
2981
+ type:
2982
+ type: string
2983
+ enum: [DynamicDeclarativeStream]
2984
+ stream_template:
2985
+ title: Stream Template
2986
+ description: Reference to the stream template.
2987
+ "$ref": "#/definitions/DeclarativeStream"
2988
+ components_resolver:
2989
+ title: Components Resolver
2990
+ description: Component resolve and populates stream templates with components values.
2991
+ "$ref": "#/definitions/HttpComponentsResolver"
2992
+ required:
2993
+ - type
2994
+ - stream_template
2995
+ - components_resolver
2899
2996
  interpolation:
2900
2997
  variables:
2901
2998
  - title: config
@@ -14,5 +14,8 @@ class NoopDecoder(Decoder):
14
14
  def is_stream_response(self) -> bool:
15
15
  return False
16
16
 
17
- def decode(self, response: requests.Response) -> Generator[Mapping[str, Any], None, None]:
17
+ def decode( # type: ignore[override] # Signature doesn't match base class
18
+ self,
19
+ response: requests.Response,
20
+ ) -> Generator[Mapping[str, Any], None, None]:
18
21
  yield from [{}]
@@ -133,8 +133,8 @@ class DatetimeBasedCursor(DeclarativeCursor):
133
133
  :param stream_state: The state of the stream as returned by get_stream_state
134
134
  """
135
135
  self._cursor = (
136
- stream_state.get(self.cursor_field.eval(self.config)) if stream_state else None
137
- ) # type: ignore # cursor_field is converted to an InterpolatedString in __post_init__
136
+ stream_state.get(self.cursor_field.eval(self.config)) if stream_state else None # type: ignore [union-attr]
137
+ )
138
138
 
139
139
  def observe(self, stream_slice: StreamSlice, record: Record) -> None:
140
140
  """
@@ -158,8 +158,10 @@ class DatetimeBasedCursor(DeclarativeCursor):
158
158
  )
159
159
  if (
160
160
  self._is_within_daterange_boundaries(
161
- record, stream_slice.get(start_field), stream_slice.get(end_field)
162
- ) # type: ignore # we know that stream_slices for these cursors will use a string representing an unparsed date
161
+ record,
162
+ stream_slice.get(start_field), # type: ignore [arg-type]
163
+ stream_slice.get(end_field), # type: ignore [arg-type]
164
+ )
163
165
  and is_highest_observed_cursor_value
164
166
  ):
165
167
  self._highest_observed_cursor_field_value = record_cursor_value
@@ -368,9 +370,9 @@ class DatetimeBasedCursor(DeclarativeCursor):
368
370
  self._partition_field_start.eval(self.config)
369
371
  )
370
372
  if self.end_time_option and self.end_time_option.inject_into == option_type:
371
- options[self.end_time_option.field_name.eval(config=self.config)] = stream_slice.get(
373
+ options[self.end_time_option.field_name.eval(config=self.config)] = stream_slice.get( # type: ignore [union-attr]
372
374
  self._partition_field_end.eval(self.config)
373
- ) # type: ignore # field_name is always casted to an interpolated string
375
+ )
374
376
  return options
375
377
 
376
378
  def should_be_synced(self, record: Record) -> bool:
@@ -4,7 +4,7 @@
4
4
 
5
5
  import ast
6
6
  from functools import cache
7
- from typing import Any, Mapping, Optional, Set, Tuple, Type
7
+ from typing import Any, Mapping, Optional, Tuple, Type
8
8
 
9
9
  from jinja2 import meta
10
10
  from jinja2.environment import Template
@@ -27,35 +27,7 @@ class StreamPartitionAccessEnvironment(SandboxedEnvironment):
27
27
  def is_safe_attribute(self, obj: Any, attr: str, value: Any) -> bool:
28
28
  if attr in ["_partition"]:
29
29
  return True
30
- return super().is_safe_attribute(obj, attr, value) # type: ignore # for some reason, mypy says 'Returning Any from function declared to return "bool"'
31
-
32
-
33
- # These aliases are used to deprecate existing keywords without breaking all existing connectors.
34
- _ALIASES = {
35
- "stream_interval": "stream_slice", # Use stream_interval to access incremental_sync values
36
- "stream_partition": "stream_slice", # Use stream_partition to access partition router's values
37
- }
38
-
39
- # These extensions are not installed so they're not currently a problem,
40
- # but we're still explicitely removing them from the jinja context.
41
- # At worst, this is documentation that we do NOT want to include these extensions because of the potential security risks
42
- _RESTRICTED_EXTENSIONS = ["jinja2.ext.loopcontrols"] # Adds support for break continue in loops
43
-
44
- # By default, these Python builtin functions are available in the Jinja context.
45
- # We explicitely remove them because of the potential security risk.
46
- # Please add a unit test to test_jinja.py when adding a restriction.
47
- _RESTRICTED_BUILTIN_FUNCTIONS = [
48
- "range"
49
- ] # The range function can cause very expensive computations
50
-
51
- _ENVIRONMENT = StreamPartitionAccessEnvironment()
52
- _ENVIRONMENT.filters.update(**filters)
53
- _ENVIRONMENT.globals.update(**macros)
54
-
55
- for extension in _RESTRICTED_EXTENSIONS:
56
- _ENVIRONMENT.extensions.pop(extension, None)
57
- for builtin in _RESTRICTED_BUILTIN_FUNCTIONS:
58
- _ENVIRONMENT.globals.pop(builtin, None)
30
+ return super().is_safe_attribute(obj, attr, value)
59
31
 
60
32
 
61
33
  class JinjaInterpolation(Interpolation):
@@ -76,6 +48,34 @@ class JinjaInterpolation(Interpolation):
76
48
  Additional information on jinja templating can be found at https://jinja.palletsprojects.com/en/3.1.x/templates/#
77
49
  """
78
50
 
51
+ # These aliases are used to deprecate existing keywords without breaking all existing connectors.
52
+ ALIASES = {
53
+ "stream_interval": "stream_slice", # Use stream_interval to access incremental_sync values
54
+ "stream_partition": "stream_slice", # Use stream_partition to access partition router's values
55
+ }
56
+
57
+ # These extensions are not installed so they're not currently a problem,
58
+ # but we're still explicitely removing them from the jinja context.
59
+ # At worst, this is documentation that we do NOT want to include these extensions because of the potential security risks
60
+ RESTRICTED_EXTENSIONS = ["jinja2.ext.loopcontrols"] # Adds support for break continue in loops
61
+
62
+ # By default, these Python builtin functions are available in the Jinja context.
63
+ # We explicitely remove them because of the potential security risk.
64
+ # Please add a unit test to test_jinja.py when adding a restriction.
65
+ RESTRICTED_BUILTIN_FUNCTIONS = [
66
+ "range"
67
+ ] # The range function can cause very expensive computations
68
+
69
+ def __init__(self) -> None:
70
+ self._environment = StreamPartitionAccessEnvironment()
71
+ self._environment.filters.update(**filters)
72
+ self._environment.globals.update(**macros)
73
+
74
+ for extension in self.RESTRICTED_EXTENSIONS:
75
+ self._environment.extensions.pop(extension, None)
76
+ for builtin in self.RESTRICTED_BUILTIN_FUNCTIONS:
77
+ self._environment.globals.pop(builtin, None)
78
+
79
79
  def eval(
80
80
  self,
81
81
  input_str: str,
@@ -86,7 +86,7 @@ class JinjaInterpolation(Interpolation):
86
86
  ) -> Any:
87
87
  context = {"config": config, **additional_parameters}
88
88
 
89
- for alias, equivalent in _ALIASES.items():
89
+ for alias, equivalent in self.ALIASES.items():
90
90
  if alias in context:
91
91
  # This is unexpected. We could ignore or log a warning, but failing loudly should result in fewer surprises
92
92
  raise ValueError(
@@ -105,7 +105,6 @@ class JinjaInterpolation(Interpolation):
105
105
  raise Exception(f"Expected a string, got {input_str}")
106
106
  except UndefinedError:
107
107
  pass
108
-
109
108
  # If result is empty or resulted in an undefined error, evaluate and return the default string
110
109
  return self._literal_eval(self._eval(default, context), valid_types)
111
110
 
@@ -133,16 +132,16 @@ class JinjaInterpolation(Interpolation):
133
132
  return s
134
133
 
135
134
  @cache
136
- def _find_undeclared_variables(self, s: Optional[str]) -> Set[str]:
135
+ def _find_undeclared_variables(self, s: Optional[str]) -> set[str]:
137
136
  """
138
137
  Find undeclared variables and cache them
139
138
  """
140
- ast = _ENVIRONMENT.parse(s) # type: ignore # parse is able to handle None
139
+ ast = self._environment.parse(s) # type: ignore # parse is able to handle None
141
140
  return meta.find_undeclared_variables(ast)
142
141
 
143
142
  @cache
144
- def _compile(self, s: str) -> Template:
143
+ def _compile(self, s: Optional[str]) -> Template:
145
144
  """
146
145
  We must cache the Jinja Template ourselves because we're using `from_string` instead of a template loader
147
146
  """
148
- return _ENVIRONMENT.from_string(s)
147
+ return self._environment.from_string(s) # type: ignore [arg-type] # Expected `str | Template` but passed `str | None`
@@ -116,7 +116,7 @@ def duration(datestring: str) -> Union[datetime.timedelta, isodate.Duration]:
116
116
  Usage:
117
117
  `"{{ now_utc() - duration('P1D') }}"`
118
118
  """
119
- return parse_duration(datestring) # type: ignore # mypy thinks this returns Any for some reason
119
+ return parse_duration(datestring)
120
120
 
121
121
 
122
122
  def format_datetime(
@@ -39,6 +39,7 @@ from airbyte_cdk.sources.declarative.parsers.manifest_reference_resolver import
39
39
  from airbyte_cdk.sources.declarative.parsers.model_to_component_factory import (
40
40
  ModelToComponentFactory,
41
41
  )
42
+ from airbyte_cdk.sources.declarative.resolvers import COMPONENTS_RESOLVER_TYPE_MAPPING
42
43
  from airbyte_cdk.sources.message import MessageRepository
43
44
  from airbyte_cdk.sources.streams.core import Stream
44
45
  from airbyte_cdk.sources.types import ConnectionDefinition
@@ -120,7 +121,10 @@ class ManifestDeclarativeSource(DeclarativeSource):
120
121
  self._emit_manifest_debug_message(
121
122
  extra_args={"source_name": self.name, "parsed_config": json.dumps(self._source_config)}
122
123
  )
123
- stream_configs = self._stream_configs(self._source_config)
124
+
125
+ stream_configs = self._stream_configs(self._source_config) + self._dynamic_stream_configs(
126
+ self._source_config, config
127
+ )
124
128
 
125
129
  source_streams = [
126
130
  self._constructor.create_component(
@@ -234,7 +238,8 @@ class ManifestDeclarativeSource(DeclarativeSource):
234
238
  )
235
239
 
236
240
  streams = self._source_config.get("streams")
237
- if not streams:
241
+ dynamic_streams = self._source_config.get("dynamic_streams")
242
+ if not (streams or dynamic_streams):
238
243
  raise ValidationError(
239
244
  f"A valid manifest should have at least one stream defined. Got {streams}"
240
245
  )
@@ -303,5 +308,51 @@ class ManifestDeclarativeSource(DeclarativeSource):
303
308
  s["type"] = "DeclarativeStream"
304
309
  return stream_configs
305
310
 
311
+ def _dynamic_stream_configs(
312
+ self, manifest: Mapping[str, Any], config: Mapping[str, Any]
313
+ ) -> List[Dict[str, Any]]:
314
+ dynamic_stream_definitions: List[Dict[str, Any]] = manifest.get("dynamic_streams", [])
315
+ dynamic_stream_configs: List[Dict[str, Any]] = []
316
+
317
+ for dynamic_definition in dynamic_stream_definitions:
318
+ components_resolver_config = dynamic_definition["components_resolver"]
319
+
320
+ if not components_resolver_config:
321
+ raise ValueError(
322
+ f"Missing 'components_resolver' in dynamic definition: {dynamic_definition}"
323
+ )
324
+
325
+ resolver_type = components_resolver_config.get("type")
326
+ if not resolver_type:
327
+ raise ValueError(
328
+ f"Missing 'type' in components resolver configuration: {components_resolver_config}"
329
+ )
330
+
331
+ if resolver_type not in COMPONENTS_RESOLVER_TYPE_MAPPING:
332
+ raise ValueError(
333
+ f"Invalid components resolver type '{resolver_type}'. "
334
+ f"Expected one of {list(COMPONENTS_RESOLVER_TYPE_MAPPING.keys())}."
335
+ )
336
+
337
+ if "retriever" in components_resolver_config:
338
+ components_resolver_config["retriever"]["requester"]["use_cache"] = True
339
+
340
+ # Create a resolver for dynamic components based on type
341
+ components_resolver = self._constructor.create_component(
342
+ COMPONENTS_RESOLVER_TYPE_MAPPING[resolver_type], components_resolver_config, config
343
+ )
344
+
345
+ stream_template_config = dynamic_definition["stream_template"]
346
+
347
+ for dynamic_stream in components_resolver.resolve_components(
348
+ stream_template_config=stream_template_config
349
+ ):
350
+ if "type" not in dynamic_stream:
351
+ dynamic_stream["type"] = "DeclarativeStream"
352
+
353
+ dynamic_stream_configs.append(dynamic_stream)
354
+
355
+ return dynamic_stream_configs
356
+
306
357
  def _emit_manifest_debug_message(self, extra_args: dict[str, Any]) -> None:
307
358
  self.logger.debug("declarative source created from manifest", extra=extra_args)
@@ -1158,6 +1158,37 @@ class WaitUntilTimeFromHeader(BaseModel):
1158
1158
  parameters: Optional[Dict[str, Any]] = Field(None, alias="$parameters")
1159
1159
 
1160
1160
 
1161
+ class ComponentMappingDefinition(BaseModel):
1162
+ type: Literal["ComponentMappingDefinition"]
1163
+ field_path: List[str] = Field(
1164
+ ...,
1165
+ description="A list of potentially nested fields indicating the full path where value will be added or updated.",
1166
+ examples=[
1167
+ ["data"],
1168
+ ["data", "records"],
1169
+ ["data", "{{ parameters.name }}"],
1170
+ ["data", "*", "record"],
1171
+ ],
1172
+ title="Field Path",
1173
+ )
1174
+ value: str = Field(
1175
+ ...,
1176
+ description="The dynamic or static value to assign to the key. Interpolated values can be used to dynamically determine the value during runtime.",
1177
+ examples=[
1178
+ "{{ components_values['updates'] }}",
1179
+ "{{ components_values['MetaData']['LastUpdatedTime'] }}",
1180
+ "{{ config['segment_id'] }}",
1181
+ ],
1182
+ title="Value",
1183
+ )
1184
+ value_type: Optional[ValueType] = Field(
1185
+ None,
1186
+ description="The expected data type of the value. If omitted, the type will be inferred from the value provided.",
1187
+ title="Value Type",
1188
+ )
1189
+ parameters: Optional[Dict[str, Any]] = Field(None, alias="$parameters")
1190
+
1191
+
1161
1192
  class AddedFieldDefinition(BaseModel):
1162
1193
  type: Literal["AddedFieldDefinition"]
1163
1194
  path: List[str] = Field(
@@ -1455,13 +1486,40 @@ class CompositeErrorHandler(BaseModel):
1455
1486
  parameters: Optional[Dict[str, Any]] = Field(None, alias="$parameters")
1456
1487
 
1457
1488
 
1458
- class DeclarativeSource(BaseModel):
1489
+ class DeclarativeSource1(BaseModel):
1459
1490
  class Config:
1460
1491
  extra = Extra.forbid
1461
1492
 
1462
1493
  type: Literal["DeclarativeSource"]
1463
1494
  check: CheckStream
1464
1495
  streams: List[DeclarativeStream]
1496
+ dynamic_streams: Optional[List[DynamicDeclarativeStream]] = None
1497
+ version: str = Field(
1498
+ ...,
1499
+ description="The version of the Airbyte CDK used to build and test the source.",
1500
+ )
1501
+ schemas: Optional[Schemas] = None
1502
+ definitions: Optional[Dict[str, Any]] = None
1503
+ spec: Optional[Spec] = None
1504
+ concurrency_level: Optional[ConcurrencyLevel] = None
1505
+ metadata: Optional[Dict[str, Any]] = Field(
1506
+ None,
1507
+ description="For internal Airbyte use only - DO NOT modify manually. Used by consumers of declarative manifests for storing related metadata.",
1508
+ )
1509
+ description: Optional[str] = Field(
1510
+ None,
1511
+ description="A description of the connector. It will be presented on the Source documentation page.",
1512
+ )
1513
+
1514
+
1515
+ class DeclarativeSource2(BaseModel):
1516
+ class Config:
1517
+ extra = Extra.forbid
1518
+
1519
+ type: Literal["DeclarativeSource"]
1520
+ check: CheckStream
1521
+ streams: Optional[List[DeclarativeStream]] = None
1522
+ dynamic_streams: List[DynamicDeclarativeStream]
1465
1523
  version: str = Field(
1466
1524
  ...,
1467
1525
  description="The version of the Airbyte CDK used to build and test the source.",
@@ -1480,6 +1538,17 @@ class DeclarativeSource(BaseModel):
1480
1538
  )
1481
1539
 
1482
1540
 
1541
+ class DeclarativeSource(BaseModel):
1542
+ class Config:
1543
+ extra = Extra.forbid
1544
+
1545
+ __root__: Union[DeclarativeSource1, DeclarativeSource2] = Field(
1546
+ ...,
1547
+ description="An API source that extracts data according to its declarative components.",
1548
+ title="DeclarativeSource",
1549
+ )
1550
+
1551
+
1483
1552
  class SelectiveAuthenticator(BaseModel):
1484
1553
  class Config:
1485
1554
  extra = Extra.allow
@@ -1883,8 +1952,32 @@ class SubstreamPartitionRouter(BaseModel):
1883
1952
  parameters: Optional[Dict[str, Any]] = Field(None, alias="$parameters")
1884
1953
 
1885
1954
 
1955
+ class HttpComponentsResolver(BaseModel):
1956
+ type: Literal["HttpComponentsResolver"]
1957
+ retriever: Union[AsyncRetriever, CustomRetriever, SimpleRetriever] = Field(
1958
+ ...,
1959
+ description="Component used to coordinate how records are extracted across stream slices and request pages.",
1960
+ title="Retriever",
1961
+ )
1962
+ components_mapping: List[ComponentMappingDefinition]
1963
+ parameters: Optional[Dict[str, Any]] = Field(None, alias="$parameters")
1964
+
1965
+
1966
+ class DynamicDeclarativeStream(BaseModel):
1967
+ type: Literal["DynamicDeclarativeStream"]
1968
+ stream_template: DeclarativeStream = Field(
1969
+ ..., description="Reference to the stream template.", title="Stream Template"
1970
+ )
1971
+ components_resolver: HttpComponentsResolver = Field(
1972
+ ...,
1973
+ description="Component resolve and populates stream templates with components values.",
1974
+ title="Components Resolver",
1975
+ )
1976
+
1977
+
1886
1978
  CompositeErrorHandler.update_forward_refs()
1887
- DeclarativeSource.update_forward_refs()
1979
+ DeclarativeSource1.update_forward_refs()
1980
+ DeclarativeSource2.update_forward_refs()
1888
1981
  SelectiveAuthenticator.update_forward_refs()
1889
1982
  DeclarativeStream.update_forward_refs()
1890
1983
  SessionTokenAuthenticator.update_forward_refs()
@@ -31,6 +31,12 @@ DEFAULT_MODEL_TYPES: Mapping[str, str] = {
31
31
  # DeclarativeStream
32
32
  "DeclarativeStream.retriever": "SimpleRetriever",
33
33
  "DeclarativeStream.schema_loader": "JsonFileSchemaLoader",
34
+ # DynamicDeclarativeStream
35
+ "DynamicDeclarativeStream.stream_template": "DeclarativeStream",
36
+ "DynamicDeclarativeStream.components_resolver": "HttpComponentsResolver",
37
+ # HttpComponentsResolver
38
+ "HttpComponentsResolver.retriever": "SimpleRetriever",
39
+ "HttpComponentsResolver.components_mapping": "ComponentMappingDefinition",
34
40
  # DefaultErrorHandler
35
41
  "DefaultErrorHandler.response_filters": "HttpResponseFilter",
36
42
  # DefaultPaginator