airbyte-cdk 0.62.0__py3-none-any.whl → 0.62.2__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
@@ -38,7 +38,7 @@ class ConnectorStateManager:
38
38
 
39
39
  def __init__(
40
40
  self,
41
- stream_instance_map: Mapping[str, AirbyteStream],
41
+ stream_instance_map: Mapping[str, Union[Stream, AirbyteStream]],
42
42
  state: Optional[Union[List[AirbyteStateMessage], MutableMapping[str, Any]]] = None,
43
43
  ):
44
44
  shared_state, per_stream_states = self._extract_from_state_message(state, stream_instance_map)
@@ -107,7 +107,9 @@ class ConnectorStateManager:
107
107
 
108
108
  @classmethod
109
109
  def _extract_from_state_message(
110
- cls, state: Optional[Union[List[AirbyteStateMessage], MutableMapping[str, Any]]], stream_instance_map: Mapping[str, AirbyteStream]
110
+ cls,
111
+ state: Optional[Union[List[AirbyteStateMessage], MutableMapping[str, Any]]],
112
+ stream_instance_map: Mapping[str, Union[Stream, AirbyteStream]],
111
113
  ) -> Tuple[Optional[AirbyteStateBlob], MutableMapping[HashableStreamDescriptor, Optional[AirbyteStateBlob]]]:
112
114
  """
113
115
  Takes an incoming list of state messages or the legacy state format and extracts state attributes according to type
@@ -159,7 +161,7 @@ class ConnectorStateManager:
159
161
 
160
162
  @staticmethod
161
163
  def _create_descriptor_to_stream_state_mapping(
162
- state: MutableMapping[str, Any], stream_to_instance_map: Mapping[str, Stream]
164
+ state: MutableMapping[str, Any], stream_to_instance_map: Mapping[str, Union[Stream, AirbyteStream]]
163
165
  ) -> MutableMapping[HashableStreamDescriptor, Optional[AirbyteStateBlob]]:
164
166
  """
165
167
  Takes incoming state received in the legacy format and transforms it into a mapping of StreamDescriptor to AirbyteStreamState
@@ -14,6 +14,7 @@ from airbyte_cdk.models import FailureType, Level
14
14
  from airbyte_cdk.sources.http_logger import format_http_message
15
15
  from airbyte_cdk.sources.message import MessageRepository, NoopMessageRepository
16
16
  from airbyte_cdk.utils import AirbyteTracedException
17
+ from airbyte_cdk.utils.airbyte_secrets_utils import add_to_secrets
17
18
  from requests.auth import AuthBase
18
19
 
19
20
  from ..exceptions import DefaultBackoffException
@@ -115,9 +116,20 @@ class AbstractOauth2Authenticator(AuthBase):
115
116
  def _get_refresh_access_token_response(self) -> Any:
116
117
  try:
117
118
  response = requests.request(method="POST", url=self.get_token_refresh_endpoint(), data=self.build_refresh_request_body())
118
- self._log_response(response)
119
- response.raise_for_status()
120
- return response.json()
119
+ if response.ok:
120
+ response_json = response.json()
121
+ # Add the access token to the list of secrets so it is replaced before logging the response
122
+ # An argument could be made to remove the prevous access key from the list of secrets, but unmasking values seems like a security incident waiting to happen...
123
+ access_key = response_json.get(self.get_access_token_name())
124
+ if not access_key:
125
+ raise Exception("Token refresh API response was missing access token {self.get_access_token_name()}")
126
+ add_to_secrets(access_key)
127
+ self._log_response(response)
128
+ return response_json
129
+ else:
130
+ # log the response even if the request failed for troubleshooting purposes
131
+ self._log_response(response)
132
+ response.raise_for_status()
121
133
  except requests.exceptions.RequestException as e:
122
134
  if e.response is not None:
123
135
  if e.response.status_code == 429 or e.response.status_code >= 500:
