Skip to content

Commit eee4eba

Browse files
authored
Added Facebook Ads Operator #7887 (#8008)
1 parent 8cae07e commit eee4eba

File tree

31 files changed

+761
-24
lines changed

31 files changed

+761
-24
lines changed

CONTRIBUTING.rst

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -316,11 +316,11 @@ This is the full list of those extras:
316316
.. START EXTRAS HERE
317317
318318
all, all_dbs, async, atlas, aws, azure, cassandra, celery, cgroups, cloudant, dask, databricks,
319-
datadog, devel, devel_ci, devel_hadoop, doc, docker, druid, elasticsearch, exasol, gcp, gcp_api,
320-
github_enterprise, google_auth, grpc, hashicorp, hdfs, hive, jdbc, jira, kerberos, kubernetes, ldap,
321-
mongo, mssql, mysql, odbc, oracle, pagerduty, papermill, password, pinot, postgres, presto, qds,
322-
rabbitmq, redis, salesforce, samba, segment, sendgrid, sentry, singularity, slack, snowflake, ssh,
323-
statsd, tableau, vertica, virtualenv, webhdfs, winrm, yandexcloud
319+
datadog, devel, devel_ci, devel_hadoop, doc, docker, druid, elasticsearch, exasol, facebook, gcp,
320+
gcp_api, github_enterprise, google_auth, grpc, hashicorp, hdfs, hive, jdbc, jira, kerberos,
321+
kubernetes, ldap, mongo, mssql, mysql, odbc, oracle, pagerduty, papermill, password, pinot,
322+
postgres, presto, qds, rabbitmq, redis, salesforce, samba, segment, sendgrid, sentry, singularity,
323+
slack, snowflake, ssh, statsd, tableau, vertica, virtualenv, webhdfs, winrm, yandexcloud
324324

325325
.. END EXTRAS HERE
326326
@@ -457,7 +457,7 @@ apache.hive amazon,microsoft.mssql,mysql,presto,samba,vertica
457457
apache.livy http
458458
dingding http
459459
discord http
460-
google amazon,apache.cassandra,cncf.kubernetes,microsoft.azure,microsoft.mssql,mysql,postgres,presto,sftp
460+
google amazon,apache.cassandra,cncf.kubernetes,facebook,microsoft.azure,microsoft.mssql,mysql,postgres,presto,sftp
461461
hashicorp google
462462
microsoft.azure oracle
463463
microsoft.mssql odbc

INSTALL

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,11 +45,11 @@ pip install . --constraint requirements/requirements-python3.7.txt
4545
# START EXTRAS HERE
4646

4747
all, all_dbs, async, atlas, aws, azure, cassandra, celery, cgroups, cloudant, dask, databricks,
48-
datadog, devel, devel_ci, devel_hadoop, doc, docker, druid, elasticsearch, exasol, gcp, gcp_api,
49-
github_enterprise, google_auth, grpc, hashicorp, hdfs, hive, jdbc, jira, kerberos, kubernetes, ldap,
50-
mongo, mssql, mysql, odbc, oracle, pagerduty, papermill, password, pinot, postgres, presto, qds,
51-
rabbitmq, redis, salesforce, samba, segment, sendgrid, sentry, singularity, slack, snowflake, ssh,
52-
statsd, tableau, vertica, virtualenv, webhdfs, winrm, yandexcloud
48+
datadog, devel, devel_ci, devel_hadoop, doc, docker, druid, elasticsearch, exasol, facebook, gcp,
49+
gcp_api, github_enterprise, google_auth, grpc, hashicorp, hdfs, hive, jdbc, jira, kerberos,
50+
kubernetes, ldap, mongo, mssql, mysql, odbc, oracle, pagerduty, papermill, password, pinot,
51+
postgres, presto, qds, rabbitmq, redis, salesforce, samba, segment, sendgrid, sentry, singularity,
52+
slack, snowflake, ssh, statsd, tableau, vertica, virtualenv, webhdfs, winrm, yandexcloud
5353

5454
# END EXTRAS HERE
5555

airflow/models/connection.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ class Connection(Base, LoggingMixin):
123123
('docker', 'Docker Registry'),
124124
('elasticsearch', 'Elasticsearch'),
125125
('exasol', 'Exasol'),
126+
('facebook_social', 'Facebook Social'),
126127
('fs', 'File (path)'),
127128
('ftp', 'FTP'),
128129
('google_cloud_platform', 'Google Cloud Platform'),

