DynamORM API¶
dynamorm
¶
The base module namespace simply imports the most frequently used objects to simplify imports in clients:
from dynamorm import DynaModel
dynamorm.model
¶
Models represent tables in DynamoDB and define the characteristics of the Dynamo service as well as the Marshmallow or Schematics schema that is used for validating and marshalling your data.
-
class
dynamorm.model.
DynaModel
(partial=False, **raw)¶ DynaModel
is the base class all of your models will extend from. This model definition encapsulates the parameters used to create and manage the table as well as the schema for validating and marshalling data into object attributes. It will also hold any custom business logic you need for your objects.Your class must define two inner classes that specify the Dynamo Table options and the Schema, respectively.
The Dynamo Table options are defined in a class named
Table
. See thedynamorm.table
module for more information.Any Local or Global Secondary Indexes you wish to create are defined as inner tables that extend from either the
LocalIndex
orGlobalIndex
classes. See thedynamorm.table
module for more information.The document schema is defined in a class named
Schema
, which should be filled out exactly as you would fill out any other MarshmallowSchema
or SchematicsModel
.For example:
# Marshmallow example import os from dynamorm import DynaModel, GlobalIndex, ProjectAll from marshmallow import fields, validate, validates, ValidationError class Thing(DynaModel): class Table: name = 'things' hash_key = 'id' read = 5 write = 1 class ByColor(GlobalIndex): name = 'by-color' hash_key = 'color' read = 5 write = 1 projection = ProjectAll() class Schema: id = fields.String(required=True) name = fields.String() color = fields.String(validate=validate.OneOf(('purple', 'red', 'yellow'))) compound = fields.Dict(required=True) @validates('name') def validate_name(self, value): # this is a very silly example just to illustrate that you can fill out the # inner Schema class just like any other Marshmallow class if name.lower() == 'evan': raise ValidationError("No Evan's allowed") def say_hello(self): print("Hello. {name} here. My ID is {id} and I'm colored {color}".format( id=self.id, name=self.name, color=self.color ))
-
delete
()¶ Delete this record in the table.
-
classmethod
get
(consistent=False, **kwargs)¶ Get an item from the table
Example:
Thing.get(hash_key="three")
- Parameters
consistent (bool) – If set to True the get will be a consistent read
**kwargs – You must supply your hash key, and range key if used
-
classmethod
get_batch
(keys, consistent=False, attrs=None)¶ Generator to get more than one item from the table.
- Parameters
keys – One or more dicts containing the hash key, and range key if used
consistent (bool) – If set to True then get_batch will be a consistent read
attrs (str) – The projection expression of which attrs to fetch, if None all attrs will be fetched
-
classmethod
new_from_raw
(raw, partial=False)¶ Return a new instance of this model from a raw (dict) of data that is loaded by our Schema
- Parameters
raw (dict) – The attributes to use when creating the instance
-
classmethod
put
(item, **kwargs)¶ Put a single item into the table for this model
The attributes on the item go through validation, so this may raise
ValidationError
.- Parameters
item (dict) – The item to put into the table
**kwargs – All other kwargs are passed through to the put method on the table
-
classmethod
put_batch
(*items, **batch_kwargs)¶ Put one or more items into the table
- Parameters
*items – The items to put into the table
**kwargs – All other kwargs are passed through to the put_batch method on the table
Example:
Thing.put_batch( {"hash_key": "one"}, {"hash_key": "two"}, {"hash_key": "three"}, )
-
classmethod
put_unique
(item, **kwargs)¶ Put a single item into the table for this model, with a unique attribute constraint on the hash key
- Parameters
item (dict) – The item to put into the table
**kwargs – All other kwargs are passed through to the put_unique method on the table
-
classmethod
query
(*args, **kwargs)¶ Execute a query on our table based on our keys
You supply the key(s) to query based on as keyword arguments:
Thing.query(foo="Mr. Foo")
By default the
eq
condition is used. If you wish to use any of the other valid conditions for keys use a double underscore syntax following the key name. For example:Thing.query(foo__begins_with="Mr.")
- Parameters
query_kwargs (dict) – Extra parameters that should be passed through to the Table query function
**kwargs – The key(s) and value(s) to query based on
-
save
(partial=False, unique=False, return_all=False, **kwargs)¶ Save this instance to the table
- Parameters
partial (bool) – When False the whole document will be
.put
or.put_unique
to the table. When True only values that have changed since the document was loaded will sent to the table via an.update
.unique (bool) – Only relevant if partial=False, ignored otherwise. When False, the document will be
.put
to the table. When True, the document will be.put_unique
.return_all (bool) – Only used for partial saves. Passed through to
.update
.**kwargs – When partial is False these are passed through to the put method on the table. When partial is True these become the kwargs for update_item. See
.put
&.update
for more details.
The attributes on the item go through validation, so this may raise
ValidationError
.TODO - Support unique, partial saves.
-
classmethod
scan
(*args, **kwargs)¶ Execute a scan on our table
You supply the attr(s) to query based on as keyword arguments:
Thing.scan(age=10)
By default the
eq
condition is used. If you wish to use any of the other valid conditions for attrs use a double underscore syntax following the key name. For example:<>
:Thing.scan(foo__ne='bar')
<
:Thing.scan(count__lt=10)
<=
:Thing.scan(count__lte=10)
>
:Thing.scan(count__gt=10)
>=
:Thing.scan(count__gte=10)
BETWEEN
:Thing.scan(count__between=[10, 20])
IN
:Thing.scan(count__in=[11, 12, 13])
attribute_exists
:Thing.scan(foo__exists=True)
attribute_not_exists
:Thing.scan(foo__not_exists=True)
attribute_type
:Thing.scan(foo__type='S')
begins_with
:Thing.scan(foo__begins_with='f')
contains
:Thing.scan(foo__contains='oo')
Accessing nested attributes also uses the double underscore syntax:
Thing.scan(address__state="CA") Thing.scan(address__state__begins_with="C")
Multiple attrs are combined with the AND (&) operator:
Thing.scan(address__state="CA", address__zip__begins_with="9")
If you want to combine them with the OR (|) operator, or negate them (~), then you can use the Q function and pass them as arguments into scan where each argument is combined with AND:
from dynamorm import Q Thing.scan(Q(address__state="CA") | Q(address__state="NY"), ~Q(address__zip__contains="5"))
The above would scan for all things with an address.state of (CA OR NY) AND address.zip does not contain 5.
This returns a generator, which will continue to yield items until all matching the scan are produced, abstracting away pagination. More information on scan pagination: http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Scan.html#Scan.Pagination
- Parameters
scan_kwargs (dict) – Extra parameters that should be passed through to the Table scan function
*args – An optional list of Q objects that can be combined with or superseded the **kwargs values
**kwargs – The key(s) and value(s) to filter based on
-
update
(conditions=None, update_item_kwargs=None, return_all=False, **kwargs)¶ Update this instance in the table
New values are set via kwargs to this function:
thing.update(foo='bar')
This would set the
foo
attribute of the thing object to'bar'
. You cannot change the Hash or Range key via an update operation – this is a property of DynamoDB.You can supply a dictionary of conditions that influence the update. In their simpliest form Conditions are supplied as a direct match (eq):
thing.update(foo='bar', conditions=dict(foo='foo'))
This update would only succeed if foo was set to ‘foo’ at the time of the update. If you wish to use any of the other valid conditions for attrs use a double underscore syntax following the key name. You can also access nested attributes using the double underscore syntac. See the scan method for examples of both.
You can also pass Q objects to conditions as either a complete expression, or a list of expressions that will be AND’d together:
thing.update(foo='bar', conditions=Q(foo='foo')) thing.update(foo='bar', conditions=Q(foo='foo') | Q(bar='bar')) # the following two statements are equivalent thing.update(foo='bar', conditions=Q(foo='foo') & ~Q(bar='bar')) thing.update(foo='bar', conditions=[Q(foo='foo'), ~Q(bar='bar')])
If your update conditions do not match then a dynamorm.exceptions.ConditionFailed exception will be raised.
As long as the update succeeds the attrs on this instance will be updated to match their new values. If you set
return_all
to true then we will update all of the attributes on the object with the current values in Dyanmo, rather than just those you updated.
-
classmethod
update_item
(conditions=None, update_item_kwargs=None, **kwargs)¶ Update a item in the table
- Params conditions
A dict of key/val pairs that should be applied as a condition to the update
- Params update_item_kwargs
A dict of other kwargs that are passed through to update_item
- Params **kwargs
Includes your hash/range key/val to match on as well as any keys to update
-
validate
()¶ Validate this instance
We do this as a “native”/load/deserialization since Marshmallow ONLY raises validation errors for required/allow_none/validate(s) during deserialization. See the note at: https://marshmallow.readthedocs.io/en/latest/quickstart.html#validation
-
-
class
dynamorm.model.
DynaModelMeta
(name, parents, attrs)¶ DynaModelMeta is a metaclass for the DynaModel class that transforms our Table and Schema classes
Since we can inspect the data we need to build the full data structures needed for working with tables and indexes users can define for more concise and readable table definitions that we transform into the final. To allow for a more concise definition of DynaModels we do not require that users define their inner Schema class as extending from the
Schema
. Instead, when the class is being defined we take the inner Schema and transform it into a new class named <Name>Schema, extending fromSchema
. For example, on a model namedFoo
the resultingFoo.Schema
object would be an instance of a class namedFooSchema
, rather than a class namedSchema
dynamorm.table
¶
The inner Table
class on DynaModel
definitions becomes an instance of our
dynamorm.table.DynamoTable3
class.
The attributes you define on your inner Table
class map to underlying boto data structures. This mapping is
expressed through the following data model:
Attribute |
Required |
Type |
Description |
---|---|---|---|
name |
True |
str |
The name of the table, as stored in Dynamo. |
hash_key |
True |
str |
The name of the field to use as the hash key. It must exist in the schema. |
range_key |
False |
str |
The name of the field to use as the range_key, if one is used. It must exist in the schema. |
read |
True |
int |
The provisioned read throughput. |
write |
True |
int |
The provisioned write throughput. |
stream |
False |
str |
The stream view type, either None or one of: ‘NEW_IMAGE’|’OLD_IMAGE’|’NEW_AND_OLD_IMAGES’|’KEYS_ONLY’ |
Indexes¶
Like the Table
definition, Indexes are also inner classes on DynaModel
definitions, and they require the same
data model with one extra field.
Attribute |
Required |
Type |
Description |
---|---|---|---|
projection |
True |
object |
An instance of of |
-
class
dynamorm.table.
DynamoCommon3
¶ Common properties & functions of Boto3 DynamORM objects – i.e. Tables & Indexes
-
property
key_schema
¶ Return an appropriate KeySchema, based on our key attributes and the schema object
-
property
provisioned_throughput
¶ Return an appropriate ProvisionedThroughput, based on our attributes
-
property
-
class
dynamorm.table.
DynamoGlobalIndex3
(table, schema)¶
-
class
dynamorm.table.
DynamoIndex3
(table, schema)¶
-
class
dynamorm.table.
DynamoLocalIndex3
(table, schema)¶
-
class
dynamorm.table.
DynamoTable3
(schema, indexes=None)¶ Represents a Table object in the Boto3 DynamoDB API
This is built in such a way that in the future, when Amazon releases future boto versions, a new DynamoTable class can be authored that implements the same methods but maps through to the new semantics.
-
property
all_attribute_fields
¶ Returns a list with the names of all the attribute fields (hash or range key on the table or indexes)
-
property
attribute_definitions
¶ Return an appropriate AttributeDefinitions, based on our key attributes and the schema object
-
create
(wait=True)¶ DEPRECATED – shim
-
create_table
(wait=True)¶ Create a new table based on our attributes
- Parameters
wait (bool) – If set to True, the default, this call will block until the table is created
-
delete
(wait=True)¶ Delete this existing table
- Parameters
wait (bool) – If set to True, the default, this call will block until the table is deleted
-
property
exists
¶ Return True or False based on the existance of this tables name in our resource
-
classmethod
get_resource
(**kwargs)¶ Return the boto3 resource
If you provide kwargs here and the class doesn’t have any resource_kwargs defined then the ones passed will permanently override the resource_kwargs on the class.
This is useful for bootstrapping test resources against a Dynamo local instance as a call to
DynamoTable3.get_resource
will end up replacing the resource_kwargs on all classes that do not define their own.
-
classmethod
get_table
(name)¶ Return the boto3 Table object for this model, create it if it doesn’t exist
The Table is stored on the class for each model, so it is shared between all instances of a given model.
-
get_update_expr_for_key
(id_, parts)¶ Given a key and a unique id, return all the information required for the update expression. This includes the actual field operations, a dictionary of generated field names, and the generated field value.
To account for nested keys, the generated field expression placeholders are of the form:
#uk_0_0 = :uv_0 #uk_0_0.#uk_0_1 = :uv_0 #uk_0_0.#uk_0_1.#uk_0_2 = :uv_0 ...
Note that if the value of a part - e.g #uk_0_1 - itself has a period
.
, that is interpreted literally and not as nest in the document path. That is:#uk_0_0.#uk_0_1 = :uv_0 { "#uk_0_0": "foo", "#uk_0_1": "bar.baz" } { ":uv_0": 42 }
…will result in the value:
"foo": { "bar.baz": 42 }
- Parameters
id – Unique id for this key
parts – List of parts that make up this key
- Return type
tuple[str, dict, str]
-
index_attribute_fields
(index_name=None)¶ Return the attribute fields for a given index, or all indexes if omitted
-
put
(item, **kwargs)¶ Put a singular item into the table
- Parameters
item (dict) – The data to put into the table
**kwargs – All other keyword arguments are passed through to the DynamoDB Table put_item function.
-
property
stream_specification
¶ Return an appropriate StreamSpecification, based on the stream attribute
-
property
table
¶ Return the boto3 table
-
property
table_attribute_fields
¶ Returns a list with the names of the table attribute fields (hash or range key)
-
update_table
()¶ Updates an existing table
Per the AWS documentation:
You can only perform one of the following operations at once:
Modify the provisioned throughput settings of the table.
Enable or disable Streams on the table.
Remove a global secondary index from the table.
Create a new global secondary index on the table.
Thus, this will recursively call itself to perform each of these operations in turn, waiting for the table to return to ‘ACTIVE’ status before performing the next.
This returns the number of update operations performed.
-
property
-
dynamorm.table.
Q
(**mapping)¶ A Q object represents an AND’d together query using boto3’s Attr object, based on a set of keyword arguments that support the full access to the operations (eq, ne, between, etc) as well as nested attributes.
It can be used input to both scan operations as well as update conditions.
-
class
dynamorm.table.
QueryIterator
(model, *args, **kwargs)¶ -
reverse
()¶ Return results from the query in reverse
-
-
class
dynamorm.table.
ReadIterator
(model, *args, **kwargs)¶ ReadIterator provides an iterator object that wraps a model and a method (either scan or query).
Since it is an object we can attach attributes and functions to it that are useful to the caller.
# Scan through a model, one at a time. Don't do this! results = MyModel.scan().limit(1) for model in results: print model.id # The next time you call scan (or query) pass the .last attribute of your previous results # in as the last argument results = MyModel.scan().start(results.last).limit(1) for model in results: print model.id # ...
- Parameters
model – The Model class to wrap
*args – Q objects, passed through to scan or query
**kwargs – filters, passed through to scan or query
-
again
()¶ Call this to reset the iterator so that you can iterate over it again.
If the previous invocation has a LastEvaluatedKey then this will resume from the next item. Otherwise it will re-do the previous invocation.
-
consistent
()¶ Make this read a consistent one
-
count
()¶ Return the count matching the current read
This triggers a new request to the table when it is invoked.
-
limit
(limit)¶ Set the limit value
-
partial
(partial)¶ Set the partial value for this iterator, which is used when creating new items from the response.
This is used by indexes
-
recursive
()¶ Set the recursive value to True for this iterator
-
specific_attributes
(attrs)¶ Return only specific attributes in the documents through a ProjectionExpression
This is a list of attribute names. See the documentation for more info: https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.ProjectionExpressions.html
-
start
(last)¶ Set the last value
-
class
dynamorm.table.
ScanIterator
(model, *args, **kwargs)¶
-
dynamorm.table.
remove_nones
(in_dict)¶ Recursively remove keys with a value of
None
from thein_dict
collection
dynamorm.relationships
¶
Relationships leverage the native tables & indexes in DynamoDB to allow more concise definition and access of related objects in your Python code.
You define relationships along side your Schema and Indexes on your model, and must provide the query used to map the related models together. You can also supply a “back reference” query to have the other side of the relationship also have a relationship back to the defining model.
DynamORM provides the following relationship types:
dynamorm.relationships.OneToOne
- Useful when you have a large number of attributes to store and you want to break them up over multiple tables for performance in querying.dynamorm.relationships.OneToMany
/dynamorm.relationships.ManyToOne
- Useful when you have an instance of one model that has a collection of related instances of another model. You useOneToMany
orManyToOne
depending on which side of the relationship you are defining the attribute on. You’ll to use both interchangeably based on how your models are laid out since you need to pass a reference to the other model into the relationship.
Here’s an example of how you could model the Forum Application from the DynamoDB Examples:
class Reply(DynaModel):
class Table:
name = 'replies'
hash_key = 'forum_thread'
range_key = 'created'
read = 1
write = 1
class ByUser(GlobalIndex):
name = 'replies-by-user'
hash_key = 'user_name'
range_key = 'message'
projection = ProjectKeys()
read = 1
write = 1
class Schema:
forum_thread = String(required=True)
created = String(required=True)
user_name = String(required=True)
message = String()
class User(DynaModel):
class Table:
name = 'users'
hash_key = 'name'
read = 1
write = 1
class Schema:
name = String(required=True)
replies = OneToMany(
Reply,
index='ByUser',
query=lambda user: dict(user_name=user.name),
back_query=lambda reply: dict(name=reply.user_name)
)
class Thread(DynaModel):
class Table:
name = 'threads'
hash_key = 'forum_name'
range_key = 'subject'
read = 1
write = 1
class ByUser(GlobalIndex):
name = 'threads-by-user'
hash_key = 'user_name'
range_key = 'subject'
projection = ProjectKeys()
read = 1
write = 1
class Schema:
forum_name = String(required=True)
user_name = String(required=True)
subject = String(required=True)
user = ManyToOne(
User,
query=lambda thread: dict(name=thread.user_name),
back_index='ByUser',
back_query=lambda user: dict(user_name=user.name)
)
replies = OneToMany(
Reply,
query=lambda thread: dict(forum_thread='{0}\n{1}'.format(thread.forum_name, thread.subject)),
back_query=lambda reply: dict(
forum_name=reply.forum_thread.split('\n')[0],
subject=reply.forum_thread.split('\n')[1]
)
)
class Forum(DynaModel):
class Table:
name = 'forums'
hash_key = 'name'
read = 1
write = 1
class Schema:
name = String(required=True)
threads = OneToMany(
Thread,
query=lambda forum: dict(forum_name=forum.name),
back_query=lambda thread: dict(name=thread.forum_name)
)
-
class
dynamorm.relationships.
DefaultBackReference
(relationship)¶ When given a relationship the string representation of this will be a “python” string name of the model the relationship exists on.
For example, if there’s a relationship defined on a model named
OrderItem
this would renderorder_item
.
-
class
dynamorm.relationships.
ManyToOne
(other, query, index=None, back_query=None, back_index=None, back_reference=<class 'dynamorm.relationships.DefaultBackReference'>, auto_create=True)¶ A Many To One relationship is defined on the “child” model, where many child models have one parent model.
-
class
dynamorm.relationships.
OneToMany
(other, query, index=None, back_query=None, back_index=None, back_reference=<class 'dynamorm.relationships.DefaultBackReference'>)¶ A One to Many relationship is defined on the “parent” model, where each instance has many related “child” instances of another model.
-
class
dynamorm.relationships.
OneToOne
(other, query, index=None, back_query=None, back_index=None, back_reference=<class 'dynamorm.relationships.DefaultBackReference'>, auto_create=True)¶ A One-to-One relationship is where two models (tables) have items that have a relation to exactly one instance in the other model.
It is a useful pattern when you wish to split up large tables with many attributes where your “main” table is queried frequently and having all of the attributes included in the query results would increase your required throughput. By splitting the data into two tables you can have lower throughput on the “secondary” table as the items will be lazily fetched only as they are accessed.
-
set_this_model
(model)¶ Called from the metaclass once the model the relationship is being placed on has been initialized
-
dynamorm.signals
¶
Signals provide a way for applications to loosely couple themselves and respond to different life cycle events.
The blinker library provides the low-level signal implementation.
To use the signals you connect
a receiver function to the signals you’re interested in:
from dynamorm.signals import post_save
def post_save_receiver(sender, instance, partial, put_kwargs):
log.info("Received post_save signal from model %s for instance %s", sender, instance)
post_save.connect(post_save_receiver)
See the blinker documentation for more details.
-
dynamorm.signals.
model_prepared
¶ Sent whenever a model class has been prepared by the metaclass.
- Param
sender: The model class that is now prepared for use.
-
dynamorm.signals.
pre_init
¶ Sent during model instantiation, before processing the raw data.
- Param
sender: The model class.
- Param
instance: The model instance.
- Param
bool partial: True if this is a partial instantiation, not all data may be present.
- Param
dict raw: The raw data to be processed by the model schema.
-
dynamorm.signals.
post_init
¶ Sent once model instantiation is complete and all raw data has been processed.
- Param
sender: The model class.
- Param
instance: The model instance.
- Param
bool partial: True if this is a partial instantiation, not all data may be present.
- Param
dict raw: The raw data to be processed by the model schema.
-
dynamorm.signals.
pre_save
¶ Sent before saving (via put) model instances.
- Param
sender: The model class.
- Param
instance: The model instance.
- Param
dict put_kwargs: A dict of the kwargs being sent to the table put method.
-
dynamorm.signals.
post_save
¶ Sent after saving (via put) model instances.
- Param
sender: The model class.
- Param
instance: The model instance.
- Param
dict put_kwargs: A dict of the kwargs being sent to the table put method.
-
dynamorm.signals.
pre_update
¶ Sent before saving (via update) model instances.
- Param
sender: The model class.
- Param
instance: The model instance.
- Param
dict conditions: The conditions for the update to succeed.
- Param
dict update_item_kwargs: A dict of the kwargs being sent to the table put method.
- Param
dict updates: The fields to update.
-
dynamorm.signals.
post_update
¶ Sent after saving (via update) model instances.
- Param
sender: The model class.
- Param
instance: The model instance.
- Param
dict conditions: The conditions for the update to succeed.
- Param
dict update_item_kwargs: A dict of the kwargs being sent to the table put method.
- Param
dict updates: The fields to update.
-
dynamorm.signals.
pre_delete
¶ Sent before deleting model instances.
- Param
sender: The model class.
- Param
instance: The model instance.
-
dynamorm.signals.
post_delete
¶ Sent after deleting model instances.
- Param
sender: The model class.
- Param
instance: The deleted model instance.
dynamorm.exceptions
¶
-
exception
dynamorm.exceptions.
ConditionFailed
¶ A condition check failed
-
exception
dynamorm.exceptions.
DynaModelException
¶ Base exception for DynaModel problems
-
exception
dynamorm.exceptions.
DynamoException
¶ Base exception for all DynamORM raised exceptions
-
exception
dynamorm.exceptions.
DynamoTableException
¶ Base exception class for all DynamoTable errors
-
exception
dynamorm.exceptions.
HashKeyExists
¶ A operating requesting a unique hash key failed
-
exception
dynamorm.exceptions.
InvalidKey
¶ A parameter is not a valid key
-
exception
dynamorm.exceptions.
InvalidSchemaField
¶ A field provided does not exist in the schema
-
exception
dynamorm.exceptions.
MissingTableAttribute
¶ A required attribute is missing
-
exception
dynamorm.exceptions.
TableNotActive
¶ The table is not ACTIVE, and you do not want to wait
-
exception
dynamorm.exceptions.
ValidationError
(raw, schema_name, errors, *args, **kwargs)¶ Schema validation failed
dynamorm.local
¶
-
class
dynamorm.local.
DynamoLocal
(dynamo_dir, port=None)¶ Spins up a local dynamo instance. This should ONLY be used for testing!! This instance will register the cleanup method
shutdown
with theatexit
module.
-
dynamorm.local.
get_random_port
()¶ Find a random port that appears to be available