PyAthena 3.27.1__tar.gz → 3.28.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.27.1 → pyathena-3.28.0}/PKG-INFO +12 -10
- {pyathena-3.27.1 → pyathena-3.28.0}/README.md +7 -7
- {pyathena-3.27.1 → pyathena-3.28.0}/pyathena/__init__.py +4 -1
- {pyathena-3.27.1 → pyathena-3.28.0}/pyathena/_version.py +2 -2
- {pyathena-3.27.1 → pyathena-3.28.0}/pyathena/arrow/async_cursor.py +2 -1
- {pyathena-3.27.1 → pyathena-3.28.0}/pyathena/arrow/cursor.py +2 -1
- {pyathena-3.27.1 → pyathena-3.28.0}/pyathena/arrow/result_set.py +21 -0
- {pyathena-3.27.1 → pyathena-3.28.0}/pyathena/common.py +6 -4
- {pyathena-3.27.1 → pyathena-3.28.0}/pyathena/connection.py +18 -8
- {pyathena-3.27.1 → pyathena-3.28.0}/pyathena/pandas/async_cursor.py +2 -1
- {pyathena-3.27.1 → pyathena-3.28.0}/pyathena/pandas/cursor.py +2 -1
- {pyathena-3.27.1 → pyathena-3.28.0}/pyathena/pandas/result_set.py +22 -0
- {pyathena-3.27.1 → pyathena-3.28.0}/pyathena/polars/async_cursor.py +2 -1
- {pyathena-3.27.1 → pyathena-3.28.0}/pyathena/polars/cursor.py +2 -1
- {pyathena-3.27.1 → pyathena-3.28.0}/pyathena/polars/result_set.py +30 -8
- {pyathena-3.27.1 → pyathena-3.28.0}/pyathena/result_set.py +126 -22
- {pyathena-3.27.1 → pyathena-3.28.0}/pyathena/s3fs/result_set.py +5 -1
- {pyathena-3.27.1 → pyathena-3.28.0}/pyproject.toml +4 -2
- {pyathena-3.27.1 → pyathena-3.28.0}/.gitignore +0 -0
- {pyathena-3.27.1 → pyathena-3.28.0}/LICENSE +0 -0
- {pyathena-3.27.1 → pyathena-3.28.0}/pyathena/arrow/__init__.py +0 -0
- {pyathena-3.27.1 → pyathena-3.28.0}/pyathena/arrow/converter.py +0 -0
- {pyathena-3.27.1 → pyathena-3.28.0}/pyathena/arrow/util.py +0 -0
- {pyathena-3.27.1 → pyathena-3.28.0}/pyathena/async_cursor.py +0 -0
- {pyathena-3.27.1 → pyathena-3.28.0}/pyathena/converter.py +0 -0
- {pyathena-3.27.1 → pyathena-3.28.0}/pyathena/cursor.py +0 -0
- {pyathena-3.27.1 → pyathena-3.28.0}/pyathena/error.py +0 -0
- {pyathena-3.27.1 → pyathena-3.28.0}/pyathena/filesystem/__init__.py +0 -0
- {pyathena-3.27.1 → pyathena-3.28.0}/pyathena/filesystem/s3.py +0 -0
- {pyathena-3.27.1 → pyathena-3.28.0}/pyathena/filesystem/s3_object.py +0 -0
- {pyathena-3.27.1 → pyathena-3.28.0}/pyathena/formatter.py +0 -0
- {pyathena-3.27.1 → pyathena-3.28.0}/pyathena/model.py +0 -0
- {pyathena-3.27.1 → pyathena-3.28.0}/pyathena/pandas/__init__.py +0 -0
- {pyathena-3.27.1 → pyathena-3.28.0}/pyathena/pandas/converter.py +0 -0
- {pyathena-3.27.1 → pyathena-3.28.0}/pyathena/pandas/util.py +0 -0
- {pyathena-3.27.1 → pyathena-3.28.0}/pyathena/polars/__init__.py +0 -0
- {pyathena-3.27.1 → pyathena-3.28.0}/pyathena/polars/converter.py +0 -0
- {pyathena-3.27.1 → pyathena-3.28.0}/pyathena/polars/util.py +0 -0
- {pyathena-3.27.1 → pyathena-3.28.0}/pyathena/py.typed +0 -0
- {pyathena-3.27.1 → pyathena-3.28.0}/pyathena/s3fs/__init__.py +0 -0
- {pyathena-3.27.1 → pyathena-3.28.0}/pyathena/s3fs/async_cursor.py +0 -0
- {pyathena-3.27.1 → pyathena-3.28.0}/pyathena/s3fs/converter.py +0 -0
- {pyathena-3.27.1 → pyathena-3.28.0}/pyathena/s3fs/cursor.py +0 -0
- {pyathena-3.27.1 → pyathena-3.28.0}/pyathena/s3fs/reader.py +0 -0
- {pyathena-3.27.1 → pyathena-3.28.0}/pyathena/spark/__init__.py +0 -0
- {pyathena-3.27.1 → pyathena-3.28.0}/pyathena/spark/async_cursor.py +0 -0
- {pyathena-3.27.1 → pyathena-3.28.0}/pyathena/spark/common.py +0 -0
- {pyathena-3.27.1 → pyathena-3.28.0}/pyathena/spark/cursor.py +0 -0
- {pyathena-3.27.1 → pyathena-3.28.0}/pyathena/sqlalchemy/__init__.py +0 -0
- {pyathena-3.27.1 → pyathena-3.28.0}/pyathena/sqlalchemy/arrow.py +0 -0
- {pyathena-3.27.1 → pyathena-3.28.0}/pyathena/sqlalchemy/base.py +0 -0
- {pyathena-3.27.1 → pyathena-3.28.0}/pyathena/sqlalchemy/compiler.py +0 -0
- {pyathena-3.27.1 → pyathena-3.28.0}/pyathena/sqlalchemy/constants.py +0 -0
- {pyathena-3.27.1 → pyathena-3.28.0}/pyathena/sqlalchemy/pandas.py +0 -0
- {pyathena-3.27.1 → pyathena-3.28.0}/pyathena/sqlalchemy/polars.py +0 -0
- {pyathena-3.27.1 → pyathena-3.28.0}/pyathena/sqlalchemy/preparer.py +0 -0
- {pyathena-3.27.1 → pyathena-3.28.0}/pyathena/sqlalchemy/requirements.py +0 -0
- {pyathena-3.27.1 → pyathena-3.28.0}/pyathena/sqlalchemy/rest.py +0 -0
- {pyathena-3.27.1 → pyathena-3.28.0}/pyathena/sqlalchemy/s3fs.py +0 -0
- {pyathena-3.27.1 → pyathena-3.28.0}/pyathena/sqlalchemy/types.py +0 -0
- {pyathena-3.27.1 → pyathena-3.28.0}/pyathena/sqlalchemy/util.py +0 -0
- {pyathena-3.27.1 → pyathena-3.28.0}/pyathena/util.py +0 -0
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: PyAthena
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.28.0
|
|
4
4
|
Summary: Python DB API 2.0 (PEP 249) client for Amazon Athena
|
|
5
|
-
Project-URL: homepage, https://github.com/
|
|
6
|
-
Project-URL: repository, https://github.com/
|
|
5
|
+
Project-URL: homepage, https://github.com/pyathena-dev/PyAthena/
|
|
6
|
+
Project-URL: repository, https://github.com/pyathena-dev/PyAthena/
|
|
7
|
+
Project-URL: documentation, https://pyathena.dev/
|
|
8
|
+
Project-URL: issues, https://github.com/pyathena-dev/PyAthena/issues
|
|
7
9
|
Author-email: laughingman7743 <laughingman7743@gmail.com>
|
|
8
10
|
License: Copyright 2017 laughingman7743
|
|
9
11
|
|
|
@@ -57,14 +59,14 @@ Description-Content-Type: text/markdown
|
|
|
57
59
|
|
|
58
60
|
<div align="center">
|
|
59
61
|
|
|
60
|
-
<img src="https://raw.githubusercontent.com/
|
|
62
|
+
<img src="https://raw.githubusercontent.com/pyathena-dev/PyAthena/master/docs/_static/icon.png" alt="PyAthena logo" width="250">
|
|
61
63
|
|
|
62
64
|
[](https://badge.fury.io/py/pyathena)
|
|
63
65
|
[](https://pypi.org/project/PyAthena/)
|
|
64
66
|
[](https://pepy.tech/project/pyathena)
|
|
65
|
-
[](https://github.com/
|
|
67
|
+
[](https://github.com/pyathena-dev/PyAthena/actions/workflows/test.yaml)
|
|
68
|
+
[](https://github.com/pyathena-dev/PyAthena/actions/workflows/docs.yaml)
|
|
69
|
+
[](https://github.com/pyathena-dev/PyAthena/blob/master/LICENSE)
|
|
68
70
|
[](https://github.com/astral-sh/ruff)
|
|
69
71
|
[](https://mypy-lang.org/)
|
|
70
72
|
|
|
@@ -115,10 +117,10 @@ Many of the implementations in this library are based on [PyHive](https://github
|
|
|
115
117
|
|
|
116
118
|
## Links
|
|
117
119
|
|
|
118
|
-
- Documentation: https://
|
|
120
|
+
- Documentation: https://pyathena.dev/
|
|
119
121
|
- PyPI Releases: https://pypi.org/project/PyAthena/
|
|
120
|
-
- Source Code: https://github.com/
|
|
121
|
-
- Issue Tracker: https://github.com/
|
|
122
|
+
- Source Code: https://github.com/pyathena-dev/PyAthena/
|
|
123
|
+
- Issue Tracker: https://github.com/pyathena-dev/PyAthena/issues
|
|
122
124
|
|
|
123
125
|
## Logo
|
|
124
126
|
|
|
@@ -2,14 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
<div align="center">
|
|
4
4
|
|
|
5
|
-
<img src="https://raw.githubusercontent.com/
|
|
5
|
+
<img src="https://raw.githubusercontent.com/pyathena-dev/PyAthena/master/docs/_static/icon.png" alt="PyAthena logo" width="250">
|
|
6
6
|
|
|
7
7
|
[](https://badge.fury.io/py/pyathena)
|
|
8
8
|
[](https://pypi.org/project/PyAthena/)
|
|
9
9
|
[](https://pepy.tech/project/pyathena)
|
|
10
|
-
[](https://github.com/
|
|
10
|
+
[](https://github.com/pyathena-dev/PyAthena/actions/workflows/test.yaml)
|
|
11
|
+
[](https://github.com/pyathena-dev/PyAthena/actions/workflows/docs.yaml)
|
|
12
|
+
[](https://github.com/pyathena-dev/PyAthena/blob/master/LICENSE)
|
|
13
13
|
[](https://github.com/astral-sh/ruff)
|
|
14
14
|
[](https://mypy-lang.org/)
|
|
15
15
|
|
|
@@ -60,10 +60,10 @@ Many of the implementations in this library are based on [PyHive](https://github
|
|
|
60
60
|
|
|
61
61
|
## Links
|
|
62
62
|
|
|
63
|
-
- Documentation: https://
|
|
63
|
+
- Documentation: https://pyathena.dev/
|
|
64
64
|
- PyPI Releases: https://pypi.org/project/PyAthena/
|
|
65
|
-
- Source Code: https://github.com/
|
|
66
|
-
- Issue Tracker: https://github.com/
|
|
65
|
+
- Source Code: https://github.com/pyathena-dev/PyAthena/
|
|
66
|
+
- Issue Tracker: https://github.com/pyathena-dev/PyAthena/issues
|
|
67
67
|
|
|
68
68
|
## Logo
|
|
69
69
|
|
|
@@ -85,6 +85,9 @@ def connect(*args, **kwargs) -> "Connection[Any]":
|
|
|
85
85
|
Args:
|
|
86
86
|
s3_staging_dir: S3 location to store query results. Required if not
|
|
87
87
|
using workgroups or if the workgroup doesn't have a result location.
|
|
88
|
+
Pass an empty string to explicitly disable S3 staging and skip
|
|
89
|
+
the ``AWS_ATHENA_S3_STAGING_DIR`` environment variable fallback
|
|
90
|
+
(required for workgroups with managed query result storage).
|
|
88
91
|
region_name: AWS region name. If not specified, uses the default region
|
|
89
92
|
from your AWS configuration.
|
|
90
93
|
schema_name: Athena database/schema name. Defaults to "default".
|
|
@@ -109,7 +112,7 @@ def connect(*args, **kwargs) -> "Connection[Any]":
|
|
|
109
112
|
A Connection object that can be used to create cursors and execute queries.
|
|
110
113
|
|
|
111
114
|
Raises:
|
|
112
|
-
|
|
115
|
+
ProgrammingError: If neither s3_staging_dir nor work_group is provided.
|
|
113
116
|
|
|
114
117
|
Example:
|
|
115
118
|
>>> import pyathena
|
|
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
|
|
|
28
28
|
commit_id: COMMIT_ID
|
|
29
29
|
__commit_id__: COMMIT_ID
|
|
30
30
|
|
|
31
|
-
__version__ = version = '3.
|
|
32
|
-
__version_tuple__ = version_tuple = (3,
|
|
31
|
+
__version__ = version = '3.28.0'
|
|
32
|
+
__version_tuple__ = version_tuple = (3, 28, 0)
|
|
33
33
|
|
|
34
34
|
__commit_id__ = commit_id = None
|
|
@@ -184,7 +184,8 @@ class AsyncArrowCursor(AsyncCursor):
|
|
|
184
184
|
) -> Tuple[str, "Future[Union[AthenaArrowResultSet, Any]]"]:
|
|
185
185
|
if self._unload:
|
|
186
186
|
s3_staging_dir = s3_staging_dir if s3_staging_dir else self._s3_staging_dir
|
|
187
|
-
|
|
187
|
+
if not s3_staging_dir:
|
|
188
|
+
raise ProgrammingError("If the unload option is used, s3_staging_dir is required.")
|
|
188
189
|
operation, unload_location = self._formatter.wrap_unload(
|
|
189
190
|
operation,
|
|
190
191
|
s3_staging_dir=s3_staging_dir,
|
|
@@ -209,7 +209,8 @@ class ArrowCursor(BaseCursor, CursorIterator, WithResultSet):
|
|
|
209
209
|
self._reset_state()
|
|
210
210
|
if self._unload:
|
|
211
211
|
s3_staging_dir = s3_staging_dir if s3_staging_dir else self._s3_staging_dir
|
|
212
|
-
|
|
212
|
+
if not s3_staging_dir:
|
|
213
|
+
raise ProgrammingError("If the unload option is used, s3_staging_dir is required.")
|
|
213
214
|
operation, unload_location = self._formatter.wrap_unload(
|
|
214
215
|
operation,
|
|
215
216
|
s3_staging_dir=s3_staging_dir,
|
|
@@ -117,6 +117,8 @@ class AthenaArrowResultSet(AthenaResultSet):
|
|
|
117
117
|
self._fs = self.__s3_file_system()
|
|
118
118
|
if self.state == AthenaQueryExecution.STATE_SUCCEEDED and self.output_location:
|
|
119
119
|
self._table = self._as_arrow()
|
|
120
|
+
elif self.state == AthenaQueryExecution.STATE_SUCCEEDED:
|
|
121
|
+
self._table = self._as_arrow_from_api()
|
|
120
122
|
else:
|
|
121
123
|
import pyarrow as pa
|
|
122
124
|
|
|
@@ -346,6 +348,25 @@ class AthenaArrowResultSet(AthenaResultSet):
|
|
|
346
348
|
table = self._read_csv()
|
|
347
349
|
return table
|
|
348
350
|
|
|
351
|
+
def _as_arrow_from_api(self, converter: Optional[Converter] = None) -> "Table":
|
|
352
|
+
"""Build an Arrow Table from GetQueryResults API.
|
|
353
|
+
|
|
354
|
+
Used as a fallback when ``output_location`` is not available
|
|
355
|
+
(e.g. managed query result storage).
|
|
356
|
+
|
|
357
|
+
Args:
|
|
358
|
+
converter: Type converter for result values. Defaults to
|
|
359
|
+
``DefaultTypeConverter`` if not specified.
|
|
360
|
+
"""
|
|
361
|
+
import pyarrow as pa
|
|
362
|
+
|
|
363
|
+
rows = self._fetch_all_rows(converter)
|
|
364
|
+
if not rows:
|
|
365
|
+
return pa.Table.from_pydict({})
|
|
366
|
+
description = self.description if self.description else []
|
|
367
|
+
columns = [d[0] for d in description]
|
|
368
|
+
return pa.table(self._rows_to_columnar(rows, columns))
|
|
369
|
+
|
|
349
370
|
def as_arrow(self) -> "Table":
|
|
350
371
|
return self._table
|
|
351
372
|
|
|
@@ -210,15 +210,15 @@ class BaseCursor(metaclass=ABCMeta):
|
|
|
210
210
|
request: Dict[str, Any] = {
|
|
211
211
|
"QueryString": query,
|
|
212
212
|
"QueryExecutionContext": {},
|
|
213
|
-
"ResultConfiguration": {},
|
|
214
213
|
}
|
|
215
214
|
if self._schema_name:
|
|
216
215
|
request["QueryExecutionContext"].update({"Database": self._schema_name})
|
|
217
216
|
if self._catalog_name:
|
|
218
217
|
request["QueryExecutionContext"].update({"Catalog": self._catalog_name})
|
|
218
|
+
result_configuration: Dict[str, Any] = {}
|
|
219
219
|
if self._s3_staging_dir or s3_staging_dir:
|
|
220
|
-
|
|
221
|
-
|
|
220
|
+
result_configuration["OutputLocation"] = (
|
|
221
|
+
s3_staging_dir if s3_staging_dir else self._s3_staging_dir
|
|
222
222
|
)
|
|
223
223
|
if self._work_group or work_group:
|
|
224
224
|
request.update({"WorkGroup": work_group if work_group else self._work_group})
|
|
@@ -228,7 +228,9 @@ class BaseCursor(metaclass=ABCMeta):
|
|
|
228
228
|
}
|
|
229
229
|
if self._kms_key:
|
|
230
230
|
enc_conf.update({"KmsKey": self._kms_key})
|
|
231
|
-
|
|
231
|
+
result_configuration["EncryptionConfiguration"] = enc_conf
|
|
232
|
+
if result_configuration:
|
|
233
|
+
request["ResultConfiguration"] = result_configuration
|
|
232
234
|
if self._result_reuse_enable or result_reuse_enable:
|
|
233
235
|
reuse_conf = {
|
|
234
236
|
"Enabled": result_reuse_enable
|
|
@@ -26,7 +26,7 @@ import pyathena
|
|
|
26
26
|
from pyathena.common import BaseCursor, CursorIterator
|
|
27
27
|
from pyathena.converter import Converter
|
|
28
28
|
from pyathena.cursor import Cursor
|
|
29
|
-
from pyathena.error import NotSupportedError
|
|
29
|
+
from pyathena.error import NotSupportedError, ProgrammingError
|
|
30
30
|
from pyathena.formatter import DefaultParameterFormatter, Formatter
|
|
31
31
|
from pyathena.util import RetryConfig
|
|
32
32
|
|
|
@@ -77,7 +77,9 @@ class Connection(Generic[ConnectionCursor]):
|
|
|
77
77
|
Note:
|
|
78
78
|
Either s3_staging_dir or work_group must be specified. If using a
|
|
79
79
|
workgroup, it must have a result location configured unless
|
|
80
|
-
s3_staging_dir is also provided.
|
|
80
|
+
s3_staging_dir is also provided. For workgroups with managed query
|
|
81
|
+
result storage, pass ``s3_staging_dir=""`` to skip the environment
|
|
82
|
+
variable fallback.
|
|
81
83
|
"""
|
|
82
84
|
|
|
83
85
|
_ENV_S3_STAGING_DIR: str = "AWS_ATHENA_S3_STAGING_DIR"
|
|
@@ -198,6 +200,10 @@ class Connection(Generic[ConnectionCursor]):
|
|
|
198
200
|
Args:
|
|
199
201
|
s3_staging_dir: S3 location to store query results. Required if not
|
|
200
202
|
using workgroups or if workgroup doesn't have result location.
|
|
203
|
+
Pass an empty string to explicitly disable S3 staging and skip
|
|
204
|
+
the ``AWS_ATHENA_S3_STAGING_DIR`` environment variable fallback.
|
|
205
|
+
This is required when connecting to a workgroup with managed
|
|
206
|
+
query result storage enabled.
|
|
201
207
|
region_name: AWS region name. Uses default region if not specified.
|
|
202
208
|
schema_name: Default database/schema name. Defaults to "default".
|
|
203
209
|
catalog_name: Data catalog name. Defaults to "awsdatacatalog".
|
|
@@ -226,12 +232,17 @@ class Connection(Generic[ConnectionCursor]):
|
|
|
226
232
|
**kwargs: Additional arguments passed to boto3 Session and client.
|
|
227
233
|
|
|
228
234
|
Raises:
|
|
229
|
-
|
|
235
|
+
ProgrammingError: If neither s3_staging_dir nor work_group is provided.
|
|
230
236
|
|
|
231
237
|
Note:
|
|
232
238
|
Either s3_staging_dir or work_group must be specified. Environment
|
|
233
239
|
variables AWS_ATHENA_S3_STAGING_DIR and AWS_ATHENA_WORK_GROUP are
|
|
234
240
|
checked if parameters are not provided.
|
|
241
|
+
|
|
242
|
+
When using a workgroup with managed query result storage, pass
|
|
243
|
+
``s3_staging_dir=""`` to prevent the environment variable fallback
|
|
244
|
+
from sending a ``ResultConfiguration`` that conflicts with
|
|
245
|
+
``ManagedQueryResultsConfiguration``.
|
|
235
246
|
"""
|
|
236
247
|
self._kwargs = {
|
|
237
248
|
**kwargs,
|
|
@@ -241,8 +252,8 @@ class Connection(Generic[ConnectionCursor]):
|
|
|
241
252
|
"serial_number": serial_number,
|
|
242
253
|
"duration_seconds": duration_seconds,
|
|
243
254
|
}
|
|
244
|
-
if s3_staging_dir:
|
|
245
|
-
self.s3_staging_dir: Optional[str] = s3_staging_dir
|
|
255
|
+
if s3_staging_dir is not None:
|
|
256
|
+
self.s3_staging_dir: Optional[str] = s3_staging_dir or None
|
|
246
257
|
else:
|
|
247
258
|
self.s3_staging_dir = os.getenv(self._ENV_S3_STAGING_DIR)
|
|
248
259
|
self.region_name = region_name
|
|
@@ -258,9 +269,8 @@ class Connection(Generic[ConnectionCursor]):
|
|
|
258
269
|
self.profile_name = profile_name
|
|
259
270
|
self.config: Optional[Config] = config if config else Config()
|
|
260
271
|
|
|
261
|
-
|
|
262
|
-
"Required argument `s3_staging_dir` or `work_group` not found."
|
|
263
|
-
)
|
|
272
|
+
if not self.s3_staging_dir and not self.work_group:
|
|
273
|
+
raise ProgrammingError("Required argument `s3_staging_dir` or `work_group` not found.")
|
|
264
274
|
|
|
265
275
|
if self.s3_staging_dir and not self.s3_staging_dir.endswith("/"):
|
|
266
276
|
self.s3_staging_dir = f"{self.s3_staging_dir}/"
|
|
@@ -161,7 +161,8 @@ class AsyncPandasCursor(AsyncCursor):
|
|
|
161
161
|
) -> Tuple[str, "Future[Union[AthenaPandasResultSet, Any]]"]:
|
|
162
162
|
if self._unload:
|
|
163
163
|
s3_staging_dir = s3_staging_dir if s3_staging_dir else self._s3_staging_dir
|
|
164
|
-
|
|
164
|
+
if not s3_staging_dir:
|
|
165
|
+
raise ProgrammingError("If the unload option is used, s3_staging_dir is required.")
|
|
165
166
|
operation, unload_location = self._formatter.wrap_unload(
|
|
166
167
|
operation,
|
|
167
168
|
s3_staging_dir=s3_staging_dir,
|
|
@@ -238,7 +238,8 @@ class PandasCursor(BaseCursor, CursorIterator, WithResultSet):
|
|
|
238
238
|
self._reset_state()
|
|
239
239
|
if self._unload:
|
|
240
240
|
s3_staging_dir = s3_staging_dir if s3_staging_dir else self._s3_staging_dir
|
|
241
|
-
|
|
241
|
+
if not s3_staging_dir:
|
|
242
|
+
raise ProgrammingError("If the unload option is used, s3_staging_dir is required.")
|
|
242
243
|
operation, unload_location = self._formatter.wrap_unload(
|
|
243
244
|
operation,
|
|
244
245
|
s3_staging_dir=s3_staging_dir,
|
|
@@ -296,6 +296,9 @@ class AthenaPandasResultSet(AthenaResultSet):
|
|
|
296
296
|
df = self._as_pandas()
|
|
297
297
|
trunc_date = _no_trunc_date if self.is_unload else self._trunc_date
|
|
298
298
|
self._df_iter = PandasDataFrameIterator(df, trunc_date)
|
|
299
|
+
elif self.state == AthenaQueryExecution.STATE_SUCCEEDED:
|
|
300
|
+
df = self._as_pandas_from_api()
|
|
301
|
+
self._df_iter = PandasDataFrameIterator(df, self._trunc_date)
|
|
299
302
|
else:
|
|
300
303
|
import pandas as pd
|
|
301
304
|
|
|
@@ -678,6 +681,25 @@ class AthenaPandasResultSet(AthenaResultSet):
|
|
|
678
681
|
df = self._read_csv()
|
|
679
682
|
return df
|
|
680
683
|
|
|
684
|
+
def _as_pandas_from_api(self, converter: Optional[Converter] = None) -> "DataFrame":
|
|
685
|
+
"""Build a DataFrame from GetQueryResults API.
|
|
686
|
+
|
|
687
|
+
Used as a fallback when ``output_location`` is not available
|
|
688
|
+
(e.g. managed query result storage).
|
|
689
|
+
|
|
690
|
+
Args:
|
|
691
|
+
converter: Type converter for result values. Defaults to
|
|
692
|
+
``DefaultTypeConverter`` if not specified.
|
|
693
|
+
"""
|
|
694
|
+
import pandas as pd
|
|
695
|
+
|
|
696
|
+
rows = self._fetch_all_rows(converter)
|
|
697
|
+
if not rows:
|
|
698
|
+
return pd.DataFrame()
|
|
699
|
+
description = self.description if self.description else []
|
|
700
|
+
columns = [d[0] for d in description]
|
|
701
|
+
return pd.DataFrame(self._rows_to_columnar(rows, columns))
|
|
702
|
+
|
|
681
703
|
def as_pandas(self) -> Union[PandasDataFrameIterator, "DataFrame"]:
|
|
682
704
|
if self._chunksize is None:
|
|
683
705
|
return next(self._df_iter)
|
|
@@ -223,7 +223,8 @@ class AsyncPolarsCursor(AsyncCursor):
|
|
|
223
223
|
"""
|
|
224
224
|
if self._unload:
|
|
225
225
|
s3_staging_dir = s3_staging_dir if s3_staging_dir else self._s3_staging_dir
|
|
226
|
-
|
|
226
|
+
if not s3_staging_dir:
|
|
227
|
+
raise ProgrammingError("If the unload option is used, s3_staging_dir is required.")
|
|
227
228
|
operation, unload_location = self._formatter.wrap_unload(
|
|
228
229
|
operation,
|
|
229
230
|
s3_staging_dir=s3_staging_dir,
|
|
@@ -250,7 +250,8 @@ class PolarsCursor(BaseCursor, CursorIterator, WithResultSet):
|
|
|
250
250
|
self._reset_state()
|
|
251
251
|
if self._unload:
|
|
252
252
|
s3_staging_dir = s3_staging_dir if s3_staging_dir else self._s3_staging_dir
|
|
253
|
-
|
|
253
|
+
if not s3_staging_dir:
|
|
254
|
+
raise ProgrammingError("If the unload option is used, s3_staging_dir is required.")
|
|
254
255
|
operation, unload_location = self._formatter.wrap_unload(
|
|
255
256
|
operation,
|
|
256
257
|
s3_staging_dir=s3_staging_dir,
|
|
@@ -251,6 +251,9 @@ class AthenaPolarsResultSet(AthenaResultSet):
|
|
|
251
251
|
# _metadata for unload queries, so we must cache column names AFTER this.
|
|
252
252
|
if self.state == AthenaQueryExecution.STATE_SUCCEEDED and self.output_location:
|
|
253
253
|
self._df_iter = self._create_dataframe_iterator()
|
|
254
|
+
elif self.state == AthenaQueryExecution.STATE_SUCCEEDED:
|
|
255
|
+
df = self._as_polars_from_api()
|
|
256
|
+
self._df_iter = PolarsDataFrameIterator(df, self.converters, self._get_column_names())
|
|
254
257
|
else:
|
|
255
258
|
import polars as pl
|
|
256
259
|
|
|
@@ -454,8 +457,8 @@ class AthenaPolarsResultSet(AthenaResultSet):
|
|
|
454
457
|
if not self._is_csv_readable():
|
|
455
458
|
return pl.DataFrame()
|
|
456
459
|
|
|
457
|
-
|
|
458
|
-
|
|
460
|
+
if self.output_location is None:
|
|
461
|
+
raise ProgrammingError("output_location is not available.")
|
|
459
462
|
|
|
460
463
|
separator, has_header, new_columns = self._get_csv_params()
|
|
461
464
|
|
|
@@ -489,8 +492,8 @@ class AthenaPolarsResultSet(AthenaResultSet):
|
|
|
489
492
|
if not self._prepare_parquet_location():
|
|
490
493
|
return pl.DataFrame()
|
|
491
494
|
|
|
492
|
-
|
|
493
|
-
|
|
495
|
+
if self._unload_location is None:
|
|
496
|
+
raise ProgrammingError("unload_location is not available.")
|
|
494
497
|
|
|
495
498
|
try:
|
|
496
499
|
return pl.read_parquet(
|
|
@@ -539,6 +542,25 @@ class AthenaPolarsResultSet(AthenaResultSet):
|
|
|
539
542
|
df = self._read_csv()
|
|
540
543
|
return df
|
|
541
544
|
|
|
545
|
+
def _as_polars_from_api(self, converter: Optional[Converter] = None) -> "pl.DataFrame":
|
|
546
|
+
"""Build a Polars DataFrame from GetQueryResults API.
|
|
547
|
+
|
|
548
|
+
Used as a fallback when ``output_location`` is not available
|
|
549
|
+
(e.g. managed query result storage).
|
|
550
|
+
|
|
551
|
+
Args:
|
|
552
|
+
converter: Type converter for result values. Defaults to
|
|
553
|
+
``DefaultTypeConverter`` if not specified.
|
|
554
|
+
"""
|
|
555
|
+
import polars as pl
|
|
556
|
+
|
|
557
|
+
rows = self._fetch_all_rows(converter)
|
|
558
|
+
if not rows:
|
|
559
|
+
return pl.DataFrame()
|
|
560
|
+
description = self.description if self.description else []
|
|
561
|
+
columns = [d[0] for d in description]
|
|
562
|
+
return pl.DataFrame(self._rows_to_columnar(rows, columns))
|
|
563
|
+
|
|
542
564
|
def as_polars(self) -> "pl.DataFrame":
|
|
543
565
|
"""Return query results as a Polars DataFrame.
|
|
544
566
|
|
|
@@ -618,8 +640,8 @@ class AthenaPolarsResultSet(AthenaResultSet):
|
|
|
618
640
|
if not self._is_csv_readable():
|
|
619
641
|
return
|
|
620
642
|
|
|
621
|
-
|
|
622
|
-
|
|
643
|
+
if self.output_location is None:
|
|
644
|
+
raise ProgrammingError("output_location is not available.")
|
|
623
645
|
|
|
624
646
|
separator, has_header, new_columns = self._get_csv_params()
|
|
625
647
|
|
|
@@ -656,8 +678,8 @@ class AthenaPolarsResultSet(AthenaResultSet):
|
|
|
656
678
|
if not self._prepare_parquet_location():
|
|
657
679
|
return
|
|
658
680
|
|
|
659
|
-
|
|
660
|
-
|
|
681
|
+
if self._unload_location is None:
|
|
682
|
+
raise ProgrammingError("unload_location is not available.")
|
|
661
683
|
|
|
662
684
|
try:
|
|
663
685
|
lazy_df = pl.scan_parquet(
|
|
@@ -19,7 +19,7 @@ from typing import (
|
|
|
19
19
|
)
|
|
20
20
|
|
|
21
21
|
from pyathena.common import CursorIterator
|
|
22
|
-
from pyathena.converter import Converter
|
|
22
|
+
from pyathena.converter import Converter, DefaultTypeConverter
|
|
23
23
|
from pyathena.error import DataError, OperationalError, ProgrammingError
|
|
24
24
|
from pyathena.model import AthenaQueryExecution
|
|
25
25
|
from pyathena.util import RetryConfig, parse_output_location, retry_api_call
|
|
@@ -73,7 +73,8 @@ class AthenaResultSet(CursorIterator):
|
|
|
73
73
|
self._connection: Optional["Connection[Any]"] = connection
|
|
74
74
|
self._converter = converter
|
|
75
75
|
self._query_execution: Optional[AthenaQueryExecution] = query_execution
|
|
76
|
-
|
|
76
|
+
if not self._query_execution:
|
|
77
|
+
raise ProgrammingError("Required argument `query_execution` not found.")
|
|
77
78
|
self._retry_config = retry_config
|
|
78
79
|
self._client = connection.session.client(
|
|
79
80
|
"s3",
|
|
@@ -328,19 +329,21 @@ class AthenaResultSet(CursorIterator):
|
|
|
328
329
|
raise ProgrammingError("AthenaResultSet is closed.")
|
|
329
330
|
return cast("Connection[Any]", self._connection)
|
|
330
331
|
|
|
331
|
-
def
|
|
332
|
+
def __get_query_results(
|
|
333
|
+
self, max_results: int, next_token: Optional[str] = None
|
|
334
|
+
) -> Dict[str, Any]:
|
|
332
335
|
if not self.query_id:
|
|
333
336
|
raise ProgrammingError("QueryExecutionId is none or empty.")
|
|
334
337
|
if self.state != AthenaQueryExecution.STATE_SUCCEEDED:
|
|
335
338
|
raise ProgrammingError("QueryExecutionState is not SUCCEEDED.")
|
|
336
339
|
if self.is_closed:
|
|
337
340
|
raise ProgrammingError("AthenaResultSet is closed.")
|
|
338
|
-
request = {
|
|
341
|
+
request: Dict[str, Any] = {
|
|
339
342
|
"QueryExecutionId": self.query_id,
|
|
340
|
-
"MaxResults":
|
|
343
|
+
"MaxResults": max_results,
|
|
341
344
|
}
|
|
342
345
|
if next_token:
|
|
343
|
-
request
|
|
346
|
+
request["NextToken"] = next_token
|
|
344
347
|
try:
|
|
345
348
|
response = retry_api_call(
|
|
346
349
|
self.connection.client.get_query_results,
|
|
@@ -354,17 +357,23 @@ class AthenaResultSet(CursorIterator):
|
|
|
354
357
|
else:
|
|
355
358
|
return cast(Dict[str, Any], response)
|
|
356
359
|
|
|
360
|
+
def __fetch(self, next_token: Optional[str] = None) -> Dict[str, Any]:
|
|
361
|
+
return self.__get_query_results(self._arraysize, next_token)
|
|
362
|
+
|
|
357
363
|
def _fetch(self) -> None:
|
|
358
364
|
if not self._next_token:
|
|
359
365
|
raise ProgrammingError("NextToken is none or empty.")
|
|
360
366
|
response = self.__fetch(self._next_token)
|
|
361
|
-
self.
|
|
367
|
+
rows, self._next_token = self._parse_result_rows(response)
|
|
368
|
+
self._process_rows(rows)
|
|
362
369
|
|
|
363
370
|
def _pre_fetch(self) -> None:
|
|
364
371
|
response = self.__fetch()
|
|
365
372
|
self._process_metadata(response)
|
|
366
373
|
self._process_update_count(response)
|
|
367
|
-
self.
|
|
374
|
+
rows, self._next_token = self._parse_result_rows(response)
|
|
375
|
+
offset = 1 if rows and self._is_first_row_column_labels(rows) else 0
|
|
376
|
+
self._process_rows(rows, offset)
|
|
368
377
|
|
|
369
378
|
def fetchone(
|
|
370
379
|
self,
|
|
@@ -433,41 +442,131 @@ class AthenaResultSet(CursorIterator):
|
|
|
433
442
|
self._rowcount = update_count
|
|
434
443
|
|
|
435
444
|
def _get_rows(
|
|
436
|
-
self,
|
|
445
|
+
self,
|
|
446
|
+
offset: int,
|
|
447
|
+
metadata: Tuple[Any, ...],
|
|
448
|
+
rows: List[Dict[str, Any]],
|
|
449
|
+
converter: Optional[Converter] = None,
|
|
437
450
|
) -> List[Union[Tuple[Optional[Any], ...], Dict[Any, Optional[Any]]]]:
|
|
451
|
+
conv = converter or self._converter
|
|
438
452
|
return [
|
|
439
453
|
tuple(
|
|
440
454
|
[
|
|
441
|
-
|
|
455
|
+
conv.convert(meta.get("Type"), row.get("VarCharValue"))
|
|
442
456
|
for meta, row in zip(metadata, rows[i].get("Data", []), strict=False)
|
|
443
457
|
]
|
|
444
458
|
)
|
|
445
459
|
for i in range(offset, len(rows))
|
|
446
460
|
]
|
|
447
461
|
|
|
448
|
-
def
|
|
462
|
+
def _parse_result_rows(
|
|
463
|
+
self, response: Dict[str, Any]
|
|
464
|
+
) -> Tuple[List[Dict[str, Any]], Optional[str]]:
|
|
465
|
+
"""Parse a GetQueryResults response into raw rows and next token.
|
|
466
|
+
|
|
467
|
+
Handles response validation and pagination token extraction.
|
|
468
|
+
This is the shared parsing logic used by both ``_pre_fetch``
|
|
469
|
+
(normal path) and ``_fetch_all_rows`` (API fallback).
|
|
470
|
+
|
|
471
|
+
Args:
|
|
472
|
+
response: Raw response dict from ``GetQueryResults`` API.
|
|
473
|
+
|
|
474
|
+
Returns:
|
|
475
|
+
Tuple of (rows, next_token).
|
|
476
|
+
"""
|
|
449
477
|
result_set = response.get("ResultSet")
|
|
450
478
|
if not result_set:
|
|
451
479
|
raise DataError("KeyError `ResultSet`")
|
|
452
480
|
rows = result_set.get("Rows")
|
|
453
481
|
if rows is None:
|
|
454
482
|
raise DataError("KeyError `Rows`")
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
483
|
+
next_token = response.get("NextToken")
|
|
484
|
+
return rows, next_token
|
|
485
|
+
|
|
486
|
+
def _process_rows(self, rows: List[Dict[str, Any]], offset: int = 0) -> None:
|
|
487
|
+
if rows and self._metadata:
|
|
488
|
+
processed_rows = self._get_rows(offset, self._metadata, rows)
|
|
489
|
+
self._rows.extend(processed_rows)
|
|
462
490
|
|
|
463
491
|
def _is_first_row_column_labels(self, rows: List[Dict[str, Any]]) -> bool:
|
|
464
492
|
first_row_data = rows[0].get("Data", [])
|
|
465
|
-
|
|
466
|
-
for meta, data in zip(metadata, first_row_data, strict=False):
|
|
493
|
+
for meta, data in zip(self._metadata or (), first_row_data, strict=False):
|
|
467
494
|
if meta.get("Name") != data.get("VarCharValue"):
|
|
468
495
|
return False
|
|
469
496
|
return True
|
|
470
497
|
|
|
498
|
+
def _fetch_all_rows(
|
|
499
|
+
self,
|
|
500
|
+
converter: Optional[Converter] = None,
|
|
501
|
+
) -> List[Tuple[Optional[Any], ...]]:
|
|
502
|
+
"""Fetch all rows via GetQueryResults API with type conversion.
|
|
503
|
+
|
|
504
|
+
Paginates through all results from the beginning using MaxResults=1000.
|
|
505
|
+
Defaults to ``DefaultTypeConverter`` for string-to-Python type conversion,
|
|
506
|
+
because subclass converters (e.g. Pandas/Arrow) are designed for S3 file
|
|
507
|
+
reading and may not handle API result strings.
|
|
508
|
+
|
|
509
|
+
This method is intended for use by subclass result sets that need to
|
|
510
|
+
fall back to the API when S3 output is not available (e.g., managed
|
|
511
|
+
query result storage).
|
|
512
|
+
|
|
513
|
+
Args:
|
|
514
|
+
converter: Type converter for result values. Defaults to
|
|
515
|
+
``DefaultTypeConverter`` if not specified.
|
|
516
|
+
|
|
517
|
+
Returns:
|
|
518
|
+
List of converted row tuples.
|
|
519
|
+
"""
|
|
520
|
+
if self._metadata is None:
|
|
521
|
+
raise ProgrammingError("Metadata is not available.")
|
|
522
|
+
|
|
523
|
+
_logger.warning(
|
|
524
|
+
"output_location is not available (e.g. managed query result storage). "
|
|
525
|
+
"Falling back to GetQueryResults API. "
|
|
526
|
+
"This may be slow for large result sets."
|
|
527
|
+
)
|
|
528
|
+
|
|
529
|
+
converter = converter or DefaultTypeConverter()
|
|
530
|
+
all_rows: List[Tuple[Optional[Any], ...]] = []
|
|
531
|
+
next_token: Optional[str] = None
|
|
532
|
+
|
|
533
|
+
while True:
|
|
534
|
+
response = self.__get_query_results(self.DEFAULT_FETCH_SIZE, next_token)
|
|
535
|
+
rows, next_token = self._parse_result_rows(response)
|
|
536
|
+
|
|
537
|
+
offset = 1 if rows and self._is_first_row_column_labels(rows) else 0
|
|
538
|
+
all_rows.extend(
|
|
539
|
+
cast(
|
|
540
|
+
List[Tuple[Optional[Any], ...]],
|
|
541
|
+
self._get_rows(offset, self._metadata, rows, converter),
|
|
542
|
+
)
|
|
543
|
+
)
|
|
544
|
+
|
|
545
|
+
if not next_token:
|
|
546
|
+
break
|
|
547
|
+
|
|
548
|
+
return all_rows
|
|
549
|
+
|
|
550
|
+
@staticmethod
|
|
551
|
+
def _rows_to_columnar(
|
|
552
|
+
rows: List[Tuple[Optional[Any], ...]],
|
|
553
|
+
columns: List[str],
|
|
554
|
+
) -> Dict[str, List[Any]]:
|
|
555
|
+
"""Convert row-oriented data to columnar format.
|
|
556
|
+
|
|
557
|
+
Args:
|
|
558
|
+
rows: List of row tuples from ``_fetch_all_rows()``.
|
|
559
|
+
columns: Column names in order.
|
|
560
|
+
|
|
561
|
+
Returns:
|
|
562
|
+
Dictionary mapping column names to lists of values.
|
|
563
|
+
"""
|
|
564
|
+
columnar: Dict[str, List[Any]] = {col: [] for col in columns}
|
|
565
|
+
for row in rows:
|
|
566
|
+
for col, val in zip(columns, row, strict=False):
|
|
567
|
+
columnar[col].append(val)
|
|
568
|
+
return columnar
|
|
569
|
+
|
|
471
570
|
def _get_content_length(self) -> int:
|
|
472
571
|
if not self.output_location:
|
|
473
572
|
raise ProgrammingError("OutputLocation is none or empty.")
|
|
@@ -530,14 +629,19 @@ class AthenaDictResultSet(AthenaResultSet):
|
|
|
530
629
|
dict_type: Type[Any] = dict
|
|
531
630
|
|
|
532
631
|
def _get_rows(
|
|
533
|
-
self,
|
|
632
|
+
self,
|
|
633
|
+
offset: int,
|
|
634
|
+
metadata: Tuple[Any, ...],
|
|
635
|
+
rows: List[Dict[str, Any]],
|
|
636
|
+
converter: Optional[Converter] = None,
|
|
534
637
|
) -> List[Union[Tuple[Optional[Any], ...], Dict[Any, Optional[Any]]]]:
|
|
638
|
+
conv = converter or self._converter
|
|
535
639
|
return [
|
|
536
640
|
self.dict_type(
|
|
537
641
|
[
|
|
538
642
|
(
|
|
539
643
|
meta.get("Name"),
|
|
540
|
-
|
|
644
|
+
conv.convert(meta.get("Type"), row.get("VarCharValue")),
|
|
541
645
|
)
|
|
542
646
|
for meta, row in zip(metadata, rows[i].get("Data", []), strict=False)
|
|
543
647
|
]
|
|
@@ -82,10 +82,14 @@ class AthenaS3FSResultSet(AthenaResultSet):
|
|
|
82
82
|
|
|
83
83
|
if self.state == AthenaQueryExecution.STATE_SUCCEEDED and self.output_location:
|
|
84
84
|
self._init_csv_reader()
|
|
85
|
+
elif self.state == AthenaQueryExecution.STATE_SUCCEEDED:
|
|
86
|
+
# Managed query result storage: no output_location, use API
|
|
87
|
+
rows = self._fetch_all_rows()
|
|
88
|
+
self._rows.extend(rows)
|
|
85
89
|
|
|
86
90
|
# If CSV reader was not initialized (e.g., CTAS, DDL),
|
|
87
91
|
# fall back to pre-fetched data from Athena API
|
|
88
|
-
if not self._csv_reader and pre_fetched_rows:
|
|
92
|
+
if not self._csv_reader and not self._rows and pre_fetched_rows:
|
|
89
93
|
self._rows.extend(pre_fetched_rows)
|
|
90
94
|
|
|
91
95
|
def _create_s3_file_system(self) -> S3FileSystem:
|
|
@@ -30,8 +30,10 @@ classifiers = [
|
|
|
30
30
|
dynamic = ["version"]
|
|
31
31
|
|
|
32
32
|
[project.urls]
|
|
33
|
-
homepage = "https://github.com/
|
|
34
|
-
repository = "https://github.com/
|
|
33
|
+
homepage = "https://github.com/pyathena-dev/PyAthena/"
|
|
34
|
+
repository = "https://github.com/pyathena-dev/PyAthena/"
|
|
35
|
+
documentation = "https://pyathena.dev/"
|
|
36
|
+
issues = "https://github.com/pyathena-dev/PyAthena/issues"
|
|
35
37
|
|
|
36
38
|
[project.entry-points."sqlalchemy.dialects"]
|
|
37
39
|
awsathena = "pyathena.sqlalchemy.base:AthenaDialect"
|
|
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
|