scrapeflow-py 0.2.0__tar.gz → 0.4.0__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 (62) hide show
  1. scrapeflow_py-0.4.0/CHANGELOG.md +53 -0
  2. scrapeflow_py-0.4.0/MANIFEST.in +5 -0
  3. {scrapeflow_py-0.2.0 → scrapeflow_py-0.4.0}/PKG-INFO +427 -53
  4. scrapeflow_py-0.2.0/scrapeflow_py.egg-info/PKG-INFO → scrapeflow_py-0.4.0/README.md +395 -67
  5. {scrapeflow_py-0.2.0 → scrapeflow_py-0.4.0}/examples/__init__.py +0 -1
  6. {scrapeflow_py-0.2.0 → scrapeflow_py-0.4.0}/examples/advanced_example.py +5 -6
  7. {scrapeflow_py-0.2.0 → scrapeflow_py-0.4.0}/examples/authenticated_quotes_example.py +2 -1
  8. {scrapeflow_py-0.2.0 → scrapeflow_py-0.4.0}/examples/basic_usage.py +2 -1
  9. scrapeflow_py-0.4.0/examples/hybrid_extraction_example.py +71 -0
  10. scrapeflow_py-0.4.0/examples/llm_extraction_example.py +70 -0
  11. scrapeflow_py-0.4.0/examples/session_persistence_example.py +79 -0
  12. {scrapeflow_py-0.2.0 → scrapeflow_py-0.4.0}/examples/specification_driven_example.py +6 -4
  13. {scrapeflow_py-0.2.0 → scrapeflow_py-0.4.0}/examples/workflow_example.py +4 -4
  14. scrapeflow_py-0.4.0/pyproject.toml +107 -0
  15. {scrapeflow_py-0.2.0 → scrapeflow_py-0.4.0}/scrapeflow/__init__.py +44 -22
  16. {scrapeflow_py-0.2.0 → scrapeflow_py-0.4.0}/scrapeflow/anti_detection.py +18 -31
  17. {scrapeflow_py-0.2.0 → scrapeflow_py-0.4.0}/scrapeflow/browser_runtime.py +19 -9
  18. {scrapeflow_py-0.2.0 → scrapeflow_py-0.4.0}/scrapeflow/config.py +25 -16
  19. scrapeflow_py-0.4.0/scrapeflow/content_utils.py +48 -0
  20. {scrapeflow_py-0.2.0 → scrapeflow_py-0.4.0}/scrapeflow/engine.py +47 -40
  21. {scrapeflow_py-0.2.0 → scrapeflow_py-0.4.0}/scrapeflow/exceptions.py +0 -1
  22. {scrapeflow_py-0.2.0 → scrapeflow_py-0.4.0}/scrapeflow/extractors.py +19 -29
  23. scrapeflow_py-0.4.0/scrapeflow/llm_extract.py +181 -0
  24. scrapeflow_py-0.4.0/scrapeflow/mcp_backend.py +240 -0
  25. {scrapeflow_py-0.2.0 → scrapeflow_py-0.4.0}/scrapeflow/monitoring.py +13 -14
  26. scrapeflow_py-0.4.0/scrapeflow/pagination.py +86 -0
  27. scrapeflow_py-0.4.0/scrapeflow/ports.py +57 -0
  28. scrapeflow_py-0.4.0/scrapeflow/py.typed +0 -0
  29. {scrapeflow_py-0.2.0 → scrapeflow_py-0.4.0}/scrapeflow/rate_limiter.py +4 -9
  30. {scrapeflow_py-0.2.0 → scrapeflow_py-0.4.0}/scrapeflow/registry.py +20 -20
  31. {scrapeflow_py-0.2.0 → scrapeflow_py-0.4.0}/scrapeflow/retry.py +9 -19
  32. {scrapeflow_py-0.2.0 → scrapeflow_py-0.4.0}/scrapeflow/robots.py +1 -4
  33. scrapeflow_py-0.4.0/scrapeflow/specifications.py +321 -0
  34. {scrapeflow_py-0.2.0 → scrapeflow_py-0.4.0}/scrapeflow/workflow.py +18 -18
  35. scrapeflow_py-0.2.0/README.md → scrapeflow_py-0.4.0/scrapeflow_py.egg-info/PKG-INFO +442 -34
  36. {scrapeflow_py-0.2.0 → scrapeflow_py-0.4.0}/scrapeflow_py.egg-info/SOURCES.txt +11 -3
  37. scrapeflow_py-0.4.0/scrapeflow_py.egg-info/requires.txt +15 -0
  38. scrapeflow_py-0.4.0/scrapeflow_py.egg-info/top_level.txt +1 -0
  39. {scrapeflow_py-0.2.0 → scrapeflow_py-0.4.0}/tests/test_config.py +5 -7
  40. scrapeflow_py-0.4.0/tests/test_content_utils.py +36 -0
  41. scrapeflow_py-0.4.0/tests/test_hybrid_extractor.py +47 -0
  42. scrapeflow_py-0.4.0/tests/test_release.py +207 -0
  43. {scrapeflow_py-0.2.0 → scrapeflow_py-0.4.0}/tests/test_robots.py +1 -0
  44. {scrapeflow_py-0.2.0 → scrapeflow_py-0.4.0}/tests/test_specifications.py +7 -5
  45. scrapeflow_py-0.2.0/MANIFEST.in +0 -4
  46. scrapeflow_py-0.2.0/pyproject.toml +0 -16
  47. scrapeflow_py-0.2.0/requirements.txt +0 -5
  48. scrapeflow_py-0.2.0/scrapeflow/mcp_backend.py +0 -144
  49. scrapeflow_py-0.2.0/scrapeflow/ports.py +0 -71
  50. scrapeflow_py-0.2.0/scrapeflow/specifications.py +0 -218
  51. scrapeflow_py-0.2.0/scrapeflow_py.egg-info/requires.txt +0 -4
  52. scrapeflow_py-0.2.0/scrapeflow_py.egg-info/top_level.txt +0 -3
  53. scrapeflow_py-0.2.0/setup.py +0 -36
  54. scrapeflow_py-0.2.0/tests/__init__.py +0 -2
  55. {scrapeflow_py-0.2.0 → scrapeflow_py-0.4.0}/LICENSE +0 -0
  56. {scrapeflow_py-0.2.0 → scrapeflow_py-0.4.0}/scrapeflow/schema_library.py +0 -0
  57. {scrapeflow_py-0.2.0 → scrapeflow_py-0.4.0}/scrapeflow/workflow_executor.py +0 -0
  58. {scrapeflow_py-0.2.0 → scrapeflow_py-0.4.0}/scrapeflow_py.egg-info/dependency_links.txt +0 -0
  59. {scrapeflow_py-0.2.0 → scrapeflow_py-0.4.0}/setup.cfg +0 -0
  60. {scrapeflow_py-0.2.0 → scrapeflow_py-0.4.0}/tests/test_engine_orchestration.py +0 -0
  61. {scrapeflow_py-0.2.0 → scrapeflow_py-0.4.0}/tests/test_public_api_compat.py +0 -0
  62. {scrapeflow_py-0.2.0 → scrapeflow_py-0.4.0}/tests/test_registry.py +0 -0
