Spaces:
Running
Running
File size: 13,417 Bytes
7e61a73 0ef5a0e b905712 0ef5a0e 7e61a73 92fd5a6 |
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 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 |
import json
import requests
from sessions import create_session
from datetime import datetime, timezone, timedelta # Added timezone, timedelta
import matplotlib.pyplot as plt # Added for plotting
import gradio as gr
API_V2_BASE = 'https://api.linkedin.com/v2'
API_REST_BASE = "https://api.linkedin.com/rest"
def extract_follower_gains(data):
"""Extracts monthly follower gains from API response."""
results = []
print(f"Raw gains data received for extraction: {json.dumps(data, indent=2)}") # Debug print
elements = data.get("elements", [])
if not elements:
print("Warning: No 'elements' found in follower statistics response.")
return []
for item in elements:
time_range = item.get("timeRange", {})
start_timestamp = time_range.get("start")
if start_timestamp is None:
print("Warning: Skipping item due to missing start timestamp.")
continue
# Convert timestamp to YYYY-MM format for clearer labeling
# Use UTC timezone explicitly
try:
date_obj = datetime.fromtimestamp(start_timestamp / 1000, tz=timezone.utc)
# Format as Year-Month (e.g., 2024-03)
date_str = date_obj.strftime('%Y-%m')
except Exception as time_e:
print(f"Warning: Could not parse timestamp {start_timestamp}. Error: {time_e}. Skipping item.")
continue
follower_gains = item.get("followerGains", {})
# Handle potential None values from API by defaulting to 0
organic_gain = follower_gains.get("organicFollowerGain", 0) or 0
paid_gain = follower_gains.get("paidFollowerGain", 0) or 0
results.append({
"date": date_str, # Store simplified date string
"organic": organic_gain,
"paid": paid_gain
})
print(f"Extracted follower gains (unsorted): {results}") # Debug print
# Sort results by date string to ensure chronological order for plotting
try:
results.sort(key=lambda x: x['date'])
except Exception as sort_e:
print(f"Warning: Could not sort follower gains by date. Error: {sort_e}")
print(f"Extracted follower gains (sorted): {results}")
return results
def fetch_analytics_data(comm_client_id, comm_token):
"""Fetches org URN, follower count, and follower gains using the Marketing token."""
print("--- Fetching Analytics Data ---")
if not comm_token:
raise ValueError("comm_token is missing.")
token_dict = comm_token if isinstance(comm_token, dict) else {'access_token': comm_token, 'token_type': 'Bearer'}
ln_mkt = create_session(comm_client_id, token=token_dict)
try:
# 1. Fetch Org URN and Name
print("Fetching Org URN for analytics...")
# This function already handles errors and raises ValueError
org_urn, org_name = fetch_org_urn(token_dict)
print(f"Analytics using Org: {org_name} ({org_urn})")
# 2. Fetch Follower Count (v2 API)
# Endpoint requires r_organization_social permission
print("Fetching follower count...")
count_url = f"{API_V2_BASE}/networkSizes/{org_urn}?edgeType=CompanyFollowedByMember"
print(f"Requesting follower count from: {count_url}")
resp_count = ln_mkt.get(count_url)
print(f"β COUNT Response Status: {resp_count.status_code}")
print(f"β COUNT Response Body: {resp_count.text}")
resp_count.raise_for_status() # Check for HTTP errors
count_data = resp_count.json()
# The follower count is in 'firstDegreeSize'
follower_count = count_data.get("firstDegreeSize", 0)
print(f"Follower count: {follower_count}")
# 3. Fetch Follower Gains (REST API)
# Endpoint requires r_organization_social permission
print("Fetching follower gains...")
# Calculate start date: 12 months ago, beginning of that month, UTC
now = datetime.now(timezone.utc)
# Go back roughly 365 days
twelve_months_ago = now - timedelta(days=365)
# Set to the first day of that month
start_of_period = twelve_months_ago.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
start_ms = int(start_of_period.timestamp() * 1000)
print(f"Requesting gains starting from: {start_of_period.strftime('%Y-%m-%d %H:%M:%S %Z')} ({start_ms} ms)")
gains_url = (
f"{API_REST_BASE}/organizationalEntityFollowerStatistics"
f"?q=organizationalEntity"
f"&organizationalEntity={org_urn}"
f"&timeIntervals.timeGranularityType=MONTH"
f"&timeIntervals.timeRange.start={start_ms}"
# No end date needed to get data up to the latest available month
)
print(f"Requesting gains from: {gains_url}")
resp_gains = ln_mkt.get(gains_url)
print(f"β GAINS Request Headers: {resp_gains.request.headers}")
print(f"β GAINS Response Status: {resp_gains.status_code}")
print(f"β GAINS Response Body (first 500 chars): {resp_gains.text[:500]}")
resp_gains.raise_for_status() # Check for HTTP errors
gains_data = resp_gains.json()
# 4. Process Gains Data using the extraction function
follower_gains_list = extract_follower_gains(gains_data)
# Return all fetched data
return org_name, follower_count, follower_gains_list
except requests.exceptions.RequestException as e:
status = e.response.status_code if e.response is not None else "N/A"
details = ""
if e.response is not None:
try:
details = f" Details: {e.response.json()}"
except json.JSONDecodeError:
details = f" Response: {e.response.text[:200]}..."
print(f"ERROR fetching analytics data (Status: {status}).{details}")
# Re-raise a user-friendly error, including the original exception context
raise ValueError(f"Failed to fetch analytics data from LinkedIn API (Status: {status}). Check permissions (r_organization_social) and API status.") from e
except ValueError as ve:
# Catch ValueErrors raised by fetch_org_urn
print(f"ERROR during analytics data fetch (likely Org URN): {ve}")
raise ve # Re-raise the specific error message
except Exception as e:
print(f"UNEXPECTED ERROR processing analytics data: {e}")
tb = traceback.format_exc()
print(tb)
raise ValueError(f"An unexpected error occurred while fetching or processing analytics data.") from e
def plot_follower_gains(follower_data):
"""Generates a matplotlib plot for follower gains. Returns the figure object."""
print(f"Plotting follower gains data: {follower_data}")
plt.style.use('seaborn-v0_8-whitegrid') # Use a nice style
if not follower_data:
print("No follower data to plot.")
# Create an empty plot with a message
fig, ax = plt.subplots(figsize=(10, 5))
ax.text(0.5, 0.5, 'No follower gains data available for the last 12 months.',
horizontalalignment='center', verticalalignment='center',
transform=ax.transAxes, fontsize=12, color='grey')
ax.set_title('Monthly Follower Gains (Last 12 Months)')
ax.set_xlabel('Month')
ax.set_ylabel('Follower Gains')
# Remove ticks if there's no data
ax.set_xticks([])
ax.set_yticks([])
plt.tight_layout()
return fig # Return the figure object
try:
# Ensure data is sorted by date (should be done in extract_follower_gains, but double-check)
follower_data.sort(key=lambda x: x['date'])
dates = [entry['date'] for entry in follower_data] # Should be 'YYYY-MM' strings
organic_gains = [entry['organic'] for entry in follower_data]
paid_gains = [entry['paid'] for entry in follower_data]
# Create the plot
fig, ax = plt.subplots(figsize=(12, 6)) # Use fig, ax pattern
ax.plot(dates, organic_gains, label='Organic Follower Gain', marker='o', linestyle='-', color='#0073b1') # LinkedIn blue
ax.plot(dates, paid_gains, label='Paid Follower Gain', marker='x', linestyle='--', color='#d9534f') # Reddish color
# Customize the plot
ax.set_xlabel('Month (YYYY-MM)')
ax.set_ylabel('Number of New Followers')
ax.set_title('Monthly Follower Gains (Last 12 Months)')
# Improve x-axis label readability
# Show fewer labels if there are many months
tick_frequency = max(1, len(dates) // 10) # Show label roughly every N months
ax.set_xticks(dates[::tick_frequency])
ax.tick_params(axis='x', rotation=45, labelsize=9) # Rotate and adjust size
ax.legend(title="Gain Type")
ax.grid(True, linestyle='--', alpha=0.6) # Lighter grid
# Add value labels on top of bars/points (optional, can get crowded)
# for i, (org, paid) in enumerate(zip(organic_gains, paid_gains)):
# if org > 0: ax.text(i, org, f'{org}', ha='center', va='bottom', fontsize=8)
# if paid > 0: ax.text(i, paid, f'{paid}', ha='center', va='bottom', fontsize=8, color='red')
plt.tight_layout() # Adjust layout to prevent labels from overlapping
print("Successfully generated follower gains plot.")
# Return the figure object for Gradio
return fig
except Exception as plot_e:
print(f"ERROR generating follower gains plot: {plot_e}")
tb = traceback.format_exc()
print(tb)
# Return an empty plot with an error message if plotting fails
fig, ax = plt.subplots(figsize=(10, 5))
ax.text(0.5, 0.5, f'Error generating plot: {plot_e}',
horizontalalignment='center', verticalalignment='center',
transform=ax.transAxes, fontsize=12, color='red', wrap=True)
ax.set_title('Follower Gains Plot Error')
plt.tight_layout()
return fig
def fetch_and_render_analytics(comm_client_id, comm_token):
"""Fetches analytics data and prepares updates for Gradio UI."""
print("--- Rendering Analytics Tab ---")
# Initial state for outputs
count_output = gr.update(value="<p>Loading follower count...</p>", visible=True)
plot_output = gr.update(value=None, visible=False) # Hide plot initially
if not comm_token:
print("ERROR: Marketing token missing for analytics.")
error_msg = "<p style='color: red; text-align: center; font-weight: bold;'>β Error: Missing LinkedIn Marketing token. Please complete the login process first.</p>"
return gr.update(value=error_msg, visible=True), gr.update(value=None, visible=False)
try:
# Fetch all data together
org_name, follower_count, follower_gains_list = fetch_analytics_data(comm_client_id, comm_token)
# Format follower count display - Nicer HTML
count_display_html = f"""
<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);'>
<p style='font-size: 1.1em; color: #31708f; margin-bottom: 5px;'>Total Followers for</p>
<p style='font-size: 1.4em; font-weight: bold; color: #005a9e; margin-bottom: 10px;'>{html.escape(org_name)}</p>
<p style='font-size: 2.8em; font-weight: bold; color: #0073b1; margin: 0;'>{follower_count:,}</p>
<p style='font-size: 0.9em; color: #777; margin-top: 5px;'>(As of latest data available)</p>
</div>
"""
count_output = gr.update(value=count_display_html, visible=True)
# Generate plot
print("Generating follower gains plot...")
plot_fig = plot_follower_gains(follower_gains_list)
# If plot generation failed, plot_fig might contain an error message plot
plot_output = gr.update(value=plot_fig, visible=True)
print("Analytics data fetched and processed successfully.")
return count_output, plot_output
except (ValueError, requests.exceptions.RequestException) as api_ve:
# Catch specific API or configuration errors from fetch_analytics_data
print(f"API or VALUE ERROR during analytics fetch: {api_ve}")
error_update = display_error(f"Failed to load analytics: {api_ve}", api_ve)
# Show error in the count area, hide plot
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)
except Exception as e:
# Catch any other unexpected errors during fetch or plotting
print(f"UNEXPECTED ERROR during analytics rendering: {e}")
tb = traceback.format_exc()
print(tb)
error_update = display_error("An unexpected error occurred while loading analytics.", e)
error_html = error_update.get('value', "<p style='color: red;'>An unexpected error occurred.</p>")
# Ensure the error message is HTML-safe
if isinstance(error_html, str) and not error_html.strip().startswith("<"):
error_html = f"<pre style='color: red; white-space: pre-wrap;'>{html.escape(error_html)}</pre>"
# Show error in the count area, hide plot
return gr.update(value=error_html, visible=True), gr.update(value=None, visible=False) |