fetchkit-agents 0.2.0__tar.gz → 0.3.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.
Files changed (54) hide show
  1. {fetchkit_agents-0.2.0 → fetchkit_agents-0.3.0}/PKG-INFO +111 -11
  2. {fetchkit_agents-0.2.0 → fetchkit_agents-0.3.0}/README.md +110 -10
  3. {fetchkit_agents-0.2.0 → fetchkit_agents-0.3.0}/pyproject.toml +1 -1
  4. {fetchkit_agents-0.2.0 → fetchkit_agents-0.3.0}/src/fetchkit/__init__.py +20 -4
  5. {fetchkit_agents-0.2.0 → fetchkit_agents-0.3.0}/src/fetchkit/cli.py +73 -1
  6. fetchkit_agents-0.3.0/src/fetchkit/fetchers/__init__.py +28 -0
  7. {fetchkit_agents-0.2.0 → fetchkit_agents-0.3.0}/src/fetchkit/fetchers/arxiv.py +44 -0
  8. fetchkit_agents-0.3.0/src/fetchkit/fetchers/bluesky.py +176 -0
  9. {fetchkit_agents-0.2.0 → fetchkit_agents-0.3.0}/src/fetchkit/fetchers/github.py +23 -1
  10. {fetchkit_agents-0.2.0 → fetchkit_agents-0.3.0}/src/fetchkit/fetchers/hackernews.py +12 -0
  11. {fetchkit_agents-0.2.0 → fetchkit_agents-0.3.0}/src/fetchkit/fetchers/lobsters.py +20 -0
  12. fetchkit_agents-0.3.0/src/fetchkit/fetchers/mastodon.py +161 -0
  13. {fetchkit_agents-0.2.0 → fetchkit_agents-0.3.0}/src/fetchkit/fetchers/rss.py +12 -0
  14. fetchkit_agents-0.3.0/src/fetchkit/fetchers/stackexchange.py +230 -0
  15. fetchkit_agents-0.3.0/src/fetchkit/fetchers/suggest_registry.py +53 -0
  16. {fetchkit_agents-0.2.0 → fetchkit_agents-0.3.0}/src/fetchkit/schemas/describe.py +50 -2
  17. {fetchkit_agents-0.2.0 → fetchkit_agents-0.3.0}/src/fetchkit/schemas/fetcher.py +76 -0
  18. {fetchkit_agents-0.2.0 → fetchkit_agents-0.3.0}/src/fetchkit/schemas/post.py +3 -0
  19. {fetchkit_agents-0.2.0 → fetchkit_agents-0.3.0}/src/fetchkit_agents.egg-info/PKG-INFO +111 -11
  20. {fetchkit_agents-0.2.0 → fetchkit_agents-0.3.0}/src/fetchkit_agents.egg-info/SOURCES.txt +4 -0
  21. fetchkit_agents-0.2.0/src/fetchkit/fetchers/__init__.py +0 -11
  22. {fetchkit_agents-0.2.0 → fetchkit_agents-0.3.0}/LICENSE +0 -0
  23. {fetchkit_agents-0.2.0 → fetchkit_agents-0.3.0}/setup.cfg +0 -0
  24. {fetchkit_agents-0.2.0 → fetchkit_agents-0.3.0}/src/fetchkit/__main__.py +0 -0
  25. {fetchkit_agents-0.2.0 → fetchkit_agents-0.3.0}/src/fetchkit/collector.py +0 -0
  26. {fetchkit_agents-0.2.0 → fetchkit_agents-0.3.0}/src/fetchkit/config_loader.py +0 -0
  27. {fetchkit_agents-0.2.0 → fetchkit_agents-0.3.0}/src/fetchkit/discovery/__init__.py +0 -0
  28. {fetchkit_agents-0.2.0 → fetchkit_agents-0.3.0}/src/fetchkit/discovery/autodiscover.py +0 -0
  29. {fetchkit_agents-0.2.0 → fetchkit_agents-0.3.0}/src/fetchkit/discovery/catalog.py +0 -0
  30. {fetchkit_agents-0.2.0 → fetchkit_agents-0.3.0}/src/fetchkit/discovery/core.py +0 -0
  31. {fetchkit_agents-0.2.0 → fetchkit_agents-0.3.0}/src/fetchkit/discovery/data/CATALOG.md +0 -0
  32. {fetchkit_agents-0.2.0 → fetchkit_agents-0.3.0}/src/fetchkit/discovery/data/catalog.json +0 -0
  33. {fetchkit_agents-0.2.0 → fetchkit_agents-0.3.0}/src/fetchkit/discovery/embedding.py +0 -0
  34. {fetchkit_agents-0.2.0 → fetchkit_agents-0.3.0}/src/fetchkit/discovery/errors.py +0 -0
  35. {fetchkit_agents-0.2.0 → fetchkit_agents-0.3.0}/src/fetchkit/discovery/external.py +0 -0
  36. {fetchkit_agents-0.2.0 → fetchkit_agents-0.3.0}/src/fetchkit/discovery/ranking.py +0 -0
  37. {fetchkit_agents-0.2.0 → fetchkit_agents-0.3.0}/src/fetchkit/discovery/schemas.py +0 -0
  38. {fetchkit_agents-0.2.0 → fetchkit_agents-0.3.0}/src/fetchkit/fetchers/base.py +0 -0
  39. {fetchkit_agents-0.2.0 → fetchkit_agents-0.3.0}/src/fetchkit/fetchers/registry.py +0 -0
  40. {fetchkit_agents-0.2.0 → fetchkit_agents-0.3.0}/src/fetchkit/http/__init__.py +0 -0
  41. {fetchkit_agents-0.2.0 → fetchkit_agents-0.3.0}/src/fetchkit/http/client.py +0 -0
  42. {fetchkit_agents-0.2.0 → fetchkit_agents-0.3.0}/src/fetchkit/http/rate_limit.py +0 -0
  43. {fetchkit_agents-0.2.0 → fetchkit_agents-0.3.0}/src/fetchkit/http/ssrf.py +0 -0
  44. {fetchkit_agents-0.2.0 → fetchkit_agents-0.3.0}/src/fetchkit/py.typed +0 -0
  45. {fetchkit_agents-0.2.0 → fetchkit_agents-0.3.0}/src/fetchkit/schemas/__init__.py +0 -0
  46. {fetchkit_agents-0.2.0 → fetchkit_agents-0.3.0}/src/fetchkit/schemas/base.py +0 -0
  47. {fetchkit_agents-0.2.0 → fetchkit_agents-0.3.0}/src/fetchkit/schemas/collector.py +0 -0
  48. {fetchkit_agents-0.2.0 → fetchkit_agents-0.3.0}/src/fetchkit/schemas/config.py +0 -0
  49. {fetchkit_agents-0.2.0 → fetchkit_agents-0.3.0}/src/fetchkit/utils/__init__.py +0 -0
  50. {fetchkit_agents-0.2.0 → fetchkit_agents-0.3.0}/src/fetchkit/utils/time.py +0 -0
  51. {fetchkit_agents-0.2.0 → fetchkit_agents-0.3.0}/src/fetchkit_agents.egg-info/dependency_links.txt +0 -0
  52. {fetchkit_agents-0.2.0 → fetchkit_agents-0.3.0}/src/fetchkit_agents.egg-info/entry_points.txt +0 -0
  53. {fetchkit_agents-0.2.0 → fetchkit_agents-0.3.0}/src/fetchkit_agents.egg-info/requires.txt +0 -0
  54. {fetchkit_agents-0.2.0 → fetchkit_agents-0.3.0}/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.2.0
