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        return RecordExpander(
2510            expand_records_from_field=model.expand_records_from_field,
2511            config=config,
2512            parameters=model.parameters or {},
2513            remain_original_record=model.remain_original_record or False,
2514            on_no_records=OnNoRecords(model.on_no_records.value)
2515            if model.on_no_records
2516            else OnNoRecords.skip,
2517        )
2518
2519    @staticmethod
2520    def create_response_to_file_extractor(
2521        model: ResponseToFileExtractorModel,
2522        **kwargs: Any,
2523    ) -> ResponseToFileExtractor:
2524        return ResponseToFileExtractor(
2525            parameters=model.parameters or {},
2526            preserve_na_values=model.preserve_na_values or False,
2527        )
2528
2529    @staticmethod
2530    def create_exponential_backoff_strategy(
2531        model: ExponentialBackoffStrategyModel, config: Config
2532    ) -> ExponentialBackoffStrategy:
2533        ModelToComponentFactory._validate_jitter_range(model.jitter_range_in_seconds)
2534        return ExponentialBackoffStrategy(
2535            factor=model.factor or 5,
2536            jitter_range_in_seconds=model.jitter_range_in_seconds,
2537            parameters=model.parameters or {},
2538            config=config,
2539        )
2540
2541    @staticmethod
2542    def create_group_by_key(model: GroupByKeyMergeStrategyModel, config: Config) -> GroupByKey:
2543        return GroupByKey(model.key, config=config, parameters=model.parameters or {})
2544
2545    def create_http_requester(
2546        self,
2547        model: HttpRequesterModel,
2548        config: Config,
2549        decoder: Decoder = JsonDecoder(parameters={}),
2550        query_properties_key: Optional[str] = None,
2551        use_cache: Optional[bool] = None,
2552        *,
2553        name: str,
2554    ) -> HttpRequester:
2555        authenticator = (
2556            self._create_component_from_model(
2557                model=model.authenticator,
2558                config=config,
2559                url_base=model.url or model.url_base,
2560                name=name,
2561                decoder=decoder,
2562            )
2563            if model.authenticator
2564            else None
2565        )
2566        error_handler = (
2567            self._create_component_from_model(model=model.error_handler, config=config)
2568            if model.error_handler
2569            else DefaultErrorHandler(
2570                backoff_strategies=[],
2571                response_filters=[],
2572                config=config,
2573                parameters=model.parameters or {},
2574            )
2575        )
2576
2577        api_budget = self._api_budget
2578
2579        request_options_provider = InterpolatedRequestOptionsProvider(
2580            request_body=model.request_body,
2581            request_body_data=model.request_body_data,
2582            request_body_json=model.request_body_json,
2583            request_headers=model.request_headers,
2584            request_parameters=model.request_parameters,  # type: ignore  # QueryProperties have been removed in `create_simple_retriever`
2585            query_properties_key=query_properties_key,
2586            config=config,
2587            parameters=model.parameters or {},
2588        )
2589
2590        assert model.use_cache is not None  # for mypy
2591        assert model.http_method is not None  # for mypy
2592
2593        should_use_cache = (model.use_cache or bool(use_cache)) and not self._disable_cache
2594
2595        return HttpRequester(
2596            name=name,
2597            url=model.url,
2598            url_base=model.url_base,
2599            path=model.path,
2600            authenticator=authenticator,
2601            error_handler=error_handler,
2602            api_budget=api_budget,
2603            http_method=HttpMethod[model.http_method.value],
2604            request_options_provider=request_options_provider,
2605            config=config,
2606            disable_retries=self._disable_retries,
2607            parameters=model.parameters or {},
2608            message_repository=self._message_repository,
2609            use_cache=should_use_cache,
2610            decoder=decoder,
2611            stream_response=decoder.is_stream_response() if decoder else False,
2612        )
2613
2614    @staticmethod
2615    def create_http_response_filter(
2616        model: HttpResponseFilterModel, config: Config, **kwargs: Any
2617    ) -> HttpResponseFilter:
2618        if model.action:
2619            action = ResponseAction(model.action.value)
2620        else:
2621            action = None
2622
2623        failure_type = FailureType(model.failure_type.value) if model.failure_type else None
2624
2625        http_codes = (
2626            set(model.http_codes) if model.http_codes else set()
2627        )  # JSON schema notation has no set data type. The schema enforces an array of unique elements
2628
2629        return HttpResponseFilter(
2630            action=action,
2631            failure_type=failure_type,
2632            error_message=model.error_message or "",
2633            error_message_contains=model.error_message_contains or "",
2634            http_codes=http_codes,
2635            predicate=model.predicate or "",
2636            config=config,
2637            parameters=model.parameters or {},
2638        )
2639
2640    @staticmethod
2641    def create_inline_schema_loader(
2642        model: InlineSchemaLoaderModel, config: Config, **kwargs: Any
2643    ) -> InlineSchemaLoader:
2644        return InlineSchemaLoader(schema=model.schema_ or {}, parameters={})
2645
2646    def create_complex_field_type(
2647        self, model: ComplexFieldTypeModel, config: Config, **kwargs: Any
2648    ) -> ComplexFieldType:
2649        items = (
2650            self._create_component_from_model(model=model.items, config=config)
2651            if isinstance(model.items, ComplexFieldTypeModel)
2652            else model.items
2653        )
2654
2655        return ComplexFieldType(field_type=model.field_type, items=items)
2656
2657    def create_types_map(self, model: TypesMapModel, config: Config, **kwargs: Any) -> TypesMap:
2658        target_type = (
2659            self._create_component_from_model(model=model.target_type, config=config)
2660            if isinstance(model.target_type, ComplexFieldTypeModel)
2661            else model.target_type
2662        )
2663
2664        return TypesMap(
2665            target_type=target_type,
2666            current_type=model.current_type,
2667            condition=model.condition if model.condition is not None else "True",
2668        )
2669
2670    def create_schema_type_identifier(
2671        self, model: SchemaTypeIdentifierModel, config: Config, **kwargs: Any
2672    ) -> SchemaTypeIdentifier:
2673        types_mapping = []
2674        if model.types_mapping:
2675            types_mapping.extend(
2676                [
2677                    self._create_component_from_model(types_map, config=config)
2678                    for types_map in model.types_mapping
2679                ]
2680            )
2681        model_schema_pointer: List[Union[InterpolatedString, str]] = (
2682            [x for x in model.schema_pointer] if model.schema_pointer else []
2683        )
2684        model_key_pointer: List[Union[InterpolatedString, str]] = [x for x in model.key_pointer]
2685        model_type_pointer: Optional[List[Union[InterpolatedString, str]]] = (
2686            [x for x in model.type_pointer] if model.type_pointer else None
2687        )
2688
2689        return SchemaTypeIdentifier(
2690            schema_pointer=model_schema_pointer,
2691            key_pointer=model_key_pointer,
2692            type_pointer=model_type_pointer,
2693            types_mapping=types_mapping,
2694            parameters=model.parameters or {},
2695        )
2696
2697    def create_dynamic_schema_loader(
2698        self, model: DynamicSchemaLoaderModel, config: Config, **kwargs: Any
2699    ) -> DynamicSchemaLoader:
2700        schema_transformations = []
2701        if model.schema_transformations:
2702            for transformation_model in model.schema_transformations:
2703                schema_transformations.append(
2704                    self._create_component_from_model(model=transformation_model, config=config)
2705                )
2706        name = "dynamic_properties"
2707        retriever = self._create_component_from_model(
2708            model=model.retriever,
2709            config=config,
2710            name=name,
2711            primary_key=None,
2712            partition_router=self._build_stream_slicer_from_partition_router(
2713                model.retriever, config
2714            ),
2715            transformations=[],
2716            use_cache=True,
2717            log_formatter=(
2718                lambda response: format_http_message(
2719                    response,
2720                    f"Schema loader '{name}' request",
2721                    f"Request performed in order to extract schema.",
2722                    name,
2723                    is_auxiliary=True,
2724                )
2725            ),
2726        )
2727        schema_type_identifier = self._create_component_from_model(
2728            model.schema_type_identifier, config=config, parameters=model.parameters or {}
2729        )
2730        schema_filter = (
2731            self._create_component_from_model(
2732                model.schema_filter, config=config, parameters=model.parameters or {}
2733            )
2734            if model.schema_filter is not None
2735            else None
2736        )
2737
2738        return DynamicSchemaLoader(
2739            retriever=retriever,
2740            config=config,
2741            schema_transformations=schema_transformations,
2742            schema_filter=schema_filter,
2743            schema_type_identifier=schema_type_identifier,
2744            parameters=model.parameters or {},
2745        )
2746
2747    @staticmethod
2748    def create_json_decoder(model: JsonDecoderModel, config: Config, **kwargs: Any) -> Decoder:
2749        return JsonDecoder(parameters={})
2750
2751    def create_csv_decoder(self, model: CsvDecoderModel, config: Config, **kwargs: Any) -> Decoder:
2752        return CompositeRawDecoder(
2753            parser=ModelToComponentFactory._get_parser(model, config),
2754            stream_response=False if self._emit_connector_builder_messages else True,
2755        )
2756
2757    def create_jsonl_decoder(
2758        self, model: JsonlDecoderModel, config: Config, **kwargs: Any
2759    ) -> Decoder:
2760        return CompositeRawDecoder(
2761            parser=ModelToComponentFactory._get_parser(model, config),
2762            stream_response=False if self._emit_connector_builder_messages else True,
2763        )
2764
2765    def create_json_items_decoder(
2766        self, model: JsonItemsDecoderModel, config: Config, **kwargs: Any
2767    ) -> Decoder:
2768        return CompositeRawDecoder(
2769            parser=ModelToComponentFactory._get_parser(model, config),
2770            stream_response=False if self._emit_connector_builder_messages else True,
2771        )
2772
2773    def create_gzip_decoder(
2774        self, model: GzipDecoderModel, config: Config, **kwargs: Any
2775    ) -> Decoder:
2776        _compressed_response_types = {
2777            "gzip",
2778            "x-gzip",
2779            "gzip, deflate",
2780            "x-gzip, deflate",
2781            "application/zip",
2782            "application/gzip",
2783            "application/x-gzip",
2784            "application/x-zip-compressed",
2785        }
2786
2787        gzip_parser: GzipParser = ModelToComponentFactory._get_parser(model, config)  # type: ignore  # based on the model, we know this will be a GzipParser
2788
2789        if self._emit_connector_builder_messages:
2790            return CompositeRawDecoder(gzip_parser, False)
2791
2792        transport_gzip_parser = GzipParser(inner_parser=gzip_parser)
2793        return CompositeRawDecoder.by_headers(
2794            [
2795                ({"Content-Encoding"}, {"gzip"}, transport_gzip_parser),
2796                ({"Content-Type"}, _compressed_response_types, gzip_parser),
2797            ],
2798            stream_response=True,
2799            fallback_parser=gzip_parser,
2800        )
2801
2802    @staticmethod
2803    def create_iterable_decoder(
2804        model: IterableDecoderModel, config: Config, **kwargs: Any
2805    ) -> IterableDecoder:
2806        return IterableDecoder(parameters={})
2807
2808    @staticmethod
2809    def create_xml_decoder(model: XmlDecoderModel, config: Config, **kwargs: Any) -> XmlDecoder:
2810        return XmlDecoder(parameters={})
2811
2812    def create_zipfile_decoder(
2813        self, model: ZipfileDecoderModel, config: Config, **kwargs: Any
2814    ) -> ZipfileDecoder:
2815        return ZipfileDecoder(parser=ModelToComponentFactory._get_parser(model.decoder, config))
2816
2817    @staticmethod
2818    def _get_parser(model: BaseModel, config: Config) -> Parser:
2819        if isinstance(model, JsonDecoderModel):
2820            # Note that the logic is a bit different from the JsonDecoder as there is some legacy that is maintained to return {} on error cases
2821            return JsonParser()
2822        elif isinstance(model, JsonItemsDecoderModel):
2823            return JsonItemsParser(
2824                items_path=model.items_path,
2825                encoding=model.encoding,
2826            )
2827        elif isinstance(model, JsonlDecoderModel):
2828            return JsonLineParser()
2829        elif isinstance(model, CsvDecoderModel):
2830            return CsvParser(
2831                encoding=model.encoding,
2832                delimiter=model.delimiter,
2833                set_values_to_none=model.set_values_to_none,
2834            )
2835        elif isinstance(model, GzipDecoderModel):
2836            return GzipParser(
2837                inner_parser=ModelToComponentFactory._get_parser(model.decoder, config)
2838            )
2839        elif isinstance(
2840            model, (CustomDecoderModel, IterableDecoderModel, XmlDecoderModel, ZipfileDecoderModel)
2841        ):
2842            raise ValueError(f"Decoder type {model} does not have parser associated to it")
2843
2844        raise ValueError(f"Unknown decoder type {model}")
2845
2846    @staticmethod
2847    def create_json_file_schema_loader(
2848        model: JsonFileSchemaLoaderModel, config: Config, **kwargs: Any
2849    ) -> JsonFileSchemaLoader:
2850        return JsonFileSchemaLoader(
2851            file_path=model.file_path or "", config=config, parameters=model.parameters or {}
2852        )
2853
2854    def create_jwt_authenticator(
2855        self, model: JwtAuthenticatorModel, config: Config, **kwargs: Any
2856    ) -> JwtAuthenticator:
2857        jwt_headers = model.jwt_headers or JwtHeadersModel(kid=None, typ="JWT", cty=None)
2858        jwt_payload = model.jwt_payload or JwtPayloadModel(iss=None, sub=None, aud=None)
2859        request_option = (
2860            self._create_component_from_model(model.request_option, config)
2861            if model.request_option
2862            else None
2863        )
2864        return JwtAuthenticator(
2865            config=config,
2866            parameters=model.parameters or {},
2867            algorithm=JwtAlgorithm(model.algorithm.value),
2868            secret_key=model.secret_key,
2869            base64_encode_secret_key=model.base64_encode_secret_key,
2870            token_duration=model.token_duration,
2871            header_prefix=model.header_prefix,
2872            kid=jwt_headers.kid,
2873            typ=jwt_headers.typ,
2874            cty=jwt_headers.cty,
2875            iss=jwt_payload.iss,
2876            sub=jwt_payload.sub,
2877            aud=jwt_payload.aud,
2878            additional_jwt_headers=model.additional_jwt_headers,
2879            additional_jwt_payload=model.additional_jwt_payload,
2880            passphrase=model.passphrase,
2881            request_option=request_option,
2882        )
2883
2884    def create_list_partition_router(
2885        self, model: ListPartitionRouterModel, config: Config, **kwargs: Any
2886    ) -> ListPartitionRouter:
2887        request_option = (
2888            self._create_component_from_model(model.request_option, config)
2889            if model.request_option
2890            else None
2891        )
2892        return ListPartitionRouter(
2893            cursor_field=model.cursor_field,
2894            request_option=request_option,
2895            values=model.values,
2896            config=config,
2897            parameters=model.parameters or {},
2898        )
2899
2900    @staticmethod
2901    def create_min_max_datetime(
2902        model: MinMaxDatetimeModel, config: Config, **kwargs: Any
2903    ) -> MinMaxDatetime:
2904        return MinMaxDatetime(
2905            datetime=model.datetime,
2906            datetime_format=model.datetime_format or "",
2907            max_datetime=model.max_datetime or "",
2908            min_datetime=model.min_datetime or "",
2909            parameters=model.parameters or {},
2910        )
2911
2912    @staticmethod
2913    def create_no_auth(model: NoAuthModel, config: Config, **kwargs: Any) -> NoAuth:
2914        return NoAuth(parameters=model.parameters or {})
2915
2916    @staticmethod
2917    def create_no_pagination(
2918        model: NoPaginationModel, config: Config, **kwargs: Any
2919    ) -> NoPagination:
2920        return NoPagination(parameters={})
2921
2922    def create_oauth_authenticator(
2923        self, model: OAuthAuthenticatorModel, config: Config, **kwargs: Any
2924    ) -> DeclarativeOauth2Authenticator:
2925        profile_assertion = (
2926            self._create_component_from_model(model.profile_assertion, config=config)
2927            if model.profile_assertion
2928            else None
2929        )
2930
2931        refresh_token_error_status_codes, refresh_token_error_key, refresh_token_error_values = (
2932            self._get_refresh_token_error_information(model)
2933        )
2934        if model.refresh_token_updater:
2935            # ignore type error because fixing it would have a lot of dependencies, revisit later
2936            return DeclarativeSingleUseRefreshTokenOauth2Authenticator(  # type: ignore
2937                config,
2938                InterpolatedString.create(
2939                    model.token_refresh_endpoint,  # type: ignore
2940                    parameters=model.parameters or {},
2941                ).eval(config),
2942                access_token_name=InterpolatedString.create(
2943                    model.access_token_name or "access_token", parameters=model.parameters or {}
2944                ).eval(config),
2945                refresh_token_name=model.refresh_token_updater.refresh_token_name,
2946                expires_in_name=InterpolatedString.create(
2947                    model.expires_in_name or "expires_in", parameters=model.parameters or {}
2948                ).eval(config),
2949                client_id_name=InterpolatedString.create(
2950                    model.client_id_name or "client_id", parameters=model.parameters or {}
2951                ).eval(config),
2952                client_id=InterpolatedString.create(
2953                    model.client_id, parameters=model.parameters or {}
2954                ).eval(config)
2955                if model.client_id
2956                else model.client_id,
2957                client_secret_name=InterpolatedString.create(
2958                    model.client_secret_name or "client_secret", parameters=model.parameters or {}
2959                ).eval(config),
2960                client_secret=InterpolatedString.create(
2961                    model.client_secret, parameters=model.parameters or {}
2962                ).eval(config)
2963                if model.client_secret
2964                else model.client_secret,
2965                access_token_config_path=model.refresh_token_updater.access_token_config_path,
2966                refresh_token_config_path=model.refresh_token_updater.refresh_token_config_path,
2967                token_expiry_date_config_path=model.refresh_token_updater.token_expiry_date_config_path,
2968                grant_type_name=InterpolatedString.create(
2969                    model.grant_type_name or "grant_type", parameters=model.parameters or {}
2970                ).eval(config),
2971                grant_type=InterpolatedString.create(
2972                    model.grant_type or "refresh_token", parameters=model.parameters or {}
2973                ).eval(config),
2974                refresh_request_body=InterpolatedMapping(
2975                    model.refresh_request_body or {}, parameters=model.parameters or {}
2976                ).eval(config),
2977                refresh_request_headers=InterpolatedMapping(
2978                    model.refresh_request_headers or {}, parameters=model.parameters or {}
2979                ).eval(config),
2980                send_refresh_request_as_query_params=bool(
2981                    model.send_refresh_request_as_query_params
2982                ),
2983                scopes=model.scopes,
2984                token_expiry_date_format=model.token_expiry_date_format,
2985                token_expiry_is_time_of_expiration=bool(model.token_expiry_date_format),
2986                message_repository=self._message_repository,
2987                refresh_token_error_status_codes=refresh_token_error_status_codes,
2988                refresh_token_error_key=refresh_token_error_key,
2989                refresh_token_error_values=refresh_token_error_values,
2990            )
2991        # ignore type error because fixing it would have a lot of dependencies, revisit later
2992        return DeclarativeOauth2Authenticator(  # type: ignore
2993            access_token_name=model.access_token_name or "access_token",
2994            access_token_value=model.access_token_value,
2995            client_id_name=model.client_id_name or "client_id",
2996            client_id=model.client_id,
2997            client_secret_name=model.client_secret_name or "client_secret",
2998            client_secret=model.client_secret,
2999            expires_in_name=model.expires_in_name or "expires_in",
3000            grant_type_name=model.grant_type_name or "grant_type",
3001            grant_type=model.grant_type or "refresh_token",
3002            refresh_request_body=model.refresh_request_body,
3003            refresh_request_headers=model.refresh_request_headers,
3004            send_refresh_request_as_query_params=bool(model.send_refresh_request_as_query_params),
3005            refresh_token_name=model.refresh_token_name or "refresh_token",
3006            refresh_token=model.refresh_token,
3007            scopes=model.scopes,
3008            token_expiry_date=model.token_expiry_date,
3009            token_expiry_date_format=model.token_expiry_date_format,
3010            token_expiry_is_time_of_expiration=bool(model.token_expiry_date_format),
3011            token_refresh_endpoint=model.token_refresh_endpoint,
3012            config=config,
3013            parameters=model.parameters or {},
3014            message_repository=self._message_repository,
3015            profile_assertion=profile_assertion,
3016            use_profile_assertion=model.use_profile_assertion,
3017            refresh_token_error_status_codes=refresh_token_error_status_codes,
3018            refresh_token_error_key=refresh_token_error_key,
3019            refresh_token_error_values=refresh_token_error_values,
3020        )
3021
3022    @staticmethod
3023    def _get_refresh_token_error_information(
3024        model: OAuthAuthenticatorModel,
3025    ) -> Tuple[Tuple[int, ...], str, Tuple[str, ...]]:
3026        """
3027        In a previous version of the CDK, the auth error as config_error was only done if a refresh token updater was
3028        defined. As a transition, we added those fields on the OAuthAuthenticatorModel. This method ensures that the
3029        information is defined only once and return the right fields.
3030        """
3031        refresh_token_updater = model.refresh_token_updater
3032        is_defined_on_refresh_token_updated = refresh_token_updater and (
3033            refresh_token_updater.refresh_token_error_status_codes
3034            or refresh_token_updater.refresh_token_error_key
3035            or refresh_token_updater.refresh_token_error_values
3036        )
3037        is_defined_on_oauth_authenticator = (
3038            model.refresh_token_error_status_codes
3039            or model.refresh_token_error_key
3040            or model.refresh_token_error_values
3041        )
3042        if is_defined_on_refresh_token_updated and is_defined_on_oauth_authenticator:
3043            raise ValueError(
3044                "refresh_token_error should either be defined on the OAuthAuthenticatorModel or the RefreshTokenUpdaterModel, not both"
3045            )
3046
3047        if is_defined_on_refresh_token_updated:
3048            not_optional_refresh_token_updater: RefreshTokenUpdaterModel = refresh_token_updater  # type: ignore  # we know from the condition that this is not None
3049            return (
3050                tuple(not_optional_refresh_token_updater.refresh_token_error_status_codes)
3051                if not_optional_refresh_token_updater.refresh_token_error_status_codes
3052                else (),
3053                not_optional_refresh_token_updater.refresh_token_error_key or "",
3054                tuple(not_optional_refresh_token_updater.refresh_token_error_values)
3055                if not_optional_refresh_token_updater.refresh_token_error_values
3056                else (),
3057            )
3058        elif is_defined_on_oauth_authenticator:
3059            return (
3060                tuple(model.refresh_token_error_status_codes)
3061                if model.refresh_token_error_status_codes
3062                else (),
3063                model.refresh_token_error_key or "",
3064                tuple(model.refresh_token_error_values) if model.refresh_token_error_values else (),
3065            )
3066
3067        # returning default values we think cover most cases
3068        return (400,), "error", ("invalid_grant", "invalid_permissions")
3069
3070    def create_offset_increment(
3071        self,
3072        model: OffsetIncrementModel,
3073        config: Config,
3074        decoder: Decoder,
3075        extractor_model: Optional[Union[CustomRecordExtractorModel, DpathExtractorModel]] = None,
3076        **kwargs: Any,
3077    ) -> OffsetIncrement:
3078        if isinstance(decoder, PaginationDecoderDecorator):
3079            inner_decoder = decoder.decoder
3080        else:
3081            inner_decoder = decoder
3082            decoder = PaginationDecoderDecorator(decoder=decoder)
3083
3084        if self._is_supported_decoder_for_pagination(inner_decoder):
3085            decoder_to_use = decoder
3086        else:
3087            raise ValueError(
3088                self._UNSUPPORTED_DECODER_ERROR.format(decoder_type=type(inner_decoder))
3089            )
3090
3091        # Ideally we would instantiate the runtime extractor from highest most level (in this case the SimpleRetriever)
3092        # so that it can be shared by OffSetIncrement and RecordSelector. However, due to how we instantiate the
3093        # decoder with various decorators here, but not in create_record_selector, it is simpler to retain existing
3094        # behavior by having two separate extractors with identical behavior since they use the same extractor model.
3095        # When we have more time to investigate we can look into reusing the same component.
3096        extractor = (
3097            self._create_component_from_model(
3098                model=extractor_model, config=config, decoder=decoder_to_use
3099            )
3100            if extractor_model
3101            else None
3102        )
3103
3104        # Pydantic v1 Union type coercion can convert int to string depending on Union order.
3105        # If page_size is a string that represents an integer (not an interpolation), convert it back.
3106        page_size = model.page_size
3107        if isinstance(page_size, str) and page_size.isdigit():
3108            page_size = int(page_size)
3109
3110        return OffsetIncrement(
3111            page_size=page_size,
3112            config=config,
3113            decoder=decoder_to_use,
3114            extractor=extractor,
3115            inject_on_first_request=model.inject_on_first_request or False,
3116            parameters=model.parameters or {},
3117        )
3118
3119    def create_page_increment(
3120        self,
3121        model: PageIncrementModel,
3122        config: Config,
3123        decoder: Optional[Decoder] = None,
3124        extractor_model: Optional[Union[CustomRecordExtractorModel, DpathExtractorModel]] = None,
3125        **kwargs: Any,
3126    ) -> PageIncrement:
3127        # Like OffsetIncrement, we instantiate a separate extractor with identical behavior to the
3128        # RecordSelector's so the strategy can count the raw records in the response. This ensures
3129        # pagination is driven by the API's page size, not the post-filter record count.
3130        extractor = (
3131            self._create_component_from_model(model=extractor_model, config=config, decoder=decoder)
3132            if extractor_model
3133            else None
3134        )
3135
3136        # Pydantic v1 Union type coercion can convert int to string depending on Union order.
3137        # If page_size is a string that represents an integer (not an interpolation), convert it back.
3138        page_size = model.page_size
3139        if isinstance(page_size, str) and page_size.isdigit():
3140            page_size = int(page_size)
3141
3142        return PageIncrement(
3143            page_size=page_size,
3144            config=config,
3145            start_from_page=model.start_from_page or 0,
3146            inject_on_first_request=model.inject_on_first_request or False,
3147            extractor=extractor,
3148            parameters=model.parameters or {},
3149        )
3150
3151    def create_parent_stream_config(
3152        self, model: ParentStreamConfigModel, config: Config, *, stream_name: str, **kwargs: Any
3153    ) -> ParentStreamConfig:
3154        declarative_stream = self._create_component_from_model(
3155            model.stream,
3156            config=config,
3157            is_parent=True,
3158            **kwargs,
3159        )
3160        request_option = (
3161            self._create_component_from_model(model.request_option, config=config)
3162            if model.request_option
3163            else None
3164        )
3165
3166        if model.lazy_read_pointer and any("*" in pointer for pointer in model.lazy_read_pointer):
3167            raise ValueError(
3168                "The '*' wildcard in 'lazy_read_pointer' is not supported — only direct paths are allowed."
3169            )
3170
3171        model_lazy_read_pointer: List[Union[InterpolatedString, str]] = (
3172            [x for x in model.lazy_read_pointer] if model.lazy_read_pointer else []
3173        )
3174
3175        return ParentStreamConfig(
3176            parent_key=model.parent_key,
3177            request_option=request_option,
3178            stream=declarative_stream,
3179            partition_field=model.partition_field,
3180            config=config,
3181            incremental_dependency=model.incremental_dependency or False,
3182            parameters=model.parameters or {},
3183            extra_fields=model.extra_fields,
3184            lazy_read_pointer=model_lazy_read_pointer,
3185        )
3186
3187    def create_properties_from_endpoint(
3188        self, model: PropertiesFromEndpointModel, config: Config, **kwargs: Any
3189    ) -> PropertiesFromEndpoint:
3190        retriever = self._create_component_from_model(
3191            model=model.retriever,
3192            config=config,
3193            name="dynamic_properties",
3194            primary_key=None,
3195            stream_slicer=None,
3196            transformations=[],
3197            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
3198        )
3199        return PropertiesFromEndpoint(
3200            property_field_path=model.property_field_path,
3201            retriever=retriever,
3202            config=config,
3203            parameters=model.parameters or {},
3204        )
3205
3206    def create_property_chunking(
3207        self, model: PropertyChunkingModel, config: Config, **kwargs: Any
3208    ) -> PropertyChunking:
3209        record_merge_strategy = (
3210            self._create_component_from_model(
3211                model=model.record_merge_strategy, config=config, **kwargs
3212            )
3213            if model.record_merge_strategy
3214            else None
3215        )
3216
3217        property_limit_type: PropertyLimitType
3218        match model.property_limit_type:
3219            case PropertyLimitTypeModel.property_count:
3220                property_limit_type = PropertyLimitType.property_count
3221            case PropertyLimitTypeModel.characters:
3222                property_limit_type = PropertyLimitType.characters
3223            case _:
3224                raise ValueError(f"Invalid PropertyLimitType {property_limit_type}")
3225
3226        return PropertyChunking(
3227            property_limit_type=property_limit_type,
3228            property_limit=model.property_limit,
3229            record_merge_strategy=record_merge_strategy,
3230            config=config,
3231            parameters=model.parameters or {},
3232        )
3233
3234    def create_query_properties(
3235        self, model: QueryPropertiesModel, config: Config, *, stream_name: str, **kwargs: Any
3236    ) -> QueryProperties:
3237        if isinstance(model.property_list, list):
3238            property_list = model.property_list
3239        else:
3240            property_list = self._create_component_from_model(
3241                model=model.property_list, config=config, **kwargs
3242            )
3243
3244        property_chunking = (
3245            self._create_component_from_model(
3246                model=model.property_chunking, config=config, **kwargs
3247            )
3248            if model.property_chunking
3249            else None
3250        )
3251
3252        property_selector = (
3253            self._create_component_from_model(
3254                model=model.property_selector, config=config, stream_name=stream_name, **kwargs
3255            )
3256            if model.property_selector
3257            else None
3258        )
3259
3260        return QueryProperties(
3261            property_list=property_list,
3262            always_include_properties=model.always_include_properties,
3263            property_chunking=property_chunking,
3264            property_selector=property_selector,
3265            config=config,
3266            parameters=model.parameters or {},
3267        )
3268
3269    def create_json_schema_property_selector(
3270        self,
3271        model: JsonSchemaPropertySelectorModel,
3272        config: Config,
3273        *,
3274        stream_name: str,
3275        **kwargs: Any,
3276    ) -> JsonSchemaPropertySelector:
3277        configured_stream = self._stream_name_to_configured_stream.get(stream_name)
3278
3279        transformations = []
3280        if model.transformations:
3281            for transformation_model in model.transformations:
3282                transformations.append(
3283                    self._create_component_from_model(model=transformation_model, config=config)
3284                )
3285
3286        return JsonSchemaPropertySelector(
3287            configured_stream=configured_stream,
3288            properties_transformations=transformations,
3289            config=config,
3290            parameters=model.parameters or {},
3291        )
3292
3293    @staticmethod
3294    def create_record_filter(
3295        model: RecordFilterModel, config: Config, **kwargs: Any
3296    ) -> RecordFilter:
3297        return RecordFilter(
3298            condition=model.condition or "", config=config, parameters=model.parameters or {}
3299        )
3300
3301    @staticmethod
3302    def create_request_path(model: RequestPathModel, config: Config, **kwargs: Any) -> RequestPath:
3303        return RequestPath(parameters={})
3304
3305    @staticmethod
3306    def create_request_option(
3307        model: RequestOptionModel, config: Config, **kwargs: Any
3308    ) -> RequestOption:
3309        inject_into = RequestOptionType(model.inject_into.value)
3310        field_path: Optional[List[Union[InterpolatedString, str]]] = (
3311            [
3312                InterpolatedString.create(segment, parameters=kwargs.get("parameters", {}))
3313                for segment in model.field_path
3314            ]
3315            if model.field_path
3316            else None
3317        )
3318        field_name = (
3319            InterpolatedString.create(model.field_name, parameters=kwargs.get("parameters", {}))
3320            if model.field_name
3321            else None
3322        )
3323        return RequestOption(
3324            field_name=field_name,
3325            field_path=field_path,
3326            inject_into=inject_into,
3327            parameters=kwargs.get("parameters", {}),
3328        )
3329
3330    def create_record_selector(
3331        self,
3332        model: RecordSelectorModel,
3333        config: Config,
3334        *,
3335        name: str,
3336        transformations: List[RecordTransformation] | None = None,
3337        decoder: Decoder | None = None,
3338        client_side_incremental_sync_cursor: Optional[Cursor] = None,
3339        is_client_side_incremental_sync: bool = False,
3340        file_uploader: Optional[DefaultFileUploader] = None,
3341        **kwargs: Any,
3342    ) -> RecordSelector:
3343        extractor = self._create_component_from_model(
3344            model=model.extractor, decoder=decoder, config=config
3345        )
3346        record_filter = (
3347            self._create_component_from_model(model.record_filter, config=config)
3348            if model.record_filter
3349            else None
3350        )
3351
3352        # A client-side incremental stream transforms before filtering by default. That default belongs to the flag,
3353        # not to the component that ends up doing the cursor comparison: a data feed does it in the retriever and
3354        # receives no cursor here, but its `record_filter` condition must keep running after the transformations.
3355        default_transform_before_filtering = bool(
3356            client_side_incremental_sync_cursor or is_client_side_incremental_sync
3357        )
3358        transform_before_filtering = (
3359            default_transform_before_filtering
3360            if model.transform_before_filtering is None
3361            else model.transform_before_filtering
3362        )
3363        if client_side_incremental_sync_cursor:
3364            record_filter = ClientSideIncrementalRecordFilterDecorator(
3365                config=config,
3366                parameters=model.parameters,
3367                condition=model.record_filter.condition
3368                if (model.record_filter and hasattr(model.record_filter, "condition"))
3369                else None,
3370                cursor=client_side_incremental_sync_cursor,
3371            )
3372
3373        if model.schema_normalization is None:
3374            # default to no schema normalization if not set
3375            model.schema_normalization = SchemaNormalizationModel.None_
3376
3377        schema_normalization = (
3378            TypeTransformer(SCHEMA_TRANSFORMER_TYPE_MAPPING[model.schema_normalization])
3379            if isinstance(model.schema_normalization, SchemaNormalizationModel)
3380            else self._create_component_from_model(model.schema_normalization, config=config)  # type: ignore[arg-type] # custom normalization model expected here
3381        )
3382
3383        return RecordSelector(
3384            extractor=extractor,
3385            name=name,
3386            config=config,
3387            record_filter=record_filter,
3388            transformations=transformations or [],
3389            file_uploader=file_uploader,
3390            schema_normalization=schema_normalization,
3391            parameters=model.parameters or {},
3392            transform_before_filtering=transform_before_filtering,
3393        )
3394
3395    @staticmethod
3396    def create_remove_fields(
3397        model: RemoveFieldsModel, config: Config, **kwargs: Any
3398    ) -> RemoveFields:
3399        return RemoveFields(
3400            field_pointers=model.field_pointers, condition=model.condition or "", parameters={}
3401        )
3402
3403    def create_selective_authenticator(
3404        self, model: SelectiveAuthenticatorModel, config: Config, **kwargs: Any
3405    ) -> DeclarativeAuthenticator:
3406        authenticators = {
3407            name: self._create_component_from_model(model=auth, config=config)
3408            for name, auth in model.authenticators.items()
3409        }
3410        # SelectiveAuthenticator will return instance of DeclarativeAuthenticator or raise ValueError error
3411        return SelectiveAuthenticator(  # type: ignore[abstract]
3412            config=config,
3413            authenticators=authenticators,
3414            authenticator_selection_path=model.authenticator_selection_path,
3415            **kwargs,
3416        )
3417
3418    @staticmethod
3419    def create_legacy_session_token_authenticator(
3420        model: LegacySessionTokenAuthenticatorModel, config: Config, *, url_base: str, **kwargs: Any
3421    ) -> LegacySessionTokenAuthenticator:
3422        return LegacySessionTokenAuthenticator(
3423            api_url=url_base,
3424            header=model.header,
3425            login_url=model.login_url,
3426            password=model.password or "",
3427            session_token=model.session_token or "",
3428            session_token_response_key=model.session_token_response_key or "",
3429            username=model.username or "",
3430            validate_session_url=model.validate_session_url,
3431            config=config,
3432            parameters=model.parameters or {},
3433        )
3434
3435    def create_simple_retriever(
3436        self,
3437        model: SimpleRetrieverModel,
3438        config: Config,
3439        *,
3440        name: str,
3441        primary_key: Optional[Union[str, List[str], List[List[str]]]],
3442        request_options_provider: Optional[RequestOptionsProvider] = None,
3443        cursor: Optional[Cursor] = None,
3444        has_stop_condition_cursor: bool = False,
3445        is_client_side_incremental_sync: bool = False,
3446        transformations: List[RecordTransformation],
3447        file_uploader: Optional[DefaultFileUploader] = None,
3448        incremental_sync: Optional[
3449            Union[IncrementingCountCursorModel, DatetimeBasedCursorModel]
3450        ] = None,
3451        use_cache: Optional[bool] = None,
3452        log_formatter: Optional[Callable[[Response], Any]] = None,
3453        partition_router: Optional[PartitionRouter] = None,
3454        **kwargs: Any,
3455    ) -> SimpleRetriever:
3456        def _get_url(req: Requester) -> str:
3457            """
3458            Closure to get the URL from the requester. This is used to get the URL in the case of a lazy retriever.
3459            This is needed because the URL is not set until the requester is created.
3460            """
3461
3462            _url: str = (
3463                model.requester.url
3464                if hasattr(model.requester, "url") and model.requester.url is not None
3465                else req.get_url(stream_state=None, stream_slice=None, next_page_token=None)
3466            )
3467            _url_base: str = (
3468                model.requester.url_base
3469                if hasattr(model.requester, "url_base") and model.requester.url_base is not None
3470                else req.get_url_base(stream_state=None, stream_slice=None, next_page_token=None)
3471            )
3472
3473            return _url or _url_base
3474
3475        if cursor is None:
3476            cursor = FinalStateCursor(name, None, self._message_repository)
3477
3478        # A data feed drops the records the cursor considers already synced in the retriever, which sits downstream of
3479        # the paginator. Letting the record selector drop them as well would be redundant and would hide them from the
3480        # pagination stop condition, so a data feed never delegates that filtering to the record selector, whether
3481        # `is_client_side_incremental` is set or not. The `condition` from `record_filter` is intentionally left out of
3482        # the post-pagination filter and stays in the record selector, which preserves the existing behaviour: the
3483        # selector runs inside the page loop, so the records the condition rejects never reach the paginator's
3484        # accounting. Moving it downstream would start counting them.
3485        post_pagination_filter = (
3486            ClientSideIncrementalRecordFilterDecorator(
3487                config=config,
3488                parameters=model.parameters or {},
3489                condition=None,
3490                cursor=cursor,
3491            )
3492            if has_stop_condition_cursor
3493            else None
3494        )
3495        client_side_incremental_cursor = (
3496            cursor if is_client_side_incremental_sync and not post_pagination_filter else None
3497        )
3498        if post_pagination_filter and is_client_side_incremental_sync:
3499            LOGGER.warning(
3500                f"Stream {name}: `is_client_side_incremental` adds no record filtering when `is_data_feed` is set, "
3501                "as a data feed already filters on the cursor value. It still makes the record selector apply the "
3502                "transformations before the `record_filter` condition."
3503            )
3504
3505        decoder = (
3506            self._create_component_from_model(model=model.decoder, config=config)
3507            if model.decoder
3508            else JsonDecoder(parameters={})
3509        )
3510        record_selector = self._create_component_from_model(
3511            model=model.record_selector,
3512            name=name,
3513            config=config,
3514            decoder=decoder,
3515            transformations=transformations,
3516            client_side_incremental_sync_cursor=client_side_incremental_cursor,
3517            is_client_side_incremental_sync=is_client_side_incremental_sync,
3518            file_uploader=file_uploader,
3519        )
3520
3521        query_properties: Optional[QueryProperties] = None
3522        query_properties_key: Optional[str] = None
3523        self._ensure_query_properties_to_model(model.requester)
3524        if self._has_query_properties_in_request_parameters(model.requester):
3525            # It is better to be explicit about an error if PropertiesFromEndpoint is defined in multiple
3526            # places instead of default to request_parameters which isn't clearly documented
3527            if (
3528                hasattr(model.requester, "fetch_properties_from_endpoint")
3529                and model.requester.fetch_properties_from_endpoint
3530            ):
3531                raise ValueError(
3532                    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"
3533                )
3534
3535            query_properties_definitions = []
3536            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()
3537                if isinstance(request_parameter, QueryPropertiesModel):
3538                    query_properties_key = key
3539                    query_properties_definitions.append(request_parameter)
3540
3541            if len(query_properties_definitions) > 1:
3542                raise ValueError(
3543                    f"request_parameters only supports defining one QueryProperties field, but found {len(query_properties_definitions)} usages"
3544                )
3545
3546            if len(query_properties_definitions) == 1:
3547                query_properties = self._create_component_from_model(
3548                    model=query_properties_definitions[0], stream_name=name, config=config
3549                )
3550
3551            # Removes QueryProperties components from the interpolated mappings because it has been designed
3552            # to be used by the SimpleRetriever and will be resolved from the provider from the slice directly
3553            # instead of through jinja interpolation
3554            if hasattr(model.requester, "request_parameters") and isinstance(
3555                model.requester.request_parameters, Mapping
3556            ):
3557                model.requester.request_parameters = self._remove_query_properties(
3558                    model.requester.request_parameters
3559                )
3560        elif (
3561            hasattr(model.requester, "fetch_properties_from_endpoint")
3562            and model.requester.fetch_properties_from_endpoint
3563        ):
3564            # todo: Deprecate this condition once dependent connectors migrate to query_properties
3565            query_properties_definition = QueryPropertiesModel(
3566                type="QueryProperties",
3567                property_list=model.requester.fetch_properties_from_endpoint,
3568                always_include_properties=None,
3569                property_chunking=None,
3570            )  # type: ignore # $parameters has a default value
3571
3572            query_properties = self.create_query_properties(
3573                model=query_properties_definition,
3574                stream_name=name,
3575                config=config,
3576            )
3577        elif hasattr(model.requester, "query_properties") and model.requester.query_properties:
3578            query_properties = self.create_query_properties(
3579                model=model.requester.query_properties,
3580                stream_name=name,
3581                config=config,
3582            )
3583
3584        requester = self._create_component_from_model(
3585            model=model.requester,
3586            decoder=decoder,
3587            name=name,
3588            query_properties_key=query_properties_key,
3589            use_cache=use_cache,
3590            config=config,
3591        )
3592
3593        if not request_options_provider:
3594            request_options_provider = DefaultRequestOptionsProvider(parameters={})
3595        if isinstance(request_options_provider, DefaultRequestOptionsProvider) and isinstance(
3596            partition_router, PartitionRouter
3597        ):
3598            request_options_provider = partition_router
3599
3600        paginator = (
3601            self._create_component_from_model(
3602                model=model.paginator,
3603                config=config,
3604                url_base=_get_url(requester),
3605                extractor_model=model.record_selector.extractor,
3606                decoder=decoder,
3607                cursor_used_for_stop_condition=cursor if has_stop_condition_cursor else None,
3608            )
3609            if model.paginator
3610            else NoPagination(parameters={})
3611        )
3612
3613        ignore_stream_slicer_parameters_on_paginated_requests = (
3614            model.ignore_stream_slicer_parameters_on_paginated_requests or False
3615        )
3616
3617        if (
3618            model.partition_router
3619            and isinstance(model.partition_router, SubstreamPartitionRouterModel)
3620            and not bool(self._connector_state_manager.get_stream_state(name, None))
3621            and any(
3622                parent_stream_config.lazy_read_pointer
3623                for parent_stream_config in model.partition_router.parent_stream_configs
3624            )
3625        ):
3626            if incremental_sync:
3627                if incremental_sync.type != "DatetimeBasedCursor":
3628                    raise ValueError(
3629                        f"LazySimpleRetriever only supports DatetimeBasedCursor. Found: {incremental_sync.type}."
3630                    )
3631
3632                elif incremental_sync.step or incremental_sync.cursor_granularity:
3633                    raise ValueError(
3634                        f"Found more that one slice per parent. LazySimpleRetriever only supports single slice read for stream - {name}."
3635                    )
3636
3637            if model.decoder and model.decoder.type != "JsonDecoder":
3638                raise ValueError(
3639                    f"LazySimpleRetriever only supports JsonDecoder. Found: {model.decoder.type}."
3640                )
3641
3642            return LazySimpleRetriever(
3643                name=name,
3644                paginator=paginator,
3645                primary_key=primary_key,
3646                requester=requester,
3647                record_selector=record_selector,
3648                stream_slicer=_NO_STREAM_SLICING,
3649                request_option_provider=request_options_provider,
3650                config=config,
3651                ignore_stream_slicer_parameters_on_paginated_requests=ignore_stream_slicer_parameters_on_paginated_requests,
3652                post_pagination_filter=post_pagination_filter,
3653                parameters=model.parameters or {},
3654            )
3655
3656        if (
3657            model.record_selector.record_filter
3658            and model.pagination_reset
3659            and model.pagination_reset.limits
3660        ):
3661            raise ValueError("PaginationResetLimits are not supported while having record filter.")
3662
3663        return SimpleRetriever(
3664            name=name,
3665            paginator=paginator,
3666            primary_key=primary_key,
3667            requester=requester,
3668            record_selector=record_selector,
3669            stream_slicer=_NO_STREAM_SLICING,
3670            request_option_provider=request_options_provider,
3671            config=config,
3672            ignore_stream_slicer_parameters_on_paginated_requests=ignore_stream_slicer_parameters_on_paginated_requests,
3673            additional_query_properties=query_properties,
3674            log_formatter=self._get_log_formatter(log_formatter, name),
3675            pagination_tracker_factory=self._create_pagination_tracker_factory(
3676                model.pagination_reset, cursor
3677            ),
3678            post_pagination_filter=post_pagination_filter,
3679            parameters=model.parameters or {},
3680        )
3681
3682    def _create_pagination_tracker_factory(
3683        self, model: Optional[PaginationResetModel], cursor: Cursor
3684    ) -> Callable[[], PaginationTracker]:
3685        if model is None:
3686            return lambda: PaginationTracker()
3687
3688        # Until we figure out a way to use any cursor for PaginationTracker, we will have to have this cursor selector logic
3689        cursor_factory: Callable[[], Optional[ConcurrentCursor]] = lambda: None
3690        if model.action == PaginationResetActionModel.RESET:
3691            # in that case, we will let cursor_factory to return None even if the stream has a cursor
3692            pass
3693        elif model.action == PaginationResetActionModel.SPLIT_USING_CURSOR:
3694            if isinstance(cursor, ConcurrentCursor):
3695                cursor_factory = lambda: cursor.copy_without_state()  # type: ignore  # the if condition validates that it is a ConcurrentCursor
3696            elif isinstance(cursor, ConcurrentPerPartitionCursor):
3697                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
3698                    {}, datetime.timedelta(0)
3699                )
3700            elif not isinstance(cursor, FinalStateCursor):
3701                LOGGER.warning(
3702                    "Unknown cursor for PaginationTracker. Pagination resets might not work properly"
3703                )
3704        else:
3705            raise ValueError(f"Unknown PaginationReset action: {model.action}")
3706
3707        limit = model.limits.number_of_records if model and model.limits else None
3708        return lambda: PaginationTracker(cursor_factory(), limit)
3709
3710    def _get_log_formatter(
3711        self, log_formatter: Callable[[Response], Any] | None, name: str
3712    ) -> Callable[[Response], Any] | None:
3713        if self._should_limit_slices_fetched():
3714            return (
3715                (
3716                    lambda response: format_http_message(
3717                        response,
3718                        f"Stream '{name}' request",
3719                        f"Request performed in order to extract records for stream '{name}'",
3720                        name,
3721                    )
3722                )
3723                if not log_formatter
3724                else log_formatter
3725            )
3726        return None
3727
3728    def _should_limit_slices_fetched(self) -> bool:
3729        """
3730        Returns True if the number of slices fetched should be limited, False otherwise.
3731        This is used to limit the number of slices fetched during tests.
3732        """
3733        return bool(self._limit_slices_fetched or self._emit_connector_builder_messages)
3734
3735    @staticmethod
3736    def _has_query_properties_in_request_parameters(
3737        requester: Union[HttpRequesterModel, CustomRequesterModel],
3738    ) -> bool:
3739        if not hasattr(requester, "request_parameters"):
3740            return False
3741        request_parameters = requester.request_parameters
3742        if request_parameters and isinstance(request_parameters, Mapping):
3743            for request_parameter in request_parameters.values():
3744                if isinstance(request_parameter, QueryPropertiesModel):
3745                    return True
3746        return False
3747
3748    @staticmethod
3749    def _remove_query_properties(
3750        request_parameters: Mapping[str, Union[str, QueryPropertiesModel]],
3751    ) -> Mapping[str, str]:
3752        return {
3753            parameter_field: request_parameter
3754            for parameter_field, request_parameter in request_parameters.items()
3755            if not isinstance(request_parameter, QueryPropertiesModel)
3756        }
3757
3758    def create_state_delegating_stream(
3759        self,
3760        model: StateDelegatingStreamModel,
3761        config: Config,
3762        **kwargs: Any,
3763    ) -> DefaultStream:
3764        if (
3765            model.full_refresh_stream.name != model.name
3766            or model.name != model.incremental_stream.name
3767        ):
3768            raise ValueError(
3769                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}."
3770            )
3771
3772        # Resolve api_retention_period with config context (supports Jinja2 interpolation)
3773        resolved_retention_period: Optional[str] = None
3774        if model.api_retention_period:
3775            interpolated_retention = InterpolatedString.create(
3776                model.api_retention_period, parameters=model.parameters or {}
3777            )
3778            resolved_value = interpolated_retention.eval(config=config)
3779            if resolved_value:
3780                resolved_retention_period = str(resolved_value)
3781
3782        if resolved_retention_period:
3783            for stream_model in (model.full_refresh_stream, model.incremental_stream):
3784                if isinstance(stream_model.incremental_sync, IncrementingCountCursorModel):
3785                    raise ValueError(
3786                        f"Stream '{model.name}' uses IncrementingCountCursor which is not supported "
3787                        f"with api_retention_period. IncrementingCountCursor does not use datetime-based "
3788                        f"cursors, so cursor age validation cannot be performed."
3789                    )
3790
3791        stream_state = self._connector_state_manager.get_stream_state(model.name, None)
3792
3793        if not stream_state:
3794            return self._create_component_from_model(  # type: ignore[no-any-return]
3795                model.full_refresh_stream, config=config, **kwargs
3796            )
3797
3798        incremental_stream: DefaultStream = self._create_component_from_model(
3799            model.incremental_stream, config=config, **kwargs
3800        )  # type: ignore[assignment]
3801
3802        # Only run cursor age validation for streams that are in the configured
3803        # catalog (or when no catalog was provided, e.g. during discover / connector
3804        # builder).  Streams not selected by the user but instantiated as parent-stream
3805        # dependencies must not go through this path because it emits state messages
3806        # that the destination does not know about, causing "Stream not found" crashes.
3807        stream_is_in_catalog = (
3808            not self._stream_name_to_configured_stream  # no catalog → validate by default
3809            or model.name in self._stream_name_to_configured_stream
3810        )
3811        if resolved_retention_period and stream_is_in_catalog:
3812            full_refresh_stream: DefaultStream = self._create_component_from_model(
3813                model.full_refresh_stream, config=config, **kwargs
3814            )  # type: ignore[assignment]
3815            if self._is_cursor_older_than_retention_period(
3816                stream_state,
3817                full_refresh_stream.cursor,
3818                incremental_stream.cursor,
3819                resolved_retention_period,
3820                model.name,
3821            ):
3822                # Clear state BEFORE constructing the full_refresh_stream so that
3823                # its cursor starts from start_date instead of the stale cursor.
3824                self._connector_state_manager.update_state_for_stream(model.name, None, {})
3825                state_message = self._connector_state_manager.create_state_message(model.name, None)
3826                self._message_repository.emit_message(state_message)
3827                return self._create_component_from_model(  # type: ignore[no-any-return]
3828                    model.full_refresh_stream, config=config, **kwargs
3829                )
3830
3831        return incremental_stream
3832
3833    @staticmethod
3834    def _is_cursor_older_than_retention_period(
3835        stream_state: Mapping[str, Any],
3836        full_refresh_cursor: Cursor,
3837        incremental_cursor: Cursor,
3838        api_retention_period: str,
3839        stream_name: str,
3840    ) -> bool:
3841        """Check if the cursor value in the state is older than the API's retention period.
3842
3843        Checks cursors in sequence: full refresh cursor first, then incremental cursor.
3844        FinalStateCursor returns now() for completed full refresh state (NO_CURSOR_STATE_KEY),
3845        which is always within retention, so we use incremental. For other states, it returns
3846        None and we fall back to checking the incremental cursor.
3847
3848        Returns True if the cursor is older than the retention period (should use full refresh).
3849        Returns False if the cursor is within the retention period (safe to use incremental).
3850        """
3851        retention_duration = parse_duration(api_retention_period)
3852        retention_cutoff = datetime.datetime.now(datetime.timezone.utc) - retention_duration
3853
3854        # Check full refresh cursor first
3855        cursor_datetime = full_refresh_cursor.get_cursor_datetime_from_state(stream_state)
3856
3857        # If full refresh cursor returns None, check incremental cursor
3858        if cursor_datetime is None:
3859            cursor_datetime = incremental_cursor.get_cursor_datetime_from_state(stream_state)
3860
3861        if cursor_datetime is None:
3862            # Neither cursor could parse the state - fall back to full refresh to be safe
3863            return True
3864
3865        if cursor_datetime < retention_cutoff:
3866            logging.warning(
3867                f"Stream '{stream_name}' has a cursor value older than "
3868                f"the API's retention period of {api_retention_period} "
3869                f"(cutoff: {retention_cutoff.isoformat()}). "
3870                f"Falling back to full refresh to avoid data loss."
3871            )
3872            return True
3873
3874        return False
3875
3876    def _get_state_delegating_stream_model(
3877        self,
3878        model: StateDelegatingStreamModel,
3879        parent_state: Optional[Mapping[str, Any]] = None,
3880    ) -> DeclarativeStreamModel:
3881        """Return the appropriate underlying stream model based on state."""
3882        return (
3883            model.incremental_stream
3884            if self._connector_state_manager.get_stream_state(model.name, None) or parent_state
3885            else model.full_refresh_stream
3886        )
3887
3888    _OPTIONAL_ASYNC_STATUS_FIELDS = {"skipped"}
3889
3890    def _create_async_job_status_mapping(
3891        self, model: AsyncJobStatusMapModel, config: Config, **kwargs: Any
3892    ) -> Mapping[str, AsyncJobStatus]:
3893        api_status_to_cdk_status = {}
3894        for cdk_status, api_statuses in model.dict().items():
3895            if cdk_status == "type":
3896                # This is an element of the dict because of the typing of the CDK but it is not a CDK status
3897                continue
3898
3899            if api_statuses is None:
3900                if cdk_status in self._OPTIONAL_ASYNC_STATUS_FIELDS:
3901                    continue
3902                raise ValueError(
3903                    f"Required CDK status '{cdk_status}' has no API statuses mapped. "
3904                    f"Please provide at least an empty list for required status fields."
3905                )
3906
3907            for status in api_statuses:
3908                if status in api_status_to_cdk_status:
3909                    raise ValueError(
3910                        f"API status {status} is already set for CDK status {cdk_status}. Please ensure API statuses are only provided once"
3911                    )
3912                api_status_to_cdk_status[status] = self._get_async_job_status(cdk_status)
3913        return api_status_to_cdk_status
3914
3915    def _get_async_job_status(self, status: str) -> AsyncJobStatus:
3916        match status:
3917            case "running":
3918                return AsyncJobStatus.RUNNING
3919            case "completed":
3920                return AsyncJobStatus.COMPLETED
3921            case "failed":
3922                return AsyncJobStatus.FAILED
3923            case "timeout":
3924                return AsyncJobStatus.TIMED_OUT
3925            case "skipped":
3926                return AsyncJobStatus.SKIPPED
3927            case _:
3928                raise ValueError(f"Unsupported CDK status {status}")
3929
3930    def create_async_retriever(
3931        self,
3932        model: AsyncRetrieverModel,
3933        config: Config,
3934        *,
3935        name: str,
3936        primary_key: Optional[
3937            Union[str, List[str], List[List[str]]]
3938        ],  # this seems to be needed to match create_simple_retriever
3939        stream_slicer: Optional[StreamSlicer],
3940        client_side_incremental_sync: Optional[Dict[str, Any]] = None,
3941        transformations: List[RecordTransformation],
3942        **kwargs: Any,
3943    ) -> AsyncRetriever:
3944        if model.download_target_requester and not model.download_target_extractor:
3945            raise ValueError(
3946                f"`download_target_extractor` required if using a `download_target_requester`"
3947            )
3948
3949        def _get_download_retriever(
3950            requester: Requester, extractor: RecordExtractor, _decoder: Decoder
3951        ) -> SimpleRetriever:
3952            # We create a record selector for the download retriever
3953            # with no schema normalization and no transformations, neither record filter
3954            # as all this occurs in the record_selector of the AsyncRetriever
3955            record_selector = RecordSelector(
3956                extractor=extractor,
3957                name=name,
3958                record_filter=None,
3959                transformations=[],
3960                schema_normalization=TypeTransformer(TransformConfig.NoTransform),
3961                config=config,
3962                parameters={},
3963            )
3964            paginator = (
3965                self._create_component_from_model(
3966                    model=model.download_paginator,
3967                    decoder=_decoder,
3968                    config=config,
3969                    url_base="",
3970                )
3971                if model.download_paginator
3972                else NoPagination(parameters={})
3973            )
3974
3975            return SimpleRetriever(
3976                requester=requester,
3977                record_selector=record_selector,
3978                primary_key=None,
3979                name=name,
3980                paginator=paginator,
3981                config=config,
3982                parameters={},
3983                log_formatter=self._get_log_formatter(None, name),
3984            )
3985
3986        def _get_job_timeout() -> datetime.timedelta:
3987            user_defined_timeout: Optional[int] = (
3988                int(
3989                    InterpolatedString.create(
3990                        str(model.polling_job_timeout),
3991                        parameters={},
3992                    ).eval(config)
3993                )
3994                if model.polling_job_timeout
3995                else None
3996            )
3997
3998            # check for user defined timeout during the test read or 15 minutes
3999            test_read_timeout = datetime.timedelta(minutes=user_defined_timeout or 15)
4000            # default value for non-connector builder is 60 minutes.
4001            default_sync_timeout = datetime.timedelta(minutes=user_defined_timeout or 60)
4002
4003            return (
4004                test_read_timeout if self._emit_connector_builder_messages else default_sync_timeout
4005            )
4006
4007        decoder = (
4008            self._create_component_from_model(model=model.decoder, config=config)
4009            if model.decoder
4010            else JsonDecoder(parameters={})
4011        )
4012        record_selector = self._create_component_from_model(
4013            model=model.record_selector,
4014            config=config,
4015            decoder=decoder,
4016            name=name,
4017            transformations=transformations,
4018            client_side_incremental_sync=client_side_incremental_sync,
4019        )
4020
4021        stream_slicer = stream_slicer or SinglePartitionRouter(parameters={})
4022        if self._should_limit_slices_fetched():
4023            stream_slicer = cast(
4024                StreamSlicer,
4025                StreamSlicerTestReadDecorator(
4026                    wrapped_slicer=stream_slicer,
4027                    maximum_number_of_slices=self._limit_slices_fetched or 5,
4028                ),
4029            )
4030
4031        creation_requester = self._create_component_from_model(
4032            model=model.creation_requester,
4033            decoder=decoder,
4034            config=config,
4035            name=f"job creation - {name}",
4036        )
4037        polling_requester = self._create_component_from_model(
4038            model=model.polling_requester,
4039            decoder=decoder,
4040            config=config,
4041            name=f"job polling - {name}",
4042        )
4043        job_download_components_name = f"job download - {name}"
4044        download_decoder = (
4045            self._create_component_from_model(model=model.download_decoder, config=config)
4046            if model.download_decoder
4047            else JsonDecoder(parameters={})
4048        )
4049        download_extractor = (
4050            self._create_component_from_model(
4051                model=model.download_extractor,
4052                config=config,
4053                decoder=download_decoder,
4054                parameters=model.parameters,
4055            )
4056            if model.download_extractor
4057            else DpathExtractor(
4058                [],
4059                config=config,
4060                decoder=download_decoder,
4061                parameters=model.parameters or {},
4062            )
4063        )
4064        download_requester = self._create_component_from_model(
4065            model=model.download_requester,
4066            decoder=download_decoder,
4067            config=config,
4068            name=job_download_components_name,
4069        )
4070        download_retriever = _get_download_retriever(
4071            download_requester, download_extractor, download_decoder
4072        )
4073        abort_requester = (
4074            self._create_component_from_model(
4075                model=model.abort_requester,
4076                decoder=decoder,
4077                config=config,
4078                name=f"job abort - {name}",
4079            )
4080            if model.abort_requester
4081            else None
4082        )
4083        delete_requester = (
4084            self._create_component_from_model(
4085                model=model.delete_requester,
4086                decoder=decoder,
4087                config=config,
4088                name=f"job delete - {name}",
4089            )
4090            if model.delete_requester
4091            else None
4092        )
4093        download_target_requester = (
4094            self._create_component_from_model(
4095                model=model.download_target_requester,
4096                decoder=decoder,
4097                config=config,
4098                name=f"job extract_url - {name}",
4099            )
4100            if model.download_target_requester
4101            else None
4102        )
4103        status_extractor = self._create_component_from_model(
4104            model=model.status_extractor, decoder=decoder, config=config, name=name
4105        )
4106        download_target_extractor = (
4107            self._create_component_from_model(
4108                model=model.download_target_extractor,
4109                decoder=decoder,
4110                config=config,
4111                name=name,
4112            )
4113            if model.download_target_extractor
4114            else None
4115        )
4116
4117        job_repository: AsyncJobRepository = AsyncHttpJobRepository(
4118            creation_requester=creation_requester,
4119            polling_requester=polling_requester,
4120            download_retriever=download_retriever,
4121            download_target_requester=download_target_requester,
4122            abort_requester=abort_requester,
4123            delete_requester=delete_requester,
4124            status_extractor=status_extractor,
4125            status_mapping=self._create_async_job_status_mapping(model.status_mapping, config),
4126            download_target_extractor=download_target_extractor,
4127            job_timeout=_get_job_timeout(),
4128        )
4129
4130        failed_retry_wait_time_in_seconds: Optional[int] = (
4131            int(
4132                InterpolatedString.create(
4133                    str(model.failed_retry_wait_time_in_seconds),
4134                    parameters={},
4135                ).eval(config)
4136            )
4137            if model.failed_retry_wait_time_in_seconds
4138            else None
4139        )
4140
4141        async_job_partition_router = AsyncJobPartitionRouter(
4142            job_orchestrator_factory=lambda stream_slices: AsyncJobOrchestrator(
4143                job_repository,
4144                stream_slices,
4145                self._job_tracker,
4146                self._message_repository,
4147                # FIXME work would need to be done here in order to detect if a stream as a parent stream that is bulk
4148                has_bulk_parent=False,
4149                # set the `job_max_retry` to 1 for the `Connector Builder`` use-case.
4150                # `None` == default retry is set to 3 attempts, under the hood.
4151                job_max_retry=1 if self._emit_connector_builder_messages else None,
4152                failed_retry_wait_time_in_seconds=failed_retry_wait_time_in_seconds,
4153            ),
4154            stream_slicer=stream_slicer,
4155            config=config,
4156            parameters=model.parameters or {},
4157        )
4158
4159        return AsyncRetriever(
4160            record_selector=record_selector,
4161            stream_slicer=async_job_partition_router,
4162            config=config,
4163            parameters=model.parameters or {},
4164        )
4165
4166    def create_spec(self, model: SpecModel, config: Config, **kwargs: Any) -> Spec:
4167        config_migrations = [
4168            self._create_component_from_model(migration, config)
4169            for migration in (
4170                model.config_normalization_rules.config_migrations
4171                if (
4172                    model.config_normalization_rules
4173                    and model.config_normalization_rules.config_migrations
4174                )
4175                else []
4176            )
4177        ]
4178        config_transformations = [
4179            self._create_component_from_model(transformation, config)
4180            for transformation in (
4181                model.config_normalization_rules.transformations
4182                if (
4183                    model.config_normalization_rules
4184                    and model.config_normalization_rules.transformations
4185                )
4186                else []
4187            )
4188        ]
4189        config_validations = [
4190            self._create_component_from_model(validation, config)
4191            for validation in (
4192                model.config_normalization_rules.validations
4193                if (
4194                    model.config_normalization_rules
4195                    and model.config_normalization_rules.validations
4196                )
4197                else []
4198            )
4199        ]
4200
4201        return Spec(
4202            connection_specification=model.connection_specification,
4203            documentation_url=model.documentation_url,
4204            advanced_auth=model.advanced_auth,
4205            parameters={},
4206            config_migrations=config_migrations,
4207            config_transformations=config_transformations,
4208            config_validations=config_validations,
4209        )
4210
4211    def create_substream_partition_router(
4212        self,
4213        model: SubstreamPartitionRouterModel,
4214        config: Config,
4215        *,
4216        stream_name: str,
4217        **kwargs: Any,
4218    ) -> SubstreamPartitionRouter:
4219        parent_stream_configs = []
4220        if model.parent_stream_configs:
4221            parent_stream_configs.extend(
4222                [
4223                    self.create_parent_stream_config_with_substream_wrapper(
4224                        model=parent_stream_config, config=config, stream_name=stream_name, **kwargs
4225                    )
4226                    for parent_stream_config in model.parent_stream_configs
4227                ]
4228            )
4229
4230        return SubstreamPartitionRouter(
4231            parent_stream_configs=parent_stream_configs,
4232            parameters=model.parameters or {},
4233            config=config,
4234        )
4235
4236    def create_parent_stream_config_with_substream_wrapper(
4237        self, model: ParentStreamConfigModel, config: Config, *, stream_name: str, **kwargs: Any
4238    ) -> Any:
4239        child_state = self._connector_state_manager.get_stream_state(stream_name, None)
4240        if NO_CURSOR_STATE_KEY in child_state:
4241            # Full refresh streams checkpoint a `{NO_CURSOR_STATE_KEY: true}` sentinel. When such a
4242            # stream is later converted to incremental with an incremental_dependency parent,
4243            # `_instantiate_parent_stream_state_manager` would treat the sentinel's boolean as a legacy
4244            # cursor value and re-key it under the parent's cursor field, crashing cursor initialization.
4245            child_state = {
4246                key: value for key, value in child_state.items() if key != NO_CURSOR_STATE_KEY
4247            }
4248
4249        parent_state: Optional[Mapping[str, Any]] = (
4250            child_state if model.incremental_dependency and child_state else None
4251        )
4252        connector_state_manager = self._instantiate_parent_stream_state_manager(
4253            child_state, config, model, parent_state
4254        )
4255
4256        substream_factory = ModelToComponentFactory(
4257            custom_components_trusted=self._custom_components_trusted,
4258            connector_state_manager=connector_state_manager,
4259            limit_pages_fetched_per_slice=self._limit_pages_fetched_per_slice,
4260            limit_slices_fetched=self._limit_slices_fetched,
4261            emit_connector_builder_messages=self._emit_connector_builder_messages,
4262            disable_retries=self._disable_retries,
4263            disable_cache=self._disable_cache,
4264            message_repository=StateFilteringMessageRepository(
4265                LogAppenderMessageRepositoryDecorator(
4266                    {
4267                        "airbyte_cdk": {"stream": {"is_substream": True}},
4268                        "http": {"is_auxiliary": True},
4269                    },
4270                    self._message_repository,
4271                    self._evaluate_log_level(self._emit_connector_builder_messages),
4272                ),
4273            ),
4274            api_budget=self._api_budget,
4275            # Share the authenticator registry so parent and child streams draw from the
4276            # same token quota counters
4277            rate_limited_authenticators=self._rate_limited_authenticators,
4278        )
4279
4280        return substream_factory.create_parent_stream_config(
4281            model=model, config=config, stream_name=stream_name, **kwargs
4282        )
4283
4284    def _instantiate_parent_stream_state_manager(
4285        self,
4286        child_state: MutableMapping[str, Any],
4287        config: Config,
4288        model: ParentStreamConfigModel,
4289        parent_state: Optional[Mapping[str, Any]] = None,
4290    ) -> ConnectorStateManager:
4291        """
4292        With DefaultStream, the state needs to be provided during __init__ of the cursor as opposed to the
4293        `set_initial_state` flow that existed for the declarative cursors. This state is taken from
4294        self._connector_state_manager.get_stream_state (`self` being a newly created ModelToComponentFactory to account
4295        for the MessageRepository being different). So we need to pass a ConnectorStateManager to the
4296        ModelToComponentFactory that has the parent states. This method populates this if there is a child state and if
4297        incremental_dependency is set.
4298        """
4299        if model.incremental_dependency and child_state:
4300            parent_stream_name = model.stream.name or ""
4301            extracted_parent_state = ConcurrentPerPartitionCursor.get_parent_state(
4302                child_state, parent_stream_name
4303            )
4304
4305            if not extracted_parent_state:
4306                extracted_parent_state = ConcurrentPerPartitionCursor.get_global_state(
4307                    child_state, parent_stream_name
4308                )
4309
4310                if not extracted_parent_state and not isinstance(extracted_parent_state, dict):
4311                    cursor_values = child_state.values()
4312                    if cursor_values and len(cursor_values) == 1:
4313                        incremental_sync_model: Union[
4314                            DatetimeBasedCursorModel,
4315                            IncrementingCountCursorModel,
4316                        ] = (
4317                            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
4318                            if isinstance(model.stream, DeclarativeStreamModel)
4319                            else self._get_state_delegating_stream_model(
4320                                model.stream, parent_state=parent_state
4321                            ).incremental_sync
4322                        )
4323                        cursor_field = InterpolatedString.create(
4324                            incremental_sync_model.cursor_field,
4325                            parameters=incremental_sync_model.parameters or {},
4326                        ).eval(config)
4327                        extracted_parent_state = AirbyteStateMessage(
4328                            type=AirbyteStateType.STREAM,
4329                            stream=AirbyteStreamState(
4330                                stream_descriptor=StreamDescriptor(
4331                                    name=parent_stream_name, namespace=None
4332                                ),
4333                                stream_state=AirbyteStateBlob(
4334                                    {cursor_field: list(cursor_values)[0]}
4335                                ),
4336                            ),
4337                        )
4338            return ConnectorStateManager([extracted_parent_state] if extracted_parent_state else [])
4339
4340        return ConnectorStateManager([])
4341
4342    @staticmethod
4343    def create_wait_time_from_header(
4344        model: WaitTimeFromHeaderModel, config: Config, **kwargs: Any
4345    ) -> WaitTimeFromHeaderBackoffStrategy:
4346        return WaitTimeFromHeaderBackoffStrategy(
4347            header=model.header,
4348            parameters=model.parameters or {},
4349            config=config,
4350            regex=model.regex,
4351            max_waiting_time_in_seconds=model.max_waiting_time_in_seconds,
4352        )
4353
4354    @staticmethod
4355    def create_wait_until_time_from_header(
4356        model: WaitUntilTimeFromHeaderModel, config: Config, **kwargs: Any
4357    ) -> WaitUntilTimeFromHeaderBackoffStrategy:
4358        return WaitUntilTimeFromHeaderBackoffStrategy(
4359            header=model.header,
4360            parameters=model.parameters or {},
4361            config=config,
4362            min_wait=model.min_wait,
4363            regex=model.regex,
4364            max_waiting_time_in_seconds=model.max_waiting_time_in_seconds,
4365        )
4366
4367    def get_message_repository(self) -> MessageRepository:
4368        return self._message_repository
4369
4370    def _evaluate_log_level(self, emit_connector_builder_messages: bool) -> Level:
4371        return Level.DEBUG if emit_connector_builder_messages else Level.INFO
4372
4373    @staticmethod
4374    def create_components_mapping_definition(
4375        model: ComponentMappingDefinitionModel, config: Config, **kwargs: Any
4376    ) -> ComponentMappingDefinition:
4377        interpolated_value = InterpolatedString.create(
4378            model.value, parameters=model.parameters or {}
4379        )
4380        field_path = [
4381            InterpolatedString.create(path, parameters=model.parameters or {})
4382            for path in model.field_path
4383        ]
4384        return ComponentMappingDefinition(
4385            field_path=field_path,  # type: ignore[arg-type] # field_path can be str and InterpolatedString
4386            value=interpolated_value,
4387            value_type=ModelToComponentFactory._json_schema_type_name_to_type(model.value_type),
4388            create_or_update=model.create_or_update,
4389            condition=model.condition,
4390            parameters=model.parameters or {},
4391        )
4392
4393    def create_http_components_resolver(
4394        self, model: HttpComponentsResolverModel, config: Config, stream_name: Optional[str] = None
4395    ) -> Any:
4396        retriever = self._create_component_from_model(
4397            model=model.retriever,
4398            config=config,
4399            name=f"{stream_name if stream_name else '__http_components_resolver'}",
4400            primary_key=None,
4401            stream_slicer=self._build_stream_slicer_from_partition_router(model.retriever, config),
4402            transformations=[],
4403        )
4404
4405        components_mapping = []
4406        for component_mapping_definition_model in model.components_mapping:
4407            if component_mapping_definition_model.condition:
4408                raise ValueError("`condition` is only supported for     `ConfigComponentsResolver`")
4409            components_mapping.append(
4410                self._create_component_from_model(
4411                    model=component_mapping_definition_model,
4412                    value_type=ModelToComponentFactory._json_schema_type_name_to_type(
4413                        component_mapping_definition_model.value_type
4414                    ),
4415                    config=config,
4416                )
4417            )
4418
4419        return HttpComponentsResolver(
4420            retriever=retriever,
4421            stream_slicer=self._build_stream_slicer_from_partition_router(model.retriever, config),
4422            config=config,
4423            components_mapping=components_mapping,
4424            parameters=model.parameters or {},
4425        )
4426
4427    @staticmethod
4428    def create_stream_config(
4429        model: StreamConfigModel, config: Config, **kwargs: Any
4430    ) -> StreamConfig:
4431        model_configs_pointer: List[Union[InterpolatedString, str]] = (
4432            [x for x in model.configs_pointer] if model.configs_pointer else []
4433        )
4434
4435        return StreamConfig(
4436            configs_pointer=model_configs_pointer,
4437            default_values=model.default_values,
4438            parameters=model.parameters or {},
4439        )
4440
4441    def create_config_components_resolver(
4442        self,
4443        model: ConfigComponentsResolverModel,
4444        config: Config,
4445    ) -> Any:
4446        model_stream_configs = (
4447            model.stream_config if isinstance(model.stream_config, list) else [model.stream_config]
4448        )
4449
4450        stream_configs = [
4451            self._create_component_from_model(
4452                stream_config, config=config, parameters=model.parameters or {}
4453            )
4454            for stream_config in model_stream_configs
4455        ]
4456
4457        components_mapping = [
4458            self._create_component_from_model(
4459                model=components_mapping_definition_model,
4460                value_type=ModelToComponentFactory._json_schema_type_name_to_type(
4461                    components_mapping_definition_model.value_type
4462                ),
4463                config=config,
4464                parameters=model.parameters,
4465            )
4466            for components_mapping_definition_model in model.components_mapping
4467        ]
4468
4469        return ConfigComponentsResolver(
4470            stream_configs=stream_configs,
4471            config=config,
4472            components_mapping=components_mapping,
4473            parameters=model.parameters or {},
4474        )
4475
4476    def create_parametrized_components_resolver(
4477        self,
4478        model: ParametrizedComponentsResolverModel,
4479        config: Config,
4480    ) -> ParametrizedComponentsResolver:
4481        stream_parameters = StreamParametersDefinition(
4482            list_of_parameters_for_stream=model.stream_parameters.list_of_parameters_for_stream
4483        )
4484
4485        components_mapping = []
4486        for components_mapping_definition_model in model.components_mapping:
4487            if components_mapping_definition_model.condition:
4488                raise ValueError("`condition` is only supported for `ConfigComponentsResolver`")
4489            components_mapping.append(
4490                self._create_component_from_model(
4491                    model=components_mapping_definition_model,
4492                    value_type=ModelToComponentFactory._json_schema_type_name_to_type(
4493                        components_mapping_definition_model.value_type
4494                    ),
4495                    config=config,
4496                )
4497            )
4498        return ParametrizedComponentsResolver(
4499            stream_parameters=stream_parameters,
4500            config=config,
4501            components_mapping=components_mapping,
4502            parameters=model.parameters or {},
4503        )
4504
4505    _UNSUPPORTED_DECODER_ERROR = (
4506        "Specified decoder of {decoder_type} is not supported for pagination."
4507        "Please set as `JsonDecoder`, `XmlDecoder`, or a `CompositeRawDecoder` with an inner_parser of `JsonParser` or `GzipParser` instead."
4508        "If using `GzipParser`, please ensure that the lowest level inner_parser is a `JsonParser`."
4509    )
4510
4511    def _is_supported_decoder_for_pagination(self, decoder: Decoder) -> bool:
4512        if isinstance(decoder, (JsonDecoder, XmlDecoder)):
4513            return True
4514        elif isinstance(decoder, CompositeRawDecoder):
4515            return self._is_supported_parser_for_pagination(decoder.parser)
4516        else:
4517            return False
4518
4519    def _is_supported_parser_for_pagination(self, parser: Parser) -> bool:
4520        if isinstance(parser, JsonParser):
4521            return True
4522        elif isinstance(parser, GzipParser):
4523            return isinstance(parser.inner_parser, JsonParser)
4524        else:
4525            return False
4526
4527    def create_http_api_budget(
4528        self, model: HTTPAPIBudgetModel, config: Config, **kwargs: Any
4529    ) -> HttpAPIBudget:
4530        policies = [
4531            self._create_component_from_model(model=policy, config=config)
4532            for policy in model.policies
4533        ]
4534
4535        return HttpAPIBudget(
4536            policies=policies,
4537            ratelimit_reset_header=model.ratelimit_reset_header or "ratelimit-reset",
4538            ratelimit_remaining_header=model.ratelimit_remaining_header or "ratelimit-remaining",
4539            status_codes_for_ratelimit_hit=model.status_codes_for_ratelimit_hit or [429],
4540        )
4541
4542    def create_fixed_window_call_rate_policy(
4543        self, model: FixedWindowCallRatePolicyModel, config: Config, **kwargs: Any
4544    ) -> FixedWindowCallRatePolicy:
4545        matchers = [
4546            self._create_component_from_model(model=matcher, config=config)
4547            for matcher in model.matchers
4548        ]
4549
4550        # Set the initial reset timestamp to 10 days from now.
4551        # This value will be updated by the first request.
4552        return FixedWindowCallRatePolicy(
4553            next_reset_ts=datetime.datetime.now() + datetime.timedelta(days=10),
4554            period=parse_duration(model.period),
4555            call_limit=model.call_limit,
4556            matchers=matchers,
4557        )
4558
4559    def create_file_uploader(
4560        self, model: FileUploaderModel, config: Config, **kwargs: Any
4561    ) -> FileUploader:
4562        name = "File Uploader"
4563        requester = self._create_component_from_model(
4564            model=model.requester,
4565            config=config,
4566            name=name,
4567            **kwargs,
4568        )
4569        download_target_extractor = self._create_component_from_model(
4570            model=model.download_target_extractor,
4571            config=config,
4572            name=name,
4573            **kwargs,
4574        )
4575        emit_connector_builder_messages = self._emit_connector_builder_messages
4576        file_uploader = DefaultFileUploader(
4577            requester=requester,
4578            download_target_extractor=download_target_extractor,
4579            config=config,
4580            file_writer=NoopFileWriter()
4581            if emit_connector_builder_messages
4582            else LocalFileSystemFileWriter(),
4583            parameters=model.parameters or {},
4584            filename_extractor=model.filename_extractor if model.filename_extractor else None,
4585        )
4586
4587        return (
4588            ConnectorBuilderFileUploader(file_uploader)
4589            if emit_connector_builder_messages
4590            else file_uploader
4591        )
4592
4593    def create_moving_window_call_rate_policy(
4594        self, model: MovingWindowCallRatePolicyModel, config: Config, **kwargs: Any
4595    ) -> MovingWindowCallRatePolicy:
4596        rates = [
4597            self._create_component_from_model(model=rate, config=config) for rate in model.rates
4598        ]
4599        matchers = [
4600            self._create_component_from_model(model=matcher, config=config)
4601            for matcher in model.matchers
4602        ]
4603        return MovingWindowCallRatePolicy(
4604            rates=rates,
4605            matchers=matchers,
4606        )
4607
4608    def create_unlimited_call_rate_policy(
4609        self, model: UnlimitedCallRatePolicyModel, config: Config, **kwargs: Any
4610    ) -> UnlimitedCallRatePolicy:
4611        matchers = [
4612            self._create_component_from_model(model=matcher, config=config)
4613            for matcher in model.matchers
4614        ]
4615
4616        return UnlimitedCallRatePolicy(
4617            matchers=matchers,
4618        )
4619
4620    def create_rate(self, model: RateModel, config: Config, **kwargs: Any) -> Rate:
4621        interpolated_limit = InterpolatedString.create(str(model.limit), parameters={})
4622        return Rate(
4623            limit=int(interpolated_limit.eval(config=config)),
4624            interval=parse_duration(model.interval),
4625        )
4626
4627    def create_http_request_matcher(
4628        self, model: HttpRequestRegexMatcherModel, config: Config, **kwargs: Any
4629    ) -> HttpRequestRegexMatcher:
4630        weight = model.weight
4631        if weight is not None:
4632            if isinstance(weight, str):
4633                weight = int(InterpolatedString.create(weight, parameters={}).eval(config))
4634            else:
4635                weight = int(weight)
4636            if weight < 1:
4637                raise ValueError(f"weight must be >= 1, got {weight}")
4638        return HttpRequestRegexMatcher(
4639            method=model.method,
4640            url_base=model.url_base,
4641            url_path_pattern=model.url_path_pattern,
4642            params=model.params,
4643            headers=model.headers,
4644            weight=weight,
4645        )
4646
4647    def create_rate_limited_multiple_token_authenticator(
4648        self,
4649        model: RateLimitedMultipleTokenAuthenticatorModel,
4650        config: Config,
4651        **kwargs: Any,
4652    ) -> RateLimitedMultipleTokenAuthenticator:
4653        if isinstance(model.tokens, str):
4654            tokens_value = InterpolatedString.create(model.tokens, parameters={}).eval(config)
4655            delimiter = model.token_delimiter or ","
4656            tokens = [
4657                token.strip() for token in str(tokens_value).split(delimiter) if token.strip()
4658            ]
4659        else:
4660            tokens = [
4661                token_value
4662                for token in model.tokens
4663                if (
4664                    token_value := str(
4665                        InterpolatedString.create(token, parameters={}).eval(config)
4666                    ).strip()
4667                )
4668            ]
4669
4670        quota_specs = [
4671            {
4672                "name": quota_model.name,
4673                "remaining_path": quota_model.remaining_path,
4674                "reset_path": quota_model.reset_path,
4675                "limit_path": quota_model.limit_path,
4676                "remaining_header": quota_model.remaining_header,
4677                "reset_header": quota_model.reset_header,
4678                "limit_header": quota_model.limit_header,
4679                # Normalize the same way as the runtime TokenQuota below, so an omitted field
4680                # and an explicit `[]` key identically and keep sharing one set of counters.
4681                "exhaustion_status_codes": quota_model.exhaustion_status_codes or [],
4682                "matchers": [
4683                    {
4684                        "method": matcher_model.method,
4685                        "url_base": matcher_model.url_base,
4686                        "url_path_pattern": matcher_model.url_path_pattern,
4687                        "params": matcher_model.params,
4688                        "headers": matcher_model.headers,
4689                        "weight": matcher_model.weight,
4690                    }
4691                    for matcher_model in quota_model.matchers or []
4692                ],
4693            }
4694            for quota_model in model.quotas
4695        ]
4696
4697        quota_status_url = str(
4698            InterpolatedString.create(model.quota_status_source.url, parameters={}).eval(config)
4699        )
4700        quota_status_http_method = (
4701            model.quota_status_source.http_method.value
4702            if model.quota_status_source.http_method
4703            else "GET"
4704        )
4705        quota_status_headers = {
4706            key: str(InterpolatedString.create(value, parameters={}).eval(config))
4707            for key, value in (model.quota_status_source.request_headers or {}).items()
4708        }
4709        # Normalize the same way as the quota specs above, so an omitted field and an explicit
4710        # `[]` key identically and keep sharing one set of counters. Deduplicated as well as
4711        # sorted, because the runtime turns this into a set: without it `[404]` and `[404, 404]`
4712        # would key differently and stop sharing counters while behaving identically.
4713        quota_status_unavailable_status_codes = sorted(
4714            set(model.quota_status_source.unavailable_status_codes or [])
4715        )
4716        auth_method = model.auth_method or "Bearer"
4717        header = model.header or "Authorization"
4718        max_wait_time_str = str(
4719            InterpolatedString.create(model.max_wait_time or "PT2H", parameters={}).eval(config)
4720        )
4721        max_wait_time = parse_duration(max_wait_time_str)
4722        if not isinstance(max_wait_time, datetime.timedelta):
4723            raise ValueError(
4724                f"max_wait_time must be a fixed-length ISO 8601 duration (e.g. 'PT2H'); "
4725                f"calendar-unit durations like '{max_wait_time_str}' are not supported"
4726            )
4727        budget_reserve_fraction = (
4728            model.budget_reserve_fraction if model.budget_reserve_fraction is not None else 0.1
4729        )
4730        budget_min_reserve = (
4731            model.budget_min_reserve if model.budget_min_reserve is not None else 50
4732        )
4733
4734        # Reuse the same instance for identical definitions so that all streams share the
4735        # same token quota counters (similar to how api_budget is shared). The key is built
4736        # from the resolved constructor arguments rather than the raw model so that
4737        # stream-specific `$parameters` propagated onto the model (and its nested components)
4738        # cannot break instance sharing.
4739        cache_key = json.dumps(
4740            {
4741                "tokens": tokens,
4742                "quotas": quota_specs,
4743                "quota_status_url": quota_status_url,
4744                "quota_status_http_method": quota_status_http_method,
4745                "quota_status_headers": quota_status_headers,
4746                "quota_status_unavailable_status_codes": quota_status_unavailable_status_codes,
4747                "auth_method": auth_method,
4748                "header": header,
4749                "max_wait_time": max_wait_time.total_seconds(),
4750                "budget_reserve_fraction": budget_reserve_fraction,
4751                "budget_min_reserve": budget_min_reserve,
4752            },
4753            sort_keys=True,
4754        )
4755        if cache_key in self._rate_limited_authenticators:
4756            return self._rate_limited_authenticators[cache_key]
4757
4758        quotas = [
4759            TokenQuota(
4760                name=quota_model.name,
4761                remaining_path=quota_model.remaining_path,
4762                reset_path=quota_model.reset_path,
4763                limit_path=quota_model.limit_path,
4764                remaining_header=quota_model.remaining_header,
4765                reset_header=quota_model.reset_header,
4766                limit_header=quota_model.limit_header,
4767                exhaustion_status_codes=quota_model.exhaustion_status_codes or [],
4768                matchers=[
4769                    self.create_http_request_matcher(matcher_model, config)
4770                    for matcher_model in quota_model.matchers or []
4771                ],
4772            )
4773            for quota_model in model.quotas
4774        ]
4775
4776        authenticator = RateLimitedMultipleTokenAuthenticator(
4777            tokens=tokens,
4778            quotas=quotas,
4779            quota_status_url=quota_status_url,
4780            quota_status_http_method=quota_status_http_method,
4781            quota_status_headers=quota_status_headers,
4782            quota_status_unavailable_status_codes=quota_status_unavailable_status_codes,
4783            auth_method=auth_method,
4784            header=header,
4785            max_wait_time=max_wait_time,
4786            budget_reserve_fraction=budget_reserve_fraction,
4787            budget_min_reserve=budget_min_reserve,
4788        )
4789        self._rate_limited_authenticators[cache_key] = authenticator
4790        return authenticator
4791
4792    def set_api_budget(self, component_definition: ComponentDefinition, config: Config) -> None:
4793        self._api_budget = self.create_component(
4794            model_type=HTTPAPIBudgetModel, component_definition=component_definition, config=config
4795        )
4796
4797    def create_grouping_partition_router(
4798        self,
4799        model: GroupingPartitionRouterModel,
4800        config: Config,
4801        *,
4802        stream_name: str,
4803        **kwargs: Any,
4804    ) -> GroupingPartitionRouter:
4805        underlying_router = self._create_component_from_model(
4806            model=model.underlying_partition_router,
4807            config=config,
4808            stream_name=stream_name,
4809            **kwargs,
4810        )
4811        if model.group_size < 1:
4812            raise ValueError(f"Group size must be greater than 0, got {model.group_size}")
4813
4814        # Request options in underlying partition routers are not supported for GroupingPartitionRouter
4815        # because they are specific to individual partitions and cannot be aggregated or handled
4816        # when grouping, potentially leading to incorrect API calls. Any request customization
4817        # should be managed at the stream level through the requester's configuration.
4818        if isinstance(underlying_router, SubstreamPartitionRouter):
4819            if any(
4820                parent_config.request_option
4821                for parent_config in underlying_router.parent_stream_configs
4822            ):
4823                raise ValueError("Request options are not supported for GroupingPartitionRouter.")
4824
4825        if isinstance(underlying_router, ListPartitionRouter):
4826            if underlying_router.request_option:
4827                raise ValueError("Request options are not supported for GroupingPartitionRouter.")
4828
4829        return GroupingPartitionRouter(
4830            group_size=model.group_size,
4831            underlying_partition_router=underlying_router,
4832            deduplicate=model.deduplicate if model.deduplicate is not None else True,
4833            config=config,
4834        )
4835
4836    def create_union_partition_router(
4837        self,
4838        model: UnionPartitionRouterModel,
4839        config: Config,
4840        *,
4841        stream_name: str,
4842        **kwargs: Any,
4843    ) -> UnionPartitionRouter:
4844        # The schema enforces minItems: 2 for manifests; this guard covers construction paths
4845        # that bypass JSON-schema validation (the generated model carries no min_items constraint).
4846        if len(model.partition_routers) < 2:
4847            raise ValueError(
4848                f"UnionPartitionRouter for stream {stream_name} needs at least 2 child partition routers"
4849            )
4850
4851        partition_routers = [
4852            self._create_component_from_model(
4853                model=child,
4854                config=config,
4855                stream_name=stream_name,
4856                **kwargs,
4857            )
4858            for child in model.partition_routers
4859        ]
4860
4861        # partition_field depends only on config/parameters, so it is evaluated once at build
4862        # time; the runtime component always receives a plain string.
4863        partition_field = InterpolatedString.create(
4864            model.partition_field, parameters=model.parameters or {}
4865        ).eval(config)
4866
4867        # Fail fast at build time when a built-in child router is statically known to emit a
4868        # partition field different from the union's. CustomPartitionRouter children are opaque
4869        # and can only be validated at runtime.
4870        for child_model in model.partition_routers:
4871            child_partition_fields: List[str] = []
4872            if isinstance(child_model, ListPartitionRouterModel):
4873                child_partition_fields.append(
4874                    InterpolatedString.create(
4875                        child_model.cursor_field, parameters=child_model.parameters or {}
4876                    ).eval(config)
4877                )
4878            elif isinstance(child_model, SubstreamPartitionRouterModel):
4879                for parent_stream_config in child_model.parent_stream_configs:
4880                    child_partition_fields.append(
4881                        InterpolatedString.create(
4882                            parent_stream_config.partition_field,
4883                            parameters=parent_stream_config.parameters
4884                            or child_model.parameters
4885                            or {},
4886                        ).eval(config)
4887                    )
4888            elif isinstance(child_model, UnionPartitionRouterModel):
4889                child_partition_fields.append(
4890                    InterpolatedString.create(
4891                        child_model.partition_field, parameters=child_model.parameters or {}
4892                    ).eval(config)
4893                )
4894            for child_partition_field in child_partition_fields:
4895                if child_partition_field != partition_field:
4896                    raise ValueError(
4897                        f"UnionPartitionRouter expects all child partition routers to emit the "
4898                        f"partition field '{partition_field}', but a "
4899                        f"{child_model.type} child emits '{child_partition_field}'."
4900                    )
4901
4902        # A union slice comes from exactly one child partition router, so request options
4903        # declared on children cannot be applied consistently to requests built from the
4904        # normalized union slices. Partition values should be consumed via interpolation
4905        # (e.g. stream_partition) instead. Note that this validation only covers built-in
4906        # router types; CustomPartitionRouter children are opaque, so any request options
4907        # they implement internally cannot be detected or rejected here.
4908        for router in partition_routers:
4909            if isinstance(router, SubstreamPartitionRouter):
4910                if any(
4911                    parent_config.request_option for parent_config in router.parent_stream_configs
4912                ):
4913                    raise ValueError("Request options are not supported for UnionPartitionRouter.")
4914            if isinstance(router, ListPartitionRouter) and router.request_option:
4915                raise ValueError("Request options are not supported for UnionPartitionRouter.")
4916
4917        return UnionPartitionRouter(
4918            partition_routers=partition_routers,
4919            partition_field=partition_field,
4920            parameters=model.parameters or {},
4921        )
4922
4923    def _ensure_query_properties_to_model(
4924        self, requester: Union[HttpRequesterModel, CustomRequesterModel]
4925    ) -> None:
4926        """
4927        For some reason, it seems like CustomRequesterModel request_parameters stays as dictionaries which means that
4928        the other conditions relying on it being QueryPropertiesModel instead of a dict fail. Here, we migrate them to
4929        proper model.
4930        """
4931        if not hasattr(requester, "request_parameters"):
4932            return
4933
4934        request_parameters = requester.request_parameters
4935        if request_parameters and isinstance(request_parameters, Dict):
4936            for request_parameter_key in request_parameters.keys():
4937                request_parameter = request_parameters[request_parameter_key]
4938                if (
4939                    isinstance(request_parameter, Dict)
4940                    and request_parameter.get("type") == "QueryProperties"
4941                ):
4942                    request_parameters[request_parameter_key] = QueryPropertiesModel.parse_obj(
4943                        request_parameter
4944                    )
4945
4946    def _get_catalog_defined_cursor_field(
4947        self, stream_name: str, allow_catalog_defined_cursor_field: bool
4948    ) -> Optional[CursorField]:
4949        if not allow_catalog_defined_cursor_field:
4950            return None
4951
4952        configured_stream = self._stream_name_to_configured_stream.get(stream_name)
4953
4954        # Depending on the operation is being performed, there may not be a configured stream yet. In this
4955        # case we return None which will then use the default cursor field defined on the cursor model.
4956        # We also treat cursor_field: [""] (list with empty string) as no cursor field, since this can
4957        # occur when the platform serializes "no cursor configured" streams incorrectly.
4958        if (
4959            not configured_stream
4960            or not configured_stream.cursor_field
4961            or not configured_stream.cursor_field[0]
4962        ):
4963            return None
4964        elif len(configured_stream.cursor_field) > 1:
4965            raise ValueError(
4966                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."
4967            )
4968        else:
4969            return CursorField(
4970                cursor_field_key=configured_stream.cursor_field[0],
4971                supports_catalog_defined_cursor_field=allow_catalog_defined_cursor_field,
4972            )
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        return RecordExpander(
2511            expand_records_from_field=model.expand_records_from_field,
2512            config=config,
2513            parameters=model.parameters or {},
2514            remain_original_record=model.remain_original_record or False,
2515            on_no_records=OnNoRecords(model.on_no_records.value)
2516            if model.on_no_records
2517            else OnNoRecords.skip,
2518        )
2519
2520    @staticmethod
2521    def create_response_to_file_extractor(
2522        model: ResponseToFileExtractorModel,
2523        **kwargs: Any,
2524    ) -> ResponseToFileExtractor:
2525        return ResponseToFileExtractor(
2526            parameters=model.parameters or {},
2527            preserve_na_values=model.preserve_na_values or False,
2528        )
2529
2530    @staticmethod
2531    def create_exponential_backoff_strategy(
2532        model: ExponentialBackoffStrategyModel, config: Config
2533    ) -> ExponentialBackoffStrategy:
2534        ModelToComponentFactory._validate_jitter_range(model.jitter_range_in_seconds)
2535        return ExponentialBackoffStrategy(
2536            factor=model.factor or 5,
2537            jitter_range_in_seconds=model.jitter_range_in_seconds,
2538            parameters=model.parameters or {},
2539            config=config,
2540        )
2541
2542    @staticmethod
2543    def create_group_by_key(model: GroupByKeyMergeStrategyModel, config: Config) -> GroupByKey:
2544        return GroupByKey(model.key, config=config, parameters=model.parameters or {})
2545
2546    def create_http_requester(
2547        self,
2548        model: HttpRequesterModel,
2549        config: Config,
2550        decoder: Decoder = JsonDecoder(parameters={}),
2551        query_properties_key: Optional[str] = None,
2552        use_cache: Optional[bool] = None,
2553        *,
2554        name: str,
2555    ) -> HttpRequester:
2556        authenticator = (
2557            self._create_component_from_model(
2558                model=model.authenticator,
2559                config=config,
2560                url_base=model.url or model.url_base,
2561                name=name,
2562                decoder=decoder,
2563            )
2564            if model.authenticator
2565            else None
2566        )
2567        error_handler = (
2568            self._create_component_from_model(model=model.error_handler, config=config)
2569            if model.error_handler
2570            else DefaultErrorHandler(
2571                backoff_strategies=[],
2572                response_filters=[],
2573                config=config,
2574                parameters=model.parameters or {},
2575            )
2576        )
2577
2578        api_budget = self._api_budget
2579
2580        request_options_provider = InterpolatedRequestOptionsProvider(
2581            request_body=model.request_body,
2582            request_body_data=model.request_body_data,
2583            request_body_json=model.request_body_json,
2584            request_headers=model.request_headers,
2585            request_parameters=model.request_parameters,  # type: ignore  # QueryProperties have been removed in `create_simple_retriever`
2586            query_properties_key=query_properties_key,
2587            config=config,
2588            parameters=model.parameters or {},
2589        )
2590
2591        assert model.use_cache is not None  # for mypy
2592        assert model.http_method is not None  # for mypy
2593
2594        should_use_cache = (model.use_cache or bool(use_cache)) and not self._disable_cache
2595
2596        return HttpRequester(
2597            name=name,
2598            url=model.url,
2599            url_base=model.url_base,
2600            path=model.path,
2601            authenticator=authenticator,
2602            error_handler=error_handler,
2603            api_budget=api_budget,
2604            http_method=HttpMethod[model.http_method.value],
2605            request_options_provider=request_options_provider,
2606            config=config,
2607            disable_retries=self._disable_retries,
2608            parameters=model.parameters or {},
2609            message_repository=self._message_repository,
2610            use_cache=should_use_cache,
2611            decoder=decoder,
2612            stream_response=decoder.is_stream_response() if decoder else False,
2613        )
2614
2615    @staticmethod
2616    def create_http_response_filter(
2617        model: HttpResponseFilterModel, config: Config, **kwargs: Any
2618    ) -> HttpResponseFilter:
2619        if model.action:
2620            action = ResponseAction(model.action.value)
2621        else:
2622            action = None
2623
2624        failure_type = FailureType(model.failure_type.value) if model.failure_type else None
2625
2626        http_codes = (
2627            set(model.http_codes) if model.http_codes else set()
2628        )  # JSON schema notation has no set data type. The schema enforces an array of unique elements
2629
2630        return HttpResponseFilter(
2631            action=action,
2632            failure_type=failure_type,
2633            error_message=model.error_message or "",
2634            error_message_contains=model.error_message_contains or "",
2635            http_codes=http_codes,
2636            predicate=model.predicate or "",
2637            config=config,
2638            parameters=model.parameters or {},
2639        )
2640
2641    @staticmethod
2642    def create_inline_schema_loader(
2643        model: InlineSchemaLoaderModel, config: Config, **kwargs: Any
2644    ) -> InlineSchemaLoader:
2645        return InlineSchemaLoader(schema=model.schema_ or {}, parameters={})
2646
2647    def create_complex_field_type(
2648        self, model: ComplexFieldTypeModel, config: Config, **kwargs: Any
2649    ) -> ComplexFieldType:
2650        items = (
2651            self._create_component_from_model(model=model.items, config=config)
2652            if isinstance(model.items, ComplexFieldTypeModel)
2653            else model.items
2654        )
2655
2656        return ComplexFieldType(field_type=model.field_type, items=items)
2657
2658    def create_types_map(self, model: TypesMapModel, config: Config, **kwargs: Any) -> TypesMap:
2659        target_type = (
2660            self._create_component_from_model(model=model.target_type, config=config)
2661            if isinstance(model.target_type, ComplexFieldTypeModel)
2662            else model.target_type
2663        )
2664
2665        return TypesMap(
2666            target_type=target_type,
2667            current_type=model.current_type,
2668            condition=model.condition if model.condition is not None else "True",
2669        )
2670
2671    def create_schema_type_identifier(
2672        self, model: SchemaTypeIdentifierModel, config: Config, **kwargs: Any
2673    ) -> SchemaTypeIdentifier:
2674        types_mapping = []
2675        if model.types_mapping:
2676            types_mapping.extend(
2677                [
2678                    self._create_component_from_model(types_map, config=config)
2679                    for types_map in model.types_mapping
2680                ]
2681            )
2682        model_schema_pointer: List[Union[InterpolatedString, str]] = (
2683            [x for x in model.schema_pointer] if model.schema_pointer else []
2684        )
2685        model_key_pointer: List[Union[InterpolatedString, str]] = [x for x in model.key_pointer]
2686        model_type_pointer: Optional[List[Union[InterpolatedString, str]]] = (
2687            [x for x in model.type_pointer] if model.type_pointer else None
2688        )
2689
2690        return SchemaTypeIdentifier(
2691            schema_pointer=model_schema_pointer,
2692            key_pointer=model_key_pointer,
2693            type_pointer=model_type_pointer,
2694            types_mapping=types_mapping,
2695            parameters=model.parameters or {},
2696        )
2697
2698    def create_dynamic_schema_loader(
2699        self, model: DynamicSchemaLoaderModel, config: Config, **kwargs: Any
2700    ) -> DynamicSchemaLoader:
2701        schema_transformations = []
2702        if model.schema_transformations:
2703            for transformation_model in model.schema_transformations:
2704                schema_transformations.append(
2705                    self._create_component_from_model(model=transformation_model, config=config)
2706                )
2707        name = "dynamic_properties"
2708        retriever = self._create_component_from_model(
2709            model=model.retriever,
2710            config=config,
2711            name=name,
2712            primary_key=None,
2713            partition_router=self._build_stream_slicer_from_partition_router(
2714                model.retriever, config
2715            ),
2716            transformations=[],
2717            use_cache=True,
2718            log_formatter=(
2719                lambda response: format_http_message(
2720                    response,
2721                    f"Schema loader '{name}' request",
2722                    f"Request performed in order to extract schema.",
2723                    name,
2724                    is_auxiliary=True,
2725                )
2726            ),
2727        )
2728        schema_type_identifier = self._create_component_from_model(
2729            model.schema_type_identifier, config=config, parameters=model.parameters or {}
2730        )
2731        schema_filter = (
2732            self._create_component_from_model(
2733                model.schema_filter, config=config, parameters=model.parameters or {}
2734            )
2735            if model.schema_filter is not None
2736            else None
2737        )
2738
2739        return DynamicSchemaLoader(
2740            retriever=retriever,
2741            config=config,
2742            schema_transformations=schema_transformations,
2743            schema_filter=schema_filter,
2744            schema_type_identifier=schema_type_identifier,
2745            parameters=model.parameters or {},
2746        )
2747
2748    @staticmethod
2749    def create_json_decoder(model: JsonDecoderModel, config: Config, **kwargs: Any) -> Decoder:
2750        return JsonDecoder(parameters={})
2751
2752    def create_csv_decoder(self, model: CsvDecoderModel, config: Config, **kwargs: Any) -> Decoder:
2753        return CompositeRawDecoder(
2754            parser=ModelToComponentFactory._get_parser(model, config),
2755            stream_response=False if self._emit_connector_builder_messages else True,
2756        )
2757
2758    def create_jsonl_decoder(
2759        self, model: JsonlDecoderModel, config: Config, **kwargs: Any
2760    ) -> Decoder:
2761        return CompositeRawDecoder(
2762            parser=ModelToComponentFactory._get_parser(model, config),
2763            stream_response=False if self._emit_connector_builder_messages else True,
2764        )
2765
2766    def create_json_items_decoder(
2767        self, model: JsonItemsDecoderModel, config: Config, **kwargs: Any
2768    ) -> Decoder:
2769        return CompositeRawDecoder(
2770            parser=ModelToComponentFactory._get_parser(model, config),
2771            stream_response=False if self._emit_connector_builder_messages else True,
2772        )
2773
2774    def create_gzip_decoder(
2775        self, model: GzipDecoderModel, config: Config, **kwargs: Any
2776    ) -> Decoder:
2777        _compressed_response_types = {
2778            "gzip",
2779            "x-gzip",
2780            "gzip, deflate",
2781            "x-gzip, deflate",
2782            "application/zip",
2783            "application/gzip",
2784            "application/x-gzip",
2785            "application/x-zip-compressed",
2786        }
2787
2788        gzip_parser: GzipParser = ModelToComponentFactory._get_parser(model, config)  # type: ignore  # based on the model, we know this will be a GzipParser
2789
2790        if self._emit_connector_builder_messages:
2791            return CompositeRawDecoder(gzip_parser, False)
2792
2793        transport_gzip_parser = GzipParser(inner_parser=gzip_parser)
2794        return CompositeRawDecoder.by_headers(
2795            [
2796                ({"Content-Encoding"}, {"gzip"}, transport_gzip_parser),
2797                ({"Content-Type"}, _compressed_response_types, gzip_parser),
2798            ],
2799            stream_response=True,
2800            fallback_parser=gzip_parser,
2801        )
2802
2803    @staticmethod
2804    def create_iterable_decoder(
2805        model: IterableDecoderModel, config: Config, **kwargs: Any
2806    ) -> IterableDecoder:
2807        return IterableDecoder(parameters={})
2808
2809    @staticmethod
2810    def create_xml_decoder(model: XmlDecoderModel, config: Config, **kwargs: Any) -> XmlDecoder:
2811        return XmlDecoder(parameters={})
2812
2813    def create_zipfile_decoder(
2814        self, model: ZipfileDecoderModel, config: Config, **kwargs: Any
2815    ) -> ZipfileDecoder:
2816        return ZipfileDecoder(parser=ModelToComponentFactory._get_parser(model.decoder, config))
2817
2818    @staticmethod
2819    def _get_parser(model: BaseModel, config: Config) -> Parser:
2820        if isinstance(model, JsonDecoderModel):
2821            # Note that the logic is a bit different from the JsonDecoder as there is some legacy that is maintained to return {} on error cases
2822            return JsonParser()
2823        elif isinstance(model, JsonItemsDecoderModel):
2824            return JsonItemsParser(
2825                items_path=model.items_path,
2826                encoding=model.encoding,
2827            )
2828        elif isinstance(model, JsonlDecoderModel):
2829            return JsonLineParser()
2830        elif isinstance(model, CsvDecoderModel):
2831            return CsvParser(
2832                encoding=model.encoding,
2833                delimiter=model.delimiter,
2834                set_values_to_none=model.set_values_to_none,
2835            )
2836        elif isinstance(model, GzipDecoderModel):
2837            return GzipParser(
2838                inner_parser=ModelToComponentFactory._get_parser(model.decoder, config)
2839            )
2840        elif isinstance(
2841            model, (CustomDecoderModel, IterableDecoderModel, XmlDecoderModel, ZipfileDecoderModel)
2842        ):
2843            raise ValueError(f"Decoder type {model} does not have parser associated to it")
2844
2845        raise ValueError(f"Unknown decoder type {model}")
2846
2847    @staticmethod
2848    def create_json_file_schema_loader(
2849        model: JsonFileSchemaLoaderModel, config: Config, **kwargs: Any
2850    ) -> JsonFileSchemaLoader:
2851        return JsonFileSchemaLoader(
2852            file_path=model.file_path or "", config=config, parameters=model.parameters or {}
2853        )
2854
2855    def create_jwt_authenticator(
2856        self, model: JwtAuthenticatorModel, config: Config, **kwargs: Any
2857    ) -> JwtAuthenticator:
2858        jwt_headers = model.jwt_headers or JwtHeadersModel(kid=None, typ="JWT", cty=None)
2859        jwt_payload = model.jwt_payload or JwtPayloadModel(iss=None, sub=None, aud=None)
2860        request_option = (
2861            self._create_component_from_model(model.request_option, config)
2862            if model.request_option
2863            else None
2864        )
2865        return JwtAuthenticator(
2866            config=config,
2867            parameters=model.parameters or {},
2868            algorithm=JwtAlgorithm(model.algorithm.value),
2869            secret_key=model.secret_key,
2870            base64_encode_secret_key=model.base64_encode_secret_key,
2871            token_duration=model.token_duration,
2872            header_prefix=model.header_prefix,
2873            kid=jwt_headers.kid,
2874            typ=jwt_headers.typ,
2875            cty=jwt_headers.cty,
2876            iss=jwt_payload.iss,
2877            sub=jwt_payload.sub,
2878            aud=jwt_payload.aud,
2879            additional_jwt_headers=model.additional_jwt_headers,
2880            additional_jwt_payload=model.additional_jwt_payload,
2881            passphrase=model.passphrase,
2882            request_option=request_option,
2883        )
2884
2885    def create_list_partition_router(
2886        self, model: ListPartitionRouterModel, config: Config, **kwargs: Any
2887    ) -> ListPartitionRouter:
2888        request_option = (
2889            self._create_component_from_model(model.request_option, config)
2890            if model.request_option
2891            else None
2892        )
2893        return ListPartitionRouter(
2894            cursor_field=model.cursor_field,
2895            request_option=request_option,
2896            values=model.values,
2897            config=config,
2898            parameters=model.parameters or {},
2899        )
2900
2901    @staticmethod
2902    def create_min_max_datetime(
2903        model: MinMaxDatetimeModel, config: Config, **kwargs: Any
2904    ) -> MinMaxDatetime:
2905        return MinMaxDatetime(
2906            datetime=model.datetime,
2907            datetime_format=model.datetime_format or "",
2908            max_datetime=model.max_datetime or "",
2909            min_datetime=model.min_datetime or "",
2910            parameters=model.parameters or {},
2911        )
2912
2913    @staticmethod
2914    def create_no_auth(model: NoAuthModel, config: Config, **kwargs: Any) -> NoAuth:
2915        return NoAuth(parameters=model.parameters or {})
2916
2917    @staticmethod
2918    def create_no_pagination(
2919        model: NoPaginationModel, config: Config, **kwargs: Any
2920    ) -> NoPagination:
2921        return NoPagination(parameters={})
2922
2923    def create_oauth_authenticator(
2924        self, model: OAuthAuthenticatorModel, config: Config, **kwargs: Any
2925    ) -> DeclarativeOauth2Authenticator:
2926        profile_assertion = (
2927            self._create_component_from_model(model.profile_assertion, config=config)
2928            if model.profile_assertion
2929            else None
2930        )
2931
2932        refresh_token_error_status_codes, refresh_token_error_key, refresh_token_error_values = (
2933            self._get_refresh_token_error_information(model)
2934        )
2935        if model.refresh_token_updater:
2936            # ignore type error because fixing it would have a lot of dependencies, revisit later
2937            return DeclarativeSingleUseRefreshTokenOauth2Authenticator(  # type: ignore
2938                config,
2939                InterpolatedString.create(
2940                    model.token_refresh_endpoint,  # type: ignore
2941                    parameters=model.parameters or {},
2942                ).eval(config),
2943                access_token_name=InterpolatedString.create(
2944                    model.access_token_name or "access_token", parameters=model.parameters or {}
2945                ).eval(config),
2946                refresh_token_name=model.refresh_token_updater.refresh_token_name,
2947                expires_in_name=InterpolatedString.create(
2948                    model.expires_in_name or "expires_in", parameters=model.parameters or {}
2949                ).eval(config),
2950                client_id_name=InterpolatedString.create(
2951                    model.client_id_name or "client_id", parameters=model.parameters or {}
2952                ).eval(config),
2953                client_id=InterpolatedString.create(
2954                    model.client_id, parameters=model.parameters or {}
2955                ).eval(config)
2956                if model.client_id
2957                else model.client_id,
2958                client_secret_name=InterpolatedString.create(
2959                    model.client_secret_name or "client_secret", parameters=model.parameters or {}
2960                ).eval(config),
2961                client_secret=InterpolatedString.create(
2962                    model.client_secret, parameters=model.parameters or {}
2963                ).eval(config)
2964                if model.client_secret
2965                else model.client_secret,
2966                access_token_config_path=model.refresh_token_updater.access_token_config_path,
2967                refresh_token_config_path=model.refresh_token_updater.refresh_token_config_path,
2968                token_expiry_date_config_path=model.refresh_token_updater.token_expiry_date_config_path,
2969                grant_type_name=InterpolatedString.create(
2970                    model.grant_type_name or "grant_type", parameters=model.parameters or {}
2971                ).eval(config),
2972                grant_type=InterpolatedString.create(
2973                    model.grant_type or "refresh_token", parameters=model.parameters or {}
2974                ).eval(config),
2975                refresh_request_body=InterpolatedMapping(
2976                    model.refresh_request_body or {}, parameters=model.parameters or {}
2977                ).eval(config),
2978                refresh_request_headers=InterpolatedMapping(
2979                    model.refresh_request_headers or {}, parameters=model.parameters or {}
2980                ).eval(config),
2981                send_refresh_request_as_query_params=bool(
2982                    model.send_refresh_request_as_query_params
2983                ),
2984                scopes=model.scopes,
2985                token_expiry_date_format=model.token_expiry_date_format,
2986                token_expiry_is_time_of_expiration=bool(model.token_expiry_date_format),
2987                message_repository=self._message_repository,
2988                refresh_token_error_status_codes=refresh_token_error_status_codes,
2989                refresh_token_error_key=refresh_token_error_key,
2990                refresh_token_error_values=refresh_token_error_values,
2991            )
2992        # ignore type error because fixing it would have a lot of dependencies, revisit later
2993        return DeclarativeOauth2Authenticator(  # type: ignore
2994            access_token_name=model.access_token_name or "access_token",
2995            access_token_value=model.access_token_value,
2996            client_id_name=model.client_id_name or "client_id",
2997            client_id=model.client_id,
2998            client_secret_name=model.client_secret_name or "client_secret",
2999            client_secret=model.client_secret,
3000            expires_in_name=model.expires_in_name or "expires_in",
3001            grant_type_name=model.grant_type_name or "grant_type",
3002            grant_type=model.grant_type or "refresh_token",
3003            refresh_request_body=model.refresh_request_body,
3004            refresh_request_headers=model.refresh_request_headers,
3005            send_refresh_request_as_query_params=bool(model.send_refresh_request_as_query_params),
3006            refresh_token_name=model.refresh_token_name or "refresh_token",
3007            refresh_token=model.refresh_token,
3008            scopes=model.scopes,
3009            token_expiry_date=model.token_expiry_date,
3010            token_expiry_date_format=model.token_expiry_date_format,
3011            token_expiry_is_time_of_expiration=bool(model.token_expiry_date_format),
3012            token_refresh_endpoint=model.token_refresh_endpoint,
3013            config=config,
3014            parameters=model.parameters or {},
3015            message_repository=self._message_repository,
3016            profile_assertion=profile_assertion,
3017            use_profile_assertion=model.use_profile_assertion,
3018            refresh_token_error_status_codes=refresh_token_error_status_codes,
3019            refresh_token_error_key=refresh_token_error_key,
3020            refresh_token_error_values=refresh_token_error_values,
3021        )
3022
3023    @staticmethod
3024    def _get_refresh_token_error_information(
3025        model: OAuthAuthenticatorModel,
3026    ) -> Tuple[Tuple[int, ...], str, Tuple[str, ...]]:
3027        """
3028        In a previous version of the CDK, the auth error as config_error was only done if a refresh token updater was
3029        defined. As a transition, we added those fields on the OAuthAuthenticatorModel. This method ensures that the
3030        information is defined only once and return the right fields.
3031        """
3032        refresh_token_updater = model.refresh_token_updater
3033        is_defined_on_refresh_token_updated = refresh_token_updater and (
3034            refresh_token_updater.refresh_token_error_status_codes
3035            or refresh_token_updater.refresh_token_error_key
3036            or refresh_token_updater.refresh_token_error_values
3037        )
3038        is_defined_on_oauth_authenticator = (
3039            model.refresh_token_error_status_codes
3040            or model.refresh_token_error_key
3041            or model.refresh_token_error_values
3042        )
3043        if is_defined_on_refresh_token_updated and is_defined_on_oauth_authenticator:
3044            raise ValueError(
3045                "refresh_token_error should either be defined on the OAuthAuthenticatorModel or the RefreshTokenUpdaterModel, not both"
3046            )
3047
3048        if is_defined_on_refresh_token_updated:
3049            not_optional_refresh_token_updater: RefreshTokenUpdaterModel = refresh_token_updater  # type: ignore  # we know from the condition that this is not None
3050            return (
3051                tuple(not_optional_refresh_token_updater.refresh_token_error_status_codes)
3052                if not_optional_refresh_token_updater.refresh_token_error_status_codes
3053                else (),
3054                not_optional_refresh_token_updater.refresh_token_error_key or "",
3055                tuple(not_optional_refresh_token_updater.refresh_token_error_values)
3056                if not_optional_refresh_token_updater.refresh_token_error_values
3057                else (),
3058            )
3059        elif is_defined_on_oauth_authenticator:
3060            return (
3061                tuple(model.refresh_token_error_status_codes)
3062                if model.refresh_token_error_status_codes
3063                else (),
3064                model.refresh_token_error_key or "",
3065                tuple(model.refresh_token_error_values) if model.refresh_token_error_values else (),
3066            )
3067
3068        # returning default values we think cover most cases
3069        return (400,), "error", ("invalid_grant", "invalid_permissions")
3070
3071    def create_offset_increment(
3072        self,
3073        model: OffsetIncrementModel,
3074        config: Config,
3075        decoder: Decoder,
3076        extractor_model: Optional[Union[CustomRecordExtractorModel, DpathExtractorModel]] = None,
3077        **kwargs: Any,
3078    ) -> OffsetIncrement:
3079        if isinstance(decoder, PaginationDecoderDecorator):
3080            inner_decoder = decoder.decoder
3081        else:
3082            inner_decoder = decoder
3083            decoder = PaginationDecoderDecorator(decoder=decoder)
3084
3085        if self._is_supported_decoder_for_pagination(inner_decoder):
3086            decoder_to_use = decoder
3087        else:
3088            raise ValueError(
3089                self._UNSUPPORTED_DECODER_ERROR.format(decoder_type=type(inner_decoder))
3090            )
3091
3092        # Ideally we would instantiate the runtime extractor from highest most level (in this case the SimpleRetriever)
3093        # so that it can be shared by OffSetIncrement and RecordSelector. However, due to how we instantiate the
3094        # decoder with various decorators here, but not in create_record_selector, it is simpler to retain existing
3095        # behavior by having two separate extractors with identical behavior since they use the same extractor model.
3096        # When we have more time to investigate we can look into reusing the same component.
3097        extractor = (
3098            self._create_component_from_model(
3099                model=extractor_model, config=config, decoder=decoder_to_use
3100            )
3101            if extractor_model
3102            else None
3103        )
3104
3105        # Pydantic v1 Union type coercion can convert int to string depending on Union order.
3106        # If page_size is a string that represents an integer (not an interpolation), convert it back.
3107        page_size = model.page_size
3108        if isinstance(page_size, str) and page_size.isdigit():
3109            page_size = int(page_size)
3110
3111        return OffsetIncrement(
3112            page_size=page_size,
3113            config=config,
3114            decoder=decoder_to_use,
3115            extractor=extractor,
3116            inject_on_first_request=model.inject_on_first_request or False,
3117            parameters=model.parameters or {},
3118        )
3119
3120    def create_page_increment(
3121        self,
3122        model: PageIncrementModel,
3123        config: Config,
3124        decoder: Optional[Decoder] = None,
3125        extractor_model: Optional[Union[CustomRecordExtractorModel, DpathExtractorModel]] = None,
3126        **kwargs: Any,
3127    ) -> PageIncrement:
3128        # Like OffsetIncrement, we instantiate a separate extractor with identical behavior to the
3129        # RecordSelector's so the strategy can count the raw records in the response. This ensures
3130        # pagination is driven by the API's page size, not the post-filter record count.
3131        extractor = (
3132            self._create_component_from_model(model=extractor_model, config=config, decoder=decoder)
3133            if extractor_model
3134            else None
3135        )
3136
3137        # Pydantic v1 Union type coercion can convert int to string depending on Union order.
3138        # If page_size is a string that represents an integer (not an interpolation), convert it back.
3139        page_size = model.page_size
3140        if isinstance(page_size, str) and page_size.isdigit():
3141            page_size = int(page_size)
3142
3143        return PageIncrement(
3144            page_size=page_size,
3145            config=config,
3146            start_from_page=model.start_from_page or 0,
3147            inject_on_first_request=model.inject_on_first_request or False,
3148            extractor=extractor,
3149            parameters=model.parameters or {},
3150        )
3151
3152    def create_parent_stream_config(
3153        self, model: ParentStreamConfigModel, config: Config, *, stream_name: str, **kwargs: Any
3154    ) -> ParentStreamConfig:
3155        declarative_stream = self._create_component_from_model(
3156            model.stream,
3157            config=config,
3158            is_parent=True,
3159            **kwargs,
3160        )
3161        request_option = (
3162            self._create_component_from_model(model.request_option, config=config)
3163            if model.request_option
3164            else None
3165        )
3166
3167        if model.lazy_read_pointer and any("*" in pointer for pointer in model.lazy_read_pointer):
3168            raise ValueError(
3169                "The '*' wildcard in 'lazy_read_pointer' is not supported — only direct paths are allowed."
3170            )
3171
3172        model_lazy_read_pointer: List[Union[InterpolatedString, str]] = (
3173            [x for x in model.lazy_read_pointer] if model.lazy_read_pointer else []
3174        )
3175
3176        return ParentStreamConfig(
3177            parent_key=model.parent_key,
3178            request_option=request_option,
3179            stream=declarative_stream,
3180            partition_field=model.partition_field,
3181            config=config,
3182            incremental_dependency=model.incremental_dependency or False,
3183            parameters=model.parameters or {},
3184            extra_fields=model.extra_fields,
3185            lazy_read_pointer=model_lazy_read_pointer,
3186        )
3187
3188    def create_properties_from_endpoint(
3189        self, model: PropertiesFromEndpointModel, config: Config, **kwargs: Any
3190    ) -> PropertiesFromEndpoint:
3191        retriever = self._create_component_from_model(
3192            model=model.retriever,
3193            config=config,
3194            name="dynamic_properties",
3195            primary_key=None,
3196            stream_slicer=None,
3197            transformations=[],
3198            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
3199        )
3200        return PropertiesFromEndpoint(
3201            property_field_path=model.property_field_path,
3202            retriever=retriever,
3203            config=config,
3204            parameters=model.parameters or {},
3205        )
3206
3207    def create_property_chunking(
3208        self, model: PropertyChunkingModel, config: Config, **kwargs: Any
3209    ) -> PropertyChunking:
3210        record_merge_strategy = (
3211            self._create_component_from_model(
3212                model=model.record_merge_strategy, config=config, **kwargs
3213            )
3214            if model.record_merge_strategy
3215            else None
3216        )
3217
3218        property_limit_type: PropertyLimitType
3219        match model.property_limit_type:
3220            case PropertyLimitTypeModel.property_count:
3221                property_limit_type = PropertyLimitType.property_count
3222            case PropertyLimitTypeModel.characters:
3223                property_limit_type = PropertyLimitType.characters
3224            case _:
3225                raise ValueError(f"Invalid PropertyLimitType {property_limit_type}")
3226
3227        return PropertyChunking(
3228            property_limit_type=property_limit_type,
3229            property_limit=model.property_limit,
3230            record_merge_strategy=record_merge_strategy,
3231            config=config,
3232            parameters=model.parameters or {},
3233        )
3234
3235    def create_query_properties(
3236        self, model: QueryPropertiesModel, config: Config, *, stream_name: str, **kwargs: Any
3237    ) -> QueryProperties:
3238        if isinstance(model.property_list, list):
3239            property_list = model.property_list
3240        else:
3241            property_list = self._create_component_from_model(
3242                model=model.property_list, config=config, **kwargs
3243            )
3244
3245        property_chunking = (
3246            self._create_component_from_model(
3247                model=model.property_chunking, config=config, **kwargs
3248            )
3249            if model.property_chunking
3250            else None
3251        )
3252
3253        property_selector = (
3254            self._create_component_from_model(
3255                model=model.property_selector, config=config, stream_name=stream_name, **kwargs
3256            )
3257            if model.property_selector
3258            else None
3259        )
3260
3261        return QueryProperties(
3262            property_list=property_list,
3263            always_include_properties=model.always_include_properties,
3264            property_chunking=property_chunking,
3265            property_selector=property_selector,
3266            config=config,
3267            parameters=model.parameters or {},
3268        )
3269
3270    def create_json_schema_property_selector(
3271        self,
3272        model: JsonSchemaPropertySelectorModel,
3273        config: Config,
3274        *,
3275        stream_name: str,
3276        **kwargs: Any,
3277    ) -> JsonSchemaPropertySelector:
3278        configured_stream = self._stream_name_to_configured_stream.get(stream_name)
3279
3280        transformations = []
3281        if model.transformations:
3282            for transformation_model in model.transformations:
3283                transformations.append(
3284                    self._create_component_from_model(model=transformation_model, config=config)
3285                )
3286
3287        return JsonSchemaPropertySelector(
3288            configured_stream=configured_stream,
3289            properties_transformations=transformations,
3290            config=config,
3291            parameters=model.parameters or {},
3292        )
3293
3294    @staticmethod
3295    def create_record_filter(
3296        model: RecordFilterModel, config: Config, **kwargs: Any
3297    ) -> RecordFilter:
3298        return RecordFilter(
3299            condition=model.condition or "", config=config, parameters=model.parameters or {}
3300        )
3301
3302    @staticmethod
3303    def create_request_path(model: RequestPathModel, config: Config, **kwargs: Any) -> RequestPath:
3304        return RequestPath(parameters={})
3305
3306    @staticmethod
3307    def create_request_option(
3308        model: RequestOptionModel, config: Config, **kwargs: Any
3309    ) -> RequestOption:
3310        inject_into = RequestOptionType(model.inject_into.value)
3311        field_path: Optional[List[Union[InterpolatedString, str]]] = (
3312            [
3313                InterpolatedString.create(segment, parameters=kwargs.get("parameters", {}))
3314                for segment in model.field_path
3315            ]
3316            if model.field_path
3317            else None
3318        )
3319        field_name = (
3320            InterpolatedString.create(model.field_name, parameters=kwargs.get("parameters", {}))
3321            if model.field_name
3322            else None
3323        )
3324        return RequestOption(
3325            field_name=field_name,
3326            field_path=field_path,
3327            inject_into=inject_into,
3328            parameters=kwargs.get("parameters", {}),
3329        )
3330
3331    def create_record_selector(
3332        self,
3333        model: RecordSelectorModel,
3334        config: Config,
3335        *,
3336        name: str,
3337        transformations: List[RecordTransformation] | None = None,
3338        decoder: Decoder | None = None,
3339        client_side_incremental_sync_cursor: Optional[Cursor] = None,
3340        is_client_side_incremental_sync: bool = False,
3341        file_uploader: Optional[DefaultFileUploader] = None,
3342        **kwargs: Any,
3343    ) -> RecordSelector:
3344        extractor = self._create_component_from_model(
3345            model=model.extractor, decoder=decoder, config=config
3346        )
3347        record_filter = (
3348            self._create_component_from_model(model.record_filter, config=config)
3349            if model.record_filter
3350            else None
3351        )
3352
3353        # A client-side incremental stream transforms before filtering by default. That default belongs to the flag,
3354        # not to the component that ends up doing the cursor comparison: a data feed does it in the retriever and
3355        # receives no cursor here, but its `record_filter` condition must keep running after the transformations.
3356        default_transform_before_filtering = bool(
3357            client_side_incremental_sync_cursor or is_client_side_incremental_sync
3358        )
3359        transform_before_filtering = (
3360            default_transform_before_filtering
3361            if model.transform_before_filtering is None
3362            else model.transform_before_filtering
3363        )
3364        if client_side_incremental_sync_cursor:
3365            record_filter = ClientSideIncrementalRecordFilterDecorator(
3366                config=config,
3367                parameters=model.parameters,
3368                condition=model.record_filter.condition
3369                if (model.record_filter and hasattr(model.record_filter, "condition"))
3370                else None,
3371                cursor=client_side_incremental_sync_cursor,
3372            )
3373
3374        if model.schema_normalization is None:
3375            # default to no schema normalization if not set
3376            model.schema_normalization = SchemaNormalizationModel.None_
3377
3378        schema_normalization = (
3379            TypeTransformer(SCHEMA_TRANSFORMER_TYPE_MAPPING[model.schema_normalization])
3380            if isinstance(model.schema_normalization, SchemaNormalizationModel)
3381            else self._create_component_from_model(model.schema_normalization, config=config)  # type: ignore[arg-type] # custom normalization model expected here
3382        )
3383
3384        return RecordSelector(
3385            extractor=extractor,
3386            name=name,
3387            config=config,
3388            record_filter=record_filter,
3389            transformations=transformations or [],
3390            file_uploader=file_uploader,
3391            schema_normalization=schema_normalization,
3392            parameters=model.parameters or {},
3393            transform_before_filtering=transform_before_filtering,
3394        )
3395
3396    @staticmethod
3397    def create_remove_fields(
3398        model: RemoveFieldsModel, config: Config, **kwargs: Any
3399    ) -> RemoveFields:
3400        return RemoveFields(
3401            field_pointers=model.field_pointers, condition=model.condition or "", parameters={}
3402        )
3403
3404    def create_selective_authenticator(
3405        self, model: SelectiveAuthenticatorModel, config: Config, **kwargs: Any
3406    ) -> DeclarativeAuthenticator:
3407        authenticators = {
3408            name: self._create_component_from_model(model=auth, config=config)
3409            for name, auth in model.authenticators.items()
3410        }
3411        # SelectiveAuthenticator will return instance of DeclarativeAuthenticator or raise ValueError error
3412        return SelectiveAuthenticator(  # type: ignore[abstract]
3413            config=config,
3414            authenticators=authenticators,
3415            authenticator_selection_path=model.authenticator_selection_path,
3416            **kwargs,
3417        )
3418
3419    @staticmethod
3420    def create_legacy_session_token_authenticator(
3421        model: LegacySessionTokenAuthenticatorModel, config: Config, *, url_base: str, **kwargs: Any
3422    ) -> LegacySessionTokenAuthenticator:
3423        return LegacySessionTokenAuthenticator(
3424            api_url=url_base,
3425            header=model.header,
3426            login_url=model.login_url,
3427            password=model.password or "",
3428            session_token=model.session_token or "",
3429            session_token_response_key=model.session_token_response_key or "",
3430            username=model.username or "",
3431            validate_session_url=model.validate_session_url,
3432            config=config,
3433            parameters=model.parameters or {},
3434        )
3435
3436    def create_simple_retriever(
3437        self,
3438        model: SimpleRetrieverModel,
3439        config: Config,
3440        *,
3441        name: str,
3442        primary_key: Optional[Union[str, List[str], List[List[str]]]],
3443        request_options_provider: Optional[RequestOptionsProvider] = None,
3444        cursor: Optional[Cursor] = None,
3445        has_stop_condition_cursor: bool = False,
3446        is_client_side_incremental_sync: bool = False,
3447        transformations: List[RecordTransformation],
3448        file_uploader: Optional[DefaultFileUploader] = None,
3449        incremental_sync: Optional[
3450            Union[IncrementingCountCursorModel, DatetimeBasedCursorModel]
3451        ] = None,
3452        use_cache: Optional[bool] = None,
3453        log_formatter: Optional[Callable[[Response], Any]] = None,
3454        partition_router: Optional[PartitionRouter] = None,
3455        **kwargs: Any,
3456    ) -> SimpleRetriever:
3457        def _get_url(req: Requester) -> str:
3458            """
3459            Closure to get the URL from the requester. This is used to get the URL in the case of a lazy retriever.
3460            This is needed because the URL is not set until the requester is created.
3461            """
3462
3463            _url: str = (
3464                model.requester.url
3465                if hasattr(model.requester, "url") and model.requester.url is not None
3466                else req.get_url(stream_state=None, stream_slice=None, next_page_token=None)
3467            )
3468            _url_base: str = (
3469                model.requester.url_base
3470                if hasattr(model.requester, "url_base") and model.requester.url_base is not None
3471                else req.get_url_base(stream_state=None, stream_slice=None, next_page_token=None)
3472            )
3473
3474            return _url or _url_base
3475
3476        if cursor is None:
3477            cursor = FinalStateCursor(name, None, self._message_repository)
3478
3479        # A data feed drops the records the cursor considers already synced in the retriever, which sits downstream of
3480        # the paginator. Letting the record selector drop them as well would be redundant and would hide them from the
3481        # pagination stop condition, so a data feed never delegates that filtering to the record selector, whether
3482        # `is_client_side_incremental` is set or not. The `condition` from `record_filter` is intentionally left out of
3483        # the post-pagination filter and stays in the record selector, which preserves the existing behaviour: the
3484        # selector runs inside the page loop, so the records the condition rejects never reach the paginator's
3485        # accounting. Moving it downstream would start counting them.
3486        post_pagination_filter = (
3487            ClientSideIncrementalRecordFilterDecorator(
3488                config=config,
3489                parameters=model.parameters or {},
3490                condition=None,
3491                cursor=cursor,
3492            )
3493            if has_stop_condition_cursor
3494            else None
3495        )
3496        client_side_incremental_cursor = (
3497            cursor if is_client_side_incremental_sync and not post_pagination_filter else None
3498        )
3499        if post_pagination_filter and is_client_side_incremental_sync:
3500            LOGGER.warning(
3501                f"Stream {name}: `is_client_side_incremental` adds no record filtering when `is_data_feed` is set, "
3502                "as a data feed already filters on the cursor value. It still makes the record selector apply the "
3503                "transformations before the `record_filter` condition."
3504            )
3505
3506        decoder = (
3507            self._create_component_from_model(model=model.decoder, config=config)
3508            if model.decoder
3509            else JsonDecoder(parameters={})
3510        )
3511        record_selector = self._create_component_from_model(
3512            model=model.record_selector,
3513            name=name,
3514            config=config,
3515            decoder=decoder,
3516            transformations=transformations,
3517            client_side_incremental_sync_cursor=client_side_incremental_cursor,
3518            is_client_side_incremental_sync=is_client_side_incremental_sync,
3519            file_uploader=file_uploader,
3520        )
3521
3522        query_properties: Optional[QueryProperties] = None
3523        query_properties_key: Optional[str] = None
3524        self._ensure_query_properties_to_model(model.requester)
3525        if self._has_query_properties_in_request_parameters(model.requester):
3526            # It is better to be explicit about an error if PropertiesFromEndpoint is defined in multiple
3527            # places instead of default to request_parameters which isn't clearly documented
3528            if (
3529                hasattr(model.requester, "fetch_properties_from_endpoint")
3530                and model.requester.fetch_properties_from_endpoint
3531            ):
3532                raise ValueError(
3533                    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"
3534                )
3535
3536            query_properties_definitions = []
3537            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()
3538                if isinstance(request_parameter, QueryPropertiesModel):
3539                    query_properties_key = key
3540                    query_properties_definitions.append(request_parameter)
3541
3542            if len(query_properties_definitions) > 1:
3543                raise ValueError(
3544                    f"request_parameters only supports defining one QueryProperties field, but found {len(query_properties_definitions)} usages"
3545                )
3546
3547            if len(query_properties_definitions) == 1:
3548                query_properties = self._create_component_from_model(
3549                    model=query_properties_definitions[0], stream_name=name, config=config
3550                )
3551
3552            # Removes QueryProperties components from the interpolated mappings because it has been designed
3553            # to be used by the SimpleRetriever and will be resolved from the provider from the slice directly
3554            # instead of through jinja interpolation
3555            if hasattr(model.requester, "request_parameters") and isinstance(
3556                model.requester.request_parameters, Mapping
3557            ):
3558                model.requester.request_parameters = self._remove_query_properties(
3559                    model.requester.request_parameters
3560                )
3561        elif (
3562            hasattr(model.requester, "fetch_properties_from_endpoint")
3563            and model.requester.fetch_properties_from_endpoint
3564        ):
3565            # todo: Deprecate this condition once dependent connectors migrate to query_properties
3566            query_properties_definition = QueryPropertiesModel(
3567                type="QueryProperties",
3568                property_list=model.requester.fetch_properties_from_endpoint,
3569                always_include_properties=None,
3570                property_chunking=None,
3571            )  # type: ignore # $parameters has a default value
3572
3573            query_properties = self.create_query_properties(
3574                model=query_properties_definition,
3575                stream_name=name,
3576                config=config,
3577            )
3578        elif hasattr(model.requester, "query_properties") and model.requester.query_properties:
3579            query_properties = self.create_query_properties(
3580                model=model.requester.query_properties,
3581                stream_name=name,
3582                config=config,
3583            )
3584
3585        requester = self._create_component_from_model(
3586            model=model.requester,
3587            decoder=decoder,
3588            name=name,
3589            query_properties_key=query_properties_key,
3590            use_cache=use_cache,
3591            config=config,
3592        )
3593
3594        if not request_options_provider:
3595            request_options_provider = DefaultRequestOptionsProvider(parameters={})
3596        if isinstance(request_options_provider, DefaultRequestOptionsProvider) and isinstance(
3597            partition_router, PartitionRouter
3598        ):
3599            request_options_provider = partition_router
3600
3601        paginator = (
3602            self._create_component_from_model(
3603                model=model.paginator,
3604                config=config,
3605                url_base=_get_url(requester),
3606                extractor_model=model.record_selector.extractor,
3607                decoder=decoder,
3608                cursor_used_for_stop_condition=cursor if has_stop_condition_cursor else None,
3609            )
3610            if model.paginator
3611            else NoPagination(parameters={})
3612        )
3613
3614        ignore_stream_slicer_parameters_on_paginated_requests = (
3615            model.ignore_stream_slicer_parameters_on_paginated_requests or False
3616        )
3617
3618        if (
3619            model.partition_router
3620            and isinstance(model.partition_router, SubstreamPartitionRouterModel)
3621            and not bool(self._connector_state_manager.get_stream_state(name, None))
3622            and any(
3623                parent_stream_config.lazy_read_pointer
3624                for parent_stream_config in model.partition_router.parent_stream_configs
3625            )
3626        ):
3627            if incremental_sync:
3628                if incremental_sync.type != "DatetimeBasedCursor":
3629                    raise ValueError(
3630                        f"LazySimpleRetriever only supports DatetimeBasedCursor. Found: {incremental_sync.type}."
3631                    )
3632
3633                elif incremental_sync.step or incremental_sync.cursor_granularity:
3634                    raise ValueError(
3635                        f"Found more that one slice per parent. LazySimpleRetriever only supports single slice read for stream - {name}."
3636                    )
3637
3638            if model.decoder and model.decoder.type != "JsonDecoder":
3639                raise ValueError(
3640                    f"LazySimpleRetriever only supports JsonDecoder. Found: {model.decoder.type}."
3641                )
3642
3643            return LazySimpleRetriever(
3644                name=name,
3645                paginator=paginator,
3646                primary_key=primary_key,
3647                requester=requester,
3648                record_selector=record_selector,
3649                stream_slicer=_NO_STREAM_SLICING,
3650                request_option_provider=request_options_provider,
3651                config=config,
3652                ignore_stream_slicer_parameters_on_paginated_requests=ignore_stream_slicer_parameters_on_paginated_requests,
3653                post_pagination_filter=post_pagination_filter,
3654                parameters=model.parameters or {},
3655            )
3656
3657        if (
3658            model.record_selector.record_filter
3659            and model.pagination_reset
3660            and model.pagination_reset.limits
3661        ):
3662            raise ValueError("PaginationResetLimits are not supported while having record filter.")
3663
3664        return SimpleRetriever(
3665            name=name,
3666            paginator=paginator,
3667            primary_key=primary_key,
3668            requester=requester,
3669            record_selector=record_selector,
3670            stream_slicer=_NO_STREAM_SLICING,
3671            request_option_provider=request_options_provider,
3672            config=config,
3673            ignore_stream_slicer_parameters_on_paginated_requests=ignore_stream_slicer_parameters_on_paginated_requests,
3674            additional_query_properties=query_properties,
3675            log_formatter=self._get_log_formatter(log_formatter, name),
3676            pagination_tracker_factory=self._create_pagination_tracker_factory(
3677                model.pagination_reset, cursor
3678            ),
3679            post_pagination_filter=post_pagination_filter,
3680            parameters=model.parameters or {},
3681        )
3682
3683    def _create_pagination_tracker_factory(
3684        self, model: Optional[PaginationResetModel], cursor: Cursor
3685    ) -> Callable[[], PaginationTracker]:
3686        if model is None:
3687            return lambda: PaginationTracker()
3688
3689        # Until we figure out a way to use any cursor for PaginationTracker, we will have to have this cursor selector logic
3690        cursor_factory: Callable[[], Optional[ConcurrentCursor]] = lambda: None
3691        if model.action == PaginationResetActionModel.RESET:
3692            # in that case, we will let cursor_factory to return None even if the stream has a cursor
3693            pass
3694        elif model.action == PaginationResetActionModel.SPLIT_USING_CURSOR:
3695            if isinstance(cursor, ConcurrentCursor):
3696                cursor_factory = lambda: cursor.copy_without_state()  # type: ignore  # the if condition validates that it is a ConcurrentCursor
3697            elif isinstance(cursor, ConcurrentPerPartitionCursor):
3698                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
3699                    {}, datetime.timedelta(0)
3700                )
3701            elif not isinstance(cursor, FinalStateCursor):
3702                LOGGER.warning(
3703                    "Unknown cursor for PaginationTracker. Pagination resets might not work properly"
3704                )
3705        else:
3706            raise ValueError(f"Unknown PaginationReset action: {model.action}")
3707
3708        limit = model.limits.number_of_records if model and model.limits else None
3709        return lambda: PaginationTracker(cursor_factory(), limit)
3710
3711    def _get_log_formatter(
3712        self, log_formatter: Callable[[Response], Any] | None, name: str
3713    ) -> Callable[[Response], Any] | None:
3714        if self._should_limit_slices_fetched():
3715            return (
3716                (
3717                    lambda response: format_http_message(
3718                        response,
3719                        f"Stream '{name}' request",
3720                        f"Request performed in order to extract records for stream '{name}'",
3721                        name,
3722                    )
3723                )
3724                if not log_formatter
3725                else log_formatter
3726            )
3727        return None
3728
3729    def _should_limit_slices_fetched(self) -> bool:
3730        """
3731        Returns True if the number of slices fetched should be limited, False otherwise.
3732        This is used to limit the number of slices fetched during tests.
3733        """
3734        return bool(self._limit_slices_fetched or self._emit_connector_builder_messages)
3735
3736    @staticmethod
3737    def _has_query_properties_in_request_parameters(
3738        requester: Union[HttpRequesterModel, CustomRequesterModel],
3739    ) -> bool:
3740        if not hasattr(requester, "request_parameters"):
3741            return False
3742        request_parameters = requester.request_parameters
3743        if request_parameters and isinstance(request_parameters, Mapping):
3744            for request_parameter in request_parameters.values():
3745                if isinstance(request_parameter, QueryPropertiesModel):
3746                    return True
3747        return False
3748
3749    @staticmethod
3750    def _remove_query_properties(
3751        request_parameters: Mapping[str, Union[str, QueryPropertiesModel]],
3752    ) -> Mapping[str, str]:
3753        return {
3754            parameter_field: request_parameter
3755            for parameter_field, request_parameter in request_parameters.items()
3756            if not isinstance(request_parameter, QueryPropertiesModel)
3757        }
3758
3759    def create_state_delegating_stream(
3760        self,
3761        model: StateDelegatingStreamModel,
3762        config: Config,
3763        **kwargs: Any,
3764    ) -> DefaultStream:
3765        if (
3766            model.full_refresh_stream.name != model.name
3767            or model.name != model.incremental_stream.name
3768        ):
3769            raise ValueError(
3770                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}."
3771            )
3772
3773        # Resolve api_retention_period with config context (supports Jinja2 interpolation)
3774        resolved_retention_period: Optional[str] = None
3775        if model.api_retention_period:
3776            interpolated_retention = InterpolatedString.create(
3777                model.api_retention_period, parameters=model.parameters or {}
3778            )
3779            resolved_value = interpolated_retention.eval(config=config)
3780            if resolved_value:
3781                resolved_retention_period = str(resolved_value)
3782
3783        if resolved_retention_period:
3784            for stream_model in (model.full_refresh_stream, model.incremental_stream):
3785                if isinstance(stream_model.incremental_sync, IncrementingCountCursorModel):
3786                    raise ValueError(
3787                        f"Stream '{model.name}' uses IncrementingCountCursor which is not supported "
3788                        f"with api_retention_period. IncrementingCountCursor does not use datetime-based "
3789                        f"cursors, so cursor age validation cannot be performed."
3790                    )
3791
3792        stream_state = self._connector_state_manager.get_stream_state(model.name, None)
3793
3794        if not stream_state:
3795            return self._create_component_from_model(  # type: ignore[no-any-return]
3796                model.full_refresh_stream, config=config, **kwargs
3797            )
3798
3799        incremental_stream: DefaultStream = self._create_component_from_model(
3800            model.incremental_stream, config=config, **kwargs
3801        )  # type: ignore[assignment]
3802
3803        # Only run cursor age validation for streams that are in the configured
3804        # catalog (or when no catalog was provided, e.g. during discover / connector
3805        # builder).  Streams not selected by the user but instantiated as parent-stream
3806        # dependencies must not go through this path because it emits state messages
3807        # that the destination does not know about, causing "Stream not found" crashes.
3808        stream_is_in_catalog = (
3809            not self._stream_name_to_configured_stream  # no catalog → validate by default
3810            or model.name in self._stream_name_to_configured_stream
3811        )
3812        if resolved_retention_period and stream_is_in_catalog:
3813            full_refresh_stream: DefaultStream = self._create_component_from_model(
3814                model.full_refresh_stream, config=config, **kwargs
3815            )  # type: ignore[assignment]
3816            if self._is_cursor_older_than_retention_period(
3817                stream_state,
3818                full_refresh_stream.cursor,
3819                incremental_stream.cursor,
3820                resolved_retention_period,
3821                model.name,
3822            ):
3823                # Clear state BEFORE constructing the full_refresh_stream so that
3824                # its cursor starts from start_date instead of the stale cursor.
3825                self._connector_state_manager.update_state_for_stream(model.name, None, {})
3826                state_message = self._connector_state_manager.create_state_message(model.name, None)
3827                self._message_repository.emit_message(state_message)
3828                return self._create_component_from_model(  # type: ignore[no-any-return]
3829                    model.full_refresh_stream, config=config, **kwargs
3830                )
3831
3832        return incremental_stream
3833
3834    @staticmethod
3835    def _is_cursor_older_than_retention_period(
3836        stream_state: Mapping[str, Any],
3837        full_refresh_cursor: Cursor,
3838        incremental_cursor: Cursor,
3839        api_retention_period: str,
3840        stream_name: str,
3841    ) -> bool:
3842        """Check if the cursor value in the state is older than the API's retention period.
3843
3844        Checks cursors in sequence: full refresh cursor first, then incremental cursor.
3845        FinalStateCursor returns now() for completed full refresh state (NO_CURSOR_STATE_KEY),
3846        which is always within retention, so we use incremental. For other states, it returns
3847        None and we fall back to checking the incremental cursor.
3848
3849        Returns True if the cursor is older than the retention period (should use full refresh).
3850        Returns False if the cursor is within the retention period (safe to use incremental).
3851        """
3852        retention_duration = parse_duration(api_retention_period)
3853        retention_cutoff = datetime.datetime.now(datetime.timezone.utc) - retention_duration
3854
3855        # Check full refresh cursor first
3856        cursor_datetime = full_refresh_cursor.get_cursor_datetime_from_state(stream_state)
3857
3858        # If full refresh cursor returns None, check incremental cursor
3859        if cursor_datetime is None:
3860            cursor_datetime = incremental_cursor.get_cursor_datetime_from_state(stream_state)
3861
3862        if cursor_datetime is None:
3863            # Neither cursor could parse the state - fall back to full refresh to be safe
3864            return True
3865
3866        if cursor_datetime < retention_cutoff:
3867            logging.warning(
3868                f"Stream '{stream_name}' has a cursor value older than "
3869                f"the API's retention period of {api_retention_period} "
3870                f"(cutoff: {retention_cutoff.isoformat()}). "
3871                f"Falling back to full refresh to avoid data loss."
3872            )
3873            return True
3874
3875        return False
3876
3877    def _get_state_delegating_stream_model(
3878        self,
3879        model: StateDelegatingStreamModel,
3880        parent_state: Optional[Mapping[str, Any]] = None,
3881    ) -> DeclarativeStreamModel:
3882        """Return the appropriate underlying stream model based on state."""
3883        return (
3884            model.incremental_stream
3885            if self._connector_state_manager.get_stream_state(model.name, None) or parent_state
3886            else model.full_refresh_stream
3887        )
3888
3889    _OPTIONAL_ASYNC_STATUS_FIELDS = {"skipped"}
3890
3891    def _create_async_job_status_mapping(
3892        self, model: AsyncJobStatusMapModel, config: Config, **kwargs: Any
3893    ) -> Mapping[str, AsyncJobStatus]:
3894        api_status_to_cdk_status = {}
3895        for cdk_status, api_statuses in model.dict().items():
3896            if cdk_status == "type":
3897                # This is an element of the dict because of the typing of the CDK but it is not a CDK status
3898                continue
3899
3900            if api_statuses is None:
3901                if cdk_status in self._OPTIONAL_ASYNC_STATUS_FIELDS:
3902                    continue
3903                raise ValueError(
3904                    f"Required CDK status '{cdk_status}' has no API statuses mapped. "
3905                    f"Please provide at least an empty list for required status fields."
3906                )
3907
3908            for status in api_statuses:
3909                if status in api_status_to_cdk_status:
3910                    raise ValueError(
3911                        f"API status {status} is already set for CDK status {cdk_status}. Please ensure API statuses are only provided once"
3912                    )
3913                api_status_to_cdk_status[status] = self._get_async_job_status(cdk_status)
3914        return api_status_to_cdk_status
3915
3916    def _get_async_job_status(self, status: str) -> AsyncJobStatus:
3917        match status:
3918            case "running":
3919                return AsyncJobStatus.RUNNING
3920            case "completed":
3921                return AsyncJobStatus.COMPLETED
3922            case "failed":
3923                return AsyncJobStatus.FAILED
3924            case "timeout":
3925                return AsyncJobStatus.TIMED_OUT
3926            case "skipped":
3927                return AsyncJobStatus.SKIPPED
3928            case _:
3929                raise ValueError(f"Unsupported CDK status {status}")
3930
3931    def create_async_retriever(
3932        self,
3933        model: AsyncRetrieverModel,
3934        config: Config,
3935        *,
3936        name: str,
3937        primary_key: Optional[
3938            Union[str, List[str], List[List[str]]]
3939        ],  # this seems to be needed to match create_simple_retriever
3940        stream_slicer: Optional[StreamSlicer],
3941        client_side_incremental_sync: Optional[Dict[str, Any]] = None,
3942        transformations: List[RecordTransformation],
3943        **kwargs: Any,
3944    ) -> AsyncRetriever:
3945        if model.download_target_requester and not model.download_target_extractor:
3946            raise ValueError(
3947                f"`download_target_extractor` required if using a `download_target_requester`"
3948            )
3949
3950        def _get_download_retriever(
3951            requester: Requester, extractor: RecordExtractor, _decoder: Decoder
3952        ) -> SimpleRetriever:
3953            # We create a record selector for the download retriever
3954            # with no schema normalization and no transformations, neither record filter
3955            # as all this occurs in the record_selector of the AsyncRetriever
3956            record_selector = RecordSelector(
3957                extractor=extractor,
3958                name=name,
3959                record_filter=None,
3960                transformations=[],
3961                schema_normalization=TypeTransformer(TransformConfig.NoTransform),
3962                config=config,
3963                parameters={},
3964            )
3965            paginator = (
3966                self._create_component_from_model(
3967                    model=model.download_paginator,
3968                    decoder=_decoder,
3969                    config=config,
3970                    url_base="",
3971                )
3972                if model.download_paginator
3973                else NoPagination(parameters={})
3974            )
3975
3976            return SimpleRetriever(
3977                requester=requester,
3978                record_selector=record_selector,
3979                primary_key=None,
3980                name=name,
3981                paginator=paginator,
3982                config=config,
3983                parameters={},
3984                log_formatter=self._get_log_formatter(None, name),
3985            )
3986
3987        def _get_job_timeout() -> datetime.timedelta:
3988            user_defined_timeout: Optional[int] = (
3989                int(
3990                    InterpolatedString.create(
3991                        str(model.polling_job_timeout),
3992                        parameters={},
3993                    ).eval(config)
3994                )
3995                if model.polling_job_timeout
3996                else None
3997            )
3998
3999            # check for user defined timeout during the test read or 15 minutes
4000            test_read_timeout = datetime.timedelta(minutes=user_defined_timeout or 15)
4001            # default value for non-connector builder is 60 minutes.
4002            default_sync_timeout = datetime.timedelta(minutes=user_defined_timeout or 60)
4003
4004            return (
4005                test_read_timeout if self._emit_connector_builder_messages else default_sync_timeout
4006            )
4007
4008        decoder = (
4009            self._create_component_from_model(model=model.decoder, config=config)
4010            if model.decoder
4011            else JsonDecoder(parameters={})
4012        )
4013        record_selector = self._create_component_from_model(
4014            model=model.record_selector,
4015            config=config,
4016            decoder=decoder,
4017            name=name,
4018            transformations=transformations,
4019            client_side_incremental_sync=client_side_incremental_sync,
4020        )
4021
4022        stream_slicer = stream_slicer or SinglePartitionRouter(parameters={})
4023        if self._should_limit_slices_fetched():
4024            stream_slicer = cast(
4025                StreamSlicer,
4026                StreamSlicerTestReadDecorator(
4027                    wrapped_slicer=stream_slicer,
4028                    maximum_number_of_slices=self._limit_slices_fetched or 5,
4029                ),
4030            )
4031
4032        creation_requester = self._create_component_from_model(
4033            model=model.creation_requester,
4034            decoder=decoder,
4035            config=config,
4036            name=f"job creation - {name}",
4037        )
4038        polling_requester = self._create_component_from_model(
4039            model=model.polling_requester,
4040            decoder=decoder,
4041            config=config,
4042            name=f"job polling - {name}",
4043        )
4044        job_download_components_name = f"job download - {name}"
4045        download_decoder = (
4046            self._create_component_from_model(model=model.download_decoder, config=config)
4047            if model.download_decoder
4048            else JsonDecoder(parameters={})
4049        )
4050        download_extractor = (
4051            self._create_component_from_model(
4052                model=model.download_extractor,
4053                config=config,
4054                decoder=download_decoder,
4055                parameters=model.parameters,
4056            )
4057            if model.download_extractor
4058            else DpathExtractor(
4059                [],
4060                config=config,
4061                decoder=download_decoder,
4062                parameters=model.parameters or {},
4063            )
4064        )
4065        download_requester = self._create_component_from_model(
4066            model=model.download_requester,
4067            decoder=download_decoder,
4068            config=config,
4069            name=job_download_components_name,
4070        )
4071        download_retriever = _get_download_retriever(
4072            download_requester, download_extractor, download_decoder
4073        )
4074        abort_requester = (
4075            self._create_component_from_model(
4076                model=model.abort_requester,
4077                decoder=decoder,
4078                config=config,
4079                name=f"job abort - {name}",
4080            )
4081            if model.abort_requester
4082            else None
4083        )
4084        delete_requester = (
4085            self._create_component_from_model(
4086                model=model.delete_requester,
4087                decoder=decoder,
4088                config=config,
4089                name=f"job delete - {name}",
4090            )
4091            if model.delete_requester
4092            else None
4093        )
4094        download_target_requester = (
4095            self._create_component_from_model(
4096                model=model.download_target_requester,
4097                decoder=decoder,
4098                config=config,
4099                name=f"job extract_url - {name}",
4100            )
4101            if model.download_target_requester
4102            else None
4103        )
4104        status_extractor = self._create_component_from_model(
4105            model=model.status_extractor, decoder=decoder, config=config, name=name
4106        )
4107        download_target_extractor = (
4108            self._create_component_from_model(
4109                model=model.download_target_extractor,
4110                decoder=decoder,
4111                config=config,
4112                name=name,
4113            )
4114            if model.download_target_extractor
4115            else None
4116        )
4117
4118        job_repository: AsyncJobRepository = AsyncHttpJobRepository(
4119            creation_requester=creation_requester,
4120            polling_requester=polling_requester,
4121            download_retriever=download_retriever,
4122            download_target_requester=download_target_requester,
4123            abort_requester=abort_requester,
4124            delete_requester=delete_requester,
4125            status_extractor=status_extractor,
4126            status_mapping=self._create_async_job_status_mapping(model.status_mapping, config),
4127            download_target_extractor=download_target_extractor,
4128            job_timeout=_get_job_timeout(),
4129        )
4130
4131        failed_retry_wait_time_in_seconds: Optional[int] = (
4132            int(
4133                InterpolatedString.create(
4134                    str(model.failed_retry_wait_time_in_seconds),
4135                    parameters={},
4136                ).eval(config)
4137            )
4138            if model.failed_retry_wait_time_in_seconds
4139            else None
4140        )
4141
4142        async_job_partition_router = AsyncJobPartitionRouter(
4143            job_orchestrator_factory=lambda stream_slices: AsyncJobOrchestrator(
4144                job_repository,
4145                stream_slices,
4146                self._job_tracker,
4147                self._message_repository,
4148                # FIXME work would need to be done here in order to detect if a stream as a parent stream that is bulk
4149                has_bulk_parent=False,
4150                # set the `job_max_retry` to 1 for the `Connector Builder`` use-case.
4151                # `None` == default retry is set to 3 attempts, under the hood.
4152                job_max_retry=1 if self._emit_connector_builder_messages else None,
4153                failed_retry_wait_time_in_seconds=failed_retry_wait_time_in_seconds,
4154            ),
4155            stream_slicer=stream_slicer,
4156            config=config,
4157            parameters=model.parameters or {},
4158        )
4159
4160        return AsyncRetriever(
4161            record_selector=record_selector,
4162            stream_slicer=async_job_partition_router,
4163            config=config,
4164            parameters=model.parameters or {},
4165        )
4166
4167    def create_spec(self, model: SpecModel, config: Config, **kwargs: Any) -> Spec:
4168        config_migrations = [
4169            self._create_component_from_model(migration, config)
4170            for migration in (
4171                model.config_normalization_rules.config_migrations
4172                if (
4173                    model.config_normalization_rules
4174                    and model.config_normalization_rules.config_migrations
4175                )
4176                else []
4177            )
4178        ]
4179        config_transformations = [
4180            self._create_component_from_model(transformation, config)
4181            for transformation in (
4182                model.config_normalization_rules.transformations
4183                if (
4184                    model.config_normalization_rules
4185                    and model.config_normalization_rules.transformations
4186                )
4187                else []
4188            )
4189        ]
4190        config_validations = [
4191            self._create_component_from_model(validation, config)
4192            for validation in (
4193                model.config_normalization_rules.validations
4194                if (
4195                    model.config_normalization_rules
4196                    and model.config_normalization_rules.validations
4197                )
4198                else []
4199            )
4200        ]
4201
4202        return Spec(
4203            connection_specification=model.connection_specification,
4204            documentation_url=model.documentation_url,
4205            advanced_auth=model.advanced_auth,
4206            parameters={},
4207            config_migrations=config_migrations,
4208            config_transformations=config_transformations,
4209            config_validations=config_validations,
4210        )
4211
4212    def create_substream_partition_router(
4213        self,
4214        model: SubstreamPartitionRouterModel,
4215        config: Config,
4216        *,
4217        stream_name: str,
4218        **kwargs: Any,
4219    ) -> SubstreamPartitionRouter:
4220        parent_stream_configs = []
4221        if model.parent_stream_configs:
4222            parent_stream_configs.extend(
4223                [
4224                    self.create_parent_stream_config_with_substream_wrapper(
4225                        model=parent_stream_config, config=config, stream_name=stream_name, **kwargs
4226                    )
4227                    for parent_stream_config in model.parent_stream_configs
4228                ]
4229            )
4230
4231        return SubstreamPartitionRouter(
4232            parent_stream_configs=parent_stream_configs,
4233            parameters=model.parameters or {},
4234            config=config,
4235        )
4236
4237    def create_parent_stream_config_with_substream_wrapper(
4238        self, model: ParentStreamConfigModel, config: Config, *, stream_name: str, **kwargs: Any
4239    ) -> Any:
4240        child_state = self._connector_state_manager.get_stream_state(stream_name, None)
4241        if NO_CURSOR_STATE_KEY in child_state:
4242            # Full refresh streams checkpoint a `{NO_CURSOR_STATE_KEY: true}` sentinel. When such a
4243            # stream is later converted to incremental with an incremental_dependency parent,
4244            # `_instantiate_parent_stream_state_manager` would treat the sentinel's boolean as a legacy
4245            # cursor value and re-key it under the parent's cursor field, crashing cursor initialization.
4246            child_state = {
4247                key: value for key, value in child_state.items() if key != NO_CURSOR_STATE_KEY
4248            }
4249
4250        parent_state: Optional[Mapping[str, Any]] = (
4251            child_state if model.incremental_dependency and child_state else None
4252        )
4253        connector_state_manager = self._instantiate_parent_stream_state_manager(
4254            child_state, config, model, parent_state
4255        )
4256
4257        substream_factory = ModelToComponentFactory(
4258            custom_components_trusted=self._custom_components_trusted,
4259            connector_state_manager=connector_state_manager,
4260            limit_pages_fetched_per_slice=self._limit_pages_fetched_per_slice,
4261            limit_slices_fetched=self._limit_slices_fetched,
4262            emit_connector_builder_messages=self._emit_connector_builder_messages,
4263            disable_retries=self._disable_retries,
4264            disable_cache=self._disable_cache,
4265            message_repository=StateFilteringMessageRepository(
4266                LogAppenderMessageRepositoryDecorator(
4267                    {
4268                        "airbyte_cdk": {"stream": {"is_substream": True}},
4269                        "http": {"is_auxiliary": True},
4270                    },
4271                    self._message_repository,
4272                    self._evaluate_log_level(self._emit_connector_builder_messages),
4273                ),
4274            ),
4275            api_budget=self._api_budget,
4276            # Share the authenticator registry so parent and child streams draw from the
4277            # same token quota counters
4278            rate_limited_authenticators=self._rate_limited_authenticators,
4279        )
4280
4281        return substream_factory.create_parent_stream_config(
4282            model=model, config=config, stream_name=stream_name, **kwargs
4283        )
4284
4285    def _instantiate_parent_stream_state_manager(
4286        self,
4287        child_state: MutableMapping[str, Any],
4288        config: Config,
4289        model: ParentStreamConfigModel,
4290        parent_state: Optional[Mapping[str, Any]] = None,
4291    ) -> ConnectorStateManager:
4292        """
4293        With DefaultStream, the state needs to be provided during __init__ of the cursor as opposed to the
4294        `set_initial_state` flow that existed for the declarative cursors. This state is taken from
4295        self._connector_state_manager.get_stream_state (`self` being a newly created ModelToComponentFactory to account
4296        for the MessageRepository being different). So we need to pass a ConnectorStateManager to the
4297        ModelToComponentFactory that has the parent states. This method populates this if there is a child state and if
4298        incremental_dependency is set.
4299        """
4300        if model.incremental_dependency and child_state:
4301            parent_stream_name = model.stream.name or ""
4302            extracted_parent_state = ConcurrentPerPartitionCursor.get_parent_state(
4303                child_state, parent_stream_name
4304            )
4305
4306            if not extracted_parent_state:
4307                extracted_parent_state = ConcurrentPerPartitionCursor.get_global_state(
4308                    child_state, parent_stream_name
4309                )
4310
4311                if not extracted_parent_state and not isinstance(extracted_parent_state, dict):
4312                    cursor_values = child_state.values()
4313                    if cursor_values and len(cursor_values) == 1:
4314                        incremental_sync_model: Union[
4315                            DatetimeBasedCursorModel,
4316                            IncrementingCountCursorModel,
4317                        ] = (
4318                            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
4319                            if isinstance(model.stream, DeclarativeStreamModel)
4320                            else self._get_state_delegating_stream_model(
4321                                model.stream, parent_state=parent_state
4322                            ).incremental_sync
4323                        )
4324                        cursor_field = InterpolatedString.create(
4325                            incremental_sync_model.cursor_field,
4326                            parameters=incremental_sync_model.parameters or {},
4327                        ).eval(config)
4328                        extracted_parent_state = AirbyteStateMessage(
4329                            type=AirbyteStateType.STREAM,
4330                            stream=AirbyteStreamState(
4331                                stream_descriptor=StreamDescriptor(
4332                                    name=parent_stream_name, namespace=None
4333                                ),
4334                                stream_state=AirbyteStateBlob(
4335                                    {cursor_field: list(cursor_values)[0]}
4336                                ),
4337                            ),
4338                        )
4339            return ConnectorStateManager([extracted_parent_state] if extracted_parent_state else [])
4340
4341        return ConnectorStateManager([])
4342
4343    @staticmethod
4344    def create_wait_time_from_header(
4345        model: WaitTimeFromHeaderModel, config: Config, **kwargs: Any
4346    ) -> WaitTimeFromHeaderBackoffStrategy:
4347        return WaitTimeFromHeaderBackoffStrategy(
4348            header=model.header,
4349            parameters=model.parameters or {},
4350            config=config,
4351            regex=model.regex,
4352            max_waiting_time_in_seconds=model.max_waiting_time_in_seconds,
4353        )
4354
4355    @staticmethod
4356    def create_wait_until_time_from_header(
4357        model: WaitUntilTimeFromHeaderModel, config: Config, **kwargs: Any
4358    ) -> WaitUntilTimeFromHeaderBackoffStrategy:
4359        return WaitUntilTimeFromHeaderBackoffStrategy(
4360            header=model.header,
4361            parameters=model.parameters or {},
4362            config=config,
4363            min_wait=model.min_wait,
4364            regex=model.regex,
4365            max_waiting_time_in_seconds=model.max_waiting_time_in_seconds,
4366        )
4367
4368    def get_message_repository(self) -> MessageRepository:
4369        return self._message_repository
4370
4371    def _evaluate_log_level(self, emit_connector_builder_messages: bool) -> Level:
4372        return Level.DEBUG if emit_connector_builder_messages else Level.INFO
4373
4374    @staticmethod
4375    def create_components_mapping_definition(
4376        model: ComponentMappingDefinitionModel, config: Config, **kwargs: Any
4377    ) -> ComponentMappingDefinition:
4378        interpolated_value = InterpolatedString.create(
4379            model.value, parameters=model.parameters or {}
4380        )
4381        field_path = [
4382            InterpolatedString.create(path, parameters=model.parameters or {})
4383            for path in model.field_path
4384        ]
4385        return ComponentMappingDefinition(
4386            field_path=field_path,  # type: ignore[arg-type] # field_path can be str and InterpolatedString
4387            value=interpolated_value,
4388            value_type=ModelToComponentFactory._json_schema_type_name_to_type(model.value_type),
4389            create_or_update=model.create_or_update,
4390            condition=model.condition,
4391            parameters=model.parameters or {},
4392        )
4393
4394    def create_http_components_resolver(
4395        self, model: HttpComponentsResolverModel, config: Config, stream_name: Optional[str] = None
4396    ) -> Any:
4397        retriever = self._create_component_from_model(
4398            model=model.retriever,
4399            config=config,
4400            name=f"{stream_name if stream_name else '__http_components_resolver'}",
4401            primary_key=None,
4402            stream_slicer=self._build_stream_slicer_from_partition_router(model.retriever, config),
4403            transformations=[],
4404        )
4405
4406        components_mapping = []
4407        for component_mapping_definition_model in model.components_mapping:
4408            if component_mapping_definition_model.condition:
4409                raise ValueError("`condition` is only supported for     `ConfigComponentsResolver`")
4410            components_mapping.append(
4411                self._create_component_from_model(
4412                    model=component_mapping_definition_model,
4413                    value_type=ModelToComponentFactory._json_schema_type_name_to_type(
4414                        component_mapping_definition_model.value_type
4415                    ),
4416                    config=config,
4417                )
4418            )
4419
4420        return HttpComponentsResolver(
4421            retriever=retriever,
4422            stream_slicer=self._build_stream_slicer_from_partition_router(model.retriever, config),
4423            config=config,
4424            components_mapping=components_mapping,
4425            parameters=model.parameters or {},
4426        )
4427
4428    @staticmethod
4429    def create_stream_config(
4430        model: StreamConfigModel, config: Config, **kwargs: Any
4431    ) -> StreamConfig:
4432        model_configs_pointer: List[Union[InterpolatedString, str]] = (
4433            [x for x in model.configs_pointer] if model.configs_pointer else []
4434        )
4435
4436        return StreamConfig(
4437            configs_pointer=model_configs_pointer,
4438            default_values=model.default_values,
4439            parameters=model.parameters or {},
4440        )
4441
4442    def create_config_components_resolver(
4443        self,
4444        model: ConfigComponentsResolverModel,
4445        config: Config,
4446    ) -> Any:
4447        model_stream_configs = (
4448            model.stream_config if isinstance(model.stream_config, list) else [model.stream_config]
4449        )
4450
4451        stream_configs = [
4452            self._create_component_from_model(
4453                stream_config, config=config, parameters=model.parameters or {}
4454            )
4455            for stream_config in model_stream_configs
4456        ]
4457
4458        components_mapping = [
4459            self._create_component_from_model(
4460                model=components_mapping_definition_model,
4461                value_type=ModelToComponentFactory._json_schema_type_name_to_type(
4462                    components_mapping_definition_model.value_type
4463                ),
4464                config=config,
4465                parameters=model.parameters,
4466            )
4467            for components_mapping_definition_model in model.components_mapping
4468        ]
4469
4470        return ConfigComponentsResolver(
4471            stream_configs=stream_configs,
4472            config=config,
4473            components_mapping=components_mapping,
4474            parameters=model.parameters or {},
4475        )
4476
4477    def create_parametrized_components_resolver(
4478        self,
4479        model: ParametrizedComponentsResolverModel,
4480        config: Config,
4481    ) -> ParametrizedComponentsResolver:
4482        stream_parameters = StreamParametersDefinition(
4483            list_of_parameters_for_stream=model.stream_parameters.list_of_parameters_for_stream
4484        )
4485
4486        components_mapping = []
4487        for components_mapping_definition_model in model.components_mapping:
4488            if components_mapping_definition_model.condition:
4489                raise ValueError("`condition` is only supported for `ConfigComponentsResolver`")
4490            components_mapping.append(
4491                self._create_component_from_model(
4492                    model=components_mapping_definition_model,
4493                    value_type=ModelToComponentFactory._json_schema_type_name_to_type(
4494                        components_mapping_definition_model.value_type
4495                    ),
4496                    config=config,
4497                )
4498            )
4499        return ParametrizedComponentsResolver(
4500            stream_parameters=stream_parameters,
4501            config=config,
4502            components_mapping=components_mapping,
4503            parameters=model.parameters or {},
4504        )
4505
4506    _UNSUPPORTED_DECODER_ERROR = (
4507        "Specified decoder of {decoder_type} is not supported for pagination."
4508        "Please set as `JsonDecoder`, `XmlDecoder`, or a `CompositeRawDecoder` with an inner_parser of `JsonParser` or `GzipParser` instead."
4509        "If using `GzipParser`, please ensure that the lowest level inner_parser is a `JsonParser`."
4510    )
4511
4512    def _is_supported_decoder_for_pagination(self, decoder: Decoder) -> bool:
4513        if isinstance(decoder, (JsonDecoder, XmlDecoder)):
4514            return True
4515        elif isinstance(decoder, CompositeRawDecoder):
4516            return self._is_supported_parser_for_pagination(decoder.parser)
4517        else:
4518            return False
4519
4520    def _is_supported_parser_for_pagination(self, parser: Parser) -> bool:
4521        if isinstance(parser, JsonParser):
4522            return True
4523        elif isinstance(parser, GzipParser):
4524            return isinstance(parser.inner_parser, JsonParser)
4525        else:
4526            return False
4527
4528    def create_http_api_budget(
4529        self, model: HTTPAPIBudgetModel, config: Config, **kwargs: Any
4530    ) -> HttpAPIBudget:
4531        policies = [
4532            self._create_component_from_model(model=policy, config=config)
4533            for policy in model.policies
4534        ]
4535
4536        return HttpAPIBudget(
4537            policies=policies,
4538            ratelimit_reset_header=model.ratelimit_reset_header or "ratelimit-reset",
4539            ratelimit_remaining_header=model.ratelimit_remaining_header or "ratelimit-remaining",
4540            status_codes_for_ratelimit_hit=model.status_codes_for_ratelimit_hit or [429],
4541        )
4542
4543    def create_fixed_window_call_rate_policy(
4544        self, model: FixedWindowCallRatePolicyModel, config: Config, **kwargs: Any
4545    ) -> FixedWindowCallRatePolicy:
4546        matchers = [
4547            self._create_component_from_model(model=matcher, config=config)
4548            for matcher in model.matchers
4549        ]
4550
4551        # Set the initial reset timestamp to 10 days from now.
4552        # This value will be updated by the first request.
4553        return FixedWindowCallRatePolicy(
4554            next_reset_ts=datetime.datetime.now() + datetime.timedelta(days=10),
4555            period=parse_duration(model.period),
4556            call_limit=model.call_limit,
4557            matchers=matchers,
4558        )
4559
4560    def create_file_uploader(
4561        self, model: FileUploaderModel, config: Config, **kwargs: Any
4562    ) -> FileUploader:
4563        name = "File Uploader"
4564        requester = self._create_component_from_model(
4565            model=model.requester,
4566            config=config,
4567            name=name,
4568            **kwargs,
4569        )
4570        download_target_extractor = self._create_component_from_model(
4571            model=model.download_target_extractor,
4572            config=config,
4573            name=name,
4574            **kwargs,
4575        )
4576        emit_connector_builder_messages = self._emit_connector_builder_messages
4577        file_uploader = DefaultFileUploader(
4578            requester=requester,
4579            download_target_extractor=download_target_extractor,
4580            config=config,
4581            file_writer=NoopFileWriter()
4582            if emit_connector_builder_messages
4583            else LocalFileSystemFileWriter(),
4584            parameters=model.parameters or {},
4585            filename_extractor=model.filename_extractor if model.filename_extractor else None,
4586        )
4587
4588        return (
4589            ConnectorBuilderFileUploader(file_uploader)
4590            if emit_connector_builder_messages
4591            else file_uploader
4592        )
4593
4594    def create_moving_window_call_rate_policy(
4595        self, model: MovingWindowCallRatePolicyModel, config: Config, **kwargs: Any
4596    ) -> MovingWindowCallRatePolicy:
4597        rates = [
4598            self._create_component_from_model(model=rate, config=config) for rate in model.rates
4599        ]
4600        matchers = [
4601            self._create_component_from_model(model=matcher, config=config)
4602            for matcher in model.matchers
4603        ]
4604        return MovingWindowCallRatePolicy(
4605            rates=rates,
4606            matchers=matchers,
4607        )
4608
4609    def create_unlimited_call_rate_policy(
4610        self, model: UnlimitedCallRatePolicyModel, config: Config, **kwargs: Any
4611    ) -> UnlimitedCallRatePolicy:
4612        matchers = [
4613            self._create_component_from_model(model=matcher, config=config)
4614            for matcher in model.matchers
4615        ]
4616
4617        return UnlimitedCallRatePolicy(
4618            matchers=matchers,
4619        )
4620
4621    def create_rate(self, model: RateModel, config: Config, **kwargs: Any) -> Rate:
4622        interpolated_limit = InterpolatedString.create(str(model.limit), parameters={})
4623        return Rate(
4624            limit=int(interpolated_limit.eval(config=config)),
4625            interval=parse_duration(model.interval),
4626        )
4627
4628    def create_http_request_matcher(
4629        self, model: HttpRequestRegexMatcherModel, config: Config, **kwargs: Any
4630    ) -> HttpRequestRegexMatcher:
4631        weight = model.weight
4632        if weight is not None:
4633            if isinstance(weight, str):
4634                weight = int(InterpolatedString.create(weight, parameters={}).eval(config))
4635            else:
4636                weight = int(weight)
4637            if weight < 1:
4638                raise ValueError(f"weight must be >= 1, got {weight}")
4639        return HttpRequestRegexMatcher(
4640            method=model.method,
4641            url_base=model.url_base,
4642            url_path_pattern=model.url_path_pattern,
4643            params=model.params,
4644            headers=model.headers,
4645            weight=weight,
4646        )
4647
4648    def create_rate_limited_multiple_token_authenticator(
4649        self,
4650        model: RateLimitedMultipleTokenAuthenticatorModel,
4651        config: Config,
4652        **kwargs: Any,
4653    ) -> RateLimitedMultipleTokenAuthenticator:
4654        if isinstance(model.tokens, str):
4655            tokens_value = InterpolatedString.create(model.tokens, parameters={}).eval(config)
4656            delimiter = model.token_delimiter or ","
4657            tokens = [
4658                token.strip() for token in str(tokens_value).split(delimiter) if token.strip()
4659            ]
4660        else:
4661            tokens = [
4662                token_value
4663                for token in model.tokens
4664                if (
4665                    token_value := str(
4666                        InterpolatedString.create(token, parameters={}).eval(config)
4667                    ).strip()
4668                )
4669            ]
4670
4671        quota_specs = [
4672            {
4673                "name": quota_model.name,
4674                "remaining_path": quota_model.remaining_path,
4675                "reset_path": quota_model.reset_path,
4676                "limit_path": quota_model.limit_path,
4677                "remaining_header": quota_model.remaining_header,
4678                "reset_header": quota_model.reset_header,
4679                "limit_header": quota_model.limit_header,
4680                # Normalize the same way as the runtime TokenQuota below, so an omitted field
4681                # and an explicit `[]` key identically and keep sharing one set of counters.
4682                "exhaustion_status_codes": quota_model.exhaustion_status_codes or [],
4683                "matchers": [
4684                    {
4685                        "method": matcher_model.method,
4686                        "url_base": matcher_model.url_base,
4687                        "url_path_pattern": matcher_model.url_path_pattern,
4688                        "params": matcher_model.params,
4689                        "headers": matcher_model.headers,
4690                        "weight": matcher_model.weight,
4691                    }
4692                    for matcher_model in quota_model.matchers or []
4693                ],
4694            }
4695            for quota_model in model.quotas
4696        ]
4697
4698        quota_status_url = str(
4699            InterpolatedString.create(model.quota_status_source.url, parameters={}).eval(config)
4700        )
4701        quota_status_http_method = (
4702            model.quota_status_source.http_method.value
4703            if model.quota_status_source.http_method
4704            else "GET"
4705        )
4706        quota_status_headers = {
4707            key: str(InterpolatedString.create(value, parameters={}).eval(config))
4708            for key, value in (model.quota_status_source.request_headers or {}).items()
4709        }
4710        # Normalize the same way as the quota specs above, so an omitted field and an explicit
4711        # `[]` key identically and keep sharing one set of counters. Deduplicated as well as
4712        # sorted, because the runtime turns this into a set: without it `[404]` and `[404, 404]`
4713        # would key differently and stop sharing counters while behaving identically.
4714        quota_status_unavailable_status_codes = sorted(
4715            set(model.quota_status_source.unavailable_status_codes or [])
4716        )
4717        auth_method = model.auth_method or "Bearer"
4718        header = model.header or "Authorization"
4719        max_wait_time_str = str(
4720            InterpolatedString.create(model.max_wait_time or "PT2H", parameters={}).eval(config)
4721        )
4722        max_wait_time = parse_duration(max_wait_time_str)
4723        if not isinstance(max_wait_time, datetime.timedelta):
4724            raise ValueError(
4725                f"max_wait_time must be a fixed-length ISO 8601 duration (e.g. 'PT2H'); "
4726                f"calendar-unit durations like '{max_wait_time_str}' are not supported"
4727            )
4728        budget_reserve_fraction = (
4729            model.budget_reserve_fraction if model.budget_reserve_fraction is not None else 0.1
4730        )
4731        budget_min_reserve = (
4732            model.budget_min_reserve if model.budget_min_reserve is not None else 50
4733        )
4734
4735        # Reuse the same instance for identical definitions so that all streams share the
4736        # same token quota counters (similar to how api_budget is shared). The key is built
4737        # from the resolved constructor arguments rather than the raw model so that
4738        # stream-specific `$parameters` propagated onto the model (and its nested components)
4739        # cannot break instance sharing.
4740        cache_key = json.dumps(
4741            {
4742                "tokens": tokens,
4743                "quotas": quota_specs,
4744                "quota_status_url": quota_status_url,
4745                "quota_status_http_method": quota_status_http_method,
4746                "quota_status_headers": quota_status_headers,
4747                "quota_status_unavailable_status_codes": quota_status_unavailable_status_codes,
4748                "auth_method": auth_method,
4749                "header": header,
4750                "max_wait_time": max_wait_time.total_seconds(),
4751                "budget_reserve_fraction": budget_reserve_fraction,
4752                "budget_min_reserve": budget_min_reserve,
4753            },
4754            sort_keys=True,
4755        )
4756        if cache_key in self._rate_limited_authenticators:
4757            return self._rate_limited_authenticators[cache_key]
4758
4759        quotas = [
4760            TokenQuota(
4761                name=quota_model.name,
4762                remaining_path=quota_model.remaining_path,
4763                reset_path=quota_model.reset_path,
4764                limit_path=quota_model.limit_path,
4765                remaining_header=quota_model.remaining_header,
4766                reset_header=quota_model.reset_header,
4767                limit_header=quota_model.limit_header,
4768                exhaustion_status_codes=quota_model.exhaustion_status_codes or [],
4769                matchers=[
4770                    self.create_http_request_matcher(matcher_model, config)
4771                    for matcher_model in quota_model.matchers or []
4772                ],
4773            )
4774            for quota_model in model.quotas
4775        ]
4776
4777        authenticator = RateLimitedMultipleTokenAuthenticator(
4778            tokens=tokens,
4779            quotas=quotas,
4780            quota_status_url=quota_status_url,
4781            quota_status_http_method=quota_status_http_method,
4782            quota_status_headers=quota_status_headers,
4783            quota_status_unavailable_status_codes=quota_status_unavailable_status_codes,
4784            auth_method=auth_method,
4785            header=header,
4786            max_wait_time=max_wait_time,
4787            budget_reserve_fraction=budget_reserve_fraction,
4788            budget_min_reserve=budget_min_reserve,
4789        )
4790        self._rate_limited_authenticators[cache_key] = authenticator
4791        return authenticator
4792
4793    def set_api_budget(self, component_definition: ComponentDefinition, config: Config) -> None:
4794        self._api_budget = self.create_component(
4795            model_type=HTTPAPIBudgetModel, component_definition=component_definition, config=config
4796        )
4797
4798    def create_grouping_partition_router(
4799        self,
4800        model: GroupingPartitionRouterModel,
4801        config: Config,
4802        *,
4803        stream_name: str,
4804        **kwargs: Any,
4805    ) -> GroupingPartitionRouter:
4806        underlying_router = self._create_component_from_model(
4807            model=model.underlying_partition_router,
4808            config=config,
4809            stream_name=stream_name,
4810            **kwargs,
4811        )
4812        if model.group_size < 1:
4813            raise ValueError(f"Group size must be greater than 0, got {model.group_size}")
4814
4815        # Request options in underlying partition routers are not supported for GroupingPartitionRouter
4816        # because they are specific to individual partitions and cannot be aggregated or handled
4817        # when grouping, potentially leading to incorrect API calls. Any request customization
4818        # should be managed at the stream level through the requester's configuration.
4819        if isinstance(underlying_router, SubstreamPartitionRouter):
4820            if any(
4821                parent_config.request_option
4822                for parent_config in underlying_router.parent_stream_configs
4823            ):
4824                raise ValueError("Request options are not supported for GroupingPartitionRouter.")
4825
4826        if isinstance(underlying_router, ListPartitionRouter):
4827            if underlying_router.request_option:
4828                raise ValueError("Request options are not supported for GroupingPartitionRouter.")
4829
4830        return GroupingPartitionRouter(
4831            group_size=model.group_size,
4832            underlying_partition_router=underlying_router,
4833            deduplicate=model.deduplicate if model.deduplicate is not None else True,
4834            config=config,
4835        )
4836
4837    def create_union_partition_router(
4838        self,
4839        model: UnionPartitionRouterModel,
4840        config: Config,
4841        *,
4842        stream_name: str,
4843        **kwargs: Any,
4844    ) -> UnionPartitionRouter:
4845        # The schema enforces minItems: 2 for manifests; this guard covers construction paths
4846        # that bypass JSON-schema validation (the generated model carries no min_items constraint).
4847        if len(model.partition_routers) < 2:
4848            raise ValueError(
4849                f"UnionPartitionRouter for stream {stream_name} needs at least 2 child partition routers"
4850            )
4851
4852        partition_routers = [
4853            self._create_component_from_model(
4854                model=child,
4855                config=config,
4856                stream_name=stream_name,
4857                **kwargs,
4858            )
4859            for child in model.partition_routers
4860        ]
4861
4862        # partition_field depends only on config/parameters, so it is evaluated once at build
4863        # time; the runtime component always receives a plain string.
4864        partition_field = InterpolatedString.create(
4865            model.partition_field, parameters=model.parameters or {}
4866        ).eval(config)
4867
4868        # Fail fast at build time when a built-in child router is statically known to emit a
4869        # partition field different from the union's. CustomPartitionRouter children are opaque
4870        # and can only be validated at runtime.
4871        for child_model in model.partition_routers:
4872            child_partition_fields: List[str] = []
4873            if isinstance(child_model, ListPartitionRouterModel):
4874                child_partition_fields.append(
4875                    InterpolatedString.create(
4876                        child_model.cursor_field, parameters=child_model.parameters or {}
4877                    ).eval(config)
4878                )
4879            elif isinstance(child_model, SubstreamPartitionRouterModel):
4880                for parent_stream_config in child_model.parent_stream_configs:
4881                    child_partition_fields.append(
4882                        InterpolatedString.create(
4883                            parent_stream_config.partition_field,
4884                            parameters=parent_stream_config.parameters
4885                            or child_model.parameters
4886                            or {},
4887                        ).eval(config)
4888                    )
4889            elif isinstance(child_model, UnionPartitionRouterModel):
4890                child_partition_fields.append(
4891                    InterpolatedString.create(
4892                        child_model.partition_field, parameters=child_model.parameters or {}
4893                    ).eval(config)
4894                )
4895            for child_partition_field in child_partition_fields:
4896                if child_partition_field != partition_field:
4897                    raise ValueError(
4898                        f"UnionPartitionRouter expects all child partition routers to emit the "
4899                        f"partition field '{partition_field}', but a "
4900                        f"{child_model.type} child emits '{child_partition_field}'."
4901                    )
4902
4903        # A union slice comes from exactly one child partition router, so request options
4904        # declared on children cannot be applied consistently to requests built from the
4905        # normalized union slices. Partition values should be consumed via interpolation
4906        # (e.g. stream_partition) instead. Note that this validation only covers built-in
4907        # router types; CustomPartitionRouter children are opaque, so any request options
4908        # they implement internally cannot be detected or rejected here.
4909        for router in partition_routers:
4910            if isinstance(router, SubstreamPartitionRouter):
4911                if any(
4912                    parent_config.request_option for parent_config in router.parent_stream_configs
4913                ):
4914                    raise ValueError("Request options are not supported for UnionPartitionRouter.")
4915            if isinstance(router, ListPartitionRouter) and router.request_option:
4916                raise ValueError("Request options are not supported for UnionPartitionRouter.")
4917
4918        return UnionPartitionRouter(
4919            partition_routers=partition_routers,
4920            partition_field=partition_field,
4921            parameters=model.parameters or {},
4922        )
4923
4924    def _ensure_query_properties_to_model(
4925        self, requester: Union[HttpRequesterModel, CustomRequesterModel]
4926    ) -> None:
4927        """
4928        For some reason, it seems like CustomRequesterModel request_parameters stays as dictionaries which means that
4929        the other conditions relying on it being QueryPropertiesModel instead of a dict fail. Here, we migrate them to
4930        proper model.
4931        """
4932        if not hasattr(requester, "request_parameters"):
4933            return
4934
4935        request_parameters = requester.request_parameters
4936        if request_parameters and isinstance(request_parameters, Dict):
4937            for request_parameter_key in request_parameters.keys():
4938                request_parameter = request_parameters[request_parameter_key]
4939                if (
4940                    isinstance(request_parameter, Dict)
4941                    and request_parameter.get("type") == "QueryProperties"
4942                ):
4943                    request_parameters[request_parameter_key] = QueryPropertiesModel.parse_obj(
4944                        request_parameter
4945                    )
4946
4947    def _get_catalog_defined_cursor_field(
4948        self, stream_name: str, allow_catalog_defined_cursor_field: bool
4949    ) -> Optional[CursorField]:
4950        if not allow_catalog_defined_cursor_field:
4951            return None
4952
4953        configured_stream = self._stream_name_to_configured_stream.get(stream_name)
4954
4955        # Depending on the operation is being performed, there may not be a configured stream yet. In this
4956        # case we return None which will then use the default cursor field defined on the cursor model.
4957        # We also treat cursor_field: [""] (list with empty string) as no cursor field, since this can
4958        # occur when the platform serializes "no cursor configured" streams incorrectly.
4959        if (
4960            not configured_stream
4961            or not configured_stream.cursor_field
4962            or not configured_stream.cursor_field[0]
4963        ):
4964            return None
4965        elif len(configured_stream.cursor_field) > 1:
4966            raise ValueError(
4967                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."
4968            )
4969        else:
4970            return CursorField(
4971                cursor_field_key=configured_stream.cursor_field[0],
4972                supports_catalog_defined_cursor_field=allow_catalog_defined_cursor_field,
4973            )
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        return RecordExpander(
2511            expand_records_from_field=model.expand_records_from_field,
2512            config=config,
2513            parameters=model.parameters or {},
2514            remain_original_record=model.remain_original_record or False,
2515            on_no_records=OnNoRecords(model.on_no_records.value)
2516            if model.on_no_records
2517            else OnNoRecords.skip,
2518        )
2520    @staticmethod
2521    def create_response_to_file_extractor(
2522        model: ResponseToFileExtractorModel,
2523        **kwargs: Any,
2524    ) -> ResponseToFileExtractor:
2525        return ResponseToFileExtractor(
2526            parameters=model.parameters or {},
2527            preserve_na_values=model.preserve_na_values or False,
2528        )
2530    @staticmethod
2531    def create_exponential_backoff_strategy(
2532        model: ExponentialBackoffStrategyModel, config: Config
2533    ) -> ExponentialBackoffStrategy:
2534        ModelToComponentFactory._validate_jitter_range(model.jitter_range_in_seconds)
2535        return ExponentialBackoffStrategy(
2536            factor=model.factor or 5,
2537            jitter_range_in_seconds=model.jitter_range_in_seconds,
2538            parameters=model.parameters or {},
2539            config=config,
2540        )
2542    @staticmethod
2543    def create_group_by_key(model: GroupByKeyMergeStrategyModel, config: Config) -> GroupByKey:
2544        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:
2546    def create_http_requester(
2547        self,
2548        model: HttpRequesterModel,
2549        config: Config,
2550        decoder: Decoder = JsonDecoder(parameters={}),
2551        query_properties_key: Optional[str] = None,
2552        use_cache: Optional[bool] = None,
2553        *,
2554        name: str,
2555    ) -> HttpRequester:
2556        authenticator = (
2557            self._create_component_from_model(
2558                model=model.authenticator,
2559                config=config,
2560                url_base=model.url or model.url_base,
2561                name=name,
2562                decoder=decoder,
2563            )
2564            if model.authenticator
2565            else None
2566        )
2567        error_handler = (
2568            self._create_component_from_model(model=model.error_handler, config=config)
2569            if model.error_handler
2570            else DefaultErrorHandler(
2571                backoff_strategies=[],
2572                response_filters=[],
2573                config=config,
2574                parameters=model.parameters or {},
2575            )
2576        )
2577
2578        api_budget = self._api_budget
2579
2580        request_options_provider = InterpolatedRequestOptionsProvider(
2581            request_body=model.request_body,
2582            request_body_data=model.request_body_data,
2583            request_body_json=model.request_body_json,
2584            request_headers=model.request_headers,
2585            request_parameters=model.request_parameters,  # type: ignore  # QueryProperties have been removed in `create_simple_retriever`
2586            query_properties_key=query_properties_key,
2587            config=config,
2588            parameters=model.parameters or {},
2589        )
2590
2591        assert model.use_cache is not None  # for mypy
2592        assert model.http_method is not None  # for mypy
2593
2594        should_use_cache = (model.use_cache or bool(use_cache)) and not self._disable_cache
2595
2596        return HttpRequester(
2597            name=name,
2598            url=model.url,
2599            url_base=model.url_base,
2600            path=model.path,
2601            authenticator=authenticator,
2602            error_handler=error_handler,
2603            api_budget=api_budget,
2604            http_method=HttpMethod[model.http_method.value],
2605            request_options_provider=request_options_provider,
2606            config=config,
2607            disable_retries=self._disable_retries,
2608            parameters=model.parameters or {},
2609            message_repository=self._message_repository,
2610            use_cache=should_use_cache,
2611            decoder=decoder,
2612            stream_response=decoder.is_stream_response() if decoder else False,
2613        )
@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:
2615    @staticmethod
2616    def create_http_response_filter(
2617        model: HttpResponseFilterModel, config: Config, **kwargs: Any
2618    ) -> HttpResponseFilter:
2619        if model.action:
2620            action = ResponseAction(model.action.value)
2621        else:
2622            action = None
2623
2624        failure_type = FailureType(model.failure_type.value) if model.failure_type else None
2625
2626        http_codes = (
2627            set(model.http_codes) if model.http_codes else set()
2628        )  # JSON schema notation has no set data type. The schema enforces an array of unique elements
2629
2630        return HttpResponseFilter(
2631            action=action,
2632            failure_type=failure_type,
2633            error_message=model.error_message or "",
2634            error_message_contains=model.error_message_contains or "",
2635            http_codes=http_codes,
2636            predicate=model.predicate or "",
2637            config=config,
2638            parameters=model.parameters or {},
2639        )
@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:
2641    @staticmethod
2642    def create_inline_schema_loader(
2643        model: InlineSchemaLoaderModel, config: Config, **kwargs: Any
2644    ) -> InlineSchemaLoader:
2645        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:
2647    def create_complex_field_type(
2648        self, model: ComplexFieldTypeModel, config: Config, **kwargs: Any
2649    ) -> ComplexFieldType:
2650        items = (
2651            self._create_component_from_model(model=model.items, config=config)
2652            if isinstance(model.items, ComplexFieldTypeModel)
2653            else model.items
2654        )
2655
2656        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:
2658    def create_types_map(self, model: TypesMapModel, config: Config, **kwargs: Any) -> TypesMap:
2659        target_type = (
2660            self._create_component_from_model(model=model.target_type, config=config)
2661            if isinstance(model.target_type, ComplexFieldTypeModel)
2662            else model.target_type
2663        )
2664
2665        return TypesMap(
2666            target_type=target_type,
2667            current_type=model.current_type,
2668            condition=model.condition if model.condition is not None else "True",
2669        )
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:
2671    def create_schema_type_identifier(
2672        self, model: SchemaTypeIdentifierModel, config: Config, **kwargs: Any
2673    ) -> SchemaTypeIdentifier:
2674        types_mapping = []
2675        if model.types_mapping:
2676            types_mapping.extend(
2677                [
2678                    self._create_component_from_model(types_map, config=config)
2679                    for types_map in model.types_mapping
2680                ]
2681            )
2682        model_schema_pointer: List[Union[InterpolatedString, str]] = (
2683            [x for x in model.schema_pointer] if model.schema_pointer else []
2684        )
2685        model_key_pointer: List[Union[InterpolatedString, str]] = [x for x in model.key_pointer]
2686        model_type_pointer: Optional[List[Union[InterpolatedString, str]]] = (
2687            [x for x in model.type_pointer] if model.type_pointer else None
2688        )
2689
2690        return SchemaTypeIdentifier(
2691            schema_pointer=model_schema_pointer,
2692            key_pointer=model_key_pointer,
2693            type_pointer=model_type_pointer,
2694            types_mapping=types_mapping,
2695            parameters=model.parameters or {},
2696        )
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:
2698    def create_dynamic_schema_loader(
2699        self, model: DynamicSchemaLoaderModel, config: Config, **kwargs: Any
2700    ) -> DynamicSchemaLoader:
2701        schema_transformations = []
2702        if model.schema_transformations:
2703            for transformation_model in model.schema_transformations:
2704                schema_transformations.append(
2705                    self._create_component_from_model(model=transformation_model, config=config)
2706                )
2707        name = "dynamic_properties"
2708        retriever = self._create_component_from_model(
2709            model=model.retriever,
2710            config=config,
2711            name=name,
2712            primary_key=None,
2713            partition_router=self._build_stream_slicer_from_partition_router(
2714                model.retriever, config
2715            ),
2716            transformations=[],
2717            use_cache=True,
2718            log_formatter=(
2719                lambda response: format_http_message(
2720                    response,
2721                    f"Schema loader '{name}' request",
2722                    f"Request performed in order to extract schema.",
2723                    name,
2724                    is_auxiliary=True,
2725                )
2726            ),
2727        )
2728        schema_type_identifier = self._create_component_from_model(
2729            model.schema_type_identifier, config=config, parameters=model.parameters or {}
2730        )
2731        schema_filter = (
2732            self._create_component_from_model(
2733                model.schema_filter, config=config, parameters=model.parameters or {}
2734            )
2735            if model.schema_filter is not None
2736            else None
2737        )
2738
2739        return DynamicSchemaLoader(
2740            retriever=retriever,
2741            config=config,
2742            schema_transformations=schema_transformations,
2743            schema_filter=schema_filter,
2744            schema_type_identifier=schema_type_identifier,
2745            parameters=model.parameters or {},
2746        )
@staticmethod
def create_json_decoder( model: airbyte_cdk.sources.declarative.models.declarative_component_schema.JsonDecoder, config: Mapping[str, Any], **kwargs: Any) -> airbyte_cdk.Decoder:
2748    @staticmethod
2749    def create_json_decoder(model: JsonDecoderModel, config: Config, **kwargs: Any) -> Decoder:
2750        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:
2752    def create_csv_decoder(self, model: CsvDecoderModel, config: Config, **kwargs: Any) -> Decoder:
2753        return CompositeRawDecoder(
2754            parser=ModelToComponentFactory._get_parser(model, config),
2755            stream_response=False if self._emit_connector_builder_messages else True,
2756        )
def create_jsonl_decoder( self, model: airbyte_cdk.sources.declarative.models.declarative_component_schema.JsonlDecoder, config: Mapping[str, Any], **kwargs: Any) -> airbyte_cdk.Decoder:
2758    def create_jsonl_decoder(
2759        self, model: JsonlDecoderModel, config: Config, **kwargs: Any
2760    ) -> Decoder:
2761        return CompositeRawDecoder(
2762            parser=ModelToComponentFactory._get_parser(model, config),
2763            stream_response=False if self._emit_connector_builder_messages else True,
2764        )
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:
2766    def create_json_items_decoder(
2767        self, model: JsonItemsDecoderModel, config: Config, **kwargs: Any
2768    ) -> Decoder:
2769        return CompositeRawDecoder(
2770            parser=ModelToComponentFactory._get_parser(model, config),
2771            stream_response=False if self._emit_connector_builder_messages else True,
2772        )
def create_gzip_decoder( self, model: airbyte_cdk.sources.declarative.models.declarative_component_schema.GzipDecoder, config: Mapping[str, Any], **kwargs: Any) -> airbyte_cdk.Decoder:
2774    def create_gzip_decoder(
2775        self, model: GzipDecoderModel, config: Config, **kwargs: Any
2776    ) -> Decoder:
2777        _compressed_response_types = {
2778            "gzip",
2779            "x-gzip",
2780            "gzip, deflate",
2781            "x-gzip, deflate",
2782            "application/zip",
2783            "application/gzip",
2784            "application/x-gzip",
2785            "application/x-zip-compressed",
2786        }
2787
2788        gzip_parser: GzipParser = ModelToComponentFactory._get_parser(model, config)  # type: ignore  # based on the model, we know this will be a GzipParser
2789
2790        if self._emit_connector_builder_messages:
2791            return CompositeRawDecoder(gzip_parser, False)
2792
2793        transport_gzip_parser = GzipParser(inner_parser=gzip_parser)
2794        return CompositeRawDecoder.by_headers(
2795            [
2796                ({"Content-Encoding"}, {"gzip"}, transport_gzip_parser),
2797                ({"Content-Type"}, _compressed_response_types, gzip_parser),
2798            ],
2799            stream_response=True,
2800            fallback_parser=gzip_parser,
2801        )
@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:
2803    @staticmethod
2804    def create_iterable_decoder(
2805        model: IterableDecoderModel, config: Config, **kwargs: Any
2806    ) -> IterableDecoder:
2807        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:
2809    @staticmethod
2810    def create_xml_decoder(model: XmlDecoderModel, config: Config, **kwargs: Any) -> XmlDecoder:
2811        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:
2813    def create_zipfile_decoder(
2814        self, model: ZipfileDecoderModel, config: Config, **kwargs: Any
2815    ) -> ZipfileDecoder:
2816        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:
2847    @staticmethod
2848    def create_json_file_schema_loader(
2849        model: JsonFileSchemaLoaderModel, config: Config, **kwargs: Any
2850    ) -> JsonFileSchemaLoader:
2851        return JsonFileSchemaLoader(
2852            file_path=model.file_path or "", config=config, parameters=model.parameters or {}
2853        )
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:
2855    def create_jwt_authenticator(
2856        self, model: JwtAuthenticatorModel, config: Config, **kwargs: Any
2857    ) -> JwtAuthenticator:
2858        jwt_headers = model.jwt_headers or JwtHeadersModel(kid=None, typ="JWT", cty=None)
2859        jwt_payload = model.jwt_payload or JwtPayloadModel(iss=None, sub=None, aud=None)
2860        request_option = (
2861            self._create_component_from_model(model.request_option, config)
2862            if model.request_option
2863            else None
2864        )
2865        return JwtAuthenticator(
2866            config=config,
2867            parameters=model.parameters or {},
2868            algorithm=JwtAlgorithm(model.algorithm.value),
2869            secret_key=model.secret_key,
2870            base64_encode_secret_key=model.base64_encode_secret_key,
2871            token_duration=model.token_duration,
2872            header_prefix=model.header_prefix,
2873            kid=jwt_headers.kid,
2874            typ=jwt_headers.typ,
2875            cty=jwt_headers.cty,
2876            iss=jwt_payload.iss,
2877            sub=jwt_payload.sub,
2878            aud=jwt_payload.aud,
2879            additional_jwt_headers=model.additional_jwt_headers,
2880            additional_jwt_payload=model.additional_jwt_payload,
2881            passphrase=model.passphrase,
2882            request_option=request_option,
2883        )
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:
2885    def create_list_partition_router(
2886        self, model: ListPartitionRouterModel, config: Config, **kwargs: Any
2887    ) -> ListPartitionRouter:
2888        request_option = (
2889            self._create_component_from_model(model.request_option, config)
2890            if model.request_option
2891            else None
2892        )
2893        return ListPartitionRouter(
2894            cursor_field=model.cursor_field,
2895            request_option=request_option,
2896            values=model.values,
2897            config=config,
2898            parameters=model.parameters or {},
2899        )
@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:
2901    @staticmethod
2902    def create_min_max_datetime(
2903        model: MinMaxDatetimeModel, config: Config, **kwargs: Any
2904    ) -> MinMaxDatetime:
2905        return MinMaxDatetime(
2906            datetime=model.datetime,
2907            datetime_format=model.datetime_format or "",
2908            max_datetime=model.max_datetime or "",
2909            min_datetime=model.min_datetime or "",
2910            parameters=model.parameters or {},
2911        )
@staticmethod
def create_no_auth( model: airbyte_cdk.sources.declarative.models.declarative_component_schema.NoAuth, config: Mapping[str, Any], **kwargs: Any) -> airbyte_cdk.NoAuth:
2913    @staticmethod
2914    def create_no_auth(model: NoAuthModel, config: Config, **kwargs: Any) -> NoAuth:
2915        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:
2917    @staticmethod
2918    def create_no_pagination(
2919        model: NoPaginationModel, config: Config, **kwargs: Any
2920    ) -> NoPagination:
2921        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:
2923    def create_oauth_authenticator(
2924        self, model: OAuthAuthenticatorModel, config: Config, **kwargs: Any
2925    ) -> DeclarativeOauth2Authenticator:
2926        profile_assertion = (
2927            self._create_component_from_model(model.profile_assertion, config=config)
2928            if model.profile_assertion
2929            else None
2930        )
2931
2932        refresh_token_error_status_codes, refresh_token_error_key, refresh_token_error_values = (
2933            self._get_refresh_token_error_information(model)
2934        )
2935        if model.refresh_token_updater:
2936            # ignore type error because fixing it would have a lot of dependencies, revisit later
2937            return DeclarativeSingleUseRefreshTokenOauth2Authenticator(  # type: ignore
2938                config,
2939                InterpolatedString.create(
2940                    model.token_refresh_endpoint,  # type: ignore
2941                    parameters=model.parameters or {},
2942                ).eval(config),
2943                access_token_name=InterpolatedString.create(
2944                    model.access_token_name or "access_token", parameters=model.parameters or {}
2945                ).eval(config),
2946                refresh_token_name=model.refresh_token_updater.refresh_token_name,
2947                expires_in_name=InterpolatedString.create(
2948                    model.expires_in_name or "expires_in", parameters=model.parameters or {}
2949                ).eval(config),
2950                client_id_name=InterpolatedString.create(
2951                    model.client_id_name or "client_id", parameters=model.parameters or {}
2952                ).eval(config),
2953                client_id=InterpolatedString.create(
2954                    model.client_id, parameters=model.parameters or {}
2955                ).eval(config)
2956                if model.client_id
2957                else model.client_id,
2958                client_secret_name=InterpolatedString.create(
2959                    model.client_secret_name or "client_secret", parameters=model.parameters or {}
2960                ).eval(config),
2961                client_secret=InterpolatedString.create(
2962                    model.client_secret, parameters=model.parameters or {}
2963                ).eval(config)
2964                if model.client_secret
2965                else model.client_secret,
2966                access_token_config_path=model.refresh_token_updater.access_token_config_path,
2967                refresh_token_config_path=model.refresh_token_updater.refresh_token_config_path,
2968                token_expiry_date_config_path=model.refresh_token_updater.token_expiry_date_config_path,
2969                grant_type_name=InterpolatedString.create(
2970                    model.grant_type_name or "grant_type", parameters=model.parameters or {}
2971                ).eval(config),
2972                grant_type=InterpolatedString.create(
2973                    model.grant_type or "refresh_token", parameters=model.parameters or {}
2974                ).eval(config),
2975                refresh_request_body=InterpolatedMapping(
2976                    model.refresh_request_body or {}, parameters=model.parameters or {}
2977                ).eval(config),
2978                refresh_request_headers=InterpolatedMapping(
2979                    model.refresh_request_headers or {}, parameters=model.parameters or {}
2980                ).eval(config),
2981                send_refresh_request_as_query_params=bool(
2982                    model.send_refresh_request_as_query_params
2983                ),
2984                scopes=model.scopes,
2985                token_expiry_date_format=model.token_expiry_date_format,
2986                token_expiry_is_time_of_expiration=bool(model.token_expiry_date_format),
2987                message_repository=self._message_repository,
2988                refresh_token_error_status_codes=refresh_token_error_status_codes,
2989                refresh_token_error_key=refresh_token_error_key,
2990                refresh_token_error_values=refresh_token_error_values,
2991            )
2992        # ignore type error because fixing it would have a lot of dependencies, revisit later
2993        return DeclarativeOauth2Authenticator(  # type: ignore
2994            access_token_name=model.access_token_name or "access_token",
2995            access_token_value=model.access_token_value,
2996            client_id_name=model.client_id_name or "client_id",
2997            client_id=model.client_id,
2998            client_secret_name=model.client_secret_name or "client_secret",
2999            client_secret=model.client_secret,
3000            expires_in_name=model.expires_in_name or "expires_in",
3001            grant_type_name=model.grant_type_name or "grant_type",
3002            grant_type=model.grant_type or "refresh_token",
3003            refresh_request_body=model.refresh_request_body,
3004            refresh_request_headers=model.refresh_request_headers,
3005            send_refresh_request_as_query_params=bool(model.send_refresh_request_as_query_params),
3006            refresh_token_name=model.refresh_token_name or "refresh_token",
3007            refresh_token=model.refresh_token,
3008            scopes=model.scopes,
3009            token_expiry_date=model.token_expiry_date,
3010            token_expiry_date_format=model.token_expiry_date_format,
3011            token_expiry_is_time_of_expiration=bool(model.token_expiry_date_format),
3012            token_refresh_endpoint=model.token_refresh_endpoint,
3013            config=config,
3014            parameters=model.parameters or {},
3015            message_repository=self._message_repository,
3016            profile_assertion=profile_assertion,
3017            use_profile_assertion=model.use_profile_assertion,
3018            refresh_token_error_status_codes=refresh_token_error_status_codes,
3019            refresh_token_error_key=refresh_token_error_key,
3020            refresh_token_error_values=refresh_token_error_values,
3021        )
3071    def create_offset_increment(
3072        self,
3073        model: OffsetIncrementModel,
3074        config: Config,
3075        decoder: Decoder,
3076        extractor_model: Optional[Union[CustomRecordExtractorModel, DpathExtractorModel]] = None,
3077        **kwargs: Any,
3078    ) -> OffsetIncrement:
3079        if isinstance(decoder, PaginationDecoderDecorator):
3080            inner_decoder = decoder.decoder
3081        else:
3082            inner_decoder = decoder
3083            decoder = PaginationDecoderDecorator(decoder=decoder)
3084
3085        if self._is_supported_decoder_for_pagination(inner_decoder):
3086            decoder_to_use = decoder
3087        else:
3088            raise ValueError(
3089                self._UNSUPPORTED_DECODER_ERROR.format(decoder_type=type(inner_decoder))
3090            )
3091
3092        # Ideally we would instantiate the runtime extractor from highest most level (in this case the SimpleRetriever)
3093        # so that it can be shared by OffSetIncrement and RecordSelector. However, due to how we instantiate the
3094        # decoder with various decorators here, but not in create_record_selector, it is simpler to retain existing
3095        # behavior by having two separate extractors with identical behavior since they use the same extractor model.
3096        # When we have more time to investigate we can look into reusing the same component.
3097        extractor = (
3098            self._create_component_from_model(
3099                model=extractor_model, config=config, decoder=decoder_to_use
3100            )
3101            if extractor_model
3102            else None
3103        )
3104
3105        # Pydantic v1 Union type coercion can convert int to string depending on Union order.
3106        # If page_size is a string that represents an integer (not an interpolation), convert it back.
3107        page_size = model.page_size
3108        if isinstance(page_size, str) and page_size.isdigit():
3109            page_size = int(page_size)
3110
3111        return OffsetIncrement(
3112            page_size=page_size,
3113            config=config,
3114            decoder=decoder_to_use,
3115            extractor=extractor,
3116            inject_on_first_request=model.inject_on_first_request or False,
3117            parameters=model.parameters or {},
3118        )
3120    def create_page_increment(
3121        self,
3122        model: PageIncrementModel,
3123        config: Config,
3124        decoder: Optional[Decoder] = None,
3125        extractor_model: Optional[Union[CustomRecordExtractorModel, DpathExtractorModel]] = None,
3126        **kwargs: Any,
3127    ) -> PageIncrement:
3128        # Like OffsetIncrement, we instantiate a separate extractor with identical behavior to the
3129        # RecordSelector's so the strategy can count the raw records in the response. This ensures
3130        # pagination is driven by the API's page size, not the post-filter record count.
3131        extractor = (
3132            self._create_component_from_model(model=extractor_model, config=config, decoder=decoder)
3133            if extractor_model
3134            else None
3135        )
3136
3137        # Pydantic v1 Union type coercion can convert int to string depending on Union order.
3138        # If page_size is a string that represents an integer (not an interpolation), convert it back.
3139        page_size = model.page_size
3140        if isinstance(page_size, str) and page_size.isdigit():
3141            page_size = int(page_size)
3142
3143        return PageIncrement(
3144            page_size=page_size,
3145            config=config,
3146            start_from_page=model.start_from_page or 0,
3147            inject_on_first_request=model.inject_on_first_request or False,
3148            extractor=extractor,
3149            parameters=model.parameters or {},
3150        )
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:
3152    def create_parent_stream_config(
3153        self, model: ParentStreamConfigModel, config: Config, *, stream_name: str, **kwargs: Any
3154    ) -> ParentStreamConfig:
3155        declarative_stream = self._create_component_from_model(
3156            model.stream,
3157            config=config,
3158            is_parent=True,
3159            **kwargs,
3160        )
3161        request_option = (
3162            self._create_component_from_model(model.request_option, config=config)
3163            if model.request_option
3164            else None
3165        )
3166
3167        if model.lazy_read_pointer and any("*" in pointer for pointer in model.lazy_read_pointer):
3168            raise ValueError(
3169                "The '*' wildcard in 'lazy_read_pointer' is not supported — only direct paths are allowed."
3170            )
3171
3172        model_lazy_read_pointer: List[Union[InterpolatedString, str]] = (
3173            [x for x in model.lazy_read_pointer] if model.lazy_read_pointer else []
3174        )
3175
3176        return ParentStreamConfig(
3177            parent_key=model.parent_key,
3178            request_option=request_option,
3179            stream=declarative_stream,
3180            partition_field=model.partition_field,
3181            config=config,
3182            incremental_dependency=model.incremental_dependency or False,
3183            parameters=model.parameters or {},
3184            extra_fields=model.extra_fields,
3185            lazy_read_pointer=model_lazy_read_pointer,
3186        )
3188    def create_properties_from_endpoint(
3189        self, model: PropertiesFromEndpointModel, config: Config, **kwargs: Any
3190    ) -> PropertiesFromEndpoint:
3191        retriever = self._create_component_from_model(
3192            model=model.retriever,
3193            config=config,
3194            name="dynamic_properties",
3195            primary_key=None,
3196            stream_slicer=None,
3197            transformations=[],
3198            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
3199        )
3200        return PropertiesFromEndpoint(
3201            property_field_path=model.property_field_path,
3202            retriever=retriever,
3203            config=config,
3204            parameters=model.parameters or {},
3205        )
3207    def create_property_chunking(
3208        self, model: PropertyChunkingModel, config: Config, **kwargs: Any
3209    ) -> PropertyChunking:
3210        record_merge_strategy = (
3211            self._create_component_from_model(
3212                model=model.record_merge_strategy, config=config, **kwargs
3213            )
3214            if model.record_merge_strategy
3215            else None
3216        )
3217
3218        property_limit_type: PropertyLimitType
3219        match model.property_limit_type:
3220            case PropertyLimitTypeModel.property_count:
3221                property_limit_type = PropertyLimitType.property_count
3222            case PropertyLimitTypeModel.characters:
3223                property_limit_type = PropertyLimitType.characters
3224            case _:
3225                raise ValueError(f"Invalid PropertyLimitType {property_limit_type}")
3226
3227        return PropertyChunking(
3228            property_limit_type=property_limit_type,
3229            property_limit=model.property_limit,
3230            record_merge_strategy=record_merge_strategy,
3231            config=config,
3232            parameters=model.parameters or {},
3233        )
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:
3235    def create_query_properties(
3236        self, model: QueryPropertiesModel, config: Config, *, stream_name: str, **kwargs: Any
3237    ) -> QueryProperties:
3238        if isinstance(model.property_list, list):
3239            property_list = model.property_list
3240        else:
3241            property_list = self._create_component_from_model(
3242                model=model.property_list, config=config, **kwargs
3243            )
3244
3245        property_chunking = (
3246            self._create_component_from_model(
3247                model=model.property_chunking, config=config, **kwargs
3248            )
3249            if model.property_chunking
3250            else None
3251        )
3252
3253        property_selector = (
3254            self._create_component_from_model(
3255                model=model.property_selector, config=config, stream_name=stream_name, **kwargs
3256            )
3257            if model.property_selector
3258            else None
3259        )
3260
3261        return QueryProperties(
3262            property_list=property_list,
3263            always_include_properties=model.always_include_properties,
3264            property_chunking=property_chunking,
3265            property_selector=property_selector,
3266            config=config,
3267            parameters=model.parameters or {},
3268        )
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:
3270    def create_json_schema_property_selector(
3271        self,
3272        model: JsonSchemaPropertySelectorModel,
3273        config: Config,
3274        *,
3275        stream_name: str,
3276        **kwargs: Any,
3277    ) -> JsonSchemaPropertySelector:
3278        configured_stream = self._stream_name_to_configured_stream.get(stream_name)
3279
3280        transformations = []
3281        if model.transformations:
3282            for transformation_model in model.transformations:
3283                transformations.append(
3284                    self._create_component_from_model(model=transformation_model, config=config)
3285                )
3286
3287        return JsonSchemaPropertySelector(
3288            configured_stream=configured_stream,
3289            properties_transformations=transformations,
3290            config=config,
3291            parameters=model.parameters or {},
3292        )
@staticmethod
def create_record_filter( model: airbyte_cdk.sources.declarative.models.declarative_component_schema.RecordFilter, config: Mapping[str, Any], **kwargs: Any) -> airbyte_cdk.RecordFilter:
3294    @staticmethod
3295    def create_record_filter(
3296        model: RecordFilterModel, config: Config, **kwargs: Any
3297    ) -> RecordFilter:
3298        return RecordFilter(
3299            condition=model.condition or "", config=config, parameters=model.parameters or {}
3300        )
@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:
3302    @staticmethod
3303    def create_request_path(model: RequestPathModel, config: Config, **kwargs: Any) -> RequestPath:
3304        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:
3306    @staticmethod
3307    def create_request_option(
3308        model: RequestOptionModel, config: Config, **kwargs: Any
3309    ) -> RequestOption:
3310        inject_into = RequestOptionType(model.inject_into.value)
3311        field_path: Optional[List[Union[InterpolatedString, str]]] = (
3312            [
3313                InterpolatedString.create(segment, parameters=kwargs.get("parameters", {}))
3314                for segment in model.field_path
3315            ]
3316            if model.field_path
3317            else None
3318        )
3319        field_name = (
3320            InterpolatedString.create(model.field_name, parameters=kwargs.get("parameters", {}))
3321            if model.field_name
3322            else None
3323        )
3324        return RequestOption(
3325            field_name=field_name,
3326            field_path=field_path,
3327            inject_into=inject_into,
3328            parameters=kwargs.get("parameters", {}),
3329        )
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:
3331    def create_record_selector(
3332        self,
3333        model: RecordSelectorModel,
3334        config: Config,
3335        *,
3336        name: str,
3337        transformations: List[RecordTransformation] | None = None,
3338        decoder: Decoder | None = None,
3339        client_side_incremental_sync_cursor: Optional[Cursor] = None,
3340        is_client_side_incremental_sync: bool = False,
3341        file_uploader: Optional[DefaultFileUploader] = None,
3342        **kwargs: Any,
3343    ) -> RecordSelector:
3344        extractor = self._create_component_from_model(
3345            model=model.extractor, decoder=decoder, config=config
3346        )
3347        record_filter = (
3348            self._create_component_from_model(model.record_filter, config=config)
3349            if model.record_filter
3350            else None
3351        )
3352
3353        # A client-side incremental stream transforms before filtering by default. That default belongs to the flag,
3354        # not to the component that ends up doing the cursor comparison: a data feed does it in the retriever and
3355        # receives no cursor here, but its `record_filter` condition must keep running after the transformations.
3356        default_transform_before_filtering = bool(
3357            client_side_incremental_sync_cursor or is_client_side_incremental_sync
3358        )
3359        transform_before_filtering = (
3360            default_transform_before_filtering
3361            if model.transform_before_filtering is None
3362            else model.transform_before_filtering
3363        )
3364        if client_side_incremental_sync_cursor:
3365            record_filter = ClientSideIncrementalRecordFilterDecorator(
3366                config=config,
3367                parameters=model.parameters,
3368                condition=model.record_filter.condition
3369                if (model.record_filter and hasattr(model.record_filter, "condition"))
3370                else None,
3371                cursor=client_side_incremental_sync_cursor,
3372            )
3373
3374        if model.schema_normalization is None:
3375            # default to no schema normalization if not set
3376            model.schema_normalization = SchemaNormalizationModel.None_
3377
3378        schema_normalization = (
3379            TypeTransformer(SCHEMA_TRANSFORMER_TYPE_MAPPING[model.schema_normalization])
3380            if isinstance(model.schema_normalization, SchemaNormalizationModel)
3381            else self._create_component_from_model(model.schema_normalization, config=config)  # type: ignore[arg-type] # custom normalization model expected here
3382        )
3383
3384        return RecordSelector(
3385            extractor=extractor,
3386            name=name,
3387            config=config,
3388            record_filter=record_filter,
3389            transformations=transformations or [],
3390            file_uploader=file_uploader,
3391            schema_normalization=schema_normalization,
3392            parameters=model.parameters or {},
3393            transform_before_filtering=transform_before_filtering,
3394        )
@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:
3396    @staticmethod
3397    def create_remove_fields(
3398        model: RemoveFieldsModel, config: Config, **kwargs: Any
3399    ) -> RemoveFields:
3400        return RemoveFields(
3401            field_pointers=model.field_pointers, condition=model.condition or "", parameters={}
3402        )
def create_selective_authenticator( self, model: airbyte_cdk.sources.declarative.models.declarative_component_schema.SelectiveAuthenticator, config: Mapping[str, Any], **kwargs: Any) -> airbyte_cdk.DeclarativeAuthenticator:
3404    def create_selective_authenticator(
3405        self, model: SelectiveAuthenticatorModel, config: Config, **kwargs: Any
3406    ) -> DeclarativeAuthenticator:
3407        authenticators = {
3408            name: self._create_component_from_model(model=auth, config=config)
3409            for name, auth in model.authenticators.items()
3410        }
3411        # SelectiveAuthenticator will return instance of DeclarativeAuthenticator or raise ValueError error
3412        return SelectiveAuthenticator(  # type: ignore[abstract]
3413            config=config,
3414            authenticators=authenticators,
3415            authenticator_selection_path=model.authenticator_selection_path,
3416            **kwargs,
3417        )
@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:
3419    @staticmethod
3420    def create_legacy_session_token_authenticator(
3421        model: LegacySessionTokenAuthenticatorModel, config: Config, *, url_base: str, **kwargs: Any
3422    ) -> LegacySessionTokenAuthenticator:
3423        return LegacySessionTokenAuthenticator(
3424            api_url=url_base,
3425            header=model.header,
3426            login_url=model.login_url,
3427            password=model.password or "",
3428            session_token=model.session_token or "",
3429            session_token_response_key=model.session_token_response_key or "",
3430            username=model.username or "",
3431            validate_session_url=model.validate_session_url,
3432            config=config,
3433            parameters=model.parameters or {},
3434        )
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:
3436    def create_simple_retriever(
3437        self,
3438        model: SimpleRetrieverModel,
3439        config: Config,
3440        *,
3441        name: str,
3442        primary_key: Optional[Union[str, List[str], List[List[str]]]],
3443        request_options_provider: Optional[RequestOptionsProvider] = None,
3444        cursor: Optional[Cursor] = None,
3445        has_stop_condition_cursor: bool = False,
3446        is_client_side_incremental_sync: bool = False,
3447        transformations: List[RecordTransformation],
3448        file_uploader: Optional[DefaultFileUploader] = None,
3449        incremental_sync: Optional[
3450            Union[IncrementingCountCursorModel, DatetimeBasedCursorModel]
3451        ] = None,
3452        use_cache: Optional[bool] = None,
3453        log_formatter: Optional[Callable[[Response], Any]] = None,
3454        partition_router: Optional[PartitionRouter] = None,
3455        **kwargs: Any,
3456    ) -> SimpleRetriever:
3457        def _get_url(req: Requester) -> str:
3458            """
3459            Closure to get the URL from the requester. This is used to get the URL in the case of a lazy retriever.
3460            This is needed because the URL is not set until the requester is created.
3461            """
3462
3463            _url: str = (
3464                model.requester.url
3465                if hasattr(model.requester, "url") and model.requester.url is not None
3466                else req.get_url(stream_state=None, stream_slice=None, next_page_token=None)
3467            )
3468            _url_base: str = (
3469                model.requester.url_base
3470                if hasattr(model.requester, "url_base") and model.requester.url_base is not None
3471                else req.get_url_base(stream_state=None, stream_slice=None, next_page_token=None)
3472            )
3473
3474            return _url or _url_base
3475
3476        if cursor is None:
3477            cursor = FinalStateCursor(name, None, self._message_repository)
3478
3479        # A data feed drops the records the cursor considers already synced in the retriever, which sits downstream of
3480        # the paginator. Letting the record selector drop them as well would be redundant and would hide them from the
3481        # pagination stop condition, so a data feed never delegates that filtering to the record selector, whether
3482        # `is_client_side_incremental` is set or not. The `condition` from `record_filter` is intentionally left out of
3483        # the post-pagination filter and stays in the record selector, which preserves the existing behaviour: the
3484        # selector runs inside the page loop, so the records the condition rejects never reach the paginator's
3485        # accounting. Moving it downstream would start counting them.
3486        post_pagination_filter = (
3487            ClientSideIncrementalRecordFilterDecorator(
3488                config=config,
3489                parameters=model.parameters or {},
3490                condition=None,
3491                cursor=cursor,
3492            )
3493            if has_stop_condition_cursor
3494            else None
3495        )
3496        client_side_incremental_cursor = (
3497            cursor if is_client_side_incremental_sync and not post_pagination_filter else None
3498        )
3499        if post_pagination_filter and is_client_side_incremental_sync:
3500            LOGGER.warning(
3501                f"Stream {name}: `is_client_side_incremental` adds no record filtering when `is_data_feed` is set, "
3502                "as a data feed already filters on the cursor value. It still makes the record selector apply the "
3503                "transformations before the `record_filter` condition."
3504            )
3505
3506        decoder = (
3507            self._create_component_from_model(model=model.decoder, config=config)
3508            if model.decoder
3509            else JsonDecoder(parameters={})
3510        )
3511        record_selector = self._create_component_from_model(
3512            model=model.record_selector,
3513            name=name,
3514            config=config,
3515            decoder=decoder,
3516            transformations=transformations,
3517            client_side_incremental_sync_cursor=client_side_incremental_cursor,
3518            is_client_side_incremental_sync=is_client_side_incremental_sync,
3519            file_uploader=file_uploader,
3520        )
3521
3522        query_properties: Optional[QueryProperties] = None
3523        query_properties_key: Optional[str] = None
3524        self._ensure_query_properties_to_model(model.requester)
3525        if self._has_query_properties_in_request_parameters(model.requester):
3526            # It is better to be explicit about an error if PropertiesFromEndpoint is defined in multiple
3527            # places instead of default to request_parameters which isn't clearly documented
3528            if (
3529                hasattr(model.requester, "fetch_properties_from_endpoint")
3530                and model.requester.fetch_properties_from_endpoint
3531            ):
3532                raise ValueError(
3533                    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"
3534                )
3535
3536            query_properties_definitions = []
3537            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()
3538                if isinstance(request_parameter, QueryPropertiesModel):
3539                    query_properties_key = key
3540                    query_properties_definitions.append(request_parameter)
3541
3542            if len(query_properties_definitions) > 1:
3543                raise ValueError(
3544                    f"request_parameters only supports defining one QueryProperties field, but found {len(query_properties_definitions)} usages"
3545                )
3546
3547            if len(query_properties_definitions) == 1:
3548                query_properties = self._create_component_from_model(
3549                    model=query_properties_definitions[0], stream_name=name, config=config
3550                )
3551
3552            # Removes QueryProperties components from the interpolated mappings because it has been designed
3553            # to be used by the SimpleRetriever and will be resolved from the provider from the slice directly
3554            # instead of through jinja interpolation
3555            if hasattr(model.requester, "request_parameters") and isinstance(
3556                model.requester.request_parameters, Mapping
3557            ):
3558                model.requester.request_parameters = self._remove_query_properties(
3559                    model.requester.request_parameters
3560                )
3561        elif (
3562            hasattr(model.requester, "fetch_properties_from_endpoint")
3563            and model.requester.fetch_properties_from_endpoint
3564        ):
3565            # todo: Deprecate this condition once dependent connectors migrate to query_properties
3566            query_properties_definition = QueryPropertiesModel(
3567                type="QueryProperties",
3568                property_list=model.requester.fetch_properties_from_endpoint,
3569                always_include_properties=None,
3570                property_chunking=None,
3571            )  # type: ignore # $parameters has a default value
3572
3573            query_properties = self.create_query_properties(
3574                model=query_properties_definition,
3575                stream_name=name,
3576                config=config,
3577            )
3578        elif hasattr(model.requester, "query_properties") and model.requester.query_properties:
3579            query_properties = self.create_query_properties(
3580                model=model.requester.query_properties,
3581                stream_name=name,
3582                config=config,
3583            )
3584
3585        requester = self._create_component_from_model(
3586            model=model.requester,
3587            decoder=decoder,
3588            name=name,
3589            query_properties_key=query_properties_key,
3590            use_cache=use_cache,
3591            config=config,
3592        )
3593
3594        if not request_options_provider:
3595            request_options_provider = DefaultRequestOptionsProvider(parameters={})
3596        if isinstance(request_options_provider, DefaultRequestOptionsProvider) and isinstance(
3597            partition_router, PartitionRouter
3598        ):
3599            request_options_provider = partition_router
3600
3601        paginator = (
3602            self._create_component_from_model(
3603                model=model.paginator,
3604                config=config,
3605                url_base=_get_url(requester),
3606                extractor_model=model.record_selector.extractor,
3607                decoder=decoder,
3608                cursor_used_for_stop_condition=cursor if has_stop_condition_cursor else None,
3609            )
3610            if model.paginator
3611            else NoPagination(parameters={})
3612        )
3613
3614        ignore_stream_slicer_parameters_on_paginated_requests = (
3615            model.ignore_stream_slicer_parameters_on_paginated_requests or False
3616        )
3617
3618        if (
3619            model.partition_router
3620            and isinstance(model.partition_router, SubstreamPartitionRouterModel)
3621            and not bool(self._connector_state_manager.get_stream_state(name, None))
3622            and any(
3623                parent_stream_config.lazy_read_pointer
3624                for parent_stream_config in model.partition_router.parent_stream_configs
3625            )
3626        ):
3627            if incremental_sync:
3628                if incremental_sync.type != "DatetimeBasedCursor":
3629                    raise ValueError(
3630                        f"LazySimpleRetriever only supports DatetimeBasedCursor. Found: {incremental_sync.type}."
3631                    )
3632
3633                elif incremental_sync.step or incremental_sync.cursor_granularity:
3634                    raise ValueError(
3635                        f"Found more that one slice per parent. LazySimpleRetriever only supports single slice read for stream - {name}."
3636                    )
3637
3638            if model.decoder and model.decoder.type != "JsonDecoder":
3639                raise ValueError(
3640                    f"LazySimpleRetriever only supports JsonDecoder. Found: {model.decoder.type}."
3641                )
3642
3643            return LazySimpleRetriever(
3644                name=name,
3645                paginator=paginator,
3646                primary_key=primary_key,
3647                requester=requester,
3648                record_selector=record_selector,
3649                stream_slicer=_NO_STREAM_SLICING,
3650                request_option_provider=request_options_provider,
3651                config=config,
3652                ignore_stream_slicer_parameters_on_paginated_requests=ignore_stream_slicer_parameters_on_paginated_requests,
3653                post_pagination_filter=post_pagination_filter,
3654                parameters=model.parameters or {},
3655            )
3656
3657        if (
3658            model.record_selector.record_filter
3659            and model.pagination_reset
3660            and model.pagination_reset.limits
3661        ):
3662            raise ValueError("PaginationResetLimits are not supported while having record filter.")
3663
3664        return SimpleRetriever(
3665            name=name,
3666            paginator=paginator,
3667            primary_key=primary_key,
3668            requester=requester,
3669            record_selector=record_selector,
3670            stream_slicer=_NO_STREAM_SLICING,
3671            request_option_provider=request_options_provider,
3672            config=config,
3673            ignore_stream_slicer_parameters_on_paginated_requests=ignore_stream_slicer_parameters_on_paginated_requests,
3674            additional_query_properties=query_properties,
3675            log_formatter=self._get_log_formatter(log_formatter, name),
3676            pagination_tracker_factory=self._create_pagination_tracker_factory(
3677                model.pagination_reset, cursor
3678            ),
3679            post_pagination_filter=post_pagination_filter,
3680            parameters=model.parameters or {},
3681        )
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:
3759    def create_state_delegating_stream(
3760        self,
3761        model: StateDelegatingStreamModel,
3762        config: Config,
3763        **kwargs: Any,
3764    ) -> DefaultStream:
3765        if (
3766            model.full_refresh_stream.name != model.name
3767            or model.name != model.incremental_stream.name
3768        ):
3769            raise ValueError(
3770                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}."
3771            )
3772
3773        # Resolve api_retention_period with config context (supports Jinja2 interpolation)
3774        resolved_retention_period: Optional[str] = None
3775        if model.api_retention_period:
3776            interpolated_retention = InterpolatedString.create(
3777                model.api_retention_period, parameters=model.parameters or {}
3778            )
3779            resolved_value = interpolated_retention.eval(config=config)
3780            if resolved_value:
3781                resolved_retention_period = str(resolved_value)
3782
3783        if resolved_retention_period:
3784            for stream_model in (model.full_refresh_stream, model.incremental_stream):
3785                if isinstance(stream_model.incremental_sync, IncrementingCountCursorModel):
3786                    raise ValueError(
3787                        f"Stream '{model.name}' uses IncrementingCountCursor which is not supported "
3788                        f"with api_retention_period. IncrementingCountCursor does not use datetime-based "
3789                        f"cursors, so cursor age validation cannot be performed."
3790                    )
3791
3792        stream_state = self._connector_state_manager.get_stream_state(model.name, None)
3793
3794        if not stream_state:
3795            return self._create_component_from_model(  # type: ignore[no-any-return]
3796                model.full_refresh_stream, config=config, **kwargs
3797            )
3798
3799        incremental_stream: DefaultStream = self._create_component_from_model(
3800            model.incremental_stream, config=config, **kwargs
3801        )  # type: ignore[assignment]
3802
3803        # Only run cursor age validation for streams that are in the configured
3804        # catalog (or when no catalog was provided, e.g. during discover / connector
3805        # builder).  Streams not selected by the user but instantiated as parent-stream
3806        # dependencies must not go through this path because it emits state messages
3807        # that the destination does not know about, causing "Stream not found" crashes.
3808        stream_is_in_catalog = (
3809            not self._stream_name_to_configured_stream  # no catalog → validate by default
3810            or model.name in self._stream_name_to_configured_stream
3811        )
3812        if resolved_retention_period and stream_is_in_catalog:
3813            full_refresh_stream: DefaultStream = self._create_component_from_model(
3814                model.full_refresh_stream, config=config, **kwargs
3815            )  # type: ignore[assignment]
3816            if self._is_cursor_older_than_retention_period(
3817                stream_state,
3818                full_refresh_stream.cursor,
3819                incremental_stream.cursor,
3820                resolved_retention_period,
3821                model.name,
3822            ):
3823                # Clear state BEFORE constructing the full_refresh_stream so that
3824                # its cursor starts from start_date instead of the stale cursor.
3825                self._connector_state_manager.update_state_for_stream(model.name, None, {})
3826                state_message = self._connector_state_manager.create_state_message(model.name, None)
3827                self._message_repository.emit_message(state_message)
3828                return self._create_component_from_model(  # type: ignore[no-any-return]
3829                    model.full_refresh_stream, config=config, **kwargs
3830                )
3831
3832        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:
3931    def create_async_retriever(
3932        self,
3933        model: AsyncRetrieverModel,
3934        config: Config,
3935        *,
3936        name: str,
3937        primary_key: Optional[
3938            Union[str, List[str], List[List[str]]]
3939        ],  # this seems to be needed to match create_simple_retriever
3940        stream_slicer: Optional[StreamSlicer],
3941        client_side_incremental_sync: Optional[Dict[str, Any]] = None,
3942        transformations: List[RecordTransformation],
3943        **kwargs: Any,
3944    ) -> AsyncRetriever:
3945        if model.download_target_requester and not model.download_target_extractor:
3946            raise ValueError(
3947                f"`download_target_extractor` required if using a `download_target_requester`"
3948            )
3949
3950        def _get_download_retriever(
3951            requester: Requester, extractor: RecordExtractor, _decoder: Decoder
3952        ) -> SimpleRetriever:
3953            # We create a record selector for the download retriever
3954            # with no schema normalization and no transformations, neither record filter
3955            # as all this occurs in the record_selector of the AsyncRetriever
3956            record_selector = RecordSelector(
3957                extractor=extractor,
3958                name=name,
3959                record_filter=None,
3960                transformations=[],
3961                schema_normalization=TypeTransformer(TransformConfig.NoTransform),
3962                config=config,
3963                parameters={},
3964            )
3965            paginator = (
3966                self._create_component_from_model(
3967                    model=model.download_paginator,
3968                    decoder=_decoder,
3969                    config=config,
3970                    url_base="",
3971                )
3972                if model.download_paginator
3973                else NoPagination(parameters={})
3974            )
3975
3976            return SimpleRetriever(
3977                requester=requester,
3978                record_selector=record_selector,
3979                primary_key=None,
3980                name=name,
3981                paginator=paginator,
3982                config=config,
3983                parameters={},
3984                log_formatter=self._get_log_formatter(None, name),
3985            )
3986
3987        def _get_job_timeout() -> datetime.timedelta:
3988            user_defined_timeout: Optional[int] = (
3989                int(
3990                    InterpolatedString.create(
3991                        str(model.polling_job_timeout),
3992                        parameters={},
3993                    ).eval(config)
3994                )
3995                if model.polling_job_timeout
3996                else None
3997            )
3998
3999            # check for user defined timeout during the test read or 15 minutes
4000            test_read_timeout = datetime.timedelta(minutes=user_defined_timeout or 15)
4001            # default value for non-connector builder is 60 minutes.
4002            default_sync_timeout = datetime.timedelta(minutes=user_defined_timeout or 60)
4003
4004            return (
4005                test_read_timeout if self._emit_connector_builder_messages else default_sync_timeout
4006            )
4007
4008        decoder = (
4009            self._create_component_from_model(model=model.decoder, config=config)
4010            if model.decoder
4011            else JsonDecoder(parameters={})
4012        )
4013        record_selector = self._create_component_from_model(
4014            model=model.record_selector,
4015            config=config,
4016            decoder=decoder,
4017            name=name,
4018            transformations=transformations,
4019            client_side_incremental_sync=client_side_incremental_sync,
4020        )
4021
4022        stream_slicer = stream_slicer or SinglePartitionRouter(parameters={})
4023        if self._should_limit_slices_fetched():
4024            stream_slicer = cast(
4025                StreamSlicer,
4026                StreamSlicerTestReadDecorator(
4027                    wrapped_slicer=stream_slicer,
4028                    maximum_number_of_slices=self._limit_slices_fetched or 5,
4029                ),
4030            )
4031
4032        creation_requester = self._create_component_from_model(
4033            model=model.creation_requester,
4034            decoder=decoder,
4035            config=config,
4036            name=f"job creation - {name}",
4037        )
4038        polling_requester = self._create_component_from_model(
4039            model=model.polling_requester,
4040            decoder=decoder,
4041            config=config,
4042            name=f"job polling - {name}",
4043        )
4044        job_download_components_name = f"job download - {name}"
4045        download_decoder = (
4046            self._create_component_from_model(model=model.download_decoder, config=config)
4047            if model.download_decoder
4048            else JsonDecoder(parameters={})
4049        )
4050        download_extractor = (
4051            self._create_component_from_model(
4052                model=model.download_extractor,
4053                config=config,
4054                decoder=download_decoder,
4055                parameters=model.parameters,
4056            )
4057            if model.download_extractor
4058            else DpathExtractor(
4059                [],
4060                config=config,
4061                decoder=download_decoder,
4062                parameters=model.parameters or {},
4063            )
4064        )
4065        download_requester = self._create_component_from_model(
4066            model=model.download_requester,
4067            decoder=download_decoder,
4068            config=config,
4069            name=job_download_components_name,
4070        )
4071        download_retriever = _get_download_retriever(
4072            download_requester, download_extractor, download_decoder
4073        )
4074        abort_requester = (
4075            self._create_component_from_model(
4076                model=model.abort_requester,
4077                decoder=decoder,
4078                config=config,
4079                name=f"job abort - {name}",
4080            )
4081            if model.abort_requester
4082            else None
4083        )
4084        delete_requester = (
4085            self._create_component_from_model(
4086                model=model.delete_requester,
4087                decoder=decoder,
4088                config=config,
4089                name=f"job delete - {name}",
4090            )
4091            if model.delete_requester
4092            else None
4093        )
4094        download_target_requester = (
4095            self._create_component_from_model(
4096                model=model.download_target_requester,
4097                decoder=decoder,
4098                config=config,
4099                name=f"job extract_url - {name}",
4100            )
4101            if model.download_target_requester
4102            else None
4103        )
4104        status_extractor = self._create_component_from_model(
4105            model=model.status_extractor, decoder=decoder, config=config, name=name
4106        )
4107        download_target_extractor = (
4108            self._create_component_from_model(
4109                model=model.download_target_extractor,
4110                decoder=decoder,
4111                config=config,
4112                name=name,
4113            )
4114            if model.download_target_extractor
4115            else None
4116        )
4117
4118        job_repository: AsyncJobRepository = AsyncHttpJobRepository(
4119            creation_requester=creation_requester,
4120            polling_requester=polling_requester,
4121            download_retriever=download_retriever,
4122            download_target_requester=download_target_requester,
4123            abort_requester=abort_requester,
4124            delete_requester=delete_requester,
4125            status_extractor=status_extractor,
4126            status_mapping=self._create_async_job_status_mapping(model.status_mapping, config),
4127            download_target_extractor=download_target_extractor,
4128            job_timeout=_get_job_timeout(),
4129        )
4130
4131        failed_retry_wait_time_in_seconds: Optional[int] = (
4132            int(
4133                InterpolatedString.create(
4134                    str(model.failed_retry_wait_time_in_seconds),
4135                    parameters={},
4136                ).eval(config)
4137            )
4138            if model.failed_retry_wait_time_in_seconds
4139            else None
4140        )
4141
4142        async_job_partition_router = AsyncJobPartitionRouter(
4143            job_orchestrator_factory=lambda stream_slices: AsyncJobOrchestrator(
4144                job_repository,
4145                stream_slices,
4146                self._job_tracker,
4147                self._message_repository,
4148                # FIXME work would need to be done here in order to detect if a stream as a parent stream that is bulk
4149                has_bulk_parent=False,
4150                # set the `job_max_retry` to 1 for the `Connector Builder`` use-case.
4151                # `None` == default retry is set to 3 attempts, under the hood.
4152                job_max_retry=1 if self._emit_connector_builder_messages else None,
4153                failed_retry_wait_time_in_seconds=failed_retry_wait_time_in_seconds,
4154            ),
4155            stream_slicer=stream_slicer,
4156            config=config,
4157            parameters=model.parameters or {},
4158        )
4159
4160        return AsyncRetriever(
4161            record_selector=record_selector,
4162            stream_slicer=async_job_partition_router,
4163            config=config,
4164            parameters=model.parameters or {},
4165        )
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:
4167    def create_spec(self, model: SpecModel, config: Config, **kwargs: Any) -> Spec:
4168        config_migrations = [
4169            self._create_component_from_model(migration, config)
4170            for migration in (
4171                model.config_normalization_rules.config_migrations
4172                if (
4173                    model.config_normalization_rules
4174                    and model.config_normalization_rules.config_migrations
4175                )
4176                else []
4177            )
4178        ]
4179        config_transformations = [
4180            self._create_component_from_model(transformation, config)
4181            for transformation in (
4182                model.config_normalization_rules.transformations
4183                if (
4184                    model.config_normalization_rules
4185                    and model.config_normalization_rules.transformations
4186                )
4187                else []
4188            )
4189        ]
4190        config_validations = [
4191            self._create_component_from_model(validation, config)
4192            for validation in (
4193                model.config_normalization_rules.validations
4194                if (
4195                    model.config_normalization_rules
4196                    and model.config_normalization_rules.validations
4197                )
4198                else []
4199            )
4200        ]
4201
4202        return Spec(
4203            connection_specification=model.connection_specification,
4204            documentation_url=model.documentation_url,
4205            advanced_auth=model.advanced_auth,
4206            parameters={},
4207            config_migrations=config_migrations,
4208            config_transformations=config_transformations,
4209            config_validations=config_validations,
4210        )
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:
4212    def create_substream_partition_router(
4213        self,
4214        model: SubstreamPartitionRouterModel,
4215        config: Config,
4216        *,
4217        stream_name: str,
4218        **kwargs: Any,
4219    ) -> SubstreamPartitionRouter:
4220        parent_stream_configs = []
4221        if model.parent_stream_configs:
4222            parent_stream_configs.extend(
4223                [
4224                    self.create_parent_stream_config_with_substream_wrapper(
4225                        model=parent_stream_config, config=config, stream_name=stream_name, **kwargs
4226                    )
4227                    for parent_stream_config in model.parent_stream_configs
4228                ]
4229            )
4230
4231        return SubstreamPartitionRouter(
4232            parent_stream_configs=parent_stream_configs,
4233            parameters=model.parameters or {},
4234            config=config,
4235        )
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:
4237    def create_parent_stream_config_with_substream_wrapper(
4238        self, model: ParentStreamConfigModel, config: Config, *, stream_name: str, **kwargs: Any
4239    ) -> Any:
4240        child_state = self._connector_state_manager.get_stream_state(stream_name, None)
4241        if NO_CURSOR_STATE_KEY in child_state:
4242            # Full refresh streams checkpoint a `{NO_CURSOR_STATE_KEY: true}` sentinel. When such a
4243            # stream is later converted to incremental with an incremental_dependency parent,
4244            # `_instantiate_parent_stream_state_manager` would treat the sentinel's boolean as a legacy
4245            # cursor value and re-key it under the parent's cursor field, crashing cursor initialization.
4246            child_state = {
4247                key: value for key, value in child_state.items() if key != NO_CURSOR_STATE_KEY
4248            }
4249
4250        parent_state: Optional[Mapping[str, Any]] = (
4251            child_state if model.incremental_dependency and child_state else None
4252        )
4253        connector_state_manager = self._instantiate_parent_stream_state_manager(
4254            child_state, config, model, parent_state
4255        )
4256
4257        substream_factory = ModelToComponentFactory(
4258            custom_components_trusted=self._custom_components_trusted,
4259            connector_state_manager=connector_state_manager,
4260            limit_pages_fetched_per_slice=self._limit_pages_fetched_per_slice,
4261            limit_slices_fetched=self._limit_slices_fetched,
4262            emit_connector_builder_messages=self._emit_connector_builder_messages,
4263            disable_retries=self._disable_retries,
4264            disable_cache=self._disable_cache,
4265            message_repository=StateFilteringMessageRepository(
4266                LogAppenderMessageRepositoryDecorator(
4267                    {
4268                        "airbyte_cdk": {"stream": {"is_substream": True}},
4269                        "http": {"is_auxiliary": True},
4270                    },
4271                    self._message_repository,
4272                    self._evaluate_log_level(self._emit_connector_builder_messages),
4273                ),
4274            ),
4275            api_budget=self._api_budget,
4276            # Share the authenticator registry so parent and child streams draw from the
4277            # same token quota counters
4278            rate_limited_authenticators=self._rate_limited_authenticators,
4279        )
4280
4281        return substream_factory.create_parent_stream_config(
4282            model=model, config=config, stream_name=stream_name, **kwargs
4283        )
4343    @staticmethod
4344    def create_wait_time_from_header(
4345        model: WaitTimeFromHeaderModel, config: Config, **kwargs: Any
4346    ) -> WaitTimeFromHeaderBackoffStrategy:
4347        return WaitTimeFromHeaderBackoffStrategy(
4348            header=model.header,
4349            parameters=model.parameters or {},
4350            config=config,
4351            regex=model.regex,
4352            max_waiting_time_in_seconds=model.max_waiting_time_in_seconds,
4353        )
4355    @staticmethod
4356    def create_wait_until_time_from_header(
4357        model: WaitUntilTimeFromHeaderModel, config: Config, **kwargs: Any
4358    ) -> WaitUntilTimeFromHeaderBackoffStrategy:
4359        return WaitUntilTimeFromHeaderBackoffStrategy(
4360            header=model.header,
4361            parameters=model.parameters or {},
4362            config=config,
4363            min_wait=model.min_wait,
4364            regex=model.regex,
4365            max_waiting_time_in_seconds=model.max_waiting_time_in_seconds,
4366        )
def get_message_repository(self) -> airbyte_cdk.MessageRepository:
4368    def get_message_repository(self) -> MessageRepository:
4369        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:
4374    @staticmethod
4375    def create_components_mapping_definition(
4376        model: ComponentMappingDefinitionModel, config: Config, **kwargs: Any
4377    ) -> ComponentMappingDefinition:
4378        interpolated_value = InterpolatedString.create(
4379            model.value, parameters=model.parameters or {}
4380        )
4381        field_path = [
4382            InterpolatedString.create(path, parameters=model.parameters or {})
4383            for path in model.field_path
4384        ]
4385        return ComponentMappingDefinition(
4386            field_path=field_path,  # type: ignore[arg-type] # field_path can be str and InterpolatedString
4387            value=interpolated_value,
4388            value_type=ModelToComponentFactory._json_schema_type_name_to_type(model.value_type),
4389            create_or_update=model.create_or_update,
4390            condition=model.condition,
4391            parameters=model.parameters or {},
4392        )
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:
4394    def create_http_components_resolver(
4395        self, model: HttpComponentsResolverModel, config: Config, stream_name: Optional[str] = None
4396    ) -> Any:
4397        retriever = self._create_component_from_model(
4398            model=model.retriever,
4399            config=config,
4400            name=f"{stream_name if stream_name else '__http_components_resolver'}",
4401            primary_key=None,
4402            stream_slicer=self._build_stream_slicer_from_partition_router(model.retriever, config),
4403            transformations=[],
4404        )
4405
4406        components_mapping = []
4407        for component_mapping_definition_model in model.components_mapping:
4408            if component_mapping_definition_model.condition:
4409                raise ValueError("`condition` is only supported for     `ConfigComponentsResolver`")
4410            components_mapping.append(
4411                self._create_component_from_model(
4412                    model=component_mapping_definition_model,
4413                    value_type=ModelToComponentFactory._json_schema_type_name_to_type(
4414                        component_mapping_definition_model.value_type
4415                    ),
4416                    config=config,
4417                )
4418            )
4419
4420        return HttpComponentsResolver(
4421            retriever=retriever,
4422            stream_slicer=self._build_stream_slicer_from_partition_router(model.retriever, config),
4423            config=config,
4424            components_mapping=components_mapping,
4425            parameters=model.parameters or {},
4426        )
@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:
4428    @staticmethod
4429    def create_stream_config(
4430        model: StreamConfigModel, config: Config, **kwargs: Any
4431    ) -> StreamConfig:
4432        model_configs_pointer: List[Union[InterpolatedString, str]] = (
4433            [x for x in model.configs_pointer] if model.configs_pointer else []
4434        )
4435
4436        return StreamConfig(
4437            configs_pointer=model_configs_pointer,
4438            default_values=model.default_values,
4439            parameters=model.parameters or {},
4440        )
def create_config_components_resolver( self, model: airbyte_cdk.sources.declarative.models.declarative_component_schema.ConfigComponentsResolver, config: Mapping[str, Any]) -> Any:
4442    def create_config_components_resolver(
4443        self,
4444        model: ConfigComponentsResolverModel,
4445        config: Config,
4446    ) -> Any:
4447        model_stream_configs = (
4448            model.stream_config if isinstance(model.stream_config, list) else [model.stream_config]
4449        )
4450
4451        stream_configs = [
4452            self._create_component_from_model(
4453                stream_config, config=config, parameters=model.parameters or {}
4454            )
4455            for stream_config in model_stream_configs
4456        ]
4457
4458        components_mapping = [
4459            self._create_component_from_model(
4460                model=components_mapping_definition_model,
4461                value_type=ModelToComponentFactory._json_schema_type_name_to_type(
4462                    components_mapping_definition_model.value_type
4463                ),
4464                config=config,
4465                parameters=model.parameters,
4466            )
4467            for components_mapping_definition_model in model.components_mapping
4468        ]
4469
4470        return ConfigComponentsResolver(
4471            stream_configs=stream_configs,
4472            config=config,
4473            components_mapping=components_mapping,
4474            parameters=model.parameters or {},
4475        )
4477    def create_parametrized_components_resolver(
4478        self,
4479        model: ParametrizedComponentsResolverModel,
4480        config: Config,
4481    ) -> ParametrizedComponentsResolver:
4482        stream_parameters = StreamParametersDefinition(
4483            list_of_parameters_for_stream=model.stream_parameters.list_of_parameters_for_stream
4484        )
4485
4486        components_mapping = []
4487        for components_mapping_definition_model in model.components_mapping:
4488            if components_mapping_definition_model.condition:
4489                raise ValueError("`condition` is only supported for `ConfigComponentsResolver`")
4490            components_mapping.append(
4491                self._create_component_from_model(
4492                    model=components_mapping_definition_model,
4493                    value_type=ModelToComponentFactory._json_schema_type_name_to_type(
4494                        components_mapping_definition_model.value_type
4495                    ),
4496                    config=config,
4497                )
4498            )
4499        return ParametrizedComponentsResolver(
4500            stream_parameters=stream_parameters,
4501            config=config,
4502            components_mapping=components_mapping,
4503            parameters=model.parameters or {},
4504        )
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:
4528    def create_http_api_budget(
4529        self, model: HTTPAPIBudgetModel, config: Config, **kwargs: Any
4530    ) -> HttpAPIBudget:
4531        policies = [
4532            self._create_component_from_model(model=policy, config=config)
4533            for policy in model.policies
4534        ]
4535
4536        return HttpAPIBudget(
4537            policies=policies,
4538            ratelimit_reset_header=model.ratelimit_reset_header or "ratelimit-reset",
4539            ratelimit_remaining_header=model.ratelimit_remaining_header or "ratelimit-remaining",
4540            status_codes_for_ratelimit_hit=model.status_codes_for_ratelimit_hit or [429],
4541        )
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:
4543    def create_fixed_window_call_rate_policy(
4544        self, model: FixedWindowCallRatePolicyModel, config: Config, **kwargs: Any
4545    ) -> FixedWindowCallRatePolicy:
4546        matchers = [
4547            self._create_component_from_model(model=matcher, config=config)
4548            for matcher in model.matchers
4549        ]
4550
4551        # Set the initial reset timestamp to 10 days from now.
4552        # This value will be updated by the first request.
4553        return FixedWindowCallRatePolicy(
4554            next_reset_ts=datetime.datetime.now() + datetime.timedelta(days=10),
4555            period=parse_duration(model.period),
4556            call_limit=model.call_limit,
4557            matchers=matchers,
4558        )
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:
4560    def create_file_uploader(
4561        self, model: FileUploaderModel, config: Config, **kwargs: Any
4562    ) -> FileUploader:
4563        name = "File Uploader"
4564        requester = self._create_component_from_model(
4565            model=model.requester,
4566            config=config,
4567            name=name,
4568            **kwargs,
4569        )
4570        download_target_extractor = self._create_component_from_model(
4571            model=model.download_target_extractor,
4572            config=config,
4573            name=name,
4574            **kwargs,
4575        )
4576        emit_connector_builder_messages = self._emit_connector_builder_messages
4577        file_uploader = DefaultFileUploader(
4578            requester=requester,
4579            download_target_extractor=download_target_extractor,
4580            config=config,
4581            file_writer=NoopFileWriter()
4582            if emit_connector_builder_messages
4583            else LocalFileSystemFileWriter(),
4584            parameters=model.parameters or {},
4585            filename_extractor=model.filename_extractor if model.filename_extractor else None,
4586        )
4587
4588        return (
4589            ConnectorBuilderFileUploader(file_uploader)
4590            if emit_connector_builder_messages
4591            else file_uploader
4592        )
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:
4594    def create_moving_window_call_rate_policy(
4595        self, model: MovingWindowCallRatePolicyModel, config: Config, **kwargs: Any
4596    ) -> MovingWindowCallRatePolicy:
4597        rates = [
4598            self._create_component_from_model(model=rate, config=config) for rate in model.rates
4599        ]
4600        matchers = [
4601            self._create_component_from_model(model=matcher, config=config)
4602            for matcher in model.matchers
4603        ]
4604        return MovingWindowCallRatePolicy(
4605            rates=rates,
4606            matchers=matchers,
4607        )
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:
4609    def create_unlimited_call_rate_policy(
4610        self, model: UnlimitedCallRatePolicyModel, config: Config, **kwargs: Any
4611    ) -> UnlimitedCallRatePolicy:
4612        matchers = [
4613            self._create_component_from_model(model=matcher, config=config)
4614            for matcher in model.matchers
4615        ]
4616
4617        return UnlimitedCallRatePolicy(
4618            matchers=matchers,
4619        )
def create_rate( self, model: airbyte_cdk.sources.declarative.models.declarative_component_schema.Rate, config: Mapping[str, Any], **kwargs: Any) -> airbyte_cdk.Rate:
4621    def create_rate(self, model: RateModel, config: Config, **kwargs: Any) -> Rate:
4622        interpolated_limit = InterpolatedString.create(str(model.limit), parameters={})
4623        return Rate(
4624            limit=int(interpolated_limit.eval(config=config)),
4625            interval=parse_duration(model.interval),
4626        )
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:
4628    def create_http_request_matcher(
4629        self, model: HttpRequestRegexMatcherModel, config: Config, **kwargs: Any
4630    ) -> HttpRequestRegexMatcher:
4631        weight = model.weight
4632        if weight is not None:
4633            if isinstance(weight, str):
4634                weight = int(InterpolatedString.create(weight, parameters={}).eval(config))
4635            else:
4636                weight = int(weight)
4637            if weight < 1:
4638                raise ValueError(f"weight must be >= 1, got {weight}")
4639        return HttpRequestRegexMatcher(
4640            method=model.method,
4641            url_base=model.url_base,
4642            url_path_pattern=model.url_path_pattern,
4643            params=model.params,
4644            headers=model.headers,
4645            weight=weight,
4646        )
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:
4648    def create_rate_limited_multiple_token_authenticator(
4649        self,
4650        model: RateLimitedMultipleTokenAuthenticatorModel,
4651        config: Config,
4652        **kwargs: Any,
4653    ) -> RateLimitedMultipleTokenAuthenticator:
4654        if isinstance(model.tokens, str):
4655            tokens_value = InterpolatedString.create(model.tokens, parameters={}).eval(config)
4656            delimiter = model.token_delimiter or ","
4657            tokens = [
4658                token.strip() for token in str(tokens_value).split(delimiter) if token.strip()
4659            ]
4660        else:
4661            tokens = [
4662                token_value
4663                for token in model.tokens
4664                if (
4665                    token_value := str(
4666                        InterpolatedString.create(token, parameters={}).eval(config)
4667                    ).strip()
4668                )
4669            ]
4670
4671        quota_specs = [
4672            {
4673                "name": quota_model.name,
4674                "remaining_path": quota_model.remaining_path,
4675                "reset_path": quota_model.reset_path,
4676                "limit_path": quota_model.limit_path,
4677                "remaining_header": quota_model.remaining_header,
4678                "reset_header": quota_model.reset_header,
4679                "limit_header": quota_model.limit_header,
4680                # Normalize the same way as the runtime TokenQuota below, so an omitted field
4681                # and an explicit `[]` key identically and keep sharing one set of counters.
4682                "exhaustion_status_codes": quota_model.exhaustion_status_codes or [],
4683                "matchers": [
4684                    {
4685                        "method": matcher_model.method,
4686                        "url_base": matcher_model.url_base,
4687                        "url_path_pattern": matcher_model.url_path_pattern,
4688                        "params": matcher_model.params,
4689                        "headers": matcher_model.headers,
4690                        "weight": matcher_model.weight,
4691                    }
4692                    for matcher_model in quota_model.matchers or []
4693                ],
4694            }
4695            for quota_model in model.quotas
4696        ]
4697
4698        quota_status_url = str(
4699            InterpolatedString.create(model.quota_status_source.url, parameters={}).eval(config)
4700        )
4701        quota_status_http_method = (
4702            model.quota_status_source.http_method.value
4703            if model.quota_status_source.http_method
4704            else "GET"
4705        )
4706        quota_status_headers = {
4707            key: str(InterpolatedString.create(value, parameters={}).eval(config))
4708            for key, value in (model.quota_status_source.request_headers or {}).items()
4709        }
4710        # Normalize the same way as the quota specs above, so an omitted field and an explicit
4711        # `[]` key identically and keep sharing one set of counters. Deduplicated as well as
4712        # sorted, because the runtime turns this into a set: without it `[404]` and `[404, 404]`
4713        # would key differently and stop sharing counters while behaving identically.
4714        quota_status_unavailable_status_codes = sorted(
4715            set(model.quota_status_source.unavailable_status_codes or [])
4716        )
4717        auth_method = model.auth_method or "Bearer"
4718        header = model.header or "Authorization"
4719        max_wait_time_str = str(
4720            InterpolatedString.create(model.max_wait_time or "PT2H", parameters={}).eval(config)
4721        )
4722        max_wait_time = parse_duration(max_wait_time_str)
4723        if not isinstance(max_wait_time, datetime.timedelta):
4724            raise ValueError(
4725                f"max_wait_time must be a fixed-length ISO 8601 duration (e.g. 'PT2H'); "
4726                f"calendar-unit durations like '{max_wait_time_str}' are not supported"
4727            )
4728        budget_reserve_fraction = (
4729            model.budget_reserve_fraction if model.budget_reserve_fraction is not None else 0.1
4730        )
4731        budget_min_reserve = (
4732            model.budget_min_reserve if model.budget_min_reserve is not None else 50
4733        )
4734
4735        # Reuse the same instance for identical definitions so that all streams share the
4736        # same token quota counters (similar to how api_budget is shared). The key is built
4737        # from the resolved constructor arguments rather than the raw model so that
4738        # stream-specific `$parameters` propagated onto the model (and its nested components)
4739        # cannot break instance sharing.
4740        cache_key = json.dumps(
4741            {
4742                "tokens": tokens,
4743                "quotas": quota_specs,
4744                "quota_status_url": quota_status_url,
4745                "quota_status_http_method": quota_status_http_method,
4746                "quota_status_headers": quota_status_headers,
4747                "quota_status_unavailable_status_codes": quota_status_unavailable_status_codes,
4748                "auth_method": auth_method,
4749                "header": header,
4750                "max_wait_time": max_wait_time.total_seconds(),
4751                "budget_reserve_fraction": budget_reserve_fraction,
4752                "budget_min_reserve": budget_min_reserve,
4753            },
4754            sort_keys=True,
4755        )
4756        if cache_key in self._rate_limited_authenticators:
4757            return self._rate_limited_authenticators[cache_key]
4758
4759        quotas = [
4760            TokenQuota(
4761                name=quota_model.name,
4762                remaining_path=quota_model.remaining_path,
4763                reset_path=quota_model.reset_path,
4764                limit_path=quota_model.limit_path,
4765                remaining_header=quota_model.remaining_header,
4766                reset_header=quota_model.reset_header,
4767                limit_header=quota_model.limit_header,
4768                exhaustion_status_codes=quota_model.exhaustion_status_codes or [],
4769                matchers=[
4770                    self.create_http_request_matcher(matcher_model, config)
4771                    for matcher_model in quota_model.matchers or []
4772                ],
4773            )
4774            for quota_model in model.quotas
4775        ]
4776
4777        authenticator = RateLimitedMultipleTokenAuthenticator(
4778            tokens=tokens,
4779            quotas=quotas,
4780            quota_status_url=quota_status_url,
4781            quota_status_http_method=quota_status_http_method,
4782            quota_status_headers=quota_status_headers,
4783            quota_status_unavailable_status_codes=quota_status_unavailable_status_codes,
4784            auth_method=auth_method,
4785            header=header,
4786            max_wait_time=max_wait_time,
4787            budget_reserve_fraction=budget_reserve_fraction,
4788            budget_min_reserve=budget_min_reserve,
4789        )
4790        self._rate_limited_authenticators[cache_key] = authenticator
4791        return authenticator
def set_api_budget( self, component_definition: Mapping[str, Any], config: Mapping[str, Any]) -> None:
4793    def set_api_budget(self, component_definition: ComponentDefinition, config: Config) -> None:
4794        self._api_budget = self.create_component(
4795            model_type=HTTPAPIBudgetModel, component_definition=component_definition, config=config
4796        )
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:
4798    def create_grouping_partition_router(
4799        self,
4800        model: GroupingPartitionRouterModel,
4801        config: Config,
4802        *,
4803        stream_name: str,
4804        **kwargs: Any,
4805    ) -> GroupingPartitionRouter:
4806        underlying_router = self._create_component_from_model(
4807            model=model.underlying_partition_router,
4808            config=config,
4809            stream_name=stream_name,
4810            **kwargs,
4811        )
4812        if model.group_size < 1:
4813            raise ValueError(f"Group size must be greater than 0, got {model.group_size}")
4814
4815        # Request options in underlying partition routers are not supported for GroupingPartitionRouter
4816        # because they are specific to individual partitions and cannot be aggregated or handled
4817        # when grouping, potentially leading to incorrect API calls. Any request customization
4818        # should be managed at the stream level through the requester's configuration.
4819        if isinstance(underlying_router, SubstreamPartitionRouter):
4820            if any(
4821                parent_config.request_option
4822                for parent_config in underlying_router.parent_stream_configs
4823            ):
4824                raise ValueError("Request options are not supported for GroupingPartitionRouter.")
4825
4826        if isinstance(underlying_router, ListPartitionRouter):
4827            if underlying_router.request_option:
4828                raise ValueError("Request options are not supported for GroupingPartitionRouter.")
4829
4830        return GroupingPartitionRouter(
4831            group_size=model.group_size,
4832            underlying_partition_router=underlying_router,
4833            deduplicate=model.deduplicate if model.deduplicate is not None else True,
4834            config=config,
4835        )
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:
4837    def create_union_partition_router(
4838        self,
4839        model: UnionPartitionRouterModel,
4840        config: Config,
4841        *,
4842        stream_name: str,
4843        **kwargs: Any,
4844    ) -> UnionPartitionRouter:
4845        # The schema enforces minItems: 2 for manifests; this guard covers construction paths
4846        # that bypass JSON-schema validation (the generated model carries no min_items constraint).
4847        if len(model.partition_routers) < 2:
4848            raise ValueError(
4849                f"UnionPartitionRouter for stream {stream_name} needs at least 2 child partition routers"
4850            )
4851
4852        partition_routers = [
4853            self._create_component_from_model(
4854                model=child,
4855                config=config,
4856                stream_name=stream_name,
4857                **kwargs,
4858            )
4859            for child in model.partition_routers
4860        ]
4861
4862        # partition_field depends only on config/parameters, so it is evaluated once at build
4863        # time; the runtime component always receives a plain string.
4864        partition_field = InterpolatedString.create(
4865            model.partition_field, parameters=model.parameters or {}
4866        ).eval(config)
4867
4868        # Fail fast at build time when a built-in child router is statically known to emit a
4869        # partition field different from the union's. CustomPartitionRouter children are opaque
4870        # and can only be validated at runtime.
4871        for child_model in model.partition_routers:
4872            child_partition_fields: List[str] = []
4873            if isinstance(child_model, ListPartitionRouterModel):
4874                child_partition_fields.append(
4875                    InterpolatedString.create(
4876                        child_model.cursor_field, parameters=child_model.parameters or {}
4877                    ).eval(config)
4878                )
4879            elif isinstance(child_model, SubstreamPartitionRouterModel):
4880                for parent_stream_config in child_model.parent_stream_configs:
4881                    child_partition_fields.append(
4882                        InterpolatedString.create(
4883                            parent_stream_config.partition_field,
4884                            parameters=parent_stream_config.parameters
4885                            or child_model.parameters
4886                            or {},
4887                        ).eval(config)
4888                    )
4889            elif isinstance(child_model, UnionPartitionRouterModel):
4890                child_partition_fields.append(
4891                    InterpolatedString.create(
4892                        child_model.partition_field, parameters=child_model.parameters or {}
4893                    ).eval(config)
4894                )
4895            for child_partition_field in child_partition_fields:
4896                if child_partition_field != partition_field:
4897                    raise ValueError(
4898                        f"UnionPartitionRouter expects all child partition routers to emit the "
4899                        f"partition field '{partition_field}', but a "
4900                        f"{child_model.type} child emits '{child_partition_field}'."
4901                    )
4902
4903        # A union slice comes from exactly one child partition router, so request options
4904        # declared on children cannot be applied consistently to requests built from the
4905        # normalized union slices. Partition values should be consumed via interpolation
4906        # (e.g. stream_partition) instead. Note that this validation only covers built-in
4907        # router types; CustomPartitionRouter children are opaque, so any request options
4908        # they implement internally cannot be detected or rejected here.
4909        for router in partition_routers:
4910            if isinstance(router, SubstreamPartitionRouter):
4911                if any(
4912                    parent_config.request_option for parent_config in router.parent_stream_configs
4913                ):
4914                    raise ValueError("Request options are not supported for UnionPartitionRouter.")
4915            if isinstance(router, ListPartitionRouter) and router.request_option:
4916                raise ValueError("Request options are not supported for UnionPartitionRouter.")
4917
4918        return UnionPartitionRouter(
4919            partition_routers=partition_routers,
4920            partition_field=partition_field,
4921            parameters=model.parameters or {},
4922        )