Skip to content

Commit eb4d7d2

Browse files
authored
Add botocore Config to wr.config (#535)
1 parent 1b6a1ae commit eb4d7d2

File tree

5 files changed

+116
-19
lines changed

5 files changed

+116
-19
lines changed

awswrangler/_config.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,19 @@
55
import os
66
from typing import Any, Callable, Dict, List, NamedTuple, Optional, Tuple, Type, Union, cast
77

8+
import botocore.config
89
import pandas as pd
910

1011
from awswrangler import exceptions
1112

1213
_logger: logging.Logger = logging.getLogger(__name__)
1314

1415

15-
_ConfigValueType = Union[str, bool, int, None]
16+
_ConfigValueType = Union[str, bool, int, botocore.config.Config, None]
1617

1718

1819
class _ConfigArg(NamedTuple):
19-
dtype: Type[Union[str, bool, int]]
20+
dtype: Type[Union[str, bool, int, botocore.config.Config]]
2021
nullable: bool
2122
enforced: bool = False
2223

@@ -41,6 +42,8 @@ class _ConfigArg(NamedTuple):
4142
"redshift_endpoint_url": _ConfigArg(dtype=str, nullable=True, enforced=True),
4243
"kms_endpoint_url": _ConfigArg(dtype=str, nullable=True, enforced=True),
4344
"emr_endpoint_url": _ConfigArg(dtype=str, nullable=True, enforced=True),
45+
# Botocore config
46+
"botocore_config": _ConfigArg(dtype=botocore.config.Config, nullable=True),
4447
}
4548

4649

@@ -57,6 +60,7 @@ def __init__(self) -> None:
5760
self.redshift_endpoint_url = None
5861
self.kms_endpoint_url = None
5962
self.emr_endpoint_url = None
63+
self.botocore_config = None
6064
for name in _CONFIG_ARGS:
6165
self._load_config(name=name)
6266

@@ -338,6 +342,15 @@ def emr_endpoint_url(self) -> Optional[str]:
338342
def emr_endpoint_url(self, value: Optional[str]) -> None:
339343
self._set_config_value(key="emr_endpoint_url", value=value)
340344

345+
@property
346+
def botocore_config(self) -> botocore.config.Config:
347+
"""Property botocore_config."""
348+
return cast(Optional[botocore.config.Config], self["botocore_config"])
349+
350+
@botocore_config.setter
351+
def botocore_config(self, value: Optional[botocore.config.Config]) -> None:
352+
self._set_config_value(key="botocore_config", value=value)
353+
341354

342355
def _insert_str(text: str, token: str, insert: str) -> str:
343356
"""Insert string into other."""

awswrangler/_utils.py

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import pandas as pd
1717

1818
from awswrangler import _config, exceptions
19+
from awswrangler._config import apply_configs
1920

2021
_logger: logging.Logger = logging.getLogger(__name__)
2122

@@ -59,9 +60,13 @@ def boto3_from_primitives(primitives: Optional[Boto3PrimitivesType] = None) -> b
5960
return boto3.Session(**args)
6061

6162

62-
def botocore_config() -> botocore.config.Config:
63+
def default_botocore_config() -> botocore.config.Config:
6364
"""Botocore configuration."""
64-
return botocore.config.Config(retries={"max_attempts": 5}, connect_timeout=10, max_pool_connections=10)
65+
retries_config: Dict[str, Union[str, int]] = {
66+
"max_attempts": int(os.getenv("AWS_MAX_ATTEMPTS", "3")),
67+
"mode": os.getenv("AWS_RETRY_MODE", "standard"),
68+
}
69+
return botocore.config.Config(retries=retries_config, connect_timeout=10, max_pool_connections=10)
6570

6671

6772
def _get_endpoint_url(service_name: str) -> Optional[str]:
@@ -83,24 +88,31 @@ def _get_endpoint_url(service_name: str) -> Optional[str]:
8388
return endpoint_url
8489

8590

91+
@apply_configs
8692
def client(
87-
service_name: str, session: Optional[boto3.Session] = None, config: Optional[botocore.config.Config] = None
93+
service_name: str, session: Optional[boto3.Session] = None, botocore_config: Optional[botocore.config.Config] = None
8894
) -> boto3.client:
8995
"""Create a valid boto3.client."""
9096
endpoint_url: Optional[str] = _get_endpoint_url(service_name=service_name)
9197
return ensure_session(session=session).client(
9298
service_name=service_name,
9399
endpoint_url=endpoint_url,
94100
use_ssl=True,
95-
config=botocore_config() if config is None else config,
101+
config=default_botocore_config() if botocore_config is None else botocore_config,
96102
)
97103

98104

