watchdiff-core 0.2.0__tar.gz → 0.2.1__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 (51) hide show
  1. {watchdiff_core-0.2.0 → watchdiff_core-0.2.1}/.gitignore +2 -1
  2. {watchdiff_core-0.2.0 → watchdiff_core-0.2.1}/PKG-INFO +185 -2
  3. {watchdiff_core-0.2.0 → watchdiff_core-0.2.1}/README.md +183 -0
  4. {watchdiff_core-0.2.0 → watchdiff_core-0.2.1}/pyproject.toml +2 -2
  5. {watchdiff_core-0.2.0 → watchdiff_core-0.2.1}/watchdiff/__init__.py +37 -0
  6. watchdiff_core-0.2.1/watchdiff/ai_summarizer/__init__.py +224 -0
  7. watchdiff_core-0.2.1/watchdiff/cert_fetcher/__init__.py +51 -0
  8. watchdiff_core-0.2.1/watchdiff/cert_models.py +109 -0
  9. watchdiff_core-0.2.1/watchdiff/cert_scheduler/__init__.py +257 -0
  10. {watchdiff_core-0.2.0 → watchdiff_core-0.2.1}/watchdiff/core.py +186 -7
  11. {watchdiff_core-0.2.0 → watchdiff_core-0.2.1}/watchdiff/db_models.py +6 -2
  12. {watchdiff_core-0.2.0 → watchdiff_core-0.2.1}/watchdiff/db_scheduler/__init__.py +86 -21
  13. watchdiff_core-0.2.1/watchdiff/file_fetcher/__init__.py +26 -0
  14. {watchdiff_core-0.2.0 → watchdiff_core-0.2.1}/watchdiff/models.py +12 -2
  15. {watchdiff_core-0.2.0 → watchdiff_core-0.2.1}/watchdiff/scheduler/scheduler.py +129 -26
  16. {watchdiff_core-0.2.0 → watchdiff_core-0.2.1}/watchdiff/status_server/server.py +1 -1
  17. {watchdiff_core-0.2.0 → watchdiff_core-0.2.1}/.github/workflows/ci.yml +0 -0
  18. {watchdiff_core-0.2.0 → watchdiff_core-0.2.1}/.github/workflows/release-testpypi.yml +0 -0
  19. {watchdiff_core-0.2.0 → watchdiff_core-0.2.1}/.github/workflows/release.yml +0 -0
  20. {watchdiff_core-0.2.0 → watchdiff_core-0.2.1}/.python-version +0 -0
  21. {watchdiff_core-0.2.0 → watchdiff_core-0.2.1}/LICENSE +0 -0
  22. {watchdiff_core-0.2.0 → watchdiff_core-0.2.1}/main.py +0 -0
  23. {watchdiff_core-0.2.0 → watchdiff_core-0.2.1}/tests/__init__.py +0 -0
  24. {watchdiff_core-0.2.0 → watchdiff_core-0.2.1}/tests/test_db.py +0 -0
  25. {watchdiff_core-0.2.0 → watchdiff_core-0.2.1}/tests/test_watchdiff.py +0 -0
  26. {watchdiff_core-0.2.0 → watchdiff_core-0.2.1}/uv.lock +0 -0
  27. {watchdiff_core-0.2.0 → watchdiff_core-0.2.1}/watchdiff/cleaner/__init__.py +0 -0
  28. {watchdiff_core-0.2.0 → watchdiff_core-0.2.1}/watchdiff/cleaner/cleaner.py +0 -0
  29. {watchdiff_core-0.2.0 → watchdiff_core-0.2.1}/watchdiff/cli/__init__.py +0 -0
  30. {watchdiff_core-0.2.0 → watchdiff_core-0.2.1}/watchdiff/cli/main.py +0 -0
  31. {watchdiff_core-0.2.0 → watchdiff_core-0.2.1}/watchdiff/db_diff/__init__.py +0 -0
  32. {watchdiff_core-0.2.0 → watchdiff_core-0.2.1}/watchdiff/db_fetcher/__init__.py +0 -0
  33. {watchdiff_core-0.2.0 → watchdiff_core-0.2.1}/watchdiff/db_fetcher/mysql.py +0 -0
  34. {watchdiff_core-0.2.0 → watchdiff_core-0.2.1}/watchdiff/db_fetcher/postgres.py +0 -0
  35. {watchdiff_core-0.2.0 → watchdiff_core-0.2.1}/watchdiff/db_fetcher/sqlite.py +0 -0
  36. {watchdiff_core-0.2.0 → watchdiff_core-0.2.1}/watchdiff/diff/__init__.py +0 -0
  37. {watchdiff_core-0.2.0 → watchdiff_core-0.2.1}/watchdiff/diff/engine.py +0 -0
  38. {watchdiff_core-0.2.0 → watchdiff_core-0.2.1}/watchdiff/exporter/__init__.py +0 -0
  39. {watchdiff_core-0.2.0 → watchdiff_core-0.2.1}/watchdiff/exporter/exporter.py +0 -0
  40. {watchdiff_core-0.2.0 → watchdiff_core-0.2.1}/watchdiff/fetcher/__init__.py +0 -0
  41. {watchdiff_core-0.2.0 → watchdiff_core-0.2.1}/watchdiff/fetcher/browser.py +0 -0
  42. {watchdiff_core-0.2.0 → watchdiff_core-0.2.1}/watchdiff/fetcher/fetcher.py +0 -0
  43. {watchdiff_core-0.2.0 → watchdiff_core-0.2.1}/watchdiff/notifier/__init__.py +0 -0
  44. {watchdiff_core-0.2.0 → watchdiff_core-0.2.1}/watchdiff/notifier/notifier.py +0 -0
  45. {watchdiff_core-0.2.0 → watchdiff_core-0.2.1}/watchdiff/parser/__init__.py +0 -0
  46. {watchdiff_core-0.2.0 → watchdiff_core-0.2.1}/watchdiff/parser/parser.py +0 -0
  47. {watchdiff_core-0.2.0 → watchdiff_core-0.2.1}/watchdiff/scheduler/__init__.py +0 -0
  48. {watchdiff_core-0.2.0 → watchdiff_core-0.2.1}/watchdiff/status_server/__init__.py +0 -0
  49. {watchdiff_core-0.2.0 → watchdiff_core-0.2.1}/watchdiff/store/__init__.py +0 -0
  50. {watchdiff_core-0.2.0 → watchdiff_core-0.2.1}/watchdiff/store/sqlite_store.py +0 -0
  51. {watchdiff_core-0.2.0 → watchdiff_core-0.2.1}/watchdiff/store/store.py +0 -0
