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,74 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import inspect
|
|
3
|
+
import logging
|
|
4
|
+
from typing import List, Type
|
|
5
|
+
from neuroglia.core import TypeFinder, ModuleLoader
|
|
6
|
+
from neuroglia.eventing.cloud_events import CloudEvent
|
|
7
|
+
from neuroglia.eventing.cloud_events.infrastructure import CloudEventBus
|
|
8
|
+
from neuroglia.hosting.abstractions import ApplicationBuilderBase, HostedService
|
|
9
|
+
from neuroglia.mediation.mediator import Mediator
|
|
10
|
+
from neuroglia.reactive import AsyncRx
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class CloudEventIngestionOptions:
|
|
14
|
+
"""Represents the service used to configure how the application should ingest incoming cloud events"""
|
|
15
|
+
|
|
16
|
+
type_maps: dict[str, Type] = dict[str, Type]()
|
|
17
|
+
""" Gets/sets a cloud event type/CLR type mapping of all supported cloud events"""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class CloudEventIngestor(HostedService):
|
|
21
|
+
"""Represents the service used to ingest cloud events"""
|
|
22
|
+
|
|
23
|
+
def __init__(self, options: CloudEventIngestionOptions, cloud_event_bus: CloudEventBus, mediator: Mediator):
|
|
24
|
+
self._options = options
|
|
25
|
+
self._cloud_event_bus = cloud_event_bus
|
|
26
|
+
self._mediator = mediator
|
|
27
|
+
|
|
28
|
+
_options: CloudEventIngestionOptions
|
|
29
|
+
|
|
30
|
+
_cloud_event_bus: CloudEventBus
|
|
31
|
+
|
|
32
|
+
_mediator: Mediator
|
|
33
|
+
|
|
34
|
+
async def start_async(self):
|
|
35
|
+
AsyncRx.subscribe(self._cloud_event_bus.input_stream, lambda e: asyncio.ensure_future(self._on_cloud_event_async(e)))
|
|
36
|
+
|
|
37
|
+
async def _on_cloud_event_async(self, cloud_event: CloudEvent):
|
|
38
|
+
event_type = self._options.type_maps.get(cloud_event.type, None)
|
|
39
|
+
if event_type is None:
|
|
40
|
+
logging.warning(f"Ignored incoming cloud event: the specified type '{cloud_event.type}' is not supported")
|
|
41
|
+
return
|
|
42
|
+
|
|
43
|
+
e: object = None
|
|
44
|
+
try:
|
|
45
|
+
e = object.__new__(event_type)
|
|
46
|
+
e.__dict__ = cloud_event.data
|
|
47
|
+
e.__cloudevent__source__ = cloud_event.source
|
|
48
|
+
e.__cloudevent__subject__ = cloud_event.subject
|
|
49
|
+
except Exception as ex:
|
|
50
|
+
logging.error(f"An error occured while reading a cloud event of type '{cloud_event.type}': '{ex}'")
|
|
51
|
+
raise
|
|
52
|
+
try:
|
|
53
|
+
await self._mediator.publish_async(e)
|
|
54
|
+
except Exception as ex:
|
|
55
|
+
logging.error(f"An error occured while dispatching an incoming cloud event of type '{cloud_event.type}': '{ex}'")
|
|
56
|
+
raise
|
|
57
|
+
|
|
58
|
+
@staticmethod
|
|
59
|
+
def configure(builder: ApplicationBuilderBase, modules: List[str]) -> ApplicationBuilderBase:
|
|
60
|
+
"""Registers and configures cloud event related services to the specified service collection.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
services (ServiceCollection): the service collection to configure
|
|
64
|
+
modules (List[str]): a list containing the names of the modules to scan for classes marked with the 'cloudevent' decorator. Marked classes as used to configure the mapping of cloud events consumed by the cloud event ingestor
|
|
65
|
+
"""
|
|
66
|
+
options: CloudEventIngestionOptions = CloudEventIngestionOptions()
|
|
67
|
+
for module in [ModuleLoader.load(module_name) for module_name in modules]:
|
|
68
|
+
for cloud_event_clr_type in TypeFinder.get_types(module, lambda cls: inspect.isclass(cls) and hasattr(cls, "__cloudevent__type__")):
|
|
69
|
+
cloud_event_type = cloud_event_clr_type.__cloudevent__type__
|
|
70
|
+
options.type_maps[cloud_event_type] = cloud_event_clr_type
|
|
71
|
+
builder.services.try_add_singleton(CloudEventBus)
|
|
72
|
+
builder.services.add_singleton(CloudEventIngestionOptions, singleton=options)
|
|
73
|
+
builder.services.add_singleton(HostedService, CloudEventIngestor)
|
|
74
|
+
return builder
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from fastapi import FastAPI, Request, Response
|
|
3
|
+
from neuroglia.dependency_injection.service_provider import ServiceProviderBase
|
|
4
|
+
from neuroglia.eventing.cloud_events.cloud_event import CloudEvent
|
|
5
|
+
from neuroglia.eventing.cloud_events.infrastructure import CloudEventBus
|
|
6
|
+
from neuroglia.serialization.json import JsonSerializer
|
|
7
|
+
from starlette.middleware.base import BaseHTTPMiddleware
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class CloudEventMiddleware(BaseHTTPMiddleware):
|
|
11
|
+
''' Represents the HTTP middleware used to handle incoming cloud events '''
|
|
12
|
+
|
|
13
|
+
def __init__(self, app, service_provider: ServiceProviderBase):
|
|
14
|
+
super().__init__(app)
|
|
15
|
+
self.service_provider = service_provider
|
|
16
|
+
self.serializer = self.service_provider.get_required_service(JsonSerializer)
|
|
17
|
+
self.cloud_event_bus = self.service_provider.get_required_service(CloudEventBus)
|
|
18
|
+
|
|
19
|
+
service_provider: ServiceProviderBase
|
|
20
|
+
''' Gets the current service provider '''
|
|
21
|
+
|
|
22
|
+
cloud_event_bus: CloudEventBus
|
|
23
|
+
''' Gets the service used to stream the application's incoming and outgoing cloud events '''
|
|
24
|
+
|
|
25
|
+
serializer: JsonSerializer
|
|
26
|
+
''' Gets the service used to serialize/deserialize values to/from JSON '''
|
|
27
|
+
|
|
28
|
+
async def dispatch(self, request: Request, call_next):
|
|
29
|
+
content_type = request.headers.get('content-type', None)
|
|
30
|
+
if content_type is None or not content_type.startswith('application/cloudevents+json'):
|
|
31
|
+
return await call_next(request)
|
|
32
|
+
try:
|
|
33
|
+
attributes = self.serializer.deserialize(await request.body(), dict)
|
|
34
|
+
cloud_event = CloudEvent(**attributes)
|
|
35
|
+
self.cloud_event_bus.input_stream.on_next(cloud_event)
|
|
36
|
+
except Exception as ex:
|
|
37
|
+
logging.error(f"An error occured while processing an incoming cloud event: {ex}")
|
|
38
|
+
return Response(content=str(ex), status_code=500)
|
|
39
|
+
return Response(status_code=202)
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import httpx
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from urllib.parse import urlparse
|
|
7
|
+
from rx.core.typing import Disposable
|
|
8
|
+
|
|
9
|
+
from neuroglia.eventing.cloud_events.cloud_event import CloudEvent
|
|
10
|
+
from neuroglia.eventing.cloud_events.infrastructure.cloud_event_bus import CloudEventBus
|
|
11
|
+
from neuroglia.hosting.abstractions import ApplicationBuilderBase, HostedService
|
|
12
|
+
from neuroglia.reactive.rx_async import AsyncRx
|
|
13
|
+
from neuroglia.serialization.json import JsonSerializer
|
|
14
|
+
|
|
15
|
+
log = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class CloudEventPublishingOptions:
|
|
20
|
+
''' Represents the service used to configure the way Cloud Events should be published by the application '''
|
|
21
|
+
|
|
22
|
+
sink_uri: str
|
|
23
|
+
''' Gets the URI to publish cloud events to '''
|
|
24
|
+
|
|
25
|
+
source: str
|
|
26
|
+
''' Gets the value of the 'source' context attribute for all cloud events produced by the application '''
|
|
27
|
+
|
|
28
|
+
type_prefix: str = "io.openbank"
|
|
29
|
+
''' Gets the prefix value of the 'type' context attribute for all cloud events produced by the application '''
|
|
30
|
+
|
|
31
|
+
retry_attempts: int = 5
|
|
32
|
+
''' Gets/sets the maximum amount of retries before giving up '''
|
|
33
|
+
|
|
34
|
+
retry_delay: float = 1
|
|
35
|
+
''' Gets/sets the delay, in seconds, to wait after each retry attempt. Configured value is multiplied by the amount of retries that have been performed '''
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class CloudEventPublisher(HostedService):
|
|
39
|
+
''' Represents the service used to publish the application's outgoing cloud events '''
|
|
40
|
+
|
|
41
|
+
def __init__(self, options: CloudEventPublishingOptions, cloud_event_bus: CloudEventBus, json_serializer: JsonSerializer):
|
|
42
|
+
self._options = options
|
|
43
|
+
self._cloud_event_bus = cloud_event_bus
|
|
44
|
+
self._json_serializer: JsonSerializer = json_serializer
|
|
45
|
+
|
|
46
|
+
_options: CloudEventPublishingOptions
|
|
47
|
+
''' Gets the current CloudEventPublishingOptions '''
|
|
48
|
+
|
|
49
|
+
_cloud_event_bus: CloudEventBus
|
|
50
|
+
''' Gets the service used to manage the cloud events produced and consumed by the application '''
|
|
51
|
+
|
|
52
|
+
_json_serializer: JsonSerializer
|
|
53
|
+
|
|
54
|
+
_subscription: Disposable
|
|
55
|
+
|
|
56
|
+
async def start_async(self):
|
|
57
|
+
# ERROR:root:An exception occured while publishing an event of type 'PersonRegisteredDomainEventV1': asyncio.run() cannot be called from a running event loop
|
|
58
|
+
# /tmp/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_trace_dispatch_regular.py:326: RuntimeWarning: coroutine 'CloudEventPublisher.on_publish_cloud_event_async' was never awaited
|
|
59
|
+
# self._subscription = AsyncRx.subscribe(self._cloud_event_bus.output_stream, lambda e: asyncio.run(self.on_publish_cloud_event_async(e)))
|
|
60
|
+
self._subscription = AsyncRx.subscribe(self._cloud_event_bus.output_stream, lambda e: asyncio.create_task(self.on_publish_cloud_event_async(e)))
|
|
61
|
+
# await self._subscription
|
|
62
|
+
|
|
63
|
+
async def stop_async(self):
|
|
64
|
+
self._subscription.dispose()
|
|
65
|
+
|
|
66
|
+
async def on_publish_cloud_event_async(self, e: CloudEvent):
|
|
67
|
+
uri = urlparse(self._options.sink_uri)
|
|
68
|
+
published = False
|
|
69
|
+
for retries in range(self._options.retry_attempts):
|
|
70
|
+
try:
|
|
71
|
+
url = uri.geturl()
|
|
72
|
+
headers = {
|
|
73
|
+
"Content-Type": "application/cloudevents+json"
|
|
74
|
+
}
|
|
75
|
+
response = None
|
|
76
|
+
with httpx.Client() as client:
|
|
77
|
+
response = client.post(url=url, headers=headers, content=self._json_serializer.serialize(e))
|
|
78
|
+
response.raise_for_status()
|
|
79
|
+
if response is not None and 200 <= response.status_code < 300:
|
|
80
|
+
log.debug(f"Published cloudevent: {e.type}")
|
|
81
|
+
published = True
|
|
82
|
+
break
|
|
83
|
+
|
|
84
|
+
except httpx.HTTPError as ex:
|
|
85
|
+
log.error(f"HTTP error occurred: {ex}")
|
|
86
|
+
except Exception as ex:
|
|
87
|
+
log.warning(f"An error occured while publishing the cloud event with id '{e.id}' [attempt {retries}/{self._options.retry_attempts}]: {ex}")
|
|
88
|
+
await asyncio.sleep(self._options.retry_delay * retries)
|
|
89
|
+
if not published:
|
|
90
|
+
raise Exception(f"Failed to publish cloud events to the specified sink '{self._options.sink_uri}' after '{self._options.retry_attempts}' attempts")
|
|
91
|
+
|
|
92
|
+
@staticmethod
|
|
93
|
+
def configure(builder: ApplicationBuilderBase) -> ApplicationBuilderBase:
|
|
94
|
+
''' Registers and configures a cloud event publisher to the specified service collection.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
services (ServiceCollection): the service collection to configure
|
|
98
|
+
'''
|
|
99
|
+
options = CloudEventPublishingOptions(builder.settings.cloud_event_sink, builder.settings.cloud_event_source, builder.settings.cloud_event_type_prefix, builder.settings.cloud_event_retry_attempts, builder.settings.cloud_event_retry_delay)
|
|
100
|
+
builder.services.try_add_singleton(CloudEventBus)
|
|
101
|
+
builder.services.add_singleton(CloudEventPublishingOptions, singleton=options)
|
|
102
|
+
builder.services.add_singleton(HostedService, CloudEventPublisher)
|
|
103
|
+
return builder
|
|
File without changes
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import ast
|
|
2
|
+
from ast import And, Attribute, BoolOp, Call, Compare, Constant, Eq, Gt, GtE, In, Is, IsNot, Lambda, Lt, LtE, Name, Not, NotEq, NotIn, Or, Subscript, USub, UnaryOp, arg, boolop, cmpop, expr
|
|
3
|
+
import importlib
|
|
4
|
+
from typing import Dict, List, Type
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class JavaScriptExpressionTranslator:
|
|
8
|
+
''' Represents a service used to translate ast expressions into JavaScript expressions'''
|
|
9
|
+
|
|
10
|
+
def translate(self, expression: expr) -> str:
|
|
11
|
+
''' Translates the specified expression into a new JavaScript expression '''
|
|
12
|
+
if isinstance(expression, arg): return self._translate_arg(expression)
|
|
13
|
+
elif isinstance(expression, Attribute): return self._translate_attribute(expression)
|
|
14
|
+
elif isinstance(expression, BoolOp): return self._translate_bool_op(expression)
|
|
15
|
+
elif isinstance(expression, Call): return self._translate_call(expression)
|
|
16
|
+
elif isinstance(expression, Compare): return self._translate_compare(expression)
|
|
17
|
+
elif isinstance(expression, Constant): return self._translate_constant(expression)
|
|
18
|
+
elif isinstance(expression, Lambda): return self._translate_lambda(expression)
|
|
19
|
+
elif isinstance(expression, Name): return self._translate_name(expression)
|
|
20
|
+
elif isinstance(expression, Subscript): return self._translate_subscript(expression)
|
|
21
|
+
elif isinstance(expression, UnaryOp): return self._translate_unary_op(expression)
|
|
22
|
+
elif isinstance(expression, ast.List): return self._translate_list(expression)
|
|
23
|
+
else: raise Exception(f"The specified expression type '{type(expression)}' is not supported in this context")
|
|
24
|
+
|
|
25
|
+
def _translate_arg(self, expression: arg) -> str:
|
|
26
|
+
return expression.arg
|
|
27
|
+
|
|
28
|
+
def _translate_attribute(self, expression: Attribute) -> str:
|
|
29
|
+
attribute_name : str = None
|
|
30
|
+
attribute_path_parts = list[str]()
|
|
31
|
+
if isinstance(expression.value, Constant):
|
|
32
|
+
return self.translate(Constant(getattr(expression.value.value, expression.attr)))
|
|
33
|
+
else:
|
|
34
|
+
try:
|
|
35
|
+
attribute_name = expression.value.id
|
|
36
|
+
attribute_path_parts.append(expression.attr)
|
|
37
|
+
except:
|
|
38
|
+
while True:
|
|
39
|
+
try:
|
|
40
|
+
attribute_name = expression.value.id
|
|
41
|
+
break
|
|
42
|
+
except:
|
|
43
|
+
attribute_path_part = self.translate(expression.value)
|
|
44
|
+
attribute_path_parts.append(attribute_path_part)
|
|
45
|
+
expression = expression.value
|
|
46
|
+
#if scope.get_all_arguments().get(attribute_name) is None: return self._evaluate(expression) #todo
|
|
47
|
+
attribute_path_parts.reverse()
|
|
48
|
+
attribute_full_name = f"this.{'.'.join(attribute_path_parts)}"
|
|
49
|
+
return attribute_full_name
|
|
50
|
+
|
|
51
|
+
def _translate_bool_op(self, expression: BoolOp) -> str:
|
|
52
|
+
operator = self._translate_boolop(expression.op)
|
|
53
|
+
parameters = [ self.translate(parameter) for parameter in expression.values ]
|
|
54
|
+
return f' {operator} '.join(parameters)
|
|
55
|
+
|
|
56
|
+
def _translate_call(self, expression: Call) -> str:
|
|
57
|
+
function_name = getattr(expression.func, 'id', None)
|
|
58
|
+
function_body : str
|
|
59
|
+
function_args : List[str]
|
|
60
|
+
if function_name is None:
|
|
61
|
+
function_name = expression.func.attr
|
|
62
|
+
function_body = self.translate(expression.func.value)
|
|
63
|
+
function_args = [self.translate(arg) for arg in expression.args]
|
|
64
|
+
else:
|
|
65
|
+
function_body = self.translate(expression.args[0])
|
|
66
|
+
function_args = [self.translate(arg) for arg in expression.args[1:]]
|
|
67
|
+
if function_name == 'count': return f"{function_body}.length"
|
|
68
|
+
elif function_name == 'len': return f"{function_body}.length"
|
|
69
|
+
elif function_name == 'lower': return f"{function_body}.toLowerCase()"
|
|
70
|
+
elif function_name == 'startswith': return f"{function_body}.startsWith({function_args[0]})"
|
|
71
|
+
elif function_name == 'endswith': return f"{function_body}.endsWith({function_args[0]})"
|
|
72
|
+
elif function_name == 'index': return f"{function_body}.indexOf({', '.join(function_args)})"
|
|
73
|
+
elif function_name == 'upper': return f"{function_body}.toUpperCase()"
|
|
74
|
+
elif function_name == 'split': return f"{function_body}.split({', '.join(function_args)})"
|
|
75
|
+
else: raise Exception(f"The specified function '{function_name}' is not supported in this context")
|
|
76
|
+
|
|
77
|
+
def _translate_compare(self, expression: Compare) -> str:
|
|
78
|
+
left = self.translate(expression.left)
|
|
79
|
+
right = self.translate(expression.comparators[0])
|
|
80
|
+
operator = self._translate_cmpop(expression.ops[0])
|
|
81
|
+
if operator == 'in': return f"{right}.includes({left})"
|
|
82
|
+
elif operator == 'not in': return f"!{right}.includes({left})"
|
|
83
|
+
else: return f"{left} {operator} {right}"
|
|
84
|
+
|
|
85
|
+
def _translate_constant(self, expression: Constant) -> str:
|
|
86
|
+
if expression.value is None: return 'null'
|
|
87
|
+
if isinstance(expression.value, str): return f"'{expression.value}'"
|
|
88
|
+
else: return str(expression.value)
|
|
89
|
+
|
|
90
|
+
def _translate_lambda(self, expression: Lambda) -> str:
|
|
91
|
+
body = self.translate(expression.body)
|
|
92
|
+
return body
|
|
93
|
+
|
|
94
|
+
def _translate_list(self, expression: ast.List) -> str:
|
|
95
|
+
return f"[ {', '.join([self.translate(expression) for expression in expression.elts])} ]"
|
|
96
|
+
|
|
97
|
+
def _translate_name(self, expression: Name) -> str:
|
|
98
|
+
return "this"
|
|
99
|
+
|
|
100
|
+
def _translate_subscript(self, expression: Subscript) -> str:
|
|
101
|
+
source = self.translate(expression.value)
|
|
102
|
+
slice = self.translate(expression.slice)
|
|
103
|
+
slice_index : int = None
|
|
104
|
+
try: slice_index = int(slice)
|
|
105
|
+
except: pass
|
|
106
|
+
if slice_index is not None and slice_index < 0: return f"{source}.slice({slice})"
|
|
107
|
+
else: return f"{source}[{slice}]"
|
|
108
|
+
|
|
109
|
+
def _translate_unary_op(self, expression: UnaryOp) -> str:
|
|
110
|
+
operand = self.translate(expression.operand)
|
|
111
|
+
if isinstance(expression.op, Not): return f"!({operand})"
|
|
112
|
+
elif isinstance(expression.op, USub): return f"-{operand}"
|
|
113
|
+
else: raise Exception(f"The specified unary operator '{type(expression.op).__name__}' is not supported in this context")
|
|
114
|
+
|
|
115
|
+
def _get_lambda_arguments(self, expression: Lambda) -> Dict[str, Type]:
|
|
116
|
+
argument_name = expression.args.args[0].arg
|
|
117
|
+
argument_type_qualified_name = expression.args.args[0].annotation.id
|
|
118
|
+
module_name, _, type_name = argument_type_qualified_name.rpartition('.')
|
|
119
|
+
module = importlib.import_module(module_name)
|
|
120
|
+
argument_type = getattr(module, type_name)
|
|
121
|
+
return { argument_name: argument_type }
|
|
122
|
+
|
|
123
|
+
def _translate_boolop(self, operator : boolop) -> str:
|
|
124
|
+
switch = {
|
|
125
|
+
And: '&&',
|
|
126
|
+
Or: '||'
|
|
127
|
+
}
|
|
128
|
+
result = switch.get(type(operator))
|
|
129
|
+
if result is None: raise Exception(f"The specified bool operator '{type(operator).__name__}' is not supported")
|
|
130
|
+
return result
|
|
131
|
+
|
|
132
|
+
def _translate_cmpop(self, operator : cmpop) -> str:
|
|
133
|
+
switch = {
|
|
134
|
+
Eq: '===',
|
|
135
|
+
NotEq: '!==',
|
|
136
|
+
Lt: '<',
|
|
137
|
+
LtE: '<=',
|
|
138
|
+
Gt: '>',
|
|
139
|
+
GtE: '>=',
|
|
140
|
+
In: 'in',
|
|
141
|
+
NotIn: 'not in', # todo: 'not in' is not directly translatable to JavaScript
|
|
142
|
+
Is: '===',
|
|
143
|
+
IsNot: '!==',
|
|
144
|
+
}
|
|
145
|
+
result = switch.get(type(operator))
|
|
146
|
+
if result is None: raise Exception(f"The specified comparison operator '{type(operator).__name__}' is not supported")
|
|
147
|
+
return result
|
|
File without changes
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from abc import ABC, abstractmethod
|
|
3
|
+
from contextlib import asynccontextmanager, contextmanager
|
|
4
|
+
from typing import Callable, cast, List
|
|
5
|
+
from neuroglia.dependency_injection.service_provider import ServiceCollection, ServiceProviderBase, ServiceScopeBase
|
|
6
|
+
from pydantic_settings import BaseSettings
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class HostBase(ABC):
|
|
10
|
+
''' Defines the fundamentals of an program's abstraction '''
|
|
11
|
+
|
|
12
|
+
services: ServiceProviderBase
|
|
13
|
+
|
|
14
|
+
@abstractmethod
|
|
15
|
+
async def start_async(self):
|
|
16
|
+
''' Starts the program '''
|
|
17
|
+
raise NotImplementedError()
|
|
18
|
+
|
|
19
|
+
@abstractmethod
|
|
20
|
+
async def stop_async(self):
|
|
21
|
+
''' Attempts to gracefully stop the program '''
|
|
22
|
+
raise NotImplementedError()
|
|
23
|
+
|
|
24
|
+
def run(self):
|
|
25
|
+
''' Runs the program '''
|
|
26
|
+
try:
|
|
27
|
+
asyncio.get_running_loop()
|
|
28
|
+
asyncio.ensure_future(self.start_async())
|
|
29
|
+
except RuntimeError:
|
|
30
|
+
asyncio.run(self.start_async())
|
|
31
|
+
|
|
32
|
+
@abstractmethod
|
|
33
|
+
def dispose(self):
|
|
34
|
+
''' Disposes of the program's resources '''
|
|
35
|
+
raise NotImplementedError()
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class HostedService:
|
|
39
|
+
''' Defines the fundamentals of a service managed by the host '''
|
|
40
|
+
|
|
41
|
+
async def start_async(self):
|
|
42
|
+
''' Starts the service '''
|
|
43
|
+
pass
|
|
44
|
+
|
|
45
|
+
async def stop_async(self):
|
|
46
|
+
''' Attempts to gracefully stop the service '''
|
|
47
|
+
pass
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class HostApplicationLifetime:
|
|
51
|
+
''' Represents a service used to manage (and notify about) the application's lifetime events '''
|
|
52
|
+
|
|
53
|
+
on_application_started: List[Callable[[], None]] = list[Callable[[], None]]()
|
|
54
|
+
''' Gets a list containing the callbacks, if any, to invoke when the application has started '''
|
|
55
|
+
|
|
56
|
+
on_application_stopping: List[Callable[[], None]] = list[Callable[[], None]]()
|
|
57
|
+
''' Gets a list containing the callbacks, if any, to invoke when the application is stopping '''
|
|
58
|
+
|
|
59
|
+
on_application_stopped: List[Callable[[], None]] = list[Callable[[], None]]()
|
|
60
|
+
''' Gets a list containing the callbacks, if any, to invoke when the application has been stopped '''
|
|
61
|
+
|
|
62
|
+
def stop_application(self):
|
|
63
|
+
''' Stops the application '''
|
|
64
|
+
|
|
65
|
+
@asynccontextmanager
|
|
66
|
+
async def _run_async(self, app: HostBase):
|
|
67
|
+
await asyncio.gather(*[handler() for handler in self.on_application_started])
|
|
68
|
+
yield
|
|
69
|
+
await asyncio.gather(*[handler() for handler in self.on_application_stopping])
|
|
70
|
+
await app.stop_async()
|
|
71
|
+
await asyncio.gather(*[handler() for handler in self.on_application_stopped])
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class Host(HostBase):
|
|
75
|
+
''' Represents the default implementation of the HostBase class '''
|
|
76
|
+
|
|
77
|
+
def __init__(self, services: ServiceProviderBase):
|
|
78
|
+
self.services = services
|
|
79
|
+
|
|
80
|
+
async def start_async(self):
|
|
81
|
+
hosted_services = [cast(HostedService, service) for service in self.services.get_services(HostedService)]
|
|
82
|
+
start_tasks = [hosted_service.start_async() for hosted_service in hosted_services]
|
|
83
|
+
await asyncio.gather(*start_tasks)
|
|
84
|
+
|
|
85
|
+
async def stop_async(self):
|
|
86
|
+
hosted_services = [cast(HostedService, service) for service in self.services.get_services(HostedService)]
|
|
87
|
+
stop_tasks = [hosted_service.stop_async() for hosted_service in hosted_services]
|
|
88
|
+
await asyncio.gather(*stop_tasks)
|
|
89
|
+
|
|
90
|
+
def dispose(self):
|
|
91
|
+
if isinstance(self.services, ServiceScopeBase):
|
|
92
|
+
self.services.dispose()
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class ApplicationSettings(BaseSettings):
|
|
96
|
+
|
|
97
|
+
consumer_group: str
|
|
98
|
+
|
|
99
|
+
connection_strings: dict[str, str] = dict[str, str]()
|
|
100
|
+
|
|
101
|
+
cloud_event_sink: str
|
|
102
|
+
|
|
103
|
+
cloud_event_source: str
|
|
104
|
+
|
|
105
|
+
cloud_event_type_prefix: str
|
|
106
|
+
|
|
107
|
+
cloud_event_retry_attempts: int = 5
|
|
108
|
+
|
|
109
|
+
cloud_event_retry_delay: float = 1
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class ApplicationBuilderBase:
|
|
113
|
+
''' Defines the fundamentals of a service used to build applications '''
|
|
114
|
+
|
|
115
|
+
def __init__(self):
|
|
116
|
+
self.services.try_add_singleton(HostApplicationLifetime)
|
|
117
|
+
|
|
118
|
+
settings: ApplicationSettings = ApplicationSettings()
|
|
119
|
+
|
|
120
|
+
services: ServiceCollection = ServiceCollection()
|
|
121
|
+
|
|
122
|
+
@abstractmethod
|
|
123
|
+
def build(self) -> HostBase:
|
|
124
|
+
''' Builds the application's host '''
|
|
125
|
+
raise NotImplementedError()
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class ApplicationBuilder(ApplicationBuilderBase):
|
|
129
|
+
''' Represents the default implementation of the ApplicationBuilderBase class '''
|
|
130
|
+
|
|
131
|
+
def __init__(self):
|
|
132
|
+
super().__init__()
|
|
133
|
+
|
|
134
|
+
@contextmanager
|
|
135
|
+
def build(self) -> HostBase:
|
|
136
|
+
try:
|
|
137
|
+
service_provider = self.services.build()
|
|
138
|
+
host = Host(service_provider)
|
|
139
|
+
yield host
|
|
140
|
+
finally:
|
|
141
|
+
host.dispose()
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
from typing import Callable, List, Type
|
|
3
|
+
from neuroglia.core import TypeFinder, ModuleLoader
|
|
4
|
+
from neuroglia.data.abstractions import AggregateRoot
|
|
5
|
+
from neuroglia.data.infrastructure.event_sourcing.read_model_reconciliator import ReadModelConciliationOptions, ReadModelReconciliator
|
|
6
|
+
from neuroglia.hosting.abstractions import ApplicationBuilderBase, HostedService
|
|
7
|
+
from neuroglia.mediation.mediator import RequestHandler
|
|
8
|
+
from neuroglia.data.queries.generic import GetByIdQueryHandler, ListQueryHandler
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class DataAccessLayer:
|
|
12
|
+
|
|
13
|
+
class WriteModel:
|
|
14
|
+
''' Represents an helper class used to configure an application's Write Model DAL '''
|
|
15
|
+
|
|
16
|
+
def configure(builder: ApplicationBuilderBase, modules: List[str], repository_setup: Callable[[ApplicationBuilderBase, Type, Type], None]) -> ApplicationBuilderBase:
|
|
17
|
+
''' Configures the application's Write Model DAL, scaning for aggregate root types within the specified modules
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
builder (ApplicationBuilderBase): the application builder to configure
|
|
21
|
+
modules (List[str]): a list containing the names of the modules to scan for aggregate root types
|
|
22
|
+
repository_setup (Callable[[ApplicationBuilderBase, Type, Type], None]): a function used to setup the repository for the specified entity and key types
|
|
23
|
+
'''
|
|
24
|
+
for module in [ModuleLoader.load(module_name) for module_name in modules]:
|
|
25
|
+
for aggregate_type in TypeFinder.get_types(module, lambda cls: inspect.isclass(cls) and issubclass(cls, AggregateRoot) and not cls == AggregateRoot):
|
|
26
|
+
key_type = str # todo: reflect from DTO base type
|
|
27
|
+
repository_setup(builder, aggregate_type, key_type)
|
|
28
|
+
|
|
29
|
+
class ReadModel:
|
|
30
|
+
''' Represents an helper class used to configure an application's Read Model DAL '''
|
|
31
|
+
|
|
32
|
+
@staticmethod
|
|
33
|
+
def configure(builder: ApplicationBuilderBase, modules: List[str], repository_setup: Callable[[ApplicationBuilderBase, Type, Type], None]) -> ApplicationBuilderBase:
|
|
34
|
+
''' Configures the application's Read Model DAL, scaning for types marked with the 'queryable' decorator within the specified modules
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
builder (ApplicationBuilderBase): the application builder to configure
|
|
38
|
+
modules (List[str]): a list containing the names of the modules to scan for types decorated with 'queryable'
|
|
39
|
+
repository_setup (Callable[[ApplicationBuilderBase, Type, Type], None]): a function used to setup the repository for the specified entity and key types
|
|
40
|
+
'''
|
|
41
|
+
consumer_group = builder.settings.consumer_group
|
|
42
|
+
builder.services.add_singleton(ReadModelConciliationOptions, singleton=ReadModelConciliationOptions(consumer_group))
|
|
43
|
+
builder.services.add_singleton(HostedService, ReadModelReconciliator)
|
|
44
|
+
for module in [ModuleLoader.load(module_name) for module_name in modules]:
|
|
45
|
+
for queryable_type in TypeFinder.get_types(module, lambda cls: inspect.isclass(cls) and hasattr(cls, "__queryable__")):
|
|
46
|
+
key_type = str # todo: reflect from DTO base type
|
|
47
|
+
repository_setup(builder, queryable_type, key_type)
|
|
48
|
+
builder.services.add_transient(RequestHandler, GetByIdQueryHandler[queryable_type, key_type])
|
|
49
|
+
builder.services.add_transient(RequestHandler, ListQueryHandler[queryable_type, key_type])
|
|
50
|
+
return builder
|
neuroglia/hosting/web.py
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
from abc import abstractmethod
|
|
2
|
+
import inspect
|
|
3
|
+
from typing import List
|
|
4
|
+
from fastapi import FastAPI, Request, Response
|
|
5
|
+
from starlette.middleware.base import BaseHTTPMiddleware
|
|
6
|
+
from neuroglia.core.problem_details import ProblemDetails
|
|
7
|
+
from neuroglia.core import ModuleLoader, TypeFinder
|
|
8
|
+
from neuroglia.dependency_injection.service_provider import ServiceCollection, ServiceProviderBase
|
|
9
|
+
from neuroglia.hosting.abstractions import ApplicationBuilderBase, Host, HostApplicationLifetime, HostBase
|
|
10
|
+
from neuroglia.mvc.controller_base import ControllerBase
|
|
11
|
+
from neuroglia.serialization.json import JsonSerializer
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class WebHostBase(HostBase, FastAPI):
|
|
15
|
+
''' Defines the fundamentals of a web application's abstraction '''
|
|
16
|
+
|
|
17
|
+
def __init__(self):
|
|
18
|
+
application_lifetime: HostApplicationLifetime = self.services.get_required_service(HostApplicationLifetime)
|
|
19
|
+
FastAPI.__init__(self, lifespan=application_lifetime._run_async, docs_url="/api/docs")
|
|
20
|
+
|
|
21
|
+
def use_controllers(self):
|
|
22
|
+
''' Configures FastAPI routes for registered controller services '''
|
|
23
|
+
controller: ControllerBase
|
|
24
|
+
for controller in self.services.get_services(ControllerBase):
|
|
25
|
+
self.include_router(controller.router, prefix="/api/v1")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class WebHost(WebHostBase, Host):
|
|
29
|
+
''' Represents the default implementation of the HostBase class '''
|
|
30
|
+
|
|
31
|
+
def __init__(self, services: ServiceProviderBase):
|
|
32
|
+
Host.__init__(self, services)
|
|
33
|
+
WebHostBase.__init__(self)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class WebApplicationBuilderBase(ApplicationBuilderBase):
|
|
37
|
+
''' Defines the fundamentals of a service used to build applications '''
|
|
38
|
+
|
|
39
|
+
def __init__(self):
|
|
40
|
+
super().__init__()
|
|
41
|
+
|
|
42
|
+
def add_controllers(self, modules: List[str]) -> ServiceCollection:
|
|
43
|
+
''' Registers all API controller types, which enables automatic configuration and implicit Dependency Injection of the application's controllers (specialized router class in FastAPI) '''
|
|
44
|
+
controller_types = []
|
|
45
|
+
for module in [ModuleLoader.load(module_name) for module_name in modules]:
|
|
46
|
+
controller_types.extend(TypeFinder.get_types(module, lambda t: inspect.isclass(t) and issubclass(t, ControllerBase) and t != ControllerBase, include_sub_packages=True))
|
|
47
|
+
for controller_type in set(controller_types):
|
|
48
|
+
self.services.add_singleton(ControllerBase, controller_type)
|
|
49
|
+
return self.services
|
|
50
|
+
|
|
51
|
+
@abstractmethod
|
|
52
|
+
def build(self) -> WebHostBase:
|
|
53
|
+
''' Builds the application's host '''
|
|
54
|
+
raise NotImplementedError()
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class WebApplicationBuilder(WebApplicationBuilderBase):
|
|
58
|
+
''' Represents the default implementation of the ApplicationBuilderBase class '''
|
|
59
|
+
|
|
60
|
+
def __init__(self):
|
|
61
|
+
super().__init__()
|
|
62
|
+
|
|
63
|
+
def build(self) -> WebHostBase:
|
|
64
|
+
return WebHost(self.services.build())
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class ExceptionHandlingMiddleware(BaseHTTPMiddleware):
|
|
68
|
+
''' Represents a Startlette HTTP middleware used to catch and describe exceptions '''
|
|
69
|
+
|
|
70
|
+
def __init__(self, app, service_provider: ServiceProviderBase):
|
|
71
|
+
super().__init__(app)
|
|
72
|
+
self.service_provider = service_provider
|
|
73
|
+
self.serializer = self.service_provider.get_required_service(JsonSerializer)
|
|
74
|
+
|
|
75
|
+
service_provider: ServiceProviderBase
|
|
76
|
+
''' Gets the current service provider '''
|
|
77
|
+
|
|
78
|
+
serializer: JsonSerializer
|
|
79
|
+
''' Gets the service used to serialize/deserialize values to/from JSON '''
|
|
80
|
+
|
|
81
|
+
async def dispatch(self, request: Request, call_next):
|
|
82
|
+
try:
|
|
83
|
+
return await call_next(request)
|
|
84
|
+
except Exception as ex:
|
|
85
|
+
problem_details = ProblemDetails("Internal Server Error", 500, str(ex), "https://www.w3.org/Protocols/HTTP/HTRESP.html#:~:text=Internal%20Error%20500")
|
|
86
|
+
response_content = self.serializer.serialize_to_text(problem_details)
|
|
87
|
+
return Response(response_content, 500, media_type="application/json")
|
|
File without changes
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from abc import ABC
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from typing import Generic, TypeVar
|
|
5
|
+
|
|
6
|
+
TKey = TypeVar("TKey")
|
|
7
|
+
''' Represents the generic argument used to specify the type of key to use '''
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class IntegrationEvent(Generic[TKey], ABC):
|
|
12
|
+
''' Represents the base class inherited by all integration events '''
|
|
13
|
+
|
|
14
|
+
created_at: datetime
|
|
15
|
+
''' Gets the date and time the integration event has been created at '''
|
|
16
|
+
|
|
17
|
+
aggregate_id: TKey
|
|
18
|
+
''' Gets the id of the aggregate that has produced the integration event '''
|