leadgen-maps 1.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. leadgen_maps-1.1.0/PKG-INFO +116 -0
  2. leadgen_maps-1.1.0/README.md +92 -0
  3. leadgen_maps-1.1.0/pyproject.toml +38 -0
  4. leadgen_maps-1.1.0/setup.cfg +4 -0
  5. leadgen_maps-1.1.0/src/leadgen_maps/__init__.py +10 -0
  6. leadgen_maps-1.1.0/src/leadgen_maps/__main__.py +8 -0
  7. leadgen_maps-1.1.0/src/leadgen_maps/cli.py +226 -0
  8. leadgen_maps-1.1.0/src/leadgen_maps/config.py +74 -0
  9. leadgen_maps-1.1.0/src/leadgen_maps/connect.py +412 -0
  10. leadgen_maps-1.1.0/src/leadgen_maps/connectors/__init__.py +30 -0
  11. leadgen_maps-1.1.0/src/leadgen_maps/connectors/base.py +40 -0
  12. leadgen_maps-1.1.0/src/leadgen_maps/connectors/csv_out.py +58 -0
  13. leadgen_maps-1.1.0/src/leadgen_maps/connectors/gsheets.py +147 -0
  14. leadgen_maps-1.1.0/src/leadgen_maps/connectors/notion.py +214 -0
  15. leadgen_maps-1.1.0/src/leadgen_maps/connectors/pdf_out.py +92 -0
  16. leadgen_maps-1.1.0/src/leadgen_maps/connectors/xlsx_out.py +92 -0
  17. leadgen_maps-1.1.0/src/leadgen_maps/credstore.py +98 -0
  18. leadgen_maps-1.1.0/src/leadgen_maps/engine.py +188 -0
  19. leadgen_maps-1.1.0/src/leadgen_maps/extract.py +257 -0
  20. leadgen_maps-1.1.0/src/leadgen_maps/fields.py +121 -0
  21. leadgen_maps-1.1.0/src/leadgen_maps/logo.svg +1 -0
  22. leadgen_maps-1.1.0/src/leadgen_maps/mcp_server.py +311 -0
  23. leadgen_maps-1.1.0/src/leadgen_maps/net.py +27 -0
  24. leadgen_maps-1.1.0/src/leadgen_maps/notify.py +38 -0
  25. leadgen_maps-1.1.0/src/leadgen_maps/oauthapp.py +60 -0
  26. leadgen_maps-1.1.0/src/leadgen_maps/progress.py +106 -0
  27. leadgen_maps-1.1.0/src/leadgen_maps.egg-info/PKG-INFO +116 -0
  28. leadgen_maps-1.1.0/src/leadgen_maps.egg-info/SOURCES.txt +30 -0
  29. leadgen_maps-1.1.0/src/leadgen_maps.egg-info/dependency_links.txt +1 -0
  30. leadgen_maps-1.1.0/src/leadgen_maps.egg-info/entry_points.txt +4 -0
  31. leadgen_maps-1.1.0/src/leadgen_maps.egg-info/requires.txt +15 -0
  32. leadgen_maps-1.1.0/src/leadgen_maps.egg-info/top_level.txt +1 -0
