airbyte-cdk 6.55.0__py3-none-any.whl → 6.55.1.post11.dev15684355943__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 (26) hide show
  1. airbyte_cdk/cli/airbyte_cdk/_connector.py +32 -8
  2. airbyte_cdk/cli/airbyte_cdk/_image.py +76 -0
  3. airbyte_cdk/cli/airbyte_cdk/_secrets.py +13 -12
  4. airbyte_cdk/models/airbyte_protocol_serializers.py +4 -0
  5. airbyte_cdk/models/connector_metadata.py +14 -0
  6. airbyte_cdk/sources/declarative/declarative_component_schema.yaml +41 -0
  7. airbyte_cdk/sources/declarative/models/declarative_component_schema.py +28 -1
  8. airbyte_cdk/sources/declarative/parsers/model_to_component_factory.py +29 -0
  9. airbyte_cdk/sources/declarative/resolvers/__init__.py +10 -0
  10. airbyte_cdk/sources/declarative/resolvers/parametrized_components_resolver.py +125 -0
  11. airbyte_cdk/test/entrypoint_wrapper.py +163 -26
  12. airbyte_cdk/test/models/scenario.py +49 -10
  13. airbyte_cdk/test/standard_tests/__init__.py +2 -4
  14. airbyte_cdk/test/standard_tests/connector_base.py +12 -80
  15. airbyte_cdk/test/standard_tests/docker_base.py +388 -0
  16. airbyte_cdk/test/standard_tests/pytest_hooks.py +115 -2
  17. airbyte_cdk/test/standard_tests/source_base.py +13 -7
  18. airbyte_cdk/test/standard_tests/util.py +4 -3
  19. airbyte_cdk/utils/connector_paths.py +3 -3
  20. airbyte_cdk/utils/docker.py +83 -34
  21. {airbyte_cdk-6.55.0.dist-info → airbyte_cdk-6.55.1.post11.dev15684355943.dist-info}/METADATA +2 -1
  22. {airbyte_cdk-6.55.0.dist-info → airbyte_cdk-6.55.1.post11.dev15684355943.dist-info}/RECORD +26 -24
  23. {airbyte_cdk-6.55.0.dist-info → airbyte_cdk-6.55.1.post11.dev15684355943.dist-info}/LICENSE.txt +0 -0
  24. {airbyte_cdk-6.55.0.dist-info → airbyte_cdk-6.55.1.post11.dev15684355943.dist-info}/LICENSE_SHORT +0 -0
  25. {airbyte_cdk-6.55.0.dist-info → airbyte_cdk-6.55.1.post11.dev15684355943.dist-info}/WHEEL +0 -0
  26. {airbyte_cdk-6.55.0.dist-info → airbyte_cdk-6.55.1.post11.dev15684355943.dist-info}/entry_points.txt +0 -0
@@ -101,7 +101,7 @@ def connector_cli_group() -> None:
101
101
  pass
102
102
 
103
103
 
104
- @connector_cli_group.command()
104
+ @connector_cli_group.command("test")
105
105
  @click.argument(
106
106
  "connector",
107
107
  required=False,
@@ -114,10 +114,18 @@ def connector_cli_group() -> None:
114
114
  default=False,
115
115
  help="Only collect tests, do not run them.",
116
116
  )
