LinkedinMonitor / app.py
GuglielmoTor's picture
Update app.py
575b933 verified
raw
history blame
10.5 kB
# app.py
# -- coding: utf-8 --
import gradio as gr
import pandas as pd
import os
import logging
# --- Module Imports ---
# Functions from your existing/provided custom modules
from analytics_fetch_and_rendering import fetch_and_render_analytics # Assuming this exists
from gradio_utils import get_url_user_token # For fetching URL parameters
# Functions from newly created/refactored modules
from config import (
LINKEDIN_CLIENT_ID_ENV_VAR, BUBBLE_APP_NAME_ENV_VAR,
BUBBLE_API_KEY_PRIVATE_ENV_VAR, BUBBLE_API_ENDPOINT_ENV_VAR
)
from state_manager import process_and_store_bubble_token
from sync_logic import sync_all_linkedin_data_orchestrator
from ui_generators import (
display_main_dashboard,
run_mentions_tab_display,
run_follower_stats_tab_display
)
# Configure logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
# --- Guarded Analytics Fetch ---
def guarded_fetch_analytics(token_state):
"""Guarded call to fetch_and_render_analytics, ensuring token and basic data structures."""
if not token_state or not token_state.get("token"):
logging.warning("Analytics fetch: Access denied. No token.")
# Ensure the number of returned Nones matches the expected number of outputs for the plots
return ("❌ Access denied. No token.", None, None, None, None, None, None, None)
# Ensure DataFrames are passed, even if empty, to avoid errors in the analytics function
posts_df_analytics = token_state.get("bubble_posts_df", pd.DataFrame())
mentions_df_analytics = token_state.get("bubble_mentions_df", pd.DataFrame())
follower_stats_df_analytics = token_state.get("bubble_follower_stats_df", pd.DataFrame())
logging.info("Calling fetch_and_render_analytics with current token_state data.")
try:
return fetch_and_render_analytics(
token_state.get("client_id"),
token_state.get("token"),
token_state.get("org_urn"),
posts_df_analytics,
mentions_df_analytics,
follower_stats_df_analytics
)
except Exception as e:
logging.error(f"Error in guarded_fetch_analytics calling fetch_and_render_analytics: {e}", exc_info=True)
return (f"❌ Error fetching analytics: {e}", None, None, None, None, None, None, None)
# --- Gradio UI Blocks ---
with gr.Blocks(theme=gr.themes.Soft(primary_hue="blue", secondary_hue="sky"),
title="LinkedIn Organization Dashboard") as app:
# Central state for holding token, client_id, org_urn, and fetched dataframes
token_state = gr.State(value={
"token": None, "client_id": None, "org_urn": None,
"bubble_posts_df": pd.DataFrame(), "fetch_count_for_api": 0,
"bubble_mentions_df": pd.DataFrame(),
"bubble_follower_stats_df": pd.DataFrame(),
"url_user_token_temp_storage": None
})
gr.Markdown("# πŸš€ LinkedIn Organization Dashboard")
# Hidden textboxes to capture URL parameters
url_user_token_display = gr.Textbox(label="User Token (from URL - Hidden)", interactive=False, visible=False)
status_box = gr.Textbox(label="Overall LinkedIn Token Status", interactive=False, value="Initializing...")
org_urn_display = gr.Textbox(label="Organization URN (from URL - Hidden)", interactive=False, visible=False)
# Load URL parameters when the Gradio app loads
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)
# This function will run after URL params are loaded and org_urn_display changes
def initial_load_sequence(url_token, org_urn_val, current_state):
logging.info(f"Initial load sequence triggered. Org URN: {org_urn_val}, URL Token: {'Present' if url_token else 'Absent'}")
# Process token, fetch Bubble data, determine sync needs
status_msg, new_state, btn_update = process_and_store_bubble_token(url_token, org_urn_val, current_state)
# Display initial dashboard content based on (potentially empty) Bubble data
dashboard_content = display_main_dashboard(new_state)
return status_msg, new_state, btn_update, dashboard_content
with gr.Tabs():
with gr.TabItem("1️⃣ Dashboard & Sync"):
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.")
sync_data_btn = gr.Button("πŸ”„ Sync LinkedIn Data", variant="primary", visible=False, interactive=False)
sync_status_html_output = gr.HTML("<p style='text-align:center;'>Sync status will appear here.</p>")
dashboard_display_html = gr.HTML("<p style='text-align:center;'>Dashboard loading...</p>")
# Chain of events for initial load:
org_urn_display.change(
fn=initial_load_sequence,
inputs=[url_user_token_display, org_urn_display, token_state],
outputs=[status_box, token_state, sync_data_btn, dashboard_display_html],
show_progress="full"
)
# Also trigger initial_load_sequence if url_user_token_display changes (e.g. if it loads after org_urn)
# This helps ensure it runs once both are potentially available.
# Note: `org_urn_display.change` might be sufficient if `get_url_user_token` updates both nearly simultaneously.
# Adding this for robustness, but ensure it doesn't cause unwanted multiple runs if state isn't managed carefully.
# Consider using a flag in token_state if multiple triggers become an issue.
# For now, relying on org_urn_display.change as the primary trigger post-load.
# When Sync button is clicked:
sync_data_btn.click(
fn=sync_all_linkedin_data_orchestrator,
inputs=[token_state],
outputs=[sync_status_html_output, token_state], # token_state is updated here
show_progress="full"
).then(
fn=process_and_store_bubble_token, # Re-check sync status and update button
inputs=[url_user_token_display, org_urn_display, token_state], # Pass current token_state
outputs=[status_box, token_state, sync_data_btn], # token_state updated again
show_progress=False # Typically "full" for user-initiated actions, "minimal" or False for quick updates
).then(
fn=display_main_dashboard, # Refresh dashboard display
inputs=[token_state],
outputs=[dashboard_display_html],
show_progress=False
)
with gr.TabItem("2️⃣ Analytics"):
fetch_analytics_btn = gr.Button("πŸ“ˆ Fetch/Refresh Full Analytics", variant="primary")
follower_count_md = gr.Markdown("Analytics data will load here...")
with gr.Row(): follower_plot, growth_plot = gr.Plot(label="Follower Demographics"), gr.Plot(label="Follower Growth")
with gr.Row(): eng_rate_plot = gr.Plot(label="Engagement Rate")
with gr.Row(): interaction_plot = gr.Plot(label="Post Interactions")
with gr.Row(): eb_plot = gr.Plot(label="Engagement Benchmark")
with gr.Row(): mentions_vol_plot, mentions_sentiment_plot = gr.Plot(label="Mentions Volume"), gr.Plot(label="Mentions Sentiment")
fetch_analytics_btn.click(
fn=guarded_fetch_analytics, inputs=[token_state],
outputs=[follower_count_md, follower_plot, growth_plot, eng_rate_plot,
interaction_plot, eb_plot, mentions_vol_plot, mentions_sentiment_plot],
show_progress="full"
)
with gr.TabItem("3️⃣ Mentions"):
refresh_mentions_display_btn = gr.Button("πŸ”„ Refresh Mentions Display (from local data)", variant="secondary")
mentions_html = gr.HTML("Mentions data loads from Bubble after sync. Click refresh to view current local data.")
mentions_sentiment_dist_plot = gr.Plot(label="Mention Sentiment Distribution")
refresh_mentions_display_btn.click(
fn=run_mentions_tab_display, inputs=[token_state],
outputs=[mentions_html, mentions_sentiment_dist_plot],
show_progress="full"
)
with gr.TabItem("4️⃣ Follower Stats"):
refresh_follower_stats_btn = gr.Button("πŸ”„ Refresh Follower Stats Display (from local data)", variant="secondary")
follower_stats_html = gr.HTML("Follower statistics load from Bubble after sync. Click refresh to view current local data.")
with gr.Row():
fs_plot_monthly_gains = gr.Plot(label="Monthly Follower Gains")
with gr.Row():
fs_plot_seniority = gr.Plot(label="Followers by Seniority (Top 10 Organic)")
fs_plot_industry = gr.Plot(label="Followers by Industry (Top 10 Organic)")
refresh_follower_stats_btn.click(
fn=run_follower_stats_tab_display, inputs=[token_state],
outputs=[follower_stats_html, fs_plot_monthly_gains, fs_plot_seniority, fs_plot_industry],
show_progress="full"
)
if __name__ == "__main__":
# Check for essential environment variables
if not os.environ.get(LINKEDIN_CLIENT_ID_ENV_VAR):
logging.warning(f"WARNING: '{LINKEDIN_CLIENT_ID_ENV_VAR}' environment variable not set. The app may not function correctly for LinkedIn API calls.")
if not os.environ.get(BUBBLE_APP_NAME_ENV_VAR) or \
not os.environ.get(BUBBLE_API_KEY_PRIVATE_ENV_VAR) or \
not os.environ.get(BUBBLE_API_ENDPOINT_ENV_VAR):
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.")
try:
import matplotlib
logging.info(f"Matplotlib version: {matplotlib.__version__} found. Backend: {matplotlib.get_backend()}")
# The backend is now set in ui_generators.py, which is good practice.
except ImportError:
logging.error("Matplotlib is not installed. Plots will not be generated. Please install it: pip install matplotlib")
# Launch the Gradio app
app.launch(server_name="0.0.0.0", server_port=7860, debug=True)