mktgtech commited on
Commit
f8d2c45
·
verified ·
1 Parent(s): 1ff9f90

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +97 -137
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: LEADFEEDER_API_TOKEN or APP_PASSWORD")
17
 
18
- ACCOUNT_ID = "255333" # PREMIUM account
19
  BASE_URL = "https://api.leadfeeder.com"
20
- PAGE_SIZE = 50 # Reduced page size to prevent timeouts
21
 
22
  HEADERS = {
23
  "Authorization": f"Token token={API_TOKEN}",
@@ -25,42 +26,30 @@ HEADERS = {
25
  }
26
 
27
  # =====================================================
28
- # UTILS: RATE LIMIT HANDLER
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=30)
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 for rate limit")
48
 
49
  # =====================================================
50
  # DATA FETCHING
51
  # =====================================================
52
- def fetch_basic_leads(start_date, end_date, progress=gr.Progress()):
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
- # Placeholders (Filled in Step 2)
105
- "landing_page_path": None,
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
- Step 2: For each company, fetch the SPECIFIC visit details.
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"🕵️ Deep fetching visits for {total} companies...")
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
- # Call the VISITS endpoint for this specific lead
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} # We only need the latest 1 visit
142
  )
143
 
144
  visits = visit_data.get("data", [])
 
 
145
  if visits:
146
- # The visits endpoint usually returns the attributes directly on the visit object
147
- # OR we look at the first page view if nested.
148
- latest_visit = visits[0]["attributes"]
149
 
150
- # Strategy: Check direct attributes first, then fallback
151
- landing = (latest_visit.get("landing_page_path") or
152
- latest_visit.get("entry_page") or
153
- latest_visit.get("first_page_view", {}).get("url"))
154
-
155
- exit_p = (latest_visit.get("exit_page_path") or
156
- latest_visit.get("exit_page") or
157
- latest_visit.get("last_page_view", {}).get("url"))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 to enrich lead {lead_id}: {e}")
165
 
166
- # Update progress bar every 5 rows
167
  if index % 5 == 0:
168
- progress(index / total, desc=f"Enriching row {index}/{total}")
169
 
170
  return df
171
 
172
  # =====================================================
173
- # UI WRAPPERS
174
  # =====================================================
175
- def load_data_preview(start_date, end_date):
176
- # 1. Get Basic List
177
- df = fetch_basic_leads(start_date, end_date)
178
-
179
- if df.empty:
180
- return df, pd.DataFrame(), "⚠️ No data found."
181
-
182
- # 2. Enrich ONLY the first 10 rows for the UI preview (Fast)
183
- df_preview = df.copy()
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
- # KPIS & DASHBOARDS (UNCHANGED)
205
  # =====================================================
206
- DASHBOARD_PRESETS = {
207
- "Total Visits by Industry": ("primary_industry", "total_visits", "sum"),
208
- "Total Visits by Country": ("country", "total_visits", "sum"),
209
- "Avg Quality by Industry": ("primary_industry", "lead_quality_score", "mean"),
210
- "Companies by Country": ("country", "company_name", "count"),
211
- }
212
-
213
- def build_kpis(df):
214
- if df is None or df.empty: return 0, 0, 0, 0
215
- return (len(df), int(df["total_visits"].sum()), round(df["lead_quality_score"].mean(), 2), round(df["crm_organization_id"].notna().mean() * 100, 1))
216
-
217
- def build_dashboard(df, preset, top_n):
218
- if df is None or df.empty: return px.bar(title="No Data")
219
- dim, metric, agg = DASHBOARD_PRESETS[preset]
220
- if agg == "count": grouped = df.groupby(dim, dropna=False).size().reset_index(name="value")
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("## 🔐 Login")
229
- pwd = gr.Textbox(type="password", label="App Password")
230
- gr.Button("Login").click(lambda p: True if p == APP_PASSWORD else gr.Error("Invalid"), pwd, None)
231
-
 
 
232
  df_state = gr.State()
233
- status = gr.Markdown()
234
-
235
  with gr.Tabs():
236
- with gr.Tab("📋 Data"):
237
- with gr.Row():
238
- start = gr.Textbox(label="Start", value=(date.today() - timedelta(days=30)).isoformat())
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
- # NOTE: This button now triggers the FULL enrichment
245
- dl_btn = gr.Button("⬇️ Enrich & Download Full Excel")
246
- dl_file = gr.File()
247
 
248
- preview = gr.Dataframe(label="Preview (Top 10 Enriched)")
 
249
 
250
- load_btn.click(load_data_preview, [start, end], [df_state, preview, status])
251
- dl_btn.click(download_full_excel, df_state, dl_file)
252
 
253
- with gr.Tab("📊 Dashboard"):
254
- kpis = [gr.Number(label=l) for l in ["Companies", "Visits", "Quality", "CRM %"]]
255
- gr.Button("Refresh KPIs").click(build_kpis, df_state, kpis)
256
-
257
- with gr.Row():
258
- preset = gr.Dropdown(choices=list(DASHBOARD_PRESETS.keys()), label="Chart")
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, ssr_mode=False)
 
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)