fetchkit-agents 0.1.0__tar.gz

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