Expanded linting rules, added codespell and pip-audit, and fixed all violations.
CI / Formatting (push) Successful in 11s
CI / Linting (push) Successful in 11s
CI / Tests (push) Successful in 15s
CI / Type Checking (push) Successful in 21s
CI / Spelling (push) Successful in 12s
Dependency Audit / Dependency Audit (push) Successful in 7s

This commit is contained in:
2026-02-20 10:30:36 -05:00
parent 0f2f43a55a
commit ee73e36f8e
19 changed files with 645 additions and 189 deletions
+72 -58
View File
@@ -12,8 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
"""
Provides async SQLite database access for all of Crabstero's persistent storage needs.
"""Async SQLite database access for Crabstero's persistent storage.
Uses aiosqlite for native async access. All methods are async def.
"""
@@ -52,7 +51,8 @@ CREATE TABLE IF NOT EXISTS markov_transitions (
word TEXT NOT NULL,
next_word TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_transitions_channel_word ON markov_transitions(channel_id, word);
CREATE INDEX IF NOT EXISTS idx_transitions_channel_word
ON markov_transitions(channel_id, word);
CREATE INDEX IF NOT EXISTS idx_transitions_user ON markov_transitions(user_id);
-- Image URLs per channel.
@@ -80,16 +80,15 @@ CREATE TABLE IF NOT EXISTS ingested_channels (
class Database:
"""
Manages all SQLite database operations for Crabstero.
"""Manages all SQLite database operations for Crabstero.
Uses aiosqlite for native async access. A single connection is held open for the lifetime of
the bot process with WAL mode enabled for concurrent read performance.
Uses aiosqlite for native async access. A single connection is held
open for the lifetime of the bot process with WAL mode enabled for
concurrent read performance.
"""
def __init__(self, connection: aiosqlite.Connection) -> None:
"""
Initializes the Database wrapper with an already-opened aiosqlite connection.
"""Initialize the Database wrapper with an already-opened aiosqlite connection.
:param connection: An open aiosqlite connection.
"""
@@ -99,9 +98,10 @@ class Database:
@classmethod
async def connect(cls, path: str) -> Self:
"""
Opens a new SQLite database at the given path, configures it for performance, and creates
the schema if it does not already exist.
"""Open a SQLite database, configure it, and create the schema.
Configure the database for performance and create the schema if it
does not already exist.
:param path: The file path to the SQLite database.
:return: A new Database instance ready for use.
@@ -121,7 +121,7 @@ class Database:
return cls(connection)
async def commit(self) -> None:
"""Commits pending writes and resets the flush timer."""
"""Commit pending writes and reset the flush timer."""
await self._connection.commit()
self._pending_writes = 0
if self._flush_task is not None:
@@ -129,7 +129,7 @@ class Database:
self._flush_task = None
async def close(self) -> None:
"""Cancels the flush timer, commits any pending writes, then closes the database connection."""
"""Cancel the flush timer, commit pending writes, and close the connection."""
if self._flush_task is not None:
self._flush_task.cancel()
with contextlib.suppress(asyncio.CancelledError):
@@ -141,26 +141,30 @@ class Database:
await self._connection.close()
async def add_start_words_batch(self, rows: list[tuple[int, int, str]]) -> None:
"""
Inserts a batch of starting words into the markov_start_words table.
"""Insert a batch of starting words into the markov_start_words table.
:param rows: A list of (channel_id, user_id, word) tuples to insert.
"""
await self._connection.executemany(
"INSERT INTO markov_start_words (channel_id, user_id, word) VALUES (?, ?, ?)",
"INSERT INTO markov_start_words"
" (channel_id, user_id, word)"
" VALUES (?, ?, ?)",
rows,
)
await self._maybe_commit()
async def get_random_start_word(self, channel_id: int) -> str | None:
"""
Returns a random starting word for a given channel, weighted by occurrence frequency.
"""Return a random starting word for a channel.
Weighted by occurrence frequency.
:param channel_id: The Discord channel ID.
:return: A random starting word, or None if no starting words exist for this channel.
:return: A random starting word, or None if none exist.
"""
async with self._connection.execute(
"SELECT word FROM markov_start_words WHERE channel_id = ? ORDER BY RANDOM() LIMIT 1",
"SELECT word FROM markov_start_words"
" WHERE channel_id = ?"
" ORDER BY RANDOM() LIMIT 1",
(channel_id,),
) as cursor:
row = await cursor.fetchone()
@@ -169,28 +173,32 @@ class Database:
async def add_transitions_batch(
self, rows: list[tuple[int, int, str, str]]
) -> None:
"""
Inserts a batch of word transitions into the markov_transitions table.
"""Insert a batch of word transitions into the markov_transitions table.
:param rows: A list of (channel_id, user_id, word, next_word) tuples to insert.
:param rows: A list of (channel_id, user_id, word, next_word)
tuples to insert.
"""
await self._connection.executemany(
"INSERT INTO markov_transitions (channel_id, user_id, word, next_word) VALUES (?, ?, ?, ?)",
"INSERT INTO markov_transitions"
" (channel_id, user_id, word, next_word)"
" VALUES (?, ?, ?, ?)",
rows,
)
await self._maybe_commit()
async def get_random_next_word(self, channel_id: int, word: str) -> str | None:
"""
Returns a random next word for a given word in a given channel, weighted by occurrence
frequency.
"""Return a random next word for a given word in a channel.
Weighted by occurrence frequency.
:param channel_id: The Discord channel ID.
:param word: The current word to find a transition for.
:return: A random next word, or None if no transitions exist.
"""
async with self._connection.execute(
"SELECT next_word FROM markov_transitions WHERE channel_id = ? AND word = ? ORDER BY RANDOM() LIMIT 1",
"SELECT next_word FROM markov_transitions"
" WHERE channel_id = ? AND word = ?"
" ORDER BY RANDOM() LIMIT 1",
(channel_id, word),
) as cursor:
row = await cursor.fetchone()
@@ -199,26 +207,28 @@ class Database:
async def get_random_completing_next_word(
self, channel_id: int, word: str
) -> str | None:
"""
Returns a random next word that ends a sentence for a given word in a given channel,
weighted by occurrence frequency.
"""Return a random sentence-ending next word for a given word in a channel.
A completing word is one whose last character is '.', '!', '?', or '§'.
Weighted by occurrence frequency. A completing word is one whose last
character is '.', '!', '?', or '§'.
:param channel_id: The Discord channel ID.
:param word: The current word to find a completing transition for.
:return: A random completing next word, or None if no completing transitions exist.
:return: A random completing next word, or None if none exist.
"""
async with self._connection.execute(
"SELECT next_word FROM markov_transitions WHERE channel_id = ? AND word = ? AND SUBSTR(next_word, -1, 1) IN ('.', '!', '?', '§') ORDER BY RANDOM() LIMIT 1",
"SELECT next_word FROM markov_transitions"
" WHERE channel_id = ? AND word = ?"
" AND SUBSTR(next_word, -1, 1)"
" IN ('.', '!', '?', '§')"
" ORDER BY RANDOM() LIMIT 1",
(channel_id, word),
) as cursor:
row = await cursor.fetchone()
return row[0] if row else None
async def add_image(self, channel_id: int, user_id: int, url: str) -> None:
"""
Stores an image URL for a given channel.
"""Store an image URL for a given channel.
:param channel_id: The Discord channel ID.
:param user_id: The Discord user ID of the contributor.
@@ -231,29 +241,31 @@ class Database:
await self._maybe_commit()
async def get_random_image(self, channel_id: int) -> str | None:
"""
Returns a random image URL for a given channel.
"""Return a random image URL for a given channel.
:param channel_id: The Discord channel ID.
:return: A random image URL, or None if no images exist for this channel.
:return: A random image URL, or None if none exist.
"""
async with self._connection.execute(
"SELECT url FROM channel_images WHERE channel_id = ? ORDER BY RANDOM() LIMIT 1",
"SELECT url FROM channel_images"
" WHERE channel_id = ?"
" ORDER BY RANDOM() LIMIT 1",
(channel_id,),
) as cursor:
row = await cursor.fetchone()
return row[0] if row else None
async def set_flag(self, entity_type: str, entity_id: str, flag_name: str) -> None:
"""
Sets a flag on a given entity. If the flag is already set, this is a no-op.
"""Set a flag on a given entity. If the flag is already set, this is a no-op.
:param entity_type: The type of entity ("channel", "server", or "user").
:param entity_type: The entity type ("channel", "server", "user").
:param entity_id: The Discord ID of the entity.
:param flag_name: The name of the flag to set.
"""
await self._connection.execute(
"INSERT OR IGNORE INTO flags (entity_type, entity_id, flag_name) VALUES (?, ?, ?)",
"INSERT OR IGNORE INTO flags"
" (entity_type, entity_id, flag_name)"
" VALUES (?, ?, ?)",
(entity_type, entity_id, flag_name),
)
await self._maybe_commit()
@@ -261,15 +273,17 @@ class Database:
async def clear_flag(
self, entity_type: str, entity_id: str, flag_name: str
) -> None:
"""
Clears a flag on a given entity. If the flag is not set, this is a no-op.
"""Clear a flag on a given entity. If the flag is not set, this is a no-op.
:param entity_type: The type of entity ("channel", "server", or "user").
:param entity_type: The entity type ("channel", "server", "user").
:param entity_id: The Discord ID of the entity.
:param flag_name: The name of the flag to clear.
"""
await self._connection.execute(
"DELETE FROM flags WHERE entity_type = ? AND entity_id = ? AND flag_name = ?",
"DELETE FROM flags"
" WHERE entity_type = ?"
" AND entity_id = ?"
" AND flag_name = ?",
(entity_type, entity_id, flag_name),
)
await self._maybe_commit()
@@ -277,23 +291,24 @@ class Database:
async def is_flag_set(
self, entity_type: str, entity_id: str, flag_name: str
) -> bool:
"""
Checks whether a flag is set on a given entity.
"""Check whether a flag is set on a given entity.
:param entity_type: The type of entity ("channel", "server", or "user").
:param entity_type: The entity type ("channel", "server", "user").
:param entity_id: The Discord ID of the entity.
:param flag_name: The name of the flag to check.
:return: True if the flag is set, False otherwise.
"""
async with self._connection.execute(
"SELECT 1 FROM flags WHERE entity_type = ? AND entity_id = ? AND flag_name = ?",
"SELECT 1 FROM flags"
" WHERE entity_type = ?"
" AND entity_id = ?"
" AND flag_name = ?",
(entity_type, entity_id, flag_name),
) as cursor:
return await cursor.fetchone() is not None
async def is_channel_ingested(self, channel_id: int) -> bool:
"""
Checks whether a channel has already been bulk-ingested.
"""Check whether a channel has already been bulk-ingested.
:param channel_id: The Discord channel ID.
:return: True if the channel has been ingested, False otherwise.
@@ -305,8 +320,7 @@ class Database:
return await cursor.fetchone() is not None
async def mark_channel_ingested(self, channel_id: int) -> None:
"""
Marks a channel as having been bulk-ingested.
"""Mark a channel as having been bulk-ingested.
:param channel_id: The Discord channel ID.
"""
@@ -317,7 +331,7 @@ class Database:
await self._maybe_commit()
async def _maybe_commit(self) -> None:
"""Tracks a pending write. Commits if the threshold is reached, otherwise starts a flush timer."""
"""Track a pending write and commit or start a flush timer."""
self._pending_writes += 1
if self._pending_writes >= AUTO_COMMIT_WRITE_THRESHOLD:
await self.commit()