Skip to content

Commit bbc0134

Browse files
committed
Support warm-up of loader caches
1 parent 3068542 commit bbc0134

8 files changed

Lines changed: 230 additions & 1 deletion

File tree

CHANGES.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
Changes
22
-------
33

4+
3.2.0 (2026-02-06)
5+
^^^^^^^^^^^^^^^^^^^
6+
* support `warm_up_loader_caches` in `AioConfig`
7+
48
3.1.2 (2026-02-05)
59
^^^^^^^^^^^^^^^^^^^
610
* relax botocore dependency specification to support ``"botocore >= 1.41.0, < 1.42.43"``

aiobotocore/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = '3.1.2'
1+
__version__ = '3.2.0'

aiobotocore/config.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ def __init__(
4444
self,
4545
connector_args: Optional[_ConnectorArgs] = None,
4646
http_session_cls: type[_HttpSessionType] = DEFAULT_HTTP_SESSION_CLS,
47+
warm_up_loader_caches: bool = False,
4748
**kwargs,
4849
):
4950
super().__init__(**kwargs)
@@ -52,6 +53,7 @@ def __init__(
5253
copy.copy(connector_args) if connector_args else {}
5354
)
5455
self.http_session_cls: type[_HttpSessionType] = http_session_cls
56+
self.warm_up_loader_caches: bool = warm_up_loader_caches
5557
self._validate_connector_args(
5658
self.connector_args, self.http_session_cls
5759
)

aiobotocore/session.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
import asyncio
2+
from typing import Optional
3+
14
from botocore import UNSIGNED, translate
25
from botocore import __version__ as botocore_version
36
from botocore.context import get_context
@@ -126,6 +129,36 @@ async def get_service_data(self, service_name, api_version=None):
126129
)
127130
return service_data
128131

132+
def warm_up_loader_caches(
133+
self,
134+
service_name: str,
135+
api_version: Optional[str] = None,
136+
):
137+
loader = self.get_component('data_loader')
138+
139+
# from session.py
140+
loader.load_data_with_path('endpoints')
141+
loader.load_data('sdk-default-configuration')
142+
loader.load_service_model(service_name, 'waiters-2', api_version)
143+
loader.load_service_model(service_name, 'paginators-1', api_version)
144+
loader.load_service_model(
145+
service_name, type_name='service-2', api_version=api_version
146+
)
147+
loader.list_available_services(type_name='service-2')
148+
149+
# from client.py
150+
loader.load_data('partitions')
151+
loader.load_service_model(
152+
service_name, 'service-2', api_version=api_version
153+
)
154+
loader.load_service_model(
155+
service_name, 'endpoint-rule-set-1', api_version=api_version
156+
)
157+
loader.load_data('_retry')
158+
159+
# from docs/service.py
160+
loader.load_service_model(service_name, 'examples-1', api_version)
161+
129162
def create_client(self, *args, **kwargs):
130163
return ClientCreatorContext(self._create_client(*args, **kwargs))
131164

@@ -167,6 +200,12 @@ async def _create_client(
167200
)
168201

169202
loader = self.get_component('data_loader')
203+
204+
if getattr(config, 'warm_up_loader_caches', False):
205+
await asyncio.to_thread(
206+
self.warm_up_loader_caches, service_name, api_version
207+
)
208+
170209
event_emitter = self.get_component('event_emitter')
171210
response_parser_factory = self.get_component('response_parser_factory')
172211
if config is not None and config.signature_version is UNSIGNED:

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ dev = [
7474
"packaging >= 24.1, < 27", # Used in test_version.py
7575
"pip >= 24.3.1, < 27", # Used in test_version.py
7676
"pre-commit >= 3.5.0, < 5",
77+
"pytest-mock >= 3.14.1, < 4", # Used in test_session.py
7778
"pytest-rerunfailures >= 16.0.1, < 17", # Used in test_lambda.py
7879
"time-machine >= 2.15.0, < 4", # Used in test_signers.py
7980
"tomli; python_version<'3.11'", # Used in test_version.py

tests/test_config.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,3 +185,20 @@ async def send(self, request):
185185
):
186186
with pytest.raises(SuccessExc):
187187
await s3_client.get_object(Bucket='foo', Key='bar')
188+
189+
190+
@pytest.mark.parametrize(
191+
"warm_up_loader_caches, expected",
192+
[
193+
(None, False),
194+
(False, False),
195+
(True, True),
196+
],
197+
)
198+
def test_config_warm_up_loader_caches(warm_up_loader_caches, expected):
199+
if warm_up_loader_caches is None:
200+
config = AioConfig()
201+
else:
202+
config = AioConfig(warm_up_loader_caches=warm_up_loader_caches)
203+
204+
assert config.warm_up_loader_caches is expected