airflow/providers/dependencies.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
"amazon",
3232
"apache.cassandra",
3333
"cncf.kubernetes",
34+
"facebook",
3435
"microsoft.azure",
3536
"microsoft.mssql",
3637
"mysql",
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# Licensed to the Apache Software Foundation (ASF) under one
2+
# or more contributor license agreements. See the NOTICE file
3+
# distributed with this work for additional information
4+
# regarding copyright ownership. The ASF licenses this file
5+
# to you under the Apache License, Version 2.0 (the
6+
# "License"); you may not use this file except in compliance
7+
# with the License. You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing,
12+
# software distributed under the License is distributed on an
13+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
# KIND, either express or implied. See the License for the
15+
# specific language governing permissions and limitations
16+
# under the License.
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# Licensed to the Apache Software Foundation (ASF) under one
2+
# or more contributor license agreements. See the NOTICE file
3+
# distributed with this work for additional information
4+
# regarding copyright ownership. The ASF licenses this file
5+
# to you under the Apache License, Version 2.0 (the
6+
# "License"); you may not use this file except in compliance
7+
# with the License. You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing,
12+
# software distributed under the License is distributed on an
13+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
# KIND, either express or implied. See the License for the
15+
# specific language governing permissions and limitations
16+
# under the License.
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# Licensed to the Apache Software Foundation (ASF) under one
2+
# or more contributor license agreements. See the NOTICE file
3+
# distributed with this work for additional information
4+
# regarding copyright ownership. The ASF licenses this file
5+
# to you under the Apache License, Version 2.0 (the
6+
# "License"); you may not use this file except in compliance
7+
# with the License. You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing,
12+
# software distributed under the License is distributed on an
13+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
# KIND, either express or implied. See the License for the
15+
# specific language governing permissions and limitations
16+
# under the License.
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
#
2+
# Licensed to the Apache Software Foundation (ASF) under one
3+
# or more contributor license agreements. See the NOTICE file
4+
# distributed with this work for additional information
5+
# regarding copyright ownership. The ASF licenses this file
6+
# to you under the Apache License, Version 2.0 (the
7+
# "License"); you may not use this file except in compliance
8+
# with the License. You may obtain a copy of the License at
9+
#
10+
# http://www.apache.org/licenses/LICENSE-2.0
11+
#
12+
# Unless required by applicable law or agreed to in writing,
13+
# software distributed under the License is distributed on an
14+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
# KIND, either express or implied. See the License for the
16+
# specific language governing permissions and limitations
17+
# under the License.
18+
"""
19+
This module contains Facebook Ads Reporting hooks
20+
"""
21+
import time
22+
from enum import Enum
23+
from typing import Any, Dict, List
24+
25+
from cached_property import cached_property
26+
from facebook_business.adobjects.adaccount import AdAccount
27+
from facebook_business.adobjects.adreportrun import AdReportRun
28+
from facebook_business.adobjects.adsinsights import AdsInsights
29+
from facebook_business.api import FacebookAdsApi
30+
31+
from airflow.exceptions import AirflowException
32+
from airflow.hooks.base_hook import BaseHook
33+
34+
35+
class JobStatus(Enum):
36+
"""
37+
Available options for facebook async task status
38+
"""
39+
COMPLETED = 'Job Completed'
40+
STARTED = 'Job Started'
41+
RUNNING = 'Job Running'
42+
FAILED = 'Job Failed'
43+
SKIPPED = 'Job Skipped'
44+
45+
46+
class FacebookAdsReportingHook(BaseHook):
47+
"""
48+
Hook for the Facebook Ads API
49+
50+
.. seealso::
51+
For more information on the Facebook Ads API, take a look at the API docs:
52+
https://developers.facebook.com/docs/marketing-apis/
53+
54+
:param facebook_conn_id: Airflow Facebook Ads connection ID
55+
:type facebook_conn_id: str
56+
:param api_version: The version of Facebook API. Default to v6.0
57+
:type api_version: str
58+
59+
"""
60+
61+
def __init__(
62+
self,
63+
facebook_conn_id: str = "facebook_default",
64+
api_version: str = "v6.0",
65+
) -> None:
66+
super().__init__()
67+
self.facebook_conn_id = facebook_conn_id
68+
self.api_version = api_version
69+
self.client_required_fields = ["app_id",
70+
"app_secret",
71+
"access_token",
72+
"account_id"]
73+
74+
def _get_service(self) -> FacebookAdsApi:
75+
""" Returns Facebook Ads Client using a service account"""
76+
config = self.facebook_ads_config
77+
missings = [_each for _each in self.client_required_fields if _each not in config]
78+
if missings:
79+
message = "{missings} fields are missing".format(missings=missings)
80+
raise AirflowException(message)
81+
return FacebookAdsApi.init(app_id=config["app_id"],
82+
app_secret=config["app_secret"],
83+
access_token=config["access_token"],
84+
account_id=config["account_id"],
85+
api_version=self.api_version)
86+
87+
@cached_property
88+
def facebook_ads_config(self) -> None:
89+
"""
90+
Gets Facebook ads connection from meta db and sets
91+
facebook_ads_config attribute with returned config file
92+
"""
93+
self.log.info("Fetching fb connection: %s", self.facebook_conn_id)
94+
conn = self.get_connection(self.facebook_conn_id)
95+
if "facebook_ads_client" not in conn.extra_dejson:
96+
raise AirflowException("facebook_ads_client not found")
97+
return conn.extra_dejson["facebook_ads_client"]
98+
99+
def bulk_facebook_report(
100+
self,
101+
params: Dict[str, Any],
102+
fields: List[str],
103+
sleep_time: int = 5,
104+
) -> List[AdsInsights]:
105+
"""
106+
Pulls data from the Facebook Ads API
107+
108+
:param fields: List of fields that is obtained from Facebook. Found in AdsInsights.Field class.
109+
https://developers.facebook.com/docs/marketing-api/insights/parameters/v6.0
110+
:type fields: List[str]
111+
:param params: Parameters that determine the query for Facebook
112+
https://developers.facebook.com/docs/marketing-api/insights/parameters/v6.0
113+
:type fields: Dict[str, Any]
114+
:param sleep_time: Time to sleep when async call is happening
115+
:type sleep_time: int
116+
117+
:return: Facebook Ads API response, converted to Facebook Ads Row objects
118+
:rtype: List[AdsInsights]
119+
"""
120+
api = self._get_service()
121+
ad_account = AdAccount(api.get_default_account_id(), api=api)
122+
_async = ad_account.get_insights(params=params, fields=fields, is_async=True)
123+
while True:
124+
request = _async.api_get()
125+
async_status = request[AdReportRun.Field.async_status]
126+
percent = request[AdReportRun.Field.async_percent_completion]
127+
self.log.info("%s %s completed, async_status: %s", percent, "%", async_status)
128+
if async_status == JobStatus.COMPLETED.value:
129+
self.log.info("Job run completed")
130+
break
131+
if async_status in [JobStatus.SKIPPED.value, JobStatus.FAILED.value]:
132+
message = "{async_status}. Please retry.".format(async_status=async_status)
133+
raise AirflowException(message)
134+
time.sleep(sleep_time)
135+
report_run_id = _async.api_get()["report_run_id"]
136+
report_object = AdReportRun(report_run_id, api=api)
137+
insights = report_object.get_insights()
138+
self.log.info("Extracting data from returned Facebook Ads Iterators")
139+
return list(insights)
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# Licensed to the Apache Software Foundation (ASF) under one
2+
# or more contributor license agreements. See the NOTICE file
3+
# distributed with this work for additional information
4+
# regarding copyright ownership. The ASF licenses this file
5+
# to you under the Apache License, Version 2.0 (the
6+
# "License"); you may not use this file except in compliance
7+
# with the License. You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing,
12+
# software distributed under the License is distributed on an
13+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
# KIND, either express or implied. See the License for the
15+
# specific language governing permissions and limitations
16+
# under the License.
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
#
2+
# Licensed to the Apache Software Foundation (ASF) under one
3+
# or more contributor license agreements. See the NOTICE file
4+
# distributed with this work for additional information
5+
# regarding copyright ownership. The ASF licenses this file
6+
# to you under the Apache License, Version 2.0 (the
7+
# "License"); you may not use this file except in compliance
8+
# with the License. You may obtain a copy of the License at
9+
#
10+
# http://www.apache.org/licenses/LICENSE-2.0
11+
#
12+
# Unless required by applicable law or agreed to in writing,
13+
# software distributed under the License is distributed on an
14+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
# KIND, either express or implied. See the License for the
16+
# specific language governing permissions and limitations
17+
# under the License.

0 commit comments

Comments
 (0)