GuglielmoTor commited on
Commit
791c130
Β·
verified Β·
1 Parent(s): d33a3a6

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +134 -75
app.py CHANGED
@@ -2,11 +2,14 @@ import gradio as gr
2
  import pandas as pd
3
  import os
4
  import logging
 
 
 
 
 
5
 
6
  # --- Module Imports ---
7
- # Functions from your existing/provided custom modules
8
- from analytics_fetch_and_rendering import fetch_and_render_analytics # Assuming this exists
9
- from gradio_utils import get_url_user_token # For fetching URL parameters
10
 
11
  # Functions from newly created/refactored modules
12
  from config import (
@@ -20,139 +23,198 @@ from ui_generators import (
20
  run_mentions_tab_display,
21
  run_follower_stats_tab_display
22
  )
 
 
 
23
 
24
  # Configure logging
25
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
26
 
27
- # --- Guarded Analytics Fetch ---
28
- def guarded_fetch_analytics(token_state):
29
- """Guarded call to fetch_and_render_analytics, ensuring token and basic data structures."""
30
- if not token_state or not token_state.get("token"):
31
- logging.warning("Analytics fetch: Access denied. No token.")
32
- # Ensure the number of returned Nones matches the expected number of outputs for the plots
33
- return ("❌ Access denied. No token.", None, None, None, None, None, None, None)
34
 
35
- # Ensure DataFrames are passed, even if empty, to avoid errors in the analytics function
36
- posts_df_analytics = token_state.get("bubble_posts_df", pd.DataFrame())
37
- mentions_df_analytics = token_state.get("bubble_mentions_df", pd.DataFrame())
38
- follower_stats_df_analytics = token_state.get("bubble_follower_stats_df", pd.DataFrame())
39
 
40
- logging.info("Calling fetch_and_render_analytics with current token_state data.")
41
  try:
42
- return fetch_and_render_analytics(
43
- token_state.get("client_id"),
44
- token_state.get("token"),
45
- token_state.get("org_urn"),
46
- posts_df_analytics,
47
- mentions_df_analytics,
48
- follower_stats_df_analytics
49
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
50
  except Exception as e:
51
- logging.error(f"Error in guarded_fetch_analytics calling fetch_and_render_analytics: {e}", exc_info=True)
52
- return (f"❌ Error fetching analytics: {e}", None, None, None, None, None, None, None)
 
53
 
54
 
55
  # --- Gradio UI Blocks ---
56
  with gr.Blocks(theme=gr.themes.Soft(primary_hue="blue", secondary_hue="sky"),
57
  title="LinkedIn Organization Dashboard") as app:
58
 
59
- # Central state for holding token, client_id, org_urn, and fetched dataframes
60
  token_state = gr.State(value={
61
  "token": None, "client_id": None, "org_urn": None,
62
  "bubble_posts_df": pd.DataFrame(), "fetch_count_for_api": 0,
63
  "bubble_mentions_df": pd.DataFrame(),
64
  "bubble_follower_stats_df": pd.DataFrame(),
65
- "url_user_token_temp_storage": None
 
 
 
66
  })
67
 
68
  gr.Markdown("# πŸš€ LinkedIn Organization Dashboard")
69
- # Hidden textboxes to capture URL parameters
70
  url_user_token_display = gr.Textbox(label="User Token (from URL - Hidden)", interactive=False, visible=False)
71
  status_box = gr.Textbox(label="Overall LinkedIn Token Status", interactive=False, value="Initializing...")
72
  org_urn_display = gr.Textbox(label="Organization URN (from URL - Hidden)", interactive=False, visible=False)
73
 
74
- # Load URL parameters when the Gradio app loads
75
  app.load(fn=get_url_user_token, inputs=None, outputs=[url_user_token_display, org_urn_display], api_name="get_url_params", show_progress=False)
76
 
77
- # This function will run after URL params are loaded and org_urn_display changes
78
  def initial_load_sequence(url_token, org_urn_val, current_state):
79
  logging.info(f"Initial load sequence triggered. Org URN: {org_urn_val}, URL Token: {'Present' if url_token else 'Absent'}")
80
- # Process token, fetch Bubble data, determine sync needs
81
  status_msg, new_state, btn_update = process_and_store_bubble_token(url_token, org_urn_val, current_state)
82
- # Display initial dashboard content based on (potentially empty) Bubble data
83
  dashboard_content = display_main_dashboard(new_state)
84
  return status_msg, new_state, btn_update, dashboard_content
85
 
86
- with gr.Tabs():
87
- with gr.TabItem("1️⃣ Dashboard & Sync"):
88
  gr.Markdown("System checks for existing data from Bubble. The 'Sync' button activates if new data needs to be fetched from LinkedIn based on the last sync times and data availability.")
89
  sync_data_btn = gr.Button("πŸ”„ Sync LinkedIn Data", variant="primary", visible=False, interactive=False)
90
  sync_status_html_output = gr.HTML("<p style='text-align:center;'>Sync status will appear here.</p>")
91
  dashboard_display_html = gr.HTML("<p style='text-align:center;'>Dashboard loading...</p>")
92
 
93
- # Chain of events for initial load:
94
  org_urn_display.change(
95
  fn=initial_load_sequence,
96
  inputs=[url_user_token_display, org_urn_display, token_state],
97
  outputs=[status_box, token_state, sync_data_btn, dashboard_display_html],
98
  show_progress="full"
99
  )
100
- # Also trigger initial_load_sequence if url_user_token_display changes (e.g. if it loads after org_urn)
101
- # This helps ensure it runs once both are potentially available.
102
- # Note: `org_urn_display.change` might be sufficient if `get_url_user_token` updates both nearly simultaneously.
103
- # Adding this for robustness, but ensure it doesn't cause unwanted multiple runs if state isn't managed carefully.
104
- # Consider using a flag in token_state if multiple triggers become an issue.
105
- # For now, relying on org_urn_display.change as the primary trigger post-load.
106
-
107
- # When Sync button is clicked:
108
- sync_data_btn.click(
109
  fn=sync_all_linkedin_data_orchestrator,
110
  inputs=[token_state],
111
- outputs=[sync_status_html_output, token_state], # token_state is updated here
112
  show_progress="full"
113
  ).then(
114
- fn=process_and_store_bubble_token, # Re-check sync status and update button
115
- inputs=[url_user_token_display, org_urn_display, token_state], # Pass current token_state
116
- outputs=[status_box, token_state, sync_data_btn], # token_state updated again
117
- show_progress=False # Typically "full" for user-initiated actions, "minimal" or False for quick updates
118
  ).then(
119
- fn=display_main_dashboard, # Refresh dashboard display
120
  inputs=[token_state],
121
  outputs=[dashboard_display_html],
122
  show_progress=False
123
  )
124
 
125
- with gr.TabItem("2️⃣ Analytics"):
126
- fetch_analytics_btn = gr.Button("πŸ“ˆ Fetch/Refresh Full Analytics", variant="primary")
127
- follower_count_md = gr.Markdown("Analytics data will load here...")
128
- with gr.Row(): follower_plot, growth_plot = gr.Plot(label="Follower Demographics"), gr.Plot(label="Follower Growth")
129
- with gr.Row(): eng_rate_plot = gr.Plot(label="Engagement Rate")
130
- with gr.Row(): interaction_plot = gr.Plot(label="Post Interactions")
131
- with gr.Row(): eb_plot = gr.Plot(label="Engagement Benchmark")
132
- with gr.Row(): mentions_vol_plot, mentions_sentiment_plot = gr.Plot(label="Mentions Volume"), gr.Plot(label="Mentions Sentiment")
133
-
134
- fetch_analytics_btn.click(
135
- fn=guarded_fetch_analytics, inputs=[token_state],
136
- outputs=[follower_count_md, follower_plot, growth_plot, eng_rate_plot,
137
- interaction_plot, eb_plot, mentions_vol_plot, mentions_sentiment_plot],
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
138
  show_progress="full"
139
  )
140
 
141
- with gr.TabItem("3️⃣ Mentions"):
 
142
  refresh_mentions_display_btn = gr.Button("πŸ”„ Refresh Mentions Display (from local data)", variant="secondary")
143
  mentions_html = gr.HTML("Mentions data loads from Bubble after sync. Click refresh to view current local data.")
144
- mentions_sentiment_dist_plot = gr.Plot(label="Mention Sentiment Distribution")
145
  refresh_mentions_display_btn.click(
146
  fn=run_mentions_tab_display, inputs=[token_state],
147
  outputs=[mentions_html, mentions_sentiment_dist_plot],
148
  show_progress="full"
149
  )
150
 
151
- with gr.TabItem("4️⃣ Follower Stats"):
152
  refresh_follower_stats_btn = gr.Button("πŸ”„ Refresh Follower Stats Display (from local data)", variant="secondary")
153
  follower_stats_html = gr.HTML("Follower statistics load from Bubble after sync. Click refresh to view current local data.")
154
  with gr.Row():
155
- fs_plot_monthly_gains = gr.Plot(label="Monthly Follower Gains")
156
  with gr.Row():
157
  fs_plot_seniority = gr.Plot(label="Followers by Seniority (Top 10 Organic)")
158
  fs_plot_industry = gr.Plot(label="Followers by Industry (Top 10 Organic)")
@@ -162,22 +224,19 @@ with gr.Blocks(theme=gr.themes.Soft(primary_hue="blue", secondary_hue="sky"),
162
  outputs=[follower_stats_html, fs_plot_monthly_gains, fs_plot_seniority, fs_plot_industry],
163
  show_progress="full"
164
  )
 
165
 
166
  if __name__ == "__main__":
167
- # Check for essential environment variables
168
  if not os.environ.get(LINKEDIN_CLIENT_ID_ENV_VAR):
169
- logging.warning(f"WARNING: '{LINKEDIN_CLIENT_ID_ENV_VAR}' environment variable not set. The app may not function correctly for LinkedIn API calls.")
170
  if not os.environ.get(BUBBLE_APP_NAME_ENV_VAR) or \
171
  not os.environ.get(BUBBLE_API_KEY_PRIVATE_ENV_VAR) or \
172
  not os.environ.get(BUBBLE_API_ENDPOINT_ENV_VAR):
173
- logging.warning("WARNING: One or more Bubble environment variables (BUBBLE_APP_NAME, BUBBLE_API_KEY_PRIVATE, BUBBLE_API_ENDPOINT) are not set. Bubble integration will fail.")
174
 
175
  try:
176
- import matplotlib
177
  logging.info(f"Matplotlib version: {matplotlib.__version__} found. Backend: {matplotlib.get_backend()}")
178
- # The backend is now set in ui_generators.py, which is good practice.
179
  except ImportError:
180
- logging.error("Matplotlib is not installed. Plots will not be generated. Please install it: pip install matplotlib")
181
 
182
- # Launch the Gradio app
183
- app.launch(server_name="0.0.0.0", server_port=7860, debug=True)
 
2
  import pandas as pd
3
  import os
4
  import logging
5
+ import matplotlib
6
+ matplotlib.use('Agg') # Set backend for Matplotlib to avoid GUI conflicts with Gradio
7
+ import matplotlib.pyplot as plt
8
+ # No longer need timedelta here if all date logic is in analytics_data_processing
9
+ # from datetime import datetime, timedelta
10
 
11
  # --- Module Imports ---
12
+ from gradio_utils import get_url_user_token
 
 
13
 
14
  # Functions from newly created/refactored modules
15
  from config import (
 
23
  run_mentions_tab_display,
24
  run_follower_stats_tab_display
25
  )
26
+ import analytics_plot_generators
27
+ # NEW: Import for data processing functions
28
+ import analytics_data_processing
29
 
30
  # Configure logging
31
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
32
 
33
+ # --- Analytics Tab: Plot Update Function ---
34
+ def update_analytics_plots(token_state_value, date_filter_option, custom_start_date, custom_end_date):
35
+ """
36
+ Prepares analytics data using external processing function and then generates plots.
37
+ """
38
+ logging.info(f"Updating analytics plots. Filter: {date_filter_option}, Custom Start: {custom_start_date}, Custom End: {custom_end_date}")
 
39
 
40
+ if not token_state_value or not token_state_value.get("token"):
41
+ message = "❌ Access denied. No token. Cannot generate analytics."
42
+ logging.warning(message)
43
+ return message, None, None, None, None, None
44
 
45
+ # --- Prepare Data (Moved to analytics_data_processing) ---
46
  try:
47
+ filtered_posts_df, filtered_mentions_df, follower_stats_df, start_dt_for_msg, end_dt_for_msg = \
48
+ analytics_data_processing.prepare_filtered_analytics_data(
49
+ token_state_value, date_filter_option, custom_start_date, custom_end_date
50
+ )
51
+ except Exception as e:
52
+ error_msg = f"❌ Error preparing analytics data: {e}"
53
+ logging.error(error_msg, exc_info=True)
54
+ return error_msg, None, None, None, None, None
55
+
56
+ # Date column names (still needed for plot generators)
57
+ date_column_posts = token_state_value.get("config_date_col_posts", "published_at")
58
+ date_column_mentions = token_state_value.get("config_date_col_mentions", "date")
59
+ date_column_followers = token_state_value.get("config_date_col_followers", "date")
60
+
61
+ logging.info(f"Data for plotting - Filtered posts: {len(filtered_posts_df)} rows, Filtered Mentions: {len(filtered_mentions_df)} rows.")
62
+ logging.info(f"Follower stats (unfiltered by global range): {len(follower_stats_df)} rows.")
63
+
64
+ # --- Generate Plots ---
65
+ try:
66
+ plot_posts_activity = analytics_plot_generators.generate_posts_activity_plot(filtered_posts_df, date_column_posts)
67
+ plot_engagement_type = analytics_plot_generators.generate_engagement_type_plot(filtered_posts_df)
68
+ plot_mentions_activity = analytics_plot_generators.generate_mentions_activity_plot(filtered_mentions_df, date_column_mentions)
69
+ plot_mention_sentiment = analytics_plot_generators.generate_mention_sentiment_plot(filtered_mentions_df)
70
+ plot_follower_growth = analytics_plot_generators.generate_follower_growth_plot(follower_stats_df, date_column_followers)
71
+
72
+ message = f"πŸ“Š Analytics updated for period: {date_filter_option}"
73
+ if date_filter_option == "Custom Range":
74
+ s_display = start_dt_for_msg.strftime('%Y-%m-%d') if start_dt_for_msg else "Any"
75
+ e_display = end_dt_for_msg.strftime('%Y-%m-%d') if end_dt_for_msg else "Any"
76
+ message += f" (From: {s_display} To: {e_display})"
77
+
78
+ num_plots_generated = sum(1 for p in [plot_posts_activity, plot_engagement_type, plot_mentions_activity, plot_mention_sentiment, plot_follower_growth] if p is not None)
79
+ logging.info(f"Successfully generated {num_plots_generated} plots.")
80
+
81
+ return message, plot_posts_activity, plot_engagement_type, plot_mentions_activity, plot_mention_sentiment, plot_follower_growth
82
  except Exception as e:
83
+ error_msg = f"❌ Error generating analytics plots: {e}"
84
+ logging.error(error_msg, exc_info=True)
85
+ return error_msg, None, None, None, None, None
86
 
87
 
88
  # --- Gradio UI Blocks ---
89
  with gr.Blocks(theme=gr.themes.Soft(primary_hue="blue", secondary_hue="sky"),
90
  title="LinkedIn Organization Dashboard") as app:
91
 
 
92
  token_state = gr.State(value={
93
  "token": None, "client_id": None, "org_urn": None,
94
  "bubble_posts_df": pd.DataFrame(), "fetch_count_for_api": 0,
95
  "bubble_mentions_df": pd.DataFrame(),
96
  "bubble_follower_stats_df": pd.DataFrame(),
97
+ "url_user_token_temp_storage": None,
98
+ "config_date_col_posts": "published_at",
99
+ "config_date_col_mentions": "date",
100
+ "config_date_col_followers": "date"
101
  })
102
 
103
  gr.Markdown("# πŸš€ LinkedIn Organization Dashboard")
 
104
  url_user_token_display = gr.Textbox(label="User Token (from URL - Hidden)", interactive=False, visible=False)
105
  status_box = gr.Textbox(label="Overall LinkedIn Token Status", interactive=False, value="Initializing...")
106
  org_urn_display = gr.Textbox(label="Organization URN (from URL - Hidden)", interactive=False, visible=False)
107
 
 
108
  app.load(fn=get_url_user_token, inputs=None, outputs=[url_user_token_display, org_urn_display], api_name="get_url_params", show_progress=False)
109
 
 
110
  def initial_load_sequence(url_token, org_urn_val, current_state):
111
  logging.info(f"Initial load sequence triggered. Org URN: {org_urn_val}, URL Token: {'Present' if url_token else 'Absent'}")
 
112
  status_msg, new_state, btn_update = process_and_store_bubble_token(url_token, org_urn_val, current_state)
 
113
  dashboard_content = display_main_dashboard(new_state)
114
  return status_msg, new_state, btn_update, dashboard_content
115
 
116
+ with gr.Tabs() as tabs:
117
+ with gr.TabItem("1️⃣ Dashboard & Sync", id="tab_dashboard_sync"):
118
  gr.Markdown("System checks for existing data from Bubble. The 'Sync' button activates if new data needs to be fetched from LinkedIn based on the last sync times and data availability.")
119
  sync_data_btn = gr.Button("πŸ”„ Sync LinkedIn Data", variant="primary", visible=False, interactive=False)
120
  sync_status_html_output = gr.HTML("<p style='text-align:center;'>Sync status will appear here.</p>")
121
  dashboard_display_html = gr.HTML("<p style='text-align:center;'>Dashboard loading...</p>")
122
 
 
123
  org_urn_display.change(
124
  fn=initial_load_sequence,
125
  inputs=[url_user_token_display, org_urn_display, token_state],
126
  outputs=[status_box, token_state, sync_data_btn, dashboard_display_html],
127
  show_progress="full"
128
  )
129
+
130
+ sync_click_event = sync_data_btn.click(
 
 
 
 
 
 
 
131
  fn=sync_all_linkedin_data_orchestrator,
132
  inputs=[token_state],
133
+ outputs=[sync_status_html_output, token_state],
134
  show_progress="full"
135
  ).then(
136
+ fn=process_and_store_bubble_token,
137
+ inputs=[url_user_token_display, org_urn_display, token_state],
138
+ outputs=[status_box, token_state, sync_data_btn],
139
+ show_progress=False
140
  ).then(
141
+ fn=display_main_dashboard,
142
  inputs=[token_state],
143
  outputs=[dashboard_display_html],
144
  show_progress=False
145
  )
146
 
147
+
148
+ with gr.TabItem("2️⃣ Analytics", id="tab_analytics"):
149
+ gr.Markdown("## πŸ“ˆ LinkedIn Performance Analytics")
150
+ gr.Markdown("Select a date range to filter Posts and Mentions analytics. Follower analytics show overall trends and are not affected by this date filter.")
151
+
152
+ analytics_status_md = gr.Markdown("Analytics status will appear here...")
153
+
154
+ with gr.Row():
155
+ date_filter_selector = gr.Radio(
156
+ ["All Time", "Last 7 Days", "Last 30 Days", "Custom Range"],
157
+ label="Select Date Range (for Posts & Mentions)",
158
+ value="Last 30 Days"
159
+ )
160
+ custom_start_date_picker = gr.DatePicker(label="Start Date (Custom)", visible=False)
161
+ custom_end_date_picker = gr.DatePicker(label="End Date (Custom)", visible=False)
162
+
163
+ apply_filter_btn = gr.Button("πŸ” Apply Filter & Refresh Analytics", variant="primary")
164
+
165
+ def toggle_custom_date_pickers(selection):
166
+ return gr.update(visible=selection == "Custom Range"), gr.update(visible=selection == "Custom Range")
167
+
168
+ date_filter_selector.change(
169
+ fn=toggle_custom_date_pickers,
170
+ inputs=[date_filter_selector],
171
+ outputs=[custom_start_date_picker, custom_end_date_picker]
172
+ )
173
+
174
+ gr.Markdown("### Posts & Engagement Overview (Filtered by Date)")
175
+ with gr.Row():
176
+ posts_activity_plot = gr.Plot(label="Posts Activity Over Time")
177
+ engagement_type_plot = gr.Plot(label="Post Engagement Types")
178
+
179
+ gr.Markdown("### Mentions Overview (Filtered by Date)")
180
+ with gr.Row():
181
+ mentions_activity_plot = gr.Plot(label="Mentions Activity Over Time")
182
+ mention_sentiment_plot = gr.Plot(label="Mention Sentiment Distribution")
183
+
184
+ gr.Markdown("### Follower Overview (Not Filtered by Date Range Selector)")
185
+ with gr.Row():
186
+ follower_growth_plot = gr.Plot(label="Follower Growth Over Time")
187
+
188
+ apply_filter_btn.click(
189
+ fn=update_analytics_plots,
190
+ inputs=[token_state, date_filter_selector, custom_start_date_picker, custom_end_date_picker],
191
+ outputs=[analytics_status_md, posts_activity_plot, engagement_type_plot, mentions_activity_plot, mention_sentiment_plot, follower_growth_plot],
192
+ show_progress="full"
193
+ )
194
+
195
+ sync_click_event.then(
196
+ fn=update_analytics_plots,
197
+ inputs=[token_state, date_filter_selector, custom_start_date_picker, custom_end_date_picker],
198
+ outputs=[analytics_status_md, posts_activity_plot, engagement_type_plot, mentions_activity_plot, mention_sentiment_plot, follower_growth_plot],
199
  show_progress="full"
200
  )
201
 
202
+
203
+ with gr.TabItem("3️⃣ Mentions", id="tab_mentions"):
204
  refresh_mentions_display_btn = gr.Button("πŸ”„ Refresh Mentions Display (from local data)", variant="secondary")
205
  mentions_html = gr.HTML("Mentions data loads from Bubble after sync. Click refresh to view current local data.")
206
+ mentions_sentiment_dist_plot = gr.Plot(label="Mention Sentiment Distribution")
207
  refresh_mentions_display_btn.click(
208
  fn=run_mentions_tab_display, inputs=[token_state],
209
  outputs=[mentions_html, mentions_sentiment_dist_plot],
210
  show_progress="full"
211
  )
212
 
213
+ with gr.TabItem("4️⃣ Follower Stats", id="tab_follower_stats"):
214
  refresh_follower_stats_btn = gr.Button("πŸ”„ Refresh Follower Stats Display (from local data)", variant="secondary")
215
  follower_stats_html = gr.HTML("Follower statistics load from Bubble after sync. Click refresh to view current local data.")
216
  with gr.Row():
217
+ fs_plot_monthly_gains = gr.Plot(label="Monthly Follower Gains")
218
  with gr.Row():
219
  fs_plot_seniority = gr.Plot(label="Followers by Seniority (Top 10 Organic)")
220
  fs_plot_industry = gr.Plot(label="Followers by Industry (Top 10 Organic)")
 
224
  outputs=[follower_stats_html, fs_plot_monthly_gains, fs_plot_seniority, fs_plot_industry],
225
  show_progress="full"
226
  )
227
+
228
 
229
  if __name__ == "__main__":
 
230
  if not os.environ.get(LINKEDIN_CLIENT_ID_ENV_VAR):
231
+ logging.warning(f"WARNING: '{LINKEDIN_CLIENT_ID_ENV_VAR}' environment variable not set.")
232
  if not os.environ.get(BUBBLE_APP_NAME_ENV_VAR) or \
233
  not os.environ.get(BUBBLE_API_KEY_PRIVATE_ENV_VAR) or \
234
  not os.environ.get(BUBBLE_API_ENDPOINT_ENV_VAR):
235
+ logging.warning("WARNING: Bubble environment variables not fully set.")
236
 
237
  try:
 
238
  logging.info(f"Matplotlib version: {matplotlib.__version__} found. Backend: {matplotlib.get_backend()}")
 
239
  except ImportError:
240
+ logging.error("Matplotlib is not installed. Plots will not be generated.")
241
 
242
+ app.launch(server_name="0.0.0.0", server_port=7860, debug=True)