Skip to content

Commit

Permalink
Add Instance.delete_tags
Browse files Browse the repository at this point in the history
The Tag resource was incorrectly modeled such that it is not a
collection on an Instance. As a result, there is no way to batch delete
tags from an Instance resource. This adds that functionality in, in a
way that won't break on future EC2 resource changes.

In the long term there are plans to codify SDK specific customizations
while maintaining a canonically correct resource model, but that will
sometime in the future as part of a different story.

Fixes #381
  • Loading branch information
JordonPhillips committed Feb 3, 2016
1 parent 8ce95c8 commit cf3308f
Show file tree
Hide file tree
Showing 9 changed files with 198 additions and 36 deletions.
3 changes: 2 additions & 1 deletion boto3/docs/action.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ def document_actions(self, section):
'automatically handle the passing in of arguments set '
'from identifiers and some attributes.'),
intro_link='actions_intro')

for action_name in sorted(resource_actions):
action_section = section.add_new_section(action_name)
if action_name in ['load', 'reload'] and self._resource_model.load:
Expand All @@ -61,7 +62,7 @@ def document_actions(self, section):
)
else:
document_custom_method(
section, action_name, resource_actions[action_name])
action_section, action_name, resource_actions[action_name])


def document_action(section, resource_name, event_emitter, action_model,
Expand Down
45 changes: 45 additions & 0 deletions boto3/ec2/deletetags.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"). You
# may not use this file except in compliance with the License. A copy of
# the License is located at
#
# http://aws.amazon.com/apache2.0/
#
# or in the "license" file accompanying this file. This file is
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
# ANY KIND, either express or implied. See the License for the specific
# language governing permissions and limitations under the License.
from boto3.utils import inject_attribute
from boto3.resources.model import Action
from boto3.docs.docstring import ActionDocstring


def inject_delete_tags(class_attributes, service_context, emitter, **kwargs):
action_model = {
'request': {
'operation': 'DeleteTags',
'params': [{
'target': 'Resources[0]',
'source': 'identifier',
'name': 'Id'
}]
}
}
action = Action('delete_tags', action_model, {})

delete_tags_action = delete_tags
delete_tags_action.__doc__ = ActionDocstring(
resource_name='Instance',
event_emitter=emitter,
action_model=action,
service_model=service_context.service_model,
include_signature=False
)

inject_attribute(class_attributes, 'delete_tags', delete_tags_action)


def delete_tags(self, **kwargs):
kwargs['Resources'] = [self.id]
return self.meta.client.delete_tags(**kwargs)
7 changes: 4 additions & 3 deletions boto3/resources/factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,9 +133,10 @@ def load_from_definition(self, resource_name,

base_classes = [ServiceResource]
if self._emitter is not None:
self._emitter.emit('creating-resource-class.%s' % cls_name,
class_attributes=attrs,
base_classes=base_classes)
self._emitter.emit(
'creating-resource-class.%s' % cls_name,
class_attributes=attrs, base_classes=base_classes,
service_context=service_context, emitter=self._emitter)
return type(str(cls_name), tuple(base_classes), attrs)

def _load_identifiers(self, attrs, meta, resource_model, resource_name):
Expand Down
4 changes: 4 additions & 0 deletions boto3/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -350,3 +350,7 @@ def _register_default_handlers(self):
'creating-resource-class.ec2.ServiceResource',
boto3.utils.lazy_call(
'boto3.ec2.createtags.inject_create_tags'))
self._session.register(
'creating-resource-class.ec2.Instance',
boto3.utils.lazy_call(
'boto3.ec2.deletetags.inject_delete_tags'))
10 changes: 5 additions & 5 deletions tests/functional/docs/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,21 +59,21 @@ def get_response_syntax_document_block(self, contents):
return contents[:end_index]

def get_request_parameter_document_block(self, param_name, contents):
start_param_document = ' :type %s:' % param_name
start_param_document = ':type %s:' % param_name
start_index = contents.find(start_param_document)
self.assertNotEqual(start_index, -1, 'Param is not found in contents')
contents = contents[start_index:]
end_index = contents.find(' :type', len(start_param_document))
end_index = contents.find(':type', len(start_param_document))
return contents[:end_index]

def get_response_parameter_document_block(self, param_name, contents):
start_param_document = ' **Response Structure**'
start_param_document = '**Response Structure**'
start_index = contents.find(start_param_document)
self.assertNotEqual(start_index, -1, 'There is no response structure')

start_param_document = ' - **%s**' % param_name
start_param_document = '- **%s**' % param_name
start_index = contents.find(start_param_document)
self.assertNotEqual(start_index, -1, 'Param is not found in contents')
contents = contents[start_index:]
end_index = contents.find(' - **', len(start_param_document))
end_index = contents.find('- **', len(start_param_document))
return contents[:end_index]
54 changes: 27 additions & 27 deletions tests/functional/docs/test_dynamodb.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,19 +42,19 @@ def test_document_interface_is_documented(self):
request_syntax_contents = self.get_request_syntax_document_block(
method_contents)
self.assert_contains_lines_in_order([
' response = table.put_item(',
' Item={',
(' \'string\': \'string\'|123|Binary(b\'bytes\')'
'response = table.put_item(',
'Item={',
('\'string\': \'string\'|123|Binary(b\'bytes\')'
'|True|None|set([\'string\'])|set([123])|'
'set([Binary(b\'bytes\')])|[]|{}'),
' },',
' Expected={',
' \'string\': {',
(' \'Value\': \'string\'|123'
'},',
'Expected={',
'\'string\': {',
('\'Value\': \'string\'|123'
'|Binary(b\'bytes\')|True|None|set([\'string\'])'
'|set([123])|set([Binary(b\'bytes\')])|[]|{},'),
' \'AttributeValueList\': [',
(' \'string\'|123|Binary(b\'bytes\')'
'\'AttributeValueList\': [',
('\'string\'|123|Binary(b\'bytes\')'
'|True|None|set([\'string\'])|set([123])|'
'set([Binary(b\'bytes\')])|[]|{},')],
request_syntax_contents)
Expand All @@ -63,22 +63,22 @@ def test_document_interface_is_documented(self):
response_syntax_contents = self.get_response_syntax_document_block(
method_contents)
self.assert_contains_lines_in_order([
' {',
' \'Attributes\': {',
(' \'string\': \'string\'|123|'
'{',
'\'Attributes\': {',
('\'string\': \'string\'|123|'
'Binary(b\'bytes\')|True|None|set([\'string\'])|'
'set([123])|set([Binary(b\'bytes\')])|[]|{}'),
' },'],
'},'],
response_syntax_contents)

# Make sure the request parameter is documented correctly.
request_param_contents = self.get_request_parameter_document_block(
'Item', method_contents)
self.assert_contains_lines_in_order([
' :type Item: dict',
' :param Item: **[REQUIRED]**',
' - *(string) --*',
(' - *(valid DynamoDB type) --* - The value of the '
':type Item: dict',
':param Item: **[REQUIRED]**',
'- *(string) --*',
('- *(valid DynamoDB type) --* - The value of the '
'attribute. The valid value types are listed in the '
':ref:`DynamoDB Reference Guide<ref_valid_dynamodb_types>`.')],
request_param_contents
Expand All @@ -88,9 +88,9 @@ def test_document_interface_is_documented(self):
response_param_contents = self.get_response_parameter_document_block(
'Attributes', method_contents)
self.assert_contains_lines_in_order([
' - **Attributes** *(dict) --*',
' - *(string) --*',
(' - *(valid DynamoDB type) --* - The value of '
'- **Attributes** *(dict) --*',
'- *(string) --*',
('- *(valid DynamoDB type) --* - The value of '
'the attribute. The valid value types are listed in the '
':ref:`DynamoDB Reference Guide<ref_valid_dynamodb_types>`.')],
response_param_contents)
Expand All @@ -106,23 +106,23 @@ def test_conditions_is_documented(self):
request_syntax_contents = self.get_request_syntax_document_block(
method_contents)
self.assert_contains_lines_in_order([
' response = table.query(',
(' FilterExpression=Attr(\'myattribute\').'
'response = table.query(',
('FilterExpression=Attr(\'myattribute\').'
'eq(\'myvalue\'),'),
(' KeyConditionExpression=Key(\'mykey\')'
('KeyConditionExpression=Key(\'mykey\')'
'.eq(\'myvalue\'),')],
request_syntax_contents)

# Make sure the request parameter is documented correctly.
self.assert_contains_lines_in_order([
(' :type FilterExpression: condition from '
(':type FilterExpression: condition from '
':py:class:`boto3.dynamodb.conditions.Attr` method'),
(' :param FilterExpression: The condition(s) an '
(':param FilterExpression: The condition(s) an '
'attribute(s) must meet. Valid conditions are listed in '
'the :ref:`DynamoDB Reference Guide<ref_dynamodb_conditions>`.'),
(' :type KeyConditionExpression: condition from '
(':type KeyConditionExpression: condition from '
':py:class:`boto3.dynamodb.conditions.Key` method'),
(' :param KeyConditionExpression: The condition(s) a '
(':param KeyConditionExpression: The condition(s) a '
'key(s) must meet. Valid conditions are listed in the '
':ref:`DynamoDB Reference Guide<ref_dynamodb_conditions>`.')],
method_contents)
35 changes: 35 additions & 0 deletions tests/functional/docs/test_ec2.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"). You
# may not use this file except in compliance with the License. A copy of
# the License is located at
#
# http://aws.amazon.com/apache2.0/
#
# or in the "license" file accompanying this file. This file is
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
# ANY KIND, either express or implied. See the License for the specific
# language governing permissions and limitations under the License.
from tests.functional.docs import BaseDocsFunctionalTests

from boto3.session import Session
from boto3.docs.service import ServiceDocumenter


class TestInstanceDeleteTags(BaseDocsFunctionalTests):
def setUp(self):
self.documenter = ServiceDocumenter(
'ec2', session=Session(region_name='us-east-1'))
self.generated_contents = self.documenter.document_service()
self.generated_contents = self.generated_contents.decode('utf-8')

def test_delete_tags_method_is_documented(self):
contents = self.get_class_document_block(
'EC2.Instance', self.generated_contents)
method_contents = self.get_method_document_block(
'delete_tags', contents)
self.assert_contains_lines_in_order([
'response = instance.delete_tags(',
'DryRun=True|False,',
'Tags=[',
], method_contents)
36 changes: 36 additions & 0 deletions tests/functional/test_ec2.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"). You
# may not use this file except in compliance with the License. A copy of
# the License is located at
#
# http://aws.amazon.com/apache2.0/
#
# or in the "license" file accompanying this file. This file is
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
# ANY KIND, either express or implied. See the License for the specific
# language governing permissions and limitations under the License.
import unittest

import boto3.session
from botocore.stub import Stubber


class TestInstanceDeleteTags(unittest.TestCase):
def setUp(self):
self.session = boto3.session.Session(region_name='us-west-2')
self.service_resource = self.session.resource('ec2')
self.instance_resource = self.service_resource.Instance('i-abc123')

def test_delete_tags_injected(self):
self.assertTrue(hasattr(self.instance_resource, 'delete_tags'),
'delete_tags was not injected onto Instance resource.')

def test_delete_tags(self):
stubber = Stubber(self.instance_resource.meta.client)
stubber.add_response('delete_tags', {})
stubber.activate()
response = self.instance_resource.delete_tags(Tags=[{'Key': 'foo'}])
stubber.assert_no_pending_responses()
self.assertEqual(response, {})
stubber.deactivate()
40 changes: 40 additions & 0 deletions tests/unit/ec2/test_deletetags.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the 'License'). You
# may not use this file except in compliance with the License. A copy of
# the License is located at
#
# http://aws.amazon.com/apache2.0/
#
# or in the 'license' file accompanying this file. This file is
# distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
# ANY KIND, either express or implied. See the License for the specific
# language governing permissions and limitations under the License.
import unittest
import mock

from boto3.ec2.deletetags import delete_tags


class TestDeleteTags(unittest.TestCase):
def setUp(self):
self.client = mock.Mock()
self.resource = mock.Mock()
self.resource.meta.client = self.client
self.instance_id = 'instance_id'
self.resource.id = self.instance_id

def test_delete_tags(self):
tags = {
'Tags': [
{'Key': 'key1', 'Value': 'value1'},
{'Key': 'key2', 'Value': 'value2'},
{'Key': 'key3', 'Value': 'value3'}
]
}

delete_tags(self.resource, **tags)

kwargs = tags
kwargs['Resources'] = [self.instance_id]
self.client.delete_tags.assert_called_with(**kwargs)

0 comments on commit cf3308f

Please sign in to comment.