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,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,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 '''
|