scrapefold 0.1.0a2__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.
Files changed (81) hide show
  1. scrapefold-0.1.0a2/.github/workflows/ci.yml +131 -0
  2. scrapefold-0.1.0a2/.gitignore +48 -0
  3. scrapefold-0.1.0a2/.python-version +1 -0
  4. scrapefold-0.1.0a2/AGENTS.md +34 -0
  5. scrapefold-0.1.0a2/CHANGELOG.md +147 -0
  6. scrapefold-0.1.0a2/CLAUDE.md +48 -0
  7. scrapefold-0.1.0a2/CONTRIBUTING.md +78 -0
  8. scrapefold-0.1.0a2/LICENSE +21 -0
  9. scrapefold-0.1.0a2/PKG-INFO +183 -0
  10. scrapefold-0.1.0a2/README.md +103 -0
  11. scrapefold-0.1.0a2/docs/README.md +51 -0
  12. scrapefold-0.1.0a2/docs/TECH_DEBT.md +106 -0
  13. scrapefold-0.1.0a2/docs/architecture/overview.md +230 -0
  14. scrapefold-0.1.0a2/docs/conventions/golden-rules.md +107 -0
  15. scrapefold-0.1.0a2/docs/post-1.0/backlog.md +38 -0
  16. scrapefold-0.1.0a2/docs/superpowers/plans/2026-05-23-pack-3-router-cloudflare-version-fix.md +1420 -0
  17. scrapefold-0.1.0a2/docs/superpowers/plans/2026-05-23-pack-4-crawler-module.md +1509 -0
  18. scrapefold-0.1.0a2/docs/superpowers/plans/2026-05-23-pack-5-disk-cache-client-reuse.md +1447 -0
  19. scrapefold-0.1.0a2/docs/superpowers/plans/2026-05-23-pack-6-typer-cli.md +524 -0
  20. scrapefold-0.1.0a2/docs/superpowers/plans/2026-05-23-pack-7-consumer-adoption-rc.md +265 -0
  21. scrapefold-0.1.0a2/docs/superpowers/plans/2026-05-23-pack-8-v0.1.0-release.md +326 -0
  22. scrapefold-0.1.0a2/docs/superpowers/specs/2026-05-23-v0.1.0-stable-roadmap-design.md +486 -0
  23. scrapefold-0.1.0a2/docs/tools/agent-mode.md +82 -0
  24. scrapefold-0.1.0a2/docs/tools/scripts.md +52 -0
  25. scrapefold-0.1.0a2/docs/workflows/development.md +93 -0
  26. scrapefold-0.1.0a2/docs/workflows/testing.md +75 -0
  27. scrapefold-0.1.0a2/pyproject.toml +119 -0
  28. scrapefold-0.1.0a2/scripts/check-changelog.sh +50 -0
  29. scrapefold-0.1.0a2/scripts/check-deps-fresh.sh +124 -0
  30. scrapefold-0.1.0a2/scripts/check.sh +28 -0
  31. scrapefold-0.1.0a2/scripts/describe.sh +36 -0
  32. scrapefold-0.1.0a2/scripts/quick-test.sh +39 -0
  33. scrapefold-0.1.0a2/src/scrapefold/__init__.py +67 -0
  34. scrapefold-0.1.0a2/src/scrapefold/cli.py +67 -0
  35. scrapefold-0.1.0a2/src/scrapefold/detection.py +206 -0
  36. scrapefold-0.1.0a2/src/scrapefold/engines/__init__.py +123 -0
  37. scrapefold-0.1.0a2/src/scrapefold/engines/anysite.py +191 -0
  38. scrapefold-0.1.0a2/src/scrapefold/engines/apify_linkedin.py +159 -0
  39. scrapefold-0.1.0a2/src/scrapefold/engines/base.py +213 -0
  40. scrapefold-0.1.0a2/src/scrapefold/engines/cloakbrowser.py +238 -0
  41. scrapefold-0.1.0a2/src/scrapefold/engines/cloudflare.py +225 -0
  42. scrapefold-0.1.0a2/src/scrapefold/engines/crawl4ai.py +245 -0
  43. scrapefold-0.1.0a2/src/scrapefold/engines/firecrawl.py +265 -0
  44. scrapefold-0.1.0a2/src/scrapefold/engines/jina.py +178 -0
  45. scrapefold-0.1.0a2/src/scrapefold/engines/outscraper.py +153 -0
  46. scrapefold-0.1.0a2/src/scrapefold/engines/requests.py +120 -0
  47. scrapefold-0.1.0a2/src/scrapefold/engines/scrapingbee.py +213 -0
  48. scrapefold-0.1.0a2/src/scrapefold/engines/scrapingdog.py +114 -0
  49. scrapefold-0.1.0a2/src/scrapefold/engines/scrapling_fast.py +145 -0
  50. scrapefold-0.1.0a2/src/scrapefold/engines/scrapling_stealth.py +168 -0
  51. scrapefold-0.1.0a2/src/scrapefold/engines/selenium.py +203 -0
  52. scrapefold-0.1.0a2/src/scrapefold/html_to_text.py +222 -0
  53. scrapefold-0.1.0a2/src/scrapefold/ladders.py +599 -0
  54. scrapefold-0.1.0a2/src/scrapefold/mcp_server.py +33 -0
  55. scrapefold-0.1.0a2/src/scrapefold/options.py +163 -0
  56. scrapefold-0.1.0a2/src/scrapefold/py.typed +0 -0
  57. scrapefold-0.1.0a2/src/scrapefold/result.py +95 -0
  58. scrapefold-0.1.0a2/src/scrapefold/router.py +386 -0
  59. scrapefold-0.1.0a2/src/scrapefold/vision.py +135 -0
  60. scrapefold-0.1.0a2/tests/conftest.py +37 -0
  61. scrapefold-0.1.0a2/tests/test_detection.py +313 -0
  62. scrapefold-0.1.0a2/tests/test_engine_anysite.py +398 -0
  63. scrapefold-0.1.0a2/tests/test_engine_apify_linkedin.py +329 -0
  64. scrapefold-0.1.0a2/tests/test_engine_base.py +96 -0
  65. scrapefold-0.1.0a2/tests/test_engine_cloakbrowser.py +368 -0
  66. scrapefold-0.1.0a2/tests/test_engine_cloudflare.py +363 -0
  67. scrapefold-0.1.0a2/tests/test_engine_crawl4ai.py +456 -0
  68. scrapefold-0.1.0a2/tests/test_engine_firecrawl.py +329 -0
  69. scrapefold-0.1.0a2/tests/test_engine_jina.py +359 -0
  70. scrapefold-0.1.0a2/tests/test_engine_outscraper.py +309 -0
  71. scrapefold-0.1.0a2/tests/test_engine_requests.py +353 -0
  72. scrapefold-0.1.0a2/tests/test_engine_scrapingbee.py +318 -0
  73. scrapefold-0.1.0a2/tests/test_engine_scrapingdog.py +303 -0
  74. scrapefold-0.1.0a2/tests/test_engine_scrapling_fast.py +238 -0
  75. scrapefold-0.1.0a2/tests/test_engine_scrapling_stealth.py +269 -0
  76. scrapefold-0.1.0a2/tests/test_engine_selenium.py +304 -0
  77. scrapefold-0.1.0a2/tests/test_html_to_text.py +407 -0
  78. scrapefold-0.1.0a2/tests/test_ladders.py +415 -0
  79. scrapefold-0.1.0a2/tests/test_router.py +1674 -0
  80. scrapefold-0.1.0a2/tests/test_smoke.py +151 -0
  81. scrapefold-0.1.0a2/tests/test_vision.py +185 -0
