findly.unified-reporting-sdk 0.6.17__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- findly/__init__.py +0 -0
- findly/unified_reporting_sdk/__init__.py +10 -0
- findly/unified_reporting_sdk/data_sources/__init__.py +0 -0
- findly/unified_reporting_sdk/data_sources/common/__init__.py +0 -0
- findly/unified_reporting_sdk/data_sources/common/common_parser.py +213 -0
- findly/unified_reporting_sdk/data_sources/common/date_range_helper.py +33 -0
- findly/unified_reporting_sdk/data_sources/common/reports_client.py +116 -0
- findly/unified_reporting_sdk/data_sources/common/where_string_comparison.py +149 -0
- findly/unified_reporting_sdk/data_sources/fb_ads/__init__.py +0 -0
- findly/unified_reporting_sdk/data_sources/fb_ads/fb_ads_client.py +608 -0
- findly/unified_reporting_sdk/data_sources/fb_ads/fb_ads_query_args_parser.py +828 -0
- findly/unified_reporting_sdk/data_sources/fb_ads/metadata/action_breakdowns.csv +11 -0
- findly/unified_reporting_sdk/data_sources/fb_ads/metadata/breakdowns.csv +44 -0
- findly/unified_reporting_sdk/data_sources/fb_ads/metadata/dimensions.jsonl +75 -0
- findly/unified_reporting_sdk/data_sources/fb_ads/metadata/fields.csv +135 -0
- findly/unified_reporting_sdk/data_sources/fb_ads/metadata/metrics.jsonl +102 -0
- findly/unified_reporting_sdk/data_sources/ga4/__init__.py +0 -0
- findly/unified_reporting_sdk/data_sources/ga4/ga4_client.py +1127 -0
- findly/unified_reporting_sdk/data_sources/ga4/ga4_query_args_parser.py +751 -0
- findly/unified_reporting_sdk/data_sources/ga4/metadata/dimensions.jsonl +109 -0
- findly/unified_reporting_sdk/data_sources/gsc/__init__.py +0 -0
- findly/unified_reporting_sdk/data_sources/gsc/gsc_client.py +0 -0
- findly/unified_reporting_sdk/data_sources/gsc/gsc_service.py +55 -0
- findly/unified_reporting_sdk/protos/.gitignore +3 -0
- findly/unified_reporting_sdk/protos/__init__.py +5 -0
- findly/unified_reporting_sdk/urs.py +87 -0
- findly/unified_reporting_sdk/util/__init__.py +0 -0
- findly/unified_reporting_sdk/util/create_numeric_string_series.py +16 -0
- findly_unified_reporting_sdk-0.6.17.dist-info/LICENSE +674 -0
- findly_unified_reporting_sdk-0.6.17.dist-info/METADATA +99 -0
- findly_unified_reporting_sdk-0.6.17.dist-info/RECORD +32 -0
- findly_unified_reporting_sdk-0.6.17.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,608 @@
|
|
|
1
|
+
import backoff
|
|
2
|
+
import logging
|
|
3
|
+
import pandas as pd
|
|
4
|
+
from typing import List, Dict, Optional, Tuple, Union
|
|
5
|
+
from aiocache import cached, Cache
|
|
6
|
+
|
|
7
|
+
from facebook_business.api import FacebookAdsApi, FacebookSession
|
|
8
|
+
from facebook_business.adobjects.adaccount import AdAccount
|
|
9
|
+
from facebook_business.adobjects.adsinsights import AdsInsights
|
|
10
|
+
from facebook_business.adobjects.user import User
|
|
11
|
+
from facebook_business.adobjects.business import Business
|
|
12
|
+
|
|
13
|
+
from findly.unified_reporting_sdk.data_sources.common.reports_client import (
|
|
14
|
+
ReportsClient,
|
|
15
|
+
)
|
|
16
|
+
from findly.unified_reporting_sdk.data_sources.fb_ads.fb_ads_query_args_parser import (
|
|
17
|
+
FbAdsQueryArgsParser,
|
|
18
|
+
DATE,
|
|
19
|
+
YEARMONTH,
|
|
20
|
+
)
|
|
21
|
+
from findly.unified_reporting_sdk.protos.findly_semantic_layer_pb2 import (
|
|
22
|
+
DimensionType,
|
|
23
|
+
Dimension,
|
|
24
|
+
Metric,
|
|
25
|
+
QueryArgs,
|
|
26
|
+
)
|
|
27
|
+
from itertools import islice
|
|
28
|
+
import json
|
|
29
|
+
from facebook_business.exceptions import (
|
|
30
|
+
FacebookUnavailablePropertyException,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
LOGGER = logging.getLogger(__name__)
|
|
34
|
+
|
|
35
|
+
logging.getLogger("backoff").setLevel(logging.WARNING)
|
|
36
|
+
|
|
37
|
+
REPORT_TIMEOUT = 30 # Maximum timeout for the report to complete
|
|
38
|
+
|
|
39
|
+
ATTRIBUTION_SETTING = "attribution_setting"
|
|
40
|
+
|
|
41
|
+
FB_PARSER = FbAdsQueryArgsParser.default()
|
|
42
|
+
|
|
43
|
+
DEFAULT_PAGE_SIZE_FOR_FB_ADS_PAGINATED_CALL = 1000
|
|
44
|
+
DEFAULT_LIMIT_FOR_ADS_INSIGHTS_CURSOR = 1000
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class FbAdsClient(ReportsClient):
|
|
48
|
+
"""
|
|
49
|
+
Attributes:
|
|
50
|
+
_api(FacebookAdsApi)
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
_api: FacebookAdsApi
|
|
54
|
+
|
|
55
|
+
def __init__(
|
|
56
|
+
self,
|
|
57
|
+
token: str,
|
|
58
|
+
client_id: str,
|
|
59
|
+
client_secret: str,
|
|
60
|
+
):
|
|
61
|
+
"""
|
|
62
|
+
Initialize the FbAdsClient with an access token.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
token (str): The access token for the Facebook Ads API.
|
|
66
|
+
"""
|
|
67
|
+
api_version = None
|
|
68
|
+
proxies = None
|
|
69
|
+
timeout = None
|
|
70
|
+
debug = False
|
|
71
|
+
|
|
72
|
+
session = FacebookSession(client_id, client_secret, token, proxies, timeout)
|
|
73
|
+
self._api = FacebookAdsApi(session, api_version, enable_debug_logger=debug)
|
|
74
|
+
|
|
75
|
+
async def list_all_ad_accounts(self, **kwargs: str) -> Optional[List[str]]:
|
|
76
|
+
"""
|
|
77
|
+
List all ad accounts for the user.
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
List[str]: A list of AdAccount Ids.
|
|
81
|
+
"""
|
|
82
|
+
try:
|
|
83
|
+
# fbid='me' is used to refer to the currently authenticated user.
|
|
84
|
+
client = User(fbid="me", api=self._api)
|
|
85
|
+
ad_accounts = client.get_ad_accounts()
|
|
86
|
+
|
|
87
|
+
# Now get the ad_accont_ids
|
|
88
|
+
ad_account_ids = [account["id"] for account in ad_accounts]
|
|
89
|
+
|
|
90
|
+
LOGGER.info(
|
|
91
|
+
{
|
|
92
|
+
"msg": "user_ad_accounts",
|
|
93
|
+
"ids": ad_account_ids,
|
|
94
|
+
}
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
# Fetch all businesses associated with the user
|
|
98
|
+
business_fields = ["id", "name"]
|
|
99
|
+
businesses = client.get_businesses(fields=business_fields)
|
|
100
|
+
|
|
101
|
+
# Define fields to fetch for ad accounts
|
|
102
|
+
ad_account_fields = ["id", "name"]
|
|
103
|
+
|
|
104
|
+
# For each business, fetch and print all ad accounts
|
|
105
|
+
for business in businesses:
|
|
106
|
+
business_id = business["id"]
|
|
107
|
+
business_name = business["name"]
|
|
108
|
+
|
|
109
|
+
# Get the business object
|
|
110
|
+
business_entity = Business(fbid=business_id, api=self._api)
|
|
111
|
+
|
|
112
|
+
# Fetch all owned ad accounts for the business
|
|
113
|
+
# These are ad accounts that the business has created and has full control over.
|
|
114
|
+
owned_ad_accounts = business_entity.get_owned_ad_accounts(
|
|
115
|
+
fields=ad_account_fields
|
|
116
|
+
)
|
|
117
|
+
owned_ad_accounts_ids = [
|
|
118
|
+
account[AdAccount.Field.id] for account in owned_ad_accounts
|
|
119
|
+
]
|
|
120
|
+
LOGGER.info(
|
|
121
|
+
{
|
|
122
|
+
"msg": "Owned ad accounts",
|
|
123
|
+
"business_name": business_name,
|
|
124
|
+
"business_id": business_id,
|
|
125
|
+
"ids": owned_ad_accounts_ids,
|
|
126
|
+
}
|
|
127
|
+
)
|
|
128
|
+
ad_account_ids.extend(owned_ad_accounts_ids)
|
|
129
|
+
|
|
130
|
+
# Fetch all client ad accounts for the business
|
|
131
|
+
# This method retrieves ad accounts that are managed by the business entity but are owned by another entity (client).
|
|
132
|
+
# In this scenario, the business entity typically has been granted access to manage these accounts on behalf of the client.
|
|
133
|
+
client_ad_accounts = business_entity.get_client_ad_accounts(
|
|
134
|
+
fields=ad_account_fields
|
|
135
|
+
)
|
|
136
|
+
client_ad_accounts_ids = [
|
|
137
|
+
account[AdAccount.Field.id] for account in client_ad_accounts
|
|
138
|
+
]
|
|
139
|
+
LOGGER.info(
|
|
140
|
+
{
|
|
141
|
+
"msg": "Client ad accounts",
|
|
142
|
+
"business_name": business_name,
|
|
143
|
+
"business_id": business_id,
|
|
144
|
+
"ids": client_ad_accounts_ids,
|
|
145
|
+
}
|
|
146
|
+
)
|
|
147
|
+
ad_account_ids.extend(client_ad_accounts_ids)
|
|
148
|
+
return ad_account_ids
|
|
149
|
+
|
|
150
|
+
except Exception as e:
|
|
151
|
+
LOGGER.error(
|
|
152
|
+
{
|
|
153
|
+
"msg": "Error listing ad accounts",
|
|
154
|
+
"error": str(e),
|
|
155
|
+
}
|
|
156
|
+
)
|
|
157
|
+
return None
|
|
158
|
+
|
|
159
|
+
async def list_property_ids(self, **kwargs: str) -> Optional[List[str]]:
|
|
160
|
+
return await self.list_all_ad_accounts()
|
|
161
|
+
|
|
162
|
+
@backoff.on_exception(backoff.expo, Exception, max_time=60, max_tries=3)
|
|
163
|
+
@cached(ttl=300, cache=Cache.MEMORY) # Cache results for 5 minutes
|
|
164
|
+
async def _decorated_query_from_parts(
|
|
165
|
+
self,
|
|
166
|
+
ad_account_id: str,
|
|
167
|
+
metrics_names: List[str],
|
|
168
|
+
dimensions: Optional[List[Dimension]] = None,
|
|
169
|
+
time_ranges: Optional[
|
|
170
|
+
List[Dict[str, str]]
|
|
171
|
+
] = None, # If not specified, defaults to last 30 days
|
|
172
|
+
sort: Optional[List[str]] = None,
|
|
173
|
+
filtering: Optional[List[Dict[str, str]]] = None,
|
|
174
|
+
limit: Union[int, str] = DEFAULT_LIMIT_FOR_ADS_INSIGHTS_CURSOR,
|
|
175
|
+
time_increment: Optional[str] = None,
|
|
176
|
+
level: AdsInsights.Level = AdsInsights.Level.account,
|
|
177
|
+
page_size: int = DEFAULT_PAGE_SIZE_FOR_FB_ADS_PAGINATED_CALL,
|
|
178
|
+
**kwargs: str,
|
|
179
|
+
) -> Optional[Tuple[List[AdsInsights], Optional[Dict]]]:
|
|
180
|
+
"""
|
|
181
|
+
Query the Facebook Ads API for insights.
|
|
182
|
+
|
|
183
|
+
Args:
|
|
184
|
+
ad_account_id (str): The ad account ID.
|
|
185
|
+
metrics_names (List[str]): The Metric fields to include in the response.
|
|
186
|
+
dimensions (Optional[List[Dimension]]): The dimensions that can be fields, breakdowns, action breakdowns...
|
|
187
|
+
time_ranges (Optional[List[Dict[str, str]]]): The time ranges to apply.
|
|
188
|
+
sort (Optional[List[str]]): The sort order to apply.
|
|
189
|
+
filtering (Optional[List[Dict[str, str]]): The filters to apply.
|
|
190
|
+
level (AdsInsights.Level): The level of insights to return.
|
|
191
|
+
|
|
192
|
+
Returns:
|
|
193
|
+
Optional[Tuple[List[AdsInsights], Optional[Dict]]]: A tuple of AdsInsights List and Summary, or None if an error occurred.
|
|
194
|
+
"""
|
|
195
|
+
try:
|
|
196
|
+
limit = int(limit)
|
|
197
|
+
except ValueError as e:
|
|
198
|
+
LOGGER.error(
|
|
199
|
+
{
|
|
200
|
+
"msg": "fb_ads_query_failed",
|
|
201
|
+
"error": str(e),
|
|
202
|
+
}
|
|
203
|
+
)
|
|
204
|
+
return None
|
|
205
|
+
except TypeError as e:
|
|
206
|
+
LOGGER.error(
|
|
207
|
+
{
|
|
208
|
+
"msg": "fb_ads_query_failed",
|
|
209
|
+
"error": str(e),
|
|
210
|
+
}
|
|
211
|
+
)
|
|
212
|
+
return None
|
|
213
|
+
|
|
214
|
+
fields = list(metrics_names)
|
|
215
|
+
params = {"level": level, "default_summary": "true"}
|
|
216
|
+
if dimensions:
|
|
217
|
+
fields_dimensions = [
|
|
218
|
+
dimension.name
|
|
219
|
+
for dimension in dimensions
|
|
220
|
+
if dimension is not None
|
|
221
|
+
and dimension.type == DimensionType.FB_ADS_FIELD
|
|
222
|
+
]
|
|
223
|
+
fields.extend(fields_dimensions)
|
|
224
|
+
# Following the Facebook Ads API documentation,
|
|
225
|
+
# Note: The attribution_setting field only returns values when use_unified_attribution_setting is set to true.
|
|
226
|
+
# https://developers.facebook.com/docs/marketing-api/reference/ads-insights/
|
|
227
|
+
if ATTRIBUTION_SETTING in fields_dimensions:
|
|
228
|
+
params["use_unified_attribution_setting"] = "true"
|
|
229
|
+
|
|
230
|
+
params["breakdowns"] = [
|
|
231
|
+
dimension.name
|
|
232
|
+
for dimension in dimensions
|
|
233
|
+
if dimension is not None
|
|
234
|
+
and dimension.type == DimensionType.FB_ADS_BREAKDOWN
|
|
235
|
+
]
|
|
236
|
+
params["action_breakdowns"] = [
|
|
237
|
+
dimension.name
|
|
238
|
+
for dimension in dimensions
|
|
239
|
+
if dimension is not None
|
|
240
|
+
and dimension.type == DimensionType.FB_ADS_ACTION_BREAKDOWN
|
|
241
|
+
]
|
|
242
|
+
|
|
243
|
+
if DATE in [dimension.name for dimension in dimensions]:
|
|
244
|
+
params["time_increment"] = "1"
|
|
245
|
+
elif YEARMONTH in [dimension.name for dimension in dimensions]:
|
|
246
|
+
params["time_increment"] = "monthly"
|
|
247
|
+
|
|
248
|
+
# in Fb ads, multiple time_ranges and time_increment don't work together
|
|
249
|
+
# So if we have multiple time_ranges, we give preference to time_ranges
|
|
250
|
+
# if we have a single one, we use singular date range and time_increment
|
|
251
|
+
if time_ranges:
|
|
252
|
+
if len(time_ranges) > 1:
|
|
253
|
+
params["time_ranges"] = time_ranges
|
|
254
|
+
else:
|
|
255
|
+
params["time_range"] = time_ranges[0]
|
|
256
|
+
# TODO (jutogashi): check if we want to add this back
|
|
257
|
+
# if time_increment:
|
|
258
|
+
# params["time_increment"] = time_increment
|
|
259
|
+
if sort:
|
|
260
|
+
params["sort"] = sort
|
|
261
|
+
if filtering:
|
|
262
|
+
params["filtering"] = filtering
|
|
263
|
+
|
|
264
|
+
params["limit"] = page_size
|
|
265
|
+
|
|
266
|
+
try:
|
|
267
|
+
insights_cursor = AdAccount(fbid=ad_account_id, api=self._api).get_insights(
|
|
268
|
+
fields=fields,
|
|
269
|
+
params=params,
|
|
270
|
+
)
|
|
271
|
+
summary: Optional[Dict] = None
|
|
272
|
+
try:
|
|
273
|
+
summary_text = insights_cursor.summary()
|
|
274
|
+
# the string has <Summary> before the JSON string
|
|
275
|
+
# This splits on "> "and takes the second part, which is the JSON string
|
|
276
|
+
summary_json = summary_text.split("> ")[1]
|
|
277
|
+
summary = json.loads(summary_json)
|
|
278
|
+
except FacebookUnavailablePropertyException as e:
|
|
279
|
+
LOGGER.info(
|
|
280
|
+
{
|
|
281
|
+
"msg": "error_getting_summary",
|
|
282
|
+
"error": str(e),
|
|
283
|
+
}
|
|
284
|
+
)
|
|
285
|
+
insights_cursor_slice = islice(insights_cursor, limit)
|
|
286
|
+
insights = list(insights_cursor_slice)
|
|
287
|
+
|
|
288
|
+
return insights, summary
|
|
289
|
+
|
|
290
|
+
except Exception as e:
|
|
291
|
+
LOGGER.info(
|
|
292
|
+
{
|
|
293
|
+
"msg": "Error querying Facebook Ads API",
|
|
294
|
+
"ad_account_id": ad_account_id,
|
|
295
|
+
"error": str(e),
|
|
296
|
+
}
|
|
297
|
+
)
|
|
298
|
+
raise e
|
|
299
|
+
|
|
300
|
+
@cached(ttl=300, cache=Cache.MEMORY) # Cache results for 5 minutes
|
|
301
|
+
async def _decorated_query(
|
|
302
|
+
self,
|
|
303
|
+
query_args: QueryArgs,
|
|
304
|
+
property_id: str,
|
|
305
|
+
**kwargs: str,
|
|
306
|
+
) -> Optional[Tuple[List[pd.DataFrame], List[pd.DataFrame]]]:
|
|
307
|
+
params = await FB_PARSER.parse_query_args_to_request_params(
|
|
308
|
+
query_args=query_args,
|
|
309
|
+
property_id=property_id,
|
|
310
|
+
)
|
|
311
|
+
insights = None
|
|
312
|
+
try:
|
|
313
|
+
insights, summary = await self._decorated_query_from_parts(**params)
|
|
314
|
+
|
|
315
|
+
except Exception as e:
|
|
316
|
+
LOGGER.error(
|
|
317
|
+
{
|
|
318
|
+
"msg": "fb_ads_query_failed",
|
|
319
|
+
"error": str(e),
|
|
320
|
+
}
|
|
321
|
+
)
|
|
322
|
+
raise e
|
|
323
|
+
|
|
324
|
+
if insights is None:
|
|
325
|
+
return None
|
|
326
|
+
|
|
327
|
+
generated_result_df = await FB_PARSER.parse_report_response_to_dataframe(
|
|
328
|
+
report_response=insights,
|
|
329
|
+
query_args=query_args,
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
generated_totals_df = FB_PARSER.parse_totals_to_dataframe(
|
|
333
|
+
summary=summary,
|
|
334
|
+
query_args=query_args,
|
|
335
|
+
)
|
|
336
|
+
return generated_result_df, generated_totals_df
|
|
337
|
+
|
|
338
|
+
async def query(
|
|
339
|
+
self,
|
|
340
|
+
query_args: QueryArgs,
|
|
341
|
+
property_id: str,
|
|
342
|
+
**kwargs: str,
|
|
343
|
+
) -> Optional[Tuple[List[pd.DataFrame], List[pd.DataFrame]]]:
|
|
344
|
+
return await self._decorated_query(
|
|
345
|
+
query_args=query_args,
|
|
346
|
+
property_id=property_id,
|
|
347
|
+
**kwargs,
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
@cached(
|
|
351
|
+
ttl=3600, cache=Cache.MEMORY, skip_cache_func=lambda x: x is None
|
|
352
|
+
) # Cache results for 1 hour
|
|
353
|
+
async def _decorated_get_dimension_values(
|
|
354
|
+
self,
|
|
355
|
+
dimension: Dimension,
|
|
356
|
+
top_n: int,
|
|
357
|
+
property_id: str,
|
|
358
|
+
level: AdsInsights.Level = AdsInsights.Level.account,
|
|
359
|
+
**kwargs: str,
|
|
360
|
+
) -> Optional[List[str]]:
|
|
361
|
+
"""
|
|
362
|
+
Get the top N values for a specified dimension from the Facebook Ads API.
|
|
363
|
+
|
|
364
|
+
This method queries the Facebook Ads API for insights, with the specified dimension.
|
|
365
|
+
It then returns the top N values for the dimension.
|
|
366
|
+
|
|
367
|
+
Args:
|
|
368
|
+
dimension (Dimension): the dimension.
|
|
369
|
+
top_n (int): The number of top values to return.
|
|
370
|
+
property_id (str): The ad account ID.
|
|
371
|
+
level (AdsInsights.Level): TODO describe level,
|
|
372
|
+
|
|
373
|
+
Returns:
|
|
374
|
+
Optional[List[str]]: A list of the top N values for the dimension, or None if an error occurred.
|
|
375
|
+
"""
|
|
376
|
+
LOGGER.info(
|
|
377
|
+
{
|
|
378
|
+
"msg": "get_fb_ads_dimension_values",
|
|
379
|
+
"dimension_name": dimension.name,
|
|
380
|
+
"dimension_type": dimension.type,
|
|
381
|
+
"top_n": top_n,
|
|
382
|
+
"level": level,
|
|
383
|
+
}
|
|
384
|
+
)
|
|
385
|
+
|
|
386
|
+
try:
|
|
387
|
+
if dimension.type == DimensionType.FB_ADS_BREAKDOWN:
|
|
388
|
+
response = await self._decorated_query_from_parts(
|
|
389
|
+
ad_account_id=property_id,
|
|
390
|
+
metrics_names=["impressions"],
|
|
391
|
+
dimensions=[dimension],
|
|
392
|
+
sort=["impressions_descending"],
|
|
393
|
+
level=level,
|
|
394
|
+
)
|
|
395
|
+
if response is None:
|
|
396
|
+
LOGGER.info(
|
|
397
|
+
{
|
|
398
|
+
"msg": "no_response_for_query",
|
|
399
|
+
"dimension_metadata_name": dimension.name,
|
|
400
|
+
"dimension_type": dimension.type,
|
|
401
|
+
"top_n": top_n,
|
|
402
|
+
}
|
|
403
|
+
)
|
|
404
|
+
return None
|
|
405
|
+
|
|
406
|
+
result = [insight[dimension.name] for insight in response[0][:top_n]]
|
|
407
|
+
|
|
408
|
+
elif dimension.type == DimensionType.FB_ADS_ACTION_BREAKDOWN:
|
|
409
|
+
response = await self._decorated_query_from_parts(
|
|
410
|
+
ad_account_id=property_id,
|
|
411
|
+
metrics_names=["actions"],
|
|
412
|
+
dimensions=[dimension],
|
|
413
|
+
level=level,
|
|
414
|
+
)
|
|
415
|
+
if response is None:
|
|
416
|
+
LOGGER.info(
|
|
417
|
+
{
|
|
418
|
+
"msg": "no_response_for_query",
|
|
419
|
+
"dimension_metadata_name": dimension.name,
|
|
420
|
+
"dimension_type": dimension.type,
|
|
421
|
+
"top_n": top_n,
|
|
422
|
+
}
|
|
423
|
+
)
|
|
424
|
+
return None
|
|
425
|
+
response_list = response[0]
|
|
426
|
+
actions_dict: Dict[str, int] = {}
|
|
427
|
+
if len(response_list) != 1:
|
|
428
|
+
logging.warning(
|
|
429
|
+
{
|
|
430
|
+
"msg": "response_length_not_1_for_action_breakdown",
|
|
431
|
+
"dimension_metadata_name": dimension.name,
|
|
432
|
+
"response_length": len(response_list),
|
|
433
|
+
}
|
|
434
|
+
)
|
|
435
|
+
# If the response_list length is 0, return an empty list
|
|
436
|
+
# If the response_list length is > 1, return the top N values for the first element
|
|
437
|
+
# but keep a warning in the logs, so we know that something is wrong
|
|
438
|
+
if len(response_list) == 0:
|
|
439
|
+
return []
|
|
440
|
+
for action in response_list[0]["actions"]:
|
|
441
|
+
if dimension.name in action:
|
|
442
|
+
if (
|
|
443
|
+
action[dimension.name] not in actions_dict
|
|
444
|
+
or int(action["value"])
|
|
445
|
+
> actions_dict[action[dimension.name]]
|
|
446
|
+
):
|
|
447
|
+
actions_dict[action[dimension.name]] = int(action["value"])
|
|
448
|
+
|
|
449
|
+
top_actions = sorted(
|
|
450
|
+
actions_dict.items(), key=lambda item: item[1], reverse=True
|
|
451
|
+
)[:top_n]
|
|
452
|
+
|
|
453
|
+
result = [action[0] for action in top_actions]
|
|
454
|
+
|
|
455
|
+
elif dimension.type == DimensionType.FB_ADS_FIELD:
|
|
456
|
+
response = await self._decorated_query_from_parts(
|
|
457
|
+
ad_account_id=property_id,
|
|
458
|
+
metrics_names=[], # No metrics needed for this query
|
|
459
|
+
dimensions=[dimension],
|
|
460
|
+
sort=[f"{dimension.name}_descending"],
|
|
461
|
+
level=AdsInsights.Level.ad,
|
|
462
|
+
)
|
|
463
|
+
|
|
464
|
+
if response is None:
|
|
465
|
+
LOGGER.info(
|
|
466
|
+
{
|
|
467
|
+
"msg": "no_response_for_query",
|
|
468
|
+
"dimension_metadata_name": dimension.name,
|
|
469
|
+
"dimension_type": dimension.type,
|
|
470
|
+
"top_n": top_n,
|
|
471
|
+
}
|
|
472
|
+
)
|
|
473
|
+
return None
|
|
474
|
+
|
|
475
|
+
result = [insight[dimension.name] for insight in response[0][:top_n]]
|
|
476
|
+
result = list(set(result)) # Remove duplicates
|
|
477
|
+
|
|
478
|
+
else:
|
|
479
|
+
LOGGER.info(
|
|
480
|
+
{
|
|
481
|
+
"msg": "dimension_type_not_supported",
|
|
482
|
+
"dimension_metadata_name": dimension.name,
|
|
483
|
+
"top_n": top_n,
|
|
484
|
+
}
|
|
485
|
+
)
|
|
486
|
+
return None
|
|
487
|
+
|
|
488
|
+
LOGGER.info(
|
|
489
|
+
{
|
|
490
|
+
"msg": "get_fb_ads_dimension_values_successful",
|
|
491
|
+
"dimension_metadata_name": dimension.name,
|
|
492
|
+
"top_n": top_n,
|
|
493
|
+
}
|
|
494
|
+
)
|
|
495
|
+
return result
|
|
496
|
+
|
|
497
|
+
except Exception as e:
|
|
498
|
+
LOGGER.info(
|
|
499
|
+
{
|
|
500
|
+
"msg": "error_getting_fb_ads_dimension_values",
|
|
501
|
+
"dimension_metadata_name": dimension.name,
|
|
502
|
+
"top_n": top_n,
|
|
503
|
+
"error": str(e),
|
|
504
|
+
}
|
|
505
|
+
)
|
|
506
|
+
return None
|
|
507
|
+
|
|
508
|
+
async def get_dimension_values(
|
|
509
|
+
self,
|
|
510
|
+
dimension: Dimension,
|
|
511
|
+
top_n: int,
|
|
512
|
+
property_id: str,
|
|
513
|
+
level: AdsInsights.Level = AdsInsights.Level.account,
|
|
514
|
+
**kwargs: str,
|
|
515
|
+
) -> Optional[List[str]]:
|
|
516
|
+
return await self._decorated_get_dimension_values(
|
|
517
|
+
dimension=dimension,
|
|
518
|
+
top_n=top_n,
|
|
519
|
+
property_id=property_id,
|
|
520
|
+
level=level,
|
|
521
|
+
**kwargs,
|
|
522
|
+
)
|
|
523
|
+
|
|
524
|
+
async def list_metrics(
|
|
525
|
+
self, property_id: str, **kwargs: str
|
|
526
|
+
) -> Optional[List[Metric]]:
|
|
527
|
+
LOGGER.info(
|
|
528
|
+
{
|
|
529
|
+
"msg": "Getting metrics from json",
|
|
530
|
+
"ad_account_id": property_id,
|
|
531
|
+
}
|
|
532
|
+
)
|
|
533
|
+
try:
|
|
534
|
+
metrics_list = FB_PARSER.parse_metrics()
|
|
535
|
+
return metrics_list
|
|
536
|
+
except Exception as e:
|
|
537
|
+
LOGGER.error(f"An unexpected error occurred parsing Metrics: {e}")
|
|
538
|
+
return None
|
|
539
|
+
|
|
540
|
+
async def list_dimensions(
|
|
541
|
+
self, property_id: str, **kwargs: str
|
|
542
|
+
) -> Optional[List[Dimension]]:
|
|
543
|
+
LOGGER.info(
|
|
544
|
+
{
|
|
545
|
+
"msg": "Getting dimensions from json",
|
|
546
|
+
"ad_account_id": property_id,
|
|
547
|
+
}
|
|
548
|
+
)
|
|
549
|
+
try:
|
|
550
|
+
dimensions_list = FB_PARSER.parse_dimensions()
|
|
551
|
+
return dimensions_list
|
|
552
|
+
except Exception as e:
|
|
553
|
+
LOGGER.error(f"An unexpected error occurred parsing dimensions: {e}")
|
|
554
|
+
return None
|
|
555
|
+
|
|
556
|
+
async def get_metric_from_name(
|
|
557
|
+
self, metric_name: str, property_id: str, **kwargs: str
|
|
558
|
+
) -> Optional[Metric]:
|
|
559
|
+
fb_metrics_list: Optional[List[Metric]] = await self.list_metrics(
|
|
560
|
+
property_id=property_id,
|
|
561
|
+
)
|
|
562
|
+
|
|
563
|
+
if fb_metrics_list is None:
|
|
564
|
+
LOGGER.info(
|
|
565
|
+
{
|
|
566
|
+
"msg": "no_fb_ads_metric_found",
|
|
567
|
+
}
|
|
568
|
+
)
|
|
569
|
+
return None
|
|
570
|
+
|
|
571
|
+
for metric in fb_metrics_list:
|
|
572
|
+
if metric.name == metric_name:
|
|
573
|
+
LOGGER.info(
|
|
574
|
+
{
|
|
575
|
+
"msg": "metric_found",
|
|
576
|
+
"metric": metric_name,
|
|
577
|
+
}
|
|
578
|
+
)
|
|
579
|
+
return metric
|
|
580
|
+
|
|
581
|
+
return None
|
|
582
|
+
|
|
583
|
+
async def get_dimension_from_name(
|
|
584
|
+
self, dimension_name: str, property_id: str, **kwargs: str
|
|
585
|
+
) -> Optional[Dimension]:
|
|
586
|
+
fb_dimensions_list: Optional[List[Dimension]] = await self.list_dimensions(
|
|
587
|
+
property_id=property_id,
|
|
588
|
+
)
|
|
589
|
+
|
|
590
|
+
if fb_dimensions_list is None:
|
|
591
|
+
LOGGER.info(
|
|
592
|
+
{
|
|
593
|
+
"msg": "no_fb_ads_dimensions_found",
|
|
594
|
+
}
|
|
595
|
+
)
|
|
596
|
+
return None
|
|
597
|
+
|
|
598
|
+
for dimension in fb_dimensions_list:
|
|
599
|
+
if dimension.name == dimension_name:
|
|
600
|
+
LOGGER.info(
|
|
601
|
+
{
|
|
602
|
+
"msg": "dimension_found",
|
|
603
|
+
"dimension": dimension_name,
|
|
604
|
+
}
|
|
605
|
+
)
|
|
606
|
+
return dimension
|
|
607
|
+
|
|
608
|
+
return None
|