@@ -60,7 +60,9 @@ class HttpMocker(contextlib.ContextDecorator):
60
60
  getattr(self._mocker, method)(
61
61
  requests_mock.ANY,
62
62
  additional_matcher=self._matches_wrapper(matcher),
63
- response_list=[{"text": response.body, "status_code": response.status_code} for response in responses],
63
+ response_list=[
64
+ {"text": response.body, "status_code": response.status_code, "headers": response.headers} for response in responses
65
+ ],
64
66
  )
65
67
 
66
68
  def get(self, request: HttpRequest, responses: Union[HttpResponse, List[HttpResponse]]) -> None:
@@ -1,10 +1,14 @@
1
1
  # Copyright (c) 2023 Airbyte, Inc., all rights reserved.
2
2
 
3
+ from types import MappingProxyType
4
+ from typing import Mapping
5
+
3
6
 
4
7
  class HttpResponse:
5
- def __init__(self, body: str, status_code: int = 200):
8
+ def __init__(self, body: str, status_code: int = 200, headers: Mapping[str, str] = MappingProxyType({})):
6
9
  self._body = body
7
10
  self._status_code = status_code
11
+ self._headers = headers
8
12
 
9
13
  @property
10
14
  def body(self) -> str:
@@ -13,3 +17,7 @@ class HttpResponse:
13
17
  @property
14
18
  def status_code(self) -> int:
15
19
  return self._status_code
20
+
21
+ @property
22
+ def headers(self) -> Mapping[str, str]:
23
+ return self._headers
@@ -10,7 +10,7 @@ import dpath.util
10
10
  def get_secret_paths(spec: Mapping[str, Any]) -> List[List[str]]:
11
11
  paths = []
12
12
 
