Skip to content

Commit b18acd6

Browse files
🎉 Connector Facebook-Marketing: update insights streams with custom entries for fields, breakdowns and action_breakdowns (#7158)
* Connector Facebook-Marketing: update streams with custom streams * update: remove custom streams, add new custom insights from config * update: add new model for InsightConfig, remove old imports * fix: format to source file and streams file * update Changelog * update: add to check a validation to insights entries, update documentation and fix to resolve in spec schema * fix: fix import logger from entrypoint, change to python logger * fix: change error message from check custom insights entries, fix logger import Co-authored-by: vladimir <[email protected]>
1 parent 214b158 commit b18acd6

File tree

10 files changed

+283
-12
lines changed

10 files changed

+283
-12
lines changed

‎airbyte-config/init/src/main/resources/config/STANDARD_SOURCE_DEFINITION/e7778cfc-e97c-4458-9ecb-b4f2bba8946c.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"sourceDefinitionId": "e7778cfc-e97c-4458-9ecb-b4f2bba8946c",
33
"name": "Facebook Marketing",
44
"dockerRepository": "airbyte/source-facebook-marketing",
5-
"dockerImageTag": "0.2.20",
5+
"dockerImageTag": "0.2.21",
66
"documentationUrl": "https://docs.airbyte.io/integrations/sources/facebook-marketing",
77
"icon": "facebook.svg"
88
}

‎airbyte-config/init/src/main/resources/seed/source_definitions.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@
147147
- sourceDefinitionId: e7778cfc-e97c-4458-9ecb-b4f2bba8946c
148148
name: Facebook Marketing
149149
dockerRepository: airbyte/source-facebook-marketing
150-
dockerImageTag: 0.2.20
150+
dockerImageTag: 0.2.21
151151
documentationUrl: https://docs.airbyte.io/integrations/sources/facebook-marketing
152152
icon: facebook.svg
153153
sourceType: api

‎airbyte-integrations/connectors/source-facebook-marketing/Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,5 @@ RUN pip install .
1212
ENV AIRBYTE_ENTRYPOINT "python /airbyte/integration_code/main.py"
1313
ENTRYPOINT ["python", "/airbyte/integration_code/main.py"]
1414

15-
LABEL io.airbyte.version=0.2.20
15+
LABEL io.airbyte.version=0.2.21
1616
LABEL io.airbyte.name=airbyte/source-facebook-marketing

‎airbyte-integrations/connectors/source-facebook-marketing/integration_tests/spec.json

Lines changed: 86 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,9 +53,94 @@
5353
"minimum": 1,
5454
"maximum": 30,
5555
"type": "integer"
56+
},
57+
"custom_insights": {
58+
"title": "Custom Insights",
59+
"description": "A list wich contains insights entries, each entry must have a name and can contains fields, breakdowns or action_breakdowns)",
60+
"type": "array",
61+
"items": {
62+
"title": "InsightConfig",
63+
"type": "object",
64+
"properties": {
65+
"name": {
66+
"title": "Name",
67+
"description": "The name value of insight",
68+
"type": "string"
69+
},
70+
"fields": {
71+
"title": "Fields",
72+
"description": "A list of chosen fields for fields parameter",
73+
"default": [],
74+
"type": "array",
75+
"items": {
76+
"type": "string"
77+
}
78+
},
79+
"breakdowns": {
80+
"title": "Breakdowns",
81+
"description": "A list of chosen breakdowns for breakdowns",
82+
"default": [],
83+
"type": "array",
84+
"items": {
85+
"type": "string"
86+
}
87+
},
88+
"action_breakdowns": {
89+
"title": "Action Breakdowns",
90+
"description": "A list of chosen action_breakdowns for action_breakdowns",
91+
"default": [],
92+
"type": "array",
93+
"items": {
94+
"type": "string"
95+
}
96+
}
97+
},
98+
"required": ["name"]
99+
}
56100
}
57101
},
58-
"required": ["account_id", "access_token", "start_date"]
102+
"required": ["account_id", "access_token", "start_date"],
103+
"definitions": {
104+
"InsightConfig": {
105+
"title": "InsightConfig",
106+
"type": "object",
107+
"properties": {
108+
"name": {
109+
"title": "Name",
110+
"description": "The name value of insight",
111+
"type": "string"
112+
},
113+
"fields": {
114+
"title": "Fields",
115+
"description": "A list of chosen fields for fields parameter",
116+
"default": [],
117+
"type": "array",
118+
"items": {
119+
"type": "string"
120+
}
121+
},
122+
"breakdowns": {
123+
"title": "Breakdowns",
124+
"description": "A list of chosen breakdowns for breakdowns",
125+
"default": [],
126+
"type": "array",
127+
"items": {
128+
"type": "string"
129+
}
130+
},
131+
"action_breakdowns": {
132+
"title": "Action Breakdowns",
133+
"description": "A list of chosen action_breakdowns for action_breakdowns",
134+
"default": [],
135+
"type": "array",
136+
"items": {
137+
"type": "string"
138+
}
139+
}
140+
},
141+
"required": ["name"]
142+
}
143+
}
59144
},
60145
"supportsIncremental": true,
61146
"supported_destination_sync_modes": ["append"],

