airbyte_cdk.sources.declarative.migrations.legacy_to_per_partition_state_migration
1# Copyright (c) 2024 Airbyte, Inc., all rights reserved. 2 3from typing import Any, Mapping, Union 4 5from airbyte_cdk.sources.declarative.interpolation.interpolated_string import InterpolatedString 6from airbyte_cdk.sources.declarative.migrations.state_migration import StateMigration 7from airbyte_cdk.sources.declarative.models import ( 8 CustomPartitionRouter, 9 DatetimeBasedCursor, 10 SubstreamPartitionRouter, 11 UnionPartitionRouter, 12) 13from airbyte_cdk.sources.declarative.models.declarative_component_schema import ParentStreamConfig 14 15 16def _is_already_migrated(stream_state: Mapping[str, Any]) -> bool: 17 return "states" in stream_state 18 19 20class LegacyToPerPartitionStateMigration(StateMigration): 21 """ 22 Transforms the input state for per-partitioned streams from the legacy format to the low-code format. 23 The cursor field and partition ID fields are automatically extracted from the stream's DatetimebasedCursor and SubstreamPartitionRouter. 24 25 Example input state: 26 { 27 "13506132": { 28 "last_changed": "2022-12-27T08:34:39+00:00" 29 } 30 Example output state: 31 { 32 "partition": {"id": "13506132"}, 33 "cursor": {"last_changed": "2022-12-27T08:34:39+00:00"} 34 } 35 """ 36 37 def __init__( 38 self, 39 partition_router: Union[ 40 SubstreamPartitionRouter, UnionPartitionRouter, CustomPartitionRouter 41 ], 42 cursor: DatetimeBasedCursor, 43 config: Mapping[str, Any], 44 parameters: Mapping[str, Any], 45 ): 46 self._partition_router = partition_router 47 self._cursor = cursor 48 self._config = config 49 self._parameters = parameters 50 self._partition_key_field = InterpolatedString.create( 51 self._get_partition_field(partition_router), parameters=self._parameters 52 ).eval(self._config) 53 self._cursor_field = InterpolatedString.create( 54 self._cursor.cursor_field, parameters=self._parameters 55 ).eval(self._config) 56 57 def _get_partition_field( 58 self, 59 partition_router: Union[ 60 SubstreamPartitionRouter, UnionPartitionRouter, CustomPartitionRouter 61 ], 62 ) -> str: 63 if isinstance(partition_router, UnionPartitionRouter): 64 return partition_router.partition_field 65 66 parent_stream_config = partition_router.parent_stream_configs[0] # type: ignore # custom partition routers are expected to expose parent_stream_configs 67 68 # Retrieve the partition field with a condition, as properties are returned as a dictionary for custom components. 69 partition_field = ( 70 parent_stream_config.partition_field 71 if isinstance(parent_stream_config, ParentStreamConfig) 72 else parent_stream_config.get("partition_field") # type: ignore # See above comment on why parent_stream_config might be a dict 73 ) 74 75 return partition_field 76 77 def should_migrate(self, stream_state: Mapping[str, Any]) -> bool: 78 if _is_already_migrated(stream_state): 79 return False 80 81 # UnionPartitionRouter has no parent_stream_configs; its partitions are already 82 # normalized to a single partition field so the parent stream check does not apply. 83 if not isinstance(self._partition_router, UnionPartitionRouter): 84 # There is exactly one parent stream 85 number_of_parent_streams = len(self._partition_router.parent_stream_configs) # type: ignore # custom partition will introduce this attribute if needed 86 if number_of_parent_streams != 1: 87 # There should be exactly one parent stream 88 return False 89 """ 90 The expected state format is 91 "<parent_key_id>" : { 92 "<cursor_field>" : "<cursor_value>" 93 } 94 """ 95 if not stream_state: 96 return False 97 for key, value in stream_state.items(): 98 # it is expected the internal value to be a dictionary according to docstring 99 if not isinstance(value, dict): 100 return False 101 keys = list(value.keys()) 102 if len(keys) != 1: 103 # The input partitioned state should only have one key 104 return False 105 if keys[0] != self._cursor_field: 106 # Unexpected key. Found {keys[0]}. Expected {self._cursor.cursor_field} 107 return False 108 109 return True 110 111 def migrate(self, stream_state: Mapping[str, Any]) -> Mapping[str, Any]: 112 states = [ 113 {"partition": {self._partition_key_field: key}, "cursor": value} 114 for key, value in stream_state.items() 115 ] 116 return {"states": states}
class
LegacyToPerPartitionStateMigration(airbyte_cdk.sources.declarative.migrations.state_migration.StateMigration):
21class LegacyToPerPartitionStateMigration(StateMigration): 22 """ 23 Transforms the input state for per-partitioned streams from the legacy format to the low-code format. 24 The cursor field and partition ID fields are automatically extracted from the stream's DatetimebasedCursor and SubstreamPartitionRouter. 25 26 Example input state: 27 { 28 "13506132": { 29 "last_changed": "2022-12-27T08:34:39+00:00" 30 } 31 Example output state: 32 { 33 "partition": {"id": "13506132"}, 34 "cursor": {"last_changed": "2022-12-27T08:34:39+00:00"} 35 } 36 """ 37 38 def __init__( 39 self, 40 partition_router: Union[ 41 SubstreamPartitionRouter, UnionPartitionRouter, CustomPartitionRouter 42 ], 43 cursor: DatetimeBasedCursor, 44 config: Mapping[str, Any], 45 parameters: Mapping[str, Any], 46 ): 47 self._partition_router = partition_router 48 self._cursor = cursor 49 self._config = config 50 self._parameters = parameters 51 self._partition_key_field = InterpolatedString.create( 52 self._get_partition_field(partition_router), parameters=self._parameters 53 ).eval(self._config) 54 self._cursor_field = InterpolatedString.create( 55 self._cursor.cursor_field, parameters=self._parameters 56 ).eval(self._config) 57 58 def _get_partition_field( 59 self, 60 partition_router: Union[ 61 SubstreamPartitionRouter, UnionPartitionRouter, CustomPartitionRouter 62 ], 63 ) -> str: 64 if isinstance(partition_router, UnionPartitionRouter): 65 return partition_router.partition_field 66 67 parent_stream_config = partition_router.parent_stream_configs[0] # type: ignore # custom partition routers are expected to expose parent_stream_configs 68 69 # Retrieve the partition field with a condition, as properties are returned as a dictionary for custom components. 70 partition_field = ( 71 parent_stream_config.partition_field 72 if isinstance(parent_stream_config, ParentStreamConfig) 73 else parent_stream_config.get("partition_field") # type: ignore # See above comment on why parent_stream_config might be a dict 74 ) 75 76 return partition_field 77 78 def should_migrate(self, stream_state: Mapping[str, Any]) -> bool: 79 if _is_already_migrated(stream_state): 80 return False 81 82 # UnionPartitionRouter has no parent_stream_configs; its partitions are already 83 # normalized to a single partition field so the parent stream check does not apply. 84 if not isinstance(self._partition_router, UnionPartitionRouter): 85 # There is exactly one parent stream 86 number_of_parent_streams = len(self._partition_router.parent_stream_configs) # type: ignore # custom partition will introduce this attribute if needed 87 if number_of_parent_streams != 1: 88 # There should be exactly one parent stream 89 return False 90 """ 91 The expected state format is 92 "<parent_key_id>" : { 93 "<cursor_field>" : "<cursor_value>" 94 } 95 """ 96 if not stream_state: 97 return False 98 for key, value in stream_state.items(): 99 # it is expected the internal value to be a dictionary according to docstring 100 if not isinstance(value, dict): 101 return False 102 keys = list(value.keys()) 103 if len(keys) != 1: 104 # The input partitioned state should only have one key 105 return False 106 if keys[0] != self._cursor_field: 107 # Unexpected key. Found {keys[0]}. Expected {self._cursor.cursor_field} 108 return False 109 110 return True 111 112 def migrate(self, stream_state: Mapping[str, Any]) -> Mapping[str, Any]: 113 states = [ 114 {"partition": {self._partition_key_field: key}, "cursor": value} 115 for key, value in stream_state.items() 116 ] 117 return {"states": states}
Transforms the input state for per-partitioned streams from the legacy format to the low-code format. The cursor field and partition ID fields are automatically extracted from the stream's DatetimebasedCursor and SubstreamPartitionRouter.
Example input state: { "13506132": { "last_changed": "2022-12-27T08:34:39+00:00" } Example output state: { "partition": {"id": "13506132"}, "cursor": {"last_changed": "2022-12-27T08:34:39+00:00"} }
LegacyToPerPartitionStateMigration( partition_router: Union[airbyte_cdk.sources.declarative.models.declarative_component_schema.SubstreamPartitionRouter, airbyte_cdk.sources.declarative.models.declarative_component_schema.UnionPartitionRouter, airbyte_cdk.sources.declarative.models.declarative_component_schema.CustomPartitionRouter], cursor: airbyte_cdk.sources.declarative.models.declarative_component_schema.DatetimeBasedCursor, config: Mapping[str, Any], parameters: Mapping[str, Any])
38 def __init__( 39 self, 40 partition_router: Union[ 41 SubstreamPartitionRouter, UnionPartitionRouter, CustomPartitionRouter 42 ], 43 cursor: DatetimeBasedCursor, 44 config: Mapping[str, Any], 45 parameters: Mapping[str, Any], 46 ): 47 self._partition_router = partition_router 48 self._cursor = cursor 49 self._config = config 50 self._parameters = parameters 51 self._partition_key_field = InterpolatedString.create( 52 self._get_partition_field(partition_router), parameters=self._parameters 53 ).eval(self._config) 54 self._cursor_field = InterpolatedString.create( 55 self._cursor.cursor_field, parameters=self._parameters 56 ).eval(self._config)
def
should_migrate(self, stream_state: Mapping[str, Any]) -> bool:
78 def should_migrate(self, stream_state: Mapping[str, Any]) -> bool: 79 if _is_already_migrated(stream_state): 80 return False 81 82 # UnionPartitionRouter has no parent_stream_configs; its partitions are already 83 # normalized to a single partition field so the parent stream check does not apply. 84 if not isinstance(self._partition_router, UnionPartitionRouter): 85 # There is exactly one parent stream 86 number_of_parent_streams = len(self._partition_router.parent_stream_configs) # type: ignore # custom partition will introduce this attribute if needed 87 if number_of_parent_streams != 1: 88 # There should be exactly one parent stream 89 return False 90 """ 91 The expected state format is 92 "<parent_key_id>" : { 93 "<cursor_field>" : "<cursor_value>" 94 } 95 """ 96 if not stream_state: 97 return False 98 for key, value in stream_state.items(): 99 # it is expected the internal value to be a dictionary according to docstring 100 if not isinstance(value, dict): 101 return False 102 keys = list(value.keys()) 103 if len(keys) != 1: 104 # The input partitioned state should only have one key 105 return False 106 if keys[0] != self._cursor_field: 107 # Unexpected key. Found {keys[0]}. Expected {self._cursor.cursor_field} 108 return False 109 110 return True
Check if the stream_state should be migrated
Parameters
- stream_state: The stream_state to potentially migrate
Returns
true if the state is of the expected format and should be migrated. False otherwise.
def
migrate(self, stream_state: Mapping[str, Any]) -> Mapping[str, Any]:
112 def migrate(self, stream_state: Mapping[str, Any]) -> Mapping[str, Any]: 113 states = [ 114 {"partition": {self._partition_key_field: key}, "cursor": value} 115 for key, value in stream_state.items() 116 ] 117 return {"states": states}
Migrate the stream_state. Assumes should_migrate(stream_state) returned True.
Parameters
- stream_state: The stream_state to migrate
Returns
The migrated stream_state