Skip to content

Commit

Permalink
Merge branch 'release/4.0.0'
Browse files Browse the repository at this point in the history
  • Loading branch information
johnbywater committed Dec 11, 2017
2 parents 881c0a6 + 9205ab4 commit 1dd9c1d
Show file tree
Hide file tree
Showing 108 changed files with 3,773 additions and 4,307 deletions.
2 changes: 2 additions & 0 deletions .readthedocs → .readthedocs.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
python:
version: 3
pip_install: true
extra_requirements:
- docs
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ python:

install:
- pip install -U pip wheel
- CASS_DRIVER_NO_CYTHON=1 pip install -e .[cassandra,sqlalchemy,crypto,testing]
- CASS_DRIVER_NO_CYTHON=1 pip install -e .[testing]
- pip install python-coveralls

env:
Expand Down
168 changes: 160 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,171 @@ A library for event sourcing in Python.
Use pip to install the [stable distribution](https://pypi.python.org/pypi/eventsourcing) from
the Python Package Index.

pip install eventsourcing
$ pip install eventsourcing

If you want to use SQLAlchemy, then please install with 'sqlalchemy'.
Please refer to [the documentation](http://eventsourcing.readthedocs.io/) for installation and usage guides.

pip install eventsourcing[sqlalchemy]
# Features

Similarly, if you want to use Cassandra, then please install with 'cassandra'.
**Event store** — appends and retrieves domain events. Uses a
sequenced item mapper with an active record strategy to map domain events
to databases in ways that can be easily extended and replaced.

pip install eventsourcing[cassandra]
**Data integrity** - stored events can be hashed to check data integrity of individual
records, so you cannot lose information in transit or get database corruption without
being able to detect it. Sequences of events can be hash-chained, and the entire sequence
of events checked for integrity, so if the last hash can be independently validated, then
so can the entire sequence.

## Documentation
**Optimistic concurrency control** — can be used to ensure a distributed or
horizontally scaled application doesn't become inconsistent due to concurrent
method execution. Leverages any optimistic concurrency controls in the database
adapted by the active record strategy.

Please refer to [the documentation](http://eventsourcing.readthedocs.io/) for installation and usage guides.
**Application-level encryption** — encrypts and decrypts stored events, using a cipher
strategy passed as an option to the sequenced item mapper. Can be used to encrypt some
events, or all events, or not applied at all (the default).

**Snapshotting** — avoids replaying an entire event stream to
obtain the state of an entity. A snapshot strategy is included which reuses
the capabilities of this library by implementing snapshots as events.

**Abstract base classes** — suggest how to structure an event sourced application.
The library has base classes for application objects, domain entities, entity repositories,
domain events of various types, mapping strategies, snapshotting strategies, cipher strategies,
etc. They are well factored, relatively simple, and can be easily extended for your own
purposes. If you wanted to create a domain model that is entirely stand-alone (recommended by
purists for maximum longevity), you might start by replicating the library classes.

**Worked examples** — a simple example application, with an example entity class,
example domain events, and an example database table. Plus lots of examples in the documentation.


## Synopsis

```python
from eventsourcing.domain.model.aggregate import AggregateRoot

class World(AggregateRoot):

def __init__(self, **kwargs):
super(World, self).__init__(**kwargs)
self.history = []

def make_it_so(self, something):
self.__trigger_event__(World.SomethingHappened, what=something)

class SomethingHappened(AggregateRoot.Event):
def mutate(self, obj):
obj.history.append(self)
```

Generate cipher key.

```python
from eventsourcing.utils.random import encode_random_bytes

# Keep this safe.
cipher_key = encode_random_bytes(num_bytes=32)
```

Configure environment variables.

```python
import os

# Cipher key (random bytes encoded with Base64).
os.environ['CIPHER_KEY'] = cipher_key

# SQLAlchemy-style database connection string.
os.environ['DB_URI'] = 'sqlite:///:memory:'
```

Run the code.

```python
from eventsourcing.application.simple import SimpleApplication
from eventsourcing.exceptions import ConcurrencyError

# Construct simple application (used here as a context manager).
with SimpleApplication() as app:

# Call library factory method.
world = World.__create__()

# Execute commands.
world.make_it_so('dinosaurs')
world.make_it_so('trucks')

version = world.__version__ # note version at this stage
world.make_it_so('internet')

# View current state of aggregate.
assert world.history[2].what == 'internet'
assert world.history[1].what == 'trucks'
assert world.history[0].what == 'dinosaurs'

# Publish pending events (to persistence subscriber).
world.__save__()

# Retrieve aggregate (replay stored events).
copy = app.repository[world.id]
assert isinstance(copy, World)

# View retrieved state.
assert copy.history[2].what == 'internet'
assert copy.history[1].what == 'trucks'
assert copy.history[0].what == 'dinosaurs'

# Verify retrieved state (cryptographically).
assert copy.__head__ == world.__head__

# Discard aggregate.
world.__discard__()

# Repository raises key error (when aggregate not found).
assert world.id not in app.repository
try:
app.repository[world.id]
except KeyError:
pass
else:
raise Exception("Shouldn't get here")

# Get historical state (at version from above).
old = app.repository.get_entity(world.id, at=version)
assert old.history[-1].what == 'trucks' # internet not happened
assert len(old.history) == 2

# Optimistic concurrency control (no branches).
old.make_it_so('future')
try:
old.__save__()
except ConcurrencyError:
pass
else:
raise Exception("Shouldn't get here")

# Check domain event data integrity (happens also during replay).
events = app.event_store.get_domain_events(world.id)
last_hash = ''
for event in events:
event.__check_hash__()
assert event.__previous_hash__ == last_hash
last_hash = event.__event_hash__

# Verify sequence of events (cryptographically).
assert last_hash == world.__head__

# Check records are encrypted (values not visible in database).
active_record_strategy = app.event_store.active_record_strategy
items = active_record_strategy.get_items(world.id)
for item in items:
assert item.originator_id == world.id
assert 'dinosaurs' not in item.state
assert 'trucks' not in item.state
assert 'internet' not in item.state
```

## Project

Expand All @@ -32,4 +183,5 @@ Please [register your questions, requests and any other issues](https://github.c

## Slack Channel

There is a [Slack channel](https://eventsourcinginpython.slack.com/messages/@slackbot/) for this project, which you are [welcome to join](https://join.slack.com/t/eventsourcinginpython/shared_invite/enQtMjczNTc2MzcxNDI0LTUwZGQ4MDk0ZDJmZmU0MjM4MjdmOTBlZGI0ZTY4NWIxMGFkZTcwNmUxM2U4NGM3YjY5MTVmZTBiYzljZjI3ZTE).
There is a [Slack channel](https://eventsourcinginpython.slack.com/messages/) for this project, which you
are [welcome to join](https://join.slack.com/t/eventsourcinginpython/shared_invite/enQtMjczNTc2MzcxNDI0LTUwZGQ4MDk0ZDJmZmU0MjM4MjdmOTBlZGI0ZTY4NWIxMGFkZTcwNmUxM2U4NGM3YjY5MTVmZTBiYzljZjI3ZTE).
2 changes: 1 addition & 1 deletion docs/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ help:


html-live:
sphinx-autobuild -z topics -z topics/user_guide -z ../eventsourcing $(SOURCEDIR) $(BUILDDIR)/html
sphinx-autobuild -z topics -z topics/examples -z ../eventsourcing $(SOURCEDIR) $(BUILDDIR)/html


# Catch-all target: route all unknown targets to Sphinx using the new
Expand Down
9 changes: 9 additions & 0 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
# -*- coding: utf-8 -*-

import sys
import types
from os.path import abspath, dirname
import sphinx_rtd_theme

Expand Down Expand Up @@ -172,3 +173,11 @@



def skip(app, what, name, obj, skip, options):
if getattr(obj, '__doc__', None) and isinstance(obj, (types.FunctionType, types.MethodType)):
return False
else:
return skip

def setup(app):
app.connect("autodoc-skip-member", skip)
27 changes: 5 additions & 22 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,7 @@ describes the :doc:`design </topics/design>` of the software,
the :doc:`infrastructure layer </topics/infrastructure>`,
the :doc:`domain model layer </topics/domainmodel>`,
the :doc:`application layer </topics/application>`, and
has :doc:`examples </topics/examples/index>` and
some :doc:`background </topics/background>` information about the project.
has some :doc:`background </topics/background>` information about the project.

This project is `hosted on GitHub <https://github.com/johnbywater/eventsourcing>`__.
Please `register any issues, questions, and requests
Expand All @@ -43,25 +42,9 @@ Please `register any issues, questions, and requests
topics/infrastructure
topics/domainmodel
topics/application
topics/examples/index

topics/snapshotting
topics/minimal
topics/notifications
topics/deployment
topics/release_notes


Reference
=========

* :ref:`search`
* :ref:`genindex`
* :ref:`modindex`


Modules
=======

.. toctree::
:maxdepth: 1

ref/modules


0 comments on commit 1dd9c1d

Please sign in to comment.