Skip to content

Commit 1da0d93

Browse files
Merge branch 'main' into daryna/low-code/pass-refresh-headers-to-oauth
2 parents 60ecfbf + 40a9f1e commit 1da0d93

File tree

23 files changed

+914
-104
lines changed

23 files changed

+914
-104
lines changed

airbyte_cdk/sources/declarative/auth/oauth.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,9 +57,13 @@ class DeclarativeOauth2Authenticator(AbstractOauth2Authenticator, DeclarativeAut
5757
token_expiry_is_time_of_expiration: bool = False
5858
access_token_name: Union[InterpolatedString, str] = "access_token"
5959
access_token_value: Optional[Union[InterpolatedString, str]] = None
60+
client_id_name: Union[InterpolatedString, str] = "client_id"
61+
client_secret_name: Union[InterpolatedString, str] = "client_secret"
6062
expires_in_name: Union[InterpolatedString, str] = "expires_in"
63+
refresh_token_name: Union[InterpolatedString, str] = "refresh_token"
6164
refresh_request_body: Optional[Mapping[str, Any]] = None
6265
refresh_request_headers: Optional[Mapping[str, Any]] = None
66+
grant_type_name: Union[InterpolatedString, str] = "grant_type"
6367
grant_type: Union[InterpolatedString, str] = "refresh_token"
6468
message_repository: MessageRepository = NoopMessageRepository()
6569

@@ -71,8 +75,15 @@ def __post_init__(self, parameters: Mapping[str, Any]) -> None:
7175
)
7276
else:
7377
self._token_refresh_endpoint = None
78+
self._client_id_name = InterpolatedString.create(self.client_id_name, parameters=parameters)
7479
self._client_id = InterpolatedString.create(self.client_id, parameters=parameters)
80+
self._client_secret_name = InterpolatedString.create(
81+
self.client_secret_name, parameters=parameters
82+
)
7583
self._client_secret = InterpolatedString.create(self.client_secret, parameters=parameters)
84+
self._refresh_token_name = InterpolatedString.create(
85+
self.refresh_token_name, parameters=parameters
86+
)
7687
if self.refresh_token is not None:
7788
self._refresh_token: Optional[InterpolatedString] = InterpolatedString.create(
7889
self.refresh_token, parameters=parameters
@@ -85,6 +96,9 @@ def __post_init__(self, parameters: Mapping[str, Any]) -> None:
8596
self.expires_in_name = InterpolatedString.create(
8697
self.expires_in_name, parameters=parameters
8798
)
99+
self.grant_type_name = InterpolatedString.create(
100+
self.grant_type_name, parameters=parameters
101+
)
88102
self.grant_type = InterpolatedString.create(self.grant_type, parameters=parameters)
89103
self._refresh_request_body = InterpolatedMapping(
90104
self.refresh_request_body or {}, parameters=parameters
@@ -127,18 +141,27 @@ def get_token_refresh_endpoint(self) -> Optional[str]:
127141
return refresh_token_endpoint
128142
return None
129143

144+
def get_client_id_name(self) -> str:
145+
return self._client_id_name.eval(self.config) # type: ignore # eval returns a string in this context
146+
130147
def get_client_id(self) -> str:
131148
client_id: str = self._client_id.eval(self.config)
132149
if not client_id:
133150
raise ValueError("OAuthAuthenticator was unable to evaluate client_id parameter")
134151
return client_id
135152

153+
def get_client_secret_name(self) -> str:
154+
return self._client_secret_name.eval(self.config) # type: ignore # eval returns a string in this context
155+
136156
def get_client_secret(self) -> str:
137157
client_secret: str = self._client_secret.eval(self.config)
138158
if not client_secret:
139159
raise ValueError("OAuthAuthenticator was unable to evaluate client_secret parameter")
140160
return client_secret
141161

162+
def get_refresh_token_name(self) -> str:
163+
return self._refresh_token_name.eval(self.config) # type: ignore # eval returns a string in this context
164+
142165
def get_refresh_token(self) -> Optional[str]:
143166
return None if self._refresh_token is None else str(self._refresh_token.eval(self.config))
144167

@@ -151,6 +174,9 @@ def get_access_token_name(self) -> str:
151174
def get_expires_in_name(self) -> str:
152175
return self.expires_in_name.eval(self.config) # type: ignore # eval returns a string in this context
153176

177+
def get_grant_type_name(self) -> str:
178+
return self.grant_type_name.eval(self.config) # type: ignore # eval returns a string in this context
179+
154180
def get_grant_type(self) -> str:
155181
return self.grant_type.eval(self.config) # type: ignore # eval returns a string in this context
156182

airbyte_cdk/sources/declarative/declarative_component_schema.yaml

Lines changed: 67 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -678,7 +678,7 @@ definitions:
678678
properties:
679679
type:
680680
type: string
681-
enum: [ CustomSchemaNormalization ]
681+
enum: [CustomSchemaNormalization]
682682
class_name:
683683
title: Class Name
684684
description: Fully-qualified name of the class that will be implementing the custom normalization. The format is `source_<name>.<package>.<class_name>`.
@@ -1047,20 +1047,41 @@ definitions:
10471047
type:
10481048
type: string
10491049
enum: [OAuthAuthenticator]
1050+
client_id_name:
1051+
title: Client ID Property Name
1052+
description: The name of the property to use to refresh the `access_token`.
1053+
type: string
1054+
default: "client_id"
1055+
examples:
1056+
- custom_app_id
10501057
client_id:
10511058
title: Client ID
10521059
description: The OAuth client ID. Fill it in the user inputs.
10531060
type: string
10541061
examples:
10551062
- "{{ config['client_id }}"
10561063
- "{{ config['credentials']['client_id }}"
1064+
client_secret_name:
1065+
title: Client Secret Property Name
1066+
description: The name of the property to use to refresh the `access_token`.
1067+
type: string
1068+
default: "client_secret"
1069+
examples:
1070+
- custom_app_secret
10571071
client_secret:
10581072
title: Client Secret
10591073
description: The OAuth client secret. Fill it in the user inputs.
10601074
type: string
10611075
examples:
10621076
- "{{ config['client_secret }}"
10631077
- "{{ config['credentials']['client_secret }}"
1078+
refresh_token_name:
1079+
title: Refresh Token Property Name
1080+
description: The name of the property to use to refresh the `access_token`.
1081+
type: string
1082+
default: "refresh_token"
1083+
examples:
1084+
- custom_app_refresh_value
10641085
refresh_token:
10651086
title: Refresh Token
10661087
description: Credential artifact used to get a new access token.
@@ -1094,6 +1115,13 @@ definitions:
10941115
default: "expires_in"
10951116
examples:
10961117
- expires_in
1118+
grant_type_name:
1119+
title: Grant Type Property Name
1120+
description: The name of the property to use to refresh the `access_token`.
1121+
type: string
1122+
default: "grant_type"
1123+
examples:
1124+
- custom_grant_type
10971125
grant_type:
10981126
title: Grant Type
10991127
description: Specifies the OAuth2 grant type. If set to refresh_token, the refresh_token needs to be provided as well. For client_credentials, only client id and secret are required. Other grant types are not officially supported.
@@ -2212,15 +2240,15 @@ definitions:
22122240
Pertains to the fields defined by the connector relating to the OAuth flow.
22132241
22142242
Interpolation capabilities:
2215-
- The variables placeholders are declared as `{my_var}`.
2216-
- The nested resolution variables like `{{my_nested_var}}` is allowed as well.
2243+
- The variables placeholders are declared as `{{my_var}}`.
2244+
- The nested resolution variables like `{{ {{my_nested_var}} }}` is allowed as well.
22172245
22182246
- The allowed interpolation context is:
2219-
+ base64Encoder - encode to `base64`, {base64Encoder:{my_var_a}:{my_var_b}}
2220-
+ base64Decorer - decode from `base64` encoded string, {base64Decoder:{my_string_variable_or_string_value}}
2221-
+ urlEncoder - encode the input string to URL-like format, {urlEncoder:https://test.host.com/endpoint}
2222-
+ urlDecorer - decode the input url-encoded string into text format, {urlDecoder:https%3A%2F%2Fairbyte.io}
2223-
+ codeChallengeS256 - get the `codeChallenge` encoded value to provide additional data-provider specific authorisation values, {codeChallengeS256:{state_value}}
2247+
+ base64Encoder - encode to `base64`, {{ {{my_var_a}}:{{my_var_b}} | base64Encoder }}
2248+
+ base64Decorer - decode from `base64` encoded string, {{ {{my_string_variable_or_string_value}} | base64Decoder }}
2249+
+ urlEncoder - encode the input string to URL-like format, {{ https://test.host.com/endpoint | urlEncoder}}
2250+
+ urlDecorer - decode the input url-encoded string into text format, {{ urlDecoder:https%3A%2F%2Fairbyte.io | urlDecoder}}
2251+
+ codeChallengeS256 - get the `codeChallenge` encoded value to provide additional data-provider specific authorisation values, {{ {{state_value}} | codeChallengeS256 }}
22242252
22252253
Examples:
22262254
- The TikTok Marketing DeclarativeOAuth spec:
@@ -2229,12 +2257,12 @@ definitions:
22292257
"type": "object",
22302258
"additionalProperties": false,
22312259
"properties": {
2232-
"consent_url": "https://ads.tiktok.com/marketing_api/auth?{client_id_key}={{client_id_key}}&{redirect_uri_key}={urlEncoder:{{redirect_uri_key}}}&{state_key}={{state_key}}",
2260+
"consent_url": "https://ads.tiktok.com/marketing_api/auth?{{client_id_key}}={{client_id_value}}&{{redirect_uri_key}}={{ {{redirect_uri_value}} | urlEncoder}}&{{state_key}}={{state_value}}",
22332261
"access_token_url": "https://business-api.tiktok.com/open_api/v1.3/oauth2/access_token/",
22342262
"access_token_params": {
2235-
"{auth_code_key}": "{{auth_code_key}}",
2236-
"{client_id_key}": "{{client_id_key}}",
2237-
"{client_secret_key}": "{{client_secret_key}}"
2263+
"{{ auth_code_key }}": "{{ auth_code_value }}",
2264+
"{{ client_id_key }}": "{{ client_id_value }}",
2265+
"{{ client_secret_key }}": "{{ client_secret_value }}"
22382266
},
22392267
"access_token_headers": {
22402268
"Content-Type": "application/json",
@@ -2252,7 +2280,6 @@ definitions:
22522280
required:
22532281
- consent_url
22542282
- access_token_url
2255-
- extract_output
22562283
properties:
22572284
consent_url:
22582285
title: Consent URL
@@ -2261,8 +2288,8 @@ definitions:
22612288
The DeclarativeOAuth Specific string URL string template to initiate the authentication.
22622289
The placeholders are replaced during the processing to provide neccessary values.
22632290
examples:
2264-
- https://domain.host.com/marketing_api/auth?{client_id_key}={{client_id_key}}&{redirect_uri_key}={urlEncoder:{{redirect_uri_key}}}&{state_key}={{state_key}}
2265-
- https://endpoint.host.com/oauth2/authorize?{client_id_key}={{client_id_key}}&{redirect_uri_key}={urlEncoder:{{redirect_uri_key}}}&{scope_key}={urlEncoder:{{scope_key}}}&{state_key}={{state_key}}&subdomain={subdomain}
2291+
- https://domain.host.com/marketing_api/auth?{{client_id_key}}={{client_id_value}}&{{redirect_uri_key}}={{{{redirect_uri_value}} | urlEncoder}}&{{state_key}}={{state_value}}
2292+
- https://endpoint.host.com/oauth2/authorize?{{client_id_key}}={{client_id_value}}&{{redirect_uri_key}}={{{{redirect_uri_value}} | urlEncoder}}&{{scope_key}}={{{{scope_value}} | urlEncoder}}&{{state_key}}={{state_value}}&subdomain={{subdomain}}
22662293
scope:
22672294
title: Scopes
22682295
type: string
@@ -2277,7 +2304,7 @@ definitions:
22772304
The DeclarativeOAuth Specific URL templated string to obtain the `access_token`, `refresh_token` etc.
22782305
The placeholders are replaced during the processing to provide neccessary values.
22792306
examples:
2280-
- https://auth.host.com/oauth2/token?{client_id_key}={{client_id_key}}&{client_secret_key}={{client_secret_key}}&{auth_code_key}={{auth_code_key}}&{redirect_uri_key}={urlEncoder:{{redirect_uri_key}}}
2307+
- https://auth.host.com/oauth2/token?{{client_id_key}}={{client_id_value}}&{{client_secret_key}}={{client_secret_value}}&{{auth_code_key}}={{auth_code_value}}&{{redirect_uri_key}}={{{{redirect_uri_value}} | urlEncoder}}
22812308
access_token_headers:
22822309
title: Access Token Headers
22832310
type: object
@@ -2286,7 +2313,7 @@ definitions:
22862313
The DeclarativeOAuth Specific optional headers to inject while exchanging the `auth_code` to `access_token` during `completeOAuthFlow` step.
22872314
examples:
22882315
- {
2289-
"Authorization": "Basic {base64Encoder:{client_id}:{client_secret}}",
2316+
"Authorization": "Basic {{ {{ client_id_value }}:{{ client_secret_value }} | base64Encoder }}",
22902317
}
22912318
access_token_params:
22922319
title: Access Token Query Params (Json Encoded)
@@ -2297,9 +2324,9 @@ definitions:
22972324
When this property is provided, the query params will be encoded as `Json` and included in the outgoing API request.
22982325
examples:
22992326
- {
2300-
"{auth_code_key}": "{{auth_code_key}}",
2301-
"{client_id_key}": "{{client_id_key}}",
2302-
"{client_secret_key}": "{{client_secret_key}}",
2327+
"{{ auth_code_key }}": "{{ auth_code_value }}",
2328+
"{{ client_id_key }}": "{{ client_id_value }}",
2329+
"{{ client_secret_key }}": "{{ client_secret_value }}",
23032330
}
23042331
extract_output:
23052332
title: Extract Output
@@ -2867,6 +2894,7 @@ definitions:
28672894
parser:
28682895
anyOf:
28692896
- "$ref": "#/definitions/GzipParser"
2897+
- "$ref": "#/definitions/JsonParser"
28702898
- "$ref": "#/definitions/JsonLineParser"
28712899
- "$ref": "#/definitions/CsvParser"
28722900
# PARSERS
@@ -2883,6 +2911,20 @@ definitions:
28832911
anyOf:
28842912
- "$ref": "#/definitions/JsonLineParser"
28852913
- "$ref": "#/definitions/CsvParser"
2914+
- "$ref": "#/definitions/JsonParser"
2915+
JsonParser:
2916+
title: JsonParser
2917+
description: Parser used for parsing str, bytes, or bytearray data and returning data in a dictionary format.
2918+
type: object
2919+
required:
2920+
- type
2921+
properties:
2922+
type:
2923+
type: string
2924+
enum: [JsonParser]
2925+
encoding:
2926+
type: string
2927+
default: utf-8
28862928
JsonLineParser:
28872929
type: object
28882930
required:
@@ -2985,6 +3027,11 @@ definitions:
29853027
anyOf:
29863028
- "$ref": "#/definitions/CustomRequester"
29873029
- "$ref": "#/definitions/HttpRequester"
3030+
url_requester:
3031+
description: Requester component that describes how to prepare HTTP requests to send to the source API to extract the url from polling response by the completed async job.
3032+
anyOf:
3033+
- "$ref": "#/definitions/CustomRequester"
3034+
- "$ref": "#/definitions/HttpRequester"
29883035
download_requester:
29893036
description: Requester component that describes how to prepare HTTP requests to send to the source API to download the data provided by the completed async job.
29903037
anyOf:

airbyte_cdk/sources/declarative/decoders/composite_raw_decoder.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,12 @@
77
from io import BufferedIOBase, TextIOWrapper
88
from typing import Any, Generator, MutableMapping, Optional
99

10+
import orjson
1011
import requests
1112

13+
from airbyte_cdk.models import FailureType
1214
from airbyte_cdk.sources.declarative.decoders.decoder import Decoder
15+
from airbyte_cdk.utils import AirbyteTracedException
1316

1417
logger = logging.getLogger("airbyte")
1518

@@ -42,6 +45,46 @@ def parse(
4245
yield from self.inner_parser.parse(gzipobj)
4346

4447

48+
@dataclass
49+
class JsonParser(Parser):
50+
encoding: str = "utf-8"
51+
52+
def parse(self, data: BufferedIOBase) -> Generator[MutableMapping[str, Any], None, None]:
53+
"""
54+
Attempts to deserialize data using orjson library. As an extra layer of safety we fallback on the json library to deserialize the data.
55+
"""
56+
raw_data = data.read()
57+
body_json = self._parse_orjson(raw_data) or self._parse_json(raw_data)
58+
59+
if body_json is None:
60+
raise AirbyteTracedException(
61+
message="Response JSON data failed to be parsed. See logs for more information.",
62+
internal_message=f"Response JSON data failed to be parsed.",
63+
failure_type=FailureType.system_error,
64+
)
65+
66+
if isinstance(body_json, list):
67+
yield from body_json
68+
else:
69+
yield from [body_json]
70+
71+
def _parse_orjson(self, raw_data: bytes) -> Optional[Any]:
72+
try:
73+
return orjson.loads(raw_data.decode(self.encoding))
74+
except Exception as exc:
75+
logger.debug(
76+
f"Failed to parse JSON data using orjson library. Falling back to json library. {exc}"
77+
)
78+
return None
79+
80+
def _parse_json(self, raw_data: bytes) -> Optional[Any]:
81+
try:
82+
return json.loads(raw_data.decode(self.encoding))
83+
except Exception as exc:
84+
logger.error(f"Failed to parse JSON data using json library. {exc}")
85+
return None
86+
87+
4588
@dataclass
4689
class JsonLineParser(Parser):
4790
encoding: Optional[str] = "utf-8"

0 commit comments

Comments
 (0)