‎airbyte-integrations/connectors/source-facebook-marketing/source_facebook_marketing/api.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,16 @@
66
from time import sleep
77

88
import pendulum
9-
from airbyte_cdk.entrypoint import logger
9+
import logging
1010
from cached_property import cached_property
1111
from facebook_business import FacebookAdsApi
1212
from facebook_business.adobjects import user as fb_user
1313
from facebook_business.adobjects.adaccount import AdAccount
1414
from facebook_business.exceptions import FacebookRequestError
1515
from source_facebook_marketing.common import FacebookAPIException
1616

17+
logger = logging.getLogger(__name__)
18+
1719

1820
class MyFacebookAdsApi(FacebookAdsApi):
1921
"""Custom Facebook API class to intercept all API calls and handle call rate limits"""

‎airbyte-integrations/connectors/source-facebook-marketing/source_facebook_marketing/common.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
import backoff
99
import pendulum
10-
from airbyte_cdk.entrypoint import logger # FIXME (Eugene K): register logger as standard python logger
10+
import logging
1111
from facebook_business.exceptions import FacebookRequestError
1212

1313
# The Facebook API error codes indicating rate-limiting are listed at
@@ -16,6 +16,8 @@
1616
FACEBOOK_UNKNOWN_ERROR_CODE = 99
1717
DEFAULT_SLEEP_INTERVAL = pendulum.duration(minutes=1)
1818

19+
logger = logging.getLogger(__name__)
20+
1921

2022
class FacebookAPIException(Exception):
2123
"""General class for all API errors"""
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
{
2+
"properties": {
3+
"action_device": {"type": ["null", "string"]},
4+
"action_canvas_component_name": {"type": ["null", "string"]},
5+
"action_carousel_card_id": {"type": ["null", "string"]},
6+
"action_carousel_card_name": {"type": ["null", "string"]},
7+
"action_destination": {"type": ["null", "string"]},
8+
"action_reaction": {"type": ["null", "string"]},
9+
"action_target_id": {"type": ["null", "string"]},
10+
"action_type": {"type": ["null", "string"]},
11+
"action_video_sound": {"type": ["null", "string"]},
12+
"action_video_type": {"type": ["null", "string"]},
13+
"ad_format_asset": {"type": ["null", "string"]},
14+
"age": {"type": ["null", "string"]},
15+
"app_id": {"type": ["null", "string"]},
16+
"body_asset": {"type": ["null", "string"]},
17+
"call_to_action_asset": {"type": ["null", "string"]},
18+
"country": {"type": ["null", "string"]},
19+
"description_asset": {"type": ["null", "string"]},
20+
"device_platform": {"type": ["null", "string"]},
21+
"dma": {"type": ["null", "string"]},
22+
"frequency_value": {"type": ["null", "string"]},
23+
"gender": {"type": ["null", "string"]},
24+
"hourly_stats_aggregated_by_advertiser_time_zone": {"type": ["null", "string"]},
25+
"hourly_stats_aggregated_by_audience_time_zone": {"type": ["null", "string"]},
26+
"image_asset": {"type": ["null", "string"]},
27+
"impression_device": {"type": ["null", "string"]},
28+
"link_url_asset": {"type": ["null", "string"]},
29+
"place_page_id": {"type": ["null", "string"]},
30+
"platform_position": {"type": ["null", "string"]},
31+
"product_id": {"type": ["null", "string"]},
32+
"publisher_platform": {"type": ["null", "string"]},
33+
"region": {"type": ["null", "string"]},
34+
"skan_conversion_id": {"type": ["null", "string"]},
35+
"title_asset": {"type": ["null", "string"]},
36+
"video_asset": {"type": ["null", "string"]}
37+
}
38+
}