3
+ Version: 0.3.0
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` model, with de-duplication,
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, and Lobsters all
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"
@@ -159,7 +172,7 @@ settings, every builtin fetcher's typed config, and the canonical `Post` output
159
172
  so a config can be written without knowing the YAML format out of band:
160
173
 
161
174
  ```bash
162
- fetchkit schema | jq '.fetchers | keys' # ["arxiv","github","hackernews","lobsters","rss"]
175
+ fetchkit schema | jq '.fetchers | keys' # ["arxiv","bluesky","github","hackernews","lobsters","mastodon","rss","stackexchange"]
163
176
  fetchkit schema -o schema.json # write to a file; supports --compact too
164
177
  ```
165
178
 
@@ -194,11 +207,12 @@ result = collect_all(config)
194
207
 
195
208
  ## Discovering RSS feeds
196
209
 
197
- Every other source has a finite, nameable set of options (`fetchkit schema`
198
- enumerates them). RSS is the exception: a feed is an *arbitrary URL*, so an agent
199
- that wants "central-bank policy" or "rust programming" has no way to know which
200
- feeds exist. The optional `discovery` module closes that gap — give it a
210
+ RSS is the hardest source to discover for: a feed is an *arbitrary URL*, so an
211
+ agent that wants "central-bank policy" or "rust programming" has no way to know
212
+ which feeds exist. The optional `discovery` module closes that gap give it a
201
213
  use case, get back ranked feeds you can drop straight into an `rss` fetcher.
214
+ (The other sources have their own, simpler discovery surface — see
215
+ [Discoverability for every source](#discoverability-for-every-source) below.)
202
216
 
203
217
  ```python