@@ -0,0 +1,53 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [0.4.0] - 2026-10-31
11
+
12
+ ### Fixed
13
+ - LLM extraction works with current `mistralai` releases (2.x/3.x moved the client to
14
+ `mistralai.client.Mistral`). Previously every LLM call failed with a misleading
15
+ "mistralai is required" `ImportError` even though it was installed.
16
+ - The wheel no longer installs stray top-level `tests` and `examples` packages into
17
+ `site-packages`.
18
+ - `create_mcp_backend()` no longer silently returns a do-nothing backend: unknown backend names
19
+ raise `ValueError`, and the not-yet-implemented `scrapy_mcp` / `playwright_mcp` backends emit a
20
+ `UserWarning`.
21
+ - Step timeouts chain the original `asyncio.TimeoutError` (`raise ... from`).
22
+ - Dockerfile: browsers are installed where `PLAYWRIGHT_BROWSERS_PATH` points, and `pytest` is
23
+ available for the default `CMD` (the image previously could not run its own default command).
24
+
25
+ ### Added
26
+ - `py.typed` marker (PEP 561) and `Typing :: Typed` classifier.
27
+ - `__version__` is read from installed package metadata (`importlib.metadata`).
28
+ - Tests: mocked Mistral schema generation and extraction (code-fence handling, bad JSON, async,
29
+ new SDK layout), MCP backend factory, and real-browser tests against a local HTTP server
30
+ (spec-driven extraction and robots.txt enforcement). 36 tests total.
31
+ - CHANGELOG, SECURITY.md, CODE_OF_CONDUCT.md, issue/PR templates, Dependabot, CITATION.cff.
32
+
33
+ ### Changed
34
+ - Requires Python 3.10+ (3.8 and 3.9 are end-of-life); classifiers for 3.10 to 3.13.
35
+ - Packaging is `pyproject.toml`-only (removed `setup.py`, `pytest.ini`, `requirements*.txt`),
36
+ with PEP 639 `license = "MIT"` and a `dev` extra (pytest, black, ruff, mypy, build, twine).
37
+ - CI: lint (ruff + black) is enforced instead of `|| true`, tests run on Python 3.10 to 3.13 with
38
+ Chromium installed, and the Docker job now runs the test suite inside the image.
39
+ - Codebase formatted with black and cleaned with ruff (imports, modern type hints).
40
+
41
+ ## [0.3.0] - 2026-03-09
42
+
43
+ ### Added
44
+ - Hybrid extraction, Mistral LLM extraction, session persistence, content cleaning.
45
+
46
+ ## [0.2.0] - 2026-03-09
47
+
48
+ ### Added
49
+ - Specification-driven extraction, robots.txt compliance, ethical crawling, architecture refactor.
50
+
51
+ ## [0.1.2] - 2025-11-27
52
+
53
+ - Initial public releases (0.1.0 to 0.1.2).
@@ -0,0 +1,5 @@
1
+ include README.md
2
+ include LICENSE
3
+ include CHANGELOG.md
4
+ include scrapeflow/py.typed
5
+ recursive-include examples *.py
@@ -1,42 +1,67 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: scrapeflow-py
3
- Version: 0.2.0
4
- Summary: An opinionated scraping workflow engine built on Playwright
5
- Home-page: https://github.com/irfanalidv/scrapeflow-py
6
- Author: Irfan Ali
7
- License: MIT
8
- Classifier: Development Status :: 3 - Alpha
3
+ Version: 0.4.0
4
+ Summary: Opinionated Playwright scraping workflow engine: retries, rate limiting, anti-detection, robots.txt compliance, Pydantic spec-driven and LLM-powered extraction, sessions, and metrics.
5
+ Author-email: Irfan Ali <irfanali29@hotmail.com>
6
+ Maintainer-email: Irfan Ali <irfanali29@hotmail.com>
7
+ License-Expression: MIT
8
+ Project-URL: Homepage, https://github.com/irfanalidv/scrapeflow-py
9
+ Project-URL: Documentation, https://github.com/irfanalidv/scrapeflow-py#readme
10
+ Project-URL: Repository, https://github.com/irfanalidv/scrapeflow-py
11
+ Project-URL: Issues, https://github.com/irfanalidv/scrapeflow-py/issues
12
+ Project-URL: Changelog, https://github.com/irfanalidv/scrapeflow-py/blob/main/CHANGELOG.md
13
+ Keywords: web-scraping,scraping,scraper,crawler,playwright,browser-automation,workflow,data-extraction,llm-extraction,mistral,pydantic,rate-limiting,retry,robots-txt,anti-detection,asyncio
14
+ Classifier: Development Status :: 4 - Beta
9
15
  Classifier: Intended Audience :: Developers
