|
245 | 245 | from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
|
246 | 246 | HttpResponseFilter as HttpResponseFilterModel,
|
247 | 247 | )
|
| 248 | +from airbyte_cdk.sources.declarative.models.declarative_component_schema import ( |
| 249 | + IncrementingCountCursor as IncrementingCountCursorModel, |
| 250 | +) |
248 | 251 | from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
|
249 | 252 | InlineSchemaLoader as InlineSchemaLoaderModel,
|
250 | 253 | )
|
|
496 | 499 | CustomFormatConcurrentStreamStateConverter,
|
497 | 500 | DateTimeStreamStateConverter,
|
498 | 501 | )
|
| 502 | +from airbyte_cdk.sources.streams.concurrent.state_converters.incrementing_count_stream_state_converter import ( |
| 503 | + IncrementingCountStreamStateConverter, |
| 504 | +) |
499 | 505 | from airbyte_cdk.sources.streams.http.error_handlers.response_models import ResponseAction
|
500 | 506 | from airbyte_cdk.sources.types import Config
|
501 | 507 | from airbyte_cdk.sources.utils.transform import TransformConfig, TypeTransformer
|
@@ -584,6 +590,7 @@ def _init_mappings(self) -> None:
|
584 | 590 | FlattenFieldsModel: self.create_flatten_fields,
|
585 | 591 | DpathFlattenFieldsModel: self.create_dpath_flatten_fields,
|
586 | 592 | IterableDecoderModel: self.create_iterable_decoder,
|
| 593 | + IncrementingCountCursorModel: self.create_incrementing_count_cursor, |
587 | 594 | XmlDecoderModel: self.create_xml_decoder,
|
588 | 595 | JsonFileSchemaLoaderModel: self.create_json_file_schema_loader,
|
589 | 596 | DynamicSchemaLoaderModel: self.create_dynamic_schema_loader,
|
@@ -1189,6 +1196,70 @@ def create_concurrent_cursor_from_datetime_based_cursor(
|
1189 | 1196 | clamping_strategy=clamping_strategy,
|
1190 | 1197 | )
|
1191 | 1198 |
|
| 1199 | + def create_concurrent_cursor_from_incrementing_count_cursor( |
| 1200 | + self, |
| 1201 | + model_type: Type[BaseModel], |
| 1202 | + component_definition: ComponentDefinition, |
| 1203 | + stream_name: str, |
| 1204 | + stream_namespace: Optional[str], |
| 1205 | + config: Config, |
| 1206 | + message_repository: Optional[MessageRepository] = None, |
| 1207 | + **kwargs: Any, |
| 1208 | + ) -> ConcurrentCursor: |
| 1209 | + # Per-partition incremental streams can dynamically create child cursors which will pass their current |
| 1210 | + # state via the stream_state keyword argument. Incremental syncs without parent streams use the |
| 1211 | + # incoming state and connector_state_manager that is initialized when the component factory is created |
| 1212 | + stream_state = ( |
| 1213 | + self._connector_state_manager.get_stream_state(stream_name, stream_namespace) |
| 1214 | + if "stream_state" not in kwargs |
| 1215 | + else kwargs["stream_state"] |
| 1216 | + ) |
| 1217 | + |
| 1218 | + component_type = component_definition.get("type") |
| 1219 | + if component_definition.get("type") != model_type.__name__: |
| 1220 | + raise ValueError( |
| 1221 | + f"Expected manifest component of type {model_type.__name__}, but received {component_type} instead" |
| 1222 | + ) |
| 1223 | + |
| 1224 | + incrementing_count_cursor_model = model_type.parse_obj(component_definition) |
| 1225 | + |
| 1226 | + if not isinstance(incrementing_count_cursor_model, IncrementingCountCursorModel): |
| 1227 | + raise ValueError( |
| 1228 | + f"Expected {model_type.__name__} component, but received {incrementing_count_cursor_model.__class__.__name__}" |
| 1229 | + ) |
| 1230 | + |
| 1231 | + interpolated_start_value = ( |
| 1232 | + InterpolatedString.create( |
| 1233 | + incrementing_count_cursor_model.start_value, # type: ignore |
| 1234 | + parameters=incrementing_count_cursor_model.parameters or {}, |
| 1235 | + ) |
| 1236 | + if incrementing_count_cursor_model.start_value |
| 1237 | + else 0 |
| 1238 | + ) |
| 1239 | + |
| 1240 | + interpolated_cursor_field = InterpolatedString.create( |
| 1241 | + incrementing_count_cursor_model.cursor_field, |
| 1242 | + parameters=incrementing_count_cursor_model.parameters or {}, |
| 1243 | + ) |
| 1244 | + cursor_field = CursorField(interpolated_cursor_field.eval(config=config)) |
| 1245 | + |
| 1246 | + connector_state_converter = IncrementingCountStreamStateConverter( |
| 1247 | + is_sequential_state=True, # ConcurrentPerPartitionCursor only works with sequential state |
| 1248 | + ) |
| 1249 | + |
| 1250 | + return ConcurrentCursor( |
| 1251 | + stream_name=stream_name, |
| 1252 | + stream_namespace=stream_namespace, |
| 1253 | + stream_state=stream_state, |
| 1254 | + message_repository=message_repository or self._message_repository, |
| 1255 | + connector_state_manager=self._connector_state_manager, |
| 1256 | + connector_state_converter=connector_state_converter, |
| 1257 | + cursor_field=cursor_field, |
| 1258 | + slice_boundary_fields=None, |
| 1259 | + start=interpolated_start_value, # type: ignore # Having issues w/ inspection for GapType and CursorValueType as shown in existing tests. Confirmed functionality is working in practice |
| 1260 | + end_provider=connector_state_converter.get_end_provider(), # type: ignore # Having issues w/ inspection for GapType and CursorValueType as shown in existing tests. Confirmed functionality is working in practice |
| 1261 | + ) |
| 1262 | + |
1192 | 1263 | def _assemble_weekday(self, weekday: str) -> Weekday:
|
1193 | 1264 | match weekday:
|
1194 | 1265 | case "MONDAY":
|
@@ -1622,6 +1693,31 @@ def create_declarative_stream(
|
1622 | 1693 | config=config,
|
1623 | 1694 | parameters=model.parameters or {},
|
1624 | 1695 | )
|
| 1696 | + elif model.incremental_sync and isinstance( |
| 1697 | + model.incremental_sync, IncrementingCountCursorModel |
| 1698 | + ): |
| 1699 | + cursor_model: IncrementingCountCursorModel = model.incremental_sync # type: ignore |
| 1700 | + |
| 1701 | + start_time_option = ( |
| 1702 | + self._create_component_from_model( |
| 1703 | + cursor_model.start_value_option, # type: ignore # mypy still thinks cursor_model of type DatetimeBasedCursor |
| 1704 | + config, |
| 1705 | + parameters=cursor_model.parameters or {}, |
| 1706 | + ) |
| 1707 | + if cursor_model.start_value_option # type: ignore # mypy still thinks cursor_model of type DatetimeBasedCursor |
| 1708 | + else None |
| 1709 | + ) |
| 1710 | + |
| 1711 | + # The concurrent engine defaults the start/end fields on the slice to "start" and "end", but |
| 1712 | + # the default DatetimeBasedRequestOptionsProvider() sets them to start_time/end_time |
| 1713 | + partition_field_start = "start" |
| 1714 | + |
| 1715 | + request_options_provider = DatetimeBasedRequestOptionsProvider( |
| 1716 | + start_time_option=start_time_option, |
| 1717 | + partition_field_start=partition_field_start, |
| 1718 | + config=config, |
| 1719 | + parameters=model.parameters or {}, |
| 1720 | + ) |
1625 | 1721 | else:
|
1626 | 1722 | request_options_provider = None
|
1627 | 1723 |
|
@@ -2111,6 +2207,22 @@ def create_gzip_decoder(
|
2111 | 2207 | stream_response=False if self._emit_connector_builder_messages else True,
|
2112 | 2208 | )
|
2113 | 2209 |
|
| 2210 | + @staticmethod |
| 2211 | + def create_incrementing_count_cursor( |
| 2212 | + model: IncrementingCountCursorModel, config: Config, **kwargs: Any |
| 2213 | + ) -> DatetimeBasedCursor: |
| 2214 | + # This should not actually get used anywhere at runtime, but needed to add this to pass checks since |
| 2215 | + # we still parse models into components. The issue is that there's no runtime implementation of a |
| 2216 | + # IncrementingCountCursor. |
| 2217 | + # A known and expected issue with this stub is running a check with the declared IncrementingCountCursor because it is run without ConcurrentCursor. |
| 2218 | + return DatetimeBasedCursor( |
| 2219 | + cursor_field=model.cursor_field, |
| 2220 | + datetime_format="%Y-%m-%d", |
| 2221 | + start_datetime="2024-12-12", |
| 2222 | + config=config, |
| 2223 | + parameters={}, |
| 2224 | + ) |
| 2225 | + |
2114 | 2226 | @staticmethod
|
2115 | 2227 | def create_iterable_decoder(
|
2116 | 2228 | model: IterableDecoderModel, config: Config, **kwargs: Any
|
|
0 commit comments