File size: 7,279 Bytes
44a2092
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9e547ed
 
44a2092
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9e547ed
 
 
 
 
44a2092
 
9e547ed
44a2092
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9e547ed
 
44a2092
 
 
 
 
 
 
 
 
 
 
 
 
9e547ed
 
 
44a2092
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
"""

trigger.py β€” push & run the scraper kernel on Kaggle from the dashboard.



Fire-and-forget: `kaggle kernels push` uploads a generated runner script and

Kaggle executes it immediately on their servers. The kernel pulls the project

code from the attached Kaggle Dataset (gmaps-scraper-code), scrapes, scores,

and pushes leads.csv to the HF Dataset repo. The dashboard never waits for it.



Credentials (Space settings):

    KAGGLE_USERNAME  variable β€” kaggle.com username

    KAGGLE_KEY       secret   β€” API key from kaggle.com/settings -> Create New Token

    HF_TOKEN         secret   β€” must be a WRITE token (the kernel uses it to upload)



Security note: the HF token is embedded in the generated kernel source, so the

kernel MUST stay private (is_private is forced to true below).

"""

import json
import os
import re
import subprocess
import tempfile
from pathlib import Path

KERNEL_SLUG = "gmaps-scraper-auto"      # kernel created/updated by the trigger
CODE_DATASET = "gmaps-scraper-code"     # Kaggle Dataset holding the project .py files

RUNNER_TEMPLATE = '''"""Auto-generated by the dashboard trigger. Do not edit by hand."""

import glob

import os

import shutil

import subprocess

import sys



os.environ["HF_TOKEN"] = "__HF_TOKEN__"

os.environ["HF_DATASET_REPO"] = "__HF_REPO__"



KEYWORD = "__KEYWORD__"

CITY = "__CITY__"

MAX_RESULTS = __MAX_RESULTS__

WITH_DETAILS = __WITH_DETAILS__

MIN_REVIEWS = __MIN_REVIEWS__

MIN_RATING = __MIN_RATING__

ONLY_NEW = __ONLY_NEW__

MAX_REVIEWS = __MAX_REVIEWS__

MAX_PHOTOS = __MAX_PHOTOS__





def sh(cmd, check=True):

    print(f"$ {cmd}")

    rc = subprocess.run(cmd, shell=True).returncode

    if check and rc != 0:

        sys.exit(rc)

    return rc





# 1. Copy the project code from the attached Kaggle Dataset (any mount layout)

REQUIRED = ("scraper.py", "process.py", "enrich.py", "push_to_hf.py")

for path in glob.glob("/kaggle/input/**/*.py", recursive=True):

    if os.path.basename(path) in REQUIRED:

        shutil.copy(path, ".")

        print(f"Copied {path}")



# 2. Install dependencies

sh("pip install -q playwright pandas requests beautifulsoup4 huggingface_hub")

sh("playwright install --with-deps chromium")



# 3. Scrape β€” never fatal: captcha/blocks leave a partial CSV behind

details = "--details" if WITH_DETAILS else ""

extra = ""

if WITH_DETAILS and MAX_REVIEWS:

    extra += f" --max-reviews {MAX_REVIEWS}"

if WITH_DETAILS and MAX_PHOTOS:

    extra += f" --max-photos {MAX_PHOTOS}"

sh(f'python scraper.py --keyword "{KEYWORD}" --city "{CITY}" '

   f"--max-results {MAX_RESULTS} --min-rating {MIN_RATING} "

   f"--min-reviews {MIN_REVIEWS} {details}{extra}", check=False)



# A killed run may only have the checkpoint file β€” promote it

if not os.path.exists("data/leads.csv") and os.path.exists("data/leads_partial.csv"):

    shutil.copy("data/leads_partial.csv", "data/leads.csv")

    print("Recovered leads from the partial checkpoint.")

if not os.path.exists("data/leads.csv"):

    print("No CSV produced (blocked before anything was scraped) β€” nothing to push.")

    sys.exit(0)



# 4. Score (+ optionally drop leads already in the HF dataset)

dedupe = "--dedupe-hf" if ONLY_NEW else ""

sh(f"python process.py --min-reviews {MIN_REVIEWS} --min-rating {MIN_RATING} {dedupe}")



# 5. Push whatever we got (an empty run is skipped, not an error)

sh(f'python push_to_hf.py --file data/leads_scored.csv --label "{KEYWORD} {CITY}"',

   check=False)

print("DONE β€” check the run list in the dashboard.")

'''


