GuglielmoTor commited on
Commit
2a3b22e
Β·
verified Β·
1 Parent(s): f806c16

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +154 -70
app.py CHANGED
@@ -1,16 +1,21 @@
1
- # -*- coding: utf-8 -*-
2
  import gradio as gr
3
  import json
4
  import os
5
  import logging
6
  import html
 
7
 
 
8
  from Data_Fetching_and_Rendering import fetch_and_render_dashboard
9
  from analytics_fetch_and_rendering import fetch_and_render_analytics
10
  from mentions_dashboard import generate_mentions_dashboard
11
  from gradio_utils import get_url_user_token
12
- from Bubble_API_Calls import fetch_linkedin_token_from_bubble, bulk_upload_to_bubble
13
-
 
 
 
 
14
  from Linkedin_Data_API_Calls import (
15
  fetch_linkedin_posts_core,
16
  fetch_comments,
@@ -19,83 +24,125 @@ from Linkedin_Data_API_Calls import (
19
  prepare_data_for_bubble
20
  )
21
 
 
 
 
22
  def check_token_status(token_state):
 
23
  return "βœ… Token available" if token_state and token_state.get("token") else "❌ Token not available"
24
 
25
-
26
  def process_and_store_bubble_token(url_user_token, org_urn, token_state):
27
- new_state = token_state.copy() if token_state else {"token": None, "client_id": None, "org_urn": None}
28
- new_state.update({"token": None, "org_urn": org_urn})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29
 
30
  client_id = os.environ.get("Linkedin_client_id")
31
  if not client_id:
32
- print("❌ CRITICAL ERROR: 'Linkedin_client_id' environment variable not set.")
33
  new_state["client_id"] = "ENV VAR MISSING"
34
- return check_token_status(new_state), new_state
35
-
36
- new_state["client_id"] = client_id
37
- if not url_user_token or "not found" in url_user_token or "Could not access" in url_user_token:
38
- return check_token_status(new_state), new_state
39
-
40
- print(f"Attempting to fetch token from Bubble with user token: {url_user_token}")
41
- parsed = fetch_linkedin_token_from_bubble(url_user_token)
42
-
43
- if isinstance(parsed, dict) and "access_token" in parsed:
44
- new_state["token"] = parsed
45
- print("βœ… Token successfully fetched from Bubble.")
46
  else:
47
- print("❌ Failed to fetch a valid token from Bubble.")
48
-
49
- return check_token_status(new_state), new_state
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
50
 
51
  def guarded_fetch_posts(token_state):
 
 
 
 
52
  logging.info("Starting guarded_fetch_posts process.")
53
  if not token_state or not token_state.get("token"):
54
- logging.error("Access denied. No token available.")
55
- return "<p style='color:red; text-align:center;'>❌ Access denied. No token available.</p>"
56
 
57
  client_id = token_state.get("client_id")
58
  token_dict = token_state.get("token")
59
- org_urn = token_state.get('org_urn') # Ensure 'org_urn' is correctly fetched from token_state
60
 
61
  if not org_urn:
62
- logging.error("Organization URN (org_urn) not found in token_state.")
63
  return "<p style='color:red; text-align:center;'>❌ Configuration error: Organization URN missing.</p>"
64
- if not client_id:
65
- logging.error("Client ID not found in token_state.")
66
- return "<p style='color:red; text-align:center;'>❌ Configuration error: Client ID missing.</p>"
67
-
68
 
69
  try:
70
- # Step 1: Fetch core post data (text, summary, category) and their basic stats
71
  logging.info(f"Step 1: Fetching core posts for org_urn: {org_urn}")
72
  processed_raw_posts, stats_map, _ = fetch_linkedin_posts_core(client_id, token_dict, org_urn)
73
- # org_name is returned as the third item, captured as _ if not used directly here
74
 
75
  if not processed_raw_posts:
76
  logging.info("No posts found to process after step 1.")
77
- return "<p style='color:orange; text-align:center;'>ℹ️ No posts found to process.</p>"
78
 
79
  post_urns = [post["id"] for post in processed_raw_posts if post.get("id")]
80
  logging.info(f"Extracted {len(post_urns)} post URNs for further processing.")
81
 
82
- # Step 2: Fetch comments for these posts
83
  logging.info("Step 2: Fetching comments.")
84
  all_comments_data = fetch_comments(client_id, token_dict, post_urns, stats_map)
85
 
86
- # Step 3: Analyze sentiment of the comments
87
  logging.info("Step 3: Analyzing sentiment.")
88
  sentiments_per_post = analyze_sentiment(all_comments_data)
89
 
90
- # Step 4: Compile detailed post objects
91
  logging.info("Step 4: Compiling detailed posts.")
92
  detailed_posts = compile_detailed_posts(processed_raw_posts, stats_map, sentiments_per_post)
93
 
94
- # Step 5: Prepare data for Bubble
95
  logging.info("Step 5: Preparing data for Bubble.")
96
  li_posts, li_post_stats, li_post_comments = prepare_data_for_bubble(detailed_posts, all_comments_data)
97
 
98
- # Step 6: Bulk upload to Bubble
99
  logging.info("Step 6: Uploading data to Bubble.")
100
  bulk_upload_to_bubble(li_posts, "LI_posts")
101
  bulk_upload_to_bubble(li_post_stats, "LI_post_stats")
@@ -104,68 +151,94 @@ def guarded_fetch_posts(token_state):
104
  logging.info("Successfully fetched and uploaded posts and comments to Bubble.")
105
  return "<p style='color:green; text-align:center;'>βœ… Posts and comments uploaded to Bubble.</p>"
106
 
107
- except ValueError as ve: # Catch specific errors like "Failed to fetch posts"
108
  logging.error(f"ValueError during LinkedIn data processing: {ve}")
109
  return f"<p style='color:red; text-align:center;'>❌ Error: {html.escape(str(ve))}</p>"
110
  except Exception as e:
111
- logging.exception("An unexpected error occurred in guarded_fetch_posts.") # Logs full traceback
112
  return "<p style='color:red; text-align:center;'>❌ An unexpected error occurred. Please check logs.</p>"
113
 
114
-
115
-
116
  def guarded_fetch_dashboard(token_state):
 
117
  if not token_state or not token_state.get("token"):
118
- return "<p style='color:red; text-align:center;'>❌ Access denied. No token available.</p>"
119
- return fetch_and_render_dashboard(token_state.get("client_id"), token_state.get("token"))
 
 
 
 
 
 
120
 
121
 
122
  def guarded_fetch_analytics(token_state):
 
123
  if not token_state or not token_state.get("token"):
124
- return ("<p style='color:red; text-align:center;'>❌ Access denied. No token available.</p>",
125
  None, None, None, None, None, None, None)
126
-
127
  return fetch_and_render_analytics(token_state.get("client_id"), token_state.get("token"))
128
 
129
-
130
  def run_mentions_and_load(token_state):
 
131
  if not token_state or not token_state.get("token"):
132
- return ("<p style='color:red; text-align:center;'>❌ Access denied. No token available.</p>", None)
133
  return generate_mentions_dashboard(token_state.get("client_id"), token_state.get("token"))
134
 
135
-
136
  with gr.Blocks(theme=gr.themes.Soft(primary_hue="blue", secondary_hue="sky"),
137
  title="LinkedIn Post Viewer & Analytics") as app:
138
 
139
- token_state = gr.State(value={"token": None, "client_id": None, "org_urn": None})
 
140
 
141
  gr.Markdown("# πŸš€ LinkedIn Organization Post Viewer & Analytics")
142
  gr.Markdown("Token is supplied via URL parameter for Bubble.io lookup. Then explore dashboard and analytics.")
143
 
144
  url_user_token_display = gr.Textbox(label="User Token (from URL - Hidden)", interactive=False, visible=False)
145
- status_box = gr.Textbox(label="Overall Token Status", interactive=False)
146
- org_urn = gr.Textbox(visible=False) # Needed for input, was missing from initial script
147
-
148
- app.load(fn=get_url_user_token, inputs=None, outputs=[url_user_token_display, org_urn])
149
-
150
- url_user_token_display.change(
151
- fn=process_and_store_bubble_token,
152
- inputs=[url_user_token_display, org_urn, token_state],
153
- outputs=[status_box, token_state]
154
- )
155
 
156
-
157
- app.load(fn=check_token_status, inputs=[token_state], outputs=status_box)
158
- gr.Timer(5.0).tick(fn=check_token_status, inputs=[token_state], outputs=status_box)
159
 
160
  with gr.Tabs():
161
- with gr.TabItem("1️⃣ Dashboard"):
162
- gr.Markdown("View your organization's recent posts and their engagement statistics.")
 
163
 
164
- sync_posts_to_bubble_btn = gr.Button("πŸ”„ Fetch, Analyze & Store Posts to Bubble", variant="primary")
165
- dashboard_html_output = gr.HTML("<p style='text-align: center; color: #555;'>Click the button to fetch posts and store them in Bubble. Status will appear here.</p>")
 
 
 
 
 
 
166
 
167
- # Corrected: The click handler now calls guarded_fetch_posts
168
- # and dashboard_html_output is correctly defined in this scope.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
169
  sync_posts_to_bubble_btn.click(
170
  fn=guarded_fetch_posts,
171
  inputs=[token_state],
@@ -205,8 +278,19 @@ with gr.Blocks(theme=gr.themes.Soft(primary_hue="blue", secondary_hue="sky"),
205
  inputs=[token_state],
206
  outputs=[mentions_html, mentions_plot]
207
  )
 
 
 
 
 
 
 
208
 
209
  if __name__ == "__main__":
210
  if not os.environ.get("Linkedin_client_id"):
211
- print("WARNING: The 'Linkedin_client_id' environment variable is not set. The application may not function correctly.")
 
 
 
212
  app.launch(server_name="0.0.0.0", server_port=7860, share=True)
 
 
 
1
  import gradio as gr
2
  import json
3
  import os
4
  import logging
5
  import html
6
+ import pandas as pd
7
 
8
+ # Import functions from your custom modules
9
  from Data_Fetching_and_Rendering import fetch_and_render_dashboard
10
  from analytics_fetch_and_rendering import fetch_and_render_analytics
11
  from mentions_dashboard import generate_mentions_dashboard
12
  from gradio_utils import get_url_user_token
13
+ # Updated import to include fetch_posts_from_bubble
14
+ from Bubble_API_Calls import (
15
+ fetch_linkedin_token_from_bubble,
16
+ bulk_upload_to_bubble,
17
+ fetch_posts_from_bubble # Added new function
18
+ )
19
  from Linkedin_Data_API_Calls import (
20
  fetch_linkedin_posts_core,
21
  fetch_comments,
 
24
  prepare_data_for_bubble
25
  )
26
 
27
+ # Configure logging
28
+ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
29
+
30
  def check_token_status(token_state):
31
+ """Checks the status of the LinkedIn token."""
32
  return "βœ… Token available" if token_state and token_state.get("token") else "❌ Token not available"
33
 
 
34
  def process_and_store_bubble_token(url_user_token, org_urn, token_state):
35
+ """
36
+ Processes the user token from the URL, fetches LinkedIn token from Bubble,
37
+ fetches initial posts from Bubble, and updates the token state and UI accordingly.
38
+ """
39
+ logging.info(f"Processing token with URL user token: '{url_user_token}', Org URN: '{org_urn}'")
40
+ # Initialize or copy existing state, adding bubble_posts_df
41
+ new_state = token_state.copy() if token_state else {"token": None, "client_id": None, "org_urn": None, "bubble_posts_df": None}
42
+ new_state.update({"token": None, "org_urn": org_urn, "bubble_posts_df": None}) # Ensure bubble_posts_df is reset/initialized
43
+
44
+ # Default button state: invisible and non-interactive
45
+ button_update = gr.Button(
46
+ value="πŸ”„ Fetch, Analyze & Store Posts to Bubble",
47
+ variant="primary",
48
+ visible=False,
49
+ interactive=False
50
+ )
51
 
52
  client_id = os.environ.get("Linkedin_client_id")
53
  if not client_id:
54
+ logging.error("CRITICAL ERROR: 'Linkedin_client_id' environment variable not set.")
55
  new_state["client_id"] = "ENV VAR MISSING"
56
+ # Even if client_id is missing, we might still be able to fetch from Bubble if org_urn is present
57
+ # and then decide button visibility.
 
 
 
 
 
 
 
 
 
 
58
  else:
59
+ new_state["client_id"] = client_id
60
+
61
+ # Attempt to fetch LinkedIn token from Bubble (related to LinkedIn API access)
62
+ if url_user_token and "not found" not in url_user_token and "Could not access" not in url_user_token:
63
+ logging.info(f"Attempting to fetch LinkedIn token from Bubble with user token: {url_user_token}")
64
+ parsed_linkedin_token = fetch_linkedin_token_from_bubble(url_user_token)
65
+ if isinstance(parsed_linkedin_token, dict) and "access_token" in parsed_linkedin_token:
66
+ new_state["token"] = parsed_linkedin_token
67
+ logging.info("βœ… LinkedIn Token successfully fetched from Bubble.")
68
+ else:
69
+ logging.warning("❌ Failed to fetch a valid LinkedIn token from Bubble.")
70
+ else:
71
+ logging.info("No valid URL user token provided for LinkedIn token fetch, or an error was indicated.")
72
+
73
+ # Fetch posts from Bubble using org_urn, regardless of LinkedIn token status for this specific fetch
74
+ current_org_urn = new_state.get("org_urn")
75
+ if current_org_urn:
76
+ logging.info(f"Attempting to fetch posts from Bubble for org_urn: {current_org_urn}")
77
+ try:
78
+ # Assuming fetch_posts_from_bubble returns a Pandas DataFrame or None
79
+ df_bubble_posts = fetch_posts_from_bubble(current_org_urn)
80
+ new_state["bubble_posts_df"] = df_bubble_posts
81
+
82
+ if df_bubble_posts is not None and not df_bubble_posts.empty:
83
+ logging.info(f"βœ… Successfully fetched {len(df_bubble_posts)} posts from Bubble. Sync button will be enabled.")
84
+ button_update = gr.Button(
85
+ value="πŸ”„ Fetch, Analyze & Store Posts to Bubble",
86
+ variant="primary",
87
+ visible=True,
88
+ interactive=True
89
+ )
90
+ else:
91
+ logging.info("ℹ️ No posts found in Bubble for this organization or DataFrame is empty. Sync button will remain hidden.")
92
+ except Exception as e:
93
+ logging.error(f"❌ Error fetching posts from Bubble: {e}")
94
+ # Keep button hidden on error
95
+ else:
96
+ logging.warning("Org URN not available in state. Cannot fetch posts from Bubble.")
97
+
98
+ token_status_message = check_token_status(new_state)
99
+ logging.info(f"Token processing complete. Status: {token_status_message}. Button visible: {button_update.visible}")
100
+ return token_status_message, new_state, button_update
101
 
102
  def guarded_fetch_posts(token_state):
103
+ """
104
+ Fetches LinkedIn posts, analyzes them, and uploads to Bubble.
105
+ This function is guarded by token availability.
106
+ """
107
  logging.info("Starting guarded_fetch_posts process.")
108
  if not token_state or not token_state.get("token"):
109
+ logging.error("Access denied for guarded_fetch_posts. No LinkedIn token available.")
110
+ return "<p style='color:red; text-align:center;'>❌ Access denied. LinkedIn token not available.</p>"
111
 
112
  client_id = token_state.get("client_id")
113
  token_dict = token_state.get("token")
114
+ org_urn = token_state.get('org_urn')
115
 
116
  if not org_urn:
117
+ logging.error("Organization URN (org_urn) not found in token_state for guarded_fetch_posts.")
118
  return "<p style='color:red; text-align:center;'>❌ Configuration error: Organization URN missing.</p>"
119
+ if not client_id or client_id == "ENV VAR MISSING":
120
+ logging.error("Client ID not found or missing in token_state for guarded_fetch_posts.")
121
+ return "<p style='color:red; text-align:center;'>❌ Configuration error: LinkedIn Client ID missing.</p>"
 
122
 
123
  try:
 
124
  logging.info(f"Step 1: Fetching core posts for org_urn: {org_urn}")
125
  processed_raw_posts, stats_map, _ = fetch_linkedin_posts_core(client_id, token_dict, org_urn)
 
126
 
127
  if not processed_raw_posts:
128
  logging.info("No posts found to process after step 1.")
129
+ return "<p style='color:orange; text-align:center;'>ℹ️ No new LinkedIn posts found to process.</p>"
130
 
131
  post_urns = [post["id"] for post in processed_raw_posts if post.get("id")]
132
  logging.info(f"Extracted {len(post_urns)} post URNs for further processing.")
133
 
 
134
  logging.info("Step 2: Fetching comments.")
135
  all_comments_data = fetch_comments(client_id, token_dict, post_urns, stats_map)
136
 
 
137
  logging.info("Step 3: Analyzing sentiment.")
138
  sentiments_per_post = analyze_sentiment(all_comments_data)
139
 
 
140
  logging.info("Step 4: Compiling detailed posts.")
141
  detailed_posts = compile_detailed_posts(processed_raw_posts, stats_map, sentiments_per_post)
142
 
 
143
  logging.info("Step 5: Preparing data for Bubble.")
144
  li_posts, li_post_stats, li_post_comments = prepare_data_for_bubble(detailed_posts, all_comments_data)
145
 
 
146
  logging.info("Step 6: Uploading data to Bubble.")
147
  bulk_upload_to_bubble(li_posts, "LI_posts")
148
  bulk_upload_to_bubble(li_post_stats, "LI_post_stats")
 
151
  logging.info("Successfully fetched and uploaded posts and comments to Bubble.")
152
  return "<p style='color:green; text-align:center;'>βœ… Posts and comments uploaded to Bubble.</p>"
153
 
154
+ except ValueError as ve:
155
  logging.error(f"ValueError during LinkedIn data processing: {ve}")
156
  return f"<p style='color:red; text-align:center;'>❌ Error: {html.escape(str(ve))}</p>"
157
  except Exception as e:
158
+ logging.exception("An unexpected error occurred in guarded_fetch_posts.")
159
  return "<p style='color:red; text-align:center;'>❌ An unexpected error occurred. Please check logs.</p>"
160
 
 
 
161
  def guarded_fetch_dashboard(token_state):
162
+ """Fetches and renders the dashboard if token is available."""
163
  if not token_state or not token_state.get("token"):
164
+ return "❌ Access denied. No token available for dashboard."
165
+ # This function is not used in the current UI structure for the first tab's main content
166
+ # but kept for potential future use or if it's called elsewhere.
167
+ # The first tab's content is now primarily the button and its output.
168
+ # If you intend to display a dashboard here *after* fetching, this would need integration.
169
+ # For now, returning a placeholder or status.
170
+ # return fetch_and_render_dashboard(token_state.get("client_id"), token_state.get("token"))
171
+ return "<p style='text-align: center; color: #555;'>Dashboard content would load here if implemented.</p>"
172
 
173
 
174
  def guarded_fetch_analytics(token_state):
175
+ """Fetches and renders analytics if token is available."""
176
  if not token_state or not token_state.get("token"):
177
+ return ("❌ Access denied. No token available for analytics.",
178
  None, None, None, None, None, None, None)
 
179
  return fetch_and_render_analytics(token_state.get("client_id"), token_state.get("token"))
180
 
 
181
  def run_mentions_and_load(token_state):
182
+ """Generates mentions dashboard if token is available."""
183
  if not token_state or not token_state.get("token"):
184
+ return ("❌ Access denied. No token available for mentions.", None)
185
  return generate_mentions_dashboard(token_state.get("client_id"), token_state.get("token"))
186
 
187
+ # --- Gradio UI Blocks ---
188
  with gr.Blocks(theme=gr.themes.Soft(primary_hue="blue", secondary_hue="sky"),
189
  title="LinkedIn Post Viewer & Analytics") as app:
190
 
191
+ # Initialize state with the new field for Bubble DataFrame
192
+ token_state = gr.State(value={"token": None, "client_id": None, "org_urn": None, "bubble_posts_df": None})
193
 
194
  gr.Markdown("# πŸš€ LinkedIn Organization Post Viewer & Analytics")
195
  gr.Markdown("Token is supplied via URL parameter for Bubble.io lookup. Then explore dashboard and analytics.")
196
 
197
  url_user_token_display = gr.Textbox(label="User Token (from URL - Hidden)", interactive=False, visible=False)
198
+ status_box = gr.Textbox(label="Overall Token Status", interactive=False, value="Initializing...") # Initial status
199
+ org_urn_display = gr.Textbox(label="Organization URN (from URL - Hidden)", interactive=False, visible=False) # Renamed for clarity
 
 
 
 
 
 
 
 
200
 
201
+ # Load user token and org URN from URL parameters
202
+ app.load(fn=get_url_user_token, inputs=None, outputs=[url_user_token_display, org_urn_display])
 
203
 
204
  with gr.Tabs():
205
+ with gr.TabItem("1️⃣ Dashboard & Sync"):
206
+ gr.Markdown("View your organization's recent posts and their engagement statistics. "
207
+ "Fetch new posts from LinkedIn, analyze, and store them in Bubble.")
208
 
209
+ # Button is initially not visible and not interactive.
210
+ # Its state will be updated by process_and_store_bubble_token
211
+ sync_posts_to_bubble_btn = gr.Button(
212
+ "πŸ”„ Fetch, Analyze & Store Posts to Bubble",
213
+ variant="primary",
214
+ visible=False,
215
+ interactive=False
216
+ )
217
 
218
+ dashboard_html_output = gr.HTML(
219
+ "<p style='text-align: center; color: #555;'>System initializing... Status and actions will appear shortly. "
220
+ "If data is found in Bubble, the 'Fetch, Analyze & Store' button will become active.</p>"
221
+ )
222
+
223
+ # Event: When URL token or org URN is loaded/changed, process it.
224
+ # This will update token_state and the sync_posts_to_bubble_btn.
225
+ # Using org_urn_display.change as the primary trigger after app.load completes.
226
+ # If get_url_user_token is very fast, app.load might be better, but .change is robust.
227
+ org_urn_display.change(
228
+ fn=process_and_store_bubble_token,
229
+ inputs=[url_user_token_display, org_urn_display, token_state],
230
+ outputs=[status_box, token_state, sync_posts_to_bubble_btn] # Added button to outputs
231
+ )
232
+ # Also trigger if url_user_token_display changes, in case org_urn loads first
233
+ # but token processing depends on url_user_token_display.
234
+ # This creates a dependency: if one changes, the function runs with current values of both.
235
+ url_user_token_display.change(
236
+ fn=process_and_store_bubble_token,
237
+ inputs=[url_user_token_display, org_urn_display, token_state],
238
+ outputs=[status_box, token_state, sync_posts_to_bubble_btn]
239
+ )
240
+
241
+ # Click handler for the sync button
242
  sync_posts_to_bubble_btn.click(
243
  fn=guarded_fetch_posts,
244
  inputs=[token_state],
 
278
  inputs=[token_state],
279
  outputs=[mentions_html, mentions_plot]
280
  )
281
+
282
+ # Initial check of token status on app load (primarily for the status_box)
283
+ # The button visibility is handled by process_and_store_bubble_token
284
+ app.load(fn=lambda ts: check_token_status(ts), inputs=[token_state], outputs=status_box)
285
+ # Timer to periodically update the token status display (optional, but good for UX)
286
+ gr.Timer(15.0).tick(fn=lambda ts: check_token_status(ts), inputs=[token_state], outputs=status_box)
287
+
288
 
289
  if __name__ == "__main__":
290
  if not os.environ.get("Linkedin_client_id"):
291
+ logging.warning("WARNING: The 'Linkedin_client_id' environment variable is not set. The application may not function correctly for LinkedIn API calls.")
292
+ # Ensure the app launches.
293
+ # For testing, you might want share=False or specific server_name/port.
294
+ # share=True is useful for public sharing via Gradio link.
295
  app.launch(server_name="0.0.0.0", server_port=7860, share=True)
296
+ # app.launch(share=True) # Simpler launch for testing if specific port/host not needed