diff --git a/airbyte_cdk/sources/declarative/auth/jwt.py b/airbyte_cdk/sources/declarative/auth/jwt.py index 86e13a99d5..65eb6d1e5e 100644 --- a/airbyte_cdk/sources/declarative/auth/jwt.py +++ b/airbyte_cdk/sources/declarative/auth/jwt.py @@ -6,7 +6,7 @@ import json from dataclasses import InitVar, dataclass from datetime import datetime -from typing import Any, Mapping, Optional, Union, cast +from typing import Any, Mapping, MutableMapping, Optional, Union, cast import jwt from cryptography.hazmat.primitives import serialization @@ -14,12 +14,15 @@ from cryptography.hazmat.primitives.asymmetric.ed448 import Ed448PrivateKey from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey -from cryptography.hazmat.primitives.asymmetric.types import PrivateKeyTypes from airbyte_cdk.sources.declarative.auth.declarative_authenticator import DeclarativeAuthenticator from airbyte_cdk.sources.declarative.interpolation.interpolated_boolean import InterpolatedBoolean from airbyte_cdk.sources.declarative.interpolation.interpolated_mapping import InterpolatedMapping from airbyte_cdk.sources.declarative.interpolation.interpolated_string import InterpolatedString +from airbyte_cdk.sources.declarative.requesters.request_option import ( + RequestOption, + RequestOptionType, +) # Type alias for keys that JWT library accepts JwtKeyTypes = Union[ @@ -86,6 +89,7 @@ class JwtAuthenticator(DeclarativeAuthenticator): additional_jwt_headers: Optional[Mapping[str, Any]] = None additional_jwt_payload: Optional[Mapping[str, Any]] = None passphrase: Optional[Union[InterpolatedString, str]] = None + request_option: Optional[RequestOption] = None def __post_init__(self, parameters: Mapping[str, Any]) -> None: self._secret_key = InterpolatedString.create(self.secret_key, parameters=parameters) @@ -121,6 +125,13 @@ def __post_init__(self, parameters: Mapping[str, Any]) -> None: else None ) + # When we first implemented the JWT authenticator, we assumed that the signed token was always supposed + # to be loaded into the request headers under the `Authorization` key. This is not always the case, but + # this default option allows for backwards compatibility to be retained for existing connectors + self._request_option = self.request_option or RequestOption( + inject_into=RequestOptionType.header, field_name="Authorization", parameters=parameters + ) + def _get_jwt_headers(self) -> dict[str, Any]: """ Builds and returns the headers used when signing the JWT. @@ -213,7 +224,8 @@ def _get_header_prefix(self) -> Union[str, None]: @property def auth_header(self) -> str: - return "Authorization" + options = self._get_request_options(RequestOptionType.header) + return next(iter(options.keys()), "") @property def token(self) -> str: @@ -222,3 +234,18 @@ def token(self) -> str: if self._get_header_prefix() else self._get_signed_token() ) + + def get_request_params(self) -> Mapping[str, Any]: + return self._get_request_options(RequestOptionType.request_parameter) + + def get_request_body_data(self) -> Union[Mapping[str, Any], str]: + return self._get_request_options(RequestOptionType.body_data) + + def get_request_body_json(self) -> Mapping[str, Any]: + return self._get_request_options(RequestOptionType.body_json) + + def _get_request_options(self, option_type: RequestOptionType) -> Mapping[str, Any]: + options: MutableMapping[str, Any] = {} + if self._request_option.inject_into == option_type: + self._request_option.inject_into_request(options, self.token, self.config) + return options diff --git a/airbyte_cdk/sources/declarative/declarative_component_schema.yaml b/airbyte_cdk/sources/declarative/declarative_component_schema.yaml index 35c3064a66..b62f4252f3 100644 --- a/airbyte_cdk/sources/declarative/declarative_component_schema.yaml +++ b/airbyte_cdk/sources/declarative/declarative_component_schema.yaml @@ -1276,6 +1276,10 @@ definitions: type: string examples: - "{{ config['passphrase'] }}" + request_option: + title: Request Option + description: A request option describing where the signed JWT token that is generated should be injected into the outbound API request. + "$ref": "#/definitions/RequestOption" $parameters: type: object additionalProperties: true diff --git a/airbyte_cdk/sources/declarative/models/declarative_component_schema.py b/airbyte_cdk/sources/declarative/models/declarative_component_schema.py index 9525f9d00e..34ef7a4639 100644 --- a/airbyte_cdk/sources/declarative/models/declarative_component_schema.py +++ b/airbyte_cdk/sources/declarative/models/declarative_component_schema.py @@ -1,5 +1,3 @@ -# Copyright (c) 2025 Airbyte, Inc., all rights reserved. - # generated by datamodel-codegen: # filename: declarative_component_schema.yaml @@ -397,66 +395,6 @@ class Config: ) -class JwtAuthenticator(BaseModel): - type: Literal["JwtAuthenticator"] - secret_key: str = Field( - ..., - description="Secret used to sign the JSON web token.", - examples=["{{ config['secret_key'] }}"], - title="Secret Key", - ) - base64_encode_secret_key: Optional[bool] = Field( - False, - description='When set to true, the secret key will be base64 encoded prior to being encoded as part of the JWT. Only set to "true" when required by the API.', - title="Base64-encode Secret Key", - ) - algorithm: Algorithm = Field( - ..., - description="Algorithm used to sign the JSON web token.", - examples=["ES256", "HS256", "RS256", "{{ config['algorithm'] }}"], - title="Algorithm", - ) - token_duration: Optional[int] = Field( - 1200, - description="The amount of time in seconds a JWT token can be valid after being issued.", - examples=[1200, 3600], - title="Token Duration", - ) - header_prefix: Optional[str] = Field( - None, - description="The prefix to be used within the Authentication header.", - examples=["Bearer", "Basic"], - title="Header Prefix", - ) - jwt_headers: Optional[JwtHeaders] = Field( - None, - description="JWT headers used when signing JSON web token.", - title="JWT Headers", - ) - additional_jwt_headers: Optional[Dict[str, Any]] = Field( - None, - description="Additional headers to be included with the JWT headers object.", - title="Additional JWT Headers", - ) - jwt_payload: Optional[JwtPayload] = Field( - None, - description="JWT Payload used when signing JSON web token.", - title="JWT Payload", - ) - additional_jwt_payload: Optional[Dict[str, Any]] = Field( - None, - description="Additional properties to be added to the JWT payload.", - title="Additional JWT Payload Properties", - ) - passphrase: Optional[str] = Field( - None, - description="A passphrase/password used to encrypt the private key. Only provide a passphrase if required by the API for JWT authentication. The API will typically provide the passphrase when generating the public/private key pair.", - examples=["{{ config['passphrase'] }}"], - title="Passphrase", - ) - parameters: Optional[Dict[str, Any]] = Field(None, alias="$parameters") - - class RefreshTokenUpdater(BaseModel): refresh_token_name: Optional[str] = Field( "refresh_token", @@ -502,148 +440,6 @@ class RefreshTokenUpdater(BaseModel): ) -class OAuthAuthenticator(BaseModel): - type: Literal["OAuthAuthenticator"] - client_id_name: Optional[str] = Field( - "client_id", - description="The name of the property to use to refresh the `access_token`.", - examples=["custom_app_id"], - title="Client ID Property Name", - ) - client_id: Optional[str] = Field( - None, - description="The OAuth client ID. Fill it in the user inputs.", - examples=[ - "{{ config['client_id'] }}", - "{{ config['credentials']['client_id }}", - ], - title="Client ID", - ) - client_secret_name: Optional[str] = Field( - "client_secret", - description="The name of the property to use to refresh the `access_token`.", - examples=["custom_app_secret"], - title="Client Secret Property Name", - ) - client_secret: Optional[str] = Field( - None, - description="The OAuth client secret. Fill it in the user inputs.", - examples=[ - "{{ config['client_secret'] }}", - "{{ config['credentials']['client_secret }}", - ], - title="Client Secret", - ) - refresh_token_name: Optional[str] = Field( - "refresh_token", - description="The name of the property to use to refresh the `access_token`.", - examples=["custom_app_refresh_value"], - title="Refresh Token Property Name", - ) - refresh_token: Optional[str] = Field( - None, - description="Credential artifact used to get a new access token.", - examples=[ - "{{ config['refresh_token'] }}", - "{{ config['credentials]['refresh_token'] }}", - ], - title="Refresh Token", - ) - token_refresh_endpoint: Optional[str] = Field( - None, - description="The full URL to call to obtain a new access token.", - examples=["https://connect.squareup.com/oauth2/token"], - title="Token Refresh Endpoint", - ) - access_token_name: Optional[str] = Field( - "access_token", - description="The name of the property which contains the access token in the response from the token refresh endpoint.", - examples=["access_token"], - title="Access Token Property Name", - ) - access_token_value: Optional[str] = Field( - None, - description="The value of the access_token to bypass the token refreshing using `refresh_token`.", - examples=["secret_access_token_value"], - title="Access Token Value", - ) - expires_in_name: Optional[str] = Field( - "expires_in", - description="The name of the property which contains the expiry date in the response from the token refresh endpoint.", - examples=["expires_in"], - title="Token Expiry Property Name", - ) - grant_type_name: Optional[str] = Field( - "grant_type", - description="The name of the property to use to refresh the `access_token`.", - examples=["custom_grant_type"], - title="Grant Type Property Name", - ) - grant_type: Optional[str] = Field( - "refresh_token", - 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.", - examples=["refresh_token", "client_credentials"], - title="Grant Type", - ) - refresh_request_body: Optional[Dict[str, Any]] = Field( - None, - description="Body of the request sent to get a new access token.", - examples=[ - { - "applicationId": "{{ config['application_id'] }}", - "applicationSecret": "{{ config['application_secret'] }}", - "token": "{{ config['token'] }}", - } - ], - title="Refresh Request Body", - ) - refresh_request_headers: Optional[Dict[str, Any]] = Field( - None, - description="Headers of the request sent to get a new access token.", - examples=[ - { - "Authorization": "", - "Content-Type": "application/x-www-form-urlencoded", - } - ], - title="Refresh Request Headers", - ) - scopes: Optional[List[str]] = Field( - None, - description="List of scopes that should be granted to the access token.", - examples=[["crm.list.read", "crm.objects.contacts.read", "crm.schema.contacts.read"]], - title="Scopes", - ) - token_expiry_date: Optional[str] = Field( - None, - description="The access token expiry date.", - examples=["2023-04-06T07:12:10.421833+00:00", 1680842386], - title="Token Expiry Date", - ) - token_expiry_date_format: Optional[str] = Field( - None, - description="The format of the time to expiration datetime. Provide it if the time is returned as a date-time string instead of seconds.", - examples=["%Y-%m-%d %H:%M:%S.%f+00:00"], - title="Token Expiry Date Format", - ) - refresh_token_updater: Optional[RefreshTokenUpdater] = Field( - None, - description="When the refresh 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.", - title="Refresh Token Updater", - ) - profile_assertion: Optional[JwtAuthenticator] = Field( - None, - description="The authenticator being used to authenticate the client authenticator.", - title="Profile Assertion", - ) - use_profile_assertion: Optional[bool] = Field( - False, - description="Enable using profile assertion as a flow for OAuth authorization.", - title="Use Profile Assertion", - ) - parameters: Optional[Dict[str, Any]] = Field(None, alias="$parameters") - - class Rate(BaseModel): class Config: extra = Extra.allow @@ -1895,6 +1691,213 @@ class DatetimeBasedCursor(BaseModel): parameters: Optional[Dict[str, Any]] = Field(None, alias="$parameters") +class JwtAuthenticator(BaseModel): + type: Literal["JwtAuthenticator"] + secret_key: str = Field( + ..., + description="Secret used to sign the JSON web token.", + examples=["{{ config['secret_key'] }}"], + title="Secret Key", + ) + base64_encode_secret_key: Optional[bool] = Field( + False, + description='When set to true, the secret key will be base64 encoded prior to being encoded as part of the JWT. Only set to "true" when required by the API.', + title="Base64-encode Secret Key", + ) + algorithm: Algorithm = Field( + ..., + description="Algorithm used to sign the JSON web token.", + examples=["ES256", "HS256", "RS256", "{{ config['algorithm'] }}"], + title="Algorithm", + ) + token_duration: Optional[int] = Field( + 1200, + description="The amount of time in seconds a JWT token can be valid after being issued.", + examples=[1200, 3600], + title="Token Duration", + ) + header_prefix: Optional[str] = Field( + None, + description="The prefix to be used within the Authentication header.", + examples=["Bearer", "Basic"], + title="Header Prefix", + ) + jwt_headers: Optional[JwtHeaders] = Field( + None, + description="JWT headers used when signing JSON web token.", + title="JWT Headers", + ) + additional_jwt_headers: Optional[Dict[str, Any]] = Field( + None, + description="Additional headers to be included with the JWT headers object.", + title="Additional JWT Headers", + ) + jwt_payload: Optional[JwtPayload] = Field( + None, + description="JWT Payload used when signing JSON web token.", + title="JWT Payload", + ) + additional_jwt_payload: Optional[Dict[str, Any]] = Field( + None, + description="Additional properties to be added to the JWT payload.", + title="Additional JWT Payload Properties", + ) + passphrase: Optional[str] = Field( + None, + description="A passphrase/password used to encrypt the private key. Only provide a passphrase if required by the API for JWT authentication. The API will typically provide the passphrase when generating the public/private key pair.", + examples=["{{ config['passphrase'] }}"], + title="Passphrase", + ) + request_option: Optional[RequestOption] = Field( + None, + description="A request option describing where the signed JWT token that is generated should be injected into the outbound API request.", + title="Request Option", + ) + parameters: Optional[Dict[str, Any]] = Field(None, alias="$parameters") + + +class OAuthAuthenticator(BaseModel): + type: Literal["OAuthAuthenticator"] + client_id_name: Optional[str] = Field( + "client_id", + description="The name of the property to use to refresh the `access_token`.", + examples=["custom_app_id"], + title="Client ID Property Name", + ) + client_id: Optional[str] = Field( + None, + description="The OAuth client ID. Fill it in the user inputs.", + examples=[ + "{{ config['client_id'] }}", + "{{ config['credentials']['client_id }}", + ], + title="Client ID", + ) + client_secret_name: Optional[str] = Field( + "client_secret", + description="The name of the property to use to refresh the `access_token`.", + examples=["custom_app_secret"], + title="Client Secret Property Name", + ) + client_secret: Optional[str] = Field( + None, + description="The OAuth client secret. Fill it in the user inputs.", + examples=[ + "{{ config['client_secret'] }}", + "{{ config['credentials']['client_secret }}", + ], + title="Client Secret", + ) + refresh_token_name: Optional[str] = Field( + "refresh_token", + description="The name of the property to use to refresh the `access_token`.", + examples=["custom_app_refresh_value"], + title="Refresh Token Property Name", + ) + refresh_token: Optional[str] = Field( + None, + description="Credential artifact used to get a new access token.", + examples=[ + "{{ config['refresh_token'] }}", + "{{ config['credentials]['refresh_token'] }}", + ], + title="Refresh Token", + ) + token_refresh_endpoint: Optional[str] = Field( + None, + description="The full URL to call to obtain a new access token.", + examples=["https://connect.squareup.com/oauth2/token"], + title="Token Refresh Endpoint", + ) + access_token_name: Optional[str] = Field( + "access_token", + description="The name of the property which contains the access token in the response from the token refresh endpoint.", + examples=["access_token"], + title="Access Token Property Name", + ) + access_token_value: Optional[str] = Field( + None, + description="The value of the access_token to bypass the token refreshing using `refresh_token`.", + examples=["secret_access_token_value"], + title="Access Token Value", + ) + expires_in_name: Optional[str] = Field( + "expires_in", + description="The name of the property which contains the expiry date in the response from the token refresh endpoint.", + examples=["expires_in"], + title="Token Expiry Property Name", + ) + grant_type_name: Optional[str] = Field( + "grant_type", + description="The name of the property to use to refresh the `access_token`.", + examples=["custom_grant_type"], + title="Grant Type Property Name", + ) + grant_type: Optional[str] = Field( + "refresh_token", + 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.", + examples=["refresh_token", "client_credentials"], + title="Grant Type", + ) + refresh_request_body: Optional[Dict[str, Any]] = Field( + None, + description="Body of the request sent to get a new access token.", + examples=[ + { + "applicationId": "{{ config['application_id'] }}", + "applicationSecret": "{{ config['application_secret'] }}", + "token": "{{ config['token'] }}", + } + ], + title="Refresh Request Body", + ) + refresh_request_headers: Optional[Dict[str, Any]] = Field( + None, + description="Headers of the request sent to get a new access token.", + examples=[ + { + "Authorization": "", + "Content-Type": "application/x-www-form-urlencoded", + } + ], + title="Refresh Request Headers", + ) + scopes: Optional[List[str]] = Field( + None, + description="List of scopes that should be granted to the access token.", + examples=[["crm.list.read", "crm.objects.contacts.read", "crm.schema.contacts.read"]], + title="Scopes", + ) + token_expiry_date: Optional[str] = Field( + None, + description="The access token expiry date.", + examples=["2023-04-06T07:12:10.421833+00:00", 1680842386], + title="Token Expiry Date", + ) + token_expiry_date_format: Optional[str] = Field( + None, + description="The format of the time to expiration datetime. Provide it if the time is returned as a date-time string instead of seconds.", + examples=["%Y-%m-%d %H:%M:%S.%f+00:00"], + title="Token Expiry Date Format", + ) + refresh_token_updater: Optional[RefreshTokenUpdater] = Field( + None, + description="When the refresh 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.", + title="Refresh Token Updater", + ) + profile_assertion: Optional[JwtAuthenticator] = Field( + None, + description="The authenticator being used to authenticate the client authenticator.", + title="Profile Assertion", + ) + use_profile_assertion: Optional[bool] = Field( + False, + description="Enable using profile assertion as a flow for OAuth authorization.", + title="Use Profile Assertion", + ) + parameters: Optional[Dict[str, Any]] = Field(None, alias="$parameters") + + class FixedWindowCallRatePolicy(BaseModel): class Config: extra = Extra.allow diff --git a/airbyte_cdk/sources/declarative/parsers/model_to_component_factory.py b/airbyte_cdk/sources/declarative/parsers/model_to_component_factory.py index 9fd5d2d449..040ac4689f 100644 --- a/airbyte_cdk/sources/declarative/parsers/model_to_component_factory.py +++ b/airbyte_cdk/sources/declarative/parsers/model_to_component_factory.py @@ -2683,12 +2683,16 @@ def create_json_file_schema_loader( file_path=model.file_path or "", config=config, parameters=model.parameters or {} ) - @staticmethod def create_jwt_authenticator( - model: JwtAuthenticatorModel, config: Config, **kwargs: Any + self, model: JwtAuthenticatorModel, config: Config, **kwargs: Any ) -> JwtAuthenticator: jwt_headers = model.jwt_headers or JwtHeadersModel(kid=None, typ="JWT", cty=None) jwt_payload = model.jwt_payload or JwtPayloadModel(iss=None, sub=None, aud=None) + request_option = ( + self._create_component_from_model(model.request_option, config) + if model.request_option + else None + ) return JwtAuthenticator( config=config, parameters=model.parameters or {}, @@ -2706,6 +2710,7 @@ def create_jwt_authenticator( additional_jwt_headers=model.additional_jwt_headers, additional_jwt_payload=model.additional_jwt_payload, passphrase=model.passphrase, + request_option=request_option, ) def create_list_partition_router( diff --git a/poetry.lock b/poetry.lock index e03ece8a03..cb83c9b160 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.1.2 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.1.3 and should not be changed by hand. [[package]] name = "aiohappyeyeballs" @@ -369,30 +369,30 @@ files = [ [[package]] name = "cattrs" -version = "24.1.2" +version = "25.2.0" description = "Composable complex class support for attrs and dataclasses." optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["main", "dev"] files = [ - {file = "cattrs-24.1.2-py3-none-any.whl", hash = "sha256:67c7495b760168d931a10233f979b28dc04daf853b30752246f4f8471c6d68d0"}, - {file = "cattrs-24.1.2.tar.gz", hash = "sha256:8028cfe1ff5382df59dd36474a86e02d817b06eaf8af84555441bac915d2ef85"}, + {file = "cattrs-25.2.0-py3-none-any.whl", hash = "sha256:539d7eedee7d2f0706e4e109182ad096d608ba84633c32c75ef3458f1d11e8f1"}, + {file = "cattrs-25.2.0.tar.gz", hash = "sha256:f46c918e955db0177be6aa559068390f71988e877c603ae2e56c71827165cc06"}, ] [package.dependencies] -attrs = ">=23.1.0" +attrs = ">=24.3.0" exceptiongroup = {version = ">=1.1.1", markers = "python_version < \"3.11\""} -typing-extensions = {version = ">=4.1.0,<4.6.3 || >4.6.3", markers = "python_version < \"3.11\""} +typing-extensions = ">=4.12.2" [package.extras] bson = ["pymongo (>=4.4.0)"] cbor2 = ["cbor2 (>=5.4.6)"] msgpack = ["msgpack (>=1.0.5)"] -msgspec = ["msgspec (>=0.18.5) ; implementation_name == \"cpython\""] -orjson = ["orjson (>=3.9.2) ; implementation_name == \"cpython\""] +msgspec = ["msgspec (>=0.19.0) ; implementation_name == \"cpython\""] +orjson = ["orjson (>=3.10.7) ; implementation_name == \"cpython\""] pyyaml = ["pyyaml (>=6.0)"] tomlkit = ["tomlkit (>=0.11.8)"] -ujson = ["ujson (>=5.7.0)"] +ujson = ["ujson (>=5.10.0)"] [[package]] name = "certifi" @@ -988,22 +988,24 @@ tests = ["pytest", "pytest-cov", "pytest-xdist"] [[package]] name = "dagger-io" -version = "0.18.6" +version = "0.19.0" description = "A client package for running Dagger pipelines in Python." optional = false python-versions = ">=3.10" groups = ["dev"] files = [ - {file = "dagger_io-0.18.6-py3-none-any.whl", hash = "sha256:357e426ce42ec5b27da87a9e3145e6b5ba5fd02dbbad317ecc1ff09c1e1a44cd"}, - {file = "dagger_io-0.18.6.tar.gz", hash = "sha256:165c1c16165feb12cfdd89fce50c898e6ebc5a9acaaa0b725569317c404e45ce"}, + {file = "dagger_io-0.19.0-py3-none-any.whl", hash = "sha256:fa05ddd10ff622f54c3c929ea47a8b90f205305bd55a94f0794a05c5243c01e7"}, + {file = "dagger_io-0.19.0.tar.gz", hash = "sha256:e5e2adcaaf0bfe12dd119d0f15853132e273788c0dc925b98aec1b7c937b06d8"}, ] [package.dependencies] anyio = ">=3.6.2" beartype = ">=0.18.2" -cattrs = ">=24.1.0" -gql = {version = ">=3.5.0", extras = ["httpx"]} +cattrs = ">=25.1.0" +exceptiongroup = ">=1.3.0" +gql = {version = ">=4.0", extras = ["httpx"]} opentelemetry-exporter-otlp-proto-http = ">=1.23.0" +opentelemetry-instrumentation-logging = ">=0.54b1" opentelemetry-sdk = ">=1.23.0" platformdirs = ">=2.6.2" rich = ">=10.11.0" @@ -1247,16 +1249,19 @@ files = [ [[package]] name = "exceptiongroup" -version = "1.2.2" +version = "1.3.0" description = "Backport of PEP 654 (exception groups)" optional = false python-versions = ">=3.7" groups = ["main", "dev"] -markers = "python_version == \"3.10\"" files = [ - {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, - {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, + {file = "exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10"}, + {file = "exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88"}, ] +markers = {main = "python_version == \"3.10\""} + +[package.dependencies] +typing-extensions = {version = ">=4.6.0", markers = "python_version < \"3.13\""} [package.extras] test = ["pytest (>=6)"] @@ -3363,6 +3368,40 @@ opentelemetry-sdk = ">=1.36.0,<1.37.0" requests = ">=2.7,<3.0" typing-extensions = ">=4.5.0" +[[package]] +name = "opentelemetry-instrumentation" +version = "0.57b0" +description = "Instrumentation Tools & Auto Instrumentation for OpenTelemetry Python" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "opentelemetry_instrumentation-0.57b0-py3-none-any.whl", hash = "sha256:9109280f44882e07cec2850db28210b90600ae9110b42824d196de357cbddf7e"}, + {file = "opentelemetry_instrumentation-0.57b0.tar.gz", hash = "sha256:f2a30135ba77cdea2b0e1df272f4163c154e978f57214795d72f40befd4fcf05"}, +] + +[package.dependencies] +opentelemetry-api = ">=1.4,<2.0" +opentelemetry-semantic-conventions = "0.57b0" +packaging = ">=18.0" +wrapt = ">=1.0.0,<2.0.0" + +[[package]] +name = "opentelemetry-instrumentation-logging" +version = "0.57b0" +description = "OpenTelemetry Logging instrumentation" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "opentelemetry_instrumentation_logging-0.57b0-py3-none-any.whl", hash = "sha256:9f57d13bf776f6507f8ccff16a7aa6e6e753672708943925a794f16152504a04"}, + {file = "opentelemetry_instrumentation_logging-0.57b0.tar.gz", hash = "sha256:44dbeb8aaf643e95d7311b699b8d368b1ecdb8404f1563f19f2b3ce2b4c6a035"}, +] + +[package.dependencies] +opentelemetry-api = ">=1.12,<2.0" +opentelemetry-instrumentation = "0.57b0" + [[package]] name = "opentelemetry-proto" version = "1.36.0" @@ -6364,10 +6403,9 @@ tzdata = {version = ">=2020.1", markers = "sys_platform == \"win32\""} name = "wrapt" version = "1.17.3" description = "Module for decorators, wrappers and monkey patching." -optional = true +optional = false python-versions = ">=3.8" -groups = ["main"] -markers = "extra == \"manifest-server\"" +groups = ["main", "dev"] files = [ {file = "wrapt-1.17.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:88bbae4d40d5a46142e70d58bf664a89b6b4befaea7b2ecc14e03cedb8e06c04"}, {file = "wrapt-1.17.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e6b13af258d6a9ad602d57d889f83b9d5543acd471eee12eb51f5b01f8eb1bc2"}, @@ -6451,6 +6489,7 @@ files = [ {file = "wrapt-1.17.3-py3-none-any.whl", hash = "sha256:7171ae35d2c33d326ac19dd8facb1e82e5fd04ef8c6c0e394d7af55a55051c22"}, {file = "wrapt-1.17.3.tar.gz", hash = "sha256:f66eb08feaa410fe4eebd17f2a2c8e2e46d3476e9f8c783daa8e09e0faa666d0"}, ] +markers = {main = "extra == \"manifest-server\""} [[package]] name = "xlsxwriter" @@ -6606,4 +6645,4 @@ vector-db-based = ["cohere", "langchain", "openai", "tiktoken"] [metadata] lock-version = "2.1" python-versions = ">=3.10,<3.14" -content-hash = "f8d4c72b0acd484de56c2ec8f09c760949c2ce009103daec3c50b5963e72ea98" +content-hash = "2060a9ead9660b9ba830e169a437c169fcdad869a4eabb43ddf0fe900978059d" diff --git a/pyproject.toml b/pyproject.toml index e00ecb13fd..2288421164 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -120,7 +120,7 @@ types-python-dateutil = "^2.9.0.20241003" types-pyyaml = "^6.0.12.20240917" types-cachetools = "^5.5.0.20240820" deptry = "^0.23.0" -dagger-io = "0.18.6" +dagger-io = "0.19.0" [tool.poetry.extras] file-based = ["avro", "fastavro", "pyarrow", "unstructured", "pdf2image", "pdfminer.six", "unstructured.pytesseract", "pytesseract", "markdown", "python-calamine", "python-snappy"] diff --git a/unit_tests/sources/declarative/auth/test_jwt.py b/unit_tests/sources/declarative/auth/test_jwt.py index 4996e53880..de6d7ac321 100644 --- a/unit_tests/sources/declarative/auth/test_jwt.py +++ b/unit_tests/sources/declarative/auth/test_jwt.py @@ -13,6 +13,10 @@ from cryptography.hazmat.primitives.asymmetric import rsa from airbyte_cdk.sources.declarative.auth.jwt import JwtAuthenticator +from airbyte_cdk.sources.declarative.requesters.request_option import ( + RequestOption, + RequestOptionType, +) LOGGER = logging.getLogger(__name__) @@ -285,3 +289,106 @@ def test_get_signed_token_with_passphrase_protected_key(self): assert decoded_payload["iss"] == "test_issuer" assert "iat" in decoded_payload assert "exp" in decoded_payload + + @pytest.mark.parametrize( + "request_option, expected_request_key", + [ + pytest.param( + RequestOption( + inject_into=RequestOptionType.request_parameter, + field_name="custom_parameter", + parameters={}, + ), + "custom_parameter", + id="test_get_request_headers", + ), + pytest.param( + RequestOption( + inject_into=RequestOptionType.body_data, field_name="custom_body", parameters={} + ), + "custom_body", + id="test_get_request_headers", + ), + pytest.param( + RequestOption( + inject_into=RequestOptionType.body_json, field_name="custom_json", parameters={} + ), + "custom_json", + id="test_get_request_headers", + ), + ], + ) + def test_get_request_options(self, request_option, expected_request_key): + authenticator = JwtAuthenticator( + config={}, + parameters={}, + algorithm="HS256", + secret_key="test_key", + token_duration=1000, + iss="test_iss", + sub="test_sub", + aud="test_aud", + additional_jwt_payload={"kid": "test_kid"}, + request_option=request_option, + ) + + expected_request_options = { + expected_request_key: jwt.encode( + payload=authenticator._get_jwt_payload(), + key=authenticator._get_secret_key(), + algorithm=authenticator._algorithm, + headers=authenticator._get_jwt_headers(), + ) + } + + match request_option.inject_into: + case RequestOptionType.request_parameter: + actual_request_options = authenticator.get_request_params() + case RequestOptionType.body_data: + actual_request_options = authenticator.get_request_body_data() + case RequestOptionType.body_json: + actual_request_options = authenticator.get_request_body_json() + case _: + actual_request_options = None + + assert actual_request_options == expected_request_options + + @pytest.mark.parametrize( + "request_option, expected_header_key", + [ + pytest.param( + RequestOption( + inject_into=RequestOptionType.header, + field_name="custom_authorization", + parameters={}, + ), + "custom_authorization", + id="test_get_request_headers", + ), + pytest.param(None, "Authorization", id="test_with_default_authorization_header"), + ], + ) + def test_get_request_headers(self, request_option, expected_header_key): + authenticator = JwtAuthenticator( + config={}, + parameters={}, + algorithm="HS256", + secret_key="test_key", + token_duration=1000, + iss="test_iss", + sub="test_sub", + aud="test_aud", + additional_jwt_payload={"kid": "test_kid"}, + request_option=request_option, + ) + + expected_headers = { + expected_header_key: jwt.encode( + payload=authenticator._get_jwt_payload(), + key=authenticator._get_secret_key(), + algorithm=authenticator._algorithm, + headers=authenticator._get_jwt_headers(), + ) + } + + assert authenticator.get_auth_header() == expected_headers diff --git a/unit_tests/sources/declarative/parsers/test_model_to_component_factory.py b/unit_tests/sources/declarative/parsers/test_model_to_component_factory.py index dc33346502..4825a9a3fc 100644 --- a/unit_tests/sources/declarative/parsers/test_model_to_component_factory.py +++ b/unit_tests/sources/declarative/parsers/test_model_to_component_factory.py @@ -3034,6 +3034,10 @@ def test_create_custom_retriever(): aud: "test aud" additional_jwt_payload: test: "test custom payload" + request_option: + type: RequestOption + inject_into: body_json + field_name: authorization """, { "secret_key": "secret_key", @@ -3141,6 +3145,11 @@ def test_create_jwt_authenticator(config, manifest, expected): ) assert authenticator._get_jwt_payload() == jwt_payload + if authenticator_manifest.get("request_option"): + assert authenticator._request_option.inject_into.value == authenticator_manifest.get( + "request_option", {} + ).get("inject_into") + def test_use_request_options_provider_for_datetime_based_cursor(): config = {