Skip to content

Commit efe50f9

Browse files
authored
Workaround for botocore 1.28 (round 2) (#1087)
In #1083 we've started passing an `endpoint_url` parameter to _convert_to_request_dict due to changes made in botocore 1.28. When a model does not specify a `host`, the `endpoint_url` would be `None`. To determine the actual `endpoint_url` in botocore ≥1.28, we must call another private method, `_resolve_endpoint_ruleset`.
1 parent b36c4fc commit efe50f9

File tree

5 files changed

+73
-13
lines changed

5 files changed

+73
-13
lines changed

docs/release_notes.rst

+7
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
Release Notes
22
=============
33

4+
v5.2.3
5+
----------
6+
* Update for botocore 1.28 private API change (#1087) which caused the following exception::
7+
8+
TypeError: Cannot mix str and non-str arguments
9+
10+
411
v5.2.2
512
----------
613
* Update for botocore 1.28 private API change (#1083) which caused the following exception::

pynamodb/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,4 @@
77
"""
88
__author__ = 'Jharrod LaFon'
99
__license__ = 'MIT'
10-
__version__ = '5.2.2'
10+
__version__ = '5.2.3'

pynamodb/connection/_botocore_private.py

+20-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"""
22
Type-annotates the private botocore APIs that we're currently relying on.
33
"""
4-
from typing import Any, Dict
4+
from typing import Any, Dict, Optional
55

66
import botocore.client
77
import botocore.credentials
@@ -24,5 +24,23 @@ class BotocoreBaseClientPrivate(botocore.client.BaseClient):
2424
_request_signer: BotocoreRequestSignerPrivate
2525
_service_model: botocore.model.ServiceModel
2626

27-
def _convert_to_request_dict(self, api_params: Dict[str, Any], operation_model: botocore.model.OperationModel, *args: Any, **kwargs: Any) -> Dict[str, Any]:
27+
def _resolve_endpoint_ruleset(
28+
self,
29+
operation_model: botocore.model.OperationModel,
30+
params: Dict[str, Any],
31+
request_context: Dict[str, Any],
32+
ignore_signing_region: bool = ...,
33+
):
34+
...
35+
36+
def _convert_to_request_dict(
37+
self,
38+
api_params: Dict[str, Any],
39+
operation_model: botocore.model.OperationModel,
40+
*,
41+
endpoint_url: str = ..., # added in botocore 1.28
42+
context: Optional[Dict[str, Any]] = ...,
43+
headers: Optional[Dict[str, Any]] = ...,
44+
set_user_agent_header: bool = ...,
45+
) -> Dict[str, Any]:
2846
...

pynamodb/connection/base.py

+26-10
Original file line numberDiff line numberDiff line change
@@ -254,7 +254,7 @@ def __init__(self,
254254
self.host = host
255255
self._local = local()
256256
self._client: Optional[BotocoreBaseClientPrivate] = None
257-
self._convert_to_request_dict_kwargs: Dict[str, Any] = {}
257+
self._convert_to_request_dict__endpoint_url = False
258258
if region:
259259
self.region = region
260260
else:
@@ -358,11 +358,28 @@ def _make_api_call(self, operation_name: str, operation_kwargs: Dict, settings:
358358
2. It provides a place to monkey patch HTTP requests for unit testing
359359
"""
360360
operation_model = self.client._service_model.operation_model(operation_name)
361-
request_dict = self.client._convert_to_request_dict(
362-
operation_kwargs,
363-
operation_model,
364-
**self._convert_to_request_dict_kwargs,
365-
)
361+
if self._convert_to_request_dict__endpoint_url:
362+
request_context = {
363+
'client_region': self.region,
364+
'client_config': self.client.meta.config,
365+
'has_streaming_input': operation_model.has_streaming_input,
366+
'auth_type': operation_model.auth_type,
367+
}
368+
endpoint_url, additional_headers = self.client._resolve_endpoint_ruleset(
369+
operation_model, operation_kwargs, request_context
370+
)
371+
request_dict = self.client._convert_to_request_dict(
372+
api_params=operation_kwargs,
373+
operation_model=operation_model,
374+
endpoint_url=endpoint_url,
375+
context=request_context,
376+
headers=additional_headers,
377+
)
378+
else:
379+
request_dict = self.client._convert_to_request_dict(
380+
operation_kwargs,
381+
operation_model,
382+
)
366383

367384
for i in range(0, self._max_retry_attempts_exception + 1):
368385
attempt_number = i + 1
@@ -536,11 +553,10 @@ def client(self) -> BotocoreBaseClientPrivate:
536553
parameter_validation=False, # Disable unnecessary validation for performance
537554
connect_timeout=self._connect_timeout_seconds,
538555
read_timeout=self._read_timeout_seconds,
539-
max_pool_connections=self._max_pool_connections)
556+
max_pool_connections=self._max_pool_connections,
557+
)
540558
self._client = cast(BotocoreBaseClientPrivate, self.session.create_client(SERVICE_NAME, self.region, endpoint_url=self.host, config=config))
541-
self._convert_to_request_dict_kwargs = {}
542-
if 'endpoint_url' in inspect.signature(self._client._convert_to_request_dict).parameters:
543-
self._convert_to_request_dict_kwargs['endpoint_url'] = self.host
559+
self._convert_to_request_dict__endpoint_url = 'endpoint_url' in inspect.signature(self._client._convert_to_request_dict).parameters
544560
return self._client
545561

546562
def get_meta_table(self, table_name: str, refresh: bool = False):

tests/test_base_connection.py

+19
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,14 @@
22
Tests for the base connection class
33
"""
44
import base64
5+
import io
56
import json
67
from unittest import mock, TestCase
78
from unittest.mock import patch
89

910
import botocore.exceptions
11+
import botocore.httpsession
12+
import urllib3
1013
from botocore.awsrequest import AWSPreparedRequest, AWSRequest, AWSResponse
1114
from botocore.client import ClientError
1215
from botocore.exceptions import BotoCoreError
@@ -1398,6 +1401,22 @@ def test_scan(self):
13981401
conn.scan,
13991402
table_name)
14001403

1404+
def test_make_api_call__happy_path(self):
1405+
response = AWSResponse(
1406+
url='https://www.example.com',
1407+
status_code=200,
1408+
raw=urllib3.HTTPResponse(
1409+
body=io.BytesIO(json.dumps({}).encode('utf-8')),
1410+
preload_content=False,
1411+
),
1412+
headers={'x-amzn-RequestId': 'abcdef'},
1413+
)
1414+
1415+
c = Connection()
1416+
1417+
with patch.object(botocore.httpsession.URLLib3Session, 'send', return_value=response):
1418+
c._make_api_call('CreateTable', {'TableName': 'MyTable'})
1419+
14011420
@mock.patch('pynamodb.connection.Connection.client')
14021421
def test_make_api_call_throws_verbose_error_after_backoff(self, client_mock):
14031422
response = AWSResponse(

0 commit comments

Comments
 (0)