abinazebinoy commited on
Commit
297af77
·
1 Parent(s): d8672be

fix(pipeline): BUG-13 — entity resolver now runs on international data

Browse files

BUG-13: entity resolution only ran on myneta+mca records.
ICIJ, OpenSanctions, and IBBI entities were never resolved
against Indian politicians/companies so cross-dataset
evidence chains were never built.
Fixed: politicians pool now includes opensanctions + icij;
companies pool now includes ibbi.
All cross-dataset matches now produce DIRECTOR_OF links.

Files changed (1) hide show
  1. processing/pipeline.py +50 -86
processing/pipeline.py CHANGED
@@ -28,7 +28,7 @@ class BharatGraphPipeline:
28
  os.makedirs("data/samples", exist_ok=True)
29
  logger.info("[Pipeline] Initialized")
30
 
31
- # ── Core scrapers (already existed) ──────────────────────────────────────
32
 
33
  def run_datagov(self) -> list:
34
  try:
@@ -50,8 +50,7 @@ class BharatGraphPipeline:
50
  try:
51
  from scrapers.cag_scraper import CAGScraper
52
  reports = CAGScraper().fetch_report_list(limit=50)
53
- for r in reports:
54
- r["_source"] = "cag"
55
  logger.success(f"[Pipeline] CAG: {len(reports)} reports")
56
  return reports
57
  except Exception as e:
@@ -105,23 +104,18 @@ class BharatGraphPipeline:
105
  try:
106
  from scrapers.pib_scraper import PIBScraper
107
  articles = PIBScraper().fetch_all_feeds(save=False)
108
- for a in articles:
109
- a["_source"] = "pib"
110
  logger.success(f"[Pipeline] PIB: {len(articles)} articles")
111
  return articles
112
  except Exception as e:
113
  logger.error(f"[Pipeline] PIB failed: {e}")
114
  return []
115
 
116
- # ── NEW: 15 additional scrapers ───────────────────────────────────────────
117
-
118
  def run_loksabha(self) -> list:
119
  try:
120
  from scrapers.loksabha_scraper import LokSabhaScraper
121
- s = LokSabhaScraper()
122
- records = s.fetch_questions(limit=50)
123
- for r in records:
124
- r["_source"] = "loksabha"
125
  logger.success(f"[Pipeline] LokSabha: {len(records)} records")
126
  return records
127
  except Exception as e:
@@ -132,8 +126,7 @@ class BharatGraphPipeline:
132
  try:
133
  from scrapers.sebi_scraper import SEBIScraper
134
  records = SEBIScraper().fetch_enforcement_orders(limit=30)
135
- for r in records:
136
- r["_source"] = "sebi"
137
  logger.success(f"[Pipeline] SEBI: {len(records)} orders")
138
  return records
139
  except Exception as e:
@@ -144,8 +137,7 @@ class BharatGraphPipeline:
144
  try:
145
  from scrapers.ed_scraper import EDScraper
146
  records = EDScraper().fetch_press_releases(limit=30)
147
- for r in records:
148
- r["_source"] = "ed"
149
  logger.success(f"[Pipeline] ED: {len(records)} press releases")
150
  return records
151
  except Exception as e:
@@ -156,8 +148,7 @@ class BharatGraphPipeline:
156
  try:
157
  from scrapers.cvc_scraper import CVCScraper
158
  records = CVCScraper().fetch_circulars(limit=30)
159
- for r in records:
160
- r["_source"] = "cvc"
161
  logger.success(f"[Pipeline] CVC: {len(records)} records")
162
  return records
163
  except Exception as e:
@@ -168,8 +159,7 @@ class BharatGraphPipeline:
168
  try:
169
  from scrapers.njdg_scraper import NJDGScraper
170
  records = NJDGScraper().fetch_pendency_stats()
171
- for r in records:
172
- r["_source"] = "njdg"
173
  logger.success(f"[Pipeline] NJDG: {len(records)} records")
174
  return records
175
  except Exception as e:
@@ -180,8 +170,7 @@ class BharatGraphPipeline:
180
  try:
181
  from scrapers.electoral_bond_scraper import ElectoralBondScraper
182
  records = ElectoralBondScraper().fetch_bond_data()
183
- for r in records:
184
- r["_source"] = "electoral_bond"
185
  logger.success(f"[Pipeline] Electoral Bonds: {len(records)} records")
186
  return records
187
  except Exception as e:
@@ -195,10 +184,8 @@ class BharatGraphPipeline:
195
  records = []
196
  scraper = ICIJScraper()
197
  for name in entities:
