rssd-fs 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.
rssd_fs-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Allie Coleman
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.
rssd_fs-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,248 @@
1
+ Metadata-Version: 2.4
2
+ Name: rssd-fs
3
+ Version: 0.1.0
4
+ Summary: A file-based RSS daemon: the filesystem is the API
5
+ Keywords: rss,atom,feed,daemon,filesystem,tui
6
+ Author: Allie Coleman
7
+ License-Expression: MIT
8
+ License-File: LICENSE
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Environment :: Console
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3.14
14
+ Classifier: Topic :: Internet :: WWW/HTTP :: Indexing/Search
15
+ Classifier: Topic :: Utilities
16
+ Requires-Dist: feedparser>=6.0.14
17
+ Requires-Dist: httpx>=0.28.1
18
+ Requires-Dist: lxml>=6.1.3
19
+ Requires-Dist: watchfiles>=1.2.0
20
+ Requires-Dist: trafilatura>=2.2.0 ; extra == 'fulltext'
21
+ Requires-Dist: textual>=8.2.8 ; extra == 'tui'
22
+ Requires-Python: >=3.14
23
+ Project-URL: Homepage, https://github.com/alliecatowo/rssd
24
+ Project-URL: Documentation, https://alliecatowo.github.io/rssd/
25
+ Project-URL: Source, https://github.com/alliecatowo/rssd
26
+ Project-URL: Issues, https://github.com/alliecatowo/rssd/issues
27
+ Provides-Extra: fulltext
28
+ Provides-Extra: tui
29
+ Description-Content-Type: text/markdown
30
+
31
+ # rssd
32
+
33
+ A file-based RSS daemon. It watches a folder of XML subscription files, polls
34
+ those feeds, and materialises every entry as an individual XML file in a
35
+ per-feed folder.
36
+
37
+ **The filesystem is the API.** There is no database, no socket to connect to and
38
+ no client library. Any program that can read a directory is a client — `grep`,
39
+ `find`, `fswatch`, a shell script, your editor. The bundled TUI is deliberately
40
+ built the same way: it reads the output tree and the event log like anyone else
41
+ would, with no privileged channel back to the daemon.
42
+
43
+ ```
44
+ demo/
45
+ ├── feeds.d/ you write these
46
+ │ └── rust-blog.xml
47
+ ├── store/ the daemon writes these
48
+ │ └── rust-blog/
49
+ │ ├── feed.xml
50
+ │ ├── status.xml
51
+ │ └── entries/
52
+ │ ├── 20260907T000000Z-9f2c1a3b-crates-io-update.xml → newest
53
+ │ ├── 20260907T000000Z-9f2c1a3b-crates-io-update.r1.xml
54
+ │ └── 20260907T000000Z-9f2c1a3b-crates-io-update.r2.xml
55
+ └── var/
56
+ └── events.jsonl tail -F this
57
+ ```
58
+
59
+ ## Quick start
60
+
61
+ ```bash
62
+ mise install
63
+ uv sync --extra tui
64
+
65
+ uv run rssd init demo/
66
+ uv run rssd daemon --root demo --poll-now # terminal 1
67
+ uv run rss tui --root demo # terminal 2
68
+ tail -F demo/var/events.jsonl | jq -c # terminal 3
69
+ ```
70
+
71
+ Drop a new `.xml` file into `demo/feeds.d/` and the daemon picks it up without a
72
+ restart. Delete one and it stops polling — but never deletes what it already
73
+ harvested.
74
+
75
+ ### Offline
76
+
77
+ Every fixture is committed, so the whole thing runs with no network at all:
78
+
79
+ ```bash
80
+ uv run rssd init demo/ --fixture-mode
81
+ uv run rssd serve-fixtures --dir demo/fixtures # terminal 0
82
+ uv run rssd daemon --root demo --poll-now
83
+ ```
84
+
85
+ ## What an entry looks like
86
+
87
+ Feed HTML is mapped onto a small fixed vocabulary rather than passed through, so
88
+ every entry has the same shape no matter how the publisher writes their markup.
89
+ Wrapper `div`s, `class` attributes and publisher CSS hooks are gone; relative
90
+ URLs are resolved; `javascript:` and `data:` URLs are dropped.
91
+
92
+ ```xml
93
+ <entry xmlns="https://rssd.dev/entry/1"
94
+ id="sha256:9f2c1a3b…" revision="1" first-seen="2026-09-15T21:14:02Z">
95
+ <source>
96
+ <link>https://blog.rust-lang.org/2026/07/13/crates-io-development-update/</link>
97
+ <feed name="rust-blog" title="Rust Blog">https://blog.rust-lang.org/feed.xml</feed>
98
+ <guid basis="atom-id">https://blog.rust-lang.org/2026/07/13/…</guid>
99
+ </source>
100
+ <title>crates.io: development update</title>
101
+ <published origin="feed">2026-07-13T00:00:00Z</published>
102
+ <content origin="feed:content" hash="sha256:bc167659…">
103
+ <paragraph>Another six months have passed since our
104
+ <link href="https://blog.rust-lang.org/2026/01/21/…">last update</link>.</paragraph>
105
+ <heading level="2">Source Code Viewer</heading>
106
+ <list><item>Browse published crate versions</item></list>
107
+ </content>
108
+ </entry>
109
+ ```
110
+
111
+ `origin` tells you where the body came from: `feed:content`, `feed:summary`,
112
+ `fulltext:trafilatura`, or `none`. rssd never fabricates content — a feed that
113
+ ships only a title and a link produces an entry that says so.
114
+
115
+ ## Three invariants
116
+
117
+ **Reads are sacred.** Every file is written to a temporary name in the same
118
+ directory, fsynced, then `rename(2)`d into place. A reader sees the old file or
119
+ the new one, never a mix, and never a half-written one. A `SIGKILL` mid-write can
120
+ only leave a `.rssd-tmp-*` orphan, which is swept on the next start. Every
121
+ document is re-parsed with a strict XML parser *before* the rename, so a
122
+ malformed entry can never reach the tree.
123
+
124
+ **The tree is the truth; `var/` is a disposable cache.** Each entry file embeds
125
+ its own ID and content hash, so `rm -rf var/` is fully recoverable — the daemon
126
+ rebuilds what it knows by scanning the tree, and writes nothing.
127
+
128
+ **An entry file is written only when its content changes.** No heartbeat, no
129
+ `last-seen` field. If nothing changed upstream, nothing on disk is touched, so
130
+ `mtime` means exactly what a reader expects it to mean.
131
+
132
+ ## Revisions
133
+
134
+ Entry files are never mutated. When a publisher edits a post, the new version
135
+ lands beside the old one as `.r2.xml` and a stable symlink is repointed:
136
+
137
+ ```bash
138
+ uv run rssd demo-mutate --root demo # forces a revision on demand
139
+ ```
140
+
141
+ Follow the symlink for "current", or list `.rN` for the full history. Because
142
+ the change is detected by hashing the *rendered semantic content* rather than
143
+ the raw HTML, a reordered attribute or a rotating ad token can't manufacture a
144
+ revision. Three further guards — a daily per-entry cap, rotating-GUID detection,
145
+ and a flat cap on new entries per poll — keep a misbehaving feed from filling
146
+ the disk.
147
+
148
+ ## Events
149
+
150
+ Everything the daemon does is appended to `var/events.jsonl`, one JSON object
151
+ per line, with a sequence number that is monotonic across restarts:
152
+
153
+ ```json
154
+ {"v":1,"seq":42,"ts":"2026-09-15T21:14:02.140Z","event":"entry.new","feed":"rust-blog","data":{"id":"sha256:9f2c…","path":"store/rust-blog/entries/2026…xml","title":"crates.io: development update"}}
155
+ ```
156
+
157
+ Events are emitted *after* the corresponding file is on disk, so **if you see
158
+ `entry.new`, the file exists and is complete**. The converse isn't promised — a
159
+ crash between the rename and the append loses the event — so consumers reconcile
160
+ by scanning the tree when they see `daemon.started`.
161
+
162
+ Use `tail -F`, not `tail -f`: the log rotates, and `-f` follows the inode and
163
+ goes quietly dead.
164
+
165
+ ## Politeness
166
+
167
+ Conditional GET via `ETag`/`Last-Modified`; `Cache-Control: max-age` raises the
168
+ poll interval; `Retry-After` on 429/503 is honoured and isn't counted as a
169
+ failure. Feeds that send no validators at all are compared by body hash and
170
+ polled less often, since there's no cheap way to ask. Failures back off
171
+ exponentially with jitter, per feed, and one broken feed never affects another.
172
+
173
+ Fetching article pages is **opt-in per subscription** and off by default — the
174
+ normal footprint is one request per feed, never one per entry.
175
+
176
+ ## Commands
177
+
178
+ Two binaries, on purpose. `rssd` writes the tree; `rss` only reads it.
179
+
180
+ ```
181
+ rssd init <root> scaffold an instance [--fixture-mode]
182
+ rssd daemon --root <root> run it [--poll-now]
183
+ rssd once --root <root> one poll pass, then exit
184
+ rssd poll <feed> --root <r> force-poll one feed
185
+ rssd validate --root <root> check every subscription parses
186
+ rssd prune --retired --yes delete retired folders (never automatic)
187
+ rssd serve-fixtures local HTTP server over the fixtures
188
+ rssd demo-mutate --root <r> force a revision on demand
189
+ ```
190
+
191
+ ```
192
+ rss feeds what's subscribed, and is it healthy
193
+ rss ls [feed] entries, newest first
194
+ rss show <ref> read an entry as wrapped text
195
+ rss link <ref> print just the URL
196
+ rss open <ref> open it in a browser
197
+ rss links <ref> every link in an entry
198
+ rss search <query> [feed] match titles and body text
199
+ rss revisions <ref> every stored version
200
+ rss diff <ref> what changed when it was edited
201
+ rss info <ref> id, dates, origin, hash, path
202
+ rss config effective settings and their source
203
+ rss tui the terminal reader
204
+ ```
205
+
206
+ Refer to an entry by id prefix, position, feed, or path — all four work:
207
+
208
+ ```bash
209
+ rss show 9f2c1a3b # unique id prefix
210
+ rss show rust-blog:3 # 3rd newest in a feed
211
+ rss show rust-blog # newest in a feed
212
+ rss show demo/store/rust-blog/entries/2026….xml
213
+ ```
214
+
215
+ An ambiguous prefix lists the candidates rather than guessing.
216
+
217
+ ## The TUI
218
+
219
+ ```bash
220
+ rss tui --root demo
221
+ ```
222
+
223
+ Modal, like vim. Three panes — feeds, entries, reader — a status line, and no
224
+ chrome. `hjkl` to move, `Enter` to read, `o` to open in a browser, `y` to yank
225
+ the URL, `/` to search, `:` for ex commands (`:e rust-blog`, `:set width=100`,
226
+ `:links`, `:open 3`, `:q`).
227
+
228
+ `:set` changes the session only, exactly like vim; persistent preferences go in
229
+ `rss.toml`.
230
+
231
+ The daemon doesn't need to be running — the TUI reads the filesystem, so it
232
+ works fine against a static tree or a directory you rsynced from elsewhere.
233
+
234
+ ## Development
235
+
236
+ ```bash
237
+ uv sync --extra tui
238
+ uv run pytest # 322 tests, no network required
239
+ ```
240
+
241
+ The pure modules — `semantic`, `identity`, `parse`, `render`, `schedule`,
242
+ `diff`, `timeutil`, `subscriptions` — hold everything that can actually be
243
+ *wrong*, and are tested against committed byte-exact captures of six real feeds
244
+ chosen to cover the awkward cases: rich Atom with `xml:base`, RSS with
245
+ `content:encoded`, CRLF line endings, an image-only body, and one feed that
246
+ sends no cache validators at all.
247
+
248
+ `SPEC.md` is the full specification.
@@ -0,0 +1,218 @@
1
+ # rssd
2
+
3
+ A file-based RSS daemon. It watches a folder of XML subscription files, polls
4
+ those feeds, and materialises every entry as an individual XML file in a
5
+ per-feed folder.
6
+
7
+ **The filesystem is the API.** There is no database, no socket to connect to and
8
+ no client library. Any program that can read a directory is a client — `grep`,
9
+ `find`, `fswatch`, a shell script, your editor. The bundled TUI is deliberately
10
+ built the same way: it reads the output tree and the event log like anyone else
11
+ would, with no privileged channel back to the daemon.
12
+
13
+ ```
14
+ demo/
15
+ ├── feeds.d/ you write these
16
+ │ └── rust-blog.xml
17
+ ├── store/ the daemon writes these
18
+ │ └── rust-blog/
19
+ │ ├── feed.xml
20
+ │ ├── status.xml
21
+ │ └── entries/
22
+ │ ├── 20260907T000000Z-9f2c1a3b-crates-io-update.xml → newest
23
+ │ ├── 20260907T000000Z-9f2c1a3b-crates-io-update.r1.xml
24
+ │ └── 20260907T000000Z-9f2c1a3b-crates-io-update.r2.xml
25
+ └── var/
26
+ └── events.jsonl tail -F this
27
+ ```
28
+
29
+ ## Quick start
30
+
31
+ ```bash
32
+ mise install
33
+ uv sync --extra tui
34
+
35
+ uv run rssd init demo/
36
+ uv run rssd daemon --root demo --poll-now # terminal 1
37
+ uv run rss tui --root demo # terminal 2
38
+ tail -F demo/var/events.jsonl | jq -c # terminal 3
39
+ ```
40
+
41
+ Drop a new `.xml` file into `demo/feeds.d/` and the daemon picks it up without a
42
+ restart. Delete one and it stops polling — but never deletes what it already
43
+ harvested.
44
+
45
+ ### Offline
46
+
47
+ Every fixture is committed, so the whole thing runs with no network at all:
48
+
49
+ ```bash
50
+ uv run rssd init demo/ --fixture-mode
51
+ uv run rssd serve-fixtures --dir demo/fixtures # terminal 0
52
+ uv run rssd daemon --root demo --poll-now
53
+ ```
54
+
55
+ ## What an entry looks like
56
+
57
+ Feed HTML is mapped onto a small fixed vocabulary rather than passed through, so
58
+ every entry has the same shape no matter how the publisher writes their markup.
59
+ Wrapper `div`s, `class` attributes and publisher CSS hooks are gone; relative
60
+ URLs are resolved; `javascript:` and `data:` URLs are dropped.
61
+
62
+ ```xml
63
+ <entry xmlns="https://rssd.dev/entry/1"
64
+ id="sha256:9f2c1a3b…" revision="1" first-seen="2026-09-15T21:14:02Z">
65
+ <source>
66
+ <link>https://blog.rust-lang.org/2026/07/13/crates-io-development-update/</link>
67
+ <feed name="rust-blog" title="Rust Blog">https://blog.rust-lang.org/feed.xml</feed>
68
+ <guid basis="atom-id">https://blog.rust-lang.org/2026/07/13/…</guid>
69
+ </source>
70
+ <title>crates.io: development update</title>
71
+ <published origin="feed">2026-07-13T00:00:00Z</published>
72
+ <content origin="feed:content" hash="sha256:bc167659…">
73
+ <paragraph>Another six months have passed since our
74
+ <link href="https://blog.rust-lang.org/2026/01/21/…">last update</link>.</paragraph>
75
+ <heading level="2">Source Code Viewer</heading>
76
+ <list><item>Browse published crate versions</item></list>
77
+ </content>
78
+ </entry>
79
+ ```
80
+
81
+ `origin` tells you where the body came from: `feed:content`, `feed:summary`,
82
+ `fulltext:trafilatura`, or `none`. rssd never fabricates content — a feed that
83
+ ships only a title and a link produces an entry that says so.
84
+
85
+ ## Three invariants
86
+
87
+ **Reads are sacred.** Every file is written to a temporary name in the same
88
+ directory, fsynced, then `rename(2)`d into place. A reader sees the old file or
89
+ the new one, never a mix, and never a half-written one. A `SIGKILL` mid-write can
90
+ only leave a `.rssd-tmp-*` orphan, which is swept on the next start. Every
91
+ document is re-parsed with a strict XML parser *before* the rename, so a
92
+ malformed entry can never reach the tree.
93
+
94
+ **The tree is the truth; `var/` is a disposable cache.** Each entry file embeds
95
+ its own ID and content hash, so `rm -rf var/` is fully recoverable — the daemon
96
+ rebuilds what it knows by scanning the tree, and writes nothing.
97
+
98
+ **An entry file is written only when its content changes.** No heartbeat, no
99
+ `last-seen` field. If nothing changed upstream, nothing on disk is touched, so
100
+ `mtime` means exactly what a reader expects it to mean.
101
+
102
+ ## Revisions
103
+
104
+ Entry files are never mutated. When a publisher edits a post, the new version
105
+ lands beside the old one as `.r2.xml` and a stable symlink is repointed:
106
+
107
+ ```bash
108
+ uv run rssd demo-mutate --root demo # forces a revision on demand
109
+ ```
110
+
111
+ Follow the symlink for "current", or list `.rN` for the full history. Because
112
+ the change is detected by hashing the *rendered semantic content* rather than
113
+ the raw HTML, a reordered attribute or a rotating ad token can't manufacture a
114
+ revision. Three further guards — a daily per-entry cap, rotating-GUID detection,
115
+ and a flat cap on new entries per poll — keep a misbehaving feed from filling
116
+ the disk.
117
+
118
+ ## Events
119
+
120
+ Everything the daemon does is appended to `var/events.jsonl`, one JSON object
121
+ per line, with a sequence number that is monotonic across restarts:
122
+
123
+ ```json
124
+ {"v":1,"seq":42,"ts":"2026-09-15T21:14:02.140Z","event":"entry.new","feed":"rust-blog","data":{"id":"sha256:9f2c…","path":"store/rust-blog/entries/2026…xml","title":"crates.io: development update"}}
125
+ ```
126
+
127
+ Events are emitted *after* the corresponding file is on disk, so **if you see
128
+ `entry.new`, the file exists and is complete**. The converse isn't promised — a
129
+ crash between the rename and the append loses the event — so consumers reconcile
130
+ by scanning the tree when they see `daemon.started`.
131
+
132
+ Use `tail -F`, not `tail -f`: the log rotates, and `-f` follows the inode and
133
+ goes quietly dead.
134
+
135
+ ## Politeness
136
+
137
+ Conditional GET via `ETag`/`Last-Modified`; `Cache-Control: max-age` raises the
138
+ poll interval; `Retry-After` on 429/503 is honoured and isn't counted as a
139
+ failure. Feeds that send no validators at all are compared by body hash and
140
+ polled less often, since there's no cheap way to ask. Failures back off
141
+ exponentially with jitter, per feed, and one broken feed never affects another.
142
+
143
+ Fetching article pages is **opt-in per subscription** and off by default — the
144
+ normal footprint is one request per feed, never one per entry.
145
+
146
+ ## Commands
147
+
148
+ Two binaries, on purpose. `rssd` writes the tree; `rss` only reads it.
149
+
150
+ ```
151
+ rssd init <root> scaffold an instance [--fixture-mode]
152
+ rssd daemon --root <root> run it [--poll-now]
153
+ rssd once --root <root> one poll pass, then exit
154
+ rssd poll <feed> --root <r> force-poll one feed
155
+ rssd validate --root <root> check every subscription parses
156
+ rssd prune --retired --yes delete retired folders (never automatic)
157
+ rssd serve-fixtures local HTTP server over the fixtures
158
+ rssd demo-mutate --root <r> force a revision on demand
159
+ ```
160
+
161
+ ```
162
+ rss feeds what's subscribed, and is it healthy
163
+ rss ls [feed] entries, newest first
164
+ rss show <ref> read an entry as wrapped text
165
+ rss link <ref> print just the URL
166
+ rss open <ref> open it in a browser
167
+ rss links <ref> every link in an entry
168
+ rss search <query> [feed] match titles and body text
169
+ rss revisions <ref> every stored version
170
+ rss diff <ref> what changed when it was edited
171
+ rss info <ref> id, dates, origin, hash, path
172
+ rss config effective settings and their source
173
+ rss tui the terminal reader
174
+ ```
175
+
176
+ Refer to an entry by id prefix, position, feed, or path — all four work:
177
+
178
+ ```bash
179
+ rss show 9f2c1a3b # unique id prefix
180
+ rss show rust-blog:3 # 3rd newest in a feed
181
+ rss show rust-blog # newest in a feed
182
+ rss show demo/store/rust-blog/entries/2026….xml
183
+ ```
184
+
185
+ An ambiguous prefix lists the candidates rather than guessing.
186
+
187
+ ## The TUI
188
+
189
+ ```bash
190
+ rss tui --root demo
191
+ ```
192
+
193
+ Modal, like vim. Three panes — feeds, entries, reader — a status line, and no
194
+ chrome. `hjkl` to move, `Enter` to read, `o` to open in a browser, `y` to yank
195
+ the URL, `/` to search, `:` for ex commands (`:e rust-blog`, `:set width=100`,
196
+ `:links`, `:open 3`, `:q`).
197
+
198
+ `:set` changes the session only, exactly like vim; persistent preferences go in
199
+ `rss.toml`.
200
+
201
+ The daemon doesn't need to be running — the TUI reads the filesystem, so it
202
+ works fine against a static tree or a directory you rsynced from elsewhere.
203
+
204
+ ## Development
205
+
206
+ ```bash
207
+ uv sync --extra tui
208
+ uv run pytest # 322 tests, no network required
209
+ ```
210
+
211
+ The pure modules — `semantic`, `identity`, `parse`, `render`, `schedule`,
212
+ `diff`, `timeutil`, `subscriptions` — hold everything that can actually be
213
+ *wrong*, and are tested against committed byte-exact captures of six real feeds
214
+ chosen to cover the awkward cases: rich Atom with `xml:base`, RSS with
215
+ `content:encoded`, CRLF line endings, an image-only body, and one feed that
216
+ sends no cache validators at all.
217
+
218
+ `SPEC.md` is the full specification.
@@ -0,0 +1,67 @@
1
+ [project]
2
+ name = "rssd-fs"
3
+ version = "0.1.0"
4
+ description = "A file-based RSS daemon: the filesystem is the API"
5
+ license = "MIT"
6
+ license-files = ["LICENSE"]
7
+ keywords = [
8
+ "rss",
9
+ "atom",
10
+ "feed",
11
+ "daemon",
12
+ "filesystem",
13
+ "tui",
14
+ ]
15
+ classifiers = [
16
+ "Development Status :: 4 - Beta",
17
+ "Environment :: Console",
18
+ "Intended Audience :: Developers",
19
+ "License :: OSI Approved :: MIT License",
20
+ "Programming Language :: Python :: 3.14",
21
+ "Topic :: Internet :: WWW/HTTP :: Indexing/Search",
22
+ "Topic :: Utilities",
23
+ ]
24
+ readme = "README.md"
25
+ requires-python = ">=3.14"
26
+ dependencies = [
27
+ "feedparser>=6.0.14",
28
+ "httpx>=0.28.1",
29
+ "lxml>=6.1.3",
30
+ "watchfiles>=1.2.0",
31
+ ]
32
+
33
+ [[project.authors]]
34
+ name = "Allie Coleman"
35
+
36
+ [project.urls]
37
+ Homepage = "https://github.com/alliecatowo/rssd"
38
+ Documentation = "https://alliecatowo.github.io/rssd/"
39
+ Source = "https://github.com/alliecatowo/rssd"
40
+ Issues = "https://github.com/alliecatowo/rssd/issues"
41
+
42
+ [project.optional-dependencies]
43
+ tui = ["textual>=8.2.8"]
44
+ fulltext = ["trafilatura>=2.2.0"]
45
+
46
+ [project.scripts]
47
+ rssd = "rssd.cli:main"
48
+ rss = "rssd.readercli:main"
49
+
50
+ [dependency-groups]
51
+ dev = [
52
+ "pytest>=9.1.1",
53
+ "pytest-asyncio>=1.4.0",
54
+ ]
55
+
56
+ [build-system]
57
+ requires = ["uv_build>=0.12,<0.13"]
58
+ build-backend = "uv_build"
59
+
60
+ [tool.uv.build-backend]
61
+ module-root = "src"
62
+ module-name = "rssd"
63
+
64
+ [tool.pytest.ini_options]
65
+ testpaths = ["tests"]
66
+ asyncio_mode = "auto"
67
+ filterwarnings = ["ignore:To avoid breaking existing software:DeprecationWarning"]
@@ -0,0 +1,64 @@
1
+ [project]
2
+ name = "rssd-fs"
3
+ version = "0.1.0"
4
+ description = "A file-based RSS daemon: the filesystem is the API"
5
+ license = "MIT"
6
+ license-files = ["LICENSE"]
7
+ authors = [{ name = "Allie Coleman" }]
8
+ keywords = ["rss", "atom", "feed", "daemon", "filesystem", "tui"]
9
+ classifiers = [
10
+ "Development Status :: 4 - Beta",
11
+ "Environment :: Console",
12
+ "Intended Audience :: Developers",
13
+ "License :: OSI Approved :: MIT License",
14
+ "Programming Language :: Python :: 3.14",
15
+ "Topic :: Internet :: WWW/HTTP :: Indexing/Search",
16
+ "Topic :: Utilities",
17
+ ]
18
+ readme = "README.md"
19
+ requires-python = ">=3.14"
20
+ dependencies = [
21
+ "feedparser>=6.0.14",
22
+ "httpx>=0.28.1",
23
+ "lxml>=6.1.3",
24
+ "watchfiles>=1.2.0",
25
+ ]
26
+
27
+ [project.urls]
28
+ Homepage = "https://github.com/alliecatowo/rssd"
29
+ Documentation = "https://alliecatowo.github.io/rssd/"
30
+ Source = "https://github.com/alliecatowo/rssd"
31
+ Issues = "https://github.com/alliecatowo/rssd/issues"
32
+
33
+ [project.optional-dependencies]
34
+ tui = ["textual>=8.2.8"]
35
+ fulltext = ["trafilatura>=2.2.0"]
36
+
37
+ [project.scripts]
38
+ # Two binaries on purpose: `rssd` writes the tree, `rss` only reads it.
39
+ rssd = "rssd.cli:main"
40
+ rss = "rssd.readercli:main"
41
+
42
+ [dependency-groups]
43
+ dev = [
44
+ "pytest>=9.1.1",
45
+ "pytest-asyncio>=1.4.0",
46
+ ]
47
+
48
+ [build-system]
49
+ requires = ["uv_build>=0.12,<0.13"]
50
+ build-backend = "uv_build"
51
+
52
+ [tool.uv.build-backend]
53
+ module-root = "src"
54
+ module-name = "rssd"
55
+
56
+ [tool.pytest.ini_options]
57
+ testpaths = ["tests"]
58
+ asyncio_mode = "auto"
59
+ filterwarnings = [
60
+ # feedparser warns once per entry that it is falling back from
61
+ # updated_parsed to published_parsed. We handle both explicitly in
62
+ # parse.py, and 500 copies of the notice drowns out real output.
63
+ "ignore:To avoid breaking existing software:DeprecationWarning",
64
+ ]
File without changes
@@ -0,0 +1,5 @@
1
+ import sys
2
+
3
+ from .cli import main
4
+
5
+ sys.exit(main())