File size: 7,455 Bytes
9e9393d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""Offline verification for the Scrapling anti-block integration.

Deterministic, no network: validates the fetch-layer shim + proxy rotation, the
Greenhouse/Lever/Ashby ATS parsers (via canned JSON), graceful degradation when
Scrapling isn't installed, and the ui/config/deps wiring. Run:

    python scripts/verify_scrapling_integration.py
"""
import os
import sys
import json
import types

sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
REPO = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))

_ok = True


def check(label, cond, extra=""):
    global _ok
    _ok = _ok and bool(cond)
    print(f"  [{'PASS' if cond else 'FAIL'}] {label}" + (f"  {extra}" if extra else ""))


def read(rel):
    with open(os.path.join(REPO, rel), encoding="utf-8") as f:
        return f.read()


print("=" * 70)
print("Scrapling anti-block integration verification")
print("=" * 70)

# ── [A] fetch layer: shim + graceful import ──────────────────────────────────
print("[A] Fetch layer (requests.Response-compatible shim + graceful degrade)")
from src.scrapers import fetch  # noqa: E402

check("scrapling_available() returns a bool (no import crash)",
      isinstance(fetch.scrapling_available(), bool),
      f"scrapling={fetch.scrapling_available()} stealthy={fetch.stealthy_available()}")

r = fetch.FetchResponse(200, '{"a": 1}', url="http://x")
check("FetchResponse.ok true for 200", r.ok is True)
check("FetchResponse.json() parses body", r.json() == {"a": 1})
check("FetchResponse.content is bytes", isinstance(r.content, bytes))
check("FetchResponse non-200 not ok", fetch.FetchResponse(403, "").ok is False)

# ── [B] proxy rotation (the real anti-IP-block hook) ─────────────────────────
print("[B] Proxy env hook + round-robin rotation")
os.environ["SCRAPER_PROXIES"] = "http://p1:1,http://p2:2"
rotated = [fetch._next_proxy() for _ in range(4)]
check("rotates configured proxies round-robin",
      rotated == ["http://p1:1", "http://p2:2", "http://p1:1", "http://p2:2"],
      str(rotated))
os.environ.pop("SCRAPER_PROXIES", None)
check("no proxy configured -> None", fetch._next_proxy() is None)

# ── [C] ATS parsers (Greenhouse/Lever/Ashby) via canned JSON ─────────────────
print("[C] Company ATS parsers normalize to Job + filter non-PM")
from src.scrapers.company_ats import CompanyATSScraper  # noqa: E402

GH = json.dumps({"jobs": [
    {"id": 1, "title": "Senior Product Manager", "absolute_url": "https://gh/1",
     "updated_at": "2026-06-01T00:00:00Z", "location": {"name": "Bangalore, India"},
     "content": "<p>Own the roadmap</p>"},
    {"id": 2, "title": "Software Engineer", "absolute_url": "https://gh/2",
     "location": {"name": "Remote"}, "content": "x"},
]})
LEVER = json.dumps([
    {"id": "a", "text": "Product Manager, Growth", "hostedUrl": "https://lever/a",
     "categories": {"location": "Remote"}, "descriptionPlain": "Lead growth",
     "createdAt": 1700000000000},
])
ASHBY = json.dumps({"jobs": [
    {"id": "z", "title": "AI Product Manager", "jobUrl": "https://ashby/z",
     "location": "Remote", "descriptionPlain": "Build AI products",
     "publishedAt": "2026-05-01"},
]})


def fake_get(self, url, **kw):
    if "greenhouse" in url:
        return fetch.FetchResponse(200, GH)
    if "lever" in url:
        return fetch.FetchResponse(200, LEVER)
    if "ashby" in url:
        return fetch.FetchResponse(200, ASHBY)
    return None


boards = [
    {"provider": "greenhouse", "token": "x", "company": "GH Co"},
    {"provider": "lever", "token": "y", "company": "Lever Co"},
    {"provider": "ashby", "token": "z", "company": "Ashby Co"},
]
s = CompanyATSScraper(boards=boards)
s._get = types.MethodType(fake_get, s)
jobs = s.search("product manager", "remote", max_results=25)

titles = [j.title for j in jobs]
check("3 PM jobs across 3 providers (engineer filtered out)", len(jobs) == 3, str(titles))
check("non-PM 'Software Engineer' excluded", "Software Engineer" not in titles)
gh = next((j for j in jobs if j.company == "GH Co"), None)
check("greenhouse HTML description stripped + unescaped",
      gh is not None and gh.description == "Own the roadmap",
      gh.description if gh else "none")
lev = next((j for j in jobs if j.company == "Lever Co"), None)
check("lever createdAt(ms) -> YYYY-MM-DD posted_date",
      lev is not None and len(lev.posted_date) == 10 and lev.posted_date.startswith("20"),
      lev.posted_date if lev else "none")
check("all ATS jobs carry platform=CompanyATS + url",
      all(j.platform == "CompanyATS" and j.url for j in jobs))

# location filter rejects mismatched city when a specific location is given
s2 = CompanyATSScraper(boards=[{"provider": "ashby", "token": "z", "company": "Ashby Co"}])
ASHBY_SF = json.dumps({"jobs": [
    {"id": "z", "title": "Product Manager", "jobUrl": "https://ashby/sf",
     "location": "Munich, Germany", "descriptionPlain": "d", "publishedAt": "2026-05-01"},
]})
s2._get = types.MethodType(lambda self, url, **kw: fetch.FetchResponse(200, ASHBY_SF), s2)
check("location filter drops a non-matching city for a specific location",
      len(s2.search("product manager", "Hyderabad, India", 25)) == 0)

# ── [D] base scraper uses the fetch layer ────────────────────────────────────
print("[D] BaseScraper._get routes through the stealth fetch layer")
base_src = read("src/scrapers/base.py")
check("base imports the fetch layer", "from . import fetch" in base_src)
check("base._get calls fetch.get(", "fetch.get(" in base_src)
check("base._get supports use_browser/solve_cloudflare",
      "use_browser" in base_src and "solve_cloudflare" in base_src)

gd_src = read("src/scrapers/glassdoor.py")
check("glassdoor tries StealthyFetcher (Cloudflare) before Playwright",
      "fetch_browser_html" in gd_src)

# ── [E] config + ui + deps wiring ────────────────────────────────────────────
print("[E] config / ui / dependency wiring")
import config  # noqa: E402
check("config.SCRAPER present with proxies + impersonate",
      isinstance(getattr(config, "SCRAPER", None), dict)
      and "proxies" in config.SCRAPER and "impersonate" in config.SCRAPER)
check("config.COMPANY_ATS_BOARDS exists (list)",
      isinstance(getattr(config, "COMPANY_ATS_BOARDS", None), list))

ui_src = read("ui.py")
check("ui adds company_ats to legacy keys (not routed to ever-jobs)",
      '"company_ats"' in ui_src and "_legacy_keys" in ui_src)
check("ui registers CompanyATSScraper", "CompanyATSScraper" in ui_src)

plat_src = read("src/ever_jobs_bridge/platforms.py")
check("platforms registry exposes company_ats option", '"company_ats"' in plat_src)
check("company_ats is an India default platform",
      "company_ats" in plat_src and "INDIA_DEFAULT_PLATFORMS" in plat_src)

reqs = read("requirements.txt")
check("requirements.txt pins scrapling[fetchers]", "scrapling[fetchers]" in reqs)
docker = read("Dockerfile")
check("Dockerfile installs the Camoufox stealth browser", "scrapling install" in docker)

print("-" * 70)
print("PASS - Scrapling integration wired + ATS parsers verified"
      if _ok else "FAIL - see above")
sys.exit(0 if _ok else 1)