Spaces:
Sleeping
Sleeping
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)
|