neuroglia-python 0.6.11__tar.gz → 0.7.8__tar.gz

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 (126) hide show
  1. {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/PKG-INFO +39 -38
  2. neuroglia_python-0.7.8/pyproject.toml +97 -0
  3. {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/__init__.py +1 -1
  4. {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/data/infrastructure/event_sourcing/__init__.py +12 -6
  5. {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/data/infrastructure/event_sourcing/abstractions.py +61 -2
  6. neuroglia_python-0.7.8/src/neuroglia/data/infrastructure/event_sourcing/event_sourcing_repository.py +278 -0
  7. neuroglia_python-0.7.8/src/neuroglia/data/infrastructure/event_sourcing/event_store/event_store.py +383 -0
  8. neuroglia_python-0.7.8/src/neuroglia/data/infrastructure/event_sourcing/read_model_reconciliator.py +328 -0
  9. {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/data/infrastructure/mongo/__init__.py +5 -0
  10. {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/data/infrastructure/mongo/mongo_repository.py +15 -7
  11. neuroglia_python-0.7.8/src/neuroglia/data/infrastructure/mongo/motor_query.py +399 -0
  12. {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/data/infrastructure/mongo/motor_repository.py +166 -20
  13. {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/data/queryable.py +69 -14
  14. neuroglia_python-0.7.8/src/neuroglia/expressions/javascript_expression_translator.py +207 -0
  15. neuroglia_python-0.7.8/src/neuroglia/hosting/configuration/data_access_layer.py +642 -0
  16. {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/serialization/json.py +114 -8
  17. neuroglia_python-0.6.11/pyproject.toml +0 -97
  18. neuroglia_python-0.6.11/src/neuroglia/data/infrastructure/event_sourcing/event_sourcing_repository.py +0 -89
  19. neuroglia_python-0.6.11/src/neuroglia/data/infrastructure/event_sourcing/event_store/event_store.py +0 -237
  20. neuroglia_python-0.6.11/src/neuroglia/data/infrastructure/event_sourcing/read_model_reconciliator.py +0 -62
  21. neuroglia_python-0.6.11/src/neuroglia/expressions/javascript_expression_translator.py +0 -147
  22. neuroglia_python-0.6.11/src/neuroglia/hosting/configuration/data_access_layer.py +0 -57
  23. {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/LICENSE +0 -0
  24. {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/README.md +0 -0
  25. {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/application/__init__.py +0 -0
  26. {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/application/background_scheduler.py +0 -0
  27. {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/coordination/__init__.py +0 -0
  28. {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/coordination/abstractions.py +0 -0
  29. {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/coordination/leader_election.py +0 -0
  30. {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/coordination/redis_backend.py +0 -0
  31. {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/core/__init__.py +0 -0
  32. {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/core/module_loader.py +0 -0
  33. {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/core/operation_result.py +0 -0
  34. {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/core/problem_details.py +0 -0
  35. {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/core/type_extensions.py +0 -0
  36. {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/core/type_finder.py +0 -0
  37. {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/core/type_registry.py +0 -0
  38. {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/data/__init__.py +0 -0
  39. {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/data/abstractions.py +0 -0
  40. {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/data/exceptions.py +0 -0
  41. {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/data/infrastructure/__init__.py +0 -0
  42. {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/data/infrastructure/abstractions.py +0 -0
  43. {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/data/infrastructure/event_sourcing/event_store/__init__.py +0 -0
  44. {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/data/infrastructure/filesystem/__init__.py +0 -0
  45. {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/data/infrastructure/filesystem/filesystem_repository.py +0 -0
  46. {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/data/infrastructure/memory/__init__.py +0 -0
  47. {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/data/infrastructure/memory/memory_repository.py +0 -0
  48. {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/data/infrastructure/mongo/enhanced_mongo_repository.py +0 -0
  49. {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/data/infrastructure/mongo/serialization_helper.py +0 -0
  50. {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/data/infrastructure/mongo/typed_mongo_query.py +0 -0
  51. {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/data/infrastructure/resources/__init__.py +0 -0
  52. {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/data/infrastructure/resources/in_memory_storage_backend.py +0 -0
  53. {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/data/infrastructure/resources/postgres_resource_store.py +0 -0
  54. {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/data/infrastructure/resources/redis_resource_store.py +0 -0
  55. {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/data/infrastructure/resources/resource_repository.py +0 -0
  56. {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/data/infrastructure/tracing_mixin.py +0 -0
  57. {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/data/queries/generic/__init__.py +0 -0
  58. {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/data/queries/generic/get_by_id_query.py +0 -0
  59. {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/data/queries/generic/list_query.py +0 -0
  60. {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/data/resources/__init__.py +0 -0
  61. {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/data/resources/abstractions.py +0 -0
  62. {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/data/resources/controller.py +0 -0
  63. {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/data/resources/serializers/__init__.py +0 -0
  64. {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/data/resources/serializers/xml_serializer.py +0 -0
  65. {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/data/resources/serializers/yaml_serializer.py +0 -0
  66. {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/data/resources/state_machine.py +0 -0
  67. {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/data/resources/watcher.py +0 -0
  68. {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/data/unit_of_work.py +0 -0
  69. {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/dependency_injection/__init__.py +0 -0
  70. {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/dependency_injection/service_provider.py +0 -0
  71. {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/dependency_injection/service_provider_backup.py +0 -0
  72. {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/eventing/__init__.py +0 -0
  73. {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/eventing/cloud_events/__init__.py +0 -0
  74. {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/eventing/cloud_events/cloud_event.py +0 -0
  75. {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/eventing/cloud_events/decorators.py +0 -0
  76. {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/eventing/cloud_events/infrastructure/__init__.py +0 -0
  77. {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/eventing/cloud_events/infrastructure/cloud_event_bus.py +0 -0
  78. {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/eventing/cloud_events/infrastructure/cloud_event_ingestor.py +0 -0
  79. {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/eventing/cloud_events/infrastructure/cloud_event_middleware.py +0 -0
  80. {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/eventing/cloud_events/infrastructure/cloud_event_publisher.py +0 -0
  81. {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/eventing/tracing.py +0 -0
  82. {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/expressions/__init__.py +0 -0
  83. {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/extensions/cqrs_metrics_extensions.py +0 -0
  84. {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/extensions/mediator_extensions.py +0 -0
  85. {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/extensions/state_persistence_extensions.py +0 -0
  86. {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/hosting/__init__.py +0 -0
  87. {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/hosting/abstractions.py +0 -0
  88. {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/hosting/web.py +0 -0
  89. {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/integration/__init__.py +0 -0
  90. {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/integration/cache_repository.py +0 -0
  91. {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/integration/http_service_client.py +0 -0
  92. {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/integration/models.py +0 -0
  93. {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/logging/__init__.py +0 -0
  94. {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/logging/logger.py +0 -0
  95. {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/mapping/__init__.py +0 -0
  96. {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/mapping/mapper.py +0 -0
  97. {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/mediation/__init__.py +0 -0
  98. {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/mediation/behaviors/domain_event_cloudevent_behavior.py +0 -0
  99. {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/mediation/behaviors/domain_event_dispatching_middleware.py +0 -0
  100. {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/mediation/mediator.py +0 -0
  101. {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/mediation/metrics_middleware.py +0 -0
  102. {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/mediation/pipeline_behavior.py +0 -0
  103. {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/mediation/simple.py +0 -0
  104. {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/mediation/tracing_middleware.py +0 -0
  105. {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/mvc/__init__.py +0 -0
  106. {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/mvc/controller_base.py +0 -0
  107. {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/observability/__init__.py +0 -0
  108. {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/observability/framework.py +0 -0
  109. {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/observability/logging.py +0 -0
  110. {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/observability/metrics.py +0 -0
  111. {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/observability/otel_sdk.py +0 -0
  112. {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/observability/settings.py +0 -0
  113. {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/observability/tracing.py +0 -0
  114. {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/py.typed +0 -0
  115. {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/reactive/__init__.py +0 -0
  116. {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/reactive/rx_async.py +0 -0
  117. {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/serialization/__init__.py +0 -0
  118. {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/serialization/abstractions.py +0 -0
  119. {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/serialization/aggregate_serializer.py +0 -0
  120. {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/utils/__init__.py +0 -0
  121. {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/utils/camel_model.py +0 -0
  122. {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/utils/case_conversion.py +0 -0
  123. {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/validation/__init__.py +0 -0
  124. {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/validation/business_rules.py +0 -0
  125. {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/validation/exceptions.py +0 -0
  126. {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/validation/validators.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: neuroglia-python
3
- Version: 0.6.11
3
+ Version: 0.7.8
4
4
  Summary: Port from .NET to Python of the Neuroglia Framework
5
5
  License: Apache
6
6
  License-File: LICENSE
@@ -21,43 +21,44 @@ Provides-Extra: etcd
21
21
  Provides-Extra: eventstore
22
22
  Provides-Extra: mongodb
23
23
  Provides-Extra: redis
24
- Requires-Dist: annotated-types (>=0.7.0,<0.8.0)
25
- Requires-Dist: apscheduler (>=3.11.0,<4.0.0)
26
- Requires-Dist: boto3 (>=1.40.64,<2.0.0) ; extra == "aws" or extra == "all"
27
- Requires-Dist: classy-fastapi (>=0.6.1,<0.7.0)
28
- Requires-Dist: email-validator (>=2.2.0,<3.0.0)
29
- Requires-Dist: esdbclient (>=1.1.1,<2.0.0) ; extra == "eventstore" or extra == "all"
30
- Requires-Dist: etcd3-py (>=0.1.6,<0.2.0) ; extra == "etcd" or extra == "all"
31
- Requires-Dist: fastapi (>=0.115.5,<0.116.0)
32
- Requires-Dist: grpcio (>=1.68.1,<2.0.0)
33
- Requires-Dist: httpx (>=0.27.2,<0.28.0)
34
- Requires-Dist: itsdangerous (>=2.2.0,<3.0.0)
35
- Requires-Dist: jinja2 (>=3.1.4,<4.0.0)
36
- Requires-Dist: motor (>=3.6.0,<4.0.0) ; extra == "mongodb" or extra == "all"
37
- Requires-Dist: multipledispatch (>=1.0.0,<2.0.0)
38
- Requires-Dist: opentelemetry-api (>=1.28.2,<2.0.0)
39
- Requires-Dist: opentelemetry-exporter-otlp-proto-grpc (>=1.28.2,<2.0.0)
40
- Requires-Dist: opentelemetry-exporter-otlp-proto-http (>=1.28.2,<2.0.0)
41
- Requires-Dist: opentelemetry-exporter-prometheus (>=0.49b2,<0.50)
42
- Requires-Dist: opentelemetry-instrumentation (>=0.49b2,<0.50)
43
- Requires-Dist: opentelemetry-instrumentation-fastapi (>=0.49b2,<0.50)
44
- Requires-Dist: opentelemetry-instrumentation-httpx (>=0.49b2,<0.50)
45
- Requires-Dist: opentelemetry-instrumentation-logging (>=0.49b2,<0.50)
46
- Requires-Dist: opentelemetry-instrumentation-system-metrics (>=0.49b2,<0.50)
47
- Requires-Dist: opentelemetry-sdk (>=1.28.2,<2.0.0)
48
- Requires-Dist: passlib[bcrypt] (>=1.7.4,<2.0.0)
49
- Requires-Dist: prometheus-client (>=0.21.0,<0.22.0)
50
- Requires-Dist: protobuf (>=5.0,<6.0) ; extra == "etcd" or extra == "all"
51
- Requires-Dist: pydantic-settings (>=2.6.1,<3.0.0)
52
- Requires-Dist: pydantic[email] (>=2.10.3,<3.0.0)
53
- Requires-Dist: pyjwt (>=2.10.1,<3.0.0)
54
- Requires-Dist: pymongo (>=4.10.1,<5.0.0) ; extra == "mongodb" or extra == "all"
55
- Requires-Dist: python-dotenv (>=1.0.1,<2.0.0)
56
- Requires-Dist: python-multipart (>=0.0.17,<0.0.18)
57
- Requires-Dist: redis (>=5.2.0,<6.0.0) ; extra == "redis" or extra == "all"
58
- Requires-Dist: rx (>=3.2.0,<4.0.0)
59
- Requires-Dist: typing-extensions (>=4.12.2,<5.0.0)
60
- Requires-Dist: uvicorn (>=0.32.1,<0.33.0)
24
+ Requires-Dist: annotated-types (==0.7.0)
25
+ Requires-Dist: apscheduler (==3.11.0)
26
+ Requires-Dist: boto3 (==1.40.64) ; extra == "aws" or extra == "all"
27
+ Requires-Dist: classy-fastapi (==0.6.1)
28
+ Requires-Dist: email-validator (==2.2.0)
29
+ Requires-Dist: etcd3-py (==0.1.6) ; extra == "etcd" or extra == "all"
30
+ Requires-Dist: fastapi (==0.115.5)
31
+ Requires-Dist: grpcio (==1.76.0)
32
+ Requires-Dist: h11 (==0.16.0)
33
+ Requires-Dist: httpx (==0.27.2)
34
+ Requires-Dist: itsdangerous (==2.2.0)
35
+ Requires-Dist: jinja2 (==3.1.4)
36
+ Requires-Dist: kurrentdbclient (==1.1.2) ; extra == "eventstore" or extra == "all"
37
+ Requires-Dist: motor (==3.7.0) ; extra == "mongodb" or extra == "all"
38
+ Requires-Dist: multipledispatch (==1.0.0)
39
+ Requires-Dist: opentelemetry-api (==1.38.0)
40
+ Requires-Dist: opentelemetry-exporter-otlp-proto-grpc (==1.38.0)
41
+ Requires-Dist: opentelemetry-exporter-otlp-proto-http (==1.38.0)
42
+ Requires-Dist: opentelemetry-exporter-prometheus (==0.59b0)
43
+ Requires-Dist: opentelemetry-instrumentation (==0.59b0)
44
+ Requires-Dist: opentelemetry-instrumentation-fastapi (==0.59b0)
45
+ Requires-Dist: opentelemetry-instrumentation-httpx (==0.59b0)
46
+ Requires-Dist: opentelemetry-instrumentation-logging (==0.59b0)
47
+ Requires-Dist: opentelemetry-instrumentation-system-metrics (==0.59b0)
48
+ Requires-Dist: opentelemetry-sdk (==1.38.0)
49
+ Requires-Dist: passlib[bcrypt] (==1.7.4)
50
+ Requires-Dist: prometheus-client (==0.21.0)
51
+ Requires-Dist: protobuf (==6.33.1) ; extra == "etcd" or extra == "all"
52
+ Requires-Dist: pydantic-settings (==2.6.1)
53
+ Requires-Dist: pydantic[email] (==2.10.3)
54
+ Requires-Dist: pyjwt (==2.10.1)
55
+ Requires-Dist: pymongo (==4.15.4) ; extra == "mongodb" or extra == "all"
56
+ Requires-Dist: python-dotenv (==1.0.1)
57
+ Requires-Dist: python-multipart (==0.0.17)
58
+ Requires-Dist: redis (==5.2.0) ; extra == "redis" or extra == "all"
59
+ Requires-Dist: rx (==3.2.0)
60
+ Requires-Dist: typing-extensions (==4.12.2)
61
+ Requires-Dist: uvicorn (==0.32.1)
61
62
  Description-Content-Type: text/markdown
62
63
 
63
64
  # Neuroglia Python Framework
@@ -0,0 +1,97 @@
1
+ [tool.poetry]
2
+ name = "neuroglia-python"
3
+ version = "0.7.8"
4
+ description = "Port from .NET to Python of the Neuroglia Framework"
5
+ authors = ["Charles d'Avernas <charles.davernas@neuroglia.io>", "Bruno van de Werve <bvandewe@gmail.com>"]
6
+ license = "Apache"
7
+ readme = "README.md"
8
+ packages = [
9
+ { include = "neuroglia", from = "src" },
10
+ ]
11
+ package-mode = true
12
+
13
+ [tool.poetry.dependencies]
14
+ python = "^3.9"
15
+ # Core framework dependencies - always required
16
+ annotated-types = "0.7.0"
17
+ classy-fastapi = "0.6.1"
18
+ fastapi = "0.115.5"
19
+ multipledispatch = "1.0.0"
20
+ pydantic-settings = "2.6.1"
21
+ python-dotenv = "1.0.1"
22
+ typing-extensions = "4.12.2"
23
+ rx = { version = "3.2.0" }
24
+ uvicorn = { version = "0.32.1" }
25
+ httpx = { version = "0.27.2" }
26
+ grpcio = { version = "1.76.0" }
27
+ h11 = { version = "0.16.0" } # Security fix for CVE-2025-43859
28
+
29
+ # Authentication dependencies
30
+ pyjwt = "2.10.1"
31
+ passlib = { extras = ["bcrypt"], version = "1.7.4" }
32
+ python-multipart = "0.0.17"
33
+ itsdangerous = "2.2.0"
34
+
35
+ # Core dependencies
36
+ protobuf = "6.33.1" # Pinned for compatibility with kurrentdbclient, OpenTelemetry, and etcd3-py
37
+ apscheduler = "3.11.0"
38
+ pydantic = {extras = ["email"], version = "2.10.3"}
39
+ email-validator = "2.2.0"
40
+ jinja2 = "3.1.4"
41
+
42
+ # Optional dependencies - install with extras
43
+ redis = { version = "5.2.0", optional = true }
44
+ pymongo = { version = "4.15.4", optional = true }
45
+ motor = { version = "3.7.0", optional = true }
46
+ kurrentdbclient = "1.1.2"
47
+ etcd3-py = { version = "0.1.6", optional = true } # Maintained fork with protobuf 5.x/6.x support
48
+
49
+ # OpenTelemetry dependencies - pinned versions for protobuf 6.x compatibility
50
+ opentelemetry-api = "1.38.0"
51
+ opentelemetry-sdk = "1.38.0"
52
+ opentelemetry-exporter-otlp-proto-grpc = "1.38.0"
53
+ opentelemetry-exporter-otlp-proto-http = "1.38.0"
54
+ opentelemetry-instrumentation = "0.59b0"
55
+ opentelemetry-instrumentation-fastapi = "0.59b0"
56
+ opentelemetry-instrumentation-httpx = "0.59b0"
57
+ opentelemetry-instrumentation-logging = "0.59b0"
58
+ opentelemetry-instrumentation-system-metrics = "0.59b0"
59
+ opentelemetry-exporter-prometheus = "0.59b0" # Prometheus /metrics endpoint support
60
+ prometheus-client = "0.21.0" # Required by opentelemetry-exporter-prometheus
61
+ boto3 = { version = "1.40.64", optional = true }
62
+
63
+ [tool.poetry.extras]
64
+ mongodb = ["pymongo", "motor"]
65
+ eventstore = ["kurrentdbclient"] # Formerly esdbclient
66
+ redis = ["redis"]
67
+ etcd = ["etcd3-py", "protobuf"]
68
+ aws = ["boto3"]
69
+ all = ["pymongo", "motor", "kurrentdbclient", "redis", "etcd3-py", "protobuf", "boto3"]
70
+
71
+ [tool.poetry.group.dev.dependencies]
72
+ # Development and testing dependencies
73
+ pre-commit = "^4.0.1"
74
+ pytest = "^8.3.3"
75
+ pytest-asyncio = "^0.24.0"
76
+ mypy = "^1.13.0"
77
+ autopep8 = "^2.3.1"
78
+ coverage = "^7.6.9"
79
+ mypy-extensions = "^1.0.0"
80
+ flake8 = "^7.1.1"
81
+ isort = "^5.13.2"
82
+
83
+ [tool.poetry.group.docs]
84
+ optional = true
85
+
86
+ [tool.poetry.group.docs.dependencies]
87
+ # Documentation dependencies
88
+ mkdocs = "^1.6.1"
89
+ mkdocs-material = "^9.5.48"
90
+ mkdocs-mermaid2-plugin = "^1.2.2"
91
+
92
+ [build-system]
93
+ requires = ["poetry-core"]
94
+ build-backend = "poetry.core.masonry.api"
95
+
96
+ [tool.black]
97
+ line-length = 500
@@ -62,7 +62,7 @@ __all__ = [
62
62
  ]
63
63
 
64
64
  # Framework metadata
65
- __version__ = "0.6.11"
65
+ __version__ = "0.7.8"
66
66
  __author__ = "Neuroglia Team"
67
67
  __email__ = "team@neuroglia.io"
68
68
  __license__ = "Apache"
@@ -5,23 +5,29 @@ Provides event store implementations and aggregate root support.
5
5
  """
6
6
 
7
7
  from .abstractions import (
8
- EventStore,
9
- EventRecord,
8
+ Aggregator,
10
9
  EventDescriptor,
10
+ EventRecord,
11
+ EventStore,
12
+ EventStoreOptions,
11
13
  StreamDescriptor,
12
14
  StreamReadDirection,
13
- EventStoreOptions,
14
- Aggregator
15
15
  )
16
16
  from .event_sourcing_repository import EventSourcingRepository
17
+ from .read_model_reconciliator import (
18
+ ReadModelConciliationOptions,
19
+ ReadModelReconciliator,
20
+ )
17
21
 
18
22
  __all__ = [
19
23
  "EventStore",
20
24
  "EventSourcingRepository",
21
25
  "EventRecord",
22
- "EventDescriptor",
26
+ "EventDescriptor",
23
27
  "StreamDescriptor",
24
28
  "StreamReadDirection",
25
29
  "EventStoreOptions",
26
30
  "Aggregator",
27
- ]
31
+ "ReadModelConciliationOptions",
32
+ "ReadModelReconciliator",
33
+ ]
@@ -8,6 +8,34 @@ from typing import Any, Optional
8
8
  from neuroglia.data.abstractions import AggregateRoot
9
9
 
10
10
 
11
+ class DeleteMode(Enum):
12
+ """Specifies how deletion should be handled in event-sourced repositories."""
13
+
14
+ DISABLED = "disabled"
15
+ """Deletion is not allowed (default behavior). Raises NotImplementedError."""
16
+
17
+ SOFT = "soft"
18
+ """
19
+ Soft delete by calling the aggregate's deletion method.
20
+
21
+ The repository will:
22
+ 1. Load the aggregate
23
+ 2. Call aggregate.mark_as_deleted() or aggregate.mark_deleted()
24
+ 3. Persist the deletion event via update_async()
25
+
26
+ The aggregate controls deletion semantics and domain events.
27
+ Event stream is preserved for audit and potential restoration.
28
+ """
29
+
30
+ HARD = "hard"
31
+ """
32
+ Physically delete the entire event stream from the event store.
33
+
34
+ WARNING: This is irreversible and removes all history for this aggregate.
35
+ Use for GDPR compliance (right to be forgotten) or data cleanup.
36
+ """
37
+
38
+
11
39
  @dataclass
12
40
  class StreamDescriptor:
13
41
  """Represents a class used to describe a stream of recorded events"""
@@ -81,11 +109,11 @@ class AckableEventRecord(EventRecord):
81
109
 
82
110
  async def ack_async(self) -> None:
83
111
  """Acks the event record"""
84
- self._ack_delegate()
112
+ await self._ack_delegate()
85
113
 
86
114
  async def nack_async(self) -> None:
87
115
  """Nacks the event record"""
88
- self._nack_delegate()
116
+ await self._nack_delegate()
89
117
 
90
118
 
91
119
  class StreamReadDirection(Enum):
@@ -155,11 +183,42 @@ class EventStore(ABC):
155
183
  """
156
184
  raise NotImplementedError()
157
185
 
186
+ @abstractmethod
187
+ async def delete_async(self, stream_id: str) -> None:
188
+ """
189
+ Deletes the entire event stream.
190
+
191
+ This operation is irreversible and should be used with caution.
192
+ Useful for GDPR compliance (right to be forgotten) or data cleanup.
193
+
194
+ Args:
195
+ stream_id: The identifier of the stream to delete
196
+
197
+ Raises:
198
+ Exception: If the stream does not exist or deletion fails
199
+ """
200
+ raise NotImplementedError()
201
+
158
202
 
159
203
  class Aggregator:
160
204
  def aggregate(self, events: list, aggregate_type: type):
205
+ """
206
+ Reconstitutes an aggregate from a list of domain events.
207
+
208
+ Creates a new aggregate instance using __new__() to bypass __init__,
209
+ then manually initializes the state and _pending_events list before
210
+ replaying events to rebuild the aggregate's state.
211
+
212
+ Args:
213
+ events: List of event records to replay
214
+ aggregate_type: The aggregate root class to instantiate
215
+
216
+ Returns:
217
+ Reconstituted aggregate with replayed state
218
+ """
161
219
  aggregate: AggregateRoot = object.__new__(aggregate_type)
162
220
  aggregate.state = aggregate.__orig_bases__[0].__args__[0]()
221
+ aggregate._pending_events = list() # Initialize _pending_events to prevent AttributeError
163
222
  for e in events:
164
223
  aggregate.state.on(e.data)
165
224
  aggregate.state.state_version = e.data.aggregate_version
@@ -0,0 +1,278 @@
1
+ from dataclasses import dataclass, field
2
+ from typing import TYPE_CHECKING, Generic, Optional
3
+
4
+ from neuroglia.data.abstractions import DomainEvent, TAggregate, TKey
5
+ from neuroglia.data.infrastructure.abstractions import Repository
6
+ from neuroglia.data.infrastructure.event_sourcing.abstractions import (
7
+ Aggregator,
8
+ DeleteMode,
9
+ EventDescriptor,
10
+ EventStore,
11
+ StreamReadDirection,
12
+ )
13
+ from neuroglia.hosting.abstractions import ApplicationBuilderBase
14
+
15
+ if TYPE_CHECKING:
16
+ from neuroglia.mediation.mediator import Mediator
17
+
18
+
19
+ @dataclass
20
+ class EventSourcingRepositoryOptions(Generic[TAggregate, TKey]):
21
+ """
22
+ Configuration options for EventSourcingRepository.
23
+
24
+ Attributes:
25
+ delete_mode: Specifies how deletion should be handled (DISABLED, SOFT, HARD)
26
+ soft_delete_method_name: Method name to call on aggregate for soft delete
27
+
28
+ Examples:
29
+ ```python
30
+ # Disable deletion (default)
31
+ options = EventSourcingRepositoryOptions[Task, str]()
32
+
33
+ # Enable soft delete
34
+ options = EventSourcingRepositoryOptions[Task, str](
35
+ delete_mode=DeleteMode.SOFT
36
+ )
37
+
38
+ # Enable hard delete (GDPR compliance)
39
+ options = EventSourcingRepositoryOptions[Task, str](
40
+ delete_mode=DeleteMode.HARD
41
+ )
42
+ ```
43
+ """
44
+
45
+ delete_mode: DeleteMode = field(default=DeleteMode.DISABLED)
46
+ """
47
+ Specifies how deletion should be handled:
48
+ - DISABLED: Raises NotImplementedError (default, event sourcing best practice)
49
+ - SOFT: Calls aggregate.mark_as_deleted() or aggregate.mark_deleted() then persists
50
+ - HARD: Physically deletes the event stream (use with caution)
51
+ """
52
+
53
+ soft_delete_method_name: str = field(default="mark_as_deleted")
54
+ """
55
+ Method name to call on aggregate for soft delete.
56
+ Defaults to 'mark_as_deleted'. Alternative: 'mark_deleted'.
57
+ Only used when delete_mode is SOFT.
58
+ """
59
+
60
+
61
+ class EventSourcingRepository(Generic[TAggregate, TKey], Repository[TAggregate, TKey]):
62
+ """
63
+ Event sourcing repository implementation with configurable deletion strategies.
64
+
65
+ Supports three deletion modes:
66
+ - DISABLED: No deletion allowed (default, follows event sourcing best practices)
67
+ - SOFT: Delegates to aggregate's deletion method, preserving event history
68
+ - HARD: Physical stream deletion for GDPR compliance or data cleanup
69
+
70
+ Examples:
71
+ ```python
72
+ # Default configuration (deletion disabled)
73
+ repo = EventSourcingRepository[Task, str](eventstore, aggregator)
74
+
75
+ # With soft delete enabled
76
+ options = EventSourcingRepositoryOptions[Task, str](
77
+ delete_mode=DeleteMode.SOFT
78
+ )
79
+ repo = EventSourcingRepository[Task, str](
80
+ eventstore, aggregator, options=options
81
+ )
82
+
83
+ # With hard delete for GDPR compliance
84
+ options = EventSourcingRepositoryOptions[Task, str](
85
+ delete_mode=DeleteMode.HARD
86
+ )
87
+ repo = EventSourcingRepository[Task, str](
88
+ eventstore, aggregator, options=options
89
+ )
90
+ ```
91
+ """
92
+
93
+ def __init__(
94
+ self,
95
+ eventstore: EventStore,
96
+ aggregator: Aggregator,
97
+ mediator: Optional["Mediator"] = None,
98
+ options: Optional[EventSourcingRepositoryOptions[TAggregate, TKey]] = None,
99
+ ):
100
+ """Initialize a new event sourcing repository"""
101
+ super().__init__(mediator) # Pass mediator to base class for event publishing
102
+ self._eventstore = eventstore
103
+ self._aggregator = aggregator
104
+ self._options = options or EventSourcingRepositoryOptions[TAggregate, TKey]()
105
+
106
+ _eventstore: EventStore
107
+ """ Gets the underlying event store """
108
+
109
+ _aggregator: Aggregator
110
+ """ Gets the underlying event store """
111
+
112
+ async def contains_async(self, id: TKey) -> bool:
113
+ return self._eventstore.contains_stream(self._build_stream_id_for(id))
114
+
115
+ async def get_async(self, id: TKey) -> Optional[TAggregate]:
116
+ """
117
+ Gets the aggregate with the specified id, if any.
118
+
119
+ Returns None if the stream does not exist.
120
+ """
121
+ stream_id = self._build_stream_id_for(id)
122
+ try:
123
+ events = await self._eventstore.read_async(stream_id, StreamReadDirection.FORWARDS, 0)
124
+ if not events:
125
+ return None
126
+ return self._aggregator.aggregate(events, self.__orig_class__.__args__[0])
127
+ except Exception:
128
+ # If stream doesn't exist or any other error occurs, return None
129
+ return None
130
+
131
+ async def _do_add_async(self, aggregate: TAggregate) -> TAggregate:
132
+ """Adds and persists the specified aggregate"""
133
+ stream_id = self._build_stream_id_for(aggregate.id())
134
+ events = aggregate._pending_events
135
+ if len(events) < 1:
136
+ raise Exception("No pending events to persist")
137
+ encoded_events = [self._encode_event(e) for e in events]
138
+ await self._eventstore.append_async(stream_id, encoded_events)
139
+ aggregate.state.state_version = events[-1].aggregate_version
140
+ # DON'T clear pending events here - let base class do it after publishing!
141
+ # aggregate.clear_pending_events()
142
+ return aggregate
143
+
144
+ async def _do_update_async(self, aggregate: TAggregate) -> TAggregate:
145
+ """Persists the changes made to the specified aggregate"""
146
+ stream_id = self._build_stream_id_for(aggregate.id())
147
+ events = aggregate._pending_events
148
+ if len(events) < 1:
149
+ raise Exception("No pending events to persist")
150
+ encoded_events = [self._encode_event(e) for e in events]
151
+ await self._eventstore.append_async(stream_id, encoded_events, aggregate.state.state_version)
152
+ aggregate.state.state_version = events[-1].aggregate_version
153
+ # DON'T clear pending events here - let base class do it after publishing!
154
+ # aggregate.clear_pending_events()
155
+ return aggregate
156
+
157
+ async def _do_remove_async(self, id: TKey) -> None:
158
+ """
159
+ Removes the aggregate root with the specified key based on configured delete mode.
160
+
161
+ Behavior depends on delete_mode configuration:
162
+ - DISABLED: Raises NotImplementedError (default)
163
+ - SOFT: Loads aggregate, calls mark_as_deleted(), persists deletion event
164
+ - HARD: Physically deletes the event stream (irreversible)
165
+
166
+ Args:
167
+ id: The identifier of the aggregate to remove
168
+
169
+ Raises:
170
+ NotImplementedError: When delete_mode is DISABLED
171
+ ValueError: When SOFT delete but aggregate lacks deletion method
172
+ Exception: When aggregate not found or deletion fails
173
+ """
174
+ match self._options.delete_mode:
175
+ case DeleteMode.DISABLED:
176
+ raise NotImplementedError("Deletion is disabled for this repository. " "Event sourcing repositories preserve immutable history by default. " "Configure delete_mode=DeleteMode.SOFT for soft delete " "or delete_mode=DeleteMode.HARD for physical stream deletion.")
177
+
178
+ case DeleteMode.SOFT:
179
+ await self._soft_delete_async(id)
180
+
181
+ case DeleteMode.HARD:
182
+ await self._hard_delete_async(id)
183
+
184
+ async def _soft_delete_async(self, id: TKey) -> None:
185
+ """
186
+ Soft delete by calling aggregate's deletion method.
187
+
188
+ The repository will:
189
+ 1. Load the aggregate via get_async()
190
+ 2. Call aggregate's deletion method (mark_as_deleted or mark_deleted)
191
+ 3. Persist the deletion event via _do_update_async()
192
+
193
+ This preserves event history while marking the aggregate as deleted.
194
+ The aggregate controls deletion semantics and domain events.
195
+
196
+ Args:
197
+ id: The identifier of the aggregate to soft delete
198
+
199
+ Raises:
200
+ Exception: If aggregate not found
201
+ ValueError: If aggregate lacks required deletion method
202
+ """
203
+ # Load the aggregate
204
+ aggregate = await self.get_async(id)
205
+ if aggregate is None:
206
+ raise Exception(f"Aggregate with id '{id}' not found")
207
+
208
+ # Call the aggregate's deletion method (convention-based)
209
+ method_name = self._options.soft_delete_method_name
210
+ if hasattr(aggregate, method_name) and callable(getattr(aggregate, method_name)):
211
+ deletion_method = getattr(aggregate, method_name)
212
+ deletion_method()
213
+ elif hasattr(aggregate, "mark_deleted") and callable(getattr(aggregate, "mark_deleted")):
214
+ # Fallback to alternative method name
215
+ aggregate.mark_deleted()
216
+ else:
217
+ aggregate_type = type(aggregate).__name__
218
+ raise ValueError(f"Aggregate {aggregate_type} does not have a '{method_name}()' or 'mark_deleted()' method. " f"Soft delete requires the aggregate to implement deletion logic. " f"Add a method like: def {method_name}(self) -> None: ...")
219
+
220
+ # Persist the deletion event via update
221
+ await self._do_update_async(aggregate)
222
+
223
+ async def _hard_delete_async(self, id: TKey) -> None:
224
+ """
225
+ Physically delete the entire event stream from the event store.
226
+
227
+ WARNING: This is irreversible and removes all history for this aggregate.
228
+ Use for:
229
+ - GDPR compliance (right to be forgotten)
230
+ - Data cleanup after retention period
231
+ - Removing test/invalid data
232
+
233
+ Args:
234
+ id: The identifier of the aggregate to hard delete
235
+
236
+ Raises:
237
+ Exception: If stream deletion fails
238
+ """
239
+ stream_id = self._build_stream_id_for(id)
240
+ await self._eventstore.delete_async(stream_id)
241
+
242
+ async def _publish_domain_events(self, entity: TAggregate) -> None:
243
+ """
244
+ Override base class event publishing for event-sourced aggregates.
245
+
246
+ Event sourcing repositories DO NOT publish events directly because:
247
+ 1. Events are already persisted to the EventStore
248
+ 2. ReadModelReconciliator subscribes to EventStore and publishes ALL events
249
+ 3. Publishing here would cause DOUBLE PUBLISHING (once here, once from ReadModelReconciliator)
250
+
251
+ For event-sourced aggregates:
252
+ - Events are persisted to EventStore by _do_add_async/_do_update_async
253
+ - ReadModelReconciliator.on_event_record_stream_next_async() publishes via mediator
254
+ - This ensures single, reliable event publishing from the source of truth (EventStore)
255
+
256
+ State-based repositories still use the base class _publish_domain_events() correctly.
257
+ """
258
+ # Do nothing - ReadModelReconciliator handles event publishing from EventStore
259
+
260
+ def _build_stream_id_for(self, aggregate_id: TKey):
261
+ """Builds a new stream id for the specified aggregate"""
262
+ aggregate_name = self.__orig_class__.__args__[0].__name__
263
+ return f"{aggregate_name.lower()}-{aggregate_id}"
264
+
265
+ def _encode_event(self, e: DomainEvent):
266
+ """Encodes a domain event into a new event descriptor"""
267
+ event_type = type(e).__name__.lower()
268
+ return EventDescriptor(event_type, e)
269
+
270
+ @staticmethod
271
+ def configure(builder: ApplicationBuilderBase, entity_type: type, key_type: type) -> ApplicationBuilderBase:
272
+ """Configures the specified application to use an event sourcing based repository implementation to manage the specified type of entity"""
273
+ builder.services.try_add_singleton(
274
+ EventSourcingRepositoryOptions[entity_type, key_type],
275
+ singleton=EventSourcingRepositoryOptions[entity_type, key_type](),
276
+ )
277
+ builder.services.try_add_singleton(Repository[entity_type, key_type], EventSourcingRepository[entity_type, key_type])
278
+ return builder