fetchkit-agents 0.2.0__tar.gz → 0.3.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.
- {fetchkit_agents-0.2.0 → fetchkit_agents-0.3.1}/PKG-INFO +143 -17
- {fetchkit_agents-0.2.0 → fetchkit_agents-0.3.1}/README.md +142 -16
- {fetchkit_agents-0.2.0 → fetchkit_agents-0.3.1}/pyproject.toml +1 -1
- {fetchkit_agents-0.2.0 → fetchkit_agents-0.3.1}/src/fetchkit/__init__.py +20 -4
- {fetchkit_agents-0.2.0 → fetchkit_agents-0.3.1}/src/fetchkit/cli.py +168 -10
- {fetchkit_agents-0.2.0 → fetchkit_agents-0.3.1}/src/fetchkit/collector.py +16 -10
- {fetchkit_agents-0.2.0 → fetchkit_agents-0.3.1}/src/fetchkit/config_loader.py +22 -11
- {fetchkit_agents-0.2.0 → fetchkit_agents-0.3.1}/src/fetchkit/discovery/__init__.py +2 -1
- {fetchkit_agents-0.2.0 → fetchkit_agents-0.3.1}/src/fetchkit/discovery/core.py +27 -1
- fetchkit_agents-0.3.1/src/fetchkit/fetchers/__init__.py +28 -0
- {fetchkit_agents-0.2.0 → fetchkit_agents-0.3.1}/src/fetchkit/fetchers/arxiv.py +44 -0
- fetchkit_agents-0.3.1/src/fetchkit/fetchers/bluesky.py +176 -0
- {fetchkit_agents-0.2.0 → fetchkit_agents-0.3.1}/src/fetchkit/fetchers/github.py +23 -1
- {fetchkit_agents-0.2.0 → fetchkit_agents-0.3.1}/src/fetchkit/fetchers/hackernews.py +12 -0
- {fetchkit_agents-0.2.0 → fetchkit_agents-0.3.1}/src/fetchkit/fetchers/lobsters.py +20 -0
- fetchkit_agents-0.3.1/src/fetchkit/fetchers/mastodon.py +161 -0
- {fetchkit_agents-0.2.0 → fetchkit_agents-0.3.1}/src/fetchkit/fetchers/rss.py +12 -0
- fetchkit_agents-0.3.1/src/fetchkit/fetchers/stackexchange.py +230 -0
- fetchkit_agents-0.3.1/src/fetchkit/fetchers/suggest_registry.py +53 -0
- fetchkit_agents-0.3.1/src/fetchkit/schemas/collector.py +54 -0
- {fetchkit_agents-0.2.0 → fetchkit_agents-0.3.1}/src/fetchkit/schemas/describe.py +50 -2
- {fetchkit_agents-0.2.0 → fetchkit_agents-0.3.1}/src/fetchkit/schemas/fetcher.py +95 -2
- {fetchkit_agents-0.2.0 → fetchkit_agents-0.3.1}/src/fetchkit/schemas/post.py +3 -0
- {fetchkit_agents-0.2.0 → fetchkit_agents-0.3.1}/src/fetchkit_agents.egg-info/PKG-INFO +143 -17
- {fetchkit_agents-0.2.0 → fetchkit_agents-0.3.1}/src/fetchkit_agents.egg-info/SOURCES.txt +4 -0
- fetchkit_agents-0.2.0/src/fetchkit/fetchers/__init__.py +0 -11
- fetchkit_agents-0.2.0/src/fetchkit/schemas/collector.py +0 -21
- {fetchkit_agents-0.2.0 → fetchkit_agents-0.3.1}/LICENSE +0 -0
- {fetchkit_agents-0.2.0 → fetchkit_agents-0.3.1}/setup.cfg +0 -0
- {fetchkit_agents-0.2.0 → fetchkit_agents-0.3.1}/src/fetchkit/__main__.py +0 -0
- {fetchkit_agents-0.2.0 → fetchkit_agents-0.3.1}/src/fetchkit/discovery/autodiscover.py +0 -0
- {fetchkit_agents-0.2.0 → fetchkit_agents-0.3.1}/src/fetchkit/discovery/catalog.py +0 -0
- {fetchkit_agents-0.2.0 → fetchkit_agents-0.3.1}/src/fetchkit/discovery/data/CATALOG.md +0 -0
- {fetchkit_agents-0.2.0 → fetchkit_agents-0.3.1}/src/fetchkit/discovery/data/catalog.json +0 -0
- {fetchkit_agents-0.2.0 → fetchkit_agents-0.3.1}/src/fetchkit/discovery/embedding.py +0 -0
- {fetchkit_agents-0.2.0 → fetchkit_agents-0.3.1}/src/fetchkit/discovery/errors.py +0 -0
- {fetchkit_agents-0.2.0 → fetchkit_agents-0.3.1}/src/fetchkit/discovery/external.py +0 -0
- {fetchkit_agents-0.2.0 → fetchkit_agents-0.3.1}/src/fetchkit/discovery/ranking.py +0 -0
- {fetchkit_agents-0.2.0 → fetchkit_agents-0.3.1}/src/fetchkit/discovery/schemas.py +0 -0
- {fetchkit_agents-0.2.0 → fetchkit_agents-0.3.1}/src/fetchkit/fetchers/base.py +0 -0
- {fetchkit_agents-0.2.0 → fetchkit_agents-0.3.1}/src/fetchkit/fetchers/registry.py +0 -0
- {fetchkit_agents-0.2.0 → fetchkit_agents-0.3.1}/src/fetchkit/http/__init__.py +0 -0
- {fetchkit_agents-0.2.0 → fetchkit_agents-0.3.1}/src/fetchkit/http/client.py +0 -0
- {fetchkit_agents-0.2.0 → fetchkit_agents-0.3.1}/src/fetchkit/http/rate_limit.py +0 -0
- {fetchkit_agents-0.2.0 → fetchkit_agents-0.3.1}/src/fetchkit/http/ssrf.py +0 -0
- {fetchkit_agents-0.2.0 → fetchkit_agents-0.3.1}/src/fetchkit/py.typed +0 -0
- {fetchkit_agents-0.2.0 → fetchkit_agents-0.3.1}/src/fetchkit/schemas/__init__.py +0 -0
- {fetchkit_agents-0.2.0 → fetchkit_agents-0.3.1}/src/fetchkit/schemas/base.py +0 -0
- {fetchkit_agents-0.2.0 → fetchkit_agents-0.3.1}/src/fetchkit/schemas/config.py +0 -0
- {fetchkit_agents-0.2.0 → fetchkit_agents-0.3.1}/src/fetchkit/utils/__init__.py +0 -0
- {fetchkit_agents-0.2.0 → fetchkit_agents-0.3.1}/src/fetchkit/utils/time.py +0 -0
- {fetchkit_agents-0.2.0 → fetchkit_agents-0.3.1}/src/fetchkit_agents.egg-info/dependency_links.txt +0 -0
- {fetchkit_agents-0.2.0 → fetchkit_agents-0.3.1}/src/fetchkit_agents.egg-info/entry_points.txt +0 -0
- {fetchkit_agents-0.2.0 → fetchkit_agents-0.3.1}/src/fetchkit_agents.egg-info/requires.txt +0 -0
- {fetchkit_agents-0.2.0 → fetchkit_agents-0.3.1}/src/fetchkit_agents.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fetchkit-agents
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.1
|
|
4
4
|
Summary: A YAML-configured data fetching library for agentic applications.
|
|
5
5
|
Author-email: mete morris <morrismete@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -50,15 +50,16 @@ Dynamic: license-file
|
|
|
50
50
|
A YAML-configured data-fetching library for agentic applications.
|
|
51
51
|
|
|
52
52
|
`fetchkit` collects posts and comments from sources (Hacker News, RSS/Atom, arXiv,
|
|
53
|
-
GitHub, Lobsters) into a single canonical `Post`
|
|
53
|
+
GitHub, Lobsters, Stack Exchange, Bluesky, Mastodon) into a single canonical `Post`
|
|
54
|
+
model, with de-duplication,
|
|
54
55
|
deterministic sorting, and a shared HTTP client with retries and rate limiting. It is
|
|
55
56
|
designed as the data-collection layer for LLM/agent pipelines — feed it configs, get
|
|
56
57
|
back clean typed data.
|
|
57
58
|
|
|
58
59
|
- **YAML-first** configuration with strict validation: unknown or misspelled keys
|
|
59
60
|
are rejected up front, so a bad config fails loudly instead of silently.
|
|
60
|
-
- **Builtin fetchers**: Hacker News, RSS/Atom, arXiv, GitHub,
|
|
61
|
-
zero-auth.
|
|
61
|
+
- **Builtin fetchers**: Hacker News, RSS/Atom, arXiv, GitHub, Lobsters, Stack Exchange,
|
|
62
|
+
Bluesky, and Mastodon — all zero-auth.
|
|
62
63
|
- **Relative time windows**: say `window: "last 6 hours"` instead of computing
|
|
63
64
|
timestamps.
|
|
64
65
|
- **Open `metadata`**: each `Post` carries a `metadata` dict for source-specific
|
|
@@ -73,6 +74,9 @@ back clean typed data.
|
|
|
73
74
|
no Python required.
|
|
74
75
|
- **RSS feed discovery**: `fetchkit discover "<use case>"` maps a natural-language
|
|
75
76
|
query onto real RSS feeds an agent can fetch — closing the "which feed URL?" gap.
|
|
77
|
+
- **Per-source discovery**: `fetchkit suggest <source>` lists the selectable knobs
|
|
78
|
+
for any source (tags, sites, arXiv categories, trending hashtags, Bluesky feeds) —
|
|
79
|
+
all zero-auth — so an agent can fill a config without guessing.
|
|
76
80
|
|
|
77
81
|
## Install
|
|
78
82
|
|
|
@@ -107,6 +111,15 @@ fetchers:
|
|
|
107
111
|
repos: ["python/cpython", "pydantic/pydantic"]
|
|
108
112
|
- type: lobsters
|
|
109
113
|
listing: hottest
|
|
114
|
+
- type: stackexchange
|
|
115
|
+
site: stackoverflow
|
|
116
|
+
tagged: ["python", "asyncio"]
|
|
117
|
+
- type: bluesky
|
|
118
|
+
resource: search
|
|
119
|
+
query: "large language models"
|
|
120
|
+
- type: mastodon
|
|
121
|
+
instance: mastodon.social
|
|
122
|
+
tag: ai
|
|
110
123
|
- type: rss
|
|
111
124
|
feeds:
|
|
112
125
|
- url: "https://feeds.bbci.co.uk/news/rss.xml"
|
|
@@ -143,33 +156,49 @@ Python. `run` prints **only** JSON to stdout (diagnostics go to stderr), so it p
|
|
|
143
156
|
cleanly into `jq`:
|
|
144
157
|
|
|
145
158
|
```bash
|
|
146
|
-
fetchkit run config.yaml # pretty JSON: {"count", "posts", "errors"}
|
|
159
|
+
fetchkit run config.yaml # pretty JSON: {"count", "posts", "sources", "errors"}
|
|
147
160
|
fetchkit run config.yaml -o out.json # write JSON to a file (stdout stays empty)
|
|
148
161
|
fetchkit run config.yaml --window "6h" # override the time window at runtime
|
|
149
162
|
fetchkit run config.yaml --compact # single-line JSON
|
|
150
163
|
fetchkit run config.yaml --fail-on-error # exit 1 if any source failed
|
|
151
|
-
fetchkit
|
|
164
|
+
echo "$CONFIG_YAML" | fetchkit run - # read the config from stdin
|
|
165
|
+
fetchkit validate config.yaml # validate a config without fetching (also accepts -)
|
|
152
166
|
fetchkit schema # JSON Schema for every config/output model
|
|
153
167
|
python -m fetchkit run config.yaml # module form, identical behavior
|
|
154
168
|
```
|
|
155
169
|
|
|
170
|
+
`fetchkit run --help` and `fetchkit validate --help` print the YAML config format
|
|
171
|
+
inline (top-level keys, a minimal example, and the fetcher type list), so simple
|
|
172
|
+
configs need no schema round-trip.
|
|
173
|
+
|
|
156
174
|
`fetchkit schema` lets an agent discover what it can fetch — it prints the JSON
|
|
157
175
|
Schema (with field descriptions) for the top-level config, the shared HTTP
|
|
158
176
|
settings, every builtin fetcher's typed config, and the canonical `Post` output —
|
|
159
|
-
so a config can be written without knowing the YAML format out of band
|
|
177
|
+
so a config can be written without knowing the YAML format out of band. Pass a
|
|
178
|
+
section name to print just that part instead of the full ~50 KB document:
|
|
160
179
|
|
|
161
180
|
```bash
|
|
162
|
-
fetchkit schema | jq '.fetchers | keys' # ["arxiv","github","hackernews","lobsters","rss"]
|
|
181
|
+
fetchkit schema | jq '.fetchers | keys' # ["arxiv","bluesky","github","hackernews","lobsters","mastodon","rss","stackexchange"]
|
|
182
|
+
fetchkit schema config # just the top-level config schema
|
|
183
|
+
fetchkit schema rss # just one fetcher's config schema
|
|
184
|
+
fetchkit schema post # just the output Post model
|
|
163
185
|
fetchkit schema -o schema.json # write to a file; supports --compact too
|
|
164
186
|
```
|
|
165
187
|
|
|
166
|
-
Output shape
|
|
188
|
+
Output shape — `sources` reports per-fetcher success/failure (one entry per
|
|
189
|
+
enabled fetcher, in config order), so partial failures are visible without
|
|
190
|
+
diffing domains against `posts`. A source can partially fail, e.g. one dead
|
|
191
|
+
feed among many: it keeps its posts, records the error, and reports `ok: false`.
|
|
167
192
|
|
|
168
193
|
```json
|
|
169
194
|
{
|
|
170
195
|
"count": 2,
|
|
171
196
|
"posts": [ { "id": "...", "source": "rss", "title": "...", "url": "...", "created_at": "..." } ],
|
|
172
|
-
"
|
|
197
|
+
"sources": [
|
|
198
|
+
{ "source": "rss", "name": null, "ok": false, "posts": 2,
|
|
199
|
+
"errors": ["Failed to fetch feed https://dead.example/feed.xml: ..."] }
|
|
200
|
+
],
|
|
201
|
+
"errors": [ { "source": "rss", "error": "Failed to fetch feed https://dead.example/feed.xml: ..." } ]
|
|
173
202
|
}
|
|
174
203
|
```
|
|
175
204
|
|
|
@@ -194,11 +223,12 @@ result = collect_all(config)
|
|
|
194
223
|
|
|
195
224
|
## Discovering RSS feeds
|
|
196
225
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
feeds exist. The optional `discovery` module closes that gap — give it a
|
|
226
|
+
RSS is the hardest source to discover for: a feed is an *arbitrary URL*, so an
|
|
227
|
+
agent that wants "central-bank policy" or "rust programming" has no way to know
|
|
228
|
+
which feeds exist. The optional `discovery` module closes that gap — give it a
|
|
201
229
|
use case, get back ranked feeds you can drop straight into an `rss` fetcher.
|
|
230
|
+
(The other sources have their own, simpler discovery surface — see
|
|
231
|
+
[Discoverability for every source](#discoverability-for-every-source) below.)
|
|
202
232
|
|
|
203
233
|
```python
|
|
204
234
|
from fetchkit.discovery import discover, to_rss_config
|
|
@@ -255,6 +285,48 @@ not the live article stream — for the latter, fetch the feed with the `rss`
|
|
|
255
285
|
fetcher and match individual posts. Catalog quality is the main lever on result
|
|
256
286
|
quality; see `src/fetchkit/discovery/data/CATALOG.md` to extend it.
|
|
257
287
|
|
|
288
|
+
Because discovery only *ranks* candidates it already holds, a niche query (a
|
|
289
|
+
specific region, product, or community) can come back filled with generic feeds
|
|
290
|
+
that matched only the query's broad terms. The CLI flags this instead of letting
|
|
291
|
+
it pass: the JSON payload carries `unmatched_terms` (query terms no returned
|
|
292
|
+
feed mentions) plus a `hint`, and the same hint is printed to stderr. When you
|
|
293
|
+
see it, feed discovery better candidates — `--from-urls` with sites from your
|
|
294
|
+
own web search, `fetchkit find-feeds <site-url>`, or `--external`.
|
|
295
|
+
|
|
296
|
+
## Discoverability for every source
|
|
297
|
+
|
|
298
|
+
`discover` solves RSS's "which feed URL?" problem. The other sources have a
|
|
299
|
+
*different* discoverability gap — *which tag / site / instance / feed do I put in
|
|
300
|
+
the config?* — answered by `fetchkit suggest <source>` (and `run_suggester()` in
|
|
301
|
+
Python). Each suggester is **no-auth** and returns JSON-ready rows:
|
|
302
|
+
|
|
303
|
+
```bash
|
|
304
|
+
fetchkit suggest lobsters # all Lobsters tags
|
|
305
|
+
fetchkit suggest stackexchange --site stackoverflow # popular SO tags
|
|
306
|
+
fetchkit suggest stackexchange --what sites # available SE sites
|
|
307
|
+
fetchkit suggest arxiv --query vision # matching arXiv categories
|
|
308
|
+
fetchkit suggest github --query "language:rust stars:>5000" # popular repos to watch
|
|
309
|
+
fetchkit suggest mastodon --instance fosstodon.org # trending hashtags
|
|
310
|
+
fetchkit suggest bluesky # popular custom feeds
|
|
311
|
+
fetchkit suggest bluesky --what actors --query "ai" # accounts for an author_feed
|
|
312
|
+
fetchkit suggest rss --query "AI safety news" # delegates to discover()
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
| Source | `suggest` returns | Fills config field |
|
|
316
|
+
|--------|-------------------|--------------------|
|
|
317
|
+
| `hackernews` | sort orders (static — HN has no tags) | `posts.order` |
|
|
318
|
+
| `rss` | ranked feeds (delegates to `discover`) | `feeds` |
|
|
319
|
+
| `arxiv` | category codes + names (static taxonomy) | `categories` |
|
|
320
|
+
| `github` | popular `owner/name` repos for a query | `repos` |
|
|
321
|
+
| `lobsters` | all tags (from `/tags.json`) | `tag` |
|
|
322
|
+
| `stackexchange` | popular tags, or sites (`--what sites`) | `tagged` / `site` |
|
|
323
|
+
| `bluesky` | popular feeds, or actors (`--what actors`) | `actor` |
|
|
324
|
+
| `mastodon` | trending hashtags on an instance | `tag` / `instance` |
|
|
325
|
+
|
|
326
|
+
> Most suggesters call the source's live API (all no-auth); `hackernews`/`arxiv`
|
|
327
|
+
> are static, and `rss` reuses the offline catalog ranker. Network suggesters fail
|
|
328
|
+
> gracefully (the CLI reports the error on stderr and exits non-zero).
|
|
329
|
+
|
|
258
330
|
## Configuration reference
|
|
259
331
|
|
|
260
332
|
### Top-level (`FetchKitConfig`)
|
|
@@ -271,7 +343,9 @@ Set the time window with **either** a relative `window` **or** both
|
|
|
271
343
|
`start_time`/`end_time` — the two are mutually exclusive. **If you specify none of
|
|
272
344
|
them, the window defaults to the last 24 hours.** `start_time <= end_time` is
|
|
273
345
|
enforced, and unknown top-level keys are rejected. Per-fetcher
|
|
274
|
-
`start_time`/`end_time` default to `None` and **inherit** the
|
|
346
|
+
`start_time`/`end_time` default to `None` and **inherit** the run window from these
|
|
347
|
+
top-level keys at runtime (there is no `global:` key — the top level *is* the
|
|
348
|
+
global scope).
|
|
275
349
|
|
|
276
350
|
#### Relative windows
|
|
277
351
|
|
|
@@ -352,6 +426,47 @@ The lobste.rs public JSON endpoints (no auth). Tags are preserved in `post.metad
|
|
|
352
426
|
| `tag` | null | Restrict to a single tag (uses `/t/<tag>.json`) |
|
|
353
427
|
| `max_items`| 50 | 1–200 |
|
|
354
428
|
|
|
429
|
+
### Stack Exchange (`type: stackexchange`)
|
|
430
|
+
|
|
431
|
+
The Stack Exchange API (no auth; anonymous access is capped at 300 requests/day/IP).
|
|
432
|
+
Questions become `Post`s; with `comments.fetch: true`, top answers are attached as
|
|
433
|
+
`Comment`s. Tags, answer count, and the accepted-answer id are kept in `post.metadata`.
|
|
434
|
+
|
|
435
|
+
| Field | Default | Notes |
|
|
436
|
+
|------------|----------------|-------------------------------------------------------------|
|
|
437
|
+
| `site` | stackoverflow | API site, e.g. `serverfault`, `superuser`, `askubuntu` |
|
|
438
|
+
| `tagged` | `[]` | Questions carrying ALL of these tags (joined with `;`) |
|
|
439
|
+
| `query` | null | Free-text search (uses `/search/advanced`) |
|
|
440
|
+
| `posts` | — | `max_items` (1–500) and `order` (`top`→votes, `new`→creation) |
|
|
441
|
+
| `comments` | — | `fetch`/`max_items` — answers attached as comments |
|
|
442
|
+
|
|
443
|
+
### Bluesky (`type: bluesky`)
|
|
444
|
+
|
|
445
|
+
The public Bluesky AppView (`public.api.bsky.app`), unauthenticated by design.
|
|
446
|
+
`uri`, `cid`, and `langs` are kept in `post.metadata`; likes map to `score` and
|
|
447
|
+
replies to `comment_count`.
|
|
448
|
+
|
|
449
|
+
| Field | Default | Notes |
|
|
450
|
+
|------------|---------|--------------------------------------------------------------|
|
|
451
|
+
| `resource` | search | `search` (full-text) or `author_feed` |
|
|
452
|
+
| `query` | null | Search query — required for `resource: search` |
|
|
453
|
+
| `actor` | null | Handle/DID — required for `resource: author_feed` |
|
|
454
|
+
| `max_items`| 50 | 1–500 (paginated, 100/page) |
|
|
455
|
+
|
|
456
|
+
### Mastodon (`type: mastodon`)
|
|
457
|
+
|
|
458
|
+
Public and hashtag timelines on any instance (no auth, when the instance keeps
|
|
459
|
+
public preview enabled — otherwise requests return 401). HTML content is reduced to
|
|
460
|
+
plain text; tags, instance, and visibility are kept in `post.metadata`.
|
|
461
|
+
|
|
462
|
+
| Field | Default | Notes |
|
|
463
|
+
|------------|------------------|--------------------------------------------------------|
|
|
464
|
+
| `instance` | mastodon.social | Instance host without scheme, e.g. `fosstodon.org` |
|
|
465
|
+
| `resource` | tag | `tag` (`/timelines/tag/<tag>`) or `public` |
|
|
466
|
+
| `tag` | null | Hashtag without `#` — required for `resource: tag` |
|
|
467
|
+
| `local` | false | Restrict to statuses originating on this instance |
|
|
468
|
+
| `max_items`| 50 | 1–200 (paginated, 40/page) |
|
|
469
|
+
|
|
355
470
|
### HTTP (`http:`)
|
|
356
471
|
|
|
357
472
|
| Field | Default | Notes |
|
|
@@ -384,6 +499,15 @@ validation, and tests. Add a new source via a PR or a local fork in four steps:
|
|
|
384
499
|
|
|
385
500
|
3. Import the module in `src/fetchkit/fetchers/__init__.py` so it registers on import.
|
|
386
501
|
4. Add tests (mock HTTP with the `responses` library — see `tests/fetchers/`).
|
|
502
|
+
5. *(Optional)* register a discovery helper so agents can find your source's knobs:
|
|
503
|
+
|
|
504
|
+
```python
|
|
505
|
+
from fetchkit.fetchers.suggest_registry import register_suggester
|
|
506
|
+
|
|
507
|
+
@register_suggester("mysource")
|
|
508
|
+
def suggest(*, query=None, limit=20, **kwargs) -> list[dict]:
|
|
509
|
+
return [{"tag": "…"}] # JSON-ready rows; surfaced via `fetchkit suggest mysource`
|
|
510
|
+
```
|
|
387
511
|
|
|
388
512
|
Use `Post.metadata` for any source-specific fields that don't map to the canonical
|
|
389
513
|
columns.
|
|
@@ -394,6 +518,7 @@ columns.
|
|
|
394
518
|
class Post(BaseModel):
|
|
395
519
|
id: str # unique within source
|
|
396
520
|
source: str # "hackernews" | "rss" | "arxiv" | "github" | "lobsters"
|
|
521
|
+
# | "stackexchange" | "bluesky" | "mastodon"
|
|
397
522
|
title: str | None
|
|
398
523
|
text: str | None # body / content
|
|
399
524
|
url: str | None # external link
|
|
@@ -410,8 +535,9 @@ All datetimes are normalized to UTC. Posts are deduplicated by `(source, id)` an
|
|
|
410
535
|
sorted descending by `(created_at, id)` for deterministic output.
|
|
411
536
|
|
|
412
537
|
> **`score` is source-relative.** Each source defines it differently — Hacker News
|
|
413
|
-
> points, Lobsters score, GitHub stars (for `search_repos`),
|
|
414
|
-
>
|
|
538
|
+
> points, Lobsters score, GitHub stars (for `search_repos`), Stack Exchange question
|
|
539
|
+
> score, Bluesky likes, Mastodon favourites, and `None` for arXiv and RSS. The values
|
|
540
|
+
> are **not comparable across sources**, so don't rank a mixed
|
|
415
541
|
> feed by `score` directly. Compare within a single `source`, or use a source-aware
|
|
416
542
|
> ranking of your own. Output is ordered by recency (`created_at`), not by `score`.
|
|
417
543
|
|
|
@@ -8,15 +8,16 @@
|
|
|
8
8
|
A YAML-configured data-fetching library for agentic applications.
|
|
9
9
|
|
|
10
10
|
`fetchkit` collects posts and comments from sources (Hacker News, RSS/Atom, arXiv,
|
|
11
|
-
GitHub, Lobsters) into a single canonical `Post`
|
|
11
|
+
GitHub, Lobsters, Stack Exchange, Bluesky, Mastodon) into a single canonical `Post`
|
|
12
|
+
model, with de-duplication,
|
|
12
13
|
deterministic sorting, and a shared HTTP client with retries and rate limiting. It is
|
|
13
14
|
designed as the data-collection layer for LLM/agent pipelines — feed it configs, get
|
|
14
15
|
back clean typed data.
|
|
15
16
|
|
|
16
17
|
- **YAML-first** configuration with strict validation: unknown or misspelled keys
|
|
17
18
|
are rejected up front, so a bad config fails loudly instead of silently.
|
|
18
|
-
- **Builtin fetchers**: Hacker News, RSS/Atom, arXiv, GitHub,
|
|
19
|
-
zero-auth.
|
|
19
|
+
- **Builtin fetchers**: Hacker News, RSS/Atom, arXiv, GitHub, Lobsters, Stack Exchange,
|
|
20
|
+
Bluesky, and Mastodon — all zero-auth.
|
|
20
21
|
- **Relative time windows**: say `window: "last 6 hours"` instead of computing
|
|
21
22
|
timestamps.
|
|
22
23
|
- **Open `metadata`**: each `Post` carries a `metadata` dict for source-specific
|
|
@@ -31,6 +32,9 @@ back clean typed data.
|
|
|
31
32
|
no Python required.
|
|
32
33
|
- **RSS feed discovery**: `fetchkit discover "<use case>"` maps a natural-language
|
|
33
34
|
query onto real RSS feeds an agent can fetch — closing the "which feed URL?" gap.
|
|
35
|
+
- **Per-source discovery**: `fetchkit suggest <source>` lists the selectable knobs
|
|
36
|
+
for any source (tags, sites, arXiv categories, trending hashtags, Bluesky feeds) —
|
|
37
|
+
all zero-auth — so an agent can fill a config without guessing.
|
|
34
38
|
|
|
35
39
|
## Install
|
|
36
40
|
|
|
@@ -65,6 +69,15 @@ fetchers:
|
|
|
65
69
|
repos: ["python/cpython", "pydantic/pydantic"]
|
|
66
70
|
- type: lobsters
|
|
67
71
|
listing: hottest
|
|
72
|
+
- type: stackexchange
|
|
73
|
+
site: stackoverflow
|
|
74
|
+
tagged: ["python", "asyncio"]
|
|
75
|
+
- type: bluesky
|
|
76
|
+
resource: search
|
|
77
|
+
query: "large language models"
|
|
78
|
+
- type: mastodon
|
|
79
|
+
instance: mastodon.social
|
|
80
|
+
tag: ai
|
|
68
81
|
- type: rss
|
|
69
82
|
feeds:
|
|
70
83
|
- url: "https://feeds.bbci.co.uk/news/rss.xml"
|
|
@@ -101,33 +114,49 @@ Python. `run` prints **only** JSON to stdout (diagnostics go to stderr), so it p
|
|
|
101
114
|
cleanly into `jq`:
|
|
102
115
|
|
|
103
116
|
```bash
|
|
104
|
-
fetchkit run config.yaml # pretty JSON: {"count", "posts", "errors"}
|
|
117
|
+
fetchkit run config.yaml # pretty JSON: {"count", "posts", "sources", "errors"}
|
|
105
118
|
fetchkit run config.yaml -o out.json # write JSON to a file (stdout stays empty)
|
|
106
119
|
fetchkit run config.yaml --window "6h" # override the time window at runtime
|
|
107
120
|
fetchkit run config.yaml --compact # single-line JSON
|
|
108
121
|
fetchkit run config.yaml --fail-on-error # exit 1 if any source failed
|
|
109
|
-
fetchkit
|
|
122
|
+
echo "$CONFIG_YAML" | fetchkit run - # read the config from stdin
|
|
123
|
+
fetchkit validate config.yaml # validate a config without fetching (also accepts -)
|
|
110
124
|
fetchkit schema # JSON Schema for every config/output model
|
|
111
125
|
python -m fetchkit run config.yaml # module form, identical behavior
|
|
112
126
|
```
|
|
113
127
|
|
|
128
|
+
`fetchkit run --help` and `fetchkit validate --help` print the YAML config format
|
|
129
|
+
inline (top-level keys, a minimal example, and the fetcher type list), so simple
|
|
130
|
+
configs need no schema round-trip.
|
|
131
|
+
|
|
114
132
|
`fetchkit schema` lets an agent discover what it can fetch — it prints the JSON
|
|
115
133
|
Schema (with field descriptions) for the top-level config, the shared HTTP
|
|
116
134
|
settings, every builtin fetcher's typed config, and the canonical `Post` output —
|
|
117
|
-
so a config can be written without knowing the YAML format out of band
|
|
135
|
+
so a config can be written without knowing the YAML format out of band. Pass a
|
|
136
|
+
section name to print just that part instead of the full ~50 KB document:
|
|
118
137
|
|
|
119
138
|
```bash
|
|
120
|
-
fetchkit schema | jq '.fetchers | keys' # ["arxiv","github","hackernews","lobsters","rss"]
|
|
139
|
+
fetchkit schema | jq '.fetchers | keys' # ["arxiv","bluesky","github","hackernews","lobsters","mastodon","rss","stackexchange"]
|
|
140
|
+
fetchkit schema config # just the top-level config schema
|
|
141
|
+
fetchkit schema rss # just one fetcher's config schema
|
|
142
|
+
fetchkit schema post # just the output Post model
|
|
121
143
|
fetchkit schema -o schema.json # write to a file; supports --compact too
|
|
122
144
|
```
|
|
123
145
|
|
|
124
|
-
Output shape
|
|
146
|
+
Output shape — `sources` reports per-fetcher success/failure (one entry per
|
|
147
|
+
enabled fetcher, in config order), so partial failures are visible without
|
|
148
|
+
diffing domains against `posts`. A source can partially fail, e.g. one dead
|
|
149
|
+
feed among many: it keeps its posts, records the error, and reports `ok: false`.
|
|
125
150
|
|
|
126
151
|
```json
|
|
127
152
|
{
|
|
128
153
|
"count": 2,
|
|
129
154
|
"posts": [ { "id": "...", "source": "rss", "title": "...", "url": "...", "created_at": "..." } ],
|
|
130
|
-
"
|
|
155
|
+
"sources": [
|
|
156
|
+
{ "source": "rss", "name": null, "ok": false, "posts": 2,
|
|
157
|
+
"errors": ["Failed to fetch feed https://dead.example/feed.xml: ..."] }
|
|
158
|
+
],
|
|
159
|
+
"errors": [ { "source": "rss", "error": "Failed to fetch feed https://dead.example/feed.xml: ..." } ]
|
|
131
160
|
}
|
|
132
161
|
```
|
|
133
162
|
|
|
@@ -152,11 +181,12 @@ result = collect_all(config)
|
|
|
152
181
|
|
|
153
182
|
## Discovering RSS feeds
|
|
154
183
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
feeds exist. The optional `discovery` module closes that gap — give it a
|
|
184
|
+
RSS is the hardest source to discover for: a feed is an *arbitrary URL*, so an
|
|
185
|
+
agent that wants "central-bank policy" or "rust programming" has no way to know
|
|
186
|
+
which feeds exist. The optional `discovery` module closes that gap — give it a
|
|
159
187
|
use case, get back ranked feeds you can drop straight into an `rss` fetcher.
|
|
188
|
+
(The other sources have their own, simpler discovery surface — see
|
|
189
|
+
[Discoverability for every source](#discoverability-for-every-source) below.)
|
|
160
190
|
|
|
161
191
|
```python
|
|
162
192
|
from fetchkit.discovery import discover, to_rss_config
|
|
@@ -213,6 +243,48 @@ not the live article stream — for the latter, fetch the feed with the `rss`
|
|
|
213
243
|
fetcher and match individual posts. Catalog quality is the main lever on result
|
|
214
244
|
quality; see `src/fetchkit/discovery/data/CATALOG.md` to extend it.
|
|
215
245
|
|
|
246
|
+
Because discovery only *ranks* candidates it already holds, a niche query (a
|
|
247
|
+
specific region, product, or community) can come back filled with generic feeds
|
|
248
|
+
that matched only the query's broad terms. The CLI flags this instead of letting
|
|
249
|
+
it pass: the JSON payload carries `unmatched_terms` (query terms no returned
|
|
250
|
+
feed mentions) plus a `hint`, and the same hint is printed to stderr. When you
|
|
251
|
+
see it, feed discovery better candidates — `--from-urls` with sites from your
|
|
252
|
+
own web search, `fetchkit find-feeds <site-url>`, or `--external`.
|
|
253
|
+
|
|
254
|
+
## Discoverability for every source
|
|
255
|
+
|
|
256
|
+
`discover` solves RSS's "which feed URL?" problem. The other sources have a
|
|
257
|
+
*different* discoverability gap — *which tag / site / instance / feed do I put in
|
|
258
|
+
the config?* — answered by `fetchkit suggest <source>` (and `run_suggester()` in
|
|
259
|
+
Python). Each suggester is **no-auth** and returns JSON-ready rows:
|
|
260
|
+
|
|
261
|
+
```bash
|
|
262
|
+
fetchkit suggest lobsters # all Lobsters tags
|
|
263
|
+
fetchkit suggest stackexchange --site stackoverflow # popular SO tags
|
|
264
|
+
fetchkit suggest stackexchange --what sites # available SE sites
|
|
265
|
+
fetchkit suggest arxiv --query vision # matching arXiv categories
|
|
266
|
+
fetchkit suggest github --query "language:rust stars:>5000" # popular repos to watch
|
|
267
|
+
fetchkit suggest mastodon --instance fosstodon.org # trending hashtags
|
|
268
|
+
fetchkit suggest bluesky # popular custom feeds
|
|
269
|
+
fetchkit suggest bluesky --what actors --query "ai" # accounts for an author_feed
|
|
270
|
+
fetchkit suggest rss --query "AI safety news" # delegates to discover()
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
| Source | `suggest` returns | Fills config field |
|
|
274
|
+
|--------|-------------------|--------------------|
|
|
275
|
+
| `hackernews` | sort orders (static — HN has no tags) | `posts.order` |
|
|
276
|
+
| `rss` | ranked feeds (delegates to `discover`) | `feeds` |
|
|
277
|
+
| `arxiv` | category codes + names (static taxonomy) | `categories` |
|
|
278
|
+
| `github` | popular `owner/name` repos for a query | `repos` |
|
|
279
|
+
| `lobsters` | all tags (from `/tags.json`) | `tag` |
|
|
280
|
+
| `stackexchange` | popular tags, or sites (`--what sites`) | `tagged` / `site` |
|
|
281
|
+
| `bluesky` | popular feeds, or actors (`--what actors`) | `actor` |
|
|
282
|
+
| `mastodon` | trending hashtags on an instance | `tag` / `instance` |
|
|
283
|
+
|
|
284
|
+
> Most suggesters call the source's live API (all no-auth); `hackernews`/`arxiv`
|
|
285
|
+
> are static, and `rss` reuses the offline catalog ranker. Network suggesters fail
|
|
286
|
+
> gracefully (the CLI reports the error on stderr and exits non-zero).
|
|
287
|
+
|
|
216
288
|
## Configuration reference
|
|
217
289
|
|
|
218
290
|
### Top-level (`FetchKitConfig`)
|
|
@@ -229,7 +301,9 @@ Set the time window with **either** a relative `window` **or** both
|
|
|
229
301
|
`start_time`/`end_time` — the two are mutually exclusive. **If you specify none of
|
|
230
302
|
them, the window defaults to the last 24 hours.** `start_time <= end_time` is
|
|
231
303
|
enforced, and unknown top-level keys are rejected. Per-fetcher
|
|
232
|
-
`start_time`/`end_time` default to `None` and **inherit** the
|
|
304
|
+
`start_time`/`end_time` default to `None` and **inherit** the run window from these
|
|
305
|
+
top-level keys at runtime (there is no `global:` key — the top level *is* the
|
|
306
|
+
global scope).
|
|
233
307
|
|
|
234
308
|
#### Relative windows
|
|
235
309
|
|
|
@@ -310,6 +384,47 @@ The lobste.rs public JSON endpoints (no auth). Tags are preserved in `post.metad
|
|
|
310
384
|
| `tag` | null | Restrict to a single tag (uses `/t/<tag>.json`) |
|
|
311
385
|
| `max_items`| 50 | 1–200 |
|
|
312
386
|
|
|
387
|
+
### Stack Exchange (`type: stackexchange`)
|
|
388
|
+
|
|
389
|
+
The Stack Exchange API (no auth; anonymous access is capped at 300 requests/day/IP).
|
|
390
|
+
Questions become `Post`s; with `comments.fetch: true`, top answers are attached as
|
|
391
|
+
`Comment`s. Tags, answer count, and the accepted-answer id are kept in `post.metadata`.
|
|
392
|
+
|
|
393
|
+
| Field | Default | Notes |
|
|
394
|
+
|------------|----------------|-------------------------------------------------------------|
|
|
395
|
+
| `site` | stackoverflow | API site, e.g. `serverfault`, `superuser`, `askubuntu` |
|
|
396
|
+
| `tagged` | `[]` | Questions carrying ALL of these tags (joined with `;`) |
|
|
397
|
+
| `query` | null | Free-text search (uses `/search/advanced`) |
|
|
398
|
+
| `posts` | — | `max_items` (1–500) and `order` (`top`→votes, `new`→creation) |
|
|
399
|
+
| `comments` | — | `fetch`/`max_items` — answers attached as comments |
|
|
400
|
+
|
|
401
|
+
### Bluesky (`type: bluesky`)
|
|
402
|
+
|
|
403
|
+
The public Bluesky AppView (`public.api.bsky.app`), unauthenticated by design.
|
|
404
|
+
`uri`, `cid`, and `langs` are kept in `post.metadata`; likes map to `score` and
|
|
405
|
+
replies to `comment_count`.
|
|
406
|
+
|
|
407
|
+
| Field | Default | Notes |
|
|
408
|
+
|------------|---------|--------------------------------------------------------------|
|
|
409
|
+
| `resource` | search | `search` (full-text) or `author_feed` |
|
|
410
|
+
| `query` | null | Search query — required for `resource: search` |
|
|
411
|
+
| `actor` | null | Handle/DID — required for `resource: author_feed` |
|
|
412
|
+
| `max_items`| 50 | 1–500 (paginated, 100/page) |
|
|
413
|
+
|
|
414
|
+
### Mastodon (`type: mastodon`)
|
|
415
|
+
|
|
416
|
+
Public and hashtag timelines on any instance (no auth, when the instance keeps
|
|
417
|
+
public preview enabled — otherwise requests return 401). HTML content is reduced to
|
|
418
|
+
plain text; tags, instance, and visibility are kept in `post.metadata`.
|
|
419
|
+
|
|
420
|
+
| Field | Default | Notes |
|
|
421
|
+
|------------|------------------|--------------------------------------------------------|
|
|
422
|
+
| `instance` | mastodon.social | Instance host without scheme, e.g. `fosstodon.org` |
|
|
423
|
+
| `resource` | tag | `tag` (`/timelines/tag/<tag>`) or `public` |
|
|
424
|
+
| `tag` | null | Hashtag without `#` — required for `resource: tag` |
|
|
425
|
+
| `local` | false | Restrict to statuses originating on this instance |
|
|
426
|
+
| `max_items`| 50 | 1–200 (paginated, 40/page) |
|
|
427
|
+
|
|
313
428
|
### HTTP (`http:`)
|
|
314
429
|
|
|
315
430
|
| Field | Default | Notes |
|
|
@@ -342,6 +457,15 @@ validation, and tests. Add a new source via a PR or a local fork in four steps:
|
|
|
342
457
|
|
|
343
458
|
3. Import the module in `src/fetchkit/fetchers/__init__.py` so it registers on import.
|
|
344
459
|
4. Add tests (mock HTTP with the `responses` library — see `tests/fetchers/`).
|
|
460
|
+
5. *(Optional)* register a discovery helper so agents can find your source's knobs:
|
|
461
|
+
|
|
462
|
+
```python
|
|
463
|
+
from fetchkit.fetchers.suggest_registry import register_suggester
|
|
464
|
+
|
|
465
|
+
@register_suggester("mysource")
|
|
466
|
+
def suggest(*, query=None, limit=20, **kwargs) -> list[dict]:
|
|
467
|
+
return [{"tag": "…"}] # JSON-ready rows; surfaced via `fetchkit suggest mysource`
|
|
468
|
+
```
|
|
345
469
|
|
|
346
470
|
Use `Post.metadata` for any source-specific fields that don't map to the canonical
|
|
347
471
|
columns.
|
|
@@ -352,6 +476,7 @@ columns.
|
|
|
352
476
|
class Post(BaseModel):
|
|
353
477
|
id: str # unique within source
|
|
354
478
|
source: str # "hackernews" | "rss" | "arxiv" | "github" | "lobsters"
|
|
479
|
+
# | "stackexchange" | "bluesky" | "mastodon"
|
|
355
480
|
title: str | None
|
|
356
481
|
text: str | None # body / content
|
|
357
482
|
url: str | None # external link
|
|
@@ -368,8 +493,9 @@ All datetimes are normalized to UTC. Posts are deduplicated by `(source, id)` an
|
|
|
368
493
|
sorted descending by `(created_at, id)` for deterministic output.
|
|
369
494
|
|
|
370
495
|
> **`score` is source-relative.** Each source defines it differently — Hacker News
|
|
371
|
-
> points, Lobsters score, GitHub stars (for `search_repos`),
|
|
372
|
-
>
|
|
496
|
+
> points, Lobsters score, GitHub stars (for `search_repos`), Stack Exchange question
|
|
497
|
+
> score, Bluesky likes, Mastodon favourites, and `None` for arXiv and RSS. The values
|
|
498
|
+
> are **not comparable across sources**, so don't rank a mixed
|
|
373
499
|
> feed by `score` directly. Compare within a single `source`, or use a source-aware
|
|
374
500
|
> ranking of your own. Output is ordered by recency (`created_at`), not by `score`.
|
|
375
501
|
|
|
@@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta"
|
|
|
6
6
|
# Distribution name on PyPI. The importable package and CLI are still `fetchkit`;
|
|
7
7
|
# the `fetchkit` PyPI name was already taken by an unrelated project.
|
|
8
8
|
name = "fetchkit-agents"
|
|
9
|
-
version = "0.
|
|
9
|
+
version = "0.3.1"
|
|
10
10
|
description = "A YAML-configured data fetching library for agentic applications."
|
|
11
11
|
readme = "README.md"
|
|
12
12
|
requires-python = ">=3.10"
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
fetchkit — a YAML-configured data fetching library for agentic applications.
|
|
3
3
|
|
|
4
4
|
Fetch posts and comments from sources (Hacker News, RSS/Atom, arXiv, GitHub,
|
|
5
|
-
Lobsters) into a single canonical ``Post``
|
|
6
|
-
and a typed config. Designed as a
|
|
7
|
-
applications.
|
|
5
|
+
Lobsters, Stack Exchange, Bluesky, Mastodon) into a single canonical ``Post``
|
|
6
|
+
model, with deduplication, sorting, and a typed config. Designed as a
|
|
7
|
+
data-collection layer for agentic / LLM applications.
|
|
8
8
|
|
|
9
9
|
Quick start::
|
|
10
10
|
|
|
@@ -19,7 +19,7 @@ See the README for the full configuration reference and the guide to adding
|
|
|
19
19
|
fetchers.
|
|
20
20
|
"""
|
|
21
21
|
|
|
22
|
-
__version__ = "0.
|
|
22
|
+
__version__ = "0.3.1"
|
|
23
23
|
|
|
24
24
|
from typing import TYPE_CHECKING, Any
|
|
25
25
|
|
|
@@ -27,6 +27,12 @@ from fetchkit.collector import collect_all
|
|
|
27
27
|
from fetchkit.config_loader import load_config, ConfigError
|
|
28
28
|
from fetchkit.fetchers.base import Fetcher, FetcherResult
|
|
29
29
|
from fetchkit.fetchers.registry import register_fetcher, get_fetcher
|
|
30
|
+
from fetchkit.fetchers.suggest_registry import (
|
|
31
|
+
register_suggester,
|
|
32
|
+
get_suggester,
|
|
33
|
+
list_suggesters,
|
|
34
|
+
run_suggester,
|
|
35
|
+
)
|
|
30
36
|
from fetchkit.http import (
|
|
31
37
|
HttpClient,
|
|
32
38
|
RateLimiter,
|
|
@@ -46,6 +52,9 @@ from fetchkit.schemas.fetcher import (
|
|
|
46
52
|
ArxivFetchConfig,
|
|
47
53
|
GitHubFetchConfig,
|
|
48
54
|
LobstersFetchConfig,
|
|
55
|
+
StackExchangeFetchConfig,
|
|
56
|
+
BlueskyFetchConfig,
|
|
57
|
+
MastodonFetchConfig,
|
|
49
58
|
FetcherConfig,
|
|
50
59
|
)
|
|
51
60
|
from fetchkit.schemas.collector import CollectorResult
|
|
@@ -85,6 +94,10 @@ __all__ = [
|
|
|
85
94
|
"FetcherResult",
|
|
86
95
|
"register_fetcher",
|
|
87
96
|
"get_fetcher",
|
|
97
|
+
"register_suggester",
|
|
98
|
+
"get_suggester",
|
|
99
|
+
"list_suggesters",
|
|
100
|
+
"run_suggester",
|
|
88
101
|
# HTTP
|
|
89
102
|
"HttpClient",
|
|
90
103
|
"RateLimiter",
|
|
@@ -105,6 +118,9 @@ __all__ = [
|
|
|
105
118
|
"ArxivFetchConfig",
|
|
106
119
|
"GitHubFetchConfig",
|
|
107
120
|
"LobstersFetchConfig",
|
|
121
|
+
"StackExchangeFetchConfig",
|
|
122
|
+
"BlueskyFetchConfig",
|
|
123
|
+
"MastodonFetchConfig",
|
|
108
124
|
"FetcherConfig",
|
|
109
125
|
"CollectorResult",
|
|
110
126
|
"FetchKitConfig",
|