watchdiff-core 0.1.2__tar.gz → 0.1.3__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 (46) hide show
  1. watchdiff_core-0.1.3/PKG-INFO +601 -0
  2. watchdiff_core-0.1.3/README.md +562 -0
  3. {watchdiff_core-0.1.2 → watchdiff_core-0.1.3}/main.py +6 -7
  4. {watchdiff_core-0.1.2 → watchdiff_core-0.1.3}/pyproject.toml +21 -14
  5. watchdiff_core-0.1.3/uv.lock +898 -0
  6. watchdiff_core-0.1.3/watchdiff/__init__.py +14 -0
  7. watchdiff_core-0.1.3/watchdiff/cli/main.py +362 -0
  8. {watchdiff_core-0.1.2 → watchdiff_core-0.1.3}/watchdiff/core.py +146 -31
  9. watchdiff_core-0.1.3/watchdiff/diff/engine.py +149 -0
  10. watchdiff_core-0.1.3/watchdiff/exporter/__init__.py +3 -0
  11. watchdiff_core-0.1.3/watchdiff/exporter/exporter.py +222 -0
  12. watchdiff_core-0.1.3/watchdiff/fetcher/__init__.py +11 -0
  13. watchdiff_core-0.1.3/watchdiff/fetcher/browser.py +104 -0
  14. watchdiff_core-0.1.3/watchdiff/fetcher/fetcher.py +122 -0
  15. {watchdiff_core-0.1.2 → watchdiff_core-0.1.3}/watchdiff/models.py +55 -35
  16. watchdiff_core-0.1.3/watchdiff/parser/parser.py +126 -0
  17. {watchdiff_core-0.1.2 → watchdiff_core-0.1.3}/watchdiff/scheduler/scheduler.py +94 -50
  18. watchdiff_core-0.1.3/watchdiff/store/__init__.py +4 -0
  19. watchdiff_core-0.1.3/watchdiff/store/sqlite_store.py +173 -0
  20. {watchdiff_core-0.1.2 → watchdiff_core-0.1.3}/watchdiff/store/store.py +20 -17
  21. watchdiff_core-0.1.2/PKG-INFO +0 -291
  22. watchdiff_core-0.1.2/README.md +0 -263
  23. watchdiff_core-0.1.2/uv.lock +0 -369
  24. watchdiff_core-0.1.2/watchdiff/__init__.py +0 -3
  25. watchdiff_core-0.1.2/watchdiff/cli/main.py +0 -217
  26. watchdiff_core-0.1.2/watchdiff/diff/engine.py +0 -131
  27. watchdiff_core-0.1.2/watchdiff/fetcher/__init__.py +0 -3
  28. watchdiff_core-0.1.2/watchdiff/fetcher/fetcher.py +0 -84
  29. watchdiff_core-0.1.2/watchdiff/parser/parser.py +0 -75
  30. watchdiff_core-0.1.2/watchdiff/store/__init__.py +0 -3
  31. {watchdiff_core-0.1.2 → watchdiff_core-0.1.3}/.github/workflows/ci.yml +0 -0
  32. {watchdiff_core-0.1.2 → watchdiff_core-0.1.3}/.github/workflows/release-testpypi.yml +0 -0
  33. {watchdiff_core-0.1.2 → watchdiff_core-0.1.3}/.github/workflows/release.yml +0 -0
  34. {watchdiff_core-0.1.2 → watchdiff_core-0.1.3}/.gitignore +0 -0
  35. {watchdiff_core-0.1.2 → watchdiff_core-0.1.3}/.python-version +0 -0
  36. {watchdiff_core-0.1.2 → watchdiff_core-0.1.3}/LICENSE +0 -0
  37. {watchdiff_core-0.1.2 → watchdiff_core-0.1.3}/tests/__init__.py +0 -0
  38. {watchdiff_core-0.1.2 → watchdiff_core-0.1.3}/tests/test_watchdiff.py +0 -0
  39. {watchdiff_core-0.1.2 → watchdiff_core-0.1.3}/watchdiff/cleaner/__init__.py +0 -0
  40. {watchdiff_core-0.1.2 → watchdiff_core-0.1.3}/watchdiff/cleaner/cleaner.py +0 -0
  41. {watchdiff_core-0.1.2 → watchdiff_core-0.1.3}/watchdiff/cli/__init__.py +0 -0
  42. {watchdiff_core-0.1.2 → watchdiff_core-0.1.3}/watchdiff/diff/__init__.py +0 -0
  43. {watchdiff_core-0.1.2 → watchdiff_core-0.1.3}/watchdiff/notifier/__init__.py +0 -0
  44. {watchdiff_core-0.1.2 → watchdiff_core-0.1.3}/watchdiff/notifier/notifier.py +0 -0
  45. {watchdiff_core-0.1.2 → watchdiff_core-0.1.3}/watchdiff/parser/__init__.py +0 -0
  46. {watchdiff_core-0.1.2 → watchdiff_core-0.1.3}/watchdiff/scheduler/__init__.py +0 -0
