Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
| 1 |
import os
|
| 2 |
import time
|
| 3 |
import requests
|
|
|
|
| 4 |
import pandas as pd
|
| 5 |
import gradio as gr
|
| 6 |
import plotly.express as px
|
|
@@ -13,11 +14,11 @@ API_TOKEN = os.getenv("LEADFEEDER_API_TOKEN")
|
|
| 13 |
APP_PASSWORD = os.getenv("APP_PASSWORD")
|
| 14 |
|
| 15 |
if not API_TOKEN or not APP_PASSWORD:
|
| 16 |
-
raise RuntimeError("Missing required secrets
|
| 17 |
|
| 18 |
-
ACCOUNT_ID = "255333"
|
| 19 |
BASE_URL = "https://api.leadfeeder.com"
|
| 20 |
-
PAGE_SIZE = 50
|
| 21 |
|
| 22 |
HEADERS = {
|
| 23 |
"Authorization": f"Token token={API_TOKEN}",
|
|
@@ -25,42 +26,30 @@ HEADERS = {
|
|
| 25 |
}
|
| 26 |
|
| 27 |
# =====================================================
|
| 28 |
-
# UTILS
|
| 29 |
# =====================================================
|
| 30 |
def make_request(url, params=None):
|
| 31 |
-
"""
|
| 32 |
-
Wrapper to handle Leadfeeder's 100 req/min limit.
|
| 33 |
-
If we hit 429 (Too Many Requests), we sleep and retry.
|
| 34 |
-
"""
|
| 35 |
retries = 3
|
| 36 |
while retries > 0:
|
| 37 |
-
r = requests.get(url, headers=HEADERS, params=params, timeout=
|
| 38 |
-
|
| 39 |
if r.status_code == 429:
|
| 40 |
-
print("⏳ Rate limit hit. Sleeping for 60 seconds...")
|
| 41 |
time.sleep(61)
|
| 42 |
retries -= 1
|
| 43 |
continue
|
| 44 |
-
|
| 45 |
r.raise_for_status()
|
| 46 |
return r.json()
|
| 47 |
-
raise Exception("Max retries exceeded
|
| 48 |
|
| 49 |
# =====================================================
|
| 50 |
# DATA FETCHING
|
| 51 |
# =====================================================
|
| 52 |
-
def fetch_basic_leads(start_date, end_date
|
| 53 |
-
"""
|
| 54 |
-
Step 1: Get the list of companies (Fast, but missing Landing Page)
|
| 55 |
-
"""
|
| 56 |
page = 1
|
| 57 |
rows = []
|
| 58 |
|
| 59 |
-
print(f"🚀 Fetching basic lead list for {start_date} to {end_date}...")
|
| 60 |
-
|
| 61 |
while True:
|
| 62 |
try:
|
| 63 |
-
# We only ask for basic info here to be fast
|
| 64 |
js = make_request(
|
| 65 |
f"{BASE_URL}/accounts/{ACCOUNT_ID}/leads",
|
| 66 |
params={
|
|
@@ -71,12 +60,9 @@ def fetch_basic_leads(start_date, end_date, progress=gr.Progress()):
|
|
| 71 |
"include": "location"
|
| 72 |
}
|
| 73 |
)
|
| 74 |
-
|
| 75 |
data = js.get("data", [])
|
| 76 |
-
if not data:
|
| 77 |
-
break
|
| 78 |
|
| 79 |
-
# Map Included Locations
|
| 80 |
included = js.get("included", [])
|
| 81 |
loc_map = {str(i["id"]): i["attributes"] for i in included if i["type"] == "locations"}
|
| 82 |
|
|
@@ -88,27 +74,21 @@ def fetch_basic_leads(start_date, end_date, progress=gr.Progress()):
|
|
| 88 |
rows.append({
|
| 89 |
"lead_id": lead.get("id"),
|
| 90 |
"company_name": a.get("name"),
|
| 91 |
-
"website_url": a.get("website_url"),
|
| 92 |
"primary_industry": a.get("industry"),
|
| 93 |
"total_visits": a.get("visits"),
|
| 94 |
"lead_quality_score": a.get("quality"),
|
| 95 |
"last_visit_date": a.get("last_visit_date"),
|
| 96 |
"country": loc.get("country"),
|
| 97 |
"city": loc.get("city"),
|
| 98 |
-
"region": loc.get("region"),
|
| 99 |
"revenue": a.get("revenue"),
|
| 100 |
"employee_count": a.get("employee_count"),
|
| 101 |
"assignee": a.get("assignee"),
|
| 102 |
"crm_organization_id": a.get("crm_organization_id"),
|
| 103 |
"linkedin_url": a.get("linkedin_url"),
|
| 104 |
-
|
| 105 |
-
"
|
| 106 |
-
"exit_page_path": None
|
| 107 |
})
|
| 108 |
-
|
| 109 |
-
print(f"✅ Page {page} loaded. Total leads: {len(rows)}")
|
| 110 |
page += 1
|
| 111 |
-
|
| 112 |
except Exception as e:
|
| 113 |
print(f"Error on page {page}: {e}")
|
| 114 |
break
|
|
@@ -116,150 +96,130 @@ def fetch_basic_leads(start_date, end_date, progress=gr.Progress()):
|
|
| 116 |
return pd.DataFrame(rows)
|
| 117 |
|
| 118 |
def enrich_leads_with_visits(df, max_rows=None, progress=gr.Progress()):
|
| 119 |
-
"""
|
| 120 |
-
|
| 121 |
-
This is slow but GUARANTEES the landing page data.
|
| 122 |
-
"""
|
| 123 |
-
if df.empty:
|
| 124 |
-
return df
|
| 125 |
|
| 126 |
-
# If max_rows is set (e.g. for Preview), we only process top N rows
|
| 127 |
target_df = df.head(max_rows) if max_rows else df
|
| 128 |
total = len(target_df)
|
| 129 |
|
| 130 |
-
print(f"🕵️
|
| 131 |
|
| 132 |
-
# We iterate and update the dataframe
|
| 133 |
for index, row in target_df.iterrows():
|
| 134 |
lead_id = row["lead_id"]
|
| 135 |
-
|
| 136 |
try:
|
| 137 |
-
#
|
| 138 |
-
# This endpoint returns the detailed visit history including page views
|
| 139 |
visit_data = make_request(
|
| 140 |
f"{BASE_URL}/accounts/{ACCOUNT_ID}/leads/{lead_id}/visits",
|
| 141 |
-
params={"page[size]": 1
|
| 142 |
)
|
| 143 |
|
| 144 |
visits = visit_data.get("data", [])
|
|
|
|
|
|
|
| 145 |
if visits:
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
latest_visit = visits[0]["attributes"]
|
| 149 |
|
| 150 |
-
#
|
| 151 |
-
landing =
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 158 |
|
| 159 |
-
# Update the main DataFrame
|
| 160 |
df.at[index, "landing_page_path"] = landing
|
| 161 |
df.at[index, "exit_page_path"] = exit_p
|
| 162 |
|
| 163 |
except Exception as e:
|
| 164 |
-
print(f"Failed
|
| 165 |
|
| 166 |
-
# Update progress bar every 5 rows
|
| 167 |
if index % 5 == 0:
|
| 168 |
-
progress(index / total, desc=
|
| 169 |
|
| 170 |
return df
|
| 171 |
|
| 172 |
# =====================================================
|
| 173 |
-
#
|
| 174 |
# =====================================================
|
| 175 |
-
def
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
df_preview = enrich_leads_with_visits(df_preview, max_rows=10)
|
| 185 |
-
|
| 186 |
-
status = f"✅ Loaded **{len(df):,} companies**. Preview shows top 10 rows enriched with Landing Pages."
|
| 187 |
-
return df, df_preview.head(20), status
|
| 188 |
-
|
| 189 |
-
def download_full_excel(df):
|
| 190 |
-
if df is None or df.empty:
|
| 191 |
-
return None
|
| 192 |
-
|
| 193 |
-
# When downloading, we must enrich ALL rows (this takes time)
|
| 194 |
-
# Note: In a real web app, this should be a background job.
|
| 195 |
-
# Here we do it inline with a progress bar.
|
| 196 |
-
print("⏳ Starting full enrichment for Excel export...")
|
| 197 |
-
enriched_df = enrich_leads_with_visits(df.copy()) # Process ALL rows
|
| 198 |
-
|
| 199 |
-
path = "/tmp/leadfeeder_company_data_full.xlsx"
|
| 200 |
-
enriched_df.to_excel(path, index=False)
|
| 201 |
-
return path
|
| 202 |
|
| 203 |
# =====================================================
|
| 204 |
-
#
|
| 205 |
# =====================================================
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
else: grouped = df.groupby(dim, dropna=False)[metric].agg(agg).reset_index(name="value")
|
| 222 |
-
return px.bar(grouped.sort_values("value", ascending=False).head(top_n), x=dim, y="value", title=preset)
|
| 223 |
|
| 224 |
# =====================================================
|
| 225 |
-
# UI
|
| 226 |
# =====================================================
|
| 227 |
-
with gr.Blocks(title="Leadfeeder Analytics") as demo:
|
| 228 |
-
gr.Markdown("##
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
|
|
|
|
|
|
|
| 232 |
df_state = gr.State()
|
| 233 |
-
|
| 234 |
-
|
| 235 |
with gr.Tabs():
|
| 236 |
-
with gr.Tab("📋 Data"):
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
end = gr.Textbox(label="End", value=date.today().isoformat())
|
| 240 |
-
|
| 241 |
-
load_btn = gr.Button("Load Data (Preview)", variant="primary")
|
| 242 |
|
| 243 |
with gr.Row():
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
| 247 |
|
| 248 |
-
|
|
|
|
| 249 |
|
| 250 |
-
|
| 251 |
-
|
| 252 |
|
| 253 |
-
with gr.Tab("
|
| 254 |
-
|
| 255 |
-
gr.
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
top_n = gr.Slider(5, 50, value=10, label="Top N")
|
| 260 |
-
|
| 261 |
-
chart = gr.Plot()
|
| 262 |
-
gr.Button("Build").click(build_dashboard, [df_state, preset, top_n], chart)
|
| 263 |
|
| 264 |
if __name__ == "__main__":
|
| 265 |
-
demo.launch(server_name="0.0.0.0", server_port=7860
|
|
|
|
| 1 |
import os
|
| 2 |
import time
|
| 3 |
import requests
|
| 4 |
+
import json
|
| 5 |
import pandas as pd
|
| 6 |
import gradio as gr
|
| 7 |
import plotly.express as px
|
|
|
|
| 14 |
APP_PASSWORD = os.getenv("APP_PASSWORD")
|
| 15 |
|
| 16 |
if not API_TOKEN or not APP_PASSWORD:
|
| 17 |
+
raise RuntimeError("Missing required secrets")
|
| 18 |
|
| 19 |
+
ACCOUNT_ID = "255333"
|
| 20 |
BASE_URL = "https://api.leadfeeder.com"
|
| 21 |
+
PAGE_SIZE = 50
|
| 22 |
|
| 23 |
HEADERS = {
|
| 24 |
"Authorization": f"Token token={API_TOKEN}",
|
|
|
|
| 26 |
}
|
| 27 |
|
| 28 |
# =====================================================
|
| 29 |
+
# UTILS
|
| 30 |
# =====================================================
|
| 31 |
def make_request(url, params=None):
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
retries = 3
|
| 33 |
while retries > 0:
|
| 34 |
+
r = requests.get(url, headers=HEADERS, params=params, timeout=45)
|
|
|
|
| 35 |
if r.status_code == 429:
|
|
|
|
| 36 |
time.sleep(61)
|
| 37 |
retries -= 1
|
| 38 |
continue
|
|
|
|
| 39 |
r.raise_for_status()
|
| 40 |
return r.json()
|
| 41 |
+
raise Exception("Max retries exceeded")
|
| 42 |
|
| 43 |
# =====================================================
|
| 44 |
# DATA FETCHING
|
| 45 |
# =====================================================
|
| 46 |
+
def fetch_basic_leads(start_date, end_date):
|
| 47 |
+
""" Step 1: Fast fetch of companies (no deep visit data yet) """
|
|
|
|
|
|
|
| 48 |
page = 1
|
| 49 |
rows = []
|
| 50 |
|
|
|
|
|
|
|
| 51 |
while True:
|
| 52 |
try:
|
|
|
|
| 53 |
js = make_request(
|
| 54 |
f"{BASE_URL}/accounts/{ACCOUNT_ID}/leads",
|
| 55 |
params={
|
|
|
|
| 60 |
"include": "location"
|
| 61 |
}
|
| 62 |
)
|
|
|
|
| 63 |
data = js.get("data", [])
|
| 64 |
+
if not data: break
|
|
|
|
| 65 |
|
|
|
|
| 66 |
included = js.get("included", [])
|
| 67 |
loc_map = {str(i["id"]): i["attributes"] for i in included if i["type"] == "locations"}
|
| 68 |
|
|
|
|
| 74 |
rows.append({
|
| 75 |
"lead_id": lead.get("id"),
|
| 76 |
"company_name": a.get("name"),
|
|
|
|
| 77 |
"primary_industry": a.get("industry"),
|
| 78 |
"total_visits": a.get("visits"),
|
| 79 |
"lead_quality_score": a.get("quality"),
|
| 80 |
"last_visit_date": a.get("last_visit_date"),
|
| 81 |
"country": loc.get("country"),
|
| 82 |
"city": loc.get("city"),
|
|
|
|
| 83 |
"revenue": a.get("revenue"),
|
| 84 |
"employee_count": a.get("employee_count"),
|
| 85 |
"assignee": a.get("assignee"),
|
| 86 |
"crm_organization_id": a.get("crm_organization_id"),
|
| 87 |
"linkedin_url": a.get("linkedin_url"),
|
| 88 |
+
"landing_page_path": None, # To be filled
|
| 89 |
+
"exit_page_path": None # To be filled
|
|
|
|
| 90 |
})
|
|
|
|
|
|
|
| 91 |
page += 1
|
|
|
|
| 92 |
except Exception as e:
|
| 93 |
print(f"Error on page {page}: {e}")
|
| 94 |
break
|
|
|
|
| 96 |
return pd.DataFrame(rows)
|
| 97 |
|
| 98 |
def enrich_leads_with_visits(df, max_rows=None, progress=gr.Progress()):
|
| 99 |
+
""" Step 2: Deep fetch using JSON:API relationships """
|
| 100 |
+
if df.empty: return df
|
|
|
|
|
|
|
|
|
|
|
|
|
| 101 |
|
|
|
|
| 102 |
target_df = df.head(max_rows) if max_rows else df
|
| 103 |
total = len(target_df)
|
| 104 |
|
| 105 |
+
print(f"🕵️ Enriching {total} rows with deep visit data...")
|
| 106 |
|
|
|
|
| 107 |
for index, row in target_df.iterrows():
|
| 108 |
lead_id = row["lead_id"]
|
|
|
|
| 109 |
try:
|
| 110 |
+
# CRITICAL CHANGE: We ask for page_views explicitly
|
|
|
|
| 111 |
visit_data = make_request(
|
| 112 |
f"{BASE_URL}/accounts/{ACCOUNT_ID}/leads/{lead_id}/visits",
|
| 113 |
+
params={"page[size]": 1, "include": "page_views"}
|
| 114 |
)
|
| 115 |
|
| 116 |
visits = visit_data.get("data", [])
|
| 117 |
+
included = visit_data.get("included", [])
|
| 118 |
+
|
| 119 |
if visits:
|
| 120 |
+
visit = visits[0] # Latest visit
|
| 121 |
+
v_attrs = visit.get("attributes", {})
|
|
|
|
| 122 |
|
| 123 |
+
# 1. Try Simple Attributes
|
| 124 |
+
landing = v_attrs.get("landing_page_path") or v_attrs.get("landing_page_url")
|
| 125 |
+
exit_p = v_attrs.get("exit_page_path") or v_attrs.get("exit_page_url")
|
| 126 |
+
|
| 127 |
+
# 2. Try Deep Linking (Page Views)
|
| 128 |
+
if not landing or not exit_p:
|
| 129 |
+
# Map all page views in the response
|
| 130 |
+
pv_map = {p["id"]: p["attributes"] for p in included if p["type"] == "page_views"}
|
| 131 |
+
|
| 132 |
+
# Get the list of page view IDs for this visit
|
| 133 |
+
pv_ids = [r["id"] for r in visit.get("relationships", {}).get("page_views", {}).get("data", [])]
|
| 134 |
+
|
| 135 |
+
if pv_ids:
|
| 136 |
+
# Assuming order is chronological.
|
| 137 |
+
# First ID = Landing, Last ID = Exit
|
| 138 |
+
first_pv = pv_map.get(pv_ids[0])
|
| 139 |
+
last_pv = pv_map.get(pv_ids[-1])
|
| 140 |
+
|
| 141 |
+
if not landing and first_pv:
|
| 142 |
+
landing = first_pv.get("url") or first_pv.get("path")
|
| 143 |
+
if not exit_p and last_pv:
|
| 144 |
+
exit_p = last_pv.get("url") or last_pv.get("path")
|
| 145 |
|
|
|
|
| 146 |
df.at[index, "landing_page_path"] = landing
|
| 147 |
df.at[index, "exit_page_path"] = exit_p
|
| 148 |
|
| 149 |
except Exception as e:
|
| 150 |
+
print(f"Failed lead {lead_id}: {e}")
|
| 151 |
|
|
|
|
| 152 |
if index % 5 == 0:
|
| 153 |
+
progress(index / total, desc="Enriching...")
|
| 154 |
|
| 155 |
return df
|
| 156 |
|
| 157 |
# =====================================================
|
| 158 |
+
# DEBUGGER
|
| 159 |
# =====================================================
|
| 160 |
+
def inspect_raw_json(lead_id):
|
| 161 |
+
if not lead_id: return "Please enter a Lead ID"
|
| 162 |
+
try:
|
| 163 |
+
url = f"{BASE_URL}/accounts/{ACCOUNT_ID}/leads/{lead_id}/visits"
|
| 164 |
+
params = {"page[size]": 1, "include": "page_views"}
|
| 165 |
+
r = requests.get(url, headers=HEADERS, params=params)
|
| 166 |
+
return json.dumps(r.json(), indent=2)
|
| 167 |
+
except Exception as e:
|
| 168 |
+
return str(e)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 169 |
|
| 170 |
# =====================================================
|
| 171 |
+
# UI LOGIC
|
| 172 |
# =====================================================
|
| 173 |
+
def load_preview(start, end):
|
| 174 |
+
df = fetch_basic_leads(start, end)
|
| 175 |
+
if df.empty: return df, pd.DataFrame(), "No data."
|
| 176 |
+
|
| 177 |
+
# Enrich top 10 for preview
|
| 178 |
+
df_prev = enrich_leads_with_visits(df.copy(), max_rows=10)
|
| 179 |
+
return df, df_prev.head(10), f"✅ Loaded {len(df)} leads. Previewing top 10."
|
| 180 |
+
|
| 181 |
+
def download_excel(df):
|
| 182 |
+
if df is None: return None
|
| 183 |
+
# Enrich ALL for download
|
| 184 |
+
final_df = enrich_leads_with_visits(df.copy())
|
| 185 |
+
path = "/tmp/leadfeeder_full.xlsx"
|
| 186 |
+
final_df.to_excel(path, index=False)
|
| 187 |
+
return path
|
|
|
|
|
|
|
| 188 |
|
| 189 |
# =====================================================
|
| 190 |
+
# UI LAYOUT
|
| 191 |
# =====================================================
|
| 192 |
+
with gr.Blocks(title="Leadfeeder Pro Analytics") as demo:
|
| 193 |
+
gr.Markdown("## 🚀 Leadfeeder Analytics")
|
| 194 |
+
|
| 195 |
+
with gr.Row():
|
| 196 |
+
pwd = gr.Textbox(type="password", label="Password")
|
| 197 |
+
gr.Button("Auth").click(lambda p: gr.Info("OK") if p==APP_PASSWORD else gr.Error("Bad Pwd"), pwd, None)
|
| 198 |
+
|
| 199 |
df_state = gr.State()
|
| 200 |
+
|
|
|
|
| 201 |
with gr.Tabs():
|
| 202 |
+
with gr.Tab("📋 Data & Report"):
|
| 203 |
+
start = gr.Textbox(label="Start", value=(date.today()-timedelta(days=30)).isoformat())
|
| 204 |
+
end = gr.Textbox(label="End", value=date.today().isoformat())
|
|
|
|
|
|
|
|
|
|
| 205 |
|
| 206 |
with gr.Row():
|
| 207 |
+
btn_load = gr.Button("1. Load Data", variant="primary")
|
| 208 |
+
btn_dl = gr.Button("2. Enrich & Download Excel")
|
| 209 |
+
file_dl = gr.File()
|
| 210 |
|
| 211 |
+
msg = gr.Markdown()
|
| 212 |
+
table = gr.Dataframe(label="Preview (Top 10)")
|
| 213 |
|
| 214 |
+
btn_load.click(load_preview, [start, end], [df_state, table, msg])
|
| 215 |
+
btn_dl.click(download_excel, df_state, file_dl)
|
| 216 |
|
| 217 |
+
with gr.Tab("🛠️ Debugger"):
|
| 218 |
+
gr.Markdown("Use this if columns are still NULL. Paste a 'lead_id' from the table above.")
|
| 219 |
+
dbg_id = gr.Textbox(label="Lead ID")
|
| 220 |
+
dbg_btn = gr.Button("Inspect Raw JSON")
|
| 221 |
+
dbg_out = gr.Code(language="json")
|
| 222 |
+
dbg_btn.click(inspect_raw_json, dbg_id, dbg_out)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 223 |
|
| 224 |
if __name__ == "__main__":
|
| 225 |
+
demo.launch(server_name="0.0.0.0", server_port=7860)
|