polite-scraper 1.0.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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 callmekriztal
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.
@@ -0,0 +1,283 @@
1
+ Metadata-Version: 2.4
2
+ Name: polite-scraper
3
+ Version: 1.0.0
4
+ Summary: A rate-limited, cache-first web scraper with a rich terminal UI, robots.txt awareness, and Pydantic validation.
5
+ Author: callmekriztal
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/callmekriztal/polite_scapper
8
+ Project-URL: Repository, https://github.com/callmekriztal/polite_scapper
9
+ Project-URL: Issues, https://github.com/callmekriztal/polite_scapper/issues
10
+ Keywords: scraper,web-scraping,crawler,polite,beautifulsoup,cli
11
+ Classifier: Development Status :: 5 - Production/Stable
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Programming Language :: Python :: 3.14
21
+ Classifier: Topic :: Internet :: WWW/HTTP :: Indexing/Search
22
+ Requires-Python: >=3.10
23
+ Description-Content-Type: text/markdown
24
+ License-File: LICENSE
25
+ Requires-Dist: requests>=2.31.0
26
+ Requires-Dist: beautifulsoup4>=4.12.0
27
+ Requires-Dist: pydantic>=2.0.0
28
+ Requires-Dist: rich>=13.0.0
29
+ Provides-Extra: dev
30
+ Requires-Dist: pytest>=7.0.0; extra == "dev"
31
+ Dynamic: license-file
32
+
33
+ <div align="center">
34
+
35
+ # ๐Ÿ•ท๏ธ The Polite Scraper
36
+
37
+ **A rate-limited, cache-first web scraper with a polished terminal UI โ€” that treats every host with respect.**
38
+
39
+ [![Python](https://img.shields.io/badge/python-3.10%2B-3776AB?logo=python&logoColor=white)](https://www.python.org/)
40
+ [![Tests](https://img.shields.io/badge/tests-10%20passing-brightgreen)](tests/)
41
+ [![Modes](https://img.shields.io/badge/modes-book%20%7C%20generic-blueviolet)](#two-modes)
42
+ [![UI](https://img.shields.io/badge/ui-rich%20terminal-FF69B4)](https://github.com/Textualize/rich)
43
+ [![Ethics](https://img.shields.io/badge/robots.txt-checked-success)](#%EF%B8%8F-politeness--ethics)
44
+
45
+ </div>
46
+
47
+ ---
48
+
49
+ The Polite Scraper crawls websites the way a good citizen would: it identifies itself with a custom User-Agent, waits at least 500 ms between live requests, caches every page it downloads so re-runs cost the server **nothing**, validates everything it extracts against strict [Pydantic](https://docs.pydantic.dev/) schemas, and survives broken pages without crashing.
50
+
51
+ Give it a URL โ€” it figures out the rest.
52
+
53
+ ```bash
54
+ python -m polite_scraper https://books.toscrape.com
55
+ ```
56
+
57
+ ```
58
+ โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ The Polite Scraper โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ
59
+ โ”‚ Target https://books.toscrape.com/ โ”‚
60
+ โ”‚ Mode book โ”‚
61
+ โ”‚ Run folder data/runs/books-toscrape-com โ”‚
62
+ โ”‚ Max pages 3 โ”‚
63
+ โ”‚ Min delay 0.5s between requests โ”‚
64
+ โ”‚ Cache data/cache โ”‚
65
+ โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ polite by design โ€” rate-limited, cached, identify-yourself โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ
66
+
67
+ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ Discovering catalogue pages โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
68
+ โœ“ 3 catalogue pages crawled โ€” 60 unique book URLs
69
+
70
+ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ Extracting & validating book details โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
71
+ โœ“ 60 valid books, 0 rejected
72
+
73
+ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ Done โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
74
+ โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ Run Summary โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ
75
+ โ”‚ Requests 0 network / 64 cache โ”‚
76
+ โ”‚ Records saved 60 โ”‚
77
+ โ”‚ Duration 0.772s โ”‚
78
+ โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ
79
+ ```
80
+
81
+ ---
82
+
83
+ ## โœจ Features
84
+
85
+ | | |
86
+ |---|---|
87
+ | ๐ŸŽฏ **One command, any site** | Pass the website as a plain argument โ€” no config files, no code changes |
88
+ | ๐Ÿง  **Auto mode detection** | Probes the target and picks the right extractor automatically |
89
+ | ๐Ÿ’พ **Disk cache** | Every fetched page is stored locally โ€” second runs are instant and offline |
90
+ | ๐Ÿข **Rate limiting** | Hard 500 ms minimum delay between live HTTP requests (configurable) |
91
+ | ๐Ÿชช **Identifiable** | Custom `User-Agent` sent with every request |
92
+ | ๐Ÿค– **robots.txt aware** | Generic mode consults `robots.txt` before crawling and aborts if disallowed |
93
+ | โœ… **Strict validation** | Pydantic schemas reject malformed records into `errors.json` instead of crashing |
94
+ | ๐Ÿงฑ **Resilient** | A broken page fails alone โ€” the run continues and reports it |
95
+ | ๐Ÿ“Š **Full reporting** | `run-report.json` with duration, cache hits, network fetches and record counts |
96
+ | ๐Ÿ–ฅ๏ธ **Clean terminal UI** | Progress bars, summary tables and panels via [`rich`](https://github.com/Textualize/rich) โ€” quiet by default, `-v` for full logs |
97
+
98
+ ## ๐Ÿ“ฆ Two Modes
99
+
100
+ ### `book` โ€” catalogue scraper
101
+ Built for e-commerce-style catalogues (originally [books.toscrape.com](https://books.toscrape.com/)). Follows pagination (`li.next a`), discovers product links, then extracts per-product: **title, price (raw + normalized), availability (raw + structured), rating, description, source page, timestamp**.
102
+
103
+ ### `generic` โ€” any website
104
+ BFS-crawls same-domain links (rate-limited, robots.txt-checked) and extracts per-page metadata: **title, h1, meta description, headings, same-domain links, image count, word count**.
105
+
106
+ ### `auto` โ€” the default
107
+ Fetches the seed page and checks for book-catalogue markup (`article.product_pod`). Book catalogue โ†’ book mode. Anything else โ†’ generic mode.
108
+
109
+ ## ๐Ÿš€ Quickstart
110
+
111
+ **Option A โ€” install as a CLI tool** (recommended after `v1.0.0`):
112
+
113
+ ```bash
114
+ # with pipx (isolated CLI install)
115
+ pipx install polite-scraper
116
+
117
+ # or from a clone
118
+ pip install .
119
+ polite-scraper https://books.toscrape.com
120
+ ```
121
+
122
+ **Option B โ€” from source:**
123
+
124
+ ```bash
125
+ git clone https://github.com/callmekriztal/polite_scapper.git
126
+ cd polite_scapper
127
+
128
+ python3 -m venv venv
129
+ source venv/bin/activate
130
+ pip install -r requirements.txt
131
+
132
+ # Book catalogue (auto-detected)
133
+ python -m polite_scraper https://books.toscrape.com
134
+
135
+ # Any other website, deeper crawl
136
+ python -m polite_scraper https://quotes.toscrape.com --mode generic --pages 5
137
+
138
+ # Bare domains get https:// added automatically
139
+ python -m polite_scraper example.com
140
+ ```
141
+
142
+ With the installed CLI, the same commands are simply:
143
+
144
+ ```bash
145
+ polite-scraper https://books.toscrape.com
146
+ polite-scraper https://quotes.toscrape.com --mode generic --pages 5
147
+ ```
148
+
149
+ Every run gets **its own output folder**, named after the website:
150
+
151
+ ```
152
+ data/runs/
153
+ โ”œโ”€โ”€ books-toscrape-com/ โ† books.toscrape.com run
154
+ โ”œโ”€โ”€ quotes-toscrape-com/ โ† quotes.toscrape.com run
155
+ โ””โ”€โ”€ my-domain-org-2/ โ† third run on my-domain.org (auto-suffixed)
156
+ ```
157
+
158
+ If a folder for that website already exists, you'll be asked to pick a new name โ€” or just press Enter to accept an auto-suffix (`-1`, `-2`, โ€ฆ) so no run ever overwrites another. Override the name with `--name my-custom-run`.
159
+
160
+ ## โŒจ๏ธ CLI Reference
161
+
162
+ ```
163
+ usage: polite-scraper [-h] [--mode {auto,book,generic}] [--pages PAGES]
164
+ [--max-links MAX_LINKS] [--delay DELAY]
165
+ [--timeout TIMEOUT] [--force] [-v] [url]
166
+ ```
167
+
168
+ | Flag | Description | Default |
169
+ |---|---|---|
170
+ | `url` | Website to scrape (required) | โ€” |
171
+ | `--mode auto\|book\|generic` | Scraper mode | `auto` |
172
+ | `--pages N` | Maximum pages to crawl | `3` |
173
+ | `--max-links N` | Generic mode: new same-domain links followed per page | `8` |
174
+ | `--delay SEC` | Minimum delay between live requests | `0.5` |
175
+ | `--timeout SEC` | Request timeout | `10` |
176
+ | `--force` | Ignore the local cache, re-fetch everything | off |
177
+ | `--name NAME` | Custom run folder name (default: derived from the website) | auto |
178
+ | `-v, --verbose` | Show per-request logs and raw record checkpoints | off |
179
+
180
+ Environment variables still work for `USER_AGENT`, `TIMEOUT`, `MIN_DELAY_SECONDS` and `BROKEN_TEST_URL` โ€” CLI flags win when both are set.
181
+
182
+ ## ๐Ÿ“ Output
183
+
184
+ Every run writes into its own folder under `data/runs/`:
185
+
186
+ | File | Mode | Contents |
187
+ |---|---|---|
188
+ | `books.json` | book | Validated product records |
189
+ | `pages.json` | generic | Validated page-metadata records |
190
+ | `errors.json` | both | Validation / fetch failures with reasons |
191
+ | `run-report.json` | both | Full execution metrics |
192
+
193
+ <details>
194
+ <summary>Example record โ€” <code>books.json</code></summary>
195
+
196
+ ```json
197
+ {
198
+ "title": "A Light in the Attic",
199
+ "product_url": "https://books.toscrape.com/catalogue/a-light-in-the-attic_1000/index.html",
200
+ "price_text": "ยฃ51.77",
201
+ "price_gbp": 51.77,
202
+ "availability_text": "In stock (22 available)",
203
+ "rating_text": "Three",
204
+ "description": "It's hard to imagine a world without A Light in the Attic. This now-classic collection of poetry and drawings from Shel Silverstein celebrates its 20th anniversary...",
205
+ "source_page": "https://books.toscrape.com/catalogue/page-1.html",
206
+ "fetched_at": "2026-11-15T03:16:34.270989+00:00"
207
+ }
208
+ ```
209
+ </details>
210
+
211
+ <details>
212
+ <summary>Example record โ€” <code>pages.json</code> (generic mode)</summary>
213
+
214
+ ```json
215
+ {
216
+ "url": "https://quotes.toscrape.com/",
217
+ "title": "Quotes to Scrape",
218
+ "h1": "Quotes to Scrape",
219
+ "meta_description": null,
220
+ "headings": ["Quotes to Scrape", "Top Ten tags"],
221
+ "same_domain_links": ["https://quotes.toscrape.com/login", "..."],
222
+ "link_count": 47,
223
+ "image_count": 0,
224
+ "word_count": 282,
225
+ "fetched_at": "2026-11-15T08:44:55.123456+00:00"
226
+ }
227
+ ```
228
+ </details>
229
+
230
+ ## ๐Ÿ—๏ธ Architecture
231
+
232
+ ```
233
+ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
234
+ CLI argument โ”€โ”€โ–บ โ”‚ cli.py โ”‚ argparse โ†’ Config
235
+ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”˜
236
+ โ–ผ
237
+ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
238
+ โ”‚ fetcher.py โ”‚ session ยท UA ยท rate limit ยท disk cache ยท robots.txt
239
+ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”˜
240
+ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
241
+ โ–ผ โ–ผ
242
+ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
243
+ โ”‚ crawler.py โ”‚ โ”‚ generic.py โ”‚
244
+ โ”‚ (book mode) โ”‚ โ”‚ (any website)โ”‚
245
+ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
246
+ โ–ผ โ–ผ
247
+ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
248
+ โ”‚ extractor.py โ”‚ โ”‚ BFS links โ”‚
249
+ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
250
+ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
251
+ โ–ผ
252
+ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
253
+ โ”‚ validator.py โ”‚ Pydantic: BookRecord / PageRecord
254
+ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
255
+ โ–ผ
256
+ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
257
+ โ”‚ reporter.py โ”‚ books.json ยท pages.json ยท errors.json ยท run-report.json
258
+ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
259
+ ```
260
+
261
+ Each stage is isolated โ€” swapping the extractor or the target site never touches the fetch, validation, or reporting layers.
262
+
263
+ ## ๐Ÿงช Testing
264
+
265
+ ```bash
266
+ pytest tests/ -v
267
+ ```
268
+
269
+ Covers price/rating/availability normalization, Pydantic validation failures, broken-page simulation, generic extraction, and CLI config mapping.
270
+
271
+ ## ๐Ÿค Politeness & Ethics
272
+
273
+ - **Identifying `User-Agent`** on every request (configurable via `USER_AGENT`)
274
+ - **Minimum 500 ms delay** between live network requests โ€” cache reads never sleep
275
+ - **`robots.txt` consulted** in generic mode; the run aborts if our agent is disallowed
276
+ - **Only `200 OK` accepted** โ€” no retries that hammer a struggling server
277
+ - **Caching by default** so development re-runs cost the target server zero requests
278
+
279
+ > *I will not reuse this code on another site without checking its rules and terms first.*
280
+
281
+ ## ๐Ÿ“š More Documentation
282
+
283
+ The deep-dive โ€” stage-by-stage design decisions, data schema tables, failure-handling hooks, and the internship assignment context โ€” lives in **[`docs/scraper.md`](docs/scraper.md)****.
@@ -0,0 +1,251 @@
1
+ <div align="center">
2
+
3
+ # ๐Ÿ•ท๏ธ The Polite Scraper
4
+
5
+ **A rate-limited, cache-first web scraper with a polished terminal UI โ€” that treats every host with respect.**
6
+
7
+ [![Python](https://img.shields.io/badge/python-3.10%2B-3776AB?logo=python&logoColor=white)](https://www.python.org/)
8
+ [![Tests](https://img.shields.io/badge/tests-10%20passing-brightgreen)](tests/)
9
+ [![Modes](https://img.shields.io/badge/modes-book%20%7C%20generic-blueviolet)](#two-modes)
10
+ [![UI](https://img.shields.io/badge/ui-rich%20terminal-FF69B4)](https://github.com/Textualize/rich)
11
+ [![Ethics](https://img.shields.io/badge/robots.txt-checked-success)](#%EF%B8%8F-politeness--ethics)
12
+
13
+ </div>
14
+
15
+ ---
16
+
17
+ The Polite Scraper crawls websites the way a good citizen would: it identifies itself with a custom User-Agent, waits at least 500 ms between live requests, caches every page it downloads so re-runs cost the server **nothing**, validates everything it extracts against strict [Pydantic](https://docs.pydantic.dev/) schemas, and survives broken pages without crashing.
18
+
19
+ Give it a URL โ€” it figures out the rest.
20
+
21
+ ```bash
22
+ python -m polite_scraper https://books.toscrape.com
23
+ ```
24
+
25
+ ```
26
+ โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ The Polite Scraper โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ
27
+ โ”‚ Target https://books.toscrape.com/ โ”‚
28
+ โ”‚ Mode book โ”‚
29
+ โ”‚ Run folder data/runs/books-toscrape-com โ”‚
30
+ โ”‚ Max pages 3 โ”‚
31
+ โ”‚ Min delay 0.5s between requests โ”‚
32
+ โ”‚ Cache data/cache โ”‚
33
+ โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ polite by design โ€” rate-limited, cached, identify-yourself โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ
34
+
35
+ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ Discovering catalogue pages โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
36
+ โœ“ 3 catalogue pages crawled โ€” 60 unique book URLs
37
+
38
+ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ Extracting & validating book details โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
39
+ โœ“ 60 valid books, 0 rejected
40
+
41
+ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ Done โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
42
+ โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ Run Summary โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ
43
+ โ”‚ Requests 0 network / 64 cache โ”‚
44
+ โ”‚ Records saved 60 โ”‚
45
+ โ”‚ Duration 0.772s โ”‚
46
+ โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ
47
+ ```
48
+
49
+ ---
50
+
51
+ ## โœจ Features
52
+
53
+ | | |
54
+ |---|---|
55
+ | ๐ŸŽฏ **One command, any site** | Pass the website as a plain argument โ€” no config files, no code changes |
56
+ | ๐Ÿง  **Auto mode detection** | Probes the target and picks the right extractor automatically |
57
+ | ๐Ÿ’พ **Disk cache** | Every fetched page is stored locally โ€” second runs are instant and offline |
58
+ | ๐Ÿข **Rate limiting** | Hard 500 ms minimum delay between live HTTP requests (configurable) |
59
+ | ๐Ÿชช **Identifiable** | Custom `User-Agent` sent with every request |
60
+ | ๐Ÿค– **robots.txt aware** | Generic mode consults `robots.txt` before crawling and aborts if disallowed |
61
+ | โœ… **Strict validation** | Pydantic schemas reject malformed records into `errors.json` instead of crashing |
62
+ | ๐Ÿงฑ **Resilient** | A broken page fails alone โ€” the run continues and reports it |
63
+ | ๐Ÿ“Š **Full reporting** | `run-report.json` with duration, cache hits, network fetches and record counts |
64
+ | ๐Ÿ–ฅ๏ธ **Clean terminal UI** | Progress bars, summary tables and panels via [`rich`](https://github.com/Textualize/rich) โ€” quiet by default, `-v` for full logs |
65
+
66
+ ## ๐Ÿ“ฆ Two Modes
67
+
68
+ ### `book` โ€” catalogue scraper
69
+ Built for e-commerce-style catalogues (originally [books.toscrape.com](https://books.toscrape.com/)). Follows pagination (`li.next a`), discovers product links, then extracts per-product: **title, price (raw + normalized), availability (raw + structured), rating, description, source page, timestamp**.
70
+
71
+ ### `generic` โ€” any website
72
+ BFS-crawls same-domain links (rate-limited, robots.txt-checked) and extracts per-page metadata: **title, h1, meta description, headings, same-domain links, image count, word count**.
73
+
74
+ ### `auto` โ€” the default
75
+ Fetches the seed page and checks for book-catalogue markup (`article.product_pod`). Book catalogue โ†’ book mode. Anything else โ†’ generic mode.
76
+
77
+ ## ๐Ÿš€ Quickstart
78
+
79
+ **Option A โ€” install as a CLI tool** (recommended after `v1.0.0`):
80
+
81
+ ```bash
82
+ # with pipx (isolated CLI install)
83
+ pipx install polite-scraper
84
+
85
+ # or from a clone
86
+ pip install .
87
+ polite-scraper https://books.toscrape.com
88
+ ```
89
+
90
+ **Option B โ€” from source:**
91
+
92
+ ```bash
93
+ git clone https://github.com/callmekriztal/polite_scapper.git
94
+ cd polite_scapper
95
+
96
+ python3 -m venv venv
97
+ source venv/bin/activate
98
+ pip install -r requirements.txt
99
+
100
+ # Book catalogue (auto-detected)
101
+ python -m polite_scraper https://books.toscrape.com
102
+
103
+ # Any other website, deeper crawl
104
+ python -m polite_scraper https://quotes.toscrape.com --mode generic --pages 5
105
+
106
+ # Bare domains get https:// added automatically
107
+ python -m polite_scraper example.com
108
+ ```
109
+
110
+ With the installed CLI, the same commands are simply:
111
+
112
+ ```bash
113
+ polite-scraper https://books.toscrape.com
114
+ polite-scraper https://quotes.toscrape.com --mode generic --pages 5
115
+ ```
116
+
117
+ Every run gets **its own output folder**, named after the website:
118
+
119
+ ```
120
+ data/runs/
121
+ โ”œโ”€โ”€ books-toscrape-com/ โ† books.toscrape.com run
122
+ โ”œโ”€โ”€ quotes-toscrape-com/ โ† quotes.toscrape.com run
123
+ โ””โ”€โ”€ my-domain-org-2/ โ† third run on my-domain.org (auto-suffixed)
124
+ ```
125
+
126
+ If a folder for that website already exists, you'll be asked to pick a new name โ€” or just press Enter to accept an auto-suffix (`-1`, `-2`, โ€ฆ) so no run ever overwrites another. Override the name with `--name my-custom-run`.
127
+
128
+ ## โŒจ๏ธ CLI Reference
129
+
130
+ ```
131
+ usage: polite-scraper [-h] [--mode {auto,book,generic}] [--pages PAGES]
132
+ [--max-links MAX_LINKS] [--delay DELAY]
133
+ [--timeout TIMEOUT] [--force] [-v] [url]
134
+ ```
135
+
136
+ | Flag | Description | Default |
137
+ |---|---|---|
138
+ | `url` | Website to scrape (required) | โ€” |
139
+ | `--mode auto\|book\|generic` | Scraper mode | `auto` |
140
+ | `--pages N` | Maximum pages to crawl | `3` |
141
+ | `--max-links N` | Generic mode: new same-domain links followed per page | `8` |
142
+ | `--delay SEC` | Minimum delay between live requests | `0.5` |
143
+ | `--timeout SEC` | Request timeout | `10` |
144
+ | `--force` | Ignore the local cache, re-fetch everything | off |
145
+ | `--name NAME` | Custom run folder name (default: derived from the website) | auto |
146
+ | `-v, --verbose` | Show per-request logs and raw record checkpoints | off |
147
+
148
+ Environment variables still work for `USER_AGENT`, `TIMEOUT`, `MIN_DELAY_SECONDS` and `BROKEN_TEST_URL` โ€” CLI flags win when both are set.
149
+
150
+ ## ๐Ÿ“ Output
151
+
152
+ Every run writes into its own folder under `data/runs/`:
153
+
154
+ | File | Mode | Contents |
155
+ |---|---|---|
156
+ | `books.json` | book | Validated product records |
157
+ | `pages.json` | generic | Validated page-metadata records |
158
+ | `errors.json` | both | Validation / fetch failures with reasons |
159
+ | `run-report.json` | both | Full execution metrics |
160
+
161
+ <details>
162
+ <summary>Example record โ€” <code>books.json</code></summary>
163
+
164
+ ```json
165
+ {
166
+ "title": "A Light in the Attic",
167
+ "product_url": "https://books.toscrape.com/catalogue/a-light-in-the-attic_1000/index.html",
168
+ "price_text": "ยฃ51.77",
169
+ "price_gbp": 51.77,
170
+ "availability_text": "In stock (22 available)",
171
+ "rating_text": "Three",
172
+ "description": "It's hard to imagine a world without A Light in the Attic. This now-classic collection of poetry and drawings from Shel Silverstein celebrates its 20th anniversary...",
173
+ "source_page": "https://books.toscrape.com/catalogue/page-1.html",
174
+ "fetched_at": "2026-11-15T03:16:34.270989+00:00"
175
+ }
176
+ ```
177
+ </details>
178
+
179
+ <details>
180
+ <summary>Example record โ€” <code>pages.json</code> (generic mode)</summary>
181
+
182
+ ```json
183
+ {
184
+ "url": "https://quotes.toscrape.com/",
185
+ "title": "Quotes to Scrape",
186
+ "h1": "Quotes to Scrape",
187
+ "meta_description": null,
188
+ "headings": ["Quotes to Scrape", "Top Ten tags"],
189
+ "same_domain_links": ["https://quotes.toscrape.com/login", "..."],
190
+ "link_count": 47,
191
+ "image_count": 0,
192
+ "word_count": 282,
193
+ "fetched_at": "2026-11-15T08:44:55.123456+00:00"
194
+ }
195
+ ```
196
+ </details>
197
+
198
+ ## ๐Ÿ—๏ธ Architecture
199
+
200
+ ```
201
+ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
202
+ CLI argument โ”€โ”€โ–บ โ”‚ cli.py โ”‚ argparse โ†’ Config
203
+ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”˜
204
+ โ–ผ
205
+ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
206
+ โ”‚ fetcher.py โ”‚ session ยท UA ยท rate limit ยท disk cache ยท robots.txt
207
+ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”˜
208
+ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
209
+ โ–ผ โ–ผ
210
+ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
211
+ โ”‚ crawler.py โ”‚ โ”‚ generic.py โ”‚
212
+ โ”‚ (book mode) โ”‚ โ”‚ (any website)โ”‚
213
+ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
214
+ โ–ผ โ–ผ
215
+ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
216
+ โ”‚ extractor.py โ”‚ โ”‚ BFS links โ”‚
217
+ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
218
+ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
219
+ โ–ผ
220
+ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
221
+ โ”‚ validator.py โ”‚ Pydantic: BookRecord / PageRecord
222
+ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
223
+ โ–ผ
224
+ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
225
+ โ”‚ reporter.py โ”‚ books.json ยท pages.json ยท errors.json ยท run-report.json
226
+ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
227
+ ```
228
+
229
+ Each stage is isolated โ€” swapping the extractor or the target site never touches the fetch, validation, or reporting layers.
230
+
231
+ ## ๐Ÿงช Testing
232
+
233
+ ```bash
234
+ pytest tests/ -v
235
+ ```
236
+
237
+ Covers price/rating/availability normalization, Pydantic validation failures, broken-page simulation, generic extraction, and CLI config mapping.
238
+
239
+ ## ๐Ÿค Politeness & Ethics
240
+
241
+ - **Identifying `User-Agent`** on every request (configurable via `USER_AGENT`)
242
+ - **Minimum 500 ms delay** between live network requests โ€” cache reads never sleep
243
+ - **`robots.txt` consulted** in generic mode; the run aborts if our agent is disallowed
244
+ - **Only `200 OK` accepted** โ€” no retries that hammer a struggling server
245
+ - **Caching by default** so development re-runs cost the target server zero requests
246
+
247
+ > *I will not reuse this code on another site without checking its rules and terms first.*
248
+
249
+ ## ๐Ÿ“š More Documentation
250
+
251
+ The deep-dive โ€” stage-by-stage design decisions, data schema tables, failure-handling hooks, and the internship assignment context โ€” lives in **[`docs/scraper.md`](docs/scraper.md)****.
@@ -0,0 +1,3 @@
1
+ """The Polite Scraper โ€” a rate-limited, cache-first web scraper."""
2
+
3
+ __version__ = "1.0.0"
@@ -0,0 +1,4 @@
1
+ from polite_scraper.main import main
2
+
3
+ if __name__ == "__main__":
4
+ raise SystemExit(main())