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.

Files changed (144) hide show
  1. eventsourcing/__init__.py +1 -1
  2. eventsourcing/application.py +116 -135
  3. eventsourcing/cipher.py +15 -12
  4. eventsourcing/dispatch.py +31 -91
  5. eventsourcing/domain.py +220 -226
  6. eventsourcing/examples/__init__.py +0 -0
  7. eventsourcing/examples/aggregate1/__init__.py +0 -0
  8. eventsourcing/examples/aggregate1/application.py +27 -0
  9. eventsourcing/examples/aggregate1/domainmodel.py +16 -0
  10. eventsourcing/examples/aggregate1/test_application.py +37 -0
  11. eventsourcing/examples/aggregate2/__init__.py +0 -0
  12. eventsourcing/examples/aggregate2/application.py +27 -0
  13. eventsourcing/examples/aggregate2/domainmodel.py +22 -0
  14. eventsourcing/examples/aggregate2/test_application.py +37 -0
  15. eventsourcing/examples/aggregate3/__init__.py +0 -0
  16. eventsourcing/examples/aggregate3/application.py +27 -0
  17. eventsourcing/examples/aggregate3/domainmodel.py +38 -0
  18. eventsourcing/examples/aggregate3/test_application.py +37 -0
  19. eventsourcing/examples/aggregate4/__init__.py +0 -0
  20. eventsourcing/examples/aggregate4/application.py +27 -0
  21. eventsourcing/examples/aggregate4/domainmodel.py +114 -0
  22. eventsourcing/examples/aggregate4/test_application.py +38 -0
  23. eventsourcing/examples/aggregate5/__init__.py +0 -0
  24. eventsourcing/examples/aggregate5/application.py +27 -0
  25. eventsourcing/examples/aggregate5/domainmodel.py +131 -0
  26. eventsourcing/examples/aggregate5/test_application.py +38 -0
  27. eventsourcing/examples/aggregate6/__init__.py +0 -0
  28. eventsourcing/examples/aggregate6/application.py +30 -0
  29. eventsourcing/examples/aggregate6/domainmodel.py +123 -0
  30. eventsourcing/examples/aggregate6/test_application.py +38 -0
  31. eventsourcing/examples/aggregate6a/__init__.py +0 -0
  32. eventsourcing/examples/aggregate6a/application.py +40 -0
  33. eventsourcing/examples/aggregate6a/domainmodel.py +149 -0
  34. eventsourcing/examples/aggregate6a/test_application.py +45 -0
  35. eventsourcing/examples/aggregate7/__init__.py +0 -0
  36. eventsourcing/examples/aggregate7/application.py +48 -0
  37. eventsourcing/examples/aggregate7/domainmodel.py +144 -0
  38. eventsourcing/examples/aggregate7/persistence.py +57 -0
  39. eventsourcing/examples/aggregate7/test_application.py +38 -0
  40. eventsourcing/examples/aggregate7/test_compression_and_encryption.py +45 -0
  41. eventsourcing/examples/aggregate7/test_snapshotting_intervals.py +67 -0
  42. eventsourcing/examples/aggregate7a/__init__.py +0 -0
  43. eventsourcing/examples/aggregate7a/application.py +56 -0
  44. eventsourcing/examples/aggregate7a/domainmodel.py +170 -0
  45. eventsourcing/examples/aggregate7a/test_application.py +46 -0
  46. eventsourcing/examples/aggregate7a/test_compression_and_encryption.py +45 -0
  47. eventsourcing/examples/aggregate8/__init__.py +0 -0
  48. eventsourcing/examples/aggregate8/application.py +47 -0
  49. eventsourcing/examples/aggregate8/domainmodel.py +65 -0
  50. eventsourcing/examples/aggregate8/persistence.py +57 -0
  51. eventsourcing/examples/aggregate8/test_application.py +37 -0
  52. eventsourcing/examples/aggregate8/test_compression_and_encryption.py +44 -0
  53. eventsourcing/examples/aggregate8/test_snapshotting_intervals.py +38 -0
  54. eventsourcing/examples/bankaccounts/__init__.py +0 -0
  55. eventsourcing/examples/bankaccounts/application.py +70 -0
  56. eventsourcing/examples/bankaccounts/domainmodel.py +56 -0
  57. eventsourcing/examples/bankaccounts/test.py +173 -0
  58. eventsourcing/examples/cargoshipping/__init__.py +0 -0
  59. eventsourcing/examples/cargoshipping/application.py +126 -0
  60. eventsourcing/examples/cargoshipping/domainmodel.py +330 -0
  61. eventsourcing/examples/cargoshipping/interface.py +143 -0
  62. eventsourcing/examples/cargoshipping/test.py +231 -0
  63. eventsourcing/examples/contentmanagement/__init__.py +0 -0
  64. eventsourcing/examples/contentmanagement/application.py +118 -0
  65. eventsourcing/examples/contentmanagement/domainmodel.py +69 -0
  66. eventsourcing/examples/contentmanagement/test.py +180 -0
  67. eventsourcing/examples/contentmanagement/utils.py +26 -0
  68. eventsourcing/examples/contentmanagementsystem/__init__.py +0 -0
  69. eventsourcing/examples/contentmanagementsystem/application.py +54 -0
  70. eventsourcing/examples/contentmanagementsystem/postgres.py +17 -0
  71. eventsourcing/examples/contentmanagementsystem/sqlite.py +17 -0
  72. eventsourcing/examples/contentmanagementsystem/system.py +14 -0
  73. eventsourcing/examples/contentmanagementsystem/test_system.py +180 -0
  74. eventsourcing/examples/searchablecontent/__init__.py +0 -0
  75. eventsourcing/examples/searchablecontent/application.py +45 -0
  76. eventsourcing/examples/searchablecontent/persistence.py +23 -0
  77. eventsourcing/examples/searchablecontent/postgres.py +118 -0
  78. eventsourcing/examples/searchablecontent/sqlite.py +136 -0
  79. eventsourcing/examples/searchablecontent/test_application.py +110 -0
  80. eventsourcing/examples/searchablecontent/test_recorder.py +68 -0
  81. eventsourcing/examples/searchabletimestamps/__init__.py +0 -0
  82. eventsourcing/examples/searchabletimestamps/application.py +32 -0
  83. eventsourcing/examples/searchabletimestamps/persistence.py +20 -0
  84. eventsourcing/examples/searchabletimestamps/postgres.py +110 -0
  85. eventsourcing/examples/searchabletimestamps/sqlite.py +99 -0
  86. eventsourcing/examples/searchabletimestamps/test_searchabletimestamps.py +94 -0
  87. eventsourcing/examples/test_invoice.py +176 -0
  88. eventsourcing/examples/test_parking_lot.py +206 -0
  89. eventsourcing/interface.py +2 -2
  90. eventsourcing/persistence.py +85 -81
  91. eventsourcing/popo.py +30 -31
  92. eventsourcing/postgres.py +379 -590
  93. eventsourcing/sqlite.py +91 -99
  94. eventsourcing/system.py +52 -57
  95. eventsourcing/tests/application.py +20 -32
  96. eventsourcing/tests/application_tests/__init__.py +0 -0
  97. eventsourcing/tests/application_tests/test_application_with_automatic_snapshotting.py +55 -0
  98. eventsourcing/tests/application_tests/test_application_with_popo.py +22 -0
  99. eventsourcing/tests/application_tests/test_application_with_postgres.py +75 -0
  100. eventsourcing/tests/application_tests/test_application_with_sqlite.py +72 -0
  101. eventsourcing/tests/application_tests/test_cache.py +134 -0
  102. eventsourcing/tests/application_tests/test_event_sourced_log.py +162 -0
  103. eventsourcing/tests/application_tests/test_notificationlog.py +232 -0
  104. eventsourcing/tests/application_tests/test_notificationlogreader.py +126 -0
  105. eventsourcing/tests/application_tests/test_processapplication.py +110 -0
  106. eventsourcing/tests/application_tests/test_processingpolicy.py +109 -0
  107. eventsourcing/tests/application_tests/test_repository.py +504 -0
  108. eventsourcing/tests/application_tests/test_snapshotting.py +68 -0
  109. eventsourcing/tests/application_tests/test_upcasting.py +459 -0
  110. eventsourcing/tests/docs_tests/__init__.py +0 -0
  111. eventsourcing/tests/docs_tests/test_docs.py +293 -0
  112. eventsourcing/tests/domain.py +1 -1
  113. eventsourcing/tests/domain_tests/__init__.py +0 -0
  114. eventsourcing/tests/domain_tests/test_aggregate.py +1180 -0
  115. eventsourcing/tests/domain_tests/test_aggregate_decorators.py +1604 -0
  116. eventsourcing/tests/domain_tests/test_domainevent.py +80 -0
  117. eventsourcing/tests/interface_tests/__init__.py +0 -0
  118. eventsourcing/tests/interface_tests/test_remotenotificationlog.py +258 -0
  119. eventsourcing/tests/persistence.py +52 -50
  120. eventsourcing/tests/persistence_tests/__init__.py +0 -0
  121. eventsourcing/tests/persistence_tests/test_aes.py +93 -0
  122. eventsourcing/tests/persistence_tests/test_connection_pool.py +722 -0
  123. eventsourcing/tests/persistence_tests/test_eventstore.py +72 -0
  124. eventsourcing/tests/persistence_tests/test_infrastructure_factory.py +21 -0
  125. eventsourcing/tests/persistence_tests/test_mapper.py +113 -0
  126. eventsourcing/tests/persistence_tests/test_noninterleaving_notification_ids.py +69 -0
  127. eventsourcing/tests/persistence_tests/test_popo.py +124 -0
  128. eventsourcing/tests/persistence_tests/test_postgres.py +1119 -0
  129. eventsourcing/tests/persistence_tests/test_sqlite.py +348 -0
  130. eventsourcing/tests/persistence_tests/test_transcoder.py +44 -0
  131. eventsourcing/tests/postgres_utils.py +7 -7
  132. eventsourcing/tests/system_tests/__init__.py +0 -0
  133. eventsourcing/tests/system_tests/test_runner.py +935 -0
  134. eventsourcing/tests/system_tests/test_system.py +284 -0
  135. eventsourcing/tests/utils_tests/__init__.py +0 -0
  136. eventsourcing/tests/utils_tests/test_utils.py +226 -0
  137. eventsourcing/utils.py +47 -50
  138. {eventsourcing-9.2.22.dist-info → eventsourcing-9.3.0.dist-info}/METADATA +29 -79
  139. eventsourcing-9.3.0.dist-info/RECORD +145 -0
  140. {eventsourcing-9.2.22.dist-info → eventsourcing-9.3.0.dist-info}/WHEEL +1 -2
  141. eventsourcing-9.2.22.dist-info/RECORD +0 -25
  142. eventsourcing-9.2.22.dist-info/top_level.txt +0 -1
  143. {eventsourcing-9.2.22.dist-info → eventsourcing-9.3.0.dist-info}/AUTHORS +0 -0
  144. {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))