@@ -0,0 +1,150 @@
# Copyright (c) 2012 Mitch Garnaat http://garnaat.org/
# Copyright (c) 2012 Amazon.com, Inc. or its affiliates.
# All Rights Reserved
#
# Permission is hereby granted, free of charge, to any person obtaining a
# copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish, dis-
# tribute, sublicense, and/or sell copies of the Software, and to permit
# persons to whom the Software is furnished to do so, subject to the fol-
# lowing conditions:
#
# The above copyright notice and this permission notice shall be included
# in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABIL-
# ITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
# SHALL THE AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
# IN THE SOFTWARE.
#
try:
import simplejson as json
except ImportError:
import json

import boto.exception
import requests
import boto

class SearchServiceException(Exception):
pass


class CommitMismatchError(Exception):
pass


class DocumentServiceConnection(object):

def __init__(self, domain=None, endpoint=None):
self.domain = domain
self.endpoint = endpoint
if not self.endpoint:
self.endpoint = domain.doc_service_endpoint
self.documents_batch = []
self._sdf = None

def add(self, _id, version, fields, lang='en'):
d = {'type': 'add', 'id': _id, 'version': version, 'lang': lang,
'fields': fields}
self.documents_batch.append(d)

def delete(self, _id, version):
d = {'type': 'delete', 'id': _id, 'version': version}
self.documents_batch.append(d)

def get_sdf(self):
return self._sdf if self._sdf else json.dumps(self.documents_batch)

def clear_sdf(self):
self._sdf = None
self.documents_batch = []

def add_sdf_from_s3(self, key_obj):
"""@todo (lucas) would be nice if this could just take an s3://uri..."""
self._sdf = key_obj.get_contents_as_string()

def commit(self):
sdf = self.get_sdf()

if ': null' in sdf:
boto.log.error('null value in sdf detected. This will probably raise '
'500 error.')
index = sdf.index(': null')
boto.log.error(sdf[index - 100:index + 100])

url = "http://%s/2011-02-01/documents/batch" % (self.endpoint)

request_config = {
'pool_connections': 20,
'keep_alive': True,
'max_retries': 5,
'pool_maxsize': 50
}

r = requests.post(url, data=sdf, config=request_config,
headers={'Content-Type': 'application/json'})

return CommitResponse(r, self, sdf)


class CommitResponse(object):
"""Wrapper for response to Cloudsearch document batch commit.
:type response: :class:`requests.models.Response`
:param response: Response from Cloudsearch /documents/batch API
:type doc_service: :class:`exfm.cloudsearch.DocumentServiceConnection`
:param doc_service: Object containing the documents posted and methods to
retry
:raises: :class:`boto.exception.BotoServerError`
:raises: :class:`exfm.cloudsearch.SearchServiceException`
"""
def __init__(self, response, doc_service, sdf):
self.response = response
self.doc_service = doc_service
self.sdf = sdf

try:
self.content = json.loads(response.content)
except:
boto.log.error('Error indexing documents.\nResponse Content:\n{}\n\n'
'SDF:\n{}'.format(response.content, self.sdf))
raise boto.exception.BotoServerError(self.response.status_code, '',
body=response.content)

self.status = self.content['status']
if self.status == 'error':
self.errors = [e.get('message') for e in self.content.get('errors',
[])]
else:
self.errors = []

self.adds = self.content['adds']
self.deletes = self.content['deletes']
self._check_num_ops('add', self.adds)
self._check_num_ops('delete', self.deletes)

def _check_num_ops(self, type_, response_num):
"""Raise exception if number of ops in response doesn't match commit
:type type_: str
:param type_: Type of commit operation: 'add' or 'delete'
:type response_num: int
:param response_num: Number of adds or deletes in the response.
:raises: :class:`exfm.cloudsearch.SearchServiceException`
"""
commit_num = len([d for d in self.doc_service.documents_batch
if d['type'] == type_])

if response_num != commit_num:
raise CommitMismatchError(
'Incorrect number of {}s returned. Commit: {} Respose: {}'\
.format(type_, commit_num, response_num))

Large diffs are not rendered by default.

Large diffs are not rendered by default.

@@ -0,0 +1,52 @@
# Copyright (c) 2012 Mitch Garnaat http://garnaat.org/
# Copyright (c) 2012 Amazon.com, Inc. or its affiliates.
# All Rights Reserved
#
# Permission is hereby granted, free of charge, to any person obtaining a
# copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish, dis-
# tribute, sublicense, and/or sell copies of the Software, and to permit
# persons to whom the Software is furnished to do so, subject to the fol-
# lowing conditions:
#
# The above copyright notice and this permission notice shall be included
# in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABIL-
# ITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
# SHALL THE AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
# IN THE SOFTWARE.
#

from .layer1 import Layer1
from .domain import Domain


class Layer2(object):

def __init__(self, aws_access_key_id=None, aws_secret_access_key=None,
is_secure=True, port=None, proxy=None, proxy_port=None,
host=None, debug=0, session_token=None, region=None):
self.layer1 = Layer1(aws_access_key_id, aws_secret_access_key,
is_secure, port, proxy, proxy_port,
host, debug, session_token, region)

def list_domains(self, domain_names=None):
"""
Return a list of :class:`boto.cloudsearch.domain.Domain`
objects for each domain defined in the current account.
"""
domain_data = self.layer1.describe_domains(domain_names)
return [Domain(self.layer1, data) for data in domain_data]

def create_domain(self, domain_name):
"""
Create a new CloudSearch domain and return the corresponding
:class:`boto.cloudsearch.domain.Domain` object.
"""
data = self.layer1.create_domain(domain_name)
return Domain(self.layer1, data)
@@ -0,0 +1,249 @@
# Copyright (c) 2012 Mitch Garnaat http://garnaat.org/
# Copyright (c) 2012 Amazon.com, Inc. or its affiliates.
# All Rights Reserved
#
# Permission is hereby granted, free of charge, to any person obtaining a
# copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish, dis-
# tribute, sublicense, and/or sell copies of the Software, and to permit
# persons to whom the Software is furnished to do so, subject to the fol-
# lowing conditions:
#
# The above copyright notice and this permission notice shall be included
# in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABIL-
# ITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
# SHALL THE AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
# IN THE SOFTWARE.
#

try:
import simplejson as json
except ImportError:
import json

class OptionStatus(dict):
"""
Presents a combination of status field (defined below) which are
accessed as attributes and option values which are stored in the
native Python dictionary. In this class, the option values are
merged from a JSON object that is stored as the Option part of
the object.
:ivar domain_name: The name of the domain this option is associated with.
:ivar create_date: A timestamp for when this option was created.
:ivar state: The state of processing a change to an option.
Possible values:
* RequiresIndexDocuments: the option's latest value will not
be visible in searches until IndexDocuments has been called
and indexing is complete.
* Processing: the option's latest value is not yet visible in
all searches but is in the process of being activated.
* Active: the option's latest value is completely visible.
:ivar update_date: A timestamp for when this option was updated.
:ivar update_version: A unique integer that indicates when this
option was last updated.
"""

def __init__(self, domain, data=None, refresh_fn=None, save_fn=None):
self.domain = domain
self.refresh_fn = refresh_fn
self.save_fn = save_fn
self.refresh(data)

def _update_status(self, status):
self.creation_date = status['creation_date']
self.status = status['state']
self.update_date = status['update_date']
self.update_version = int(status['update_version'])

def _update_options(self, options):
if options:
self.update(json.loads(options))

def refresh(self, data=None):
"""
Refresh the local state of the object. You can either pass
new state data in as the parameter ``data`` or, if that parameter
is omitted, the state data will be retrieved from CloudSearch.
"""
if not data:
if self.refresh_fn:
data = self.refresh_fn(self.domain.name)
if data:
self._update_status(data['status'])
self._update_options(data['options'])

def to_json(self):
"""
Return the JSON representation of the options as a string.
"""
return json.dumps(self)

def startElement(self, name, attrs, connection):
return None

def endElement(self, name, value, connection):
if name == 'CreationDate':
self.created = value
elif name == 'State':
self.state = value
elif name == 'UpdateDate':
self.updated = value
elif name == 'UpdateVersion':
self.update_version = int(value)
elif name == 'Options':
self.update_from_json_doc(value)
else:
setattr(self, name, value)

def save(self):
"""
Write the current state of the local object back to the
CloudSearch service.
"""
if self.save_fn:
data = self.save_fn(self.domain.name, self.to_json())
self.refresh(data)

def wait_for_state(self, state):
"""
Performs polling of CloudSearch to wait for the ``state``
of this object to change to the provided state.
"""
while self.state != state:
time.sleep(5)
self.refresh()


class IndexFieldStatus(OptionStatus):

def _update_options(self, options):
self.update(options)

def save(self):
pass


class RankExpressionStatus(IndexFieldStatus):

pass

class ServicePoliciesStatus(OptionStatus):

def new_statement(self, arn, ip):
"""
Returns a new policy statement that will allow
access to the service described by ``arn`` by the
ip specified in ``ip``.
:type arn: string
:param arn: The Amazon Resource Notation identifier for the
service you wish to provide access to. This would be
either the search service or the document service.
:type ip: string
:param ip: An IP address or CIDR block you wish to grant access
to.
"""
return {
"Effect":"Allow",
"Action":"*", # Docs say use GET, but denies unless *
"Resource": arn,
"Condition": {
"IpAddress": {
"aws:SourceIp": [ip]
}
}
}

