neuroglia-python 0.0.23__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.
Files changed (58) hide show
  1. neuroglia/__init__.py +0 -0
  2. neuroglia/core/__init__.py +4 -0
  3. neuroglia/core/module_loader.py +13 -0
  4. neuroglia/core/operation_result.py +11 -0
  5. neuroglia/core/problem_details.py +25 -0
  6. neuroglia/core/type_extensions.py +75 -0
  7. neuroglia/core/type_finder.py +24 -0
  8. neuroglia/data/__init__.py +1 -0
  9. neuroglia/data/abstractions.py +124 -0
  10. neuroglia/data/infrastructure/__init__.py +0 -0
  11. neuroglia/data/infrastructure/abstractions.py +78 -0
  12. neuroglia/data/infrastructure/event_sourcing/__init__.py +0 -0
  13. neuroglia/data/infrastructure/event_sourcing/abstractions.py +126 -0
  14. neuroglia/data/infrastructure/event_sourcing/event_sourcing_repository.py +77 -0
  15. neuroglia/data/infrastructure/event_sourcing/event_store/__init__.py +0 -0
  16. neuroglia/data/infrastructure/event_sourcing/event_store/event_store.py +175 -0
  17. neuroglia/data/infrastructure/event_sourcing/read_model_reconciliator.py +62 -0
  18. neuroglia/data/infrastructure/memory/__init__.py +1 -0
  19. neuroglia/data/infrastructure/memory/memory_repository.py +29 -0
  20. neuroglia/data/infrastructure/mongo/__init__.py +0 -0
  21. neuroglia/data/infrastructure/mongo/mongo_repository.py +331 -0
  22. neuroglia/data/queries/generic/__init__.py +2 -0
  23. neuroglia/data/queries/generic/get_by_id_query.py +30 -0
  24. neuroglia/data/queries/generic/list_query.py +23 -0
  25. neuroglia/data/queryable.py +175 -0
  26. neuroglia/dependency_injection/__init__.py +1 -0
  27. neuroglia/dependency_injection/service_provider.py +341 -0
  28. neuroglia/eventing/cloud_events/__init__.py +1 -0
  29. neuroglia/eventing/cloud_events/cloud_event.py +72 -0
  30. neuroglia/eventing/cloud_events/decorators.py +5 -0
  31. neuroglia/eventing/cloud_events/infrastructure/__init__.py +4 -0
  32. neuroglia/eventing/cloud_events/infrastructure/cloud_event_bus.py +11 -0
  33. neuroglia/eventing/cloud_events/infrastructure/cloud_event_ingestor.py +74 -0
  34. neuroglia/eventing/cloud_events/infrastructure/cloud_event_middleware.py +39 -0
  35. neuroglia/eventing/cloud_events/infrastructure/cloud_event_publisher.py +103 -0
  36. neuroglia/expressions/__init__.py +0 -0
  37. neuroglia/expressions/javascript_expression_translator.py +147 -0
  38. neuroglia/hosting/__init__.py +0 -0
  39. neuroglia/hosting/abstractions.py +141 -0
  40. neuroglia/hosting/configuration/data_access_layer.py +50 -0
  41. neuroglia/hosting/web.py +87 -0
  42. neuroglia/integration/__init__.py +0 -0
  43. neuroglia/integration/models.py +18 -0
  44. neuroglia/mapping/__init__.py +1 -0
  45. neuroglia/mapping/mapper.py +238 -0
  46. neuroglia/mediation/__init__.py +1 -0
  47. neuroglia/mediation/mediator.py +182 -0
  48. neuroglia/mvc/__init__.py +0 -0
  49. neuroglia/mvc/controller_base.py +72 -0
  50. neuroglia/reactive/__init__.py +1 -0
  51. neuroglia/reactive/rx_async.py +16 -0
  52. neuroglia/serialization/__init__.py +0 -0
  53. neuroglia/serialization/abstractions.py +30 -0
  54. neuroglia/serialization/json.py +212 -0
  55. neuroglia_python-0.0.23.dist-info/LICENSE +201 -0
  56. neuroglia_python-0.0.23.dist-info/METADATA +127 -0
  57. neuroglia_python-0.0.23.dist-info/RECORD +58 -0
  58. neuroglia_python-0.0.23.dist-info/WHEEL +4 -0
