airbyte-cdk 0.39.2__py3-none-any.whl → 0.39.4__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
@@ -16,11 +16,13 @@ from airbyte_cdk.sources.declarative.declarative_source import DeclarativeSource
16
16
  from airbyte_cdk.utils import AirbyteTracedException
17
17
  from airbyte_cdk.utils.schema_inferrer import SchemaInferrer
18
18
  from airbyte_protocol.models.airbyte_protocol import (
19
+ AirbyteControlMessage,
19
20
  AirbyteLogMessage,
20
21
  AirbyteMessage,
21
22
  AirbyteTraceMessage,
22
23
  ConfiguredAirbyteCatalog,
23
24
  Level,
25
+ OrchestratorType,
24
26
  TraceType,
25
27
  )
26
28
  from airbyte_protocol.models.airbyte_protocol import Type as MessageType
@@ -52,6 +54,7 @@ class MessageGrouper:
52
54
 
53
55
  slices = []
54
56
  log_messages = []
57
+ latest_config_update: AirbyteControlMessage = None
55
58
  for message_group in self._get_message_groups(
56
59
  self._read_stream(source, config, configured_catalog),
57
60
  schema_inferrer,
@@ -63,7 +66,9 @@ class MessageGrouper:
63
66
  if message_group.type == TraceType.ERROR:
64
67
  error_message = f"{message_group.error.message} - {message_group.error.stack_trace}"
65
68
  log_messages.append(LogMessage(**{"message": error_message, "level": "ERROR"}))
66
-
69
+ elif isinstance(message_group, AirbyteControlMessage):
70
+ if not latest_config_update or latest_config_update.emitted_at <= message_group.emitted_at:
71
+ latest_config_update = message_group
67
72
  else:
68
73
  slices.append(message_group)
69
74
 
@@ -74,11 +79,12 @@ class MessageGrouper:
74
79
  inferred_schema=schema_inferrer.get_stream_schema(
75
80
  configured_catalog.streams[0].stream.name
76
81
  ), # The connector builder currently only supports reading from a single stream at a time
82
+ latest_config_update=latest_config_update.connectorConfig.config if latest_config_update else self._clean_config(config),
77
83
  )
78
84
 