117
- def test(
117
+ @click.option(
118
+ "--pytest-arg",
119
+ "pytest_args", # ← map --pytest-arg into pytest_args
120
+ type=str,
121
+ multiple=True,
122
+ help="Additional argument(s) to pass to pytest. Can be specified multiple times.",
123
+ )
124
+ def connector_test(
118
125
  connector: str | Path | None = None,
119
126
  *,
120
127
  collect_only: bool = False,
128
+ pytest_args: list[str] | None = None,
121
129
  ) -> None:
122
130
  """Run connector tests.
123
131
 
@@ -130,19 +138,36 @@ def test(
130
138
  directory. If the current working directory is not a connector directory (e.g. starting
131
139
  with 'source-') and no connector name or path is provided, the process will fail.
132
140
  """
141
+ click.echo("Connector test command executed.")
142
+ connector_name, connector_directory = resolve_connector_name_and_directory(connector)
143
+
144
+ pytest_args = pytest_args or []
145
+ if collect_only:
146
+ pytest_args.append("--collect-only")
147
+
148
+ run_connector_tests(
149
+ connector_name=connector_name,
150
+ connector_directory=connector_directory,
151
+ extra_pytest_args=pytest_args,
152
+ )
153
+
154
+
155
+ def run_connector_tests(
156
+ connector_name: str,
157
+ connector_directory: Path,
158
+ extra_pytest_args: list[str],
159
+ ) -> None:
133
160
  if pytest is None:
134
161
  raise ImportError(
135
162
  "pytest is not installed. Please install pytest to run the connector tests."
136
163
  )
137
- click.echo("Connector test command executed.")
138
- connector_name, connector_directory = resolve_connector_name_and_directory(connector)
139
164
 
140
165
  connector_test_suite = create_connector_test_suite(
141
166
  connector_name=connector_name if not connector_directory else None,
142
167
  connector_directory=connector_directory,
143
168
  )
144
169
 
145
- pytest_args: list[str] = []
170
+ pytest_args: list[str] = ["-p", "airbyte_cdk.test.standard_tests.pytest_hooks"]
146
171
  if connector_directory:
147
172
  pytest_args.append(f"--rootdir={connector_directory}")
148
173
  os.chdir(str(connector_directory))
@@ -158,8 +183,8 @@ def test(
158
183
  test_file_path.parent.mkdir(parents=True, exist_ok=True)
159
184
  test_file_path.write_text(file_text)
160
185
 
161
- if collect_only:
162
- pytest_args.append("--collect-only")
186
+ if extra_pytest_args:
187
+ pytest_args.extend(extra_pytest_args)
163
188
 
164
189
  pytest_args.append(str(test_file_path))
165
190
 
@@ -170,7 +195,6 @@ def test(
170
195
 
171
196
  click.echo(f"Running tests from connector directory: {connector_directory}...")
172
197
  click.echo(f"Test file: {test_file_path}")
173
- click.echo(f"Collect only: {collect_only}")
174
198
  click.echo(f"Pytest args: {pytest_args}")
175
199
  click.echo("Invoking Pytest...")
176
200
  exit_code = pytest.main(
@@ -10,6 +10,7 @@ from pathlib import Path
10
10
 
11
11
  import rich_click as click
12
12
 
13
+ from airbyte_cdk.cli.airbyte_cdk._connector import run_connector_tests
13
14
  from airbyte_cdk.models.connector_metadata import MetadataFile
14
15
  from airbyte_cdk.utils.connector_paths import resolve_connector_name_and_directory
15
16
  from airbyte_cdk.utils.docker import (
@@ -88,6 +89,81 @@ def build(
88
89
  sys.exit(1)
89
90
 
90
91
 
92
+ @image_cli_group.command("test")
93
+ @click.argument(
94
+ "connector",
95
+ required=False,
96
+ type=str,
97
+ metavar="[CONNECTOR]",
98
+ )
99
+ @click.option(
100
+ "--image",
101
+ help="Image to test, instead of building a new one.",
102
+ )
103
+ def image_test( # "image test" command
104
+ connector: str | None = None,
105
+ *,
106
+ image: str | None = None,
107
+ ) -> None:
108
+ """Test a connector Docker image.
109
+
110
+ [CONNECTOR] can be a connector name (e.g. 'source-pokeapi'), a path to a connector directory, or omitted to use the current working directory.
111
+ If a string containing '/' is provided, it is treated as a path. Otherwise, it is treated as a connector name.
112
+
113
+ If an image is provided, it will be used for testing instead of building a new one.
114
+
115
+ Note: You should run `airbyte-cdk secrets fetch` before running this command to ensure
116
+ that the secrets are available for the connector tests.
117
+ """
118
+ if not verify_docker_installation():
119
+ click.echo(
120
+ "Docker is not installed or not running. Please install Docker and try again.", err=True
121
+ )
122
+ sys.exit(1)
123
+
124
+ connector_name, connector_directory = resolve_connector_name_and_directory(connector)
125
+
126
+ # Select only tests with the 'image_tests' mark
127
+ pytest_args = ["-m", "image_tests"]
128
+ if not image:
129
+ metadata_file_path: Path = connector_directory / "metadata.yaml"
130
+ try:
131
+ metadata = MetadataFile.from_file(metadata_file_path)
132
+ except (FileNotFoundError, ValueError) as e:
133
+ click.echo(
134
+ f"Error loading metadata file '{metadata_file_path}': {e!s}",
135
+ err=True,
136
+ )
137
+ sys.exit(1)
138
+
139
+ tag = "dev-latest"
140
+ image = f"{metadata.data.dockerRepository}:{tag}"
141
+ click.echo(f"Building Image for Connector: {image}")
142
+ try:
143
+ image = build_connector_image(
144
+ connector_directory=connector_directory,
145
+ connector_name=connector_name,
146
+ metadata=metadata,
147
+ tag=tag,
148
+ no_verify=True,
149
+ )
150
+ except ConnectorImageBuildError as e:
151
+ click.echo(
152
+ f"Error building connector image: {e!s}",
153
+ err=True,
154
+ )
155
+ sys.exit(1)
156
+
157
+ pytest_args.extend(["--connector-image", image])
158
+
159
+ click.echo(f"Testing Connector Image: {image}")
160
+ run_connector_tests(
161
+ connector_name=connector_name,
162
+ connector_directory=connector_directory,
163
+ extra_pytest_args=pytest_args,
164
+ )
165
+
166
+
91
167
  __all__ = [
92
168
  "image_cli_group",
93
169
  ]
@@ -99,12 +99,12 @@ def secrets_cli_group() -> None:
99
99
  help="Print GitHub CI mask for secrets.",
100
100
  type=bool,
101
101
  is_flag=True,
102
- default=False,
102
+ default=None,
103
103
  )
104
104
  def fetch(
105
105
  connector: str | Path | None = None,
106
106
  gcp_project_id: str = GCP_PROJECT_ID,
107
- print_ci_secrets_masks: bool = False,
107
+ print_ci_secrets_masks: bool | None = None,
108
108
  ) -> None:
109
109
  """Fetch secrets for a connector from Google Secret Manager.
110
110
 
@@ -181,22 +181,23 @@ def fetch(
181
181
  if secret_count == 0:
182
182
  raise exceptions[0]
183
183
 
184
- if not print_ci_secrets_masks:
185
- return
186
-
187
- if not os.environ.get("CI", None):
184
+ if print_ci_secrets_masks and "CI" not in os.environ:
188
185
  click.echo(
189
186
  "The `--print-ci-secrets-masks` option is only available in CI environments. "
190
187
  "The `CI` env var is either not set or not set to a truthy value. "
191
188
  "Skipping printing secret masks.",
192
189
  err=True,
193
190
  )
194
- return
195
-
196
- # Else print the CI mask
197
- _print_ci_secrets_masks(
198
- secrets_dir=secrets_dir,
199
- )
191
+ print_ci_secrets_masks = False
192
+ elif print_ci_secrets_masks is None:
193
+ # If not explicitly set, we check if we are in a CI environment
194
+ # and set to True if so.
195
+ print_ci_secrets_masks = os.environ.get("CI", "") != ""
196
+
197
+ if print_ci_secrets_masks:
198
+ _print_ci_secrets_masks(
199
+ secrets_dir=secrets_dir,
200
+ )
200
201
 
201
202
 
202
203
  @secrets_cli_group.command("list")
@@ -4,9 +4,11 @@ from typing import Any, Dict
4
4
  from serpyco_rs import CustomType, Serializer
5
5
 
6
6
  from .airbyte_protocol import ( # type: ignore[attr-defined] # all classes are imported to airbyte_protocol via *
7
+ AirbyteCatalog,
7
8
  AirbyteMessage,
8
9
  AirbyteStateBlob,
9
10
  AirbyteStateMessage,
11
+ AirbyteStream,
10
12
  AirbyteStreamState,
11
13
  ConfiguredAirbyteCatalog,
12
14
  ConfiguredAirbyteStream,
@@ -30,6 +32,8 @@ def custom_type_resolver(t: type) -> CustomType[AirbyteStateBlob, Dict[str, Any]
30
32
  return AirbyteStateBlobType() if t is AirbyteStateBlob else None
31
33
 
32
34
 
35
+ AirbyteCatalogSerializer = Serializer(AirbyteCatalog, omit_none=True)
36
+ AirbyteStreamSerializer = Serializer(AirbyteStream, omit_none=True)
33
37
  AirbyteStreamStateSerializer = Serializer(
34
38
  AirbyteStreamState, omit_none=True, custom_type_resolver=custom_type_resolver
35
39
  )
@@ -34,6 +34,15 @@ class ConnectorBuildOptions(BaseModel):
34
34
  )
35
35
 
36
36
 
37
+ class SuggestedStreams(BaseModel):
38
+ """Suggested streams from metadata.yaml."""
39
+
40
+ streams: list[str] = Field(
41
+ default=[],
42
+ description="List of suggested streams for the connector",
43
+ )
44
+
45
+
37
46
  class ConnectorMetadata(BaseModel):
38
47
  """Connector metadata from metadata.yaml."""
39
48
 
@@ -47,6 +56,11 @@ class ConnectorMetadata(BaseModel):
47
56
  description="List of tags for the connector",
48
57
  )
49
58
 
59
+ suggestedStreams: SuggestedStreams | None = Field(
60
+ default=None,
61
+ description="Suggested streams for the connector",
62
+ )
63
+
50
64
  @property
51
65
  def language(self) -> ConnectorLanguage:
52
66
  """Get the connector language."""
@@ -4186,6 +4186,46 @@ definitions:
4186
4186
  - type
4187
4187
  - stream_config
4188
4188
  - components_mapping
4189
+ StreamParametersDefinition:
4190
+ title: Stream Parameters Definition
4191
+ description: (This component is experimental. Use at your own risk.) Represents a stream parameters definition to set up dynamic streams from defined values in manifest.
4192
+ type: object
4193
+ required:
4194
+ - type
4195
+ - list_of_parameters_for_stream
4196
+ properties:
4197
+ type:
4198
+ type: string
4199
+ enum: [StreamParametersDefinition]
4200
+ list_of_parameters_for_stream:
4201
+ title: Stream Parameters
4202
+ description: A list of object of parameters for stream, each object in the list represents params for one stream.
4203
+ type: array
4204
+ items:
4205
+ type: object
4206
+ examples:
4207
+ - [{"name": "test stream", "$parameters": {"entity": "test entity"}, "primary_key": "test key"}]
4208
+ ParametrizedComponentsResolver:
4209
+ type: object
4210
+ title: Parametrized Components Resolver
4211
+ description: (This component is experimental. Use at your own risk.) Resolves and populates dynamic streams from defined parametrized values in manifest.
4212
+ properties:
4213
+ type:
4214
+ type: string
4215
+ enum: [ParametrizedComponentsResolver]
4216
+ stream_parameters:
4217
+ "$ref": "#/definitions/StreamParametersDefinition"
4218
+ components_mapping:
4219
+ type: array
4220
+ items:
4221
+ "$ref": "#/definitions/ComponentMappingDefinition"
4222
+ $parameters:
4223
+ type: object
4224
+ additionalProperties: true
4225
+ required:
4226
+ - type
4227
+ - stream_parameters
4228
+ - components_mapping
4189
4229
  DynamicDeclarativeStream:
4190
4230
  type: object
4191
4231
  description: (This component is experimental. Use at your own risk.) A component that described how will be created declarative streams based on stream template.
@@ -4212,6 +4252,7 @@ definitions:
4212
4252
  anyOf:
4213
4253
  - "$ref": "#/definitions/HttpComponentsResolver"
4214
4254
  - "$ref": "#/definitions/ConfigComponentsResolver"
4255
+ - "$ref": "#/definitions/ParametrizedComponentsResolver"
4215
4256
  required:
4216
4257
  - type
4217
4258
  - stream_template
@@ -1512,6 +1512,31 @@ class ConfigComponentsResolver(BaseModel):
1512
1512
  parameters: Optional[Dict[str, Any]] = Field(None, alias="$parameters")
1513
1513
 
1514
1514
 
1515
+ class StreamParametersDefinition(BaseModel):
1516
+ type: Literal["StreamParametersDefinition"]
1517
+ list_of_parameters_for_stream: List[Dict[str, Any]] = Field(
1518
+ ...,
1519
+ description="A list of object of parameters for stream, each object in the list represents params for one stream.",
1520
+ examples=[
1521
+ [
1522
+ {
1523
+ "name": "test stream",
1524
+ "$parameters": {"entity": "test entity"},
1525
+ "primary_key": "test key",
1526
+ }
1527
+ ]
1528
+ ],
1529
+ title="Stream Parameters",
1530
+ )
1531
+
1532
+
1533
+ class ParametrizedComponentsResolver(BaseModel):
1534
+ type: Literal["ParametrizedComponentsResolver"]
1535
+ stream_parameters: StreamParametersDefinition
1536
+ components_mapping: List[ComponentMappingDefinition]
1537
+ parameters: Optional[Dict[str, Any]] = Field(None, alias="$parameters")
1538
+
1539
+
1515
1540
  class RequestBodyPlainText(BaseModel):
1516
1541
  type: Literal["RequestBodyPlainText"]
1517
1542
  value: str
@@ -2943,7 +2968,9 @@ class DynamicDeclarativeStream(BaseModel):
2943
2968
  stream_template: Union[DeclarativeStream, StateDelegatingStream] = Field(
2944
2969
  ..., description="Reference to the stream template.", title="Stream Template"
2945
2970
  )
2946
- components_resolver: Union[HttpComponentsResolver, ConfigComponentsResolver] = Field(
2971
+ components_resolver: Union[
2972
+ HttpComponentsResolver, ConfigComponentsResolver, ParametrizedComponentsResolver
2973
+ ] = Field(
2947
2974
  ...,
2948
2975
  description="Component resolve and populates stream templates with components values.",
2949
2976
  title="Components Resolver",
@@ -351,6 +351,9 @@ from airbyte_cdk.sources.declarative.models.declarative_component_schema import
351
351
  from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
352
352
  PageIncrement as PageIncrementModel,
353
353
  )
354
+ from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
355
+ ParametrizedComponentsResolver as ParametrizedComponentsResolverModel,
356
+ )
354
357
  from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
355
358
  ParentStreamConfig as ParentStreamConfigModel,
356
359
  )
@@ -504,7 +507,9 @@ from airbyte_cdk.sources.declarative.resolvers import (
504
507
  ComponentMappingDefinition,
505
508
  ConfigComponentsResolver,
506
509
  HttpComponentsResolver,
510
+ ParametrizedComponentsResolver,
507
511
  StreamConfig,
512
+ StreamParametersDefinition,
508
513
  )
509
514
  from airbyte_cdk.sources.declarative.retrievers import (
510
515
  AsyncRetriever,
@@ -738,6 +743,7 @@ class ModelToComponentFactory:
738
743
  AsyncRetrieverModel: self.create_async_retriever,
739
744
  HttpComponentsResolverModel: self.create_http_components_resolver,
740
745
  ConfigComponentsResolverModel: self.create_config_components_resolver,
746
+ ParametrizedComponentsResolverModel: self.create_parametrized_components_resolver,
741
747
  StreamConfigModel: self.create_stream_config,
742
748
  ComponentMappingDefinitionModel: self.create_components_mapping_definition,
743
749
  ZipfileDecoderModel: self.create_zipfile_decoder,
@@ -3861,6 +3867,29 @@ class ModelToComponentFactory:
3861
3867
  parameters=model.parameters or {},
3862
3868
  )
3863
3869
 
3870
+ def create_parametrized_components_resolver(
3871
+ self, model: ParametrizedComponentsResolverModel, config: Config
3872
+ ) -> ParametrizedComponentsResolver:
3873
+ stream_parameters = StreamParametersDefinition(
3874
+ list_of_parameters_for_stream=model.stream_parameters.list_of_parameters_for_stream
3875
+ )
3876
+ components_mapping = [
3877
+ self._create_component_from_model(
3878
+ model=components_mapping_definition_model,
3879
+ value_type=ModelToComponentFactory._json_schema_type_name_to_type(
3880
+ components_mapping_definition_model.value_type
3881
+ ),
3882
+ config=config,
3883
+ )
3884
+ for components_mapping_definition_model in model.components_mapping
3885
+ ]
3886
+ return ParametrizedComponentsResolver(
3887
+ stream_parameters=stream_parameters,
3888
+ config=config,
3889
+ components_mapping=components_mapping,
3890
+ parameters=model.parameters or {},
3891
+ )
3892
+
3864
3893
  _UNSUPPORTED_DECODER_ERROR = (
3865
3894
  "Specified decoder of {decoder_type} is not supported for pagination."
3866
3895
  "Please set as `JsonDecoder`, `XmlDecoder`, or a `CompositeRawDecoder` with an inner_parser of `JsonParser` or `GzipParser` instead."
@@ -12,6 +12,9 @@ from airbyte_cdk.sources.declarative.models import (
12
12
  from airbyte_cdk.sources.declarative.models import (
13
13
  HttpComponentsResolver as HttpComponentsResolverModel,
14
14
  )
15
+ from airbyte_cdk.sources.declarative.models import (
16
+ ParametrizedComponentsResolver as ParametrizedComponentsResolverModel,
17
+ )
15
18
  from airbyte_cdk.sources.declarative.resolvers.components_resolver import (
16
19
  ComponentMappingDefinition,
17
20
  ComponentsResolver,
@@ -24,10 +27,15 @@ from airbyte_cdk.sources.declarative.resolvers.config_components_resolver import
24
27
  from airbyte_cdk.sources.declarative.resolvers.http_components_resolver import (
25
28
  HttpComponentsResolver,
26
29
  )
30
+ from airbyte_cdk.sources.declarative.resolvers.parametrized_components_resolver import (
31
+ ParametrizedComponentsResolver,
32
+ StreamParametersDefinition,
33
+ )
27
34
 
28
35
  COMPONENTS_RESOLVER_TYPE_MAPPING: Mapping[str, type[BaseModel]] = {
29
36
  "HttpComponentsResolver": HttpComponentsResolverModel,
30
37
  "ConfigComponentsResolver": ConfigComponentsResolverModel,
38
+ "ParametrizedComponentsResolver": ParametrizedComponentsResolverModel,
31
39
  }
32
40
 
33
41
  __all__ = [
@@ -38,4 +46,6 @@ __all__ = [
38
46
  "StreamConfig",
39
47
  "ConfigComponentsResolver",
40
48
  "COMPONENTS_RESOLVER_TYPE_MAPPING",
49
+ "ParametrizedComponentsResolver",
50
+ "StreamParametersDefinition",
41
51
  ]
@@ -0,0 +1,125 @@
1
+ #
2
+ # Copyright (c) 2024 Airbyte, Inc., all rights reserved.
3
+ #
4
+
5
+ from copy import deepcopy
6
+ from dataclasses import InitVar, dataclass, field
7
+ from typing import Any, Dict, Iterable, List, Mapping
8
+
9
+ import dpath
10
+ import yaml
11
+ from typing_extensions import deprecated
12
+ from yaml.parser import ParserError
13
+
14
+ from airbyte_cdk.sources.declarative.interpolation import InterpolatedString
15
+ from airbyte_cdk.sources.declarative.resolvers.components_resolver import (
16
+ ComponentMappingDefinition,
17
+ ComponentsResolver,
18
+ ResolvedComponentMappingDefinition,
19
+ )
20
+ from airbyte_cdk.sources.source import ExperimentalClassWarning
21
+ from airbyte_cdk.sources.types import Config
22
+
23
+
24
+ @deprecated("This class is experimental. Use at your own risk.", category=ExperimentalClassWarning)
25
+ @dataclass
26
+ class StreamParametersDefinition:
27
+ """
28
+ Represents a stream parameters definition to set up dynamic streams from defined values in manifest.
29
+ """
30
+
31
+ list_of_parameters_for_stream: List[Dict[str, Any]]
32
+
33
+
34
+ @deprecated("This class is experimental. Use at your own risk.", category=ExperimentalClassWarning)
35
+ @dataclass
36
+ class ParametrizedComponentsResolver(ComponentsResolver):
37
+ """
38
+ Resolves and populates dynamic streams from defined parametrized values in manifest.
39
+ """
40
+
41
+ stream_parameters: StreamParametersDefinition
42
+ config: Config
43
+ components_mapping: List[ComponentMappingDefinition]
44
+ parameters: InitVar[Mapping[str, Any]]
45
+ _resolved_components: List[ResolvedComponentMappingDefinition] = field(
46
+ init=False, repr=False, default_factory=list
47
+ )
48
+
49
+ def __post_init__(self, parameters: Mapping[str, Any]) -> None:
50
+ """
51
+ Initializes and parses component mappings, converting them to resolved definitions.
52
+
53
+ Args:
54
+ parameters (Mapping[str, Any]): Parameters for interpolation.
55
+ """
56
+
57
+ for component_mapping in self.components_mapping:
58
+ if isinstance(component_mapping.value, (str, InterpolatedString)):
59
+ interpolated_value = (
60
+ InterpolatedString.create(component_mapping.value, parameters=parameters)
61
+ if isinstance(component_mapping.value, str)
62
+ else component_mapping.value
63
+ )
64
+
65
+ field_path = [
66
+ InterpolatedString.create(path, parameters=parameters)
67
+ for path in component_mapping.field_path
68
+ ]
69
+
70
+ self._resolved_components.append(
71
+ ResolvedComponentMappingDefinition(
72
+ field_path=field_path,
73
+ value=interpolated_value,
74
+ value_type=component_mapping.value_type,
75
+ create_or_update=component_mapping.create_or_update,
76
+ parameters=parameters,
77
+ )
78
+ )
79
+ else:
80
+ raise ValueError(
81
+ f"Expected a string or InterpolatedString for value in mapping: {component_mapping}"
82
+ )
83
+
84
+ def resolve_components(
85
+ self, stream_template_config: Dict[str, Any]
86
+ ) -> Iterable[Dict[str, Any]]:
87
+ kwargs = {"stream_template_config": stream_template_config}
88
+
89
+ for components_values in self.stream_parameters.list_of_parameters_for_stream:
90
+ updated_config = deepcopy(stream_template_config)
91
+ kwargs["components_values"] = components_values # type: ignore[assignment] # component_values will always be of type Mapping[str, Any]
92
+ for resolved_component in self._resolved_components:
93
+ valid_types = (
94
+ (resolved_component.value_type,) if resolved_component.value_type else None
95
+ )
96
+ value = resolved_component.value.eval(
97
+ self.config, valid_types=valid_types, **kwargs
98
+ )
99
+ path = [path.eval(self.config, **kwargs) for path in resolved_component.field_path]
100
+ parsed_value = self._parse_yaml_if_possible(value)
101
+ # https://github.com/dpath-maintainers/dpath-python/blob/master/dpath/__init__.py#L136
102
+ # dpath.set returns the number of changed elements, 0 when no elements changed
103
+ updated = dpath.set(updated_config, path, parsed_value)
104
+
105
+ if parsed_value and not updated and resolved_component.create_or_update:
106
+ dpath.new(updated_config, path, parsed_value)
107
+
108
+ yield updated_config
109
+
110
+ @staticmethod
111
+ def _parse_yaml_if_possible(value: Any) -> Any:
112
+ """
113
+ Try to turn value into a Python object by YAML-parsing it.
114
+
115
+ * If value is a `str` and can be parsed by `yaml.safe_load`,
116
+ return the parsed result.
117
+ * If parsing fails (`yaml.parser.ParserError`) – or value is not
118
+ a string at all – return the original value unchanged.
119
+ """
120
+ if isinstance(value, str):
121
+ try:
122
+ return yaml.safe_load(value)
123
+ except ParserError: # "{{ record[0] in ['cohortActiveUsers'] }}" # not valid YAML
124
+ return value
125
+ return value