kopia lustrzana https://github.com/Yakifo/amqtt
rebuild of the persistence plugin to handle storing / restoring sessions
rodzic
71d6e141a5
commit
1e5a78f601
|
@ -101,7 +101,7 @@ class BrokerContext(BaseContext):
|
|||
|
||||
def __init__(self, broker: "Broker") -> None:
|
||||
super().__init__()
|
||||
self.config: _CONFIG_LISTENER | None = None
|
||||
self.config: _CONFIG_LISTENER | object | None = None
|
||||
self._broker_instance = broker
|
||||
|
||||
async def broadcast_message(self, topic: str, data: bytes, qos: int | None = None) -> None:
|
||||
|
@ -123,6 +123,18 @@ class BrokerContext(BaseContext):
|
|||
def subscriptions(self) -> dict[str, list[tuple[Session, int]]]:
|
||||
return self._broker_instance.subscriptions
|
||||
|
||||
async def add_subscription(self, client_id: str, topic: str, qos: int) -> None:
|
||||
|
||||
if client_id not in self._broker_instance.sessions:
|
||||
broker_handler, session = self._broker_instance.create_offline_session(client_id)
|
||||
self._broker_instance._sessions[client_id] = (session, broker_handler) # noqa: SLF001
|
||||
|
||||
session, _ = self._broker_instance.sessions[client_id]
|
||||
await self._broker_instance.add_subscription((topic, qos), session)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class Broker:
|
||||
"""MQTT 3.1.1 compliant broker implementation.
|
||||
|
@ -451,6 +463,14 @@ class Broker:
|
|||
self.logger.debug(f"Keep-alive timeout={client_session.keep_alive}")
|
||||
return handler, client_session
|
||||
|
||||
def create_offline_session(self, client_id: str) -> tuple[BrokerProtocolHandler, Session]:
|
||||
session = Session()
|
||||
session.client_id = client_id
|
||||
|
||||
bph = BrokerProtocolHandler(self.plugins_manager, session)
|
||||
session.transitions.disconnect()
|
||||
return bph, session
|
||||
|
||||
async def _handle_client_session(
|
||||
self,
|
||||
reader: ReaderAdapter,
|
||||
|
@ -603,7 +623,7 @@ class Broker:
|
|||
"""Handle client subscription."""
|
||||
self.logger.debug(f"{client_session.client_id} handling subscription")
|
||||
subscriptions = subscribe_waiter.result()
|
||||
return_codes = [await self._add_subscription(subscription, client_session) for subscription in subscriptions.topics]
|
||||
return_codes = [await self.add_subscription(subscription, client_session) for subscription in subscriptions.topics]
|
||||
await handler.mqtt_acknowledge_subscription(subscriptions.packet_id, return_codes)
|
||||
for index, subscription in enumerate(subscriptions.topics):
|
||||
if return_codes[index] != AMQTT_MAGIC_VALUE_RET_SUBSCRIBED:
|
||||
|
@ -724,7 +744,7 @@ class Broker:
|
|||
self.logger.debug(f"Clearing retained messages for topic '{topic_name}'")
|
||||
del self._retained_messages[topic_name]
|
||||
|
||||
async def _add_subscription(self, subscription: tuple[str, int], session: Session) -> int:
|
||||
async def add_subscription(self, subscription: tuple[str, int], session: Session) -> int:
|
||||
topic_filter, qos = subscription
|
||||
if "#" in topic_filter and not topic_filter.endswith("#"):
|
||||
# [MQTT-4.7.1-2] Wildcard character '#' is only allowed as last character in filter
|
||||
|
|
|
@ -1,85 +1,125 @@
|
|||
import json
|
||||
import sqlite3
|
||||
from typing import Any
|
||||
from dataclasses import asdict, dataclass, is_dataclass
|
||||
from typing import Any, TypeVar
|
||||
import warnings
|
||||
|
||||
from amqtt.contexts import BaseContext
|
||||
from amqtt.session import Session
|
||||
from sqlalchemy import JSON, Boolean, Integer, LargeBinary, String
|
||||
from sqlalchemy.ext.asyncio import create_async_engine
|
||||
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
|
||||
from sqlalchemy.types import TypeDecorator
|
||||
|
||||
from amqtt.broker import BrokerContext
|
||||
from amqtt.errors import PluginError
|
||||
from amqtt.mqtt.constants import QOS_0
|
||||
from amqtt.plugins.base import BasePlugin
|
||||
|
||||
|
||||
class SQLitePlugin:
|
||||
def __init__(self, context: BaseContext) -> None:
|
||||
self.context: BaseContext = context
|
||||
self.conn: sqlite3.Connection | None = None
|
||||
self.cursor: sqlite3.Cursor | None = None
|
||||
self.db_file: str | None = None
|
||||
self.persistence_config: dict[str, Any]
|
||||
|
||||
if (
|
||||
persistence_config := self.context.config.get("persistence") if self.context.config is not None else None
|
||||
) is not None:
|
||||
self.persistence_config = persistence_config
|
||||
self.init_db()
|
||||
else:
|
||||
self.context.logger.warning("'persistence' section not found in context configuration")
|
||||
def __init__(self) -> None:
|
||||
warnings.warn("SQLitePlugin is deprecated, use amqtt.plugins.persistence.SessionDBPlugin", stacklevel=1)
|
||||
|
||||
def init_db(self) -> None:
|
||||
self.db_file = self.persistence_config.get("file")
|
||||
if not self.db_file:
|
||||
self.context.logger.warning("'file' persistence parameter not found")
|
||||
else:
|
||||
try:
|
||||
self.conn = sqlite3.connect(self.db_file)
|
||||
self.cursor = self.conn.cursor()
|
||||
self.context.logger.info(f"Database file '{self.db_file}' opened")
|
||||
except Exception:
|
||||
self.context.logger.exception(f"Error while initializing database '{self.db_file}'")
|
||||
if self.cursor:
|
||||
self.cursor.execute(
|
||||
"CREATE TABLE IF NOT EXISTS session(client_id TEXT PRIMARY KEY, data BLOB)",
|
||||
)
|
||||
self.cursor.execute("PRAGMA table_info(session)")
|
||||
columns = {col[1] for col in self.cursor.fetchall()}
|
||||
required_columns = {"client_id", "data"}
|
||||
if not required_columns.issubset(columns):
|
||||
self.context.logger.error("Database schema for 'session' table is incompatible.")
|
||||
class Base(DeclarativeBase):
|
||||
pass
|
||||
|
||||
async def save_session(self, session: Session) -> None:
|
||||
if self.cursor and self.conn:
|
||||
dump: str = json.dumps(session, default=str)
|
||||
try:
|
||||
self.cursor.execute(
|
||||
"INSERT OR REPLACE INTO session (client_id, data) VALUES (?, ?)",
|
||||
(session.client_id, dump),
|
||||
)
|
||||
self.conn.commit()
|
||||
except Exception:
|
||||
self.context.logger.exception(f"Failed saving session '{session}'")
|
||||
|
||||
async def find_session(self, client_id: str) -> Session | None:
|
||||
if self.cursor:
|
||||
row = self.cursor.execute(
|
||||
"SELECT data FROM session where client_id=?",
|
||||
(client_id,),
|
||||
).fetchone()
|
||||
return json.loads(row[0]) if row else None
|
||||
return None
|
||||
class RetainedMessage:
|
||||
topic: str
|
||||
data: str
|
||||
qos: int
|
||||
|
||||
async def del_session(self, client_id: str) -> None:
|
||||
if self.cursor and self.conn:
|
||||
try:
|
||||
exists = self.cursor.execute("SELECT 1 FROM session WHERE client_id=?", (client_id,)).fetchone()
|
||||
if exists:
|
||||
self.cursor.execute("DELETE FROM session where client_id=?", (client_id,))
|
||||
self.conn.commit()
|
||||
except Exception:
|
||||
self.context.logger.exception(f"Failed deleting session with client_id '{client_id}'")
|
||||
|
||||
async def on_broker_post_shutdown(self) -> None:
|
||||
if self.conn:
|
||||
try:
|
||||
self.conn.close()
|
||||
self.context.logger.info(f"Database file '{self.db_file}' closed")
|
||||
except Exception:
|
||||
self.context.logger.exception("Error closing database connection")
|
||||
finally:
|
||||
self.conn = None
|
||||
T = TypeVar("T")
|
||||
|
||||
class DataClassListJSON(TypeDecorator[list[dict[str, Any]]]):
|
||||
impl = JSON
|
||||
cache_ok = True
|
||||
|
||||
def __init__(self, dataclass_type: type[T]) -> None:
|
||||
if not is_dataclass(dataclass_type):
|
||||
msg = f"{dataclass_type} must be a dataclass type"
|
||||
raise TypeError(msg)
|
||||
self.dataclass_type: type[T] = dataclass_type
|
||||
super().__init__()
|
||||
|
||||
def process_bind_param(
|
||||
self,
|
||||
value: list[Any] | None, # Python -> DB
|
||||
dialect: Any
|
||||
) -> list[dict[str, Any]] | None:
|
||||
if value is None:
|
||||
return None
|
||||
return [asdict(item) for item in value]
|
||||
|
||||
def process_result_value(
|
||||
self,
|
||||
value: list[dict[str, Any]] | None, # DB -> Python
|
||||
dialect: Any
|
||||
) -> list[Any] | None:
|
||||
if value is None:
|
||||
return None
|
||||
return [self.dataclass_type(**item) for item in value]
|
||||
|
||||
class Session(Base):
|
||||
__tablename__ = "sessions"
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
client_id: Mapped[str] = mapped_column(String)
|
||||
|
||||
clean_session: Mapped[bool | None] = mapped_column(Boolean, nullable=True)
|
||||
|
||||
will_flag: Mapped[bool] = mapped_column(Boolean, default=False, server_default="false")
|
||||
|
||||
will_message: Mapped[bytes | None] = mapped_column(LargeBinary, nullable=True, default=None)
|
||||
will_qos: Mapped[int | None] = mapped_column(Integer, nullable=True, default=None)
|
||||
will_retain: Mapped[bool | None] = mapped_column(Boolean, nullable=True, default=None)
|
||||
will_topic: Mapped[str | None] = mapped_column(String, nullable=True, default=None)
|
||||
|
||||
keep_alive: Mapped[int] = mapped_column(Integer, default=0)
|
||||
retained: Mapped[list[RetainedMessage]] = mapped_column(DataClassListJSON(RetainedMessage), default=list)
|
||||
|
||||
class SessionDBPlugin(BasePlugin[BrokerContext]):
|
||||
def __init__(self, context: BrokerContext) -> None:
|
||||
super().__init__(context)
|
||||
|
||||
self._engine = create_async_engine(f"sqlite+aiosqlite:///{context.config.file}")
|
||||
|
||||
|
||||
async def on_broker_client_connected(self, client_id:str) -> None:
|
||||
"""Search to see if session already exists."""
|
||||
# if client id doesn't exist, create (can ignore if session is anonymous)
|
||||
# update session information (will, clean_session, etc)
|
||||
|
||||
async def on_broker_client_subscribed(self, client_id: str, topic: str, qos: int) -> None:
|
||||
"""Store subscription if clean session = false."""
|
||||
|
||||
async def on_broker_client_unsubscribed(self, client_id: str, topic: str) -> None:
|
||||
"""Remove subscription if clean session = false."""
|
||||
|
||||
async def on_broker_pre_start(self) -> None:
|
||||
"""Initialize the database and db connection."""
|
||||
async with self._engine.begin() as conn:
|
||||
await conn.run_sync(Base.metadata.create_all)
|
||||
|
||||
|
||||
async def on_broker_post_start(self) -> None:
|
||||
"""Load subscriptions."""
|
||||
if len(self.context.subscriptions) > 0:
|
||||
msg = "SessionDBPlugin : broker shouldn't have any subscriptions yet"
|
||||
raise PluginError(msg)
|
||||
|
||||
|
||||
if len(list(self.context.sessions)) > 0:
|
||||
msg = "SessionDBPlugin : broker shouldn't have any sessions yet"
|
||||
raise PluginError(msg)
|
||||
|
||||
await self.context.add_subscription("test_client1", "a/b", QOS_0)
|
||||
|
||||
async def on_broker_pre_shutdown(self) -> None:
|
||||
"""Clean up the db connection."""
|
||||
await self._engine.dispose()
|
||||
|
||||
@dataclass
|
||||
class Config:
|
||||
"""Configuration variables."""
|
||||
|
||||
file: str = "amqtt.sqlite3"
|
||||
|
|
|
@ -34,7 +34,7 @@ dependencies = [
|
|||
"PyYAML==6.0.2", # https://pypi.org/project/PyYAML
|
||||
"typer==0.15.4",
|
||||
"dacite>=1.9.2",
|
||||
"psutil>=7.0.0",
|
||||
"psutil>=7.0.0"
|
||||
]
|
||||
|
||||
[dependency-groups]
|
||||
|
@ -80,6 +80,10 @@ docs = [
|
|||
|
||||
[project.optional-dependencies]
|
||||
ci = ["coveralls==4.0.1"]
|
||||
db = [
|
||||
"aiosqlite>=0.21.0",
|
||||
"sqlalchemy[asyncio]>=2.0.41",
|
||||
]
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -4,53 +4,99 @@ from pathlib import Path
|
|||
import sqlite3
|
||||
import unittest
|
||||
|
||||
import pytest
|
||||
|
||||
from amqtt.broker import Broker
|
||||
from amqtt.client import MQTTClient
|
||||
from amqtt.contexts import BaseContext
|
||||
from amqtt.plugins.persistence import SQLitePlugin
|
||||
from amqtt.session import Session
|
||||
from samples.client_publish_ssl import client
|
||||
|
||||
formatter = "[%(asctime)s] %(name)s {%(filename)s:%(lineno)d} %(levelname)s - %(message)s"
|
||||
logging.basicConfig(level=logging.DEBUG, format=formatter)
|
||||
|
||||
|
||||
class TestSQLitePlugin(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
self.loop = asyncio.new_event_loop()
|
||||
|
||||
def test_create_tables(self) -> None:
|
||||
dbfile = Path(__file__).resolve().parent / "test.db"
|
||||
@pytest.mark.asyncio
|
||||
async def test_rehydrate_subscriptions() -> None:
|
||||
|
||||
context = BaseContext()
|
||||
context.logger = logging.getLogger(__name__)
|
||||
context.config = {"persistence": {"file": str(dbfile)}} # Ensure string path for config
|
||||
SQLitePlugin(context)
|
||||
cfg = {
|
||||
'listeners': {
|
||||
'default': {
|
||||
'type': 'tcp',
|
||||
'bind': '127.0.0.1:1883'
|
||||
}
|
||||
},
|
||||
'plugins': {
|
||||
'amqtt.plugins.authentication.AnonymousAuthPlugin': {'allow-anonymous': True},
|
||||
'amqtt.plugins.persistence.SessionDBPlugin': {}
|
||||
}
|
||||
}
|
||||
|
||||
try:
|
||||
conn = sqlite3.connect(str(dbfile)) # Convert Path to string for sqlite connection
|
||||
cursor = conn.cursor()
|
||||
rows = cursor.execute("SELECT name FROM sqlite_master WHERE type = 'table'")
|
||||
tables = [row[0] for row in rows] # List comprehension for brevity
|
||||
assert "session" in tables
|
||||
finally:
|
||||
conn.close()
|
||||
b = Broker(config=cfg)
|
||||
await b.start()
|
||||
await asyncio.sleep(1)
|
||||
|
||||
def test_save_session(self) -> None:
|
||||
dbfile = Path(__file__).resolve().parent / "test.db"
|
||||
# c = MQTTClient(client_id='test_client1', config={'auto_reconnect':False})
|
||||
# await c.connect(cleansession=False)
|
||||
#
|
||||
# await c.publish('a/b', b'my messages without subscription')
|
||||
#
|
||||
# msg = await c.deliver_message(timeout_duration=1)
|
||||
# assert msg is not None
|
||||
# assert msg.topic == 'a/b'
|
||||
# assert msg.data == b'my messages without subscription'
|
||||
#
|
||||
# await c.disconnect()
|
||||
# await asyncio.sleep(0.5)
|
||||
await b.shutdown()
|
||||
|
||||
context = BaseContext()
|
||||
context.logger = logging.getLogger(__name__)
|
||||
context.config = {"persistence": {"file": str(dbfile)}} # Ensure string path for config
|
||||
sql_plugin = SQLitePlugin(context)
|
||||
|
||||
s = Session()
|
||||
s.client_id = "test_save_session"
|
||||
|
||||
self.loop.run_until_complete(sql_plugin.save_session(session=s))
|
||||
|
||||
try:
|
||||
conn = sqlite3.connect(str(dbfile)) # Convert Path to string for sqlite connection
|
||||
cursor = conn.cursor()
|
||||
row = cursor.execute("SELECT client_id FROM session WHERE client_id = 'test_save_session'").fetchone()
|
||||
assert row is not None
|
||||
assert row[0] == s.client_id
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
|
||||
|
||||
"""
|
||||
|
||||
def test_create_tables(self) -> None:
|
||||
dbfile = Path(__file__).resolve().parent / "test.db"
|
||||
|
||||
context = BaseContext()
|
||||
context.logger = logging.getLogger(__name__)
|
||||
context.config = {"persistence": {"file": str(dbfile)}} # Ensure string path for config
|
||||
SQLitePlugin(context)
|
||||
|
||||
try:
|
||||
conn = sqlite3.connect(str(dbfile)) # Convert Path to string for sqlite connection
|
||||
cursor = conn.cursor()
|
||||
rows = cursor.execute("SELECT name FROM sqlite_master WHERE type = 'table'")
|
||||
tables = [row[0] for row in rows] # List comprehension for brevity
|
||||
assert "session" in tables
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
def test_save_session(self) -> None:
|
||||
dbfile = Path(__file__).resolve().parent / "test.db"
|
||||
|
||||
context = BaseContext()
|
||||
context.logger = logging.getLogger(__name__)
|
||||
context.config = {"persistence": {"file": str(dbfile)}} # Ensure string path for config
|
||||
sql_plugin = SQLitePlugin(context)
|
||||
|
||||
s = Session()
|
||||
s.client_id = "test_save_session"
|
||||
|
||||
self.loop.run_until_complete(sql_plugin.save_session(session=s))
|
||||
|
||||
try:
|
||||
conn = sqlite3.connect(str(dbfile)) # Convert Path to string for sqlite connection
|
||||
cursor = conn.cursor()
|
||||
row = cursor.execute("SELECT client_id FROM session WHERE client_id = 'test_save_session'").fetchone()
|
||||
assert row is not None
|
||||
assert row[0] == s.client_id
|
||||
finally:
|
||||
conn.close()
|
||||
"""
|
121
uv.lock
121
uv.lock
|
@ -7,6 +7,18 @@ resolution-markers = [
|
|||
"python_full_version < '3.11'",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aiosqlite"
|
||||
version = "0.21.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/13/7d/8bca2bf9a247c2c5dfeec1d7a5f40db6518f88d314b8bca9da29670d2671/aiosqlite-0.21.0.tar.gz", hash = "sha256:131bb8056daa3bc875608c631c678cda73922a2d4ba8aec373b19f18c17e7aa3", size = 13454, upload-time = "2025-02-03T07:30:16.235Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/10/6c25ed6de94c49f88a91fa5018cb4c0f3625f31d5be9f771ebe5cc7cd506/aiosqlite-0.21.0-py3-none-any.whl", hash = "sha256:2549cf4057f95f53dcba16f2b64e8e2791d7e1adedb13197dd8ed77bb226d7d0", size = 15792, upload-time = "2025-02-03T07:30:13.6Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "amqtt"
|
||||
version = "0.11.1"
|
||||
|
@ -25,6 +37,10 @@ dependencies = [
|
|||
ci = [
|
||||
{ name = "coveralls" },
|
||||
]
|
||||
db = [
|
||||
{ name = "aiosqlite" },
|
||||
{ name = "sqlalchemy", extra = ["asyncio"] },
|
||||
]
|
||||
|
||||
[package.dev-dependencies]
|
||||
dev = [
|
||||
|
@ -67,16 +83,18 @@ docs = [
|
|||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "aiosqlite", marker = "extra == 'db'", specifier = ">=0.21.0" },
|
||||
{ name = "coveralls", marker = "extra == 'ci'", specifier = "==4.0.1" },
|
||||
{ name = "dacite", specifier = ">=1.9.2" },
|
||||
{ name = "passlib", specifier = "==1.7.4" },
|
||||
{ name = "psutil", specifier = ">=7.0.0" },
|
||||
{ name = "pyyaml", specifier = "==6.0.2" },
|
||||
{ name = "sqlalchemy", extras = ["asyncio"], marker = "extra == 'db'", specifier = ">=2.0.41" },
|
||||
{ name = "transitions", specifier = "==0.9.2" },
|
||||
{ name = "typer", specifier = "==0.15.4" },
|
||||
{ name = "websockets", specifier = "==15.0.1" },
|
||||
]
|
||||
provides-extras = ["ci"]
|
||||
provides-extras = ["ci", "db"]
|
||||
|
||||
[package.metadata.requires-dev]
|
||||
dev = [
|
||||
|
@ -579,6 +597,57 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/1d/9a/4114a9057db2f1462d5c8f8390ab7383925fe1ac012eaa42402ad65c2963/GitPython-3.1.44-py3-none-any.whl", hash = "sha256:9e0e10cda9bed1ee64bc9a6de50e7e38a9c9943241cd7f585f6df3ed28011110", size = 207599, upload-time = "2025-01-02T07:32:40.731Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "greenlet"
|
||||
version = "3.2.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c9/92/bb85bd6e80148a4d2e0c59f7c0c2891029f8fd510183afc7d8d2feeed9b6/greenlet-3.2.3.tar.gz", hash = "sha256:8b0dd8ae4c0d6f5e54ee55ba935eeb3d735a9b58a8a1e5b5cbab64e01a39f365", size = 185752, upload-time = "2025-06-05T16:16:09.955Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/92/db/b4c12cff13ebac2786f4f217f06588bccd8b53d260453404ef22b121fc3a/greenlet-3.2.3-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:1afd685acd5597349ee6d7a88a8bec83ce13c106ac78c196ee9dde7c04fe87be", size = 268977, upload-time = "2025-06-05T16:10:24.001Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/52/61/75b4abd8147f13f70986df2801bf93735c1bd87ea780d70e3b3ecda8c165/greenlet-3.2.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:761917cac215c61e9dc7324b2606107b3b292a8349bdebb31503ab4de3f559ac", size = 627351, upload-time = "2025-06-05T16:38:50.685Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/35/aa/6894ae299d059d26254779a5088632874b80ee8cf89a88bca00b0709d22f/greenlet-3.2.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:a433dbc54e4a37e4fff90ef34f25a8c00aed99b06856f0119dcf09fbafa16392", size = 638599, upload-time = "2025-06-05T16:41:34.057Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/30/64/e01a8261d13c47f3c082519a5e9dbf9e143cc0498ed20c911d04e54d526c/greenlet-3.2.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:72e77ed69312bab0434d7292316d5afd6896192ac4327d44f3d613ecb85b037c", size = 634482, upload-time = "2025-06-05T16:48:16.26Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/47/48/ff9ca8ba9772d083a4f5221f7b4f0ebe8978131a9ae0909cf202f94cd879/greenlet-3.2.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:68671180e3849b963649254a882cd544a3c75bfcd2c527346ad8bb53494444db", size = 633284, upload-time = "2025-06-05T16:13:01.599Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/45/626e974948713bc15775b696adb3eb0bd708bec267d6d2d5c47bb47a6119/greenlet-3.2.3-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:49c8cfb18fb419b3d08e011228ef8a25882397f3a859b9fe1436946140b6756b", size = 582206, upload-time = "2025-06-05T16:12:48.51Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/8e/8b6f42c67d5df7db35b8c55c9a850ea045219741bb14416255616808c690/greenlet-3.2.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:efc6dc8a792243c31f2f5674b670b3a95d46fa1c6a912b8e310d6f542e7b0712", size = 1111412, upload-time = "2025-06-05T16:36:45.479Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/46/ab58828217349500a7ebb81159d52ca357da747ff1797c29c6023d79d798/greenlet-3.2.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:731e154aba8e757aedd0781d4b240f1225b075b4409f1bb83b05ff410582cf00", size = 1135054, upload-time = "2025-06-05T16:12:36.478Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/68/7f/d1b537be5080721c0f0089a8447d4ef72839039cdb743bdd8ffd23046e9a/greenlet-3.2.3-cp310-cp310-win_amd64.whl", hash = "sha256:96c20252c2f792defe9a115d3287e14811036d51e78b3aaddbee23b69b216302", size = 296573, upload-time = "2025-06-05T16:34:26.521Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fc/2e/d4fcb2978f826358b673f779f78fa8a32ee37df11920dc2bb5589cbeecef/greenlet-3.2.3-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:784ae58bba89fa1fa5733d170d42486580cab9decda3484779f4759345b29822", size = 270219, upload-time = "2025-06-05T16:10:10.414Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/16/24/929f853e0202130e4fe163bc1d05a671ce8dcd604f790e14896adac43a52/greenlet-3.2.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0921ac4ea42a5315d3446120ad48f90c3a6b9bb93dd9b3cf4e4d84a66e42de83", size = 630383, upload-time = "2025-06-05T16:38:51.785Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/b2/0320715eb61ae70c25ceca2f1d5ae620477d246692d9cc284c13242ec31c/greenlet-3.2.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:d2971d93bb99e05f8c2c0c2f4aa9484a18d98c4c3bd3c62b65b7e6ae33dfcfaf", size = 642422, upload-time = "2025-06-05T16:41:35.259Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bd/49/445fd1a210f4747fedf77615d941444349c6a3a4a1135bba9701337cd966/greenlet-3.2.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:c667c0bf9d406b77a15c924ef3285e1e05250948001220368e039b6aa5b5034b", size = 638375, upload-time = "2025-06-05T16:48:18.235Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/c8/ca19760cf6eae75fa8dc32b487e963d863b3ee04a7637da77b616703bc37/greenlet-3.2.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:592c12fb1165be74592f5de0d70f82bc5ba552ac44800d632214b76089945147", size = 637627, upload-time = "2025-06-05T16:13:02.858Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/65/89/77acf9e3da38e9bcfca881e43b02ed467c1dedc387021fc4d9bd9928afb8/greenlet-3.2.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:29e184536ba333003540790ba29829ac14bb645514fbd7e32af331e8202a62a5", size = 585502, upload-time = "2025-06-05T16:12:49.642Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/97/c6/ae244d7c95b23b7130136e07a9cc5aadd60d59b5951180dc7dc7e8edaba7/greenlet-3.2.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:93c0bb79844a367782ec4f429d07589417052e621aa39a5ac1fb99c5aa308edc", size = 1114498, upload-time = "2025-06-05T16:36:46.598Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/89/5f/b16dec0cbfd3070658e0d744487919740c6d45eb90946f6787689a7efbce/greenlet-3.2.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:751261fc5ad7b6705f5f76726567375bb2104a059454e0226e1eef6c756748ba", size = 1139977, upload-time = "2025-06-05T16:12:38.262Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/66/77/d48fb441b5a71125bcac042fc5b1494c806ccb9a1432ecaa421e72157f77/greenlet-3.2.3-cp311-cp311-win_amd64.whl", hash = "sha256:83a8761c75312361aa2b5b903b79da97f13f556164a7dd2d5448655425bd4c34", size = 297017, upload-time = "2025-06-05T16:25:05.225Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f3/94/ad0d435f7c48debe960c53b8f60fb41c2026b1d0fa4a99a1cb17c3461e09/greenlet-3.2.3-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:25ad29caed5783d4bd7a85c9251c651696164622494c00802a139c00d639242d", size = 271992, upload-time = "2025-06-05T16:11:23.467Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/93/5d/7c27cf4d003d6e77749d299c7c8f5fd50b4f251647b5c2e97e1f20da0ab5/greenlet-3.2.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:88cd97bf37fe24a6710ec6a3a7799f3f81d9cd33317dcf565ff9950c83f55e0b", size = 638820, upload-time = "2025-06-05T16:38:52.882Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c6/7e/807e1e9be07a125bb4c169144937910bf59b9d2f6d931578e57f0bce0ae2/greenlet-3.2.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:baeedccca94880d2f5666b4fa16fc20ef50ba1ee353ee2d7092b383a243b0b0d", size = 653046, upload-time = "2025-06-05T16:41:36.343Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9d/ab/158c1a4ea1068bdbc78dba5a3de57e4c7aeb4e7fa034320ea94c688bfb61/greenlet-3.2.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:be52af4b6292baecfa0f397f3edb3c6092ce071b499dd6fe292c9ac9f2c8f264", size = 647701, upload-time = "2025-06-05T16:48:19.604Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cc/0d/93729068259b550d6a0288da4ff72b86ed05626eaf1eb7c0d3466a2571de/greenlet-3.2.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0cc73378150b8b78b0c9fe2ce56e166695e67478550769536a6742dca3651688", size = 649747, upload-time = "2025-06-05T16:13:04.628Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/f6/c82ac1851c60851302d8581680573245c8fc300253fc1ff741ae74a6c24d/greenlet-3.2.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:706d016a03e78df129f68c4c9b4c4f963f7d73534e48a24f5f5a7101ed13dbbb", size = 605461, upload-time = "2025-06-05T16:12:50.792Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/98/82/d022cf25ca39cf1200650fc58c52af32c90f80479c25d1cbf57980ec3065/greenlet-3.2.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:419e60f80709510c343c57b4bb5a339d8767bf9aef9b8ce43f4f143240f88b7c", size = 1121190, upload-time = "2025-06-05T16:36:48.59Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/e1/25297f70717abe8104c20ecf7af0a5b82d2f5a980eb1ac79f65654799f9f/greenlet-3.2.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:93d48533fade144203816783373f27a97e4193177ebaaf0fc396db19e5d61163", size = 1149055, upload-time = "2025-06-05T16:12:40.457Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/8f/8f9e56c5e82eb2c26e8cde787962e66494312dc8cb261c460e1f3a9c88bc/greenlet-3.2.3-cp312-cp312-win_amd64.whl", hash = "sha256:7454d37c740bb27bdeddfc3f358f26956a07d5220818ceb467a483197d84f849", size = 297817, upload-time = "2025-06-05T16:29:49.244Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/cf/f5c0b23309070ae93de75c90d29300751a5aacefc0a3ed1b1d8edb28f08b/greenlet-3.2.3-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:500b8689aa9dd1ab26872a34084503aeddefcb438e2e7317b89b11eaea1901ad", size = 270732, upload-time = "2025-06-05T16:10:08.26Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/48/ae/91a957ba60482d3fecf9be49bc3948f341d706b52ddb9d83a70d42abd498/greenlet-3.2.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a07d3472c2a93117af3b0136f246b2833fdc0b542d4a9799ae5f41c28323faef", size = 639033, upload-time = "2025-06-05T16:38:53.983Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/df/20ffa66dd5a7a7beffa6451bdb7400d66251374ab40b99981478c69a67a8/greenlet-3.2.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:8704b3768d2f51150626962f4b9a9e4a17d2e37c8a8d9867bbd9fa4eb938d3b3", size = 652999, upload-time = "2025-06-05T16:41:37.89Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/51/b4/ebb2c8cb41e521f1d72bf0465f2f9a2fd803f674a88db228887e6847077e/greenlet-3.2.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:5035d77a27b7c62db6cf41cf786cfe2242644a7a337a0e155c80960598baab95", size = 647368, upload-time = "2025-06-05T16:48:21.467Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8e/6a/1e1b5aa10dced4ae876a322155705257748108b7fd2e4fae3f2a091fe81a/greenlet-3.2.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2d8aa5423cd4a396792f6d4580f88bdc6efcb9205891c9d40d20f6e670992efb", size = 650037, upload-time = "2025-06-05T16:13:06.402Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/26/f2/ad51331a157c7015c675702e2d5230c243695c788f8f75feba1af32b3617/greenlet-3.2.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2c724620a101f8170065d7dded3f962a2aea7a7dae133a009cada42847e04a7b", size = 608402, upload-time = "2025-06-05T16:12:51.91Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/26/bc/862bd2083e6b3aff23300900a956f4ea9a4059de337f5c8734346b9b34fc/greenlet-3.2.3-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:873abe55f134c48e1f2a6f53f7d1419192a3d1a4e873bace00499a4e45ea6af0", size = 1119577, upload-time = "2025-06-05T16:36:49.787Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/94/1fc0cc068cfde885170e01de40a619b00eaa8f2916bf3541744730ffb4c3/greenlet-3.2.3-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:024571bbce5f2c1cfff08bf3fbaa43bbc7444f580ae13b0099e95d0e6e67ed36", size = 1147121, upload-time = "2025-06-05T16:12:42.527Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/27/1a/199f9587e8cb08a0658f9c30f3799244307614148ffe8b1e3aa22f324dea/greenlet-3.2.3-cp313-cp313-win_amd64.whl", hash = "sha256:5195fb1e75e592dd04ce79881c8a22becdfa3e6f500e7feb059b1e6fdd54d3e3", size = 297603, upload-time = "2025-06-05T16:20:12.651Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d8/ca/accd7aa5280eb92b70ed9e8f7fd79dc50a2c21d8c73b9a0856f5b564e222/greenlet-3.2.3-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:3d04332dddb10b4a211b68111dabaee2e1a073663d117dc10247b5b1642bac86", size = 271479, upload-time = "2025-06-05T16:10:47.525Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/55/71/01ed9895d9eb49223280ecc98a557585edfa56b3d0e965b9fa9f7f06b6d9/greenlet-3.2.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8186162dffde068a465deab08fc72c767196895c39db26ab1c17c0b77a6d8b97", size = 683952, upload-time = "2025-06-05T16:38:55.125Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ea/61/638c4bdf460c3c678a0a1ef4c200f347dff80719597e53b5edb2fb27ab54/greenlet-3.2.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f4bfbaa6096b1b7a200024784217defedf46a07c2eee1a498e94a1b5f8ec5728", size = 696917, upload-time = "2025-06-05T16:41:38.959Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/22/cc/0bd1a7eb759d1f3e3cc2d1bc0f0b487ad3cc9f34d74da4b80f226fde4ec3/greenlet-3.2.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:ed6cfa9200484d234d8394c70f5492f144b20d4533f69262d530a1a082f6ee9a", size = 692443, upload-time = "2025-06-05T16:48:23.113Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/67/10/b2a4b63d3f08362662e89c103f7fe28894a51ae0bc890fabf37d1d780e52/greenlet-3.2.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:02b0df6f63cd15012bed5401b47829cfd2e97052dc89da3cfaf2c779124eb892", size = 692995, upload-time = "2025-06-05T16:13:07.972Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/c6/ad82f148a4e3ce9564056453a71529732baf5448ad53fc323e37efe34f66/greenlet-3.2.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:86c2d68e87107c1792e2e8d5399acec2487a4e993ab76c792408e59394d52141", size = 655320, upload-time = "2025-06-05T16:12:53.453Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/4f/aab73ecaa6b3086a4c89863d94cf26fa84cbff63f52ce9bc4342b3087a06/greenlet-3.2.3-cp314-cp314-win_amd64.whl", hash = "sha256:8c47aae8fbbfcf82cc13327ae802ba13c9c36753b67e760023fd116bc124a62a", size = 301236, upload-time = "2025-06-05T16:15:20.111Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "griffe"
|
||||
version = "1.7.3"
|
||||
|
@ -1858,6 +1927,56 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/e7/9c/0e6afc12c269578be5c0c1c9f4b49a8d32770a080260c333ac04cc1c832d/soupsieve-2.7-py3-none-any.whl", hash = "sha256:6e60cc5c1ffaf1cebcc12e8188320b72071e922c2e897f737cadce79ad5d30c4", size = 36677, upload-time = "2025-04-20T18:50:07.196Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sqlalchemy"
|
||||
version = "2.0.41"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "greenlet", marker = "(python_full_version < '3.14' and platform_machine == 'AMD64') or (python_full_version < '3.14' and platform_machine == 'WIN32') or (python_full_version < '3.14' and platform_machine == 'aarch64') or (python_full_version < '3.14' and platform_machine == 'amd64') or (python_full_version < '3.14' and platform_machine == 'ppc64le') or (python_full_version < '3.14' and platform_machine == 'win32') or (python_full_version < '3.14' and platform_machine == 'x86_64')" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/63/66/45b165c595ec89aa7dcc2c1cd222ab269bc753f1fc7a1e68f8481bd957bf/sqlalchemy-2.0.41.tar.gz", hash = "sha256:edba70118c4be3c2b1f90754d308d0b79c6fe2c0fdc52d8ddf603916f83f4db9", size = 9689424, upload-time = "2025-05-14T17:10:32.339Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/12/d7c445b1940276a828efce7331cb0cb09d6e5f049651db22f4ebb0922b77/sqlalchemy-2.0.41-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b1f09b6821406ea1f94053f346f28f8215e293344209129a9c0fcc3578598d7b", size = 2117967, upload-time = "2025-05-14T17:48:15.841Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/b8/cb90f23157e28946b27eb01ef401af80a1fab7553762e87df51507eaed61/sqlalchemy-2.0.41-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1936af879e3db023601196a1684d28e12f19ccf93af01bf3280a3262c4b6b4e5", size = 2107583, upload-time = "2025-05-14T17:48:18.688Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/c2/eef84283a1c8164a207d898e063edf193d36a24fb6a5bb3ce0634b92a1e8/sqlalchemy-2.0.41-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2ac41acfc8d965fb0c464eb8f44995770239668956dc4cdf502d1b1ffe0d747", size = 3186025, upload-time = "2025-05-14T17:51:51.226Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bd/72/49d52bd3c5e63a1d458fd6d289a1523a8015adedbddf2c07408ff556e772/sqlalchemy-2.0.41-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:81c24e0c0fde47a9723c81d5806569cddef103aebbf79dbc9fcbb617153dea30", size = 3186259, upload-time = "2025-05-14T17:55:22.526Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4f/9e/e3ffc37d29a3679a50b6bbbba94b115f90e565a2b4545abb17924b94c52d/sqlalchemy-2.0.41-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:23a8825495d8b195c4aa9ff1c430c28f2c821e8c5e2d98089228af887e5d7e29", size = 3126803, upload-time = "2025-05-14T17:51:53.277Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/76/56b21e363f6039978ae0b72690237b38383e4657281285a09456f313dd77/sqlalchemy-2.0.41-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:60c578c45c949f909a4026b7807044e7e564adf793537fc762b2489d522f3d11", size = 3148566, upload-time = "2025-05-14T17:55:24.398Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3b/92/11b8e1b69bf191bc69e300a99badbbb5f2f1102f2b08b39d9eee2e21f565/sqlalchemy-2.0.41-cp310-cp310-win32.whl", hash = "sha256:118c16cd3f1b00c76d69343e38602006c9cfb9998fa4f798606d28d63f23beda", size = 2086696, upload-time = "2025-05-14T17:55:59.136Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/88/2d706c9cc4502654860f4576cd54f7db70487b66c3b619ba98e0be1a4642/sqlalchemy-2.0.41-cp310-cp310-win_amd64.whl", hash = "sha256:7492967c3386df69f80cf67efd665c0f667cee67032090fe01d7d74b0e19bb08", size = 2110200, upload-time = "2025-05-14T17:56:00.757Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/37/4e/b00e3ffae32b74b5180e15d2ab4040531ee1bef4c19755fe7926622dc958/sqlalchemy-2.0.41-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6375cd674fe82d7aa9816d1cb96ec592bac1726c11e0cafbf40eeee9a4516b5f", size = 2121232, upload-time = "2025-05-14T17:48:20.444Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ef/30/6547ebb10875302074a37e1970a5dce7985240665778cfdee2323709f749/sqlalchemy-2.0.41-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9f8c9fdd15a55d9465e590a402f42082705d66b05afc3ffd2d2eb3c6ba919560", size = 2110897, upload-time = "2025-05-14T17:48:21.634Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/21/59df2b41b0f6c62da55cd64798232d7349a9378befa7f1bb18cf1dfd510a/sqlalchemy-2.0.41-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:32f9dc8c44acdee06c8fc6440db9eae8b4af8b01e4b1aee7bdd7241c22edff4f", size = 3273313, upload-time = "2025-05-14T17:51:56.205Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/62/e4/b9a7a0e5c6f79d49bcd6efb6e90d7536dc604dab64582a9dec220dab54b6/sqlalchemy-2.0.41-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90c11ceb9a1f482c752a71f203a81858625d8df5746d787a4786bca4ffdf71c6", size = 3273807, upload-time = "2025-05-14T17:55:26.928Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/39/d8/79f2427251b44ddee18676c04eab038d043cff0e764d2d8bb08261d6135d/sqlalchemy-2.0.41-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:911cc493ebd60de5f285bcae0491a60b4f2a9f0f5c270edd1c4dbaef7a38fc04", size = 3209632, upload-time = "2025-05-14T17:51:59.384Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d4/16/730a82dda30765f63e0454918c982fb7193f6b398b31d63c7c3bd3652ae5/sqlalchemy-2.0.41-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:03968a349db483936c249f4d9cd14ff2c296adfa1290b660ba6516f973139582", size = 3233642, upload-time = "2025-05-14T17:55:29.901Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/04/61/c0d4607f7799efa8b8ea3c49b4621e861c8f5c41fd4b5b636c534fcb7d73/sqlalchemy-2.0.41-cp311-cp311-win32.whl", hash = "sha256:293cd444d82b18da48c9f71cd7005844dbbd06ca19be1ccf6779154439eec0b8", size = 2086475, upload-time = "2025-05-14T17:56:02.095Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9d/8e/8344f8ae1cb6a479d0741c02cd4f666925b2bf02e2468ddaf5ce44111f30/sqlalchemy-2.0.41-cp311-cp311-win_amd64.whl", hash = "sha256:3d3549fc3e40667ec7199033a4e40a2f669898a00a7b18a931d3efb4c7900504", size = 2110903, upload-time = "2025-05-14T17:56:03.499Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/2a/f1f4e068b371154740dd10fb81afb5240d5af4aa0087b88d8b308b5429c2/sqlalchemy-2.0.41-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:81f413674d85cfd0dfcd6512e10e0f33c19c21860342a4890c3a2b59479929f9", size = 2119645, upload-time = "2025-05-14T17:55:24.854Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9b/e8/c664a7e73d36fbfc4730f8cf2bf930444ea87270f2825efbe17bf808b998/sqlalchemy-2.0.41-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:598d9ebc1e796431bbd068e41e4de4dc34312b7aa3292571bb3674a0cb415dd1", size = 2107399, upload-time = "2025-05-14T17:55:28.097Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/78/8a9cf6c5e7135540cb682128d091d6afa1b9e48bd049b0d691bf54114f70/sqlalchemy-2.0.41-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a104c5694dfd2d864a6f91b0956eb5d5883234119cb40010115fd45a16da5e70", size = 3293269, upload-time = "2025-05-14T17:50:38.227Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/35/f74add3978c20de6323fb11cb5162702670cc7a9420033befb43d8d5b7a4/sqlalchemy-2.0.41-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6145afea51ff0af7f2564a05fa95eb46f542919e6523729663a5d285ecb3cf5e", size = 3303364, upload-time = "2025-05-14T17:51:49.829Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/d4/c990f37f52c3f7748ebe98883e2a0f7d038108c2c5a82468d1ff3eec50b7/sqlalchemy-2.0.41-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b46fa6eae1cd1c20e6e6f44e19984d438b6b2d8616d21d783d150df714f44078", size = 3229072, upload-time = "2025-05-14T17:50:39.774Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/15/69/cab11fecc7eb64bc561011be2bd03d065b762d87add52a4ca0aca2e12904/sqlalchemy-2.0.41-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41836fe661cc98abfae476e14ba1906220f92c4e528771a8a3ae6a151242d2ae", size = 3268074, upload-time = "2025-05-14T17:51:51.736Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/ca/0c19ec16858585d37767b167fc9602593f98998a68a798450558239fb04a/sqlalchemy-2.0.41-cp312-cp312-win32.whl", hash = "sha256:a8808d5cf866c781150d36a3c8eb3adccfa41a8105d031bf27e92c251e3969d6", size = 2084514, upload-time = "2025-05-14T17:55:49.915Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7f/23/4c2833d78ff3010a4e17f984c734f52b531a8c9060a50429c9d4b0211be6/sqlalchemy-2.0.41-cp312-cp312-win_amd64.whl", hash = "sha256:5b14e97886199c1f52c14629c11d90c11fbb09e9334fa7bb5f6d068d9ced0ce0", size = 2111557, upload-time = "2025-05-14T17:55:51.349Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d3/ad/2e1c6d4f235a97eeef52d0200d8ddda16f6c4dd70ae5ad88c46963440480/sqlalchemy-2.0.41-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4eeb195cdedaf17aab6b247894ff2734dcead6c08f748e617bfe05bd5a218443", size = 2115491, upload-time = "2025-05-14T17:55:31.177Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cf/8d/be490e5db8400dacc89056f78a52d44b04fbf75e8439569d5b879623a53b/sqlalchemy-2.0.41-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d4ae769b9c1c7757e4ccce94b0641bc203bbdf43ba7a2413ab2523d8d047d8dc", size = 2102827, upload-time = "2025-05-14T17:55:34.921Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a0/72/c97ad430f0b0e78efaf2791342e13ffeafcbb3c06242f01a3bb8fe44f65d/sqlalchemy-2.0.41-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a62448526dd9ed3e3beedc93df9bb6b55a436ed1474db31a2af13b313a70a7e1", size = 3225224, upload-time = "2025-05-14T17:50:41.418Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5e/51/5ba9ea3246ea068630acf35a6ba0d181e99f1af1afd17e159eac7e8bc2b8/sqlalchemy-2.0.41-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc56c9788617b8964ad02e8fcfeed4001c1f8ba91a9e1f31483c0dffb207002a", size = 3230045, upload-time = "2025-05-14T17:51:54.722Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/78/2f/8c14443b2acea700c62f9b4a8bad9e49fc1b65cfb260edead71fd38e9f19/sqlalchemy-2.0.41-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c153265408d18de4cc5ded1941dcd8315894572cddd3c58df5d5b5705b3fa28d", size = 3159357, upload-time = "2025-05-14T17:50:43.483Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fc/b2/43eacbf6ccc5276d76cea18cb7c3d73e294d6fb21f9ff8b4eef9b42bbfd5/sqlalchemy-2.0.41-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4f67766965996e63bb46cfbf2ce5355fc32d9dd3b8ad7e536a920ff9ee422e23", size = 3197511, upload-time = "2025-05-14T17:51:57.308Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/2e/677c17c5d6a004c3c45334ab1dbe7b7deb834430b282b8a0f75ae220c8eb/sqlalchemy-2.0.41-cp313-cp313-win32.whl", hash = "sha256:bfc9064f6658a3d1cadeaa0ba07570b83ce6801a1314985bf98ec9b95d74e15f", size = 2082420, upload-time = "2025-05-14T17:55:52.69Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/61/e8c1b9b6307c57157d328dd8b8348ddc4c47ffdf1279365a13b2b98b8049/sqlalchemy-2.0.41-cp313-cp313-win_amd64.whl", hash = "sha256:82ca366a844eb551daff9d2e6e7a9e5e76d2612c8564f58db6c19a726869c1df", size = 2108329, upload-time = "2025-05-14T17:55:54.495Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1c/fc/9ba22f01b5cdacc8f5ed0d22304718d2c758fce3fd49a5372b886a86f37c/sqlalchemy-2.0.41-py3-none-any.whl", hash = "sha256:57df5dc6fdb5ed1a88a1ed2195fd31927e705cad62dedd86b46972752a80f576", size = 1911224, upload-time = "2025-05-14T17:39:42.154Z" },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
asyncio = [
|
||||
{ name = "greenlet" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tomli"
|
||||
version = "2.2.1"
|
||||
|
|
Ładowanie…
Reference in New Issue