10
- Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Framework :: AsyncIO
18
+ Classifier: Topic :: Internet :: WWW/HTTP :: Browsers
19
+ Classifier: Topic :: Internet :: WWW/HTTP :: Indexing/Search
20
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
11
21
  Classifier: Programming Language :: Python :: 3
12
- Classifier: Programming Language :: Python :: 3.8
13
- Classifier: Programming Language :: Python :: 3.9
14
22
  Classifier: Programming Language :: Python :: 3.10
15
23
  Classifier: Programming Language :: Python :: 3.11
16
24
  Classifier: Programming Language :: Python :: 3.12
17
- Requires-Python: >=3.8
25
+ Classifier: Programming Language :: Python :: 3.13
26
+ Classifier: Programming Language :: Python :: 3 :: Only
27
+ Classifier: Typing :: Typed
28
+ Requires-Python: >=3.10
18
29
  Description-Content-Type: text/markdown
19
30
  License-File: LICENSE
20
31
  Requires-Dist: playwright>=1.40.0
21
32
  Requires-Dist: aiohttp>=3.9.0
22
33
  Requires-Dist: tenacity>=8.2.0
23
34
  Requires-Dist: pydantic>=2.0.0
24
- Dynamic: author
25
- Dynamic: classifier
26
- Dynamic: description
27
- Dynamic: description-content-type
28
- Dynamic: home-page
29
- Dynamic: license
35
+ Requires-Dist: mistralai>=1.0.0
36
+ Requires-Dist: python-dotenv>=1.0.0
37
+ Provides-Extra: dev
38
+ Requires-Dist: pytest>=7.4.0; extra == "dev"
39
+ Requires-Dist: pytest-asyncio>=0.23.0; extra == "dev"
40
+ Requires-Dist: black>=24.0.0; extra == "dev"
41
+ Requires-Dist: ruff>=0.6.0; extra == "dev"
42
+ Requires-Dist: mypy>=1.7.0; extra == "dev"
43
+ Requires-Dist: build>=1.0.0; extra == "dev"
44
+ Requires-Dist: twine>=5.0.0; extra == "dev"
30
45
  Dynamic: license-file
31
- Dynamic: requires-dist
32
- Dynamic: requires-python
33
- Dynamic: summary
46
+
47
+ <div align="center">
34
48
 
35
49
  # ScrapeFlow
36
50
 