@@ -12,4 +12,5 @@ wheels/
12
12
  # other
13
13
  .ruff_cache
14
14
  .pytest_cache
15
- .env
15
+ .env
16
+ .watchdiff/
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: watchdiff-core
3
- Version: 0.2.0
4
- Summary: Lightweight web change monitoring library - clean diffs, structured alerts, no AI required.
3
+ Version: 0.2.1
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
7
7
  License-File: LICENSE
@@ -1049,6 +1049,125 @@ watchdiff status
1049
1049
  watchdiff status --config watchdiff.config.json
1050
1050
  ```
1051
1051
 
1052
+ ## AI summaries
1053
+
1054
+ Add a natural-language summary to every change alert. WatchDiff sends the diff to your AI provider of choice and attaches the result to `DiffReport.ai_summary`.
1055
+
1056
+ ### Auto-discovery (recommended)
1057
+
1058
+ Set one of the following environment variables - WatchDiff picks it up automatically:
1059
+
1060
+ ```bash
1061
+ export ANTHROPIC_API_KEY="sk-ant-..."
1062
+ export OPENAI_API_KEY="sk-..."
1063
+ export GEMINI_API_KEY="AIza..."
1064
+ ```
1065
+
1066
+ ```python
1067
+ wd.watch("https://example.com/prices", target=".price",
1068
+ ai_summary=True, # provider auto-detected from env
1069
+ on_change=lambda r: print(r.ai_summary))
1070
+ ```
1071
+
1072
+ Priority order when multiple keys are set: `ANTHROPIC_API_KEY` → `OPENAI_API_KEY` → `GEMINI_API_KEY` / `GOOGLE_AI_API_KEY`.
1073
+
1074
+ ### Explicit provider
1075
+
1076
+ ```python
1077
+ from watchdiff import AiProvider
1078
+
1079
+ wd.watch("https://example.com", ai_summary=True,
1080
+ ai_provider=AiProvider(type="gemini", api_key="...", model="gemini-3.1-flash-lite"))
1081
+ wd.watch("https://example.com", ai_summary=True,
1082
+ ai_provider=AiProvider(type="anthropic", api_key="...", model="claude-haiku-4-5-20251001"))
1083
+ wd.watch("https://example.com", ai_summary=True,
1084
+ ai_provider=AiProvider(type="openai", api_key="...", model="gpt-4o-mini"))
1085
+ ```
1086
+
1087
+ ### OpenAI-compatible providers (Ollama, Mistral, Groq, ...)
1088
+
1089
+ ```python
1090
+ # Local Ollama
1091
+ AiProvider(type="openai", base_url="http://localhost:11434/v1", model="llama3")
1092
+
1093
+ # Mistral
1094
+ AiProvider(type="openai", api_key="...", base_url="https://api.mistral.ai/v1", model="mistral-small")
1095
+
1096
+ # Groq
1097
+ AiProvider(type="openai", api_key="...", base_url="https://api.groq.com/openai/v1", model="llama3-8b-8192")
1098
+ ```
1099
+
1100
+ ### Custom function (any provider, any library)
1101
+
1102
+ ```python
1103
+ import anthropic
1104
+
1105
+ client = anthropic.Anthropic()
1106
+
1107
+ wd.watch("https://example.com", ai_summary=True,
1108
+ ai_provider=AiProvider(
1109
+ type="custom",
1110
+ call_ai=lambda prompt: client.messages.create(
1111
+ model="claude-opus-4-7",
1112
+ max_tokens=200,
1113
+ messages=[{"role": "user", "content": prompt}],
1114
+ ).content[0].text,
1115
+ ))
1116
+ ```
1117
+
1118
+ ### Custom prompt
1119
+
1120
+ Override the default summary template with `ai_prompt` - pass a static string or a callable that receives the `DiffReport` and returns a prompt string.
1121
+
1122
+ ```python
1123
+ # Static prompt - replaces the full instruction sent to the AI
1124
+ wd.watch_file("/tmp/prices.txt", ai_summary=True,
1125
+ ai_prompt="Summarize this change in French in one sentence.")
1126
+
1127
+ # Dynamic prompt - build the prompt from the diff report
1128
+ wd.watch("https://example.com/stock", ai_summary=True,
1129
+ ai_prompt=lambda r: (
1130
+ f"You are monitoring {r.url}. "
1131
+ f"Explain in plain English what changed: "
1132
+ + ", ".join(c.after or c.before or "" for c in r.changes)
1133
+ ))
1134
+ ```
1135
+
1136
+ The prompt is sent as-is to the provider - no variable substitution happens automatically. Use the callable form to interpolate any data from the `DiffReport`.
1137
+
1138
+ ### Built-in providers
1139
+
1140
+ | Provider | `type` | Default model |
1141
+ |---|---|---|
1142
+ | Google Gemini | `"gemini"` | `gemini-3.1-flash-lite` |
1143
+ | Anthropic Claude | `"anthropic"` | `claude-haiku-4-5-20251001` |
1144
+ | OpenAI / compatible | `"openai"` | `gpt-4o-mini` |
1145
+ | Any custom function | `"custom"` | - |
1146
+
1147
+ ### Error handling
1148
+
1149
+ AI failures are non-fatal - the watcher keeps running and fires its normal alerts.
1150
+
1151
+ | Error kind | What triggers it | Behaviour |
1152
+ |---|---|---|
1153
+ | `invalid_key` | 401 / 403 | AI disabled for this watcher for the rest of the session |
1154
+ | `quota_exceeded` | 429 rate limit | Skips AI this check, retries next interval |
1155
+ | `model_error` | 400 / unknown model | AI disabled - check your `model` value |
1156
+ | `network_error` | DNS / timeout | Skips this check, retries next interval |
1157
+
1158
+ ```python
1159
+ from watchdiff import AiError
1160
+
1161
+ try:
1162
+ from watchdiff.ai_summarizer import generate_ai_summary
1163
+ summary = generate_ai_summary(report, provider)
1164
+ except AiError as e:
1165
+ print(e.kind) # AiErrorKind.QUOTA_EXCEEDED
1166
+ print(e.is_retryable) # True - retry next check
1167
+ print(e.is_permanent) # False - don't disable
1168
+ print(e.status_code) # 429
1169
+ ```
1170
+
1052
1171
  ## API reference
1053
1172
 
1054
1173
  ### `WatchDiff`
@@ -1103,6 +1222,12 @@ Register a URL to monitor. All keyword arguments are optional. Returns `self` (c
1103
1222
  | `on_spike` | `Callable \| None` | `None` | Called with `SpikeInfo` when spike is detected |
1104
1223
  | `alert_on_status_change` | `bool` | `False` | Alert when HTTP status code changes (200→503, etc.) |
1105
1224
  | `on_status_change` | `Callable \| None` | `None` | Called with `StatusChangeInfo` on status code change |
1225
+ | `alert_if` | `Callable[[DiffReport], bool] \| None` | `None` | Custom gate - only alert when this function returns `True` |
1226
+ | `expected_status` | `int \| None` | `None` | Fire `on_error` when actual HTTP status differs from this value |
1227
+ | `track_response_time` | `bool` | `False` | Record response time in milliseconds in `DiffReport.response_time_ms` |
1228
+ | `ai_summary` | `bool` | `False` | Generate an AI natural-language summary of detected changes |
1229
+ | `ai_provider` | `AiProvider \| None` | `None` | AI provider to use. Auto-detected from env vars when `None` |
1230
+ | `ai_prompt` | `str \| Callable[[DiffReport], str] \| None` | `None` | Custom prompt sent to the AI instead of the default template |
1106
1231
 
1107
1232
  ```python
