airbyte_cdk.sources.declarative.parsers.model_to_component_factory

   1#
   2# Copyright (c) 2025 Airbyte, Inc., all rights reserved.
   3#
   4
   5from __future__ import annotations
   6
   7import datetime
   8import importlib
   9import inspect
  10import json
  11import logging
  12import re
  13from functools import partial
  14from typing import (
  15    TYPE_CHECKING,
  16    Any,
  17    Callable,
  18    Dict,
  19    List,
  20    Mapping,
  21    MutableMapping,
  22    Optional,
  23    Tuple,
  24    Type,
  25    Union,
  26    cast,
  27    get_args,
  28    get_origin,
  29    get_type_hints,
  30)
  31
  32if TYPE_CHECKING:
  33    from airbyte_cdk.legacy.sources.declarative.incremental.datetime_based_cursor import (
  34        DatetimeBasedCursor,
  35    )
  36
  37from airbyte_protocol_dataclasses.models import ConfiguredAirbyteStream
  38from isodate import parse_duration
  39from pydantic.v1 import BaseModel
  40from requests import Response
  41
  42from airbyte_cdk.connector_builder.models import (
  43    LogMessage as ConnectorBuilderLogMessage,
  44)
  45from airbyte_cdk.models import (
  46    AirbyteStateBlob,
  47    AirbyteStateMessage,
  48    AirbyteStateType,
  49    AirbyteStreamState,
  50    ConfiguredAirbyteCatalog,
  51    FailureType,
  52    Level,
  53    StreamDescriptor,
  54)
  55from airbyte_cdk.sources.connector_state_manager import ConnectorStateManager
  56from airbyte_cdk.sources.declarative.async_job.job_orchestrator import AsyncJobOrchestrator
  57from airbyte_cdk.sources.declarative.async_job.job_tracker import JobTracker
  58from airbyte_cdk.sources.declarative.async_job.repository import AsyncJobRepository
  59from airbyte_cdk.sources.declarative.async_job.status import AsyncJobStatus
  60from airbyte_cdk.sources.declarative.auth import DeclarativeOauth2Authenticator, JwtAuthenticator
  61from airbyte_cdk.sources.declarative.auth.declarative_authenticator import (
  62    DeclarativeAuthenticator,
  63    NoAuth,
  64)
  65from airbyte_cdk.sources.declarative.auth.jwt import JwtAlgorithm
  66from airbyte_cdk.sources.declarative.auth.oauth import (
  67    DeclarativeSingleUseRefreshTokenOauth2Authenticator,
  68)
  69from airbyte_cdk.sources.declarative.auth.rate_limited_multiple_token import (
  70    RateLimitedMultipleTokenAuthenticator,
  71    TokenQuota,
  72)
  73from airbyte_cdk.sources.declarative.auth.selective_authenticator import SelectiveAuthenticator
  74from airbyte_cdk.sources.declarative.auth.token import (
  75    ApiKeyAuthenticator,
  76    BasicHttpAuthenticator,
  77    BearerAuthenticator,
  78    LegacySessionTokenAuthenticator,
  79)
  80from airbyte_cdk.sources.declarative.auth.token_provider import (
  81    InterpolatedSessionTokenProvider,
  82    InterpolatedStringTokenProvider,
  83    SessionTokenProvider,
  84    TokenProvider,
  85)
  86from airbyte_cdk.sources.declarative.checks import (
  87    CheckDynamicStream,
  88    CheckStream,
  89    DynamicStreamCheckConfig,
  90)
  91from airbyte_cdk.sources.declarative.concurrency_level import ConcurrencyLevel
  92from airbyte_cdk.sources.declarative.datetime.min_max_datetime import MinMaxDatetime
  93from airbyte_cdk.sources.declarative.decoders import (
  94    Decoder,
  95    IterableDecoder,
  96    JsonDecoder,
  97    PaginationDecoderDecorator,
  98    XmlDecoder,
  99    ZipfileDecoder,
 100)
 101from airbyte_cdk.sources.declarative.decoders.composite_raw_decoder import (
 102    CompositeRawDecoder,
 103    CsvParser,
 104    GzipParser,
 105    JsonItemsParser,
 106    JsonLineParser,
 107    JsonParser,
 108    Parser,
 109)
 110from airbyte_cdk.sources.declarative.expanders.record_expander import (
 111    OnNoRecords,
 112    RecordExpander,
 113)
 114from airbyte_cdk.sources.declarative.extractors import (
 115    DpathExtractor,
 116    RecordFilter,
 117    RecordSelector,
 118    ResponseToFileExtractor,
 119)
 120from airbyte_cdk.sources.declarative.extractors.record_extractor import RecordExtractor
 121from airbyte_cdk.sources.declarative.extractors.record_filter import (
 122    ClientSideIncrementalRecordFilterDecorator,
 123)
 124from airbyte_cdk.sources.declarative.incremental import (
 125    ConcurrentCursorFactory,
 126    ConcurrentPerPartitionCursor,
 127)
 128from airbyte_cdk.sources.declarative.interpolation import InterpolatedString
 129from airbyte_cdk.sources.declarative.interpolation.interpolated_mapping import InterpolatedMapping
 130from airbyte_cdk.sources.declarative.migrations.legacy_to_per_partition_state_migration import (
 131    LegacyToPerPartitionStateMigration,
 132)
 133from airbyte_cdk.sources.declarative.models import (
 134    CustomStateMigration,
 135    PaginationResetLimits,
 136)
 137from airbyte_cdk.sources.declarative.models.base_model_with_deprecations import (
 138    DEPRECATION_LOGS_TAG,
 139    BaseModelWithDeprecations,
 140)
 141from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
 142    Action1 as PaginationResetActionModel,
 143)
 144from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
 145    AddedFieldDefinition as AddedFieldDefinitionModel,
 146)
 147from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
 148    AddFields as AddFieldsModel,
 149)
 150from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
 151    ApiKeyAuthenticator as ApiKeyAuthenticatorModel,
 152)
 153from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
 154    AsyncJobStatusMap as AsyncJobStatusMapModel,
 155)
 156from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
 157    AsyncRetriever as AsyncRetrieverModel,
 158)
 159from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
 160    BasicHttpAuthenticator as BasicHttpAuthenticatorModel,
 161)
 162from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
 163    BearerAuthenticator as BearerAuthenticatorModel,
 164)
 165from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
 166    CheckDynamicStream as CheckDynamicStreamModel,
 167)
 168from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
 169    CheckStream as CheckStreamModel,
 170)
 171from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
 172    ComplexFieldType as ComplexFieldTypeModel,
 173)
 174from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
 175    ComponentMappingDefinition as ComponentMappingDefinitionModel,
 176)
 177from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
 178    CompositeErrorHandler as CompositeErrorHandlerModel,
 179)
 180from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
 181    ConcurrencyLevel as ConcurrencyLevelModel,
 182)
 183from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
 184    ConfigAddFields as ConfigAddFieldsModel,
 185)
 186from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
 187    ConfigComponentsResolver as ConfigComponentsResolverModel,
 188)
 189from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
 190    ConfigMigration as ConfigMigrationModel,
 191)
 192from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
 193    ConfigRemapField as ConfigRemapFieldModel,
 194)
 195from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
 196    ConfigRemoveFields as ConfigRemoveFieldsModel,
 197)
 198from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
 199    ConstantBackoffStrategy as ConstantBackoffStrategyModel,
 200)
 201from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
 202    CsvDecoder as CsvDecoderModel,
 203)
 204from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
 205    CursorPagination as CursorPaginationModel,
 206)
 207from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
 208    CustomAuthenticator as CustomAuthenticatorModel,
 209)
 210from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
 211    CustomBackoffStrategy as CustomBackoffStrategyModel,
 212)
 213from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
 214    CustomConfigTransformation as CustomConfigTransformationModel,
 215)
 216from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
 217    CustomDecoder as CustomDecoderModel,
 218)
 219from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
 220    CustomErrorHandler as CustomErrorHandlerModel,
 221)
 222from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
 223    CustomPaginationStrategy as CustomPaginationStrategyModel,
 224)
 225from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
 226    CustomPartitionRouter as CustomPartitionRouterModel,
 227)
 228from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
 229    CustomRecordExtractor as CustomRecordExtractorModel,
 230)
 231from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
 232    CustomRecordFilter as CustomRecordFilterModel,
 233)
 234from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
 235    CustomRequester as CustomRequesterModel,
 236)
 237from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
 238    CustomRetriever as CustomRetrieverModel,
 239)
 240from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
 241    CustomSchemaLoader as CustomSchemaLoader,
 242)
 243from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
 244    CustomSchemaNormalization as CustomSchemaNormalizationModel,
 245)
 246from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
 247    CustomTransformation as CustomTransformationModel,
 248)
 249from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
 250    CustomValidationStrategy as CustomValidationStrategyModel,
 251)
 252from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
 253    DatetimeBasedCursor as DatetimeBasedCursorModel,
 254)
 255from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
 256    DeclarativeStream as DeclarativeStreamModel,
 257)
 258from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
 259    DefaultErrorHandler as DefaultErrorHandlerModel,
 260)
 261from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
 262    DefaultPaginator as DefaultPaginatorModel,
 263)
 264from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
 265    DpathExtractor as DpathExtractorModel,
 266)
 267from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
 268    DpathFlattenFields as DpathFlattenFieldsModel,
 269)
 270from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
 271    DpathValidator as DpathValidatorModel,
 272)
 273from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
 274    DynamicSchemaLoader as DynamicSchemaLoaderModel,
 275)
 276from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
 277    DynamicStreamCheckConfig as DynamicStreamCheckConfigModel,
 278)
 279from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
 280    ExponentialBackoffStrategy as ExponentialBackoffStrategyModel,
 281)
 282from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
 283    FileUploader as FileUploaderModel,
 284)
 285from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
 286    FixedWindowCallRatePolicy as FixedWindowCallRatePolicyModel,
 287)
 288from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
 289    FlattenFields as FlattenFieldsModel,
 290)
 291from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
 292    GroupByKeyMergeStrategy as GroupByKeyMergeStrategyModel,
 293)
 294from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
 295    GroupingPartitionRouter as GroupingPartitionRouterModel,
 296)
 297from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
 298    GzipDecoder as GzipDecoderModel,
 299)
 300from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
 301    HTTPAPIBudget as HTTPAPIBudgetModel,
 302)
 303from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
 304    HttpComponentsResolver as HttpComponentsResolverModel,
 305)
 306from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
 307    HttpRequester as HttpRequesterModel,
 308)
 309from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
 310    HttpRequestRegexMatcher as HttpRequestRegexMatcherModel,
 311)
 312from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
 313    HttpResponseFilter as HttpResponseFilterModel,
 314)
 315from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
 316    IncrementingCountCursor as IncrementingCountCursorModel,
 317)
 318from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
 319    InlineSchemaLoader as InlineSchemaLoaderModel,
 320)
 321from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
 322    IterableDecoder as IterableDecoderModel,
 323)
 324from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
 325    JsonDecoder as JsonDecoderModel,
 326)
 327from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
 328    JsonFileSchemaLoader as JsonFileSchemaLoaderModel,
 329)
 330from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
 331    JsonItemsDecoder as JsonItemsDecoderModel,
 332)
 333from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
 334    JsonlDecoder as JsonlDecoderModel,
 335)
 336from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
 337    JsonSchemaPropertySelector as JsonSchemaPropertySelectorModel,
 338)
 339from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
 340    JwtAuthenticator as JwtAuthenticatorModel,
 341)
 342from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
 343    JwtHeaders as JwtHeadersModel,
 344)
 345from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
 346    JwtPayload as JwtPayloadModel,
 347)
 348from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
 349    KeysReplace as KeysReplaceModel,
 350)
 351from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
 352    KeysToLower as KeysToLowerModel,
 353)
 354from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
 355    KeysToSnakeCase as KeysToSnakeCaseModel,
 356)
 357from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
 358    LegacySessionTokenAuthenticator as LegacySessionTokenAuthenticatorModel,
 359)
 360from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
 361    LegacyToPerPartitionStateMigration as LegacyToPerPartitionStateMigrationModel,
 362)
 363from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
 364    ListPartitionRouter as ListPartitionRouterModel,
 365)
 366from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
 367    MinMaxDatetime as MinMaxDatetimeModel,
 368)
 369from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
 370    MovingWindowCallRatePolicy as MovingWindowCallRatePolicyModel,
 371)
 372from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
 373    NoAuth as NoAuthModel,
 374)
 375from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
 376    NoPagination as NoPaginationModel,
 377)
 378from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
 379    OAuthAuthenticator as OAuthAuthenticatorModel,
 380)
 381from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
 382    OffsetIncrement as OffsetIncrementModel,
 383)
 384from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
 385    PageIncrement as PageIncrementModel,
 386)
 387from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
 388    PaginationReset as PaginationResetModel,
 389)
 390from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
 391    ParametrizedComponentsResolver as ParametrizedComponentsResolverModel,
 392)
 393from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
 394    ParentStreamConfig as ParentStreamConfigModel,
 395)
 396from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
 397    PredicateValidator as PredicateValidatorModel,
 398)
 399from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
 400    PropertiesFromEndpoint as PropertiesFromEndpointModel,
 401)
 402from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
 403    PropertyChunking as PropertyChunkingModel,
 404)
 405from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
 406    PropertyLimitType as PropertyLimitTypeModel,
 407)
 408from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
 409    QueryProperties as QueryPropertiesModel,
 410)
 411from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
 412    Rate as RateModel,
 413)
 414from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
 415    RateLimitedMultipleTokenAuthenticator as RateLimitedMultipleTokenAuthenticatorModel,
 416)
 417from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
 418    RecordExpander as RecordExpanderModel,
 419)
 420from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
 421    RecordFilter as RecordFilterModel,
 422)
 423from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
 424    RecordSelector as RecordSelectorModel,
 425)
 426from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
 427    RefreshTokenUpdater as RefreshTokenUpdaterModel,
 428)
 429from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
 430    RemoveFields as RemoveFieldsModel,
 431)
 432from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
 433    RequestOption as RequestOptionModel,
 434)
 435from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
 436    RequestPath as RequestPathModel,
 437)
 438from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
 439    ResponseToFileExtractor as ResponseToFileExtractorModel,
 440)
 441from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
 442    SchemaNormalization as SchemaNormalizationModel,
 443)
 444from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
 445    SchemaTypeIdentifier as SchemaTypeIdentifierModel,
 446)
 447from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
 448    SelectiveAuthenticator as SelectiveAuthenticatorModel,
 449)
 450from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
 451    SessionTokenAuthenticator as SessionTokenAuthenticatorModel,
 452)
 453from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
 454    SimpleRetriever as SimpleRetrieverModel,
 455)
 456from airbyte_cdk.sources.declarative.models.declarative_component_schema import Spec as SpecModel
 457from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
 458    StateDelegatingStream as StateDelegatingStreamModel,
 459)
 460from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
 461    StreamConfig as StreamConfigModel,
 462)
 463from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
 464    SubstreamPartitionRouter as SubstreamPartitionRouterModel,
 465)
 466from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
 467    TypesMap as TypesMapModel,
 468)
 469from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
 470    UnionPartitionRouter as UnionPartitionRouterModel,
 471)
 472from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
 473    UnlimitedCallRatePolicy as UnlimitedCallRatePolicyModel,
 474)
 475from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
 476    ValidateAdheresToSchema as ValidateAdheresToSchemaModel,
 477)
 478from airbyte_cdk.sources.declarative.models.declarative_component_schema import ValueType
 479from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
 480    WaitTimeFromHeader as WaitTimeFromHeaderModel,
 481)
 482from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
 483    WaitUntilTimeFromHeader as WaitUntilTimeFromHeaderModel,
 484)
 485from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
 486    XmlDecoder as XmlDecoderModel,
 487)
 488from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
 489    ZipfileDecoder as ZipfileDecoderModel,
 490)
 491from airbyte_cdk.sources.declarative.parsers.custom_code_compiler import (
 492    INJECTED_MANIFEST,
 493    AirbyteCustomCodeNotPermittedError,
 494    custom_code_execution_permitted,
 495)
 496from airbyte_cdk.sources.declarative.partition_routers import (
 497    CartesianProductStreamSlicer,
 498    GroupingPartitionRouter,
 499    ListPartitionRouter,
 500    PartitionRouter,
 501    SinglePartitionRouter,
 502    SubstreamPartitionRouter,
 503    UnionPartitionRouter,
 504)
 505from airbyte_cdk.sources.declarative.partition_routers.async_job_partition_router import (
 506    AsyncJobPartitionRouter,
 507)
 508from airbyte_cdk.sources.declarative.partition_routers.substream_partition_router import (
 509    ParentStreamConfig,
 510)
 511from airbyte_cdk.sources.declarative.requesters import HttpRequester, RequestOption
 512from airbyte_cdk.sources.declarative.requesters.error_handlers import (
 513    CompositeErrorHandler,
 514    DefaultErrorHandler,
 515    HttpResponseFilter,
 516)
 517from airbyte_cdk.sources.declarative.requesters.error_handlers.backoff_strategies import (
 518    ConstantBackoffStrategy,
 519    ExponentialBackoffStrategy,
 520    WaitTimeFromHeaderBackoffStrategy,
 521    WaitUntilTimeFromHeaderBackoffStrategy,
 522)
 523from airbyte_cdk.sources.declarative.requesters.http_job_repository import AsyncHttpJobRepository
 524from airbyte_cdk.sources.declarative.requesters.paginators import (
 525    DefaultPaginator,
 526    NoPagination,
 527    PaginatorTestReadDecorator,
 528)
 529from airbyte_cdk.sources.declarative.requesters.paginators.strategies import (
 530    CursorPaginationStrategy,
 531    CursorStopCondition,
 532    OffsetIncrement,
 533    PageIncrement,
 534    StopConditionPaginationStrategyDecorator,
 535)
 536from airbyte_cdk.sources.declarative.requesters.query_properties import (
 537    PropertiesFromEndpoint,
 538    PropertyChunking,
 539    QueryProperties,
 540)
 541from airbyte_cdk.sources.declarative.requesters.query_properties.property_chunking import (
 542    PropertyLimitType,
 543)
 544from airbyte_cdk.sources.declarative.requesters.query_properties.property_selector import (
 545    JsonSchemaPropertySelector,
 546)
 547from airbyte_cdk.sources.declarative.requesters.query_properties.strategies import (
 548    GroupByKey,
 549)
 550from airbyte_cdk.sources.declarative.requesters.request_option import RequestOptionType
 551from airbyte_cdk.sources.declarative.requesters.request_options import (
 552    DatetimeBasedRequestOptionsProvider,
 553    DefaultRequestOptionsProvider,
 554    InterpolatedRequestOptionsProvider,
 555    RequestOptionsProvider,
 556)
 557from airbyte_cdk.sources.declarative.requesters.request_options.per_partition_request_option_provider import (
 558    PerPartitionRequestOptionsProvider,
 559)
 560from airbyte_cdk.sources.declarative.requesters.request_path import RequestPath
 561from airbyte_cdk.sources.declarative.requesters.requester import HttpMethod, Requester
 562from airbyte_cdk.sources.declarative.resolvers import (
 563    ComponentMappingDefinition,
 564    ConfigComponentsResolver,
 565    HttpComponentsResolver,
 566    ParametrizedComponentsResolver,
 567    StreamConfig,
 568    StreamParametersDefinition,
 569)
 570from airbyte_cdk.sources.declarative.retrievers import (
 571    AsyncRetriever,
 572    LazySimpleRetriever,
 573    SimpleRetriever,
 574)
 575from airbyte_cdk.sources.declarative.retrievers.file_uploader import (
 576    ConnectorBuilderFileUploader,
 577    DefaultFileUploader,
 578    FileUploader,
 579    LocalFileSystemFileWriter,
 580    NoopFileWriter,
 581)
 582from airbyte_cdk.sources.declarative.retrievers.pagination_tracker import PaginationTracker
 583from airbyte_cdk.sources.declarative.schema import (
 584    ComplexFieldType,
 585    DefaultSchemaLoader,
 586    DynamicSchemaLoader,
 587    InlineSchemaLoader,
 588    JsonFileSchemaLoader,
 589    SchemaLoader,
 590    SchemaTypeIdentifier,
 591    TypesMap,
 592)
 593from airbyte_cdk.sources.declarative.schema.caching_schema_loader_decorator import (
 594    CachingSchemaLoaderDecorator,
 595)
 596from airbyte_cdk.sources.declarative.schema.composite_schema_loader import CompositeSchemaLoader
 597from airbyte_cdk.sources.declarative.spec import ConfigMigration, Spec
 598from airbyte_cdk.sources.declarative.stream_slicers import (
 599    StreamSlicer,
 600    StreamSlicerTestReadDecorator,
 601)
 602from airbyte_cdk.sources.declarative.stream_slicers.declarative_partition_generator import (
 603    DeclarativePartitionFactory,
 604    StreamSlicerPartitionGenerator,
 605)
 606from airbyte_cdk.sources.declarative.transformations import (
 607    AddFields,
 608    RecordTransformation,
 609    RemoveFields,
 610)
 611from airbyte_cdk.sources.declarative.transformations.add_fields import AddedFieldDefinition
 612from airbyte_cdk.sources.declarative.transformations.config_transformations import (
 613    ConfigAddFields,
 614    ConfigRemapField,
 615    ConfigRemoveFields,
 616)
 617from airbyte_cdk.sources.declarative.transformations.config_transformations.config_transformation import (
 618    ConfigTransformation,
 619)
 620from airbyte_cdk.sources.declarative.transformations.dpath_flatten_fields import (
 621    DpathFlattenFields,
 622    KeyTransformation,
 623)
 624from airbyte_cdk.sources.declarative.transformations.flatten_fields import (
 625    FlattenFields,
 626)
 627from airbyte_cdk.sources.declarative.transformations.keys_replace_transformation import (
 628    KeysReplaceTransformation,
 629)
 630from airbyte_cdk.sources.declarative.transformations.keys_to_lower_transformation import (
 631    KeysToLowerTransformation,
 632)
 633from airbyte_cdk.sources.declarative.transformations.keys_to_snake_transformation import (
 634    KeysToSnakeCaseTransformation,
 635)
 636from airbyte_cdk.sources.declarative.validators import (
 637    DpathValidator,
 638    PredicateValidator,
 639    ValidateAdheresToSchema,
 640)
 641from airbyte_cdk.sources.http_logger import format_http_message
 642from airbyte_cdk.sources.message import (
 643    InMemoryMessageRepository,
 644    LogAppenderMessageRepositoryDecorator,
 645    MessageRepository,
 646    NoopMessageRepository,
 647)
 648from airbyte_cdk.sources.message.repository import StateFilteringMessageRepository
 649from airbyte_cdk.sources.streams import NO_CURSOR_STATE_KEY
 650from airbyte_cdk.sources.streams.call_rate import (
 651    APIBudget,
 652    FixedWindowCallRatePolicy,
 653    HttpAPIBudget,
 654    HttpRequestRegexMatcher,
 655    MovingWindowCallRatePolicy,
 656    Rate,
 657    UnlimitedCallRatePolicy,
 658)
 659from airbyte_cdk.sources.streams.concurrent.abstract_stream import AbstractStream
 660from airbyte_cdk.sources.streams.concurrent.clamping import (
 661    ClampingEndProvider,
 662    ClampingStrategy,
 663    DayClampingStrategy,
 664    MonthClampingStrategy,
 665    NoClamping,
 666    WeekClampingStrategy,
 667    Weekday,
 668)
 669from airbyte_cdk.sources.streams.concurrent.cursor import (
 670    ConcurrentCursor,
 671    Cursor,
 672    CursorField,
 673    FinalStateCursor,
 674)
 675from airbyte_cdk.sources.streams.concurrent.default_stream import DefaultStream
 676from airbyte_cdk.sources.streams.concurrent.helpers import get_primary_key_from_stream
 677from airbyte_cdk.sources.streams.concurrent.partitions.stream_slicer import (
 678    StreamSlicer as ConcurrentStreamSlicer,
 679)
 680from airbyte_cdk.sources.streams.concurrent.state_converters.datetime_stream_state_converter import (
 681    CustomFormatConcurrentStreamStateConverter,
 682    DateTimeStreamStateConverter,
 683)
 684from airbyte_cdk.sources.streams.concurrent.state_converters.incrementing_count_stream_state_converter import (
 685    IncrementingCountStreamStateConverter,
 686)
 687from airbyte_cdk.sources.streams.http.error_handlers.response_models import ResponseAction
 688from airbyte_cdk.sources.types import Config
 689from airbyte_cdk.sources.utils.transform import TransformConfig, TypeTransformer
 690
 691ComponentDefinition = Mapping[str, Any]
 692
 693SCHEMA_TRANSFORMER_TYPE_MAPPING = {
 694    SchemaNormalizationModel.None_: TransformConfig.NoTransform,
 695    SchemaNormalizationModel.Default: TransformConfig.DefaultSchemaNormalization,
 696}
 697_NO_STREAM_SLICING = SinglePartitionRouter(parameters={})
 698
 699# Ideally this should use the value defined in ConcurrentDeclarativeSource, but
 700# this would be a circular import
 701MAX_SLICES = 5
 702
 703LOGGER = logging.getLogger(f"airbyte.model_to_component_factory")
 704
 705
 706class ModelToComponentFactory:
 707    EPOCH_DATETIME_FORMAT = "%s"
 708
 709    def __init__(
 710        self,
 711        limit_pages_fetched_per_slice: Optional[int] = None,
 712        limit_slices_fetched: Optional[int] = None,
 713        emit_connector_builder_messages: bool = False,
 714        disable_retries: bool = False,
 715        disable_cache: bool = False,
 716        message_repository: Optional[MessageRepository] = None,
 717        connector_state_manager: Optional[ConnectorStateManager] = None,
 718        max_concurrent_async_job_count: Optional[int] = None,
 719        configured_catalog: Optional[ConfiguredAirbyteCatalog] = None,
 720        api_budget: Optional[APIBudget] = None,
 721        rate_limited_authenticators: Optional[
 722            Dict[str, RateLimitedMultipleTokenAuthenticator]
 723        ] = None,
 724        custom_components_trusted: bool = True,
 725    ):
 726        self._init_mappings()
 727        self._custom_components_trusted = custom_components_trusted
 728        self._limit_pages_fetched_per_slice = limit_pages_fetched_per_slice
 729        self._limit_slices_fetched = limit_slices_fetched
 730        self._emit_connector_builder_messages = emit_connector_builder_messages
 731        self._disable_retries = disable_retries
 732        self._disable_cache = disable_cache
 733        self._message_repository = message_repository or InMemoryMessageRepository(
 734            self._evaluate_log_level(emit_connector_builder_messages)
 735        )
 736        self._stream_name_to_configured_stream = self._create_stream_name_to_configured_stream(
 737            configured_catalog
 738        )
 739        self._connector_state_manager = connector_state_manager or ConnectorStateManager()
 740        self._api_budget: Optional[Union[APIBudget]] = api_budget
 741        # Shared instances so all streams see the same token quota counters (like api_budget)
 742        self._rate_limited_authenticators: Dict[str, RateLimitedMultipleTokenAuthenticator] = (
 743            rate_limited_authenticators if rate_limited_authenticators is not None else {}
 744        )
 745        self._job_tracker: JobTracker = JobTracker(max_concurrent_async_job_count or 1)
 746        # placeholder for deprecation warnings
 747        self._collected_deprecation_logs: List[ConnectorBuilderLogMessage] = []
 748
 749    def _init_mappings(self) -> None:
 750        self.PYDANTIC_MODEL_TO_CONSTRUCTOR: Mapping[Type[BaseModel], Callable[..., Any]] = {
 751            AddedFieldDefinitionModel: self.create_added_field_definition,
 752            AddFieldsModel: self.create_add_fields,
 753            ApiKeyAuthenticatorModel: self.create_api_key_authenticator,
 754            BasicHttpAuthenticatorModel: self.create_basic_http_authenticator,
 755            BearerAuthenticatorModel: self.create_bearer_authenticator,
 756            CheckStreamModel: self.create_check_stream,
 757            DynamicStreamCheckConfigModel: self.create_dynamic_stream_check_config,
 758            CheckDynamicStreamModel: self.create_check_dynamic_stream,
 759            CompositeErrorHandlerModel: self.create_composite_error_handler,
 760            ConcurrencyLevelModel: self.create_concurrency_level,
 761            ConfigMigrationModel: self.create_config_migration,
 762            ConfigAddFieldsModel: self.create_config_add_fields,
 763            ConfigRemapFieldModel: self.create_config_remap_field,
 764            ConfigRemoveFieldsModel: self.create_config_remove_fields,
 765            ConstantBackoffStrategyModel: self.create_constant_backoff_strategy,
 766            CsvDecoderModel: self.create_csv_decoder,
 767            CursorPaginationModel: self.create_cursor_pagination,
 768            CustomAuthenticatorModel: self.create_custom_component,
 769            CustomBackoffStrategyModel: self.create_custom_component,
 770            CustomDecoderModel: self.create_custom_component,
 771            CustomErrorHandlerModel: self.create_custom_component,
 772            CustomRecordExtractorModel: self.create_custom_component,
 773            CustomRecordFilterModel: self.create_custom_component,
 774            CustomRequesterModel: self.create_custom_component,
 775            CustomRetrieverModel: self.create_custom_component,
 776            CustomSchemaLoader: self.create_custom_component,
 777            CustomSchemaNormalizationModel: self.create_custom_component,
 778            CustomStateMigration: self.create_custom_component,
 779            CustomPaginationStrategyModel: self.create_custom_component,
 780            CustomPartitionRouterModel: self.create_custom_component,
 781            CustomTransformationModel: self.create_custom_component,
 782            CustomValidationStrategyModel: self.create_custom_component,
 783            CustomConfigTransformationModel: self.create_custom_component,
 784            DeclarativeStreamModel: self.create_default_stream,
 785            DefaultErrorHandlerModel: self.create_default_error_handler,
 786            DefaultPaginatorModel: self.create_default_paginator,
 787            DpathExtractorModel: self.create_dpath_extractor,
 788            DpathValidatorModel: self.create_dpath_validator,
 789            ResponseToFileExtractorModel: self.create_response_to_file_extractor,
 790            ExponentialBackoffStrategyModel: self.create_exponential_backoff_strategy,
 791            SessionTokenAuthenticatorModel: self.create_session_token_authenticator,
 792            GroupByKeyMergeStrategyModel: self.create_group_by_key,
 793            HttpRequesterModel: self.create_http_requester,
 794            HttpResponseFilterModel: self.create_http_response_filter,
 795            InlineSchemaLoaderModel: self.create_inline_schema_loader,
 796            JsonDecoderModel: self.create_json_decoder,
 797            JsonItemsDecoderModel: self.create_json_items_decoder,
 798            JsonlDecoderModel: self.create_jsonl_decoder,
 799            JsonSchemaPropertySelectorModel: self.create_json_schema_property_selector,
 800            GzipDecoderModel: self.create_gzip_decoder,
 801            KeysToLowerModel: self.create_keys_to_lower_transformation,
 802            KeysToSnakeCaseModel: self.create_keys_to_snake_transformation,
 803            KeysReplaceModel: self.create_keys_replace_transformation,
 804            FlattenFieldsModel: self.create_flatten_fields,
 805            DpathFlattenFieldsModel: self.create_dpath_flatten_fields,
 806            IterableDecoderModel: self.create_iterable_decoder,
 807            XmlDecoderModel: self.create_xml_decoder,
 808            JsonFileSchemaLoaderModel: self.create_json_file_schema_loader,
 809            DynamicSchemaLoaderModel: self.create_dynamic_schema_loader,
 810            SchemaTypeIdentifierModel: self.create_schema_type_identifier,
 811            TypesMapModel: self.create_types_map,
 812            ComplexFieldTypeModel: self.create_complex_field_type,
 813            JwtAuthenticatorModel: self.create_jwt_authenticator,
 814            LegacyToPerPartitionStateMigrationModel: self.create_legacy_to_per_partition_state_migration,
 815            ListPartitionRouterModel: self.create_list_partition_router,
 816            MinMaxDatetimeModel: self.create_min_max_datetime,
 817            NoAuthModel: self.create_no_auth,
 818            NoPaginationModel: self.create_no_pagination,
 819            OAuthAuthenticatorModel: self.create_oauth_authenticator,
 820            OffsetIncrementModel: self.create_offset_increment,
 821            PageIncrementModel: self.create_page_increment,
 822            ParentStreamConfigModel: self.create_parent_stream_config_with_substream_wrapper,
 823            PredicateValidatorModel: self.create_predicate_validator,
 824            PropertiesFromEndpointModel: self.create_properties_from_endpoint,
 825            PropertyChunkingModel: self.create_property_chunking,
 826            QueryPropertiesModel: self.create_query_properties,
 827            RecordExpanderModel: self.create_record_expander,
 828            RecordFilterModel: self.create_record_filter,
 829            RecordSelectorModel: self.create_record_selector,
 830            RemoveFieldsModel: self.create_remove_fields,
 831            RequestPathModel: self.create_request_path,
 832            RequestOptionModel: self.create_request_option,
 833            LegacySessionTokenAuthenticatorModel: self.create_legacy_session_token_authenticator,
 834            SelectiveAuthenticatorModel: self.create_selective_authenticator,
 835            SimpleRetrieverModel: self.create_simple_retriever,
 836            StateDelegatingStreamModel: self.create_state_delegating_stream,
 837            SpecModel: self.create_spec,
 838            SubstreamPartitionRouterModel: self.create_substream_partition_router,
 839            ValidateAdheresToSchemaModel: self.create_validate_adheres_to_schema,
 840            WaitTimeFromHeaderModel: self.create_wait_time_from_header,
 841            WaitUntilTimeFromHeaderModel: self.create_wait_until_time_from_header,
 842            AsyncRetrieverModel: self.create_async_retriever,
 843            HttpComponentsResolverModel: self.create_http_components_resolver,
 844            ConfigComponentsResolverModel: self.create_config_components_resolver,
 845            ParametrizedComponentsResolverModel: self.create_parametrized_components_resolver,
 846            StreamConfigModel: self.create_stream_config,
 847            ComponentMappingDefinitionModel: self.create_components_mapping_definition,
 848            ZipfileDecoderModel: self.create_zipfile_decoder,
 849            HTTPAPIBudgetModel: self.create_http_api_budget,
 850            FileUploaderModel: self.create_file_uploader,
 851            FixedWindowCallRatePolicyModel: self.create_fixed_window_call_rate_policy,
 852            MovingWindowCallRatePolicyModel: self.create_moving_window_call_rate_policy,
 853            UnlimitedCallRatePolicyModel: self.create_unlimited_call_rate_policy,
 854            RateModel: self.create_rate,
 855            HttpRequestRegexMatcherModel: self.create_http_request_matcher,
 856            RateLimitedMultipleTokenAuthenticatorModel: self.create_rate_limited_multiple_token_authenticator,
 857            GroupingPartitionRouterModel: self.create_grouping_partition_router,
 858            UnionPartitionRouterModel: self.create_union_partition_router,
 859        }
 860
 861        # Needed for the case where we need to perform a second parse on the fields of a custom component
 862        self.TYPE_NAME_TO_MODEL = {cls.__name__: cls for cls in self.PYDANTIC_MODEL_TO_CONSTRUCTOR}
 863
 864    @staticmethod
 865    def _create_stream_name_to_configured_stream(
 866        configured_catalog: Optional[ConfiguredAirbyteCatalog],
 867    ) -> Mapping[str, ConfiguredAirbyteStream]:
 868        return (
 869            {stream.stream.name: stream for stream in configured_catalog.streams}
 870            if configured_catalog
 871            else {}
 872        )
 873
 874    def create_component(
 875        self,
 876        model_type: Type[BaseModel],
 877        component_definition: ComponentDefinition,
 878        config: Config,
 879        **kwargs: Any,
 880    ) -> Any:
 881        """
 882        Takes a given Pydantic model type and Mapping representing a component definition and creates a declarative component and
 883        subcomponents which will be used at runtime. This is done by first parsing the mapping into a Pydantic model and then creating
 884        creating declarative components from that model.
 885
 886        :param model_type: The type of declarative component that is being initialized
 887        :param component_definition: The mapping that represents a declarative component
 888        :param config: The connector config that is provided by the customer
 889        :return: The declarative component to be used at runtime
 890        """
 891
 892        component_type = component_definition.get("type")
 893        if component_definition.get("type") != model_type.__name__:
 894            raise ValueError(
 895                f"Expected manifest component of type {model_type.__name__}, but received {component_type} instead"
 896            )
 897
 898        declarative_component_model = model_type.parse_obj(component_definition)
 899
 900        if not isinstance(declarative_component_model, model_type):
 901            raise ValueError(
 902                f"Expected {model_type.__name__} component, but received {declarative_component_model.__class__.__name__}"
 903            )
 904
 905        return self._create_component_from_model(
 906            model=declarative_component_model, config=config, **kwargs
 907        )
 908
 909    def _create_component_from_model(self, model: BaseModel, config: Config, **kwargs: Any) -> Any:
 910        if model.__class__ not in self.PYDANTIC_MODEL_TO_CONSTRUCTOR:
 911            raise ValueError(
 912                f"{model.__class__} with attributes {model} is not a valid component type"
 913            )
 914        component_constructor = self.PYDANTIC_MODEL_TO_CONSTRUCTOR.get(model.__class__)
 915        if not component_constructor:
 916            raise ValueError(f"Could not find constructor for {model.__class__}")
 917
 918        # collect deprecation warnings for supported models.
 919        if isinstance(model, BaseModelWithDeprecations):
 920            self._collect_model_deprecations(model)
 921
 922        return component_constructor(model=model, config=config, **kwargs)
 923
 924    def get_model_deprecations(self) -> List[ConnectorBuilderLogMessage]:
 925        """
 926        Returns the deprecation warnings that were collected during the creation of components.
 927        """
 928        return self._collected_deprecation_logs
 929
 930    def _collect_model_deprecations(self, model: BaseModelWithDeprecations) -> None:
 931        """
 932        Collects deprecation logs from the given model and appends any new logs to the internal collection.
 933
 934        This method checks if the provided model has deprecation logs (identified by the presence of the DEPRECATION_LOGS_TAG attribute and a non-None `_deprecation_logs` property). It iterates through each deprecation log in the model and appends it to the `_collected_deprecation_logs` list if it has not already been collected, ensuring that duplicate logs are avoided.
 935
 936        Args:
 937            model (BaseModelWithDeprecations): The model instance from which to collect deprecation logs.
 938        """
 939        if hasattr(model, DEPRECATION_LOGS_TAG) and model._deprecation_logs is not None:
 940            for log in model._deprecation_logs:
 941                # avoid duplicates for deprecation logs observed.
 942                if log not in self._collected_deprecation_logs:
 943                    self._collected_deprecation_logs.append(log)
 944
 945    def create_config_migration(
 946        self, model: ConfigMigrationModel, config: Config
 947    ) -> ConfigMigration:
 948        transformations: List[ConfigTransformation] = [
 949            self._create_component_from_model(transformation, config)
 950            for transformation in model.transformations
 951        ]
 952
 953        return ConfigMigration(
 954            description=model.description,
 955            transformations=transformations,
 956        )
 957
 958    def create_config_add_fields(
 959        self, model: ConfigAddFieldsModel, config: Config, **kwargs: Any
 960    ) -> ConfigAddFields:
 961        fields = [self._create_component_from_model(field, config) for field in model.fields]
 962        return ConfigAddFields(
 963            fields=fields,
 964            condition=model.condition or "",
 965        )
 966
 967    @staticmethod
 968    def create_config_remove_fields(
 969        model: ConfigRemoveFieldsModel, config: Config, **kwargs: Any
 970    ) -> ConfigRemoveFields:
 971        return ConfigRemoveFields(
 972            field_pointers=model.field_pointers,
 973            condition=model.condition or "",
 974        )
 975
 976    @staticmethod
 977    def create_config_remap_field(
 978        model: ConfigRemapFieldModel, config: Config, **kwargs: Any
 979    ) -> ConfigRemapField:
 980        mapping = cast(Mapping[str, Any], model.map)
 981        return ConfigRemapField(
 982            map=mapping,
 983            field_path=model.field_path,
 984            config=config,
 985        )
 986
 987    def create_dpath_validator(self, model: DpathValidatorModel, config: Config) -> DpathValidator:
 988        strategy = self._create_component_from_model(model.validation_strategy, config)
 989
 990        return DpathValidator(
 991            field_path=model.field_path,
 992            strategy=strategy,
 993        )
 994
 995    def create_predicate_validator(
 996        self, model: PredicateValidatorModel, config: Config
 997    ) -> PredicateValidator:
 998        strategy = self._create_component_from_model(model.validation_strategy, config)
 999
1000        return PredicateValidator(
1001            value=model.value,
1002            strategy=strategy,
1003        )
1004
1005    @staticmethod
1006    def create_validate_adheres_to_schema(
1007        model: ValidateAdheresToSchemaModel, config: Config, **kwargs: Any
1008    ) -> ValidateAdheresToSchema:
1009        base_schema = cast(Mapping[str, Any], model.base_schema)
1010        return ValidateAdheresToSchema(
1011            schema=base_schema,
1012        )
1013
1014    @staticmethod
1015    def create_added_field_definition(
1016        model: AddedFieldDefinitionModel, config: Config, **kwargs: Any
1017    ) -> AddedFieldDefinition:
1018        interpolated_value = InterpolatedString.create(
1019            model.value, parameters=model.parameters or {}
1020        )
1021        return AddedFieldDefinition(
1022            path=model.path,
1023            value=interpolated_value,
1024            value_type=ModelToComponentFactory._json_schema_type_name_to_type(model.value_type),
1025            parameters=model.parameters or {},
1026        )
1027
1028    def create_add_fields(self, model: AddFieldsModel, config: Config, **kwargs: Any) -> AddFields:
1029        added_field_definitions = [
1030            self._create_component_from_model(
1031                model=added_field_definition_model,
1032                value_type=ModelToComponentFactory._json_schema_type_name_to_type(
1033                    added_field_definition_model.value_type
1034                ),
1035                config=config,
1036            )
1037            for added_field_definition_model in model.fields
1038        ]
1039        return AddFields(
1040            fields=added_field_definitions,
1041            condition=model.condition or "",
1042            parameters=model.parameters or {},
1043        )
1044
1045    def create_keys_to_lower_transformation(
1046        self, model: KeysToLowerModel, config: Config, **kwargs: Any
1047    ) -> KeysToLowerTransformation:
1048        return KeysToLowerTransformation()
1049
1050    def create_keys_to_snake_transformation(
1051        self, model: KeysToSnakeCaseModel, config: Config, **kwargs: Any
1052    ) -> KeysToSnakeCaseTransformation:
1053        return KeysToSnakeCaseTransformation()
1054
1055    def create_keys_replace_transformation(
1056        self, model: KeysReplaceModel, config: Config, **kwargs: Any
1057    ) -> KeysReplaceTransformation:
1058        return KeysReplaceTransformation(
1059            old=model.old, new=model.new, parameters=model.parameters or {}
1060        )
1061
1062    def create_flatten_fields(
1063        self, model: FlattenFieldsModel, config: Config, **kwargs: Any
1064    ) -> FlattenFields:
1065        return FlattenFields(
1066            flatten_lists=model.flatten_lists if model.flatten_lists is not None else True
1067        )
1068
1069    def create_dpath_flatten_fields(
1070        self, model: DpathFlattenFieldsModel, config: Config, **kwargs: Any
1071    ) -> DpathFlattenFields:
1072        model_field_path: List[Union[InterpolatedString, str]] = [x for x in model.field_path]
1073        key_transformation = (
1074            KeyTransformation(
1075                config=config,
1076                prefix=model.key_transformation.prefix,
1077                suffix=model.key_transformation.suffix,
1078                parameters=model.parameters or {},
1079            )
1080            if model.key_transformation is not None
1081            else None
1082        )
1083        return DpathFlattenFields(
1084            config=config,
1085            field_path=model_field_path,
1086            delete_origin_value=model.delete_origin_value
1087            if model.delete_origin_value is not None
1088            else False,
1089            replace_record=model.replace_record if model.replace_record is not None else False,
1090            key_transformation=key_transformation,
1091            parameters=model.parameters or {},
1092        )
1093
1094    @staticmethod
1095    def _json_schema_type_name_to_type(value_type: Optional[ValueType]) -> Optional[Type[Any]]:
1096        if not value_type:
1097            return None
1098        names_to_types = {
1099            ValueType.string: str,
1100            ValueType.number: float,
1101            ValueType.integer: int,
1102            ValueType.boolean: bool,
1103        }
1104        return names_to_types[value_type]
1105
1106    def create_api_key_authenticator(
1107        self,
1108        model: ApiKeyAuthenticatorModel,
1109        config: Config,
1110        token_provider: Optional[TokenProvider] = None,
1111        **kwargs: Any,
1112    ) -> ApiKeyAuthenticator:
1113        if model.inject_into is None and model.header is None:
1114            raise ValueError(
1115                "Expected either inject_into or header to be set for ApiKeyAuthenticator"
1116            )
1117
1118        if model.inject_into is not None and model.header is not None:
1119            raise ValueError(
1120                "inject_into and header cannot be set both for ApiKeyAuthenticator - remove the deprecated header option"
1121            )
1122
1123        if token_provider is not None and model.api_token != "":
1124            raise ValueError(
1125                "If token_provider is set, api_token is ignored and has to be set to empty string."
1126            )
1127
1128        request_option = (
1129            self._create_component_from_model(
1130                model.inject_into, config, parameters=model.parameters or {}
1131            )
1132            if model.inject_into
1133            else RequestOption(
1134                inject_into=RequestOptionType.header,
1135                field_name=model.header or "",
1136                parameters=model.parameters or {},
1137            )
1138        )
1139
1140        return ApiKeyAuthenticator(
1141            token_provider=(
1142                token_provider
1143                if token_provider is not None
1144                else InterpolatedStringTokenProvider(
1145                    api_token=model.api_token or "",
1146                    config=config,
1147                    parameters=model.parameters or {},
1148                )
1149            ),
1150            request_option=request_option,
1151            config=config,
1152            parameters=model.parameters or {},
1153        )
1154
1155    def create_legacy_to_per_partition_state_migration(
1156        self,
1157        model: LegacyToPerPartitionStateMigrationModel,
1158        config: Mapping[str, Any],
1159        declarative_stream: DeclarativeStreamModel,
1160    ) -> LegacyToPerPartitionStateMigration:
1161        retriever = declarative_stream.retriever
1162        if not isinstance(retriever, (SimpleRetrieverModel, AsyncRetrieverModel)):
1163            raise ValueError(
1164                f"LegacyToPerPartitionStateMigrations can only be applied on a DeclarativeStream with a SimpleRetriever or AsyncRetriever. Got {type(retriever)}"
1165            )
1166        partition_router = retriever.partition_router
1167        if not isinstance(
1168            partition_router,
1169            (
1170                SubstreamPartitionRouterModel,
1171                CustomPartitionRouterModel,
1172                UnionPartitionRouterModel,
1173            ),
1174        ):
1175            raise ValueError(
1176                f"LegacyToPerPartitionStateMigrations can only be applied on a SimpleRetriever with a SubstreamPartitionRouter, UnionPartitionRouter or CustomPartitionRouter. Got {type(partition_router)}"
1177            )
1178        if not isinstance(partition_router, UnionPartitionRouterModel) and not hasattr(
1179            partition_router, "parent_stream_configs"
1180        ):
1181            raise ValueError(
1182                "LegacyToPerPartitionStateMigrations can only be applied with a parent stream configuration."
1183            )
1184
1185        if not hasattr(declarative_stream, "incremental_sync"):
1186            raise ValueError(
1187                "LegacyToPerPartitionStateMigrations can only be applied with an incremental_sync configuration."
1188            )
1189
1190        return LegacyToPerPartitionStateMigration(
1191            partition_router,  # type: ignore # was already checked above
1192            declarative_stream.incremental_sync,  # type: ignore # was already checked. Migration can be applied only to incremental streams.
1193            config,
1194            declarative_stream.parameters,  # type: ignore # different type is expected here Mapping[str, Any], got Dict[str, Any]
1195        )
1196
1197    def create_session_token_authenticator(
1198        self, model: SessionTokenAuthenticatorModel, config: Config, name: str, **kwargs: Any
1199    ) -> Union[ApiKeyAuthenticator, BearerAuthenticator]:
1200        decoder = (
1201            self._create_component_from_model(model=model.decoder, config=config)
1202            if model.decoder
1203            else JsonDecoder(parameters={})
1204        )
1205        login_requester = self._create_component_from_model(
1206            model=model.login_requester,
1207            config=config,
1208            name=f"{name}_login_requester",
1209            decoder=decoder,
1210        )
1211        token_provider = SessionTokenProvider(
1212            login_requester=login_requester,
1213            session_token_path=model.session_token_path,
1214            expiration_duration=parse_duration(model.expiration_duration)
1215            if model.expiration_duration
1216            else None,
1217            parameters=model.parameters or {},
1218            message_repository=self._message_repository,
1219            decoder=decoder,
1220        )
1221        if model.request_authentication.type == "Bearer":
1222            return ModelToComponentFactory.create_bearer_authenticator(
1223                BearerAuthenticatorModel(type="BearerAuthenticator", api_token=""),  # type: ignore # $parameters has a default value
1224                config,
1225                token_provider=token_provider,
1226            )
1227        else:
1228            # Get the api_token template if specified, default to just the session token
1229            api_token_template = (
1230                getattr(model.request_authentication, "api_token", None) or "{{ session_token }}"
1231            )
1232            final_token_provider: TokenProvider = InterpolatedSessionTokenProvider(
1233                config=config,
1234                api_token=api_token_template,
1235                session_token_provider=token_provider,
1236                parameters=model.parameters or {},
1237            )
1238            return self.create_api_key_authenticator(
1239                ApiKeyAuthenticatorModel(
1240                    type="ApiKeyAuthenticator",
1241                    api_token="",
1242                    inject_into=model.request_authentication.inject_into,
1243                ),  # type: ignore # $parameters and headers default to None
1244                config=config,
1245                token_provider=final_token_provider,
1246            )
1247
1248    @staticmethod
1249    def create_basic_http_authenticator(
1250        model: BasicHttpAuthenticatorModel, config: Config, **kwargs: Any
1251    ) -> BasicHttpAuthenticator:
1252        return BasicHttpAuthenticator(
1253            password=model.password or "",
1254            username=model.username,
1255            config=config,
1256            parameters=model.parameters or {},
1257        )
1258
1259    @staticmethod
1260    def create_bearer_authenticator(
1261        model: BearerAuthenticatorModel,
1262        config: Config,
1263        token_provider: Optional[TokenProvider] = None,
1264        **kwargs: Any,
1265    ) -> BearerAuthenticator:
1266        if token_provider is not None and model.api_token != "":
1267            raise ValueError(
1268                "If token_provider is set, api_token is ignored and has to be set to empty string."
1269            )
1270        return BearerAuthenticator(
1271            token_provider=(
1272                token_provider
1273                if token_provider is not None
1274                else InterpolatedStringTokenProvider(
1275                    api_token=model.api_token or "",
1276                    config=config,
1277                    parameters=model.parameters or {},
1278                )
1279            ),
1280            config=config,
1281            parameters=model.parameters or {},
1282        )
1283
1284    @staticmethod
1285    def create_dynamic_stream_check_config(
1286        model: DynamicStreamCheckConfigModel, config: Config, **kwargs: Any
1287    ) -> DynamicStreamCheckConfig:
1288        return DynamicStreamCheckConfig(
1289            dynamic_stream_name=model.dynamic_stream_name,
1290            stream_count=model.stream_count,
1291        )
1292
1293    def create_check_stream(
1294        self, model: CheckStreamModel, config: Config, **kwargs: Any
1295    ) -> CheckStream:
1296        if model.dynamic_streams_check_configs is None and model.stream_names is None:
1297            raise ValueError(
1298                "Expected either stream_names or dynamic_streams_check_configs to be set for CheckStream"
1299            )
1300
1301        dynamic_streams_check_configs = (
1302            [
1303                self._create_component_from_model(model=dynamic_stream_check_config, config=config)
1304                for dynamic_stream_check_config in model.dynamic_streams_check_configs
1305            ]
1306            if model.dynamic_streams_check_configs
1307            else []
1308        )
1309
1310        # `model.config_overrides` is deliberately not read here. The source applies it around the whole
1311        # check operation (`ConcurrentDeclarativeSource._config_overridden_for_check`), which is what makes
1312        # it work for every checker type rather than only this one. Do not wire it in a second time.
1313        return CheckStream(
1314            stream_names=model.stream_names or [],
1315            dynamic_streams_check_configs=dynamic_streams_check_configs,
1316            parameters={},
1317        )
1318
1319    @staticmethod
1320    def create_check_dynamic_stream(
1321        model: CheckDynamicStreamModel, config: Config, **kwargs: Any
1322    ) -> CheckDynamicStream:
1323        assert model.use_check_availability is not None  # for mypy
1324
1325        use_check_availability = model.use_check_availability
1326
1327        # See `create_check_stream`: `model.config_overrides` is applied by the source, not here.
1328        return CheckDynamicStream(
1329            stream_count=model.stream_count,
1330            use_check_availability=use_check_availability,
1331            parameters={},
1332        )
1333
1334    def create_composite_error_handler(
1335        self, model: CompositeErrorHandlerModel, config: Config, **kwargs: Any
1336    ) -> CompositeErrorHandler:
1337        error_handlers = [
1338            self._create_component_from_model(model=error_handler_model, config=config)
1339            for error_handler_model in model.error_handlers
1340        ]
1341        return CompositeErrorHandler(
1342            error_handlers=error_handlers, parameters=model.parameters or {}
1343        )
1344
1345    @staticmethod
1346    def create_concurrency_level(
1347        model: ConcurrencyLevelModel, config: Config, **kwargs: Any
1348    ) -> ConcurrencyLevel:
1349        return ConcurrencyLevel(
1350            default_concurrency=model.default_concurrency,
1351            max_concurrency=model.max_concurrency,
1352            config=config,
1353            parameters={},
1354        )
1355
1356    @staticmethod
1357    def apply_stream_state_migrations(
1358        stream_state_migrations: List[Any] | None, stream_state: MutableMapping[str, Any]
1359    ) -> MutableMapping[str, Any]:
1360        if stream_state_migrations:
1361            for state_migration in stream_state_migrations:
1362                if state_migration.should_migrate(stream_state):
1363                    # The state variable is expected to be mutable but the migrate method returns an immutable mapping.
1364                    stream_state = dict(state_migration.migrate(stream_state))
1365        return stream_state
1366
1367    def create_concurrent_cursor_from_datetime_based_cursor(
1368        self,
1369        model_type: Type[BaseModel],
1370        component_definition: ComponentDefinition,
1371        stream_name: str,
1372        stream_namespace: Optional[str],
1373        stream_state: MutableMapping[str, Any],
1374        config: Config,
1375        message_repository: Optional[MessageRepository] = None,
1376        runtime_lookback_window: Optional[datetime.timedelta] = None,
1377        **kwargs: Any,
1378    ) -> ConcurrentCursor:
1379        component_type = component_definition.get("type")
1380        if component_definition.get("type") != model_type.__name__:
1381            raise ValueError(
1382                f"Expected manifest component of type {model_type.__name__}, but received {component_type} instead"
1383            )
1384
1385        # FIXME the interfaces of the concurrent cursor are kind of annoying as they take a `ComponentDefinition` instead of the actual model. This was done because the ConcurrentDeclarativeSource didn't have access to the models [here for example](https://github.com/airbytehq/airbyte-python-cdk/blob/f525803b3fec9329e4cc8478996a92bf884bfde9/airbyte_cdk/sources/declarative/concurrent_declarative_source.py#L354C54-L354C91). So now we have two cases:
1386        # * The ComponentDefinition comes from model.__dict__ in which case we have `parameters`
1387        # * The ComponentDefinition comes from the manifest as a dict in which case we have `$parameters`
1388        # We should change those interfaces to use the model once we clean up the code in CDS at which point the parameter propagation should happen as part of the ModelToComponentFactory.
1389        if "$parameters" not in component_definition and "parameters" in component_definition:
1390            component_definition["$parameters"] = component_definition.get("parameters")  # type: ignore  # This is a dict
1391        datetime_based_cursor_model = model_type.parse_obj(component_definition)
1392
1393        if not isinstance(datetime_based_cursor_model, DatetimeBasedCursorModel):
1394            raise ValueError(
1395                f"Expected {model_type.__name__} component, but received {datetime_based_cursor_model.__class__.__name__}"
1396            )
1397
1398        model_parameters = datetime_based_cursor_model.parameters or {}
1399
1400        cursor_field = self._get_catalog_defined_cursor_field(
1401            stream_name=stream_name,
1402            allow_catalog_defined_cursor_field=datetime_based_cursor_model.allow_catalog_defined_cursor_field
1403            or False,
1404        )
1405
1406        if not cursor_field:
1407            interpolated_cursor_field = InterpolatedString.create(
1408                datetime_based_cursor_model.cursor_field,
1409                parameters=model_parameters,
1410            )
1411            cursor_field = CursorField(
1412                cursor_field_key=interpolated_cursor_field.eval(config=config),
1413                supports_catalog_defined_cursor_field=datetime_based_cursor_model.allow_catalog_defined_cursor_field
1414                or False,
1415            )
1416
1417        interpolated_partition_field_start = InterpolatedString.create(
1418            datetime_based_cursor_model.partition_field_start or "start_time",
1419            parameters=model_parameters,
1420        )
1421        interpolated_partition_field_end = InterpolatedString.create(
1422            datetime_based_cursor_model.partition_field_end or "end_time",
1423            parameters=model_parameters,
1424        )
1425
1426        slice_boundary_fields = (
1427            interpolated_partition_field_start.eval(config=config),
1428            interpolated_partition_field_end.eval(config=config),
1429        )
1430
1431        datetime_format = datetime_based_cursor_model.datetime_format
1432
1433        cursor_granularity = (
1434            parse_duration(datetime_based_cursor_model.cursor_granularity)
1435            if datetime_based_cursor_model.cursor_granularity
1436            else None
1437        )
1438
1439        lookback_window = None
1440        interpolated_lookback_window = (
1441            InterpolatedString.create(
1442                datetime_based_cursor_model.lookback_window,
1443                parameters=model_parameters,
1444            )
1445            if datetime_based_cursor_model.lookback_window
1446            else None
1447        )
1448        if interpolated_lookback_window:
1449            evaluated_lookback_window = interpolated_lookback_window.eval(config=config)
1450            if evaluated_lookback_window:
1451                lookback_window = parse_duration(evaluated_lookback_window)
1452
1453        connector_state_converter: DateTimeStreamStateConverter
1454        connector_state_converter = CustomFormatConcurrentStreamStateConverter(
1455            datetime_format=datetime_format,
1456            input_datetime_formats=datetime_based_cursor_model.cursor_datetime_formats,
1457            is_sequential_state=True,  # ConcurrentPerPartitionCursor only works with sequential state
1458            cursor_granularity=cursor_granularity,
1459        )
1460
1461        # Adjusts the stream state by applying the runtime lookback window.
1462        # This is used to ensure correct state handling in case of failed partitions.
1463        stream_state_value = stream_state.get(cursor_field.cursor_field_key)
1464        if runtime_lookback_window and stream_state_value:
1465            new_stream_state = (
1466                connector_state_converter.parse_timestamp(stream_state_value)
1467                - runtime_lookback_window
1468            )
1469            stream_state[cursor_field.cursor_field_key] = connector_state_converter.output_format(
1470                new_stream_state
1471            )
1472
1473        start_date_runtime_value: Union[InterpolatedString, str, MinMaxDatetime]
1474        if isinstance(datetime_based_cursor_model.start_datetime, MinMaxDatetimeModel):
1475            start_date_runtime_value = self.create_min_max_datetime(
1476                model=datetime_based_cursor_model.start_datetime, config=config
1477            )
1478        else:
1479            start_date_runtime_value = datetime_based_cursor_model.start_datetime
1480
1481        end_date_runtime_value: Optional[Union[InterpolatedString, str, MinMaxDatetime]]
1482        if isinstance(datetime_based_cursor_model.end_datetime, MinMaxDatetimeModel):
1483            end_date_runtime_value = self.create_min_max_datetime(
1484                model=datetime_based_cursor_model.end_datetime, config=config
1485            )
1486        else:
1487            end_date_runtime_value = datetime_based_cursor_model.end_datetime
1488
1489        interpolated_start_date = MinMaxDatetime.create(
1490            interpolated_string_or_min_max_datetime=start_date_runtime_value,
1491            parameters=datetime_based_cursor_model.parameters,
1492        )
1493        interpolated_end_date = (
1494            None
1495            if not end_date_runtime_value
1496            else MinMaxDatetime.create(
1497                end_date_runtime_value, datetime_based_cursor_model.parameters
1498            )
1499        )
1500
1501        # If datetime format is not specified then start/end datetime should inherit it from the stream slicer
1502        if not interpolated_start_date.datetime_format:
1503            interpolated_start_date.datetime_format = datetime_format
1504        if interpolated_end_date and not interpolated_end_date.datetime_format:
1505            interpolated_end_date.datetime_format = datetime_format
1506
1507        start_date = interpolated_start_date.get_datetime(config=config)
1508        end_date_provider = (
1509            partial(interpolated_end_date.get_datetime, config)
1510            if interpolated_end_date
1511            else connector_state_converter.get_end_provider()
1512        )
1513
1514        if (
1515            datetime_based_cursor_model.step and not datetime_based_cursor_model.cursor_granularity
1516        ) or (
1517            not datetime_based_cursor_model.step and datetime_based_cursor_model.cursor_granularity
1518        ):
1519            raise ValueError(
1520                f"If step is defined, cursor_granularity should be as well and vice-versa. "
1521                f"Right now, step is `{datetime_based_cursor_model.step}` and cursor_granularity is `{datetime_based_cursor_model.cursor_granularity}`"
1522            )
1523
1524        # When step is not defined, default to a step size from the starting date to the present moment
1525        step_length = datetime.timedelta.max
1526        interpolated_step = (
1527            InterpolatedString.create(
1528                datetime_based_cursor_model.step,
1529                parameters=model_parameters,
1530            )
1531            if datetime_based_cursor_model.step
1532            else None
1533        )
1534        if interpolated_step:
1535            evaluated_step = interpolated_step.eval(config)
1536            if evaluated_step:
1537                step_length = parse_duration(evaluated_step)
1538
1539        clamping_strategy: ClampingStrategy = NoClamping()
1540        if datetime_based_cursor_model.clamping:
1541            # While it is undesirable to interpolate within the model factory (as opposed to at runtime),
1542            # it is still better than shifting interpolation low-code concept into the ConcurrentCursor runtime
1543            # object which we want to keep agnostic of being low-code
1544            target = InterpolatedString(
1545                string=datetime_based_cursor_model.clamping.target,
1546                parameters=model_parameters,
1547            )
1548            evaluated_target = target.eval(config=config)
1549            match evaluated_target:
1550                case "DAY":
1551                    clamping_strategy = DayClampingStrategy()
1552                    end_date_provider = ClampingEndProvider(
1553                        DayClampingStrategy(is_ceiling=False),
1554                        end_date_provider,  # type: ignore  # Having issues w/ inspection for GapType and CursorValueType as shown in existing tests. Confirmed functionality is working in practice
1555                        granularity=cursor_granularity or datetime.timedelta(seconds=1),
1556                    )
1557                case "WEEK":
1558                    if (
1559                        not datetime_based_cursor_model.clamping.target_details
1560                        or "weekday" not in datetime_based_cursor_model.clamping.target_details
1561                    ):
1562                        raise ValueError(
1563                            "Given WEEK clamping, weekday needs to be provided as target_details"
1564                        )
1565                    weekday = self._assemble_weekday(
1566                        datetime_based_cursor_model.clamping.target_details["weekday"]
1567                    )
1568                    clamping_strategy = WeekClampingStrategy(weekday)
1569                    end_date_provider = ClampingEndProvider(
1570                        WeekClampingStrategy(weekday, is_ceiling=False),
1571                        end_date_provider,  # type: ignore  # Having issues w/ inspection for GapType and CursorValueType as shown in existing tests. Confirmed functionality is working in practice
1572                        granularity=cursor_granularity or datetime.timedelta(days=1),
1573                    )
1574                case "MONTH":
1575                    clamping_strategy = MonthClampingStrategy()
1576                    end_date_provider = ClampingEndProvider(
1577                        MonthClampingStrategy(is_ceiling=False),
1578                        end_date_provider,  # type: ignore  # Having issues w/ inspection for GapType and CursorValueType as shown in existing tests. Confirmed functionality is working in practice
1579                        granularity=cursor_granularity or datetime.timedelta(days=1),
1580                    )
1581                case _:
1582                    raise ValueError(
1583                        f"Invalid clamping target {evaluated_target}, expected DAY, WEEK, MONTH"
1584                    )
1585
1586        return ConcurrentCursor(
1587            stream_name=stream_name,
1588            stream_namespace=stream_namespace,
1589            stream_state=stream_state,
1590            message_repository=message_repository or self._message_repository,
1591            connector_state_manager=self._connector_state_manager,
1592            connector_state_converter=connector_state_converter,
1593            cursor_field=cursor_field,
1594            slice_boundary_fields=slice_boundary_fields,
1595            start=start_date,  # type: ignore  # Having issues w/ inspection for GapType and CursorValueType as shown in existing tests. Confirmed functionality is working in practice
1596            end_provider=end_date_provider,  # type: ignore  # Having issues w/ inspection for GapType and CursorValueType as shown in existing tests. Confirmed functionality is working in practice
1597            lookback_window=lookback_window,
1598            slice_range=step_length,
1599            cursor_granularity=cursor_granularity,
1600            clamping_strategy=clamping_strategy,
1601        )
1602
1603    def create_concurrent_cursor_from_incrementing_count_cursor(
1604        self,
1605        model_type: Type[BaseModel],
1606        component_definition: ComponentDefinition,
1607        stream_name: str,
1608        stream_namespace: Optional[str],
1609        stream_state: MutableMapping[str, Any],
1610        config: Config,
1611        message_repository: Optional[MessageRepository] = None,
1612        **kwargs: Any,
1613    ) -> ConcurrentCursor:
1614        component_type = component_definition.get("type")
1615        if component_definition.get("type") != model_type.__name__:
1616            raise ValueError(
1617                f"Expected manifest component of type {model_type.__name__}, but received {component_type} instead"
1618            )
1619
1620        incrementing_count_cursor_model = model_type.parse_obj(component_definition)
1621
1622        if not isinstance(incrementing_count_cursor_model, IncrementingCountCursorModel):
1623            raise ValueError(
1624                f"Expected {model_type.__name__} component, but received {incrementing_count_cursor_model.__class__.__name__}"
1625            )
1626
1627        start_value: Union[int, str, None] = incrementing_count_cursor_model.start_value
1628        # Pydantic Union type coercion can convert int 0 to string '0' depending on Union order.
1629        # We need to handle both int and str representations of numeric values.
1630        # Evaluate the InterpolatedString and convert to int for the ConcurrentCursor.
1631        if start_value is not None:
1632            interpolated_start_value = InterpolatedString.create(
1633                str(start_value),  # Ensure we pass a string to InterpolatedString.create
1634                parameters=incrementing_count_cursor_model.parameters or {},
1635            )
1636            evaluated_start_value: int = int(interpolated_start_value.eval(config=config))
1637        else:
1638            evaluated_start_value = 0
1639
1640        cursor_field = self._get_catalog_defined_cursor_field(
1641            stream_name=stream_name,
1642            allow_catalog_defined_cursor_field=incrementing_count_cursor_model.allow_catalog_defined_cursor_field
1643            or False,
1644        )
1645
1646        if not cursor_field:
1647            interpolated_cursor_field = InterpolatedString.create(
1648                incrementing_count_cursor_model.cursor_field,
1649                parameters=incrementing_count_cursor_model.parameters or {},
1650            )
1651            cursor_field = CursorField(
1652                cursor_field_key=interpolated_cursor_field.eval(config=config),
1653                supports_catalog_defined_cursor_field=incrementing_count_cursor_model.allow_catalog_defined_cursor_field
1654                or False,
1655            )
1656
1657        connector_state_converter = IncrementingCountStreamStateConverter(
1658            is_sequential_state=True,  # ConcurrentPerPartitionCursor only works with sequential state
1659        )
1660
1661        return ConcurrentCursor(
1662            stream_name=stream_name,
1663            stream_namespace=stream_namespace,
1664            stream_state=stream_state,
1665            message_repository=message_repository or self._message_repository,
1666            connector_state_manager=self._connector_state_manager,
1667            connector_state_converter=connector_state_converter,
1668            cursor_field=cursor_field,
1669            slice_boundary_fields=None,
1670            start=evaluated_start_value,  # type: ignore  # Having issues w/ inspection for GapType and CursorValueType as shown in existing tests. Confirmed functionality is working in practice
1671            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
1672        )
1673
1674    def _assemble_weekday(self, weekday: str) -> Weekday:
1675        match weekday:
1676            case "MONDAY":
1677                return Weekday.MONDAY
1678            case "TUESDAY":
1679                return Weekday.TUESDAY
1680            case "WEDNESDAY":
1681                return Weekday.WEDNESDAY
1682            case "THURSDAY":
1683                return Weekday.THURSDAY
1684            case "FRIDAY":
1685                return Weekday.FRIDAY
1686            case "SATURDAY":
1687                return Weekday.SATURDAY
1688            case "SUNDAY":
1689                return Weekday.SUNDAY
1690            case _:
1691                raise ValueError(f"Unknown weekday {weekday}")
1692
1693    def create_concurrent_cursor_from_perpartition_cursor(
1694        self,
1695        state_manager: ConnectorStateManager,
1696        model_type: Type[BaseModel],
1697        component_definition: ComponentDefinition,
1698        stream_name: str,
1699        stream_namespace: Optional[str],
1700        config: Config,
1701        stream_state: MutableMapping[str, Any],
1702        partition_router: PartitionRouter,
1703        attempt_to_create_cursor_if_not_provided: bool = False,
1704        **kwargs: Any,
1705    ) -> ConcurrentPerPartitionCursor:
1706        component_type = component_definition.get("type")
1707        if component_definition.get("type") != model_type.__name__:
1708            raise ValueError(
1709                f"Expected manifest component of type {model_type.__name__}, but received {component_type} instead"
1710            )
1711
1712        # FIXME the interfaces of the concurrent cursor are kind of annoying as they take a `ComponentDefinition` instead of the actual model. This was done because the ConcurrentDeclarativeSource didn't have access to the models [here for example](https://github.com/airbytehq/airbyte-python-cdk/blob/f525803b3fec9329e4cc8478996a92bf884bfde9/airbyte_cdk/sources/declarative/concurrent_declarative_source.py#L354C54-L354C91). So now we have two cases:
1713        # * The ComponentDefinition comes from model.__dict__ in which case we have `parameters`
1714        # * The ComponentDefinition comes from the manifest as a dict in which case we have `$parameters`
1715        # We should change those interfaces to use the model once we clean up the code in CDS at which point the parameter propagation should happen as part of the ModelToComponentFactory.
1716        if "$parameters" not in component_definition and "parameters" in component_definition:
1717            component_definition["$parameters"] = component_definition.get("parameters")  # type: ignore  # This is a dict
1718        datetime_based_cursor_model = model_type.parse_obj(component_definition)
1719
1720        if not isinstance(datetime_based_cursor_model, DatetimeBasedCursorModel):
1721            raise ValueError(
1722                f"Expected {model_type.__name__} component, but received {datetime_based_cursor_model.__class__.__name__}"
1723            )
1724
1725        cursor_field = self._get_catalog_defined_cursor_field(
1726            stream_name=stream_name,
1727            allow_catalog_defined_cursor_field=datetime_based_cursor_model.allow_catalog_defined_cursor_field
1728            or False,
1729        )
1730
1731        if not cursor_field:
1732            interpolated_cursor_field = InterpolatedString.create(
1733                datetime_based_cursor_model.cursor_field,
1734                # FIXME the interfaces of the concurrent cursor are kind of annoying as they take a `ComponentDefinition` instead of the actual model. This was done because the ConcurrentDeclarativeSource didn't have access to the models [here for example](https://github.com/airbytehq/airbyte-python-cdk/blob/f525803b3fec9329e4cc8478996a92bf884bfde9/airbyte_cdk/sources/declarative/concurrent_declarative_source.py#L354C54-L354C91). So now we have two cases:
1735                # * The ComponentDefinition comes from model.__dict__ in which case we have `parameters`
1736                # * The ComponentDefinition comes from the manifest as a dict in which case we have `$parameters`
1737                # We should change those interfaces to use the model once we clean up the code in CDS at which point the parameter propagation should happen as part of the ModelToComponentFactory.
1738                parameters=datetime_based_cursor_model.parameters or {},
1739            )
1740            cursor_field = CursorField(
1741                cursor_field_key=interpolated_cursor_field.eval(config=config),
1742                supports_catalog_defined_cursor_field=datetime_based_cursor_model.allow_catalog_defined_cursor_field
1743                or False,
1744            )
1745
1746        datetime_format = datetime_based_cursor_model.datetime_format
1747
1748        cursor_granularity = (
1749            parse_duration(datetime_based_cursor_model.cursor_granularity)
1750            if datetime_based_cursor_model.cursor_granularity
1751            else None
1752        )
1753
1754        connector_state_converter: DateTimeStreamStateConverter
1755        connector_state_converter = CustomFormatConcurrentStreamStateConverter(
1756            datetime_format=datetime_format,
1757            input_datetime_formats=datetime_based_cursor_model.cursor_datetime_formats,
1758            is_sequential_state=True,  # ConcurrentPerPartitionCursor only works with sequential state
1759            cursor_granularity=cursor_granularity,
1760        )
1761
1762        # Create the cursor factory
1763        cursor_factory = ConcurrentCursorFactory(
1764            partial(
1765                self.create_concurrent_cursor_from_datetime_based_cursor,
1766                state_manager=state_manager,
1767                model_type=model_type,
1768                component_definition=component_definition,
1769                stream_name=stream_name,
1770                stream_namespace=stream_namespace,
1771                config=config,
1772                message_repository=NoopMessageRepository(),
1773            )
1774        )
1775
1776        # Per-partition state doesn't make sense for GroupingPartitionRouter, so force the global state
1777        use_global_cursor = isinstance(
1778            partition_router, GroupingPartitionRouter
1779        ) or component_definition.get("global_substream_cursor", False)
1780
1781        # Return the concurrent cursor and state converter
1782        return ConcurrentPerPartitionCursor(
1783            cursor_factory=cursor_factory,
1784            partition_router=partition_router,
1785            stream_name=stream_name,
1786            stream_namespace=stream_namespace,
1787            stream_state=stream_state,
1788            message_repository=self._message_repository,  # type: ignore
1789            connector_state_manager=state_manager,
1790            connector_state_converter=connector_state_converter,
1791            cursor_field=cursor_field,
1792            use_global_cursor=use_global_cursor,
1793            attempt_to_create_cursor_if_not_provided=attempt_to_create_cursor_if_not_provided,
1794        )
1795
1796    @staticmethod
1797    def create_constant_backoff_strategy(
1798        model: ConstantBackoffStrategyModel, config: Config, **kwargs: Any
1799    ) -> ConstantBackoffStrategy:
1800        ModelToComponentFactory._validate_jitter_range(model.jitter_range_in_seconds)
1801        return ConstantBackoffStrategy(
1802            backoff_time_in_seconds=model.backoff_time_in_seconds,
1803            jitter_range_in_seconds=model.jitter_range_in_seconds,
1804            config=config,
1805            parameters=model.parameters or {},
1806        )
1807
1808    @staticmethod
1809    def _validate_jitter_range(jitter_range_in_seconds: Optional[float]) -> None:
1810        if jitter_range_in_seconds is not None and jitter_range_in_seconds < 0:
1811            raise ValueError("jitter_range_in_seconds must be greater than or equal to 0")
1812
1813    def create_cursor_pagination(
1814        self, model: CursorPaginationModel, config: Config, decoder: Decoder, **kwargs: Any
1815    ) -> CursorPaginationStrategy:
1816        if isinstance(decoder, PaginationDecoderDecorator):
1817            inner_decoder = decoder.decoder
1818        else:
1819            inner_decoder = decoder
1820            decoder = PaginationDecoderDecorator(decoder=decoder)
1821
1822        if self._is_supported_decoder_for_pagination(inner_decoder):
1823            decoder_to_use = decoder
1824        else:
1825            raise ValueError(
1826                self._UNSUPPORTED_DECODER_ERROR.format(decoder_type=type(inner_decoder))
1827            )
1828
1829        # Pydantic v1 Union type coercion can convert int to string depending on Union order.
1830        # If page_size is a string that represents an integer (not an interpolation), convert it back.
1831        page_size = model.page_size
1832        if isinstance(page_size, str) and page_size.isdigit():
1833            page_size = int(page_size)
1834
1835        return CursorPaginationStrategy(
1836            cursor_value=model.cursor_value,
1837            decoder=decoder_to_use,
1838            page_size=page_size,
1839            stop_condition=model.stop_condition,
1840            config=config,
1841            parameters=model.parameters or {},
1842        )
1843
1844    def create_custom_component(self, model: Any, config: Config, **kwargs: Any) -> Any:
1845        """
1846        Generically creates a custom component based on the model type and a class_name reference to the custom Python class being
1847        instantiated. Only the model's additional properties that match the custom class definition are passed to the constructor
1848        :param model: The Pydantic model of the custom component being created
1849        :param config: The custom defined connector config
1850        :return: The declarative component built from the Pydantic model to be used at runtime
1851        """
1852        # Instantiating a custom component means importing and executing arbitrary code referenced
1853        # by `class_name`. Manifests supplied by a caller, whether through the config or directly to
1854        # the manifest server, are untrusted input and could point `class_name` at any importable
1855        # callable, so they honor the same `AIRBYTE_ENABLE_UNSAFE_CODE` gate as injected
1856        # `components.py` code. Manifests bundled in a published connector image are trusted and may
1857        # always use their bundled custom components.
1858        manifest_is_untrusted = not self._custom_components_trusted or bool(
1859            config.get(INJECTED_MANIFEST)
1860        )
1861        if manifest_is_untrusted and not custom_code_execution_permitted():
1862            raise AirbyteCustomCodeNotPermittedError
1863
1864        custom_component_class = self._get_class_from_fully_qualified_class_name(model.class_name)
1865        component_fields = get_type_hints(custom_component_class)
1866        model_args = model.dict()
1867        model_args["config"] = config
1868
1869        # There are cases where a parent component will pass arguments to a child component via kwargs. When there are field collisions
1870        # we defer to these arguments over the component's definition
1871        for key, arg in kwargs.items():
1872            model_args[key] = arg
1873
1874        # Pydantic is unable to parse a custom component's fields that are subcomponents into models because their fields and types are not
1875        # defined in the schema. The fields and types are defined within the Python class implementation. Pydantic can only parse down to
1876        # the custom component and this code performs a second parse to convert the sub-fields first into models, then declarative components
1877        for model_field, model_value in model_args.items():
1878            # If a custom component field doesn't have a type set, we try to use the type hints to infer the type
1879            if (
1880                isinstance(model_value, dict)
1881                and "type" not in model_value
1882                and model_field in component_fields
1883            ):
1884                derived_type = self._derive_component_type_from_type_hints(
1885                    component_fields.get(model_field)
1886                )
1887                if derived_type:
1888                    model_value["type"] = derived_type
1889
1890            if self._is_component(model_value):
1891                model_args[model_field] = self._create_nested_component(
1892                    model,
1893                    model_field,
1894                    model_value,
1895                    config,
1896                    **kwargs,
1897                )
1898            elif isinstance(model_value, list):
1899                vals = []
1900                for v in model_value:
1901                    if isinstance(v, dict) and "type" not in v and model_field in component_fields:
1902                        derived_type = self._derive_component_type_from_type_hints(
1903                            component_fields.get(model_field)
1904                        )
1905                        if derived_type:
1906                            v["type"] = derived_type
1907                    if self._is_component(v):
1908                        vals.append(
1909                            self._create_nested_component(
1910                                model,
1911                                model_field,
1912                                v,
1913                                config,
1914                                **kwargs,
1915                            )
1916                        )
1917                    else:
1918                        vals.append(v)
1919                model_args[model_field] = vals
1920
1921        kwargs = {
1922            class_field: model_args[class_field]
1923            for class_field in component_fields.keys()
1924            if class_field in model_args
1925        }
1926
1927        if "api_budget" in component_fields and kwargs.get("api_budget") is None:
1928            kwargs["api_budget"] = self._api_budget
1929
1930        return custom_component_class(**kwargs)
1931
1932    @staticmethod
1933    def _get_class_from_fully_qualified_class_name(
1934        full_qualified_class_name: str,
1935    ) -> Any:
1936        """Get a class from its fully qualified name.
1937
1938        If a custom components module is needed, we assume it is already registered - probably
1939        as `source_declarative_manifest.components` or `components`.
1940
1941        Args:
1942            full_qualified_class_name (str): The fully qualified name of the class (e.g., "module.ClassName").
1943
1944        Returns:
1945            Any: The class object.
1946
1947        Raises:
1948            ValueError: If the class cannot be loaded.
1949        """
1950        split = full_qualified_class_name.split(".")
1951        module_name_full = ".".join(split[:-1])
1952        class_name = split[-1]
1953
1954        try:
1955            module_ref = importlib.import_module(module_name_full)
1956        except ModuleNotFoundError as e:
1957            if split[0] == "source_declarative_manifest":
1958                # During testing, the modules containing the custom components are not moved to source_declarative_manifest. In order to run the test, add the source folder to your PYTHONPATH or add it runtime using sys.path.append
1959                try:
1960                    import os
1961
1962                    module_name_with_source_declarative_manifest = ".".join(split[1:-1])
1963                    module_ref = importlib.import_module(
1964                        module_name_with_source_declarative_manifest
1965                    )
1966                except ModuleNotFoundError:
1967                    raise ValueError(f"Could not load module `{module_name_full}`.") from e
1968            else:
1969                raise ValueError(f"Could not load module `{module_name_full}`.") from e
1970
1971        try:
1972            return getattr(module_ref, class_name)
1973        except AttributeError as e:
1974            raise ValueError(
1975                f"Could not load class `{class_name}` from module `{module_name_full}`.",
1976            ) from e
1977
1978    @staticmethod
1979    def _derive_component_type_from_type_hints(field_type: Any) -> Optional[str]:
1980        interface = field_type
1981        while True:
1982            origin = get_origin(interface)
1983            if origin:
1984                # Unnest types until we reach the raw type
1985                # List[T] -> T
1986                # Optional[List[T]] -> T
1987                args = get_args(interface)
1988                interface = args[0]
1989            else:
1990                break
1991        if isinstance(interface, type) and not ModelToComponentFactory.is_builtin_type(interface):
1992            return interface.__name__
1993        return None
1994
1995    @staticmethod
1996    def is_builtin_type(cls: Optional[Type[Any]]) -> bool:
1997        if not cls:
1998            return False
1999        return cls.__module__ == "builtins"
2000
2001    @staticmethod
2002    def _extract_missing_parameters(error: TypeError) -> List[str]:
2003        parameter_search = re.search(r"keyword-only.*:\s(.*)", str(error))
2004        if parameter_search:
2005            return re.findall(r"\'(.+?)\'", parameter_search.group(1))
2006        else:
2007            return []
2008
2009    def _create_nested_component(
2010        self, model: Any, model_field: str, model_value: Any, config: Config, **kwargs: Any
2011    ) -> Any:
2012        type_name = model_value.get("type", None)
2013        if not type_name:
2014            # If no type is specified, we can assume this is a dictionary object which can be returned instead of a subcomponent
2015            return model_value
2016
2017        model_type = self.TYPE_NAME_TO_MODEL.get(type_name, None)
2018        if model_type:
2019            parsed_model = model_type.parse_obj(model_value)
2020            try:
2021                # To improve usability of the language, certain fields are shared between components. This can come in the form of
2022                # a parent component passing some of its fields to a child component or the parent extracting fields from other child
2023                # components and passing it to others. One example is the DefaultPaginator referencing the HttpRequester url_base
2024                # while constructing a SimpleRetriever. However, custom components don't support this behavior because they are created
2025                # generically in create_custom_component(). This block allows developers to specify extra arguments in $parameters that
2026                # are needed by a component and could not be shared.
2027                model_constructor = self.PYDANTIC_MODEL_TO_CONSTRUCTOR.get(parsed_model.__class__)
2028                constructor_kwargs = inspect.getfullargspec(model_constructor).kwonlyargs
2029                model_parameters = model_value.get("$parameters", {})
2030                matching_parameters = {
2031                    kwarg: model_parameters[kwarg]
2032                    for kwarg in constructor_kwargs
2033                    if kwarg in model_parameters
2034                }
2035                matching_kwargs = {
2036                    kwarg: kwargs[kwarg] for kwarg in constructor_kwargs if kwarg in kwargs
2037                }
2038                return self._create_component_from_model(
2039                    model=parsed_model, config=config, **(matching_parameters | matching_kwargs)
2040                )
2041            except TypeError as error:
2042                missing_parameters = self._extract_missing_parameters(error)
2043                if missing_parameters:
2044                    raise ValueError(
2045                        f"Error creating component '{type_name}' with parent custom component {model.class_name}: Please provide "
2046                        + ", ".join(
2047                            (
2048                                f"{type_name}.$parameters.{parameter}"
2049                                for parameter in missing_parameters
2050                            )
2051                        )
2052                    )
2053                raise TypeError(
2054                    f"Error creating component '{type_name}' with parent custom component {model.class_name}: {error}"
2055                )
2056        else:
2057            raise ValueError(
2058                f"Error creating custom component {model.class_name}. Subcomponent creation has not been implemented for '{type_name}'"
2059            )
2060
2061    @staticmethod
2062    def _is_component(model_value: Any) -> bool:
2063        return isinstance(model_value, dict) and model_value.get("type") is not None
2064
2065    def create_default_stream(
2066        self, model: DeclarativeStreamModel, config: Config, is_parent: bool = False, **kwargs: Any
2067    ) -> AbstractStream:
2068        primary_key = model.primary_key.__root__ if model.primary_key else None
2069        self._migrate_state(model, config)
2070        self._warn_on_ineffective_incremental_dependency(model)
2071
2072        partition_router = self._build_stream_slicer_from_partition_router(
2073            model.retriever,
2074            config,
2075            stream_name=model.name,
2076            **kwargs,
2077        )
2078        concurrent_cursor = self._build_concurrent_cursor(model, partition_router, config)
2079        if model.incremental_sync and isinstance(model.incremental_sync, DatetimeBasedCursorModel):
2080            cursor_model: DatetimeBasedCursorModel = model.incremental_sync
2081
2082            end_time_option = (
2083                self._create_component_from_model(
2084                    cursor_model.end_time_option, config, parameters=cursor_model.parameters or {}
2085                )
2086                if cursor_model.end_time_option
2087                else None
2088            )
2089            start_time_option = (
2090                self._create_component_from_model(
2091                    cursor_model.start_time_option, config, parameters=cursor_model.parameters or {}
2092                )
2093                if cursor_model.start_time_option
2094                else None
2095            )
2096
2097            datetime_request_options_provider = DatetimeBasedRequestOptionsProvider(
2098                start_time_option=start_time_option,
2099                end_time_option=end_time_option,
2100                partition_field_start=cursor_model.partition_field_start,
2101                partition_field_end=cursor_model.partition_field_end,
2102                config=config,
2103                parameters=model.parameters or {},
2104            )
2105            request_options_provider = (
2106                datetime_request_options_provider
2107                if not isinstance(concurrent_cursor, ConcurrentPerPartitionCursor)
2108                else PerPartitionRequestOptionsProvider(
2109                    partition_router, datetime_request_options_provider
2110                )
2111            )
2112        elif model.incremental_sync and isinstance(
2113            model.incremental_sync, IncrementingCountCursorModel
2114        ):
2115            if isinstance(concurrent_cursor, ConcurrentPerPartitionCursor):
2116                raise ValueError(
2117                    "PerPartition does not support per partition states because switching to global state is time based"
2118                )
2119
2120            cursor_model: IncrementingCountCursorModel = model.incremental_sync  # type: ignore
2121
2122            start_time_option = (
2123                self._create_component_from_model(
2124                    cursor_model.start_value_option,  # type: ignore # mypy still thinks cursor_model of type DatetimeBasedCursor
2125                    config,
2126                    parameters=cursor_model.parameters or {},
2127                )
2128                if cursor_model.start_value_option  # type: ignore # mypy still thinks cursor_model of type DatetimeBasedCursor
2129                else None
2130            )
2131
2132            # The concurrent engine defaults the start/end fields on the slice to "start" and "end", but
2133            # the default DatetimeBasedRequestOptionsProvider() sets them to start_time/end_time
2134            partition_field_start = "start"
2135
2136            request_options_provider = DatetimeBasedRequestOptionsProvider(
2137                start_time_option=start_time_option,
2138                partition_field_start=partition_field_start,
2139                config=config,
2140                parameters=model.parameters or {},
2141            )
2142        else:
2143            request_options_provider = None
2144
2145        transformations = []
2146        if model.transformations:
2147            for transformation_model in model.transformations:
2148                transformations.append(
2149                    self._create_component_from_model(model=transformation_model, config=config)
2150                )
2151        file_uploader = None
2152        if model.file_uploader:
2153            file_uploader = self._create_component_from_model(
2154                model=model.file_uploader, config=config
2155            )
2156
2157        stream_slicer: ConcurrentStreamSlicer = (
2158            partition_router
2159            if isinstance(concurrent_cursor, FinalStateCursor)
2160            else concurrent_cursor
2161        )
2162
2163        retriever = self._create_component_from_model(
2164            model=model.retriever,
2165            config=config,
2166            name=model.name,
2167            primary_key=primary_key,
2168            request_options_provider=request_options_provider,
2169            stream_slicer=stream_slicer,
2170            partition_router=partition_router,
2171            has_stop_condition_cursor=self._is_stop_condition_on_cursor(model),
2172            is_client_side_incremental_sync=self._is_client_side_filtering_enabled(model),
2173            cursor=concurrent_cursor,
2174            transformations=transformations,
2175            file_uploader=file_uploader,
2176            incremental_sync=model.incremental_sync,
2177        )
2178        if isinstance(retriever, AsyncRetriever):
2179            stream_slicer = retriever.stream_slicer
2180
2181        schema_loader: SchemaLoader
2182        if model.schema_loader and isinstance(model.schema_loader, list):
2183            nested_schema_loaders = [
2184                self._create_component_from_model(model=nested_schema_loader, config=config)
2185                for nested_schema_loader in model.schema_loader
2186            ]
2187            schema_loader = CompositeSchemaLoader(
2188                schema_loaders=nested_schema_loaders, parameters={}
2189            )
2190        elif model.schema_loader:
2191            schema_loader = self._create_component_from_model(
2192                model=model.schema_loader,  # type: ignore # If defined, schema_loader is guaranteed not to be a list and will be one of the existing base models
2193                config=config,
2194            )
2195        else:
2196            options = model.parameters or {}
2197            if "name" not in options:
2198                options["name"] = model.name
2199            schema_loader = DefaultSchemaLoader(config=config, parameters=options)
2200        schema_loader = CachingSchemaLoaderDecorator(schema_loader)
2201
2202        stream_name = model.name or ""
2203        return DefaultStream(
2204            partition_generator=StreamSlicerPartitionGenerator(
2205                DeclarativePartitionFactory(
2206                    stream_name,
2207                    schema_loader,
2208                    retriever,
2209                    self._message_repository,
2210                ),
2211                stream_slicer,
2212                slice_limit=self._limit_slices_fetched,
2213            ),
2214            name=stream_name,
2215            json_schema=schema_loader.get_json_schema,
2216            primary_key=get_primary_key_from_stream(primary_key),
2217            cursor_field=(
2218                concurrent_cursor.cursor_field
2219                if hasattr(concurrent_cursor, "cursor_field")
2220                else None
2221            ),
2222            logger=logging.getLogger(f"airbyte.{stream_name}"),
2223            cursor=concurrent_cursor,
2224            supports_file_transfer=hasattr(model, "file_uploader") and bool(model.file_uploader),
2225        )
2226
2227    def _warn_on_ineffective_incremental_dependency(self, model: DeclarativeStreamModel) -> None:
2228        """
2229        `incremental_dependency: true` only takes effect when the substream defines its own
2230        `incremental_sync`: the parent cursor is persisted under the `parent_state` key of the
2231        substream's state, which is only emitted by incremental substreams. On a stream without
2232        `incremental_sync`, the setting is silently ignored and all parent records are re-read on
2233        every sync, so we warn about the misconfiguration instead.
2234        """
2235        if model.incremental_sync:
2236            return
2237
2238        partition_router = getattr(model.retriever, "partition_router", None)
2239        if not partition_router:
2240            return
2241
2242        routers = partition_router if isinstance(partition_router, list) else [partition_router]
2243        for router in routers:
2244            if isinstance(router, GroupingPartitionRouterModel):
2245                router = router.underlying_partition_router
2246            if isinstance(router, SubstreamPartitionRouterModel) and any(
2247                parent_stream_config.incremental_dependency
2248                for parent_stream_config in router.parent_stream_configs
2249            ):
2250                LOGGER.warning(
2251                    f"Stream `{model.name}` has `incremental_dependency: true` in its parent stream configuration but does not define `incremental_sync`. "
2252                    "The parent stream's cursor is only persisted in the state of an incremental substream, so this setting has no effect and all parent records will be re-read on every sync. "
2253                    "Define `incremental_sync` on this stream or remove `incremental_dependency`."
2254                )
2255                return
2256
2257    def _migrate_state(self, model: DeclarativeStreamModel, config: Config) -> None:
2258        stream_name = model.name or ""
2259        stream_state = self._connector_state_manager.get_stream_state(
2260            stream_name=stream_name, namespace=None
2261        )
2262        if model.state_migrations:
2263            state_transformations = [
2264                self._create_component_from_model(state_migration, config, declarative_stream=model)
2265                for state_migration in model.state_migrations
2266            ]
2267        else:
2268            state_transformations = []
2269        stream_state = self.apply_stream_state_migrations(state_transformations, stream_state)
2270        self._connector_state_manager.update_state_for_stream(
2271            stream_name=stream_name, namespace=None, value=stream_state
2272        )
2273
2274    def _is_stop_condition_on_cursor(self, model: DeclarativeStreamModel) -> bool:
2275        return bool(
2276            model.incremental_sync
2277            and hasattr(model.incremental_sync, "is_data_feed")
2278            and model.incremental_sync.is_data_feed
2279        )
2280
2281    def _is_client_side_filtering_enabled(self, model: DeclarativeStreamModel) -> bool:
2282        return bool(
2283            model.incremental_sync
2284            and hasattr(model.incremental_sync, "is_client_side_incremental")
2285            and model.incremental_sync.is_client_side_incremental
2286        )
2287
2288    def _build_stream_slicer_from_partition_router(
2289        self,
2290        model: Union[
2291            AsyncRetrieverModel,
2292            CustomRetrieverModel,
2293            SimpleRetrieverModel,
2294        ],
2295        config: Config,
2296        stream_name: Optional[str] = None,
2297        **kwargs: Any,
2298    ) -> PartitionRouter:
2299        if (
2300            hasattr(model, "partition_router")
2301            and isinstance(model, (SimpleRetrieverModel, AsyncRetrieverModel, CustomRetrieverModel))
2302            and model.partition_router
2303        ):
2304            stream_slicer_model = model.partition_router
2305            if isinstance(stream_slicer_model, list):
2306                return CartesianProductStreamSlicer(
2307                    [
2308                        self._create_component_from_model(
2309                            model=slicer, config=config, stream_name=stream_name or ""
2310                        )
2311                        for slicer in stream_slicer_model
2312                    ],
2313                    parameters={},
2314                )
2315            elif isinstance(stream_slicer_model, dict):
2316                # partition router comes from CustomRetrieverModel therefore has not been parsed as a model
2317                params = stream_slicer_model.get("$parameters")
2318                if not isinstance(params, dict):
2319                    params = {}
2320                    stream_slicer_model["$parameters"] = params
2321
2322                if stream_name is not None:
2323                    params["stream_name"] = stream_name
2324
2325                return self._create_nested_component(  # type: ignore[no-any-return] # There is no guarantee that this will return a stream slicer. If not, we expect an AttributeError during the call to `stream_slices`
2326                    model,
2327                    "partition_router",
2328                    stream_slicer_model,
2329                    config,
2330                    **kwargs,
2331                )
2332            else:
2333                return self._create_component_from_model(  # type: ignore[no-any-return] # Will be created PartitionRouter as stream_slicer_model is model.partition_router
2334                    model=stream_slicer_model, config=config, stream_name=stream_name or ""
2335                )
2336        return SinglePartitionRouter(parameters={})
2337
2338    def _build_concurrent_cursor(
2339        self,
2340        model: DeclarativeStreamModel,
2341        stream_slicer: Optional[PartitionRouter],
2342        config: Config,
2343    ) -> Cursor:
2344        stream_name = model.name or ""
2345        stream_state = self._connector_state_manager.get_stream_state(stream_name, None)
2346
2347        if (
2348            model.incremental_sync
2349            and stream_slicer
2350            and not isinstance(stream_slicer, SinglePartitionRouter)
2351        ):
2352            if isinstance(model.incremental_sync, IncrementingCountCursorModel):
2353                # We don't currently support usage of partition routing and IncrementingCountCursor at the
2354                # same time because we didn't solve for design questions like what the lookback window would
2355                # be as well as global cursor fall backs. We have not seen customers that have needed both
2356                # at the same time yet and are currently punting on this until we need to solve it.
2357                raise ValueError(
2358                    f"The low-code framework does not currently support usage of a PartitionRouter and an IncrementingCountCursor at the same time. Please specify only one of these options for stream {stream_name}."
2359                )
2360            return self.create_concurrent_cursor_from_perpartition_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
2361                state_manager=self._connector_state_manager,
2362                model_type=DatetimeBasedCursorModel,
2363                component_definition=model.incremental_sync.__dict__,
2364                stream_name=stream_name,
2365                stream_state=stream_state,
2366                stream_namespace=None,
2367                config=config or {},
2368                partition_router=stream_slicer,
2369                attempt_to_create_cursor_if_not_provided=True,  # FIXME can we remove that now?
2370            )
2371        elif model.incremental_sync:
2372            if type(model.incremental_sync) == IncrementingCountCursorModel:
2373                return self.create_concurrent_cursor_from_incrementing_count_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
2374                    model_type=IncrementingCountCursorModel,
2375                    component_definition=model.incremental_sync.__dict__,
2376                    stream_name=stream_name,
2377                    stream_namespace=None,
2378                    stream_state=stream_state,
2379                    config=config or {},
2380                )
2381            elif type(model.incremental_sync) == DatetimeBasedCursorModel:
2382                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
2383                    model_type=type(model.incremental_sync),
2384                    component_definition=model.incremental_sync.__dict__,
2385                    stream_name=stream_name,
2386                    stream_namespace=None,
2387                    stream_state=stream_state,
2388                    config=config or {},
2389                    attempt_to_create_cursor_if_not_provided=True,
2390                )
2391            else:
2392                raise ValueError(
2393                    f"Incremental sync of type {type(model.incremental_sync)} is not supported"
2394                )
2395        return FinalStateCursor(stream_name, None, self._message_repository)
2396
2397    def create_default_error_handler(
2398        self, model: DefaultErrorHandlerModel, config: Config, **kwargs: Any
2399    ) -> DefaultErrorHandler:
2400        backoff_strategies = []
2401        if model.backoff_strategies:
2402            for backoff_strategy_model in model.backoff_strategies:
2403                backoff_strategies.append(
2404                    self._create_component_from_model(model=backoff_strategy_model, config=config)
2405                )
2406
2407        response_filters = []
2408        if model.response_filters:
2409            for response_filter_model in model.response_filters:
2410                response_filters.append(
2411                    self._create_component_from_model(model=response_filter_model, config=config)
2412                )
2413        response_filters.append(
2414            HttpResponseFilter(config=config, parameters=model.parameters or {})
2415        )
2416
2417        return DefaultErrorHandler(
2418            backoff_strategies=backoff_strategies,
2419            max_retries=model.max_retries,
2420            response_filters=response_filters,
2421            config=config,
2422            parameters=model.parameters or {},
2423        )
2424
2425    def create_default_paginator(
2426        self,
2427        model: DefaultPaginatorModel,
2428        config: Config,
2429        *,
2430        url_base: str,
2431        extractor_model: Optional[Union[CustomRecordExtractorModel, DpathExtractorModel]] = None,
2432        decoder: Optional[Decoder] = None,
2433        cursor_used_for_stop_condition: Optional[Cursor] = None,
2434    ) -> Union[DefaultPaginator, PaginatorTestReadDecorator]:
2435        if decoder:
2436            if self._is_supported_decoder_for_pagination(decoder):
2437                decoder_to_use = PaginationDecoderDecorator(decoder=decoder)
2438            else:
2439                raise ValueError(self._UNSUPPORTED_DECODER_ERROR.format(decoder_type=type(decoder)))
2440        else:
2441            decoder_to_use = PaginationDecoderDecorator(decoder=JsonDecoder(parameters={}))
2442        page_size_option = (
2443            self._create_component_from_model(model=model.page_size_option, config=config)
2444            if model.page_size_option
2445            else None
2446        )
2447        page_token_option = (
2448            self._create_component_from_model(model=model.page_token_option, config=config)
2449            if model.page_token_option
2450            else None
2451        )
2452        pagination_strategy = self._create_component_from_model(
2453            model=model.pagination_strategy,
2454            config=config,
2455            decoder=decoder_to_use,
2456            extractor_model=extractor_model,
2457        )
2458        if cursor_used_for_stop_condition:
2459            pagination_strategy = StopConditionPaginationStrategyDecorator(
2460                pagination_strategy, CursorStopCondition(cursor_used_for_stop_condition)
2461            )
2462        paginator = DefaultPaginator(
2463            decoder=decoder_to_use,
2464            page_size_option=page_size_option,
2465            page_token_option=page_token_option,
2466            pagination_strategy=pagination_strategy,
2467            url_base=url_base,
2468            config=config,
2469            parameters=model.parameters or {},
2470        )
2471        if self._limit_pages_fetched_per_slice:
2472            return PaginatorTestReadDecorator(paginator, self._limit_pages_fetched_per_slice)
2473        return paginator
2474
2475    def create_dpath_extractor(
2476        self,
2477        model: DpathExtractorModel,
2478        config: Config,
2479        decoder: Optional[Decoder] = None,
2480        **kwargs: Any,
2481    ) -> DpathExtractor:
2482        if decoder:
2483            decoder_to_use = decoder
2484        else:
2485            decoder_to_use = JsonDecoder(parameters={})
2486        model_field_path: List[Union[InterpolatedString, str]] = [x for x in model.field_path]
2487
2488        record_expander = None
2489        if model.record_expander:
2490            record_expander = self._create_component_from_model(
2491                model=model.record_expander,
2492                config=config,
2493            )
2494
2495        return DpathExtractor(
2496            decoder=decoder_to_use,
2497            field_path=model_field_path,
2498            config=config,
2499            parameters=model.parameters or {},
2500            record_expander=record_expander,
2501        )
2502
2503    def create_record_expander(
2504        self,
2505        model: RecordExpanderModel,
2506        config: Config,
2507        **kwargs: Any,
2508    ) -> RecordExpander:
2509        truncated_list_retriever = None
2510        suppress_incomplete_fetch_warning = False
2511        if model.truncated_list_retriever:
2512            retriever_model = model.truncated_list_retriever
2513            name = "record_expander_truncated_list"
2514            # `CustomRetriever` allows extra fields, so read from the dumped model to cover both types.
2515            retriever_fields = retriever_model.dict()
2516            for unsupported_option in ("partition_router", "pagination_reset"):
2517                if retriever_fields.get(unsupported_option):
2518                    raise ValueError(
2519                        f"`{unsupported_option}` is not supported on `truncated_list_retriever`."
2520                    )
2521            log_formatter = lambda response: format_http_message(
2522                response,
2523                f"Record expander '{name}' request",
2524                "Request performed in order to fetch the complete nested list of a truncated record.",
2525                name,
2526                is_auxiliary=True,
2527            )
2528            # `name`/`primary_key`/`transformations` are also what a `CustomRetriever` forwards to its
2529            # nested `requester`/`record_selector`, so they are passed for both retriever types.
2530            truncated_list_retriever = self._create_component_from_model(
2531                model=retriever_model,
2532                config=config,
2533                name=name,
2534                primary_key=None,
2535                transformations=[],
2536                log_formatter=log_formatter,
2537            )
2538            # Only a capped paginator makes a shortfall expected; `NoPagination` and retrievers
2539            # without a paginator are never capped.
2540            suppress_incomplete_fetch_warning = isinstance(
2541                truncated_list_retriever, SimpleRetriever
2542            ) and isinstance(truncated_list_retriever.paginator, PaginatorTestReadDecorator)
2543        return RecordExpander(
2544            expand_records_from_field=model.expand_records_from_field,
2545            config=config,
2546            parameters=model.parameters or {},
2547            remain_original_record=model.remain_original_record or False,
2548            on_no_records=OnNoRecords(model.on_no_records.value)
2549            if model.on_no_records
2550            else OnNoRecords.skip,
2551            truncation_indicator_path=model.truncation_indicator_path,
2552            truncated_list_retriever=truncated_list_retriever,
2553            message_repository=self._message_repository,
2554            suppress_incomplete_fetch_warning=suppress_incomplete_fetch_warning,
2555        )
2556
2557    @staticmethod
2558    def create_response_to_file_extractor(
2559        model: ResponseToFileExtractorModel,
2560        **kwargs: Any,
2561    ) -> ResponseToFileExtractor:
2562        return ResponseToFileExtractor(
2563            parameters=model.parameters or {},
2564            preserve_na_values=model.preserve_na_values or False,
2565        )
2566
2567    @staticmethod
2568    def create_exponential_backoff_strategy(
2569        model: ExponentialBackoffStrategyModel, config: Config
2570    ) -> ExponentialBackoffStrategy:
2571        ModelToComponentFactory._validate_jitter_range(model.jitter_range_in_seconds)
2572        return ExponentialBackoffStrategy(
2573            factor=model.factor or 5,
2574            jitter_range_in_seconds=model.jitter_range_in_seconds,
2575            parameters=model.parameters or {},
2576            config=config,
2577        )
2578
2579    @staticmethod
2580    def create_group_by_key(model: GroupByKeyMergeStrategyModel, config: Config) -> GroupByKey:
2581        return GroupByKey(model.key, config=config, parameters=model.parameters or {})
2582
2583    def create_http_requester(
2584        self,
2585        model: HttpRequesterModel,
2586        config: Config,
2587        decoder: Decoder = JsonDecoder(parameters={}),
2588        query_properties_key: Optional[str] = None,
2589        use_cache: Optional[bool] = None,
2590        *,
2591        name: str,
2592    ) -> HttpRequester:
2593        authenticator = (
2594            self._create_component_from_model(
2595                model=model.authenticator,
2596                config=config,
2597                url_base=model.url or model.url_base,
2598                name=name,
2599                decoder=decoder,
2600            )
2601            if model.authenticator
2602            else None
2603        )
2604        error_handler = (
2605            self._create_component_from_model(model=model.error_handler, config=config)
2606            if model.error_handler
2607            else DefaultErrorHandler(
2608                backoff_strategies=[],
2609                response_filters=[],
2610                config=config,
2611                parameters=model.parameters or {},
2612            )
2613        )
2614
2615        api_budget = self._api_budget
2616
2617        request_options_provider = InterpolatedRequestOptionsProvider(
2618            request_body=model.request_body,
2619            request_body_data=model.request_body_data,
2620            request_body_json=model.request_body_json,
2621            request_headers=model.request_headers,
2622            request_parameters=model.request_parameters,  # type: ignore  # QueryProperties have been removed in `create_simple_retriever`
2623            query_properties_key=query_properties_key,
2624            config=config,
2625            parameters=model.parameters or {},
2626        )
2627
2628        assert model.use_cache is not None  # for mypy
2629        assert model.http_method is not None  # for mypy
2630
2631        should_use_cache = (model.use_cache or bool(use_cache)) and not self._disable_cache
2632
2633        return HttpRequester(
2634            name=name,
2635            url=model.url,
2636            url_base=model.url_base,
2637            path=model.path,
2638            authenticator=authenticator,
2639            error_handler=error_handler,
2640            api_budget=api_budget,
2641            http_method=HttpMethod[model.http_method.value],
2642            request_options_provider=request_options_provider,
2643            config=config,
2644            disable_retries=self._disable_retries,
2645            parameters=model.parameters or {},
2646            message_repository=self._message_repository,
2647            use_cache=should_use_cache,
2648            decoder=decoder,
2649            stream_response=decoder.is_stream_response() if decoder else False,
2650        )
2651
2652    @staticmethod
2653    def create_http_response_filter(
2654        model: HttpResponseFilterModel, config: Config, **kwargs: Any
2655    ) -> HttpResponseFilter:
2656        if model.action:
2657            action = ResponseAction(model.action.value)
2658        else:
2659            action = None
2660
2661        failure_type = FailureType(model.failure_type.value) if model.failure_type else None
2662
2663        http_codes = (
2664            set(model.http_codes) if model.http_codes else set()
2665        )  # JSON schema notation has no set data type. The schema enforces an array of unique elements
2666
2667        return HttpResponseFilter(
2668            action=action,
2669            failure_type=failure_type,
2670            error_message=model.error_message or "",
2671            error_message_contains=model.error_message_contains or "",
2672            http_codes=http_codes,
2673            predicate=model.predicate or "",
2674            config=config,
2675            parameters=model.parameters or {},
2676        )
2677
2678    @staticmethod
2679    def create_inline_schema_loader(
2680        model: InlineSchemaLoaderModel, config: Config, **kwargs: Any
2681    ) -> InlineSchemaLoader:
2682        return InlineSchemaLoader(schema=model.schema_ or {}, parameters={})
2683
2684    def create_complex_field_type(
2685        self, model: ComplexFieldTypeModel, config: Config, **kwargs: Any
2686    ) -> ComplexFieldType:
2687        items = (
2688            self._create_component_from_model(model=model.items, config=config)
2689            if isinstance(model.items, ComplexFieldTypeModel)
2690            else model.items
2691        )
2692
2693        return ComplexFieldType(field_type=model.field_type, items=items)
2694
2695    def create_types_map(self, model: TypesMapModel, config: Config, **kwargs: Any) -> TypesMap:
2696        target_type = (
2697            self._create_component_from_model(model=model.target_type, config=config)
2698            if isinstance(model.target_type, ComplexFieldTypeModel)
2699            else model.target_type
2700        )
2701
2702        return TypesMap(
2703            target_type=target_type,
2704            current_type=model.current_type,
2705            condition=model.condition if model.condition is not None else "True",
2706        )
2707
2708    def create_schema_type_identifier(
2709        self, model: SchemaTypeIdentifierModel, config: Config, **kwargs: Any
2710    ) -> SchemaTypeIdentifier:
2711        types_mapping = []
2712        if model.types_mapping:
2713            types_mapping.extend(
2714                [
2715                    self._create_component_from_model(types_map, config=config)
2716                    for types_map in model.types_mapping
2717                ]
2718            )
2719        model_schema_pointer: List[Union[InterpolatedString, str]] = (
2720            [x for x in model.schema_pointer] if model.schema_pointer else []
2721        )
2722        model_key_pointer: List[Union[InterpolatedString, str]] = [x for x in model.key_pointer]
2723        model_type_pointer: Optional[List[Union[InterpolatedString, str]]] = (
2724            [x for x in model.type_pointer] if model.type_pointer else None
2725        )
2726
2727        return SchemaTypeIdentifier(
2728            schema_pointer=model_schema_pointer,
2729            key_pointer=model_key_pointer,
2730            type_pointer=model_type_pointer,
2731            types_mapping=types_mapping,
2732            parameters=model.parameters or {},
2733        )
2734
2735    def create_dynamic_schema_loader(
2736        self, model: DynamicSchemaLoaderModel, config: Config, **kwargs: Any
2737    ) -> DynamicSchemaLoader:
2738        schema_transformations = []
2739        if model.schema_transformations:
2740            for transformation_model in model.schema_transformations:
2741                schema_transformations.append(
2742                    self._create_component_from_model(model=transformation_model, config=config)
2743                )
2744        name = "dynamic_properties"
2745        retriever = self._create_component_from_model(
2746            model=model.retriever,
2747            config=config,
2748            name=name,
2749            primary_key=None,
2750            partition_router=self._build_stream_slicer_from_partition_router(
2751                model.retriever, config
2752            ),
2753            transformations=[],
2754            use_cache=True,
2755            log_formatter=(
2756                lambda response: format_http_message(
2757                    response,
2758                    f"Schema loader '{name}' request",
2759                    f"Request performed in order to extract schema.",
2760                    name,
2761                    is_auxiliary=True,
2762                )
2763            ),
2764        )
2765        schema_type_identifier = self._create_component_from_model(
2766            model.schema_type_identifier, config=config, parameters=model.parameters or {}
2767        )
2768        schema_filter = (
2769            self._create_component_from_model(
2770                model.schema_filter, config=config, parameters=model.parameters or {}
2771            )
2772            if model.schema_filter is not None
2773            else None
2774        )
2775
2776        return DynamicSchemaLoader(
2777            retriever=retriever,
2778            config=config,
2779            schema_transformations=schema_transformations,
2780            schema_filter=schema_filter,
2781            schema_type_identifier=schema_type_identifier,
2782            parameters=model.parameters or {},
2783        )
2784
2785    @staticmethod
2786    def create_json_decoder(model: JsonDecoderModel, config: Config, **kwargs: Any) -> Decoder:
2787        return JsonDecoder(parameters={})
2788
2789    def create_csv_decoder(self, model: CsvDecoderModel, config: Config, **kwargs: Any) -> Decoder:
2790        return CompositeRawDecoder(
2791            parser=ModelToComponentFactory._get_parser(model, config),
2792            stream_response=False if self._emit_connector_builder_messages else True,
2793        )
2794
2795    def create_jsonl_decoder(
2796        self, model: JsonlDecoderModel, config: Config, **kwargs: Any
2797    ) -> Decoder:
2798        return CompositeRawDecoder(
2799            parser=ModelToComponentFactory._get_parser(model, config),
2800            stream_response=False if self._emit_connector_builder_messages else True,
2801        )
2802
2803    def create_json_items_decoder(
2804        self, model: JsonItemsDecoderModel, config: Config, **kwargs: Any
2805    ) -> Decoder:
2806        return CompositeRawDecoder(
2807            parser=ModelToComponentFactory._get_parser(model, config),
2808            stream_response=False if self._emit_connector_builder_messages else True,
2809        )
2810
2811    def create_gzip_decoder(
2812        self, model: GzipDecoderModel, config: Config, **kwargs: Any
2813    ) -> Decoder:
2814        _compressed_response_types = {
2815            "gzip",
2816            "x-gzip",
2817            "gzip, deflate",
2818            "x-gzip, deflate",
2819            "application/zip",
2820            "application/gzip",
2821            "application/x-gzip",
2822            "application/x-zip-compressed",
2823        }
2824
2825        gzip_parser: GzipParser = ModelToComponentFactory._get_parser(model, config)  # type: ignore  # based on the model, we know this will be a GzipParser
2826
2827        if self._emit_connector_builder_messages:
2828            return CompositeRawDecoder(gzip_parser, False)
2829
2830        transport_gzip_parser = GzipParser(inner_parser=gzip_parser)
2831        return CompositeRawDecoder.by_headers(
2832            [
2833                ({"Content-Encoding"}, {"gzip"}, transport_gzip_parser),
2834                ({"Content-Type"}, _compressed_response_types, gzip_parser),
2835            ],
2836            stream_response=True,
2837            fallback_parser=gzip_parser,
2838        )
2839
2840    @staticmethod
2841    def create_iterable_decoder(
2842        model: IterableDecoderModel, config: Config, **kwargs: Any
2843    ) -> IterableDecoder:
2844        return IterableDecoder(parameters={})
2845
2846    @staticmethod
2847    def create_xml_decoder(model: XmlDecoderModel, config: Config, **kwargs: Any) -> XmlDecoder:
2848        return XmlDecoder(parameters={})
2849
2850    def create_zipfile_decoder(
2851        self, model: ZipfileDecoderModel, config: Config, **kwargs: Any
2852    ) -> ZipfileDecoder:
2853        return ZipfileDecoder(parser=ModelToComponentFactory._get_parser(model.decoder, config))
2854
2855    @staticmethod
2856    def _get_parser(model: BaseModel, config: Config) -> Parser:
2857        if isinstance(model, JsonDecoderModel):
2858            # Note that the logic is a bit different from the JsonDecoder as there is some legacy that is maintained to return {} on error cases
2859            return JsonParser()
2860        elif isinstance(model, JsonItemsDecoderModel):
2861            return JsonItemsParser(
2862                items_path=model.items_path,
2863                encoding=model.encoding,
2864            )
2865        elif isinstance(model, JsonlDecoderModel):
2866            return JsonLineParser()
2867        elif isinstance(model, CsvDecoderModel):
2868            return CsvParser(
2869                encoding=model.encoding,
2870                delimiter=model.delimiter,
2871                set_values_to_none=model.set_values_to_none,
2872            )
2873        elif isinstance(model, GzipDecoderModel):
2874            return GzipParser(
2875                inner_parser=ModelToComponentFactory._get_parser(model.decoder, config)
2876            )
2877        elif isinstance(
2878            model, (CustomDecoderModel, IterableDecoderModel, XmlDecoderModel, ZipfileDecoderModel)
2879        ):
2880            raise ValueError(f"Decoder type {model} does not have parser associated to it")
2881
2882        raise ValueError(f"Unknown decoder type {model}")
2883
2884    @staticmethod
2885    def create_json_file_schema_loader(
2886        model: JsonFileSchemaLoaderModel, config: Config, **kwargs: Any
2887    ) -> JsonFileSchemaLoader:
2888        return JsonFileSchemaLoader(
2889            file_path=model.file_path or "", config=config, parameters=model.parameters or {}
2890        )
2891
2892    def create_jwt_authenticator(
2893        self, model: JwtAuthenticatorModel, config: Config, **kwargs: Any
2894    ) -> JwtAuthenticator:
2895        jwt_headers = model.jwt_headers or JwtHeadersModel(kid=None, typ="JWT", cty=None)
2896        jwt_payload = model.jwt_payload or JwtPayloadModel(iss=None, sub=None, aud=None)
2897        request_option = (
2898            self._create_component_from_model(model.request_option, config)
2899            if model.request_option
2900            else None
2901        )
2902        return JwtAuthenticator(
2903            config=config,
2904            parameters=model.parameters or {},
2905            algorithm=JwtAlgorithm(model.algorithm.value),
2906            secret_key=model.secret_key,
2907            base64_encode_secret_key=model.base64_encode_secret_key,
2908            token_duration=model.token_duration,
2909            header_prefix=model.header_prefix,
2910            kid=jwt_headers.kid,
2911            typ=jwt_headers.typ,
2912            cty=jwt_headers.cty,
2913            iss=jwt_payload.iss,
2914            sub=jwt_payload.sub,
2915            aud=jwt_payload.aud,
2916            additional_jwt_headers=model.additional_jwt_headers,
2917            additional_jwt_payload=model.additional_jwt_payload,
2918            passphrase=model.passphrase,
2919            request_option=request_option,
2920        )
2921
2922    def create_list_partition_router(
2923        self, model: ListPartitionRouterModel, config: Config, **kwargs: Any
2924    ) -> ListPartitionRouter:
2925        request_option = (
2926            self._create_component_from_model(model.request_option, config)
2927            if model.request_option
2928            else None
2929        )
2930        return ListPartitionRouter(
2931            cursor_field=model.cursor_field,
2932            request_option=request_option,
2933            values=model.values,
2934            config=config,
2935            parameters=model.parameters or {},
2936        )
2937
2938    @staticmethod
2939    def create_min_max_datetime(
2940        model: MinMaxDatetimeModel, config: Config, **kwargs: Any
2941    ) -> MinMaxDatetime:
2942        return MinMaxDatetime(
2943            datetime=model.datetime,
2944            datetime_format=model.datetime_format or "",
2945            max_datetime=model.max_datetime or "",
2946            min_datetime=model.min_datetime or "",
2947            parameters=model.parameters or {},
2948        )
2949
2950    @staticmethod
2951    def create_no_auth(model: NoAuthModel, config: Config, **kwargs: Any) -> NoAuth:
2952        return NoAuth(parameters=model.parameters or {})
2953
2954    @staticmethod
2955    def create_no_pagination(
2956        model: NoPaginationModel, config: Config, **kwargs: Any
2957    ) -> NoPagination:
2958        return NoPagination(parameters={})
2959
2960    def create_oauth_authenticator(
2961        self, model: OAuthAuthenticatorModel, config: Config, **kwargs: Any
2962    ) -> DeclarativeOauth2Authenticator:
2963        profile_assertion = (
2964            self._create_component_from_model(model.profile_assertion, config=config)
2965            if model.profile_assertion
2966            else None
2967        )
2968
2969        refresh_token_error_status_codes, refresh_token_error_key, refresh_token_error_values = (
2970            self._get_refresh_token_error_information(model)
2971        )
2972        if model.refresh_token_updater:
2973            # ignore type error because fixing it would have a lot of dependencies, revisit later
2974            return DeclarativeSingleUseRefreshTokenOauth2Authenticator(  # type: ignore
2975                config,
2976                InterpolatedString.create(
2977                    model.token_refresh_endpoint,  # type: ignore
2978                    parameters=model.parameters or {},
2979                ).eval(config),
2980                access_token_name=InterpolatedString.create(
2981                    model.access_token_name or "access_token", parameters=model.parameters or {}
2982                ).eval(config),
2983                refresh_token_name=model.refresh_token_updater.refresh_token_name,
2984                expires_in_name=InterpolatedString.create(
2985                    model.expires_in_name or "expires_in", parameters=model.parameters or {}
2986                ).eval(config),
2987                client_id_name=InterpolatedString.create(
2988                    model.client_id_name or "client_id", parameters=model.parameters or {}
2989                ).eval(config),
2990                client_id=InterpolatedString.create(
2991                    model.client_id, parameters=model.parameters or {}
2992                ).eval(config)
2993                if model.client_id
2994                else model.client_id,
2995                client_secret_name=InterpolatedString.create(
2996                    model.client_secret_name or "client_secret", parameters=model.parameters or {}
2997                ).eval(config),
2998                client_secret=InterpolatedString.create(
2999                    model.client_secret, parameters=model.parameters or {}
3000                ).eval(config)
3001                if model.client_secret
3002                else model.client_secret,
3003                access_token_config_path=model.refresh_token_updater.access_token_config_path,
3004                refresh_token_config_path=model.refresh_token_updater.refresh_token_config_path,
3005                token_expiry_date_config_path=model.refresh_token_updater.token_expiry_date_config_path,
3006                grant_type_name=InterpolatedString.create(
3007                    model.grant_type_name or "grant_type", parameters=model.parameters or {}
3008                ).eval(config),
3009                grant_type=InterpolatedString.create(
3010                    model.grant_type or "refresh_token", parameters=model.parameters or {}
3011                ).eval(config),
3012                refresh_request_body=InterpolatedMapping(
3013                    model.refresh_request_body or {}, parameters=model.parameters or {}
3014                ).eval(config),
3015                refresh_request_headers=InterpolatedMapping(
3016                    model.refresh_request_headers or {}, parameters=model.parameters or {}
3017                ).eval(config),
3018                send_refresh_request_as_query_params=bool(
3019                    model.send_refresh_request_as_query_params
3020                ),
3021                scopes=model.scopes,
3022                token_expiry_date_format=model.token_expiry_date_format,
3023                token_expiry_is_time_of_expiration=bool(model.token_expiry_date_format),
3024                message_repository=self._message_repository,
3025                refresh_token_error_status_codes=refresh_token_error_status_codes,
3026                refresh_token_error_key=refresh_token_error_key,
3027                refresh_token_error_values=refresh_token_error_values,
3028            )
3029        # ignore type error because fixing it would have a lot of dependencies, revisit later
3030        return DeclarativeOauth2Authenticator(  # type: ignore
3031            access_token_name=model.access_token_name or "access_token",
3032            access_token_value=model.access_token_value,
3033            client_id_name=model.client_id_name or "client_id",
3034            client_id=model.client_id,
3035            client_secret_name=model.client_secret_name or "client_secret",
3036            client_secret=model.client_secret,
3037            expires_in_name=model.expires_in_name or "expires_in",
3038            grant_type_name=model.grant_type_name or "grant_type",
3039            grant_type=model.grant_type or "refresh_token",
3040            refresh_request_body=model.refresh_request_body,
3041            refresh_request_headers=model.refresh_request_headers,
3042            send_refresh_request_as_query_params=bool(model.send_refresh_request_as_query_params),
3043            refresh_token_name=model.refresh_token_name or "refresh_token",
3044            refresh_token=model.refresh_token,
3045            scopes=model.scopes,
3046            token_expiry_date=model.token_expiry_date,
3047            token_expiry_date_format=model.token_expiry_date_format,
3048            token_expiry_is_time_of_expiration=bool(model.token_expiry_date_format),
3049            token_refresh_endpoint=model.token_refresh_endpoint,
3050            config=config,
3051            parameters=model.parameters or {},
3052            message_repository=self._message_repository,
3053            profile_assertion=profile_assertion,
3054            use_profile_assertion=model.use_profile_assertion,
3055            refresh_token_error_status_codes=refresh_token_error_status_codes,
3056            refresh_token_error_key=refresh_token_error_key,
3057            refresh_token_error_values=refresh_token_error_values,
3058        )
3059
3060    @staticmethod
3061    def _get_refresh_token_error_information(
3062        model: OAuthAuthenticatorModel,
3063    ) -> Tuple[Tuple[int, ...], str, Tuple[str, ...]]:
3064        """
3065        In a previous version of the CDK, the auth error as config_error was only done if a refresh token updater was
3066        defined. As a transition, we added those fields on the OAuthAuthenticatorModel. This method ensures that the
3067        information is defined only once and return the right fields.
3068        """
3069        refresh_token_updater = model.refresh_token_updater
3070        is_defined_on_refresh_token_updated = refresh_token_updater and (
3071            refresh_token_updater.refresh_token_error_status_codes
3072            or refresh_token_updater.refresh_token_error_key
3073            or refresh_token_updater.refresh_token_error_values
3074        )
3075        is_defined_on_oauth_authenticator = (
3076            model.refresh_token_error_status_codes
3077            or model.refresh_token_error_key
3078            or model.refresh_token_error_values
3079        )
3080        if is_defined_on_refresh_token_updated and is_defined_on_oauth_authenticator:
3081            raise ValueError(
3082                "refresh_token_error should either be defined on the OAuthAuthenticatorModel or the RefreshTokenUpdaterModel, not both"
3083            )
3084
3085        if is_defined_on_refresh_token_updated:
3086            not_optional_refresh_token_updater: RefreshTokenUpdaterModel = refresh_token_updater  # type: ignore  # we know from the condition that this is not None
3087            return (
3088                tuple(not_optional_refresh_token_updater.refresh_token_error_status_codes)
3089                if not_optional_refresh_token_updater.refresh_token_error_status_codes
3090                else (),
3091                not_optional_refresh_token_updater.refresh_token_error_key or "",
3092                tuple(not_optional_refresh_token_updater.refresh_token_error_values)
3093                if not_optional_refresh_token_updater.refresh_token_error_values
3094                else (),
3095            )
3096        elif is_defined_on_oauth_authenticator:
3097            return (
3098                tuple(model.refresh_token_error_status_codes)
3099                if model.refresh_token_error_status_codes
3100                else (),
3101                model.refresh_token_error_key or "",
3102                tuple(model.refresh_token_error_values) if model.refresh_token_error_values else (),
3103            )
3104
3105        # returning default values we think cover most cases
3106        return (400,), "error", ("invalid_grant", "invalid_permissions")
3107
3108    def create_offset_increment(
3109        self,
3110        model: OffsetIncrementModel,
3111        config: Config,
3112        decoder: Decoder,
3113        extractor_model: Optional[Union[CustomRecordExtractorModel, DpathExtractorModel]] = None,
3114        **kwargs: Any,
3115    ) -> OffsetIncrement:
3116        if isinstance(decoder, PaginationDecoderDecorator):
3117            inner_decoder = decoder.decoder
3118        else:
3119            inner_decoder = decoder
3120            decoder = PaginationDecoderDecorator(decoder=decoder)
3121
3122        if self._is_supported_decoder_for_pagination(inner_decoder):
3123            decoder_to_use = decoder
3124        else:
3125            raise ValueError(
3126                self._UNSUPPORTED_DECODER_ERROR.format(decoder_type=type(inner_decoder))
3127            )
3128
3129        # Ideally we would instantiate the runtime extractor from highest most level (in this case the SimpleRetriever)
3130        # so that it can be shared by OffSetIncrement and RecordSelector. However, due to how we instantiate the
3131        # decoder with various decorators here, but not in create_record_selector, it is simpler to retain existing
3132        # behavior by having two separate extractors with identical behavior since they use the same extractor model.
3133        # When we have more time to investigate we can look into reusing the same component.
3134        extractor = (
3135            self._create_component_from_model(
3136                model=extractor_model, config=config, decoder=decoder_to_use
3137            )
3138            if extractor_model
3139            else None
3140        )
3141
3142        # Pydantic v1 Union type coercion can convert int to string depending on Union order.
3143        # If page_size is a string that represents an integer (not an interpolation), convert it back.
3144        page_size = model.page_size
3145        if isinstance(page_size, str) and page_size.isdigit():
3146            page_size = int(page_size)
3147
3148        return OffsetIncrement(
3149            page_size=page_size,
3150            config=config,
3151            decoder=decoder_to_use,
3152            extractor=extractor,
3153            inject_on_first_request=model.inject_on_first_request or False,
3154            parameters=model.parameters or {},
3155        )
3156
3157    def create_page_increment(
3158        self,
3159        model: PageIncrementModel,
3160        config: Config,
3161        decoder: Optional[Decoder] = None,
3162        extractor_model: Optional[Union[CustomRecordExtractorModel, DpathExtractorModel]] = None,
3163        **kwargs: Any,
3164    ) -> PageIncrement:
3165        # Like OffsetIncrement, we instantiate a separate extractor with identical behavior to the
3166        # RecordSelector's so the strategy can count the raw records in the response. This ensures
3167        # pagination is driven by the API's page size, not the post-filter record count.
3168        extractor = (
3169            self._create_component_from_model(model=extractor_model, config=config, decoder=decoder)
3170            if extractor_model
3171            else None
3172        )
3173
3174        # Pydantic v1 Union type coercion can convert int to string depending on Union order.
3175        # If page_size is a string that represents an integer (not an interpolation), convert it back.
3176        page_size = model.page_size
3177        if isinstance(page_size, str) and page_size.isdigit():
3178            page_size = int(page_size)
3179
3180        return PageIncrement(
3181            page_size=page_size,
3182            config=config,
3183            start_from_page=model.start_from_page or 0,
3184            inject_on_first_request=model.inject_on_first_request or False,
3185            extractor=extractor,
3186            parameters=model.parameters or {},
3187        )
3188
3189    def create_parent_stream_config(
3190        self, model: ParentStreamConfigModel, config: Config, *, stream_name: str, **kwargs: Any
3191    ) -> ParentStreamConfig:
3192        declarative_stream = self._create_component_from_model(
3193            model.stream,
3194            config=config,
3195            is_parent=True,
3196            **kwargs,
3197        )
3198        request_option = (
3199            self._create_component_from_model(model.request_option, config=config)
3200            if model.request_option
3201            else None
3202        )
3203
3204        if model.lazy_read_pointer and any("*" in pointer for pointer in model.lazy_read_pointer):
3205            raise ValueError(
3206                "The '*' wildcard in 'lazy_read_pointer' is not supported — only direct paths are allowed."
3207            )
3208
3209        model_lazy_read_pointer: List[Union[InterpolatedString, str]] = (
3210            [x for x in model.lazy_read_pointer] if model.lazy_read_pointer else []
3211        )
3212
3213        return ParentStreamConfig(
3214            parent_key=model.parent_key,
3215            request_option=request_option,
3216            stream=declarative_stream,
3217            partition_field=model.partition_field,
3218            config=config,
3219            incremental_dependency=model.incremental_dependency or False,
3220            parameters=model.parameters or {},
3221            extra_fields=model.extra_fields,
3222            lazy_read_pointer=model_lazy_read_pointer,
3223        )
3224
3225    def create_properties_from_endpoint(
3226        self, model: PropertiesFromEndpointModel, config: Config, **kwargs: Any
3227    ) -> PropertiesFromEndpoint:
3228        retriever = self._create_component_from_model(
3229            model=model.retriever,
3230            config=config,
3231            name="dynamic_properties",
3232            primary_key=None,
3233            stream_slicer=None,
3234            transformations=[],
3235            use_cache=True,  # Enable caching on the HttpRequester/HttpClient because the properties endpoint will be called for every slice being processed, and it is highly unlikely for the response to different
3236        )
3237        return PropertiesFromEndpoint(
3238            property_field_path=model.property_field_path,
3239            retriever=retriever,
3240            config=config,
3241            parameters=model.parameters or {},
3242        )
3243
3244    def create_property_chunking(
3245        self, model: PropertyChunkingModel, config: Config, **kwargs: Any
3246    ) -> PropertyChunking:
3247        record_merge_strategy = (
3248            self._create_component_from_model(
3249                model=model.record_merge_strategy, config=config, **kwargs
3250            )
3251            if model.record_merge_strategy
3252            else None
3253        )
3254
3255        property_limit_type: PropertyLimitType
3256        match model.property_limit_type:
3257            case PropertyLimitTypeModel.property_count:
3258                property_limit_type = PropertyLimitType.property_count
3259            case PropertyLimitTypeModel.characters:
3260                property_limit_type = PropertyLimitType.characters
3261            case _:
3262                raise ValueError(f"Invalid PropertyLimitType {property_limit_type}")
3263
3264        return PropertyChunking(
3265            property_limit_type=property_limit_type,
3266            property_limit=model.property_limit,
3267            record_merge_strategy=record_merge_strategy,
3268            config=config,
3269            parameters=model.parameters or {},
3270        )
3271
3272    def create_query_properties(
3273        self, model: QueryPropertiesModel, config: Config, *, stream_name: str, **kwargs: Any
3274    ) -> QueryProperties:
3275        if isinstance(model.property_list, list):
3276            property_list = model.property_list
3277        else:
3278            property_list = self._create_component_from_model(
3279                model=model.property_list, config=config, **kwargs
3280            )
3281
3282        property_chunking = (
3283            self._create_component_from_model(
3284                model=model.property_chunking, config=config, **kwargs
3285            )
3286            if model.property_chunking
3287            else None
3288        )
3289
3290        property_selector = (
3291            self._create_component_from_model(
3292                model=model.property_selector, config=config, stream_name=stream_name, **kwargs
3293            )
3294            if model.property_selector
3295            else None
3296        )
3297
3298        return QueryProperties(
3299            property_list=property_list,
3300            always_include_properties=model.always_include_properties,
3301            property_chunking=property_chunking,
3302            property_selector=property_selector,
3303            config=config,
3304            parameters=model.parameters or {},
3305        )
3306
3307    def create_json_schema_property_selector(
3308        self,
3309        model: JsonSchemaPropertySelectorModel,
3310        config: Config,
3311        *,
3312        stream_name: str,
3313        **kwargs: Any,
3314    ) -> JsonSchemaPropertySelector:
3315        configured_stream = self._stream_name_to_configured_stream.get(stream_name)
3316
3317        transformations = []
3318        if model.transformations:
3319            for transformation_model in model.transformations:
3320                transformations.append(
3321                    self._create_component_from_model(model=transformation_model, config=config)
3322                )
3323
3324        return JsonSchemaPropertySelector(
3325            configured_stream=configured_stream,
3326            properties_transformations=transformations,
3327            config=config,
3328            parameters=model.parameters or {},
3329        )
3330
3331    @staticmethod
3332    def create_record_filter(
3333        model: RecordFilterModel, config: Config, **kwargs: Any
3334    ) -> RecordFilter:
3335        return RecordFilter(
3336            condition=model.condition or "", config=config, parameters=model.parameters or {}
3337        )
3338
3339    @staticmethod
3340    def create_request_path(model: RequestPathModel, config: Config, **kwargs: Any) -> RequestPath:
3341        return RequestPath(parameters={})
3342
3343    @staticmethod
3344    def create_request_option(
3345        model: RequestOptionModel, config: Config, **kwargs: Any
3346    ) -> RequestOption:
3347        inject_into = RequestOptionType(model.inject_into.value)
3348        field_path: Optional[List[Union[InterpolatedString, str]]] = (
3349            [
3350                InterpolatedString.create(segment, parameters=kwargs.get("parameters", {}))
3351                for segment in model.field_path
3352            ]
3353            if model.field_path
3354            else None
3355        )
3356        field_name = (
3357            InterpolatedString.create(model.field_name, parameters=kwargs.get("parameters", {}))
3358            if model.field_name
3359            else None
3360        )
3361        return RequestOption(
3362            field_name=field_name,
3363            field_path=field_path,
3364            inject_into=inject_into,
3365            parameters=kwargs.get("parameters", {}),
3366        )
3367
3368    def create_record_selector(
3369        self,
3370        model: RecordSelectorModel,
3371        config: Config,
3372        *,
3373        name: str,
3374        transformations: List[RecordTransformation] | None = None,
3375        decoder: Decoder | None = None,
3376        client_side_incremental_sync_cursor: Optional[Cursor] = None,
3377        is_client_side_incremental_sync: bool = False,
3378        file_uploader: Optional[DefaultFileUploader] = None,
3379        **kwargs: Any,
3380    ) -> RecordSelector:
3381        extractor = self._create_component_from_model(
3382            model=model.extractor, decoder=decoder, config=config
3383        )
3384        record_filter = (
3385            self._create_component_from_model(model.record_filter, config=config)
3386            if model.record_filter
3387            else None
3388        )
3389
3390        # A client-side incremental stream transforms before filtering by default. That default belongs to the flag,
3391        # not to the component that ends up doing the cursor comparison: a data feed does it in the retriever and
3392        # receives no cursor here, but its `record_filter` condition must keep running after the transformations.
3393        default_transform_before_filtering = bool(
3394            client_side_incremental_sync_cursor or is_client_side_incremental_sync
3395        )
3396        transform_before_filtering = (
3397            default_transform_before_filtering
3398            if model.transform_before_filtering is None
3399            else model.transform_before_filtering
3400        )
3401        if client_side_incremental_sync_cursor:
3402            record_filter = ClientSideIncrementalRecordFilterDecorator(
3403                config=config,
3404                parameters=model.parameters,
3405                condition=model.record_filter.condition
3406                if (model.record_filter and hasattr(model.record_filter, "condition"))
3407                else None,
3408                cursor=client_side_incremental_sync_cursor,
3409            )
3410
3411        if model.schema_normalization is None:
3412            # default to no schema normalization if not set
3413            model.schema_normalization = SchemaNormalizationModel.None_
3414
3415        schema_normalization = (
3416            TypeTransformer(SCHEMA_TRANSFORMER_TYPE_MAPPING[model.schema_normalization])
3417            if isinstance(model.schema_normalization, SchemaNormalizationModel)
3418            else self._create_component_from_model(model.schema_normalization, config=config)  # type: ignore[arg-type] # custom normalization model expected here
3419        )
3420
3421        return RecordSelector(
3422            extractor=extractor,
3423            name=name,
3424            config=config,
3425            record_filter=record_filter,
3426            transformations=transformations or [],
3427            file_uploader=file_uploader,
3428            schema_normalization=schema_normalization,
3429            parameters=model.parameters or {},
3430            transform_before_filtering=transform_before_filtering,
3431        )
3432
3433    @staticmethod
3434    def create_remove_fields(
3435        model: RemoveFieldsModel, config: Config, **kwargs: Any
3436    ) -> RemoveFields:
3437        return RemoveFields(
3438            field_pointers=model.field_pointers, condition=model.condition or "", parameters={}
3439        )
3440
3441    def create_selective_authenticator(
3442        self, model: SelectiveAuthenticatorModel, config: Config, **kwargs: Any
3443    ) -> DeclarativeAuthenticator:
3444        authenticators = {
3445            name: self._create_component_from_model(model=auth, config=config)
3446            for name, auth in model.authenticators.items()
3447        }
3448        # SelectiveAuthenticator will return instance of DeclarativeAuthenticator or raise ValueError error
3449        return SelectiveAuthenticator(  # type: ignore[abstract]
3450            config=config,
3451            authenticators=authenticators,
3452            authenticator_selection_path=model.authenticator_selection_path,
3453            **kwargs,
3454        )
3455
3456    @staticmethod
3457    def create_legacy_session_token_authenticator(
3458        model: LegacySessionTokenAuthenticatorModel, config: Config, *, url_base: str, **kwargs: Any
3459    ) -> LegacySessionTokenAuthenticator:
3460        return LegacySessionTokenAuthenticator(
3461            api_url=url_base,
3462            header=model.header,
3463            login_url=model.login_url,
3464            password=model.password or "",
3465            session_token=model.session_token or "",
3466            session_token_response_key=model.session_token_response_key or "",
3467            username=model.username or "",
3468            validate_session_url=model.validate_session_url,
3469            config=config,
3470            parameters=model.parameters or {},
3471        )
3472
3473    def create_simple_retriever(
3474        self,
3475        model: SimpleRetrieverModel,
3476        config: Config,
3477        *,
3478        name: str,
3479        primary_key: Optional[Union[str, List[str], List[List[str]]]],
3480        request_options_provider: Optional[RequestOptionsProvider] = None,
3481        cursor: Optional[Cursor] = None,
3482        has_stop_condition_cursor: bool = False,
3483        is_client_side_incremental_sync: bool = False,
3484        transformations: List[RecordTransformation],
3485        file_uploader: Optional[DefaultFileUploader] = None,
3486        incremental_sync: Optional[
3487            Union[IncrementingCountCursorModel, DatetimeBasedCursorModel]
3488        ] = None,
3489        use_cache: Optional[bool] = None,
3490        log_formatter: Optional[Callable[[Response], Any]] = None,
3491        partition_router: Optional[PartitionRouter] = None,
3492        **kwargs: Any,
3493    ) -> SimpleRetriever:
3494        def _get_url(req: Requester) -> str:
3495            """
3496            Closure to get the URL from the requester. This is used to get the URL in the case of a lazy retriever.
3497            This is needed because the URL is not set until the requester is created.
3498            """
3499
3500            _url: str = (
3501                model.requester.url
3502                if hasattr(model.requester, "url") and model.requester.url is not None
3503                else req.get_url(stream_state=None, stream_slice=None, next_page_token=None)
3504            )
3505            _url_base: str = (
3506                model.requester.url_base
3507                if hasattr(model.requester, "url_base") and model.requester.url_base is not None
3508                else req.get_url_base(stream_state=None, stream_slice=None, next_page_token=None)
3509            )
3510
3511            return _url or _url_base
3512
3513        if cursor is None:
3514            cursor = FinalStateCursor(name, None, self._message_repository)
3515
3516        # A data feed drops the records the cursor considers already synced in the retriever, which sits downstream of
3517        # the paginator. Letting the record selector drop them as well would be redundant and would hide them from the
3518        # pagination stop condition, so a data feed never delegates that filtering to the record selector, whether
3519        # `is_client_side_incremental` is set or not. The `condition` from `record_filter` is intentionally left out of
3520        # the post-pagination filter and stays in the record selector, which preserves the existing behaviour: the
3521        # selector runs inside the page loop, so the records the condition rejects never reach the paginator's
3522        # accounting. Moving it downstream would start counting them.
3523        post_pagination_filter = (
3524            ClientSideIncrementalRecordFilterDecorator(
3525                config=config,
3526                parameters=model.parameters or {},
3527                condition=None,
3528                cursor=cursor,
3529            )
3530            if has_stop_condition_cursor
3531            else None
3532        )
3533        client_side_incremental_cursor = (
3534            cursor if is_client_side_incremental_sync and not post_pagination_filter else None
3535        )
3536        if post_pagination_filter and is_client_side_incremental_sync:
3537            LOGGER.warning(
3538                f"Stream {name}: `is_client_side_incremental` adds no record filtering when `is_data_feed` is set, "
3539                "as a data feed already filters on the cursor value. It still makes the record selector apply the "
3540                "transformations before the `record_filter` condition."
3541            )
3542
3543        decoder = (
3544            self._create_component_from_model(model=model.decoder, config=config)
3545            if model.decoder
3546            else JsonDecoder(parameters={})
3547        )
3548        record_selector = self._create_component_from_model(
3549            model=model.record_selector,
3550            name=name,
3551            config=config,
3552            decoder=decoder,
3553            transformations=transformations,
3554            client_side_incremental_sync_cursor=client_side_incremental_cursor,
3555            is_client_side_incremental_sync=is_client_side_incremental_sync,
3556            file_uploader=file_uploader,
3557        )
3558
3559        query_properties: Optional[QueryProperties] = None
3560        query_properties_key: Optional[str] = None
3561        self._ensure_query_properties_to_model(model.requester)
3562        if self._has_query_properties_in_request_parameters(model.requester):
3563            # It is better to be explicit about an error if PropertiesFromEndpoint is defined in multiple
3564            # places instead of default to request_parameters which isn't clearly documented
3565            if (
3566                hasattr(model.requester, "fetch_properties_from_endpoint")
3567                and model.requester.fetch_properties_from_endpoint
3568            ):
3569                raise ValueError(
3570                    f"PropertiesFromEndpoint should only be specified once per stream, but found in {model.requester.type}.fetch_properties_from_endpoint and {model.requester.type}.request_parameters"
3571                )
3572
3573            query_properties_definitions = []
3574            for key, request_parameter in model.requester.request_parameters.items():  # type: ignore # request_parameters is already validated to be a Mapping using _has_query_properties_in_request_parameters()
3575                if isinstance(request_parameter, QueryPropertiesModel):
3576                    query_properties_key = key
3577                    query_properties_definitions.append(request_parameter)
3578
3579            if len(query_properties_definitions) > 1:
3580                raise ValueError(
3581                    f"request_parameters only supports defining one QueryProperties field, but found {len(query_properties_definitions)} usages"
3582                )
3583
3584            if len(query_properties_definitions) == 1:
3585                query_properties = self._create_component_from_model(
3586                    model=query_properties_definitions[0], stream_name=name, config=config
3587                )
3588
3589            # Removes QueryProperties components from the interpolated mappings because it has been designed
3590            # to be used by the SimpleRetriever and will be resolved from the provider from the slice directly
3591            # instead of through jinja interpolation
3592            if hasattr(model.requester, "request_parameters") and isinstance(
3593                model.requester.request_parameters, Mapping
3594            ):
3595                model.requester.request_parameters = self._remove_query_properties(
3596                    model.requester.request_parameters
3597                )
3598        elif (
3599            hasattr(model.requester, "fetch_properties_from_endpoint")
3600            and model.requester.fetch_properties_from_endpoint
3601        ):
3602            # todo: Deprecate this condition once dependent connectors migrate to query_properties
3603            query_properties_definition = QueryPropertiesModel(
3604                type="QueryProperties",
3605                property_list=model.requester.fetch_properties_from_endpoint,
3606                always_include_properties=None,
3607                property_chunking=None,
3608            )  # type: ignore # $parameters has a default value
3609
3610            query_properties = self.create_query_properties(
3611                model=query_properties_definition,
3612                stream_name=name,
3613                config=config,
3614            )
3615        elif hasattr(model.requester, "query_properties") and model.requester.query_properties:
3616            query_properties = self.create_query_properties(
3617                model=model.requester.query_properties,
3618                stream_name=name,
3619                config=config,
3620            )
3621
3622        requester = self._create_component_from_model(
3623            model=model.requester,
3624            decoder=decoder,
3625            name=name,
3626            query_properties_key=query_properties_key,
3627            use_cache=use_cache,
3628            config=config,
3629        )
3630
3631        if not request_options_provider:
3632            request_options_provider = DefaultRequestOptionsProvider(parameters={})
3633        if isinstance(request_options_provider, DefaultRequestOptionsProvider) and isinstance(
3634            partition_router, PartitionRouter
3635        ):
3636            request_options_provider = partition_router
3637
3638        paginator = (
3639            self._create_component_from_model(
3640                model=model.paginator,
3641                config=config,
3642                url_base=_get_url(requester),
3643                extractor_model=model.record_selector.extractor,
3644                decoder=decoder,
3645                cursor_used_for_stop_condition=cursor if has_stop_condition_cursor else None,
3646            )
3647            if model.paginator
3648            else NoPagination(parameters={})
3649        )
3650
3651        ignore_stream_slicer_parameters_on_paginated_requests = (
3652            model.ignore_stream_slicer_parameters_on_paginated_requests or False
3653        )
3654
3655        if (
3656            model.partition_router
3657            and isinstance(model.partition_router, SubstreamPartitionRouterModel)
3658            and not bool(self._connector_state_manager.get_stream_state(name, None))
3659            and any(
3660                parent_stream_config.lazy_read_pointer
3661                for parent_stream_config in model.partition_router.parent_stream_configs
3662            )
3663        ):
3664            if incremental_sync:
3665                if incremental_sync.type != "DatetimeBasedCursor":
3666                    raise ValueError(
3667                        f"LazySimpleRetriever only supports DatetimeBasedCursor. Found: {incremental_sync.type}."
3668                    )
3669
3670                elif incremental_sync.step or incremental_sync.cursor_granularity:
3671                    raise ValueError(
3672                        f"Found more that one slice per parent. LazySimpleRetriever only supports single slice read for stream - {name}."
3673                    )
3674
3675            if model.decoder and model.decoder.type != "JsonDecoder":
3676                raise ValueError(
3677                    f"LazySimpleRetriever only supports JsonDecoder. Found: {model.decoder.type}."
3678                )
3679
3680            return LazySimpleRetriever(
3681                name=name,
3682                paginator=paginator,
3683                primary_key=primary_key,
3684                requester=requester,
3685                record_selector=record_selector,
3686                stream_slicer=_NO_STREAM_SLICING,
3687                request_option_provider=request_options_provider,
3688                config=config,
3689                ignore_stream_slicer_parameters_on_paginated_requests=ignore_stream_slicer_parameters_on_paginated_requests,
3690                post_pagination_filter=post_pagination_filter,
3691                parameters=model.parameters or {},
3692            )
3693
3694        if (
3695            model.record_selector.record_filter
3696            and model.pagination_reset
3697            and model.pagination_reset.limits
3698        ):
3699            raise ValueError("PaginationResetLimits are not supported while having record filter.")
3700
3701        return SimpleRetriever(
3702            name=name,
3703            paginator=paginator,
3704            primary_key=primary_key,
3705            requester=requester,
3706            record_selector=record_selector,
3707            stream_slicer=_NO_STREAM_SLICING,
3708            request_option_provider=request_options_provider,
3709            config=config,
3710            ignore_stream_slicer_parameters_on_paginated_requests=ignore_stream_slicer_parameters_on_paginated_requests,
3711            additional_query_properties=query_properties,
3712            log_formatter=self._get_log_formatter(log_formatter, name),
3713            pagination_tracker_factory=self._create_pagination_tracker_factory(
3714                model.pagination_reset, cursor
3715            ),
3716            post_pagination_filter=post_pagination_filter,
3717            parameters=model.parameters or {},
3718        )
3719
3720    def _create_pagination_tracker_factory(
3721        self, model: Optional[PaginationResetModel], cursor: Cursor
3722    ) -> Callable[[], PaginationTracker]:
3723        if model is None:
3724            return lambda: PaginationTracker()
3725
3726        # Until we figure out a way to use any cursor for PaginationTracker, we will have to have this cursor selector logic
3727        cursor_factory: Callable[[], Optional[ConcurrentCursor]] = lambda: None
3728        if model.action == PaginationResetActionModel.RESET:
3729            # in that case, we will let cursor_factory to return None even if the stream has a cursor
3730            pass
3731        elif model.action == PaginationResetActionModel.SPLIT_USING_CURSOR:
3732            if isinstance(cursor, ConcurrentCursor):
3733                cursor_factory = lambda: cursor.copy_without_state()  # type: ignore  # the if condition validates that it is a ConcurrentCursor
3734            elif isinstance(cursor, ConcurrentPerPartitionCursor):
3735                cursor_factory = lambda: cursor._cursor_factory.create(  # type: ignore  # if this becomes a problem, we would need to extract the cursor_factory instantiation logic and make it accessible here
3736                    {}, datetime.timedelta(0)
3737                )
3738            elif not isinstance(cursor, FinalStateCursor):
3739                LOGGER.warning(
3740                    "Unknown cursor for PaginationTracker. Pagination resets might not work properly"
3741                )
3742        else:
3743            raise ValueError(f"Unknown PaginationReset action: {model.action}")
3744
3745        limit = model.limits.number_of_records if model and model.limits else None
3746        return lambda: PaginationTracker(cursor_factory(), limit)
3747
3748    def _get_log_formatter(
3749        self, log_formatter: Callable[[Response], Any] | None, name: str
3750    ) -> Callable[[Response], Any] | None:
3751        if self._should_limit_slices_fetched():
3752            return (
3753                (
3754                    lambda response: format_http_message(
3755                        response,
3756                        f"Stream '{name}' request",
3757                        f"Request performed in order to extract records for stream '{name}'",
3758                        name,
3759                    )
3760                )
3761                if not log_formatter
3762                else log_formatter
3763            )
3764        return None
3765
3766    def _should_limit_slices_fetched(self) -> bool:
3767        """
3768        Returns True if the number of slices fetched should be limited, False otherwise.
3769        This is used to limit the number of slices fetched during tests.
3770        """
3771        return bool(self._limit_slices_fetched or self._emit_connector_builder_messages)
3772
3773    @staticmethod
3774    def _has_query_properties_in_request_parameters(
3775        requester: Union[HttpRequesterModel, CustomRequesterModel],
3776    ) -> bool:
3777        if not hasattr(requester, "request_parameters"):
3778            return False
3779        request_parameters = requester.request_parameters
3780        if request_parameters and isinstance(request_parameters, Mapping):
3781            for request_parameter in request_parameters.values():
3782                if isinstance(request_parameter, QueryPropertiesModel):
3783                    return True
3784        return False
3785
3786    @staticmethod
3787    def _remove_query_properties(
3788        request_parameters: Mapping[str, Union[str, QueryPropertiesModel]],
3789    ) -> Mapping[str, str]:
3790        return {
3791            parameter_field: request_parameter
3792            for parameter_field, request_parameter in request_parameters.items()
3793            if not isinstance(request_parameter, QueryPropertiesModel)
3794        }
3795
3796    def create_state_delegating_stream(
3797        self,
3798        model: StateDelegatingStreamModel,
3799        config: Config,
3800        **kwargs: Any,
3801    ) -> DefaultStream:
3802        if (
3803            model.full_refresh_stream.name != model.name
3804            or model.name != model.incremental_stream.name
3805        ):
3806            raise ValueError(
3807                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}."
3808            )
3809
3810        # Resolve api_retention_period with config context (supports Jinja2 interpolation)
3811        resolved_retention_period: Optional[str] = None
3812        if model.api_retention_period:
3813            interpolated_retention = InterpolatedString.create(
3814                model.api_retention_period, parameters=model.parameters or {}
3815            )
3816            resolved_value = interpolated_retention.eval(config=config)
3817            if resolved_value:
3818                resolved_retention_period = str(resolved_value)
3819
3820        if resolved_retention_period:
3821            for stream_model in (model.full_refresh_stream, model.incremental_stream):
3822                if isinstance(stream_model.incremental_sync, IncrementingCountCursorModel):
3823                    raise ValueError(
3824                        f"Stream '{model.name}' uses IncrementingCountCursor which is not supported "
3825                        f"with api_retention_period. IncrementingCountCursor does not use datetime-based "
3826                        f"cursors, so cursor age validation cannot be performed."
3827                    )
3828
3829        stream_state = self._connector_state_manager.get_stream_state(model.name, None)
3830
3831        if not stream_state:
3832            return self._create_component_from_model(  # type: ignore[no-any-return]
3833                model.full_refresh_stream, config=config, **kwargs
3834            )
3835
3836        incremental_stream: DefaultStream = self._create_component_from_model(
3837            model.incremental_stream, config=config, **kwargs
3838        )  # type: ignore[assignment]
3839
3840        # Only run cursor age validation for streams that are in the configured
3841        # catalog (or when no catalog was provided, e.g. during discover / connector
3842        # builder).  Streams not selected by the user but instantiated as parent-stream
3843        # dependencies must not go through this path because it emits state messages
3844        # that the destination does not know about, causing "Stream not found" crashes.
3845        stream_is_in_catalog = (
3846            not self._stream_name_to_configured_stream  # no catalog → validate by default
3847            or model.name in self._stream_name_to_configured_stream
3848        )
3849        if resolved_retention_period and stream_is_in_catalog:
3850            full_refresh_stream: DefaultStream = self._create_component_from_model(
3851                model.full_refresh_stream, config=config, **kwargs
3852            )  # type: ignore[assignment]
3853            if self._is_cursor_older_than_retention_period(
3854                stream_state,
3855                full_refresh_stream.cursor,
3856                incremental_stream.cursor,
3857                resolved_retention_period,
3858                model.name,
3859            ):
3860                # Clear state BEFORE constructing the full_refresh_stream so that
3861                # its cursor starts from start_date instead of the stale cursor.
3862                self._connector_state_manager.update_state_for_stream(model.name, None, {})
3863                state_message = self._connector_state_manager.create_state_message(model.name, None)
3864                self._message_repository.emit_message(state_message)
3865                return self._create_component_from_model(  # type: ignore[no-any-return]
3866                    model.full_refresh_stream, config=config, **kwargs
3867                )
3868
3869        return incremental_stream
3870
3871    @staticmethod
3872    def _is_cursor_older_than_retention_period(
3873        stream_state: Mapping[str, Any],
3874        full_refresh_cursor: Cursor,
3875        incremental_cursor: Cursor,
3876        api_retention_period: str,
3877        stream_name: str,
3878    ) -> bool:
3879        """Check if the cursor value in the state is older than the API's retention period.
3880
3881        Checks cursors in sequence: full refresh cursor first, then incremental cursor.
3882        FinalStateCursor returns now() for completed full refresh state (NO_CURSOR_STATE_KEY),
3883        which is always within retention, so we use incremental. For other states, it returns
3884        None and we fall back to checking the incremental cursor.
3885
3886        Returns True if the cursor is older than the retention period (should use full refresh).
3887        Returns False if the cursor is within the retention period (safe to use incremental).
3888        """
3889        retention_duration = parse_duration(api_retention_period)
3890        retention_cutoff = datetime.datetime.now(datetime.timezone.utc) - retention_duration
3891
3892        # Check full refresh cursor first
3893        cursor_datetime = full_refresh_cursor.get_cursor_datetime_from_state(stream_state)
3894
3895        # If full refresh cursor returns None, check incremental cursor
3896        if cursor_datetime is None:
3897            cursor_datetime = incremental_cursor.get_cursor_datetime_from_state(stream_state)
3898
3899        if cursor_datetime is None:
3900            # Neither cursor could parse the state - fall back to full refresh to be safe
3901            return True
3902
3903        if cursor_datetime < retention_cutoff:
3904            logging.warning(
3905                f"Stream '{stream_name}' has a cursor value older than "
3906                f"the API's retention period of {api_retention_period} "
3907                f"(cutoff: {retention_cutoff.isoformat()}). "
3908                f"Falling back to full refresh to avoid data loss."
3909            )
3910            return True
3911
3912        return False
3913
3914    def _get_state_delegating_stream_model(
3915        self,
3916        model: StateDelegatingStreamModel,
3917        parent_state: Optional[Mapping[str, Any]] = None,
3918    ) -> DeclarativeStreamModel:
3919        """Return the appropriate underlying stream model based on state."""
3920        return (
3921            model.incremental_stream
3922            if self._connector_state_manager.get_stream_state(model.name, None) or parent_state
3923            else model.full_refresh_stream
3924        )
3925
3926    _OPTIONAL_ASYNC_STATUS_FIELDS = {"skipped"}
3927
3928    def _create_async_job_status_mapping(
3929        self, model: AsyncJobStatusMapModel, config: Config, **kwargs: Any
3930    ) -> Mapping[str, AsyncJobStatus]:
3931        api_status_to_cdk_status = {}
3932        for cdk_status, api_statuses in model.dict().items():
3933            if cdk_status == "type":
3934                # This is an element of the dict because of the typing of the CDK but it is not a CDK status
3935                continue
3936
3937            if api_statuses is None:
3938                if cdk_status in self._OPTIONAL_ASYNC_STATUS_FIELDS:
3939                    continue
3940                raise ValueError(
3941                    f"Required CDK status '{cdk_status}' has no API statuses mapped. "
3942                    f"Please provide at least an empty list for required status fields."
3943                )
3944
3945            for status in api_statuses:
3946                if status in api_status_to_cdk_status:
3947                    raise ValueError(
3948                        f"API status {status} is already set for CDK status {cdk_status}. Please ensure API statuses are only provided once"
3949                    )
3950                api_status_to_cdk_status[status] = self._get_async_job_status(cdk_status)
3951        return api_status_to_cdk_status
3952
3953    def _get_async_job_status(self, status: str) -> AsyncJobStatus:
3954        match status:
3955            case "running":
3956                return AsyncJobStatus.RUNNING
3957            case "completed":
3958                return AsyncJobStatus.COMPLETED
3959            case "failed":
3960                return AsyncJobStatus.FAILED
3961            case "timeout":
3962                return AsyncJobStatus.TIMED_OUT
3963            case "skipped":
3964                return AsyncJobStatus.SKIPPED
3965            case _:
3966                raise ValueError(f"Unsupported CDK status {status}")
3967
3968    def create_async_retriever(
3969        self,
3970        model: AsyncRetrieverModel,
3971        config: Config,
3972        *,
3973        name: str,
3974        primary_key: Optional[
3975            Union[str, List[str], List[List[str]]]
3976        ],  # this seems to be needed to match create_simple_retriever
3977        stream_slicer: Optional[StreamSlicer],
3978        client_side_incremental_sync: Optional[Dict[str, Any]] = None,
3979        transformations: List[RecordTransformation],
3980        **kwargs: Any,
3981    ) -> AsyncRetriever:
3982        if model.download_target_requester and not model.download_target_extractor:
3983            raise ValueError(
3984                f"`download_target_extractor` required if using a `download_target_requester`"
3985            )
3986
3987        def _get_download_retriever(
3988            requester: Requester, extractor: RecordExtractor, _decoder: Decoder
3989        ) -> SimpleRetriever:
3990            # We create a record selector for the download retriever
3991            # with no schema normalization and no transformations, neither record filter
3992            # as all this occurs in the record_selector of the AsyncRetriever
3993            record_selector = RecordSelector(
3994                extractor=extractor,
3995                name=name,
3996                record_filter=None,
3997                transformations=[],
3998                schema_normalization=TypeTransformer(TransformConfig.NoTransform),
3999                config=config,
4000                parameters={},
4001            )
4002            paginator = (
4003                self._create_component_from_model(
4004                    model=model.download_paginator,
4005                    decoder=_decoder,
4006                    config=config,
4007                    url_base="",
4008                )
4009                if model.download_paginator
4010                else NoPagination(parameters={})
4011            )
4012
4013            return SimpleRetriever(
4014                requester=requester,
4015                record_selector=record_selector,
4016                primary_key=None,
4017                name=name,
4018                paginator=paginator,
4019                config=config,
4020                parameters={},
4021                log_formatter=self._get_log_formatter(None, name),
4022            )
4023
4024        def _get_job_timeout() -> datetime.timedelta:
4025            user_defined_timeout: Optional[int] = (
4026                int(
4027                    InterpolatedString.create(
4028                        str(model.polling_job_timeout),
4029                        parameters={},
4030                    ).eval(config)
4031                )
4032                if model.polling_job_timeout
4033                else None
4034            )
4035
4036            # check for user defined timeout during the test read or 15 minutes
4037            test_read_timeout = datetime.timedelta(minutes=user_defined_timeout or 15)
4038            # default value for non-connector builder is 60 minutes.
4039            default_sync_timeout = datetime.timedelta(minutes=user_defined_timeout or 60)
4040
4041            return (
4042                test_read_timeout if self._emit_connector_builder_messages else default_sync_timeout
4043            )
4044
4045        decoder = (
4046            self._create_component_from_model(model=model.decoder, config=config)
4047            if model.decoder
4048            else JsonDecoder(parameters={})
4049        )
4050        record_selector = self._create_component_from_model(
4051            model=model.record_selector,
4052            config=config,
4053            decoder=decoder,
4054            name=name,
4055            transformations=transformations,
4056            client_side_incremental_sync=client_side_incremental_sync,
4057        )
4058
4059        stream_slicer = stream_slicer or SinglePartitionRouter(parameters={})
4060        if self._should_limit_slices_fetched():
4061            stream_slicer = cast(
4062                StreamSlicer,
4063                StreamSlicerTestReadDecorator(
4064                    wrapped_slicer=stream_slicer,
4065                    maximum_number_of_slices=self._limit_slices_fetched or 5,
4066                ),
4067            )
4068
4069        creation_requester = self._create_component_from_model(
4070            model=model.creation_requester,
4071            decoder=decoder,
4072            config=config,
4073            name=f"job creation - {name}",
4074        )
4075        polling_requester = self._create_component_from_model(
4076            model=model.polling_requester,
4077            decoder=decoder,
4078            config=config,
4079            name=f"job polling - {name}",
4080        )
4081        job_download_components_name = f"job download - {name}"
4082        download_decoder = (
4083            self._create_component_from_model(model=model.download_decoder, config=config)
4084            if model.download_decoder
4085            else JsonDecoder(parameters={})
4086        )
4087        download_extractor = (
4088            self._create_component_from_model(
4089                model=model.download_extractor,
4090                config=config,
4091                decoder=download_decoder,
4092                parameters=model.parameters,
4093            )
4094            if model.download_extractor
4095            else DpathExtractor(
4096                [],
4097                config=config,
4098                decoder=download_decoder,
4099                parameters=model.parameters or {},
4100            )
4101        )
4102        download_requester = self._create_component_from_model(
4103            model=model.download_requester,
4104            decoder=download_decoder,
4105            config=config,
4106            name=job_download_components_name,
4107        )
4108        download_retriever = _get_download_retriever(
4109            download_requester, download_extractor, download_decoder
4110        )
4111        abort_requester = (
4112            self._create_component_from_model(
4113                model=model.abort_requester,
4114                decoder=decoder,
4115                config=config,
4116                name=f"job abort - {name}",
4117            )
4118            if model.abort_requester
4119            else None
4120        )
4121        delete_requester = (
4122            self._create_component_from_model(
4123                model=model.delete_requester,
4124                decoder=decoder,
4125                config=config,
4126                name=f"job delete - {name}",
4127            )
4128            if model.delete_requester
4129            else None
4130        )
4131        download_target_requester = (
4132            self._create_component_from_model(
4133                model=model.download_target_requester,
4134                decoder=decoder,
4135                config=config,
4136                name=f"job extract_url - {name}",
4137            )
4138            if model.download_target_requester
4139            else None
4140        )
4141        status_extractor = self._create_component_from_model(
4142            model=model.status_extractor, decoder=decoder, config=config, name=name
4143        )
4144        download_target_extractor = (
4145            self._create_component_from_model(
4146                model=model.download_target_extractor,
4147                decoder=decoder,
4148                config=config,
4149                name=name,
4150            )
4151            if model.download_target_extractor
4152            else None
4153        )
4154
4155        job_repository: AsyncJobRepository = AsyncHttpJobRepository(
4156            creation_requester=creation_requester,
4157            polling_requester=polling_requester,
4158            download_retriever=download_retriever,
4159            download_target_requester=download_target_requester,
4160            abort_requester=abort_requester,
4161            delete_requester=delete_requester,
4162            status_extractor=status_extractor,
4163            status_mapping=self._create_async_job_status_mapping(model.status_mapping, config),
4164            download_target_extractor=download_target_extractor,
4165            job_timeout=_get_job_timeout(),
4166        )
4167
4168        failed_retry_wait_time_in_seconds: Optional[int] = (
4169            int(
4170                InterpolatedString.create(
4171                    str(model.failed_retry_wait_time_in_seconds),
4172                    parameters={},
4173                ).eval(config)
4174            )
4175            if model.failed_retry_wait_time_in_seconds
4176            else None
4177        )
4178
4179        async_job_partition_router = AsyncJobPartitionRouter(
4180            job_orchestrator_factory=lambda stream_slices: AsyncJobOrchestrator(
4181                job_repository,
4182                stream_slices,
4183                self._job_tracker,
4184                self._message_repository,
4185                # FIXME work would need to be done here in order to detect if a stream as a parent stream that is bulk
4186                has_bulk_parent=False,
4187                # set the `job_max_retry` to 1 for the `Connector Builder`` use-case.
4188                # `None` == default retry is set to 3 attempts, under the hood.
4189                job_max_retry=1 if self._emit_connector_builder_messages else None,
4190                failed_retry_wait_time_in_seconds=failed_retry_wait_time_in_seconds,
4191            ),
4192            stream_slicer=stream_slicer,
4193            config=config,
4194            parameters=model.parameters or {},
4195        )
4196
4197        return AsyncRetriever(
4198            record_selector=record_selector,
4199            stream_slicer=async_job_partition_router,
4200            config=config,
4201            parameters=model.parameters or {},
4202        )
4203
4204    def create_spec(self, model: SpecModel, config: Config, **kwargs: Any) -> Spec:
4205        config_migrations = [
4206            self._create_component_from_model(migration, config)
4207            for migration in (
4208                model.config_normalization_rules.config_migrations
4209                if (
4210                    model.config_normalization_rules
4211                    and model.config_normalization_rules.config_migrations
4212                )
4213                else []
4214            )
4215        ]
4216        config_transformations = [
4217            self._create_component_from_model(transformation, config)
4218            for transformation in (
4219                model.config_normalization_rules.transformations
4220                if (
4221                    model.config_normalization_rules
4222                    and model.config_normalization_rules.transformations
4223                )
4224                else []
4225            )
4226        ]
4227        config_validations = [
4228            self._create_component_from_model(validation, config)
4229            for validation in (
4230                model.config_normalization_rules.validations
4231                if (
4232                    model.config_normalization_rules
4233                    and model.config_normalization_rules.validations
4234                )
4235                else []
4236            )
4237        ]
4238
4239        return Spec(
4240            connection_specification=model.connection_specification,
4241            documentation_url=model.documentation_url,
4242            advanced_auth=model.advanced_auth,
4243            parameters={},
4244            config_migrations=config_migrations,
4245            config_transformations=config_transformations,
4246            config_validations=config_validations,
4247        )
4248
4249    def create_substream_partition_router(
4250        self,
4251        model: SubstreamPartitionRouterModel,
4252        config: Config,
4253        *,
4254        stream_name: str,
4255        **kwargs: Any,
4256    ) -> SubstreamPartitionRouter:
4257        parent_stream_configs = []
4258        if model.parent_stream_configs:
4259            parent_stream_configs.extend(
4260                [
4261                    self.create_parent_stream_config_with_substream_wrapper(
4262                        model=parent_stream_config, config=config, stream_name=stream_name, **kwargs
4263                    )
4264                    for parent_stream_config in model.parent_stream_configs
4265                ]
4266            )
4267
4268        return SubstreamPartitionRouter(
4269            parent_stream_configs=parent_stream_configs,
4270            parameters=model.parameters or {},
4271            config=config,
4272        )
4273
4274    def create_parent_stream_config_with_substream_wrapper(
4275        self, model: ParentStreamConfigModel, config: Config, *, stream_name: str, **kwargs: Any
4276    ) -> Any:
4277        child_state = self._connector_state_manager.get_stream_state(stream_name, None)
4278        if NO_CURSOR_STATE_KEY in child_state:
4279            # Full refresh streams checkpoint a `{NO_CURSOR_STATE_KEY: true}` sentinel. When such a
4280            # stream is later converted to incremental with an incremental_dependency parent,
4281            # `_instantiate_parent_stream_state_manager` would treat the sentinel's boolean as a legacy
4282            # cursor value and re-key it under the parent's cursor field, crashing cursor initialization.
4283            child_state = {
4284                key: value for key, value in child_state.items() if key != NO_CURSOR_STATE_KEY
4285            }
4286
4287        parent_state: Optional[Mapping[str, Any]] = (
4288            child_state if model.incremental_dependency and child_state else None
4289        )
4290        connector_state_manager = self._instantiate_parent_stream_state_manager(
4291            child_state, config, model, parent_state
4292        )
4293
4294        substream_factory = ModelToComponentFactory(
4295            custom_components_trusted=self._custom_components_trusted,
4296            connector_state_manager=connector_state_manager,
4297            limit_pages_fetched_per_slice=self._limit_pages_fetched_per_slice,
4298            limit_slices_fetched=self._limit_slices_fetched,
4299            emit_connector_builder_messages=self._emit_connector_builder_messages,
4300            disable_retries=self._disable_retries,
4301            disable_cache=self._disable_cache,
4302            message_repository=StateFilteringMessageRepository(
4303                LogAppenderMessageRepositoryDecorator(
4304                    {
4305                        "airbyte_cdk": {"stream": {"is_substream": True}},
4306                        "http": {"is_auxiliary": True},
4307                    },
4308                    self._message_repository,
4309                    self._evaluate_log_level(self._emit_connector_builder_messages),
4310                ),
4311            ),
4312            api_budget=self._api_budget,
4313            # Share the authenticator registry so parent and child streams draw from the
4314            # same token quota counters
4315            rate_limited_authenticators=self._rate_limited_authenticators,
4316        )
4317
4318        return substream_factory.create_parent_stream_config(
4319            model=model, config=config, stream_name=stream_name, **kwargs
4320        )
4321
4322    def _instantiate_parent_stream_state_manager(
4323        self,
4324        child_state: MutableMapping[str, Any],
4325        config: Config,
4326        model: ParentStreamConfigModel,
4327        parent_state: Optional[Mapping[str, Any]] = None,
4328    ) -> ConnectorStateManager:
4329        """
4330        With DefaultStream, the state needs to be provided during __init__ of the cursor as opposed to the
4331        `set_initial_state` flow that existed for the declarative cursors. This state is taken from
4332        self._connector_state_manager.get_stream_state (`self` being a newly created ModelToComponentFactory to account
4333        for the MessageRepository being different). So we need to pass a ConnectorStateManager to the
4334        ModelToComponentFactory that has the parent states. This method populates this if there is a child state and if
4335        incremental_dependency is set.
4336        """
4337        if model.incremental_dependency and child_state:
4338            parent_stream_name = model.stream.name or ""
4339            extracted_parent_state = ConcurrentPerPartitionCursor.get_parent_state(
4340                child_state, parent_stream_name
4341            )
4342
4343            if not extracted_parent_state:
4344                extracted_parent_state = ConcurrentPerPartitionCursor.get_global_state(
4345                    child_state, parent_stream_name
4346                )
4347
4348                if not extracted_parent_state and not isinstance(extracted_parent_state, dict):
4349                    cursor_values = child_state.values()
4350                    if cursor_values and len(cursor_values) == 1:
4351                        incremental_sync_model: Union[
4352                            DatetimeBasedCursorModel,
4353                            IncrementingCountCursorModel,
4354                        ] = (
4355                            model.stream.incremental_sync  # type: ignore  # if we are there, it is because there is incremental_dependency and therefore there is an incremental_sync on the parent stream
4356                            if isinstance(model.stream, DeclarativeStreamModel)
4357                            else self._get_state_delegating_stream_model(
4358                                model.stream, parent_state=parent_state
4359                            ).incremental_sync
4360                        )
4361                        cursor_field = InterpolatedString.create(
4362                            incremental_sync_model.cursor_field,
4363                            parameters=incremental_sync_model.parameters or {},
4364                        ).eval(config)
4365                        extracted_parent_state = AirbyteStateMessage(
4366                            type=AirbyteStateType.STREAM,
4367                            stream=AirbyteStreamState(
4368                                stream_descriptor=StreamDescriptor(
4369                                    name=parent_stream_name, namespace=None
4370                                ),
4371                                stream_state=AirbyteStateBlob(
4372                                    {cursor_field: list(cursor_values)[0]}
4373                                ),
4374                            ),
4375                        )
4376            return ConnectorStateManager([extracted_parent_state] if extracted_parent_state else [])
4377
4378        return ConnectorStateManager([])
4379
4380    @staticmethod
4381    def create_wait_time_from_header(
4382        model: WaitTimeFromHeaderModel, config: Config, **kwargs: Any
4383    ) -> WaitTimeFromHeaderBackoffStrategy:
4384        return WaitTimeFromHeaderBackoffStrategy(
4385            header=model.header,
4386            parameters=model.parameters or {},
4387            config=config,
4388            regex=model.regex,
4389            max_waiting_time_in_seconds=model.max_waiting_time_in_seconds,
4390        )
4391
4392    @staticmethod
4393    def create_wait_until_time_from_header(
4394        model: WaitUntilTimeFromHeaderModel, config: Config, **kwargs: Any
4395    ) -> WaitUntilTimeFromHeaderBackoffStrategy:
4396        return WaitUntilTimeFromHeaderBackoffStrategy(
4397            header=model.header,
4398            parameters=model.parameters or {},
4399            config=config,
4400            min_wait=model.min_wait,
4401            regex=model.regex,
4402            max_waiting_time_in_seconds=model.max_waiting_time_in_seconds,
4403        )
4404
4405    def get_message_repository(self) -> MessageRepository:
4406        return self._message_repository
4407
4408    def _evaluate_log_level(self, emit_connector_builder_messages: bool) -> Level:
4409        return Level.DEBUG if emit_connector_builder_messages else Level.INFO
4410
4411    @staticmethod
4412    def create_components_mapping_definition(
4413        model: ComponentMappingDefinitionModel, config: Config, **kwargs: Any
4414    ) -> ComponentMappingDefinition:
4415        interpolated_value = InterpolatedString.create(
4416            model.value, parameters=model.parameters or {}
4417        )
4418        field_path = [
4419            InterpolatedString.create(path, parameters=model.parameters or {})
4420            for path in model.field_path
4421        ]
4422        return ComponentMappingDefinition(
4423            field_path=field_path,  # type: ignore[arg-type] # field_path can be str and InterpolatedString
4424            value=interpolated_value,
4425            value_type=ModelToComponentFactory._json_schema_type_name_to_type(model.value_type),
4426            create_or_update=model.create_or_update,
4427            condition=model.condition,
4428            parameters=model.parameters or {},
4429        )
4430
4431    def create_http_components_resolver(
4432        self, model: HttpComponentsResolverModel, config: Config, stream_name: Optional[str] = None
4433    ) -> Any:
4434        retriever = self._create_component_from_model(
4435            model=model.retriever,
4436            config=config,
4437            name=f"{stream_name if stream_name else '__http_components_resolver'}",
4438            primary_key=None,
4439            stream_slicer=self._build_stream_slicer_from_partition_router(model.retriever, config),
4440            transformations=[],
4441        )
4442
4443        components_mapping = []
4444        for component_mapping_definition_model in model.components_mapping:
4445            if component_mapping_definition_model.condition:
4446                raise ValueError("`condition` is only supported for     `ConfigComponentsResolver`")
4447            components_mapping.append(
4448                self._create_component_from_model(
4449                    model=component_mapping_definition_model,
4450                    value_type=ModelToComponentFactory._json_schema_type_name_to_type(
4451                        component_mapping_definition_model.value_type
4452                    ),
4453                    config=config,
4454                )
4455            )
4456
4457        return HttpComponentsResolver(
4458            retriever=retriever,
4459            stream_slicer=self._build_stream_slicer_from_partition_router(model.retriever, config),
4460            config=config,
4461            components_mapping=components_mapping,
4462            parameters=model.parameters or {},
4463        )
4464
4465    @staticmethod
4466    def create_stream_config(
4467        model: StreamConfigModel, config: Config, **kwargs: Any
4468    ) -> StreamConfig:
4469        model_configs_pointer: List[Union[InterpolatedString, str]] = (
4470            [x for x in model.configs_pointer] if model.configs_pointer else []
4471        )
4472
4473        return StreamConfig(
4474            configs_pointer=model_configs_pointer,
4475            default_values=model.default_values,
4476            parameters=model.parameters or {},
4477        )
4478
4479    def create_config_components_resolver(
4480        self,
4481        model: ConfigComponentsResolverModel,
4482        config: Config,
4483    ) -> Any:
4484        model_stream_configs = (
4485            model.stream_config if isinstance(model.stream_config, list) else [model.stream_config]
4486        )
4487
4488        stream_configs = [
4489            self._create_component_from_model(
4490                stream_config, config=config, parameters=model.parameters or {}
4491            )
4492            for stream_config in model_stream_configs
4493        ]
4494
4495        components_mapping = [
4496            self._create_component_from_model(
4497                model=components_mapping_definition_model,
4498                value_type=ModelToComponentFactory._json_schema_type_name_to_type(
4499                    components_mapping_definition_model.value_type
4500                ),
4501                config=config,
4502                parameters=model.parameters,
4503            )
4504            for components_mapping_definition_model in model.components_mapping
4505        ]
4506
4507        return ConfigComponentsResolver(
4508            stream_configs=stream_configs,
4509            config=config,
4510            components_mapping=components_mapping,
4511            parameters=model.parameters or {},
4512        )
4513
4514    def create_parametrized_components_resolver(
4515        self,
4516        model: ParametrizedComponentsResolverModel,
4517        config: Config,
4518    ) -> ParametrizedComponentsResolver:
4519        stream_parameters = StreamParametersDefinition(
4520            list_of_parameters_for_stream=model.stream_parameters.list_of_parameters_for_stream
4521        )
4522
4523        components_mapping = []
4524        for components_mapping_definition_model in model.components_mapping:
4525            if components_mapping_definition_model.condition:
4526                raise ValueError("`condition` is only supported for `ConfigComponentsResolver`")
4527            components_mapping.append(
4528                self._create_component_from_model(
4529                    model=components_mapping_definition_model,
4530                    value_type=ModelToComponentFactory._json_schema_type_name_to_type(
4531                        components_mapping_definition_model.value_type
4532                    ),
4533                    config=config,
4534                )
4535            )
4536        return ParametrizedComponentsResolver(
4537            stream_parameters=stream_parameters,
4538            config=config,
4539            components_mapping=components_mapping,
4540            parameters=model.parameters or {},
4541        )
4542
4543    _UNSUPPORTED_DECODER_ERROR = (
4544        "Specified decoder of {decoder_type} is not supported for pagination."
4545        "Please set as `JsonDecoder`, `XmlDecoder`, or a `CompositeRawDecoder` with an inner_parser of `JsonParser` or `GzipParser` instead."
4546        "If using `GzipParser`, please ensure that the lowest level inner_parser is a `JsonParser`."
4547    )
4548
4549    def _is_supported_decoder_for_pagination(self, decoder: Decoder) -> bool:
4550        if isinstance(decoder, (JsonDecoder, XmlDecoder)):
4551            return True
4552        elif isinstance(decoder, CompositeRawDecoder):
4553            return self._is_supported_parser_for_pagination(decoder.parser)
4554        else:
4555            return False
4556
4557    def _is_supported_parser_for_pagination(self, parser: Parser) -> bool:
4558        if isinstance(parser, JsonParser):
4559            return True
4560        elif isinstance(parser, GzipParser):
4561            return isinstance(parser.inner_parser, JsonParser)
4562        else:
4563            return False
4564
4565    def create_http_api_budget(
4566        self, model: HTTPAPIBudgetModel, config: Config, **kwargs: Any
4567    ) -> HttpAPIBudget:
4568        policies = [
4569            self._create_component_from_model(model=policy, config=config)
4570            for policy in model.policies
4571        ]
4572
4573        return HttpAPIBudget(
4574            policies=policies,
4575            ratelimit_reset_header=model.ratelimit_reset_header or "ratelimit-reset",
4576            ratelimit_remaining_header=model.ratelimit_remaining_header or "ratelimit-remaining",
4577            status_codes_for_ratelimit_hit=model.status_codes_for_ratelimit_hit or [429],
4578        )
4579
4580    def create_fixed_window_call_rate_policy(
4581        self, model: FixedWindowCallRatePolicyModel, config: Config, **kwargs: Any
4582    ) -> FixedWindowCallRatePolicy:
4583        matchers = [
4584            self._create_component_from_model(model=matcher, config=config)
4585            for matcher in model.matchers
4586        ]
4587
4588        # Set the initial reset timestamp to 10 days from now.
4589        # This value will be updated by the first request.
4590        return FixedWindowCallRatePolicy(
4591            next_reset_ts=datetime.datetime.now() + datetime.timedelta(days=10),
4592            period=parse_duration(model.period),
4593            call_limit=model.call_limit,
4594            matchers=matchers,
4595        )
4596
4597    def create_file_uploader(
4598        self, model: FileUploaderModel, config: Config, **kwargs: Any
4599    ) -> FileUploader:
4600        name = "File Uploader"
4601        requester = self._create_component_from_model(
4602            model=model.requester,
4603            config=config,
4604            name=name,
4605            **kwargs,
4606        )
4607        download_target_extractor = self._create_component_from_model(
4608            model=model.download_target_extractor,
4609            config=config,
4610            name=name,
4611            **kwargs,
4612        )
4613        emit_connector_builder_messages = self._emit_connector_builder_messages
4614        file_uploader = DefaultFileUploader(
4615            requester=requester,
4616            download_target_extractor=download_target_extractor,
4617            config=config,
4618            file_writer=NoopFileWriter()
4619            if emit_connector_builder_messages
4620            else LocalFileSystemFileWriter(),
4621            parameters=model.parameters or {},
4622            filename_extractor=model.filename_extractor if model.filename_extractor else None,
4623        )
4624
4625        return (
4626            ConnectorBuilderFileUploader(file_uploader)
4627            if emit_connector_builder_messages
4628            else file_uploader
4629        )
4630
4631    def create_moving_window_call_rate_policy(
4632        self, model: MovingWindowCallRatePolicyModel, config: Config, **kwargs: Any
4633    ) -> MovingWindowCallRatePolicy:
4634        rates = [
4635            self._create_component_from_model(model=rate, config=config) for rate in model.rates
4636        ]
4637        matchers = [
4638            self._create_component_from_model(model=matcher, config=config)
4639            for matcher in model.matchers
4640        ]
4641        return MovingWindowCallRatePolicy(
4642            rates=rates,
4643            matchers=matchers,
4644        )
4645
4646    def create_unlimited_call_rate_policy(
4647        self, model: UnlimitedCallRatePolicyModel, config: Config, **kwargs: Any
4648    ) -> UnlimitedCallRatePolicy:
4649        matchers = [
4650            self._create_component_from_model(model=matcher, config=config)
4651            for matcher in model.matchers
4652        ]
4653
4654        return UnlimitedCallRatePolicy(
4655            matchers=matchers,
4656        )
4657
4658    def create_rate(self, model: RateModel, config: Config, **kwargs: Any) -> Rate:
4659        interpolated_limit = InterpolatedString.create(str(model.limit), parameters={})
4660        return Rate(
4661            limit=int(interpolated_limit.eval(config=config)),
4662            interval=parse_duration(model.interval),
4663        )
4664
4665    def create_http_request_matcher(
4666        self, model: HttpRequestRegexMatcherModel, config: Config, **kwargs: Any
4667    ) -> HttpRequestRegexMatcher:
4668        weight = model.weight
4669        if weight is not None:
4670            if isinstance(weight, str):
4671                weight = int(InterpolatedString.create(weight, parameters={}).eval(config))
4672            else:
4673                weight = int(weight)
4674            if weight < 1:
4675                raise ValueError(f"weight must be >= 1, got {weight}")
4676        return HttpRequestRegexMatcher(
4677            method=model.method,
4678            url_base=model.url_base,
4679            url_path_pattern=model.url_path_pattern,
4680            params=model.params,
4681            headers=model.headers,
4682            weight=weight,
4683        )
4684
4685    def create_rate_limited_multiple_token_authenticator(
4686        self,
4687        model: RateLimitedMultipleTokenAuthenticatorModel,
4688        config: Config,
4689        **kwargs: Any,
4690    ) -> RateLimitedMultipleTokenAuthenticator:
4691        if isinstance(model.tokens, str):
4692            tokens_value = InterpolatedString.create(model.tokens, parameters={}).eval(config)
4693            delimiter = model.token_delimiter or ","
4694            tokens = [
4695                token.strip() for token in str(tokens_value).split(delimiter) if token.strip()
4696            ]
4697        else:
4698            tokens = [
4699                token_value
4700                for token in model.tokens
4701                if (
4702                    token_value := str(
4703                        InterpolatedString.create(token, parameters={}).eval(config)
4704                    ).strip()
4705                )
4706            ]
4707
4708        quota_specs = [
4709            {
4710                "name": quota_model.name,
4711                "remaining_path": quota_model.remaining_path,
4712                "reset_path": quota_model.reset_path,
4713                "limit_path": quota_model.limit_path,
4714                "remaining_header": quota_model.remaining_header,
4715                "reset_header": quota_model.reset_header,
4716                "limit_header": quota_model.limit_header,
4717                # Normalize the same way as the runtime TokenQuota below, so an omitted field
4718                # and an explicit `[]` key identically and keep sharing one set of counters.
4719                "exhaustion_status_codes": quota_model.exhaustion_status_codes or [],
4720                "matchers": [
4721                    {
4722                        "method": matcher_model.method,
4723                        "url_base": matcher_model.url_base,
4724                        "url_path_pattern": matcher_model.url_path_pattern,
4725                        "params": matcher_model.params,
4726                        "headers": matcher_model.headers,
4727                        "weight": matcher_model.weight,
4728                    }
4729                    for matcher_model in quota_model.matchers or []
4730                ],
4731            }
4732            for quota_model in model.quotas
4733        ]
4734
4735        quota_status_url = str(
4736            InterpolatedString.create(model.quota_status_source.url, parameters={}).eval(config)
4737        )
4738        quota_status_http_method = (
4739            model.quota_status_source.http_method.value
4740            if model.quota_status_source.http_method
4741            else "GET"
4742        )
4743        quota_status_headers = {
4744            key: str(InterpolatedString.create(value, parameters={}).eval(config))
4745            for key, value in (model.quota_status_source.request_headers or {}).items()
4746        }
4747        # Normalize the same way as the quota specs above, so an omitted field and an explicit
4748        # `[]` key identically and keep sharing one set of counters. Deduplicated as well as
4749        # sorted, because the runtime turns this into a set: without it `[404]` and `[404, 404]`
4750        # would key differently and stop sharing counters while behaving identically.
4751        quota_status_unavailable_status_codes = sorted(
4752            set(model.quota_status_source.unavailable_status_codes or [])
4753        )
4754        auth_method = model.auth_method or "Bearer"
4755        header = model.header or "Authorization"
4756        max_wait_time_str = str(
4757            InterpolatedString.create(model.max_wait_time or "PT2H", parameters={}).eval(config)
4758        )
4759        max_wait_time = parse_duration(max_wait_time_str)
4760        if not isinstance(max_wait_time, datetime.timedelta):
4761            raise ValueError(
4762                f"max_wait_time must be a fixed-length ISO 8601 duration (e.g. 'PT2H'); "
4763                f"calendar-unit durations like '{max_wait_time_str}' are not supported"
4764            )
4765        budget_reserve_fraction = (
4766            model.budget_reserve_fraction if model.budget_reserve_fraction is not None else 0.1
4767        )
4768        budget_min_reserve = (
4769            model.budget_min_reserve if model.budget_min_reserve is not None else 50
4770        )
4771
4772        # Reuse the same instance for identical definitions so that all streams share the
4773        # same token quota counters (similar to how api_budget is shared). The key is built
4774        # from the resolved constructor arguments rather than the raw model so that
4775        # stream-specific `$parameters` propagated onto the model (and its nested components)
4776        # cannot break instance sharing.
4777        cache_key = json.dumps(
4778            {
4779                "tokens": tokens,
4780                "quotas": quota_specs,
4781                "quota_status_url": quota_status_url,
4782                "quota_status_http_method": quota_status_http_method,
4783                "quota_status_headers": quota_status_headers,
4784                "quota_status_unavailable_status_codes": quota_status_unavailable_status_codes,
4785                "auth_method": auth_method,
4786                "header": header,
4787                "max_wait_time": max_wait_time.total_seconds(),
4788                "budget_reserve_fraction": budget_reserve_fraction,
4789                "budget_min_reserve": budget_min_reserve,
4790            },
4791            sort_keys=True,
4792        )
4793        if cache_key in self._rate_limited_authenticators:
4794            return self._rate_limited_authenticators[cache_key]
4795
4796        quotas = [
4797            TokenQuota(
4798                name=quota_model.name,
4799                remaining_path=quota_model.remaining_path,
4800                reset_path=quota_model.reset_path,
4801                limit_path=quota_model.limit_path,
4802                remaining_header=quota_model.remaining_header,
4803                reset_header=quota_model.reset_header,
4804                limit_header=quota_model.limit_header,
4805                exhaustion_status_codes=quota_model.exhaustion_status_codes or [],
4806                matchers=[
4807                    self.create_http_request_matcher(matcher_model, config)
4808                    for matcher_model in quota_model.matchers or []
4809                ],
4810            )
4811            for quota_model in model.quotas
4812        ]
4813
4814        authenticator = RateLimitedMultipleTokenAuthenticator(
4815            tokens=tokens,
4816            quotas=quotas,
4817            quota_status_url=quota_status_url,
4818            quota_status_http_method=quota_status_http_method,
4819            quota_status_headers=quota_status_headers,
4820            quota_status_unavailable_status_codes=quota_status_unavailable_status_codes,
4821            auth_method=auth_method,
4822            header=header,
4823            max_wait_time=max_wait_time,
4824            budget_reserve_fraction=budget_reserve_fraction,
4825            budget_min_reserve=budget_min_reserve,
4826        )
4827        self._rate_limited_authenticators[cache_key] = authenticator
4828        return authenticator
4829
4830    def set_api_budget(self, component_definition: ComponentDefinition, config: Config) -> None:
4831        self._api_budget = self.create_component(
4832            model_type=HTTPAPIBudgetModel, component_definition=component_definition, config=config
4833        )
4834
4835    def create_grouping_partition_router(
4836        self,
4837        model: GroupingPartitionRouterModel,
4838        config: Config,
4839        *,
4840        stream_name: str,
4841        **kwargs: Any,
4842    ) -> GroupingPartitionRouter:
4843        underlying_router = self._create_component_from_model(
4844            model=model.underlying_partition_router,
4845            config=config,
4846            stream_name=stream_name,
4847            **kwargs,
4848        )
4849        if model.group_size < 1:
4850            raise ValueError(f"Group size must be greater than 0, got {model.group_size}")
4851
4852        # Request options in underlying partition routers are not supported for GroupingPartitionRouter
4853        # because they are specific to individual partitions and cannot be aggregated or handled
4854        # when grouping, potentially leading to incorrect API calls. Any request customization
4855        # should be managed at the stream level through the requester's configuration.
4856        if isinstance(underlying_router, SubstreamPartitionRouter):
4857            if any(
4858                parent_config.request_option
4859                for parent_config in underlying_router.parent_stream_configs
4860            ):
4861                raise ValueError("Request options are not supported for GroupingPartitionRouter.")
4862
4863        if isinstance(underlying_router, ListPartitionRouter):
4864            if underlying_router.request_option:
4865                raise ValueError("Request options are not supported for GroupingPartitionRouter.")
4866
4867        return GroupingPartitionRouter(
4868            group_size=model.group_size,
4869            underlying_partition_router=underlying_router,
4870            deduplicate=model.deduplicate if model.deduplicate is not None else True,
4871            config=config,
4872        )
4873
4874    def create_union_partition_router(
4875        self,
4876        model: UnionPartitionRouterModel,
4877        config: Config,
4878        *,
4879        stream_name: str,
4880        **kwargs: Any,
4881    ) -> UnionPartitionRouter:
4882        # The schema enforces minItems: 2 for manifests; this guard covers construction paths
4883        # that bypass JSON-schema validation (the generated model carries no min_items constraint).
4884        if len(model.partition_routers) < 2:
4885            raise ValueError(
4886                f"UnionPartitionRouter for stream {stream_name} needs at least 2 child partition routers"
4887            )
4888
4889        partition_routers = [
4890            self._create_component_from_model(
4891                model=child,
4892                config=config,
4893                stream_name=stream_name,
4894                **kwargs,
4895            )
4896            for child in model.partition_routers
4897        ]
4898
4899        # partition_field depends only on config/parameters, so it is evaluated once at build
4900        # time; the runtime component always receives a plain string.
4901        partition_field = InterpolatedString.create(
4902            model.partition_field, parameters=model.parameters or {}
4903        ).eval(config)
4904
4905        # Fail fast at build time when a built-in child router is statically known to emit a
4906        # partition field different from the union's. CustomPartitionRouter children are opaque
4907        # and can only be validated at runtime.
4908        for child_model in model.partition_routers:
4909            child_partition_fields: List[str] = []
4910            if isinstance(child_model, ListPartitionRouterModel):
4911                child_partition_fields.append(
4912                    InterpolatedString.create(
4913                        child_model.cursor_field, parameters=child_model.parameters or {}
4914                    ).eval(config)
4915                )
4916            elif isinstance(child_model, SubstreamPartitionRouterModel):
4917                for parent_stream_config in child_model.parent_stream_configs:
4918                    child_partition_fields.append(
4919                        InterpolatedString.create(
4920                            parent_stream_config.partition_field,
4921                            parameters=parent_stream_config.parameters
4922                            or child_model.parameters
4923                            or {},
4924                        ).eval(config)
4925                    )
4926            elif isinstance(child_model, UnionPartitionRouterModel):
4927                child_partition_fields.append(
4928                    InterpolatedString.create(
4929                        child_model.partition_field, parameters=child_model.parameters or {}
4930                    ).eval(config)
4931                )
4932            for child_partition_field in child_partition_fields:
4933                if child_partition_field != partition_field:
4934                    raise ValueError(
4935                        f"UnionPartitionRouter expects all child partition routers to emit the "
4936                        f"partition field '{partition_field}', but a "
4937                        f"{child_model.type} child emits '{child_partition_field}'."
4938                    )
4939
4940        # A union slice comes from exactly one child partition router, so request options
4941        # declared on children cannot be applied consistently to requests built from the
4942        # normalized union slices. Partition values should be consumed via interpolation
4943        # (e.g. stream_partition) instead. Note that this validation only covers built-in
4944        # router types; CustomPartitionRouter children are opaque, so any request options
4945        # they implement internally cannot be detected or rejected here.
4946        for router in partition_routers:
4947            if isinstance(router, SubstreamPartitionRouter):
4948                if any(
4949                    parent_config.request_option for parent_config in router.parent_stream_configs
4950                ):
4951                    raise ValueError("Request options are not supported for UnionPartitionRouter.")
4952            if isinstance(router, ListPartitionRouter) and router.request_option:
4953                raise ValueError("Request options are not supported for UnionPartitionRouter.")
4954
4955        return UnionPartitionRouter(
4956            partition_routers=partition_routers,
4957            partition_field=partition_field,
4958            parameters=model.parameters or {},
4959        )
4960
4961    def _ensure_query_properties_to_model(
4962        self, requester: Union[HttpRequesterModel, CustomRequesterModel]
4963    ) -> None:
4964        """
4965        For some reason, it seems like CustomRequesterModel request_parameters stays as dictionaries which means that
4966        the other conditions relying on it being QueryPropertiesModel instead of a dict fail. Here, we migrate them to
4967        proper model.
4968        """
4969        if not hasattr(requester, "request_parameters"):
4970            return
4971
4972        request_parameters = requester.request_parameters
4973        if request_parameters and isinstance(request_parameters, Dict):
4974            for request_parameter_key in request_parameters.keys():
4975                request_parameter = request_parameters[request_parameter_key]
4976                if (
4977                    isinstance(request_parameter, Dict)
4978                    and request_parameter.get("type") == "QueryProperties"
4979                ):
4980                    request_parameters[request_parameter_key] = QueryPropertiesModel.parse_obj(
4981                        request_parameter
4982                    )
4983
4984    def _get_catalog_defined_cursor_field(
4985        self, stream_name: str, allow_catalog_defined_cursor_field: bool
4986    ) -> Optional[CursorField]:
4987        if not allow_catalog_defined_cursor_field:
4988            return None
4989
4990        configured_stream = self._stream_name_to_configured_stream.get(stream_name)
4991
4992        # Depending on the operation is being performed, there may not be a configured stream yet. In this
4993        # case we return None which will then use the default cursor field defined on the cursor model.
4994        # We also treat cursor_field: [""] (list with empty string) as no cursor field, since this can
4995        # occur when the platform serializes "no cursor configured" streams incorrectly.
4996        if (
4997            not configured_stream
4998            or not configured_stream.cursor_field
4999            or not configured_stream.cursor_field[0]
5000        ):
5001            return None
5002        elif len(configured_stream.cursor_field) > 1:
5003            raise ValueError(
5004                f"The `{stream_name}` stream does not support nested cursor_field. Please specify only a single cursor_field for the stream in the configured catalog."
5005            )
5006        else:
5007            return CursorField(
5008                cursor_field_key=configured_stream.cursor_field[0],
5009                supports_catalog_defined_cursor_field=allow_catalog_defined_cursor_field,
5010            )
ComponentDefinition = typing.Mapping[str, typing.Any]
SCHEMA_TRANSFORMER_TYPE_MAPPING = {<SchemaNormalization.None_: 'None'>: <TransformConfig.NoTransform: 1>, <SchemaNormalization.Default: 'Default'>: <TransformConfig.DefaultSchemaNormalization: 2>}
MAX_SLICES = 5
LOGGER = <Logger airbyte.model_to_component_factory (INFO)>
class ModelToComponentFactory:
 707class ModelToComponentFactory:
 708    EPOCH_DATETIME_FORMAT = "%s"
 709
 710    def __init__(
 711        self,
 712        limit_pages_fetched_per_slice: Optional[int] = None,
 713        limit_slices_fetched: Optional[int] = None,
 714        emit_connector_builder_messages: bool = False,
 715        disable_retries: bool = False,
 716        disable_cache: bool = False,
 717        message_repository: Optional[MessageRepository] = None,
 718        connector_state_manager: Optional[ConnectorStateManager] = None,
 719        max_concurrent_async_job_count: Optional[int] = None,
 720        configured_catalog: Optional[ConfiguredAirbyteCatalog] = None,
 721        api_budget: Optional[APIBudget] = None,
 722        rate_limited_authenticators: Optional[
 723            Dict[str, RateLimitedMultipleTokenAuthenticator]
 724        ] = None,
 725        custom_components_trusted: bool = True,
 726    ):
 727        self._init_mappings()
 728        self._custom_components_trusted = custom_components_trusted
 729        self._limit_pages_fetched_per_slice = limit_pages_fetched_per_slice
 730        self._limit_slices_fetched = limit_slices_fetched
 731        self._emit_connector_builder_messages = emit_connector_builder_messages
 732        self._disable_retries = disable_retries
 733        self._disable_cache = disable_cache
 734        self._message_repository = message_repository or InMemoryMessageRepository(
 735            self._evaluate_log_level(emit_connector_builder_messages)
 736        )
 737        self._stream_name_to_configured_stream = self._create_stream_name_to_configured_stream(
 738            configured_catalog
 739        )
 740        self._connector_state_manager = connector_state_manager or ConnectorStateManager()
 741        self._api_budget: Optional[Union[APIBudget]] = api_budget
 742        # Shared instances so all streams see the same token quota counters (like api_budget)
 743        self._rate_limited_authenticators: Dict[str, RateLimitedMultipleTokenAuthenticator] = (
 744            rate_limited_authenticators if rate_limited_authenticators is not None else {}
 745        )
 746        self._job_tracker: JobTracker = JobTracker(max_concurrent_async_job_count or 1)
 747        # placeholder for deprecation warnings
 748        self._collected_deprecation_logs: List[ConnectorBuilderLogMessage] = []
 749
 750    def _init_mappings(self) -> None:
 751        self.PYDANTIC_MODEL_TO_CONSTRUCTOR: Mapping[Type[BaseModel], Callable[..., Any]] = {
 752            AddedFieldDefinitionModel: self.create_added_field_definition,
 753            AddFieldsModel: self.create_add_fields,
 754            ApiKeyAuthenticatorModel: self.create_api_key_authenticator,
 755            BasicHttpAuthenticatorModel: self.create_basic_http_authenticator,
 756            BearerAuthenticatorModel: self.create_bearer_authenticator,
 757            CheckStreamModel: self.create_check_stream,
 758            DynamicStreamCheckConfigModel: self.create_dynamic_stream_check_config,
 759            CheckDynamicStreamModel: self.create_check_dynamic_stream,
 760            CompositeErrorHandlerModel: self.create_composite_error_handler,
 761            ConcurrencyLevelModel: self.create_concurrency_level,
 762            ConfigMigrationModel: self.create_config_migration,
 763            ConfigAddFieldsModel: self.create_config_add_fields,
 764            ConfigRemapFieldModel: self.create_config_remap_field,
 765            ConfigRemoveFieldsModel: self.create_config_remove_fields,
 766            ConstantBackoffStrategyModel: self.create_constant_backoff_strategy,
 767            CsvDecoderModel: self.create_csv_decoder,
 768            CursorPaginationModel: self.create_cursor_pagination,
 769            CustomAuthenticatorModel: self.create_custom_component,
 770            CustomBackoffStrategyModel: self.create_custom_component,
 771            CustomDecoderModel: self.create_custom_component,
 772            CustomErrorHandlerModel: self.create_custom_component,
 773            CustomRecordExtractorModel: self.create_custom_component,
 774            CustomRecordFilterModel: self.create_custom_component,
 775            CustomRequesterModel: self.create_custom_component,
 776            CustomRetrieverModel: self.create_custom_component,
 777            CustomSchemaLoader: self.create_custom_component,
 778            CustomSchemaNormalizationModel: self.create_custom_component,
 779            CustomStateMigration: self.create_custom_component,
 780            CustomPaginationStrategyModel: self.create_custom_component,
 781            CustomPartitionRouterModel: self.create_custom_component,
 782            CustomTransformationModel: self.create_custom_component,
 783            CustomValidationStrategyModel: self.create_custom_component,
 784            CustomConfigTransformationModel: self.create_custom_component,
 785            DeclarativeStreamModel: self.create_default_stream,
 786            DefaultErrorHandlerModel: self.create_default_error_handler,
 787            DefaultPaginatorModel: self.create_default_paginator,
 788            DpathExtractorModel: self.create_dpath_extractor,
 789            DpathValidatorModel: self.create_dpath_validator,
 790            ResponseToFileExtractorModel: self.create_response_to_file_extractor,
 791            ExponentialBackoffStrategyModel: self.create_exponential_backoff_strategy,
 792            SessionTokenAuthenticatorModel: self.create_session_token_authenticator,
 793            GroupByKeyMergeStrategyModel: self.create_group_by_key,
 794            HttpRequesterModel: self.create_http_requester,
 795            HttpResponseFilterModel: self.create_http_response_filter,
 796            InlineSchemaLoaderModel: self.create_inline_schema_loader,
 797            JsonDecoderModel: self.create_json_decoder,
 798            JsonItemsDecoderModel: self.create_json_items_decoder,
 799            JsonlDecoderModel: self.create_jsonl_decoder,
 800            JsonSchemaPropertySelectorModel: self.create_json_schema_property_selector,
 801            GzipDecoderModel: self.create_gzip_decoder,
 802            KeysToLowerModel: self.create_keys_to_lower_transformation,
 803            KeysToSnakeCaseModel: self.create_keys_to_snake_transformation,
 804            KeysReplaceModel: self.create_keys_replace_transformation,
 805            FlattenFieldsModel: self.create_flatten_fields,
 806            DpathFlattenFieldsModel: self.create_dpath_flatten_fields,
 807            IterableDecoderModel: self.create_iterable_decoder,
 808            XmlDecoderModel: self.create_xml_decoder,
 809            JsonFileSchemaLoaderModel: self.create_json_file_schema_loader,
 810            DynamicSchemaLoaderModel: self.create_dynamic_schema_loader,
 811            SchemaTypeIdentifierModel: self.create_schema_type_identifier,
 812            TypesMapModel: self.create_types_map,
 813            ComplexFieldTypeModel: self.create_complex_field_type,
 814            JwtAuthenticatorModel: self.create_jwt_authenticator,
 815            LegacyToPerPartitionStateMigrationModel: self.create_legacy_to_per_partition_state_migration,
 816            ListPartitionRouterModel: self.create_list_partition_router,
 817            MinMaxDatetimeModel: self.create_min_max_datetime,
 818            NoAuthModel: self.create_no_auth,
 819            NoPaginationModel: self.create_no_pagination,
 820            OAuthAuthenticatorModel: self.create_oauth_authenticator,
 821            OffsetIncrementModel: self.create_offset_increment,
 822            PageIncrementModel: self.create_page_increment,
 823            ParentStreamConfigModel: self.create_parent_stream_config_with_substream_wrapper,
 824            PredicateValidatorModel: self.create_predicate_validator,
 825            PropertiesFromEndpointModel: self.create_properties_from_endpoint,
 826            PropertyChunkingModel: self.create_property_chunking,
 827            QueryPropertiesModel: self.create_query_properties,
 828            RecordExpanderModel: self.create_record_expander,
 829            RecordFilterModel: self.create_record_filter,
 830            RecordSelectorModel: self.create_record_selector,
 831            RemoveFieldsModel: self.create_remove_fields,
 832            RequestPathModel: self.create_request_path,
 833            RequestOptionModel: self.create_request_option,
 834            LegacySessionTokenAuthenticatorModel: self.create_legacy_session_token_authenticator,
 835            SelectiveAuthenticatorModel: self.create_selective_authenticator,
 836            SimpleRetrieverModel: self.create_simple_retriever,
 837            StateDelegatingStreamModel: self.create_state_delegating_stream,
 838            SpecModel: self.create_spec,
 839            SubstreamPartitionRouterModel: self.create_substream_partition_router,
 840            ValidateAdheresToSchemaModel: self.create_validate_adheres_to_schema,
 841            WaitTimeFromHeaderModel: self.create_wait_time_from_header,
 842            WaitUntilTimeFromHeaderModel: self.create_wait_until_time_from_header,
 843            AsyncRetrieverModel: self.create_async_retriever,
 844            HttpComponentsResolverModel: self.create_http_components_resolver,
 845            ConfigComponentsResolverModel: self.create_config_components_resolver,
 846            ParametrizedComponentsResolverModel: self.create_parametrized_components_resolver,
 847            StreamConfigModel: self.create_stream_config,
 848            ComponentMappingDefinitionModel: self.create_components_mapping_definition,
 849            ZipfileDecoderModel: self.create_zipfile_decoder,
 850            HTTPAPIBudgetModel: self.create_http_api_budget,
 851            FileUploaderModel: self.create_file_uploader,
 852            FixedWindowCallRatePolicyModel: self.create_fixed_window_call_rate_policy,
 853            MovingWindowCallRatePolicyModel: self.create_moving_window_call_rate_policy,
 854            UnlimitedCallRatePolicyModel: self.create_unlimited_call_rate_policy,
 855            RateModel: self.create_rate,
 856            HttpRequestRegexMatcherModel: self.create_http_request_matcher,
 857            RateLimitedMultipleTokenAuthenticatorModel: self.create_rate_limited_multiple_token_authenticator,
 858            GroupingPartitionRouterModel: self.create_grouping_partition_router,
 859            UnionPartitionRouterModel: self.create_union_partition_router,
 860        }
 861
 862        # Needed for the case where we need to perform a second parse on the fields of a custom component
 863        self.TYPE_NAME_TO_MODEL = {cls.__name__: cls for cls in self.PYDANTIC_MODEL_TO_CONSTRUCTOR}
 864
 865    @staticmethod
 866    def _create_stream_name_to_configured_stream(
 867        configured_catalog: Optional[ConfiguredAirbyteCatalog],
 868    ) -> Mapping[str, ConfiguredAirbyteStream]:
 869        return (
 870            {stream.stream.name: stream for stream in configured_catalog.streams}
 871            if configured_catalog
 872            else {}
 873        )
 874
 875    def create_component(
 876        self,
 877        model_type: Type[BaseModel],
 878        component_definition: ComponentDefinition,
 879        config: Config,
 880        **kwargs: Any,
 881    ) -> Any:
 882        """
 883        Takes a given Pydantic model type and Mapping representing a component definition and creates a declarative component and
 884        subcomponents which will be used at runtime. This is done by first parsing the mapping into a Pydantic model and then creating
 885        creating declarative components from that model.
 886
 887        :param model_type: The type of declarative component that is being initialized
 888        :param component_definition: The mapping that represents a declarative component
 889        :param config: The connector config that is provided by the customer
 890        :return: The declarative component to be used at runtime
 891        """
 892
 893        component_type = component_definition.get("type")
 894        if component_definition.get("type") != model_type.__name__:
 895            raise ValueError(
 896                f"Expected manifest component of type {model_type.__name__}, but received {component_type} instead"
 897            )
 898
 899        declarative_component_model = model_type.parse_obj(component_definition)
 900
 901        if not isinstance(declarative_component_model, model_type):
 902            raise ValueError(
 903                f"Expected {model_type.__name__} component, but received {declarative_component_model.__class__.__name__}"
 904            )
 905
 906        return self._create_component_from_model(
 907            model=declarative_component_model, config=config, **kwargs
 908        )
 909
 910    def _create_component_from_model(self, model: BaseModel, config: Config, **kwargs: Any) -> Any:
 911        if model.__class__ not in self.PYDANTIC_MODEL_TO_CONSTRUCTOR:
 912            raise ValueError(
 913                f"{model.__class__} with attributes {model} is not a valid component type"
 914            )
 915        component_constructor = self.PYDANTIC_MODEL_TO_CONSTRUCTOR.get(model.__class__)
 916        if not component_constructor:
 917            raise ValueError(f"Could not find constructor for {model.__class__}")
 918
 919        # collect deprecation warnings for supported models.
 920        if isinstance(model, BaseModelWithDeprecations):
 921            self._collect_model_deprecations(model)
 922
 923        return component_constructor(model=model, config=config, **kwargs)
 924
 925    def get_model_deprecations(self) -> List[ConnectorBuilderLogMessage]:
 926        """
 927        Returns the deprecation warnings that were collected during the creation of components.
 928        """
 929        return self._collected_deprecation_logs
 930
 931    def _collect_model_deprecations(self, model: BaseModelWithDeprecations) -> None:
 932        """
 933        Collects deprecation logs from the given model and appends any new logs to the internal collection.
 934
 935        This method checks if the provided model has deprecation logs (identified by the presence of the DEPRECATION_LOGS_TAG attribute and a non-None `_deprecation_logs` property). It iterates through each deprecation log in the model and appends it to the `_collected_deprecation_logs` list if it has not already been collected, ensuring that duplicate logs are avoided.
 936
 937        Args:
 938            model (BaseModelWithDeprecations): The model instance from which to collect deprecation logs.
 939        """
 940        if hasattr(model, DEPRECATION_LOGS_TAG) and model._deprecation_logs is not None:
 941            for log in model._deprecation_logs:
 942                # avoid duplicates for deprecation logs observed.
 943                if log not in self._collected_deprecation_logs:
 944                    self._collected_deprecation_logs.append(log)
 945
 946    def create_config_migration(
 947        self, model: ConfigMigrationModel, config: Config
 948    ) -> ConfigMigration:
 949        transformations: List[ConfigTransformation] = [
 950            self._create_component_from_model(transformation, config)
 951            for transformation in model.transformations
 952        ]
 953
 954        return ConfigMigration(
 955            description=model.description,
 956            transformations=transformations,
 957        )
 958
 959    def create_config_add_fields(
 960        self, model: ConfigAddFieldsModel, config: Config, **kwargs: Any
 961    ) -> ConfigAddFields:
 962        fields = [self._create_component_from_model(field, config) for field in model.fields]
 963        return ConfigAddFields(
 964            fields=fields,
 965            condition=model.condition or "",
 966        )
 967
 968    @staticmethod
 969    def create_config_remove_fields(
 970        model: ConfigRemoveFieldsModel, config: Config, **kwargs: Any
 971    ) -> ConfigRemoveFields:
 972        return ConfigRemoveFields(
 973            field_pointers=model.field_pointers,
 974            condition=model.condition or "",
 975        )
 976
 977    @staticmethod
 978    def create_config_remap_field(
 979        model: ConfigRemapFieldModel, config: Config, **kwargs: Any
 980    ) -> ConfigRemapField:
 981        mapping = cast(Mapping[str, Any], model.map)
 982        return ConfigRemapField(
 983            map=mapping,
 984            field_path=model.field_path,
 985            config=config,
 986        )
 987
 988    def create_dpath_validator(self, model: DpathValidatorModel, config: Config) -> DpathValidator:
 989        strategy = self._create_component_from_model(model.validation_strategy, config)
 990
 991        return DpathValidator(
 992            field_path=model.field_path,
 993            strategy=strategy,
 994        )
 995
 996    def create_predicate_validator(
 997        self, model: PredicateValidatorModel, config: Config
 998    ) -> PredicateValidator:
 999        strategy = self._create_component_from_model(model.validation_strategy, config)
1000
1001        return PredicateValidator(
1002            value=model.value,
1003            strategy=strategy,
1004        )
1005
1006    @staticmethod
1007    def create_validate_adheres_to_schema(
1008        model: ValidateAdheresToSchemaModel, config: Config, **kwargs: Any
1009    ) -> ValidateAdheresToSchema:
1010        base_schema = cast(Mapping[str, Any], model.base_schema)
1011        return ValidateAdheresToSchema(
1012            schema=base_schema,
1013        )
1014
1015    @staticmethod
1016    def create_added_field_definition(
1017        model: AddedFieldDefinitionModel, config: Config, **kwargs: Any
1018    ) -> AddedFieldDefinition:
1019        interpolated_value = InterpolatedString.create(
1020            model.value, parameters=model.parameters or {}
1021        )
1022        return AddedFieldDefinition(
1023            path=model.path,
1024            value=interpolated_value,
1025            value_type=ModelToComponentFactory._json_schema_type_name_to_type(model.value_type),
1026            parameters=model.parameters or {},
1027        )
1028
1029    def create_add_fields(self, model: AddFieldsModel, config: Config, **kwargs: Any) -> AddFields:
1030        added_field_definitions = [
1031            self._create_component_from_model(
1032                model=added_field_definition_model,
1033                value_type=ModelToComponentFactory._json_schema_type_name_to_type(
1034                    added_field_definition_model.value_type
1035                ),
1036                config=config,
1037            )
1038            for added_field_definition_model in model.fields
1039        ]
1040        return AddFields(
1041            fields=added_field_definitions,
1042            condition=model.condition or "",
1043            parameters=model.parameters or {},
1044        )
1045
1046    def create_keys_to_lower_transformation(
1047        self, model: KeysToLowerModel, config: Config, **kwargs: Any
1048    ) -> KeysToLowerTransformation:
1049        return KeysToLowerTransformation()
1050
1051    def create_keys_to_snake_transformation(
1052        self, model: KeysToSnakeCaseModel, config: Config, **kwargs: Any
1053    ) -> KeysToSnakeCaseTransformation:
1054        return KeysToSnakeCaseTransformation()
1055
1056    def create_keys_replace_transformation(
1057        self, model: KeysReplaceModel, config: Config, **kwargs: Any
1058    ) -> KeysReplaceTransformation:
1059        return KeysReplaceTransformation(
1060            old=model.old, new=model.new, parameters=model.parameters or {}
1061        )
1062
1063    def create_flatten_fields(
1064        self, model: FlattenFieldsModel, config: Config, **kwargs: Any
1065    ) -> FlattenFields:
1066        return FlattenFields(
1067            flatten_lists=model.flatten_lists if model.flatten_lists is not None else True
1068        )
1069
1070    def create_dpath_flatten_fields(
1071        self, model: DpathFlattenFieldsModel, config: Config, **kwargs: Any
1072    ) -> DpathFlattenFields:
1073        model_field_path: List[Union[InterpolatedString, str]] = [x for x in model.field_path]
1074        key_transformation = (
1075            KeyTransformation(
1076                config=config,
1077                prefix=model.key_transformation.prefix,
1078                suffix=model.key_transformation.suffix,
1079                parameters=model.parameters or {},
1080            )
1081            if model.key_transformation is not None
1082            else None
1083        )
1084        return DpathFlattenFields(
1085            config=config,
1086            field_path=model_field_path,
1087            delete_origin_value=model.delete_origin_value
1088            if model.delete_origin_value is not None
1089            else False,
1090            replace_record=model.replace_record if model.replace_record is not None else False,
1091            key_transformation=key_transformation,
1092            parameters=model.parameters or {},
1093        )
1094
1095    @staticmethod
1096    def _json_schema_type_name_to_type(value_type: Optional[ValueType]) -> Optional[Type[Any]]:
1097        if not value_type:
1098            return None
1099        names_to_types = {
1100            ValueType.string: str,
1101            ValueType.number: float,
1102            ValueType.integer: int,
1103            ValueType.boolean: bool,
1104        }
1105        return names_to_types[value_type]
1106
1107    def create_api_key_authenticator(
1108        self,
1109        model: ApiKeyAuthenticatorModel,
1110        config: Config,
1111        token_provider: Optional[TokenProvider] = None,
1112        **kwargs: Any,
1113    ) -> ApiKeyAuthenticator:
1114        if model.inject_into is None and model.header is None:
1115            raise ValueError(
1116                "Expected either inject_into or header to be set for ApiKeyAuthenticator"
1117            )
1118
1119        if model.inject_into is not None and model.header is not None:
1120            raise ValueError(
1121                "inject_into and header cannot be set both for ApiKeyAuthenticator - remove the deprecated header option"
1122            )
1123
1124        if token_provider is not None and model.api_token != "":
1125            raise ValueError(
1126                "If token_provider is set, api_token is ignored and has to be set to empty string."
1127            )
1128
1129        request_option = (
1130            self._create_component_from_model(
1131                model.inject_into, config, parameters=model.parameters or {}
1132            )
1133            if model.inject_into
1134            else RequestOption(
1135                inject_into=RequestOptionType.header,
1136                field_name=model.header or "",
1137                parameters=model.parameters or {},
1138            )
1139        )
1140
1141        return ApiKeyAuthenticator(
1142            token_provider=(
1143                token_provider
1144                if token_provider is not None
1145                else InterpolatedStringTokenProvider(
1146                    api_token=model.api_token or "",
1147                    config=config,
1148                    parameters=model.parameters or {},
1149                )
1150            ),
1151            request_option=request_option,
1152            config=config,
1153            parameters=model.parameters or {},
1154        )
1155
1156    def create_legacy_to_per_partition_state_migration(
1157        self,
1158        model: LegacyToPerPartitionStateMigrationModel,
1159        config: Mapping[str, Any],
1160        declarative_stream: DeclarativeStreamModel,
1161    ) -> LegacyToPerPartitionStateMigration:
1162        retriever = declarative_stream.retriever
1163        if not isinstance(retriever, (SimpleRetrieverModel, AsyncRetrieverModel)):
1164            raise ValueError(
1165                f"LegacyToPerPartitionStateMigrations can only be applied on a DeclarativeStream with a SimpleRetriever or AsyncRetriever. Got {type(retriever)}"
1166            )
1167        partition_router = retriever.partition_router
1168        if not isinstance(
1169            partition_router,
1170            (
1171                SubstreamPartitionRouterModel,
1172                CustomPartitionRouterModel,
1173                UnionPartitionRouterModel,
1174            ),
1175        ):
1176            raise ValueError(
1177                f"LegacyToPerPartitionStateMigrations can only be applied on a SimpleRetriever with a SubstreamPartitionRouter, UnionPartitionRouter or CustomPartitionRouter. Got {type(partition_router)}"
1178            )
1179        if not isinstance(partition_router, UnionPartitionRouterModel) and not hasattr(
1180            partition_router, "parent_stream_configs"
1181        ):
1182            raise ValueError(
1183                "LegacyToPerPartitionStateMigrations can only be applied with a parent stream configuration."
1184            )
1185
1186        if not hasattr(declarative_stream, "incremental_sync"):
1187            raise ValueError(
1188                "LegacyToPerPartitionStateMigrations can only be applied with an incremental_sync configuration."
1189            )
1190
1191        return LegacyToPerPartitionStateMigration(
1192            partition_router,  # type: ignore # was already checked above
1193            declarative_stream.incremental_sync,  # type: ignore # was already checked. Migration can be applied only to incremental streams.
1194            config,
1195            declarative_stream.parameters,  # type: ignore # different type is expected here Mapping[str, Any], got Dict[str, Any]
1196        )
1197
1198    def create_session_token_authenticator(
1199        self, model: SessionTokenAuthenticatorModel, config: Config, name: str, **kwargs: Any
1200    ) -> Union[ApiKeyAuthenticator, BearerAuthenticator]:
1201        decoder = (
1202            self._create_component_from_model(model=model.decoder, config=config)
1203            if model.decoder
1204            else JsonDecoder(parameters={})
1205        )
1206        login_requester = self._create_component_from_model(
1207            model=model.login_requester,
1208            config=config,
1209            name=f"{name}_login_requester",
1210            decoder=decoder,
1211        )
1212        token_provider = SessionTokenProvider(
1213            login_requester=login_requester,
1214            session_token_path=model.session_token_path,
1215            expiration_duration=parse_duration(model.expiration_duration)
1216            if model.expiration_duration
1217            else None,
1218            parameters=model.parameters or {},
1219            message_repository=self._message_repository,
1220            decoder=decoder,
1221        )
1222        if model.request_authentication.type == "Bearer":
1223            return ModelToComponentFactory.create_bearer_authenticator(
1224                BearerAuthenticatorModel(type="BearerAuthenticator", api_token=""),  # type: ignore # $parameters has a default value
1225                config,
1226                token_provider=token_provider,
1227            )
1228        else:
1229            # Get the api_token template if specified, default to just the session token
1230            api_token_template = (
1231                getattr(model.request_authentication, "api_token", None) or "{{ session_token }}"
1232            )
1233            final_token_provider: TokenProvider = InterpolatedSessionTokenProvider(
1234                config=config,
1235                api_token=api_token_template,
1236                session_token_provider=token_provider,
1237                parameters=model.parameters or {},
1238            )
1239            return self.create_api_key_authenticator(
1240                ApiKeyAuthenticatorModel(
1241                    type="ApiKeyAuthenticator",
1242                    api_token="",
1243                    inject_into=model.request_authentication.inject_into,
1244                ),  # type: ignore # $parameters and headers default to None
1245                config=config,
1246                token_provider=final_token_provider,
1247            )
1248
1249    @staticmethod
1250    def create_basic_http_authenticator(
1251        model: BasicHttpAuthenticatorModel, config: Config, **kwargs: Any
1252    ) -> BasicHttpAuthenticator:
1253        return BasicHttpAuthenticator(
1254            password=model.password or "",
1255            username=model.username,
1256            config=config,
1257            parameters=model.parameters or {},
1258        )
1259
1260    @staticmethod
1261    def create_bearer_authenticator(
1262        model: BearerAuthenticatorModel,
1263        config: Config,
1264        token_provider: Optional[TokenProvider] = None,
1265        **kwargs: Any,
1266    ) -> BearerAuthenticator:
1267        if token_provider is not None and model.api_token != "":
1268            raise ValueError(
1269                "If token_provider is set, api_token is ignored and has to be set to empty string."
1270            )
1271        return BearerAuthenticator(
1272            token_provider=(
1273                token_provider
1274                if token_provider is not None
1275                else InterpolatedStringTokenProvider(
1276                    api_token=model.api_token or "",
1277                    config=config,
1278                    parameters=model.parameters or {},
1279                )
1280            ),
1281            config=config,
1282            parameters=model.parameters or {},
1283        )
1284
1285    @staticmethod
1286    def create_dynamic_stream_check_config(
1287        model: DynamicStreamCheckConfigModel, config: Config, **kwargs: Any
1288    ) -> DynamicStreamCheckConfig:
1289        return DynamicStreamCheckConfig(
1290            dynamic_stream_name=model.dynamic_stream_name,
1291            stream_count=model.stream_count,
1292        )
1293
1294    def create_check_stream(
1295        self, model: CheckStreamModel, config: Config, **kwargs: Any
1296    ) -> CheckStream:
1297        if model.dynamic_streams_check_configs is None and model.stream_names is None:
1298            raise ValueError(
1299                "Expected either stream_names or dynamic_streams_check_configs to be set for CheckStream"
1300            )
1301
1302        dynamic_streams_check_configs = (
1303            [
1304                self._create_component_from_model(model=dynamic_stream_check_config, config=config)
1305                for dynamic_stream_check_config in model.dynamic_streams_check_configs
1306            ]
1307            if model.dynamic_streams_check_configs
1308            else []
1309        )
1310
1311        # `model.config_overrides` is deliberately not read here. The source applies it around the whole
1312        # check operation (`ConcurrentDeclarativeSource._config_overridden_for_check`), which is what makes
1313        # it work for every checker type rather than only this one. Do not wire it in a second time.
1314        return CheckStream(
1315            stream_names=model.stream_names or [],
1316            dynamic_streams_check_configs=dynamic_streams_check_configs,
1317            parameters={},
1318        )
1319
1320    @staticmethod
1321    def create_check_dynamic_stream(
1322        model: CheckDynamicStreamModel, config: Config, **kwargs: Any
1323    ) -> CheckDynamicStream:
1324        assert model.use_check_availability is not None  # for mypy
1325
1326        use_check_availability = model.use_check_availability
1327
1328        # See `create_check_stream`: `model.config_overrides` is applied by the source, not here.
1329        return CheckDynamicStream(
1330            stream_count=model.stream_count,
1331            use_check_availability=use_check_availability,
1332            parameters={},
1333        )
1334
1335    def create_composite_error_handler(
1336        self, model: CompositeErrorHandlerModel, config: Config, **kwargs: Any
1337    ) -> CompositeErrorHandler:
1338        error_handlers = [
1339            self._create_component_from_model(model=error_handler_model, config=config)
1340            for error_handler_model in model.error_handlers
1341        ]
1342        return CompositeErrorHandler(
1343            error_handlers=error_handlers, parameters=model.parameters or {}
1344        )
1345
1346    @staticmethod
1347    def create_concurrency_level(
1348        model: ConcurrencyLevelModel, config: Config, **kwargs: Any
1349    ) -> ConcurrencyLevel:
1350        return ConcurrencyLevel(
1351            default_concurrency=model.default_concurrency,
1352            max_concurrency=model.max_concurrency,
1353            config=config,
1354            parameters={},
1355        )
1356
1357    @staticmethod
1358    def apply_stream_state_migrations(
1359        stream_state_migrations: List[Any] | None, stream_state: MutableMapping[str, Any]
1360    ) -> MutableMapping[str, Any]:
1361        if stream_state_migrations:
1362            for state_migration in stream_state_migrations:
1363                if state_migration.should_migrate(stream_state):
1364                    # The state variable is expected to be mutable but the migrate method returns an immutable mapping.
1365                    stream_state = dict(state_migration.migrate(stream_state))
1366        return stream_state
1367
1368    def create_concurrent_cursor_from_datetime_based_cursor(
1369        self,
1370        model_type: Type[BaseModel],
1371        component_definition: ComponentDefinition,
1372        stream_name: str,
1373        stream_namespace: Optional[str],
1374        stream_state: MutableMapping[str, Any],
1375        config: Config,
1376        message_repository: Optional[MessageRepository] = None,
1377        runtime_lookback_window: Optional[datetime.timedelta] = None,
1378        **kwargs: Any,
1379    ) -> ConcurrentCursor:
1380        component_type = component_definition.get("type")
1381        if component_definition.get("type") != model_type.__name__:
1382            raise ValueError(
1383                f"Expected manifest component of type {model_type.__name__}, but received {component_type} instead"
1384            )
1385
1386        # FIXME the interfaces of the concurrent cursor are kind of annoying as they take a `ComponentDefinition` instead of the actual model. This was done because the ConcurrentDeclarativeSource didn't have access to the models [here for example](https://github.com/airbytehq/airbyte-python-cdk/blob/f525803b3fec9329e4cc8478996a92bf884bfde9/airbyte_cdk/sources/declarative/concurrent_declarative_source.py#L354C54-L354C91). So now we have two cases:
1387        # * The ComponentDefinition comes from model.__dict__ in which case we have `parameters`
1388        # * The ComponentDefinition comes from the manifest as a dict in which case we have `$parameters`
1389        # We should change those interfaces to use the model once we clean up the code in CDS at which point the parameter propagation should happen as part of the ModelToComponentFactory.
1390        if "$parameters" not in component_definition and "parameters" in component_definition:
1391            component_definition["$parameters"] = component_definition.get("parameters")  # type: ignore  # This is a dict
1392        datetime_based_cursor_model = model_type.parse_obj(component_definition)
1393
1394        if not isinstance(datetime_based_cursor_model, DatetimeBasedCursorModel):
1395            raise ValueError(
1396                f"Expected {model_type.__name__} component, but received {datetime_based_cursor_model.__class__.__name__}"
1397            )
1398
1399        model_parameters = datetime_based_cursor_model.parameters or {}
1400
1401        cursor_field = self._get_catalog_defined_cursor_field(
1402            stream_name=stream_name,
1403            allow_catalog_defined_cursor_field=datetime_based_cursor_model.allow_catalog_defined_cursor_field
1404            or False,
1405        )
1406
1407        if not cursor_field:
1408            interpolated_cursor_field = InterpolatedString.create(
1409                datetime_based_cursor_model.cursor_field,
1410                parameters=model_parameters,
1411            )
1412            cursor_field = CursorField(
1413                cursor_field_key=interpolated_cursor_field.eval(config=config),
1414                supports_catalog_defined_cursor_field=datetime_based_cursor_model.allow_catalog_defined_cursor_field
1415                or False,
1416            )
1417
1418        interpolated_partition_field_start = InterpolatedString.create(
1419            datetime_based_cursor_model.partition_field_start or "start_time",
1420            parameters=model_parameters,
1421        )
1422        interpolated_partition_field_end = InterpolatedString.create(
1423            datetime_based_cursor_model.partition_field_end or "end_time",
1424            parameters=model_parameters,
1425        )
1426
1427        slice_boundary_fields = (
1428            interpolated_partition_field_start.eval(config=config),
1429            interpolated_partition_field_end.eval(config=config),
1430        )
1431
1432        datetime_format = datetime_based_cursor_model.datetime_format
1433
1434        cursor_granularity = (
1435            parse_duration(datetime_based_cursor_model.cursor_granularity)
1436            if datetime_based_cursor_model.cursor_granularity
1437            else None
1438        )
1439
1440        lookback_window = None
1441        interpolated_lookback_window = (
1442            InterpolatedString.create(
1443                datetime_based_cursor_model.lookback_window,
1444                parameters=model_parameters,
1445            )
1446            if datetime_based_cursor_model.lookback_window
1447            else None
1448        )
1449        if interpolated_lookback_window:
1450            evaluated_lookback_window = interpolated_lookback_window.eval(config=config)
1451            if evaluated_lookback_window:
1452                lookback_window = parse_duration(evaluated_lookback_window)
1453
1454        connector_state_converter: DateTimeStreamStateConverter
1455        connector_state_converter = CustomFormatConcurrentStreamStateConverter(
1456            datetime_format=datetime_format,
1457            input_datetime_formats=datetime_based_cursor_model.cursor_datetime_formats,
1458            is_sequential_state=True,  # ConcurrentPerPartitionCursor only works with sequential state
1459            cursor_granularity=cursor_granularity,
1460        )
1461
1462        # Adjusts the stream state by applying the runtime lookback window.
1463        # This is used to ensure correct state handling in case of failed partitions.
1464        stream_state_value = stream_state.get(cursor_field.cursor_field_key)
1465        if runtime_lookback_window and stream_state_value:
1466            new_stream_state = (
1467                connector_state_converter.parse_timestamp(stream_state_value)
1468                - runtime_lookback_window
1469            )
1470            stream_state[cursor_field.cursor_field_key] = connector_state_converter.output_format(
1471                new_stream_state
1472            )
1473
1474        start_date_runtime_value: Union[InterpolatedString, str, MinMaxDatetime]
1475        if isinstance(datetime_based_cursor_model.start_datetime, MinMaxDatetimeModel):
1476            start_date_runtime_value = self.create_min_max_datetime(
1477                model=datetime_based_cursor_model.start_datetime, config=config
1478            )
1479        else:
1480            start_date_runtime_value = datetime_based_cursor_model.start_datetime
1481
1482        end_date_runtime_value: Optional[Union[InterpolatedString, str, MinMaxDatetime]]
1483        if isinstance(datetime_based_cursor_model.end_datetime, MinMaxDatetimeModel):
1484            end_date_runtime_value = self.create_min_max_datetime(
1485                model=datetime_based_cursor_model.end_datetime, config=config
1486            )
1487        else:
1488            end_date_runtime_value = datetime_based_cursor_model.end_datetime
1489
1490        interpolated_start_date = MinMaxDatetime.create(
1491            interpolated_string_or_min_max_datetime=start_date_runtime_value,
1492            parameters=datetime_based_cursor_model.parameters,
1493        )
1494        interpolated_end_date = (
1495            None
1496            if not end_date_runtime_value
1497            else MinMaxDatetime.create(
1498                end_date_runtime_value, datetime_based_cursor_model.parameters
1499            )
1500        )
1501
1502        # If datetime format is not specified then start/end datetime should inherit it from the stream slicer
1503        if not interpolated_start_date.datetime_format:
1504            interpolated_start_date.datetime_format = datetime_format
1505        if interpolated_end_date and not interpolated_end_date.datetime_format:
1506            interpolated_end_date.datetime_format = datetime_format
1507
1508        start_date = interpolated_start_date.get_datetime(config=config)
1509        end_date_provider = (
1510            partial(interpolated_end_date.get_datetime, config)
1511            if interpolated_end_date
1512            else connector_state_converter.get_end_provider()
1513        )
1514
1515        if (
1516            datetime_based_cursor_model.step and not datetime_based_cursor_model.cursor_granularity
1517        ) or (
1518            not datetime_based_cursor_model.step and datetime_based_cursor_model.cursor_granularity
1519        ):
1520            raise ValueError(
1521                f"If step is defined, cursor_granularity should be as well and vice-versa. "
1522                f"Right now, step is `{datetime_based_cursor_model.step}` and cursor_granularity is `{datetime_based_cursor_model.cursor_granularity}`"
1523            )
1524
1525        # When step is not defined, default to a step size from the starting date to the present moment
1526        step_length = datetime.timedelta.max
1527        interpolated_step = (
1528            InterpolatedString.create(
1529                datetime_based_cursor_model.step,
1530                parameters=model_parameters,
1531            )
1532            if datetime_based_cursor_model.step
1533            else None
1534        )
1535        if interpolated_step:
1536            evaluated_step = interpolated_step.eval(config)
1537            if evaluated_step:
1538                step_length = parse_duration(evaluated_step)
1539
1540        clamping_strategy: ClampingStrategy = NoClamping()
1541        if datetime_based_cursor_model.clamping:
1542            # While it is undesirable to interpolate within the model factory (as opposed to at runtime),
1543            # it is still better than shifting interpolation low-code concept into the ConcurrentCursor runtime
1544            # object which we want to keep agnostic of being low-code
1545            target = InterpolatedString(
1546                string=datetime_based_cursor_model.clamping.target,
1547                parameters=model_parameters,
1548            )
1549            evaluated_target = target.eval(config=config)
1550            match evaluated_target:
1551                case "DAY":
1552                    clamping_strategy = DayClampingStrategy()
1553                    end_date_provider = ClampingEndProvider(
1554                        DayClampingStrategy(is_ceiling=False),
1555                        end_date_provider,  # type: ignore  # Having issues w/ inspection for GapType and CursorValueType as shown in existing tests. Confirmed functionality is working in practice
1556                        granularity=cursor_granularity or datetime.timedelta(seconds=1),
1557                    )
1558                case "WEEK":
1559                    if (
1560                        not datetime_based_cursor_model.clamping.target_details
1561                        or "weekday" not in datetime_based_cursor_model.clamping.target_details
1562                    ):
1563                        raise ValueError(
1564                            "Given WEEK clamping, weekday needs to be provided as target_details"
1565                        )
1566                    weekday = self._assemble_weekday(
1567                        datetime_based_cursor_model.clamping.target_details["weekday"]
1568                    )
1569                    clamping_strategy = WeekClampingStrategy(weekday)
1570                    end_date_provider = ClampingEndProvider(
1571                        WeekClampingStrategy(weekday, is_ceiling=False),
1572                        end_date_provider,  # type: ignore  # Having issues w/ inspection for GapType and CursorValueType as shown in existing tests. Confirmed functionality is working in practice
1573                        granularity=cursor_granularity or datetime.timedelta(days=1),
1574                    )
1575                case "MONTH":
1576                    clamping_strategy = MonthClampingStrategy()
1577                    end_date_provider = ClampingEndProvider(
1578                        MonthClampingStrategy(is_ceiling=False),
1579                        end_date_provider,  # type: ignore  # Having issues w/ inspection for GapType and CursorValueType as shown in existing tests. Confirmed functionality is working in practice
1580                        granularity=cursor_granularity or datetime.timedelta(days=1),
1581                    )
1582                case _:
1583                    raise ValueError(
1584                        f"Invalid clamping target {evaluated_target}, expected DAY, WEEK, MONTH"
1585                    )
1586
1587        return ConcurrentCursor(
1588            stream_name=stream_name,
1589            stream_namespace=stream_namespace,
1590            stream_state=stream_state,
1591            message_repository=message_repository or self._message_repository,
1592            connector_state_manager=self._connector_state_manager,
1593            connector_state_converter=connector_state_converter,
1594            cursor_field=cursor_field,
1595            slice_boundary_fields=slice_boundary_fields,
1596            start=start_date,  # type: ignore  # Having issues w/ inspection for GapType and CursorValueType as shown in existing tests. Confirmed functionality is working in practice
1597            end_provider=end_date_provider,  # type: ignore  # Having issues w/ inspection for GapType and CursorValueType as shown in existing tests. Confirmed functionality is working in practice
1598            lookback_window=lookback_window,
1599            slice_range=step_length,
1600            cursor_granularity=cursor_granularity,
1601            clamping_strategy=clamping_strategy,
1602        )
1603
1604    def create_concurrent_cursor_from_incrementing_count_cursor(
1605        self,
1606        model_type: Type[BaseModel],
1607        component_definition: ComponentDefinition,
1608        stream_name: str,
1609        stream_namespace: Optional[str],
1610        stream_state: MutableMapping[str, Any],
1611        config: Config,
1612        message_repository: Optional[MessageRepository] = None,
1613        **kwargs: Any,
1614    ) -> ConcurrentCursor:
1615        component_type = component_definition.get("type")
1616        if component_definition.get("type") != model_type.__name__:
1617            raise ValueError(
1618                f"Expected manifest component of type {model_type.__name__}, but received {component_type} instead"
1619            )
1620
1621        incrementing_count_cursor_model = model_type.parse_obj(component_definition)
1622
1623        if not isinstance(incrementing_count_cursor_model, IncrementingCountCursorModel):
1624            raise ValueError(
1625                f"Expected {model_type.__name__} component, but received {incrementing_count_cursor_model.__class__.__name__}"
1626            )
1627
1628        start_value: Union[int, str, None] = incrementing_count_cursor_model.start_value
1629        # Pydantic Union type coercion can convert int 0 to string '0' depending on Union order.
1630        # We need to handle both int and str representations of numeric values.
1631        # Evaluate the InterpolatedString and convert to int for the ConcurrentCursor.
1632        if start_value is not None:
1633            interpolated_start_value = InterpolatedString.create(
1634                str(start_value),  # Ensure we pass a string to InterpolatedString.create
1635                parameters=incrementing_count_cursor_model.parameters or {},
1636            )
1637            evaluated_start_value: int = int(interpolated_start_value.eval(config=config))
1638        else:
1639            evaluated_start_value = 0
1640
1641        cursor_field = self._get_catalog_defined_cursor_field(
1642            stream_name=stream_name,
1643            allow_catalog_defined_cursor_field=incrementing_count_cursor_model.allow_catalog_defined_cursor_field
1644            or False,
1645        )
1646
1647        if not cursor_field:
1648            interpolated_cursor_field = InterpolatedString.create(
1649                incrementing_count_cursor_model.cursor_field,
1650                parameters=incrementing_count_cursor_model.parameters or {},
1651            )
1652            cursor_field = CursorField(
1653                cursor_field_key=interpolated_cursor_field.eval(config=config),
1654                supports_catalog_defined_cursor_field=incrementing_count_cursor_model.allow_catalog_defined_cursor_field
1655                or False,
1656            )
1657
1658        connector_state_converter = IncrementingCountStreamStateConverter(
1659            is_sequential_state=True,  # ConcurrentPerPartitionCursor only works with sequential state
1660        )
1661
1662        return ConcurrentCursor(
1663            stream_name=stream_name,
1664            stream_namespace=stream_namespace,
1665            stream_state=stream_state,
1666            message_repository=message_repository or self._message_repository,
1667            connector_state_manager=self._connector_state_manager,
1668            connector_state_converter=connector_state_converter,
1669            cursor_field=cursor_field,
1670            slice_boundary_fields=None,
1671            start=evaluated_start_value,  # type: ignore  # Having issues w/ inspection for GapType and CursorValueType as shown in existing tests. Confirmed functionality is working in practice
1672            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
1673        )
1674
1675    def _assemble_weekday(self, weekday: str) -> Weekday:
1676        match weekday:
1677            case "MONDAY":
1678                return Weekday.MONDAY
1679            case "TUESDAY":
1680                return Weekday.TUESDAY
1681            case "WEDNESDAY":
1682                return Weekday.WEDNESDAY
1683            case "THURSDAY":
1684                return Weekday.THURSDAY
1685            case "FRIDAY":
1686                return Weekday.FRIDAY
1687            case "SATURDAY":
1688                return Weekday.SATURDAY
1689            case "SUNDAY":
1690                return Weekday.SUNDAY
1691            case _:
1692                raise ValueError(f"Unknown weekday {weekday}")
1693
1694    def create_concurrent_cursor_from_perpartition_cursor(
1695        self,
1696        state_manager: ConnectorStateManager,
1697        model_type: Type[BaseModel],
1698        component_definition: ComponentDefinition,
1699        stream_name: str,
1700        stream_namespace: Optional[str],
1701        config: Config,
1702        stream_state: MutableMapping[str, Any],
1703        partition_router: PartitionRouter,
1704        attempt_to_create_cursor_if_not_provided: bool = False,
1705        **kwargs: Any,
1706    ) -> ConcurrentPerPartitionCursor:
1707        component_type = component_definition.get("type")
1708        if component_definition.get("type") != model_type.__name__:
1709            raise ValueError(
1710                f"Expected manifest component of type {model_type.__name__}, but received {component_type} instead"
1711            )
1712
1713        # FIXME the interfaces of the concurrent cursor are kind of annoying as they take a `ComponentDefinition` instead of the actual model. This was done because the ConcurrentDeclarativeSource didn't have access to the models [here for example](https://github.com/airbytehq/airbyte-python-cdk/blob/f525803b3fec9329e4cc8478996a92bf884bfde9/airbyte_cdk/sources/declarative/concurrent_declarative_source.py#L354C54-L354C91). So now we have two cases:
1714        # * The ComponentDefinition comes from model.__dict__ in which case we have `parameters`
1715        # * The ComponentDefinition comes from the manifest as a dict in which case we have `$parameters`
1716        # We should change those interfaces to use the model once we clean up the code in CDS at which point the parameter propagation should happen as part of the ModelToComponentFactory.
1717        if "$parameters" not in component_definition and "parameters" in component_definition:
1718            component_definition["$parameters"] = component_definition.get("parameters")  # type: ignore  # This is a dict
1719        datetime_based_cursor_model = model_type.parse_obj(component_definition)
1720
1721        if not isinstance(datetime_based_cursor_model, DatetimeBasedCursorModel):
1722            raise ValueError(
1723                f"Expected {model_type.__name__} component, but received {datetime_based_cursor_model.__class__.__name__}"
1724            )
1725
1726        cursor_field = self._get_catalog_defined_cursor_field(
1727            stream_name=stream_name,
1728            allow_catalog_defined_cursor_field=datetime_based_cursor_model.allow_catalog_defined_cursor_field
1729            or False,
1730        )
1731
1732        if not cursor_field:
1733            interpolated_cursor_field = InterpolatedString.create(
1734                datetime_based_cursor_model.cursor_field,
1735                # FIXME the interfaces of the concurrent cursor are kind of annoying as they take a `ComponentDefinition` instead of the actual model. This was done because the ConcurrentDeclarativeSource didn't have access to the models [here for example](https://github.com/airbytehq/airbyte-python-cdk/blob/f525803b3fec9329e4cc8478996a92bf884bfde9/airbyte_cdk/sources/declarative/concurrent_declarative_source.py#L354C54-L354C91). So now we have two cases:
1736                # * The ComponentDefinition comes from model.__dict__ in which case we have `parameters`
1737                # * The ComponentDefinition comes from the manifest as a dict in which case we have `$parameters`
1738                # We should change those interfaces to use the model once we clean up the code in CDS at which point the parameter propagation should happen as part of the ModelToComponentFactory.
1739                parameters=datetime_based_cursor_model.parameters or {},
1740            )
1741            cursor_field = CursorField(
1742                cursor_field_key=interpolated_cursor_field.eval(config=config),
1743                supports_catalog_defined_cursor_field=datetime_based_cursor_model.allow_catalog_defined_cursor_field
1744                or False,
1745            )
1746
1747        datetime_format = datetime_based_cursor_model.datetime_format
1748
1749        cursor_granularity = (
1750            parse_duration(datetime_based_cursor_model.cursor_granularity)
1751            if datetime_based_cursor_model.cursor_granularity
1752            else None
1753        )
1754
1755        connector_state_converter: DateTimeStreamStateConverter
1756        connector_state_converter = CustomFormatConcurrentStreamStateConverter(
1757            datetime_format=datetime_format,
1758            input_datetime_formats=datetime_based_cursor_model.cursor_datetime_formats,
1759            is_sequential_state=True,  # ConcurrentPerPartitionCursor only works with sequential state
1760            cursor_granularity=cursor_granularity,
1761        )
1762
1763        # Create the cursor factory
1764        cursor_factory = ConcurrentCursorFactory(
1765            partial(
1766                self.create_concurrent_cursor_from_datetime_based_cursor,
1767                state_manager=state_manager,
1768                model_type=model_type,
1769                component_definition=component_definition,
1770                stream_name=stream_name,
1771                stream_namespace=stream_namespace,
1772                config=config,
1773                message_repository=NoopMessageRepository(),
1774            )
1775        )
1776
1777        # Per-partition state doesn't make sense for GroupingPartitionRouter, so force the global state
1778        use_global_cursor = isinstance(
1779            partition_router, GroupingPartitionRouter
1780        ) or component_definition.get("global_substream_cursor", False)
1781
1782        # Return the concurrent cursor and state converter
1783        return ConcurrentPerPartitionCursor(
1784            cursor_factory=cursor_factory,
1785            partition_router=partition_router,
1786            stream_name=stream_name,
1787            stream_namespace=stream_namespace,
1788            stream_state=stream_state,
1789            message_repository=self._message_repository,  # type: ignore
1790            connector_state_manager=state_manager,
1791            connector_state_converter=connector_state_converter,
1792            cursor_field=cursor_field,
1793            use_global_cursor=use_global_cursor,
1794            attempt_to_create_cursor_if_not_provided=attempt_to_create_cursor_if_not_provided,
1795        )
1796
1797    @staticmethod
1798    def create_constant_backoff_strategy(
1799        model: ConstantBackoffStrategyModel, config: Config, **kwargs: Any
1800    ) -> ConstantBackoffStrategy:
1801        ModelToComponentFactory._validate_jitter_range(model.jitter_range_in_seconds)
1802        return ConstantBackoffStrategy(
1803            backoff_time_in_seconds=model.backoff_time_in_seconds,
1804            jitter_range_in_seconds=model.jitter_range_in_seconds,
1805            config=config,
1806            parameters=model.parameters or {},
1807        )
1808
1809    @staticmethod
1810    def _validate_jitter_range(jitter_range_in_seconds: Optional[float]) -> None:
1811        if jitter_range_in_seconds is not None and jitter_range_in_seconds < 0:
1812            raise ValueError("jitter_range_in_seconds must be greater than or equal to 0")
1813
1814    def create_cursor_pagination(
1815        self, model: CursorPaginationModel, config: Config, decoder: Decoder, **kwargs: Any
1816    ) -> CursorPaginationStrategy:
1817        if isinstance(decoder, PaginationDecoderDecorator):
1818            inner_decoder = decoder.decoder
1819        else:
1820            inner_decoder = decoder
1821            decoder = PaginationDecoderDecorator(decoder=decoder)
1822
1823        if self._is_supported_decoder_for_pagination(inner_decoder):
1824            decoder_to_use = decoder
1825        else:
1826            raise ValueError(
1827                self._UNSUPPORTED_DECODER_ERROR.format(decoder_type=type(inner_decoder))
1828            )
1829
1830        # Pydantic v1 Union type coercion can convert int to string depending on Union order.
1831        # If page_size is a string that represents an integer (not an interpolation), convert it back.
1832        page_size = model.page_size
1833        if isinstance(page_size, str) and page_size.isdigit():
1834            page_size = int(page_size)
1835
1836        return CursorPaginationStrategy(
1837            cursor_value=model.cursor_value,
1838            decoder=decoder_to_use,
1839            page_size=page_size,
1840            stop_condition=model.stop_condition,
1841            config=config,
1842            parameters=model.parameters or {},
1843        )
1844
1845    def create_custom_component(self, model: Any, config: Config, **kwargs: Any) -> Any:
1846        """
1847        Generically creates a custom component based on the model type and a class_name reference to the custom Python class being
1848        instantiated. Only the model's additional properties that match the custom class definition are passed to the constructor
1849        :param model: The Pydantic model of the custom component being created
1850        :param config: The custom defined connector config
1851        :return: The declarative component built from the Pydantic model to be used at runtime
1852        """
1853        # Instantiating a custom component means importing and executing arbitrary code referenced
1854        # by `class_name`. Manifests supplied by a caller, whether through the config or directly to
1855        # the manifest server, are untrusted input and could point `class_name` at any importable
1856        # callable, so they honor the same `AIRBYTE_ENABLE_UNSAFE_CODE` gate as injected
1857        # `components.py` code. Manifests bundled in a published connector image are trusted and may
1858        # always use their bundled custom components.
1859        manifest_is_untrusted = not self._custom_components_trusted or bool(
1860            config.get(INJECTED_MANIFEST)
1861        )
1862        if manifest_is_untrusted and not custom_code_execution_permitted():
1863            raise AirbyteCustomCodeNotPermittedError
1864
1865        custom_component_class = self._get_class_from_fully_qualified_class_name(model.class_name)
1866        component_fields = get_type_hints(custom_component_class)
1867        model_args = model.dict()
1868        model_args["config"] = config
1869
1870        # There are cases where a parent component will pass arguments to a child component via kwargs. When there are field collisions
1871        # we defer to these arguments over the component's definition
1872        for key, arg in kwargs.items():
1873            model_args[key] = arg
1874
1875        # Pydantic is unable to parse a custom component's fields that are subcomponents into models because their fields and types are not
1876        # defined in the schema. The fields and types are defined within the Python class implementation. Pydantic can only parse down to
1877        # the custom component and this code performs a second parse to convert the sub-fields first into models, then declarative components
1878        for model_field, model_value in model_args.items():
1879            # If a custom component field doesn't have a type set, we try to use the type hints to infer the type
1880            if (
1881                isinstance(model_value, dict)
1882                and "type" not in model_value
1883                and model_field in component_fields
1884            ):
1885                derived_type = self._derive_component_type_from_type_hints(
1886                    component_fields.get(model_field)
1887                )
1888                if derived_type:
1889                    model_value["type"] = derived_type
1890
1891            if self._is_component(model_value):
1892                model_args[model_field] = self._create_nested_component(
1893                    model,
1894                    model_field,
1895                    model_value,
1896                    config,
1897                    **kwargs,
1898                )
1899            elif isinstance(model_value, list):
1900                vals = []
1901                for v in model_value:
1902                    if isinstance(v, dict) and "type" not in v and model_field in component_fields:
1903                        derived_type = self._derive_component_type_from_type_hints(
1904                            component_fields.get(model_field)
1905                        )
1906                        if derived_type:
1907                            v["type"] = derived_type
1908                    if self._is_component(v):
1909                        vals.append(
1910                            self._create_nested_component(
1911                                model,
1912                                model_field,
1913                                v,
1914                                config,
1915                                **kwargs,
1916                            )
1917                        )
1918                    else:
1919                        vals.append(v)
1920                model_args[model_field] = vals
1921
1922        kwargs = {
1923            class_field: model_args[class_field]
1924            for class_field in component_fields.keys()
1925            if class_field in model_args
1926        }
1927
1928        if "api_budget" in component_fields and kwargs.get("api_budget") is None:
1929            kwargs["api_budget"] = self._api_budget
1930
1931        return custom_component_class(**kwargs)
1932
1933    @staticmethod
1934    def _get_class_from_fully_qualified_class_name(
1935        full_qualified_class_name: str,
1936    ) -> Any:
1937        """Get a class from its fully qualified name.
1938
1939        If a custom components module is needed, we assume it is already registered - probably
1940        as `source_declarative_manifest.components` or `components`.
1941
1942        Args:
1943            full_qualified_class_name (str): The fully qualified name of the class (e.g., "module.ClassName").
1944
1945        Returns:
1946            Any: The class object.
1947
1948        Raises:
1949            ValueError: If the class cannot be loaded.
1950        """
1951        split = full_qualified_class_name.split(".")
1952        module_name_full = ".".join(split[:-1])
1953        class_name = split[-1]
1954
1955        try:
1956            module_ref = importlib.import_module(module_name_full)
1957        except ModuleNotFoundError as e:
1958            if split[0] == "source_declarative_manifest":
1959                # During testing, the modules containing the custom components are not moved to source_declarative_manifest. In order to run the test, add the source folder to your PYTHONPATH or add it runtime using sys.path.append
1960                try:
1961                    import os
1962
1963                    module_name_with_source_declarative_manifest = ".".join(split[1:-1])
1964                    module_ref = importlib.import_module(
1965                        module_name_with_source_declarative_manifest
1966                    )
1967                except ModuleNotFoundError:
1968                    raise ValueError(f"Could not load module `{module_name_full}`.") from e
1969            else:
1970                raise ValueError(f"Could not load module `{module_name_full}`.") from e
1971
1972        try:
1973            return getattr(module_ref, class_name)
1974        except AttributeError as e:
1975            raise ValueError(
1976                f"Could not load class `{class_name}` from module `{module_name_full}`.",
1977            ) from e
1978
1979    @staticmethod
1980    def _derive_component_type_from_type_hints(field_type: Any) -> Optional[str]:
1981        interface = field_type
1982        while True:
1983            origin = get_origin(interface)
1984            if origin:
1985                # Unnest types until we reach the raw type
1986                # List[T] -> T
1987                # Optional[List[T]] -> T
1988                args = get_args(interface)
1989                interface = args[0]
1990            else:
1991                break
1992        if isinstance(interface, type) and not ModelToComponentFactory.is_builtin_type(interface):
1993            return interface.__name__
1994        return None
1995
1996    @staticmethod
1997    def is_builtin_type(cls: Optional[Type[Any]]) -> bool:
1998        if not cls:
1999            return False
2000        return cls.__module__ == "builtins"
2001
2002    @staticmethod
2003    def _extract_missing_parameters(error: TypeError) -> List[str]:
2004        parameter_search = re.search(r"keyword-only.*:\s(.*)", str(error))
2005        if parameter_search:
2006            return re.findall(r"\'(.+?)\'", parameter_search.group(1))
2007        else:
2008            return []
2009
2010    def _create_nested_component(
2011        self, model: Any, model_field: str, model_value: Any, config: Config, **kwargs: Any
2012    ) -> Any:
2013        type_name = model_value.get("type", None)
2014        if not type_name:
2015            # If no type is specified, we can assume this is a dictionary object which can be returned instead of a subcomponent
2016            return model_value
2017
2018        model_type = self.TYPE_NAME_TO_MODEL.get(type_name, None)
2019        if model_type:
2020            parsed_model = model_type.parse_obj(model_value)
2021            try:
2022                # To improve usability of the language, certain fields are shared between components. This can come in the form of
2023                # a parent component passing some of its fields to a child component or the parent extracting fields from other child
2024                # components and passing it to others. One example is the DefaultPaginator referencing the HttpRequester url_base
2025                # while constructing a SimpleRetriever. However, custom components don't support this behavior because they are created
2026                # generically in create_custom_component(). This block allows developers to specify extra arguments in $parameters that
2027                # are needed by a component and could not be shared.
2028                model_constructor = self.PYDANTIC_MODEL_TO_CONSTRUCTOR.get(parsed_model.__class__)
2029                constructor_kwargs = inspect.getfullargspec(model_constructor).kwonlyargs
2030                model_parameters = model_value.get("$parameters", {})
2031                matching_parameters = {
2032                    kwarg: model_parameters[kwarg]
2033                    for kwarg in constructor_kwargs
2034                    if kwarg in model_parameters
2035                }
2036                matching_kwargs = {
2037                    kwarg: kwargs[kwarg] for kwarg in constructor_kwargs if kwarg in kwargs
2038                }
2039                return self._create_component_from_model(
2040                    model=parsed_model, config=config, **(matching_parameters | matching_kwargs)
2041                )
2042            except TypeError as error:
2043                missing_parameters = self._extract_missing_parameters(error)
2044                if missing_parameters:
2045                    raise ValueError(
2046                        f"Error creating component '{type_name}' with parent custom component {model.class_name}: Please provide "
2047                        + ", ".join(
2048                            (
2049                                f"{type_name}.$parameters.{parameter}"
2050                                for parameter in missing_parameters
2051                            )
2052                        )
2053                    )
2054                raise TypeError(
2055                    f"Error creating component '{type_name}' with parent custom component {model.class_name}: {error}"
2056                )
2057        else:
2058            raise ValueError(
2059                f"Error creating custom component {model.class_name}. Subcomponent creation has not been implemented for '{type_name}'"
2060            )
2061
2062    @staticmethod
2063    def _is_component(model_value: Any) -> bool:
2064        return isinstance(model_value, dict) and model_value.get("type") is not None
2065
2066    def create_default_stream(
2067        self, model: DeclarativeStreamModel, config: Config, is_parent: bool = False, **kwargs: Any
2068    ) -> AbstractStream:
2069        primary_key = model.primary_key.__root__ if model.primary_key else None
2070        self._migrate_state(model, config)
2071        self._warn_on_ineffective_incremental_dependency(model)
2072
2073        partition_router = self._build_stream_slicer_from_partition_router(
2074            model.retriever,
2075            config,
2076            stream_name=model.name,
2077            **kwargs,
2078        )
2079        concurrent_cursor = self._build_concurrent_cursor(model, partition_router, config)
2080        if model.incremental_sync and isinstance(model.incremental_sync, DatetimeBasedCursorModel):
2081            cursor_model: DatetimeBasedCursorModel = model.incremental_sync
2082
2083            end_time_option = (
2084                self._create_component_from_model(
2085                    cursor_model.end_time_option, config, parameters=cursor_model.parameters or {}
2086                )
2087                if cursor_model.end_time_option
2088                else None
2089            )
2090            start_time_option = (
2091                self._create_component_from_model(
2092                    cursor_model.start_time_option, config, parameters=cursor_model.parameters or {}
2093                )
2094                if cursor_model.start_time_option
2095                else None
2096            )
2097
2098            datetime_request_options_provider = DatetimeBasedRequestOptionsProvider(
2099                start_time_option=start_time_option,
2100                end_time_option=end_time_option,
2101                partition_field_start=cursor_model.partition_field_start,
2102                partition_field_end=cursor_model.partition_field_end,
2103                config=config,
2104                parameters=model.parameters or {},
2105            )
2106            request_options_provider = (
2107                datetime_request_options_provider
2108                if not isinstance(concurrent_cursor, ConcurrentPerPartitionCursor)
2109                else PerPartitionRequestOptionsProvider(
2110                    partition_router, datetime_request_options_provider
2111                )
2112            )
2113        elif model.incremental_sync and isinstance(
2114            model.incremental_sync, IncrementingCountCursorModel
2115        ):
2116            if isinstance(concurrent_cursor, ConcurrentPerPartitionCursor):
2117                raise ValueError(
2118                    "PerPartition does not support per partition states because switching to global state is time based"
2119                )
2120
2121            cursor_model: IncrementingCountCursorModel = model.incremental_sync  # type: ignore
2122
2123            start_time_option = (
2124                self._create_component_from_model(
2125                    cursor_model.start_value_option,  # type: ignore # mypy still thinks cursor_model of type DatetimeBasedCursor
2126                    config,
2127                    parameters=cursor_model.parameters or {},
2128                )
2129                if cursor_model.start_value_option  # type: ignore # mypy still thinks cursor_model of type DatetimeBasedCursor
2130                else None
2131            )
2132
2133            # The concurrent engine defaults the start/end fields on the slice to "start" and "end", but
2134            # the default DatetimeBasedRequestOptionsProvider() sets them to start_time/end_time
2135            partition_field_start = "start"
2136
2137            request_options_provider = DatetimeBasedRequestOptionsProvider(
2138                start_time_option=start_time_option,
2139                partition_field_start=partition_field_start,
2140                config=config,
2141                parameters=model.parameters or {},
2142            )
2143        else:
2144            request_options_provider = None
2145
2146        transformations = []
2147        if model.transformations:
2148            for transformation_model in model.transformations:
2149                transformations.append(
2150                    self._create_component_from_model(model=transformation_model, config=config)
2151                )
2152        file_uploader = None
2153        if model.file_uploader:
2154            file_uploader = self._create_component_from_model(
2155                model=model.file_uploader, config=config
2156            )
2157
2158        stream_slicer: ConcurrentStreamSlicer = (
2159            partition_router
2160            if isinstance(concurrent_cursor, FinalStateCursor)
2161            else concurrent_cursor
2162        )
2163
2164        retriever = self._create_component_from_model(
2165            model=model.retriever,
2166            config=config,
2167            name=model.name,
2168            primary_key=primary_key,
2169            request_options_provider=request_options_provider,
2170            stream_slicer=stream_slicer,
2171            partition_router=partition_router,
2172            has_stop_condition_cursor=self._is_stop_condition_on_cursor(model),
2173            is_client_side_incremental_sync=self._is_client_side_filtering_enabled(model),
2174            cursor=concurrent_cursor,
2175            transformations=transformations,
2176            file_uploader=file_uploader,
2177            incremental_sync=model.incremental_sync,
2178        )
2179        if isinstance(retriever, AsyncRetriever):
2180            stream_slicer = retriever.stream_slicer
2181
2182        schema_loader: SchemaLoader
2183        if model.schema_loader and isinstance(model.schema_loader, list):
2184            nested_schema_loaders = [
2185                self._create_component_from_model(model=nested_schema_loader, config=config)
2186                for nested_schema_loader in model.schema_loader
2187            ]
2188            schema_loader = CompositeSchemaLoader(
2189                schema_loaders=nested_schema_loaders, parameters={}
2190            )
2191        elif model.schema_loader:
2192            schema_loader = self._create_component_from_model(
2193                model=model.schema_loader,  # type: ignore # If defined, schema_loader is guaranteed not to be a list and will be one of the existing base models
2194                config=config,
2195            )
2196        else:
2197            options = model.parameters or {}
2198            if "name" not in options:
2199                options["name"] = model.name
2200            schema_loader = DefaultSchemaLoader(config=config, parameters=options)
2201        schema_loader = CachingSchemaLoaderDecorator(schema_loader)
2202
2203        stream_name = model.name or ""
2204        return DefaultStream(
2205            partition_generator=StreamSlicerPartitionGenerator(
2206                DeclarativePartitionFactory(
2207                    stream_name,
2208                    schema_loader,
2209                    retriever,
2210                    self._message_repository,
2211                ),
2212                stream_slicer,
2213                slice_limit=self._limit_slices_fetched,
2214            ),
2215            name=stream_name,
2216            json_schema=schema_loader.get_json_schema,
2217            primary_key=get_primary_key_from_stream(primary_key),
2218            cursor_field=(
2219                concurrent_cursor.cursor_field
2220                if hasattr(concurrent_cursor, "cursor_field")
2221                else None
2222            ),
2223            logger=logging.getLogger(f"airbyte.{stream_name}"),
2224            cursor=concurrent_cursor,
2225            supports_file_transfer=hasattr(model, "file_uploader") and bool(model.file_uploader),
2226        )
2227
2228    def _warn_on_ineffective_incremental_dependency(self, model: DeclarativeStreamModel) -> None:
2229        """
2230        `incremental_dependency: true` only takes effect when the substream defines its own
2231        `incremental_sync`: the parent cursor is persisted under the `parent_state` key of the
2232        substream's state, which is only emitted by incremental substreams. On a stream without
2233        `incremental_sync`, the setting is silently ignored and all parent records are re-read on
2234        every sync, so we warn about the misconfiguration instead.
2235        """
2236        if model.incremental_sync:
2237            return
2238
2239        partition_router = getattr(model.retriever, "partition_router", None)
2240        if not partition_router:
2241            return
2242
2243        routers = partition_router if isinstance(partition_router, list) else [partition_router]
2244        for router in routers:
2245            if isinstance(router, GroupingPartitionRouterModel):
2246                router = router.underlying_partition_router
2247            if isinstance(router, SubstreamPartitionRouterModel) and any(
2248                parent_stream_config.incremental_dependency
2249                for parent_stream_config in router.parent_stream_configs
2250            ):
2251                LOGGER.warning(
2252                    f"Stream `{model.name}` has `incremental_dependency: true` in its parent stream configuration but does not define `incremental_sync`. "
2253                    "The parent stream's cursor is only persisted in the state of an incremental substream, so this setting has no effect and all parent records will be re-read on every sync. "
2254                    "Define `incremental_sync` on this stream or remove `incremental_dependency`."
2255                )
2256                return
2257
2258    def _migrate_state(self, model: DeclarativeStreamModel, config: Config) -> None:
2259        stream_name = model.name or ""
2260        stream_state = self._connector_state_manager.get_stream_state(
2261            stream_name=stream_name, namespace=None
2262        )
2263        if model.state_migrations:
2264            state_transformations = [
2265                self._create_component_from_model(state_migration, config, declarative_stream=model)
2266                for state_migration in model.state_migrations
2267            ]
2268        else:
2269            state_transformations = []
2270        stream_state = self.apply_stream_state_migrations(state_transformations, stream_state)
2271        self._connector_state_manager.update_state_for_stream(
2272            stream_name=stream_name, namespace=None, value=stream_state
2273        )
2274
2275    def _is_stop_condition_on_cursor(self, model: DeclarativeStreamModel) -> bool:
2276        return bool(
2277            model.incremental_sync
2278            and hasattr(model.incremental_sync, "is_data_feed")
2279            and model.incremental_sync.is_data_feed
2280        )
2281
2282    def _is_client_side_filtering_enabled(self, model: DeclarativeStreamModel) -> bool:
2283        return bool(
2284            model.incremental_sync
2285            and hasattr(model.incremental_sync, "is_client_side_incremental")
2286            and model.incremental_sync.is_client_side_incremental
2287        )
2288
2289    def _build_stream_slicer_from_partition_router(
2290        self,
2291        model: Union[
2292            AsyncRetrieverModel,
2293            CustomRetrieverModel,
2294            SimpleRetrieverModel,
2295        ],
2296        config: Config,
2297        stream_name: Optional[str] = None,
2298        **kwargs: Any,
2299    ) -> PartitionRouter:
2300        if (
2301            hasattr(model, "partition_router")
2302            and isinstance(model, (SimpleRetrieverModel, AsyncRetrieverModel, CustomRetrieverModel))
2303            and model.partition_router
2304        ):
2305            stream_slicer_model = model.partition_router
2306            if isinstance(stream_slicer_model, list):
2307                return CartesianProductStreamSlicer(
2308                    [
2309                        self._create_component_from_model(
2310                            model=slicer, config=config, stream_name=stream_name or ""
2311                        )
2312                        for slicer in stream_slicer_model
2313                    ],
2314                    parameters={},
2315                )
2316            elif isinstance(stream_slicer_model, dict):
2317                # partition router comes from CustomRetrieverModel therefore has not been parsed as a model
2318                params = stream_slicer_model.get("$parameters")
2319                if not isinstance(params, dict):
2320                    params = {}
2321                    stream_slicer_model["$parameters"] = params
2322
2323                if stream_name is not None:
2324                    params["stream_name"] = stream_name
2325
2326                return self._create_nested_component(  # type: ignore[no-any-return] # There is no guarantee that this will return a stream slicer. If not, we expect an AttributeError during the call to `stream_slices`
2327                    model,
2328                    "partition_router",
2329                    stream_slicer_model,
2330                    config,
2331                    **kwargs,
2332                )
2333            else:
2334                return self._create_component_from_model(  # type: ignore[no-any-return] # Will be created PartitionRouter as stream_slicer_model is model.partition_router
2335                    model=stream_slicer_model, config=config, stream_name=stream_name or ""
2336                )
2337        return SinglePartitionRouter(parameters={})
2338
2339    def _build_concurrent_cursor(
2340        self,
2341        model: DeclarativeStreamModel,
2342        stream_slicer: Optional[PartitionRouter],
2343        config: Config,
2344    ) -> Cursor:
2345        stream_name = model.name or ""
2346        stream_state = self._connector_state_manager.get_stream_state(stream_name, None)
2347
2348        if (
2349            model.incremental_sync
2350            and stream_slicer
2351            and not isinstance(stream_slicer, SinglePartitionRouter)
2352        ):
2353            if isinstance(model.incremental_sync, IncrementingCountCursorModel):
2354                # We don't currently support usage of partition routing and IncrementingCountCursor at the
2355                # same time because we didn't solve for design questions like what the lookback window would
2356                # be as well as global cursor fall backs. We have not seen customers that have needed both
2357                # at the same time yet and are currently punting on this until we need to solve it.
2358                raise ValueError(
2359                    f"The low-code framework does not currently support usage of a PartitionRouter and an IncrementingCountCursor at the same time. Please specify only one of these options for stream {stream_name}."
2360                )
2361            return self.create_concurrent_cursor_from_perpartition_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
2362                state_manager=self._connector_state_manager,
2363                model_type=DatetimeBasedCursorModel,
2364                component_definition=model.incremental_sync.__dict__,
2365                stream_name=stream_name,
2366                stream_state=stream_state,
2367                stream_namespace=None,
2368                config=config or {},
2369                partition_router=stream_slicer,
2370                attempt_to_create_cursor_if_not_provided=True,  # FIXME can we remove that now?
2371            )
2372        elif model.incremental_sync:
2373            if type(model.incremental_sync) == IncrementingCountCursorModel:
2374                return self.create_concurrent_cursor_from_incrementing_count_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
2375                    model_type=IncrementingCountCursorModel,
2376                    component_definition=model.incremental_sync.__dict__,
2377                    stream_name=stream_name,
2378                    stream_namespace=None,
2379                    stream_state=stream_state,
2380                    config=config or {},
2381                )
2382            elif type(model.incremental_sync) == DatetimeBasedCursorModel:
2383                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
2384                    model_type=type(model.incremental_sync),
2385                    component_definition=model.incremental_sync.__dict__,
2386                    stream_name=stream_name,
2387                    stream_namespace=None,
2388                    stream_state=stream_state,
2389                    config=config or {},
2390                    attempt_to_create_cursor_if_not_provided=True,
2391                )
2392            else:
2393                raise ValueError(
2394                    f"Incremental sync of type {type(model.incremental_sync)} is not supported"
2395                )
2396        return FinalStateCursor(stream_name, None, self._message_repository)
2397
2398    def create_default_error_handler(
2399        self, model: DefaultErrorHandlerModel, config: Config, **kwargs: Any
2400    ) -> DefaultErrorHandler:
2401        backoff_strategies = []
2402        if model.backoff_strategies:
2403            for backoff_strategy_model in model.backoff_strategies:
2404                backoff_strategies.append(
2405                    self._create_component_from_model(model=backoff_strategy_model, config=config)
2406                )
2407
2408        response_filters = []
2409        if model.response_filters:
2410            for response_filter_model in model.response_filters:
2411                response_filters.append(
2412                    self._create_component_from_model(model=response_filter_model, config=config)
2413                )
2414        response_filters.append(
2415            HttpResponseFilter(config=config, parameters=model.parameters or {})
2416        )
2417
2418        return DefaultErrorHandler(
2419            backoff_strategies=backoff_strategies,
2420            max_retries=model.max_retries,
2421            response_filters=response_filters,
2422            config=config,
2423            parameters=model.parameters or {},
2424        )
2425
2426    def create_default_paginator(
2427        self,
2428        model: DefaultPaginatorModel,
2429        config: Config,
2430        *,
2431        url_base: str,
2432        extractor_model: Optional[Union[CustomRecordExtractorModel, DpathExtractorModel]] = None,
2433        decoder: Optional[Decoder] = None,
2434        cursor_used_for_stop_condition: Optional[Cursor] = None,
2435    ) -> Union[DefaultPaginator, PaginatorTestReadDecorator]:
2436        if decoder:
2437            if self._is_supported_decoder_for_pagination(decoder):
2438                decoder_to_use = PaginationDecoderDecorator(decoder=decoder)
2439            else:
2440                raise ValueError(self._UNSUPPORTED_DECODER_ERROR.format(decoder_type=type(decoder)))
2441        else:
2442            decoder_to_use = PaginationDecoderDecorator(decoder=JsonDecoder(parameters={}))
2443        page_size_option = (
2444            self._create_component_from_model(model=model.page_size_option, config=config)
2445            if model.page_size_option
2446            else None
2447        )
2448        page_token_option = (
2449            self._create_component_from_model(model=model.page_token_option, config=config)
2450            if model.page_token_option
2451            else None
2452        )
2453        pagination_strategy = self._create_component_from_model(
2454            model=model.pagination_strategy,
2455            config=config,
2456            decoder=decoder_to_use,
2457            extractor_model=extractor_model,
2458        )
2459        if cursor_used_for_stop_condition:
2460            pagination_strategy = StopConditionPaginationStrategyDecorator(
2461                pagination_strategy, CursorStopCondition(cursor_used_for_stop_condition)
2462            )
2463        paginator = DefaultPaginator(
2464            decoder=decoder_to_use,
2465            page_size_option=page_size_option,
2466            page_token_option=page_token_option,
2467            pagination_strategy=pagination_strategy,
2468            url_base=url_base,
2469            config=config,
2470            parameters=model.parameters or {},
2471        )
2472        if self._limit_pages_fetched_per_slice:
2473            return PaginatorTestReadDecorator(paginator, self._limit_pages_fetched_per_slice)
2474        return paginator
2475
2476    def create_dpath_extractor(
2477        self,
2478        model: DpathExtractorModel,
2479        config: Config,
2480        decoder: Optional[Decoder] = None,
2481        **kwargs: Any,
2482    ) -> DpathExtractor:
2483        if decoder:
2484            decoder_to_use = decoder
2485        else:
2486            decoder_to_use = JsonDecoder(parameters={})
2487        model_field_path: List[Union[InterpolatedString, str]] = [x for x in model.field_path]
2488
2489        record_expander = None
2490        if model.record_expander:
2491            record_expander = self._create_component_from_model(
2492                model=model.record_expander,
2493                config=config,
2494            )
2495
2496        return DpathExtractor(
2497            decoder=decoder_to_use,
2498            field_path=model_field_path,
2499            config=config,
2500            parameters=model.parameters or {},
2501            record_expander=record_expander,
2502        )
2503
2504    def create_record_expander(
2505        self,
2506        model: RecordExpanderModel,
2507        config: Config,
2508        **kwargs: Any,
2509    ) -> RecordExpander:
2510        truncated_list_retriever = None
2511        suppress_incomplete_fetch_warning = False
2512        if model.truncated_list_retriever:
2513            retriever_model = model.truncated_list_retriever
2514            name = "record_expander_truncated_list"
2515            # `CustomRetriever` allows extra fields, so read from the dumped model to cover both types.
2516            retriever_fields = retriever_model.dict()
2517            for unsupported_option in ("partition_router", "pagination_reset"):
2518                if retriever_fields.get(unsupported_option):
2519                    raise ValueError(
2520                        f"`{unsupported_option}` is not supported on `truncated_list_retriever`."
2521                    )
2522            log_formatter = lambda response: format_http_message(
2523                response,
2524                f"Record expander '{name}' request",
2525                "Request performed in order to fetch the complete nested list of a truncated record.",
2526                name,
2527                is_auxiliary=True,
2528            )
2529            # `name`/`primary_key`/`transformations` are also what a `CustomRetriever` forwards to its
2530            # nested `requester`/`record_selector`, so they are passed for both retriever types.
2531            truncated_list_retriever = self._create_component_from_model(
2532                model=retriever_model,
2533                config=config,
2534                name=name,
2535                primary_key=None,
2536                transformations=[],
2537                log_formatter=log_formatter,
2538            )
2539            # Only a capped paginator makes a shortfall expected; `NoPagination` and retrievers
2540            # without a paginator are never capped.
2541            suppress_incomplete_fetch_warning = isinstance(
2542                truncated_list_retriever, SimpleRetriever
2543            ) and isinstance(truncated_list_retriever.paginator, PaginatorTestReadDecorator)
2544        return RecordExpander(
2545            expand_records_from_field=model.expand_records_from_field,
2546            config=config,
2547            parameters=model.parameters or {},
2548            remain_original_record=model.remain_original_record or False,
2549            on_no_records=OnNoRecords(model.on_no_records.value)
2550            if model.on_no_records
2551            else OnNoRecords.skip,
2552            truncation_indicator_path=model.truncation_indicator_path,
2553            truncated_list_retriever=truncated_list_retriever,
2554            message_repository=self._message_repository,
2555            suppress_incomplete_fetch_warning=suppress_incomplete_fetch_warning,
2556        )
2557
2558    @staticmethod
2559    def create_response_to_file_extractor(
2560        model: ResponseToFileExtractorModel,
2561        **kwargs: Any,
2562    ) -> ResponseToFileExtractor:
2563        return ResponseToFileExtractor(
2564            parameters=model.parameters or {},
2565            preserve_na_values=model.preserve_na_values or False,
2566        )
2567
2568    @staticmethod
2569    def create_exponential_backoff_strategy(
2570        model: ExponentialBackoffStrategyModel, config: Config
2571    ) -> ExponentialBackoffStrategy:
2572        ModelToComponentFactory._validate_jitter_range(model.jitter_range_in_seconds)
2573        return ExponentialBackoffStrategy(
2574            factor=model.factor or 5,
2575            jitter_range_in_seconds=model.jitter_range_in_seconds,
2576            parameters=model.parameters or {},
2577            config=config,
2578        )
2579
2580    @staticmethod
2581    def create_group_by_key(model: GroupByKeyMergeStrategyModel, config: Config) -> GroupByKey:
2582        return GroupByKey(model.key, config=config, parameters=model.parameters or {})
2583
2584    def create_http_requester(
2585        self,
2586        model: HttpRequesterModel,
2587        config: Config,
2588        decoder: Decoder = JsonDecoder(parameters={}),
2589        query_properties_key: Optional[str] = None,
2590        use_cache: Optional[bool] = None,
2591        *,
2592        name: str,
2593    ) -> HttpRequester:
2594        authenticator = (
2595            self._create_component_from_model(
2596                model=model.authenticator,
2597                config=config,
2598                url_base=model.url or model.url_base,
2599                name=name,
2600                decoder=decoder,
2601            )
2602            if model.authenticator
2603            else None
2604        )
2605        error_handler = (
2606            self._create_component_from_model(model=model.error_handler, config=config)
2607            if model.error_handler
2608            else DefaultErrorHandler(
2609                backoff_strategies=[],
2610                response_filters=[],
2611                config=config,
2612                parameters=model.parameters or {},
2613            )
2614        )
2615
2616        api_budget = self._api_budget
2617
2618        request_options_provider = InterpolatedRequestOptionsProvider(
2619            request_body=model.request_body,
2620            request_body_data=model.request_body_data,
2621            request_body_json=model.request_body_json,
2622            request_headers=model.request_headers,
2623            request_parameters=model.request_parameters,  # type: ignore  # QueryProperties have been removed in `create_simple_retriever`
2624            query_properties_key=query_properties_key,
2625            config=config,
2626            parameters=model.parameters or {},
2627        )
2628
2629        assert model.use_cache is not None  # for mypy
2630        assert model.http_method is not None  # for mypy
2631
2632        should_use_cache = (model.use_cache or bool(use_cache)) and not self._disable_cache
2633
2634        return HttpRequester(
2635            name=name,
2636            url=model.url,
2637            url_base=model.url_base,
2638            path=model.path,
2639            authenticator=authenticator,
2640            error_handler=error_handler,
2641            api_budget=api_budget,
2642            http_method=HttpMethod[model.http_method.value],
2643            request_options_provider=request_options_provider,
2644            config=config,
2645            disable_retries=self._disable_retries,
2646            parameters=model.parameters or {},
2647            message_repository=self._message_repository,
2648            use_cache=should_use_cache,
2649            decoder=decoder,
2650            stream_response=decoder.is_stream_response() if decoder else False,
2651        )
2652
2653    @staticmethod
2654    def create_http_response_filter(
2655        model: HttpResponseFilterModel, config: Config, **kwargs: Any
2656    ) -> HttpResponseFilter:
2657        if model.action:
2658            action = ResponseAction(model.action.value)
2659        else:
2660            action = None
2661
2662        failure_type = FailureType(model.failure_type.value) if model.failure_type else None
2663
2664        http_codes = (
2665            set(model.http_codes) if model.http_codes else set()
2666        )  # JSON schema notation has no set data type. The schema enforces an array of unique elements
2667
2668        return HttpResponseFilter(
2669            action=action,
2670            failure_type=failure_type,
2671            error_message=model.error_message or "",
2672            error_message_contains=model.error_message_contains or "",
2673            http_codes=http_codes,
2674            predicate=model.predicate or "",
2675            config=config,
2676            parameters=model.parameters or {},
2677        )
2678
2679    @staticmethod
2680    def create_inline_schema_loader(
2681        model: InlineSchemaLoaderModel, config: Config, **kwargs: Any
2682    ) -> InlineSchemaLoader:
2683        return InlineSchemaLoader(schema=model.schema_ or {}, parameters={})
2684
2685    def create_complex_field_type(
2686        self, model: ComplexFieldTypeModel, config: Config, **kwargs: Any
2687    ) -> ComplexFieldType:
2688        items = (
2689            self._create_component_from_model(model=model.items, config=config)
2690            if isinstance(model.items, ComplexFieldTypeModel)
2691            else model.items
2692        )
2693
2694        return ComplexFieldType(field_type=model.field_type, items=items)
2695
2696    def create_types_map(self, model: TypesMapModel, config: Config, **kwargs: Any) -> TypesMap:
2697        target_type = (
2698            self._create_component_from_model(model=model.target_type, config=config)
2699            if isinstance(model.target_type, ComplexFieldTypeModel)
2700            else model.target_type
2701        )
2702
2703        return TypesMap(
2704            target_type=target_type,
2705            current_type=model.current_type,
2706            condition=model.condition if model.condition is not None else "True",
2707        )
2708
2709    def create_schema_type_identifier(
2710        self, model: SchemaTypeIdentifierModel, config: Config, **kwargs: Any
2711    ) -> SchemaTypeIdentifier:
2712        types_mapping = []
2713        if model.types_mapping:
2714            types_mapping.extend(
2715                [
2716                    self._create_component_from_model(types_map, config=config)
2717                    for types_map in model.types_mapping
2718                ]
2719            )
2720        model_schema_pointer: List[Union[InterpolatedString, str]] = (
2721            [x for x in model.schema_pointer] if model.schema_pointer else []
2722        )
2723        model_key_pointer: List[Union[InterpolatedString, str]] = [x for x in model.key_pointer]
2724        model_type_pointer: Optional[List[Union[InterpolatedString, str]]] = (
2725            [x for x in model.type_pointer] if model.type_pointer else None
2726        )
2727
2728        return SchemaTypeIdentifier(
2729            schema_pointer=model_schema_pointer,
2730            key_pointer=model_key_pointer,
2731            type_pointer=model_type_pointer,
2732            types_mapping=types_mapping,
2733            parameters=model.parameters or {},
2734        )
2735
2736    def create_dynamic_schema_loader(
2737        self, model: DynamicSchemaLoaderModel, config: Config, **kwargs: Any
2738    ) -> DynamicSchemaLoader:
2739        schema_transformations = []
2740        if model.schema_transformations:
2741            for transformation_model in model.schema_transformations:
2742                schema_transformations.append(
2743                    self._create_component_from_model(model=transformation_model, config=config)
2744                )
2745        name = "dynamic_properties"
2746        retriever = self._create_component_from_model(
2747            model=model.retriever,
2748            config=config,
2749            name=name,
2750            primary_key=None,
2751            partition_router=self._build_stream_slicer_from_partition_router(
2752                model.retriever, config
2753            ),
2754            transformations=[],
2755            use_cache=True,
2756            log_formatter=(
2757                lambda response: format_http_message(
2758                    response,
2759                    f"Schema loader '{name}' request",
2760                    f"Request performed in order to extract schema.",
2761                    name,
2762                    is_auxiliary=True,
2763                )
2764            ),
2765        )
2766        schema_type_identifier = self._create_component_from_model(
2767            model.schema_type_identifier, config=config, parameters=model.parameters or {}
2768        )
2769        schema_filter = (
2770            self._create_component_from_model(
2771                model.schema_filter, config=config, parameters=model.parameters or {}
2772            )
2773            if model.schema_filter is not None
2774            else None
2775        )
2776
2777        return DynamicSchemaLoader(
2778            retriever=retriever,
2779            config=config,
2780            schema_transformations=schema_transformations,
2781            schema_filter=schema_filter,
2782            schema_type_identifier=schema_type_identifier,
2783            parameters=model.parameters or {},
2784        )
2785
2786    @staticmethod
2787    def create_json_decoder(model: JsonDecoderModel, config: Config, **kwargs: Any) -> Decoder:
2788        return JsonDecoder(parameters={})
2789
2790    def create_csv_decoder(self, model: CsvDecoderModel, config: Config, **kwargs: Any) -> Decoder:
2791        return CompositeRawDecoder(
2792            parser=ModelToComponentFactory._get_parser(model, config),
2793            stream_response=False if self._emit_connector_builder_messages else True,
2794        )
2795
2796    def create_jsonl_decoder(
2797        self, model: JsonlDecoderModel, config: Config, **kwargs: Any
2798    ) -> Decoder:
2799        return CompositeRawDecoder(
2800            parser=ModelToComponentFactory._get_parser(model, config),
2801            stream_response=False if self._emit_connector_builder_messages else True,
2802        )
2803
2804    def create_json_items_decoder(
2805        self, model: JsonItemsDecoderModel, config: Config, **kwargs: Any
2806    ) -> Decoder:
2807        return CompositeRawDecoder(
2808            parser=ModelToComponentFactory._get_parser(model, config),
2809            stream_response=False if self._emit_connector_builder_messages else True,
2810        )
2811
2812    def create_gzip_decoder(
2813        self, model: GzipDecoderModel, config: Config, **kwargs: Any
2814    ) -> Decoder:
2815        _compressed_response_types = {
2816            "gzip",
2817            "x-gzip",
2818            "gzip, deflate",
2819            "x-gzip, deflate",
2820            "application/zip",
2821            "application/gzip",
2822            "application/x-gzip",
2823            "application/x-zip-compressed",
2824        }
2825
2826        gzip_parser: GzipParser = ModelToComponentFactory._get_parser(model, config)  # type: ignore  # based on the model, we know this will be a GzipParser
2827
2828        if self._emit_connector_builder_messages:
2829            return CompositeRawDecoder(gzip_parser, False)
2830
2831        transport_gzip_parser = GzipParser(inner_parser=gzip_parser)
2832        return CompositeRawDecoder.by_headers(
2833            [
2834                ({"Content-Encoding"}, {"gzip"}, transport_gzip_parser),
2835                ({"Content-Type"}, _compressed_response_types, gzip_parser),
2836            ],
2837            stream_response=True,
2838            fallback_parser=gzip_parser,
2839        )
2840
2841    @staticmethod
2842    def create_iterable_decoder(
2843        model: IterableDecoderModel, config: Config, **kwargs: Any
2844    ) -> IterableDecoder:
2845        return IterableDecoder(parameters={})
2846
2847    @staticmethod
2848    def create_xml_decoder(model: XmlDecoderModel, config: Config, **kwargs: Any) -> XmlDecoder:
2849        return XmlDecoder(parameters={})
2850
2851    def create_zipfile_decoder(
2852        self, model: ZipfileDecoderModel, config: Config, **kwargs: Any
2853    ) -> ZipfileDecoder:
2854        return ZipfileDecoder(parser=ModelToComponentFactory._get_parser(model.decoder, config))
2855
2856    @staticmethod
2857    def _get_parser(model: BaseModel, config: Config) -> Parser:
2858        if isinstance(model, JsonDecoderModel):
2859            # Note that the logic is a bit different from the JsonDecoder as there is some legacy that is maintained to return {} on error cases
2860            return JsonParser()
2861        elif isinstance(model, JsonItemsDecoderModel):
2862            return JsonItemsParser(
2863                items_path=model.items_path,
2864                encoding=model.encoding,
2865            )
2866        elif isinstance(model, JsonlDecoderModel):
2867            return JsonLineParser()
2868        elif isinstance(model, CsvDecoderModel):
2869            return CsvParser(
2870                encoding=model.encoding,
2871                delimiter=model.delimiter,
2872                set_values_to_none=model.set_values_to_none,
2873            )
2874        elif isinstance(model, GzipDecoderModel):
2875            return GzipParser(
2876                inner_parser=ModelToComponentFactory._get_parser(model.decoder, config)
2877            )
2878        elif isinstance(
2879            model, (CustomDecoderModel, IterableDecoderModel, XmlDecoderModel, ZipfileDecoderModel)
2880        ):
2881            raise ValueError(f"Decoder type {model} does not have parser associated to it")
2882
2883        raise ValueError(f"Unknown decoder type {model}")
2884
2885    @staticmethod
2886    def create_json_file_schema_loader(
2887        model: JsonFileSchemaLoaderModel, config: Config, **kwargs: Any
2888    ) -> JsonFileSchemaLoader:
2889        return JsonFileSchemaLoader(
2890            file_path=model.file_path or "", config=config, parameters=model.parameters or {}
2891        )
2892
2893    def create_jwt_authenticator(
2894        self, model: JwtAuthenticatorModel, config: Config, **kwargs: Any
2895    ) -> JwtAuthenticator:
2896        jwt_headers = model.jwt_headers or JwtHeadersModel(kid=None, typ="JWT", cty=None)
2897        jwt_payload = model.jwt_payload or JwtPayloadModel(iss=None, sub=None, aud=None)
2898        request_option = (
2899            self._create_component_from_model(model.request_option, config)
2900            if model.request_option
2901            else None
2902        )
2903        return JwtAuthenticator(
2904            config=config,
2905            parameters=model.parameters or {},
2906            algorithm=JwtAlgorithm(model.algorithm.value),
2907            secret_key=model.secret_key,
2908            base64_encode_secret_key=model.base64_encode_secret_key,
2909            token_duration=model.token_duration,
2910            header_prefix=model.header_prefix,
2911            kid=jwt_headers.kid,
2912            typ=jwt_headers.typ,
2913            cty=jwt_headers.cty,
2914            iss=jwt_payload.iss,
2915            sub=jwt_payload.sub,
2916            aud=jwt_payload.aud,
2917            additional_jwt_headers=model.additional_jwt_headers,
2918            additional_jwt_payload=model.additional_jwt_payload,
2919            passphrase=model.passphrase,
2920            request_option=request_option,
2921        )
2922
2923    def create_list_partition_router(
2924        self, model: ListPartitionRouterModel, config: Config, **kwargs: Any
2925    ) -> ListPartitionRouter:
2926        request_option = (
2927            self._create_component_from_model(model.request_option, config)
2928            if model.request_option
2929            else None
2930        )
2931        return ListPartitionRouter(
2932            cursor_field=model.cursor_field,
2933            request_option=request_option,
2934            values=model.values,
2935            config=config,
2936            parameters=model.parameters or {},
2937        )
2938
2939    @staticmethod
2940    def create_min_max_datetime(
2941        model: MinMaxDatetimeModel, config: Config, **kwargs: Any
2942    ) -> MinMaxDatetime:
2943        return MinMaxDatetime(
2944            datetime=model.datetime,
2945            datetime_format=model.datetime_format or "",
2946            max_datetime=model.max_datetime or "",
2947            min_datetime=model.min_datetime or "",
2948            parameters=model.parameters or {},
2949        )
2950
2951    @staticmethod
2952    def create_no_auth(model: NoAuthModel, config: Config, **kwargs: Any) -> NoAuth:
2953        return NoAuth(parameters=model.parameters or {})
2954
2955    @staticmethod
2956    def create_no_pagination(
2957        model: NoPaginationModel, config: Config, **kwargs: Any
2958    ) -> NoPagination:
2959        return NoPagination(parameters={})
2960
2961    def create_oauth_authenticator(
2962        self, model: OAuthAuthenticatorModel, config: Config, **kwargs: Any
2963    ) -> DeclarativeOauth2Authenticator:
2964        profile_assertion = (
2965            self._create_component_from_model(model.profile_assertion, config=config)
2966            if model.profile_assertion
2967            else None
2968        )
2969
2970        refresh_token_error_status_codes, refresh_token_error_key, refresh_token_error_values = (
2971            self._get_refresh_token_error_information(model)
2972        )
2973        if model.refresh_token_updater:
2974            # ignore type error because fixing it would have a lot of dependencies, revisit later
2975            return DeclarativeSingleUseRefreshTokenOauth2Authenticator(  # type: ignore
2976                config,
2977                InterpolatedString.create(
2978                    model.token_refresh_endpoint,  # type: ignore
2979                    parameters=model.parameters or {},
2980                ).eval(config),
2981                access_token_name=InterpolatedString.create(
2982                    model.access_token_name or "access_token", parameters=model.parameters or {}
2983                ).eval(config),
2984                refresh_token_name=model.refresh_token_updater.refresh_token_name,
2985                expires_in_name=InterpolatedString.create(
2986                    model.expires_in_name or "expires_in", parameters=model.parameters or {}
2987                ).eval(config),
2988                client_id_name=InterpolatedString.create(
2989                    model.client_id_name or "client_id", parameters=model.parameters or {}
2990                ).eval(config),
2991                client_id=InterpolatedString.create(
2992                    model.client_id, parameters=model.parameters or {}
2993                ).eval(config)
2994                if model.client_id
2995                else model.client_id,
2996                client_secret_name=InterpolatedString.create(
2997                    model.client_secret_name or "client_secret", parameters=model.parameters or {}
2998                ).eval(config),
2999                client_secret=InterpolatedString.create(
3000                    model.client_secret, parameters=model.parameters or {}
3001                ).eval(config)
3002                if model.client_secret
3003                else model.client_secret,
3004                access_token_config_path=model.refresh_token_updater.access_token_config_path,
3005                refresh_token_config_path=model.refresh_token_updater.refresh_token_config_path,
3006                token_expiry_date_config_path=model.refresh_token_updater.token_expiry_date_config_path,
3007                grant_type_name=InterpolatedString.create(
3008                    model.grant_type_name or "grant_type", parameters=model.parameters or {}
3009                ).eval(config),
3010                grant_type=InterpolatedString.create(
3011                    model.grant_type or "refresh_token", parameters=model.parameters or {}
3012                ).eval(config),
3013                refresh_request_body=InterpolatedMapping(
3014                    model.refresh_request_body or {}, parameters=model.parameters or {}
3015                ).eval(config),
3016                refresh_request_headers=InterpolatedMapping(
3017                    model.refresh_request_headers or {}, parameters=model.parameters or {}
3018                ).eval(config),
3019                send_refresh_request_as_query_params=bool(
3020                    model.send_refresh_request_as_query_params
3021                ),
3022                scopes=model.scopes,
3023                token_expiry_date_format=model.token_expiry_date_format,
3024                token_expiry_is_time_of_expiration=bool(model.token_expiry_date_format),
3025                message_repository=self._message_repository,
3026                refresh_token_error_status_codes=refresh_token_error_status_codes,
3027                refresh_token_error_key=refresh_token_error_key,
3028                refresh_token_error_values=refresh_token_error_values,
3029            )
3030        # ignore type error because fixing it would have a lot of dependencies, revisit later
3031        return DeclarativeOauth2Authenticator(  # type: ignore
3032            access_token_name=model.access_token_name or "access_token",
3033            access_token_value=model.access_token_value,
3034            client_id_name=model.client_id_name or "client_id",
3035            client_id=model.client_id,
3036            client_secret_name=model.client_secret_name or "client_secret",
3037            client_secret=model.client_secret,
3038            expires_in_name=model.expires_in_name or "expires_in",
3039            grant_type_name=model.grant_type_name or "grant_type",
3040            grant_type=model.grant_type or "refresh_token",
3041            refresh_request_body=model.refresh_request_body,
3042            refresh_request_headers=model.refresh_request_headers,
3043            send_refresh_request_as_query_params=bool(model.send_refresh_request_as_query_params),
3044            refresh_token_name=model.refresh_token_name or "refresh_token",
3045            refresh_token=model.refresh_token,
3046            scopes=model.scopes,
3047            token_expiry_date=model.token_expiry_date,
3048            token_expiry_date_format=model.token_expiry_date_format,
3049            token_expiry_is_time_of_expiration=bool(model.token_expiry_date_format),
3050            token_refresh_endpoint=model.token_refresh_endpoint,
3051            config=config,
3052            parameters=model.parameters or {},
3053            message_repository=self._message_repository,
3054            profile_assertion=profile_assertion,
3055            use_profile_assertion=model.use_profile_assertion,
3056            refresh_token_error_status_codes=refresh_token_error_status_codes,
3057            refresh_token_error_key=refresh_token_error_key,
3058            refresh_token_error_values=refresh_token_error_values,
3059        )
3060
3061    @staticmethod
3062    def _get_refresh_token_error_information(
3063        model: OAuthAuthenticatorModel,
3064    ) -> Tuple[Tuple[int, ...], str, Tuple[str, ...]]:
3065        """
3066        In a previous version of the CDK, the auth error as config_error was only done if a refresh token updater was
3067        defined. As a transition, we added those fields on the OAuthAuthenticatorModel. This method ensures that the
3068        information is defined only once and return the right fields.
3069        """
3070        refresh_token_updater = model.refresh_token_updater
3071        is_defined_on_refresh_token_updated = refresh_token_updater and (
3072            refresh_token_updater.refresh_token_error_status_codes
3073            or refresh_token_updater.refresh_token_error_key
3074            or refresh_token_updater.refresh_token_error_values
3075        )
3076        is_defined_on_oauth_authenticator = (
3077            model.refresh_token_error_status_codes
3078            or model.refresh_token_error_key
3079            or model.refresh_token_error_values
3080        )
3081        if is_defined_on_refresh_token_updated and is_defined_on_oauth_authenticator:
3082            raise ValueError(
3083                "refresh_token_error should either be defined on the OAuthAuthenticatorModel or the RefreshTokenUpdaterModel, not both"
3084            )
3085
3086        if is_defined_on_refresh_token_updated:
3087            not_optional_refresh_token_updater: RefreshTokenUpdaterModel = refresh_token_updater  # type: ignore  # we know from the condition that this is not None
3088            return (
3089                tuple(not_optional_refresh_token_updater.refresh_token_error_status_codes)
3090                if not_optional_refresh_token_updater.refresh_token_error_status_codes
3091                else (),
3092                not_optional_refresh_token_updater.refresh_token_error_key or "",
3093                tuple(not_optional_refresh_token_updater.refresh_token_error_values)
3094                if not_optional_refresh_token_updater.refresh_token_error_values
3095                else (),
3096            )
3097        elif is_defined_on_oauth_authenticator:
3098            return (
3099                tuple(model.refresh_token_error_status_codes)
3100                if model.refresh_token_error_status_codes
3101                else (),
3102                model.refresh_token_error_key or "",
3103                tuple(model.refresh_token_error_values) if model.refresh_token_error_values else (),
3104            )
3105
3106        # returning default values we think cover most cases
3107        return (400,), "error", ("invalid_grant", "invalid_permissions")
3108
3109    def create_offset_increment(
3110        self,
3111        model: OffsetIncrementModel,
3112        config: Config,
3113        decoder: Decoder,
3114        extractor_model: Optional[Union[CustomRecordExtractorModel, DpathExtractorModel]] = None,
3115        **kwargs: Any,
3116    ) -> OffsetIncrement:
3117        if isinstance(decoder, PaginationDecoderDecorator):
3118            inner_decoder = decoder.decoder
3119        else:
3120            inner_decoder = decoder
3121            decoder = PaginationDecoderDecorator(decoder=decoder)
3122
3123        if self._is_supported_decoder_for_pagination(inner_decoder):
3124            decoder_to_use = decoder
3125        else:
3126            raise ValueError(
3127                self._UNSUPPORTED_DECODER_ERROR.format(decoder_type=type(inner_decoder))
3128            )
3129
3130        # Ideally we would instantiate the runtime extractor from highest most level (in this case the SimpleRetriever)
3131        # so that it can be shared by OffSetIncrement and RecordSelector. However, due to how we instantiate the
3132        # decoder with various decorators here, but not in create_record_selector, it is simpler to retain existing
3133        # behavior by having two separate extractors with identical behavior since they use the same extractor model.
3134        # When we have more time to investigate we can look into reusing the same component.
3135        extractor = (
3136            self._create_component_from_model(
3137                model=extractor_model, config=config, decoder=decoder_to_use
3138            )
3139            if extractor_model
3140            else None
3141        )
3142
3143        # Pydantic v1 Union type coercion can convert int to string depending on Union order.
3144        # If page_size is a string that represents an integer (not an interpolation), convert it back.
3145        page_size = model.page_size
3146        if isinstance(page_size, str) and page_size.isdigit():
3147            page_size = int(page_size)
3148
3149        return OffsetIncrement(
3150            page_size=page_size,
3151            config=config,
3152            decoder=decoder_to_use,
3153            extractor=extractor,
3154            inject_on_first_request=model.inject_on_first_request or False,
3155            parameters=model.parameters or {},
3156        )
3157
3158    def create_page_increment(
3159        self,
3160        model: PageIncrementModel,
3161        config: Config,
3162        decoder: Optional[Decoder] = None,
3163        extractor_model: Optional[Union[CustomRecordExtractorModel, DpathExtractorModel]] = None,
3164        **kwargs: Any,
3165    ) -> PageIncrement:
3166        # Like OffsetIncrement, we instantiate a separate extractor with identical behavior to the
3167        # RecordSelector's so the strategy can count the raw records in the response. This ensures
3168        # pagination is driven by the API's page size, not the post-filter record count.
3169        extractor = (
3170            self._create_component_from_model(model=extractor_model, config=config, decoder=decoder)
3171            if extractor_model
3172            else None
3173        )
3174
3175        # Pydantic v1 Union type coercion can convert int to string depending on Union order.
3176        # If page_size is a string that represents an integer (not an interpolation), convert it back.
3177        page_size = model.page_size
3178        if isinstance(page_size, str) and page_size.isdigit():
3179            page_size = int(page_size)
3180
3181        return PageIncrement(
3182            page_size=page_size,
3183            config=config,
3184            start_from_page=model.start_from_page or 0,
3185            inject_on_first_request=model.inject_on_first_request or False,
3186            extractor=extractor,
3187            parameters=model.parameters or {},
3188        )
3189
3190    def create_parent_stream_config(
3191        self, model: ParentStreamConfigModel, config: Config, *, stream_name: str, **kwargs: Any
3192    ) -> ParentStreamConfig:
3193        declarative_stream = self._create_component_from_model(
3194            model.stream,
3195            config=config,
3196            is_parent=True,
3197            **kwargs,
3198        )
3199        request_option = (
3200            self._create_component_from_model(model.request_option, config=config)
3201            if model.request_option
3202            else None
3203        )
3204
3205        if model.lazy_read_pointer and any("*" in pointer for pointer in model.lazy_read_pointer):
3206            raise ValueError(
3207                "The '*' wildcard in 'lazy_read_pointer' is not supported — only direct paths are allowed."
3208            )
3209
3210        model_lazy_read_pointer: List[Union[InterpolatedString, str]] = (
3211            [x for x in model.lazy_read_pointer] if model.lazy_read_pointer else []
3212        )
3213
3214        return ParentStreamConfig(
3215            parent_key=model.parent_key,
3216            request_option=request_option,
3217            stream=declarative_stream,
3218            partition_field=model.partition_field,
3219            config=config,
3220            incremental_dependency=model.incremental_dependency or False,
3221            parameters=model.parameters or {},
3222            extra_fields=model.extra_fields,
3223            lazy_read_pointer=model_lazy_read_pointer,
3224        )
3225
3226    def create_properties_from_endpoint(
3227        self, model: PropertiesFromEndpointModel, config: Config, **kwargs: Any
3228    ) -> PropertiesFromEndpoint:
3229        retriever = self._create_component_from_model(
3230            model=model.retriever,
3231            config=config,
3232            name="dynamic_properties",
3233            primary_key=None,
3234            stream_slicer=None,
3235            transformations=[],
3236            use_cache=True,  # Enable caching on the HttpRequester/HttpClient because the properties endpoint will be called for every slice being processed, and it is highly unlikely for the response to different
3237        )
3238        return PropertiesFromEndpoint(
3239            property_field_path=model.property_field_path,
3240            retriever=retriever,
3241            config=config,
3242            parameters=model.parameters or {},
3243        )
3244
3245    def create_property_chunking(
3246        self, model: PropertyChunkingModel, config: Config, **kwargs: Any
3247    ) -> PropertyChunking:
3248        record_merge_strategy = (
3249            self._create_component_from_model(
3250                model=model.record_merge_strategy, config=config, **kwargs
3251            )
3252            if model.record_merge_strategy
3253            else None
3254        )
3255
3256        property_limit_type: PropertyLimitType
3257        match model.property_limit_type:
3258            case PropertyLimitTypeModel.property_count:
3259                property_limit_type = PropertyLimitType.property_count
3260            case PropertyLimitTypeModel.characters:
3261                property_limit_type = PropertyLimitType.characters
3262            case _:
3263                raise ValueError(f"Invalid PropertyLimitType {property_limit_type}")
3264
3265        return PropertyChunking(
3266            property_limit_type=property_limit_type,
3267            property_limit=model.property_limit,
3268            record_merge_strategy=record_merge_strategy,
3269            config=config,
3270            parameters=model.parameters or {},
3271        )
3272
3273    def create_query_properties(
3274        self, model: QueryPropertiesModel, config: Config, *, stream_name: str, **kwargs: Any
3275    ) -> QueryProperties:
3276        if isinstance(model.property_list, list):
3277            property_list = model.property_list
3278        else:
3279            property_list = self._create_component_from_model(
3280                model=model.property_list, config=config, **kwargs
3281            )
3282
3283        property_chunking = (
3284            self._create_component_from_model(
3285                model=model.property_chunking, config=config, **kwargs
3286            )
3287            if model.property_chunking
3288            else None
3289        )
3290
3291        property_selector = (
3292            self._create_component_from_model(
3293                model=model.property_selector, config=config, stream_name=stream_name, **kwargs
3294            )
3295            if model.property_selector
3296            else None
3297        )
3298
3299        return QueryProperties(
3300            property_list=property_list,
3301            always_include_properties=model.always_include_properties,
3302            property_chunking=property_chunking,
3303            property_selector=property_selector,
3304            config=config,
3305            parameters=model.parameters or {},
3306        )
3307
3308    def create_json_schema_property_selector(
3309        self,
3310        model: JsonSchemaPropertySelectorModel,
3311        config: Config,
3312        *,
3313        stream_name: str,
3314        **kwargs: Any,
3315    ) -> JsonSchemaPropertySelector:
3316        configured_stream = self._stream_name_to_configured_stream.get(stream_name)
3317
3318        transformations = []
3319        if model.transformations:
3320            for transformation_model in model.transformations:
3321                transformations.append(
3322                    self._create_component_from_model(model=transformation_model, config=config)
3323                )
3324
3325        return JsonSchemaPropertySelector(
3326            configured_stream=configured_stream,
3327            properties_transformations=transformations,
3328            config=config,
3329            parameters=model.parameters or {},
3330        )
3331
3332    @staticmethod
3333    def create_record_filter(
3334        model: RecordFilterModel, config: Config, **kwargs: Any
3335    ) -> RecordFilter:
3336        return RecordFilter(
3337            condition=model.condition or "", config=config, parameters=model.parameters or {}
3338        )
3339
3340    @staticmethod
3341    def create_request_path(model: RequestPathModel, config: Config, **kwargs: Any) -> RequestPath:
3342        return RequestPath(parameters={})
3343
3344    @staticmethod
3345    def create_request_option(
3346        model: RequestOptionModel, config: Config, **kwargs: Any
3347    ) -> RequestOption:
3348        inject_into = RequestOptionType(model.inject_into.value)
3349        field_path: Optional[List[Union[InterpolatedString, str]]] = (
3350            [
3351                InterpolatedString.create(segment, parameters=kwargs.get("parameters", {}))
3352                for segment in model.field_path
3353            ]
3354            if model.field_path
3355            else None
3356        )
3357        field_name = (
3358            InterpolatedString.create(model.field_name, parameters=kwargs.get("parameters", {}))
3359            if model.field_name
3360            else None
3361        )
3362        return RequestOption(
3363            field_name=field_name,
3364            field_path=field_path,
3365            inject_into=inject_into,
3366            parameters=kwargs.get("parameters", {}),
3367        )
3368
3369    def create_record_selector(
3370        self,
3371        model: RecordSelectorModel,
3372        config: Config,
3373        *,
3374        name: str,
3375        transformations: List[RecordTransformation] | None = None,
3376        decoder: Decoder | None = None,
3377        client_side_incremental_sync_cursor: Optional[Cursor] = None,
3378        is_client_side_incremental_sync: bool = False,
3379        file_uploader: Optional[DefaultFileUploader] = None,
3380        **kwargs: Any,
3381    ) -> RecordSelector:
3382        extractor = self._create_component_from_model(
3383            model=model.extractor, decoder=decoder, config=config
3384        )
3385        record_filter = (
3386            self._create_component_from_model(model.record_filter, config=config)
3387            if model.record_filter
3388            else None
3389        )
3390
3391        # A client-side incremental stream transforms before filtering by default. That default belongs to the flag,
3392        # not to the component that ends up doing the cursor comparison: a data feed does it in the retriever and
3393        # receives no cursor here, but its `record_filter` condition must keep running after the transformations.
3394        default_transform_before_filtering = bool(
3395            client_side_incremental_sync_cursor or is_client_side_incremental_sync
3396        )
3397        transform_before_filtering = (
3398            default_transform_before_filtering
3399            if model.transform_before_filtering is None
3400            else model.transform_before_filtering
3401        )
3402        if client_side_incremental_sync_cursor:
3403            record_filter = ClientSideIncrementalRecordFilterDecorator(
3404                config=config,
3405                parameters=model.parameters,
3406                condition=model.record_filter.condition
3407                if (model.record_filter and hasattr(model.record_filter, "condition"))
3408                else None,
3409                cursor=client_side_incremental_sync_cursor,
3410            )
3411
3412        if model.schema_normalization is None:
3413            # default to no schema normalization if not set
3414            model.schema_normalization = SchemaNormalizationModel.None_
3415
3416        schema_normalization = (
3417            TypeTransformer(SCHEMA_TRANSFORMER_TYPE_MAPPING[model.schema_normalization])
3418            if isinstance(model.schema_normalization, SchemaNormalizationModel)
3419            else self._create_component_from_model(model.schema_normalization, config=config)  # type: ignore[arg-type] # custom normalization model expected here
3420        )
3421
3422        return RecordSelector(
3423            extractor=extractor,
3424            name=name,
3425            config=config,
3426            record_filter=record_filter,
3427            transformations=transformations or [],
3428            file_uploader=file_uploader,
3429            schema_normalization=schema_normalization,
3430            parameters=model.parameters or {},
3431            transform_before_filtering=transform_before_filtering,
3432        )
3433
3434    @staticmethod
3435    def create_remove_fields(
3436        model: RemoveFieldsModel, config: Config, **kwargs: Any
3437    ) -> RemoveFields:
3438        return RemoveFields(
3439            field_pointers=model.field_pointers, condition=model.condition or "", parameters={}
3440        )
3441
3442    def create_selective_authenticator(
3443        self, model: SelectiveAuthenticatorModel, config: Config, **kwargs: Any
3444    ) -> DeclarativeAuthenticator:
3445        authenticators = {
3446            name: self._create_component_from_model(model=auth, config=config)
3447            for name, auth in model.authenticators.items()
3448        }
3449        # SelectiveAuthenticator will return instance of DeclarativeAuthenticator or raise ValueError error
3450        return SelectiveAuthenticator(  # type: ignore[abstract]
3451            config=config,
3452            authenticators=authenticators,
3453            authenticator_selection_path=model.authenticator_selection_path,
3454            **kwargs,
3455        )
3456
3457    @staticmethod
3458    def create_legacy_session_token_authenticator(
3459        model: LegacySessionTokenAuthenticatorModel, config: Config, *, url_base: str, **kwargs: Any
3460    ) -> LegacySessionTokenAuthenticator:
3461        return LegacySessionTokenAuthenticator(
3462            api_url=url_base,
3463            header=model.header,
3464            login_url=model.login_url,
3465            password=model.password or "",
3466            session_token=model.session_token or "",
3467            session_token_response_key=model.session_token_response_key or "",
3468            username=model.username or "",
3469            validate_session_url=model.validate_session_url,
3470            config=config,
3471            parameters=model.parameters or {},
3472        )
3473
3474    def create_simple_retriever(
3475        self,
3476        model: SimpleRetrieverModel,
3477        config: Config,
3478        *,
3479        name: str,
3480        primary_key: Optional[Union[str, List[str], List[List[str]]]],
3481        request_options_provider: Optional[RequestOptionsProvider] = None,
3482        cursor: Optional[Cursor] = None,
3483        has_stop_condition_cursor: bool = False,
3484        is_client_side_incremental_sync: bool = False,
3485        transformations: List[RecordTransformation],
3486        file_uploader: Optional[DefaultFileUploader] = None,
3487        incremental_sync: Optional[
3488            Union[IncrementingCountCursorModel, DatetimeBasedCursorModel]
3489        ] = None,
3490        use_cache: Optional[bool] = None,
3491        log_formatter: Optional[Callable[[Response], Any]] = None,
3492        partition_router: Optional[PartitionRouter] = None,
3493        **kwargs: Any,
3494    ) -> SimpleRetriever:
3495        def _get_url(req: Requester) -> str:
3496            """
3497            Closure to get the URL from the requester. This is used to get the URL in the case of a lazy retriever.
3498            This is needed because the URL is not set until the requester is created.
3499            """
3500
3501            _url: str = (
3502                model.requester.url
3503                if hasattr(model.requester, "url") and model.requester.url is not None
3504                else req.get_url(stream_state=None, stream_slice=None, next_page_token=None)
3505            )
3506            _url_base: str = (
3507                model.requester.url_base
3508                if hasattr(model.requester, "url_base") and model.requester.url_base is not None
3509                else req.get_url_base(stream_state=None, stream_slice=None, next_page_token=None)
3510            )
3511
3512            return _url or _url_base
3513
3514        if cursor is None:
3515            cursor = FinalStateCursor(name, None, self._message_repository)
3516
3517        # A data feed drops the records the cursor considers already synced in the retriever, which sits downstream of
3518        # the paginator. Letting the record selector drop them as well would be redundant and would hide them from the
3519        # pagination stop condition, so a data feed never delegates that filtering to the record selector, whether
3520        # `is_client_side_incremental` is set or not. The `condition` from `record_filter` is intentionally left out of
3521        # the post-pagination filter and stays in the record selector, which preserves the existing behaviour: the
3522        # selector runs inside the page loop, so the records the condition rejects never reach the paginator's
3523        # accounting. Moving it downstream would start counting them.
3524        post_pagination_filter = (
3525            ClientSideIncrementalRecordFilterDecorator(
3526                config=config,
3527                parameters=model.parameters or {},
3528                condition=None,
3529                cursor=cursor,
3530            )
3531            if has_stop_condition_cursor
3532            else None
3533        )
3534        client_side_incremental_cursor = (
3535            cursor if is_client_side_incremental_sync and not post_pagination_filter else None
3536        )
3537        if post_pagination_filter and is_client_side_incremental_sync:
3538            LOGGER.warning(
3539                f"Stream {name}: `is_client_side_incremental` adds no record filtering when `is_data_feed` is set, "
3540                "as a data feed already filters on the cursor value. It still makes the record selector apply the "
3541                "transformations before the `record_filter` condition."
3542            )
3543
3544        decoder = (
3545            self._create_component_from_model(model=model.decoder, config=config)
3546            if model.decoder
3547            else JsonDecoder(parameters={})
3548        )
3549        record_selector = self._create_component_from_model(
3550            model=model.record_selector,
3551            name=name,
3552            config=config,
3553            decoder=decoder,
3554            transformations=transformations,
3555            client_side_incremental_sync_cursor=client_side_incremental_cursor,
3556            is_client_side_incremental_sync=is_client_side_incremental_sync,
3557            file_uploader=file_uploader,
3558        )
3559
3560        query_properties: Optional[QueryProperties] = None
3561        query_properties_key: Optional[str] = None
3562        self._ensure_query_properties_to_model(model.requester)
3563        if self._has_query_properties_in_request_parameters(model.requester):
3564            # It is better to be explicit about an error if PropertiesFromEndpoint is defined in multiple
3565            # places instead of default to request_parameters which isn't clearly documented
3566            if (
3567                hasattr(model.requester, "fetch_properties_from_endpoint")
3568                and model.requester.fetch_properties_from_endpoint
3569            ):
3570                raise ValueError(
3571                    f"PropertiesFromEndpoint should only be specified once per stream, but found in {model.requester.type}.fetch_properties_from_endpoint and {model.requester.type}.request_parameters"
3572                )
3573
3574            query_properties_definitions = []
3575            for key, request_parameter in model.requester.request_parameters.items():  # type: ignore # request_parameters is already validated to be a Mapping using _has_query_properties_in_request_parameters()
3576                if isinstance(request_parameter, QueryPropertiesModel):
3577                    query_properties_key = key
3578                    query_properties_definitions.append(request_parameter)
3579
3580            if len(query_properties_definitions) > 1:
3581                raise ValueError(
3582                    f"request_parameters only supports defining one QueryProperties field, but found {len(query_properties_definitions)} usages"
3583                )
3584
3585            if len(query_properties_definitions) == 1:
3586                query_properties = self._create_component_from_model(
3587                    model=query_properties_definitions[0], stream_name=name, config=config
3588                )
3589
3590            # Removes QueryProperties components from the interpolated mappings because it has been designed
3591            # to be used by the SimpleRetriever and will be resolved from the provider from the slice directly
3592            # instead of through jinja interpolation
3593            if hasattr(model.requester, "request_parameters") and isinstance(
3594                model.requester.request_parameters, Mapping
3595            ):
3596                model.requester.request_parameters = self._remove_query_properties(
3597                    model.requester.request_parameters
3598                )
3599        elif (
3600            hasattr(model.requester, "fetch_properties_from_endpoint")
3601            and model.requester.fetch_properties_from_endpoint
3602        ):
3603            # todo: Deprecate this condition once dependent connectors migrate to query_properties
3604            query_properties_definition = QueryPropertiesModel(
3605                type="QueryProperties",
3606                property_list=model.requester.fetch_properties_from_endpoint,
3607                always_include_properties=None,
3608                property_chunking=None,
3609            )  # type: ignore # $parameters has a default value
3610
3611            query_properties = self.create_query_properties(
3612                model=query_properties_definition,
3613                stream_name=name,
3614                config=config,
3615            )
3616        elif hasattr(model.requester, "query_properties") and model.requester.query_properties:
3617            query_properties = self.create_query_properties(
3618                model=model.requester.query_properties,
3619                stream_name=name,
3620                config=config,
3621            )
3622
3623        requester = self._create_component_from_model(
3624            model=model.requester,
3625            decoder=decoder,
3626            name=name,
3627            query_properties_key=query_properties_key,
3628            use_cache=use_cache,
3629            config=config,
3630        )
3631
3632        if not request_options_provider:
3633            request_options_provider = DefaultRequestOptionsProvider(parameters={})
3634        if isinstance(request_options_provider, DefaultRequestOptionsProvider) and isinstance(
3635            partition_router, PartitionRouter
3636        ):
3637            request_options_provider = partition_router
3638
3639        paginator = (
3640            self._create_component_from_model(
3641                model=model.paginator,
3642                config=config,
3643                url_base=_get_url(requester),
3644                extractor_model=model.record_selector.extractor,
3645                decoder=decoder,
3646                cursor_used_for_stop_condition=cursor if has_stop_condition_cursor else None,
3647            )
3648            if model.paginator
3649            else NoPagination(parameters={})
3650        )
3651
3652        ignore_stream_slicer_parameters_on_paginated_requests = (
3653            model.ignore_stream_slicer_parameters_on_paginated_requests or False
3654        )
3655
3656        if (
3657            model.partition_router
3658            and isinstance(model.partition_router, SubstreamPartitionRouterModel)
3659            and not bool(self._connector_state_manager.get_stream_state(name, None))
3660            and any(
3661                parent_stream_config.lazy_read_pointer
3662                for parent_stream_config in model.partition_router.parent_stream_configs
3663            )
3664        ):
3665            if incremental_sync:
3666                if incremental_sync.type != "DatetimeBasedCursor":
3667                    raise ValueError(
3668                        f"LazySimpleRetriever only supports DatetimeBasedCursor. Found: {incremental_sync.type}."
3669                    )
3670
3671                elif incremental_sync.step or incremental_sync.cursor_granularity:
3672                    raise ValueError(
3673                        f"Found more that one slice per parent. LazySimpleRetriever only supports single slice read for stream - {name}."
3674                    )
3675
3676            if model.decoder and model.decoder.type != "JsonDecoder":
3677                raise ValueError(
3678                    f"LazySimpleRetriever only supports JsonDecoder. Found: {model.decoder.type}."
3679                )
3680
3681            return LazySimpleRetriever(
3682                name=name,
3683                paginator=paginator,
3684                primary_key=primary_key,
3685                requester=requester,
3686                record_selector=record_selector,
3687                stream_slicer=_NO_STREAM_SLICING,
3688                request_option_provider=request_options_provider,
3689                config=config,
3690                ignore_stream_slicer_parameters_on_paginated_requests=ignore_stream_slicer_parameters_on_paginated_requests,
3691                post_pagination_filter=post_pagination_filter,
3692                parameters=model.parameters or {},
3693            )
3694
3695        if (
3696            model.record_selector.record_filter
3697            and model.pagination_reset
3698            and model.pagination_reset.limits
3699        ):
3700            raise ValueError("PaginationResetLimits are not supported while having record filter.")
3701
3702        return SimpleRetriever(
3703            name=name,
3704            paginator=paginator,
3705            primary_key=primary_key,
3706            requester=requester,
3707            record_selector=record_selector,
3708            stream_slicer=_NO_STREAM_SLICING,
3709            request_option_provider=request_options_provider,
3710            config=config,
3711            ignore_stream_slicer_parameters_on_paginated_requests=ignore_stream_slicer_parameters_on_paginated_requests,
3712            additional_query_properties=query_properties,
3713            log_formatter=self._get_log_formatter(log_formatter, name),
3714            pagination_tracker_factory=self._create_pagination_tracker_factory(
3715                model.pagination_reset, cursor
3716            ),
3717            post_pagination_filter=post_pagination_filter,
3718            parameters=model.parameters or {},
3719        )
3720
3721    def _create_pagination_tracker_factory(
3722        self, model: Optional[PaginationResetModel], cursor: Cursor
3723    ) -> Callable[[], PaginationTracker]:
3724        if model is None:
3725            return lambda: PaginationTracker()
3726
3727        # Until we figure out a way to use any cursor for PaginationTracker, we will have to have this cursor selector logic
3728        cursor_factory: Callable[[], Optional[ConcurrentCursor]] = lambda: None
3729        if model.action == PaginationResetActionModel.RESET:
3730            # in that case, we will let cursor_factory to return None even if the stream has a cursor
3731            pass
3732        elif model.action == PaginationResetActionModel.SPLIT_USING_CURSOR:
3733            if isinstance(cursor, ConcurrentCursor):
3734                cursor_factory = lambda: cursor.copy_without_state()  # type: ignore  # the if condition validates that it is a ConcurrentCursor
3735            elif isinstance(cursor, ConcurrentPerPartitionCursor):
3736                cursor_factory = lambda: cursor._cursor_factory.create(  # type: ignore  # if this becomes a problem, we would need to extract the cursor_factory instantiation logic and make it accessible here
3737                    {}, datetime.timedelta(0)
3738                )
3739            elif not isinstance(cursor, FinalStateCursor):
3740                LOGGER.warning(
3741                    "Unknown cursor for PaginationTracker. Pagination resets might not work properly"
3742                )
3743        else:
3744            raise ValueError(f"Unknown PaginationReset action: {model.action}")
3745
3746        limit = model.limits.number_of_records if model and model.limits else None
3747        return lambda: PaginationTracker(cursor_factory(), limit)
3748
3749    def _get_log_formatter(
3750        self, log_formatter: Callable[[Response], Any] | None, name: str
3751    ) -> Callable[[Response], Any] | None:
3752        if self._should_limit_slices_fetched():
3753            return (
3754                (
3755                    lambda response: format_http_message(
3756                        response,
3757                        f"Stream '{name}' request",
3758                        f"Request performed in order to extract records for stream '{name}'",
3759                        name,
3760                    )
3761                )
3762                if not log_formatter
3763                else log_formatter
3764            )
3765        return None
3766
3767    def _should_limit_slices_fetched(self) -> bool:
3768        """
3769        Returns True if the number of slices fetched should be limited, False otherwise.
3770        This is used to limit the number of slices fetched during tests.
3771        """
3772        return bool(self._limit_slices_fetched or self._emit_connector_builder_messages)
3773
3774    @staticmethod
3775    def _has_query_properties_in_request_parameters(
3776        requester: Union[HttpRequesterModel, CustomRequesterModel],
3777    ) -> bool:
3778        if not hasattr(requester, "request_parameters"):
3779            return False
3780        request_parameters = requester.request_parameters
3781        if request_parameters and isinstance(request_parameters, Mapping):
3782            for request_parameter in request_parameters.values():
3783                if isinstance(request_parameter, QueryPropertiesModel):
3784                    return True
3785        return False
3786
3787    @staticmethod
3788    def _remove_query_properties(
3789        request_parameters: Mapping[str, Union[str, QueryPropertiesModel]],
3790    ) -> Mapping[str, str]:
3791        return {
3792            parameter_field: request_parameter
3793            for parameter_field, request_parameter in request_parameters.items()
3794            if not isinstance(request_parameter, QueryPropertiesModel)
3795        }
3796
3797    def create_state_delegating_stream(
3798        self,
3799        model: StateDelegatingStreamModel,
3800        config: Config,
3801        **kwargs: Any,
3802    ) -> DefaultStream:
3803        if (
3804            model.full_refresh_stream.name != model.name
3805            or model.name != model.incremental_stream.name
3806        ):
3807            raise ValueError(
3808                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}."
3809            )
3810
3811        # Resolve api_retention_period with config context (supports Jinja2 interpolation)
3812        resolved_retention_period: Optional[str] = None
3813        if model.api_retention_period:
3814            interpolated_retention = InterpolatedString.create(
3815                model.api_retention_period, parameters=model.parameters or {}
3816            )
3817            resolved_value = interpolated_retention.eval(config=config)
3818            if resolved_value:
3819                resolved_retention_period = str(resolved_value)
3820
3821        if resolved_retention_period:
3822            for stream_model in (model.full_refresh_stream, model.incremental_stream):
3823                if isinstance(stream_model.incremental_sync, IncrementingCountCursorModel):
3824                    raise ValueError(
3825                        f"Stream '{model.name}' uses IncrementingCountCursor which is not supported "
3826                        f"with api_retention_period. IncrementingCountCursor does not use datetime-based "
3827                        f"cursors, so cursor age validation cannot be performed."
3828                    )
3829
3830        stream_state = self._connector_state_manager.get_stream_state(model.name, None)
3831
3832        if not stream_state:
3833            return self._create_component_from_model(  # type: ignore[no-any-return]
3834                model.full_refresh_stream, config=config, **kwargs
3835            )
3836
3837        incremental_stream: DefaultStream = self._create_component_from_model(
3838            model.incremental_stream, config=config, **kwargs
3839        )  # type: ignore[assignment]
3840
3841        # Only run cursor age validation for streams that are in the configured
3842        # catalog (or when no catalog was provided, e.g. during discover / connector
3843        # builder).  Streams not selected by the user but instantiated as parent-stream
3844        # dependencies must not go through this path because it emits state messages
3845        # that the destination does not know about, causing "Stream not found" crashes.
3846        stream_is_in_catalog = (
3847            not self._stream_name_to_configured_stream  # no catalog → validate by default
3848            or model.name in self._stream_name_to_configured_stream
3849        )
3850        if resolved_retention_period and stream_is_in_catalog:
3851            full_refresh_stream: DefaultStream = self._create_component_from_model(
3852                model.full_refresh_stream, config=config, **kwargs
3853            )  # type: ignore[assignment]
3854            if self._is_cursor_older_than_retention_period(
3855                stream_state,
3856                full_refresh_stream.cursor,
3857                incremental_stream.cursor,
3858                resolved_retention_period,
3859                model.name,
3860            ):
3861                # Clear state BEFORE constructing the full_refresh_stream so that
3862                # its cursor starts from start_date instead of the stale cursor.
3863                self._connector_state_manager.update_state_for_stream(model.name, None, {})
3864                state_message = self._connector_state_manager.create_state_message(model.name, None)
3865                self._message_repository.emit_message(state_message)
3866                return self._create_component_from_model(  # type: ignore[no-any-return]
3867                    model.full_refresh_stream, config=config, **kwargs
3868                )
3869
3870        return incremental_stream
3871
3872    @staticmethod
3873    def _is_cursor_older_than_retention_period(
3874        stream_state: Mapping[str, Any],
3875        full_refresh_cursor: Cursor,
3876        incremental_cursor: Cursor,
3877        api_retention_period: str,
3878        stream_name: str,
3879    ) -> bool:
3880        """Check if the cursor value in the state is older than the API's retention period.
3881
3882        Checks cursors in sequence: full refresh cursor first, then incremental cursor.
3883        FinalStateCursor returns now() for completed full refresh state (NO_CURSOR_STATE_KEY),
3884        which is always within retention, so we use incremental. For other states, it returns
3885        None and we fall back to checking the incremental cursor.
3886
3887        Returns True if the cursor is older than the retention period (should use full refresh).
3888        Returns False if the cursor is within the retention period (safe to use incremental).
3889        """
3890        retention_duration = parse_duration(api_retention_period)
3891        retention_cutoff = datetime.datetime.now(datetime.timezone.utc) - retention_duration
3892
3893        # Check full refresh cursor first
3894        cursor_datetime = full_refresh_cursor.get_cursor_datetime_from_state(stream_state)
3895
3896        # If full refresh cursor returns None, check incremental cursor
3897        if cursor_datetime is None:
3898            cursor_datetime = incremental_cursor.get_cursor_datetime_from_state(stream_state)
3899
3900        if cursor_datetime is None:
3901            # Neither cursor could parse the state - fall back to full refresh to be safe
3902            return True
3903
3904        if cursor_datetime < retention_cutoff:
3905            logging.warning(
3906                f"Stream '{stream_name}' has a cursor value older than "
3907                f"the API's retention period of {api_retention_period} "
3908                f"(cutoff: {retention_cutoff.isoformat()}). "
3909                f"Falling back to full refresh to avoid data loss."
3910            )
3911            return True
3912
3913        return False
3914
3915    def _get_state_delegating_stream_model(
3916        self,
3917        model: StateDelegatingStreamModel,
3918        parent_state: Optional[Mapping[str, Any]] = None,
3919    ) -> DeclarativeStreamModel:
3920        """Return the appropriate underlying stream model based on state."""
3921        return (
3922            model.incremental_stream
3923            if self._connector_state_manager.get_stream_state(model.name, None) or parent_state
3924            else model.full_refresh_stream
3925        )
3926
3927    _OPTIONAL_ASYNC_STATUS_FIELDS = {"skipped"}
3928
3929    def _create_async_job_status_mapping(
3930        self, model: AsyncJobStatusMapModel, config: Config, **kwargs: Any
3931    ) -> Mapping[str, AsyncJobStatus]:
3932        api_status_to_cdk_status = {}
3933        for cdk_status, api_statuses in model.dict().items():
3934            if cdk_status == "type":
3935                # This is an element of the dict because of the typing of the CDK but it is not a CDK status
3936                continue
3937
3938            if api_statuses is None:
3939                if cdk_status in self._OPTIONAL_ASYNC_STATUS_FIELDS:
3940                    continue
3941                raise ValueError(
3942                    f"Required CDK status '{cdk_status}' has no API statuses mapped. "
3943                    f"Please provide at least an empty list for required status fields."
3944                )
3945
3946            for status in api_statuses:
3947                if status in api_status_to_cdk_status:
3948                    raise ValueError(
3949                        f"API status {status} is already set for CDK status {cdk_status}. Please ensure API statuses are only provided once"
3950                    )
3951                api_status_to_cdk_status[status] = self._get_async_job_status(cdk_status)
3952        return api_status_to_cdk_status
3953
3954    def _get_async_job_status(self, status: str) -> AsyncJobStatus:
3955        match status:
3956            case "running":
3957                return AsyncJobStatus.RUNNING
3958            case "completed":
3959                return AsyncJobStatus.COMPLETED
3960            case "failed":
3961                return AsyncJobStatus.FAILED
3962            case "timeout":
3963                return AsyncJobStatus.TIMED_OUT
3964            case "skipped":
3965                return AsyncJobStatus.SKIPPED
3966            case _:
3967                raise ValueError(f"Unsupported CDK status {status}")
3968
3969    def create_async_retriever(
3970        self,
3971        model: AsyncRetrieverModel,
3972        config: Config,
3973        *,
3974        name: str,
3975        primary_key: Optional[
3976            Union[str, List[str], List[List[str]]]
3977        ],  # this seems to be needed to match create_simple_retriever
3978        stream_slicer: Optional[StreamSlicer],
3979        client_side_incremental_sync: Optional[Dict[str, Any]] = None,
3980        transformations: List[RecordTransformation],
3981        **kwargs: Any,
3982    ) -> AsyncRetriever:
3983        if model.download_target_requester and not model.download_target_extractor:
3984            raise ValueError(
3985                f"`download_target_extractor` required if using a `download_target_requester`"
3986            )
3987
3988        def _get_download_retriever(
3989            requester: Requester, extractor: RecordExtractor, _decoder: Decoder
3990        ) -> SimpleRetriever:
3991            # We create a record selector for the download retriever
3992            # with no schema normalization and no transformations, neither record filter
3993            # as all this occurs in the record_selector of the AsyncRetriever
3994            record_selector = RecordSelector(
3995                extractor=extractor,
3996                name=name,
3997                record_filter=None,
3998                transformations=[],
3999                schema_normalization=TypeTransformer(TransformConfig.NoTransform),
4000                config=config,
4001                parameters={},
4002            )
4003            paginator = (
4004                self._create_component_from_model(
4005                    model=model.download_paginator,
4006                    decoder=_decoder,
4007                    config=config,
4008                    url_base="",
4009                )
4010                if model.download_paginator
4011                else NoPagination(parameters={})
4012            )
4013
4014            return SimpleRetriever(
4015                requester=requester,
4016                record_selector=record_selector,
4017                primary_key=None,
4018                name=name,
4019                paginator=paginator,
4020                config=config,
4021                parameters={},
4022                log_formatter=self._get_log_formatter(None, name),
4023            )
4024
4025        def _get_job_timeout() -> datetime.timedelta:
4026            user_defined_timeout: Optional[int] = (
4027                int(
4028                    InterpolatedString.create(
4029                        str(model.polling_job_timeout),
4030                        parameters={},
4031                    ).eval(config)
4032                )
4033                if model.polling_job_timeout
4034                else None
4035            )
4036
4037            # check for user defined timeout during the test read or 15 minutes
4038            test_read_timeout = datetime.timedelta(minutes=user_defined_timeout or 15)
4039            # default value for non-connector builder is 60 minutes.
4040            default_sync_timeout = datetime.timedelta(minutes=user_defined_timeout or 60)
4041
4042            return (
4043                test_read_timeout if self._emit_connector_builder_messages else default_sync_timeout
4044            )
4045
4046        decoder = (
4047            self._create_component_from_model(model=model.decoder, config=config)
4048            if model.decoder
4049            else JsonDecoder(parameters={})
4050        )
4051        record_selector = self._create_component_from_model(
4052            model=model.record_selector,
4053            config=config,
4054            decoder=decoder,
4055            name=name,
4056            transformations=transformations,
4057            client_side_incremental_sync=client_side_incremental_sync,
4058        )
4059
4060        stream_slicer = stream_slicer or SinglePartitionRouter(parameters={})
4061        if self._should_limit_slices_fetched():
4062            stream_slicer = cast(
4063                StreamSlicer,
4064                StreamSlicerTestReadDecorator(
4065                    wrapped_slicer=stream_slicer,
4066                    maximum_number_of_slices=self._limit_slices_fetched or 5,
4067                ),
4068            )
4069
4070        creation_requester = self._create_component_from_model(
4071            model=model.creation_requester,
4072            decoder=decoder,
4073            config=config,
4074            name=f"job creation - {name}",
4075        )
4076        polling_requester = self._create_component_from_model(
4077            model=model.polling_requester,
4078            decoder=decoder,
4079            config=config,
4080            name=f"job polling - {name}",
4081        )
4082        job_download_components_name = f"job download - {name}"
4083        download_decoder = (
4084            self._create_component_from_model(model=model.download_decoder, config=config)
4085            if model.download_decoder
4086            else JsonDecoder(parameters={})
4087        )
4088        download_extractor = (
4089            self._create_component_from_model(
4090                model=model.download_extractor,
4091                config=config,
4092                decoder=download_decoder,
4093                parameters=model.parameters,
4094            )
4095            if model.download_extractor
4096            else DpathExtractor(
4097                [],
4098                config=config,
4099                decoder=download_decoder,
4100                parameters=model.parameters or {},
4101            )
4102        )
4103        download_requester = self._create_component_from_model(
4104            model=model.download_requester,
4105            decoder=download_decoder,
4106            config=config,
4107            name=job_download_components_name,
4108        )
4109        download_retriever = _get_download_retriever(
4110            download_requester, download_extractor, download_decoder
4111        )
4112        abort_requester = (
4113            self._create_component_from_model(
4114                model=model.abort_requester,
4115                decoder=decoder,
4116                config=config,
4117                name=f"job abort - {name}",
4118            )
4119            if model.abort_requester
4120            else None
4121        )
4122        delete_requester = (
4123            self._create_component_from_model(
4124                model=model.delete_requester,
4125                decoder=decoder,
4126                config=config,
4127                name=f"job delete - {name}",
4128            )
4129            if model.delete_requester
4130            else None
4131        )
4132        download_target_requester = (
4133            self._create_component_from_model(
4134                model=model.download_target_requester,
4135                decoder=decoder,
4136                config=config,
4137                name=f"job extract_url - {name}",
4138            )
4139            if model.download_target_requester
4140            else None
4141        )
4142        status_extractor = self._create_component_from_model(
4143            model=model.status_extractor, decoder=decoder, config=config, name=name
4144        )
4145        download_target_extractor = (
4146            self._create_component_from_model(
4147                model=model.download_target_extractor,
4148                decoder=decoder,
4149                config=config,
4150                name=name,
4151            )
4152            if model.download_target_extractor
4153            else None
4154        )
4155
4156        job_repository: AsyncJobRepository = AsyncHttpJobRepository(
4157            creation_requester=creation_requester,
4158            polling_requester=polling_requester,
4159            download_retriever=download_retriever,
4160            download_target_requester=download_target_requester,
4161            abort_requester=abort_requester,
4162            delete_requester=delete_requester,
4163            status_extractor=status_extractor,
4164            status_mapping=self._create_async_job_status_mapping(model.status_mapping, config),
4165            download_target_extractor=download_target_extractor,
4166            job_timeout=_get_job_timeout(),
4167        )
4168
4169        failed_retry_wait_time_in_seconds: Optional[int] = (
4170            int(
4171                InterpolatedString.create(
4172                    str(model.failed_retry_wait_time_in_seconds),
4173                    parameters={},
4174                ).eval(config)
4175            )
4176            if model.failed_retry_wait_time_in_seconds
4177            else None
4178        )
4179
4180        async_job_partition_router = AsyncJobPartitionRouter(
4181            job_orchestrator_factory=lambda stream_slices: AsyncJobOrchestrator(
4182                job_repository,
4183                stream_slices,
4184                self._job_tracker,
4185                self._message_repository,
4186                # FIXME work would need to be done here in order to detect if a stream as a parent stream that is bulk
4187                has_bulk_parent=False,
4188                # set the `job_max_retry` to 1 for the `Connector Builder`` use-case.
4189                # `None` == default retry is set to 3 attempts, under the hood.
4190                job_max_retry=1 if self._emit_connector_builder_messages else None,
4191                failed_retry_wait_time_in_seconds=failed_retry_wait_time_in_seconds,
4192            ),
4193            stream_slicer=stream_slicer,
4194            config=config,
4195            parameters=model.parameters or {},
4196        )
4197
4198        return AsyncRetriever(
4199            record_selector=record_selector,
4200            stream_slicer=async_job_partition_router,
4201            config=config,
4202            parameters=model.parameters or {},
4203        )
4204
4205    def create_spec(self, model: SpecModel, config: Config, **kwargs: Any) -> Spec:
4206        config_migrations = [
4207            self._create_component_from_model(migration, config)
4208            for migration in (
4209                model.config_normalization_rules.config_migrations
4210                if (
4211                    model.config_normalization_rules
4212                    and model.config_normalization_rules.config_migrations
4213                )
4214                else []
4215            )
4216        ]
4217        config_transformations = [
4218            self._create_component_from_model(transformation, config)
4219            for transformation in (
4220                model.config_normalization_rules.transformations
4221                if (
4222                    model.config_normalization_rules
4223                    and model.config_normalization_rules.transformations
4224                )
4225                else []
4226            )
4227        ]
4228        config_validations = [
4229            self._create_component_from_model(validation, config)
4230            for validation in (
4231                model.config_normalization_rules.validations
4232                if (
4233                    model.config_normalization_rules
4234                    and model.config_normalization_rules.validations
4235                )
4236                else []
4237            )
4238        ]
4239
4240        return Spec(
4241            connection_specification=model.connection_specification,
4242            documentation_url=model.documentation_url,
4243            advanced_auth=model.advanced_auth,
4244            parameters={},
4245            config_migrations=config_migrations,
4246            config_transformations=config_transformations,
4247            config_validations=config_validations,
4248        )
4249
4250    def create_substream_partition_router(
4251        self,
4252        model: SubstreamPartitionRouterModel,
4253        config: Config,
4254        *,
4255        stream_name: str,
4256        **kwargs: Any,
4257    ) -> SubstreamPartitionRouter:
4258        parent_stream_configs = []
4259        if model.parent_stream_configs:
4260            parent_stream_configs.extend(
4261                [
4262                    self.create_parent_stream_config_with_substream_wrapper(
4263                        model=parent_stream_config, config=config, stream_name=stream_name, **kwargs
4264                    )
4265                    for parent_stream_config in model.parent_stream_configs
4266                ]
4267            )
4268
4269        return SubstreamPartitionRouter(
4270            parent_stream_configs=parent_stream_configs,
4271            parameters=model.parameters or {},
4272            config=config,
4273        )
4274
4275    def create_parent_stream_config_with_substream_wrapper(
4276        self, model: ParentStreamConfigModel, config: Config, *, stream_name: str, **kwargs: Any
4277    ) -> Any:
4278        child_state = self._connector_state_manager.get_stream_state(stream_name, None)
4279        if NO_CURSOR_STATE_KEY in child_state:
4280            # Full refresh streams checkpoint a `{NO_CURSOR_STATE_KEY: true}` sentinel. When such a
4281            # stream is later converted to incremental with an incremental_dependency parent,
4282            # `_instantiate_parent_stream_state_manager` would treat the sentinel's boolean as a legacy
4283            # cursor value and re-key it under the parent's cursor field, crashing cursor initialization.
4284            child_state = {
4285                key: value for key, value in child_state.items() if key != NO_CURSOR_STATE_KEY
4286            }
4287
4288        parent_state: Optional[Mapping[str, Any]] = (
4289            child_state if model.incremental_dependency and child_state else None
4290        )
4291        connector_state_manager = self._instantiate_parent_stream_state_manager(
4292            child_state, config, model, parent_state
4293        )
4294
4295        substream_factory = ModelToComponentFactory(
4296            custom_components_trusted=self._custom_components_trusted,
4297            connector_state_manager=connector_state_manager,
4298            limit_pages_fetched_per_slice=self._limit_pages_fetched_per_slice,
4299            limit_slices_fetched=self._limit_slices_fetched,
4300            emit_connector_builder_messages=self._emit_connector_builder_messages,
4301            disable_retries=self._disable_retries,
4302            disable_cache=self._disable_cache,
4303            message_repository=StateFilteringMessageRepository(
4304                LogAppenderMessageRepositoryDecorator(
4305                    {
4306                        "airbyte_cdk": {"stream": {"is_substream": True}},
4307                        "http": {"is_auxiliary": True},
4308                    },
4309                    self._message_repository,
4310                    self._evaluate_log_level(self._emit_connector_builder_messages),
4311                ),
4312            ),
4313            api_budget=self._api_budget,
4314            # Share the authenticator registry so parent and child streams draw from the
4315            # same token quota counters
4316            rate_limited_authenticators=self._rate_limited_authenticators,
4317        )
4318
4319        return substream_factory.create_parent_stream_config(
4320            model=model, config=config, stream_name=stream_name, **kwargs
4321        )
4322
4323    def _instantiate_parent_stream_state_manager(
4324        self,
4325        child_state: MutableMapping[str, Any],
4326        config: Config,
4327        model: ParentStreamConfigModel,
4328        parent_state: Optional[Mapping[str, Any]] = None,
4329    ) -> ConnectorStateManager:
4330        """
4331        With DefaultStream, the state needs to be provided during __init__ of the cursor as opposed to the
4332        `set_initial_state` flow that existed for the declarative cursors. This state is taken from
4333        self._connector_state_manager.get_stream_state (`self` being a newly created ModelToComponentFactory to account
4334        for the MessageRepository being different). So we need to pass a ConnectorStateManager to the
4335        ModelToComponentFactory that has the parent states. This method populates this if there is a child state and if
4336        incremental_dependency is set.
4337        """
4338        if model.incremental_dependency and child_state:
4339            parent_stream_name = model.stream.name or ""
4340            extracted_parent_state = ConcurrentPerPartitionCursor.get_parent_state(
4341                child_state, parent_stream_name
4342            )
4343
4344            if not extracted_parent_state:
4345                extracted_parent_state = ConcurrentPerPartitionCursor.get_global_state(
4346                    child_state, parent_stream_name
4347                )
4348
4349                if not extracted_parent_state and not isinstance(extracted_parent_state, dict):
4350                    cursor_values = child_state.values()
4351                    if cursor_values and len(cursor_values) == 1:
4352                        incremental_sync_model: Union[
4353                            DatetimeBasedCursorModel,
4354                            IncrementingCountCursorModel,
4355                        ] = (
4356                            model.stream.incremental_sync  # type: ignore  # if we are there, it is because there is incremental_dependency and therefore there is an incremental_sync on the parent stream
4357                            if isinstance(model.stream, DeclarativeStreamModel)
4358                            else self._get_state_delegating_stream_model(
4359                                model.stream, parent_state=parent_state
4360                            ).incremental_sync
4361                        )
4362                        cursor_field = InterpolatedString.create(
4363                            incremental_sync_model.cursor_field,
4364                            parameters=incremental_sync_model.parameters or {},
4365                        ).eval(config)
4366                        extracted_parent_state = AirbyteStateMessage(
4367                            type=AirbyteStateType.STREAM,
4368                            stream=AirbyteStreamState(
4369                                stream_descriptor=StreamDescriptor(
4370                                    name=parent_stream_name, namespace=None
4371                                ),
4372                                stream_state=AirbyteStateBlob(
4373                                    {cursor_field: list(cursor_values)[0]}
4374                                ),
4375                            ),
4376                        )
4377            return ConnectorStateManager([extracted_parent_state] if extracted_parent_state else [])
4378
4379        return ConnectorStateManager([])
4380
4381    @staticmethod
4382    def create_wait_time_from_header(
4383        model: WaitTimeFromHeaderModel, config: Config, **kwargs: Any
4384    ) -> WaitTimeFromHeaderBackoffStrategy:
4385        return WaitTimeFromHeaderBackoffStrategy(
4386            header=model.header,
4387            parameters=model.parameters or {},
4388            config=config,
4389            regex=model.regex,
4390            max_waiting_time_in_seconds=model.max_waiting_time_in_seconds,
4391        )
4392
4393    @staticmethod
4394    def create_wait_until_time_from_header(
4395        model: WaitUntilTimeFromHeaderModel, config: Config, **kwargs: Any
4396    ) -> WaitUntilTimeFromHeaderBackoffStrategy:
4397        return WaitUntilTimeFromHeaderBackoffStrategy(
4398            header=model.header,
4399            parameters=model.parameters or {},
4400            config=config,
4401            min_wait=model.min_wait,
4402            regex=model.regex,
4403            max_waiting_time_in_seconds=model.max_waiting_time_in_seconds,
4404        )
4405
4406    def get_message_repository(self) -> MessageRepository:
4407        return self._message_repository
4408
4409    def _evaluate_log_level(self, emit_connector_builder_messages: bool) -> Level:
4410        return Level.DEBUG if emit_connector_builder_messages else Level.INFO
4411
4412    @staticmethod
4413    def create_components_mapping_definition(
4414        model: ComponentMappingDefinitionModel, config: Config, **kwargs: Any
4415    ) -> ComponentMappingDefinition:
4416        interpolated_value = InterpolatedString.create(
4417            model.value, parameters=model.parameters or {}
4418        )
4419        field_path = [
4420            InterpolatedString.create(path, parameters=model.parameters or {})
4421            for path in model.field_path
4422        ]
4423        return ComponentMappingDefinition(
4424            field_path=field_path,  # type: ignore[arg-type] # field_path can be str and InterpolatedString
4425            value=interpolated_value,
4426            value_type=ModelToComponentFactory._json_schema_type_name_to_type(model.value_type),
4427            create_or_update=model.create_or_update,
4428            condition=model.condition,
4429            parameters=model.parameters or {},
4430        )
4431
4432    def create_http_components_resolver(
4433        self, model: HttpComponentsResolverModel, config: Config, stream_name: Optional[str] = None
4434    ) -> Any:
4435        retriever = self._create_component_from_model(
4436            model=model.retriever,
4437            config=config,
4438            name=f"{stream_name if stream_name else '__http_components_resolver'}",
4439            primary_key=None,
4440            stream_slicer=self._build_stream_slicer_from_partition_router(model.retriever, config),
4441            transformations=[],
4442        )
4443
4444        components_mapping = []
4445        for component_mapping_definition_model in model.components_mapping:
4446            if component_mapping_definition_model.condition:
4447                raise ValueError("`condition` is only supported for     `ConfigComponentsResolver`")
4448            components_mapping.append(
4449                self._create_component_from_model(
4450                    model=component_mapping_definition_model,
4451                    value_type=ModelToComponentFactory._json_schema_type_name_to_type(
4452                        component_mapping_definition_model.value_type
4453                    ),
4454                    config=config,
4455                )
4456            )
4457
4458        return HttpComponentsResolver(
4459            retriever=retriever,
4460            stream_slicer=self._build_stream_slicer_from_partition_router(model.retriever, config),
4461            config=config,
4462            components_mapping=components_mapping,
4463            parameters=model.parameters or {},
4464        )
4465
4466    @staticmethod
4467    def create_stream_config(
4468        model: StreamConfigModel, config: Config, **kwargs: Any
4469    ) -> StreamConfig:
4470        model_configs_pointer: List[Union[InterpolatedString, str]] = (
4471            [x for x in model.configs_pointer] if model.configs_pointer else []
4472        )
4473
4474        return StreamConfig(
4475            configs_pointer=model_configs_pointer,
4476            default_values=model.default_values,
4477            parameters=model.parameters or {},
4478        )
4479
4480    def create_config_components_resolver(
4481        self,
4482        model: ConfigComponentsResolverModel,
4483        config: Config,
4484    ) -> Any:
4485        model_stream_configs = (
4486            model.stream_config if isinstance(model.stream_config, list) else [model.stream_config]
4487        )
4488
4489        stream_configs = [
4490            self._create_component_from_model(
4491                stream_config, config=config, parameters=model.parameters or {}
4492            )
4493            for stream_config in model_stream_configs
4494        ]
4495
4496        components_mapping = [
4497            self._create_component_from_model(
4498                model=components_mapping_definition_model,
4499                value_type=ModelToComponentFactory._json_schema_type_name_to_type(
4500                    components_mapping_definition_model.value_type
4501                ),
4502                config=config,
4503                parameters=model.parameters,
4504            )
4505            for components_mapping_definition_model in model.components_mapping
4506        ]
4507
4508        return ConfigComponentsResolver(
4509            stream_configs=stream_configs,
4510            config=config,
4511            components_mapping=components_mapping,
4512            parameters=model.parameters or {},
4513        )
4514
4515    def create_parametrized_components_resolver(
4516        self,
4517        model: ParametrizedComponentsResolverModel,
4518        config: Config,
4519    ) -> ParametrizedComponentsResolver:
4520        stream_parameters = StreamParametersDefinition(
4521            list_of_parameters_for_stream=model.stream_parameters.list_of_parameters_for_stream
4522        )
4523
4524        components_mapping = []
4525        for components_mapping_definition_model in model.components_mapping:
4526            if components_mapping_definition_model.condition:
4527                raise ValueError("`condition` is only supported for `ConfigComponentsResolver`")
4528            components_mapping.append(
4529                self._create_component_from_model(
4530                    model=components_mapping_definition_model,
4531                    value_type=ModelToComponentFactory._json_schema_type_name_to_type(
4532                        components_mapping_definition_model.value_type
4533                    ),
4534                    config=config,
4535                )
4536            )
4537        return ParametrizedComponentsResolver(
4538            stream_parameters=stream_parameters,
4539            config=config,
4540            components_mapping=components_mapping,
4541            parameters=model.parameters or {},
4542        )
4543
4544    _UNSUPPORTED_DECODER_ERROR = (
4545        "Specified decoder of {decoder_type} is not supported for pagination."
4546        "Please set as `JsonDecoder`, `XmlDecoder`, or a `CompositeRawDecoder` with an inner_parser of `JsonParser` or `GzipParser` instead."
4547        "If using `GzipParser`, please ensure that the lowest level inner_parser is a `JsonParser`."
4548    )
4549
4550    def _is_supported_decoder_for_pagination(self, decoder: Decoder) -> bool:
4551        if isinstance(decoder, (JsonDecoder, XmlDecoder)):
4552            return True
4553        elif isinstance(decoder, CompositeRawDecoder):
4554            return self._is_supported_parser_for_pagination(decoder.parser)
4555        else:
4556            return False
4557
4558    def _is_supported_parser_for_pagination(self, parser: Parser) -> bool:
4559        if isinstance(parser, JsonParser):
4560            return True
4561        elif isinstance(parser, GzipParser):
4562            return isinstance(parser.inner_parser, JsonParser)
4563        else:
4564            return False
4565
4566    def create_http_api_budget(
4567        self, model: HTTPAPIBudgetModel, config: Config, **kwargs: Any
4568    ) -> HttpAPIBudget:
4569        policies = [
4570            self._create_component_from_model(model=policy, config=config)
4571            for policy in model.policies
4572        ]
4573
4574        return HttpAPIBudget(
4575            policies=policies,
4576            ratelimit_reset_header=model.ratelimit_reset_header or "ratelimit-reset",
4577            ratelimit_remaining_header=model.ratelimit_remaining_header or "ratelimit-remaining",
4578            status_codes_for_ratelimit_hit=model.status_codes_for_ratelimit_hit or [429],
4579        )
4580
4581    def create_fixed_window_call_rate_policy(
4582        self, model: FixedWindowCallRatePolicyModel, config: Config, **kwargs: Any
4583    ) -> FixedWindowCallRatePolicy:
4584        matchers = [
4585            self._create_component_from_model(model=matcher, config=config)
4586            for matcher in model.matchers
4587        ]
4588
4589        # Set the initial reset timestamp to 10 days from now.
4590        # This value will be updated by the first request.
4591        return FixedWindowCallRatePolicy(
4592            next_reset_ts=datetime.datetime.now() + datetime.timedelta(days=10),
4593            period=parse_duration(model.period),
4594            call_limit=model.call_limit,
4595            matchers=matchers,
4596        )
4597
4598    def create_file_uploader(
4599        self, model: FileUploaderModel, config: Config, **kwargs: Any
4600    ) -> FileUploader:
4601        name = "File Uploader"
4602        requester = self._create_component_from_model(
4603            model=model.requester,
4604            config=config,
4605            name=name,
4606            **kwargs,
4607        )
4608        download_target_extractor = self._create_component_from_model(
4609            model=model.download_target_extractor,
4610            config=config,
4611            name=name,
4612            **kwargs,
4613        )
4614        emit_connector_builder_messages = self._emit_connector_builder_messages
4615        file_uploader = DefaultFileUploader(
4616            requester=requester,
4617            download_target_extractor=download_target_extractor,
4618            config=config,
4619            file_writer=NoopFileWriter()
4620            if emit_connector_builder_messages
4621            else LocalFileSystemFileWriter(),
4622            parameters=model.parameters or {},
4623            filename_extractor=model.filename_extractor if model.filename_extractor else None,
4624        )
4625
4626        return (
4627            ConnectorBuilderFileUploader(file_uploader)
4628            if emit_connector_builder_messages
4629            else file_uploader
4630        )
4631
4632    def create_moving_window_call_rate_policy(
4633        self, model: MovingWindowCallRatePolicyModel, config: Config, **kwargs: Any
4634    ) -> MovingWindowCallRatePolicy:
4635        rates = [
4636            self._create_component_from_model(model=rate, config=config) for rate in model.rates
4637        ]
4638        matchers = [
4639            self._create_component_from_model(model=matcher, config=config)
4640            for matcher in model.matchers
4641        ]
4642        return MovingWindowCallRatePolicy(
4643            rates=rates,
4644            matchers=matchers,
4645        )
4646
4647    def create_unlimited_call_rate_policy(
4648        self, model: UnlimitedCallRatePolicyModel, config: Config, **kwargs: Any
4649    ) -> UnlimitedCallRatePolicy:
4650        matchers = [
4651            self._create_component_from_model(model=matcher, config=config)
4652            for matcher in model.matchers
4653        ]
4654
4655        return UnlimitedCallRatePolicy(
4656            matchers=matchers,
4657        )
4658
4659    def create_rate(self, model: RateModel, config: Config, **kwargs: Any) -> Rate:
4660        interpolated_limit = InterpolatedString.create(str(model.limit), parameters={})
4661        return Rate(
4662            limit=int(interpolated_limit.eval(config=config)),
4663            interval=parse_duration(model.interval),
4664        )
4665
4666    def create_http_request_matcher(
4667        self, model: HttpRequestRegexMatcherModel, config: Config, **kwargs: Any
4668    ) -> HttpRequestRegexMatcher:
4669        weight = model.weight
4670        if weight is not None:
4671            if isinstance(weight, str):
4672                weight = int(InterpolatedString.create(weight, parameters={}).eval(config))
4673            else:
4674                weight = int(weight)
4675            if weight < 1:
4676                raise ValueError(f"weight must be >= 1, got {weight}")
4677        return HttpRequestRegexMatcher(
4678            method=model.method,
4679            url_base=model.url_base,
4680            url_path_pattern=model.url_path_pattern,
4681            params=model.params,
4682            headers=model.headers,
4683            weight=weight,
4684        )
4685
4686    def create_rate_limited_multiple_token_authenticator(
4687        self,
4688        model: RateLimitedMultipleTokenAuthenticatorModel,
4689        config: Config,
4690        **kwargs: Any,
4691    ) -> RateLimitedMultipleTokenAuthenticator:
4692        if isinstance(model.tokens, str):
4693            tokens_value = InterpolatedString.create(model.tokens, parameters={}).eval(config)
4694            delimiter = model.token_delimiter or ","
4695            tokens = [
4696                token.strip() for token in str(tokens_value).split(delimiter) if token.strip()
4697            ]
4698        else:
4699            tokens = [
4700                token_value
4701                for token in model.tokens
4702                if (
4703                    token_value := str(
4704                        InterpolatedString.create(token, parameters={}).eval(config)
4705                    ).strip()
4706                )
4707            ]
4708
4709        quota_specs = [
4710            {
4711                "name": quota_model.name,
4712                "remaining_path": quota_model.remaining_path,
4713                "reset_path": quota_model.reset_path,
4714                "limit_path": quota_model.limit_path,
4715                "remaining_header": quota_model.remaining_header,
4716                "reset_header": quota_model.reset_header,
4717                "limit_header": quota_model.limit_header,
4718                # Normalize the same way as the runtime TokenQuota below, so an omitted field
4719                # and an explicit `[]` key identically and keep sharing one set of counters.
4720                "exhaustion_status_codes": quota_model.exhaustion_status_codes or [],
4721                "matchers": [
4722                    {
4723                        "method": matcher_model.method,
4724                        "url_base": matcher_model.url_base,
4725                        "url_path_pattern": matcher_model.url_path_pattern,
4726                        "params": matcher_model.params,
4727                        "headers": matcher_model.headers,
4728                        "weight": matcher_model.weight,
4729                    }
4730                    for matcher_model in quota_model.matchers or []
4731                ],
4732            }
4733            for quota_model in model.quotas
4734        ]
4735
4736        quota_status_url = str(
4737            InterpolatedString.create(model.quota_status_source.url, parameters={}).eval(config)
4738        )
4739        quota_status_http_method = (
4740            model.quota_status_source.http_method.value
4741            if model.quota_status_source.http_method
4742            else "GET"
4743        )
4744        quota_status_headers = {
4745            key: str(InterpolatedString.create(value, parameters={}).eval(config))
4746            for key, value in (model.quota_status_source.request_headers or {}).items()
4747        }
4748        # Normalize the same way as the quota specs above, so an omitted field and an explicit
4749        # `[]` key identically and keep sharing one set of counters. Deduplicated as well as
4750        # sorted, because the runtime turns this into a set: without it `[404]` and `[404, 404]`
4751        # would key differently and stop sharing counters while behaving identically.
4752        quota_status_unavailable_status_codes = sorted(
4753            set(model.quota_status_source.unavailable_status_codes or [])
4754        )
4755        auth_method = model.auth_method or "Bearer"
4756        header = model.header or "Authorization"
4757        max_wait_time_str = str(
4758            InterpolatedString.create(model.max_wait_time or "PT2H", parameters={}).eval(config)
4759        )
4760        max_wait_time = parse_duration(max_wait_time_str)
4761        if not isinstance(max_wait_time, datetime.timedelta):
4762            raise ValueError(
4763                f"max_wait_time must be a fixed-length ISO 8601 duration (e.g. 'PT2H'); "
4764                f"calendar-unit durations like '{max_wait_time_str}' are not supported"
4765            )
4766        budget_reserve_fraction = (
4767            model.budget_reserve_fraction if model.budget_reserve_fraction is not None else 0.1
4768        )
4769        budget_min_reserve = (
4770            model.budget_min_reserve if model.budget_min_reserve is not None else 50
4771        )
4772
4773        # Reuse the same instance for identical definitions so that all streams share the
4774        # same token quota counters (similar to how api_budget is shared). The key is built
4775        # from the resolved constructor arguments rather than the raw model so that
4776        # stream-specific `$parameters` propagated onto the model (and its nested components)
4777        # cannot break instance sharing.
4778        cache_key = json.dumps(
4779            {
4780                "tokens": tokens,
4781                "quotas": quota_specs,
4782                "quota_status_url": quota_status_url,
4783                "quota_status_http_method": quota_status_http_method,
4784                "quota_status_headers": quota_status_headers,
4785                "quota_status_unavailable_status_codes": quota_status_unavailable_status_codes,
4786                "auth_method": auth_method,
4787                "header": header,
4788                "max_wait_time": max_wait_time.total_seconds(),
4789                "budget_reserve_fraction": budget_reserve_fraction,
4790                "budget_min_reserve": budget_min_reserve,
4791            },
4792            sort_keys=True,
4793        )
4794        if cache_key in self._rate_limited_authenticators:
4795            return self._rate_limited_authenticators[cache_key]
4796
4797        quotas = [
4798            TokenQuota(
4799                name=quota_model.name,
4800                remaining_path=quota_model.remaining_path,
4801                reset_path=quota_model.reset_path,
4802                limit_path=quota_model.limit_path,
4803                remaining_header=quota_model.remaining_header,
4804                reset_header=quota_model.reset_header,
4805                limit_header=quota_model.limit_header,
4806                exhaustion_status_codes=quota_model.exhaustion_status_codes or [],
4807                matchers=[
4808                    self.create_http_request_matcher(matcher_model, config)
4809                    for matcher_model in quota_model.matchers or []
4810                ],
4811            )
4812            for quota_model in model.quotas
4813        ]
4814
4815        authenticator = RateLimitedMultipleTokenAuthenticator(
4816            tokens=tokens,
4817            quotas=quotas,
4818            quota_status_url=quota_status_url,
4819            quota_status_http_method=quota_status_http_method,
4820            quota_status_headers=quota_status_headers,
4821            quota_status_unavailable_status_codes=quota_status_unavailable_status_codes,
4822            auth_method=auth_method,
4823            header=header,
4824            max_wait_time=max_wait_time,
4825            budget_reserve_fraction=budget_reserve_fraction,
4826            budget_min_reserve=budget_min_reserve,
4827        )
4828        self._rate_limited_authenticators[cache_key] = authenticator
4829        return authenticator
4830
4831    def set_api_budget(self, component_definition: ComponentDefinition, config: Config) -> None:
4832        self._api_budget = self.create_component(
4833            model_type=HTTPAPIBudgetModel, component_definition=component_definition, config=config
4834        )
4835
4836    def create_grouping_partition_router(
4837        self,
4838        model: GroupingPartitionRouterModel,
4839        config: Config,
4840        *,
4841        stream_name: str,
4842        **kwargs: Any,
4843    ) -> GroupingPartitionRouter:
4844        underlying_router = self._create_component_from_model(
4845            model=model.underlying_partition_router,
4846            config=config,
4847            stream_name=stream_name,
4848            **kwargs,
4849        )
4850        if model.group_size < 1:
4851            raise ValueError(f"Group size must be greater than 0, got {model.group_size}")
4852
4853        # Request options in underlying partition routers are not supported for GroupingPartitionRouter
4854        # because they are specific to individual partitions and cannot be aggregated or handled
4855        # when grouping, potentially leading to incorrect API calls. Any request customization
4856        # should be managed at the stream level through the requester's configuration.
4857        if isinstance(underlying_router, SubstreamPartitionRouter):
4858            if any(
4859                parent_config.request_option
4860                for parent_config in underlying_router.parent_stream_configs
4861            ):
4862                raise ValueError("Request options are not supported for GroupingPartitionRouter.")
4863
4864        if isinstance(underlying_router, ListPartitionRouter):
4865            if underlying_router.request_option:
4866                raise ValueError("Request options are not supported for GroupingPartitionRouter.")
4867
4868        return GroupingPartitionRouter(
4869            group_size=model.group_size,
4870            underlying_partition_router=underlying_router,
4871            deduplicate=model.deduplicate if model.deduplicate is not None else True,
4872            config=config,
4873        )
4874
4875    def create_union_partition_router(
4876        self,
4877        model: UnionPartitionRouterModel,
4878        config: Config,
4879        *,
4880        stream_name: str,
4881        **kwargs: Any,
4882    ) -> UnionPartitionRouter:
4883        # The schema enforces minItems: 2 for manifests; this guard covers construction paths
4884        # that bypass JSON-schema validation (the generated model carries no min_items constraint).
4885        if len(model.partition_routers) < 2:
4886            raise ValueError(
4887                f"UnionPartitionRouter for stream {stream_name} needs at least 2 child partition routers"
4888            )
4889
4890        partition_routers = [
4891            self._create_component_from_model(
4892                model=child,
4893                config=config,
4894                stream_name=stream_name,
4895                **kwargs,
4896            )
4897            for child in model.partition_routers
4898        ]
4899
4900        # partition_field depends only on config/parameters, so it is evaluated once at build
4901        # time; the runtime component always receives a plain string.
4902        partition_field = InterpolatedString.create(
4903            model.partition_field, parameters=model.parameters or {}
4904        ).eval(config)
4905
4906        # Fail fast at build time when a built-in child router is statically known to emit a
4907        # partition field different from the union's. CustomPartitionRouter children are opaque
4908        # and can only be validated at runtime.
4909        for child_model in model.partition_routers:
4910            child_partition_fields: List[str] = []
4911            if isinstance(child_model, ListPartitionRouterModel):
4912                child_partition_fields.append(
4913                    InterpolatedString.create(
4914                        child_model.cursor_field, parameters=child_model.parameters or {}
4915                    ).eval(config)
4916                )
4917            elif isinstance(child_model, SubstreamPartitionRouterModel):
4918                for parent_stream_config in child_model.parent_stream_configs:
4919                    child_partition_fields.append(
4920                        InterpolatedString.create(
4921                            parent_stream_config.partition_field,
4922                            parameters=parent_stream_config.parameters
4923                            or child_model.parameters
4924                            or {},
4925                        ).eval(config)
4926                    )
4927            elif isinstance(child_model, UnionPartitionRouterModel):
4928                child_partition_fields.append(
4929                    InterpolatedString.create(
4930                        child_model.partition_field, parameters=child_model.parameters or {}
4931                    ).eval(config)
4932                )
4933            for child_partition_field in child_partition_fields:
4934                if child_partition_field != partition_field:
4935                    raise ValueError(
4936                        f"UnionPartitionRouter expects all child partition routers to emit the "
4937                        f"partition field '{partition_field}', but a "
4938                        f"{child_model.type} child emits '{child_partition_field}'."
4939                    )
4940
4941        # A union slice comes from exactly one child partition router, so request options
4942        # declared on children cannot be applied consistently to requests built from the
4943        # normalized union slices. Partition values should be consumed via interpolation
4944        # (e.g. stream_partition) instead. Note that this validation only covers built-in
4945        # router types; CustomPartitionRouter children are opaque, so any request options
4946        # they implement internally cannot be detected or rejected here.
4947        for router in partition_routers:
4948            if isinstance(router, SubstreamPartitionRouter):
4949                if any(
4950                    parent_config.request_option for parent_config in router.parent_stream_configs
4951                ):
4952                    raise ValueError("Request options are not supported for UnionPartitionRouter.")
4953            if isinstance(router, ListPartitionRouter) and router.request_option:
4954                raise ValueError("Request options are not supported for UnionPartitionRouter.")
4955
4956        return UnionPartitionRouter(
4957            partition_routers=partition_routers,
4958            partition_field=partition_field,
4959            parameters=model.parameters or {},
4960        )
4961
4962    def _ensure_query_properties_to_model(
4963        self, requester: Union[HttpRequesterModel, CustomRequesterModel]
4964    ) -> None:
4965        """
4966        For some reason, it seems like CustomRequesterModel request_parameters stays as dictionaries which means that
4967        the other conditions relying on it being QueryPropertiesModel instead of a dict fail. Here, we migrate them to
4968        proper model.
4969        """
4970        if not hasattr(requester, "request_parameters"):
4971            return
4972
4973        request_parameters = requester.request_parameters
4974        if request_parameters and isinstance(request_parameters, Dict):
4975            for request_parameter_key in request_parameters.keys():
4976                request_parameter = request_parameters[request_parameter_key]
4977                if (
4978                    isinstance(request_parameter, Dict)
4979                    and request_parameter.get("type") == "QueryProperties"
4980                ):
4981                    request_parameters[request_parameter_key] = QueryPropertiesModel.parse_obj(
4982                        request_parameter
4983                    )
4984
4985    def _get_catalog_defined_cursor_field(
4986        self, stream_name: str, allow_catalog_defined_cursor_field: bool
4987    ) -> Optional[CursorField]:
4988        if not allow_catalog_defined_cursor_field:
4989            return None
4990
4991        configured_stream = self._stream_name_to_configured_stream.get(stream_name)
4992
4993        # Depending on the operation is being performed, there may not be a configured stream yet. In this
4994        # case we return None which will then use the default cursor field defined on the cursor model.
4995        # We also treat cursor_field: [""] (list with empty string) as no cursor field, since this can
4996        # occur when the platform serializes "no cursor configured" streams incorrectly.
4997        if (
4998            not configured_stream
4999            or not configured_stream.cursor_field
5000            or not configured_stream.cursor_field[0]
5001        ):
5002            return None
5003        elif len(configured_stream.cursor_field) > 1:
5004            raise ValueError(
5005                f"The `{stream_name}` stream does not support nested cursor_field. Please specify only a single cursor_field for the stream in the configured catalog."
5006            )
5007        else:
5008            return CursorField(
5009                cursor_field_key=configured_stream.cursor_field[0],
5010                supports_catalog_defined_cursor_field=allow_catalog_defined_cursor_field,
5011            )
ModelToComponentFactory( limit_pages_fetched_per_slice: Optional[int] = None, limit_slices_fetched: Optional[int] = None, emit_connector_builder_messages: bool = False, disable_retries: bool = False, disable_cache: bool = False, message_repository: Optional[airbyte_cdk.MessageRepository] = None, connector_state_manager: Optional[airbyte_cdk.ConnectorStateManager] = None, max_concurrent_async_job_count: Optional[int] = None, configured_catalog: Optional[airbyte_protocol_dataclasses.models.airbyte_protocol.ConfiguredAirbyteCatalog] = None, api_budget: Optional[airbyte_cdk.sources.streams.call_rate.APIBudget] = None, rate_limited_authenticators: Optional[Dict[str, airbyte_cdk.sources.declarative.auth.RateLimitedMultipleTokenAuthenticator]] = None, custom_components_trusted: bool = True)
710    def __init__(
711        self,
712        limit_pages_fetched_per_slice: Optional[int] = None,
713        limit_slices_fetched: Optional[int] = None,
714        emit_connector_builder_messages: bool = False,
715        disable_retries: bool = False,
716        disable_cache: bool = False,
717        message_repository: Optional[MessageRepository] = None,
718        connector_state_manager: Optional[ConnectorStateManager] = None,
719        max_concurrent_async_job_count: Optional[int] = None,
720        configured_catalog: Optional[ConfiguredAirbyteCatalog] = None,
721        api_budget: Optional[APIBudget] = None,
722        rate_limited_authenticators: Optional[
723            Dict[str, RateLimitedMultipleTokenAuthenticator]
724        ] = None,
725        custom_components_trusted: bool = True,
726    ):
727        self._init_mappings()
728        self._custom_components_trusted = custom_components_trusted
729        self._limit_pages_fetched_per_slice = limit_pages_fetched_per_slice
730        self._limit_slices_fetched = limit_slices_fetched
731        self._emit_connector_builder_messages = emit_connector_builder_messages
732        self._disable_retries = disable_retries
733        self._disable_cache = disable_cache
734        self._message_repository = message_repository or InMemoryMessageRepository(
735            self._evaluate_log_level(emit_connector_builder_messages)
736        )
737        self._stream_name_to_configured_stream = self._create_stream_name_to_configured_stream(
738            configured_catalog
739        )
740        self._connector_state_manager = connector_state_manager or ConnectorStateManager()
741        self._api_budget: Optional[Union[APIBudget]] = api_budget
742        # Shared instances so all streams see the same token quota counters (like api_budget)
743        self._rate_limited_authenticators: Dict[str, RateLimitedMultipleTokenAuthenticator] = (
744            rate_limited_authenticators if rate_limited_authenticators is not None else {}
745        )
746        self._job_tracker: JobTracker = JobTracker(max_concurrent_async_job_count or 1)
747        # placeholder for deprecation warnings
748        self._collected_deprecation_logs: List[ConnectorBuilderLogMessage] = []
EPOCH_DATETIME_FORMAT = '%s'
def create_component( self, model_type: Type[pydantic.v1.main.BaseModel], component_definition: Mapping[str, Any], config: Mapping[str, Any], **kwargs: Any) -> Any:
875    def create_component(
876        self,
877        model_type: Type[BaseModel],
878        component_definition: ComponentDefinition,
879        config: Config,
880        **kwargs: Any,
881    ) -> Any:
882        """
883        Takes a given Pydantic model type and Mapping representing a component definition and creates a declarative component and
884        subcomponents which will be used at runtime. This is done by first parsing the mapping into a Pydantic model and then creating
885        creating declarative components from that model.
886
887        :param model_type: The type of declarative component that is being initialized
888        :param component_definition: The mapping that represents a declarative component
889        :param config: The connector config that is provided by the customer
890        :return: The declarative component to be used at runtime
891        """
892
893        component_type = component_definition.get("type")
894        if component_definition.get("type") != model_type.__name__:
895            raise ValueError(
896                f"Expected manifest component of type {model_type.__name__}, but received {component_type} instead"
897            )
898
899        declarative_component_model = model_type.parse_obj(component_definition)
900
901        if not isinstance(declarative_component_model, model_type):
902            raise ValueError(
903                f"Expected {model_type.__name__} component, but received {declarative_component_model.__class__.__name__}"
904            )
905
906        return self._create_component_from_model(
907            model=declarative_component_model, config=config, **kwargs
908        )

Takes a given Pydantic model type and Mapping representing a component definition and creates a declarative component and subcomponents which will be used at runtime. This is done by first parsing the mapping into a Pydantic model and then creating creating declarative components from that model.

Parameters
  • model_type: The type of declarative component that is being initialized
  • component_definition: The mapping that represents a declarative component
  • config: The connector config that is provided by the customer
Returns

The declarative component to be used at runtime

def get_model_deprecations(self) -> List[airbyte_cdk.connector_builder.models.LogMessage]:
925    def get_model_deprecations(self) -> List[ConnectorBuilderLogMessage]:
926        """
927        Returns the deprecation warnings that were collected during the creation of components.
928        """
929        return self._collected_deprecation_logs

Returns the deprecation warnings that were collected during the creation of components.

946    def create_config_migration(
947        self, model: ConfigMigrationModel, config: Config
948    ) -> ConfigMigration:
949        transformations: List[ConfigTransformation] = [
950            self._create_component_from_model(transformation, config)
951            for transformation in model.transformations
952        ]
953
954        return ConfigMigration(
955            description=model.description,
956            transformations=transformations,
957        )
959    def create_config_add_fields(
960        self, model: ConfigAddFieldsModel, config: Config, **kwargs: Any
961    ) -> ConfigAddFields:
962        fields = [self._create_component_from_model(field, config) for field in model.fields]
963        return ConfigAddFields(
964            fields=fields,
965            condition=model.condition or "",
966        )
968    @staticmethod
969    def create_config_remove_fields(
970        model: ConfigRemoveFieldsModel, config: Config, **kwargs: Any
971    ) -> ConfigRemoveFields:
972        return ConfigRemoveFields(
973            field_pointers=model.field_pointers,
974            condition=model.condition or "",
975        )
@staticmethod
def create_config_remap_field( model: airbyte_cdk.sources.declarative.models.declarative_component_schema.ConfigRemapField, config: Mapping[str, Any], **kwargs: Any) -> airbyte_cdk.sources.declarative.transformations.config_transformations.ConfigRemapField:
977    @staticmethod
978    def create_config_remap_field(
979        model: ConfigRemapFieldModel, config: Config, **kwargs: Any
980    ) -> ConfigRemapField:
981        mapping = cast(Mapping[str, Any], model.map)
982        return ConfigRemapField(
983            map=mapping,
984            field_path=model.field_path,
985            config=config,
986        )
988    def create_dpath_validator(self, model: DpathValidatorModel, config: Config) -> DpathValidator:
989        strategy = self._create_component_from_model(model.validation_strategy, config)
990
991        return DpathValidator(
992            field_path=model.field_path,
993            strategy=strategy,
994        )
 996    def create_predicate_validator(
 997        self, model: PredicateValidatorModel, config: Config
 998    ) -> PredicateValidator:
 999        strategy = self._create_component_from_model(model.validation_strategy, config)
1000
1001        return PredicateValidator(
1002            value=model.value,
1003            strategy=strategy,
1004        )
@staticmethod
def create_validate_adheres_to_schema( model: airbyte_cdk.sources.declarative.models.declarative_component_schema.ValidateAdheresToSchema, config: Mapping[str, Any], **kwargs: Any) -> airbyte_cdk.sources.declarative.validators.ValidateAdheresToSchema:
1006    @staticmethod
1007    def create_validate_adheres_to_schema(
1008        model: ValidateAdheresToSchemaModel, config: Config, **kwargs: Any
1009    ) -> ValidateAdheresToSchema:
1010        base_schema = cast(Mapping[str, Any], model.base_schema)
1011        return ValidateAdheresToSchema(
1012            schema=base_schema,
1013        )
@staticmethod
def create_added_field_definition( model: airbyte_cdk.sources.declarative.models.declarative_component_schema.AddedFieldDefinition, config: Mapping[str, Any], **kwargs: Any) -> airbyte_cdk.AddedFieldDefinition:
1015    @staticmethod
1016    def create_added_field_definition(
1017        model: AddedFieldDefinitionModel, config: Config, **kwargs: Any
1018    ) -> AddedFieldDefinition:
1019        interpolated_value = InterpolatedString.create(
1020            model.value, parameters=model.parameters or {}
1021        )
1022        return AddedFieldDefinition(
1023            path=model.path,
1024            value=interpolated_value,
1025            value_type=ModelToComponentFactory._json_schema_type_name_to_type(model.value_type),
1026            parameters=model.parameters or {},
1027        )
def create_add_fields( self, model: airbyte_cdk.sources.declarative.models.declarative_component_schema.AddFields, config: Mapping[str, Any], **kwargs: Any) -> airbyte_cdk.AddFields:
1029    def create_add_fields(self, model: AddFieldsModel, config: Config, **kwargs: Any) -> AddFields:
1030        added_field_definitions = [
1031            self._create_component_from_model(
1032                model=added_field_definition_model,
1033                value_type=ModelToComponentFactory._json_schema_type_name_to_type(
1034                    added_field_definition_model.value_type
1035                ),
1036                config=config,
1037            )
1038            for added_field_definition_model in model.fields
1039        ]
1040        return AddFields(
1041            fields=added_field_definitions,
1042            condition=model.condition or "",
1043            parameters=model.parameters or {},
1044        )
1046    def create_keys_to_lower_transformation(
1047        self, model: KeysToLowerModel, config: Config, **kwargs: Any
1048    ) -> KeysToLowerTransformation:
1049        return KeysToLowerTransformation()
1051    def create_keys_to_snake_transformation(
1052        self, model: KeysToSnakeCaseModel, config: Config, **kwargs: Any
1053    ) -> KeysToSnakeCaseTransformation:
1054        return KeysToSnakeCaseTransformation()
1056    def create_keys_replace_transformation(
1057        self, model: KeysReplaceModel, config: Config, **kwargs: Any
1058    ) -> KeysReplaceTransformation:
1059        return KeysReplaceTransformation(
1060            old=model.old, new=model.new, parameters=model.parameters or {}
1061        )
1063    def create_flatten_fields(
1064        self, model: FlattenFieldsModel, config: Config, **kwargs: Any
1065    ) -> FlattenFields:
1066        return FlattenFields(
1067            flatten_lists=model.flatten_lists if model.flatten_lists is not None else True
1068        )
1070    def create_dpath_flatten_fields(
1071        self, model: DpathFlattenFieldsModel, config: Config, **kwargs: Any
1072    ) -> DpathFlattenFields:
1073        model_field_path: List[Union[InterpolatedString, str]] = [x for x in model.field_path]
1074        key_transformation = (
1075            KeyTransformation(
1076                config=config,
1077                prefix=model.key_transformation.prefix,
1078                suffix=model.key_transformation.suffix,
1079                parameters=model.parameters or {},
1080            )
1081            if model.key_transformation is not None
1082            else None
1083        )
1084        return DpathFlattenFields(
1085            config=config,
1086            field_path=model_field_path,
1087            delete_origin_value=model.delete_origin_value
1088            if model.delete_origin_value is not None
1089            else False,
1090            replace_record=model.replace_record if model.replace_record is not None else False,
1091            key_transformation=key_transformation,
1092            parameters=model.parameters or {},
1093        )
def create_api_key_authenticator( self, model: airbyte_cdk.sources.declarative.models.declarative_component_schema.ApiKeyAuthenticator, config: Mapping[str, Any], token_provider: Optional[airbyte_cdk.sources.declarative.auth.token_provider.TokenProvider] = None, **kwargs: Any) -> airbyte_cdk.ApiKeyAuthenticator:
1107    def create_api_key_authenticator(
1108        self,
1109        model: ApiKeyAuthenticatorModel,
1110        config: Config,
1111        token_provider: Optional[TokenProvider] = None,
1112        **kwargs: Any,
1113    ) -> ApiKeyAuthenticator:
1114        if model.inject_into is None and model.header is None:
1115            raise ValueError(
1116                "Expected either inject_into or header to be set for ApiKeyAuthenticator"
1117            )
1118
1119        if model.inject_into is not None and model.header is not None:
1120            raise ValueError(
1121                "inject_into and header cannot be set both for ApiKeyAuthenticator - remove the deprecated header option"
1122            )
1123
1124        if token_provider is not None and model.api_token != "":
1125            raise ValueError(
1126                "If token_provider is set, api_token is ignored and has to be set to empty string."
1127            )
1128
1129        request_option = (
1130            self._create_component_from_model(
1131                model.inject_into, config, parameters=model.parameters or {}
1132            )
1133            if model.inject_into
1134            else RequestOption(
1135                inject_into=RequestOptionType.header,
1136                field_name=model.header or "",
1137                parameters=model.parameters or {},
1138            )
1139        )
1140
1141        return ApiKeyAuthenticator(
1142            token_provider=(
1143                token_provider
1144                if token_provider is not None
1145                else InterpolatedStringTokenProvider(
1146                    api_token=model.api_token or "",
1147                    config=config,
1148                    parameters=model.parameters or {},
1149                )
1150            ),
1151            request_option=request_option,
1152            config=config,
1153            parameters=model.parameters or {},
1154        )
1156    def create_legacy_to_per_partition_state_migration(
1157        self,
1158        model: LegacyToPerPartitionStateMigrationModel,
1159        config: Mapping[str, Any],
1160        declarative_stream: DeclarativeStreamModel,
1161    ) -> LegacyToPerPartitionStateMigration:
1162        retriever = declarative_stream.retriever
1163        if not isinstance(retriever, (SimpleRetrieverModel, AsyncRetrieverModel)):
1164            raise ValueError(
1165                f"LegacyToPerPartitionStateMigrations can only be applied on a DeclarativeStream with a SimpleRetriever or AsyncRetriever. Got {type(retriever)}"
1166            )
1167        partition_router = retriever.partition_router
1168        if not isinstance(
1169            partition_router,
1170            (
1171                SubstreamPartitionRouterModel,
1172                CustomPartitionRouterModel,
1173                UnionPartitionRouterModel,
1174            ),
1175        ):
1176            raise ValueError(
1177                f"LegacyToPerPartitionStateMigrations can only be applied on a SimpleRetriever with a SubstreamPartitionRouter, UnionPartitionRouter or CustomPartitionRouter. Got {type(partition_router)}"
1178            )
1179        if not isinstance(partition_router, UnionPartitionRouterModel) and not hasattr(
1180            partition_router, "parent_stream_configs"
1181        ):
1182            raise ValueError(
1183                "LegacyToPerPartitionStateMigrations can only be applied with a parent stream configuration."
1184            )
1185
1186        if not hasattr(declarative_stream, "incremental_sync"):
1187            raise ValueError(
1188                "LegacyToPerPartitionStateMigrations can only be applied with an incremental_sync configuration."
1189            )
1190
1191        return LegacyToPerPartitionStateMigration(
1192            partition_router,  # type: ignore # was already checked above
1193            declarative_stream.incremental_sync,  # type: ignore # was already checked. Migration can be applied only to incremental streams.
1194            config,
1195            declarative_stream.parameters,  # type: ignore # different type is expected here Mapping[str, Any], got Dict[str, Any]
1196        )
def create_session_token_authenticator( self, model: airbyte_cdk.sources.declarative.models.declarative_component_schema.SessionTokenAuthenticator, config: Mapping[str, Any], name: str, **kwargs: Any) -> Union[airbyte_cdk.ApiKeyAuthenticator, airbyte_cdk.BearerAuthenticator]:
1198    def create_session_token_authenticator(
1199        self, model: SessionTokenAuthenticatorModel, config: Config, name: str, **kwargs: Any
1200    ) -> Union[ApiKeyAuthenticator, BearerAuthenticator]:
1201        decoder = (
1202            self._create_component_from_model(model=model.decoder, config=config)
1203            if model.decoder
1204            else JsonDecoder(parameters={})
1205        )
1206        login_requester = self._create_component_from_model(
1207            model=model.login_requester,
1208            config=config,
1209            name=f"{name}_login_requester",
1210            decoder=decoder,
1211        )
1212        token_provider = SessionTokenProvider(
1213            login_requester=login_requester,
1214            session_token_path=model.session_token_path,
1215            expiration_duration=parse_duration(model.expiration_duration)
1216            if model.expiration_duration
1217            else None,
1218            parameters=model.parameters or {},
1219            message_repository=self._message_repository,
1220            decoder=decoder,
1221        )
1222        if model.request_authentication.type == "Bearer":
1223            return ModelToComponentFactory.create_bearer_authenticator(
1224                BearerAuthenticatorModel(type="BearerAuthenticator", api_token=""),  # type: ignore # $parameters has a default value
1225                config,
1226                token_provider=token_provider,
1227            )
1228        else:
1229            # Get the api_token template if specified, default to just the session token
1230            api_token_template = (
1231                getattr(model.request_authentication, "api_token", None) or "{{ session_token }}"
1232            )
1233            final_token_provider: TokenProvider = InterpolatedSessionTokenProvider(
1234                config=config,
1235                api_token=api_token_template,
1236                session_token_provider=token_provider,
1237                parameters=model.parameters or {},
1238            )
1239            return self.create_api_key_authenticator(
1240                ApiKeyAuthenticatorModel(
1241                    type="ApiKeyAuthenticator",
1242                    api_token="",
1243                    inject_into=model.request_authentication.inject_into,
1244                ),  # type: ignore # $parameters and headers default to None
1245                config=config,
1246                token_provider=final_token_provider,
1247            )
@staticmethod
def create_basic_http_authenticator( model: airbyte_cdk.sources.declarative.models.declarative_component_schema.BasicHttpAuthenticator, config: Mapping[str, Any], **kwargs: Any) -> airbyte_cdk.BasicHttpAuthenticator:
1249    @staticmethod
1250    def create_basic_http_authenticator(
1251        model: BasicHttpAuthenticatorModel, config: Config, **kwargs: Any
1252    ) -> BasicHttpAuthenticator:
1253        return BasicHttpAuthenticator(
1254            password=model.password or "",
1255            username=model.username,
1256            config=config,
1257            parameters=model.parameters or {},
1258        )
@staticmethod
def create_bearer_authenticator( model: airbyte_cdk.sources.declarative.models.declarative_component_schema.BearerAuthenticator, config: Mapping[str, Any], token_provider: Optional[airbyte_cdk.sources.declarative.auth.token_provider.TokenProvider] = None, **kwargs: Any) -> airbyte_cdk.BearerAuthenticator:
1260    @staticmethod
1261    def create_bearer_authenticator(
1262        model: BearerAuthenticatorModel,
1263        config: Config,
1264        token_provider: Optional[TokenProvider] = None,
1265        **kwargs: Any,
1266    ) -> BearerAuthenticator:
1267        if token_provider is not None and model.api_token != "":
1268            raise ValueError(
1269                "If token_provider is set, api_token is ignored and has to be set to empty string."
1270            )
1271        return BearerAuthenticator(
1272            token_provider=(
1273                token_provider
1274                if token_provider is not None
1275                else InterpolatedStringTokenProvider(
1276                    api_token=model.api_token or "",
1277                    config=config,
1278                    parameters=model.parameters or {},
1279                )
1280            ),
1281            config=config,
1282            parameters=model.parameters or {},
1283        )
@staticmethod
def create_dynamic_stream_check_config( model: airbyte_cdk.sources.declarative.models.declarative_component_schema.DynamicStreamCheckConfig, config: Mapping[str, Any], **kwargs: Any) -> airbyte_cdk.sources.declarative.checks.DynamicStreamCheckConfig:
1285    @staticmethod
1286    def create_dynamic_stream_check_config(
1287        model: DynamicStreamCheckConfigModel, config: Config, **kwargs: Any
1288    ) -> DynamicStreamCheckConfig:
1289        return DynamicStreamCheckConfig(
1290            dynamic_stream_name=model.dynamic_stream_name,
1291            stream_count=model.stream_count,
1292        )
def create_check_stream( self, model: airbyte_cdk.sources.declarative.models.declarative_component_schema.CheckStream, config: Mapping[str, Any], **kwargs: Any) -> airbyte_cdk.sources.declarative.checks.CheckStream:
1294    def create_check_stream(
1295        self, model: CheckStreamModel, config: Config, **kwargs: Any
1296    ) -> CheckStream:
1297        if model.dynamic_streams_check_configs is None and model.stream_names is None:
1298            raise ValueError(
1299                "Expected either stream_names or dynamic_streams_check_configs to be set for CheckStream"
1300            )
1301
1302        dynamic_streams_check_configs = (
1303            [
1304                self._create_component_from_model(model=dynamic_stream_check_config, config=config)
1305                for dynamic_stream_check_config in model.dynamic_streams_check_configs
1306            ]
1307            if model.dynamic_streams_check_configs
1308            else []
1309        )
1310
1311        # `model.config_overrides` is deliberately not read here. The source applies it around the whole
1312        # check operation (`ConcurrentDeclarativeSource._config_overridden_for_check`), which is what makes
1313        # it work for every checker type rather than only this one. Do not wire it in a second time.
1314        return CheckStream(
1315            stream_names=model.stream_names or [],
1316            dynamic_streams_check_configs=dynamic_streams_check_configs,
1317            parameters={},
1318        )
@staticmethod
def create_check_dynamic_stream( model: airbyte_cdk.sources.declarative.models.declarative_component_schema.CheckDynamicStream, config: Mapping[str, Any], **kwargs: Any) -> airbyte_cdk.sources.declarative.checks.CheckDynamicStream:
1320    @staticmethod
1321    def create_check_dynamic_stream(
1322        model: CheckDynamicStreamModel, config: Config, **kwargs: Any
1323    ) -> CheckDynamicStream:
1324        assert model.use_check_availability is not None  # for mypy
1325
1326        use_check_availability = model.use_check_availability
1327
1328        # See `create_check_stream`: `model.config_overrides` is applied by the source, not here.
1329        return CheckDynamicStream(
1330            stream_count=model.stream_count,
1331            use_check_availability=use_check_availability,
1332            parameters={},
1333        )
1335    def create_composite_error_handler(
1336        self, model: CompositeErrorHandlerModel, config: Config, **kwargs: Any
1337    ) -> CompositeErrorHandler:
1338        error_handlers = [
1339            self._create_component_from_model(model=error_handler_model, config=config)
1340            for error_handler_model in model.error_handlers
1341        ]
1342        return CompositeErrorHandler(
1343            error_handlers=error_handlers, parameters=model.parameters or {}
1344        )
@staticmethod
def create_concurrency_level( model: airbyte_cdk.sources.declarative.models.declarative_component_schema.ConcurrencyLevel, config: Mapping[str, Any], **kwargs: Any) -> airbyte_cdk.sources.declarative.concurrency_level.ConcurrencyLevel:
1346    @staticmethod
1347    def create_concurrency_level(
1348        model: ConcurrencyLevelModel, config: Config, **kwargs: Any
1349    ) -> ConcurrencyLevel:
1350        return ConcurrencyLevel(
1351            default_concurrency=model.default_concurrency,
1352            max_concurrency=model.max_concurrency,
1353            config=config,
1354            parameters={},
1355        )
@staticmethod
def apply_stream_state_migrations( stream_state_migrations: Optional[List[Any]], stream_state: MutableMapping[str, Any]) -> MutableMapping[str, Any]:
1357    @staticmethod
1358    def apply_stream_state_migrations(
1359        stream_state_migrations: List[Any] | None, stream_state: MutableMapping[str, Any]
1360    ) -> MutableMapping[str, Any]:
1361        if stream_state_migrations:
1362            for state_migration in stream_state_migrations:
1363                if state_migration.should_migrate(stream_state):
1364                    # The state variable is expected to be mutable but the migrate method returns an immutable mapping.
1365                    stream_state = dict(state_migration.migrate(stream_state))
1366        return stream_state
def create_concurrent_cursor_from_datetime_based_cursor( self, model_type: Type[pydantic.v1.main.BaseModel], component_definition: Mapping[str, Any], stream_name: str, stream_namespace: Optional[str], stream_state: MutableMapping[str, Any], config: Mapping[str, Any], message_repository: Optional[airbyte_cdk.MessageRepository] = None, runtime_lookback_window: Optional[datetime.timedelta] = None, **kwargs: Any) -> airbyte_cdk.ConcurrentCursor:
1368    def create_concurrent_cursor_from_datetime_based_cursor(
1369        self,
1370        model_type: Type[BaseModel],
1371        component_definition: ComponentDefinition,
1372        stream_name: str,
1373        stream_namespace: Optional[str],
1374        stream_state: MutableMapping[str, Any],
1375        config: Config,
1376        message_repository: Optional[MessageRepository] = None,
1377        runtime_lookback_window: Optional[datetime.timedelta] = None,
1378        **kwargs: Any,
1379    ) -> ConcurrentCursor:
1380        component_type = component_definition.get("type")
1381        if component_definition.get("type") != model_type.__name__:
1382            raise ValueError(
1383                f"Expected manifest component of type {model_type.__name__}, but received {component_type} instead"
1384            )
1385
1386        # FIXME the interfaces of the concurrent cursor are kind of annoying as they take a `ComponentDefinition` instead of the actual model. This was done because the ConcurrentDeclarativeSource didn't have access to the models [here for example](https://github.com/airbytehq/airbyte-python-cdk/blob/f525803b3fec9329e4cc8478996a92bf884bfde9/airbyte_cdk/sources/declarative/concurrent_declarative_source.py#L354C54-L354C91). So now we have two cases:
1387        # * The ComponentDefinition comes from model.__dict__ in which case we have `parameters`
1388        # * The ComponentDefinition comes from the manifest as a dict in which case we have `$parameters`
1389        # We should change those interfaces to use the model once we clean up the code in CDS at which point the parameter propagation should happen as part of the ModelToComponentFactory.
1390        if "$parameters" not in component_definition and "parameters" in component_definition:
1391            component_definition["$parameters"] = component_definition.get("parameters")  # type: ignore  # This is a dict
1392        datetime_based_cursor_model = model_type.parse_obj(component_definition)
1393
1394        if not isinstance(datetime_based_cursor_model, DatetimeBasedCursorModel):
1395            raise ValueError(
1396                f"Expected {model_type.__name__} component, but received {datetime_based_cursor_model.__class__.__name__}"
1397            )
1398
1399        model_parameters = datetime_based_cursor_model.parameters or {}
1400
1401        cursor_field = self._get_catalog_defined_cursor_field(
1402            stream_name=stream_name,
1403            allow_catalog_defined_cursor_field=datetime_based_cursor_model.allow_catalog_defined_cursor_field
1404            or False,
1405        )
1406
1407        if not cursor_field:
1408            interpolated_cursor_field = InterpolatedString.create(
1409                datetime_based_cursor_model.cursor_field,
1410                parameters=model_parameters,
1411            )
1412            cursor_field = CursorField(
1413                cursor_field_key=interpolated_cursor_field.eval(config=config),
1414                supports_catalog_defined_cursor_field=datetime_based_cursor_model.allow_catalog_defined_cursor_field
1415                or False,
1416            )
1417
1418        interpolated_partition_field_start = InterpolatedString.create(
1419            datetime_based_cursor_model.partition_field_start or "start_time",
1420            parameters=model_parameters,
1421        )
1422        interpolated_partition_field_end = InterpolatedString.create(
1423            datetime_based_cursor_model.partition_field_end or "end_time",
1424            parameters=model_parameters,
1425        )
1426
1427        slice_boundary_fields = (
1428            interpolated_partition_field_start.eval(config=config),
1429            interpolated_partition_field_end.eval(config=config),
1430        )
1431
1432        datetime_format = datetime_based_cursor_model.datetime_format
1433
1434        cursor_granularity = (
1435            parse_duration(datetime_based_cursor_model.cursor_granularity)
1436            if datetime_based_cursor_model.cursor_granularity
1437            else None
1438        )
1439
1440        lookback_window = None
1441        interpolated_lookback_window = (
1442            InterpolatedString.create(
1443                datetime_based_cursor_model.lookback_window,
1444                parameters=model_parameters,
1445            )
1446            if datetime_based_cursor_model.lookback_window
1447            else None
1448        )
1449        if interpolated_lookback_window:
1450            evaluated_lookback_window = interpolated_lookback_window.eval(config=config)
1451            if evaluated_lookback_window:
1452                lookback_window = parse_duration(evaluated_lookback_window)
1453
1454        connector_state_converter: DateTimeStreamStateConverter
1455        connector_state_converter = CustomFormatConcurrentStreamStateConverter(
1456            datetime_format=datetime_format,
1457            input_datetime_formats=datetime_based_cursor_model.cursor_datetime_formats,
1458            is_sequential_state=True,  # ConcurrentPerPartitionCursor only works with sequential state
1459            cursor_granularity=cursor_granularity,
1460        )
1461
1462        # Adjusts the stream state by applying the runtime lookback window.
1463        # This is used to ensure correct state handling in case of failed partitions.
1464        stream_state_value = stream_state.get(cursor_field.cursor_field_key)
1465        if runtime_lookback_window and stream_state_value:
1466            new_stream_state = (
1467                connector_state_converter.parse_timestamp(stream_state_value)
1468                - runtime_lookback_window
1469            )
1470            stream_state[cursor_field.cursor_field_key] = connector_state_converter.output_format(
1471                new_stream_state
1472            )
1473
1474        start_date_runtime_value: Union[InterpolatedString, str, MinMaxDatetime]
1475        if isinstance(datetime_based_cursor_model.start_datetime, MinMaxDatetimeModel):
1476            start_date_runtime_value = self.create_min_max_datetime(
1477                model=datetime_based_cursor_model.start_datetime, config=config
1478            )
1479        else:
1480            start_date_runtime_value = datetime_based_cursor_model.start_datetime
1481
1482        end_date_runtime_value: Optional[Union[InterpolatedString, str, MinMaxDatetime]]
1483        if isinstance(datetime_based_cursor_model.end_datetime, MinMaxDatetimeModel):
1484            end_date_runtime_value = self.create_min_max_datetime(
1485                model=datetime_based_cursor_model.end_datetime, config=config
1486            )
1487        else:
1488            end_date_runtime_value = datetime_based_cursor_model.end_datetime
1489
1490        interpolated_start_date = MinMaxDatetime.create(
1491            interpolated_string_or_min_max_datetime=start_date_runtime_value,
1492            parameters=datetime_based_cursor_model.parameters,
1493        )
1494        interpolated_end_date = (
1495            None
1496            if not end_date_runtime_value
1497            else MinMaxDatetime.create(
1498                end_date_runtime_value, datetime_based_cursor_model.parameters
1499            )
1500        )
1501
1502        # If datetime format is not specified then start/end datetime should inherit it from the stream slicer
1503        if not interpolated_start_date.datetime_format:
1504            interpolated_start_date.datetime_format = datetime_format
1505        if interpolated_end_date and not interpolated_end_date.datetime_format:
1506            interpolated_end_date.datetime_format = datetime_format
1507
1508        start_date = interpolated_start_date.get_datetime(config=config)
1509        end_date_provider = (
1510            partial(interpolated_end_date.get_datetime, config)
1511            if interpolated_end_date
1512            else connector_state_converter.get_end_provider()
1513        )
1514
1515        if (
1516            datetime_based_cursor_model.step and not datetime_based_cursor_model.cursor_granularity
1517        ) or (
1518            not datetime_based_cursor_model.step and datetime_based_cursor_model.cursor_granularity
1519        ):
1520            raise ValueError(
1521                f"If step is defined, cursor_granularity should be as well and vice-versa. "
1522                f"Right now, step is `{datetime_based_cursor_model.step}` and cursor_granularity is `{datetime_based_cursor_model.cursor_granularity}`"
1523            )
1524
1525        # When step is not defined, default to a step size from the starting date to the present moment
1526        step_length = datetime.timedelta.max
1527        interpolated_step = (
1528            InterpolatedString.create(
1529                datetime_based_cursor_model.step,
1530                parameters=model_parameters,
1531            )
1532            if datetime_based_cursor_model.step
1533            else None
1534        )
1535        if interpolated_step:
1536            evaluated_step = interpolated_step.eval(config)
1537            if evaluated_step:
1538                step_length = parse_duration(evaluated_step)
1539
1540        clamping_strategy: ClampingStrategy = NoClamping()
1541        if datetime_based_cursor_model.clamping:
1542            # While it is undesirable to interpolate within the model factory (as opposed to at runtime),
1543            # it is still better than shifting interpolation low-code concept into the ConcurrentCursor runtime
1544            # object which we want to keep agnostic of being low-code
1545            target = InterpolatedString(
1546                string=datetime_based_cursor_model.clamping.target,
1547                parameters=model_parameters,
1548            )
1549            evaluated_target = target.eval(config=config)
1550            match evaluated_target:
1551                case "DAY":
1552                    clamping_strategy = DayClampingStrategy()
1553                    end_date_provider = ClampingEndProvider(
1554                        DayClampingStrategy(is_ceiling=False),
1555                        end_date_provider,  # type: ignore  # Having issues w/ inspection for GapType and CursorValueType as shown in existing tests. Confirmed functionality is working in practice
1556                        granularity=cursor_granularity or datetime.timedelta(seconds=1),
1557                    )
1558                case "WEEK":
1559                    if (
1560                        not datetime_based_cursor_model.clamping.target_details
1561                        or "weekday" not in datetime_based_cursor_model.clamping.target_details
1562                    ):
1563                        raise ValueError(
1564                            "Given WEEK clamping, weekday needs to be provided as target_details"
1565                        )
1566                    weekday = self._assemble_weekday(
1567                        datetime_based_cursor_model.clamping.target_details["weekday"]
1568                    )
1569                    clamping_strategy = WeekClampingStrategy(weekday)
1570                    end_date_provider = ClampingEndProvider(
1571                        WeekClampingStrategy(weekday, is_ceiling=False),
1572                        end_date_provider,  # type: ignore  # Having issues w/ inspection for GapType and CursorValueType as shown in existing tests. Confirmed functionality is working in practice
1573                        granularity=cursor_granularity or datetime.timedelta(days=1),
1574                    )
1575                case "MONTH":
1576                    clamping_strategy = MonthClampingStrategy()
1577                    end_date_provider = ClampingEndProvider(
1578                        MonthClampingStrategy(is_ceiling=False),
1579                        end_date_provider,  # type: ignore  # Having issues w/ inspection for GapType and CursorValueType as shown in existing tests. Confirmed functionality is working in practice
1580                        granularity=cursor_granularity or datetime.timedelta(days=1),
1581                    )
1582                case _:
1583                    raise ValueError(
1584                        f"Invalid clamping target {evaluated_target}, expected DAY, WEEK, MONTH"
1585                    )
1586
1587        return ConcurrentCursor(
1588            stream_name=stream_name,
1589            stream_namespace=stream_namespace,
1590            stream_state=stream_state,
1591            message_repository=message_repository or self._message_repository,
1592            connector_state_manager=self._connector_state_manager,
1593            connector_state_converter=connector_state_converter,
1594            cursor_field=cursor_field,
1595            slice_boundary_fields=slice_boundary_fields,
1596            start=start_date,  # type: ignore  # Having issues w/ inspection for GapType and CursorValueType as shown in existing tests. Confirmed functionality is working in practice
1597            end_provider=end_date_provider,  # type: ignore  # Having issues w/ inspection for GapType and CursorValueType as shown in existing tests. Confirmed functionality is working in practice
1598            lookback_window=lookback_window,
1599            slice_range=step_length,
1600            cursor_granularity=cursor_granularity,
1601            clamping_strategy=clamping_strategy,
1602        )
def create_concurrent_cursor_from_incrementing_count_cursor( self, model_type: Type[pydantic.v1.main.BaseModel], component_definition: Mapping[str, Any], stream_name: str, stream_namespace: Optional[str], stream_state: MutableMapping[str, Any], config: Mapping[str, Any], message_repository: Optional[airbyte_cdk.MessageRepository] = None, **kwargs: Any) -> airbyte_cdk.ConcurrentCursor:
1604    def create_concurrent_cursor_from_incrementing_count_cursor(
1605        self,
1606        model_type: Type[BaseModel],
1607        component_definition: ComponentDefinition,
1608        stream_name: str,
1609        stream_namespace: Optional[str],
1610        stream_state: MutableMapping[str, Any],
1611        config: Config,
1612        message_repository: Optional[MessageRepository] = None,
1613        **kwargs: Any,
1614    ) -> ConcurrentCursor:
1615        component_type = component_definition.get("type")
1616        if component_definition.get("type") != model_type.__name__:
1617            raise ValueError(
1618                f"Expected manifest component of type {model_type.__name__}, but received {component_type} instead"
1619            )
1620
1621        incrementing_count_cursor_model = model_type.parse_obj(component_definition)
1622
1623        if not isinstance(incrementing_count_cursor_model, IncrementingCountCursorModel):
1624            raise ValueError(
1625                f"Expected {model_type.__name__} component, but received {incrementing_count_cursor_model.__class__.__name__}"
1626            )
1627
1628        start_value: Union[int, str, None] = incrementing_count_cursor_model.start_value
1629        # Pydantic Union type coercion can convert int 0 to string '0' depending on Union order.
1630        # We need to handle both int and str representations of numeric values.
1631        # Evaluate the InterpolatedString and convert to int for the ConcurrentCursor.
1632        if start_value is not None:
1633            interpolated_start_value = InterpolatedString.create(
1634                str(start_value),  # Ensure we pass a string to InterpolatedString.create
1635                parameters=incrementing_count_cursor_model.parameters or {},
1636            )
1637            evaluated_start_value: int = int(interpolated_start_value.eval(config=config))
1638        else:
1639            evaluated_start_value = 0
1640
1641        cursor_field = self._get_catalog_defined_cursor_field(
1642            stream_name=stream_name,
1643            allow_catalog_defined_cursor_field=incrementing_count_cursor_model.allow_catalog_defined_cursor_field
1644            or False,
1645        )
1646
1647        if not cursor_field:
1648            interpolated_cursor_field = InterpolatedString.create(
1649                incrementing_count_cursor_model.cursor_field,
1650                parameters=incrementing_count_cursor_model.parameters or {},
1651            )
1652            cursor_field = CursorField(
1653                cursor_field_key=interpolated_cursor_field.eval(config=config),
1654                supports_catalog_defined_cursor_field=incrementing_count_cursor_model.allow_catalog_defined_cursor_field
1655                or False,
1656            )
1657
1658        connector_state_converter = IncrementingCountStreamStateConverter(
1659            is_sequential_state=True,  # ConcurrentPerPartitionCursor only works with sequential state
1660        )
1661
1662        return ConcurrentCursor(
1663            stream_name=stream_name,
1664            stream_namespace=stream_namespace,
1665            stream_state=stream_state,
1666            message_repository=message_repository or self._message_repository,
1667            connector_state_manager=self._connector_state_manager,
1668            connector_state_converter=connector_state_converter,
1669            cursor_field=cursor_field,
1670            slice_boundary_fields=None,
1671            start=evaluated_start_value,  # type: ignore  # Having issues w/ inspection for GapType and CursorValueType as shown in existing tests. Confirmed functionality is working in practice
1672            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
1673        )
def create_concurrent_cursor_from_perpartition_cursor( self, state_manager: airbyte_cdk.ConnectorStateManager, model_type: Type[pydantic.v1.main.BaseModel], component_definition: Mapping[str, Any], stream_name: str, stream_namespace: Optional[str], config: Mapping[str, Any], stream_state: MutableMapping[str, Any], partition_router: airbyte_cdk.sources.declarative.partition_routers.PartitionRouter, attempt_to_create_cursor_if_not_provided: bool = False, **kwargs: Any) -> airbyte_cdk.sources.declarative.incremental.ConcurrentPerPartitionCursor:
1694    def create_concurrent_cursor_from_perpartition_cursor(
1695        self,
1696        state_manager: ConnectorStateManager,
1697        model_type: Type[BaseModel],
1698        component_definition: ComponentDefinition,
1699        stream_name: str,
1700        stream_namespace: Optional[str],
1701        config: Config,
1702        stream_state: MutableMapping[str, Any],
1703        partition_router: PartitionRouter,
1704        attempt_to_create_cursor_if_not_provided: bool = False,
1705        **kwargs: Any,
1706    ) -> ConcurrentPerPartitionCursor:
1707        component_type = component_definition.get("type")
1708        if component_definition.get("type") != model_type.__name__:
1709            raise ValueError(
1710                f"Expected manifest component of type {model_type.__name__}, but received {component_type} instead"
1711            )
1712
1713        # FIXME the interfaces of the concurrent cursor are kind of annoying as they take a `ComponentDefinition` instead of the actual model. This was done because the ConcurrentDeclarativeSource didn't have access to the models [here for example](https://github.com/airbytehq/airbyte-python-cdk/blob/f525803b3fec9329e4cc8478996a92bf884bfde9/airbyte_cdk/sources/declarative/concurrent_declarative_source.py#L354C54-L354C91). So now we have two cases:
1714        # * The ComponentDefinition comes from model.__dict__ in which case we have `parameters`
1715        # * The ComponentDefinition comes from the manifest as a dict in which case we have `$parameters`
1716        # We should change those interfaces to use the model once we clean up the code in CDS at which point the parameter propagation should happen as part of the ModelToComponentFactory.
1717        if "$parameters" not in component_definition and "parameters" in component_definition:
1718            component_definition["$parameters"] = component_definition.get("parameters")  # type: ignore  # This is a dict
1719        datetime_based_cursor_model = model_type.parse_obj(component_definition)
1720
1721        if not isinstance(datetime_based_cursor_model, DatetimeBasedCursorModel):
1722            raise ValueError(
1723                f"Expected {model_type.__name__} component, but received {datetime_based_cursor_model.__class__.__name__}"
1724            )
1725
1726        cursor_field = self._get_catalog_defined_cursor_field(
1727            stream_name=stream_name,
1728            allow_catalog_defined_cursor_field=datetime_based_cursor_model.allow_catalog_defined_cursor_field
1729            or False,
1730        )
1731
1732        if not cursor_field:
1733            interpolated_cursor_field = InterpolatedString.create(
1734                datetime_based_cursor_model.cursor_field,
1735                # FIXME the interfaces of the concurrent cursor are kind of annoying as they take a `ComponentDefinition` instead of the actual model. This was done because the ConcurrentDeclarativeSource didn't have access to the models [here for example](https://github.com/airbytehq/airbyte-python-cdk/blob/f525803b3fec9329e4cc8478996a92bf884bfde9/airbyte_cdk/sources/declarative/concurrent_declarative_source.py#L354C54-L354C91). So now we have two cases:
1736                # * The ComponentDefinition comes from model.__dict__ in which case we have `parameters`
1737                # * The ComponentDefinition comes from the manifest as a dict in which case we have `$parameters`
1738                # We should change those interfaces to use the model once we clean up the code in CDS at which point the parameter propagation should happen as part of the ModelToComponentFactory.
1739                parameters=datetime_based_cursor_model.parameters or {},
1740            )
1741            cursor_field = CursorField(
1742                cursor_field_key=interpolated_cursor_field.eval(config=config),
1743                supports_catalog_defined_cursor_field=datetime_based_cursor_model.allow_catalog_defined_cursor_field
1744                or False,
1745            )
1746
1747        datetime_format = datetime_based_cursor_model.datetime_format
1748
1749        cursor_granularity = (
1750            parse_duration(datetime_based_cursor_model.cursor_granularity)
1751            if datetime_based_cursor_model.cursor_granularity
1752            else None
1753        )
1754
1755        connector_state_converter: DateTimeStreamStateConverter
1756        connector_state_converter = CustomFormatConcurrentStreamStateConverter(
1757            datetime_format=datetime_format,
1758            input_datetime_formats=datetime_based_cursor_model.cursor_datetime_formats,
1759            is_sequential_state=True,  # ConcurrentPerPartitionCursor only works with sequential state
1760            cursor_granularity=cursor_granularity,
1761        )
1762
1763        # Create the cursor factory
1764        cursor_factory = ConcurrentCursorFactory(
1765            partial(
1766                self.create_concurrent_cursor_from_datetime_based_cursor,
1767                state_manager=state_manager,
1768                model_type=model_type,
1769                component_definition=component_definition,
1770                stream_name=stream_name,
1771                stream_namespace=stream_namespace,
1772                config=config,
1773                message_repository=NoopMessageRepository(),
1774            )
1775        )
1776
1777        # Per-partition state doesn't make sense for GroupingPartitionRouter, so force the global state
1778        use_global_cursor = isinstance(
1779            partition_router, GroupingPartitionRouter
1780        ) or component_definition.get("global_substream_cursor", False)
1781
1782        # Return the concurrent cursor and state converter
1783        return ConcurrentPerPartitionCursor(
1784            cursor_factory=cursor_factory,
1785            partition_router=partition_router,
1786            stream_name=stream_name,
1787            stream_namespace=stream_namespace,
1788            stream_state=stream_state,
1789            message_repository=self._message_repository,  # type: ignore
1790            connector_state_manager=state_manager,
1791            connector_state_converter=connector_state_converter,
1792            cursor_field=cursor_field,
1793            use_global_cursor=use_global_cursor,
1794            attempt_to_create_cursor_if_not_provided=attempt_to_create_cursor_if_not_provided,
1795        )
1797    @staticmethod
1798    def create_constant_backoff_strategy(
1799        model: ConstantBackoffStrategyModel, config: Config, **kwargs: Any
1800    ) -> ConstantBackoffStrategy:
1801        ModelToComponentFactory._validate_jitter_range(model.jitter_range_in_seconds)
1802        return ConstantBackoffStrategy(
1803            backoff_time_in_seconds=model.backoff_time_in_seconds,
1804            jitter_range_in_seconds=model.jitter_range_in_seconds,
1805            config=config,
1806            parameters=model.parameters or {},
1807        )
def create_cursor_pagination( self, model: airbyte_cdk.sources.declarative.models.declarative_component_schema.CursorPagination, config: Mapping[str, Any], decoder: airbyte_cdk.Decoder, **kwargs: Any) -> airbyte_cdk.CursorPaginationStrategy:
1814    def create_cursor_pagination(
1815        self, model: CursorPaginationModel, config: Config, decoder: Decoder, **kwargs: Any
1816    ) -> CursorPaginationStrategy:
1817        if isinstance(decoder, PaginationDecoderDecorator):
1818            inner_decoder = decoder.decoder
1819        else:
1820            inner_decoder = decoder
1821            decoder = PaginationDecoderDecorator(decoder=decoder)
1822
1823        if self._is_supported_decoder_for_pagination(inner_decoder):
1824            decoder_to_use = decoder
1825        else:
1826            raise ValueError(
1827                self._UNSUPPORTED_DECODER_ERROR.format(decoder_type=type(inner_decoder))
1828            )
1829
1830        # Pydantic v1 Union type coercion can convert int to string depending on Union order.
1831        # If page_size is a string that represents an integer (not an interpolation), convert it back.
1832        page_size = model.page_size
1833        if isinstance(page_size, str) and page_size.isdigit():
1834            page_size = int(page_size)
1835
1836        return CursorPaginationStrategy(
1837            cursor_value=model.cursor_value,
1838            decoder=decoder_to_use,
1839            page_size=page_size,
1840            stop_condition=model.stop_condition,
1841            config=config,
1842            parameters=model.parameters or {},
1843        )
def create_custom_component(self, model: Any, config: Mapping[str, Any], **kwargs: Any) -> Any:
1845    def create_custom_component(self, model: Any, config: Config, **kwargs: Any) -> Any:
1846        """
1847        Generically creates a custom component based on the model type and a class_name reference to the custom Python class being
1848        instantiated. Only the model's additional properties that match the custom class definition are passed to the constructor
1849        :param model: The Pydantic model of the custom component being created
1850        :param config: The custom defined connector config
1851        :return: The declarative component built from the Pydantic model to be used at runtime
1852        """
1853        # Instantiating a custom component means importing and executing arbitrary code referenced
1854        # by `class_name`. Manifests supplied by a caller, whether through the config or directly to
1855        # the manifest server, are untrusted input and could point `class_name` at any importable
1856        # callable, so they honor the same `AIRBYTE_ENABLE_UNSAFE_CODE` gate as injected
1857        # `components.py` code. Manifests bundled in a published connector image are trusted and may
1858        # always use their bundled custom components.
1859        manifest_is_untrusted = not self._custom_components_trusted or bool(
1860            config.get(INJECTED_MANIFEST)
1861        )
1862        if manifest_is_untrusted and not custom_code_execution_permitted():
1863            raise AirbyteCustomCodeNotPermittedError
1864
1865        custom_component_class = self._get_class_from_fully_qualified_class_name(model.class_name)
1866        component_fields = get_type_hints(custom_component_class)
1867        model_args = model.dict()
1868        model_args["config"] = config
1869
1870        # There are cases where a parent component will pass arguments to a child component via kwargs. When there are field collisions
1871        # we defer to these arguments over the component's definition
1872        for key, arg in kwargs.items():
1873            model_args[key] = arg
1874
1875        # Pydantic is unable to parse a custom component's fields that are subcomponents into models because their fields and types are not
1876        # defined in the schema. The fields and types are defined within the Python class implementation. Pydantic can only parse down to
1877        # the custom component and this code performs a second parse to convert the sub-fields first into models, then declarative components
1878        for model_field, model_value in model_args.items():
1879            # If a custom component field doesn't have a type set, we try to use the type hints to infer the type
1880            if (
1881                isinstance(model_value, dict)
1882                and "type" not in model_value
1883                and model_field in component_fields
1884            ):
1885                derived_type = self._derive_component_type_from_type_hints(
1886                    component_fields.get(model_field)
1887                )
1888                if derived_type:
1889                    model_value["type"] = derived_type
1890
1891            if self._is_component(model_value):
1892                model_args[model_field] = self._create_nested_component(
1893                    model,
1894                    model_field,
1895                    model_value,
1896                    config,
1897                    **kwargs,
1898                )
1899            elif isinstance(model_value, list):
1900                vals = []
1901                for v in model_value:
1902                    if isinstance(v, dict) and "type" not in v and model_field in component_fields:
1903                        derived_type = self._derive_component_type_from_type_hints(
1904                            component_fields.get(model_field)
1905                        )
1906                        if derived_type:
1907                            v["type"] = derived_type
1908                    if self._is_component(v):
1909                        vals.append(
1910                            self._create_nested_component(
1911                                model,
1912                                model_field,
1913                                v,
1914                                config,
1915                                **kwargs,
1916                            )
1917                        )
1918                    else:
1919                        vals.append(v)
1920                model_args[model_field] = vals
1921
1922        kwargs = {
1923            class_field: model_args[class_field]
1924            for class_field in component_fields.keys()
1925            if class_field in model_args
1926        }
1927
1928        if "api_budget" in component_fields and kwargs.get("api_budget") is None:
1929            kwargs["api_budget"] = self._api_budget
1930
1931        return custom_component_class(**kwargs)

Generically creates a custom component based on the model type and a class_name reference to the custom Python class being instantiated. Only the model's additional properties that match the custom class definition are passed to the constructor

Parameters
  • model: The Pydantic model of the custom component being created
  • config: The custom defined connector config
Returns

The declarative component built from the Pydantic model to be used at runtime

@staticmethod
def is_builtin_type(cls: Optional[Type[Any]]) -> bool:
1996    @staticmethod
1997    def is_builtin_type(cls: Optional[Type[Any]]) -> bool:
1998        if not cls:
1999            return False
2000        return cls.__module__ == "builtins"
def create_default_stream( self, model: airbyte_cdk.sources.declarative.models.declarative_component_schema.DeclarativeStream, config: Mapping[str, Any], is_parent: bool = False, **kwargs: Any) -> airbyte_cdk.sources.streams.concurrent.abstract_stream.AbstractStream:
2066    def create_default_stream(
2067        self, model: DeclarativeStreamModel, config: Config, is_parent: bool = False, **kwargs: Any
2068    ) -> AbstractStream:
2069        primary_key = model.primary_key.__root__ if model.primary_key else None
2070        self._migrate_state(model, config)
2071        self._warn_on_ineffective_incremental_dependency(model)
2072
2073        partition_router = self._build_stream_slicer_from_partition_router(
2074            model.retriever,
2075            config,
2076            stream_name=model.name,
2077            **kwargs,
2078        )
2079        concurrent_cursor = self._build_concurrent_cursor(model, partition_router, config)
2080        if model.incremental_sync and isinstance(model.incremental_sync, DatetimeBasedCursorModel):
2081            cursor_model: DatetimeBasedCursorModel = model.incremental_sync
2082
2083            end_time_option = (
2084                self._create_component_from_model(
2085                    cursor_model.end_time_option, config, parameters=cursor_model.parameters or {}
2086                )
2087                if cursor_model.end_time_option
2088                else None
2089            )
2090            start_time_option = (
2091                self._create_component_from_model(
2092                    cursor_model.start_time_option, config, parameters=cursor_model.parameters or {}
2093                )
2094                if cursor_model.start_time_option
2095                else None
2096            )
2097
2098            datetime_request_options_provider = DatetimeBasedRequestOptionsProvider(
2099                start_time_option=start_time_option,
2100                end_time_option=end_time_option,
2101                partition_field_start=cursor_model.partition_field_start,
2102                partition_field_end=cursor_model.partition_field_end,
2103                config=config,
2104                parameters=model.parameters or {},
2105            )
2106            request_options_provider = (
2107                datetime_request_options_provider
2108                if not isinstance(concurrent_cursor, ConcurrentPerPartitionCursor)
2109                else PerPartitionRequestOptionsProvider(
2110                    partition_router, datetime_request_options_provider
2111                )
2112            )
2113        elif model.incremental_sync and isinstance(
2114            model.incremental_sync, IncrementingCountCursorModel
2115        ):
2116            if isinstance(concurrent_cursor, ConcurrentPerPartitionCursor):
2117                raise ValueError(
2118                    "PerPartition does not support per partition states because switching to global state is time based"
2119                )
2120
2121            cursor_model: IncrementingCountCursorModel = model.incremental_sync  # type: ignore
2122
2123            start_time_option = (
2124                self._create_component_from_model(
2125                    cursor_model.start_value_option,  # type: ignore # mypy still thinks cursor_model of type DatetimeBasedCursor
2126                    config,
2127                    parameters=cursor_model.parameters or {},
2128                )
2129                if cursor_model.start_value_option  # type: ignore # mypy still thinks cursor_model of type DatetimeBasedCursor
2130                else None
2131            )
2132
2133            # The concurrent engine defaults the start/end fields on the slice to "start" and "end", but
2134            # the default DatetimeBasedRequestOptionsProvider() sets them to start_time/end_time
2135            partition_field_start = "start"
2136
2137            request_options_provider = DatetimeBasedRequestOptionsProvider(
2138                start_time_option=start_time_option,
2139                partition_field_start=partition_field_start,
2140                config=config,
2141                parameters=model.parameters or {},
2142            )
2143        else:
2144            request_options_provider = None
2145
2146        transformations = []
2147        if model.transformations:
2148            for transformation_model in model.transformations:
2149                transformations.append(
2150                    self._create_component_from_model(model=transformation_model, config=config)
2151                )
2152        file_uploader = None
2153        if model.file_uploader:
2154            file_uploader = self._create_component_from_model(
2155                model=model.file_uploader, config=config
2156            )
2157
2158        stream_slicer: ConcurrentStreamSlicer = (
2159            partition_router
2160            if isinstance(concurrent_cursor, FinalStateCursor)
2161            else concurrent_cursor
2162        )
2163
2164        retriever = self._create_component_from_model(
2165            model=model.retriever,
2166            config=config,
2167            name=model.name,
2168            primary_key=primary_key,
2169            request_options_provider=request_options_provider,
2170            stream_slicer=stream_slicer,
2171            partition_router=partition_router,
2172            has_stop_condition_cursor=self._is_stop_condition_on_cursor(model),
2173            is_client_side_incremental_sync=self._is_client_side_filtering_enabled(model),
2174            cursor=concurrent_cursor,
2175            transformations=transformations,
2176            file_uploader=file_uploader,
2177            incremental_sync=model.incremental_sync,
2178        )
2179        if isinstance(retriever, AsyncRetriever):
2180            stream_slicer = retriever.stream_slicer
2181
2182        schema_loader: SchemaLoader
2183        if model.schema_loader and isinstance(model.schema_loader, list):
2184            nested_schema_loaders = [
2185                self._create_component_from_model(model=nested_schema_loader, config=config)
2186                for nested_schema_loader in model.schema_loader
2187            ]
2188            schema_loader = CompositeSchemaLoader(
2189                schema_loaders=nested_schema_loaders, parameters={}
2190            )
2191        elif model.schema_loader:
2192            schema_loader = self._create_component_from_model(
2193                model=model.schema_loader,  # type: ignore # If defined, schema_loader is guaranteed not to be a list and will be one of the existing base models
2194                config=config,
2195            )
2196        else:
2197            options = model.parameters or {}
2198            if "name" not in options:
2199                options["name"] = model.name
2200            schema_loader = DefaultSchemaLoader(config=config, parameters=options)
2201        schema_loader = CachingSchemaLoaderDecorator(schema_loader)
2202
2203        stream_name = model.name or ""
2204        return DefaultStream(
2205            partition_generator=StreamSlicerPartitionGenerator(
2206                DeclarativePartitionFactory(
2207                    stream_name,
2208                    schema_loader,
2209                    retriever,
2210                    self._message_repository,
2211                ),
2212                stream_slicer,
2213                slice_limit=self._limit_slices_fetched,
2214            ),
2215            name=stream_name,
2216            json_schema=schema_loader.get_json_schema,
2217            primary_key=get_primary_key_from_stream(primary_key),
2218            cursor_field=(
2219                concurrent_cursor.cursor_field
2220                if hasattr(concurrent_cursor, "cursor_field")
2221                else None
2222            ),
2223            logger=logging.getLogger(f"airbyte.{stream_name}"),
2224            cursor=concurrent_cursor,
2225            supports_file_transfer=hasattr(model, "file_uploader") and bool(model.file_uploader),
2226        )
2398    def create_default_error_handler(
2399        self, model: DefaultErrorHandlerModel, config: Config, **kwargs: Any
2400    ) -> DefaultErrorHandler:
2401        backoff_strategies = []
2402        if model.backoff_strategies:
2403            for backoff_strategy_model in model.backoff_strategies:
2404                backoff_strategies.append(
2405                    self._create_component_from_model(model=backoff_strategy_model, config=config)
2406                )
2407
2408        response_filters = []
2409        if model.response_filters:
2410            for response_filter_model in model.response_filters:
2411                response_filters.append(
2412                    self._create_component_from_model(model=response_filter_model, config=config)
2413                )
2414        response_filters.append(
2415            HttpResponseFilter(config=config, parameters=model.parameters or {})
2416        )
2417
2418        return DefaultErrorHandler(
2419            backoff_strategies=backoff_strategies,
2420            max_retries=model.max_retries,
2421            response_filters=response_filters,
2422            config=config,
2423            parameters=model.parameters or {},
2424        )
2426    def create_default_paginator(
2427        self,
2428        model: DefaultPaginatorModel,
2429        config: Config,
2430        *,
2431        url_base: str,
2432        extractor_model: Optional[Union[CustomRecordExtractorModel, DpathExtractorModel]] = None,
2433        decoder: Optional[Decoder] = None,
2434        cursor_used_for_stop_condition: Optional[Cursor] = None,
2435    ) -> Union[DefaultPaginator, PaginatorTestReadDecorator]:
2436        if decoder:
2437            if self._is_supported_decoder_for_pagination(decoder):
2438                decoder_to_use = PaginationDecoderDecorator(decoder=decoder)
2439            else:
2440                raise ValueError(self._UNSUPPORTED_DECODER_ERROR.format(decoder_type=type(decoder)))
2441        else:
2442            decoder_to_use = PaginationDecoderDecorator(decoder=JsonDecoder(parameters={}))
2443        page_size_option = (
2444            self._create_component_from_model(model=model.page_size_option, config=config)
2445            if model.page_size_option
2446            else None
2447        )
2448        page_token_option = (
2449            self._create_component_from_model(model=model.page_token_option, config=config)
2450            if model.page_token_option
2451            else None
2452        )
2453        pagination_strategy = self._create_component_from_model(
2454            model=model.pagination_strategy,
2455            config=config,
2456            decoder=decoder_to_use,
2457            extractor_model=extractor_model,
2458        )
2459        if cursor_used_for_stop_condition:
2460            pagination_strategy = StopConditionPaginationStrategyDecorator(
2461                pagination_strategy, CursorStopCondition(cursor_used_for_stop_condition)
2462            )
2463        paginator = DefaultPaginator(
2464            decoder=decoder_to_use,
2465            page_size_option=page_size_option,
2466            page_token_option=page_token_option,
2467            pagination_strategy=pagination_strategy,
2468            url_base=url_base,
2469            config=config,
2470            parameters=model.parameters or {},
2471        )
2472        if self._limit_pages_fetched_per_slice:
2473            return PaginatorTestReadDecorator(paginator, self._limit_pages_fetched_per_slice)
2474        return paginator
def create_dpath_extractor( self, model: airbyte_cdk.sources.declarative.models.declarative_component_schema.DpathExtractor, config: Mapping[str, Any], decoder: Optional[airbyte_cdk.Decoder] = None, **kwargs: Any) -> airbyte_cdk.DpathExtractor:
2476    def create_dpath_extractor(
2477        self,
2478        model: DpathExtractorModel,
2479        config: Config,
2480        decoder: Optional[Decoder] = None,
2481        **kwargs: Any,
2482    ) -> DpathExtractor:
2483        if decoder:
2484            decoder_to_use = decoder
2485        else:
2486            decoder_to_use = JsonDecoder(parameters={})
2487        model_field_path: List[Union[InterpolatedString, str]] = [x for x in model.field_path]
2488
2489        record_expander = None
2490        if model.record_expander:
2491            record_expander = self._create_component_from_model(
2492                model=model.record_expander,
2493                config=config,
2494            )
2495
2496        return DpathExtractor(
2497            decoder=decoder_to_use,
2498            field_path=model_field_path,
2499            config=config,
2500            parameters=model.parameters or {},
2501            record_expander=record_expander,
2502        )
def create_record_expander( self, model: airbyte_cdk.sources.declarative.models.declarative_component_schema.RecordExpander, config: Mapping[str, Any], **kwargs: Any) -> airbyte_cdk.sources.declarative.expanders.RecordExpander:
2504    def create_record_expander(
2505        self,
2506        model: RecordExpanderModel,
2507        config: Config,
2508        **kwargs: Any,
2509    ) -> RecordExpander:
2510        truncated_list_retriever = None
2511        suppress_incomplete_fetch_warning = False
2512        if model.truncated_list_retriever:
2513            retriever_model = model.truncated_list_retriever
2514            name = "record_expander_truncated_list"
2515            # `CustomRetriever` allows extra fields, so read from the dumped model to cover both types.
2516            retriever_fields = retriever_model.dict()
2517            for unsupported_option in ("partition_router", "pagination_reset"):
2518                if retriever_fields.get(unsupported_option):
2519                    raise ValueError(
2520                        f"`{unsupported_option}` is not supported on `truncated_list_retriever`."
2521                    )
2522            log_formatter = lambda response: format_http_message(
2523                response,
2524                f"Record expander '{name}' request",
2525                "Request performed in order to fetch the complete nested list of a truncated record.",
2526                name,
2527                is_auxiliary=True,
2528            )
2529            # `name`/`primary_key`/`transformations` are also what a `CustomRetriever` forwards to its
2530            # nested `requester`/`record_selector`, so they are passed for both retriever types.
2531            truncated_list_retriever = self._create_component_from_model(
2532                model=retriever_model,
2533                config=config,
2534                name=name,
2535                primary_key=None,
2536                transformations=[],
2537                log_formatter=log_formatter,
2538            )
2539            # Only a capped paginator makes a shortfall expected; `NoPagination` and retrievers
2540            # without a paginator are never capped.
2541            suppress_incomplete_fetch_warning = isinstance(
2542                truncated_list_retriever, SimpleRetriever
2543            ) and isinstance(truncated_list_retriever.paginator, PaginatorTestReadDecorator)
2544        return RecordExpander(
2545            expand_records_from_field=model.expand_records_from_field,
2546            config=config,
2547            parameters=model.parameters or {},
2548            remain_original_record=model.remain_original_record or False,
2549            on_no_records=OnNoRecords(model.on_no_records.value)
2550            if model.on_no_records
2551            else OnNoRecords.skip,
2552            truncation_indicator_path=model.truncation_indicator_path,
2553            truncated_list_retriever=truncated_list_retriever,
2554            message_repository=self._message_repository,
2555            suppress_incomplete_fetch_warning=suppress_incomplete_fetch_warning,
2556        )
2558    @staticmethod
2559    def create_response_to_file_extractor(
2560        model: ResponseToFileExtractorModel,
2561        **kwargs: Any,
2562    ) -> ResponseToFileExtractor:
2563        return ResponseToFileExtractor(
2564            parameters=model.parameters or {},
2565            preserve_na_values=model.preserve_na_values or False,
2566        )
2568    @staticmethod
2569    def create_exponential_backoff_strategy(
2570        model: ExponentialBackoffStrategyModel, config: Config
2571    ) -> ExponentialBackoffStrategy:
2572        ModelToComponentFactory._validate_jitter_range(model.jitter_range_in_seconds)
2573        return ExponentialBackoffStrategy(
2574            factor=model.factor or 5,
2575            jitter_range_in_seconds=model.jitter_range_in_seconds,
2576            parameters=model.parameters or {},
2577            config=config,
2578        )
2580    @staticmethod
2581    def create_group_by_key(model: GroupByKeyMergeStrategyModel, config: Config) -> GroupByKey:
2582        return GroupByKey(model.key, config=config, parameters=model.parameters or {})
def create_http_requester( self, model: airbyte_cdk.sources.declarative.models.declarative_component_schema.HttpRequester, config: Mapping[str, Any], decoder: airbyte_cdk.Decoder = JsonDecoder(), query_properties_key: Optional[str] = None, use_cache: Optional[bool] = None, *, name: str) -> airbyte_cdk.HttpRequester:
2584    def create_http_requester(
2585        self,
2586        model: HttpRequesterModel,
2587        config: Config,
2588        decoder: Decoder = JsonDecoder(parameters={}),
2589        query_properties_key: Optional[str] = None,
2590        use_cache: Optional[bool] = None,
2591        *,
2592        name: str,
2593    ) -> HttpRequester:
2594        authenticator = (
2595            self._create_component_from_model(
2596                model=model.authenticator,
2597                config=config,
2598                url_base=model.url or model.url_base,
2599                name=name,
2600                decoder=decoder,
2601            )
2602            if model.authenticator
2603            else None
2604        )
2605        error_handler = (
2606            self._create_component_from_model(model=model.error_handler, config=config)
2607            if model.error_handler
2608            else DefaultErrorHandler(
2609                backoff_strategies=[],
2610                response_filters=[],
2611                config=config,
2612                parameters=model.parameters or {},
2613            )
2614        )
2615
2616        api_budget = self._api_budget
2617
2618        request_options_provider = InterpolatedRequestOptionsProvider(
2619            request_body=model.request_body,
2620            request_body_data=model.request_body_data,
2621            request_body_json=model.request_body_json,
2622            request_headers=model.request_headers,
2623            request_parameters=model.request_parameters,  # type: ignore  # QueryProperties have been removed in `create_simple_retriever`
2624            query_properties_key=query_properties_key,
2625            config=config,
2626            parameters=model.parameters or {},
2627        )
2628
2629        assert model.use_cache is not None  # for mypy
2630        assert model.http_method is not None  # for mypy
2631
2632        should_use_cache = (model.use_cache or bool(use_cache)) and not self._disable_cache
2633
2634        return HttpRequester(
2635            name=name,
2636            url=model.url,
2637            url_base=model.url_base,
2638            path=model.path,
2639            authenticator=authenticator,
2640            error_handler=error_handler,
2641            api_budget=api_budget,
2642            http_method=HttpMethod[model.http_method.value],
2643            request_options_provider=request_options_provider,
2644            config=config,
2645            disable_retries=self._disable_retries,
2646            parameters=model.parameters or {},
2647            message_repository=self._message_repository,
2648            use_cache=should_use_cache,
2649            decoder=decoder,
2650            stream_response=decoder.is_stream_response() if decoder else False,
2651        )
@staticmethod
def create_http_response_filter( model: airbyte_cdk.sources.declarative.models.declarative_component_schema.HttpResponseFilter, config: Mapping[str, Any], **kwargs: Any) -> airbyte_cdk.sources.declarative.requesters.error_handlers.HttpResponseFilter:
2653    @staticmethod
2654    def create_http_response_filter(
2655        model: HttpResponseFilterModel, config: Config, **kwargs: Any
2656    ) -> HttpResponseFilter:
2657        if model.action:
2658            action = ResponseAction(model.action.value)
2659        else:
2660            action = None
2661
2662        failure_type = FailureType(model.failure_type.value) if model.failure_type else None
2663
2664        http_codes = (
2665            set(model.http_codes) if model.http_codes else set()
2666        )  # JSON schema notation has no set data type. The schema enforces an array of unique elements
2667
2668        return HttpResponseFilter(
2669            action=action,
2670            failure_type=failure_type,
2671            error_message=model.error_message or "",
2672            error_message_contains=model.error_message_contains or "",
2673            http_codes=http_codes,
2674            predicate=model.predicate or "",
2675            config=config,
2676            parameters=model.parameters or {},
2677        )
@staticmethod
def create_inline_schema_loader( model: airbyte_cdk.sources.declarative.models.declarative_component_schema.InlineSchemaLoader, config: Mapping[str, Any], **kwargs: Any) -> airbyte_cdk.sources.declarative.schema.InlineSchemaLoader:
2679    @staticmethod
2680    def create_inline_schema_loader(
2681        model: InlineSchemaLoaderModel, config: Config, **kwargs: Any
2682    ) -> InlineSchemaLoader:
2683        return InlineSchemaLoader(schema=model.schema_ or {}, parameters={})
def create_complex_field_type( self, model: airbyte_cdk.sources.declarative.models.declarative_component_schema.ComplexFieldType, config: Mapping[str, Any], **kwargs: Any) -> airbyte_cdk.sources.declarative.schema.ComplexFieldType:
2685    def create_complex_field_type(
2686        self, model: ComplexFieldTypeModel, config: Config, **kwargs: Any
2687    ) -> ComplexFieldType:
2688        items = (
2689            self._create_component_from_model(model=model.items, config=config)
2690            if isinstance(model.items, ComplexFieldTypeModel)
2691            else model.items
2692        )
2693
2694        return ComplexFieldType(field_type=model.field_type, items=items)
def create_types_map( self, model: airbyte_cdk.sources.declarative.models.declarative_component_schema.TypesMap, config: Mapping[str, Any], **kwargs: Any) -> airbyte_cdk.sources.declarative.schema.TypesMap:
2696    def create_types_map(self, model: TypesMapModel, config: Config, **kwargs: Any) -> TypesMap:
2697        target_type = (
2698            self._create_component_from_model(model=model.target_type, config=config)
2699            if isinstance(model.target_type, ComplexFieldTypeModel)
2700            else model.target_type
2701        )
2702
2703        return TypesMap(
2704            target_type=target_type,
2705            current_type=model.current_type,
2706            condition=model.condition if model.condition is not None else "True",
2707        )
def create_schema_type_identifier( self, model: airbyte_cdk.sources.declarative.models.declarative_component_schema.SchemaTypeIdentifier, config: Mapping[str, Any], **kwargs: Any) -> airbyte_cdk.sources.declarative.schema.SchemaTypeIdentifier:
2709    def create_schema_type_identifier(
2710        self, model: SchemaTypeIdentifierModel, config: Config, **kwargs: Any
2711    ) -> SchemaTypeIdentifier:
2712        types_mapping = []
2713        if model.types_mapping:
2714            types_mapping.extend(
2715                [
2716                    self._create_component_from_model(types_map, config=config)
2717                    for types_map in model.types_mapping
2718                ]
2719            )
2720        model_schema_pointer: List[Union[InterpolatedString, str]] = (
2721            [x for x in model.schema_pointer] if model.schema_pointer else []
2722        )
2723        model_key_pointer: List[Union[InterpolatedString, str]] = [x for x in model.key_pointer]
2724        model_type_pointer: Optional[List[Union[InterpolatedString, str]]] = (
2725            [x for x in model.type_pointer] if model.type_pointer else None
2726        )
2727
2728        return SchemaTypeIdentifier(
2729            schema_pointer=model_schema_pointer,
2730            key_pointer=model_key_pointer,
2731            type_pointer=model_type_pointer,
2732            types_mapping=types_mapping,
2733            parameters=model.parameters or {},
2734        )
def create_dynamic_schema_loader( self, model: airbyte_cdk.sources.declarative.models.declarative_component_schema.DynamicSchemaLoader, config: Mapping[str, Any], **kwargs: Any) -> airbyte_cdk.sources.declarative.schema.DynamicSchemaLoader:
2736    def create_dynamic_schema_loader(
2737        self, model: DynamicSchemaLoaderModel, config: Config, **kwargs: Any
2738    ) -> DynamicSchemaLoader:
2739        schema_transformations = []
2740        if model.schema_transformations:
2741            for transformation_model in model.schema_transformations:
2742                schema_transformations.append(
2743                    self._create_component_from_model(model=transformation_model, config=config)
2744                )
2745        name = "dynamic_properties"
2746        retriever = self._create_component_from_model(
2747            model=model.retriever,
2748            config=config,
2749            name=name,
2750            primary_key=None,
2751            partition_router=self._build_stream_slicer_from_partition_router(
2752                model.retriever, config
2753            ),
2754            transformations=[],
2755            use_cache=True,
2756            log_formatter=(
2757                lambda response: format_http_message(
2758                    response,
2759                    f"Schema loader '{name}' request",
2760                    f"Request performed in order to extract schema.",
2761                    name,
2762                    is_auxiliary=True,
2763                )
2764            ),
2765        )
2766        schema_type_identifier = self._create_component_from_model(
2767            model.schema_type_identifier, config=config, parameters=model.parameters or {}
2768        )
2769        schema_filter = (
2770            self._create_component_from_model(
2771                model.schema_filter, config=config, parameters=model.parameters or {}
2772            )
2773            if model.schema_filter is not None
2774            else None
2775        )
2776
2777        return DynamicSchemaLoader(
2778            retriever=retriever,
2779            config=config,
2780            schema_transformations=schema_transformations,
2781            schema_filter=schema_filter,
2782            schema_type_identifier=schema_type_identifier,
2783            parameters=model.parameters or {},
2784        )
@staticmethod
def create_json_decoder( model: airbyte_cdk.sources.declarative.models.declarative_component_schema.JsonDecoder, config: Mapping[str, Any], **kwargs: Any) -> airbyte_cdk.Decoder:
2786    @staticmethod
2787    def create_json_decoder(model: JsonDecoderModel, config: Config, **kwargs: Any) -> Decoder:
2788        return JsonDecoder(parameters={})
def create_csv_decoder( self, model: airbyte_cdk.sources.declarative.models.declarative_component_schema.CsvDecoder, config: Mapping[str, Any], **kwargs: Any) -> airbyte_cdk.Decoder:
2790    def create_csv_decoder(self, model: CsvDecoderModel, config: Config, **kwargs: Any) -> Decoder:
2791        return CompositeRawDecoder(
2792            parser=ModelToComponentFactory._get_parser(model, config),
2793            stream_response=False if self._emit_connector_builder_messages else True,
2794        )
def create_jsonl_decoder( self, model: airbyte_cdk.sources.declarative.models.declarative_component_schema.JsonlDecoder, config: Mapping[str, Any], **kwargs: Any) -> airbyte_cdk.Decoder:
2796    def create_jsonl_decoder(
2797        self, model: JsonlDecoderModel, config: Config, **kwargs: Any
2798    ) -> Decoder:
2799        return CompositeRawDecoder(
2800            parser=ModelToComponentFactory._get_parser(model, config),
2801            stream_response=False if self._emit_connector_builder_messages else True,
2802        )
def create_json_items_decoder( self, model: airbyte_cdk.sources.declarative.models.declarative_component_schema.JsonItemsDecoder, config: Mapping[str, Any], **kwargs: Any) -> airbyte_cdk.Decoder:
2804    def create_json_items_decoder(
2805        self, model: JsonItemsDecoderModel, config: Config, **kwargs: Any
2806    ) -> Decoder:
2807        return CompositeRawDecoder(
2808            parser=ModelToComponentFactory._get_parser(model, config),
2809            stream_response=False if self._emit_connector_builder_messages else True,
2810        )
def create_gzip_decoder( self, model: airbyte_cdk.sources.declarative.models.declarative_component_schema.GzipDecoder, config: Mapping[str, Any], **kwargs: Any) -> airbyte_cdk.Decoder:
2812    def create_gzip_decoder(
2813        self, model: GzipDecoderModel, config: Config, **kwargs: Any
2814    ) -> Decoder:
2815        _compressed_response_types = {
2816            "gzip",
2817            "x-gzip",
2818            "gzip, deflate",
2819            "x-gzip, deflate",
2820            "application/zip",
2821            "application/gzip",
2822            "application/x-gzip",
2823            "application/x-zip-compressed",
2824        }
2825
2826        gzip_parser: GzipParser = ModelToComponentFactory._get_parser(model, config)  # type: ignore  # based on the model, we know this will be a GzipParser
2827
2828        if self._emit_connector_builder_messages:
2829            return CompositeRawDecoder(gzip_parser, False)
2830
2831        transport_gzip_parser = GzipParser(inner_parser=gzip_parser)
2832        return CompositeRawDecoder.by_headers(
2833            [
2834                ({"Content-Encoding"}, {"gzip"}, transport_gzip_parser),
2835                ({"Content-Type"}, _compressed_response_types, gzip_parser),
2836            ],
2837            stream_response=True,
2838            fallback_parser=gzip_parser,
2839        )
@staticmethod
def create_iterable_decoder( model: airbyte_cdk.sources.declarative.models.declarative_component_schema.IterableDecoder, config: Mapping[str, Any], **kwargs: Any) -> airbyte_cdk.sources.declarative.decoders.IterableDecoder:
2841    @staticmethod
2842    def create_iterable_decoder(
2843        model: IterableDecoderModel, config: Config, **kwargs: Any
2844    ) -> IterableDecoder:
2845        return IterableDecoder(parameters={})
@staticmethod
def create_xml_decoder( model: airbyte_cdk.sources.declarative.models.declarative_component_schema.XmlDecoder, config: Mapping[str, Any], **kwargs: Any) -> airbyte_cdk.sources.declarative.decoders.XmlDecoder:
2847    @staticmethod
2848    def create_xml_decoder(model: XmlDecoderModel, config: Config, **kwargs: Any) -> XmlDecoder:
2849        return XmlDecoder(parameters={})
def create_zipfile_decoder( self, model: airbyte_cdk.sources.declarative.models.declarative_component_schema.ZipfileDecoder, config: Mapping[str, Any], **kwargs: Any) -> airbyte_cdk.sources.declarative.decoders.ZipfileDecoder:
2851    def create_zipfile_decoder(
2852        self, model: ZipfileDecoderModel, config: Config, **kwargs: Any
2853    ) -> ZipfileDecoder:
2854        return ZipfileDecoder(parser=ModelToComponentFactory._get_parser(model.decoder, config))
@staticmethod
def create_json_file_schema_loader( model: airbyte_cdk.sources.declarative.models.declarative_component_schema.JsonFileSchemaLoader, config: Mapping[str, Any], **kwargs: Any) -> airbyte_cdk.JsonFileSchemaLoader:
2885    @staticmethod
2886    def create_json_file_schema_loader(
2887        model: JsonFileSchemaLoaderModel, config: Config, **kwargs: Any
2888    ) -> JsonFileSchemaLoader:
2889        return JsonFileSchemaLoader(
2890            file_path=model.file_path or "", config=config, parameters=model.parameters or {}
2891        )
def create_jwt_authenticator( self, model: airbyte_cdk.sources.declarative.models.declarative_component_schema.JwtAuthenticator, config: Mapping[str, Any], **kwargs: Any) -> airbyte_cdk.sources.declarative.auth.JwtAuthenticator:
2893    def create_jwt_authenticator(
2894        self, model: JwtAuthenticatorModel, config: Config, **kwargs: Any
2895    ) -> JwtAuthenticator:
2896        jwt_headers = model.jwt_headers or JwtHeadersModel(kid=None, typ="JWT", cty=None)
2897        jwt_payload = model.jwt_payload or JwtPayloadModel(iss=None, sub=None, aud=None)
2898        request_option = (
2899            self._create_component_from_model(model.request_option, config)
2900            if model.request_option
2901            else None
2902        )
2903        return JwtAuthenticator(
2904            config=config,
2905            parameters=model.parameters or {},
2906            algorithm=JwtAlgorithm(model.algorithm.value),
2907            secret_key=model.secret_key,
2908            base64_encode_secret_key=model.base64_encode_secret_key,
2909            token_duration=model.token_duration,
2910            header_prefix=model.header_prefix,
2911            kid=jwt_headers.kid,
2912            typ=jwt_headers.typ,
2913            cty=jwt_headers.cty,
2914            iss=jwt_payload.iss,
2915            sub=jwt_payload.sub,
2916            aud=jwt_payload.aud,
2917            additional_jwt_headers=model.additional_jwt_headers,
2918            additional_jwt_payload=model.additional_jwt_payload,
2919            passphrase=model.passphrase,
2920            request_option=request_option,
2921        )
def create_list_partition_router( self, model: airbyte_cdk.sources.declarative.models.declarative_component_schema.ListPartitionRouter, config: Mapping[str, Any], **kwargs: Any) -> airbyte_cdk.sources.declarative.partition_routers.ListPartitionRouter:
2923    def create_list_partition_router(
2924        self, model: ListPartitionRouterModel, config: Config, **kwargs: Any
2925    ) -> ListPartitionRouter:
2926        request_option = (
2927            self._create_component_from_model(model.request_option, config)
2928            if model.request_option
2929            else None
2930        )
2931        return ListPartitionRouter(
2932            cursor_field=model.cursor_field,
2933            request_option=request_option,
2934            values=model.values,
2935            config=config,
2936            parameters=model.parameters or {},
2937        )
@staticmethod
def create_min_max_datetime( model: airbyte_cdk.sources.declarative.models.declarative_component_schema.MinMaxDatetime, config: Mapping[str, Any], **kwargs: Any) -> airbyte_cdk.MinMaxDatetime:
2939    @staticmethod
2940    def create_min_max_datetime(
2941        model: MinMaxDatetimeModel, config: Config, **kwargs: Any
2942    ) -> MinMaxDatetime:
2943        return MinMaxDatetime(
2944            datetime=model.datetime,
2945            datetime_format=model.datetime_format or "",
2946            max_datetime=model.max_datetime or "",
2947            min_datetime=model.min_datetime or "",
2948            parameters=model.parameters or {},
2949        )
@staticmethod
def create_no_auth( model: airbyte_cdk.sources.declarative.models.declarative_component_schema.NoAuth, config: Mapping[str, Any], **kwargs: Any) -> airbyte_cdk.NoAuth:
2951    @staticmethod
2952    def create_no_auth(model: NoAuthModel, config: Config, **kwargs: Any) -> NoAuth:
2953        return NoAuth(parameters=model.parameters or {})
@staticmethod
def create_no_pagination( model: airbyte_cdk.sources.declarative.models.declarative_component_schema.NoPagination, config: Mapping[str, Any], **kwargs: Any) -> airbyte_cdk.sources.declarative.requesters.paginators.NoPagination:
2955    @staticmethod
2956    def create_no_pagination(
2957        model: NoPaginationModel, config: Config, **kwargs: Any
2958    ) -> NoPagination:
2959        return NoPagination(parameters={})
def create_oauth_authenticator( self, model: airbyte_cdk.sources.declarative.models.declarative_component_schema.OAuthAuthenticator, config: Mapping[str, Any], **kwargs: Any) -> airbyte_cdk.DeclarativeOauth2Authenticator:
2961    def create_oauth_authenticator(
2962        self, model: OAuthAuthenticatorModel, config: Config, **kwargs: Any
2963    ) -> DeclarativeOauth2Authenticator:
2964        profile_assertion = (
2965            self._create_component_from_model(model.profile_assertion, config=config)
2966            if model.profile_assertion
2967            else None
2968        )
2969
2970        refresh_token_error_status_codes, refresh_token_error_key, refresh_token_error_values = (
2971            self._get_refresh_token_error_information(model)
2972        )
2973        if model.refresh_token_updater:
2974            # ignore type error because fixing it would have a lot of dependencies, revisit later
2975            return DeclarativeSingleUseRefreshTokenOauth2Authenticator(  # type: ignore
2976                config,
2977                InterpolatedString.create(
2978                    model.token_refresh_endpoint,  # type: ignore
2979                    parameters=model.parameters or {},
2980                ).eval(config),
2981                access_token_name=InterpolatedString.create(
2982                    model.access_token_name or "access_token", parameters=model.parameters or {}
2983                ).eval(config),
2984                refresh_token_name=model.refresh_token_updater.refresh_token_name,
2985                expires_in_name=InterpolatedString.create(
2986                    model.expires_in_name or "expires_in", parameters=model.parameters or {}
2987                ).eval(config),
2988                client_id_name=InterpolatedString.create(
2989                    model.client_id_name or "client_id", parameters=model.parameters or {}
2990                ).eval(config),
2991                client_id=InterpolatedString.create(
2992                    model.client_id, parameters=model.parameters or {}
2993                ).eval(config)
2994                if model.client_id
2995                else model.client_id,
2996                client_secret_name=InterpolatedString.create(
2997                    model.client_secret_name or "client_secret", parameters=model.parameters or {}
2998                ).eval(config),
2999                client_secret=InterpolatedString.create(
3000                    model.client_secret, parameters=model.parameters or {}
3001                ).eval(config)
3002                if model.client_secret
3003                else model.client_secret,
3004                access_token_config_path=model.refresh_token_updater.access_token_config_path,
3005                refresh_token_config_path=model.refresh_token_updater.refresh_token_config_path,
3006                token_expiry_date_config_path=model.refresh_token_updater.token_expiry_date_config_path,
3007                grant_type_name=InterpolatedString.create(
3008                    model.grant_type_name or "grant_type", parameters=model.parameters or {}
3009                ).eval(config),
3010                grant_type=InterpolatedString.create(
3011                    model.grant_type or "refresh_token", parameters=model.parameters or {}
3012                ).eval(config),
3013                refresh_request_body=InterpolatedMapping(
3014                    model.refresh_request_body or {}, parameters=model.parameters or {}
3015                ).eval(config),
3016                refresh_request_headers=InterpolatedMapping(
3017                    model.refresh_request_headers or {}, parameters=model.parameters or {}
3018                ).eval(config),
3019                send_refresh_request_as_query_params=bool(
3020                    model.send_refresh_request_as_query_params
3021                ),
3022                scopes=model.scopes,
3023                token_expiry_date_format=model.token_expiry_date_format,
3024                token_expiry_is_time_of_expiration=bool(model.token_expiry_date_format),
3025                message_repository=self._message_repository,
3026                refresh_token_error_status_codes=refresh_token_error_status_codes,
3027                refresh_token_error_key=refresh_token_error_key,
3028                refresh_token_error_values=refresh_token_error_values,
3029            )
3030        # ignore type error because fixing it would have a lot of dependencies, revisit later
3031        return DeclarativeOauth2Authenticator(  # type: ignore
3032            access_token_name=model.access_token_name or "access_token",
3033            access_token_value=model.access_token_value,
3034            client_id_name=model.client_id_name or "client_id",
3035            client_id=model.client_id,
3036            client_secret_name=model.client_secret_name or "client_secret",
3037            client_secret=model.client_secret,
3038            expires_in_name=model.expires_in_name or "expires_in",
3039            grant_type_name=model.grant_type_name or "grant_type",
3040            grant_type=model.grant_type or "refresh_token",
3041            refresh_request_body=model.refresh_request_body,
3042            refresh_request_headers=model.refresh_request_headers,
3043            send_refresh_request_as_query_params=bool(model.send_refresh_request_as_query_params),
3044            refresh_token_name=model.refresh_token_name or "refresh_token",
3045            refresh_token=model.refresh_token,
3046            scopes=model.scopes,
3047            token_expiry_date=model.token_expiry_date,
3048            token_expiry_date_format=model.token_expiry_date_format,
3049            token_expiry_is_time_of_expiration=bool(model.token_expiry_date_format),
3050            token_refresh_endpoint=model.token_refresh_endpoint,
3051            config=config,
3052            parameters=model.parameters or {},
3053            message_repository=self._message_repository,
3054            profile_assertion=profile_assertion,
3055            use_profile_assertion=model.use_profile_assertion,
3056            refresh_token_error_status_codes=refresh_token_error_status_codes,
3057            refresh_token_error_key=refresh_token_error_key,
3058            refresh_token_error_values=refresh_token_error_values,
3059        )
3109    def create_offset_increment(
3110        self,
3111        model: OffsetIncrementModel,
3112        config: Config,
3113        decoder: Decoder,
3114        extractor_model: Optional[Union[CustomRecordExtractorModel, DpathExtractorModel]] = None,
3115        **kwargs: Any,
3116    ) -> OffsetIncrement:
3117        if isinstance(decoder, PaginationDecoderDecorator):
3118            inner_decoder = decoder.decoder
3119        else:
3120            inner_decoder = decoder
3121            decoder = PaginationDecoderDecorator(decoder=decoder)
3122
3123        if self._is_supported_decoder_for_pagination(inner_decoder):
3124            decoder_to_use = decoder
3125        else:
3126            raise ValueError(
3127                self._UNSUPPORTED_DECODER_ERROR.format(decoder_type=type(inner_decoder))
3128            )
3129
3130        # Ideally we would instantiate the runtime extractor from highest most level (in this case the SimpleRetriever)
3131        # so that it can be shared by OffSetIncrement and RecordSelector. However, due to how we instantiate the
3132        # decoder with various decorators here, but not in create_record_selector, it is simpler to retain existing
3133        # behavior by having two separate extractors with identical behavior since they use the same extractor model.
3134        # When we have more time to investigate we can look into reusing the same component.
3135        extractor = (
3136            self._create_component_from_model(
3137                model=extractor_model, config=config, decoder=decoder_to_use
3138            )
3139            if extractor_model
3140            else None
3141        )
3142
3143        # Pydantic v1 Union type coercion can convert int to string depending on Union order.
3144        # If page_size is a string that represents an integer (not an interpolation), convert it back.
3145        page_size = model.page_size
3146        if isinstance(page_size, str) and page_size.isdigit():
3147            page_size = int(page_size)
3148
3149        return OffsetIncrement(
3150            page_size=page_size,
3151            config=config,
3152            decoder=decoder_to_use,
3153            extractor=extractor,
3154            inject_on_first_request=model.inject_on_first_request or False,
3155            parameters=model.parameters or {},
3156        )
3158    def create_page_increment(
3159        self,
3160        model: PageIncrementModel,
3161        config: Config,
3162        decoder: Optional[Decoder] = None,
3163        extractor_model: Optional[Union[CustomRecordExtractorModel, DpathExtractorModel]] = None,
3164        **kwargs: Any,
3165    ) -> PageIncrement:
3166        # Like OffsetIncrement, we instantiate a separate extractor with identical behavior to the
3167        # RecordSelector's so the strategy can count the raw records in the response. This ensures
3168        # pagination is driven by the API's page size, not the post-filter record count.
3169        extractor = (
3170            self._create_component_from_model(model=extractor_model, config=config, decoder=decoder)
3171            if extractor_model
3172            else None
3173        )
3174
3175        # Pydantic v1 Union type coercion can convert int to string depending on Union order.
3176        # If page_size is a string that represents an integer (not an interpolation), convert it back.
3177        page_size = model.page_size
3178        if isinstance(page_size, str) and page_size.isdigit():
3179            page_size = int(page_size)
3180
3181        return PageIncrement(
3182            page_size=page_size,
3183            config=config,
3184            start_from_page=model.start_from_page or 0,
3185            inject_on_first_request=model.inject_on_first_request or False,
3186            extractor=extractor,
3187            parameters=model.parameters or {},
3188        )
def create_parent_stream_config( self, model: airbyte_cdk.sources.declarative.models.declarative_component_schema.ParentStreamConfig, config: Mapping[str, Any], *, stream_name: str, **kwargs: Any) -> airbyte_cdk.ParentStreamConfig:
3190    def create_parent_stream_config(
3191        self, model: ParentStreamConfigModel, config: Config, *, stream_name: str, **kwargs: Any
3192    ) -> ParentStreamConfig:
3193        declarative_stream = self._create_component_from_model(
3194            model.stream,
3195            config=config,
3196            is_parent=True,
3197            **kwargs,
3198        )
3199        request_option = (
3200            self._create_component_from_model(model.request_option, config=config)
3201            if model.request_option
3202            else None
3203        )
3204
3205        if model.lazy_read_pointer and any("*" in pointer for pointer in model.lazy_read_pointer):
3206            raise ValueError(
3207                "The '*' wildcard in 'lazy_read_pointer' is not supported — only direct paths are allowed."
3208            )
3209
3210        model_lazy_read_pointer: List[Union[InterpolatedString, str]] = (
3211            [x for x in model.lazy_read_pointer] if model.lazy_read_pointer else []
3212        )
3213
3214        return ParentStreamConfig(
3215            parent_key=model.parent_key,
3216            request_option=request_option,
3217            stream=declarative_stream,
3218            partition_field=model.partition_field,
3219            config=config,
3220            incremental_dependency=model.incremental_dependency or False,
3221            parameters=model.parameters or {},
3222            extra_fields=model.extra_fields,
3223            lazy_read_pointer=model_lazy_read_pointer,
3224        )
3226    def create_properties_from_endpoint(
3227        self, model: PropertiesFromEndpointModel, config: Config, **kwargs: Any
3228    ) -> PropertiesFromEndpoint:
3229        retriever = self._create_component_from_model(
3230            model=model.retriever,
3231            config=config,
3232            name="dynamic_properties",
3233            primary_key=None,
3234            stream_slicer=None,
3235            transformations=[],
3236            use_cache=True,  # Enable caching on the HttpRequester/HttpClient because the properties endpoint will be called for every slice being processed, and it is highly unlikely for the response to different
3237        )
3238        return PropertiesFromEndpoint(
3239            property_field_path=model.property_field_path,
3240            retriever=retriever,
3241            config=config,
3242            parameters=model.parameters or {},
3243        )
3245    def create_property_chunking(
3246        self, model: PropertyChunkingModel, config: Config, **kwargs: Any
3247    ) -> PropertyChunking:
3248        record_merge_strategy = (
3249            self._create_component_from_model(
3250                model=model.record_merge_strategy, config=config, **kwargs
3251            )
3252            if model.record_merge_strategy
3253            else None
3254        )
3255
3256        property_limit_type: PropertyLimitType
3257        match model.property_limit_type:
3258            case PropertyLimitTypeModel.property_count:
3259                property_limit_type = PropertyLimitType.property_count
3260            case PropertyLimitTypeModel.characters:
3261                property_limit_type = PropertyLimitType.characters
3262            case _:
3263                raise ValueError(f"Invalid PropertyLimitType {property_limit_type}")
3264
3265        return PropertyChunking(
3266            property_limit_type=property_limit_type,
3267            property_limit=model.property_limit,
3268            record_merge_strategy=record_merge_strategy,
3269            config=config,
3270            parameters=model.parameters or {},
3271        )
def create_query_properties( self, model: airbyte_cdk.sources.declarative.models.declarative_component_schema.QueryProperties, config: Mapping[str, Any], *, stream_name: str, **kwargs: Any) -> airbyte_cdk.sources.declarative.requesters.query_properties.QueryProperties:
3273    def create_query_properties(
3274        self, model: QueryPropertiesModel, config: Config, *, stream_name: str, **kwargs: Any
3275    ) -> QueryProperties:
3276        if isinstance(model.property_list, list):
3277            property_list = model.property_list
3278        else:
3279            property_list = self._create_component_from_model(
3280                model=model.property_list, config=config, **kwargs
3281            )
3282
3283        property_chunking = (
3284            self._create_component_from_model(
3285                model=model.property_chunking, config=config, **kwargs
3286            )
3287            if model.property_chunking
3288            else None
3289        )
3290
3291        property_selector = (
3292            self._create_component_from_model(
3293                model=model.property_selector, config=config, stream_name=stream_name, **kwargs
3294            )
3295            if model.property_selector
3296            else None
3297        )
3298
3299        return QueryProperties(
3300            property_list=property_list,
3301            always_include_properties=model.always_include_properties,
3302            property_chunking=property_chunking,
3303            property_selector=property_selector,
3304            config=config,
3305            parameters=model.parameters or {},
3306        )
def create_json_schema_property_selector( self, model: airbyte_cdk.sources.declarative.models.declarative_component_schema.JsonSchemaPropertySelector, config: Mapping[str, Any], *, stream_name: str, **kwargs: Any) -> airbyte_cdk.sources.declarative.requesters.query_properties.property_selector.JsonSchemaPropertySelector:
3308    def create_json_schema_property_selector(
3309        self,
3310        model: JsonSchemaPropertySelectorModel,
3311        config: Config,
3312        *,
3313        stream_name: str,
3314        **kwargs: Any,
3315    ) -> JsonSchemaPropertySelector:
3316        configured_stream = self._stream_name_to_configured_stream.get(stream_name)
3317
3318        transformations = []
3319        if model.transformations:
3320            for transformation_model in model.transformations:
3321                transformations.append(
3322                    self._create_component_from_model(model=transformation_model, config=config)
3323                )
3324
3325        return JsonSchemaPropertySelector(
3326            configured_stream=configured_stream,
3327            properties_transformations=transformations,
3328            config=config,
3329            parameters=model.parameters or {},
3330        )
@staticmethod
def create_record_filter( model: airbyte_cdk.sources.declarative.models.declarative_component_schema.RecordFilter, config: Mapping[str, Any], **kwargs: Any) -> airbyte_cdk.RecordFilter:
3332    @staticmethod
3333    def create_record_filter(
3334        model: RecordFilterModel, config: Config, **kwargs: Any
3335    ) -> RecordFilter:
3336        return RecordFilter(
3337            condition=model.condition or "", config=config, parameters=model.parameters or {}
3338        )
@staticmethod
def create_request_path( model: airbyte_cdk.sources.declarative.models.declarative_component_schema.RequestPath, config: Mapping[str, Any], **kwargs: Any) -> airbyte_cdk.sources.declarative.requesters.request_path.RequestPath:
3340    @staticmethod
3341    def create_request_path(model: RequestPathModel, config: Config, **kwargs: Any) -> RequestPath:
3342        return RequestPath(parameters={})
@staticmethod
def create_request_option( model: airbyte_cdk.sources.declarative.models.declarative_component_schema.RequestOption, config: Mapping[str, Any], **kwargs: Any) -> airbyte_cdk.RequestOption:
3344    @staticmethod
3345    def create_request_option(
3346        model: RequestOptionModel, config: Config, **kwargs: Any
3347    ) -> RequestOption:
3348        inject_into = RequestOptionType(model.inject_into.value)
3349        field_path: Optional[List[Union[InterpolatedString, str]]] = (
3350            [
3351                InterpolatedString.create(segment, parameters=kwargs.get("parameters", {}))
3352                for segment in model.field_path
3353            ]
3354            if model.field_path
3355            else None
3356        )
3357        field_name = (
3358            InterpolatedString.create(model.field_name, parameters=kwargs.get("parameters", {}))
3359            if model.field_name
3360            else None
3361        )
3362        return RequestOption(
3363            field_name=field_name,
3364            field_path=field_path,
3365            inject_into=inject_into,
3366            parameters=kwargs.get("parameters", {}),
3367        )
def create_record_selector( self, model: airbyte_cdk.sources.declarative.models.declarative_component_schema.RecordSelector, config: Mapping[str, Any], *, name: str, transformations: Optional[List[airbyte_cdk.RecordTransformation]] = None, decoder: airbyte_cdk.Decoder | None = None, client_side_incremental_sync_cursor: Optional[airbyte_cdk.Cursor] = None, is_client_side_incremental_sync: bool = False, file_uploader: Optional[airbyte_cdk.sources.declarative.retrievers.file_uploader.DefaultFileUploader] = None, **kwargs: Any) -> airbyte_cdk.RecordSelector:
3369    def create_record_selector(
3370        self,
3371        model: RecordSelectorModel,
3372        config: Config,
3373        *,
3374        name: str,
3375        transformations: List[RecordTransformation] | None = None,
3376        decoder: Decoder | None = None,
3377        client_side_incremental_sync_cursor: Optional[Cursor] = None,
3378        is_client_side_incremental_sync: bool = False,
3379        file_uploader: Optional[DefaultFileUploader] = None,
3380        **kwargs: Any,
3381    ) -> RecordSelector:
3382        extractor = self._create_component_from_model(
3383            model=model.extractor, decoder=decoder, config=config
3384        )
3385        record_filter = (
3386            self._create_component_from_model(model.record_filter, config=config)
3387            if model.record_filter
3388            else None
3389        )
3390
3391        # A client-side incremental stream transforms before filtering by default. That default belongs to the flag,
3392        # not to the component that ends up doing the cursor comparison: a data feed does it in the retriever and
3393        # receives no cursor here, but its `record_filter` condition must keep running after the transformations.
3394        default_transform_before_filtering = bool(
3395            client_side_incremental_sync_cursor or is_client_side_incremental_sync
3396        )
3397        transform_before_filtering = (
3398            default_transform_before_filtering
3399            if model.transform_before_filtering is None
3400            else model.transform_before_filtering
3401        )
3402        if client_side_incremental_sync_cursor:
3403            record_filter = ClientSideIncrementalRecordFilterDecorator(
3404                config=config,
3405                parameters=model.parameters,
3406                condition=model.record_filter.condition
3407                if (model.record_filter and hasattr(model.record_filter, "condition"))
3408                else None,
3409                cursor=client_side_incremental_sync_cursor,
3410            )
3411
3412        if model.schema_normalization is None:
3413            # default to no schema normalization if not set
3414            model.schema_normalization = SchemaNormalizationModel.None_
3415
3416        schema_normalization = (
3417            TypeTransformer(SCHEMA_TRANSFORMER_TYPE_MAPPING[model.schema_normalization])
3418            if isinstance(model.schema_normalization, SchemaNormalizationModel)
3419            else self._create_component_from_model(model.schema_normalization, config=config)  # type: ignore[arg-type] # custom normalization model expected here
3420        )
3421
3422        return RecordSelector(
3423            extractor=extractor,
3424            name=name,
3425            config=config,
3426            record_filter=record_filter,
3427            transformations=transformations or [],
3428            file_uploader=file_uploader,
3429            schema_normalization=schema_normalization,
3430            parameters=model.parameters or {},
3431            transform_before_filtering=transform_before_filtering,
3432        )
@staticmethod
def create_remove_fields( model: airbyte_cdk.sources.declarative.models.declarative_component_schema.RemoveFields, config: Mapping[str, Any], **kwargs: Any) -> airbyte_cdk.sources.declarative.transformations.RemoveFields:
3434    @staticmethod
3435    def create_remove_fields(
3436        model: RemoveFieldsModel, config: Config, **kwargs: Any
3437    ) -> RemoveFields:
3438        return RemoveFields(
3439            field_pointers=model.field_pointers, condition=model.condition or "", parameters={}
3440        )
def create_selective_authenticator( self, model: airbyte_cdk.sources.declarative.models.declarative_component_schema.SelectiveAuthenticator, config: Mapping[str, Any], **kwargs: Any) -> airbyte_cdk.DeclarativeAuthenticator:
3442    def create_selective_authenticator(
3443        self, model: SelectiveAuthenticatorModel, config: Config, **kwargs: Any
3444    ) -> DeclarativeAuthenticator:
3445        authenticators = {
3446            name: self._create_component_from_model(model=auth, config=config)
3447            for name, auth in model.authenticators.items()
3448        }
3449        # SelectiveAuthenticator will return instance of DeclarativeAuthenticator or raise ValueError error
3450        return SelectiveAuthenticator(  # type: ignore[abstract]
3451            config=config,
3452            authenticators=authenticators,
3453            authenticator_selection_path=model.authenticator_selection_path,
3454            **kwargs,
3455        )
@staticmethod
def create_legacy_session_token_authenticator( model: airbyte_cdk.sources.declarative.models.declarative_component_schema.LegacySessionTokenAuthenticator, config: Mapping[str, Any], *, url_base: str, **kwargs: Any) -> airbyte_cdk.sources.declarative.auth.token.LegacySessionTokenAuthenticator:
3457    @staticmethod
3458    def create_legacy_session_token_authenticator(
3459        model: LegacySessionTokenAuthenticatorModel, config: Config, *, url_base: str, **kwargs: Any
3460    ) -> LegacySessionTokenAuthenticator:
3461        return LegacySessionTokenAuthenticator(
3462            api_url=url_base,
3463            header=model.header,
3464            login_url=model.login_url,
3465            password=model.password or "",
3466            session_token=model.session_token or "",
3467            session_token_response_key=model.session_token_response_key or "",
3468            username=model.username or "",
3469            validate_session_url=model.validate_session_url,
3470            config=config,
3471            parameters=model.parameters or {},
3472        )
def create_simple_retriever( self, model: airbyte_cdk.sources.declarative.models.declarative_component_schema.SimpleRetriever, config: Mapping[str, Any], *, name: str, primary_key: Union[str, List[str], List[List[str]], NoneType], request_options_provider: Optional[airbyte_cdk.sources.declarative.requesters.request_options.RequestOptionsProvider] = None, cursor: Optional[airbyte_cdk.Cursor] = None, has_stop_condition_cursor: bool = False, is_client_side_incremental_sync: bool = False, transformations: List[airbyte_cdk.RecordTransformation], file_uploader: Optional[airbyte_cdk.sources.declarative.retrievers.file_uploader.DefaultFileUploader] = None, incremental_sync: Union[airbyte_cdk.sources.declarative.models.declarative_component_schema.IncrementingCountCursor, airbyte_cdk.sources.declarative.models.declarative_component_schema.DatetimeBasedCursor, NoneType] = None, use_cache: Optional[bool] = None, log_formatter: Optional[Callable[[requests.models.Response], Any]] = None, partition_router: Optional[airbyte_cdk.sources.declarative.partition_routers.PartitionRouter] = None, **kwargs: Any) -> airbyte_cdk.SimpleRetriever:
3474    def create_simple_retriever(
3475        self,
3476        model: SimpleRetrieverModel,
3477        config: Config,
3478        *,
3479        name: str,
3480        primary_key: Optional[Union[str, List[str], List[List[str]]]],
3481        request_options_provider: Optional[RequestOptionsProvider] = None,
3482        cursor: Optional[Cursor] = None,
3483        has_stop_condition_cursor: bool = False,
3484        is_client_side_incremental_sync: bool = False,
3485        transformations: List[RecordTransformation],
3486        file_uploader: Optional[DefaultFileUploader] = None,
3487        incremental_sync: Optional[
3488            Union[IncrementingCountCursorModel, DatetimeBasedCursorModel]
3489        ] = None,
3490        use_cache: Optional[bool] = None,
3491        log_formatter: Optional[Callable[[Response], Any]] = None,
3492        partition_router: Optional[PartitionRouter] = None,
3493        **kwargs: Any,
3494    ) -> SimpleRetriever:
3495        def _get_url(req: Requester) -> str:
3496            """
3497            Closure to get the URL from the requester. This is used to get the URL in the case of a lazy retriever.
3498            This is needed because the URL is not set until the requester is created.
3499            """
3500
3501            _url: str = (
3502                model.requester.url
3503                if hasattr(model.requester, "url") and model.requester.url is not None
3504                else req.get_url(stream_state=None, stream_slice=None, next_page_token=None)
3505            )
3506            _url_base: str = (
3507                model.requester.url_base
3508                if hasattr(model.requester, "url_base") and model.requester.url_base is not None
3509                else req.get_url_base(stream_state=None, stream_slice=None, next_page_token=None)
3510            )
3511
3512            return _url or _url_base
3513
3514        if cursor is None:
3515            cursor = FinalStateCursor(name, None, self._message_repository)
3516
3517        # A data feed drops the records the cursor considers already synced in the retriever, which sits downstream of
3518        # the paginator. Letting the record selector drop them as well would be redundant and would hide them from the
3519        # pagination stop condition, so a data feed never delegates that filtering to the record selector, whether
3520        # `is_client_side_incremental` is set or not. The `condition` from `record_filter` is intentionally left out of
3521        # the post-pagination filter and stays in the record selector, which preserves the existing behaviour: the
3522        # selector runs inside the page loop, so the records the condition rejects never reach the paginator's
3523        # accounting. Moving it downstream would start counting them.
3524        post_pagination_filter = (
3525            ClientSideIncrementalRecordFilterDecorator(
3526                config=config,
3527                parameters=model.parameters or {},
3528                condition=None,
3529                cursor=cursor,
3530            )
3531            if has_stop_condition_cursor
3532            else None
3533        )
3534        client_side_incremental_cursor = (
3535            cursor if is_client_side_incremental_sync and not post_pagination_filter else None
3536        )
3537        if post_pagination_filter and is_client_side_incremental_sync:
3538            LOGGER.warning(
3539                f"Stream {name}: `is_client_side_incremental` adds no record filtering when `is_data_feed` is set, "
3540                "as a data feed already filters on the cursor value. It still makes the record selector apply the "
3541                "transformations before the `record_filter` condition."
3542            )
3543
3544        decoder = (
3545            self._create_component_from_model(model=model.decoder, config=config)
3546            if model.decoder
3547            else JsonDecoder(parameters={})
3548        )
3549        record_selector = self._create_component_from_model(
3550            model=model.record_selector,
3551            name=name,
3552            config=config,
3553            decoder=decoder,
3554            transformations=transformations,
3555            client_side_incremental_sync_cursor=client_side_incremental_cursor,
3556            is_client_side_incremental_sync=is_client_side_incremental_sync,
3557            file_uploader=file_uploader,
3558        )
3559
3560        query_properties: Optional[QueryProperties] = None
3561        query_properties_key: Optional[str] = None
3562        self._ensure_query_properties_to_model(model.requester)
3563        if self._has_query_properties_in_request_parameters(model.requester):
3564            # It is better to be explicit about an error if PropertiesFromEndpoint is defined in multiple
3565            # places instead of default to request_parameters which isn't clearly documented
3566            if (
3567                hasattr(model.requester, "fetch_properties_from_endpoint")
3568                and model.requester.fetch_properties_from_endpoint
3569            ):
3570                raise ValueError(
3571                    f"PropertiesFromEndpoint should only be specified once per stream, but found in {model.requester.type}.fetch_properties_from_endpoint and {model.requester.type}.request_parameters"
3572                )
3573
3574            query_properties_definitions = []
3575            for key, request_parameter in model.requester.request_parameters.items():  # type: ignore # request_parameters is already validated to be a Mapping using _has_query_properties_in_request_parameters()
3576                if isinstance(request_parameter, QueryPropertiesModel):
3577                    query_properties_key = key
3578                    query_properties_definitions.append(request_parameter)
3579
3580            if len(query_properties_definitions) > 1:
3581                raise ValueError(
3582                    f"request_parameters only supports defining one QueryProperties field, but found {len(query_properties_definitions)} usages"
3583                )
3584
3585            if len(query_properties_definitions) == 1:
3586                query_properties = self._create_component_from_model(
3587                    model=query_properties_definitions[0], stream_name=name, config=config
3588                )
3589
3590            # Removes QueryProperties components from the interpolated mappings because it has been designed
3591            # to be used by the SimpleRetriever and will be resolved from the provider from the slice directly
3592            # instead of through jinja interpolation
3593            if hasattr(model.requester, "request_parameters") and isinstance(
3594                model.requester.request_parameters, Mapping
3595            ):
3596                model.requester.request_parameters = self._remove_query_properties(
3597                    model.requester.request_parameters
3598                )
3599        elif (
3600            hasattr(model.requester, "fetch_properties_from_endpoint")
3601            and model.requester.fetch_properties_from_endpoint
3602        ):
3603            # todo: Deprecate this condition once dependent connectors migrate to query_properties
3604            query_properties_definition = QueryPropertiesModel(
3605                type="QueryProperties",
3606                property_list=model.requester.fetch_properties_from_endpoint,
3607                always_include_properties=None,
3608                property_chunking=None,
3609            )  # type: ignore # $parameters has a default value
3610
3611            query_properties = self.create_query_properties(
3612                model=query_properties_definition,
3613                stream_name=name,
3614                config=config,
3615            )
3616        elif hasattr(model.requester, "query_properties") and model.requester.query_properties:
3617            query_properties = self.create_query_properties(
3618                model=model.requester.query_properties,
3619                stream_name=name,
3620                config=config,
3621            )
3622
3623        requester = self._create_component_from_model(
3624            model=model.requester,
3625            decoder=decoder,
3626            name=name,
3627            query_properties_key=query_properties_key,
3628            use_cache=use_cache,
3629            config=config,
3630        )
3631
3632        if not request_options_provider:
3633            request_options_provider = DefaultRequestOptionsProvider(parameters={})
3634        if isinstance(request_options_provider, DefaultRequestOptionsProvider) and isinstance(
3635            partition_router, PartitionRouter
3636        ):
3637            request_options_provider = partition_router
3638
3639        paginator = (
3640            self._create_component_from_model(
3641                model=model.paginator,
3642                config=config,
3643                url_base=_get_url(requester),
3644                extractor_model=model.record_selector.extractor,
3645                decoder=decoder,
3646                cursor_used_for_stop_condition=cursor if has_stop_condition_cursor else None,
3647            )
3648            if model.paginator
3649            else NoPagination(parameters={})
3650        )
3651
3652        ignore_stream_slicer_parameters_on_paginated_requests = (
3653            model.ignore_stream_slicer_parameters_on_paginated_requests or False
3654        )
3655
3656        if (
3657            model.partition_router
3658            and isinstance(model.partition_router, SubstreamPartitionRouterModel)
3659            and not bool(self._connector_state_manager.get_stream_state(name, None))
3660            and any(
3661                parent_stream_config.lazy_read_pointer
3662                for parent_stream_config in model.partition_router.parent_stream_configs
3663            )
3664        ):
3665            if incremental_sync:
3666                if incremental_sync.type != "DatetimeBasedCursor":
3667                    raise ValueError(
3668                        f"LazySimpleRetriever only supports DatetimeBasedCursor. Found: {incremental_sync.type}."
3669                    )
3670
3671                elif incremental_sync.step or incremental_sync.cursor_granularity:
3672                    raise ValueError(
3673                        f"Found more that one slice per parent. LazySimpleRetriever only supports single slice read for stream - {name}."
3674                    )
3675
3676            if model.decoder and model.decoder.type != "JsonDecoder":
3677                raise ValueError(
3678                    f"LazySimpleRetriever only supports JsonDecoder. Found: {model.decoder.type}."
3679                )
3680
3681            return LazySimpleRetriever(
3682                name=name,
3683                paginator=paginator,
3684                primary_key=primary_key,
3685                requester=requester,
3686                record_selector=record_selector,
3687                stream_slicer=_NO_STREAM_SLICING,
3688                request_option_provider=request_options_provider,
3689                config=config,
3690                ignore_stream_slicer_parameters_on_paginated_requests=ignore_stream_slicer_parameters_on_paginated_requests,
3691                post_pagination_filter=post_pagination_filter,
3692                parameters=model.parameters or {},
3693            )
3694
3695        if (
3696            model.record_selector.record_filter
3697            and model.pagination_reset
3698            and model.pagination_reset.limits
3699        ):
3700            raise ValueError("PaginationResetLimits are not supported while having record filter.")
3701
3702        return SimpleRetriever(
3703            name=name,
3704            paginator=paginator,
3705            primary_key=primary_key,
3706            requester=requester,
3707            record_selector=record_selector,
3708            stream_slicer=_NO_STREAM_SLICING,
3709            request_option_provider=request_options_provider,
3710            config=config,
3711            ignore_stream_slicer_parameters_on_paginated_requests=ignore_stream_slicer_parameters_on_paginated_requests,
3712            additional_query_properties=query_properties,
3713            log_formatter=self._get_log_formatter(log_formatter, name),
3714            pagination_tracker_factory=self._create_pagination_tracker_factory(
3715                model.pagination_reset, cursor
3716            ),
3717            post_pagination_filter=post_pagination_filter,
3718            parameters=model.parameters or {},
3719        )
def create_state_delegating_stream( self, model: airbyte_cdk.sources.declarative.models.declarative_component_schema.StateDelegatingStream, config: Mapping[str, Any], **kwargs: Any) -> airbyte_cdk.sources.streams.concurrent.default_stream.DefaultStream:
3797    def create_state_delegating_stream(
3798        self,
3799        model: StateDelegatingStreamModel,
3800        config: Config,
3801        **kwargs: Any,
3802    ) -> DefaultStream:
3803        if (
3804            model.full_refresh_stream.name != model.name
3805            or model.name != model.incremental_stream.name
3806        ):
3807            raise ValueError(
3808                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}."
3809            )
3810
3811        # Resolve api_retention_period with config context (supports Jinja2 interpolation)
3812        resolved_retention_period: Optional[str] = None
3813        if model.api_retention_period:
3814            interpolated_retention = InterpolatedString.create(
3815                model.api_retention_period, parameters=model.parameters or {}
3816            )
3817            resolved_value = interpolated_retention.eval(config=config)
3818            if resolved_value:
3819                resolved_retention_period = str(resolved_value)
3820
3821        if resolved_retention_period:
3822            for stream_model in (model.full_refresh_stream, model.incremental_stream):
3823                if isinstance(stream_model.incremental_sync, IncrementingCountCursorModel):
3824                    raise ValueError(
3825                        f"Stream '{model.name}' uses IncrementingCountCursor which is not supported "
3826                        f"with api_retention_period. IncrementingCountCursor does not use datetime-based "
3827                        f"cursors, so cursor age validation cannot be performed."
3828                    )
3829
3830        stream_state = self._connector_state_manager.get_stream_state(model.name, None)
3831
3832        if not stream_state:
3833            return self._create_component_from_model(  # type: ignore[no-any-return]
3834                model.full_refresh_stream, config=config, **kwargs
3835            )
3836
3837        incremental_stream: DefaultStream = self._create_component_from_model(
3838            model.incremental_stream, config=config, **kwargs
3839        )  # type: ignore[assignment]
3840
3841        # Only run cursor age validation for streams that are in the configured
3842        # catalog (or when no catalog was provided, e.g. during discover / connector
3843        # builder).  Streams not selected by the user but instantiated as parent-stream
3844        # dependencies must not go through this path because it emits state messages
3845        # that the destination does not know about, causing "Stream not found" crashes.
3846        stream_is_in_catalog = (
3847            not self._stream_name_to_configured_stream  # no catalog → validate by default
3848            or model.name in self._stream_name_to_configured_stream
3849        )
3850        if resolved_retention_period and stream_is_in_catalog:
3851            full_refresh_stream: DefaultStream = self._create_component_from_model(
3852                model.full_refresh_stream, config=config, **kwargs
3853            )  # type: ignore[assignment]
3854            if self._is_cursor_older_than_retention_period(
3855                stream_state,
3856                full_refresh_stream.cursor,
3857                incremental_stream.cursor,
3858                resolved_retention_period,
3859                model.name,
3860            ):
3861                # Clear state BEFORE constructing the full_refresh_stream so that
3862                # its cursor starts from start_date instead of the stale cursor.
3863                self._connector_state_manager.update_state_for_stream(model.name, None, {})
3864                state_message = self._connector_state_manager.create_state_message(model.name, None)
3865                self._message_repository.emit_message(state_message)
3866                return self._create_component_from_model(  # type: ignore[no-any-return]
3867                    model.full_refresh_stream, config=config, **kwargs
3868                )
3869
3870        return incremental_stream
def create_async_retriever( self, model: airbyte_cdk.sources.declarative.models.declarative_component_schema.AsyncRetriever, config: Mapping[str, Any], *, name: str, primary_key: Union[str, List[str], List[List[str]], NoneType], stream_slicer: Optional[airbyte_cdk.sources.declarative.stream_slicers.StreamSlicer], client_side_incremental_sync: Optional[Dict[str, Any]] = None, transformations: List[airbyte_cdk.RecordTransformation], **kwargs: Any) -> airbyte_cdk.sources.declarative.retrievers.AsyncRetriever:
3969    def create_async_retriever(
3970        self,
3971        model: AsyncRetrieverModel,
3972        config: Config,
3973        *,
3974        name: str,
3975        primary_key: Optional[
3976            Union[str, List[str], List[List[str]]]
3977        ],  # this seems to be needed to match create_simple_retriever
3978        stream_slicer: Optional[StreamSlicer],
3979        client_side_incremental_sync: Optional[Dict[str, Any]] = None,
3980        transformations: List[RecordTransformation],
3981        **kwargs: Any,
3982    ) -> AsyncRetriever:
3983        if model.download_target_requester and not model.download_target_extractor:
3984            raise ValueError(
3985                f"`download_target_extractor` required if using a `download_target_requester`"
3986            )
3987
3988        def _get_download_retriever(
3989            requester: Requester, extractor: RecordExtractor, _decoder: Decoder
3990        ) -> SimpleRetriever:
3991            # We create a record selector for the download retriever
3992            # with no schema normalization and no transformations, neither record filter
3993            # as all this occurs in the record_selector of the AsyncRetriever
3994            record_selector = RecordSelector(
3995                extractor=extractor,
3996                name=name,
3997                record_filter=None,
3998                transformations=[],
3999                schema_normalization=TypeTransformer(TransformConfig.NoTransform),
4000                config=config,
4001                parameters={},
4002            )
4003            paginator = (
4004                self._create_component_from_model(
4005                    model=model.download_paginator,
4006                    decoder=_decoder,
4007                    config=config,
4008                    url_base="",
4009                )
4010                if model.download_paginator
4011                else NoPagination(parameters={})
4012            )
4013
4014            return SimpleRetriever(
4015                requester=requester,
4016                record_selector=record_selector,
4017                primary_key=None,
4018                name=name,
4019                paginator=paginator,
4020                config=config,
4021                parameters={},
4022                log_formatter=self._get_log_formatter(None, name),
4023            )
4024
4025        def _get_job_timeout() -> datetime.timedelta:
4026            user_defined_timeout: Optional[int] = (
4027                int(
4028                    InterpolatedString.create(
4029                        str(model.polling_job_timeout),
4030                        parameters={},
4031                    ).eval(config)
4032                )
4033                if model.polling_job_timeout
4034                else None
4035            )
4036
4037            # check for user defined timeout during the test read or 15 minutes
4038            test_read_timeout = datetime.timedelta(minutes=user_defined_timeout or 15)
4039            # default value for non-connector builder is 60 minutes.
4040            default_sync_timeout = datetime.timedelta(minutes=user_defined_timeout or 60)
4041
4042            return (
4043                test_read_timeout if self._emit_connector_builder_messages else default_sync_timeout
4044            )
4045
4046        decoder = (
4047            self._create_component_from_model(model=model.decoder, config=config)
4048            if model.decoder
4049            else JsonDecoder(parameters={})
4050        )
4051        record_selector = self._create_component_from_model(
4052            model=model.record_selector,
4053            config=config,
4054            decoder=decoder,
4055            name=name,
4056            transformations=transformations,
4057            client_side_incremental_sync=client_side_incremental_sync,
4058        )
4059
4060        stream_slicer = stream_slicer or SinglePartitionRouter(parameters={})
4061        if self._should_limit_slices_fetched():
4062            stream_slicer = cast(
4063                StreamSlicer,
4064                StreamSlicerTestReadDecorator(
4065                    wrapped_slicer=stream_slicer,
4066                    maximum_number_of_slices=self._limit_slices_fetched or 5,
4067                ),
4068            )
4069
4070        creation_requester = self._create_component_from_model(
4071            model=model.creation_requester,
4072            decoder=decoder,
4073            config=config,
4074            name=f"job creation - {name}",
4075        )
4076        polling_requester = self._create_component_from_model(
4077            model=model.polling_requester,
4078            decoder=decoder,
4079            config=config,
4080            name=f"job polling - {name}",
4081        )
4082        job_download_components_name = f"job download - {name}"
4083        download_decoder = (
4084            self._create_component_from_model(model=model.download_decoder, config=config)
4085            if model.download_decoder
4086            else JsonDecoder(parameters={})
4087        )
4088        download_extractor = (
4089            self._create_component_from_model(
4090                model=model.download_extractor,
4091                config=config,
4092                decoder=download_decoder,
4093                parameters=model.parameters,
4094            )
4095            if model.download_extractor
4096            else DpathExtractor(
4097                [],
4098                config=config,
4099                decoder=download_decoder,
4100                parameters=model.parameters or {},
4101            )
4102        )
4103        download_requester = self._create_component_from_model(
4104            model=model.download_requester,
4105            decoder=download_decoder,
4106            config=config,
4107            name=job_download_components_name,
4108        )
4109        download_retriever = _get_download_retriever(
4110            download_requester, download_extractor, download_decoder
4111        )
4112        abort_requester = (
4113            self._create_component_from_model(
4114                model=model.abort_requester,
4115                decoder=decoder,
4116                config=config,
4117                name=f"job abort - {name}",
4118            )
4119            if model.abort_requester
4120            else None
4121        )
4122        delete_requester = (
4123            self._create_component_from_model(
4124                model=model.delete_requester,
4125                decoder=decoder,
4126                config=config,
4127                name=f"job delete - {name}",
4128            )
4129            if model.delete_requester
4130            else None
4131        )
4132        download_target_requester = (
4133            self._create_component_from_model(
4134                model=model.download_target_requester,
4135                decoder=decoder,
4136                config=config,
4137                name=f"job extract_url - {name}",
4138            )
4139            if model.download_target_requester
4140            else None
4141        )
4142        status_extractor = self._create_component_from_model(
4143            model=model.status_extractor, decoder=decoder, config=config, name=name
4144        )
4145        download_target_extractor = (
4146            self._create_component_from_model(
4147                model=model.download_target_extractor,
4148                decoder=decoder,
4149                config=config,
4150                name=name,
4151            )
4152            if model.download_target_extractor
4153            else None
4154        )
4155
4156        job_repository: AsyncJobRepository = AsyncHttpJobRepository(
4157            creation_requester=creation_requester,
4158            polling_requester=polling_requester,
4159            download_retriever=download_retriever,
4160            download_target_requester=download_target_requester,
4161            abort_requester=abort_requester,
4162            delete_requester=delete_requester,
4163            status_extractor=status_extractor,
4164            status_mapping=self._create_async_job_status_mapping(model.status_mapping, config),
4165            download_target_extractor=download_target_extractor,
4166            job_timeout=_get_job_timeout(),
4167        )
4168
4169        failed_retry_wait_time_in_seconds: Optional[int] = (
4170            int(
4171                InterpolatedString.create(
4172                    str(model.failed_retry_wait_time_in_seconds),
4173                    parameters={},
4174                ).eval(config)
4175            )
4176            if model.failed_retry_wait_time_in_seconds
4177            else None
4178        )
4179
4180        async_job_partition_router = AsyncJobPartitionRouter(
4181            job_orchestrator_factory=lambda stream_slices: AsyncJobOrchestrator(
4182                job_repository,
4183                stream_slices,
4184                self._job_tracker,
4185                self._message_repository,
4186                # FIXME work would need to be done here in order to detect if a stream as a parent stream that is bulk
4187                has_bulk_parent=False,
4188                # set the `job_max_retry` to 1 for the `Connector Builder`` use-case.
4189                # `None` == default retry is set to 3 attempts, under the hood.
4190                job_max_retry=1 if self._emit_connector_builder_messages else None,
4191                failed_retry_wait_time_in_seconds=failed_retry_wait_time_in_seconds,
4192            ),
4193            stream_slicer=stream_slicer,
4194            config=config,
4195            parameters=model.parameters or {},
4196        )
4197
4198        return AsyncRetriever(
4199            record_selector=record_selector,
4200            stream_slicer=async_job_partition_router,
4201            config=config,
4202            parameters=model.parameters or {},
4203        )
def create_spec( self, model: airbyte_cdk.sources.declarative.models.declarative_component_schema.Spec, config: Mapping[str, Any], **kwargs: Any) -> airbyte_cdk.sources.declarative.spec.Spec:
4205    def create_spec(self, model: SpecModel, config: Config, **kwargs: Any) -> Spec:
4206        config_migrations = [
4207            self._create_component_from_model(migration, config)
4208            for migration in (
4209                model.config_normalization_rules.config_migrations
4210                if (
4211                    model.config_normalization_rules
4212                    and model.config_normalization_rules.config_migrations
4213                )
4214                else []
4215            )
4216        ]
4217        config_transformations = [
4218            self._create_component_from_model(transformation, config)
4219            for transformation in (
4220                model.config_normalization_rules.transformations
4221                if (
4222                    model.config_normalization_rules
4223                    and model.config_normalization_rules.transformations
4224                )
4225                else []
4226            )
4227        ]
4228        config_validations = [
4229            self._create_component_from_model(validation, config)
4230            for validation in (
4231                model.config_normalization_rules.validations
4232                if (
4233                    model.config_normalization_rules
4234                    and model.config_normalization_rules.validations
4235                )
4236                else []
4237            )
4238        ]
4239
4240        return Spec(
4241            connection_specification=model.connection_specification,
4242            documentation_url=model.documentation_url,
4243            advanced_auth=model.advanced_auth,
4244            parameters={},
4245            config_migrations=config_migrations,
4246            config_transformations=config_transformations,
4247            config_validations=config_validations,
4248        )
def create_substream_partition_router( self, model: airbyte_cdk.sources.declarative.models.declarative_component_schema.SubstreamPartitionRouter, config: Mapping[str, Any], *, stream_name: str, **kwargs: Any) -> airbyte_cdk.SubstreamPartitionRouter:
4250    def create_substream_partition_router(
4251        self,
4252        model: SubstreamPartitionRouterModel,
4253        config: Config,
4254        *,
4255        stream_name: str,
4256        **kwargs: Any,
4257    ) -> SubstreamPartitionRouter:
4258        parent_stream_configs = []
4259        if model.parent_stream_configs:
4260            parent_stream_configs.extend(
4261                [
4262                    self.create_parent_stream_config_with_substream_wrapper(
4263                        model=parent_stream_config, config=config, stream_name=stream_name, **kwargs
4264                    )
4265                    for parent_stream_config in model.parent_stream_configs
4266                ]
4267            )
4268
4269        return SubstreamPartitionRouter(
4270            parent_stream_configs=parent_stream_configs,
4271            parameters=model.parameters or {},
4272            config=config,
4273        )
def create_parent_stream_config_with_substream_wrapper( self, model: airbyte_cdk.sources.declarative.models.declarative_component_schema.ParentStreamConfig, config: Mapping[str, Any], *, stream_name: str, **kwargs: Any) -> Any:
4275    def create_parent_stream_config_with_substream_wrapper(
4276        self, model: ParentStreamConfigModel, config: Config, *, stream_name: str, **kwargs: Any
4277    ) -> Any:
4278        child_state = self._connector_state_manager.get_stream_state(stream_name, None)
4279        if NO_CURSOR_STATE_KEY in child_state:
4280            # Full refresh streams checkpoint a `{NO_CURSOR_STATE_KEY: true}` sentinel. When such a
4281            # stream is later converted to incremental with an incremental_dependency parent,
4282            # `_instantiate_parent_stream_state_manager` would treat the sentinel's boolean as a legacy
4283            # cursor value and re-key it under the parent's cursor field, crashing cursor initialization.
4284            child_state = {
4285                key: value for key, value in child_state.items() if key != NO_CURSOR_STATE_KEY
4286            }
4287
4288        parent_state: Optional[Mapping[str, Any]] = (
4289            child_state if model.incremental_dependency and child_state else None
4290        )
4291        connector_state_manager = self._instantiate_parent_stream_state_manager(
4292            child_state, config, model, parent_state
4293        )
4294
4295        substream_factory = ModelToComponentFactory(
4296            custom_components_trusted=self._custom_components_trusted,
4297            connector_state_manager=connector_state_manager,
4298            limit_pages_fetched_per_slice=self._limit_pages_fetched_per_slice,
4299            limit_slices_fetched=self._limit_slices_fetched,
4300            emit_connector_builder_messages=self._emit_connector_builder_messages,
4301            disable_retries=self._disable_retries,
4302            disable_cache=self._disable_cache,
4303            message_repository=StateFilteringMessageRepository(
4304                LogAppenderMessageRepositoryDecorator(
4305                    {
4306                        "airbyte_cdk": {"stream": {"is_substream": True}},
4307                        "http": {"is_auxiliary": True},
4308                    },
4309                    self._message_repository,
4310                    self._evaluate_log_level(self._emit_connector_builder_messages),
4311                ),
4312            ),
4313            api_budget=self._api_budget,
4314            # Share the authenticator registry so parent and child streams draw from the
4315            # same token quota counters
4316            rate_limited_authenticators=self._rate_limited_authenticators,
4317        )
4318
4319        return substream_factory.create_parent_stream_config(
4320            model=model, config=config, stream_name=stream_name, **kwargs
4321        )
4381    @staticmethod
4382    def create_wait_time_from_header(
4383        model: WaitTimeFromHeaderModel, config: Config, **kwargs: Any
4384    ) -> WaitTimeFromHeaderBackoffStrategy:
4385        return WaitTimeFromHeaderBackoffStrategy(
4386            header=model.header,
4387            parameters=model.parameters or {},
4388            config=config,
4389            regex=model.regex,
4390            max_waiting_time_in_seconds=model.max_waiting_time_in_seconds,
4391        )
4393    @staticmethod
4394    def create_wait_until_time_from_header(
4395        model: WaitUntilTimeFromHeaderModel, config: Config, **kwargs: Any
4396    ) -> WaitUntilTimeFromHeaderBackoffStrategy:
4397        return WaitUntilTimeFromHeaderBackoffStrategy(
4398            header=model.header,
4399            parameters=model.parameters or {},
4400            config=config,
4401            min_wait=model.min_wait,
4402            regex=model.regex,
4403            max_waiting_time_in_seconds=model.max_waiting_time_in_seconds,
4404        )
def get_message_repository(self) -> airbyte_cdk.MessageRepository:
4406    def get_message_repository(self) -> MessageRepository:
4407        return self._message_repository
@staticmethod
def create_components_mapping_definition( model: airbyte_cdk.sources.declarative.models.declarative_component_schema.ComponentMappingDefinition, config: Mapping[str, Any], **kwargs: Any) -> airbyte_cdk.sources.declarative.resolvers.ComponentMappingDefinition:
4412    @staticmethod
4413    def create_components_mapping_definition(
4414        model: ComponentMappingDefinitionModel, config: Config, **kwargs: Any
4415    ) -> ComponentMappingDefinition:
4416        interpolated_value = InterpolatedString.create(
4417            model.value, parameters=model.parameters or {}
4418        )
4419        field_path = [
4420            InterpolatedString.create(path, parameters=model.parameters or {})
4421            for path in model.field_path
4422        ]
4423        return ComponentMappingDefinition(
4424            field_path=field_path,  # type: ignore[arg-type] # field_path can be str and InterpolatedString
4425            value=interpolated_value,
4426            value_type=ModelToComponentFactory._json_schema_type_name_to_type(model.value_type),
4427            create_or_update=model.create_or_update,
4428            condition=model.condition,
4429            parameters=model.parameters or {},
4430        )
def create_http_components_resolver( self, model: airbyte_cdk.sources.declarative.models.declarative_component_schema.HttpComponentsResolver, config: Mapping[str, Any], stream_name: Optional[str] = None) -> Any:
4432    def create_http_components_resolver(
4433        self, model: HttpComponentsResolverModel, config: Config, stream_name: Optional[str] = None
4434    ) -> Any:
4435        retriever = self._create_component_from_model(
4436            model=model.retriever,
4437            config=config,
4438            name=f"{stream_name if stream_name else '__http_components_resolver'}",
4439            primary_key=None,
4440            stream_slicer=self._build_stream_slicer_from_partition_router(model.retriever, config),
4441            transformations=[],
4442        )
4443
4444        components_mapping = []
4445        for component_mapping_definition_model in model.components_mapping:
4446            if component_mapping_definition_model.condition:
4447                raise ValueError("`condition` is only supported for     `ConfigComponentsResolver`")
4448            components_mapping.append(
4449                self._create_component_from_model(
4450                    model=component_mapping_definition_model,
4451                    value_type=ModelToComponentFactory._json_schema_type_name_to_type(
4452                        component_mapping_definition_model.value_type
4453                    ),
4454                    config=config,
4455                )
4456            )
4457
4458        return HttpComponentsResolver(
4459            retriever=retriever,
4460            stream_slicer=self._build_stream_slicer_from_partition_router(model.retriever, config),
4461            config=config,
4462            components_mapping=components_mapping,
4463            parameters=model.parameters or {},
4464        )
@staticmethod
def create_stream_config( model: airbyte_cdk.sources.declarative.models.declarative_component_schema.StreamConfig, config: Mapping[str, Any], **kwargs: Any) -> airbyte_cdk.sources.declarative.resolvers.StreamConfig:
4466    @staticmethod
4467    def create_stream_config(
4468        model: StreamConfigModel, config: Config, **kwargs: Any
4469    ) -> StreamConfig:
4470        model_configs_pointer: List[Union[InterpolatedString, str]] = (
4471            [x for x in model.configs_pointer] if model.configs_pointer else []
4472        )
4473
4474        return StreamConfig(
4475            configs_pointer=model_configs_pointer,
4476            default_values=model.default_values,
4477            parameters=model.parameters or {},
4478        )
def create_config_components_resolver( self, model: airbyte_cdk.sources.declarative.models.declarative_component_schema.ConfigComponentsResolver, config: Mapping[str, Any]) -> Any:
4480    def create_config_components_resolver(
4481        self,
4482        model: ConfigComponentsResolverModel,
4483        config: Config,
4484    ) -> Any:
4485        model_stream_configs = (
4486            model.stream_config if isinstance(model.stream_config, list) else [model.stream_config]
4487        )
4488
4489        stream_configs = [
4490            self._create_component_from_model(
4491                stream_config, config=config, parameters=model.parameters or {}
4492            )
4493            for stream_config in model_stream_configs
4494        ]
4495
4496        components_mapping = [
4497            self._create_component_from_model(
4498                model=components_mapping_definition_model,
4499                value_type=ModelToComponentFactory._json_schema_type_name_to_type(
4500                    components_mapping_definition_model.value_type
4501                ),
4502                config=config,
4503                parameters=model.parameters,
4504            )
4505            for components_mapping_definition_model in model.components_mapping
4506        ]
4507
4508        return ConfigComponentsResolver(
4509            stream_configs=stream_configs,
4510            config=config,
4511            components_mapping=components_mapping,
4512            parameters=model.parameters or {},
4513        )
4515    def create_parametrized_components_resolver(
4516        self,
4517        model: ParametrizedComponentsResolverModel,
4518        config: Config,
4519    ) -> ParametrizedComponentsResolver:
4520        stream_parameters = StreamParametersDefinition(
4521            list_of_parameters_for_stream=model.stream_parameters.list_of_parameters_for_stream
4522        )
4523
4524        components_mapping = []
4525        for components_mapping_definition_model in model.components_mapping:
4526            if components_mapping_definition_model.condition:
4527                raise ValueError("`condition` is only supported for `ConfigComponentsResolver`")
4528            components_mapping.append(
4529                self._create_component_from_model(
4530                    model=components_mapping_definition_model,
4531                    value_type=ModelToComponentFactory._json_schema_type_name_to_type(
4532                        components_mapping_definition_model.value_type
4533                    ),
4534                    config=config,
4535                )
4536            )
4537        return ParametrizedComponentsResolver(
4538            stream_parameters=stream_parameters,
4539            config=config,
4540            components_mapping=components_mapping,
4541            parameters=model.parameters or {},
4542        )
def create_http_api_budget( self, model: airbyte_cdk.sources.declarative.models.declarative_component_schema.HTTPAPIBudget, config: Mapping[str, Any], **kwargs: Any) -> airbyte_cdk.HttpAPIBudget:
4566    def create_http_api_budget(
4567        self, model: HTTPAPIBudgetModel, config: Config, **kwargs: Any
4568    ) -> HttpAPIBudget:
4569        policies = [
4570            self._create_component_from_model(model=policy, config=config)
4571            for policy in model.policies
4572        ]
4573
4574        return HttpAPIBudget(
4575            policies=policies,
4576            ratelimit_reset_header=model.ratelimit_reset_header or "ratelimit-reset",
4577            ratelimit_remaining_header=model.ratelimit_remaining_header or "ratelimit-remaining",
4578            status_codes_for_ratelimit_hit=model.status_codes_for_ratelimit_hit or [429],
4579        )
def create_fixed_window_call_rate_policy( self, model: airbyte_cdk.sources.declarative.models.declarative_component_schema.FixedWindowCallRatePolicy, config: Mapping[str, Any], **kwargs: Any) -> airbyte_cdk.sources.streams.call_rate.FixedWindowCallRatePolicy:
4581    def create_fixed_window_call_rate_policy(
4582        self, model: FixedWindowCallRatePolicyModel, config: Config, **kwargs: Any
4583    ) -> FixedWindowCallRatePolicy:
4584        matchers = [
4585            self._create_component_from_model(model=matcher, config=config)
4586            for matcher in model.matchers
4587        ]
4588
4589        # Set the initial reset timestamp to 10 days from now.
4590        # This value will be updated by the first request.
4591        return FixedWindowCallRatePolicy(
4592            next_reset_ts=datetime.datetime.now() + datetime.timedelta(days=10),
4593            period=parse_duration(model.period),
4594            call_limit=model.call_limit,
4595            matchers=matchers,
4596        )
def create_file_uploader( self, model: airbyte_cdk.sources.declarative.models.declarative_component_schema.FileUploader, config: Mapping[str, Any], **kwargs: Any) -> airbyte_cdk.sources.declarative.retrievers.file_uploader.FileUploader:
4598    def create_file_uploader(
4599        self, model: FileUploaderModel, config: Config, **kwargs: Any
4600    ) -> FileUploader:
4601        name = "File Uploader"
4602        requester = self._create_component_from_model(
4603            model=model.requester,
4604            config=config,
4605            name=name,
4606            **kwargs,
4607        )
4608        download_target_extractor = self._create_component_from_model(
4609            model=model.download_target_extractor,
4610            config=config,
4611            name=name,
4612            **kwargs,
4613        )
4614        emit_connector_builder_messages = self._emit_connector_builder_messages
4615        file_uploader = DefaultFileUploader(
4616            requester=requester,
4617            download_target_extractor=download_target_extractor,
4618            config=config,
4619            file_writer=NoopFileWriter()
4620            if emit_connector_builder_messages
4621            else LocalFileSystemFileWriter(),
4622            parameters=model.parameters or {},
4623            filename_extractor=model.filename_extractor if model.filename_extractor else None,
4624        )
4625
4626        return (
4627            ConnectorBuilderFileUploader(file_uploader)
4628            if emit_connector_builder_messages
4629            else file_uploader
4630        )
def create_moving_window_call_rate_policy( self, model: airbyte_cdk.sources.declarative.models.declarative_component_schema.MovingWindowCallRatePolicy, config: Mapping[str, Any], **kwargs: Any) -> airbyte_cdk.MovingWindowCallRatePolicy:
4632    def create_moving_window_call_rate_policy(
4633        self, model: MovingWindowCallRatePolicyModel, config: Config, **kwargs: Any
4634    ) -> MovingWindowCallRatePolicy:
4635        rates = [
4636            self._create_component_from_model(model=rate, config=config) for rate in model.rates
4637        ]
4638        matchers = [
4639            self._create_component_from_model(model=matcher, config=config)
4640            for matcher in model.matchers
4641        ]
4642        return MovingWindowCallRatePolicy(
4643            rates=rates,
4644            matchers=matchers,
4645        )
def create_unlimited_call_rate_policy( self, model: airbyte_cdk.sources.declarative.models.declarative_component_schema.UnlimitedCallRatePolicy, config: Mapping[str, Any], **kwargs: Any) -> airbyte_cdk.sources.streams.call_rate.UnlimitedCallRatePolicy:
4647    def create_unlimited_call_rate_policy(
4648        self, model: UnlimitedCallRatePolicyModel, config: Config, **kwargs: Any
4649    ) -> UnlimitedCallRatePolicy:
4650        matchers = [
4651            self._create_component_from_model(model=matcher, config=config)
4652            for matcher in model.matchers
4653        ]
4654
4655        return UnlimitedCallRatePolicy(
4656            matchers=matchers,
4657        )
def create_rate( self, model: airbyte_cdk.sources.declarative.models.declarative_component_schema.Rate, config: Mapping[str, Any], **kwargs: Any) -> airbyte_cdk.Rate:
4659    def create_rate(self, model: RateModel, config: Config, **kwargs: Any) -> Rate:
4660        interpolated_limit = InterpolatedString.create(str(model.limit), parameters={})
4661        return Rate(
4662            limit=int(interpolated_limit.eval(config=config)),
4663            interval=parse_duration(model.interval),
4664        )
def create_http_request_matcher( self, model: airbyte_cdk.sources.declarative.models.declarative_component_schema.HttpRequestRegexMatcher, config: Mapping[str, Any], **kwargs: Any) -> airbyte_cdk.sources.streams.call_rate.HttpRequestRegexMatcher:
4666    def create_http_request_matcher(
4667        self, model: HttpRequestRegexMatcherModel, config: Config, **kwargs: Any
4668    ) -> HttpRequestRegexMatcher:
4669        weight = model.weight
4670        if weight is not None:
4671            if isinstance(weight, str):
4672                weight = int(InterpolatedString.create(weight, parameters={}).eval(config))
4673            else:
4674                weight = int(weight)
4675            if weight < 1:
4676                raise ValueError(f"weight must be >= 1, got {weight}")
4677        return HttpRequestRegexMatcher(
4678            method=model.method,
4679            url_base=model.url_base,
4680            url_path_pattern=model.url_path_pattern,
4681            params=model.params,
4682            headers=model.headers,
4683            weight=weight,
4684        )
def create_rate_limited_multiple_token_authenticator( self, model: airbyte_cdk.sources.declarative.models.declarative_component_schema.RateLimitedMultipleTokenAuthenticator, config: Mapping[str, Any], **kwargs: Any) -> airbyte_cdk.sources.declarative.auth.RateLimitedMultipleTokenAuthenticator:
4686    def create_rate_limited_multiple_token_authenticator(
4687        self,
4688        model: RateLimitedMultipleTokenAuthenticatorModel,
4689        config: Config,
4690        **kwargs: Any,
4691    ) -> RateLimitedMultipleTokenAuthenticator:
4692        if isinstance(model.tokens, str):
4693            tokens_value = InterpolatedString.create(model.tokens, parameters={}).eval(config)
4694            delimiter = model.token_delimiter or ","
4695            tokens = [
4696                token.strip() for token in str(tokens_value).split(delimiter) if token.strip()
4697            ]
4698        else:
4699            tokens = [
4700                token_value
4701                for token in model.tokens
4702                if (
4703                    token_value := str(
4704                        InterpolatedString.create(token, parameters={}).eval(config)
4705                    ).strip()
4706                )
4707            ]
4708
4709        quota_specs = [
4710            {
4711                "name": quota_model.name,
4712                "remaining_path": quota_model.remaining_path,
4713                "reset_path": quota_model.reset_path,
4714                "limit_path": quota_model.limit_path,
4715                "remaining_header": quota_model.remaining_header,
4716                "reset_header": quota_model.reset_header,
4717                "limit_header": quota_model.limit_header,
4718                # Normalize the same way as the runtime TokenQuota below, so an omitted field
4719                # and an explicit `[]` key identically and keep sharing one set of counters.
4720                "exhaustion_status_codes": quota_model.exhaustion_status_codes or [],
4721                "matchers": [
4722                    {
4723                        "method": matcher_model.method,
4724                        "url_base": matcher_model.url_base,
4725                        "url_path_pattern": matcher_model.url_path_pattern,
4726                        "params": matcher_model.params,
4727                        "headers": matcher_model.headers,
4728                        "weight": matcher_model.weight,
4729                    }
4730                    for matcher_model in quota_model.matchers or []
4731                ],
4732            }
4733            for quota_model in model.quotas
4734        ]
4735
4736        quota_status_url = str(
4737            InterpolatedString.create(model.quota_status_source.url, parameters={}).eval(config)
4738        )
4739        quota_status_http_method = (
4740            model.quota_status_source.http_method.value
4741            if model.quota_status_source.http_method
4742            else "GET"
4743        )
4744        quota_status_headers = {
4745            key: str(InterpolatedString.create(value, parameters={}).eval(config))
4746            for key, value in (model.quota_status_source.request_headers or {}).items()
4747        }
4748        # Normalize the same way as the quota specs above, so an omitted field and an explicit
4749        # `[]` key identically and keep sharing one set of counters. Deduplicated as well as
4750        # sorted, because the runtime turns this into a set: without it `[404]` and `[404, 404]`
4751        # would key differently and stop sharing counters while behaving identically.
4752        quota_status_unavailable_status_codes = sorted(
4753            set(model.quota_status_source.unavailable_status_codes or [])
4754        )
4755        auth_method = model.auth_method or "Bearer"
4756        header = model.header or "Authorization"
4757        max_wait_time_str = str(
4758            InterpolatedString.create(model.max_wait_time or "PT2H", parameters={}).eval(config)
4759        )
4760        max_wait_time = parse_duration(max_wait_time_str)
4761        if not isinstance(max_wait_time, datetime.timedelta):
4762            raise ValueError(
4763                f"max_wait_time must be a fixed-length ISO 8601 duration (e.g. 'PT2H'); "
4764                f"calendar-unit durations like '{max_wait_time_str}' are not supported"
4765            )
4766        budget_reserve_fraction = (
4767            model.budget_reserve_fraction if model.budget_reserve_fraction is not None else 0.1
4768        )
4769        budget_min_reserve = (
4770            model.budget_min_reserve if model.budget_min_reserve is not None else 50
4771        )
4772
4773        # Reuse the same instance for identical definitions so that all streams share the
4774        # same token quota counters (similar to how api_budget is shared). The key is built
4775        # from the resolved constructor arguments rather than the raw model so that
4776        # stream-specific `$parameters` propagated onto the model (and its nested components)
4777        # cannot break instance sharing.
4778        cache_key = json.dumps(
4779            {
4780                "tokens": tokens,
4781                "quotas": quota_specs,
4782                "quota_status_url": quota_status_url,
4783                "quota_status_http_method": quota_status_http_method,
4784                "quota_status_headers": quota_status_headers,
4785                "quota_status_unavailable_status_codes": quota_status_unavailable_status_codes,
4786                "auth_method": auth_method,
4787                "header": header,
4788                "max_wait_time": max_wait_time.total_seconds(),
4789                "budget_reserve_fraction": budget_reserve_fraction,
4790                "budget_min_reserve": budget_min_reserve,
4791            },
4792            sort_keys=True,
4793        )
4794        if cache_key in self._rate_limited_authenticators:
4795            return self._rate_limited_authenticators[cache_key]
4796
4797        quotas = [
4798            TokenQuota(
4799                name=quota_model.name,
4800                remaining_path=quota_model.remaining_path,
4801                reset_path=quota_model.reset_path,
4802                limit_path=quota_model.limit_path,
4803                remaining_header=quota_model.remaining_header,
4804                reset_header=quota_model.reset_header,
4805                limit_header=quota_model.limit_header,
4806                exhaustion_status_codes=quota_model.exhaustion_status_codes or [],
4807                matchers=[
4808                    self.create_http_request_matcher(matcher_model, config)
4809                    for matcher_model in quota_model.matchers or []
4810                ],
4811            )
4812            for quota_model in model.quotas
4813        ]
4814
4815        authenticator = RateLimitedMultipleTokenAuthenticator(
4816            tokens=tokens,
4817            quotas=quotas,
4818            quota_status_url=quota_status_url,
4819            quota_status_http_method=quota_status_http_method,
4820            quota_status_headers=quota_status_headers,
4821            quota_status_unavailable_status_codes=quota_status_unavailable_status_codes,
4822            auth_method=auth_method,
4823            header=header,
4824            max_wait_time=max_wait_time,
4825            budget_reserve_fraction=budget_reserve_fraction,
4826            budget_min_reserve=budget_min_reserve,
4827        )
4828        self._rate_limited_authenticators[cache_key] = authenticator
4829        return authenticator
def set_api_budget( self, component_definition: Mapping[str, Any], config: Mapping[str, Any]) -> None:
4831    def set_api_budget(self, component_definition: ComponentDefinition, config: Config) -> None:
4832        self._api_budget = self.create_component(
4833            model_type=HTTPAPIBudgetModel, component_definition=component_definition, config=config
4834        )
def create_grouping_partition_router( self, model: airbyte_cdk.sources.declarative.models.declarative_component_schema.GroupingPartitionRouter, config: Mapping[str, Any], *, stream_name: str, **kwargs: Any) -> airbyte_cdk.sources.declarative.partition_routers.GroupingPartitionRouter:
4836    def create_grouping_partition_router(
4837        self,
4838        model: GroupingPartitionRouterModel,
4839        config: Config,
4840        *,
4841        stream_name: str,
4842        **kwargs: Any,
4843    ) -> GroupingPartitionRouter:
4844        underlying_router = self._create_component_from_model(
4845            model=model.underlying_partition_router,
4846            config=config,
4847            stream_name=stream_name,
4848            **kwargs,
4849        )
4850        if model.group_size < 1:
4851            raise ValueError(f"Group size must be greater than 0, got {model.group_size}")
4852
4853        # Request options in underlying partition routers are not supported for GroupingPartitionRouter
4854        # because they are specific to individual partitions and cannot be aggregated or handled
4855        # when grouping, potentially leading to incorrect API calls. Any request customization
4856        # should be managed at the stream level through the requester's configuration.
4857        if isinstance(underlying_router, SubstreamPartitionRouter):
4858            if any(
4859                parent_config.request_option
4860                for parent_config in underlying_router.parent_stream_configs
4861            ):
4862                raise ValueError("Request options are not supported for GroupingPartitionRouter.")
4863
4864        if isinstance(underlying_router, ListPartitionRouter):
4865            if underlying_router.request_option:
4866                raise ValueError("Request options are not supported for GroupingPartitionRouter.")
4867
4868        return GroupingPartitionRouter(
4869            group_size=model.group_size,
4870            underlying_partition_router=underlying_router,
4871            deduplicate=model.deduplicate if model.deduplicate is not None else True,
4872            config=config,
4873        )
def create_union_partition_router( self, model: airbyte_cdk.sources.declarative.models.declarative_component_schema.UnionPartitionRouter, config: Mapping[str, Any], *, stream_name: str, **kwargs: Any) -> airbyte_cdk.UnionPartitionRouter:
4875    def create_union_partition_router(
4876        self,
4877        model: UnionPartitionRouterModel,
4878        config: Config,
4879        *,
4880        stream_name: str,
4881        **kwargs: Any,
4882    ) -> UnionPartitionRouter:
4883        # The schema enforces minItems: 2 for manifests; this guard covers construction paths
4884        # that bypass JSON-schema validation (the generated model carries no min_items constraint).
4885        if len(model.partition_routers) < 2:
4886            raise ValueError(
4887                f"UnionPartitionRouter for stream {stream_name} needs at least 2 child partition routers"
4888            )
4889
4890        partition_routers = [
4891            self._create_component_from_model(
4892                model=child,
4893                config=config,
4894                stream_name=stream_name,
4895                **kwargs,
4896            )
4897            for child in model.partition_routers
4898        ]
4899
4900        # partition_field depends only on config/parameters, so it is evaluated once at build
4901        # time; the runtime component always receives a plain string.
4902        partition_field = InterpolatedString.create(
4903            model.partition_field, parameters=model.parameters or {}
4904        ).eval(config)
4905
4906        # Fail fast at build time when a built-in child router is statically known to emit a
4907        # partition field different from the union's. CustomPartitionRouter children are opaque
4908        # and can only be validated at runtime.
4909        for child_model in model.partition_routers:
4910            child_partition_fields: List[str] = []
4911            if isinstance(child_model, ListPartitionRouterModel):
4912                child_partition_fields.append(
4913                    InterpolatedString.create(
4914                        child_model.cursor_field, parameters=child_model.parameters or {}
4915                    ).eval(config)
4916                )
4917            elif isinstance(child_model, SubstreamPartitionRouterModel):
4918                for parent_stream_config in child_model.parent_stream_configs:
4919                    child_partition_fields.append(
4920                        InterpolatedString.create(
4921                            parent_stream_config.partition_field,
4922                            parameters=parent_stream_config.parameters
4923                            or child_model.parameters
4924                            or {},
4925                        ).eval(config)
4926                    )
4927            elif isinstance(child_model, UnionPartitionRouterModel):
4928                child_partition_fields.append(
4929                    InterpolatedString.create(
4930                        child_model.partition_field, parameters=child_model.parameters or {}
4931                    ).eval(config)
4932                )
4933            for child_partition_field in child_partition_fields:
4934                if child_partition_field != partition_field:
4935                    raise ValueError(
4936                        f"UnionPartitionRouter expects all child partition routers to emit the "
4937                        f"partition field '{partition_field}', but a "
4938                        f"{child_model.type} child emits '{child_partition_field}'."
4939                    )
4940
4941        # A union slice comes from exactly one child partition router, so request options
4942        # declared on children cannot be applied consistently to requests built from the
4943        # normalized union slices. Partition values should be consumed via interpolation
4944        # (e.g. stream_partition) instead. Note that this validation only covers built-in
4945        # router types; CustomPartitionRouter children are opaque, so any request options
4946        # they implement internally cannot be detected or rejected here.
4947        for router in partition_routers:
4948            if isinstance(router, SubstreamPartitionRouter):
4949                if any(
4950                    parent_config.request_option for parent_config in router.parent_stream_configs
4951                ):
4952                    raise ValueError("Request options are not supported for UnionPartitionRouter.")
4953            if isinstance(router, ListPartitionRouter) and router.request_option:
4954                raise ValueError("Request options are not supported for UnionPartitionRouter.")
4955
4956        return UnionPartitionRouter(
4957            partition_routers=partition_routers,
4958            partition_field=partition_field,
4959            parameters=model.parameters or {},
4960        )