From 28a8c0e4fc5ab7dcf9a47d7d675f6123c86115e5 Mon Sep 17 00:00:00 2001 From: Christophe Bornet Date: Tue, 5 Nov 2024 23:51:03 +0100 Subject: [PATCH] Add AsyncSession support for non-blocking db operations --- pyproject.toml | 3 +- src/backend/base/langflow/__main__.py | 50 ++++++----- src/backend/base/langflow/api/utils.py | 4 +- src/backend/base/langflow/api/v1/api_key.py | 14 +-- .../services/database/models/api_key/crud.py | 19 ++-- .../langflow/services/database/service.py | 82 +++++++++++------ src/backend/base/langflow/services/deps.py | 42 ++++++++- uv.lock | 89 ++++++++++++++++++- 8 files changed, 234 insertions(+), 69 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 315f20ea5ee1..78befd07c159 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -49,7 +49,7 @@ dependencies = [ "pymongo>=4.6.0", "supabase~=2.6.0", "certifi>=2023.11.17,<2025.0.0", - "psycopg>=3.1.9", + "psycopg[binary,pool]>=3.1.9", "fastavro>=1.8.0", "redis>=5.0.1", "metaphor-python>=0.1.11", @@ -110,6 +110,7 @@ dependencies = [ "langchain-google-community~=2.0.1", "langchain-elasticsearch>=0.2.0", "langchain-ollama>=0.2.0", + "aiosqlite>=0.20.0", ] [project.urls] diff --git a/src/backend/base/langflow/__main__.py b/src/backend/base/langflow/__main__.py index 1e668dee18aa..330c1c671504 100644 --- a/src/backend/base/langflow/__main__.py +++ b/src/backend/base/langflow/__main__.py @@ -1,3 +1,4 @@ +import asyncio import inspect import platform import socket @@ -27,7 +28,7 @@ create_default_folder_if_it_doesnt_exist, ) from langflow.services.database.utils import session_getter -from langflow.services.deps import get_db_service, get_settings_service, session_scope +from langflow.services.deps import async_session_scope, get_db_service, get_settings_service from langflow.services.settings.constants import DEFAULT_SUPERUSER from langflow.services.utils import initialize_services from langflow.utils.version import fetch_latest_version, get_version_info @@ -486,28 +487,35 @@ def api_key( if not auth_settings.AUTO_LOGIN: typer.echo("Auto login is disabled. API keys cannot be created through the CLI.") return - with session_scope() as session: - from langflow.services.database.models.user.model import User - - superuser = session.exec(select(User).where(User.username == DEFAULT_SUPERUSER)).first() - if not superuser: - typer.echo("Default superuser not found. This command requires a superuser and AUTO_LOGIN to be enabled.") - return - from langflow.services.database.models.api_key import ApiKey, ApiKeyCreate - from langflow.services.database.models.api_key.crud import ( - create_api_key, - delete_api_key, - ) - api_key = session.exec(select(ApiKey).where(ApiKey.user_id == superuser.id)).first() - if api_key: - delete_api_key(session, api_key.id) + async def aapi_key(): + async with async_session_scope() as session: + from langflow.services.database.models.user.model import User - api_key_create = ApiKeyCreate(name="CLI") - unmasked_api_key = create_api_key(session, api_key_create, user_id=superuser.id) - session.commit() - # Create a banner to display the API key and tell the user it won't be shown again - api_key_banner(unmasked_api_key) + superuser = (await session.exec(select(User).where(User.username == DEFAULT_SUPERUSER))).first() + if not superuser: + typer.echo( + "Default superuser not found. This command requires a superuser and AUTO_LOGIN to be enabled." + ) + return None + from langflow.services.database.models.api_key import ApiKey, ApiKeyCreate + from langflow.services.database.models.api_key.crud import ( + create_api_key, + delete_api_key, + ) + + api_key = (await session.exec(select(ApiKey).where(ApiKey.user_id == superuser.id))).first() + if api_key: + await delete_api_key(session, api_key.id) + + api_key_create = ApiKeyCreate(name="CLI") + unmasked_api_key = await create_api_key(session, api_key_create, user_id=superuser.id) + await session.commit() + return unmasked_api_key + + unmasked_api_key = asyncio.run(aapi_key()) + # Create a banner to display the API key and tell the user it won't be shown again + api_key_banner(unmasked_api_key) def api_key_banner(unmasked_api_key) -> None: diff --git a/src/backend/base/langflow/api/utils.py b/src/backend/base/langflow/api/utils.py index 97f802f1c5ef..22efc62b8145 100644 --- a/src/backend/base/langflow/api/utils.py +++ b/src/backend/base/langflow/api/utils.py @@ -9,6 +9,7 @@ from loguru import logger from sqlalchemy import delete from sqlmodel import Session +from sqlmodel.ext.asyncio.session import AsyncSession from langflow.graph.graph.base import Graph from langflow.services.auth.utils import get_current_active_user @@ -16,7 +17,7 @@ from langflow.services.database.models.flow import Flow from langflow.services.database.models.transactions.model import TransactionTable from langflow.services.database.models.vertex_builds.model import VertexBuildTable -from langflow.services.deps import get_session +from langflow.services.deps import get_async_session, get_session from langflow.services.store.utils import get_lf_version_from_pypi if TYPE_CHECKING: @@ -31,6 +32,7 @@ CurrentActiveUser = Annotated[User, Depends(get_current_active_user)] DbSession = Annotated[Session, Depends(get_session)] +AsyncDbSession = Annotated[AsyncSession, Depends(get_async_session)] def has_api_terms(word: str): diff --git a/src/backend/base/langflow/api/v1/api_key.py b/src/backend/base/langflow/api/v1/api_key.py index c5d78c78a876..09b542fd09d7 100644 --- a/src/backend/base/langflow/api/v1/api_key.py +++ b/src/backend/base/langflow/api/v1/api_key.py @@ -3,7 +3,7 @@ from fastapi import APIRouter, Depends, HTTPException, Response -from langflow.api.utils import CurrentActiveUser, DbSession +from langflow.api.utils import AsyncDbSession, CurrentActiveUser, DbSession from langflow.api.v1.schemas import ApiKeyCreateRequest, ApiKeysResponse from langflow.services.auth import utils as auth_utils @@ -20,12 +20,12 @@ @router.get("/") async def get_api_keys_route( - db: DbSession, + db: AsyncDbSession, current_user: CurrentActiveUser, ) -> ApiKeysResponse: try: user_id = current_user.id - keys = get_api_keys(db, user_id) + keys = await get_api_keys(db, user_id) return ApiKeysResponse(total_count=len(keys), user_id=user_id, api_keys=keys) except Exception as exc: @@ -36,11 +36,11 @@ async def get_api_keys_route( async def create_api_key_route( req: ApiKeyCreate, current_user: CurrentActiveUser, - db: DbSession, + db: AsyncDbSession, ) -> UnmaskedApiKeyRead: try: user_id = current_user.id - return create_api_key(db, req, user_id=user_id) + return await create_api_key(db, req, user_id=user_id) except Exception as e: raise HTTPException(status_code=400, detail=str(e)) from e @@ -48,10 +48,10 @@ async def create_api_key_route( @router.delete("/{api_key_id}", dependencies=[Depends(auth_utils.get_current_active_user)]) async def delete_api_key_route( api_key_id: UUID, - db: DbSession, + db: AsyncDbSession, ): try: - delete_api_key(db, api_key_id) + await delete_api_key(db, api_key_id) except Exception as e: raise HTTPException(status_code=400, detail=str(e)) from e return {"detail": "API Key deleted"} diff --git a/src/backend/base/langflow/services/database/models/api_key/crud.py b/src/backend/base/langflow/services/database/models/api_key/crud.py index 389e2a81ba66..faa210deaf66 100644 --- a/src/backend/base/langflow/services/database/models/api_key/crud.py +++ b/src/backend/base/langflow/services/database/models/api_key/crud.py @@ -5,6 +5,7 @@ from uuid import UUID from sqlmodel import Session, select +from sqlmodel.ext.asyncio.session import AsyncSession from langflow.services.database.models.api_key import ApiKey, ApiKeyCreate, ApiKeyRead, UnmaskedApiKeyRead @@ -12,13 +13,13 @@ from sqlmodel.sql.expression import SelectOfScalar -def get_api_keys(session: Session, user_id: UUID) -> list[ApiKeyRead]: +async def get_api_keys(session: AsyncSession, user_id: UUID) -> list[ApiKeyRead]: query: SelectOfScalar = select(ApiKey).where(ApiKey.user_id == user_id) - api_keys = session.exec(query).all() + api_keys = (await session.exec(query)).all() return [ApiKeyRead.model_validate(api_key) for api_key in api_keys] -def create_api_key(session: Session, api_key_create: ApiKeyCreate, user_id: UUID) -> UnmaskedApiKeyRead: +async def create_api_key(session: AsyncSession, api_key_create: ApiKeyCreate, user_id: UUID) -> UnmaskedApiKeyRead: # Generate a random API key with 32 bytes of randomness generated_api_key = f"sk-{secrets.token_urlsafe(32)}" @@ -30,20 +31,20 @@ def create_api_key(session: Session, api_key_create: ApiKeyCreate, user_id: UUID ) session.add(api_key) - session.commit() - session.refresh(api_key) + await session.commit() + await session.refresh(api_key) unmasked = UnmaskedApiKeyRead.model_validate(api_key, from_attributes=True) unmasked.api_key = generated_api_key return unmasked -def delete_api_key(session: Session, api_key_id: UUID) -> None: - api_key = session.get(ApiKey, api_key_id) +async def delete_api_key(session: AsyncSession, api_key_id: UUID) -> None: + api_key = await session.get(ApiKey, api_key_id) if api_key is None: msg = "API Key not found" raise ValueError(msg) - session.delete(api_key) - session.commit() + await session.delete(api_key) + await session.commit() def check_key(session: Session, api_key: str) -> ApiKey | None: diff --git a/src/backend/base/langflow/services/database/service.py b/src/backend/base/langflow/services/database/service.py index b5059eb79507..22d05f2e307d 100644 --- a/src/backend/base/langflow/services/database/service.py +++ b/src/backend/base/langflow/services/database/service.py @@ -1,8 +1,9 @@ from __future__ import annotations import asyncio +import sqlite3 import time -from contextlib import contextmanager +from contextlib import asynccontextmanager, contextmanager from datetime import datetime, timezone from pathlib import Path from typing import TYPE_CHECKING @@ -14,7 +15,9 @@ from sqlalchemy import event, inspect from sqlalchemy.engine import Engine from sqlalchemy.exc import OperationalError +from sqlalchemy.ext.asyncio import AsyncEngine, create_async_engine from sqlmodel import Session, SQLModel, create_engine, select, text +from sqlmodel.ext.asyncio.session import AsyncSession from langflow.services.base import Service from langflow.services.database import models @@ -39,12 +42,17 @@ def __init__(self, settings_service: SettingsService): msg = "No database URL provided" raise ValueError(msg) self.database_url: str = settings_service.settings.database_url + self._sanitize_database_url() # This file is in langflow.services.database.manager.py # the ini is in langflow langflow_dir = Path(__file__).parent.parent.parent self.script_location = langflow_dir / "alembic" self.alembic_cfg_path = langflow_dir / "alembic.ini" + # register the event listener for sqlite as part of this class. + # Using decorator will make the method not able to use self + event.listen(Engine, "connect", self.on_connection) self.engine = self._create_engine() + self.async_engine = self._create_async_engine() alembic_log_file = self.settings_service.settings.alembic_log_file # Check if the provided path is absolute, cross-platform. @@ -56,10 +64,47 @@ def __init__(self, settings_service: SettingsService): self.alembic_log_path = Path(langflow_dir) / alembic_log_file def reload_engine(self) -> None: + self._sanitize_database_url() self.engine = self._create_engine() + self.async_engine = self._create_async_engine() + + def _sanitize_database_url(self): + if self.database_url.startswith("postgres://"): + self.database_url = self.database_url.replace("postgres://", "postgresql://") + logger.warning( + "Fixed postgres dialect in database URL. Replacing postgres:// with postgresql://. " + "To avoid this warning, update the database URL." + ) def _create_engine(self) -> Engine: """Create the engine for the database.""" + return create_engine( + self.database_url, + connect_args=self._get_connect_args(), + pool_size=self.settings_service.settings.pool_size, + max_overflow=self.settings_service.settings.max_overflow, + ) + + def _create_async_engine(self) -> AsyncEngine: + """Create the engine for the database.""" + url_components = self.database_url.split("://", maxsplit=1) + if url_components[0].startswith("sqlite"): + database_url = "sqlite+aiosqlite://" + kwargs = {} + else: + kwargs = { + "pool_size": self.settings_service.settings.pool_size, + "max_overflow": self.settings_service.settings.max_overflow, + } + database_url = "postgresql+psycopg://" if url_components[0].startswith("postgresql") else url_components[0] + database_url += url_components[1] + return create_async_engine( + database_url, + connect_args=self._get_connect_args(), + **kwargs, + ) + + def _get_connect_args(self): if self.settings_service.settings.database_url and self.settings_service.settings.database_url.startswith( "sqlite" ): @@ -69,33 +114,12 @@ def _create_engine(self) -> Engine: } else: connect_args = {} - try: - # register the event listener for sqlite as part of this class. - # Using decorator will make the method not able to use self - event.listen(Engine, "connect", self.on_connection) - - return create_engine( - self.database_url, - connect_args=connect_args, - pool_size=self.settings_service.settings.pool_size, - max_overflow=self.settings_service.settings.max_overflow, - ) - except sa.exc.NoSuchModuleError as exc: - if "postgres" in str(exc) and not self.database_url.startswith("postgresql"): - # https://stackoverflow.com/questions/62688256/sqlalchemy-exc-nosuchmoduleerror-cant-load-plugin-sqlalchemy-dialectspostgre - self.database_url = self.database_url.replace("postgres://", "postgresql://") - logger.warning( - "Fixed postgres dialect in database URL. Replacing postgres:// with postgresql://. " - "To avoid this warning, update the database URL." - ) - return self._create_engine() - msg = "Error creating database engine" - raise RuntimeError(msg) from exc + return connect_args def on_connection(self, dbapi_connection, _connection_record) -> None: - from sqlite3 import Connection as sqliteConnection - - if isinstance(dbapi_connection, sqliteConnection): + if isinstance( + dbapi_connection, sqlite3.Connection | sa.dialects.sqlite.aiosqlite.AsyncAdapt_aiosqlite_connection + ): pragmas: dict = self.settings_service.settings.sqlite_pragmas or {} pragmas_list = [] for key, val in pragmas.items(): @@ -117,6 +141,11 @@ def with_session(self): with Session(self.engine) as session: yield session + @asynccontextmanager + async def with_async_session(self): + async with AsyncSession(self.async_engine) as session: + yield session + def migrate_flows_if_auto_login(self) -> None: # if auto_login is enabled, we need to migrate the flows # to the default superuser if they don't have a user id @@ -334,3 +363,4 @@ def _teardown(self) -> None: async def teardown(self) -> None: await asyncio.to_thread(self._teardown) + await self.async_engine.dispose() diff --git a/src/backend/base/langflow/services/deps.py b/src/backend/base/langflow/services/deps.py index 40152a0175bc..0a7291199e67 100644 --- a/src/backend/base/langflow/services/deps.py +++ b/src/backend/base/langflow/services/deps.py @@ -1,6 +1,6 @@ from __future__ import annotations -from contextlib import contextmanager +from contextlib import asynccontextmanager, contextmanager from typing import TYPE_CHECKING from loguru import logger @@ -8,9 +8,10 @@ from langflow.services.schema import ServiceType if TYPE_CHECKING: - from collections.abc import Generator + from collections.abc import AsyncGenerator, Generator from sqlmodel import Session + from sqlmodel.ext.asyncio.session import AsyncSession from langflow.services.cache.service import AsyncBaseCacheService, CacheService from langflow.services.chat.service import ChatService @@ -162,6 +163,17 @@ def get_session() -> Generator[Session, None, None]: yield session +async def get_async_session() -> AsyncGenerator[AsyncSession, None]: + """Retrieves an async session from the database service. + + Yields: + Session: An async session object. + + """ + async with get_db_service().with_async_session() as session: + yield session + + @contextmanager def session_scope() -> Generator[Session, None, None]: """Context manager for managing a session scope. @@ -188,6 +200,32 @@ def session_scope() -> Generator[Session, None, None]: raise +@asynccontextmanager +async def async_session_scope() -> AsyncGenerator[AsyncSession, None]: + """Context manager for managing an async session scope. + + This context manager is used to manage an async session scope for database operations. + It ensures that the session is properly committed if no exceptions occur, + and rolled back if an exception is raised. + + Yields: + session: The async session object. + + Raises: + Exception: If an error occurs during the session scope. + + """ + db_service = get_db_service() + async with db_service.with_async_session() as session: + try: + yield session + await session.commit() + except Exception: + logger.exception("An error occurred during the session scope.") + await session.rollback() + raise + + def get_cache_service() -> CacheService | AsyncBaseCacheService: """Retrieves the cache service from the service manager. diff --git a/uv.lock b/uv.lock index 884b5bfdc07d..56ca1e8f18b4 100644 --- a/uv.lock +++ b/uv.lock @@ -104,6 +104,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/76/ac/a7305707cb852b7e16ff80eaf5692309bde30e2b1100a1fcacdc8f731d97/aiosignal-1.3.1-py3-none-any.whl", hash = "sha256:f8376fb07dd1e86a584e4fcdec80b36b7f81aac666ebc724e2c090300dd83b17", size = 7617 }, ] +[[package]] +name = "aiosqlite" +version = "0.20.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0d/3a/22ff5415bf4d296c1e92b07fd746ad42c96781f13295a074d58e77747848/aiosqlite-0.20.0.tar.gz", hash = "sha256:6d35c8c256637f4672f843c31021464090805bf925385ac39473fb16eaaca3d7", size = 21691 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/c4/c93eb22025a2de6b83263dfe3d7df2e19138e345bca6f18dba7394120930/aiosqlite-0.20.0-py3-none-any.whl", hash = "sha256:36a1deaca0cac40ebe32aac9977a6e2bbc7f5189f23f4a54d5908986729e5bd6", size = 15564 }, +] + [[package]] name = "alembic" version = "1.13.3" @@ -3573,6 +3585,7 @@ name = "langflow" version = "1.0.19" source = { editable = "." } dependencies = [ + { name = "aiosqlite" }, { name = "assemblyai" }, { name = "astra-assistants", extra = ["tools"] }, { name = "beautifulsoup4" }, @@ -3630,7 +3643,7 @@ dependencies = [ { name = "numexpr" }, { name = "opensearch-py" }, { name = "pgvector" }, - { name = "psycopg" }, + { name = "psycopg", extra = ["binary", "pool"] }, { name = "psycopg2-binary" }, { name = "pyarrow" }, { name = "pyautogen" }, @@ -3714,6 +3727,7 @@ dev = [ [package.metadata] requires-dist = [ + { name = "aiosqlite", specifier = ">=0.20.0" }, { name = "assemblyai", specifier = ">=0.33.0" }, { name = "astra-assistants", extras = ["tools"], specifier = "~=2.2.6" }, { name = "beautifulsoup4", specifier = ">=4.12.2" }, @@ -3778,7 +3792,7 @@ requires-dist = [ { name = "numexpr", specifier = ">=2.8.6" }, { name = "opensearch-py", specifier = ">=2.7.1" }, { name = "pgvector", specifier = ">=0.2.3" }, - { name = "psycopg", specifier = ">=3.1.9" }, + { name = "psycopg", extras = ["binary", "pool"], specifier = ">=3.1.9" }, { name = "psycopg2-binary", specifier = ">=2.9.6" }, { name = "pyarrow", specifier = ">=14.0.0" }, { name = "pyautogen", specifier = ">=0.2.0" }, @@ -5777,6 +5791,77 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ce/21/534b8f5bd9734b7a2fcd3a16b1ee82ef6cad81a4796e95ebf4e0c6a24119/psycopg-3.2.3-py3-none-any.whl", hash = "sha256:644d3973fe26908c73d4be746074f6e5224b03c1101d302d9a53bf565ad64907", size = 197934 }, ] +[package.optional-dependencies] +binary = [ + { name = "psycopg-binary", marker = "implementation_name != 'pypy'" }, +] +pool = [ + { name = "psycopg-pool" }, +] + +[[package]] +name = "psycopg-binary" +version = "3.2.3" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/1c/1fc9d53844c15059b98b27d7037a8af87e43832e367c88c8ee43b8bb650f/psycopg_binary-3.2.3-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:965455eac8547f32b3181d5ec9ad8b9be500c10fe06193543efaaebe3e4ce70c", size = 3383146 }, + { url = "https://files.pythonhosted.org/packages/fb/80/0d0eca43756578738a14f747b3d27e8e22ba468765071eaf61cd517c52a3/psycopg_binary-3.2.3-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:71adcc8bc80a65b776510bc39992edf942ace35b153ed7a9c6c573a6849ce308", size = 3504185 }, + { url = "https://files.pythonhosted.org/packages/7c/02/1db86752a2a663cf59d410374e9aced220d1a883a64b7256ed1171685a27/psycopg_binary-3.2.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f73adc05452fb85e7a12ed3f69c81540a8875960739082e6ea5e28c373a30774", size = 4469268 }, + { url = "https://files.pythonhosted.org/packages/59/04/b8cbc84f494247fa887dcc5cba15f99d261dc44b94fbb10fdaa44c4d6dac/psycopg_binary-3.2.3-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e8630943143c6d6ca9aefc88bbe5e76c90553f4e1a3b2dc339e67dc34aa86f7e", size = 4270625 }, + { url = "https://files.pythonhosted.org/packages/74/94/851a58aeab1e2aa30a564133f84229242b2fc774eabb3fc5c164b2423dcd/psycopg_binary-3.2.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3bffb61e198a91f712cc3d7f2d176a697cb05b284b2ad150fb8edb308eba9002", size = 4515573 }, + { url = "https://files.pythonhosted.org/packages/5a/95/e3e600687e59df7d5214e81d9aa2d324f2c5dece32068d66b03a4fd6edf6/psycopg_binary-3.2.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc4fa2240c9fceddaa815a58f29212826fafe43ce80ff666d38c4a03fb036955", size = 4214078 }, + { url = "https://files.pythonhosted.org/packages/2e/1e/4b50e1a2c35a7ee1fc65f8a5fed36026c16b05c9549dc4247914dfbfa2f5/psycopg_binary-3.2.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:192a5f8496e6e1243fdd9ac20e117e667c0712f148c5f9343483b84435854c78", size = 3139319 }, + { url = "https://files.pythonhosted.org/packages/a0/bb/fc88304a7b759d87ad79f538f1b605c23802f36963d207b6e8e9062a57bd/psycopg_binary-3.2.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:64dc6e9ec64f592f19dc01a784e87267a64a743d34f68488924251253da3c818", size = 3118977 }, + { url = "https://files.pythonhosted.org/packages/92/19/88e14b615291b472b616bb3078206eac63dd6cb806c79b12119b7c39e519/psycopg_binary-3.2.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:79498df398970abcee3d326edd1d4655de7d77aa9aecd578154f8af35ce7bbd2", size = 3224533 }, + { url = "https://files.pythonhosted.org/packages/98/cd/6cedff641f1ffb7008b6c511233814d2934df8caf2ec93c50412c37e5f91/psycopg_binary-3.2.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:949551752930d5e478817e0b49956350d866b26578ced0042a61967e3fcccdea", size = 3258089 }, + { url = "https://files.pythonhosted.org/packages/31/2c/8059fbcd513d4b7c9e25dd93c438ab174e8ce389b85d8432b4ce3c0e8958/psycopg_binary-3.2.3-cp310-cp310-win_amd64.whl", hash = "sha256:80a2337e2dfb26950894c8301358961430a0304f7bfe729d34cc036474e9c9b1", size = 2921689 }, + { url = "https://files.pythonhosted.org/packages/3d/78/8e8b4063b5cd1cc91cc100fc3e9296b96f52c9a709750b24ade6cfa8021b/psycopg_binary-3.2.3-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:6d8f2144e0d5808c2e2aed40fbebe13869cd00c2ae745aca4b3b16a435edb056", size = 3391535 }, + { url = "https://files.pythonhosted.org/packages/36/7f/04eed0c415d158a0fb1c196957b9c7faec43c7b50d20db05c62e5bd22c93/psycopg_binary-3.2.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:94253be2b57ef2fea7ffe08996067aabf56a1eb9648342c9e3bad9e10c46e045", size = 3509175 }, + { url = "https://files.pythonhosted.org/packages/0d/91/042fe504220a6e1a423e6a26d24f198da976b9cce11bc9ab7e9415bac08f/psycopg_binary-3.2.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fda0162b0dbfa5eaed6cdc708179fa27e148cb8490c7d62e5cf30713909658ea", size = 4465647 }, + { url = "https://files.pythonhosted.org/packages/35/7c/4cf02ee263431b306453b7b086ec8e91dcbd5008382d711e82afa829f73e/psycopg_binary-3.2.3-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2c0419cdad8c70eaeb3116bb28e7b42d546f91baf5179d7556f230d40942dc78", size = 4267051 }, + { url = "https://files.pythonhosted.org/packages/f5/9b/cea713d8d75621481ece2dfc7edae6e4f05dfbcaab28fac0dbff9b96fc3a/psycopg_binary-3.2.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:74fbf5dd3ef09beafd3557631e282f00f8af4e7a78fbfce8ab06d9cd5a789aae", size = 4517398 }, + { url = "https://files.pythonhosted.org/packages/56/65/cd4165c45359f4117147b861c16c7b85afbd93cc9efac6116b13f62bc725/psycopg_binary-3.2.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d784f614e4d53050cbe8abf2ae9d1aaacf8ed31ce57b42ce3bf2a48a66c3a5c", size = 4210644 }, + { url = "https://files.pythonhosted.org/packages/f3/80/14e7bf67613c4344e74fe6ac5c9876a7acb4ddc15e5455c54e24cdc087f8/psycopg_binary-3.2.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4e76ce2475ed4885fe13b8254058be710ec0de74ebd8ef8224cf44a9a3358e5f", size = 3138032 }, + { url = "https://files.pythonhosted.org/packages/7e/81/e18c36de78e0f7a491a754dc74c1bb6b16469d8c240b2add1e856801d567/psycopg_binary-3.2.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5938b257b04c851c2d1e6cb2f8c18318f06017f35be9a5fe761ee1e2e344dfb7", size = 3114329 }, + { url = "https://files.pythonhosted.org/packages/48/39/07b0bf8355cb535ccdd58261a18fb6e786e175492363f5255b446fff6427/psycopg_binary-3.2.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:257c4aea6f70a9aef39b2a77d0658a41bf05c243e2bf41895eb02220ac6306f3", size = 3219579 }, + { url = "https://files.pythonhosted.org/packages/64/ea/92c700989b5bdeb8e8e59732191547e32da732692d6c016830c82f9b4ac7/psycopg_binary-3.2.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:06b5cc915e57621eebf2393f4173793ed7e3387295f07fed93ed3fb6a6ccf585", size = 3257145 }, + { url = "https://files.pythonhosted.org/packages/84/49/39f0875fd32a6d77cd22b44887df39eb470039b389c388cee4ba75c0bda7/psycopg_binary-3.2.3-cp311-cp311-win_amd64.whl", hash = "sha256:09baa041856b35598d335b1a74e19a49da8500acedf78164600694c0ba8ce21b", size = 2924948 }, + { url = "https://files.pythonhosted.org/packages/55/6b/9805a5c743c1d54dcd035bd5c069202fde21b4cf69857ca40c2a55e69f8c/psycopg_binary-3.2.3-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:48f8ca6ee8939bab760225b2ab82934d54330eec10afe4394a92d3f2a0c37dd6", size = 3363376 }, + { url = "https://files.pythonhosted.org/packages/a8/82/45ac156b20e08e8f556a323c9568a011c71cf6e734e49667a398719ce0e4/psycopg_binary-3.2.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:5361ea13c241d4f0ec3f95e0bf976c15e2e451e9cc7ef2e5ccfc9d170b197a40", size = 3506449 }, + { url = "https://files.pythonhosted.org/packages/e4/be/760cef50e1adfbc87dab2b05b30f544d7297040cce495835df9016556517/psycopg_binary-3.2.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb987f14af7da7c24f803111dbc7392f5070fd350146af3345103f76ea82e339", size = 4445757 }, + { url = "https://files.pythonhosted.org/packages/b4/9c/bae6a9c6949aac577cc93f58705f649b50c62827038903bd75ff8956e63e/psycopg_binary-3.2.3-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0463a11b1cace5a6aeffaf167920707b912b8986a9c7920341c75e3686277920", size = 4248376 }, + { url = "https://files.pythonhosted.org/packages/e5/0e/9db06ef94e4a156f3ed06043ee4f370e21866b0e3b7959691c8c4abfb698/psycopg_binary-3.2.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8b7be9a6c06518967b641fb15032b1ed682fd3b0443f64078899c61034a0bca6", size = 4487765 }, + { url = "https://files.pythonhosted.org/packages/9f/5f/8afc32b60ee8bc5c4af51e7cf6c42d93a989a09609524d0a393106e300cd/psycopg_binary-3.2.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64a607e630d9f4b2797f641884e52b9f8e239d35943f51bef817a384ec1678fe", size = 4188374 }, + { url = "https://files.pythonhosted.org/packages/ed/5d/210cb75aff0296dc5c09bcf67babf8679905412d7a11357b983f0d877360/psycopg_binary-3.2.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:fa33ead69ed133210d96af0c63448b1385df48b9c0247eda735c5896b9e6dbbf", size = 3113180 }, + { url = "https://files.pythonhosted.org/packages/40/ec/46b1a5cdb2fe995b8ec0376f0695003e97fed9ac077e090a3165ea15f735/psycopg_binary-3.2.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:1f8b0d0e99d8e19923e6e07379fa00570be5182c201a8c0b5aaa9a4d4a4ea20b", size = 3099455 }, + { url = "https://files.pythonhosted.org/packages/11/68/eaf85b3421b3f01b638dd6b16f4e9bc8de42eb1d000da62964fb29f8c823/psycopg_binary-3.2.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:709447bd7203b0b2debab1acec23123eb80b386f6c29e7604a5d4326a11e5bd6", size = 3189977 }, + { url = "https://files.pythonhosted.org/packages/83/5a/cf94c3ba87ea6c8331aa0aba36a18a837a3231764457780661968804673e/psycopg_binary-3.2.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5e37d5027e297a627da3551a1e962316d0f88ee4ada74c768f6c9234e26346d9", size = 3232263 }, + { url = "https://files.pythonhosted.org/packages/0e/3a/9d912b16059e87b04e3eb4fca457f079d78d6468f627d5622fbda80e9378/psycopg_binary-3.2.3-cp312-cp312-win_amd64.whl", hash = "sha256:261f0031ee6074765096a19b27ed0f75498a8338c3dcd7f4f0d831e38adf12d1", size = 2912530 }, + { url = "https://files.pythonhosted.org/packages/c6/bf/717c5e51c68e2498b60a6e9f1476cc47953013275a54bf8e23fd5082a72d/psycopg_binary-3.2.3-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:41fdec0182efac66b27478ac15ef54c9ebcecf0e26ed467eb7d6f262a913318b", size = 3360874 }, + { url = "https://files.pythonhosted.org/packages/31/d5/6f9ad6fe5ef80ca9172bc3d028ebae8e9a1ee8aebd917c95c747a5efd85f/psycopg_binary-3.2.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:07d019a786eb020c0f984691aa1b994cb79430061065a694cf6f94056c603d26", size = 3502320 }, + { url = "https://files.pythonhosted.org/packages/fb/7b/c58dd26c27fe7a491141ca765c103e702872ff1c174ebd669d73d7fb0b5d/psycopg_binary-3.2.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4c57615791a337378fe5381143259a6c432cdcbb1d3e6428bfb7ce59fff3fb5c", size = 4446950 }, + { url = "https://files.pythonhosted.org/packages/ed/75/acf6a81c788007b7bc0a43b02c22eff7cb19a6ace9e84c32838e86083a3f/psycopg_binary-3.2.3-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e8eb9a4e394926b93ad919cad1b0a918e9b4c846609e8c1cfb6b743683f64da0", size = 4252409 }, + { url = "https://files.pythonhosted.org/packages/83/a5/8a01b923fe42acd185d53f24fb98ead717725ede76a4cd183ff293daf1f1/psycopg_binary-3.2.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5905729668ef1418bd36fbe876322dcb0f90b46811bba96d505af89e6fbdce2f", size = 4488121 }, + { url = "https://files.pythonhosted.org/packages/14/8f/b00e65e204340ab1259ecc8d4cc4c1f72c386be5ca7bfb90ae898a058d68/psycopg_binary-3.2.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd65774ed7d65101b314808b6893e1a75b7664f680c3ef18d2e5c84d570fa393", size = 4190653 }, + { url = "https://files.pythonhosted.org/packages/ce/fc/ba830fc6c9b02b66d1e2fb420736df4d78369760144169a9046f04d72ac6/psycopg_binary-3.2.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:700679c02f9348a0d0a2adcd33a0275717cd0d0aee9d4482b47d935023629505", size = 3118074 }, + { url = "https://files.pythonhosted.org/packages/b8/75/b62d06930a615435e909e05de126aa3d49f6ec2993d1aa6a99e7faab5570/psycopg_binary-3.2.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:96334bb64d054e36fed346c50c4190bad9d7c586376204f50bede21a913bf942", size = 3100457 }, + { url = "https://files.pythonhosted.org/packages/57/e5/32dc7518325d0010813853a87b19c784d8b11fdb17f5c0e0c148c5ac77af/psycopg_binary-3.2.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:9099e443d4cc24ac6872e6a05f93205ba1a231b1a8917317b07c9ef2b955f1f4", size = 3192788 }, + { url = "https://files.pythonhosted.org/packages/23/a3/d1aa04329253c024a2323051774446770d47b43073874a3de8cca797ed8e/psycopg_binary-3.2.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1985ab05e9abebfbdf3163a16ebb37fbc5d49aff2bf5b3d7375ff0920bbb54cd", size = 3234247 }, + { url = "https://files.pythonhosted.org/packages/03/20/b675af723b9a61d48abd6a3d64cbb9797697d330255d1f8105713d54ed8e/psycopg_binary-3.2.3-cp313-cp313-win_amd64.whl", hash = "sha256:e90352d7b610b4693fad0feea48549d4315d10f1eba5605421c92bb834e90170", size = 2913413 }, +] + +[[package]] +name = "psycopg-pool" +version = "3.2.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ad/a6/efa7fa8204d2dedf5f1b53b24154c8da0291788e99b7fb544fcaf4a3a29c/psycopg_pool-3.2.3.tar.gz", hash = "sha256:bb942f123bef4b7fbe4d55421bd3fb01829903c95c0f33fd42b7e94e5ac9b52a", size = 29661 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/30/7bc6c070ef1135a44005fe39dd749a6bde41c5770fed7349b544f277fa78/psycopg_pool-3.2.3-py3-none-any.whl", hash = "sha256:53bd8e640625e01b2927b2ad96df8ed8e8f91caea4597d45e7673fc7bbb85eb1", size = 38141 }, +] + [[package]] name = "psycopg2-binary" version = "2.9.9"