37
- **An opinionated scraping workflow engine built on Playwright**
51
+ **An opinionated scraping workflow engine built on Playwright.**
52
+
53
+ Retries, rate limiting, anti-detection, robots.txt compliance, Pydantic spec-driven and
54
+ Mistral LLM-powered extraction, login sessions, workflows, and metrics in one async API.
55
+
56
+ [![PyPI version](https://img.shields.io/pypi/v/scrapeflow-py.svg)](https://pypi.org/project/scrapeflow-py/)
57
+ [![Python versions](https://img.shields.io/pypi/pyversions/scrapeflow-py.svg)](https://pypi.org/project/scrapeflow-py/)
58
+ [![CI](https://github.com/irfanalidv/scrapeflow-py/actions/workflows/ci.yml/badge.svg)](https://github.com/irfanalidv/scrapeflow-py/actions/workflows/ci.yml)
59
+ [![PyPI Downloads](https://static.pepy.tech/badge/scrapeflow-py)](https://pepy.tech/projects/scrapeflow-py)
60
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/irfanalidv/scrapeflow-py/blob/main/LICENSE)
61
+ [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
62
+ [![Playwright](https://img.shields.io/badge/Playwright-1.40%2B-green)](https://playwright.dev/)
38
63
 
39
- [![GitHub](https://img.shields.io/github/license/irfanalidv/scrapeflow-py)](https://github.com/irfanalidv/scrapeflow-py/blob/main/LICENSE) [![PyPI](https://img.shields.io/pypi/v/scrapeflow-py)](https://pypi.org/project/scrapeflow-py/) [![PyPI Downloads](https://static.pepy.tech/personalized-badge/scrapeflow-py?period=total&units=INTERNATIONAL_SYSTEM&left_color=BLACK&right_color=GREEN&left_text=downloads)](https://pepy.tech/projects/scrapeflow-py) [![Python](https://img.shields.io/badge/python-3.8%2B-blue)](https://www.python.org/) [![Playwright](https://img.shields.io/badge/Playwright-1.40%2B-green)](https://playwright.dev/) [![Status](https://img.shields.io/badge/status-active-success)](https://github.com/irfanalidv/scrapeflow-py)
64
+ </div>
40
65
 
41
66
  ---
42
67
 
@@ -49,7 +74,12 @@ ScrapeFlow is a production-ready Python library that transforms Playwright into
49
74
  - **⚖️ Ethical Crawling (GDPR/CCPA)**: Configurable data retention, anonymization, and consent options in the specification layer
50
75
  - **📦 Component Registry**: Shared, versioned selectors, pagination handlers, and login flows—platform thinking over one-off scrapers
51
76
  - **🔔 Monitoring & Alerting**: Alert callbacks on failure thresholds; rollback hooks for failed extraction runs
52
- - **🔌 MCP Extensibility**: Pluggable backends for Scrapy MCP Server, Playwright MCP, or LLM-based semantic extraction
77
+ - **🔌 Pluggable Extraction Backends**: `MCPBackend` interface with a built-in Mistral LLM backend; subclass it to plug in an MCP server (Scrapy/Playwright MCP adapters are planned, not yet implemented)
78
+ - **🤖 Mistral LLM Extraction**: Generate schemas from natural language, extract without selectors—uses `MISTRAL_API_KEY`
79
+ - **🔄 Hybrid Extraction**: Selector-first, LLM fallback when validation fails—self-healing spiders
80
+ - **🛡️ Selector Fallback Chain**: Try multiple selectors per field (`[".price", ".cost"]`) for resilience
81
+ - **🫙 Session Persistence**: Save/load cookies and localStorage via `save_storage_state()` and `storage_state_path`
82
+ - **🧹 Content Cleaning**: Strip scripts/styles before LLM extraction for better accuracy and token usage
53
83
  - **🔄 Intelligent Retry Logic**: Automatic retries with exponential backoff and jitter
54
84
  - **⚡ Rate Limiting**: Token bucket algorithm to respect server limits
55
85
  - **🕵️ Anti-Detection**: Stealth mode, user agent rotation, and proxy support
@@ -60,13 +90,16 @@ ScrapeFlow is a production-ready Python library that transforms Playwright into
60
90
  - **📝 Type Hints**: Full type support for better IDE experience
61
91
 
62
92
  <p align="center">
63
- <img src="docs/scrapeflow-architecture.png" alt="ScrapeFlow Architecture" width="800">
93
+ <img src="https://raw.githubusercontent.com/irfanalidv/scrapeflow-py/main/docs/scrapeflow-architecture.png" alt="ScrapeFlow Architecture" width="800">
64
94
  </p>
65
95
 
66
96
  ## 📦 Installation
67
97
 
98
+ Requires Python 3.10+.
99
+
68
100
  ```bash
69
101
  pip install scrapeflow-py
102
+ playwright install chromium # download the browser Playwright drives
70
103
  ```
71
104
 
72
105
  Or install from source:
@@ -75,17 +108,24 @@ Or install from source:
75
108
  git clone https://github.com/irfanalidv/scrapeflow-py.git
76
109
  cd scrapeflow-py
77
110
  pip install -e .
111
+ playwright install chromium
78
112
  ```
79
113
 
80
- **Note**: After installation, install Playwright browsers:
114
+ On a fresh Linux machine or container, `playwright install --with-deps chromium` also installs
115
+ the system libraries Chromium needs. A `Dockerfile` is included.
81
116
 
82
- ```bash
83
- playwright install
84
- ```
117
+ ## ⚙️ Configuration (environment variables)
118
+
119
+ | Variable | Purpose |
120
+ | --- | --- |
121
+ | `MISTRAL_API_KEY` | Required only for LLM features (`generate_schema_from_prompt`, `extract_with_schema`, `MistralLLMBackend`, `HybridExtractor` LLM fallback). Read from the environment or a `.env` file. |
122
+
123
+ Everything else (rate limits, retries, browser, anti-detection, robots.txt, ethical crawling) is
124
+ configured in code via `ScrapeFlowConfig`; see [Configuration](#configuration) below.
85
125
 
86
126
  ## 🎯 Real-World Use Cases
87
127
 
88
- ScrapeFlow is used in production for:
128
+ Typical use cases:
89
129
 
90
130
  - **💰 E-commerce Price Monitoring** - Track competitor prices, monitor deals, and optimize pricing strategies
91
131
  - **📰 News & Content Aggregation** - Collect articles from multiple sources for content platforms
@@ -371,6 +411,132 @@ print(f"💼 Found {len(quotes)} quotes across pages")
371
411
  💼 Found 20 quotes across pages
372
412
  ```
373
413
 
414
+ ### Use Case 5: LLM-Powered Extraction (No Selectors)
415
+
416
+ **Real-world scenario:** Extract structured data using Mistral AI—no CSS selectors, just natural language field descriptions. Ideal for pages with inconsistent markup or when you want semantic understanding.
417
+
418
+ **Requires:** `MISTRAL_API_KEY` in `.env` or environment.
419
+
420
+ ```python
421
+ import asyncio
422
+ import os
423
+
424
+ try:
425
+ from dotenv import load_dotenv
426
+ load_dotenv()
427
+ except ImportError:
428
+ pass
429
+
430
+ from scrapeflow import ScrapeFlow, create_mcp_backend
431
+ from scrapeflow.config import ScrapeFlowConfig, EthicalCrawlingConfig
432
+ from scrapeflow.llm_extract import generate_schema_from_prompt, extract_with_schema
433
+
434
+ async def run_llm_extraction():
435
+ if not os.environ.get("MISTRAL_API_KEY"):
436
+ print("Set MISTRAL_API_KEY in .env to run this example")
437
+ return
438
+
439
+ config = ScrapeFlowConfig(
440
+ ethical_crawling=EthicalCrawlingConfig(respect_robots_txt=True),
441
+ )
442
+
443
+ async with ScrapeFlow(config) as scraper:
444
+ await scraper.navigate("https://quotes.toscrape.com/")
445
+
446
+ # Option 1: Generate schema from natural language
447
+ schema = generate_schema_from_prompt(
448
+ "Extract quote text, author name, and list of tags"
449
+ )
450
+
451
+ # Option 2: Extract using schema + page content (no selectors!)
452
+ content = await scraper.page.evaluate("() => document.body.innerText")
453
+ data = extract_with_schema(
454
+ content,
455
+ schema,
456
+ prompt="Extract all quotes from this page with text, author, and tags",
457
+ )
458
+ print("LLM extracted:", data)
459
+
460
+ # Option 3: MistralLLMBackend—field descriptions only
461
+ backend = create_mcp_backend("mistral")
462
+ semantic_data = await backend.extract_with_semantics(
463
+ scraper.page,
464
+ field_descriptions={
465
+ "quote": "The quote text",
466
+ "author": "Author name",
467
+ "tags": "Comma-separated tags",
468
+ },
469
+ context="Extract the first quote on the page",
470
+ )
471
+ print("Semantic extraction:", semantic_data)
472
+
473
+ asyncio.run(run_llm_extraction())
474
+ ```
475
+
476
+ **Real Output:**
477
+
478
+ ```
479
+ Generated schema: {'type': 'object', 'properties': {'quote_text': {...}, 'author_name': {...}, 'tags': {...}}, 'required': ['quote_text', 'author_name', 'tags']}
480
+
481
+ LLM extracted: [{'quote_text': 'The world as we have created it is a process of our thinking...', 'author_name': 'Albert Einstein', 'tags': ['change', 'deep-thoughts', 'thinking', 'world']}, ...]
482
+
483
+ Semantic extraction: {'quote': 'The world as we have created it is a process of our thinking. It cannot be changed without changing our thinking.', 'author': 'Albert Einstein', 'tags': 'change, deep-thoughts, thinking, world'}
484
+ ```
485
+
486
+ **What you get:** Schema generation from prompts, extraction without brittle selectors, and semantic understanding of page content—powerful for dynamic or inconsistent sites.
487
+
488
+ ### Use Case 6: Login + Session Persistence
489
+
490
+ **Real-world scenario:** Log in once, save cookies, then reuse the session on future runs—no re-login needed.
491
+
492
+ ```python
493
+ import asyncio
494
+ import os
495
+ from scrapeflow import ScrapeFlow, SpecificationExtractor, get_registry, register_quotes_login_handler
496
+ from scrapeflow.config import ScrapeFlowConfig, EthicalCrawlingConfig, BrowserConfig
497
+ from scrapeflow.specifications import FieldSpec, ItemSpec
498
+ from pydantic import BaseModel
499
+
500
+ class QuoteItem(BaseModel):
501
+ text: str
502
+ author: str
503
+
504
+ class QuotesPage(BaseModel):
505
+ quotes: list[QuoteItem]
506
+
507
+ async def run():
508
+ state_file = "/tmp/scrapeflow_session.json"
509
+ schema = {"quotes": ItemSpec(items_selector=".quote", fields={"text": ".text", "author": ".author"})}
510
+
511
+ # Step 1: Login and save (if first run)
512
+ if not os.path.exists(state_file):
513
+ async with ScrapeFlow(ScrapeFlowConfig(ethical_crawling=EthicalCrawlingConfig(respect_robots_txt=True))) as scraper:
514
+ register_quotes_login_handler(get_registry())
515
+ handler = get_registry().get_login("quotes_login")
516
+ await scraper.login_with_handler("https://quotes.toscrape.com/login", "admin", "amdin", handler)
517
+ await scraper.save_storage_state(state_file)
518
+
519
+ # Step 2: Use saved session (no login)
520
+ config = ScrapeFlowConfig(ethical_crawling=EthicalCrawlingConfig(respect_robots_txt=True),
521
+ browser=BrowserConfig(storage_state_path=state_file))
522
+ async with ScrapeFlow(config) as scraper:
523
+ await scraper.navigate("https://quotes.toscrape.com/")
524
+ data = await SpecificationExtractor(QuotesPage, schema=schema).extract(scraper.page)
525
+ print(f"Extracted {len(data.quotes)} quotes (authenticated)")
526
+
527
+ asyncio.run(run())
528
+ ```
529
+
530
+ **Real Output:**
531
+
532
+ ```
533
+ Extracted 10 quotes (authenticated)
534
+ ```
535
+
536
+ *(On first run: login + save; on subsequent runs: load session and extract without re-login.)*
537
+
538
+ **What you get:** `login_with_handler`, `save_storage_state`, `storage_state_path`—reusable authenticated sessions.
539
+
374
540
  ## 📚 Documentation
375
541
 
376
542
  ### Configuration
@@ -568,6 +734,40 @@ async with ScrapeFlow(config) as scraper:
568
734
  # - Temporary blocks
569
735
  ```
570
736
 
737
+ ### Pagination
738
+
739
+ **Use Case:** Scrape multiple pages (e.g. search results, product listings) with limits.
740
+
741
+ ```python
742
+ import asyncio
743
+ from scrapeflow import ScrapeFlow
744
+ from scrapeflow.config import ScrapeFlowConfig, PaginationConfig
745
+ from scrapeflow.pagination import paginate
746
+ from scrapeflow.registry import get_registry
747
+
748
+ async def extract_quotes(page, context):
749
+ quotes = []
750
+ for i in range(await page.locator(".quote").count()):
751
+ q = page.locator(".quote").nth(i)
752
+ text = await q.locator(".text").text_content()
753
+ author = await q.locator(".author").text_content()
754
+ quotes.append({"text": text or "", "author": author or ""})
755
+ return quotes
756
+
757
+ async def main():
758
+ get_registry().register_pagination("quotes", next_selector="li.next a", has_next="li.next")
759
+ handler = get_registry().get_pagination("quotes")
760
+ config = PaginationConfig(max_pages=3, max_results=25)
761
+
762
+ async with ScrapeFlow(ScrapeFlowConfig()) as scraper:
763
+ async for page_data in paginate(scraper, "https://quotes.toscrape.com/", handler, extract_quotes, config):
764
+ print(f"Page: {len(page_data)} quotes")
765
+ for q in page_data[:2]:
766
+ print(f" - {q['author']}: {q['text'][:40]}...")
767
+
768
+ asyncio.run(main())
769
+ ```
770
+
571
771
  ### Data Extraction
572
772
 
573
773
  **Use Case:** Extracting structured data from [quotes.toscrape.com](https://quotes.toscrape.com/) and [books.toscrape.com](https://books.toscrape.com/).
@@ -798,13 +998,175 @@ asyncio.run(scrape_with_error_handling())
798
998
  ✅ Successfully scraped: Quotes to Scrape
799
999
  ```
800
1000
 
1001
+ ## 📋 Feature Coverage—Nothing Hidden
1002
+
1003
+ Every ScrapeFlow feature is demonstrated somewhere. Quick reference:
1004
+
1005
+ | Feature | Where to See It |
1006
+ |---------|-----------------|
1007
+ | **Extractors** | Use Case 1, `basic_usage.py`, `advanced_example.py` |
1008
+ | **StructuredExtractor** | Use Case 2, `workflow_example.py`, `advanced_example.py` |
1009
+ | **SpecificationExtractor** | Use Case 6, Doc section, `specification_driven_example.py` |
1010
+ | **HybridExtractor** | `hybrid_extraction_example.py` |
1011
+ | **FieldSpec, ItemSpec** | Doc section, `specification_driven_example.py`, `hybrid_extraction_example.py` |
1012
+ | **Selector fallback** `[".a", ".b"]` | `hybrid_extraction_example.py` |
1013
+ | **LLM extraction** | Use Case 5, `llm_extraction_example.py` |
1014
+ | **Login** | Use Case 6, `authenticated_quotes_example.py`, `session_persistence_example.py` |
1015
+ | **Session persistence** | Use Case 6, `session_persistence_example.py` |
1016
+ | **Registry, LoginHandler** | `authenticated_quotes_example.py`, `specification_driven_example.py` |
1017
+ | **Workflow** | Use Case 2, `workflow_example.py` |
1018
+ | **Rate limit, Retry** | Use Cases 1, 4, `basic_usage.py`, `advanced_example.py` |
1019
+ | **Anti-detection** | Use Case 3, `advanced_example.py` |
1020
+ | **Ethical crawling, robots.txt** | `specification_driven_example.py`, `authenticated_quotes_example.py` |
1021
+ | **Pagination** | Doc section, `scrapeflow.pagination.paginate`, `PaginationConfig` |
1022
+ | **Content cleaning** | Used internally by MistralLLMBackend; `scrapeflow.content_utils` |
1023
+
801
1024
  ## 🎨 Complete Examples
802
1025
 
803
- Check out the `examples/` directory for more examples:
1026
+ Check out the `examples/` directory—each example showcases specific features:
1027
+
1028
+ | Example | Features Demonstrated |
1029
+ |---------|------------------------|
1030
+ | `basic_usage.py` | ScrapeFlow, Extractor, rate limit, retry, metrics |
1031
+ | `workflow_example.py` | Workflow, steps, on_success/on_error, StructuredExtractor |
1032
+ | `advanced_example.py` | Anti-detection, rate limit, retry, StructuredExtractor |
1033
+ | `specification_driven_example.py` | SpecificationExtractor, FieldSpec, ItemSpec, ethical crawling, registry |
1034
+ | `authenticated_quotes_example.py` | Login, `login_with_handler`, registry, LoginHandler |
1035
+ | `llm_extraction_example.py` | Mistral LLM, `generate_schema_from_prompt`, `extract_with_schema`, MistralLLMBackend *(requires `MISTRAL_API_KEY`)* |
1036
+ | `hybrid_extraction_example.py` | HybridExtractor, selector fallback chain `[".a", ".b"]`, LLM fallback |
1037
+ | `session_persistence_example.py` | `save_storage_state`, `storage_state_path`, skip login on reuse |
1038
+
1039
+ ### Real Outputs from Examples
1040
+
1041
+ Outputs below are from running each example against live sites (quotes.toscrape.com, books.toscrape.com).
1042
+
1043
+ **`basic_usage.py`**
1044
+
1045
+ ```
1046
+ Page title: Quotes to Scrape
1047
+
1048
+ Extracted 5 quotes:
1049
+
1050
+ 1. "The world as we have created it is a process of our thinkin...
1051
+ Author: Albert Einstein
1052
+ Tags: change, deep-thoughts, thinking, world
1053
+
1054
+ 2. "It is our choices, Harry, that show what we truly are, far ...
1055
+ Author: J.K. Rowling
1056
+ Tags: abilities, choices
1057
+
1058
+ 3. "There are only two ways to live your life. One is as though...
1059
+ Author: Albert Einstein
1060
+ Tags: inspirational, life, live, miracle, miracles
1061
+
1062
+ 4. "The person, be it gentleman or lady, who has not pleasure i...
1063
+ Author: Jane Austen
1064
+ Tags: aliteracy, books, classic, humor
1065
+
1066
+ 5. "Imperfection is beauty, madness is genius and it's better t...
1067
+ Author: Marilyn Monroe
1068
+ Tags: be-yourself, inspirational
1069
+
1070
+ 📊 Metrics:
1071
+ Success rate: 100.00%
1072
+ Total requests: 1
1073
+ Average response time: 1.72s
1074
+ ```
1075
+
1076
+ **`advanced_example.py`**
1077
+
1078
+ ```
1079
+ 📝 Extracted quotes:
1080
+
1081
+ 1. "The world as we have created it is a process of our thinkin...
1082
+ Author: Albert Einstein
1083
+ Tags: change, deep-thoughts, thinking, world
1084
+
1085
+ 2. "It is our choices, Harry, that show what we truly are, far ...
1086
+ Author: J.K. Rowling
1087
+ Tags: abilities, choices
1088
+
1089
+ 3. "There are only two ways to live your life. One is as though...
1090
+ Author: Albert Einstein
1091
+ Tags: inspirational, life, live, miracle, miracles
1092
+
1093
+ 4. "The person, be it gentleman or lady, who has not pleasure i...
1094
+ Author: Jane Austen
1095
+ Tags: aliteracy, books, classic, humor
1096
+
1097
+ 5. "Imperfection is beauty, madness is genius and it's better t...
1098
+ Author: Marilyn Monroe
1099
+ Tags: be-yourself, inspirational
1100
+
1101
+ Scraping completed:
1102
+ Success rate: 100.00%
1103
+ Total requests: 1
1104
+ Retries: 0
1105
+ ```
1106
+
1107
+ **`workflow_example.py`**
804
1108
 
805
- - `basic_usage.py` - Simple scraping example
806
- - `workflow_example.py` - Workflow orchestration
807
- - `advanced_example.py` - All features combined
1109
+ ```
1110
+ 💾 Saving 20 books to database...
1111
+ - A Light in the ...... - £51.77
1112
+ - Tipping the Velvet... - £53.74
1113
+ - Soumission... - £50.10
1114
+
1115
+ ✅ Workflow completed successfully!
1116
+ 📚 Extracted 20 books
1117
+
1118
+ 📊 Metrics:
1119
+ Total requests: 3
1120
+ Success rate: 100.00%
1121
+ Average response time: 5.85s
1122
+ ```
1123
+
1124
+ **`specification_driven_example.py`**
1125
+
1126
+ ```
1127
+ Extracted 10 items (validated via Pydantic)
1128
+ 1. "The world as we have created it is a process of o... | Albert Einstein
1129
+ 2. "It is our choices, Harry, that show what we truly... | J.K. Rowling
1130
+ 3. "There are only two ways to live your life. One is... | Albert Einstein
1131
+
1132
+ Metrics: 1 requests, 100.0% success
1133
+ ```
1134
+
1135
+ **`authenticated_quotes_example.py`**
1136
+
1137
+ ```
1138
+ Authenticated login: True
1139
+ Quotes extracted: 10
1140
+ 1. Albert Einstein: "The world as we have created it is a process of our thinking. It cannot be chan...
1141
+ 2. J.K. Rowling: "It is our choices, Harry, that show what we truly are, far more than our abilit...
1142
+ 3. Albert Einstein: "There are only two ways to live your life. One is as though nothing is a miracl...
1143
+ ```
1144
+
1145
+ **`hybrid_extraction_example.py`**
1146
+
1147
+ ```
1148
+ Extracted (selector or LLM):
1149
+ Title: A Light in the Attic
1150
+ Price: £51.77
1151
+ Availability: In stock (22 available)
1152
+ ```
1153
+
1154
+ **`llm_extraction_example.py`** *(requires MISTRAL_API_KEY)*
1155
+
1156
+ ```
1157
+ Generated schema: {'type': 'object', 'properties': {'quote_text': {'type': 'string', 'description': 'The text of the quote'}, 'author_name': {'type': 'string', 'description': 'The name of the author of the quote'}, 'tags': {'type': 'array', 'description': 'A list of tags associated with the quote'}}, 'required': ['quote_text', 'author_name', 'tags']}
1158
+
1159
+ LLM extracted: [{'quote_text': 'The world as we have created it is a process of our thinking...', 'author_name': 'Albert Einstein', 'tags': ['change', 'deep-thoughts', 'thinking', 'world']}, ...]
1160
+
1161
+ Semantic extraction: {'quote': 'The world as we have created it is a process of our thinking. It cannot be changed without changing our thinking.', 'author': 'Albert Einstein', 'tags': 'change, deep-thoughts, thinking, world'}
1162
+ ```
1163
+
1164
+ **`session_persistence_example.py`**
1165
+
1166
+ ```
1167
+ Session saved to /tmp/scrapeflow_quotes_session.json
1168
+ Loaded session: extracted 10 quotes (authenticated)
1169
+ ```
808
1170
 
809
1171
  ### Example: Complete Book Scraper with All Features
810
1172
 
@@ -896,30 +1258,42 @@ ScrapeFlow is built with a modular architecture:
896
1258
 
897
1259
  ```
898
1260
  scrapeflow/
899
- ├── engine.py # Main ScrapeFlow engine
900
- ├── ports.py # Protocols for dependency inversion
901
- ├── browser_runtime.py # Playwright runtime adapter
902
- ├── workflow.py # Workflow definition entities
1261
+ ├── engine.py # Main ScrapeFlow engine
1262
+ ├── ports.py # Protocols for dependency inversion
1263
+ ├── browser_runtime.py # Playwright runtime adapter
1264
+ ├── workflow.py # Workflow definition entities
903
1265
  ├── workflow_executor.py # Workflow execution service
904
- ├── config.py # Configuration classes (incl. EthicalCrawlingConfig)
905
- ├── specifications.py # Pydantic specification-driven extraction
906
- ├── schema_library.py # Reusable schema definitions
907
- ├── robots.py # robots.txt parsing and enforcement
908
- ├── registry.py # Shared selector/component registry
909
- ├── mcp_backend.py # MCP integration extensibility
910
- ├── anti_detection.py # Anti-detection utilities
911
- ├── rate_limiter.py # Rate limiting implementation
912
- ├── retry.py # Retry logic and error classification
913
- ├── monitoring.py # Metrics, logging, alerting
914
- ├── extractors.py # Data extraction utilities
915
- └── exceptions.py # Custom exceptions
916
- ```
917
-
918
- For deeper design details, see [`ARCHITECTURE.md`](ARCHITECTURE.md).
1266
+ ├── config.py # Configuration (EthicalCrawling, Pagination, etc.)
1267
+ ├── specifications.py # SpecificationExtractor, HybridExtractor, FieldSpec
1268
+ ├── schema_library.py # Reusable schema definitions
1269
+ ├── extractors.py # Extractor, StructuredExtractor
1270
+ ├── llm_extract.py # Mistral LLM schema + extraction
1271
+ ├── content_utils.py # HTML cleaning for LLM
1272
+ ├── mcp_backend.py # MCPBackend, MistralLLMBackend
1273
+ ├── pagination.py # paginate() helper
1274
+ ├── robots.py # robots.txt parsing and enforcement
1275
+ ├── registry.py # Selectors, login handlers, pagination
1276
+ ├── anti_detection.py # Stealth mode, user agent rotation
1277
+ ├── rate_limiter.py # Rate limiting implementation
1278
+ ├── retry.py # Retry logic and error classification
1279
+ ├── monitoring.py # Metrics, logging, alerting
1280
+ └── exceptions.py # Custom exceptions
1281
+ ```
1282
+
1283
+ For deeper design details, see [`ARCHITECTURE.md`](https://github.com/irfanalidv/scrapeflow-py/blob/main/ARCHITECTURE.md). Release notes are in [`CHANGELOG.md`](https://github.com/irfanalidv/scrapeflow-py/blob/main/CHANGELOG.md).
919
1284
 
920
1285
  ## 🤝 Contributing
921
1286
 
922
- Contributions are welcome! Please feel free to submit a Pull Request.
1287
+ Contributions are welcome! See [CONTRIBUTING.md](https://github.com/irfanalidv/scrapeflow-py/blob/main/CONTRIBUTING.md)
1288
+ and the [Code of Conduct](https://github.com/irfanalidv/scrapeflow-py/blob/main/CODE_OF_CONDUCT.md).
1289
+
1290
+ ```bash
1291
+ git clone https://github.com/irfanalidv/scrapeflow-py.git && cd scrapeflow-py
1292
+ pip install -e ".[dev]" && playwright install chromium
1293
+ ruff check . && black --check . && pytest
1294
+ ```
1295
+
1296
+ Security issues: please follow [SECURITY.md](https://github.com/irfanalidv/scrapeflow-py/blob/main/SECURITY.md).
923
1297
 
924
1298
  1. Fork the repository
925
1299
  2. Create your feature branch (`git checkout -b feature/AmazingFeature`)
@@ -929,7 +1303,7 @@ Contributions are welcome! Please feel free to submit a Pull Request.
929
1303
 
930
1304
  ## 📝 License
931
1305
 
932
- This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
1306
+ This project is licensed under the MIT License - see the [LICENSE](https://github.com/irfanalidv/scrapeflow-py/blob/main/LICENSE) file for details.
933
1307
 
934
1308
  ## 🙏 Acknowledgments
935
1309