@@ -3565,86 +3565,70 @@ def create_state_delegating_stream(
35653565 f"state_delegating_stream, full_refresh_stream name and incremental_stream must have equal names. Instead has { model .name } , { model .full_refresh_stream .name } and { model .incremental_stream .name } ."
35663566 )
35673567
3568- stream_model = self ._get_state_delegating_stream_model (
3569- False if has_parent_state is None else has_parent_state , model , config
3570- )
3571-
3572- return self ._create_component_from_model (stream_model , config = config , ** kwargs ) # type: ignore[no-any-return] # DeclarativeStream will be created as stream_model is alwyas DeclarativeStreamModel
3568+ if model .api_retention_period :
3569+ for stream_model in (model .full_refresh_stream , model .incremental_stream ):
3570+ if isinstance (stream_model .incremental_sync , IncrementingCountCursorModel ):
3571+ raise ValueError (
3572+ f"Stream '{ model .name } ' uses IncrementingCountCursor which is not supported "
3573+ f"with api_retention_period. IncrementingCountCursor does not use datetime-based "
3574+ f"cursors, so cursor age validation cannot be performed."
3575+ )
35733576
3574- def _get_state_delegating_stream_model (
3575- self , has_parent_state : bool , model : StateDelegatingStreamModel , config : Config
3576- ) -> DeclarativeStreamModel :
35773577 stream_state = self ._connector_state_manager .get_stream_state (model .name , None )
3578+ has_parent = False if has_parent_state is None else has_parent_state
35783579
3579- if not stream_state and not has_parent_state :
3580- return model .full_refresh_stream
3580+ if not stream_state and not has_parent :
3581+ return self ._create_component_from_model (model .full_refresh_stream , config = config , ** kwargs ) # type: ignore[no-any-return]
3582+
3583+ incremental_stream : DefaultStream = self ._create_component_from_model (model .incremental_stream , config = config , ** kwargs ) # type: ignore[assignment]
35813584
35823585 if model .api_retention_period and stream_state :
3583- incremental_sync_sources = [
3584- model .full_refresh_stream .incremental_sync ,
3585- model .incremental_stream .incremental_sync ,
3586- ]
3587- incremental_sync_sources = [s for s in incremental_sync_sources if s is not None ]
3588- if incremental_sync_sources and self ._is_cursor_older_than_retention_period (
3589- stream_state ,
3590- incremental_sync_sources ,
3591- model .api_retention_period ,
3592- model .name ,
3593- config ,
3586+ cursor = incremental_stream .cursor
3587+ if self ._is_cursor_older_than_retention_period (
3588+ stream_state , cursor , model .api_retention_period , model .name
35943589 ):
3595- return model .full_refresh_stream
3590+ return self . _create_component_from_model ( model .full_refresh_stream , config = config , ** kwargs ) # type: ignore[no-any-return]
35963591
3597- return model . incremental_stream
3592+ return incremental_stream
35983593
3594+ @staticmethod
35993595 def _is_cursor_older_than_retention_period (
3600- self ,
36013596 stream_state : Mapping [str , Any ],
3602- incremental_sync_sources : list [ Any ] ,
3597+ cursor : Any ,
36033598 api_retention_period : str ,
36043599 stream_name : str ,
3605- config : Config ,
36063600 ) -> bool :
36073601 """Check if the cursor value in the state is older than the API's retention period.
36083602
3609- Delegates cursor datetime extraction to cursor class instances via
3610- get_cursor_datetime_from_state, which handles format-specific parsing .
3603+ Delegates cursor datetime extraction to the cursor instance via
3604+ get_cursor_datetime_from_state.
36113605
36123606 Returns True if the cursor is older than the retention period (should use full refresh).
36133607 Returns False if the cursor is within the retention period (safe to use incremental).
3614- Raises ValueError if the cursor datetime could not be parsed from state.
36153608 """
3616- # Skip retention check for concurrent state format (e.g. {"state_type": "date-range", "slices": [...]}).
3617- # The DatetimeBasedCursor used for the age check only handles sequential state format.
3618- # Today, is_sequential_state=True is hardcoded for all declarative cursors, so concurrent
3619- # format state should never appear in practice. If that changes in the future, this guard
3620- # prevents spurious full-refresh fallbacks until proper concurrent cursor delegation is added.
3621- if "state_type" in stream_state or "slices" in stream_state :
3622- return False
3623-
3624- datetime_cursor_sources = [
3625- s for s in incremental_sync_sources if isinstance (s , DatetimeBasedCursorModel )
3626- ]
3627- if not datetime_cursor_sources :
3628- return False
3609+ if not hasattr (cursor , "get_cursor_datetime_from_state" ):
3610+ raise SystemError (
3611+ f"Stream '{ stream_name } ' cursor type '{ type (cursor ).__name__ } ' does not have "
3612+ f"get_cursor_datetime_from_state method. Cursor age validation with "
3613+ f"api_retention_period is not supported for this cursor type."
3614+ )
36293615
3630- cursor_datetime : datetime .datetime | None = None
3631- for incremental_sync in datetime_cursor_sources :
3632- cursor = self ._create_cursor_for_age_check (incremental_sync , config )
3616+ try :
36333617 cursor_datetime = cursor .get_cursor_datetime_from_state (stream_state )
3634- if cursor_datetime is not None :
3635- break
3618+ except NotImplementedError :
3619+ raise SystemError (
3620+ f"Stream '{ stream_name } ' cursor type '{ type (cursor ).__name__ } ' does not implement "
3621+ f"get_cursor_datetime_from_state. Cursor age validation with "
3622+ f"api_retention_period is not supported for this cursor type."
3623+ )
3624+
3625+ if cursor_datetime is None :
36363626 global_state = stream_state .get ("state" )
36373627 if isinstance (global_state , dict ):
36383628 cursor_datetime = cursor .get_cursor_datetime_from_state (global_state )
3639- if cursor_datetime is not None :
3640- break
36413629
36423630 if cursor_datetime is None :
3643- raise ValueError (
3644- f"Stream '{ stream_name } ' has api_retention_period set to '{ api_retention_period } ' "
3645- f"but the cursor datetime could not be parsed from state. Check that cursor_field "
3646- f"and datetime_format match the state format."
3647- )
3631+ return True
36483632
36493633 retention_duration = parse_duration (api_retention_period )
36503634 retention_cutoff = datetime .datetime .now (datetime .timezone .utc ) - retention_duration
@@ -3660,24 +3644,6 @@ def _is_cursor_older_than_retention_period(
36603644
36613645 return False
36623646
3663- @staticmethod
3664- def _create_cursor_for_age_check (
3665- model : DatetimeBasedCursorModel , config : Config
3666- ) -> "DatetimeBasedCursor" :
3667- """Create a lightweight DatetimeBasedCursor for cursor age validation."""
3668- from airbyte_cdk .legacy .sources .declarative .incremental .datetime_based_cursor import (
3669- DatetimeBasedCursor as _DatetimeBasedCursor ,
3670- )
3671-
3672- return _DatetimeBasedCursor (
3673- start_datetime = "2000-01-01T00:00:00Z" ,
3674- cursor_field = model .cursor_field ,
3675- datetime_format = model .datetime_format ,
3676- config = config ,
3677- parameters = model .parameters or {},
3678- cursor_datetime_formats = model .cursor_datetime_formats or [],
3679- )
3680-
36813647 def _create_async_job_status_mapping (
36823648 self , model : AsyncJobStatusMapModel , config : Config , ** kwargs : Any
36833649 ) -> Mapping [str , AsyncJobStatus ]:
0 commit comments