PyAthena 3.31.1__tar.gz → 3.33.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pyathena-3.31.1 → pyathena-3.33.0}/PKG-INFO +2 -2
- {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/_version.py +2 -2
- {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/aio/common.py +2 -0
- {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/aio/spark/cursor.py +2 -0
- {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/arrow/cursor.py +0 -3
- {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/common.py +18 -0
- {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/connection.py +10 -1
- {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/cursor.py +0 -2
- {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/pandas/cursor.py +0 -3
- {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/polars/cursor.py +0 -3
- {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/s3fs/cursor.py +0 -3
- {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/spark/common.py +2 -0
- {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/sqlalchemy/base.py +7 -2
- {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/sqlalchemy/compiler.py +95 -13
- {pyathena-3.31.1 → pyathena-3.33.0}/pyproject.toml +5 -5
- {pyathena-3.31.1 → pyathena-3.33.0}/.gitignore +0 -0
- {pyathena-3.31.1 → pyathena-3.33.0}/LICENSE +0 -0
- {pyathena-3.31.1 → pyathena-3.33.0}/README.md +0 -0
- {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/__init__.py +0 -0
- {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/aio/__init__.py +0 -0
- {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/aio/arrow/__init__.py +0 -0
- {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/aio/arrow/cursor.py +0 -0
- {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/aio/connection.py +0 -0
- {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/aio/cursor.py +0 -0
- {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/aio/pandas/__init__.py +0 -0
- {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/aio/pandas/cursor.py +0 -0
- {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/aio/polars/__init__.py +0 -0
- {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/aio/polars/cursor.py +0 -0
- {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/aio/result_set.py +0 -0
- {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/aio/s3fs/__init__.py +0 -0
- {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/aio/s3fs/cursor.py +0 -0
- {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/aio/spark/__init__.py +0 -0
- {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/aio/sqlalchemy/__init__.py +0 -0
- {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/aio/sqlalchemy/arrow.py +0 -0
- {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/aio/sqlalchemy/base.py +0 -0
- {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/aio/sqlalchemy/pandas.py +0 -0
- {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/aio/sqlalchemy/polars.py +0 -0
- {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/aio/sqlalchemy/rest.py +0 -0
- {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/aio/sqlalchemy/s3fs.py +0 -0
- {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/aio/util.py +0 -0
- {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/arrow/__init__.py +0 -0
- {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/arrow/async_cursor.py +0 -0
- {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/arrow/converter.py +0 -0
- {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/arrow/result_set.py +0 -0
- {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/arrow/util.py +0 -0
- {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/async_cursor.py +0 -0
- {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/converter.py +0 -0
- {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/error.py +0 -0
- {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/filesystem/__init__.py +0 -0
- {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/filesystem/s3.py +0 -0
- {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/filesystem/s3_async.py +0 -0
- {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/filesystem/s3_executor.py +0 -0
- {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/filesystem/s3_object.py +0 -0
- {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/formatter.py +0 -0
- {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/model.py +0 -0
- {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/pandas/__init__.py +0 -0
- {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/pandas/async_cursor.py +0 -0
- {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/pandas/converter.py +0 -0
- {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/pandas/result_set.py +0 -0
- {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/pandas/util.py +0 -0
- {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/parser.py +0 -0
- {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/polars/__init__.py +0 -0
- {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/polars/async_cursor.py +0 -0
- {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/polars/converter.py +0 -0
- {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/polars/result_set.py +0 -0
- {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/polars/util.py +0 -0
- {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/py.typed +0 -0
- {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/result_set.py +0 -0
- {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/s3fs/__init__.py +0 -0
- {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/s3fs/async_cursor.py +0 -0
- {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/s3fs/converter.py +0 -0
- {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/s3fs/reader.py +0 -0
- {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/s3fs/result_set.py +0 -0
- {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/spark/__init__.py +0 -0
- {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/spark/async_cursor.py +0 -0
- {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/spark/cursor.py +0 -0
- {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/sqlalchemy/__init__.py +0 -0
- {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/sqlalchemy/arrow.py +0 -0
- {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/sqlalchemy/constants.py +0 -0
- {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/sqlalchemy/pandas.py +0 -0
- {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/sqlalchemy/polars.py +0 -0
- {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/sqlalchemy/preparer.py +0 -0
- {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/sqlalchemy/requirements.py +0 -0
- {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/sqlalchemy/rest.py +0 -0
- {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/sqlalchemy/s3fs.py +0 -0
- {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/sqlalchemy/types.py +0 -0
- {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/sqlalchemy/util.py +0 -0
- {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/util.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: PyAthena
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.33.0
|
|
4
4
|
Summary: Python DB API 2.0 (PEP 249) client for Amazon Athena
|
|
5
5
|
Project-URL: homepage, https://github.com/pyathena-dev/PyAthena/
|
|
6
6
|
Project-URL: repository, https://github.com/pyathena-dev/PyAthena/
|
|
@@ -38,7 +38,7 @@ Classifier: Programming Language :: Python :: 3.13
|
|
|
38
38
|
Classifier: Programming Language :: Python :: 3.14
|
|
39
39
|
Classifier: Topic :: Database :: Front-Ends
|
|
40
40
|
Requires-Python: >=3.10
|
|
41
|
-
Requires-Dist: boto3>=1.
|
|
41
|
+
Requires-Dist: boto3>=1.41.2
|
|
42
42
|
Requires-Dist: botocore>=1.41.2
|
|
43
43
|
Requires-Dist: fsspec
|
|
44
44
|
Requires-Dist: python-dateutil
|
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '3.
|
|
22
|
-
__version_tuple__ = version_tuple = (3,
|
|
21
|
+
__version__ = version = '3.33.0'
|
|
22
|
+
__version_tuple__ = version_tuple = (3, 33, 0)
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -83,6 +83,8 @@ class AioBaseCursor(BaseCursor):
|
|
|
83
83
|
async def __poll(self, query_id: str) -> AthenaQueryExecution:
|
|
84
84
|
while True:
|
|
85
85
|
query_execution = await self._get_query_execution(query_id)
|
|
86
|
+
if self._on_poll:
|
|
87
|
+
self._on_poll(query_execution)
|
|
86
88
|
if query_execution.state in [
|
|
87
89
|
AthenaQueryExecution.STATE_SUCCEEDED,
|
|
88
90
|
AthenaQueryExecution.STATE_FAILED,
|
|
@@ -128,6 +128,8 @@ class AioSparkCursor(SparkBaseCursor, WithCalculationExecution):
|
|
|
128
128
|
async def __poll(self, query_id: str) -> AthenaQueryExecution | AthenaCalculationExecution:
|
|
129
129
|
while True:
|
|
130
130
|
calculation_status = await self._get_calculation_execution_status(query_id)
|
|
131
|
+
if self._on_poll:
|
|
132
|
+
self._on_poll(calculation_status)
|
|
131
133
|
if calculation_status.state in [
|
|
132
134
|
AthenaCalculationExecutionStatus.STATE_COMPLETED,
|
|
133
135
|
AthenaCalculationExecutionStatus.STATE_FAILED,
|
|
@@ -63,7 +63,6 @@ class ArrowCursor(WithFetch):
|
|
|
63
63
|
unload: bool = False,
|
|
64
64
|
result_reuse_enable: bool = False,
|
|
65
65
|
result_reuse_minutes: int = CursorIterator.DEFAULT_RESULT_REUSE_MINUTES,
|
|
66
|
-
on_start_query_execution: Callable[[str], None] | None = None,
|
|
67
66
|
connect_timeout: float | None = None,
|
|
68
67
|
request_timeout: float | None = None,
|
|
69
68
|
**kwargs,
|
|
@@ -82,7 +81,6 @@ class ArrowCursor(WithFetch):
|
|
|
82
81
|
unload: Enable UNLOAD for high-performance Parquet output.
|
|
83
82
|
result_reuse_enable: Enable Athena query result reuse.
|
|
84
83
|
result_reuse_minutes: Minutes to reuse cached results.
|
|
85
|
-
on_start_query_execution: Callback invoked when query starts.
|
|
86
84
|
connect_timeout: Socket connection timeout in seconds for S3 operations.
|
|
87
85
|
Defaults to AWS SDK default (typically 1 second) if not specified.
|
|
88
86
|
request_timeout: Request timeout in seconds for S3 operations.
|
|
@@ -113,7 +111,6 @@ class ArrowCursor(WithFetch):
|
|
|
113
111
|
**kwargs,
|
|
114
112
|
)
|
|
115
113
|
self._unload = unload
|
|
116
|
-
self._on_start_query_execution = on_start_query_execution
|
|
117
114
|
self._connect_timeout = connect_timeout
|
|
118
115
|
self._request_timeout = request_timeout
|
|
119
116
|
|
|
@@ -4,6 +4,7 @@ import logging
|
|
|
4
4
|
import sys
|
|
5
5
|
import time
|
|
6
6
|
from abc import ABCMeta, abstractmethod
|
|
7
|
+
from collections.abc import Callable
|
|
7
8
|
from datetime import datetime, timedelta, timezone
|
|
8
9
|
from typing import TYPE_CHECKING, Any, cast
|
|
9
10
|
|
|
@@ -27,6 +28,14 @@ if TYPE_CHECKING:
|
|
|
27
28
|
|
|
28
29
|
_logger = logging.getLogger(__name__)
|
|
29
30
|
|
|
31
|
+
OnPollCallback = Callable[[AthenaQueryExecution | AthenaCalculationExecutionStatus], None]
|
|
32
|
+
"""Type of the optional ``on_poll`` callback.
|
|
33
|
+
|
|
34
|
+
Invoked once per poll iteration with the current execution object: an
|
|
35
|
+
:class:`~pyathena.model.AthenaQueryExecution` for SQL queries, or an
|
|
36
|
+
:class:`~pyathena.model.AthenaCalculationExecutionStatus` for Spark calculations.
|
|
37
|
+
"""
|
|
38
|
+
|
|
30
39
|
|
|
31
40
|
class CursorIterator(metaclass=ABCMeta):
|
|
32
41
|
"""Abstract base class providing iteration and result fetching capabilities for cursors.
|
|
@@ -164,6 +173,8 @@ class BaseCursor(metaclass=ABCMeta):
|
|
|
164
173
|
kill_on_interrupt: bool,
|
|
165
174
|
result_reuse_enable: bool,
|
|
166
175
|
result_reuse_minutes: int,
|
|
176
|
+
on_start_query_execution: Callable[[str], None] | None = None,
|
|
177
|
+
on_poll: OnPollCallback | None = None,
|
|
167
178
|
**kwargs,
|
|
168
179
|
) -> None:
|
|
169
180
|
super().__init__()
|
|
@@ -181,6 +192,11 @@ class BaseCursor(metaclass=ABCMeta):
|
|
|
181
192
|
self._kill_on_interrupt = kill_on_interrupt
|
|
182
193
|
self._result_reuse_enable = result_reuse_enable
|
|
183
194
|
self._result_reuse_minutes = result_reuse_minutes
|
|
195
|
+
# ``on_start_query_execution`` is invoked only by cursors whose ``execute()``
|
|
196
|
+
# supports it (the synchronous cursors). Async/aio/Spark cursors return the
|
|
197
|
+
# query id immediately through their execution model and do not invoke it.
|
|
198
|
+
self._on_start_query_execution = on_start_query_execution
|
|
199
|
+
self._on_poll = on_poll
|
|
184
200
|
|
|
185
201
|
@staticmethod
|
|
186
202
|
def get_default_converter(unload: bool = False) -> DefaultTypeConverter | Any:
|
|
@@ -558,6 +574,8 @@ class BaseCursor(metaclass=ABCMeta):
|
|
|
558
574
|
def __poll(self, query_id: str) -> AthenaQueryExecution | AthenaCalculationExecution:
|
|
559
575
|
while True:
|
|
560
576
|
query_execution = self._get_query_execution(query_id)
|
|
577
|
+
if self._on_poll:
|
|
578
|
+
self._on_poll(query_execution)
|
|
561
579
|
if query_execution.state in [
|
|
562
580
|
AthenaQueryExecution.STATE_SUCCEEDED,
|
|
563
581
|
AthenaQueryExecution.STATE_FAILED,
|
|
@@ -18,7 +18,7 @@ from boto3.session import Session
|
|
|
18
18
|
from botocore.config import Config
|
|
19
19
|
|
|
20
20
|
import pyathena
|
|
21
|
-
from pyathena.common import BaseCursor, CursorIterator
|
|
21
|
+
from pyathena.common import BaseCursor, CursorIterator, OnPollCallback
|
|
22
22
|
from pyathena.converter import Converter
|
|
23
23
|
from pyathena.cursor import Cursor
|
|
24
24
|
from pyathena.error import NotSupportedError, ProgrammingError
|
|
@@ -127,6 +127,7 @@ class Connection(Generic[ConnectionCursor]):
|
|
|
127
127
|
result_reuse_enable: bool = ...,
|
|
128
128
|
result_reuse_minutes: int = ...,
|
|
129
129
|
on_start_query_execution: Callable[[str], None] | None = ...,
|
|
130
|
+
on_poll: OnPollCallback | None = ...,
|
|
130
131
|
**kwargs,
|
|
131
132
|
) -> None: ...
|
|
132
133
|
|
|
@@ -158,6 +159,7 @@ class Connection(Generic[ConnectionCursor]):
|
|
|
158
159
|
result_reuse_enable: bool = ...,
|
|
159
160
|
result_reuse_minutes: int = ...,
|
|
160
161
|
on_start_query_execution: Callable[[str], None] | None = ...,
|
|
162
|
+
on_poll: OnPollCallback | None = ...,
|
|
161
163
|
**kwargs,
|
|
162
164
|
) -> None: ...
|
|
163
165
|
|
|
@@ -188,6 +190,7 @@ class Connection(Generic[ConnectionCursor]):
|
|
|
188
190
|
result_reuse_enable: bool = False,
|
|
189
191
|
result_reuse_minutes: int = CursorIterator.DEFAULT_RESULT_REUSE_MINUTES,
|
|
190
192
|
on_start_query_execution: Callable[[str], None] | None = None,
|
|
193
|
+
on_poll: OnPollCallback | None = None,
|
|
191
194
|
**kwargs,
|
|
192
195
|
) -> None:
|
|
193
196
|
"""Initialize a new Athena database connection.
|
|
@@ -224,6 +227,10 @@ class Connection(Generic[ConnectionCursor]):
|
|
|
224
227
|
result_reuse_enable: Enable Athena query result reuse. Defaults to False.
|
|
225
228
|
result_reuse_minutes: Minutes to reuse cached results.
|
|
226
229
|
on_start_query_execution: Callback function called when query starts.
|
|
230
|
+
on_poll: Callback invoked once per poll iteration with the current
|
|
231
|
+
execution object (``AthenaQueryExecution``, or
|
|
232
|
+
``AthenaCalculationExecutionStatus`` for Spark). Useful for
|
|
233
|
+
monitoring live query progress. Defaults to None.
|
|
227
234
|
**kwargs: Additional arguments passed to boto3 Session and client.
|
|
228
235
|
|
|
229
236
|
Raises:
|
|
@@ -331,6 +338,7 @@ class Connection(Generic[ConnectionCursor]):
|
|
|
331
338
|
self.result_reuse_enable = result_reuse_enable
|
|
332
339
|
self.result_reuse_minutes = result_reuse_minutes
|
|
333
340
|
self.on_start_query_execution = on_start_query_execution
|
|
341
|
+
self.on_poll = on_poll
|
|
334
342
|
|
|
335
343
|
def _assume_role(
|
|
336
344
|
self,
|
|
@@ -555,6 +563,7 @@ class Connection(Generic[ConnectionCursor]):
|
|
|
555
563
|
on_start_query_execution=kwargs.pop(
|
|
556
564
|
"on_start_query_execution", self.on_start_query_execution
|
|
557
565
|
),
|
|
566
|
+
on_poll=kwargs.pop("on_poll", self.on_poll),
|
|
558
567
|
**kwargs,
|
|
559
568
|
)
|
|
560
569
|
|
|
@@ -52,7 +52,6 @@ class Cursor(WithFetch):
|
|
|
52
52
|
kill_on_interrupt: bool = True,
|
|
53
53
|
result_reuse_enable: bool = False,
|
|
54
54
|
result_reuse_minutes: int = CursorIterator.DEFAULT_RESULT_REUSE_MINUTES,
|
|
55
|
-
on_start_query_execution: Callable[[str], None] | None = None,
|
|
56
55
|
**kwargs,
|
|
57
56
|
) -> None:
|
|
58
57
|
super().__init__(
|
|
@@ -69,7 +68,6 @@ class Cursor(WithFetch):
|
|
|
69
68
|
**kwargs,
|
|
70
69
|
)
|
|
71
70
|
self._result_set_class = AthenaResultSet
|
|
72
|
-
self._on_start_query_execution = on_start_query_execution
|
|
73
71
|
|
|
74
72
|
@property
|
|
75
73
|
def arraysize(self) -> int:
|
|
@@ -79,7 +79,6 @@ class PandasCursor(WithFetch):
|
|
|
79
79
|
result_reuse_enable: bool = False,
|
|
80
80
|
result_reuse_minutes: int = CursorIterator.DEFAULT_RESULT_REUSE_MINUTES,
|
|
81
81
|
auto_optimize_chunksize: bool = False,
|
|
82
|
-
on_start_query_execution: Callable[[str], None] | None = None,
|
|
83
82
|
**kwargs,
|
|
84
83
|
) -> None:
|
|
85
84
|
"""Initialize PandasCursor with configuration options.
|
|
@@ -105,7 +104,6 @@ class PandasCursor(WithFetch):
|
|
|
105
104
|
auto_optimize_chunksize: Enable automatic chunksize determination for
|
|
106
105
|
large files. Only effective when chunksize is None.
|
|
107
106
|
Default: False (no automatic chunking).
|
|
108
|
-
on_start_query_execution: Callback for query start events.
|
|
109
107
|
**kwargs: Additional arguments passed to pandas.read_csv.
|
|
110
108
|
"""
|
|
111
109
|
super().__init__(
|
|
@@ -128,7 +126,6 @@ class PandasCursor(WithFetch):
|
|
|
128
126
|
self._cache_type = cache_type
|
|
129
127
|
self._max_workers = max_workers
|
|
130
128
|
self._auto_optimize_chunksize = auto_optimize_chunksize
|
|
131
|
-
self._on_start_query_execution = on_start_query_execution
|
|
132
129
|
|
|
133
130
|
@staticmethod
|
|
134
131
|
def get_default_converter(
|
|
@@ -74,7 +74,6 @@ class PolarsCursor(WithFetch):
|
|
|
74
74
|
unload: bool = False,
|
|
75
75
|
result_reuse_enable: bool = False,
|
|
76
76
|
result_reuse_minutes: int = CursorIterator.DEFAULT_RESULT_REUSE_MINUTES,
|
|
77
|
-
on_start_query_execution: Callable[[str], None] | None = None,
|
|
78
77
|
block_size: int | None = None,
|
|
79
78
|
cache_type: str | None = None,
|
|
80
79
|
max_workers: int = (cpu_count() or 1) * 5,
|
|
@@ -95,7 +94,6 @@ class PolarsCursor(WithFetch):
|
|
|
95
94
|
unload: Enable UNLOAD for high-performance Parquet output.
|
|
96
95
|
result_reuse_enable: Enable Athena query result reuse.
|
|
97
96
|
result_reuse_minutes: Minutes to reuse cached results.
|
|
98
|
-
on_start_query_execution: Callback invoked when query starts.
|
|
99
97
|
block_size: S3 read block size.
|
|
100
98
|
cache_type: S3 caching strategy.
|
|
101
99
|
max_workers: Maximum worker threads for parallel S3 operations.
|
|
@@ -123,7 +121,6 @@ class PolarsCursor(WithFetch):
|
|
|
123
121
|
**kwargs,
|
|
124
122
|
)
|
|
125
123
|
self._unload = unload
|
|
126
|
-
self._on_start_query_execution = on_start_query_execution
|
|
127
124
|
self._block_size = block_size
|
|
128
125
|
self._cache_type = cache_type
|
|
129
126
|
self._max_workers = max_workers
|
|
@@ -58,7 +58,6 @@ class S3FSCursor(WithFetch):
|
|
|
58
58
|
kill_on_interrupt: bool = True,
|
|
59
59
|
result_reuse_enable: bool = False,
|
|
60
60
|
result_reuse_minutes: int = CursorIterator.DEFAULT_RESULT_REUSE_MINUTES,
|
|
61
|
-
on_start_query_execution: Callable[[str], None] | None = None,
|
|
62
61
|
csv_reader: CSVReaderType | None = None,
|
|
63
62
|
**kwargs,
|
|
64
63
|
) -> None:
|
|
@@ -75,7 +74,6 @@ class S3FSCursor(WithFetch):
|
|
|
75
74
|
kill_on_interrupt: Cancel running query on keyboard interrupt.
|
|
76
75
|
result_reuse_enable: Enable Athena query result reuse.
|
|
77
76
|
result_reuse_minutes: Minutes to reuse cached results.
|
|
78
|
-
on_start_query_execution: Callback invoked when query starts.
|
|
79
77
|
csv_reader: CSV reader class to use for parsing results.
|
|
80
78
|
Use AthenaCSVReader (default) to distinguish between NULL
|
|
81
79
|
(unquoted empty) and empty string (quoted empty "").
|
|
@@ -104,7 +102,6 @@ class S3FSCursor(WithFetch):
|
|
|
104
102
|
result_reuse_minutes=result_reuse_minutes,
|
|
105
103
|
**kwargs,
|
|
106
104
|
)
|
|
107
|
-
self._on_start_query_execution = on_start_query_execution
|
|
108
105
|
self._csv_reader = csv_reader
|
|
109
106
|
|
|
110
107
|
@staticmethod
|
|
@@ -201,6 +201,8 @@ class SparkBaseCursor(BaseCursor, metaclass=ABCMeta):
|
|
|
201
201
|
def __poll(self, query_id: str) -> AthenaQueryExecution | AthenaCalculationExecution:
|
|
202
202
|
while True:
|
|
203
203
|
calculation_status = self._get_calculation_execution_status(query_id)
|
|
204
|
+
if self._on_poll:
|
|
205
|
+
self._on_poll(calculation_status)
|
|
204
206
|
if calculation_status.state in [
|
|
205
207
|
AthenaCalculationExecutionStatus.STATE_COMPLETED,
|
|
206
208
|
AthenaCalculationExecutionStatus.STATE_FAILED,
|
|
@@ -207,8 +207,7 @@ class AthenaDialect(DefaultDialect):
|
|
|
207
207
|
# awsathena+rest://
|
|
208
208
|
# {aws_access_key_id}:{aws_secret_access_key}@athena.{region_name}.amazonaws.com:443/
|
|
209
209
|
# {schema_name}?s3_staging_dir={s3_staging_dir}&...
|
|
210
|
-
|
|
211
|
-
return cast(tuple[str], ()), self._connect_options
|
|
210
|
+
return cast(tuple[str], ()), self._create_connect_args(url)
|
|
212
211
|
|
|
213
212
|
def _create_connect_args(self, url: URL) -> dict[str, Any]:
|
|
214
213
|
opts: dict[str, Any] = {
|
|
@@ -238,6 +237,12 @@ class AthenaDialect(DefaultDialect):
|
|
|
238
237
|
opts.update({"result_reuse_enable": bool(strtobool(opts["result_reuse_enable"]))})
|
|
239
238
|
if "result_reuse_minutes" in opts:
|
|
240
239
|
opts.update({"result_reuse_minutes": int(opts["result_reuse_minutes"])})
|
|
240
|
+
# Store on the dialect so compilers can consult connection options
|
|
241
|
+
# (e.g. catalog_name for S3 Tables detection). Assigned here rather than
|
|
242
|
+
# in create_connect_args because subclass dialects call this method
|
|
243
|
+
# directly and mutate the returned dict afterwards; sharing the same
|
|
244
|
+
# object keeps _connect_options in sync with their updates.
|
|
245
|
+
self._connect_options = opts
|
|
241
246
|
return opts
|
|
242
247
|
|
|
243
248
|
@reflection.cache
|
|
@@ -39,6 +39,13 @@ if TYPE_CHECKING:
|
|
|
39
39
|
_DialectArgDict = Mapping[str, Any]
|
|
40
40
|
CreateColumn = Any
|
|
41
41
|
|
|
42
|
+
# Prefix of the Athena data catalog name registered for an Amazon S3 Tables
|
|
43
|
+
# table bucket (e.g. ``s3tablescatalog/my-bucket``). It is selected via the
|
|
44
|
+
# connection ``catalog_name``. S3 Tables are Iceberg-backed and use managed
|
|
45
|
+
# storage, so their CREATE TABLE statements must not include a LOCATION clause.
|
|
46
|
+
# https://docs.aws.amazon.com/AmazonS3/latest/userguide/s3-tables-integrations-query-athena.html
|
|
47
|
+
S3_TABLES_CATALOG_PREFIX = "s3tablescatalog/"
|
|
48
|
+
|
|
42
49
|
|
|
43
50
|
class AthenaTypeCompiler(GenericTypeCompiler):
|
|
44
51
|
"""Type compiler for Amazon Athena SQL types.
|
|
@@ -305,6 +312,10 @@ class AthenaDDLCompiler(DDLCompiler):
|
|
|
305
312
|
|
|
306
313
|
- External table creation (EXTERNAL keyword for Hive-style tables)
|
|
307
314
|
- Iceberg table creation (managed tables with ACID support)
|
|
315
|
+
- Amazon S3 Tables (Iceberg-backed, managed storage): set the connection
|
|
316
|
+
``catalog_name`` to ``s3tablescatalog/<table-bucket>`` and use the
|
|
317
|
+
namespace as the table ``schema``. The LOCATION clause is omitted since
|
|
318
|
+
storage is managed.
|
|
308
319
|
- File formats: PARQUET, ORC, TEXTFILE, JSON, AVRO, etc.
|
|
309
320
|
- Row formats with SerDe specifications
|
|
310
321
|
- Compression settings for various file formats
|
|
@@ -445,6 +456,74 @@ class AthenaDDLCompiler(DDLCompiler):
|
|
|
445
456
|
text.append(")")
|
|
446
457
|
return "\n".join(text)
|
|
447
458
|
|
|
459
|
+
@staticmethod
|
|
460
|
+
def _is_s3_tables_catalog(connect_opts: Mapping[str, Any]) -> bool:
|
|
461
|
+
"""Return whether the connection targets an Amazon S3 Tables catalog.
|
|
462
|
+
|
|
463
|
+
S3 Tables are queried by setting the connection ``catalog_name`` to
|
|
464
|
+
``s3tablescatalog/<table-bucket>`` and using the namespace as the table
|
|
465
|
+
``schema`` (a two-part ``namespace.table`` identifier). Athena rejects a
|
|
466
|
+
three-part ``catalog.namespace.table`` identifier in DDL, so the catalog
|
|
467
|
+
must be selected at the connection level. Such tables use managed
|
|
468
|
+
storage, so their CREATE TABLE statement must omit the LOCATION clause.
|
|
469
|
+
|
|
470
|
+
Args:
|
|
471
|
+
connect_opts: The dialect connection options.
|
|
472
|
+
|
|
473
|
+
Returns:
|
|
474
|
+
True if ``catalog_name`` names an S3 Tables catalog.
|
|
475
|
+
"""
|
|
476
|
+
if not connect_opts:
|
|
477
|
+
return False
|
|
478
|
+
catalog = connect_opts.get("catalog_name") or ""
|
|
479
|
+
# Athena resolves catalog names case-insensitively.
|
|
480
|
+
return catalog.lower().startswith(S3_TABLES_CATALOG_PREFIX)
|
|
481
|
+
|
|
482
|
+
def _is_iceberg_table(
|
|
483
|
+
self, dialect_opts: _DialectArgDict, connect_opts: Mapping[str, Any]
|
|
484
|
+
) -> bool:
|
|
485
|
+
"""Return whether the table properties declare an Iceberg table.
|
|
486
|
+
|
|
487
|
+
Args:
|
|
488
|
+
dialect_opts: The table's ``awsathena_*`` dialect options.
|
|
489
|
+
connect_opts: The dialect connection options.
|
|
490
|
+
|
|
491
|
+
Returns:
|
|
492
|
+
True if the rendered TBLPROPERTIES set ``table_type`` to Iceberg.
|
|
493
|
+
"""
|
|
494
|
+
table_properties = self._get_table_properties_specification(
|
|
495
|
+
dialect_opts, connect_opts
|
|
496
|
+
).lower()
|
|
497
|
+
return ("table_type" in table_properties) and ("iceberg" in table_properties)
|
|
498
|
+
|
|
499
|
+
def _validate_s3_tables_create_table(
|
|
500
|
+
self, dialect_opts: _DialectArgDict, connect_opts: Mapping[str, Any]
|
|
501
|
+
) -> None:
|
|
502
|
+
"""Validate a CREATE TABLE compiled against an S3 Tables catalog.
|
|
503
|
+
|
|
504
|
+
S3 Tables support only Iceberg tables on managed storage, so the table
|
|
505
|
+
must declare ``table_type`` ICEBERG and must not specify a location.
|
|
506
|
+
Raising here surfaces a clear client-side error instead of emitting DDL
|
|
507
|
+
that Athena would reject.
|
|
508
|
+
|
|
509
|
+
Args:
|
|
510
|
+
dialect_opts: The table's ``awsathena_*`` dialect options.
|
|
511
|
+
connect_opts: The dialect connection options.
|
|
512
|
+
|
|
513
|
+
Raises:
|
|
514
|
+
exc.CompileError: If the table is not Iceberg or specifies a location.
|
|
515
|
+
"""
|
|
516
|
+
if not self._is_iceberg_table(dialect_opts, connect_opts):
|
|
517
|
+
raise exc.CompileError(
|
|
518
|
+
"S3 Tables support only Iceberg tables; specify the dialect keyword "
|
|
519
|
+
"argument `awsathena_tblproperties={'table_type': 'ICEBERG'}`"
|
|
520
|
+
)
|
|
521
|
+
if dialect_opts["location"]:
|
|
522
|
+
raise exc.CompileError(
|
|
523
|
+
"S3 Tables use managed storage and do not accept a table location; "
|
|
524
|
+
"remove the dialect keyword argument `awsathena_location`"
|
|
525
|
+
)
|
|
526
|
+
|
|
448
527
|
def _get_table_location(
|
|
449
528
|
self, table: Table, dialect_opts: _DialectArgDict, connect_opts: Mapping[str, Any]
|
|
450
529
|
) -> str | None:
|
|
@@ -662,12 +741,7 @@ class AthenaDDLCompiler(DDLCompiler):
|
|
|
662
741
|
dialect = cast("AthenaDialect", self.dialect)
|
|
663
742
|
connect_opts = dialect._connect_options
|
|
664
743
|
|
|
665
|
-
|
|
666
|
-
dialect_opts, connect_opts
|
|
667
|
-
).lower()
|
|
668
|
-
is_iceberg = False
|
|
669
|
-
if ("table_type" in table_properties) and ("iceberg" in table_properties):
|
|
670
|
-
is_iceberg = True
|
|
744
|
+
is_iceberg = self._is_iceberg_table(dialect_opts, connect_opts)
|
|
671
745
|
|
|
672
746
|
# https://docs.aws.amazon.com/athena/latest/ug/querying-iceberg-creating-tables.html
|
|
673
747
|
text = ["\nCREATE TABLE"] if is_iceberg else ["\nCREATE EXTERNAL TABLE"]
|
|
@@ -705,11 +779,19 @@ class AthenaDDLCompiler(DDLCompiler):
|
|
|
705
779
|
dialect_opts: _DialectArgDict = table.dialect_options["awsathena"]
|
|
706
780
|
dialect = cast("AthenaDialect", self.dialect)
|
|
707
781
|
connect_opts = dialect._connect_options
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
self.
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
782
|
+
if self._is_s3_tables_catalog(connect_opts):
|
|
783
|
+
# S3 Tables are managed Iceberg tables: ROW FORMAT, SERDEPROPERTIES,
|
|
784
|
+
# STORED AS, and LOCATION are not accepted, so emit only TBLPROPERTIES.
|
|
785
|
+
self._validate_s3_tables_create_table(dialect_opts, connect_opts)
|
|
786
|
+
text = [
|
|
787
|
+
self._get_table_properties_specification(dialect_opts, connect_opts),
|
|
788
|
+
]
|
|
789
|
+
else:
|
|
790
|
+
text = [
|
|
791
|
+
self._get_row_format_specification(dialect_opts, connect_opts),
|
|
792
|
+
self._get_serde_properties_specification(dialect_opts, connect_opts),
|
|
793
|
+
self._get_file_format_specification(dialect_opts, connect_opts),
|
|
794
|
+
self._get_table_location_specification(table, dialect_opts, connect_opts),
|
|
795
|
+
self._get_table_properties_specification(dialect_opts, connect_opts),
|
|
796
|
+
]
|
|
715
797
|
return "\n".join([t for t in text if t])
|
|
@@ -5,7 +5,7 @@ authors = [
|
|
|
5
5
|
{name = "laughingman7743", email = "laughingman7743@gmail.com"},
|
|
6
6
|
]
|
|
7
7
|
dependencies = [
|
|
8
|
-
"boto3>=1.
|
|
8
|
+
"boto3>=1.41.2",
|
|
9
9
|
"botocore>=1.41.2",
|
|
10
10
|
"tenacity>=4.1.0",
|
|
11
11
|
"fsspec",
|
|
@@ -223,12 +223,12 @@ TEST_TYPE =
|
|
|
223
223
|
allowlist_externals =
|
|
224
224
|
uv
|
|
225
225
|
uvx
|
|
226
|
-
|
|
226
|
+
just
|
|
227
227
|
commands =
|
|
228
228
|
uv sync --group dev
|
|
229
|
-
pyathena:
|
|
230
|
-
sqla:
|
|
231
|
-
sqla_async:
|
|
229
|
+
pyathena: just test pyathena
|
|
230
|
+
sqla: just test sqla
|
|
231
|
+
sqla_async: just test sqla-async
|
|
232
232
|
passenv =
|
|
233
233
|
TOXENV
|
|
234
234
|
AWS_*
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|