eventsourcing 9.4.0b1__py3-none-any.whl → 9.4.0b3__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/application.py +53 -104
- eventsourcing/cipher.py +3 -8
- eventsourcing/compressor.py +2 -6
- eventsourcing/cryptography.py +3 -8
- eventsourcing/dispatch.py +2 -2
- eventsourcing/domain.py +211 -291
- eventsourcing/interface.py +10 -24
- eventsourcing/persistence.py +122 -219
- eventsourcing/popo.py +2 -2
- eventsourcing/postgres.py +7 -10
- eventsourcing/projection.py +81 -54
- eventsourcing/sqlite.py +4 -7
- eventsourcing/system.py +89 -156
- eventsourcing/tests/application.py +137 -74
- eventsourcing/tests/domain.py +14 -34
- eventsourcing/tests/persistence.py +21 -18
- eventsourcing/utils.py +11 -17
- {eventsourcing-9.4.0b1.dist-info → eventsourcing-9.4.0b3.dist-info}/METADATA +1 -1
- eventsourcing-9.4.0b3.dist-info/RECORD +26 -0
- eventsourcing-9.4.0b1.dist-info/RECORD +0 -26
- {eventsourcing-9.4.0b1.dist-info → eventsourcing-9.4.0b3.dist-info}/AUTHORS +0 -0
- {eventsourcing-9.4.0b1.dist-info → eventsourcing-9.4.0b3.dist-info}/LICENSE +0 -0
- {eventsourcing-9.4.0b1.dist-info → eventsourcing-9.4.0b3.dist-info}/WHEEL +0 -0
|
@@ -5,27 +5,30 @@ import sys
|
|
|
5
5
|
import traceback
|
|
6
6
|
import warnings
|
|
7
7
|
from concurrent.futures import ThreadPoolExecutor
|
|
8
|
-
from datetime import datetime
|
|
9
8
|
from decimal import Decimal
|
|
10
9
|
from threading import Event, get_ident
|
|
11
10
|
from time import sleep
|
|
12
11
|
from timeit import timeit
|
|
13
|
-
from typing import ClassVar
|
|
12
|
+
from typing import TYPE_CHECKING, Any, ClassVar
|
|
14
13
|
from unittest import TestCase
|
|
15
14
|
from uuid import UUID, uuid4
|
|
16
15
|
|
|
17
16
|
from eventsourcing.application import AggregateNotFoundError, Application
|
|
18
|
-
from eventsourcing.domain import Aggregate
|
|
17
|
+
from eventsourcing.domain import Aggregate, datetime_now_with_tzinfo
|
|
19
18
|
from eventsourcing.persistence import (
|
|
20
19
|
InfrastructureFactory,
|
|
20
|
+
InfrastructureFactoryError,
|
|
21
21
|
IntegrityError,
|
|
22
22
|
JSONTranscoder,
|
|
23
23
|
Transcoding,
|
|
24
24
|
)
|
|
25
25
|
from eventsourcing.tests.domain import BankAccount, EmailAddress
|
|
26
|
-
from eventsourcing.utils import get_topic
|
|
26
|
+
from eventsourcing.utils import EnvType, get_topic
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
if TYPE_CHECKING:
|
|
29
|
+
from datetime import datetime
|
|
30
|
+
|
|
31
|
+
TIMEIT_FACTOR = int(os.environ.get("TEST_TIMEIT_FACTOR", default="10"))
|
|
29
32
|
|
|
30
33
|
|
|
31
34
|
class ExampleApplicationTestCase(TestCase):
|
|
@@ -163,7 +166,7 @@ class ExampleApplicationTestCase(TestCase):
|
|
|
163
166
|
def print_time(self, test_label: str, duration: float) -> None:
|
|
164
167
|
cls = type(self)
|
|
165
168
|
if cls not in self.started_ats:
|
|
166
|
-
self.started_ats[cls] =
|
|
169
|
+
self.started_ats[cls] = datetime_now_with_tzinfo()
|
|
167
170
|
print(f"{cls.__name__: <29} timeit number: {cls.timeit_number}")
|
|
168
171
|
self.counts[cls] = 1
|
|
169
172
|
else:
|
|
@@ -178,7 +181,7 @@ class ExampleApplicationTestCase(TestCase):
|
|
|
178
181
|
)
|
|
179
182
|
|
|
180
183
|
if self.counts[cls] == 3:
|
|
181
|
-
cls_duration =
|
|
184
|
+
cls_duration = datetime_now_with_tzinfo() - cls.started_ats[cls]
|
|
182
185
|
print(f"{cls.__name__: <29} timeit duration: {cls_duration}")
|
|
183
186
|
sys.stdout.flush()
|
|
184
187
|
|
|
@@ -263,7 +266,7 @@ class ApplicationTestCase(TestCase):
|
|
|
263
266
|
self.assertIsInstance(app.factory, InfrastructureFactory)
|
|
264
267
|
|
|
265
268
|
# Check exceptions.
|
|
266
|
-
with self.assertRaises(
|
|
269
|
+
with self.assertRaises(InfrastructureFactoryError) as cm:
|
|
267
270
|
Application(env={"PERSISTENCE_MODULE": "eventsourcing.application"})
|
|
268
271
|
self.assertEqual(
|
|
269
272
|
cm.exception.args[0],
|
|
@@ -271,14 +274,15 @@ class ApplicationTestCase(TestCase):
|
|
|
271
274
|
"'eventsourcing.application', expected 1.",
|
|
272
275
|
)
|
|
273
276
|
|
|
274
|
-
with self.assertRaises(
|
|
277
|
+
with self.assertRaises(InfrastructureFactoryError) as cm:
|
|
275
278
|
Application(
|
|
276
279
|
env={"PERSISTENCE_MODULE": "eventsourcing.application:Application"}
|
|
277
280
|
)
|
|
278
281
|
self.assertEqual(
|
|
282
|
+
"Topic 'eventsourcing.application:Application' didn't "
|
|
283
|
+
"resolve to a persistence module or infrastructure factory class: "
|
|
284
|
+
"<class 'eventsourcing.application.Application'>",
|
|
279
285
|
cm.exception.args[0],
|
|
280
|
-
"Not an infrastructure factory class or module: "
|
|
281
|
-
"eventsourcing.application:Application",
|
|
282
286
|
)
|
|
283
287
|
|
|
284
288
|
def test_save_returns_recording_event(self) -> None:
|
|
@@ -344,96 +348,131 @@ class ApplicationTestCase(TestCase):
|
|
|
344
348
|
app.repository.get(aggregate.id)
|
|
345
349
|
self.assertEqual(aggregate, app.repository.cache.get(aggregate.id))
|
|
346
350
|
|
|
347
|
-
def
|
|
348
|
-
|
|
351
|
+
def test_check_aggregate_fastforwarding_nonblocking(self) -> None:
|
|
352
|
+
self._check_aggregate_fastforwarding_during_contention(
|
|
349
353
|
env={
|
|
350
354
|
"AGGREGATE_CACHE_MAXSIZE": "10",
|
|
351
355
|
"AGGREGATE_CACHE_FASTFORWARD_SKIPPING": "y",
|
|
352
356
|
}
|
|
353
357
|
)
|
|
354
358
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
359
|
+
def test_check_aggregate_fastforwarding_blocking(self) -> None:
|
|
360
|
+
self._check_aggregate_fastforwarding_during_contention(
|
|
361
|
+
env={"AGGREGATE_CACHE_MAXSIZE": "10"}
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
def _check_aggregate_fastforwarding_during_contention(self, env: EnvType) -> None:
|
|
365
|
+
app = Application(env=env)
|
|
366
|
+
|
|
367
|
+
self.assertEqual(len(app.repository._fastforward_locks_inuse), 0)
|
|
368
|
+
|
|
369
|
+
# Create one aggregate.
|
|
370
|
+
original_aggregate = Aggregate()
|
|
371
|
+
app.save(original_aggregate)
|
|
372
|
+
obj_ids = set()
|
|
373
|
+
|
|
374
|
+
# Prime the cache.
|
|
375
|
+
app.repository.get(original_aggregate.id)
|
|
376
|
+
|
|
377
|
+
# Remember the aggregate ID.
|
|
378
|
+
aggregate_id = original_aggregate.id
|
|
358
379
|
|
|
359
380
|
stopped = Event()
|
|
381
|
+
errors: list[BaseException] = []
|
|
382
|
+
successful_thread_ids = set()
|
|
360
383
|
|
|
361
|
-
# Trigger, save, get, check.
|
|
362
384
|
def trigger_save_get_check() -> None:
|
|
363
385
|
while not stopped.is_set():
|
|
364
386
|
try:
|
|
387
|
+
# Get the aggregate.
|
|
365
388
|
aggregate: Aggregate = app.repository.get(aggregate_id)
|
|
389
|
+
original_version = aggregate.version
|
|
390
|
+
|
|
391
|
+
# Try to record a new event.
|
|
366
392
|
aggregate.trigger_event(Aggregate.Event)
|
|
367
|
-
|
|
393
|
+
# Give other threads a chance.
|
|
368
394
|
try:
|
|
369
395
|
app.save(aggregate)
|
|
370
396
|
except IntegrityError:
|
|
397
|
+
# Start again if we didn't record a new event.
|
|
398
|
+
# print("Got integrity error")
|
|
399
|
+
sleep(0.001)
|
|
371
400
|
continue
|
|
372
|
-
cached: Aggregate = app.repository.get(aggregate_id)
|
|
373
|
-
if saved_version > cached.version:
|
|
374
|
-
print(f"Skipped fast-forwarding at version {saved_version}")
|
|
375
|
-
stopped.set()
|
|
376
|
-
if aggregate.version % 1000 == 0:
|
|
377
|
-
print("Version:", aggregate.version, get_ident())
|
|
378
|
-
sleep(0.00)
|
|
379
|
-
except BaseException:
|
|
380
|
-
print(traceback.format_exc())
|
|
381
|
-
raise
|
|
382
401
|
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
402
|
+
# Get the aggregate from the cache.
|
|
403
|
+
assert app.repository.cache is not None
|
|
404
|
+
cached: Any = app.repository.cache.get(aggregate_id)
|
|
405
|
+
obj_ids.add(id(cached))
|
|
386
406
|
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
executor.shutdown()
|
|
407
|
+
if len(obj_ids) > 1:
|
|
408
|
+
stopped.set()
|
|
409
|
+
continue
|
|
391
410
|
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
env={
|
|
395
|
-
"AGGREGATE_CACHE_MAXSIZE": "10",
|
|
396
|
-
}
|
|
397
|
-
)
|
|
411
|
+
# Fast-forward the cached aggregate.
|
|
412
|
+
fastforwarded: Aggregate = app.repository.get(aggregate_id)
|
|
398
413
|
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
414
|
+
# Check cached aggregate was fast-forwarded with recorded event.
|
|
415
|
+
if fastforwarded.version < original_version:
|
|
416
|
+
try:
|
|
417
|
+
self.fail(
|
|
418
|
+
f"Failed to fast-forward at version {original_version}"
|
|
419
|
+
)
|
|
420
|
+
except AssertionError as e:
|
|
421
|
+
errors.append(e)
|
|
422
|
+
stopped.set()
|
|
423
|
+
continue
|
|
402
424
|
|
|
403
|
-
|
|
425
|
+
# Monitor number of threads getting involved.
|
|
426
|
+
thread_id = get_ident()
|
|
427
|
+
successful_thread_ids.add(thread_id)
|
|
404
428
|
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
aggregate: Aggregate = app.repository.get(aggregate_id)
|
|
410
|
-
aggregate.trigger_event(Aggregate.Event)
|
|
411
|
-
saved_version = aggregate.version
|
|
412
|
-
try:
|
|
413
|
-
app.save(aggregate)
|
|
414
|
-
except IntegrityError:
|
|
415
|
-
continue
|
|
416
|
-
cached: Aggregate = app.repository.get(aggregate_id)
|
|
417
|
-
if saved_version > cached.version:
|
|
418
|
-
print(f"Skipped fast-forwarding at version {saved_version}")
|
|
429
|
+
# print("Version:", aggregate.version, thread_id)
|
|
430
|
+
|
|
431
|
+
# See if we have done enough.
|
|
432
|
+
if len(successful_thread_ids) > 10 and aggregate.version >= 25:
|
|
419
433
|
stopped.set()
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
sleep(0.
|
|
423
|
-
|
|
434
|
+
continue
|
|
435
|
+
|
|
436
|
+
sleep(0.0001)
|
|
437
|
+
# sleep(0.001)
|
|
438
|
+
except BaseException as e:
|
|
439
|
+
errors.append(e)
|
|
440
|
+
stopped.set()
|
|
424
441
|
print(traceback.format_exc())
|
|
425
442
|
raise
|
|
426
443
|
|
|
427
444
|
executor = ThreadPoolExecutor(max_workers=100)
|
|
445
|
+
futures = []
|
|
428
446
|
for _ in range(100):
|
|
429
|
-
executor.submit(trigger_save_get_check)
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
447
|
+
f = executor.submit(trigger_save_get_check)
|
|
448
|
+
futures.append(f)
|
|
449
|
+
|
|
450
|
+
# Run for three seconds.
|
|
451
|
+
stopped.wait(timeout=10)
|
|
452
|
+
for f in futures:
|
|
453
|
+
f.result()
|
|
454
|
+
# print("Got all results, shutting down executor")
|
|
435
455
|
executor.shutdown()
|
|
436
456
|
|
|
457
|
+
try:
|
|
458
|
+
if errors:
|
|
459
|
+
raise errors[0]
|
|
460
|
+
if len(obj_ids) > 1:
|
|
461
|
+
self.fail(f"More than one instance used in the cache: {len(obj_ids)}")
|
|
462
|
+
if len(successful_thread_ids) < 3:
|
|
463
|
+
self.fail("Insufficient sharing across contentious threads")
|
|
464
|
+
|
|
465
|
+
final_aggregate: Aggregate = app.repository.get(aggregate_id)
|
|
466
|
+
# print("Final aggregate version:", final_aggregate.version)
|
|
467
|
+
if final_aggregate.version < 25:
|
|
468
|
+
self.fail(f"Insufficient version increment: {final_aggregate.version}")
|
|
469
|
+
|
|
470
|
+
self.assertEqual(len(app.repository._fastforward_locks_inuse), 0)
|
|
471
|
+
|
|
472
|
+
finally:
|
|
473
|
+
# print("Closing application")
|
|
474
|
+
app.close()
|
|
475
|
+
|
|
437
476
|
def test_application_with_cached_aggregates_not_fastforward(self) -> None:
|
|
438
477
|
app = Application(
|
|
439
478
|
env={
|
|
@@ -441,13 +480,37 @@ class ApplicationTestCase(TestCase):
|
|
|
441
480
|
"AGGREGATE_CACHE_FASTFORWARD": "f",
|
|
442
481
|
}
|
|
443
482
|
)
|
|
444
|
-
|
|
445
|
-
app.save(
|
|
483
|
+
aggregate1 = Aggregate()
|
|
484
|
+
app.save(aggregate1)
|
|
485
|
+
aggregate_id = aggregate1.id
|
|
486
|
+
|
|
446
487
|
# Should put the aggregate in the cache.
|
|
447
488
|
assert app.repository.cache is not None # for mypy
|
|
448
|
-
self.assertEqual(
|
|
449
|
-
app.repository.get(
|
|
450
|
-
self.assertEqual(
|
|
489
|
+
self.assertEqual(aggregate1, app.repository.cache.get(aggregate_id))
|
|
490
|
+
app.repository.get(aggregate_id)
|
|
491
|
+
self.assertEqual(aggregate1, app.repository.cache.get(aggregate_id))
|
|
492
|
+
|
|
493
|
+
aggregate2 = Aggregate()
|
|
494
|
+
aggregate2._id = aggregate_id
|
|
495
|
+
aggregate2.trigger_event(Aggregate.Event)
|
|
496
|
+
|
|
497
|
+
# This will replace object in cache.
|
|
498
|
+
app.save(aggregate2)
|
|
499
|
+
|
|
500
|
+
self.assertEqual(aggregate2.version, aggregate1.version + 1)
|
|
501
|
+
aggregate3: Aggregate = app.repository.get(aggregate_id)
|
|
502
|
+
self.assertEqual(aggregate3.version, aggregate3.version)
|
|
503
|
+
self.assertEqual(id(aggregate3.version), id(aggregate3.version))
|
|
504
|
+
|
|
505
|
+
# This will mess things up because the cache has a stale aggregate.
|
|
506
|
+
aggregate3.trigger_event(Aggregate.Event)
|
|
507
|
+
app.events.put(aggregate3.collect_events())
|
|
508
|
+
|
|
509
|
+
# And so using the aggregate to record new events will cause an IntegrityError.
|
|
510
|
+
aggregate4: Aggregate = app.repository.get(aggregate_id)
|
|
511
|
+
aggregate4.trigger_event(Aggregate.Event)
|
|
512
|
+
with self.assertRaises(IntegrityError):
|
|
513
|
+
app.save(aggregate4)
|
|
451
514
|
|
|
452
515
|
def test_application_with_deepcopy_from_cache_arg(self) -> None:
|
|
453
516
|
app = Application(
|
eventsourcing/tests/domain.py
CHANGED
|
@@ -14,9 +14,7 @@ class EmailAddress:
|
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
class BankAccount(Aggregate):
|
|
17
|
-
"""
|
|
18
|
-
Aggregate root for bank accounts.
|
|
19
|
-
"""
|
|
17
|
+
"""Aggregate root for bank accounts."""
|
|
20
18
|
|
|
21
19
|
def __init__(self, full_name: str, email_address: EmailAddress):
|
|
22
20
|
self.full_name = full_name
|
|
@@ -27,9 +25,7 @@ class BankAccount(Aggregate):
|
|
|
27
25
|
|
|
28
26
|
@classmethod
|
|
29
27
|
def open(cls, full_name: str, email_address: str) -> BankAccount:
|
|
30
|
-
"""
|
|
31
|
-
Creates new bank account object.
|
|
32
|
-
"""
|
|
28
|
+
"""Creates new bank account object."""
|
|
33
29
|
return cls._create(
|
|
34
30
|
cls.Opened,
|
|
35
31
|
id=uuid4(),
|
|
@@ -42,9 +38,7 @@ class BankAccount(Aggregate):
|
|
|
42
38
|
email_address: str
|
|
43
39
|
|
|
44
40
|
def append_transaction(self, amount: Decimal) -> None:
|
|
45
|
-
"""
|
|
46
|
-
Appends given amount as transaction on account.
|
|
47
|
-
"""
|
|
41
|
+
"""Appends given amount as transaction on account."""
|
|
48
42
|
self.check_account_is_not_closed()
|
|
49
43
|
self.check_has_sufficient_funds(amount)
|
|
50
44
|
self.trigger_event(
|
|
@@ -62,23 +56,18 @@ class BankAccount(Aggregate):
|
|
|
62
56
|
|
|
63
57
|
@dataclass(frozen=True)
|
|
64
58
|
class TransactionAppended(AggregateEvent):
|
|
65
|
-
"""
|
|
66
|
-
Domain event for when transaction
|
|
59
|
+
"""Domain event for when transaction
|
|
67
60
|
is appended to bank account.
|
|
68
61
|
"""
|
|
69
62
|
|
|
70
63
|
amount: Decimal
|
|
71
64
|
|
|
72
65
|
def apply(self, aggregate: Aggregate) -> None:
|
|
73
|
-
"""
|
|
74
|
-
|
|
75
|
-
"""
|
|
76
|
-
cast(BankAccount, aggregate).balance += self.amount
|
|
66
|
+
"""Increments the account balance."""
|
|
67
|
+
cast("BankAccount", aggregate).balance += self.amount
|
|
77
68
|
|
|
78
69
|
def set_overdraft_limit(self, overdraft_limit: Decimal) -> None:
|
|
79
|
-
"""
|
|
80
|
-
Sets the overdraft limit.
|
|
81
|
-
"""
|
|
70
|
+
"""Sets the overdraft limit."""
|
|
82
71
|
# Check the limit is not a negative value.
|
|
83
72
|
assert overdraft_limit >= Decimal("0.00")
|
|
84
73
|
self.check_account_is_not_closed()
|
|
@@ -88,38 +77,29 @@ class BankAccount(Aggregate):
|
|
|
88
77
|
)
|
|
89
78
|
|
|
90
79
|
class OverdraftLimitSet(AggregateEvent):
|
|
91
|
-
"""
|
|
92
|
-
Domain event for when overdraft
|
|
80
|
+
"""Domain event for when overdraft
|
|
93
81
|
limit is set.
|
|
94
82
|
"""
|
|
95
83
|
|
|
96
84
|
overdraft_limit: Decimal
|
|
97
85
|
|
|
98
86
|
def apply(self, aggregate: Aggregate) -> None:
|
|
99
|
-
cast(BankAccount, aggregate).overdraft_limit = self.overdraft_limit
|
|
87
|
+
cast("BankAccount", aggregate).overdraft_limit = self.overdraft_limit
|
|
100
88
|
|
|
101
89
|
def close(self) -> None:
|
|
102
|
-
"""
|
|
103
|
-
Closes the bank account.
|
|
104
|
-
"""
|
|
90
|
+
"""Closes the bank account."""
|
|
105
91
|
self.trigger_event(self.Closed)
|
|
106
92
|
|
|
107
93
|
class Closed(AggregateEvent):
|
|
108
|
-
"""
|
|
109
|
-
Domain event for when account is closed.
|
|
110
|
-
"""
|
|
94
|
+
"""Domain event for when account is closed."""
|
|
111
95
|
|
|
112
96
|
def apply(self, aggregate: Aggregate) -> None:
|
|
113
|
-
cast(BankAccount, aggregate).is_closed = True
|
|
97
|
+
cast("BankAccount", aggregate).is_closed = True
|
|
114
98
|
|
|
115
99
|
|
|
116
100
|
class AccountClosedError(Exception):
|
|
117
|
-
"""
|
|
118
|
-
Raised when attempting to operate a closed account.
|
|
119
|
-
"""
|
|
101
|
+
"""Raised when attempting to operate a closed account."""
|
|
120
102
|
|
|
121
103
|
|
|
122
104
|
class InsufficientFundsError(Exception):
|
|
123
|
-
"""
|
|
124
|
-
Raised when attempting to go past overdraft limit.
|
|
125
|
-
"""
|
|
105
|
+
"""Raised when attempting to go past overdraft limit."""
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
import os
|
|
4
3
|
import traceback
|
|
5
4
|
import zlib
|
|
6
5
|
from abc import ABC, abstractmethod
|
|
7
6
|
from concurrent.futures import ThreadPoolExecutor
|
|
8
|
-
from
|
|
7
|
+
from pathlib import Path
|
|
9
8
|
from tempfile import NamedTemporaryFile
|
|
10
9
|
from threading import Event, Thread, get_ident
|
|
11
10
|
from time import sleep
|
|
@@ -18,7 +17,7 @@ from typing_extensions import TypeVar
|
|
|
18
17
|
|
|
19
18
|
from eventsourcing.cipher import AESCipher
|
|
20
19
|
from eventsourcing.compressor import ZlibCompressor
|
|
21
|
-
from eventsourcing.domain import DomainEvent
|
|
20
|
+
from eventsourcing.domain import DomainEvent, datetime_now_with_tzinfo
|
|
22
21
|
from eventsourcing.persistence import (
|
|
23
22
|
AggregateRecorder,
|
|
24
23
|
ApplicationRecorder,
|
|
@@ -476,7 +475,7 @@ class ApplicationRecorderTestCase(TestCase, ABC, Generic[_TApplicationRecorder])
|
|
|
476
475
|
)
|
|
477
476
|
for i in range(num_events_per_write)
|
|
478
477
|
]
|
|
479
|
-
started =
|
|
478
|
+
started = datetime_now_with_tzinfo()
|
|
480
479
|
# print(f"Thread {thread_num} write beginning #{count + 1}")
|
|
481
480
|
try:
|
|
482
481
|
recorder.insert_events(stored_events)
|
|
@@ -484,16 +483,15 @@ class ApplicationRecorderTestCase(TestCase, ABC, Generic[_TApplicationRecorder])
|
|
|
484
483
|
except Exception as e: # pragma: no cover
|
|
485
484
|
if errors:
|
|
486
485
|
return
|
|
487
|
-
ended =
|
|
486
|
+
ended = datetime_now_with_tzinfo()
|
|
488
487
|
duration = (ended - started).total_seconds()
|
|
489
488
|
print(f"Error after starting {duration}", e)
|
|
490
489
|
errors.append(e)
|
|
491
490
|
else:
|
|
492
|
-
ended =
|
|
491
|
+
ended = datetime_now_with_tzinfo()
|
|
493
492
|
duration = (ended - started).total_seconds()
|
|
494
493
|
counts[thread_id] += 1
|
|
495
|
-
|
|
496
|
-
durations[thread_id] = duration
|
|
494
|
+
durations[thread_id] = max(durations[thread_id], duration)
|
|
497
495
|
sleep(writer_sleep)
|
|
498
496
|
|
|
499
497
|
stop_reading = Event()
|
|
@@ -584,7 +582,7 @@ class ApplicationRecorderTestCase(TestCase, ABC, Generic[_TApplicationRecorder])
|
|
|
584
582
|
|
|
585
583
|
# Run.
|
|
586
584
|
with ThreadPoolExecutor(max_workers=num_workers) as executor:
|
|
587
|
-
started =
|
|
585
|
+
started = datetime_now_with_tzinfo()
|
|
588
586
|
futures = []
|
|
589
587
|
for _ in range(num_jobs):
|
|
590
588
|
future = executor.submit(insert_events)
|
|
@@ -593,7 +591,7 @@ class ApplicationRecorderTestCase(TestCase, ABC, Generic[_TApplicationRecorder])
|
|
|
593
591
|
future.result()
|
|
594
592
|
|
|
595
593
|
self.assertFalse(errors_happened.is_set(), "There were errors (see above)")
|
|
596
|
-
ended =
|
|
594
|
+
ended = datetime_now_with_tzinfo()
|
|
597
595
|
rate = num_jobs * num_events_per_job / (ended - started).total_seconds()
|
|
598
596
|
print(f"Rate: {rate:.0f} inserts per second")
|
|
599
597
|
|
|
@@ -1099,7 +1097,7 @@ class InfrastructureFactoryTestCase(ABC, TestCase, Generic[_TInfrastrutureFactor
|
|
|
1099
1097
|
|
|
1100
1098
|
def setUp(self) -> None:
|
|
1101
1099
|
self.factory = cast(
|
|
1102
|
-
_TInfrastrutureFactory, InfrastructureFactory.construct(self.env)
|
|
1100
|
+
"_TInfrastrutureFactory", InfrastructureFactory.construct(self.env)
|
|
1103
1101
|
)
|
|
1104
1102
|
self.assertIsInstance(self.factory, self.expected_factory_class())
|
|
1105
1103
|
self.transcoder = JSONTranscoder()
|
|
@@ -1279,14 +1277,17 @@ class InfrastructureFactoryTestCase(ABC, TestCase, Generic[_TInfrastrutureFactor
|
|
|
1279
1277
|
|
|
1280
1278
|
def tmpfile_uris() -> Iterator[str]:
|
|
1281
1279
|
tmp_files = []
|
|
1282
|
-
ram_disk_path = "/Volumes/RAM DISK/"
|
|
1283
|
-
prefix = None
|
|
1284
|
-
if
|
|
1285
|
-
prefix = ram_disk_path
|
|
1280
|
+
ram_disk_path = Path("/Volumes/RAM DISK/")
|
|
1281
|
+
prefix: str | None = None
|
|
1282
|
+
if ram_disk_path.exists():
|
|
1283
|
+
prefix = str(ram_disk_path)
|
|
1286
1284
|
while True:
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1285
|
+
with NamedTemporaryFile(
|
|
1286
|
+
prefix=prefix,
|
|
1287
|
+
suffix="_eventsourcing_test.db",
|
|
1288
|
+
) as tmp_file:
|
|
1289
|
+
tmp_files.append(tmp_file)
|
|
1290
|
+
yield "file:" + tmp_file.name
|
|
1290
1291
|
|
|
1291
1292
|
|
|
1292
1293
|
class CustomType1:
|
|
@@ -1329,6 +1330,8 @@ class MyList(list[_T]):
|
|
|
1329
1330
|
|
|
1330
1331
|
|
|
1331
1332
|
class MyStr(str):
|
|
1333
|
+
__slots__ = ()
|
|
1334
|
+
|
|
1332
1335
|
def __repr__(self) -> str:
|
|
1333
1336
|
return f"{type(self).__name__}({super().__repr__()})"
|
|
1334
1337
|
|
eventsourcing/utils.py
CHANGED
|
@@ -16,9 +16,7 @@ if TYPE_CHECKING:
|
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
class TopicError(Exception):
|
|
19
|
-
"""
|
|
20
|
-
Raised when topic doesn't resolve.
|
|
21
|
-
"""
|
|
19
|
+
"""Raised when topic doesn't resolve."""
|
|
22
20
|
|
|
23
21
|
|
|
24
22
|
SupportsTopic = Union[type, Callable[..., Any], ModuleType]
|
|
@@ -29,8 +27,7 @@ _topic_cache_lock = Lock()
|
|
|
29
27
|
|
|
30
28
|
|
|
31
29
|
def get_topic(obj: SupportsTopic, /) -> str:
|
|
32
|
-
"""
|
|
33
|
-
Returns a "topic string" that locates the given class
|
|
30
|
+
"""Returns a "topic string" that locates the given class
|
|
34
31
|
in its module. The string is formed by joining the
|
|
35
32
|
module name and the class qualname separated by the
|
|
36
33
|
colon character.
|
|
@@ -45,8 +42,7 @@ def get_topic(obj: SupportsTopic, /) -> str:
|
|
|
45
42
|
|
|
46
43
|
|
|
47
44
|
def resolve_topic(topic: str) -> Any:
|
|
48
|
-
"""
|
|
49
|
-
Returns an object located by the given topic.
|
|
45
|
+
"""Returns an object located by the given topic.
|
|
50
46
|
|
|
51
47
|
This function can be (is) used to locate domain
|
|
52
48
|
event classes and aggregate classes from the
|
|
@@ -101,8 +97,7 @@ def resolve_topic(topic: str) -> Any:
|
|
|
101
97
|
|
|
102
98
|
|
|
103
99
|
def register_topic(topic: str, obj: SupportsTopic) -> None:
|
|
104
|
-
"""
|
|
105
|
-
Registers a topic with an object, so the object will be
|
|
100
|
+
"""Registers a topic with an object, so the object will be
|
|
106
101
|
returned whenever the topic is resolved.
|
|
107
102
|
|
|
108
103
|
This function can be used to cache the topic of a class, so
|
|
@@ -134,8 +129,7 @@ def retry(
|
|
|
134
129
|
wait: float = 0,
|
|
135
130
|
stall: float = 0,
|
|
136
131
|
) -> Callable[[Any], Any]:
|
|
137
|
-
"""
|
|
138
|
-
Retry decorator.
|
|
132
|
+
"""Retry decorator.
|
|
139
133
|
|
|
140
134
|
:param exc: List of exceptions that will cause the call to be retried if raised.
|
|
141
135
|
:param max_attempts: Maximum number of attempts to try.
|
|
@@ -145,9 +139,9 @@ def retry(
|
|
|
145
139
|
"""
|
|
146
140
|
|
|
147
141
|
@no_type_check
|
|
148
|
-
def _retry(func):
|
|
142
|
+
def _retry(func: Callable) -> Callable:
|
|
149
143
|
@wraps(func)
|
|
150
|
-
def retry_decorator(*args, **kwargs):
|
|
144
|
+
def retry_decorator(*args: Any, **kwargs: Any) -> Any:
|
|
151
145
|
if stall:
|
|
152
146
|
sleep(stall)
|
|
153
147
|
attempts = 0
|
|
@@ -236,16 +230,16 @@ class Environment(dict[str, str]):
|
|
|
236
230
|
self.name = name
|
|
237
231
|
|
|
238
232
|
@overload # type: ignore[override]
|
|
239
|
-
def get(self, __key: str) -> str | None: ... # pragma: no cover
|
|
233
|
+
def get(self, __key: str, /) -> str | None: ... # pragma: no cover
|
|
240
234
|
|
|
241
235
|
@overload
|
|
242
|
-
def get(self, __key: str, __default: str) -> str: ... # pragma: no cover
|
|
236
|
+
def get(self, __key: str, /, __default: str) -> str: ... # pragma: no cover
|
|
243
237
|
|
|
244
238
|
@overload
|
|
245
|
-
def get(self, __key: str, __default: T) -> str | T: ... # pragma: no cover
|
|
239
|
+
def get(self, __key: str, /, __default: T) -> str | T: ... # pragma: no cover
|
|
246
240
|
|
|
247
241
|
def get( # pyright: ignore [reportIncompatibleMethodOverride]
|
|
248
|
-
self, __key: str, __default: str | T | None = None
|
|
242
|
+
self, __key: str, /, __default: str | T | None = None
|
|
249
243
|
) -> str | T | None:
|
|
250
244
|
for _key in self.create_keys(__key):
|
|
251
245
|
value = super().get(_key, None)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
eventsourcing/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
eventsourcing/application.py,sha256=K3M9_Rh2jaYzBDPMvmyemLXHQ4GsGfGsxmMfUeVSqXw,35507
|
|
3
|
+
eventsourcing/cipher.py,sha256=ulTBtX5K9ejRAkdUaUbdIaj4H7anYwDOi7JxOolj2uo,3295
|
|
4
|
+
eventsourcing/compressor.py,sha256=qEYWvsUXFLyhKgfuv-HGNJ6VF4sRw4z0IxbNW9ukOfc,385
|
|
5
|
+
eventsourcing/cryptography.py,sha256=aFZLlJxxSb5seVbh94-T8FA_RIGOe-VFu5SJrbOnwUU,2969
|
|
6
|
+
eventsourcing/dispatch.py,sha256=j03cIVPziq6LFEgJxvQMMIPlixuZ4bB8ynXXdd_Tj8Q,2740
|
|
7
|
+
eventsourcing/domain.py,sha256=Vk7MvA7GsY3krFBcotIxuMoDxYkBPP-EMnEIOj-JTU8,60062
|
|
8
|
+
eventsourcing/interface.py,sha256=-VLoqcd9a0PXpD_Bv0LjCiG21xLREG6tXK6phgtShOw,5035
|
|
9
|
+
eventsourcing/persistence.py,sha256=y_1o3LNi9tkOTqkvjgsGF4un4XPXEgxzt0Iwhk7UzEI,46340
|
|
10
|
+
eventsourcing/popo.py,sha256=xZD6mig7bVwAoHe-UdraXvuu2iL5a8b2b41cEcBHlBU,9642
|
|
11
|
+
eventsourcing/postgres.py,sha256=lcbLvHCiUCYyHeIn4Fh7RHEjrbdmIrZC9XwfTOH4t4Q,37544
|
|
12
|
+
eventsourcing/projection.py,sha256=-vhjyOr08D42TGJX9ISeGx8H3sOuyKlIajPGek4aUtM,9775
|
|
13
|
+
eventsourcing/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
14
|
+
eventsourcing/sqlite.py,sha256=_9ENoxEY7hqHGhOS4HBgElibBXyHI2vJ5nUVAQuffD0,22016
|
|
15
|
+
eventsourcing/system.py,sha256=3mbxQRFLg8wQ8ulThd6y0CKBjtmo6lR3gZf9ZFITNCI,46888
|
|
16
|
+
eventsourcing/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
17
|
+
eventsourcing/tests/application.py,sha256=pAn9Cugp_1rjOtj_nruGhh7PxdrQWibDlrnOAlOKXwo,20614
|
|
18
|
+
eventsourcing/tests/domain.py,sha256=yN-F6gMRumeX6nIXIcZGxAR3RrUslzmEMM8JksnkI8Q,3227
|
|
19
|
+
eventsourcing/tests/persistence.py,sha256=DVSKySk86iRW-QKGgs-4ca1C956N1LoGhYEXXesBF6k,58648
|
|
20
|
+
eventsourcing/tests/postgres_utils.py,sha256=0ywklGp6cXZ5PmV8ANVkwSHsZZCl5zTmOk7iG-RmrCE,1548
|
|
21
|
+
eventsourcing/utils.py,sha256=pOnczXzaE5q7UbQbPmgcpWaP660fsmfiDJs6Gmo8QCM,8558
|
|
22
|
+
eventsourcing-9.4.0b3.dist-info/AUTHORS,sha256=8aHOM4UbNZcKlD-cHpFRcM6RWyCqtwtxRev6DeUgVRs,137
|
|
23
|
+
eventsourcing-9.4.0b3.dist-info/LICENSE,sha256=CQEQzcZO8AWXL5i3hIo4yVKrYjh2FBz6hCM7kpXWpw4,1512
|
|
24
|
+
eventsourcing-9.4.0b3.dist-info/METADATA,sha256=EZrHitWWM3yUoXD38I2u0SSNsDutr9zNg8AfFXno8mQ,9796
|
|
25
|
+
eventsourcing-9.4.0b3.dist-info/WHEEL,sha256=fGIA9gx4Qxk2KDKeNJCbOEwSrmLtjWCwzBz351GyrPQ,88
|
|
26
|
+
eventsourcing-9.4.0b3.dist-info/RECORD,,
|