Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Source Retently - replace custom authenticator with SelectiveAuthenticator #33536

Open
wants to merge 20 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
#
# Copyright (c) 2023 Airbyte, Inc., all rights reserved.
#

from dataclasses import dataclass
from typing import Any, List, Mapping

import dpath
from airbyte_cdk.sources.declarative.auth.declarative_authenticator import DeclarativeAuthenticator


@dataclass
class SelectiveAuthenticator(DeclarativeAuthenticator):
"""Authenticator that selects concrete implementation based on specific config value."""

config: Mapping[str, Any]
authenticators: Mapping[str, DeclarativeAuthenticator]
authenticator_selection_path: List[str]

# returns "DeclarativeAuthenticator", but must return a subtype of "SelectiveAuthenticator"
def __new__( # type: ignore[misc]
cls,
config: Mapping[str, Any],
authenticators: Mapping[str, DeclarativeAuthenticator],
authenticator_selection_path: List[str],
*arg: Any,
**kwargs: Any,
) -> DeclarativeAuthenticator:
try:
selected_key = str(dpath.util.get(config, authenticator_selection_path))
except KeyError as err:
raise ValueError("The path from `authenticator_selection_path` is not found in the config.") from err

try:
return authenticators[selected_key]
except KeyError as err:
raise ValueError(f"The authenticator `{selected_key}` is not found.") from err
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,49 @@ definitions:
$parameters:
type: object
additionalProperties: true
SelectiveAuthenticator:
title: Selective Authenticator
description: Authenticator that selects concrete authenticator based on config property.
type: object
additionalProperties: true
required:
- type
- authenticators
- authenticator_selection_path
properties:
type:
type: string
enum: [SelectiveAuthenticator]
authenticator_selection_path:
title: Authenticator Selection Path
description: Path of the field in config with selected authenticator name
type: array
items:
type: string
examples:
- ["auth"]
- ["auth", "type"]
authenticators:
title: Authenticators
description: Authenticators to select from.
type: object
additionalProperties:
anyOf:
- "$ref": "#/definitions/ApiKeyAuthenticator"
- "$ref": "#/definitions/BasicHttpAuthenticator"
- "$ref": "#/definitions/BearerAuthenticator"
- "$ref": "#/definitions/CustomAuthenticator"
- "$ref": "#/definitions/OAuthAuthenticator"
- "$ref": "#/definitions/NoAuth"
- "$ref": "#/definitions/SessionTokenAuthenticator"
- "$ref": "#/definitions/LegacySessionTokenAuthenticator"
examples:
- authenticators:
token: "#/definitions/ApiKeyAuthenticator"
oauth: "#/definitions/OAuthAuthenticator"
$parameters:
type: object
additionalProperties: true
CheckStream:
title: Streams to Check
description: Defines the streams to try reading when running a check operation.
Expand Down Expand Up @@ -1149,6 +1192,7 @@ definitions:
- "$ref": "#/definitions/NoAuth"
- "$ref": "#/definitions/SessionTokenAuthenticator"
- "$ref": "#/definitions/LegacySessionTokenAuthenticator"
- "$ref": "#/definitions/SelectiveAuthenticator"
error_handler:
title: Error Handler
description: Error handler component that defines how to handle errors.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1069,6 +1069,45 @@ class Config:
)


class SelectiveAuthenticator(BaseModel):
class Config:
extra = Extra.allow

type: Literal['SelectiveAuthenticator']
authenticator_selection_path: List[str] = Field(
...,
description='Path of the field in config with selected authenticator name',
examples=[['auth'], ['auth', 'type']],
title='Authenticator Selection Path',
)
authenticators: Dict[
str,
Union[
ApiKeyAuthenticator,
BasicHttpAuthenticator,
BearerAuthenticator,
CustomAuthenticator,
OAuthAuthenticator,
NoAuth,
SessionTokenAuthenticator,
LegacySessionTokenAuthenticator,
],
] = Field(
...,
description='Authenticators to select from.',
examples=[
{
'authenticators': {
'token': '#/definitions/ApiKeyAuthenticator',
'oauth': '#/definitions/OAuthAuthenticator',
}
}
],
title='Authenticators',
)
parameters: Optional[Dict[str, Any]] = Field(None, alias='$parameters')


