mktgtech commited on
Commit
7546cd3
ยท
verified ยท
1 Parent(s): f8d2c45

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +154 -59
app.py CHANGED
@@ -14,9 +14,9 @@ API_TOKEN = os.getenv("LEADFEEDER_API_TOKEN")
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
 
@@ -26,13 +26,14 @@ HEADERS = {
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
@@ -44,10 +45,14 @@ def make_request(url, params=None):
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(
@@ -61,8 +66,10 @@ def fetch_basic_leads(start_date, end_date):
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,80 +81,106 @@ def fetch_basic_leads(start_date, end_date):
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
95
 
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...")
@@ -155,13 +188,19 @@ def enrich_leads_with_visits(df, max_rows=None, progress=gr.Progress()):
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:
@@ -172,20 +211,59 @@ def inspect_raw_json(lead_id):
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
  # =====================================================
@@ -193,33 +271,50 @@ 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)
 
14
  APP_PASSWORD = os.getenv("APP_PASSWORD")
15
 
16
  if not API_TOKEN or not APP_PASSWORD:
17
+ raise RuntimeError("Missing required secrets: LEADFEEDER_API_TOKEN or APP_PASSWORD")
18
 
19
+ ACCOUNT_ID = "255333" # PREMIUM account
20
  BASE_URL = "https://api.leadfeeder.com"
21
  PAGE_SIZE = 50
22
 
 
26
  }
27
 
28
  # =====================================================
29
+ # UTILS: RATE LIMIT HANDLER
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
+ print("โณ Rate limit hit. Sleeping for 60 seconds...")
37
  time.sleep(61)
38
  retries -= 1
39
  continue
 
45
  # DATA FETCHING
46
  # =====================================================
47
  def fetch_basic_leads(start_date, end_date):
48
+ """
49
+ Step 1: Fetch ALL company details (fast fetch).
50
+ """
51
  page = 1
52
  rows = []
53
 
54
+ print(f"๐Ÿš€ Fetching full company list for {start_date} to {end_date}...")
55
+
56
  while True:
57
  try:
58
  js = make_request(
 
66
  }
67
  )
68
  data = js.get("data", [])
69
+ if not data:
70
+ break
71
 
72
+ # Map Included Locations
73
  included = js.get("included", [])
74
  loc_map = {str(i["id"]): i["attributes"] for i in included if i["type"] == "locations"}
75
 
 
81
  rows.append({
82
  "lead_id": lead.get("id"),
83
  "company_name": a.get("name"),
84
+ "website_url": a.get("website_url"),
85
+ "phone": a.get("phone"),
86
+ "business_id": a.get("business_id"),
87
  "primary_industry": a.get("industry"),
88
+ "all_industries": ", ".join([i.get("name") for i in a.get("industries", [])]) if a.get("industries") else None,
89
+ "first_visit_date": a.get("first_visit_date"),
90
+ "last_visit_date": a.get("last_visit_date"),
91
  "total_visits": a.get("visits"),
92
  "lead_quality_score": a.get("quality"),
 
 
 
93
  "revenue": a.get("revenue"),
94
  "employee_count": a.get("employee_count"),
95
+ "employees_min": a.get("employees_range", {}).get("min") if a.get("employees_range") else None,
96
+ "employees_max": a.get("employees_range", {}).get("max") if a.get("employees_range") else None,
97
  "assignee": a.get("assignee"),
98
+ "emailed_to": a.get("emailed_to"),
99
+ "crm_lead_id": a.get("crm_lead_id"),
100
  "crm_organization_id": a.get("crm_organization_id"),
101
+ "tags": ", ".join(a.get("tags", [])) if a.get("tags") else None,
102
  "linkedin_url": a.get("linkedin_url"),
103
+ "twitter_handle": a.get("twitter_handle"),
104
+ "facebook_url": a.get("facebook_url"),
105
+ "country": loc.get("country"),
106
+ "region": loc.get("region"),
107
+ "city": loc.get("city"),
108
+ "leadfeeder_url": a.get("view_in_leadfeeder"),
109
+ "landing_page_path": None,
110
+ "exit_page_path": None
111
  })
112
+
113
+ print(f"โœ… Page {page} loaded. Rows: {len(rows)}")
114
  page += 1
115
+
116
  except Exception as e:
117
  print(f"Error on page {page}: {e}")
118
  break
119
 
120
+ df = pd.DataFrame(rows)
121
+ if not df.empty:
122
+ df["last_visit_date"] = pd.to_datetime(df["last_visit_date"], errors="coerce")
123
+ return df
124
 
