Skip to content

Commit

Permalink
Merge pull request #45 from boto/meta-object
Browse files Browse the repository at this point in the history
Switch `resource.meta` to be an object.
  • Loading branch information
danielgtaylor committed Dec 19, 2014
2 parents bc866bb + e31eba1 commit 59136a4
Show file tree
Hide file tree
Showing 12 changed files with 126 additions and 112 deletions.
8 changes: 8 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
Changelog
=========

Unreleased
----------

* feature:Resources: Make ``resource.meta`` a proper object. This allows
you to do things like ``resource.meta.client``. This is a **backward-
incompatible** change.
(`issue 45 <https://github.com/boto/boto3/pull/45>`__)

0.0.6 - 2014-12-18
------------------

Expand Down
12 changes: 6 additions & 6 deletions boto3/resources/action.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,10 +71,10 @@ def __call__(self, parent, *args, **kwargs):
params = create_request_parameters(parent, self._action_model.request)
params.update(kwargs)

logger.info('Calling %s:%s with %r', parent.meta['service_name'],
logger.info('Calling %s:%s with %r', parent.meta.service_name,
operation_name, params)

response = getattr(parent.meta['client'], operation_name)(**params)
response = getattr(parent.meta.client, operation_name)(**params)

logger.debug('Response: %r', response)

Expand Down Expand Up @@ -125,9 +125,9 @@ def __call__(self, parent, *args, **kwargs):
# or low-level client from a collection, so we get
# these from the first resource in the collection.
if service_name is None:
service_name = resource.meta['service_name']
service_name = resource.meta.service_name
if client is None:
client = resource.meta['client']
client = resource.meta.client

create_request_parameters(
resource, self._action_model.request, params=params)
Expand Down Expand Up @@ -186,10 +186,10 @@ def __call__(self, parent, *args, **kwargs):
params.update(kwargs)

logger.info('Calling %s:%s with %r',
parent.meta['service_name'],
parent.meta.service_name,
self._waiter_resource_name, params)

client = parent.meta['client']
client = parent.meta.client
waiter = client.get_waiter(client_waiter_name)
response = waiter.wait(**params)

Expand Down
59 changes: 50 additions & 9 deletions boto3/resources/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,45 @@
import boto3


class ResourceMeta(object):
"""
An object containing metadata about a resource.
"""
def __init__(self, service_name, identifiers=None, client=None,
data=None):
#: (``string``) The service name, e.g. 's3'
self.service_name = service_name

if identifiers is None:
identifiers = []
#: (``list``) List of identifier names
self.identifiers = identifiers

#: (:py:class:`~botocore.client.BaseClient`) Low-level Botocore client
self.client = client
#: (``dict``) Loaded resource data attributes
self.data = data

def __repr__(self):
return 'ResourceMeta(\'{0}\', identifiers={1})'.format(
self.service_name, self.identifiers)

def __eq__(self, other):
# Two metas are equal if their components are all equal
if other.__class__.__name__ != self.__class__.__name__:
return False

return self.__dict__ == other.__dict__

def copy(self):
"""
Create a copy of this metadata object.
"""
params = self.__dict__.copy()
service_name = params.pop('service_name')
return ResourceMeta(service_name, **params)


class ServiceResource(object):
"""
A base class for resources.
Expand All @@ -29,11 +68,13 @@ class ServiceResource(object):
from when the instance was hydrated. For example::
# Get a low-level client from a resource instance
client = resource.meta['client']
client = resource.meta.client
response = client.operation(Param='foo')
# Print the resource instance's service short name
print(resource.meta['service_name'])
print(resource.meta.service_name)
See :py:class:`ResourceMeta` for more information.
"""

def __init__(self, *args, **kwargs):
Expand All @@ -43,35 +84,35 @@ def __init__(self, *args, **kwargs):

# Create a default client if none was passed
if kwargs.get('client') is not None:
self.meta['client'] = kwargs.get('client')
self.meta.client = kwargs.get('client')
else:
self.meta['client'] = boto3.client(self.meta['service_name'])
self.meta.client = boto3.client(self.meta.service_name)

# Allow setting identifiers as positional arguments in the order
# in which they were defined in the ResourceJSON.
for i, value in enumerate(args):
setattr(self, self.meta['identifiers'][i], value)
setattr(self, self.meta.identifiers[i], value)

# Allow setting identifiers via keyword arguments. Here we need
# extra logic to ignore other keyword arguments like ``client``.
for name, value in kwargs.items():
if name == 'client':
continue

if name not in self.meta['identifiers']:
if name not in self.meta.identifiers:
raise ValueError('Unknown keyword argument: {0}'.format(name))

setattr(self, name, value)

# Validate that all identifiers have been set.
for identifier in self.meta['identifiers']:
for identifier in self.meta.identifiers:
if getattr(self, identifier) is None:
raise ValueError(
'Required parameter {0} not set'.format(identifier))

def __repr__(self):
identifiers = []
for identifier in self.meta['identifiers']:
for identifier in self.meta.identifiers:
identifiers.append('{0}={1}'.format(
identifier, repr(getattr(self, identifier))))
return "{0}({1})".format(
Expand All @@ -86,7 +127,7 @@ def __eq__(self, other):

# Each of the identifiers should have the same value in both
# instances, e.g. two buckets need the same name to be equal.
for identifier in self.meta['identifiers']:
for identifier in self.meta.identifiers:
if getattr(self, identifier) != getattr(other, identifier):
return False

Expand Down
10 changes: 5 additions & 5 deletions boto3/resources/collection.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ def __repr__(self):
self.__class__.__name__,
self._parent,
'{0}.{1}'.format(
self._parent.meta['service_name'],
self._parent.meta.service_name,
self._model.resource.type
)
)
Expand Down Expand Up @@ -131,7 +131,7 @@ def pages(self):
:rtype: list(:py:class:`~boto3.resources.base.ServiceResource`)
:return: List of resource instances
"""
client = self._parent.meta['client']
client = self._parent.meta.client
cleaned_params = self._params.copy()
limit = cleaned_params.pop('limit', None)
page_size = cleaned_params.pop('page_size', None)
Expand All @@ -147,14 +147,14 @@ def pages(self):
# the page size parameter.
if client.can_paginate(self._py_operation_name):
logger.info('Calling paginated %s:%s with %r',
self._parent.meta['service_name'],
self._parent.meta.service_name,
self._py_operation_name, params)
paginator = client.get_paginator(self._py_operation_name)
pages = paginator.paginate(
max_items=limit, page_size=page_size, **params)
else:
logger.info('Calling %s:%s with %r',
self._parent.meta['service_name'],
self._parent.meta.service_name,
self._py_operation_name, params)
pages = [getattr(client, self._py_operation_name)(**params)]

Expand Down Expand Up @@ -321,7 +321,7 @@ def __repr__(self):
self.__class__.__name__,
self._parent,
'{0}.{1}'.format(
self._parent.meta['service_name'],
self._parent.meta.service_name,
self._model.resource.type
)
)
Expand Down
26 changes: 11 additions & 15 deletions boto3/resources/factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@

from .action import ServiceAction
from .action import WaiterAction
from .base import ServiceResource
from .base import ResourceMeta, ServiceResource
from .collection import CollectionFactory
from .model import ResourceModel
from .response import all_not_none, build_identifiers
Expand Down Expand Up @@ -65,11 +65,7 @@ def load_from_definition(self, service_name, resource_name, model,
:return: The service or resource class.
"""
# Set some basic info
meta = {
'service_name': service_name,
'identifiers': [],
'data': None,
}
meta = ResourceMeta(service_name)
attrs = {
'meta': meta,
}
Expand Down Expand Up @@ -107,7 +103,7 @@ def _load_identifiers(self, attrs, meta, model):
snake_cased = xform_name(identifier.name)
snake_cased = self._check_allowed_name(
attrs, snake_cased, 'identifier', model.name)
meta['identifiers'].append(snake_cased)
meta.identifiers.append(snake_cased)
attrs[snake_cased] = None

def _load_subresources(self, attrs, service_name, resource_name,
Expand Down Expand Up @@ -168,7 +164,7 @@ def _load_attributes(self, attrs, meta, model, service_model):

for name, member in shape.members.items():
snake_cased = xform_name(name)
if snake_cased in meta['identifiers']:
if snake_cased in meta.identifiers:
# Skip identifiers, these are set through other means
continue

Expand All @@ -190,7 +186,7 @@ def _load_collections(self, attrs, model, resource_defs, service_model):
attrs, snake_cased, 'collection', model.name)

attrs[snake_cased] = self._create_collection(
attrs['meta']['service_name'], model.name, snake_cased,
attrs['meta'].service_name, model.name, snake_cased,
collection_model, resource_defs, service_model)

def _load_references(self, attrs, service_name, resource_name,
Expand Down Expand Up @@ -272,14 +268,14 @@ def _create_autoload_property(factory_self, name, snake_cased):
# it first checks to see if it CAN be loaded (raise if not), then
# calls the load before returning the value.
def property_loader(self):
if self.meta['data'] is None:
if self.meta.data is None:
if hasattr(self, 'load'):
self.load()
else:
raise ResourceLoadException(
'{0} has no load method'.format(self.__class__.__name__))

return self.meta['data'].get(name)
return self.meta.data.get(name)

property_loader.__name__ = str(snake_cased)
property_loader.__doc__ = 'TODO'
Expand Down Expand Up @@ -366,12 +362,12 @@ def create_resource(self, *args, **kwargs):
pargs.append(getattr(self, xform_name(key)))

return partial(resource_cls, *pargs,
client=self.meta.get('client'))(*args, **kwargs)
client=self.meta.client)(*args, **kwargs)

# Generate documentation about required and optional params
doc = 'Create a new instance of {0}\n\nRequired identifiers:\n'

for identifier in resource_cls.meta['identifiers']:
for identifier in resource_cls.meta.identifiers:
doc += ':type {0}: string\n'.format(identifier)
doc += ':param {0}: {0} identifier\n'.format(identifier)

Expand Down Expand Up @@ -405,7 +401,7 @@ def _create_action(factory_self, snake_cased, action_model, resource_defs,
# instance via ``self``.
def do_action(self, *args, **kwargs):
response = action(self, *args, **kwargs)
self.meta['data'] = response
self.meta.data = response
else:
# We need a new method here because we want access to the
# instance via ``self``.
Expand All @@ -416,7 +412,7 @@ def do_action(self, *args, **kwargs):
# Clear cached data. It will be reloaded the next
# time that an attribute is accessed.
# TODO: Make this configurable in the future?
self.meta['data'] = None
self.meta.data = None

return response

Expand Down
8 changes: 4 additions & 4 deletions boto3/resources/response.py
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ def __call__(self, parent, params, response):
"""
resource_name = self.resource_model.type
resource_cls = self.factory.load_from_definition(
parent.meta['service_name'], resource_name,
parent.meta.service_name, resource_name,
self.resource_defs.get(resource_name), self.resource_defs,
self.service_model)

Expand All @@ -196,7 +196,7 @@ def __call__(self, parent, params, response):

# Anytime a path is defined, it means the response contains the
# resource's attributes, so resource_data gets set here. It
# eventually ends up in resource.meta['data'], which is where
# eventually ends up in resource.meta.data, which is where
# the attribute properties look for data.
if self.search_path:
search_response = jmespath.search(self.search_path, raw_response)
Expand Down Expand Up @@ -258,7 +258,7 @@ def handle_response_item(self, resource_cls, parent, identifiers,
:return: New resource instance.
"""
kwargs = {
'client': parent.meta.get('client'),
'client': parent.meta.client,
}

for name, value in identifiers.items():
Expand All @@ -271,6 +271,6 @@ def handle_response_item(self, resource_cls, parent, identifiers,
resource = resource_cls(**kwargs)

if resource_data is not None:
resource.meta['data'] = resource_data
resource.meta.data = resource_data

return resource
2 changes: 1 addition & 1 deletion docs/source/guide/clients.rst
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ resource::
sqs_resource = boto3.resource('sqs')

# Get the client from the resource
sqs = sqs_resource.meta['client']
sqs = sqs_resource.meta.client

Service Operations
------------------
Expand Down
2 changes: 1 addition & 1 deletion tests/integration/test_s3.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ def create_bucket_resource(self, bucket_name, region=None):
return bucket

def test_s3(self):
client = self.s3.meta['client']
client = self.s3.meta.client

# Create a bucket (resource action with a resource response)
bucket = self.create_bucket_resource(self.bucket_name)
Expand Down
Loading

0 comments on commit 59136a4

Please # to comment.