github-actions[bot] commited on
Commit
bc7640d
·
1 Parent(s): eae7723

sync: automatic content update from github

Browse files
README.md CHANGED
@@ -1,10 +1,12 @@
1
  ---
2
  title: Sales Toolkit
3
- emoji: 🚀
4
- colorFrom: gray
5
- colorTo: purple
6
- sdk: static
 
 
7
  pinned: false
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
1
  ---
2
  title: Sales Toolkit
3
+ emoji: 🦀
4
+ colorFrom: purple
5
+ colorTo: pink
6
+ sdk: streamlit
7
+ sdk_version: 1.45.0
8
+ app_file: app.py
9
  pinned: false
10
  ---
11
 
12
+ Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
Universal%20Media%20Plan%20-%20Input%20Table (1).csv ADDED
@@ -0,0 +1,298 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Product,Placement,Text,Package Exist,Package Elements,Unit,PMP Rate,PG Rate,Direct IO Rate,Rate Type,Site(s),Ad Size,Start Date,End Date,Rate,Units,Cost,SOV,Notes,Desktop,Tablet,Mobile,Available PG,Available PMP,Third Party Creative
2
+ Creator Content+ - Written,# Pieces of Creator Content+ [Article(s)/Recipes] & # Social Posts_Package,Written,Yes,,,-,-,1.15,CPPV,Raptive,PKG,Insert,Insert,See rate card,,,100%,"No third party tracking on post or social; page views listed; must bill off of first party numbers
3
+
4
+ Non-cancellable",x,x,x,No,No,N
5
+ Creator Content+ - Written,Creator Content+ Sponsorship_Surrounding Newly Created Content_Leaderboard,Written,Yes,Leaderboard,,-,-,-,-,Raptive,728x90,-,-,-,-,-,-,,x,x,,-,-,Y
6
+ Creator Content+ - Written,Creator Content+ Sponsorship_Surrounding Newly Created Content_Medium Rectangle,Written,Yes,Medium Rectangle,,-,-,-,-,Raptive,300x250,-,-,-,-,-,-,,x,x,x,-,-,Y
7
+ Creator Content+ - Written,Creator Content+ Sponsorship_Surrounding Newly Created Content_Mobile Banner,Written,Yes,Mobile Banner,,-,-,-,-,Raptive,320x50,-,-,-,-,-,-,,,,x,-,-,Y
8
+ Creator Content+ - Video,# Pieces of Creator Content+ [Videos] + # Social Posts,Video,No,,# Pieces of Creator Content+ [Videos] + # Social Posts,-,-,0.05,CPV,Raptive,n/a,Insert,Insert,See rate card,,,100%,"No third party tracking; social views listed (3s); must bill off of first party numbers
9
+
10
+ Non-cancellable",x,x,x,No,No,N
11
+ Creator Content+ - In-Feed Social,Creator Content+_# Creators_Each doing # In-feed Social Post(s) [total of # posts],In-Feed Social,No,,Creator Content+_# Creators_Each doing # In-feed Social Post(s) [total of # posts],-,-,8,CPM,Raptive,n/a,Insert,Insert,See rate card,,,100%,"No third party tracking; must bill off of first party numbers
12
+
13
+ Non-cancellable",x,x,x,No,No,N
14
+ Creator Content Social Amplifier+,Creator Content Social Amplifier+ [# Articles]_Package,Articles,Yes,,,-,-,8,CPM,Raptive,PKG,Insert,Insert,See rate card,,,100%,"No third party tracking on social; must bill off of first party numbers
15
+
16
+ On-site roabdlock will appear when the user enters from social
17
+
18
+ Non-cancellable",x,x,x,No,No,N
19
+ Creator Content Social Amplifier+,Creator Content Social Amplifier+ Roadblock_Leaderboard,Articles,Yes,Leaderboard,,-,-,-,-,Raptive,728x90,-,-,-,-,-,-,,x,x,,-,-,Y
20
+ Creator Content Social Amplifier+,Creator Content Social Amplifier+ Roadblock_Medium Rectangle,Articles,Yes,Medium Rectangle,,-,-,-,-,Raptive,300x250,-,-,-,-,-,-,,x,x,x,-,-,Y
21
+ Creator Content Social Amplifier+,Creator Content Social Amplifier+ Roadblock_Mobile Banner,Articles,Yes,Mobile Banner,,-,-,-,-,Raptive,320x50,-,-,-,-,-,-,,,,x,-,-,Y
22
+ Creator Social Video Amplifier+,Creator Social Video Amplifier+ [# Videos],Videos,Yes,,,-,-,8,CPM,Raptive,PKG,Insert,Insert,See rate card,,,100%,"No third party tracking on social; must bill off of first party numbers
23
+
24
+ Non-cancellable",x,x,x,No,No,N
25
+ Newsletters (featuring creators included in Creator Content+),Newsletter Sponsorship Package_# Sponsored Email Newsletters per Creator (with Creator Content + Content Distribution) - Creator with ~#k - #k Subscribers (# Email Newsletters Total),,No,,Newsletter Sponsorship Package_# Sponsored Email Newsletters per Creator (with Creator Content + Content Distribution) - Creator with ~#k - #k Subscribers (# Email Newsletters Total),-,-,25,CPM,Raptive,n/a,Insert,Insert,See rate card,,,100%,"Clicks from Email Newsletter to Creator Content+ Will Not Count Toward CPPV Goal
26
+
27
+ No third party tracking; sends listed; must bill off first party numbers
28
+
29
+ Non-cancellable
30
+
31
+ If the creators selected for this program do not have newsletters, we will shift that budget into other placements.",x,x,x,No,No,N
32
+ Content Sponsorships,[Insert Topic] Creator Content Sponsorship_Package,,Yes,,,-,-,17,CPM,Raptive,PKG,Insert (1 week min),Insert (1 week min),CPM,,,100%,"Guaranteed placement for first 60s of user session
33
+
34
+ Package delivery may include related content impressions
35
+
36
+ Package will go live at 9am eastern",PKG,PKG,PKG,No,No,PKG
37
+ Content Sponsorships,[Insert Topic] Creator Content Sponsorship_Leaderboard,,Yes,Leaderboard,,-,-,-,-,Raptive,728x90,-,-,-,-,-,-,,x,x,,-,-,Y
38
+ Content Sponsorships,[Insert Topic] Creator Content Sponsorship_Medium Rectangle,,Yes,Medium Rectangle,,-,-,-,-,Raptive,300x250,-,-,-,-,-,-,,x,x,x,-,-,Y
39
+ Content Sponsorships,[Insert Topic] Creator Content Sponsorship_Mobile Banner,,Yes,Mobile Banner,,-,-,-,-,Raptive,320x50,-,-,-,-,-,-,,,,x,-,-,Y
40
+ Content Sponsorships,[Insert Topic] Creator Content Sponsorship_Sponsorship Tile,,Yes,Sponsorship Tile,,-,-,-,-,Raptive,Logo,-,-,-,-,-,-,"OPTIONAL
41
+ No third party tracking",x,x,x,-,-,N
42
+ Content Sponsorships,[Insert Topic] Creator Content Sponsorship_Package,Food Product Advertisers Only,Yes,,,-,-,17,CPM,Raptive,PKG,Insert (1 week min),Insert (1 week min),CPM,,,100%,"Guaranteed placement for first 60s of user session (728x90, 300x250, 320x50)
43
+
44
+ Package delivery may include related content impressions
45
+
46
+ Package will go live at 9am eastern",PKG,PKG,PKG,No,No,PKG
47
+ Content Sponsorships,[Insert Topic] Creator Content Sponsorship_Leaderboard,Food Product Advertisers Only,Yes,Leaderboard,,-,-,-,-,Raptive,728x90,-,-,-,-,-,-,,x,x,,-,-,Y
48
+ Content Sponsorships,[Insert Topic] Creator Content Sponsorship_Medium Rectangle,Food Product Advertisers Only,Yes,Medium Rectangle,,-,-,-,-,Raptive,300x250,-,-,-,-,-,-,,x,x,x,-,-,Y
49
+ Content Sponsorships,[Insert Topic] Creator Content Sponsorship_Mobile Banner,Food Product Advertisers Only,Yes,Mobile Banner,,-,-,-,-,Raptive,320x50,-,-,-,-,-,-,,,,x,-,-,Y
50
+ Content Sponsorships,[Insert Topic] Creator Content Sponsorship_In-Recipe Product Recommendation Unit,Food Product Advertisers Only,Yes,In-Recipe Product Recommendation Unit,,-,-,-,-,Raptive,1x1,-,-,-,-,-,-,Persistent,x,x,x,-,-,N
51
+ Content Sponsorships,[Insert Topic] Creator Content Sponsorship_Sponsorship Tile,Food Product Advertisers Only,Yes,Sponsorship Tile,,-,-,-,-,Raptive,Logo,-,-,-,-,-,-,"OPTIONAL
52
+ No third party tracking",x,x,x,-,-,N
53
+ Roadblocks,First Impression Roadblock Targeted to [Insert Channel]_Package,,Yes,,,-,-,16,CPM,Raptive,PKG,Insert,Insert,See rate card,,,<10%,"Impressions are delivered on a first non-sponsored view. Brand does not have permanent roadblocks on qualifying content.
54
+
55
+ Package will go live at 9am eastern",PKG,PKG,PKG,No,No,PKG
56
+ Roadblocks,First Impression Roadblock Targeted to [Insert Channel]_Leaderboard,,Yes,Leaderboard,,-,-,-,-,Raptive,728x90,-,-,-,-,-,-,,x,x,,-,-,Y
57
+ Roadblocks,First Impression Roadblock Targeted to [Insert Channel]_Medium Rectangle,,Yes,Medium Rectangle,,-,-,-,-,Raptive,300x250,-,-,-,-,-,-,,x,x,x,-,-,Y
58
+ Roadblocks,First Impression Roadblock Targeted to [Insert Channel]_Mobile Banner,,Yes,Mobile Banner,,-,-,-,-,Raptive,320x50,-,-,-,-,-,-,,,,x,-,-,Y
59
+ Roadblocks,First Impression Roadblock with Video_Targeted to [Insert Channel]_Package,,Yes,,,-,-,18,CPM,Raptive,PKG,Insert,Insert,See rate card,,,<10%,"Impressions are delivered on a first non-sponsored view. Brand does not have permanent roadblocks on qualifying content.
60
+
61
+ Package will go live at 9am eastern",PKG,PKG,PKG,No,No,PKG
62
+ Roadblocks,First Impression Roadblock with Video_Targeted to [Insert Channel]_Leaderboard,,Yes,Leaderboard,,-,-,-,-,Raptive,728x90,-,-,-,-,-,-,,x,x,,-,-,Y
63
+ Roadblocks,First Impression Roadblock with Video_Targeted to [Insert Channel]_Medium Rectangle,,Yes,Medium Rectangle,,-,-,-,-,Raptive,300x250,-,-,-,-,-,-,,x,x,x,-,-,Y
64
+ Roadblocks,First Impression Roadblock with Video_Targeted to [Insert Channel]_Mobile Banner,,Yes,Mobile Banner,,-,-,-,-,Raptive,320x50,-,-,-,-,-,-,,,,x,-,-,Y
65
+ Roadblocks,First Impression Roadblock with Video_Targeted to [Insert Channel]_Pre-roll,,Yes,Pre-roll,,-,-,-,-,Raptive,"16:9 Aspect Ratio, :06s, :15s, :30s",-,-,-,-,-,-,,x,x,x,-,-,Y
66
+ Roadblocks,Recipe Recommendation First Impression Roadblock [Insert Channel]_Package,???,Yes,,,-,-,16,CPM,Raptive,PKG,Insert,Insert,See rate card,,,<10%,"Impressions are delivered on a first non-sponsored view. Brand does not have permanent roadblocks on qualifying content.
67
+
68
+ Package will go live at 9am eastern",PKG,PKG,PKG,Yes,No,PKG
69
+ Roadblocks,Recipe Recommendation First Impression Roadblock [Insert Channel]_Leaderboard,,Yes,Leaderboard,,-,-,-,-,Raptive,728x90,-,-,-,-,-,-,,x,x,,-,-,Y
70
+ Roadblocks,Recipe Recommendation First Impression Roadblock [Insert Channel]_Medium Rectangle,,Yes,Medium Rectangle,,-,-,-,-,Raptive,300x250,-,-,-,-,-,-,,x,x,x,-,-,Y
71
+ Roadblocks,Recipe Recommendation First Impression Roadblock [Insert Channel]_Mobile Banner,,Yes,Mobile Banner,,-,-,-,-,Raptive,320x50,-,-,-,-,-,-,,,,x,-,-,Y
72
+ Roadblocks,Recipe Recommendation First Impression Roadblock [Insert Channel]_In-Recipe Product Recommendation Unit,,Yes,In-Recipe Product Recommendation Unit,,-,-,-,-,Raptive,1x1,-,-,-,-,-,-,,x,x,x,-,-,N
73
+ Roadblocks,Recipe Box First Impression Roadblock [Insert Channel]_Package,???,Yes,,,-,-,16,CPM,Raptive,PKG,Insert,Insert,See rate card,,,<10%,"Impressions are delivered on a first non-sponsored view. Brand does not have permanent roadblocks on qualifying content.
74
+
75
+ Package will go live at 9am eastern",PKG,PKG,PKG,Yes,No,PKG
76
+ Roadblocks,Recipe Box First Impression Roadblock [Insert Channel]_Medium Rectangle,,Yes,Medium Rectangle,,-,-,-,-,Raptive,300x250,-,-,-,-,-,-,,x,x,x,-,-,Y
77
+ Roadblocks,Recipe Box First Impression Roadblock [Insert Channel]_In-Recipe Product Recommendation Unit,,Yes,In-Recipe Product Recommendation Unit,,-,-,-,-,Raptive,1x1,-,-,-,-,-,-,,x,x,x,-,-,N
78
+ Roadblocks,Beyond Banner First Impression Roadblock Targeted to [Insert Channel]_Package,,Yes,,,-,-,17,CPM,Raptive,PKG,Insert,Insert,See rate card,,,<10%,"Impressions are delivered on a first non-sponsored view. Brand does not have permanent roadblocks on qualifying content.
79
+
80
+ Package will go live at 9am eastern",PKG,PKG,PKG,No,No,PKG
81
+ Roadblocks,Beyond Banner First Impression Roadblock Targeted to [Insert Channel]_Beyond Banner,,Yes,Beyond Banner,,-,-,-,-,Raptive,1x1,-,-,-,-,-,-,,x,x,x,-,-,N
82
+ Roadblocks,Beyond Banner First Impression Roadblock Targeted to [Insert Channel]_Medium Rectangle,,Yes,Medium Rectangle,,-,-,-,-,Raptive,300x250,-,-,-,-,-,-,,x,x,x,-,-,Y
83
+ Roadblocks,Mural First Impression Roadblock Targeted to [Insert Channel]_Package,???,Yes,,,-,-,16,CPM,Raptive,PKG,Insert,Insert,See rate card,,,<10%,"Impressions are delivered on a first non-sponsored view. Brand does not have permanent roadblocks on qualifying content.
84
+
85
+ Package will go live at 9am eastern",PKG,PKG,PKG,Yes,No,PKG
86
+ Roadblocks,Mural First Impression Roadblock Targeted to [Insert Channel]_Leaderboard,,Yes,Leaderboard,,-,-,-,-,Raptive,728x90,-,-,-,-,-,-,,x,x,,-,-,Y
87
+ Roadblocks,Mural First Impression Roadblock Targeted to [Insert Channel]_Medium Rectangle,,Yes,Medium Rectangle,,-,-,-,-,Raptive,300x250,-,-,-,-,-,-,,x,x,x,-,-,Y
88
+ Roadblocks,Mural First Impression Roadblock Targeted to [Insert Channel]_Mural,,Yes,Mural,,-,-,-,-,Raptive,1x1,-,-,-,-,-,-,,x,x,x,-,-,N
89
+ Product Recommendation Units,In-Recipe Product Recommendation Unit Targeted to [Insert Recipe Type],,No,,In-Recipe Product Recommendation Unit Targeted to [Insert Recipe Type],-,8,8,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,Yes,No,N
90
+ Product Recommendation Units,In-Recipe Digital Circular Unit Targeted to [Insert Recipe Type],,No,,In-Recipe Digital Circular Unit Targeted to [Insert Recipe Type],-,8,8,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,Yes,No,N
91
+ Product Recommendation Units,Product Recommendation Unit Targeted to [Insert Content Type],,No,,Product Recommendation Unit Targeted to [Insert Content Type],-,-,9,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,Yes,No,N
92
+ High-Impact Media,Frame Targeted to [Insert Channel],,No,,Frame Targeted to [Insert Channel],7,8,8,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,Yes,Yes,N
93
+ High-Impact Media,Frame 360 View Targeted to [Insert Channel],,No,,Frame 360 View Targeted to [Insert Channel],8,9,9,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,,,
94
+ High-Impact Media,Frame Carousel Targeted to [Insert Channel],,No,,Frame Carousel Targeted to [Insert Channel],7,8,8,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,,,
95
+ High-Impact Media,Frame Cart Connect Targeted to [Insert Channel],,No,,Frame Cart Connect Targeted to [Insert Channel],-,10,10,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,,,
96
+ High-Impact Media,Frame Content Collection Targeted to [Insert Channel],,No,,Frame Content Collection Targeted to [Insert Channel],8,9,9,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,,,
97
+ High-Impact Media,Frame Countdown Targeted to [Insert Channel],,No,,Frame Countdown Targeted to [Insert Channel],7,8,8,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,,,
98
+ High-Impact Media,Frame Digital Circular Targeted to [Insert Channel],,No,,Frame Digital Circular Targeted to [Insert Channel],-,9,9,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,,,
99
+ High-Impact Media,Frame Dynamic Creative Targeted to [Insert Channel],,No,,Frame Dynamic Creative Targeted to [Insert Channel],7,8,8,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,,,
100
+ High-Impact Media,Frame Glider Targeted to [Insert Channel],,No,,Frame Glider Targeted to [Insert Channel],8,9,9,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,,,
101
+ High-Impact Media,Frame Hotspot Targeted to [Insert Channel],,No,,Frame Hotspot Targeted to [Insert Channel],8,9,9,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,,,
102
+ High-Impact Media,Frame Retail Connect Targeted to [Insert Channel],,No,,Frame Retail Connect Targeted to [Insert Channel],7,8,8,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,,,
103
+ High-Impact Media,Frame Scratch Off/Wipe Away Targeted to [Insert Channel],,No,,Frame Scratch Off/Wipe Away Targeted to [Insert Channel],8,9,9,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,,,
104
+ High-Impact Media,Frame Scroll Reactive Animation Targeted to [Insert Channel],,No,,Frame Scroll Reactive Animation Targeted to [Insert Channel],7,8,8,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,,,
105
+ High-Impact Media,Frame Visualizer Targeted to [Insert Channel],,No,,Frame Visualizer Targeted to [Insert Channel],8,9,9,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,,,
106
+ High-Impact Media,Panorama Targeted to [Insert Channel],,No,,Panorama Targeted to [Insert Channel],8,11,11,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,Yes,Yes,N
107
+ High-Impact Media,Panorama 360 View Targeted to [Insert Channel],,No,,Panorama 360 View Targeted to [Insert Channel],9,12,12,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,,,
108
+ High-Impact Media,Panorama Carousel Targeted to [Insert Channel],,No,,Panorama Carousel Targeted to [Insert Channel],8,11,11,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,,,
109
+ High-Impact Media,Panorama Cart Connect Targeted to [Insert Channel],,No,,Panorama Cart Connect Targeted to [Insert Channel],-,13,13,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,,,
110
+ High-Impact Media,Panorama Content Collection Targeted to [Insert Channel],,No,,Panorama Content Collection Targeted to [Insert Channel],9,12,12,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,,,
111
+ High-Impact Media,Panorama Countdown Targeted to [Insert Channel],,No,,Panorama Countdown Targeted to [Insert Channel],8,11,11,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,,,
112
+ High-Impact Media,Panorama Creator Endorsement Targeted to [Insert Channel],,No,,Panorama Creator Endorsement Targeted to [Insert Channel],-,15,15,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,Non-cancellable,x,x,x,,,
113
+ High-Impact Media,Panorama Creator Product Curation Targeted to [Insert Channel],,No,,Panorama Creator Product Curation Targeted to [Insert Channel],-,15,15,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,Non-cancellable,x,x,x,,,
114
+ High-Impact Media,Panorama Digital Circular Targeted to [Insert Channel],,No,,Panorama Digital Circular Targeted to [Insert Channel],-,12,12,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,,,
115
+ High-Impact Media,Panorama Dynamic Creative Targeted to [Insert Channel],,No,,Panorama Dynamic Creative Targeted to [Insert Channel],8,11,11,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,,,
116
+ High-Impact Media,Panorama Glider Targeted to [Insert Channel],,No,,Panorama Glider Targeted to [Insert Channel],9,12,12,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,,,
117
+ High-Impact Media,Panorama Hotspot Targeted to [Insert Channel],,No,,Panorama Hotspot Targeted to [Insert Channel],9,12,12,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,,,
118
+ High-Impact Media,Panorama Interactive Shopper Targeted to [Insert Channel],,No,,Panorama Interactive Shopper Targeted to [Insert Channel],9,12,12,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,,,
119
+ High-Impact Media,Panorama Quiz Targeted to [Insert Channel],,No,,Panorama Quiz Targeted to [Insert Channel],10,13,13,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,,,
120
+ High-Impact Media,Panorama Retail Connect Targeted to [Insert Channel],,No,,Panorama Retail Connect Targeted to [Insert Channel],8,11,11,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,,,
121
+ High-Impact Media,Panorama Scratch Off/Wipe Away Targeted to [Insert Channel],,No,,Panorama Scratch Off/Wipe Away Targeted to [Insert Channel],9,12,12,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,,,
122
+ High-Impact Media,Panorama Scroll Reactive Animation Targeted to [Insert Channel],,No,,Panorama Scroll Reactive Animation Targeted to [Insert Channel],8,11,11,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,,,
123
+ High-Impact Media,Panorama Shoppable Video Targeted to [Insert Channel],,No,,Panorama Shoppable Video Targeted to [Insert Channel],10,13,13,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,,,
124
+ High-Impact Media,Panorama Video Targeted to [Insert Channel],,No,,Panorama Video Targeted to [Insert Channel],10,13,13,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,,,
125
+ High-Impact Media,Panorama Visualizer Targeted to [Insert Channel],,No,,Panorama Visualizer Targeted to [Insert Channel],9,12,12,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,,,
126
+ High-Impact Media,Mural Targeted to [Insert Channel],,No,,Mural Targeted to [Insert Channel],-,14,14,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,Yes,No,N
127
+ High-Impact Media,Mural 360 View Targeted to [Insert Channel],,No,,Mural 360 View Targeted to [Insert Channel],-,15,15,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,,,
128
+ High-Impact Media,Mural Carousel Targeted to [Insert Channel],,No,,Mural Carousel Targeted to [Insert Channel],-,14,14,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,,,
129
+ High-Impact Media,Mural Cart Connect Targeted to [Insert Channel],,No,,Mural Cart Connect Targeted to [Insert Channel],-,16,16,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,,,
130
+ High-Impact Media,Mural Cart View Targeted to [Insert Channel],,No,,Mural Cart View Targeted to [Insert Channel],-,16,16,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,,,
131
+ High-Impact Media,Mural Content Collection Targeted to [Insert Channel],,No,,Mural Content Collection Targeted to [Insert Channel],-,15,15,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,,,
132
+ High-Impact Media,Mural Countdown Targeted to [Insert Channel],,No,,Mural Countdown Targeted to [Insert Channel],-,14,14,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,,,
133
+ High-Impact Media,Mural Creator Endorsement Targeted to [Insert Channel],,No,,Mural Creator Endorsement Targeted to [Insert Channel],-,18,18,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,Non-cancellable,x,x,x,,,
134
+ High-Impact Media,Mural Creator Product Curation Targeted to [Insert Channel],,No,,Mural Creator Product Curation Targeted to [Insert Channel],-,18,18,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,Non-cancellable,x,x,x,,,
135
+ High-Impact Media,Mural Digital Circular Targeted to [Insert Channel],,No,,Mural Digital Circular Targeted to [Insert Channel],-,15,15,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,,,
136
+ High-Impact Media,Mural Dynamic Creative Targeted to [Insert Channel],,No,,Mural Dynamic Creative Targeted to [Insert Channel],-,14,14,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,,,
137
+ High-Impact Media,Mural Glider Targeted to [Insert Channel],,No,,Mural Glider Targeted to [Insert Channel],-,15,15,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,,,
138
+ High-Impact Media,Mural Hotspot Targeted to [Insert Channel],,No,,Mural Hotspot Targeted to [Insert Channel],-,15,15,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,,,
139
+ High-Impact Media,Mural Interactive Shopper Targeted to [Insert Channel],,No,,Mural Interactive Shopper Targeted to [Insert Channel],-,15,15,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,,,
140
+ High-Impact Media,Mural Quiz Targeted to [Insert Channel],,No,,Mural Quiz Targeted to [Insert Channel],-,16,16,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,,,
141
+ High-Impact Media,Mural Retail Connect Targeted to [Insert Channel],,No,,Mural Retail Connect Targeted to [Insert Channel],-,14,14,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,,,
142
+ High-Impact Media,Mural Scratch Off/Wipe Away Targeted to [Insert Channel],,No,,Mural Scratch Off/Wipe Away Targeted to [Insert Channel],-,15,15,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,,,
143
+ High-Impact Media,Mural Scroll Reactive Animation Targeted to [Insert Channel],,No,,Mural Scroll Reactive Animation Targeted to [Insert Channel],-,14,14,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,,,
144
+ High-Impact Media,Mural Shoppable Video Targeted to [Insert Channel],,No,,Mural Shoppable Video Targeted to [Insert Channel],-,16,16,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,,,
145
+ High-Impact Media,Mural Video Targeted to [Insert Channel],,No,,Mural Video Targeted to [Insert Channel],-,16,16,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,,,
146
+ High-Impact Media,Mural Visualizer Targeted to [Insert Channel],,No,,Mural Visualizer Targeted to [Insert Channel],-,15,15,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,,,
147
+ High-Impact Media,Backdrop Targeted to [Insert Channel],,No,,Backdrop Targeted to [Insert Channel],-,12,12,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,,,x,Yes,No,N
148
+ High-Impact Media,Backdrop Dynamic Creative Targeted to [Insert Channel],,No,,Backdrop Dynamic Creative Targeted to [Insert Channel],-,12,12,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,,,x,,,
149
+ High-Impact Media,Backdrop Retail Connect Targeted to [Insert Channel],,No,,Backdrop Retail Connect Targeted to [Insert Channel],-,12,12,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,,,x,,,
150
+ High-Impact Media,Beyond Banner Targeted to [Insert Channel],,No,,Beyond Banner Targeted to [Insert Channel],8,11,11,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,Yes,Yes,N
151
+ High-Impact Media,Beyond Banner Carousel Targeted to [Insert Channel],,No,,Beyond Banner Carousel Targeted to [Insert Channel],8,11,11,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,,,
152
+ High-Impact Media,Beyond Banner Cart Connect Targeted to [Insert Channel],,No,,Beyond Banner Cart Connect Targeted to [Insert Channel],-,13,13,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,,,
153
+ High-Impact Media,Beyond Banner Countdown Targeted to [Insert Channel],,No,,Beyond Banner Countdown Targeted to [Insert Channel],8,11,11,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,,,
154
+ High-Impact Media,Beyond Banner Digital Circular Targeted to [Insert Channel],,No,,Beyond Banner Digital Circular Targeted to [Insert Channel],-,12,12,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,,,
155
+ High-Impact Media,Beyond Banner Dynamic Creative Targeted to [Insert Channel],,No,,Beyond Banner Dynamic Creative Targeted to [Insert Channel],8,11,11,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,,,
156
+ High-Impact Media,Beyond Banner Expandable Targeted to [Insert Channel],,No,,Beyond Banner Expandable Targeted to [Insert Channel],-,13,13,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,,,
157
+ High-Impact Media,Beyond Banner Expandable Content Collection Targeted to [Insert Channel],,No,,Beyond Banner Expandable Content Collection Targeted to [Insert Channel],-,13,13,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,,,
158
+ High-Impact Media,Beyond Banner Expandable Hotspot Targeted to [Insert Channel],,No,,Beyond Banner Expandable Hotspot Targeted to [Insert Channel],-,13,13,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,,,
159
+ High-Impact Media,Beyond Banner Expandable Interactive Shopper Targeted to [Insert Channel],,No,,Beyond Banner Expandable Interactive Shopper Targeted to [Insert Channel],-,13,13,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,,,
160
+ High-Impact Media,Beyond Banner Expandable Video Targeted to [Insert Channel],,No,,Beyond Banner Expandable Video Targeted to [Insert Channel],-,13,13,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,,,
161
+ High-Impact Media,Beyond Banner Expandable Visualizer Targeted to [Insert Channel],,No,,Beyond Banner Expandable Visualizer Targeted to [Insert Channel],-,13,13,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,,,
162
+ High-Impact Media,Beyond Banner Retail Connect Targeted to [Insert Channel],,No,,Beyond Banner Retail Connect Targeted to [Insert Channel],-,11,11,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,,,
163
+ High-Impact Media,Social Display Ad Targeted to [Insert Channel],,No,,Social Display Ad Targeted to [Insert Channel],-,10,10,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,Yes,No,N
164
+ High-Impact Media,Sticker Targeted to [Insert Channel],,No,,Sticker Targeted to [Insert Channel],-,12,12,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,Yes,No,N
165
+ High-Impact Media,Sticker Dynamic Creative Targeted to [Insert Channel],,No,,Sticker Dynamic Creative Targeted to [Insert Channel],-,12,12,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,,,
166
+ High-Impact Media,Sticker Expandable Targeted to [Insert Channel],,No,,Sticker Expandable Targeted to [Insert Channel],-,14,14,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,,,
167
+ High-Impact Media,Sticker Expandable Content Collection Targeted to [Insert Channel],,No,,Sticker Expandable Content Collection Targeted to [Insert Channel],-,14,14,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,,,
168
+ High-Impact Media,Sticker Expandable Hotspot Targeted to [Insert Channel],,No,,Sticker Expandable Hotspot Targeted to [Insert Channel],-,14,14,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,,,
169
+ High-Impact Media,Sticker Expandable Interactive Shopper Targeted to [Insert Channel],,No,,Sticker Expandable Interactive Shopper Targeted to [Insert Channel],-,14,14,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,,,
170
+ High-Impact Media,Sticker Expandable Video Targeted to [Insert Channel],,No,,Sticker Expandable Video Targeted to [Insert Channel],-,14,14,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,,,
171
+ High-Impact Media,Sticker Expandable Visualizer Targeted to [Insert Channel],,No,,Sticker Expandable Visualizer Targeted to [Insert Channel],-,14,14,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,,,
172
+ High-Impact Media,Sticker Mini Expandable Carousel Targeted to [Insert Channel],,No,,Sticker Mini Expandable Carousel Targeted to [Insert Channel],-,13,13,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,,,
173
+ High-Impact Media,Sticker Retail Connect Targeted to [Insert Channel],,No,,Sticker Retail Connect Targeted to [Insert Channel],-,12,12,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,,,
174
+ High-Impact Media,Lens Free Fall Game Targeted to [Insert Channel],,No,,Lens Free Fall Game Targeted to [Insert Channel],-,10,10,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,,,x,Yes,No,N
175
+ High-Impact Media,Lens Story Spotlight Targeted to [Insert Channel],,No,,Lens Story Spotlight Targeted to [Insert Channel],-,9,9,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,,,x,,,
176
+ High-Impact Media,Lens Swipe Navigator Targeted to [Insert Channel],,No,,Lens Swipe Navigator Targeted to [Insert Channel],8,9,9,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,,,x,,,
177
+ High-Impact Media,Tower Banner Targeted to [Insert Channel],,No,,Tower Banner Targeted to [Insert Channel],9,12,12,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,,,x,Yes,Yes,N
178
+ High-Impact Media,Tower Banner 360 View Targeted to [Insert Channel],,No,,Tower Banner 360 View Targeted to [Insert Channel],10,13,13,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,,,x,,,
179
+ High-Impact Media,Tower Banner Carousel Targeted to [Insert Channel],,No,,Tower Banner Carousel Targeted to [Insert Channel],9,12,12,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,,,x,,,
180
+ High-Impact Media,Tower Banner Countdown Targeted to [Insert Channel],,No,,Tower Banner Countdown Targeted to [Insert Channel],9,12,12,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,,,x,,,
181
+ High-Impact Media,Tower Banner Digital Circular Targeted to [Insert Channel],,No,,Tower Banner Digital Circular Targeted to [Insert Channel],10,13,13,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,,,x,,,
182
+ High-Impact Media,Tower Banner Dynamic Creative Targeted to [Insert Channel],,No,,Tower Banner Dynamic Creative Targeted to [Insert Channel],9,12,12,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,,,x,,,
183
+ High-Impact Media,Tower Banner Glider Targeted to [Insert Channel],,No,,Tower Banner Glider Targeted to [Insert Channel],10,13,13,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,,,x,,,
184
+ High-Impact Media,Tower Banner Hotspot Targeted to [Insert Channel],,No,,Tower Banner Hotspot Targeted to [Insert Channel],10,13,13,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,,,x,,,
185
+ High-Impact Media,Tower Banner Interactive Shopper Targeted to [Insert Channel],,No,,Tower Banner Interactive Shopper Targeted to [Insert Channel],10,13,13,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,,,x,,,
186
+ High-Impact Media,Tower Banner Retail Connect Targeted to [Insert Channel],,No,,Tower Banner Retail Connect Targeted to [Insert Channel],9,12,12,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,,,x,,,
187
+ High-Impact Media,Tower Banner Scratch Off/Wipe Away Targeted to [Insert Channel],,No,,Tower Banner Scratch Off/Wipe Away Targeted to [Insert Channel],10,13,13,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,,,x,,,
188
+ High-Impact Media,Tower Banner Visualizer Targeted to [Insert Channel],,No,,Tower Banner Visualizer Targeted to [Insert Channel],10,13,13,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,,,x,,,
189
+ High-Impact Media,Video Ribbon Targeted to [Insert Channel],,No,,Video Ribbon Targeted to [Insert Channel],11,13,13,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,,,x,Yes,Yes,N
190
+ High-Impact Media,Skyline Targeted to [Insert Channel],,No,,Skyline Targeted to [Insert Channel],-,-,17,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,,,No,No,N
191
+ High-Impact Media,Skyline 360 View Targeted to [Insert Channel],,No,,Skyline 360 View Targeted to [Insert Channel],-,-,18,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,,,,,
192
+ High-Impact Media,Skyline Carousel Targeted to [Insert Channel],,No,,Skyline Carousel Targeted to [Insert Channel],-,-,17,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,,,,,
193
+ High-Impact Media,Skyline Countdown Targeted to [Insert Channel],,No,,Skyline Countdown Targeted to [Insert Channel],-,-,17,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,,,,,
194
+ High-Impact Media,Skyline Dynamic Creative Targeted to [Insert Channel],,No,,Skyline Dynamic Creative Targeted to [Insert Channel],-,-,17,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,,,,,
195
+ High-Impact Media,Skyline Glider Targeted to [Insert Channel],,No,,Skyline Glider Targeted to [Insert Channel],-,-,18,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,,,,,
196
+ High-Impact Media,Skyline Interactive Shopper Targeted to [Insert Channel],,No,,Skyline Interactive Shopper Targeted to [Insert Channel],-,-,18,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,,,,,
197
+ High-Impact Media,Skyline Retail Connect Targeted to [Insert Channel],,No,,Skyline Retail Connect Targeted to [Insert Channel],-,-,17,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,,,,,
198
+ High-Impact Media,Skyline Shoppable Video Targeted to [Insert Channel],,No,,Skyline Shoppable Video Targeted to [Insert Channel],-,-,19,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,,,,,
199
+ High-Impact Media,Skyline Video Targeted to [Insert Channel],,No,,Skyline Video Targeted to [Insert Channel],-,-,19,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,,,,,
200
+ High-Impact Media,Skyline Visualizer Targeted to [Insert Channel],,No,,Skyline Visualizer Targeted to [Insert Channel],-,-,18,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,,,,,
201
+ High-Impact Media Packages,High-Impact Package Targeted to [Insert Channel]_Package,,Yes,,,-,-,12.5,CPM,Raptive,PKG,Insert,Insert,See rate card,,,< 10%,,PKG,PKG,PKG,No,No,PKG
202
+ High-Impact Media Packages,Mural Targeted to [Insert Channel],,Yes,,,-,-,-,CPM,Raptive,1x1,-,-,-,-,-,-,,x,x,x,-,-,N
203
+ High-Impact Media Packages,Beyond Banner Targeted to [Insert Channel],,Yes,,,-,-,-,CPM,Raptive,1x1,-,-,-,-,-,-,,x,x,x,-,-,N
204
+ Native Units,Recipe Recommendation Ad Targeted to [Insert Channel],,No,,Recipe Recommendation Ad Targeted to [Insert Channel],6,9,9,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,Yes,Yes,N
205
+ Native Units,Content Recommendation Ad Targeted to [Insert Channel],,No,,Content Recommendation Ad Targeted to [Insert Channel],6,9,9,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,Yes,Yes,N
206
+ Video,Pre-Roll Video (²15 Sec) Targeted to [Insert Targeting Type],,No,,Pre-Roll Video (²15 Sec) Targeted to [Insert Targeting Type],12,14,14,CPM,Raptive,"16:9 Aspect Ratio, :06s, :15s",Insert,Insert,See rate card,,,< 10%,,x,x,x,Yes,Yes,N
207
+ Video,Pre-Roll Video (30 Sec) Targeted to [Insert Targeting Type],,No,,Pre-Roll Video (30 Sec) Targeted to [Insert Targeting Type],12,22,22,CPM,Raptive,"16:9 Aspect Ratio, :30s",Insert,Insert,See rate card,,,< 10%,,x,x,x,Yes,Yes,N
208
+ Video,Pre-Roll Video (Click to Play) Targeted to [Insert Targeting Type],,No,,Pre-Roll Video (Click to Play) Targeted to [Insert Targeting Type],20,22.5,22.5,CPM,Raptive,"16:9 Aspect Ratio, :06s, :15s, :30s",Insert,Insert,See rate card,,,< 10%,,x,x,x,,,
209
+ Video,Outstream Video Targeted to [Insert Targeting Type],,No,,Outstream Video Targeted to [Insert Targeting Type],5,9.5,-,CPM,Raptive,640x480 or 640x360 or 300x250,Insert,Insert,See rate card,,,< 10%,"Ads must be at least 256px in both width and height with the exception of 300x250, 16:9 or 4:3 aspect ratios",x,x,x,No,Yes,N
210
+ Video,YouTube Network Bumper Ad (06 Sec) Targeted to [Insert Targeting Type],,No,,YouTube Network Bumper Ad (06 Sec) Targeted to [Insert Targeting Type],-,12,12,CPM,Raptive,"16:9,9:16, 1:1, 4:3 or 2:3 Aspect Ratio, :06s",Insert,Insert,See rate card,,,< 10%,"Must be purchased with Raptive on-site pre-roll
211
+
212
+ Impressions are estimated. If actual impressions exceed or fall short of the goal, budget will be shifted between this line and the Raptive on-site preroll line.
213
+
214
+ YouTube does not accept VPAID tags. We can accept site-served video and a 1x1 OR 3P VAST tags.
215
+
216
+ Raptive cannot accept 3P tracking tags. Please provide the following IDs for Raptive to implement in order to enable tracking.
217
+
218
+ DoubleVerify:
219
+ +Viewability Client ID
220
+ +Viewability Reporting ID
221
+
222
+ Dynata, Intage (JP only), or Kantar:
223
+ +Publisher Viewability Client ID
224
+ +Publisher Viewability Reporting ID
225
+
226
+ Raptive can also accept the ID's from the following partners:
227
+
228
+ +Viewability: MOAT, IAS
229
+ +Reach: comScore, Nielsen, VideoAmp (US only), iSpotTV (US only), Video Research (JP only), Gemius SA (DE only), AudienceProject (UK)
230
+ +Other vendors integrated with Ads Data Hub: Adform, AdRiver, AudienceProject, C3 Metrics, Extreme Reach, Exactag, Flashtalking, Innovid, Weborama
231
+
232
+ Pixels allowed:
233
+ +Google (including Campaign Manager 360)",x,x,x,"Yes
234
+
235
+ DV360 only",No,N
236
+ Video,YouTube Network Non-Skippable Video (15 Sec) Targeted to [Insert Targeting Type],,No,,YouTube Network Non-Skippable Video (15 Sec) Targeted to [Insert Targeting Type],-,23,23,CPM,Raptive,"16:9,9:16, 1:1, 4:3 or 2:3 Aspect Ratio, :15s",Insert,Insert,See rate card,,,< 10%,"Must be purchased with Raptive on-site pre-roll
237
+
238
+ Impressions are estimated. If actual impressions exceed or fall short of the goal, budget will be shifted between this line and the Raptive on-site preroll line.
239
+
240
+ YouTube does not accept VPAID tags. We can accept site-served video and a 1x1 OR 3P VAST tags.
241
+
242
+ Raptive cannot accept 3P tracking tags. Please provide the following IDs for Raptive to implement in order to enable tracking.
243
+
244
+ DoubleVerify:
245
+ +Viewability Client ID
246
+ +Viewability Reporting ID
247
+
248
+ Dynata, Intage (JP only), or Kantar:
249
+ +Publisher Viewability Client ID
250
+ +Publisher Viewability Reporting ID
251
+
252
+ Raptive can also accept the ID's from the following partners:
253
+
254
+ +Viewability: MOAT, IAS
255
+ +Reach: comScore, Nielsen, VideoAmp (US only), iSpotTV (US only), Video Research (JP only), Gemius SA (DE only), AudienceProject (UK)
256
+ +Other vendors integrated with Ads Data Hub: Adform, AdRiver, AudienceProject, C3 Metrics, Extreme Reach, Exactag, Flashtalking, Innovid, Weborama
257
+
258
+ Pixels allowed:
259
+ +Google (including Campaign Manager 360)",x,x,x,"Yes
260
+
261
+ DV360 only",No,N
262
+ Video,"YouTube Network Skippable Video (30 Sec+, Skippable at 05 Sec) Targeted to [Insert Targeting Type]",,No,,"YouTube Network Skippable Video (30 Sec+, Skippable at 05 Sec) Targeted to [Insert Targeting Type]",-,18,18,CPM,Raptive,"16:9,9:16, 1:1, 4:3 or 2:3 Aspect Ratio, :30s+",Insert,Insert,See rate card,,,< 10%,"Must be purchased with Raptive on-site pre-roll
263
+
264
+ Impressions are estimated. If actual impressions exceed or fall short of the goal, budget will be shifted between this line and the Raptive on-site preroll line.
265
+
266
+ YouTube does not accept VPAID tags. We can accept site-served video and a 1x1 OR 3P VAST tags.
267
+
268
+ Raptive cannot accept 3P tracking tags. Please provide the following IDs for Raptive to implement in order to enable tracking.
269
+
270
+ DoubleVerify:
271
+ +Viewability Client ID
272
+ +Viewability Reporting ID
273
+
274
+ Dynata, Intage (JP only), or Kantar:
275
+ +Publisher Viewability Client ID
276
+ +Publisher Viewability Reporting ID
277
+
278
+ Raptive can also accept the ID's from the following partners:
279
+
280
+ +Viewability: MOAT, IAS
281
+ +Reach: comScore, Nielsen, VideoAmp (US only), iSpotTV (US only), Video Research (JP only), Gemius SA (DE only), AudienceProject (UK)
282
+ +Other vendors integrated with Ads Data Hub: Adform, AdRiver, AudienceProject, C3 Metrics, Extreme Reach, Exactag, Flashtalking, Innovid, Weborama
283
+
284
+ Pixels allowed:
285
+ +Google (including Campaign Manager 360)",x,x,x,"Yes
286
+
287
+ DV360 only",No,N
288
+ Targeted Media,[Insert Targeting Type] Media_Package,,Yes,,,4,7,7,CPM,Raptive,PKG,Insert,Insert,See rate card,,,< 10%,,PKG,PKG,PKG,Yes,Yes,PKG
289
+ Targeted Media,[Insert Targeting Type] Media_Leaderboard,,Yes,Leaderboard,,-,-,-,-,Raptive,728x90,-,-,-,-,-,-,,x,,x,-,-,Y
290
+ Targeted Media,[Insert Targeting Type] Media_Medium Rectangle,,Yes,Medium Rectangle,,-,-,-,-,Raptive,300x250,-,-,-,-,-,-,,x,x,x,-,-,Y
291
+ Targeted Media,[Insert Targeting Type] Media_Mobile Banner,,Yes,Mobile Banner,,-,-,-,-,Raptive,320x50,-,-,-,-,-,-,,,,x,-,-,Y
292
+ Added Value,AV Media [Insert Targeting Type] _Package,,Yes,,,0,0,0,Added Value,Raptive,PKG,Insert,Insert,Added Value,,$0,< 10%,"Value = $x,xxx",PKG,PKG,PKG,No,No,PKG
293
+ Added Value,AV Media [Insert Targeting Type] _Leaderboard,,Yes,Leaderboard,,-,-,-,-,Raptive,728x90,-,-,-,-,-,-,,x,,x,-,-,Y
294
+ Added Value,AV Media [Insert Targeting Type] _Medium Rectangle,,Yes,Medium Rectangle,,-,-,-,-,Raptive,300x250,-,-,-,-,-,-,,x,x,x,-,-,Y
295
+ Added Value,AV Media [Insert Targeting Type] _Mobile Banner,,Yes,Mobile Banner,,-,-,-,-,Raptive,320x50,-,-,-,-,-,-,,,,x,-,-,Y
296
+ Added Value,AV [Insert Name] Study,,No,,AV [Insert Name] Study,0,0,0,Added Value,Raptive,n/a,Insert,Insert,Added Value,n/a,$0,n/a,"Value = $x,xxx
297
+
298
+ Once study starts, entire program becomes non-cancellable",n/a,n/a,n/a,TBD,TBD,n/a
app.py ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import json
3
+ import pandas as pd
4
+ import streamlit as st
5
+ import snowflake.connector
6
+ from cryptography.hazmat.primitives import serialization
7
+
8
+ from comscore_site_list import render as render_comscore_site_list
9
+ from media_plan_templater import render as render_media_plan_templater
10
+
11
+
12
+ st.set_page_config(layout="wide", page_title="Sales Toolkit", page_icon="📊")
13
+
14
+
15
+ # ─── Snowflake Helpers ───────────────────────────────────────
16
+ def get_conn():
17
+ raw_key = os.getenv("snowflake_private_key")
18
+ priv_key = serialization.load_pem_private_key(raw_key.encode(), password=None)
19
+ return snowflake.connector.connect(
20
+ user=os.getenv("snowflake_user"),
21
+ account=os.getenv("snowflake_account_identifier"),
22
+ private_key=priv_key,
23
+ role=os.getenv("snowflake_role"),
24
+ warehouse=os.getenv("snowflake_warehouse"),
25
+ database=os.getenv("snowflake_database"),
26
+ )
27
+
28
+
29
+ @st.cache_data
30
+ def get_lookup_table():
31
+ conn = get_conn()
32
+ cs = conn.cursor()
33
+ database = os.getenv("snowflake_database") # e.g. ANALYTICS
34
+ schema = "SIGMA_SCRATCH"
35
+ view = "VIEW_NEW_DATASET_FROM_SQL_3B3605DCE9F84DBC8F8508F33C4364FF"
36
+
37
+ cs.execute(
38
+ f"""
39
+ SELECT
40
+ "ENTITY" AS entity,
41
+ "SITE ID" AS site_id,
42
+ "SITE NAME" AS site_name,
43
+ "STATUS" AS status,
44
+ "AD OPTIONS" AS ad_options
45
+ FROM {database}.{schema}.{view}
46
+ """
47
+ )
48
+ cols = [d[0].lower() for d in cs.description]
49
+ rows = cs.fetchall()
50
+ cs.close()
51
+ conn.close()
52
+ return pd.DataFrame(rows, columns=cols)
53
+
54
+
55
+ def extract_pmp(ad_opts):
56
+ """Parse the JSON in `ad_options` and return the top-level 'pmp' boolean."""
57
+ try:
58
+ obj = json.loads(ad_opts)
59
+ return obj.get("pmp")
60
+ except Exception:
61
+ return None
62
+
63
+
64
+ # ─── App Layout ───────────────────────────────────────────────
65
+ st.title("Sales Toolkit")
66
+ tab1, tab2 = st.tabs(["📊 Comscore Site List", "Media Plan Templater"])
67
+
68
+ with tab1:
69
+ render_comscore_site_list(get_conn, get_lookup_table, extract_pmp)
70
+
71
+ with tab2:
72
+ render_media_plan_templater(get_conn)
changelog.md ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ # Changelog
2
+
3
+ - 2025-08-07 14:28 UTC: Initialized changelog to track project updates.
4
+ - 2025-08-09 00:00 UTC: Explode bulleted Details in Media Plan Templater for package Products.
5
+ - 2025-08-07 18:44 UTC: Display full placement names in Details column of Media Plan Templater.
6
+ - 2025-08-08 13:31 UTC: Deduplicate package rows in Media Plan Templater.
7
+ - 2025-08-11 17:27 UTC: Split tabs into separate modules for easier maintenance.
8
+ - 2025-08-13 20:35 UTC: Reload Excel data when switching sheets in Media Plan Templater.
9
+ - 2025-08-13 21:10 UTC: Unmerge target columns to prevent 'MergedCell' Excel download errors.
comscore_site_list.py ADDED
@@ -0,0 +1,199 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import csv
2
+ import pandas as pd
3
+ import streamlit as st
4
+
5
+
6
+ def render(get_conn, get_lookup_table, extract_pmp):
7
+ st.header("📊 Comscore Site List")
8
+
9
+ # 1) Composition Index Threshold input
10
+ threshold = st.number_input(
11
+ "Composition Index Threshold",
12
+ min_value=0.0,
13
+ step=1.0,
14
+ value=150.0,
15
+ help="Show only rows where Composition Index UV ≥ this value",
16
+ )
17
+
18
+ # 2) Exclude Codes selector
19
+ # ── Fetch all distinct Codes for the dropdown
20
+ conn_codes = get_conn()
21
+ cs_codes = conn_codes.cursor()
22
+ cs_codes.execute(
23
+ """
24
+ SELECT DISTINCT Codes
25
+ FROM ANALYTICS.SIGMA_SCRATCH.VIEW_GAMLOG_AGG_SITE_CODE_VOLUME_188E5F89A6FD42C7994D0F012A2EECD5
26
+ ORDER BY Codes
27
+ """
28
+ )
29
+ all_codes = [row[0] for row in cs_codes.fetchall()]
30
+ cs_codes.close()
31
+ conn_codes.close()
32
+
33
+ exclude_codes = st.multiselect(
34
+ "Exclude Codes",
35
+ options=all_codes,
36
+ default=["bsopt_23"] if "bsopt_23" in all_codes else [],
37
+ help="Select Codes whose SITEIDs you want to filter out of the main table",
38
+ )
39
+
40
+ # 3) File uploader
41
+ upload = st.file_uploader(
42
+ "Upload your Comscore CSV", type="csv", key="comscore_uploader"
43
+ )
44
+ if not upload:
45
+ st.info("Please upload your file from Comscore.")
46
+ else:
47
+ try:
48
+ # ─── Parse the raw into rows ───────────────────────────────────────
49
+ raw_text = upload.read().decode("latin-1")
50
+ lines = raw_text.splitlines()
51
+ reader = csv.reader(lines, skipinitialspace=True)
52
+ rows = list(reader)
53
+
54
+ # ─── Locate your three headers anywhere in the first N lines ───────
55
+ required = ["Media", "Target Audience (000)", "Composition Index UV"]
56
+ header_pos = {}
57
+ for i, row in enumerate(rows[:30]):
58
+ cleaned = [cell.strip() for cell in row]
59
+ for col in required:
60
+ if col not in header_pos and col in cleaned:
61
+ header_pos[col] = i
62
+ if len(header_pos) == len(required):
63
+ break
64
+
65
+ missing = [c for c in required if c not in header_pos]
66
+ if missing:
67
+ st.error(f"Missing required column(s): {', '.join(missing)}")
68
+ st.stop()
69
+
70
+ # map header names → column indices
71
+ idx_map = {}
72
+ for col, prow in header_pos.items():
73
+ cleaned = [cell.strip() for cell in rows[prow]]
74
+ idx_map[col] = cleaned.index(col)
75
+
76
+ # data starts right after the bottom header row
77
+ data_start = max(header_pos.values()) + 1
78
+ data_rows = rows[data_start:]
79
+
80
+ # pull out exactly those three columns
81
+ data = []
82
+ for row in data_rows:
83
+ if not row:
84
+ continue
85
+ vals = []
86
+ for col in required:
87
+ idx = idx_map[col]
88
+ vals.append(row[idx].strip() if idx < len(row) else None)
89
+ data.append(vals)
90
+
91
+ # build dataframe & coerce types
92
+ df_clean = pd.DataFrame(data, columns=required)
93
+ df_clean["Target Audience (000)"] = pd.to_numeric(
94
+ df_clean["Target Audience (000)"], errors="coerce"
95
+ )
96
+ df_clean["Composition Index UV"] = pd.to_numeric(
97
+ df_clean["Composition Index UV"], errors="coerce"
98
+ )
99
+
100
+ # ─── ORIGINAL Snowflake lookup & merge ───────────────────────────
101
+ df_lookup = (
102
+ get_lookup_table()
103
+ ) # DISCUSSION: this still uses your SIGMA_SCRATCH view
104
+ lookup_small = df_lookup[
105
+ ["entity", "site_id", "site_name", "status", "ad_options"]
106
+ ]
107
+ df_merged = pd.merge(
108
+ df_clean,
109
+ lookup_small,
110
+ how="left",
111
+ left_on="Media",
112
+ right_on="entity",
113
+ ).drop(columns=["entity"])
114
+ df_merged["pmp"] = (
115
+ df_merged["ad_options"]
116
+ .astype(str)
117
+ .apply(extract_pmp)
118
+ .map({True: "Yes", False: "No"})
119
+ .fillna("No")
120
+ )
121
+
122
+ # ─── NEW: fetch SITEIDs to exclude based on Codes ────────────────
123
+ if exclude_codes:
124
+ conn_ex = get_conn()
125
+ cs_ex = conn_ex.cursor()
126
+ quoted = ", ".join(f"'{c}'" for c in exclude_codes)
127
+ cs_ex.execute(
128
+ f"""
129
+ SELECT DISTINCT SITEID
130
+ FROM ANALYTICS.SIGMA_SCRATCH.VIEW_GAMLOG_AGG_SITE_CODE_VOLUME_188E5F89A6FD42C7994D0F012A2EECD5
131
+ WHERE Codes IN ({quoted})
132
+ """
133
+ )
134
+ excluded_site_ids = {row[0] for row in cs_ex.fetchall()}
135
+ cs_ex.close()
136
+ conn_ex.close()
137
+ else:
138
+ excluded_site_ids = set()
139
+
140
+ # ─── Combined filter ─────────────────────────────────────────────
141
+ df_filtered = df_merged[
142
+ (df_merged["Composition Index UV"] >= threshold)
143
+ & (df_merged["site_name"].notna())
144
+ & (df_merged["status"] != "Dropped")
145
+ & (df_merged["pmp"] == "Yes")
146
+ & (~df_merged["site_id"].isin(excluded_site_ids))
147
+ ]
148
+
149
+ # ─── drop extras, sort, display & export ──────────────────────────
150
+ df_display = (
151
+ df_filtered.drop(columns=["ad_options"])
152
+ .sort_values("Target Audience (000)", ascending=False)
153
+ .reset_index(drop=True)
154
+ )
155
+
156
+ if df_display.empty:
157
+ st.warning("No rows meet the criteria.")
158
+ else:
159
+ st.success(f"Showing {len(df_display)} rows after filtering")
160
+ st.dataframe(df_display)
161
+
162
+ # Export – GAM
163
+ site_ids = df_display["site_id"].dropna().astype(str)
164
+ gam_str = ",".join(site_ids)
165
+ gam_df = pd.DataFrame({"site_ids": [gam_str]})
166
+ gam_csv = gam_df.to_csv(index=False, header=False)
167
+ st.download_button(
168
+ label="Export - GAM",
169
+ data=gam_csv,
170
+ file_name="gam_export.csv",
171
+ mime="text/csv",
172
+ )
173
+
174
+ # Export – Client Facing
175
+ client_df = df_display[["site_name", "Media"]].rename(
176
+ columns={"Media": "Domain"}
177
+ )
178
+ client_csv = client_df.to_csv(index=False)
179
+ st.download_button(
180
+ label="Export - Client Facing",
181
+ data=client_csv,
182
+ file_name="client_facing_export.csv",
183
+ mime="text/csv",
184
+ )
185
+
186
+ # Export – Site Names
187
+ site_names = df_display["site_name"].dropna().astype(str)
188
+ names_str = ",".join(site_names)
189
+ names_df = pd.DataFrame({"site_names": [names_str]})
190
+ names_csv = names_df.to_csv(index=False, header=False)
191
+ st.download_button(
192
+ label="Export - Site Names",
193
+ data=names_csv,
194
+ file_name="site_names_export.csv",
195
+ mime="text/csv",
196
+ )
197
+
198
+ except Exception as e:
199
+ st.error(f"Could not parse, merge, or export file: {e}")
.gitattributes → gitattributes RENAMED
File without changes
index.html DELETED
@@ -1,19 +0,0 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
19
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
media_plan_templater.py ADDED
@@ -0,0 +1,633 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import io
2
+ import os
3
+ import re
4
+ import json
5
+ import hashlib
6
+ import numpy as np
7
+ import pandas as pd
8
+ import streamlit as st
9
+ from docx import Document
10
+ from openpyxl import Workbook, load_workbook
11
+ from openpyxl.styles import numbers
12
+ from datetime import date, timedelta
13
+
14
+
15
+ def render(get_conn):
16
+ st.header("📊 Media Plan Templater")
17
+
18
+ # ─────────────────────────────────────────────────────────────────────────────
19
+ # 1) Upload and process Word document
20
+ # ─────────────────────────────────────────────────────────────────────────────
21
+ upload_doc = st.file_uploader(
22
+ "1. Upload Word document (.docx) with media placements table",
23
+ type=["docx"],
24
+ key="media_plan_docx_uploader",
25
+ )
26
+
27
+ if "df_word" not in st.session_state:
28
+ if not upload_doc:
29
+ st.info(
30
+ "Please upload a Word (.docx) file to extract the media placements table."
31
+ )
32
+ st.stop()
33
+
34
+ # Parse Word tables → DataFrame
35
+ doc = Document(io.BytesIO(upload_doc.getvalue()))
36
+ dfs = []
37
+ for table in doc.tables:
38
+ rows = [[cell.text.strip() for cell in row.cells] for row in table.rows]
39
+ if len(rows) < 2:
40
+ continue
41
+ header, *data = rows
42
+ dfs.append(pd.DataFrame(data, columns=header))
43
+
44
+ if not dfs:
45
+ st.error("No valid tables found in the Word document.")
46
+ st.stop()
47
+
48
+ df_word = pd.concat(dfs, ignore_index=True)
49
+ df_word.columns = df_word.columns.str.strip()
50
+
51
+ # Normalize Details: remove newlines → split bullets → explode
52
+ if "Details" in df_word.columns:
53
+ df_word["Details"] = (
54
+ df_word["Details"].astype(str).str.replace("\n", " ").str.strip()
55
+ )
56
+ df_word["Details"] = (
57
+ df_word["Details"]
58
+ .fillna("")
59
+ .str.split("•")
60
+ .apply(lambda parts: [p.strip() for p in parts if str(p).strip()])
61
+ )
62
+ df_word = df_word.explode("Details")
63
+ else:
64
+ df_word["Details"] = ""
65
+
66
+ # Ensure required columns exist
67
+ for col in [
68
+ "Media Plan",
69
+ "Budget",
70
+ "Product",
71
+ "Targeting",
72
+ "Notes",
73
+ "Rate",
74
+ "Rate_Type",
75
+ "Ad Size",
76
+ ]:
77
+ if col not in df_word.columns:
78
+ df_word[col] = ""
79
+
80
+ # ── Pull rate card from Snowflake
81
+ conn = get_conn()
82
+ cs = conn.cursor()
83
+ cs.execute(
84
+ """
85
+ SELECT
86
+ Package AS Product,
87
+ Placement,
88
+ PLACEMENT_EXTRACTION,
89
+ RATE_CARD_LOOKUP_Concat,
90
+ Rate_Type,
91
+ Ad_Size,
92
+ PMP_Rate,
93
+ PG_Rate,
94
+ Direct_IO_Rate
95
+ FROM ANALYTICS.SIGMA_SCRATCH.UNIVERSAL_MEDIA_PLAN_WITH_RATE_CARD_VALUES
96
+ """
97
+ )
98
+ rows, cols = cs.fetchall(), [d[0] for d in cs.description]
99
+ cs.close()
100
+ conn.close()
101
+
102
+ df_rate = pd.DataFrame(rows, columns=cols)
103
+ df_rate.columns = [c.strip().upper() for c in df_rate.columns]
104
+ df_rate = df_rate.rename(
105
+ columns={
106
+ "PRODUCT": "Product",
107
+ "PLACEMENT": "Placement",
108
+ "PLACEMENT_EXTRACTION": "Details_key",
109
+ "RATE_CARD_LOOKUP_CONCAT": "LookupKey_sql",
110
+ "RATE_TYPE": "Rate_Type",
111
+ "AD_SIZE": "Ad Size",
112
+ "PMP_RATE": "PMP_Rate",
113
+ "PG_RATE": "PG_Rate",
114
+ "DIRECT_IO_RATE": "Direct_IO_Rate",
115
+ }
116
+ )
117
+
118
+ # Build exact concat from Word doc
119
+ df_word["Details_fixed"] = df_word["Details"].astype(str)
120
+ mask_pkg = df_word["Details_fixed"].str.strip().str.lower().eq("_package")
121
+ df_word.loc[mask_pkg, "Details_fixed"] = "Package"
122
+ if "Product" not in df_word.columns:
123
+ df_word["Product"] = ""
124
+
125
+ df_word["LookupKey_word"] = (
126
+ df_word["Product"].astype(str) + " - " + df_word["Details_fixed"].astype(str)
127
+ )
128
+
129
+ # Merge on LookupKey
130
+ df_word = df_word.merge(
131
+ df_rate,
132
+ left_on="LookupKey_word",
133
+ right_on="LookupKey_sql",
134
+ how="left",
135
+ suffixes=("", "_sql"),
136
+ )
137
+
138
+ # Prefer SQL-standardized fields when present
139
+ def _pick_sql_fallback(df, base_col):
140
+ sql_col = f"{base_col}_sql"
141
+ if sql_col in df.columns:
142
+ df[base_col] = np.where(
143
+ df[sql_col].notna() & (df[sql_col].astype(str).str.strip() != ""),
144
+ df[sql_col],
145
+ df[base_col],
146
+ )
147
+ return df
148
+
149
+ df_word = _pick_sql_fallback(df_word, "Ad Size")
150
+ df_word = _pick_sql_fallback(df_word, "Rate_Type")
151
+ df_word = _pick_sql_fallback(df_word, "Product")
152
+
153
+ # Remove now-redundant columns
154
+ df_word = df_word.drop(
155
+ columns=["Ad Size_sql", "Rate_Type_sql", "Product_sql"], errors=True
156
+ )
157
+
158
+ # Display: prefer SQL concat (Product - PLACEMENT_EXTRACTION) if matched
159
+ df_word["Details"] = np.where(
160
+ df_word["Placement"].notna(),
161
+ df_word["LookupKey_sql"],
162
+ df_word["LookupKey_word"],
163
+ )
164
+
165
+ # Rate_Value chosen ONLY from Word 'Rate' column
166
+ if "Rate" not in df_word.columns:
167
+ df_word["Rate"] = ""
168
+
169
+ def pick_rate(r):
170
+ key = (r.get("Rate") or "").strip().lower()
171
+ if "pmp" in key:
172
+ return r.get("PMP_Rate")
173
+ if "pg" in key or "programmatic guaranteed" in key or "programmatic-guaranteed" in key:
174
+ return r.get("PG_Rate")
175
+ if "direct io" in key or "direct-io" in key or key == "direct" or key.startswith("direct"):
176
+ return r.get("Direct_IO_Rate")
177
+ return None
178
+
179
+ df_word["Rate_Value"] = df_word.apply(pick_rate, axis=1)
180
+
181
+ # Calculate Impressions = (Budget / Rate_Value) * 1000
182
+ budget = pd.to_numeric(df_word["Budget"], errors="coerce")
183
+ rate = pd.to_numeric(df_word["Rate_Value"], errors="coerce")
184
+ df_word["Budget"] = budget.round(2)
185
+ df_word["Rate_Value"] = rate.round(2)
186
+ df_word["Impressions"] = np.where(rate > 0, (budget / rate) * 1000, np.nan)
187
+ df_word["Impressions"] = df_word["Impressions"].round()
188
+
189
+ # Ensure true numeric dtypes
190
+ df_word["Budget"] = pd.to_numeric(df_word["Budget"], errors="coerce").astype(float)
191
+ df_word["Rate_Value"] = pd.to_numeric(df_word["Rate_Value"], errors="coerce").astype(float)
192
+ # keep Impressions as float for editor (avoids %d errors on NaN), cast to int on export
193
+ df_word["Impressions"] = pd.to_numeric(df_word["Impressions"], errors="coerce").astype(float)
194
+
195
+ # Tidy df_word exposed to mapping
196
+ df_word = df_word.drop(
197
+ columns=[
198
+ "PMP_Rate",
199
+ "PG_Rate",
200
+ "Direct_IO_Rate",
201
+ "Details_key",
202
+ "Details_fixed",
203
+ "LookupKey_word",
204
+ "LookupKey_sql",
205
+ "Placement",
206
+ ],
207
+ errors=True,
208
+ )
209
+
210
+ final_cols = [
211
+ "Media Plan",
212
+ "Budget",
213
+ "Product",
214
+ "Details",
215
+ "Targeting",
216
+ "Notes",
217
+ "Rate_Value",
218
+ "Rate_Type",
219
+ "Ad Size",
220
+ "Impressions",
221
+ ]
222
+ remaining = [c for c in df_word.columns if c not in final_cols]
223
+ df_word = df_word[final_cols + remaining].drop_duplicates().reset_index(drop=True)
224
+
225
+ st.session_state["df_word"] = df_word
226
+
227
+ df_word = st.session_state["df_word"]
228
+
229
+ # Preview (Word doc table) — show $ with commas and no decimals for Budget,
230
+ # $ with 2 decimals for Rate_Value, and comma-separated Impressions.
231
+ st.subheader("Extracted table from Word document")
232
+
233
+ df_word_preview = df_word.copy()
234
+
235
+ def _fmt_money0(x):
236
+ return "" if pd.isna(x) else f"${x:,.0f}"
237
+
238
+ def _fmt_money2(x):
239
+ return "" if pd.isna(x) else f"${x:,.2f}"
240
+
241
+ def _fmt_int(x):
242
+ return "" if pd.isna(x) else f"{int(round(float(x))):,}"
243
+
244
+ df_word_preview["Budget"] = df_word["Budget"].map(_fmt_money0)
245
+ df_word_preview["Rate_Value"] = df_word["Rate_Value"].map(_fmt_money2)
246
+ df_word_preview["Impressions"] = df_word["Impressions"].map(_fmt_int)
247
+
248
+ editor = st.data_editor if hasattr(st, "data_editor") else st.experimental_data_editor
249
+ _ = editor(
250
+ df_word_preview,
251
+ num_rows="dynamic",
252
+ use_container_width=True,
253
+ disabled=True, # read-only preview so numeric df_word stays intact
254
+ )
255
+
256
+
257
+ # ─────────────────────────────────────────────────────────────────────────────
258
+ # 2) Upload and load template (Excel/CSV)
259
+ # ─────────────────────────────────────────────────────────────────────────────
260
+ upload_plan = st.file_uploader(
261
+ "2. Upload Media Plan template (.csv, .xls, .xlsx)",
262
+ type=["csv", "xls", "xlsx"],
263
+ key="media_plan_template_uploader",
264
+ )
265
+ if "plan_bytes" not in st.session_state:
266
+ if not upload_plan:
267
+ st.info("Please upload a CSV or Excel file to load the media plan template.")
268
+ st.stop()
269
+ st.session_state["plan_bytes"] = upload_plan.getvalue()
270
+ st.session_state["plan_ext"] = os.path.splitext(upload_plan.name)[1].lower()
271
+ st.session_state["plan_name"] = upload_plan.name
272
+
273
+ plan_bytes = st.session_state["plan_bytes"]
274
+ plan_ext = st.session_state["plan_ext"]
275
+ plan_name = st.session_state.get("plan_name", "template.xlsx")
276
+
277
+ if plan_ext in [".xls", ".xlsx"]:
278
+ xls = pd.ExcelFile(io.BytesIO(plan_bytes), engine="openpyxl")
279
+ sheet = st.selectbox("Select sheet to load from", xls.sheet_names, key="sheet")
280
+ else:
281
+ sheet = None
282
+
283
+ # ─────────────────────────────────────────────────────────────────────────────
284
+ # 3) Detect header & keep ALL headered columns
285
+ # ─────────────────────────────────────────────────────────────────────────────
286
+ if "df_slice" not in st.session_state:
287
+ if plan_ext in [".xls", ".xlsx"]:
288
+ df_all = pd.read_excel(
289
+ io.BytesIO(plan_bytes), sheet_name=sheet, header=None, engine="openpyxl"
290
+ )
291
+ else:
292
+ df_all = pd.read_csv(
293
+ io.BytesIO(plan_bytes), header=None, dtype=str, keep_default_na=False
294
+ )
295
+
296
+ # Heuristic: pick row with most non-blank cells among first 30 rows
297
+ max_cnt, hdr = 0, None
298
+
299
+ def _notblank(v):
300
+ s = "" if pd.isna(v) else str(v).strip()
301
+ return s != ""
302
+
303
+ for idx, row in df_all.head(30).iterrows():
304
+ cnt = sum(1 for v in row if _notblank(v))
305
+ if cnt > max_cnt:
306
+ max_cnt, hdr = cnt, idx
307
+
308
+ if hdr is None or max_cnt < 1:
309
+ st.error("Could not detect header row.")
310
+ st.stop()
311
+
312
+ row = df_all.iloc[hdr]
313
+ keep_idx = [i for i, v in enumerate(row) if _notblank(v)]
314
+ header_cols = [str(row[i]).strip() for i in keep_idx]
315
+
316
+ df_slice = df_all.iloc[hdr + 1 :, keep_idx].reset_index(drop=True)
317
+ df_slice.columns = header_cols
318
+
319
+ st.session_state["hdr_idx"] = hdr
320
+ st.session_state["df_slice"] = df_slice
321
+ st.session_state["header_cols"] = header_cols
322
+ st.session_state["keep_idx"] = keep_idx
323
+
324
+ df_slice = st.session_state["df_slice"]
325
+ header_cols = st.session_state["header_cols"]
326
+ keep_idx = st.session_state.get("keep_idx", list(range(len(header_cols))))
327
+
328
+ # ─────────────────────────────────────────────────────────────────────────────
329
+ # 4) Mapping UI (NO FUZZY)
330
+ # ─────────────────────────────────────────────────────────────────────────────
331
+ def canon(s: str) -> str:
332
+ if s is None:
333
+ return ""
334
+ s = re.sub(r"\s+", " ", str(s)).strip().lower()
335
+ s = re.sub(r"[^a-z0-9]+", " ", s)
336
+ return re.sub(r"\s+", " ", s).strip()
337
+
338
+ word_cols = df_word.columns.tolist()
339
+
340
+ ALIASES = {
341
+ "media plan": ["media plan", "plan", "campaign id", "mp", "line id"],
342
+ "budget": ["budget", "cost", "net cost", "amount", "spend"],
343
+ "product": ["product", "package", "pkg name"],
344
+ "details": [
345
+ "details",
346
+ "placement",
347
+ "tactic/placement description",
348
+ "description",
349
+ "line item",
350
+ "line name",
351
+ ],
352
+ "targeting": ["targeting", "audience", "segment", "geo", "demo"],
353
+ "notes": ["notes", "comments", "remark"],
354
+ "rate value": ["rate value", "rate", "price", "cpm value", "cppv value"],
355
+ "rate type": ["rate type", "unit", "rate unit", "cpm", "cppv", "cpv"],
356
+ "ad size": ["ad size", "size", "dimensions", "adsize"],
357
+ "impressions": ["impressions", "imps", "imp", "units (impressions)"],
358
+ }
359
+ WORD_COLS = {
360
+ "media plan": "Media Plan",
361
+ "budget": "Budget",
362
+ "product": "Product",
363
+ "details": "Details",
364
+ "targeting": "Targeting",
365
+ "notes": "Notes",
366
+ "rate value": "Rate_Value",
367
+ "rate type": "Rate_Type",
368
+ "ad size": "Ad Size",
369
+ "impressions": "Impressions",
370
+ }
371
+
372
+ alias_index = {}
373
+ for key, examples in ALIASES.items():
374
+ target = WORD_COLS.get(key)
375
+ if not target or target not in word_cols:
376
+ continue
377
+ for ex in examples:
378
+ alias_index[canon(ex)] = target
379
+
380
+ mapping = st.session_state.get("mapping", {})
381
+ for h in header_cols:
382
+ hc = canon(h)
383
+ suggested = alias_index.get(hc, "<leave blank>")
384
+ mapping.setdefault(h, suggested)
385
+
386
+ st.subheader("Map Excel headers to Word columns")
387
+ cols_ui = st.columns(4)
388
+ opts = ["<leave blank>"] + word_cols
389
+
390
+ def status_badge(is_filled: bool) -> str:
391
+ bg = "#198754" if is_filled else "#DC3545"
392
+ text = "mapped" if is_filled else "blank"
393
+ return (
394
+ f'<span style="display:inline-block; background:{bg}; color:white; '
395
+ f"padding:2px 8px; border-radius:999px; font-size:12px; "
396
+ f'line-height:18px; margin-left:6px;">{text}</span>'
397
+ )
398
+
399
+ for i, excel_col in enumerate(header_cols):
400
+ default = mapping.get(excel_col, "<leave blank>")
401
+ idx = opts.index(default) if default in opts else 0
402
+ col = cols_ui[i % 4]
403
+ current_value = st.session_state.get(f"map_{i}", default)
404
+ is_filled = current_value != "<leave blank>"
405
+ col.markdown(
406
+ f'<div style="font-weight:600;">{excel_col}{status_badge(is_filled)}</div>',
407
+ unsafe_allow_html=True,
408
+ )
409
+ sel = col.selectbox(
410
+ label="",
411
+ options=opts,
412
+ index=idx,
413
+ key=f"map_{i}",
414
+ label_visibility="collapsed",
415
+ )
416
+ mapping[excel_col] = sel
417
+
418
+ st.session_state["mapping"] = mapping
419
+
420
+ # ─────────────────────────────────────────────────────────────────────────────
421
+ # Extra inputs – campaign date range
422
+ # ─────────────────────────────────────────────────────────────────────────────
423
+ st.subheader("Campaign Dates")
424
+ default_start = date.today()
425
+ default_end = default_start + timedelta(days=30)
426
+ st.date_input(
427
+ "Select campaign start and end dates",
428
+ value=(default_start, default_end),
429
+ key="campaign_dates",
430
+ )
431
+
432
+ # ─────────────────────────────────────────────────────────────────────────────
433
+ # 5) Fill template columns
434
+ # ─────────────────────────────────────────────────────────────────────────────
435
+ if st.button("Fill Template Columns", key="fill_btn"):
436
+ combined = df_slice.copy()
437
+ used = set()
438
+ duplicates = []
439
+
440
+ # Apply mapping, enforcing uniqueness
441
+ for excel_col in header_cols:
442
+ word_col = mapping.get(excel_col, "<leave blank>")
443
+ if word_col == "<leave blank>":
444
+ continue
445
+ if word_col in used:
446
+ duplicates.append(word_col)
447
+ continue
448
+ if word_col == "Budget":
449
+ # Only fill Budget on rows that have a Rate_Value
450
+ if excel_col not in combined.columns:
451
+ continue
452
+
453
+ src_budget = pd.to_numeric(df_word["Budget"], errors="coerce")
454
+ rv = pd.to_numeric(df_word["Rate_Value"], errors="coerce")
455
+
456
+ # rows with a non-null, non-zero rate_value
457
+ has_rate_idx = df_word.index[rv.notna()]
458
+ # restrict to rows that exist in the combined DF
459
+ has_rate_idx = [i for i in has_rate_idx if i < len(combined)]
460
+
461
+ # clear the entire target column, then fill only those rows
462
+ combined[excel_col] = np.nan
463
+ if has_rate_idx:
464
+ combined.loc[has_rate_idx, excel_col] = src_budget.loc[has_rate_idx].values
465
+ else:
466
+ if excel_col in combined.columns:
467
+ combined[excel_col] = df_word[word_col].reset_index(drop=True)
468
+
469
+ # mark as used for BOTH branches
470
+ used.add(word_col)
471
+
472
+
473
+
474
+ if duplicates:
475
+ st.warning(
476
+ "The following Word columns were selected more than once and were ignored for duplicates: "
477
+ f"{sorted(set(duplicates))}"
478
+ )
479
+
480
+ # Fill Start/End Date columns if present
481
+ campaign_dates = st.session_state.get("campaign_dates")
482
+ if campaign_dates and len(campaign_dates) == 2:
483
+ start_date, end_date = campaign_dates
484
+ start_aliases = {"start date", "flight start", "flight start date"}
485
+ end_aliases = {"end date", "flight end", "flight end date"}
486
+ start_col = next((c for c in header_cols if canon(c) in start_aliases), None)
487
+ end_col = next((c for c in header_cols if canon(c) in end_aliases), None)
488
+ if start_col and start_col in combined.columns:
489
+ combined[start_col] = start_date
490
+ if end_col and end_col in combined.columns:
491
+ combined[end_col] = end_date
492
+
493
+ # Format numeric columns based on mapping (strict numerics for export)
494
+ for col in header_cols:
495
+ word_col = mapping.get(col)
496
+ if word_col == "Budget" and col in combined.columns:
497
+ combined[col] = pd.to_numeric(combined[col], errors="coerce").round(2).astype(float)
498
+ elif word_col == "Impressions" and col in combined.columns:
499
+ # keep as float for editor (no %d/NaN issue); export will coerce to int
500
+ combined[col] = pd.to_numeric(combined[col], errors="coerce").round(0).astype(float)
501
+ elif word_col == "Rate_Value" and col in combined.columns:
502
+ combined[col] = pd.to_numeric(combined[col], errors="coerce").round(2).astype(float)
503
+
504
+ st.session_state["combined"] = combined
505
+
506
+ # ─────────────────────────────────────────────────────────────────────────────
507
+ # 6) Show + Download
508
+ # ─────────────────────────────────────────────────────────────────────────────
509
+ if "combined" in st.session_state:
510
+ # Filled Media Plan Template (formatted preview)
511
+ st.subheader("Filled Media Plan Template")
512
+
513
+ combined = st.session_state["combined"]
514
+ combined_preview = combined.copy()
515
+ mapping = st.session_state.get("mapping", {}) # Excel col name -> Word col name
516
+
517
+ def _fmt_money0(x):
518
+ return "" if pd.isna(x) else f"${float(x):,.0f}"
519
+
520
+ def _fmt_money2(x):
521
+ return "" if pd.isna(x) else f"${float(x):,.2f}"
522
+
523
+ def _fmt_int(x):
524
+ if pd.isna(x):
525
+ return ""
526
+ try:
527
+ return f"{int(round(float(x))):,}"
528
+ except Exception:
529
+ return ""
530
+
531
+ # Helper: find the Excel column that was mapped to a given Word column
532
+ def _excel_col_for(word_col: str):
533
+ for excel_col, mapped in mapping.items():
534
+ if mapped == word_col and excel_col in combined_preview.columns:
535
+ return excel_col
536
+ return None
537
+
538
+ budget_col = _excel_col_for("Budget")
539
+ rate_col = _excel_col_for("Rate_Value")
540
+ imps_col = _excel_col_for("Impressions")
541
+
542
+ # Apply formatting ONLY to columns that exist
543
+ if budget_col:
544
+ combined_preview[budget_col] = combined_preview[budget_col].map(_fmt_money0)
545
+ if rate_col:
546
+ combined_preview[rate_col] = combined_preview[rate_col].map(_fmt_money2)
547
+ if imps_col:
548
+ combined_preview[imps_col] = combined_preview[imps_col].map(_fmt_int)
549
+
550
+ editor = st.data_editor if hasattr(st, "data_editor") else st.experimental_data_editor
551
+ _ = editor(
552
+ combined_preview,
553
+ num_rows="dynamic",
554
+ use_container_width=True,
555
+ disabled=True, # keeps numeric types intact in st.session_state["combined"]
556
+ )
557
+
558
+
559
+
560
+ # Export filled template as Excel, preserving original workbook formatting
561
+ try:
562
+ if plan_ext in [".xls", ".xlsx"]:
563
+ wb = load_workbook(io.BytesIO(plan_bytes))
564
+ ws = wb[sheet]
565
+ else:
566
+ wb = Workbook()
567
+ ws = wb.active
568
+
569
+ hdr_idx = st.session_state.get("hdr_idx", 0)
570
+ keep_idx = st.session_state.get("keep_idx", list(range(len(header_cols))))
571
+
572
+ # Clear existing data below header for mapped columns
573
+ for r in range(hdr_idx + 2, ws.max_row + 1):
574
+ for c in keep_idx:
575
+ ws.cell(row=r, column=c + 1).value = None
576
+
577
+ # Accounting formats: 0-decimals for Budget, 2-decimals for Rate_Value
578
+ acct_usd_0 = '_($* #,##0_);_($* (#,##0);_($* "-"_);_(@_)'
579
+ acct_usd_2 = '_($* #,##0.00_);_($* (#,##0.00);_($* "-"??_);_(@_)'
580
+
581
+ mapping = st.session_state.get("mapping", {})
582
+ cols = st.session_state["combined"].columns.tolist()
583
+
584
+ def _to_excel_numeric(v):
585
+ if pd.isna(v):
586
+ return None
587
+ if isinstance(v, (np.floating,)):
588
+ return float(v)
589
+ if isinstance(v, (np.integer,)):
590
+ return int(v)
591
+ return v
592
+
593
+ # Write data while retaining styles
594
+ for r_offset, row in enumerate(
595
+ st.session_state["combined"].itertuples(index=False), start=0
596
+ ):
597
+ for c_offset, val in enumerate(row):
598
+ excel_row = hdr_idx + 2 + r_offset
599
+ excel_col_idx = keep_idx[c_offset] + 1
600
+ excel_col_name = cols[c_offset]
601
+ word_col = mapping.get(excel_col_name)
602
+
603
+ py_val = _to_excel_numeric(val)
604
+
605
+ # Impressions should be integer for "#,##0"
606
+ if word_col == "Impressions" and py_val is not None:
607
+ try:
608
+ py_val = int(round(float(py_val)))
609
+ except Exception:
610
+ pass
611
+
612
+ cell = ws.cell(row=excel_row, column=excel_col_idx, value=py_val)
613
+
614
+ # Number formats
615
+ if word_col == "Impressions":
616
+ cell.number_format = "#,##0"
617
+ elif word_col == "Budget":
618
+ cell.number_format = acct_usd_0 # Accounting, no decimals
619
+ elif word_col == "Rate_Value":
620
+ cell.number_format = acct_usd_2 # Accounting, 2 decimals
621
+
622
+ out = io.BytesIO()
623
+ wb.save(out)
624
+ out.seek(0)
625
+ st.download_button(
626
+ "Download Filled Excel",
627
+ out.read(),
628
+ file_name=f"filled_{os.path.splitext(plan_name)[0]}.xlsx",
629
+ mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
630
+ )
631
+ except Exception as e:
632
+ st.error(f"Excel download error: {e}")
633
+
requirements.txt ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ altair
2
+ streamlit
3
+ pandas
4
+ gspread
5
+ google-auth
6
+ openai
7
+ snowflake-connector-python[pandas]
8
+ cryptography
9
+ openpyxl
10
+ python-docx
style.css DELETED
@@ -1,28 +0,0 @@
1
- body {
2
- padding: 2rem;
3
- font-family: -apple-system, BlinkMacSystemFont, "Arial", sans-serif;
4
- }
5
-
6
- h1 {
7
- font-size: 16px;
8
- margin-top: 0;
9
- }
10
-
11
- p {
12
- color: rgb(107, 114, 128);
13
- font-size: 15px;
14
- margin-bottom: 10px;
15
- margin-top: 5px;
16
- }
17
-
18
- .card {
19
- max-width: 620px;
20
- margin: 0 auto;
21
- padding: 16px;
22
- border: 1px solid lightgray;
23
- border-radius: 16px;
24
- }
25
-
26
- .card p:last-child {
27
- margin-bottom: 0;
28
- }