def _env(username: str, key: str) -> dict:
    """Provide Kaggle credentials via env vars AND ~/.kaggle/kaggle.json.



    Different kaggle CLI versions look in different places, so cover both.

    """
    env = os.environ.copy()
    env["KAGGLE_USERNAME"] = username
    env["KAGGLE_KEY"] = key

    cfg_dir = Path.home() / ".kaggle"
    cfg_file = cfg_dir / "kaggle.json"
    try:
        cfg_dir.mkdir(parents=True, exist_ok=True)
        cfg_file.write_text(json.dumps({"username": username, "key": key}),
                            encoding="utf-8")
        cfg_file.chmod(0o600)
    except OSError:
        pass  # read-only home: env vars alone still work on the pinned CLI

    return env


def _sanitize(text: str) -> str:
    """Keep kernel source injection-proof: strip quotes/backslashes/newlines."""
    return re.sub(r'["\'\\\n\r]', " ", text).strip()


def trigger_scrape(*, keyword: str, city: str, max_results: int,

                   with_details: bool, min_reviews: int,

                   hf_repo: str, hf_token: str,

                   kaggle_username: str, kaggle_key: str,

                   min_rating: float = 0.0, only_new: bool = False,

                   max_reviews: int = 0, max_photos: int = 0) -> str:
    """Generate the kernel folder and push it to Kaggle. Returns CLI output."""
    if not hf_token:
        raise ValueError("HF_TOKEN is missing β€” it must be a WRITE token.")

    runner = (RUNNER_TEMPLATE
              .replace("__HF_TOKEN__", hf_token)
              .replace("__HF_REPO__", _sanitize(hf_repo))
              .replace("__KEYWORD__", _sanitize(keyword))
              .replace("__CITY__", _sanitize(city))
              .replace("__MAX_RESULTS__", str(int(max_results)))
              .replace("__WITH_DETAILS__", str(bool(with_details)))
              .replace("__MIN_REVIEWS__", str(int(min_reviews)))
              .replace("__MIN_RATING__", str(float(min_rating)))
              .replace("__ONLY_NEW__", str(bool(only_new)))
              .replace("__MAX_REVIEWS__", str(int(max_reviews)))
              .replace("__MAX_PHOTOS__", str(int(max_photos))))

    metadata = {
        "id": f"{kaggle_username}/{KERNEL_SLUG}",
        "title": KERNEL_SLUG,
        "code_file": "runner.py",
        "language": "python",
        "kernel_type": "script",
        "is_private": True,               # the HF token lives in the source β€” keep private
        "enable_internet": True,          # required: Maps + HF upload
        "enable_gpu": False,
        "dataset_sources": [f"{kaggle_username}/{CODE_DATASET}"],
        "competition_sources": [],
        "kernel_sources": [],
    }

    with tempfile.TemporaryDirectory() as d:
        Path(d, "runner.py").write_text(runner, encoding="utf-8")
        Path(d, "kernel-metadata.json").write_text(
            json.dumps(metadata, indent=1), encoding="utf-8")

        result = subprocess.run(
            ["kaggle", "kernels", "push", "-p", d],
            capture_output=True, text=True, timeout=120,
            env=_env(kaggle_username, kaggle_key),
        )

    if result.returncode != 0:
        raise RuntimeError(result.stderr.strip() or result.stdout.strip())
    return result.stdout.strip()


def kernel_status(kaggle_username: str, kaggle_key: str) -> str:
    """Ask Kaggle for the current status of the triggered kernel."""
    result = subprocess.run(
        ["kaggle", "kernels", "status", f"{kaggle_username}/{KERNEL_SLUG}"],
        capture_output=True, text=True, timeout=60,
        env=_env(kaggle_username, kaggle_key),
    )
    return (result.stdout + result.stderr).strip()