125
+ def enrich_leads_with_visits(df, start_date, end_date, max_rows=None, progress=gr.Progress()):
126
+ """
127
+ Step 2: Go back and fetch the specific Visit URLs for each company.
128
+ CRITICAL FIX: Now accepts start_date and end_date.
129
+ """
130
+ if df.empty:
131
+ return df
132
 
133
  target_df = df.head(max_rows) if max_rows else df
134
  total = len(target_df)
135
 
136
+ print(f"๐Ÿ•ต๏ธ Deep enriching {total} rows with visit data ({start_date} to {end_date})...")
137
 
138
  for index, row in target_df.iterrows():
139
  lead_id = row["lead_id"]
140
+
141
  try:
142
+ # FIX: Added start_date and end_date to params!
143
  visit_data = make_request(
144
  f"{BASE_URL}/accounts/{ACCOUNT_ID}/leads/{lead_id}/visits",
145
+ params={
146
+ "start_date": start_date,
147
+ "end_date": end_date,
148
+ "page[size]": 1,
149
+ "include": "page_views"
150
+ }
151
  )
152
 
153
  visits = visit_data.get("data", [])
154
  included = visit_data.get("included", [])
155
 
156
+ landing = None
157
+ exit_p = None
158
+
159
  if visits:
160
+ visit = visits[0]
161
  v_attrs = visit.get("attributes", {})
162
 
163
+ # A. Direct Attributes
164
  landing = v_attrs.get("landing_page_path") or v_attrs.get("landing_page_url")
165
  exit_p = v_attrs.get("exit_page_path") or v_attrs.get("exit_page_url")
166
 
167
+ # B. Page Views (Deep Link)
168
  if not landing or not exit_p:
 
169
  pv_map = {p["id"]: p["attributes"] for p in included if p["type"] == "page_views"}
 
 
170
  pv_ids = [r["id"] for r in visit.get("relationships", {}).get("page_views", {}).get("data", [])]
171
 
172
  if pv_ids:
 
 
173
  first_pv = pv_map.get(pv_ids[0])
174
+ if first_pv: landing = first_pv.get("url") or first_pv.get("path")
175
+
176
  last_pv = pv_map.get(pv_ids[-1])
177
+ if last_pv: exit_p = last_pv.get("url") or last_pv.get("path")
 
 
 
 
178
 
179
  df.at[index, "landing_page_path"] = landing
180
  df.at[index, "exit_page_path"] = exit_p
181
 
182
  except Exception as e:
183
+ print(f"Failed to enrich lead {lead_id}: {e}")
184
 
185
  if index % 5 == 0:
186
  progress(index / total, desc="Enriching...")
 
188
  return df
189
 
190
  # =====================================================
191
+ # DEBUGGER TOOL
192
  # =====================================================
193
+ def inspect_raw_json(lead_id, start_date, end_date):
194
  if not lead_id: return "Please enter a Lead ID"
195
  try:
196
+ # FIX: Added dates to Debugger URL params
197
  url = f"{BASE_URL}/accounts/{ACCOUNT_ID}/leads/{lead_id}/visits"
198
+ params = {
199
+ "start_date": start_date,
200
+ "end_date": end_date,
201
+ "page[size]": 1,
202
+ "include": "page_views"
203
+ }
204
  r = requests.get(url, headers=HEADERS, params=params)
205
  return json.dumps(r.json(), indent=2)
206
  except Exception as e:
 
211
  # =====================================================
212
  def load_preview(start, end):
213
  df = fetch_basic_leads(start, end)
214
+ if df.empty:
215
+ return df, pd.DataFrame(), "โš ๏ธ No data found."
216
 
217
+ # Pass dates to enrichment!
218
+ df_preview = df.copy()
219
+ df_preview = enrich_leads_with_visits(df_preview, start, end, max_rows=10)
220
+
221
+ status = f"โœ… Loaded **{len(df):,} companies**. Preview shows top 10 rows enriched."
222
+ return df, df_preview.head(10), status
223
+
224
+ def download_full_excel(df, start, end):
225
+ if df is None or df.empty: return None
226
+
227
+ print("โณ Starting full enrichment for Excel export...")
228
+ # Pass dates to enrichment!
229
+ enriched_df = enrich_leads_with_visits(df.copy(), start, end)
230
+
231
+ path = "/tmp/leadfeeder_company_data_full.xlsx"
232
+ enriched_df.to_excel(path, index=False)
233
  return path
234
 
