eventsourcing 9.2.22__py3-none-any.whl → 9.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of eventsourcing might be problematic. Click here for more details.
- eventsourcing/__init__.py +1 -1
- eventsourcing/application.py +116 -135
- eventsourcing/cipher.py +15 -12
- eventsourcing/dispatch.py +31 -91
- eventsourcing/domain.py +220 -226
- eventsourcing/examples/__init__.py +0 -0
- eventsourcing/examples/aggregate1/__init__.py +0 -0
- eventsourcing/examples/aggregate1/application.py +27 -0
- eventsourcing/examples/aggregate1/domainmodel.py +16 -0
- eventsourcing/examples/aggregate1/test_application.py +37 -0
- eventsourcing/examples/aggregate2/__init__.py +0 -0
- eventsourcing/examples/aggregate2/application.py +27 -0
- eventsourcing/examples/aggregate2/domainmodel.py +22 -0
- eventsourcing/examples/aggregate2/test_application.py +37 -0
- eventsourcing/examples/aggregate3/__init__.py +0 -0
- eventsourcing/examples/aggregate3/application.py +27 -0
- eventsourcing/examples/aggregate3/domainmodel.py +38 -0
- eventsourcing/examples/aggregate3/test_application.py +37 -0
- eventsourcing/examples/aggregate4/__init__.py +0 -0
- eventsourcing/examples/aggregate4/application.py +27 -0
- eventsourcing/examples/aggregate4/domainmodel.py +114 -0
- eventsourcing/examples/aggregate4/test_application.py +38 -0
- eventsourcing/examples/aggregate5/__init__.py +0 -0
- eventsourcing/examples/aggregate5/application.py +27 -0
- eventsourcing/examples/aggregate5/domainmodel.py +131 -0
- eventsourcing/examples/aggregate5/test_application.py +38 -0
- eventsourcing/examples/aggregate6/__init__.py +0 -0
- eventsourcing/examples/aggregate6/application.py +30 -0
- eventsourcing/examples/aggregate6/domainmodel.py +123 -0
- eventsourcing/examples/aggregate6/test_application.py +38 -0
- eventsourcing/examples/aggregate6a/__init__.py +0 -0
- eventsourcing/examples/aggregate6a/application.py +40 -0
- eventsourcing/examples/aggregate6a/domainmodel.py +149 -0
- eventsourcing/examples/aggregate6a/test_application.py +45 -0
- eventsourcing/examples/aggregate7/__init__.py +0 -0
- eventsourcing/examples/aggregate7/application.py +48 -0
- eventsourcing/examples/aggregate7/domainmodel.py +144 -0
- eventsourcing/examples/aggregate7/persistence.py +57 -0
- eventsourcing/examples/aggregate7/test_application.py +38 -0
- eventsourcing/examples/aggregate7/test_compression_and_encryption.py +45 -0
- eventsourcing/examples/aggregate7/test_snapshotting_intervals.py +67 -0
- eventsourcing/examples/aggregate7a/__init__.py +0 -0
- eventsourcing/examples/aggregate7a/application.py +56 -0
- eventsourcing/examples/aggregate7a/domainmodel.py +170 -0
- eventsourcing/examples/aggregate7a/test_application.py +46 -0
- eventsourcing/examples/aggregate7a/test_compression_and_encryption.py +45 -0
- eventsourcing/examples/aggregate8/__init__.py +0 -0
- eventsourcing/examples/aggregate8/application.py +47 -0
- eventsourcing/examples/aggregate8/domainmodel.py +65 -0
- eventsourcing/examples/aggregate8/persistence.py +57 -0
- eventsourcing/examples/aggregate8/test_application.py +37 -0
- eventsourcing/examples/aggregate8/test_compression_and_encryption.py +44 -0
- eventsourcing/examples/aggregate8/test_snapshotting_intervals.py +38 -0
- eventsourcing/examples/bankaccounts/__init__.py +0 -0
- eventsourcing/examples/bankaccounts/application.py +70 -0
- eventsourcing/examples/bankaccounts/domainmodel.py +56 -0
- eventsourcing/examples/bankaccounts/test.py +173 -0
- eventsourcing/examples/cargoshipping/__init__.py +0 -0
- eventsourcing/examples/cargoshipping/application.py +126 -0
- eventsourcing/examples/cargoshipping/domainmodel.py +330 -0
- eventsourcing/examples/cargoshipping/interface.py +143 -0
- eventsourcing/examples/cargoshipping/test.py +231 -0
- eventsourcing/examples/contentmanagement/__init__.py +0 -0
- eventsourcing/examples/contentmanagement/application.py +118 -0
- eventsourcing/examples/contentmanagement/domainmodel.py +69 -0
- eventsourcing/examples/contentmanagement/test.py +180 -0
- eventsourcing/examples/contentmanagement/utils.py +26 -0
- eventsourcing/examples/contentmanagementsystem/__init__.py +0 -0
- eventsourcing/examples/contentmanagementsystem/application.py +54 -0
- eventsourcing/examples/contentmanagementsystem/postgres.py +17 -0
- eventsourcing/examples/contentmanagementsystem/sqlite.py +17 -0
- eventsourcing/examples/contentmanagementsystem/system.py +14 -0
- eventsourcing/examples/contentmanagementsystem/test_system.py +180 -0
- eventsourcing/examples/searchablecontent/__init__.py +0 -0
- eventsourcing/examples/searchablecontent/application.py +45 -0
- eventsourcing/examples/searchablecontent/persistence.py +23 -0
- eventsourcing/examples/searchablecontent/postgres.py +118 -0
- eventsourcing/examples/searchablecontent/sqlite.py +136 -0
- eventsourcing/examples/searchablecontent/test_application.py +110 -0
- eventsourcing/examples/searchablecontent/test_recorder.py +68 -0
- eventsourcing/examples/searchabletimestamps/__init__.py +0 -0
- eventsourcing/examples/searchabletimestamps/application.py +32 -0
- eventsourcing/examples/searchabletimestamps/persistence.py +20 -0
- eventsourcing/examples/searchabletimestamps/postgres.py +110 -0
- eventsourcing/examples/searchabletimestamps/sqlite.py +99 -0
- eventsourcing/examples/searchabletimestamps/test_searchabletimestamps.py +94 -0
- eventsourcing/examples/test_invoice.py +176 -0
- eventsourcing/examples/test_parking_lot.py +206 -0
- eventsourcing/interface.py +2 -2
- eventsourcing/persistence.py +85 -81
- eventsourcing/popo.py +30 -31
- eventsourcing/postgres.py +379 -590
- eventsourcing/sqlite.py +91 -99
- eventsourcing/system.py +52 -57
- eventsourcing/tests/application.py +20 -32
- eventsourcing/tests/application_tests/__init__.py +0 -0
- eventsourcing/tests/application_tests/test_application_with_automatic_snapshotting.py +55 -0
- eventsourcing/tests/application_tests/test_application_with_popo.py +22 -0
- eventsourcing/tests/application_tests/test_application_with_postgres.py +75 -0
- eventsourcing/tests/application_tests/test_application_with_sqlite.py +72 -0
- eventsourcing/tests/application_tests/test_cache.py +134 -0
- eventsourcing/tests/application_tests/test_event_sourced_log.py +162 -0
- eventsourcing/tests/application_tests/test_notificationlog.py +232 -0
- eventsourcing/tests/application_tests/test_notificationlogreader.py +126 -0
- eventsourcing/tests/application_tests/test_processapplication.py +110 -0
- eventsourcing/tests/application_tests/test_processingpolicy.py +109 -0
- eventsourcing/tests/application_tests/test_repository.py +504 -0
- eventsourcing/tests/application_tests/test_snapshotting.py +68 -0
- eventsourcing/tests/application_tests/test_upcasting.py +459 -0
- eventsourcing/tests/docs_tests/__init__.py +0 -0
- eventsourcing/tests/docs_tests/test_docs.py +293 -0
- eventsourcing/tests/domain.py +1 -1
- eventsourcing/tests/domain_tests/__init__.py +0 -0
- eventsourcing/tests/domain_tests/test_aggregate.py +1180 -0
- eventsourcing/tests/domain_tests/test_aggregate_decorators.py +1604 -0
- eventsourcing/tests/domain_tests/test_domainevent.py +80 -0
- eventsourcing/tests/interface_tests/__init__.py +0 -0
- eventsourcing/tests/interface_tests/test_remotenotificationlog.py +258 -0
- eventsourcing/tests/persistence.py +52 -50
- eventsourcing/tests/persistence_tests/__init__.py +0 -0
- eventsourcing/tests/persistence_tests/test_aes.py +93 -0
- eventsourcing/tests/persistence_tests/test_connection_pool.py +722 -0
- eventsourcing/tests/persistence_tests/test_eventstore.py +72 -0
- eventsourcing/tests/persistence_tests/test_infrastructure_factory.py +21 -0
- eventsourcing/tests/persistence_tests/test_mapper.py +113 -0
- eventsourcing/tests/persistence_tests/test_noninterleaving_notification_ids.py +69 -0
- eventsourcing/tests/persistence_tests/test_popo.py +124 -0
- eventsourcing/tests/persistence_tests/test_postgres.py +1119 -0
- eventsourcing/tests/persistence_tests/test_sqlite.py +348 -0
- eventsourcing/tests/persistence_tests/test_transcoder.py +44 -0
- eventsourcing/tests/postgres_utils.py +7 -7
- eventsourcing/tests/system_tests/__init__.py +0 -0
- eventsourcing/tests/system_tests/test_runner.py +935 -0
- eventsourcing/tests/system_tests/test_system.py +284 -0
- eventsourcing/tests/utils_tests/__init__.py +0 -0
- eventsourcing/tests/utils_tests/test_utils.py +226 -0
- eventsourcing/utils.py +47 -50
- {eventsourcing-9.2.22.dist-info → eventsourcing-9.3.0.dist-info}/METADATA +29 -79
- eventsourcing-9.3.0.dist-info/RECORD +145 -0
- {eventsourcing-9.2.22.dist-info → eventsourcing-9.3.0.dist-info}/WHEEL +1 -2
- eventsourcing-9.2.22.dist-info/RECORD +0 -25
- eventsourcing-9.2.22.dist-info/top_level.txt +0 -1
- {eventsourcing-9.2.22.dist-info → eventsourcing-9.3.0.dist-info}/AUTHORS +0 -0
- {eventsourcing-9.2.22.dist-info → eventsourcing-9.3.0.dist-info}/LICENSE +0 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from unittest import TestCase
|
|
4
|
+
|
|
5
|
+
from eventsourcing.cipher import AESCipher
|
|
6
|
+
from eventsourcing.examples.aggregate8.application import DogSchool
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class TestDogSchool(TestCase):
|
|
10
|
+
def test_dog_school(self) -> None:
|
|
11
|
+
# Construct application object.
|
|
12
|
+
school = DogSchool(
|
|
13
|
+
env={
|
|
14
|
+
"COMPRESSOR_TOPIC": "eventsourcing.compressor:ZlibCompressor",
|
|
15
|
+
"CIPHER_TOPIC": "eventsourcing.cipher:AESCipher",
|
|
16
|
+
"CIPHER_KEY": AESCipher.create_key(num_bytes=32),
|
|
17
|
+
}
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
# Evolve application state.
|
|
21
|
+
dog_id = school.register_dog("Fido")
|
|
22
|
+
school.add_trick(dog_id, "roll over")
|
|
23
|
+
school.add_trick(dog_id, "play dead")
|
|
24
|
+
|
|
25
|
+
# Query application state.
|
|
26
|
+
dog = school.get_dog(dog_id)
|
|
27
|
+
assert dog["name"] == "Fido"
|
|
28
|
+
assert dog["tricks"] == ("roll over", "play dead")
|
|
29
|
+
|
|
30
|
+
# Select notifications.
|
|
31
|
+
notifications = school.notification_log.select(start=1, limit=10)
|
|
32
|
+
assert len(notifications) == 3
|
|
33
|
+
|
|
34
|
+
# Take snapshot.
|
|
35
|
+
school.take_snapshot(dog_id, version=3)
|
|
36
|
+
dog = school.get_dog(dog_id)
|
|
37
|
+
assert dog["name"] == "Fido"
|
|
38
|
+
assert dog["tricks"] == ("roll over", "play dead")
|
|
39
|
+
|
|
40
|
+
# Continue with snapshotted aggregate.
|
|
41
|
+
school.add_trick(dog_id, "fetch ball")
|
|
42
|
+
dog = school.get_dog(dog_id)
|
|
43
|
+
assert dog["name"] == "Fido"
|
|
44
|
+
assert dog["tricks"] == ("roll over", "play dead", "fetch ball")
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, ClassVar, Dict, Type
|
|
4
|
+
from unittest import TestCase
|
|
5
|
+
|
|
6
|
+
from eventsourcing.examples.aggregate8.application import DogSchool
|
|
7
|
+
from eventsourcing.examples.aggregate8.domainmodel import Dog
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING: # pragma: nocover
|
|
10
|
+
from eventsourcing.domain import MutableOrImmutableAggregate
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class SubDogSchool(DogSchool):
|
|
14
|
+
snapshotting_intervals: ClassVar[
|
|
15
|
+
Dict[Type[MutableOrImmutableAggregate], int] | None
|
|
16
|
+
] = {Dog: 1}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class TestDogSchool(TestCase):
|
|
20
|
+
def test_dog_school(self) -> None:
|
|
21
|
+
# Construct application object.
|
|
22
|
+
school = SubDogSchool()
|
|
23
|
+
|
|
24
|
+
# Evolve application state.
|
|
25
|
+
dog_id = school.register_dog("Fido")
|
|
26
|
+
assert school.snapshots is not None
|
|
27
|
+
self.assertEqual(1, len(list(school.snapshots.get(dog_id))))
|
|
28
|
+
|
|
29
|
+
school.add_trick(dog_id, "roll over")
|
|
30
|
+
self.assertEqual(2, len(list(school.snapshots.get(dog_id))))
|
|
31
|
+
|
|
32
|
+
school.add_trick(dog_id, "play dead")
|
|
33
|
+
self.assertEqual(3, len(list(school.snapshots.get(dog_id))))
|
|
34
|
+
|
|
35
|
+
# Query application state.
|
|
36
|
+
dog = school.get_dog(dog_id)
|
|
37
|
+
self.assertEqual(dog["name"], "Fido")
|
|
38
|
+
self.assertEqual(dog["tricks"], ("roll over", "play dead"))
|
|
File without changes
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
from eventsourcing.application import AggregateNotFoundError, Application
|
|
6
|
+
from eventsourcing.examples.bankaccounts.domainmodel import BankAccount
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING: # pragma: nocover
|
|
9
|
+
from decimal import Decimal
|
|
10
|
+
from uuid import UUID
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class BankAccounts(Application):
|
|
14
|
+
def open_account(self, full_name: str, email_address: str) -> UUID:
|
|
15
|
+
account = BankAccount(
|
|
16
|
+
full_name=full_name,
|
|
17
|
+
email_address=email_address,
|
|
18
|
+
)
|
|
19
|
+
self.save(account)
|
|
20
|
+
return account.id
|
|
21
|
+
|
|
22
|
+
def get_account(self, account_id: UUID) -> BankAccount:
|
|
23
|
+
try:
|
|
24
|
+
return self.repository.get(account_id)
|
|
25
|
+
except AggregateNotFoundError:
|
|
26
|
+
raise AccountNotFoundError(account_id) from None
|
|
27
|
+
|
|
28
|
+
def get_balance(self, account_id: UUID) -> Decimal:
|
|
29
|
+
account = self.get_account(account_id)
|
|
30
|
+
return account.balance
|
|
31
|
+
|
|
32
|
+
def deposit_funds(self, credit_account_id: UUID, amount: Decimal) -> None:
|
|
33
|
+
account = self.get_account(credit_account_id)
|
|
34
|
+
account.credit(amount)
|
|
35
|
+
self.save(account)
|
|
36
|
+
|
|
37
|
+
def withdraw_funds(self, debit_account_id: UUID, amount: Decimal) -> None:
|
|
38
|
+
account = self.get_account(debit_account_id)
|
|
39
|
+
account.debit(amount)
|
|
40
|
+
self.save(account)
|
|
41
|
+
|
|
42
|
+
def transfer_funds(
|
|
43
|
+
self,
|
|
44
|
+
debit_account_id: UUID,
|
|
45
|
+
credit_account_id: UUID,
|
|
46
|
+
amount: Decimal,
|
|
47
|
+
) -> None:
|
|
48
|
+
debit_account = self.get_account(debit_account_id)
|
|
49
|
+
credit_account = self.get_account(credit_account_id)
|
|
50
|
+
debit_account.debit(amount)
|
|
51
|
+
credit_account.credit(amount)
|
|
52
|
+
self.save(debit_account, credit_account)
|
|
53
|
+
|
|
54
|
+
def set_overdraft_limit(self, account_id: UUID, overdraft_limit: Decimal) -> None:
|
|
55
|
+
account = self.get_account(account_id)
|
|
56
|
+
account.set_overdraft_limit(overdraft_limit)
|
|
57
|
+
self.save(account)
|
|
58
|
+
|
|
59
|
+
def get_overdraft_limit(self, account_id: UUID) -> Decimal:
|
|
60
|
+
account = self.get_account(account_id)
|
|
61
|
+
return account.overdraft_limit
|
|
62
|
+
|
|
63
|
+
def close_account(self, account_id: UUID) -> None:
|
|
64
|
+
account = self.get_account(account_id)
|
|
65
|
+
account.close()
|
|
66
|
+
self.save(account)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class AccountNotFoundError(Exception):
|
|
70
|
+
pass
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from decimal import Decimal
|
|
4
|
+
|
|
5
|
+
from eventsourcing.domain import Aggregate, event
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class BankAccount(Aggregate):
|
|
9
|
+
@event("Opened")
|
|
10
|
+
def __init__(self, full_name: str, email_address: str):
|
|
11
|
+
self.full_name = full_name
|
|
12
|
+
self.email_address = email_address
|
|
13
|
+
self.balance = Decimal("0.00")
|
|
14
|
+
self.overdraft_limit = Decimal("0.00")
|
|
15
|
+
self.is_closed = False
|
|
16
|
+
|
|
17
|
+
@event("Credited")
|
|
18
|
+
def credit(self, amount: Decimal) -> None:
|
|
19
|
+
self.check_account_is_not_closed()
|
|
20
|
+
self.balance += amount
|
|
21
|
+
|
|
22
|
+
@event("Debited")
|
|
23
|
+
def debit(self, amount: Decimal) -> None:
|
|
24
|
+
self.check_account_is_not_closed()
|
|
25
|
+
self.check_has_sufficient_funds(amount)
|
|
26
|
+
self.balance -= amount
|
|
27
|
+
|
|
28
|
+
@event("OverdraftLimitSet")
|
|
29
|
+
def set_overdraft_limit(self, overdraft_limit: Decimal) -> None:
|
|
30
|
+
assert overdraft_limit > Decimal("0.00")
|
|
31
|
+
self.check_account_is_not_closed()
|
|
32
|
+
self.overdraft_limit = overdraft_limit
|
|
33
|
+
|
|
34
|
+
@event("Closed")
|
|
35
|
+
def close(self) -> None:
|
|
36
|
+
self.is_closed = True
|
|
37
|
+
|
|
38
|
+
def check_account_is_not_closed(self) -> None:
|
|
39
|
+
if self.is_closed:
|
|
40
|
+
raise AccountClosedError({"account_id": self.id})
|
|
41
|
+
|
|
42
|
+
def check_has_sufficient_funds(self, amount: Decimal) -> None:
|
|
43
|
+
if self.balance - amount < -self.overdraft_limit:
|
|
44
|
+
raise InsufficientFundsError({"account_id": self.id})
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class TransactionError(Exception):
|
|
48
|
+
pass
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class AccountClosedError(TransactionError):
|
|
52
|
+
pass
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class InsufficientFundsError(TransactionError):
|
|
56
|
+
pass
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import unittest
|
|
4
|
+
from decimal import Decimal
|
|
5
|
+
from uuid import uuid4
|
|
6
|
+
|
|
7
|
+
from eventsourcing.examples.bankaccounts.application import (
|
|
8
|
+
AccountNotFoundError,
|
|
9
|
+
BankAccounts,
|
|
10
|
+
)
|
|
11
|
+
from eventsourcing.examples.bankaccounts.domainmodel import (
|
|
12
|
+
AccountClosedError,
|
|
13
|
+
InsufficientFundsError,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class TestBankAccounts(unittest.TestCase):
|
|
18
|
+
def test(self) -> None:
|
|
19
|
+
app = BankAccounts()
|
|
20
|
+
|
|
21
|
+
# Check account not found error.
|
|
22
|
+
with self.assertRaises(AccountNotFoundError):
|
|
23
|
+
app.get_balance(uuid4())
|
|
24
|
+
|
|
25
|
+
# Create account #1.
|
|
26
|
+
account_id1 = app.open_account(
|
|
27
|
+
full_name="Alice",
|
|
28
|
+
email_address="alice@example.com",
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
# Check balance of account #1.
|
|
32
|
+
self.assertEqual(app.get_balance(account_id1), Decimal("0.00"))
|
|
33
|
+
|
|
34
|
+
# Deposit funds in account #1.
|
|
35
|
+
app.deposit_funds(
|
|
36
|
+
credit_account_id=account_id1,
|
|
37
|
+
amount=Decimal("200.00"),
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
# Check balance of account #1.
|
|
41
|
+
self.assertEqual(app.get_balance(account_id1), Decimal("200.00"))
|
|
42
|
+
|
|
43
|
+
# Withdraw funds from account #1.
|
|
44
|
+
app.withdraw_funds(
|
|
45
|
+
debit_account_id=account_id1,
|
|
46
|
+
amount=Decimal("50.00"),
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
# Check balance of account #1.
|
|
50
|
+
self.assertEqual(app.get_balance(account_id1), Decimal("150.00"))
|
|
51
|
+
|
|
52
|
+
# Fail to withdraw funds from account #1- insufficient funds.
|
|
53
|
+
with self.assertRaises(InsufficientFundsError):
|
|
54
|
+
app.withdraw_funds(
|
|
55
|
+
debit_account_id=account_id1,
|
|
56
|
+
amount=Decimal("151.00"),
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
# Check balance of account #1 - should be unchanged.
|
|
60
|
+
self.assertEqual(app.get_balance(account_id1), Decimal("150.00"))
|
|
61
|
+
|
|
62
|
+
# Create account #2.
|
|
63
|
+
account_id2 = app.open_account(
|
|
64
|
+
full_name="Bob",
|
|
65
|
+
email_address="bob@example.com",
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
# Transfer funds from account #1 to account #2.
|
|
69
|
+
app.transfer_funds(
|
|
70
|
+
debit_account_id=account_id1,
|
|
71
|
+
credit_account_id=account_id2,
|
|
72
|
+
amount=Decimal("100.00"),
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
# Check balances.
|
|
76
|
+
self.assertEqual(app.get_balance(account_id1), Decimal("50.00"))
|
|
77
|
+
self.assertEqual(app.get_balance(account_id2), Decimal("100.00"))
|
|
78
|
+
|
|
79
|
+
# Fail to transfer funds - insufficient funds.
|
|
80
|
+
with self.assertRaises(InsufficientFundsError):
|
|
81
|
+
app.transfer_funds(
|
|
82
|
+
debit_account_id=account_id1,
|
|
83
|
+
credit_account_id=account_id2,
|
|
84
|
+
amount=Decimal("1000.00"),
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
# Check balances - should be unchanged.
|
|
88
|
+
self.assertEqual(app.get_balance(account_id1), Decimal("50.00"))
|
|
89
|
+
self.assertEqual(app.get_balance(account_id2), Decimal("100.00"))
|
|
90
|
+
|
|
91
|
+
# Close account #1.
|
|
92
|
+
app.close_account(account_id1)
|
|
93
|
+
|
|
94
|
+
# Fail to transfer funds - account #1 is closed.
|
|
95
|
+
with self.assertRaises(AccountClosedError):
|
|
96
|
+
app.transfer_funds(
|
|
97
|
+
debit_account_id=account_id1,
|
|
98
|
+
credit_account_id=account_id2,
|
|
99
|
+
amount=Decimal("50.00"),
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
# Fail to withdraw funds - account #1 is closed.
|
|
103
|
+
with self.assertRaises(AccountClosedError):
|
|
104
|
+
app.withdraw_funds(
|
|
105
|
+
debit_account_id=account_id1,
|
|
106
|
+
amount=Decimal("1.00"),
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
# Fail to deposit funds - account #1 is closed.
|
|
110
|
+
with self.assertRaises(AccountClosedError):
|
|
111
|
+
app.deposit_funds(
|
|
112
|
+
credit_account_id=account_id1,
|
|
113
|
+
amount=Decimal("1000.00"),
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
# Fail to set overdraft limit on account #1 - account is closed.
|
|
117
|
+
with self.assertRaises(AccountClosedError):
|
|
118
|
+
app.set_overdraft_limit(
|
|
119
|
+
account_id=account_id1,
|
|
120
|
+
overdraft_limit=Decimal("500.00"),
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
# Check balances - should be unchanged.
|
|
124
|
+
self.assertEqual(app.get_balance(account_id1), Decimal("50.00"))
|
|
125
|
+
self.assertEqual(app.get_balance(account_id2), Decimal("100.00"))
|
|
126
|
+
|
|
127
|
+
# Check overdraft limits - should be unchanged.
|
|
128
|
+
self.assertEqual(
|
|
129
|
+
app.get_overdraft_limit(account_id1),
|
|
130
|
+
Decimal("0.00"),
|
|
131
|
+
)
|
|
132
|
+
self.assertEqual(
|
|
133
|
+
app.get_overdraft_limit(account_id2),
|
|
134
|
+
Decimal("0.00"),
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
# Set overdraft limit on account #2.
|
|
138
|
+
app.set_overdraft_limit(
|
|
139
|
+
account_id=account_id2,
|
|
140
|
+
overdraft_limit=Decimal("500.00"),
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
# Can't set negative overdraft limit.
|
|
144
|
+
with self.assertRaises(AssertionError):
|
|
145
|
+
app.set_overdraft_limit(
|
|
146
|
+
account_id=account_id2,
|
|
147
|
+
overdraft_limit=Decimal("-500.00"),
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
# Check overdraft limit of account #2.
|
|
151
|
+
self.assertEqual(
|
|
152
|
+
app.get_overdraft_limit(account_id2),
|
|
153
|
+
Decimal("500.00"),
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
# Withdraw funds from account #2.
|
|
157
|
+
app.withdraw_funds(
|
|
158
|
+
debit_account_id=account_id2,
|
|
159
|
+
amount=Decimal("500.00"),
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
# Check balance of account #2 - should be overdrawn.
|
|
163
|
+
self.assertEqual(
|
|
164
|
+
app.get_balance(account_id2),
|
|
165
|
+
Decimal("-400.00"),
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
# Fail to withdraw funds from account #2 - insufficient funds.
|
|
169
|
+
with self.assertRaises(InsufficientFundsError):
|
|
170
|
+
app.withdraw_funds(
|
|
171
|
+
debit_account_id=account_id2,
|
|
172
|
+
amount=Decimal("101.00"),
|
|
173
|
+
)
|
|
File without changes
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Any, Dict, List, cast
|
|
4
|
+
|
|
5
|
+
from eventsourcing.application import Application
|
|
6
|
+
from eventsourcing.examples.cargoshipping.domainmodel import (
|
|
7
|
+
REGISTERED_ROUTES,
|
|
8
|
+
Cargo,
|
|
9
|
+
HandlingActivity,
|
|
10
|
+
Itinerary,
|
|
11
|
+
Leg,
|
|
12
|
+
Location,
|
|
13
|
+
)
|
|
14
|
+
from eventsourcing.persistence import Transcoder, Transcoding
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING: # pragma: nocover
|
|
17
|
+
from datetime import datetime
|
|
18
|
+
from uuid import UUID
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class LocationAsName(Transcoding):
|
|
22
|
+
type = Location
|
|
23
|
+
name = "location"
|
|
24
|
+
|
|
25
|
+
def encode(self, obj: Location) -> str:
|
|
26
|
+
return obj.name
|
|
27
|
+
|
|
28
|
+
def decode(self, data: str) -> Location:
|
|
29
|
+
assert isinstance(data, str)
|
|
30
|
+
return Location[data]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class HandlingActivityAsName(Transcoding):
|
|
34
|
+
type = HandlingActivity
|
|
35
|
+
name = "handling_activity"
|
|
36
|
+
|
|
37
|
+
def encode(self, obj: HandlingActivity) -> str:
|
|
38
|
+
return obj.name
|
|
39
|
+
|
|
40
|
+
def decode(self, data: str) -> HandlingActivity:
|
|
41
|
+
assert isinstance(data, str)
|
|
42
|
+
return HandlingActivity[data]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class ItineraryAsDict(Transcoding):
|
|
46
|
+
type = Itinerary
|
|
47
|
+
name = "itinerary"
|
|
48
|
+
|
|
49
|
+
def encode(self, obj: Itinerary) -> Dict[str, Any]:
|
|
50
|
+
return obj.__dict__
|
|
51
|
+
|
|
52
|
+
def decode(self, data: Dict[str, Any]) -> Itinerary:
|
|
53
|
+
assert isinstance(data, dict)
|
|
54
|
+
return Itinerary(**data)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class LegAsDict(Transcoding):
|
|
58
|
+
type = Leg
|
|
59
|
+
name = "leg"
|
|
60
|
+
|
|
61
|
+
def encode(self, obj: Leg) -> Dict[str, Any]:
|
|
62
|
+
return obj.__dict__
|
|
63
|
+
|
|
64
|
+
def decode(self, data: Dict[str, Any]) -> Leg:
|
|
65
|
+
assert isinstance(data, dict)
|
|
66
|
+
return Leg(**data)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class BookingApplication(Application):
|
|
70
|
+
def register_transcodings(self, transcoder: Transcoder) -> None:
|
|
71
|
+
super().register_transcodings(transcoder)
|
|
72
|
+
transcoder.register(LocationAsName())
|
|
73
|
+
transcoder.register(HandlingActivityAsName())
|
|
74
|
+
transcoder.register(ItineraryAsDict())
|
|
75
|
+
transcoder.register(LegAsDict())
|
|
76
|
+
|
|
77
|
+
def book_new_cargo(
|
|
78
|
+
self,
|
|
79
|
+
origin: Location,
|
|
80
|
+
destination: Location,
|
|
81
|
+
arrival_deadline: datetime,
|
|
82
|
+
) -> UUID:
|
|
83
|
+
cargo = Cargo.new_booking(origin, destination, arrival_deadline)
|
|
84
|
+
self.save(cargo)
|
|
85
|
+
return cargo.id
|
|
86
|
+
|
|
87
|
+
def change_destination(self, tracking_id: UUID, destination: Location) -> None:
|
|
88
|
+
cargo = self.get_cargo(tracking_id)
|
|
89
|
+
cargo.change_destination(destination)
|
|
90
|
+
self.save(cargo)
|
|
91
|
+
|
|
92
|
+
def request_possible_routes_for_cargo(self, tracking_id: UUID) -> List[Itinerary]:
|
|
93
|
+
cargo = self.get_cargo(tracking_id)
|
|
94
|
+
from_location = (cargo.last_known_location or cargo.origin).value
|
|
95
|
+
to_location = cargo.destination.value
|
|
96
|
+
try:
|
|
97
|
+
possible_routes = REGISTERED_ROUTES[(from_location, to_location)]
|
|
98
|
+
except KeyError:
|
|
99
|
+
msg = f"Can't find routes from {from_location} to {to_location}"
|
|
100
|
+
raise ValueError(msg) from None
|
|
101
|
+
|
|
102
|
+
return possible_routes
|
|
103
|
+
|
|
104
|
+
def assign_route(self, tracking_id: UUID, itinerary: Itinerary) -> None:
|
|
105
|
+
cargo = self.get_cargo(tracking_id)
|
|
106
|
+
cargo.assign_route(itinerary)
|
|
107
|
+
self.save(cargo)
|
|
108
|
+
|
|
109
|
+
def register_handling_event(
|
|
110
|
+
self,
|
|
111
|
+
tracking_id: UUID,
|
|
112
|
+
voyage_number: str | None,
|
|
113
|
+
location: Location,
|
|
114
|
+
handing_activity: HandlingActivity,
|
|
115
|
+
) -> None:
|
|
116
|
+
cargo = self.get_cargo(tracking_id)
|
|
117
|
+
cargo.register_handling_event(
|
|
118
|
+
tracking_id,
|
|
119
|
+
voyage_number,
|
|
120
|
+
location,
|
|
121
|
+
handing_activity,
|
|
122
|
+
)
|
|
123
|
+
self.save(cargo)
|
|
124
|
+
|
|
125
|
+
def get_cargo(self, tracking_id: UUID) -> Cargo:
|
|
126
|
+
return cast(Cargo, self.repository.get(tracking_id))
|