Skip to content

Commit

Permalink
Workaround for _convert_to_request_dict change (#1083)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
ikonst committed Dec 7, 2022
1 parent 9f058d3 commit 54a0be4
Show file tree
Hide file tree
Showing 5 changed files with 108 additions and 9 deletions.
10 changes: 10 additions & 0 deletions docs/release_notes.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
Release Notes
=============

v4.4.0
----------
* Update for botocore 1.28 private API change (#1130) which caused the following exception::

TypeError: _convert_to_request_dict() missing 1 required positional argument: 'endpoint_url'


v4.3.3
----------

Expand All @@ -14,6 +21,9 @@ v4.3.3

MyModel.query(..., condition=MyModel.my_list[0] == 42)

* Fix a warning about ``collections.abc`` deprecation (#782)


v4.3.2
----------

Expand Down
2 changes: 1 addition & 1 deletion pynamodb/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,4 @@
"""
__author__ = 'Jharrod LaFon'
__license__ = 'MIT'
__version__ = '4.3.3'
__version__ = '4.4.0'
46 changes: 46 additions & 0 deletions pynamodb/connection/_botocore_private.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
"""
Type-annotates the private botocore APIs that we're currently relying on.
"""
from typing import Any, Dict, Optional

import botocore.client
import botocore.credentials
import botocore.endpoint
import botocore.hooks
import botocore.model
import botocore.signers


class BotocoreEndpointPrivate(botocore.endpoint.Endpoint):
_event_emitter: botocore.hooks.HierarchicalEmitter


class BotocoreRequestSignerPrivate(botocore.signers.RequestSigner):
_credentials: botocore.credentials.Credentials


class BotocoreBaseClientPrivate(botocore.client.BaseClient):
_endpoint: BotocoreEndpointPrivate
_request_signer: BotocoreRequestSignerPrivate
_service_model: botocore.model.ServiceModel

def _resolve_endpoint_ruleset(
self,
operation_model: botocore.model.OperationModel,
params: Dict[str, Any],
request_context: Dict[str, Any],
ignore_signing_region: bool = ...,
):
...

def _convert_to_request_dict(
self,
api_params: Dict[str, Any],
operation_model: botocore.model.OperationModel,
*,
endpoint_url: str = ..., # added in botocore 1.28
context: Optional[Dict[str, Any]] = ...,
headers: Optional[Dict[str, Any]] = ...,
set_user_agent_header: bool = ...,
) -> Dict[str, Any]:
...
40 changes: 32 additions & 8 deletions pynamodb/connection/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"""
from __future__ import division

import inspect
import json
import logging
import random
Expand All @@ -11,6 +12,7 @@
import uuid
from base64 import b64decode
from threading import local
from typing import Any, Dict, List, Mapping, Optional, Sequence, cast

import six
import botocore.client
Expand All @@ -22,6 +24,7 @@
from botocore.session import get_session
from six.moves import range

from pynamodb.connection._botocore_private import BotocoreBaseClientPrivate
from pynamodb.connection.util import pythonic
from pynamodb.constants import (
RETURN_CONSUMED_CAPACITY_VALUES, RETURN_ITEM_COLL_METRICS_VALUES,
Expand Down Expand Up @@ -247,7 +250,8 @@ def __init__(self, region=None, host=None,
self._tables = {}
self.host = host
self._local = local()
self._client = None
self._client: Optional[BotocoreBaseClientPrivate] = None
self._convert_to_request_dict__endpoint_url = False
if region:
self.region = region
else:
Expand Down Expand Up @@ -364,10 +368,28 @@ def _make_api_call(self, operation_name, operation_kwargs):
2. It provides a place to monkey patch HTTP requests for unit testing
"""
operation_model = self.client._service_model.operation_model(operation_name)
request_dict = self.client._convert_to_request_dict(
operation_kwargs,
operation_model,
)
if self._convert_to_request_dict__endpoint_url:
request_context = {
'client_region': self.region,
'client_config': self.client.meta.config,
'has_streaming_input': operation_model.has_streaming_input,
'auth_type': operation_model.auth_type,
}
endpoint_url, additional_headers = self.client._resolve_endpoint_ruleset(
operation_model, operation_kwargs, request_context
)
request_dict = self.client._convert_to_request_dict(
api_params=operation_kwargs,
operation_model=operation_model,
endpoint_url=endpoint_url,
context=request_context,
headers=additional_headers,
)
else:
request_dict = self.client._convert_to_request_dict(
operation_kwargs,
operation_model,
)

for i in range(0, self._max_retry_attempts_exception + 1):
attempt_number = i + 1
Expand Down Expand Up @@ -518,7 +540,7 @@ def session(self):
return self._local.session

@property
def client(self):
def client(self) -> BotocoreBaseClientPrivate:
"""
Returns a botocore dynamodb client
"""
Expand All @@ -531,8 +553,10 @@ def client(self):
parameter_validation=False, # Disable unnecessary validation for performance
connect_timeout=self._connect_timeout_seconds,
read_timeout=self._read_timeout_seconds,
max_pool_connections=self._max_pool_connections)
self._client = self.session.create_client(SERVICE_NAME, self.region, endpoint_url=self.host, config=config)
max_pool_connections=self._max_pool_connections,
)
self._client = cast(BotocoreBaseClientPrivate, self.session.create_client(SERVICE_NAME, self.region, endpoint_url=self.host, config=config))
self._convert_to_request_dict__endpoint_url = 'endpoint_url' in inspect.signature(self._client._convert_to_request_dict).parameters
return self._client

def get_meta_table(self, table_name, refresh=False):
Expand Down
19 changes: 19 additions & 0 deletions tests/test_base_connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@
Tests for the base connection class
"""
import base64
import io
import json
import six
from unittest import TestCase

import botocore.exceptions
import botocore.httpsession
import urllib3
from botocore.awsrequest import AWSPreparedRequest, AWSRequest, AWSResponse
from botocore.client import ClientError
from botocore.exceptions import BotoCoreError
Expand Down Expand Up @@ -1449,6 +1452,22 @@ def test_scan(self):
conn.scan,
table_name)

def test_make_api_call__happy_path(self):
response = AWSResponse(
url='https://www.example.com',
status_code=200,
raw=urllib3.HTTPResponse(
body=io.BytesIO(json.dumps({}).encode('utf-8')),
preload_content=False,
),
headers={'x-amzn-RequestId': 'abcdef'},
)

c = Connection()

with patch.object(botocore.httpsession.URLLib3Session, 'send', return_value=response):
c._make_api_call('CreateTable', {'TableName': 'MyTable'})

@mock.patch('pynamodb.connection.Connection.client')
def test_make_api_call_throws_verbose_error_after_backoff(self, client_mock):
response = AWSResponse(
Expand Down

0 comments on commit 54a0be4

Please sign in to comment.