logos-sdk 0.0.23__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.
logos_sdk/__init__.py ADDED
@@ -0,0 +1,4 @@
1
+ PRODUCTION = "PRODUCTION"
2
+ DEVELOPMENT = "DEVELOPMENT"
3
+ TESTING = "TESTING"
4
+ CLOUD_DEVELOPMENT = "CLOUD_DEVELOPMENT"
@@ -0,0 +1,152 @@
1
+ import os
2
+ from datetime import datetime
3
+
4
+ import google.auth.exceptions
5
+ from typing import List, Dict, Union, Optional
6
+
7
+ from google.api_core.exceptions import NotFound
8
+ from google.cloud.logging import Client as LoggerClient
9
+ from google.auth.exceptions import DefaultCredentialsError
10
+ from google.cloud.bigquery import Table
11
+ from google.cloud import bigquery
12
+ import numpy as np
13
+ import time
14
+
15
+ from google.api_core.retry import Retry
16
+
17
+ from logos_sdk.big_query import retry_on_not_found
18
+ from dotenv import load_dotenv
19
+
20
+
21
+ class BigQueryException(Exception):
22
+ def __init__(self, messages):
23
+ self.messages = messages
24
+
25
+
26
+ class BigQuery:
27
+ BQ_ROWS_LIMIT = 10000
28
+ _service = None
29
+
30
+ def __init__(self):
31
+ load_dotenv()
32
+ self.project_id = os.environ.get("PROJECT_ID")
33
+ self._service = bigquery.Client(project=self.project_id)
34
+ try:
35
+ self.logger = LoggerClient(_use_grpc=False).logger(name="logos-logging")
36
+ except DefaultCredentialsError:
37
+ self.logger = None
38
+
39
+ def get_dataset(self, dataset_id: str):
40
+ return self._service.get_dataset(dataset_id)
41
+
42
+ def create_dataset(self, dataset_id: str):
43
+ return self._service.create_dataset(dataset_id)
44
+
45
+ def delete_dataset(self, dataset_id: str):
46
+ self._service.delete_dataset(dataset_id, not_found_ok=True)
47
+
48
+ def _get_table_id_sql_format(self, dataset_id: str, table_id: str):
49
+ return f"{self.project_id}.{dataset_id}.{table_id}"
50
+
51
+ def run_query(self, query: str) -> List[Dict]:
52
+ df = self._service.query(query).result().to_dataframe().fillna(np.nan)
53
+ return df.replace([np.nan], [None]).to_dict("records")
54
+
55
+ def get_table(self, dataset_id: str, table_id: str) -> Table:
56
+ sql_format = self._get_table_id_sql_format(dataset_id, table_id)
57
+ return self._service.get_table(sql_format)
58
+
59
+ def insert_into_table(
60
+ self, dataset_id: str, table_id: str, records: List[Dict]
61
+ ) -> None:
62
+ bq_table = self.get_table(dataset_id, table_id)
63
+ self._insert_into_table(bq_table, records)
64
+
65
+ def insert_create_table(
66
+ self,
67
+ dataset_id: str,
68
+ table_id: str,
69
+ records: List[Dict],
70
+ schema_columns: List[Dict],
71
+ ) -> None:
72
+ bq_table = self.check_table_exists(dataset_id, table_id)
73
+ if bq_table is None:
74
+ bq_table = self.create_table(dataset_id, table_id, schema_columns)
75
+
76
+ self._insert_into_table(bq_table, records)
77
+
78
+ def delete_table(self, dataset_id: str, table_id: str) -> None:
79
+ if self.check_table_exists(dataset_id, table_id):
80
+ sql_format = self._get_table_id_sql_format(dataset_id, table_id)
81
+ self._service.delete_table(sql_format)
82
+
83
+ def check_table_exists(self, dataset_id: str, table_id: str) -> Optional[Table]:
84
+ try:
85
+ return self.get_table(dataset_id, table_id)
86
+ except google.cloud.exceptions.NotFound:
87
+ return None
88
+
89
+ def create_table(
90
+ self, dataset_id: str, table_id: str, schema_columns: List[Dict]
91
+ ) -> Union[bool, Table]:
92
+ table_schema = [
93
+ bigquery.schema.SchemaField(row["name"], row["col_type"], mode=row["mode"])
94
+ for row in schema_columns
95
+ ]
96
+ try:
97
+ sql_format = self._get_table_id_sql_format(dataset_id, table_id)
98
+ table_object = bigquery.Table(sql_format, schema=table_schema)
99
+ return self._service.create_table(table_object)
100
+ except google.cloud.exceptions.Conflict:
101
+ return False
102
+
103
+ def create_view(self, dataset_id, view_id, sql_string):
104
+ try:
105
+ sql_format = self._get_table_id_sql_format(dataset_id, view_id)
106
+ view = bigquery.Table(sql_format)
107
+ view.view_query = sql_string
108
+ return self._service.create_table(view)
109
+ except google.cloud.exceptions.Conflict:
110
+ return False
111
+
112
+ def get_table_last_modified_date(self, dataset_id: str, table_id: str):
113
+ last_modified_timestamp = (
114
+ self._service.query(
115
+ f"SELECT TIMESTAMP_MILLIS(last_modified_time) as time_stamp "
116
+ f"FROM `{self.project_id}.{dataset_id}.__TABLES__` "
117
+ f"WHERE table_id = '{table_id}'"
118
+ )
119
+ .result()
120
+ .to_dataframe()
121
+ )
122
+
123
+ if last_modified_timestamp.empty:
124
+ raise NotFound("Table does not exist!")
125
+
126
+ last_modified_timestamp = last_modified_timestamp["time_stamp"].iloc[0]
127
+ last_modified_date = datetime.strptime(
128
+ str(last_modified_timestamp), "%Y-%m-%d %H:%M:%S.%f+00:00"
129
+ )
130
+
131
+ return last_modified_date
132
+
133
+ @retry_on_not_found
134
+ def _insert_into_table(
135
+ self, bq_table: Table, records: List[Dict], attempts: int
136
+ ) -> None:
137
+ if len(records) > self.BQ_ROWS_LIMIT:
138
+ for index in range(0, len(records), self.BQ_ROWS_LIMIT):
139
+ errors = self._service.insert_rows(
140
+ bq_table,
141
+ records[index: (index + self.BQ_ROWS_LIMIT)],
142
+ retry=Retry(
143
+ total=2, connect=4, backoff_factor=2, allowed_methods=None
144
+ ),
145
+ )
146
+ time.sleep(2)
147
+ if errors:
148
+ raise BigQueryException(errors)
149
+ else:
150
+ errors = self._service.insert_rows(bq_table, records)
151
+ if errors:
152
+ raise BigQueryException(errors)
@@ -0,0 +1,22 @@
1
+ from functools import wraps
2
+ from google.api_core.exceptions import NotFound
3
+ import time
4
+
5
+ MAX_NUMBER_OF_ATTEMPTS = 2
6
+
7
+
8
+ def retry_on_not_found(wrapped_function):
9
+ """This decorator retry call when table is not found. Insert into newly created table often fails with error
10
+ because API probably needs few seconds to see new created table"""
11
+
12
+ @wraps(wrapped_function)
13
+ def inner(*args, **kwargs):
14
+ for i in range(1, MAX_NUMBER_OF_ATTEMPTS + 1):
15
+ try:
16
+ kwargs["attempts"] = i
17
+ return wrapped_function(*args, **kwargs)
18
+ # this is because all request share same service
19
+ except NotFound as err:
20
+ time.sleep(2)
21
+
22
+ return inner
@@ -0,0 +1,66 @@
1
+ import os
2
+ import logging
3
+ from google.cloud.logging import Client as LoggerClient
4
+ from dotenv import load_dotenv
5
+ from logos_sdk import DEVELOPMENT, TESTING, CLOUD_DEVELOPMENT, PRODUCTION
6
+
7
+
8
+ class LogosLogger:
9
+
10
+ def __init__(self, name="logos-logging", labels=None, trace=None):
11
+
12
+ load_dotenv()
13
+
14
+ self.labels = labels
15
+ self.trace = trace
16
+ self.name = name
17
+ self.settings = {}
18
+ self.accesses = []
19
+
20
+ # if current environment is local development or Bitbucket pipeline testing, we do not care
21
+ # we want the name of the logger to be 'logos-logging', because it is not being sent to Cloud
22
+ # and we want to be able to test it as a real thing. On the other hand, if we are in production or
23
+ # cloud development, we need to be logging the logs into Cloud, therefore these two environments need
24
+ # to be separated, so that the logs do not clash.
25
+
26
+ if os.environ.get(DEVELOPMENT):
27
+ self.env = DEVELOPMENT
28
+ elif os.environ.get(TESTING):
29
+ self.env = TESTING
30
+ elif os.environ.get(CLOUD_DEVELOPMENT):
31
+ self.env = CLOUD_DEVELOPMENT
32
+ else:
33
+ self.env = PRODUCTION
34
+
35
+ if self.env == DEVELOPMENT or self.env == TESTING:
36
+ self.stream_logger = logging.getLogger(name=self.name)
37
+ else:
38
+ if self.env == CLOUD_DEVELOPMENT and self.name == "logos-logging":
39
+ self.name = "logos-logging-development"
40
+
41
+ self.cloud_client = LoggerClient()
42
+ self.cloud_logger = self.cloud_client.logger(name=self.name)
43
+
44
+ def get_name(self):
45
+ return self.name
46
+
47
+ def log(self, message, severity, log_type=None):
48
+ labels = self.labels if log_type is None else {"log_type": log_type, **(self.labels or {})}
49
+ if self.env == DEVELOPMENT or self.env == TESTING:
50
+ self.stream_logger.log(
51
+ msg={"message": message, "settings": self.settings, "accesses": self.accesses},
52
+ level=severity,
53
+ extra={
54
+ "json_fields": {
55
+ "logging.googleapis.com/trace": self.trace,
56
+ "logging.googleapis.com/labels": labels
57
+ }
58
+ }
59
+ )
60
+ else:
61
+ self.cloud_logger.log_struct(
62
+ info={"message": message, "settings": self.settings, "accesses": self.accesses},
63
+ labels=labels,
64
+ severity=logging.getLevelName(severity),
65
+ trace=self.trace
66
+ )
@@ -0,0 +1,76 @@
1
+ from logos_sdk.logging.LogosLogger import LogosLogger
2
+ import logging
3
+
4
+ # log types in labels
5
+ NOTIFICATION = "notification"
6
+ DEBUG = "debug"
7
+ RESULT = "result"
8
+ RESULT_UI_ONLY = "result-ui-only"
9
+
10
+ def setup_from_request(request, logger_name="logos-logging"):
11
+ # We want to parse as much as possible without raising an exception.
12
+ # we want to be able to at least create a complete notification log, for this we need labels without accesses and trace.
13
+ try:
14
+ trace = request.headers["X-Cloud-Trace-Context"]
15
+ except Exception as err:
16
+ raise Exception(f"Unable to retrieve trace from request ({err})")
17
+
18
+ try:
19
+ logger = LogosLogger(logger_name, {}, trace)
20
+ except Exception as err:
21
+ raise Exception(f"Unable to ser up LogosLogger ({err})")
22
+
23
+ try:
24
+ body = request.get_json()
25
+ except Exception as err:
26
+ message = f"Unable to parse request body as a valid json ({err})"
27
+ logger.log(message=message, severity=logging.ERROR, log_type=NOTIFICATION)
28
+ raise Exception(message)
29
+
30
+ try:
31
+ labels = {
32
+ "id": str(body["id"]),
33
+ "client": str(body["client"]),
34
+ "script": str(body["script"]),
35
+ "author": str(body["author"]),
36
+ }
37
+ logger.labels = labels
38
+ except Exception as err:
39
+ message = f"Unable to create labels, missing key ({err})"
40
+ logger.log(message=message, severity=logging.ERROR, log_type=NOTIFICATION)
41
+ raise Exception(message)
42
+
43
+ # To run the script itself, we need the rest. If we are not able to run it,
44
+ # we want to be able to at least create a complete notification log.
45
+ try:
46
+ settings = body["settings"]
47
+ logger.settings = body["settings"]
48
+ except Exception as err:
49
+ message = f"Missing required settings ({err})"
50
+ logger.log(message=message, severity=logging.ERROR, log_type=NOTIFICATION)
51
+ raise Exception(message)
52
+
53
+ try:
54
+ accesses = {
55
+ str(access["platform"]["short_name"]): str(access["account"]["account_platform_id"]) for access in
56
+ body["accesses"]
57
+ }
58
+ labels = {**labels, **accesses}
59
+ logger.labels = labels
60
+ logger.accesses = body["accesses"]
61
+ except Exception as err:
62
+ message = f"Unable to parse accesses ({err})"
63
+ logger.log(message=message, severity=logging.ERROR, log_type=NOTIFICATION)
64
+ raise Exception(message)
65
+
66
+ try:
67
+ secrets = {
68
+ str(access["platform"]["short_name"]): str(access["secret"]["name"]) for access in body["accesses"]
69
+ }
70
+ except Exception as err:
71
+ message = f"Unable to parse secrets ({err})"
72
+ logger.log(message=message, severity=logging.ERROR, log_type=NOTIFICATION)
73
+ raise Exception(message)
74
+
75
+
76
+ return logger, labels, settings, secrets
@@ -0,0 +1,346 @@
1
+ import io
2
+ import json
3
+ import httplib2
4
+ import os
5
+ import time
6
+
7
+ from random import randint
8
+ from logos_sdk.services import get_headers
9
+ from requests import request
10
+ from typing import Dict, List
11
+ from http import HTTPStatus
12
+ from googleapiclient.http import MediaIoBaseDownload, HttpRequest
13
+ from pandas import read_csv
14
+ from dotenv import load_dotenv
15
+
16
+
17
+ class CampaignManagerServiceException(Exception):
18
+ pass
19
+
20
+
21
+ class CampaignManagerService:
22
+ def __init__(self, url: str = None):
23
+ load_dotenv()
24
+ self._URL = url or os.environ.get("CM360_SERVICE_PATH")
25
+ self._CREATE_REPORT = self._URL + "/create-report"
26
+ self._GET_REPORT = self._URL + "/get-report"
27
+ self._RUN_REPORT = self._URL + "/run-report"
28
+ self._PATCH_REPORT = self._URL + "/patch-report"
29
+ self._DELETE_REPORT = self._URL + "/delete-report"
30
+ self._GET_FILE = self._URL + "/get-file"
31
+ self._GET_FILE_MEDIA_REQUEST = self._URL + "/get-file-media-request"
32
+ self._QUERY_DIMENSION_VALUES = self._URL + "/query-dimension-values"
33
+
34
+ def create_report(
35
+ self,
36
+ account_id: str,
37
+ name: str,
38
+ start_date: str,
39
+ end_date: str,
40
+ dimensions: list,
41
+ metrics_names: list,
42
+ dimension_filters: list,
43
+ secret_id: str,
44
+ ) -> Dict:
45
+ """
46
+ Method for creating a report in API and returning a dict containing its ID for further use
47
+ :param account_id: the Campaign Manager 360 account ID
48
+ :param name: the name for the report
49
+ :param start_date: start date for the report, in yyyy-mm-dd format
50
+ :param end_date: end date for the report, in yyyy-mm-dd format
51
+ :param dimensions: a list containing dimensions for the report
52
+ :param metrics_names: a list containing metrics for the report
53
+ :param dimension_filters: a list containing filters for the dimensions
54
+ :param secret_id: ID of the secret in Secret Manager to be used to access Campaign Manager 360
55
+ :return Dict
56
+ """
57
+ body = {
58
+ "account_id": account_id,
59
+ "name": name,
60
+ "date_range": {"startDate": start_date, "endDate": end_date},
61
+ "dimensions": dimensions,
62
+ "metrics_names": metrics_names,
63
+ "dimension_filters": dimension_filters,
64
+ "secret_id": secret_id,
65
+ }
66
+ header = get_headers(self._CREATE_REPORT)
67
+ response = request(
68
+ method="post", url=self._CREATE_REPORT, json=body, headers=header
69
+ )
70
+
71
+ if response.status_code == HTTPStatus.OK:
72
+ service_response = response.json()
73
+ return service_response["data"]
74
+ else:
75
+ raise CampaignManagerServiceException(response.content)
76
+
77
+ def get_report(self, account_id: str, report_id: str, secret_id: str) -> Dict:
78
+ """
79
+ Method to return report
80
+ :param account_id: The ID of the account to fetch the report from
81
+ :param report_id: The ID of the report
82
+ :param secret_id: ID of the secret in Secret Manager to be used to access Campaign Manager 360
83
+ :return Dict
84
+ """
85
+ body = {
86
+ "account_id": account_id,
87
+ "report_id": report_id,
88
+ "secret_id": secret_id,
89
+ }
90
+
91
+ header = get_headers(self._GET_REPORT)
92
+ response = request("post", url=self._GET_REPORT, json=body, headers=header)
93
+
94
+ if response.status_code == HTTPStatus.OK:
95
+ service_response = response.json()
96
+ return service_response["data"]
97
+ else:
98
+ raise CampaignManagerServiceException(response.content)
99
+
100
+ def run_report(self, account_id: str, report_id: str, secret_id: str) -> Dict:
101
+ """
102
+ Method to run report
103
+ :param account_id: The ID of the account to run the report for
104
+ :param report_id: The ID of the report
105
+ :param secret_id: ID of the secret in Secret Manager to be used to access Campaign Manager 360
106
+ :return Dict
107
+ """
108
+ body = {
109
+ "account_id": account_id,
110
+ "report_id": report_id,
111
+ "secret_id": secret_id,
112
+ }
113
+ header = get_headers(self._RUN_REPORT)
114
+ response = request("post", url=self._RUN_REPORT, json=body, headers=header)
115
+ service_response = response.json()
116
+
117
+ if response.status_code == HTTPStatus.OK:
118
+ return service_response["data"]
119
+ else:
120
+ raise CampaignManagerServiceException(service_response)
121
+
122
+ # def patch_report(
123
+ # self,
124
+ # report_id: str,
125
+ # advertiser_name: str,
126
+ # start_date: str,
127
+ # end_date: str,
128
+ # dimensions: list,
129
+ # metrics_names: list,
130
+ # dimension_filters: list,
131
+ # secret_id: str,
132
+ # ) -> bool:
133
+ # """
134
+ # Method to patch report
135
+ # :param report_id: The ID of the report.
136
+ # :param advertiser_name: advertiser name of advertiser dimension filter.
137
+ # :param start_date: The start date for which this report should be run.
138
+ # :param end_date: The end date for which this report should be run.
139
+ # :param dimensions: The list of standard dimensions. Valid dimensions: dv360InsertionOrder, dv360LineItem, dv360Site.
140
+ # :param metrics_names: The list of names of metrics. Valid metric names: clickRate, impressions, totalConversions.
141
+ # :param dimension_filters: The list of filters on which dimensions are filtered.
142
+ # :param secret_id: ID of the secret in Secret Manager to be used to access Campaign Manager 360
143
+ # :return Boolean
144
+ # """
145
+ # body = {
146
+ # "report_id": report_id,
147
+ # "advertiser_name": advertiser_name,
148
+ # "date_range": {"startDate": start_date, "endDate": end_date},
149
+ # "dimensions": dimensions,
150
+ # "metrics_names": metrics_names,
151
+ # "dimension_filters": dimension_filters,
152
+ # "secret_id": secret_id,
153
+ # }
154
+ #
155
+ # header = get_headers(self._PATCH_REPORT)
156
+ # response = request("patch", url=self._PATCH_REPORT, json=body, headers=header)
157
+ # service_response = response.json()
158
+ # if response.status_code == HTTPStatus.OK:
159
+ # return True
160
+ # else:
161
+ # raise CampaignManagerServiceException(service_response)
162
+
163
+ def check_report_ready(self, report_id: str, file_id: str, secret_id: str) -> Dict:
164
+ """
165
+ Method to check weather the report is already available for download
166
+ :param report_id: The ID of the report
167
+ :param file_id: The ID of the report file to be downloaded
168
+ :param secret_id: ID of the secret in Secret Manager to be used to access Campaign Manager 360
169
+ :return Dict
170
+ """
171
+ body = {"report_id": report_id, "file_id": file_id, "secret_id": secret_id}
172
+ header = get_headers(self._GET_FILE)
173
+ response = request("post", url=self._GET_FILE, json=body, headers=header)
174
+
175
+ if response.status_code == HTTPStatus.OK:
176
+ service_response = response.json()
177
+ return service_response["data"]["status"] == "REPORT_AVAILABLE"
178
+ else:
179
+ raise CampaignManagerServiceException(response.content)
180
+
181
+ def check_report_ready_with_exponential_backoff(
182
+ self, report_id: str, file_id: str, secret_id: str, backoff_attempts: int = 10
183
+ ) -> bool:
184
+ """
185
+ Implements exponential backoff for pooling the API for readiness of the report, suggested in
186
+ algorithm suggested by https://developers.google.com/doubleclick-advertisers/upload#exp-backoff
187
+ :param report_id: The ID of the report
188
+ :param file_id: The ID of the report file to be downloaded
189
+ :param secret_id: ID of the secret in Secret Manager to be used to access Campaign Manager 360
190
+ :param backoff_attempts: number of attempts
191
+ :return Bool
192
+ """
193
+ for attempt in range(0, backoff_attempts):
194
+ if self.check_report_ready(report_id, file_id, secret_id):
195
+ return True
196
+ else:
197
+ time.sleep((2 ** attempt) + randint(1, 20))
198
+
199
+ return False
200
+
201
+ def get_report_results(self, report_id: str, file_id: str, secret_id: str) -> Dict:
202
+ """
203
+ Method to fetch report, based on https://developers.google.com/doubleclick-advertisers/guides/download_reports
204
+ and https://googleapis.github.io/google-api-python-client/docs/epy/googleapiclient.http.MediaIoBaseDownload-class.html
205
+ :param account_id: The ID of the account to check for the report
206
+ :param report_id: The ID of the report
207
+ :param file_id: The ID of the report file to be downloaded
208
+ :param secret_id: ID of the secret in Secret Manager to be used to access Campaign Manager 360
209
+ :return Dict
210
+ """
211
+ body = {"report_id": report_id, "file_id": file_id, "secret_id": secret_id}
212
+ header = get_headers(self._GET_FILE_MEDIA_REQUEST)
213
+
214
+ # fetch the authorized request from our service for downloading the report from API
215
+ response = request(
216
+ "post", url=self._GET_FILE_MEDIA_REQUEST, json=body, headers=header
217
+ )
218
+
219
+ if response.status_code == HTTPStatus.OK:
220
+ service_response = response.json()
221
+ authorised_request = HttpRequest.from_json(
222
+ json.dumps(service_response["data"]),
223
+ http=httplib2.Http(),
224
+ postproc=None,
225
+ )
226
+
227
+ # crete tmp directory if not exist
228
+ if not os.path.exists("tmp"):
229
+ os.makedirs("tmp")
230
+ result_temp_file_location = os.path.join("tmp", "report-result-temp.csv")
231
+ # prepare a local file to download the report contents to
232
+ result_temp_file = io.FileIO(result_temp_file_location, mode="wb")
233
+ # 1024 * 1024 * 10 = 10 MB, based on best practices described in CM360 API documentation, default would be 1 MB
234
+ chunk_size = 10485760
235
+ # create a media downloader instance
236
+ media_downloader = MediaIoBaseDownload(
237
+ result_temp_file, authorised_request, chunksize=chunk_size
238
+ )
239
+ # execute the get request and download the file.
240
+ download_finished = False
241
+ while download_finished is False:
242
+ _, download_finished = media_downloader.next_chunk()
243
+
244
+ result_temp_file.close()
245
+
246
+ skip = 0
247
+ with open(result_temp_file_location, "r") as file:
248
+ for line in file:
249
+ skip += 1
250
+ if "Report Fields" == line.strip():
251
+ break
252
+
253
+ results = read_csv(
254
+ result_temp_file_location,
255
+ on_bad_lines="skip",
256
+ skiprows=skip, # skip metadata
257
+ skipfooter=1, # last row contains "grand total"
258
+ engine="python", # otherwise warning that skipfooter arg does not exist
259
+ )
260
+ results = results.to_dict(orient="records")
261
+
262
+ # get rid of the tmp file
263
+ os.remove(result_temp_file_location)
264
+ return results
265
+
266
+ else:
267
+ raise CampaignManagerServiceException(response.content)
268
+
269
+ def delete_report(self, account_id: str, report_id: str, secret_id: str) -> bool:
270
+ """
271
+ Method to delete report
272
+ :param account_id: The ID of the account to check for the report
273
+ :param report_id: The ID of the report
274
+ :param secret_id: ID of the secret in Secret Manager to be used to access Campaign Manager 360
275
+ :return Boolean
276
+ """
277
+ body = {
278
+ "account_id": account_id,
279
+ "report_id": report_id,
280
+ "secret_id": secret_id,
281
+ }
282
+ header = get_headers(self._DELETE_REPORT)
283
+ response = request("post", url=self._DELETE_REPORT, json=body, headers=header)
284
+
285
+ if response.status_code == HTTPStatus.OK:
286
+ return True
287
+ else:
288
+ raise CampaignManagerServiceException(response.content)
289
+
290
+ def query_dimension_values(
291
+ self,
292
+ account_id: str,
293
+ secret_id: str,
294
+ dimension_name: str,
295
+ filters: List,
296
+ start_date: str,
297
+ end_date: int,
298
+ max_results: int = 100,
299
+ ) -> List[Dict]:
300
+ """
301
+ Retrieves values of selected dimensions for data filtered according to rules set in list of filters
302
+ :param account_id: Account ID
303
+ :param dimension_name: The name of the dimension for which values should be requested
304
+ :param secret_id: The ID of the secret in secret manager
305
+ :param filters: The list of filters by which to filter values
306
+ :param start_date: The start date of the date range for which to retrieve dimension values
307
+ :param end_date: The end date of the date range for which to retrieve dimension values
308
+ :param max_results: Maximum number of results to return
309
+
310
+
311
+ return {"next_page_token": token, items: values of selected dimensions}
312
+ """
313
+ body = {
314
+ "account_id": account_id,
315
+ "dimension_name": dimension_name,
316
+ "secret_id": secret_id,
317
+ "filters": filters,
318
+ "start_date": start_date,
319
+ "end_date": end_date,
320
+ "page_token": None,
321
+ "max_results": max_results,
322
+ }
323
+
324
+ header = get_headers(self._QUERY_DIMENSION_VALUES)
325
+ response = request(
326
+ "post", url=self._QUERY_DIMENSION_VALUES, json=body, headers=header
327
+ )
328
+
329
+ if response.status_code != HTTPStatus.OK:
330
+ raise CampaignManagerServiceException(response.content)
331
+
332
+ service_response = response.json()
333
+ yield service_response["data"]["items"]
334
+
335
+ # if there was a last page response is empty string
336
+ while service_response["data"]["nextPageToken"]:
337
+ body["page_token"] = service_response["data"]["nextPageToken"]
338
+ response = request(
339
+ "post", url=self._QUERY_DIMENSION_VALUES, json=body, headers=header
340
+ )
341
+
342
+ if response.status_code != HTTPStatus.OK:
343
+ raise CampaignManagerServiceException(response.content)
344
+
345
+ service_response = response.json()
346
+ yield service_response["data"]["items"]