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.
- airbyte_cdk/cli/airbyte_cdk/_connector.py +32 -8
- airbyte_cdk/cli/airbyte_cdk/_image.py +76 -0
- airbyte_cdk/cli/airbyte_cdk/_secrets.py +13 -12
- airbyte_cdk/models/airbyte_protocol_serializers.py +4 -0
- airbyte_cdk/models/connector_metadata.py +14 -0
- airbyte_cdk/sources/declarative/declarative_component_schema.yaml +41 -0
- airbyte_cdk/sources/declarative/models/declarative_component_schema.py +28 -1
- airbyte_cdk/sources/declarative/parsers/model_to_component_factory.py +29 -0
- airbyte_cdk/sources/declarative/resolvers/__init__.py +10 -0
- airbyte_cdk/sources/declarative/resolvers/parametrized_components_resolver.py +125 -0
- airbyte_cdk/test/entrypoint_wrapper.py +163 -26
- airbyte_cdk/test/models/scenario.py +49 -10
- airbyte_cdk/test/standard_tests/__init__.py +2 -4
- airbyte_cdk/test/standard_tests/connector_base.py +12 -80
- airbyte_cdk/test/standard_tests/docker_base.py +388 -0
- airbyte_cdk/test/standard_tests/pytest_hooks.py +115 -2
- airbyte_cdk/test/standard_tests/source_base.py +13 -7
- airbyte_cdk/test/standard_tests/util.py +4 -3
- airbyte_cdk/utils/connector_paths.py +3 -3
- airbyte_cdk/utils/docker.py +83 -34
- {airbyte_cdk-6.55.0.dist-info → airbyte_cdk-6.55.1.post11.dev15684355943.dist-info}/METADATA +2 -1
- {airbyte_cdk-6.55.0.dist-info → airbyte_cdk-6.55.1.post11.dev15684355943.dist-info}/RECORD +26 -24
- {airbyte_cdk-6.55.0.dist-info → airbyte_cdk-6.55.1.post11.dev15684355943.dist-info}/LICENSE.txt +0 -0
- {airbyte_cdk-6.55.0.dist-info → airbyte_cdk-6.55.1.post11.dev15684355943.dist-info}/LICENSE_SHORT +0 -0
- {airbyte_cdk-6.55.0.dist-info → airbyte_cdk-6.55.1.post11.dev15684355943.dist-info}/WHEEL +0 -0
- {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
|
-
|
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
|
162
|
-
pytest_args.
|
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=
|
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 =
|
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
|
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
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
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[
|
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
|