GuglielmoTor commited on
Commit
92fd5a6
Β·
verified Β·
1 Parent(s): d252c6d

Create analytics_fetch_and_rendering.py

Browse files
Files changed (1) hide show
  1. analytics_fetch_and_rendering.py +262 -0
analytics_fetch_and_rendering.py ADDED
@@ -0,0 +1,262 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ def extract_follower_gains(data):
2
+ """Extracts monthly follower gains from API response."""
3
+ results = []
4
+ print(f"Raw gains data received for extraction: {json.dumps(data, indent=2)}") # Debug print
5
+ elements = data.get("elements", [])
6
+ if not elements:
7
+ print("Warning: No 'elements' found in follower statistics response.")
8
+ return []
9
+
10
+ for item in elements:
11
+ time_range = item.get("timeRange", {})
12
+ start_timestamp = time_range.get("start")
13
+ if start_timestamp is None:
14
+ print("Warning: Skipping item due to missing start timestamp.")
15
+ continue
16
+
17
+ # Convert timestamp to YYYY-MM format for clearer labeling
18
+ # Use UTC timezone explicitly
19
+ try:
20
+ date_obj = datetime.fromtimestamp(start_timestamp / 1000, tz=timezone.utc)
21
+ # Format as Year-Month (e.g., 2024-03)
22
+ date_str = date_obj.strftime('%Y-%m')
23
+ except Exception as time_e:
24
+ print(f"Warning: Could not parse timestamp {start_timestamp}. Error: {time_e}. Skipping item.")
25
+ continue
26
+
27
+ follower_gains = item.get("followerGains", {})
28
+ # Handle potential None values from API by defaulting to 0
29
+ organic_gain = follower_gains.get("organicFollowerGain", 0) or 0
30
+ paid_gain = follower_gains.get("paidFollowerGain", 0) or 0
31
+
32
+ results.append({
33
+ "date": date_str, # Store simplified date string
34
+ "organic": organic_gain,
35
+ "paid": paid_gain
36
+ })
37
+
38
+ print(f"Extracted follower gains (unsorted): {results}") # Debug print
39
+ # Sort results by date string to ensure chronological order for plotting
40
+ try:
41
+ results.sort(key=lambda x: x['date'])
42
+ except Exception as sort_e:
43
+ print(f"Warning: Could not sort follower gains by date. Error: {sort_e}")
44
+
45
+ print(f"Extracted follower gains (sorted): {results}")
46
+ return results
47
+
48
+ def fetch_analytics_data(comm_client_id, comm_token):
49
+ """Fetches org URN, follower count, and follower gains using the Marketing token."""
50
+ print("--- Fetching Analytics Data ---")
51
+ if not comm_token:
52
+ raise ValueError("comm_token is missing.")
53
+
54
+ token_dict = comm_token if isinstance(comm_token, dict) else {'access_token': comm_token, 'token_type': 'Bearer'}
55
+ ln_mkt = create_session(comm_client_id, token=token_dict)
56
+
57
+ try:
58
+ # 1. Fetch Org URN and Name
59
+ print("Fetching Org URN for analytics...")
60
+ # This function already handles errors and raises ValueError
61
+ org_urn, org_name = fetch_org_urn(token_dict)
62
+ print(f"Analytics using Org: {org_name} ({org_urn})")
63
+
64
+ # 2. Fetch Follower Count (v2 API)
65
+ # Endpoint requires r_organization_social permission
66
+ print("Fetching follower count...")
67
+ count_url = f"{API_V2_BASE}/networkSizes/{org_urn}?edgeType=CompanyFollowedByMember"
68
+ print(f"Requesting follower count from: {count_url}")
69
+ resp_count = ln_mkt.get(count_url)
70
+ print(f"β†’ COUNT Response Status: {resp_count.status_code}")
71
+ print(f"β†’ COUNT Response Body: {resp_count.text}")
72
+ resp_count.raise_for_status() # Check for HTTP errors
73
+ count_data = resp_count.json()
74
+ # The follower count is in 'firstDegreeSize'
75
+ follower_count = count_data.get("firstDegreeSize", 0)
76
+ print(f"Follower count: {follower_count}")
77
+
78
+
79
+ # 3. Fetch Follower Gains (REST API)
80
+ # Endpoint requires r_organization_social permission
81
+ print("Fetching follower gains...")
82
+ # Calculate start date: 12 months ago, beginning of that month, UTC
83
+ now = datetime.now(timezone.utc)
84
+ # Go back roughly 365 days
85
+ twelve_months_ago = now - timedelta(days=365)
86
+ # Set to the first day of that month
87
+ start_of_period = twelve_months_ago.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
88
+ start_ms = int(start_of_period.timestamp() * 1000)
89
+ print(f"Requesting gains starting from: {start_of_period.strftime('%Y-%m-%d %H:%M:%S %Z')} ({start_ms} ms)")
90
+
91
+ gains_url = (
92
+ f"{API_REST_BASE}/organizationalEntityFollowerStatistics"
93
+ f"?q=organizationalEntity"
94
+ f"&organizationalEntity={org_urn}"
95
+ f"&timeIntervals.timeGranularityType=MONTH"
96
+ f"&timeIntervals.timeRange.start={start_ms}"
97
+ # No end date needed to get data up to the latest available month
98
+ )
99
+ print(f"Requesting gains from: {gains_url}")
100
+ resp_gains = ln_mkt.get(gains_url)
101
+ print(f"β†’ GAINS Request Headers: {resp_gains.request.headers}")
102
+ print(f"β†’ GAINS Response Status: {resp_gains.status_code}")
103
+ print(f"β†’ GAINS Response Body (first 500 chars): {resp_gains.text[:500]}")
104
+ resp_gains.raise_for_status() # Check for HTTP errors
105
+ gains_data = resp_gains.json()
106
+
107
+ # 4. Process Gains Data using the extraction function
108
+ follower_gains_list = extract_follower_gains(gains_data)
109
+
110
+ # Return all fetched data
111
+ return org_name, follower_count, follower_gains_list
112
+
113
+ except requests.exceptions.RequestException as e:
114
+ status = e.response.status_code if e.response is not None else "N/A"
115
+ details = ""
116
+ if e.response is not None:
117
+ try:
118
+ details = f" Details: {e.response.json()}"
119
+ except json.JSONDecodeError:
120
+ details = f" Response: {e.response.text[:200]}..."
121
+ print(f"ERROR fetching analytics data (Status: {status}).{details}")
122
+ # Re-raise a user-friendly error, including the original exception context
123
+ raise ValueError(f"Failed to fetch analytics data from LinkedIn API (Status: {status}). Check permissions (r_organization_social) and API status.") from e
124
+ except ValueError as ve:
125
+ # Catch ValueErrors raised by fetch_org_urn
126
+ print(f"ERROR during analytics data fetch (likely Org URN): {ve}")
127
+ raise ve # Re-raise the specific error message
128
+ except Exception as e:
129
+ print(f"UNEXPECTED ERROR processing analytics data: {e}")
130
+ tb = traceback.format_exc()
131
+ print(tb)
132
+ raise ValueError(f"An unexpected error occurred while fetching or processing analytics data.") from e
133
+
134
+
135
+ def plot_follower_gains(follower_data):
136
+ """Generates a matplotlib plot for follower gains. Returns the figure object."""
137
+ print(f"Plotting follower gains data: {follower_data}")
138
+ plt.style.use('seaborn-v0_8-whitegrid') # Use a nice style
139
+
140
+ if not follower_data:
141
+ print("No follower data to plot.")
142
+ # Create an empty plot with a message
143
+ fig, ax = plt.subplots(figsize=(10, 5))
144
+ ax.text(0.5, 0.5, 'No follower gains data available for the last 12 months.',
145
+ horizontalalignment='center', verticalalignment='center',
146
+ transform=ax.transAxes, fontsize=12, color='grey')
147
+ ax.set_title('Monthly Follower Gains (Last 12 Months)')
148
+ ax.set_xlabel('Month')
149
+ ax.set_ylabel('Follower Gains')
150
+ # Remove ticks if there's no data
151
+ ax.set_xticks([])
152
+ ax.set_yticks([])
153
+ plt.tight_layout()
154
+ return fig # Return the figure object
155
+
156
+ try:
157
+ # Ensure data is sorted by date (should be done in extract_follower_gains, but double-check)
158
+ follower_data.sort(key=lambda x: x['date'])
159
+
160
+ dates = [entry['date'] for entry in follower_data] # Should be 'YYYY-MM' strings
161
+ organic_gains = [entry['organic'] for entry in follower_data]
162
+ paid_gains = [entry['paid'] for entry in follower_data]
163
+
164
+ # Create the plot
165
+ fig, ax = plt.subplots(figsize=(12, 6)) # Use fig, ax pattern
166
+
167
+ ax.plot(dates, organic_gains, label='Organic Follower Gain', marker='o', linestyle='-', color='#0073b1') # LinkedIn blue
168
+ ax.plot(dates, paid_gains, label='Paid Follower Gain', marker='x', linestyle='--', color='#d9534f') # Reddish color
169
+
170
+ # Customize the plot
171
+ ax.set_xlabel('Month (YYYY-MM)')
172
+ ax.set_ylabel('Number of New Followers')
173
+ ax.set_title('Monthly Follower Gains (Last 12 Months)')
174
+
175
+ # Improve x-axis label readability
176
+ # Show fewer labels if there are many months
177
+ tick_frequency = max(1, len(dates) // 10) # Show label roughly every N months
178
+ ax.set_xticks(dates[::tick_frequency])
179
+ ax.tick_params(axis='x', rotation=45, labelsize=9) # Rotate and adjust size
180
+
181
+ ax.legend(title="Gain Type")
182
+ ax.grid(True, linestyle='--', alpha=0.6) # Lighter grid
183
+
184
+ # Add value labels on top of bars/points (optional, can get crowded)
185
+ # for i, (org, paid) in enumerate(zip(organic_gains, paid_gains)):
186
+ # if org > 0: ax.text(i, org, f'{org}', ha='center', va='bottom', fontsize=8)
187
+ # if paid > 0: ax.text(i, paid, f'{paid}', ha='center', va='bottom', fontsize=8, color='red')
188
+
189
+
190
+ plt.tight_layout() # Adjust layout to prevent labels from overlapping
191
+
192
+ print("Successfully generated follower gains plot.")
193
+ # Return the figure object for Gradio
194
+ return fig
195
+ except Exception as plot_e:
196
+ print(f"ERROR generating follower gains plot: {plot_e}")
197
+ tb = traceback.format_exc()
198
+ print(tb)
199
+ # Return an empty plot with an error message if plotting fails
200
+ fig, ax = plt.subplots(figsize=(10, 5))
201
+ ax.text(0.5, 0.5, f'Error generating plot: {plot_e}',
202
+ horizontalalignment='center', verticalalignment='center',
203
+ transform=ax.transAxes, fontsize=12, color='red', wrap=True)
204
+ ax.set_title('Follower Gains Plot Error')
205
+ plt.tight_layout()
206
+ return fig
207
+
208
+
209
+ def fetch_and_render_analytics(comm_client_id, comm_token):
210
+ """Fetches analytics data and prepares updates for Gradio UI."""
211
+ print("--- Rendering Analytics Tab ---")
212
+ # Initial state for outputs
213
+ count_output = gr.update(value="<p>Loading follower count...</p>", visible=True)
214
+ plot_output = gr.update(value=None, visible=False) # Hide plot initially
215
+
216
+ if not comm_token:
217
+ print("ERROR: Marketing token missing for analytics.")
218
+ error_msg = "<p style='color: red; text-align: center; font-weight: bold;'>❌ Error: Missing LinkedIn Marketing token. Please complete the login process first.</p>"
219
+ return gr.update(value=error_msg, visible=True), gr.update(value=None, visible=False)
220
+
221
+ try:
222
+ # Fetch all data together
223
+ org_name, follower_count, follower_gains_list = fetch_analytics_data(comm_client_id, comm_token)
224
+
225
+ # Format follower count display - Nicer HTML
226
+ count_display_html = f"""
227
+ <div style='text-align: center; padding: 20px; background-color: #e7f3ff; border: 1px solid #bce8f1; border-radius: 8px; margin-bottom: 20px; box-shadow: 0 2px 4px rgba(0,0,0,0.1);'>
228
+ <p style='font-size: 1.1em; color: #31708f; margin-bottom: 5px;'>Total Followers for</p>
229
+ <p style='font-size: 1.4em; font-weight: bold; color: #005a9e; margin-bottom: 10px;'>{html.escape(org_name)}</p>
230
+ <p style='font-size: 2.8em; font-weight: bold; color: #0073b1; margin: 0;'>{follower_count:,}</p>
231
+ <p style='font-size: 0.9em; color: #777; margin-top: 5px;'>(As of latest data available)</p>
232
+ </div>
233
+ """
234
+ count_output = gr.update(value=count_display_html, visible=True)
235
+
236
+ # Generate plot
237
+ print("Generating follower gains plot...")
238
+ plot_fig = plot_follower_gains(follower_gains_list)
239
+ # If plot generation failed, plot_fig might contain an error message plot
240
+ plot_output = gr.update(value=plot_fig, visible=True)
241
+
242
+ print("Analytics data fetched and processed successfully.")
243
+ return count_output, plot_output
244
+
245
+ except (ValueError, requests.exceptions.RequestException) as api_ve:
246
+ # Catch specific API or configuration errors from fetch_analytics_data
247
+ print(f"API or VALUE ERROR during analytics fetch: {api_ve}")
248
+ error_update = display_error(f"Failed to load analytics: {api_ve}", api_ve)
249
+ # Show error in the count area, hide plot
250
+ return gr.update(value=error_update.get('value', "<p style='color: red;'>Error loading follower count.</p>"), visible=True), gr.update(value=None, visible=False)
251
+ except Exception as e:
252
+ # Catch any other unexpected errors during fetch or plotting
253
+ print(f"UNEXPECTED ERROR during analytics rendering: {e}")
254
+ tb = traceback.format_exc()
255
+ print(tb)
256
+ error_update = display_error("An unexpected error occurred while loading analytics.", e)
257
+ error_html = error_update.get('value', "<p style='color: red;'>An unexpected error occurred.</p>")
258
+ # Ensure the error message is HTML-safe
259
+ if isinstance(error_html, str) and not error_html.strip().startswith("<"):
260
+ error_html = f"<pre style='color: red; white-space: pre-wrap;'>{html.escape(error_html)}</pre>"
261
+ # Show error in the count area, hide plot
262
+ return gr.update(value=error_html, visible=True), gr.update(value=None, visible=False)