fetchkit-agents 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.
- fetchkit_agents-0.1.0/LICENSE +21 -0
- fetchkit_agents-0.1.0/PKG-INFO +356 -0
- fetchkit_agents-0.1.0/README.md +319 -0
- fetchkit_agents-0.1.0/pyproject.toml +97 -0
- fetchkit_agents-0.1.0/setup.cfg +4 -0
- fetchkit_agents-0.1.0/src/fetchkit/__init__.py +91 -0
- fetchkit_agents-0.1.0/src/fetchkit/__main__.py +8 -0
- fetchkit_agents-0.1.0/src/fetchkit/cli.py +169 -0
- fetchkit_agents-0.1.0/src/fetchkit/collector.py +116 -0
- fetchkit_agents-0.1.0/src/fetchkit/config_loader.py +59 -0
- fetchkit_agents-0.1.0/src/fetchkit/fetchers/__init__.py +11 -0
- fetchkit_agents-0.1.0/src/fetchkit/fetchers/arxiv.py +133 -0
- fetchkit_agents-0.1.0/src/fetchkit/fetchers/base.py +31 -0
- fetchkit_agents-0.1.0/src/fetchkit/fetchers/github.py +124 -0
- fetchkit_agents-0.1.0/src/fetchkit/fetchers/hackernews.py +295 -0
- fetchkit_agents-0.1.0/src/fetchkit/fetchers/lobsters.py +85 -0
- fetchkit_agents-0.1.0/src/fetchkit/fetchers/registry.py +52 -0
- fetchkit_agents-0.1.0/src/fetchkit/fetchers/rss.py +252 -0
- fetchkit_agents-0.1.0/src/fetchkit/http/__init__.py +17 -0
- fetchkit_agents-0.1.0/src/fetchkit/http/client.py +223 -0
- fetchkit_agents-0.1.0/src/fetchkit/http/rate_limit.py +46 -0
- fetchkit_agents-0.1.0/src/fetchkit/py.typed +0 -0
- fetchkit_agents-0.1.0/src/fetchkit/schemas/__init__.py +40 -0
- fetchkit_agents-0.1.0/src/fetchkit/schemas/base.py +27 -0
- fetchkit_agents-0.1.0/src/fetchkit/schemas/collector.py +21 -0
- fetchkit_agents-0.1.0/src/fetchkit/schemas/config.py +103 -0
- fetchkit_agents-0.1.0/src/fetchkit/schemas/contracts.py +41 -0
- fetchkit_agents-0.1.0/src/fetchkit/schemas/fetcher.py +157 -0
- fetchkit_agents-0.1.0/src/fetchkit/schemas/post.py +100 -0
- fetchkit_agents-0.1.0/src/fetchkit/utils/__init__.py +1 -0
- fetchkit_agents-0.1.0/src/fetchkit/utils/time.py +95 -0
- fetchkit_agents-0.1.0/src/fetchkit_agents.egg-info/PKG-INFO +356 -0
- fetchkit_agents-0.1.0/src/fetchkit_agents.egg-info/SOURCES.txt +35 -0
- fetchkit_agents-0.1.0/src/fetchkit_agents.egg-info/dependency_links.txt +1 -0
- fetchkit_agents-0.1.0/src/fetchkit_agents.egg-info/entry_points.txt +2 -0
- fetchkit_agents-0.1.0/src/fetchkit_agents.egg-info/requires.txt +12 -0
- fetchkit_agents-0.1.0/src/fetchkit_agents.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 mete morris
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: fetchkit-agents
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A YAML-configured data fetching library for agentic applications.
|
|
5
|
+
Author-email: mete morris <morrismete@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/metemorris/fetchkit
|
|
8
|
+
Project-URL: Repository, https://github.com/metemorris/fetchkit
|
|
9
|
+
Project-URL: Issues, https://github.com/metemorris/fetchkit/issues
|
|
10
|
+
Keywords: rss,hackernews,fetcher,data-collection,agents,llm,feed
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
21
|
+
Classifier: Typing :: Typed
|
|
22
|
+
Requires-Python: >=3.10
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
License-File: LICENSE
|
|
25
|
+
Requires-Dist: pydantic>=2.0
|
|
26
|
+
Requires-Dist: requests>=2.28
|
|
27
|
+
Requires-Dist: feedparser>=6.0
|
|
28
|
+
Requires-Dist: PyYAML>=6.0
|
|
29
|
+
Provides-Extra: dev
|
|
30
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
31
|
+
Requires-Dist: responses>=0.23; extra == "dev"
|
|
32
|
+
Requires-Dist: mypy>=1.0; extra == "dev"
|
|
33
|
+
Requires-Dist: ruff>=0.4; extra == "dev"
|
|
34
|
+
Requires-Dist: types-PyYAML; extra == "dev"
|
|
35
|
+
Requires-Dist: types-requests; extra == "dev"
|
|
36
|
+
Dynamic: license-file
|
|
37
|
+
|
|
38
|
+
# fetchkit
|
|
39
|
+
|
|
40
|
+
A YAML-configured data-fetching library for agentic applications.
|
|
41
|
+
|
|
42
|
+
`fetchkit` collects posts and comments from sources (Hacker News, RSS/Atom, arXiv,
|
|
43
|
+
GitHub, Lobsters) into a single canonical `Post` model, with de-duplication,
|
|
44
|
+
deterministic sorting, and a shared HTTP client with retries and rate limiting. It is
|
|
45
|
+
designed as the data-collection layer for LLM/agent pipelines — feed it configs, get
|
|
46
|
+
back clean typed data.
|
|
47
|
+
|
|
48
|
+
- **YAML-first** configuration with strict validation: unknown or misspelled keys
|
|
49
|
+
are rejected up front, so a bad config fails loudly instead of silently.
|
|
50
|
+
- **Builtin fetchers**: Hacker News, RSS/Atom, arXiv, GitHub, and Lobsters — all
|
|
51
|
+
zero-auth.
|
|
52
|
+
- **Relative time windows**: say `window: "last 6 hours"` instead of computing
|
|
53
|
+
timestamps.
|
|
54
|
+
- **Open `metadata`**: each `Post` carries a `metadata` dict for source-specific
|
|
55
|
+
detail (arXiv categories/DOI, GitHub language/stars, tags) without bloating the
|
|
56
|
+
core model.
|
|
57
|
+
- **Robust HTTP**: shared session pooling, exponential backoff + retries, `Retry-After`
|
|
58
|
+
handling, and optional per-host rate limiting.
|
|
59
|
+
- **Typed end-to-end**: Pydantic v2 models throughout.
|
|
60
|
+
- **Deterministic output**: dedup by `(source, id)`, sorted descending by
|
|
61
|
+
`(created_at, id)`.
|
|
62
|
+
- **Agent-friendly CLI**: `fetchkit run config.yaml` emits clean JSON on stdout —
|
|
63
|
+
no Python required.
|
|
64
|
+
|
|
65
|
+
## Install
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
pip install fetchkit-agents # PyPI distribution name
|
|
69
|
+
|
|
70
|
+
# or install the latest from source:
|
|
71
|
+
pip install "git+https://github.com/metemorris/fetchkit.git"
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
The PyPI package is `fetchkit-agents` (the `fetchkit` name was taken), but you still
|
|
75
|
+
`import fetchkit` and the CLI command is `fetchkit`. Requires Python ≥ 3.10.
|
|
76
|
+
|
|
77
|
+
## Quick start
|
|
78
|
+
|
|
79
|
+
### 1. YAML config
|
|
80
|
+
|
|
81
|
+
`config.yaml`:
|
|
82
|
+
|
|
83
|
+
```yaml
|
|
84
|
+
window: "last 24 hours" # or set explicit start_time / end_time
|
|
85
|
+
fetchers:
|
|
86
|
+
- type: hackernews
|
|
87
|
+
posts:
|
|
88
|
+
max_items: 50
|
|
89
|
+
order: new
|
|
90
|
+
- type: arxiv
|
|
91
|
+
categories: ["cs.AI", "cs.LG"]
|
|
92
|
+
max_items: 40
|
|
93
|
+
- type: github
|
|
94
|
+
resource: releases
|
|
95
|
+
repos: ["python/cpython", "pydantic/pydantic"]
|
|
96
|
+
- type: lobsters
|
|
97
|
+
listing: hottest
|
|
98
|
+
- type: rss
|
|
99
|
+
feeds:
|
|
100
|
+
- url: "https://feeds.bbci.co.uk/news/rss.xml"
|
|
101
|
+
name: "BBC News"
|
|
102
|
+
max_items_per_feed: 40
|
|
103
|
+
max_total_items: 200
|
|
104
|
+
http: # optional
|
|
105
|
+
timeout: 15
|
|
106
|
+
max_retries: 5
|
|
107
|
+
rate_limit_per_host: 2.0
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### 2. Run
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
from fetchkit import load_config, collect_all
|
|
114
|
+
|
|
115
|
+
config = load_config("config.yaml")
|
|
116
|
+
result = collect_all(config)
|
|
117
|
+
|
|
118
|
+
print(f"Collected {len(result.posts)} posts")
|
|
119
|
+
for post in result.posts:
|
|
120
|
+
print(post.created_at, post.source, post.title, post.url)
|
|
121
|
+
|
|
122
|
+
if result.has_errors:
|
|
123
|
+
for source, err in result.errors:
|
|
124
|
+
print(f" {source}: {err}")
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### 3. Command line (for agents & scripts)
|
|
128
|
+
|
|
129
|
+
`fetchkit` installs a CLI so an agent can shell out and parse JSON without writing
|
|
130
|
+
Python. `run` prints **only** JSON to stdout (diagnostics go to stderr), so it pipes
|
|
131
|
+
cleanly into `jq`:
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
fetchkit run config.yaml # pretty JSON: {"count", "posts", "errors"}
|
|
135
|
+
fetchkit run config.yaml -o out.json # write JSON to a file (stdout stays empty)
|
|
136
|
+
fetchkit run config.yaml --window "6h" # override the time window at runtime
|
|
137
|
+
fetchkit run config.yaml --compact # single-line JSON
|
|
138
|
+
fetchkit run config.yaml --fail-on-error # exit 1 if any source failed
|
|
139
|
+
fetchkit validate config.yaml # validate a config without fetching
|
|
140
|
+
python -m fetchkit run config.yaml # module form, identical behavior
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Output shape:
|
|
144
|
+
|
|
145
|
+
```json
|
|
146
|
+
{
|
|
147
|
+
"count": 2,
|
|
148
|
+
"posts": [ { "id": "...", "source": "rss", "title": "...", "url": "...", "created_at": "..." } ],
|
|
149
|
+
"errors": [ { "source": "hackernews", "error": "..." } ]
|
|
150
|
+
}
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Exit codes: `0` success · `1` a source failed (only with `--fail-on-error`) ·
|
|
154
|
+
`2` configuration error.
|
|
155
|
+
|
|
156
|
+
### 4. Programmatic (no YAML)
|
|
157
|
+
|
|
158
|
+
```python
|
|
159
|
+
from datetime import datetime, timezone, timedelta
|
|
160
|
+
from fetchkit import FetchKitConfig, HackerNewsFetchConfig, PostFetchConfig, SortOrder, collect_all
|
|
161
|
+
|
|
162
|
+
config = FetchKitConfig(
|
|
163
|
+
start_time=datetime.now(timezone.utc) - timedelta(days=1),
|
|
164
|
+
end_time=datetime.now(timezone.utc),
|
|
165
|
+
fetchers=[
|
|
166
|
+
HackerNewsFetchConfig(posts=PostFetchConfig(max_items=30, order=SortOrder.NEW)),
|
|
167
|
+
],
|
|
168
|
+
)
|
|
169
|
+
result = collect_all(config)
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## Configuration reference
|
|
173
|
+
|
|
174
|
+
### Top-level (`FetchKitConfig`)
|
|
175
|
+
|
|
176
|
+
| Field | Type | Required | Description |
|
|
177
|
+
|---------------|---------------------|----------|--------------------------------------------|
|
|
178
|
+
| `window` | str | no | Relative window (resolves to start/end). |
|
|
179
|
+
| `start_time` | datetime | no | Global window start (inclusive). |
|
|
180
|
+
| `end_time` | datetime | no | Global window end (inclusive). |
|
|
181
|
+
| `fetchers` | list[FetcherConfig] | no | Fetcher instances to run. |
|
|
182
|
+
| `http` | HttpConfig | no | Shared HTTP client settings. |
|
|
183
|
+
|
|
184
|
+
Set the time window with **either** a relative `window` **or** both
|
|
185
|
+
`start_time`/`end_time` — the two are mutually exclusive. **If you specify none of
|
|
186
|
+
them, the window defaults to the last 24 hours.** `start_time <= end_time` is
|
|
187
|
+
enforced, and unknown top-level keys are rejected. Per-fetcher
|
|
188
|
+
`start_time`/`end_time` default to `None` and **inherit** the global window at runtime.
|
|
189
|
+
|
|
190
|
+
#### Relative windows
|
|
191
|
+
|
|
192
|
+
`window` accepts (case-insensitive): `"last 6 hours"`, `"past 30 minutes"`,
|
|
193
|
+
`"last 7 days"`, `"last week"`, `"last month"`, `"today"`, `"yesterday"`, or a bare
|
|
194
|
+
duration like `"6h"`, `"2d"`, `"90m"`. It resolves **once** to a concrete
|
|
195
|
+
`(start_time, end_time)` pair (end = now), so collection stays deterministic for that
|
|
196
|
+
resolved pair. The same parsing is available programmatically via
|
|
197
|
+
`fetchkit.resolve_window(spec)` and `fetchkit.parse_duration(text)`.
|
|
198
|
+
|
|
199
|
+
### Hacker News (`type: hackernews`)
|
|
200
|
+
|
|
201
|
+
| Field | Default | Notes |
|
|
202
|
+
|----------------------|---------|----------------------------------------|
|
|
203
|
+
| `posts.max_items` | 10 | 1–500 |
|
|
204
|
+
| `posts.order` | top | top / new / controversial / asc / desc |
|
|
205
|
+
| `comments.fetch` | false | Fetch comment threads for each post |
|
|
206
|
+
| `comments.max_items` | 10 | 1–100 roots per post |
|
|
207
|
+
| `comments.max_depth` | 1 | 0 = roots only |
|
|
208
|
+
| `comments.order` | top | Sort order for comment roots |
|
|
209
|
+
|
|
210
|
+
### RSS / Atom (`type: rss`)
|
|
211
|
+
|
|
212
|
+
| Field | Default | Notes |
|
|
213
|
+
|------------------------|---------|--------------------------------|
|
|
214
|
+
| `feeds` | — | List of `{url, name?}` |
|
|
215
|
+
| `max_items_per_feed` | 50 | 1–500 |
|
|
216
|
+
| `max_total_items` | 200 | 1–2000 |
|
|
217
|
+
| `include_content` | true | Include full entry content |
|
|
218
|
+
| `allow_local_files` | false | Permit local-path / `file://` feeds |
|
|
219
|
+
|
|
220
|
+
`feeds[].url` is restricted to HTTP(S) URLs by default. Setting
|
|
221
|
+
`allow_local_files: true` additionally permits local file paths and `file://`
|
|
222
|
+
URLs — see the security note below before enabling it.
|
|
223
|
+
|
|
224
|
+
> **⚠️ Security — untrusted configs.** fetchkit is built to run YAML that may be
|
|
225
|
+
> produced by an LLM/agent. With `allow_local_files: true`, a config could point a
|
|
226
|
+
> "feed" at an arbitrary local path (e.g. `file:///etc/passwd`) and surface its
|
|
227
|
+
> contents in `post.text`. Local file reads are therefore **off by default**;
|
|
228
|
+
> enable them only for configs and fixtures you trust. Keep the default (`false`)
|
|
229
|
+
> when feeding fetchkit configs you did not author.
|
|
230
|
+
|
|
231
|
+
### arXiv (`type: arxiv`)
|
|
232
|
+
|
|
233
|
+
Uses the arXiv export API (Atom, parsed with feedparser). Authors, categories, DOI,
|
|
234
|
+
PDF link, and primary category are preserved in `post.metadata`.
|
|
235
|
+
|
|
236
|
+
| Field | Default | Notes |
|
|
237
|
+
|--------------|---------|--------------------------------------------------|
|
|
238
|
+
| `categories` | `[]` | arXiv categories, e.g. `["cs.AI", "cs.LG"]` (empty = all) |
|
|
239
|
+
| `query` | null | Free-text query (combined with categories via AND) |
|
|
240
|
+
| `max_items` | 50 | 1–500 |
|
|
241
|
+
|
|
242
|
+
### GitHub (`type: github`)
|
|
243
|
+
|
|
244
|
+
Public GitHub REST API (no auth; tighter rate limits without a token). Repo,
|
|
245
|
+
language, stars, forks, and topics are preserved in `post.metadata`.
|
|
246
|
+
|
|
247
|
+
| Field | Default | Notes |
|
|
248
|
+
|------------|----------|--------------------------------------------------------------|
|
|
249
|
+
| `resource` | releases | `releases` (per-repo) or `search_repos` |
|
|
250
|
+
| `repos` | `[]` | `owner/name` list — required for `resource: releases` |
|
|
251
|
+
| `query` | null | Search query — required for `resource: search_repos` |
|
|
252
|
+
| `max_items`| 50 | 1–300 |
|
|
253
|
+
|
|
254
|
+
### Lobsters (`type: lobsters`)
|
|
255
|
+
|
|
256
|
+
The lobste.rs public JSON endpoints (no auth). Tags are preserved in `post.metadata`.
|
|
257
|
+
|
|
258
|
+
| Field | Default | Notes |
|
|
259
|
+
|------------|---------|-------------------------------------------------|
|
|
260
|
+
| `listing` | hottest | `hottest` or `newest` |
|
|
261
|
+
| `tag` | null | Restrict to a single tag (uses `/t/<tag>.json`) |
|
|
262
|
+
| `max_items`| 50 | 1–200 |
|
|
263
|
+
|
|
264
|
+
### HTTP (`http:`)
|
|
265
|
+
|
|
266
|
+
| Field | Default | Notes |
|
|
267
|
+
|-----------------------|--------------------------|----------------------------------------------------|
|
|
268
|
+
| `timeout` | 10.0 | Per-request timeout in seconds |
|
|
269
|
+
| `max_retries` | 3 | Retries on transient errors (0–10) |
|
|
270
|
+
| `backoff_factor` | 0.5 | `wait = backoff_factor * 2^attempt` seconds |
|
|
271
|
+
| `rate_limit_per_host` | null | Max requests/sec per host (null = disabled) |
|
|
272
|
+
| `retry_statuses` | 429,500,502,503,504 | Status codes that trigger a retry |
|
|
273
|
+
|
|
274
|
+
## Adding a fetcher
|
|
275
|
+
|
|
276
|
+
Fetchers live in the library itself, so each one ships with a typed config,
|
|
277
|
+
validation, and tests. Add a new source via a PR or a local fork in four steps:
|
|
278
|
+
|
|
279
|
+
1. Add a typed config to `src/fetchkit/schemas/fetcher.py` (subclass `FetcherBase`,
|
|
280
|
+
give it a `Literal` `type`), and register it in `_BUILTIN_TYPES` + `FetcherConfig`.
|
|
281
|
+
2. Write the fetcher module in `src/fetchkit/fetchers/`, returning a `FetcherResult`:
|
|
282
|
+
|
|
283
|
+
```python
|
|
284
|
+
from fetchkit.fetchers.base import FetcherResult
|
|
285
|
+
from fetchkit.fetchers.registry import register_fetcher
|
|
286
|
+
from fetchkit.schemas.post import Post
|
|
287
|
+
|
|
288
|
+
@register_fetcher("mysource")
|
|
289
|
+
def fetch(config) -> FetcherResult:
|
|
290
|
+
posts = [Post(id="1", source="mysource", title="…", source_url="https://…")]
|
|
291
|
+
return FetcherResult(posts=posts, errors=[])
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
3. Import the module in `src/fetchkit/fetchers/__init__.py` so it registers on import.
|
|
295
|
+
4. Add tests (mock HTTP with the `responses` library — see `tests/fetchers/`).
|
|
296
|
+
|
|
297
|
+
Use `Post.metadata` for any source-specific fields that don't map to the canonical
|
|
298
|
+
columns.
|
|
299
|
+
|
|
300
|
+
## The `Post` model
|
|
301
|
+
|
|
302
|
+
```python
|
|
303
|
+
class Post(BaseModel):
|
|
304
|
+
id: str # unique within source
|
|
305
|
+
source: str # "hackernews" | "rss" | "arxiv" | "github" | "lobsters"
|
|
306
|
+
title: str | None
|
|
307
|
+
text: str | None # body / content
|
|
308
|
+
url: str | None # external link
|
|
309
|
+
author: str | None
|
|
310
|
+
score: int | None
|
|
311
|
+
comment_count: int | None
|
|
312
|
+
created_at: datetime | None # UTC-aware
|
|
313
|
+
source_url: str # direct link on the source platform
|
|
314
|
+
comments: list[Comment] # nested threads (HN)
|
|
315
|
+
metadata: dict[str, Any] # source-specific extras (categories, stars, tags, …)
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
All datetimes are normalized to UTC. Posts are deduplicated by `(source, id)` and
|
|
319
|
+
sorted descending by `(created_at, id)` for deterministic output.
|
|
320
|
+
|
|
321
|
+
## Collector invariants
|
|
322
|
+
|
|
323
|
+
`collect_all` preserves three guarantees:
|
|
324
|
+
|
|
325
|
+
1. **Window inheritance** — per-fetcher `start_time`/`end_time` fall back to the
|
|
326
|
+
global window when omitted.
|
|
327
|
+
2. **Dedup** — by `(source, id)`; first occurrence wins.
|
|
328
|
+
3. **Sort** — descending by `(created_at or UTC_MIN, id)`.
|
|
329
|
+
|
|
330
|
+
Partial failures are aggregated, not fatal: a failing source yields an entry in
|
|
331
|
+
`result.errors` while other sources still collect. Check `result.has_errors`.
|
|
332
|
+
|
|
333
|
+
## Development
|
|
334
|
+
|
|
335
|
+
```bash
|
|
336
|
+
python -m venv .venv && source .venv/bin/activate
|
|
337
|
+
pip install -e ".[dev]"
|
|
338
|
+
|
|
339
|
+
# tests (skip live/networked)
|
|
340
|
+
pytest -m "not live"
|
|
341
|
+
|
|
342
|
+
# live network smoke (opt-in)
|
|
343
|
+
pytest -m live
|
|
344
|
+
|
|
345
|
+
# strict typecheck
|
|
346
|
+
mypy src
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
## License
|
|
350
|
+
|
|
351
|
+
MIT — see [LICENSE](LICENSE). MIT is intentional: as a small, dependency-light
|
|
352
|
+
utility meant to be embedded freely in agent pipelines (including commercial and
|
|
353
|
+
closed-source ones), a permissive license maximizes adoption with no copyleft
|
|
354
|
+
obligations. A weak-copyleft license (e.g. MPL-2.0) or strong copyleft (GPL/AGPL)
|
|
355
|
+
would force redistribution terms on downstream users and discourage exactly the
|
|
356
|
+
embedded use fetchkit is built for.
|