watchdiff-core 0.2.3__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.3 → watchdiff_core-0.2.4}/PKG-INFO +351 -11
- {watchdiff_core-0.2.3 → watchdiff_core-0.2.4}/README.md +350 -10
- {watchdiff_core-0.2.3 → watchdiff_core-0.2.4}/pyproject.toml +1 -1
- {watchdiff_core-0.2.3 → watchdiff_core-0.2.4}/tests/test_watchdiff.py +217 -2
- {watchdiff_core-0.2.3 → watchdiff_core-0.2.4}/uv.lock +1 -1
- {watchdiff_core-0.2.3 → watchdiff_core-0.2.4}/watchdiff/__init__.py +6 -0
- {watchdiff_core-0.2.3 → watchdiff_core-0.2.4}/watchdiff/cli/main.py +176 -5
- {watchdiff_core-0.2.3 → watchdiff_core-0.2.4}/watchdiff/core.py +19 -3
- {watchdiff_core-0.2.3 → watchdiff_core-0.2.4}/watchdiff/models.py +42 -1
- {watchdiff_core-0.2.3 → watchdiff_core-0.2.4}/watchdiff/scheduler/scheduler.py +229 -34
- {watchdiff_core-0.2.3 → watchdiff_core-0.2.4}/.github/workflows/ci.yml +0 -0
- {watchdiff_core-0.2.3 → watchdiff_core-0.2.4}/.github/workflows/release-testpypi.yml +0 -0
- {watchdiff_core-0.2.3 → watchdiff_core-0.2.4}/.github/workflows/release.yml +0 -0
- {watchdiff_core-0.2.3 → watchdiff_core-0.2.4}/.gitignore +0 -0
- {watchdiff_core-0.2.3 → watchdiff_core-0.2.4}/.python-version +0 -0
- {watchdiff_core-0.2.3 → watchdiff_core-0.2.4}/LICENSE +0 -0
- {watchdiff_core-0.2.3 → watchdiff_core-0.2.4}/main.py +0 -0
- {watchdiff_core-0.2.3 → watchdiff_core-0.2.4}/tests/__init__.py +0 -0
- {watchdiff_core-0.2.3 → watchdiff_core-0.2.4}/tests/test_db.py +0 -0
- {watchdiff_core-0.2.3 → watchdiff_core-0.2.4}/watchdiff/ai_summarizer/__init__.py +0 -0
- {watchdiff_core-0.2.3 → watchdiff_core-0.2.4}/watchdiff/cert_fetcher/__init__.py +0 -0
- {watchdiff_core-0.2.3 → watchdiff_core-0.2.4}/watchdiff/cert_models.py +0 -0
- {watchdiff_core-0.2.3 → watchdiff_core-0.2.4}/watchdiff/cert_scheduler/__init__.py +0 -0
- {watchdiff_core-0.2.3 → watchdiff_core-0.2.4}/watchdiff/cleaner/__init__.py +0 -0
- {watchdiff_core-0.2.3 → watchdiff_core-0.2.4}/watchdiff/cleaner/cleaner.py +0 -0
- {watchdiff_core-0.2.3 → watchdiff_core-0.2.4}/watchdiff/cli/__init__.py +0 -0
- {watchdiff_core-0.2.3 → watchdiff_core-0.2.4}/watchdiff/cron_parser/__init__.py +0 -0
- {watchdiff_core-0.2.3 → watchdiff_core-0.2.4}/watchdiff/db_diff/__init__.py +0 -0
- {watchdiff_core-0.2.3 → watchdiff_core-0.2.4}/watchdiff/db_fetcher/__init__.py +0 -0
- {watchdiff_core-0.2.3 → watchdiff_core-0.2.4}/watchdiff/db_fetcher/mysql.py +0 -0
- {watchdiff_core-0.2.3 → watchdiff_core-0.2.4}/watchdiff/db_fetcher/postgres.py +0 -0
- {watchdiff_core-0.2.3 → watchdiff_core-0.2.4}/watchdiff/db_fetcher/sqlite.py +0 -0
- {watchdiff_core-0.2.3 → watchdiff_core-0.2.4}/watchdiff/db_models.py +0 -0
- {watchdiff_core-0.2.3 → watchdiff_core-0.2.4}/watchdiff/db_scheduler/__init__.py +0 -0
- {watchdiff_core-0.2.3 → watchdiff_core-0.2.4}/watchdiff/diff/__init__.py +0 -0
- {watchdiff_core-0.2.3 → watchdiff_core-0.2.4}/watchdiff/diff/engine.py +0 -0
- {watchdiff_core-0.2.3 → watchdiff_core-0.2.4}/watchdiff/exporter/__init__.py +0 -0
- {watchdiff_core-0.2.3 → watchdiff_core-0.2.4}/watchdiff/exporter/exporter.py +0 -0
- {watchdiff_core-0.2.3 → watchdiff_core-0.2.4}/watchdiff/fetcher/__init__.py +0 -0
- {watchdiff_core-0.2.3 → watchdiff_core-0.2.4}/watchdiff/fetcher/browser.py +0 -0
- {watchdiff_core-0.2.3 → watchdiff_core-0.2.4}/watchdiff/fetcher/fetcher.py +0 -0
- {watchdiff_core-0.2.3 → watchdiff_core-0.2.4}/watchdiff/file_fetcher/__init__.py +0 -0
- {watchdiff_core-0.2.3 → watchdiff_core-0.2.4}/watchdiff/json_path/__init__.py +0 -0
- {watchdiff_core-0.2.3 → watchdiff_core-0.2.4}/watchdiff/mailer/__init__.py +0 -0
- {watchdiff_core-0.2.3 → watchdiff_core-0.2.4}/watchdiff/notifier/__init__.py +0 -0
- {watchdiff_core-0.2.3 → watchdiff_core-0.2.4}/watchdiff/notifier/notifier.py +0 -0
- {watchdiff_core-0.2.3 → watchdiff_core-0.2.4}/watchdiff/parser/__init__.py +0 -0
- {watchdiff_core-0.2.3 → watchdiff_core-0.2.4}/watchdiff/parser/parser.py +0 -0
- {watchdiff_core-0.2.3 → watchdiff_core-0.2.4}/watchdiff/scheduler/__init__.py +0 -0
- {watchdiff_core-0.2.3 → watchdiff_core-0.2.4}/watchdiff/sitemap/__init__.py +0 -0
- {watchdiff_core-0.2.3 → watchdiff_core-0.2.4}/watchdiff/status_server/__init__.py +0 -0
- {watchdiff_core-0.2.3 → watchdiff_core-0.2.4}/watchdiff/status_server/server.py +0 -0
- {watchdiff_core-0.2.3 → watchdiff_core-0.2.4}/watchdiff/store/__init__.py +0 -0
- {watchdiff_core-0.2.3 → watchdiff_core-0.2.4}/watchdiff/store/sqlite_store.py +0 -0
- {watchdiff_core-0.2.3 → 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
|
|
@@ -92,6 +92,11 @@ WatchDiff watches web pages, files, APIs, databases, and SSL certificates - then
|
|
|
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
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)` |
|
|
95
100
|
| Send email on change | `alert=AlertConfig(email=EmailConfig(to="...", smtp=SmtpConfig(...)))` |
|
|
96
101
|
| AI summary of changes | `ai_summary=True, ai_provider=AiProvider(type="gemini", api_key="...")` |
|
|
97
102
|
| Customize the AI prompt | `ai_prompt="Summarize in French."` or `ai_prompt=lambda r: ...` |
|
|
@@ -149,6 +154,11 @@ WatchDiff watches web pages, files, APIs, databases, and SSL certificates - then
|
|
|
149
154
|
- [JSON path targeting](#json-path-targeting)
|
|
150
155
|
- [Cookie-based authentication](#cookie-based-authentication)
|
|
151
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)
|
|
152
162
|
- [API reference](#api-reference)
|
|
153
163
|
- [`.watch()`](#watchurl--)
|
|
154
164
|
- [`.watch_db()`](#watch_dbconnection_string-table--)
|
|
@@ -156,8 +166,13 @@ WatchDiff watches web pages, files, APIs, databases, and SSL certificates - then
|
|
|
156
166
|
- [`.watch_api()`](#watch_apiurl--)
|
|
157
167
|
- [`.watch_cert()`](#watch_certhost--port443-warning_days30-)
|
|
158
168
|
- [`.watch_sitemap()`](#watch_sitemapurl--)
|
|
159
|
-
- [`.
|
|
160
|
-
- [`.
|
|
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)
|
|
161
176
|
- [`DiffReport`](#diffreport)
|
|
162
177
|
- [`Change`](#change)
|
|
163
178
|
- [`Snapshot`](#snapshot)
|
|
@@ -165,6 +180,14 @@ WatchDiff watches web pages, files, APIs, databases, and SSL certificates - then
|
|
|
165
180
|
- [`SilenceInfo`](#silenceinfo)
|
|
166
181
|
- [`AlertConfig`](#alertconfig)
|
|
167
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)
|
|
168
191
|
- [`DbDiffReport`](#dbdiffreport)
|
|
169
192
|
- [`DbChange`](#dbchange)
|
|
170
193
|
- [`DbWatcherStatus`](#dbwatcherstatus)
|
|
@@ -297,6 +320,9 @@ watchdiff db "postgresql://user:pass@localhost/mydb" products \
|
|
|
297
320
|
# Generate a config file
|
|
298
321
|
watchdiff init
|
|
299
322
|
|
|
323
|
+
# Validate a config file without starting watchers
|
|
324
|
+
watchdiff validate watchdiff.config.json
|
|
325
|
+
|
|
300
326
|
# Run from config file
|
|
301
327
|
watchdiff run --config watchdiff.config.json
|
|
302
328
|
|
|
@@ -1089,7 +1115,15 @@ Edit `watchdiff.config.json`:
|
|
|
1089
1115
|
"ignore_selectors": [".cookie-banner", "#ad-container"],
|
|
1090
1116
|
"ignore_patterns": ["\\d+ views"],
|
|
1091
1117
|
"timeout": 15,
|
|
1092
|
-
"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
|
|
1093
1127
|
},
|
|
1094
1128
|
{
|
|
1095
1129
|
"url": "https://hnrss.org/frontpage",
|
|
@@ -1424,32 +1458,191 @@ wd.watch_api("https://api.example.com/account",
|
|
|
1424
1458
|
|
|
1425
1459
|
Send email notifications via SMTP when a change is detected. Uses Python stdlib `smtplib` — no extra dependency required.
|
|
1426
1460
|
|
|
1461
|
+
### Quickstart — `email=` shortcut
|
|
1462
|
+
|
|
1463
|
+
Pass an `EmailConfig` directly to `.watch()`:
|
|
1464
|
+
|
|
1427
1465
|
```python
|
|
1428
1466
|
from watchdiff import WatchDiff
|
|
1429
|
-
from watchdiff.models import
|
|
1467
|
+
from watchdiff.models import EmailConfig, SmtpConfig
|
|
1430
1468
|
|
|
1431
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
|
+
|
|
1432
1490
|
wd.watch("https://example.com/prices",
|
|
1433
1491
|
alert=AlertConfig(
|
|
1434
|
-
on_change=[],
|
|
1435
|
-
webhooks=[],
|
|
1492
|
+
on_change=[lambda r: print(r.summary())],
|
|
1493
|
+
webhooks=["https://ntfy.sh/my-topic"],
|
|
1436
1494
|
min_changes=1,
|
|
1437
1495
|
email=EmailConfig(
|
|
1438
|
-
to="alerts@example.com",
|
|
1496
|
+
to=["alerts@example.com", "team@example.com"],
|
|
1497
|
+
from_="watchdiff@example.com",
|
|
1498
|
+
subject="Price change detected",
|
|
1439
1499
|
smtp=SmtpConfig(
|
|
1440
1500
|
host="smtp.gmail.com",
|
|
1441
|
-
port=
|
|
1501
|
+
port=587, # STARTTLS
|
|
1442
1502
|
user="you@gmail.com",
|
|
1443
1503
|
password="app-password",
|
|
1444
1504
|
),
|
|
1445
1505
|
),
|
|
1446
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
|
+
)
|
|
1607
|
+
wd.start()
|
|
1608
|
+
```
|
|
1609
|
+
|
|
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"`, …).
|
|
1614
|
+
|
|
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
|
+
)
|
|
1447
1633
|
wd.start()
|
|
1448
1634
|
```
|
|
1449
1635
|
|
|
1450
|
-
|
|
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.
|
|
1451
1642
|
|
|
1452
|
-
|
|
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.
|
|
1453
1646
|
|
|
1454
1647
|
## API reference
|
|
1455
1648
|
|
|
@@ -1462,8 +1655,15 @@ from watchdiff.store import SqliteStore
|
|
|
1462
1655
|
wd = WatchDiff() # JSON store in .watchdiff/
|
|
1463
1656
|
wd = WatchDiff(storage_dir="/data/watchdiff") # custom JSON store path
|
|
1464
1657
|
wd = WatchDiff(store=SqliteStore("db.sqlite")) # SQLite store
|
|
1658
|
+
wd = WatchDiff(concurrency=5) # at most 5 parallel fetch workers
|
|
1465
1659
|
```
|
|
1466
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
|
+
|
|
1467
1667
|
#### `.watch(url, *, ...)`
|
|
1468
1668
|
|
|
1469
1669
|
Register a URL to monitor. All keyword arguments are optional. Returns `self` (chainable).
|
|
@@ -1516,6 +1716,11 @@ Register a URL to monitor. All keyword arguments are optional. Returns `self` (c
|
|
|
1516
1716
|
| `schedule` | `str \| None` | `None` | 5-field cron expression. Overrides `interval` when set. |
|
|
1517
1717
|
| `confirm_after` | `int \| None` | `None` | Re-verify after N seconds before alerting (flapping detection) |
|
|
1518
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 |
|
|
1519
1724
|
|
|
1520
1725
|
```python
|
|
1521
1726
|
# Chainable
|
|
@@ -1823,6 +2028,8 @@ status.checks_count # int
|
|
|
1823
2028
|
status.changes_count # int
|
|
1824
2029
|
status.errors_count # int
|
|
1825
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
|
|
1826
2033
|
|
|
1827
2034
|
status.as_dict() # JSON-serialisable dict
|
|
1828
2035
|
```
|
|
@@ -1891,6 +2098,128 @@ server.start()
|
|
|
1891
2098
|
server.stop()
|
|
1892
2099
|
```
|
|
1893
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
|
+
|
|
1894
2223
|
### `DbDiffReport`
|
|
1895
2224
|
|
|
1896
2225
|
Returned by `on_change` and `DbDiffEngine.compare()`:
|
|
@@ -1994,6 +2323,7 @@ summary = db_report_summary(report) # "orders: 1 inserted"
|
|
|
1994
2323
|
```
|
|
1995
2324
|
Commands:
|
|
1996
2325
|
init Generate a watchdiff.config.json template
|
|
2326
|
+
validate Check a config file for errors without starting any watchers
|
|
1997
2327
|
run Start continuous monitoring (URL or config file)
|
|
1998
2328
|
db Monitor a database table for changes
|
|
1999
2329
|
compare Fetch two URLs and compare their content
|
|
@@ -2028,11 +2358,18 @@ Options for run:
|
|
|
2028
2358
|
--alert-if-no-change Fire silence alert after N seconds without change (0 = off)
|
|
2029
2359
|
--proxy Proxy URL (repeatable)
|
|
2030
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
|
|
2031
2364
|
--webhook -w Webhook URL (repeatable)
|
|
2032
2365
|
--log-format Log format: text | json (default text)
|
|
2033
2366
|
--verbose -v Enable debug logging
|
|
2034
2367
|
--quiet -q Suppress change output
|
|
2035
2368
|
|
|
2369
|
+
Options for validate:
|
|
2370
|
+
--json Output result as JSON (exit code 0 = valid, 1 = invalid)
|
|
2371
|
+
--verbose -v Enable debug logging
|
|
2372
|
+
|
|
2036
2373
|
Options for db:
|
|
2037
2374
|
--diff-mode -m row | schema | aggregate | value (default row)
|
|
2038
2375
|
--interval -i Seconds between checks (default 60)
|
|
@@ -2112,6 +2449,9 @@ Every CLI option can be set via environment variable — useful for Docker, CI,
|
|
|
2112
2449
|
| `WATCHDIFF_ALERT_IF_NO_CHANGE` | `--alert-if-no-change` | `86400` |
|
|
2113
2450
|
| `WATCHDIFF_PROXY` | `--proxy` | `http://proxy:8080` |
|
|
2114
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` |
|
|
2115
2455
|
| `WATCHDIFF_TARGET` | `--target` | `.price` |
|
|
2116
2456
|
| `WATCHDIFF_QUIET` | `--quiet` | `true` |
|
|
2117
2457
|
| `WATCHDIFF_LOG_FORMAT` | `--log-format` | `json` |
|