airbyte_cdk.connector_builder.connector_builder_handler
1# 2# Copyright (c) 2023 Airbyte, Inc., all rights reserved. 3# 4 5 6from dataclasses import asdict 7from typing import Any, Dict, List, Mapping, Optional 8 9from airbyte_cdk.connector_builder.test_reader import TestReader 10from airbyte_cdk.models import ( 11 AirbyteMessage, 12 AirbyteRecordMessage, 13 AirbyteStateMessage, 14 ConfiguredAirbyteCatalog, 15 Type, 16) 17from airbyte_cdk.models import Type as MessageType 18from airbyte_cdk.sources.declarative.concurrent_declarative_source import ( 19 ConcurrentDeclarativeSource, 20 TestLimits, 21) 22from airbyte_cdk.utils.airbyte_secrets_utils import filter_secrets 23from airbyte_cdk.utils.datetime_helpers import ab_datetime_now 24from airbyte_cdk.utils.traced_exception import AirbyteTracedException 25 26MAX_PAGES_PER_SLICE_KEY = "max_pages_per_slice" 27MAX_SLICES_KEY = "max_slices" 28MAX_RECORDS_KEY = "max_records" 29MAX_STREAMS_KEY = "max_streams" 30 31 32def get_limits(config: Mapping[str, Any]) -> TestLimits: 33 command_config = config.get("__test_read_config", {}) 34 return TestLimits( 35 max_records=command_config.get(MAX_RECORDS_KEY, TestLimits.DEFAULT_MAX_RECORDS), 36 max_pages_per_slice=command_config.get( 37 MAX_PAGES_PER_SLICE_KEY, TestLimits.DEFAULT_MAX_PAGES_PER_SLICE 38 ), 39 max_slices=command_config.get(MAX_SLICES_KEY, TestLimits.DEFAULT_MAX_SLICES), 40 max_streams=command_config.get(MAX_STREAMS_KEY, TestLimits.DEFAULT_MAX_STREAMS), 41 ) 42 43 44def should_migrate_manifest(config: Mapping[str, Any]) -> bool: 45 """ 46 Determines whether the manifest should be migrated, 47 based on the presence of the "__should_migrate" key in the config. 48 49 This flag is set by the UI. 50 """ 51 return config.get("__should_migrate", False) 52 53 54def should_normalize_manifest(config: Mapping[str, Any]) -> bool: 55 """ 56 Check if the manifest should be normalized. 57 :param config: The configuration to check 58 :return: True if the manifest should be normalized, False otherwise. 59 """ 60 return config.get("__should_normalize", False) 61 62 63def create_source( 64 config: Mapping[str, Any], 65 limits: TestLimits | None = None, 66 catalog: ConfiguredAirbyteCatalog | None = None, 67 state: List[AirbyteStateMessage] | None = None, 68) -> ConcurrentDeclarativeSource: 69 manifest = config["__injected_declarative_manifest"] 70 71 # We enforce a concurrency level of 1 so that the stream is processed on a single thread 72 # to retain ordering for the grouping of the builder message responses. 73 if "concurrency_level" in manifest: 74 manifest["concurrency_level"]["default_concurrency"] = 1 75 else: 76 manifest["concurrency_level"] = {"type": "ConcurrencyLevel", "default_concurrency": 1} 77 78 return ConcurrentDeclarativeSource( 79 catalog=catalog, 80 config=config, 81 state=state, 82 source_config=manifest, 83 emit_connector_builder_messages=True, 84 migrate_manifest=should_migrate_manifest(config), 85 normalize_manifest=should_normalize_manifest(config), 86 limits=limits, 87 # The manifest is supplied by the Connector Builder caller rather than bundled in a 88 # connector image, so any custom components it references are untrusted code. 89 custom_components_trusted=False, 90 ) 91 92 93def read_stream( 94 source: ConcurrentDeclarativeSource, 95 config: Mapping[str, Any], 96 configured_catalog: ConfiguredAirbyteCatalog, 97 state: List[AirbyteStateMessage], 98 limits: TestLimits, 99) -> AirbyteMessage: 100 try: 101 test_read_handler = TestReader( 102 limits.max_pages_per_slice, limits.max_slices, limits.max_records 103 ) 104 # The connector builder only supports a single stream 105 stream_name = configured_catalog.streams[0].stream.name 106 107 stream_read = test_read_handler.run_test_read( 108 source, 109 config, 110 configured_catalog, 111 stream_name, 112 state, 113 limits.max_records, 114 ) 115 116 return AirbyteMessage( 117 type=MessageType.RECORD, 118 record=AirbyteRecordMessage( 119 data=asdict(stream_read), stream=stream_name, emitted_at=_emitted_at() 120 ), 121 ) 122 except Exception as exc: 123 error = AirbyteTracedException.from_exception( 124 exc, 125 message=filter_secrets( 126 f"Error reading stream with config={config} and catalog={configured_catalog}: {str(exc)}" 127 ), 128 ) 129 return error.as_airbyte_message() 130 131 132def resolve_manifest( 133 source: ConcurrentDeclarativeSource, 134) -> AirbyteMessage: 135 try: 136 return AirbyteMessage( 137 type=Type.RECORD, 138 record=AirbyteRecordMessage( 139 data={"manifest": source.resolved_manifest}, 140 emitted_at=_emitted_at(), 141 stream="resolve_manifest", 142 ), 143 ) 144 except Exception as exc: 145 error = AirbyteTracedException.from_exception( 146 exc, message=f"Error resolving manifest: {str(exc)}" 147 ) 148 return error.as_airbyte_message() 149 150 151def full_resolve_manifest( 152 source: ConcurrentDeclarativeSource, limits: TestLimits 153) -> AirbyteMessage: 154 try: 155 manifest = {**source.resolved_manifest} 156 streams = manifest.get("streams", []) 157 for stream in streams: 158 stream["dynamic_stream_name"] = None 159 160 mapped_streams: Dict[str, List[Dict[str, Any]]] = {} 161 for stream in source.dynamic_streams: 162 generated_streams = mapped_streams.setdefault(stream["dynamic_stream_name"], []) 163 164 if len(generated_streams) < limits.max_streams: 165 generated_streams += [stream] 166 167 for generated_streams_list in mapped_streams.values(): 168 streams.extend(generated_streams_list) 169 170 manifest["streams"] = streams 171 return AirbyteMessage( 172 type=Type.RECORD, 173 record=AirbyteRecordMessage( 174 data={"manifest": manifest}, 175 emitted_at=_emitted_at(), 176 stream="full_resolve_manifest", 177 ), 178 ) 179 except AirbyteTracedException as exc: 180 return exc.as_airbyte_message() 181 except Exception as exc: 182 error = AirbyteTracedException.from_exception( 183 exc, message=f"Error full resolving manifest: {str(exc)}" 184 ) 185 return error.as_airbyte_message() 186 187 188def _emitted_at() -> int: 189 return ab_datetime_now().to_epoch_millis()
MAX_PAGES_PER_SLICE_KEY =
'max_pages_per_slice'
MAX_SLICES_KEY =
'max_slices'
MAX_RECORDS_KEY =
'max_records'
MAX_STREAMS_KEY =
'max_streams'
def
get_limits( config: Mapping[str, Any]) -> airbyte_cdk.sources.declarative.concurrent_declarative_source.TestLimits:
33def get_limits(config: Mapping[str, Any]) -> TestLimits: 34 command_config = config.get("__test_read_config", {}) 35 return TestLimits( 36 max_records=command_config.get(MAX_RECORDS_KEY, TestLimits.DEFAULT_MAX_RECORDS), 37 max_pages_per_slice=command_config.get( 38 MAX_PAGES_PER_SLICE_KEY, TestLimits.DEFAULT_MAX_PAGES_PER_SLICE 39 ), 40 max_slices=command_config.get(MAX_SLICES_KEY, TestLimits.DEFAULT_MAX_SLICES), 41 max_streams=command_config.get(MAX_STREAMS_KEY, TestLimits.DEFAULT_MAX_STREAMS), 42 )
def
should_migrate_manifest(config: Mapping[str, Any]) -> bool:
45def should_migrate_manifest(config: Mapping[str, Any]) -> bool: 46 """ 47 Determines whether the manifest should be migrated, 48 based on the presence of the "__should_migrate" key in the config. 49 50 This flag is set by the UI. 51 """ 52 return config.get("__should_migrate", False)
Determines whether the manifest should be migrated, based on the presence of the "__should_migrate" key in the config.
This flag is set by the UI.
def
should_normalize_manifest(config: Mapping[str, Any]) -> bool:
55def should_normalize_manifest(config: Mapping[str, Any]) -> bool: 56 """ 57 Check if the manifest should be normalized. 58 :param config: The configuration to check 59 :return: True if the manifest should be normalized, False otherwise. 60 """ 61 return config.get("__should_normalize", False)
Check if the manifest should be normalized.
Parameters
- config: The configuration to check
Returns
True if the manifest should be normalized, False otherwise.
def
create_source( config: Mapping[str, Any], limits: airbyte_cdk.sources.declarative.concurrent_declarative_source.TestLimits | None = None, catalog: airbyte_protocol_dataclasses.models.airbyte_protocol.ConfiguredAirbyteCatalog | None = None, state: Optional[List[airbyte_cdk.models.airbyte_protocol.AirbyteStateMessage]] = None) -> airbyte_cdk.sources.declarative.concurrent_declarative_source.ConcurrentDeclarativeSource:
64def create_source( 65 config: Mapping[str, Any], 66 limits: TestLimits | None = None, 67 catalog: ConfiguredAirbyteCatalog | None = None, 68 state: List[AirbyteStateMessage] | None = None, 69) -> ConcurrentDeclarativeSource: 70 manifest = config["__injected_declarative_manifest"] 71 72 # We enforce a concurrency level of 1 so that the stream is processed on a single thread 73 # to retain ordering for the grouping of the builder message responses. 74 if "concurrency_level" in manifest: 75 manifest["concurrency_level"]["default_concurrency"] = 1 76 else: 77 manifest["concurrency_level"] = {"type": "ConcurrencyLevel", "default_concurrency": 1} 78 79 return ConcurrentDeclarativeSource( 80 catalog=catalog, 81 config=config, 82 state=state, 83 source_config=manifest, 84 emit_connector_builder_messages=True, 85 migrate_manifest=should_migrate_manifest(config), 86 normalize_manifest=should_normalize_manifest(config), 87 limits=limits, 88 # The manifest is supplied by the Connector Builder caller rather than bundled in a 89 # connector image, so any custom components it references are untrusted code. 90 custom_components_trusted=False, 91 )
def
read_stream( source: airbyte_cdk.sources.declarative.concurrent_declarative_source.ConcurrentDeclarativeSource, config: Mapping[str, Any], configured_catalog: airbyte_protocol_dataclasses.models.airbyte_protocol.ConfiguredAirbyteCatalog, state: List[airbyte_cdk.models.airbyte_protocol.AirbyteStateMessage], limits: airbyte_cdk.sources.declarative.concurrent_declarative_source.TestLimits) -> airbyte_cdk.AirbyteMessage:
94def read_stream( 95 source: ConcurrentDeclarativeSource, 96 config: Mapping[str, Any], 97 configured_catalog: ConfiguredAirbyteCatalog, 98 state: List[AirbyteStateMessage], 99 limits: TestLimits, 100) -> AirbyteMessage: 101 try: 102 test_read_handler = TestReader( 103 limits.max_pages_per_slice, limits.max_slices, limits.max_records 104 ) 105 # The connector builder only supports a single stream 106 stream_name = configured_catalog.streams[0].stream.name 107 108 stream_read = test_read_handler.run_test_read( 109 source, 110 config, 111 configured_catalog, 112 stream_name, 113 state, 114 limits.max_records, 115 ) 116 117 return AirbyteMessage( 118 type=MessageType.RECORD, 119 record=AirbyteRecordMessage( 120 data=asdict(stream_read), stream=stream_name, emitted_at=_emitted_at() 121 ), 122 ) 123 except Exception as exc: 124 error = AirbyteTracedException.from_exception( 125 exc, 126 message=filter_secrets( 127 f"Error reading stream with config={config} and catalog={configured_catalog}: {str(exc)}" 128 ), 129 ) 130 return error.as_airbyte_message()
def
resolve_manifest( source: airbyte_cdk.sources.declarative.concurrent_declarative_source.ConcurrentDeclarativeSource) -> airbyte_cdk.AirbyteMessage:
133def resolve_manifest( 134 source: ConcurrentDeclarativeSource, 135) -> AirbyteMessage: 136 try: 137 return AirbyteMessage( 138 type=Type.RECORD, 139 record=AirbyteRecordMessage( 140 data={"manifest": source.resolved_manifest}, 141 emitted_at=_emitted_at(), 142 stream="resolve_manifest", 143 ), 144 ) 145 except Exception as exc: 146 error = AirbyteTracedException.from_exception( 147 exc, message=f"Error resolving manifest: {str(exc)}" 148 ) 149 return error.as_airbyte_message()
def
full_resolve_manifest( source: airbyte_cdk.sources.declarative.concurrent_declarative_source.ConcurrentDeclarativeSource, limits: airbyte_cdk.sources.declarative.concurrent_declarative_source.TestLimits) -> airbyte_cdk.AirbyteMessage:
152def full_resolve_manifest( 153 source: ConcurrentDeclarativeSource, limits: TestLimits 154) -> AirbyteMessage: 155 try: 156 manifest = {**source.resolved_manifest} 157 streams = manifest.get("streams", []) 158 for stream in streams: 159 stream["dynamic_stream_name"] = None 160 161 mapped_streams: Dict[str, List[Dict[str, Any]]] = {} 162 for stream in source.dynamic_streams: 163 generated_streams = mapped_streams.setdefault(stream["dynamic_stream_name"], []) 164 165 if len(generated_streams) < limits.max_streams: 166 generated_streams += [stream] 167 168 for generated_streams_list in mapped_streams.values(): 169 streams.extend(generated_streams_list) 170 171 manifest["streams"] = streams 172 return AirbyteMessage( 173 type=Type.RECORD, 174 record=AirbyteRecordMessage( 175 data={"manifest": manifest}, 176 emitted_at=_emitted_at(), 177 stream="full_resolve_manifest", 178 ), 179 ) 180 except AirbyteTracedException as exc: 181 return exc.as_airbyte_message() 182 except Exception as exc: 183 error = AirbyteTracedException.from_exception( 184 exc, message=f"Error full resolving manifest: {str(exc)}" 185 ) 186 return error.as_airbyte_message()