eventsourcing 9.3.3__py3-none-any.whl → 9.3.5__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 (129) hide show
  1. eventsourcing/application.py +0 -15
  2. eventsourcing/system.py +25 -2
  3. {eventsourcing-9.3.3.dist-info → eventsourcing-9.3.5.dist-info}/METADATA +1 -1
  4. eventsourcing-9.3.5.dist-info/RECORD +24 -0
  5. eventsourcing/examples/__init__.py +0 -0
  6. eventsourcing/examples/aggregate1/__init__.py +0 -0
  7. eventsourcing/examples/aggregate1/application.py +0 -27
  8. eventsourcing/examples/aggregate1/domainmodel.py +0 -16
  9. eventsourcing/examples/aggregate1/test_application.py +0 -37
  10. eventsourcing/examples/aggregate2/__init__.py +0 -0
  11. eventsourcing/examples/aggregate2/application.py +0 -27
  12. eventsourcing/examples/aggregate2/domainmodel.py +0 -22
  13. eventsourcing/examples/aggregate2/test_application.py +0 -37
  14. eventsourcing/examples/aggregate3/__init__.py +0 -0
  15. eventsourcing/examples/aggregate3/application.py +0 -27
  16. eventsourcing/examples/aggregate3/domainmodel.py +0 -38
  17. eventsourcing/examples/aggregate3/test_application.py +0 -37
  18. eventsourcing/examples/aggregate4/__init__.py +0 -0
  19. eventsourcing/examples/aggregate4/application.py +0 -27
  20. eventsourcing/examples/aggregate4/domainmodel.py +0 -114
  21. eventsourcing/examples/aggregate4/test_application.py +0 -38
  22. eventsourcing/examples/aggregate5/__init__.py +0 -0
  23. eventsourcing/examples/aggregate5/application.py +0 -27
  24. eventsourcing/examples/aggregate5/domainmodel.py +0 -131
  25. eventsourcing/examples/aggregate5/test_application.py +0 -38
  26. eventsourcing/examples/aggregate6/__init__.py +0 -0
  27. eventsourcing/examples/aggregate6/application.py +0 -30
  28. eventsourcing/examples/aggregate6/domainmodel.py +0 -123
  29. eventsourcing/examples/aggregate6/test_application.py +0 -38
  30. eventsourcing/examples/aggregate6a/__init__.py +0 -0
  31. eventsourcing/examples/aggregate6a/application.py +0 -40
  32. eventsourcing/examples/aggregate6a/domainmodel.py +0 -149
  33. eventsourcing/examples/aggregate6a/test_application.py +0 -45
  34. eventsourcing/examples/aggregate7/__init__.py +0 -0
  35. eventsourcing/examples/aggregate7/application.py +0 -53
  36. eventsourcing/examples/aggregate7/domainmodel.py +0 -142
  37. eventsourcing/examples/aggregate7/persistence.py +0 -57
  38. eventsourcing/examples/aggregate7/test_application.py +0 -45
  39. eventsourcing/examples/aggregate7/test_compression_and_encryption.py +0 -45
  40. eventsourcing/examples/aggregate7/test_snapshotting_intervals.py +0 -67
  41. eventsourcing/examples/aggregate7a/__init__.py +0 -0
  42. eventsourcing/examples/aggregate7a/application.py +0 -56
  43. eventsourcing/examples/aggregate7a/domainmodel.py +0 -168
  44. eventsourcing/examples/aggregate7a/test_application.py +0 -46
  45. eventsourcing/examples/aggregate7a/test_compression_and_encryption.py +0 -45
  46. eventsourcing/examples/aggregate8/__init__.py +0 -0
  47. eventsourcing/examples/aggregate8/application.py +0 -47
  48. eventsourcing/examples/aggregate8/domainmodel.py +0 -71
  49. eventsourcing/examples/aggregate8/persistence.py +0 -57
  50. eventsourcing/examples/aggregate8/test_application.py +0 -44
  51. eventsourcing/examples/aggregate8/test_compression_and_encryption.py +0 -44
  52. eventsourcing/examples/aggregate8/test_snapshotting_intervals.py +0 -38
  53. eventsourcing/examples/bankaccounts/__init__.py +0 -0
  54. eventsourcing/examples/bankaccounts/application.py +0 -70
  55. eventsourcing/examples/bankaccounts/domainmodel.py +0 -56
  56. eventsourcing/examples/bankaccounts/test.py +0 -173
  57. eventsourcing/examples/cargoshipping/__init__.py +0 -0
  58. eventsourcing/examples/cargoshipping/application.py +0 -126
  59. eventsourcing/examples/cargoshipping/domainmodel.py +0 -330
  60. eventsourcing/examples/cargoshipping/interface.py +0 -143
  61. eventsourcing/examples/cargoshipping/test.py +0 -231
  62. eventsourcing/examples/contentmanagement/__init__.py +0 -0
  63. eventsourcing/examples/contentmanagement/application.py +0 -118
  64. eventsourcing/examples/contentmanagement/domainmodel.py +0 -69
  65. eventsourcing/examples/contentmanagement/test.py +0 -180
  66. eventsourcing/examples/contentmanagement/utils.py +0 -26
  67. eventsourcing/examples/contentmanagementsystem/__init__.py +0 -0
  68. eventsourcing/examples/contentmanagementsystem/application.py +0 -54
  69. eventsourcing/examples/contentmanagementsystem/postgres.py +0 -17
  70. eventsourcing/examples/contentmanagementsystem/sqlite.py +0 -17
  71. eventsourcing/examples/contentmanagementsystem/system.py +0 -14
  72. eventsourcing/examples/contentmanagementsystem/test_system.py +0 -180
  73. eventsourcing/examples/searchablecontent/__init__.py +0 -0
  74. eventsourcing/examples/searchablecontent/application.py +0 -45
  75. eventsourcing/examples/searchablecontent/persistence.py +0 -23
  76. eventsourcing/examples/searchablecontent/postgres.py +0 -118
  77. eventsourcing/examples/searchablecontent/sqlite.py +0 -136
  78. eventsourcing/examples/searchablecontent/test_application.py +0 -110
  79. eventsourcing/examples/searchablecontent/test_recorder.py +0 -68
  80. eventsourcing/examples/searchabletimestamps/__init__.py +0 -0
  81. eventsourcing/examples/searchabletimestamps/application.py +0 -32
  82. eventsourcing/examples/searchabletimestamps/persistence.py +0 -20
  83. eventsourcing/examples/searchabletimestamps/postgres.py +0 -110
  84. eventsourcing/examples/searchabletimestamps/sqlite.py +0 -99
  85. eventsourcing/examples/searchabletimestamps/test_searchabletimestamps.py +0 -94
  86. eventsourcing/examples/test_invoice.py +0 -176
  87. eventsourcing/examples/test_parking_lot.py +0 -206
  88. eventsourcing/tests/application_tests/__init__.py +0 -0
  89. eventsourcing/tests/application_tests/test_application_with_automatic_snapshotting.py +0 -55
  90. eventsourcing/tests/application_tests/test_application_with_popo.py +0 -22
  91. eventsourcing/tests/application_tests/test_application_with_postgres.py +0 -75
  92. eventsourcing/tests/application_tests/test_application_with_sqlite.py +0 -72
  93. eventsourcing/tests/application_tests/test_cache.py +0 -134
  94. eventsourcing/tests/application_tests/test_event_sourced_log.py +0 -162
  95. eventsourcing/tests/application_tests/test_notificationlog.py +0 -232
  96. eventsourcing/tests/application_tests/test_notificationlogreader.py +0 -126
  97. eventsourcing/tests/application_tests/test_processapplication.py +0 -110
  98. eventsourcing/tests/application_tests/test_processingpolicy.py +0 -109
  99. eventsourcing/tests/application_tests/test_repository.py +0 -504
  100. eventsourcing/tests/application_tests/test_snapshotting.py +0 -68
  101. eventsourcing/tests/application_tests/test_upcasting.py +0 -459
  102. eventsourcing/tests/docs_tests/__init__.py +0 -0
  103. eventsourcing/tests/docs_tests/test_docs.py +0 -293
  104. eventsourcing/tests/domain_tests/__init__.py +0 -0
  105. eventsourcing/tests/domain_tests/test_aggregate.py +0 -1200
  106. eventsourcing/tests/domain_tests/test_aggregate_decorators.py +0 -1604
  107. eventsourcing/tests/domain_tests/test_domainevent.py +0 -80
  108. eventsourcing/tests/interface_tests/__init__.py +0 -0
  109. eventsourcing/tests/interface_tests/test_remotenotificationlog.py +0 -258
  110. eventsourcing/tests/persistence_tests/__init__.py +0 -0
  111. eventsourcing/tests/persistence_tests/test_aes.py +0 -93
  112. eventsourcing/tests/persistence_tests/test_connection_pool.py +0 -722
  113. eventsourcing/tests/persistence_tests/test_eventstore.py +0 -72
  114. eventsourcing/tests/persistence_tests/test_infrastructure_factory.py +0 -21
  115. eventsourcing/tests/persistence_tests/test_mapper.py +0 -113
  116. eventsourcing/tests/persistence_tests/test_noninterleaving_notification_ids.py +0 -69
  117. eventsourcing/tests/persistence_tests/test_popo.py +0 -124
  118. eventsourcing/tests/persistence_tests/test_postgres.py +0 -1120
  119. eventsourcing/tests/persistence_tests/test_sqlite.py +0 -348
  120. eventsourcing/tests/persistence_tests/test_transcoder.py +0 -44
  121. eventsourcing/tests/system_tests/__init__.py +0 -0
  122. eventsourcing/tests/system_tests/test_runner.py +0 -935
  123. eventsourcing/tests/system_tests/test_system.py +0 -284
  124. eventsourcing/tests/utils_tests/__init__.py +0 -0
  125. eventsourcing/tests/utils_tests/test_utils.py +0 -226
  126. eventsourcing-9.3.3.dist-info/RECORD +0 -145
  127. {eventsourcing-9.3.3.dist-info → eventsourcing-9.3.5.dist-info}/AUTHORS +0 -0
  128. {eventsourcing-9.3.3.dist-info → eventsourcing-9.3.5.dist-info}/LICENSE +0 -0
  129. {eventsourcing-9.3.3.dist-info → eventsourcing-9.3.5.dist-info}/WHEEL +0 -0