79
85
  def _get_message_groups(
80
86
  self, messages: Iterator[AirbyteMessage], schema_inferrer: SchemaInferrer, limit: int
81
- ) -> Iterable[Union[StreamReadPages, AirbyteLogMessage, AirbyteTraceMessage]]:
87
+ ) -> Iterable[Union[StreamReadPages, AirbyteControlMessage, AirbyteLogMessage, AirbyteTraceMessage]]:
82
88
  """
83
89
  Message groups are partitioned according to when request log messages are received. Subsequent response log messages
84
90
  and record messages belong to the prior request log message and when we encounter another request, append the latest
@@ -135,6 +141,8 @@ class MessageGrouper:
135
141
  current_page_records.append(message.record.data)
136
142
  records_count += 1
137
143
  schema_inferrer.accumulate(message.record)
144
+ elif message.type == MessageType.CONTROL and message.control.type == OrchestratorType.CONNECTOR_CONFIG:
145
+ yield message.control
138
146
  else:
139
147
  self._close_page(current_page_request, current_page_response, current_slice_pages, current_page_records, validate_page_complete=not had_error)
140
148
  yield StreamReadSlices(pages=current_slice_pages, slice_descriptor=current_slice_descriptor)
@@ -217,20 +225,10 @@ class MessageGrouper:
217
225
  def _parse_slice_description(self, log_message):
218
226
  return json.loads(log_message.replace(AbstractSource.SLICE_LOG_PREFIX, "", 1))
219
227
 
220
- @classmethod
221
- def _create_configure_catalog(cls, stream_name: str) -> ConfiguredAirbyteCatalog:
222
- return ConfiguredAirbyteCatalog.parse_obj(
223
- {
224
- "streams": [
225
- {
226
- "stream": {
227
- "name": stream_name,
228
- "json_schema": {},
229
- "supported_sync_modes": ["full_refresh", "incremental"],
230
- },
231
- "sync_mode": "full_refresh",
232
- "destination_sync_mode": "overwrite",
233
- }
234
- ]
235
- }
236
- )
228
+ @staticmethod
229
+ def _clean_config(config: Mapping[str, Any]):
230
+ cleaned_config = deepcopy(config)
231
+ for key in config.keys():
232
+ if key.startswith("__"):
233
+ del cleaned_config[key]
234
+ return cleaned_config
@@ -48,6 +48,7 @@ class StreamRead(object):
48
48
  slices: List[StreamReadSlices]
49
49
  test_read_limit_reached: bool
50
50
  inferred_schema: Optional[Dict[str, Any]]
51
+ latest_config_update: Optional[Dict[str, Any]]
51
52
 
52
53
 
53
54
  @dataclass
@@ -10,6 +10,7 @@ from airbyte_cdk.sources.declarative.auth.declarative_authenticator import Decla
10
10
  from airbyte_cdk.sources.declarative.interpolation.interpolated_mapping import InterpolatedMapping
11
11
  from airbyte_cdk.sources.declarative.interpolation.interpolated_string import InterpolatedString
12
12
  from airbyte_cdk.sources.streams.http.requests_native_auth.abstract_oauth import AbstractOauth2Authenticator
13
+ from airbyte_cdk.sources.streams.http.requests_native_auth.oauth import SingleUseRefreshTokenOauth2Authenticator
13
14
 
14
15
 
15
16
  @dataclass
@@ -133,3 +134,13 @@ class DeclarativeOauth2Authenticator(AbstractOauth2Authenticator, DeclarativeAut
133
134
  @access_token.setter
134
135
  def access_token(self, value: str):
135
136
  self._access_token = value
137
+
138
+
139
+ @dataclass
140
+ class DeclarativeSingleUseRefreshTokenOauth2Authenticator(SingleUseRefreshTokenOauth2Authenticator, DeclarativeAuthenticator):
141
+ """
142
+ Declarative version of SingleUseRefreshTokenOauth2Authenticator which can be used in declarative connectors.
143
+ """
144
+
145
+ def __init__(self, *args, **kwargs):
146
+ super().__init__(*args, **kwargs)
@@ -733,6 +733,46 @@ definitions:
733
733
  type: string
734
734
  examples:
735
735
  - "%Y-%m-%d %H:%M:%S.%f+00:00"
736
+ refresh_token_updater:
737
+ title: Token Updater
738
+ description: When the token updater is defined, new refresh tokens, access tokens and the access token expiry date are written back from the authentication response to the config object. This is important if the refresh token can only used once.
739
+ properties:
740
+ refresh_token_name:
741
+ title: Refresh Token Property Name
742
+ description: The name of the property which contains the updated refresh token in the response from the token refresh endpoint.
743
+ type: string
744
+ default: "refresh_token"
745
+ examples:
746
+ - "refresh_token"
747
+ access_token_config_path:
748
+ title: Config Path To Access Token
749
+ description: Config path to the access token. Make sure the field actually exists in the config.
750
+ type: array
751
+ items:
752
+ type: string
753
+ default: ["credentials", "access_token"]
754
+ examples:
755
+ - ["credentials", "access_token"]
756
+ - ["access_token"]
757
+ refresh_token_config_path:
758
+ title: Config Path To Refresh Token
759
+ description: Config path to the access token. Make sure the field actually exists in the config.
760
+ type: array
761
+ items:
762
+ type: string
763
+ default: ["credentials", "refresh_token"]
764
+ examples:
765
+ - ["credentials", "refresh_token"]
766
+ - ["refresh_token"]
767
+ token_expiry_date_config_path:
768
+ title: Config Path To Expiry Date
769
+ description: Config path to the expiry date. Make sure actually exists in the config.
770
+ type: array
771
+ items:
772
+ type: string
773
+ default: ["credentials", "token_expiry_date"]
774
+ examples:
775
+ - ["credentials", "token_expiry_date"]
736
776
  $parameters:
737
777
  type: object
738
778
  additionalProperties: true
@@ -260,6 +260,33 @@ class CustomTransformation(BaseModel):
260
260
  parameters: Optional[Dict[str, Any]] = Field(None, alias="$parameters")
261
261
 
262
262
 
263
+ class RefreshTokenUpdater(BaseModel):
264
+ refresh_token_name: Optional[str] = Field(
265
+ "refresh_token",
266
+ description="The name of the property which contains the updated refresh token in the response from the token refresh endpoint.",
267
+ examples=["refresh_token"],
268
+ title="Refresh Token Property Name",
269
+ )
270
+ access_token_config_path: Optional[List[str]] = Field(
271
+ ["credentials", "access_token"],
272
+ description="Config path to the access token. Make sure the field actually exists in the config.",
273
+ examples=[["credentials", "access_token"], ["access_token"]],
274
+ title="Config Path To Access Token",
275
+ )
276
+ refresh_token_config_path: Optional[List[str]] = Field(
277
+ ["credentials", "refresh_token"],
278
+ description="Config path to the access token. Make sure the field actually exists in the config.",
279
+ examples=[["credentials", "refresh_token"], ["refresh_token"]],
280
+ title="Config Path To Refresh Token",
281
+ )
282
+ token_expiry_date_config_path: Optional[List[str]] = Field(
283
+ ["credentials", "token_expiry_date"],
284
+ description="Config path to the expiry date. Make sure actually exists in the config.",
285
+ examples=[["credentials", "token_expiry_date"]],
286
+ title="Config Path To Expiry Date",
287
+ )
288
+
289
+
263
290
  class OAuthAuthenticator(BaseModel):
264
291
  type: Literal["OAuthAuthenticator"]
265
292
  client_id: str = Field(
@@ -340,6 +367,11 @@ class OAuthAuthenticator(BaseModel):
340
367
  examples=["%Y-%m-%d %H:%M:%S.%f+00:00"],
341
368
  title="Token Expiry Date Format",
342
369
  )
370
+ refresh_token_updater: Optional[RefreshTokenUpdater] = Field(
371
+ None,
372
+ description="When the token updater is defined, new refresh tokens, access tokens and the access token expiry date are written back from the authentication response to the config object. This is important if the refresh token can only used once.",
373
+ title="Token Updater",
374
+ )
343
375
  parameters: Optional[Dict[str, Any]] = Field(None, alias="$parameters")
344
376
 
345
377
 
@@ -9,8 +9,10 @@ import inspect
9
9
  import re
10
10
  from typing import Any, Callable, List, Literal, Mapping, Optional, Type, Union, get_args, get_origin, get_type_hints
11
11
 
12
+ import dpath
12
13
  from airbyte_cdk.sources.declarative.auth import DeclarativeOauth2Authenticator
13
14
  from airbyte_cdk.sources.declarative.auth.declarative_authenticator import NoAuth
15
+ from airbyte_cdk.sources.declarative.auth.oauth import DeclarativeSingleUseRefreshTokenOauth2Authenticator
14
16
  from airbyte_cdk.sources.declarative.auth.token import (
15
17
  ApiKeyAuthenticator,
16
18
  BasicHttpAuthenticator,
@@ -24,6 +26,7 @@ from airbyte_cdk.sources.declarative.decoders import JsonDecoder
24
26
  from airbyte_cdk.sources.declarative.extractors import DpathExtractor, RecordFilter, RecordSelector
25
27
  from airbyte_cdk.sources.declarative.incremental import DatetimeBasedCursor
26
28
  from airbyte_cdk.sources.declarative.interpolation import InterpolatedString
29
+ from airbyte_cdk.sources.declarative.interpolation.interpolated_mapping import InterpolatedMapping
27
30
  from airbyte_cdk.sources.declarative.models.declarative_component_schema import AddedFieldDefinition as AddedFieldDefinitionModel
28
31
  from airbyte_cdk.sources.declarative.models.declarative_component_schema import AddFields as AddFieldsModel
29
32
  from airbyte_cdk.sources.declarative.models.declarative_component_schema import ApiKeyAuthenticator as ApiKeyAuthenticatorModel
@@ -659,6 +662,23 @@ class ModelToComponentFactory:
659
662
 
660
663
  @staticmethod
661
664
  def create_oauth_authenticator(model: OAuthAuthenticatorModel, config: Config, **kwargs) -> DeclarativeOauth2Authenticator:
665
+ if model.refresh_token_updater:
666
+ return DeclarativeSingleUseRefreshTokenOauth2Authenticator(
667
+ config,
668
+ InterpolatedString.create(model.token_refresh_endpoint, parameters=model.parameters).eval(config),
669
+ access_token_name=InterpolatedString.create(model.access_token_name, parameters=model.parameters).eval(config),
670
+ refresh_token_name=model.refresh_token_updater.refresh_token_name,
671
+ expires_in_name=InterpolatedString.create(model.expires_in_name, parameters=model.parameters).eval(config),
672
+ client_id=InterpolatedString.create(model.client_id, parameters=model.parameters).eval(config),
673
+ client_secret=InterpolatedString.create(model.client_secret, parameters=model.parameters).eval(config),
674
+ access_token_config_path=model.refresh_token_updater.access_token_config_path,
675
+ refresh_token_config_path=model.refresh_token_updater.refresh_token_config_path,
676
+ token_expiry_date_config_path=model.refresh_token_updater.token_expiry_date_config_path,
677
+ grant_type=InterpolatedString.create(model.grant_type, parameters=model.parameters).eval(config),
678
+ refresh_request_body=InterpolatedMapping(model.refresh_request_body or {}, parameters=model.parameters).eval(config),
679
+ scopes=model.scopes,
680
+ token_expiry_date_format=model.token_expiry_date_format,
681
+ )
662
682
  return DeclarativeOauth2Authenticator(
663
683
  access_token_name=model.access_token_name,
664
684
  client_id=model.client_id,
@@ -685,8 +705,8 @@ class ModelToComponentFactory:
685
705
  access_token_name=model.access_token_name,
686
706
  refresh_token_name=model.refresh_token_name,
687
707
  expires_in_name=model.expires_in_name,
688
- client_id_config_path=model.client_id_config_path,
689
- client_secret_config_path=model.client_secret_config_path,
708
+ client_id=dpath.util.get(config, model.client_id_config_path),
709
+ client_secret=dpath.util.get(config, model.client_secret_config_path),
690
710
  access_token_config_path=model.access_token_config_path,
691
711
  refresh_token_config_path=model.refresh_token_config_path,
692
712
  token_expiry_date_config_path=model.token_expiry_date_config_path,
@@ -2,7 +2,7 @@
2
2
  # Copyright (c) 2023 Airbyte, Inc., all rights reserved.
3
3
  #
4
4
 
5
- from typing import Any, List, Mapping, Sequence, Tuple, Union
5
+ from typing import Any, List, Mapping, Optional, Sequence, Tuple, Union
6
6
 
7
7
  import dpath
8
8
  import pendulum
@@ -109,11 +109,12 @@ class SingleUseRefreshTokenOauth2Authenticator(Oauth2Authenticator):
109
109
  refresh_token_name: str = "refresh_token",
110
110
  refresh_request_body: Mapping[str, Any] = None,
111
111
  grant_type: str = "refresh_token",
112
- client_id_config_path: Sequence[str] = ("credentials", "client_id"),
113
- client_secret_config_path: Sequence[str] = ("credentials", "client_secret"),
112
+ client_id: Optional[str] = None,
113
+ client_secret: Optional[str] = None,
114
114
  access_token_config_path: Sequence[str] = ("credentials", "access_token"),
115
115
  refresh_token_config_path: Sequence[str] = ("credentials", "refresh_token"),
116
116
  token_expiry_date_config_path: Sequence[str] = ("credentials", "token_expiry_date"),
117
+ token_expiry_date_format: Optional[str] = None,
117
118
  ):
118
119
  """
