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