198
- hits = scraper.search_entity(name)
199
- records.extend(hits)
200
- for r in records:
201
- r["_source"] = "icij"
202
  logger.success(f"[Pipeline] ICIJ: {len(records)} matches")
203
  return records
204
  except Exception as e:
@@ -212,10 +199,8 @@ class BharatGraphPipeline:
212
  records = []
213
  scraper = OpenSanctionsScraper()
214
  for name in entities:
215
- hits = scraper.search_entity(name)
216
- records.extend(hits)
217
- for r in records:
218
- r["_source"] = "opensanctions"
219
  logger.success(f"[Pipeline] OpenSanctions: {len(records)} matches")
220
  return records
221
  except Exception as e:
@@ -231,8 +216,7 @@ class BharatGraphPipeline:
231
  "Yogi Adityanath", "Shashi Tharoor", "Anurag Thakur",
232
  ]
233
  records = WikidataScraper().enrich_entity_list(politicians)
234
- for r in records:
235
- r["_source"] = "wikidata"
236
  logger.success(f"[Pipeline] Wikidata: {len(records)} enrichments")
237
  return records
238
  except Exception as e:
@@ -243,8 +227,7 @@ class BharatGraphPipeline:
243
  try:
244
  from scrapers.ibbi_scraper import IBBIScraper
245
  records = IBBIScraper().fetch_orders(limit=30)
246
- for r in records:
247
- r["_source"] = "ibbi"
248
  logger.success(f"[Pipeline] IBBI: {len(records)} orders")
249
  return records
250
  except Exception as e:
@@ -255,8 +238,7 @@ class BharatGraphPipeline:
255
  try:
256
  from scrapers.ngo_darpan_scraper import NGODarpanScraper
257
  records = NGODarpanScraper().fetch_ngo_list(limit=30)
258
- for r in records:
259
- r["_source"] = "ngo_darpan"
260
  logger.success(f"[Pipeline] NGO Darpan: {len(records)} NGOs")
261
  return records
262
  except Exception as e:
@@ -267,8 +249,7 @@ class BharatGraphPipeline:
267
  try:
268
  from scrapers.cppp_scraper import CPPPScraper
269
  records = CPPPScraper().fetch_tenders(limit=30)
270
- for r in records:
271
- r["_source"] = "cppp"
272
  logger.success(f"[Pipeline] CPPP: {len(records)} tenders")
273
  return records
274
  except Exception as e:
@@ -279,8 +260,7 @@ class BharatGraphPipeline:
279
  try:
280
  from scrapers.ncrb_scraper import NCRBScraper
281
  records = NCRBScraper().fetch_crime_statistics(limit=20)
282
- for r in records:
283
- r["_source"] = "ncrb"
284
  logger.success(f"[Pipeline] NCRB: {len(records)} records")
285
  return records
286
  except Exception as e:
@@ -291,18 +271,13 @@ class BharatGraphPipeline:
291
  try:
292
  from scrapers.lgd_scraper import LGDScraper
293
  records = LGDScraper().fetch_state_codes()
294
- for r in records:
295
- r["_source"] = "lgd"
296
  logger.success(f"[Pipeline] LGD: {len(records)} records")
297
  return records
298
  except Exception as e:
299
  logger.warning(f"[Pipeline] LGD failed: {e}")
300
  return []
301
 
302
- # ── Runner map — all 20 scrapers ─────────────────────────────────────────
303
-
304
- RUNNER_MAP = None # set in run()
305
-
306
  def _get_runner_map(self):
307
  return {
308
  "datagov": self.run_datagov,
@@ -327,8 +302,6 @@ class BharatGraphPipeline:
327
  "lgd": self.run_lgd,
328
  }
329
 
330
- # ── Entity resolution ────────────────────────────────────────────────────
331
-
332
  def find_politician_company_links(self, politicians, companies):
333
  logger.info("[Pipeline] Finding politician-company links...")
