pagefetch 0.1.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 (49) hide show
  1. pagefetch-0.1.0/LICENSE +22 -0
  2. pagefetch-0.1.0/PKG-INFO +423 -0
  3. pagefetch-0.1.0/README.md +398 -0
  4. pagefetch-0.1.0/pagefetch/__init__.py +29 -0
  5. pagefetch-0.1.0/pagefetch/__main__.py +4 -0
  6. pagefetch-0.1.0/pagefetch/bootstrap.py +113 -0
  7. pagefetch-0.1.0/pagefetch/cache/__init__.py +7 -0
  8. pagefetch-0.1.0/pagefetch/cache/keys.py +28 -0
  9. pagefetch-0.1.0/pagefetch/cache/sqlite.py +103 -0
  10. pagefetch-0.1.0/pagefetch/cli.py +165 -0
  11. pagefetch-0.1.0/pagefetch/client.py +643 -0
  12. pagefetch-0.1.0/pagefetch/config.py +167 -0
  13. pagefetch-0.1.0/pagefetch/constants.py +32 -0
  14. pagefetch-0.1.0/pagefetch/exceptions.py +15 -0
  15. pagefetch-0.1.0/pagefetch/fetching/__init__.py +7 -0
  16. pagefetch-0.1.0/pagefetch/fetching/browser.py +261 -0
  17. pagefetch-0.1.0/pagefetch/fetching/http.py +130 -0
  18. pagefetch-0.1.0/pagefetch/fetching/readiness.py +81 -0
  19. pagefetch-0.1.0/pagefetch/models.py +100 -0
  20. pagefetch-0.1.0/pagefetch/processing/__init__.py +6 -0
  21. pagefetch-0.1.0/pagefetch/processing/cleaner.py +39 -0
  22. pagefetch-0.1.0/pagefetch/processing/detector.py +142 -0
  23. pagefetch-0.1.0/pagefetch/processing/html.py +47 -0
  24. pagefetch-0.1.0/pagefetch/processing/images.py +31 -0
  25. pagefetch-0.1.0/pagefetch/processing/links.py +40 -0
  26. pagefetch-0.1.0/pagefetch/processing/markdown.py +217 -0
  27. pagefetch-0.1.0/pagefetch/processing/metadata.py +84 -0
  28. pagefetch-0.1.0/pagefetch/processing/non_html.py +74 -0
  29. pagefetch-0.1.0/pagefetch/proxy/__init__.py +6 -0
  30. pagefetch-0.1.0/pagefetch/proxy/providers.py +84 -0
  31. pagefetch-0.1.0/pagefetch/utils/__init__.py +2 -0
  32. pagefetch-0.1.0/pagefetch/utils/durations.py +25 -0
  33. pagefetch-0.1.0/pagefetch/utils/urls.py +68 -0
  34. pagefetch-0.1.0/pagefetch.egg-info/PKG-INFO +423 -0
  35. pagefetch-0.1.0/pagefetch.egg-info/SOURCES.txt +47 -0
  36. pagefetch-0.1.0/pagefetch.egg-info/dependency_links.txt +1 -0
  37. pagefetch-0.1.0/pagefetch.egg-info/entry_points.txt +2 -0
  38. pagefetch-0.1.0/pagefetch.egg-info/requires.txt +15 -0
  39. pagefetch-0.1.0/pagefetch.egg-info/top_level.txt +1 -0
  40. pagefetch-0.1.0/pyproject.toml +46 -0
  41. pagefetch-0.1.0/setup.cfg +4 -0
  42. pagefetch-0.1.0/tests/test_bootstrap.py +70 -0
  43. pagefetch-0.1.0/tests/test_browser.py +183 -0
  44. pagefetch-0.1.0/tests/test_client.py +306 -0
  45. pagefetch-0.1.0/tests/test_config_cli.py +25 -0
  46. pagefetch-0.1.0/tests/test_non_html.py +83 -0
  47. pagefetch-0.1.0/tests/test_processing.py +114 -0
  48. pagefetch-0.1.0/tests/test_proxy_cache.py +78 -0
  49. pagefetch-0.1.0/tests/test_utils_models.py +56 -0
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 PageFetch contributors
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.
22
+
@@ -0,0 +1,423 @@
1
+ Metadata-Version: 2.4
2
+ Name: pagefetch
3
+ Version: 0.1.0
4
+ Summary: Asynchronous HTTP-first web fetching with conservative content extraction and browser fallback
5
+ Author: PageFetch contributors
6
+ License-Expression: MIT
7
+ Requires-Python: >=3.11
8
+ Description-Content-Type: text/markdown
9
+ License-File: LICENSE
10
+ Requires-Dist: aiosqlite<1,>=0.20
11
+ Requires-Dist: beautifulsoup4<5,>=4.12
12
+ Requires-Dist: brotlicffi<2,>=1.0
13
+ Requires-Dist: camoufox<1,>=0.4
14
+ Requires-Dist: httpx[http2,socks]<1,>=0.27
15
+ Requires-Dist: lxml<6,>=5.2
16
+ Requires-Dist: platformdirs<5,>=4.2
17
+ Requires-Dist: pypdf<6,>=5.0
18
+ Requires-Dist: pyyaml<7,>=6.0
19
+ Requires-Dist: tldextract<6,>=5.1
20
+ Provides-Extra: test
21
+ Requires-Dist: pytest<9,>=8.0; extra == "test"
22
+ Requires-Dist: pytest-asyncio<1,>=0.23; extra == "test"
23
+ Requires-Dist: ruff<1,>=0.6; extra == "test"
24
+ Dynamic: license-file
25
+
26
+ # PageFetch
27
+
28
+ **Async-first web scraping and content extraction library for Python 3.11+ — with
29
+ intelligent browser fallback, built-in caching, and structured Markdown output.**
30
+
31
+ <p align="center">
32
+ <img src="https://img.shields.io/badge/python-3.11%2B-blue" alt="Python 3.11+">
33
+ <img src="https://img.shields.io/badge/license-MIT-green" alt="MIT License">
34
+ <img src="https://img.shields.io/badge/code%20style-ruff-261230" alt="Ruff">
35
+ </p>
36
+
37
+ PageFetch is a modern, asynchronous Python library for **fetching web pages** and
38
+ extracting clean, structured content. It attempts a pooled **HTTP request** first,
39
+ scores the returned HTML using multiple **completeness signals**, and lazily falls
40
+ back to a **Camoufox stealth browser** when the page looks blocked, empty, or
41
+ dependent on client-side JavaScript rendering. The result is always a consistent
42
+ `FetchResult` object carrying **Markdown**, raw HTML, extracted links, images,
43
+ metadata, and more — regardless of which method succeeded.
44
+
45
+ Perfect for **web scraping**, **content aggregation**, **LLM data pipelines**,
46
+ **SEO analysis**, **archiving**, and any workflow that needs reliable page
47
+ content without fighting bot detection.
48
+
49
+ ---
50
+
51
+ ## Table of Contents
52
+
53
+ - [Quick Start](#quick-start)
54
+ - [Why PageFetch](#why-pagefetch)
55
+ - [How It Works](#how-it-works)
56
+ - [Installation](#installation)
57
+ - [API Reference](#api-reference)
58
+ - [Output Model](#output-model)
59
+ - [CLI Usage](#cli-usage)
60
+ - [Caching](#caching)
61
+ - [Proxy Support](#proxy-support)
62
+ - [Non-HTML Content](#non-html-content)
63
+ - [Configuration Reference](#configuration-reference)
64
+ - [Development](#development)
65
+ - [License](#license)
66
+
67
+ ---
68
+
69
+ ## Quick Start
70
+
71
+ ```python
72
+ import asyncio
73
+
74
+ from pagefetch import PageFetch
75
+
76
+
77
+ async def main() -> None:
78
+ async with PageFetch(mode="auto", cache_ttl="24h") as client:
79
+ result = await client.fetch("https://example.com")
80
+ if result.success:
81
+ print(f"Title: {result.title}")
82
+ print(f"Markdown length: {len(result.markdown or '')} chars")
83
+ print(f"Links found: {len(result.links or [])}")
84
+ print(f"Images found: {len(result.images or [])}")
85
+ else:
86
+ print(f"Failed: {result.error.message}")
87
+
88
+
89
+ asyncio.run(main())
90
+ ```
91
+
92
+ ### Fetch Multiple URLs in Parallel
93
+
94
+ ```python
95
+ async with PageFetch(mode="auto") as client:
96
+ results = await client.fetch_many([
97
+ "https://example.com",
98
+ "https://example.org",
99
+ "https://httpbin.org/html",
100
+ ])
101
+ for r in results:
102
+ status = "✓" if r.success else "✗"
103
+ print(f"{status} {r.url} ({r.fetch_method}, {r.duration_ms:.0f}ms)")
104
+ ```
105
+
106
+ ---
107
+
108
+ ## Why PageFetch
109
+
110
+ | Challenge | PageFetch Solution |
111
+ |---|---|
112
+ | **Bot detection & blocking** | Camoufox stealth browser with automatic fallback when HTTP returns empty, blocked (403/429), or JavaScript-dependent pages |
113
+ | **Over-fetching with heavy browsers** | HTTP-first strategy — only ~5–15% of pages need the browser in auto mode |
114
+ | **Inconsistent output formats** | Single `FetchResult` model: always get `.markdown`, `.html`, `.text`, `.links`, `.images`, `.metadata` |
115
+ | **Managing concurrency** | Built-in semaphores for HTTP (default 10) and browser (default 4) — safe for hundreds of URLs |
116
+ | **Repeated requests waste bandwidth** | SQLite disk cache with configurable TTL, shared across runs |
117
+ | **Proxy rotation complexity** | Native Decodo and DataImpulse integration — configure via env vars |
118
+ | **Content that isn't HTML** | PDFs auto-detected and extracted; XML documents parsed; plain text preserved |
119
+ | **Dependency management friction** | Automatic bootstrap installs missing Python packages and the Camoufox binary at first startup |
120
+
121
+ ---
122
+
123
+ ## How It Works
124
+
125
+ 1. **Normalize** the URL (scheme, encoding, fragments).
126
+ 2. **Check cache** — if a valid SQLite entry exists, return instantly.
127
+ 3. **HTTP fetch** using `httpx` with HTTP/2, connection pooling, and configurable retries.
128
+ Non-HTML responses (PDF, XML, plain text) are handled directly without browser overhead.
129
+ 4. **Content analysis** — the `confidence` score evaluates HTML completeness using
130
+ text density, structural markup, heading presence, link counts, and common blocking signals
131
+ (captcha walls, empty bodies, access-denied patterns).
132
+ 5. **Browser fallback** (auto mode only) — if confidence is below the threshold (default 0.80),
133
+ Camoufox takes over: renders JavaScript, scrolls for lazy-loaded content, waits for
134
+ network stability, and re-extracts.
135
+ 6. **Processing pipeline** — cleaned HTML → extracted links, images, metadata →
136
+ converted to Markdown via a custom converter that preserves tables, code blocks,
137
+ and nested lists.
138
+ 7. **Cache & return** — the structured `FetchResult` is persisted to SQLite and returned.
139
+
140
+ ---
141
+
142
+ ## Installation
143
+
144
+ ### Prerequisites
145
+
146
+ - **Python** ≥ 3.11
147
+ - **Camoufox** browser binary — *downloaded automatically* at first startup
148
+
149
+ ### Install
150
+
151
+ ```bash
152
+ pip install .
153
+ ```
154
+
155
+ Automatic bootstrap is **enabled by default**. At first import, PageFetch will:
156
+
157
+ 1. Install any missing Python dependencies via `pip`.
158
+ 2. Download the Camoufox browser binary.
159
+
160
+ Subsequent starts perform quick availability checks only — the browser is launched
161
+ only when `mode="browser"` is selected or auto mode detects incomplete HTTP content.
162
+
163
+ ### Disable Auto-Install (Managed / Offline Environments)
164
+
165
+ ```bash
166
+ # Windows
167
+ set PAGEFETCH_AUTO_INSTALL=0
168
+
169
+ # Linux / macOS
170
+ export PAGEFETCH_AUTO_INSTALL=0
171
+ ```
172
+
173
+ With auto-install disabled, provision both components manually:
174
+
175
+ ```bash
176
+ pip install .
177
+ python -m camoufox fetch
178
+ ```
179
+
180
+ ---
181
+
182
+ ## API Reference
183
+
184
+ ### `PageFetch` Client
185
+
186
+ The main entry point. Use as an async context manager for automatic cleanup.
187
+
188
+ ```python
189
+ from pagefetch import PageFetch
190
+
191
+ client = PageFetch(
192
+ mode="auto", # "auto" | "http" | "browser"
193
+ proxy="none", # "none" | "decodo" | "dataimpulse"
194
+ http_concurrency=10, # Max parallel HTTP requests
195
+ browser_concurrency=4, # Max parallel browser instances
196
+ cache_enabled=True, # Enable SQLite disk cache
197
+ cache_ttl="24h", # TTL: "30m", "2h", "7d", or seconds as int
198
+ cache_path=None, # Custom SQLite cache path (None = platform default)
199
+ http_timeout=20.0, # Per-request HTTP timeout (seconds)
200
+ browser_timeout=45.0, # Per-page browser timeout (seconds)
201
+ retries_http=3, # Retry on 429/5xx for HTTP
202
+ retries_browser=2, # Retry on browser failure
203
+ max_redirects=10, # Maximum redirect chain
204
+ max_content_size=25 * 1024 * 1024, # Max response body bytes (25 MiB)
205
+ confidence_threshold=0.80, # Min confidence before browser fallback
206
+ raise_on_error=False, # Raise PageFetchError instead of returning error result
207
+ )
208
+ ```
209
+
210
+ ### Modes
211
+
212
+ | Mode | Behavior |
213
+ |---|---|
214
+ | `"auto"` | HTTP first; falls back to Camoufox if confidence < threshold (default) |
215
+ | `"http"` | Pure HTTP/2 fetching — no browser, no confidence scoring |
216
+ | `"browser"` | Camoufox stealth browser for every request |
217
+
218
+ ### Methods
219
+
220
+ **`fetch(url, *, mode=None, proxy=None, use_cache=True, cache_ttl=None, raise_on_error=None) → FetchResult`**
221
+
222
+ Fetch a single URL. All keyword arguments override the client-level defaults
223
+ for this individual request only.
224
+
225
+ **`fetch_many(urls, *, mode=None, proxy=None, use_cache=True, cache_ttl=None, raise_on_error=None) → list[FetchResult]`**
226
+
227
+ Fetch multiple URLs concurrently. Deduplicates identical inputs internally,
228
+ preserves the original input order, and isolates individual failures — one
229
+ bad URL never affects the others.
230
+
231
+ ---
232
+
233
+ ## Output Model
234
+
235
+ Every fetch returns a `FetchResult` dataclass:
236
+
237
+ ```python
238
+ from dataclasses import dataclass
239
+
240
+ @dataclass
241
+ class FetchResult:
242
+ url: str # Normalized request URL
243
+ final_url: str | None # URL after all redirects
244
+ status_code: int | None # HTTP status code
245
+ success: bool # Did the fetch succeed?
246
+ content_type: str | None # e.g. "text/html", "application/pdf"
247
+ encoding: str | None # Detected charset
248
+ title: str | None # Page <title> or PDF title
249
+ markdown: str | None # Cleaned Markdown body
250
+ html: str | None # Raw HTML (excluded from JSON by default)
251
+ text: str | None # Plain-text body fallback
252
+ metadata: dict # OpenGraph, Twitter Cards, meta tags
253
+ links: list[LinkInfo] # All <a> tags with text, URL, rel
254
+ images: list[ImageInfo] # All <img> tags with src, alt, title
255
+ fetch_method: str | None # "http" or "browser"
256
+ proxy_provider: str # "none", "decodo", or "dataimpulse"
257
+ content_confidence: float | None # 0–1 completeness score (None for browser mode)
258
+ from_cache: bool # Was this served from cache?
259
+ duration_ms: float | None # Total fetch duration
260
+ fetched_at: datetime | None # ISO 8601 timestamp
261
+ warnings: list[str] # Non-fatal issues (cache skip, etc.)
262
+ error: FetchErrorInfo | None # Error details when success=False
263
+ ```
264
+
265
+ ### Serialization
266
+
267
+ ```python
268
+ # JSON output (HTML excluded by default for compactness)
269
+ print(result.json(indent=2))
270
+ print(result.json(include_html=True)) # Include raw HTML
271
+
272
+ # Python dict
273
+ data = result.to_dict()
274
+ data = result.to_dict(include_html=True)
275
+
276
+ # Reconstruct from cached JSON
277
+ reconstructed = FetchResult.from_dict(data)
278
+ ```
279
+
280
+ ---
281
+
282
+ ## CLI Usage
283
+
284
+ PageFetch ships with a command-line interface accessible via `pagefetch`:
285
+
286
+ ```bash
287
+ # Fetch and print Markdown
288
+ pagefetch https://example.com --format markdown
289
+
290
+ # Fetch from a list and output JSON
291
+ pagefetch urls.txt --format json --mode auto
292
+
293
+ # Save output to a file
294
+ pagefetch https://example.com --mode browser -o output.md
295
+
296
+ # Structured JSON with raw HTML included
297
+ pagefetch https://example.com --format json --include-html
298
+
299
+ # Multiple URLs from a file (one URL per line)
300
+ pagefetch urls.txt --format json --mode auto
301
+ ```
302
+
303
+ CLI arguments map directly to the Python API — `--mode`, `--proxy`, `--timeout`,
304
+ `--http-concurrency`, and `--browser-concurrency` are all supported.
305
+
306
+ ---
307
+
308
+ ## Caching
309
+
310
+ PageFetch uses a **SQLite-backed disk cache** (`platformdirs` user cache directory
311
+ by default). Cache entries are keyed by normalized URL + mode + proxy + relevant
312
+ fetch settings, so switching from `"auto"` to `"browser"` mode produces a
313
+ different cache key.
314
+
315
+ - **Default TTL**: 24 hours (configurable: `"30m"`, `"2h"`, `"7d"`, or integer seconds)
316
+ - **Automatic**: cache hits skip all network and browser work
317
+ - **Graceful degradation**: cache read/write failures never crash a fetch — they produce warnings
318
+
319
+ ```python
320
+ # Disable caching for a single request
321
+ result = await client.fetch("https://example.com", use_cache=False)
322
+
323
+ # Override TTL per-request
324
+ result = await client.fetch("https://example.com", cache_ttl="1h")
325
+
326
+ # Use a custom cache location
327
+ client = PageFetch(cache_path="/path/to/custom_cache.sqlite3")
328
+ ```
329
+
330
+ ---
331
+
332
+ ## Proxy Support
333
+
334
+ PageFetch natively supports **rotating residential proxy** providers:
335
+
336
+ | Provider | Env Var (Full URL) | Env Vars (Components) |
337
+ |---|---|---|
338
+ | **Decodo** | `DECODO_PROXY_URL` | `DECODO_HOST`, `DECODO_PORT`, `DECODO_USERNAME`, `DECODO_PASSWORD` |
339
+ | **DataImpulse** | `DATAIMPULSE_PROXY_URL` | `DATAIMPULSE_HOST`, `DATAIMPULSE_PORT`, `DATAIMPULSE_USERNAME`, `DATAIMPULSE_PASSWORD` |
340
+
341
+ ```python
342
+ # Use a proxy provider
343
+ async with PageFetch(proxy="decodo") as client:
344
+ result = await client.fetch("https://example.com")
345
+ ```
346
+
347
+ Credentials are **never** included in results, logs, or cache keys. Configure
348
+ either a full proxy URL or the individual components — PageFetch validates
349
+ both forms automatically.
350
+
351
+ ---
352
+
353
+ ## Non-HTML Content
354
+
355
+ PageFetch handles content types beyond HTML natively:
356
+
357
+ | Content Type | Detection | Extraction |
358
+ |---|---|---|
359
+ | **PDF** | Magic bytes + `Content-Type` | Text via `pypdf` |
360
+ | **XML** | `Content-Type` matching `+xml` or `application/xml` | Preserved as `.text` |
361
+ | **Plain text** | Fallback when no structured type matches | Served as `.text` directly |
362
+
363
+ No browser overhead is incurred for non-HTML content — detection happens
364
+ at the HTTP response level before any processing pipeline runs.
365
+
366
+ ---
367
+
368
+ ## Configuration Reference
369
+
370
+ | Parameter | Type | Default | Description |
371
+ |---|---|---|---|
372
+ | `mode` | `str` | `"auto"` | Fetch strategy: `"auto"`, `"http"`, or `"browser"` |
373
+ | `proxy` | `str` | `"none"` | Proxy provider: `"none"`, `"decodo"`, or `"dataimpulse"` |
374
+ | `http_concurrency` | `int` | `10` | Maximum concurrent HTTP connections |
375
+ | `browser_concurrency` | `int` | `4` | Maximum concurrent browser instances |
376
+ | `cache_enabled` | `bool` | `True` | Enable SQLite disk cache |
377
+ | `cache_ttl` | `str \| int` | `"24h"` | Cache time-to-live |
378
+ | `cache_path` | `str \| Path` | *platform default* | Custom SQLite cache file path |
379
+ | `http_timeout` | `float` | `20.0` | HTTP request timeout in seconds |
380
+ | `browser_timeout` | `float` | `45.0` | Browser page load timeout in seconds |
381
+ | `retries_http` | `int` | `3` | Automatic retries on retryable HTTP errors |
382
+ | `retries_browser` | `int` | `2` | Automatic retries on browser failures |
383
+ | `max_redirects` | `int` | `10` | Maximum redirect chain to follow |
384
+ | `max_content_size` | `int` | `25 MiB` | Maximum response body in bytes |
385
+ | `confidence_threshold` | `float` | `0.80` | Threshold for browser fallback in auto mode |
386
+ | `raise_on_error` | `bool` | `False` | Raise `PageFetchError` on failure instead of returning error result |
387
+
388
+ ---
389
+
390
+ ## Development
391
+
392
+ ```bash
393
+ # Clone and set up
394
+ git clone <repo-url> && cd pagefetch
395
+
396
+ # Install with test dependencies
397
+ pip install -e ".[test]"
398
+
399
+ # Run the test suite (browser integration tests are opt-in)
400
+ pytest
401
+
402
+ # Lint
403
+ ruff check .
404
+ ```
405
+
406
+ Browser integration requires the separately downloaded Camoufox binary and is
407
+ therefore kept optional in deterministic test environments. The test suite is
408
+ designed to run fully offline — URLs are served via local fixtures.
409
+
410
+ ---
411
+
412
+ ## License
413
+
414
+ PageFetch is released under the [MIT License](LICENSE).
415
+
416
+ ---
417
+
418
+ <p align="center">
419
+ <sub>
420
+ Built with ❤️ for developers who need reliable, structured web content
421
+ without fighting bot detection.
422
+ </sub>
423
+ </p>