Skip to content

Commit

Permalink
Fixed README.md. Updated package to use eventsourcing 9.3.5, which do…
Browse files Browse the repository at this point in the history
…esn't depend on database tables having been created when application is constructed. Also reworked discussion in README.md to improve explanation, and include suggestion about putting application environment variables in Django settings file.
  • Loading branch information
johnbywater committed Nov 9, 2024
1 parent 780b666 commit 70648cd
Show file tree
Hide file tree
Showing 13 changed files with 326 additions and 90 deletions.
210 changes: 164 additions & 46 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
# Event Sourcing with Django

This package supports using the Python
[eventsourcing](https://github.com/pyeventsourcing/eventsourcing) library
with [Django ORM](https://www.djangoproject.com/).
This package is a Django app that supports using the Python
[eventsourcing](https://github.com/pyeventsourcing/eventsourcing) library with the [Django ORM](https://www.djangoproject.com/).

To use Django with your Python eventsourcing applications:
* install the Python package `eventsourcing_django`
Expand All @@ -21,24 +20,13 @@ install Python packages into a Python virtual environment.

$ pip install eventsourcing_django

Alternatively, add `eventsourcing_django` to your project's `pyproject.yaml`
or `requirements.txt` file and update your virtual environment accordingly.

## Django
## Event sourcing application

Add `'eventsourcing_django'` to your Django project's `INSTALLED_APPS` setting.

INSTALLED_APPS = [
...
'eventsourcing_django',
]

Run Django's `manage.py migrate` command to create database tables for storing events.

$ python manage.py migrate eventsourcing_django


## Event sourcing

Define aggregates and applications in the usual way.
Define event-sourced aggregates and applications using the `Application` and
`Aggregate` classes from the `eventsourcing` package.

```python
from eventsourcing.application import Application
Expand Down Expand Up @@ -75,63 +63,193 @@ class Dog(Aggregate):
def add_trick(self, trick):
self.tricks.append(trick)
```
Construct and use the application in the usual way.
Set `PERSISTENCE_MODULE` to `'eventsourcing_django'`
in the application's environment.
You may wish to construct the application object on a signal
when the Django project is "ready". You can use the `ready()`
method of the `AppConfig` class in the `apps.py` module of a
Django app. If you migrate before including the TrainingSchool object into your code,
this way should work fine in development:

The event sourcing application can be developed and tested independently of Django.

Next, let's configure a Django project, and our event sourcing application, so
that events of the event sourcing application are stored in a Django database.

## Django project settings

Add `'eventsourcing_django'` to your Django project's `INSTALLED_APPS` setting.

INSTALLED_APPS = [
...
'eventsourcing_django',
]

This will make the Django models for storing events available in your Django project,
and allow Django to create tables in your database for storing events.

## Django database migration

Run Django's `manage.py migrate` command to create database tables for storing events.

$ python manage.py migrate

Use the `--database` option to create tables in a non-default database. The database
alias must be a key in the `DATABASES` setting of your Django project.

$ python manage.py migrate --database=postgres

Alternatively, after the Django framework has been set up for your project, you
can call Django's `call_command()` function to create the database tables.

```python
school = TrainingSchool(env={
"PERSISTENCE_MODULE": "eventsourcing_django",
})
from django.core.management import call_command

call_command("migrate")
```

Use the `database` keyword argument to create tables in a non-default database.

```python
call_command("migrate", database="postgres")
```

To set up the Django framework for your Django project, `django.setup()` must have
been called after setting environment variable `DJANGO_SETTINGS_MODULE` to indicate the
settings module of your Django project. This is often done by a Django project's
`manage.py`, `wsgi.py`, and `wsgi.py` files, and by tools that support Django users
such as test suite runners provided by IDEs that support Django. Django test suites
usually automatically create and migrate databases when tests are run.

## Event sourcing in Django

The event sourcing application can be configured to store events in the Django project's
database by setting the environment variable `'PERSISTENCE_MODULE'` to
`'eventsourcing_django'`. This step also depends on the Django framework having been
set up to for your Django project, but it doesn't depend on the database tables having
been created.

```python
training_school = TrainingSchool(
env={"PERSISTENCE_MODULE": "eventsourcing_django"},
)
```

Use the application environment variable `'DJANGO_DB_ALIAS'` to configure the application
to store events in a non-default Django project database. The value of `'DJANGO_DB_ALIAS'`
must correspond to one of the keys in the `DATABASES` setting of the Django project.

```python
training_school = TrainingSchool(
env={
"PERSISTENCE_MODULE": "eventsourcing_django",
"DJANGO_DB_ALIAS": "postgres",
}
)
```

But usually you need migrations to run before creating the objects from database data
and also put the created object into django app config:
You may wish to define your event sourcing application in a separate Django app,
and construct your event sourcing application in a Django `AppConfig` subclass
in its `apps.py` module.

```python
# In your apps.py file.
from django.apps import AppConfig
from django.core.management import call_command

class TrainingSchoolConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "<project_name>.training_school"
name = "<django-project-name>.training_school"

def ready(self):
call_command("migrate")
self.create_training_school()

def create_training_school(self):
training_school = TrainingSchool(
self.training_school = TrainingSchool(
env={"PERSISTENCE_MODULE": "eventsourcing_django"}
)
apps.get_app_config("training_school").training_school = training_school

```

You may also wish to centralize the definition of your event sourcing application's
environment variables in your Django project's settings module, and use this when
constructing the event sourcing application.

```python
# Create secret cipher key.
import os
from eventsourcing.cipher import AESCipher
os.environ["CIPHER_KEY"] = AESCipher.create_key(32)

# In your settings.py file.
import os

EVENT_SOURCING_APPLICATION = {
"PERSISTENCE_MODULE": "eventsourcing_django",
"DJANGO_DB_ALIAS": "postgres",
"IS_SNAPSHOTTING_ENABLED": "y",
"COMPRESSOR_TOPIC": "eventsourcing.compressor:ZlibCompressor",
"CIPHER_TOPIC": "eventsourcing.cipher:AESCipher",
"CIPHER_KEY": os.environ["CIPHER_KEY"],
}

# In your apps.py file.
from django.apps import AppConfig
from django.conf import settings

class TrainingSchoolConfig(AppConfig):
name = "<django-project-name>.training_school"

def ready(self):
self.training_school = TrainingSchool(env=settings.EVENT_SOURCING_APPLICATION)
```

And then use it like:
The single instance of the event sourcing application can then be obtained in other
places, such as views, forms, management commands, and tests.

```python
school = apps.get_app_config("training_school").training_school
from django.apps import apps

training_school = apps.get_app_config("training_school").training_school
```

The application's methods may be called from Django views and forms.
The event sourcing application's methods can be called in views, forms,
management commands, and tests.

```python
school.register('Fido')
school.add_trick('Fido', 'roll over')
school.add_trick('Fido', 'play dead')
tricks = school.get_tricks('Fido')
training_school.register('Fido')

training_school.add_trick('Fido', 'roll over')
training_school.add_trick('Fido', 'play dead')

tricks = training_school.get_tricks('Fido')
assert tricks == ['roll over', 'play dead']
```

Events will be stored in the Django project's database, so long as the
database tables have been created before the event sourcing application
methods are called. If the database tables have not been created, an
`OperationalError` will be raised to indicate that the tables are not found.

## Summary

In summary, before constructing an event sourcing application with `eventsourcing_django`
as its persistence module, the Django framework must have been set up for a Django
project that has `'eventsourcing_django'` included in its `INSTALLED_APPS` setting.
And, before calling the methods of the event sourcing application, the Django project's
database must have been migrated.

For more information, please refer to the Python
[eventsourcing](https://github.com/johnbywater/eventsourcing) library
and the [Django](https://www.djangoproject.com/) project.


## Management Commands
## Management commands

The `eventsourcing_django` package is a Django app which ships with the following
Django management commands. They are available in Django projects that have
`'eventsourcing_django'` included in their `INSTALLED_APPS` setting.

At the moment, there is only one management command: `sync_followers`.

The Django app `eventsourcing_django` ships with the following management commands.
The `sync_followers` management command helps users of the `eventsourcing.system`
module. Please refer to the `eventsourcing` package docs for more information
about the `eventsourcing.system` module.

### Synchronise Followers
### Synchronise followers

Manually synchronise followers (i.e. `ProcessApplication` instances) with all of their
leaders, as defined in the `eventsourcing.system.System`'s pipes.
Expand Down
31 changes: 31 additions & 0 deletions eventsourcing_django/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# -*- coding: utf-8 -*-
from typing import Any

from eventsourcing.persistence import (
AggregateRecorder,
ApplicationRecorder,
InfrastructureFactory,
OperationalError,
ProcessRecorder,
)


class Factory(InfrastructureFactory):
def __init__(self, **kwargs: Any) -> None:
msg = (
"Django app not ready. Please call django.setup() after setting "
"environment variable DJANGO_SETTINGS_MODULE to the settings module of a "
"Django project that has 'eventsourcing_django' included in its "
"INSTALLED_APPS setting, and ensure the Django project's database has been "
"migrated before calling the methods of your event sourcing application."
)
raise OperationalError(msg)

def aggregate_recorder(self, purpose: str = "events") -> AggregateRecorder:
raise NotImplementedError()

def application_recorder(self) -> ApplicationRecorder:
raise NotImplementedError()

def process_recorder(self) -> ProcessRecorder:
raise NotImplementedError()
18 changes: 9 additions & 9 deletions eventsourcing_django/recorders.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,23 +72,23 @@ def _wrapper(*args: Any, **kwargs: Any) -> Any:
try:
return f(*args, **kwargs)
except django.db.InterfaceError as e:
raise InterfaceError from e
raise InterfaceError(e) from e
except django.db.DataError as e:
raise DataError from e
raise DataError(e) from e
except django.db.OperationalError as e:
raise OperationalError from e
raise OperationalError(e) from e
except django.db.IntegrityError as e:
raise IntegrityError from e
raise IntegrityError(e) from e
except django.db.InternalError as e:
raise InternalError from e
raise InternalError(e) from e
except django.db.ProgrammingError as e:
raise ProgrammingError from e
raise ProgrammingError(e) from e
except django.db.NotSupportedError as e:
raise NotSupportedError from e
raise NotSupportedError(e) from e
except django.db.DatabaseError as e:
raise DatabaseError from e
raise DatabaseError(e) from e
except django.db.Error as e:
raise PersistenceError from e
raise PersistenceError(e) from e

return _wrapper

Expand Down
8 changes: 4 additions & 4 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ include = ["eventsourcing_django/py.typed"]

[tool.poetry.dependencies]
python = "^3.8"
eventsourcing = { version = "~9.3" }
eventsourcing = { version = ">=9.3.5,<9.4" }
#eventsourcing = { path = "../eventsourcing/", extras = ["crypto"], develop = true }
#eventsourcing = { git = "https://github.com/pyeventsourcing/eventsourcing.git", branch = "main", extras = ["crypto"]}

Expand Down
4 changes: 2 additions & 2 deletions tests/djangoproject/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"eventsourcing_django.apps.EventsourcingConfig",
"eventsourcing_django",
"tests.eventsourcing_runner_django.apps.EventSourcingSystemRunnerConfig",
]

Expand Down Expand Up @@ -111,7 +111,7 @@
"ENGINE": "django.db.backends.postgresql_psycopg2",
"NAME": os.getenv("POSTGRES_DB", "eventsourcing_django"),
"TEST": {
"NAME": "test_" + os.getenv("POSTGRES_DB", "eventsourcing_django"),
"NAME": os.getenv("POSTGRES_DB", "eventsourcing_django"),
},
"USER": os.getenv("POSTGRES_USER", "eventsourcing"),
"PASSWORD": os.getenv("POSTGRES_PASSWORD", "eventsourcing"),
Expand Down
15 changes: 15 additions & 0 deletions tests/djangoproject/settings_testdocs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# -*- coding: utf-8 -*-
from eventsourcing.cipher import AESCipher

from tests.djangoproject.settings import * # noqa: F403

INSTALLED_APPS = INSTALLED_APPS + ["tests.training_school"] # noqa: F405

EVENT_SOURCING_SETTINGS = EVENT_SOURCING_APPLICATION = {
"PERSISTENCE_MODULE": "eventsourcing_django",
"DJANGO_DB_ALIAS": "postgres",
"IS_SNAPSHOTTING_ENABLED": "y",
"COMPRESSOR_TOPIC": "eventsourcing.compressor:ZlibCompressor",
"CIPHER_TOPIC": "eventsourcing.cipher:AESCipher",
"CIPHER_KEY": AESCipher.create_key(num_bytes=32),
}
Loading

0 comments on commit 70648cd

Please sign in to comment.