204
218
  from fetchkit.discovery import discover, to_rss_config
@@ -255,6 +269,40 @@ not the live article stream — for the latter, fetch the feed with the `rss`
255
269
  fetcher and match individual posts. Catalog quality is the main lever on result
256
270
  quality; see `src/fetchkit/discovery/data/CATALOG.md` to extend it.
257
271
 
272
+ ## Discoverability for every source
273
+
274
+ `discover` solves RSS's "which feed URL?" problem. The other sources have a
275
+ *different* discoverability gap — *which tag / site / instance / feed do I put in
276
+ the config?* — answered by `fetchkit suggest <source>` (and `run_suggester()` in
277
+ Python). Each suggester is **no-auth** and returns JSON-ready rows:
278
+
279
+ ```bash
280
+ fetchkit suggest lobsters # all Lobsters tags
281
+ fetchkit suggest stackexchange --site stackoverflow # popular SO tags
282
+ fetchkit suggest stackexchange --what sites # available SE sites
283
+ fetchkit suggest arxiv --query vision # matching arXiv categories
284
+ fetchkit suggest github --query "language:rust stars:>5000" # popular repos to watch
285
+ fetchkit suggest mastodon --instance fosstodon.org # trending hashtags
286
+ fetchkit suggest bluesky # popular custom feeds
287
+ fetchkit suggest bluesky --what actors --query "ai" # accounts for an author_feed
288
+ fetchkit suggest rss --query "AI safety news" # delegates to discover()
289
+ ```
290
+
291
+ | Source | `suggest` returns | Fills config field |
292
+ |--------|-------------------|--------------------|
293
+ | `hackernews` | sort orders (static — HN has no tags) | `posts.order` |
294
+ | `rss` | ranked feeds (delegates to `discover`) | `feeds` |
295
+ | `arxiv` | category codes + names (static taxonomy) | `categories` |
296
+ | `github` | popular `owner/name` repos for a query | `repos` |
297
+ | `lobsters` | all tags (from `/tags.json`) | `tag` |
298
+ | `stackexchange` | popular tags, or sites (`--what sites`) | `tagged` / `site` |
299
+ | `bluesky` | popular feeds, or actors (`--what actors`) | `actor` |
300
+ | `mastodon` | trending hashtags on an instance | `tag` / `instance` |
301
+
302
+ > Most suggesters call the source's live API (all no-auth); `hackernews`/`arxiv`
303
+ > are static, and `rss` reuses the offline catalog ranker. Network suggesters fail
304
+ > gracefully (the CLI reports the error on stderr and exits non-zero).
305
+
258
306
  ## Configuration reference
259
307
 
260
308
  ### Top-level (`FetchKitConfig`)
@@ -352,6 +400,47 @@ The lobste.rs public JSON endpoints (no auth). Tags are preserved in `post.metad
352
400
  | `tag` | null | Restrict to a single tag (uses `/t/<tag>.json`) |
353
401
  | `max_items`| 50 | 1–200 |
354
402
 
403
+ ### Stack Exchange (`type: stackexchange`)
404
+
405
+ The Stack Exchange API (no auth; anonymous access is capped at 300 requests/day/IP).
406
+ Questions become `Post`s; with `comments.fetch: true`, top answers are attached as
407
+ `Comment`s. Tags, answer count, and the accepted-answer id are kept in `post.metadata`.
408
+
409
+ | Field | Default | Notes |
410
+ |------------|----------------|-------------------------------------------------------------|
411
+ | `site` | stackoverflow | API site, e.g. `serverfault`, `superuser`, `askubuntu` |
412
+ | `tagged` | `[]` | Questions carrying ALL of these tags (joined with `;`) |
413
+ | `query` | null | Free-text search (uses `/search/advanced`) |
414
+ | `posts` | — | `max_items` (1–500) and `order` (`top`→votes, `new`→creation) |
415
+ | `comments` | — | `fetch`/`max_items` — answers attached as comments |
416
+
417
+ ### Bluesky (`type: bluesky`)
418
+
419
+ The public Bluesky AppView (`public.api.bsky.app`), unauthenticated by design.
420
+ `uri`, `cid`, and `langs` are kept in `post.metadata`; likes map to `score` and
421
+ replies to `comment_count`.
422
+
423
+ | Field | Default | Notes |
424
+ |------------|---------|--------------------------------------------------------------|
425
+ | `resource` | search | `search` (full-text) or `author_feed` |
426
+ | `query` | null | Search query — required for `resource: search` |
427
+ | `actor` | null | Handle/DID — required for `resource: author_feed` |
428
+ | `max_items`| 50 | 1–500 (paginated, 100/page) |
429
+
430
+ ### Mastodon (`type: mastodon`)
431
+
432
+ Public and hashtag timelines on any instance (no auth, when the instance keeps
433
+ public preview enabled — otherwise requests return 401). HTML content is reduced to
434
+ plain text; tags, instance, and visibility are kept in `post.metadata`.
435
+
436
+ | Field | Default | Notes |
437
+ |------------|------------------|--------------------------------------------------------|
438
+ | `instance` | mastodon.social | Instance host without scheme, e.g. `fosstodon.org` |
439
+ | `resource` | tag | `tag` (`/timelines/tag/<tag>`) or `public` |
440
+ | `tag` | null | Hashtag without `#` — required for `resource: tag` |
441
+ | `local` | false | Restrict to statuses originating on this instance |
442
+ | `max_items`| 50 | 1–200 (paginated, 40/page) |
443
+
355
444
  ### HTTP (`http:`)