@@ -0,0 +1,131 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ tags: ['v*']
7
+ pull_request:
8
+ branches: [main]
9
+ workflow_dispatch:
10
+ inputs:
11
+ run_paid:
12
+ description: 'Also run tests marked "paid" (uses real vendor API keys)'
13
+ type: boolean
14
+ default: false
15
+ run_network:
16
+ description: 'Also run tests marked "network" (hits live internet)'
17
+ type: boolean
18
+ default: false
19
+
20
+ concurrency:
21
+ group: ${{ github.workflow }}-${{ github.ref }}
22
+ cancel-in-progress: true
23
+
24
+ jobs:
25
+ test:
26
+ name: Test (py${{ matrix.python-version }})
27
+ runs-on: ubuntu-latest
28
+ timeout-minutes: 20
29
+ strategy:
30
+ fail-fast: false
31
+ matrix:
32
+ python-version: ['3.10', '3.11', '3.12']
33
+ steps:
34
+ - uses: actions/checkout@v4
35
+ with:
36
+ fetch-depth: 0
37
+
38
+ - name: Set up Python
39
+ uses: actions/setup-python@v5
40
+ with:
41
+ python-version: ${{ matrix.python-version }}
42
+ cache: pip
43
+
44
+ - name: Install
45
+ run: |
46
+ python -m pip install --upgrade pip
47
+ pip install -e ".[test]"
48
+
49
+ - name: Changelog gate (skips on main pushes)
50
+ if: github.event_name == 'pull_request'
51
+ run: bash scripts/check-changelog.sh "origin/${{ github.base_ref }}"
52
+
53
+ - name: Ruff lint
54
+ run: ruff check src tests
55
+
56
+ - name: Ruff format
57
+ run: ruff format --check src tests
58
+
59
+ - name: Mypy
60
+ run: mypy src
61
+
62
+ - name: Pytest — offline default
63
+ run: pytest -m "not paid and not network" --maxfail=3
64
+
65
+ - name: Pytest — network (manual dispatch only)
66
+ if: ${{ github.event_name == 'workflow_dispatch' && inputs.run_network == true }}
67
+ run: pytest -m network --maxfail=3
68
+
69
+ - name: Pytest — paid (manual dispatch only)
70
+ if: ${{ github.event_name == 'workflow_dispatch' && inputs.run_paid == true }}
71
+ env:
72
+ FIRECRAWL_API_KEY: ${{ secrets.FIRECRAWL_API_KEY }}
73
+ SCRAPINGBEE_API_KEY: ${{ secrets.SCRAPINGBEE_API_KEY }}
74
+ SCRAPINGDOG_API_KEY: ${{ secrets.SCRAPINGDOG_API_KEY }}
75
+ JINA_API_KEY: ${{ secrets.JINA_API_KEY }}
76
+ OUTSCRAPER_API_KEY: ${{ secrets.OUTSCRAPER_API_KEY }}
77
+ APIFY_API_TOKEN: ${{ secrets.APIFY_API_TOKEN }}
78
+ BRIGHTDATA_API_KEY: ${{ secrets.BRIGHTDATA_API_KEY }}
79
+ ANYSITE_API_KEY: ${{ secrets.ANYSITE_API_KEY }}
80
+ run: pytest -m paid --maxfail=3
81
+
82
+ release-rehearsal:
83
+ name: Release rehearsal (build + twine check + install)
84
+ runs-on: ubuntu-latest
85
+ timeout-minutes: 10
86
+ needs: test
87
+ steps:
88
+ - uses: actions/checkout@v4
89
+
90
+ - name: Set up Python
91
+ uses: actions/setup-python@v5
92
+ with:
93
+ python-version: '3.11'
94
+
95
+ - name: Install build + twine
96
+ run: pip install build twine
97
+
98
+ - name: Build sdist + wheel
99
+ run: python -m build
100
+
101
+ - name: twine check
102
+ run: python -m twine check dist/*
103
+
104
+ - name: Install built wheel in a clean env
105
+ run: pip install dist/*.whl
106
+
107
+ - name: Smoke-import installed package
108
+ run: python -c "import scrapefold; print(scrapefold.__version__)"
109
+
110
+ publish:
111
+ name: Publish to PyPI
112
+ needs: [test, release-rehearsal]
113
+ runs-on: ubuntu-latest
114
+ if: startsWith(github.ref, 'refs/tags/v')
115
+ permissions:
116
+ id-token: write # trusted publishing
117
+ steps:
118
+ - uses: actions/checkout@v4
119
+
120
+ - name: Set up Python
121
+ uses: actions/setup-python@v5
122
+ with:
123
+ python-version: '3.11'
124
+
125
+ - name: Build
126
+ run: |
127
+ python -m pip install --upgrade pip build
128
+ python -m build
129
+
130
+ - name: Publish
131
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,48 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ *.so
6
+ .Python
7
+ build/
8
+ dist/
9
+ *.egg-info/
10
+ *.egg
11
+ .eggs/
12
+ .installed.cfg
13
+ MANIFEST
14
+
15
+ # Environments
16
+ .venv/
17
+ venv/
18
+ env/
19
+ .env
20
+ .env.local
21
+ .env.*.local
22
+
23
+ # Tooling
24
+ .pytest_cache/
25
+ .mypy_cache/
26
+ .ruff_cache/
27
+ .coverage
28
+ htmlcov/
29
+ .tox/
30
+ .hypothesis/
31
+
32
+ # Editors
33
+ .vscode/
34
+ .idea/
35
+ *.swp
36
+ *.swo
37
+ .DS_Store
38
+
39
+ # Project-specific
40
+ .scrapefold/
41
+ ~/.scrapefold/
42
+ *.cache
43
+ Data/
44
+
45
+ # Secrets
46
+ *.pem
47
+ *.key
48
+ secrets.json
@@ -0,0 +1 @@
1
+ 3.11
@@ -0,0 +1,34 @@
1
+ # AGENTS.md
2
+
3
+ Entry point for non-Claude AI agents (Codex, Cursor, custom orchestrators) working on this repo.
4
+
5
+ This file mirrors [CLAUDE.md](CLAUDE.md) — pointers, not content. Read the docs/ graph for everything.
6
+
7
+ ## What this project is
8
+
9
+ `scrapefold` — unified Python library for web scraping with one async API across 16 engines and a built-in anti-bot escalation ladder.
10
+
11
+ ## Start here
12
+
13
+ 1. [docs/README.md](docs/README.md) — index of all docs
14
+ 2. [docs/conventions/golden-rules.md](docs/conventions/golden-rules.md) — the constraints
15
+ 3. [CONTRIBUTING.md](CONTRIBUTING.md) — how to add an engine
16
+
17
+ ## Run / test / lint
18
+
19
+ ```bash
20
+ pip install -e ".[test]"
21
+ ./scripts/check.sh # lint + type-check + offline tests
22
+ pytest -m "not paid and not network" # default suite
23
+ ```
24
+
25
+ ## Conventions
26
+
27
+ - All code under `src/scrapefold/`. Tests under `tests/`. Docs under `docs/`. Scripts under `scripts/`.
28
+ - Async-everywhere. `httpx` for HTTP, `asyncio` for concurrency. No `requests` package.
29
+ - No vendor LLM SDK imports anywhere in `src/`. LLM is an injected callable.
30
+ - Engines are lazy-imported via `src/scrapefold/engines/__init__.py` registry.
31
+
32
+ ## Output
33
+
34
+ Default CLI output is human-readable. Pass `--json` everywhere for machine-parseable output. Errors are fatal (non-zero exit + clear stderr message).
@@ -0,0 +1,147 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Versioning follows [SemVer](https://semver.org/).
6
+
7
+ ## [Unreleased]
8
+
9
+ ## [0.1.0a2] - 2026-05-24
10
+
11
+ Pack 3 — sequential router shell + Cloudflare Browser Rendering engine + comprehensive cost/policy/timeout enforcement. The public `scrape()` path is live for sequential ladders and explicit `opts.engines` overrides; the router now walks `RaceStep` members sequentially (parallel fan-out remains deferred to v0.2). `AllEnginesFailed` is now a structured exception with `.url` and `.failures` attributes. Release mechanics scaffolded: dynamic version from `__init__.py`, dep-freshness audit script, CHANGELOG gate.
12
+
13
+ ### Changed — dependency floors refreshed
14
+
15
+ - Bumped lower-bound pins to current PyPI stable (pack-opening
16
+ freshness policy from spec §4.4). Affected pins:
17
+ - `tldextract` 5.0 → 5.3
18
+ - `beautifulsoup4` 4.12 → 4.14
19
+ - `typer` 0.12 → 0.25
20
+ - `firecrawl-py` 4.0 → 4.27
21
+ - `selenium` 4.25 → 4.44
22
+ - `apify-client` 2.2 → 3.0
23
+ - `mcp` 1.0 → 1.27
24
+ - `pytest` 7.0 → 9.0
25
+ - `pytest-asyncio` 0.21 → 1.3
26
+ - `pytest-httpx` 0.30 → 0.36
27
+ - `ruff` 0.4 → 0.15
28
+ - `mypy` 1.8 → 2.1
29
+
30
+ ### Changed — structured AllEnginesFailed (consumer error contract)
31
+
32
+ - `AllEnginesFailed` now carries `.url: str` and `.failures: list[str]`.
33
+ Consumers no longer need to parse the exception message. The
34
+ `failures` list shape is `"<engine>:<reason>:<detail>"` (e.g.
35
+ `"firecrawl:error:404 Not Found"`, `"jina:empty"`,
36
+ `"scrapingbee:unavailable"`, `"budget:cost"`). Pack 7 consumer
37
+ migrations (phynder + ai-utils) target this contract directly.
38
+
39
+ ### Fixed — golden-rule violation: router now consults detection.is_suspicious
40
+
41
+ - The sequential router shell originally only checked `result.is_empty()`
42
+ for advancement — a captcha page with non-empty text would have been
43
+ returned as success. Per `docs/conventions/golden-rules.md` ("Suspicious-
44
+ content detection lives in one place"), the router now calls
45
+ `scrapefold.detection.is_suspicious(result)` after the empty check and
46
+ advances on True with a `"<engine>:suspicious"` failure entry.
47
+ - Internal: `_resolve_policy` site_class arg typed as `SiteClass` instead
48
+ of `str`; load-bearing `type: ignore[arg-type]` removed.
49
+
50
+ ### Fixed — detection: is_suspicious short-text check is now conjoint
51
+
52
+ - `detection.is_suspicious` previously flagged any result with fewer than
53
+ 50 chars regardless of HTTP status. Now short-text is only suspicious
54
+ when the status code is also non-2xx or when the result is empty,
55
+ eliminating false positives for legitimately short pages (e.g. API
56
+ endpoints returning `{"ok":true}`).
57
+
58
+ ### Added — Pack 3 cloudflare engine
59
+
60
+ - `src/scrapefold/engines/cloudflare.py` — port of ai-utils
61
+ `get_content_cloudflare` to scrapefold's `ScrapeEngine` ABC. Calls
62
+ Cloudflare Browser Rendering `/markdown` first (native markdown), falls
63
+ back to `/content` (raw HTML → html_to_text). Env: `CLOUDFLARE_API_TOKEN`
64
+ + `CLOUDFLARE_ACCOUNT_ID`.
65
+ - `tests/test_engine_cloudflare.py` — 13 tests covering both endpoints,
66
+ fallback paths, auth headers, body shape, is_available gating,
67
+ registry registration.
68
+ - `docs/workflows/development.md` — env-var table fixed
69
+ (`CLOUDFLARE_API_KEY` → `CLOUDFLARE_API_TOKEN`, matching Cloudflare's
70
+ Bearer-token convention).
71
+
72
+ ### Added — Pack 3 router shell (sequential)
73
+
74
+ - `src/scrapefold/router.py` — `async walk(url, opts) -> ScrapeResult` walks the per-site-class ladder. Honors `Policy` (paid_allowed / legal_constraints_blocked / geography_required), `WalkBudget` ceilings (`max_engines`, `max_cost_usd`, `timeout_s`), and the `engines_tried` dedup set. `RaceStep` entries are walked sequentially with a DEBUG log until Pack 9.
75
+ - `tests/test_router.py` — 13 tests covering happy path, empty-result advance, EngineError advance, unavailable-engine skip, AllEnginesFailed, unknown-engine skip, policy gating, budget halt, RaceStep skip, public `scrape()` delegation, failures-list, no-retry-within-walk, EngineError-non-propagation.
76
+ - `scrapefold.scrape(url, opts)` now delegates to `router.walk` instead of raising `NotImplementedError`.
77
+ - `tests/test_smoke.py` — the obsolete `NotImplementedError` smoke test is removed.
78
+ - Budget enforcement: 12 rounds of Codex review hardening. Key fixes: cost-budget skips engine (not halts walk), unavailable engines don't consume `max_engines` slot, timeout boundary uses `>=`, actual cost credits over estimate, `opts.engines` override respects all budget/policy gates, geography `()` means global.
79
+
80
+ ### Added — release mechanics scaffolding
81
+
82
+ - `scripts/check-deps-fresh.sh` — pack-opening dep-floor nag: parses `pyproject.toml` lower bounds and warns when a dependency is more than 90 days behind PyPI stable.
83
+ - `scripts/check-changelog.sh` — PR-time gate: fails if `## [Unreleased]` is empty (no changes documented), scoped to the `[Unreleased]` section only.
84
+ - `scripts/check.sh` — version-equality gate added: reads dynamic version from `pyproject.toml` (via `tomli`) and asserts it matches `__init__.__version__`; prevents version drift between the two sources.
85
+ - `pyproject.toml` — `tomli` added as Python 3.10 compat dep for the version check; broken `obscura` and `brightdata` optional extras removed.
86
+
87
+ ## [0.1.0a1] — 2026-05-22
88
+
89
+ ### Added — S1.5 per-site-class escalation ladders
90
+
91
+ - `src/scrapefold/ladders.py` — full v3 design after three Codex review rounds.
92
+ - `SiteClass` literal with 27 classes (LinkedIn ×5, Amazon ×2, social ×4, SERP ×3, easy content ×4, paywall_news, yandex_protected, anti-bot vendor ×4, js_spa, static_general).
93
+ - Sum type for steps: `SequentialStep` + `RaceStep` (no more weak tuple of tuples).
94
+ - `RaceStep` carries explicit `winner_policy`, `cancel_policy`, `cancel_grace_ms`, `budget_accounting` — race semantics are data, not router convention.
95
+ - `LADDERS: dict[SiteClass, Ladder]` — per-class ordered tuple of steps.
96
+ - `WalkBudget` — mutable walk state (elapsed_ms, cost_usd, engines_tried, **visited_site_classes** to block A→B→A reclassification loops, reclassifications, `MAX_RECLASSIFICATIONS=3`).
97
+ - `BudgetExceeded` + `AllEnginesFailed` — typed exceptions surfaced to the router.
98
+ - `Policy` + `DEFAULT_POLICY` — `paid_allowed`, `legal_constraints_blocked`, `geography_required`. Government class defaults to `paid_allowed=False`.
99
+ - `is_step_allowed(step, policy)` and `check_budget(step, walk, ...)` — pure functions consumed by the router. `check_budget` accounts for **race fan-out** in the engine-count ceiling (Codex round-3 fix).
100
+ - `_estimate_step_cost(step, avg_response_mb)` — converts `(estimated_cost_usd, billing_unit)` into per-call USD; `gb` billing scales with response size.
101
+ - `URL_PATTERNS` + `classify_url` — ordered specific-first regex table, fallback `static_general`.
102
+ - `GOLDEN_CORPUS` — 22 `(url, class)` rows pinning regex-order safety. The parametrized test `test_url_classification_golden_corpus` makes any reorder regression visible per-row.
103
+ - `SIGNATURES` — response-content matchers (Cloudflare / Datadome / PerimeterX / Akamai cookies, body phrases, headers) for mid-walk reclassification by `detection.py` (S2). `min_matches=2` prevents single-phrase false positives.
104
+ - `src/scrapefold/engines/base.py` — extended `EngineCapabilities` with `estimated_cost_usd`, `billing_unit`, `avg_response_mb_estimate`, `geography`, `proxy_type`, `legal_constraints`, `default_timeout_s`. Added `PROBE_SCOPE: Literal["none", "per_url", "per_domain", "per_session"]` and default `async def probe(url) -> bool` returning `True`. Reddit's requests-engine override (`PROBE_SCOPE="per_domain"`) means a 50-URL crawl on reddit.com costs one probe, not 50.
105
+ - `src/scrapefold/engines/__init__.py` — `ENGINE_ALIASES`, `register_alias`, `resolve_alias`. Multi-mode engines (Bright Data Unlocker async/sync, Scrapling stealth/fast) register as distinct names; user-facing aliases route a bare `scrapling` to `scrapling_stealth`.
106
+ - `src/scrapefold/__init__.py` — re-exports `Policy`, `SequentialStep`, `RaceStep`, `WalkBudget`, `SiteClass`, `BudgetExceeded`, `AllEnginesFailed`, `classify_url`, `get_ladder`. Version bumped to `0.1.0a1`.
107
+ - `tests/test_ladders.py` — 59 tests covering structural well-formedness, 22-row golden corpus + completeness, `is_step_allowed` policy enforcement (paid/legal/geography), `check_budget` ceilings + race fan-out, sum-type defaults, multi-mode engine separation, `WalkBudget` visited-class loop guard, `_estimate_step_cost` billing-unit math.
108
+
109
+ ### Changed
110
+
111
+ - Architecture overview (`docs/architecture/overview.md`) — replaced universal T0-T5 ladder narrative with per-class ladder description, race-step semantics, walk-time contracts.
112
+ - Golden rules (`docs/conventions/golden-rules.md`) — rewrote the escalation rule, added three new rules: "Ladders are data", "Multi-mode engines register as distinct names", "New URL pattern → new GOLDEN_CORPUS row".
113
+
114
+ ### Deferred
115
+
116
+ Seven Codex round-3 implementation items tracked in `docs/TECH_DEBT.md`:
117
+
118
+ 1. `budget_mode` wiring in the router (S7).
119
+ 2. Race fan-out cost crediting when `budget_accounting="sum_all"`.
120
+ 3. Per-engine `avg_response_mb` override wiring through `_estimate_step_cost`.
121
+ 4. Race billing default re-examination once benchmarks land.
122
+ 5. `avg_response_mb_estimate` default tuning per engine.
123
+ 6. Engine-registration code must populate `ENGINE_ALIASES` (S2-S11).
124
+ 7. Probe-cache implementation in the router (S7).
125
+
126
+ ## [0.1.0a0] — 2026-05-22
127
+
128
+ ### Added — S1 scaffold
129
+
130
+ - Package layout: `src/scrapefold/` with `options.py`, `result.py`, `engines/base.py`, lazy engine registry.
131
+ - `ScrapeOptions` dataclass — single unified parameter schema (language, country, render_js, wait_ms, stealth, premium_proxy, user_agent, custom_headers, cookies, output_format, take_screenshot, max_pages, max_depth, engines, parallel, timeout_s, skip_cache, extra).
132
+ - `ScrapeResult` dataclass — four format slots (`text`, `markdown`, `html`, `json`) + `screenshot_b64`, `meta`, `failures`, `elapsed_ms`, `cost_usd`.
133
+ - `ScrapeEngine` ABC with `EngineCapabilities` + `EngineError`. Base class handles option-stripping (DEBUG log, never raises), timing, error wrapping.
134
+ - Public `scrape()` and `crawl_site()` stubs in `__init__.py` (raise `NotImplementedError` until S7/S8).
135
+ - CLI scaffold (`scrapefold` console script via Typer).
136
+ - MCP server scaffold (`scrapefold-mcp` console script).
137
+ - Docs graph per HARNESS_BOOTSTRAP: `docs/README.md`, `docs/architecture/overview.md`, `docs/workflows/{development,testing}.md`, `docs/conventions/golden-rules.md`, `docs/tools/{agent-mode,scripts}.md`.
138
+ - Anti-bot escalation ladder documented (T0→T4 + stop rules + suspicious-content detection heuristics planned in `detection.py`).
139
+ - Golden rules: unified options, options-dropping-never-raises, ScrapeResult invariant (all four slots), escalate-and-stop, suspicious-detection-in-one-place, no vendor LLM SDK, lazy engine imports, async everywhere.
140
+ - Helper scripts: `scripts/check.sh`, `scripts/describe.sh`, `scripts/quick-test.sh`.
141
+ - GitHub Actions CI: lint + type-check + offline tests on Python 3.10/3.11/3.12; PyPI publish via trusted publishing on `v*` tag; opt-in `paid` and `network` test jobs via `workflow_dispatch`.
142
+ - Smoke tests + `ScrapeEngine` ABC contract tests.
143
+
144
+ [Unreleased]: https://github.com/mihailorama/scrapefold/compare/v0.1.0a2...HEAD
145
+ [0.1.0a2]: https://github.com/mihailorama/scrapefold/compare/v0.1.0a1...v0.1.0a2
146
+ [0.1.0a1]: https://github.com/mihailorama/scrapefold/compare/v0.1.0a0...v0.1.0a1
147
+ [0.1.0a0]: https://github.com/mihailorama/scrapefold/releases/tag/v0.1.0a0
@@ -0,0 +1,48 @@
1
+ # scrapefold
2
+
3
+ Unified Python library for web scraping — single URL or whole-site → markdown, with stealth, JS rendering, and LLM-ready output.
4
+
5
+ ## Quick Start
6
+
7
+ ```bash
8
+ # Run tests (offline)
9
+ pytest -m "not paid and not network"
10
+
11
+ # Pre-commit gate
12
+ ./scripts/check.sh
13
+
14
+ # Project state snapshot
15
+ ./scripts/describe.sh
16
+ ```
17
+
18
+ ## Key Files
19
+
20
+ | File / Dir | Purpose |
21
+ |---|---|
22
+ | [docs/](docs/) | Full documentation graph — start with [docs/README.md](docs/README.md) |
23
+ | [docs/conventions/golden-rules.md](docs/conventions/golden-rules.md) | Rules you MUST follow |
24
+ | [docs/architecture/overview.md](docs/architecture/overview.md) | Module map, data flow, escalation ladder, result format slots |
25
+ | [docs/workflows/development.md](docs/workflows/development.md) | Dev env setup |
26
+ | [docs/workflows/testing.md](docs/workflows/testing.md) | Test markers, layered plan |
27
+ | [docs/tools/agent-mode.md](docs/tools/agent-mode.md) | CLI / MCP for AI agents |
28
+ | [CONTRIBUTING.md](CONTRIBUTING.md) | How to add a new engine (5-step checklist) |
29
+ | `src/scrapefold/options.py` | `ScrapeOptions` — unified parameter schema |
30
+ | `src/scrapefold/result.py` | `ScrapeResult` — text/markdown/html/json slots |
31
+ | `src/scrapefold/engines/base.py` | `ScrapeEngine` ABC + `EngineCapabilities` |
32
+ | `scripts/check.sh` | Lint + type-check + offline tests |
33
+
34
+ ## Golden Rules (top 5)
35
+
36
+ 1. **One unified options schema** — every engine takes the same `ScrapeOptions`.
37
+ 2. **Engines drop unsupported options, never raise** — `SUPPORTED_OPTIONS` set + DEBUG log.
38
+ 3. **All four format slots are populated when achievable** — `text`+`markdown` always, `html`/`json` when native.
39
+ 4. **Escalate cheap-to-expensive, stop at first good response** — T0→T1→T2→T3→T4 ladder with stop rules.
40
+ 5. **No vendor LLM SDK in the library** — LLM passed as a user-provided async callable.
41
+
42
+ _Full rules: [docs/conventions/golden-rules.md](docs/conventions/golden-rules.md)_
43
+
44
+ ## Architecture (one-liner)
45
+
46
+ `scrape(url, opts) → ScrapeRouter walks an escalation ladder of engines, each implementing `ScrapeEngine._fetch(url, opts) → ScrapeResult`, until a non-suspicious response is returned.
47
+
48
+ _Full details: [docs/architecture/overview.md](docs/architecture/overview.md)_
@@ -0,0 +1,78 @@
1
+ # Contributing to scrapefold
2
+
3
+ ## Add a new engine — 5-step checklist
4
+
5
+ 1. **Create the module** at `src/scrapefold/engines/<name>.py`:
6
+
7
+ ```python
8
+ from __future__ import annotations
9
+
10
+ from scrapefold.engines.base import EngineCapabilities, ScrapeEngine
11
+ from scrapefold.options import ScrapeOptions
12
+ from scrapefold.result import ScrapeResult
13
+
14
+
15
+ class FooEngine(ScrapeEngine):
16
+ NAME = "foo"
17
+ CAPABILITIES = EngineCapabilities(
18
+ js_rendering=True,
19
+ stealth=True,
20
+ cost_per_1k=1.0,
21
+ requires_api_key=True,
22
+ )
23
+ SUPPORTED_OPTIONS = frozenset({
24
+ "language", "country", "render_js", "wait_ms",
25
+ "stealth", "user_agent", "custom_headers", "timeout_s",
26
+ })
27
+
28
+ async def _fetch(self, url: str, opts: ScrapeOptions) -> ScrapeResult:
29
+ # 1. Translate opts -> native vendor params via local _adapt(opts)
30
+ # 2. Call vendor (httpx.AsyncClient or vendor SDK, lazy-imported)
31
+ # 3. Convert HTML→markdown if needed via scrapefold.html_to_text
32
+ # 4. Return ScrapeResult with text+markdown+html (+json if structured)
33
+ ...
34
+ ```
35
+
36
+ 2. **Document the native parameter surface** in the module docstring as a table — every vendor parameter you support, what unified option it maps from, defaults. This is the input to the global adapter matrix.
37
+
38
+ 3. **Register it lazily** in `src/scrapefold/engines/__init__.py`:
39
+
40
+ ```python
41
+ def _load_foo():
42
+ from scrapefold.engines.foo import FooEngine
43
+ return FooEngine
44
+
45
+ register("foo", _load_foo)
46
+ ```
47
+
48
+ 4. **Add tests** under `tests/test_engines/test_foo.py` covering:
49
+ - Success path (vendor mocked via `pytest-httpx`)
50
+ - Vendor error (4xx/5xx → `EngineError`)
51
+ - Timeout (engine respects `opts.timeout_s`)
52
+ - Missing API key (`is_available() is False`)
53
+ - One unified-opt → native-param adapter assertion
54
+
55
+ 5. **Add an extra** in `pyproject.toml` if the engine needs a non-stdlib dependency:
56
+
57
+ ```toml
58
+ [project.optional-dependencies]
59
+ foo = ["foo-sdk>=1.0"]
60
+ ```
61
+
62
+ And update `all = [...]`.
63
+
64
+ ## Style
65
+
66
+ - `ruff check` and `ruff format` must be clean.
67
+ - `mypy src` must be clean.
68
+ - Async everywhere — `async def`, `await`, `httpx.AsyncClient`.
69
+ - No top-level imports of optional vendor SDKs. Import inside the class or function.
70
+ - No `print()` — use `logging.getLogger(__name__)`.
71
+
72
+ ## Commits
73
+
74
+ Conventional-ish prefixes: `feat:`, `fix:`, `chore:`, `docs:`, `test:`, `refactor:`, `release:`.
75
+
76
+ ## Release
77
+
78
+ See `.claude/commands/release-scrapefold.md` (lands in S12). TL;DR: bump version in `pyproject.toml`, update `CHANGELOG.md`, tag `vX.Y.Z`, push — GitHub Actions publishes via PyPI trusted publishing.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Mihailorama
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.