scrapeflow-py 0.1.2__tar.gz → 0.2.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 (45) hide show
  1. {scrapeflow_py-0.1.2/scrapeflow_py.egg-info → scrapeflow_py-0.2.0}/PKG-INFO +83 -15
  2. {scrapeflow_py-0.1.2 → scrapeflow_py-0.2.0}/README.md +81 -14
  3. scrapeflow_py-0.2.0/examples/authenticated_quotes_example.py +77 -0
  4. scrapeflow_py-0.2.0/examples/specification_driven_example.py +80 -0
  5. {scrapeflow_py-0.1.2 → scrapeflow_py-0.2.0}/requirements.txt +1 -0
  6. scrapeflow_py-0.2.0/scrapeflow/__init__.py +71 -0
  7. scrapeflow_py-0.2.0/scrapeflow/browser_runtime.py +96 -0
  8. {scrapeflow_py-0.1.2 → scrapeflow_py-0.2.0}/scrapeflow/config.py +21 -0
  9. {scrapeflow_py-0.1.2 → scrapeflow_py-0.2.0}/scrapeflow/engine.py +106 -98
  10. {scrapeflow_py-0.1.2 → scrapeflow_py-0.2.0}/scrapeflow/exceptions.py +6 -0
  11. {scrapeflow_py-0.1.2 → scrapeflow_py-0.2.0}/scrapeflow/extractors.py +13 -10
  12. scrapeflow_py-0.2.0/scrapeflow/mcp_backend.py +144 -0
  13. {scrapeflow_py-0.1.2 → scrapeflow_py-0.2.0}/scrapeflow/monitoring.py +54 -3
  14. scrapeflow_py-0.2.0/scrapeflow/ports.py +71 -0
  15. scrapeflow_py-0.2.0/scrapeflow/registry.py +189 -0
  16. scrapeflow_py-0.2.0/scrapeflow/robots.py +113 -0
  17. scrapeflow_py-0.2.0/scrapeflow/schema_library.py +49 -0
  18. scrapeflow_py-0.2.0/scrapeflow/specifications.py +218 -0
  19. {scrapeflow_py-0.1.2 → scrapeflow_py-0.2.0}/scrapeflow/workflow.py +5 -49
  20. scrapeflow_py-0.2.0/scrapeflow/workflow_executor.py +49 -0
  21. {scrapeflow_py-0.1.2 → scrapeflow_py-0.2.0/scrapeflow_py.egg-info}/PKG-INFO +95 -15
  22. {scrapeflow_py-0.1.2 → scrapeflow_py-0.2.0}/scrapeflow_py.egg-info/SOURCES.txt +16 -1
  23. {scrapeflow_py-0.1.2 → scrapeflow_py-0.2.0}/scrapeflow_py.egg-info/requires.txt +1 -0
  24. {scrapeflow_py-0.1.2 → scrapeflow_py-0.2.0}/setup.py +2 -1
  25. {scrapeflow_py-0.1.2 → scrapeflow_py-0.2.0}/tests/test_config.py +13 -0
  26. scrapeflow_py-0.2.0/tests/test_engine_orchestration.py +125 -0
  27. scrapeflow_py-0.2.0/tests/test_public_api_compat.py +18 -0
  28. scrapeflow_py-0.2.0/tests/test_registry.py +16 -0
  29. scrapeflow_py-0.2.0/tests/test_robots.py +20 -0
  30. scrapeflow_py-0.2.0/tests/test_specifications.py +48 -0
  31. scrapeflow_py-0.1.2/scrapeflow/__init__.py +0 -30
  32. {scrapeflow_py-0.1.2 → scrapeflow_py-0.2.0}/LICENSE +0 -0
  33. {scrapeflow_py-0.1.2 → scrapeflow_py-0.2.0}/MANIFEST.in +0 -0
  34. {scrapeflow_py-0.1.2 → scrapeflow_py-0.2.0}/examples/__init__.py +0 -0
  35. {scrapeflow_py-0.1.2 → scrapeflow_py-0.2.0}/examples/advanced_example.py +0 -0
  36. {scrapeflow_py-0.1.2 → scrapeflow_py-0.2.0}/examples/basic_usage.py +0 -0
  37. {scrapeflow_py-0.1.2 → scrapeflow_py-0.2.0}/examples/workflow_example.py +0 -0
  38. {scrapeflow_py-0.1.2 → scrapeflow_py-0.2.0}/pyproject.toml +0 -0
  39. {scrapeflow_py-0.1.2 → scrapeflow_py-0.2.0}/scrapeflow/anti_detection.py +0 -0
  40. {scrapeflow_py-0.1.2 → scrapeflow_py-0.2.0}/scrapeflow/rate_limiter.py +0 -0
  41. {scrapeflow_py-0.1.2 → scrapeflow_py-0.2.0}/scrapeflow/retry.py +0 -0
  42. {scrapeflow_py-0.1.2 → scrapeflow_py-0.2.0}/scrapeflow_py.egg-info/dependency_links.txt +0 -0
  43. {scrapeflow_py-0.1.2 → scrapeflow_py-0.2.0}/scrapeflow_py.egg-info/top_level.txt +0 -0
  44. {scrapeflow_py-0.1.2 → scrapeflow_py-0.2.0}/setup.cfg +0 -0
  45. {scrapeflow_py-0.1.2 → scrapeflow_py-0.2.0}/tests/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: scrapeflow-py
