diff --git a/airbyte-integrations/connectors/source-facebook-marketing/acceptance-test-config.yml b/airbyte-integrations/connectors/source-facebook-marketing/acceptance-test-config.yml index dae55df93f18f..c7cdf62e8bb7e 100644 --- a/airbyte-integrations/connectors/source-facebook-marketing/acceptance-test-config.yml +++ b/airbyte-integrations/connectors/source-facebook-marketing/acceptance-test-config.yml @@ -6,9 +6,10 @@ acceptance_tests: spec: tests: - spec_path: "integration_tests/spec.json" + # the source now supports 2 auth types, thus we should skip the check, + # this change is intentional backward_compatibility_tests_config: - disable_for_version: "1.2.2" - previous_connector_version: "1.2.1" + disable_for_version: "3.1.0" connection: tests: - config_path: "secrets/config.json" diff --git a/airbyte-integrations/connectors/source-facebook-marketing/integration_tests/spec.json b/airbyte-integrations/connectors/source-facebook-marketing/integration_tests/spec.json index 4566a1a627e7e..7e56eab74cf6d 100644 --- a/airbyte-integrations/connectors/source-facebook-marketing/integration_tests/spec.json +++ b/airbyte-integrations/connectors/source-facebook-marketing/integration_tests/spec.json @@ -26,6 +26,72 @@ "airbyte_secret": true, "type": "string" }, + "credentials": { + "title": "Authentication", + "description": "Credentials for connecting to the Facebook Marketing API", + "type": "object", + "discriminator": { + "propertyName": "auth_type", + "mapping": { + "Client": "#/definitions/OAuthCredentials", + "Service": "#/definitions/ServiceAccountCredentials" + } + }, + "oneOf": [ + { + "title": "Authenticate via Facebook Marketing (Oauth)", + "type": "object", + "properties": { + "auth_type": { + "title": "Auth Type", + "default": "Client", + "const": "Client", + "enum": ["Client"], + "type": "string" + }, + "client_id": { + "title": "Client ID", + "description": "Client ID for the Facebook Marketing API", + "airbyte_secret": true, + "type": "string" + }, + "client_secret": { + "title": "Client Secret", + "description": "Client Secret for the Facebook Marketing API", + "airbyte_secret": true, + "type": "string" + }, + "access_token": { + "title": "Access Token", + "description": "The value of the generated access token. From your App\u2019s Dashboard, click on \"Marketing API\" then \"Tools\". Select permissions ads_management, ads_read, read_insights, business_management. Then click on \"Get token\". See the docs for more information.", + "airbyte_secret": true, + "type": "string" + } + }, + "required": ["client_id", "client_secret", "auth_type"] + }, + { + "title": "Service Account Key Authentication", + "type": "object", + "properties": { + "auth_type": { + "title": "Auth Type", + "default": "Service", + "const": "Service", + "enum": ["Service"], + "type": "string" + }, + "access_token": { + "title": "Access Token", + "description": "The value of the generated access token. From your App\u2019s Dashboard, click on \"Marketing API\" then \"Tools\". Select permissions ads_management, ads_read, read_insights, business_management. Then click on \"Get token\". See the docs for more information.", + "airbyte_secret": true, + "type": "string" + } + }, + "required": ["access_token", "auth_type"] + } + ] + }, "start_date": { "title": "Start Date", "description": "The date from which you'd like to replicate data for all incremental streams, in the format YYYY-MM-DDT00:00:00Z. If not set then all data will be replicated for usual streams and only last 2 years for insight streams.", @@ -462,19 +528,21 @@ "type": "string" } }, - "required": ["account_ids", "access_token"] + "required": ["account_ids"] }, "supportsIncremental": true, "supported_destination_sync_modes": ["append"], "advanced_auth": { "auth_flow_type": "oauth2.0", + "predicate_key": ["credentials", "auth_type"], + "predicate_value": "Client", "oauth_config_specification": { "complete_oauth_output_specification": { "type": "object", "properties": { "access_token": { "type": "string", - "path_in_connector_config": ["access_token"] + "path_in_connector_config": ["credentials", "access_token"] } } }, @@ -495,11 +563,11 @@ "properties": { "client_id": { "type": "string", - "path_in_connector_config": ["client_id"] + "path_in_connector_config": ["credentials", "client_id"] }, "client_secret": { "type": "string", - "path_in_connector_config": ["client_secret"] + "path_in_connector_config": ["credentials", "client_secret"] } } } diff --git a/airbyte-integrations/connectors/source-facebook-marketing/metadata.yaml b/airbyte-integrations/connectors/source-facebook-marketing/metadata.yaml index 432dda39f892b..af996f249492a 100644 --- a/airbyte-integrations/connectors/source-facebook-marketing/metadata.yaml +++ b/airbyte-integrations/connectors/source-facebook-marketing/metadata.yaml @@ -10,7 +10,7 @@ data: connectorSubtype: api connectorType: source definitionId: e7778cfc-e97c-4458-9ecb-b4f2bba8946c - dockerImageTag: 3.1.0 + dockerImageTag: 3.2.0 dockerRepository: airbyte/source-facebook-marketing documentationUrl: https://docs.airbyte.com/integrations/sources/facebook-marketing githubIssueLabel: source-facebook-marketing diff --git a/airbyte-integrations/connectors/source-facebook-marketing/pyproject.toml b/airbyte-integrations/connectors/source-facebook-marketing/pyproject.toml index 68df9dfb0b75e..fb99288fdc3ca 100644 --- a/airbyte-integrations/connectors/source-facebook-marketing/pyproject.toml +++ b/airbyte-integrations/connectors/source-facebook-marketing/pyproject.toml @@ -3,7 +3,7 @@ requires = [ "poetry-core>=1.0.0",] build-backend = "poetry.core.masonry.api" [tool.poetry] -version = "3.1.0" +version = "3.2.0" name = "source-facebook-marketing" description = "Source implementation for Facebook Marketing." authors = [ "Airbyte ",] diff --git a/airbyte-integrations/connectors/source-facebook-marketing/sample_files/sample_config.json b/airbyte-integrations/connectors/source-facebook-marketing/sample_files/sample_config.json index f69ba65fd2672..2ca7d6392db08 100644 --- a/airbyte-integrations/connectors/source-facebook-marketing/sample_files/sample_config.json +++ b/airbyte-integrations/connectors/source-facebook-marketing/sample_files/sample_config.json @@ -2,5 +2,8 @@ "start_date": "2020-09-25T00:00:00Z", "end_date": "2021-01-01T00:00:00Z", "account_id": "", - "access_token": "" + "credentials": { + "auth_type": "Service", + "access_token": "" + } } diff --git a/airbyte-integrations/connectors/source-facebook-marketing/source_facebook_marketing/config_migrations.py b/airbyte-integrations/connectors/source-facebook-marketing/source_facebook_marketing/config_migrations.py index 8ca9289c97e28..67d54a939428a 100644 --- a/airbyte-integrations/connectors/source-facebook-marketing/source_facebook_marketing/config_migrations.py +++ b/airbyte-integrations/connectors/source-facebook-marketing/source_facebook_marketing/config_migrations.py @@ -121,3 +121,73 @@ def transform(cls, config: Mapping[str, Any]) -> Mapping[str, Any]: config[stream_filter] = statuses # return transformed config return config + + +class MigrateSecretsPathInConnector: + """ + This class stands for migrating the config at runtime. + This migration is intended for backwards compatibility with the previous version, so existing secrets configurations gets migrated to new path. + + Starting from `2.2.0`, the `client_id`, `client_secret` and `access_token` will be placed at `credentials` path. + """ + + @classmethod + def _should_migrate(cls, config: Mapping[str, Any]) -> bool: + """ + This method determines whether the config should be migrated to nest existing fields at credentials. + It is assumed if credentials does not exist on configuration, `client_id`, `client_secret` and `access_token` exists on root path. + Returns: + > True, if the migration is necessary + > False, otherwise. + """ + return "access_token" in config or "client_id" in config or "client_secret" in config + + @classmethod + def migrate(cls, args: List[str], source: Source) -> None: + """ + This method checks the input args, should the config be migrated, + transform if neccessary and emit the CONTROL message. + """ + # get config path + config_path = AirbyteEntrypoint(source).extract_config(args) + # proceed only if `--config` arg is provided + if config_path: + # read the existing config + config = source.read_config(config_path) + # migration check + if cls._should_migrate(config): + cls._emit_control_message( + cls._modify_and_save(config_path, source, config), + ) + + @classmethod + def _transform(cls, config: Mapping[str, Any]) -> Mapping[str, Any]: + # transform the config + if "credentials" not in config: + config["credentials"] = { + "auth_type": "Service", + } + if "access_token" in config: + config["credentials"]["access_token"] = config.pop("access_token") + if "client_id" in config: + config["credentials"]["auth_type"] = "Client" + config["credentials"]["client_id"] = config.pop("client_id") + if "client_secret" in config: + config["credentials"]["auth_type"] = "Client" + config["credentials"]["client_secret"] = config.pop("client_secret") + # return transformed config + return config + + @classmethod + def _modify_and_save(cls, config_path: str, source: Source, config: Mapping[str, Any]) -> Mapping[str, Any]: + # modify the config + migrated_config = cls._transform(config) + # save the config + source.write_config(migrated_config, config_path) + # return modified config + return migrated_config + + @classmethod + def _emit_control_message(cls, migrated_config: Mapping[str, Any]) -> None: + # add the Airbyte Control Message to message repo + print(create_connector_config_control_message(migrated_config).json(exclude_unset=True)) diff --git a/airbyte-integrations/connectors/source-facebook-marketing/source_facebook_marketing/run.py b/airbyte-integrations/connectors/source-facebook-marketing/source_facebook_marketing/run.py index b070561488f09..ecb5dcd5cbad6 100644 --- a/airbyte-integrations/connectors/source-facebook-marketing/source_facebook_marketing/run.py +++ b/airbyte-integrations/connectors/source-facebook-marketing/source_facebook_marketing/run.py @@ -7,7 +7,7 @@ from airbyte_cdk.entrypoint import launch -from .config_migrations import MigrateAccountIdToArray, MigrateIncludeDeletedToStatusFilters +from .config_migrations import MigrateAccountIdToArray, MigrateIncludeDeletedToStatusFilters, MigrateSecretsPathInConnector from .source import SourceFacebookMarketing @@ -15,4 +15,5 @@ def run(): source = SourceFacebookMarketing() MigrateAccountIdToArray.migrate(sys.argv[1:], source) MigrateIncludeDeletedToStatusFilters.migrate(sys.argv[1:], source) + MigrateSecretsPathInConnector.migrate(sys.argv[1:], source) launch(source, sys.argv[1:]) diff --git a/airbyte-integrations/connectors/source-facebook-marketing/source_facebook_marketing/source.py b/airbyte-integrations/connectors/source-facebook-marketing/source_facebook_marketing/source.py index 0e4744be48f29..811d3c41b84ac 100644 --- a/airbyte-integrations/connectors/source-facebook-marketing/source_facebook_marketing/source.py +++ b/airbyte-integrations/connectors/source-facebook-marketing/source_facebook_marketing/source.py @@ -95,7 +95,10 @@ def check_connection(self, logger: logging.Logger, config: Mapping[str, Any]) -> if config.start_date and config.end_date < config.start_date: return False, "End date must be equal or after start date." - api = API(access_token=config.access_token, page_size=config.page_size) + if config.credentials is not None: + api = API(access_token=config.credentials.access_token, page_size=config.page_size) + else: + api = API(access_token=config.access_token, page_size=config.page_size) for account_id in config.account_ids: # Get Ad Account to check creds @@ -129,7 +132,10 @@ def streams(self, config: Mapping[str, Any]) -> List[Type[Stream]]: config.start_date = validate_start_date(config.start_date) config.end_date = validate_end_date(config.start_date, config.end_date) - api = API(access_token=config.access_token, page_size=config.page_size) + if config.credentials is not None: + api = API(access_token=config.credentials.access_token, page_size=config.page_size) + else: + api = API(access_token=config.access_token, page_size=config.page_size) # if start_date not specified then set default start_date for report streams to 2 years ago report_start_date = config.start_date or pendulum.now().add(years=-2) @@ -242,14 +248,16 @@ def spec(self, *args, **kwargs) -> ConnectorSpecification: connectionSpecification=ConnectorConfig.schema(), advanced_auth=AdvancedAuth( auth_flow_type=AuthFlowType.oauth2_0, + predicate_key=["credentials", "auth_type"], + predicate_value="Client", oauth_config_specification=OAuthConfigSpecification( complete_oauth_output_specification={ "type": "object", "properties": { "access_token": { "type": "string", - "path_in_connector_config": ["access_token"], - } + "path_in_connector_config": ["credentials", "access_token"], + }, }, }, complete_oauth_server_input_specification={ @@ -265,11 +273,11 @@ def spec(self, *args, **kwargs) -> ConnectorSpecification: "properties": { "client_id": { "type": "string", - "path_in_connector_config": ["client_id"], + "path_in_connector_config": ["credentials", "client_id"], }, "client_secret": { "type": "string", - "path_in_connector_config": ["client_secret"], + "path_in_connector_config": ["credentials", "client_secret"], }, }, }, diff --git a/airbyte-integrations/connectors/source-facebook-marketing/source_facebook_marketing/spec.py b/airbyte-integrations/connectors/source-facebook-marketing/source_facebook_marketing/spec.py index 470312b4db04b..a578830ab3ad9 100644 --- a/airbyte-integrations/connectors/source-facebook-marketing/source_facebook_marketing/spec.py +++ b/airbyte-integrations/connectors/source-facebook-marketing/source_facebook_marketing/spec.py @@ -4,9 +4,10 @@ import logging from datetime import datetime, timezone from enum import Enum -from typing import List, Optional, Set +from typing import List, Literal, Optional, Set, Union from airbyte_cdk.sources.config import BaseConfig +from airbyte_cdk.utils.oneof_option_config import OneOfOptionConfig from facebook_business.adobjects.ad import Ad from facebook_business.adobjects.adset import AdSet from facebook_business.adobjects.adsinsights import AdsInsights @@ -31,6 +32,48 @@ EMPTY_PATTERN = "^$" +class OAuthCredentials(BaseModel): + class Config(OneOfOptionConfig): + title = "Authenticate via Facebook Marketing (Oauth)" + discriminator = "auth_type" + + auth_type: Literal["Client"] = Field("Client", const=True) + client_id: str = Field( + title="Client ID", + description="Client ID for the Facebook Marketing API", + airbyte_secret=True, + ) + client_secret: str = Field( + title="Client Secret", + description="Client Secret for the Facebook Marketing API", + airbyte_secret=True, + ) + access_token: Optional[str] = Field( + title="Access Token", + description="The value of the generated access token. " + 'From your App’s Dashboard, click on "Marketing API" then "Tools". ' + 'Select permissions ads_management, ads_read, read_insights, business_management. Then click on "Get token". ' + 'See the docs for more information.', + airbyte_secret=True, + ) + + +class ServiceAccountCredentials(BaseModel): + class Config(OneOfOptionConfig): + title = "Service Account Key Authentication" + discriminator = "auth_type" + + auth_type: Literal["Service"] = Field("Service", const=True) + access_token: str = Field( + title="Access Token", + description="The value of the generated access token. " + 'From your App’s Dashboard, click on "Marketing API" then "Tools". ' + 'Select permissions ads_management, ads_read, read_insights, business_management. Then click on "Get token". ' + 'See the docs for more information.', + airbyte_secret=True, + ) + + class InsightConfig(BaseModel): """Config for custom insights""" @@ -142,7 +185,7 @@ class Config: min_items=1, ) - access_token: str = Field( + access_token: Optional[str] = Field( title="Access Token", order=1, description=( @@ -154,6 +197,13 @@ class Config: airbyte_secret=True, ) + credentials: Optional[Union[OAuthCredentials, ServiceAccountCredentials]] = Field( + title="Authentication", + description="Credentials for connecting to the Facebook Marketing API", + discriminator="auth_type", + type="object", + ) + start_date: Optional[datetime] = Field( title="Start Date", order=2, diff --git a/airbyte-integrations/connectors/source-facebook-marketing/unit_tests/integration/config.py b/airbyte-integrations/connectors/source-facebook-marketing/unit_tests/integration/config.py index c8e65f5085b0a..1e76b6403a501 100644 --- a/airbyte-integrations/connectors/source-facebook-marketing/unit_tests/integration/config.py +++ b/airbyte-integrations/connectors/source-facebook-marketing/unit_tests/integration/config.py @@ -26,6 +26,10 @@ def __init__(self) -> None: self._config: MutableMapping[str, Any] = { "account_ids": [ACCOUNT_ID], "access_token": ACCESS_TOKEN, + "credentials": { + "auth_type": "Service", + "access_token": ACCESS_TOKEN, + }, "start_date": START_DATE, "end_date": END_DATE, "include_deleted": True, diff --git a/airbyte-integrations/connectors/source-facebook-marketing/unit_tests/test_config_migrations.py b/airbyte-integrations/connectors/source-facebook-marketing/unit_tests/test_config_migrations.py index 759eb6e8f8485..54bb48f02a1b9 100644 --- a/airbyte-integrations/connectors/source-facebook-marketing/unit_tests/test_config_migrations.py +++ b/airbyte-integrations/connectors/source-facebook-marketing/unit_tests/test_config_migrations.py @@ -10,7 +10,11 @@ import pytest from airbyte_cdk.models import OrchestratorType, Type from airbyte_cdk.sources import Source -from source_facebook_marketing.config_migrations import MigrateAccountIdToArray, MigrateIncludeDeletedToStatusFilters +from source_facebook_marketing.config_migrations import ( + MigrateAccountIdToArray, + MigrateIncludeDeletedToStatusFilters, + MigrateSecretsPathInConnector, +) from source_facebook_marketing.source import SourceFacebookMarketing # BASE ARGS @@ -19,6 +23,7 @@ _EXCLUDE_DELETE_CONFIGS_PATH = "test_migrations/include_deleted_to_status_filters/include_deleted_false" _INCLUDE_DELETE_CONFIGS_PATH = "test_migrations/include_deleted_to_status_filters/include_deleted_true" _ACCOUNT_ID_TO_ARRAY_CONFIGS_PATH = "test_migrations/account_id_to_array" +_SECRETS_TO_CREDENTIALS_CONFIGS_PATH = "test_migrations/secrets_to_credentials" def load_config(config_path: str) -> Mapping[str, Any]: @@ -162,3 +167,69 @@ def test_should_not_migrate_upgraded_config(self): new_config = load_config(self.UPGRADED_TEST_CONFIG_PATH) migration_instance = MigrateIncludeDeletedToStatusFilters() assert not migration_instance.should_migrate(new_config) + +class TestMigrateSecretsPathInConnector: + OLD_TEST_CONFIG_PATH_ACCESS_TOKEN = _config_path(f"{_SECRETS_TO_CREDENTIALS_CONFIGS_PATH}/test_old_access_token_config.json") + NEW_TEST_CONFIG_PATH_ACCESS_TOKEN = _config_path(f"{_SECRETS_TO_CREDENTIALS_CONFIGS_PATH}/test_new_access_token_config.json") + OLD_TEST_CONFIG_PATH_CLIENT = _config_path(f"{_SECRETS_TO_CREDENTIALS_CONFIGS_PATH}/test_old_client_config.json") + NEW_TEST_CONFIG_PATH_CLIENT = _config_path(f"{_SECRETS_TO_CREDENTIALS_CONFIGS_PATH}/test_new_client_config.json") + + @staticmethod + def revert_migration(config_path: str) -> None: + with open(config_path, "r") as test_config: + config = json.load(test_config) + credentials = config.pop("credentials",{}) + credentials.pop("auth_type", None) + with open(config_path, "w") as updated_config: + config = json.dumps({**config, **credentials}) + updated_config.write(config) + + def test_migrate_access_token_config(self): + migration_instance = MigrateSecretsPathInConnector() + original_config = load_config(self.OLD_TEST_CONFIG_PATH_ACCESS_TOKEN) + # migrate the test_config + migration_instance.migrate([CMD, "--config", self.OLD_TEST_CONFIG_PATH_ACCESS_TOKEN], SOURCE) + # load the updated config + test_migrated_config = load_config(self.OLD_TEST_CONFIG_PATH_ACCESS_TOKEN) + # check migrated property + assert "credentials" in test_migrated_config + assert isinstance(test_migrated_config["credentials"], dict) + credentials = test_migrated_config["credentials"] + assert "access_token" in credentials + # check the migration should be skipped, once already done + assert not migration_instance._should_migrate(test_migrated_config) + # load the old custom reports VS migrated + assert original_config["access_token"] == credentials["access_token"] + # revert the test_config to the starting point + self.revert_migration(self.OLD_TEST_CONFIG_PATH_ACCESS_TOKEN) + + def test_migrate_client_config(self): + migration_instance = MigrateSecretsPathInConnector() + original_config = load_config(self.OLD_TEST_CONFIG_PATH_CLIENT) + # migrate the test_config + migration_instance.migrate([CMD, "--config", self.OLD_TEST_CONFIG_PATH_CLIENT], SOURCE) + # load the updated config + test_migrated_config = load_config(self.OLD_TEST_CONFIG_PATH_CLIENT) + # check migrated property + assert "credentials" in test_migrated_config + assert isinstance(test_migrated_config["credentials"], dict) + credentials = test_migrated_config["credentials"] + assert "client_id" in credentials + assert "client_secret" in credentials + # check the migration should be skipped, once already done + assert not migration_instance._should_migrate(test_migrated_config) + # load the old custom reports VS migrated + assert original_config["client_id"] == credentials["client_id"] + assert original_config["client_secret"] == credentials["client_secret"] + # revert the test_config to the starting point + self.revert_migration(self.OLD_TEST_CONFIG_PATH_CLIENT) + + def test_should_not_migrate_new_client_config(self): + new_config = load_config(self.NEW_TEST_CONFIG_PATH_CLIENT) + migration_instance = MigrateSecretsPathInConnector() + assert not migration_instance._should_migrate(new_config) + + def test_should_not_migrate_new_access_token_config(self): + new_config = load_config(self.NEW_TEST_CONFIG_PATH_ACCESS_TOKEN) + migration_instance = MigrateSecretsPathInConnector() + assert not migration_instance._should_migrate(new_config) diff --git a/airbyte-integrations/connectors/source-facebook-marketing/unit_tests/test_migrations/secrets_to_credentials/test_new_access_token_config.json b/airbyte-integrations/connectors/source-facebook-marketing/unit_tests/test_migrations/secrets_to_credentials/test_new_access_token_config.json new file mode 100644 index 0000000000000..f735cac033a0a --- /dev/null +++ b/airbyte-integrations/connectors/source-facebook-marketing/unit_tests/test_migrations/secrets_to_credentials/test_new_access_token_config.json @@ -0,0 +1,17 @@ +{ + "start_date": "2021-02-08T00:00:00Z", + "end_date": "2021-02-15T00:00:00Z", + "custom_insights": [ + { + "name": "custom_insight_stream", + "fields": ["account_name", "clicks", "cpc", "account_id", "ad_id"], + "breakdowns": ["gender"], + "action_breakdowns": [] + } + ], + "account_ids": ["01234567890"], + "credentials": { + "auth_type": "Service", + "access_token": "access_token" + } +} diff --git a/airbyte-integrations/connectors/source-facebook-marketing/unit_tests/test_migrations/secrets_to_credentials/test_new_client_config.json b/airbyte-integrations/connectors/source-facebook-marketing/unit_tests/test_migrations/secrets_to_credentials/test_new_client_config.json new file mode 100644 index 0000000000000..6a30bdb8b82a3 --- /dev/null +++ b/airbyte-integrations/connectors/source-facebook-marketing/unit_tests/test_migrations/secrets_to_credentials/test_new_client_config.json @@ -0,0 +1,18 @@ +{ + "start_date": "2021-02-08T00:00:00Z", + "end_date": "2021-02-15T00:00:00Z", + "custom_insights": [ + { + "name": "custom_insight_stream", + "fields": ["account_name", "clicks", "cpc", "account_id", "ad_id"], + "breakdowns": ["gender"], + "action_breakdowns": [] + } + ], + "account_ids": ["01234567890"], + "credentials": { + "auth_type": "Client", + "client_id": "client_id", + "client_secret": "client_secret" + } +} diff --git a/airbyte-integrations/connectors/source-facebook-marketing/unit_tests/test_migrations/secrets_to_credentials/test_old_access_token_config.json b/airbyte-integrations/connectors/source-facebook-marketing/unit_tests/test_migrations/secrets_to_credentials/test_old_access_token_config.json new file mode 100644 index 0000000000000..b499ed5ab91a8 --- /dev/null +++ b/airbyte-integrations/connectors/source-facebook-marketing/unit_tests/test_migrations/secrets_to_credentials/test_old_access_token_config.json @@ -0,0 +1,15 @@ +{ + "start_date": "2021-02-08T00:00:00Z", + "end_date": "2021-02-15T00:00:00Z", + "custom_insights": [ + { + "name": "custom_insight_stream", + "fields": ["account_name", "clicks", "cpc", "account_id", "ad_id"], + "breakdowns": ["gender"], + "action_breakdowns": [] + } + ], + "account_id": "01234567890", + "auth_type": "Service", + "access_token": "access_token" +} diff --git a/airbyte-integrations/connectors/source-facebook-marketing/unit_tests/test_migrations/secrets_to_credentials/test_old_client_config.json b/airbyte-integrations/connectors/source-facebook-marketing/unit_tests/test_migrations/secrets_to_credentials/test_old_client_config.json new file mode 100644 index 0000000000000..e8434efbec9b7 --- /dev/null +++ b/airbyte-integrations/connectors/source-facebook-marketing/unit_tests/test_migrations/secrets_to_credentials/test_old_client_config.json @@ -0,0 +1,16 @@ +{ + "start_date": "2021-02-08T00:00:00Z", + "end_date": "2021-02-15T00:00:00Z", + "custom_insights": [ + { + "name": "custom_insight_stream", + "fields": ["account_name", "clicks", "cpc", "account_id", "ad_id"], + "breakdowns": ["gender"], + "action_breakdowns": [] + } + ], + "account_id": "01234567890", + "auth_type": "Client", + "client_id": "client_id", + "client_secret": "client_secret" +} diff --git a/airbyte-integrations/connectors/source-facebook-marketing/unit_tests/test_source.py b/airbyte-integrations/connectors/source-facebook-marketing/unit_tests/test_source.py index 202c1ce1fd67e..70d7653152943 100644 --- a/airbyte-integrations/connectors/source-facebook-marketing/unit_tests/test_source.py +++ b/airbyte-integrations/connectors/source-facebook-marketing/unit_tests/test_source.py @@ -28,7 +28,11 @@ def config_fixture(requests_mock): config = { "account_ids": ["123"], - "access_token": "TOKEN", + "access_token": "ACCESS_TOKEN", + "credentials": { + "auth_type": "Service", + "access_token": "ACCESS_TOKEN", + }, "start_date": "2019-10-10T00:00:00Z", "end_date": "2020-10-10T00:00:00Z", } diff --git a/docs/integrations/sources/facebook-marketing.md b/docs/integrations/sources/facebook-marketing.md index b9d69b7ea2347..df21cfb3c30eb 100644 --- a/docs/integrations/sources/facebook-marketing.md +++ b/docs/integrations/sources/facebook-marketing.md @@ -205,6 +205,7 @@ The Facebook Marketing connector uses the `lookback_window` parameter to repeate | Version | Date | Pull Request | Subject | |:--------|:-----------|:---------------------------------------------------------|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| 3.2.0 | 2024-06-05 | [37625](https://github.com/airbytehq/airbyte/pull/37625) | Source Facebook-Marketing: Add Selectable Auth | | 3.1.0 | 2024-06-01 | [38845](https://github.com/airbytehq/airbyte/pull/38845) | Update AdsInsights fields - removed `cost_per_conversion_lead` and `conversion_lead_rate` | | 3.0.0 | 2024-04-30 | [36608](https://github.com/airbytehq/airbyte/pull/36608) | Update `body_asset, call_to_action_asset, description_asset, image_asset, link_url_asset, title_asset, video_asset` breakdowns schema. | | 2.1.9 | 2024-05-17 | [38301](https://github.com/airbytehq/airbyte/pull/38301) | Fix data inaccuracies when `wish_bid` is requested |