def _allow_ip(self, arn, ip):
if 'Statement' not in self:
s = self.new_statement(arn, ip)
self['Statement'] = [s]
self.save()
else:
add_statement = True
for statement in self['Statement']:
if statement['Resource'] == arn:
for condition_name in statement['Condition']:
if condition_name == 'IpAddress':
add_statement = False
condition = statement['Condition'][condition_name]
if ip not in condition['aws:SourceIp']:
condition['aws:SourceIp'].append(ip)

if add_statement:
s = self.new_statement(arn, ip)
self['Statement'].append(s)
self.save()

def allow_search_ip(self, ip):
"""
Add the provided ip address or CIDR block to the list of
allowable address for the search service.
:type ip: string
:param ip: An IP address or CIDR block you wish to grant access
to.
"""
arn = self.domain.search_service_arn
self._allow_ip(arn, ip)

def allow_doc_ip(self, ip):
"""
Add the provided ip address or CIDR block to the list of
allowable address for the document service.
:type ip: string
:param ip: An IP address or CIDR block you wish to grant access
to.
"""
arn = self.domain.doc_service_arn
self._allow_ip(arn, ip)

def _disallow_ip(self, arn, ip):
if 'Statement' not in self:
return
need_update = False
for statement in self['Statement']:
if statement['Resource'] == arn:
for condition_name in statement['Condition']:
if condition_name == 'IpAddress':
condition = statement['Condition'][condition_name]
if ip in condition['aws:SourceIp']:
condition['aws:SourceIp'].remove(ip)
need_update = True
if need_update:
self.save()

def disallow_search_ip(self, ip):
"""
Remove the provided ip address or CIDR block from the list of
allowable address for the search service.
:type ip: string
:param ip: An IP address or CIDR block you wish to grant access
to.
"""
arn = self.domain.search_service_arn
self._disallow_ip(arn, ip)

def disallow_doc_ip(self, ip):
"""
Remove the provided ip address or CIDR block from the list of
allowable address for the document service.
:type ip: string
:param ip: An IP address or CIDR block you wish to grant access
to.
"""
arn = self.domain.doc_service_arn
self._disallow_ip(arn, ip)
@@ -0,0 +1,298 @@
# Copyright (c) 2012 Mitch Garnaat http://garnaat.org/
# Copyright (c) 2012 Amazon.com, Inc. or its affiliates.
# All Rights Reserved
#
# Permission is hereby granted, free of charge, to any person obtaining a
# copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish, dis-
# tribute, sublicense, and/or sell copies of the Software, and to permit
# persons to whom the Software is furnished to do so, subject to the fol-
# lowing conditions:
#
# The above copyright notice and this permission notice shall be included
# in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABIL-
# ITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
# SHALL THE AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
# IN THE SOFTWARE.
#
from math import ceil
import time
import json
import boto
import requests


class SearchServiceException(Exception):
pass


class CommitMismatchError(Exception):
pass


class SearchResults(object):

def __init__(self, **attrs):
self.rid = attrs['info']['rid']
# self.doc_coverage_pct = attrs['info']['doc-coverage-pct']
self.cpu_time_ms = attrs['info']['cpu-time-ms']
self.time_ms = attrs['info']['time-ms']
self.hits = attrs['hits']['found']
self.docs = attrs['hits']['hit']
self.start = attrs['hits']['start']
self.rank = attrs['rank']
self.match_expression = attrs['match-expr']
self.query = attrs['query']
self.search_service = attrs['search_service']

self.num_pages_needed = ceil(self.hits / self.query.real_size)

def __len__(self):
return len(self.docs)

def __iter__(self):
return iter(self.docs)

def next_page(self):
"""Call Cloudsearch to get the next page of search results
:rtype: :class:`exfm.cloudsearch.SearchResults`
:return: A cloudsearch SearchResults object
"""
if self.query.page <= self.num_pages_needed:
self.query.start += self.query.real_size
self.query.page += 1
return self.search_service(self.query)
else:
raise StopIteration


class Query(object):

RESULTS_PER_PAGE = 500

def __init__(self, q=None, bq=None, rank=None,
return_fields=None, size=10,
start=0, facet=None, facet_constraints=None,
facet_sort=None, facet_top_n=None, t=None):

self.q = q
self.bq = bq
self.rank = rank or []
self.return_fields = return_fields or []
self.start = start
self.facet = facet or []
self.facet_constraints = facet_constraints or {}
self.facet_sort = facet_sort or {}
self.facet_top_n = facet_top_n or {}
self.t = t or {}
self.page = 0
self.update_size(size)

def update_size(self, new_size):
self.size = new_size
self.real_size = Query.RESULTS_PER_PAGE if (self.size >
Query.RESULTS_PER_PAGE or self.size == 0) else self.size

def to_params(self):
"""Transform search parameters from instance properties to a dictionary
:rtype: dict
:return: search parameters
"""
params = {'start': self.start, 'size': self.real_size}

if self.q:
params['q'] = self.q

if self.bq:
params['bq'] = self.bq

if self.rank:
params['rank'] = ','.join(self.rank)

if self.return_fields:
params['return-fields'] = ','.join(self.return_fields)

if self.facet:
params['facet'] = ','.join(self.facet)

if self.facet_constraints:
for k, v in self.facet_constraints.iteritems():
params['facet-%s-constraints' % k] = v

if self.facet_sort:
for k, v in self.facet_sort.iteritems():
params['facet-%s-sort' % k] = v

if self.facet_top_n:
for k, v in self.facet_top_n.iteritems():
params['facet-%s-top-n' % k] = v

if self.t:
for k, v in self.t.iteritems():
params['t-%s' % k] = v
return params


class SearchConnection(object):

def __init__(self, domain=None, endpoint=None):
self.domain = domain
self.endpoint = endpoint
if not endpoint:
self.endpoint = domain.search_service_endpoint

def build_query(self, q=None, bq=None, rank=None, return_fields=None,
size=10, start=0, facet=None, facet_constraints=None,
facet_sort=None, facet_top_n=None, t=None):
return Query(q=q, bq=bq, rank=rank, return_fields=return_fields,
size=size, start=start, facet=facet,
facet_constraints=facet_constraints,
facet_sort=facet_sort, facet_top_n=facet_top_n, t=t)

def search(self, q=None, bq=None, rank=None, return_fields=None,
size=10, start=0, facet=None, facet_constraints=None,
facet_sort=None, facet_top_n=None, t=None):
"""
Query Cloudsearch
:type q:
:param q:
:type bq:
:param bq:
:type rank:
:param rank:
:type return_fields:
:param return_fields:
:type size:
:param size:
:type start:
:param start:
:type facet:
:param facet:
:type facet_constraints:
:param facet_constraints:
:type facet_sort:
:param facet_sort:
:type facet_top_n:
:param facet_top_n:
:type t:
:param t:
:rtype: :class:`exfm.cloudsearch.SearchResults`
:return: A cloudsearch SearchResults object
"""

query = self.build_query(q=q, bq=bq, rank=rank,
return_fields=return_fields,
size=size, start=start, facet=facet,
facet_constraints=facet_constraints,
facet_sort=facet_sort,
facet_top_n=facet_top_n, t=t)
return self(query)

def __call__(self, query):
"""Make a call to CloudSearch
:type query: :class:`exfm.cloudsearch.Query`
:param query: A fully specified Query instance
:rtype: :class:`exfm.cloudsearch.SearchResults`
:return: A cloudsearch SearchResults object
"""
url = "http://%s/2011-02-01/search" % (self.endpoint)
params = query.to_params()

r = requests.get(url, params=params)
data = json.loads(r.content)
data['query'] = query
data['search_service'] = self

if 'messages' in data and 'error' in data:
for m in data['messages']:
if m['severity'] == 'fatal':
raise SearchServiceException("Error processing search %s "
"=> %s" % (params, m['message']), query)
elif 'error' in data:
raise SearchServiceException("Unknown error processing search %s"
% (params), query)

return SearchResults(**data)

def get_all_paged(self, query, per_page):
"""Get a generator to iterate over all pages of search results
:type query: :class:`exfm.cloudsearch.Query`
:param query: A fully specified Query instance
:type per_page: int
:param per_page: Number of docs in each SearchResults object.
:rtype: generator
:return: Generator containing :class:`exfm.cloudsearch.SearchResults`
"""
query.update_size(per_page)
page = 0
num_pages_needed = 0
while page <= num_pages_needed:
results = self(query)
num_pages_needed = results.num_pages_needed
yield results
query.start += query.real_size
page += 1

def get_all_hits(self, query):
"""Get a generator to iterate over all search results
Transparently handles the results paging from Cloudsearch
search results so even if you have many thousands of results
you can iterate over all results in a reasonably efficient
manner.
:type query: :class:`exfm.cloudsearch.Query`
:param query: A fully specified Query instance
:rtype: generator
:return: All docs matching query
"""
page = 0
num_pages_needed = 0
while page <= num_pages_needed:
results = self(query)
num_pages_needed = results.num_pages_needed
for doc in results:
yield doc
query.start += query.real_size
page += 1

