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.
- neuroglia/__init__.py +0 -0
- neuroglia/core/__init__.py +4 -0
- neuroglia/core/module_loader.py +13 -0
- neuroglia/core/operation_result.py +11 -0
- neuroglia/core/problem_details.py +25 -0
- neuroglia/core/type_extensions.py +75 -0
- neuroglia/core/type_finder.py +24 -0
- neuroglia/data/__init__.py +1 -0
- neuroglia/data/abstractions.py +124 -0
- neuroglia/data/infrastructure/__init__.py +0 -0
- neuroglia/data/infrastructure/abstractions.py +78 -0
- neuroglia/data/infrastructure/event_sourcing/__init__.py +0 -0
- neuroglia/data/infrastructure/event_sourcing/abstractions.py +126 -0
- neuroglia/data/infrastructure/event_sourcing/event_sourcing_repository.py +77 -0
- neuroglia/data/infrastructure/event_sourcing/event_store/__init__.py +0 -0
- neuroglia/data/infrastructure/event_sourcing/event_store/event_store.py +175 -0
- neuroglia/data/infrastructure/event_sourcing/read_model_reconciliator.py +62 -0
- neuroglia/data/infrastructure/memory/__init__.py +1 -0
- neuroglia/data/infrastructure/memory/memory_repository.py +29 -0
- neuroglia/data/infrastructure/mongo/__init__.py +0 -0
- neuroglia/data/infrastructure/mongo/mongo_repository.py +331 -0
- neuroglia/data/queries/generic/__init__.py +2 -0
- neuroglia/data/queries/generic/get_by_id_query.py +30 -0
- neuroglia/data/queries/generic/list_query.py +23 -0
- neuroglia/data/queryable.py +175 -0
- neuroglia/dependency_injection/__init__.py +1 -0
- neuroglia/dependency_injection/service_provider.py +341 -0
- neuroglia/eventing/cloud_events/__init__.py +1 -0
- neuroglia/eventing/cloud_events/cloud_event.py +72 -0
- neuroglia/eventing/cloud_events/decorators.py +5 -0
- neuroglia/eventing/cloud_events/infrastructure/__init__.py +4 -0
- neuroglia/eventing/cloud_events/infrastructure/cloud_event_bus.py +11 -0
- neuroglia/eventing/cloud_events/infrastructure/cloud_event_ingestor.py +74 -0
- neuroglia/eventing/cloud_events/infrastructure/cloud_event_middleware.py +39 -0
- neuroglia/eventing/cloud_events/infrastructure/cloud_event_publisher.py +103 -0
- neuroglia/expressions/__init__.py +0 -0
- neuroglia/expressions/javascript_expression_translator.py +147 -0
- neuroglia/hosting/__init__.py +0 -0
- neuroglia/hosting/abstractions.py +141 -0
- neuroglia/hosting/configuration/data_access_layer.py +50 -0
- neuroglia/hosting/web.py +87 -0
- neuroglia/integration/__init__.py +0 -0
- neuroglia/integration/models.py +18 -0
- neuroglia/mapping/__init__.py +1 -0
- neuroglia/mapping/mapper.py +238 -0
- neuroglia/mediation/__init__.py +1 -0
- neuroglia/mediation/mediator.py +182 -0
- neuroglia/mvc/__init__.py +0 -0
- neuroglia/mvc/controller_base.py +72 -0
- neuroglia/reactive/__init__.py +1 -0
- neuroglia/reactive/rx_async.py +16 -0
- neuroglia/serialization/__init__.py +0 -0
- neuroglia/serialization/abstractions.py +30 -0
- neuroglia/serialization/json.py +212 -0
- neuroglia_python-0.0.23.dist-info/LICENSE +201 -0
- neuroglia_python-0.0.23.dist-info/METADATA +127 -0
- neuroglia_python-0.0.23.dist-info/RECORD +58 -0
- neuroglia_python-0.0.23.dist-info/WHEEL +4 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .mapper import Mapper as Mapper
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import importlib
|
|
2
|
+
import inspect
|
|
3
|
+
from typing import Any, Callable, List, Type
|
|
4
|
+
from neuroglia.core import ModuleLoader, TypeFinder
|
|
5
|
+
from neuroglia.hosting.abstractions import ApplicationBuilderBase
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def label(name: str, value: str):
|
|
9
|
+
''' Represents a decorator used to add a custom label (key, value) to a class'''
|
|
10
|
+
def decorator(cls):
|
|
11
|
+
if "__labels__" not in dir(cls):
|
|
12
|
+
cls.__labels__ = dict()
|
|
13
|
+
cls.__labels__[name] = value
|
|
14
|
+
return cls
|
|
15
|
+
return decorator
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def labels(**kwargs):
|
|
19
|
+
''' Represents a decorator used to add one or more custom labels to a class'''
|
|
20
|
+
def decorator(cls):
|
|
21
|
+
if "__labels__" not in dir(cls):
|
|
22
|
+
cls.__labels__ = dict()
|
|
23
|
+
for k, v in kwargs.items():
|
|
24
|
+
cls.__labels__[k] = v
|
|
25
|
+
return cls
|
|
26
|
+
return decorator
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def map_to(target_type: Type):
|
|
30
|
+
''' Represents a decorator used to create a mapping of the marked class to a specified type '''
|
|
31
|
+
def decorator(cls):
|
|
32
|
+
cls.__map_to__ = target_type
|
|
33
|
+
return cls
|
|
34
|
+
return decorator
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def map_from(source_type: Type):
|
|
38
|
+
''' Represents a decorator used to create a mapping from a specified type to the marked class '''
|
|
39
|
+
def decorator(cls):
|
|
40
|
+
cls.__map_from__ = source_type
|
|
41
|
+
return cls
|
|
42
|
+
return decorator
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class TypeMappingContext:
|
|
46
|
+
''' Represents the context of a type mapping '''
|
|
47
|
+
|
|
48
|
+
def __init__(self, source: Any, source_type: Type, destination_type: Type):
|
|
49
|
+
self.source = source
|
|
50
|
+
self.source_type = source_type
|
|
51
|
+
self.destination_type = destination_type
|
|
52
|
+
|
|
53
|
+
source: Any
|
|
54
|
+
''' Gets the value to map '''
|
|
55
|
+
|
|
56
|
+
source_type: Type
|
|
57
|
+
''' Gets the type of the value to map '''
|
|
58
|
+
|
|
59
|
+
destination_type: Type
|
|
60
|
+
''' Gets the type to map the source to '''
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class MemberMappingContext(TypeMappingContext):
|
|
64
|
+
''' Represents the context of a type mapping '''
|
|
65
|
+
|
|
66
|
+
def __init__(self, source: Any, source_type: Type, destination_type: Type, member_name: str, source_member_value: Any):
|
|
67
|
+
super().__init__(source, source_type, destination_type)
|
|
68
|
+
self.member_name = member_name
|
|
69
|
+
self.source_member_value = source_member_value
|
|
70
|
+
|
|
71
|
+
member_name: str
|
|
72
|
+
|
|
73
|
+
source_member_value: Any
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class MemberMapConfiguration:
|
|
77
|
+
''' Represents an object used to configure the mapping of a type member '''
|
|
78
|
+
|
|
79
|
+
def __init__(self, name: str, is_ignored: bool = False, value_converter: Callable[[MemberMappingContext], Any] = None):
|
|
80
|
+
self.name = name
|
|
81
|
+
self.is_ignored = is_ignored
|
|
82
|
+
self.value_converter = value_converter
|
|
83
|
+
|
|
84
|
+
name: str
|
|
85
|
+
''' Gets the name of the configured member '''
|
|
86
|
+
|
|
87
|
+
is_ignored: bool
|
|
88
|
+
''' Gets a boolean indicating whether or not the member is ignored '''
|
|
89
|
+
|
|
90
|
+
value_converter: Callable[[MemberMappingContext], Any]
|
|
91
|
+
''' Gets a callable, if any, used to convert the value of the member '''
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class TypeMapConfiguration:
|
|
95
|
+
''' Represents an object used to configure the mapping of a type to another '''
|
|
96
|
+
|
|
97
|
+
def __init__(self, source_type: Type, destination_type: Type, type_converter: Callable[[Any], Any] = None):
|
|
98
|
+
self.source_type = source_type
|
|
99
|
+
self.destination_type = destination_type
|
|
100
|
+
self.type_converter = type_converter
|
|
101
|
+
|
|
102
|
+
source_type: Type
|
|
103
|
+
''' Gets the type to convert to the specified type '''
|
|
104
|
+
|
|
105
|
+
destination_type: Type
|
|
106
|
+
''' Gets the type to convert source values to '''
|
|
107
|
+
|
|
108
|
+
type_converter: Callable[[TypeMappingContext], Any]
|
|
109
|
+
''' Gets the callable, if any, used to convert source instances to the configured destination type '''
|
|
110
|
+
|
|
111
|
+
member_configurations: List[MemberMapConfiguration] = list[MemberMapConfiguration]()
|
|
112
|
+
''' Gets a list containing objects used to configure how to map members of the configured source and destination types '''
|
|
113
|
+
|
|
114
|
+
def map(self, source: Any):
|
|
115
|
+
''' Maps the specified value to the configured destination type '''
|
|
116
|
+
mapping_context = TypeMappingContext(source, self.source_type, self.destination_type)
|
|
117
|
+
if self.type_converter is not None:
|
|
118
|
+
return self.type_converter(mapping_context)
|
|
119
|
+
source_attributes = dict([(key, value) for key, value in source.__dict__.items() if not key.startswith('_')]) if hasattr(source, "__dict__") else dict()
|
|
120
|
+
destination_attributes = dict()
|
|
121
|
+
declared_attributes = [key for key, _ in self.destination_type.__annotations__.items() if not key.startswith('_')] if hasattr(self.destination_type, "__annotations__") else list[str]()
|
|
122
|
+
for source_attribute_key, source_attribute_value in source_attributes.items():
|
|
123
|
+
if not source_attribute_key in declared_attributes:
|
|
124
|
+
continue
|
|
125
|
+
member_map = next((member for member in self.member_configurations if member.name == source_attribute_key), None)
|
|
126
|
+
if member_map is None:
|
|
127
|
+
destination_attributes[source_attribute_key] = source_attribute_value
|
|
128
|
+
elif member_map.is_ignored:
|
|
129
|
+
continue
|
|
130
|
+
else:
|
|
131
|
+
destination_attributes[source_attribute_key] = member_map.value_converter(MemberMappingContext(source, self.source_type, self.destination_type, source_attribute_key, source_attribute_value))
|
|
132
|
+
for configured_attribute in [attr for attr in self.member_configurations if attr not in source_attributes.keys()]:
|
|
133
|
+
if configured_attribute.is_ignored or configured_attribute.value_converter is None:
|
|
134
|
+
continue
|
|
135
|
+
destination_attributes[configured_attribute.name] = configured_attribute.value_converter(MemberMappingContext(source, self.source_type, self.destination_type, configured_attribute.name, None))
|
|
136
|
+
destination = object.__new__(self.destination_type)
|
|
137
|
+
destination.__dict__ = destination_attributes
|
|
138
|
+
return destination
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
class TypeMapExpression:
|
|
142
|
+
''' Represents a type mapping expression, used to fluently build and configure a new TypeMapConfiguration '''
|
|
143
|
+
|
|
144
|
+
def __init__(self, configuration: TypeMapConfiguration):
|
|
145
|
+
self._configuration = configuration
|
|
146
|
+
|
|
147
|
+
_configuration: TypeMapConfiguration
|
|
148
|
+
|
|
149
|
+
def convert_using(self, converter: Callable[[TypeMappingContext], Any]) -> None:
|
|
150
|
+
''' Maps values using the specified converter function '''
|
|
151
|
+
self._configuration.type_converter = converter
|
|
152
|
+
|
|
153
|
+
def ignore_member(self, name: str) -> 'TypeMapExpression':
|
|
154
|
+
''' Configures the map to ignore the specified member of the source type '''
|
|
155
|
+
configuration = next((member for member in self._configuration.member_configurations if member.name == name), None)
|
|
156
|
+
if configuration is None:
|
|
157
|
+
self._configuration.member_configurations.append(MemberMapConfiguration(name, True))
|
|
158
|
+
else:
|
|
159
|
+
configuration.is_ignored = True
|
|
160
|
+
return self
|
|
161
|
+
|
|
162
|
+
def for_member(self, name: str, converter: Callable[[MemberMappingContext], Any]):
|
|
163
|
+
''' Configures the mapping of the specified member to use a converter function '''
|
|
164
|
+
configuration = next((member for member in self._configuration.member_configurations if member.name == name), None)
|
|
165
|
+
if configuration is None:
|
|
166
|
+
self._configuration.member_configurations.append(MemberMapConfiguration(name, value_converter=converter))
|
|
167
|
+
else:
|
|
168
|
+
configuration.value_converter = converter
|
|
169
|
+
return self
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
class MapperConfiguration:
|
|
173
|
+
|
|
174
|
+
type_maps: List[TypeMapConfiguration] = list[TypeMapConfiguration]()
|
|
175
|
+
|
|
176
|
+
def create_map(self, source_type: Type, destination_type: Type) -> TypeMapExpression:
|
|
177
|
+
''' Creates a new expression used to convert how to map instances of the source type to instances of the destination type '''
|
|
178
|
+
configuration: TypeMapConfiguration = next((tmc for tmc in self.type_maps if tmc.source_type == source_type and tmc.destination_type == destination_type), None)
|
|
179
|
+
if configuration is None:
|
|
180
|
+
configuration = TypeMapConfiguration(source_type, destination_type)
|
|
181
|
+
self.type_maps.append(configuration)
|
|
182
|
+
return TypeMapExpression(configuration)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
class MappingProfile:
|
|
186
|
+
''' Represents a class used to configure a mapper'''
|
|
187
|
+
|
|
188
|
+
_configuration_actions: List[Callable[[MapperConfiguration], None]] = list[Callable[[MapperConfiguration], None]]()
|
|
189
|
+
''' Gets the mapper configuration to configure '''
|
|
190
|
+
|
|
191
|
+
def create_map(self, source_type: Type, destination_type: Type) -> TypeMapExpression:
|
|
192
|
+
''' Creates a new expression used to convert how to map instances of the source type to instances of the destination type '''
|
|
193
|
+
callable: Callable[[MapperConfiguration], None] = lambda config: config.create_map(source_type, destination_type)
|
|
194
|
+
self._configuration_actions.append(callable)
|
|
195
|
+
|
|
196
|
+
def apply_to(self, configuration: MapperConfiguration) -> MapperConfiguration:
|
|
197
|
+
''' Applies the mapping profile to the specified mapper configuration '''
|
|
198
|
+
for configuration_action in self._configuration_actions:
|
|
199
|
+
configuration_action(configuration)
|
|
200
|
+
return configuration
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
class Mapper:
|
|
204
|
+
''' Represents the service used to map objects '''
|
|
205
|
+
|
|
206
|
+
def __init__(self, options: MapperConfiguration):
|
|
207
|
+
self.options = options
|
|
208
|
+
|
|
209
|
+
options: MapperConfiguration
|
|
210
|
+
''' Gets the options used to configure the mapper '''
|
|
211
|
+
|
|
212
|
+
def map(self, source: Any, destination_type: Type) -> Any:
|
|
213
|
+
''' Maps the specified value into a new instance of the destination type '''
|
|
214
|
+
source_type = type(source)
|
|
215
|
+
type_map = next((tmc for tmc in self.options.type_maps if tmc.source_type == source_type and tmc.destination_type == destination_type), None)
|
|
216
|
+
if type_map is None:
|
|
217
|
+
raise Exception(f"Missing type map configuration or unsupported mapping. Mapping types: {source_type.__name__} -> {destination_type.__name__}")
|
|
218
|
+
destination = type_map.map(source)
|
|
219
|
+
return destination
|
|
220
|
+
|
|
221
|
+
@staticmethod
|
|
222
|
+
def configure(builder: ApplicationBuilderBase, modules: List[str] = list[str]()) -> ApplicationBuilderBase:
|
|
223
|
+
''' Registers and configures mapping-related services to the specified service collection.
|
|
224
|
+
|
|
225
|
+
Args:
|
|
226
|
+
services (ServiceCollection): the service collection to configure
|
|
227
|
+
modules (List[str]): a list containing the names of the modules to scan for MappingProfiles, which are classes used to configure the Mapper
|
|
228
|
+
'''
|
|
229
|
+
configuration: MapperConfiguration = MapperConfiguration()
|
|
230
|
+
for module in [ModuleLoader.load(module_name) for module_name in modules]:
|
|
231
|
+
mapping_profile_types = TypeFinder.get_types(module, lambda cls: inspect.isclass(cls) and issubclass(cls, MappingProfile) and cls != MappingProfile, include_sub_packages=True)
|
|
232
|
+
for mapping_profile_type in mapping_profile_types:
|
|
233
|
+
mapping_profile: MappingProfile = object.__new__(mapping_profile_type)
|
|
234
|
+
mapping_profile.__init__()
|
|
235
|
+
mapping_profile.apply_to(configuration)
|
|
236
|
+
builder.services.add_singleton(MapperConfiguration, singleton=configuration)
|
|
237
|
+
builder.services.add_singleton(Mapper)
|
|
238
|
+
return builder
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .mediator import *
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import inspect
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
from abc import ABC, abstractmethod
|
|
6
|
+
from types import UnionType
|
|
7
|
+
from typing import Any, Generic, List, Optional, TypeVar
|
|
8
|
+
from neuroglia.core import ModuleLoader, OperationResult, TypeFinder, TypeExtensions
|
|
9
|
+
from neuroglia.data.abstractions import DomainEvent
|
|
10
|
+
from neuroglia.dependency_injection.service_provider import ServiceProviderBase
|
|
11
|
+
from neuroglia.hosting.abstractions import ApplicationBuilderBase
|
|
12
|
+
from neuroglia.integration.models import IntegrationEvent
|
|
13
|
+
|
|
14
|
+
log = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
TResult = TypeVar('TResult', bound=OperationResult)
|
|
18
|
+
''' Represents the expected type of result returned by the operation, in case of success '''
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Request(Generic[TResult], ABC):
|
|
22
|
+
''' Represents a CQRS request '''
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class Command(Generic[TResult], Request[TResult], ABC):
|
|
27
|
+
''' Represents a CQRS command '''
|
|
28
|
+
pass
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class Query(Generic[TResult], Request[TResult], ABC):
|
|
32
|
+
''' Represents a CQRS query '''
|
|
33
|
+
pass
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
TRequest = TypeVar('TRequest', bound=Request)
|
|
37
|
+
''' Represents the type of CQRS request to handle '''
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class RequestHandler(Generic[TRequest, TResult], ABC):
|
|
41
|
+
''' Represents a service used to handle a specific type of CQRS request '''
|
|
42
|
+
|
|
43
|
+
@abstractmethod
|
|
44
|
+
async def handle_async(self, request: TRequest) -> TResult:
|
|
45
|
+
''' Handles the specified request '''
|
|
46
|
+
raise NotImplementedError()
|
|
47
|
+
|
|
48
|
+
def ok(self, data: Optional[Any] = None) -> TResult:
|
|
49
|
+
result: OperationResult = OperationResult("OK", 200)
|
|
50
|
+
result.data = data
|
|
51
|
+
return result
|
|
52
|
+
|
|
53
|
+
def created(self, data: Optional[Any] = None) -> TResult:
|
|
54
|
+
result: OperationResult = OperationResult("Created", 201)
|
|
55
|
+
result.data = data
|
|
56
|
+
return result
|
|
57
|
+
|
|
58
|
+
def bad_request(self, detail: str) -> TResult:
|
|
59
|
+
''' Creates a new OperationResult to describe the fact that the request is invalid '''
|
|
60
|
+
return OperationResult("Bad Request", 400, detail, "https://www.w3.org/Protocols/HTTP/HTRESP.html#:~:text=Bad%20Request")
|
|
61
|
+
|
|
62
|
+
def not_found(self, entity_type, entity_key, key_name: str = "id") -> TResult:
|
|
63
|
+
''' Creates a new OperationResult to describe the fact that an entity of the specified type and key could not be found or does not exist '''
|
|
64
|
+
return OperationResult("Not Found", 404, f"Failed to find an entity of type '{entity_type.__name__}' with the specified {key_name} '{entity_key}'", "https://www.w3.org/Protocols/HTTP/HTRESP.html#:~:text=Not%20found%20404")
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
TCommand = TypeVar('TCommand', bound=Command)
|
|
68
|
+
''' Represents the type of CQRS command to handle '''
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class CommandHandler(Generic[TCommand, TResult], RequestHandler[TCommand, TResult], ABC):
|
|
72
|
+
''' Represents a service used to handle a specific type of CQRS command '''
|
|
73
|
+
|
|
74
|
+
pass
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
TQuery = TypeVar('TQuery', bound=Query)
|
|
78
|
+
''' Represents the type of CQRS query to handle '''
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class QueryHandler(Generic[TQuery, TResult], RequestHandler[TQuery, TResult], ABC):
|
|
82
|
+
''' Represents a service used to handle a specific type of CQRS query '''
|
|
83
|
+
|
|
84
|
+
pass
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
TNotification = TypeVar('TNotification', bound=object)
|
|
88
|
+
''' Represents the type of CQRS notification to handle '''
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class NotificationHandler(Generic[TNotification], ABC):
|
|
92
|
+
''' Represents a service used to handle a specific type of CQRS notification'''
|
|
93
|
+
|
|
94
|
+
@abstractmethod
|
|
95
|
+
async def handle_async(self, notification: TNotification) -> None:
|
|
96
|
+
''' Handles the specified notification '''
|
|
97
|
+
raise NotImplementedError()
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
TDomainEvent = TypeVar('TDomainEvent', bound=DomainEvent)
|
|
101
|
+
''' Represents the type of domain event to handle '''
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class DomainEventHandler(Generic[TDomainEvent], NotificationHandler[TDomainEvent], ABC):
|
|
105
|
+
''' Represents a service used to handle a specific domain event '''
|
|
106
|
+
|
|
107
|
+
pass
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
TIntegrationEvent = TypeVar('TIntegrationEvent', bound=IntegrationEvent)
|
|
111
|
+
''' Represents the type of integration event to handle '''
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class IntegrationEventHandler(Generic[TIntegrationEvent], NotificationHandler[TIntegrationEvent], ABC):
|
|
115
|
+
''' Represents a service used to handle a specific integration event '''
|
|
116
|
+
|
|
117
|
+
pass
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class Mediator:
|
|
121
|
+
''' Represents the default implementation of the IMediator class '''
|
|
122
|
+
|
|
123
|
+
_service_provider: ServiceProviderBase
|
|
124
|
+
|
|
125
|
+
def __init__(self, service_provider: ServiceProviderBase):
|
|
126
|
+
self._service_provider = service_provider
|
|
127
|
+
|
|
128
|
+
async def execute_async(self, request: Request) -> OperationResult:
|
|
129
|
+
''' Executes the specified request '''
|
|
130
|
+
handlers: List[RequestHandler] = [candidate for candidate in self._service_provider.get_services(RequestHandler) if self._request_handler_matches(candidate, request)]
|
|
131
|
+
if handlers is None or len(handlers) < 1:
|
|
132
|
+
raise Exception(f"Failed to find a handler for request of type '{type(request).__name__}'")
|
|
133
|
+
elif len(handlers) > 1:
|
|
134
|
+
raise Exception(f"There must be exactly one handler defined for the command of type '{type(request).__name__}'")
|
|
135
|
+
log.info(f"Executing request type {type(request).__name__}")
|
|
136
|
+
handler = handlers[0]
|
|
137
|
+
return await handler.handle_async(request)
|
|
138
|
+
|
|
139
|
+
async def publish_async(self, notification: object):
|
|
140
|
+
''' Publishes the specified notification '''
|
|
141
|
+
handlers: List[NotificationHandler] = [candidate for candidate in self._service_provider.get_services(NotificationHandler) if self._notification_handler_matches(candidate, type(notification))]
|
|
142
|
+
if handlers is None or len(handlers) < 1:
|
|
143
|
+
return
|
|
144
|
+
await asyncio.gather(*(handler.handle_async(notification) for handler in handlers))
|
|
145
|
+
|
|
146
|
+
def _request_handler_matches(self, candidate, request_type) -> bool:
|
|
147
|
+
expected_request_type = request_type.__orig_class__ if hasattr(request_type, "__orig_class__") else request_type
|
|
148
|
+
handler_type = TypeExtensions.get_generic_implementation(candidate, RequestHandler)
|
|
149
|
+
handled_request_type = handler_type.__args__[0]
|
|
150
|
+
if type(handled_request_type) == type(expected_request_type):
|
|
151
|
+
matches = handled_request_type == expected_request_type
|
|
152
|
+
return matches
|
|
153
|
+
else:
|
|
154
|
+
return handled_request_type == type(expected_request_type)
|
|
155
|
+
|
|
156
|
+
def _notification_handler_matches(self, candidate, request_type) -> bool:
|
|
157
|
+
candidate_type = type(candidate)
|
|
158
|
+
handler_type = next(base for base in candidate_type.__orig_bases__ if (issubclass(base.__origin__, NotificationHandler) if hasattr(base, '__origin__') else issubclass(base, NotificationHandler)))
|
|
159
|
+
handled_notification_type = handler_type.__args__[0]
|
|
160
|
+
if isinstance(handled_notification_type, UnionType):
|
|
161
|
+
return any(issubclass(t, request_type) for t in handled_notification_type.__args__)
|
|
162
|
+
else:
|
|
163
|
+
return issubclass(handled_notification_type.__origin__, request_type) if hasattr(handled_notification_type, '__origin__') else issubclass(handled_notification_type, request_type)
|
|
164
|
+
|
|
165
|
+
def configure(app: ApplicationBuilderBase, modules: List[str] = list[str]()) -> ApplicationBuilderBase:
|
|
166
|
+
''' Registers and configures mediation-related services (command/query/notification handlers) to the specified service collection.
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
services (ServiceCollection): the service collection to configure
|
|
170
|
+
modules (List[str]): a list containing the names of the modules to scan for mediation services to register
|
|
171
|
+
'''
|
|
172
|
+
for module in [ModuleLoader.load(module_name) for module_name in modules]:
|
|
173
|
+
for command_handler_type in TypeFinder.get_types(module, lambda cls: inspect.isclass(cls) and (not hasattr(cls, "__parameters__") or len(cls.__parameters__) < 1) and issubclass(cls, CommandHandler) and cls != CommandHandler, include_sub_modules=True):
|
|
174
|
+
app.services.add_transient(RequestHandler, command_handler_type)
|
|
175
|
+
for queryhandler_type in TypeFinder.get_types(module, lambda cls: inspect.isclass(cls) and not hasattr(cls, "__parameters__") and issubclass(cls, QueryHandler) and cls != QueryHandler, include_sub_modules=True):
|
|
176
|
+
app.services.add_transient(RequestHandler, queryhandler_type)
|
|
177
|
+
for domain_event_handler_type in TypeFinder.get_types(module, lambda cls: inspect.isclass(cls) and issubclass(cls, DomainEventHandler) and cls != DomainEventHandler, include_sub_modules=True):
|
|
178
|
+
app.services.add_transient(NotificationHandler, domain_event_handler_type)
|
|
179
|
+
for integration_event_handler_type in TypeFinder.get_types(module, lambda cls: inspect.isclass(cls) and issubclass(cls, IntegrationEventHandler) and cls != IntegrationEventHandler, include_sub_packages=True):
|
|
180
|
+
app.services.add_transient(NotificationHandler, integration_event_handler_type)
|
|
181
|
+
app.services.add_singleton(Mediator)
|
|
182
|
+
return app
|
|
File without changes
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import uuid
|
|
2
|
+
|
|
3
|
+
from typing import Dict, Any
|
|
4
|
+
from classy_fastapi import Routable
|
|
5
|
+
from fastapi import Response
|
|
6
|
+
from fastapi.routing import APIRoute
|
|
7
|
+
from neuroglia.core.operation_result import OperationResult
|
|
8
|
+
from neuroglia.core.problem_details import ProblemDetails
|
|
9
|
+
from neuroglia.dependency_injection.service_provider import ServiceProviderBase
|
|
10
|
+
from neuroglia.mapping.mapper import Mapper
|
|
11
|
+
from neuroglia.mediation.mediator import Mediator
|
|
12
|
+
from neuroglia.serialization.json import JsonSerializer
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def generate_unique_id_function(route: APIRoute) -> str | APIRoute:
|
|
16
|
+
if route.tags and route.name:
|
|
17
|
+
tag = str(route.tags[0])
|
|
18
|
+
return f"{tag.lower()}_{route.name}"
|
|
19
|
+
elif route.methods and route.name:
|
|
20
|
+
method = str(route.methods.pop()).lower()
|
|
21
|
+
name = route.name.lower()
|
|
22
|
+
return f"{name}_{method}"
|
|
23
|
+
else:
|
|
24
|
+
method = str(route.methods.pop()).lower()
|
|
25
|
+
name = route.name.lower()
|
|
26
|
+
return f"{name}"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class ControllerBase(Routable):
|
|
30
|
+
"""Represents the base class of all API controllers"""
|
|
31
|
+
|
|
32
|
+
def __init__(self, service_provider: ServiceProviderBase, mapper: Mapper, mediator: Mediator):
|
|
33
|
+
"""Initializes a new ControllerBase"""
|
|
34
|
+
self.service_provider = service_provider
|
|
35
|
+
self.mapper = mapper
|
|
36
|
+
self.mediator = mediator
|
|
37
|
+
self.json_serializer = self.service_provider.get_required_service(JsonSerializer)
|
|
38
|
+
self.name = self.__class__.__name__.replace("Controller", "").strip()
|
|
39
|
+
super().__init__(
|
|
40
|
+
prefix=f"/{self.name.lower()}",
|
|
41
|
+
tags=[self.name],
|
|
42
|
+
generate_unique_id_function=generate_unique_id_function,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
service_provider: ServiceProviderBase
|
|
46
|
+
""" Gets the current ServiceProviderBase """
|
|
47
|
+
|
|
48
|
+
mapper: Mapper
|
|
49
|
+
""" Gets the service used to map objects """
|
|
50
|
+
|
|
51
|
+
mediator: Mediator
|
|
52
|
+
""" Gets the service used to mediate calls """
|
|
53
|
+
|
|
54
|
+
json_serializer: JsonSerializer
|
|
55
|
+
|
|
56
|
+
name: str
|
|
57
|
+
""" Gets/sets the name of the controller, which is used to configure the controller's router. Defaults to the lowercased name of the implementing controller class, excluding the term 'Controller' """
|
|
58
|
+
|
|
59
|
+
def process(self, result: OperationResult):
|
|
60
|
+
"""Processes the specified operation result"""
|
|
61
|
+
content = result.data if result.status >= 200 and result.status < 300 else result
|
|
62
|
+
media_type = "application/json"
|
|
63
|
+
if content is not None:
|
|
64
|
+
content = self.json_serializer.serialize_to_text(content)
|
|
65
|
+
media_type = "application/json"
|
|
66
|
+
return Response(status_code=result.status, content=content, media_type=media_type)
|
|
67
|
+
|
|
68
|
+
error_responses: Dict[int | str, Dict[str, Any]] | None = {
|
|
69
|
+
400: {"model": ProblemDetails, "description": "Bad Request"},
|
|
70
|
+
404: {"model": ProblemDetails, "description": "Not Found"},
|
|
71
|
+
500: {"model": ProblemDetails, "description": "Internal Server Error"},
|
|
72
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .rx_async import AsyncRx
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# import asyncio
|
|
2
|
+
from rx import operators as ops
|
|
3
|
+
from rx.core.observable.observable import Observable
|
|
4
|
+
from typing import Any, Callable
|
|
5
|
+
|
|
6
|
+
import rx
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class AsyncRx:
|
|
10
|
+
|
|
11
|
+
@staticmethod
|
|
12
|
+
def subscribe(source: Observable, on_next: Callable[[Any], None]):
|
|
13
|
+
return source.pipe(
|
|
14
|
+
ops.map(lambda value: rx.from_future(on_next(value))),
|
|
15
|
+
ops.concat()
|
|
16
|
+
).subscribe(lambda e: None)
|
|
File without changes
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from typing import Any, Optional, Type
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class Serializer(ABC):
|
|
6
|
+
''' Represents the base class for all serializers '''
|
|
7
|
+
|
|
8
|
+
@abstractmethod
|
|
9
|
+
def serialize(self, value: Any) -> bytearray:
|
|
10
|
+
''' Serializes the specified value '''
|
|
11
|
+
raise NotImplementedError()
|
|
12
|
+
|
|
13
|
+
@abstractmethod
|
|
14
|
+
def deserialize(self, input: bytearray, expected_type: Optional[Type]) -> Any:
|
|
15
|
+
''' Deserializes the specified input into a new value of the specified type, if any '''
|
|
16
|
+
raise NotImplementedError()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class TextSerializer(Serializer, ABC):
|
|
20
|
+
''' Represents the base class for all text serializers '''
|
|
21
|
+
|
|
22
|
+
@abstractmethod
|
|
23
|
+
def serialize_to_text(self, value: Any) -> str:
|
|
24
|
+
''' Serializes the specified value to text '''
|
|
25
|
+
raise NotImplementedError()
|
|
26
|
+
|
|
27
|
+
@abstractmethod
|
|
28
|
+
def deserialize_from_text(self, input: str, expected_type: Optional[Type] = None) -> Any:
|
|
29
|
+
''' Deserializes the specified input into a new value of the specified type, if any '''
|
|
30
|
+
raise NotImplementedError()
|