watchdiff-core 0.2.2__tar.gz → 0.2.4__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.
- {watchdiff_core-0.2.2 → watchdiff_core-0.2.4}/PKG-INFO +377 -11
- {watchdiff_core-0.2.2 → watchdiff_core-0.2.4}/README.md +376 -10
- {watchdiff_core-0.2.2 → watchdiff_core-0.2.4}/pyproject.toml +1 -1
- {watchdiff_core-0.2.2 → watchdiff_core-0.2.4}/tests/test_watchdiff.py +217 -2
- {watchdiff_core-0.2.2 → watchdiff_core-0.2.4}/uv.lock +1 -1
- {watchdiff_core-0.2.2 → watchdiff_core-0.2.4}/watchdiff/__init__.py +6 -0
- {watchdiff_core-0.2.2 → watchdiff_core-0.2.4}/watchdiff/cli/main.py +176 -5
- {watchdiff_core-0.2.2 → watchdiff_core-0.2.4}/watchdiff/core.py +19 -3
- {watchdiff_core-0.2.2 → watchdiff_core-0.2.4}/watchdiff/fetcher/fetcher.py +8 -0
- {watchdiff_core-0.2.2 → watchdiff_core-0.2.4}/watchdiff/models.py +43 -1
- {watchdiff_core-0.2.2 → watchdiff_core-0.2.4}/watchdiff/scheduler/scheduler.py +229 -34
- {watchdiff_core-0.2.2 → watchdiff_core-0.2.4}/.github/workflows/ci.yml +0 -0
- {watchdiff_core-0.2.2 → watchdiff_core-0.2.4}/.github/workflows/release-testpypi.yml +0 -0
- {watchdiff_core-0.2.2 → watchdiff_core-0.2.4}/.github/workflows/release.yml +0 -0
- {watchdiff_core-0.2.2 → watchdiff_core-0.2.4}/.gitignore +0 -0
- {watchdiff_core-0.2.2 → watchdiff_core-0.2.4}/.python-version +0 -0
- {watchdiff_core-0.2.2 → watchdiff_core-0.2.4}/LICENSE +0 -0
- {watchdiff_core-0.2.2 → watchdiff_core-0.2.4}/main.py +0 -0
- {watchdiff_core-0.2.2 → watchdiff_core-0.2.4}/tests/__init__.py +0 -0
- {watchdiff_core-0.2.2 → watchdiff_core-0.2.4}/tests/test_db.py +0 -0
- {watchdiff_core-0.2.2 → watchdiff_core-0.2.4}/watchdiff/ai_summarizer/__init__.py +0 -0
- {watchdiff_core-0.2.2 → watchdiff_core-0.2.4}/watchdiff/cert_fetcher/__init__.py +0 -0
- {watchdiff_core-0.2.2 → watchdiff_core-0.2.4}/watchdiff/cert_models.py +0 -0
- {watchdiff_core-0.2.2 → watchdiff_core-0.2.4}/watchdiff/cert_scheduler/__init__.py +0 -0
- {watchdiff_core-0.2.2 → watchdiff_core-0.2.4}/watchdiff/cleaner/__init__.py +0 -0
- {watchdiff_core-0.2.2 → watchdiff_core-0.2.4}/watchdiff/cleaner/cleaner.py +0 -0
- {watchdiff_core-0.2.2 → watchdiff_core-0.2.4}/watchdiff/cli/__init__.py +0 -0
- {watchdiff_core-0.2.2 → watchdiff_core-0.2.4}/watchdiff/cron_parser/__init__.py +0 -0
- {watchdiff_core-0.2.2 → watchdiff_core-0.2.4}/watchdiff/db_diff/__init__.py +0 -0
- {watchdiff_core-0.2.2 → watchdiff_core-0.2.4}/watchdiff/db_fetcher/__init__.py +0 -0
- {watchdiff_core-0.2.2 → watchdiff_core-0.2.4}/watchdiff/db_fetcher/mysql.py +0 -0
- {watchdiff_core-0.2.2 → watchdiff_core-0.2.4}/watchdiff/db_fetcher/postgres.py +0 -0
- {watchdiff_core-0.2.2 → watchdiff_core-0.2.4}/watchdiff/db_fetcher/sqlite.py +0 -0
- {watchdiff_core-0.2.2 → watchdiff_core-0.2.4}/watchdiff/db_models.py +0 -0
- {watchdiff_core-0.2.2 → watchdiff_core-0.2.4}/watchdiff/db_scheduler/__init__.py +0 -0
- {watchdiff_core-0.2.2 → watchdiff_core-0.2.4}/watchdiff/diff/__init__.py +0 -0
- {watchdiff_core-0.2.2 → watchdiff_core-0.2.4}/watchdiff/diff/engine.py +0 -0
- {watchdiff_core-0.2.2 → watchdiff_core-0.2.4}/watchdiff/exporter/__init__.py +0 -0
- {watchdiff_core-0.2.2 → watchdiff_core-0.2.4}/watchdiff/exporter/exporter.py +0 -0
- {watchdiff_core-0.2.2 → watchdiff_core-0.2.4}/watchdiff/fetcher/__init__.py +0 -0
- {watchdiff_core-0.2.2 → watchdiff_core-0.2.4}/watchdiff/fetcher/browser.py +0 -0
- {watchdiff_core-0.2.2 → watchdiff_core-0.2.4}/watchdiff/file_fetcher/__init__.py +0 -0
- {watchdiff_core-0.2.2 → watchdiff_core-0.2.4}/watchdiff/json_path/__init__.py +0 -0
- {watchdiff_core-0.2.2 → watchdiff_core-0.2.4}/watchdiff/mailer/__init__.py +0 -0
- {watchdiff_core-0.2.2 → watchdiff_core-0.2.4}/watchdiff/notifier/__init__.py +0 -0
- {watchdiff_core-0.2.2 → watchdiff_core-0.2.4}/watchdiff/notifier/notifier.py +0 -0
- {watchdiff_core-0.2.2 → watchdiff_core-0.2.4}/watchdiff/parser/__init__.py +0 -0
- {watchdiff_core-0.2.2 → watchdiff_core-0.2.4}/watchdiff/parser/parser.py +0 -0
- {watchdiff_core-0.2.2 → watchdiff_core-0.2.4}/watchdiff/scheduler/__init__.py +0 -0
- {watchdiff_core-0.2.2 → watchdiff_core-0.2.4}/watchdiff/sitemap/__init__.py +0 -0
- {watchdiff_core-0.2.2 → watchdiff_core-0.2.4}/watchdiff/status_server/__init__.py +0 -0
- {watchdiff_core-0.2.2 → watchdiff_core-0.2.4}/watchdiff/status_server/server.py +0 -0
- {watchdiff_core-0.2.2 → watchdiff_core-0.2.4}/watchdiff/store/__init__.py +0 -0
- {watchdiff_core-0.2.2 → watchdiff_core-0.2.4}/watchdiff/store/sqlite_store.py +0 -0
- {watchdiff_core-0.2.2 → watchdiff_core-0.2.4}/watchdiff/store/store.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: watchdiff-core
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.4
|
|
4
4
|
Summary: Lightweight web change monitoring library - clean diffs, AI summaries, file/API/SSL/DB monitoring, structured alerts.
|
|
5
5
|
Author: WatchDiff Contributors
|
|
6
6
|
License: BSD-2-Clause
|
|
@@ -91,6 +91,12 @@ WatchDiff watches web pages, files, APIs, databases, and SSL certificates - then
|
|
|
91
91
|
| Confirm change before alerting | `confirm_after=30` (flapping detection) |
|
|
92
92
|
| Monitor a sitemap for URL changes | `.watch_sitemap("https://example.com/sitemap.xml")` |
|
|
93
93
|
| Watch one JSON field only | `json_path="$.data.price"` |
|
|
94
|
+
| Monitor authenticated pages | `cookies={"session": "abc123", "csrftoken": "xyz"}` |
|
|
95
|
+
| Give a watcher a stable ID | `id="product-price"` — use ID instead of URL in `.pause()`/`.resume()` |
|
|
96
|
+
| Limit concurrent checks | `WatchDiff(concurrency=5)` — cap parallel fetch workers |
|
|
97
|
+
| Skip checks during downtime | `maintenance_windows=[MaintenanceWindow(from_="2026-01-15T02:00Z", to="2026-01-15T04:00Z")]` |
|
|
98
|
+
| Restrict checks to business hours | `active_between=ActiveBetween(from_="09:00", to="17:00", days=[0,1,2,3,4], timezone="Europe/Paris")` |
|
|
99
|
+
| Suppress noisy error alerts | `failure_policy=FailurePolicy(consecutive_failures=3, recovery_checks=2)` |
|
|
94
100
|
| Send email on change | `alert=AlertConfig(email=EmailConfig(to="...", smtp=SmtpConfig(...)))` |
|
|
95
101
|
| AI summary of changes | `ai_summary=True, ai_provider=AiProvider(type="gemini", api_key="...")` |
|
|
96
102
|
| Customize the AI prompt | `ai_prompt="Summarize in French."` or `ai_prompt=lambda r: ...` |
|
|
@@ -146,7 +152,13 @@ WatchDiff watches web pages, files, APIs, databases, and SSL certificates - then
|
|
|
146
152
|
- [Flapping detection](#flapping-detection)
|
|
147
153
|
- [Sitemap monitoring](#sitemap-monitoring)
|
|
148
154
|
- [JSON path targeting](#json-path-targeting)
|
|
155
|
+
- [Cookie-based authentication](#cookie-based-authentication)
|
|
149
156
|
- [Email alerts](#email-alerts)
|
|
157
|
+
- [Watcher ID](#watcher-id)
|
|
158
|
+
- [Concurrency limiting](#concurrency-limiting)
|
|
159
|
+
- [Maintenance windows](#maintenance-windows)
|
|
160
|
+
- [Active hours](#active-hours)
|
|
161
|
+
- [Failure policy](#failure-policy)
|
|
150
162
|
- [API reference](#api-reference)
|
|
151
163
|
- [`.watch()`](#watchurl--)
|
|
152
164
|
- [`.watch_db()`](#watch_dbconnection_string-table--)
|
|
@@ -154,8 +166,13 @@ WatchDiff watches web pages, files, APIs, databases, and SSL certificates - then
|
|
|
154
166
|
- [`.watch_api()`](#watch_apiurl--)
|
|
155
167
|
- [`.watch_cert()`](#watch_certhost--port443-warning_days30-)
|
|
156
168
|
- [`.watch_sitemap()`](#watch_sitemapurl--)
|
|
157
|
-
- [`.
|
|
158
|
-
- [`.
|
|
169
|
+
- [`.on_change()`](#on_changecallback)
|
|
170
|
+
- [`.start()` / `start_async()` / `.stop()`](#startblocktrue--await-start_async--stop)
|
|
171
|
+
- [`.check_once()`](#check_onceurl)
|
|
172
|
+
- [`.compare_urls()`](#compare_urlsurl_a-url_b--)
|
|
173
|
+
- [`.start_status_server()` / `.stop_status_server()`](#start_status_serverport-host--stop_status_server)
|
|
174
|
+
- [`.pause()` / `.resume()` / `.status()` / `.db_status()`](#pauseurl--resumeurl--status--db_status)
|
|
175
|
+
- [`.history()` / `.reports()` / `.clear()`](#historyurl--reportsurl--clearurl)
|
|
159
176
|
- [`DiffReport`](#diffreport)
|
|
160
177
|
- [`Change`](#change)
|
|
161
178
|
- [`Snapshot`](#snapshot)
|
|
@@ -163,6 +180,14 @@ WatchDiff watches web pages, files, APIs, databases, and SSL certificates - then
|
|
|
163
180
|
- [`SilenceInfo`](#silenceinfo)
|
|
164
181
|
- [`AlertConfig`](#alertconfig)
|
|
165
182
|
- [`BrowserOptions`](#browseroptions)
|
|
183
|
+
- [`SpikeInfo`](#spikeinfo)
|
|
184
|
+
- [`StatusChangeInfo`](#statuschangeinfo)
|
|
185
|
+
- [`StatusServer`](#statusserver)
|
|
186
|
+
- [`EmailConfig` / `SmtpConfig`](#emailconfig--smtpconfig)
|
|
187
|
+
- [`SitemapDiffReport` / `SitemapEntry`](#sitemapdifreport--sitemapentry)
|
|
188
|
+
- [`MaintenanceWindow`](#maintenancewindow)
|
|
189
|
+
- [`ActiveBetween`](#activebetween)
|
|
190
|
+
- [`FailurePolicy`](#failurepolicy)
|
|
166
191
|
- [`DbDiffReport`](#dbdiffreport)
|
|
167
192
|
- [`DbChange`](#dbchange)
|
|
168
193
|
- [`DbWatcherStatus`](#dbwatcherstatus)
|
|
@@ -295,6 +320,9 @@ watchdiff db "postgresql://user:pass@localhost/mydb" products \
|
|
|
295
320
|
# Generate a config file
|
|
296
321
|
watchdiff init
|
|
297
322
|
|
|
323
|
+
# Validate a config file without starting watchers
|
|
324
|
+
watchdiff validate watchdiff.config.json
|
|
325
|
+
|
|
298
326
|
# Run from config file
|
|
299
327
|
watchdiff run --config watchdiff.config.json
|
|
300
328
|
|
|
@@ -1087,7 +1115,15 @@ Edit `watchdiff.config.json`:
|
|
|
1087
1115
|
"ignore_selectors": [".cookie-banner", "#ad-container"],
|
|
1088
1116
|
"ignore_patterns": ["\\d+ views"],
|
|
1089
1117
|
"timeout": 15,
|
|
1090
|
-
"headers": {}
|
|
1118
|
+
"headers": {},
|
|
1119
|
+
"schedule": null,
|
|
1120
|
+
"confirm_after": null,
|
|
1121
|
+
"json_path": null,
|
|
1122
|
+
"email": null,
|
|
1123
|
+
"id": null,
|
|
1124
|
+
"maintenance_windows": [],
|
|
1125
|
+
"active_between": null,
|
|
1126
|
+
"failure_policy": null
|
|
1091
1127
|
},
|
|
1092
1128
|
{
|
|
1093
1129
|
"url": "https://hnrss.org/frontpage",
|
|
@@ -1395,36 +1431,218 @@ value = extract_json_path('{"data": {"price": 42.5}}', "$.data.price")
|
|
|
1395
1431
|
# "42.5"
|
|
1396
1432
|
```
|
|
1397
1433
|
|
|
1434
|
+
## Cookie-based authentication
|
|
1435
|
+
|
|
1436
|
+
Pass cookies with every request to monitor pages that require a login session. Cookies are merged into the outgoing `Cookie` header on top of any `headers` you supply.
|
|
1437
|
+
|
|
1438
|
+
```python
|
|
1439
|
+
wd.watch("https://app.example.com/dashboard",
|
|
1440
|
+
cookies={"session": "your-session-token", "csrftoken": "your-csrf-token"},
|
|
1441
|
+
interval=300,
|
|
1442
|
+
on_change=lambda r: print(r.changes))
|
|
1443
|
+
```
|
|
1444
|
+
|
|
1445
|
+
Combine `cookies` with `headers` — the cookie string is appended to any existing `Cookie` header:
|
|
1446
|
+
|
|
1447
|
+
```python
|
|
1448
|
+
wd.watch_api("https://api.example.com/account",
|
|
1449
|
+
headers={"Authorization": "Bearer token123"},
|
|
1450
|
+
cookies={"_ga": "GA1.1.0000000000.0000000000"},
|
|
1451
|
+
json_path="$.balance",
|
|
1452
|
+
interval=60)
|
|
1453
|
+
```
|
|
1454
|
+
|
|
1455
|
+
> Tip: copy cookie values from your browser's DevTools → Network tab → Request Headers.
|
|
1456
|
+
|
|
1398
1457
|
## Email alerts
|
|
1399
1458
|
|
|
1400
1459
|
Send email notifications via SMTP when a change is detected. Uses Python stdlib `smtplib` — no extra dependency required.
|
|
1401
1460
|
|
|
1461
|
+
### Quickstart — `email=` shortcut
|
|
1462
|
+
|
|
1463
|
+
Pass an `EmailConfig` directly to `.watch()`:
|
|
1464
|
+
|
|
1402
1465
|
```python
|
|
1403
1466
|
from watchdiff import WatchDiff
|
|
1404
|
-
from watchdiff.models import
|
|
1467
|
+
from watchdiff.models import EmailConfig, SmtpConfig
|
|
1405
1468
|
|
|
1406
1469
|
wd = WatchDiff()
|
|
1470
|
+
wd.watch(
|
|
1471
|
+
"https://example.com/prices",
|
|
1472
|
+
email=EmailConfig(
|
|
1473
|
+
to="alerts@example.com",
|
|
1474
|
+
smtp=SmtpConfig(
|
|
1475
|
+
host="smtp.gmail.com",
|
|
1476
|
+
port=465,
|
|
1477
|
+
user="you@gmail.com",
|
|
1478
|
+
password="app-password",
|
|
1479
|
+
),
|
|
1480
|
+
),
|
|
1481
|
+
)
|
|
1482
|
+
wd.start()
|
|
1483
|
+
```
|
|
1484
|
+
|
|
1485
|
+
### Via `AlertConfig` — combine with webhooks and callbacks
|
|
1486
|
+
|
|
1487
|
+
```python
|
|
1488
|
+
from watchdiff.models import AlertConfig, EmailConfig, SmtpConfig
|
|
1489
|
+
|
|
1407
1490
|
wd.watch("https://example.com/prices",
|
|
1408
1491
|
alert=AlertConfig(
|
|
1409
|
-
on_change=[],
|
|
1410
|
-
webhooks=[],
|
|
1492
|
+
on_change=[lambda r: print(r.summary())],
|
|
1493
|
+
webhooks=["https://ntfy.sh/my-topic"],
|
|
1411
1494
|
min_changes=1,
|
|
1412
1495
|
email=EmailConfig(
|
|
1413
|
-
to="alerts@example.com",
|
|
1496
|
+
to=["alerts@example.com", "team@example.com"],
|
|
1497
|
+
from_="watchdiff@example.com",
|
|
1498
|
+
subject="Price change detected",
|
|
1414
1499
|
smtp=SmtpConfig(
|
|
1415
1500
|
host="smtp.gmail.com",
|
|
1416
|
-
port=
|
|
1501
|
+
port=587, # STARTTLS
|
|
1417
1502
|
user="you@gmail.com",
|
|
1418
1503
|
password="app-password",
|
|
1419
1504
|
),
|
|
1420
1505
|
),
|
|
1421
1506
|
))
|
|
1507
|
+
```
|
|
1508
|
+
|
|
1509
|
+
### Config file (`watchdiff.config.json`)
|
|
1510
|
+
|
|
1511
|
+
```json
|
|
1512
|
+
{
|
|
1513
|
+
"url": "https://example.com/prices",
|
|
1514
|
+
"email": {
|
|
1515
|
+
"to": "alerts@example.com",
|
|
1516
|
+
"from": "watchdiff@example.com",
|
|
1517
|
+
"subject": "Price change detected",
|
|
1518
|
+
"smtp": {
|
|
1519
|
+
"host": "smtp.gmail.com",
|
|
1520
|
+
"port": 465,
|
|
1521
|
+
"user": "you@gmail.com",
|
|
1522
|
+
"password": "app-password"
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
```
|
|
1527
|
+
|
|
1528
|
+
**Notes:**
|
|
1529
|
+
- `from_` defaults to `user@host` when omitted
|
|
1530
|
+
- `subject` defaults to `"[WatchDiff] Change detected: {label}"`
|
|
1531
|
+
- Multiple recipients: `to=["a@x.com", "b@x.com"]`
|
|
1532
|
+
- Port `465` uses SSL from the start (`SMTP_SSL`). All other ports use `STARTTLS`.
|
|
1533
|
+
|
|
1534
|
+
## Watcher ID
|
|
1535
|
+
|
|
1536
|
+
Assign a stable string identifier to any watcher. The ID is used instead of the URL for `.pause()`, `.resume()`, and `.status()` lookups, which is useful when two watchers share the same URL or when you want readable keys in your control code.
|
|
1537
|
+
|
|
1538
|
+
```python
|
|
1539
|
+
from watchdiff import WatchDiff
|
|
1540
|
+
|
|
1541
|
+
wd = WatchDiff()
|
|
1542
|
+
wd.watch("https://example.com/prices", id="product-price", interval=60)
|
|
1543
|
+
wd.watch("https://example.com/prices", id="product-price-2", interval=120)
|
|
1544
|
+
|
|
1545
|
+
wd.pause("product-price") # pause by ID, not URL
|
|
1546
|
+
wd.resume("product-price")
|
|
1547
|
+
```
|
|
1548
|
+
|
|
1549
|
+
Without `id`, `pause(url)` pauses **all** watchers registered for that URL. With `id`, each watcher is independent and can be paused or resumed individually.
|
|
1550
|
+
|
|
1551
|
+
IDs also appear in `WatcherStatus.id` and in the JSON returned by `status()`.
|
|
1552
|
+
|
|
1553
|
+
## Concurrency limiting
|
|
1554
|
+
|
|
1555
|
+
Cap the number of checks that run simultaneously. Without a limit all watchers fire in parallel; with a limit the scheduler queues excess checks.
|
|
1556
|
+
|
|
1557
|
+
```python
|
|
1558
|
+
wd = WatchDiff(concurrency=5) # at most 5 simultaneous fetches
|
|
1559
|
+
```
|
|
1560
|
+
|
|
1561
|
+
Useful when you monitor hundreds of URLs and want to avoid hammering a shared proxy pool or saturating a network interface.
|
|
1562
|
+
|
|
1563
|
+
## Maintenance windows
|
|
1564
|
+
|
|
1565
|
+
Skip checks during a known downtime window. Checks resume automatically once the window closes.
|
|
1566
|
+
|
|
1567
|
+
```python
|
|
1568
|
+
from watchdiff import WatchDiff, MaintenanceWindow
|
|
1569
|
+
|
|
1570
|
+
wd = WatchDiff()
|
|
1571
|
+
wd.watch(
|
|
1572
|
+
"https://api.example.com/health",
|
|
1573
|
+
interval=60,
|
|
1574
|
+
maintenance_windows=[
|
|
1575
|
+
MaintenanceWindow(
|
|
1576
|
+
from_="2026-02-01T02:00:00+00:00", # ISO 8601 UTC start
|
|
1577
|
+
to="2026-02-01T04:00:00+00:00", # UTC end
|
|
1578
|
+
),
|
|
1579
|
+
],
|
|
1580
|
+
)
|
|
1581
|
+
wd.start()
|
|
1582
|
+
```
|
|
1583
|
+
|
|
1584
|
+
- `from_` and `to` accept either an ISO 8601 string or a `datetime` object.
|
|
1585
|
+
- Multiple windows can be listed; any overlapping window suppresses the check.
|
|
1586
|
+
- The window is evaluated fresh on each tick — no restart needed after the window passes.
|
|
1587
|
+
- `WatcherStatus.in_maintenance` reflects the current state.
|
|
1588
|
+
|
|
1589
|
+
## Active hours
|
|
1590
|
+
|
|
1591
|
+
Restrict checks to specific hours and/or days. Checks outside the window are silently skipped until the next window opens.
|
|
1592
|
+
|
|
1593
|
+
```python
|
|
1594
|
+
from watchdiff import WatchDiff, ActiveBetween
|
|
1595
|
+
|
|
1596
|
+
wd = WatchDiff()
|
|
1597
|
+
wd.watch(
|
|
1598
|
+
"https://example.com/prices",
|
|
1599
|
+
interval=60,
|
|
1600
|
+
active_between=ActiveBetween(
|
|
1601
|
+
from_="09:00", # "HH:MM" — start of active window
|
|
1602
|
+
to="17:00", # "HH:MM" — end of active window
|
|
1603
|
+
days=[0, 1, 2, 3, 4], # Mon–Fri (0=Monday, 6=Sunday). None = every day
|
|
1604
|
+
timezone="Europe/Paris", # IANA timezone name. None = UTC
|
|
1605
|
+
),
|
|
1606
|
+
)
|
|
1422
1607
|
wd.start()
|
|
1423
1608
|
```
|
|
1424
1609
|
|
|
1425
|
-
|
|
1610
|
+
- `to` before `from_` (e.g. `from_="22:00"`, `to="06:00"`) is treated as an overnight window.
|
|
1611
|
+
- `days` defaults to all 7 days when omitted. `timezone` defaults to UTC when omitted.
|
|
1612
|
+
- Requires Python 3.9+ `zoneinfo` stdlib. Falls back to UTC if the IANA database is unavailable.
|
|
1613
|
+
- **Note:** the Python API uses integer weekday indices (`0`=Monday … `6`=Sunday, matching `datetime.weekday()`). The TypeScript port uses string names (`"monday"`, `"friday"`, …).
|
|
1426
1614
|
|
|
1427
|
-
|
|
1615
|
+
## Failure policy
|
|
1616
|
+
|
|
1617
|
+
Gate the `on_error` callback until a URL has failed a configurable number of times in a row, and require a configurable number of consecutive successes before considering it recovered. Reduces alert noise from transient blips.
|
|
1618
|
+
|
|
1619
|
+
```python
|
|
1620
|
+
from watchdiff import WatchDiff, FailurePolicy
|
|
1621
|
+
|
|
1622
|
+
wd = WatchDiff()
|
|
1623
|
+
wd.watch(
|
|
1624
|
+
"https://example.com/status",
|
|
1625
|
+
interval=30,
|
|
1626
|
+
failure_policy=FailurePolicy(
|
|
1627
|
+
consecutive_failures=3, # only fire on_error after 3 failures in a row
|
|
1628
|
+
recovery_checks=2, # require 2 consecutive successes to clear the failure state
|
|
1629
|
+
respect_retry_after=True, # honour Retry-After response header when present
|
|
1630
|
+
),
|
|
1631
|
+
on_error=lambda exc, cfg: print(f"Confirmed failure: {exc}"),
|
|
1632
|
+
)
|
|
1633
|
+
wd.start()
|
|
1634
|
+
```
|
|
1635
|
+
|
|
1636
|
+
Without `failure_policy`, the first fetch error fires `on_error` immediately. With it:
|
|
1637
|
+
|
|
1638
|
+
- Errors below the threshold are silently swallowed — no callback, no alert.
|
|
1639
|
+
- After `consecutive_failures` errors in a row, `on_error` fires **once**, then is suppressed until recovery.
|
|
1640
|
+
- Recovery requires `recovery_checks` consecutive successful fetches.
|
|
1641
|
+
- When `respect_retry_after=True` and the server returns a `Retry-After` header (e.g. on 429/503), the next check is postponed until the header deadline.
|
|
1642
|
+
|
|
1643
|
+
Default: `consecutive_failures=3`, `recovery_checks=1`, `respect_retry_after=False`.
|
|
1644
|
+
|
|
1645
|
+
> **Note:** the TypeScript port defaults `consecutiveFailures` to **1** (fire on the first error). Python defaults to **3**. A bare `FailurePolicy()` therefore behaves differently across the two implementations.
|
|
1428
1646
|
|
|
1429
1647
|
## API reference
|
|
1430
1648
|
|
|
@@ -1437,8 +1655,15 @@ from watchdiff.store import SqliteStore
|
|
|
1437
1655
|
wd = WatchDiff() # JSON store in .watchdiff/
|
|
1438
1656
|
wd = WatchDiff(storage_dir="/data/watchdiff") # custom JSON store path
|
|
1439
1657
|
wd = WatchDiff(store=SqliteStore("db.sqlite")) # SQLite store
|
|
1658
|
+
wd = WatchDiff(concurrency=5) # at most 5 parallel fetch workers
|
|
1440
1659
|
```
|
|
1441
1660
|
|
|
1661
|
+
| Parameter | Type | Default | Description |
|
|
1662
|
+
|---|---|---|---|
|
|
1663
|
+
| `storage_dir` | `str` | `".watchdiff"` | Directory for JSON snapshot/report files |
|
|
1664
|
+
| `store` | `Store \| None` | `None` | Custom store implementation (e.g. `SqliteStore`) |
|
|
1665
|
+
| `concurrency` | `int \| None` | `None` | Max simultaneous check workers. `None` = unlimited |
|
|
1666
|
+
|
|
1442
1667
|
#### `.watch(url, *, ...)`
|
|
1443
1668
|
|
|
1444
1669
|
Register a URL to monitor. All keyword arguments are optional. Returns `self` (chainable).
|
|
@@ -1480,6 +1705,7 @@ Register a URL to monitor. All keyword arguments are optional. Returns `self` (c
|
|
|
1480
1705
|
| `on_spike` | `Callable \| None` | `None` | Called with `SpikeInfo` when spike is detected |
|
|
1481
1706
|
| `alert_on_status_change` | `bool` | `False` | Alert when HTTP status code changes (200→503, etc.) |
|
|
1482
1707
|
| `on_status_change` | `Callable \| None` | `None` | Called with `StatusChangeInfo` on status code change |
|
|
1708
|
+
| `cookies` | `dict[str, str]` | `{}` | Cookies sent with every request — merged into the `Cookie` header (e.g. `{"session": "abc"}`) |
|
|
1483
1709
|
| `alert` | `AlertConfig \| None` | `None` | Full alert config — use instead of `on_change`/`webhooks` to include email or fine-tune retries |
|
|
1484
1710
|
| `alert_if` | `Callable[[DiffReport], bool] \| None` | `None` | Custom gate - only alert when this function returns `True` |
|
|
1485
1711
|
| `expected_status` | `int \| None` | `None` | Fire `on_error` when actual HTTP status differs from this value |
|
|
@@ -1490,6 +1716,11 @@ Register a URL to monitor. All keyword arguments are optional. Returns `self` (c
|
|
|
1490
1716
|
| `schedule` | `str \| None` | `None` | 5-field cron expression. Overrides `interval` when set. |
|
|
1491
1717
|
| `confirm_after` | `int \| None` | `None` | Re-verify after N seconds before alerting (flapping detection) |
|
|
1492
1718
|
| `json_path` | `str \| None` | `None` | JSON path expression to extract a sub-value before diffing (e.g. `"$.data.price"`) |
|
|
1719
|
+
| `email` | `EmailConfig \| None` | `None` | SMTP email alert fired on every detected change — shortcut over building a full `AlertConfig` |
|
|
1720
|
+
| `id` | `str \| None` | `None` | Stable identifier for this watcher — used instead of URL in `.pause()`/`.resume()` |
|
|
1721
|
+
| `maintenance_windows` | `list[MaintenanceWindow]` | `[]` | One-time UTC time ranges during which checks are skipped |
|
|
1722
|
+
| `active_between` | `ActiveBetween \| None` | `None` | Restrict checks to a recurring daily/weekly time window |
|
|
1723
|
+
| `failure_policy` | `FailurePolicy \| None` | `None` | Gate `on_error` until N consecutive failures; require M consecutive successes to recover |
|
|
1493
1724
|
|
|
1494
1725
|
```python
|
|
1495
1726
|
# Chainable
|
|
@@ -1797,6 +2028,8 @@ status.checks_count # int
|
|
|
1797
2028
|
status.changes_count # int
|
|
1798
2029
|
status.errors_count # int
|
|
1799
2030
|
status.last_status_code # int — last known HTTP status (0 = unknown)
|
|
2031
|
+
status.id # str | None — stable watcher ID (set via id= on .watch())
|
|
2032
|
+
status.in_maintenance # bool — True if currently inside a maintenance window
|
|
1800
2033
|
|
|
1801
2034
|
status.as_dict() # JSON-serialisable dict
|
|
1802
2035
|
```
|
|
@@ -1865,6 +2098,128 @@ server.start()
|
|
|
1865
2098
|
server.stop()
|
|
1866
2099
|
```
|
|
1867
2100
|
|
|
2101
|
+
### `EmailConfig` / `SmtpConfig`
|
|
2102
|
+
|
|
2103
|
+
```python
|
|
2104
|
+
from watchdiff import EmailConfig, SmtpConfig
|
|
2105
|
+
|
|
2106
|
+
email = EmailConfig(
|
|
2107
|
+
to="ops@example.com", # str or list[str]
|
|
2108
|
+
smtp=SmtpConfig(
|
|
2109
|
+
host="smtp.example.com",
|
|
2110
|
+
port=587,
|
|
2111
|
+
user="alerts@example.com",
|
|
2112
|
+
password="secret",
|
|
2113
|
+
secure=None, # None = auto (SSL on 465, STARTTLS otherwise)
|
|
2114
|
+
),
|
|
2115
|
+
from_="alerts@example.com", # optional sender address
|
|
2116
|
+
subject="[WatchDiff] change", # optional subject override
|
|
2117
|
+
)
|
|
2118
|
+
```
|
|
2119
|
+
|
|
2120
|
+
Pass as `email=` to `.watch()` or inside `AlertConfig(email=...)`.
|
|
2121
|
+
|
|
2122
|
+
---
|
|
2123
|
+
|
|
2124
|
+
### `SitemapDiffReport` / `SitemapEntry`
|
|
2125
|
+
|
|
2126
|
+
Passed to callbacks registered with `.watch_sitemap()`:
|
|
2127
|
+
|
|
2128
|
+
```python
|
|
2129
|
+
report.sitemap_url # str — URL of the sitemap
|
|
2130
|
+
report.label # str — human-readable label
|
|
2131
|
+
report.added # list[SitemapEntry] — URLs newly present in the sitemap
|
|
2132
|
+
report.removed # list[SitemapEntry] — URLs no longer in the sitemap
|
|
2133
|
+
report.compared_at # datetime — UTC timestamp of the comparison
|
|
2134
|
+
```
|
|
2135
|
+
|
|
2136
|
+
Each `SitemapEntry`:
|
|
2137
|
+
|
|
2138
|
+
```python
|
|
2139
|
+
entry.url # str
|
|
2140
|
+
entry.last_modified # str | None — <lastmod> value from the sitemap
|
|
2141
|
+
entry.change_freq # str | None — <changefreq> value
|
|
2142
|
+
entry.priority # str | None — <priority> value
|
|
2143
|
+
```
|
|
2144
|
+
|
|
2145
|
+
---
|
|
2146
|
+
|
|
2147
|
+
### `MaintenanceWindow`
|
|
2148
|
+
|
|
2149
|
+
Defines a one-time UTC window during which checks are paused:
|
|
2150
|
+
|
|
2151
|
+
```python
|
|
2152
|
+
from watchdiff import MaintenanceWindow
|
|
2153
|
+
from datetime import datetime, timezone
|
|
2154
|
+
|
|
2155
|
+
# From ISO 8601 strings (recommended)
|
|
2156
|
+
w = MaintenanceWindow(
|
|
2157
|
+
from_="2026-02-01T02:00:00+00:00",
|
|
2158
|
+
to="2026-02-01T04:00:00+00:00",
|
|
2159
|
+
)
|
|
2160
|
+
|
|
2161
|
+
# From datetime objects
|
|
2162
|
+
w = MaintenanceWindow(
|
|
2163
|
+
from_=datetime(2026, 2, 1, 2, 0, tzinfo=timezone.utc),
|
|
2164
|
+
to=datetime(2026, 2, 1, 4, 0, tzinfo=timezone.utc),
|
|
2165
|
+
)
|
|
2166
|
+
```
|
|
2167
|
+
|
|
2168
|
+
| Field | Type | Description |
|
|
2169
|
+
|---|---|---|
|
|
2170
|
+
| `from_` | `datetime \| str` | UTC start; ISO 8601 string or `datetime` |
|
|
2171
|
+
| `to` | `datetime \| str` | UTC end |
|
|
2172
|
+
|
|
2173
|
+
ISO strings are parsed in `__post_init__`. `"Z"` suffix is accepted as `+00:00`.
|
|
2174
|
+
|
|
2175
|
+
---
|
|
2176
|
+
|
|
2177
|
+
### `ActiveBetween`
|
|
2178
|
+
|
|
2179
|
+
Restricts checks to a recurring daily or weekly time window:
|
|
2180
|
+
|
|
2181
|
+
```python
|
|
2182
|
+
from watchdiff import ActiveBetween
|
|
2183
|
+
|
|
2184
|
+
ab = ActiveBetween(
|
|
2185
|
+
from_="09:00",
|
|
2186
|
+
to="17:00",
|
|
2187
|
+
days=[0, 1, 2, 3, 4], # 0=Monday … 6=Sunday. None = every day
|
|
2188
|
+
timezone="Europe/Paris", # IANA name. None = UTC
|
|
2189
|
+
)
|
|
2190
|
+
```
|
|
2191
|
+
|
|
2192
|
+
| Field | Type | Default | Description |
|
|
2193
|
+
|---|---|---|---|
|
|
2194
|
+
| `from_` | `str` | — | Window start as `"HH:MM"` |
|
|
2195
|
+
| `to` | `str` | — | Window end as `"HH:MM"`. If before `from_`, treated as overnight |
|
|
2196
|
+
| `days` | `list[int] \| None` | `None` | Weekdays to restrict to (`0`=Mon). `None` = every day |
|
|
2197
|
+
| `timezone` | `str \| None` | `None` | IANA timezone name (e.g. `"America/New_York"`). `None` = UTC |
|
|
2198
|
+
|
|
2199
|
+
---
|
|
2200
|
+
|
|
2201
|
+
### `FailurePolicy`
|
|
2202
|
+
|
|
2203
|
+
Gates `on_error` to avoid alert noise from transient blips:
|
|
2204
|
+
|
|
2205
|
+
```python
|
|
2206
|
+
from watchdiff import FailurePolicy
|
|
2207
|
+
|
|
2208
|
+
fp = FailurePolicy(
|
|
2209
|
+
consecutive_failures=3, # fire on_error only after this many consecutive failures
|
|
2210
|
+
recovery_checks=2, # require this many consecutive successes to clear failure state
|
|
2211
|
+
respect_retry_after=True, # honour Retry-After response header if present
|
|
2212
|
+
)
|
|
2213
|
+
```
|
|
2214
|
+
|
|
2215
|
+
| Field | Type | Default | Description |
|
|
2216
|
+
|---|---|---|---|
|
|
2217
|
+
| `consecutive_failures` | `int` | `3` | Minimum consecutive failures before `on_error` fires (TypeScript default: `1`) |
|
|
2218
|
+
| `recovery_checks` | `int` | `1` | Consecutive successes required to exit failure mode |
|
|
2219
|
+
| `respect_retry_after` | `bool` | `False` | Use `Retry-After` header delay for the next check |
|
|
2220
|
+
|
|
2221
|
+
---
|
|
2222
|
+
|
|
1868
2223
|
### `DbDiffReport`
|
|
1869
2224
|
|
|
1870
2225
|
Returned by `on_change` and `DbDiffEngine.compare()`:
|
|
@@ -1968,6 +2323,7 @@ summary = db_report_summary(report) # "orders: 1 inserted"
|
|
|
1968
2323
|
```
|
|
1969
2324
|
Commands:
|
|
1970
2325
|
init Generate a watchdiff.config.json template
|
|
2326
|
+
validate Check a config file for errors without starting any watchers
|
|
1971
2327
|
run Start continuous monitoring (URL or config file)
|
|
1972
2328
|
db Monitor a database table for changes
|
|
1973
2329
|
compare Fetch two URLs and compare their content
|
|
@@ -2002,11 +2358,18 @@ Options for run:
|
|
|
2002
2358
|
--alert-if-no-change Fire silence alert after N seconds without change (0 = off)
|
|
2003
2359
|
--proxy Proxy URL (repeatable)
|
|
2004
2360
|
--user-agent User-Agent string (repeatable)
|
|
2361
|
+
--schedule 5-field cron expression — overrides --interval when set
|
|
2362
|
+
--confirm-after Re-fetch after N seconds before confirming a change (0 = off)
|
|
2363
|
+
--json-path $.dot.path expression to extract from JSON response before diffing
|
|
2005
2364
|
--webhook -w Webhook URL (repeatable)
|
|
2006
2365
|
--log-format Log format: text | json (default text)
|
|
2007
2366
|
--verbose -v Enable debug logging
|
|
2008
2367
|
--quiet -q Suppress change output
|
|
2009
2368
|
|
|
2369
|
+
Options for validate:
|
|
2370
|
+
--json Output result as JSON (exit code 0 = valid, 1 = invalid)
|
|
2371
|
+
--verbose -v Enable debug logging
|
|
2372
|
+
|
|
2010
2373
|
Options for db:
|
|
2011
2374
|
--diff-mode -m row | schema | aggregate | value (default row)
|
|
2012
2375
|
--interval -i Seconds between checks (default 60)
|
|
@@ -2086,6 +2449,9 @@ Every CLI option can be set via environment variable — useful for Docker, CI,
|
|
|
2086
2449
|
| `WATCHDIFF_ALERT_IF_NO_CHANGE` | `--alert-if-no-change` | `86400` |
|
|
2087
2450
|
| `WATCHDIFF_PROXY` | `--proxy` | `http://proxy:8080` |
|
|
2088
2451
|
| `WATCHDIFF_USER_AGENT` | `--user-agent` | `MyBot/1.0` |
|
|
2452
|
+
| `WATCHDIFF_SCHEDULE` | `--schedule` | `0 9 * * *` |
|
|
2453
|
+
| `WATCHDIFF_CONFIRM_AFTER` | `--confirm-after` | `30` |
|
|
2454
|
+
| `WATCHDIFF_JSON_PATH` | `--json-path` | `$.data.price` |
|
|
2089
2455
|
| `WATCHDIFF_TARGET` | `--target` | `.price` |
|
|
2090
2456
|
| `WATCHDIFF_QUIET` | `--quiet` | `true` |
|
|
2091
2457
|
| `WATCHDIFF_LOG_FORMAT` | `--log-format` | `json` |
|