356
445
 
357
446
  | Field | Default | Notes |
@@ -384,6 +473,15 @@ validation, and tests. Add a new source via a PR or a local fork in four steps:
384
473
 
385
474
  3. Import the module in `src/fetchkit/fetchers/__init__.py` so it registers on import.
386
475
  4. Add tests (mock HTTP with the `responses` library — see `tests/fetchers/`).
476
+ 5. *(Optional)* register a discovery helper so agents can find your source's knobs:
477
+
478
+ ```python
479
+ from fetchkit.fetchers.suggest_registry import register_suggester
480
+
481
+ @register_suggester("mysource")
482
+ def suggest(*, query=None, limit=20, **kwargs) -> list[dict]:
483
+ return [{"tag": "…"}] # JSON-ready rows; surfaced via `fetchkit suggest mysource`
484
+ ```
387
485
 
388
486
  Use `Post.metadata` for any source-specific fields that don't map to the canonical
389
487
  columns.
@@ -394,6 +492,7 @@ columns.
394
492
  class Post(BaseModel):
395
493
  id: str # unique within source
396
494
  source: str # "hackernews" | "rss" | "arxiv" | "github" | "lobsters"
495
+ # | "stackexchange" | "bluesky" | "mastodon"
397
496
  title: str | None
398
497
  text: str | None # body / content
399
498
  url: str | None # external link
@@ -410,8 +509,9 @@ All datetimes are normalized to UTC. Posts are deduplicated by `(source, id)` an
410
509
  sorted descending by `(created_at, id)` for deterministic output.
411
510
 
412
511
  > **`score` is source-relative.** Each source defines it differently — Hacker News
413
- > points, Lobsters score, GitHub stars (for `search_repos`), and `None` for arXiv
414
- > and RSS. The values are **not comparable across sources**, so don't rank a mixed
512
+ > points, Lobsters score, GitHub stars (for `search_repos`), Stack Exchange question
513
+ > score, Bluesky likes, Mastodon favourites, and `None` for arXiv and RSS. The values
514
+ > are **not comparable across sources**, so don't rank a mixed
415
515
  > feed by `score` directly. Compare within a single `source`, or use a source-aware
416
516
  > ranking of your own. Output is ordered by recency (`created_at`), not by `score`.
417
517
 
@@ -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` model, with de-duplication,
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, and Lobsters all
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"
@@ -117,7 +130,7 @@ settings, every builtin fetcher's typed config, and the canonical `Post` output
117
130
  so a config can be written without knowing the YAML format out of band:
118
131
 
119
132
  ```bash
120
- fetchkit schema | jq '.fetchers | keys' # ["arxiv","github","hackernews","lobsters","rss"]
133
+ fetchkit schema | jq '.fetchers | keys' # ["arxiv","bluesky","github","hackernews","lobsters","mastodon","rss","stackexchange"]
121
134
  fetchkit schema -o schema.json # write to a file; supports --compact too
