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.
Files changed (88) hide show
  1. {pyathena-3.31.1 → pyathena-3.33.0}/PKG-INFO +2 -2
  2. {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/_version.py +2 -2
  3. {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/aio/common.py +2 -0
  4. {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/aio/spark/cursor.py +2 -0
  5. {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/arrow/cursor.py +0 -3
  6. {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/common.py +18 -0
  7. {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/connection.py +10 -1
  8. {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/cursor.py +0 -2
  9. {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/pandas/cursor.py +0 -3
  10. {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/polars/cursor.py +0 -3
  11. {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/s3fs/cursor.py +0 -3
  12. {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/spark/common.py +2 -0
  13. {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/sqlalchemy/base.py +7 -2
  14. {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/sqlalchemy/compiler.py +95 -13
  15. {pyathena-3.31.1 → pyathena-3.33.0}/pyproject.toml +5 -5
  16. {pyathena-3.31.1 → pyathena-3.33.0}/.gitignore +0 -0
  17. {pyathena-3.31.1 → pyathena-3.33.0}/LICENSE +0 -0
  18. {pyathena-3.31.1 → pyathena-3.33.0}/README.md +0 -0
  19. {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/__init__.py +0 -0
  20. {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/aio/__init__.py +0 -0
  21. {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/aio/arrow/__init__.py +0 -0
  22. {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/aio/arrow/cursor.py +0 -0
  23. {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/aio/connection.py +0 -0
  24. {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/aio/cursor.py +0 -0
  25. {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/aio/pandas/__init__.py +0 -0
  26. {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/aio/pandas/cursor.py +0 -0
  27. {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/aio/polars/__init__.py +0 -0
  28. {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/aio/polars/cursor.py +0 -0
  29. {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/aio/result_set.py +0 -0
  30. {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/aio/s3fs/__init__.py +0 -0
  31. {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/aio/s3fs/cursor.py +0 -0
  32. {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/aio/spark/__init__.py +0 -0
  33. {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/aio/sqlalchemy/__init__.py +0 -0
  34. {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/aio/sqlalchemy/arrow.py +0 -0
  35. {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/aio/sqlalchemy/base.py +0 -0
  36. {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/aio/sqlalchemy/pandas.py +0 -0
  37. {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/aio/sqlalchemy/polars.py +0 -0
  38. {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/aio/sqlalchemy/rest.py +0 -0
  39. {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/aio/sqlalchemy/s3fs.py +0 -0
  40. {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/aio/util.py +0 -0
  41. {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/arrow/__init__.py +0 -0
  42. {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/arrow/async_cursor.py +0 -0
  43. {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/arrow/converter.py +0 -0
  44. {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/arrow/result_set.py +0 -0
  45. {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/arrow/util.py +0 -0
  46. {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/async_cursor.py +0 -0
  47. {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/converter.py +0 -0
  48. {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/error.py +0 -0
  49. {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/filesystem/__init__.py +0 -0
  50. {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/filesystem/s3.py +0 -0
  51. {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/filesystem/s3_async.py +0 -0
  52. {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/filesystem/s3_executor.py +0 -0
  53. {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/filesystem/s3_object.py +0 -0
  54. {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/formatter.py +0 -0
  55. {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/model.py +0 -0
  56. {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/pandas/__init__.py +0 -0
  57. {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/pandas/async_cursor.py +0 -0
  58. {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/pandas/converter.py +0 -0
  59. {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/pandas/result_set.py +0 -0
  60. {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/pandas/util.py +0 -0
  61. {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/parser.py +0 -0
  62. {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/polars/__init__.py +0 -0
  63. {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/polars/async_cursor.py +0 -0
  64. {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/polars/converter.py +0 -0
  65. {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/polars/result_set.py +0 -0
  66. {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/polars/util.py +0 -0
  67. {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/py.typed +0 -0
  68. {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/result_set.py +0 -0
  69. {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/s3fs/__init__.py +0 -0
  70. {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/s3fs/async_cursor.py +0 -0
  71. {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/s3fs/converter.py +0 -0
  72. {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/s3fs/reader.py +0 -0
  73. {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/s3fs/result_set.py +0 -0
  74. {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/spark/__init__.py +0 -0
  75. {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/spark/async_cursor.py +0 -0
  76. {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/spark/cursor.py +0 -0
  77. {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/sqlalchemy/__init__.py +0 -0
  78. {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/sqlalchemy/arrow.py +0 -0
  79. {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/sqlalchemy/constants.py +0 -0
  80. {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/sqlalchemy/pandas.py +0 -0
  81. {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/sqlalchemy/polars.py +0 -0
  82. {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/sqlalchemy/preparer.py +0 -0
  83. {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/sqlalchemy/requirements.py +0 -0
  84. {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/sqlalchemy/rest.py +0 -0
  85. {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/sqlalchemy/s3fs.py +0 -0
  86. {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/sqlalchemy/types.py +0 -0
  87. {pyathena-3.31.1 → pyathena-3.33.0}/pyathena/sqlalchemy/util.py +0 -0
  88. {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.31.1
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.38.2
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.31.1'
22
- __version_tuple__ = version_tuple = (3, 31, 1)
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
- self._connect_options = self._create_connect_args(url)
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
- table_properties = self._get_table_properties_specification(
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
- text = [
709
- self._get_row_format_specification(dialect_opts, connect_opts),
710
- self._get_serde_properties_specification(dialect_opts, connect_opts),
711
- self._get_file_format_specification(dialect_opts, connect_opts),
712
- self._get_table_location_specification(table, dialect_opts, connect_opts),
713
- self._get_table_properties_specification(dialect_opts, connect_opts),
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.38.2",
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
- make
226
+ just
227
227
  commands =
228
228
  uv sync --group dev
229
- pyathena: make test/pyathena
230
- sqla: make test/sqla
231
- sqla_async: make test/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