@@ -0,0 +1,601 @@
1
+ Metadata-Version: 2.4
2
+ Name: watchdiff-core
3
+ Version: 0.1.3
4
+ Summary: Lightweight web change monitoring library - clean diffs, structured alerts, no AI required.
5
+ Author: WatchDiff Contributors
6
+ License: GPL-3.0-or-later
7
+ License-File: LICENSE
8
+ Keywords: automation,change-detection,diff,monitoring,scraping,web
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.9
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Topic :: Internet :: WWW/HTTP
19
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
20
+ Requires-Python: >=3.9
21
+ Requires-Dist: beautifulsoup4>=4.12.0
22
+ Requires-Dist: httpx>=0.24.0
23
+ Requires-Dist: lxml>=4.9.0
24
+ Requires-Dist: rich>=13.0.0
25
+ Requires-Dist: typer>=0.9.0
26
+ Provides-Extra: all
27
+ Requires-Dist: openpyxl>=3.1.0; extra == 'all'
28
+ Requires-Dist: playwright>=1.40.0; extra == 'all'
29
+ Provides-Extra: browser
30
+ Requires-Dist: playwright>=1.40.0; extra == 'browser'
31
+ Provides-Extra: dev
32
+ Requires-Dist: pytest-asyncio>=0.21.0; extra == 'dev'
33
+ Requires-Dist: pytest-httpx>=0.22.0; extra == 'dev'
34
+ Requires-Dist: pytest>=7.0.0; extra == 'dev'
35
+ Requires-Dist: ruff>=0.1.0; extra == 'dev'
36
+ Provides-Extra: xlsx
37
+ Requires-Dist: openpyxl>=3.1.0; extra == 'xlsx'
38
+ Description-Content-Type: text/markdown
39
+
40
+ # WatchDiff
41
+
42
+ [![PyPI version](https://img.shields.io/pypi/v/watchdiff-core?color=blue)](https://pypi.org/project/watchdiff-core/)
43
+ [![Python versions](https://img.shields.io/pypi/pyversions/watchdiff-core)](https://pypi.org/project/watchdiff-core/)
44
+ [![CI](https://github.com/r-seize/watchdiff-py/actions/workflows/ci.yml/badge.svg)](https://github.com/r-seize/watchdiff-py/actions/workflows/ci.yml)
45
+ [![License: GPL v3](https://img.shields.io/badge/license-GPL--3.0-blue)](https://www.gnu.org/licenses/gpl-3.0)
46
+
47
+ **Lightweight web change monitoring - clean diffs, structured alerts, no AI required.**
48
+
49
+ WatchDiff watches web pages and tells you **exactly what changed**, in plain language.
50
+ No noisy HTML diffs. No external services. No AI black boxes.
51
+
52
+ ## At a glance
53
+
54
+ | What you want | How |
55
+ |---|---|
56
+ | Monitor a URL for changes | `.watch(url, target=".price", interval=300)` + `.start()` |
57
+ | Target a specific element | `target=".price"` (CSS) or `target="//span[@class='p']"` (XPath) |
58
+ | Get notified on change | `on_change=lambda r: print(r.summary())` or `webhooks=["https://discord.com/..."]` |
59
+ | Render JS-heavy pages | `browser=True` (requires `pip install "watchdiff-core[browser]"`) |
60
+ | Avoid notification spam | `cooldown=3600` (min seconds between alerts per URL) |
61
+ | Rotate proxies / UAs | `proxies=[...]`, `user_agents=[...]` |
62
+ | Diff at paragraph level | `diff_mode="semantic"` |
63
+ | Persist to SQLite | `WatchDiff(store=SqliteStore(".watchdiff.db"))` |
64
+ | Export history | `.export_reports_csv(url)` / `.export_reports_xlsx(url)` |
65
+ | CLI one-liner | `watchdiff run https://example.com --target .price --interval 60` |
66
+ | Multi-URL config file | `watchdiff init` then edit `watchdiff.config.json` |
67
+
68
+ ### Quick navigation
69
+
70
+ - [Install](#install)
71
+ - [Quick start](#quick-start)
72
+ - [Feature details](#features)
73
+ - [JS-heavy pages (Playwright)](#javascript-pages-with-playwright)
74
+ - [Proxy and User-Agent rotation](#proxy-rotation-and-user-agent-rotation)
75
+ - [Semantic diff mode](#semantic-diff-mode)
76
+ - [XPath selectors](#xpath-selectors)
77
+ - [SQLite storage backend](#sqlite-storage-backend)
78
+ - [CSV and XLSX export](#csv-and-xlsx-export)
79
+ - [Alert cooldown](#cooldown-anti-spam)
80
+ - [Config file](#config-file-workflow-watchdiff-init)
81
+ - [API reference](#api-reference)
82
+ - [Webhooks](#webhooks)
83
+ - [CLI reference](#cli-reference)
84
+ - [Use cases](#use-cases)
85
+
86
+ ## Why WatchDiff?
87
+
88
+ Most change detection tools compare raw HTML — which means every minor script reload or ad rotation triggers a false positive. WatchDiff strips the noise first, then diffs only the content that matters.
89
+
90
+ - **Deterministic** — same input always produces the same output
91
+ - **Human-readable diffs** — "Price changed: $19 → $24", not a wall of HTML
92
+ - **Zero external services** — snapshots stored locally (JSON or SQLite)
93
+ - **Async-ready** — sync and async schedulers included
94
+ - **Python 3.9+** — works on Debian Bullseye, Bookworm, and Trixie
95
+
96
+
97
+ ## Install
98
+
99
+ ```bash
100
+ pip install watchdiff-core
101
+ ```
102
+
103
+ Or with [uv](https://github.com/astral-sh/uv):
104
+
105
+ ```bash
106
+ uv add watchdiff-core
107
+ ```
108
+
109
+ ### Optional extras
110
+
111
+ ```bash
112
+ # JavaScript / SPA pages (Playwright headless browser)
113
+ pip install "watchdiff-core[browser]"
114
+ playwright install chromium
115
+
116
+ # XLSX export
117
+ pip install "watchdiff-core[xlsx]"
118
+
119
+ # Everything at once
120
+ pip install "watchdiff-core[all]"
121
+ ```
122
+
123
+ ## Quick start
124
+
125
+ ### Python API
126
+
127
+ ```python
128
+ from watchdiff import WatchDiff
129
+
130
+ wd = WatchDiff()
131
+
132
+ wd.watch(
133
+ "https://books.toscrape.com/catalogue/a-light-in-the-attic_1000/index.html",
134
+ target=".price_color",
135
+ interval=60,
136
+ label="Book price",
137
+ on_change=lambda r: print(r.summary()),
138
+ )
139
+
140
+ wd.start()
141
+ ```
142
+
143
+ ### CLI
144
+
145
+ ```bash
146
+ # Generate a config file
147
+ watchdiff init
148
+
149
+ # Run from config file
150
+ watchdiff run --config watchdiff.config.json
151
+
152
+ # One-shot check
153
+ watchdiff check https://example.com --target .price
154
+
155
+ # Continuous monitoring (Ctrl+C to stop)
156
+ watchdiff run https://example.com --target .price --interval 60
157
+
158
+ # Snapshot history and reports
159
+ watchdiff history https://example.com
160
+ watchdiff reports https://example.com
161
+
162
+ # Clear stored data
163
+ watchdiff clear https://example.com
164
+ ```
165
+
166
+
167
+ ## Features
168
+
169
+ ### JavaScript pages with Playwright
170
+
171
+ For pages that render content via JavaScript (SPAs, React, Vue, etc.), use the headless browser mode:
172
+
173
+ ```bash
174
+ pip install "watchdiff-core[browser]"
175
+ playwright install chromium
176
+ ```
177
+
178
+ ```python
179
+ from watchdiff import WatchDiff
180
+ from watchdiff.models import BrowserOptions
181
+
182
+ wd = WatchDiff()
183
+ wd.watch(
184
+ "https://spa.example.com/pricing",
185
+ target=".price",
186
+ browser=True,
187
+ browser_options=BrowserOptions(
188
+ wait_for="networkidle", # wait until network is quiet
189
+ wait_for_selector=".price", # also wait for this element to appear
190
+ timeout=30000, # ms - max wait time
191
+ ),
192
+ )
193
+ wd.start()
194
+ ```
195
+
196
+ `wait_for` accepts:
197
+ - `"load"` — default, waits for the `load` event
198
+ - `"domcontentloaded"` — faster, waits for DOM only
199
+ - `"networkidle"` — waits until no network requests for 500ms
200
+
201
+
202
+ ### Proxy rotation and User-Agent rotation
203
+
204
+ Avoid blocks with automatic rotation on every request:
205
+
206
+ ```python
207
+ wd.watch(
208
+ "https://example.com",
209
+ proxies=[
210
+ "http://proxy1.example.com:8080",
211
+ "http://proxy2.example.com:8080",
212
+ "socks5://proxy3.example.com:1080",
213
+ ],
214
+ user_agents=[
215
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ...",
216
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 ...",
217
+ ],
218
+ )
219
+ ```
220
+
221
+ If `user_agents` is empty, WatchDiff rotates automatically among **4 built-in modern UA strings** (Chrome, Safari, Firefox, Chrome Linux). No configuration required.
222
+
223
+ Proxies also work in browser mode — Playwright passes the selected proxy to Chromium.
224
+
225
+
226
+ ### Semantic diff mode
227
+
228
+ By default, WatchDiff diffs line by line. In **semantic mode**, it extracts meaningful HTML blocks — `<p>`, `<h1>`-`<h6>`, `<li>`, `<td>`, `<th>`, `<blockquote>` — and diffs those instead. This gives cleaner results on content-heavy pages where a single paragraph change doesn't shift dozens of lines.
229
+
230
+ ```python
231
+ wd.watch(
232
+ "https://blog.example.com/article",
233
+ diff_mode="semantic", # "line" (default) or "semantic"
234
+ )
235
+ ```
236
+
237
+ If no semantic blocks are found, the engine falls back to line mode automatically.
238
+
239
+ In the CLI:
240
+
241
+ ```bash
242
+ watchdiff check https://blog.example.com/article --diff-mode semantic
243
+ watchdiff run https://blog.example.com/article --diff-mode semantic --interval 3600
244
+ ```
245
+
246
+
247
+ ### XPath selectors
248
+
249
+ `target` accepts both **CSS selectors** and **XPath expressions**. XPath is detected automatically by a leading `/` or `(`:
250
+
251
+ ```python
252
+ # CSS selector (default)
253
+ wd.watch("https://example.com", target=".price")
254
+ wd.watch("https://example.com", target="#main > h1")
255
+
256
+ # XPath expressions
257
+ wd.watch("https://example.com", target="//div[@class='price']")
258
+ wd.watch("https://example.com", target="//table//tr[td[1]='Revenue']/td[2]")
259
+ wd.watch("https://example.com", target="(//h2)[1]") # first <h2> only
260
+ wd.watch("https://example.com", target="//p[contains(@class,'intro')]")
261
+ ```
262
+
263
+ XPath is implemented via `lxml` (already a dependency — no extra install needed).
264
+
265
+
266
+ ### SQLite storage backend
267
+
268
+ By default, WatchDiff stores data as JSON files. For larger datasets or concurrent access, switch to the built-in SQLite backend — no extra dependencies required:
269
+
270
+ ```python
271
+ from watchdiff import WatchDiff
272
+ from watchdiff.store import SqliteStore
273
+
274
+ wd = WatchDiff(store=SqliteStore(".watchdiff.db"))
275
+ wd.watch("https://example.com").start()
276
+ ```
277
+
278
+ `SqliteStore` is a **drop-in replacement** for the default `Store` — same interface, same behaviour. It runs in WAL mode for concurrent-read safety.
279
+
280
+
281
+ ### CSV and XLSX export
282
+
283
+ Export your snapshot history and diff reports to CSV (no dependencies) or XLSX (requires `openpyxl`):
284
+
285
+ ```python
286
+ from watchdiff import WatchDiff
287
+
288
+ wd = WatchDiff()
289
+ wd.watch("https://example.com", target=".price")
290
+
291
+ # CSV - always available, returns the CSV string
292
+ csv_text = wd.export_reports_csv("https://example.com", dest="reports.csv")
293
+ csv_text = wd.export_snapshots_csv("https://example.com", dest="snapshots.csv")
294
+
295
+ # XLSX - requires: pip install "watchdiff-core[xlsx]"
296
+ path = wd.export_reports_xlsx("https://example.com", dest="reports.xlsx")
297
+ path = wd.export_snapshots_xlsx("https://example.com", dest="snapshots.xlsx")
298
+ ```
299
+
300
+ All export methods accept:
301
+ - `url` — the watched URL
302
+ - `target` — CSS/XPath filter (optional, `None` = full page)
303
+ - `limit` — max rows to include (default 500)
304
+ - `dest` — file path to write (optional for CSV, required for XLSX)
305
+
306
+
307
+ ### Cooldown anti-spam
308
+
309
+ Use `cooldown` to set a minimum delay in seconds between two alerts for the same URL. Useful when a page changes frequently but you don't want to be notified on every single check.
310
+
311
+ ```python
312
+ wd.watch(
313
+ "https://news.example.com/live",
314
+ target=".headline",
315
+ interval=30, # check every 30 seconds
316
+ cooldown=600, # but alert at most every 10 minutes
317
+ on_change=lambda r: print(r.summary()),
318
+ )
319
+ ```
320
+
321
+ Important: **changes are still detected and stored** during the cooldown period. Only the alerts (callbacks, webhooks) are suppressed. The full history remains available via `.history()` and `.reports()`.
322
+
323
+ `cooldown=0` (default) disables the feature — every change triggers an alert immediately.
324
+
325
+ In the CLI:
326
+
327
+ ```bash
328
+ watchdiff run https://news.example.com --interval 30 --cooldown 600
329
+ ```
330
+
331
+ In `watchdiff.config.json`:
332
+
333
+ ```json
334
+ {
335
+ "url": "https://news.example.com/live",
336
+ "interval": 30,
337
+ "cooldown": 600
338
+ }
339
+ ```
340
+
341
+
342
+ ### Config file workflow (`watchdiff init`)
343
+
344
+ Generate a ready-to-edit config file, then run all your watchers in one command:
345
+
346
+ ```bash
347
+ watchdiff init
348
+ # Created watchdiff.config.json
349
+ ```
350
+
351
+ Edit `watchdiff.config.json`:
352
+
353
+ ```json
354
+ {
355
+ "storage": ".watchdiff",
356
+ "watchers": [
357
+ {
358
+ "url": "https://store.example.com/product/42",
359
+ "target": ".price",
360
+ "interval": 300,
361
+ "label": "Product 42 price",
362
+ "diff_mode": "line",
363
+ "browser": false,
364
+ "cooldown": 0,
365
+ "webhooks": ["https://discord.com/api/webhooks/YOUR_ID/YOUR_TOKEN"],
366
+ "proxies": [],
367
+ "user_agents": [],
368
+ "ignore_selectors": [".cookie-banner", "#ad-container"],
369
+ "ignore_patterns": ["\\d+ views"],
370
+ "timeout": 15,
371
+ "headers": {}
372
+ },
373
+ {
374
+ "url": "https://blog.example.com/changelog",
375
+ "target": "//article//p",
376
+ "interval": 3600,
377
+ "label": "Changelog",
378
+ "diff_mode": "semantic",
379
+ "browser": false,
380
+ "webhooks": []
381
+ }
382
+ ]
383
+ }
384
+ ```
385
+
386
+ Run:
387
+
388
+ ```bash
389
+ # Explicit path
390
+ watchdiff run --config watchdiff.config.json
391
+
392
+ # Auto-discovery: if watchdiff.config.json exists in CWD, this also works
393
+ watchdiff run
394
+ ```
395
+
396
+
397
+ ## API reference
398
+
399
+ ### `WatchDiff`
400
+
401
+ ```python
402
+ from watchdiff import WatchDiff
403
+ from watchdiff.store import SqliteStore
404
+
405
+ wd = WatchDiff() # JSON store in .watchdiff/
406
+ wd = WatchDiff(storage_dir="/data/watchdiff") # custom JSON store path
407
+ wd = WatchDiff(store=SqliteStore("db.sqlite")) # SQLite store
408
+ ```
409
+
410
+ #### `.watch(url, *, ...)`
411
+
412
+ Register a URL to monitor. All keyword arguments are optional. Returns `self` (chainable).
413
+
414
+ | Parameter | Type | Default | Description |
415
+ |---|---|---|---|
416
+ | `url` | `str` | - | URL to watch |
417
+ | `target` | `str \| None` | `None` | CSS selector or XPath. `None` = full page |
418
+ | `interval` | `int` | `300` | Seconds between checks |
419
+ | `label` | `str \| None` | URL | Human-readable name shown in logs |
420
+ | `headers` | `dict` | `{}` | Extra HTTP headers |
421
+ | `timeout` | `int` | `15` | Request timeout in seconds |
422
+ | `ignore_selectors` | `list[str]` | `[]` | CSS selectors to strip before diffing |
423
+ | `ignore_patterns` | `list[str]` | `[]` | Regex patterns to strip from text |
424
+ | `on_change` | `Callable \| list` | `None` | Callback(s) fired on each change |
425
+ | `webhooks` | `list[str]` | `[]` | Webhook URLs to POST on change |
426
+ | `min_changes` | `int` | `1` | Minimum number of changes to trigger alert |
427
+ | `diff_mode` | `str` | `"line"` | `"line"` or `"semantic"` |
428
+ | `browser` | `bool` | `False` | Use Playwright headless browser |
429
+ | `browser_options` | `BrowserOptions \| None` | `None` | Fine-tune Playwright behaviour |
430
+ | `proxies` | `list[str]` | `[]` | Proxy URLs - one picked randomly per request |
431
+ | `user_agents` | `list[str]` | `[]` | UA strings - rotated per request (built-ins used if empty) |
432
+ | `cooldown` | `int` | `0` | Min seconds between two alerts for this URL (0 = disabled) |
433
+
434
+ ```python
435
+ # Chainable
436
+ wd.watch("https://site.com/product", target=".price", interval=300) \
437
+ .watch("https://site.com/stock", target=".availability") \
438
+ .on_change(lambda r: print(r.summary())) \
439
+ .start()
440
+ ```
441
+
442
+ #### `.on_change(callback)`
443
+
444
+ Register a global callback called whenever **any** watched URL changes.
445
+
446
+ ```python
447
+ def handle(report):
448
+ print(report.summary())
449
+ for change in report.changes:
450
+ print(change.human())
451
+
452
+ wd.on_change(handle)
453
+ ```
454
+
455
+ #### `.start(block=True)`
456
+
457
+ Start the synchronous scheduler. Blocks until `Ctrl+C` by default.
458
+ Pass `block=False` to run in the background (daemon threads).
459
+
460
+ #### `await .start_async()`
461
+
462
+ Async variant — use inside an existing event loop (FastAPI, aiohttp, etc.):
463
+
464
+ ```python
465
+ import asyncio
466
+ from watchdiff import WatchDiff
467
+
468
+ async def main():
469
+ wd = WatchDiff()
470
+ wd.watch("https://example.com", target="h1", interval=30)
471
+ wd.on_change(lambda r: print(r.summary()))
472
+ await wd.start_async()
473
+
474
+ asyncio.run(main())
475
+ ```
476
+
477
+ #### `.check_once(url)`
478
+
479
+ Run a single immediate check without starting the scheduler loop:
480
+
481
+ ```python
482
+ report = wd.check_once("https://example.com")
483
+ if report:
484
+ print(report.summary())
485
+ ```
486
+
487
+ #### `.history(url, limit=20)` / `.reports(url, limit=20)` / `.clear(url)`
488
+
489
+ Access stored data programmatically:
490
+
491
+ ```python
492
+ snaps = wd.history("https://example.com", limit=10)
493
+ reports = wd.reports("https://example.com", limit=10)
494
+ wd.clear("https://example.com")
495
+ ```
496
+
497
+ ### `DiffReport`
498
+
499
+ ```python
500
+ report.url # str
501
+ report.target # str | None
502
+ report.label # str
503
+ report.has_changes # bool
504
+ report.added # list[Change]
505
+ report.removed # list[Change]
506
+ report.modified # list[Change]
507
+ report.changes # list[Change] - all changes
508
+ report.compared_at # datetime
509
+
510
+ report.summary() # "[Book price] 1 modified - 2024-01-15 10:30:00 UTC"
511
+ report.as_dict() # JSON-serialisable dict
512
+ ```
513
+
514
+ ### `Change`
515
+
516
+ ```python
517
+ change.kind # ChangeType.ADDED | REMOVED | MODIFIED | UNCHANGED
518
+ change.before # str | None - previous value
519
+ change.after # str | None - new value
520
+ change.context # str | None - surrounding text hint
521
+
522
+ change.human() # "[~] Changed: '$19.00' - '$24.00'"
523
+ ```
524
+
525
+ ## Webhooks
526
+
527
+ WatchDiff auto-detects the target service and adapts the payload:
528
+
529
+ | Service | Detection | Payload |
530
+ |---|---|---|
531
+ | Discord | `discord.com` in URL | `{"content": "..."}` (2000-char limit) |
532
+ | Slack | `hooks.slack.com` in URL | `{"text": "..."}` |
533
+ | Custom | anything else | full `report.as_dict()` |
534
+
535
+ ```python
536
+ wd.watch(
537
+ "https://example.com",
538
+ webhooks=[
539
+ "https://discord.com/api/webhooks/YOUR_ID/YOUR_TOKEN",
540
+ "https://hooks.slack.com/services/YOUR/SLACK/WEBHOOK",
541
+ "https://your-api.com/watchdiff-hook",
542
+ ],
543
+ )
544
+ ```
545
+
546
+ ## CLI reference
547
+
548
+ ```
549
+ Usage: watchdiff [COMMAND] [OPTIONS]
550
+
551
+ Commands:
552
+ init Generate a watchdiff.config.json template
553
+ run Start continuous monitoring (URL or config file)
554
+ check Run a single check and print the result
555
+ history Show snapshot history for a URL
556
+ reports Show diff reports for a URL
557
+ clear Delete all stored data for a URL
558
+
559
+ Options for run / check:
560
+ --target -t CSS selector or XPath to watch
561
+ --storage -s Storage directory (default: .watchdiff)
562
+ --interval -i Seconds between checks (run only)
563
+ --config -c Path to a watchdiff.config.json file
564
+ --diff-mode Diff strategy: line (default) | semantic
565
+ --browser Use headless browser (requires playwright)
566
+ --cooldown Min seconds between alerts (0 = disabled)
567
+ --verbose -v Enable debug logging
568
+
569
+ Options for history / reports:
570
+ --limit -n Number of entries to show (default 20)
571
+
572
+ Options for clear:
573
+ --yes -y Skip confirmation prompt
574
+
575
+ Options for check:
576
+ --json Output raw JSON instead of formatted output
577
+ ```
578
+
579
+ ## Use cases
580
+
581
+ - **E-commerce** — track product prices and stock availability
582
+ - **News monitoring** — detect article updates or new publications
583
+ - **Documentation** — alert when API docs or changelogs change
584
+ - **Public APIs** — watch JSON endpoints for schema or value changes
585
+ - **SPA / React apps** — monitor JS-rendered content with `browser=True`
586
+ - **Compliance** — audit changes on public-facing pages over time
587
+ - **Research** — collect snapshots for longitudinal content analysis
588
+
589
+
590
+ ## Contributing
591
+
592
+ Missing a feature? Found a bug? Pull requests are welcome on [GitHub](https://github.com/r-seize/watchdiff-py).
593
+
594
+ If you want a feature that is not yet in the project, open an issue or submit a PR directly - contributions of any size are appreciated.
595
+
596
+ ## License
597
+
598
+ This project is licensed under the [GNU General Public License v3.0](LICENSE).
599
+
600
+ You are free to use, study, modify, and distribute this software under the terms of the GPL v3.
601
+ Any derivative work must also be distributed under the same license.