@@ 2,275 2,425 @@
# -*- coding: utf-8 -*-
## Syslog Server in Python with asyncio and SQLite.
-# Configuration
-import os
-
-DEBUG = os.environ.get("DEBUG") == "True"
-LOG_DUMP = os.environ.get("LOG_DUMP") == "True"
-SQL_DUMP = os.environ.get("SQL_DUMP") == "True"
-SQL_WRITE = os.environ.get("SQL_WRITE") == "True"
-BINDING_IP = os.environ.get("BINDING_IP", "0.0.0.0")
-BINDING_PORT = os.environ.get("BINDING_PORT", "5140")
-
-del os
-
import asyncio
import aiosqlite
import signal
-from datetime import datetime, timedelta
-from priority import SyslogMatrix
-from rfc5424 import convert_rfc5424_to_rfc3164
+import os
+import re
+from datetime import datetime, UTC
+from typing import Dict, Any, Tuple, List, Type, Self
+from types import ModuleType
+# Relative imports for package structure
+from .priority import SyslogMatrix
+
+uvloop: ModuleType | None = None
try:
import uvloop
+
except ImportError:
- uvloop = None
+ pass
+
+
+# --- Configuration ---
+# Environment variables are now read inside the functions that use them.
+DEBUG: bool = os.environ.get("DEBUG") == "True"
+LOG_DUMP: bool = os.environ.get("LOG_DUMP") == "True"
+SQL_DUMP: bool = os.environ.get("SQL_DUMP") == "True"
+SQL_WRITE: bool = os.environ.get("SQL_WRITE") == "True"
+BINDING_IP: str = os.environ.get("BINDING_IP", "0.0.0.0")
+BINDING_PORT: int = int(os.environ.get("BINDING_PORT", "5140"))
+BATCH_SIZE: int = int(os.environ.get("BATCH_SIZE", "1000"))
+BATCH_TIMEOUT: int = int(os.environ.get("BATCH_TIMEOUT", "5"))
+
+
+# --- Conversion Utilities ---
+
+
+def convert_rfc3164_to_rfc5424(message: str, debug_mode: bool = False) -> str:
+ """
+ Converts a best-effort RFC 3164 syslog message to an RFC 5424 message.
+ This version is more flexible to handle formats like FortiGate's.
+ """
+ # Pattern for RFC 3164: <PRI>MMM DD HH:MM:SS HOSTNAME TAG[PID]: MSG
+ # Made the colon after the tag optional and adjusted tag capture.
+ pattern: re.Pattern[str] = re.compile(
+ r"<(?P<pri>\d{1,3})>"
+ r"(?P<mon>\w{3})\s+(?P<day>\d{1,2})\s+(?P<hr>\d{2}):(?P<min>\d{2}):(?P<sec>\d{2})"
+ r"\s+(?P<host>[\w\-\.]+)"
+ r"\s+(?P<tag>\S+?)(:|\s-)?\s" # Flexible tag/separator matching
+ r"(?P<msg>.*)",
+ re.DOTALL,
+ )
+ match: re.Match[str] | None = pattern.match(message)
+
+ if not match:
+ if debug_mode:
+ print(
+ f"[RFC-CONVERT] Not an RFC 3164 message, returning original: {message}"
+ )
+ return message
+
+ parts: Dict[str, str] = match.groupdict()
+ priority: str = parts["pri"]
+ hostname: str = parts["host"]
+ raw_tag: str = parts["tag"]
+ msg: str = parts["msg"].strip()
+
+ app_name: str = raw_tag
+ procid: str = "-"
+ pid_match: re.Match[str] | None = re.match(r"^(.*)\[(\d+)\]$", raw_tag)
+ if pid_match:
+ app_name = pid_match.group(1)
+ procid = pid_match.group(2)
+
+ try:
+ now: datetime = datetime.now()
+ dt_naive: datetime = datetime.strptime(
+ f"{parts['mon']} {parts['day']} {parts['hr']}:{parts['min']}:{parts['sec']}",
+ "%b %d %H:%M:%S",
+ ).replace(year=now.year)
+
+ if dt_naive > now:
+ dt_naive = dt_naive.replace(year=now.year - 1)
+
+ dt_aware: datetime = dt_naive.astimezone().astimezone(UTC)
+ timestamp: str = dt_aware.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z"
+ except ValueError:
+ if debug_mode:
+ print(
+ "[RFC-CONVERT] Could not parse RFC-3164 timestamp, using current time."
+ )
+ timestamp = (
+ datetime.now(UTC).strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z"
+ )
+
+ return f"<{priority}>1 {timestamp} {hostname} {app_name} {procid} - - {msg}"
-class SyslogUDPServer:
- syslog_matrix = SyslogMatrix()
+def normalize_to_rfc5424(message: str, debug_mode: bool = False) -> str:
+ """
+ Ensures a syslog message is in RFC 5424 format.
+ Converts RFC 3164 messages, and leaves RFC 5424 as is.
+ """
+ pri_end: int = message.find(">")
+ if pri_end > 0 and len(message) > pri_end + 2:
+ if message[pri_end + 1] == "1" and message[pri_end + 2].isspace():
+ return message
+
+ return convert_rfc3164_to_rfc5424(message, debug_mode)
+
+
+class SyslogUDPServer(asyncio.DatagramProtocol):
+ """An asynchronous Syslog UDP server with batch database writing."""
- def __init__(self, host, port, loop=None):
- self.host = host
- self.port = port
- self.loop = loop or asyncio.get_event_loop()
- self.running = False
- self.fts_buffer = [] # Buffer for FTS messages
- self.last_fts_sync = None # Timestamp of last FTS sync
- self.loop.add_signal_handler(signal.SIGINT, self.handle_sigint_wrapper)
- self.setup()
- print(f"Listening on UDP {host}:{port} using {self.loop.__module__}")
+ syslog_matrix: SyslogMatrix = SyslogMatrix()
+ RFC5424_PATTERN: re.Pattern[str] = re.compile(
+ r"<(?P<pri>\d+)>"
+ r"(?P<ver>\d+)\s"
+ r"(?P<ts>\S+)\s"
+ r"(?P<host>\S+)\s"
+ r"(?P<app>\S+)\s"
+ r"(?P<pid>\S+)\s"
+ r"(?P<msgid>\S+)\s"
+ r"(?P<sd>(\-|(?:\[.+?\])+))\s?"
+ r"(?P<msg>.*)",
+ re.DOTALL,
+ )
- def setup(self):
+ # __init__ is now synchronous and lightweight
+ def __init__(self, host: str, port: int) -> None:
+ self.host: str = host
+ self.port: int = port
+ self.loop: asyncio.AbstractEventLoop = asyncio.get_running_loop()
+ self.transport: asyncio.DatagramTransport | None = None
+ self.db: aiosqlite.Connection | None = None
+ self._shutting_down: bool = False
+ self._db_writer_task: asyncio.Task[None] | None = None
+ self._message_queue: asyncio.Queue[
+ Tuple[bytes, Tuple[str, int], datetime]
+ ] = asyncio.Queue()
+
+ # An async factory to properly create and initialize the server
+ @classmethod
+ async def create(cls: Type[Self], host: str, port: int) -> Self:
+ server = cls(host, port)
+ print(f"aiosyslogd starting on UDP {host}:{port}...")
if SQL_WRITE:
- self.loop.run_until_complete(self.connect_to_sqlite())
+ print(
+ f"SQLite writing ENABLED. Batch size: {BATCH_SIZE}, Timeout: {BATCH_TIMEOUT}s"
+ )
+ await server.connect_to_sqlite()
+ if DEBUG:
+ print("Debug mode is ON.")
+ return server
+
+ def connection_made(self, transport: asyncio.BaseTransport) -> None:
+ self.transport = transport # type: ignore
+ if SQL_WRITE and not self._db_writer_task:
+ self._db_writer_task = self.loop.create_task(self.database_writer())
+ print("Database writer task started.")
+
+ def datagram_received(self, data: bytes, addr: Tuple[str, int]) -> None:
+ """Quickly queue incoming messages without processing."""
+ if self._shutting_down:
+ return
+ self._message_queue.put_nowait((data, addr, datetime.now()))
+
+ def error_received(self, exc: Exception) -> None:
+ if DEBUG:
+ print(f"Error received: {exc}")
- async def handle_sigint(self, signum, frame):
- print("\nSIGINT received. Shutting down gracefully...")
- self.running = False
- # Sync any remaining FTS messages before shutdown
- if self.fts_buffer:
- await self.sync_fts_buffer()
- await self.shutdown()
+ def connection_lost(self, exc: Exception | None) -> None:
+ if DEBUG:
+ print(f"Connection lost: {exc}")
- def handle_sigint_wrapper(self):
- asyncio.ensure_future(self.handle_sigint(signal.SIGINT, None))
-
- async def close_db_connection(self):
- if SQL_WRITE and hasattr(self, "db") and self.db:
+ async def database_writer(self) -> None:
+ """A dedicated task to write messages to the database in batches."""
+ batch: List[Dict[str, Any]] = []
+ while not self._shutting_down or not self._message_queue.empty():
try:
- await self.db.close()
+ data, addr, received_at = await asyncio.wait_for(
+ self._message_queue.get(), timeout=BATCH_TIMEOUT
+ )
+ params = self.process_datagram(data, addr, received_at)
+ if params:
+ batch.append(params)
+ self._message_queue.task_done()
+ if len(batch) >= BATCH_SIZE:
+ await self.write_batch_to_db(batch)
+ batch.clear()
+ except asyncio.TimeoutError:
+ if batch:
+ await self.write_batch_to_db(batch)
+ batch.clear()
except Exception as e:
if DEBUG:
- print(f"Error while closing the database connection: {e}")
-
- async def shutdown(self):
- self.stop()
- await self.close_db_connection()
- self.loop.stop()
-
- async def connect_to_sqlite(self):
- self.db = await aiosqlite.connect("syslog.db", loop=self.loop)
- await self.db.execute("PRAGMA journal_mode=WAL")
- await self.db.execute("PRAGMA auto_vacuum = FULL")
- await self.db.commit()
+ print(f"[DB-WRITER-ERROR] {e}")
+ if batch:
+ await self.write_batch_to_db(batch)
+ batch.clear()
+ print("Database writer task finished.")
- async def create_monthly_table(self, year_month):
- table_name = f"SystemEvents{year_month}"
- fts_table_name = f"SystemEventsFTS{year_month}"
- await self.db.execute(
- f"""CREATE TABLE IF NOT EXISTS {table_name} (
- ID INTEGER PRIMARY KEY AUTOINCREMENT,
- Facility INTEGER,
- Priority INTEGER,
- FromHost TEXT,
- InfoUnitID INTEGER,
- ReceivedAt TIMESTAMP,
- DeviceReportedTime TIMESTAMP,
- SysLogTag TEXT,
- ProcessID TEXT,
- Message TEXT
- )"""
+ def process_datagram(
+ self, data: bytes, address: Tuple[str, int], received_at: datetime
+ ) -> Dict[str, Any] | None:
+ """Processes a single datagram and returns a dictionary of params for DB insert."""
+ try:
+ decoded_data: str = data.decode("utf-8")
+ except UnicodeDecodeError:
+ if DEBUG:
+ print(f"Cannot decode message from {address}: {data!r}")
+ return None
+
+ processed_data: str = normalize_to_rfc5424(
+ decoded_data, debug_mode=DEBUG
)
- await self.db.execute(
- f"""CREATE INDEX IF NOT EXISTS idx_ReceivedAt_{year_month}
- ON {table_name} (ReceivedAt)"""
- )
- await self.db.execute(
- f"""CREATE VIRTUAL TABLE IF NOT EXISTS {fts_table_name}
- USING FTS5(Message)"""
- )
- await self.db.commit()
- return table_name
- def escape(self, msg):
- if not isinstance(msg, str):
- return str(msg)
- return msg.replace("'", "''").replace('"', '""').replace("\\", "\\\\")
-
- async def sync_fts_buffer(self, table_name=None):
- if not self.fts_buffer:
- return
-
- year_month = self.fts_buffer[0]["ReceivedAt"].strftime("%Y%m")
- fts_table_name = f"SystemEventsFTS{year_month}"
- messages = [(entry["Message"],) for entry in self.fts_buffer]
+ if LOG_DUMP and not SQL_DUMP:
+ print(
+ f"\n[{received_at}] FROM {address[0]}:\n RFC5424 DATA: {processed_data}"
+ )
try:
- await self.db.executemany(
- f"INSERT INTO {fts_table_name} (Message) VALUES (?)", messages
+ match: re.Match[str] | None = self.RFC5424_PATTERN.match(
+ processed_data
)
- await self.db.commit()
- self.last_fts_sync = datetime.now()
- self.fts_buffer = []
- if DEBUG:
- print(f"Synced {len(messages)} messages to {fts_table_name}")
- except Exception as e:
- if DEBUG:
- print(f"Failed to sync FTS buffer: {e}")
- await self.db.rollback()
-
- async def handle_datagram(self, data, address):
- ReceivedAt = datetime.now()
- data = data.decode()
- data = convert_rfc5424_to_rfc3164(data)
- if LOG_DUMP:
- print(f"\n DATA: {data}")
-
- year_month = ReceivedAt.strftime("%Y%m")
- table_name = await self.create_monthly_table(year_month)
-
- datetime_hostname_program = data[data.find(">") + 1 : data.find(": ")]
- try:
- m, d, t, hostname = datetime_hostname_program.split()[:4]
- except ValueError:
- if DEBUG:
- print(f"Invalid message format: {data}")
- return
+ if not match:
+ if DEBUG:
+ print(f"Failed to parse as RFC-5424: {processed_data}")
+ pri_end: int = processed_data.find(">")
+ code: str = processed_data[1:pri_end] if pri_end != -1 else "14"
+ Facility, Priority = self.syslog_matrix.decode_int(code)
+ FromHost, DeviceReportedTime = address[0], received_at
+ SysLogTag, ProcessID, Message = "UNKNOWN", "0", processed_data
+ else:
+ parts: Dict[str, Any] = match.groupdict()
+ code = parts["pri"]
+ Facility, Priority = self.syslog_matrix.decode_int(code)
+ try:
+ ts_str: str = parts["ts"].upper().replace("Z", "+00:00")
+ DeviceReportedTime = datetime.fromisoformat(ts_str)
+ except (ValueError, TypeError):
+ DeviceReportedTime = received_at
+ FromHost = parts["host"] if parts["host"] != "-" else address[0]
+ SysLogTag = parts["app"] if parts["app"] != "-" else "UNKNOWN"
+ ProcessID = parts["pid"] if parts["pid"] != "-" else "0"
+ Message = parts["msg"].strip()
- formatted_datetime = f"{m} {d.zfill(2)} {t} {ReceivedAt.year}"
- DeviceReportedTime = datetime.strptime(formatted_datetime, "%b %d %H:%M:%S %Y")
- time_delta = ReceivedAt - DeviceReportedTime
- if abs(time_delta.days) > 1:
- formatted_datetime = f"{m} {d.zfill(2)} {t} {ReceivedAt.year - 1}"
- DeviceReportedTime = datetime.strptime(
- formatted_datetime, "%b %d %H:%M:%S %Y"
- )
- time_delta = ReceivedAt - DeviceReportedTime
- if abs(time_delta.days) > 1:
- pass
- else:
- program = "-".join(datetime_hostname_program.split()[4:])
- SysLogTag = program[: program.find("[")] if "[" in program else program
- ProcessID = (
- program[program.find("[") + 1 : program.find("]")]
- if "[" in program
- else "0"
- )
- Message = data[data.find(": ") + 2 :]
- code = data[data.find("<") + 1 : data.find(">")]
- Facility, Priority = self.syslog_matrix.decode_int(code)
- FromHost = hostname or address[0]
- InfoUnitID = 1
-
- FromHost = self.escape(FromHost)
- SysLogTag = self.escape(SysLogTag)
- ProcessID = self.escape(ProcessID)
- Message = self.escape(Message)
-
- params = {
+ return {
"Facility": Facility,
"Priority": Priority,
"FromHost": FromHost,
- "InfoUnitID": InfoUnitID,
- "ReceivedAt": ReceivedAt,
+ "InfoUnitID": 1,
+ "ReceivedAt": received_at,
"DeviceReportedTime": DeviceReportedTime,
"SysLogTag": SysLogTag,
"ProcessID": ProcessID,
"Message": Message,
}
-
- sql_command = (
- f"INSERT INTO {table_name} (Facility, Priority, FromHost, "
- "InfoUnitID, ReceivedAt, DeviceReportedTime, SysLogTag, "
- "ProcessID, Message) VALUES "
- "(:Facility, :Priority, :FromHost, :InfoUnitID, :ReceivedAt, "
- ":DeviceReportedTime, :SysLogTag, :ProcessID, :Message)"
- )
-
- if SQL_DUMP:
- print(f"\n SQL: {sql_command}")
- print(f"\nPARAMS: {params}")
-
- if SQL_WRITE:
- try:
- async with self.db.execute(sql_command, params) as cursor:
- pass
- await self.db.commit()
-
- # Add to FTS buffer
- self.fts_buffer.append(params)
+ except Exception as e:
+ if DEBUG:
+ print(
+ f"CRITICAL PARSE FAILURE on: {processed_data}\nError: {e}"
+ )
+ return None
- # Check buffer size or time since last sync
- if len(self.fts_buffer) >= 100:
- await self.sync_fts_buffer(table_name)
- elif (
- self.last_fts_sync
- and (ReceivedAt - self.last_fts_sync).total_seconds() >= 10
- ):
- await self.sync_fts_buffer(table_name)
+ async def write_batch_to_db(self, batch: List[Dict[str, Any]]) -> None:
+ """Writes a batch of messages to the database."""
+ if not batch or (self._shutting_down and not self.db) or not self.db:
+ return
- except Exception as e:
- if DEBUG:
- print(f"\n SQL: {sql_command}")
- print(f"\nPARAMS: {params}")
- print(f"\nEXCEPT: {e}")
- await self.db.rollback()
+ year_month: str = batch[0]["ReceivedAt"].strftime("%Y%m")
+ table_name: str = await self.create_monthly_table(year_month)
- def start(self):
- self.endpoint, _ = self.loop.run_until_complete(
- self.loop.create_datagram_endpoint(
- lambda: DatagramProtocol(self.handle_datagram),
- local_addr=(self.host, self.port),
- )
+ sql_command: str = (
+ f"INSERT INTO {table_name} (Facility, Priority, FromHost, InfoUnitID, "
+ "ReceivedAt, DeviceReportedTime, SysLogTag, ProcessID, Message) VALUES "
+ "(:Facility, :Priority, :FromHost, :InfoUnitID, :ReceivedAt, "
+ ":DeviceReportedTime, :SysLogTag, :ProcessID, :Message)"
)
- def stop(self):
- self.endpoint.close()
+ if SQL_DUMP:
+ print(f"\n SQL: {sql_command}")
+ summary: str = (
+ f"PARAMS: {batch[0]} and {len(batch) - 1} more logs..."
+ if len(batch) > 1
+ else f"PARAMS: {batch[0]}"
+ )
+ print(f" {summary}")
+
+ try:
+ await self.db.executemany(sql_command, batch)
+ await self.db.commit()
+ await self.sync_fts_for_month(year_month)
+ if DEBUG:
+ print(f"Successfully wrote batch of {len(batch)} messages.")
+ except Exception as e:
+ if DEBUG and not self._shutting_down:
+ print(f"\nBATCH SQL_ERROR: {e}")
+ await self.db.rollback()
+
+ async def connect_to_sqlite(self) -> None:
+ """Initializes the database connection."""
+ self.db = await aiosqlite.connect("syslog.db")
+ await self.db.execute("PRAGMA journal_mode=WAL")
+ await self.db.execute("PRAGMA auto_vacuum = FULL")
+ await self.db.commit()
+ print("SQLite database connected.")
+
+ async def create_monthly_table(self, year_month: str) -> str:
+ """Creates tables for the given month if they don't exist."""
+ table_name: str = f"SystemEvents{year_month}"
+ fts_table_name: str = f"SystemEventsFTS{year_month}"
+ if not self.db:
+ raise ConnectionError("Database is not connected.")
+
+ async with self.db.cursor() as cursor:
+ await cursor.execute(
+ f"SELECT name FROM sqlite_master WHERE type='table' AND name='{table_name}'"
+ )
+ if await cursor.fetchone() is None:
+ if DEBUG:
+ print(
+ f"Creating new tables for {year_month}: {table_name}, {fts_table_name}"
+ )
+ await self.db.execute(
+ f"""CREATE TABLE {table_name} (
+ ID INTEGER PRIMARY KEY AUTOINCREMENT, Facility INTEGER, Priority INTEGER,
+ FromHost TEXT, InfoUnitID INTEGER, ReceivedAt TIMESTAMP, DeviceReportedTime TIMESTAMP,
+ SysLogTag TEXT, ProcessID TEXT, Message TEXT
+ )"""
+ )
+ await self.db.execute(
+ f"CREATE INDEX idx_ReceivedAt_{year_month} ON {table_name} (ReceivedAt)"
+ )
+ await self.db.execute(
+ f"CREATE VIRTUAL TABLE {fts_table_name} USING fts5(Message, content='{table_name}', content_rowid='ID')"
+ )
+ await self.db.commit()
+ return table_name
+
+ async def sync_fts_for_month(self, year_month: str) -> None:
+ """Syncs the FTS index for a given month."""
+ if (
+ not SQL_WRITE
+ or (self._shutting_down and not self.db)
+ or not self.db
+ ):
+ return
+
+ fts_table_name: str = f"SystemEventsFTS{year_month}"
+ try:
+ await self.db.execute(
+ f"INSERT INTO {fts_table_name}({fts_table_name}) VALUES('rebuild')"
+ )
+ await self.db.commit()
+ if DEBUG:
+ print(f"Synced FTS table {fts_table_name}.")
+ except Exception as e:
+ if DEBUG and not self._shutting_down:
+ print(f"Failed to sync FTS table {fts_table_name}: {e}")
+
+ async def shutdown(self) -> None:
+ """Gracefully shuts down the server."""
+ print("\nShutting down server...")
+ self._shutting_down = True
+
+ if self.transport:
+ self.transport.close()
+
+ if self._db_writer_task:
+ print("Waiting for database writer to finish...")
+ # Give the writer a moment to process the last items
+ await asyncio.sleep(0.1)
+ # The writer task loop will exit gracefully.
+ await self._db_writer_task
+
+ if self.db:
+ await self.db.close()
+ print("Database connection closed.")
-class DatagramProtocol:
- def __init__(self, datagram_callback):
- self.datagram_callback = datagram_callback
+async def run_server() -> None:
+ """Sets up and runs the server until a shutdown signal is received."""
+ loop: asyncio.AbstractEventLoop = asyncio.get_running_loop()
+ # Use the async factory to create the server instance
+ server: SyslogUDPServer = await SyslogUDPServer.create(
+ host=BINDING_IP, port=BINDING_PORT
+ )
- def connection_made(self, transport):
- self.transport = transport
+ # Setup signal handlers
+ stop_event = asyncio.Event()
+ for sig in (signal.SIGINT, signal.SIGTERM):
+ loop.add_signal_handler(sig, stop_event.set)
+
+ transport, _ = await loop.create_datagram_endpoint(
+ lambda: server, local_addr=(server.host, server.port)
+ )
+ print(f"Server is running. Press Ctrl+C to stop.")
- def datagram_received(self, data, addr):
- asyncio.create_task(self.datagram_callback(data, addr))
+ try:
+ await stop_event.wait()
+ finally:
+ print("\nShutdown signal received.")
+ transport.close()
+ await server.shutdown()
+
- def error_received(self, exc):
- if DEBUG:
- print(f"Error received: {exc}")
+def main() -> None:
+ """CLI Entry point."""
+ if uvloop:
+ print("Using uvloop for the event loop.")
+ uvloop.install()
- def connection_lost(self, exc):
- if DEBUG:
- print("Closing transport")
+ try:
+ asyncio.run(run_server())
+ except (KeyboardInterrupt, asyncio.CancelledError):
+ pass
+ finally:
+ print("Server has been shut down.")
if __name__ == "__main__":
- loop = uvloop.new_event_loop() if uvloop else None
- try:
- syslog_server = SyslogUDPServer(BINDING_IP, BINDING_PORT, loop)
- syslog_server.start()
- loop = syslog_server.loop
- loop.run_forever()
- except (IOError, SystemExit):
- raise
- except Exception as e:
- print(f"Error occurred: {e}")
- finally:
- print("Shutting down the server...")
- syslog_server.stop()
- loop.run_until_complete(syslog_server.close_db_connection())
- loop.close()
+ main()
@@ 0,0 1,479 @@
+# This file is automatically @generated by Poetry 2.1.3 and should not be changed by hand.
+
+[[package]]
+name = "aiosqlite"
+version = "0.21.0"
+description = "asyncio bridge to the standard sqlite3 module"
+optional = false
+python-versions = ">=3.9"
+groups = ["main"]
+files = [
+ {file = "aiosqlite-0.21.0-py3-none-any.whl", hash = "sha256:2549cf4057f95f53dcba16f2b64e8e2791d7e1adedb13197dd8ed77bb226d7d0"},
+ {file = "aiosqlite-0.21.0.tar.gz", hash = "sha256:131bb8056daa3bc875608c631c678cda73922a2d4ba8aec373b19f18c17e7aa3"},
+]
+
+[package.dependencies]
+typing_extensions = ">=4.0"
+
+[package.extras]
+dev = ["attribution (==1.7.1)", "black (==24.3.0)", "build (>=1.2)", "coverage[toml] (==7.6.10)", "flake8 (==7.0.0)", "flake8-bugbear (==24.12.12)", "flit (==3.10.1)", "mypy (==1.14.1)", "ufmt (==2.5.1)", "usort (==1.0.8.post1)"]
+docs = ["sphinx (==8.1.3)", "sphinx-mdinclude (==0.6.1)"]
+
+[[package]]
+name = "black"
+version = "25.1.0"
+description = "The uncompromising code formatter."
+optional = false
+python-versions = ">=3.9"
+groups = ["dev"]
+files = [
+ {file = "black-25.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:759e7ec1e050a15f89b770cefbf91ebee8917aac5c20483bc2d80a6c3a04df32"},
+ {file = "black-25.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e519ecf93120f34243e6b0054db49c00a35f84f195d5bce7e9f5cfc578fc2da"},
+ {file = "black-25.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:055e59b198df7ac0b7efca5ad7ff2516bca343276c466be72eb04a3bcc1f82d7"},
+ {file = "black-25.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:db8ea9917d6f8fc62abd90d944920d95e73c83a5ee3383493e35d271aca872e9"},
+ {file = "black-25.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a39337598244de4bae26475f77dda852ea00a93bd4c728e09eacd827ec929df0"},
+ {file = "black-25.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:96c1c7cd856bba8e20094e36e0f948718dc688dba4a9d78c3adde52b9e6c2299"},
+ {file = "black-25.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bce2e264d59c91e52d8000d507eb20a9aca4a778731a08cfff7e5ac4a4bb7096"},
+ {file = "black-25.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:172b1dbff09f86ce6f4eb8edf9dede08b1fce58ba194c87d7a4f1a5aa2f5b3c2"},
+ {file = "black-25.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4b60580e829091e6f9238c848ea6750efed72140b91b048770b64e74fe04908b"},
+ {file = "black-25.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1e2978f6df243b155ef5fa7e558a43037c3079093ed5d10fd84c43900f2d8ecc"},
+ {file = "black-25.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3b48735872ec535027d979e8dcb20bf4f70b5ac75a8ea99f127c106a7d7aba9f"},
+ {file = "black-25.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:ea0213189960bda9cf99be5b8c8ce66bb054af5e9e861249cd23471bd7b0b3ba"},
+ {file = "black-25.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8f0b18a02996a836cc9c9c78e5babec10930862827b1b724ddfe98ccf2f2fe4f"},
+ {file = "black-25.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:afebb7098bfbc70037a053b91ae8437c3857482d3a690fefc03e9ff7aa9a5fd3"},
+ {file = "black-25.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:030b9759066a4ee5e5aca28c3c77f9c64789cdd4de8ac1df642c40b708be6171"},
+ {file = "black-25.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:a22f402b410566e2d1c950708c77ebf5ebd5d0d88a6a2e87c86d9fb48afa0d18"},
+ {file = "black-25.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a1ee0a0c330f7b5130ce0caed9936a904793576ef4d2b98c40835d6a65afa6a0"},
+ {file = "black-25.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f3df5f1bf91d36002b0a75389ca8663510cf0531cca8aa5c1ef695b46d98655f"},
+ {file = "black-25.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d9e6827d563a2c820772b32ce8a42828dc6790f095f441beef18f96aa6f8294e"},
+ {file = "black-25.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:bacabb307dca5ebaf9c118d2d2f6903da0d62c9faa82bd21a33eecc319559355"},
+ {file = "black-25.1.0-py3-none-any.whl", hash = "sha256:95e8176dae143ba9097f351d174fdaf0ccd29efb414b362ae3fd72bf0f710717"},
+ {file = "black-25.1.0.tar.gz", hash = "sha256:33496d5cd1222ad73391352b4ae8da15253c5de89b93a80b3e2c8d9a19ec2666"},
+]
+
+[package.dependencies]
+click = ">=8.0.0"
+mypy-extensions = ">=0.4.3"
+packaging = ">=22.0"
+pathspec = ">=0.9.0"
+platformdirs = ">=2"
+
+[package.extras]
+colorama = ["colorama (>=0.4.3)"]
+d = ["aiohttp (>=3.10)"]
+jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"]
+uvloop = ["uvloop (>=0.15.2)"]
+
+[[package]]
+name = "click"
+version = "8.2.1"
+description = "Composable command line interface toolkit"
+optional = false
+python-versions = ">=3.10"
+groups = ["dev"]
+files = [
+ {file = "click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b"},
+ {file = "click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202"},
+]
+
+[package.dependencies]
+colorama = {version = "*", markers = "platform_system == \"Windows\""}
+
+[[package]]
+name = "colorama"
+version = "0.4.6"
+description = "Cross-platform colored terminal text."
+optional = false
+python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
+groups = ["dev"]
+markers = "platform_system == \"Windows\" or sys_platform == \"win32\""
+files = [
+ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
+ {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
+]
+
+[[package]]
+name = "coverage"
+version = "7.8.2"
+description = "Code coverage measurement for Python"
+optional = false
+python-versions = ">=3.9"
+groups = ["dev"]
+files = [
+ {file = "coverage-7.8.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bd8ec21e1443fd7a447881332f7ce9d35b8fbd2849e761bb290b584535636b0a"},
+ {file = "coverage-7.8.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4c26c2396674816deaeae7ded0e2b42c26537280f8fe313335858ffff35019be"},
+ {file = "coverage-7.8.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1aec326ed237e5880bfe69ad41616d333712c7937bcefc1343145e972938f9b3"},
+ {file = "coverage-7.8.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5e818796f71702d7a13e50c70de2a1924f729228580bcba1607cccf32eea46e6"},
+ {file = "coverage-7.8.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:546e537d9e24efc765c9c891328f30f826e3e4808e31f5d0f87c4ba12bbd1622"},
+ {file = "coverage-7.8.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ab9b09a2349f58e73f8ebc06fac546dd623e23b063e5398343c5270072e3201c"},
+ {file = "coverage-7.8.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:fd51355ab8a372d89fb0e6a31719e825cf8df8b6724bee942fb5b92c3f016ba3"},
+ {file = "coverage-7.8.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:0774df1e093acb6c9e4d58bce7f86656aeed6c132a16e2337692c12786b32404"},
+ {file = "coverage-7.8.2-cp310-cp310-win32.whl", hash = "sha256:00f2e2f2e37f47e5f54423aeefd6c32a7dbcedc033fcd3928a4f4948e8b96af7"},
+ {file = "coverage-7.8.2-cp310-cp310-win_amd64.whl", hash = "sha256:145b07bea229821d51811bf15eeab346c236d523838eda395ea969d120d13347"},
+ {file = "coverage-7.8.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b99058eef42e6a8dcd135afb068b3d53aff3921ce699e127602efff9956457a9"},
+ {file = "coverage-7.8.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5feb7f2c3e6ea94d3b877def0270dff0947b8d8c04cfa34a17be0a4dc1836879"},
+ {file = "coverage-7.8.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:670a13249b957bb9050fab12d86acef7bf8f6a879b9d1a883799276e0d4c674a"},
+ {file = "coverage-7.8.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0bdc8bf760459a4a4187b452213e04d039990211f98644c7292adf1e471162b5"},
+ {file = "coverage-7.8.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:07a989c867986c2a75f158f03fdb413128aad29aca9d4dbce5fc755672d96f11"},
+ {file = "coverage-7.8.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2db10dedeb619a771ef0e2949ccba7b75e33905de959c2643a4607bef2f3fb3a"},
+ {file = "coverage-7.8.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e6ea7dba4e92926b7b5f0990634b78ea02f208d04af520c73a7c876d5a8d36cb"},
+ {file = "coverage-7.8.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ef2f22795a7aca99fc3c84393a55a53dd18ab8c93fb431004e4d8f0774150f54"},
+ {file = "coverage-7.8.2-cp311-cp311-win32.whl", hash = "sha256:641988828bc18a6368fe72355df5f1703e44411adbe49bba5644b941ce6f2e3a"},
+ {file = "coverage-7.8.2-cp311-cp311-win_amd64.whl", hash = "sha256:8ab4a51cb39dc1933ba627e0875046d150e88478dbe22ce145a68393e9652975"},
+ {file = "coverage-7.8.2-cp311-cp311-win_arm64.whl", hash = "sha256:8966a821e2083c74d88cca5b7dcccc0a3a888a596a04c0b9668a891de3a0cc53"},
+ {file = "coverage-7.8.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e2f6fe3654468d061942591aef56686131335b7a8325684eda85dacdf311356c"},
+ {file = "coverage-7.8.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:76090fab50610798cc05241bf83b603477c40ee87acd358b66196ab0ca44ffa1"},
+ {file = "coverage-7.8.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2bd0a0a5054be160777a7920b731a0570284db5142abaaf81bcbb282b8d99279"},
+ {file = "coverage-7.8.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:da23ce9a3d356d0affe9c7036030b5c8f14556bd970c9b224f9c8205505e3b99"},
+ {file = "coverage-7.8.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9392773cffeb8d7e042a7b15b82a414011e9d2b5fdbbd3f7e6a6b17d5e21b20"},
+ {file = "coverage-7.8.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:876cbfd0b09ce09d81585d266c07a32657beb3eaec896f39484b631555be0fe2"},
+ {file = "coverage-7.8.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3da9b771c98977a13fbc3830f6caa85cae6c9c83911d24cb2d218e9394259c57"},
+ {file = "coverage-7.8.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9a990f6510b3292686713bfef26d0049cd63b9c7bb17e0864f133cbfd2e6167f"},
+ {file = "coverage-7.8.2-cp312-cp312-win32.whl", hash = "sha256:bf8111cddd0f2b54d34e96613e7fbdd59a673f0cf5574b61134ae75b6f5a33b8"},
+ {file = "coverage-7.8.2-cp312-cp312-win_amd64.whl", hash = "sha256:86a323a275e9e44cdf228af9b71c5030861d4d2610886ab920d9945672a81223"},
+ {file = "coverage-7.8.2-cp312-cp312-win_arm64.whl", hash = "sha256:820157de3a589e992689ffcda8639fbabb313b323d26388d02e154164c57b07f"},
+ {file = "coverage-7.8.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ea561010914ec1c26ab4188aef8b1567272ef6de096312716f90e5baa79ef8ca"},
+ {file = "coverage-7.8.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cb86337a4fcdd0e598ff2caeb513ac604d2f3da6d53df2c8e368e07ee38e277d"},
+ {file = "coverage-7.8.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26a4636ddb666971345541b59899e969f3b301143dd86b0ddbb570bd591f1e85"},
+ {file = "coverage-7.8.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5040536cf9b13fb033f76bcb5e1e5cb3b57c4807fef37db9e0ed129c6a094257"},
+ {file = "coverage-7.8.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc67994df9bcd7e0150a47ef41278b9e0a0ea187caba72414b71dc590b99a108"},
+ {file = "coverage-7.8.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6e6c86888fd076d9e0fe848af0a2142bf606044dc5ceee0aa9eddb56e26895a0"},
+ {file = "coverage-7.8.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:684ca9f58119b8e26bef860db33524ae0365601492e86ba0b71d513f525e7050"},
+ {file = "coverage-7.8.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8165584ddedb49204c4e18da083913bdf6a982bfb558632a79bdaadcdafd0d48"},
+ {file = "coverage-7.8.2-cp313-cp313-win32.whl", hash = "sha256:34759ee2c65362163699cc917bdb2a54114dd06d19bab860725f94ef45a3d9b7"},
+ {file = "coverage-7.8.2-cp313-cp313-win_amd64.whl", hash = "sha256:2f9bc608fbafaee40eb60a9a53dbfb90f53cc66d3d32c2849dc27cf5638a21e3"},
+ {file = "coverage-7.8.2-cp313-cp313-win_arm64.whl", hash = "sha256:9fe449ee461a3b0c7105690419d0b0aba1232f4ff6d120a9e241e58a556733f7"},
+ {file = "coverage-7.8.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:8369a7c8ef66bded2b6484053749ff220dbf83cba84f3398c84c51a6f748a008"},
+ {file = "coverage-7.8.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:159b81df53a5fcbc7d45dae3adad554fdbde9829a994e15227b3f9d816d00b36"},
+ {file = "coverage-7.8.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e6fcbbd35a96192d042c691c9e0c49ef54bd7ed865846a3c9d624c30bb67ce46"},
+ {file = "coverage-7.8.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:05364b9cc82f138cc86128dc4e2e1251c2981a2218bfcd556fe6b0fbaa3501be"},
+ {file = "coverage-7.8.2-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46d532db4e5ff3979ce47d18e2fe8ecad283eeb7367726da0e5ef88e4fe64740"},
+ {file = "coverage-7.8.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4000a31c34932e7e4fa0381a3d6deb43dc0c8f458e3e7ea6502e6238e10be625"},
+ {file = "coverage-7.8.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:43ff5033d657cd51f83015c3b7a443287250dc14e69910577c3e03bd2e06f27b"},
+ {file = "coverage-7.8.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:94316e13f0981cbbba132c1f9f365cac1d26716aaac130866ca812006f662199"},
+ {file = "coverage-7.8.2-cp313-cp313t-win32.whl", hash = "sha256:3f5673888d3676d0a745c3d0e16da338c5eea300cb1f4ada9c872981265e76d8"},
+ {file = "coverage-7.8.2-cp313-cp313t-win_amd64.whl", hash = "sha256:2c08b05ee8d7861e45dc5a2cc4195c8c66dca5ac613144eb6ebeaff2d502e73d"},
+ {file = "coverage-7.8.2-cp313-cp313t-win_arm64.whl", hash = "sha256:1e1448bb72b387755e1ff3ef1268a06617afd94188164960dba8d0245a46004b"},
+ {file = "coverage-7.8.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:496948261eaac5ac9cf43f5d0a9f6eb7a6d4cb3bedb2c5d294138142f5c18f2a"},
+ {file = "coverage-7.8.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:eacd2de0d30871eff893bab0b67840a96445edcb3c8fd915e6b11ac4b2f3fa6d"},
+ {file = "coverage-7.8.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b039ffddc99ad65d5078ef300e0c7eed08c270dc26570440e3ef18beb816c1ca"},
+ {file = "coverage-7.8.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0e49824808d4375ede9dd84e9961a59c47f9113039f1a525e6be170aa4f5c34d"},
+ {file = "coverage-7.8.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b069938961dfad881dc2f8d02b47645cd2f455d3809ba92a8a687bf513839787"},
+ {file = "coverage-7.8.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:de77c3ba8bb686d1c411e78ee1b97e6e0b963fb98b1637658dd9ad2c875cf9d7"},
+ {file = "coverage-7.8.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:1676628065a498943bd3f64f099bb573e08cf1bc6088bbe33cf4424e0876f4b3"},
+ {file = "coverage-7.8.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:8e1a26e7e50076e35f7afafde570ca2b4d7900a491174ca357d29dece5aacee7"},
+ {file = "coverage-7.8.2-cp39-cp39-win32.whl", hash = "sha256:6782a12bf76fa61ad9350d5a6ef5f3f020b57f5e6305cbc663803f2ebd0f270a"},
+ {file = "coverage-7.8.2-cp39-cp39-win_amd64.whl", hash = "sha256:1efa4166ba75ccefd647f2d78b64f53f14fb82622bc94c5a5cb0a622f50f1c9e"},
+ {file = "coverage-7.8.2-pp39.pp310.pp311-none-any.whl", hash = "sha256:ec455eedf3ba0bbdf8f5a570012617eb305c63cb9f03428d39bf544cb2b94837"},
+ {file = "coverage-7.8.2-py3-none-any.whl", hash = "sha256:726f32ee3713f7359696331a18daf0c3b3a70bb0ae71141b9d3c52be7c595e32"},
+ {file = "coverage-7.8.2.tar.gz", hash = "sha256:a886d531373a1f6ff9fad2a2ba4a045b68467b779ae729ee0b3b10ac20033b27"},
+]
+
+[package.extras]
+toml = ["tomli ; python_full_version <= \"3.11.0a6\""]
+
+[[package]]
+name = "iniconfig"
+version = "2.1.0"
+description = "brain-dead simple config-ini parsing"
+optional = false
+python-versions = ">=3.8"
+groups = ["dev"]
+files = [
+ {file = "iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760"},
+ {file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"},
+]
+
+[[package]]
+name = "mypy"
+version = "1.16.0"
+description = "Optional static typing for Python"
+optional = false
+python-versions = ">=3.9"
+groups = ["dev"]
+files = [
+ {file = "mypy-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7909541fef256527e5ee9c0a7e2aeed78b6cda72ba44298d1334fe7881b05c5c"},
+ {file = "mypy-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e71d6f0090c2256c713ed3d52711d01859c82608b5d68d4fa01a3fe30df95571"},
+ {file = "mypy-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:936ccfdd749af4766be824268bfe22d1db9eb2f34a3ea1d00ffbe5b5265f5491"},
+ {file = "mypy-1.16.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4086883a73166631307fdd330c4a9080ce24913d4f4c5ec596c601b3a4bdd777"},
+ {file = "mypy-1.16.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:feec38097f71797da0231997e0de3a58108c51845399669ebc532c815f93866b"},
+ {file = "mypy-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:09a8da6a0ee9a9770b8ff61b39c0bb07971cda90e7297f4213741b48a0cc8d93"},
+ {file = "mypy-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9f826aaa7ff8443bac6a494cf743f591488ea940dd360e7dd330e30dd772a5ab"},
+ {file = "mypy-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:82d056e6faa508501af333a6af192c700b33e15865bda49611e3d7d8358ebea2"},
+ {file = "mypy-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:089bedc02307c2548eb51f426e085546db1fa7dd87fbb7c9fa561575cf6eb1ff"},
+ {file = "mypy-1.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6a2322896003ba66bbd1318c10d3afdfe24e78ef12ea10e2acd985e9d684a666"},
+ {file = "mypy-1.16.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:021a68568082c5b36e977d54e8f1de978baf401a33884ffcea09bd8e88a98f4c"},
+ {file = "mypy-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:54066fed302d83bf5128632d05b4ec68412e1f03ef2c300434057d66866cea4b"},
+ {file = "mypy-1.16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c5436d11e89a3ad16ce8afe752f0f373ae9620841c50883dc96f8b8805620b13"},
+ {file = "mypy-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f2622af30bf01d8fc36466231bdd203d120d7a599a6d88fb22bdcb9dbff84090"},
+ {file = "mypy-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d045d33c284e10a038f5e29faca055b90eee87da3fc63b8889085744ebabb5a1"},
+ {file = "mypy-1.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b4968f14f44c62e2ec4a038c8797a87315be8df7740dc3ee8d3bfe1c6bf5dba8"},
+ {file = "mypy-1.16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:eb14a4a871bb8efb1e4a50360d4e3c8d6c601e7a31028a2c79f9bb659b63d730"},
+ {file = "mypy-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:bd4e1ebe126152a7bbaa4daedd781c90c8f9643c79b9748caa270ad542f12bec"},
+ {file = "mypy-1.16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a9e056237c89f1587a3be1a3a70a06a698d25e2479b9a2f57325ddaaffc3567b"},
+ {file = "mypy-1.16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0b07e107affb9ee6ce1f342c07f51552d126c32cd62955f59a7db94a51ad12c0"},
+ {file = "mypy-1.16.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c6fb60cbd85dc65d4d63d37cb5c86f4e3a301ec605f606ae3a9173e5cf34997b"},
+ {file = "mypy-1.16.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a7e32297a437cc915599e0578fa6bc68ae6a8dc059c9e009c628e1c47f91495d"},
+ {file = "mypy-1.16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:afe420c9380ccec31e744e8baff0d406c846683681025db3531b32db56962d52"},
+ {file = "mypy-1.16.0-cp313-cp313-win_amd64.whl", hash = "sha256:55f9076c6ce55dd3f8cd0c6fff26a008ca8e5131b89d5ba6d86bd3f47e736eeb"},
+ {file = "mypy-1.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f56236114c425620875c7cf71700e3d60004858da856c6fc78998ffe767b73d3"},
+ {file = "mypy-1.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:15486beea80be24ff067d7d0ede673b001d0d684d0095803b3e6e17a886a2a92"},
+ {file = "mypy-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f2ed0e0847a80655afa2c121835b848ed101cc7b8d8d6ecc5205aedc732b1436"},
+ {file = "mypy-1.16.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eb5fbc8063cb4fde7787e4c0406aa63094a34a2daf4673f359a1fb64050e9cb2"},
+ {file = "mypy-1.16.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a5fcfdb7318c6a8dd127b14b1052743b83e97a970f0edb6c913211507a255e20"},
+ {file = "mypy-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:2e7e0ad35275e02797323a5aa1be0b14a4d03ffdb2e5f2b0489fa07b89c67b21"},
+ {file = "mypy-1.16.0-py3-none-any.whl", hash = "sha256:29e1499864a3888bca5c1542f2d7232c6e586295183320caa95758fc84034031"},
+ {file = "mypy-1.16.0.tar.gz", hash = "sha256:84b94283f817e2aa6350a14b4a8fb2a35a53c286f97c9d30f53b63620e7af8ab"},
+]
+
+[package.dependencies]
+mypy_extensions = ">=1.0.0"
+pathspec = ">=0.9.0"
+typing_extensions = ">=4.6.0"
+
+[package.extras]
+dmypy = ["psutil (>=4.0)"]
+faster-cache = ["orjson"]
+install-types = ["pip"]
+mypyc = ["setuptools (>=50)"]
+reports = ["lxml"]
+
+[[package]]
+name = "mypy-extensions"
+version = "1.1.0"
+description = "Type system extensions for programs checked with the mypy type checker."
+optional = false
+python-versions = ">=3.8"
+groups = ["dev"]
+files = [
+ {file = "mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505"},
+ {file = "mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558"},
+]
+
+[[package]]
+name = "packaging"
+version = "25.0"
+description = "Core utilities for Python packages"
+optional = false
+python-versions = ">=3.8"
+groups = ["dev"]
+files = [
+ {file = "packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484"},
+ {file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"},
+]
+
+[[package]]
+name = "pathspec"
+version = "0.12.1"
+description = "Utility library for gitignore style pattern matching of file paths."
+optional = false
+python-versions = ">=3.8"
+groups = ["dev"]
+files = [
+ {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"},
+ {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"},
+]
+
+[[package]]
+name = "platformdirs"
+version = "4.3.8"
+description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`."
+optional = false
+python-versions = ">=3.9"
+groups = ["dev"]
+files = [
+ {file = "platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4"},
+ {file = "platformdirs-4.3.8.tar.gz", hash = "sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc"},
+]
+
+[package.extras]
+docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.1.3)", "sphinx-autodoc-typehints (>=3)"]
+test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.4)", "pytest-cov (>=6)", "pytest-mock (>=3.14)"]
+type = ["mypy (>=1.14.1)"]
+
+[[package]]
+name = "pluggy"
+version = "1.6.0"
+description = "plugin and hook calling mechanisms for python"
+optional = false
+python-versions = ">=3.9"
+groups = ["dev"]
+files = [
+ {file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"},
+ {file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"},
+]
+
+[package.extras]
+dev = ["pre-commit", "tox"]
+testing = ["coverage", "pytest", "pytest-benchmark"]
+
+[[package]]
+name = "pygments"
+version = "2.19.1"
+description = "Pygments is a syntax highlighting package written in Python."
+optional = false
+python-versions = ">=3.8"
+groups = ["dev"]
+files = [
+ {file = "pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c"},
+ {file = "pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f"},
+]
+
+[package.extras]
+windows-terminal = ["colorama (>=0.4.6)"]
+
+[[package]]
+name = "pytest"
+version = "8.4.0"
+description = "pytest: simple powerful testing with Python"
+optional = false
+python-versions = ">=3.9"
+groups = ["dev"]
+files = [
+ {file = "pytest-8.4.0-py3-none-any.whl", hash = "sha256:f40f825768ad76c0977cbacdf1fd37c6f7a468e460ea6a0636078f8972d4517e"},
+ {file = "pytest-8.4.0.tar.gz", hash = "sha256:14d920b48472ea0dbf68e45b96cd1ffda4705f33307dcc86c676c1b5104838a6"},
+]
+
+[package.dependencies]
+colorama = {version = ">=0.4", markers = "sys_platform == \"win32\""}
+iniconfig = ">=1"
+packaging = ">=20"
+pluggy = ">=1.5,<2"
+pygments = ">=2.7.2"
+
+[package.extras]
+dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests", "setuptools", "xmlschema"]
+
+[[package]]
+name = "pytest-asyncio"
+version = "0.26.0"
+description = "Pytest support for asyncio"
+optional = false
+python-versions = ">=3.9"
+groups = ["dev"]
+files = [
+ {file = "pytest_asyncio-0.26.0-py3-none-any.whl", hash = "sha256:7b51ed894f4fbea1340262bdae5135797ebbe21d8638978e35d31c6d19f72fb0"},
+ {file = "pytest_asyncio-0.26.0.tar.gz", hash = "sha256:c4df2a697648241ff39e7f0e4a73050b03f123f760673956cf0d72a4990e312f"},
+]
+
+[package.dependencies]
+pytest = ">=8.2,<9"
+
+[package.extras]
+docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1)"]
+testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"]
+
+[[package]]
+name = "pytest-cov"
+version = "6.1.1"
+description = "Pytest plugin for measuring coverage."
+optional = false
+python-versions = ">=3.9"
+groups = ["dev"]
+files = [
+ {file = "pytest_cov-6.1.1-py3-none-any.whl", hash = "sha256:bddf29ed2d0ab6f4df17b4c55b0a657287db8684af9c42ea546b21b1041b3dde"},
+ {file = "pytest_cov-6.1.1.tar.gz", hash = "sha256:46935f7aaefba760e716c2ebfbe1c216240b9592966e7da99ea8292d4d3e2a0a"},
+]
+
+[package.dependencies]
+coverage = {version = ">=7.5", extras = ["toml"]}
+pytest = ">=4.6"
+
+[package.extras]
+testing = ["fields", "hunter", "process-tests", "pytest-xdist", "virtualenv"]
+
+[[package]]
+name = "typing-extensions"
+version = "4.14.0"
+description = "Backported and Experimental Type Hints for Python 3.9+"
+optional = false
+python-versions = ">=3.9"
+groups = ["main", "dev"]
+files = [
+ {file = "typing_extensions-4.14.0-py3-none-any.whl", hash = "sha256:a1514509136dd0b477638fc68d6a91497af5076466ad0fa6c338e44e359944af"},
+ {file = "typing_extensions-4.14.0.tar.gz", hash = "sha256:8676b788e32f02ab42d9e7c61324048ae4c6d844a399eebace3d4979d75ceef4"},
+]
+
+[[package]]
+name = "uvloop"
+version = "0.21.0"
+description = "Fast implementation of asyncio event loop on top of libuv"
+optional = true
+python-versions = ">=3.8.0"
+groups = ["main"]
+markers = "sys_platform != \"win32\" and extra == \"speed\""
+files = [
+ {file = "uvloop-0.21.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ec7e6b09a6fdded42403182ab6b832b71f4edaf7f37a9a0e371a01db5f0cb45f"},
+ {file = "uvloop-0.21.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:196274f2adb9689a289ad7d65700d37df0c0930fd8e4e743fa4834e850d7719d"},
+ {file = "uvloop-0.21.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f38b2e090258d051d68a5b14d1da7203a3c3677321cf32a95a6f4db4dd8b6f26"},
+ {file = "uvloop-0.21.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87c43e0f13022b998eb9b973b5e97200c8b90823454d4bc06ab33829e09fb9bb"},
+ {file = "uvloop-0.21.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:10d66943def5fcb6e7b37310eb6b5639fd2ccbc38df1177262b0640c3ca68c1f"},
+ {file = "uvloop-0.21.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:67dd654b8ca23aed0a8e99010b4c34aca62f4b7fce88f39d452ed7622c94845c"},
+ {file = "uvloop-0.21.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c0f3fa6200b3108919f8bdabb9a7f87f20e7097ea3c543754cabc7d717d95cf8"},
+ {file = "uvloop-0.21.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0878c2640cf341b269b7e128b1a5fed890adc4455513ca710d77d5e93aa6d6a0"},
+ {file = "uvloop-0.21.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9fb766bb57b7388745d8bcc53a359b116b8a04c83a2288069809d2b3466c37e"},
+ {file = "uvloop-0.21.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a375441696e2eda1c43c44ccb66e04d61ceeffcd76e4929e527b7fa401b90fb"},
+ {file = "uvloop-0.21.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:baa0e6291d91649c6ba4ed4b2f982f9fa165b5bbd50a9e203c416a2797bab3c6"},
+ {file = "uvloop-0.21.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4509360fcc4c3bd2c70d87573ad472de40c13387f5fda8cb58350a1d7475e58d"},
+ {file = "uvloop-0.21.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:359ec2c888397b9e592a889c4d72ba3d6befba8b2bb01743f72fffbde663b59c"},
+ {file = "uvloop-0.21.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f7089d2dc73179ce5ac255bdf37c236a9f914b264825fdaacaded6990a7fb4c2"},
+ {file = "uvloop-0.21.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:baa4dcdbd9ae0a372f2167a207cd98c9f9a1ea1188a8a526431eef2f8116cc8d"},
+ {file = "uvloop-0.21.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86975dca1c773a2c9864f4c52c5a55631038e387b47eaf56210f873887b6c8dc"},
+ {file = "uvloop-0.21.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:461d9ae6660fbbafedd07559c6a2e57cd553b34b0065b6550685f6653a98c1cb"},
+ {file = "uvloop-0.21.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:183aef7c8730e54c9a3ee3227464daed66e37ba13040bb3f350bc2ddc040f22f"},
+ {file = "uvloop-0.21.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:bfd55dfcc2a512316e65f16e503e9e450cab148ef11df4e4e679b5e8253a5281"},
+ {file = "uvloop-0.21.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:787ae31ad8a2856fc4e7c095341cccc7209bd657d0e71ad0dc2ea83c4a6fa8af"},
+ {file = "uvloop-0.21.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ee4d4ef48036ff6e5cfffb09dd192c7a5027153948d85b8da7ff705065bacc6"},
+ {file = "uvloop-0.21.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3df876acd7ec037a3d005b3ab85a7e4110422e4d9c1571d4fc89b0fc41b6816"},
+ {file = "uvloop-0.21.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bd53ecc9a0f3d87ab847503c2e1552b690362e005ab54e8a48ba97da3924c0dc"},
+ {file = "uvloop-0.21.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a5c39f217ab3c663dc699c04cbd50c13813e31d917642d459fdcec07555cc553"},
+ {file = "uvloop-0.21.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:17df489689befc72c39a08359efac29bbee8eee5209650d4b9f34df73d22e414"},
+ {file = "uvloop-0.21.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:bc09f0ff191e61c2d592a752423c767b4ebb2986daa9ed62908e2b1b9a9ae206"},
+ {file = "uvloop-0.21.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f0ce1b49560b1d2d8a2977e3ba4afb2414fb46b86a1b64056bc4ab929efdafbe"},
+ {file = "uvloop-0.21.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e678ad6fe52af2c58d2ae3c73dc85524ba8abe637f134bf3564ed07f555c5e79"},
+ {file = "uvloop-0.21.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:460def4412e473896ef179a1671b40c039c7012184b627898eea5072ef6f017a"},
+ {file = "uvloop-0.21.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:10da8046cc4a8f12c91a1c39d1dd1585c41162a15caaef165c2174db9ef18bdc"},
+ {file = "uvloop-0.21.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c097078b8031190c934ed0ebfee8cc5f9ba9642e6eb88322b9958b649750f72b"},
+ {file = "uvloop-0.21.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:46923b0b5ee7fc0020bef24afe7836cb068f5050ca04caf6b487c513dc1a20b2"},
+ {file = "uvloop-0.21.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:53e420a3afe22cdcf2a0f4846e377d16e718bc70103d7088a4f7623567ba5fb0"},
+ {file = "uvloop-0.21.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88cb67cdbc0e483da00af0b2c3cdad4b7c61ceb1ee0f33fe00e09c81e3a6cb75"},
+ {file = "uvloop-0.21.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:221f4f2a1f46032b403bf3be628011caf75428ee3cc204a22addf96f586b19fd"},
+ {file = "uvloop-0.21.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:2d1f581393673ce119355d56da84fe1dd9d2bb8b3d13ce792524e1607139feff"},
+ {file = "uvloop-0.21.0.tar.gz", hash = "sha256:3bf12b0fda68447806a7ad847bfa591613177275d35b6724b1ee573faa3704e3"},
+]
+
+[package.extras]
+dev = ["Cython (>=3.0,<4.0)", "setuptools (>=60)"]
+docs = ["Sphinx (>=4.1.2,<4.2.0)", "sphinx-rtd-theme (>=0.5.2,<0.6.0)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)"]
+test = ["aiohttp (>=3.10.5)", "flake8 (>=5.0,<6.0)", "mypy (>=0.800)", "psutil", "pyOpenSSL (>=23.0.0,<23.1.0)", "pycodestyle (>=2.9.0,<2.10.0)"]
+
+[[package]]
+name = "winloop"
+version = "0.1.8"
+description = "Windows version of uvloop"
+optional = true
+python-versions = ">=3.8.0"
+groups = ["main"]
+markers = "sys_platform == \"win32\" and extra == \"speed\""
+files = [
+ {file = "winloop-0.1.8-cp310-cp310-win_amd64.whl", hash = "sha256:5c871a0d3100ac80ca813294f6c601d4537c2215bf608f395171003cf2540b5f"},
+ {file = "winloop-0.1.8-cp311-cp311-win_amd64.whl", hash = "sha256:d2bbcd6fc2e3eb6b0e15b9641fd1fb80a2e573c9601689ad108815ce1cdaf9bf"},
+ {file = "winloop-0.1.8-cp312-cp312-win_amd64.whl", hash = "sha256:736a21489419369e2affdbc8f274bb0d56df318d58106fc8febf1002ffd450d7"},
+ {file = "winloop-0.1.8-cp313-cp313-win_amd64.whl", hash = "sha256:0c1c2d2087cb2c1b7defefed44bd875c9b040d46a32caa27d1847e06cb4e5f50"},
+ {file = "winloop-0.1.8-cp38-cp38-win_amd64.whl", hash = "sha256:8fb60cad2f25ada8e0c1a9d2eace2d9f54fa28b630102ae5ee3c467c14c90ac3"},
+ {file = "winloop-0.1.8-cp39-cp39-win_amd64.whl", hash = "sha256:c3599890716d229b2b657cf0893bd1c9c14833704529c17d8764bc058f64e598"},
+ {file = "winloop-0.1.8.tar.gz", hash = "sha256:bbb1b8e12bd9d231153e4a143440d862886a67675aa1a0701f98dff42c19d857"},
+]
+
+[package.extras]
+dev = ["Cython (>=3.0,<4.0)", "setuptools (>=60)"]
+docs = ["Sphinx (>=4.1.2,<4.2.0)", "sphinx_rtd_theme (>=0.5.2,<0.6.0)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)"]
+test = ["aiohttp (>=3.10.5)", "flake8 (>=5.0,<6.0)", "mypy (>=0.800)", "psutil", "pyOpenSSL (>=23.0.0,<23.1.0)", "pycodestyle (>=2.9.0,<2.10.0)"]
+
+[extras]
+speed = ["uvloop", "winloop"]
+
+[metadata]
+lock-version = "2.1"
+python-versions = ">=3.11"
+content-hash = "20167cd8fc16aa8ba19c714e451053ef6db7518f589746233f2648c4c09ac13c"