334
  matches = self.resolver.cross_dataset_match(
@@ -339,8 +312,6 @@ class BharatGraphPipeline:
339
  logger.warning(f"[Pipeline] {len(matches)} politician-company links found!")
340
  return matches
341
 
342
- # ── Save ─────────────────────────────────────────────────────────────────
343
-
344
  def save(self, data: dict) -> str:
345
  filepath = f"data/processed/pipeline_{self.timestamp}.json"
346
  with open(filepath, "w", encoding="utf-8") as f:
@@ -348,14 +319,10 @@ class BharatGraphPipeline:
348
  logger.success(f"[Pipeline] Saved to {filepath}")
349
  return filepath
350
 
351
- # ── Main run ─────────────────────────────────────────────────────────────
352
-
353
  def run(self, scrapers: list = None, parallel: bool = True) -> dict:
354
- runner_map = self._get_runner_map()
355
  all_scrapers = list(runner_map.keys())
356
  to_run = scrapers if scrapers else all_scrapers
357
-
358
- # validate
359
  to_run = [s.strip() for s in to_run if s.strip() in runner_map]
360
  if not to_run:
361
  to_run = all_scrapers
@@ -367,10 +334,7 @@ class BharatGraphPipeline:
367
 
368
  if parallel and len(to_run) > 1:
369
  with ThreadPoolExecutor(max_workers=6) as pool:
370
- futures = {
371
- pool.submit(runner_map[name]): name
372
- for name in to_run
373
- }
374
  for future in as_completed(futures):
375
  name = futures[future]
376
  try:
@@ -382,15 +346,27 @@ class BharatGraphPipeline:
382
  for name in to_run:
383
  raw[name] = runner_map[name]()
384
 
385
- politicians = raw.get("myneta", []) + raw.get("wikidata", [])
386
- companies = raw.get("mca", [])
387
- links = []
 
 
 
 
 
 
 
 
 
 
 
 
388
  if politicians and companies:
389
  politicians = self.resolver.resolve_dataset(politicians, "name")
390
  companies = self.resolver.resolve_dataset(companies, "name")
391
  links = self.find_politician_company_links(politicians, companies)
392
 
393
- duration = (datetime.now() - start).seconds
394
  per_source = {name: len(records) for name, records in raw.items()}
395
 
396
  summary = {
@@ -411,44 +387,32 @@ class BharatGraphPipeline:
411
  f"{summary['total_raw_records']} total records")
412
 
413
  results = {
414
- "raw": raw,
415
- "links": links,
416
- "summary": summary,
417
- "run_at": start.isoformat(),
418
  }
419
  results["saved_to"] = self.save(results)
420
  return results
421
 
422
 
423
  if __name__ == "__main__":
424
- parser = argparse.ArgumentParser(description="BharatGraph Full Pipeline")
425
- parser.add_argument("--scrapers", type=str, default=None,
426
- help="comma-separated list e.g. cag,gem,pib,wikidata")
427
- parser.add_argument("--parallel", action="store_true", default=True,
428
- help="run scrapers in parallel threads (default: True)")
429
- parser.add_argument("--sequential",action="store_true", default=False,
430
- help="run scrapers one by one")
431
- args = parser.parse_args()
432
  scrapers = args.scrapers.split(",") if args.scrapers else None
433
  parallel = not args.sequential
434
 
435
- print("=" * 55)
436
- print("BharatGraph - Full Data Pipeline (20 scrapers)")
437
- print("=" * 55)
438
-
439
  results = BharatGraphPipeline().run(scrapers=scrapers, parallel=parallel)
440
  s = results["summary"]
441
-
442
- print(f"\n{'='*55}")
443
- print("PIPELINE SUMMARY")
444
- print(f"{'='*55}")
445
  print(f" Scrapers run: {len(s['scrapers_run'])}")
446
  print(f" Duration: {s['duration_seconds']}s")
447
  print(f" Total records: {s['total_raw_records']}")
448
- print(f"\n Per source:")
449
  for src, n in sorted(s["per_source"].items(), key=lambda x: -x[1]):
450
  if n > 0:
451
  print(f" {src:<20} {n}")
452
- print(f"\n Politician-co links: {s['politician_co_links']}")
453
- print(f" Saved to: {results.get('saved_to')}")
454
- print(f"{'='*55}")
 
28
  os.makedirs("data/samples", exist_ok=True)
29
  logger.info("[Pipeline] Initialized")
30
 
31
+ # ── Scrapers ─────────────────────────────────────────────────────────────
32
 
33
  def run_datagov(self) -> list:
34
  try:
 
50
  try:
51
  from scrapers.cag_scraper import CAGScraper
52
  reports = CAGScraper().fetch_report_list(limit=50)
53
+ for r in reports: r["_source"] = "cag"
 
54
  logger.success(f"[Pipeline] CAG: {len(reports)} reports")
55
  return reports
56
  except Exception as e:
 
104
  try:
105
  from scrapers.pib_scraper import PIBScraper
106
  articles = PIBScraper().fetch_all_feeds(save=False)
107
+ for a in articles: a["_source"] = "pib"
 
108
  logger.success(f"[Pipeline] PIB: {len(articles)} articles")
109
  return articles
110
  except Exception as e:
111
  logger.error(f"[Pipeline] PIB failed: {e}")
112
  return []
113
 
 
 
114
  def run_loksabha(self) -> list:
115
  try:
116
  from scrapers.loksabha_scraper import LokSabhaScraper
117
+ records = LokSabhaScraper().fetch_questions(limit=50)
118
+ for r in records: r["_source"] = "loksabha"
 
 
119
  logger.success(f"[Pipeline] LokSabha: {len(records)} records")
120
  return records
121
  except Exception as e:
 
126
  try:
127
  from scrapers.sebi_scraper import SEBIScraper
128
  records = SEBIScraper().fetch_enforcement_orders(limit=30)
129
+ for r in records: r["_source"] = "sebi"
 
130
  logger.success(f"[Pipeline] SEBI: {len(records)} orders")
131
  return records
132
  except Exception as e:
 
137
  try:
138
  from scrapers.ed_scraper import EDScraper
139
  records = EDScraper().fetch_press_releases(limit=30)
140
+ for r in records: r["_source"] = "ed"
 
141
  logger.success(f"[Pipeline] ED: {len(records)} press releases")
142
  return records
143
  except Exception as e:
 
148
  try:
149
  from scrapers.cvc_scraper import CVCScraper
150
  records = CVCScraper().fetch_circulars(limit=30)
151
+ for r in records: r["_source"] = "cvc"
 
152
  logger.success(f"[Pipeline] CVC: {len(records)} records")
153
  return records
154
  except Exception as e:
 
159
  try:
160
  from scrapers.njdg_scraper import NJDGScraper
161
  records = NJDGScraper().fetch_pendency_stats()
162
+ for r in records: r["_source"] = "njdg"
 
163
  logger.success(f"[Pipeline] NJDG: {len(records)} records")
164
  return records
165
  except Exception as e:
 
170
  try:
171
  from scrapers.electoral_bond_scraper import ElectoralBondScraper
172
  records = ElectoralBondScraper().fetch_bond_data()
173
+ for r in records: r["_source"] = "electoral_bond"
 
174
  logger.success(f"[Pipeline] Electoral Bonds: {len(records)} records")
175
  return records
176
  except Exception as e:
 
184
  records = []
185
  scraper = ICIJScraper()
186
  for name in entities:
187
+ records.extend(scraper.search_entity(name))
188
+ for r in records: r["_source"] = "icij"
 
 
189
  logger.success(f"[Pipeline] ICIJ: {len(records)} matches")
190
  return records
191
  except Exception as e:
 
199
  records = []
200
  scraper = OpenSanctionsScraper()
201
  for name in entities:
202
+ records.extend(scraper.search_entity(name))
203
+ for r in records: r["_source"] = "opensanctions"
 
 
204
  logger.success(f"[Pipeline] OpenSanctions: {len(records)} matches")
205
  return records
206
  except Exception as e:
 
216
  "Yogi Adityanath", "Shashi Tharoor", "Anurag Thakur",
217
  ]
