-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathanalytics_export.py
More file actions
247 lines (201 loc) · 8.51 KB
/
Copy pathanalytics_export.py
File metadata and controls
247 lines (201 loc) · 8.51 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
"""
Analytics export example — top videos with ad stats.
Generates an Excel workbook with:
- Sheet 1: Top N content videos by views (with viewcount reach & ad metrics)
- Sheet 2: Per-video pre-roll creative breakdown
Usage::
pip install bb-sapi-python-sdk openpyxl
python examples/analytics_export.py
Configure via environment variables or edit the CONFIG dict below.
"""
from __future__ import annotations
import os
from typing import Any
try:
import openpyxl
from openpyxl.styles import Font, PatternFill
from openpyxl.utils import get_column_letter
except ImportError:
raise SystemExit("Install openpyxl: pip install openpyxl")
from bb_sapi import SapiClient
from bb_sapi.exceptions import SapiAuthError, SapiError
# ---------------------------------------------------------------------------
# Configuration
# ---------------------------------------------------------------------------
CONFIG = {
"base_url": os.getenv("SAPI_BASE_URL", "https://mypublication.bbvms.com"),
"shared_secret": os.getenv("SAPI_SHARED_SECRET", ""),
"from_date": os.getenv("SAPI_FROM_DATE", "2026-01-01"),
"to_date": os.getenv("SAPI_TO_DATE", "2026-03-31"),
"top_n": int(os.getenv("SAPI_TOP_N", "50")),
"output_file": os.getenv("SAPI_OUTPUT", "analytics_export.xlsx"),
}
REACH_THRESHOLDS = (20, 40, 60, 80, 95)
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def fetch_top_videos(client: SapiClient, cfg: dict[str, Any]) -> list[dict[str, Any]]:
"""Fetch the top N content videos by view count."""
top = client.analytics.top_videos(cfg["from_date"], cfg["to_date"], limit=cfg["top_n"])
# Enrich with metadata and filter out ad creatives (usetype: commercial)
ids = [v["id"] for v in top]
meta: dict[str, dict] = {}
for chunk_ids in _chunks(ids, 50):
q = "id:(" + " OR ".join(str(i) for i in chunk_ids) + ")"
results = client.search(q, entity_type="MediaClip", fields="id,title,usetype,duration")
for item in results.get("items", []):
meta[str(item["id"])] = item
content = []
for v in top:
vid_meta = meta.get(str(v["id"]))
if vid_meta is None:
print(f" Warning: video {v['id']} not found in metadata search; skipping.")
continue
if vid_meta.get("usetype") != "commercial":
v.update(vid_meta)
content.append(v)
return content
def enrich_with_stats(
client: SapiClient,
videos: list[dict[str, Any]],
cfg: dict[str, Any],
) -> None:
"""Add analytics stats in-place to each video dict."""
from_date, to_date = cfg["from_date"], cfg["to_date"]
failed = 0
for v in videos:
vid_id = str(v["id"])
try:
ad = client.analytics.ad_stats_per_video(vid_id, from_date, to_date)
reach = client.analytics.viewcount_reach(
vid_id, from_date, to_date, thresholds=REACH_THRESHOLDS
)
except SapiAuthError:
raise # credentials broken — abort immediately
except SapiError as exc:
print(
f" ERROR: failed stats for {vid_id} ({v.get('title', '?')}): {exc}; "
f"recording as empty."
)
v["impressions"] = None
v["lineitems"] = {}
v["vast_quartiles"] = {}
v["reach"] = {}
failed += 1
continue
v["impressions"] = ad["impressions"]
v["lineitems"] = ad["lineitems"]
v["vast_quartiles"] = ad["vastQuartiles"]
v["reach"] = reach
print(f" {v.get('title', vid_id)}: {v['views']} views, {v['impressions']} impressions")
if failed:
print(f" WARNING: {failed}/{len(videos)} videos failed stats fetch.")
def fetch_creatives(
client: SapiClient,
videos: list[dict[str, Any]],
cfg: dict[str, Any],
) -> dict[str, list[dict]]:
"""For each lineitem that appeared, fetch which creative(s) were active."""
all_lineitems: set[str] = set()
for v in videos:
all_lineitems.update(v.get("lineitems", {}).keys())
creative_map: dict[str, list[dict]] = {}
failed = 0
for name in sorted(all_lineitems):
try:
creative_map[name] = client.lineitem.creatives_for_period(
name, cfg["from_date"], cfg["to_date"]
)
except SapiAuthError:
raise # credentials broken — abort immediately
except SapiError as exc:
print(f" Warning: could not fetch creatives for {name!r}: {exc}")
creative_map[name] = []
failed += 1
if failed:
print(f" WARNING: {failed}/{len(all_lineitems)} lineitems failed creative resolution.")
return creative_map
# ---------------------------------------------------------------------------
# Excel generation
# ---------------------------------------------------------------------------
HEADER_FILL = PatternFill("solid", fgColor="1F4E79")
HEADER_FONT = Font(color="FFFFFF", bold=True)
ALT_FILL = PatternFill("solid", fgColor="EBF3FB")
def _header(ws: Any, row: int, values: list[str]) -> None:
for col, val in enumerate(values, start=1):
cell = ws.cell(row=row, column=col, value=val)
cell.fill = HEADER_FILL
cell.font = HEADER_FONT
def _autofit(ws: Any) -> None:
for col in ws.columns:
max_len = max((len(str(c.value or "")) for c in col), default=10)
ws.column_dimensions[get_column_letter(col[0].column)].width = min(max_len + 2, 40)
def build_workbook(
videos: list[dict[str, Any]],
creative_map: dict[str, list[dict]],
) -> openpyxl.Workbook:
wb = openpyxl.Workbook()
# ------------------------------------------------------------------
# Sheet 1: video overview
# ------------------------------------------------------------------
ws1 = wb.active
ws1.title = "Videos"
headers = [
"ID", "Title", "Views", "Impressions",
"VAST 25%", "VAST 50%", "VAST 75%", "VAST 100%",
] + [f"Reach ≥{t}%" for t in REACH_THRESHOLDS]
_header(ws1, 1, headers)
for i, v in enumerate(videos, start=2):
vq = v.get("vast_quartiles", {})
reach = v.get("reach", {})
row = [
v["id"], v.get("title", ""), v["views"], v.get("impressions", 0),
vq.get("25", 0), vq.get("50", 0), vq.get("75", 0), vq.get("100", 0),
] + [reach.get(t, 0) for t in REACH_THRESHOLDS]
for col, val in enumerate(row, start=1):
cell = ws1.cell(row=i, column=col, value=val)
if i % 2 == 0:
cell.fill = ALT_FILL
_autofit(ws1)
# ------------------------------------------------------------------
# Sheet 2: pre-roll creative breakdown
# ------------------------------------------------------------------
ws2 = wb.create_sheet("Pre-roll Creatives")
_header(ws2, 1, ["Lineitem", "Creative ID", "Version Date", "VAST URL"])
row_num = 2
for name in sorted(creative_map):
for c in creative_map[name]:
ws2.cell(row=row_num, column=1, value=name)
ws2.cell(row=row_num, column=2, value=c.get("creative_id", "external"))
ws2.cell(row=row_num, column=3, value=c.get("date", ""))
ws2.cell(row=row_num, column=4, value=c.get("vast_url", ""))
if row_num % 2 == 0:
for col in range(1, 5):
ws2.cell(row=row_num, column=col).fill = ALT_FILL
row_num += 1
_autofit(ws2)
return wb
# ---------------------------------------------------------------------------
# Entry point
# ---------------------------------------------------------------------------
def _chunks(lst: list, size: int):
for i in range(0, len(lst), size):
yield lst[i : i + size]
def main() -> None:
cfg = CONFIG
if not cfg["shared_secret"]:
raise SystemExit("Set SAPI_SHARED_SECRET environment variable.")
client = SapiClient(cfg["base_url"], cfg["shared_secret"])
print(f"Fetching top {cfg['top_n']} videos ({cfg['from_date']} – {cfg['to_date']})…")
videos = fetch_top_videos(client, cfg)
print(f"Found {len(videos)} content videos.")
print("Fetching per-video stats…")
enrich_with_stats(client, videos, cfg)
print("Resolving pre-roll creatives…")
creative_map = fetch_creatives(client, videos, cfg)
print("Building workbook…")
wb = build_workbook(videos, creative_map)
wb.save(cfg["output_file"])
print(f"Saved: {cfg['output_file']}")
if __name__ == "__main__":
main()