‎airbyte-integrations/connectors/source-facebook-marketing/source_facebook_marketing/source.py

Lines changed: 115 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,22 @@
22
# Copyright (c) 2021 Airbyte, Inc., all rights reserved.
33
#
44

5+
import logging
56
from datetime import datetime
7+
from typing import Any, List, Mapping, Optional, Tuple, Type, MutableMapping
8+
from jsonschema import RefResolver
9+
10+
11+
from airbyte_cdk.logger import AirbyteLogger
12+
from airbyte_cdk.models import AirbyteConnectionStatus, AuthSpecification, ConnectorSpecification, DestinationSyncMode, OAuth2Specification, Status
13+
614
from typing import Any, List, Mapping, Optional, Tuple, Type
715

816
import pendulum
9-
from airbyte_cdk.models import AuthSpecification, ConnectorSpecification, DestinationSyncMode, OAuth2Specification
1017
from airbyte_cdk.sources import AbstractSource
1118
from airbyte_cdk.sources.streams import Stream
19+
from airbyte_cdk.sources.streams.core import package_name_from_class
20+
from airbyte_cdk.sources.utils.schema_helpers import ResourceSchemaLoader
1221
from pydantic import BaseModel, Field
1322
from source_facebook_marketing.api import API
1423
from source_facebook_marketing.streams import (
@@ -26,6 +35,19 @@
2635
)
2736

2837

38+
logger = logging.getLogger(__name__)
39+
40+
class InsightConfig(BaseModel):
41+
42+
name: str = Field(description="The name value of insight")
43+
44+
fields: Optional[List[str]] = Field(description="A list of chosen fields for fields parameter", default=[])
45+
46+
breakdowns: Optional[List[str]] = Field(description="A list of chosen breakdowns for breakdowns", default=[])
47+
48+
action_breakdowns: Optional[List[str]] = Field(description="A list of chosen action_breakdowns for action_breakdowns", default=[])
49+
50+
2951
class ConnectorConfig(BaseModel):
3052
class Config:
3153
title = "Source Facebook Marketing"
@@ -65,6 +87,9 @@ class Config:
6587
minimum=1,
6688
maximum=30,
6789
)
90+
custom_insights: Optional[List[InsightConfig]] = Field(
91+
description="A list wich contains insights entries, each entry must have a name and can contains fields, breakdowns or action_breakdowns)"
92+
)
6893

6994

7095
class SourceFacebookMarketing(AbstractSource):
@@ -104,10 +129,11 @@ def streams(self, config: Mapping[str, Any]) -> List[Type[Stream]]:
104129
days_per_job=config.insights_days_per_job,
105130
)
106131