119
120
 
@@ -126,20 +127,23 @@ class SingleUseRefreshTokenOauth2Authenticator(Oauth2Authenticator):
126
127
  refresh_token_name (str, optional): Name of the name of the refresh token field, used to parse the refresh token response. Defaults to "refresh_token".
127
128
  refresh_request_body (Mapping[str, Any], optional): Custom key value pair that will be added to the refresh token request body. Defaults to None.
128
129
  grant_type (str, optional): OAuth grant type. Defaults to "refresh_token".
129
- client_id_config_path (Sequence[str]): Dpath to the client_id field in the connector configuration. Defaults to ("credentials", "client_id").
130
- client_secret_config_path (Sequence[str]): Dpath to the client_secret field in the connector configuration. Defaults to ("credentials", "client_secret").
130
+ client_id (Optional[str]): The client id to authenticate. If not specified, defaults to credentials.client_id in the config object.
131
+ client_secret (Optional[str]): The client secret to authenticate. If not specified, defaults to credentials.client_secret in the config object.
131
132
  access_token_config_path (Sequence[str]): Dpath to the access_token field in the connector configuration. Defaults to ("credentials", "access_token").
132
133
  refresh_token_config_path (Sequence[str]): Dpath to the refresh_token field in the connector configuration. Defaults to ("credentials", "refresh_token").
133
134
  token_expiry_date_config_path (Sequence[str]): Dpath to the token_expiry_date field in the connector configuration. Defaults to ("credentials", "token_expiry_date").
135
+ token_expiry_date_format (Optional[str]): Date format of the token expiry date field (set by expires_in_name). If not specified the token expiry date is interpreted as number of seconds until expiration.
134
136
  """
135
- self._client_id_config_path = client_id_config_path
136
- self._client_secret_config_path = client_secret_config_path
137
+ self._client_id = client_id if client_id is not None else dpath.util.get(connector_config, ("credentials", "client_id"))
138
+ self._client_secret = (
139
+ client_secret if client_secret is not None else dpath.util.get(connector_config, ("credentials", "client_secret"))
140
+ )
137
141
  self._access_token_config_path = access_token_config_path
138
142
  self._refresh_token_config_path = refresh_token_config_path
139
143
  self._token_expiry_date_config_path = token_expiry_date_config_path
144
+ self._token_expiry_date_format = token_expiry_date_format
140
145
  self._refresh_token_name = refresh_token_name
141
146
  self._connector_config = connector_config
142
- self._validate_connector_config()
143
147
  super().__init__(
144
148
  token_refresh_endpoint,
145
149
  self.get_client_id(),
@@ -151,69 +155,49 @@ class SingleUseRefreshTokenOauth2Authenticator(Oauth2Authenticator):
151
155
  expires_in_name=expires_in_name,
152
156
  refresh_request_body=refresh_request_body,
153
157
  grant_type=grant_type,
158
+ token_expiry_date_format=token_expiry_date_format,
154
159
  )
155
160
 
156
- def _validate_connector_config(self):
157
- """Validates the defined getters for configuration values are returning values.
158
-
159
- Raises:
160
- ValueError: Raised if the defined getters are not returning a value.
161
- """
162
- try:
163
- assert self.access_token
164
- except KeyError:
165
- raise ValueError(
166
- f"This authenticator expects a value under the {self._access_token_config_path} field path. Please check your configuration structure or change the access_token_config_path value at initialization of this authenticator."
167
- )
168
- for field_path, getter, parameter_name in [
169
- (self._client_id_config_path, self.get_client_id, "client_id_config_path"),
170
- (self._client_secret_config_path, self.get_client_secret, "client_secret_config_path"),
171
- (self._refresh_token_config_path, self.get_refresh_token, "refresh_token_config_path"),
172
- (self._token_expiry_date_config_path, self.get_token_expiry_date, "token_expiry_date_config_path"),
173
- ]:
174
- try:
175
- assert getter()
176
- except KeyError:
177
- raise ValueError(
178
- f"This authenticator expects a value under the {field_path} field path. Please check your configuration structure or change the {parameter_name} value at initialization of this authenticator."
179
- )
180
-
181
161
  def get_refresh_token_name(self) -> str:
182
162
  return self._refresh_token_name
183
163
 
184
164
  def get_client_id(self) -> str:
185
- return dpath.util.get(self._connector_config, self._client_id_config_path)
165
+ return self._client_id
186
166
 
187
167
  def get_client_secret(self) -> str:
188
- return dpath.util.get(self._connector_config, self._client_secret_config_path)
168
+ return self._client_secret
189
169
 
190
170
  @property
191
171
  def access_token(self) -> str:
192
- return dpath.util.get(self._connector_config, self._access_token_config_path)
172
+ return dpath.util.get(self._connector_config, self._access_token_config_path, default="")
193
173
 
194
174
  @access_token.setter
195
175
  def access_token(self, new_access_token: str):
196
- dpath.util.set(self._connector_config, self._access_token_config_path, new_access_token)
176
+ dpath.util.new(self._connector_config, self._access_token_config_path, new_access_token)
197
177
 
198
178
  def get_refresh_token(self) -> str:
199
- return dpath.util.get(self._connector_config, self._refresh_token_config_path)
179
+ return dpath.util.get(self._connector_config, self._refresh_token_config_path, default="")
200
180
 
201
181
  def set_refresh_token(self, new_refresh_token: str):
202
- dpath.util.set(self._connector_config, self._refresh_token_config_path, new_refresh_token)
182
+ dpath.util.new(self._connector_config, self._refresh_token_config_path, new_refresh_token)
203
183
 
204
184
  def get_token_expiry_date(self) -> pendulum.DateTime:
205
- return pendulum.parse(dpath.util.get(self._connector_config, self._token_expiry_date_config_path))
185
+ expiry_date = dpath.util.get(self._connector_config, self._token_expiry_date_config_path, default="")
186
+ return pendulum.now().subtract(days=1) if expiry_date == "" else pendulum.parse(expiry_date)
206
187
 
207
188
  def set_token_expiry_date(self, new_token_expiry_date):
208
- dpath.util.set(self._connector_config, self._token_expiry_date_config_path, str(new_token_expiry_date))
189
+ dpath.util.new(self._connector_config, self._token_expiry_date_config_path, str(new_token_expiry_date))
209
190
 
210
191
  def token_has_expired(self) -> bool:
211
192
  """Returns True if the token is expired"""
212
193
  return pendulum.now("UTC") > self.get_token_expiry_date()
213
194
 
214
195
  @staticmethod
215
- def get_new_token_expiry_date(access_token_expires_in: int):
216
- return pendulum.now("UTC").add(seconds=access_token_expires_in)
196
+ def get_new_token_expiry_date(access_token_expires_in: str, token_expiry_date_format: str = None) -> pendulum.DateTime:
197
+ if token_expiry_date_format:
198
+ return pendulum.from_format(access_token_expires_in, token_expiry_date_format)
199
+ else:
200
+ return pendulum.now("UTC").add(seconds=int(access_token_expires_in))
217
201
 
218
202
  def get_access_token(self) -> str:
219
203
  """Retrieve new access and refresh token if the access token has expired.
