Skip to content

Create common interface for aio response classes #1328

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ Changes
^^^^^^^^^^^^^^^^^^^
* patch `ResponseParser`
* use SPDX license identifier for project metadata
* refactor `AioResponseParser` to have a consistent async implementation of the `parse` method.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's RST

Suggested change
* refactor `AioResponseParser` to have a consistent async implementation of the `parse` method.
* refactor ``AioResponseParser`` to have a consistent async implementation of the ``parse`` method.


2.21.1 (2025-03-04)
^^^^^^^^^^^^^^^^^^^
Expand Down
17 changes: 4 additions & 13 deletions aiobotocore/endpoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -204,15 +204,9 @@ async def _do_get_response(self, request, operation_model, context):
)
parser = self._response_parser_factory.create_parser(protocol)

if asyncio.iscoroutinefunction(parser.parse):
parsed_response = await parser.parse(
response_dict, operation_model.output_shape
)
else:
parsed_response = parser.parse(
response_dict, operation_model.output_shape
)

parsed_response = await parser.parse(
response_dict, operation_model.output_shape
)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

have we validated across all the parsers this is now true? Are there parsers that don't inherit from our baseclass?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i see everything parents to ResponseParser 👍🏼

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call!

Upstream has a number of parsers that we have not patched:

  • BaseXMLResponseParser
  • BaseJSONParser
  • BaseEventStreamParser
  • EventStreamJSONParser
  • EventStreamXMLParser
  • BaseRestParser

The Base* parsers should be fine, since we patch any relevant child parsers. But I believe the EventStream* parsers might be problematic. They are used indirectly and might need to be patched to work correctly.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I could prepare a follow-up of #1312 to fully patch the parser class hierarchy. That would be a prerequisite for this PR, IMO. Any thoughts?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#1329 makes sure that EVENT_STREAM_PARSER_CLS class attributes ("indirect" uses) point to patched parsers. Once that is merged, we can continue with this one.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My belief atm is that not having an async version for EventStream* is fine.

I guess it would be, as long as we continue to allow for both sync and async variants of parse(). However, this PR takes away that flexibility and we need to be careful to handle all parser classes, as @thehesiod rightfully pointed out. As far as I understand, that includes parser classes that we have not patched previously, hence #1329. I could well be on the wrong track here, though.

An alternative would be to simply keep the current implementation of parse() and move it up the AioResponseParser, but leave everything else as it is.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ya that's why I was waiting to do a review of this PR when I had some time, we need to thoroughly look at how these classes are used

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lets report back when one of us does that

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about allowing sync parse methods but emit a deprecation warning?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sounds like a plan, would be safer

parsed_response.update(customized_response_dict)

if http_response.status_code >= 300:
Expand Down Expand Up @@ -240,10 +234,7 @@ async def _add_modeled_error_fields(
if error_shape is None:
return

if asyncio.iscoroutinefunction(parser.parse):
modeled_parse = await parser.parse(response_dict, error_shape)
else:
modeled_parse = parser.parse(response_dict, error_shape)
modeled_parse = await parser.parse(response_dict, error_shape)
# TODO: avoid naming conflicts with ResponseMetadata and Error
parsed_response.update(modeled_parse)

Expand Down
69 changes: 34 additions & 35 deletions aiobotocore/parsers.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
lowercase_dict,
)

from ._helpers import resolve_awaitable
from .eventstream import AioEventStream


Expand All @@ -31,6 +32,39 @@ def _create_event_stream(self, response, shape):
name = response['context'].get('operation_name')
return AioEventStream(response['body'], shape, parser, name)

async def parse(self, response, shape):
LOG.debug('Response headers: %s', response['headers'])
LOG.debug('Response body:\n%s', response['body'])
if response['status_code'] >= 301:
if self._is_generic_error_response(response):
parsed = self._do_generic_error_parse(response)
elif self._is_modeled_error_shape(shape):
parsed = self._do_modeled_error_parse(response, shape)
# We don't want to decorate the modeled fields with metadata
return parsed
else:
parsed = self._do_error_parse(response, shape)
else:
parsed = await resolve_awaitable(self._do_parse(response, shape))