218
  records = WikidataScraper().enrich_entity_list(politicians)
219
+ for r in records: r["_source"] = "wikidata"
 
220
  logger.success(f"[Pipeline] Wikidata: {len(records)} enrichments")
221
  return records
222
  except Exception as e:
 
227
  try:
228
  from scrapers.ibbi_scraper import IBBIScraper
229
  records = IBBIScraper().fetch_orders(limit=30)
230
+ for r in records: r["_source"] = "ibbi"
 
231
  logger.success(f"[Pipeline] IBBI: {len(records)} orders")
232
  return records
233
  except Exception as e:
 
238
  try:
239
  from scrapers.ngo_darpan_scraper import NGODarpanScraper
240
  records = NGODarpanScraper().fetch_ngo_list(limit=30)
241
+ for r in records: r["_source"] = "ngo_darpan"
 
242
  logger.success(f"[Pipeline] NGO Darpan: {len(records)} NGOs")
243
  return records
244
  except Exception as e:
 
249
  try:
250
  from scrapers.cppp_scraper import CPPPScraper
251
  records = CPPPScraper().fetch_tenders(limit=30)
252
+ for r in records: r["_source"] = "cppp"
 
253
  logger.success(f"[Pipeline] CPPP: {len(records)} tenders")
254
  return records
255
  except Exception as e:
 
260
  try:
261
  from scrapers.ncrb_scraper import NCRBScraper
262
  records = NCRBScraper().fetch_crime_statistics(limit=20)
263
+ for r in records: r["_source"] = "ncrb"
 
264
  logger.success(f"[Pipeline] NCRB: {len(records)} records")
265
  return records
266
  except Exception as e:
 
271
  try:
272
  from scrapers.lgd_scraper import LGDScraper