@@ -1,47 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from typing import TYPE_CHECKING, Any, Dict
4
-
5
- from eventsourcing.application import Application
6
- from eventsourcing.examples.aggregate8.domainmodel import Dog, Trick
7
- from eventsourcing.examples.aggregate8.persistence import (
8
- OrjsonTranscoder,
9
- PydanticMapper,
10
- )
11
-
12
- if TYPE_CHECKING: # pragma: nocover
13
- from uuid import UUID
14
-
15
- from eventsourcing.persistence import Mapper, Transcoder
16
-
17
-
18
- class DogSchool(Application):
19
- is_snapshotting_enabled = True
20
-
21
- def register_dog(self, name: str) -> UUID:
22
- dog = Dog(name)
23
- self.save(dog)
24
- return dog.id
25
-
26
- def add_trick(self, dog_id: UUID, trick: str) -> None:
27
- dog: Dog = self.repository.get(dog_id)
28
- dog.add_trick(Trick(name=trick))
29
- self.save(dog)
30
-
31
- def get_dog(self, dog_id: UUID) -> Dict[str, Any]:
32
- dog: Dog = self.repository.get(dog_id)
33
- return {
34
- "name": dog.name,
35
- "tricks": tuple([t.name for t in dog.tricks]),
36
- "created_on": dog.created_on,
37
- "modified_on": dog.modified_on,
38
- }
39
-
40
- def construct_mapper(self) -> Mapper:
41
- return self.factory.mapper(
42
- transcoder=self.construct_transcoder(),
43
- mapper_class=PydanticMapper,
44
- )
45
-
46
- def construct_transcoder(self) -> Transcoder:
47
- return OrjsonTranscoder()
@@ -1,71 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from datetime import datetime
4
- from typing import Any, List
5
- from uuid import UUID
6
-
7
- from pydantic import BaseModel, ConfigDict, TypeAdapter
8
-
9
- from eventsourcing.domain import (
10
- Aggregate as BaseAggregate,
11
- CanInitAggregate,
12
- CanMutateAggregate,
13
- CanSnapshotAggregate,
14
- event,
15
- )
16
-
17
-
18
- class DomainEvent(BaseModel):
19
- model_config = ConfigDict(frozen=True, extra="forbid")
20
-
21
- originator_id: UUID
22
- originator_version: int
23
- timestamp: datetime
24
-
25
-
26
- datetime_adapter = TypeAdapter(datetime)
27
-
28
-
29
- class SnapshotState(BaseModel):
30
- model_config = ConfigDict(frozen=True, extra="allow")
31
-
32
- def __init__(self, **kwargs: Any) -> None:
33
- for key in ["_created_on", "_modified_on"]:
34
- kwargs[key] = datetime_adapter.validate_python(kwargs[key])
35
- super().__init__(**kwargs)
36
-
37
-
38
- class AggregateSnapshot(DomainEvent, CanSnapshotAggregate):
39
- topic: str
40
- state: SnapshotState
41
-
42
-
43
- class Aggregate(BaseAggregate):
44
- class Event(DomainEvent, CanMutateAggregate):
45
- pass
46
-
47
- class Created(Event, CanInitAggregate):
48
- originator_topic: str
49
-
50
-
51
- class Trick(BaseModel):
52
- name: str
53
-
54
-
55
- class DogSnapshotState(SnapshotState):
56
- name: str
57
- tricks: List[Trick]
58
-
59
-
60
- class Dog(Aggregate):
61
- class Snapshot(AggregateSnapshot):
62
- state: DogSnapshotState
63
-
64
- @event("Registered")
65
- def __init__(self, name: str) -> None:
66
- self.name = name
67
- self.tricks: List[Trick] = []
68
-
69
- @event("TrickAdded")
70
- def add_trick(self, trick: Trick) -> None:
71
- self.tricks.append(trick)
@@ -1,57 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from typing import TYPE_CHECKING, Any, Dict, cast
4
-
5
- import orjson
6
- from pydantic import BaseModel
7
-
8
- from eventsourcing.persistence import (
9
- Mapper,
10
- ProgrammingError,
11
- StoredEvent,
12
- Transcoder,
13
- Transcoding,
14
- )
15
- from eventsourcing.utils import get_topic, resolve_topic
16
-
17
- if TYPE_CHECKING: # pragma: nocover
18
- from eventsourcing.domain import DomainEventProtocol
19
-
20
-
21
- class PydanticMapper(Mapper):
22
- def to_stored_event(self, domain_event: DomainEventProtocol) -> StoredEvent:
23
- topic = get_topic(domain_event.__class__)
24
- event_state = cast(BaseModel, domain_event).model_dump()
25
- stored_state = self.transcoder.encode(event_state)
26
- if self.compressor:
27
- stored_state = self.compressor.compress(stored_state)
28
- if self.cipher:
29
- stored_state = self.cipher.encrypt(stored_state)
30
- return StoredEvent(
31
- originator_id=domain_event.originator_id,
32
- originator_version=domain_event.originator_version,
33
- topic=topic,
34
- state=stored_state,
35
- )
36
-
37
- def to_domain_event(self, stored: StoredEvent) -> DomainEventProtocol:
38
- stored_state = stored.state
39
- if self.cipher:
40
- stored_state = self.cipher.decrypt(stored_state)
41
- if self.compressor:
42
- stored_state = self.compressor.decompress(stored_state)
43
- event_state: Dict[str, Any] = self.transcoder.decode(stored_state)
44
- cls = resolve_topic(stored.topic)
45
- return cls(**event_state)
46
-
47
-
48
- class OrjsonTranscoder(Transcoder):
49
- def encode(self, obj: Any) -> bytes:
50
- return orjson.dumps(obj)
51
-
52
- def decode(self, data: bytes) -> Any:
53
- return orjson.loads(data)
54
-
55
- def register(self, _: Transcoding) -> None: # pragma: no cover
56
- msg = "Please use Pydantic BaseModel"
57
- raise ProgrammingError(msg)
@@ -1,44 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from datetime import datetime
4
- from unittest import TestCase
5
-
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
-
14
- # Evolve application state.
15
- dog_id = school.register_dog("Fido")
16
- school.add_trick(dog_id, "roll over")
17
- school.add_trick(dog_id, "play dead")
18
-
19
- # Query application state.
20
- dog = school.get_dog(dog_id)
21
- self.assertEqual(dog["name"], "Fido")
22
- self.assertEqual(dog["tricks"], ("roll over", "play dead"))
23
- self.assertIsInstance(dog["created_on"], datetime)
24
- self.assertIsInstance(dog["modified_on"], datetime)
25
-
26
- # Select notifications.
27
- notifications = school.notification_log.select(start=1, limit=10)
28
- assert len(notifications) == 3
29
-
30
- # Take snapshot.
31
- school.take_snapshot(dog_id, version=3)
32
- dog = school.get_dog(dog_id)
33
- self.assertEqual(dog["name"], "Fido")
34
- self.assertEqual(dog["tricks"], ("roll over", "play dead"))
35
- self.assertIsInstance(dog["created_on"], datetime)
36
- self.assertIsInstance(dog["modified_on"], datetime)
37
-
38
- # Continue with snapshotted aggregate.
39
- school.add_trick(dog_id, "fetch ball")
40
- dog = school.get_dog(dog_id)
41
- self.assertEqual(dog["name"], "Fido")
42
- self.assertEqual(dog["tricks"], ("roll over", "play dead", "fetch ball"))
43
- self.assertIsInstance(dog["created_on"], datetime)
44
- self.assertIsInstance(dog["modified_on"], datetime)
@@ -1,44 +0,0 @@
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")
@@ -1,38 +0,0 @@
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
@@ -1,70 +0,0 @@
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
@@ -1,56 +0,0 @@
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
@@ -1,173 +0,0 @@
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