122
135
  ```
123
136
 
@@ -152,11 +165,12 @@ result = collect_all(config)
152
165
 
153
166
  ## Discovering RSS feeds
154
167
 
155
- Every other source has a finite, nameable set of options (`fetchkit schema`
156
- enumerates them). RSS is the exception: a feed is an *arbitrary URL*, so an agent
157
- that wants "central-bank policy" or "rust programming" has no way to know which
158
- feeds exist. The optional `discovery` module closes that gap — give it a
168
+ RSS is the hardest source to discover for: a feed is an *arbitrary URL*, so an
169
+ agent that wants "central-bank policy" or "rust programming" has no way to know
170
+ which feeds exist. The optional `discovery` module closes that gap give it a
159
171
  use case, get back ranked feeds you can drop straight into an `rss` fetcher.
172
+ (The other sources have their own, simpler discovery surface — see
173
+ [Discoverability for every source](#discoverability-for-every-source) below.)
160
174
 
161
175
  ```python
162
176
  from fetchkit.discovery import discover, to_rss_config
@@ -213,6 +227,40 @@ not the live article stream — for the latter, fetch the feed with the `rss`
213
227
  fetcher and match individual posts. Catalog quality is the main lever on result
214
228
  quality; see `src/fetchkit/discovery/data/CATALOG.md` to extend it.
215
229
 
230
+ ## Discoverability for every source
231
+
232
+ `discover` solves RSS's "which feed URL?" problem. The other sources have a
233
+ *different* discoverability gap — *which tag / site / instance / feed do I put in
234
+ the config?* — answered by `fetchkit suggest <source>` (and `run_suggester()` in
235
+ Python). Each suggester is **no-auth** and returns JSON-ready rows:
236
+
237
+ ```bash
238
+ fetchkit suggest lobsters # all Lobsters tags
239
+ fetchkit suggest stackexchange --site stackoverflow # popular SO tags
240
+ fetchkit suggest stackexchange --what sites # available SE sites
241
+ fetchkit suggest arxiv --query vision # matching arXiv categories
242
+ fetchkit suggest github --query "language:rust stars:>5000" # popular repos to watch
243
+ fetchkit suggest mastodon --instance fosstodon.org # trending hashtags
244
+ fetchkit suggest bluesky # popular custom feeds
245
+ fetchkit suggest bluesky --what actors --query "ai" # accounts for an author_feed
246
+ fetchkit suggest rss --query "AI safety news" # delegates to discover()
247
+ ```
248
+
249
+ | Source | `suggest` returns | Fills config field |
250
+ |--------|-------------------|--------------------|
251
+ | `hackernews` | sort orders (static — HN has no tags) | `posts.order` |
252
+ | `rss` | ranked feeds (delegates to `discover`) | `feeds` |
253
+ | `arxiv` | category codes + names (static taxonomy) | `categories` |
254
+ | `github` | popular `owner/name` repos for a query | `repos` |
255
+ | `lobsters` | all tags (from `/tags.json`) | `tag` |
256
+ | `stackexchange` | popular tags, or sites (`--what sites`) | `tagged` / `site` |
257
+ | `bluesky` | popular feeds, or actors (`--what actors`) | `actor` |
258
+ | `mastodon` | trending hashtags on an instance | `tag` / `instance` |
259
+
260
+ > Most suggesters call the source's live API (all no-auth); `hackernews`/`arxiv`
261
+ > are static, and `rss` reuses the offline catalog ranker. Network suggesters fail
262
+ > gracefully (the CLI reports the error on stderr and exits non-zero).
263
+
216
264
  ## Configuration reference
217
265
 
218
266
  ### Top-level (`FetchKitConfig`)
