diff --git a/README.md b/README.md index 2f4285f..cfafec0 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ DynamoDB is not like other document-based databases you might know, and is very ``` * boto3 +* six * pytz * dateutil * simplejson @@ -25,7 +26,7 @@ pip install git+https://github.com/gusibi/dynamodb-py.git@master ynamodb-py has some sensible defaults for you when you create a new table, including the table name and the primary key column. But you can change those if you like on table creation. -``` +```python from dynamodb.model import Model from dynamodb.fields import CharField, IntegerField, FloatField, DictField from dynamodb.table import Table @@ -54,6 +55,36 @@ Table(Movies()).update() Table(Movies()).delete() ``` +### Global Secondary Indexes Example + +```python +from dynamodb.model import Model +from dynamodb.fields import CharField, IntegerField, FloatField, DictField +from dynamodb.table import Table + +class GameScores(Model): + + __table_name__ = 'GameScores' + + ReadCapacityUnits = 10 + WriteCapacityUnits = 10 + + __global_indexes__ = [ + ('game_scores-index', ('title', 'top_score'), ['title', 'top_score', 'user_id']), + ] + + user_id = IntegerField(name='user_id', hash_key=True) + title = CharField(name='title', range_key=True) + top_score = FloatField(name='top_score', indexed=True) + top_score_date = CharField(name='top_score_date') + wins = IntegerField(name='wins', indexed=True) + losses = IntegerField(name='losses', indexed=True) + + +# query use global secondary indexes +GameScores.global_query(index_name='game_scores-index').where(GameScores.title.eq("Puzzle Battle")).order_by(GameScores.top_score, asc=False).all() +``` + ## Fields You'll have to define all the fields on the model and the data type of each field. Every field on the object must be included here; if you miss any they'll be completely bypassed during DynamoDB's initialization and will not appear on the model objects. diff --git a/dynamodb/errors.py b/dynamodb/errors.py index eef41bc..e2592e2 100644 --- a/dynamodb/errors.py +++ b/dynamodb/errors.py @@ -24,6 +24,10 @@ class ParameterException(Exception): pass +class GlobalSecondaryIndexesException(Exception): + pass + + class FieldValidationException(Exception): def __init__(self, errors, *args, **kwargs): diff --git a/dynamodb/expression.py b/dynamodb/expression.py index ee6bc88..5fda395 100644 --- a/dynamodb/expression.py +++ b/dynamodb/expression.py @@ -10,7 +10,7 @@ from boto3.dynamodb.conditions import Key, Attr from .errors import ValidationException -from .helpers import smart_unicode +from .helpers import smart_text __all__ = ['Expression'] @@ -145,24 +145,26 @@ def add(self, value, path=None, attr_label=None): return exp, exp_attr, 'ADD' def typecast_for_storage(self, value): - return smart_unicode(value) + return smart_text(value) def _expression_func(self, op, *values, **kwargs): + # print(op, values, kwargs) # for use by index ... bad - values = map(self.typecast_for_storage, values) + # list 是为了兼容py3 python3 中 map 返回的是class map + values = list(map(self.typecast_for_storage, values)) self.op = op self.express_args = values - use_key = kwargs.get('use_key', False) + is_key = kwargs.get('is_key', False) if self.hash_key and op != 'eq': raise ValidationException('Query key condition not supported') - elif self.hash_key or self.range_key or use_key: - use_key = True + elif self.hash_key or self.range_key or is_key: + is_key = True func = getattr(Key(self.name), op, None) else: func = getattr(Attr(self.name), op, None) if not func: raise ValidationException('Query key condition not supported') - return self, func(*values), use_key + return self, func(*values), is_key def _expression(self, op, value): if self.use_decimal_types: diff --git a/dynamodb/fields.py b/dynamodb/fields.py index 9bd7a09..b3b3b44 100644 --- a/dynamodb/fields.py +++ b/dynamodb/fields.py @@ -9,9 +9,11 @@ import decimal from datetime import datetime, date, timedelta +import six + from .json_import import json from .errors import FieldValidationException -from .helpers import str_time, str_to_time, date2timestamp, timestamp2date, smart_unicode +from .helpers import str_time, str_to_time, date2timestamp, timestamp2date, smart_text from .expression import Expression @@ -20,9 +22,13 @@ 'BooleanField', 'DictField', 'SetField', 'ListField'] # TODO -# 完成index # 完成 query scan +if six.PY3: + basestring = str + unicode = str + long = int + class DecimalEncoder(json.JSONEncoder): # Helper class to convert a DynamoDB item to JSON. @@ -107,7 +113,7 @@ def typecast_for_read(self, value): def typecast_for_storage(self, value): """Typecasts the value for storing to DynamoDB.""" # default store unicode - return smart_unicode(value) + return smart_text(value) def value_type(self): return unicode @@ -144,14 +150,14 @@ def __init__(self, min_length=0, max_length=None, **kwargs): def typecast_for_read(self, value): if value == 'None': return '' - return smart_unicode(value) + return smart_text(value) def typecast_for_storage(self, value): """Typecasts the value for storing to DynamoDB.""" if value is None: return '' try: - return unicode(value) + return smart_text(value) except UnicodeError: return value.decode('utf-8') diff --git a/dynamodb/helpers.py b/dynamodb/helpers.py index 5b9173d..8fdfde4 100644 --- a/dynamodb/helpers.py +++ b/dynamodb/helpers.py @@ -194,6 +194,7 @@ def force_bytes(s, encoding='utf-8', strings_only=False, errors='strict'): if six.PY3: smart_str = smart_text + smart_unicode = smart_text force_str = force_text else: smart_str = smart_bytes @@ -232,7 +233,7 @@ def fn(*args, **kwargs): def get_attribute_type(attribute): - from .fields import CharField, IntegerField, DateTimeField, FloatField, TimeField + from .fields import CharField, IntegerField, DateTimeField, FloatField, TimeField, BooleanField if isinstance(attribute, (CharField, DateTimeField)): return 'S' elif isinstance(attribute, (IntegerField, FloatField, TimeField)): diff --git a/dynamodb/model.py b/dynamodb/model.py index 38cfe78..294f219 100644 --- a/dynamodb/model.py +++ b/dynamodb/model.py @@ -1,10 +1,12 @@ #! -*- coding: utf-8 -*- import copy +import six +from six import with_metaclass from botocore.exceptions import ClientError from .table import Table -from .query import Query +from .query import Query, GlobalQuery from .fields import Attribute from .errors import FieldValidationException, ValidationException, ClientException from .helpers import get_items_for_storage, cache_for @@ -21,10 +23,10 @@ def _initialize_attributes(model_class, name, bases, attrs): for parent in bases: if not isinstance(parent, ModelMetaclass): continue - for k, v in parent._attributes.iteritems(): + for k, v in six.iteritems(parent._attributes): model_class._attributes[k] = v - for k, v in attrs.iteritems(): + for k, v in six.iteritems(attrs): if isinstance(v, Attribute): model_class._attributes[k] = v v.name = v.name or k @@ -36,18 +38,21 @@ def _initialize_indexes(model_class, name, bases, attrs): """ model_class._local_indexed_fields = [] model_class._local_indexes = {} - model_class._global_indexed_fields = model_class.__global_index__ - model_class._global_indexes = {} + model_class._global_indexed_fields = [] + model_class._global_indexes = model_class.__global_indexes__ + model_class._global_secondary_indexes = [] model_class._hash_key = None model_class._range_key = None for parent in bases: if not isinstance(parent, ModelMetaclass): continue - for k, v in parent._attributes.iteritems(): + for k, v in six.iteritems(attrs): + if isinstance(v, (Attribute,)): if v.indexed: model_class._local_indexed_fields.append(k) - for k, v in attrs.iteritems(): + # setting hash_key and range_key and local indexes + for k, v in six.iteritems(attrs): if isinstance(v, (Attribute,)): if v.indexed: model_class._local_indexed_fields.append(k) @@ -60,6 +65,38 @@ def _initialize_indexes(model_class, name, bases, attrs): if name not in ('ModelBase', 'Model') and not model_class._hash_key: raise ValidationException('hash_key is required') + # setting global_indexes + global_indexes = [] + global_fields = set() + for gindex in model_class._global_indexes: + index = {} + name, primary_key, fields = gindex + index['name'] = name + # 处理主键 + if len(primary_key) == 2: + hash_key, range_key = primary_key + elif len(primary_key) == 1: + hash_key, range_key = primary_key[0], None + else: + raise ValidationException('invalid primary key') + index['hash_key'] = hash_key + global_fields.add(hash_key) + index['range_key'] = range_key + if range_key: + global_fields.add(range_key) + if hash_key not in model_class._attributes: + raise ValidationException('invalid hash key: %s' % hash_key) + if range_key and range_key not in model_class._attributes: + raise ValidationException('invalid range key: %s' % range_key) + # 处理备份键 + for field in fields: + if field not in model_class._attributes: + raise ValidationException('invalid include field: %s' % field) + index['include_fields'] = fields + global_indexes.append(index) + model_class._global_indexed_fields = global_fields + model_class._global_secondary_indexes = global_indexes + class ModelMetaclass(type): @@ -68,7 +105,7 @@ class ModelMetaclass(type): """ __table_name__ = None - __global_index__ = [] + __global_indexes__ = [] __local_index__ = {} def __init__(cls, name, bases, attrs): @@ -78,9 +115,9 @@ def __init__(cls, name, bases, attrs): _initialize_indexes(cls, name, bases, attrs) -class ModelBase(object): +class ModelBase(with_metaclass(ModelMetaclass, object)): - __metaclass__ = ModelMetaclass + # __metaclass__ = ModelMetaclass @classmethod def create(cls, **kwargs): @@ -129,7 +166,7 @@ def update(self, *args, **kwargs): ReturnConsumedCapacity=ReturnConsumedCapacity) if not self.validate_attrs(**kwargs): raise FieldValidationException(self._errors) - for k, v in kwargs.items(): + for k, v in six.iteritems(kwargs): field = self.attributes[k] update_fields[k] = field.typecast_for_storage(v) # use storage value @@ -177,6 +214,11 @@ def query(cls, *args): instance = cls() return Query(instance, *args) + @classmethod + def global_query(cls, index_name=None, *args): + instance = cls() + return GlobalQuery(instance, index_name, *args) + @classmethod def scan(cls): instance = cls() @@ -238,7 +280,7 @@ def is_valid(self): def validate_attrs(self, **kwargs): self._errors = [] - for attr, value in kwargs.iteritems(): + for attr, value in six.iteritems(kwargs): field = self.attributes.get(attr) if not field: raise ValidationException('Field not found: %s' % attr) @@ -306,7 +348,7 @@ def fields(self): def _get_values_for_read(self, values): read_values = {} - for att, value in values.iteritems(): + for att, value in six.iteritems(values): if att not in self.attributes: continue descriptor = self.attributes[att] @@ -318,7 +360,7 @@ def _get_values_for_storage(self): data = {} if not self.is_valid(): raise FieldValidationException(self.errors) - for attr, field in self.attributes.iteritems(): + for attr, field in six.iteritems(self.attributes): value = getattr(self, attr) if value is not None: data[attr] = field.typecast_for_storage(value) diff --git a/dynamodb/query.py b/dynamodb/query.py index ff5c190..4cf7aee 100644 --- a/dynamodb/query.py +++ b/dynamodb/query.py @@ -6,7 +6,7 @@ from .table import Table from .fields import Fields from .connection import db -from .errors import FieldValidationException +from .errors import FieldValidationException, GlobalSecondaryIndexesException class Paginator(object): @@ -124,6 +124,7 @@ def _projection_expression(self, *args): instance = self.model_object projections = [] for arg in args: + if isinstance(arg, Fields): name = arg.name if arg not in instance.fields: @@ -155,7 +156,7 @@ def _filter_expression(self, *args): # field_inst = field_instance if field_inst.name == self.filter_index_field: _, exp, is_key = field_inst._expression_func( - field_inst.op, *field_inst.express_args, use_key=True) + field_inst.op, *field_inst.express_args, is_key=True) if is_key: if not KeyConditionExpression: KeyConditionExpression = exp @@ -288,3 +289,190 @@ def all(self): results.append(instance) response['Items'] = results return response + + +class GlobalQuery(Query): + + def __init__(self, model_object, index_name, *args, **kwargs): + super(GlobalQuery, self).__init__(model_object, *args, **kwargs) + self.GlobalSecondaryIndexes = {g['name']: g for g in self.instance._global_secondary_indexes} + if index_name not in self.GlobalSecondaryIndexes: + raise GlobalSecondaryIndexesException("Invalid GSI") + self.GSI = index_name + self.GSIHashKey = None + self.GSIRangeKey = None + self.init_gsi() + + def init_gsi(self): + gindex = self.GlobalSecondaryIndexes[self.GSI] + hash_key, range_key = gindex['hash_key'], gindex.get('range_key', None) + self.GSIHashKey = self.instance.attributes[hash_key] + if range_key: + self.GSIRangeKey = self.instance.attributes[range_key] + + def _projection_expression(self, *args): + instance = self.model_object + projections = [] + for arg in args: + if isinstance(arg, Fields): + name = arg.name + if arg not in instance.fields: + raise FieldValidationException('%s not found' % name) + projections.append(name) + else: + raise FieldValidationException('Bad type must be Attribute type') + ProjectionExpression = ",".join(projections) + return ProjectionExpression + + def _get_primary_key(self): + hash_key, range_key = self.GSIHashKey, self.GSIRangeKey + key = { + "hash_key": hash_key + } + if range_key: + key["range_key"] = range_key + return key + + def _filter_expression(self, *args): + # get filter expression and key condition expression + FilterExpression = None + KeyConditionExpression = None + params = {} + for field_inst, exp, is_key in args: + # field_inst = field_instance + if (field_inst.name == self.GSIHashKey.name or + (self.GSIRangeKey and field_inst.name == self.GSIRangeKey.name)): + _, exp, is_key = field_inst._expression_func( + field_inst.op, *field_inst.express_args, is_key=True) + if is_key: + if not KeyConditionExpression: + KeyConditionExpression = exp + else: + KeyConditionExpression = KeyConditionExpression & exp + else: + if not FilterExpression: + FilterExpression = exp + else: + FilterExpression = FilterExpression & exp + if FilterExpression: + params['FilterExpression'] = FilterExpression + if KeyConditionExpression: + params['KeyConditionExpression'] = KeyConditionExpression + return params + + def _get_query_params(self): + # get filter params + params = {"IndexName": self.GSI} + # update filter expression + filter_params = self._filter_expression(*self.filter_args) + FilterExpression = filter_params.get('FilterExpression') + if FilterExpression: + params['FilterExpression'] = FilterExpression + KeyConditionExpression = filter_params.get('KeyConditionExpression') + if KeyConditionExpression: + params['KeyConditionExpression'] = KeyConditionExpression + if self.ProjectionExpression: + params['ProjectionExpression'] = self.ProjectionExpression + if self.ConsistentRead: + params['ConsistentRead'] = self.ConsistentRead + if self.ReturnConsumedCapacity: + params['ReturnConsumedCapacity'] = self.ReturnConsumedCapacity + self.query_params.update(params) + return self.query_params + + def where(self, *args): + # Find by any number of matching criteria... though presently only + # "where" is supported. + self.filter_args.extend(args) + return copy.deepcopy(self) + + def limit(self, limit): + self.Limit = limit + self.query_params['Limit'] = limit + return copy.deepcopy(self) + + def get(self, **primary_key): + # get directly by primary key + hash_key_value, range_key_value = None, None + for k, v in primary_key.items(): + print(k, v) + if k == self.GSIHashKey.name: + hash_key_value = v + elif k == self.GSIRangeKey.name: + range_key_value = v + else: + raise GlobalSecondaryIndexesException('Invalid primary key') + self.instance = self.model_class(**primary_key) + if not hash_key_value: + raise GlobalSecondaryIndexesException('Invalid primary key') + if self.GSIRangeKey and not range_key_value: + raise GlobalSecondaryIndexesException('Invalid primary key') + query = self.where(self.GSIHashKey.eq(hash_key_value)) + if range_key_value: + query = query.where(self.GSIRangeKey.eq(range_key_value)) + item = query.first() + return item + + def first(self): + response = self.limit(1).all() + items = response['Items'] + return items[0] if items else None + + def order_by(self, index_field, asc=True): + if isinstance(index_field, Fields): + name = index_field.name + index_name = self.instance._local_indexes.get(name) + if not (index_name or index_field.range_key): + raise FieldValidationException('index not found') + self.filter_index_field = name + self.query_params['ScanIndexForward'] = asc + else: + raise FieldValidationException('%s not a field' % index_field) + return copy.deepcopy(self) + + def _yield_all(self, method): + if method == 'scan': + func = getattr(Table(self.instance), 'scan') + elif method == 'query': + func = getattr(Table(self.instance), 'query') + else: + return + result_count = 0 + response = func(**self.query_params) + while True: + metadata = response.get('ResponseMetadata', {}) + for item in response['Items']: + result_count += 1 + yield item + if self.Limit > 0 and result_count >= self.Limit: + return + LastEvaluatedKey = response.get('LastEvaluatedKey') + if LastEvaluatedKey: + self.query_params['ExclusiveStartKey'] = LastEvaluatedKey + response = func(**self.query_params) + else: + break + + def _yield(self): + if self.Scan: + return self._yield_all('scan') + else: + return self._yield_all('query') + + def all(self): + self._get_query_params() + if self.Scan: + func = getattr(Table(self.instance), 'scan') + return self._yield_all('scan') + else: + func = getattr(Table(self.instance), 'query') + response = func(**self.query_params) + items = response['Items'] + results = [] + for item in items: + _instance = self.model_class(**item) + value_for_read = _instance._get_values_for_read(item) + instance = self.model_class(**value_for_read) + results.append(instance) + response['Items'] = results + return response diff --git a/dynamodb/table.py b/dynamodb/table.py index 3ef9aa5..303fa53 100644 --- a/dynamodb/table.py +++ b/dynamodb/table.py @@ -7,6 +7,8 @@ from botocore.exceptions import ClientError from botocore.vendored.requests.exceptions import ConnectionError +import six + from .connection import db from .helpers import get_attribute_type from .errors import ClientException, ConnectionException, ParameterException @@ -62,6 +64,8 @@ def _prepare_key_schema(self): return KeySchema def _prepare_attribute_definitions(self): + # Member must satisfy enum value set: [B, N, S] + attrs_set = set() AttributeDefinitions = [] attributes = self.instance.attributes hash_key = self.instance._hash_key @@ -69,17 +73,30 @@ def _prepare_attribute_definitions(self): 'AttributeName': hash_key, 'AttributeType': get_attribute_type(attributes[hash_key]), }) + attrs_set.add(hash_key) range_key = self.instance._range_key if range_key: AttributeDefinitions.append({ 'AttributeName': range_key, 'AttributeType': get_attribute_type(attributes[range_key]), }) + attrs_set.add(range_key) for field in self.instance._local_indexed_fields: + if field in attrs_set: + continue + AttributeDefinitions.append({ + 'AttributeName': field, + 'AttributeType': get_attribute_type(attributes[field]), + }) + attrs_set.add(field) + for field in self.instance._global_indexed_fields: + if field in attrs_set: + continue AttributeDefinitions.append({ 'AttributeName': field, 'AttributeType': get_attribute_type(attributes[field]), }) + attrs_set.add(field) return AttributeDefinitions def _prepare_primary_key(self, params): @@ -106,7 +123,59 @@ def _prepare_local_indexes(self): return indexes def _prepare_global_indexes(self): - return [] + ''' + { + 'IndexName': 'string', + 'KeySchema': [ + { + 'AttributeName': 'string', + 'KeyType': 'HASH'|'RANGE' + }, + ], + 'Projection': { + 'ProjectionType': 'ALL'|'KEYS_ONLY'|'INCLUDE', + 'NonKeyAttributes': [ + 'string', + ] + }, + 'ProvisionedThroughput': { + 'ReadCapacityUnits': 123, + 'WriteCapacityUnits': 123 + } + } + ''' + global_indexes = [] + for _gindex in self.instance._global_secondary_indexes: + index = { + "IndexName": _gindex['name'], + } + # setting key schema + hash_key = _gindex['hash_key'] + key_schema = [ + { + "AttributeName": hash_key, + "KeyType": "HASH" + } + ] + range_key = _gindex['range_key'] + if range_key: + key_schema.append({ + "AttributeName": range_key, + "KeyType": "RANGE" + }) + index['KeySchema'] = key_schema + # setting projection + index['Projection'] = { + 'ProjectionType': 'INCLUDE', + 'NonKeyAttributes': list(_gindex['include_fields']) + } + # setting ProvisionedThroughput + index['ProvisionedThroughput'] = { + 'ReadCapacityUnits': self.instance.ReadCapacityUnits, + 'WriteCapacityUnits': self.instance.WriteCapacityUnits + } + global_indexes.append(index) + return global_indexes def _prepare_create_table_params(self): # TableName @@ -128,6 +197,13 @@ def _prepare_create_table_params(self): 'ReadCapacityUnits': self.instance.ReadCapacityUnits, 'WriteCapacityUnits': self.instance.WriteCapacityUnits } + # TODO update botocore + # BillingMode='PROVISIONED'|'PAY_PER_REQUEST', + # billing_model = getattr(self.instance, 'BillingMode', None) + # if not billing_model: + # # 默认使用预配置 + # billing_model = 'PROVISIONED' + # table_params['BillingMode'] = billing_model return table_params def create(self): @@ -187,6 +263,7 @@ def create(self): } }, ], + BillingMode='PROVISIONED'|'PAY_PER_REQUEST', # 使用PAY_PER_REQUEST 吞吐量可设置为0 ProvisionedThroughput={ 'ReadCapacityUnits': 123, 'WriteCapacityUnits': 123 @@ -476,7 +553,7 @@ def _prepare_update_item_params(self, update_fields=None, *args, **kwargs): action_exp_dict[action] = action_exp ExpressionAttributeValues.update(eav) ExpressionAttributeNames.update(ean) - for action, _exp in action_exp_dict.iteritems(): + for action, _exp in six.iteritems(action_exp_dict): action_exp_dict[action] = '{action} {exp}'.format(action=action, exp=_exp) if ExpressionAttributeValues: @@ -584,4 +661,4 @@ def delete_item(self, **kwargs): return True def item_count(self): - return self.table.item_count + return self.table.item_count \ No newline at end of file diff --git a/dynamodb_py.egg-info/SOURCES.txt b/dynamodb_py.egg-info/SOURCES.txt index 46f0231..9b13e4e 100644 --- a/dynamodb_py.egg-info/SOURCES.txt +++ b/dynamodb_py.egg-info/SOURCES.txt @@ -1,3 +1,4 @@ +README.md setup.py dynamodb/__init__.py dynamodb/connection.py diff --git a/examples/create_item.py b/examples/create_item.py index 6681620..a9a3ecd 100644 --- a/examples/create_item.py +++ b/examples/create_item.py @@ -1,6 +1,10 @@ #! -*- coding: utf-8 -*- from __future__ import print_function # Python 2/3 compatibility +from os import environ + +environ['DEBUG'] = '1' + import json import boto3 import decimal diff --git a/examples/create_table.py b/examples/create_table.py index 1fbb2cc..1e4b0cd 100644 --- a/examples/create_table.py +++ b/examples/create_table.py @@ -1,10 +1,14 @@ #! -*- coding: utf-8 -*- - from __future__ import print_function # Python 2/3 compatibility +from os import environ + +environ['DEBUG'] = '1' + import boto3 from dynamodb.model import Model -from dynamodb.fields import CharField, IntegerField, FloatField, DictField +from dynamodb.fields import (CharField, IntegerField, FloatField, + DictField, BooleanField, DateTimeField) from dynamodb.table import Table ''' @@ -15,6 +19,7 @@ ''' + dynamodb = boto3.resource('dynamodb', region_name='us-west-2', endpoint_url="http://localhost:8000") @@ -67,12 +72,39 @@ class Movies(Model): info = DictField(name='info', default={}) +class Authentication(Model): + __table_name__ = 'authentication' + + ReadCapacityUnits = 100 + WriteCapacityUnits = 10 + + APPROACH_MOBILE = 'mobile' + APPROACH_WEIXIN = 'weixin' + APPROACH_MOBILE_PASSWORD = 'mobile_password' + + __global_indexes__ = [ + ('authentication_account_id-index', ('account_id', 'approach'), ['account_id', 'approach', 'identity', 'is_verified']), + ] + + account_id = CharField(name='account_id') + approach = CharField(name='approach', range_key=True) + identity = CharField(name='identity', hash_key=True) + is_verified = BooleanField(name='is_verified', default=False) + date_created = DateTimeField(name='date_created') + + def create_table(): # Table(Movies()).delete() - table = Table(Movies()).create() + # table = Table(Movies()).create() + # print("Table status:", table.table_status) + # print("Table info:", Table(Movies()).info()) + # print("Table indexes", Movies()._local_indexes) + + # Table(Authentication()).delete() + table = Table(Authentication()).create() print("Table status:", table.table_status) - print("Table info:", Table(Movies()).info()) - print("Table indexes", Movies()._local_indexes) + print("Table info:", Table(Authentication()).info()) + print("Table indexes", Authentication()._local_indexes) if __name__ == '__main__': diff --git a/examples/get_item.py b/examples/get_item.py index 11f0759..16a0286 100644 --- a/examples/get_item.py +++ b/examples/get_item.py @@ -1,12 +1,15 @@ #! -*- coding: utf-8 -*- from __future__ import print_function # Python 2/3 compatibility +from os import environ + +environ['DEBUG'] = '1' import boto3 import json import decimal from botocore.exceptions import ClientError -from movies import Movies +from movies import Movies, Authentication # Helper class to convert a DynamoDB item to JSON. @@ -40,14 +43,21 @@ def get_item_by_boto3(): except ClientError as e: print(e.response['Error']['Message']) else: - item = response['Item'] - print("GetItem succeeded:") - print(json.dumps(item, indent=4, cls=DecimalEncoder)) + print(response) + item = response.get('Item') + if item: + print("GetItem succeeded:") + print(json.dumps(item, indent=4, cls=DecimalEncoder)) + else: + print("NotFound") def get_item(): item = Movies.get(year=year, title=title) - print("GetItem succeeded:", item.info, item.year) + if item: + print("GetItem succeeded:", item.info, item.year) + else: + print("GetItem not found") primary_keys = [ {'year': 1990, 'title': 'Edward Scissorhands'}, @@ -58,6 +68,19 @@ def get_item(): print('items len:', len(items)) +def get_item_with_global_index(): + Authentication.create(identity='123', approach='mobile', account_id="123") + Authentication.create(identity='124', approach='weixin', account_id="123") + Authentication.create(identity='125', approach='mobile', account_id="125") + Authentication.create(identity='126', approach='weixin', account_id="126") + item = (Authentication.global_query(index_name='authentication_account_id-index') + .get(account_id="123", approach='mobile')) + print(item) + print("Authentication with account_id and approach") + print(item.account_id, ":", item.approach, item.identity) + + if __name__ == '__main__': get_item_by_boto3() get_item() + get_item_with_global_index() diff --git a/examples/movies.py b/examples/movies.py index a791a65..9242d92 100644 --- a/examples/movies.py +++ b/examples/movies.py @@ -4,7 +4,7 @@ from dynamodb.model import Model from dynamodb.fields import (CharField, IntegerField, FloatField, - DateTimeField, DictField) + DateTimeField, DictField, BooleanField) class Movies(Model): @@ -20,3 +20,25 @@ class Movies(Model): rank = IntegerField(name='rank', indexed=True) release_date = DateTimeField(name='release_date') info = DictField(name='info', default={}) + + +class Authentication(Model): + + __table_name__ = 'authentication' + + ReadCapacityUnits = 100 + WriteCapacityUnits = 10 + + APPROACH_MOBILE = 'mobile' + APPROACH_WEIXIN = 'weixin' + APPROACH_MOBILE_PASSWORD = 'mobile_password' + + __global_indexes__ = [ + ('authentication_account_id-index', ('account_id', 'approach'), ['account_id', 'approach', 'identity', 'is_verified']), + ] + + account_id = CharField(name='account_id') + approach = CharField(name='approach', range_key=True) + identity = CharField(name='identity', hash_key=True) + is_verified = BooleanField(name='is_verified', default=False) + date_created = DateTimeField(name='date_created') \ No newline at end of file diff --git a/examples/query_items.py b/examples/query_items.py index caf1b54..a735c3a 100644 --- a/examples/query_items.py +++ b/examples/query_items.py @@ -2,6 +2,8 @@ from __future__ import print_function # Python 2/3 compatibility from os import environ +environ['DEBUG'] = '1' + import boto3 import decimal from botocore.exceptions import ClientError @@ -9,7 +11,7 @@ from dynamodb.json_import import json -from movies import Movies +from movies import Movies, Authentication # Helper class to convert a DynamoDB item to JSON. @@ -175,6 +177,30 @@ def query_with_index(): print(i.year, ":", i.title, i.rating) +def query_with_global_index(): + Authentication.create(identity='123', approach='mobile', account_id="123") + Authentication.create(identity='124', approach='weixin', account_id="123") + Authentication.create(identity='125', approach='mobile', account_id="125") + Authentication.create(identity='126', approach='weixin', account_id="126") + response = (Authentication.global_query(index_name='authentication_account_id-index') + .where(Authentication.account_id.eq("123"), + Authentication.approach.eq('mobile')) + .order_by(Authentication.approach) # use rating as range key by local index + .all()) + items = response['Items'] + print("Authentication with account_id and approach") + for i in items: + print(i.account_id, ":", i.approach, i.identity) + response = (Authentication.global_query(index_name='authentication_account_id-index') + .where(Authentication.account_id.eq("123")) + .order_by(Authentication.approach, asc=False) # use rating as range key by local index + .all()) + items = response['Items'] + print("Authentication with account_id") + for i in items: + print(i.account_id, ":", i.approach, i.identity) + + def query_with_paginator(): response = (Movies.query() .where(Movies.year.eq(1992)) @@ -208,5 +234,6 @@ def query_with_paginator(): # query_with_limit_and_filter_by_boto3() # query_without_index() # query_with_index() + query_with_global_index() # query_with_paginator_by_boto3() - query_with_paginator() + # query_with_paginator() diff --git a/examples/update_item.py b/examples/update_item.py index 4b94fcd..0220d93 100644 --- a/examples/update_item.py +++ b/examples/update_item.py @@ -1,6 +1,10 @@ #! -*- coding: utf-8 -*- from __future__ import print_function # Python 2/3 compatibility +from os import environ + +environ['DEBUG'] = '1' + import json import boto3 import decimal