class DeclarativeStream(BaseModel):
class Config:
extra = Extra.allow
Expand Down Expand Up @@ -1179,6 +1218,7 @@ class HttpRequester(BaseModel):
NoAuth,
SessionTokenAuthenticator,
LegacySessionTokenAuthenticator,
SelectiveAuthenticator,
]
] = Field(
None,
Expand Down Expand Up @@ -1313,6 +1353,7 @@ class SubstreamPartitionRouter(BaseModel):

CompositeErrorHandler.update_forward_refs()
DeclarativeSource.update_forward_refs()
SelectiveAuthenticator.update_forward_refs()
DeclarativeStream.update_forward_refs()
SessionTokenAuthenticator.update_forward_refs()
SimpleRetriever.update_forward_refs()
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@

from airbyte_cdk.models import Level
from airbyte_cdk.sources.declarative.auth import DeclarativeOauth2Authenticator
from airbyte_cdk.sources.declarative.auth.declarative_authenticator import NoAuth
from airbyte_cdk.sources.declarative.auth.declarative_authenticator import DeclarativeAuthenticator, NoAuth
from airbyte_cdk.sources.declarative.auth.oauth import DeclarativeSingleUseRefreshTokenOauth2Authenticator
from airbyte_cdk.sources.declarative.auth.selective_authenticator import SelectiveAuthenticator
from airbyte_cdk.sources.declarative.auth.token import (
ApiKeyAuthenticator,
BasicHttpAuthenticator,
Expand Down Expand Up @@ -76,6 +77,7 @@
from airbyte_cdk.sources.declarative.models.declarative_component_schema import RemoveFields as RemoveFieldsModel
from airbyte_cdk.sources.declarative.models.declarative_component_schema import RequestOption as RequestOptionModel
from airbyte_cdk.sources.declarative.models.declarative_component_schema import RequestPath as RequestPathModel
from airbyte_cdk.sources.declarative.models.declarative_component_schema import SelectiveAuthenticator as SelectiveAuthenticatorModel
from airbyte_cdk.sources.declarative.models.declarative_component_schema import SessionTokenAuthenticator as SessionTokenAuthenticatorModel
from airbyte_cdk.sources.declarative.models.declarative_component_schema import SimpleRetriever as SimpleRetrieverModel
from airbyte_cdk.sources.declarative.models.declarative_component_schema import Spec as SpecModel
Expand Down Expand Up @@ -187,6 +189,7 @@ def _init_mappings(self) -> None:
RequestPathModel: self.create_request_path,
RequestOptionModel: self.create_request_option,
LegacySessionTokenAuthenticatorModel: self.create_legacy_session_token_authenticator,
SelectiveAuthenticatorModel: self.create_selective_authenticator,
SimpleRetrieverModel: self.create_simple_retriever,
SpecModel: self.create_spec,
SubstreamPartitionRouterModel: self.create_substream_partition_router,
Expand Down Expand Up @@ -711,6 +714,8 @@ def create_http_requester(self, model: HttpRequesterModel, config: Config, *, na
model.http_method if isinstance(model.http_method, str) else model.http_method.value if model.http_method is not None else "GET"
)

assert model.use_cache is not None # for mypy

return HttpRequester(
name=name,
url_base=model.url_base,
Expand Down Expand Up @@ -896,6 +901,16 @@ def create_record_selector(
def create_remove_fields(model: RemoveFieldsModel, config: Config, **kwargs: Any) -> RemoveFields:
return RemoveFields(field_pointers=model.field_pointers, parameters={})

def create_selective_authenticator(self, model: SelectiveAuthenticatorModel, config: Config, **kwargs: Any) -> DeclarativeAuthenticator:
authenticators = {name: self._create_component_from_model(model=auth, config=config) for name, auth in model.authenticators.items()}
# SelectiveAuthenticator will return instance of DeclarativeAuthenticator or raise ValueError error
return SelectiveAuthenticator( # type: ignore[abstract]
config=config,
authenticators=authenticators,
authenticator_selection_path=model.authenticator_selection_path,
**kwargs,
)

@staticmethod
def create_legacy_session_token_authenticator(
model: LegacySessionTokenAuthenticatorModel, config: Config, *, url_base: str, **kwargs: Any
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
#
# Copyright (c) 2023 Airbyte, Inc., all rights reserved.
#

import pytest
from airbyte_cdk.sources.declarative.auth.selective_authenticator import SelectiveAuthenticator


def test_authenticator_selected(mocker):
authenticators = {"one": mocker.Mock(), "two": mocker.Mock()}
auth = SelectiveAuthenticator(
config={"auth": {"type": "one"}},
authenticators=authenticators,
authenticator_selection_path=["auth", "type"],
)

assert auth is authenticators["one"]


def test_selection_path_not_found(mocker):
authenticators = {"one": mocker.Mock(), "two": mocker.Mock()}

with pytest.raises(ValueError, match="The path from `authenticator_selection_path` is not found in the config"):
_ = SelectiveAuthenticator(
config={"auth": {"type": "one"}},
authenticators=authenticators,
authenticator_selection_path=["auth_type"],
)


def test_selected_auth_not_found(mocker):
authenticators = {"one": mocker.Mock(), "two": mocker.Mock()}

with pytest.raises(ValueError, match="The authenticator `unknown` is not found"):
_ = SelectiveAuthenticator(
config={"auth": {"type": "unknown"}},
authenticators=authenticators,
authenticator_selection_path=["auth", "type"],
)
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
from airbyte_cdk.sources.declarative.models import SubstreamPartitionRouter as SubstreamPartitionRouterModel
from airbyte_cdk.sources.declarative.models.declarative_component_schema import OffsetIncrement as OffsetIncrementModel
from airbyte_cdk.sources.declarative.models.declarative_component_schema import PageIncrement as PageIncrementModel
from airbyte_cdk.sources.declarative.models.declarative_component_schema import SelectiveAuthenticator
from airbyte_cdk.sources.declarative.parsers.manifest_component_transformer import ManifestComponentTransformer
from airbyte_cdk.sources.declarative.parsers.manifest_reference_resolver import ManifestReferenceResolver
from airbyte_cdk.sources.declarative.parsers.model_to_component_factory import ModelToComponentFactory
Expand Down Expand Up @@ -936,6 +937,50 @@ def test_create_request_with_session_authenticator():
}


@pytest.mark.parametrize("input_config, expected_authenticator_class", [
pytest.param(
{"auth": {"type": "token"}, "credentials": {"api_key": "some_key"}},
ApiKeyAuthenticator,
id="test_create_requester_with_selective_authenticator_and_token_selected",
),
pytest.param(
{"auth": {"type": "oauth"}, "credentials": {"client_id": "ABC"}},
DeclarativeOauth2Authenticator,
id="test_create_requester_with_selective_authenticator_and_oauth_selected",
),
]
)
def test_create_requester_with_selective_authenticator(input_config, expected_authenticator_class):
content = """
authenticator:
type: SelectiveAuthenticator
authenticator_selection_path:
- auth
- type
authenticators:
token:
type: ApiKeyAuthenticator
header: "Authorization"
api_token: "api_key={{ config['credentials']['api_key'] }}"
oauth:
type: OAuthAuthenticator
token_refresh_endpoint: https://api.url.com
client_id: "{{ config['credentials']['client_id'] }}"
client_secret: some_secret
refresh_token: some_token
"""
name = "name"
parsed_manifest = YamlDeclarativeSource._parse(content)
resolved_manifest = resolver.preprocess_manifest(parsed_manifest)
authenticator_manifest = transformer.propagate_types_and_parameters("", resolved_manifest["authenticator"], {})

authenticator = factory.create_component(
model_type=SelectiveAuthenticator, component_definition=authenticator_manifest, config=input_config, name=name
)

assert isinstance(authenticator, expected_authenticator_class)


def test_create_composite_error_handler():
content = """
error_handler:
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,13 @@ definitions:
url_base: "https://app.retently.com/api/v2/"
http_method: "GET"
authenticator:
class_name: source_retently.components.AuthenticatorRetently
api_auth: "#/definitions/api_authenticator"
oauth: "#/definitions/oauth_authenticator"
type: SelectiveAuthenticator
authenticator_selection_path:
- auth
- type
authenticators:
token: "#/definitions/api_authenticator"
oauth: "#/definitions/oauth_authenticator"

retriever:
type: SimpleRetriever
Expand Down
Loading