@@ -310,6 +358,47 @@ The lobste.rs public JSON endpoints (no auth). Tags are preserved in `post.metad
310
358
  | `tag` | null | Restrict to a single tag (uses `/t/<tag>.json`) |
311
359
  | `max_items`| 50 | 1–200 |
312
360
 
361
+ ### Stack Exchange (`type: stackexchange`)
362
+
363
+ The Stack Exchange API (no auth; anonymous access is capped at 300 requests/day/IP).
364
+ Questions become `Post`s; with `comments.fetch: true`, top answers are attached as
365
+ `Comment`s. Tags, answer count, and the accepted-answer id are kept in `post.metadata`.
366
+
367
+ | Field | Default | Notes |
368
+ |------------|----------------|-------------------------------------------------------------|
369
+ | `site` | stackoverflow | API site, e.g. `serverfault`, `superuser`, `askubuntu` |
370
+ | `tagged` | `[]` | Questions carrying ALL of these tags (joined with `;`) |
371
+ | `query` | null | Free-text search (uses `/search/advanced`) |
372
+ | `posts` | — | `max_items` (1–500) and `order` (`top`→votes, `new`→creation) |
373
+ | `comments` | — | `fetch`/`max_items` — answers attached as comments |
374
+
375
+ ### Bluesky (`type: bluesky`)
376
+
377
+ The public Bluesky AppView (`public.api.bsky.app`), unauthenticated by design.
378
+ `uri`, `cid`, and `langs` are kept in `post.metadata`; likes map to `score` and
379
+ replies to `comment_count`.
380
+
381
+ | Field | Default | Notes |
382
+ |------------|---------|--------------------------------------------------------------|
383
+ | `resource` | search | `search` (full-text) or `author_feed` |
384
+ | `query` | null | Search query — required for `resource: search` |
385
+ | `actor` | null | Handle/DID — required for `resource: author_feed` |
386
+ | `max_items`| 50 | 1–500 (paginated, 100/page) |
387
+
388
+ ### Mastodon (`type: mastodon`)
389
+
390
+ Public and hashtag timelines on any instance (no auth, when the instance keeps
391
+ public preview enabled — otherwise requests return 401). HTML content is reduced to
392
+ plain text; tags, instance, and visibility are kept in `post.metadata`.
393
+
394
+ | Field | Default | Notes |
395
+ |------------|------------------|--------------------------------------------------------|
396
+ | `instance` | mastodon.social | Instance host without scheme, e.g. `fosstodon.org` |
397
+ | `resource` | tag | `tag` (`/timelines/tag/<tag>`) or `public` |
398
+ | `tag` | null | Hashtag without `#` — required for `resource: tag` |
399
+ | `local` | false | Restrict to statuses originating on this instance |
400
+ | `max_items`| 50 | 1–200 (paginated, 40/page) |
401
+
313
402
  ### HTTP (`http:`)
314
403
 
315
404
  | Field | Default | Notes |
@@ -342,6 +431,15 @@ validation, and tests. Add a new source via a PR or a local fork in four steps:
342
431
 
343
432
  3. Import the module in `src/fetchkit/fetchers/__init__.py` so it registers on import.
344
433
  4. Add tests (mock HTTP with the `responses` library — see `tests/fetchers/`).
434
+ 5. *(Optional)* register a discovery helper so agents can find your source's knobs:
435
+
436
+ ```python
437
+ from fetchkit.fetchers.suggest_registry import register_suggester
438
+
439
+ @register_suggester("mysource")
440
+ def suggest(*, query=None, limit=20, **kwargs) -> list[dict]:
441
+ return [{"tag": "…"}] # JSON-ready rows; surfaced via `fetchkit suggest mysource`
442
+ ```
345
443
 
346
444
  Use `Post.metadata` for any source-specific fields that don't map to the canonical
347
445
  columns.
@@ -352,6 +450,7 @@ columns.
352
450
  class Post(BaseModel):
353
451
  id: str # unique within source
354
452
  source: str # "hackernews" | "rss" | "arxiv" | "github" | "lobsters"
453
+ # | "stackexchange" | "bluesky" | "mastodon"
355
454
  title: str | None
356
455
  text: str | None # body / content
357
456
  url: str | None # external link
@@ -368,8 +467,9 @@ All datetimes are normalized to UTC. Posts are deduplicated by `(source, id)` an
368
467
  sorted descending by `(created_at, id)` for deterministic output.
369
468
 
370
469
  > **`score` is source-relative.** Each source defines it differently — Hacker News
371
- > points, Lobsters score, GitHub stars (for `search_repos`), and `None` for arXiv
372
- > and RSS. The values are **not comparable across sources**, so don't rank a mixed
470
+ > points, Lobsters score, GitHub stars (for `search_repos`), Stack Exchange question
471
+ > score, Bluesky likes, Mastodon favourites, and `None` for arXiv and RSS. The values
472
+ > are **not comparable across sources**, so don't rank a mixed
373
473
  > feed by `score` directly. Compare within a single `source`, or use a source-aware
374
474
  > ranking of your own. Output is ordered by recency (`created_at`), not by `score`.
375
475
 
@@ -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.2.0"
9
+ version = "0.3.0"
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`` model, with deduplication, sorting,
6
- and a typed config. Designed as a data-collection layer for agentic / LLM
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.2.0"
22
+ __version__ = "0.3.0"
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",
@@ -10,8 +10,9 @@ YAML spec and parse structured JSON from stdout::
10
10
  fetchkit schema # JSON Schema for every config/output model
11
11
  fetchkit discover "AI safety news" # find RSS feeds for a use case
12
12
  fetchkit find-feeds https://example.com # autodiscover feeds from a site
