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,175 @@
1
+ import ast
2
+ import inspect
3
+ import os
4
+ from abc import ABC, abstractclassmethod
5
+ from ast import Attribute, Name, NodeTransformer, expr
6
+ from typing import Any, Callable, Dict, Generic, List, Optional, Type, TypeVar
7
+
8
+
9
+ T = TypeVar('T')
10
+ ''' Represents the type of data associated to a queryable '''
11
+
12
+
13
+ class QueryProvider(ABC):
14
+ ''' Defines the fundamentals of a service used to create and execute queries associated with the data source '''
15
+
16
+ @abstractclassmethod
17
+ def create_query(self, element_type: Type, expression: expr) -> 'Queryable':
18
+ ''' Creates a new queryable based on the specified expression '''
19
+ raise NotImplementedError()
20
+
21
+ @abstractclassmethod
22
+ def execute(self, expression: expr, query_type: Type) -> Any:
23
+ ''' Executes the specified query expression '''
24
+ raise NotImplementedError()
25
+
26
+
27
+ class Queryable(Generic[T]):
28
+ ''' Provides functionality to evaluate queries against a specific data source '''
29
+
30
+ def __init__(self, provider: QueryProvider, expression: Optional[expr] = None):
31
+ self.provider = provider
32
+ self.expression = ast.Name(id='__query') if expression is None else expression
33
+
34
+ expression: expr
35
+ ''' Gets the expression that is associated with the queryable '''
36
+
37
+ provider: QueryProvider
38
+ ''' Gets the service used to create and execute queries associated with the data source '''
39
+
40
+ def get_element_type(self) -> Type:
41
+ ''' Gets the type of elements to query against '''
42
+ return self.__orig_class__.__args__[0]
43
+
44
+ def first_or_default(self, predicate: Callable[[T], bool] = None) -> T:
45
+ ''' Gets the first element in the sequence that matches the specified predicate, if any '''
46
+ frame = inspect.currentframe().f_back
47
+ frame_info = inspect.getframeinfo(frame)
48
+ variables = {**frame.f_locals}
49
+ lambda_src = self._get_lambda_source_code(predicate, frame_info.positions.end_col_offset)
50
+ lambda_tree = ast.parse(lambda_src)
51
+ lambda_expression = lambda_tree.body[0].value
52
+ expression = VariableExpressionReplacer(variables).visit(ast.Call(func=ast.Attribute(value=self.expression, attr='first', ctx=ast.Load()), args=[lambda_expression], keywords=[]))
53
+ query = self.provider.create_query(self.get_element_type(), expression)
54
+ return self.provider.execute(query.expression, T)
55
+
56
+ def first(self, predicate: Callable[[T], bool] = None) -> T:
57
+ ''' Gets the first element in the sequence that matches the specified predicate, if any '''
58
+ result = self.first_or_default(predicate)
59
+ if result is None and T != None:
60
+ raise Exception("No match")
61
+ return result
62
+
63
+ def last_or_default(self, predicate: Callable[[T], bool] = None) -> T:
64
+ ''' Gets the last element in the sequence that matches the specified predicate, if any '''
65
+ frame = inspect.currentframe().f_back
66
+ frame_info = inspect.getframeinfo(frame)
67
+ variables = {**frame.f_locals}
68
+ lambda_src = self._get_lambda_source_code(predicate, frame_info.positions.end_col_offset)
69
+ lambda_tree = ast.parse(lambda_src)
70
+ lambda_expression = lambda_tree.body[0].value
71
+ expression = VariableExpressionReplacer(variables).visit(ast.Call(func=ast.Attribute(value=self.expression, attr='last', ctx=ast.Load()), args=[lambda_expression], keywords=[]))
72
+ query = self.provider.create_query(self.get_element_type(), expression)
73
+ return self.provider.execute(query.expression, T)
74
+
75
+ def last(self, predicate: Callable[[T], bool] = None) -> T:
76
+ ''' Gets the last element in the sequence that matches the specified predicate, if any '''
77
+ result = self.last_or_default(predicate)
78
+ if result is None and T != None:
79
+ raise Exception("No match")
80
+ return result
81
+
82
+ def order_by(self, selector: Callable[[T], Any]):
83
+ ''' Orders the sequence using the specified attribute '''
84
+ selector_source = self._get_lambda_source_code(selector)
85
+ selector_tree = ast.parse(selector_source)
86
+ selector_lambda_expression = selector_tree.body[0].value
87
+ if not isinstance(selector_lambda_expression.body, Attribute):
88
+ raise Exception("The specified expression must be of type Attribute")
89
+ expression = ast.Call(func=ast.Attribute(value=self.expression, attr='order_by', ctx=ast.Load()), args=[selector_lambda_expression], keywords=[])
90
+ return self.provider.create_query(self.get_element_type(), expression)
91
+
92
+ def order_by_descending(self, selector: Callable[[T], Any]):
93
+ ''' Orders the sequence in a descending fashion using the specified attribute '''
94
+ selector_source = self._get_lambda_source_code(selector)
95
+ selector_tree = ast.parse(selector_source)
96
+ selector_lambda_expression = selector_tree.body[0].value
97
+ if not isinstance(selector_lambda_expression.body, Attribute):
98
+ raise Exception("The specified expression must be of type Attribute")
99
+ expression = ast.Call(func=ast.Attribute(value=self.expression, attr='order_by_descending', ctx=ast.Load()), args=[selector_lambda_expression], keywords=[])
100
+ return self.provider.create_query(self.get_element_type(), expression)
101
+
102
+ def select(self, selector: Callable[[T], Any]):
103
+ ''' Projects each element of a sequence into a new form '''
104
+ frame = inspect.currentframe().f_back
105
+ variables = {**frame.f_locals}
106
+ selector_source = self._get_lambda_source_code(selector)
107
+ selector_tree = ast.parse(selector_source)
108
+ selector_lambda_expression = selector_tree.body[0].value
109
+ if not isinstance(selector_lambda_expression.body, Attribute) and not isinstance(selector_lambda_expression.body, ast.List):
110
+ raise Exception("The specified expression must be of type Attribute or List[Attribute]")
111
+ expression = VariableExpressionReplacer(variables).visit(ast.Call(func=ast.Attribute(value=self.expression, attr='select', ctx=ast.Load()), args=[selector_lambda_expression], keywords=[]))
112
+ return self.provider.create_query(self.get_element_type(), expression)
113
+
114
+ def skip(self, amount: int):
115
+ ''' Bypasses a specified number of elements in a sequence and then returns the remaining elements. '''
116
+ expression = ast.Call(func=ast.Attribute(value=self.expression, attr='skip', ctx=ast.Load()), args=[ast.Constant(value=amount)], keywords=[])
117
+ return self.provider.create_query(self.get_element_type(), expression)
118
+
119
+ def take(self, amount: int):
120
+ ''' Selects a specified amount of contiguous elements from the start of a sequence '''
121
+ expression = ast.Call(func=ast.Attribute(value=self.expression, attr='take', ctx=ast.Load()), args=[ast.Constant(value=amount)], keywords=[])
122
+ return self.provider.create_query(self.get_element_type(), expression)
123
+
124
+ def where(self, predicate: Callable[[T], bool]) -> 'Queryable[T]':
125
+ ''' Filters a sequence of values based on a predicate. '''
126
+ frame = inspect.currentframe().f_back
127
+ frame_info = inspect.getframeinfo(frame)
128
+ variables = {**frame.f_locals}
129
+ lambda_src = self._get_lambda_source_code(predicate, frame_info.positions.end_col_offset)
130
+ lambda_tree = ast.parse(lambda_src)
131
+ lambda_expression = lambda_tree.body[0].value
132
+ expression = VariableExpressionReplacer(variables).visit(ast.Call(func=ast.Attribute(value=self.expression, attr='where', ctx=ast.Load()), args=[lambda_expression], keywords=[]))
133
+ return self.provider.create_query(self.get_element_type(), expression)
134
+
135
+ def to_list(self) -> List[T]:
136
+ ''' Executes the queryable '''
137
+ return self.provider.execute(self.expression, List)
138
+
139
+ def __str__(self) -> str: return ast.unparse(self.expression)
140
+
141
+ def _get_lambda_source_code(self, function: Callable, max_col_offset: int):
142
+ ''' Gets the source code of the specified lambda
143
+
144
+ Args:
145
+ function (Callable): The lambda to get the source code of
146
+ max_col_offset (int): The maximum column offset to walk the AST tree for the target lamba
147
+
148
+ Notes:
149
+ Credits to https://gist.github.com/Xion/617c1496ff45f3673a5692c3b0e3f75a
150
+ '''
151
+ source_lines, _ = inspect.getsourcelines(function)
152
+ if len(source_lines) != 1:
153
+ return None
154
+ source_text = os.linesep.join(source_lines).strip()
155
+ source_ast = ast.parse(source_text)
156
+ lambda_node = next((node for node in ast.walk(source_ast) if isinstance(node, ast.Lambda) and node.col_offset <= max_col_offset), None)
157
+ if lambda_node is None:
158
+ return None
159
+ lambda_text = source_text[lambda_node.col_offset:lambda_node.end_col_offset]
160
+ return lambda_text
161
+
162
+
163
+ class VariableExpressionReplacer(NodeTransformer):
164
+
165
+ def __init__(self, variables: Dict[str, Any]):
166
+ super().__init__()
167
+ self.variables = variables
168
+
169
+ variables: Dict[str, Any] = dict[str, Any]()
170
+
171
+ def visit_Name(self, node: Name) -> Any:
172
+ if not node.id in self.variables.keys():
173
+ return node
174
+ value = self.variables[node.id]
175
+ return ast.Constant(value)
@@ -0,0 +1 @@
1
+ from .service_provider import *
@@ -0,0 +1,341 @@
1
+ from abc import ABC, abstractclassmethod
2
+ from contextlib import contextmanager
3
+ from enum import Enum
4
+ import inspect
5
+ from typing import Any, Callable, ForwardRef, List, Optional, Type, Dict, TypeVar
6
+
7
+ from neuroglia.core.type_extensions import TypeExtensions
8
+
9
+
10
+ class ServiceLifetime(Enum):
11
+ '''Enumerates all supported service lifetimes.'''
12
+
13
+ TRANSIENT = 'transient'
14
+ '''Transient services are created each time they are requested.'''
15
+
16
+ SCOPED = 'scoped'
17
+ '''Scoped services are created once per request within a scope (e.g., a web request).'''
18
+
19
+ SINGLETON = 'singleton'
20
+ '''Singleton services are created only once and reused for the entire application lifetime.'''
21
+
22
+
23
+ ServiceDescriptor = ForwardRef("ServiceDescriptor")
24
+
25
+ ServiceProvider = ForwardRef("ServiceProvider")
26
+
27
+ ServiceScopeBase = ForwardRef("IServiceScope")
28
+
29
+
30
+ class ServiceProviderBase(ABC):
31
+ ''' Defines the fundamentals of a container used to manage and provide instances of dependencies, enabling dependency injection to promote modularity and maintainability. '''
32
+
33
+ def get_service(self, type: Type) -> Optional[any]:
34
+ ''' Gets the service with the specified type, if any has been registered '''
35
+ raise NotImplementedError()
36
+
37
+ def get_required_service(self, type: Type) -> any:
38
+ ''' Gets the required service with the specified type '''
39
+ raise NotImplementedError()
40
+
41
+ def get_services(self, type: Type) -> List:
42
+ ''' Gets all services of the specified type '''
43
+ raise NotImplementedError()
44
+
45
+ def create_scope(self) -> ServiceScopeBase:
46
+ ''' Creates a new service scope '''
47
+ raise NotImplementedError()
48
+
49
+
50
+ class ServiceScopeBase(ABC):
51
+ ''' Defines the fundamentals of a a limited context within which services are resolved and managed by a service provider, allowing for scoped instances and controlled lifetimes of dependencies. '''
52
+
53
+ @abstractclassmethod
54
+ def get_service_provider(self) -> ServiceProviderBase:
55
+ ''' Gets the scoped service provider '''
56
+ raise NotImplementedError()
57
+
58
+ @abstractclassmethod
59
+ def dispose(self):
60
+ ''' Disposes of the service scope '''
61
+ raise NotImplementedError()
62
+
63
+
64
+ class ServiceScope(ServiceScopeBase, ServiceProviderBase):
65
+ ''' Represents the default implementation of the IServiceScope class '''
66
+
67
+ def __init__(self, root_service_provider: ServiceProviderBase, scoped_service_descriptors: List[ServiceDescriptor]):
68
+ self._root_service_provider = root_service_provider
69
+ self._scoped_service_descriptors = scoped_service_descriptors
70
+
71
+ _root_service_provider: ServiceProviderBase
72
+ ''' Gets the IServiceProvider that has created the service scope '''
73
+
74
+ _scoped_service_descriptors: List[ServiceDescriptor]
75
+ ''' Gets a list containing the configurations of all scoped dependencies '''
76
+
77
+ _realized_scoped_services: Dict[Type, List] = dict[Type, List]()
78
+ ''' Gets a type/list mapping containing all scoped services that have already been built/resolved '''
79
+
80
+ def get_service_provider(self) -> ServiceProviderBase: return self
81
+
82
+ def get_service(self, type: Type) -> Optional[any]:
83
+ if type == ServiceProviderBase:
84
+ return self
85
+ realized_services = self._realized_scoped_services.get(type)
86
+ realized_service = self._root_service_provider.get_service(type) if realized_services is None else realized_services[0]
87
+ if realized_service is not None:
88
+ return realized_service
89
+ descriptor = next(descriptor for descriptor in self._scoped_service_descriptors if descriptor.service_type == type)
90
+ if descriptor is None:
91
+ return None
92
+ return self._build_service(descriptor)
93
+
94
+ def get_required_service(self, type: Type) -> any:
95
+ service = self.get_service(type)
96
+ if service is None:
97
+ raise Exception(f"Failed to resolve service of type '{type.__name__}'")
98
+ return service
99
+
100
+ def get_services(self, type: Type) -> List:
101
+ if type == ServiceProviderBase:
102
+ return [self]
103
+ service_descriptors = [descriptor for descriptor in self._scoped_service_descriptors if descriptor.service_type == type]
104
+ realized_services = self._realized_scoped_services.get(type)
105
+ if realized_services is None:
106
+ realized_services = List()
107
+ for descriptor in service_descriptors:
108
+ if any(type(service) == descriptor.service_type for service in realized_services):
109
+ continue
110
+ realized_services.append(self._build_service(descriptor))
111
+ return realized_services + self._root_service_provider.get_services(type)
112
+
113
+ def _build_service(self, service_descriptor: ServiceDescriptor) -> any:
114
+ ''' Builds a new service provider based on the configured dependencies '''
115
+ if service_descriptor.lifetime == ServiceLifetime.SCOPED:
116
+ raise Exception(f"Failed to resolve scoped service of type '{service_descriptor.implementation_type}' from root service provider")
117
+ if service_descriptor.singleton is not None:
118
+ service = service_descriptor.singleton
119
+ elif service_descriptor.implementation_factory is not None:
120
+ service = service_descriptor.implementation_factory(self)
121
+ else:
122
+ init_args = [param for param in inspect.signature(service_descriptor.implementation_type.__init__).parameters.values() if param.name not in ['self', 'args', 'kwargs']]
123
+ args = dict[Type, any]()
124
+ for init_arg in init_args:
125
+ dependency = self.get_service(init_arg.annotation)
126
+ if dependency is None and init_arg.default == init_arg.empty and init_arg.name != 'self':
127
+ raise Exception(f"Failed to build service of type '{service_descriptor.service_type.__name__}' because the service provider failed to resolve service '{init_arg.annotation}'")
128
+ args[init_arg.name] = dependency
129
+ service = service_descriptor.implementation_type(**args)
130
+ if service_descriptor.lifetime != ServiceLifetime.TRANSIENT:
131
+ realized_services = self._realized_scoped_services.get(service_descriptor.service_type)
132
+ if realized_services is None:
133
+ self._realized_scoped_services[service_descriptor.service_type] = {service}
134
+ else:
135
+ realized_services.append(service)
136
+ return service
137
+
138
+ def create_scope(self) -> ServiceScopeBase: return self
139
+
140
+ def dispose(self):
141
+ for service in self._realized_scoped_services:
142
+ service.__exit__()
143
+ self._realized_scoped_services = dict[Type, List]()
144
+
145
+
146
+ class ServiceProvider(ServiceProviderBase):
147
+ ''' Represents the default implementation of the IServiceProvider class '''
148
+
149
+ def __init__(self, service_descriptors: List[ServiceDescriptor]):
150
+ ''' Initializes a new service provider using the specified service dependency configuration '''
151
+ self._service_descriptors = service_descriptors
152
+
153
+ _service_descriptors: List[ServiceDescriptor]
154
+ ''' Gets a list containing the configuration of all registered dependencies '''
155
+
156
+ _realized_services: Dict[Type, List] = dict[Type, List]()
157
+ ''' Gets a type/list mapping containing all services that have already been built/resolved '''
158
+
159
+ def get_service(self, type: Type) -> Optional[any]:
160
+ if type == ServiceProviderBase:
161
+ return self
162
+ realized_services = self._realized_services.get(type)
163
+ if realized_services is not None:
164
+ return realized_services[0]
165
+ if len(self._service_descriptors) < 1:
166
+ return None
167
+ descriptor = next((descriptor for descriptor in self._service_descriptors if descriptor.service_type == type), None)
168
+ if descriptor is None:
169
+ return None
170
+ return self._build_service(descriptor)
171
+
172
+ def get_required_service(self, type: Type) -> any:
173
+ service = self.get_service(type)
174
+ if service is None:
175
+ raise Exception(f"Failed to resolve service of type '{type.__name__}'")
176
+ return service
177
+
178
+ def get_services(self, type: Type) -> List:
179
+ if type == ServiceProviderBase:
180
+ return [self]
181
+ service_descriptors = [descriptor for descriptor in self._service_descriptors if descriptor.service_type == type]
182
+ realized_services = self._realized_services.get(type)
183
+ if realized_services is None:
184
+ realized_services = list()
185
+ for descriptor in service_descriptors:
186
+ implementation_type = descriptor.get_implementation_type()
187
+ realized_service = next((service for service in realized_services if self._is_service_instance_of(service, implementation_type)), None)
188
+ if realized_service is None:
189
+ realized_services.append(self._build_service(descriptor))
190
+ return realized_services
191
+
192
+ def _is_service_instance_of(self, service: Any, type_: Type) -> bool:
193
+ if hasattr(type_, "__origin__"):
194
+ service_type = service.__orig_class__ if hasattr(service, "__orig_class__") else type(service)
195
+ service_generic_arguments = TypeExtensions.get_generic_arguments(service_type)
196
+ implementation_generic_arguments = TypeExtensions.get_generic_arguments(type_)
197
+ for i in range(len(implementation_generic_arguments)):
198
+ generic_argument_name = list(implementation_generic_arguments.keys())[i]
199
+ service_generic_argument = service_generic_arguments.get(generic_argument_name, None)
200
+ if service_generic_argument is None or service_generic_argument != implementation_generic_arguments[generic_argument_name]:
201
+ return False
202
+ return isinstance(service, type_.__origin__)
203
+ else:
204
+ return isinstance(service, type_)
205
+
206
+ def _build_service(self, service_descriptor: ServiceDescriptor) -> any:
207
+ ''' Builds a new service provider based on the configured dependencies '''
208
+ if service_descriptor.lifetime == ServiceLifetime.SCOPED:
209
+ raise Exception(f"Failed to resolve scoped service of type '{service_descriptor.implementation_type}' from root service provider")
210
+ if service_descriptor.singleton is not None:
211
+ service = service_descriptor.singleton
212
+ elif service_descriptor.implementation_factory is not None:
213
+ service = service_descriptor.implementation_factory(self)
214
+ else:
215
+ is_service_generic = not inspect.isclass(service_descriptor.implementation_type) # if implementation_type is not a class, then it must be a generic type
216
+ service_generic_type = service_descriptor.implementation_type.__origin__ if is_service_generic else None # retrieve the generic type, used to determine the __init__ args
217
+ service_type = service_descriptor.implementation_type if service_generic_type is None else service_generic_type # get the type used to determine the __init__ args: the implementation type as is or its generic type definition
218
+ service_init_args = [param for param in inspect.signature(service_type.__init__).parameters.values() if param.name not in ['self', 'args', 'kwargs']] # gets the __init__ args and leave out self, args and kwargs
219
+ service_generic_args = TypeExtensions.get_generic_arguments(service_descriptor.implementation_type) # gets the generic args: we will need them to substitute the type args of potential generic dependencies
220
+ service_args = dict[Type, any]()
221
+ for init_arg in service_init_args:
222
+ is_dependency_generic = not inspect.isclass(init_arg.annotation)
223
+ dependency_generic_type = init_arg.annotation.__origin__ if is_dependency_generic else None
224
+ dependency_generic_args = None if dependency_generic_type is None else init_arg.annotation.__args__
225
+ if dependency_generic_args is not None:
226
+ dependency_generic_args = [service_generic_args[arg.__name__] if type(arg) == TypeVar else arg for arg in dependency_generic_args] # replace TypeVar generic arguments by the service's matching generic argument
227
+ dependency_type = init_arg.annotation.__origin__[*dependency_generic_args] if is_dependency_generic else init_arg.annotation
228
+ dependency = self.get_service(dependency_type)
229
+ if dependency is None and init_arg.default == init_arg.empty and init_arg.name != 'self':
230
+ raise Exception(f"Failed to build service of type '{service_descriptor.service_type.__name__}' because the service provider failed to resolve service '{dependency_type.__name__}'")
231
+ service_args[init_arg.name] = dependency
232
+ service = service_descriptor.implementation_type(**service_args)
233
+ if service_descriptor.lifetime != ServiceLifetime.TRANSIENT:
234
+ realized_services = self._realized_services.get(service_descriptor.service_type)
235
+ if realized_services is None:
236
+ self._realized_services[service_descriptor.service_type] = [service]
237
+ else:
238
+ realized_services.append(service)
239
+ return service
240
+
241
+ @contextmanager
242
+ def create_scope(self) -> ServiceScopeBase:
243
+ service_scope = ServiceScope(self, [descriptor for descriptor in self._service_descriptors if descriptor.lifetime == ServiceLifetime.SCOPED])
244
+ try:
245
+ yield service_scope
246
+ finally:
247
+ service_scope.dispose()
248
+
249
+ def dispose(self):
250
+ for service in self._realized_services:
251
+ try:
252
+ service.__exit__()
253
+ except:
254
+ pass
255
+ self._realized_services = dict[Type, List]()
256
+
257
+
258
+ class ServiceDescriptor:
259
+ ''' Represents an object used to describe and configure a service dependency '''
260
+
261
+ def __init__(self, service_type: Type, implementation_type: Optional[Type] = None, singleton: any = None, implementation_factory: Callable[[ServiceProvider], any] = None, lifetime: ServiceLifetime = ServiceLifetime.SINGLETON):
262
+ ''' Initializes a new service descriptor '''
263
+ if singleton is not None and lifetime != ServiceLifetime.SINGLETON:
264
+ raise Exception("A singleton service dependency must have lifetime set to 'SINGLETON'")
265
+ self.service_type = service_type
266
+ self.implementation_type = implementation_type
267
+ self.singleton = singleton
268
+ self.implementation_factory = implementation_factory
269
+ self.lifetime = lifetime
270
+ if self.singleton is None and self.implementation_factory is None and self.implementation_type is None:
271
+ self.implementation_type = self.service_type
272
+
273
+ service_type: Type
274
+ ''' Gets the type of the service dependency '''
275
+
276
+ implementation_type: Optional[Type]
277
+ ''' Gets the service dependency's implementation/concretion type, if any, to be instanciated on demand by a service provider. If set, 'singleton' and 'implementation-factory' are ignored. '''
278
+
279
+ singleton: any
280
+ ''' Gets the service instance singleton, if any. If set, 'implementation_type' and 'implementation-factory' are ignored. '''
281
+
282
+ implementation_factory: Callable[[ServiceProvider], any]
283
+ ''' Gets a function, if any, use to create a new instance of the service dependency. If set, 'implementation_type' and 'singleton' are ignored. '''
284
+
285
+ lifetime: ServiceLifetime = ServiceLifetime.SINGLETON
286
+ ''' Gets the service's lifetime. Defaults to 'SINGLETON' '''
287
+
288
+ def get_implementation_type(self) -> Type:
289
+ ''' Gets the service's implementation type '''
290
+ if self.implementation_type is not None:
291
+ return self.implementation_type
292
+ return_type = inspect.signature(self.implementation_factory).return_annotation if self.implementation_factory != None else None
293
+ if return_type is None and self.implementation_factory != None:
294
+ if self.implementation_type is None:
295
+ raise Exception(f"Failed to determine the return type of the implementation factory configured for service of type '{self.service_type.__name__}'. Either specify the implementation type, or use a function instead of a lambda as factory callable.")
296
+ else:
297
+ return_type = self.implementation_type
298
+ return type(self.singleton) if self.singleton is not None else inspect.signature(self.implementation_factory).return_annotation
299
+
300
+
301
+ ServiceCollection = ForwardRef("ServiceCollection")
302
+
303
+
304
+ class ServiceCollection(List[ServiceDescriptor]):
305
+ ''' Represents a collection of service descriptors used to configure a service provider '''
306
+
307
+ def add_singleton(self, service_type: Type, implementation_type: Optional[Type] = None, singleton: any = None, implementation_factory: Callable[[ServiceProvider], any] = None) -> ServiceCollection:
308
+ ''' Registers a new singleton service dependency '''
309
+ self.append(ServiceDescriptor(service_type, implementation_type, singleton, implementation_factory, ServiceLifetime.SINGLETON))
310
+ return self
311
+
312
+ def try_add_singleton(self, service_type: Type, implementation_type: Optional[Type] = None, singleton: any = None, implementation_factory: Callable[[ServiceProvider], any] = None) -> ServiceCollection:
313
+ ''' Attempts to register a new singleton service dependency, if one has not already been registered'''
314
+ if any(descriptor.service_type == service_type for descriptor in self):
315
+ return self
316
+ return self.add_singleton(service_type, implementation_type, singleton, implementation_factory)
317
+
318
+ def add_transient(self, service_type: Type, implementation_type: Optional[Type] = None, implementation_factory: Callable[[ServiceProvider], any] = None) -> ServiceCollection:
319
+ ''' Registers a new transient service dependency '''
320
+ self.append(ServiceDescriptor(service_type, implementation_type, None, implementation_factory, ServiceLifetime.SINGLETON))
321
+ return self
322
+
323
+ def try_add_transient(self, service_type: Type, implementation_type: Optional[Type] = None, implementation_factory: Callable[[ServiceProvider], any] = None) -> ServiceCollection:
324
+ ''' Attempts to register a new transient service dependency, if one has not already been registered'''
325
+ if any(descriptor.service_type == service_type for descriptor in self):
326
+ return self
327
+ return self.add_transient(service_type, implementation_type, implementation_factory)
328
+
329
+ def add_scoped(self, service_type: Type, implementation_type: Optional[Type] = None, implementation_factory: Callable[[ServiceProvider], any] = None) -> ServiceCollection:
330
+ ''' Registers a new scoped service dependency '''
331
+ self.append(ServiceDescriptor(service_type, implementation_type, None, implementation_factory, ServiceLifetime.SINGLETON))
332
+ return self
333
+
334
+ def try_add_scoped(self, service_type: Type, implementation_type: Optional[Type] = None, implementation_factory: Callable[[ServiceProvider], any] = None) -> ServiceCollection:
335
+ ''' Attempts to register a new scoped service dependency, if one has not already been registered'''
336
+ if any(descriptor.service_type == service_type for descriptor in self):
337
+ return self
338
+ return self.add_scoped(service_type, implementation_type, implementation_factory)
339
+
340
+ def build(self) -> ServiceProviderBase:
341
+ return ServiceProvider(self)
@@ -0,0 +1 @@
1
+ from .cloud_event import CloudEvent as CloudEvent
@@ -0,0 +1,72 @@
1
+ from dataclasses import dataclass
2
+ from datetime import datetime
3
+ from typing import Any, Optional
4
+
5
+
6
+ class CloudEventSpecVersion:
7
+ ''' Exposes all supported versions of the Cloud Event Specification '''
8
+
9
+ v1_0: str = "1.0"
10
+ ''' Gets the version '1.0' of the Cloud Event Specification '''
11
+
12
+
13
+ @dataclass
14
+ class CloudEvent:
15
+ '''
16
+ Represents a Cloud Event.
17
+
18
+ Attributes:
19
+ id (str): A string that uniquely identifies the cloud event in the scope of its source.
20
+ specversion (str): The version of the CloudEvents specification which the event uses. Defaults to '1.0'.
21
+ time (Optional[datetime]): The date and time at which the event has been produced.
22
+ source (str): The cloud event's source.
23
+ type (str): The cloud event's type.
24
+ subject (Optional[str]): A value that describes the subject of the event in the context of the event producer.
25
+ datacontenttype (Optional[str]): The cloud event's data content type. Defaults to 'application/json'.
26
+ dataschema (Optional[str]): An URI that references the versioned schema of the event's data.
27
+ data (Optional[Any]): The event's data, if any. Only used if the event has been formatted using the structured mode.
28
+ data_base64 (Optional[str]): The event's binary data, encoded in base 64. Used if the event has been formatted using the binary mode.
29
+ extensionAttributes (Optional[Dict[str, Any]]): A mapping containing the event's extension attributes.
30
+ '''
31
+
32
+ id: str
33
+ ''' Gets/sets string that uniquely identifies the cloud event in the scope of its source. '''
34
+
35
+ source: str
36
+ ''' Gets/sets the cloud event's source. Must be an absolute URI. '''
37
+
38
+ type: str
39
+ ''' Gets/sets the cloud event's source. Should be a reverse DNS domain name, which must only contain lowercase alphanumeric, '-' and '.' characters. '''
40
+
41
+ specversion: str = '1.0' # Default value for specversion
42
+ ''' Gets/sets the version of the CloudEvents specification which the event uses. Defaults to '1.0'. '''
43
+
44
+ sequencetype: Optional[str] = None
45
+ ''' Gets/sets the type of the sequence. '''
46
+
47
+ sequence: Optional[int] = None
48
+ ''' Gets/sets the sequence of the event. '''
49
+
50
+ time: Optional[datetime] = None
51
+ ''' Gets/sets the date and time at which the event has been produced. '''
52
+
53
+ subject: Optional[str] = None
54
+ ''' Gets/sets value that describes the subject of the event in the context of the event producer. '''
55
+
56
+ datacontenttype: Optional[str] = 'application/json' # Default value for datacontenttype
57
+ ''' Gets/sets the cloud event's data content type. Defaults to 'application/json'. '''
58
+
59
+ dataschema: Optional[str] = None
60
+ ''' Gets/sets an URI, if any, that references the versioned schema of the event's data. '''
61
+
62
+ data: Optional[Any] = None
63
+ ''' Gets/sets the event's data, if any. Only used if the event has been formatted using the structured mode. '''
64
+
65
+ data_base64: Optional[str] = None
66
+ ''' Gets/sets the event's binary data, encoded in base 64. Used if the event has been formatted using the binary mode. '''
67
+
68
+ def get_attribute(self, name: str) -> Optional[Any]:
69
+ ''' Gets the value of the attribute with the specified name, if any '''
70
+ if not name:
71
+ raise ValueError("Attribute name cannot be empty or None.")
72
+ self.__dict__[name] if name in self.__dict__.keys() else None
@@ -0,0 +1,5 @@
1
+ def cloudevent(cloud_event_type: str):
2
+ def decorator(cls):
3
+ cls.__cloudevent__type__ = cloud_event_type
4
+ return cls
5
+ return decorator
@@ -0,0 +1,4 @@
1
+ from .cloud_event_bus import CloudEventBus
2
+ from .cloud_event_ingestor import CloudEventIngestor
3
+ from .cloud_event_middleware import CloudEventMiddleware
4
+ from .cloud_event_publisher import CloudEventPublisher
@@ -0,0 +1,11 @@
1
+ from rx.subject import Subject
2
+
3
+
4
+ class CloudEventBus:
5
+ ''' Defines the fundamentals of a service used to manage incoming and outgoing streams of cloud events '''
6
+
7
+ input_stream: Subject = Subject()
8
+ ''' Gets the stream of events ingested by the application '''
9
+
10
+ output_stream: Subject = Subject()
11
+ ''' Gets the stream of events published by the application '''