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.
- {fetchkit_agents-0.2.0 → fetchkit_agents-0.3.0}/PKG-INFO +111 -11
- {fetchkit_agents-0.2.0 → fetchkit_agents-0.3.0}/README.md +110 -10
- {fetchkit_agents-0.2.0 → fetchkit_agents-0.3.0}/pyproject.toml +1 -1
- {fetchkit_agents-0.2.0 → fetchkit_agents-0.3.0}/src/fetchkit/__init__.py +20 -4
- {fetchkit_agents-0.2.0 → fetchkit_agents-0.3.0}/src/fetchkit/cli.py +73 -1
- fetchkit_agents-0.3.0/src/fetchkit/fetchers/__init__.py +28 -0
- {fetchkit_agents-0.2.0 → fetchkit_agents-0.3.0}/src/fetchkit/fetchers/arxiv.py +44 -0
- fetchkit_agents-0.3.0/src/fetchkit/fetchers/bluesky.py +176 -0
- {fetchkit_agents-0.2.0 → fetchkit_agents-0.3.0}/src/fetchkit/fetchers/github.py +23 -1
- {fetchkit_agents-0.2.0 → fetchkit_agents-0.3.0}/src/fetchkit/fetchers/hackernews.py +12 -0
- {fetchkit_agents-0.2.0 → fetchkit_agents-0.3.0}/src/fetchkit/fetchers/lobsters.py +20 -0
- fetchkit_agents-0.3.0/src/fetchkit/fetchers/mastodon.py +161 -0
- {fetchkit_agents-0.2.0 → fetchkit_agents-0.3.0}/src/fetchkit/fetchers/rss.py +12 -0
- fetchkit_agents-0.3.0/src/fetchkit/fetchers/stackexchange.py +230 -0
- fetchkit_agents-0.3.0/src/fetchkit/fetchers/suggest_registry.py +53 -0
- {fetchkit_agents-0.2.0 → fetchkit_agents-0.3.0}/src/fetchkit/schemas/describe.py +50 -2
- {fetchkit_agents-0.2.0 → fetchkit_agents-0.3.0}/src/fetchkit/schemas/fetcher.py +76 -0
- {fetchkit_agents-0.2.0 → fetchkit_agents-0.3.0}/src/fetchkit/schemas/post.py +3 -0
- {fetchkit_agents-0.2.0 → fetchkit_agents-0.3.0}/src/fetchkit_agents.egg-info/PKG-INFO +111 -11
- {fetchkit_agents-0.2.0 → fetchkit_agents-0.3.0}/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 → fetchkit_agents-0.3.0}/LICENSE +0 -0
- {fetchkit_agents-0.2.0 → fetchkit_agents-0.3.0}/setup.cfg +0 -0
- {fetchkit_agents-0.2.0 → fetchkit_agents-0.3.0}/src/fetchkit/__main__.py +0 -0
- {fetchkit_agents-0.2.0 → fetchkit_agents-0.3.0}/src/fetchkit/collector.py +0 -0
- {fetchkit_agents-0.2.0 → fetchkit_agents-0.3.0}/src/fetchkit/config_loader.py +0 -0
- {fetchkit_agents-0.2.0 → fetchkit_agents-0.3.0}/src/fetchkit/discovery/__init__.py +0 -0
- {fetchkit_agents-0.2.0 → fetchkit_agents-0.3.0}/src/fetchkit/discovery/autodiscover.py +0 -0
- {fetchkit_agents-0.2.0 → fetchkit_agents-0.3.0}/src/fetchkit/discovery/catalog.py +0 -0
- {fetchkit_agents-0.2.0 → fetchkit_agents-0.3.0}/src/fetchkit/discovery/core.py +0 -0
- {fetchkit_agents-0.2.0 → fetchkit_agents-0.3.0}/src/fetchkit/discovery/data/CATALOG.md +0 -0
- {fetchkit_agents-0.2.0 → fetchkit_agents-0.3.0}/src/fetchkit/discovery/data/catalog.json +0 -0
- {fetchkit_agents-0.2.0 → fetchkit_agents-0.3.0}/src/fetchkit/discovery/embedding.py +0 -0
- {fetchkit_agents-0.2.0 → fetchkit_agents-0.3.0}/src/fetchkit/discovery/errors.py +0 -0
- {fetchkit_agents-0.2.0 → fetchkit_agents-0.3.0}/src/fetchkit/discovery/external.py +0 -0
- {fetchkit_agents-0.2.0 → fetchkit_agents-0.3.0}/src/fetchkit/discovery/ranking.py +0 -0
- {fetchkit_agents-0.2.0 → fetchkit_agents-0.3.0}/src/fetchkit/discovery/schemas.py +0 -0
- {fetchkit_agents-0.2.0 → fetchkit_agents-0.3.0}/src/fetchkit/fetchers/base.py +0 -0
- {fetchkit_agents-0.2.0 → fetchkit_agents-0.3.0}/src/fetchkit/fetchers/registry.py +0 -0
- {fetchkit_agents-0.2.0 → fetchkit_agents-0.3.0}/src/fetchkit/http/__init__.py +0 -0
- {fetchkit_agents-0.2.0 → fetchkit_agents-0.3.0}/src/fetchkit/http/client.py +0 -0
- {fetchkit_agents-0.2.0 → fetchkit_agents-0.3.0}/src/fetchkit/http/rate_limit.py +0 -0
- {fetchkit_agents-0.2.0 → fetchkit_agents-0.3.0}/src/fetchkit/http/ssrf.py +0 -0
- {fetchkit_agents-0.2.0 → fetchkit_agents-0.3.0}/src/fetchkit/py.typed +0 -0
- {fetchkit_agents-0.2.0 → fetchkit_agents-0.3.0}/src/fetchkit/schemas/__init__.py +0 -0
- {fetchkit_agents-0.2.0 → fetchkit_agents-0.3.0}/src/fetchkit/schemas/base.py +0 -0
- {fetchkit_agents-0.2.0 → fetchkit_agents-0.3.0}/src/fetchkit/schemas/collector.py +0 -0
- {fetchkit_agents-0.2.0 → fetchkit_agents-0.3.0}/src/fetchkit/schemas/config.py +0 -0
- {fetchkit_agents-0.2.0 → fetchkit_agents-0.3.0}/src/fetchkit/utils/__init__.py +0 -0
- {fetchkit_agents-0.2.0 → fetchkit_agents-0.3.0}/src/fetchkit/utils/time.py +0 -0
- {fetchkit_agents-0.2.0 → fetchkit_agents-0.3.0}/src/fetchkit_agents.egg-info/dependency_links.txt +0 -0
- {fetchkit_agents-0.2.0 → fetchkit_agents-0.3.0}/src/fetchkit_agents.egg-info/entry_points.txt +0 -0
- {fetchkit_agents-0.2.0 → fetchkit_agents-0.3.0}/src/fetchkit_agents.egg-info/requires.txt +0 -0
- {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.
|
|
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`
|
|
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"
|
|
@@ -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
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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`),
|
|
414
|
-
>
|
|
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`
|
|
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"
|
|
@@ -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
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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`),
|
|
372
|
-
>
|
|
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.
|
|
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``
|
|
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.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."""
|