switchback 0.6.0__tar.gz → 0.6.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {switchback-0.6.0 → switchback-0.6.1}/CHANGELOG.md +19 -0
- {switchback-0.6.0 → switchback-0.6.1}/PKG-INFO +5 -4
- {switchback-0.6.0 → switchback-0.6.1}/README.md +1 -0
- {switchback-0.6.0 → switchback-0.6.1}/pyproject.toml +4 -4
- {switchback-0.6.0 → switchback-0.6.1}/switchback/tiers/tier_1.py +4 -2
- {switchback-0.6.0 → switchback-0.6.1}/switchback/tiers/tier_2.py +1 -1
- {switchback-0.6.0 → switchback-0.6.1}/switchback/tiers/tier_5.py +10 -8
- {switchback-0.6.0 → switchback-0.6.1}/switchback.egg-info/PKG-INFO +5 -4
- {switchback-0.6.0 → switchback-0.6.1}/switchback.egg-info/requires.txt +3 -3
- {switchback-0.6.0 → switchback-0.6.1}/.env.example +0 -0
- {switchback-0.6.0 → switchback-0.6.1}/CONTRIBUTING.md +0 -0
- {switchback-0.6.0 → switchback-0.6.1}/LICENSE +0 -0
- {switchback-0.6.0 → switchback-0.6.1}/MANIFEST.in +0 -0
- {switchback-0.6.0 → switchback-0.6.1}/NOTICE +0 -0
- {switchback-0.6.0 → switchback-0.6.1}/SECURITY.md +0 -0
- {switchback-0.6.0 → switchback-0.6.1}/clients/node_bridge.md +0 -0
- {switchback-0.6.0 → switchback-0.6.1}/clients/python_client.py +0 -0
- {switchback-0.6.0 → switchback-0.6.1}/config/botwall_skip_urls.txt +0 -0
- {switchback-0.6.0 → switchback-0.6.1}/config/extraction.example.json +0 -0
- {switchback-0.6.0 → switchback-0.6.1}/setup.cfg +0 -0
- {switchback-0.6.0 → switchback-0.6.1}/switchback/__init__.py +0 -0
- {switchback-0.6.0 → switchback-0.6.1}/switchback/__main__.py +0 -0
- {switchback-0.6.0 → switchback-0.6.1}/switchback/api.py +0 -0
- {switchback-0.6.0 → switchback-0.6.1}/switchback/concurrency.py +0 -0
- {switchback-0.6.0 → switchback-0.6.1}/switchback/content_cache.py +0 -0
- {switchback-0.6.0 → switchback-0.6.1}/switchback/doctor.py +0 -0
- {switchback-0.6.0 → switchback-0.6.1}/switchback/egress.py +0 -0
- {switchback-0.6.0 → switchback-0.6.1}/switchback/extract.py +0 -0
- {switchback-0.6.0 → switchback-0.6.1}/switchback/flags.py +0 -0
- {switchback-0.6.0 → switchback-0.6.1}/switchback/highlights.py +0 -0
- {switchback-0.6.0 → switchback-0.6.1}/switchback/mcp_server.py +0 -0
- {switchback-0.6.0 → switchback-0.6.1}/switchback/normalize.py +0 -0
- {switchback-0.6.0 → switchback-0.6.1}/switchback/orchestrator.py +0 -0
- {switchback-0.6.0 → switchback-0.6.1}/switchback/policy/__init__.py +0 -0
- {switchback-0.6.0 → switchback-0.6.1}/switchback/policy/botwall.py +0 -0
- {switchback-0.6.0 → switchback-0.6.1}/switchback/policy/gates.py +0 -0
- {switchback-0.6.0 → switchback-0.6.1}/switchback/py.typed +0 -0
- {switchback-0.6.0 → switchback-0.6.1}/switchback/reporting.py +0 -0
- {switchback-0.6.0 → switchback-0.6.1}/switchback/search.py +0 -0
- {switchback-0.6.0 → switchback-0.6.1}/switchback/server.py +0 -0
- {switchback-0.6.0 → switchback-0.6.1}/switchback/session_cache.py +0 -0
- {switchback-0.6.0 → switchback-0.6.1}/switchback/session_trace.py +0 -0
- {switchback-0.6.0 → switchback-0.6.1}/switchback/state.py +0 -0
- {switchback-0.6.0 → switchback-0.6.1}/switchback/tiers/__init__.py +0 -0
- {switchback-0.6.0 → switchback-0.6.1}/switchback/tiers/_browser.py +0 -0
- {switchback-0.6.0 → switchback-0.6.1}/switchback/tiers/tier_3.py +0 -0
- {switchback-0.6.0 → switchback-0.6.1}/switchback/tiers/tier_4.py +0 -0
- {switchback-0.6.0 → switchback-0.6.1}/switchback/tiers/tier_6.py +0 -0
- {switchback-0.6.0 → switchback-0.6.1}/switchback/tiers/tier_7.py +0 -0
- {switchback-0.6.0 → switchback-0.6.1}/switchback/tracing.py +0 -0
- {switchback-0.6.0 → switchback-0.6.1}/switchback.egg-info/SOURCES.txt +0 -0
- {switchback-0.6.0 → switchback-0.6.1}/switchback.egg-info/dependency_links.txt +0 -0
- {switchback-0.6.0 → switchback-0.6.1}/switchback.egg-info/entry_points.txt +0 -0
- {switchback-0.6.0 → switchback-0.6.1}/switchback.egg-info/top_level.txt +0 -0
|
@@ -4,6 +4,25 @@ All notable changes to this project are documented here. Format loosely follows
|
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/); this project uses semantic-ish
|
|
5
5
|
versioning while pre-1.0.
|
|
6
6
|
|
|
7
|
+
## [0.6.1] - 2026-10-14
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
- **camoufox geoip on fresh installs.** camoufox 0.5 swapped `geoip2` for
|
|
11
|
+
`maxminddb` in its `[geoip]` extra, so tier_5 decided geoip was missing and ran
|
|
12
|
+
proxied sessions without matching locale/timezone. It now asks camoufox's own
|
|
13
|
+
flag, which works on 0.4 and 0.5.
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
- **Newer Chrome TLS fingerprints in tier_2** (`chrome142`, `chrome145`,
|
|
17
|
+
`chrome146` instead of `chrome131`, `chrome136`, `chrome142`); two-year-old
|
|
18
|
+
Chrome versions are a fingerprint tell. Needs `curl_cffi>=0.15`, now the floor.
|
|
19
|
+
- **Security floors:** `pypdf>=6.19` (older releases have DoS CVEs on crafted
|
|
20
|
+
PDFs, which this library parses from the web) and `requests>=2.33`
|
|
21
|
+
(CVE-2024-35195, CVE-2024-47081, CVE-2026-25645).
|
|
22
|
+
- **arXiv calls no longer send the maintainer's email.** tier_1's arXiv
|
|
23
|
+
User-Agent named a fixed contact address on every install; it now names the
|
|
24
|
+
project URL, and `SCRAPER_ARXIV_CONTACT` adds your own address if you want one.
|
|
25
|
+
|
|
7
26
|
## [0.6.0] - 2026-10-14
|
|
8
27
|
|
|
9
28
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: switchback
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.1
|
|
4
4
|
Summary: One cost-ordered scrape cascade (HTTP → stealth browser → paid), shared by every tool.
|
|
5
5
|
Author-email: Akash Kodavuru <akash@theaklabs.com>
|
|
6
6
|
License: MIT
|
|
@@ -27,9 +27,9 @@ License-File: LICENSE
|
|
|
27
27
|
License-File: NOTICE
|
|
28
28
|
Requires-Dist: markdownify
|
|
29
29
|
Requires-Dist: beautifulsoup4
|
|
30
|
-
Requires-Dist: pypdf
|
|
31
|
-
Requires-Dist: requests
|
|
32
|
-
Requires-Dist: curl_cffi
|
|
30
|
+
Requires-Dist: pypdf>=6.19
|
|
31
|
+
Requires-Dist: requests>=2.33
|
|
32
|
+
Requires-Dist: curl_cffi>=0.15
|
|
33
33
|
Provides-Extra: cloudflare
|
|
34
34
|
Requires-Dist: cloudscraper; extra == "cloudflare"
|
|
35
35
|
Provides-Extra: browser
|
|
@@ -398,6 +398,7 @@ export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317
|
|
|
398
398
|
- `SCRAPER_PROXY` — route *all* tiers/URLs through a proxy
|
|
399
399
|
- `SCRAPER_EGRESS_PROXY` — route only walled hosts through a proxy (see [Cost-scoped residential egress](#cost-scoped-residential-egress))
|
|
400
400
|
- `SEARXNG_URL` — defaults to `http://localhost:8888`
|
|
401
|
+
- `SCRAPER_ARXIV_CONTACT` — optional email added to the User-Agent of arXiv API calls (arXiv asks clients to identify themselves; the default names only the project URL)
|
|
401
402
|
- `SCRAPER_STATE_DIR` — where the botwall DB/event log + session cache live
|
|
402
403
|
- `SCRAPER_COOKIES_FILE` — Netscape `cookies.txt` to scrape login-gated hosts (injected into the HTTP and browser tiers)
|
|
403
404
|
- `SCRAPER_CAPTCHA_PROVIDER` + `SCRAPER_CAPTCHA_API_KEY` — opt-in, off by default: wire a third-party solver (2captcha/capsolver/capmonster/anticaptcha/deathbycaptcha/9kw) into tier_3 for Turnstile/reCAPTCHA/hCaptcha on CF hosts. **Paid**, billed per solve by the provider.
|
|
@@ -333,6 +333,7 @@ export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317
|
|
|
333
333
|
- `SCRAPER_PROXY` — route *all* tiers/URLs through a proxy
|
|
334
334
|
- `SCRAPER_EGRESS_PROXY` — route only walled hosts through a proxy (see [Cost-scoped residential egress](#cost-scoped-residential-egress))
|
|
335
335
|
- `SEARXNG_URL` — defaults to `http://localhost:8888`
|
|
336
|
+
- `SCRAPER_ARXIV_CONTACT` — optional email added to the User-Agent of arXiv API calls (arXiv asks clients to identify themselves; the default names only the project URL)
|
|
336
337
|
- `SCRAPER_STATE_DIR` — where the botwall DB/event log + session cache live
|
|
337
338
|
- `SCRAPER_COOKIES_FILE` — Netscape `cookies.txt` to scrape login-gated hosts (injected into the HTTP and browser tiers)
|
|
338
339
|
- `SCRAPER_CAPTCHA_PROVIDER` + `SCRAPER_CAPTCHA_API_KEY` — opt-in, off by default: wire a third-party solver (2captcha/capsolver/capmonster/anticaptcha/deathbycaptcha/9kw) into tier_3 for Turnstile/reCAPTCHA/hCaptcha on CF hosts. **Paid**, billed per solve by the provider.
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "switchback"
|
|
7
|
-
version = "0.6.
|
|
7
|
+
version = "0.6.1"
|
|
8
8
|
description = "One cost-ordered scrape cascade (HTTP → stealth browser → paid), shared by every tool."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -37,9 +37,9 @@ classifiers = [
|
|
|
37
37
|
dependencies = [
|
|
38
38
|
"markdownify",
|
|
39
39
|
"beautifulsoup4",
|
|
40
|
-
"pypdf",
|
|
41
|
-
"requests",
|
|
42
|
-
"curl_cffi",
|
|
40
|
+
"pypdf>=6.19", # older releases hang or blow up memory on crafted PDFs (DoS CVEs)
|
|
41
|
+
"requests>=2.33", # CVE-2024-35195, CVE-2024-47081, CVE-2026-25645
|
|
42
|
+
"curl_cffi>=0.15", # tier_2: TLS-impersonating HTTP (chrome145/146 targets)
|
|
43
43
|
]
|
|
44
44
|
|
|
45
45
|
[project.optional-dependencies]
|
|
@@ -64,9 +64,11 @@ def _arxiv(arxiv_id: str, url: str) -> str:
|
|
|
64
64
|
# arxiv wants plain requests + an identifying UA (their published guidance);
|
|
65
65
|
# impersonating Chrome triggers aggressive 429s from their Akamai front-end.
|
|
66
66
|
import requests
|
|
67
|
+
# The contact is the operator's to give, so it's opt-in; the project URL identifies the client.
|
|
68
|
+
contact = os.getenv("SCRAPER_ARXIV_CONTACT", "").strip()
|
|
69
|
+
ua = f"switchback/1.0 (+https://github.com/akash-kr/switchback{'; mailto:' + contact if contact else ''})"
|
|
67
70
|
r = requests.get(f"https://export.arxiv.org/api/query?id_list={arxiv_id}",
|
|
68
|
-
timeout=_TIMEOUT_S,
|
|
69
|
-
headers={"User-Agent": "switchback/1.0 (mailto:akash@theaklabs.com)"})
|
|
71
|
+
timeout=_TIMEOUT_S, headers={"User-Agent": ua})
|
|
70
72
|
r.raise_for_status()
|
|
71
73
|
ns = {"atom": "http://www.w3.org/2005/Atom"}
|
|
72
74
|
entry = ET.fromstring(r.text).find("atom:entry", ns)
|
|
@@ -37,7 +37,7 @@ _TIMEOUT_S = float(os.getenv("SCRAPER_TIER_2_TIMEOUT_S", "15"))
|
|
|
37
37
|
|
|
38
38
|
# Recent Chrome JA3 targets available in curl_cffi 0.15.x. A small spread of real
|
|
39
39
|
# versions mirrors how live traffic is distributed across Chrome releases.
|
|
40
|
-
_IMPERSONATE_TARGETS = ("
|
|
40
|
+
_IMPERSONATE_TARGETS = ("chrome142", "chrome145", "chrome146")
|
|
41
41
|
|
|
42
42
|
|
|
43
43
|
def _impersonate_for(url: str) -> str:
|
|
@@ -13,6 +13,7 @@ before the paid Firecrawl tier.
|
|
|
13
13
|
"""
|
|
14
14
|
from __future__ import annotations
|
|
15
15
|
|
|
16
|
+
import importlib
|
|
16
17
|
import logging
|
|
17
18
|
import os
|
|
18
19
|
|
|
@@ -43,14 +44,15 @@ def disabled() -> bool:
|
|
|
43
44
|
|
|
44
45
|
|
|
45
46
|
def _geoip_available() -> bool:
|
|
46
|
-
"""camoufox's geoip matching needs
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
47
|
+
"""camoufox's geoip matching needs its camoufox[geoip] extra (`geoip2` before
|
|
48
|
+
0.5, `maxminddb` from 0.5). Without it, requesting geoip raises and kills the
|
|
49
|
+
whole tier — so we ask camoufox itself and degrade gracefully instead."""
|
|
50
|
+
for module in ("camoufox.geolocation", "camoufox.locale"): # 0.5+, then 0.4
|
|
51
|
+
try:
|
|
52
|
+
return bool(importlib.import_module(module).ALLOW_GEOIP)
|
|
53
|
+
except (ImportError, AttributeError):
|
|
54
|
+
continue
|
|
55
|
+
return False
|
|
54
56
|
|
|
55
57
|
|
|
56
58
|
def _launch_opts() -> dict:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: switchback
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.1
|
|
4
4
|
Summary: One cost-ordered scrape cascade (HTTP → stealth browser → paid), shared by every tool.
|
|
5
5
|
Author-email: Akash Kodavuru <akash@theaklabs.com>
|
|
6
6
|
License: MIT
|
|
@@ -27,9 +27,9 @@ License-File: LICENSE
|
|
|
27
27
|
License-File: NOTICE
|
|
28
28
|
Requires-Dist: markdownify
|
|
29
29
|
Requires-Dist: beautifulsoup4
|
|
30
|
-
Requires-Dist: pypdf
|
|
31
|
-
Requires-Dist: requests
|
|
32
|
-
Requires-Dist: curl_cffi
|
|
30
|
+
Requires-Dist: pypdf>=6.19
|
|
31
|
+
Requires-Dist: requests>=2.33
|
|
32
|
+
Requires-Dist: curl_cffi>=0.15
|
|
33
33
|
Provides-Extra: cloudflare
|
|
34
34
|
Requires-Dist: cloudscraper; extra == "cloudflare"
|
|
35
35
|
Provides-Extra: browser
|
|
@@ -398,6 +398,7 @@ export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317
|
|
|
398
398
|
- `SCRAPER_PROXY` — route *all* tiers/URLs through a proxy
|
|
399
399
|
- `SCRAPER_EGRESS_PROXY` — route only walled hosts through a proxy (see [Cost-scoped residential egress](#cost-scoped-residential-egress))
|
|
400
400
|
- `SEARXNG_URL` — defaults to `http://localhost:8888`
|
|
401
|
+
- `SCRAPER_ARXIV_CONTACT` — optional email added to the User-Agent of arXiv API calls (arXiv asks clients to identify themselves; the default names only the project URL)
|
|
401
402
|
- `SCRAPER_STATE_DIR` — where the botwall DB/event log + session cache live
|
|
402
403
|
- `SCRAPER_COOKIES_FILE` — Netscape `cookies.txt` to scrape login-gated hosts (injected into the HTTP and browser tiers)
|
|
403
404
|
- `SCRAPER_CAPTCHA_PROVIDER` + `SCRAPER_CAPTCHA_API_KEY` — opt-in, off by default: wire a third-party solver (2captcha/capsolver/capmonster/anticaptcha/deathbycaptcha/9kw) into tier_3 for Turnstile/reCAPTCHA/hCaptcha on CF hosts. **Paid**, billed per solve by the provider.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|