def get_num_hits(self, query):
"""Return the total number of hits for query
:type query: :class:`exfm.cloudsearch.Query`
:param query: A fully specified Query instance
:rtype: int
:return: Total number of hits for query
"""
query.update_size(1)
return self(query).hits



@@ -0,0 +1,75 @@
# Copyright (c) 202 Mitch Garnaat http://garnaat.org/
# Copyright (c) 2012 Amazon.com, Inc. or its affiliates.
# All Rights Reserved
#
# Permission is hereby granted, free of charge, to any person obtaining a
# copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish, dis-
# tribute, sublicense, and/or sell copies of the Software, and to permit
# persons to whom the Software is furnished to do so, subject to the fol-
# lowing conditions:
#
# The above copyright notice and this permission notice shall be included
# in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABIL-
# ITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
# SHALL THE AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
# IN THE SOFTWARE.

class SourceAttribute(object):
"""
Provide information about attributes for an index field.
A maximum of 20 source attributes can be configured for
each index field.
:ivar default: Optional default value if the source attribute
is not specified in a document.
:ivar name: The name of the document source field to add
to this ``IndexField``.
:ivar data_function: Identifies the transformation to apply
when copying data from a source attribute.
:ivar data_map: The value is a dict with the following keys:
* cases - A dict that translates source field values
to custom values.
* default - An optional default value to use if the
source attribute is not specified in a document.
* name - the name of the document source field to add
to this ``IndexField``
:ivar data_trim_title: Trims common title words from a source
document attribute when populating an ``IndexField``.
This can be used to create an ``IndexField`` you can
use for sorting. The value is a dict with the following
fields:
* default - An optional default value.
* language - an IETF RFC 4646 language code.
* separator - The separator that follows the text to trim.
* name - The name of the document source field to add.
"""

ValidDataFunctions = ('Copy', 'TrimTitle', 'Map')

def __init__(self):
self.data_copy = {}
self._data_function = self.ValidDataFunctions[0]
self.data_map = {}
self.data_trim_title = {}

@property
def data_function(self):
return self._data_function

@data_function.setter
def data_function(self, value):
if value not in self.ValidDataFunctions:
valid = '|'.join(self.ValidDataFunctions)
raise ValueError('data_function must be one of: %s' % valid)
self._data_function = value