@@ -223,17 +207,17 @@ class SingleUseRefreshTokenOauth2Authenticator(Oauth2Authenticator):
223
207
  """
224
208
  if self.token_has_expired():
225
209
  new_access_token, access_token_expires_in, new_refresh_token = self.refresh_access_token()
226
- new_token_expiry_date = self.get_new_token_expiry_date(access_token_expires_in)
210
+ new_token_expiry_date = self.get_new_token_expiry_date(access_token_expires_in, self._token_expiry_date_format)
227
211
  self.access_token = new_access_token
228
212
  self.set_refresh_token(new_refresh_token)
229
213
  self.set_token_expiry_date(new_token_expiry_date)
230
214
  emit_configuration_as_airbyte_control_message(self._connector_config)
231
215
  return self.access_token
232
216
 
233
- def refresh_access_token(self) -> Tuple[str, int, str]:
217
+ def refresh_access_token(self) -> Tuple[str, str, str]:
234
218
  response_json = self._get_refresh_access_token_response()
235
219
  return (
236
220
  response_json[self.get_access_token_name()],
237
- int(response_json[self.get_expires_in_name()]),
221
+ response_json[self.get_expires_in_name()],
238
222
  response_json[self.get_refresh_token_name()],
239
223
  )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: airbyte-cdk
3
- Version: 0.39.2
3
+ Version: 0.39.4
4
4
  Summary: A framework for writing Airbyte Connectors.
5
5
  Home-page: https://github.com/airbytehq/airbyte
6
6
  Author: Airbyte
@@ -8,8 +8,8 @@ airbyte_cdk/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
8
  airbyte_cdk/connector_builder/__init__.py,sha256=4Hw-PX1-VgESLF16cDdvuYCzGJtHntThLF4qIiULWeo,61
9
9
  airbyte_cdk/connector_builder/connector_builder_handler.py,sha256=q8mqQjNqpvHZgwVbNuvSe19o4Aw6MQTuhA2URmdz0K0,5443
10
10
  airbyte_cdk/connector_builder/main.py,sha256=jn2gqaYAvd6uDoFe0oVhnY23grm5sL-jfIX6kGvhVxk,2994
11
- airbyte_cdk/connector_builder/message_grouper.py,sha256=BcQxrTmeRrF6phR2NcfapgjyPWYRBWGDI5cWuLZSsPg,11851
12
- airbyte_cdk/connector_builder/models.py,sha256=pdS057gg7N8kp-7LP5BIyC2chNktUhAzpnJLNksuQ3w,1170
11
+ airbyte_cdk/connector_builder/message_grouper.py,sha256=uJGOBhinvbisgAa-bQN3XE2L2xFTeVeykLwDCRYcxgc,12110
12
+ airbyte_cdk/connector_builder/models.py,sha256=yW_j91B-3FYNTNbWjR2ZVYTXBHlskT55uxdAqg7FhAE,1221
13
13
  airbyte_cdk/destinations/__init__.py,sha256=0Uxmz3iBAyZJdk_bqUVt2pb0UwRTpFjTnFE6fQFbWKY,126
14
14
  airbyte_cdk/destinations/destination.py,sha256=_tIMnKcRQbtIsjVvNOVjfbIxgCNLuBXQwQj8MyVm3BI,5420
15
15
  airbyte_cdk/models/__init__.py,sha256=LPQcYdDPwrCXiBPe_jexO4UAcbovIb1V9tHB6I7Un30,633
@@ -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=Kc9_ANsBCScguAibWeL4nWC6IFrmjCTk1xxgl33qKbY,75787
25
+ airbyte_cdk/sources/declarative/declarative_component_schema.yaml,sha256=cqLvo9fbE0Hk4qgjbZJHG04Not67ibWIaimIKNdVXhg,77681
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=pD0KnWA8kECcm1wMHsIrLJzUT9sil0iZu1ZUTn-los0,7403
34
+ airbyte_cdk/sources/declarative/auth/oauth.py,sha256=TseI38UgBme6FcXJuYhlzZs-eleY1KzUmG2K9oVXDmQ,7869
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
@@ -60,14 +60,14 @@ airbyte_cdk/sources/declarative/interpolation/interpolation.py,sha256=dyIM-bzh54
60
60
  airbyte_cdk/sources/declarative/interpolation/jinja.py,sha256=Dc0F87nElWsz_Ikj938eQ9uqZvyqgFhZ8Dqf_-hvndc,4800
61
61
  airbyte_cdk/sources/declarative/interpolation/macros.py,sha256=V6WGKJ9cXX1rjuM4bK3Cs9xEryMlkY2U3FMsSBhrgC8,3098
62
62
  airbyte_cdk/sources/declarative/models/__init__.py,sha256=EiYnzwCHZV7EYqMJqcy6xKSeHvTKZBsQndjbEwmiTW4,93
63
- airbyte_cdk/sources/declarative/models/declarative_component_schema.py,sha256=5ayQWOLkhzUoxGfu4NAIoB6XouAEhgwto4PfvUFyHJ4,51239
63
+ airbyte_cdk/sources/declarative/models/declarative_component_schema.py,sha256=CoLRPXA2vVa03QyPJnDNNjb6xgt7BMRHLozySPmtABg,52923
64
64
  airbyte_cdk/sources/declarative/parsers/__init__.py,sha256=ZnqYNxHsKCgO38IwB34RQyRMXTs4GTvlRi3ImKnIioo,61
65
65
  airbyte_cdk/sources/declarative/parsers/class_types_registry.py,sha256=bK4a74opm6WHyV7HqOVws6GE5Z7cLNc5MaTha69abIQ,6086
66
66
  airbyte_cdk/sources/declarative/parsers/custom_exceptions.py,sha256=y7_G5mM07zxT5YG975kdC2PAja-Uc83pYp8WrV3GNdo,522
67
67
  airbyte_cdk/sources/declarative/parsers/default_implementation_registry.py,sha256=W8BcK4KOg4ifNXgsdeIoV4oneHjXBKcPHEZHIC4r-hM,3801
68
68
  airbyte_cdk/sources/declarative/parsers/manifest_component_transformer.py,sha256=H23H3nURCxsvjq66Gn9naffp0HJ1fU03wLFu-5F0AhQ,7701
69
69
  airbyte_cdk/sources/declarative/parsers/manifest_reference_resolver.py,sha256=6ukHx0bBrCJm9rek1l_MEfS3U_gdJcM4pJRyifJEOp0,6412
70
- airbyte_cdk/sources/declarative/parsers/model_to_component_factory.py,sha256=AzSjGeLahAA1FWvzbjIOk4iLiwHcaeS-tUhjs7bnyBk,46588
70
+ airbyte_cdk/sources/declarative/parsers/model_to_component_factory.py,sha256=1QIRvvw_a1qrpCBRyXyvEXtAiUQheuxAu1i3MXkqLkg,48318
71
71
  airbyte_cdk/sources/declarative/partition_routers/__init__.py,sha256=27sOWhw2LBQs62HchURakHQ2M_mtnOatNgU6q8RUtpU,476
72
72
  airbyte_cdk/sources/declarative/partition_routers/list_partition_router.py,sha256=fa6VtTwSoIkDI3SBoRtVx79opVtJX80_gU9bt31lspc,4785
73
73
  airbyte_cdk/sources/declarative/partition_routers/single_partition_router.py,sha256=Fi3ocNZZoYkr0uvRgwoVSqne6enxRvi8DOHrASVK2PQ,1851
@@ -143,7 +143,7 @@ airbyte_cdk/sources/streams/http/auth/token.py,sha256=oU1ul0LsGsPGN_vOJOKw1xX2y_
143
143
  airbyte_cdk/sources/streams/http/requests_native_auth/__init__.py,sha256=RN0D3nOX1xLgwEwKWu6pkGy3XqBFzKSNZ8Lf6umU2eY,413
144
144
  airbyte_cdk/sources/streams/http/requests_native_auth/abstract_oauth.py,sha256=dw9mmIOf05NDqKzzvRA3tXKjx1LvVGm1tPt8TQhf5Y8,5339
145
145
  airbyte_cdk/sources/streams/http/requests_native_auth/abstract_token.py,sha256=T0hVF2cBXGgIfrCslvTC1uNm9rNbYjENNl2Cb3mXuSY,961
146
- airbyte_cdk/sources/streams/http/requests_native_auth/oauth.py,sha256=QWTjL6blaEAK457TSJlTDcczITdAu0RqMEhxX-rpAWo,11704
146
+ airbyte_cdk/sources/streams/http/requests_native_auth/oauth.py,sha256=Y94eU0Ad8tEnCurW-_vrrAnbbCc0Mo5W38aigr85oEw,11005
147
147
  airbyte_cdk/sources/streams/http/requests_native_auth/token.py,sha256=hDti8DlF_R5YYX95hg9BPogYtG-KUYtOifrFDv_L3Hk,2456
148
148
  airbyte_cdk/sources/streams/utils/__init__.py,sha256=4Hw-PX1-VgESLF16cDdvuYCzGJtHntThLF4qIiULWeo,61
149
149
  airbyte_cdk/sources/streams/utils/stream_helper.py,sha256=8n1e27DqELN_KRXuWW1IE3ZjE9zvhclNqsKtOosI_Ds,1480
@@ -163,8 +163,8 @@ airbyte_cdk/utils/traced_exception.py,sha256=9G2sG9eYkvn6Aa7rMuUW_KIRszRaTc_xdnT
163
163
  source_declarative_manifest/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
164
164
  source_declarative_manifest/main.py,sha256=HXzuRsRyhHwPrGU-hc4S7RrgoOoHImqkdfbmO2geBeE,1027
165
165
  unit_tests/connector_builder/__init__.py,sha256=4Hw-PX1-VgESLF16cDdvuYCzGJtHntThLF4qIiULWeo,61
166
- unit_tests/connector_builder/test_connector_builder_handler.py,sha256=iTUz7Iw_eT2yPEwzaoEQxWx92irgsaSuTATsNVw6ym8,26757
167
- unit_tests/connector_builder/test_message_grouper.py,sha256=VNbvRn3U3Ltsh9TyXFQ74Aw4dN1XbNpSWsx4FsW1r3c,24361
166
+ unit_tests/connector_builder/test_connector_builder_handler.py,sha256=V9p7AFECaLqSK-iGvu0OqwV6qREQC2BhWo0H4OoiiK4,26895
167
+ unit_tests/connector_builder/test_message_grouper.py,sha256=XMVRW45RDTgy1YVzkV-jOXj7Ar2mzgDV8OW2QDzZjYU,28510
168
168
  unit_tests/connector_builder/utils.py,sha256=AAggdGWP-mNuWOZUHLAVIbjTeIcdPo-3pbMm5zdYpS0,796
169
169
  unit_tests/destinations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
170
170
  unit_tests/destinations/test_destination.py,sha256=koG_j812KMkcIxoUH6XlAL3zsephZJmlHvyzJXm0dCs,10269
@@ -207,7 +207,7 @@ unit_tests/sources/declarative/interpolation/test_macros.py,sha256=q6kNuNfsrpupm
207
207
  unit_tests/sources/declarative/parsers/__init__.py,sha256=ZnqYNxHsKCgO38IwB34RQyRMXTs4GTvlRi3ImKnIioo,61
208
208
  unit_tests/sources/declarative/parsers/test_manifest_component_transformer.py,sha256=5lHUFv2n32b6h5IRh65S7EfqPkP5-IrGE3VUxDoPflI,12483
209
209
  unit_tests/sources/declarative/parsers/test_manifest_reference_resolver.py,sha256=K3q9eyx-sJFQ8nGYjAgS7fxau4sX_FlNreEAjiCYOeE,5306
210
- unit_tests/sources/declarative/parsers/test_model_to_component_factory.py,sha256=LEP-hx1NO2DR477vKcoVlVMJCQd8GGDFx5rAwiOLwqs,58815
210
+ unit_tests/sources/declarative/parsers/test_model_to_component_factory.py,sha256=utXKxTtm9Qaf3toJk6rhK98ywG-jhTDqGYvKFuikHO8,60926
211
211
  unit_tests/sources/declarative/parsers/testing_components.py,sha256=_yUijmYRM-yYHPGDB2JsfEiOuVrgexGW9QwHf1xxNW8,1326
212
212
  unit_tests/sources/declarative/partition_routers/__init__.py,sha256=O8MZg4Bv_DghdRy9BoJCPIqdV75VtiUrhEkExQgb2nE,61
213
213
  unit_tests/sources/declarative/partition_routers/test_list_partition_router.py,sha256=gyivHDJ7iA6dslrI886GQnT_if0BfGmLPfiviVGiSqo,5118
@@ -256,14 +256,14 @@ unit_tests/sources/streams/http/test_http.py,sha256=H0lGcb0XHuM1R7GC3wAaaxhGoNwi
256
256
  unit_tests/sources/streams/http/auth/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
257
257
  unit_tests/sources/streams/http/auth/test_auth.py,sha256=gdWpJ-cR64qRXmmPOQWhVd4E6ekXyJEIEfJxA0jlDvc,6546
258
258
  unit_tests/sources/streams/http/requests_native_auth/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
259
- unit_tests/sources/streams/http/requests_native_auth/test_requests_native_auth.py,sha256=0IXXpP6dbc0ainWZbro7ODfC2RsqK8XcCyo9-YgG7Z4,12201
259
+ unit_tests/sources/streams/http/requests_native_auth/test_requests_native_auth.py,sha256=_BZVsG_LZUXfBmHWTlKIw65eGkdwFSiKRlpjsccj61U,12396
260
260
  unit_tests/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
261
261
  unit_tests/utils/test_schema_inferrer.py,sha256=Z2jHBZ540wnYkylIdV_2xr75Vtwlxuyg4MNPAG-xhpk,7817
262
262
  unit_tests/utils/test_secret_utils.py,sha256=XKe0f1RHYii8iwE6ATmBr5JGDI1pzzrnZUGdUSMJQP4,4886
263
263
  unit_tests/utils/test_stream_status_utils.py,sha256=NpV155JMXA6CG-2Zvofa14lItobyh3Onttc59X4m5DI,3382
264
264
  unit_tests/utils/test_traced_exception.py,sha256=bDFP5zMBizFenz6V2WvEZTRCKGB5ijh3DBezjbfoYIs,4198
265
- airbyte_cdk-0.39.2.dist-info/LICENSE.txt,sha256=Wfe61S4BaGPj404v8lrAbvhjYR68SHlkzeYrg3_bbuM,1051
266
- airbyte_cdk-0.39.2.dist-info/METADATA,sha256=0MNWbqgV0bELDgVFUj3JHbQkulzWpDPSQw9COqwkhOc,8902
267
- airbyte_cdk-0.39.2.dist-info/WHEEL,sha256=pkctZYzUS4AYVn6dJ-7367OJZivF2e8RA9b_ZBjif18,92
268
- airbyte_cdk-0.39.2.dist-info/top_level.txt,sha256=edvsDKTnE6sD2wfCUaeTfKf5gQIL6CPVMwVL2sWZzqo,51
269
- airbyte_cdk-0.39.2.dist-info/RECORD,,
265
+ airbyte_cdk-0.39.4.dist-info/LICENSE.txt,sha256=Wfe61S4BaGPj404v8lrAbvhjYR68SHlkzeYrg3_bbuM,1051
266
+ airbyte_cdk-0.39.4.dist-info/METADATA,sha256=pIdFzow6PMTMXmX-gel8XxP1dX6FwtRScbO35opWUzM,8902
267
+ airbyte_cdk-0.39.4.dist-info/WHEEL,sha256=pkctZYzUS4AYVn6dJ-7367OJZivF2e8RA9b_ZBjif18,92
268
+ airbyte_cdk-0.39.4.dist-info/top_level.txt,sha256=edvsDKTnE6sD2wfCUaeTfKf5gQIL6CPVMwVL2sWZzqo,51
269
+ airbyte_cdk-0.39.4.dist-info/RECORD,,
@@ -354,6 +354,7 @@ def test_read():
354
354
  ],
355
355
  test_read_limit_reached=False,
356
356
  inferred_schema=None,
357
+ latest_config_update={}
357
358
  )
358
359
 
359
360
  expected_airbyte_message = AirbyteMessage(
@@ -367,6 +368,7 @@ def test_read():
367
368
  ],
368
369
  "test_read_limit_reached": False,
369
370
  "inferred_schema": None,
371
+ "latest_config_update": {}
370
372
  },
371
373
  emitted_at=1,
372
374
  ),
@@ -407,7 +409,8 @@ def test_read_returns_error_response(mock_from_exception):
407
409
  pages=[StreamReadPages(records=[], request=None, response=None)],
408
410
  slice_descriptor=None, state=None)],
409
411
  test_read_limit_reached=False,
410
- inferred_schema=None)
412
+ inferred_schema=None,
413
+ latest_config_update={})
411
414
 
412
415
  expected_message = AirbyteMessage(
413
416
  type=MessageType.RECORD,
@@ -9,7 +9,15 @@ from unittest.mock import MagicMock, patch
9
9
  import pytest
10
10
  from airbyte_cdk.connector_builder.message_grouper import MessageGrouper
11
11
  from airbyte_cdk.connector_builder.models import HttpRequest, HttpResponse, LogMessage, StreamRead, StreamReadPages
12
- from airbyte_cdk.models import AirbyteLogMessage, AirbyteMessage, AirbyteRecordMessage, Level
12
+ from airbyte_cdk.models import (
13
+ AirbyteControlConnectorConfigMessage,
14
+ AirbyteControlMessage,
15
+ AirbyteLogMessage,
16
+ AirbyteMessage,
17
+ AirbyteRecordMessage,
18
+ Level,
19
+ OrchestratorType,
20
+ )
13
21
  from airbyte_cdk.models import Type as MessageType
14
22
  from unit_tests.connector_builder.utils import create_configured_catalog
15
23
 
@@ -463,9 +471,9 @@ def test_get_grouped_messages_with_many_slices(mock_entrypoint_read):
463
471
  )
464
472
  )
465
473
 
466
- connecto_builder_handler = MessageGrouper(MAX_PAGES_PER_SLICE, MAX_SLICES)
474
+ connector_builder_handler = MessageGrouper(MAX_PAGES_PER_SLICE, MAX_SLICES)
467
475
 
468
- stream_read: StreamRead = connecto_builder_handler.get_message_groups(
476
+ stream_read: StreamRead = connector_builder_handler.get_message_groups(
469
477
  source=mock_source, config=CONFIG, configured_catalog=create_configured_catalog("hashiras")
470
478
  )
471
479
 
@@ -530,6 +538,76 @@ def test_read_stream_returns_error_if_stream_does_not_exist():
530
538
  assert "ERROR" in actual_response.logs[0].level
531
539
 
532
540
 
541
+ @patch('airbyte_cdk.connector_builder.message_grouper.AirbyteEntrypoint.read')
542
+ def test_given_control_message_then_stream_read_has_config_update(mock_entrypoint_read):
543
+ updated_config = {"x": 1}
544
+ mock_source = make_mock_source(mock_entrypoint_read, iter(
545
+ any_request_and_response_with_a_record() + [connector_configuration_control_message(1, updated_config)]
546
+ ))
547
+ connector_builder_handler = MessageGrouper(MAX_PAGES_PER_SLICE, MAX_SLICES)
548
+ stream_read: StreamRead = connector_builder_handler.get_message_groups(
549
+ source=mock_source, config=CONFIG, configured_catalog=create_configured_catalog("hashiras")
550
+ )
551
+
552
+ assert stream_read.latest_config_update == updated_config
553
+
554
+
555
+ @patch('airbyte_cdk.connector_builder.message_grouper.AirbyteEntrypoint.read')
556
+ def test_given_no_control_message_then_use_in_memory_config_change_as_update(mock_entrypoint_read):
557
+ mock_source = make_mock_source(mock_entrypoint_read, iter(any_request_and_response_with_a_record()))
558
+ connector_builder_handler = MessageGrouper(MAX_PAGES_PER_SLICE, MAX_SLICES)
559
+ full_config = {**CONFIG, **{"__injected_declarative_manifest": MANIFEST}}
560
+ stream_read: StreamRead = connector_builder_handler.get_message_groups(
561
+ source=mock_source, config=full_config, configured_catalog=create_configured_catalog("hashiras")
562
+ )
563
+
564
+ assert stream_read.latest_config_update == CONFIG
565
+
566
+
567
+ @patch('airbyte_cdk.connector_builder.message_grouper.AirbyteEntrypoint.read')
568
+ def test_given_multiple_control_messages_then_stream_read_has_latest_based_on_emitted_at(mock_entrypoint_read):
569
+ earliest = 0
570
+ earliest_config = {"earliest": 0}
571
+ latest = 1
572
+ latest_config = {"latest": 1}
573
+ mock_source = make_mock_source(mock_entrypoint_read, iter(
574
+ any_request_and_response_with_a_record() +
575
+ [
576
+ # here, we test that even if messages are emitted in a different order, we still rely on `emitted_at`
577
+ connector_configuration_control_message(latest, latest_config),
578
+ connector_configuration_control_message(earliest, earliest_config),
579
+ ]
580
+ )
581
+ )
582
+ connector_builder_handler = MessageGrouper(MAX_PAGES_PER_SLICE, MAX_SLICES)
583
+ stream_read: StreamRead = connector_builder_handler.get_message_groups(
584
+ source=mock_source, config=CONFIG, configured_catalog=create_configured_catalog("hashiras")
585
+ )
586
+
587
+ assert stream_read.latest_config_update == latest_config
588
+
589
+
590
+ @patch('airbyte_cdk.connector_builder.message_grouper.AirbyteEntrypoint.read')
591
+ def test_given_multiple_control_messages_with_same_timestamp_then_stream_read_has_latest_based_on_message_order(mock_entrypoint_read):
592
+ emitted_at = 0
593
+ earliest_config = {"earliest": 0}
594
+ latest_config = {"latest": 1}
595
+ mock_source = make_mock_source(mock_entrypoint_read, iter(
596
+ any_request_and_response_with_a_record() +
597
+ [
598
+ connector_configuration_control_message(emitted_at, earliest_config),
599
+ connector_configuration_control_message(emitted_at, latest_config),
600
+ ]
601
+ )
602
+ )
603
+ connector_builder_handler = MessageGrouper(MAX_PAGES_PER_SLICE, MAX_SLICES)
604
+ stream_read: StreamRead = connector_builder_handler.get_message_groups(
605
+ source=mock_source, config=CONFIG, configured_catalog=create_configured_catalog("hashiras")
606
+ )
607
+
608
+ assert stream_read.latest_config_update == latest_config
609
+
610
+
533
611
  def make_mock_source(mock_entrypoint_read, return_value: Iterator) -> MagicMock:
534
612
  mock_source = MagicMock()
535
613
  mock_entrypoint_read.return_value = return_value
@@ -550,3 +628,22 @@ def record_message(stream: str, data: dict) -> AirbyteMessage:
550
628
 
551
629
  def slice_message(slice_descriptor: str = '{"key": "value"}') -> AirbyteMessage:
552
630
  return AirbyteMessage(type=MessageType.LOG, log=AirbyteLogMessage(level=Level.INFO, message="slice:" + slice_descriptor))
631
+
632
+
633
+ def connector_configuration_control_message(emitted_at: float, config: dict) -> AirbyteMessage:
634
+ return AirbyteMessage(
635
+ type=MessageType.CONTROL,
636
+ control=AirbyteControlMessage(
637
+ type=OrchestratorType.CONNECTOR_CONFIG,
638
+ emitted_at=emitted_at,
639
+ connectorConfig=AirbyteControlConnectorConfigMessage(config=config),
640
+ )
641
+ )
642
+
643
+
644
+ def any_request_and_response_with_a_record():
645
+ return [
646
+ request_log_message({"request": 1}),
647
+ response_log_message({"response": 2}),
648
+ record_message("hashiras", {"name": "Shinobu Kocho"}),
649
+ ]
@@ -54,6 +54,7 @@ from airbyte_cdk.sources.declarative.stream_slicers import CartesianProductStrea
54
54
  from airbyte_cdk.sources.declarative.transformations import AddFields, RemoveFields
55
55
  from airbyte_cdk.sources.declarative.transformations.add_fields import AddedFieldDefinition
56
56
  from airbyte_cdk.sources.declarative.yaml_declarative_source import YamlDeclarativeSource
57
+ from airbyte_cdk.sources.streams.http.requests_native_auth.oauth import SingleUseRefreshTokenOauth2Authenticator
57
58
  from unit_tests.sources.declarative.parsers.testing_components import TestingCustomSubstreamPartitionRouter, TestingSomeComponent
58
59
 
59
60
  factory = ModelToComponentFactory()
@@ -293,6 +294,45 @@ def test_interpolate_config():
293
294
  assert authenticator.get_refresh_request_body() == {"body_field": "yoyoyo", "interpolated_body_field": "verysecrettoken"}
294
295
 
295
296
 
297
+ def test_single_use_oauth_branch():
298
+ single_use_input_config = {"apikey": "verysecrettoken", "repos": ["airbyte", "airbyte-cloud"], "credentials": {"access_token": "access_token", "token_expiry_date": "1970-01-01"}}
299
+
300
+ content = """
301
+ authenticator:
302
+ type: OAuthAuthenticator
303
+ client_id: "some_client_id"
304
+ client_secret: "some_client_secret"
305
+ token_refresh_endpoint: "https://api.sendgrid.com/v3/auth"
306
+ refresh_token: "{{ config['apikey'] }}"
307
+ refresh_request_body:
308
+ body_field: "yoyoyo"
309
+ interpolated_body_field: "{{ config['apikey'] }}"
310
+ refresh_token_updater:
311
+ refresh_token_name: "the_refresh_token"
312
+ refresh_token_config_path:
313
+ - apikey
314
+ """
315
+ parsed_manifest = YamlDeclarativeSource._parse(content)
316
+ resolved_manifest = resolver.preprocess_manifest(parsed_manifest)
317
+ authenticator_manifest = transformer.propagate_types_and_parameters("", resolved_manifest["authenticator"], {})
318
+
319
+ authenticator: SingleUseRefreshTokenOauth2Authenticator = factory.create_component(
320
+ model_type=OAuthAuthenticatorModel, component_definition=authenticator_manifest, config=single_use_input_config
321
+ )
322
+
323
+ assert isinstance(authenticator, SingleUseRefreshTokenOauth2Authenticator)
324
+ assert authenticator._client_id == "some_client_id"
325
+ assert authenticator._client_secret == "some_client_secret"
326
+ assert authenticator._token_refresh_endpoint == "https://api.sendgrid.com/v3/auth"
327
+ assert authenticator._refresh_token == "verysecrettoken"
328
+ assert authenticator._refresh_request_body == {"body_field": "yoyoyo", "interpolated_body_field": "verysecrettoken"}
329
+ assert authenticator._refresh_token_name == "the_refresh_token"
330
+ assert authenticator._refresh_token_config_path == ["apikey"]
331
+ # default values
332
+ assert authenticator._access_token_config_path == ["credentials", "access_token"]
333
+ assert authenticator._token_expiry_date_config_path == ["credentials", "token_expiry_date"]
334
+
335
+
296
336
  def test_list_based_stream_slicer_with_values_refd():
297
337
  content = """
