diff --git a/README.md b/README.md index a243ecba..af97a163 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,7 @@ This list is not final. If you have a driver you'd like to see added, please ope | [`asyncpg`](https://magicstack.github.io/asyncpg/current/) | PostgreSQL | Async | ✅ | | [`psycopg`](https://www.psycopg.org/) | PostgreSQL | Sync | ✅ | | [`psycopg`](https://www.psycopg.org/) | PostgreSQL | Async | ✅ | +| [`psqlpy`](https://psqlpy-python.github.io/) | PostgreSQL | Async | ✅ | | [`aiosqlite`](https://github.com/omnilib/aiosqlite) | SQLite | Async | ✅ | | `sqlite3` | SQLite | Sync | ✅ | | [`oracledb`](https://oracle.github.io/python-oracledb/) | Oracle | Async | ✅ | diff --git a/docs/PYPI_README.md b/docs/PYPI_README.md index a243ecba..af97a163 100644 --- a/docs/PYPI_README.md +++ b/docs/PYPI_README.md @@ -43,6 +43,7 @@ This list is not final. If you have a driver you'd like to see added, please ope | [`asyncpg`](https://magicstack.github.io/asyncpg/current/) | PostgreSQL | Async | ✅ | | [`psycopg`](https://www.psycopg.org/) | PostgreSQL | Sync | ✅ | | [`psycopg`](https://www.psycopg.org/) | PostgreSQL | Async | ✅ | +| [`psqlpy`](https://psqlpy-python.github.io/) | PostgreSQL | Async | ✅ | | [`aiosqlite`](https://github.com/omnilib/aiosqlite) | SQLite | Async | ✅ | | `sqlite3` | SQLite | Sync | ✅ | | [`oracledb`](https://oracle.github.io/python-oracledb/) | Oracle | Async | ✅ | diff --git a/pyproject.toml b/pyproject.toml index 40721e03..3e4cdf64 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,6 +25,7 @@ nanoid = ["fastnanoid>=0.4.1"] oracledb = ["oracledb"] orjson = ["orjson"] performance = ["sqlglot[rs]"] +psqlpy = ["psqlpy"] psycopg = ["psycopg[binary,pool]"] pydantic = ["pydantic", "pydantic-extra-types"] pymssql = ["pymssql"] @@ -212,6 +213,7 @@ markers = [ "psycopg: marks tests using psycopg", "pymssql: marks tests using pymssql", "pymysql: marks tests using pymysql", + "psqlpy: marks tests using psqlpy", ] testpaths = ["tests"] diff --git a/sqlspec/adapters/asyncmy/config.py b/sqlspec/adapters/asyncmy/config.py index 94f5ec95..19c190e2 100644 --- a/sqlspec/adapters/asyncmy/config.py +++ b/sqlspec/adapters/asyncmy/config.py @@ -162,7 +162,7 @@ def pool_config_dict(self) -> "dict[str, Any]": raise ImproperConfigurationError(msg) async def create_connection(self) -> "Connection": # pyright: ignore[reportUnknownParameterType] - """Create and return a new asyncmy connection. + """Create and return a new asyncmy connection from the pool. Returns: A Connection instance. @@ -171,9 +171,8 @@ async def create_connection(self) -> "Connection": # pyright: ignore[reportUnkn ImproperConfigurationError: If the connection could not be created. """ try: - import asyncmy # pyright: ignore[reportMissingTypeStubs] - - return await asyncmy.connect(**self.connection_config_dict) # pyright: ignore + async with self.provide_connection() as conn: + return conn except Exception as e: msg = f"Could not configure the Asyncmy connection. Error: {e!s}" raise ImproperConfigurationError(msg) from e diff --git a/sqlspec/adapters/asyncpg/config.py b/sqlspec/adapters/asyncpg/config.py index ad61811e..3710bcea 100644 --- a/sqlspec/adapters/asyncpg/config.py +++ b/sqlspec/adapters/asyncpg/config.py @@ -174,7 +174,7 @@ def provide_pool(self, *args: "Any", **kwargs: "Any") -> "Awaitable[Pool]": # p return self.create_pool() # pyright: ignore[reportUnknownMemberType,reportUnknownVariableType] async def create_connection(self) -> "AsyncpgConnection": - """Create and return a new asyncpg connection. + """Create and return a new asyncpg connection from the pool. Returns: A Connection instance. @@ -183,9 +183,8 @@ async def create_connection(self) -> "AsyncpgConnection": ImproperConfigurationError: If the connection could not be created. """ try: - import asyncpg - - return await asyncpg.connect(**self.connection_config_dict) # type: ignore[no-any-return] + pool = await self.provide_pool() + return await pool.acquire() except Exception as e: msg = f"Could not configure the asyncpg connection. Error: {e!s}" raise ImproperConfigurationError(msg) from e diff --git a/sqlspec/adapters/oracledb/config/_asyncio.py b/sqlspec/adapters/oracledb/config/_asyncio.py index 4ee03c94..bb9c765f 100644 --- a/sqlspec/adapters/oracledb/config/_asyncio.py +++ b/sqlspec/adapters/oracledb/config/_asyncio.py @@ -1,6 +1,6 @@ from contextlib import asynccontextmanager from dataclasses import dataclass, field -from typing import TYPE_CHECKING, Any, Optional +from typing import TYPE_CHECKING, Any, Optional, cast from oracledb import create_pool_async as oracledb_create_pool # pyright: ignore[reportUnknownVariableType] from oracledb.connection import AsyncConnection @@ -112,7 +112,7 @@ def pool_config_dict(self) -> "dict[str, Any]": raise ImproperConfigurationError(msg) async def create_connection(self) -> "AsyncConnection": - """Create and return a new oracledb async connection. + """Create and return a new oracledb async connection from the pool. Returns: An AsyncConnection instance. @@ -121,9 +121,8 @@ async def create_connection(self) -> "AsyncConnection": ImproperConfigurationError: If the connection could not be created. """ try: - import oracledb - - return await oracledb.connect_async(**self.connection_config_dict) # type: ignore[no-any-return] + pool = await self.provide_pool() + return cast("AsyncConnection", await pool.acquire()) # type: ignore[no-any-return,unused-ignore] except Exception as e: msg = f"Could not configure the Oracle async connection. Error: {e!s}" raise ImproperConfigurationError(msg) from e diff --git a/sqlspec/adapters/oracledb/config/_sync.py b/sqlspec/adapters/oracledb/config/_sync.py index 3e85b384..35a87e16 100644 --- a/sqlspec/adapters/oracledb/config/_sync.py +++ b/sqlspec/adapters/oracledb/config/_sync.py @@ -112,7 +112,7 @@ def pool_config_dict(self) -> "dict[str, Any]": raise ImproperConfigurationError(msg) def create_connection(self) -> "Connection": - """Create and return a new oracledb connection. + """Create and return a new oracledb connection from the pool. Returns: A Connection instance. @@ -121,9 +121,8 @@ def create_connection(self) -> "Connection": ImproperConfigurationError: If the connection could not be created. """ try: - import oracledb - - return oracledb.connect(**self.connection_config_dict) + pool = self.provide_pool() + return pool.acquire() except Exception as e: msg = f"Could not configure the Oracle connection. Error: {e!s}" raise ImproperConfigurationError(msg) from e diff --git a/sqlspec/adapters/psqlpy/__init__.py b/sqlspec/adapters/psqlpy/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/sqlspec/adapters/psqlpy/config.py b/sqlspec/adapters/psqlpy/config.py new file mode 100644 index 00000000..b88c3d3b --- /dev/null +++ b/sqlspec/adapters/psqlpy/config.py @@ -0,0 +1,258 @@ +"""Configuration for the psqlpy PostgreSQL adapter.""" + +from contextlib import asynccontextmanager +from dataclasses import dataclass, field +from typing import TYPE_CHECKING, Any, Optional, Union + +from psqlpy import Connection, ConnectionPool + +from sqlspec.adapters.psqlpy.driver import PsqlpyDriver +from sqlspec.base import AsyncDatabaseConfig, GenericPoolConfig +from sqlspec.exceptions import ImproperConfigurationError +from sqlspec.typing import Empty, EmptyType, dataclass_to_dict + +if TYPE_CHECKING: + from collections.abc import AsyncGenerator, Awaitable + + +__all__ = ( + "PsqlpyConfig", + "PsqlpyPoolConfig", +) + + +@dataclass +class PsqlpyPoolConfig(GenericPoolConfig): + """Configuration for psqlpy connection pool. + + Ref: https://psqlpy-python.github.io/components/connection_pool.html#all-available-connectionpool-parameters + """ + + dsn: Optional[Union[str, EmptyType]] = Empty + """DSN of the PostgreSQL.""" + # Required connection parameters + username: Optional[Union[str, EmptyType]] = Empty + """Username of the user in the PostgreSQL.""" + password: Optional[Union[str, EmptyType]] = Empty + """Password of the user in the PostgreSQL.""" + db_name: Optional[Union[str, EmptyType]] = Empty + """Name of the database in PostgreSQL.""" + + # Single or Multi-host parameters (mutually exclusive) + host: Optional[Union[str, EmptyType]] = Empty + """Host of the PostgreSQL (use for single host).""" + port: Optional[Union[int, EmptyType]] = Empty + """Port of the PostgreSQL (use for single host).""" + hosts: Optional[Union[list[str], EmptyType]] = Empty + """List of hosts of the PostgreSQL (use for multiple hosts).""" + ports: Optional[Union[list[int], EmptyType]] = Empty + """List of ports of the PostgreSQL (use for multiple hosts).""" + + # Pool size + max_db_pool_size: int = 10 + """Maximum size of the connection pool. Defaults to 10.""" + + # Optional timeouts + connect_timeout_sec: Optional[Union[int, EmptyType]] = Empty + """The time limit in seconds applied to each socket-level connection attempt.""" + connect_timeout_nanosec: Optional[Union[int, EmptyType]] = Empty + """Nanoseconds for connection timeout, can be used only with `connect_timeout_sec`.""" + tcp_user_timeout_sec: Optional[Union[int, EmptyType]] = Empty + """The time limit that transmitted data may remain unacknowledged before a connection is forcibly closed.""" + tcp_user_timeout_nanosec: Optional[Union[int, EmptyType]] = Empty + """Nanoseconds for tcp_user_timeout, can be used only with `tcp_user_timeout_sec`.""" + + # Optional keepalives + keepalives: bool = True + """Controls the use of TCP keepalive. Defaults to True (on).""" + keepalives_idle_sec: Optional[Union[int, EmptyType]] = Empty + """The number of seconds of inactivity after which a keepalive message is sent to the server.""" + keepalives_idle_nanosec: Optional[Union[int, EmptyType]] = Empty + """Nanoseconds for keepalives_idle_sec.""" + keepalives_interval_sec: Optional[Union[int, EmptyType]] = Empty + """The time interval between TCP keepalive probes.""" + keepalives_interval_nanosec: Optional[Union[int, EmptyType]] = Empty + """Nanoseconds for keepalives_interval_sec.""" + keepalives_retries: Optional[Union[int, EmptyType]] = Empty + """The maximum number of TCP keepalive probes that will be sent before dropping a connection.""" + + # Other optional parameters + load_balance_hosts: Optional[Union[str, EmptyType]] = Empty + """Controls the order in which the client tries to connect to the available hosts and addresses ('disable' or 'random').""" + conn_recycling_method: Optional[Union[str, EmptyType]] = Empty + """How a connection is recycled.""" + ssl_mode: Optional[Union[str, EmptyType]] = Empty + """SSL mode.""" + ca_file: Optional[Union[str, EmptyType]] = Empty + """Path to ca_file for SSL.""" + target_session_attrs: Optional[Union[str, EmptyType]] = Empty + """Specifies requirements of the session (e.g., 'read-write').""" + options: Optional[Union[str, EmptyType]] = Empty + """Command line options used to configure the server.""" + application_name: Optional[Union[str, EmptyType]] = Empty + """Sets the application_name parameter on the server.""" + + +@dataclass +class PsqlpyConfig(AsyncDatabaseConfig[Connection, ConnectionPool, PsqlpyDriver]): + """Configuration for psqlpy database connections, managing a connection pool. + + This configuration class wraps `PsqlpyPoolConfig` and manages the lifecycle + of a `psqlpy.ConnectionPool`. + """ + + pool_config: Optional[PsqlpyPoolConfig] = field(default=None) + """Psqlpy Pool configuration""" + driver_type: type[PsqlpyDriver] = field(default=PsqlpyDriver, init=False, hash=False) + """Type of the driver object""" + connection_type: type[Connection] = field(default=Connection, init=False, hash=False) + """Type of the connection object""" + pool_instance: Optional[ConnectionPool] = field(default=None, hash=False) + """The connection pool instance. If set, this will be used instead of creating a new pool.""" + + @property + def connection_config_dict(self) -> "dict[str, Any]": + """Return the minimal connection configuration as a dict for standalone use. + + Returns: + A string keyed dict of config kwargs for a psqlpy.Connection. + + Raises: + ImproperConfigurationError: If essential connection parameters are missing. + """ + if self.pool_config: + # Exclude pool-specific keys and internal metadata + pool_specific_keys = { + "max_db_pool_size", + "load_balance_hosts", + "conn_recycling_method", + "pool_instance", + "connection_type", + "driver_type", + } + return dataclass_to_dict( + self.pool_config, + exclude_empty=True, + convert_nested=False, + exclude_none=True, + exclude=pool_specific_keys, + ) + msg = "You must provide a 'pool_config' for this adapter." + raise ImproperConfigurationError(msg) + + @property + def pool_config_dict(self) -> "dict[str, Any]": + """Return the pool configuration as a dict. + + Raises: + ImproperConfigurationError: If no pool_config is provided but a pool_instance + + Returns: + A string keyed dict of config kwargs for creating a psqlpy pool. + """ + if self.pool_config: + # Extract the config from the pool_config + return dataclass_to_dict( + self.pool_config, + exclude_empty=True, + convert_nested=False, + exclude_none=True, + exclude={"pool_instance", "connection_type", "driver_type"}, + ) + + msg = "'pool_config' methods can not be used when a 'pool_instance' is provided." + raise ImproperConfigurationError(msg) + + async def create_pool(self) -> "ConnectionPool": + """Return a pool. If none exists yet, create one. + + Ensures that the pool is initialized and returns the instance. + + Returns: + The pool instance used by the plugin. + + Raises: + ImproperConfigurationError: If the pool could not be configured. + """ + if self.pool_instance is not None: + return self.pool_instance + + if self.pool_config is None: + msg = "One of 'pool_config' or 'pool_instance' must be provided." + raise ImproperConfigurationError(msg) + + # pool_config is guaranteed to exist due to __post_init__ + try: + # psqlpy ConnectionPool doesn't have an explicit async connect/startup method + # It creates connections on demand. + self.pool_instance = ConnectionPool(**self.pool_config_dict) + except Exception as e: + msg = f"Could not configure the 'pool_instance'. Error: {e!s}. Please check your configuration." + raise ImproperConfigurationError(msg) from e + + return self.pool_instance + + def provide_pool(self, *args: "Any", **kwargs: "Any") -> "Awaitable[ConnectionPool]": + """Create or return the pool instance. + + Returns: + An awaitable resolving to the Pool instance. + """ + + async def _create() -> "ConnectionPool": + return await self.create_pool() + + return _create() + + def create_connection(self) -> "Awaitable[Connection]": + """Create and return a new, standalone psqlpy connection using the configured parameters. + + Note: This method is not supported by the psqlpy adapter as connection + creation is primarily handled via the ConnectionPool. + Use `provide_connection` or `provide_session` for pooled connections. + + Returns: + An awaitable that resolves to a new Connection instance. + + Raises: + NotImplementedError: This method is not implemented for psqlpy. + """ + + async def _create() -> "Connection": + # psqlpy does not seem to offer a public API for creating + # standalone async connections easily outside the pool context. + msg = ( + "Creating standalone connections is not directly supported by the psqlpy adapter. " + "Please use the pool via `provide_connection` or `provide_session`." + ) + raise NotImplementedError(msg) + + return _create() + + @asynccontextmanager + async def provide_connection(self, *args: "Any", **kwargs: "Any") -> "AsyncGenerator[Connection, None]": + """Acquire a connection from the pool. + + Yields: + A connection instance managed by the pool. + """ + db_pool = await self.provide_pool(*args, **kwargs) + async with db_pool.acquire() as conn: + yield conn + + def close_pool(self) -> None: + """Close the connection pool.""" + if self.pool_instance is not None: + # psqlpy pool close is synchronous + self.pool_instance.close() + self.pool_instance = None + + @asynccontextmanager + async def provide_session(self, *args: Any, **kwargs: Any) -> "AsyncGenerator[PsqlpyDriver, None]": + """Create and provide a database session using a pooled connection. + + Yields: + A Psqlpy driver instance wrapping a pooled connection. + """ + async with self.provide_connection(*args, **kwargs) as connection: + yield self.driver_type(connection) diff --git a/sqlspec/adapters/psqlpy/driver.py b/sqlspec/adapters/psqlpy/driver.py new file mode 100644 index 00000000..a8272e48 --- /dev/null +++ b/sqlspec/adapters/psqlpy/driver.py @@ -0,0 +1,335 @@ +# ruff: noqa: PLR0915, PLR0914, PLR0912, C901 +"""Psqlpy Driver Implementation.""" + +import logging +import re +from typing import TYPE_CHECKING, Any, Optional, Union, cast + +from psqlpy.exceptions import RustPSQLDriverPyBaseError + +from sqlspec.base import AsyncDriverAdapterProtocol, T +from sqlspec.exceptions import SQLParsingError +from sqlspec.statement import PARAM_REGEX, SQLStatement + +if TYPE_CHECKING: + from psqlpy import Connection, QueryResult + + from sqlspec.typing import ModelDTOT, StatementParameterType + +__all__ = ("PsqlpyDriver",) + + +# Regex to find '?' placeholders, skipping those inside quotes or SQL comments +QMARK_REGEX = re.compile( + r"""(?P"[^"]*") | # Double-quoted strings + (?P\'[^\']*\') | # Single-quoted strings + (?P--[^\n]*|/\*.*?\*/) | # SQL comments (single/multi-line) + (?P\?) # The question mark placeholder + """, + re.VERBOSE | re.DOTALL, +) +logger = logging.getLogger("sqlspec") + + +class PsqlpyDriver(AsyncDriverAdapterProtocol["Connection"]): + """Psqlpy Postgres Driver Adapter.""" + + connection: "Connection" + dialect: str = "postgres" + + def __init__(self, connection: "Connection") -> None: + self.connection = connection + + def _process_sql_params( + self, + sql: str, + parameters: "Optional[StatementParameterType]" = None, + /, + **kwargs: Any, + ) -> "tuple[str, Optional[Union[tuple[Any, ...], list[Any], dict[str, Any]]]]": + """Process SQL and parameters for psqlpy. + + psqlpy uses $1, $2 style parameters natively. + This method converts '?' (tuple/list) and ':name' (dict) styles to $n. + It relies on SQLStatement for initial parameter validation and merging. + """ + stmt = SQLStatement(sql=sql, parameters=parameters, dialect=self.dialect, kwargs=kwargs or None) + sql, parameters = stmt.process() + + # Case 1: Parameters are a dictionary + if isinstance(parameters, dict): + processed_sql_parts: list[str] = [] + ordered_params = [] + last_end = 0 + param_index = 1 + found_params_regex: list[str] = [] + + for match in PARAM_REGEX.finditer(sql): + if match.group("dquote") or match.group("squote") or match.group("comment"): + continue + + if match.group("var_name"): # Finds :var_name + var_name = match.group("var_name") + found_params_regex.append(var_name) + start = match.start("var_name") - 1 + end = match.end("var_name") + + if var_name not in parameters: + msg = f"Named parameter ':{var_name}' missing from parameters. SQL: {sql}" + raise SQLParsingError(msg) + + processed_sql_parts.extend((sql[last_end:start], f"${param_index}")) + ordered_params.append(parameters[var_name]) + last_end = end + param_index += 1 + + processed_sql_parts.append(sql[last_end:]) + final_sql = "".join(processed_sql_parts) + + if not found_params_regex and parameters: + logger.warning( + "Dict params provided (%s), but no :name placeholders found. SQL: %s", + list(parameters.keys()), + sql, + ) + return sql, () + + provided_keys = set(parameters.keys()) + found_keys = set(found_params_regex) + unused_keys = provided_keys - found_keys + if unused_keys: + logger.warning("Unused parameters provided: %s. SQL: %s", unused_keys, sql) + + return final_sql, tuple(ordered_params) + + # Case 2: Parameters are a sequence/scalar + if isinstance(parameters, (list, tuple)): + sequence_processed_parts: list[str] = [] + param_index = 1 + last_end = 0 + qmark_found = False + + for match in QMARK_REGEX.finditer(sql): + if match.group("dquote") or match.group("squote") or match.group("comment"): + continue + + if match.group("qmark"): + qmark_found = True + start = match.start("qmark") + end = match.end("qmark") + sequence_processed_parts.extend((sql[last_end:start], f"${param_index}")) + last_end = end + param_index += 1 + + sequence_processed_parts.append(sql[last_end:]) + final_sql = "".join(sequence_processed_parts) + + if parameters and not qmark_found: + logger.warning("Sequence parameters provided, but no '?' placeholders found. SQL: %s", sql) + return sql, parameters + + expected_params = param_index - 1 + actual_params = len(parameters) + if expected_params != actual_params: + msg = f"Parameter count mismatch: Expected {expected_params}, got {actual_params}. SQL: {final_sql}" + raise SQLParsingError(msg) + + return final_sql, parameters + + # Case 3: Parameters are None + if PARAM_REGEX.search(sql) or QMARK_REGEX.search(sql): + # Perform a simpler check if any placeholders might exist if no params are given + for match in PARAM_REGEX.finditer(sql): + if not (match.group("dquote") or match.group("squote") or match.group("comment")) and match.group( + "var_name" + ): + msg = f"SQL contains named parameters (:name) but no parameters provided. SQL: {sql}" + raise SQLParsingError(msg) + for match in QMARK_REGEX.finditer(sql): + if not (match.group("dquote") or match.group("squote") or match.group("comment")) and match.group( + "qmark" + ): + msg = f"SQL contains positional parameters (?) but no parameters provided. SQL: {sql}" + raise SQLParsingError(msg) + + return sql, () + + async def select( + self, + sql: str, + parameters: Optional["StatementParameterType"] = None, + /, + *, + connection: Optional["Connection"] = None, + schema_type: "Optional[type[ModelDTOT]]" = None, + **kwargs: Any, + ) -> "list[Union[ModelDTOT, dict[str, Any]]]": + connection = self._connection(connection) + sql, parameters = self._process_sql_params(sql, parameters, **kwargs) + parameters = parameters or [] # psqlpy expects a list/tuple + + results: QueryResult = await connection.fetch(sql, parameters=parameters) + + if schema_type is None: + return cast("list[dict[str, Any]]", results.result()) # type: ignore[return-value] + return results.as_class(as_class=schema_type) + + async def select_one( + self, + sql: str, + parameters: Optional["StatementParameterType"] = None, + /, + *, + connection: Optional["Connection"] = None, + schema_type: "Optional[type[ModelDTOT]]" = None, + **kwargs: Any, + ) -> "Union[ModelDTOT, dict[str, Any]]": + connection = self._connection(connection) + sql, parameters = self._process_sql_params(sql, parameters, **kwargs) + parameters = parameters or [] + + result = await connection.fetch(sql, parameters=parameters) + + if schema_type is None: + result = cast("list[dict[str, Any]]", result.result()) # type: ignore[assignment] + return cast("dict[str, Any]", result[0]) # type: ignore[index] + return result.as_class(as_class=schema_type)[0] + + async def select_one_or_none( + self, + sql: str, + parameters: Optional["StatementParameterType"] = None, + /, + *, + connection: Optional["Connection"] = None, + schema_type: "Optional[type[ModelDTOT]]" = None, + **kwargs: Any, + ) -> "Optional[Union[ModelDTOT, dict[str, Any]]]": + connection = self._connection(connection) + sql, parameters = self._process_sql_params(sql, parameters, **kwargs) + parameters = parameters or [] + + result = await connection.fetch(sql, parameters=parameters) + if schema_type is None: + result = cast("list[dict[str, Any]]", result.result()) # type: ignore[assignment] + if len(result) == 0: # type: ignore[arg-type] + return None + return cast("dict[str, Any]", result[0]) # type: ignore[index] + result = cast("list[ModelDTOT]", result.as_class(as_class=schema_type)) # type: ignore[assignment] + if len(result) == 0: # type: ignore[arg-type] + return None + return cast("ModelDTOT", result[0]) # type: ignore[index] + + async def select_value( + self, + sql: str, + parameters: "Optional[StatementParameterType]" = None, + /, + *, + connection: "Optional[Connection]" = None, + schema_type: "Optional[type[T]]" = None, + **kwargs: Any, + ) -> "Union[T, Any]": + connection = self._connection(connection) + sql, parameters = self._process_sql_params(sql, parameters, **kwargs) + parameters = parameters or [] + + value = await connection.fetch_val(sql, parameters=parameters) + + if schema_type is None: + return value + return schema_type(value) # type: ignore[call-arg] + + async def select_value_or_none( + self, + sql: str, + parameters: "Optional[StatementParameterType]" = None, + /, + *, + connection: "Optional[Connection]" = None, + schema_type: "Optional[type[T]]" = None, + **kwargs: Any, + ) -> "Optional[Union[T, Any]]": + connection = self._connection(connection) + sql, parameters = self._process_sql_params(sql, parameters, **kwargs) + parameters = parameters or [] + try: + value = await connection.fetch_val(sql, parameters=parameters) + except RustPSQLDriverPyBaseError: + return None + + if value is None: + return None + if schema_type is None: + return value + return schema_type(value) # type: ignore[call-arg] + + async def insert_update_delete( + self, + sql: str, + parameters: Optional["StatementParameterType"] = None, + /, + *, + connection: Optional["Connection"] = None, + **kwargs: Any, + ) -> int: + connection = self._connection(connection) + sql, parameters = self._process_sql_params(sql, parameters, **kwargs) + parameters = parameters or [] + + await connection.execute(sql, parameters=parameters) + # For INSERT/UPDATE/DELETE, psqlpy returns an empty list but the operation succeeded + # if no error was raised + return 1 + + async def insert_update_delete_returning( + self, + sql: str, + parameters: Optional["StatementParameterType"] = None, + /, + *, + connection: Optional["Connection"] = None, + schema_type: "Optional[type[ModelDTOT]]" = None, + **kwargs: Any, + ) -> "Optional[Union[dict[str, Any], ModelDTOT]]": + connection = self._connection(connection) + sql, parameters = self._process_sql_params(sql, parameters, **kwargs) + parameters = parameters or [] + + result = await connection.execute(sql, parameters=parameters) + if schema_type is None: + result = result.result() # type: ignore[assignment] + if len(result) == 0: # type: ignore[arg-type] + return None + return cast("dict[str, Any]", result[0]) # type: ignore[index] + result = result.as_class(as_class=schema_type) # type: ignore[assignment] + if len(result) == 0: # type: ignore[arg-type] + return None + return cast("ModelDTOT", result[0]) # type: ignore[index] + + async def execute_script( + self, + sql: str, + parameters: Optional["StatementParameterType"] = None, + /, + *, + connection: Optional["Connection"] = None, + **kwargs: Any, + ) -> str: + connection = self._connection(connection) + sql, parameters = self._process_sql_params(sql, parameters, **kwargs) + parameters = parameters or [] + + await connection.execute(sql, parameters=parameters) + return sql + + def _connection(self, connection: Optional["Connection"] = None) -> "Connection": + """Get the connection to use. + + Args: + connection: Optional connection to use. If not provided, use the default connection. + + Returns: + The connection to use. + """ + return connection or self.connection diff --git a/sqlspec/adapters/psycopg/config/_async.py b/sqlspec/adapters/psycopg/config/_async.py index a7d1ad1f..a0f0569d 100644 --- a/sqlspec/adapters/psycopg/config/_async.py +++ b/sqlspec/adapters/psycopg/config/_async.py @@ -94,7 +94,7 @@ def pool_config_dict(self) -> "dict[str, Any]": raise ImproperConfigurationError(msg) async def create_connection(self) -> "AsyncConnection": - """Create and return a new psycopg async connection. + """Create and return a new psycopg async connection from the pool. Returns: An AsyncConnection instance. @@ -103,9 +103,8 @@ async def create_connection(self) -> "AsyncConnection": ImproperConfigurationError: If the connection could not be created. """ try: - from psycopg import AsyncConnection - - return await AsyncConnection.connect(**self.connection_config_dict) + pool = await self.provide_pool() + return await pool.getconn() except Exception as e: msg = f"Could not configure the Psycopg connection. Error: {e!s}" raise ImproperConfigurationError(msg) from e @@ -132,6 +131,7 @@ async def create_pool(self) -> "AsyncConnectionPool": if self.pool_instance is None: # pyright: ignore[reportUnnecessaryComparison] msg = "Could not configure the 'pool_instance'. Please check your configuration." # type: ignore[unreachable] raise ImproperConfigurationError(msg) + await self.pool_instance.open() return self.pool_instance def provide_pool(self, *args: "Any", **kwargs: "Any") -> "Awaitable[AsyncConnectionPool]": diff --git a/sqlspec/adapters/psycopg/config/_sync.py b/sqlspec/adapters/psycopg/config/_sync.py index 40a6a0b9..75ea340c 100644 --- a/sqlspec/adapters/psycopg/config/_sync.py +++ b/sqlspec/adapters/psycopg/config/_sync.py @@ -93,7 +93,7 @@ def pool_config_dict(self) -> "dict[str, Any]": raise ImproperConfigurationError(msg) def create_connection(self) -> "Connection": - """Create and return a new psycopg connection. + """Create and return a new psycopg connection from the pool. Returns: A Connection instance. @@ -102,9 +102,8 @@ def create_connection(self) -> "Connection": ImproperConfigurationError: If the connection could not be created. """ try: - from psycopg import connect - - return connect(**self.connection_config_dict) + pool = self.provide_pool() + return pool.getconn() except Exception as e: msg = f"Could not configure the Psycopg connection. Error: {e!s}" raise ImproperConfigurationError(msg) from e @@ -131,6 +130,7 @@ def create_pool(self) -> "ConnectionPool": if self.pool_instance is None: # pyright: ignore[reportUnnecessaryComparison] msg = "Could not configure the 'pool_instance'. Please check your configuration." # type: ignore[unreachable] raise ImproperConfigurationError(msg) + self.pool_instance.open() return self.pool_instance def provide_pool(self, *args: "Any", **kwargs: "Any") -> "ConnectionPool": diff --git a/tests/integration/test_adapters/test_psqlpy/__init__.py b/tests/integration/test_adapters/test_psqlpy/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/integration/test_adapters/test_psqlpy/test_connection.py b/tests/integration/test_adapters/test_psqlpy/test_connection.py new file mode 100644 index 00000000..f9eb39f1 --- /dev/null +++ b/tests/integration/test_adapters/test_psqlpy/test_connection.py @@ -0,0 +1,66 @@ +"""Test Psqlpy connection functionality.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import pytest + +from sqlspec.adapters.psqlpy.config import PsqlpyConfig, PsqlpyPoolConfig + +if TYPE_CHECKING: + from pytest_databases.docker.postgres import PostgresService + +pytestmark = [pytest.mark.psqlpy, pytest.mark.postgres, pytest.mark.integration] + + +@pytest.fixture +def psqlpy_config(postgres_service: PostgresService) -> PsqlpyConfig: + """Fixture for PsqlpyConfig using the postgres service.""" + # Construct DSN manually like in asyncpg tests + dsn = f"postgres://{postgres_service.user}:{postgres_service.password}@{postgres_service.host}:{postgres_service.port}/{postgres_service.database}" + return PsqlpyConfig( + pool_config=PsqlpyPoolConfig( + dsn=dsn, + max_db_pool_size=2, + ) + ) + + +@pytest.mark.asyncio +async def test_connect_via_pool(psqlpy_config: PsqlpyConfig) -> None: + """Test establishing a connection via the pool.""" + pool = await psqlpy_config.create_pool() + conn = await pool.connection() + assert conn is not None + # Optionally, perform a simple query to confirm connection + result = await conn.fetch_val("SELECT 1") # Corrected method name + assert result == 1 + conn.back_to_pool() + + +@pytest.mark.asyncio +async def test_connect_direct(psqlpy_config: PsqlpyConfig) -> None: + """Test establishing a connection via the provide_connection context manager.""" + # This test now uses provide_connection for a cleaner approach + # to getting a managed connection. + async with psqlpy_config.provide_connection() as conn: + assert conn is not None + # Perform a simple query + result = await conn.fetch_val("SELECT 1") # Corrected method name + assert result == 1 + # Connection is automatically released by the context manager + + +@pytest.mark.asyncio +async def test_provide_session_context_manager(psqlpy_config: PsqlpyConfig) -> None: + """Test the provide_session context manager.""" + async with psqlpy_config.provide_session() as driver: + assert driver is not None + assert driver.connection is not None + # Test a simple query within the session + val = await driver.select_value("SELECT 'test'") + assert val == "test" + + # After exiting context, connection should be released/closed (handled by config) + # Verification depends on pool implementation, might be hard to check directly diff --git a/tests/integration/test_adapters/test_psqlpy/test_driver.py b/tests/integration/test_adapters/test_psqlpy/test_driver.py new file mode 100644 index 00000000..254f143f --- /dev/null +++ b/tests/integration/test_adapters/test_psqlpy/test_driver.py @@ -0,0 +1,191 @@ +"""Test Psqlpy driver implementation.""" + +from __future__ import annotations + +from collections.abc import AsyncGenerator +from typing import TYPE_CHECKING, Any, Literal + +import pytest + +from sqlspec.adapters.psqlpy.config import PsqlpyConfig, PsqlpyPoolConfig + +if TYPE_CHECKING: + from pytest_databases.docker.postgres import PostgresService + +# Define supported parameter styles for testing +ParamStyle = Literal["tuple_binds", "dict_binds"] + +pytestmark = [pytest.mark.psqlpy, pytest.mark.postgres, pytest.mark.integration] + + +@pytest.fixture +def psqlpy_config(postgres_service: PostgresService) -> PsqlpyConfig: + """Fixture for PsqlpyConfig using the postgres service.""" + dsn = f"postgres://{postgres_service.user}:{postgres_service.password}@{postgres_service.host}:{postgres_service.port}/{postgres_service.database}" + return PsqlpyConfig( + pool_config=PsqlpyPoolConfig( + dsn=dsn, + max_db_pool_size=5, # Adjust pool size as needed for tests + ) + ) + + +@pytest.fixture(autouse=True) +async def _manage_table(psqlpy_config: PsqlpyConfig) -> AsyncGenerator[None, None]: # pyright: ignore[reportUnusedFunction] + """Fixture to create and drop the test table for each test.""" + create_sql = """ + CREATE TABLE IF NOT EXISTS test_table ( + id SERIAL PRIMARY KEY, + name VARCHAR(50) + ); + """ + drop_sql = "DROP TABLE IF EXISTS test_table;" + async with psqlpy_config.provide_session() as driver: + await driver.execute_script(create_sql) + yield + async with psqlpy_config.provide_session() as driver: + await driver.execute_script(drop_sql) + + +# --- Test Parameter Styles --- # + + +@pytest.mark.parametrize( + ("params", "style"), + [ + pytest.param(("test_name",), "tuple_binds", id="tuple_binds"), + pytest.param({"name": "test_name"}, "dict_binds", id="dict_binds"), + ], +) +@pytest.mark.asyncio +async def test_insert_returning_param_styles(psqlpy_config: PsqlpyConfig, params: Any, style: ParamStyle) -> None: + """Test insert returning with different parameter styles.""" + if style == "tuple_binds": + sql = "INSERT INTO test_table (name) VALUES (?) RETURNING *" + else: # dict_binds + sql = "INSERT INTO test_table (name) VALUES (:name) RETURNING *" + + async with psqlpy_config.provide_session() as driver: + result = await driver.insert_update_delete_returning(sql, params) + assert result is not None + assert result["name"] == "test_name" + assert result["id"] is not None + + +@pytest.mark.parametrize( + ("params", "style"), + [ + pytest.param(("test_name",), "tuple_binds", id="tuple_binds"), + pytest.param({"name": "test_name"}, "dict_binds", id="dict_binds"), + ], +) +@pytest.mark.asyncio +async def test_select_param_styles(psqlpy_config: PsqlpyConfig, params: Any, style: ParamStyle) -> None: + """Test select with different parameter styles.""" + # Insert test data first (using tuple style for simplicity here) + insert_sql = "INSERT INTO test_table (name) VALUES (?)" + async with psqlpy_config.provide_session() as driver: + await driver.insert_update_delete(insert_sql, ("test_name",)) + + # Prepare select SQL based on style + if style == "tuple_binds": + select_sql = "SELECT id, name FROM test_table WHERE name = ?" + else: # dict_binds + select_sql = "SELECT id, name FROM test_table WHERE name = :name" + + results = await driver.select(select_sql, params) + assert len(results) == 1 + assert results[0]["name"] == "test_name" + + +# --- Test Core Driver Methods --- # + + +@pytest.mark.asyncio +async def test_insert_update_delete(psqlpy_config: PsqlpyConfig) -> None: + """Test basic insert, update, delete operations.""" + async with psqlpy_config.provide_session() as driver: + # Insert + insert_sql = "INSERT INTO test_table (name) VALUES (?)" + row_count = await driver.insert_update_delete(insert_sql, ("initial_name",)) + assert row_count == 1 + + # Verify Insert + select_sql = "SELECT name FROM test_table WHERE name = ?" + result = await driver.select_one(select_sql, ("initial_name",)) + assert result["name"] == "initial_name" + + # Update + update_sql = "UPDATE test_table SET name = ? WHERE name = ?" + row_count = await driver.insert_update_delete(update_sql, ("updated_name", "initial_name")) + assert row_count == 1 + + # Verify Update + result_or_none = await driver.select_one_or_none(select_sql, ("updated_name",)) + assert result_or_none is not None + assert result_or_none["name"] == "updated_name" + result_or_none = await driver.select_one_or_none(select_sql, "initial_name") + assert result_or_none is None + + # Delete + delete_sql = "DELETE FROM test_table WHERE name = ?" + row_count = await driver.insert_update_delete(delete_sql, ("updated_name",)) + assert row_count == 1 + + # Verify Delete + result_or_none = await driver.select_one_or_none(select_sql, ("updated_name",)) + assert result_or_none is None + + +@pytest.mark.asyncio +async def test_select_methods(psqlpy_config: PsqlpyConfig) -> None: + """Test various select methods (select, select_one, select_one_or_none, select_value).""" + async with psqlpy_config.provide_session() as driver: + # Insert multiple records + await driver.insert_update_delete("INSERT INTO test_table (name) VALUES (?), (?)", ("name1", "name2")) + + # Test select (multiple results) + results = await driver.select("SELECT name FROM test_table ORDER BY name") + assert len(results) == 2 + assert results[0]["name"] == "name1" + assert results[1]["name"] == "name2" + + # Test select_one + result_one = await driver.select_one("SELECT name FROM test_table WHERE name = ?", ("name1",)) + assert result_one["name"] == "name1" + + # Test select_one_or_none (found) + result_one_none = await driver.select_one_or_none("SELECT name FROM test_table WHERE name = ?", ("name2",)) + assert result_one_none is not None + assert result_one_none["name"] == "name2" + + # Test select_one_or_none (not found) + result_one_none_missing = await driver.select_one_or_none( + "SELECT name FROM test_table WHERE name = ?", ("missing",) + ) + assert result_one_none_missing is None + + # Test select_value + value = await driver.select_value("SELECT id FROM test_table WHERE name = ?", ("name1",)) + assert isinstance(value, int) + + # Test select_value_or_none (found) + value_or_none = await driver.select_value_or_none("SELECT id FROM test_table WHERE name = ?", ("name2",)) + assert isinstance(value_or_none, int) + + # Test select_value_or_none (not found) + value_or_none_missing = await driver.select_value_or_none( + "SELECT id FROM test_table WHERE name = ?", ("missing",) + ) + assert value_or_none_missing is None + + +@pytest.mark.asyncio +async def test_execute_script(psqlpy_config: PsqlpyConfig) -> None: + """Test execute_script method for non-query operations.""" + sql = "SELECT 1;" # Simple script + async with psqlpy_config.provide_session() as driver: + status = await driver.execute_script(sql) + # psqlpy execute returns a status string, exact content might vary + assert isinstance(status, str) + # We don't assert exact status content as it might change, just that it runs diff --git a/tests/integration/test_adapters/test_psycopg/test_connection.py b/tests/integration/test_adapters/test_psycopg/test_connection.py index a928736d..b4cd2d9c 100644 --- a/tests/integration/test_adapters/test_psycopg/test_connection.py +++ b/tests/integration/test_adapters/test_psycopg/test_connection.py @@ -26,7 +26,7 @@ async def test_async_connection(postgres_service: PostgresService) -> None: await cur.execute("SELECT 1") result = await cur.fetchone() assert result == (1,) - + await async_config.close_pool() # Test connection pool pool_config = PsycopgAsyncPoolConfig( conninfo=f"host={postgres_service.host} port={postgres_service.port} user={postgres_service.user} password={postgres_service.password} dbname={postgres_service.database}", @@ -42,6 +42,7 @@ async def test_async_connection(postgres_service: PostgresService) -> None: await cur.execute("SELECT 1") result = await cur.fetchone() assert result == (1,) + await another_config.close_pool() @pytest.mark.xdist_group("postgres") @@ -61,7 +62,7 @@ def test_sync_connection(postgres_service: PostgresService) -> None: cur.execute("SELECT 1") result = cur.fetchone() assert result == (1,) - + sync_config.close_pool() # Test connection pool pool_config = PsycopgSyncPoolConfig( conninfo=f"postgres://{postgres_service.user}:{postgres_service.password}@{postgres_service.host}:{postgres_service.port}/{postgres_service.database}", @@ -77,3 +78,4 @@ def test_sync_connection(postgres_service: PostgresService) -> None: cur.execute("SELECT 1") result = cur.fetchone() assert result == (1,) + another_config.close_pool() diff --git a/uv.lock b/uv.lock index 4bafccd4..ff8ef690 100644 --- a/uv.lock +++ b/uv.lock @@ -1343,11 +1343,11 @@ wheels = [ [[package]] name = "identify" -version = "2.6.9" +version = "2.6.10" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9b/98/a71ab060daec766acc30fb47dfca219d03de34a70d616a79a38c6066c5bf/identify-2.6.9.tar.gz", hash = "sha256:d40dfe3142a1421d8518e3d3985ef5ac42890683e32306ad614a29490abeb6bf", size = 99249 } +sdist = { url = "https://files.pythonhosted.org/packages/0c/83/b6ea0334e2e7327084a46aaaf71f2146fc061a192d6518c0d020120cd0aa/identify-2.6.10.tar.gz", hash = "sha256:45e92fd704f3da71cc3880036633f48b4b7265fd4de2b57627cb157216eb7eb8", size = 99201 } wheels = [ - { url = "https://files.pythonhosted.org/packages/07/ce/0845144ed1f0e25db5e7a79c2354c1da4b5ce392b8966449d5db8dca18f1/identify-2.6.9-py2.py3-none-any.whl", hash = "sha256:c98b4322da415a8e5a70ff6e51fbc2d2932c015532d77e9f8537b4ba7813b150", size = 99101 }, + { url = "https://files.pythonhosted.org/packages/2b/d3/85feeba1d097b81a44bcffa6a0beab7b4dfffe78e82fc54978d3ac380736/identify-2.6.10-py2.py3-none-any.whl", hash = "sha256:5f34248f54136beed1a7ba6a6b5c4b6cf21ff495aac7c359e1ef831ae3b8ab25", size = 99101 }, ] [[package]] @@ -2055,11 +2055,11 @@ wheels = [ [[package]] name = "packaging" -version = "24.2" +version = "25.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950 } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727 } wheels = [ - { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 }, + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469 }, ] [[package]] @@ -2096,15 +2096,15 @@ wheels = [ [[package]] name = "polyfactory" -version = "2.20.0" +version = "2.21.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "faker" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/59/c8/9d5c64495e3a7b672455859399aa0098b2728a7820ebe856db0cd0590197/polyfactory-2.20.0.tar.gz", hash = "sha256:86017160f05332baadb5eaf89885e1ba7bb447a3140e46ba4546848c76cbdec5", size = 243596 } +sdist = { url = "https://files.pythonhosted.org/packages/bd/d0/8ce6a9912a6f1077710ebc46a6aa9a79a64a06b69d2d6b4ccefc9765ce8f/polyfactory-2.21.0.tar.gz", hash = "sha256:a6d8dba91b2515d744cc014b5be48835633f7ccb72519a68f8801759e5b1737a", size = 246314 } wheels = [ - { url = "https://files.pythonhosted.org/packages/57/1a/c74707cb643b36ab490690d8b4501c2e839a81dcfbc14c9639d3d8ee9ad3/polyfactory-2.20.0-py3-none-any.whl", hash = "sha256:6a808454bb03afacf54abeeb50d79b86c9e5b8476efc2bc3788e5ece26dd561a", size = 60535 }, + { url = "https://files.pythonhosted.org/packages/e0/ba/c148fba517a0aaccfc4fca5e61bf2a051e084a417403e930dc615886d4e6/polyfactory-2.21.0-py3-none-any.whl", hash = "sha256:9483b764756c8622313d99f375889b1c0d92f09affb05742d7bcfa2b5198d8c5", size = 60875 }, ] [[package]] @@ -2163,6 +2163,94 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/12/fb/a586e0c973c95502e054ac5f81f88394f24ccc7982dac19c515acd9e2c93/protobuf-5.29.4-py3-none-any.whl", hash = "sha256:3fde11b505e1597f71b875ef2fc52062b6a9740e5f7c8997ce878b6009145862", size = 172551 }, ] +[[package]] +name = "psqlpy" +version = "0.9.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/df/39/bfa6bec547d9b197b72e547aee1440c300911895798fce00b0439ac8978b/psqlpy-0.9.3.tar.gz", hash = "sha256:f369528a4a1f5c8dea6efe0d469a07a735ab4d07e680faff4508699b183bcc37", size = 269627 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1f/ca/85ed2d5a927b1c0336e1c5ffc22ec576a9e9edc35a09895d9aa01f165c9b/psqlpy-0.9.3-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:3c36efab33733d6fb9064a0c9fb59de4a529376837b080277e6825437bb1efa9", size = 4240144 }, + { url = "https://files.pythonhosted.org/packages/93/72/45efb35a79ce717e282917610b3ff93fddd869d3e7702dfac8852c49b80f/psqlpy-0.9.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:57d9b5054494daaa60a7337351c3f9fc36354bac5764dd868134f9e91cd11245", size = 4476802 }, + { url = "https://files.pythonhosted.org/packages/83/2b/2b25af4cc385bede8e1ba29a0e17ec0e72e8c6318c1c551af536f5216c54/psqlpy-0.9.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a32b7f7b700180058ac7b5e7a02bac4c91de1ac94404e2a2a4eb67c91d2369a", size = 4936843 }, + { url = "https://files.pythonhosted.org/packages/28/04/bff643f7bd711b41ccc15f7cadc041291350a3a5d989b07bc42b9515da20/psqlpy-0.9.3-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:40ce3f9b67c7cea8bff8ce7b2f435191f1f064168134c17ba457695278145cd1", size = 4207043 }, + { url = "https://files.pythonhosted.org/packages/04/ba/0e5822e81a7205a4d9cd558f4b0b7d2fae3b2c5ad64b66e08c8ee6fb2404/psqlpy-0.9.3-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e6704cd34d1ed0f0873c448eb206565fdcd3eb0fc435428860aa529ce203bec", size = 4806688 }, + { url = "https://files.pythonhosted.org/packages/c8/7a/e803e1c33ceb0c219c49db4767f1888294e08ddbf58e62bad81189e8f575/psqlpy-0.9.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d3215cac58e8a6fb867a4cafaac3303d03ace5a623cb2ed7858ee797ab6b2bcd", size = 4651636 }, + { url = "https://files.pythonhosted.org/packages/50/a5/b69d8da7aee1c6c5c40396d832e0d970a3c4fbbe3974296010d7397c207b/psqlpy-0.9.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d6a741e1694325c737f1917fa80ffcda27a3a533c03d033a059b39e5fb85eca6", size = 4775602 }, + { url = "https://files.pythonhosted.org/packages/64/5f/27d3215bab4246d1978eecc74e649e4a12b7fbe895a24cc8ed1f0c6c5d09/psqlpy-0.9.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc9013962aac0ca16d59e8b7a20856c0838fb09ddbf9a3e5bb2cde2add3c5a89", size = 4825250 }, + { url = "https://files.pythonhosted.org/packages/31/9c/beb520f93f8f5fccb8d09bf9afb5b99b844e89e154de733eb3f54dbcdc82/psqlpy-0.9.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:2bdf0afa1cae9e79553665a906315d7c6421ed587b012ec9c4397e91c200fd7b", size = 4857456 }, + { url = "https://files.pythonhosted.org/packages/89/c4/5344648456b9184ef142baafd216396f2566e7025177070e26afbd52442c/psqlpy-0.9.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:bd2c3c3a5b0305d42430cc8af1d5c4130008b9a80cf5bb3fedfd414cd9b95181", size = 4977942 }, + { url = "https://files.pythonhosted.org/packages/5a/52/b376c046d4f4da66d22aa6db62a33fca8ccecc41538beb6640cef0c081b3/psqlpy-0.9.3-cp310-cp310-win32.whl", hash = "sha256:79a0ec837dd314df5af74577f8b5b2fb752e74641a5cb66d58d779d88648d191", size = 3308783 }, + { url = "https://files.pythonhosted.org/packages/08/24/f6552cdaf3e062966e6beac948c4f8daa28d32de9aae68011ebbeadd590b/psqlpy-0.9.3-cp310-cp310-win_amd64.whl", hash = "sha256:ee0d53b33f7b841cd606a8dbbf8b36e809dddee526dc7e226a7d1b1fdb0e3e3b", size = 3721262 }, + { url = "https://files.pythonhosted.org/packages/5e/1a/48bdacd23b7d2e2dd2c74bef7591e35a40795e9071faf672828f947bc9b5/psqlpy-0.9.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:66830960242bdec926ebb697284731f643176f4694419ecc4e48d064545c6e40", size = 4240197 }, + { url = "https://files.pythonhosted.org/packages/4b/48/299439ba665aeafbd053150d77c451254a7ef797c143e9760cfbcfe240c2/psqlpy-0.9.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:45a12aead3ca4d2b18b114b541289bcb5894bd5746670ee9fe5d783f55a565d4", size = 4477221 }, + { url = "https://files.pythonhosted.org/packages/09/cb/fbddd6c99f770a0fece65a3470d98dc0873a16a6c4e1adda246e5a5e6551/psqlpy-0.9.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e3fec5e579f519b98f5feb9e2148549c48391811e3153156deee7d2bc129f605", size = 4937618 }, + { url = "https://files.pythonhosted.org/packages/6c/c0/4ace3aacbe3a7b9e87a282c3ca2313e25ef7970739e1f8392fa6da5781e7/psqlpy-0.9.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:11a2b3574e9cf37f49dd5a8376823bb1c416222c133b59873ad2fdb91b0ba25f", size = 4206271 }, + { url = "https://files.pythonhosted.org/packages/eb/1b/194462eddc3373af65c04ed2d6d710f57812937f5361fb8492659e43987f/psqlpy-0.9.3-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:92afd8603006a18461d91167eff7ac100029b47034bbdd477f5e3805f9d23ff8", size = 4805586 }, + { url = "https://files.pythonhosted.org/packages/b9/1f/fcdb60dda446b392f27243f9e5f633d4f8a95ab3e222c120d26b6b6ebf47/psqlpy-0.9.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:71a912328513d6c89a8c3d1be615916ab360075c810ff0445e7c53030ff03186", size = 4653073 }, + { url = "https://files.pythonhosted.org/packages/c9/a7/b82afd9f9950304468cecc9b027fc8248d846764f9f9a0251b3c77c9c0f4/psqlpy-0.9.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4861bb23574f2275bbd4f695bd4b2393fe6a5442f0a1bb603ebb6061f9926f9e", size = 4786884 }, + { url = "https://files.pythonhosted.org/packages/29/49/11b42d762702be8624f6e1824a8743eb496951204389f1faaef1b31c60b7/psqlpy-0.9.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cf151599e516d7b0bbe0f8e2c10d45b7a48a72be2716c9fe7a77ecf41b17e3b9", size = 4823808 }, + { url = "https://files.pythonhosted.org/packages/f5/39/300dfdfa8780e7a1cf0dc5569a489a87f0061a264ce87ffd762351f53d7f/psqlpy-0.9.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:b3fece2b78d568d74435b5ff0f05406bb3a43b1c571a66b7c1909efd52c31508", size = 4857527 }, + { url = "https://files.pythonhosted.org/packages/c1/ec/178c575c45ad4884151d5f741e87ffe00712d491ad8c70413f2a4e95e173/psqlpy-0.9.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3c49ea203ef3007b8dbd12a453dd6d45aa9ffd3e395181e837138d172e30c293", size = 4979751 }, + { url = "https://files.pythonhosted.org/packages/8a/0c/e5ebe4c100ac134ef16ad95469cd38e9654ceca017561789680020149765/psqlpy-0.9.3-cp311-cp311-win32.whl", hash = "sha256:60833a0187dfdd859248bc424326d34b96b74c627e057a286632adbe4fb185a1", size = 3308639 }, + { url = "https://files.pythonhosted.org/packages/90/e8/a0e1b19d7035e5b849ca57b1c201d0243268bd6a215781ebd197ccfdca81/psqlpy-0.9.3-cp311-cp311-win_amd64.whl", hash = "sha256:185d508f9165ae51ed8e659ce8356986523ac14021233162e7b565a1b50e0486", size = 3721553 }, + { url = "https://files.pythonhosted.org/packages/6b/73/f65c655c2c3d89f36a6eb160db86e8aecbe7a0258368d05111b08456a3d6/psqlpy-0.9.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:6502545e46f9453a6103152e223f43461ca90e4d6d353b8567b40b45a01d43f8", size = 4226965 }, + { url = "https://files.pythonhosted.org/packages/32/9b/e17f40ecf4c518d83ff4cd742869d769f2a26224531a6fe1dfee88bba210/psqlpy-0.9.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f447410df05b0b927a8cc995fa011477b291d57f1b8f8ca52ee4a3799b698c8b", size = 4440561 }, + { url = "https://files.pythonhosted.org/packages/6c/62/6f9e6b409c6af33792386ba0e7a2beece6cf96fdffa8739144dfc1c897f4/psqlpy-0.9.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b825a3534d6a2810aed9c53873b8a58562bd28afc95506643ec4cc095bdda1a0", size = 4936741 }, + { url = "https://files.pythonhosted.org/packages/ae/d3/9f5ed5a8703dd3b1349f4ff53b03eb4b2589cb0034186cdf8a562a97653c/psqlpy-0.9.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:842a06192c4d6748aa63df3966b8685630ce1d2f1cdfbe54f95177c2db7436ad", size = 4219026 }, + { url = "https://files.pythonhosted.org/packages/7b/ca/9788d4ad663f6841825f89013414ac28a483bb031417ccf6e5f087065139/psqlpy-0.9.3-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7ece78105348c482bbcc1c7504c2067e8baf8ec64600aea40c293880995fb39c", size = 4805613 }, + { url = "https://files.pythonhosted.org/packages/f8/a3/2702d69e47ef26a75c1f8fbf2cfe9e280da45945501998082644f98f1411/psqlpy-0.9.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c099c16aa3d224d3b0c6950eabbbf58154f04612522511c3d83a0e97cd689eed", size = 4645177 }, + { url = "https://files.pythonhosted.org/packages/aa/8a/c452f47cc9cabdcd694681ca2c8326ea3887cbf481678266e67de334d984/psqlpy-0.9.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bb9746cdb5c8ddbbcf952bad67e07f4c46722de072cf6cfd69d8fb67d296156c", size = 4775536 }, + { url = "https://files.pythonhosted.org/packages/c6/1d/fa6f86bf856019b0f48cdc92497afe632e1176d1d12773d5a77971de0096/psqlpy-0.9.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4beb0fc66c7924edcf6f842682554910d93376e31d527fa3d055ee24326bd84f", size = 4826617 }, + { url = "https://files.pythonhosted.org/packages/6e/5c/8d85a5295ea236d85213aced33560ff93f8224dd7830d80e6bb37b5488d9/psqlpy-0.9.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:40e530d71441af61b791723bbf4a198b0da4e95f59d261e67074790564b629e5", size = 4826660 }, + { url = "https://files.pythonhosted.org/packages/d3/dd/d6181320adb9271bb8559c1588cf6c956592dd19876bf09937f8b647e5c6/psqlpy-0.9.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a74926fa272acc2889f510317894307c2dda7702feb8195a159b564603d0c74c", size = 4946588 }, + { url = "https://files.pythonhosted.org/packages/e0/0c/9fa61aec2daf03e257fe314a2db0671828e1b397b028473b190fac921067/psqlpy-0.9.3-cp312-cp312-win32.whl", hash = "sha256:2b125d06a0c2718b5cf3b9c866ea9bf19764f51c2aa054919534e4767f43b1f8", size = 3293165 }, + { url = "https://files.pythonhosted.org/packages/0a/7c/49717e45df23318bf1ebc6775ad4755e5b2ca7d920aaaeed514900a9fe65/psqlpy-0.9.3-cp312-cp312-win_amd64.whl", hash = "sha256:613facca51bbf0682d863989e7368222e8040b3ce5f5b0ae3ce30d9df4e44d60", size = 3700904 }, + { url = "https://files.pythonhosted.org/packages/4d/70/491f46e652492b22a8038082eae8821c3f74febba3d89eb72efbaa50e41c/psqlpy-0.9.3-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:9c7b371f5bc3e4e60e7f0b34db5c8ec82c6aee3e967f6916251895fc0123015b", size = 4225496 }, + { url = "https://files.pythonhosted.org/packages/2a/e2/83d29c062876383ad016897ecec2d8b67ca36fe3d37f5c25367e83c9a9db/psqlpy-0.9.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8546854232368d26eb2c7eba64df2765a4b4698d1465a9c20e6af0b61f495558", size = 4438780 }, + { url = "https://files.pythonhosted.org/packages/19/6a/4f520027cf6474f20e45a9292c9e80e297763c82ffb47cc4f9176d286865/psqlpy-0.9.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5e83ede139b7c650b5be3019de769db5bab44e72c3365604719222ca9c0825d", size = 4935733 }, + { url = "https://files.pythonhosted.org/packages/84/4d/44165581b4bc0eb7cda0b35da1a72681c38cb67506e18b244e5bd8373d9f/psqlpy-0.9.3-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5441f83b707f698c23b9c498845caabf523248adda1276be4efc58474991ba88", size = 4217304 }, + { url = "https://files.pythonhosted.org/packages/51/0b/357b9f0e303f6378cb5a03b58c5d0be31d219b013f5a670c5fec82ca3fc5/psqlpy-0.9.3-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c9070b7a767aa3ad6d16a6b4008b3590e576296d1ef810f11fbf7ad317ca902e", size = 4804254 }, + { url = "https://files.pythonhosted.org/packages/e7/da/3721d5457d64584d521219328210a216790b7a911710864a9260bd2760c2/psqlpy-0.9.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2452e0c1b5fa1f4d4b0c73daf277de0785e2a4aaa7d75282b100b2d1758a7677", size = 4644033 }, + { url = "https://files.pythonhosted.org/packages/44/fc/1170684ea74be268d2b27ae7e69517c29f6bd5bafb192bb7f76615d3fa79/psqlpy-0.9.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:231f57d6129358bdd828f1a24667c727ac9cd07ae374b76ab9d860bc9778be36", size = 4774739 }, + { url = "https://files.pythonhosted.org/packages/4d/bb/cd369c0d7b9a022b07740167f1d7eae33ccac1a239e10b21306ca6ebc99e/psqlpy-0.9.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b9d258b4af8593fafd02ec00dd594f0de7cb9c5a6d59bb30c0883bb314046bf4", size = 4826125 }, + { url = "https://files.pythonhosted.org/packages/2c/ff/6bba939bab5e6b7b42aa2b3f8eedb46f7c9d5e5c21e008c856986d594255/psqlpy-0.9.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:5a45f42936faa2839b72d8ba6f31c7f22490cf5211a5acfe507655b22110df6e", size = 4826300 }, + { url = "https://files.pythonhosted.org/packages/94/2f/2702b621b8d68683b14d954f5ff2a7a65904a9ae8ea1b62278a1d81cdd84/psqlpy-0.9.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:81f2c9f7f94388d5977e28ab00522ddccd1b880e9f382e350207a80a6c2b790f", size = 4944879 }, + { url = "https://files.pythonhosted.org/packages/46/97/b6dacbe274cbf95419af1c8c6c5f06a0290a22a66b4202cbc493db97d127/psqlpy-0.9.3-cp313-cp313-win32.whl", hash = "sha256:9126f5e942c2a8ed1aefa8a662a2865f7aaf293a7610911fd4a07200c133a27a", size = 3292642 }, + { url = "https://files.pythonhosted.org/packages/ea/55/aaadda7cbdfdbef9f880b57fb25d3cb641e01a71fbd20d76f59a5eddab78/psqlpy-0.9.3-cp313-cp313-win_amd64.whl", hash = "sha256:e438732440dbba845b1f2fcec426cc8a70461d31e81b330efe5ab4e5a83ead52", size = 3700554 }, + { url = "https://files.pythonhosted.org/packages/ae/4d/3395a1a83e4501f9928c83fbfdc88f643158a3224d76d06f6eca837c61a7/psqlpy-0.9.3-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:583f93e6dcdc5f0c6a9259ed7da686c5a888f10ac8546cdd7e4cacfac5826700", size = 4240939 }, + { url = "https://files.pythonhosted.org/packages/d4/46/610c514db36e83e1f5f9e3f309d30795eff8ef7cba45bc223bea98811fa4/psqlpy-0.9.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8fc690c4b56b5aa3242518309167c78820621fd6b3284951c65d7426218be3f6", size = 4479396 }, + { url = "https://files.pythonhosted.org/packages/0d/36/fe06b173bee1bc09aac918c4759071773d433c04c943a7d161de1c93c87f/psqlpy-0.9.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1395fcfdb294d75ee9044b1da60fcaaca3d01c62cfa338f3c18ecd097e0bd738", size = 4937303 }, + { url = "https://files.pythonhosted.org/packages/c8/91/a2187176a1799c2b5d409f6d4fc39c40c2abfd3dfb2d08435c2956a86539/psqlpy-0.9.3-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:033e41fecdfe3b4dee6050d4002c5d619b9325bef4167cc497d644c093ee87f4", size = 4210807 }, + { url = "https://files.pythonhosted.org/packages/b7/d7/313638aee4d3f73c4c07bf37a232d83348c37c251f609881796ee6324267/psqlpy-0.9.3-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7fadb57a391db8ac1b172a2f3882796e8f77ef2e01795db9fe96870b3d434116", size = 4807024 }, + { url = "https://files.pythonhosted.org/packages/01/09/64469aa84e596d8c08210ff6e86d7aafb75eedb7ffae12daa75519eb9f97/psqlpy-0.9.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d7cb7714554cf08e4eaea8fd547dddb65a32927db4c37afd0fbe9a941d839743", size = 4653704 }, + { url = "https://files.pythonhosted.org/packages/e6/47/28eb3014eff4642be833abe1c5c745cd31efd63ef5706528901e9efad820/psqlpy-0.9.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ddbcd8438cc439862e05e03f4cf364f76dc51d918fe405878c1acfab1848112c", size = 4789815 }, + { url = "https://files.pythonhosted.org/packages/17/7c/509f51dfae6b4fd4282d2589199ab9a895bb3984bf27244231566d5f8cad/psqlpy-0.9.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5fd086508cf11e3c390f5d6ed638a39e3d2b4186e53e33ffe6af9b37981ef615", size = 4826656 }, + { url = "https://files.pythonhosted.org/packages/b5/79/bb9725575368f46570b646b45da22ef943e97b32e336a485ac169906c1d1/psqlpy-0.9.3-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:35aa277d6585ed14936e6c1e4de677564959e379485272906c88090cc5e635dd", size = 4860622 }, + { url = "https://files.pythonhosted.org/packages/d3/69/3d80d7286b116734fa8c256b711a0a9554d31df82f7f33cd52e7772ba77b/psqlpy-0.9.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:81a25ef924322a15a53ee6c36dd38c3d609d05c96f3cf500af33cd929cbe30a1", size = 4980015 }, + { url = "https://files.pythonhosted.org/packages/6c/09/b9c0f007c6686193a8f6362980c20c1b33e35cf7bac061071a2a84370e7c/psqlpy-0.9.3-cp39-cp39-win32.whl", hash = "sha256:fa1cbd09536576b22e7aa5e312cf802d4e7da2438c48064bf6886028e8977bec", size = 3311353 }, + { url = "https://files.pythonhosted.org/packages/2a/78/181d4f7fbb9f77dce11513b50406ae65484fb5e933143a8c038d73f0a549/psqlpy-0.9.3-cp39-cp39-win_amd64.whl", hash = "sha256:ca15a64cbd4de84c7b9a34f388c70dded46316fac8af851c2f1ba51a755f378f", size = 3722931 }, + { url = "https://files.pythonhosted.org/packages/2b/b0/e5e9c3006523bf9a32d1b3506bb2dfc109101d1d8e9d28badba64af9ff38/psqlpy-0.9.3-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:8c4a6035ebfba0744b15a5c9a3668b2166393619bfb882a189468966ec45f7f0", size = 4236200 }, + { url = "https://files.pythonhosted.org/packages/97/23/af1bbbe53c04c029a45dd4c65bb19434c152f4c314d2362f5913ecf09c4d/psqlpy-0.9.3-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:681dee1d030873f08e55b154c8907a2062f0f6e4b10646411f5ce94028eb0642", size = 4474528 }, + { url = "https://files.pythonhosted.org/packages/a3/2a/60cad4acd6be95d0df233f13ac810fb357347f2d224eb2d70471df6c7353/psqlpy-0.9.3-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e26ed13c864b21542eab15f1dfccf02e40fae046076d9c48b3ff062f8406053b", size = 4937463 }, + { url = "https://files.pythonhosted.org/packages/fa/45/ba6e10effa92090ed508988dad48d776f466aa2cbc94c7a16b5f14d75e1e/psqlpy-0.9.3-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1baa0200bcdacc9f61efbd273074395bfc7f32b7f7a30fcebb260ae175545914", size = 4210011 }, + { url = "https://files.pythonhosted.org/packages/bc/fe/cd176b3a342b45d2f10ad24b109db7a069b437f411302b27eaea77cf0b28/psqlpy-0.9.3-pp310-pypy310_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:925677b1c2ea70addbcb28b86ddc826b77a8107ec489e2d191fb74074ea423b2", size = 4806658 }, + { url = "https://files.pythonhosted.org/packages/59/8f/1898a03913f9b697d1b8ae02b3d435a5f05f60e81be5a91934e51044e310/psqlpy-0.9.3-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d2c01d44e04f2e9fc7de1c3a8692f38fa22d0a354ce51888b91d8653cae4e0a3", size = 4656552 }, + { url = "https://files.pythonhosted.org/packages/60/4d/b67e4b9ffd963850107befa435e8b38b8e7d4f393ee15964c73706c67b13/psqlpy-0.9.3-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b89fb5906bffa757f76c1ef46ec48e3ec3488d484d95d9bc57503b30107e3041", size = 4788063 }, + { url = "https://files.pythonhosted.org/packages/25/36/ea1a770563db9105624d0ce73d1b16a80d82d9d278383f9b4633b97ea9ec/psqlpy-0.9.3-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3c193eeca5dbceb770332197cc77a6e5e6ead210c34c4fff1d8cb8675e42e1f6", size = 4827112 }, + { url = "https://files.pythonhosted.org/packages/94/fe/248438003e8fcd3ba261da0de09b19a679f34c1b0d633222eef4c826729b/psqlpy-0.9.3-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:eab75d64e3b27a52bcbee690e8651a3b0dbaeef3028a19afad3a848ccb1b2951", size = 4860284 }, + { url = "https://files.pythonhosted.org/packages/7f/43/70111a42df62bf3d1b4053192cd0a3e55f18c2297f8c2732b6baae4b9703/psqlpy-0.9.3-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:a4f77af73b77bd73285efc0576a3ac087ee0aca39821371ea16d8e2bf9a16c90", size = 4980559 }, + { url = "https://files.pythonhosted.org/packages/c7/95/7089fd34c80e948d7f544c73f2ff1a0c20938024f61323e31a71bd85fe21/psqlpy-0.9.3-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:d7d4c6fcc343e0c0238e767caccb534d08b42d234d96e35aa5ce55d195f81281", size = 4237481 }, + { url = "https://files.pythonhosted.org/packages/5a/ce/5819a1f1e8f69639789b523d227ce07b989b1fc565fed2e3efb3efc67dc2/psqlpy-0.9.3-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:7d0220fa8127db04433c2be2a55da1042e4581531a36692c45668c3484bda437", size = 4474728 }, + { url = "https://files.pythonhosted.org/packages/2c/55/daeab10685010204bfb18246fbdab892eeb0a0db94de5bfb3cabd524344b/psqlpy-0.9.3-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bbdd68fa7f61e00d7f14f010cd4a7f242935cb9df560181bb947fde75c8c1af5", size = 4938916 }, + { url = "https://files.pythonhosted.org/packages/1d/35/2feb538ee0fa6d3e248727501b911dd99f20c58e65fd03c22d0d047bf7a5/psqlpy-0.9.3-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1c1e3c82aeec4c58328c723ef0755c2f02e2d30874ce850e0d7505db8bd90397", size = 4211573 }, + { url = "https://files.pythonhosted.org/packages/dd/4e/588b6c548363dab19bb58d808760f785058cef0af7649c16c80ddaad2eea/psqlpy-0.9.3-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c936f35fa793b421b116aa37040291ff5a33fc54e7f636e7ae946ec987070a94", size = 4809847 }, + { url = "https://files.pythonhosted.org/packages/d6/cc/2aef99aef78b0df24e43790786c0507bb63f4e6fa3e338fbdccb4e1b70e7/psqlpy-0.9.3-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:28ee25f791ccbad04269e22a21a79d52609d65298fe9b5300b8fe00fef42bab6", size = 4658403 }, + { url = "https://files.pythonhosted.org/packages/67/33/9d5094a57caddd9ffe2eb176e3e30a367312929323daa333bbc4dd11e3a4/psqlpy-0.9.3-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b3a1d90bb1da48649852e636612bb12fa0e3ec07f230446836195054598f0de4", size = 4789341 }, + { url = "https://files.pythonhosted.org/packages/52/5a/3506b0b6731b7e3f6a818fb3dcef7b2f8685254790a7a4bb0a1c9c2ac3cc/psqlpy-0.9.3-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a764bf80ce4e9d284bc11e255aff836894ee55e540c1be20120e67f855d8b07c", size = 4829845 }, + { url = "https://files.pythonhosted.org/packages/6b/fd/cd5e2929cfe3e357694118638fa227cd9dc6b4717b39590e731d0e3bd2ee/psqlpy-0.9.3-pp39-pypy39_pp73-musllinux_1_2_i686.whl", hash = "sha256:7939bfa80b649e650b9b4efe9064936837b14082851f1ad95c24548a105277cd", size = 4862164 }, + { url = "https://files.pythonhosted.org/packages/b2/af/e4ca9ccaed96d27c1c753b6d4f16264f0f7a2c752249ea25074f636a6734/psqlpy-0.9.3-pp39-pypy39_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:23496ed36433c88308f7e85f36091b203bb6c8c655ded639ce1626a1c8319afe", size = 4981643 }, +] + [[package]] name = "psycopg" version = "3.2.6" @@ -2476,15 +2564,16 @@ wheels = [ [[package]] name = "pydantic-settings" -version = "2.8.1" +version = "2.9.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pydantic" }, { name = "python-dotenv" }, + { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/88/82/c79424d7d8c29b994fb01d277da57b0a9b09cc03c3ff875f9bd8a86b2145/pydantic_settings-2.8.1.tar.gz", hash = "sha256:d5c663dfbe9db9d5e1c646b2e161da12f0d734d422ee56f567d0ea2cee4e8585", size = 83550 } +sdist = { url = "https://files.pythonhosted.org/packages/67/1d/42628a2c33e93f8e9acbde0d5d735fa0850f3e6a2f8cb1eb6c40b9a732ac/pydantic_settings-2.9.1.tar.gz", hash = "sha256:c509bf79d27563add44e8446233359004ed85066cd096d8b510f715e6ef5d268", size = 163234 } wheels = [ - { url = "https://files.pythonhosted.org/packages/0b/53/a64f03044927dc47aafe029c42a5b7aabc38dfb813475e0e1bf71c4a59d0/pydantic_settings-2.8.1-py3-none-any.whl", hash = "sha256:81942d5ac3d905f7f3ee1a70df5dfb62d5569c12f51a5a647defc1c3d9ee2e9c", size = 30839 }, + { url = "https://files.pythonhosted.org/packages/b6/5f/d6d641b490fd3ec2c4c13b4244d68deea3a1b970a97be64f34fb5504ff72/pydantic_settings-2.9.1-py3-none-any.whl", hash = "sha256:59b4f431b1defb26fe620c71a7d3968a710d719f5f4cdbbdb7926edeb770f6ef", size = 44356 }, ] [[package]] @@ -2993,11 +3082,11 @@ wheels = [ [[package]] name = "setuptools" -version = "78.1.0" +version = "78.1.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a9/5a/0db4da3bc908df06e5efae42b44e75c81dd52716e10192ff36d0c1c8e379/setuptools-78.1.0.tar.gz", hash = "sha256:18fd474d4a82a5f83dac888df697af65afa82dec7323d09c3e37d1f14288da54", size = 1367827 } +sdist = { url = "https://files.pythonhosted.org/packages/81/9c/42314ee079a3e9c24b27515f9fbc7a3c1d29992c33451779011c74488375/setuptools-78.1.1.tar.gz", hash = "sha256:fcc17fd9cd898242f6b4adfaca46137a9edef687f43e6f78469692a5e70d851d", size = 1368163 } wheels = [ - { url = "https://files.pythonhosted.org/packages/54/21/f43f0a1fa8b06b32812e0975981f4677d28e0f3271601dc88ac5a5b83220/setuptools-78.1.0-py3-none-any.whl", hash = "sha256:3e386e96793c8702ae83d17b853fb93d3e09ef82ec62722e61da5cd22376dcd8", size = 1256108 }, + { url = "https://files.pythonhosted.org/packages/90/99/158ad0609729111163fc1f674a5a42f2605371a4cf036d0441070e2f7455/setuptools-78.1.1-py3-none-any.whl", hash = "sha256:c3a9c4211ff4c309edb8b8c4f1cbfa7ae324c4ba9f91ff254e3d305b9fd54561", size = 1256462 }, ] [[package]] @@ -3606,6 +3695,9 @@ orjson = [ performance = [ { name = "sqlglot", extra = ["rs"] }, ] +psqlpy = [ + { name = "psqlpy" }, +] psycopg = [ { name = "psycopg", extra = ["binary", "pool"] }, ] @@ -3744,6 +3836,7 @@ requires-dist = [ { name = "msgspec", marker = "extra == 'msgspec'" }, { name = "oracledb", marker = "extra == 'oracledb'" }, { name = "orjson", marker = "extra == 'orjson'" }, + { name = "psqlpy", marker = "extra == 'psqlpy'" }, { name = "psycopg", extras = ["binary", "pool"], marker = "extra == 'psycopg'" }, { name = "pyarrow", marker = "extra == 'adbc'" }, { name = "pydantic", marker = "extra == 'pydantic'" }, @@ -3755,7 +3848,7 @@ requires-dist = [ { name = "typing-extensions" }, { name = "uuid-utils", marker = "extra == 'uuid'", specifier = ">=0.6.1" }, ] -provides-extras = ["adbc", "aioodbc", "aiosqlite", "asyncmy", "asyncpg", "bigquery", "duckdb", "fastapi", "flask", "litestar", "msgspec", "nanoid", "oracledb", "orjson", "performance", "psycopg", "pydantic", "pymssql", "pymysql", "spanner", "uuid"] +provides-extras = ["adbc", "aioodbc", "aiosqlite", "asyncmy", "asyncpg", "bigquery", "duckdb", "fastapi", "flask", "litestar", "msgspec", "nanoid", "oracledb", "orjson", "performance", "psqlpy", "psycopg", "pydantic", "pymssql", "pymysql", "spanner", "uuid"] [package.metadata.requires-dev] build = [{ name = "bump-my-version" }] @@ -4040,16 +4133,16 @@ wheels = [ [[package]] name = "uvicorn" -version = "0.34.1" +version = "0.34.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, { name = "h11" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/86/37/dd92f1f9cedb5eaf74d9999044306e06abe65344ff197864175dbbd91871/uvicorn-0.34.1.tar.gz", hash = "sha256:af981725fc4b7ffc5cb3b0e9eda6258a90c4b52cb2a83ce567ae0a7ae1757afc", size = 76755 } +sdist = { url = "https://files.pythonhosted.org/packages/a6/ae/9bbb19b9e1c450cf9ecaef06463e40234d98d95bf572fab11b4f19ae5ded/uvicorn-0.34.2.tar.gz", hash = "sha256:0e929828f6186353a80b58ea719861d2629d766293b6d19baf086ba31d4f3328", size = 76815 } wheels = [ - { url = "https://files.pythonhosted.org/packages/5f/38/a5801450940a858c102a7ad9e6150146a25406a119851c993148d56ab041/uvicorn-0.34.1-py3-none-any.whl", hash = "sha256:984c3a8c7ca18ebaad15995ee7401179212c59521e67bfc390c07fa2b8d2e065", size = 62404 }, + { url = "https://files.pythonhosted.org/packages/b1/4b/4cef6ce21a2aaca9d852a6e84ef4f135d99fcd74fa75105e2fc0c8308acd/uvicorn-0.34.2-py3-none-any.whl", hash = "sha256:deb49af569084536d269fe0a6d67e3754f104cf03aba7c11c40f01aadf33c403", size = 62483 }, ] [[package]]