aboutsummaryrefslogtreecommitdiff
path: root/tagrss.py
diff options
context:
space:
mode:
Diffstat (limited to 'tagrss.py')
-rw-r--r--tagrss.py125
1 files changed, 101 insertions, 24 deletions
diff --git a/tagrss.py b/tagrss.py
index 7b1d084..c1dfbf5 100644
--- a/tagrss.py
+++ b/tagrss.py
@@ -18,24 +18,51 @@ class FeedFetchError(Exception):
super().__init__(f"Get {feed_source} returned HTTP {status_code}")
+class SqliteMissingForeignKeySupportError(Exception):
+ pass
+
+
class TagRss:
def __init__(self, *, storage_path: str | pathlib.Path):
self.connection: sqlite3.Connection = sqlite3.connect(storage_path)
+
with self.connection:
self.connection.executescript(
"""
-CREATE TABLE IF NOT EXISTS feeds(id INTEGER PRIMARY KEY, source TEXT UNIQUE, title TEXT);
-CREATE INDEX IF NOT EXISTS feed_source ON feeds(source);
+PRAGMA foreign_keys = ON;
-CREATE TABLE IF NOT EXISTS feed_tags(feed_id INTEGER, tag TEXT);
-CREATE INDEX IF NOT EXISTS feed_tags_feed_id ON feed_tags(feed_id);
+CREATE TABLE IF NOT EXISTS
+ feeds(
+ id INTEGER PRIMARY KEY,
+ source TEXT UNIQUE,
+ title TEXT
+ ) STRICT;
-CREATE TABLE IF NOT EXISTS entries(id INTEGER PRIMARY KEY, feed_id INTEGER, title TEXT, link TEXT, epoch_published INTEGER, epoch_updated INTEGER, epoch_downloaded INTEGER);
-CREATE INDEX IF NOT EXISTS entry_epoch_downloaded ON entries(epoch_downloaded);
+CREATE TABLE IF NOT EXISTS
+ feed_tags(
+ feed_id INTEGER REFERENCES feeds(id) ON DELETE CASCADE,
+ tag TEXT
+ ) STRICT;
+CREATE INDEX IF NOT EXISTS idx_feed_tags__feed_id__tag ON feed_tags(feed_id, tag);
+CREATE INDEX IF NOT EXISTS idx_feed_tags__tag__feed_id ON feed_tags(tag, feed_id);
+
+CREATE TABLE IF NOT EXISTS
+ entries(
+ id INTEGER PRIMARY KEY,
+ feed_id INTEGER REFERENCES feeds(id) ON DELETE CASCADE,
+ title TEXT,
+ link TEXT,
+ epoch_published INTEGER,
+ epoch_updated INTEGER,
+ epoch_downloaded INTEGER
+ ) STRICT;
+CREATE INDEX IF NOT EXISTS idx_entries__epoch_downloaded ON entries(epoch_downloaded);
"""
)
+ if (1,) not in self.connection.execute("PRAGMA foreign_keys;").fetchmany(1):
+ raise SqliteMissingForeignKeySupportError
- def add_feed(self, *, feed_source: str, tags: tuple[str]):
+ def add_feed(self, feed_source: str, tags: list[str]) -> None:
response = requests.get(feed_source)
if response.status_code != requests.codes.ok:
raise FeedFetchError(feed_source, response.status_code)
@@ -44,7 +71,8 @@ CREATE INDEX IF NOT EXISTS entry_epoch_downloaded ON entries(epoch_downloaded);
except KeyError:
base: str = feed_source
parsed = feedparser.parse(
- io.BytesIO(bytes(response.text, encoding="utf-8")), response_headers={"Content-Location": base}
+ io.BytesIO(bytes(response.text, encoding="utf-8")),
+ response_headers={"Content-Location": base},
)
with self.connection:
feed_title: str = parsed.feed.get("title", "")
@@ -52,36 +80,37 @@ CREATE INDEX IF NOT EXISTS entry_epoch_downloaded ON entries(epoch_downloaded);
self.connection.execute(
"INSERT INTO feeds(source, title) VALUES(?, ?);",
(feed_source, feed_title),
- )
+ ) # Note: ensure no more INSERTs between this and the last_insert_rowid() call
except sqlite3.IntegrityError:
raise FeedAlreadyAddedError
feed_id: int = int(
- self.connection.execute(
- "SELECT id FROM feeds WHERE source = ?;", (feed_source,)
- ).fetchone()[0]
+ self.connection.execute("SELECT last_insert_rowid();").fetchone()[0]
)
self.connection.executemany(
- f"INSERT INTO feed_tags(feed_id, tag) VALUES({feed_id}, ?);", tuple(((tag,) for tag in tags))
+ "INSERT INTO feed_tags(feed_id, tag) VALUES(?, ?);",
+ ((feed_id, tag) for tag in tags),
)
for entry in reversed(parsed.entries):
- link: str = entry.get("link", "")
+ link: str = entry.get("link", None)
+ title: str = entry.get("title", None)
try:
epoch_published: typing.Optional[int] = calendar.timegm(
entry.get("published_parsed", None)
)
- except ValueError:
+ except TypeError:
epoch_published = None
try:
epoch_updated: typing.Optional[int] = calendar.timegm(
entry.get("updated_parsed", None)
)
- except ValueError:
+ except TypeError:
epoch_updated = None
self.connection.execute(
- "INSERT INTO entries(feed_id, title, link, epoch_published, epoch_updated, epoch_downloaded) VALUES(?, ?, ?, ?, ?, ?);",
+ "INSERT INTO entries(feed_id, title, link, epoch_published, epoch_updated, epoch_downloaded) \
+ VALUES(?, ?, ?, ?, ?, ?);",
(
feed_id,
- feed_title,
+ title,
link,
epoch_published,
epoch_updated,
@@ -89,15 +118,16 @@ CREATE INDEX IF NOT EXISTS entry_epoch_downloaded ON entries(epoch_downloaded);
),
)
- def get_entries(self, *, limit: int):
+ def get_entries(self, *, limit: int) -> list[dict[str, typing.Any]]:
with self.connection:
- result = self.connection.execute(
- "SELECT feed_id, title, link, epoch_published, epoch_updated FROM entries ORDER BY epoch_downloaded DESC LIMIT ?;",
+ resp = self.connection.execute(
+ "SELECT feed_id, title, link, epoch_published, epoch_updated FROM entries \
+ ORDER BY epoch_downloaded DESC LIMIT ?;",
(limit,),
).fetchall()
entries = []
- for entry in result:
+ for entry in resp:
entries.append(
{
"feed_id": entry[0],
@@ -108,6 +138,53 @@ CREATE INDEX IF NOT EXISTS entry_epoch_downloaded ON entries(epoch_downloaded);
}
)
return entries
- def get_feed_tags(self, feed_id: int) -> tuple[str]:
+
+ def get_feed_source(self, feed_id: int) -> str:
+ with self.connection:
+ return self.connection.execute(
+ "SELECT source FROM feeds WHERE id = ?;", (feed_id,)
+ ).fetchone()[0]
+
+ def get_feed_title(self, feed_id: int) -> str:
+ with self.connection:
+ return self.connection.execute(
+ "SELECT title FROM feeds WHERE id = ?;", (feed_id,)
+ ).fetchone()[0]
+
+ def get_feed_tags(self, feed_id: int) -> list[str]:
+ with self.connection:
+ return [
+ t[0]
+ for t in self.connection.execute(
+ "SELECT tag FROM feed_tags WHERE feed_id = ?;", (feed_id,)
+ ).fetchall()
+ ]
+
+ def set_feed_source(self, feed_id: int, feed_source: str):
+ with self.connection:
+ self.connection.execute(
+ "UPDATE feeds SET source = ? WHERE id = ?;", (feed_source, feed_id)
+ )
+
+ def set_feed_title(self, feed_id: int, feed_title: str):
+ with self.connection:
+ self.connection.execute(
+ "UPDATE feeds SET title = ? WHERE id = ?;", (feed_title, feed_id)
+ )
+
+ def set_feed_tags(self, feed_id: int, feed_tags: list[str]):
+ with self.connection:
+ self.connection.execute("DELETE FROM feed_tags WHERE feed_id = ?;", (feed_id,))
+ self.connection.executemany(
+ "INSERT INTO feed_tags(feed_id, tag) VALUES(?, ?);",
+ ((feed_id, tag) for tag in feed_tags),
+ )
+
+ def delete_feed(self, feed_id: int) -> None:
+ with self.connection:
+ self.connection.execute("DELETE FROM feeds WHERE id = ?;", (feed_id,))
+
+ def close(self) -> None:
with self.connection:
- return tuple((t[0] for t in self.connection.execute("SELECT tag FROM feed_tags WHERE feed_id = ?;", (feed_id,)).fetchall())) \ No newline at end of file
+ self.connection.execute("PRAGMA optimize;")
+ self.connection.close()