airbyte-cdk 6.33.0.dev0__py3-none-any.whl → 6.33.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- airbyte_cdk/sources/declarative/auth/token.py +3 -8
- airbyte_cdk/sources/declarative/concurrent_declarative_source.py +13 -2
- airbyte_cdk/sources/declarative/declarative_component_schema.yaml +15 -212
- airbyte_cdk/sources/declarative/incremental/datetime_based_cursor.py +7 -6
- airbyte_cdk/sources/declarative/manifest_declarative_source.py +0 -4
- airbyte_cdk/sources/declarative/models/declarative_component_schema.py +10 -169
- airbyte_cdk/sources/declarative/parsers/model_to_component_factory.py +34 -171
- airbyte_cdk/sources/declarative/partition_routers/list_partition_router.py +4 -2
- airbyte_cdk/sources/declarative/partition_routers/substream_partition_router.py +26 -18
- airbyte_cdk/sources/declarative/requesters/http_requester.py +5 -4
- airbyte_cdk/sources/declarative/requesters/paginators/default_paginator.py +6 -5
- airbyte_cdk/sources/declarative/requesters/request_option.py +83 -4
- airbyte_cdk/sources/declarative/requesters/request_options/datetime_based_request_options_provider.py +7 -6
- airbyte_cdk/sources/declarative/retrievers/simple_retriever.py +4 -1
- airbyte_cdk/sources/streams/call_rate.py +71 -84
- airbyte_cdk/utils/mapping_helpers.py +86 -27
- {airbyte_cdk-6.33.0.dev0.dist-info → airbyte_cdk-6.33.1.dist-info}/METADATA +1 -1
- {airbyte_cdk-6.33.0.dev0.dist-info → airbyte_cdk-6.33.1.dist-info}/RECORD +22 -22
- {airbyte_cdk-6.33.0.dev0.dist-info → airbyte_cdk-6.33.1.dist-info}/LICENSE.txt +0 -0
- {airbyte_cdk-6.33.0.dev0.dist-info → airbyte_cdk-6.33.1.dist-info}/LICENSE_SHORT +0 -0
- {airbyte_cdk-6.33.0.dev0.dist-info → airbyte_cdk-6.33.1.dist-info}/WHEEL +0 -0
- {airbyte_cdk-6.33.0.dev0.dist-info → airbyte_cdk-6.33.1.dist-info}/entry_points.txt +0 -0
@@ -112,9 +112,6 @@ from airbyte_cdk.sources.declarative.models.declarative_component_schema import
|
|
112
112
|
from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
|
113
113
|
AddFields as AddFieldsModel,
|
114
114
|
)
|
115
|
-
from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
|
116
|
-
APIBudget as APIBudgetModel,
|
117
|
-
)
|
118
115
|
from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
|
119
116
|
ApiKeyAuthenticator as ApiKeyAuthenticatorModel,
|
120
117
|
)
|
@@ -229,9 +226,6 @@ from airbyte_cdk.sources.declarative.models.declarative_component_schema import
|
|
229
226
|
from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
|
230
227
|
ExponentialBackoffStrategy as ExponentialBackoffStrategyModel,
|
231
228
|
)
|
232
|
-
from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
|
233
|
-
FixedWindowCallRatePolicy as FixedWindowCallRatePolicyModel,
|
234
|
-
)
|
235
229
|
from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
|
236
230
|
FlattenFields as FlattenFieldsModel,
|
237
231
|
)
|
@@ -241,18 +235,12 @@ from airbyte_cdk.sources.declarative.models.declarative_component_schema import
|
|
241
235
|
from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
|
242
236
|
GzipParser as GzipParserModel,
|
243
237
|
)
|
244
|
-
from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
|
245
|
-
HTTPAPIBudget as HTTPAPIBudgetModel,
|
246
|
-
)
|
247
238
|
from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
|
248
239
|
HttpComponentsResolver as HttpComponentsResolverModel,
|
249
240
|
)
|
250
241
|
from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
|
251
242
|
HttpRequester as HttpRequesterModel,
|
252
243
|
)
|
253
|
-
from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
|
254
|
-
HttpRequestMatcher as HttpRequestMatcherModel,
|
255
|
-
)
|
256
244
|
from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
|
257
245
|
HttpResponseFilter as HttpResponseFilterModel,
|
258
246
|
)
|
@@ -307,9 +295,6 @@ from airbyte_cdk.sources.declarative.models.declarative_component_schema import
|
|
307
295
|
from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
|
308
296
|
MinMaxDatetime as MinMaxDatetimeModel,
|
309
297
|
)
|
310
|
-
from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
|
311
|
-
MovingWindowCallRatePolicy as MovingWindowCallRatePolicyModel,
|
312
|
-
)
|
313
298
|
from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
|
314
299
|
NoAuth as NoAuthModel,
|
315
300
|
)
|
@@ -328,9 +313,6 @@ from airbyte_cdk.sources.declarative.models.declarative_component_schema import
|
|
328
313
|
from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
|
329
314
|
ParentStreamConfig as ParentStreamConfigModel,
|
330
315
|
)
|
331
|
-
from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
|
332
|
-
Rate as RateModel,
|
333
|
-
)
|
334
316
|
from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
|
335
317
|
RecordFilter as RecordFilterModel,
|
336
318
|
)
|
@@ -374,9 +356,6 @@ from airbyte_cdk.sources.declarative.models.declarative_component_schema import
|
|
374
356
|
from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
|
375
357
|
TypesMap as TypesMapModel,
|
376
358
|
)
|
377
|
-
from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
|
378
|
-
UnlimitedCallRatePolicy as UnlimitedCallRatePolicyModel,
|
379
|
-
)
|
380
359
|
from airbyte_cdk.sources.declarative.models.declarative_component_schema import ValueType
|
381
360
|
from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
|
382
361
|
WaitTimeFromHeader as WaitTimeFromHeaderModel,
|
@@ -490,15 +469,6 @@ from airbyte_cdk.sources.message import (
|
|
490
469
|
MessageRepository,
|
491
470
|
NoopMessageRepository,
|
492
471
|
)
|
493
|
-
from airbyte_cdk.sources.streams.call_rate import (
|
494
|
-
APIBudget,
|
495
|
-
FixedWindowCallRatePolicy,
|
496
|
-
HttpAPIBudget,
|
497
|
-
HttpRequestMatcher,
|
498
|
-
MovingWindowCallRatePolicy,
|
499
|
-
Rate,
|
500
|
-
UnlimitedCallRatePolicy,
|
501
|
-
)
|
502
472
|
from airbyte_cdk.sources.streams.concurrent.clamping import (
|
503
473
|
ClampingEndProvider,
|
504
474
|
ClampingStrategy,
|
@@ -550,7 +520,6 @@ class ModelToComponentFactory:
|
|
550
520
|
self._evaluate_log_level(emit_connector_builder_messages)
|
551
521
|
)
|
552
522
|
self._connector_state_manager = connector_state_manager or ConnectorStateManager()
|
553
|
-
self._api_budget: Optional[Union[APIBudget, HttpAPIBudget]] = None
|
554
523
|
|
555
524
|
def _init_mappings(self) -> None:
|
556
525
|
self.PYDANTIC_MODEL_TO_CONSTRUCTOR: Mapping[Type[BaseModel], Callable[..., Any]] = {
|
@@ -638,13 +607,6 @@ class ModelToComponentFactory:
|
|
638
607
|
StreamConfigModel: self.create_stream_config,
|
639
608
|
ComponentMappingDefinitionModel: self.create_components_mapping_definition,
|
640
609
|
ZipfileDecoderModel: self.create_zipfile_decoder,
|
641
|
-
APIBudgetModel: self.create_api_budget,
|
642
|
-
HTTPAPIBudgetModel: self.create_http_api_budget,
|
643
|
-
FixedWindowCallRatePolicyModel: self.create_fixed_window_call_rate_policy,
|
644
|
-
MovingWindowCallRatePolicyModel: self.create_moving_window_call_rate_policy,
|
645
|
-
UnlimitedCallRatePolicyModel: self.create_unlimited_call_rate_policy,
|
646
|
-
RateModel: self.create_rate,
|
647
|
-
HttpRequestMatcherModel: self.create_http_request_matcher,
|
648
610
|
}
|
649
611
|
|
650
612
|
# Needed for the case where we need to perform a second parse on the fields of a custom component
|
@@ -771,8 +733,8 @@ class ModelToComponentFactory:
|
|
771
733
|
}
|
772
734
|
return names_to_types[value_type]
|
773
735
|
|
774
|
-
@staticmethod
|
775
736
|
def create_api_key_authenticator(
|
737
|
+
self,
|
776
738
|
model: ApiKeyAuthenticatorModel,
|
777
739
|
config: Config,
|
778
740
|
token_provider: Optional[TokenProvider] = None,
|
@@ -794,10 +756,8 @@ class ModelToComponentFactory:
|
|
794
756
|
)
|
795
757
|
|
796
758
|
request_option = (
|
797
|
-
|
798
|
-
inject_into=
|
799
|
-
field_name=model.inject_into.field_name,
|
800
|
-
parameters=model.parameters or {},
|
759
|
+
self._create_component_from_model(
|
760
|
+
model.inject_into, config, parameters=model.parameters or {}
|
801
761
|
)
|
802
762
|
if model.inject_into
|
803
763
|
else RequestOption(
|
@@ -806,6 +766,7 @@ class ModelToComponentFactory:
|
|
806
766
|
parameters=model.parameters or {},
|
807
767
|
)
|
808
768
|
)
|
769
|
+
|
809
770
|
return ApiKeyAuthenticator(
|
810
771
|
token_provider=(
|
811
772
|
token_provider
|
@@ -887,7 +848,7 @@ class ModelToComponentFactory:
|
|
887
848
|
token_provider=token_provider,
|
888
849
|
)
|
889
850
|
else:
|
890
|
-
return
|
851
|
+
return self.create_api_key_authenticator(
|
891
852
|
ApiKeyAuthenticatorModel(
|
892
853
|
type="ApiKeyAuthenticator",
|
893
854
|
api_token="",
|
@@ -1527,19 +1488,15 @@ class ModelToComponentFactory:
|
|
1527
1488
|
)
|
1528
1489
|
|
1529
1490
|
end_time_option = (
|
1530
|
-
|
1531
|
-
|
1532
|
-
field_name=model.end_time_option.field_name,
|
1533
|
-
parameters=model.parameters or {},
|
1491
|
+
self._create_component_from_model(
|
1492
|
+
model.end_time_option, config, parameters=model.parameters or {}
|
1534
1493
|
)
|
1535
1494
|
if model.end_time_option
|
1536
1495
|
else None
|
1537
1496
|
)
|
1538
1497
|
start_time_option = (
|
1539
|
-
|
1540
|
-
|
1541
|
-
field_name=model.start_time_option.field_name,
|
1542
|
-
parameters=model.parameters or {},
|
1498
|
+
self._create_component_from_model(
|
1499
|
+
model.start_time_option, config, parameters=model.parameters or {}
|
1543
1500
|
)
|
1544
1501
|
if model.start_time_option
|
1545
1502
|
else None
|
@@ -1610,19 +1567,15 @@ class ModelToComponentFactory:
|
|
1610
1567
|
cursor_model = model.incremental_sync
|
1611
1568
|
|
1612
1569
|
end_time_option = (
|
1613
|
-
|
1614
|
-
|
1615
|
-
field_name=cursor_model.end_time_option.field_name,
|
1616
|
-
parameters=cursor_model.parameters or {},
|
1570
|
+
self._create_component_from_model(
|
1571
|
+
cursor_model.end_time_option, config, parameters=cursor_model.parameters or {}
|
1617
1572
|
)
|
1618
1573
|
if cursor_model.end_time_option
|
1619
1574
|
else None
|
1620
1575
|
)
|
1621
1576
|
start_time_option = (
|
1622
|
-
|
1623
|
-
|
1624
|
-
field_name=cursor_model.start_time_option.field_name,
|
1625
|
-
parameters=cursor_model.parameters or {},
|
1577
|
+
self._create_component_from_model(
|
1578
|
+
cursor_model.start_time_option, config, parameters=cursor_model.parameters or {}
|
1626
1579
|
)
|
1627
1580
|
if cursor_model.start_time_option
|
1628
1581
|
else None
|
@@ -1949,8 +1902,6 @@ class ModelToComponentFactory:
|
|
1949
1902
|
)
|
1950
1903
|
)
|
1951
1904
|
|
1952
|
-
api_budget = self._api_budget
|
1953
|
-
|
1954
1905
|
request_options_provider = InterpolatedRequestOptionsProvider(
|
1955
1906
|
request_body_data=model.request_body_data,
|
1956
1907
|
request_body_json=model.request_body_json,
|
@@ -1971,7 +1922,6 @@ class ModelToComponentFactory:
|
|
1971
1922
|
path=model.path,
|
1972
1923
|
authenticator=authenticator,
|
1973
1924
|
error_handler=error_handler,
|
1974
|
-
api_budget=api_budget,
|
1975
1925
|
http_method=HttpMethod[model.http_method.value],
|
1976
1926
|
request_options_provider=request_options_provider,
|
1977
1927
|
config=config,
|
@@ -2191,16 +2141,11 @@ class ModelToComponentFactory:
|
|
2191
2141
|
additional_jwt_payload=model.additional_jwt_payload,
|
2192
2142
|
)
|
2193
2143
|
|
2194
|
-
@staticmethod
|
2195
2144
|
def create_list_partition_router(
|
2196
|
-
model: ListPartitionRouterModel, config: Config, **kwargs: Any
|
2145
|
+
self, model: ListPartitionRouterModel, config: Config, **kwargs: Any
|
2197
2146
|
) -> ListPartitionRouter:
|
2198
2147
|
request_option = (
|
2199
|
-
|
2200
|
-
inject_into=RequestOptionType(model.request_option.inject_into.value),
|
2201
|
-
field_name=model.request_option.field_name,
|
2202
|
-
parameters=model.parameters or {},
|
2203
|
-
)
|
2148
|
+
self._create_component_from_model(model.request_option, config)
|
2204
2149
|
if model.request_option
|
2205
2150
|
else None
|
2206
2151
|
)
|
@@ -2396,7 +2341,25 @@ class ModelToComponentFactory:
|
|
2396
2341
|
model: RequestOptionModel, config: Config, **kwargs: Any
|
2397
2342
|
) -> RequestOption:
|
2398
2343
|
inject_into = RequestOptionType(model.inject_into.value)
|
2399
|
-
|
2344
|
+
field_path: Optional[List[Union[InterpolatedString, str]]] = (
|
2345
|
+
[
|
2346
|
+
InterpolatedString.create(segment, parameters=kwargs.get("parameters", {}))
|
2347
|
+
for segment in model.field_path
|
2348
|
+
]
|
2349
|
+
if model.field_path
|
2350
|
+
else None
|
2351
|
+
)
|
2352
|
+
field_name = (
|
2353
|
+
InterpolatedString.create(model.field_name, parameters=kwargs.get("parameters", {}))
|
2354
|
+
if model.field_name
|
2355
|
+
else None
|
2356
|
+
)
|
2357
|
+
return RequestOption(
|
2358
|
+
field_name=field_name,
|
2359
|
+
field_path=field_path,
|
2360
|
+
inject_into=inject_into,
|
2361
|
+
parameters=kwargs.get("parameters", {}),
|
2362
|
+
)
|
2400
2363
|
|
2401
2364
|
def create_record_selector(
|
2402
2365
|
self,
|
@@ -2960,103 +2923,3 @@ class ModelToComponentFactory:
|
|
2960
2923
|
return isinstance(parser.inner_parser, JsonParser)
|
2961
2924
|
else:
|
2962
2925
|
return False
|
2963
|
-
|
2964
|
-
def create_api_budget(self, model: APIBudgetModel, config: Config, **kwargs: Any) -> APIBudget:
|
2965
|
-
policies = [
|
2966
|
-
self._create_component_from_model(model=policy, config=config)
|
2967
|
-
for policy in model.policies
|
2968
|
-
]
|
2969
|
-
|
2970
|
-
return APIBudget(
|
2971
|
-
policies=policies,
|
2972
|
-
maximum_attempts_to_acquire=model.maximum_attempts_to_acquire or 100000,
|
2973
|
-
)
|
2974
|
-
|
2975
|
-
def create_http_api_budget(
|
2976
|
-
self, model: HTTPAPIBudgetModel, config: Config, **kwargs: Any
|
2977
|
-
) -> HttpAPIBudget:
|
2978
|
-
policies = [
|
2979
|
-
self._create_component_from_model(model=policy, config=config)
|
2980
|
-
for policy in model.policies
|
2981
|
-
]
|
2982
|
-
|
2983
|
-
return HttpAPIBudget(
|
2984
|
-
policies=policies,
|
2985
|
-
maximum_attempts_to_acquire=model.maximum_attempts_to_acquire or 100000,
|
2986
|
-
ratelimit_reset_header=model.ratelimit_reset_header or "ratelimit-reset",
|
2987
|
-
ratelimit_remaining_header=model.ratelimit_remaining_header or "ratelimit-remaining",
|
2988
|
-
status_codes_for_ratelimit_hit=model.status_codes_for_ratelimit_hit or (429,),
|
2989
|
-
)
|
2990
|
-
|
2991
|
-
def create_fixed_window_call_rate_policy(
|
2992
|
-
self, model: FixedWindowCallRatePolicyModel, config: Config, **kwargs: Any
|
2993
|
-
) -> FixedWindowCallRatePolicy:
|
2994
|
-
matchers = [
|
2995
|
-
self._create_component_from_model(model=matcher, config=config)
|
2996
|
-
for matcher in model.matchers
|
2997
|
-
]
|
2998
|
-
return FixedWindowCallRatePolicy(
|
2999
|
-
next_reset_ts=model.next_reset_ts,
|
3000
|
-
period=parse_duration(model.period),
|
3001
|
-
call_limit=model.call_limit,
|
3002
|
-
matchers=matchers,
|
3003
|
-
)
|
3004
|
-
|
3005
|
-
def create_moving_window_call_rate_policy(
|
3006
|
-
self, model: MovingWindowCallRatePolicyModel, config: Config, **kwargs: Any
|
3007
|
-
) -> MovingWindowCallRatePolicy:
|
3008
|
-
rates = [
|
3009
|
-
self._create_component_from_model(model=rate, config=config) for rate in model.rates
|
3010
|
-
]
|
3011
|
-
matchers = [
|
3012
|
-
self._create_component_from_model(model=matcher, config=config)
|
3013
|
-
for matcher in model.matchers
|
3014
|
-
]
|
3015
|
-
return MovingWindowCallRatePolicy(
|
3016
|
-
rates=rates,
|
3017
|
-
matchers=matchers,
|
3018
|
-
)
|
3019
|
-
|
3020
|
-
def create_unlimited_call_rate_policy(
|
3021
|
-
self, model: UnlimitedCallRatePolicyModel, config: Config, **kwargs: Any
|
3022
|
-
) -> UnlimitedCallRatePolicy:
|
3023
|
-
matchers = [
|
3024
|
-
self._create_component_from_model(model=matcher, config=config)
|
3025
|
-
for matcher in model.matchers
|
3026
|
-
]
|
3027
|
-
|
3028
|
-
return UnlimitedCallRatePolicy(
|
3029
|
-
matchers=matchers,
|
3030
|
-
)
|
3031
|
-
|
3032
|
-
def create_rate(self, model: RateModel, config: Config, **kwargs: Any) -> Rate:
|
3033
|
-
return Rate(
|
3034
|
-
limit=model.limit,
|
3035
|
-
interval=model.interval,
|
3036
|
-
)
|
3037
|
-
|
3038
|
-
def create_http_request_matcher(
|
3039
|
-
self, model: HttpRequestMatcherModel, config: Config, **kwargs: Any
|
3040
|
-
) -> HttpRequestMatcher:
|
3041
|
-
return HttpRequestMatcher(
|
3042
|
-
method=model.method,
|
3043
|
-
url_base=model.url_base,
|
3044
|
-
url_path_pattern=model.url_path_pattern,
|
3045
|
-
params=model.params,
|
3046
|
-
headers=model.headers,
|
3047
|
-
)
|
3048
|
-
|
3049
|
-
def set_api_budget(self, component_definition: ComponentDefinition, config: Config) -> None:
|
3050
|
-
model_str = component_definition.get("type")
|
3051
|
-
if model_str == "APIBudget":
|
3052
|
-
# Annotate model_type as a type that is a subclass of BaseModel
|
3053
|
-
model_type: Union[Type[APIBudgetModel], Type[HTTPAPIBudgetModel]] = APIBudgetModel
|
3054
|
-
elif model_str == "HTTPAPIBudget":
|
3055
|
-
model_type = HTTPAPIBudgetModel
|
3056
|
-
else:
|
3057
|
-
raise ValueError(f"Unknown API Budget type: {model_str}")
|
3058
|
-
|
3059
|
-
# create_component expects a type[BaseModel] and returns an instance of that model.
|
3060
|
-
self._api_budget = self.create_component(
|
3061
|
-
model_type=model_type, component_definition=component_definition, config=config
|
3062
|
-
)
|
@@ -3,7 +3,7 @@
|
|
3
3
|
#
|
4
4
|
|
5
5
|
from dataclasses import InitVar, dataclass
|
6
|
-
from typing import Any, Iterable, List, Mapping, Optional, Union
|
6
|
+
from typing import Any, Iterable, List, Mapping, MutableMapping, Optional, Union
|
7
7
|
|
8
8
|
from airbyte_cdk.sources.declarative.interpolation.interpolated_string import InterpolatedString
|
9
9
|
from airbyte_cdk.sources.declarative.partition_routers.partition_router import PartitionRouter
|
@@ -100,7 +100,9 @@ class ListPartitionRouter(PartitionRouter):
|
|
100
100
|
):
|
101
101
|
slice_value = stream_slice.get(self._cursor_field.eval(self.config))
|
102
102
|
if slice_value:
|
103
|
-
|
103
|
+
options: MutableMapping[str, Any] = {}
|
104
|
+
self.request_option.inject_into_request(options, slice_value, self.config)
|
105
|
+
return options
|
104
106
|
else:
|
105
107
|
return {}
|
106
108
|
else:
|
@@ -4,7 +4,7 @@
|
|
4
4
|
import copy
|
5
5
|
import logging
|
6
6
|
from dataclasses import InitVar, dataclass
|
7
|
-
from typing import TYPE_CHECKING, Any, Iterable, List, Mapping, Optional, Union
|
7
|
+
from typing import TYPE_CHECKING, Any, Iterable, List, Mapping, MutableMapping, Optional, Union
|
8
8
|
|
9
9
|
import dpath
|
10
10
|
|
@@ -118,7 +118,7 @@ class SubstreamPartitionRouter(PartitionRouter):
|
|
118
118
|
def _get_request_option(
|
119
119
|
self, option_type: RequestOptionType, stream_slice: Optional[StreamSlice]
|
120
120
|
) -> Mapping[str, Any]:
|
121
|
-
params = {}
|
121
|
+
params: MutableMapping[str, Any] = {}
|
122
122
|
if stream_slice:
|
123
123
|
for parent_config in self.parent_stream_configs:
|
124
124
|
if (
|
@@ -128,13 +128,7 @@ class SubstreamPartitionRouter(PartitionRouter):
|
|
128
128
|
key = parent_config.partition_field.eval(self.config) # type: ignore # partition_field is always casted to an interpolated string
|
129
129
|
value = stream_slice.get(key)
|
130
130
|
if value:
|
131
|
-
params.
|
132
|
-
{
|
133
|
-
parent_config.request_option.field_name.eval( # type: ignore [union-attr]
|
134
|
-
config=self.config
|
135
|
-
): value
|
136
|
-
}
|
137
|
-
)
|
131
|
+
parent_config.request_option.inject_into_request(params, value, self.config)
|
138
132
|
return params
|
139
133
|
|
140
134
|
def stream_slices(self) -> Iterable[StreamSlice]:
|
@@ -305,23 +299,33 @@ class SubstreamPartitionRouter(PartitionRouter):
|
|
305
299
|
|
306
300
|
def _migrate_child_state_to_parent_state(self, stream_state: StreamState) -> StreamState:
|
307
301
|
"""
|
308
|
-
Migrate the child stream state
|
302
|
+
Migrate the child or global stream state into the parent stream's state format.
|
303
|
+
|
304
|
+
This method converts the child stream state—or, if present, the global state—into a format that is
|
305
|
+
compatible with parent streams that use incremental synchronization. The migration occurs only for
|
306
|
+
parent streams with incremental dependencies. It filters out per-partition states and retains only the
|
307
|
+
global state in the form {cursor_field: cursor_value}.
|
309
308
|
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
309
|
+
The method supports multiple input formats:
|
310
|
+
- A simple global state, e.g.:
|
311
|
+
{"updated_at": "2023-05-27T00:00:00Z"}
|
312
|
+
- A state object that contains a "state" key (which is assumed to hold the global state), e.g.:
|
313
|
+
{"state": {"updated_at": "2023-05-27T00:00:00Z"}, ...}
|
314
|
+
In this case, the migration uses the first value from the "state" dictionary.
|
315
|
+
- Any per-partition state formats or other non-simple structures are ignored during migration.
|
314
316
|
|
315
317
|
Args:
|
316
318
|
stream_state (StreamState): The state to migrate. Expected formats include:
|
317
319
|
- {"updated_at": "2023-05-27T00:00:00Z"}
|
318
|
-
- {"
|
320
|
+
- {"state": {"updated_at": "2023-05-27T00:00:00Z"}, ...}
|
321
|
+
(In this format, only the first global state value is used, and per-partition states are ignored.)
|
319
322
|
|
320
323
|
Returns:
|
321
324
|
StreamState: A migrated state for parent streams in the format:
|
322
325
|
{
|
323
326
|
"parent_stream_name": {"parent_stream_cursor": "2023-05-27T00:00:00Z"}
|
324
327
|
}
|
328
|
+
where each parent stream with an incremental dependency is assigned its corresponding cursor value.
|
325
329
|
|
326
330
|
Example:
|
327
331
|
Input: {"updated_at": "2023-05-27T00:00:00Z"}
|
@@ -332,11 +336,15 @@ class SubstreamPartitionRouter(PartitionRouter):
|
|
332
336
|
substream_state_values = list(stream_state.values())
|
333
337
|
substream_state = substream_state_values[0] if substream_state_values else {}
|
334
338
|
|
335
|
-
# Ignore per-partition states or invalid formats
|
339
|
+
# Ignore per-partition states or invalid formats.
|
336
340
|
if isinstance(substream_state, (list, dict)) or len(substream_state_values) != 1:
|
337
|
-
|
341
|
+
# If a global state is present under the key "state", use its first value.
|
342
|
+
if "state" in stream_state and isinstance(stream_state["state"], dict):
|
343
|
+
substream_state = list(stream_state["state"].values())[0]
|
344
|
+
else:
|
345
|
+
return {}
|
338
346
|
|
339
|
-
#
|
347
|
+
# Build the parent state for all parent streams with incremental dependencies.
|
340
348
|
parent_state = {}
|
341
349
|
if substream_state:
|
342
350
|
for parent_config in self.parent_stream_configs:
|
@@ -22,7 +22,6 @@ from airbyte_cdk.sources.declarative.requesters.request_options.interpolated_req
|
|
22
22
|
)
|
23
23
|
from airbyte_cdk.sources.declarative.requesters.requester import HttpMethod, Requester
|
24
24
|
from airbyte_cdk.sources.message import MessageRepository, NoopMessageRepository
|
25
|
-
from airbyte_cdk.sources.streams.call_rate import APIBudget
|
26
25
|
from airbyte_cdk.sources.streams.http import HttpClient
|
27
26
|
from airbyte_cdk.sources.streams.http.error_handlers import ErrorHandler
|
28
27
|
from airbyte_cdk.sources.types import Config, StreamSlice, StreamState
|
@@ -56,7 +55,6 @@ class HttpRequester(Requester):
|
|
56
55
|
http_method: Union[str, HttpMethod] = HttpMethod.GET
|
57
56
|
request_options_provider: Optional[InterpolatedRequestOptionsProvider] = None
|
58
57
|
error_handler: Optional[ErrorHandler] = None
|
59
|
-
api_budget: Optional[APIBudget] = None
|
60
58
|
disable_retries: bool = False
|
61
59
|
message_repository: MessageRepository = NoopMessageRepository()
|
62
60
|
use_cache: bool = False
|
@@ -93,7 +91,6 @@ class HttpRequester(Requester):
|
|
93
91
|
name=self.name,
|
94
92
|
logger=self.logger,
|
95
93
|
error_handler=self.error_handler,
|
96
|
-
api_budget=self.api_budget,
|
97
94
|
authenticator=self._authenticator,
|
98
95
|
use_cache=self.use_cache,
|
99
96
|
backoff_strategy=backoff_strategies,
|
@@ -202,6 +199,9 @@ class HttpRequester(Requester):
|
|
202
199
|
Raise a ValueError if there's a key collision
|
203
200
|
Returned merged mapping otherwise
|
204
201
|
"""
|
202
|
+
|
203
|
+
is_body_json = requester_method.__name__ == "get_request_body_json"
|
204
|
+
|
205
205
|
return combine_mappings(
|
206
206
|
[
|
207
207
|
requester_method(
|
@@ -211,7 +211,8 @@ class HttpRequester(Requester):
|
|
211
211
|
),
|
212
212
|
auth_options_method(),
|
213
213
|
extra_options,
|
214
|
-
]
|
214
|
+
],
|
215
|
+
allow_same_value_merge=is_body_json,
|
215
216
|
)
|
216
217
|
|
217
218
|
def _request_headers(
|
@@ -187,7 +187,7 @@ class DefaultPaginator(Paginator):
|
|
187
187
|
def _get_request_options(
|
188
188
|
self, option_type: RequestOptionType, next_page_token: Optional[Mapping[str, Any]]
|
189
189
|
) -> MutableMapping[str, Any]:
|
190
|
-
options = {}
|
190
|
+
options: MutableMapping[str, Any] = {}
|
191
191
|
|
192
192
|
token = next_page_token.get("next_page_token") if next_page_token else None
|
193
193
|
if (
|
@@ -196,15 +196,16 @@ class DefaultPaginator(Paginator):
|
|
196
196
|
and isinstance(self.page_token_option, RequestOption)
|
197
197
|
and self.page_token_option.inject_into == option_type
|
198
198
|
):
|
199
|
-
|
199
|
+
self.page_token_option.inject_into_request(options, token, self.config)
|
200
|
+
|
200
201
|
if (
|
201
202
|
self.page_size_option
|
202
203
|
and self.pagination_strategy.get_page_size()
|
203
204
|
and self.page_size_option.inject_into == option_type
|
204
205
|
):
|
205
|
-
|
206
|
-
|
207
|
-
|
206
|
+
page_size = self.pagination_strategy.get_page_size()
|
207
|
+
self.page_size_option.inject_into_request(options, page_size, self.config)
|
208
|
+
|
208
209
|
return options
|
209
210
|
|
210
211
|
|
@@ -4,9 +4,10 @@
|
|
4
4
|
|
5
5
|
from dataclasses import InitVar, dataclass
|
6
6
|
from enum import Enum
|
7
|
-
from typing import Any, Mapping, Union
|
7
|
+
from typing import Any, List, Literal, Mapping, MutableMapping, Optional, Union
|
8
8
|
|
9
9
|
from airbyte_cdk.sources.declarative.interpolation.interpolated_string import InterpolatedString
|
10
|
+
from airbyte_cdk.sources.types import Config
|
10
11
|
|
11
12
|
|
12
13
|
class RequestOptionType(Enum):
|
@@ -26,13 +27,91 @@ class RequestOption:
|
|
26
27
|
Describes an option to set on a request
|
27
28
|
|
28
29
|
Attributes:
|
29
|
-
field_name (str): Describes the name of the parameter to inject
|
30
|
+
field_name (str): Describes the name of the parameter to inject. Mutually exclusive with field_path.
|
31
|
+
field_path (list(str)): Describes the path to a nested field as a list of field names.
|
32
|
+
Only valid for body_json injection type, and mutually exclusive with field_name.
|
30
33
|
inject_into (RequestOptionType): Describes where in the HTTP request to inject the parameter
|
31
34
|
"""
|
32
35
|
|
33
|
-
field_name: Union[InterpolatedString, str]
|
34
36
|
inject_into: RequestOptionType
|
35
37
|
parameters: InitVar[Mapping[str, Any]]
|
38
|
+
field_name: Optional[Union[InterpolatedString, str]] = None
|
39
|
+
field_path: Optional[List[Union[InterpolatedString, str]]] = None
|
36
40
|
|
37
41
|
def __post_init__(self, parameters: Mapping[str, Any]) -> None:
|
38
|
-
|
42
|
+
# Validate inputs. We should expect either field_name or field_path, but not both
|
43
|
+
if self.field_name is None and self.field_path is None:
|
44
|
+
raise ValueError("RequestOption requires either a field_name or field_path")
|
45
|
+
|
46
|
+
if self.field_name is not None and self.field_path is not None:
|
47
|
+
raise ValueError(
|
48
|
+
"Only one of field_name or field_path can be provided to RequestOption"
|
49
|
+
)
|
50
|
+
|
51
|
+
# Nested field injection is only supported for body JSON injection
|
52
|
+
if self.field_path is not None and self.inject_into != RequestOptionType.body_json:
|
53
|
+
raise ValueError(
|
54
|
+
"Nested field injection is only supported for body JSON injection. Please use a top-level field_name for other injection types."
|
55
|
+
)
|
56
|
+
|
57
|
+
# Convert field_name and field_path into InterpolatedString objects if they are strings
|
58
|
+
if self.field_name is not None:
|
59
|
+
self.field_name = InterpolatedString.create(self.field_name, parameters=parameters)
|
60
|
+
elif self.field_path is not None:
|
61
|
+
self.field_path = [
|
62
|
+
InterpolatedString.create(segment, parameters=parameters)
|
63
|
+
for segment in self.field_path
|
64
|
+
]
|
65
|
+
|
66
|
+
@property
|
67
|
+
def _is_field_path(self) -> bool:
|
68
|
+
"""Returns whether this option is a field path (ie, a nested field)"""
|
69
|
+
return self.field_path is not None
|
70
|
+
|
71
|
+
def inject_into_request(
|
72
|
+
self,
|
73
|
+
target: MutableMapping[str, Any],
|
74
|
+
value: Any,
|
75
|
+
config: Config,
|
76
|
+
) -> None:
|
77
|
+
"""
|
78
|
+
Inject a request option value into a target request structure using either field_name or field_path.
|
79
|
+
For non-body-json injection, only top-level field names are supported.
|
80
|
+
For body-json injection, both field names and nested field paths are supported.
|
81
|
+
|
82
|
+
Args:
|
83
|
+
target: The request structure to inject the value into
|
84
|
+
value: The value to inject
|
85
|
+
config: The config object to use for interpolation
|
86
|
+
"""
|
87
|
+
if self._is_field_path:
|
88
|
+
if self.inject_into != RequestOptionType.body_json:
|
89
|
+
raise ValueError(
|
90
|
+
"Nested field injection is only supported for body JSON injection. Please use a top-level field_name for other injection types."
|
91
|
+
)
|
92
|
+
|
93
|
+
assert self.field_path is not None # for type checker
|
94
|
+
current = target
|
95
|
+
# Convert path segments into strings, evaluating any interpolated segments
|
96
|
+
# Example: ["data", "{{ config[user_type] }}", "id"] -> ["data", "admin", "id"]
|
97
|
+
*path_parts, final_key = [
|
98
|
+
str(
|
99
|
+
segment.eval(config=config)
|
100
|
+
if isinstance(segment, InterpolatedString)
|
101
|
+
else segment
|
102
|
+
)
|
103
|
+
for segment in self.field_path
|
104
|
+
]
|
105
|
+
|
106
|
+
# Build a nested dictionary structure and set the final value at the deepest level
|
107
|
+
for part in path_parts:
|
108
|
+
current = current.setdefault(part, {})
|
109
|
+
current[final_key] = value
|
110
|
+
else:
|
111
|
+
# For non-nested fields, evaluate the field name if it's an interpolated string
|
112
|
+
key = (
|
113
|
+
self.field_name.eval(config=config)
|
114
|
+
if isinstance(self.field_name, InterpolatedString)
|
115
|
+
else self.field_name
|
116
|
+
)
|
117
|
+
target[str(key)] = value
|
@@ -80,12 +80,13 @@ class DatetimeBasedRequestOptionsProvider(RequestOptionsProvider):
|
|
80
80
|
options: MutableMapping[str, Any] = {}
|
81
81
|
if not stream_slice:
|
82
82
|
return options
|
83
|
+
|
83
84
|
if self.start_time_option and self.start_time_option.inject_into == option_type:
|
84
|
-
|
85
|
-
|
86
|
-
|
85
|
+
start_time_value = stream_slice.get(self._partition_field_start.eval(self.config))
|
86
|
+
self.start_time_option.inject_into_request(options, start_time_value, self.config)
|
87
|
+
|
87
88
|
if self.end_time_option and self.end_time_option.inject_into == option_type:
|
88
|
-
|
89
|
-
|
90
|
-
|
89
|
+
end_time_value = stream_slice.get(self._partition_field_end.eval(self.config))
|
90
|
+
self.end_time_option.inject_into_request(options, end_time_value, self.config)
|
91
|
+
|
91
92
|
return options
|