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
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:
+72
-58
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user