13
+ fetchkit suggest lobsters # list what you can query for a source
13
14
 
14
- stdout carries only JSON (for ``run``, ``schema``, ``discover``, and
15
+ stdout carries only JSON (for ``run``, ``schema``, ``discover``, ``suggest``, and
15
16
  ``find-feeds``) so it is safe to pipe into ``jq`` or parse programmatically.
16
17
  Diagnostics and ``--verbose`` logging go to stderr. ``schema`` lets an agent
17
18
  discover the available fetchers and their options without being told the YAML
@@ -163,6 +164,31 @@ def _cmd_discover(args: argparse.Namespace) -> int:
163
164
  return 0
164
165
 
165
166
 
167
+ def _cmd_suggest(args: argparse.Namespace) -> int:
168
+ """List what you can query for a source (tags, sites, categories, feeds); emit JSON."""
169
+ from fetchkit.fetchers import run_suggester
170
+
171
+ params: dict[str, Any] = {"limit": args.limit}
172
+ for key in ("query", "site", "instance", "what"):
173
+ value = getattr(args, key)
174
+ if value is not None:
175
+ params[key] = value
176
+
177
+ try:
178
+ suggestions = run_suggester(args.source, **params)
179
+ except ValueError as exc:
180
+ print(str(exc), file=sys.stderr)
181
+ return 2
182
+ except Exception as exc: # network/HTTP failure — report, don't crash
183
+ print(f"suggest failed for {args.source}: {exc}", file=sys.stderr)
184
+ return 2
185
+
186
+ indent = None if args.compact else args.indent
187
+ payload = {"source": args.source, "count": len(suggestions), "suggestions": suggestions}
188
+ _emit_json(payload, indent, args.output)
189
+ return 0
190
+
191
+
166
192
  def _cmd_find_feeds(args: argparse.Namespace) -> int:
167
193
  """Autodiscover RSS/Atom feeds from one or more site URLs; emit JSON."""
168
194
  from fetchkit.discovery import find_feeds
@@ -294,6 +320,52 @@ def build_parser() -> argparse.ArgumentParser:
294
320
  )
295
321
  discover.set_defaults(func=_cmd_discover)
296
322
 