neuroglia/__init__.py ADDED
File without changes
@@ -0,0 +1,4 @@
1
+ from .module_loader import *
2
+ from .operation_result import *
3
+ from .type_finder import *
4
+ from .type_extensions import *
@@ -0,0 +1,13 @@
1
+ import importlib
2
+ import sys
3
+ from types import ModuleType
4
+
5
+
6
+ class ModuleLoader:
7
+
8
+ @staticmethod
9
+ def load(module_name: str) -> ModuleType:
10
+ module = sys.modules.get(module_name, None)
11
+ if module is None:
12
+ module = importlib.import_module(module_name)
13
+ return module
@@ -0,0 +1,11 @@
1
+ from typing import Generic, Optional, TypeVar
2
+ from neuroglia.core.problem_details import ProblemDetails
3
+
4
+ TData = TypeVar('TData')
5
+
6
+
7
+ class OperationResult(Generic[TData], ProblemDetails):
8
+ ''' Represents the result of an operation '''
9
+
10
+ data: Optional[TData]
11
+ ''' Gets the data, if any, returned by the operation in case of success '''
@@ -0,0 +1,25 @@
1
+ import dataclasses
2
+ from typing import Optional
3
+
4
+
5
+ @dataclasses.dataclass
6
+ class ProblemDetails:
7
+ '''Represents the structure of a Problem Details response (RFC 7807).'''
8
+
9
+ title: str
10
+ '''A short, human-readable summary of the problem.'''
11
+
12
+ status: int
13
+ '''The HTTP status code for this occurrence of the problem.'''
14
+
15
+ detail: Optional[str] = None
16
+ '''A human-readable explanation specific to this occurrence of the problem.'''
17
+
18
+ type: Optional[str] = None
19
+ '''A URI reference that identifies the problem type.'''
20
+
21
+ instance: Optional[str] = None
22
+ '''A URI reference that identifies the specific occurrence of the problem.'''
23
+
24
+ def is_success_status_code(self) -> bool: self.status >= 200 and self.status < 300
25
+ ''' Determines whether the operation is successfull or not '''
@@ -0,0 +1,75 @@
1
+ from typing import Dict, List, Type, TypeVar
2
+
3
+
4
+ class TypeExtensions:
5
+
6
+ @staticmethod
7
+ def get_generic_implementation(type_: Type, generic_type_definition: Type, generic_args: Dict[str, Type] = None):
8
+ base_types = TypeExtensions._get_base_types(type_)
9
+ base_types = [base for base in base_types if (issubclass(base.__origin__, generic_type_definition) if hasattr(base, "__origin__") else issubclass(base, generic_type_definition))]
10
+ base_type = next((base for base in base_types if base == generic_type_definition or (hasattr(base, "__origin__")) and base.__origin__ == generic_type_definition), None)
11
+ if hasattr(type_, "__orig_class__") and hasattr(type_.__orig_class__.__origin__, "__parameters__"):
12
+ if generic_args is None:
13
+ generic_args = dict[str, Type]()
14
+ for i in range(len(type_.__orig_class__.__origin__.__parameters__)):
15
+ name = type_.__orig_class__.__origin__.__parameters__[i].__name__
16
+ arg_type = type_.__orig_class__.__args__[i]
17
+ generic_args[name] = arg_type
18
+ if (hasattr(type_, "__origin__") and hasattr(type_.__origin__, "__parameters__")):
19
+ if generic_args is None:
20
+ generic_args = dict[str, Type]()
21
+ for i in range(len(type_.__origin__.__parameters__)):
22
+ name = type_.__origin__.__parameters__[i].__name__
23
+ arg_type = type_.__args__[i]
24
+ generic_args[name] = arg_type
25
+ if base_type is not None:
26
+ return TypeExtensions._substitute_generic_arguments(base_type, generic_args)
27
+ else:
28
+ return next((implementation_type for implementation_type in [TypeExtensions.get_generic_implementation(base, generic_type_definition, generic_args) for base in base_types]), None)
29
+
30
+ @staticmethod
31
+ def get_generic_arguments(type_: Type, generic_arguments: Dict[str, Type] = None) -> Dict[str, Type]:
32
+ if hasattr(type_, "__orig_class__") and hasattr(type_.__orig_class__.__origin__, "__parameters__"):
33
+ if generic_arguments is None:
34
+ generic_arguments = dict[str, Type]()
35
+ for i in range(len(type_.__orig_class__.__origin__.__parameters__)):
36
+ name = type_.__orig_class__.__origin__.__parameters__[i].__name__
37
+ arg_type = type_.__orig_class__.__args__[i]
38
+ generic_arguments[name] = TypeExtensions._substitute_generic_arguments(arg_type, generic_arguments)
39
+ if (hasattr(type_, "__origin__") and hasattr(type_.__origin__, "__parameters__")):
40
+ if generic_arguments is None:
41
+ generic_arguments = dict[str, Type]()
42
+ for i in range(len(type_.__origin__.__parameters__)):
43
+ name = type_.__origin__.__parameters__[i].__name__
44
+ arg_type = type_.__args__[i]
45
+ generic_arguments[name] = TypeExtensions._substitute_generic_arguments(arg_type, generic_arguments)
46
+ for base_type in TypeExtensions._get_base_types(type_):
47
+ generic_arguments = TypeExtensions.get_generic_arguments(base_type, generic_arguments)
48
+ return generic_arguments
49
+
50
+ @staticmethod
51
+ def get_generic_argument(type_: Type, name: str) -> Type | None:
52
+ generic_arguments = TypeExtensions.get_generic_arguments(type_)
53
+ return generic_arguments.get(name, None)
54
+
55
+ @staticmethod
56
+ def _get_base_types(type_: Type) -> List[Type]:
57
+ if hasattr(type_, "__origin__") and hasattr(type_.__origin__, "__orig_bases__"):
58
+ return type_.__origin__.__orig_bases__
59
+ elif hasattr(type_, "__orig_bases__"):
60
+ return type_.__orig_bases__
61
+ else:
62
+ return list[Type]()
63
+
64
+ @staticmethod
65
+ def _substitute_generic_arguments(type_: Type, generic_args: Dict[str, Type] | None = None) -> Type:
66
+ if not hasattr(type_, "__parameters__") and generic_args is not None:
67
+ if isinstance(type_, TypeVar):
68
+ return generic_args.get(type_.__name__, None)
69
+ else:
70
+ return type_
71
+ parameters = [param.__name__ for param in type_.__parameters__]
72
+ type_args = [TypeExtensions._substitute_generic_arguments(generic_arg_value, generic_args) for generic_arg_key, generic_arg_value in generic_args.items() if generic_arg_key in parameters]
73
+ if len(type_args) < 1:
74
+ return type_
75
+ return type_[*type_args]
@@ -0,0 +1,24 @@
1
+ import importlib
2
+ import inspect
3
+ import pkgutil
4
+ from types import ModuleType
5
+ from typing import Callable, List, Type
6
+
7
+
8
+ class TypeFinder:
9
+ ''' Represents an utility class that exposes methods to find and filter types '''
10
+
11
+ def get_types(module: ModuleType, predicate: Callable[[Type], bool] = lambda t: True, include_sub_modules=True, include_sub_packages=False) -> List[Type]:
12
+ ''' Recursively finds all types contained in the specified module that match the specified predicate, if any '''
13
+ results: List[Type] = list[Type]()
14
+ results.extend([cls for _, cls in inspect.getmembers(module, inspect.isclass) if predicate(cls)])
15
+ if include_sub_modules:
16
+ for submodule in [submodule for _, submodule in inspect.getmembers(module, inspect.ismodule) if submodule.__name__.startswith(module.__name__)]:
17
+ results.extend(TypeFinder.get_types(submodule, predicate, include_sub_modules, include_sub_packages))
18
+ # todo: the following creates big problems, as it will actually (re)load, thus (re)run, imported packages <= it sucks
19
+ if include_sub_packages:
20
+ if hasattr(module, "__path__"):
21
+ sub_packages = [importlib.import_module(module_name) for _, module_name, _ in pkgutil.walk_packages(module.__path__, module.__name__ + ".") if module_name.startswith(module.__name__)]
22
+ for submodule in sub_packages:
23
+ results.extend(TypeFinder.get_types(submodule, predicate, include_sub_modules, include_sub_packages))
24
+ return list(set(results))
@@ -0,0 +1 @@
1
+ from .queryable import *
@@ -0,0 +1,124 @@
1
+ from abc import ABC
2
+ from datetime import datetime
3
+ from typing import Generic, List, Type, TypeVar
4
+
5
+ TKey = TypeVar("TKey")
6
+ ''' Represents the generic argument used to specify the type of key to use '''
7
+
8
+
9
+ class Identifiable(Generic[TKey], ABC):
10
+ ''' Defines the fundamentals of an object that can be identified based on a unique identifier '''
11
+
12
+ id: TKey
13
+ ''' Gets the object's unique identifier '''
14
+
15
+
16
+ TEntity = TypeVar("TEntity", bound=Identifiable)
17
+ ''' Represents the generic argument used to specify the type of entity to use '''
18
+
19
+
20
+ class Entity(Generic[TKey], Identifiable[TKey], ABC):
21
+ ''' Represents the abstract class inherited by all entities in the application '''
22
+
23
+ def __init__(self) -> None:
24
+ super().__init__()
25
+ self.created_at = datetime.now()
26
+
27
+ created_at: datetime
28
+ ''' Gets the date and time the entity has been created at '''
29
+
30
+ last_modified: datetime
31
+ ''' Gets the date and time the entity was last modified at, if any '''
32
+
33
+
34
+ class VersionedState(ABC):
35
+ ''' Represents the abstract class inherited by all versioned states '''
36
+
37
+ def __init__(self):
38
+ self.state_version = 0
39
+
40
+ state_version: int = 0
41
+ ''' Gets the state's version '''
42
+
43
+
44
+ class AggregateState(Generic[TKey], Identifiable[TKey], VersionedState, ABC):
45
+ ''' Represents the abstract class inherited by all aggregate root states '''
46
+
47
+ def __init__(self):
48
+ super().__init__()
49
+
50
+ id: TKey
51
+ ''' Gets the id of the aggregate the state belongs to '''
52
+
53
+ created_at: datetime
54
+ ''' Gets the date and time the aggregate has been created at '''
55
+
56
+ last_modified: datetime
57
+ ''' Gets the date and time, if any, the aggregate was last modified at '''
58
+
59
+
60
+ TState = TypeVar("TState", bound=AggregateState)
61
+ ''' Represents the generic argument used to specify the state of an aggregate root '''
62
+
63
+
64
+ class DomainEvent(Generic[TKey], ABC):
65
+ ''' Represents the base class inherited by all domain events '''
66
+
67
+ def __init__(self, aggregate_id: TKey):
68
+ ''' Initializes a new domain event '''
69
+ self.created_at = datetime.now()
70
+ self.aggregate_id = aggregate_id
71
+
72
+ created_at: datetime
73
+ ''' Gets the date and time the domain event has been created at '''
74
+
75
+ aggregate_id: TKey
76
+ ''' Gets the id of the aggregate that has produced the domain event '''
77
+
78
+ aggregate_version: int
79
+ ''' Gets the version of the aggregate's state, after reducing the domain event '''
80
+
81
+
82
+ TEvent = TypeVar("TEvent", bound=DomainEvent)
83
+ ''' Represents the generic argument used to specify the state of an aggregate root '''
84
+
85
+
86
+ class AggregateRoot(Generic[TState, TKey], Entity[TKey], ABC):
87
+ ''' Represents the base class for all aggregate roots '''
88
+
89
+ _pending_events: List[DomainEvent]
90
+ ''' Gets a list containing all domain events pending persistence '''
91
+
92
+ def __init__(self):
93
+ ''' Initializes a new aggregate root '''
94
+ self.state = object.__new__(self.__orig_bases__[0].__args__[0])
95
+ self.state.__init__()
96
+ self._pending_events = list[DomainEvent]()
97
+
98
+ def id(self):
99
+ ''' Gets the aggregate root's id '''
100
+ return self.state.id
101
+
102
+ state: TState
103
+ ''' Gets the aggregate root's state '''
104
+
105
+ def register_event(self, e: TEvent) -> TEvent:
106
+ ''' Registers the specified domain event '''
107
+ if not hasattr(self, "_pending_events"):
108
+ self._pending_events = list[DomainEvent]()
109
+ self._pending_events.append(e)
110
+ e.aggregate_version = self.state.state_version + len(self._pending_events)
111
+ return e
112
+
113
+ def clear_pending_events(self):
114
+ ''' Clears all pending domain events '''
115
+ self._pending_events.clear()
116
+
117
+
118
+ TAggregate = TypeVar("TAggregate", bound=AggregateRoot)
119
+ ''' Represents the generic argument used to specify an aggregate root type '''
120
+
121
+
122
+ def queryable(cls):
123
+ cls.__queryable__ = True
124
+ return cls
File without changes
@@ -0,0 +1,78 @@
1
+ from abc import ABC, abstractmethod
2
+ from typing import Generic, List, Optional
3
+ from neuroglia.data.abstractions import TEntity, TKey
4
+ from neuroglia.data.queryable import Queryable
5
+
6
+
7
+ class Repository(Generic[TEntity, TKey], ABC):
8
+ ''' Defines the fundamentals of a repository '''
9
+
10
+ @abstractmethod
11
+ async def contains_async(self, id: TKey) -> bool:
12
+ ''' Determines whether or not the repository contains an entity with the specified id '''
13
+ raise NotImplementedError()
14
+
15
+ @abstractmethod
16
+ async def get_async(self, id: TKey) -> Optional[TEntity]:
17
+ ''' Gets the entity with the specified id, if any '''
18
+ raise NotImplementedError()
19
+
20
+ @abstractmethod
21
+ async def add_async(self, entity: TEntity) -> TEntity:
22
+ ''' Adds the specified entity '''
23
+ raise NotImplementedError()
24
+
25
+ @abstractmethod
26
+ async def update_async(self, entity: TEntity) -> TEntity:
27
+ ''' Persists the changes that were made to the specified entity '''
28
+ raise NotImplementedError()
29
+
30
+ @abstractmethod
31
+ async def remove_async(self, id: TKey) -> None:
32
+ ''' Removes the entity with the specified key '''
33
+ raise NotImplementedError()
34
+
35
+
36
+ class QueryableRepository(Generic[TEntity, TKey], Repository[TEntity, TKey], ABC):
37
+ ''' Defines the fundamentals of a queryable repository '''
38
+
39
+ @abstractmethod
40
+ async def query_async(self) -> Queryable[TEntity]:
41
+ raise NotImplementedError()
42
+
43
+
44
+ class FlexibleRepository(Generic[TEntity, TKey], Repository[TEntity, TKey], ABC):
45
+ ''' Defines the fundamentals of a flexible repository '''
46
+
47
+ @abstractmethod
48
+ async def set_database(self, database: str) -> None:
49
+ raise NotImplementedError()
50
+
51
+ @abstractmethod
52
+ async def get_database(self) -> str:
53
+ raise NotImplementedError()
54
+
55
+ @abstractmethod
56
+ async def contains_by_collection_name_async(self, collection_nam: str, id: TKey) -> bool:
57
+ ''' Determines whether or not the repository contains an entity with the specified id '''
58
+ raise NotImplementedError()
59
+
60
+ @abstractmethod
61
+ async def get_by_collection_name_async(self, collection_name: str, id: TKey) -> Optional[TEntity]:
62
+ ''' Gets the entity with the specified id, if any '''
63
+ raise NotImplementedError()
64
+
65
+ @abstractmethod
66
+ async def add_by_collection_name_async(self, collection_name: str, entity: TEntity) -> TEntity:
67
+ ''' Adds the specified entity '''
68
+ raise NotImplementedError()
69
+
70
+ @abstractmethod
71
+ async def update_by_collection_name_async(self, collection_name: str, entity: TEntity) -> TEntity:
72
+ ''' Persists the changes that were made to the specified entity '''
73
+ raise NotImplementedError()
74
+
75
+ @abstractmethod
76
+ async def remove_by_collection_name_async(self, collection_name: str, id: TKey) -> None:
77
+ ''' Removes the entity with the specified key '''
78
+ raise NotImplementedError()
@@ -0,0 +1,126 @@
1
+ from abc import ABC, abstractmethod
2
+ from dataclasses import dataclass
3
+ from datetime import datetime
4
+ from enum import Enum
5
+ from typing import Any, List, Optional, Type
6
+ from neuroglia.data.abstractions import AggregateRoot
7
+
8
+
9
+ @dataclass
10
+ class StreamDescriptor:
11
+ ''' Represents a class used to describe a stream of recorded events '''
12
+
13
+ id: str
14
+ ''' Gets the stream's id '''
15
+
16
+ length: int
17
+ ''' Gets the stream's length '''
18
+
19
+ first_event_at: Optional[datetime] = None
20
+ ''' Gets the date and time at which the first event, if any, has been recorded to the stream '''
21
+
22
+ last_event_at: Optional[datetime] = None
23
+ ''' Gets the date and time at which the last event, if any, has been recorded to the stream '''
24
+
25
+
26
+ @dataclass
27
+ class EventDescriptor:
28
+ ''' Represents a class used to describe an event to record '''
29
+
30
+ type: str
31
+ ''' Gets the type of the event to record '''
32
+
33
+ data: Optional[Any] = None
34
+ ''' Gets the data of the event to record '''
35
+
36
+ metadata: Optional[Any] = None
37
+ ''' Gets the metadata of the event to record, if any '''
38
+
39
+
40
+ @dataclass
41
+ class EventRecord:
42
+ ''' Represents a recorded event '''
43
+
44
+ stream_id: str
45
+ ''' Gets the id of the stream the recorded event belongs to '''
46
+
47
+ id: str
48
+ ''' Gets the id of the recorded event '''
49
+
50
+ offset: int
51
+ ''' Gets the offset of the recorded event in the stream it belongs to '''
52
+
53
+ position: int
54
+ ''' Gets the position of the recorded event in the global stream '''
55
+
56
+ timestamp: datetime
57
+ ''' Gets the date and time at which the event has been recorded '''
58
+
59
+ type: str
60
+ ''' Gets the type of the recorded event. Should be a non-versioned reverse uri made out alphanumeric, '-' and '.' characters '''
61
+
62
+ data: Optional[any] = None
63
+ ''' Gets the recorded event's data, if any '''
64
+
65
+ metadata: Optional[any] = None
66
+ ''' Gets the recorded event's metadadata, if any '''
67
+
68
+ replayed: bool = False
69
+ ''' Gets a boolean indicating whether or not the recorded event is being replayed to its consumer/consumer group '''
70
+
71
+
72
+ class StreamReadDirection(Enum):
73
+ ''' Enumerates all directions in which a event sourcing stream can be read '''
74
+ FORWARDS = 0,
75
+ ''' Indicates a forwards direction '''
76
+ BACKWARDS = 1
77
+ ''' Indicates a backwards direction '''
78
+
79
+
80
+ @dataclass
81
+ class EventStoreOptions:
82
+
83
+ database_name: str
84
+ ''' Gets/sets the name of the database to use, if any '''
85
+
86
+
87
+ class EventStore(ABC):
88
+ ''' Defines the fundamentals of a service used to append and subscribe to sourced events '''
89
+
90
+ @abstractmethod
91
+ async def contains_async(self, stream_id: str) -> bool:
92
+ ''' Determines whether or not the event store contains a stream with the specified id '''
93
+ raise NotImplementedError()
94
+
95
+ @abstractmethod
96
+ async def append_async(self, streamId: str, events: List[EventDescriptor], expectedVersion: Optional[int] = None):
97
+ ''' Appends a list of events to the specified stream '''
98
+ raise NotImplementedError()
99
+
100
+ @abstractmethod
101
+ async def get_async(self, stream_id: str):
102
+ ''' Gets information about the specified stream '''
103
+ raise NotImplementedError()
104
+
105
+ @abstractmethod
106
+ async def read_async(self, stream_id: str, read_direction: StreamReadDirection, offset: int, length: Optional[int] = None) -> List[EventRecord]:
107
+ ''' Reads recorded events from the specified stream '''
108
+ raise NotImplementedError()
109
+
110
+ async def observe_async(self, stream_id: Optional[str], consumer_group: Optional[str] = None, offset: Optional[int] = None):
111
+ '''
112
+ Creates a new observable used to stream events published by the event store.
113
+ Typically, this is used by some kind of reconciliation mechanism to consume domain events then publish them to their related handlers, if any.
114
+ '''
115
+ raise NotImplementedError()
116
+
117
+
118
+ class Aggregator:
119
+
120
+ def aggregate(self, events: List, aggregate_type: Type):
121
+ aggregate: AggregateRoot = object.__new__(aggregate_type)
122
+ aggregate.state = aggregate.__orig_bases__[0].__args__[0]()
123
+ for e in events:
124
+ aggregate.state.on(e.data)
125
+ aggregate.state.state_version = e.data.aggregate_version
126
+ return aggregate
@@ -0,0 +1,77 @@
1
+ from dataclasses import dataclass
2
+ from typing import Generic, Optional, Type
3
+ from neuroglia.data.infrastructure.abstractions import Repository
4
+ from neuroglia.data.abstractions import DomainEvent, TAggregate, TKey
5
+ from neuroglia.data.infrastructure.event_sourcing.abstractions import Aggregator, EventDescriptor, EventStore, StreamReadDirection
6
+ from neuroglia.hosting.abstractions import ApplicationBuilderBase
7
+
8
+
9
+ @dataclass
10
+ class EventSourcingRepositoryOptions(Generic[TAggregate, TKey]):
11
+ ''' Represents the options used to configure an event sourcing repository '''
12
+ pass
13
+
14
+
15
+ class EventSourcingRepository(Generic[TAggregate, TKey], Repository[TAggregate, TKey]):
16
+ ''' Represents an event sourcing repository implementation '''
17
+
18
+ def __init__(self, eventstore: EventStore, aggregator: Aggregator):
19
+ ''' Initialize a new event sourcing repository '''
20
+ self._eventstore = eventstore
21
+ self._aggregator = aggregator
22
+
23
+ _eventstore : EventStore
24
+ ''' Gets the underlying event store '''
25
+
26
+ _aggregator : Aggregator
27
+ ''' Gets the underlying event store '''
28
+
29
+ async def contains_async(self, id: TKey) -> bool: return self._eventstore.contains_stream(self._build_stream_id_for(id))
30
+
31
+ async def get_async(self, id: TKey) -> Optional[TAggregate]:
32
+ ''' Gets the aggregate with the specified id, if any '''
33
+ stream_id = self._build_stream_id_for(id)
34
+ events = await self._eventstore.read_async(stream_id, StreamReadDirection.FORWARDS, 0)
35
+ return self._aggregator.aggregate(events, self.__orig_class__.__args__[0])
36
+
37
+ async def add_async(self, aggregate: TAggregate) -> TAggregate:
38
+ ''' Adds and persists the specified aggregate '''
39
+ stream_id = self._build_stream_id_for(aggregate.id())
40
+ events = aggregate._pending_events
41
+ if len(events) < 1 : raise Exception()
42
+ encoded_events = [self._encode_event(e) for e in events]
43
+ await self._eventstore.append_async(stream_id, encoded_events)
44
+ aggregate.state.state_version = events[-1].aggregate_version
45
+ aggregate.clear_pending_events()
46
+ return aggregate
47
+
48
+ async def update_async(self, aggregate: TAggregate) -> TAggregate:
49
+ ''' Perists the changes made to the specified aggregate '''
50
+ stream_id = self._build_stream_id_for(aggregate.id())
51
+ events = aggregate._pending_events
52
+ if len(events) < 1 : raise Exception()
53
+ encoded_events = [self._encode_event(e) for e in events]
54
+ await self._eventstore.append_async(stream_id, encoded_events, aggregate.state.state_version)
55
+ aggregate.state.state_version = events[-1].aggregate_version
56
+ aggregate.clear_pending_events()
57
+ return aggregate
58
+
59
+ async def remove_async(self, id: TKey) -> None:
60
+ ''' Removes the aggregate root with the specified key, if any '''
61
+ raise NotImplementedError()
62
+
63
+ def _build_stream_id_for(self, aggregate_id : TKey):
64
+ ''' Builds a new stream id for the specified aggregate '''
65
+ aggregate_name = self.__orig_class__.__args__[0].__name__
66
+ return f"{aggregate_name.lower()}-{aggregate_id}"
67
+
68
+ def _encode_event(self, e: DomainEvent):
69
+ ''' Encodes a domain event into a new event descriptor '''
70
+ event_type = type(e).__name__.lower()
71
+ return EventDescriptor(event_type, e)
72
+
73
+ def configure(builder: ApplicationBuilderBase, entity_type : Type, key_type : Type) -> ApplicationBuilderBase:
74
+ ''' Configures the specified application to use an event sourcing based repository implementation to manage the specified type of entity '''
75
+ builder.services.try_add_singleton(EventSourcingRepositoryOptions[entity_type, key_type], singleton= EventSourcingRepositoryOptions[entity_type, key_type]())
76
+ builder.services.try_add_singleton(Repository[entity_type, key_type], EventSourcingRepository[entity_type, key_type])
77
+ return builder