235
+ # =====================================================
236
+ # DASHBOARD LOGIC (KPIs)
237
+ # =====================================================
238
+ DASHBOARD_PRESETS = {
239
+ "Total Visits by Industry": ("primary_industry", "total_visits", "sum"),
240
+ "Total Visits by Country": ("country", "total_visits", "sum"),
241
+ "Avg Quality by Industry": ("primary_industry", "lead_quality_score", "mean"),
242
+ "Top Accounts by Visits": ("company_name", "total_visits", "sum"),
243
+ "Companies by Country": ("country", "company_name", "count"),
244
+ "Visits by City": ("city", "total_visits", "sum"),
245
+ }
246
+
247
+ def build_kpis(df):
248
+ if df is None or df.empty: return 0, 0, 0, 0, 0, 0
249
+ return (
250
+ len(df),
251
+ df["total_visits"].gt(0).sum(),
252
+ int(df["total_visits"].sum()),
253
+ round(df["lead_quality_score"].mean(), 2),
254
+ round(df["crm_organization_id"].notna().mean() * 100, 1),
255
+ round(df["linkedin_url"].notna().mean() * 100, 1),
256
+ )
257
+
258
+ def build_dashboard(df, preset, top_n):
259
+ if df is None or df.empty: return px.bar(title="No Data")
260
+ dim, metric, agg = DASHBOARD_PRESETS[preset]
261
+ if agg == "count":
262
+ grouped = df.groupby(dim, dropna=False).size().reset_index(name="value")
263
+ else:
264
+ grouped = df.groupby(dim, dropna=False)[metric].agg(agg).reset_index(name="value")
265
+ return px.bar(grouped.sort_values("value", ascending=False).head(top_n), x=dim, y="value", title=preset)
266
+
267
  # =====================================================
268
  # UI LAYOUT
269
  # =====================================================
 
271
  gr.Markdown("## ๐Ÿš€ Leadfeeder Analytics")
272
 
273
  with gr.Row():
274
+ pwd = gr.Textbox(type="password", label="App Password")
275
+ gr.Button("Auth").click(lambda p: gr.Info("Success") if p==APP_PASSWORD else gr.Error("Invalid"), pwd, None)
276
 
277
  df_state = gr.State()
278
+ status = gr.Markdown()
279
 
280
  with gr.Tabs():
281
+ # --- TAB 1: DATA ---
282
  with gr.Tab("๐Ÿ“‹ Data & Report"):
283
+ with gr.Row():
284
+ start = gr.Textbox(label="Start Date", value=(date.today()-timedelta(days=30)).isoformat())
285
+ end = gr.Textbox(label="End Date", value=date.today().isoformat())
286
 
287
  with gr.Row():
288
+ btn_load = gr.Button("1. Load Data (Preview)", variant="primary")
289
+ btn_dl = gr.Button("2. Enrich & Download Full Excel")
290
+ file_dl = gr.File(label="Download Excel")
291
+
292
+ table = gr.Dataframe(label="Preview (Top 10 Enriched)", interactive=True)
293
 
294
+ btn_load.click(load_preview, [start, end], [df_state, table, status])
295
+ # Updated to pass start/end inputs
296
+ btn_dl.click(download_full_excel, [df_state, start, end], file_dl)
297
+
298
+ # --- TAB 2: DASHBOARD ---
299
+ with gr.Tab("๐Ÿ“Š Dashboard"):
300
+ kpis = [gr.Number(label=l) for l in ["Total Companies", "Active Companies", "Total Visits", "Avg Quality", "CRM %", "LinkedIn %"]]
301
+ gr.Button("Refresh KPIs").click(build_kpis, df_state, kpis)
302
+
303
+ with gr.Row():
304
+ preset = gr.Dropdown(choices=list(DASHBOARD_PRESETS.keys()), label="Chart")
305
+ top_n = gr.Slider(5, 50, value=10, label="Top N")
306
 
307
+ chart = gr.Plot()
308
+ gr.Button("Build View").click(build_dashboard, [df_state, preset, top_n], chart)
309
 
310
+ # --- TAB 3: DEBUGGER ---
311
  with gr.Tab("๐Ÿ› ๏ธ Debugger"):
312
+ gr.Markdown("Use this to inspect raw API data for a specific lead if columns are empty.")
313
+ dbg_id = gr.Textbox(label="Lead ID (Copy from Data tab)")
314
  dbg_btn = gr.Button("Inspect Raw JSON")
315
  dbg_out = gr.Code(language="json")
316
+ # Updated to pass start/end inputs
317
+ dbg_btn.click(inspect_raw_json, [dbg_id, start, end], dbg_out)
318
 
319
  if __name__ == "__main__":
320
  demo.launch(server_name="0.0.0.0", server_port=7860)