Versions¶
This document provides migration instructions for each major version, as well as the complete changelog for versions dating back to v0.9.0 from December 2015. The migration guides provide detailed examples and tips for migrating from the previous major version (excluding the 1.0.0 guide, which only covers migration from 0.9.0 and newer).
Migrating to 3.0.0¶
The 3.0.0 release includes two api changes from 2.4.0 that you may need to update your code to handle.
The
atomic=
kwarg toEngine.save
andEngine.delete
was deprecated in 2.4.0 and is removed in 3.0.0.The return type of
Type._dump
must now be abloop.action.Action
instance, even when the value isNone
. This does not impact custom types that only implementdynamo_load
anddynamo_dump
.
atomic
keyword¶
The atomic keyword to Engine.save and Engine.delete has been removed in favor of a user pattern. This offers a reasonable performance improvement for users that never used the atomic keyword, and addresses ambiguity related to per-row atomic vs transactional atomic operations. For context on the deprecation, see Issue #138. For the equivalent user pattern, see Snapshot Condition. To migrate your existing code, you can use the following:
# pre-3.0 code to migrate:
engine.load(some_object)
some_object.some_attr = "new value"
engine.save(some_object, atomic=True)
# post-3.0 code:
# https://bloop.readthedocs.io/en/latest/user/patterns.html#snapshot-condition
from your_patterns import snapshot
engine.load(some_object)
condition = snapshot(some_object)
some_object.some_attr = "new value"
engine.save(some_object, condition=condition)
Type._dump¶
Bloop now allows users to specify how a value should be applied in an UpdateExpression by wrapping a value in a
bloop.actions.Action
object. This is done transparently for raw values, which are interpreted
as either bloop.actions.set
or bloop.actions.remove
. With 2.4 and to support Issue #136 you can also
specify an add
or delete
action:
my_user.aliases = bloop.actions.add("new_alias")
my_website.views = bloop.actions.add(1)
To maintain flexibility the bloop Type
class has the final say as to which action a value should use. This allows
eg. the List
type to take a literal []
and change the action from actions.set
to actions.remove(None)
to indicate that the value should be cleared. This also means your custom type could see an actions.delete
and
modify the value to instead be expressible in an actions.set
.
If your custom types today only override dynamo_dump
or dynamo_load
then you don't need to do anything for this
migration. However if you currently override _dump
then you should update your function to (1) handle input that
may be an action or not, and (2) always return an action instance. In general, you should not modify an input
action and instead should return a new instance (possibly with the same action_type).
Here's the migration of the base Type._dump
:
# pre-3.0 code to migrate:
def _dump(self, value, **kwargs):
value = self.dynamo_dump(value, **kwargs)
if value is None:
return None
return {self.backing_type: value}
# post-3.0 code:
from bloop import actions
def _dump(self, value, **kwargs):
wrapped = actions.wrap(value) # [1]
value = self.dynamo_dump(wrapped.value, **kwargs)
if value is None:
return actions.wrap(None) # [2]
else:
value = {self.backing_type: value}
return wrapped.type.new_action(value) # [3]
# [1] always wrap the input value to ensure you're working with an Action instance
# [2] returns actions.remove(None) which will remove the value like None previously did
# [3] new_action uses the **same action type** as the input.
# If you want to always return a SET action instead use: return actions.set(value)
Migrating to 2.0.0¶
The 2.0.0 release includes a number of api changes and new features.
The largest functional change is the ability to compose models through subclassing; this is referred to as Abstract Inheritance and Mixins throughout the User Guide.
Python 3.6.0 is the minimum required version.
Meta.init
now defaults tocls.__new__(cls)
instead ofcls.__init__()
; when model instances are created as part ofengine.query
,engine.stream
etc. these will not call your model's__init__
method. The defaultBaseModel.__init__
is not meant for use outside of local instantiation.The
Column
andIndex
kwargname
was renamed todynamo_name
to accurately reflect how the value was used:Column(SomeType, name="foo")
becomesColumn(SomeType, dynamo_name="foo")
. Additionally, the column and index attributemodel_name
was renamed toname
;dynamo_name
is unchanged and reflects the kwarg value, if provided.
Engine¶
A new Engine kwarg table_name_template
can be used to modify the table name used per-engine, as documented in
the new Engine Configuration section of the User Guide. Previously, you may have used
the before_create_table
signal as follows:
# Nonce table names to avoid testing collisions
@before_create_table.connect
def apply_table_nonce(_, model, **__):
nonce = datetime.now().isoformat()
model.Meta.table_name += "-test-{}".format(nonce)
This will modify the actual model's Meta.table_name
, whereas the new kwarg can be used to only modify the bound
table name for a single engine. The following can be expressed for a single Engine as follows:
def apply_nonce(model):
nonce = datetime.now().isoformat()
return f"{model.Meta.table_name}-test-{nonce}"
engine = Engine(table_name_template=apply_nonce)
Inheritance¶
You can now use abstract base models to more easily compose common models. For example, you may use the same id structure for classes. Previously, this would look like the following:
class User(BaseModel):
id = Column(String, hash_key=True)
version = Column(Integer, range_key=True)
data = Column(Binary)
class Profile(BaseModel):
id = Column(String, hash_key=True)
version = Column(Integer, range_key=True)
summary = Column(String)
Now, you can define an abstract base and re-use the id
and version
columns in both:
class MyBase(BaseModel):
class Meta:
abstract = True
id = Column(String, hash_key=True)
version = Column(Integer, range_key=True)
class User(MyBase):
data = Column(Binary)
class Profile(MyBase):
summary = Column(String)
You can use multiple inheritance to compose models from multiple mixins; base classes do not need to subclass
BaseModel
. Here's the same two models as above, but the hash and range keys are defined across two mixins:
class StringHash:
id = Column(String, hash_key=True)
class IntegerRange:
version = Column(Integer, range_key=True)
class User(StringHash, IntegerRange, BaseModel):
data = Column(Binary)
class Profile(StringHash, IntegerRange, BaseModel):
summary = Column(String)
Mixins may also contain GlobalSecondaryIndex
and LocalSecondaryIndex
, even if their hash/range keys aren't
defined in that mixin:
class ByEmail:
by_email = GlobalSecondaryIndex(projection="keys", hash_key="email")
class User(StringHash, IntegerRange, ByEmail, BaseModel):
email = Column(String)
Meta.init¶
With the addition of column defaults (see below) Bloop needed to differentiate local mode instantiation from remote
model instantiation. Local model instantiation still uses __init__
, as in:
user = User(email="me@gmail.com", verified=False)
Unlike Engine.load
which takes existing model instances, all of Engine.query, Engine.scan, Engine.stream
will create their own instances. These methods use the model's Meta.init
to create new instances. Previously
this defaulted to __init__
. However, with the default __init__
method applying defaults in 2.0.0 this is
no longer acceptable for remote instantiation. Instead, cls.__new__(cls)
is used by default to create instances
during query/scan/stream.
This is an important distinction that Bloop should have made early on, but was forced due to defaults. For example,
imagine querying an index that doesn't project a column with a default. If the base __init__
was still used, the
Column's default would be used for the non-projected column even if there was already a value in DynamoDB. Here's one
model that would have the problem:
class User(BaseModel):
id = Column(UUID, hash_key=True)
created = Column(DateTime, default=datetime.datetime.now)
email = Column(String)
by_email = GlobalSecondaryIndex(projection="keys", hash_key=email)
user = User(id=uuid.uuid4(), email="me@gmail.com")
engine.save(user)
print(user.created) # Some datetime T1
query = engine.Query(User.by_email, hash_key=User.email=="me@gmail.com")
partial_user = query.first()
partial_user.created # This column isn't part of the index's projection!
If User.Meta.init
was still User.__init__
then partial_user.created
would invoke the default function for
User.created
and give us the current datetime. Instead, Bloop 2.0.0 will call User.__new__(User)
and we'll
get an AttributeError
because partial_user
doesn't have a created
value.
Column Defaults¶
Many columns have the same initialization value, even across models. For example, all but one of the following columns will be set to the same value or using the same logic:
class User(BaseModel):
email = Column(String, hash_key=True)
id = Column(UUID)
verified = Column(Boolean)
created = Column(DateTime)
followers = Column(Integer)
Previously, you might apply defaults by creating a simple function:
def new_user(email) -> User:
return User(
email=email,
id=uuid.uuid4(),
verified=False,
created=datetime.datetime.now(),
followers=0)
You'll still need a function for related initialization (eg. across fields or model instances) but for simple defaults, you can now specify them with the Column:
class User(BaseModel):
email = Column(String, hash_key=True)
id = Column(UUID, default=uuid.uuid4)
verified = Column(Boolean, default=False)
created = Column(DateTime, default=datetime.datetime.now)
followers = Column(Integer, default=0)
def new_user(email) -> User:
return User(email=email)
Defaults are only applied when creating new local instances inside the default BaseModel.__init__
- they are not
evaluated when loading objects with Engine.load
, Engine.query
, Engine.stream
etc. If you define a custom
__init__
without calling super().__init__(...)
they will not be applied.
In a related change, see above for the BaseModel.Meta.init
change. By default Bloop uses cls.__new__(cls)
to
create new instances of your models during Engine.scan
and Engine.query
instead of the previous default to
__init__
. This is intentional, to avoid applying unnecessary defaults to partially-loaded objects.
TTL¶
DynamoDB introduced the ability to specify a TTL column, which indicates a date (in seconds since the epoch) after
which the row may be automatically (eventually) cleaned up. This column must be a Number, and Bloop exposes the
Timestamp
type which is used as a datetime.datetime
. Like the DynamoDBStreams feature, the TTL is configured
on a model's Meta attribute:
class TemporaryPaste(BaseModel):
class Meta:
ttl = {
"column": "delete_after"
}
id = Column(String, hash_key=True)
s3_location = Column(String, dynamo_name="s3")
delete_after = Column(Timestamp)
Remember that it can take up to 24 hours for the row to be deleted; you should guard your reads using the current time against the cleanup time, or a filter with your queries:
# made up index
query = engine.Query(
TemporaryPaste.by_email,
key=TemporaryPaste.email=="me@gmail.com",
filter=TemporaryPaste.delete_after <= datetime.datetime.now())
print(query.first())
Bloop still refuses to update existing tables, so TTL will only be enabled on tables if they are created by Bloop
during Engine.bind
. Otherwise, the declaration exists exclusively to verify configuration.
Types¶
A new type Timestamp
was added for use with the new TTL feature (see above). This is a datetime.datetime
in
Python just like the DateTime
type, but is stored as an integer (whole seconds since epoch) instead of an ISO 8601
string. As with DateTime
, drop-in replacements are available for arrow
, delorean
, and pendulum
.
Exceptions¶
InvalidIndex
was replaced by the existingInvalidModel
InvalidSearchMode
,InvalidKeyCondition
,InvalidFilterCondition
, andInvalidProjection
were replaced byInvalidSearch
UnboundModel
was removed without replacement;Engine.bind
was refactored so that it would never be raised.InvalidComparisonOperator
was removed without replacement; it was never raised.
Migrating to 1.0.0¶
The 1.0.0 release includes a number of api changes, although functionally not much has changed since 0.9.6. The biggest changes are to Query and Scan syntax, which has changed from a builder pattern to a single call. The remaining changes are mostly resolved through a different import or parameter/attribute name.
Session, Client¶
In 1.0.0 the Engine wraps two clients: one for DynamoDB, and one for DynamoDBStreams. Bloop will create default
clients for any missing parameters using boto3.client
:
import boto3
from bloop import Engine
ddb = boto3.client("dynamodb")
streams = boto3.client("dynamodbstreams")
engine = Engine(dynamodb=ddb, dynamodbstreams=streams)
Before 0.9.11¶
Prior to 0.9.11, you could customize the session that an Engine used to talk to DynamoDB by creating an instance of a
boto3.session.Session
and passing it to the Engine during instantiation. This allowed you to use a different
profile name:
from boto3 import Session
from bloop import Engine
session = Session(profile_name="my-profile")
engine = Engine(session=session)
Now, you will need to create client instances from that session:
from boto3 import session
from bloop import Engine
session = Session(profile_name="my-profile")
engine = Engine(
dynamodb=session.client("dynamodb"),
dynamodbstreams=session.client("dynamodbstreams")
)
After 0.9.11¶
In 0.9.11, the Engine changed to take a bloop.Client
which wrapped a boto3 client. This allowed you to
connect to a different endpoint, such as a DynamoDBLocal instance:
import boto3
from bloop import Client, Engine
boto_client = boto3.client("dynamodb", endpoint_url="http://localhost:8000")
bloop_client = Client(boto_client=boto_client)
engine = Engine(client=bloop_client)
The intermediate bloop Client is no longer necessary, but a dynamodbstreams client can be provided:
import boto3
from bloop import Client, Engine
ddb = boto3.client("dynamodb", endpoint_url="http://localhost:8000")
streams = boto3.client("dynamodbstreams", endpoint_url="http://localhost:8000")
engine = Engine(dynamodb=ddb, dynamodbstreams=streams)
Engine¶
Config¶
Prior to 1.0.0, Engine took a number of configuration options. These have all been removed, and baked into existing
structures, or are only specified at the operation level. Engine no longer takes **config
kwargs.
atomic
controlled the default value fordelete
andsave
operations. If your engine had a defaultatomic
ofTrue
, you must now explicitly specify that with eachdelete
andsave
. The same is true forconsistent
, which controlled the default forload
,query
, andscan
.prefetch
controlled the default number of items that Bloop would fetch for aquery
orscan
. Bloop now uses the built-in pagination controls, and will fetch the next page when the currently buffered page has been iterated. There is no way to control the number of items loaded into the buffer at once.strict
controlled the default setting forquery
andscan
against an LSI. This is now part of the declaration of an LSI:by_create = LocalSecondaryIndex(projection="all", range_key="created", strict=False)
. By default an LSI is strict, which matches the default configuration option. This change means an LSI must be accessed by every caller the same way. You can't have one caller usestrict=True
while another usesstrict=False
.
EngineView and context
¶
Because there are no more engine.config
values, there is no point to using engines as context managers.
Previously, you could use an EngineView
to change one config option of an engine for a local command, without
changing the underlying engine's configuration:
with engine.context(atomic=True) as atomic:
atomic.save(...)
# a bunch of operations that perform atomic saves
Engine.context
and the EngineView
class have been removed since there is no longer an Engine.config
.
Engine.save, Engine.delete¶
These functions take *objs
instead of objs
, which makes passing a small number of items more comfortable.
user = User(...)
tweet = Tweet(...)
# Old: explicit list required
engine.save([user, tweet])
# 1.0.0: *varargs
engine.save(user, tweet)
# 1.0.0: save a list
some_users = get_modified()
engine.save(*some_users)
Query, Scan¶
Queries and Scans are now created in a single call, instead of using an ambiguous builder pattern. This will simplify most calls, but will be disruptive if you rely on partially building queries in different parts of your code.
Creating Queries¶
The most common issue with the builder pattern was creating multi-condition filters. Each call would replace the existing filter, not append to it. For example:
# This only checks the date, NOT the count
q = engine.query(User).key(User.id == 0)
q = q.filter(User.friends >= 3)
q = q.filter(User.created >= arrow.now().replace(years=-1))
# 1.0.0 only has one filter option
q = engine.query(
User, key=User.id == 0,
filter=(
(User.friends >= 3) &
(User.created >= ...)
)
)
The other query controls have been baked in, including projection
, consistent
, and forward
. Previously,
you changed the forward
option through the properties ascending
and descending
. Use forward=False
to
sort descending.
Here is a query with all options before and after. The structure is largely the same, with a lower symbolic overhead:
# Pre 1.0.0
q = (
engine.query(User)
.key(User.id == 0)
.projection("all")
.descending
.consistent
.filter(User.name.begins_with("a"))
)
# 1.0.0
q = engine.query(
User,
key=User.id == 0,
projection="all",
forward=False,
consistent=True,
filter=User.name.begins_with("a")
)
The same changes apply to Engine.scan
, although Scans can't be performed in
descending order.
Parallel Scans¶
1.0.0 allows you to create a parallel scan by specifying the segment that this scan covers. This is just a tuple of
(Segment, TotalSegments)
. For example, to scan Users
in three pieces:
scans = [
engine.scan(User, parallel=(0, 3)),
engine.scan(User, parallel=(1, 3)),
engine.scan(User, parallel=(2, 3))
]
for worker, scan in zip(workers, scans):
worker.process(scan)
Iteration and Properties¶
The all
method and prefetch
and limit
options have been removed. Each call to Engine.query()
or
Engine.scan()
will create a new iterator that tracks its progress and can be reset. To create different
iterators over the same parameters, you must call Engine.query()
multiple times.
# All the same iterator
>>> scan = engine.scan(User, filter=...)
>>> it_one = iter(scan)
>>> it_two = iter(scan)
>>> it_one is it_two is scan
True
Query and Scan no longer buffer their results, and you will need to reset the query to execute it again.
>>> scan = engine.scan(User)
>>> for result in scan:
... pass
...
>>> scan.exhausted
True
>>> scan.reset()
>>> for result in scan:
... print(result.id)
...
0
1
2
The
complete
property has been renamed toexhausted
to match the newStream
interface.The
results
property has been removed.count
,scanned
,one()
, andfirst()
are unchanged.
Models¶
Base Model and abstract
¶
Model declaration is largely unchanged, except for the model hierarchy. Early versions tied one base model to one
engine; later versions required a function to create each new base. In 1.0.0, every model inherits from a single
abstract model, BaseModel
:
from bloop import BaseModel, Column, Integer
class User(BaseModel):
id = Column(Integer, hash_key=True)
...
Additionally, any model can be an abstract base for a number of other models (to simplify binding subsets of all
models) by setting the Meta
attribute abstract
to True
:
from bloop import BaseModel
class AbstractUser(BaseModel):
class Meta:
abstract = True
@property
def is_email_verified(self):
return bool(getattr(self, "verified", False))
Before 0.9.6¶
Models were tied to a single Engine, and so the base class for any model had to come from that Engine:
from bloop import Engine
primary = Engine()
secondary = Engine()
class User(primary.model):
...
# secondary can't save or load instances of User!
Now that models are decoupled from engines, any engine can bind and load any model:
from bloop import BaseModel, Engine
primary = Engine()
secondary = Engine()
class User(BaseModel):
...
primary.bind(User)
secondary.bind(User)
After 0.9.6¶
After models were decoupled from Engines, Bloop still used some magic to create base models that didn't have hash keys
but also didn't fail various model creation validation. This meant you had to get a base model from new_base()
:
from bloop import Engine, new_base
primary = Engine()
secondary = Engine()
Base = new_base()
class User(Base):
...
primary.bind(User)
secondary.bind(User)
Now, the base model is imported directly. You can simplify the transition using an alias import. To adapt the above
code, we would alias BaseModel
to Base
:
from bloop import Engine
from bloop import BaseModel as Base
primary = Engine()
secondary = Engine()
class User(Base):
...
primary.bind(User)
secondary.bind(User)
Binding¶
Engine.bind
has undergone a few stylistic tweaks, and started offering recursive
binding. The parameter base
is no longer keyword-only.
To bind all concrete (Meta.abstract=False
) models from a single base, pass the base model:
from bloop import BaseModel, Engine
class AbstractUser(BaseModel):
class Meta:
abstract = True
class AbstractDataBlob(BaseModel):
class Meta:
abstract = True
class User(AbstractUser):
...
class S3Blob(AbstractDataBlob):
...
engine = Engine()
engine.bind(AbstractUser)
This will bind User
but not S3Blob
.
Indexes¶
Projection is Required¶
In 1.0.0, projection
is required for both GlobalSecondaryIndex
and
LocalSecondaryIndex
. This is because Bloop now supports binding multiple models to the same
table, and the "all"
projection is not really DynamoDB's all, but instead an INCLUDE
with all columns that
the model defines.
Previously:
from bloop import new_base, Column, Integer, GlobalSecondaryIndex
class MyModel(new_base()):
id = Column(Integer, hash_key=True)
data = Column(Integer)
# implicit "keys"
by_data = GlobalSecondaryIndex(hash_key="data")
Now, this must explicitly state that the projection is "keys":
from bloop import BaseModel, Column, Integer, GlobalSecondaryIndex
class MyModel(BaseModel):
id = Column(Integer, hash_key=True)
data = Column(Integer)
by_data = GlobalSecondaryIndex(
projection="keys", hash_key="data")
Hash and Range Key¶
1.0.0 also lets you use the Column object (and not just its model name) as the parameter to hash_key
and
range_key
:
class MyModel(BaseModel):
id = Column(Integer, hash_key=True)
data = Column(Integer)
by_data = GlobalSecondaryIndex(
projection="keys", hash_key=data)
__set__
and __del__
¶
Finally, Bloop disallows setting and deleting attributes on objects with the same name as an index. Previously, it would simply set that value on the object and silently ignore it when loading or saving. It wasn't clear that the value wasn't applied to the Index's hash or range key.
>>> class MyModel(BaseModel):
... id = Column(Integer, hash_key=True)
... data = Column(Integer)
... by_data = GlobalSecondaryIndex(
... projection="keys", hash_key=data)
...
>>> obj = MyModel()
>>> obj.by_data = "foo"
Traceback (most recent call last):
...
AttributeError: MyModel.by_data is a GlobalSecondaryIndex
Types¶
DateTime¶
Previously, DateTime
was backed by arrow. Instead of forcing a particular library on users --
and there are a number of high-quality choices -- Bloop's built-in datetime type is now backed by the standard
library's datetime.datetime
. This type only loads and dumps values in UTC, and uses a fixed ISO8601 format
string which always uses +00:00
for the timezone. DateTime
will forcefully convert the
timezone when saving to DynamoDB with datetime.datetime.astimezone()
which raises on naive datetime objects.
For this reason, you must specify a timezone when using this type.
Most users are expected to have a preferred datetime library, and so Bloop now includes implementations of DateTime
in a new extensions module bloop.ext
for the three most popular datetime libraries: arrow, delorean, and pendulum.
These expose the previous interface, which allows you to specify a local timezone to apply when loading values from
DynamoDB. It still defaults to UTC.
To swap out an existing DateTime class and continue using arrow objects:
# from bloop import DateTime
from bloop.ext.arrow import DateTime
To use delorean instead:
# from bloop import DateTime
from bloop.ext.delorean import DateTime
Future extensions will also be grouped by external package, and are not limited to types. For example, an alternate
Engine implementation could be provided in bloop.ext.sqlalchemy
that can bind SQLAlchemy's ORM models and
transparently maps Bloop types to SQLALchemy types.
Float¶
Float has been renamed to Number
and now takes an optional decimal.Context
to use when
translating numbers to DynamoDB's wire format. The same context used in previous versions (which comes
from the specifications in DynamoDB's User Guide) is used as the default; existing code only needs to use the new
name or alias it on import:
# from bloop import Float
from bloop import Number as Float
A new pattern has been added that provides a less restrictive type which always loads and
dumps float
instead of decimal.Decimal
. This comes at the expense of exactness, since Float's decimal
context does not trap Rounding or Inexact signals. This is a common request for boto3; keep its limitations in mind
when storing and loading values. It's probably fine for a cached version of a product rating, but you're playing with
fire storing account balances with it.
String¶
A minor change, String
no longer calls str(value)
when dumping to DynamoDB. This was
obscuring cases where the wrong value was provided, but the type silently coerced a string using that object's
__str__
. Now, you will need to manually call str
on objects, or boto3 will complain of an incorrect type.
>>> from bloop import BaseModel, Column, Engine, String
>>> class MyModel(BaseModel):
... id = Column(String, hash_key=True)
...
>>> engine = Engine()
>>> engine.bind(MyModel)
>>> not_a_str = object()
>>> obj = MyModel(id=not_a_str)
# previously, this would store "<object object at 0x7f92a5a2f680>"
# since that is str(not_a_str).
>>> engine.save(obj)
# now, this raises (newlines for readability)
Traceback (most recent call last):
..
ParamValidationError: Parameter validation failed:
Invalid type for
parameter Key.id.S,
value: <object object at 0x7f92a5a2f680>,
type: <class 'object'>,
valid types: <class 'str'>
Exceptions¶
NotModified
was raised by Engine.load
when some objects were not found. This
has been renamed to MissingObjects
and is otherwise unchanged.
Exceptions for unknown or abstract models have changed slightly. When an Engine fails to load or dump a model,
it will raise UnboundModel
. When a value fails to load or dump but isn't a subclass of
BaseModel
, the engine raises UnknownType
. When you attempt to perform
a mutating operation (load, save, ...) on an abstract model, the engine raises InvalidModel
.
Changelog¶
This changelog structure is based on Keep a Changelog v0.3.0. Bloop follows Semantic Versioning 2.0.0 and a draft appendix for its Public API.
Unreleased¶
(no unreleased changes)
3.1.0 - 2021-11-11¶
Fixed an issue where copying an Index
would lose projection information when the projection mode was
"include"
. This fix should have no effect for most users. You would only run into this issue if you
were manually calling bind_index
with copy=True
on a projection mode "include"
or you subclass
a model that has an index with that projection mode. This does not require a major version change since
there is no reasonable workaround that would be broken by making this fix. For example, a user might
decide to monkeypatch Index.__copy__
, bind_index
or refresh_index
to preserve the projection
information. Those workarounds will not be broken by this change. For an example of the issue, see
Issue #147.
[Changed]¶
Index.projection
is now aset
instead of alist`. Since ``Column
implements__hash__
this won't affect any existing calls that pass in lists. To remain consistent, this change is reflected inEngine.search
,Search.__init__
,Index.__init__
, and any docs or examples that refer to passing lists/sets of Columns.
[Fixed]¶
Index.__copy__
preservesIndex.projection["included"]
when projection mode is"include"
.
3.0.0 - 2019-10-11¶
Remove deprecated keyword atomic=
from Engine.save
and Engine.delete
, and Type._dump
must return
a bloop.actions.Action
instance. See the Migration Guide for context on these changes, and sample code to
easily migrate your existing custom Types.
[Added]¶
(internal)
util.default_context
can be used to create a new load/dump context and respects existing dict objects and keys (even if empty).
[Changed]¶
Type._dump
must return abloop.actions.Action
now. Most users won't need to change any code since custom types usually overridedynamo_dump
. If you have implemented your own_dump
function, you can probably just useactions.wrap
andactions.unwrap
to migrate:def _dump(self, value, *, context, **kwargs): value = actions.unwrap(value) # the rest of your function here return actions.wrap(value)
[Removed]¶
The deprecated
atomic=
keyword has been removed fromEngine.save
andEngine.delete
.The exception
bloop.exceptions.UnknownType
is no longer raised and has been removed.(internal)
BaseModel._load
andBaseModel._dump
have been removed. These were not documented or used anywhere in the code base, andunpack_from_dynamodb
should be used where_load
was anyway.(internal)
Engine._load
andEngine._dump
have been removed. These were not documented and are trivially replaced with calls totypedef._load
andtypedef._dump
instead.(internal) The
dumped
attr for Conditions is no longer needed since there's no need to dump objects except at render time.
2.4.1 - 2019-10-11¶
Bug fix. Thanks to @wilfre in PR #141!
[Fixed]¶
bloop.stream.shard.py::unpack_shards
no longer raises when a Shard in the DescribeStream has a ParentId that is not also available in the DescribeStream response (the parent shard has been deleted). Previously the code would raise while trying to link the two shard objects in memory. Now, the shard will have a ParentId ofNone
.
2.4.0 - 2019-06-13¶
The atomic=
keyword for Engine.save
and Engine.delete
is deprecated and will be removed in 3.0.
In 2.4 your code will continue to work but will raise DeprecationWarning
when you specify a value for atomic=
.
The Type._dump
function return value is changing to Union[Any, bloop.Action]
in 2.4 to prepare for the
change in 3.0 to exclusively returning a bloop.Action
. For built-in types and most custom types that only
override dynamo_dump
this is a no-op, but if you call Type._dump
you can use bloop.actions.unwrap()
on
the result to get the inner value. If you have a custom Type._dump
method it must return an action in 3.0. For
ease of use you can use bloop.actions.wrap()
which will specify either the SET
or REMOVE
action to match
existing behavior. Here's an example of how you can quickly modify your code:
# current pre-2.4 method, continues to work until 3.0
def _dump(self, value, **kwargs):
value = self.dynamo_dump(value, **kwargs)
if value is None:
return None
return {self.backing_type: value}
# works in 2.4 and 3.0
from bloop import actions
def _dump(self, value, **kwargs):
value = actions.unwrap(value)
value = self.dynamo_dump(value, **kwargs)
return actions.wrap(value)
Note that this is backwards compatible in 2.4: Type._dump
will not change unless you opt to pass the new
Action
object to it.
[Added]¶
SearchIterator.token
provides a way to start a new Query or Scan from a previous query/scan's state. See Issue #132.SearchIterator.move_to
takes a token to update the search state. Count/ScannedCount state are lost when moving to a token.Engine.delete
andEngine.save
take an optional argumentsync=
which can be used to update objects with the old or new values from DynamoDB after saving or deleting. See the Return Values section of the User Guide and Issue #137.bloop.actions
expose a way to manipulate atomic counters and sets. See the Atomic Counters section of the User Guide and Issue #136.
[Changed]¶
The
atomic=
keyword forEngine.save
andEngine.delete
emitsDeprecationWarning
and will be removed in 3.0.Type._dump
will return abloop.action.Action
object if one is passed in, in preparation for the change in 3.0.
2.3.3 - 2019-01-27¶
Engine.bind
is much faster for multi-model tables. See Issue #130.
[Changed]¶
(internal)
SessionWrapper
cachesDescribeTable
responses. You can clear these withSessionWrapper.clear_cache
; mutating calls such as.enable_ttl
will invalidate the cached description.(internal) Each
Engine.bind
will callCreateTable
at most once per table. Subsequent calls tobind
will callCreateTable
again.
2.3.2 - 2019-01-27¶
Minor bug fix.
[Fixed]¶
(internal)
bloop.conditions.iter_columns
no longer yieldsNone
onCondition()
(or any other condition whose.column
attribute isNone
).
2.3.0 - 2019-01-24¶
This release adds support for Transactions and On-Demand Billing. Transactions can include changes across tables, and provide ACID guarantees at a 2x throughput cost and a limit of 10 items per transaction. See the User Guide for details.
with engine.transaction() as tx:
tx.save(user, tweet)
tx.delete(event, task)
tx.check(meta, condition=Metadata.worker_id == current_worker)
[Added]¶
Engine.transaction(mode="w")
returns a transaction object which can be used directly or as a context manager. By default this creates aWriteTransaction
, but you can passmode="r"
to create a read transaction.WriteTransaction
andReadTransaction
can be prepared for committing with.prepare()
which returns aPreparedTransaction
which can be committed with.commit()
some number of times. These calls are usually handled automatically when using the read/write transaction as a context manager:# manual calls tx = engine.transaction() tx.save(user) p = tx.prepare() p.commit() # equivalent functionality with engine.transaction() as tx: tx.save(user)
Meta supports On-Demand Billing:
class MyModel(BaseModel): id = Column(String, hash_key=True) class Meta: billing = {"mode": "on_demand"}
(internal)
bloop.session.SessionWrapper.transaction_read
andbloop.session.SessionWrapper.transaction_write
can be used to call TransactGetItems and TransactWriteItems with fully serialized request objects. The write api requires a client request token to provide idempotency guards, but does not provide temporal bounds checks for those tokens.
[Changed]¶
Engine.load
now logs atINFO
instead ofWARNING
when failing to load some objects.Meta.ttl["enabled"]
will now be a literalTrue
orFalse
after binding the model, rather than the string "enabled" or "disabled".If
Meta.encryption
orMeta.backups
is None or missing, it will now be set after binding the model.Meta
and GSI read/write units are not validated if billing mode is"on_demand"
since they will be 0 and the provided setting is ignored.
2.2.0 - 2018-08-30¶
[Added]¶
DynamicList
andDynamicMap
types can store arbitrary values, although they will only be loaded as their primitive, direct mapping to DynamoDB backing types. For example:class MyModel(BaseModel): id = Column(String, hash_key=True) blob = Column(DynamicMap) i = MyModel(id="i") i.blob = {"foo": "bar", "inner": [True, {1, 2, 3}, b""]}
Meta supports Continuous Backups for Point-In-Time Recovery:
class MyModel(BaseModel): id = Column(String, hash_key=True) class Meta: backups = {"enabled": True}
SearchIterator
exposes anall()
method which eagerly loads all results and returns a single list. Note that the query or scan is reset each time the method is called, discarding any previously buffered state.
[Changed]¶
String
andBinary
types loadNone
as""
andb""
respectively.Saving an empty String or Binary (
""
orb""
) will no longer throw a botocore exception, and will instead be treated asNone
. This brings behavior in line with the Set, List, and Map types.
2.1.0 - 2018-04-07¶
Added support for Server-Side Encryption. This uses an AWS-managed Customer Master Key (CMK) stored in KMS which is managed for free: "You are not charged for the following: AWS-managed CMKs, which are automatically created on your behalf when you first attempt to encrypt a resource in a supported AWS service."
[Added]¶
Meta
supports Server Side Encryption:class MyModel(BaseModel): id = Column(String, hash_key=True) class Meta: encryption = {"enabled": True}
2.0.1 - 2018-02-03¶
Fix a bug where the last records in a closed shard in a Stream were dropped. See Issue #87 and PR #112.
[Fixed]¶
Stream
no longer drops the last records from a closed Shard when moving to the child shard.
2.0.0 - 2017-11-27¶
2.0.0 introduces 4 significant new features:
Model inheritance and mixins
Table name templates:
table_name_template="prod-{table_name}"
TTL support:
ttl = {"column": "not_after"}
Column defaults:
verified=Column(Boolean, default=False) not_after = Column( Timestamp, default=lambda: ( datetime.datetime.now() + datetime.timedelta(days=30) ) )
Python 3.6.0 is now the minimum required version, as Bloop takes advantage of __set_name__
and
__init_subclass__
to avoid the need for a Metaclass.
A number of internal-only and rarely-used external methods have been removed, as the processes which required them have been simplified:
Column.get, Column.set, Column.delete
in favor of their descriptor protocol counterpartsbloop.Type._register
is no longer necessary before using a custom TypeIndex._bind
is replaced by helpersbind_index
andrefresh_index
. You should not need to call these.A number of overly-specific exceptions have been removed.
[Added]¶
Engine
takes an optional keyword-only arg"table_name_template"
which takes either a string used to format each name, or a function which will be called with the model to get the table name of. This removes the need to connect to thebefore_create_table
signal, which also could not handle multiple table names for the same model. With this changeBaseModel.Meta.table_name
will no longer be authoritative, and the engine must be consulted to find a given model's table name. An internal functionEngine._compute_table_name
is available, and the per-engine table names may be added to the model.Meta in the future. (see Issue #96)A new exception
InvalidTemplate
is raised when an Engine's table_name_template is a string but does not contain the required"{table_name}"
formatting key.You can now specify a TTL (see Issue #87) on a model much like a Stream:
class MyModel(BaseModel): class Meta: ttl = { "column": "expire_after" } id = Column(UUID, hash_key=True) expire_after = Column(Timestamp)
A new type,
Timestamp
was added. This stores adatetime.datetime
as a unix timestamp in whole seconds.Corresponding
Timestamp
types were added to the following extensions, mirroring theDateTime
extension:bloop.ext.arrow.Timestamp
,bloop.ext.delorean.Timestamp
, andbloop.ext.pendulum.Timestamp
.Column
takes an optional kwargdefault
, either a single value or a no-arg function that returns a value. Defaults are applied only duringBaseModel.__init__
and not when loading objects from a Query, Scan, or Stream. If your function returnsbloop.util.missing
, no default will be applied. (see PR #90, PR #105 for extensive discussion)(internal) A new abstract interface,
bloop.models.IMeta
was added to assist with code completion. This fully describes the contents of aBaseModel.Meta
instance, and can safely be subclassed to provide hints to your editor:class MyModel(BaseModel): class Meta(bloop.models.IMeta): table_name = "my-table" ...
(internal)
bloop.session.SessionWrapper.enable_ttl
can be used to enable a TTL on a table. This SHOULD NOT be called unless the table was just created by bloop.(internal) helpers for dynamic model inheritance have been added to the
bloop.models
package:bloop.models.bind_column
bloop.models.bind_index
bloop.models.refresh_index
bloop.models.unbind
Direct use is discouraged without a strong understanding of how binding and inheritance work within bloop.
[Changed]¶
Python 3.6 is the minimum supported version.
BaseModel
no longer requires a Metaclass, which allows it to be used as a mixin to an existing class which may have a Metaclass.BaseModel.Meta.init
no longer defaults to the model's__init__
method, and will instead usecls.__new__(cls)
to obtain an instance of the model. You can still specify a custom initialization function:class MyModel(BaseModel): class Meta: @classmethod def init(_): instance = MyModel.__new__(MyModel) instance.created_from_init = True id = Column(...)
Column
andIndex
support the shallow copy method__copy__
to simplify inheritance with custom subclasses. You may override this to change how your subclasses are inherited.DateTime
explicitly guards againsttzinfo is None
, sincedatetime.astimezone
started silently allowing this in Python 3.6 -- you should not use a naive datetime for any reason.Column.model_name
is nowColumn.name
, andIndex.model_name
is nowIndex.name
.Column(name=)
is nowColumn(dynamo_name=)
andIndex(name=)
is nowIndex(dynamo_name=)
The exception
InvalidModel
is raised instead ofInvalidIndex
.The exception
InvalidSearch
is raised instead of the following:InvalidSearchMode
,InvalidKeyCondition
,InvalidFilterCondition
, andInvalidProjection
.(internal)
bloop.session.SessionWrapper
methods now require an explicit table name, which is not read from the model name. This exists to support different computed table names per engine. The following methods now require a table name:create_table
,describe_table
(new),validate_table
, andenable_ttl
(new).
[Removed]¶
bloop no longer supports Python versions below 3.6.0
bloop no longer depends on declare
Column.get
,Column.set
, andColumn.delete
helpers have been removed in favor of using the Descriptor protocol methods directly:Column.__get__
,Column.__set__
, andColumn.__delete__
.bloop.Type
no longer exposes a_register
method; there is no need to register types before using them, and you can remove the call entirely.Column.model_name
,Index.model_name
, and the kwargsColumn(name=)
,Index(name=)
(see above)The exception
InvalidIndex
has been removed.The exception
InvalidComparisonOperator
was unused and has been removed.The exception
UnboundModel
is no longer raised duringEngine.bind
and has been removed.The exceptions
InvalidSearchMode
,InvalidKeyCondition
,InvalidFilterCondition
, andInvalidProjection
have been removed.(internal)
Index._bind
has been replaced with the more complete solutions inbloop.models.bind_column
andbloop.models.bind_index
.
1.3.0 - 2017-10-08¶
This release is exclusively to prepare users for the name
/model_name
/dynamo_name
changes coming in 2.0;
your 1.2.0 code will continue to work as usual but will raise DeprecationWarning
when accessing model_name
on
a Column or Index, or when specifying the name=
kwarg in the __init__
method of Column
,
GlobalSecondaryIndex
, or LocalSecondaryIndex
.
Previously it was unclear if Column.model_name
was the name of this column in its model, or the name of the model
it is attached to (eg. a shortcut for Column.model.__name__
). Additionally the name=
kwarg actually mapped to
the object's .dynamo_name
value, which was not obvious.
Now the Column.name
attribute will hold the name of the column in its model, while Column.dynamo_name
will
hold the name used in DynamoDB, and is passed during initialization as dynamo_name=
. Accessing model_name
or
passing name=
during __init__
will raise deprecation warnings, and bloop 2.0.0 will remove the deprecated
properties and ignore the deprecated kwargs.
[Added]¶
Column.name
is the new home of theColumn.model_name
attribute. The same is true forIndex
,GlobalSecondaryIndex
, andLocalSecondaryIndex
.The
__init__
method ofColumn
,Index
,GlobalSecondaryIndex
, andLocalSecondaryIndex
now takesdynamo_name=
in place ofname=
.
[Changed]¶
Accessing
Column.model_name
raisesDeprecationWarning
, and the same for Index/GSI/LSI.Providing
Column(name=)
raisesDeprecationWarning
, and the same for Index/GSI/LSI.
1.2.0 - 2017-09-11¶
[Changed]¶
When a Model's Meta does not explicitly set
read_units
andwrite_units
, it will only default to 1/1 if the table does not exist and needs to be created. If the table already exists, any throughput will be considered valid. This will still ensure new tables have 1/1 iops as a default, but won't fail if an existing table has more than one of either.There is no behavior change for explicit integer values of
read_units
andwrite_units
: if the table does not exist it will be created with those values, and if it does exist then validation will fail if the actual values differ from the modeled values.An explicit
None
for eitherread_units
orwrite_units
is equivalent to omitting the value, but allows for a more explicit declaration in the model.Because this is a relaxing of a default only within the context of validation (creation has the same semantics) the only users that should be impacted are those that do not declare
read_units
andwrite_units
and rely on the built-in validation failing to match on values != 1. Users that rely on the validation to succeed on tables with values of 1 will see no change in behavior. This fits within the extended criteria of a minor release since there is a viable and obvious workaround for the current behavior (declare 1/1 and ensure failure on other values).When a Query or Scan has projection type "count", accessing the
count
orscanned
properties will immediately execute and exhaust the iterator to provide the count or scanned count. This simplifies the previous workaround of callingnext(query, None)
before usingquery.count
.
[Fixed]¶
1.1.0 - 2017-04-26¶
[Added]¶
1.0.3 - 2017-03-05¶
Bug fix.
[Fixed]¶
Stream orders records on the integer of SequenceNumber, not the lexicographical sorting of its string representation. This is an annoying bug, because as documented we should be using lexicographical sorting on the opaque string. However, without leading 0s that sort fails, and we must assume the string represents an integer to sort on. Particularly annoying, tomorrow the SequenceNumber could start with non-numeric characters and still conform to the spec, but the sorting-as-int assumption breaks. However, we can't properly sort without making that assumption.
1.0.2 - 2017-03-05¶
Minor bug fix.
[Fixed]¶
extension types in
ext.arrow
,ext.delorean
, andext.pendulum
now load and dumpNone
correctly.
1.0.1 - 2017-03-04¶
Bug fixes.
[Changed]¶
The
arrow
,delorean
, andpendulum
extensions now have a default timezone of"utc"
instead ofdatetime.timezone.utc
. There are open issues for both projects to verify if that is the expected behavior.
[Fixed]¶
DynamoDBStreams return a Timestamp for each record's ApproximateCreationDateTime, which botocore is translating into a real datetime.datetime object. Previously, the record parser assumed an int was used. While this fix is a breaking change for an internal API, this bug broke the Stream iterator interface entirely, which means no one could have been using it anyway.
1.0.0 - 2016-11-16¶
1.0.0 is the culmination of just under a year of redesigns, bug fixes, and new features. Over 550 commits, more than 60 issues closed, over 1200 new unit tests. At an extremely high level:
The query and scan interfaces have been polished and simplified. Extraneous methods and configuration settings have been cut out, while ambiguous properties and methods have been merged into a single call.
A new, simple API exposes DynamoDBStreams with just a few methods; no need to manage individual shards, maintain shard hierarchies and open/closed polling. I believe this is a first since the Kinesis Adapter and KCL, although they serve different purposes. When a single worker can keep up with a model's stream, Bloop's interface is immensely easier to use.
Engine's methods are more consistent with each other and across the code base, and all of the configuration settings have been made redundant. This removes the need for
EngineView
and its associated temporary config changes.Blinker-powered signals make it easy to plug in additional logic when certain events occur: before a table is created; after a model is validated; whenever an object is modified.
Types have been pared down while their flexibility has increased significantly. It's possible to create a type that loads another object as a column's value, using the engine and context passed into the load and dump functions. Be careful with this; transactions on top of DynamoDB are very hard to get right.
See the Migration Guide above for specific examples of breaking changes and how to fix them, or the User Guide for a tour of the new Bloop. Lastly, the Public and Internal API References are finally available and should cover everything you need to extend or replace whole subsystems in Bloop (if not, please open an issue).
[Added]¶
bloop.signals
exposes Blinker signals which can be used to monitor object changes, when instances are loaded from a query, before models are bound, etc.before_create_table
object_loaded
object_saved
object_deleted
object_modified
model_bound
model_created
model_validated
Engine.stream
can be used to iterate over all records in a stream, with a total ordering over approximate record creation time. Useengine.stream(model, "trim_horizon")
to get started. See the User Guide for details.New exceptions
RecordsExpired
andShardIteratorExpired
for errors in stream stateNew exceptions
Invalid*
for bad input subclassBloopException
andValueError
DateTime
types for the three most common date time libraries:bloop.ext.arrow.DateTime
bloop.ext.delorean.DateTime
bloop.ext.pendulum.DateTime
model.Meta
has a new optional attributestream
which can be used to enable a stream on the model's table.model.Meta
exposes the sameprojection
attribute asIndex
so that(index or model.Meta).projection
can be used interchangeablyNew
Stream
class exposes DynamoDBStreams API as a single iterable with powerful seek/jump options, and simple json-friendly tokens for pausing and resuming iteration.Over 1200 unit tests added
Initial integration tests added
(internal)
bloop.conditions.ReferenceTracker
handles building#n0
,:v1
, and associated values. Useany_ref
to build a reference to a name/path/value, andpop_refs
when backtracking (eg. when a value is actually another column, or when correcting a partially valid condition)(internal)
bloop.conditions.render
is the preferred entry point for rendering, and handles all permutations of conditions, filters, projections. Use overConditionRenderer
unless you need very specific control over rendering sequencing.(internal)
bloop.session.SessionWrapper
exposes DynamoDBStreams operations in addition to previousbloop.Client
wrappers around DynamoDB client(internal) New supporting classes
streams.buffer.RecordBuffer
,streams.shard.Shard
, andstreams.coordinator.Coordinator
to encapsulate the hell^Wjoy that is working with DynamoDBStreams(internal) New class
util.Sentinel
for placeholder values likemissing
andlast_token
that provide clearer docstrings, instead of showingfunc(..., default=object<0x...>)
these will showfunc(..., default=Sentinel<[Missing]>)
[Changed]¶
bloop.Column
emitsobject_modified
on__set__
and__del__
Conditions now check if they can be used with a column's
typedef
and raiseInvalidCondition
when they can't. For example,contains
can't be used onNumber
, nor>
onSet(String)
bloop.Engine
no longer takes an optionalbloop.Client
but instead optionaldynamodb
anddynamodbstreams
clients (usually created fromboto3.client("dynamodb")
etc.)Engine
no longer takes**config
-- its settings have been dispersed to their local touch pointsatomic
is a parameter ofsave
anddelete
and defaults toFalse
consistent
is a parameter ofload
,query
,scan
and defaults toFalse
prefetch
has no equivalent, and is baked into the new Query/Scan iterator logicstrict
is a parameter of aLocalSecondaryIndex
, defaults toTrue
Engine
no longer has acontext
to create temporary views with different configurationEngine.bind
is no longer by keyword arg only:engine.bind(MyBase)
is acceptable in addition toengine.bind(base=MyBase)
Engine.bind
emits new signalsbefore_create_table
,model_validated
, andmodel_bound
Engine.delete
andEngine.save
take*objs
instead ofobjs
to easily save/delete small multiples of objects (engine.save(user, tweet)
instead ofengine.save([user, tweet])
)Engine
guards against loading, saving, querying, etc against abstract modelsEngine.load
raisesMissingObjects
instead ofNotModified
(exception rename)Engine.scan
andEngine.query
take all query and scan arguments immediately, instead of using the builder pattern. For example,engine.scan(model).filter(Model.x==3)
has becomeengine.scan(model, filter=Model.x==3)
.bloop.exceptions.NotModified
renamed tobloop.exceptions.MissingObjects
Any code that raised
AbstractModelException
now raisesUnboundModel
bloop.types.DateTime
is now backed bydatetime.datetime
instead ofarrow
. Only supports UTC now, no local timezone. Use thebloop.ext.arrow.DateTime
class to continue usingarrow
.The query and scan interfaces have been entirely refactored:
count
,consistent
,ascending
and other properties are part of theEngine.query(...)
parameters.all()
is no longer needed, asEngine.scan
and.query
immediately return an iterable object. There is noprefetch
setting, orlimit
.The
complete
property for Query and Scan have been replaced withexhausted
, to be consistent with the Stream moduleThe query and scan iterator no longer cache results
The
projection
parameter is now required forGlobalSecondaryIndex
andLocalSecondaryIndex
Calling
Index.__set__
orIndex.__del__
will raiseAttributeError
. For example,some_user.by_email = 3
raises ifUser.by_email
is a GSIbloop.Number
replacesbloop.Float
and takes an optionaldecimal.Context
for converting numbers. For a less strict, lossyFloat
type see the Patterns section of the User Guidebloop.String.dynamo_dump
no longer callsstr()
on the value, which was hiding bugs where a non-string object was passed (eg.some_user.name = object()
would save with a name of<object <0x...>
)bloop.DateTime
is now backed bydatetime.datetime
and only knows UTC in a fixed format. Adapters forarrow
,delorean
, andpendulum
are available inbloop.ext
bloop.DateTime
does not support naive datetimes; they must always have atzinfo
docs:
use RTD theme
rewritten three times
now includes public and internal api references
(internal) Path lookups on
Column
(eg.User.profile["name"]["last"]
) use simpler proxies(internal) Proxy behavior split out from
Column
's base classbloop.conditions.ComparisonMixin
for a cleaner namespace(internal)
bloop.conditions.ConditionRenderer
rewritten, uses a newbloop.conditions.ReferenceTracker
with a much clearer api(internal)
ConditionRenderer
can backtrack references and handles columns as values (eg.User.name.in_([User.email, "literal"])
)(internal)
_MultiCondition
logic rolled intobloop.conditions.BaseCondition
,AndCondition
andOrCondition
no longer have intermediate base class(internal)
AttributeExists
logic rolled intobloop.conditions.ComparisonCondition
(internal)
bloop.tracking
rolled intobloop.conditions
and is hooked into theobject_*
signals. Methods are no longer called directly (eg. no need fortracking.sync(some_obj, engine)
)(internal) update condition is built from a set of columns, not a dict of updates to apply
(internal)
bloop.conditions.BaseCondition
is a more comprehensive base class, and handles all manner of out-of-order merges (and(x, y)
vsand(y, x)
where x is anand
condition and y is not)(internal) almost all
*Condition
classes simply implement__repr__
andrender
;BaseCondition
takes care of everything else(internal)
bloop.Client
becamebloop.session.SessionWrapper
(internal)
Engine._dump
takes an optionalcontext
,**kwargs
, matching the signature ofEngine._load
(internal)
BaseModel
no longer implements__hash__
,__eq__
, or__ne__
butModelMetaclass
will always ensure a__hash__
function when the subclass is created(internal)
Filter
andFilterIterator
rewritten entirely in thebloop.search
module across multiple classes
[Removed]¶
AbstractModelException
has been rolled intoUnboundModel
The
all()
method has been removed from the query and scan iterator interface. Simply iterate withnext(query)
orfor result in query:
Query.results
andScan.results
have been removed and results are no longer cached. You can begin the search again withquery.reset()
The
new_base()
function has been removed in favor of subclassingBaseModel
directlybloop.Float
has been replaced bybloop.Number
(internal)
bloop.engine.LoadManager
logic was rolled intobloop.engine.load(...)
EngineView
has been removed since engines no longer have a baselineconfig
and don't need a context to temporarily modify it(internal)
Engine._update
has been removed in favor ofutil.unpack_from_dynamodb
(internal)
Engine._instance
has been removed in favor of directly creating instances frommodel.Meta.init()
inunpack_from_dynamodb
[Fixed]¶
Column.contains(value)
now rendersvalue
with the column typedef's inner type. Previously, the container type was used, soData.some_list.contains("foo"))
would render as(contains(some_list, ["f", "o", "o"]))
instead of(contains(some_list, "foo"))
Set
renders correct wire format -- previously, it incorrectly sent{"SS": [{"S": "h"}, {"S": "i"}]}
instead of the correct{"SS": ["h", "i"]}
(internal)
Set
andList
expose aninner_typedef
for conditions to force rendering of inner values (currently only used byContainsCondition
)
0.9.13 - 2016-10-31¶
[Fixed]¶
Set
was rendering an invalid wire format, and now renders the correct "SS", "NS", or "BS" values.Set
andList
were renderingcontains
conditions incorrectly, by trying to dump each value in the value passed to contains. For example,MyModel.strings.contains("foo")
would rendercontains(#n0, :v1)
where:v1
was{"SS": [{"S": "f"}, {"S": "o"}, {"S": "o"}]}
. Now, non-iterable values are rendered singularly, so:v1
would be{"S": "foo"}
. This is a temporary fix, and only works for simple cases. For example,List(List(String))
will still break when performing acontains
check. This is fixed correctly in 1.0.0 and you should migrate as soon as possible.
0.9.12 - 2016-06-13¶
[Added]¶
model.Meta
now exposesgsis
andlsis
, in addition to the existingindexes
. This simplifies code that needs to iterate over each type of index and not all indexes.
[Removed]¶
engine_for_profile
was no longer necessary, since the client instances could simply be created with a given profile.
0.9.11 - 2016-06-12¶
[Changed]¶
bloop.Client
now takesboto_client
, which should be an instance ofboto3.client("dynamodb")
instead of aboto3.session.Session
. This lets you specify endpoints and other configuration only exposed during the client creation process.Engine
no longer uses"session"
from the config, and instead takes aclient
param which should be an instance ofbloop.Client
. bloop.Client will be going away in 1.0.0 and Engine will simply take the boto3 clients directly.
0.9.10 - 2016-06-07¶
[Added]¶
New exception
AbstractModelException
is raised when attempting to perform an operation which requires a table, on an abstract model. Raised by all Engine functions as well asbloop.Client
operations.
[Changed]¶
Engine
operations raiseAbstractModelException
when attempting to perform operations on abstract models.Previously, models were considered non-abstract if
model.Meta.abstract
was False, or there was no value. Now,ModelMetaclass
will explicitly setabstract
to False so thatmodel.Meta.abstract
can be used everywhere, instead ofgetattr(model.Meta, "abstract", False)
.
0.9.9 - 2016-06-06¶
[Added]¶
Column
has a new attributemodel
, the model it is bound to. This is set during the model's creation by theModelMetaclass
.
[Changed]¶
Engine.bind
will now skip intermediate models that are abstract. This makes it easier to pass abstract models, or models whose subclasses may be abstract (and have non-abstract grandchildren).
0.9.8 - 2016-06-05¶
(no public changes)
0.9.7 - 2016-06-05¶
[Changed]¶
Conditions implement
__eq__
for checking if two conditions will evaluate the same. For example:>>> large = Blob.size > 1024**2 >>> small = Blob.size < 1024**2 >>> large == small False >>> also_large = Blob.size > 1024**2 >>> large == also_large True >>> large is also_large False
0.9.6 - 2016-06-04¶
0.9.6 is the first significant change to how Bloop binds models, engines, and tables. There are a few breaking changes, although they should be easy to update.
Where you previously created a model from the Engine's model:
from bloop import Engine
engine = Engine()
class MyModel(engine.model):
...
You'll now create a base without any relation to an engine, and then bind it to any engines you want:
from bloop import Engine, new_base
BaseModel = new_base()
class MyModel(BaseModel):
...
engine = Engine()
engine.bind(base=MyModel) # or base=BaseModel
[Added]¶
A new function
engine_for_profile
takes a profile name for the config file and creates an appropriate session. This is a temporary utility, sinceEngine
will eventually take instances of dynamodb and dynamodbstreams clients. This will be going away in 1.0.0.A new base exception
BloopException
which can be used to catch anything thrown by Bloop.A new function
new_base()
creates an abstract base for models. This replacesEngine.model
now that multiple engines can bind the same model. This will be going away in 1.0.0 which will provide aBaseModel
class.
[Changed]¶
The
session
parameter toEngine
is now part of theconfig
kwargs. The underlyingbloop.Client
is no longer created inEngine.__init__
, which provides an opportunity to swap out the client entirely before the firstEngine.bind
call. The semantics of session and client are unchanged.Engine._load
,Engine._dump
, and all Type signatures now pass an engine explicitly through thecontext
parameter. This was mentioned in 0.9.2 andcontext
is now required.Engine.bind
now binds the given class and all subclasses. This simplifies most workflows, since you can now create a base withMyBase = new_base()
and then bind every model you create withengine.bind(base=MyBase)
.All exceptions now subclass a new base exception
BloopException
instead ofException
.Vector types
Set
,List
,Map
, andTypedMap
accept a typedef ofNone
so they can raise a more helpful error message. This will be reverted in 1.0.0 and will once again be a required parameter.
[Removed]¶
Engine no longer has
model
,unbound_models
, ormodels
attributes.Engine.model
has been replaced by thenew_base()
function, and models are bound directly to the underlying type engine without tracking on theEngine
instance itself.EngineView dropped the corresponding attributes above.
0.9.5 - 2016-06-01¶
[Changed]¶
EngineView
attributes are now properties, and point to the underlying engine's attributes; this includesclient
,model
,type_engine
, andunbound_models
. This fixed an issue when usingwith engine.context(...) as view:
to perform operations on models bound to the engine but not the engine view. EngineView will be going away in 1.0.0.
0.9.4 - 2015-12-31¶
[Added]¶
Engine functions now take optional config parameters to override the engine's config. You should update your code to use these values instead of
engine.config
, since engine.config is going away in 1.0.0.Engine.delete
andEngine.save
expose theatomic
parameter, whileEngine.load
exposesconsistent
.Added the
TypedMap
class, which provides dict mapping for a single typedef over any number of keys. This differs fromMap
, which must know all keys ahead of time and can use different types.TypedMap
only supports a single type, but can have arbitrary keys. This will be going away in 1.0.0.
0.9.2 - 2015-12-11¶
[Changed]¶
Type functions
_load
,_dump
,dynamo_load
,dynamo_dump
now take an optional keyword-only argcontext
. This dict will become required in 0.9.6, and contains the engine instance that should be used for recursive types. If your type currently usescls.Meta.bloop_engine
, you should start usingcontext["engine"]
in the next release. Thebloop_engine
attribute is being removed, since models will be able to bind to multiple engines.
0.9.1 - 2015-12-07¶
(no public changes)