eventsourcing 9.3.4__py3-none-any.whl → 9.4.0a1__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 +75 -26
- eventsourcing/cipher.py +1 -1
- eventsourcing/domain.py +29 -3
- eventsourcing/interface.py +23 -5
- eventsourcing/persistence.py +292 -71
- eventsourcing/popo.py +113 -32
- eventsourcing/postgres.py +265 -103
- eventsourcing/projection.py +157 -0
- eventsourcing/sqlite.py +143 -36
- eventsourcing/system.py +89 -44
- eventsourcing/tests/application.py +48 -12
- eventsourcing/tests/persistence.py +304 -75
- eventsourcing/utils.py +1 -1
- {eventsourcing-9.3.4.dist-info → eventsourcing-9.4.0a1.dist-info}/LICENSE +1 -1
- {eventsourcing-9.3.4.dist-info → eventsourcing-9.4.0a1.dist-info}/METADATA +2 -2
- eventsourcing-9.4.0a1.dist-info/RECORD +25 -0
- eventsourcing-9.3.4.dist-info/RECORD +0 -24
- {eventsourcing-9.3.4.dist-info → eventsourcing-9.4.0a1.dist-info}/AUTHORS +0 -0
- {eventsourcing-9.3.4.dist-info → eventsourcing-9.4.0a1.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import weakref
|
|
5
|
+
from abc import ABC, abstractmethod
|
|
6
|
+
from threading import Event, Thread
|
|
7
|
+
from traceback import format_exc
|
|
8
|
+
from typing import TYPE_CHECKING, Any, Dict, Generic, Type, TypeVar
|
|
9
|
+
from warnings import warn
|
|
10
|
+
|
|
11
|
+
from eventsourcing.application import Application
|
|
12
|
+
from eventsourcing.dispatch import singledispatchmethod
|
|
13
|
+
from eventsourcing.persistence import (
|
|
14
|
+
InfrastructureFactory,
|
|
15
|
+
Tracking,
|
|
16
|
+
TrackingRecorder,
|
|
17
|
+
TTrackingRecorder,
|
|
18
|
+
WaitInterruptedError,
|
|
19
|
+
)
|
|
20
|
+
from eventsourcing.utils import Environment, EnvType
|
|
21
|
+
|
|
22
|
+
if TYPE_CHECKING: # pragma: no cover
|
|
23
|
+
from typing_extensions import Self
|
|
24
|
+
|
|
25
|
+
from eventsourcing.application import ApplicationSubscription
|
|
26
|
+
from eventsourcing.domain import DomainEventProtocol
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class Projection(ABC, Generic[TTrackingRecorder]):
|
|
30
|
+
name: str = ""
|
|
31
|
+
|
|
32
|
+
def __init__(
|
|
33
|
+
self,
|
|
34
|
+
tracking_recorder: TTrackingRecorder,
|
|
35
|
+
):
|
|
36
|
+
self.tracking_recorder = tracking_recorder
|
|
37
|
+
|
|
38
|
+
@singledispatchmethod
|
|
39
|
+
@abstractmethod
|
|
40
|
+
def process_event(
|
|
41
|
+
self, domain_event: DomainEventProtocol, tracking: Tracking
|
|
42
|
+
) -> None:
|
|
43
|
+
"""
|
|
44
|
+
Process a domain event and track it.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
TProjection = TypeVar("TProjection", bound=Projection[Any])
|
|
49
|
+
TApplication = TypeVar("TApplication", bound=Application)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class ProjectionRunner(Generic[TApplication, TTrackingRecorder]):
|
|
53
|
+
def __init__(
|
|
54
|
+
self,
|
|
55
|
+
*,
|
|
56
|
+
application_class: Type[TApplication],
|
|
57
|
+
projection_class: Type[Projection[TTrackingRecorder]],
|
|
58
|
+
tracking_recorder_class: Type[TTrackingRecorder] | None = None,
|
|
59
|
+
env: EnvType | None = None,
|
|
60
|
+
):
|
|
61
|
+
self.app: TApplication = application_class(env)
|
|
62
|
+
|
|
63
|
+
projection_environment = self._construct_env(
|
|
64
|
+
name=projection_class.name or projection_class.__name__, env=env
|
|
65
|
+
)
|
|
66
|
+
self.projection_factory: InfrastructureFactory[TTrackingRecorder] = (
|
|
67
|
+
InfrastructureFactory.construct(env=projection_environment)
|
|
68
|
+
)
|
|
69
|
+
self.tracking_recorder: TTrackingRecorder = (
|
|
70
|
+
self.projection_factory.tracking_recorder(tracking_recorder_class)
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
self.subscription = self.app.subscribe(
|
|
74
|
+
gt=self.tracking_recorder.max_tracking_id(self.app.name)
|
|
75
|
+
)
|
|
76
|
+
self.projection = projection_class(
|
|
77
|
+
tracking_recorder=self.tracking_recorder,
|
|
78
|
+
)
|
|
79
|
+
self._has_error = Event()
|
|
80
|
+
self.thread_error: BaseException | None = None
|
|
81
|
+
self.processing_thread = Thread(
|
|
82
|
+
target=self._process_events_loop,
|
|
83
|
+
kwargs={
|
|
84
|
+
"subscription": self.subscription,
|
|
85
|
+
"projection": self.projection,
|
|
86
|
+
"has_error": self._has_error,
|
|
87
|
+
"runner": weakref.ref(self),
|
|
88
|
+
},
|
|
89
|
+
)
|
|
90
|
+
self.processing_thread.start()
|
|
91
|
+
|
|
92
|
+
def _construct_env(self, name: str, env: EnvType | None = None) -> Environment:
|
|
93
|
+
"""
|
|
94
|
+
Constructs environment from which projection will be configured.
|
|
95
|
+
"""
|
|
96
|
+
_env: Dict[str, str] = {}
|
|
97
|
+
_env.update(os.environ)
|
|
98
|
+
if env is not None:
|
|
99
|
+
_env.update(env)
|
|
100
|
+
return Environment(name, _env)
|
|
101
|
+
|
|
102
|
+
def stop(self) -> None:
|
|
103
|
+
self.subscription.subscription.stop()
|
|
104
|
+
|
|
105
|
+
@staticmethod
|
|
106
|
+
def _process_events_loop(
|
|
107
|
+
subscription: ApplicationSubscription,
|
|
108
|
+
projection: Projection[TrackingRecorder],
|
|
109
|
+
has_error: Event,
|
|
110
|
+
runner: weakref.ReferenceType[ProjectionRunner[Application, TrackingRecorder]],
|
|
111
|
+
) -> None:
|
|
112
|
+
try:
|
|
113
|
+
with subscription:
|
|
114
|
+
for domain_event, tracking in subscription:
|
|
115
|
+
projection.process_event(domain_event, tracking)
|
|
116
|
+
except BaseException as e:
|
|
117
|
+
_runner = runner() # get reference from weakref
|
|
118
|
+
if _runner is not None:
|
|
119
|
+
_runner.thread_error = e
|
|
120
|
+
else:
|
|
121
|
+
msg = "ProjectionRunner was deleted before error could be assigned:\n"
|
|
122
|
+
msg += format_exc()
|
|
123
|
+
warn(
|
|
124
|
+
msg,
|
|
125
|
+
RuntimeWarning,
|
|
126
|
+
stacklevel=2,
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
has_error.set()
|
|
130
|
+
subscription.subscription.stop()
|
|
131
|
+
|
|
132
|
+
def run_forever(self, timeout: float | None = None) -> None:
|
|
133
|
+
if self._has_error.wait(timeout=timeout):
|
|
134
|
+
assert self.thread_error is not None # for mypy
|
|
135
|
+
raise self.thread_error
|
|
136
|
+
|
|
137
|
+
def wait(self, notification_id: int, timeout: float = 1.0) -> None:
|
|
138
|
+
try:
|
|
139
|
+
self.projection.tracking_recorder.wait(
|
|
140
|
+
application_name=self.subscription.name,
|
|
141
|
+
notification_id=notification_id,
|
|
142
|
+
timeout=timeout,
|
|
143
|
+
interrupt=self._has_error,
|
|
144
|
+
)
|
|
145
|
+
except WaitInterruptedError:
|
|
146
|
+
assert self.thread_error is not None # for mypy
|
|
147
|
+
raise self.thread_error from None
|
|
148
|
+
|
|
149
|
+
def __enter__(self) -> Self:
|
|
150
|
+
return self
|
|
151
|
+
|
|
152
|
+
def __exit__(self, *args: object, **kwargs: Any) -> None:
|
|
153
|
+
self.stop()
|
|
154
|
+
self.processing_thread.join()
|
|
155
|
+
|
|
156
|
+
def __del__(self) -> None:
|
|
157
|
+
self.stop()
|
eventsourcing/sqlite.py
CHANGED
|
@@ -23,12 +23,15 @@ from eventsourcing.persistence import (
|
|
|
23
23
|
PersistenceError,
|
|
24
24
|
ProcessRecorder,
|
|
25
25
|
ProgrammingError,
|
|
26
|
+
Recorder,
|
|
26
27
|
StoredEvent,
|
|
28
|
+
Subscription,
|
|
27
29
|
Tracking,
|
|
30
|
+
TrackingRecorder,
|
|
28
31
|
)
|
|
29
|
-
from eventsourcing.utils import Environment, strtobool
|
|
32
|
+
from eventsourcing.utils import Environment, resolve_topic, strtobool
|
|
30
33
|
|
|
31
|
-
if TYPE_CHECKING: # pragma:
|
|
34
|
+
if TYPE_CHECKING: # pragma: no cover
|
|
32
35
|
from types import TracebackType
|
|
33
36
|
|
|
34
37
|
SQLITE3_DEFAULT_LOCK_TIMEOUT = 5
|
|
@@ -242,16 +245,32 @@ class SQLiteDatastore:
|
|
|
242
245
|
self.close()
|
|
243
246
|
|
|
244
247
|
|
|
245
|
-
class
|
|
248
|
+
class SQLiteRecorder(Recorder):
|
|
246
249
|
def __init__(
|
|
247
250
|
self,
|
|
248
251
|
datastore: SQLiteDatastore,
|
|
249
|
-
events_table_name: str = "stored_events",
|
|
250
252
|
):
|
|
251
253
|
assert isinstance(datastore, SQLiteDatastore)
|
|
252
254
|
self.datastore = datastore
|
|
253
|
-
self.events_table_name = events_table_name
|
|
254
255
|
self.create_table_statements = self.construct_create_table_statements()
|
|
256
|
+
|
|
257
|
+
def construct_create_table_statements(self) -> List[str]:
|
|
258
|
+
return []
|
|
259
|
+
|
|
260
|
+
def create_table(self) -> None:
|
|
261
|
+
with self.datastore.transaction(commit=True) as c:
|
|
262
|
+
for statement in self.create_table_statements:
|
|
263
|
+
c.execute(statement)
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
class SQLiteAggregateRecorder(SQLiteRecorder, AggregateRecorder):
|
|
267
|
+
def __init__(
|
|
268
|
+
self,
|
|
269
|
+
datastore: SQLiteDatastore,
|
|
270
|
+
events_table_name: str = "stored_events",
|
|
271
|
+
):
|
|
272
|
+
self.events_table_name = events_table_name
|
|
273
|
+
super().__init__(datastore)
|
|
255
274
|
self.insert_events_statement = (
|
|
256
275
|
f"INSERT INTO {self.events_table_name} VALUES (?,?,?,?)"
|
|
257
276
|
)
|
|
@@ -260,7 +279,8 @@ class SQLiteAggregateRecorder(AggregateRecorder):
|
|
|
260
279
|
)
|
|
261
280
|
|
|
262
281
|
def construct_create_table_statements(self) -> List[str]:
|
|
263
|
-
|
|
282
|
+
statements = super().construct_create_table_statements()
|
|
283
|
+
statements.append(
|
|
264
284
|
"CREATE TABLE IF NOT EXISTS "
|
|
265
285
|
f"{self.events_table_name} ("
|
|
266
286
|
"originator_id TEXT, "
|
|
@@ -271,12 +291,7 @@ class SQLiteAggregateRecorder(AggregateRecorder):
|
|
|
271
291
|
"(originator_id, originator_version)) "
|
|
272
292
|
"WITHOUT ROWID"
|
|
273
293
|
)
|
|
274
|
-
return
|
|
275
|
-
|
|
276
|
-
def create_table(self) -> None:
|
|
277
|
-
with self.datastore.transaction(commit=True) as c:
|
|
278
|
-
for statement in self.create_table_statements:
|
|
279
|
-
c.execute(statement)
|
|
294
|
+
return statements
|
|
280
295
|
|
|
281
296
|
def insert_events(
|
|
282
297
|
self, stored_events: List[StoredEvent], **kwargs: Any
|
|
@@ -389,25 +404,45 @@ class SQLiteApplicationRecorder(
|
|
|
389
404
|
|
|
390
405
|
def select_notifications(
|
|
391
406
|
self,
|
|
392
|
-
start: int,
|
|
407
|
+
start: int | None,
|
|
393
408
|
limit: int,
|
|
394
409
|
stop: int | None = None,
|
|
395
410
|
topics: Sequence[str] = (),
|
|
411
|
+
*,
|
|
412
|
+
inclusive_of_start: bool = True,
|
|
396
413
|
) -> List[Notification]:
|
|
397
414
|
"""
|
|
398
415
|
Returns a list of event notifications
|
|
399
416
|
from 'start', limited by 'limit'.
|
|
400
417
|
"""
|
|
401
|
-
params: List[int | str] = [
|
|
402
|
-
statement = f"SELECT rowid, * FROM {self.events_table_name}
|
|
418
|
+
params: List[int | str] = []
|
|
419
|
+
statement = f"SELECT rowid, * FROM {self.events_table_name} "
|
|
420
|
+
has_where = False
|
|
421
|
+
if start is not None:
|
|
422
|
+
has_where = True
|
|
423
|
+
statement += "WHERE "
|
|
424
|
+
params.append(start)
|
|
425
|
+
if inclusive_of_start:
|
|
426
|
+
statement += "rowid>=? "
|
|
427
|
+
else:
|
|
428
|
+
statement += "rowid>? "
|
|
403
429
|
|
|
404
430
|
if stop is not None:
|
|
431
|
+
if not has_where:
|
|
432
|
+
has_where = True
|
|
433
|
+
statement += "WHERE "
|
|
434
|
+
else:
|
|
435
|
+
statement += "AND "
|
|
405
436
|
params.append(stop)
|
|
406
|
-
statement += "
|
|
437
|
+
statement += "rowid<=? "
|
|
407
438
|
|
|
408
439
|
if topics:
|
|
440
|
+
if not has_where:
|
|
441
|
+
statement += "WHERE "
|
|
442
|
+
else:
|
|
443
|
+
statement += "AND "
|
|
409
444
|
params += list(topics)
|
|
410
|
-
statement += "
|
|
445
|
+
statement += "topic IN (%s) " % ",".join("?" * len(topics))
|
|
411
446
|
|
|
412
447
|
params.append(limit)
|
|
413
448
|
statement += "ORDER BY rowid LIMIT ?"
|
|
@@ -434,19 +469,20 @@ class SQLiteApplicationRecorder(
|
|
|
434
469
|
|
|
435
470
|
def _max_notification_id(self, c: SQLiteCursor) -> int:
|
|
436
471
|
c.execute(self.select_max_notification_id_statement)
|
|
437
|
-
return c.fetchone()[0]
|
|
472
|
+
return c.fetchone()[0]
|
|
438
473
|
|
|
474
|
+
def subscribe(self, gt: int | None = None) -> Subscription[ApplicationRecorder]:
|
|
475
|
+
msg = f"The {type(self).__qualname__} recorder does not support subscriptions"
|
|
476
|
+
raise NotImplementedError(msg)
|
|
439
477
|
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
ProcessRecorder,
|
|
443
|
-
):
|
|
478
|
+
|
|
479
|
+
class SQLiteTrackingRecorder(SQLiteRecorder, TrackingRecorder):
|
|
444
480
|
def __init__(
|
|
445
481
|
self,
|
|
446
482
|
datastore: SQLiteDatastore,
|
|
447
|
-
|
|
483
|
+
**kwargs: Any,
|
|
448
484
|
):
|
|
449
|
-
super().__init__(datastore,
|
|
485
|
+
super().__init__(datastore, **kwargs)
|
|
450
486
|
self.insert_tracking_statement = "INSERT INTO tracking VALUES (?,?)"
|
|
451
487
|
self.select_max_tracking_id_statement = (
|
|
452
488
|
"SELECT MAX(notification_id) FROM tracking WHERE application_name=?"
|
|
@@ -468,11 +504,28 @@ class SQLiteProcessRecorder(
|
|
|
468
504
|
)
|
|
469
505
|
return statements
|
|
470
506
|
|
|
471
|
-
def
|
|
507
|
+
def insert_tracking(self, tracking: Tracking) -> None:
|
|
508
|
+
with self.datastore.transaction(commit=True) as c:
|
|
509
|
+
self._insert_tracking(c, tracking)
|
|
510
|
+
|
|
511
|
+
def _insert_tracking(
|
|
512
|
+
self,
|
|
513
|
+
c: SQLiteCursor,
|
|
514
|
+
tracking: Tracking,
|
|
515
|
+
) -> None:
|
|
516
|
+
c.execute(
|
|
517
|
+
self.insert_tracking_statement,
|
|
518
|
+
(
|
|
519
|
+
tracking.application_name,
|
|
520
|
+
tracking.notification_id,
|
|
521
|
+
),
|
|
522
|
+
)
|
|
523
|
+
|
|
524
|
+
def max_tracking_id(self, application_name: str) -> int | None:
|
|
472
525
|
params = [application_name]
|
|
473
526
|
with self.datastore.transaction(commit=False) as c:
|
|
474
527
|
c.execute(self.select_max_tracking_id_statement, params)
|
|
475
|
-
return c.fetchone()[0]
|
|
528
|
+
return c.fetchone()[0]
|
|
476
529
|
|
|
477
530
|
def has_tracking_id(self, application_name: str, notification_id: int) -> bool:
|
|
478
531
|
params = [application_name, notification_id]
|
|
@@ -480,6 +533,20 @@ class SQLiteProcessRecorder(
|
|
|
480
533
|
c.execute(self.count_tracking_id_statement, params)
|
|
481
534
|
return bool(c.fetchone()[0])
|
|
482
535
|
|
|
536
|
+
|
|
537
|
+
class SQLiteProcessRecorder(
|
|
538
|
+
SQLiteTrackingRecorder,
|
|
539
|
+
SQLiteApplicationRecorder,
|
|
540
|
+
ProcessRecorder,
|
|
541
|
+
):
|
|
542
|
+
def __init__(
|
|
543
|
+
self,
|
|
544
|
+
datastore: SQLiteDatastore,
|
|
545
|
+
*,
|
|
546
|
+
events_table_name: str = "stored_events",
|
|
547
|
+
):
|
|
548
|
+
super().__init__(datastore, events_table_name=events_table_name)
|
|
549
|
+
|
|
483
550
|
def _insert_events(
|
|
484
551
|
self,
|
|
485
552
|
c: SQLiteCursor,
|
|
@@ -489,23 +556,18 @@ class SQLiteProcessRecorder(
|
|
|
489
556
|
returning = super()._insert_events(c, stored_events, **kwargs)
|
|
490
557
|
tracking: Tracking | None = kwargs.get("tracking", None)
|
|
491
558
|
if tracking is not None:
|
|
492
|
-
|
|
493
|
-
self.insert_tracking_statement,
|
|
494
|
-
(
|
|
495
|
-
tracking.application_name,
|
|
496
|
-
tracking.notification_id,
|
|
497
|
-
),
|
|
498
|
-
)
|
|
559
|
+
self._insert_tracking(c, tracking)
|
|
499
560
|
return returning
|
|
500
561
|
|
|
501
562
|
|
|
502
|
-
class
|
|
563
|
+
class SQLiteFactory(InfrastructureFactory[SQLiteTrackingRecorder]):
|
|
503
564
|
SQLITE_DBNAME = "SQLITE_DBNAME"
|
|
504
565
|
SQLITE_LOCK_TIMEOUT = "SQLITE_LOCK_TIMEOUT"
|
|
505
566
|
CREATE_TABLE = "CREATE_TABLE"
|
|
506
567
|
|
|
507
568
|
aggregate_recorder_class = SQLiteAggregateRecorder
|
|
508
569
|
application_recorder_class = SQLiteApplicationRecorder
|
|
570
|
+
tracking_recorder_class = SQLiteTrackingRecorder
|
|
509
571
|
process_recorder_class = SQLiteProcessRecorder
|
|
510
572
|
|
|
511
573
|
def __init__(self, env: Environment):
|
|
@@ -549,13 +611,55 @@ class Factory(InfrastructureFactory):
|
|
|
549
611
|
return recorder
|
|
550
612
|
|
|
551
613
|
def application_recorder(self) -> ApplicationRecorder:
|
|
552
|
-
|
|
614
|
+
application_recorder_topic = self.env.get(self.APPLICATION_RECORDER_TOPIC)
|
|
615
|
+
|
|
616
|
+
if application_recorder_topic:
|
|
617
|
+
application_recorder_class: Type[SQLiteApplicationRecorder] = resolve_topic(
|
|
618
|
+
application_recorder_topic
|
|
619
|
+
)
|
|
620
|
+
assert issubclass(application_recorder_class, SQLiteApplicationRecorder)
|
|
621
|
+
else:
|
|
622
|
+
application_recorder_class = self.application_recorder_class
|
|
623
|
+
|
|
624
|
+
recorder = application_recorder_class(datastore=self.datastore)
|
|
625
|
+
|
|
626
|
+
if self.env_create_table():
|
|
627
|
+
recorder.create_table()
|
|
628
|
+
return recorder
|
|
629
|
+
|
|
630
|
+
def tracking_recorder(
|
|
631
|
+
self, tracking_recorder_class: Type[SQLiteTrackingRecorder] | None = None
|
|
632
|
+
) -> SQLiteTrackingRecorder:
|
|
633
|
+
if tracking_recorder_class is None:
|
|
634
|
+
tracking_recorder_topic = self.env.get(self.TRACKING_RECORDER_TOPIC)
|
|
635
|
+
|
|
636
|
+
if tracking_recorder_topic:
|
|
637
|
+
tracking_recorder_class = resolve_topic(tracking_recorder_topic)
|
|
638
|
+
else:
|
|
639
|
+
tracking_recorder_class = self.tracking_recorder_class
|
|
640
|
+
|
|
641
|
+
assert tracking_recorder_class is not None
|
|
642
|
+
assert issubclass(tracking_recorder_class, SQLiteTrackingRecorder)
|
|
643
|
+
|
|
644
|
+
recorder = tracking_recorder_class(datastore=self.datastore)
|
|
645
|
+
|
|
553
646
|
if self.env_create_table():
|
|
554
647
|
recorder.create_table()
|
|
555
648
|
return recorder
|
|
556
649
|
|
|
557
650
|
def process_recorder(self) -> ProcessRecorder:
|
|
558
|
-
|
|
651
|
+
process_recorder_topic = self.env.get(self.PROCESS_RECORDER_TOPIC)
|
|
652
|
+
|
|
653
|
+
if process_recorder_topic:
|
|
654
|
+
process_recorder_class: Type[SQLiteProcessRecorder] = resolve_topic(
|
|
655
|
+
process_recorder_topic
|
|
656
|
+
)
|
|
657
|
+
assert issubclass(process_recorder_class, SQLiteProcessRecorder)
|
|
658
|
+
else:
|
|
659
|
+
process_recorder_class = self.process_recorder_class
|
|
660
|
+
|
|
661
|
+
recorder = process_recorder_class(datastore=self.datastore)
|
|
662
|
+
|
|
559
663
|
if self.env_create_table():
|
|
560
664
|
recorder.create_table()
|
|
561
665
|
return recorder
|
|
@@ -566,3 +670,6 @@ class Factory(InfrastructureFactory):
|
|
|
566
670
|
|
|
567
671
|
def close(self) -> None:
|
|
568
672
|
self.datastore.close()
|
|
673
|
+
|
|
674
|
+
|
|
675
|
+
Factory = SQLiteFactory
|