diff --git a/Makefile b/Makefile index 6babf716..4b2a6261 100644 --- a/Makefile +++ b/Makefile @@ -21,6 +21,10 @@ test: chk test-sqla: uv run pytest -n 8 --cov pyathena --cov-report html --cov-report term tests/sqlalchemy/ +.PHONY: test-sqla-async +test-sqla-async: + uv run pytest -n 8 --cov pyathena --cov-report html --cov-report term tests/sqlalchemy/ --dburi async + .PHONY: tox tox: uvx tox@$(TOX_VERSION) -c pyproject.toml run diff --git a/docs/sqlalchemy.md b/docs/sqlalchemy.md index 4235bfd6..4e85d273 100644 --- a/docs/sqlalchemy.md +++ b/docs/sqlalchemy.md @@ -2,9 +2,14 @@ # SQLAlchemy -Install SQLAlchemy with `pip install "SQLAlchemy>=1.0.0"` or `pip install PyAthena[SQLAlchemy]`. +Install SQLAlchemy with `pip install "SQLAlchemy>=1.0.0"` or `pip install PyAthena[sqlalchemy]`. Supported SQLAlchemy is 1.0.0 or higher. +For async support (`create_async_engine`), install with `pip install PyAthena[aiosqlalchemy]` +(requires SQLAlchemy 2.0+). + +### Sync + ```python from sqlalchemy import func, select from sqlalchemy.engine import create_engine @@ -24,6 +29,48 @@ with engine.connect() as connection: print(result.scalar()) ``` +### Async + +```python +from sqlalchemy import text +from sqlalchemy.ext.asyncio import create_async_engine + +conn_str = "awsathena+aiorest://{aws_access_key_id}:{aws_secret_access_key}@athena.{region_name}.amazonaws.com:443/"\ + "{schema_name}?s3_staging_dir={s3_staging_dir}" +engine = create_async_engine(conn_str.format( + aws_access_key_id="YOUR_ACCESS_KEY_ID", + aws_secret_access_key="YOUR_SECRET_ACCESS_KEY", + region_name="us-west-2", + schema_name="default", + s3_staging_dir="s3://YOUR_S3_BUCKET/path/to/")) + +async def main(): + async with engine.connect() as connection: + result = await connection.execute(text("SELECT * FROM many_rows")) + print(result.fetchall()) + await engine.dispose() +``` + +SQLAlchemy's reflection API (`Table(..., autoload_with=)`, `inspect()`) is synchronous +internally, so it cannot be called directly on an async connection. Use `run_sync()` to +bridge the gap: + +```python +from sqlalchemy.sql.schema import Table, MetaData + +async with engine.connect() as connection: + # Table reflection + table = await connection.run_sync( + lambda sync_conn: Table("my_table", MetaData(), autoload_with=sync_conn) + ) + + # Schema inspection + import sqlalchemy + schemas = await connection.run_sync( + lambda sync_conn: sqlalchemy.inspect(sync_conn).get_schema_names() + ) +``` + ## Connection string The connection string has the following format: @@ -38,8 +85,16 @@ If you do not specify `aws_access_key_id` and `aws_secret_access_key` using inst awsathena+rest://:@athena.{region_name}.amazonaws.com:443/{schema_name}?s3_staging_dir={s3_staging_dir}&... ``` +For async, replace the driver portion (e.g. `+rest` with `+aiorest`): + +```text +awsathena+aiorest://:@athena.{region_name}.amazonaws.com:443/{schema_name}?s3_staging_dir={s3_staging_dir}&... +``` + ## Dialect & driver +### Sync + | Dialect | Driver | Schema | Cursor | |-----------|--------|------------------|------------------------| | awsathena | | awsathena | DefaultCursor | @@ -49,6 +104,18 @@ awsathena+rest://:@athena.{region_name}.amazonaws.com:443/{schema_name}?s3_stagi | awsathena | polars | awsathena+polars | {ref}`polars-cursor` | | awsathena | s3fs | awsathena+s3fs | {ref}`s3fs-cursor` | +### Async + +Requires `pip install PyAthena[aiosqlalchemy]` (SQLAlchemy 2.0+). + +| Dialect | Driver | Schema | Cursor | +|-----------|-----------|---------------------|------------------------------| +| awsathena | aiorest | awsathena+aiorest | DefaultCursor (async) | +| awsathena | aiopandas | awsathena+aiopandas | {ref}`pandas-cursor` (async) | +| awsathena | aioarrow | awsathena+aioarrow | {ref}`arrow-cursor` (async) | +| awsathena | aiopolars | awsathena+aiopolars | {ref}`polars-cursor` (async) | +| awsathena | aios3fs | awsathena+aios3fs | {ref}`s3fs-cursor` (async) | + ## Dialect options ### Table options diff --git a/pyathena/aio/sqlalchemy/__init__.py b/pyathena/aio/sqlalchemy/__init__.py new file mode 100644 index 00000000..40a96afc --- /dev/null +++ b/pyathena/aio/sqlalchemy/__init__.py @@ -0,0 +1 @@ +# -*- coding: utf-8 -*- diff --git a/pyathena/aio/sqlalchemy/arrow.py b/pyathena/aio/sqlalchemy/arrow.py new file mode 100644 index 00000000..91c9f0d5 --- /dev/null +++ b/pyathena/aio/sqlalchemy/arrow.py @@ -0,0 +1,55 @@ +# -*- coding: utf-8 -*- +from typing import TYPE_CHECKING + +from pyathena.aio.sqlalchemy.base import AthenaAioDialect +from pyathena.util import strtobool + +if TYPE_CHECKING: + from types import ModuleType + + +class AthenaAioArrowDialect(AthenaAioDialect): + """Async SQLAlchemy dialect for Amazon Athena with Apache Arrow result format. + + This dialect uses ``AioArrowCursor`` for native asyncio query execution + with Apache Arrow Table results. + + Connection URL Format: + ``awsathena+aioarrow://{access_key}:{secret_key}@athena.{region}.amazonaws.com/{schema}`` + + Query Parameters: + In addition to the base dialect parameters: + - unload: If "true", use UNLOAD for Parquet output + + Example: + >>> from sqlalchemy.ext.asyncio import create_async_engine + >>> engine = create_async_engine( + ... "awsathena+aioarrow://:@athena.us-west-2.amazonaws.com/default" + ... "?s3_staging_dir=s3://my-bucket/athena-results/" + ... "&unload=true" + ... ) + + See Also: + :class:`~pyathena.aio.arrow.cursor.AioArrowCursor`: The underlying async cursor. + :class:`~pyathena.aio.sqlalchemy.base.AthenaAioDialect`: Base async dialect. + """ + + driver = "aioarrow" + supports_statement_cache = True + + def create_connect_args(self, url): + from pyathena.aio.arrow.cursor import AioArrowCursor + + opts = super()._create_connect_args(url) + opts.update({"cursor_class": AioArrowCursor}) + cursor_kwargs = {} + if "unload" in opts: + cursor_kwargs.update({"unload": bool(strtobool(opts.pop("unload")))}) + if cursor_kwargs: + opts.update({"cursor_kwargs": cursor_kwargs}) + self._connect_options = opts + return [[], opts] + + @classmethod + def import_dbapi(cls) -> "ModuleType": + return super().import_dbapi() diff --git a/pyathena/aio/sqlalchemy/base.py b/pyathena/aio/sqlalchemy/base.py new file mode 100644 index 00000000..f2023ba4 --- /dev/null +++ b/pyathena/aio/sqlalchemy/base.py @@ -0,0 +1,218 @@ +# -*- coding: utf-8 -*- +from __future__ import annotations + +from collections import deque +from typing import TYPE_CHECKING, Any, Dict, List, MutableMapping, Optional, Tuple, Union, cast + +from sqlalchemy import pool +from sqlalchemy.engine import AdaptedConnection +from sqlalchemy.util.concurrency import await_only + +import pyathena +from pyathena.aio.connection import AioConnection +from pyathena.error import ( + DatabaseError, + DataError, + Error, + IntegrityError, + InterfaceError, + InternalError, + NotSupportedError, + OperationalError, + ProgrammingError, +) +from pyathena.sqlalchemy.base import AthenaDialect + +if TYPE_CHECKING: + from types import ModuleType + + from sqlalchemy import URL + + +class AsyncAdapt_pyathena_cursor: # noqa: N801 - follows SQLAlchemy's internal async adapter naming convention (e.g. AsyncAdapt_asyncpg_dbapi) + """Wraps any async PyAthena cursor with a sync DBAPI interface. + + SQLAlchemy's async engine uses greenlet-based ``await_only()`` to call + async methods from synchronous code running inside the greenlet context. + This adapter wraps an ``AioCursor`` (or variant) so that the dialect can + use a normal synchronous DBAPI interface while the underlying I/O is async. + """ + + server_side = False + __slots__ = ("_cursor", "_rows") + + def __init__(self, cursor: Any) -> None: + self._cursor = cursor + self._rows: deque[Any] = deque() + + @property + def description(self) -> Any: + return self._cursor.description + + @property + def rowcount(self) -> int: + return self._cursor.rowcount # type: ignore[no-any-return] + + def close(self) -> None: + self._cursor.close() + self._rows.clear() + + def execute(self, operation: str, parameters: Any = None, **kwargs: Any) -> Any: + result = await_only(self._cursor.execute(operation, parameters, **kwargs)) + if self._cursor.description: + self._rows = deque(await_only(self._cursor.fetchall())) + else: + self._rows.clear() + return result + + def executemany( + self, + operation: str, + seq_of_parameters: List[Optional[Union[Dict[str, Any], List[str]]]], + **kwargs: Any, + ) -> None: + for parameters in seq_of_parameters: + await_only(self._cursor.execute(operation, parameters, **kwargs)) + self._rows.clear() + + def fetchone(self) -> Any: + if self._rows: + return self._rows.popleft() + return None + + def fetchmany(self, size: Optional[int] = None) -> Any: + if size is None: + size = self._cursor.arraysize if hasattr(self._cursor, "arraysize") else 1 + return [self._rows.popleft() for _ in range(min(size, len(self._rows)))] + + def fetchall(self) -> Any: + items = list(self._rows) + self._rows.clear() + return items + + def setinputsizes(self, sizes: Any) -> None: + self._cursor.setinputsizes(sizes) + + # PyAthena-specific methods used by AthenaDialect reflection + def list_databases(self, *args: Any, **kwargs: Any) -> Any: + return await_only(self._cursor.list_databases(*args, **kwargs)) + + def get_table_metadata(self, *args: Any, **kwargs: Any) -> Any: + return await_only(self._cursor.get_table_metadata(*args, **kwargs)) + + def list_table_metadata(self, *args: Any, **kwargs: Any) -> Any: + return await_only(self._cursor.list_table_metadata(*args, **kwargs)) + + def __enter__(self) -> "AsyncAdapt_pyathena_cursor": + return self + + def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None: + self.close() + + +class AsyncAdapt_pyathena_connection(AdaptedConnection): # noqa: N801 - follows SQLAlchemy's internal async adapter naming convention (e.g. AsyncAdapt_asyncpg_dbapi) + """Wraps ``AioConnection`` with a sync DBAPI interface. + + This adapted connection delegates ``cursor()`` to the underlying + ``AioConnection`` and wraps each returned async cursor with + ``AsyncAdapt_pyathena_cursor``. + """ + + __slots__ = ("dbapi", "_connection") + + def __init__(self, dbapi: "AsyncAdapt_pyathena_dbapi", connection: AioConnection) -> None: + self.dbapi = dbapi + self._connection = connection + + @property + def driver_connection(self) -> AioConnection: + return self._connection # type: ignore[no-any-return] + + @property + def catalog_name(self) -> Optional[str]: + return self._connection.catalog_name # type: ignore[no-any-return] + + @property + def schema_name(self) -> Optional[str]: + return self._connection.schema_name # type: ignore[no-any-return] + + def cursor(self) -> AsyncAdapt_pyathena_cursor: + raw_cursor = self._connection.cursor() + return AsyncAdapt_pyathena_cursor(raw_cursor) + + def close(self) -> None: + self._connection.close() + + def commit(self) -> None: + self._connection.commit() + + def rollback(self) -> None: + pass + + +class AsyncAdapt_pyathena_dbapi: # noqa: N801 - follows SQLAlchemy's internal async adapter naming convention (e.g. AsyncAdapt_asyncpg_dbapi) + """Fake DBAPI module for the async SQLAlchemy engine. + + SQLAlchemy expects ``import_dbapi()`` to return a module-like object + with ``connect()``, ``paramstyle``, and the standard DBAPI exception + hierarchy. This class fulfils that contract while routing connections + through ``AioConnection``. + """ + + paramstyle = "pyformat" + + # DBAPI exception hierarchy + Error = Error + Warning = pyathena.Warning + InterfaceError = InterfaceError + DatabaseError = DatabaseError + InternalError = InternalError + OperationalError = OperationalError + ProgrammingError = ProgrammingError + IntegrityError = IntegrityError + DataError = DataError + NotSupportedError = NotSupportedError + + def connect(self, **kwargs: Any) -> AsyncAdapt_pyathena_connection: + connection = await_only(AioConnection.create(**kwargs)) + return AsyncAdapt_pyathena_connection(self, connection) + + +class AthenaAioDialect(AthenaDialect): + """Base async SQLAlchemy dialect for Amazon Athena. + + Extends the synchronous ``AthenaDialect`` with async capability + by setting ``is_async = True`` and providing an adapted DBAPI module + that wraps ``AioConnection`` and async cursors via greenlet-based + ``await_only()``. + + Subclasses (e.g. ``AthenaAioRestDialect``, ``AthenaAioPandasDialect``) + register concrete ``awsathena+aio*`` drivers. + + See Also: + :class:`~pyathena.sqlalchemy.base.AthenaDialect`: Synchronous base dialect. + :class:`~pyathena.aio.connection.AioConnection`: Native async connection. + """ + + is_async = True + supports_statement_cache = True + + @classmethod + def get_pool_class(cls, url: "URL") -> type: + return pool.AsyncAdaptedQueuePool + + @classmethod + def import_dbapi(cls) -> "ModuleType": + return AsyncAdapt_pyathena_dbapi() # type: ignore[return-value] + + @classmethod + def dbapi(cls) -> "ModuleType": # type: ignore[override] + return AsyncAdapt_pyathena_dbapi() # type: ignore[return-value] + + def create_connect_args(self, url: "URL") -> Tuple[Tuple[str], MutableMapping[str, Any]]: + opts = self._create_connect_args(url) + self._connect_options = opts + return cast(Tuple[str], ()), opts + + def get_driver_connection(self, connection: Any) -> Any: + return connection diff --git a/pyathena/aio/sqlalchemy/pandas.py b/pyathena/aio/sqlalchemy/pandas.py new file mode 100644 index 00000000..bd6bad71 --- /dev/null +++ b/pyathena/aio/sqlalchemy/pandas.py @@ -0,0 +1,61 @@ +# -*- coding: utf-8 -*- +from typing import TYPE_CHECKING + +from pyathena.aio.sqlalchemy.base import AthenaAioDialect +from pyathena.util import strtobool + +if TYPE_CHECKING: + from types import ModuleType + + +class AthenaAioPandasDialect(AthenaAioDialect): + """Async SQLAlchemy dialect for Amazon Athena with pandas DataFrame result format. + + This dialect uses ``AioPandasCursor`` for native asyncio query execution + with pandas DataFrame results. + + Connection URL Format: + ``awsathena+aiopandas://{access_key}:{secret_key}@athena.{region}.amazonaws.com/{schema}`` + + Query Parameters: + In addition to the base dialect parameters: + - unload: If "true", use UNLOAD for Parquet output + - engine: CSV parsing engine ("c", "python", or "pyarrow") + - chunksize: Number of rows per chunk for memory-efficient processing + + Example: + >>> from sqlalchemy.ext.asyncio import create_async_engine + >>> engine = create_async_engine( + ... "awsathena+aiopandas://:@athena.us-west-2.amazonaws.com/default" + ... "?s3_staging_dir=s3://my-bucket/athena-results/" + ... "&unload=true&chunksize=10000" + ... ) + + See Also: + :class:`~pyathena.aio.pandas.cursor.AioPandasCursor`: The underlying async cursor. + :class:`~pyathena.aio.sqlalchemy.base.AthenaAioDialect`: Base async dialect. + """ + + driver = "aiopandas" + supports_statement_cache = True + + def create_connect_args(self, url): + from pyathena.aio.pandas.cursor import AioPandasCursor + + opts = super()._create_connect_args(url) + opts.update({"cursor_class": AioPandasCursor}) + cursor_kwargs = {} + if "unload" in opts: + cursor_kwargs.update({"unload": bool(strtobool(opts.pop("unload")))}) + if "engine" in opts: + cursor_kwargs.update({"engine": opts.pop("engine")}) + if "chunksize" in opts: + cursor_kwargs.update({"chunksize": int(opts.pop("chunksize"))}) # type: ignore + if cursor_kwargs: + opts.update({"cursor_kwargs": cursor_kwargs}) + self._connect_options = opts + return [[], opts] + + @classmethod + def import_dbapi(cls) -> "ModuleType": + return super().import_dbapi() diff --git a/pyathena/aio/sqlalchemy/polars.py b/pyathena/aio/sqlalchemy/polars.py new file mode 100644 index 00000000..d7daa7e9 --- /dev/null +++ b/pyathena/aio/sqlalchemy/polars.py @@ -0,0 +1,55 @@ +# -*- coding: utf-8 -*- +from typing import TYPE_CHECKING + +from pyathena.aio.sqlalchemy.base import AthenaAioDialect +from pyathena.util import strtobool + +if TYPE_CHECKING: + from types import ModuleType + + +class AthenaAioPolarsDialect(AthenaAioDialect): + """Async SQLAlchemy dialect for Amazon Athena with Polars DataFrame result format. + + This dialect uses ``AioPolarsCursor`` for native asyncio query execution + with Polars DataFrame results. + + Connection URL Format: + ``awsathena+aiopolars://{access_key}:{secret_key}@athena.{region}.amazonaws.com/{schema}`` + + Query Parameters: + In addition to the base dialect parameters: + - unload: If "true", use UNLOAD for Parquet output + + Example: + >>> from sqlalchemy.ext.asyncio import create_async_engine + >>> engine = create_async_engine( + ... "awsathena+aiopolars://:@athena.us-west-2.amazonaws.com/default" + ... "?s3_staging_dir=s3://my-bucket/athena-results/" + ... "&unload=true" + ... ) + + See Also: + :class:`~pyathena.aio.polars.cursor.AioPolarsCursor`: The underlying async cursor. + :class:`~pyathena.aio.sqlalchemy.base.AthenaAioDialect`: Base async dialect. + """ + + driver = "aiopolars" + supports_statement_cache = True + + def create_connect_args(self, url): + from pyathena.aio.polars.cursor import AioPolarsCursor + + opts = super()._create_connect_args(url) + opts.update({"cursor_class": AioPolarsCursor}) + cursor_kwargs = {} + if "unload" in opts: + cursor_kwargs.update({"unload": bool(strtobool(opts.pop("unload")))}) + if cursor_kwargs: + opts.update({"cursor_kwargs": cursor_kwargs}) + self._connect_options = opts + return [[], opts] + + @classmethod + def import_dbapi(cls) -> "ModuleType": + return super().import_dbapi() diff --git a/pyathena/aio/sqlalchemy/rest.py b/pyathena/aio/sqlalchemy/rest.py new file mode 100644 index 00000000..86c93f5c --- /dev/null +++ b/pyathena/aio/sqlalchemy/rest.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- +from typing import TYPE_CHECKING + +from pyathena.aio.sqlalchemy.base import AthenaAioDialect + +if TYPE_CHECKING: + from types import ModuleType + + +class AthenaAioRestDialect(AthenaAioDialect): + """Async SQLAlchemy dialect for Amazon Athena using the standard REST API cursor. + + This dialect uses ``AioCursor`` for native asyncio query execution. + Results are returned as Python tuples with type conversion handled by + the default converter. + + Connection URL Format: + ``awsathena+aiorest://{access_key}:{secret_key}@athena.{region}.amazonaws.com/{schema}`` + + Example: + >>> from sqlalchemy.ext.asyncio import create_async_engine + >>> engine = create_async_engine( + ... "awsathena+aiorest://:@athena.us-west-2.amazonaws.com/default" + ... "?s3_staging_dir=s3://my-bucket/athena-results/" + ... ) + + See Also: + :class:`~pyathena.aio.cursor.AioCursor`: The underlying async cursor. + :class:`~pyathena.aio.sqlalchemy.base.AthenaAioDialect`: Base async dialect. + """ + + driver = "aiorest" + supports_statement_cache = True + + @classmethod + def import_dbapi(cls) -> "ModuleType": + return super().import_dbapi() diff --git a/pyathena/aio/sqlalchemy/s3fs.py b/pyathena/aio/sqlalchemy/s3fs.py new file mode 100644 index 00000000..945b016e --- /dev/null +++ b/pyathena/aio/sqlalchemy/s3fs.py @@ -0,0 +1,44 @@ +# -*- coding: utf-8 -*- +from typing import TYPE_CHECKING + +from pyathena.aio.sqlalchemy.base import AthenaAioDialect + +if TYPE_CHECKING: + from types import ModuleType + + +class AthenaAioS3FSDialect(AthenaAioDialect): + """Async SQLAlchemy dialect for PyAthena with S3FS cursor. + + This dialect uses ``AioS3FSCursor`` for native asyncio query execution + with S3 filesystem-based CSV result reading. + + Connection URL Format: + ``awsathena+aios3fs://{access_key}:{secret_key}@athena.{region}.amazonaws.com/{schema}`` + + Example: + >>> from sqlalchemy.ext.asyncio import create_async_engine + >>> engine = create_async_engine( + ... "awsathena+aios3fs://:@athena.us-east-1.amazonaws.com/database" + ... "?s3_staging_dir=s3://bucket/path" + ... ) + + See Also: + :class:`~pyathena.aio.s3fs.cursor.AioS3FSCursor`: The underlying async cursor. + :class:`~pyathena.aio.sqlalchemy.base.AthenaAioDialect`: Base async dialect. + """ + + driver = "aios3fs" + supports_statement_cache = True + + def create_connect_args(self, url): + from pyathena.aio.s3fs.cursor import AioS3FSCursor + + opts = super()._create_connect_args(url) + opts.update({"cursor_class": AioS3FSCursor}) + self._connect_options = opts + return [[], opts] + + @classmethod + def import_dbapi(cls) -> "ModuleType": + return super().import_dbapi() diff --git a/pyproject.toml b/pyproject.toml index 6106c255..d591f43f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,9 +42,17 @@ awsathena = "pyathena.sqlalchemy.base:AthenaDialect" "awsathena.arrow" = "pyathena.sqlalchemy.arrow:AthenaArrowDialect" "awsathena.polars" = "pyathena.sqlalchemy.polars:AthenaPolarsDialect" "awsathena.s3fs" = "pyathena.sqlalchemy.s3fs:AthenaS3FSDialect" +"awsathena.aiorest" = "pyathena.aio.sqlalchemy.rest:AthenaAioRestDialect" +"awsathena.aiopandas" = "pyathena.aio.sqlalchemy.pandas:AthenaAioPandasDialect" +"awsathena.aioarrow" = "pyathena.aio.sqlalchemy.arrow:AthenaAioArrowDialect" +"awsathena.aiopolars" = "pyathena.aio.sqlalchemy.polars:AthenaAioPolarsDialect" +"awsathena.aios3fs" = "pyathena.aio.sqlalchemy.s3fs:AthenaAioS3FSDialect" [project.optional-dependencies] sqlalchemy = ["sqlalchemy>=1.0.0"] +aiosqlalchemy = [ + "sqlalchemy[asyncio]>=2.0.0", +] pandas = [ "pandas>=1.3.0; python_version<'3.13'", "pandas>=2.3.0; python_version>='3.13'", @@ -59,7 +67,7 @@ polars = [ [dependency-groups] dev = [ - "sqlalchemy>=1.0.0", + "sqlalchemy[asyncio]>=1.0.0", "pandas>=1.3.0; python_version<'3.13'", "pandas>=2.3.0; python_version>='3.13'", "numpy>=1.26.0; python_version<'3.13'", @@ -189,6 +197,7 @@ commands = uv sync --group dev make test make test-sqla + make test-sqla-async passenv = TOXENV AWS_* diff --git a/tests/__init__.py b/tests/__init__.py index 5a35f4f8..4cb72813 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -7,6 +7,10 @@ "awsathena+rest://athena.{region_name}.amazonaws.com:443/" "{schema_name}?s3_staging_dir={s3_staging_dir}&location={location}" ) +ASYNC_SQLALCHEMY_CONNECTION_STRING = ( + "awsathena+aiorest://athena.{region_name}.amazonaws.com:443/" + "{schema_name}?s3_staging_dir={s3_staging_dir}&location={location}" +) class Env: diff --git a/tests/pyathena/aio/sqlalchemy/__init__.py b/tests/pyathena/aio/sqlalchemy/__init__.py new file mode 100644 index 00000000..40a96afc --- /dev/null +++ b/tests/pyathena/aio/sqlalchemy/__init__.py @@ -0,0 +1 @@ +# -*- coding: utf-8 -*- diff --git a/tests/pyathena/aio/sqlalchemy/test_base.py b/tests/pyathena/aio/sqlalchemy/test_base.py new file mode 100644 index 00000000..da2f6b2e --- /dev/null +++ b/tests/pyathena/aio/sqlalchemy/test_base.py @@ -0,0 +1,101 @@ +# -*- coding: utf-8 -*- +import pytest +import sqlalchemy +from sqlalchemy import text +from sqlalchemy.sql.schema import MetaData, Table + +from tests import ENV + + +class TestAsyncSQLAlchemyAthena: + @pytest.mark.parametrize( + "async_engine", + [ + {"driver": "aiorest"}, + {"driver": "aiopandas"}, + {"driver": "aioarrow"}, + {"driver": "aiopolars"}, + {"driver": "aios3fs"}, + ], + indirect=True, + ) + async def test_basic_query(self, async_engine): + engine, conn = async_engine + rows = (await conn.execute(text("SELECT * FROM one_row"))).fetchall() + assert len(rows) == 1 + assert rows[0].number_of_rows == 1 + assert len(rows[0]) == 1 + + async def test_unicode(self, async_engine): + _, conn = async_engine + unicode_str = "密林" + returned_str = ( + await conn.execute( + sqlalchemy.select( + sqlalchemy.sql.expression.bindparam( + "あまぞん", unicode_str, type_=sqlalchemy.types.String() + ) + ) + ) + ).scalar() + assert returned_str == unicode_str + + async def test_reflect_table(self, async_engine): + _, conn = async_engine + one_row = await conn.run_sync( + lambda sync_conn: Table("one_row", MetaData(schema=ENV.schema), autoload_with=sync_conn) + ) + assert len(one_row.c) == 1 + assert one_row.c.number_of_rows is not None + assert one_row.comment == "table comment" + + async def test_reflect_schemas(self, async_engine): + _, conn = async_engine + + def _inspect(sync_conn): + insp = sqlalchemy.inspect(sync_conn) + return insp.get_schema_names() + + schemas = await conn.run_sync(_inspect) + assert ENV.schema in schemas + assert "default" in schemas + + async def test_get_table_names(self, async_engine): + _, conn = async_engine + + def _inspect(sync_conn): + insp = sqlalchemy.inspect(sync_conn) + return insp.get_table_names(schema=ENV.schema) + + table_names = await conn.run_sync(_inspect) + assert "many_rows" in table_names + + async def test_has_table(self, async_engine): + _, conn = async_engine + + def _inspect(sync_conn): + insp = sqlalchemy.inspect(sync_conn) + return ( + insp.has_table("one_row", schema=ENV.schema), + insp.has_table("this_table_does_not_exist", schema=ENV.schema), + ) + + exists, not_exists = await conn.run_sync(_inspect) + assert exists + assert not not_exists + + async def test_get_columns(self, async_engine): + _, conn = async_engine + + def _inspect(sync_conn): + insp = sqlalchemy.inspect(sync_conn) + return insp.get_columns(table_name="one_row", schema=ENV.schema) + + columns = await conn.run_sync(_inspect) + actual = columns[0] + assert actual["name"] == "number_of_rows" + assert isinstance(actual["type"], sqlalchemy.types.INTEGER) + assert actual["nullable"] + assert actual["default"] is None + assert not actual["autoincrement"] + assert actual["comment"] == "some comment" diff --git a/tests/pyathena/conftest.py b/tests/pyathena/conftest.py index c244bae1..e9cabb02 100644 --- a/tests/pyathena/conftest.py +++ b/tests/pyathena/conftest.py @@ -6,8 +6,9 @@ import boto3 import pytest import sqlalchemy +from sqlalchemy.ext.asyncio import create_async_engine as _create_async_engine -from tests import ENV, SQLALCHEMY_CONNECTION_STRING +from tests import ASYNC_SQLALCHEMY_CONNECTION_STRING, ENV, SQLALCHEMY_CONNECTION_STRING from tests.pyathena.util import read_query @@ -72,7 +73,8 @@ def connect(schema_name="default", **kwargs): def create_engine(**kwargs): - conn_str = SQLALCHEMY_CONNECTION_STRING + driver = kwargs.pop("driver", "rest") + conn_str = SQLALCHEMY_CONNECTION_STRING.replace("+rest", f"+{driver}") for arg in [ "bucket_count", "cluster", @@ -102,6 +104,20 @@ def create_engine(**kwargs): ) +def create_async_engine(**kwargs): + driver = kwargs.pop("driver", "aiorest") + conn_str = ASYNC_SQLALCHEMY_CONNECTION_STRING.replace("+aiorest", f"+{driver}") + return _create_async_engine( + conn_str.format( + region_name=ENV.region_name, + schema_name=ENV.schema, + s3_staging_dir=ENV.s3_staging_dir, + location=ENV.s3_staging_dir, + **kwargs, + ) + ) + + def _cursor(cursor_class, request): if not hasattr(request, "param"): setattr(request, "param", {}) # noqa: B010 @@ -230,6 +246,18 @@ def engine(request): engine_.dispose() +@pytest.fixture +async def async_engine(request): + if not hasattr(request, "param"): + setattr(request, "param", {}) # noqa: B010 + engine_ = create_async_engine(**request.param) + try: + async with engine_.connect() as conn: + yield engine_, conn + finally: + await engine_.dispose() + + @pytest.fixture def formatter(): from pyathena.formatter import DefaultParameterFormatter diff --git a/tests/pyathena/sqlalchemy/test_base.py b/tests/pyathena/sqlalchemy/test_base.py index 2d68e765..73e3442c 100644 --- a/tests/pyathena/sqlalchemy/test_base.py +++ b/tests/pyathena/sqlalchemy/test_base.py @@ -30,6 +30,17 @@ class TestSQLAlchemyAthena: + @pytest.mark.parametrize( + "engine", + [ + {"driver": "rest"}, + {"driver": "pandas"}, + {"driver": "arrow"}, + {"driver": "polars"}, + {"driver": "s3fs"}, + ], + indirect=True, + ) def test_basic_query(self, engine): engine, conn = engine rows = conn.execute(sqlalchemy.text("SELECT * FROM one_row")).fetchall() diff --git a/tests/sqlalchemy/__init__.py b/tests/sqlalchemy/__init__.py index 7e3c1b4c..fd74e829 100644 --- a/tests/sqlalchemy/__init__.py +++ b/tests/sqlalchemy/__init__.py @@ -6,3 +6,8 @@ registry.register("awsathena.pandas", "pyathena.sqlalchemy.pandas", "AthenaPandasDialect") registry.register("awsathena.arrow", "pyathena.sqlalchemy.arrow", "AthenaArrowDialect") registry.register("awsathena.s3fs", "pyathena.sqlalchemy.s3fs", "AthenaS3FSDialect") +registry.register("awsathena.aiorest", "pyathena.aio.sqlalchemy.rest", "AthenaAioRestDialect") +registry.register("awsathena.aiopandas", "pyathena.aio.sqlalchemy.pandas", "AthenaAioPandasDialect") +registry.register("awsathena.aioarrow", "pyathena.aio.sqlalchemy.arrow", "AthenaAioArrowDialect") +registry.register("awsathena.aiopolars", "pyathena.aio.sqlalchemy.polars", "AthenaAioPolarsDialect") +registry.register("awsathena.aios3fs", "pyathena.aio.sqlalchemy.s3fs", "AthenaAioS3FSDialect") diff --git a/tests/sqlalchemy/conftest.py b/tests/sqlalchemy/conftest.py index f5ee2ca7..ca0f0b26 100644 --- a/tests/sqlalchemy/conftest.py +++ b/tests/sqlalchemy/conftest.py @@ -13,13 +13,15 @@ temp_table_keyword_args, ) -from tests import ENV, SQLALCHEMY_CONNECTION_STRING +from tests import ASYNC_SQLALCHEMY_CONNECTION_STRING, ENV, SQLALCHEMY_CONNECTION_STRING def pytest_sessionstart(session): - conn_str = ( - SQLALCHEMY_CONNECTION_STRING + "&tblproperties=" + quote_plus("'table_type'='ICEBERG'") + use_async = session.config.getoption("--dburi", None) == ["async"] + base_conn_str = ( + ASYNC_SQLALCHEMY_CONNECTION_STRING if use_async else SQLALCHEMY_CONNECTION_STRING ) + conn_str = base_conn_str + "&tblproperties=" + quote_plus("'table_type'='ICEBERG'") session.config.option.dburi = [ conn_str.format( region_name=ENV.region_name, diff --git a/uv.lock b/uv.lock index dfb39569..cf663da4 100644 --- a/uv.lock +++ b/uv.lock @@ -287,48 +287,62 @@ wheels = [ [[package]] name = "greenlet" -version = "3.1.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2f/ff/df5fede753cc10f6a5be0931204ea30c35fa2f2ea7a35b25bdaf4fe40e46/greenlet-3.1.1.tar.gz", hash = "sha256:4ce3ac6cdb6adf7946475d7ef31777c26d94bccc377e070a7986bd2d5c515467", size = 186022, upload-time = "2024-09-20T18:21:04.506Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/25/90/5234a78dc0ef6496a6eb97b67a42a8e96742a56f7dc808cb954a85390448/greenlet-3.1.1-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:0bbae94a29c9e5c7e4a2b7f0aae5c17e8e90acbfd3bf6270eeba60c39fce3563", size = 271235, upload-time = "2024-09-20T17:07:18.761Z" }, - { url = "https://files.pythonhosted.org/packages/7c/16/cd631fa0ab7d06ef06387135b7549fdcc77d8d859ed770a0d28e47b20972/greenlet-3.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fde093fb93f35ca72a556cf72c92ea3ebfda3d79fc35bb19fbe685853869a83", size = 637168, upload-time = "2024-09-20T17:36:43.774Z" }, - { url = "https://files.pythonhosted.org/packages/2f/b1/aed39043a6fec33c284a2c9abd63ce191f4f1a07319340ffc04d2ed3256f/greenlet-3.1.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:36b89d13c49216cadb828db8dfa6ce86bbbc476a82d3a6c397f0efae0525bdd0", size = 648826, upload-time = "2024-09-20T17:39:16.921Z" }, - { url = "https://files.pythonhosted.org/packages/fb/2f/3850b867a9af519794784a7eeed1dd5bc68ffbcc5b28cef703711025fd0a/greenlet-3.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:93147c513fac16385d1036b7e5b102c7fbbdb163d556b791f0f11eada7ba65dc", size = 643295, upload-time = "2024-09-20T17:08:37.951Z" }, - { url = "https://files.pythonhosted.org/packages/cf/69/79e4d63b9387b48939096e25115b8af7cd8a90397a304f92436bcb21f5b2/greenlet-3.1.1-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:da7a9bff22ce038e19bf62c4dd1ec8391062878710ded0a845bcf47cc0200617", size = 599544, upload-time = "2024-09-20T17:08:27.894Z" }, - { url = "https://files.pythonhosted.org/packages/46/1d/44dbcb0e6c323bd6f71b8c2f4233766a5faf4b8948873225d34a0b7efa71/greenlet-3.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b2795058c23988728eec1f36a4e5e4ebad22f8320c85f3587b539b9ac84128d7", size = 1125456, upload-time = "2024-09-20T17:44:11.755Z" }, - { url = "https://files.pythonhosted.org/packages/e0/1d/a305dce121838d0278cee39d5bb268c657f10a5363ae4b726848f833f1bb/greenlet-3.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:ed10eac5830befbdd0c32f83e8aa6288361597550ba669b04c48f0f9a2c843c6", size = 1149111, upload-time = "2024-09-20T17:09:22.104Z" }, - { url = "https://files.pythonhosted.org/packages/96/28/d62835fb33fb5652f2e98d34c44ad1a0feacc8b1d3f1aecab035f51f267d/greenlet-3.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:77c386de38a60d1dfb8e55b8c1101d68c79dfdd25c7095d51fec2dd800892b80", size = 298392, upload-time = "2024-09-20T17:28:51.988Z" }, - { url = "https://files.pythonhosted.org/packages/28/62/1c2665558618553c42922ed47a4e6d6527e2fa3516a8256c2f431c5d0441/greenlet-3.1.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:e4d333e558953648ca09d64f13e6d8f0523fa705f51cae3f03b5983489958c70", size = 272479, upload-time = "2024-09-20T17:07:22.332Z" }, - { url = "https://files.pythonhosted.org/packages/76/9d/421e2d5f07285b6e4e3a676b016ca781f63cfe4a0cd8eaecf3fd6f7a71ae/greenlet-3.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09fc016b73c94e98e29af67ab7b9a879c307c6731a2c9da0db5a7d9b7edd1159", size = 640404, upload-time = "2024-09-20T17:36:45.588Z" }, - { url = "https://files.pythonhosted.org/packages/e5/de/6e05f5c59262a584e502dd3d261bbdd2c97ab5416cc9c0b91ea38932a901/greenlet-3.1.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d5e975ca70269d66d17dd995dafc06f1b06e8cb1ec1e9ed54c1d1e4a7c4cf26e", size = 652813, upload-time = "2024-09-20T17:39:19.052Z" }, - { url = "https://files.pythonhosted.org/packages/15/85/72f77fc02d00470c86a5c982b8daafdf65d38aefbbe441cebff3bf7037fc/greenlet-3.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e347b3bfcf985a05e8c0b7d462ba6f15b1ee1c909e2dcad795e49e91b152c383", size = 647831, upload-time = "2024-09-20T17:08:40.577Z" }, - { url = "https://files.pythonhosted.org/packages/f7/4b/1c9695aa24f808e156c8f4813f685d975ca73c000c2a5056c514c64980f6/greenlet-3.1.1-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e8f8c9cb53cdac7ba9793c276acd90168f416b9ce36799b9b885790f8ad6c0a", size = 602413, upload-time = "2024-09-20T17:08:31.728Z" }, - { url = "https://files.pythonhosted.org/packages/76/70/ad6e5b31ef330f03b12559d19fda2606a522d3849cde46b24f223d6d1619/greenlet-3.1.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:62ee94988d6b4722ce0028644418d93a52429e977d742ca2ccbe1c4f4a792511", size = 1129619, upload-time = "2024-09-20T17:44:14.222Z" }, - { url = "https://files.pythonhosted.org/packages/f4/fb/201e1b932e584066e0f0658b538e73c459b34d44b4bd4034f682423bc801/greenlet-3.1.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1776fd7f989fc6b8d8c8cb8da1f6b82c5814957264d1f6cf818d475ec2bf6395", size = 1155198, upload-time = "2024-09-20T17:09:23.903Z" }, - { url = "https://files.pythonhosted.org/packages/12/da/b9ed5e310bb8b89661b80cbcd4db5a067903bbcd7fc854923f5ebb4144f0/greenlet-3.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:48ca08c771c268a768087b408658e216133aecd835c0ded47ce955381105ba39", size = 298930, upload-time = "2024-09-20T17:25:18.656Z" }, - { url = "https://files.pythonhosted.org/packages/7d/ec/bad1ac26764d26aa1353216fcbfa4670050f66d445448aafa227f8b16e80/greenlet-3.1.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:4afe7ea89de619adc868e087b4d2359282058479d7cfb94970adf4b55284574d", size = 274260, upload-time = "2024-09-20T17:08:07.301Z" }, - { url = "https://files.pythonhosted.org/packages/66/d4/c8c04958870f482459ab5956c2942c4ec35cac7fe245527f1039837c17a9/greenlet-3.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f406b22b7c9a9b4f8aa9d2ab13d6ae0ac3e85c9a809bd590ad53fed2bf70dc79", size = 649064, upload-time = "2024-09-20T17:36:47.628Z" }, - { url = "https://files.pythonhosted.org/packages/51/41/467b12a8c7c1303d20abcca145db2be4e6cd50a951fa30af48b6ec607581/greenlet-3.1.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c3a701fe5a9695b238503ce5bbe8218e03c3bcccf7e204e455e7462d770268aa", size = 663420, upload-time = "2024-09-20T17:39:21.258Z" }, - { url = "https://files.pythonhosted.org/packages/57/5c/7c6f50cb12be092e1dccb2599be5a942c3416dbcfb76efcf54b3f8be4d8d/greenlet-3.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:99cfaa2110534e2cf3ba31a7abcac9d328d1d9f1b95beede58294a60348fba36", size = 660105, upload-time = "2024-09-20T17:08:42.048Z" }, - { url = "https://files.pythonhosted.org/packages/f1/66/033e58a50fd9ec9df00a8671c74f1f3a320564c6415a4ed82a1c651654ba/greenlet-3.1.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1443279c19fca463fc33e65ef2a935a5b09bb90f978beab37729e1c3c6c25fe9", size = 613077, upload-time = "2024-09-20T17:08:33.707Z" }, - { url = "https://files.pythonhosted.org/packages/19/c5/36384a06f748044d06bdd8776e231fadf92fc896bd12cb1c9f5a1bda9578/greenlet-3.1.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b7cede291382a78f7bb5f04a529cb18e068dd29e0fb27376074b6d0317bf4dd0", size = 1135975, upload-time = "2024-09-20T17:44:15.989Z" }, - { url = "https://files.pythonhosted.org/packages/38/f9/c0a0eb61bdf808d23266ecf1d63309f0e1471f284300ce6dac0ae1231881/greenlet-3.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:23f20bb60ae298d7d8656c6ec6db134bca379ecefadb0b19ce6f19d1f232a942", size = 1163955, upload-time = "2024-09-20T17:09:25.539Z" }, - { url = "https://files.pythonhosted.org/packages/43/21/a5d9df1d21514883333fc86584c07c2b49ba7c602e670b174bd73cfc9c7f/greenlet-3.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:7124e16b4c55d417577c2077be379514321916d5790fa287c9ed6f23bd2ffd01", size = 299655, upload-time = "2024-09-20T17:21:22.427Z" }, - { url = "https://files.pythonhosted.org/packages/f3/57/0db4940cd7bb461365ca8d6fd53e68254c9dbbcc2b452e69d0d41f10a85e/greenlet-3.1.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:05175c27cb459dcfc05d026c4232f9de8913ed006d42713cb8a5137bd49375f1", size = 272990, upload-time = "2024-09-20T17:08:26.312Z" }, - { url = "https://files.pythonhosted.org/packages/1c/ec/423d113c9f74e5e402e175b157203e9102feeb7088cee844d735b28ef963/greenlet-3.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:935e943ec47c4afab8965954bf49bfa639c05d4ccf9ef6e924188f762145c0ff", size = 649175, upload-time = "2024-09-20T17:36:48.983Z" }, - { url = "https://files.pythonhosted.org/packages/a9/46/ddbd2db9ff209186b7b7c621d1432e2f21714adc988703dbdd0e65155c77/greenlet-3.1.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:667a9706c970cb552ede35aee17339a18e8f2a87a51fba2ed39ceeeb1004798a", size = 663425, upload-time = "2024-09-20T17:39:22.705Z" }, - { url = "https://files.pythonhosted.org/packages/d9/42/b87bc2a81e3a62c3de2b0d550bf91a86939442b7ff85abb94eec3fc0e6aa/greenlet-3.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:efc0f674aa41b92da8c49e0346318c6075d734994c3c4e4430b1c3f853e498e4", size = 660347, upload-time = "2024-09-20T17:08:45.56Z" }, - { url = "https://files.pythonhosted.org/packages/37/fa/71599c3fd06336cdc3eac52e6871cfebab4d9d70674a9a9e7a482c318e99/greenlet-3.1.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0153404a4bb921f0ff1abeb5ce8a5131da56b953eda6e14b88dc6bbc04d2049e", size = 615583, upload-time = "2024-09-20T17:08:36.85Z" }, - { url = "https://files.pythonhosted.org/packages/4e/96/e9ef85de031703ee7a4483489b40cf307f93c1824a02e903106f2ea315fe/greenlet-3.1.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:275f72decf9932639c1c6dd1013a1bc266438eb32710016a1c742df5da6e60a1", size = 1133039, upload-time = "2024-09-20T17:44:18.287Z" }, - { url = "https://files.pythonhosted.org/packages/87/76/b2b6362accd69f2d1889db61a18c94bc743e961e3cab344c2effaa4b4a25/greenlet-3.1.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:c4aab7f6381f38a4b42f269057aee279ab0fc7bf2e929e3d4abfae97b682a12c", size = 1160716, upload-time = "2024-09-20T17:09:27.112Z" }, - { url = "https://files.pythonhosted.org/packages/1f/1b/54336d876186920e185066d8c3024ad55f21d7cc3683c856127ddb7b13ce/greenlet-3.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:b42703b1cf69f2aa1df7d1030b9d77d3e584a70755674d60e710f0af570f3761", size = 299490, upload-time = "2024-09-20T17:17:09.501Z" }, - { url = "https://files.pythonhosted.org/packages/5f/17/bea55bf36990e1638a2af5ba10c1640273ef20f627962cf97107f1e5d637/greenlet-3.1.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f1695e76146579f8c06c1509c7ce4dfe0706f49c6831a817ac04eebb2fd02011", size = 643731, upload-time = "2024-09-20T17:36:50.376Z" }, - { url = "https://files.pythonhosted.org/packages/78/d2/aa3d2157f9ab742a08e0fd8f77d4699f37c22adfbfeb0c610a186b5f75e0/greenlet-3.1.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7876452af029456b3f3549b696bb36a06db7c90747740c5302f74a9e9fa14b13", size = 649304, upload-time = "2024-09-20T17:39:24.55Z" }, - { url = "https://files.pythonhosted.org/packages/05/79/e15408220bbb989469c8871062c97c6c9136770657ba779711b90870d867/greenlet-3.1.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8320f64b777d00dd7ccdade271eaf0cad6636343293a25074cc5566160e4de7b", size = 642506, upload-time = "2024-09-20T17:08:47.852Z" }, - { url = "https://files.pythonhosted.org/packages/18/87/470e01a940307796f1d25f8167b551a968540fbe0551c0ebb853cb527dd6/greenlet-3.1.1-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6510bf84a6b643dabba74d3049ead221257603a253d0a9873f55f6a59a65f822", size = 602753, upload-time = "2024-09-20T17:08:38.079Z" }, - { url = "https://files.pythonhosted.org/packages/e2/72/576815ba674eddc3c25028238f74d7b8068902b3968cbe456771b166455e/greenlet-3.1.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:04b013dc07c96f83134b1e99888e7a79979f1a247e2a9f59697fa14b5862ed01", size = 1122731, upload-time = "2024-09-20T17:44:20.556Z" }, - { url = "https://files.pythonhosted.org/packages/ac/38/08cc303ddddc4b3d7c628c3039a61a3aae36c241ed01393d00c2fd663473/greenlet-3.1.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:411f015496fec93c1c8cd4e5238da364e1da7a124bcb293f085bf2860c32c6f6", size = 1142112, upload-time = "2024-09-20T17:09:28.753Z" }, +version = "3.3.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a3/51/1664f6b78fc6ebbd98019a1fd730e83fa78f2db7058f72b1463d3612b8db/greenlet-3.3.2.tar.gz", hash = "sha256:2eaf067fc6d886931c7962e8c6bede15d2f01965560f3359b27c80bde2d151f2", size = 188267, upload-time = "2026-02-20T20:54:15.531Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/3f/9859f655d11901e7b2996c6e3d33e0caa9a1d4572c3bc61ed0faa64b2f4c/greenlet-3.3.2-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:9bc885b89709d901859cf95179ec9f6bb67a3d2bb1f0e88456461bd4b7f8fd0d", size = 277747, upload-time = "2026-02-20T20:16:21.325Z" }, + { url = "https://files.pythonhosted.org/packages/fb/07/cb284a8b5c6498dbd7cba35d31380bb123d7dceaa7907f606c8ff5993cbf/greenlet-3.3.2-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b568183cf65b94919be4438dc28416b234b678c608cafac8874dfeeb2a9bbe13", size = 579202, upload-time = "2026-02-20T20:47:28.955Z" }, + { url = "https://files.pythonhosted.org/packages/ed/45/67922992b3a152f726163b19f890a85129a992f39607a2a53155de3448b8/greenlet-3.3.2-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:527fec58dc9f90efd594b9b700662ed3fb2493c2122067ac9c740d98080a620e", size = 590620, upload-time = "2026-02-20T20:55:55.581Z" }, + { url = "https://files.pythonhosted.org/packages/03/5f/6e2a7d80c353587751ef3d44bb947f0565ec008a2e0927821c007e96d3a7/greenlet-3.3.2-cp310-cp310-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:508c7f01f1791fbc8e011bd508f6794cb95397fdb198a46cb6635eb5b78d85a7", size = 602132, upload-time = "2026-02-20T21:02:43.261Z" }, + { url = "https://files.pythonhosted.org/packages/ad/55/9f1ebb5a825215fadcc0f7d5073f6e79e3007e3282b14b22d6aba7ca6cb8/greenlet-3.3.2-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ad0c8917dd42a819fe77e6bdfcb84e3379c0de956469301d9fd36427a1ca501f", size = 591729, upload-time = "2026-02-20T20:20:58.395Z" }, + { url = "https://files.pythonhosted.org/packages/24/b4/21f5455773d37f94b866eb3cf5caed88d6cea6dd2c6e1f9c34f463cba3ec/greenlet-3.3.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:97245cc10e5515dbc8c3104b2928f7f02b6813002770cfaffaf9a6e0fc2b94ef", size = 1551946, upload-time = "2026-02-20T20:49:31.102Z" }, + { url = "https://files.pythonhosted.org/packages/00/68/91f061a926abead128fe1a87f0b453ccf07368666bd59ffa46016627a930/greenlet-3.3.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8c1fdd7d1b309ff0da81d60a9688a8bd044ac4e18b250320a96fc68d31c209ca", size = 1618494, upload-time = "2026-02-20T20:21:06.541Z" }, + { url = "https://files.pythonhosted.org/packages/ac/78/f93e840cbaef8becaf6adafbaf1319682a6c2d8c1c20224267a5c6c8c891/greenlet-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:5d0e35379f93a6d0222de929a25ab47b5eb35b5ef4721c2b9cbcc4036129ff1f", size = 230092, upload-time = "2026-02-20T20:17:09.379Z" }, + { url = "https://files.pythonhosted.org/packages/f3/47/16400cb42d18d7a6bb46f0626852c1718612e35dcb0dffa16bbaffdf5dd2/greenlet-3.3.2-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:c56692189a7d1c7606cb794be0a8381470d95c57ce5be03fb3d0ef57c7853b86", size = 278890, upload-time = "2026-02-20T20:19:39.263Z" }, + { url = "https://files.pythonhosted.org/packages/a3/90/42762b77a5b6aa96cd8c0e80612663d39211e8ae8a6cd47c7f1249a66262/greenlet-3.3.2-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ebd458fa8285960f382841da585e02201b53a5ec2bac6b156fc623b5ce4499f", size = 581120, upload-time = "2026-02-20T20:47:30.161Z" }, + { url = "https://files.pythonhosted.org/packages/bf/6f/f3d64f4fa0a9c7b5c5b3c810ff1df614540d5aa7d519261b53fba55d4df9/greenlet-3.3.2-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a443358b33c4ec7b05b79a7c8b466f5d275025e750298be7340f8fc63dff2a55", size = 594363, upload-time = "2026-02-20T20:55:56.965Z" }, + { url = "https://files.pythonhosted.org/packages/9c/8b/1430a04657735a3f23116c2e0d5eb10220928846e4537a938a41b350bed6/greenlet-3.3.2-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4375a58e49522698d3e70cc0b801c19433021b5c37686f7ce9c65b0d5c8677d2", size = 605046, upload-time = "2026-02-20T21:02:45.234Z" }, + { url = "https://files.pythonhosted.org/packages/72/83/3e06a52aca8128bdd4dcd67e932b809e76a96ab8c232a8b025b2850264c5/greenlet-3.3.2-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e2cd90d413acbf5e77ae41e5d3c9b3ac1d011a756d7284d7f3f2b806bbd6358", size = 594156, upload-time = "2026-02-20T20:20:59.955Z" }, + { url = "https://files.pythonhosted.org/packages/70/79/0de5e62b873e08fe3cef7dbe84e5c4bc0e8ed0c7ff131bccb8405cd107c8/greenlet-3.3.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:442b6057453c8cb29b4fb36a2ac689382fc71112273726e2423f7f17dc73bf99", size = 1554649, upload-time = "2026-02-20T20:49:32.293Z" }, + { url = "https://files.pythonhosted.org/packages/5a/00/32d30dee8389dc36d42170a9c66217757289e2afb0de59a3565260f38373/greenlet-3.3.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:45abe8eb6339518180d5a7fa47fa01945414d7cca5ecb745346fc6a87d2750be", size = 1619472, upload-time = "2026-02-20T20:21:07.966Z" }, + { url = "https://files.pythonhosted.org/packages/f1/3a/efb2cf697fbccdf75b24e2c18025e7dfa54c4f31fab75c51d0fe79942cef/greenlet-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:1e692b2dae4cc7077cbb11b47d258533b48c8fde69a33d0d8a82e2fe8d8531d5", size = 230389, upload-time = "2026-02-20T20:17:18.772Z" }, + { url = "https://files.pythonhosted.org/packages/e1/a1/65bbc059a43a7e2143ec4fc1f9e3f673e04f9c7b371a494a101422ac4fd5/greenlet-3.3.2-cp311-cp311-win_arm64.whl", hash = "sha256:02b0a8682aecd4d3c6c18edf52bc8e51eacdd75c8eac52a790a210b06aa295fd", size = 229645, upload-time = "2026-02-20T20:18:18.695Z" }, + { url = "https://files.pythonhosted.org/packages/ea/ab/1608e5a7578e62113506740b88066bf09888322a311cff602105e619bd87/greenlet-3.3.2-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:ac8d61d4343b799d1e526db579833d72f23759c71e07181c2d2944e429eb09cd", size = 280358, upload-time = "2026-02-20T20:17:43.971Z" }, + { url = "https://files.pythonhosted.org/packages/a5/23/0eae412a4ade4e6623ff7626e38998cb9b11e9ff1ebacaa021e4e108ec15/greenlet-3.3.2-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ceec72030dae6ac0c8ed7591b96b70410a8be370b6a477b1dbc072856ad02bd", size = 601217, upload-time = "2026-02-20T20:47:31.462Z" }, + { url = "https://files.pythonhosted.org/packages/f8/16/5b1678a9c07098ecb9ab2dd159fafaf12e963293e61ee8d10ecb55273e5e/greenlet-3.3.2-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a2a5be83a45ce6188c045bcc44b0ee037d6a518978de9a5d97438548b953a1ac", size = 611792, upload-time = "2026-02-20T20:55:58.423Z" }, + { url = "https://files.pythonhosted.org/packages/5c/c5/cc09412a29e43406eba18d61c70baa936e299bc27e074e2be3806ed29098/greenlet-3.3.2-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ae9e21c84035c490506c17002f5c8ab25f980205c3e61ddb3a2a2a2e6c411fcb", size = 626250, upload-time = "2026-02-20T21:02:46.596Z" }, + { url = "https://files.pythonhosted.org/packages/50/1f/5155f55bd71cabd03765a4aac9ac446be129895271f73872c36ebd4b04b6/greenlet-3.3.2-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43e99d1749147ac21dde49b99c9abffcbc1e2d55c67501465ef0930d6e78e070", size = 613875, upload-time = "2026-02-20T20:21:01.102Z" }, + { url = "https://files.pythonhosted.org/packages/fc/dd/845f249c3fcd69e32df80cdab059b4be8b766ef5830a3d0aa9d6cad55beb/greenlet-3.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4c956a19350e2c37f2c48b336a3afb4bff120b36076d9d7fb68cb44e05d95b79", size = 1571467, upload-time = "2026-02-20T20:49:33.495Z" }, + { url = "https://files.pythonhosted.org/packages/2a/50/2649fe21fcc2b56659a452868e695634722a6655ba245d9f77f5656010bf/greenlet-3.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6c6f8ba97d17a1e7d664151284cb3315fc5f8353e75221ed4324f84eb162b395", size = 1640001, upload-time = "2026-02-20T20:21:09.154Z" }, + { url = "https://files.pythonhosted.org/packages/9b/40/cc802e067d02af8b60b6771cea7d57e21ef5e6659912814babb42b864713/greenlet-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:34308836d8370bddadb41f5a7ce96879b72e2fdfb4e87729330c6ab52376409f", size = 231081, upload-time = "2026-02-20T20:17:28.121Z" }, + { url = "https://files.pythonhosted.org/packages/58/2e/fe7f36ff1982d6b10a60d5e0740c759259a7d6d2e1dc41da6d96de32fff6/greenlet-3.3.2-cp312-cp312-win_arm64.whl", hash = "sha256:d3a62fa76a32b462a97198e4c9e99afb9ab375115e74e9a83ce180e7a496f643", size = 230331, upload-time = "2026-02-20T20:17:23.34Z" }, + { url = "https://files.pythonhosted.org/packages/ac/48/f8b875fa7dea7dd9b33245e37f065af59df6a25af2f9561efa8d822fde51/greenlet-3.3.2-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:aa6ac98bdfd716a749b84d4034486863fd81c3abde9aa3cf8eff9127981a4ae4", size = 279120, upload-time = "2026-02-20T20:19:01.9Z" }, + { url = "https://files.pythonhosted.org/packages/49/8d/9771d03e7a8b1ee456511961e1b97a6d77ae1dea4a34a5b98eee706689d3/greenlet-3.3.2-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ab0c7e7901a00bc0a7284907273dc165b32e0d109a6713babd04471327ff7986", size = 603238, upload-time = "2026-02-20T20:47:32.873Z" }, + { url = "https://files.pythonhosted.org/packages/59/0e/4223c2bbb63cd5c97f28ffb2a8aee71bdfb30b323c35d409450f51b91e3e/greenlet-3.3.2-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d248d8c23c67d2291ffd47af766e2a3aa9fa1c6703155c099feb11f526c63a92", size = 614219, upload-time = "2026-02-20T20:55:59.817Z" }, + { url = "https://files.pythonhosted.org/packages/94/2b/4d012a69759ac9d77210b8bfb128bc621125f5b20fc398bce3940d036b1c/greenlet-3.3.2-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ccd21bb86944ca9be6d967cf7691e658e43417782bce90b5d2faeda0ff78a7dd", size = 628268, upload-time = "2026-02-20T21:02:48.024Z" }, + { url = "https://files.pythonhosted.org/packages/7a/34/259b28ea7a2a0c904b11cd36c79b8cef8019b26ee5dbe24e73b469dea347/greenlet-3.3.2-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b6997d360a4e6a4e936c0f9625b1c20416b8a0ea18a8e19cabbefc712e7397ab", size = 616774, upload-time = "2026-02-20T20:21:02.454Z" }, + { url = "https://files.pythonhosted.org/packages/0a/03/996c2d1689d486a6e199cb0f1cf9e4aa940c500e01bdf201299d7d61fa69/greenlet-3.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:64970c33a50551c7c50491671265d8954046cb6e8e2999aacdd60e439b70418a", size = 1571277, upload-time = "2026-02-20T20:49:34.795Z" }, + { url = "https://files.pythonhosted.org/packages/d9/c4/2570fc07f34a39f2caf0bf9f24b0a1a0a47bc2e8e465b2c2424821389dfc/greenlet-3.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1a9172f5bf6bd88e6ba5a84e0a68afeac9dc7b6b412b245dd64f52d83c81e55b", size = 1640455, upload-time = "2026-02-20T20:21:10.261Z" }, + { url = "https://files.pythonhosted.org/packages/91/39/5ef5aa23bc545aa0d31e1b9b55822b32c8da93ba657295840b6b34124009/greenlet-3.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:a7945dd0eab63ded0a48e4dcade82939783c172290a7903ebde9e184333ca124", size = 230961, upload-time = "2026-02-20T20:16:58.461Z" }, + { url = "https://files.pythonhosted.org/packages/62/6b/a89f8456dcb06becff288f563618e9f20deed8dd29beea14f9a168aef64b/greenlet-3.3.2-cp313-cp313-win_arm64.whl", hash = "sha256:394ead29063ee3515b4e775216cb756b2e3b4a7e55ae8fd884f17fa579e6b327", size = 230221, upload-time = "2026-02-20T20:17:37.152Z" }, + { url = "https://files.pythonhosted.org/packages/3f/ae/8bffcbd373b57a5992cd077cbe8858fff39110480a9d50697091faea6f39/greenlet-3.3.2-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:8d1658d7291f9859beed69a776c10822a0a799bc4bfe1bd4272bb60e62507dab", size = 279650, upload-time = "2026-02-20T20:18:00.783Z" }, + { url = "https://files.pythonhosted.org/packages/d1/c0/45f93f348fa49abf32ac8439938726c480bd96b2a3c6f4d949ec0124b69f/greenlet-3.3.2-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:18cb1b7337bca281915b3c5d5ae19f4e76d35e1df80f4ad3c1a7be91fadf1082", size = 650295, upload-time = "2026-02-20T20:47:34.036Z" }, + { url = "https://files.pythonhosted.org/packages/b3/de/dd7589b3f2b8372069ab3e4763ea5329940fc7ad9dcd3e272a37516d7c9b/greenlet-3.3.2-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c2e47408e8ce1c6f1ceea0dffcdf6ebb85cc09e55c7af407c99f1112016e45e9", size = 662163, upload-time = "2026-02-20T20:56:01.295Z" }, + { url = "https://files.pythonhosted.org/packages/cd/ac/85804f74f1ccea31ba518dcc8ee6f14c79f73fe36fa1beba38930806df09/greenlet-3.3.2-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e3cb43ce200f59483eb82949bf1835a99cf43d7571e900d7c8d5c62cdf25d2f9", size = 675371, upload-time = "2026-02-20T21:02:49.664Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d8/09bfa816572a4d83bccd6750df1926f79158b1c36c5f73786e26dbe4ee38/greenlet-3.3.2-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:63d10328839d1973e5ba35e98cccbca71b232b14051fd957b6f8b6e8e80d0506", size = 664160, upload-time = "2026-02-20T20:21:04.015Z" }, + { url = "https://files.pythonhosted.org/packages/48/cf/56832f0c8255d27f6c35d41b5ec91168d74ec721d85f01a12131eec6b93c/greenlet-3.3.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8e4ab3cfb02993c8cc248ea73d7dae6cec0253e9afa311c9b37e603ca9fad2ce", size = 1619181, upload-time = "2026-02-20T20:49:36.052Z" }, + { url = "https://files.pythonhosted.org/packages/0a/23/b90b60a4aabb4cec0796e55f25ffbfb579a907c3898cd2905c8918acaa16/greenlet-3.3.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:94ad81f0fd3c0c0681a018a976e5c2bd2ca2d9d94895f23e7bb1af4e8af4e2d5", size = 1687713, upload-time = "2026-02-20T20:21:11.684Z" }, + { url = "https://files.pythonhosted.org/packages/f3/ca/2101ca3d9223a1dc125140dbc063644dca76df6ff356531eb27bc267b446/greenlet-3.3.2-cp314-cp314-win_amd64.whl", hash = "sha256:8c4dd0f3997cf2512f7601563cc90dfb8957c0cff1e3a1b23991d4ea1776c492", size = 232034, upload-time = "2026-02-20T20:20:08.186Z" }, + { url = "https://files.pythonhosted.org/packages/f6/4a/ecf894e962a59dea60f04877eea0fd5724618da89f1867b28ee8b91e811f/greenlet-3.3.2-cp314-cp314-win_arm64.whl", hash = "sha256:cd6f9e2bbd46321ba3bbb4c8a15794d32960e3b0ae2cc4d49a1a53d314805d71", size = 231437, upload-time = "2026-02-20T20:18:59.722Z" }, + { url = "https://files.pythonhosted.org/packages/98/6d/8f2ef704e614bcf58ed43cfb8d87afa1c285e98194ab2cfad351bf04f81e/greenlet-3.3.2-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:e26e72bec7ab387ac80caa7496e0f908ff954f31065b0ffc1f8ecb1338b11b54", size = 286617, upload-time = "2026-02-20T20:19:29.856Z" }, + { url = "https://files.pythonhosted.org/packages/5e/0d/93894161d307c6ea237a43988f27eba0947b360b99ac5239ad3fe09f0b47/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b466dff7a4ffda6ca975979bab80bdadde979e29fc947ac3be4451428d8b0e4", size = 655189, upload-time = "2026-02-20T20:47:35.742Z" }, + { url = "https://files.pythonhosted.org/packages/f5/2c/d2d506ebd8abcb57386ec4f7ba20f4030cbe56eae541bc6fd6ef399c0b41/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b8bddc5b73c9720bea487b3bffdb1840fe4e3656fba3bd40aa1489e9f37877ff", size = 658225, upload-time = "2026-02-20T20:56:02.527Z" }, + { url = "https://files.pythonhosted.org/packages/d1/67/8197b7e7e602150938049d8e7f30de1660cfb87e4c8ee349b42b67bdb2e1/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:59b3e2c40f6706b05a9cd299c836c6aa2378cabe25d021acd80f13abf81181cf", size = 666581, upload-time = "2026-02-20T21:02:51.526Z" }, + { url = "https://files.pythonhosted.org/packages/8e/30/3a09155fbf728673a1dea713572d2d31159f824a37c22da82127056c44e4/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b26b0f4428b871a751968285a1ac9648944cea09807177ac639b030bddebcea4", size = 657907, upload-time = "2026-02-20T20:21:05.259Z" }, + { url = "https://files.pythonhosted.org/packages/f3/fd/d05a4b7acd0154ed758797f0a43b4c0962a843bedfe980115e842c5b2d08/greenlet-3.3.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1fb39a11ee2e4d94be9a76671482be9398560955c9e568550de0224e41104727", size = 1618857, upload-time = "2026-02-20T20:49:37.309Z" }, + { url = "https://files.pythonhosted.org/packages/6f/e1/50ee92a5db521de8f35075b5eff060dd43d39ebd46c2181a2042f7070385/greenlet-3.3.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:20154044d9085151bc309e7689d6f7ba10027f8f5a8c0676ad398b951913d89e", size = 1680010, upload-time = "2026-02-20T20:21:13.427Z" }, + { url = "https://files.pythonhosted.org/packages/29/4b/45d90626aef8e65336bed690106d1382f7a43665e2249017e9527df8823b/greenlet-3.3.2-cp314-cp314t-win_amd64.whl", hash = "sha256:c04c5e06ec3e022cbfe2cd4a846e1d4e50087444f875ff6d2c2ad8445495cf1a", size = 237086, upload-time = "2026-02-20T20:20:45.786Z" }, ] [[package]] @@ -988,6 +1002,9 @@ dependencies = [ ] [package.optional-dependencies] +aiosqlalchemy = [ + { name = "sqlalchemy", extra = ["asyncio"] }, +] arrow = [ { name = "pyarrow", version = "18.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14'" }, { name = "pyarrow", version = "22.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14'" }, @@ -1028,7 +1045,7 @@ dev = [ { name = "sphinx-design", version = "0.7.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "sphinx-multiversion" }, { name = "sphinxext-opengraph" }, - { name = "sqlalchemy" }, + { name = "sqlalchemy", extra = ["asyncio"] }, { name = "types-python-dateutil" }, ] @@ -1044,9 +1061,10 @@ requires-dist = [ { name = "pyarrow", marker = "python_full_version < '3.14' and extra == 'arrow'", specifier = ">=10.0.0" }, { name = "python-dateutil" }, { name = "sqlalchemy", marker = "extra == 'sqlalchemy'", specifier = ">=1.0.0" }, + { name = "sqlalchemy", extras = ["asyncio"], marker = "extra == 'aiosqlalchemy'", specifier = ">=2.0.0" }, { name = "tenacity", specifier = ">=4.1.0" }, ] -provides-extras = ["arrow", "pandas", "polars", "sqlalchemy"] +provides-extras = ["aiosqlalchemy", "arrow", "pandas", "polars", "sqlalchemy"] [package.metadata.requires-dev] dev = [ @@ -1070,7 +1088,7 @@ dev = [ { name = "sphinx-design" }, { name = "sphinx-multiversion" }, { name = "sphinxext-opengraph" }, - { name = "sqlalchemy", specifier = ">=1.0.0" }, + { name = "sqlalchemy", extras = ["asyncio"], specifier = ">=1.0.0" }, { name = "types-python-dateutil" }, ] @@ -1556,6 +1574,11 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b8/49/21633706dd6feb14cd3f7935fc00b60870ea057686035e1a99ae6d9d9d53/SQLAlchemy-2.0.36-py3-none-any.whl", hash = "sha256:fddbe92b4760c6f5d48162aef14824add991aeda8ddadb3c31d56eb15ca69f8e", size = 1883787, upload-time = "2024-10-15T20:04:30.265Z" }, ] +[package.optional-dependencies] +asyncio = [ + { name = "greenlet" }, +] + [[package]] name = "tenacity" version = "9.0.0"