@@ -0,0 +1,116 @@
1
+ Metadata-Version: 2.4
2
+ Name: leadgen-maps
3
+ Version: 1.1.0
4
+ Summary: Apollo-class local lead generation from Google Maps β€” no API. Streams leads to Notion, Google Sheets, or CSV with live progress and zero duplicates.
5
+ Author: Auradevs
6
+ License: Proprietary
7
+ Project-URL: Homepage, https://github.com/subhadeeproy3902/lead-gen
8
+ Project-URL: Repository, https://github.com/subhadeeproy3902/lead-gen
9
+ Keywords: lead-generation,google-maps,scraper,notion,apollo,outreach,automation
10
+ Requires-Python: >=3.9
11
+ Description-Content-Type: text/markdown
12
+ Requires-Dist: playwright>=1.49
13
+ Requires-Dist: openpyxl>=3.1
14
+ Requires-Dist: fpdf2>=2.7
15
+ Provides-Extra: sheets
16
+ Requires-Dist: gspread>=6.0; extra == "sheets"
17
+ Requires-Dist: google-auth>=2.0; extra == "sheets"
18
+ Provides-Extra: mcp
19
+ Requires-Dist: mcp>=1.0; extra == "mcp"
20
+ Provides-Extra: all
21
+ Requires-Dist: gspread>=6.0; extra == "all"
22
+ Requires-Dist: google-auth>=2.0; extra == "all"
23
+ Requires-Dist: mcp>=1.0; extra == "all"
24
+
25
+ # leadgen πŸ—ΊοΈ β†’ πŸ“‡
26
+
27
+ **Apollo-class local lead generation from Google Maps. No API. Streams leads
28
+ straight into Notion or CSV β€” with live progress and zero duplicates.**
29
+
30
+ Ships in **two interchangeable editions** with the *same* CLI and the *same*
31
+ 41-field Apollo-grade output:
32
+
33
+ | Edition | Best for | Install |
34
+ |---|---|---|
35
+ | 🐍 **Python** (`src/leadgen`) | Notion + **Google Sheets** + CSV + **XLSX/PDF**, scripting | `pipx install leadgen-maps` |
36
+ | 🐹 **Go** (`go/`) | a single zero-dependency binary, terminal installs | `go install …` |
37
+
38
+ > Better than Apollo for local SMBs: phone, website, socials, **geo-coordinates,
39
+ > Google Place ID, rating, reviews, hours, photos, Lead Score** β€” and a
40
+ > no-website filter to find your prime prospects. Apollo has none of that for a
41
+ > neighbourhood business.
42
+
43
+ ---
44
+
45
+ ## πŸ”Œ Connect Notion in one click β€” no keys, ever
46
+ You **never paste keys into `.env`**. The first time you send leads to Notion,
47
+ your browser opens, you authorize once, and the token is cached **only on your
48
+ computer** (`~/.leadgen/credentials.json`). Every run after that is silent.
49
+ ```bash
50
+ leadgen connect notion # opens the browser, authorize once β†’ done
51
+ # or just run β€” it auto-connects the first time:
52
+ leadgen run --niche "cafe" --location "Kolkata" --limit 30 --to notion
53
+ ```
54
+ Google Sheets is the same β€” `leadgen connect google` opens the browser, you click
55
+ **Allow**, and we **auto-create a Sheet** in your Drive:
56
+ ```bash
57
+ leadgen connect google
58
+ leadgen run --niche "cafe" --location "Kolkata" --limit 30 --to gsheets
59
+ ```
60
+ Switch accounts or log out any time:
61
+ ```bash
62
+ leadgen disconnect notion | google | all # removes the local token
63
+ ```
64
+ See **[CONNECT.md](CONNECT.md)** for the one-time OAuth setup (Notion + Google)
65
+ and uninstall steps.
66
+
67
+ ## 🐍 Python edition
68
+ ```bash
69
+ pipx install leadgen-maps # public on PyPI β€” repo stays private
70
+ python -m playwright install chromium
71
+ leadgen connect notion # one-time browser authorize (no .env)
72
+ leadgen run --niche "cafe" --location "Kolkata" --limit 30 --to notion
73
+ ```
74
+ Connectors: **Notion, Google Sheets, CSV, XLSX, PDF**. Hosting + email on
75
+ completion in [`deploy/`](deploy/).
76
+
77
+ ## 🐹 Go edition (single binary)
78
+ ```bash
79
+ go install github.com/subhadeeproy3902/lead-gen/go/cmd/leadgen@latest
80
+
81
+ leadgen connect notion # one-time browser authorize (no .env)
82
+ leadgen run --niche "cafe" --location "Kolkata" --limit 30 --to notion
83
+ ```
84
+ Connectors: **Notion, CSV, XLSX, PDF**. Drives your installed Chrome. See [`go/README.md`](go/README.md).
85
+
86
+ ---
87
+
88
+ ## Same CLI, both editions
89
+ ```bash
90
+ leadgen run --niche "<type>" --location "<city>" --limit <N> --to notion --website without
91
+ leadgen doctor # internet, connectors, browser
92
+ leadgen fields # the 41-field Apollo-grade schema
93
+ ```
94
+ ```
95
+ [β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–‘β–‘β–‘β–‘β–‘β–‘] 21/30 leads (70%) β”‚ #54 of 110 scanned β”‚ ETA 1m20s β”‚ Blue Tokai
96
+ ```
97
+
98
+ ## Let AI run it (Claude Code AND claude.ai)
99
+ Ships an **MCP server** so any MCP-capable AI can call leadgen as a tool:
100
+ - **Claude Code** β€” the included `.mcp.json` exposes it automatically; just ask "pull 15 gyms in Mumbai".
101
+ - **claude.ai (website)** β€” run `python -m leadgen_maps.mcp_server --http`, expose it with a free tunnel, add it as a custom connector β†’ the website executes leadgen on your machine.
102
+
103
+ Exact steps: **[CONNECT-CLAUDE.md](CONNECT-CLAUDE.md)**. The plain CLI (`--json`
104
+ events) and the agent skill ([`skills/leadgen/SKILL.md`](skills/leadgen/SKILL.md))
105
+ also work.
106
+
107
+ ## Docs
108
+ - **[CONNECT-CLAUDE.md](CONNECT-CLAUDE.md)** β€” let Claude Code + claude.ai run leadgen (MCP connector).
109
+ - **[deploy/HOSTING-VPS.md](deploy/HOSTING-VPS.md)** β€” host a public, multi-tenant MCP server on a free VPS (e.g. `leadgen.auradevs.co/mcp`).
110
+ - **[BUYER-GUIDE.md](BUYER-GUIDE.md)** β€” bought it? Dead-simple per-OS setup (Windows/Mac/Linux).
111
+ - **[DOCUMENTATION.md](DOCUMENTATION.md)** β€” full guide (install, connectors, fields, hosting, edge cases).
112
+ - **[DISTRIBUTION.md](DISTRIBUTION.md)** β€” install channels (release binary, `go install`, Homebrew, Scoop, winget).
113
+ - **[ABOUT.md](ABOUT.md)** β€” what it is and why Β· **[prompt.md](prompt.md)** β€” paste into ChatGPT to drive it.
114
+ - **[TESTS.md](TESTS.md)** β€” real runs with captured output + screenshots.
115
+
116
+ Built by Auradevs.
@@ -0,0 +1,92 @@
1
+ # leadgen πŸ—ΊοΈ β†’ πŸ“‡
2
+
3
+ **Apollo-class local lead generation from Google Maps. No API. Streams leads
4
+ straight into Notion or CSV β€” with live progress and zero duplicates.**
5
+
6
+ Ships in **two interchangeable editions** with the *same* CLI and the *same*
7
+ 41-field Apollo-grade output:
8
+
9
+ | Edition | Best for | Install |
10
+ |---|---|---|
11
+ | 🐍 **Python** (`src/leadgen`) | Notion + **Google Sheets** + CSV + **XLSX/PDF**, scripting | `pipx install leadgen-maps` |
12
+ | 🐹 **Go** (`go/`) | a single zero-dependency binary, terminal installs | `go install …` |
13
+
14
+ > Better than Apollo for local SMBs: phone, website, socials, **geo-coordinates,
15
+ > Google Place ID, rating, reviews, hours, photos, Lead Score** β€” and a
16
+ > no-website filter to find your prime prospects. Apollo has none of that for a
17
+ > neighbourhood business.
18
+
19
+ ---
20
+
21
+ ## πŸ”Œ Connect Notion in one click β€” no keys, ever
22
+ You **never paste keys into `.env`**. The first time you send leads to Notion,
23
+ your browser opens, you authorize once, and the token is cached **only on your
24
+ computer** (`~/.leadgen/credentials.json`). Every run after that is silent.
25
+ ```bash
26
+ leadgen connect notion # opens the browser, authorize once β†’ done
27
+ # or just run β€” it auto-connects the first time:
28
+ leadgen run --niche "cafe" --location "Kolkata" --limit 30 --to notion
29
+ ```
30
+ Google Sheets is the same β€” `leadgen connect google` opens the browser, you click
31
+ **Allow**, and we **auto-create a Sheet** in your Drive:
32
+ ```bash
33
+ leadgen connect google
34
+ leadgen run --niche "cafe" --location "Kolkata" --limit 30 --to gsheets
35
+ ```
36
+ Switch accounts or log out any time:
37
+ ```bash
38
+ leadgen disconnect notion | google | all # removes the local token
39
+ ```
40
+ See **[CONNECT.md](CONNECT.md)** for the one-time OAuth setup (Notion + Google)
41
+ and uninstall steps.
42
+
43
+ ## 🐍 Python edition
44
+ ```bash
45
+ pipx install leadgen-maps # public on PyPI β€” repo stays private
46
+ python -m playwright install chromium
47
+ leadgen connect notion # one-time browser authorize (no .env)
48
+ leadgen run --niche "cafe" --location "Kolkata" --limit 30 --to notion
49
+ ```
50
+ Connectors: **Notion, Google Sheets, CSV, XLSX, PDF**. Hosting + email on
51
+ completion in [`deploy/`](deploy/).
52
+
53
+ ## 🐹 Go edition (single binary)
54
+ ```bash
55
+ go install github.com/subhadeeproy3902/lead-gen/go/cmd/leadgen@latest
56
+
57
+ leadgen connect notion # one-time browser authorize (no .env)
58
+ leadgen run --niche "cafe" --location "Kolkata" --limit 30 --to notion
59
+ ```
60
+ Connectors: **Notion, CSV, XLSX, PDF**. Drives your installed Chrome. See [`go/README.md`](go/README.md).
61
+
62
+ ---
63
+
64
+ ## Same CLI, both editions
65
+ ```bash
66
+ leadgen run --niche "<type>" --location "<city>" --limit <N> --to notion --website without
67
+ leadgen doctor # internet, connectors, browser
68
+ leadgen fields # the 41-field Apollo-grade schema
69
+ ```
70
+ ```
71
+ [β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–‘β–‘β–‘β–‘β–‘β–‘] 21/30 leads (70%) β”‚ #54 of 110 scanned β”‚ ETA 1m20s β”‚ Blue Tokai
72
+ ```
73
+
74
+ ## Let AI run it (Claude Code AND claude.ai)
75
+ Ships an **MCP server** so any MCP-capable AI can call leadgen as a tool:
76
+ - **Claude Code** β€” the included `.mcp.json` exposes it automatically; just ask "pull 15 gyms in Mumbai".
77
+ - **claude.ai (website)** β€” run `python -m leadgen_maps.mcp_server --http`, expose it with a free tunnel, add it as a custom connector β†’ the website executes leadgen on your machine.
78
+
79
+ Exact steps: **[CONNECT-CLAUDE.md](CONNECT-CLAUDE.md)**. The plain CLI (`--json`
80
+ events) and the agent skill ([`skills/leadgen/SKILL.md`](skills/leadgen/SKILL.md))
81
+ also work.
82
+
83
+ ## Docs
84
+ - **[CONNECT-CLAUDE.md](CONNECT-CLAUDE.md)** β€” let Claude Code + claude.ai run leadgen (MCP connector).
85
+ - **[deploy/HOSTING-VPS.md](deploy/HOSTING-VPS.md)** β€” host a public, multi-tenant MCP server on a free VPS (e.g. `leadgen.auradevs.co/mcp`).
86
+ - **[BUYER-GUIDE.md](BUYER-GUIDE.md)** β€” bought it? Dead-simple per-OS setup (Windows/Mac/Linux).
87
+ - **[DOCUMENTATION.md](DOCUMENTATION.md)** β€” full guide (install, connectors, fields, hosting, edge cases).
88
+ - **[DISTRIBUTION.md](DISTRIBUTION.md)** β€” install channels (release binary, `go install`, Homebrew, Scoop, winget).
89
+ - **[ABOUT.md](ABOUT.md)** β€” what it is and why Β· **[prompt.md](prompt.md)** β€” paste into ChatGPT to drive it.
90
+ - **[TESTS.md](TESTS.md)** β€” real runs with captured output + screenshots.
91
+
92
+ Built by Auradevs.
@@ -0,0 +1,38 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "leadgen-maps"
7
+ version = "1.1.0"
8
+ description = "Apollo-class local lead generation from Google Maps β€” no API. Streams leads to Notion, Google Sheets, or CSV with live progress and zero duplicates."
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = { text = "Proprietary" }
12
+ authors = [{ name = "Auradevs" }]
13
+ keywords = ["lead-generation", "google-maps", "scraper", "notion", "apollo", "outreach", "automation"]
14
+ dependencies = [
15
+ "playwright>=1.49",
16
+ "openpyxl>=3.1",
17
+ "fpdf2>=2.7",
18
+ ]
19
+
20
+ [project.optional-dependencies]
21
+ sheets = ["gspread>=6.0", "google-auth>=2.0"]
22
+ mcp = ["mcp>=1.0"]
23
+ all = ["gspread>=6.0", "google-auth>=2.0", "mcp>=1.0"]
24
+
25
+ [project.scripts]
26
+ leadgen = "leadgen_maps.cli:main"
27
+ leadgen-maps = "leadgen_maps.cli:main"
28
+ leadgen-mcp = "leadgen_maps.mcp_server:main"
29
+
30
+ [project.urls]
31
+ Homepage = "https://github.com/subhadeeproy3902/lead-gen"
32
+ Repository = "https://github.com/subhadeeproy3902/lead-gen"
33
+
34
+ [tool.setuptools.packages.find]
35
+ where = ["src"]
36
+
37
+ [tool.setuptools.package-data]
38
+ leadgen_maps = ["py.typed", "logo.svg"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,10 @@
1
+ """
2
+ leadgen β€” Apollo-class local lead generation from Google Maps, no API.
3
+
4
+ Scrapes Google Maps via real browser automation, extracts a rich Apollo-grade
5
+ field set, and streams leads directly into your connector of choice (Notion,
6
+ Google Sheets, or CSV) with live progress and guaranteed de-duplication.
7
+ """
8
+
9
+ __version__ = "1.0.0"
10
+ __all__ = ["__version__"]
@@ -0,0 +1,8 @@
1
+ """Enable `python -m leadgen_maps ...`."""
2
+
3
+ import sys
4
+
5
+ from .cli import main
6
+
7
+ if __name__ == "__main__":
8
+ sys.exit(main())
@@ -0,0 +1,226 @@
1
+ """
2
+ leadgen command-line interface.
3
+
4
+ leadgen run --niche "cafe" --location "Kolkata" --limit 30 --to notion
5
+ leadgen run --niche "dentist" --location "Pune" --limit 50 --website any --to csv,notion
6
+ leadgen doctor # check internet, connectors, license
7
+ leadgen fields # list the Apollo-grade field set
8
+ leadgen connectors # list available push targets
9
+
10
+ AI agents: add --json to stream machine-readable progress + a final summary
11
+ object on stdout (one JSON event per line).
12
+ """
13
+
14
+ import argparse
15
+ import json
16
+ import sys
17
+
18
+ from . import __version__, config, fields, net
19
+
20
+
21
+ def _err(msg, json_mode=False):
22
+ if json_mode:
23
+ sys.stdout.write(json.dumps({"event": "fatal", "error": msg}) + "\n")
24
+ else:
25
+ sys.stderr.write(f"\nERROR: {msg}\n")
26
+ return 2
27
+
28
+
29
+ def cmd_run(args):
30
+ to = [t.strip() for t in args.to.split(",") if t.strip()]
31
+ columns = fields.select_columns(args.fields)
32
+
33
+ # Keyless connect: if a destination is requested but not linked yet, open the
34
+ # browser and connect once β€” no .env, no keys.
35
+ if not args.json:
36
+ lowered = [t.lower() for t in to]
37
+ if "notion" in lowered:
38
+ from .connectors.notion import notion_connected
39
+ if not notion_connected():
40
+ from .connect import connect_notion
41
+ print("[leadgen] Notion isn't connected yet β€” let's fix that once.")
42
+ try:
43
+ connect_notion()
44
+ except Exception as e:
45
+ return _err(f"connect notion: {e}", args.json)
46
+ print("[leadgen] βœ“ Notion connected. Continuing your run…")
47
+ if any(t in lowered for t in ("gsheets", "sheets", "google")):
48
+ from .connectors.gsheets import gsheets_connected
49
+ if not gsheets_connected():
50
+ from .connect import connect_google
51
+ print("[leadgen] Google Sheets isn't connected yet β€” let's fix that once.")
52
+ try:
53
+ connect_google()
54
+ except Exception as e:
55
+ return _err(f"connect google: {e}", args.json)
56
+ print("[leadgen] βœ“ Google Sheets connected. Continuing your run…")
57
+
58
+ try:
59
+ from .engine import run
60
+ summary = run(
61
+ niche=args.niche, location=args.location, limit=args.limit,
62
+ website=args.website, to=to, columns=columns, country_code=args.cc,
63
+ headless=not args.show, want_reviews=not args.no_reviews,
64
+ json_mode=args.json, notify_email=args.email,
65
+ )
66
+ except net.OfflineError as e:
67
+ return _err(str(e), args.json)
68
+ except Exception as e:
69
+ return _err(f"{type(e).__name__}: {e}", args.json)
70
+
71
+ if args.json:
72
+ sys.stdout.write(json.dumps({"event": "summary", **summary}) + "\n")
73
+ else:
74
+ print(f"\n{'='*58}")
75
+ print(f" βœ“ {summary['kept']} new leads (target {summary['target']})")
76
+ print(f" scanned {summary['queued']} listings Β· "
77
+ f"{summary['skipped_filtered']} filtered Β· "
78
+ f"{summary['skipped_duplicate']} duplicate Β· "
79
+ f"{summary['skipped_no_phone']} no-phone")
80
+ for name, loc in summary["connectors"].items():
81
+ print(f" β†’ {name}: {loc}")
82
+ if summary.get("emailed"):
83
+ print(f" βœ‰ emailed {summary['emailed']}")
84
+ print(f"{'='*58}")
85
+ return 0
86
+
87
+
88
+ def cmd_connect(args):
89
+ target = (args.target or "notion").lower()
90
+ if target == "notion":
91
+ from .connect import connect_notion
92
+ try:
93
+ info = connect_notion(allow_paste=getattr(args, "paste", False))
94
+ except Exception as e:
95
+ return _err(str(e))
96
+ ws = info.get("workspace_name") or "your workspace"
97
+ print(f"\n[leadgen] βœ“ Connected to {ws}. Leads database is ready.")
98
+ print('[leadgen] Try: leadgen run --niche "cafe" --location "Kolkata" --limit 10 --to notion')
99
+ return 0
100
+ if target in ("google", "gsheets", "sheets"):
101
+ from .connect import connect_google
102
+ try:
103
+ info = connect_google()
104
+ except Exception as e:
105
+ return _err(str(e))
106
+ print(f"\n[leadgen] βœ“ Connected Google Sheets. Your sheet:")
107
+ print(f" https://docs.google.com/spreadsheets/d/{info['spreadsheet_id']}")
108
+ print('[leadgen] Try: leadgen run --niche "cafe" --location "Kolkata" --limit 10 --to gsheets')
109
+ return 0
110
+ return _err(f"don't know how to connect '{target}' (supported: notion, google)")
111
+
112
+
113
+ def cmd_disconnect(args):
114
+ target = (args.target or "all").lower()
115
+ from . import credstore
116
+ if target == "notion":
117
+ credstore.clear("notion"); what = "Notion"
118
+ elif target in ("google", "gsheets", "sheets"):
119
+ credstore.clear("google"); what = "Google Sheets"
120
+ elif target in ("all", ""):
121
+ credstore.clear_all(); what = "all connections"
122
+ else:
123
+ return _err(f"don't know how to disconnect '{target}' (supported: notion, google, all)")
124
+ print(f"[leadgen] βœ“ Disconnected {what}. Re-link any time with `leadgen connect`.")
125
+ return 0
126
+
127
+
128
+ def cmd_doctor(args):
129
+ print("leadgen doctor")
130
+ print(f" version : {__version__}")
131
+ online = net.is_online()
132
+ print(f" internet : {'OK' if online else 'OFFLINE (required!)'}")
133
+ from .connectors.notion import notion_connected
134
+ from .connectors.gsheets import gsheets_connected
135
+ print(f" Notion connected : {'yes (relink: leadgen connect notion)' if notion_connected() else 'no β€” run `leadgen connect notion`'}")
136
+ print(f" Google Sheets : {'connected (relink: leadgen connect google)' if gsheets_connected() else 'not connected β€” run `leadgen connect google`'}")
137
+ print(f" SMTP (email) : {'configured' if config.SMTP_HOST else 'not configured'}")
138
+ try:
139
+ import playwright # noqa: F401
140
+ print(" Playwright : installed")
141
+ except ImportError:
142
+ print(" Playwright : MISSING β€” run `python -m playwright install chromium`")
143
+ return 0 if online else 1
144
+
145
+
146
+ def cmd_fields(args):
147
+ if args.json:
148
+ out = [{"key": k, "label": fields.LABELS[k], "type": fields.TYPES[k],
149
+ "group": fields.GROUPS[k], "source": fields.SOURCE[k]} for k in fields.KEYS]
150
+ print(json.dumps(out, indent=2)); return 0
151
+ group = None
152
+ for k in fields.KEYS:
153
+ if fields.GROUPS[k] != group:
154
+ group = fields.GROUPS[k]
155
+ print(f"\n{group}")
156
+ print(f" {k:<16} {fields.LABELS[k]:<20} {fields.TYPES[k]:<12} [{fields.SOURCE[k]}]")
157
+ print(f"\n{len(fields.KEYS)} fields. Use --fields default|all|apollo|<comma,list>")
158
+ return 0
159
+
160
+
161
+ def cmd_connectors(args):
162
+ from .connectors import NAMES
163
+ print("Available connectors (--to):")
164
+ print(" notion β€” Notion database (rich pages, photos, reviews) Β· `leadgen connect notion`")
165
+ print(" gsheets β€” Google Sheets (keyless OAuth) Β· `leadgen connect google`")
166
+ print(" csv β€” local CSV table view")
167
+ print(" xlsx β€” local Excel workbook (.xlsx)")
168
+ print(" pdf β€” local printable lead sheet (.pdf)")
169
+ print("\nCombine with commas, e.g. --to notion,gsheets,xlsx,pdf")
170
+ return 0
171
+
172
+
173
+ def build_parser():
174
+ p = argparse.ArgumentParser(prog="leadgen", description="Apollo-class local lead-gen from Google Maps (no API).")
175
+ p.add_argument("--version", action="version", version=f"leadgen {__version__}")
176
+ sub = p.add_subparsers(dest="cmd")
177
+
178
+ r = sub.add_parser("run", help="run a lead-gen job")
179
+ r.add_argument("--niche", required=True, help='business type, e.g. "cafe"')
180
+ r.add_argument("--location", required=True, help='city/area, e.g. "Kolkata"')
181
+ r.add_argument("--limit", type=int, default=30, help=f"how many NEW leads (max {config.MAX_LIMIT})")
182
+ r.add_argument("--website", choices=["without", "with", "any"], default="without",
183
+ help="keep businesses without a website (default), with one, or any")
184
+ r.add_argument("--to", default="notion", help="connector(s), comma list: notion,gsheets,csv,xlsx,pdf")
185
+ r.add_argument("--fields", default="default", help="default|all|apollo|<comma,list of keys>")
186
+ r.add_argument("--cc", default=None, help="country calling code (default 91 β†’ Region India)")
187
+ r.add_argument("--show", action="store_true", help="show the browser window")
188
+ r.add_argument("--no-reviews", action="store_true", help="skip review snippets (faster)")
189
+ r.add_argument("--email", default=None, help="email a summary when the job finishes")
190
+ r.add_argument("--json", action="store_true", help="stream JSON events (for AI agents)")
191
+ r.set_defaults(func=cmd_run)
192
+
193
+ cn = sub.add_parser("connect", help="connect a destination (keyless, browser-based)")
194
+ cn.add_argument("target", nargs="?", default="notion", help="what to connect: notion | google")
195
+ cn.add_argument("--paste", action="store_true", help="(Notion) paste an integration token once instead of OAuth")
196
+ cn.set_defaults(func=cmd_connect)
197
+
198
+ dc = sub.add_parser("disconnect", help="remove a stored connection (logout) β€” notion | google | all")
199
+ dc.add_argument("target", nargs="?", default="all", help="what to disconnect: notion | google | all")
200
+ dc.set_defaults(func=cmd_disconnect)
201
+
202
+ d = sub.add_parser("doctor", help="check environment, connectors, license")
203
+ d.set_defaults(func=cmd_doctor)
204
+
205
+ f = sub.add_parser("fields", help="list the field set")
206
+ f.add_argument("--json", action="store_true")
207
+ f.set_defaults(func=cmd_fields)
208
+
209
+ c = sub.add_parser("connectors", help="list push targets")
210
+ c.set_defaults(func=cmd_connectors)
211
+ return p
212
+
213
+
214
+ def main(argv=None):
215
+ parser = build_parser()
216
+ args = parser.parse_args(argv)
217
+ if not getattr(args, "cmd", None):
218
+ parser.print_help()
219
+ return 0
220
+ if not hasattr(args, "json"):
221
+ args.json = False
222
+ return args.func(args)
223
+
224
+
225
+ if __name__ == "__main__":
226
+ sys.exit(main())
@@ -0,0 +1,74 @@
1
+ """Configuration + .env loading for leadgen. Secrets never live in code."""
2
+
3
+ import os
4
+
5
+ MAX_LIMIT = 256 # hard cap on leads per run (product constraint)
6
+
7
+
8
+ def load_dotenv(explicit=None):
9
+ """Load KEY=VALUE pairs from the first .env found (cwd, then ~/.leadgen)."""
10
+ candidates = []
11
+ if explicit:
12
+ candidates.append(explicit)
13
+ d = os.getcwd()
14
+ for _ in range(6): # walk up to find a shared root .env (works from subfolders)
15
+ candidates.append(os.path.join(d, ".env"))
16
+ parent = os.path.dirname(d)
17
+ if parent == d:
18
+ break
19
+ d = parent
20
+ candidates.append(os.path.expanduser(os.path.join("~", ".leadgen", ".env")))
21
+ for path in candidates:
22
+ if path and os.path.exists(path):
23
+ with open(path, encoding="utf-8") as f:
24
+ for line in f:
25
+ line = line.strip()
26
+ if not line or line.startswith("#") or "=" not in line:
27
+ continue
28
+ k, v = line.split("=", 1)
29
+ os.environ.setdefault(k.strip(), v.strip().strip('"').strip("'"))
30
+ return path
31
+ return None
32
+
33
+
34
+ load_dotenv()
35
+
36
+
37
+ def get(name, default=""):
38
+ return os.environ.get(name, default)
39
+
40
+
41
+ def get_bool(name, default=False):
42
+ v = os.environ.get(name)
43
+ if v is None:
44
+ return default
45
+ return v.strip().lower() in ("1", "true", "yes", "on")
46
+
47
+
48
+ def get_float(name, default):
49
+ try:
50
+ return float(os.environ.get(name, default))
51
+ except (TypeError, ValueError):
52
+ return default
53
+
54
+
55
+ # ── Connectors ───────────────────────────────────────────────────────────────
56
+ NOTION_TOKEN = get("NOTION_TOKEN")
57
+ NOTION_DATABASE_ID = get("NOTION_DATABASE_ID")
58
+
59
+ GOOGLE_SERVICE_ACCOUNT_JSON = get("GOOGLE_SERVICE_ACCOUNT_JSON") # path to creds file
60
+ GSHEET_ID = get("GSHEET_ID")
61
+ GSHEET_TAB = get("GSHEET_TAB", "Leads")
62
+
63
+ # ── Notifications (email on completion) ──────────────────────────────────────
64
+ SMTP_HOST = get("SMTP_HOST")
65
+ SMTP_PORT = int(get("SMTP_PORT", "587") or 587)
66
+ SMTP_USER = get("SMTP_USER")
67
+ SMTP_PASS = get("SMTP_PASS")
68
+ NOTIFY_EMAIL = get("NOTIFY_EMAIL")
69
+
70
+ # ── Scrape defaults ──────────────────────────────────────────────────────────
71
+ DEFAULT_COUNTRY_CODE = get("DEFAULT_COUNTRY_CODE", "91")
72
+ HEADLESS = get_bool("HEADLESS", True)
73
+ MIN_DELAY = get_float("MIN_DELAY", 1.0)
74
+ MAX_DELAY = get_float("MAX_DELAY", 2.5)