diffdelta 0.1.0__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.
@@ -0,0 +1,25 @@
1
+ Metadata-Version: 2.4
2
+ Name: diffdelta
3
+ Version: 0.1.0
4
+ Summary: Python client for DiffDelta — agent-ready intelligence feeds
5
+ Author-email: DiffDelta <human@diffdelta.io>
6
+ License: MIT
7
+ Project-URL: Homepage, https://diffdelta.io
8
+ Project-URL: Documentation, https://diffdelta.io/#quickstart
9
+ Project-URL: Repository, https://github.com/diffdelta/diffdelta-python
10
+ Project-URL: Issues, https://github.com/diffdelta/diffdelta-python/issues
11
+ Keywords: diffdelta,ai-agents,security,changefeed,intelligence
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.8
17
+ Classifier: Programming Language :: Python :: 3.9
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Topic :: Security
23
+ Classifier: Topic :: Software Development :: Libraries
24
+ Requires-Python: >=3.8
25
+ Requires-Dist: requests>=2.25.0
@@ -0,0 +1,17 @@
1
+ """
2
+ DiffDelta — Agent-ready intelligence feeds.
3
+
4
+ from diffdelta import DiffDelta
5
+
6
+ dd = DiffDelta()
7
+ for item in dd.poll(tags=["security"]):
8
+ print(f"{item.source}: {item.headline}")
9
+
10
+ Full docs: https://diffdelta.io/#quickstart
11
+ """
12
+
13
+ from diffdelta.client import DiffDelta
14
+ from diffdelta.models import FeedItem, SourceInfo, Feed, Head
15
+
16
+ __version__ = "0.1.0"
17
+ __all__ = ["DiffDelta", "FeedItem", "SourceInfo", "Feed", "Head"]
@@ -0,0 +1,327 @@
1
+ """DiffDelta client — the main interface for polling intelligence feeds."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import time
6
+ from typing import Any, Callable, Dict, List, Optional, Sequence
7
+
8
+ import requests
9
+
10
+ from diffdelta.cursor import CursorStore
11
+ from diffdelta.models import Feed, FeedItem, Head, SourceInfo
12
+
13
+ DEFAULT_BASE_URL = "https://diffdelta.io"
14
+ DEFAULT_TIMEOUT = 15 # seconds
15
+
16
+
17
+ class DiffDelta:
18
+ """Client for polling DiffDelta intelligence feeds.
19
+
20
+ Usage::
21
+
22
+ from diffdelta import DiffDelta
23
+
24
+ dd = DiffDelta()
25
+
26
+ # Poll for new items across all sources
27
+ for item in dd.poll():
28
+ print(f"{item.source}: {item.headline}")
29
+
30
+ # Poll only security sources
31
+ for item in dd.poll(tags=["security"]):
32
+ print(f"🚨 {item.headline}")
33
+
34
+ # Poll a specific source
35
+ for item in dd.poll_source("cisa_kev"):
36
+ print(item.headline)
37
+
38
+ Args:
39
+ base_url: DiffDelta API base URL. Defaults to https://diffdelta.io.
40
+ api_key: Optional Pro/Enterprise API key (dd_live_...).
41
+ cursor_path: Path to cursor persistence file. Set to None to disable persistence.
42
+ timeout: HTTP request timeout in seconds.
43
+ """
44
+
45
+ def __init__(
46
+ self,
47
+ base_url: str = DEFAULT_BASE_URL,
48
+ api_key: Optional[str] = None,
49
+ cursor_path: Optional[str] = "", # "" = default path, None = disabled
50
+ timeout: int = DEFAULT_TIMEOUT,
51
+ ) -> None:
52
+ self.base_url = base_url.rstrip("/")
53
+ self.api_key = api_key
54
+ self.timeout = timeout
55
+
56
+ # Set up cursor persistence
57
+ if cursor_path is None:
58
+ self._cursors: Optional[CursorStore] = None
59
+ else:
60
+ self._cursors = CursorStore(cursor_path if cursor_path else None)
61
+
62
+ # Set up HTTP session
63
+ self._session = requests.Session()
64
+ self._session.headers["User-Agent"] = "diffdelta-python/0.1.0"
65
+ if self.api_key:
66
+ self._session.headers["X-DiffDelta-Key"] = self.api_key
67
+
68
+ # ── Core polling ──
69
+
70
+ def poll(
71
+ self,
72
+ tags: Optional[List[str]] = None,
73
+ sources: Optional[List[str]] = None,
74
+ buckets: Optional[List[str]] = None,
75
+ ) -> List[FeedItem]:
76
+ """Poll the global feed for new items since last poll.
77
+
78
+ Checks head.json first (400 bytes). Only fetches the full feed
79
+ if the cursor has changed. Automatically saves the new cursor.
80
+
81
+ Args:
82
+ tags: Filter items to these tags (e.g. ["security"]).
83
+ sources: Filter items to these source IDs (e.g. ["cisa_kev", "nist_nvd"]).
84
+ buckets: Which buckets to return. Defaults to ["new", "updated"].
85
+ Use ["new", "updated", "removed"] to include removals.
86
+
87
+ Returns:
88
+ List of FeedItem objects that are new since your last poll.
89
+ Empty list if nothing has changed.
90
+ """
91
+ if buckets is None:
92
+ buckets = ["new", "updated"]
93
+
94
+ cursor_key = "global"
95
+ return self._poll_feed(
96
+ head_url=f"{self.base_url}/diff/head.json",
97
+ latest_url=f"{self.base_url}/diff/latest.json",
98
+ cursor_key=cursor_key,
99
+ tags=tags,
100
+ sources=sources,
101
+ buckets=buckets,
102
+ )
103
+
104
+ def poll_source(
105
+ self,
106
+ source_id: str,
107
+ buckets: Optional[List[str]] = None,
108
+ ) -> List[FeedItem]:
109
+ """Poll a specific source for new items since last poll.
110
+
111
+ More efficient than poll() with sources= filter if you only
112
+ care about one source, since it fetches a smaller payload.
113
+
114
+ Args:
115
+ source_id: Source identifier (e.g. "cisa_kev").
116
+ buckets: Which buckets to return. Defaults to ["new", "updated"].
117
+
118
+ Returns:
119
+ List of FeedItem objects that are new since your last poll.
120
+ """
121
+ if buckets is None:
122
+ buckets = ["new", "updated"]
123
+
124
+ cursor_key = f"source:{source_id}"
125
+ return self._poll_feed(
126
+ head_url=f"{self.base_url}/diff/source/{source_id}/head.json",
127
+ latest_url=f"{self.base_url}/diff/source/{source_id}/latest.json",
128
+ cursor_key=cursor_key,
129
+ tags=None,
130
+ sources=None,
131
+ buckets=buckets,
132
+ )
133
+
134
+ def _poll_feed(
135
+ self,
136
+ head_url: str,
137
+ latest_url: str,
138
+ cursor_key: str,
139
+ tags: Optional[List[str]],
140
+ sources: Optional[List[str]],
141
+ buckets: List[str],
142
+ ) -> List[FeedItem]:
143
+ """Internal: poll a feed with cursor comparison."""
144
+ # Step 1: Fetch head.json (~400 bytes)
145
+ head = self.head(head_url)
146
+
147
+ # Step 2: Compare cursor
148
+ stored_cursor = self._cursors.get(cursor_key) if self._cursors else None
149
+
150
+ if stored_cursor and stored_cursor == head.cursor:
151
+ # Nothing changed — return empty list
152
+ return []
153
+
154
+ # Step 3: Fetch full feed
155
+ feed = self.fetch_feed(latest_url)
156
+
157
+ # Step 4: Save new cursor
158
+ if self._cursors and feed.cursor:
159
+ self._cursors.set(cursor_key, feed.cursor)
160
+
161
+ # Step 5: Filter and return
162
+ items = []
163
+ for item in feed.items:
164
+ # Filter by bucket
165
+ if item.bucket not in buckets:
166
+ continue
167
+
168
+ # Filter by source
169
+ if sources and item.source not in sources:
170
+ continue
171
+
172
+ # Filter by tags (requires knowing source tags — use source index)
173
+ if tags:
174
+ # We need source metadata to filter by tags
175
+ # Load source info if we haven't yet
176
+ source_tags = self._get_source_tags()
177
+ item_tags = source_tags.get(item.source, [])
178
+ if not any(t in item_tags for t in tags):
179
+ continue
180
+
181
+ items.append(item)
182
+
183
+ return items
184
+
185
+ # ── Low-level fetch methods ──
186
+
187
+ def head(self, url: Optional[str] = None) -> Head:
188
+ """Fetch a head.json pointer.
189
+
190
+ Args:
191
+ url: Full URL to head.json. Defaults to global head.
192
+
193
+ Returns:
194
+ Head object with cursor, hash, and metadata.
195
+ """
196
+ if url is None:
197
+ url = f"{self.base_url}/diff/head.json"
198
+ data = self._get_json(url)
199
+ return Head.from_raw(data)
200
+
201
+ def fetch_feed(self, url: Optional[str] = None) -> Feed:
202
+ """Fetch a full latest.json feed.
203
+
204
+ Args:
205
+ url: Full URL to latest.json. Defaults to global latest.
206
+
207
+ Returns:
208
+ Feed object with all items, buckets, and metadata.
209
+ """
210
+ if url is None:
211
+ url = f"{self.base_url}/diff/latest.json"
212
+ data = self._get_json(url)
213
+ return Feed.from_raw(data)
214
+
215
+ def sources(self) -> List[SourceInfo]:
216
+ """List all available DiffDelta sources.
217
+
218
+ Returns:
219
+ List of SourceInfo objects with metadata about each source.
220
+ """
221
+ data = self._get_json(f"{self.base_url}/diff/sources.json")
222
+ return [SourceInfo.from_raw(s) for s in data.get("sources", [])]
223
+
224
+ # ── Continuous monitoring ──
225
+
226
+ def watch(
227
+ self,
228
+ callback: Callable[[FeedItem], None],
229
+ tags: Optional[List[str]] = None,
230
+ sources: Optional[List[str]] = None,
231
+ interval: Optional[int] = None,
232
+ buckets: Optional[List[str]] = None,
233
+ ) -> None:
234
+ """Continuously poll and call a function for each new item.
235
+
236
+ This runs an infinite loop. Use Ctrl+C to stop, or run in a thread.
237
+
238
+ Args:
239
+ callback: Function called for each new FeedItem.
240
+ tags: Filter to these tags.
241
+ sources: Filter to these source IDs.
242
+ interval: Seconds between polls. Defaults to feed's TTL (usually 900s).
243
+ buckets: Which buckets to process. Defaults to ["new", "updated"].
244
+
245
+ Example::
246
+
247
+ def handle(item):
248
+ print(f"🚨 {item.source}: {item.headline}")
249
+ # send_slack_alert(item)
250
+
251
+ dd = DiffDelta()
252
+ dd.watch(handle, tags=["security"]) # Runs forever
253
+ """
254
+ if buckets is None:
255
+ buckets = ["new", "updated"]
256
+
257
+ # Determine polling interval from feed TTL if not specified
258
+ if interval is None:
259
+ try:
260
+ h = self.head()
261
+ interval = max(h.ttl_sec, 60) # At least 60 seconds
262
+ except Exception:
263
+ interval = 900 # Default 15 minutes
264
+
265
+ print(f"[diffdelta] Watching for changes every {interval}s...")
266
+ print(f"[diffdelta] Press Ctrl+C to stop.")
267
+
268
+ while True:
269
+ try:
270
+ items = self.poll(tags=tags, sources=sources, buckets=buckets)
271
+ if items:
272
+ print(f"[diffdelta] {len(items)} new item(s) found.")
273
+ for item in items:
274
+ callback(item)
275
+ else:
276
+ print(f"[diffdelta] No changes.")
277
+ except KeyboardInterrupt:
278
+ print("\n[diffdelta] Stopped.")
279
+ break
280
+ except Exception as e:
281
+ print(f"[diffdelta] Error: {e}. Retrying in {interval}s...")
282
+
283
+ try:
284
+ time.sleep(interval)
285
+ except KeyboardInterrupt:
286
+ print("\n[diffdelta] Stopped.")
287
+ break
288
+
289
+ # ── Cursor management ──
290
+
291
+ def reset_cursors(self, source_id: Optional[str] = None) -> None:
292
+ """Reset stored cursors so the next poll returns all current items.
293
+
294
+ Args:
295
+ source_id: Reset cursor for a specific source. None = reset all.
296
+ """
297
+ if self._cursors:
298
+ if source_id:
299
+ self._cursors.clear(f"source:{source_id}")
300
+ else:
301
+ self._cursors.clear()
302
+
303
+ # ── Internal ──
304
+
305
+ _source_tags_cache: Optional[Dict[str, List[str]]] = None
306
+
307
+ def _get_source_tags(self) -> Dict[str, List[str]]:
308
+ """Cache source → tags mapping for tag-based filtering."""
309
+ if self._source_tags_cache is None:
310
+ try:
311
+ all_sources = self.sources()
312
+ self._source_tags_cache = {
313
+ s.source_id: s.tags for s in all_sources
314
+ }
315
+ except Exception:
316
+ self._source_tags_cache = {}
317
+ return self._source_tags_cache
318
+
319
+ def _get_json(self, url: str) -> Dict[str, Any]:
320
+ """Fetch a URL and return parsed JSON."""
321
+ resp = self._session.get(url, timeout=self.timeout)
322
+ resp.raise_for_status()
323
+ return resp.json()
324
+
325
+ def __repr__(self) -> str:
326
+ tier = "pro" if self.api_key else "free"
327
+ return f"DiffDelta(base_url={self.base_url!r}, tier={tier!r})"
@@ -0,0 +1,68 @@
1
+ """Cursor persistence — automatically saves and loads polling state."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ from pathlib import Path
8
+ from typing import Dict, Optional
9
+
10
+
11
+ DEFAULT_CURSOR_DIR = os.path.join(Path.home(), ".diffdelta")
12
+ DEFAULT_CURSOR_FILE = "cursors.json"
13
+
14
+
15
+ class CursorStore:
16
+ """Persists cursors to a local JSON file so bots survive restarts.
17
+
18
+ By default, cursors are saved to ~/.diffdelta/cursors.json.
19
+ Each feed URL gets its own cursor entry.
20
+ """
21
+
22
+ def __init__(self, path: Optional[str] = None) -> None:
23
+ if path:
24
+ self._path = path
25
+ else:
26
+ self._path = os.path.join(DEFAULT_CURSOR_DIR, DEFAULT_CURSOR_FILE)
27
+
28
+ self._cursors: Dict[str, str] = {}
29
+ self._load()
30
+
31
+ def get(self, key: str) -> Optional[str]:
32
+ """Get the stored cursor for a feed key."""
33
+ return self._cursors.get(key)
34
+
35
+ def set(self, key: str, cursor: str) -> None:
36
+ """Save a cursor and persist to disk."""
37
+ self._cursors[key] = cursor
38
+ self._save()
39
+
40
+ def clear(self, key: Optional[str] = None) -> None:
41
+ """Clear cursor(s). If key is None, clears all cursors."""
42
+ if key:
43
+ self._cursors.pop(key, None)
44
+ else:
45
+ self._cursors.clear()
46
+ self._save()
47
+
48
+ def _load(self) -> None:
49
+ """Load cursors from disk."""
50
+ try:
51
+ if os.path.exists(self._path):
52
+ with open(self._path, "r") as f:
53
+ data = json.load(f)
54
+ if isinstance(data, dict):
55
+ self._cursors = data
56
+ except (json.JSONDecodeError, OSError):
57
+ # Corrupted file — start fresh
58
+ self._cursors = {}
59
+
60
+ def _save(self) -> None:
61
+ """Persist cursors to disk."""
62
+ try:
63
+ os.makedirs(os.path.dirname(self._path), exist_ok=True)
64
+ with open(self._path, "w") as f:
65
+ json.dump(self._cursors, f, indent=2)
66
+ except OSError:
67
+ # Can't write — silently continue (in-memory only)
68
+ pass
@@ -0,0 +1,186 @@
1
+ """Data models for DiffDelta feeds."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from datetime import datetime
7
+ from typing import Any, Dict, List, Optional
8
+
9
+
10
+ @dataclass
11
+ class FeedItem:
12
+ """A single item from a DiffDelta feed.
13
+
14
+ Attributes:
15
+ source: Source identifier (e.g. "cisa_kev", "nist_nvd").
16
+ id: Unique item ID within the source.
17
+ headline: Human/agent-readable headline.
18
+ url: Link to the original source.
19
+ excerpt: Summary text extracted from the source.
20
+ published_at: When the item was originally published.
21
+ updated_at: When the item was last updated.
22
+ bucket: Which change bucket: "new", "updated", or "removed".
23
+ provenance: Raw provenance data (fetched_at, evidence_urls, content_hash).
24
+ raw: The full raw item dict from the feed.
25
+ """
26
+
27
+ source: str
28
+ id: str
29
+ headline: str
30
+ url: str = ""
31
+ excerpt: str = ""
32
+ published_at: Optional[str] = None
33
+ updated_at: Optional[str] = None
34
+ bucket: str = "new" # "new" | "updated" | "removed"
35
+ provenance: Dict[str, Any] = field(default_factory=dict)
36
+ raw: Dict[str, Any] = field(default_factory=dict)
37
+
38
+ @classmethod
39
+ def from_raw(cls, data: Dict[str, Any], bucket: str = "new") -> "FeedItem":
40
+ """Create a FeedItem from a raw feed item dict."""
41
+ content = data.get("content", {})
42
+ excerpt = ""
43
+ if isinstance(content, dict):
44
+ excerpt = content.get("excerpt_text", "") or content.get("summary", "")
45
+ elif isinstance(content, str):
46
+ excerpt = content
47
+
48
+ return cls(
49
+ source=data.get("source", ""),
50
+ id=data.get("id", ""),
51
+ headline=data.get("headline", ""),
52
+ url=data.get("url", ""),
53
+ excerpt=excerpt,
54
+ published_at=data.get("published_at"),
55
+ updated_at=data.get("updated_at"),
56
+ bucket=bucket,
57
+ provenance=data.get("provenance", {}),
58
+ raw=data,
59
+ )
60
+
61
+ def __repr__(self) -> str:
62
+ return f"FeedItem(source={self.source!r}, id={self.id!r}, headline={self.headline!r}, bucket={self.bucket!r})"
63
+
64
+
65
+ @dataclass
66
+ class Head:
67
+ """The lightweight head pointer for change detection.
68
+
69
+ Attributes:
70
+ cursor: Opaque cursor string for change detection.
71
+ hash: Content hash of the latest feed.
72
+ changed: Whether content has changed since last generation.
73
+ generated_at: When this head was generated.
74
+ ttl_sec: Recommended polling interval in seconds.
75
+ """
76
+
77
+ cursor: str
78
+ hash: str = ""
79
+ changed: bool = False
80
+ generated_at: str = ""
81
+ ttl_sec: int = 900
82
+
83
+ @classmethod
84
+ def from_raw(cls, data: Dict[str, Any]) -> "Head":
85
+ return cls(
86
+ cursor=data.get("cursor", ""),
87
+ hash=data.get("hash", ""),
88
+ changed=data.get("changed", False),
89
+ generated_at=data.get("generated_at", ""),
90
+ ttl_sec=data.get("ttl_sec", 900),
91
+ )
92
+
93
+
94
+ @dataclass
95
+ class Feed:
96
+ """A full DiffDelta feed response.
97
+
98
+ Attributes:
99
+ cursor: The new cursor (save this for next poll).
100
+ prev_cursor: The previous cursor.
101
+ source_id: Source ID (if per-source feed) or "global".
102
+ generated_at: When this feed was generated.
103
+ items: All items across all buckets.
104
+ new: Items in the "new" bucket.
105
+ updated: Items in the "updated" bucket.
106
+ removed: Items in the "removed" bucket.
107
+ narrative: Human-readable summary of what changed.
108
+ raw: The full raw feed dict.
109
+ """
110
+
111
+ cursor: str
112
+ prev_cursor: str = ""
113
+ source_id: str = ""
114
+ generated_at: str = ""
115
+ items: List[FeedItem] = field(default_factory=list)
116
+ new: List[FeedItem] = field(default_factory=list)
117
+ updated: List[FeedItem] = field(default_factory=list)
118
+ removed: List[FeedItem] = field(default_factory=list)
119
+ narrative: str = ""
120
+ raw: Dict[str, Any] = field(default_factory=dict)
121
+
122
+ @classmethod
123
+ def from_raw(cls, data: Dict[str, Any]) -> "Feed":
124
+ buckets = data.get("buckets", {})
125
+
126
+ new_items = [FeedItem.from_raw(i, "new") for i in buckets.get("new", [])]
127
+ updated_items = [FeedItem.from_raw(i, "updated") for i in buckets.get("updated", [])]
128
+ removed_items = [FeedItem.from_raw(i, "removed") for i in buckets.get("removed", [])]
129
+ all_items = new_items + updated_items + removed_items
130
+
131
+ return cls(
132
+ cursor=data.get("cursor", ""),
133
+ prev_cursor=data.get("prev_cursor", ""),
134
+ source_id=data.get("source_id", ""),
135
+ generated_at=data.get("generated_at", ""),
136
+ items=all_items,
137
+ new=new_items,
138
+ updated=updated_items,
139
+ removed=removed_items,
140
+ narrative=data.get("batch_narrative", ""),
141
+ raw=data,
142
+ )
143
+
144
+
145
+ @dataclass
146
+ class SourceInfo:
147
+ """Metadata about an available DiffDelta source.
148
+
149
+ Attributes:
150
+ source_id: Unique source identifier (e.g. "cisa_kev").
151
+ name: Human-readable display name.
152
+ tags: List of tags (e.g. ["security"]).
153
+ description: Brief description of the source.
154
+ homepage: URL of the source's homepage.
155
+ enabled: Whether the source is currently active.
156
+ status: Health status ("ok", "degraded", "error").
157
+ head_url: Path to the source's head.json.
158
+ latest_url: Path to the source's latest.json.
159
+ """
160
+
161
+ source_id: str
162
+ name: str
163
+ tags: List[str] = field(default_factory=list)
164
+ description: str = ""
165
+ homepage: str = ""
166
+ enabled: bool = True
167
+ status: str = "ok"
168
+ head_url: str = ""
169
+ latest_url: str = ""
170
+
171
+ @classmethod
172
+ def from_raw(cls, data: Dict[str, Any]) -> "SourceInfo":
173
+ return cls(
174
+ source_id=data.get("source_id", ""),
175
+ name=data.get("name", ""),
176
+ tags=data.get("tags", []),
177
+ description=data.get("description", ""),
178
+ homepage=data.get("homepage", ""),
179
+ enabled=data.get("enabled", True),
180
+ status=data.get("status", "ok"),
181
+ head_url=data.get("head_url", ""),
182
+ latest_url=data.get("latest_url", ""),
183
+ )
184
+
185
+ def __repr__(self) -> str:
186
+ return f"SourceInfo(source_id={self.source_id!r}, name={self.name!r}, status={self.status!r})"
@@ -0,0 +1,25 @@
1
+ Metadata-Version: 2.4
2
+ Name: diffdelta
3
+ Version: 0.1.0
4
+ Summary: Python client for DiffDelta — agent-ready intelligence feeds
5
+ Author-email: DiffDelta <human@diffdelta.io>
6
+ License: MIT
7
+ Project-URL: Homepage, https://diffdelta.io
8
+ Project-URL: Documentation, https://diffdelta.io/#quickstart
9
+ Project-URL: Repository, https://github.com/diffdelta/diffdelta-python
10
+ Project-URL: Issues, https://github.com/diffdelta/diffdelta-python/issues
11
+ Keywords: diffdelta,ai-agents,security,changefeed,intelligence
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.8
17
+ Classifier: Programming Language :: Python :: 3.9
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Topic :: Security
23
+ Classifier: Topic :: Software Development :: Libraries
24
+ Requires-Python: >=3.8
25
+ Requires-Dist: requests>=2.25.0
@@ -0,0 +1,10 @@
1
+ pyproject.toml
2
+ diffdelta/__init__.py
3
+ diffdelta/client.py
4
+ diffdelta/cursor.py
5
+ diffdelta/models.py
6
+ diffdelta.egg-info/PKG-INFO
7
+ diffdelta.egg-info/SOURCES.txt
8
+ diffdelta.egg-info/dependency_links.txt
9
+ diffdelta.egg-info/requires.txt
10
+ diffdelta.egg-info/top_level.txt
@@ -0,0 +1 @@
1
+ requests>=2.25.0
@@ -0,0 +1 @@
1
+ diffdelta
@@ -0,0 +1,38 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "diffdelta"
7
+ version = "0.1.0"
8
+ description = "Python client for DiffDelta — agent-ready intelligence feeds"
9
+ requires-python = ">=3.8"
10
+ license = {text = "MIT"}
11
+ authors = [{name = "DiffDelta", email = "human@diffdelta.io"}]
12
+ keywords = ["diffdelta", "ai-agents", "security", "changefeed", "intelligence"]
13
+ classifiers = [
14
+ "Development Status :: 4 - Beta",
15
+ "Intended Audience :: Developers",
16
+ "License :: OSI Approved :: MIT License",
17
+ "Programming Language :: Python :: 3",
18
+ "Programming Language :: Python :: 3.8",
19
+ "Programming Language :: Python :: 3.9",
20
+ "Programming Language :: Python :: 3.10",
21
+ "Programming Language :: Python :: 3.11",
22
+ "Programming Language :: Python :: 3.12",
23
+ "Programming Language :: Python :: 3.13",
24
+ "Topic :: Security",
25
+ "Topic :: Software Development :: Libraries",
26
+ ]
27
+ dependencies = [
28
+ "requests>=2.25.0",
29
+ ]
30
+
31
+ [project.urls]
32
+ Homepage = "https://diffdelta.io"
33
+ Documentation = "https://diffdelta.io/#quickstart"
34
+ Repository = "https://github.com/diffdelta/diffdelta-python"
35
+ Issues = "https://github.com/diffdelta/diffdelta-python/issues"
36
+
37
+ [tool.setuptools.packages.find]
38
+ include = ["diffdelta*"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+