66from dataclasses import InitVar , dataclass , field
77from functools import partial
88from itertools import islice
9- from typing import Any , Callable , Iterable , List , Mapping , Optional , Set , Tuple , Union
9+ from typing import Any , Callable , Iterable , List , Mapping , Optional , Set , Tuple , Union , MutableMapping
1010
1111import dpath
1212import requests
13+ from typing_extensions import deprecated
1314
1415from airbyte_cdk .models import AirbyteMessage
1516from airbyte_cdk .sources .declarative .extractors .http_selector import HttpSelector
3536from airbyte_cdk .sources .streams .core import StreamData
3637from airbyte_cdk .sources .types import Config , Record , StreamSlice , StreamState
3738from airbyte_cdk .utils .mapping_helpers import combine_mappings
39+ from airbyte_cdk .sources .source import ExperimentalClassWarning
3840
3941FULL_REFRESH_SYNC_COMPLETE_KEY = "__ab_full_refresh_sync_complete"
4042
@@ -625,25 +627,29 @@ def _fetch_next_page(
625627
626628
627629class SafeResponse (requests .Response ):
628- def __getattr__ (self , name ) :
630+ def __getattr__ (self , name : str ) -> Any :
629631 return getattr (requests .Response , name , None )
630632
631633 @property
632- def content (self ):
634+ def content (self ) -> Optional [ bytes ] :
633635 return super ().content
634636
635637 @content .setter
636- def content (self , value ) :
638+ def content (self , value : Union [ str , bytes ]) -> None :
637639 self ._content = value .encode () if isinstance (value , str ) else value
638640
639641
642+ @deprecated (
643+ "This class is experimental. Use at your own risk." ,
644+ category = ExperimentalClassWarning ,
645+ )
640646@dataclass
641647class LazySimpleRetriever (SimpleRetriever ):
642648 """
643649 A retriever that supports lazy loading from parent streams.
644650 """
645651
646- partition_router : SubstreamPartitionRouter = field (init = True , repr = False , default = None )
652+ partition_router : SubstreamPartitionRouter = field (init = True , repr = False , default = None ) # type: ignore[assignment] # 'partition_router' is required for LazySimpleRetriever and is validated in the constructor
647653 lazy_read_pointer : Optional [List [InterpolatedString ]] = None
648654
649655 def _read_pages (
@@ -655,9 +661,9 @@ def _read_pages(
655661 parent_stream_config = self .partition_router .parent_stream_configs [- 1 ]
656662 parent_stream = parent_stream_config .stream
657663
658- for parent_record in parent_stream .read_only_records ():
664+ for raw_parent_record in parent_stream .read_only_records ():
659665 parent_record , parent_partition = self .partition_router .process_parent_record (
660- parent_record , parent_stream .name
666+ raw_parent_record , parent_stream .name
661667 )
662668 if parent_record is None :
663669 continue
@@ -676,19 +682,19 @@ def _read_pages(
676682
677683 yield from []
678684
679- def _extract_child_records (self , parent_record : Mapping ) -> Mapping :
685+ def _extract_child_records (self , parent_record : MutableMapping [ str , Any ] ) -> MutableMapping [ str , Any ] :
680686 """Extract child records from a parent record based on lazy pointers."""
681687 if not self .lazy_read_pointer :
682688 return parent_record
683689
684690 path = [path .eval (self .config ) for path in self .lazy_read_pointer ]
685691 return (
686- dpath .values (parent_record , path )
692+ dpath .values (parent_record , path ) # type: ignore # return value will be a MutableMapping, given input data structure
687693 if "*" in path
688694 else dpath .get (parent_record , path , default = [])
689695 )
690696
691- def _create_response (self , data : Mapping ) -> SafeResponse :
697+ def _create_response (self , data : Mapping [ str , Any ] ) -> SafeResponse :
692698 """Create a SafeResponse with the given data."""
693699 response = SafeResponse ()
694700 response .content = json .dumps (data ).encode ("utf-8" )
@@ -701,7 +707,7 @@ def _yield_records_with_pagination(
701707 records_generator_fn : Callable [[Optional [requests .Response ]], Iterable [Record ]],
702708 stream_state : Mapping [str , Any ],
703709 stream_slice : StreamSlice ,
704- parent_record : Record ,
710+ parent_record : MutableMapping [ str , Any ] ,
705711 parent_stream_config : Any ,
706712 ) -> Iterable [Record ]:
707713 """Yield records, handling pagination if needed."""
@@ -729,7 +735,7 @@ def _paginate(
729735 records_generator_fn : Callable [[Optional [requests .Response ]], Iterable [Record ]],
730736 stream_state : Mapping [str , Any ],
731737 stream_slice : StreamSlice ,
732- parent_record : Record ,
738+ parent_record : MutableMapping [ str , Any ] ,
733739 parent_stream_config : Any ,
734740 ) -> Iterable [Record ]:
735741 """Handle pagination by fetching subsequent pages."""
@@ -742,7 +748,9 @@ def _paginate(
742748 cursor_slice = stream_slice .cursor_slice ,
743749 )
744750
745- while next_page_token :
751+ pagination_complete = False
752+
753+ while not pagination_complete :
746754 response = self ._fetch_next_page (stream_state , stream_slice , next_page_token )
747755 last_page_size , last_record = 0 , None
748756
@@ -751,9 +759,15 @@ def _paginate(
751759 last_record = record
752760 yield record
753761
754- last_page_token_value = (
755- next_page_token .get ("next_page_token" ) if next_page_token else None
756- )
757- next_page_token = self ._next_page_token (
758- response , last_page_size , last_record , last_page_token_value
759- )
762+ if not response :
763+ pagination_complete = True
764+ else :
765+ last_page_token_value = (
766+ next_page_token .get ("next_page_token" ) if next_page_token else None
767+ )
768+ next_page_token = self ._next_page_token (
769+ response , last_page_size , last_record , last_page_token_value
770+ )
771+
772+ if not next_page_token :
773+ pagination_complete = True
0 commit comments