273
  records = LGDScraper().fetch_state_codes()
274
+ for r in records: r["_source"] = "lgd"
 
275
  logger.success(f"[Pipeline] LGD: {len(records)} records")
276
  return records
277
  except Exception as e:
278
  logger.warning(f"[Pipeline] LGD failed: {e}")
279
  return []
280
 
 
 
 
 
281
  def _get_runner_map(self):
282
  return {
283
  "datagov": self.run_datagov,
 
302
  "lgd": self.run_lgd,
303
  }
304
 
 
 
305
  def find_politician_company_links(self, politicians, companies):
306
  logger.info("[Pipeline] Finding politician-company links...")
307
  matches = self.resolver.cross_dataset_match(
 
312
  logger.warning(f"[Pipeline] {len(matches)} politician-company links found!")
313
  return matches
314
 
 
 
315
  def save(self, data: dict) -> str:
316
  filepath = f"data/processed/pipeline_{self.timestamp}.json"
317
  with open(filepath, "w", encoding="utf-8") as f:
 
319
  logger.success(f"[Pipeline] Saved to {filepath}")
320
  return filepath
321
 
 
 
322
  def run(self, scrapers: list = None, parallel: bool = True) -> dict:
323
+ runner_map = self._get_runner_map()
324
  all_scrapers = list(runner_map.keys())
325
  to_run = scrapers if scrapers else all_scrapers
 
 
326
  to_run = [s.strip() for s in to_run if s.strip() in runner_map]
327
  if not to_run:
328
  to_run = all_scrapers
 
334
 
335
  if parallel and len(to_run) > 1:
336
  with ThreadPoolExecutor(max_workers=6) as pool:
337
+ futures = {pool.submit(runner_map[name]): name for name in to_run}
 
 
 
338
  for future in as_completed(futures):
339
  name = futures[future]
340
  try:
 
346
  for name in to_run:
347
  raw[name] = runner_map[name]()
348
 
349
+ # BUG-13 FIX: entity resolver now runs on ALL data sources that could
350
+ # contain Indian persons/companies, not just myneta+mca.
351
+ # ICIJ and OpenSanctions entities are now resolved against politicians
352
+ # and companies so cross-dataset evidence chains are created.
353
+ politicians = (
354
+ raw.get("myneta", []) +
355
+ raw.get("wikidata", []) +
356
+ raw.get("opensanctions",[]) + # BUG-13 FIX: added
357
+ raw.get("icij", []) # BUG-13 FIX: added
358
+ )
359
+ companies = (
360
+ raw.get("mca", []) +
361
+ raw.get("ibbi", []) # BUG-13 FIX: added
362
+ )
363
+ links = []
364
  if politicians and companies:
365
  politicians = self.resolver.resolve_dataset(politicians, "name")
366
  companies = self.resolver.resolve_dataset(companies, "name")
367
  links = self.find_politician_company_links(politicians, companies)
368
 
369
+ duration = (datetime.now() - start).seconds
370
  per_source = {name: len(records) for name, records in raw.items()}
371
 
372
  summary = {
 
387
  f"{summary['total_raw_records']} total records")
388
 
389
  results = {
390
+ "raw": raw,
391
+ "links": links,
392
+ "summary": summary,
393
+ "run_at": start.isoformat(),
394
  }
395
  results["saved_to"] = self.save(results)
396
  return results
397
 
398
 
399
  if __name__ == "__main__":
400
+ parser = argparse.ArgumentParser(description="BharatGraph Full Pipeline (20 scrapers)")
401
+ parser.add_argument("--scrapers", type=str, default=None)
402
+ parser.add_argument("--parallel", action="store_true", default=True)
403
+ parser.add_argument("--sequential", action="store_true", default=False)
404
+ args = parser.parse_args()
 
 
 
405
  scrapers = args.scrapers.split(",") if args.scrapers else None
406
  parallel = not args.sequential
407
 
 
 
 
 
408
  results = BharatGraphPipeline().run(scrapers=scrapers, parallel=parallel)
409
  s = results["summary"]
410
+ print(f"\n{'='*55}\nPIPELINE SUMMARY\n{'='*55}")
 
 
 
411
  print(f" Scrapers run: {len(s['scrapers_run'])}")
412
  print(f" Duration: {s['duration_seconds']}s")
413
  print(f" Total records: {s['total_raw_records']}")
 
414
  for src, n in sorted(s["per_source"].items(), key=lambda x: -x[1]):
415
  if n > 0:
416
  print(f" {src:<20} {n}")
417
+ print(f" Saved to: {results.get('saved_to')}")
418
+ print("=" * 55)