@@ -450,10 +450,10 @@ def __init__(self, host, aws_access_key_id=None, aws_secret_access_key=None,
self.protocol = 'http'
self.host = host
self.path = path
if isinstance(debug, (int, long)):
self.debug = debug
else:
self.debug = config.getint('Boto', 'debug', 0)
# if the value passed in for debug
if not isinstance(debug, (int, long)):
debug = 0
self.debug = config.getint('Boto', 'debug', debug)
if port:
self.port = port
else:
@@ -470,10 +470,14 @@ def __init__(self, host, aws_access_key_id=None, aws_secret_access_key=None,
timeout = config.getint('Boto', 'http_socket_timeout')
self.http_connection_kwargs['timeout'] = timeout

self.provider = Provider(provider,
aws_access_key_id,
aws_secret_access_key,
security_token)
if isinstance(provider, Provider):
# Allow overriding Provider
self.provider = provider
else:
self.provider = Provider(provider,
aws_access_key_id,
aws_secret_access_key,
security_token)

# allow config file to override default host
if self.provider.host:
@@ -645,7 +649,12 @@ def proxy_ssl(self):
if self.proxy_user and self.proxy_pass:
for k, v in self.get_proxy_auth_header().items():
sock.sendall("%s: %s\r\n" % (k, v))
sock.sendall("\r\n")
# See discussion about this config option at
# https://groups.google.com/forum/?fromgroups#!topic/boto-dev/teenFvOq2Cc
if config.getbool('Boto', 'send_crlf_after_proxy_auth_headers', False):
sock.sendall("\r\n")
else:
sock.sendall("\r\n")
resp = httplib.HTTPResponse(sock, strict=True, debuglevel=self.debug)
resp.begin()

@@ -15,36 +15,46 @@
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABIL-
# ITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
# SHALL THE AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
# SHALL THE AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
# IN THE SOFTWARE.
#

from boto.ec2.regioninfo import RegionInfo


def regions():
"""
Get all available regions for the Amazon DynamoDB service.
:rtype: list
:return: A list of :class:`boto.regioninfo.RegionInfo`
"""
import boto.dynamodb.layer2
return [RegionInfo(name='us-east-1',
endpoint='dynamodb.us-east-1.amazonaws.com',
connection_cls=boto.dynamodb.layer2.Layer2),
RegionInfo(name='us-west-1',
endpoint='dynamodb.us-west-1.amazonaws.com',
connection_cls=boto.dynamodb.layer2.Layer2),
RegionInfo(name='us-west-2',
endpoint='dynamodb.us-west-2.amazonaws.com',
connection_cls=boto.dynamodb.layer2.Layer2),
RegionInfo(name='ap-northeast-1',
endpoint='dynamodb.ap-northeast-1.amazonaws.com',
connection_cls=boto.dynamodb.layer2.Layer2),
RegionInfo(name='ap-southeast-1',
endpoint='dynamodb.ap-southeast-1.amazonaws.com',
connection_cls=boto.dynamodb.layer2.Layer2),
RegionInfo(name='eu-west-1',
endpoint='dynamodb.eu-west-1.amazonaws.com',
connection_cls=boto.dynamodb.layer2.Layer2),
]


def connect_to_region(region_name, **kw_params):
for region in regions():
if region.name == region_name:
return region.connect(**kw_params)
return None

@@ -15,14 +15,17 @@
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABIL-
# ITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
# SHALL THE AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
# SHALL THE AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
# IN THE SOFTWARE.
#


class Batch(object):
"""
Used to construct a BatchGet request.
:ivar table: The Table object from which the item is retrieved.
:ivar keys: A list of scalar or tuple values. Each element in the
@@ -31,8 +34,10 @@ class Batch(object):
list should be a tuple consisting of (hash_key, range_key). If
the schema for the table contains only a HashKey, each element
in the list should be a scalar value of the appropriate type
for the table schema.
for the table schema. NOTE: The maximum number of items that
can be retrieved for a single operation is 100. Also, the
number of items retrieved is constrained by a 1 MB size limit.
:ivar attributes_to_get: A list of attribute names.
If supplied, only the specified attribute names will
be returned. Otherwise, all attributes will be returned.
@@ -42,7 +47,74 @@ def __init__(self, table, keys, attributes_to_get=None):
self.table = table
self.keys = keys
self.attributes_to_get = attributes_to_get


def to_dict(self):
"""
Convert the Batch object into the format required for Layer1.
"""
batch_dict = {}
key_list = []
for key in self.keys:
if isinstance(key, tuple):
hash_key, range_key = key
else:
hash_key = key
range_key = None
k = self.table.layer2.build_key_from_values(self.table.schema,
hash_key, range_key)
key_list.append(k)
batch_dict['Keys'] = key_list
if self.attributes_to_get:
batch_dict['AttributesToGet'] = self.attributes_to_get
return batch_dict

class BatchWrite(object):
"""
Used to construct a BatchWrite request. Each BatchWrite object
represents a collection of PutItem and DeleteItem requests for
a single Table.
:ivar table: The Table object from which the item is retrieved.
:ivar puts: A list of :class:`boto.dynamodb.item.Item` objects
that you want to write to DynamoDB.
:ivar deletes: A list of scalar or tuple values. Each element in the
list represents one Item to delete. If the schema for the
table has both a HashKey and a RangeKey, each element in the
list should be a tuple consisting of (hash_key, range_key). If
the schema for the table contains only a HashKey, each element
in the list should be a scalar value of the appropriate type
for the table schema.
"""

def __init__(self, table, puts=None, deletes=None):
self.table = table
self.puts = puts or []
self.deletes = deletes or []

def to_dict(self):
"""
Convert the Batch object into the format required for Layer1.
"""
op_list = []
for item in self.puts:
d = {'Item': self.table.layer2.dynamize_item(item)}
d = {'PutRequest': d}
op_list.append(d)
for key in self.deletes:
if isinstance(key, tuple):
hash_key, range_key = key
else:
hash_key = key
range_key = None
k = self.table.layer2.build_key_from_values(self.table.schema,
hash_key, range_key)
d = {'Key': k}
op_list.append({'DeleteRequest': d})
return (self.table.name, op_list)


class BatchList(list):
"""
A subclass of a list object that contains a collection of
@@ -56,7 +128,7 @@ def __init__(self, layer2):
def add_batch(self, table, keys, attributes_to_get=None):
"""
Add a Batch to this BatchList.
:type table: :class:`boto.dynamodb.table.Table`
:param table: The Table object in which the items are contained.
@@ -67,8 +139,10 @@ def add_batch(self, table, keys, attributes_to_get=None):
list should be a tuple consisting of (hash_key, range_key). If
the schema for the table contains only a HashKey, each element
in the list should be a scalar value of the appropriate type
for the table schema.
for the table schema. NOTE: The maximum number of items that
can be retrieved for a single operation is 100. Also, the
number of items retrieved is constrained by a 1 MB size limit.
:type attributes_to_get: list
:param attributes_to_get: A list of attribute names.
If supplied, only the specified attribute names will
@@ -79,5 +153,57 @@ def add_batch(self, table, keys, attributes_to_get=None):
def submit(self):
return self.layer2.batch_get_item(self)


def to_dict(self):
"""
Convert a BatchList object into format required for Layer1.
"""
d = {}
for batch in self:
d[batch.table.name] = batch.to_dict()
return d

class BatchWriteList(list):
"""
A subclass of a list object that contains a collection of
:class:`boto.dynamodb.batch.BatchWrite` objects.
"""

def __init__(self, layer2):
list.__init__(self)
self.layer2 = layer2

def add_batch(self, table, puts=None, deletes=None):
"""
Add a BatchWrite to this BatchWriteList.
:type table: :class:`boto.dynamodb.table.Table`
:param table: The Table object in which the items are contained.
:type puts: list of :class:`boto.dynamodb.item.Item` objects
:param puts: A list of items that you want to write to DynamoDB.
:type deletes: A list
:param deletes: A list of scalar or tuple values. Each element
in the list represents one Item to delete. If the schema
for the table has both a HashKey and a RangeKey, each
element in the list should be a tuple consisting of
(hash_key, range_key). If the schema for the table
contains only a HashKey, each element in the list should
be a scalar value of the appropriate type for the table
schema.
"""
self.append(BatchWrite(table, puts, deletes))

def submit(self):
return self.layer2.batch_write_item(self)

def to_dict(self):
"""
Convert a BatchWriteList object into format required for Layer1.
"""
d = {}
for batch in self:
table_name, batch_dict = batch.to_dict()
d[table_name] = batch_dict
return d

@@ -15,13 +15,14 @@
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABIL-
# ITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
# SHALL THE AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
# SHALL THE AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
# IN THE SOFTWARE.
#

from boto.dynamodb.types import get_dynamodb_type, dynamize_value, convert_num
from boto.dynamodb.types import dynamize_value


class Condition(object):
"""
@@ -31,40 +32,43 @@ class Condition(object):

pass


class ConditionNoArgs(Condition):
"""
Abstract class for Conditions that require no arguments, such
as NULL or NOT_NULL.
"""

def __repr__(self):
return '%s' % self.__class__.__name__

def to_dict(self):
return {'ComparisonOperator': self.__class__.__name__}


class ConditionOneArg(Condition):
"""
Abstract class for Conditions that require a single argument
such as EQ or NE.
"""

def __init__(self, v1):
self.v1 = v1

def __repr__(self):
return '%s:%s' % (self.__class__.__name__, self.v1)

def to_dict(self):
return {'AttributeValueList': [dynamize_value(self.v1)],
'ComparisonOperator': self.__class__.__name__}


class ConditionTwoArgs(Condition):
"""
Abstract class for Conditions that require two arguments.
The only example of this currently is BETWEEN.
"""

def __init__(self, v1, v2):
self.v1 = v1
self.v2 = v2
@@ -76,64 +80,73 @@ def to_dict(self):
values = (self.v1, self.v2)
return {'AttributeValueList': [dynamize_value(v) for v in values],
'ComparisonOperator': self.__class__.__name__}



class EQ(ConditionOneArg):

pass



class NE(ConditionOneArg):

pass



class LE(ConditionOneArg):

pass



class LT(ConditionOneArg):

pass



class GE(ConditionOneArg):

pass



class GT(ConditionOneArg):

pass



class NULL(ConditionNoArgs):

pass



class NOT_NULL(ConditionNoArgs):

pass



class CONTAINS(ConditionOneArg):

pass



class NOT_CONTAINS(ConditionOneArg):

pass



class BEGINS_WITH(ConditionOneArg):

pass



class IN(ConditionOneArg):

pass



class BEGINS_WITH(ConditionOneArg):

pass

class BETWEEN(ConditionTwoArgs):

pass





class BETWEEN(ConditionTwoArgs):

pass
@@ -2,6 +2,7 @@
Exceptions that are specific to the dynamodb module.
"""
from boto.exception import BotoServerError, BotoClientError
from boto.exception import DynamoDBResponseError

class DynamoDBExpiredTokenError(BotoServerError):
"""
@@ -10,17 +11,35 @@ class DynamoDBExpiredTokenError(BotoServerError):
"""
pass


class DynamoDBKeyNotFoundError(BotoClientError):
"""
Raised when attempting to retrieve or interact with an item whose key
can't be found.
"""
pass


class DynamoDBItemError(BotoClientError):
"""
Raised when invalid parameters are passed when creating a
new Item in DynamoDB.
"""
pass


class DynamoDBConditionalCheckFailedError(DynamoDBResponseError):
"""
Raised when a ConditionalCheckFailedException response is received.
This happens when a conditional check, expressed via the expected_value
paramenter, fails.
"""
pass

class DynamoDBValidationError(DynamoDBResponseError):
"""
Raised when a ValidationException response is received. This happens
when one or more required parameter values are missing, or if the item
has exceeded the 64Kb size limit.
"""
pass
@@ -15,14 +15,15 @@
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABIL-
# ITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
# SHALL THE AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
# SHALL THE AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
# IN THE SOFTWARE.
#

from boto.dynamodb.exceptions import DynamoDBItemError


class Item(dict):
"""
An item in Amazon DynamoDB.
@@ -34,41 +35,39 @@ class Item(dict):
:ivar range_key_name: The name of the RangeKey associated with this item.
:ivar table: The Table this item belongs to.
"""

def __init__(self, table, hash_key=None, range_key=None, attrs=None):
self.table = table
self._updates = None
self._hash_key_name = self.table.schema.hash_key_name
self._range_key_name = self.table.schema.range_key_name
hash_key = hash_key or attrs.get(self._hash_key_name, None)
if hash_key is None:
raise DynamoDBItemError('You must supply a hash_key')
if self._range_key_name:
range_key = range_key or attrs.get(self._range_key_name, None)
if range_key is None:
raise DynamoDBItemError('You must supply a range_key')
if attrs == None:
attrs = {}
if hash_key == None:
hash_key = attrs.get(self._hash_key_name, None)
self[self._hash_key_name] = hash_key
if self._range_key_name:
if range_key == None:
range_key = attrs.get(self._range_key_name, None)
self[self._range_key_name] = range_key
if attrs:
for key, value in attrs.items():
if key != self._hash_key_name and key != self._range_key_name:
self[key] = value
for key, value in attrs.items():
if key != self._hash_key_name and key != self._range_key_name:
self[key] = value
self.consumed_units = 0
self._updates = {}

@property
def hash_key(self):
return self[self._hash_key_name]

@property
def range_key(self):
return self.get(self._range_key_name)

@property
def hash_key_name(self):
return self._hash_key_name

@property
def range_key_name(self):
return self._range_key_name
@@ -140,18 +139,18 @@ def save(self, expected_value=None, return_values=None):
"""
return self.table.layer2.update_item(self, expected_value,
return_values)

def delete(self, expected_value=None, return_values=None):
"""
Delete the item from DynamoDB.
:type expected_value: dict
:param expected_value: A dictionary of name/value pairs that you expect.
This dictionary should have name/value pairs where the name
is the name of the attribute and the value is either the value
you are expecting or False if you expect the attribute not to
exist.
:param expected_value: A dictionary of name/value pairs that
you expect. This dictionary should have name/value pairs
where the name is the name of the attribute and the value
is either the value you are expecting or False if you expect
the attribute not to exist.
:type return_values: str
:param return_values: Controls the return of attribute
name-value pairs before then were changed. Possible
@@ -168,11 +167,11 @@ def put(self, expected_value=None, return_values=None):
in Amazon DynamoDB.
:type expected_value: dict
:param expected_value: A dictionary of name/value pairs that you expect.
This dictionary should have name/value pairs where the name
is the name of the attribute and the value is either the value
you are expecting or False if you expect the attribute not to
exist.
:param expected_value: A dictionary of name/value pairs that
you expect. This dictionary should have name/value pairs
where the name is the name of the attribute and the value
is either the value you are expecting or False if you expect
the attribute not to exist.
:type return_values: str
:param return_values: Controls the return of attribute
@@ -15,7 +15,7 @@
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABIL-
# ITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
# SHALL THE AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
# SHALL THE AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
# IN THE SOFTWARE.
@@ -26,7 +26,6 @@
from boto.exception import DynamoDBResponseError
from boto.provider import Provider
from boto.dynamodb import exceptions as dynamodb_exceptions
from boto.dynamodb.table import Table

import time
try:
@@ -39,7 +38,8 @@
# value of Debug to be 2
#
#boto.set_stream_logger('dynamodb')
Debug=0
Debug = 0


class Layer1(AWSAuthConnection):
"""
@@ -54,13 +54,13 @@ class Layer1(AWSAuthConnection):
keeps a running total of the number of ThroughputExceeded
responses this connection has received from Amazon DynamoDB.
"""

DefaultRegionName = 'us-east-1'
"""The default region name for DynamoDB API."""

ServiceName = 'DynamoDB'
"""The name of the Service"""

Version = '20111205'
"""DynamoDB API version."""

@@ -69,12 +69,18 @@ class Layer1(AWSAuthConnection):

SessionExpiredError = 'com.amazon.coral.service#ExpiredTokenException'
"""The error response returned when session token has expired"""


ConditionalCheckFailedError = 'ConditionalCheckFailedException'
"""The error response returned when a conditional check fails"""

ValidationError = 'ValidationException'
"""The error response returned when an item is invalid in some way"""

ResponseError = DynamoDBResponseError

def __init__(self, aws_access_key_id=None, aws_secret_access_key=None,
is_secure=True, port=None, proxy=None, proxy_port=None,
host=None, debug=0, session_token=None, region=None):
debug=0, session_token=None, region=None):
if not region:
region_name = boto.config.get('DynamoDB', 'region',
self.DefaultRegionName)
@@ -106,7 +112,7 @@ def _update_provider(self):
self.creds.secret_key,
self.creds.session_token)
self._auth_handler.update_provider(self.provider)

def _get_session_token(self):
boto.log.debug('Creating new Session Token')
sts = boto.connect_sts(self._passed_access_key,
@@ -120,10 +126,11 @@ def make_request(self, action, body='', object_hook=None):
"""
:raises: ``DynamoDBExpiredTokenError`` if the security token expires.
"""
headers = {'X-Amz-Target' : '%s_%s.%s' % (self.ServiceName,
self.Version, action),
'Content-Type' : 'application/x-amz-json-1.0',
'Content-Length' : str(len(body))}
headers = {'X-Amz-Target': '%s_%s.%s' % (self.ServiceName,
self.Version, action),
'Host': self.region.endpoint,
'Content-Type': 'application/x-amz-json-1.0',
'Content-Length': str(len(body))}
http_request = self.build_base_http_request('POST', '/', '/',
{}, headers, body, None)
if self.do_instrumentation:
@@ -132,6 +139,7 @@ def make_request(self, action, body='', object_hook=None):
override_num_retries=10,
retry_handler=self._retry_handler)
self.request_id = response.getheader('x-amzn-RequestId')
boto.log.debug('RequestId: %s' % self.request_id)
if self.do_instrumentation:
self.instrumentation['times'].append(time.time() - start)
self.instrumentation['ids'].append(self.request_id)
@@ -151,14 +159,20 @@ def _retry_handler(self, response, i, next_sleep):
if i == 0:
next_sleep = 0
else:
next_sleep = 0.05 * (2**i)
next_sleep = 0.05 * (2 ** i)
i += 1
status = (msg, i, next_sleep)
elif self.SessionExpiredError in data.get('__type'):
msg = 'Renewing Session Token'
self.creds = self._get_session_token()
self._update_provider()
status = (msg, i+self.num_retries-1, next_sleep)
status = (msg, i + self.num_retries - 1, 0)
elif self.ConditionalCheckFailedError in data.get('__type'):
raise dynamodb_exceptions.DynamoDBConditionalCheckFailedError(
response.status, response.reason, data)
elif self.ValidationError in data.get('__type'):
raise dynamodb_exceptions.DynamoDBValidationError(
response.status, response.reason, data)
else:
raise self.ResponseError(response.status, response.reason,
data)
@@ -201,7 +215,7 @@ def describe_table(self, table_name):
:type table_name: str
:param table_name: The name of the table to describe.
"""
data = {'TableName' : table_name}
data = {'TableName': table_name}
json_input = json.dumps(data)
return self.make_request('DescribeTable', json_input)

@@ -215,7 +229,7 @@ def create_table(self, table_name, schema, provisioned_throughput):
:type table_name: str
:param table_name: The name of the table to create.
:type schema: dict
:param schema: A Python version of the KeySchema data structure
as defined by DynamoDB
@@ -224,10 +238,9 @@ def create_table(self, table_name, schema, provisioned_throughput):
:param provisioned_throughput: A Python version of the
ProvisionedThroughput data structure defined by
DynamoDB.
"""
data = {'TableName' : table_name,
'KeySchema' : schema,
data = {'TableName': table_name,
'KeySchema': schema,
'ProvisionedThroughput': provisioned_throughput}
json_input = json.dumps(data)
response_dict = self.make_request('CreateTable', json_input)
@@ -236,10 +249,10 @@ def create_table(self, table_name, schema, provisioned_throughput):
def update_table(self, table_name, provisioned_throughput):
"""
Updates the provisioned throughput for a given table.
:type table_name: str
:param table_name: The name of the table to update.
:type provisioned_throughput: dict
:param provisioned_throughput: A Python version of the
ProvisionedThroughput data structure defined by
@@ -295,12 +308,12 @@ def get_item(self, table_name, key, attributes_to_get=None,
json_input = json.dumps(data)
response = self.make_request('GetItem', json_input,
object_hook=object_hook)
if not response.has_key('Item'):
if 'Item' not in response:
raise dynamodb_exceptions.DynamoDBKeyNotFoundError(
"Key does not exist."
)
return response

def batch_get_item(self, request_items, object_hook=None):
"""
Return a set of attributes for a multiple items in
@@ -310,11 +323,25 @@ def batch_get_item(self, request_items, object_hook=None):
:param request_items: A Python version of the RequestItems
data structure defined by DynamoDB.
"""
data = {'RequestItems' : request_items}
data = {'RequestItems': request_items}
json_input = json.dumps(data)
return self.make_request('BatchGetItem', json_input,
object_hook=object_hook)

def batch_write_item(self, request_items, object_hook=None):
"""
This operation enables you to put or delete several items
across multiple tables in a single API call.
:type request_items: dict
:param request_items: A Python version of the RequestItems
data structure defined by DynamoDB.
"""
data = {'RequestItems': request_items}
json_input = json.dumps(data)
return self.make_request('BatchWriteItem', json_input,
object_hook=object_hook)

def put_item(self, table_name, item,
expected=None, return_values=None,
object_hook=None):
@@ -344,8 +371,8 @@ def put_item(self, table_name, item,
specified and the item is overwritten, the content
of the old item is returned.
"""
data = {'TableName' : table_name,
'Item' : item}
data = {'TableName': table_name,
'Item': item}
if expected:
data['Expected'] = expected
if return_values:
@@ -385,8 +412,8 @@ def update_item(self, table_name, key, attribute_updates,
specified and the item is overwritten, the content
of the old item is returned.
"""
data = {'TableName' : table_name,
'Key' : key,
data = {'TableName': table_name,
'Key': key,
'AttributeUpdates': attribute_updates}
if expected:
data['Expected'] = expected
@@ -422,8 +449,8 @@ def delete_item(self, table_name, key,
specified and the item is overwritten, the content
of the old item is returned.
"""
data = {'TableName' : table_name,
'Key' : key}
data = {'TableName': table_name,
'Key': key}
if expected:
data['Expected'] = expected
if return_values:
@@ -540,5 +567,3 @@ def scan(self, table_name, scan_filter=None,
data['ExclusiveStartKey'] = exclusive_start_key
json_input = json.dumps(data)
return self.make_request('Scan', json_input, object_hook=object_hook)


Large diffs are not rendered by default.

@@ -15,12 +15,13 @@
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABIL-
# ITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
# SHALL THE AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
# SHALL THE AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
# IN THE SOFTWARE.
#


class Schema(object):
"""
Represents a DynamoDB schema.
@@ -49,26 +50,25 @@ def __repr__(self):
@property
def dict(self):
return self._dict

@property
def hash_key_name(self):
return self._dict['HashKeyElement']['AttributeName']

@property
def hash_key_type(self):
return self._dict['HashKeyElement']['AttributeType']

@property
def range_key_name(self):
name = None
if 'RangeKeyElement' in self._dict:
name = self._dict['RangeKeyElement']['AttributeName']
return name

@property
def range_key_type(self):
type = None
if 'RangeKeyElement' in self._dict:
type = self._dict['RangeKeyElement']['AttributeType']
return type

@@ -15,7 +15,7 @@
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABIL-
# ITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
# SHALL THE AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
# SHALL THE AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
# IN THE SOFTWARE.
@@ -26,6 +26,7 @@
from boto.dynamodb import exceptions as dynamodb_exceptions
import time


class Table(object):
"""
An Amazon DynamoDB table.
@@ -60,35 +61,35 @@ def __repr__(self):
@property
def name(self):
return self._dict['TableName']

@property
def create_time(self):
return self._dict['CreationDateTime']

@property
def status(self):
return self._dict['TableStatus']

@property
def item_count(self):
return self._dict.get('ItemCount', 0)

@property
def size_bytes(self):
return self._dict.get('TableSizeBytes', 0)

@property
def schema(self):
return self._schema

@property
def read_units(self):
return self._dict['ProvisionedThroughput']['ReadCapacityUnits']

@property
def write_units(self):
return self._dict['ProvisionedThroughput']['WriteCapacityUnits']

def update_from_response(self, response):
"""
Update the state of the Table object based on the response
@@ -134,12 +135,12 @@ def update_throughput(self, read_units, write_units):
:type read_units: int
:param read_units: The new value for ReadCapacityUnits.
:type write_units: int
:param write_units: The new value for WriteCapacityUnits.
"""
self.layer2.update_throughput(self, read_units, write_units)

def delete(self):
"""
Delete this table and all items in it. After calling this
@@ -157,12 +158,12 @@ def get_item(self, hash_key, range_key=None,
:param hash_key: The HashKey of the requested item. The
type of the value must match the type defined in the
schema for the table.
:type range_key: int|long|float|str|unicode
:param range_key: The optional RangeKey of the requested item.
The type of the value must match the type defined in the
schema for the table.
:type attributes_to_get: list
:param attributes_to_get: A list of attribute names.
If supplied, only the specified attribute names will
@@ -182,7 +183,7 @@ def get_item(self, hash_key, range_key=None,
attributes_to_get, consistent_read,
item_class)
lookup = get_item

def has_item(self, hash_key, range_key=None, consistent_read=False):
"""
Checks the table to see if the Item with the specified ``hash_key``
@@ -231,7 +232,8 @@ def new_item(self, hash_key=None, range_key=None, attrs=None,
the hash_key and range_key values of the item. You can use
these explicit parameters when calling the method, such as::
>>> my_item = my_table.new_item(hash_key='a', range_key=1, attrs={'key1': 'val1', 'key2': 'val2'})
>>> my_item = my_table.new_item(hash_key='a', range_key=1,
attrs={'key1': 'val1', 'key2': 'val2'})
>>> my_item
{u'bar': 1, u'foo': 'a', 'key1': 'val1', 'key2': 'val2'}
@@ -256,7 +258,7 @@ def new_item(self, hash_key=None, range_key=None, attrs=None,
:param hash_key: The HashKey of the new item. The
type of the value must match the type defined in the
schema for the table.
:type range_key: int|long|float|str|unicode
:param range_key: The optional RangeKey of the new item.
The type of the value must match the type defined in the
@@ -265,12 +267,11 @@ def new_item(self, hash_key=None, range_key=None, attrs=None,
:type attrs: dict
:param attrs: A dictionary of key value pairs used to
populate the new item.
:type item_class: Class
:param item_class: Allows you to override the class used
to generate the items. This should be a subclass of
:class:`boto.dynamodb.item.Item`
"""
return item_class(self, hash_key, range_key, attrs)

@@ -281,25 +282,22 @@ def query(self, hash_key, range_key_condition=None,
item_class=Item):
"""
Perform a query on the table.
:type hash_key: int|long|float|str|unicode
:param hash_key: The HashKey of the requested item. The
type of the value must match the type defined in the
schema for the table.
:type range_key_condition: dict
:param range_key_condition: A dict where the key is either
a scalar value appropriate for the RangeKey in the schema
of the database or a tuple of such values. The value
associated with this key in the dict will be one of the
following conditions:
:type range_key_condition: :class:`boto.dynamodb.condition.Condition`
:param range_key_condition: A Condition object.
Condition object can be one of the following types:
EQ|LE|LT|GE|GT|BEGINS_WITH|BETWEEN
'EQ'|'LE'|'LT'|'GE'|'GT'|'BEGINS_WITH'|'BETWEEN'
The only condition which expects or will accept two
values is 'BETWEEN', otherwise a single value should
be passed to the Condition constructor.
The only condition which expects or will accept a tuple
of values is 'BETWEEN', otherwise a scalar value should
be used as the key in the dict.
:type attributes_to_get: list
:param attributes_to_get: A list of attribute names.
If supplied, only the specified attribute names will
@@ -15,7 +15,7 @@
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABIL-
# ITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
# SHALL THE AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
# SHALL THE AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
# IN THE SOFTWARE.
@@ -25,11 +25,15 @@
Python types and vice-versa.
"""


def is_num(n):
return isinstance(n, (int, long, float, bool))
types = (int, long, float, bool)
return isinstance(n, types) or n in types


def is_str(n):
return isinstance(n, basestring)
return isinstance(n, basestring) or (isinstance(n, type) and issubclass(n, basestring))


def convert_num(s):
if '.' in s:
@@ -38,6 +42,7 @@ def convert_num(s):
n = int(s)
return n


def get_dynamodb_type(val):
"""
Take a scalar Python value and return a string representing
@@ -55,9 +60,11 @@ def get_dynamodb_type(val):
elif False not in map(is_str, val):
dynamodb_type = 'SS'
if dynamodb_type is None:
raise TypeError('Unsupported type "%s" for value "%s"' % (type(val), val))
msg = 'Unsupported type "%s" for value "%s"' % (type(val), val)
raise TypeError(msg)
return dynamodb_type


def dynamize_value(val):
"""
Take a scalar Python value and return a dict consisting
@@ -77,12 +84,11 @@ def _str(val):

dynamodb_type = get_dynamodb_type(val)
if dynamodb_type == 'N':
val = {dynamodb_type : _str(val)}
val = {dynamodb_type: _str(val)}
elif dynamodb_type == 'S':
val = {dynamodb_type : val}
val = {dynamodb_type: val}
elif dynamodb_type == 'NS':
val = {dynamodb_type : [ str(n) for n in val]}
val = {dynamodb_type: [str(n) for n in val]}
elif dynamodb_type == 'SS':
val = {dynamodb_type : [ n for n in val]}
val = {dynamodb_type: [n for n in val]}
return val

@@ -173,7 +173,10 @@ def endElement(self, name, value, connection):
elif name == 'RamdiskId':
self.ramdisk_id = value
elif name == 'UserData':
self.user_data = base64.b64decode(value)
try:
self.user_data = base64.b64decode(value)
except TypeError:
self.user_data = value
elif name == 'LaunchConfigurationARN':
self.launch_configuration_arn = value
elif name == 'InstanceMonitoring':
@@ -113,17 +113,20 @@ def _required_auth_capability(self):

def build_dimension_param(self, dimension, params):
prefix = 'Dimensions.member'
for i, dim_name in enumerate(dimension):
i=0
for dim_name in dimension:
dim_value = dimension[dim_name]
if dim_value:
if isinstance(dim_value, basestring):
dim_value = [dim_value]
for j, value in enumerate(dim_value):
params['%s.%d.Name.%d' % (prefix, i+1, j+1)] = dim_name
params['%s.%d.Value.%d' % (prefix, i+1, j+1)] = value
for value in dim_value:
params['%s.%d.Name' % (prefix, i+1)] = dim_name
params['%s.%d.Value' % (prefix, i+1)] = value
i += 1
else:
params['%s.%d.Name' % (prefix, i+1)] = dim_name

i += 1

def build_list_params(self, params, items, label):
if isinstance(items, basestring):
items = [items]
@@ -139,7 +142,7 @@ def build_list_params(self, params, items, label):

def build_put_params(self, params, name, value=None, timestamp=None,
unit=None, dimensions=None, statistics=None):
args = (name, value, unit, dimensions, statistics)
args = (name, value, unit, dimensions, statistics, timestamp)
length = max(map(lambda a: len(a) if isinstance(a, list) else 1, args))

def aslist(a):
@@ -149,11 +152,11 @@ def aslist(a):
return a
return [a] * length

for index, (n, v, u, d, s) in enumerate(zip(*map(aslist, args))):
for index, (n, v, u, d, s, t) in enumerate(zip(*map(aslist, args))):
metric_data = {'MetricName': n}

if timestamp:
metric_data['Timestamp'] = timestamp.isoformat()
metric_data['Timestamp'] = t.isoformat()

if unit:
metric_data['Unit'] = u
@@ -320,7 +323,7 @@ def put_metric_data(self, namespace, name, value=None, timestamp=None,
self.build_put_params(params, name, value=value, timestamp=timestamp,
unit=unit, dimensions=dimensions, statistics=statistics)

return self.get_status('PutMetricData', params)
return self.get_status('PutMetricData', params, verb="POST")


def describe_alarms(self, action_prefix=None, alarm_name_prefix=None,
@@ -519,7 +519,8 @@ def run_instances(self, image_id, min_count=1, max_count=1,
instance_initiated_shutdown_behavior=None,
private_ip_address=None,
placement_group=None, client_token=None,
security_group_ids=None):
security_group_ids=None,
additional_info=None):
"""
Runs an image on EC2.
@@ -610,6 +611,10 @@ def run_instances(self, image_id, min_count=1, max_count=1,
to ensure idempotency of the request.
Maximum 64 ASCII characters
:type additional_info: string
:param additional_info: Specifies additional information to make
available to the instance(s)
:rtype: Reservation
:return: The :class:`boto.ec2.instance.Reservation` associated with
the request for machines
@@ -668,6 +673,8 @@ def run_instances(self, image_id, min_count=1, max_count=1,
params['InstanceInitiatedShutdownBehavior'] = val
if client_token:
params['ClientToken'] = client_token
if additional_info:
params['AdditionalInfo'] = additional_info
return self.get_object('RunInstances', params, Reservation, verb='POST')

def terminate_instances(self, instance_ids=None):
@@ -1212,7 +1219,8 @@ def allocate_address(self, domain=None):

return self.get_object('AllocateAddress', params, Address, verb='POST')

def associate_address(self, instance_id, public_ip=None, allocation_id=None):
def associate_address(self, instance_id=None, public_ip=None,
allocation_id=None, network_interface_id=None):
"""
Associate an Elastic IP address with a currently running instance.
This requires one of ``public_ip`` or ``allocation_id`` depending
@@ -1227,10 +1235,18 @@ def associate_address(self, instance_id, public_ip=None, allocation_id=None):
:type allocation_id: string
:param allocation_id: The allocation ID for a VPC-based elastic IP.
:type network_interface_id: string
: param network_interface_id: The network interface ID to which
elastic IP is to be assigned to
:rtype: bool
:return: True if successful
"""
params = { 'InstanceId' : instance_id }
params = {}
if instance_id is not None:
params['InstanceId'] = instance_id
elif network_interface_id is not None:
params['NetworkInterfaceId'] = network_interface_id

if public_ip is not None:
params['PublicIp'] = public_ip
@@ -1993,6 +2009,8 @@ def create_security_group(self, name, description, vpc_id=None):
SecurityGroup, verb='POST')
group.name = name
group.description = description
if vpc_id is not None:
group.vpc_id = vpc_id
return group

def delete_security_group(self, name=None, group_id=None):
@@ -2031,15 +2049,15 @@ def authorize_security_group_deprecated(self, group_name,
:type group_name: string
:param group_name: The name of the security group you are adding
the rule to.
the rule to.
:type src_security_group_name: string
:param src_security_group_name: The name of the security group you are
granting access to.
granting access to.
:type src_security_group_owner_id: string
:param src_security_group_owner_id: The ID of the owner of the security
group you are granting access to.
group you are granting access to.
:type ip_protocol: string
:param ip_protocol: Either tcp | udp | icmp
@@ -2052,7 +2070,7 @@ def authorize_security_group_deprecated(self, group_name,
:type to_port: string
:param to_port: The CIDR block you are providing access to.
See http://goo.gl/Yj5QC
See http://goo.gl/Yj5QC
:rtype: bool
:return: True if successful.
@@ -2087,15 +2105,15 @@ def authorize_security_group(self, group_name=None,
:type group_name: string
:param group_name: The name of the security group you are adding
the rule to.
the rule to.
:type src_security_group_name: string
:param src_security_group_name: The name of the security group you are
granting access to.
granting access to.
:type src_security_group_owner_id: string
:param src_security_group_owner_id: The ID of the owner of the security
group you are granting access to.
group you are granting access to.
:type ip_protocol: string
:param ip_protocol: Either tcp | udp | icmp
@@ -2108,19 +2126,17 @@ def authorize_security_group(self, group_name=None,
:type cidr_ip: string or list of strings
:param cidr_ip: The CIDR block you are providing access to.
See http://goo.gl/Yj5QC
See http://goo.gl/Yj5QC
:type group_id: string
:param group_id: ID of the EC2 or VPC security group to modify.
This is required for VPC security groups and
can be used instead of group_name for EC2
security groups.
:param group_id: ID of the EC2 or VPC security group to
modify. This is required for VPC security groups and can
be used instead of group_name for EC2 security groups.
:type group_id: string
:param group_id: ID of the EC2 or VPC source security group.
This is required for VPC security groups and
can be used instead of group_name for EC2
security groups.
:type src_security_group_group_id: string
:param src_security_group_group_id: The ID of the security
group you are granting access to. Can be used instead of
src_security_group_name
:rtype: bool
:return: True if successful.
@@ -2235,18 +2251,6 @@ def revoke_security_group_deprecated(self, group_name,
:param to_port: The CIDR block you are revoking access to.
http://goo.gl/Yj5QC
:type group_id: string
:param group_id: ID of the EC2 or VPC security group to modify.
This is required for VPC security groups and
can be used instead of group_name for EC2
security groups.
:type group_id: string
:param group_id: ID of the EC2 or VPC source security group.
This is required for VPC security groups and
can be used instead of group_name for EC2
security groups.
:rtype: bool
:return: True if successful.
"""
@@ -2302,6 +2306,17 @@ def revoke_security_group(self, group_name=None, src_security_group_name=None,
:param cidr_ip: The CIDR block you are revoking access to.
See http://goo.gl/Yj5QC
:type group_id: string
:param group_id: ID of the EC2 or VPC security group to modify.
This is required for VPC security groups and
can be used instead of group_name for EC2
security groups.
:type src_security_group_group_id: string
:param src_security_group_group_id: The ID of the security group
for which you are revoking access.
Can be used instead of src_security_group_name
:rtype: bool
:return: True if successful.
"""
@@ -2918,7 +2933,7 @@ def attach_network_interface(self, network_interface_id,
"""
params = {'NetworkInterfaceId' : network_interface_id,
'InstanceId' : instance_id,
'Deviceindex' : device_index}
'DeviceIndex' : device_index}
return self.get_status('AttachNetworkInterface', params, verb='POST')

def detach_network_interface(self, network_interface_id, force=False):
@@ -157,10 +157,11 @@ def create_load_balancer(self, name, zones, listeners, subnets=None,
params = {'LoadBalancerName' : name}
for index, listener in enumerate(listeners):
i = index + 1
protocol = listener[2].upper()
params['Listeners.member.%d.LoadBalancerPort' % i] = listener[0]
params['Listeners.member.%d.InstancePort' % i] = listener[1]
params['Listeners.member.%d.Protocol' % i] = listener[2]
if listener[2]=='HTTPS':
if protocol == 'HTTPS' or protocol == 'SSL':
params['Listeners.member.%d.SSLCertificateId' % i] = listener[3]
if zones:
self.build_list_params(params, zones, 'AvailabilityZones.member.%d')
@@ -169,7 +170,7 @@ def create_load_balancer(self, name, zones, listeners, subnets=None,
self.build_list_params(params, subnets, 'Subnets.member.%d')

if security_groups:
self.build_list_params(params, security_groups,
self.build_list_params(params, security_groups,
'SecurityGroups.member.%d')

load_balancer = self.get_object('CreateLoadBalancer',
@@ -194,19 +195,20 @@ def create_load_balancer_listeners(self, name, listeners):
[SSLCertificateId])
where LoadBalancerPortNumber and InstancePortNumber are
integer values between 1 and 65535, Protocol is a
string containing either 'TCP', 'HTTP' or 'HTTPS';
string containing either 'TCP', 'HTTP', 'HTTPS', or 'SSL';
SSLCertificateID is the ARN of a AWS AIM certificate,
and must be specified when doing HTTPS.
and must be specified when doing HTTPS or SSL.
:return: The status of the request
"""
params = {'LoadBalancerName' : name}
for index, listener in enumerate(listeners):
i = index + 1
protocol = listener[2].upper()
params['Listeners.member.%d.LoadBalancerPort' % i] = listener[0]
params['Listeners.member.%d.InstancePort' % i] = listener[1]
params['Listeners.member.%d.Protocol' % i] = listener[2]
if listener[2]=='HTTPS':
if protocol == 'HTTPS' or protocol == 'SSL':
params['Listeners.member.%d.SSLCertificateId' % i] = listener[3]
return self.get_status('CreateLoadBalancerListeners', params)

@@ -461,7 +463,7 @@ def set_lb_policies_of_listener(self, lb_name, lb_port, policies):
def apply_security_groups_to_lb(self, name, security_groups):
"""
Applies security groups to the load balancer.
Applying security groups that are already registered with the
Applying security groups that are already registered with the
Load Balancer has no effect.
:type name: string
@@ -475,16 +477,16 @@ def apply_security_groups_to_lb(self, name, security_groups):
"""
params = {'LoadBalancerName' : name}
self.build_list_params(params, security_groups,
self.build_list_params(params, security_groups,
'SecurityGroups.member.%d')
return self.get_list('ApplySecurityGroupsToLoadBalancer',
return self.get_list('ApplySecurityGroupsToLoadBalancer',
params,
None)

def attach_lb_to_subnets(self, name, subnets):
"""
Attaches load balancer to one or more subnets.
Attaching subnets that are already registered with the
Attaching subnets that are already registered with the
Load Balancer has no effect.
:type name: string
@@ -498,9 +500,9 @@ def attach_lb_to_subnets(self, name, subnets):
"""
params = {'LoadBalancerName' : name}
self.build_list_params(params, subnets,
self.build_list_params(params, subnets,
'Subnets.member.%d')
return self.get_list('AttachLoadBalancerToSubnets',
return self.get_list('AttachLoadBalancerToSubnets',
params,
None)

@@ -519,10 +521,8 @@ def detach_lb_from_subnets(self, name, subnets):
"""
params = {'LoadBalancerName' : name}
self.build_list_params(params, subnets,
self.build_list_params(params, subnets,
'Subnets.member.%d')
return self.get_list('DettachLoadBalancerFromSubnets',
return self.get_list('DettachLoadBalancerFromSubnets',
params,
None)


@@ -160,7 +160,8 @@ def run(self, min_count=1, max_count=1, key_name=None,
disable_api_termination=False,
instance_initiated_shutdown_behavior=None,
private_ip_address=None,
placement_group=None, security_group_ids=None):
placement_group=None, security_group_ids=None,
additional_info=None):
"""
Runs this instance.
@@ -229,11 +230,16 @@ def run(self, min_count=1, max_count=1, key_name=None,
:param placement_group: If specified, this is the name of the placement
group in which the instance(s) will be launched.
:rtype: Reservation
:return: The :class:`boto.ec2.instance.Reservation` associated with the request for machines
:type additional_info: string
:param additional_info: Specifies additional information to make
available to the instance(s)
:type security_group_ids:
:param security_group_ids:
:rtype: Reservation
:return: The :class:`boto.ec2.instance.Reservation` associated with the request for machines
"""

return self.connection.run_instances(self.id, min_count, max_count,
@@ -245,7 +251,8 @@ def run(self, min_count=1, max_count=1, key_name=None,
block_device_map, disable_api_termination,
instance_initiated_shutdown_behavior,
private_ip_address, placement_group,
security_group_ids=security_group_ids)
security_group_ids=security_group_ids,
additional_info=additional_info)

def deregister(self, delete_snapshot=False):
return self.connection.deregister_image(self.id, delete_snapshot)
@@ -111,6 +111,9 @@ def __repr__(self):
return 'NetworkInterface:%s' % self.id

def startElement(self, name, attrs, connection):
retval = TaggedEC2Object.startElement(self, name, attrs, connection)
if retval is not None:
return retval
if name == 'groupSet':
self.groups = ResultSet([('item', Group)])
return self.groups
@@ -82,10 +82,13 @@ def endElement(self, name, value, connection):
setattr(self, name, value)

def delete(self):
return self.connection.delete_security_group(self.name)
if self.vpc_id:
return self.connection.delete_security_group(group_id=self.id)
else:
return self.connection.delete_security_group(self.name)

def add_rule(self, ip_protocol, from_port, to_port,
src_group_name, src_group_owner_id, cidr_ip):
src_group_name, src_group_owner_id, cidr_ip, src_group_group_id):
"""
Add a rule to the SecurityGroup object. Note that this method
only changes the local version of the object. No information
@@ -96,10 +99,10 @@ def add_rule(self, ip_protocol, from_port, to_port,
rule.from_port = from_port
rule.to_port = to_port
self.rules.append(rule)
rule.add_grant(src_group_name, src_group_owner_id, cidr_ip)
rule.add_grant(src_group_name, src_group_owner_id, cidr_ip, src_group_group_id)

def remove_rule(self, ip_protocol, from_port, to_port,
src_group_name, src_group_owner_id, cidr_ip):
src_group_name, src_group_owner_id, cidr_ip, src_group_group_id):
"""
Remove a rule to the SecurityGroup object. Note that this method
only changes the local version of the object. No information
@@ -113,7 +116,7 @@ def remove_rule(self, ip_protocol, from_port, to_port,
target_rule = rule
target_grant = None
for grant in rule.grants:
if grant.name == src_group_name:
if grant.name == src_group_name or grant.group_id == src_group_group_id:
if grant.owner_id == src_group_owner_id:
if grant.cidr_ip == cidr_ip:
target_grant = grant
@@ -151,48 +154,75 @@ def authorize(self, ip_protocol=None, from_port=None, to_port=None,
:rtype: bool
:return: True if successful.
"""
group_name = None
if not self.vpc_id:
group_name = self.name
group_id = None
if self.vpc_id:
group_id = self.id
src_group_name = None
src_group_owner_id = None
src_group_group_id = None
if src_group:
cidr_ip = None
src_group_name = src_group.name
src_group_owner_id = src_group.owner_id
else:
src_group_name = None
src_group_owner_id = None
status = self.connection.authorize_security_group(self.name,
if not self.vpc_id:
src_group_name = src_group.name
else:
if hasattr(src_group, 'group_id'):
src_group_group_id = src_group.group_id
else:
src_group_group_id = src_group.id
status = self.connection.authorize_security_group(group_name,
src_group_name,
src_group_owner_id,
ip_protocol,
from_port,
to_port,
cidr_ip)
cidr_ip,
group_id,
src_group_group_id)
if status:
if type(cidr_ip) != list:
cidr_ip = [cidr_ip]
for single_cidr_ip in cidr_ip:
self.add_rule(ip_protocol, from_port, to_port, src_group_name,
src_group_owner_id, single_cidr_ip)

src_group_owner_id, single_cidr_ip, src_group_group_id)
return status

def revoke(self, ip_protocol=None, from_port=None, to_port=None,
cidr_ip=None, src_group=None):
group_name = None
if not self.vpc_id:
group_name = self.name
group_id = None
if self.vpc_id:
group_id = self.id
src_group_name = None
src_group_owner_id = None
src_group_group_id = None
if src_group:
cidr_ip=None
src_group_name = src_group.name
cidr_ip = None
src_group_owner_id = src_group.owner_id
else:
src_group_name = None
src_group_owner_id = None
status = self.connection.revoke_security_group(self.name,
if not self.vpc_id:
src_group_name = src_group.name
else:
if hasattr(src_group, 'group_id'):
src_group_group_id = src_group.group_id
else:
src_group_group_id = src_group.id
status = self.connection.revoke_security_group(group_name,
src_group_name,
src_group_owner_id,
ip_protocol,
from_port,
to_port,
cidr_ip)
cidr_ip,
group_id,
src_group_group_id)
if status:
self.remove_rule(ip_protocol, from_port, to_port, src_group_name,
src_group_owner_id, cidr_ip)
src_group_owner_id, cidr_ip, src_group_group_id)
return status

def copy_to_region(self, region, name=None):
@@ -220,9 +250,10 @@ def copy_to_region(self, region, name=None):
source_groups = []
for rule in self.rules:
for grant in rule.grants:
if grant.name:
if grant.name not in source_groups:
source_groups.append(grant.name)
grant_nom = grant.name or grant.group_id
if grant_nom:
if grant_nom not in source_groups:
source_groups.append(grant_nom)
sg.authorize(None, None, None, None, grant)
else:
sg.authorize(rule.ip_protocol, rule.from_port, rule.to_port,
@@ -287,9 +318,10 @@ def endElement(self, name, value, connection):
else:
setattr(self, name, value)

def add_grant(self, name=None, owner_id=None, cidr_ip=None):
def add_grant(self, name=None, owner_id=None, cidr_ip=None, group_id=None):
grant = GroupOrCIDR(self)
grant.owner_id = owner_id
grant.group_id = group_id
grant.name = name
grant.cidr_ip = cidr_ip
self.grants.append(grant)
@@ -299,21 +331,24 @@ class GroupOrCIDR(object):

def __init__(self, parent=None):
self.owner_id = None
self.group_id = None
self.name = None
self.cidr_ip = None

def __repr__(self):
if self.cidr_ip:
return '%s' % self.cidr_ip
else:
return '%s-%s' % (self.name, self.owner_id)
return '%s-%s' % (self.name or self.group_id, self.owner_id)

def startElement(self, name, attrs, connection):
return None

def endElement(self, name, value, connection):
if name == 'userId':
self.owner_id = value
elif name == 'groupId':
self.group_id = value
elif name == 'groupName':
self.name = value
if name == 'cidrIp':
@@ -37,6 +37,7 @@ def __init__(self, connection=None):
self.progress = None
self.start_time = None
self.owner_id = None
self.owner_alias = None
self.volume_size = None
self.description = None

@@ -54,6 +55,8 @@ def endElement(self, name, value, connection):
self.start_time = value
elif name == 'ownerId':
self.owner_id = value
elif name == 'ownerAlias':
self.owner_alias = value
elif name == 'volumeSize':
try:
self.volume_size = int(value)
@@ -15,7 +15,7 @@
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABIL-
# ITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
# SHALL THE AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
# SHALL THE AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
# IN THE SOFTWARE.
@@ -27,6 +27,7 @@
from boto.ec2.ec2object import TaggedEC2Object
from boto.ec2.launchspecification import LaunchSpecification


class SpotInstanceStateFault(object):

def __init__(self, code=None, message=None):
@@ -46,8 +47,9 @@ def endElement(self, name, value, connection):
self.message = value
setattr(self, name, value)


class SpotInstanceRequest(TaggedEC2Object):

def __init__(self, connection=None):
TaggedEC2Object.__init__(self, connection)
self.id = None
@@ -58,6 +60,7 @@ def __init__(self, connection=None):
self.valid_from = None
self.valid_until = None
self.launch_group = None
self.launched_availability_zone = None
self.product_description = None
self.availability_zone_group = None
self.create_time = None
@@ -89,8 +92,6 @@ def endElement(self, name, value, connection):
self.type = value
elif name == 'state':
self.state = value
elif name == 'productDescription':
self.product_description = value
elif name == 'validFrom':
self.valid_from = value
elif name == 'validUntil':
@@ -99,15 +100,16 @@ def endElement(self, name, value, connection):
self.launch_group = value
elif name == 'availabilityZoneGroup':
self.availability_zone_group = value
elif name == 'createTime':
self.create_time = value
elif name == 'launchedAvailabilityZone':
self.launched_availability_zone = value
elif name == 'instanceId':
self.instance_id = value
elif name == 'createTime':
self.create_time = value
elif name == 'productDescription':
self.product_description = value
else:
setattr(self, name, value)

def cancel(self):
self.connection.cancel_spot_instance_requests([self.id])



@@ -197,7 +197,7 @@ def modify_instance_groups(self, instance_group_ids, new_sizes):
return self.get_object('ModifyInstanceGroups', params,
ModifyInstanceGroupsResponse, verb='POST')

def run_jobflow(self, name, log_uri, ec2_keyname=None,
def run_jobflow(self, name, log_uri=None, ec2_keyname=None,
availability_zone=None,
master_instance_type='m1.small',
slave_instance_type='m1.small', num_instances=1,
@@ -282,8 +282,9 @@ def run_jobflow(self, name, log_uri, ec2_keyname=None,
params = {}
if action_on_failure:
params['ActionOnFailure'] = action_on_failure
if log_uri:
params['LogUri'] = log_uri
params['Name'] = name
params['LogUri'] = log_uri

# Common instance args
common_params = self._build_instance_common_args(ec2_keyname,