JAA-ATS-Tool / scripts /verify_scrapling_integration.py
saitejatirunagari's picture
feat: Scrapling anti-block fetch layer + Direct Company ATS source
9e9393d
Raw
History Blame
7.46 kB
"""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)