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