1108
1233
  # Chainable
@@ -1151,6 +1276,64 @@ wd.watch_db("sqlite:///app.db", "orders",
1151
1276
  wd.start()
1152
1277
  ```
1153
1278
 
1279
+ #### `.watch_file(path, *, ...)`
1280
+
1281
+ Watch a local file for changes. Accepts all the same options as `.watch()` (except browser-related ones). The path is converted to a `file://` URL internally. Content is compared as raw text, bypassing the HTML cleaner.
1282
+
1283
+ ```python
1284
+ wd.watch_file("/etc/nginx/nginx.conf", interval=30, diff_mode="line",
1285
+ on_change=lambda r: print("Config changed:", r.changes))
1286
+
1287
+ wd.watch_file("/var/log/app.log", interval=5,
1288
+ alert_if=lambda r: any("ERROR" in (c.after or "") for c in r.changes))
1289
+
1290
+ wd.watch_file("/tmp/prices.txt", interval=2, ai_summary=True,
1291
+ on_change=lambda r: print(r.ai_summary))
1292
+ ```
1293
+
1294
+ #### `.watch_api(url, *, ...)`
1295
+
1296
+ Convenience wrapper over `.watch()` pre-configured for JSON API monitoring: sets `diff_mode="json"`, `track_response_time=True`, and `expected_status=200` by default.
1297
+
1298
+ ```python
1299
+ wd.watch_api("https://api.example.com/prices",
1300
+ interval=60,
1301
+ on_change=lambda r: print(f"API changed, responded in {r.response_time_ms:.0f}ms"))
1302
+
1303
+ # Alert when response time exceeds 500ms
1304
+ wd.watch_api("https://api.example.com/health",
1305
+ interval=30,
1306
+ alert_if=lambda r: (r.response_time_ms or 0) > 500)
1307
+ ```
1308
+
1309
+ #### `.watch_cert(host, *, port=443, warning_days=30, ...)`
1310
+
1311
+ Monitor an SSL/TLS certificate for expiry and fingerprint changes.
1312
+
1313
+ | Parameter | Type | Default | Description |
1314
+ |---|---|---|---|
1315
+ | `hostname` | `str` | — | Hostname to connect to |
1316
+ | `port` | `int` | `443` | TLS port |
1317
+ | `interval` | `int` | `86400` | Seconds between checks |
1318
+ | `label` | `str \| None` | `hostname:port` | Human-readable name |
1319
+ | `warn_days_before_expiry` | `int` | `30` | Days before expiry to start firing `on_expiry` |
1320
+ | `alert_on_change` | `bool` | `True` | Fire `on_change` when cert fingerprint changes |
1321
+ | `alert_on_expiry` | `bool` | `True` | Fire `on_expiry` when cert is nearing expiry |
1322
+ | `on_expiry` | `Callable \| None` | `None` | Called with `CertExpiryInfo` when expiry is approaching |
1323
+ | `on_change` | `Callable \| None` | `None` | Called with `CertChangeInfo` when fingerprint changes |
1324
+ | `on_error` | `Callable \| None` | `None` | Called with `(exc, config)` on fetch error |
1325
+
1326
+ ```python
1327
+ wd.watch_cert("api.example.com",
1328
+ warn_days_before_expiry=14,
1329
+ on_expiry=lambda i: print(f"Cert expires in {i.days_until_expiry} days"),
1330
+ on_change=lambda i: print("Cert fingerprint changed!", i.current_fingerprint))
1331
+
1332
+ statuses = wd.get_cert_statuses() # list[CertWatcherStatus]
1333
+ for s in statuses:
1334
+ print(s.hostname, s.last_check_at, s.days_until_expiry, s.is_expiring_soon)
1335
+ ```
1336
+
1154
1337
  #### `.on_change(callback)`
1155
1338
 
1156
1339
  Register a global callback called whenever any watched URL changes:
@@ -1001,6 +1001,125 @@ watchdiff status
1001
1001
  watchdiff status --config watchdiff.config.json
1002
1002
  ```
1003
1003
 
1004
+ ## AI summaries
1005
+
1006
+ Add a natural-language summary to every change alert. WatchDiff sends the diff to your AI provider of choice and attaches the result to `DiffReport.ai_summary`.
1007
+
1008
+ ### Auto-discovery (recommended)
1009
+
1010
+ Set one of the following environment variables - WatchDiff picks it up automatically:
1011
+
1012
+ ```bash
1013
+ export ANTHROPIC_API_KEY="sk-ant-..."
1014
+ export OPENAI_API_KEY="sk-..."
1015
+ export GEMINI_API_KEY="AIza..."
1016
+ ```
1017
+
1018
+ ```python
1019
+ wd.watch("https://example.com/prices", target=".price",
1020
+ ai_summary=True, # provider auto-detected from env
1021
+ on_change=lambda r: print(r.ai_summary))
1022
+ ```
1023
+
1024
+ Priority order when multiple keys are set: `ANTHROPIC_API_KEY` → `OPENAI_API_KEY` → `GEMINI_API_KEY` / `GOOGLE_AI_API_KEY`.
1025
+
1026
+ ### Explicit provider
1027
+
1028
+ ```python
1029
+ from watchdiff import AiProvider
1030
+
1031
+ wd.watch("https://example.com", ai_summary=True,
1032
+ ai_provider=AiProvider(type="gemini", api_key="...", model="gemini-3.1-flash-lite"))
1033
+ wd.watch("https://example.com", ai_summary=True,
1034
+ ai_provider=AiProvider(type="anthropic", api_key="...", model="claude-haiku-4-5-20251001"))
1035
+ wd.watch("https://example.com", ai_summary=True,
1036
+ ai_provider=AiProvider(type="openai", api_key="...", model="gpt-4o-mini"))
1037
+ ```
1038
+
1039
+ ### OpenAI-compatible providers (Ollama, Mistral, Groq, ...)
1040
+
1041
+ ```python
1042
+ # Local Ollama
1043
+ AiProvider(type="openai", base_url="http://localhost:11434/v1", model="llama3")
1044
+
1045
+ # Mistral
1046
+ AiProvider(type="openai", api_key="...", base_url="https://api.mistral.ai/v1", model="mistral-small")
1047
+
1048
+ # Groq
1049
+ AiProvider(type="openai", api_key="...", base_url="https://api.groq.com/openai/v1", model="llama3-8b-8192")
1050
+ ```
1051
+
1052
+ ### Custom function (any provider, any library)
1053
+
1054
+ ```python
1055
+ import anthropic
1056
+
1057
+ client = anthropic.Anthropic()
1058
+
1059
+ wd.watch("https://example.com", ai_summary=True,
1060
+ ai_provider=AiProvider(
1061
+ type="custom",
1062
+ call_ai=lambda prompt: client.messages.create(
1063
+ model="claude-opus-4-7",
1064
+ max_tokens=200,
1065
+ messages=[{"role": "user", "content": prompt}],
1066
+ ).content[0].text,
1067
+ ))
1068
+ ```
1069
+
1070
+ ### Custom prompt
1071
+
1072
+ Override the default summary template with `ai_prompt` - pass a static string or a callable that receives the `DiffReport` and returns a prompt string.
1073
+
1074
+ ```python
1075
+ # Static prompt - replaces the full instruction sent to the AI
1076
+ wd.watch_file("/tmp/prices.txt", ai_summary=True,
1077
+ ai_prompt="Summarize this change in French in one sentence.")
1078
+
1079
+ # Dynamic prompt - build the prompt from the diff report
1080
+ wd.watch("https://example.com/stock", ai_summary=True,
1081
+ ai_prompt=lambda r: (
1082
+ f"You are monitoring {r.url}. "
1083
+ f"Explain in plain English what changed: "
1084
+ + ", ".join(c.after or c.before or "" for c in r.changes)
1085
+ ))
1086
+ ```
1087
+
1088
+ The prompt is sent as-is to the provider - no variable substitution happens automatically. Use the callable form to interpolate any data from the `DiffReport`.
1089
+
1090
+ ### Built-in providers
1091
+
1092
+ | Provider | `type` | Default model |
1093
+ |---|---|---|
1094
+ | Google Gemini | `"gemini"` | `gemini-3.1-flash-lite` |
1095
+ | Anthropic Claude | `"anthropic"` | `claude-haiku-4-5-20251001` |
1096
+ | OpenAI / compatible | `"openai"` | `gpt-4o-mini` |
1097
+ | Any custom function | `"custom"` | - |
1098
+
1099
+ ### Error handling
1100
+
1101
+ AI failures are non-fatal - the watcher keeps running and fires its normal alerts.
1102
+
1103
+ | Error kind | What triggers it | Behaviour |
1104
+ |---|---|---|
1105
+ | `invalid_key` | 401 / 403 | AI disabled for this watcher for the rest of the session |
1106
+ | `quota_exceeded` | 429 rate limit | Skips AI this check, retries next interval |
1107
+ | `model_error` | 400 / unknown model | AI disabled - check your `model` value |
1108
+ | `network_error` | DNS / timeout | Skips this check, retries next interval |
1109
+
1110
+ ```python
1111
+ from watchdiff import AiError
1112
+
1113
+ try:
1114
+ from watchdiff.ai_summarizer import generate_ai_summary
1115
+ summary = generate_ai_summary(report, provider)
1116
+ except AiError as e:
1117
+ print(e.kind) # AiErrorKind.QUOTA_EXCEEDED
1118
+ print(e.is_retryable) # True - retry next check
1119
+ print(e.is_permanent) # False - don't disable
1120
+ print(e.status_code) # 429
1121
+ ```
1122
+
1004
1123
  ## API reference
1005
1124
 
1006
1125
  ### `WatchDiff`
@@ -1055,6 +1174,12 @@ Register a URL to monitor. All keyword arguments are optional. Returns `self` (c
1055
1174
  | `on_spike` | `Callable \| None` | `None` | Called with `SpikeInfo` when spike is detected |
1056
1175
  | `alert_on_status_change` | `bool` | `False` | Alert when HTTP status code changes (200→503, etc.) |
1057
1176
  | `on_status_change` | `Callable \| None` | `None` | Called with `StatusChangeInfo` on status code change |
1177
+ | `alert_if` | `Callable[[DiffReport], bool] \| None` | `None` | Custom gate - only alert when this function returns `True` |
1178
+ | `expected_status` | `int \| None` | `None` | Fire `on_error` when actual HTTP status differs from this value |
1179
+ | `track_response_time` | `bool` | `False` | Record response time in milliseconds in `DiffReport.response_time_ms` |
1180
+ | `ai_summary` | `bool` | `False` | Generate an AI natural-language summary of detected changes |
1181
+ | `ai_provider` | `AiProvider \| None` | `None` | AI provider to use. Auto-detected from env vars when `None` |
1182
+ | `ai_prompt` | `str \| Callable[[DiffReport], str] \| None` | `None` | Custom prompt sent to the AI instead of the default template |
1058
1183
 
1059
1184
  ```python
1060
1185
  # Chainable
@@ -1103,6 +1228,64 @@ wd.watch_db("sqlite:///app.db", "orders",
1103
1228
  wd.start()
1104
1229
  ```
1105
1230
 
1231
+ #### `.watch_file(path, *, ...)`
1232
+
1233
+ Watch a local file for changes. Accepts all the same options as `.watch()` (except browser-related ones). The path is converted to a `file://` URL internally. Content is compared as raw text, bypassing the HTML cleaner.
1234
+
1235
+ ```python
1236
+ wd.watch_file("/etc/nginx/nginx.conf", interval=30, diff_mode="line",
1237
+ on_change=lambda r: print("Config changed:", r.changes))
1238
+
1239
+ wd.watch_file("/var/log/app.log", interval=5,
1240
+ alert_if=lambda r: any("ERROR" in (c.after or "") for c in r.changes))
1241
+
1242
+ wd.watch_file("/tmp/prices.txt", interval=2, ai_summary=True,
1243
+ on_change=lambda r: print(r.ai_summary))
1244
+ ```
1245
+
1246
+ #### `.watch_api(url, *, ...)`
1247
+
1248
+ Convenience wrapper over `.watch()` pre-configured for JSON API monitoring: sets `diff_mode="json"`, `track_response_time=True`, and `expected_status=200` by default.
1249
+
1250
+ ```python
1251
+ wd.watch_api("https://api.example.com/prices",
1252
+ interval=60,
1253
+ on_change=lambda r: print(f"API changed, responded in {r.response_time_ms:.0f}ms"))
1254
+
1255
+ # Alert when response time exceeds 500ms
1256
+ wd.watch_api("https://api.example.com/health",
1257
+ interval=30,
1258
+ alert_if=lambda r: (r.response_time_ms or 0) > 500)
1259
+ ```
1260
+
1261
+ #### `.watch_cert(host, *, port=443, warning_days=30, ...)`
1262
+
1263
+ Monitor an SSL/TLS certificate for expiry and fingerprint changes.
1264
+
1265
+ | Parameter | Type | Default | Description |
1266
+ |---|---|---|---|
1267
+ | `hostname` | `str` | — | Hostname to connect to |
1268
+ | `port` | `int` | `443` | TLS port |
1269
+ | `interval` | `int` | `86400` | Seconds between checks |
1270
+ | `label` | `str \| None` | `hostname:port` | Human-readable name |
1271
+ | `warn_days_before_expiry` | `int` | `30` | Days before expiry to start firing `on_expiry` |
1272
+ | `alert_on_change` | `bool` | `True` | Fire `on_change` when cert fingerprint changes |
1273
+ | `alert_on_expiry` | `bool` | `True` | Fire `on_expiry` when cert is nearing expiry |
1274
+ | `on_expiry` | `Callable \| None` | `None` | Called with `CertExpiryInfo` when expiry is approaching |
1275
+ | `on_change` | `Callable \| None` | `None` | Called with `CertChangeInfo` when fingerprint changes |
1276
+ | `on_error` | `Callable \| None` | `None` | Called with `(exc, config)` on fetch error |
1277
+
1278
+ ```python
1279
+ wd.watch_cert("api.example.com",
1280
+ warn_days_before_expiry=14,
1281
+ on_expiry=lambda i: print(f"Cert expires in {i.days_until_expiry} days"),
1282
+ on_change=lambda i: print("Cert fingerprint changed!", i.current_fingerprint))
1283
+
1284
+ statuses = wd.get_cert_statuses() # list[CertWatcherStatus]
1285
+ for s in statuses:
1286
+ print(s.hostname, s.last_check_at, s.days_until_expiry, s.is_expiring_soon)
1287
+ ```
1288
+
1106
1289
  #### `.on_change(callback)`
1107
1290
 
1108
1291
  Register a global callback called whenever any watched URL changes:
@@ -4,8 +4,8 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "watchdiff-core"
7
- version = "0.2.0"
8
- description = "Lightweight web change monitoring library - clean diffs, structured alerts, no AI required."
7
+ version = "0.2.1"
8
+ description = "Lightweight web change monitoring library - clean diffs, AI summaries, file/API/SSL/DB monitoring, structured alerts."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
11
11
  license = { text = "BSD-2-Clause" }
@@ -1,3 +1,22 @@
1
+ from watchdiff.ai_summarizer import (
2
+ AiError,
3
+ AiErrorKind,
4
+ AiProvider,
5
+ ai_summary_enabled,
6
+ call_provider,
7
+ generate_ai_summary,
8
+ get_provider,
9
+ resolve_provider,
10
+ )
11
+ from watchdiff.cert_models import (
12
+ CertChangeInfo,
13
+ CertExpiryInfo,
14
+ CertReport,
15
+ CertSnapshot,
16
+ CertWatchConfig,
17
+ CertWatcherStatus,
18
+ make_cert_watch_config,
19
+ )
1
20
  from watchdiff.core import WatchDiff
2
21
  from watchdiff.db_models import (
3
22
  DbChange,
@@ -16,12 +35,22 @@ from watchdiff.db_models import (
16
35
  make_db_watch_config,
17
36
  )
18
37
  from watchdiff.exporter import Exporter
38
+ from watchdiff.file_fetcher import FileFetcher, file_path_from_url
19
39
  from watchdiff.models import BrowserOptions, DiffMode, SpikeInfo, StatusChangeInfo, WatchConfig
20
40
  from watchdiff.status_server import StatusServer
21
41
  from watchdiff.store import SqliteStore, Store
22
42
 
23
43
  __all__ = [
44
+ "AiError",
45
+ "AiErrorKind",
46
+ "AiProvider",
24
47
  "BrowserOptions",
48
+ "CertChangeInfo",
49
+ "CertExpiryInfo",
50
+ "CertReport",
51
+ "CertSnapshot",
52
+ "CertWatchConfig",
53
+ "CertWatcherStatus",
25
54
  "DbChange",
26
55
  "DbChangeKind",
27
56
  "DbDiffMode",
@@ -32,6 +61,7 @@ __all__ = [
32
61
  "DbWatcherStatus",
33
62
  "DiffMode",
34
63
  "Exporter",
64
+ "FileFetcher",
35
65
  "SchemaChangeInfo",
36
66
  "SqliteStore",
37
67
  "SpikeInfo",
@@ -41,8 +71,15 @@ __all__ = [
41
71
  "ThresholdInfo",
42
72
  "WatchConfig",
43
73
  "WatchDiff",
74
+ "ai_summary_enabled",
75
+ "call_provider",
44
76
  "db_has_changes",
45
77
  "db_report_summary",
46
78
  "db_snapshot_key",
79
+ "file_path_from_url",
80
+ "generate_ai_summary",
81
+ "get_provider",
82
+ "make_cert_watch_config",
47
83
  "make_db_watch_config",
84
+ "resolve_provider",
48
85
  ]