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:
@@ -0,0 +1,32 @@
|
||||
name: Dependency Audit
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 0 * * 1"
|
||||
push:
|
||||
branches: [master]
|
||||
paths: [uv.lock]
|
||||
pull_request:
|
||||
paths: [uv.lock]
|
||||
|
||||
jobs:
|
||||
audit:
|
||||
name: Dependency Audit
|
||||
runs-on: logaldeveloper-archlinux
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Cache uv packages
|
||||
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||
with:
|
||||
path: ~/.cache/uv
|
||||
key: uv-${{ hashFiles('uv.lock') }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: uv sync
|
||||
|
||||
- name: Audit dependencies with pip-audit
|
||||
run: uv run pip-audit --skip-editable
|
||||
@@ -92,3 +92,24 @@ jobs:
|
||||
|
||||
- name: Check types with Mypy
|
||||
run: uv run mypy .
|
||||
|
||||
spelling:
|
||||
name: Spelling
|
||||
runs-on: logaldeveloper-archlinux
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Cache uv packages
|
||||
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||
with:
|
||||
path: ~/.cache/uv
|
||||
key: uv-${{ hashFiles('uv.lock') }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: uv sync
|
||||
|
||||
- name: Check spelling with codespell
|
||||
run: uv run codespell
|
||||
|
||||
+29
-15
@@ -12,10 +12,10 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""
|
||||
Entry point for the Crabstero Discord bot.
|
||||
"""Entry point for the Crabstero Discord bot.
|
||||
|
||||
Provides an argparse CLI with environment variable and systemd credential fallbacks for --token and --database-path.
|
||||
Provides an argparse CLI with environment variable and systemd credential
|
||||
fallbacks for --token and --database-path.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
@@ -32,8 +32,7 @@ logger = logging.getLogger("crabstero")
|
||||
|
||||
|
||||
def _read_credential(name: str) -> str | None:
|
||||
"""
|
||||
Read a value from a systemd credential file.
|
||||
"""Read a value from a systemd credential file.
|
||||
|
||||
Looks for a file named *name* inside the directory pointed to by the
|
||||
``CREDENTIALS_DIRECTORY`` environment variable (set automatically by
|
||||
@@ -53,8 +52,7 @@ def _read_credential(name: str) -> str | None:
|
||||
|
||||
|
||||
def _parse_args(argv: list[str] | None = None) -> argparse.Namespace:
|
||||
"""
|
||||
Parses command-line arguments with environment variable fallbacks.
|
||||
"""Parse command-line arguments with environment variable fallbacks.
|
||||
|
||||
:param argv: Optional argument list (defaults to sys.argv[1:]).
|
||||
:return: Parsed arguments namespace.
|
||||
@@ -66,41 +64,57 @@ def _parse_args(argv: list[str] | None = None) -> argparse.Namespace:
|
||||
parser.add_argument(
|
||||
"--token",
|
||||
default=os.environ.get("TOKEN") or _read_credential("token"),
|
||||
help="Discord bot token (default: TOKEN environment variable or systemd credential 'token').",
|
||||
help=(
|
||||
"Discord bot token"
|
||||
" (default: TOKEN environment variable"
|
||||
" or systemd credential 'token')."
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--database-path",
|
||||
"--database",
|
||||
default=os.environ.get("DATABASE_PATH", "crabstero.db"),
|
||||
help='Path to the SQLite database file (default: DATABASE_PATH environment variable or "crabstero.db").',
|
||||
help=(
|
||||
"Path to the SQLite database file"
|
||||
" (default: DATABASE_PATH environment"
|
||||
' variable or "crabstero.db").'
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--ingestion-workers",
|
||||
type=int,
|
||||
default=int(os.environ.get("INGESTION_WORKERS", "4")),
|
||||
help="Number of concurrent ingestion workers (default: INGESTION_WORKERS environment variable or 4).",
|
||||
help=(
|
||||
"Number of concurrent ingestion workers"
|
||||
" (default: INGESTION_WORKERS environment"
|
||||
" variable or 4)."
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--ingest-only",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Run in ingest-only mode: ingest channel history and real-time messages but never respond.",
|
||||
help=(
|
||||
"Run in ingest-only mode: ingest channel"
|
||||
" history and real-time messages but"
|
||||
" never respond."
|
||||
),
|
||||
)
|
||||
|
||||
args = parser.parse_args(argv)
|
||||
|
||||
if args.token is None:
|
||||
parser.error(
|
||||
"a Discord bot token is required via --token, the TOKEN environment variable, or a systemd credential named 'token'"
|
||||
"a Discord bot token is required via --token,"
|
||||
" the TOKEN environment variable,"
|
||||
" or a systemd credential named 'token'"
|
||||
)
|
||||
|
||||
return args
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""
|
||||
Main entry point. Parses arguments, configures logging, and starts the bot.
|
||||
"""
|
||||
"""Run the bot. Parse arguments, configure logging, and start the bot."""
|
||||
args = _parse_args()
|
||||
|
||||
logging.basicConfig(
|
||||
|
||||
+14
-17
@@ -12,8 +12,7 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""
|
||||
The simple nonversation Discord bot.
|
||||
"""The simple nonversation Discord bot.
|
||||
|
||||
Provides the Crabstero subclass that owns the full bot lifecycle: database connection,
|
||||
cog loading, ingestion worker pool, and graceful shutdown.
|
||||
@@ -35,8 +34,7 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Crabstero(commands.Bot):
|
||||
"""
|
||||
Central bot subclass that owns all lifecycle state.
|
||||
"""Central bot subclass that owns all lifecycle state.
|
||||
|
||||
The database is opened in setup_hook and closed in close(). Background
|
||||
ingestion is handled by a bounded queue and a fixed worker pool.
|
||||
@@ -49,8 +47,7 @@ class Crabstero(commands.Bot):
|
||||
ingestion_workers: int = 4,
|
||||
ingest_only: bool = False,
|
||||
) -> None:
|
||||
"""
|
||||
Configures intents, stores configuration, and prepares ingestion queue state.
|
||||
"""Configure intents, store configuration, and prepare ingestion queue state.
|
||||
|
||||
:param token: The Discord bot token.
|
||||
:param database_path: The file path to the SQLite database.
|
||||
@@ -78,10 +75,11 @@ class Crabstero(commands.Bot):
|
||||
self._ingestion_workers: list[asyncio.Task[None]] = []
|
||||
self.db: Database
|
||||
|
||||
self.http.user_agent = f"DiscordBot (https://git.logal.dev/LogalDeveloper/Crabstero, {crabstero_version})"
|
||||
repo_url = "https://git.logal.dev/LogalDeveloper/Crabstero"
|
||||
self.http.user_agent = f"DiscordBot ({repo_url}, {crabstero_version})"
|
||||
|
||||
async def setup_hook(self) -> None:
|
||||
"""Opens the database, starts ingestion workers, loads all cogs, and syncs slash commands if changed."""
|
||||
"""Open the database, start ingestion workers, and load all cogs."""
|
||||
self.db = await Database.connect(self._database_path)
|
||||
self._start_ingestion_workers()
|
||||
|
||||
@@ -92,7 +90,8 @@ class Crabstero(commands.Bot):
|
||||
await server_events.setup(self)
|
||||
|
||||
if not self._ingest_only:
|
||||
# Only sync slash commands if the registered commands differ from local definitions.
|
||||
# Only sync slash commands if the registered commands
|
||||
# differ from local definitions.
|
||||
local_commands = {
|
||||
cmd.name: cmd.description
|
||||
for cmd in self.tree.get_commands()
|
||||
@@ -111,12 +110,11 @@ class Crabstero(commands.Bot):
|
||||
await self.tree.sync()
|
||||
|
||||
async def on_ready(self) -> None:
|
||||
"""Logs that the bot has started successfully."""
|
||||
"""Log that the bot has started successfully."""
|
||||
logger.info("Crabstero started!")
|
||||
|
||||
async def start(self, token: str = "", *, reconnect: bool = True) -> None:
|
||||
"""
|
||||
Starts the bot using the stored token by default.
|
||||
"""Start the bot using the stored token by default.
|
||||
|
||||
:param token: Optional token override. Falls back to the stored token if empty.
|
||||
:param reconnect: Whether to automatically reconnect on disconnect.
|
||||
@@ -124,7 +122,7 @@ class Crabstero(commands.Bot):
|
||||
await super().start(token or self._token, reconnect=reconnect)
|
||||
|
||||
async def close(self) -> None:
|
||||
"""Cancels ingestion workers, closes the database, and then the bot connection."""
|
||||
"""Cancel ingestion workers, close the database, and then the bot connection."""
|
||||
if self.is_closed():
|
||||
return
|
||||
logger.info("Shutting down Crabstero...")
|
||||
@@ -139,21 +137,20 @@ class Crabstero(commands.Bot):
|
||||
def queue_channel_for_ingestion(
|
||||
self, channel: discord.TextChannel | discord.VoiceChannel
|
||||
) -> None:
|
||||
"""
|
||||
Enqueues a single channel for background message history ingestion.
|
||||
"""Enqueue a single channel for background message history ingestion.
|
||||
|
||||
:param channel: The channel to enqueue.
|
||||
"""
|
||||
self._ingestion_queue.put_nowait(channel)
|
||||
|
||||
def _start_ingestion_workers(self) -> None:
|
||||
"""Spawns the fixed pool of ingestion worker tasks."""
|
||||
"""Spawn the fixed pool of ingestion worker tasks."""
|
||||
for _ in range(self._ingestion_worker_count):
|
||||
task = asyncio.create_task(self._ingestion_worker())
|
||||
self._ingestion_workers.append(task)
|
||||
|
||||
async def _ingestion_worker(self) -> None:
|
||||
"""Loops forever pulling channels from the ingestion queue and ingesting them."""
|
||||
"""Loop forever pulling channels from the ingestion queue and ingesting them."""
|
||||
while True:
|
||||
channel = await self._ingestion_queue.get()
|
||||
try:
|
||||
|
||||
+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()
|
||||
|
||||
+5
-10
@@ -12,8 +12,7 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""
|
||||
Provides async convenience wrappers around the database flag methods.
|
||||
"""Provides async convenience wrappers around the database flag methods.
|
||||
|
||||
Accepts discord.py objects or raw integer IDs and translates them into the
|
||||
entity_type/entity_id pairs used by the database layer.
|
||||
@@ -45,8 +44,7 @@ class EntityType(enum.Enum):
|
||||
|
||||
|
||||
def _entity_id(entity: discord.abc.Snowflake | int) -> str:
|
||||
"""
|
||||
Extracts a string entity ID from a Discord object or raw integer ID.
|
||||
"""Extract a string entity ID from a Discord object or raw integer ID.
|
||||
|
||||
:param entity: A Discord entity or raw integer ID.
|
||||
:return: The entity ID as a string.
|
||||
@@ -60,8 +58,7 @@ async def set_flag(
|
||||
entity_type: EntityType,
|
||||
flag: Flag,
|
||||
) -> None:
|
||||
"""
|
||||
Sets a flag on a given entity.
|
||||
"""Set a flag on a given entity.
|
||||
|
||||
:param db: The database instance.
|
||||
:param entity: The Discord entity or raw integer ID to set the flag on.
|
||||
@@ -77,8 +74,7 @@ async def clear_flag(
|
||||
entity_type: EntityType,
|
||||
flag: Flag,
|
||||
) -> None:
|
||||
"""
|
||||
Clears a flag on a given entity.
|
||||
"""Clear a flag on a given entity.
|
||||
|
||||
:param db: The database instance.
|
||||
:param entity: The Discord entity or raw integer ID to clear the flag on.
|
||||
@@ -94,8 +90,7 @@ async def is_flag_set(
|
||||
entity_type: EntityType,
|
||||
flag: Flag,
|
||||
) -> bool:
|
||||
"""
|
||||
Checks whether a flag is set on a given entity.
|
||||
"""Check whether a flag is set on a given entity.
|
||||
|
||||
:param db: The database instance.
|
||||
:param entity: The Discord entity or raw integer ID to check.
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
"""Listener cogs for Discord events."""
|
||||
|
||||
@@ -12,8 +12,7 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""
|
||||
Handles responding to interactions.
|
||||
"""Handles responding to interactions.
|
||||
|
||||
Provides the /pingme slash command as a Cog with an app command.
|
||||
"""
|
||||
@@ -35,13 +34,10 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class InteractionCog(commands.Cog):
|
||||
"""
|
||||
Cog for handling slash command interactions.
|
||||
"""
|
||||
"""Cog for handling slash command interactions."""
|
||||
|
||||
def __init__(self, bot: Crabstero) -> None:
|
||||
"""
|
||||
Creates a new interaction handler cog.
|
||||
"""Create a new interaction handler cog.
|
||||
|
||||
:param bot: The bot instance.
|
||||
"""
|
||||
@@ -49,11 +45,13 @@ class InteractionCog(commands.Cog):
|
||||
|
||||
@app_commands.command(
|
||||
name="pingme",
|
||||
description="Opts into (or back out of) receiving pings for generated message which mention you.",
|
||||
description=(
|
||||
"Opts into (or back out of) receiving pings"
|
||||
" for generated message which mention you."
|
||||
),
|
||||
)
|
||||
async def pingme(self, interaction: discord.Interaction) -> None:
|
||||
"""
|
||||
Toggles the allowPings flag for the user who ran the command.
|
||||
"""Toggle the allowPings flag for the user who ran the command.
|
||||
|
||||
:param interaction: The interaction event.
|
||||
"""
|
||||
@@ -66,7 +64,9 @@ class InteractionCog(commands.Cog):
|
||||
self.bot.db, interaction.user, EntityType.USER, Flag.ALLOW_PINGS
|
||||
)
|
||||
await interaction.response.send_message(
|
||||
"I will no longer ping you for messages which mention you. If you decide to opt back in, run `/pingme` any time.",
|
||||
"I will no longer ping you for messages which"
|
||||
" mention you. If you decide to opt back in,"
|
||||
" run `/pingme` any time.",
|
||||
ephemeral=True,
|
||||
)
|
||||
else:
|
||||
@@ -74,23 +74,26 @@ class InteractionCog(commands.Cog):
|
||||
self.bot.db, interaction.user, EntityType.USER, Flag.ALLOW_PINGS
|
||||
)
|
||||
await interaction.response.send_message(
|
||||
"I will now ping you for messages which mention you. If you change your mind, run `/pingme` any time.",
|
||||
"I will now ping you for messages which mention"
|
||||
" you. If you change your mind, run"
|
||||
" `/pingme` any time.",
|
||||
ephemeral=True,
|
||||
)
|
||||
|
||||
except Exception:
|
||||
logger.exception(
|
||||
"An exception occurred while attempting to execute slash command responder for command 'pingme'."
|
||||
"An exception occurred while attempting to execute"
|
||||
" slash command responder for command 'pingme'."
|
||||
)
|
||||
await interaction.response.send_message(
|
||||
"An error occurred while executing your command. Please try again later.",
|
||||
"An error occurred while executing your command."
|
||||
" Please try again later.",
|
||||
ephemeral=True,
|
||||
)
|
||||
|
||||
|
||||
async def setup(bot: Crabstero) -> None:
|
||||
"""
|
||||
Adds the InteractionCog to the bot.
|
||||
"""Add the InteractionCog to the bot.
|
||||
|
||||
:param bot: The bot instance.
|
||||
"""
|
||||
|
||||
@@ -12,10 +12,10 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""
|
||||
Handles created messages.
|
||||
"""Handles created messages.
|
||||
|
||||
Responds to mentions and ingests normal text messages as a Cog with an on_message listener.
|
||||
Responds to mentions and ingests normal text messages as a Cog with an
|
||||
on_message listener.
|
||||
"""
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
@@ -30,13 +30,10 @@ if TYPE_CHECKING:
|
||||
|
||||
|
||||
class MessageCog(commands.Cog):
|
||||
"""
|
||||
Cog for handling message creation events.
|
||||
"""
|
||||
"""Cog for handling message creation events."""
|
||||
|
||||
def __init__(self, bot: Crabstero) -> None:
|
||||
"""
|
||||
Creates a new message creation handler cog.
|
||||
"""Create a new message creation handler cog.
|
||||
|
||||
:param bot: The bot instance.
|
||||
"""
|
||||
@@ -44,8 +41,7 @@ class MessageCog(commands.Cog):
|
||||
|
||||
@commands.Cog.listener()
|
||||
async def on_message(self, message: discord.Message) -> None:
|
||||
"""
|
||||
Responds to mentions and ingests normal text messages.
|
||||
"""Respond to mentions and ingest normal text messages.
|
||||
|
||||
:param message: The message event.
|
||||
"""
|
||||
@@ -79,8 +75,7 @@ class MessageCog(commands.Cog):
|
||||
|
||||
|
||||
async def setup(bot: Crabstero) -> None:
|
||||
"""
|
||||
Adds the MessageCog to the bot.
|
||||
"""Add the MessageCog to the bot.
|
||||
|
||||
:param bot: The bot instance.
|
||||
"""
|
||||
|
||||
@@ -12,11 +12,11 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""
|
||||
Handles server-level events which trigger channel history ingestion.
|
||||
"""Handles server-level events which trigger channel history ingestion.
|
||||
|
||||
All of these events share the same outcome: queuing all textable channels for message history
|
||||
ingestion when something changes that may grant new read permissions.
|
||||
All of these events share the same outcome: queuing all textable channels
|
||||
for message history ingestion when something changes that may grant new
|
||||
read permissions.
|
||||
"""
|
||||
|
||||
import contextlib
|
||||
@@ -35,13 +35,10 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ServerEventsCog(commands.Cog):
|
||||
"""
|
||||
Cog for handling server-level events that trigger channel history ingestion.
|
||||
"""
|
||||
"""Cog for handling server-level events that trigger channel history ingestion."""
|
||||
|
||||
def __init__(self, bot: Crabstero) -> None:
|
||||
"""
|
||||
Creates a new server events handler cog.
|
||||
"""Create a new server events handler cog.
|
||||
|
||||
:param bot: The bot instance.
|
||||
"""
|
||||
@@ -49,9 +46,9 @@ class ServerEventsCog(commands.Cog):
|
||||
|
||||
@commands.Cog.listener()
|
||||
async def on_guild_join(self, guild: discord.Guild) -> None:
|
||||
"""
|
||||
Queues all text channels for message history ingestion when joining a new server.
|
||||
Also logs the join and sends an embed to the bot owner.
|
||||
"""Queue all text channels for ingestion when joining a new server.
|
||||
|
||||
Also log the join and send an embed to the bot owner.
|
||||
|
||||
:param guild: The guild that was joined.
|
||||
"""
|
||||
@@ -79,8 +76,7 @@ class ServerEventsCog(commands.Cog):
|
||||
|
||||
@commands.Cog.listener()
|
||||
async def on_guild_available(self, guild: discord.Guild) -> None:
|
||||
"""
|
||||
Queues all textable channels for message history ingestion when a server becomes available.
|
||||
"""Queue all textable channels for ingestion when a server becomes available.
|
||||
|
||||
:param guild: The guild that became available.
|
||||
"""
|
||||
@@ -90,9 +86,9 @@ class ServerEventsCog(commands.Cog):
|
||||
async def on_guild_role_update(
|
||||
self, before: discord.Role, after: discord.Role
|
||||
) -> None:
|
||||
"""
|
||||
Queues all text channels for message history ingestion when role permissions change,
|
||||
but only if the bot is a member of the updated role.
|
||||
"""Queue all text channels for ingestion when role permissions change.
|
||||
|
||||
Only triggers if the bot is a member of the updated role.
|
||||
|
||||
:param before: The role before the update.
|
||||
:param after: The role after the update.
|
||||
@@ -107,9 +103,7 @@ class ServerEventsCog(commands.Cog):
|
||||
async def on_guild_channel_update(
|
||||
self, before: discord.abc.GuildChannel, after: discord.abc.GuildChannel
|
||||
) -> None:
|
||||
"""
|
||||
Queues all text channels for message history ingestion when channel override permissions
|
||||
change.
|
||||
"""Queue all text channels for ingestion on permission change.
|
||||
|
||||
:param before: The channel before the update.
|
||||
:param after: The channel after the update.
|
||||
@@ -121,8 +115,7 @@ class ServerEventsCog(commands.Cog):
|
||||
|
||||
|
||||
async def setup(bot: Crabstero) -> None:
|
||||
"""
|
||||
Adds the ServerEventsCog to the bot.
|
||||
"""Add the ServerEventsCog to the bot.
|
||||
|
||||
:param bot: The bot instance.
|
||||
"""
|
||||
|
||||
+18
-18
@@ -12,11 +12,11 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""
|
||||
Ingests sentences and generates new ones using a Markov chain backed by SQLite storage.
|
||||
"""Markov chain sentence ingestion and generation backed by SQLite.
|
||||
|
||||
The Markov chain stores word transitions per channel, using duplicate rows to represent frequency
|
||||
weight. Random selection via ORDER BY RANDOM() LIMIT 1 naturally preserves this weighting.
|
||||
The Markov chain stores word transitions per channel, using duplicate rows
|
||||
to represent frequency weight. Random selection via ORDER BY RANDOM()
|
||||
LIMIT 1 naturally preserves this weighting.
|
||||
"""
|
||||
|
||||
import re
|
||||
@@ -31,9 +31,10 @@ DEFAULT_SENTENCE_END = "\u00a7"
|
||||
|
||||
|
||||
def is_complete_sentence(sentence: str) -> bool:
|
||||
"""
|
||||
Checks whether a given sentence ends with a default sentence end character, a period, an
|
||||
exclamation mark, or a question mark.
|
||||
"""Check whether a sentence ends with a valid terminator.
|
||||
|
||||
Valid terminators are the section sign, period, exclamation mark, or
|
||||
question mark.
|
||||
|
||||
:param sentence: The sentence to test.
|
||||
:return: True if the sentence ends with a valid terminator, False otherwise.
|
||||
@@ -45,9 +46,10 @@ def is_complete_sentence(sentence: str) -> bool:
|
||||
|
||||
|
||||
async def ingest(db: Database, channel_id: int, user_id: int, paragraph: str) -> None:
|
||||
"""
|
||||
Ingests a string potentially containing multiple smaller sentences into the Markov chain
|
||||
for a given channel.
|
||||
"""Ingest a paragraph into the Markov chain for a given channel.
|
||||
|
||||
The paragraph may contain multiple sentences which are split and ingested
|
||||
individually.
|
||||
|
||||
:param db: The database instance.
|
||||
:param channel_id: The Discord channel ID to associate with this data.
|
||||
@@ -57,7 +59,8 @@ async def ingest(db: Database, channel_id: int, user_id: int, paragraph: str) ->
|
||||
if not is_complete_sentence(paragraph):
|
||||
paragraph += DEFAULT_SENTENCE_END
|
||||
|
||||
# Normalize whitespace, then split on sentence-ending punctuation followed by a space.
|
||||
# Normalize whitespace, then split on sentence-ending punctuation
|
||||
# followed by a space.
|
||||
normalized = re.sub(r" +", " ", paragraph.strip().replace("\n", " "))
|
||||
sentences = re.split(r"(?<=[.!?]) ", normalized)
|
||||
|
||||
@@ -68,8 +71,7 @@ async def ingest(db: Database, channel_id: int, user_id: int, paragraph: str) ->
|
||||
async def _ingest_sentence(
|
||||
db: Database, channel_id: int, user_id: int, sentence: str
|
||||
) -> None:
|
||||
"""
|
||||
Ingests a string containing a single sentence into the Markov chain for a given channel.
|
||||
"""Ingest a single sentence into the Markov chain for a given channel.
|
||||
|
||||
:param db: The database instance.
|
||||
:param channel_id: The Discord channel ID to associate with this data.
|
||||
@@ -99,14 +101,12 @@ async def _ingest_sentence(
|
||||
async def generate(
|
||||
db: Database, channel_id: int, soft_limit: int = 750, hard_limit: int = 1000
|
||||
) -> str:
|
||||
"""
|
||||
Generates a new sentence using words learned from previously ingested sentences for a given
|
||||
channel.
|
||||
"""Generate a new sentence from previously ingested words for a channel.
|
||||
|
||||
:param db: The database instance.
|
||||
:param channel_id: The Discord channel ID to generate from.
|
||||
:param soft_limit: The amount of characters to try and limit sentence length around.
|
||||
:param hard_limit: The amount of characters to cut off the sentence at if it gets too long.
|
||||
:param soft_limit: Character count to aim for when wrapping up.
|
||||
:param hard_limit: Character count to hard-cut the sentence at.
|
||||
:return: A new generated sentence.
|
||||
"""
|
||||
word = await db.get_random_start_word(channel_id)
|
||||
|
||||
+7
-10
@@ -12,11 +12,11 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""
|
||||
Assists with generating Discord messages in response to other users and ingesting raw messages.
|
||||
"""Generate Discord reply messages and ingest raw messages.
|
||||
|
||||
Orchestrates Markov chain generation and ingestion in the context of Discord messages, handling
|
||||
reply logic, embed generation, mention filtering, and message ingestion with flag checks.
|
||||
Orchestrates Markov chain generation and ingestion in the context of
|
||||
Discord messages, handling reply logic, embed generation, mention
|
||||
filtering, and message ingestion with flag checks.
|
||||
"""
|
||||
|
||||
import re
|
||||
@@ -39,8 +39,7 @@ _EMBED_CHANCE_THRESHOLD = 95 # Out of 100; sends an embed ~5% of the time.
|
||||
|
||||
|
||||
async def reply_to_message(db: Database, message: discord.Message) -> None:
|
||||
"""
|
||||
Sends a new message in Discord in response to a given message.
|
||||
"""Send a new message in Discord in response to a given message.
|
||||
|
||||
:param db: The database instance.
|
||||
:param message: The message prompting the response.
|
||||
@@ -115,8 +114,7 @@ async def reply_to_message(db: Database, message: discord.Message) -> None:
|
||||
|
||||
|
||||
async def ingest_message(db: Database, message: discord.Message) -> None:
|
||||
"""
|
||||
Ingests a given message into its channel's Markov chain.
|
||||
"""Ingest a given message into its channel's Markov chain.
|
||||
|
||||
:param db: The database instance.
|
||||
:param message: The message to ingest.
|
||||
@@ -148,8 +146,7 @@ async def ingest_message(db: Database, message: discord.Message) -> None:
|
||||
async def _ingest_embed(
|
||||
db: Database, channel_id: int, user_id: int, embed: discord.Embed
|
||||
) -> None:
|
||||
"""
|
||||
Ingests a given embed into a given channel's Markov chain.
|
||||
"""Ingest a given embed into a given channel's Markov chain.
|
||||
|
||||
:param db: The database instance.
|
||||
:param channel_id: The ID of the channel to use for the Markov chain.
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
"""Background tasks for Crabstero."""
|
||||
|
||||
@@ -12,8 +12,7 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""
|
||||
Bulk-ingests the message history of channels.
|
||||
"""Bulk-ingests the message history of channels.
|
||||
|
||||
Channels are enqueued for processing by the bot's fixed ingestion worker pool.
|
||||
"""
|
||||
@@ -37,8 +36,7 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def queue_channels_for_ingestion(guild: discord.Guild, bot: Crabstero) -> None:
|
||||
"""
|
||||
Enqueues every textable channel in a guild for message history ingestion.
|
||||
"""Enqueue every textable channel in a guild for message history ingestion.
|
||||
|
||||
:param guild: The Discord guild whose channels should be ingested.
|
||||
:param bot: The bot instance.
|
||||
@@ -52,9 +50,10 @@ async def ingest_channel(
|
||||
channel: discord.TextChannel | discord.VoiceChannel,
|
||||
db: Database,
|
||||
) -> None:
|
||||
"""
|
||||
Bulk-ingests the message history of a given channel. The task will be ended early if
|
||||
permissions do not allow ingesting this channel or if it has already been ingested in the past.
|
||||
"""Bulk-ingest the message history of a given channel.
|
||||
|
||||
End early if permissions do not allow ingesting this channel or if it has
|
||||
already been ingested.
|
||||
|
||||
:param channel: The channel to ingest.
|
||||
:param db: The database instance.
|
||||
@@ -62,7 +61,8 @@ async def ingest_channel(
|
||||
try:
|
||||
if not channel.permissions_for(channel.guild.me).read_message_history:
|
||||
logger.warning(
|
||||
"[%s] Unable to ingest textable channel history due to lacking permissions. Ignoring.",
|
||||
"[%s] Unable to ingest channel history"
|
||||
" due to lacking permissions. Ignoring.",
|
||||
channel.id,
|
||||
)
|
||||
return
|
||||
@@ -80,7 +80,7 @@ async def ingest_channel(
|
||||
await ingest_message(db, message)
|
||||
|
||||
logger.info(
|
||||
"[%s] Ingestion of textable channel history complete. %d messages ingested.",
|
||||
"[%s] Ingestion of channel history complete. %d messages ingested.",
|
||||
channel.id,
|
||||
count,
|
||||
)
|
||||
|
||||
+47
-2
@@ -25,6 +25,8 @@ dev = [
|
||||
"pytest-cov>=7.0.0",
|
||||
"mypy>=1.18.1",
|
||||
"ruff>=0.15.1",
|
||||
"pip-audit>=2.10.0",
|
||||
"codespell>=2.4.1",
|
||||
]
|
||||
|
||||
[tool.uv]
|
||||
@@ -60,20 +62,63 @@ extend-exclude = ["crabstero/_version.py"]
|
||||
|
||||
[tool.ruff.lint]
|
||||
select = [
|
||||
# Core
|
||||
"F", # Pyflakes
|
||||
"E", # pycodestyle errors
|
||||
"W", # pycodestyle warnings
|
||||
"N", # pep8-naming
|
||||
"D", # pydocstyle
|
||||
"I", # isort
|
||||
"UP", # pyupgrade
|
||||
"ICN", # flake8-import-conventions
|
||||
|
||||
# Correctness & bugs
|
||||
"B", # flake8-bugbear
|
||||
"ASYNC", # flake8-async
|
||||
"DTZ", # flake8-datetimez
|
||||
"RSE", # flake8-raise
|
||||
"RET", # flake8-return
|
||||
"A", # flake8-builtins
|
||||
"PIE", # flake8-pie
|
||||
|
||||
# Modernization & simplification
|
||||
"UP", # pyupgrade
|
||||
"SIM", # flake8-simplify
|
||||
"C4", # flake8-comprehensions
|
||||
"FLY", # flynt (f-string conversion)
|
||||
"PTH", # flake8-use-pathlib
|
||||
|
||||
# Performance
|
||||
"PERF", # Perflint
|
||||
|
||||
# Security
|
||||
"S", # flake8-bandit
|
||||
|
||||
# Code hygiene
|
||||
"T10", # flake8-debugger
|
||||
"T20", # flake8-print
|
||||
"ERA", # eradicate
|
||||
"PGH", # pygrep-hooks
|
||||
"TCH", # flake8-type-checking
|
||||
|
||||
# Testing
|
||||
"PT", # flake8-pytest-style
|
||||
|
||||
# Ruff-specific
|
||||
"RUF", # Ruff-specific rules
|
||||
]
|
||||
ignore = [
|
||||
"E501", # line length enforced by the formatter; residual violations are in strings/docstrings/comments
|
||||
"D203", # incompatible with D211 (no blank line before class docstring)
|
||||
"D213", # incompatible with D212 (summary on first line)
|
||||
]
|
||||
|
||||
[tool.ruff.lint.per-file-ignores]
|
||||
"crabstero/__init__.py" = ["E402"] # constants defined before imports intentionally
|
||||
"crabstero/__main__.py" = ["T201"] # CLI entry point uses print() for user output
|
||||
"tests/**" = ["S101", "S311"] # assert is standard for pytest; random is fine in tests
|
||||
|
||||
[tool.codespell]
|
||||
skip = "crabstero/_version.py,uv.lock"
|
||||
|
||||
[tool.coverage.run]
|
||||
source = ["crabstero"]
|
||||
omit = [
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
"""Test suite for Crabstero."""
|
||||
|
||||
@@ -11,3 +11,5 @@
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Shared pytest fixtures for the test suite."""
|
||||
|
||||
@@ -12,9 +12,12 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Tests for version availability."""
|
||||
|
||||
import crabstero
|
||||
|
||||
|
||||
def test_version_is_available() -> None:
|
||||
"""Verify __version__ is a non-empty string."""
|
||||
assert isinstance(crabstero.__version__, str)
|
||||
assert crabstero.__version__
|
||||
|
||||
@@ -132,6 +132,76 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/22/91616fe707a5c5510de2cac9b046a30defe7007ba8a0c04f9c08f27df312/audioop_lts-0.2.2-cp314-cp314t-win_arm64.whl", hash = "sha256:b492c3b040153e68b9fdaff5913305aaaba5bb433d8a7f73d5cf6a64ed3cc1dd", size = 25206, upload-time = "2025-08-05T16:43:16.444Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "boolean-py"
|
||||
version = "5.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c4/cf/85379f13b76f3a69bca86b60237978af17d6aa0bc5998978c3b8cf05abb2/boolean_py-5.0.tar.gz", hash = "sha256:60cbc4bad079753721d32649545505362c754e121570ada4658b852a3a318d95", size = 37047, upload-time = "2025-04-03T10:39:49.734Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/ca/78d423b324b8d77900030fa59c4aa9054261ef0925631cd2501dd015b7b7/boolean_py-5.0-py3-none-any.whl", hash = "sha256:ef28a70bd43115208441b53a045d1549e2f0ec6e3d08a9d142cbc41c1938e8d9", size = 26577, upload-time = "2025-04-03T10:39:48.449Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cachecontrol"
|
||||
version = "0.14.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "msgpack" },
|
||||
{ name = "requests" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/2d/f6/c972b32d80760fb79d6b9eeb0b3010a46b89c0b23cf6329417ff7886cd22/cachecontrol-0.14.4.tar.gz", hash = "sha256:e6220afafa4c22a47dd0badb319f84475d79108100d04e26e8542ef7d3ab05a1", size = 16150, upload-time = "2025-11-14T04:32:13.138Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ef/79/c45f2d53efe6ada1110cf6f9fca095e4ff47a0454444aefdde6ac4789179/cachecontrol-0.14.4-py3-none-any.whl", hash = "sha256:b7ac014ff72ee199b5f8af1de29d60239954f223e948196fa3d84adaffc71d2b", size = 22247, upload-time = "2025-11-14T04:32:11.733Z" },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
filecache = [
|
||||
{ name = "filelock" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "certifi"
|
||||
version = "2026.1.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e0/2d/a891ca51311197f6ad14a7ef42e2399f36cf2f9bd44752b3dc4eab60fdc5/certifi-2026.1.4.tar.gz", hash = "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120", size = 154268, upload-time = "2026-01-04T02:42:41.825Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c", size = 152900, upload-time = "2026-01-04T02:42:40.15Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "charset-normalizer"
|
||||
version = "3.4.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746, upload-time = "2025-10-14T04:41:33.773Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889, upload-time = "2025-10-14T04:41:34.897Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641, upload-time = "2025-10-14T04:41:36.116Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779, upload-time = "2025-10-14T04:41:37.229Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035, upload-time = "2025-10-14T04:41:38.368Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542, upload-time = "2025-10-14T04:41:39.862Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524, upload-time = "2025-10-14T04:41:41.319Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395, upload-time = "2025-10-14T04:41:42.539Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680, upload-time = "2025-10-14T04:41:43.661Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045, upload-time = "2025-10-14T04:41:44.821Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687, upload-time = "2025-10-14T04:41:46.442Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014, upload-time = "2025-10-14T04:41:47.631Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044, upload-time = "2025-10-14T04:41:48.81Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940, upload-time = "2025-10-14T04:41:49.946Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104, upload-time = "2025-10-14T04:41:51.051Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743, upload-time = "2025-10-14T04:41:52.122Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codespell"
|
||||
version = "2.4.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/15/e0/709453393c0ea77d007d907dd436b3ee262e28b30995ea1aa36c6ffbccaf/codespell-2.4.1.tar.gz", hash = "sha256:299fcdcb09d23e81e35a671bbe746d5ad7e8385972e65dbb833a2eaac33c01e5", size = 344740, upload-time = "2025-01-28T18:52:39.411Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/20/01/b394922252051e97aab231d416c86da3d8a6d781eeadcdca1082867de64e/codespell-2.4.1-py3-none-any.whl", hash = "sha256:3dadafa67df7e4a3dbf51e0d7315061b80d265f9552ebd699b3dd6834b47e425", size = 344501, upload-time = "2025-01-28T18:52:37.057Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "colorama"
|
||||
version = "0.4.6"
|
||||
@@ -190,7 +260,9 @@ dependencies = [
|
||||
|
||||
[package.dev-dependencies]
|
||||
dev = [
|
||||
{ name = "codespell" },
|
||||
{ name = "mypy" },
|
||||
{ name = "pip-audit" },
|
||||
{ name = "pytest" },
|
||||
{ name = "pytest-asyncio" },
|
||||
{ name = "pytest-cov" },
|
||||
@@ -205,13 +277,39 @@ requires-dist = [
|
||||
|
||||
[package.metadata.requires-dev]
|
||||
dev = [
|
||||
{ name = "codespell", specifier = ">=2.4.1" },
|
||||
{ name = "mypy", specifier = ">=1.18.1" },
|
||||
{ name = "pip-audit", specifier = ">=2.10.0" },
|
||||
{ name = "pytest", specifier = ">=9.0.2" },
|
||||
{ name = "pytest-asyncio", specifier = ">=1.3.0" },
|
||||
{ name = "pytest-cov", specifier = ">=7.0.0" },
|
||||
{ name = "ruff", specifier = ">=0.15.1" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cyclonedx-python-lib"
|
||||
version = "11.6.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "license-expression" },
|
||||
{ name = "packageurl-python" },
|
||||
{ name = "py-serializable" },
|
||||
{ name = "sortedcontainers" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/89/ed/54ecfa25fc145c58bf4f98090f7b6ffe5188d0759248c57dde44427ea239/cyclonedx_python_lib-11.6.0.tar.gz", hash = "sha256:7fb85a4371fa3a203e5be577ac22b7e9a7157f8b0058b7448731474d6dea7bf0", size = 1408147, upload-time = "2025-12-02T12:28:46.446Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/1b/534ad8a5e0f9470522811a8e5a9bc5d328fb7738ba29faf357467a4ef6d0/cyclonedx_python_lib-11.6.0-py3-none-any.whl", hash = "sha256:94f4aae97db42a452134dafdddcfab9745324198201c4777ed131e64c8380759", size = 511157, upload-time = "2025-12-02T12:28:44.158Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "defusedxml"
|
||||
version = "0.7.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/0f/d5/c66da9b79e5bdb124974bfe172b4daf3c984ebd9c2a06e2b8a4dc7331c72/defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69", size = 75520, upload-time = "2021-03-08T10:59:26.269Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61", size = 25604, upload-time = "2021-03-08T10:59:24.45Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "discord-py"
|
||||
version = "2.6.4"
|
||||
@@ -225,6 +323,15 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ca/ae/3d3a89b06f005dc5fa8618528dde519b3ba7775c365750f7932b9831ef05/discord_py-2.6.4-py3-none-any.whl", hash = "sha256:2783b7fb7f8affa26847bfc025144652c294e8fe6e0f8877c67ed895749eb227", size = 1209284, upload-time = "2025-10-08T21:45:41.679Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "filelock"
|
||||
version = "3.24.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/73/92/a8e2479937ff39185d20dd6a851c1a63e55849e447a55e798cc2e1f49c65/filelock-3.24.3.tar.gz", hash = "sha256:011a5644dc937c22699943ebbfc46e969cdde3e171470a6e40b9533e5a72affa", size = 37935, upload-time = "2026-02-19T00:48:20.543Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/0f/5d0c71a1aefeb08efff26272149e07ab922b64f46c63363756224bd6872e/filelock-3.24.3-py3-none-any.whl", hash = "sha256:426e9a4660391f7f8a810d71b0555bce9008b0a1cc342ab1f6947d37639e002d", size = 24331, upload-time = "2026-02-19T00:48:18.465Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "frozenlist"
|
||||
version = "1.8.0"
|
||||
@@ -314,6 +421,65 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/fc/85/69f92b2a7b3c0f88ffe107c86b952b397004b5b8ea5a81da3d9c04c04422/librt-0.7.8-cp314-cp314t-win_arm64.whl", hash = "sha256:8766ece9de08527deabcd7cb1b4f1a967a385d26e33e536d6d8913db6ef74f06", size = 40550, upload-time = "2026-01-14T12:56:01.542Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "license-expression"
|
||||
version = "30.4.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "boolean-py" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/40/71/d89bb0e71b1415453980fd32315f2a037aad9f7f70f695c7cec7035feb13/license_expression-30.4.4.tar.gz", hash = "sha256:73448f0aacd8d0808895bdc4b2c8e01a8d67646e4188f887375398c761f340fd", size = 186402, upload-time = "2025-07-22T11:13:32.17Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/af/40/791891d4c0c4dab4c5e187c17261cedc26285fd41541577f900470a45a4d/license_expression-30.4.4-py3-none-any.whl", hash = "sha256:421788fdcadb41f049d2dc934ce666626265aeccefddd25e162a26f23bcbf8a4", size = 120615, upload-time = "2025-07-22T11:13:31.217Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "markdown-it-py"
|
||||
version = "4.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "mdurl" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mdurl"
|
||||
version = "0.1.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "msgpack"
|
||||
version = "1.1.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/4d/f2/bfb55a6236ed8725a96b0aa3acbd0ec17588e6a2c3b62a93eb513ed8783f/msgpack-1.1.2.tar.gz", hash = "sha256:3b60763c1373dd60f398488069bcdc703cd08a711477b5d480eecc9f9626f47e", size = 173581, upload-time = "2025-10-08T09:15:56.596Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/22/71/201105712d0a2ff07b7873ed3c220292fb2ea5120603c00c4b634bcdafb3/msgpack-1.1.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e23ce8d5f7aa6ea6d2a2b326b4ba46c985dbb204523759984430db7114f8aa00", size = 81127, upload-time = "2025-10-08T09:15:24.408Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1b/9f/38ff9e57a2eade7bf9dfee5eae17f39fc0e998658050279cbb14d97d36d9/msgpack-1.1.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6c15b7d74c939ebe620dd8e559384be806204d73b4f9356320632d783d1f7939", size = 84981, upload-time = "2025-10-08T09:15:25.812Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8e/a9/3536e385167b88c2cc8f4424c49e28d49a6fc35206d4a8060f136e71f94c/msgpack-1.1.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:99e2cb7b9031568a2a5c73aa077180f93dd2e95b4f8d3b8e14a73ae94a9e667e", size = 411885, upload-time = "2025-10-08T09:15:27.22Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2f/40/dc34d1a8d5f1e51fc64640b62b191684da52ca469da9cd74e84936ffa4a6/msgpack-1.1.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:180759d89a057eab503cf62eeec0aa61c4ea1200dee709f3a8e9397dbb3b6931", size = 419658, upload-time = "2025-10-08T09:15:28.4Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3b/ef/2b92e286366500a09a67e03496ee8b8ba00562797a52f3c117aa2b29514b/msgpack-1.1.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:04fb995247a6e83830b62f0b07bf36540c213f6eac8e851166d8d86d83cbd014", size = 403290, upload-time = "2025-10-08T09:15:29.764Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/78/90/e0ea7990abea5764e4655b8177aa7c63cdfa89945b6e7641055800f6c16b/msgpack-1.1.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8e22ab046fa7ede9e36eeb4cfad44d46450f37bb05d5ec482b02868f451c95e2", size = 415234, upload-time = "2025-10-08T09:15:31.022Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/72/4e/9390aed5db983a2310818cd7d3ec0aecad45e1f7007e0cda79c79507bb0d/msgpack-1.1.2-cp314-cp314-win32.whl", hash = "sha256:80a0ff7d4abf5fecb995fcf235d4064b9a9a8a40a3ab80999e6ac1e30b702717", size = 66391, upload-time = "2025-10-08T09:15:32.265Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6e/f1/abd09c2ae91228c5f3998dbd7f41353def9eac64253de3c8105efa2082f7/msgpack-1.1.2-cp314-cp314-win_amd64.whl", hash = "sha256:9ade919fac6a3e7260b7f64cea89df6bec59104987cbea34d34a2fa15d74310b", size = 73787, upload-time = "2025-10-08T09:15:33.219Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/b0/9d9f667ab48b16ad4115c1935d94023b82b3198064cb84a123e97f7466c1/msgpack-1.1.2-cp314-cp314-win_arm64.whl", hash = "sha256:59415c6076b1e30e563eb732e23b994a61c159cec44deaf584e5cc1dd662f2af", size = 66453, upload-time = "2025-10-08T09:15:34.225Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/16/67/93f80545eb1792b61a217fa7f06d5e5cb9e0055bed867f43e2b8e012e137/msgpack-1.1.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:897c478140877e5307760b0ea66e0932738879e7aa68144d9b78ea4c8302a84a", size = 85264, upload-time = "2025-10-08T09:15:35.61Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/87/1c/33c8a24959cf193966ef11a6f6a2995a65eb066bd681fd085afd519a57ce/msgpack-1.1.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a668204fa43e6d02f89dbe79a30b0d67238d9ec4c5bd8a940fc3a004a47b721b", size = 89076, upload-time = "2025-10-08T09:15:36.619Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fc/6b/62e85ff7193663fbea5c0254ef32f0c77134b4059f8da89b958beb7696f3/msgpack-1.1.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5559d03930d3aa0f3aacb4c42c776af1a2ace2611871c84a75afe436695e6245", size = 435242, upload-time = "2025-10-08T09:15:37.647Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c1/47/5c74ecb4cc277cf09f64e913947871682ffa82b3b93c8dad68083112f412/msgpack-1.1.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:70c5a7a9fea7f036b716191c29047374c10721c389c21e9ffafad04df8c52c90", size = 432509, upload-time = "2025-10-08T09:15:38.794Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/24/a4/e98ccdb56dc4e98c929a3f150de1799831c0a800583cde9fa022fa90602d/msgpack-1.1.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f2cb069d8b981abc72b41aea1c580ce92d57c673ec61af4c500153a626cb9e20", size = 415957, upload-time = "2025-10-08T09:15:40.238Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/28/6951f7fb67bc0a4e184a6b38ab71a92d9ba58080b27a77d3e2fb0be5998f/msgpack-1.1.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d62ce1f483f355f61adb5433ebfd8868c5f078d1a52d042b0a998682b4fa8c27", size = 422910, upload-time = "2025-10-08T09:15:41.505Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f0/03/42106dcded51f0a0b5284d3ce30a671e7bd3f7318d122b2ead66ad289fed/msgpack-1.1.2-cp314-cp314t-win32.whl", hash = "sha256:1d1418482b1ee984625d88aa9585db570180c286d942da463533b238b98b812b", size = 75197, upload-time = "2025-10-08T09:15:42.954Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/15/86/d0071e94987f8db59d4eeb386ddc64d0bb9b10820a8d82bcd3e53eeb2da6/msgpack-1.1.2-cp314-cp314t-win_amd64.whl", hash = "sha256:5a46bf7e831d09470ad92dff02b8b1ac92175ca36b087f904a0519857c6be3ff", size = 85772, upload-time = "2025-10-08T09:15:43.954Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/81/f2/08ace4142eb281c12701fc3b93a10795e4d4dc7f753911d836675050f886/msgpack-1.1.2-cp314-cp314t-win_arm64.whl", hash = "sha256:d99ef64f349d5ec3293688e91486c5fdb925ed03807f64d98d205d2713c60b46", size = 70868, upload-time = "2025-10-08T09:15:44.959Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "multidict"
|
||||
version = "6.7.1"
|
||||
@@ -389,6 +555,15 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "packageurl-python"
|
||||
version = "0.17.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f5/d6/3b5a4e3cfaef7a53869a26ceb034d1ff5e5c27c814ce77260a96d50ab7bb/packageurl_python-0.17.6.tar.gz", hash = "sha256:1252ce3a102372ca6f86eb968e16f9014c4ba511c5c37d95a7f023e2ca6e5c25", size = 50618, upload-time = "2025-11-24T15:20:17.998Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/2f/c7277b7615a93f51b5fbc1eacfc1b75e8103370e786fd8ce2abf6e5c04ab/packageurl_python-0.17.6-py3-none-any.whl", hash = "sha256:31a85c2717bc41dd818f3c62908685ff9eebcb68588213745b14a6ee9e7df7c9", size = 36776, upload-time = "2025-11-24T15:20:16.962Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "packaging"
|
||||
version = "26.0"
|
||||
@@ -407,6 +582,70 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ef/3c/2c197d226f9ea224a9ab8d197933f9da0ae0aac5b6e0f884e2b8d9c8e9f7/pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723", size = 55206, upload-time = "2026-01-27T03:59:45.137Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pip"
|
||||
version = "26.0.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/48/83/0d7d4e9efe3344b8e2fe25d93be44f64b65364d3c8d7bc6dc90198d5422e/pip-26.0.1.tar.gz", hash = "sha256:c4037d8a277c89b320abe636d59f91e6d0922d08a05b60e85e53b296613346d8", size = 1812747, upload-time = "2026-02-05T02:20:18.702Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/de/f0/c81e05b613866b76d2d1066490adf1a3dbc4ee9d9c839961c3fc8a6997af/pip-26.0.1-py3-none-any.whl", hash = "sha256:bdb1b08f4274833d62c1aa29e20907365a2ceb950410df15fc9521bad440122b", size = 1787723, upload-time = "2026-02-05T02:20:16.416Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pip-api"
|
||||
version = "0.0.34"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "pip" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b9/f1/ee85f8c7e82bccf90a3c7aad22863cc6e20057860a1361083cd2adacb92e/pip_api-0.0.34.tar.gz", hash = "sha256:9b75e958f14c5a2614bae415f2adf7eeb54d50a2cfbe7e24fd4826471bac3625", size = 123017, upload-time = "2024-07-09T20:32:30.641Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/91/f7/ebf5003e1065fd00b4cbef53bf0a65c3d3e1b599b676d5383ccb7a8b88ba/pip_api-0.0.34-py3-none-any.whl", hash = "sha256:8b2d7d7c37f2447373aa2cf8b1f60a2f2b27a84e1e9e0294a3f6ef10eb3ba6bb", size = 120369, upload-time = "2024-07-09T20:32:29.099Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pip-audit"
|
||||
version = "2.10.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "cachecontrol", extra = ["filecache"] },
|
||||
{ name = "cyclonedx-python-lib" },
|
||||
{ name = "packaging" },
|
||||
{ name = "pip-api" },
|
||||
{ name = "pip-requirements-parser" },
|
||||
{ name = "platformdirs" },
|
||||
{ name = "requests" },
|
||||
{ name = "rich" },
|
||||
{ name = "tomli" },
|
||||
{ name = "tomli-w" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/bd/89/0e999b413facab81c33d118f3ac3739fd02c0622ccf7c4e82e37cebd8447/pip_audit-2.10.0.tar.gz", hash = "sha256:427ea5bf61d1d06b98b1ae29b7feacc00288a2eced52c9c58ceed5253ef6c2a4", size = 53776, upload-time = "2025-12-01T23:42:40.612Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/be/f3/4888f895c02afa085630a3a3329d1b18b998874642ad4c530e9a4d7851fe/pip_audit-2.10.0-py3-none-any.whl", hash = "sha256:16e02093872fac97580303f0848fa3ad64f7ecf600736ea7835a2b24de49613f", size = 61518, upload-time = "2025-12-01T23:42:39.193Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pip-requirements-parser"
|
||||
version = "32.0.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "packaging" },
|
||||
{ name = "pyparsing" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/5e/2a/63b574101850e7f7b306ddbdb02cb294380d37948140eecd468fae392b54/pip-requirements-parser-32.0.1.tar.gz", hash = "sha256:b4fa3a7a0be38243123cf9d1f3518da10c51bdb165a2b2985566247f9155a7d3", size = 209359, upload-time = "2022-12-21T15:25:22.732Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/54/d0/d04f1d1e064ac901439699ee097f58688caadea42498ec9c4b4ad2ef84ab/pip_requirements_parser-32.0.1-py3-none-any.whl", hash = "sha256:4659bc2a667783e7a15d190f6fccf8b2486685b6dba4c19c3876314769c57526", size = 35648, upload-time = "2022-12-21T15:25:21.046Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "platformdirs"
|
||||
version = "4.9.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/1b/04/fea538adf7dbbd6d186f551d595961e564a3b6715bdf276b477460858672/platformdirs-4.9.2.tar.gz", hash = "sha256:9a33809944b9db043ad67ca0db94b14bf452cc6aeaac46a88ea55b26e2e9d291", size = 28394, upload-time = "2026-02-16T03:56:10.574Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/48/31/05e764397056194206169869b50cf2fee4dbbbc71b344705b9c0d878d4d8/platformdirs-4.9.2-py3-none-any.whl", hash = "sha256:9170634f126f8efdae22fb58ae8a0eaa86f38365bc57897a6c4f781d1f5875bd", size = 21168, upload-time = "2026-02-16T03:56:08.891Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pluggy"
|
||||
version = "1.6.0"
|
||||
@@ -455,6 +694,18 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/5b/5a/bc7b4a4ef808fa59a816c17b20c4bef6884daebbdf627ff2a161da67da19/propcache-0.4.1-py3-none-any.whl", hash = "sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237", size = 13305, upload-time = "2025-10-08T19:49:00.792Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "py-serializable"
|
||||
version = "2.1.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "defusedxml" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/73/21/d250cfca8ff30c2e5a7447bc13861541126ce9bd4426cd5d0c9f08b5547d/py_serializable-2.1.0.tar.gz", hash = "sha256:9d5db56154a867a9b897c0163b33a793c804c80cee984116d02d49e4578fc103", size = 52368, upload-time = "2025-07-21T09:56:48.07Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/9b/bf/7595e817906a29453ba4d99394e781b6fabe55d21f3c15d240f85dd06bb1/py_serializable-2.1.0-py3-none-any.whl", hash = "sha256:b56d5d686b5a03ba4f4db5e769dc32336e142fc3bd4d68a8c25579ebb0a67304", size = 23045, upload-time = "2025-07-21T09:56:46.848Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pygments"
|
||||
version = "2.19.2"
|
||||
@@ -464,6 +715,15 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyparsing"
|
||||
version = "3.3.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f3/91/9c6ee907786a473bf81c5f53cf703ba0957b23ab84c264080fb5a450416f/pyparsing-3.3.2.tar.gz", hash = "sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc", size = 6851574, upload-time = "2026-01-21T03:57:59.36Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d", size = 122781, upload-time = "2026-01-21T03:57:55.912Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pytest"
|
||||
version = "9.0.2"
|
||||
@@ -506,6 +766,34 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424, upload-time = "2025-09-09T10:57:00.695Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "requests"
|
||||
version = "2.32.5"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "certifi" },
|
||||
{ name = "charset-normalizer" },
|
||||
{ name = "idna" },
|
||||
{ name = "urllib3" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rich"
|
||||
version = "14.3.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "markdown-it-py" },
|
||||
{ name = "pygments" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b3/c6/f3b320c27991c46f43ee9d856302c70dc2d0fb2dba4842ff739d5f46b393/rich-14.3.3.tar.gz", hash = "sha256:b8daa0b9e4eef54dd8cf7c86c03713f53241884e814f4e2f5fb342fe520f639b", size = 230582, upload-time = "2026-02-19T17:23:12.474Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/14/25/b208c5683343959b670dc001595f2f3737e051da617f66c31f7c4fa93abc/rich-14.3.3-py3-none-any.whl", hash = "sha256:793431c1f8619afa7d3b52b2cdec859562b950ea0d4b6b505397612db8d5362d", size = 310458, upload-time = "2026-02-19T17:23:13.732Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.15.1"
|
||||
@@ -531,6 +819,51 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/07/5bda6a85b220c64c65686bc85bd0bbb23b29c62b3a9f9433fa55f17cda93/ruff-0.15.1-py3-none-win_arm64.whl", hash = "sha256:5ff7d5f0f88567850f45081fac8f4ec212be8d0b963e385c3f7d0d2eb4899416", size = 10874604, upload-time = "2026-02-12T23:09:05.515Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sortedcontainers"
|
||||
version = "2.4.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e8/c4/ba2f8066cceb6f23394729afe52f3bf7adec04bf9ed2c820b39e19299111/sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", size = 30594, upload-time = "2021-05-16T22:03:42.897Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575, upload-time = "2021-05-16T22:03:41.177Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tomli"
|
||||
version = "2.4.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/82/30/31573e9457673ab10aa432461bee537ce6cef177667deca369efb79df071/tomli-2.4.0.tar.gz", hash = "sha256:aa89c3f6c277dd275d8e243ad24f3b5e701491a860d5121f2cdd399fbb31fc9c", size = 17477, upload-time = "2026-01-11T11:22:38.165Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f3/c4/84047a97eb1004418bc10bdbcfebda209fca6338002eba2dc27cc6d13563/tomli-2.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:26ab906a1eb794cd4e103691daa23d95c6919cc2fa9160000ac02370cc9dd3f6", size = 154725, upload-time = "2026-01-11T11:22:17.269Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a8/5d/d39038e646060b9d76274078cddf146ced86dc2b9e8bbf737ad5983609a0/tomli-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:20cedb4ee43278bc4f2fee6cb50daec836959aadaf948db5172e776dd3d993fc", size = 148901, upload-time = "2026-01-11T11:22:18.287Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/73/e5/383be1724cb30f4ce44983d249645684a48c435e1cd4f8b5cded8a816d3c/tomli-2.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39b0b5d1b6dd03684b3fb276407ebed7090bbec989fa55838c98560c01113b66", size = 243375, upload-time = "2026-01-11T11:22:19.154Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/31/f0/bea80c17971c8d16d3cc109dc3585b0f2ce1036b5f4a8a183789023574f2/tomli-2.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a26d7ff68dfdb9f87a016ecfd1e1c2bacbe3108f4e0f8bcd2228ef9a766c787d", size = 250639, upload-time = "2026-01-11T11:22:20.168Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/8f/2853c36abbb7608e3f945d8a74e32ed3a74ee3a1f468f1ffc7d1cb3abba6/tomli-2.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:20ffd184fb1df76a66e34bd1b36b4a4641bd2b82954befa32fe8163e79f1a702", size = 246897, upload-time = "2026-01-11T11:22:21.544Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/f0/6c05e3196ed5337b9fe7ea003e95fd3819a840b7a0f2bf5a408ef1dad8ed/tomli-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75c2f8bbddf170e8effc98f5e9084a8751f8174ea6ccf4fca5398436e0320bc8", size = 254697, upload-time = "2026-01-11T11:22:23.058Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f3/f5/2922ef29c9f2951883525def7429967fc4d8208494e5ab524234f06b688b/tomli-2.4.0-cp314-cp314-win32.whl", hash = "sha256:31d556d079d72db7c584c0627ff3a24c5d3fb4f730221d3444f3efb1b2514776", size = 98567, upload-time = "2026-01-11T11:22:24.033Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/31/22b52e2e06dd2a5fdbc3ee73226d763b184ff21fc24e20316a44ccc4d96b/tomli-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:43e685b9b2341681907759cf3a04e14d7104b3580f808cfde1dfdb60ada85475", size = 108556, upload-time = "2026-01-11T11:22:25.378Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/48/3d/5058dff3255a3d01b705413f64f4306a141a8fd7a251e5a495e3f192a998/tomli-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:3d895d56bd3f82ddd6faaff993c275efc2ff38e52322ea264122d72729dca2b2", size = 96014, upload-time = "2026-01-11T11:22:26.138Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b8/4e/75dab8586e268424202d3a1997ef6014919c941b50642a1682df43204c22/tomli-2.4.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5b5807f3999fb66776dbce568cc9a828544244a8eb84b84b9bafc080c99597b9", size = 163339, upload-time = "2026-01-11T11:22:27.143Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/06/e3/b904d9ab1016829a776d97f163f183a48be6a4deb87304d1e0116a349519/tomli-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c084ad935abe686bd9c898e62a02a19abfc9760b5a79bc29644463eaf2840cb0", size = 159490, upload-time = "2026-01-11T11:22:28.399Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e3/5a/fc3622c8b1ad823e8ea98a35e3c632ee316d48f66f80f9708ceb4f2a0322/tomli-2.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f2e3955efea4d1cfbcb87bc321e00dc08d2bcb737fd1d5e398af111d86db5df", size = 269398, upload-time = "2026-01-11T11:22:29.345Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/33/62bd6152c8bdd4c305ad9faca48f51d3acb2df1f8791b1477d46ff86e7f8/tomli-2.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e0fe8a0b8312acf3a88077a0802565cb09ee34107813bba1c7cd591fa6cfc8d", size = 276515, upload-time = "2026-01-11T11:22:30.327Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4b/ff/ae53619499f5235ee4211e62a8d7982ba9e439a0fb4f2f351a93d67c1dd2/tomli-2.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:413540dce94673591859c4c6f794dfeaa845e98bf35d72ed59636f869ef9f86f", size = 273806, upload-time = "2026-01-11T11:22:32.56Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/47/71/cbca7787fa68d4d0a9f7072821980b39fbb1b6faeb5f5cf02f4a5559fa28/tomli-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0dc56fef0e2c1c470aeac5b6ca8cc7b640bb93e92d9803ddaf9ea03e198f5b0b", size = 281340, upload-time = "2026-01-11T11:22:33.505Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/00/d595c120963ad42474cf6ee7771ad0d0e8a49d0f01e29576ee9195d9ecdf/tomli-2.4.0-cp314-cp314t-win32.whl", hash = "sha256:d878f2a6707cc9d53a1be1414bbb419e629c3d6e67f69230217bb663e76b5087", size = 108106, upload-time = "2026-01-11T11:22:34.451Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/de/69/9aa0c6a505c2f80e519b43764f8b4ba93b5a0bbd2d9a9de6e2b24271b9a5/tomli-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2add28aacc7425117ff6364fe9e06a183bb0251b03f986df0e78e974047571fd", size = 120504, upload-time = "2026-01-11T11:22:35.764Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/9f/f1668c281c58cfae01482f7114a4b88d345e4c140386241a1a24dcc9e7bc/tomli-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2b1e3b80e1d5e52e40e9b924ec43d81570f0e7d09d11081b797bc4692765a3d4", size = 99561, upload-time = "2026-01-11T11:22:36.624Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/23/d1/136eb2cb77520a31e1f64cbae9d33ec6df0d78bdf4160398e86eec8a8754/tomli-2.4.0-py3-none-any.whl", hash = "sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a", size = 14477, upload-time = "2026-01-11T11:22:37.446Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tomli-w"
|
||||
version = "1.2.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/19/75/241269d1da26b624c0d5e110e8149093c759b7a286138f4efd61a60e75fe/tomli_w-1.2.0.tar.gz", hash = "sha256:2dd14fac5a47c27be9cd4c976af5a12d87fb1f0b4512f81d69cce3b35ae25021", size = 7184, upload-time = "2025-01-15T12:07:24.262Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/18/c86eb8e0202e32dd3df50d43d7ff9854f8e0603945ff398974c1d91ac1ef/tomli_w-1.2.0-py3-none-any.whl", hash = "sha256:188306098d013b691fcadc011abd66727d3c414c571bb01b1a174ba8c983cf90", size = 6675, upload-time = "2025-01-15T12:07:22.074Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typing-extensions"
|
||||
version = "4.15.0"
|
||||
@@ -540,6 +873,15 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "urllib3"
|
||||
version = "2.6.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yarl"
|
||||
version = "1.22.0"
|
||||
|
||||
Reference in New Issue
Block a user