airbyte-cdk 6.31.2.dev0__py3-none-any.whl → 6.32.0__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/cli/source_declarative_manifest/_run.py +9 -3
- airbyte_cdk/connector_builder/connector_builder_handler.py +3 -2
- airbyte_cdk/sources/declarative/async_job/job_orchestrator.py +7 -7
- airbyte_cdk/sources/declarative/auth/jwt.py +17 -11
- airbyte_cdk/sources/declarative/auth/oauth.py +89 -23
- airbyte_cdk/sources/declarative/auth/token.py +8 -3
- airbyte_cdk/sources/declarative/auth/token_provider.py +4 -5
- airbyte_cdk/sources/declarative/checks/check_dynamic_stream.py +19 -9
- airbyte_cdk/sources/declarative/concurrent_declarative_source.py +134 -43
- airbyte_cdk/sources/declarative/declarative_component_schema.yaml +55 -16
- airbyte_cdk/sources/declarative/declarative_stream.py +3 -1
- airbyte_cdk/sources/declarative/extractors/record_filter.py +3 -5
- airbyte_cdk/sources/declarative/incremental/__init__.py +6 -0
- airbyte_cdk/sources/declarative/incremental/concurrent_partition_cursor.py +400 -0
- airbyte_cdk/sources/declarative/incremental/datetime_based_cursor.py +6 -7
- airbyte_cdk/sources/declarative/incremental/global_substream_cursor.py +3 -0
- airbyte_cdk/sources/declarative/incremental/per_partition_cursor.py +35 -3
- airbyte_cdk/sources/declarative/manifest_declarative_source.py +20 -7
- airbyte_cdk/sources/declarative/models/declarative_component_schema.py +45 -15
- airbyte_cdk/sources/declarative/parsers/custom_code_compiler.py +143 -0
- airbyte_cdk/sources/declarative/parsers/model_to_component_factory.py +343 -64
- airbyte_cdk/sources/declarative/partition_routers/async_job_partition_router.py +5 -5
- airbyte_cdk/sources/declarative/partition_routers/list_partition_router.py +2 -4
- airbyte_cdk/sources/declarative/partition_routers/substream_partition_router.py +55 -15
- airbyte_cdk/sources/declarative/requesters/error_handlers/composite_error_handler.py +22 -0
- airbyte_cdk/sources/declarative/requesters/error_handlers/http_response_filter.py +4 -4
- airbyte_cdk/sources/declarative/requesters/http_requester.py +1 -5
- airbyte_cdk/sources/declarative/requesters/paginators/default_paginator.py +5 -6
- airbyte_cdk/sources/declarative/requesters/request_option.py +4 -83
- airbyte_cdk/sources/declarative/requesters/request_options/datetime_based_request_options_provider.py +6 -7
- airbyte_cdk/sources/declarative/retrievers/async_retriever.py +6 -12
- airbyte_cdk/sources/declarative/retrievers/simple_retriever.py +2 -5
- airbyte_cdk/sources/declarative/schema/__init__.py +2 -0
- airbyte_cdk/sources/declarative/schema/dynamic_schema_loader.py +44 -5
- airbyte_cdk/sources/http_logger.py +1 -1
- airbyte_cdk/sources/streams/concurrent/clamping.py +99 -0
- airbyte_cdk/sources/streams/concurrent/cursor.py +51 -57
- airbyte_cdk/sources/streams/concurrent/cursor_types.py +32 -0
- airbyte_cdk/sources/streams/concurrent/state_converters/datetime_stream_state_converter.py +22 -13
- airbyte_cdk/sources/streams/core.py +6 -6
- airbyte_cdk/sources/streams/http/http.py +1 -2
- airbyte_cdk/sources/streams/http/requests_native_auth/abstract_oauth.py +231 -62
- airbyte_cdk/sources/streams/http/requests_native_auth/oauth.py +171 -88
- airbyte_cdk/sources/types.py +4 -2
- airbyte_cdk/sources/utils/transform.py +23 -2
- airbyte_cdk/test/utils/manifest_only_fixtures.py +1 -2
- airbyte_cdk/utils/datetime_helpers.py +499 -0
- airbyte_cdk/utils/mapping_helpers.py +27 -86
- airbyte_cdk/utils/slice_hasher.py +8 -1
- airbyte_cdk-6.32.0.dist-info/LICENSE_SHORT +1 -0
- {airbyte_cdk-6.31.2.dev0.dist-info → airbyte_cdk-6.32.0.dist-info}/METADATA +6 -6
- {airbyte_cdk-6.31.2.dev0.dist-info → airbyte_cdk-6.32.0.dist-info}/RECORD +55 -49
- {airbyte_cdk-6.31.2.dev0.dist-info → airbyte_cdk-6.32.0.dist-info}/WHEEL +1 -1
- {airbyte_cdk-6.31.2.dev0.dist-info → airbyte_cdk-6.32.0.dist-info}/LICENSE.txt +0 -0
- {airbyte_cdk-6.31.2.dev0.dist-info → airbyte_cdk-6.32.0.dist-info}/entry_points.txt +0 -0
@@ -21,7 +21,6 @@ import pkgutil
|
|
21
21
|
import sys
|
22
22
|
import traceback
|
23
23
|
from collections.abc import Mapping
|
24
|
-
from datetime import datetime
|
25
24
|
from pathlib import Path
|
26
25
|
from typing import Any, cast
|
27
26
|
|
@@ -44,6 +43,7 @@ from airbyte_cdk.sources.declarative.concurrent_declarative_source import (
|
|
44
43
|
)
|
45
44
|
from airbyte_cdk.sources.declarative.yaml_declarative_source import YamlDeclarativeSource
|
46
45
|
from airbyte_cdk.sources.source import TState
|
46
|
+
from airbyte_cdk.utils.datetime_helpers import ab_datetime_now
|
47
47
|
|
48
48
|
|
49
49
|
class SourceLocalYaml(YamlDeclarativeSource):
|
@@ -101,7 +101,7 @@ def _get_local_yaml_source(args: list[str]) -> SourceLocalYaml:
|
|
101
101
|
type=Type.TRACE,
|
102
102
|
trace=AirbyteTraceMessage(
|
103
103
|
type=TraceType.ERROR,
|
104
|
-
emitted_at=
|
104
|
+
emitted_at=ab_datetime_now().to_epoch_millis(),
|
105
105
|
error=AirbyteErrorTraceMessage(
|
106
106
|
message=f"Error starting the sync. This could be due to an invalid configuration or catalog. Please contact Support for assistance. Error: {error}",
|
107
107
|
stack_trace=traceback.format_exc(),
|
@@ -171,6 +171,12 @@ def create_declarative_source(
|
|
171
171
|
"Invalid config: `__injected_declarative_manifest` should be provided at the root "
|
172
172
|
f"of the config but config only has keys: {list(config.keys() if config else [])}"
|
173
173
|
)
|
174
|
+
if not isinstance(config["__injected_declarative_manifest"], dict):
|
175
|
+
raise ValueError(
|
176
|
+
"Invalid config: `__injected_declarative_manifest` should be a dictionary, "
|
177
|
+
f"but got type: {type(config['__injected_declarative_manifest'])}"
|
178
|
+
)
|
179
|
+
|
174
180
|
return ConcurrentDeclarativeSource(
|
175
181
|
config=config,
|
176
182
|
catalog=catalog,
|
@@ -185,7 +191,7 @@ def create_declarative_source(
|
|
185
191
|
type=Type.TRACE,
|
186
192
|
trace=AirbyteTraceMessage(
|
187
193
|
type=TraceType.ERROR,
|
188
|
-
emitted_at=
|
194
|
+
emitted_at=ab_datetime_now().to_epoch_millis(),
|
189
195
|
error=AirbyteErrorTraceMessage(
|
190
196
|
message=f"Error starting the sync. This could be due to an invalid configuration or catalog. Please contact Support for assistance. Error: {error}",
|
191
197
|
stack_trace=traceback.format_exc(),
|
@@ -3,7 +3,6 @@
|
|
3
3
|
#
|
4
4
|
|
5
5
|
import dataclasses
|
6
|
-
from datetime import datetime
|
7
6
|
from typing import Any, List, Mapping
|
8
7
|
|
9
8
|
from airbyte_cdk.connector_builder.message_grouper import MessageGrouper
|
@@ -21,6 +20,7 @@ from airbyte_cdk.sources.declarative.parsers.model_to_component_factory import (
|
|
21
20
|
ModelToComponentFactory,
|
22
21
|
)
|
23
22
|
from airbyte_cdk.utils.airbyte_secrets_utils import filter_secrets
|
23
|
+
from airbyte_cdk.utils.datetime_helpers import ab_datetime_now
|
24
24
|
from airbyte_cdk.utils.traced_exception import AirbyteTracedException
|
25
25
|
|
26
26
|
DEFAULT_MAXIMUM_NUMBER_OF_PAGES_PER_SLICE = 5
|
@@ -52,6 +52,7 @@ def get_limits(config: Mapping[str, Any]) -> TestReadLimits:
|
|
52
52
|
def create_source(config: Mapping[str, Any], limits: TestReadLimits) -> ManifestDeclarativeSource:
|
53
53
|
manifest = config["__injected_declarative_manifest"]
|
54
54
|
return ManifestDeclarativeSource(
|
55
|
+
config=config,
|
55
56
|
emit_connector_builder_messages=True,
|
56
57
|
source_config=manifest,
|
57
58
|
component_factory=ModelToComponentFactory(
|
@@ -113,4 +114,4 @@ def resolve_manifest(source: ManifestDeclarativeSource) -> AirbyteMessage:
|
|
113
114
|
|
114
115
|
|
115
116
|
def _emitted_at() -> int:
|
116
|
-
return
|
117
|
+
return ab_datetime_now().to_epoch_millis()
|
@@ -437,10 +437,10 @@ class AsyncJobOrchestrator:
|
|
437
437
|
yield from self._process_running_partitions_and_yield_completed_ones()
|
438
438
|
self._wait_on_status_update()
|
439
439
|
except Exception as exception:
|
440
|
+
LOGGER.warning(
|
441
|
+
f"Caught exception that stops the processing of the jobs: {exception}. Traceback: {traceback.format_exc()}"
|
442
|
+
)
|
440
443
|
if self._is_breaking_exception(exception):
|
441
|
-
LOGGER.warning(
|
442
|
-
f"Caught exception that stops the processing of the jobs: {exception}"
|
443
|
-
)
|
444
444
|
self._abort_all_running_jobs()
|
445
445
|
raise exception
|
446
446
|
|
@@ -482,16 +482,16 @@ class AsyncJobOrchestrator:
|
|
482
482
|
and exception.failure_type == FailureType.config_error
|
483
483
|
)
|
484
484
|
|
485
|
-
def fetch_records(self,
|
485
|
+
def fetch_records(self, async_jobs: Iterable[AsyncJob]) -> Iterable[Mapping[str, Any]]:
|
486
486
|
"""
|
487
|
-
Fetches records from the given
|
487
|
+
Fetches records from the given jobs.
|
488
488
|
|
489
489
|
Args:
|
490
|
-
|
490
|
+
async_jobs Iterable[AsyncJob]: The list of AsyncJobs.
|
491
491
|
|
492
492
|
Yields:
|
493
493
|
Iterable[Mapping[str, Any]]: The fetched records from the jobs.
|
494
494
|
"""
|
495
|
-
for job in
|
495
|
+
for job in async_jobs:
|
496
496
|
yield from self._job_repository.fetch_records(job)
|
497
497
|
self._job_repository.delete(job)
|
@@ -3,6 +3,7 @@
|
|
3
3
|
#
|
4
4
|
|
5
5
|
import base64
|
6
|
+
import json
|
6
7
|
from dataclasses import InitVar, dataclass
|
7
8
|
from datetime import datetime
|
8
9
|
from typing import Any, Mapping, Optional, Union
|
@@ -104,21 +105,21 @@ class JwtAuthenticator(DeclarativeAuthenticator):
|
|
104
105
|
)
|
105
106
|
|
106
107
|
def _get_jwt_headers(self) -> dict[str, Any]:
|
107
|
-
"""
|
108
|
+
"""
|
108
109
|
Builds and returns the headers used when signing the JWT.
|
109
110
|
"""
|
110
|
-
headers = self._additional_jwt_headers.eval(self.config)
|
111
|
+
headers = self._additional_jwt_headers.eval(self.config, json_loads=json.loads)
|
111
112
|
if any(prop in headers for prop in ["kid", "alg", "typ", "cty"]):
|
112
113
|
raise ValueError(
|
113
114
|
"'kid', 'alg', 'typ', 'cty' are reserved headers and should not be set as part of 'additional_jwt_headers'"
|
114
115
|
)
|
115
116
|
|
116
117
|
if self._kid:
|
117
|
-
headers["kid"] = self._kid.eval(self.config)
|
118
|
+
headers["kid"] = self._kid.eval(self.config, json_loads=json.loads)
|
118
119
|
if self._typ:
|
119
|
-
headers["typ"] = self._typ.eval(self.config)
|
120
|
+
headers["typ"] = self._typ.eval(self.config, json_loads=json.loads)
|
120
121
|
if self._cty:
|
121
|
-
headers["cty"] = self._cty.eval(self.config)
|
122
|
+
headers["cty"] = self._cty.eval(self.config, json_loads=json.loads)
|
122
123
|
headers["alg"] = self._algorithm
|
123
124
|
return headers
|
124
125
|
|
@@ -130,18 +131,19 @@ class JwtAuthenticator(DeclarativeAuthenticator):
|
|
130
131
|
exp = now + self._token_duration if isinstance(self._token_duration, int) else now
|
131
132
|
nbf = now
|
132
133
|
|
133
|
-
payload = self._additional_jwt_payload.eval(self.config)
|
134
|
+
payload = self._additional_jwt_payload.eval(self.config, json_loads=json.loads)
|
134
135
|
if any(prop in payload for prop in ["iss", "sub", "aud", "iat", "exp", "nbf"]):
|
135
136
|
raise ValueError(
|
136
137
|
"'iss', 'sub', 'aud', 'iat', 'exp', 'nbf' are reserved properties and should not be set as part of 'additional_jwt_payload'"
|
137
138
|
)
|
138
139
|
|
139
140
|
if self._iss:
|
140
|
-
payload["iss"] = self._iss.eval(self.config)
|
141
|
+
payload["iss"] = self._iss.eval(self.config, json_loads=json.loads)
|
141
142
|
if self._sub:
|
142
|
-
payload["sub"] = self._sub.eval(self.config)
|
143
|
+
payload["sub"] = self._sub.eval(self.config, json_loads=json.loads)
|
143
144
|
if self._aud:
|
144
|
-
payload["aud"] = self._aud.eval(self.config)
|
145
|
+
payload["aud"] = self._aud.eval(self.config, json_loads=json.loads)
|
146
|
+
|
145
147
|
payload["iat"] = now
|
146
148
|
payload["exp"] = exp
|
147
149
|
payload["nbf"] = nbf
|
@@ -151,7 +153,7 @@ class JwtAuthenticator(DeclarativeAuthenticator):
|
|
151
153
|
"""
|
152
154
|
Returns the secret key used to sign the JWT.
|
153
155
|
"""
|
154
|
-
secret_key: str = self._secret_key.eval(self.config)
|
156
|
+
secret_key: str = self._secret_key.eval(self.config, json_loads=json.loads)
|
155
157
|
return (
|
156
158
|
base64.b64encode(secret_key.encode()).decode()
|
157
159
|
if self._base64_encode_secret_key
|
@@ -176,7 +178,11 @@ class JwtAuthenticator(DeclarativeAuthenticator):
|
|
176
178
|
"""
|
177
179
|
Returns the header prefix to be used when attaching the token to the request.
|
178
180
|
"""
|
179
|
-
return
|
181
|
+
return (
|
182
|
+
self._header_prefix.eval(self.config, json_loads=json.loads)
|
183
|
+
if self._header_prefix
|
184
|
+
else None
|
185
|
+
)
|
180
186
|
|
181
187
|
@property
|
182
188
|
def auth_header(self) -> str:
|
@@ -3,11 +3,11 @@
|
|
3
3
|
#
|
4
4
|
|
5
5
|
from dataclasses import InitVar, dataclass, field
|
6
|
-
from
|
7
|
-
|
8
|
-
import pendulum
|
6
|
+
from datetime import timedelta
|
7
|
+
from typing import Any, List, Mapping, MutableMapping, Optional, Union
|
9
8
|
|
10
9
|
from airbyte_cdk.sources.declarative.auth.declarative_authenticator import DeclarativeAuthenticator
|
10
|
+
from airbyte_cdk.sources.declarative.interpolation.interpolated_boolean import InterpolatedBoolean
|
11
11
|
from airbyte_cdk.sources.declarative.interpolation.interpolated_mapping import InterpolatedMapping
|
12
12
|
from airbyte_cdk.sources.declarative.interpolation.interpolated_string import InterpolatedString
|
13
13
|
from airbyte_cdk.sources.message import MessageRepository, NoopMessageRepository
|
@@ -17,6 +17,7 @@ from airbyte_cdk.sources.streams.http.requests_native_auth.abstract_oauth import
|
|
17
17
|
from airbyte_cdk.sources.streams.http.requests_native_auth.oauth import (
|
18
18
|
SingleUseRefreshTokenOauth2Authenticator,
|
19
19
|
)
|
20
|
+
from airbyte_cdk.utils.datetime_helpers import AirbyteDateTime, ab_datetime_now, ab_datetime_parse
|
20
21
|
|
21
22
|
|
22
23
|
@dataclass
|
@@ -44,15 +45,15 @@ class DeclarativeOauth2Authenticator(AbstractOauth2Authenticator, DeclarativeAut
|
|
44
45
|
message_repository (MessageRepository): the message repository used to emit logs on HTTP requests
|
45
46
|
"""
|
46
47
|
|
47
|
-
client_id: Union[InterpolatedString, str]
|
48
|
-
client_secret: Union[InterpolatedString, str]
|
49
48
|
config: Mapping[str, Any]
|
50
49
|
parameters: InitVar[Mapping[str, Any]]
|
50
|
+
client_id: Optional[Union[InterpolatedString, str]] = None
|
51
|
+
client_secret: Optional[Union[InterpolatedString, str]] = None
|
51
52
|
token_refresh_endpoint: Optional[Union[InterpolatedString, str]] = None
|
52
53
|
refresh_token: Optional[Union[InterpolatedString, str]] = None
|
53
54
|
scopes: Optional[List[str]] = None
|
54
55
|
token_expiry_date: Optional[Union[InterpolatedString, str]] = None
|
55
|
-
_token_expiry_date: Optional[
|
56
|
+
_token_expiry_date: Optional[AirbyteDateTime] = field(init=False, repr=False, default=None)
|
56
57
|
token_expiry_date_format: Optional[str] = None
|
57
58
|
token_expiry_is_time_of_expiration: bool = False
|
58
59
|
access_token_name: Union[InterpolatedString, str] = "access_token"
|
@@ -66,6 +67,8 @@ class DeclarativeOauth2Authenticator(AbstractOauth2Authenticator, DeclarativeAut
|
|
66
67
|
grant_type_name: Union[InterpolatedString, str] = "grant_type"
|
67
68
|
grant_type: Union[InterpolatedString, str] = "refresh_token"
|
68
69
|
message_repository: MessageRepository = NoopMessageRepository()
|
70
|
+
profile_assertion: Optional[DeclarativeAuthenticator] = None
|
71
|
+
use_profile_assertion: Optional[Union[InterpolatedBoolean, str, bool]] = False
|
69
72
|
|
70
73
|
def __post_init__(self, parameters: Mapping[str, Any]) -> None:
|
71
74
|
super().__init__()
|
@@ -76,11 +79,19 @@ class DeclarativeOauth2Authenticator(AbstractOauth2Authenticator, DeclarativeAut
|
|
76
79
|
else:
|
77
80
|
self._token_refresh_endpoint = None
|
78
81
|
self._client_id_name = InterpolatedString.create(self.client_id_name, parameters=parameters)
|
79
|
-
self._client_id =
|
82
|
+
self._client_id = (
|
83
|
+
InterpolatedString.create(self.client_id, parameters=parameters)
|
84
|
+
if self.client_id
|
85
|
+
else self.client_id
|
86
|
+
)
|
80
87
|
self._client_secret_name = InterpolatedString.create(
|
81
88
|
self.client_secret_name, parameters=parameters
|
82
89
|
)
|
83
|
-
self._client_secret =
|
90
|
+
self._client_secret = (
|
91
|
+
InterpolatedString.create(self.client_secret, parameters=parameters)
|
92
|
+
if self.client_secret
|
93
|
+
else self.client_secret
|
94
|
+
)
|
84
95
|
self._refresh_token_name = InterpolatedString.create(
|
85
96
|
self.refresh_token_name, parameters=parameters
|
86
97
|
)
|
@@ -99,22 +110,43 @@ class DeclarativeOauth2Authenticator(AbstractOauth2Authenticator, DeclarativeAut
|
|
99
110
|
self.grant_type_name = InterpolatedString.create(
|
100
111
|
self.grant_type_name, parameters=parameters
|
101
112
|
)
|
102
|
-
self.grant_type = InterpolatedString.create(
|
113
|
+
self.grant_type = InterpolatedString.create(
|
114
|
+
"urn:ietf:params:oauth:grant-type:jwt-bearer"
|
115
|
+
if self.use_profile_assertion
|
116
|
+
else self.grant_type,
|
117
|
+
parameters=parameters,
|
118
|
+
)
|
103
119
|
self._refresh_request_body = InterpolatedMapping(
|
104
120
|
self.refresh_request_body or {}, parameters=parameters
|
105
121
|
)
|
106
122
|
self._refresh_request_headers = InterpolatedMapping(
|
107
123
|
self.refresh_request_headers or {}, parameters=parameters
|
108
124
|
)
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
125
|
+
try:
|
126
|
+
if (
|
127
|
+
isinstance(self.token_expiry_date, (int, str))
|
128
|
+
and str(self.token_expiry_date).isdigit()
|
129
|
+
):
|
130
|
+
self._token_expiry_date = ab_datetime_parse(self.token_expiry_date)
|
131
|
+
else:
|
132
|
+
self._token_expiry_date = (
|
133
|
+
ab_datetime_parse(
|
134
|
+
InterpolatedString.create(
|
135
|
+
self.token_expiry_date, parameters=parameters
|
136
|
+
).eval(self.config)
|
137
|
+
)
|
138
|
+
if self.token_expiry_date
|
139
|
+
else ab_datetime_now() - timedelta(days=1)
|
113
140
|
)
|
114
|
-
|
115
|
-
|
116
|
-
|
141
|
+
except ValueError as e:
|
142
|
+
raise ValueError(f"Invalid token expiry date format: {e}")
|
143
|
+
self.use_profile_assertion = (
|
144
|
+
InterpolatedBoolean(self.use_profile_assertion, parameters=parameters)
|
145
|
+
if isinstance(self.use_profile_assertion, str)
|
146
|
+
else self.use_profile_assertion
|
117
147
|
)
|
148
|
+
self.assertion_name = "assertion"
|
149
|
+
|
118
150
|
if self.access_token_value is not None:
|
119
151
|
self._access_token_value = InterpolatedString.create(
|
120
152
|
self.access_token_value, parameters=parameters
|
@@ -126,9 +158,20 @@ class DeclarativeOauth2Authenticator(AbstractOauth2Authenticator, DeclarativeAut
|
|
126
158
|
self._access_token_value if self.access_token_value else None
|
127
159
|
)
|
128
160
|
|
161
|
+
if not self.use_profile_assertion and any(
|
162
|
+
client_creds is None for client_creds in [self.client_id, self.client_secret]
|
163
|
+
):
|
164
|
+
raise ValueError(
|
165
|
+
"OAuthAuthenticator configuration error: Both 'client_id' and 'client_secret' are required for the "
|
166
|
+
"basic OAuth flow."
|
167
|
+
)
|
168
|
+
if self.profile_assertion is None and self.use_profile_assertion:
|
169
|
+
raise ValueError(
|
170
|
+
"OAuthAuthenticator configuration error: 'profile_assertion' is required when using the profile assertion flow."
|
171
|
+
)
|
129
172
|
if self.get_grant_type() == "refresh_token" and self._refresh_token is None:
|
130
173
|
raise ValueError(
|
131
|
-
"OAuthAuthenticator
|
174
|
+
"OAuthAuthenticator configuration error: A 'refresh_token' is required when the 'grant_type' is set to 'refresh_token'."
|
132
175
|
)
|
133
176
|
|
134
177
|
def get_token_refresh_endpoint(self) -> Optional[str]:
|
@@ -145,19 +188,21 @@ class DeclarativeOauth2Authenticator(AbstractOauth2Authenticator, DeclarativeAut
|
|
145
188
|
return self._client_id_name.eval(self.config) # type: ignore # eval returns a string in this context
|
146
189
|
|
147
190
|
def get_client_id(self) -> str:
|
148
|
-
client_id
|
191
|
+
client_id = self._client_id.eval(self.config) if self._client_id else self._client_id
|
149
192
|
if not client_id:
|
150
193
|
raise ValueError("OAuthAuthenticator was unable to evaluate client_id parameter")
|
151
|
-
return client_id
|
194
|
+
return client_id # type: ignore # value will be returned as a string, or an error will be raised
|
152
195
|
|
153
196
|
def get_client_secret_name(self) -> str:
|
154
197
|
return self._client_secret_name.eval(self.config) # type: ignore # eval returns a string in this context
|
155
198
|
|
156
199
|
def get_client_secret(self) -> str:
|
157
|
-
client_secret
|
200
|
+
client_secret = (
|
201
|
+
self._client_secret.eval(self.config) if self._client_secret else self._client_secret
|
202
|
+
)
|
158
203
|
if not client_secret:
|
159
204
|
raise ValueError("OAuthAuthenticator was unable to evaluate client_secret parameter")
|
160
|
-
return client_secret
|
205
|
+
return client_secret # type: ignore # value will be returned as a string, or an error will be raised
|
161
206
|
|
162
207
|
def get_refresh_token_name(self) -> str:
|
163
208
|
return self._refresh_token_name.eval(self.config) # type: ignore # eval returns a string in this context
|
@@ -186,12 +231,33 @@ class DeclarativeOauth2Authenticator(AbstractOauth2Authenticator, DeclarativeAut
|
|
186
231
|
def get_refresh_request_headers(self) -> Mapping[str, Any]:
|
187
232
|
return self._refresh_request_headers.eval(self.config)
|
188
233
|
|
189
|
-
def get_token_expiry_date(self) ->
|
190
|
-
return self._token_expiry_date # type: ignore # _token_expiry_date is
|
234
|
+
def get_token_expiry_date(self) -> AirbyteDateTime:
|
235
|
+
return self._token_expiry_date # type: ignore # _token_expiry_date is an AirbyteDateTime. It is never None despite what mypy thinks
|
191
236
|
|
192
237
|
def set_token_expiry_date(self, value: Union[str, int]) -> None:
|
193
238
|
self._token_expiry_date = self._parse_token_expiration_date(value)
|
194
239
|
|
240
|
+
def get_assertion_name(self) -> str:
|
241
|
+
return self.assertion_name
|
242
|
+
|
243
|
+
def get_assertion(self) -> str:
|
244
|
+
if self.profile_assertion is None:
|
245
|
+
raise ValueError("profile_assertion is not set")
|
246
|
+
return self.profile_assertion.token
|
247
|
+
|
248
|
+
def build_refresh_request_body(self) -> Mapping[str, Any]:
|
249
|
+
"""
|
250
|
+
Returns the request body to set on the refresh request
|
251
|
+
|
252
|
+
Override to define additional parameters
|
253
|
+
"""
|
254
|
+
if self.use_profile_assertion:
|
255
|
+
return {
|
256
|
+
self.get_grant_type_name(): self.get_grant_type(),
|
257
|
+
self.get_assertion_name(): self.get_assertion(),
|
258
|
+
}
|
259
|
+
return super().build_refresh_request_body()
|
260
|
+
|
195
261
|
@property
|
196
262
|
def access_token(self) -> str:
|
197
263
|
if self._access_token is None:
|
@@ -5,7 +5,7 @@
|
|
5
5
|
import base64
|
6
6
|
import logging
|
7
7
|
from dataclasses import InitVar, dataclass
|
8
|
-
from typing import Any, Mapping,
|
8
|
+
from typing import Any, Mapping, Union
|
9
9
|
|
10
10
|
import requests
|
11
11
|
from cachetools import TTLCache, cached
|
@@ -45,6 +45,11 @@ class ApiKeyAuthenticator(DeclarativeAuthenticator):
|
|
45
45
|
config: Config
|
46
46
|
parameters: InitVar[Mapping[str, Any]]
|
47
47
|
|
48
|
+
def __post_init__(self, parameters: Mapping[str, Any]) -> None:
|
49
|
+
self._field_name = InterpolatedString.create(
|
50
|
+
self.request_option.field_name, parameters=parameters
|
51
|
+
)
|
52
|
+
|
48
53
|
@property
|
49
54
|
def auth_header(self) -> str:
|
50
55
|
options = self._get_request_options(RequestOptionType.header)
|
@@ -55,9 +60,9 @@ class ApiKeyAuthenticator(DeclarativeAuthenticator):
|
|
55
60
|
return self.token_provider.get_token()
|
56
61
|
|
57
62
|
def _get_request_options(self, option_type: RequestOptionType) -> Mapping[str, Any]:
|
58
|
-
options
|
63
|
+
options = {}
|
59
64
|
if self.request_option.inject_into == option_type:
|
60
|
-
self.
|
65
|
+
options[self._field_name.eval(self.config)] = self.token
|
61
66
|
return options
|
62
67
|
|
63
68
|
def get_request_params(self) -> Mapping[str, Any]:
|
@@ -9,9 +9,7 @@ from dataclasses import InitVar, dataclass, field
|
|
9
9
|
from typing import Any, List, Mapping, Optional, Union
|
10
10
|
|
11
11
|
import dpath
|
12
|
-
import pendulum
|
13
12
|
from isodate import Duration
|
14
|
-
from pendulum import DateTime
|
15
13
|
|
16
14
|
from airbyte_cdk.sources.declarative.decoders.decoder import Decoder
|
17
15
|
from airbyte_cdk.sources.declarative.decoders.json_decoder import JsonDecoder
|
@@ -21,6 +19,7 @@ from airbyte_cdk.sources.declarative.requesters.requester import Requester
|
|
21
19
|
from airbyte_cdk.sources.http_logger import format_http_message
|
22
20
|
from airbyte_cdk.sources.message import MessageRepository, NoopMessageRepository
|
23
21
|
from airbyte_cdk.sources.types import Config
|
22
|
+
from airbyte_cdk.utils.datetime_helpers import AirbyteDateTime, ab_datetime_now
|
24
23
|
|
25
24
|
|
26
25
|
class TokenProvider:
|
@@ -38,7 +37,7 @@ class SessionTokenProvider(TokenProvider):
|
|
38
37
|
message_repository: MessageRepository = NoopMessageRepository()
|
39
38
|
decoder: Decoder = field(default_factory=lambda: JsonDecoder(parameters={}))
|
40
39
|
|
41
|
-
_next_expiration_time: Optional[
|
40
|
+
_next_expiration_time: Optional[AirbyteDateTime] = None
|
42
41
|
_token: Optional[str] = None
|
43
42
|
|
44
43
|
def get_token(self) -> str:
|
@@ -48,7 +47,7 @@ class SessionTokenProvider(TokenProvider):
|
|
48
47
|
return self._token
|
49
48
|
|
50
49
|
def _refresh_if_necessary(self) -> None:
|
51
|
-
if self._next_expiration_time is None or self._next_expiration_time <
|
50
|
+
if self._next_expiration_time is None or self._next_expiration_time < ab_datetime_now():
|
52
51
|
self._refresh()
|
53
52
|
|
54
53
|
def _refresh(self) -> None:
|
@@ -65,7 +64,7 @@ class SessionTokenProvider(TokenProvider):
|
|
65
64
|
raise ReadException("Failed to get session token, response got ignored by requester")
|
66
65
|
session_token = dpath.get(next(self.decoder.decode(response)), self.session_token_path)
|
67
66
|
if self.expiration_duration is not None:
|
68
|
-
self._next_expiration_time =
|
67
|
+
self._next_expiration_time = ab_datetime_now() + self.expiration_duration
|
69
68
|
self._token = session_token # type: ignore # Returned decoded response will be Mapping and therefore session_token will be str or None
|
70
69
|
|
71
70
|
|
@@ -21,8 +21,12 @@ class CheckDynamicStream(ConnectionChecker):
|
|
21
21
|
stream_count (int): numbers of streams to check
|
22
22
|
"""
|
23
23
|
|
24
|
+
# TODO: Add field stream_names to check_connection for static streams
|
25
|
+
# https://github.com/airbytehq/airbyte-python-cdk/pull/293#discussion_r1934933483
|
26
|
+
|
24
27
|
stream_count: int
|
25
28
|
parameters: InitVar[Mapping[str, Any]]
|
29
|
+
use_check_availability: bool = True
|
26
30
|
|
27
31
|
def __post_init__(self, parameters: Mapping[str, Any]) -> None:
|
28
32
|
self._parameters = parameters
|
@@ -31,21 +35,27 @@ class CheckDynamicStream(ConnectionChecker):
|
|
31
35
|
self, source: AbstractSource, logger: logging.Logger, config: Mapping[str, Any]
|
32
36
|
) -> Tuple[bool, Any]:
|
33
37
|
streams = source.streams(config=config)
|
38
|
+
|
34
39
|
if len(streams) == 0:
|
35
40
|
return False, f"No streams to connect to from source {source}"
|
41
|
+
if not self.use_check_availability:
|
42
|
+
return True, None
|
43
|
+
|
44
|
+
availability_strategy = HttpAvailabilityStrategy()
|
36
45
|
|
37
|
-
|
38
|
-
stream
|
39
|
-
availability_strategy = HttpAvailabilityStrategy()
|
40
|
-
try:
|
46
|
+
try:
|
47
|
+
for stream in streams[: min(self.stream_count, len(streams))]:
|
41
48
|
stream_is_available, reason = availability_strategy.check_availability(
|
42
49
|
stream, logger
|
43
50
|
)
|
44
51
|
if not stream_is_available:
|
52
|
+
logger.warning(f"Stream {stream.name} is not available: {reason}")
|
45
53
|
return False, reason
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
54
|
+
except Exception as error:
|
55
|
+
error_message = (
|
56
|
+
f"Encountered an error trying to connect to stream {stream.name}. Error: {error}"
|
57
|
+
)
|
58
|
+
logger.error(error_message, exc_info=True)
|
59
|
+
return False, error_message
|
60
|
+
|
51
61
|
return True, None
|