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.
Files changed (62) hide show
  1. {pyathena-3.27.1 → pyathena-3.28.0}/PKG-INFO +12 -10
  2. {pyathena-3.27.1 → pyathena-3.28.0}/README.md +7 -7
  3. {pyathena-3.27.1 → pyathena-3.28.0}/pyathena/__init__.py +4 -1
  4. {pyathena-3.27.1 → pyathena-3.28.0}/pyathena/_version.py +2 -2
  5. {pyathena-3.27.1 → pyathena-3.28.0}/pyathena/arrow/async_cursor.py +2 -1
  6. {pyathena-3.27.1 → pyathena-3.28.0}/pyathena/arrow/cursor.py +2 -1
  7. {pyathena-3.27.1 → pyathena-3.28.0}/pyathena/arrow/result_set.py +21 -0
  8. {pyathena-3.27.1 → pyathena-3.28.0}/pyathena/common.py +6 -4
  9. {pyathena-3.27.1 → pyathena-3.28.0}/pyathena/connection.py +18 -8
  10. {pyathena-3.27.1 → pyathena-3.28.0}/pyathena/pandas/async_cursor.py +2 -1
  11. {pyathena-3.27.1 → pyathena-3.28.0}/pyathena/pandas/cursor.py +2 -1
  12. {pyathena-3.27.1 → pyathena-3.28.0}/pyathena/pandas/result_set.py +22 -0
  13. {pyathena-3.27.1 → pyathena-3.28.0}/pyathena/polars/async_cursor.py +2 -1
  14. {pyathena-3.27.1 → pyathena-3.28.0}/pyathena/polars/cursor.py +2 -1
  15. {pyathena-3.27.1 → pyathena-3.28.0}/pyathena/polars/result_set.py +30 -8
  16. {pyathena-3.27.1 → pyathena-3.28.0}/pyathena/result_set.py +126 -22
  17. {pyathena-3.27.1 → pyathena-3.28.0}/pyathena/s3fs/result_set.py +5 -1
  18. {pyathena-3.27.1 → pyathena-3.28.0}/pyproject.toml +4 -2
  19. {pyathena-3.27.1 → pyathena-3.28.0}/.gitignore +0 -0
  20. {pyathena-3.27.1 → pyathena-3.28.0}/LICENSE +0 -0
  21. {pyathena-3.27.1 → pyathena-3.28.0}/pyathena/arrow/__init__.py +0 -0
  22. {pyathena-3.27.1 → pyathena-3.28.0}/pyathena/arrow/converter.py +0 -0
  23. {pyathena-3.27.1 → pyathena-3.28.0}/pyathena/arrow/util.py +0 -0
  24. {pyathena-3.27.1 → pyathena-3.28.0}/pyathena/async_cursor.py +0 -0
  25. {pyathena-3.27.1 → pyathena-3.28.0}/pyathena/converter.py +0 -0
  26. {pyathena-3.27.1 → pyathena-3.28.0}/pyathena/cursor.py +0 -0
  27. {pyathena-3.27.1 → pyathena-3.28.0}/pyathena/error.py +0 -0
  28. {pyathena-3.27.1 → pyathena-3.28.0}/pyathena/filesystem/__init__.py +0 -0
  29. {pyathena-3.27.1 → pyathena-3.28.0}/pyathena/filesystem/s3.py +0 -0
  30. {pyathena-3.27.1 → pyathena-3.28.0}/pyathena/filesystem/s3_object.py +0 -0
  31. {pyathena-3.27.1 → pyathena-3.28.0}/pyathena/formatter.py +0 -0
  32. {pyathena-3.27.1 → pyathena-3.28.0}/pyathena/model.py +0 -0
  33. {pyathena-3.27.1 → pyathena-3.28.0}/pyathena/pandas/__init__.py +0 -0
  34. {pyathena-3.27.1 → pyathena-3.28.0}/pyathena/pandas/converter.py +0 -0
  35. {pyathena-3.27.1 → pyathena-3.28.0}/pyathena/pandas/util.py +0 -0
  36. {pyathena-3.27.1 → pyathena-3.28.0}/pyathena/polars/__init__.py +0 -0
  37. {pyathena-3.27.1 → pyathena-3.28.0}/pyathena/polars/converter.py +0 -0
  38. {pyathena-3.27.1 → pyathena-3.28.0}/pyathena/polars/util.py +0 -0
  39. {pyathena-3.27.1 → pyathena-3.28.0}/pyathena/py.typed +0 -0
  40. {pyathena-3.27.1 → pyathena-3.28.0}/pyathena/s3fs/__init__.py +0 -0
  41. {pyathena-3.27.1 → pyathena-3.28.0}/pyathena/s3fs/async_cursor.py +0 -0
  42. {pyathena-3.27.1 → pyathena-3.28.0}/pyathena/s3fs/converter.py +0 -0
  43. {pyathena-3.27.1 → pyathena-3.28.0}/pyathena/s3fs/cursor.py +0 -0
  44. {pyathena-3.27.1 → pyathena-3.28.0}/pyathena/s3fs/reader.py +0 -0
  45. {pyathena-3.27.1 → pyathena-3.28.0}/pyathena/spark/__init__.py +0 -0
  46. {pyathena-3.27.1 → pyathena-3.28.0}/pyathena/spark/async_cursor.py +0 -0
  47. {pyathena-3.27.1 → pyathena-3.28.0}/pyathena/spark/common.py +0 -0
  48. {pyathena-3.27.1 → pyathena-3.28.0}/pyathena/spark/cursor.py +0 -0
  49. {pyathena-3.27.1 → pyathena-3.28.0}/pyathena/sqlalchemy/__init__.py +0 -0
  50. {pyathena-3.27.1 → pyathena-3.28.0}/pyathena/sqlalchemy/arrow.py +0 -0
  51. {pyathena-3.27.1 → pyathena-3.28.0}/pyathena/sqlalchemy/base.py +0 -0
  52. {pyathena-3.27.1 → pyathena-3.28.0}/pyathena/sqlalchemy/compiler.py +0 -0
  53. {pyathena-3.27.1 → pyathena-3.28.0}/pyathena/sqlalchemy/constants.py +0 -0
  54. {pyathena-3.27.1 → pyathena-3.28.0}/pyathena/sqlalchemy/pandas.py +0 -0
  55. {pyathena-3.27.1 → pyathena-3.28.0}/pyathena/sqlalchemy/polars.py +0 -0
  56. {pyathena-3.27.1 → pyathena-3.28.0}/pyathena/sqlalchemy/preparer.py +0 -0
  57. {pyathena-3.27.1 → pyathena-3.28.0}/pyathena/sqlalchemy/requirements.py +0 -0
  58. {pyathena-3.27.1 → pyathena-3.28.0}/pyathena/sqlalchemy/rest.py +0 -0
  59. {pyathena-3.27.1 → pyathena-3.28.0}/pyathena/sqlalchemy/s3fs.py +0 -0
  60. {pyathena-3.27.1 → pyathena-3.28.0}/pyathena/sqlalchemy/types.py +0 -0
  61. {pyathena-3.27.1 → pyathena-3.28.0}/pyathena/sqlalchemy/util.py +0 -0
  62. {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.27.1
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/laughingman7743/PyAthena/
6
- Project-URL: repository, https://github.com/laughingman7743/PyAthena/
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/laughingman7743/PyAthena/master/docs/_static/icon.png" alt="PyAthena logo" width="250">
62
+ <img src="https://raw.githubusercontent.com/pyathena-dev/PyAthena/master/docs/_static/icon.png" alt="PyAthena logo" width="250">
61
63
 
62
64
  [![PyPI - Version](https://badge.fury.io/py/pyathena.svg)](https://badge.fury.io/py/pyathena)
63
65
  [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/PyAthena.svg)](https://pypi.org/project/PyAthena/)
64
66
  [![PyPI - Downloads](https://static.pepy.tech/badge/pyathena/month)](https://pepy.tech/project/pyathena)
65
- [![CI - Test](https://github.com/laughingman7743/PyAthena/actions/workflows/test.yaml/badge.svg)](https://github.com/laughingman7743/PyAthena/actions/workflows/test.yaml)
66
- [![CD - Docs](https://github.com/laughingman7743/PyAthena/actions/workflows/docs.yaml/badge.svg)](https://github.com/laughingman7743/PyAthena/actions/workflows/docs.yaml)
67
- [![License - MIT](https://img.shields.io/pypi/l/PyAthena.svg)](https://github.com/laughingman7743/PyAthena/blob/master/LICENSE)
67
+ [![CI - Test](https://github.com/pyathena-dev/PyAthena/actions/workflows/test.yaml/badge.svg)](https://github.com/pyathena-dev/PyAthena/actions/workflows/test.yaml)
68
+ [![CD - Docs](https://github.com/pyathena-dev/PyAthena/actions/workflows/docs.yaml/badge.svg)](https://github.com/pyathena-dev/PyAthena/actions/workflows/docs.yaml)
69
+ [![License - MIT](https://img.shields.io/pypi/l/PyAthena.svg)](https://github.com/pyathena-dev/PyAthena/blob/master/LICENSE)
68
70
  [![linting - Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
69
71
  [![types - Mypy](https://www.mypy-lang.org/static/mypy_badge.svg)](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://laughingman7743.github.io/PyAthena/
120
+ - Documentation: https://pyathena.dev/
119
121
  - PyPI Releases: https://pypi.org/project/PyAthena/
120
- - Source Code: https://github.com/laughingman7743/PyAthena/
121
- - Issue Tracker: https://github.com/laughingman7743/PyAthena/issues
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/laughingman7743/PyAthena/master/docs/_static/icon.png" alt="PyAthena logo" width="250">
5
+ <img src="https://raw.githubusercontent.com/pyathena-dev/PyAthena/master/docs/_static/icon.png" alt="PyAthena logo" width="250">
6
6
 
7
7
  [![PyPI - Version](https://badge.fury.io/py/pyathena.svg)](https://badge.fury.io/py/pyathena)
8
8
  [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/PyAthena.svg)](https://pypi.org/project/PyAthena/)
9
9
  [![PyPI - Downloads](https://static.pepy.tech/badge/pyathena/month)](https://pepy.tech/project/pyathena)
10
- [![CI - Test](https://github.com/laughingman7743/PyAthena/actions/workflows/test.yaml/badge.svg)](https://github.com/laughingman7743/PyAthena/actions/workflows/test.yaml)
11
- [![CD - Docs](https://github.com/laughingman7743/PyAthena/actions/workflows/docs.yaml/badge.svg)](https://github.com/laughingman7743/PyAthena/actions/workflows/docs.yaml)
12
- [![License - MIT](https://img.shields.io/pypi/l/PyAthena.svg)](https://github.com/laughingman7743/PyAthena/blob/master/LICENSE)
10
+ [![CI - Test](https://github.com/pyathena-dev/PyAthena/actions/workflows/test.yaml/badge.svg)](https://github.com/pyathena-dev/PyAthena/actions/workflows/test.yaml)
11
+ [![CD - Docs](https://github.com/pyathena-dev/PyAthena/actions/workflows/docs.yaml/badge.svg)](https://github.com/pyathena-dev/PyAthena/actions/workflows/docs.yaml)
12
+ [![License - MIT](https://img.shields.io/pypi/l/PyAthena.svg)](https://github.com/pyathena-dev/PyAthena/blob/master/LICENSE)
13
13
  [![linting - Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
14
14
  [![types - Mypy](https://www.mypy-lang.org/static/mypy_badge.svg)](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://laughingman7743.github.io/PyAthena/
63
+ - Documentation: https://pyathena.dev/
64
64
  - PyPI Releases: https://pypi.org/project/PyAthena/
65
- - Source Code: https://github.com/laughingman7743/PyAthena/
66
- - Issue Tracker: https://github.com/laughingman7743/PyAthena/issues
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
- AssertionError: If neither s3_staging_dir nor work_group is provided.
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.27.1'
32
- __version_tuple__ = version_tuple = (3, 27, 1)
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
- assert s3_staging_dir, "If the unload option is used, s3_staging_dir is required."
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
- assert s3_staging_dir, "If the unload option is used, s3_staging_dir is required."
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
- request["ResultConfiguration"].update(
221
- {"OutputLocation": s3_staging_dir if s3_staging_dir else self._s3_staging_dir}
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
- request["ResultConfiguration"].update({"EncryptionConfiguration": enc_conf})
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
- AssertionError: If neither s3_staging_dir nor work_group is provided.
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
- assert self.s3_staging_dir or self.work_group, (
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
- assert s3_staging_dir, "If the unload option is used, s3_staging_dir is required."
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
- assert s3_staging_dir, "If the unload option is used, s3_staging_dir is required."
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
- assert s3_staging_dir, "If the unload option is used, s3_staging_dir is required."
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
- assert s3_staging_dir, "If the unload option is used, s3_staging_dir is required."
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
- # After validation, output_location is guaranteed to be set
458
- assert self.output_location is not None
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
- # After preparation, unload_location is guaranteed to be set
493
- assert self._unload_location is not None
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
- # After validation, output_location is guaranteed to be set
622
- assert self.output_location is not None
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
- # After preparation, unload_location is guaranteed to be set
660
- assert self._unload_location is not None
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
- assert self._query_execution, "Required argument `query_execution` not found."
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 __fetch(self, next_token: Optional[str] = None) -> Dict[str, Any]:
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": self._arraysize,
343
+ "MaxResults": max_results,
341
344
  }
342
345
  if next_token:
343
- request.update({"NextToken": next_token})
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._process_rows(response)
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._process_rows(response)
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, offset: int, metadata: Tuple[Any, ...], rows: List[Dict[str, Any]]
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
- self._converter.convert(meta.get("Type"), row.get("VarCharValue"))
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 _process_rows(self, response: Dict[str, Any]) -> None:
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
- processed_rows = []
456
- if len(rows) > 0:
457
- offset = 1 if not self._next_token and self._is_first_row_column_labels(rows) else 0
458
- metadata = cast(Tuple[Any, ...], self._metadata)
459
- processed_rows = self._get_rows(offset, metadata, rows)
460
- self._rows.extend(processed_rows)
461
- self._next_token = response.get("NextToken")
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
- metadata = cast(Tuple[Any, Any], self._metadata)
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, offset: int, metadata: Tuple[Any, ...], rows: List[Dict[str, Any]]
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
- self._converter.convert(meta.get("Type"), row.get("VarCharValue")),
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/laughingman7743/PyAthena/"
34
- repository = "https://github.com/laughingman7743/PyAthena/"
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