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
@@ -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()