airbyte-cdk 0.36.5__py3-none-any.whl → 0.37.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/sources/declarative/auth/oauth.py +13 -6
- airbyte_cdk/sources/declarative/declarative_component_schema.yaml +0 -1
- airbyte_cdk/sources/declarative/models/declarative_component_schema.py +2 -2
- airbyte_cdk/sources/streams/http/requests_native_auth/abstract_oauth.py +2 -2
- {airbyte_cdk-0.36.5.dist-info → airbyte_cdk-0.37.0.dist-info}/METADATA +1 -1
- {airbyte_cdk-0.36.5.dist-info → airbyte_cdk-0.37.0.dist-info}/RECORD +10 -10
- unit_tests/sources/declarative/auth/test_oauth.py +77 -2
- {airbyte_cdk-0.36.5.dist-info → airbyte_cdk-0.37.0.dist-info}/LICENSE.txt +0 -0
- {airbyte_cdk-0.36.5.dist-info → airbyte_cdk-0.37.0.dist-info}/WHEEL +0 -0
- {airbyte_cdk-0.36.5.dist-info → airbyte_cdk-0.37.0.dist-info}/top_level.txt +0 -0
@@ -31,15 +31,15 @@ class DeclarativeOauth2Authenticator(AbstractOauth2Authenticator, DeclarativeAut
|
|
31
31
|
token_expiry_date (Optional[Union[InterpolatedString, str]]): The access token expiration date
|
32
32
|
token_expiry_date_format str: format of the datetime; provide it if expires_in is returned in datetime instead of seconds
|
33
33
|
refresh_request_body (Optional[Mapping[str, Any]]): The request body to send in the refresh request
|
34
|
-
grant_type: The grant_type to request for access_token
|
34
|
+
grant_type: The grant_type to request for access_token. If set to refresh_token, the refresh_token parameter has to be provided
|
35
35
|
"""
|
36
36
|
|
37
37
|
token_refresh_endpoint: Union[InterpolatedString, str]
|
38
38
|
client_id: Union[InterpolatedString, str]
|
39
39
|
client_secret: Union[InterpolatedString, str]
|
40
|
-
refresh_token: Union[InterpolatedString, str]
|
41
40
|
config: Mapping[str, Any]
|
42
41
|
parameters: InitVar[Mapping[str, Any]]
|
42
|
+
refresh_token: Optional[Union[InterpolatedString, str]] = None
|
43
43
|
scopes: Optional[List[str]] = None
|
44
44
|
token_expiry_date: Optional[Union[InterpolatedString, str]] = None
|
45
45
|
_token_expiry_date: pendulum.DateTime = field(init=False, repr=False, default=None)
|
@@ -53,7 +53,8 @@ class DeclarativeOauth2Authenticator(AbstractOauth2Authenticator, DeclarativeAut
|
|
53
53
|
self.token_refresh_endpoint = InterpolatedString.create(self.token_refresh_endpoint, parameters=parameters)
|
54
54
|
self.client_id = InterpolatedString.create(self.client_id, parameters=parameters)
|
55
55
|
self.client_secret = InterpolatedString.create(self.client_secret, parameters=parameters)
|
56
|
-
self.refresh_token
|
56
|
+
if self.refresh_token is not None:
|
57
|
+
self.refresh_token = InterpolatedString.create(self.refresh_token, parameters=parameters)
|
57
58
|
self.access_token_name = InterpolatedString.create(self.access_token_name, parameters=parameters)
|
58
59
|
self.expires_in_name = InterpolatedString.create(self.expires_in_name, parameters=parameters)
|
59
60
|
self.grant_type = InterpolatedString.create(self.grant_type, parameters=parameters)
|
@@ -65,6 +66,9 @@ class DeclarativeOauth2Authenticator(AbstractOauth2Authenticator, DeclarativeAut
|
|
65
66
|
)
|
66
67
|
self._access_token = None
|
67
68
|
|
69
|
+
if self.get_grant_type() == "refresh_token" and self.refresh_token is None:
|
70
|
+
raise ValueError("OAuthAuthenticator needs a refresh_token parameter if grant_type is set to `refresh_token`")
|
71
|
+
|
68
72
|
def get_token_refresh_endpoint(self) -> str:
|
69
73
|
return self.token_refresh_endpoint.eval(self.config)
|
70
74
|
|
@@ -74,8 +78,8 @@ class DeclarativeOauth2Authenticator(AbstractOauth2Authenticator, DeclarativeAut
|
|
74
78
|
def get_client_secret(self) -> str:
|
75
79
|
return self.client_secret.eval(self.config)
|
76
80
|
|
77
|
-
def get_refresh_token(self) -> str:
|
78
|
-
return self.refresh_token.eval(self.config)
|
81
|
+
def get_refresh_token(self) -> Optional[str]:
|
82
|
+
return None if self.refresh_token is None else self.refresh_token.eval(self.config)
|
79
83
|
|
80
84
|
def get_scopes(self) -> [str]:
|
81
85
|
return self.scopes
|
@@ -117,7 +121,10 @@ class DeclarativeOauth2Authenticator(AbstractOauth2Authenticator, DeclarativeAut
|
|
117
121
|
if self.token_expiry_date_format:
|
118
122
|
self._token_expiry_date = pendulum.from_format(value, self.token_expiry_date_format)
|
119
123
|
else:
|
120
|
-
|
124
|
+
try:
|
125
|
+
self._token_expiry_date = pendulum.now().add(seconds=int(float(value)))
|
126
|
+
except ValueError:
|
127
|
+
raise ValueError(f"Invalid token expiry value {value}; a number is required.")
|
121
128
|
|
122
129
|
@property
|
123
130
|
def access_token(self) -> str:
|
@@ -277,8 +277,8 @@ class OAuthAuthenticator(BaseModel):
|
|
277
277
|
],
|
278
278
|
title="Client Secret",
|
279
279
|
)
|
280
|
-
refresh_token: str = Field(
|
281
|
-
|
280
|
+
refresh_token: Optional[str] = Field(
|
281
|
+
None,
|
282
282
|
description="Credential artifact used to get a new access token.",
|
283
283
|
examples=[
|
284
284
|
"{{ config['refresh_token'] }}",
|
@@ -4,7 +4,7 @@
|
|
4
4
|
|
5
5
|
import logging
|
6
6
|
from abc import abstractmethod
|
7
|
-
from typing import Any, List, Mapping, MutableMapping, Tuple, Union
|
7
|
+
from typing import Any, List, Mapping, MutableMapping, Optional, Tuple, Union
|
8
8
|
|
9
9
|
import backoff
|
10
10
|
import pendulum
|
@@ -111,7 +111,7 @@ class AbstractOauth2Authenticator(AuthBase):
|
|
111
111
|
"""The client secret to authenticate"""
|
112
112
|
|
113
113
|
@abstractmethod
|
114
|
-
def get_refresh_token(self) -> str:
|
114
|
+
def get_refresh_token(self) -> Optional[str]:
|
115
115
|
"""The token used to refresh the access token when it expires"""
|
116
116
|
|
117
117
|
@abstractmethod
|
@@ -22,7 +22,7 @@ airbyte_cdk/sources/connector_state_manager.py,sha256=_R-2QnMGimKL0t5aV4f6P1dgd-
|
|
22
22
|
airbyte_cdk/sources/source.py,sha256=N3vHZzdUsBETFsql-YpO-LcgjolT_jcnAuHBhGD6Hqk,4278
|
23
23
|
airbyte_cdk/sources/declarative/__init__.py,sha256=ZnqYNxHsKCgO38IwB34RQyRMXTs4GTvlRi3ImKnIioo,61
|
24
24
|
airbyte_cdk/sources/declarative/create_partial.py,sha256=sUJOwD8hBzW4pxw2XhYlSTMgl-WMc5WpP5Oq_jo3fHw,3371
|
25
|
-
airbyte_cdk/sources/declarative/declarative_component_schema.yaml,sha256=
|
25
|
+
airbyte_cdk/sources/declarative/declarative_component_schema.yaml,sha256=0BMvpifiVZ-IZytuxGlPRJAFMtiVBEFRjYBot9Wl40I,75087
|
26
26
|
airbyte_cdk/sources/declarative/declarative_source.py,sha256=U2As9PDKmcWDgbsWUo-RetJ9fxQOBlwntWZ0NOgs5Ac,1453
|
27
27
|
airbyte_cdk/sources/declarative/declarative_stream.py,sha256=0iZSpypxt8bhO3Lmf3BpGRTO7Fp0Q2GI8m8xyJJUjeM,6580
|
28
28
|
airbyte_cdk/sources/declarative/exceptions.py,sha256=kTPUA4I2NV4J6HDz-mKPGMrfuc592akJnOyYx38l_QM,176
|
@@ -31,7 +31,7 @@ airbyte_cdk/sources/declarative/types.py,sha256=b_RJpL9TyAgxJIRYZx5BxpC39p-WccHK
|
|
31
31
|
airbyte_cdk/sources/declarative/yaml_declarative_source.py,sha256=I9Bs9RDsFT8JNiJWRDjKYhqwvv4pqzgYZtF5hVuTDqI,1684
|
32
32
|
airbyte_cdk/sources/declarative/auth/__init__.py,sha256=DyQdO5mdKGsttWdEUqxb6WVgD7zTcvpJz-Oet_VNeBg,201
|
33
33
|
airbyte_cdk/sources/declarative/auth/declarative_authenticator.py,sha256=4nEvMQWGmQ_-KROwcI8dsDbU4NjjxiGz3nsxxfWqBF8,663
|
34
|
-
airbyte_cdk/sources/declarative/auth/oauth.py,sha256=
|
34
|
+
airbyte_cdk/sources/declarative/auth/oauth.py,sha256=pD0KnWA8kECcm1wMHsIrLJzUT9sil0iZu1ZUTn-los0,7403
|
35
35
|
airbyte_cdk/sources/declarative/auth/token.py,sha256=8WVZoV02ujk5KuWC6eOsnUSI6zp4Jm7TvWdgxKFiffk,10520
|
36
36
|
airbyte_cdk/sources/declarative/checks/__init__.py,sha256=WWXMfvKkndqwAUZdgSr7xVHVXDFTKCUQ9EubqT7H4QE,274
|
37
37
|
airbyte_cdk/sources/declarative/checks/check_stream.py,sha256=9fJt1ma31tBiKfuqjTO2SIK_b14NgjqkPJa4om0eJM0,2065
|
@@ -59,7 +59,7 @@ airbyte_cdk/sources/declarative/interpolation/interpolation.py,sha256=dyIM-bzh54
|
|
59
59
|
airbyte_cdk/sources/declarative/interpolation/jinja.py,sha256=Dc0F87nElWsz_Ikj938eQ9uqZvyqgFhZ8Dqf_-hvndc,4800
|
60
60
|
airbyte_cdk/sources/declarative/interpolation/macros.py,sha256=V6WGKJ9cXX1rjuM4bK3Cs9xEryMlkY2U3FMsSBhrgC8,3098
|
61
61
|
airbyte_cdk/sources/declarative/models/__init__.py,sha256=EiYnzwCHZV7EYqMJqcy6xKSeHvTKZBsQndjbEwmiTW4,93
|
62
|
-
airbyte_cdk/sources/declarative/models/declarative_component_schema.py,sha256=
|
62
|
+
airbyte_cdk/sources/declarative/models/declarative_component_schema.py,sha256=PghniMTIvEHi-NPIRK7rpPJtYRtYgigR1T0hyKP6fl8,50452
|
63
63
|
airbyte_cdk/sources/declarative/parsers/__init__.py,sha256=ZnqYNxHsKCgO38IwB34RQyRMXTs4GTvlRi3ImKnIioo,61
|
64
64
|
airbyte_cdk/sources/declarative/parsers/class_types_registry.py,sha256=bK4a74opm6WHyV7HqOVws6GE5Z7cLNc5MaTha69abIQ,6086
|
65
65
|
airbyte_cdk/sources/declarative/parsers/custom_exceptions.py,sha256=y7_G5mM07zxT5YG975kdC2PAja-Uc83pYp8WrV3GNdo,522
|
@@ -139,7 +139,7 @@ airbyte_cdk/sources/streams/http/auth/core.py,sha256=_s9wewvvIcOgYjhHGDj_YHApnF5
|
|
139
139
|
airbyte_cdk/sources/streams/http/auth/oauth.py,sha256=zchPWN1utNg02F93f5b4UFI5OXYo8-QhocbsXhLdG4U,4135
|
140
140
|
airbyte_cdk/sources/streams/http/auth/token.py,sha256=oU1ul0LsGsPGN_vOJOKw1xX2y_XWULRxjqXu7Rivcr8,1940
|
141
141
|
airbyte_cdk/sources/streams/http/requests_native_auth/__init__.py,sha256=RN0D3nOX1xLgwEwKWu6pkGy3XqBFzKSNZ8Lf6umU2eY,413
|
142
|
-
airbyte_cdk/sources/streams/http/requests_native_auth/abstract_oauth.py,sha256=
|
142
|
+
airbyte_cdk/sources/streams/http/requests_native_auth/abstract_oauth.py,sha256=dw9mmIOf05NDqKzzvRA3tXKjx1LvVGm1tPt8TQhf5Y8,5339
|
143
143
|
airbyte_cdk/sources/streams/http/requests_native_auth/abstract_token.py,sha256=T0hVF2cBXGgIfrCslvTC1uNm9rNbYjENNl2Cb3mXuSY,961
|
144
144
|
airbyte_cdk/sources/streams/http/requests_native_auth/oauth.py,sha256=QWTjL6blaEAK457TSJlTDcczITdAu0RqMEhxX-rpAWo,11704
|
145
145
|
airbyte_cdk/sources/streams/http/requests_native_auth/token.py,sha256=hDti8DlF_R5YYX95hg9BPogYtG-KUYtOifrFDv_L3Hk,2456
|
@@ -181,7 +181,7 @@ unit_tests/sources/declarative/test_declarative_stream.py,sha256=3leJnZIYHiFq8XI
|
|
181
181
|
unit_tests/sources/declarative/test_manifest_declarative_source.py,sha256=GckUc3nepzZkD1UM24woHlYCVZb5DP4IAQC3IeMyZF0,58924
|
182
182
|
unit_tests/sources/declarative/test_yaml_declarative_source.py,sha256=6HhsUFgB7ueN0yOUHWb4gpPYLng5jasxN_plvz3x37g,5097
|
183
183
|
unit_tests/sources/declarative/auth/__init__.py,sha256=4Hw-PX1-VgESLF16cDdvuYCzGJtHntThLF4qIiULWeo,61
|
184
|
-
unit_tests/sources/declarative/auth/test_oauth.py,sha256=
|
184
|
+
unit_tests/sources/declarative/auth/test_oauth.py,sha256=j-xEUbRPs5jnRAvKCNLKDpEbAZLmXHEy9tSEkYUrYx0,8442
|
185
185
|
unit_tests/sources/declarative/auth/test_session_token_auth.py,sha256=mxWCm_0AyVI6J1Q5CjogXY-EkXFfWkMZjNtBeb0bOow,6135
|
186
186
|
unit_tests/sources/declarative/auth/test_token_auth.py,sha256=voeoFiZOp2BUSJ0FvlXdRsan_vwIbteA2tpb1oRb40g,3711
|
187
187
|
unit_tests/sources/declarative/checks/__init__.py,sha256=ZnqYNxHsKCgO38IwB34RQyRMXTs4GTvlRi3ImKnIioo,61
|
@@ -259,8 +259,8 @@ unit_tests/utils/test_schema_inferrer.py,sha256=ckl17GlNOZInqgxni7Z2A0bg_p6JDy0G
|
|
259
259
|
unit_tests/utils/test_secret_utils.py,sha256=XKe0f1RHYii8iwE6ATmBr5JGDI1pzzrnZUGdUSMJQP4,4886
|
260
260
|
unit_tests/utils/test_stream_status_utils.py,sha256=NpV155JMXA6CG-2Zvofa14lItobyh3Onttc59X4m5DI,3382
|
261
261
|
unit_tests/utils/test_traced_exception.py,sha256=bDFP5zMBizFenz6V2WvEZTRCKGB5ijh3DBezjbfoYIs,4198
|
262
|
-
airbyte_cdk-0.
|
263
|
-
airbyte_cdk-0.
|
264
|
-
airbyte_cdk-0.
|
265
|
-
airbyte_cdk-0.
|
266
|
-
airbyte_cdk-0.
|
262
|
+
airbyte_cdk-0.37.0.dist-info/LICENSE.txt,sha256=Wfe61S4BaGPj404v8lrAbvhjYR68SHlkzeYrg3_bbuM,1051
|
263
|
+
airbyte_cdk-0.37.0.dist-info/METADATA,sha256=6HRZ3Hyt-WKn2QNme7H6B-s3mCYhi1ur_apLnXMnODU,8902
|
264
|
+
airbyte_cdk-0.37.0.dist-info/WHEEL,sha256=pkctZYzUS4AYVn6dJ-7367OJZivF2e8RA9b_ZBjif18,92
|
265
|
+
airbyte_cdk-0.37.0.dist-info/top_level.txt,sha256=edvsDKTnE6sD2wfCUaeTfKf5gQIL6CPVMwVL2sWZzqo,51
|
266
|
+
airbyte_cdk-0.37.0.dist-info/RECORD,,
|
@@ -65,6 +65,42 @@ class TestOauth2Authenticator:
|
|
65
65
|
}
|
66
66
|
assert body == expected
|
67
67
|
|
68
|
+
def test_refresh_without_refresh_token(self):
|
69
|
+
"""
|
70
|
+
Should work fine for grant_type client_credentials.
|
71
|
+
"""
|
72
|
+
oauth = DeclarativeOauth2Authenticator(
|
73
|
+
token_refresh_endpoint="{{ config['refresh_endpoint'] }}",
|
74
|
+
client_id="{{ config['client_id'] }}",
|
75
|
+
client_secret="{{ config['client_secret'] }}",
|
76
|
+
config=config,
|
77
|
+
parameters={},
|
78
|
+
grant_type="client_credentials",
|
79
|
+
)
|
80
|
+
body = oauth.build_refresh_request_body()
|
81
|
+
expected = {
|
82
|
+
"grant_type": "client_credentials",
|
83
|
+
"client_id": "some_client_id",
|
84
|
+
"client_secret": "some_client_secret",
|
85
|
+
"refresh_token": None,
|
86
|
+
"scopes": None,
|
87
|
+
}
|
88
|
+
assert body == expected
|
89
|
+
|
90
|
+
def test_error_on_refresh_token_grant_without_refresh_token(self):
|
91
|
+
"""
|
92
|
+
Should throw an error if grant_type refresh_token is configured without refresh_token.
|
93
|
+
"""
|
94
|
+
with pytest.raises(ValueError):
|
95
|
+
DeclarativeOauth2Authenticator(
|
96
|
+
token_refresh_endpoint="{{ config['refresh_endpoint'] }}",
|
97
|
+
client_id="{{ config['client_id'] }}",
|
98
|
+
client_secret="{{ config['client_secret'] }}",
|
99
|
+
config=config,
|
100
|
+
parameters={},
|
101
|
+
grant_type="refresh_token",
|
102
|
+
)
|
103
|
+
|
68
104
|
def test_refresh_access_token(self, mocker):
|
69
105
|
oauth = DeclarativeOauth2Authenticator(
|
70
106
|
token_refresh_endpoint="{{ config['refresh_endpoint'] }}",
|
@@ -92,12 +128,11 @@ class TestOauth2Authenticator:
|
|
92
128
|
@pytest.mark.parametrize(
|
93
129
|
"expires_in_response, token_expiry_date_format",
|
94
130
|
[
|
95
|
-
(86400, None),
|
96
131
|
("2020-01-02T00:00:00Z", "YYYY-MM-DDTHH:mm:ss[Z]"),
|
97
132
|
("2020-01-02T00:00:00.000000+00:00", "YYYY-MM-DDTHH:mm:ss.SSSSSSZ"),
|
98
133
|
("2020-01-02", "YYYY-MM-DD"),
|
99
134
|
],
|
100
|
-
ids=["
|
135
|
+
ids=["rfc3339", "iso8601", "simple_date"],
|
101
136
|
)
|
102
137
|
@freezegun.freeze_time("2020-01-01")
|
103
138
|
def test_refresh_access_token_expire_format(self, mocker, expires_in_response, token_expiry_date_format):
|
@@ -127,6 +162,46 @@ class TestOauth2Authenticator:
|
|
127
162
|
assert "access_token" == token
|
128
163
|
assert oauth.get_token_expiry_date() == pendulum.parse(next_day)
|
129
164
|
|
165
|
+
@pytest.mark.parametrize(
|
166
|
+
"expires_in_response, next_day, raises",
|
167
|
+
[
|
168
|
+
(86400, "2020-01-02T00:00:00Z", False),
|
169
|
+
(86400.1, "2020-01-02T00:00:00Z", False),
|
170
|
+
("86400", "2020-01-02T00:00:00Z", False),
|
171
|
+
("86400.1", "2020-01-02T00:00:00Z", False),
|
172
|
+
("2020-01-02T00:00:00Z", "2020-01-02T00:00:00Z", True),
|
173
|
+
],
|
174
|
+
ids=["time_in_seconds", "time_in_seconds_float", "time_in_seconds_str", "time_in_seconds_str_float", "invalid"],
|
175
|
+
)
|
176
|
+
@freezegun.freeze_time("2020-01-01")
|
177
|
+
def test_set_token_expiry_date_no_format(self, mocker, expires_in_response, next_day, raises):
|
178
|
+
config.update({"token_expiry_date": pendulum.parse(next_day).subtract(days=2).to_rfc3339_string()})
|
179
|
+
oauth = DeclarativeOauth2Authenticator(
|
180
|
+
token_refresh_endpoint="{{ config['refresh_endpoint'] }}",
|
181
|
+
client_id="{{ config['client_id'] }}",
|
182
|
+
client_secret="{{ config['client_secret'] }}",
|
183
|
+
refresh_token="{{ config['refresh_token'] }}",
|
184
|
+
config=config,
|
185
|
+
scopes=["scope1", "scope2"],
|
186
|
+
refresh_request_body={
|
187
|
+
"custom_field": "{{ config['custom_field'] }}",
|
188
|
+
"another_field": "{{ config['another_field'] }}",
|
189
|
+
"scopes": ["no_override"],
|
190
|
+
},
|
191
|
+
parameters={},
|
192
|
+
)
|
193
|
+
|
194
|
+
resp.status_code = 200
|
195
|
+
mocker.patch.object(resp, "json", return_value={"access_token": "access_token", "expires_in": expires_in_response})
|
196
|
+
mocker.patch.object(requests, "request", side_effect=mock_request, autospec=True)
|
197
|
+
if raises:
|
198
|
+
with pytest.raises(ValueError):
|
199
|
+
oauth.get_access_token()
|
200
|
+
else:
|
201
|
+
token = oauth.get_access_token()
|
202
|
+
assert "access_token" == token
|
203
|
+
assert oauth.get_token_expiry_date() == pendulum.parse(next_day)
|
204
|
+
|
130
205
|
|
131
206
|
def mock_request(method, url, data):
|
132
207
|
if url == "refresh_end":
|
File without changes
|
File without changes
|
File without changes
|