# We don't want to decorate event stream responses with metadata
if shape and shape.serialization.get('eventstream'):
return parsed

# Add ResponseMetadata if it doesn't exist and inject the HTTP
# status code and headers from the response.
if isinstance(parsed, dict):
response_metadata = parsed.get('ResponseMetadata', {})
response_metadata['HTTPStatusCode'] = response['status_code']
# Ensure that the http header keys are all lower cased. Older
# versions of urllib3 (< 1.11) would unintentionally do this for us
# (see urllib3#633). We need to do this conversion manually now.
headers = response['headers']
response_metadata['HTTPHeaders'] = lowercase_dict(headers)
parsed['ResponseMetadata'] = response_metadata
self._add_checksum_response_metadata(response, response_metadata)
return parsed


class AioQueryParser(QueryParser, AioResponseParser):
pass
Expand Down Expand Up @@ -66,41 +100,6 @@ async def _handle_event_stream(self, response, shape, event_name):
parsed[event_name] = event_stream
return parsed

# this is actually from ResponseParser however for now JSONParser is the
# only class that needs this async
async def parse(self, response, shape):
LOG.debug('Response headers: %s', response['headers'])
LOG.debug('Response body:\n%s', response['body'])
if response['status_code'] >= 301:
if self._is_generic_error_response(response):
parsed = self._do_generic_error_parse(response)
elif self._is_modeled_error_shape(shape):
parsed = self._do_modeled_error_parse(response, shape)
# We don't want to decorate the modeled fields with metadata
return parsed
else:
parsed = self._do_error_parse(response, shape)
else:
parsed = await self._do_parse(response, shape)

# We don't want to decorate event stream responses with metadata
if shape and shape.serialization.get('eventstream'):
return parsed

# Add ResponseMetadata if it doesn't exist and inject the HTTP
# status code and headers from the response.
if isinstance(parsed, dict):
response_metadata = parsed.get('ResponseMetadata', {})
response_metadata['HTTPStatusCode'] = response['status_code']
# Ensure that the http header keys are all lower cased. Older
# versions of urllib3 (< 1.11) would unintentionally do this for us
# (see urllib3#633). We need to do this conversion manually now.
headers = response['headers']
response_metadata['HTTPHeaders'] = lowercase_dict(headers)
parsed['ResponseMetadata'] = response_metadata
self._add_checksum_response_metadata(response, response_metadata)
return parsed


class AioRestJSONParser(RestJSONParser, AioResponseParser):
pass
Expand Down
7 changes: 1 addition & 6 deletions aiobotocore/response.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,10 +150,5 @@ async def get_response(operation_model, http_response):
response_dict['body'] = await http_response.content

parser = parsers.create_parser(protocol)
if asyncio.iscoroutinefunction(parser.parse):
parsed = await parser.parse(
response_dict, operation_model.output_shape
)
else:
parsed = parser.parse(response_dict, operation_model.output_shape)
parsed = await parser.parse(response_dict, operation_model.output_shape)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Apply deprecation pattern from endpoints.py

return http_response, parsed
3 changes: 1 addition & 2 deletions tests/test_patches.py
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That test was slightly refactored in the default branch. Please do a rebase or merge in new commits.

Original file line number Diff line number Diff line change
Expand Up @@ -428,6 +428,7 @@
ResponseParser._create_event_stream: {
'0564ba55383a71cc1ba3e5be7110549d7e9992f5',
},
ResponseParser.parse: {'c2153eac3789855f4fc6a816a1f30a6afe0cf969'},
RestXMLParser._create_event_stream: {
'0564ba55383a71cc1ba3e5be7110549d7e9992f5'
},
Expand All @@ -444,8 +445,6 @@
JSONParser._handle_event_stream: {
'3cf7bb1ecff0d72bafd7e7fd6625595b4060abd6'
},
# NOTE: if this hits we need to change our ResponseParser impl in JSONParser
JSONParser.parse: {'c2153eac3789855f4fc6a816a1f30a6afe0cf969'},
RestJSONParser._create_event_stream: {
'0564ba55383a71cc1ba3e5be7110549d7e9992f5'
},
Expand Down
Loading