Skip to content

Commit 798ac21

Browse files
authored
4.x: workaround for _convert_to_request_dict change (#1083) (#1130)
botocore 1.28 changed the signature of private method botocore.client.BaseClient._convert_to_request_dict adding an endpoint_url parameter. We are updating pynamodb to inspect the signature and add this parameter as needed.
1 parent 43542dc commit 798ac21

File tree

5 files changed

+104
-9
lines changed

5 files changed

+104
-9
lines changed

docs/release_notes.rst

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

4+
v4.4.0
5+
----------
6+
* Update for botocore 1.28 private API change (#1130) which caused the following exception::
7+
8+
TypeError: _convert_to_request_dict() missing 1 required positional argument: 'endpoint_url'
9+
410
v4.3.3
511
----------
612

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__ = '4.3.3'
10+
__version__ = '4.4.0'
+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
"""
2+
Type-annotates the private botocore APIs that we're currently relying on.
3+
"""
4+
from typing import Any, Dict, Optional
5+
6+
import botocore.client
7+
import botocore.credentials
8+
import botocore.endpoint
9+
import botocore.hooks
10+
import botocore.model
11+
import botocore.signers
12+
13+
14+
class BotocoreEndpointPrivate(botocore.endpoint.Endpoint):
15+
_event_emitter: botocore.hooks.HierarchicalEmitter
16+
17+
18+
class BotocoreRequestSignerPrivate(botocore.signers.RequestSigner):
19+
_credentials: botocore.credentials.Credentials
20+
21+
22+
class BotocoreBaseClientPrivate(botocore.client.BaseClient):
23+
_endpoint: BotocoreEndpointPrivate
24+
_request_signer: BotocoreRequestSignerPrivate
25+
_service_model: botocore.model.ServiceModel
26+
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]:
46+
...

pynamodb/connection/base.py

+32-8
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
"""
44
from __future__ import division
55

6+
import inspect
67
import json
78
import logging
89
import random
@@ -11,6 +12,7 @@
1112
import uuid
1213
from base64 import b64decode
1314
from threading import local
15+
from typing import Any, Dict, List, Mapping, Optional, Sequence, cast
1416

1517
import six
1618
import botocore.client
@@ -22,6 +24,7 @@
2224
from botocore.session import get_session
2325
from six.moves import range
2426

27+
from pynamodb.connection._botocore_private import BotocoreBaseClientPrivate
2528
from pynamodb.connection.util import pythonic
2629
from pynamodb.constants import (
2730
RETURN_CONSUMED_CAPACITY_VALUES, RETURN_ITEM_COLL_METRICS_VALUES,
@@ -247,7 +250,8 @@ def __init__(self, region=None, host=None,
247250
self._tables = {}
248251
self.host = host
249252
self._local = local()
250-
self._client = None
253+
self._client: Optional[BotocoreBaseClientPrivate] = None
254+
self._convert_to_request_dict__endpoint_url = False
251255
if region:
252256
self.region = region
253257
else:
@@ -364,10 +368,28 @@ def _make_api_call(self, operation_name, operation_kwargs):
364368
2. It provides a place to monkey patch HTTP requests for unit testing
365369
"""
366370
operation_model = self.client._service_model.operation_model(operation_name)
367-
request_dict = self.client._convert_to_request_dict(
368-
operation_kwargs,
369-
operation_model,
370-
)
371+
if self._convert_to_request_dict__endpoint_url:
372+
request_context = {
373+
'client_region': self.region,
374+
'client_config': self.client.meta.config,
375+
'has_streaming_input': operation_model.has_streaming_input,
376+
'auth_type': operation_model.auth_type,
377+
}
378+
endpoint_url, additional_headers = self.client._resolve_endpoint_ruleset(
379+
operation_model, operation_kwargs, request_context
380+
)
381+
request_dict = self.client._convert_to_request_dict(
382+
api_params=operation_kwargs,
383+
operation_model=operation_model,
384+
endpoint_url=endpoint_url,
385+
context=request_context,
386+
headers=additional_headers,
387+
)
388+
else:
389+
request_dict = self.client._convert_to_request_dict(
390+
operation_kwargs,
391+
operation_model,
392+
)
371393

372394
for i in range(0, self._max_retry_attempts_exception + 1):
373395
attempt_number = i + 1
@@ -518,7 +540,7 @@ def session(self):
518540
return self._local.session
519541

520542
@property
521-
def client(self):
543+
def client(self) -> BotocoreBaseClientPrivate:
522544
"""
523545
Returns a botocore dynamodb client
524546
"""
@@ -531,8 +553,10 @@ def client(self):
531553
parameter_validation=False, # Disable unnecessary validation for performance
532554
connect_timeout=self._connect_timeout_seconds,
533555
read_timeout=self._read_timeout_seconds,
534-
max_pool_connections=self._max_pool_connections)
535-
self._client = self.session.create_client(SERVICE_NAME, self.region, endpoint_url=self.host, config=config)
556+
max_pool_connections=self._max_pool_connections,
557+
)
558+
self._client = cast(BotocoreBaseClientPrivate, self.session.create_client(SERVICE_NAME, self.region, endpoint_url=self.host, config=config))
559+
self._convert_to_request_dict__endpoint_url = 'endpoint_url' in inspect.signature(self._client._convert_to_request_dict).parameters
536560
return self._client
537561

538562
def get_meta_table(self, table_name, refresh=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
import six
78
from unittest import TestCase
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
@@ -1449,6 +1452,22 @@ def test_scan(self):
14491452
conn.scan,
14501453
table_name)
14511454

1455+
def test_make_api_call__happy_path(self):
1456+
response = AWSResponse(
1457+
url='https://www.example.com',
1458+
status_code=200,
1459+
raw=urllib3.HTTPResponse(
1460+
body=io.BytesIO(json.dumps({}).encode('utf-8')),
1461+
preload_content=False,
1462+
),
1463+
headers={'x-amzn-RequestId': 'abcdef'},
1464+
)
1465+
1466+
c = Connection()
1467+
1468+
with patch.object(botocore.httpsession.URLLib3Session, 'send', return_value=response):
1469+
c._make_api_call('CreateTable', {'TableName': 'MyTable'})
1470+
14521471
@mock.patch('pynamodb.connection.Connection.client')
14531472
def test_make_api_call_throws_verbose_error_after_backoff(self, client_mock):
14541473
response = AWSResponse(

0 commit comments

Comments
 (0)