Skip to content

Commit 42fd595

Browse files
author
maxime.c
committed
WIP push
1 parent aa74041 commit 42fd595

15 files changed

Lines changed: 159 additions & 147 deletions

airbyte_cdk/sources/declarative/concurrent_declarative_source.py

Lines changed: 5 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -313,7 +313,7 @@ def _group_streams(
313313
partition_generator = StreamSlicerPartitionGenerator(
314314
partition_factory=DeclarativePartitionFactory(
315315
declarative_stream.name,
316-
declarative_stream.get_json_schema(),
316+
declarative_stream.schema_loader,
317317
retriever,
318318
self.message_repository,
319319
),
@@ -344,7 +344,7 @@ def _group_streams(
344344
partition_generator = StreamSlicerPartitionGenerator(
345345
DeclarativePartitionFactory(
346346
declarative_stream.name,
347-
declarative_stream.get_json_schema(),
347+
declarative_stream.schema_loader,
348348
declarative_stream.retriever,
349349
self.message_repository,
350350
),
@@ -361,7 +361,7 @@ def _group_streams(
361361
DefaultStream(
362362
partition_generator=partition_generator,
363363
name=declarative_stream.name,
364-
json_schema=declarative_stream.get_json_schema(),
364+
json_schema=lambda: declarative_stream.schema_loader.get_json_schema(),
365365
availability_strategy=AlwaysAvailableAvailabilityStrategy(),
366366
primary_key=get_primary_key_from_stream(declarative_stream.primary_key),
367367
cursor_field=None,
@@ -455,49 +455,14 @@ def _is_concurrent_cursor_incremental_without_partition_routing(
455455
in (DatetimeBasedCursorModel.__name__, IncrementingCountCursorModel.__name__)
456456
)
457457
and hasattr(declarative_stream.retriever, "stream_slicer")
458-
and (
459-
isinstance(declarative_stream.retriever.stream_slicer, DatetimeBasedCursor)
460-
# IncrementingCountCursorModel is hardcoded to be of type DatetimeBasedCursor
461-
# add isintance check here if we want to create a Declarative IncrementingCountCursor
462-
# or isinstance(
463-
# declarative_stream.retriever.stream_slicer, IncrementingCountCursor
464-
# )
465-
or isinstance(declarative_stream.retriever.stream_slicer, AsyncJobPartitionRouter)
466-
)
458+
and isinstance(declarative_stream.retriever.stream_slicer, (ConcurrentCursor, AsyncJobPartitionRouter))
467459
)
468460

469461
@staticmethod
470462
def _get_retriever(
471463
declarative_stream: DeclarativeStream, stream_state: Mapping[str, Any]
472464
) -> Retriever:
473-
retriever = declarative_stream.retriever
474-
475-
# This is an optimization so that we don't invoke any cursor or state management flows within the
476-
# low-code framework because state management is handled through the ConcurrentCursor.
477-
if declarative_stream and isinstance(retriever, SimpleRetriever):
478-
# Also a temporary hack. In the legacy Stream implementation, as part of the read,
479-
# set_initial_state() is called to instantiate incoming state on the cursor. Although we no
480-
# longer rely on the legacy low-code cursor for concurrent checkpointing, low-code components
481-
# like StopConditionPaginationStrategyDecorator still rely on a DatetimeBasedCursor that is
482-
# properly initialized with state.
483-
if retriever.cursor:
484-
retriever.cursor.set_initial_state(stream_state=stream_state)
485-
486-
# Similar to above, the ClientSideIncrementalRecordFilterDecorator cursor is a separate instance
487-
# from the one initialized on the SimpleRetriever, so it also must also have state initialized
488-
# for semi-incremental streams using is_client_side_incremental to filter properly
489-
if isinstance(retriever.record_selector, RecordSelector) and isinstance(
490-
retriever.record_selector.record_filter, ClientSideIncrementalRecordFilterDecorator
491-
):
492-
retriever.record_selector.record_filter._cursor.set_initial_state(
493-
stream_state=stream_state
494-
) # type: ignore # After non-concurrent cursors are deprecated we can remove these cursor workarounds
495-
496-
# We zero it out here, but since this is a cursor reference, the state is still properly
497-
# instantiated for the other components that reference it
498-
retriever.cursor = None
499-
500-
return retriever
465+
return declarative_stream.retriever
501466

502467
@staticmethod
503468
def _select_streams(

airbyte_cdk/sources/declarative/extractors/record_filter.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from airbyte_cdk.sources.declarative.incremental import (
88
DatetimeBasedCursor,
99
GlobalSubstreamCursor,
10-
PerPartitionWithGlobalCursor,
10+
PerPartitionWithGlobalCursor, ConcurrentPerPartitionCursor,
1111
)
1212
from airbyte_cdk.sources.declarative.interpolation.interpolated_boolean import InterpolatedBoolean
1313
from airbyte_cdk.sources.types import Config, Record, StreamSlice, StreamState

airbyte_cdk/sources/declarative/manifest_declarative_source.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,6 @@
3636
from airbyte_cdk.sources.declarative.checks.connection_checker import ConnectionChecker
3737
from airbyte_cdk.sources.declarative.declarative_source import DeclarativeSource
3838
from airbyte_cdk.sources.declarative.interpolation import InterpolatedBoolean
39-
from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
40-
ConditionalStreams as ConditionalStreamsModel,
41-
)
4239
from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
4340
DeclarativeStream as DeclarativeStreamModel,
4441
)
@@ -297,7 +294,7 @@ def connection_checker(self) -> ConnectionChecker:
297294
f"Expected to generate a ConnectionChecker component, but received {check_stream.__class__}"
298295
)
299296

300-
def streams(self, config: Mapping[str, Any]) -> List[Stream]:
297+
def streams(self, config: Mapping[str, Any]) -> List[Stream]: # FIXME with the recent change, this method can return Stream and AbstractStream which means it does not align with AbstractSource interface anymore. How big of a deal is this?
301298
if self._spec_component:
302299
self._spec_component.validate_config(config)
303300

airbyte_cdk/sources/declarative/parsers/model_to_component_factory.py

Lines changed: 30 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1620,7 +1620,6 @@ def create_concurrent_cursor_from_perpartition_cursor(
16201620
stream_namespace=stream_namespace,
16211621
config=config,
16221622
message_repository=NoopMessageRepository(),
1623-
stream_state_migrations=stream_state_migrations,
16241623
)
16251624
)
16261625

@@ -1955,7 +1954,7 @@ def create_declarative_stream(
19551954
)
19561955
if combined_slicers and not isinstance(combined_slicers, Cursor):
19571956
raise ValueError(
1958-
"Unsupported Slicer is used. PerPartitionWithGlobalCursor should be used here instead"
1957+
"Unsupported Slicer is used. PerPartitionWithGlobalCursor should be used here instead" # FIXME update error message
19591958
)
19601959
cursor = (
19611960
combined_slicers
@@ -2090,15 +2089,15 @@ def create_declarative_stream(
20902089
options["name"] = model.name
20912090
schema_loader = DefaultSchemaLoader(config=config, parameters=options)
20922091

2093-
if concurrent or (hasattr(model.retriever, "partition_router") and model.retriever.partition_router):
2092+
if concurrent or (hasattr(model.retriever, "partition_router") and model.retriever.partition_router and model.incremental_sync):
20942093
stream_name = model.name or ""
20952094
cursor = combined_slicers \
20962095
if combined_slicers and isinstance(combined_slicers, Cursor) \
20972096
else FinalStateCursor(stream_name, None, self._message_repository)
20982097
partition_generator = StreamSlicerPartitionGenerator(
20992098
DeclarativePartitionFactory(
21002099
stream_name,
2101-
schema_loader.get_json_schema(),
2100+
schema_loader,
21022101
retriever,
21032102
self._message_repository,
21042103
),
@@ -2107,12 +2106,13 @@ def create_declarative_stream(
21072106
return DefaultStream(
21082107
partition_generator=partition_generator,
21092108
name=stream_name,
2110-
json_schema=schema_loader.get_json_schema(),
2109+
json_schema=lambda: schema_loader.get_json_schema(),
21112110
availability_strategy=AlwaysAvailableAvailabilityStrategy(), # FIXME it seems this is what we do in the ConcurrentDeclarativeSource but it feels wrong
21122111
primary_key=get_primary_key_from_stream(primary_key),
21132112
cursor_field=cursor.cursor_field.cursor_field_key if hasattr(cursor, "cursor_field") else "", # FIXME we should have the cursor field has part of the interface of cursor
21142113
logger=logging.getLogger(f"airbyte.{stream_name}"), # FIXME this is a breaking change compared to the old implementation,
21152114
cursor=cursor,
2115+
supports_file_transfer=hasattr(model, "file_uploader") and bool(model.file_uploader)
21162116
)
21172117
return DeclarativeStream(
21182118
name=model.name or "",
@@ -2188,14 +2188,26 @@ def _build_incremental_cursor(
21882188
partition_router=stream_slicer,
21892189
)
21902190
elif model.incremental_sync:
2191-
return self.create_concurrent_cursor_from_datetime_based_cursor( # type: ignore # This is a known issue that we are creating and returning a ConcurrentCursor which does not technically implement the (low-code) StreamSlicer. However, (low-code) StreamSlicer and ConcurrentCursor both implement StreamSlicer.stream_slices() which is the primary method needed for checkpointing
2192-
model_type=DatetimeBasedCursorModel,
2193-
component_definition=model.incremental_sync.__dict__,
2194-
stream_name=model.name or "",
2195-
stream_namespace=None,
2196-
config=config or {},
2197-
stream_state_migrations=model.state_migrations,
2198-
)
2191+
if type(model.incremental_sync) == IncrementingCountCursorModel:
2192+
return self.create_concurrent_cursor_from_incrementing_count_cursor(
2193+
model_type=IncrementingCountCursorModel,
2194+
component_definition=model.incremental_sync.__dict__,
2195+
stream_name=model.name or "",
2196+
stream_namespace=None,
2197+
config=config or {},
2198+
stream_state_migrations=model.state_migrations,
2199+
)
2200+
elif type(model.incremental_sync) == DatetimeBasedCursorModel:
2201+
return self.create_concurrent_cursor_from_datetime_based_cursor( # type: ignore # This is a known issue that we are creating and returning a ConcurrentCursor which does not technically implement the (low-code) StreamSlicer. However, (low-code) StreamSlicer and ConcurrentCursor both implement StreamSlicer.stream_slices() which is the primary method needed for checkpointing
2202+
model_type=type(model.incremental_sync),
2203+
component_definition=model.incremental_sync.__dict__,
2204+
stream_name=model.name or "",
2205+
stream_namespace=None,
2206+
config=config or {},
2207+
stream_state_migrations=model.state_migrations,
2208+
)
2209+
else:
2210+
raise ValueError(f"Incremental sync of type {type(model.incremental_sync)} is not supported")
21992211
return None
22002212

22012213
def _build_resumable_cursor(
@@ -3235,9 +3247,6 @@ def _get_url() -> str:
32353247
config=config,
32363248
)
32373249

3238-
# Define cursor only if per partition or common incremental support is needed
3239-
cursor = stream_slicer if isinstance(stream_slicer, DeclarativeCursor) else None
3240-
32413250
if (
32423251
not isinstance(stream_slicer, (ConcurrentCursor, ConcurrentPerPartitionCursor))
32433252
or type(stream_slicer) not in [ConcurrentCursor, ConcurrentPerPartitionCursor]
@@ -3260,6 +3269,7 @@ def _get_url() -> str:
32603269
),
32613270
)
32623271

3272+
cursor = stream_slicer if isinstance(stream_slicer, (ConcurrentCursor, DeclarativeCursor)) else None
32633273
cursor_used_for_stop_condition = cursor if stop_condition_on_cursor else None
32643274
paginator = (
32653275
self._create_component_from_model(
@@ -3311,7 +3321,7 @@ def _get_url() -> str:
33113321
record_selector=record_selector,
33123322
stream_slicer=stream_slicer,
33133323
request_option_provider=request_options_provider,
3314-
cursor=cursor,
3324+
cursor=None,
33153325
config=config,
33163326
ignore_stream_slicer_parameters_on_paginated_requests=ignore_stream_slicer_parameters_on_paginated_requests,
33173327
parameters=model.parameters or {},
@@ -3325,7 +3335,7 @@ def _get_url() -> str:
33253335
record_selector=record_selector,
33263336
stream_slicer=stream_slicer,
33273337
request_option_provider=request_options_provider,
3328-
cursor=cursor,
3338+
cursor=None,
33293339
config=config,
33303340
ignore_stream_slicer_parameters_on_paginated_requests=ignore_stream_slicer_parameters_on_paginated_requests,
33313341
additional_query_properties=query_properties,
@@ -3713,7 +3723,7 @@ def _create_message_repository_substream_wrapper(
37133723
self, model: ParentStreamConfigModel, config: Config, **kwargs: Any
37143724
) -> Any:
37153725
# getting the parent state
3716-
child_state = self._connector_state_manager.get_stream_state(kwargs["stream_name"], None)
3726+
child_state = self._connector_state_manager.get_stream_state(kwargs["stream_name"], None) # FIXME adding `stream_name` as a parameter means it will be a breaking change. I assume this is mostly called internally so I don't think we need to bother that much about this but still raising the flag
37173727
if model.incremental_dependency and child_state:
37183728
parent_stream_name = model.stream.name or ""
37193729
parent_state = ConcurrentPerPartitionCursor.get_parent_state(child_state, parent_stream_name)
@@ -4070,7 +4080,7 @@ def create_grouping_partition_router(
40704080
self, model: GroupingPartitionRouterModel, config: Config, **kwargs: Any
40714081
) -> GroupingPartitionRouter:
40724082
underlying_router = self._create_component_from_model(
4073-
model=model.underlying_partition_router, config=config
4083+
model=model.underlying_partition_router, config=config, **kwargs,
40744084
)
40754085
if model.group_size < 1:
40764086
raise ValueError(f"Group size must be greater than 0, got {model.group_size}")

airbyte_cdk/sources/declarative/partition_routers/substream_partition_router.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
from airbyte_cdk.sources.types import Config, StreamSlice, StreamState
2424

2525
if TYPE_CHECKING:
26-
from airbyte_cdk.sources.streams.concurrent.default_stream import DefaultStream
26+
from airbyte_cdk.sources.streams.concurrent.abstract_stream import AbstractStream
2727

2828

2929
def iterate_with_last_flag(generator: Iterable[Partition]) -> Iterable[tuple[Partition, bool]]:
@@ -55,7 +55,7 @@ class ParentStreamConfig:
5555
incremental_dependency (bool): Indicates if the parent stream should be read incrementally.
5656
"""
5757

58-
stream: "DefaultStream"
58+
stream: "AbstractStream"
5959
parent_key: Union[InterpolatedString, str]
6060
partition_field: Union[InterpolatedString, str]
6161
config: Config

airbyte_cdk/sources/declarative/schema/dynamic_schema_loader.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
from copy import deepcopy
77
from dataclasses import InitVar, dataclass, field
8+
from functools import lru_cache
89
from typing import Any, List, Mapping, MutableMapping, Optional, Union
910

1011
import dpath
@@ -129,6 +130,7 @@ class DynamicSchemaLoader(SchemaLoader):
129130
schema_transformations: List[RecordTransformation] = field(default_factory=lambda: [])
130131
schema_filter: Optional[RecordFilter] = None
131132

133+
@lru_cache(maxsize=None)
132134
def get_json_schema(self) -> Mapping[str, Any]:
133135
"""
134136
Constructs a JSON Schema based on retrieved data.
@@ -298,3 +300,36 @@ def _extract_data(
298300
]
299301

300302
return dpath.get(body, path, default=default) # type: ignore # extracted will be a MutableMapping, given input data structure
303+
304+
def __hash__(self) -> int:
305+
"""
306+
Autogenerated by cursor
307+
308+
Returns a hash of the DynamicSchemaLoader instance based on its configuration.
309+
310+
Note: This implementation handles potentially unhashable components by converting
311+
them to hashable types or using fallback strategies.
312+
"""
313+
try:
314+
# Convert config to hashable type if it's a dict
315+
config_hash = frozenset(self.config.items()) if isinstance(self.config, dict) else self.config
316+
317+
# Convert list of transformations to tuple for hashing
318+
transformations_hash = tuple(self.schema_transformations)
319+
320+
return hash((
321+
self.retriever,
322+
config_hash,
323+
self.schema_type_identifier,
324+
transformations_hash,
325+
self.schema_filter,
326+
))
327+
except TypeError:
328+
# Fallback to id-based hashing if any component is not hashable
329+
return hash((
330+
id(self.retriever),
331+
id(self.config),
332+
id(self.schema_type_identifier),
333+
tuple(id(t) for t in self.schema_transformations),
334+
id(self.schema_filter) if self.schema_filter else None,
335+
))

airbyte_cdk/sources/declarative/schema/json_file_schema_loader.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import pkgutil
77
import sys
88
from dataclasses import InitVar, dataclass, field
9+
from functools import lru_cache
910
from typing import Any, Mapping, Tuple, Union
1011

1112
from airbyte_cdk.sources.declarative.interpolation.interpolated_string import InterpolatedString

0 commit comments

Comments
 (0)