Spaces:
Running
Running
Update app.py
Browse files
app.py
CHANGED
@@ -4,7 +4,8 @@ import json
|
|
4 |
import os
|
5 |
import logging
|
6 |
import html
|
7 |
-
import pandas as pd # Ensure pandas is imported
|
|
|
8 |
|
9 |
# Import functions from your custom modules
|
10 |
from Data_Fetching_and_Rendering import fetch_and_render_dashboard
|
@@ -15,7 +16,7 @@ from gradio_utils import get_url_user_token
|
|
15 |
from Bubble_API_Calls import (
|
16 |
fetch_linkedin_token_from_bubble,
|
17 |
bulk_upload_to_bubble,
|
18 |
-
fetch_linkedin_posts_data_from_bubble
|
19 |
)
|
20 |
from Linkedin_Data_API_Calls import (
|
21 |
fetch_linkedin_posts_core,
|
@@ -34,21 +35,20 @@ def check_token_status(token_state):
|
|
34 |
|
35 |
def process_and_store_bubble_token(url_user_token, org_urn, token_state):
|
36 |
"""
|
37 |
-
Processes
|
38 |
-
|
39 |
-
|
40 |
"""
|
41 |
logging.info(f"Processing token with URL user token: '{url_user_token}', Org URN: '{org_urn}'")
|
42 |
|
43 |
-
|
44 |
-
|
45 |
-
|
46 |
-
|
47 |
-
new_state.update({"org_urn": org_urn, "bubble_posts_df":
|
48 |
|
49 |
-
#
|
50 |
-
|
51 |
-
button_interactive = True
|
52 |
|
53 |
client_id = os.environ.get("Linkedin_client_id")
|
54 |
if not client_id:
|
@@ -57,87 +57,131 @@ def process_and_store_bubble_token(url_user_token, org_urn, token_state):
|
|
57 |
else:
|
58 |
new_state["client_id"] = client_id
|
59 |
|
60 |
-
# Attempt to fetch LinkedIn token from Bubble (related to LinkedIn API access)
|
61 |
if url_user_token and "not found" not in url_user_token and "Could not access" not in url_user_token:
|
62 |
logging.info(f"Attempting to fetch LinkedIn token from Bubble with user token: {url_user_token}")
|
63 |
try:
|
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 |
-
new_state["token"] = None
|
70 |
logging.warning(f"β Failed to fetch a valid LinkedIn token from Bubble. Response: {parsed_linkedin_token}")
|
71 |
except Exception as e:
|
72 |
-
new_state["token"] = None
|
73 |
logging.error(f"β Exception while fetching LinkedIn token from Bubble: {e}")
|
74 |
else:
|
75 |
-
new_state["token"] = None
|
76 |
logging.info("No valid URL user token provided for LinkedIn token fetch, or an error was indicated.")
|
77 |
|
78 |
-
# Fetch posts from Bubble
|
79 |
current_org_urn = new_state.get("org_urn")
|
|
|
80 |
if current_org_urn:
|
81 |
logging.info(f"Attempting to fetch posts from Bubble for org_urn: {current_org_urn}")
|
82 |
try:
|
83 |
-
|
84 |
-
|
85 |
-
|
86 |
-
logging.info(f"df {len(bubble_posts_df)}")
|
87 |
-
|
88 |
-
if bubble_posts_df is not None and not new_state["bubble_posts_df"].empty:
|
89 |
-
logging.info(f"β
Successfully fetched {len(bubble_posts_df)} posts from Bubble. Sync button will be disabled.")
|
90 |
-
button_visible = False
|
91 |
-
button_interactive = False
|
92 |
else:
|
93 |
-
|
94 |
-
|
95 |
except Exception as e:
|
96 |
-
logging.error(f"β Error fetching posts from Bubble: {e}")
|
97 |
-
#
|
98 |
else:
|
99 |
logging.warning("Org URN not available in state. Cannot fetch posts from Bubble.")
|
100 |
-
|
101 |
-
|
102 |
-
|
103 |
-
|
104 |
-
|
105 |
-
|
106 |
-
|
107 |
-
|
108 |
-
|
109 |
-
|
110 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
111 |
|
112 |
def guarded_fetch_posts(token_state):
|
113 |
"""
|
114 |
-
Fetches LinkedIn posts
|
115 |
-
|
116 |
"""
|
117 |
logging.info("Starting guarded_fetch_posts process.")
|
118 |
-
if not token_state or not token_state.get("token"):
|
119 |
logging.error("Access denied for guarded_fetch_posts. No LinkedIn token available.")
|
120 |
-
return "<p style='color:red; text-align:center;'>β Access denied. LinkedIn token not available
|
121 |
|
122 |
client_id = token_state.get("client_id")
|
123 |
-
token_dict = token_state.get("token")
|
124 |
org_urn = token_state.get('org_urn')
|
|
|
125 |
|
126 |
if not org_urn:
|
127 |
logging.error("Organization URN (org_urn) not found in token_state for guarded_fetch_posts.")
|
128 |
return "<p style='color:red; text-align:center;'>β Configuration error: Organization URN missing.</p>"
|
129 |
if not client_id or client_id == "ENV VAR MISSING":
|
130 |
logging.error("Client ID not found or missing in token_state for guarded_fetch_posts.")
|
131 |
-
return "<p style='color:red; text-align:center;'>β Configuration error: LinkedIn Client ID missing
|
132 |
|
133 |
-
|
134 |
-
|
135 |
-
|
136 |
-
|
|
|
|
|
|
|
|
|
137 |
|
138 |
try:
|
139 |
-
logging.info(f"Step 1: Fetching core posts for org_urn: {org_urn}
|
140 |
-
processed_raw_posts, stats_map, _ = fetch_linkedin_posts_core(client_id, token_dict, org_urn)
|
141 |
|
142 |
if not processed_raw_posts:
|
143 |
logging.info("No posts found to process via LinkedIn API after step 1.")
|
@@ -163,32 +207,33 @@ def guarded_fetch_posts(token_state):
|
|
163 |
bulk_upload_to_bubble(li_post_stats, "LI_post_stats")
|
164 |
bulk_upload_to_bubble(li_post_comments, "LI_post_comments")
|
165 |
|
166 |
-
|
167 |
-
|
|
|
168 |
|
169 |
except ValueError as ve:
|
170 |
logging.error(f"ValueError during LinkedIn data processing: {ve}")
|
171 |
return f"<p style='color:red; text-align:center;'>β Error: {html.escape(str(ve))}</p>"
|
172 |
except Exception as e:
|
173 |
-
logging.exception("An unexpected error occurred in guarded_fetch_posts.")
|
174 |
-
return "<p style='color:red; text-align:center;'>β An unexpected error occurred
|
175 |
|
176 |
def guarded_fetch_dashboard(token_state):
|
177 |
-
"""Fetches and renders the dashboard if token is available."""
|
178 |
if not token_state or not token_state.get("token"):
|
179 |
return "β Access denied. No token available for dashboard."
|
180 |
-
|
|
|
|
|
|
|
181 |
|
182 |
|
183 |
def guarded_fetch_analytics(token_state):
|
184 |
-
"""Fetches and renders analytics if token is available."""
|
185 |
if not token_state or not token_state.get("token"):
|
186 |
return ("β Access denied. No token available for analytics.",
|
187 |
None, None, None, None, None, None, None)
|
188 |
return fetch_and_render_analytics(token_state.get("client_id"), token_state.get("token"))
|
189 |
|
190 |
def run_mentions_and_load(token_state):
|
191 |
-
"""Generates mentions dashboard if token is available."""
|
192 |
if not token_state or not token_state.get("token"):
|
193 |
return ("β Access denied. No token available for mentions.", None)
|
194 |
return generate_mentions_dashboard(token_state.get("client_id"), token_state.get("token"))
|
@@ -197,44 +242,50 @@ def run_mentions_and_load(token_state):
|
|
197 |
with gr.Blocks(theme=gr.themes.Soft(primary_hue="blue", secondary_hue="sky"),
|
198 |
title="LinkedIn Post Viewer & Analytics") as app:
|
199 |
|
200 |
-
#
|
201 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
202 |
|
203 |
gr.Markdown("# π LinkedIn Organization Post Viewer & Analytics")
|
204 |
gr.Markdown("Token is supplied via URL parameter for Bubble.io lookup. Then explore dashboard and analytics.")
|
205 |
|
206 |
url_user_token_display = gr.Textbox(label="User Token (from URL - Hidden)", interactive=False, visible=False)
|
207 |
-
status_box = gr.Textbox(label="Overall LinkedIn Token Status", interactive=False, value="Initializing...")
|
208 |
org_urn_display = gr.Textbox(label="Organization URN (from URL - Hidden)", interactive=False, visible=False)
|
209 |
|
210 |
app.load(fn=get_url_user_token, inputs=None, outputs=[url_user_token_display, org_urn_display])
|
211 |
|
212 |
with gr.Tabs():
|
213 |
with gr.TabItem("1οΈβ£ Dashboard & Sync"):
|
214 |
-
gr.Markdown("
|
215 |
|
216 |
sync_posts_to_bubble_btn = gr.Button(
|
217 |
-
"π
|
218 |
variant="primary",
|
219 |
-
visible=False,
|
220 |
interactive=False
|
221 |
)
|
222 |
|
223 |
dashboard_html_output = gr.HTML(
|
224 |
"<p style='text-align: center; color: #555;'>System initializing... "
|
225 |
-
"Checking for existing data in Bubble
|
226 |
)
|
227 |
|
228 |
-
# Combined trigger: process tokens and Bubble data once both URL params are potentially loaded.
|
229 |
-
# Using .then() to chain after initial load.
|
230 |
-
# The `process_and_store_bubble_token` will run when `org_urn_display` (which is an output of app.load)
|
231 |
-
# receives its value.
|
232 |
org_urn_display.change(
|
233 |
fn=process_and_store_bubble_token,
|
234 |
inputs=[url_user_token_display, org_urn_display, token_state],
|
235 |
-
outputs=[status_box, token_state, sync_posts_to_bubble_btn]
|
236 |
)
|
237 |
-
# Fallback if url_user_token_display changes after org_urn_display (less likely but for robustness)
|
238 |
url_user_token_display.change(
|
239 |
fn=process_and_store_bubble_token,
|
240 |
inputs=[url_user_token_display, org_urn_display, token_state],
|
@@ -242,11 +293,15 @@ with gr.Blocks(theme=gr.themes.Soft(primary_hue="blue", secondary_hue="sky"),
|
|
242 |
)
|
243 |
|
244 |
sync_posts_to_bubble_btn.click(
|
245 |
-
fn=guarded_fetch_posts,
|
246 |
-
inputs=[token_state],
|
247 |
outputs=[dashboard_html_output]
|
|
|
|
|
|
|
|
|
248 |
)
|
249 |
-
|
250 |
with gr.TabItem("2οΈβ£ Analytics"):
|
251 |
gr.Markdown("View follower count and monthly gains for your organization (requires LinkedIn token).")
|
252 |
fetch_analytics_btn = gr.Button("π Fetch Follower Analytics", variant="primary")
|
@@ -281,10 +336,7 @@ with gr.Blocks(theme=gr.themes.Soft(primary_hue="blue", secondary_hue="sky"),
|
|
281 |
outputs=[mentions_html, mentions_plot]
|
282 |
)
|
283 |
|
284 |
-
# This app.load updates the status_box based on the initial token_state.
|
285 |
-
# The process_and_store_bubble_token function will provide a more definitive update soon after.
|
286 |
app.load(fn=lambda ts: check_token_status(ts), inputs=[token_state], outputs=status_box)
|
287 |
-
# Timer to periodically update the LinkedIn token status display
|
288 |
gr.Timer(15.0).tick(fn=lambda ts: check_token_status(ts), inputs=[token_state], outputs=status_box)
|
289 |
|
290 |
|
|
|
4 |
import os
|
5 |
import logging
|
6 |
import html
|
7 |
+
import pandas as pd # Ensure pandas is imported
|
8 |
+
from datetime import datetime # Used for pd.Timestamp
|
9 |
|
10 |
# Import functions from your custom modules
|
11 |
from Data_Fetching_and_Rendering import fetch_and_render_dashboard
|
|
|
16 |
from Bubble_API_Calls import (
|
17 |
fetch_linkedin_token_from_bubble,
|
18 |
bulk_upload_to_bubble,
|
19 |
+
fetch_linkedin_posts_data_from_bubble
|
20 |
)
|
21 |
from Linkedin_Data_API_Calls import (
|
22 |
fetch_linkedin_posts_core,
|
|
|
35 |
|
36 |
def process_and_store_bubble_token(url_user_token, org_urn, token_state):
|
37 |
"""
|
38 |
+
Processes user token, fetches LinkedIn token, fetches Bubble posts,
|
39 |
+
and determines if an initial fetch or update is needed for LinkedIn posts.
|
40 |
+
Updates token state and UI for the sync button.
|
41 |
"""
|
42 |
logging.info(f"Processing token with URL user token: '{url_user_token}', Org URN: '{org_urn}'")
|
43 |
|
44 |
+
new_state = token_state.copy() if token_state else {
|
45 |
+
"token": None, "client_id": None, "org_urn": None,
|
46 |
+
"bubble_posts_df": None, "fetch_count_for_api": 0
|
47 |
+
}
|
48 |
+
new_state.update({"org_urn": org_urn, "bubble_posts_df": new_state.get("bubble_posts_df"), "fetch_count_for_api": new_state.get("fetch_count_for_api", 0)})
|
49 |
|
50 |
+
# Default button update: hidden and non-interactive
|
51 |
+
button_update = gr.update(visible=False, interactive=False, value="π Sync LinkedIn Posts")
|
|
|
52 |
|
53 |
client_id = os.environ.get("Linkedin_client_id")
|
54 |
if not client_id:
|
|
|
57 |
else:
|
58 |
new_state["client_id"] = client_id
|
59 |
|
|
|
60 |
if url_user_token and "not found" not in url_user_token and "Could not access" not in url_user_token:
|
61 |
logging.info(f"Attempting to fetch LinkedIn token from Bubble with user token: {url_user_token}")
|
62 |
try:
|
63 |
parsed_linkedin_token = fetch_linkedin_token_from_bubble(url_user_token)
|
64 |
if isinstance(parsed_linkedin_token, dict) and "access_token" in parsed_linkedin_token:
|
65 |
+
new_state["token"] = parsed_linkedin_token
|
66 |
logging.info("β
LinkedIn Token successfully fetched from Bubble.")
|
67 |
else:
|
68 |
+
new_state["token"] = None
|
69 |
logging.warning(f"β Failed to fetch a valid LinkedIn token from Bubble. Response: {parsed_linkedin_token}")
|
70 |
except Exception as e:
|
71 |
+
new_state["token"] = None
|
72 |
logging.error(f"β Exception while fetching LinkedIn token from Bubble: {e}")
|
73 |
else:
|
74 |
+
new_state["token"] = None
|
75 |
logging.info("No valid URL user token provided for LinkedIn token fetch, or an error was indicated.")
|
76 |
|
77 |
+
# Fetch posts from Bubble
|
78 |
current_org_urn = new_state.get("org_urn")
|
79 |
+
bubble_posts_df = None
|
80 |
if current_org_urn:
|
81 |
logging.info(f"Attempting to fetch posts from Bubble for org_urn: {current_org_urn}")
|
82 |
try:
|
83 |
+
fetched_df, error_message = fetch_linkedin_posts_data_from_bubble(current_org_urn, "LI_posts")
|
84 |
+
if error_message:
|
85 |
+
logging.warning(f"Error reported by fetch_linkedin_posts_data_from_bubble: {error_message}. Treating as no data.")
|
|
|
|
|
|
|
|
|
|
|
|
|
86 |
else:
|
87 |
+
bubble_posts_df = fetched_df
|
88 |
+
new_state["bubble_posts_df"] = bubble_posts_df
|
89 |
except Exception as e:
|
90 |
+
logging.error(f"β Error fetching posts from Bubble: {e}. Treating as no data.")
|
91 |
+
new_state["bubble_posts_df"] = None # Ensure it's None on error
|
92 |
else:
|
93 |
logging.warning("Org URN not available in state. Cannot fetch posts from Bubble.")
|
94 |
+
|
95 |
+
# Logic for determining fetch/update based on bubble_posts_df
|
96 |
+
# DATE_COLUMN_NAME is now 'published_at' and contains ISO datetime strings.
|
97 |
+
DATE_COLUMN_NAME = 'published_at'
|
98 |
+
DEFAULT_INITIAL_FETCH_COUNT = 100 # Standard number of posts for initial fetch
|
99 |
+
|
100 |
+
if new_state["bubble_posts_df"] is None or new_state["bubble_posts_df"].empty:
|
101 |
+
logging.info(f"βΉοΈ No posts found in Bubble or DataFrame is empty. Button to fetch initial {DEFAULT_INITIAL_FETCH_COUNT} posts will be visible.")
|
102 |
+
new_state['fetch_count_for_api'] = DEFAULT_INITIAL_FETCH_COUNT
|
103 |
+
button_update = gr.update(value=f"π Fetch Initial {DEFAULT_INITIAL_FETCH_COUNT} LinkedIn Posts", visible=True, interactive=True)
|
104 |
+
else:
|
105 |
+
try:
|
106 |
+
df_for_date_check = new_state["bubble_posts_df"].copy() # Use a copy to avoid SettingWithCopyWarning
|
107 |
+
if DATE_COLUMN_NAME not in df_for_date_check.columns:
|
108 |
+
logging.warning(f"Date column '{DATE_COLUMN_NAME}' not found in Bubble posts DataFrame. Assuming initial fetch of {DEFAULT_INITIAL_FETCH_COUNT} posts.")
|
109 |
+
new_state['fetch_count_for_api'] = DEFAULT_INITIAL_FETCH_COUNT
|
110 |
+
button_update = gr.update(value=f"π Fetch Initial {DEFAULT_INITIAL_FETCH_COUNT} (Date Column Missing)", visible=True, interactive=True)
|
111 |
+
elif df_for_date_check[DATE_COLUMN_NAME].isnull().all():
|
112 |
+
logging.warning(f"Date column '{DATE_COLUMN_NAME}' contains all null values. Assuming initial fetch of {DEFAULT_INITIAL_FETCH_COUNT} posts.")
|
113 |
+
new_state['fetch_count_for_api'] = DEFAULT_INITIAL_FETCH_COUNT
|
114 |
+
button_update = gr.update(value=f"π Fetch Initial {DEFAULT_INITIAL_FETCH_COUNT} (Date Column Empty)", visible=True, interactive=True)
|
115 |
+
else:
|
116 |
+
# Convert ISO datetime strings to datetime objects
|
117 |
+
df_for_date_check[DATE_COLUMN_NAME] = pd.to_datetime(df_for_date_check[DATE_COLUMN_NAME], errors='coerce', utc=True)
|
118 |
+
last_post_date_utc = df_for_date_check[DATE_COLUMN_NAME].dropna().max()
|
119 |
+
|
120 |
+
if pd.isna(last_post_date_utc):
|
121 |
+
logging.warning(f"No valid dates found in '{DATE_COLUMN_NAME}' after conversion. Assuming initial fetch of {DEFAULT_INITIAL_FETCH_COUNT} posts.")
|
122 |
+
new_state['fetch_count_for_api'] = DEFAULT_INITIAL_FETCH_COUNT
|
123 |
+
button_update = gr.update(value=f"π Fetch Initial {DEFAULT_INITIAL_FETCH_COUNT} (No Valid Dates)", visible=True, interactive=True)
|
124 |
+
else:
|
125 |
+
today_utc = pd.Timestamp('now', tz='UTC').normalize()
|
126 |
+
last_post_date_utc_normalized = last_post_date_utc.normalize()
|
127 |
+
|
128 |
+
time_difference_days = (today_utc - last_post_date_utc_normalized).days
|
129 |
+
logging.info(f"Last post date (UTC, normalized): {last_post_date_utc_normalized}, Today (UTC, normalized): {today_utc}, Difference: {time_difference_days} days.")
|
130 |
+
|
131 |
+
if time_difference_days >= 7:
|
132 |
+
num_weeks = max(1, time_difference_days // 7)
|
133 |
+
fetch_count = num_weeks * 10
|
134 |
+
new_state['fetch_count_for_api'] = fetch_count
|
135 |
+
button_label = f"π Update Last {num_weeks} Week(s) (~{fetch_count} Posts)"
|
136 |
+
logging.info(f"Data is {time_difference_days} days old. Update needed for {num_weeks} weeks, ~{fetch_count} posts.")
|
137 |
+
button_update = gr.update(value=button_label, visible=True, interactive=True)
|
138 |
+
else:
|
139 |
+
logging.info(f"Data is fresh ({time_difference_days} days old). No update needed now.")
|
140 |
+
new_state['fetch_count_for_api'] = 0
|
141 |
+
button_update = gr.update(visible=False, interactive=False)
|
142 |
+
except Exception as e:
|
143 |
+
logging.error(f"Error processing dates from Bubble posts: {e}. Defaulting to initial fetch of {DEFAULT_INITIAL_FETCH_COUNT} posts.")
|
144 |
+
new_state['fetch_count_for_api'] = DEFAULT_INITIAL_FETCH_COUNT
|
145 |
+
button_update = gr.update(value=f"π Fetch Initial {DEFAULT_INITIAL_FETCH_COUNT} (Date Error)", visible=True, interactive=True)
|
146 |
+
|
147 |
+
token_status_message = check_token_status(new_state)
|
148 |
+
logging.info(f"Token processing complete. LinkedIn Token Status: {token_status_message}. Button update: {button_update}. Fetch count for API: {new_state['fetch_count_for_api']}")
|
149 |
+
return token_status_message, new_state, button_update
|
150 |
|
151 |
def guarded_fetch_posts(token_state):
|
152 |
"""
|
153 |
+
Fetches LinkedIn posts based on 'fetch_count_for_api' in token_state,
|
154 |
+
analyzes them, and uploads to Bubble.
|
155 |
"""
|
156 |
logging.info("Starting guarded_fetch_posts process.")
|
157 |
+
if not token_state or not token_state.get("token"):
|
158 |
logging.error("Access denied for guarded_fetch_posts. No LinkedIn token available.")
|
159 |
+
return "<p style='color:red; text-align:center;'>β Access denied. LinkedIn token not available.</p>"
|
160 |
|
161 |
client_id = token_state.get("client_id")
|
162 |
+
token_dict = token_state.get("token")
|
163 |
org_urn = token_state.get('org_urn')
|
164 |
+
fetch_count_value = token_state.get('fetch_count_for_api')
|
165 |
|
166 |
if not org_urn:
|
167 |
logging.error("Organization URN (org_urn) not found in token_state for guarded_fetch_posts.")
|
168 |
return "<p style='color:red; text-align:center;'>β Configuration error: Organization URN missing.</p>"
|
169 |
if not client_id or client_id == "ENV VAR MISSING":
|
170 |
logging.error("Client ID not found or missing in token_state for guarded_fetch_posts.")
|
171 |
+
return "<p style='color:red; text-align:center;'>β Configuration error: LinkedIn Client ID missing.</p>"
|
172 |
|
173 |
+
if fetch_count_value == 0:
|
174 |
+
logging.info("guarded_fetch_posts called, but fetch_count_for_api is 0. Data is fresh.")
|
175 |
+
return "<p style='color:green; text-align:center;'>β
Data is already up-to-date. No new posts fetched.</p>"
|
176 |
+
|
177 |
+
if fetch_count_value is None: # Should ideally not happen with new logic, but as a safeguard
|
178 |
+
logging.warning("fetch_count_for_api is None in guarded_fetch_posts. This might indicate an issue. Defaulting to fetching a standard amount if your API supports it or all.")
|
179 |
+
# Depending on your API, None might mean fetch all or a default.
|
180 |
+
# If your API requires a specific count for "all", you might need to adjust here or in fetch_linkedin_posts_core.
|
181 |
|
182 |
try:
|
183 |
+
logging.info(f"Step 1: Fetching core posts for org_urn: {org_urn}. Fetch count parameter for API: {fetch_count_value}")
|
184 |
+
processed_raw_posts, stats_map, _ = fetch_linkedin_posts_core(client_id, token_dict, org_urn, count=fetch_count_value)
|
185 |
|
186 |
if not processed_raw_posts:
|
187 |
logging.info("No posts found to process via LinkedIn API after step 1.")
|
|
|
207 |
bulk_upload_to_bubble(li_post_stats, "LI_post_stats")
|
208 |
bulk_upload_to_bubble(li_post_comments, "LI_post_comments")
|
209 |
|
210 |
+
action_performed = f"Initial data fetch (~{fetch_count_value} posts)" if fetch_count_value == DEFAULT_INITIAL_FETCH_COUNT else f"Data update (target: ~{fetch_count_value} posts)"
|
211 |
+
logging.info(f"Successfully completed: {action_performed}. Uploaded posts and comments to Bubble.")
|
212 |
+
return f"<p style='color:green; text-align:center;'>β
{action_performed} complete. Posts and comments from LinkedIn uploaded to Bubble.</p>"
|
213 |
|
214 |
except ValueError as ve:
|
215 |
logging.error(f"ValueError during LinkedIn data processing: {ve}")
|
216 |
return f"<p style='color:red; text-align:center;'>β Error: {html.escape(str(ve))}</p>"
|
217 |
except Exception as e:
|
218 |
+
logging.exception("An unexpected error occurred in guarded_fetch_posts.")
|
219 |
+
return "<p style='color:red; text-align:center;'>β An unexpected error occurred. Please check logs.</p>"
|
220 |
|
221 |
def guarded_fetch_dashboard(token_state):
|
|
|
222 |
if not token_state or not token_state.get("token"):
|
223 |
return "β Access denied. No token available for dashboard."
|
224 |
+
if token_state.get("bubble_posts_df") is not None and not token_state["bubble_posts_df"].empty:
|
225 |
+
return f"<p style='text-align: center;'>Dashboard would show {len(token_state['bubble_posts_df'])} posts from Bubble.</p>"
|
226 |
+
else:
|
227 |
+
return "<p style='text-align: center; color: #555;'>No posts loaded from Bubble yet for the dashboard.</p>"
|
228 |
|
229 |
|
230 |
def guarded_fetch_analytics(token_state):
|
|
|
231 |
if not token_state or not token_state.get("token"):
|
232 |
return ("β Access denied. No token available for analytics.",
|
233 |
None, None, None, None, None, None, None)
|
234 |
return fetch_and_render_analytics(token_state.get("client_id"), token_state.get("token"))
|
235 |
|
236 |
def run_mentions_and_load(token_state):
|
|
|
237 |
if not token_state or not token_state.get("token"):
|
238 |
return ("β Access denied. No token available for mentions.", None)
|
239 |
return generate_mentions_dashboard(token_state.get("client_id"), token_state.get("token"))
|
|
|
242 |
with gr.Blocks(theme=gr.themes.Soft(primary_hue="blue", secondary_hue="sky"),
|
243 |
title="LinkedIn Post Viewer & Analytics") as app:
|
244 |
|
245 |
+
# Define DEFAULT_INITIAL_FETCH_COUNT here if needed by guarded_fetch_posts for its messages,
|
246 |
+
# or ensure it's passed/accessible if logic depends on it there.
|
247 |
+
# For now, it's only used within process_and_store_bubble_token.
|
248 |
+
DEFAULT_INITIAL_FETCH_COUNT = 100
|
249 |
+
|
250 |
+
|
251 |
+
token_state = gr.State(value={
|
252 |
+
"token": None,
|
253 |
+
"client_id": None,
|
254 |
+
"org_urn": None,
|
255 |
+
"bubble_posts_df": None,
|
256 |
+
"fetch_count_for_api": 0
|
257 |
+
})
|
258 |
|
259 |
gr.Markdown("# π LinkedIn Organization Post Viewer & Analytics")
|
260 |
gr.Markdown("Token is supplied via URL parameter for Bubble.io lookup. Then explore dashboard and analytics.")
|
261 |
|
262 |
url_user_token_display = gr.Textbox(label="User Token (from URL - Hidden)", interactive=False, visible=False)
|
263 |
+
status_box = gr.Textbox(label="Overall LinkedIn Token Status", interactive=False, value="Initializing...")
|
264 |
org_urn_display = gr.Textbox(label="Organization URN (from URL - Hidden)", interactive=False, visible=False)
|
265 |
|
266 |
app.load(fn=get_url_user_token, inputs=None, outputs=[url_user_token_display, org_urn_display])
|
267 |
|
268 |
with gr.Tabs():
|
269 |
with gr.TabItem("1οΈβ£ Dashboard & Sync"):
|
270 |
+
gr.Markdown("System checks for existing data in Bubble. The button below will activate if new posts need to be fetched or updated from LinkedIn.")
|
271 |
|
272 |
sync_posts_to_bubble_btn = gr.Button(
|
273 |
+
value="π Sync LinkedIn Posts",
|
274 |
variant="primary",
|
275 |
+
visible=False,
|
276 |
interactive=False
|
277 |
)
|
278 |
|
279 |
dashboard_html_output = gr.HTML(
|
280 |
"<p style='text-align: center; color: #555;'>System initializing... "
|
281 |
+
"Checking for existing data in Bubble and LinkedIn token.</p>"
|
282 |
)
|
283 |
|
|
|
|
|
|
|
|
|
284 |
org_urn_display.change(
|
285 |
fn=process_and_store_bubble_token,
|
286 |
inputs=[url_user_token_display, org_urn_display, token_state],
|
287 |
+
outputs=[status_box, token_state, sync_posts_to_bubble_btn]
|
288 |
)
|
|
|
289 |
url_user_token_display.change(
|
290 |
fn=process_and_store_bubble_token,
|
291 |
inputs=[url_user_token_display, org_urn_display, token_state],
|
|
|
293 |
)
|
294 |
|
295 |
sync_posts_to_bubble_btn.click(
|
296 |
+
fn=guarded_fetch_posts,
|
297 |
+
inputs=[token_state],
|
298 |
outputs=[dashboard_html_output]
|
299 |
+
).then(
|
300 |
+
fn=process_and_store_bubble_token,
|
301 |
+
inputs=[url_user_token_display, org_urn_display, token_state],
|
302 |
+
outputs=[status_box, token_state, sync_posts_to_bubble_btn]
|
303 |
)
|
304 |
+
|
305 |
with gr.TabItem("2οΈβ£ Analytics"):
|
306 |
gr.Markdown("View follower count and monthly gains for your organization (requires LinkedIn token).")
|
307 |
fetch_analytics_btn = gr.Button("π Fetch Follower Analytics", variant="primary")
|
|
|
336 |
outputs=[mentions_html, mentions_plot]
|
337 |
)
|
338 |
|
|
|
|
|
339 |
app.load(fn=lambda ts: check_token_status(ts), inputs=[token_state], outputs=status_box)
|
|
|
340 |
gr.Timer(15.0).tick(fn=lambda ts: check_token_status(ts), inputs=[token_state], outputs=status_box)
|
341 |
|
342 |
|