diff --git a/airbyte_cdk/connector_builder/connector_builder_handler.py b/airbyte_cdk/connector_builder/connector_builder_handler.py index 56da5f848..c80884d09 100644 --- a/airbyte_cdk/connector_builder/connector_builder_handler.py +++ b/airbyte_cdk/connector_builder/connector_builder_handler.py @@ -117,5 +117,30 @@ def resolve_manifest(source: ManifestDeclarativeSource) -> AirbyteMessage: return error.as_airbyte_message() +def full_resolve_manifest(source: ManifestDeclarativeSource) -> AirbyteMessage: + try: + manifest = {**source.resolved_manifest} + streams = manifest.get("streams", []) + for stream in streams: + stream["dynamic_stream_name"] = None + streams.extend(source.dynamic_streams) + manifest["streams"] = streams + return AirbyteMessage( + type=Type.RECORD, + record=AirbyteRecordMessage( + data={"manifest": manifest}, + emitted_at=_emitted_at(), + stream="full_resolve_manifest", + ), + ) + except AirbyteTracedException as exc: + return exc.as_airbyte_message() + except Exception as exc: + error = AirbyteTracedException.from_exception( + exc, message=f"Error full resolving manifest: {str(exc)}" + ) + return error.as_airbyte_message() + + def _emitted_at() -> int: return ab_datetime_now().to_epoch_millis() diff --git a/airbyte_cdk/connector_builder/main.py b/airbyte_cdk/connector_builder/main.py index e122cee8c..525df8752 100644 --- a/airbyte_cdk/connector_builder/main.py +++ b/airbyte_cdk/connector_builder/main.py @@ -12,6 +12,7 @@ from airbyte_cdk.connector_builder.connector_builder_handler import ( TestReadLimits, create_source, + full_resolve_manifest, get_limits, read_stream, resolve_manifest, @@ -81,6 +82,8 @@ def handle_connector_builder_request( catalog is not None ), "`test_read` requires a valid `ConfiguredAirbyteCatalog`, got None." return read_stream(source, config, catalog, state, limits) + elif command == "full_resolve_manifest": + return full_resolve_manifest(source) else: raise ValueError(f"Unrecognized command {command}.") diff --git a/airbyte_cdk/sources/declarative/declarative_component_schema.yaml b/airbyte_cdk/sources/declarative/declarative_component_schema.yaml index b7c0d84a0..2573e8b8a 100644 --- a/airbyte_cdk/sources/declarative/declarative_component_schema.yaml +++ b/airbyte_cdk/sources/declarative/declarative_component_schema.yaml @@ -3766,6 +3766,13 @@ definitions: type: type: string enum: [DynamicDeclarativeStream] + name: + title: Name + description: The dynamic stream name. + type: string + default: "" + example: + - "Tables" stream_template: title: Stream Template description: Reference to the stream template. diff --git a/airbyte_cdk/sources/declarative/manifest_declarative_source.py b/airbyte_cdk/sources/declarative/manifest_declarative_source.py index 23d41b174..ba0ea5eca 100644 --- a/airbyte_cdk/sources/declarative/manifest_declarative_source.py +++ b/airbyte_cdk/sources/declarative/manifest_declarative_source.py @@ -106,6 +106,7 @@ def __init__( AlwaysLogSliceLogger() if emit_connector_builder_messages else DebugSliceLogger() ) + self._config = config or {} self._validate_source() @property @@ -116,6 +117,12 @@ def resolved_manifest(self) -> Mapping[str, Any]: def message_repository(self) -> MessageRepository: return self._message_repository + @property + def dynamic_streams(self) -> List[Dict[str, Any]]: + return self._dynamic_stream_configs( + manifest=self._source_config, config=self._config, with_dynamic_stream_name=True + ) + @property def connection_checker(self) -> ConnectionChecker: check = self._source_config["check"] @@ -348,13 +355,16 @@ def _stream_configs(self, manifest: Mapping[str, Any]) -> List[Dict[str, Any]]: return stream_configs def _dynamic_stream_configs( - self, manifest: Mapping[str, Any], config: Mapping[str, Any] + self, + manifest: Mapping[str, Any], + config: Mapping[str, Any], + with_dynamic_stream_name: Optional[bool] = None, ) -> List[Dict[str, Any]]: dynamic_stream_definitions: List[Dict[str, Any]] = manifest.get("dynamic_streams", []) dynamic_stream_configs: List[Dict[str, Any]] = [] seen_dynamic_streams: Set[str] = set() - for dynamic_definition in dynamic_stream_definitions: + for dynamic_definition_index, dynamic_definition in enumerate(dynamic_stream_definitions): components_resolver_config = dynamic_definition["components_resolver"] if not components_resolver_config: @@ -393,6 +403,11 @@ def _dynamic_stream_configs( # Ensure that each stream is created with a unique name name = dynamic_stream.get("name") + if with_dynamic_stream_name: + dynamic_stream["dynamic_stream_name"] = dynamic_definition.get( + "name", f"dynamic_stream_{dynamic_definition_index}" + ) + if not isinstance(name, str): raise ValueError( f"Expected stream name {name} to be a string, got {type(name)}." diff --git a/airbyte_cdk/sources/declarative/models/declarative_component_schema.py b/airbyte_cdk/sources/declarative/models/declarative_component_schema.py index c43f550db..021a99729 100644 --- a/airbyte_cdk/sources/declarative/models/declarative_component_schema.py +++ b/airbyte_cdk/sources/declarative/models/declarative_component_schema.py @@ -2489,6 +2489,9 @@ class HttpComponentsResolver(BaseModel): class DynamicDeclarativeStream(BaseModel): type: Literal["DynamicDeclarativeStream"] + name: Optional[str] = Field( + "", description="The dynamic stream name.", example=["Tables"], title="Name" + ) stream_template: DeclarativeStream = Field( ..., description="Reference to the stream template.", title="Stream Template" ) diff --git a/unit_tests/connector_builder/test_connector_builder_handler.py b/unit_tests/connector_builder/test_connector_builder_handler.py index af5968faa..4b3d80237 100644 --- a/unit_tests/connector_builder/test_connector_builder_handler.py +++ b/unit_tests/connector_builder/test_connector_builder_handler.py @@ -60,6 +60,7 @@ from airbyte_cdk.sources.declarative.manifest_declarative_source import ManifestDeclarativeSource from airbyte_cdk.sources.declarative.retrievers import SimpleRetrieverTestReadDecorator from airbyte_cdk.sources.declarative.retrievers.simple_retriever import SimpleRetriever +from airbyte_cdk.test.mock_http import HttpMocker, HttpRequest, HttpResponse from airbyte_cdk.utils.airbyte_secrets_utils import filter_secrets, update_secrets from unit_tests.connector_builder.utils import create_configured_catalog @@ -154,6 +155,179 @@ }, } +DYNAMIC_STREAM_MANIFEST = { + "version": "0.30.3", + "definitions": { + "retriever": { + "paginator": { + "type": "DefaultPaginator", + "page_size": _page_size, + "page_size_option": {"inject_into": "request_parameter", "field_name": "page_size"}, + "page_token_option": {"inject_into": "path", "type": "RequestPath"}, + "pagination_strategy": { + "type": "CursorPagination", + "cursor_value": "{{ response._metadata.next }}", + "page_size": _page_size, + }, + }, + "partition_router": { + "type": "ListPartitionRouter", + "values": ["0", "1", "2", "3", "4", "5", "6", "7"], + "cursor_field": "item_id", + }, + "" "requester": { + "path": "/v3/marketing/lists", + "authenticator": { + "type": "BearerAuthenticator", + "api_token": "{{ config.apikey }}", + }, + "request_parameters": {"a_param": "10"}, + }, + "record_selector": {"extractor": {"field_path": ["result"]}}, + }, + }, + "streams": [ + { + "type": "DeclarativeStream", + "$parameters": _stream_options, + "retriever": "#/definitions/retriever", + }, + ], + "check": {"type": "CheckStream", "stream_names": ["lists"]}, + "spec": { + "connection_specification": { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "required": [], + "properties": {}, + "additionalProperties": True, + }, + "type": "Spec", + }, + "dynamic_streams": [ + { + "type": "DynamicDeclarativeStream", + "name": "TestDynamicStream", + "stream_template": { + "type": "DeclarativeStream", + "name": "", + "primary_key": [], + "schema_loader": { + "type": "InlineSchemaLoader", + "schema": { + "$schema": "http://json-schema.org/schema#", + "properties": { + "ABC": {"type": "number"}, + "AED": {"type": "number"}, + }, + "type": "object", + }, + }, + "retriever": { + "type": "SimpleRetriever", + "requester": { + "type": "HttpRequester", + "url_base": "https://api.test.com", + "path": "", + "http_method": "GET", + "authenticator": { + "type": "ApiKeyAuthenticator", + "header": "apikey", + "api_token": "{{ config['api_key'] }}", + }, + }, + "record_selector": { + "type": "RecordSelector", + "extractor": {"type": "DpathExtractor", "field_path": []}, + }, + "paginator": {"type": "NoPagination"}, + }, + }, + "components_resolver": { + "type": "HttpComponentsResolver", + "retriever": { + "type": "SimpleRetriever", + "requester": { + "type": "HttpRequester", + "url_base": "https://api.test.com", + "path": "parent/{{ stream_partition.parent_id }}/items", + "http_method": "GET", + "authenticator": { + "type": "ApiKeyAuthenticator", + "header": "apikey", + "api_token": "{{ config['api_key'] }}", + }, + }, + "record_selector": { + "type": "RecordSelector", + "extractor": {"type": "DpathExtractor", "field_path": []}, + }, + "paginator": {"type": "NoPagination"}, + "partition_router": { + "type": "SubstreamPartitionRouter", + "parent_stream_configs": [ + { + "type": "ParentStreamConfig", + "parent_key": "id", + "partition_field": "parent_id", + "stream": { + "type": "DeclarativeStream", + "name": "parent", + "retriever": { + "type": "SimpleRetriever", + "requester": { + "type": "HttpRequester", + "url_base": "https://api.test.com", + "path": "/parents", + "http_method": "GET", + "authenticator": { + "type": "ApiKeyAuthenticator", + "header": "apikey", + "api_token": "{{ config['api_key'] }}", + }, + }, + "record_selector": { + "type": "RecordSelector", + "extractor": { + "type": "DpathExtractor", + "field_path": [], + }, + }, + }, + "schema_loader": { + "type": "InlineSchemaLoader", + "schema": { + "$schema": "http://json-schema.org/schema#", + "properties": {"id": {"type": "integer"}}, + "type": "object", + }, + }, + }, + } + ], + }, + }, + "components_mapping": [ + { + "type": "ComponentMappingDefinition", + "field_path": ["name"], + "value": "parent_{{stream_slice['parent_id']}}_{{components_values['name']}}", + }, + { + "type": "ComponentMappingDefinition", + "field_path": [ + "retriever", + "requester", + "path", + ], + "value": "{{ stream_slice['parent_id'] }}/{{ components_values['id'] }}", + }, + ], + }, + } + ], +} + OAUTH_MANIFEST = { "version": "0.30.3", "definitions": { @@ -207,6 +381,11 @@ "__command": "resolve_manifest", } +RESOLVE_DYNAMIC_STREAM_MANIFEST_CONFIG = { + "__injected_declarative_manifest": DYNAMIC_STREAM_MANIFEST, + "__command": "full_resolve_manifest", +} + TEST_READ_CONFIG = { "__injected_declarative_manifest": MANIFEST, "__command": "test_read", @@ -1182,3 +1361,465 @@ def test_read_stream_exception_with_secrets(): assert response.type == Type.TRACE assert filtered_message in response.trace.error.message assert "super_secret_key" not in response.trace.error.message + + +def test_full_resolve_manifest(valid_resolve_manifest_config_file): + config = copy.deepcopy(RESOLVE_DYNAMIC_STREAM_MANIFEST_CONFIG) + command = config["__command"] + source = ManifestDeclarativeSource(source_config=DYNAMIC_STREAM_MANIFEST) + limits = TestReadLimits() + with HttpMocker() as http_mocker: + http_mocker.get( + HttpRequest(url="https://api.test.com/parents"), + HttpResponse(body=json.dumps([{"id": 1}, {"id": 2}])), + ) + parent_ids = [1, 2] + for parent_id in parent_ids: + http_mocker.get( + HttpRequest(url=f"https://api.test.com/parent/{parent_id}/items"), + HttpResponse( + body=json.dumps( + [ + {"id": 1, "name": "item_1"}, + {"id": 2, "name": "item_2"}, + ] + ) + ), + ) + resolved_manifest = handle_connector_builder_request( + source, command, config, create_configured_catalog("dummy_stream"), _A_STATE, limits + ) + + expected_resolved_manifest = { + "version": "0.30.3", + "definitions": { + "retriever": { + "paginator": { + "type": "DefaultPaginator", + "page_size": 2, + "page_size_option": { + "inject_into": "request_parameter", + "field_name": "page_size", + }, + "page_token_option": {"inject_into": "path", "type": "RequestPath"}, + "pagination_strategy": { + "type": "CursorPagination", + "cursor_value": "{{ response._metadata.next }}", + "page_size": 2, + }, + }, + "partition_router": { + "type": "ListPartitionRouter", + "values": ["0", "1", "2", "3", "4", "5", "6", "7"], + "cursor_field": "item_id", + }, + "requester": { + "path": "/v3/marketing/lists", + "authenticator": { + "type": "BearerAuthenticator", + "api_token": "{{ config.apikey }}", + }, + "request_parameters": {"a_param": "10"}, + }, + "record_selector": {"extractor": {"field_path": ["result"]}}, + } + }, + "streams": [ + { + "type": "DeclarativeStream", + "retriever": { + "paginator": { + "type": "DefaultPaginator", + "page_size": 2, + "page_size_option": { + "inject_into": "request_parameter", + "field_name": "page_size", + "type": "RequestOption", + "name": "stream_with_custom_requester", + "primary_key": "id", + "url_base": "https://10.0.27.27/api/v1/", + "$parameters": { + "name": "stream_with_custom_requester", + "primary_key": "id", + "url_base": "https://10.0.27.27/api/v1/", + }, + }, + "page_token_option": { + "inject_into": "path", + "type": "RequestPath", + "name": "stream_with_custom_requester", + "primary_key": "id", + "url_base": "https://10.0.27.27/api/v1/", + "$parameters": { + "name": "stream_with_custom_requester", + "primary_key": "id", + "url_base": "https://10.0.27.27/api/v1/", + }, + }, + "pagination_strategy": { + "type": "CursorPagination", + "cursor_value": "{{ response._metadata.next }}", + "page_size": 2, + "name": "stream_with_custom_requester", + "primary_key": "id", + "url_base": "https://10.0.27.27/api/v1/", + "$parameters": { + "name": "stream_with_custom_requester", + "primary_key": "id", + "url_base": "https://10.0.27.27/api/v1/", + }, + }, + "name": "stream_with_custom_requester", + "primary_key": "id", + "url_base": "https://10.0.27.27/api/v1/", + "$parameters": { + "name": "stream_with_custom_requester", + "primary_key": "id", + "url_base": "https://10.0.27.27/api/v1/", + }, + }, + "partition_router": { + "type": "ListPartitionRouter", + "values": ["0", "1", "2", "3", "4", "5", "6", "7"], + "cursor_field": "item_id", + "name": "stream_with_custom_requester", + "primary_key": "id", + "url_base": "https://10.0.27.27/api/v1/", + "$parameters": { + "name": "stream_with_custom_requester", + "primary_key": "id", + "url_base": "https://10.0.27.27/api/v1/", + }, + }, + "requester": { + "path": "/v3/marketing/lists", + "authenticator": { + "type": "BearerAuthenticator", + "api_token": "{{ config.apikey }}", + "name": "stream_with_custom_requester", + "primary_key": "id", + "url_base": "https://10.0.27.27/api/v1/", + "$parameters": { + "name": "stream_with_custom_requester", + "primary_key": "id", + "url_base": "https://10.0.27.27/api/v1/", + }, + }, + "request_parameters": {"a_param": "10"}, + "type": "HttpRequester", + "name": "stream_with_custom_requester", + "primary_key": "id", + "url_base": "https://10.0.27.27/api/v1/", + "$parameters": { + "name": "stream_with_custom_requester", + "primary_key": "id", + "url_base": "https://10.0.27.27/api/v1/", + }, + }, + "record_selector": { + "extractor": { + "field_path": ["result"], + "type": "DpathExtractor", + "name": "stream_with_custom_requester", + "primary_key": "id", + "url_base": "https://10.0.27.27/api/v1/", + "$parameters": { + "name": "stream_with_custom_requester", + "primary_key": "id", + "url_base": "https://10.0.27.27/api/v1/", + }, + }, + "type": "RecordSelector", + "name": "stream_with_custom_requester", + "primary_key": "id", + "url_base": "https://10.0.27.27/api/v1/", + "$parameters": { + "name": "stream_with_custom_requester", + "primary_key": "id", + "url_base": "https://10.0.27.27/api/v1/", + }, + }, + "type": "SimpleRetriever", + "name": "stream_with_custom_requester", + "primary_key": "id", + "url_base": "https://10.0.27.27/api/v1/", + "$parameters": { + "name": "stream_with_custom_requester", + "primary_key": "id", + "url_base": "https://10.0.27.27/api/v1/", + }, + }, + "name": "stream_with_custom_requester", + "primary_key": "id", + "url_base": "https://10.0.27.27/api/v1/", + "$parameters": { + "name": "stream_with_custom_requester", + "primary_key": "id", + "url_base": "https://10.0.27.27/api/v1/", + }, + "dynamic_stream_name": None, + }, + { + "type": "DeclarativeStream", + "name": "parent_1_item_1", + "primary_key": [], + "schema_loader": { + "type": "InlineSchemaLoader", + "schema": { + "$schema": "http://json-schema.org/schema#", + "properties": {"ABC": {"type": "number"}, "AED": {"type": "number"}}, + "type": "object", + }, + }, + "retriever": { + "type": "SimpleRetriever", + "requester": { + "type": "HttpRequester", + "url_base": "https://api.test.com", + "path": "1/1", + "http_method": "GET", + "authenticator": { + "type": "ApiKeyAuthenticator", + "header": "apikey", + "api_token": "{{ config['api_key'] }}", + }, + }, + "record_selector": { + "type": "RecordSelector", + "extractor": {"type": "DpathExtractor", "field_path": []}, + }, + "paginator": {"type": "NoPagination"}, + }, + "dynamic_stream_name": "TestDynamicStream", + }, + { + "type": "DeclarativeStream", + "name": "parent_1_item_2", + "primary_key": [], + "schema_loader": { + "type": "InlineSchemaLoader", + "schema": { + "$schema": "http://json-schema.org/schema#", + "properties": {"ABC": {"type": "number"}, "AED": {"type": "number"}}, + "type": "object", + }, + }, + "retriever": { + "type": "SimpleRetriever", + "requester": { + "type": "HttpRequester", + "url_base": "https://api.test.com", + "path": "1/2", + "http_method": "GET", + "authenticator": { + "type": "ApiKeyAuthenticator", + "header": "apikey", + "api_token": "{{ config['api_key'] }}", + }, + }, + "record_selector": { + "type": "RecordSelector", + "extractor": {"type": "DpathExtractor", "field_path": []}, + }, + "paginator": {"type": "NoPagination"}, + }, + "dynamic_stream_name": "TestDynamicStream", + }, + { + "type": "DeclarativeStream", + "name": "parent_2_item_1", + "primary_key": [], + "schema_loader": { + "type": "InlineSchemaLoader", + "schema": { + "$schema": "http://json-schema.org/schema#", + "properties": {"ABC": {"type": "number"}, "AED": {"type": "number"}}, + "type": "object", + }, + }, + "retriever": { + "type": "SimpleRetriever", + "requester": { + "type": "HttpRequester", + "url_base": "https://api.test.com", + "path": "2/1", + "http_method": "GET", + "authenticator": { + "type": "ApiKeyAuthenticator", + "header": "apikey", + "api_token": "{{ config['api_key'] }}", + }, + }, + "record_selector": { + "type": "RecordSelector", + "extractor": {"type": "DpathExtractor", "field_path": []}, + }, + "paginator": {"type": "NoPagination"}, + }, + "dynamic_stream_name": "TestDynamicStream", + }, + { + "type": "DeclarativeStream", + "name": "parent_2_item_2", + "primary_key": [], + "schema_loader": { + "type": "InlineSchemaLoader", + "schema": { + "$schema": "http://json-schema.org/schema#", + "properties": {"ABC": {"type": "number"}, "AED": {"type": "number"}}, + "type": "object", + }, + }, + "retriever": { + "type": "SimpleRetriever", + "requester": { + "type": "HttpRequester", + "url_base": "https://api.test.com", + "path": "2/2", + "http_method": "GET", + "authenticator": { + "type": "ApiKeyAuthenticator", + "header": "apikey", + "api_token": "{{ config['api_key'] }}", + }, + }, + "record_selector": { + "type": "RecordSelector", + "extractor": {"type": "DpathExtractor", "field_path": []}, + }, + "paginator": {"type": "NoPagination"}, + }, + "dynamic_stream_name": "TestDynamicStream", + }, + ], + "check": {"type": "CheckStream", "stream_names": ["lists"]}, + "spec": { + "connection_specification": { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "required": [], + "properties": {}, + "additionalProperties": True, + }, + "type": "Spec", + }, + "dynamic_streams": [ + { + "type": "DynamicDeclarativeStream", + "name": "TestDynamicStream", + "stream_template": { + "type": "DeclarativeStream", + "name": "", + "primary_key": [], + "schema_loader": { + "type": "InlineSchemaLoader", + "schema": { + "$schema": "http://json-schema.org/schema#", + "properties": {"ABC": {"type": "number"}, "AED": {"type": "number"}}, + "type": "object", + }, + }, + "retriever": { + "type": "SimpleRetriever", + "requester": { + "type": "HttpRequester", + "url_base": "https://api.test.com", + "path": "", + "http_method": "GET", + "authenticator": { + "type": "ApiKeyAuthenticator", + "header": "apikey", + "api_token": "{{ config['api_key'] }}", + }, + }, + "record_selector": { + "type": "RecordSelector", + "extractor": {"type": "DpathExtractor", "field_path": []}, + }, + "paginator": {"type": "NoPagination"}, + }, + }, + "components_resolver": { + "type": "HttpComponentsResolver", + "retriever": { + "type": "SimpleRetriever", + "requester": { + "type": "HttpRequester", + "url_base": "https://api.test.com", + "path": "parent/{{ stream_partition.parent_id }}/items", + "http_method": "GET", + "authenticator": { + "type": "ApiKeyAuthenticator", + "header": "apikey", + "api_token": "{{ config['api_key'] }}", + }, + "use_cache": True, + }, + "record_selector": { + "type": "RecordSelector", + "extractor": {"type": "DpathExtractor", "field_path": []}, + }, + "paginator": {"type": "NoPagination"}, + "partition_router": { + "type": "SubstreamPartitionRouter", + "parent_stream_configs": [ + { + "type": "ParentStreamConfig", + "parent_key": "id", + "partition_field": "parent_id", + "stream": { + "type": "DeclarativeStream", + "name": "parent", + "retriever": { + "type": "SimpleRetriever", + "requester": { + "type": "HttpRequester", + "url_base": "https://api.test.com", + "path": "/parents", + "http_method": "GET", + "authenticator": { + "type": "ApiKeyAuthenticator", + "header": "apikey", + "api_token": "{{ config['api_key'] }}", + }, + }, + "record_selector": { + "type": "RecordSelector", + "extractor": { + "type": "DpathExtractor", + "field_path": [], + }, + }, + }, + "schema_loader": { + "type": "InlineSchemaLoader", + "schema": { + "$schema": "http://json-schema.org/schema#", + "properties": {"id": {"type": "integer"}}, + "type": "object", + }, + }, + }, + } + ], + }, + }, + "components_mapping": [ + { + "type": "ComponentMappingDefinition", + "field_path": ["name"], + "value": "parent_{{stream_slice['parent_id']}}_{{components_values['name']}}", + }, + { + "type": "ComponentMappingDefinition", + "field_path": ["retriever", "requester", "path"], + "value": "{{ stream_slice['parent_id'] }}/{{ components_values['id'] }}", + }, + ], + }, + } + ], + "type": "DeclarativeSource", + } + assert resolved_manifest.record.data["manifest"] == expected_resolved_manifest + assert resolved_manifest.record.stream == "full_resolve_manifest" diff --git a/unit_tests/sources/declarative/resolvers/test_http_components_resolver.py b/unit_tests/sources/declarative/resolvers/test_http_components_resolver.py index 09d069bff..6b2aa3fb9 100644 --- a/unit_tests/sources/declarative/resolvers/test_http_components_resolver.py +++ b/unit_tests/sources/declarative/resolvers/test_http_components_resolver.py @@ -57,6 +57,7 @@ def to_configured_catalog( "dynamic_streams": [ { "type": "DynamicDeclarativeStream", + "name": "TestDynamicStream", "stream_template": { "type": "DeclarativeStream", "name": "",