Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions docs/arrow.md
Original file line number Diff line number Diff line change
Expand Up @@ -256,18 +256,16 @@ pyathena.error.OperationalError: SYNTAX_ERROR: line 1:1: Column name not specifi
ArrowCursor supports configuring S3 connection and request timeouts through `connect_timeout` and `request_timeout` parameters.
These parameters are particularly useful when experiencing timeout errors due to:

- Role assumption with AWS STS (cross-account access)
- High network latency between your environment and S3
- Connecting from regions far from the S3 bucket

By default, PyArrow uses AWS SDK default timeouts (typically 1 second for connection, 3 seconds for requests).
You can increase these values to accommodate slower authentication or network conditions.
You can increase these values to accommodate slower network conditions.

```python
from pyathena import connect
from pyathena.arrow.cursor import ArrowCursor

# Configure higher timeouts for role assumption scenarios
cursor = connect(
s3_staging_dir="s3://YOUR_S3_BUCKET/path/to/",
region_name="us-west-2",
Expand All @@ -291,6 +289,7 @@ cursor = connect(

The timeout parameters accept float values in seconds and apply to all S3 operations performed by the cursor,
including HeadObject and GetObject operations when retrieving query results.
The pyarrow S3 filesystem uses the credentials of the connection's boto3 session, including the credentials of an assumed role.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Self-review round two (claims, compatibility, operations): CLEAN

Base 7fc7f07310f7f32af08afacb0d5eb8ea80a61746, head e3caedc9bcf214d51588c21b7dcc8f7458bd9b21.

Claims checked:

  • Conflict causes in the commit message and PR body: the rename and docstring come from Replace name-mangled helpers with single-underscore methods #881 (f4cc848b, merged in a86a180e); the paragraph rewrite from Correct user guide claims that contradict the implementation #940 (8bd91260, merged in 23ff61f6); the PyArrow 10 note removal from Require pandas 3.0 and pyarrow 22.0 #885 (842f0f05). The cherry-pick source SHA is 209549db8ff551dfd66290bdbad19130e129aff3 (verified with git rev-parse; an earlier local draft of the message had a wrong SHA and was corrected before the first push).
  • PyArrow version claims: session_token first appears in _s3fs.pyx at apache-arrow-2.0.0 (absent at 1.0.0); connect_timeout/request_timeout at 10.0.0 (absent at 9.0.0); access_key/secret_key at 0.17.0.
  • Release-note items hold for 3.x's Connection (same branches as master).
  • Docs: other role-assumption text (docs/usage.md:743-791) describes connection arguments and does not mention Arrow.

Deferred (pre-existing, not introduced): 3.x's sentence that the timeouts "apply to all S3 operations performed by the cursor, including HeadObject and GetObject" is inaccurate; master corrected it in #940, which is not part of this backport.

Operations: as on master, one fewer sts:AssumeRole per Arrow result set with role_arn.


```{note}
These timeout parameters require PyArrow >= 10.0.0, which added support for configuring S3FileSystem timeouts.
Expand Down
71 changes: 10 additions & 61 deletions pyathena/arrow/result_set.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,69 +127,18 @@ def __init__(
def __s3_file_system(self):

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Independent review (relayed): CLEAN

Reviewer: Codex CLI 0.162.1 (codex exec -s read-only --ephemeral, model gpt-6.1-sol), session 01a1248a-68f6-7242-b828-12246ec46a7e. Static review only: no tests, builds, edits, or GitHub access. Snapshot: detached worktree at head e3caedc9bcf214d51588c21b7dcc8f7458bd9b21, base 7fc7f07310f7f32af08afacb0d5eb8ea80a61746, compared with upstream 209549db8ff551dfd66290bdbad19130e129aff3; HEAD and clean status remained unchanged. The prompt gave the intended behavior, backport policy, and SHAs only (no PR number, description, or self-review findings).

Covered (as reported): both diffs and the factory/test correspondence; the commit message's resolutions against #881, #940, and #885 history; connection credentials for default/static/process/SSO profiles, profile-based roles, role/MFA arguments, supplied sessions, and absent/error paths; ArrowCursor, AsyncArrowCursor, AioArrowCursor, the CSV/Parquet readers, timeout propagation, Python 3.10 syntax, and PyArrow 10 constructor arguments; changed docs and the regression test.

The backport faithfully applies the upstream delta with the documented 3.x adaptations. The conflict-resolution description is accurate. No actionable defects found. The test can detect the original defect: the process-profile case requires PROCESSKEY, while the role case requires ROLEKEY and an empty PyArrow role_arn.

Non-actionable (as reported): docs/arrow.md:290 retains the pre-existing overstatement that the timeouts cover all cursor S3 operations (the boto3 HeadObject and manifest GetObject requests use connection.config); preserving it follows the backport policy. This matches the deferral in self-review round two (corrected on master by #940).

from pyarrow import fs

connection = self.connection

# Build timeout parameters dict
timeout_kwargs = {}
kwargs: dict[str, Any] = {"region": self.connection.region_name}
if self._connect_timeout is not None:
timeout_kwargs["connect_timeout"] = self._connect_timeout
kwargs["connect_timeout"] = self._connect_timeout
if self._request_timeout is not None:
timeout_kwargs["request_timeout"] = self._request_timeout

if connection._kwargs.get("role_arn"):
external_id = connection._kwargs.get("external_id")
fs = fs.S3FileSystem(
role_arn=connection._kwargs["role_arn"],
session_name=connection._kwargs["role_session_name"],
external_id="" if external_id is None else external_id,
load_frequency=connection._kwargs["duration_seconds"],
region=connection.region_name,
**timeout_kwargs,
)
elif connection.profile_name:
profile = connection.session._session.full_config["profiles"][connection.profile_name]
fs = fs.S3FileSystem(
access_key=profile.get("aws_access_key_id", None),
secret_key=profile.get("aws_secret_access_key", None),
session_token=profile.get("aws_session_token", None),
region=connection.region_name,
**timeout_kwargs,
)
else:
# Try explicit credentials first
explicit_access_key = connection._kwargs.get("aws_access_key_id")
explicit_secret_key = connection._kwargs.get("aws_secret_access_key")

if explicit_access_key and explicit_secret_key:
# Use explicitly provided credentials
fs = fs.S3FileSystem(
access_key=explicit_access_key,
secret_key=explicit_secret_key,
session_token=connection._kwargs.get("aws_session_token"),
region=connection.region_name,
**timeout_kwargs,
)
else:
# Fall back to dynamic credentials from boto3 session
# This handles EC2 instance profiles, temporary credentials, etc.
try:
credentials = connection.session._session.get_credentials()
if credentials:
fs = fs.S3FileSystem(
access_key=credentials.access_key,
secret_key=credentials.secret_key,
session_token=credentials.token,
region=connection.region_name,
**timeout_kwargs,
)
else:
# Fall back to default (no explicit credentials)
fs = fs.S3FileSystem(region=connection.region_name, **timeout_kwargs)
except Exception:
# Fall back to default if credential retrieval fails
fs = fs.S3FileSystem(region=connection.region_name, **timeout_kwargs)

return fs
kwargs["request_timeout"] = self._request_timeout
credentials = self.connection.session.get_credentials()

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Self-review round one (implementation behavior): FINDINGS (1, repaired)

Base 7fc7f07310f7f32af08afacb0d5eb8ea80a61746 (3.x), head 0133b152cec94ad188cffb823acf8c5bee7a1412 (initial pass), repaired head e3caedc9bcf214d51588c21b7dcc8f7458bd9b21.

Covered: the three changed files against #1116 (209549db) and 3.x's surrounding code.

  • The method body is byte-identical to master's; only the 3.x name __s3_file_system differs, and its only caller is __init__ (pyathena/arrow/result_set.py:113). Any is already imported on 3.x.
  • 3.x reads self._fs only in _read_csv() and _read_parquet() (:270, :304), reached through _as_arrow() from __init__ (:115), so the frozen snapshot covers every read, as on master. AioArrowCursor builds the result set in asyncio.to_thread (pyathena/aio/arrow/cursor.py:148).
  • 3.x's Connection.__init__ has the same session / role_arn / serial_number branches as master (pyathena/connection.py:280-320), so the session holds the assumed-role or MFA credentials there too.
  • PyArrow floor: every argument passed exists in pyarrow 10.0.0; the new test passed with Python 3.10 + pyarrow 10.0.0, and the fake S3/STS endpoint showed pyarrow 10.0.0 signing with the session's credentials in all four scenarios.
  • Regression test: the credential_process and role_arn cases fail on 3.x with the source change reverted.
  • Finding (repaired in e3caedc): the new test file lacked the license header that master's file has and that other files added to 3.x by backports carry (for example tests/pyathena/pandas/test_result_set.py from Return the whole result from as_pandas() when auto chunking applies #950). The amend adds master's header; commit message and PR body updated.

Validation after repair: just lint passed; the new test 4 passed. Live Arrow suite (110 passed) and the pyarrow 10.0.0 runs were at 0133b15; the amend changes only the header comment.

if credentials:
frozen_credentials = credentials.get_frozen_credentials()
kwargs["access_key"] = frozen_credentials.access_key
kwargs["secret_key"] = frozen_credentials.secret_key
kwargs["session_token"] = frozen_credentials.token
return fs.S3FileSystem(**kwargs)

@property
def timestamp_parsers(self) -> list[str]:
Expand Down
73 changes: 73 additions & 0 deletions tests/pyathena/arrow/test_result_set.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# Copyright 2026 The PyAthena authors
#
# Licensed under the MIT License.
# See LICENSE or https://opensource.org/licenses/MIT.
#
# SPDX-License-Identifier: MIT
import os
import sys
from unittest.mock import patch

import pytest

from pyathena.arrow.result_set import AthenaArrowResultSet
from pyathena.connection import Connection


class TestAthenaArrowResultSet:
@pytest.mark.parametrize(
("connection_kwargs", "expected_access_key"),
[
({}, "DEFAULTKEY"),
({"profile_name": "static"}, "STATICKEY"),
({"profile_name": "process"}, "PROCESSKEY"),
(
{"profile_name": "process", "role_arn": "arn:aws:iam::123456789012:role/r"},
"ROLEKEY",
),
],
ids=["default", "static", "credential_process", "role_arn"],
)
def test_create_s3_file_system_uses_session_credentials(
self, monkeypatch, tmp_path, connection_kwargs, expected_access_key
):
"""The pyarrow filesystem reads with the credentials of the connection's session.

No AWS calls; the profiles come from temporary config files, and the role
assumption is mocked.
"""
for key in list(os.environ):
if key.startswith("AWS_"):
monkeypatch.delenv(key)
process = tmp_path / "credential_process.py"
process.write_text(
'print(\'{"Version": 1, "AccessKeyId": "PROCESSKEY", "SecretAccessKey": "secret"}\')\n'
)
config = tmp_path / "config"
config.write_text(
"[default]\naws_access_key_id = DEFAULTKEY\naws_secret_access_key = secret\n"
"[profile static]\naws_access_key_id = STATICKEY\naws_secret_access_key = secret\n"
f'[profile process]\ncredential_process = "{sys.executable}" "{process}"\n'
)
monkeypatch.setenv("AWS_CONFIG_FILE", str(config))
monkeypatch.setenv("AWS_SHARED_CREDENTIALS_FILE", str(tmp_path / "credentials"))
role_credentials = {
"AccessKeyId": "ROLEKEY",
"SecretAccessKey": "secret",
"SessionToken": "token",
}
with patch.object(Connection, "_assume_role", return_value=role_credentials):
connection = Connection(
region_name="us-east-1", s3_staging_dir="s3://bucket/path/", **connection_kwargs
)
with connection:
result_set = AthenaArrowResultSet.__new__(AthenaArrowResultSet) # bypass __init__
result_set._connection = connection
result_set._connect_timeout = None
result_set._request_timeout = None
fs = result_set._AthenaArrowResultSet__s3_file_system()

# pyarrow exposes the filesystem options only through its pickle support.
options = fs.__reduce__()[1][0]
assert options["access_key"] == expected_access_key
assert options["role_arn"] == ""
Loading