107-
return [
132+
streams = [
108133
Campaigns(api=api, start_date=config.start_date, end_date=config.end_date, include_deleted=config.include_deleted),
109134
AdSets(api=api, start_date=config.start_date, end_date=config.end_date, include_deleted=config.include_deleted),
110135
Ads(api=api, start_date=config.start_date, end_date=config.end_date, include_deleted=config.include_deleted),
136+
111137
AdCreatives(api=api),
112138
AdsInsights(**insights_args),
113139
AdsInsightsAgeAndGender(**insights_args),
@@ -118,6 +144,22 @@ def streams(self, config: Mapping[str, Any]) -> List[Type[Stream]]:
118144
AdsInsightsActionType(**insights_args),
119145
]
120146

147+
return self._update_insights_streams(insights=config.custom_insights, args=insights_args, streams=streams)
148+
149+
def check(self, logger: AirbyteLogger, config: Mapping[str, Any]) -> AirbyteConnectionStatus:
150+
"""Implements the Check Connection operation from the Airbyte Specification. See https://docs.airbyte.io/architecture/airbyte-specification."""
151+
try:
152+
check_succeeded, error = self.check_connection(logger, config)
153+
if not check_succeeded:
154+
return AirbyteConnectionStatus(status=Status.FAILED, message=repr(error))
155+
except Exception as e:
156+
return AirbyteConnectionStatus(status=Status.FAILED, message=repr(e))
157+
158+
self._check_custom_insights_entries(config.get('custom_insights', []))
159+
160+
return AirbyteConnectionStatus(status=Status.SUCCEEDED)
161+
162+
121163
def spec(self, *args, **kwargs) -> ConnectorSpecification:
122164
"""
123165
Returns the spec for this integration. The spec is a JSON-Schema object describing the required configurations (e.g: username and password)
@@ -128,11 +170,80 @@ def spec(self, *args, **kwargs) -> ConnectorSpecification:
128170
changelogUrl="https://docs.airbyte.io/integrations/sources/facebook-marketing",
129171
supportsIncremental=True,
130172
supported_destination_sync_modes=[DestinationSyncMode.append],
131-
connectionSpecification=ConnectorConfig.schema(),
173+
connectionSpecification=expand_local_ref(ConnectorConfig.schema()),
132174
authSpecification=AuthSpecification(
133175
auth_type="oauth2.0",
134176
oauth2Specification=OAuth2Specification(
135177
rootObject=[], oauthFlowInitParameters=[], oauthFlowOutputParameters=[["access_token"]]
136-
),
178+
)
137179
),
138180
)
181+
182+
def _update_insights_streams(self, insights, args, streams) -> List[Type[Stream]]:
183+
"""Update method, if insights have values returns streams replacing the
184+
default insights streams else returns streams
185+
186+
"""
187+
if not insights:
188+
return streams
189+
190+
insights_custom_streams = list()
191+
192+
for insight in insights:
193+
args["name"] = f"Custom{insight.name}"
194+
args["fields"] = list(set(insight.fields))
195+
args["breakdowns"] = list(set(insight.breakdowns))
196+
args["action_breakdowns"] = list(set(insight.action_breakdowns))
197+
insight_stream = AdsInsights(**args)
198+
insights_custom_streams.append(insight_stream)
199+
200+
return streams + insights_custom_streams
201+
202+
def _check_custom_insights_entries(self, insights: List[Mapping[str, Any]]):
203+
204+
default_fields = list(ResourceSchemaLoader(package_name_from_class(self.__class__)).get_schema("ads_insights").get("properties", {}).keys())
205+
default_breakdowns = list(ResourceSchemaLoader(package_name_from_class(self.__class__)).get_schema("ads_insights_breakdowns").get("properties", {}).keys())
206+
default_actions_breakdowns = [e for e in default_breakdowns if 'action_' in e]
207+
208+
for insight in insights:
209+
if insight.get('fields'):
210+
value_checked, value = self._check_values(default_fields, insight.get('fields'))
211+
if not value_checked:
212+
message = f"{value} is not a valid field name"
213+
raise Exception("Config validation error: " + message) from None
214+
if insight.get('breakdowns'):
215+
value_checked, value = self._check_values(default_breakdowns, insight.get('breakdowns'))
216+
if not value_checked:
217+
message = f"{value} is not a valid breakdown name"
218+
raise Exception("Config validation error: " + message) from None
219+
if insight.get('action_breakdowns'):
220+
value_checked, value = self._check_values(default_actions_breakdowns, insight.get('action_breakdowns'))
221+
if not value_checked:
222+
message = f"{value} is not a valid action_breakdown name"
223+
raise Exception("Config validation error: " + message) from None
224+
225+
return True
226+
227+
def _check_values(self, default_value: List[str], custom_value: List[str]) -> Tuple[bool, Any]:
228+
for e in custom_value:
229+
if e not in default_value:
230+
logger.error(f"{e} does not appear in {default_value}")
231+
return False, e
232+
233+
return True, None
234+
235+
236+
def expand_local_ref(schema, resolver=None, **kwargs):
237+
resolver = resolver or RefResolver("", schema)
238+
if isinstance(schema, MutableMapping):
239+
if "$ref" in schema:
240+
ref_url = schema.pop("$ref")
241+
url, resolved_schema = resolver.resolve(ref_url)
242+
schema.update(resolved_schema)
243+
for key, value in schema.items():
244+
schema[key] = expand_local_ref(value, resolver=resolver)
245+
return schema
246+
elif isinstance(schema, List):
247+
return [expand_local_ref(item, resolver=resolver) for item in schema]
248+
249+
return schema

0 commit comments

Comments
 (0)