323
+ # `suggest` is the cross-source analog of `discover` (which is RSS-only): it
324
+ # lists the selectable knobs for any source — SE sites/tags, arXiv categories,
325
+ # Lobsters tags, Mastodon trending tags, Bluesky feeds/actors, RSS feeds.
326
+ from fetchkit.fetchers import list_suggesters
327
+
328
+ suggest = sub.add_parser(
329
+ "suggest",
330
+ help="List what you can query for a source (tags, sites, categories, feeds).",
331
+ )
332
+ suggest.add_argument(
333
+ "source", choices=list_suggesters(),
334
+ help="Source to get discovery suggestions for.",
335
+ )
336
+ suggest.add_argument(
337
+ "--query", default=None, metavar="Q",
338
+ help="Filter/seed term (e.g. RSS use case, GitHub repo search, Bluesky actor search).",
339
+ )
340
+ suggest.add_argument(
341
+ "--site", default=None,
342
+ help="Stack Exchange site for tag suggestions (default: stackoverflow).",
343
+ )
344
+ suggest.add_argument(
345
+ "--instance", default=None,
346
+ help="Mastodon instance host for trending tags (default: mastodon.social).",
347
+ )
348
+ suggest.add_argument(
349
+ "--what", default=None,
350
+ help="Sub-selection: stackexchange 'sites'|'tags'; bluesky 'feeds'|'actors'.",
351
+ )
352
+ suggest.add_argument(
353
+ "--limit", type=int, default=20, help="Max suggestions to return (default: 20).",
354
+ )
355
+ suggest.add_argument(
356
+ "-o", "--output", default=None, metavar="PATH",
357
+ help="Write JSON to this file instead of stdout.",
358
+ )
359
+ suggest.add_argument(
360
+ "--indent", type=int, default=2,
361
+ help="JSON indentation for pretty output (default: 2).",
362
+ )
363
+ suggest.add_argument(
364
+ "--compact", action="store_true",
365
+ help="Emit single-line JSON (overrides --indent).",
366
+ )
367
+ suggest.set_defaults(func=_cmd_suggest)
368
+
297
369
  find_feeds = sub.add_parser(
298
370
  "find-feeds",
299
371
  help="Autodiscover RSS/Atom feeds from one or more site URLs.",
@@ -0,0 +1,28 @@
1
+ """Fetcher package exports and builtin fetcher registration side effects."""
2
+
3
+ from .registry import get_fetcher, register_fetcher
4
+ from .suggest_registry import (
5
+ get_suggester,
6
+ list_suggesters,
7
+ register_suggester,
8
+ run_suggester,
9
+ )
10
+ # Import built-in fetchers to ensure they register themselves (fetchers and
11
+ # suggesters) on import.
12
+ from . import hackernews
13
+ from . import rss
14
+ from . import arxiv
15
+ from . import github
16
+ from . import lobsters
17
+ from . import stackexchange
18
+ from . import bluesky
19
+ from . import mastodon
20
+
21
+ __all__ = [
22
+ "get_fetcher",
23
+ "register_fetcher",
24
+ "get_suggester",
25
+ "list_suggesters",
26
+ "register_suggester",
27
+ "run_suggester",
28
+ ]
@@ -18,6 +18,7 @@ from fetchkit.schemas.post import Post, Source
18
18
  from fetchkit.schemas.fetcher import ArxivFetchConfig, FetcherConfig
19
19
  from fetchkit.fetchers.base import FetcherResult
20
20
  from fetchkit.fetchers.registry import register_fetcher
21
+ from fetchkit.fetchers.suggest_registry import register_suggester
21
22
 
22
23
  logger = logging.getLogger(__name__)
23
24
 
@@ -25,6 +26,39 @@ ARXIV_API_URL = "https://export.arxiv.org/api/query"
25
26
  SOURCE_NAME = Source.ARXIV
26
27
  DEFAULT_TIMEOUT_S = 30 # arXiv can be slow
27
28
 
29
+ # arXiv's category taxonomy is fixed and well-known, so discoverability is a static
30
+ # lookup (no network/auth). A representative cross-discipline subset of the most-used
31
+ # categories — enough for an agent to pick a `categories` value without guessing.
32
+ ARXIV_CATEGORIES: dict[str, str] = {
33
+ "cs.AI": "Artificial Intelligence",
34
+ "cs.CL": "Computation and Language (NLP)",
35
+ "cs.CV": "Computer Vision and Pattern Recognition",
36
+ "cs.LG": "Machine Learning",
37
+ "cs.CR": "Cryptography and Security",
38
+ "cs.DC": "Distributed, Parallel, and Cluster Computing",
39
+ "cs.DS": "Data Structures and Algorithms",
40
+ "cs.HC": "Human-Computer Interaction",
41
+ "cs.IR": "Information Retrieval",
42
+ "cs.NE": "Neural and Evolutionary Computing",
43
+ "cs.RO": "Robotics",
44
+ "cs.SE": "Software Engineering",
45
+ "cs.SY": "Systems and Control",
46
+ "stat.ML": "Machine Learning (Statistics)",
47
+ "stat.ME": "Methodology (Statistics)",
48
+ "math.OC": "Optimization and Control",
49
+ "math.PR": "Probability",
50
+ "math.ST": "Statistics Theory",
51
+ "eess.AS": "Audio and Speech Processing",
52
+ "eess.IV": "Image and Video Processing",
53
+ "eess.SP": "Signal Processing",
54
+ "physics.comp-ph": "Computational Physics",
55
+ "q-bio.NC": "Neurons and Cognition",
56
+ "q-bio.QM": "Quantitative Methods",
57
+ "q-fin.CP": "Computational Finance",
58
+ "q-fin.PM": "Portfolio Management",
59
+ "econ.EM": "Econometrics",
60
+ }
61
+
28
62
 
29
63
  def _build_search_query(config: ArxivFetchConfig) -> str:
30
64
  """Combine categories and free-text query into an arXiv ``search_query``."""
@@ -122,6 +156,16 @@ def fetch_posts(config: ArxivFetchConfig) -> list[Post]:
122
156
  return posts
123
157
 
124
158
 
159
+ @register_suggester("arxiv")
160
+ def suggest(*, query: Optional[str] = None, limit: int = 100, **kwargs: Any) -> list[dict[str, Any]]:
161
+ """Discoverability for arXiv: list selectable categories (filtered by ``query``)."""
162
+ items = [{"category": code, "name": name} for code, name in ARXIV_CATEGORIES.items()]
163
+ if query:
164
+ q = query.lower()
165
+ items = [it for it in items if q in it["category"].lower() or q in it["name"].lower()]
166
+ return items[:limit]
167
+
168
+
125
169
  @register_fetcher("arxiv")
126
170
  def fetch(config: FetcherConfig) -> FetcherResult:
127
171
  """Fetcher protocol implementation for arXiv."""