99-
def resource(service_name: str, session: Optional[boto3.Session] = None) -> boto3.resource:
105+
@apply_configs
106+
def resource(
107+
service_name: str, session: Optional[boto3.Session] = None, botocore_config: Optional[botocore.config.Config] = None
108+
) -> boto3.resource:
100109
"""Create a valid boto3.resource."""
101110
endpoint_url: Optional[str] = _get_endpoint_url(service_name=service_name)
102111
return ensure_session(session=session).resource(
103-
service_name=service_name, endpoint_url=endpoint_url, use_ssl=True, config=botocore_config()
112+
service_name=service_name,
113+
endpoint_url=endpoint_url,
114+
use_ssl=True,
115+
config=default_botocore_config() if botocore_config is None else botocore_config,
104116
)
105117

106118

awswrangler/timestream.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ def _write_batch(
3939
client: boto3.client = _utils.client(
4040
service_name="timestream-write",
4141
session=boto3_session,
42-
config=Config(read_timeout=20, max_pool_connections=5000, retries={"max_attempts": 10}),
42+
botocore_config=Config(read_timeout=20, max_pool_connections=5000, retries={"max_attempts": 10}),
4343
)
4444
try:
4545
_utils.try_it(
@@ -218,7 +218,7 @@ def query(sql: str, boto3_session: Optional[boto3.Session] = None) -> pd.DataFra
218218
client: boto3.client = _utils.client(
219219
service_name="timestream-query",
220220
session=boto3_session,
221-
config=Config(read_timeout=60, retries={"max_attempts": 10}),
221+
botocore_config=Config(read_timeout=60, retries={"max_attempts": 10}),
222222
)
223223
paginator = client.get_paginator("query")
224224
rows: List[List[Any]] = []

tests/test_config.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
import boto3
66
import botocore
7+
import botocore.client
8+
import botocore.config
79
import pytest
810

911
import awswrangler as wr
@@ -121,3 +123,60 @@ def test_basics(path, glue_database, glue_table, workgroup0, workgroup1):
121123
def test_athena_cache_configuration():
122124
wr.config.max_local_cache_entries = 20
123125
assert wr.config.max_remote_cache_entries == 20
126+
127+
128+
def test_botocore_config(path):
129+
original = botocore.client.ClientCreator.create_client
130+
131+
# Default values for botocore.config.Config
132+
expected_max_retries_attempt = 3
133+
expected_connect_timeout = 10
134+
expected_max_pool_connections = 10
135+
expected_retry_mode = "standard"
136+
137+
def wrapper(self, **kwarg):
138+
assert kwarg["client_config"].retries["max_attempts"] == expected_max_retries_attempt
139+
assert kwarg["client_config"].connect_timeout == expected_connect_timeout
140+
assert kwarg["client_config"].max_pool_connections == expected_max_pool_connections
141+
assert kwarg["client_config"].retries["mode"] == expected_retry_mode
142+
return original(self, **kwarg)
143+
144+
# Check for default values
145+
with patch("botocore.client.ClientCreator.create_client", new=wrapper):
146+
with open_s3_object(path, mode="wb") as s3obj:
147+
s3obj.write(b"foo")
148+
149+
# Update default config with environment variables
150+
expected_max_retries_attempt = 20
151+
expected_connect_timeout = 10
152+
expected_max_pool_connections = 10
153+
expected_retry_mode = "adaptive"
154+
155+
os.environ["AWS_MAX_ATTEMPTS"] = str(expected_max_retries_attempt)
156+
os.environ["AWS_RETRY_MODE"] = expected_retry_mode
157+
158+
with patch("botocore.client.ClientCreator.create_client", new=wrapper):
159+
with open_s3_object(path, mode="wb") as s3obj:
160+
s3obj.write(b"foo")
161+
162+
del os.environ["AWS_MAX_ATTEMPTS"]
163+
del os.environ["AWS_RETRY_MODE"]
164+
165+
# Update botocore.config.Config
166+
expected_max_retries_attempt = 30
167+
expected_connect_timeout = 40
168+
expected_max_pool_connections = 50
169+
expected_retry_mode = "legacy"
170+
171+
botocore_config = botocore.config.Config(
172+
retries={"max_attempts": expected_max_retries_attempt, "mode": expected_retry_mode},
173+
connect_timeout=expected_connect_timeout,
174+
max_pool_connections=expected_max_pool_connections,
175+
)
176+
wr.config.botocore_config = botocore_config
177+
178+
with patch("botocore.client.ClientCreator.create_client", new=wrapper):
179+
with open_s3_object(path, mode="wb") as s3obj:
180+
s3obj.write(b"foo")
181+
182+
wr.config.reset()

0 commit comments

Comments
 (0)