298
338
  repositories: ["airbyte", "airbyte-cloud"]
@@ -227,25 +227,31 @@ class TestSingleUseRefreshTokenOauth2Authenticator:
227
227
  authenticator = SingleUseRefreshTokenOauth2Authenticator(
228
228
  connector_config,
229
229
  token_refresh_endpoint="foobar",
230
+ client_id=connector_config["credentials"]["client_id"],
231
+ client_secret=connector_config["credentials"]["client_secret"],
230
232
  )
231
233
  assert authenticator.access_token == connector_config["credentials"]["access_token"]
232
234
  assert authenticator.get_refresh_token() == connector_config["credentials"]["refresh_token"]
233
235
  assert authenticator.get_token_expiry_date() == pendulum.parse(connector_config["credentials"]["token_expiry_date"])
234
236
 
235
- def test_init_with_invalid_config(self, invalid_connector_config):
236
- with pytest.raises(ValueError):
237
- SingleUseRefreshTokenOauth2Authenticator(
238
- invalid_connector_config,
239
- token_refresh_endpoint="foobar",
240
- )
241
-
242
237
  @freezegun.freeze_time("2022-12-31")
243
- def test_get_access_token(self, capsys, mocker, connector_config):
238
+ @pytest.mark.parametrize(
239
+ "test_name, expires_in_value, expiry_date_format, expected_expiry_date",
240
+ [
241
+ ("number_of_seconds", 42, None, "2022-12-31T00:00:42+00:00"),
242
+ ("string_of_seconds", "42", None, "2022-12-31T00:00:42+00:00"),
243
+ ("date_format", "2023-04-04", "YYYY-MM-DD", "2023-04-04T00:00:00+00:00"),
244
+ ]
245
+ )
246
+ def test_get_access_token(self, test_name, expires_in_value, expiry_date_format, expected_expiry_date, capsys, mocker, connector_config):
244
247
  authenticator = SingleUseRefreshTokenOauth2Authenticator(
245
248
  connector_config,
246
249
  token_refresh_endpoint="foobar",
250
+ client_id=connector_config["credentials"]["client_id"],
251
+ client_secret=connector_config["credentials"]["client_secret"],
252
+ token_expiry_date_format=expiry_date_format,
247
253
  )