3
- Version: 0.1.2
3
+ Version: 0.2.0
4
4
  Summary: An opinionated scraping workflow engine built on Playwright
5
5
  Home-page: https://github.com/irfanalidv/scrapeflow-py
6
6
  Author: Irfan Ali
@@ -20,6 +20,7 @@ License-File: LICENSE
20
20
  Requires-Dist: playwright>=1.40.0
21
21
  Requires-Dist: aiohttp>=3.9.0
22
22
  Requires-Dist: tenacity>=8.2.0
23
+ Requires-Dist: pydantic>=2.0.0
23
24
  Dynamic: author
24
25
  Dynamic: classifier
25
26
  Dynamic: description
@@ -35,17 +36,7 @@ Dynamic: summary
35
36
 
36
37
  **An opinionated scraping workflow engine built on Playwright**
37
38
 
38
- [![GitHub](https://img.shields.io/github/license/irfanalidv/scrapeflow-py)](https://github.com/irfanalidv/scrapeflow-py/blob/main/LICENSE)
39
-
40
- [![PyPI](https://img.shields.io/pypi/v/scrapeflow-py)](https://pypi.org/project/scrapeflow-py/)
41
-
42
- [![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)
43
-
44
- [![Python](https://img.shields.io/badge/python-3.8%2B-blue)](https://www.python.org/)
45
-
46
- [![Playwright](https://img.shields.io/badge/Playwright-1.40%2B-green)](https://playwright.dev/)
47
-
48
- [![Status](https://img.shields.io/badge/status-active-success)](https://github.com/irfanalidv/scrapeflow-py)
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)
49
40
 
50
41
  ---
51
42
 
@@ -53,6 +44,12 @@ ScrapeFlow is a production-ready Python library that transforms Playwright into
53
44
 
54
45
  ## 🚀 Features
55
46
 
47
+ - **📋 Specification-Driven Extraction**: Declarative Pydantic models define fields, types, and validation—decouple field definitions from page structure
48
+ - **🤖 robots.txt Compliance**: Built-in robots.txt parsing and enforcement; ethical crawling by design
49
+ - **⚖️ Ethical Crawling (GDPR/CCPA)**: Configurable data retention, anonymization, and consent options in the specification layer
50
+ - **📦 Component Registry**: Shared, versioned selectors, pagination handlers, and login flows—platform thinking over one-off scrapers
51
+ - **🔔 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
56
53
  - **🔄 Intelligent Retry Logic**: Automatic retries with exponential backoff and jitter
57
54
  - **⚡ Rate Limiting**: Token bucket algorithm to respect server limits
58
55
  - **🕵️ Anti-Detection**: Stealth mode, user agent rotation, and proxy support
@@ -62,6 +59,10 @@ ScrapeFlow is a production-ready Python library that transforms Playwright into
62
59
  - **🔧 Error Handling**: Comprehensive error classification and recovery
63
60
  - **📝 Type Hints**: Full type support for better IDE experience
64
61
 
62
+ <p align="center">
63
+ <img src="docs/scrapeflow-architecture.png" alt="ScrapeFlow Architecture" width="800">
64
+ </p>
65
+
65
66
  ## 📦 Installation
66
67
 
67
68
  ```bash
@@ -414,6 +415,63 @@ config = ScrapeFlowConfig(
414
415
  )
415
416
  ```
416
417
 
418
+ ### Specification-Driven Extraction (Pydantic)
419
+
420
+ **Use Case:** Declarative extraction with validation—fields, types, and rules in specs, not fragile XPaths.
421
+
422
+ ```python
423
+ from pydantic import BaseModel
424
+ from scrapeflow import ScrapeFlow, SpecificationExtractor
425
+ from scrapeflow.specifications import FieldSpec, ItemSpec, ProductPriceSpec
426
+
427
+ # Model for list of products
428
+ class BookListing(BaseModel):
429
+ books: list[ProductPriceSpec]
430
+
431
+ # Schema maps fields to selectors
432
+ schema = {
433
+ "books": ItemSpec(
434
+ items_selector="article.product_pod",
435
+ fields={
436
+ "title": FieldSpec(selector="h3 a"),
437
+ "price": FieldSpec(selector=".price_color"),
438
+ "availability": FieldSpec(selector=".instock.availability", default=""),
439
+ "url": FieldSpec(selector="h3 a", type="attribute", attribute="href"),
440
+ },
441
+ )
442
+ }
443
+
444
+ async with ScrapeFlow() as scraper:
445
+ await scraper.navigate("https://books.toscrape.com/")
446
+ extractor = SpecificationExtractor(BookListing, schema=schema)
447
+ # Extract and validate in one step
448
+ data = await extractor.extract(scraper.page)
449
+ for book in data.books:
450
+ print(f"{book.title}: {book.price}")
451
+ ```
452
+
453
+ ### Ethical Crawling & robots.txt
454
+
455
+ **Use Case:** GDPR/CCPA compliance and robots.txt respect built into the specification layer.
456
+
457
+ ```python
458
+ from scrapeflow import ScrapeFlow
459
+ from scrapeflow.config import ScrapeFlowConfig, EthicalCrawlingConfig
460
+
461
+ config = ScrapeFlowConfig(
462
+ ethical_crawling=EthicalCrawlingConfig(
463
+ respect_robots_txt=True, # Check robots.txt before each request
464
+ user_agent_for_robots="ScrapeFlow",
465
+ anonymize_ip=True, # GDPR: minimize personal data
466
+ data_retention_days=30, # Document retention policy
467
+ )
468
+ )
469
+
470
+ async with ScrapeFlow(config) as scraper:
471
+ # navigate() automatically checks robots.txt
472
+ await scraper.navigate("https://example.com/page")
473
+ ```
474
+
417
475
  ### Anti-Detection
418
476
 
419
477
  **Use Case:** Scraping protected e-commerce sites that block automated access.
@@ -839,16 +897,26 @@ ScrapeFlow is built with a modular architecture:
839
897
  ```
840
898
  scrapeflow/
841
899
  ├── engine.py # Main ScrapeFlow engine
842
- ├── workflow.py # Workflow definition and execution
843
- ├── config.py # Configuration classes
900
+ ├── ports.py # Protocols for dependency inversion
901
+ ├── browser_runtime.py # Playwright runtime adapter
902
+ ├── workflow.py # Workflow definition entities
903
+ ├── 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
844
910
  ├── anti_detection.py # Anti-detection utilities
845
911
  ├── rate_limiter.py # Rate limiting implementation
846
912
  ├── retry.py # Retry logic and error classification
847
- ├── monitoring.py # Metrics and logging
913
+ ├── monitoring.py # Metrics, logging, alerting
848
914
  ├── extractors.py # Data extraction utilities
849
915
  └── exceptions.py # Custom exceptions
850
916
  ```
851
917
 
918
+ For deeper design details, see [`ARCHITECTURE.md`](ARCHITECTURE.md).
919
+
852
920
  ## 🤝 Contributing
853
921
 
854
922
  Contributions are welcome! Please feel free to submit a Pull Request.
@@ -2,17 +2,7 @@
2
2
 
3
3
  **An opinionated scraping workflow engine built on Playwright**
4
4
 
5
- [![GitHub](https://img.shields.io/github/license/irfanalidv/scrapeflow-py)](https://github.com/irfanalidv/scrapeflow-py/blob/main/LICENSE)
6
-
7
- [![PyPI](https://img.shields.io/pypi/v/scrapeflow-py)](https://pypi.org/project/scrapeflow-py/)
8
-
9
- [![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)
10
-
11
- [![Python](https://img.shields.io/badge/python-3.8%2B-blue)](https://www.python.org/)
12
-
13
- [![Playwright](https://img.shields.io/badge/Playwright-1.40%2B-green)](https://playwright.dev/)
14
-
15
- [![Status](https://img.shields.io/badge/status-active-success)](https://github.com/irfanalidv/scrapeflow-py)
5
+ [![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)
16
6
 
17
7
  ---
18
8
 
@@ -20,6 +10,12 @@ ScrapeFlow is a production-ready Python library that transforms Playwright into
20
10
 
21
11
  ## 🚀 Features
22
12
 
13
+ - **📋 Specification-Driven Extraction**: Declarative Pydantic models define fields, types, and validation—decouple field definitions from page structure
14
+ - **🤖 robots.txt Compliance**: Built-in robots.txt parsing and enforcement; ethical crawling by design
15
+ - **⚖️ Ethical Crawling (GDPR/CCPA)**: Configurable data retention, anonymization, and consent options in the specification layer
16
+ - **📦 Component Registry**: Shared, versioned selectors, pagination handlers, and login flows—platform thinking over one-off scrapers
17
+ - **🔔 Monitoring & Alerting**: Alert callbacks on failure thresholds; rollback hooks for failed extraction runs
18
+ - **🔌 MCP Extensibility**: Pluggable backends for Scrapy MCP Server, Playwright MCP, or LLM-based semantic extraction
23
19
  - **🔄 Intelligent Retry Logic**: Automatic retries with exponential backoff and jitter
24
20
  - **⚡ Rate Limiting**: Token bucket algorithm to respect server limits
25
21
  - **🕵️ Anti-Detection**: Stealth mode, user agent rotation, and proxy support
@@ -29,6 +25,10 @@ ScrapeFlow is a production-ready Python library that transforms Playwright into
29
25
  - **🔧 Error Handling**: Comprehensive error classification and recovery
30
26
  - **📝 Type Hints**: Full type support for better IDE experience
31
27
 
28
+ <p align="center">
29
+ <img src="docs/scrapeflow-architecture.png" alt="ScrapeFlow Architecture" width="800">
30
+ </p>
31
+
32
32
  ## 📦 Installation
33
33
 
34
34
  ```bash
@@ -381,6 +381,63 @@ config = ScrapeFlowConfig(
381
381
  )
382
382
  ```
383
383
 
384
+ ### Specification-Driven Extraction (Pydantic)
385
+
386
+ **Use Case:** Declarative extraction with validation—fields, types, and rules in specs, not fragile XPaths.
387
+
388
+ ```python
389
+ from pydantic import BaseModel
390
+ from scrapeflow import ScrapeFlow, SpecificationExtractor
391
+ from scrapeflow.specifications import FieldSpec, ItemSpec, ProductPriceSpec
392
+
393
+ # Model for list of products
394
+ class BookListing(BaseModel):
395
+ books: list[ProductPriceSpec]
396
+
397
+ # Schema maps fields to selectors
398
+ schema = {
399
+ "books": ItemSpec(
400
+ items_selector="article.product_pod",
401
+ fields={
402
+ "title": FieldSpec(selector="h3 a"),
403
+ "price": FieldSpec(selector=".price_color"),
404
+ "availability": FieldSpec(selector=".instock.availability", default=""),
405
+ "url": FieldSpec(selector="h3 a", type="attribute", attribute="href"),
406
+ },
407
+ )
408
+ }
409
+
410
+ async with ScrapeFlow() as scraper:
411
+ await scraper.navigate("https://books.toscrape.com/")
412
+ extractor = SpecificationExtractor(BookListing, schema=schema)
413
+ # Extract and validate in one step
414
+ data = await extractor.extract(scraper.page)
415
+ for book in data.books:
416
+ print(f"{book.title}: {book.price}")
417
+ ```
418
+
419
+ ### Ethical Crawling & robots.txt
420
+
421
+ **Use Case:** GDPR/CCPA compliance and robots.txt respect built into the specification layer.
422
+
423
+ ```python
424
+ from scrapeflow import ScrapeFlow
425
+ from scrapeflow.config import ScrapeFlowConfig, EthicalCrawlingConfig
426
+
427
+ config = ScrapeFlowConfig(
428
+ ethical_crawling=EthicalCrawlingConfig(
429
+ respect_robots_txt=True, # Check robots.txt before each request
430
+ user_agent_for_robots="ScrapeFlow",
431
+ anonymize_ip=True, # GDPR: minimize personal data
432
+ data_retention_days=30, # Document retention policy
433
+ )
434
+ )
435
+
436
+ async with ScrapeFlow(config) as scraper:
437
+ # navigate() automatically checks robots.txt
438
+ await scraper.navigate("https://example.com/page")
439
+ ```
440
+
384
441
  ### Anti-Detection
385
442
 
386
443
  **Use Case:** Scraping protected e-commerce sites that block automated access.
@@ -806,16 +863,26 @@ ScrapeFlow is built with a modular architecture:
806
863
  ```
807
864
  scrapeflow/
808
865
  ├── engine.py # Main ScrapeFlow engine
809
- ├── workflow.py # Workflow definition and execution
810
- ├── config.py # Configuration classes
866
+ ├── ports.py # Protocols for dependency inversion
867
+ ├── browser_runtime.py # Playwright runtime adapter
868
+ ├── workflow.py # Workflow definition entities
869
+ ├── workflow_executor.py # Workflow execution service
870
+ ├── config.py # Configuration classes (incl. EthicalCrawlingConfig)
871
+ ├── specifications.py # Pydantic specification-driven extraction
872
+ ├── schema_library.py # Reusable schema definitions
873
+ ├── robots.py # robots.txt parsing and enforcement
874
+ ├── registry.py # Shared selector/component registry
875
+ ├── mcp_backend.py # MCP integration extensibility
811
876
  ├── anti_detection.py # Anti-detection utilities
812
877
  ├── rate_limiter.py # Rate limiting implementation
813
878
  ├── retry.py # Retry logic and error classification
814
- ├── monitoring.py # Metrics and logging
879
+ ├── monitoring.py # Metrics, logging, alerting
815
880
  ├── extractors.py # Data extraction utilities
816
881
  └── exceptions.py # Custom exceptions
817
882
  ```
818
883
 
884
+ For deeper design details, see [`ARCHITECTURE.md`](ARCHITECTURE.md).
885
+
819
886
  ## 🤝 Contributing
820
887
 
821
888
  Contributions are welcome! Please feel free to submit a Pull Request.
@@ -0,0 +1,77 @@
1
+ """
2
+ Authenticated extraction example using quotes.toscrape.com.
3
+
4
+ Demonstrates:
5
+ - login workflow with username/password
6
+ - authenticated data extraction using SpecificationExtractor
7
+ - robots.txt-aware navigation
8
+ """
9
+
10
+ import asyncio
11
+ from pydantic import BaseModel
12
+
13
+ from scrapeflow import (
14
+ ScrapeFlow,
15
+ SpecificationExtractor,
16
+ get_registry,
17
+ register_quotes_login_handler,
18
+ )
19
+ from scrapeflow.config import ScrapeFlowConfig, EthicalCrawlingConfig
20
+ from scrapeflow.specifications import FieldSpec, ItemSpec
21
+
22
+
23
+ class QuoteItem(BaseModel):
24
+ text: str
25
+ author: str
26
+
27
+
28
+ class QuotesPage(BaseModel):
29
+ quotes: list[QuoteItem]
30
+
31
+
32
+ async def run_authenticated_scrape(username: str, password: str) -> None:
33
+ schema = {
34
+ "quotes": ItemSpec(
35
+ items_selector=".quote",
36
+ fields={
37
+ "text": FieldSpec(selector=".text"),
38
+ "author": FieldSpec(selector=".author"),
39
+ },
40
+ )
41
+ }
42
+
43
+ config = ScrapeFlowConfig(
44
+ ethical_crawling=EthicalCrawlingConfig(
45
+ respect_robots_txt=True,
46
+ user_agent_for_robots="ScrapeFlow",
47
+ )
48
+ )
49
+
50
+ async with ScrapeFlow(config) as scraper:
51
+ register_quotes_login_handler(get_registry())
52
+ handler = get_registry().get_login("quotes_login")
53
+ if handler is None:
54
+ raise RuntimeError("Missing quotes_login handler in registry")
55
+
56
+ is_logged_in = await scraper.login_with_handler(
57
+ login_url="https://quotes.toscrape.com/login",
58
+ username=username,
59
+ password=password,
60
+ handler=handler,
61
+ timeout=10000,
62
+ )
63
+ if not is_logged_in:
64
+ error_text = await scraper.page.locator(".error").first.text_content()
65
+ raise RuntimeError(f"Login failed: {(error_text or '').strip()}")
66
+
67
+ extractor = SpecificationExtractor(QuotesPage, schema=schema)
68
+ data = await extractor.extract(scraper.page)
69
+
70
+ print(f"Authenticated login: {is_logged_in}")
71
+ print(f"Quotes extracted: {len(data.quotes)}")
72
+ for idx, quote in enumerate(data.quotes[:3], start=1):
73
+ print(f"{idx}. {quote.author}: {quote.text[:80]}...")
74
+
75
+
76
+ if __name__ == "__main__":
77
+ asyncio.run(run_authenticated_scrape(username="admin", password="amdin"))
@@ -0,0 +1,80 @@
1
+ """
2
+ Specification-driven extraction example - aligns with PST.AG job requirements.
3
+
4
+ Demonstrates:
5
+ - Pydantic specification-driven extraction
6
+ - robots.txt compliance
7
+ - Ethical crawling (GDPR/CCPA config)
8
+ - Shared selector registry
9
+ - Monitoring with alerting
10
+ """
11
+
12
+ import asyncio
13
+ from pydantic import BaseModel, Field
14
+
15
+ from scrapeflow import ScrapeFlow, SpecificationExtractor
16
+ from scrapeflow.config import (
17
+ ScrapeFlowConfig,
18
+ EthicalCrawlingConfig,
19
+ RateLimitConfig,
20
+ )
21
+ from scrapeflow.specifications import FieldSpec, ItemSpec, JobListingSpec
22
+ from scrapeflow.registry import get_registry, register_job_listing_schema
23
+
24
+
25
+ class JobListingsPage(BaseModel):
26
+ """Output model for job listings extraction."""
27
+
28
+ jobs: list[JobListingSpec]
29
+
30
+
31
+ async def scrape_job_listings():
32
+ """Scrape job listings using specification-driven extraction."""
33
+ # Ethical crawling: robots.txt, rate limiting
34
+ config = ScrapeFlowConfig(
35
+ ethical_crawling=EthicalCrawlingConfig(
36
+ respect_robots_txt=True,
37
+ user_agent_for_robots="ScrapeFlow",
38
+ ),
39
+ rate_limit=RateLimitConfig(requests_per_second=1.0),
40
+ )
41
+
42
+ schema = {
43
+ "jobs": ItemSpec(
44
+ items_selector=".job-card, .job-listing, article.job, .job-item, .quote",
45
+ fields={
46
+ "title": FieldSpec(selector="h2 a, .job-title, .title a, .text"),
47
+ "company": FieldSpec(
48
+ selector=".company, .employer, .author",
49
+ default="",
50
+ ),
51
+ "location": FieldSpec(
52
+ selector=".location, .job-location",
53
+ default="",
54
+ ),
55
+ "url": FieldSpec(
56
+ selector="h2 a, .job-title a, a",
57
+ type="attribute",
58
+ attribute="href",
59
+ ),
60
+ },
61
+ )
62
+ }
63
+
64
+ async with ScrapeFlow(config) as scraper:
65
+ # navigate() checks robots.txt automatically
66
+ await scraper.navigate("https://quotes.toscrape.com/")
67
+
68
+ extractor = SpecificationExtractor(JobListingsPage, schema=schema)
69
+ data = await extractor.extract(scraper.page)
70
+
71
+ print(f"Extracted {len(data.jobs)} items (validated via Pydantic)")
72
+ for i, job in enumerate(data.jobs[:3]):
73
+ print(f" {i+1}. {job.title[:50]}... | {job.company or 'N/A'}")
74
+
75
+ metrics = scraper.get_metrics()
76
+ print(f"\nMetrics: {metrics.total_requests} requests, {metrics.get_success_rate():.1f}% success")
77
+
78
+
79
+ if __name__ == "__main__":
80
+ asyncio.run(scrape_job_listings())
@@ -1,4 +1,5 @@
1
1
  playwright>=1.40.0
2
2
  aiohttp>=3.9.0
3
3
  tenacity>=8.2.0
4
+ pydantic>=2.0.0
4
5
 
@@ -0,0 +1,71 @@
1
+ """
2
+ ScrapeFlow - An opinionated scraping workflow engine built on Playwright.
3
+
4
+ ScrapeFlow provides a robust, production-ready framework for web scraping
5
+ with built-in error handling, retry logic, anti-detection, and workflow management.
6
+
7
+ Specification-driven extraction, robots.txt compliance, and ethical crawling
8
+ (GDPR/CCPA) are built into the design.
9
+ """
10
+
11
+ from scrapeflow.engine import ScrapeFlow
12
+ from scrapeflow.workflow import Workflow, Step
13
+ from scrapeflow.workflow_executor import WorkflowExecutor
14
+ from scrapeflow.extractors import Extractor, StructuredExtractor
15
+ from scrapeflow.specifications import (
16
+ FieldSpec,
17
+ ItemSpec,
18
+ SpecificationExtractor,
19
+ ProductPriceSpec,
20
+ JobListingSpec,
21
+ TariffCodeSpec,
22
+ )
23
+ from scrapeflow.robots import RobotsChecker
24
+ from scrapeflow.registry import (
25
+ SelectorRegistry,
26
+ LoginHandler,
27
+ get_registry,
28
+ register_quotes_login_handler,
29
+ )
30
+ from scrapeflow.browser_runtime import PlaywrightBrowserRuntime
31
+ from scrapeflow.mcp_backend import MCPBackend, PlaceholderMCPBackend, create_mcp_backend
32
+ from scrapeflow.exceptions import (
33
+ ScrapeFlowError,
34
+ ScrapeFlowRetryError,
35
+ ScrapeFlowTimeoutError,
36
+ ScrapeFlowBlockedError,
37
+ ScrapeFlowValidationError,
38
+ ScrapeFlowRobotsDisallowedError,
39
+ )
40
+
41
+ __version__ = "0.2.0"
42
+ __all__ = [
43
+ "ScrapeFlow",
44
+ "Workflow",
45
+ "Step",
46
+ "WorkflowExecutor",
47
+ "Extractor",
48
+ "StructuredExtractor",
49
+ "FieldSpec",
50
+ "ItemSpec",
51
+ "SpecificationExtractor",
52
+ "ProductPriceSpec",
53
+ "JobListingSpec",
54
+ "TariffCodeSpec",
55
+ "RobotsChecker",
56
+ "SelectorRegistry",
57
+ "LoginHandler",
58
+ "get_registry",
59
+ "register_quotes_login_handler",
60
+ "PlaywrightBrowserRuntime",
61
+ "MCPBackend",
62
+ "PlaceholderMCPBackend",
63
+ "create_mcp_backend",
64
+ "ScrapeFlowError",
65
+ "ScrapeFlowRetryError",
66
+ "ScrapeFlowTimeoutError",
67
+ "ScrapeFlowBlockedError",
68
+ "ScrapeFlowValidationError",
69
+ "ScrapeFlowRobotsDisallowedError",
70
+ ]
71
+
@@ -0,0 +1,96 @@
1
+ """Playwright browser runtime adapter."""
2
+
3
+ from typing import Optional
4
+ from playwright.async_api import (
5
+ async_playwright,
6
+ Playwright,
7
+ Browser,
8
+ BrowserContext,
9
+ Page,
10
+ )
11
+
12
+ from scrapeflow.config import ScrapeFlowConfig, BrowserType
13
+ from scrapeflow.anti_detection import AntiDetectionManager
14
+
15
+
16
+ class PlaywrightBrowserRuntime:
17
+ """Infrastructure adapter for Playwright lifecycle and navigation."""
18
+
19
+ def __init__(self, config: ScrapeFlowConfig, anti_detection: AntiDetectionManager):
20
+ self.config = config
21
+ self.anti_detection = anti_detection
22
+ self.playwright: Optional[Playwright] = None
23
+ self.browser: Optional[Browser] = None
24
+ self.context: Optional[BrowserContext] = None
25
+ self._page: Optional[Page] = None
26
+ self._is_running = False
27
+
28
+ @property
29
+ def page(self) -> Optional[Page]:
30
+ return self._page
31
+
32
+ async def start(self) -> None:
33
+ if self._is_running:
34
+ return
35
+
36
+ self.playwright = await async_playwright().start()
37
+ browser_type_map = {
38
+ BrowserType.CHROMIUM: self.playwright.chromium,
39
+ BrowserType.FIREFOX: self.playwright.firefox,
40
+ BrowserType.WEBKIT: self.playwright.webkit,
41
+ }
42
+ browser_launcher = browser_type_map[self.config.browser.browser_type]
43
+
44
+ proxy = self.config.browser.proxy or self.anti_detection.get_proxy()
45
+ browser_args = self.config.browser.args.copy()
46
+ if self.config.anti_detection.stealth_mode:
47
+ browser_args.extend(
48
+ [
49
+ "--disable-blink-features=AutomationControlled",
50
+ "--disable-dev-shm-usage",
51
+ "--no-sandbox",
52
+ ]
53
+ )
54
+
55
+ self.browser = await browser_launcher.launch(
56
+ headless=self.config.browser.headless,
57
+ slow_mo=self.config.browser.slow_mo,
58
+ args=browser_args,
59
+ proxy=proxy,
60
+ )
61
+
62
+ context_options = {
63
+ "viewport": {
64
+ "width": self.config.anti_detection.viewport_width,
65
+ "height": self.config.anti_detection.viewport_height,
66
+ },
67
+ "user_agent": self.anti_detection.get_user_agent(),
68
+ }
69
+ self.context = await self.browser.new_context(**context_options)
70
+ self._page = await self.context.new_page()
71
+ await self.anti_detection.apply_stealth(self._page)
72
+ self._page.set_default_timeout(self.config.browser.timeout)
73
+ self._is_running = True
74
+
75
+ async def close(self) -> None:
76
+ if not self._is_running:
77
+ return
78
+
79
+ if self._page:
80
+ await self._page.close()
81
+ self._page = None
82
+ if self.context:
83
+ await self.context.close()
84
+ self.context = None
85
+ if self.browser:
86
+ await self.browser.close()
87
+ self.browser = None
88
+ if self.playwright:
89
+ await self.playwright.stop()
90
+ self.playwright = None
91
+ self._is_running = False
92
+
93
+ async def goto(self, url: str, wait_until: str, timeout: int) -> None:
94
+ if not self._page:
95
+ raise RuntimeError("Browser runtime is not started.")
96
+ await self._page.goto(url, wait_until=wait_until, timeout=timeout)
@@ -61,6 +61,24 @@ class BrowserConfig:
61
61
  proxy: Optional[Dict[str, str]] = None
62
62
 
63
63
 
64
+ @dataclass
65
+ class EthicalCrawlingConfig:
66
+ """
67
+ Ethical crawling configuration—GDPR/CCPA baked into specification design.
68
+
69
+ Rate limiting, robots.txt respect, and compliance are built into the
70
+ specification layer, not retrofitted.
71
+ """
72
+
73
+ respect_robots_txt: bool = True
74
+ user_agent_for_robots: str = "ScrapeFlow"
75
+ max_pages_per_domain: Optional[int] = None
76
+ honor_noindex: bool = True
77
+ data_retention_days: Optional[int] = None # GDPR: document retention
78
+ anonymize_ip: bool = False # GDPR: minimize personal data
79
+ consent_required: bool = False # GDPR: require consent for tracking
80
+
81
+
64
82
  @dataclass
65
83
  class ScrapeFlowConfig:
66
84
  """Main configuration for ScrapeFlow."""
@@ -69,6 +87,9 @@ class ScrapeFlowConfig:
69
87
  retry: RetryConfig = field(default_factory=RetryConfig)
70
88
  rate_limit: RateLimitConfig = field(default_factory=RateLimitConfig)
71
89
  anti_detection: AntiDetectionConfig = field(default_factory=AntiDetectionConfig)
90
+ ethical_crawling: EthicalCrawlingConfig = field(
91
+ default_factory=EthicalCrawlingConfig
92
+ )
72
93
  log_level: str = "INFO"
73
94
  log_file: Optional[str] = None
74
95
  debug: bool = False