watchdiff-core 0.1.2__py3-none-any.whl
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/__init__.py +3 -0
- watchdiff/cleaner/__init__.py +3 -0
- watchdiff/cleaner/cleaner.py +131 -0
- watchdiff/cli/__init__.py +0 -0
- watchdiff/cli/main.py +217 -0
- watchdiff/core.py +202 -0
- watchdiff/diff/__init__.py +3 -0
- watchdiff/diff/engine.py +131 -0
- watchdiff/fetcher/__init__.py +3 -0
- watchdiff/fetcher/fetcher.py +84 -0
- watchdiff/models.py +163 -0
- watchdiff/notifier/__init__.py +3 -0
- watchdiff/notifier/notifier.py +81 -0
- watchdiff/parser/__init__.py +3 -0
- watchdiff/parser/parser.py +75 -0
- watchdiff/scheduler/__init__.py +3 -0
- watchdiff/scheduler/scheduler.py +223 -0
- watchdiff/store/__init__.py +3 -0
- watchdiff/store/store.py +144 -0
- watchdiff_core-0.1.2.dist-info/METADATA +291 -0
- watchdiff_core-0.1.2.dist-info/RECORD +24 -0
- watchdiff_core-0.1.2.dist-info/WHEEL +4 -0
- watchdiff_core-0.1.2.dist-info/entry_points.txt +2 -0
- watchdiff_core-0.1.2.dist-info/licenses/LICENSE +674 -0
watchdiff/store/store.py
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Store - persists snapshots and diff reports to disk.
|
|
3
|
+
|
|
4
|
+
Format: one JSON file per watched URL, stored in a configurable directory.
|
|
5
|
+
Each file contains an ordered list of Snapshot dicts (newest last).
|
|
6
|
+
|
|
7
|
+
Why JSON and not SQLite?
|
|
8
|
+
→ Zero external dependencies for storage.
|
|
9
|
+
→ Easy to inspect, back up, or pipe to other tools.
|
|
10
|
+
→ Fine for typical monitoring workloads (< thousands of entries).
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import hashlib
|
|
16
|
+
import json
|
|
17
|
+
from datetime import datetime
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
from watchdiff.models import DiffReport, Snapshot
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class Store:
|
|
24
|
+
"""Filesystem-based snapshot store."""
|
|
25
|
+
|
|
26
|
+
def __init__(self, directory: str | Path = ".watchdiff") -> None:
|
|
27
|
+
self.directory = Path(directory)
|
|
28
|
+
self.directory.mkdir(parents=True, exist_ok=True)
|
|
29
|
+
|
|
30
|
+
# ------------------------------------------------------------------
|
|
31
|
+
# Snapshots
|
|
32
|
+
# ------------------------------------------------------------------
|
|
33
|
+
|
|
34
|
+
def save_snapshot(self, snapshot: Snapshot) -> None:
|
|
35
|
+
"""Append a snapshot to the history for its URL + target combo."""
|
|
36
|
+
path = self._snapshot_path(snapshot.url, snapshot.target)
|
|
37
|
+
history = self._load_raw(path)
|
|
38
|
+
history.append(_snapshot_to_dict(snapshot))
|
|
39
|
+
self._save_raw(path, history)
|
|
40
|
+
|
|
41
|
+
def load_latest(self, url: str, target: str | None) -> Snapshot | None:
|
|
42
|
+
"""Return the most recent snapshot, or None if no history exists."""
|
|
43
|
+
path = self._snapshot_path(url, target)
|
|
44
|
+
if not path.exists():
|
|
45
|
+
return None
|
|
46
|
+
history = self._load_raw(path)
|
|
47
|
+
if not history:
|
|
48
|
+
return None
|
|
49
|
+
return _dict_to_snapshot(history[-1])
|
|
50
|
+
|
|
51
|
+
def load_history(
|
|
52
|
+
self,
|
|
53
|
+
url: str,
|
|
54
|
+
target: str | None,
|
|
55
|
+
limit: int = 50,
|
|
56
|
+
) -> list[Snapshot]:
|
|
57
|
+
"""Return up to `limit` most recent snapshots (newest last)."""
|
|
58
|
+
path = self._snapshot_path(url, target)
|
|
59
|
+
if not path.exists():
|
|
60
|
+
return []
|
|
61
|
+
history = self._load_raw(path)
|
|
62
|
+
return [_dict_to_snapshot(d) for d in history[-limit:]]
|
|
63
|
+
|
|
64
|
+
def clear_history(self, url: str, target: str | None) -> None:
|
|
65
|
+
"""Delete all stored snapshots for a URL + target combo."""
|
|
66
|
+
path = self._snapshot_path(url, target)
|
|
67
|
+
if path.exists():
|
|
68
|
+
path.unlink()
|
|
69
|
+
|
|
70
|
+
# ------------------------------------------------------------------
|
|
71
|
+
# Diff reports
|
|
72
|
+
# ------------------------------------------------------------------
|
|
73
|
+
|
|
74
|
+
def save_report(self, report: DiffReport) -> None:
|
|
75
|
+
"""Persist a DiffReport for audit purposes."""
|
|
76
|
+
path = self._report_path(report.url, report.target)
|
|
77
|
+
reports = self._load_raw(path)
|
|
78
|
+
reports.append(report.as_dict())
|
|
79
|
+
self._save_raw(path, reports)
|
|
80
|
+
|
|
81
|
+
def load_reports(
|
|
82
|
+
self,
|
|
83
|
+
url: str,
|
|
84
|
+
target: str | None,
|
|
85
|
+
limit: int = 50,
|
|
86
|
+
) -> list[dict]:
|
|
87
|
+
"""Return raw report dicts (newest last)."""
|
|
88
|
+
path = self._report_path(url, target)
|
|
89
|
+
if not path.exists():
|
|
90
|
+
return []
|
|
91
|
+
return self._load_raw(path)[-limit:]
|
|
92
|
+
|
|
93
|
+
# ------------------------------------------------------------------
|
|
94
|
+
# Internal
|
|
95
|
+
# ------------------------------------------------------------------
|
|
96
|
+
|
|
97
|
+
def _key(self, url: str, target: str | None) -> str:
|
|
98
|
+
"""Stable short key derived from URL + target."""
|
|
99
|
+
raw = f"{url}::{target or ''}"
|
|
100
|
+
return hashlib.md5(raw.encode()).hexdigest()[:12]
|
|
101
|
+
|
|
102
|
+
def _snapshot_path(self, url: str, target: str | None) -> Path:
|
|
103
|
+
return self.directory / f"snap_{self._key(url, target)}.json"
|
|
104
|
+
|
|
105
|
+
def _report_path(self, url: str, target: str | None) -> Path:
|
|
106
|
+
return self.directory / f"report_{self._key(url, target)}.json"
|
|
107
|
+
|
|
108
|
+
def _load_raw(self, path: Path) -> list:
|
|
109
|
+
if not path.exists():
|
|
110
|
+
return []
|
|
111
|
+
try:
|
|
112
|
+
return json.loads(path.read_text(encoding="utf-8"))
|
|
113
|
+
except (json.JSONDecodeError, OSError):
|
|
114
|
+
return []
|
|
115
|
+
|
|
116
|
+
def _save_raw(self, path: Path, data: list) -> None:
|
|
117
|
+
path.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
# ---------------------------------------------------------------------------
|
|
121
|
+
# Serialisation helpers
|
|
122
|
+
# ---------------------------------------------------------------------------
|
|
123
|
+
|
|
124
|
+
def _snapshot_to_dict(s: Snapshot) -> dict:
|
|
125
|
+
return {
|
|
126
|
+
"url": s.url,
|
|
127
|
+
"target": s.target,
|
|
128
|
+
"content": s.content,
|
|
129
|
+
"raw_html": s.raw_html,
|
|
130
|
+
"captured_at": s.captured_at.isoformat(),
|
|
131
|
+
"checksum": s.checksum,
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _dict_to_snapshot(d: dict) -> Snapshot:
|
|
136
|
+
snap = Snapshot(
|
|
137
|
+
url = d["url"],
|
|
138
|
+
target = d.get("target"),
|
|
139
|
+
content = d["content"],
|
|
140
|
+
raw_html = d.get("raw_html", ""),
|
|
141
|
+
)
|
|
142
|
+
snap.captured_at = datetime.fromisoformat(d["captured_at"])
|
|
143
|
+
snap.checksum = d["checksum"]
|
|
144
|
+
return snap
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: watchdiff-core
|
|
3
|
+
Version: 0.1.2
|
|
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.14
|
|
14
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
15
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
16
|
+
Requires-Python: >=3.14
|
|
17
|
+
Requires-Dist: beautifulsoup4>=4.14.3
|
|
18
|
+
Requires-Dist: httpx>=0.28.1
|
|
19
|
+
Requires-Dist: lxml>=6.1.0
|
|
20
|
+
Requires-Dist: rich>=15.0.0
|
|
21
|
+
Requires-Dist: typer>=0.25.1
|
|
22
|
+
Provides-Extra: dev
|
|
23
|
+
Requires-Dist: pytest-asyncio>=1.3.0; extra == 'dev'
|
|
24
|
+
Requires-Dist: pytest-httpx>=0.36.2; extra == 'dev'
|
|
25
|
+
Requires-Dist: pytest>=9.0.3; extra == 'dev'
|
|
26
|
+
Requires-Dist: ruff>=0.15.12; extra == 'dev'
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
|
|
29
|
+
# WatchDiff
|
|
30
|
+
|
|
31
|
+
**Lightweight web change monitoring - clean diffs, structured alerts, no AI required.**
|
|
32
|
+
|
|
33
|
+
WatchDiff watches web pages and tells you **exactly what changed**, in plain language.
|
|
34
|
+
No noisy HTML diffs. No external services. No AI black boxes.
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
## Why WatchDiff?
|
|
38
|
+
|
|
39
|
+
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.
|
|
40
|
+
|
|
41
|
+
- **Deterministic** - same input always produces the same output
|
|
42
|
+
- **Human-readable diffs** - "Price changed: $19 → $24", not a wall of HTML
|
|
43
|
+
- **Zero external services** - snapshots stored locally as JSON
|
|
44
|
+
- **Async-ready** - sync and async schedulers included
|
|
45
|
+
- **Configurable** - target any CSS selector, ignore patterns, set webhooks
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
## Install
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
pip install watchdiff-core
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Or with [uv](https://github.com/astral-sh/uv):
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
uv add watchdiff-core
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
## Quick start
|
|
62
|
+
|
|
63
|
+
### Python API
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
from watchdiff import WatchDiff
|
|
67
|
+
|
|
68
|
+
wd = WatchDiff()
|
|
69
|
+
|
|
70
|
+
wd.watch(
|
|
71
|
+
"https://books.toscrape.com/catalogue/a-light-in-the-attic_1000/index.html",
|
|
72
|
+
target=".price_color",
|
|
73
|
+
interval=60,
|
|
74
|
+
label="Book price",
|
|
75
|
+
on_change=lambda r: print(r.summary()),
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
wd.start()
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### CLI
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
# One-shot check
|
|
85
|
+
watchdiff check https://example.com --target .price
|
|
86
|
+
|
|
87
|
+
# Continuous monitoring (Ctrl+C to stop)
|
|
88
|
+
watchdiff run https://example.com --target .price --interval 60
|
|
89
|
+
|
|
90
|
+
# Snapshot history
|
|
91
|
+
watchdiff history https://example.com
|
|
92
|
+
|
|
93
|
+
# Diff reports
|
|
94
|
+
watchdiff reports https://example.com
|
|
95
|
+
|
|
96
|
+
# Clear stored data
|
|
97
|
+
watchdiff clear https://example.com
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
## How it works
|
|
102
|
+
|
|
103
|
+
Every check runs through a fixed pipeline:
|
|
104
|
+
|
|
105
|
+
```
|
|
106
|
+
Fetcher → Cleaner → Parser → DiffEngine → Store → Notifier
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
1. **Fetcher** - downloads the page via `httpx` (sync or async)
|
|
110
|
+
2. **Cleaner** - strips scripts, styles, ads, and tracking noise
|
|
111
|
+
3. **Parser** - extracts the target CSS selector (or full body)
|
|
112
|
+
4. **DiffEngine** - compares content using Python's `difflib.SequenceMatcher`
|
|
113
|
+
5. **Store** - persists snapshots and reports as local JSON files
|
|
114
|
+
6. **Notifier** - fires callbacks and/or webhooks on detected changes
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
## API reference
|
|
118
|
+
|
|
119
|
+
### `WatchDiff`
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
wd = WatchDiff(storage_dir=".watchdiff") # default storage directory
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
#### `.watch(url, *, ...)`
|
|
126
|
+
|
|
127
|
+
Register a URL to monitor. All keyword arguments are optional.
|
|
128
|
+
|
|
129
|
+
| Parameter | Type | Default | Description |
|
|
130
|
+
|---|---|---|---|
|
|
131
|
+
| `url` | `str` | - | URL to watch |
|
|
132
|
+
| `target` | `str \| None` | `None` | CSS selector (e.g. `.price`). `None` = full page |
|
|
133
|
+
| `interval` | `int` | `300` | Seconds between checks |
|
|
134
|
+
| `label` | `str \| None` | URL | Human-readable name shown in logs |
|
|
135
|
+
| `headers` | `dict` | `{}` | Extra HTTP headers |
|
|
136
|
+
| `timeout` | `int` | `15` | Request timeout in seconds |
|
|
137
|
+
| `ignore_selectors` | `list[str]` | `[]` | CSS selectors to strip before diffing |
|
|
138
|
+
| `ignore_patterns` | `list[str]` | `[]` | Regex patterns to strip from text |
|
|
139
|
+
| `on_change` | `Callable \| list` | `None` | Callback(s) fired on each change |
|
|
140
|
+
| `webhooks` | `list[str]` | `[]` | Webhook URLs to POST on change |
|
|
141
|
+
| `min_changes` | `int` | `1` | Minimum number of changes to trigger alert |
|
|
142
|
+
|
|
143
|
+
All methods are **chainable**:
|
|
144
|
+
|
|
145
|
+
```python
|
|
146
|
+
wd.watch("https://site.com/product", target=".price", interval=300) \
|
|
147
|
+
.watch("https://site.com/stock", target=".availability") \
|
|
148
|
+
.on_change(lambda r: print(r.summary())) \
|
|
149
|
+
.start()
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
#### `.on_change(callback)`
|
|
153
|
+
|
|
154
|
+
Register a global callback called whenever **any** watched URL changes.
|
|
155
|
+
|
|
156
|
+
```python
|
|
157
|
+
def handle(report):
|
|
158
|
+
print(report.summary())
|
|
159
|
+
for change in report.changes:
|
|
160
|
+
print(change.human())
|
|
161
|
+
|
|
162
|
+
wd.on_change(handle)
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
#### `.start(block=True)`
|
|
166
|
+
|
|
167
|
+
Start the synchronous scheduler. Blocks until `Ctrl+C` by default.
|
|
168
|
+
Pass `block=False` to run in the background (daemon threads).
|
|
169
|
+
|
|
170
|
+
#### `await .start_async()`
|
|
171
|
+
|
|
172
|
+
Async variant - use inside an existing event loop (FastAPI, aiohttp, etc.).
|
|
173
|
+
|
|
174
|
+
```python
|
|
175
|
+
import asyncio
|
|
176
|
+
asyncio.run(wd.start_async())
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
#### `.check_once(url)`
|
|
180
|
+
|
|
181
|
+
Run a single immediate check without starting the scheduler loop.
|
|
182
|
+
|
|
183
|
+
```python
|
|
184
|
+
report = wd.check_once("https://example.com")
|
|
185
|
+
if report:
|
|
186
|
+
print(report.summary())
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
---
|
|
190
|
+
|
|
191
|
+
### `DiffReport`
|
|
192
|
+
|
|
193
|
+
```python
|
|
194
|
+
report.url # str
|
|
195
|
+
report.target # str | None
|
|
196
|
+
report.label # str
|
|
197
|
+
report.has_changes # bool
|
|
198
|
+
report.added # list[Change]
|
|
199
|
+
report.removed # list[Change]
|
|
200
|
+
report.modified # list[Change]
|
|
201
|
+
report.changes # list[Change] (all changes)
|
|
202
|
+
report.compared_at # datetime
|
|
203
|
+
|
|
204
|
+
report.summary() # "[Book price] 1 modified - 2024-01-15 10:30:00 UTC"
|
|
205
|
+
report.as_dict() # JSON-serialisable dict
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
### `Change`
|
|
209
|
+
|
|
210
|
+
```python
|
|
211
|
+
change.kind # ChangeType.ADDED | REMOVED | MODIFIED | UNCHANGED
|
|
212
|
+
change.before # str | None - previous value
|
|
213
|
+
change.after # str | None - new value
|
|
214
|
+
change.context # str | None - surrounding text hint
|
|
215
|
+
|
|
216
|
+
change.human() # "[~] Changed: '$19.00' → '$24.00'"
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
## Webhooks
|
|
220
|
+
|
|
221
|
+
WatchDiff auto-detects the target service and adapts the payload format:
|
|
222
|
+
|
|
223
|
+
| Service | Detection | Payload |
|
|
224
|
+
|---|---|---|
|
|
225
|
+
| Discord | `discord.com` in URL | `{"content": "..."}` (2000-char limit) |
|
|
226
|
+
| Slack | `hooks.slack.com` in URL | `{"text": "..."}` |
|
|
227
|
+
| Custom | anything else | full `report.as_dict()` |
|
|
228
|
+
|
|
229
|
+
```python
|
|
230
|
+
wd.watch(
|
|
231
|
+
"https://example.com",
|
|
232
|
+
webhooks=[
|
|
233
|
+
"https://discord.com/api/webhooks/YOUR_ID/YOUR_TOKEN",
|
|
234
|
+
"https://hooks.slack.com/services/YOUR/SLACK/WEBHOOK",
|
|
235
|
+
"https://your-api.com/watchdiff-hook",
|
|
236
|
+
],
|
|
237
|
+
)
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
## Async usage
|
|
241
|
+
|
|
242
|
+
```python
|
|
243
|
+
import asyncio
|
|
244
|
+
from watchdiff import WatchDiff
|
|
245
|
+
|
|
246
|
+
async def main():
|
|
247
|
+
wd = WatchDiff()
|
|
248
|
+
wd.watch("https://example.com", target="h1", interval=30)
|
|
249
|
+
wd.on_change(lambda r: print(r.summary()))
|
|
250
|
+
await wd.start_async()
|
|
251
|
+
|
|
252
|
+
asyncio.run(main())
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
## CLI reference
|
|
256
|
+
|
|
257
|
+
```
|
|
258
|
+
Usage: watchdiff [COMMAND] [OPTIONS]
|
|
259
|
+
|
|
260
|
+
Commands:
|
|
261
|
+
run Start continuous monitoring
|
|
262
|
+
check Run a single check and print the result
|
|
263
|
+
history Show snapshot history for a URL
|
|
264
|
+
reports Show diff reports for a URL
|
|
265
|
+
clear Delete all stored data for a URL
|
|
266
|
+
|
|
267
|
+
Options (shared):
|
|
268
|
+
--target -t CSS selector to watch
|
|
269
|
+
--storage -s Storage directory (default: .watchdiff)
|
|
270
|
+
--interval -i Seconds between checks (run only)
|
|
271
|
+
--limit -n Number of entries to show (history/reports)
|
|
272
|
+
--verbose -v Enable debug logging
|
|
273
|
+
--json Output raw JSON (check only)
|
|
274
|
+
--yes -y Skip confirmation (clear only)
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
## Use cases
|
|
278
|
+
|
|
279
|
+
- **E-commerce** - track product prices and stock availability
|
|
280
|
+
- **News monitoring** - detect article updates or new publications
|
|
281
|
+
- **Documentation** - alert when API docs change
|
|
282
|
+
- **Public APIs** - watch JSON endpoints for schema or value changes
|
|
283
|
+
- **Compliance** - audit changes on public-facing pages over time
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
## License
|
|
287
|
+
|
|
288
|
+
This project is licensed under the [GNU General Public License v3.0](LICENSE).
|
|
289
|
+
|
|
290
|
+
You are free to use, study, modify, and distribute this software under the terms of the GPL v3.
|
|
291
|
+
Any derivative work must also be distributed under the same license.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
watchdiff/__init__.py,sha256=HlBhsOpCcnYJ-f9m0zLg9gAa4TRfCwlVpq2STxSmFU8,62
|
|
2
|
+
watchdiff/core.py,sha256=UMqorOkCIxr7Vwu-k1_1E9M5F3N0x5PpWmlYXOgagXU,7544
|
|
3
|
+
watchdiff/models.py,sha256=Wata-ijRhZprdqnivPtQCBjrRE565VHd7wg6MkHAlY4,5446
|
|
4
|
+
watchdiff/cleaner/__init__.py,sha256=00jw16wtAFiYqza2ntmP0exIbut2kG7EfZTR-omSaVM,69
|
|
5
|
+
watchdiff/cleaner/cleaner.py,sha256=FYwyWb3qRH6skhQG22yjE2hO85ZVEBVQYNZtkT-Kn4Y,4010
|
|
6
|
+
watchdiff/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
|
+
watchdiff/cli/main.py,sha256=DSlbeSUevjYhMWkHwpX9rjrTnIXj_tELDf2GWBFKzKo,6960
|
|
8
|
+
watchdiff/diff/__init__.py,sha256=zP2rL-zhlQy117isgd4JHkpKXGqBJl3DR3WAkaW_WvY,71
|
|
9
|
+
watchdiff/diff/engine.py,sha256=AIvqusPjwvSLIb74dZZKiF2D0QM7o2aZzLILYQK-Hng,4539
|
|
10
|
+
watchdiff/fetcher/__init__.py,sha256=q1i_WZbjfnhB5mbOF4DE6DojY-Tzv12gR1oBqHESb_s,125
|
|
11
|
+
watchdiff/fetcher/fetcher.py,sha256=z_B4gwxqfrorsVo6K9dd0HDkQE1IudVqVZEHmfq-RQc,2463
|
|
12
|
+
watchdiff/notifier/__init__.py,sha256=1Q4V121FfjLFdk9XFgfWZZ_jz-QNfYCV1_PomJmKIZ4,73
|
|
13
|
+
watchdiff/notifier/notifier.py,sha256=o7vAFAHEdArBg6BH2brMKtbyTSMjlYQb_jQdfbtU6tI,2580
|
|
14
|
+
watchdiff/parser/__init__.py,sha256=mK2TosyMo0nDfRP6UqUNls5QAelbwbVbMUHqsZQvylU,93
|
|
15
|
+
watchdiff/parser/parser.py,sha256=Ij3QyeiXxXQHwySjRaDu1P8li9xcV4jkzYaQo2hnp8A,2300
|
|
16
|
+
watchdiff/scheduler/__init__.py,sha256=hgbIPr9-gZa_VOdjeHe01Ji7kEBUlfR6PUdSd9Gctfk,119
|
|
17
|
+
watchdiff/scheduler/scheduler.py,sha256=8gQd0n72XH4QDCjP7o9y2sBARvU9zcLFfKdetHSvpMQ,8142
|
|
18
|
+
watchdiff/store/__init__.py,sha256=hr7rwyCqdAeACffzWKC3e8mM16QYZa4iWzRU5PdreJs,61
|
|
19
|
+
watchdiff/store/store.py,sha256=JTt6uQPhJ1rgz6_zmZ-Xyvrit4VSRjMi60rMgIqAOwA,4944
|
|
20
|
+
watchdiff_core-0.1.2.dist-info/METADATA,sha256=eLOgkzboib63dnJkDv1XhetYDpJ7FzU22aFG52cXjdY,8197
|
|
21
|
+
watchdiff_core-0.1.2.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
22
|
+
watchdiff_core-0.1.2.dist-info/entry_points.txt,sha256=qlFOiDMDG_QyzMD_-uu1YfpGKA_xHm23pIqGpiYZxWk,53
|
|
23
|
+
watchdiff_core-0.1.2.dist-info/licenses/LICENSE,sha256=ixuiBLtpoK3iv89l7ylKkg9rs2GzF9ukPH7ynZYzK5s,35148
|
|
24
|
+
watchdiff_core-0.1.2.dist-info/RECORD,,
|