tests/test_session.py

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,3 +54,155 @@ async def test_set_user_agent_for_session(session: AioSession):
5454
assert session.user_agent_name == "aiobotocore"
5555
assert session.user_agent_version == __version__
5656
assert session.user_agent_extra.startswith("botocore/")
57+
58+
59+
@pytest.mark.parametrize(
60+
"service_name, api_version",
61+
[
62+
("s3", None),
63+
("s3", "2006-03-01"),
64+
("ec2", "2016-11-16"),
65+
],
66+
)
67+
def test_warm_up_loader_caches(
68+
session: AioSession, service_name, api_version, mocker
69+
):
70+
loader = mocker.Mock()
71+
get_component = mocker.patch.object(
72+
session, "get_component", return_value=loader
73+
)
74+
75+
session.warm_up_loader_caches(service_name, api_version)
76+
77+
get_component.assert_called_once_with("data_loader")
78+
assert loader.mock_calls == [
79+
mocker.call.load_data_with_path("endpoints"),
80+
mocker.call.load_data("sdk-default-configuration"),
81+
mocker.call.load_service_model(service_name, "waiters-2", api_version),
82+
mocker.call.load_service_model(
83+
service_name, "paginators-1", api_version
84+
),
85+
mocker.call.load_service_model(
86+
service_name, type_name="service-2", api_version=api_version
87+
),
88+
mocker.call.list_available_services(type_name="service-2"),
89+
mocker.call.load_data("partitions"),
90+
mocker.call.load_service_model(
91+
service_name, "service-2", api_version=api_version
92+
),
93+
mocker.call.load_service_model(
94+
service_name, "endpoint-rule-set-1", api_version=api_version
95+
),
96+
mocker.call.load_data("_retry"),
97+
mocker.call.load_service_model(
98+
service_name, "examples-1", api_version
99+
),
100+
]
101+
102+
103+
@pytest.mark.parametrize(
104+
"warm_up_loader_caches",
105+
[False, True],
106+
)
107+
async def test_warm_up_loader_caches_config(
108+
session: AioSession,
109+
warm_up_loader_caches: bool,
110+
mocker,
111+
):
112+
config = AioConfig(warm_up_loader_caches=warm_up_loader_caches)
113+
mocker.patch.object(
114+
session, "warm_up_loader_caches", wraps=session.warm_up_loader_caches
115+
)
116+
117+
async with session.create_client(
118+
"s3",
119+
config=config,
120+
aws_secret_access_key="xxx",
121+
aws_access_key_id="xxx",
122+
):
123+
pass
124+
125+
if warm_up_loader_caches:
126+
session.warm_up_loader_caches.assert_called_once_with("s3", None)
127+
else:
128+
session.warm_up_loader_caches.assert_not_called()
129+
130+
131+
@pytest.mark.parametrize(
132+
"warm_up_loader_caches",
133+
[False, True],
134+
)
135+
async def test_non_blocking_create_client(
136+
session: AioSession,
137+
warm_up_loader_caches: bool,
138+
mocker,
139+
):
140+
config = AioConfig(warm_up_loader_caches=warm_up_loader_caches)
141+
loader = session.get_component("data_loader")
142+
file_loader = mocker.patch.object(
143+
loader, "file_loader", wraps=loader.file_loader
144+
)
145+
# perform implicit warm-up, while avoiding any other file I/O by stubbing relevant codepathes
146+
session._internal_components.lazy_register_component(
147+
'endpoint_resolver', lambda: None
148+
)
149+
mocker.patch.object(
150+
session, "_resolve_defaults_mode", return_value="legacy"
151+
)
152+
client_creator_cls_mock = mocker.patch(
153+
"aiobotocore.session.AioClientCreator", autospec=True
154+
)
155+
156+
async with session.create_client(
157+
"s3",
158+
config=config,
159+
aws_secret_access_key="xxx",
160+
aws_access_key_id="xxx",
161+
):
162+
pass
163+
164+
if warm_up_loader_caches:
165+
# warm-up triggered file I/O (non-blocking)
166+
file_loader.exists.assert_called()
167+
file_loader.load_file.assert_called()
168+
else:
169+
# no file I/O
170+
file_loader.exists.assert_not_called()
171+
file_loader.load_file.assert_not_called()
172+
173+
mocker.stop(client_creator_cls_mock)
174+
session._register_endpoint_resolver()
175+
file_loader.reset_mock()
176+
177+
# regular client creation #1
178+
async with session.create_client(
179+
"s3",
180+
config=config,
181+
aws_secret_access_key="xxx",
182+
aws_access_key_id="xxx",
183+
):
184+
pass
185+
186+
if warm_up_loader_caches:
187+
# no file I/O
188+
file_loader.exists.assert_not_called()
189+
file_loader.load_file.assert_not_called()
190+
else:
191+
# file I/O (blocking)
192+
file_loader.exists.assert_called()
193+
file_loader.load_file.assert_called()
194+
195+
file_loader.reset_mock()
196+
197+
# regular client creation #2
198+
async with session.create_client(
199+
"s3",
200+
config=config,
201+
aws_secret_access_key="xxx",
202+
aws_access_key_id="xxx",
203+
):
204+
pass
205+
206+
# no file I/O
207+
file_loader.exists.assert_not_called()
208+
file_loader.load_file.assert_not_called()

uv.lock

Lines changed: 14 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)