13
- def traverse_schema(schema_item: Any, path: List[str]):
13
+ def traverse_schema(schema_item: Any, path: List[str]) -> None:
14
14
  """
15
15
  schema_item can be any property or value in the originally input jsonschema, depending on how far down the recursion stack we go
16
16
  path is the path to that schema item in the original input
@@ -56,12 +56,18 @@ def get_secrets(connection_specification: Mapping[str, Any], config: Mapping[str
56
56
  __SECRETS_FROM_CONFIG: List[str] = []
57
57
 
58
58
 
59
- def update_secrets(secrets: List[str]):
59
+ def update_secrets(secrets: List[str]) -> None:
60
60
  """Update the list of secrets to be replaced"""
61
61
  global __SECRETS_FROM_CONFIG
62
62
  __SECRETS_FROM_CONFIG = secrets
63
63
 
64
64
 
65
+ def add_to_secrets(secret: str) -> None:
66
+ """Add to the list of secrets to be replaced"""
67
+ global __SECRETS_FROM_CONFIG
68
+ __SECRETS_FROM_CONFIG.append(secret)
69
+
70
+
65
71
  def filter_secrets(string: str) -> str:
66
72
  """Filter secrets from a string by replacing them with ****"""
67
73
  # TODO this should perform a maximal match for each secret. if "x" and "xk" are both secret values, and this method is called twice on
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: airbyte-cdk
3
- Version: 0.62.0
3
+ Version: 0.62.2
4
4
  Summary: A framework for writing Airbyte Connectors.
5
5
  Home-page: https://github.com/airbytehq/airbyte
6
6
  Author: Airbyte
@@ -26,7 +26,7 @@ airbyte_cdk/models/well_known_types.py,sha256=KKfNbow2gdLoC1Z4hcXy_JR8m_acsB2ol7
26
26
  airbyte_cdk/sources/__init__.py,sha256=Ov7Uf03KPSZUmMZqZfUAK3tQwsdKjDQUDvTb-H0JyfA,1141
27
27
  airbyte_cdk/sources/abstract_source.py,sha256=GSpNwbwJ0v-KvxWa0u_nWeC0r6G2fZNkpKUhXzf6YlI,14399
28
28
  airbyte_cdk/sources/config.py,sha256=PYsY7y2u3EUwxLiEb96JnuKwH_E8CuxKggsRO2ZPSRc,856
29
- airbyte_cdk/sources/connector_state_manager.py,sha256=5E0O5yOj-4XRr7pSZF-SD1RrSvkKN1V-833HDCVBzaw,10983
29
+ airbyte_cdk/sources/connector_state_manager.py,sha256=p9iwWbb5uqRbsrHsdZBMXKmyHgLVbsOcV3QQexBFnPE,11052
30
30
  airbyte_cdk/sources/http_config.py,sha256=OBZeuyFilm6NlDlBhFQvHhTWabEvZww6OHDIlZujIS0,730
31
31
  airbyte_cdk/sources/http_logger.py,sha256=v0kkpDtA0GUOgj6_3AayrYaBrSHBqG4t3MGbrtxaNmU,1437
32
32
  airbyte_cdk/sources/source.py,sha256=dk50z8Roc28MJ8FxWe652B-GwItO__bTZqFm7WOtHnw,4412
@@ -232,7 +232,7 @@ airbyte_cdk/sources/streams/http/auth/core.py,sha256=_s9wewvvIcOgYjhHGDj_YHApnF5
232
232
  airbyte_cdk/sources/streams/http/auth/oauth.py,sha256=zchPWN1utNg02F93f5b4UFI5OXYo8-QhocbsXhLdG4U,4135
233
233
  airbyte_cdk/sources/streams/http/auth/token.py,sha256=oU1ul0LsGsPGN_vOJOKw1xX2y_XWULRxjqXu7Rivcr8,1940
234
234
  airbyte_cdk/sources/streams/http/requests_native_auth/__init__.py,sha256=RN0D3nOX1xLgwEwKWu6pkGy3XqBFzKSNZ8Lf6umU2eY,413
235
- airbyte_cdk/sources/streams/http/requests_native_auth/abstract_oauth.py,sha256=sknTSDgwPX3IhHRqEnxp9tuRE5Iu8jU_dyIKsFad1wY,9257
235
+ airbyte_cdk/sources/streams/http/requests_native_auth/abstract_oauth.py,sha256=VfI81zpWYMrzhU0nzN7J1lYz-aZCyHzvv-cLqzm8in0,10125
236
236
  airbyte_cdk/sources/streams/http/requests_native_auth/abstract_token.py,sha256=T0hVF2cBXGgIfrCslvTC1uNm9rNbYjENNl2Cb3mXuSY,961
237
237
  airbyte_cdk/sources/streams/http/requests_native_auth/oauth.py,sha256=HrnA76k4i-YOp7ygDwyMg0Hf80gjJaNSU3GbXiTzxMc,13381
238
238
  airbyte_cdk/sources/streams/http/requests_native_auth/token.py,sha256=hDti8DlF_R5YYX95hg9BPogYtG-KUYtOifrFDv_L3Hk,2456
@@ -253,12 +253,12 @@ airbyte_cdk/test/entrypoint_wrapper.py,sha256=uTOEYoWkYnbkooPJ4a4gZ-NEll5j1tTCAz
253
253
  airbyte_cdk/test/state_builder.py,sha256=SlKadhKVi38ZSKMeceVAxjowxsDDT9vJoG6gU4zDrQE,705
254
254
  airbyte_cdk/test/mock_http/__init__.py,sha256=uil6k-0NbUyDFZXtWw88HaS7r13i43VzA9H7hOHzZx8,322
255
255
  airbyte_cdk/test/mock_http/matcher.py,sha256=J4C8g8PkdKo4OwHWMJGYJIyrLnQpXI5gXWUtyxsxHpM,1240
256
- airbyte_cdk/test/mock_http/mocker.py,sha256=R-RvMufoI22TtZno8WpSIy0BVhtwoI9OvEGDL6HJU5w,6143
256
+ airbyte_cdk/test/mock_http/mocker.py,sha256=Sb1Nnf3bVEJfiy5_IliRcyIiIPQL8esSWmm5j9u0E_E,6202
257
257
  airbyte_cdk/test/mock_http/request.py,sha256=dd_i47FOGD5iRlU23daotv2gEn5NOVqTBAqykxdG6-0,3687
258
- airbyte_cdk/test/mock_http/response.py,sha256=XjkouKy3eCsylZRROzl1qZs6LfjnO4C3L1hcJoqTHOs,354
258
+ airbyte_cdk/test/mock_http/response.py,sha256=F09QGG8N3Z8fL_b0rmSKTYoKgku5yZJQCpj0Fwwxu3s,588
259
259
  airbyte_cdk/test/mock_http/response_builder.py,sha256=sc0lU_LN3wjBc4mFFV-3Y5IhYeapRdtB_-EDdHfyArA,7804
260
260
  airbyte_cdk/utils/__init__.py,sha256=qZoNqzEKhIXdN_ZfvXlIGnmiDDjCFy6BVCzzWjUZcuU,294
261
- airbyte_cdk/utils/airbyte_secrets_utils.py,sha256=q3aDl8T10ufGbeqnUPqbZLxQcHdkf2kDfQK_upWzBbI,2894
261
+ airbyte_cdk/utils/airbyte_secrets_utils.py,sha256=UIu8jzVGstjrlT8iKxvWieiO57rmxuOtx8QphHEzs9Y,3079
262
262
  airbyte_cdk/utils/analytics_message.py,sha256=om0y9U_Y1RNHREi_K8D3mkZgBNfwiOyG71ixOBKZbVs,598
263
263
  airbyte_cdk/utils/constants.py,sha256=QzCi7j5SqpI5I06uRvQ8FC73JVJi7rXaRnR3E_gro5c,108
264
264
  airbyte_cdk/utils/datetime_format_inferrer.py,sha256=gGKDQ3OdY18R5CVFhq4c7zB_E4Cxe6J6SLA29cz3cJM,3954
@@ -299,7 +299,7 @@ unit_tests/sources/declarative/test_declarative_stream.py,sha256=Tt3PBIAo7DeQgvX
299
299
  unit_tests/sources/declarative/test_manifest_declarative_source.py,sha256=HsDeDgtipkciNOnOeaM1H7eUh1Noq_OVDoFEMugm124,63391
300
300
  unit_tests/sources/declarative/test_yaml_declarative_source.py,sha256=6HhsUFgB7ueN0yOUHWb4gpPYLng5jasxN_plvz3x37g,5097
301
301
  unit_tests/sources/declarative/auth/__init__.py,sha256=4Hw-PX1-VgESLF16cDdvuYCzGJtHntThLF4qIiULWeo,61
302
- unit_tests/sources/declarative/auth/test_oauth.py,sha256=pNUzscHDR8K7tedF7v_lSMVSOEOeq9bAlxHG2cttdU0,12854
302
+ unit_tests/sources/declarative/auth/test_oauth.py,sha256=NLa7zUhN2pmFsEZFykxpzKVYuw_9luHMkQtFfxP72U4,14039
303
303
  unit_tests/sources/declarative/auth/test_selective_authenticator.py,sha256=RAolWBLCLtibul5wlteQzLAdnUF8vh893qAr9fhoYOk,1315
304
304
  unit_tests/sources/declarative/auth/test_session_token_auth.py,sha256=nKNBx7yGrrvFW9BUwG2xI472Q2sNXl2j1LhWZuUaWmY,6183
305
305
  unit_tests/sources/declarative/auth/test_token_auth.py,sha256=Kg90S04_4WjTUCzwcj62OxnF_TPQcjL_r7-BaeDhxPI,7384
@@ -445,7 +445,7 @@ unit_tests/test/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
445
445
  unit_tests/test/test_entrypoint_wrapper.py,sha256=m4csYvjO2PzvZZma7K322SBBiL5D33xuv8eUMjitDXE,10839
446
446
  unit_tests/test/mock_http/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
447
447
  unit_tests/test/mock_http/test_matcher.py,sha256=dBndYzqvo3AdHRilLrqruXdPviwi91gWt-ubDsGb-yg,2327
448
- unit_tests/test/mock_http/test_mocker.py,sha256=Zp7nhuEGWLsysNFMIcNhu5GhWrNd_JQMIXOjZHYRdY4,8409
448
+ unit_tests/test/mock_http/test_mocker.py,sha256=sOoWutnrPDKB99Y3bkEyr3HFELGivwuklVJ_ii8C-ew,8523
449
449
  unit_tests/test/mock_http/test_request.py,sha256=O9ihefGNiZKpHqsGtis6BjF8VoaOULNR8zOblVqmsL4,7602
450
450
  unit_tests/test/mock_http/test_response_builder.py,sha256=IxAww4gaOxG-9MW8kEZkRzYL2mO6xe4jIsxhi40i2ow,7878
451
451
  unit_tests/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -453,11 +453,11 @@ unit_tests/utils/test_datetime_format_inferrer.py,sha256=1EUW1_afccMDrZM6YZyyPqr
453
453
  unit_tests/utils/test_mapping_helpers.py,sha256=hqRppuban9hGKviiNFqp2fNdAz77d1_gjvgg8L7-jy8,1408
454
454
  unit_tests/utils/test_rate_limiting.py,sha256=ESrPBH61EZSeAf-hYWnd49igCgkUWnT21rUHPQaOLQM,873
455
455
  unit_tests/utils/test_schema_inferrer.py,sha256=Z2jHBZ540wnYkylIdV_2xr75Vtwlxuyg4MNPAG-xhpk,7817
456
- unit_tests/utils/test_secret_utils.py,sha256=XKe0f1RHYii8iwE6ATmBr5JGDI1pzzrnZUGdUSMJQP4,4886
456
+ unit_tests/utils/test_secret_utils.py,sha256=CdKK8A2-5XVxbXVtX22FK9dwwMeP5KNqDH6luWRXSNw,5256
457
457
  unit_tests/utils/test_stream_status_utils.py,sha256=Xr8MZ2HWgTVIyMbywDvuYkRaUF4RZLQOT8-JjvcfR24,2970
458
458
  unit_tests/utils/test_traced_exception.py,sha256=bDFP5zMBizFenz6V2WvEZTRCKGB5ijh3DBezjbfoYIs,4198
459
- airbyte_cdk-0.62.0.dist-info/LICENSE.txt,sha256=Wfe61S4BaGPj404v8lrAbvhjYR68SHlkzeYrg3_bbuM,1051
460
- airbyte_cdk-0.62.0.dist-info/METADATA,sha256=tppN9XjW0XKd-E_5J7xTL6F6RG9u6TI3HmjnYbH-klw,11073
461
- airbyte_cdk-0.62.0.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
462
- airbyte_cdk-0.62.0.dist-info/top_level.txt,sha256=edvsDKTnE6sD2wfCUaeTfKf5gQIL6CPVMwVL2sWZzqo,51
463
- airbyte_cdk-0.62.0.dist-info/RECORD,,
459
+ airbyte_cdk-0.62.2.dist-info/LICENSE.txt,sha256=Wfe61S4BaGPj404v8lrAbvhjYR68SHlkzeYrg3_bbuM,1051
460
+ airbyte_cdk-0.62.2.dist-info/METADATA,sha256=mg5FUvzFvSF_W3YQZY6V6fUcSoUy4C03oFt2hF6w0FI,11073
461
+ airbyte_cdk-0.62.2.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
462
+ airbyte_cdk-0.62.2.dist-info/top_level.txt,sha256=edvsDKTnE6sD2wfCUaeTfKf5gQIL6CPVMwVL2sWZzqo,51
463
+ airbyte_cdk-0.62.2.dist-info/RECORD,,
@@ -10,6 +10,7 @@ import pendulum
10
10
  import pytest
11
11
  import requests
12
12
  from airbyte_cdk.sources.declarative.auth import DeclarativeOauth2Authenticator
13
+ from airbyte_cdk.utils.airbyte_secrets_utils import filter_secrets
13
14
  from requests import Response
14
15
 
15
16
  LOGGER = logging.getLogger(__name__)
@@ -165,6 +166,32 @@ class TestOauth2Authenticator:
165
166
 
166
167
  assert ("access_token", 1000) == token
167
168
 
169
+ filtered = filter_secrets("access_token")
170
+ assert filtered == "****"
171
+
172
+ def test_refresh_access_token_missing_access_token(self, mocker):
173
+ oauth = DeclarativeOauth2Authenticator(
174
+ token_refresh_endpoint="{{ config['refresh_endpoint'] }}",
175
+ client_id="{{ config['client_id'] }}",
176
+ client_secret="{{ config['client_secret'] }}",
177
+ refresh_token="{{ config['refresh_token'] }}",
178
+ config=config,
179
+ scopes=["scope1", "scope2"],
180
+ token_expiry_date="{{ config['token_expiry_date'] }}",
181
+ refresh_request_body={
182
+ "custom_field": "{{ config['custom_field'] }}",
183
+ "another_field": "{{ config['another_field'] }}",
184
+ "scopes": ["no_override"],
185
+ },
186
+ parameters={},
187
+ )
188
+
189
+ resp.status_code = 200
190
+ mocker.patch.object(resp, "json", return_value={"expires_in": 1000})
191
+ mocker.patch.object(requests, "request", side_effect=mock_request, autospec=True)
192
+ with pytest.raises(Exception):
193
+ oauth.refresh_access_token()
194
+
168
195
  @pytest.mark.parametrize(
169
196
  "timestamp, expected_date",
170
197
  [
@@ -15,6 +15,7 @@ _ANOTHER_RESPONSE_BODY = "another body"
15
15
  _A_RESPONSE = HttpResponse("any response")
16
16
  _SOME_QUERY_PARAMS = {"q1": "query value"}
17
17
  _SOME_HEADERS = {"h1": "header value"}
18
+ _OTHER_HEADERS = {"h2": "another header value"}
18
19
  _SOME_REQUEST_BODY_MAPPING = {"first_field": "first_value", "second_field": 2}
19
20
  _SOME_REQUEST_BODY_STR = "some_request_body"
20
21
 
@@ -24,13 +25,14 @@ class HttpMockerTest(TestCase):
24
25
  def test_given_get_request_match_when_decorate_then_return_response(self, http_mocker):
25
26
  http_mocker.get(
26
27
  HttpRequest(_A_URL, _SOME_QUERY_PARAMS, _SOME_HEADERS),
27
- HttpResponse(_A_RESPONSE_BODY, 474),
28
+ HttpResponse(_A_RESPONSE_BODY, 474, _OTHER_HEADERS),
28
29
  )
29
30
 
30
31
  response = requests.get(_A_URL, params=_SOME_QUERY_PARAMS, headers=_SOME_HEADERS)
31
32
 
32
33
  assert response.text == _A_RESPONSE_BODY
33
34
  assert response.status_code == 474
35
+ assert response.headers == _OTHER_HEADERS
34
36
 
35
37
  @HttpMocker()
36
38
  def test_given_loose_headers_matching_when_decorate_then_match(self, http_mocker):
@@ -3,7 +3,7 @@
3
3
  #
4
4
 
5
5
  import pytest
6
- from airbyte_cdk.utils.airbyte_secrets_utils import filter_secrets, get_secret_paths, get_secrets, update_secrets
6
+ from airbyte_cdk.utils.airbyte_secrets_utils import add_to_secrets, filter_secrets, get_secret_paths, get_secrets, update_secrets
7
7
 
8
8
  SECRET_STRING_KEY = "secret_key1"
9
9
  SECRET_STRING_VALUE = "secret_value"
@@ -121,3 +121,15 @@ def test_secret_filtering():
121
121
  update_secrets([SECRET_STRING_VALUE, SECRET_STRING_2_VALUE])
122
122
  filtered = filter_secrets(sensitive_str)
123
123
  assert filtered == f"**** {NOT_SECRET_VALUE} **** ****"
124
+
125
+
126
+ def test_secrets_added_are_filtered():
127
+ ADDED_SECRET = "only_a_secret_if_added"
128
+ sensitive_str = f"{ADDED_SECRET} {NOT_SECRET_VALUE}"
129
+
130
+ filtered = filter_secrets(sensitive_str)
131
+ assert filtered == sensitive_str
132
+
133
+ add_to_secrets(ADDED_SECRET)
134
+ filtered = filter_secrets(sensitive_str)
135
+ assert filtered == f"**** {NOT_SECRET_VALUE}"