248
- authenticator.refresh_access_token = mocker.Mock(return_value=("new_access_token", 42, "new_refresh_token"))
254
+ authenticator.refresh_access_token = mocker.Mock(return_value=("new_access_token", expires_in_value, "new_refresh_token"))
249
255
  authenticator.token_has_expired = mocker.Mock(return_value=True)
250
256
  access_token = authenticator.get_access_token()
251
257
  captured = capsys.readouterr()
@@ -253,7 +259,7 @@ class TestSingleUseRefreshTokenOauth2Authenticator:
253
259
  expected_new_config = connector_config.copy()
254
260
  expected_new_config["credentials"]["access_token"] = "new_access_token"
255
261
  expected_new_config["credentials"]["refresh_token"] = "new_refresh_token"
256
- expected_new_config["credentials"]["token_expiry_date"] = "2022-12-31T00:00:42+00:00"
262
+ expected_new_config["credentials"]["token_expiry_date"] = expected_expiry_date
257
263
  assert airbyte_message["control"]["connectorConfig"]["config"] == expected_new_config
258
264
  assert authenticator.access_token == access_token == "new_access_token"
259
265
  assert authenticator.get_refresh_token() == "new_refresh_token"
@@ -268,26 +274,18 @@ class TestSingleUseRefreshTokenOauth2Authenticator:
268
274
  authenticator = SingleUseRefreshTokenOauth2Authenticator(
269
275
  connector_config,
270
276
  token_refresh_endpoint="foobar",
277
+ client_id=connector_config["credentials"]["client_id"],
278
+ client_secret=connector_config["credentials"]["client_secret"],
271
279
  )
272
280
 
273
281
  authenticator._get_refresh_access_token_response = mocker.Mock(
274
282
  return_value={
275
283
  authenticator.get_access_token_name(): "new_access_token",
276
- authenticator.get_expires_in_name(): 42,
277
- authenticator.get_refresh_token_name(): "new_refresh_token",
278
- }
279
- )
280
- assert authenticator.refresh_access_token() == ("new_access_token", 42, "new_refresh_token")
281
-
282
- # Test with expires_in as str
283
- authenticator._get_refresh_access_token_response = mocker.Mock(
284
- return_value={
285
- authenticator.get_access_token_name(): "new_access_token",
286
- authenticator.get_expires_in_name(): "1000",
284
+ authenticator.get_expires_in_name(): "42",
287
285
  authenticator.get_refresh_token_name(): "new_refresh_token",
288
286
  }
289
287
  )
290
- assert authenticator.refresh_access_token() == ("new_access_token", 1000, "new_refresh_token")
288
+ assert authenticator.refresh_access_token() == ("new_access_token", "42", "new_refresh_token")
291
289
 
292
290
 
293
291
  def mock_request(method, url, data):