hexcore 2.1.1__tar.gz → 2.3.0__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 (163) hide show
  1. {hexcore-2.1.1 → hexcore-2.3.0}/LICENSE +1 -5
  2. hexcore-2.3.0/MANIFEST.in +1 -0
  3. hexcore-2.3.0/PKG-INFO +584 -0
  4. hexcore-2.3.0/README.md +558 -0
  5. {hexcore-2.1.1 → hexcore-2.3.0}/hexcore/__init__.py +6 -10
  6. hexcore-2.3.0/hexcore/__main__.py +4 -0
  7. hexcore-2.3.0/hexcore/application/cqrs/__init__.py +24 -0
  8. hexcore-2.3.0/hexcore/application/cqrs/adapters.py +33 -0
  9. hexcore-2.3.0/hexcore/application/cqrs/config.py +69 -0
  10. hexcore-2.3.0/hexcore/application/cqrs/factory.py +131 -0
  11. hexcore-2.3.0/hexcore/application/cqrs/in_memory_buses.py +164 -0
  12. hexcore-2.3.0/hexcore/application/cqrs/pipeline.py +58 -0
  13. hexcore-2.3.0/hexcore/application/cqrs/registry.py +115 -0
  14. hexcore-2.3.0/hexcore/application/dtos/__init__.py +23 -0
  15. hexcore-2.3.0/hexcore/application/dtos/query.py +66 -0
  16. hexcore-2.3.0/hexcore/application/use_cases/__init__.py +13 -0
  17. {hexcore-2.1.1 → hexcore-2.3.0}/hexcore/application/use_cases/base.py +4 -4
  18. hexcore-2.3.0/hexcore/application/use_cases/query.py +37 -0
  19. hexcore-2.3.0/hexcore/config.py +185 -0
  20. hexcore-2.3.0/hexcore/domain/auth/__init__.py +13 -0
  21. hexcore-2.3.0/hexcore/domain/auth/permissions.py +71 -0
  22. {hexcore-2.1.1 → hexcore-2.3.0}/hexcore/domain/auth/value_objects.py +2 -3
  23. {hexcore-2.1.1 → hexcore-2.3.0}/hexcore/domain/base.py +3 -3
  24. hexcore-2.3.0/hexcore/domain/cqrs/__init__.py +50 -0
  25. hexcore-2.3.0/hexcore/domain/cqrs/buses.py +80 -0
  26. hexcore-2.3.0/hexcore/domain/cqrs/commands.py +30 -0
  27. hexcore-2.3.0/hexcore/domain/cqrs/decorators.py +78 -0
  28. hexcore-2.3.0/hexcore/domain/cqrs/exceptions.py +34 -0
  29. hexcore-2.3.0/hexcore/domain/cqrs/handlers.py +52 -0
  30. hexcore-2.3.0/hexcore/domain/cqrs/middleware.py +51 -0
  31. hexcore-2.3.0/hexcore/domain/cqrs/queries.py +27 -0
  32. hexcore-2.3.0/hexcore/domain/cqrs/serializer.py +39 -0
  33. hexcore-2.3.0/hexcore/domain/cqrs/task_queues.py +64 -0
  34. {hexcore-2.1.1 → hexcore-2.3.0}/hexcore/domain/events.py +36 -4
  35. {hexcore-2.1.1 → hexcore-2.3.0}/hexcore/domain/repositories.py +6 -11
  36. hexcore-2.3.0/hexcore/domain/services.py +232 -0
  37. {hexcore-2.1.1 → hexcore-2.3.0}/hexcore/domain/uow.py +7 -10
  38. hexcore-2.3.0/hexcore/infrastructure/api/__init__.py +3 -0
  39. hexcore-2.3.0/hexcore/infrastructure/api/utils.py +174 -0
  40. {hexcore-2.1.1 → hexcore-2.3.0}/hexcore/infrastructure/cache/cache_backends/memory.py +3 -3
  41. {hexcore-2.1.1 → hexcore-2.3.0}/hexcore/infrastructure/cache/cache_backends/redis.py +2 -2
  42. {hexcore-2.1.1 → hexcore-2.3.0}/hexcore/infrastructure/cli.py +184 -145
  43. hexcore-2.3.0/hexcore/infrastructure/cqrs/__init__.py +24 -0
  44. hexcore-2.3.0/hexcore/infrastructure/cqrs/middlewares.py +136 -0
  45. hexcore-2.3.0/hexcore/infrastructure/cqrs/procrastinate.py +99 -0
  46. hexcore-2.3.0/hexcore/infrastructure/cqrs/pydantic_serializer.py +56 -0
  47. hexcore-2.3.0/hexcore/infrastructure/cqrs/rabbitmq.py +182 -0
  48. hexcore-2.3.0/hexcore/infrastructure/events/events_backends/memory.py +32 -0
  49. hexcore-2.3.0/hexcore/infrastructure/repositories/base.py +28 -0
  50. {hexcore-2.1.1 → hexcore-2.3.0}/hexcore/infrastructure/repositories/implementations.py +55 -35
  51. hexcore-2.3.0/hexcore/infrastructure/repositories/orms/beanie/utils.py +283 -0
  52. hexcore-2.1.1/hexcore/infrastructure/repositories/orms/sql/alchemy.py → hexcore-2.3.0/hexcore/infrastructure/repositories/orms/sqlalchemy/__init__.py +1 -1
  53. hexcore-2.3.0/hexcore/infrastructure/repositories/orms/sqlalchemy/session.py +66 -0
  54. {hexcore-2.1.1/hexcore/infrastructure/repositories/orms/sql → hexcore-2.3.0/hexcore/infrastructure/repositories/orms/sqlalchemy}/utils.py +132 -12
  55. hexcore-2.3.0/hexcore/infrastructure/repositories/utils.py +352 -0
  56. hexcore-2.3.0/hexcore/infrastructure/uow/__init__.py +181 -0
  57. hexcore-2.3.0/hexcore/infrastructure/uow/helpers.py +21 -0
  58. hexcore-2.3.0/hexcore/infrastructure/workers/__init__.py +1 -0
  59. hexcore-2.3.0/hexcore/infrastructure/workers/consumer.py +123 -0
  60. hexcore-2.3.0/hexcore/infrastructure/workers/rabbitmq_worker.py +93 -0
  61. {hexcore-2.1.1 → hexcore-2.3.0}/hexcore/types.py +5 -1
  62. hexcore-2.3.0/hexcore.egg-info/PKG-INFO +584 -0
  63. hexcore-2.3.0/hexcore.egg-info/SOURCES.txt +97 -0
  64. {hexcore-2.1.1 → hexcore-2.3.0}/hexcore.egg-info/requires.txt +7 -3
  65. {hexcore-2.1.1 → hexcore-2.3.0}/hexcore.egg-info/top_level.txt +2 -1
  66. {hexcore-2.1.1 → hexcore-2.3.0}/pyproject.toml +18 -28
  67. hexcore-2.3.0/scripts/main.py +10 -0
  68. hexcore-2.3.0/tests/conftest.py +21 -0
  69. hexcore-2.3.0/tests/test_beanie_query_utils.py +138 -0
  70. hexcore-2.3.0/tests/test_config_loading.py +187 -0
  71. hexcore-2.3.0/tests/test_cqrs.py +602 -0
  72. hexcore-2.3.0/tests/test_domain_service_query.py +153 -0
  73. hexcore-2.3.0/tests/test_infrastructure_query_path.py +150 -0
  74. hexcore-2.3.0/tests/test_query_field_validation.py +218 -0
  75. hexcore-2.3.0/tests/test_rabbitmq_bus.py +95 -0
  76. hexcore-2.3.0/tests/test_repositories_utils.py +335 -0
  77. hexcore-2.3.0/tests/test_smart_routing.py +218 -0
  78. hexcore-2.3.0/tests/test_uow_session_regression.py +187 -0
  79. hexcore-2.3.0/tests/test_use_cases_query.py +224 -0
  80. hexcore-2.1.1/PKG-INFO +0 -38
  81. hexcore-2.1.1/README.md +0 -14
  82. hexcore-2.1.1/hexcore/__init__.pyi +0 -10
  83. hexcore-2.1.1/hexcore/application/__init__.pyi +0 -4
  84. hexcore-2.1.1/hexcore/application/dtos/__init__.py +0 -7
  85. hexcore-2.1.1/hexcore/application/dtos/__init__.pyi +0 -3
  86. hexcore-2.1.1/hexcore/application/dtos/base.pyi +0 -4
  87. hexcore-2.1.1/hexcore/application/use_cases/__init__.py +0 -7
  88. hexcore-2.1.1/hexcore/application/use_cases/__init__.pyi +0 -3
  89. hexcore-2.1.1/hexcore/application/use_cases/base.pyi +0 -11
  90. hexcore-2.1.1/hexcore/config.py +0 -99
  91. hexcore-2.1.1/hexcore/config.pyi +0 -35
  92. hexcore-2.1.1/hexcore/domain/auth/__init__.py +0 -17
  93. hexcore-2.1.1/hexcore/domain/auth/__init__.pyi +0 -4
  94. hexcore-2.1.1/hexcore/domain/auth/permissions.py +0 -73
  95. hexcore-2.1.1/hexcore/domain/auth/permissions.pyi +0 -25
  96. hexcore-2.1.1/hexcore/domain/auth/value_objects.pyi +0 -12
  97. hexcore-2.1.1/hexcore/domain/base.pyi +0 -19
  98. hexcore-2.1.1/hexcore/domain/events.pyi +0 -35
  99. hexcore-2.1.1/hexcore/domain/exceptions.pyi +0 -2
  100. hexcore-2.1.1/hexcore/domain/repositories.pyi +0 -25
  101. hexcore-2.1.1/hexcore/domain/services.py +0 -11
  102. hexcore-2.1.1/hexcore/domain/services.pyi +0 -8
  103. hexcore-2.1.1/hexcore/domain/uow.pyi +0 -18
  104. hexcore-2.1.1/hexcore/infrastructure/__init__.pyi +0 -0
  105. hexcore-2.1.1/hexcore/infrastructure/api/__init__.pyi +0 -0
  106. hexcore-2.1.1/hexcore/infrastructure/api/utils.py +0 -20
  107. hexcore-2.1.1/hexcore/infrastructure/api/utils.pyi +0 -9
  108. hexcore-2.1.1/hexcore/infrastructure/cache/__init__.pyi +0 -10
  109. hexcore-2.1.1/hexcore/infrastructure/cache/cache_backends/__init__.pyi +0 -0
  110. hexcore-2.1.1/hexcore/infrastructure/cache/cache_backends/memory.pyi +0 -10
  111. hexcore-2.1.1/hexcore/infrastructure/cache/cache_backends/redis.pyi +0 -12
  112. hexcore-2.1.1/hexcore/infrastructure/cli.pyi +0 -21
  113. hexcore-2.1.1/hexcore/infrastructure/events/__init__.pyi +0 -0
  114. hexcore-2.1.1/hexcore/infrastructure/events/events_backends/__init__.pyi +0 -0
  115. hexcore-2.1.1/hexcore/infrastructure/events/events_backends/memory.py +0 -20
  116. hexcore-2.1.1/hexcore/infrastructure/events/events_backends/memory.pyi +0 -8
  117. hexcore-2.1.1/hexcore/infrastructure/repositories/__init__.pyi +0 -0
  118. hexcore-2.1.1/hexcore/infrastructure/repositories/base.py +0 -24
  119. hexcore-2.1.1/hexcore/infrastructure/repositories/base.pyi +0 -13
  120. hexcore-2.1.1/hexcore/infrastructure/repositories/decorators.pyi +0 -5
  121. hexcore-2.1.1/hexcore/infrastructure/repositories/implementations.pyi +0 -39
  122. hexcore-2.1.1/hexcore/infrastructure/repositories/orms/__init__.pyi +0 -0
  123. hexcore-2.1.1/hexcore/infrastructure/repositories/orms/nosql/__init__.py +0 -0
  124. hexcore-2.1.1/hexcore/infrastructure/repositories/orms/nosql/__init__.pyi +0 -0
  125. hexcore-2.1.1/hexcore/infrastructure/repositories/orms/nosql/beanie.pyi +0 -14
  126. hexcore-2.1.1/hexcore/infrastructure/repositories/orms/nosql/utils.py +0 -134
  127. hexcore-2.1.1/hexcore/infrastructure/repositories/orms/nosql/utils.pyi +0 -18
  128. hexcore-2.1.1/hexcore/infrastructure/repositories/orms/sql/__init__.py +0 -0
  129. hexcore-2.1.1/hexcore/infrastructure/repositories/orms/sql/__init__.pyi +0 -0
  130. hexcore-2.1.1/hexcore/infrastructure/repositories/orms/sql/alchemy.pyi +0 -19
  131. hexcore-2.1.1/hexcore/infrastructure/repositories/orms/sql/session.py +0 -31
  132. hexcore-2.1.1/hexcore/infrastructure/repositories/orms/sql/session.pyi +0 -9
  133. hexcore-2.1.1/hexcore/infrastructure/repositories/orms/sql/utils.pyi +0 -22
  134. hexcore-2.1.1/hexcore/infrastructure/repositories/utils.py +0 -119
  135. hexcore-2.1.1/hexcore/infrastructure/repositories/utils.pyi +0 -13
  136. hexcore-2.1.1/hexcore/infrastructure/uow/__init__.py +0 -121
  137. hexcore-2.1.1/hexcore/infrastructure/uow/__init__.pyi +0 -22
  138. hexcore-2.1.1/hexcore/infrastructure/uow/decorators.pyi +0 -4
  139. hexcore-2.1.1/hexcore/infrastructure/uow/helpers.py +0 -9
  140. hexcore-2.1.1/hexcore/infrastructure/uow/helpers.pyi +0 -7
  141. hexcore-2.1.1/hexcore/types.pyi +0 -21
  142. hexcore-2.1.1/hexcore.egg-info/PKG-INFO +0 -38
  143. hexcore-2.1.1/hexcore.egg-info/SOURCES.txt +0 -100
  144. {hexcore-2.1.1 → hexcore-2.3.0}/hexcore/application/__init__.py +0 -0
  145. {hexcore-2.1.1 → hexcore-2.3.0}/hexcore/application/dtos/base.py +0 -0
  146. {hexcore-2.1.1 → hexcore-2.3.0}/hexcore/domain/__init__.py +0 -0
  147. {hexcore-2.1.1 → hexcore-2.3.0}/hexcore/domain/exceptions.py +0 -0
  148. {hexcore-2.1.1 → hexcore-2.3.0}/hexcore/infrastructure/__init__.py +0 -0
  149. {hexcore-2.1.1 → hexcore-2.3.0}/hexcore/infrastructure/cache/__init__.py +0 -0
  150. {hexcore-2.1.1/hexcore/infrastructure/api → hexcore-2.3.0/hexcore/infrastructure/cache/cache_backends}/__init__.py +0 -0
  151. {hexcore-2.1.1/hexcore/infrastructure/cache/cache_backends → hexcore-2.3.0/hexcore/infrastructure/events}/__init__.py +0 -0
  152. {hexcore-2.1.1/hexcore/infrastructure/events → hexcore-2.3.0/hexcore/infrastructure/events/events_backends}/__init__.py +0 -0
  153. {hexcore-2.1.1/hexcore/infrastructure/events/events_backends → hexcore-2.3.0/hexcore/infrastructure/repositories}/__init__.py +0 -0
  154. {hexcore-2.1.1 → hexcore-2.3.0}/hexcore/infrastructure/repositories/decorators.py +0 -0
  155. {hexcore-2.1.1/hexcore/infrastructure/repositories → hexcore-2.3.0/hexcore/infrastructure/repositories/orms}/__init__.py +0 -0
  156. /hexcore-2.1.1/hexcore/infrastructure/repositories/orms/nosql/beanie.py → /hexcore-2.3.0/hexcore/infrastructure/repositories/orms/beanie/__init__.py +0 -0
  157. {hexcore-2.1.1 → hexcore-2.3.0}/hexcore/infrastructure/uow/decorators.py +0 -0
  158. /hexcore-2.1.1/hexcore/domain/__init__.pyi → /hexcore-2.3.0/hexcore/py.typed +0 -0
  159. {hexcore-2.1.1 → hexcore-2.3.0}/hexcore.egg-info/dependency_links.txt +0 -0
  160. {hexcore-2.1.1 → hexcore-2.3.0}/hexcore.egg-info/entry_points.txt +0 -0
  161. {hexcore-2.1.1/hexcore/infrastructure/repositories/orms → hexcore-2.3.0/scripts}/__init__.py +0 -0
  162. {hexcore-2.1.1 → hexcore-2.3.0}/setup.cfg +0 -0
  163. {hexcore-2.1.1 → hexcore-2.3.0}/tests/test_basic.py +0 -0
@@ -1,10 +1,6 @@
1
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
2
- copies of the Software, and to permit persons to whom the Software is
3
- furnished to do so, subject to the following conditions:
4
-
5
1
  MIT License
6
2
 
7
- Copyright (c) 2025 David Latosefki (Indroic)
3
+ Copyright (c) 2025 David Latosefki
8
4
 
9
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
10
6
  of this software and associated documentation files (the "Software"), to deal
@@ -0,0 +1 @@
1
+ recursive-include hexcore *.pyi py.typed
hexcore-2.3.0/PKG-INFO ADDED
@@ -0,0 +1,584 @@
1
+ Metadata-Version: 2.4
2
+ Name: hexcore
3
+ Version: 2.3.0
4
+ Summary: Núcleo reutilizable para proyectos Python con arquitectura hexagonal y event handling. Provee abstracciones, utilidades y contratos para DDD, eventos y desacoplamiento de infraestructura.
5
+ Author-email: "David Latosefki (Indroic)" <indroic@outlook.com>
6
+ License-Expression: MIT
7
+ Requires-Python: >=3.12
8
+ Description-Content-Type: text/markdown
9
+ License-File: LICENSE
10
+ Requires-Dist: aiosqlite>=0.21.0
11
+ Requires-Dist: alembic>=1.16.5
12
+ Requires-Dist: asyncpg>=0.30.0
13
+ Requires-Dist: beanie>=2.0.0
14
+ Requires-Dist: fastapi>=0.116.1
15
+ Requires-Dist: pika>=1.3.2
16
+ Requires-Dist: pydantic>=2.11.7
17
+ Requires-Dist: redis>=6.4.0
18
+ Requires-Dist: sqlalchemy>=2.0.43
19
+ Requires-Dist: typer>=0.17.3
20
+ Requires-Dist: ruff>=0.12.11
21
+ Provides-Extra: procrastinate
22
+ Requires-Dist: procrastinate>=3.0.0; extra == "procrastinate"
23
+ Provides-Extra: rabbitmq
24
+ Requires-Dist: aio-pika>=9.4.0; extra == "rabbitmq"
25
+ Dynamic: license-file
26
+
27
+ # HexCore [![PyPI Downloads](https://static.pepy.tech/personalized-badge/hexcore?period=total&units=INTERNATIONAL_SYSTEM&left_color=BLACK&right_color=GREEN&left_text=downloads)](https://pepy.tech/projects/hexcore)
28
+ HexCore es un módulo base reutilizable para proyectos Python que implementan arquitectura hexagonal y event handling.
29
+
30
+ ---
31
+
32
+ ## Skills del Proyecto
33
+
34
+ Este repositorio cuenta con un conjunto de skills adicionales para extender y personalizar funcionalidades en VS Code y otros entornos compatibles. Puedes encontrarlas en:
35
+
36
+ - [Repositorio de Skills de HexCore](https://github.com/Indroic/hexcore-skill)
37
+
38
+ ---
39
+
40
+ ## ¿Qué provee HexCore?
41
+
42
+ - **Clases base y abstracciones** para entidades, repositorios, servicios y unidad de trabajo (UoW), siguiendo los principios de DDD y arquitectura hexagonal.
43
+ - **Interfaces y contratos** para caché, eventos y manejo de dependencias, desacoplando la lógica de negocio de la infraestructura.
44
+ - **Utilidades para event sourcing y event dispatching** listas para usar en cualquier proyecto.
45
+ - **Estructura flexible** para que puedas construir microservicios o aplicaciones monolíticas desacopladas y testeables.
46
+
47
+ ---
48
+
49
+ ## Instalación
50
+
51
+ ```sh
52
+ pip install hexcore
53
+ ```
54
+
55
+ ## Templates de Proyecto (CLI)
56
+
57
+ HexCore incluye templates base para bootstrap de proyectos:
58
+
59
+ ```sh
60
+ hexcore init mi_proyecto --template hexagonal
61
+ hexcore init mi_proyecto --template vertical-slice
62
+ ```
63
+
64
+ - `hexagonal`: crea `src/domain`, `src/application`, `src/infrastructure`.
65
+ - `vertical-slice`: crea `src/features`, `src/shared/domain`, `src/shared/application`, `src/shared/infrastructure`.
66
+
67
+ En ambos templates se generan:
68
+ - `config.py` en raíz con `repository_discovery_paths` de ejemplo.
69
+ - estructura de migraciones con Alembic.
70
+
71
+ ---
72
+
73
+ ## Configuración v2 (Folder-Agnostic)
74
+
75
+ Desde v2, HexCore usa configuración explícita y no aplica fallback implícito para descubrir repositorios.
76
+
77
+ ### 1. Configuración visible en raíz
78
+
79
+ Define un archivo `config.py` en la raíz del proyecto:
80
+
81
+ ```python
82
+ from hexcore.config import ServerConfig
83
+
84
+ config = ServerConfig(
85
+ repository_discovery_paths={
86
+ "myapp.features.users.infrastructure.repositories",
87
+ "myapp.features.billing.infrastructure.repositories",
88
+ }
89
+ )
90
+ ```
91
+
92
+ ### 2. Prioridad para cargar configuración
93
+
94
+ `LazyConfig` resuelve módulos en este orden:
95
+
96
+ 1. `HEXCORE_CONFIG_MODULE`
97
+ 2. `HEXCORE_CONFIG_MODULES` (lista separada por comas)
98
+ 3. módulos configurados por `LazyConfig.set_config_modules(...)`
99
+ 4. `config` por defecto (raíz del proyecto)
100
+
101
+ ### 3. Regla de discovery en v2
102
+
103
+ - Si `repository_discovery_paths` está vacío, no se cargan módulos de repositorios.
104
+ - UoW falla con error explícito para evitar comportamiento ambiguo.
105
+
106
+ ---
107
+
108
+ ## Pautas de Colaboración
109
+
110
+ ¡Gracias por tu interés en contribuir a HexCore! Para mantener una colaboración organizada y eficiente, sigue estas pautas:
111
+
112
+ ### 1. Código de Conducta
113
+ Mantén siempre una comunicación respetuosa y profesional. Revisa el [Código de Conducta](CODE_OF_CONDUCT.md) antes de interactuar.
114
+
115
+ ### 2. Cómo Contribuir
116
+ - **Forkea** el repositorio y crea una rama para tu contribución (`feature/nombre`, `fix/nombre`, etc.).
117
+ - Realiza tus cambios en la rama y asegúrate de que el código funcione correctamente.
118
+ - Escribe una descripción clara y detallada en tu pull request (PR).
119
+ - Relaciona los issues relevantes en tu PR si aplica.
120
+
121
+ ### 3. Estilo y Formato de Código
122
+ - Sigue la guía de estilos de Python ([PEP8](https://pep8.org/)).
123
+ - Usa comentarios cuando sea necesario para clarificar el propósito del código.
124
+ - Idealmente, incluye pruebas unitarias para nuevas funciones y arreglos.
125
+
126
+ ### 4. Revisión de Pull Requests
127
+ - Todos los PR serán revisados antes de ser aceptados. Se pueden solicitar cambios o aclaraciones.
128
+ - Responde a los comentarios de los revisores para facilitar el proceso.
129
+
130
+ ### 5. Issues
131
+ - Describe claramente los problemas que encuentres.
132
+ - Proporciona información relevante (logs, versiones, pasos para reproducir, etc.).
133
+
134
+ ### 6. Comunicación
135
+ - Usa los issues y las discusiones para preguntas, sugerencias o propuestas.
136
+ - Si tienes dudas sobre cómo empezar, puedes abrir un issue para orientación.
137
+
138
+ ### 7. Licencia
139
+ Al contribuir, aceptas que tu código será distribuido bajo la licencia del repositorio.
140
+
141
+ ---
142
+
143
+ ## Documentación Básica
144
+
145
+ ### Estructura principal
146
+
147
+ HexCore se organiza con los siguientes submódulos y carpetas:
148
+
149
+ - **src/domain/**: Módulos de dominio, entidades, repositorios, servicios, objetos de valor, eventos, enums y excepciones.
150
+ ```
151
+ src/domain/{modulo}/
152
+ ├─ __init__.py
153
+ ├─ entities.py
154
+ ├─ repositories.py
155
+ ├─ services.py
156
+ ├─ value_objects.py
157
+ ├─ events.py
158
+ ├─ enums.py
159
+ └─ exceptions.py
160
+ ```
161
+ - **src/application/**: Casos de uso (UseCase) y DTOs para orquestar la lógica de negocio.
162
+ - **src/infrastructure/**: Implementaciones técnicas (ORM/ODM, CLI, caché, base de datos, repositorios, unit of work).
163
+ - **src/infrastructure/database/models/**: Modelos SQLAlchemy para base de datos relacional.
164
+ - **src/infrastructure/database/documents/**: Documentos Beanie para MongoDB.
165
+ - **tests/**: Pruebas para módulos de dominio e infraestructura.
166
+
167
+ ---
168
+
169
+ ### Abstracciones de Entidades y Eventos
170
+
171
+ #### BaseEntity
172
+
173
+ Clase base para entidades de dominio. Provee atributos comunes y gestión de eventos.
174
+
175
+ ```python
176
+ from hexcore.domain.base import BaseEntity
177
+
178
+ class User(BaseEntity):
179
+ id: UUID
180
+ name: str
181
+ ```
182
+
183
+ #### DomainEvent y eventos de entidad
184
+
185
+ Abstracciones para eventos de dominio y para ciclo de vida de entidades.
186
+
187
+ ```python
188
+ from hexcore.domain.events import DomainEvent, EntityCreatedEvent
189
+
190
+ class UserCreatedEvent(EntityCreatedEvent[User]):
191
+ pass
192
+
193
+ user = User(...)
194
+ event = UserCreatedEvent(entity_id=user.id, payload={"name": user.name})
195
+ ```
196
+
197
+ ---
198
+
199
+ ### Implementaciones de Repositorios
200
+
201
+ #### SQLAlchemyCommonImplementationsRepo
202
+
203
+ Repositorio genérico para modelos SQLAlchemy con métodos CRUD reutilizables.
204
+
205
+ ```python
206
+ class SQLAlchemyCommonImplementationsRepo(BaseSQLAlchemyRepository[T], HasBasicArgs[T, M], t.Generic[T, M]):
207
+ # Métodos principales: get_by_id, list_all, save, delete
208
+ ...
209
+ ```
210
+
211
+ **Ejemplo:**
212
+
213
+ ```python
214
+ class UserRepository(SQLAlchemyCommonImplementationsRepo[UserEntity, UserModel]):
215
+ def __init__(self, uow):
216
+ super().__init__(
217
+ entity_cls=UserEntity,
218
+ model_cls=UserModel,
219
+ not_found_exception=UserNotFoundException,
220
+ fields_resolvers=None,
221
+ fields_serializers=None,
222
+ uow=uow
223
+ )
224
+ ```
225
+
226
+ #### BeanieODMCommonImplementationsRepo
227
+
228
+ Repositorio genérico para documentos Beanie ODM (MongoDB) con métodos CRUD reutilizables.
229
+
230
+ ```python
231
+ class BeanieODMCommonImplementationsRepo(IBaseRepository[T], HasBasicArgs[T, D], t.Generic[T, D]):
232
+ # Métodos principales: get_by_id, list_all, save, delete
233
+ ...
234
+ ```
235
+
236
+ **Ejemplo:**
237
+
238
+ ```python
239
+ class UserRepository(BeanieODMCommonImplementationsRepo[UserEntity, UserDocument]):
240
+ def __init__(self, uow):
241
+ super().__init__(
242
+ entity_cls=UserEntity,
243
+ document_cls=UserDocument,
244
+ not_found_exception=UserNotFoundException,
245
+ fields_resolvers=None,
246
+ fields_serializers=None,
247
+ uow=uow
248
+ )
249
+ ```
250
+
251
+ ---
252
+
253
+ ### Inicialización y Descubrimiento de Documentos Beanie
254
+
255
+ Para inicializar y registrar automáticamente todos los documentos Beanie:
256
+
257
+ ```python
258
+ from hexcore.infrastructure.repositories.orms.beanie.utils import init_beanie_documents
259
+
260
+ await init_beanie_documents()
261
+ ```
262
+
263
+ ---
264
+
265
+ ### Conversión entre modelos/documentos y entidades
266
+
267
+ Ambos repositorios utilizan `to_entity_from_model_or_document` para convertir modelos ORM/ODM en entidades del dominio, aplicando resolvers para atributos complejos.
268
+
269
+ ---
270
+
271
+ ---
272
+
273
+ ## Arquitectura CQRS en HexCore
274
+
275
+ HexCore v2 integra de forma nativa soporte para el patrón **CQRS (Command Query Responsibility Segregation)**, permitiendo separar conceptual y técnicamente las operaciones de escritura (Commands) de las de lectura (Queries).
276
+
277
+ ### ¿Cómo funciona el CQRS en HexCore?
278
+
279
+ El sistema se basa en 3 buses principales, configurables e independientes:
280
+
281
+ 1. **`AbstractCommandBus`**: Despacha inteniones de mutación (`Command`) a un único `AbstractCommandHandler`. Los commands modifican el estado del sistema y se ejecutan (por defecto) dentro de una transacción de base de datos (Unit of Work).
282
+ 2. **`AbstractQueryBus`**: Despacha intenciones de lectura (`Query`) a un único `AbstractQueryHandler`. Retornan un resultado sin mutar el estado.
283
+ 3. **`EventBus`**: Distribuye eventos de dominio (`DomainEvent`) a múltiples suscriptores asíncronamente (vía `subscribe`/`publish`).
284
+
285
+ La configuración de CQRS se activa mediante el `CQRSConfig` en tu `ServerConfig`:
286
+
287
+ ```python
288
+ from hexcore.config import ServerConfig
289
+ from hexcore.application.cqrs.config import CQRSConfig, BusConfig
290
+
291
+ config = ServerConfig(
292
+ cqrs=CQRSConfig(
293
+ command_bus=BusConfig(
294
+ # Por defecto incluye TransactionMiddleware
295
+ middlewares=["hexcore.infrastructure.cqrs.middlewares.TransactionMiddleware"]
296
+ ),
297
+ # Puedes sustituir el backend en memoria por uno distribuido (Ej: Celery, Procrastinate)
298
+ # backend="mi_app.infrastructure.ProcrastinateCommandBus"
299
+ )
300
+ )
301
+ ```
302
+
303
+ ---
304
+
305
+ ### Guía de Migración: De Casos de Uso Clásicos a CQRS
306
+
307
+ Si ya tienes una aplicación escrita con la abstracción `UseCase` de HexCore, puedes migrar progresivamente a CQRS sin reescribir todo tu código, utilizando los adaptadores incluidos.
308
+
309
+ #### Paso 1: Usar el adaptador `UseCaseCommandHandler`
310
+
311
+ En lugar de instanciar un UseCase directamente en tu endpoint, envuélvelo en un comando:
312
+
313
+ ```python
314
+ from hexcore.application.cqrs.adapters import UseCaseCommandHandler
315
+ from hexcore.application.cqrs.registry import HandlerRegistry
316
+
317
+ # 1. Tienes tu UseCase legado
318
+ class CreateUserUseCase(UseCase[CreateUserCommand, UserDTO]):
319
+ async def execute(self, request: CreateUserCommand) -> UserDTO:
320
+ # logica legacy
321
+ pass
322
+
323
+ # 2. Lo registras en el registry de CQRS utilizando el adaptador
324
+ registry = HandlerRegistry()
325
+ registry.register_command(
326
+ CreateUserCommand,
327
+ UseCaseCommandHandler(CreateUserUseCase())
328
+ )
329
+ ```
330
+
331
+ #### Paso 2: Consumirlo desde el endpoint usando el CommandBus
332
+
333
+ ```python
334
+ @router.post("/users")
335
+ async def create_user(
336
+ cmd: CreateUserCommand,
337
+ # factory inyectado por dependencias
338
+ bus: AbstractCommandBus = Depends(get_command_bus)
339
+ ):
340
+ # El bus despacha el comando al UseCase legacy de forma transparente
341
+ result = await bus.dispatch(cmd)
342
+ return result
343
+ ```
344
+
345
+ #### Paso 3 (Final): Refactor a Handler Puro
346
+
347
+ Cuando estés listo, convierte tu UseCase directamente en un `AbstractCommandHandler`:
348
+
349
+ ```python
350
+ from hexcore.domain.cqrs import AbstractCommandHandler
351
+
352
+ class CreateUserHandler(AbstractCommandHandler[CreateUserCommand, UserDTO]):
353
+ def __init__(self, uow: IUnitOfWork):
354
+ self.uow = uow
355
+
356
+ async def handle(self, command: CreateUserCommand) -> UserDTO:
357
+ # Lógica refactorizada
358
+ return dto
359
+ ```
360
+
361
+ ---
362
+
363
+ ### Guía: Almacenamiento Híbrido para Queries (Mongo, Redis, SQL)
364
+
365
+ La mayor ventaja de CQRS es optimizar las lecturas. HexCore permite que tus **Commands** escriban en una base de datos relacional (SQLAlchemy) fuertemente normalizada, mientras que los **Queries** leen de vistas desnormalizadas súper rápidas en MongoDB o Redis.
366
+
367
+ #### 1. Sincronización a través del EventBus (La Proyección)
368
+
369
+ Cuando un Command modifica SQL, dispara un Evento de Dominio. Un handler de eventos intercepta este evento y actualiza el "Read Model" en MongoDB o Redis.
370
+
371
+ ```python
372
+ from hexcore.domain.events import EventBus, DomainEvent
373
+
374
+ class UserCreatedEvent(DomainEvent):
375
+ user_id: str
376
+ full_name: str
377
+ email: str
378
+
379
+ async def project_user_to_mongodb(event: UserCreatedEvent):
380
+ """Proyecta el evento en la BD de lectura (MongoDB)"""
381
+ doc = UserReadDocument(
382
+ id=event.user_id,
383
+ name=event.full_name,
384
+ email=event.email
385
+ )
386
+ await doc.insert() # usando Beanie (Mongo)
387
+
388
+ # Registrar la proyección
389
+ event_bus.subscribe(UserCreatedEvent, project_user_to_mongodb)
390
+ ```
391
+
392
+ #### 2. Query Handler leyendo del Read Model
393
+
394
+ Tu QueryHandler nunca toca SQL, simplemente ataca directamente a Mongo o Redis para máxima velocidad.
395
+
396
+ ```python
397
+ from hexcore.domain.cqrs import AbstractQueryHandler, Query
398
+
399
+ class GetUserQuery(Query[UserReadDTO]):
400
+ user_id: str
401
+
402
+ class GetUserQueryHandler(AbstractQueryHandler[GetUserQuery, UserReadDTO]):
403
+ async def handle(self, query: GetUserQuery) -> UserReadDTO:
404
+ # Consulta ultra rápida a la colección de lectura en MongoDB
405
+ doc = await UserReadDocument.get(query.user_id)
406
+
407
+ # O desde Redis:
408
+ # data = await redis_client.get(f"user:{query.user_id}")
409
+
410
+ if not doc:
411
+ raise UserNotFoundException()
412
+ return UserReadDTO(**doc.dict())
413
+ ```
414
+
415
+ Con este esquema, alcanzas una alta escalabilidad: tus endpoints GET son despachados por el `QueryBus` respondiendo en milisegundos desde Mongo/Redis, y tus operaciones POST/PUT/DELETE van por el `CommandBus` transaccionando con ACID en SQL.
416
+
417
+ #### 3. Definición de Modelos de Lectura (Proyecciones)
418
+
419
+ Una pregunta frecuente es: **¿HexCore genera automáticamente estos modelos de lectura?**
420
+ La respuesta es **No**. El patrón CQRS sugiere que tus modelos de lectura estén diseñados *específicamente* para lo que tus interfaces visuales (UI) o APIs van a consultar. Por lo tanto, debes definir estos modelos manualmente.
421
+
422
+ **Si usas MongoDB (Beanie) para lecturas:**
423
+ Debes crear un documento manual optimizado. Por ejemplo, en lugar de tener joins, puedes embeber datos:
424
+ ```python
425
+ from beanie import Document
426
+
427
+ # Modelo desnormalizado optimizado para la lectura
428
+ class UserReadDocument(Document):
429
+ id: str # ID referenciado de la tabla SQL
430
+ name: str
431
+ email: str
432
+ total_purchases_cache: int = 0 # Dato pre-calculado por eventos
433
+
434
+ class Settings:
435
+ name = "users_read_projections"
436
+ ```
437
+
438
+ **Si usas PostgreSQL/MySQL (SQLAlchemy) para lecturas:**
439
+ Si prefieres mantenerte 100% en SQL pero aislando lecturas, puedes crear tablas específicas para proyecciones (Materialized Views o tablas planas):
440
+ ```python
441
+ from sqlalchemy.orm import declarative_base
442
+ from sqlalchemy import Column, String, Integer
443
+
444
+ Base = declarative_base()
445
+
446
+ class UserReadProjection(Base):
447
+ __tablename__ = 'users_read_projection'
448
+
449
+ # Modelo totalmente plano sin relaciones ForeignKey complejas
450
+ id = Column(String, primary_key=True)
451
+ full_name = Column(String)
452
+ email = Column(String)
453
+ total_purchases_cache = Column(Integer, default=0)
454
+ ```
455
+ En ambos casos, es tu **EventBus** (o un consumidor como Procrastinate) el encargado de instanciar estos modelos manuales y persistirlos cada vez que se detecte un cambio en los modelos de escritura.
456
+
457
+ ---
458
+
459
+ ### Integración con Task Queues (Smart Routing)
460
+
461
+ HexCore v2 hace que la delegación de tareas a **Celery**, **Procrastinate** o **ARQ** sea increíblemente sencilla y mágica a través del patrón de **Smart Routing**.
462
+
463
+ Ya no necesitas instanciar buses separados para código síncrono y asíncrono. HexCore enruta automáticamente tus comandos y eventos hacia las colas de background usando simples decoradores.
464
+
465
+ #### 1. Decoradores de Background
466
+
467
+ HexCore ofrece 3 decoradores esenciales en `hexcore.domain.cqrs.decorators` para cubrir todos los casos de uso:
468
+
469
+ 1. **`@background_command(queue="...")`**: Aplícalo sobre una clase `Command`. Todo el comando y su handler se ejecutarán asíncronamente en el Worker. Ideal para operaciones pesadas iniciadas por el usuario (ej. Generar un reporte PDF masivo).
470
+ 2. **`@background_handler(queue="...")`**: Aplícalo sobre una función que maneje un evento (`DomainEvent`). Permite que un solo evento dispare algunas acciones síncronas rápidas y otras asíncronas lentas (ej. Enviar emails).
471
+ 3. **`@background_task(queue="...")`**: Aplícalo sobre funciones o utilidades genéricas que no pertenecen al modelo estricto de CQRS (ej. Limpiar base de datos, tareas tipo CRON).
472
+
473
+ **Ejemplos de uso:**
474
+
475
+ ```python
476
+ from hexcore.domain.cqrs.decorators import background_command, background_handler, background_task
477
+ from hexcore.domain.cqrs.commands import Command
478
+
479
+ # 1. Comando de ejecución asíncrona obligatoria
480
+ @background_command(queue="high_priority")
481
+ class SendEmailCommand(Command):
482
+ user_id: str
483
+ template: str
484
+
485
+ # 2. Handler de evento asíncrono
486
+ @background_handler(queue="analytics")
487
+ async def send_analytics_on_user_created(event: UserCreatedEvent):
488
+ # Lógica costosa...
489
+ pass
490
+
491
+ # 3. Tarea genérica (Non-CQRS)
492
+ @background_task(queue="maintenance")
493
+ async def clean_old_records_task(days_retention: int):
494
+ # Limpieza de base de datos...
495
+ pass
496
+ ```
497
+
498
+ #### 2. Crear el Enqueuer (Adaptador)
499
+
500
+ Implementa la interfaz genérica `ITaskEnqueuer` usando la SDK de tu Task Queue favorito:
501
+
502
+ ```python
503
+ from hexcore.domain.cqrs.task_queues import ITaskEnqueuer
504
+
505
+ class ProcrastinateEnqueuer(ITaskEnqueuer):
506
+ async def enqueue_command(self, command_name: str, payload: dict, queue: str) -> None:
507
+ await process_cqrs_command.defer_async(payload=payload)
508
+
509
+ async def enqueue_handler(self, handler_name: str, payload: dict, queue: str) -> None:
510
+ await process_cqrs_handler.defer_async(handler_name=handler_name, payload=payload)
511
+
512
+ async def enqueue_event(self, event_name: str, payload: dict, queue: str) -> None:
513
+ pass # Usualmente no se usa directamente si utilizas @background_handler
514
+
515
+ async def enqueue_task(self, task_name: str, payload: dict, queue: str) -> None:
516
+ await process_generic_task.defer_async(task_name=task_name, payload=payload)
517
+ ```
518
+
519
+ #### 3. Configurar tus Buses (Enrutamiento Automático)
520
+
521
+ Al inyectar tu enqueuer en los buses estándar de memoria en tu API, estos adquieren la habilidad de enrutamiento inteligente. (Si usas un comando decorado pero no inyectas un enqueuer, HexCore levantará una excepción tempranamente).
522
+
523
+ ```python
524
+ from hexcore.application.cqrs.in_memory_buses import InMemoryCommandBus, InMemoryEventBus
525
+
526
+ enqueuer = ProcrastinateEnqueuer()
527
+ serializer = PydanticSerializer()
528
+
529
+ # El bus evalúa: ¿Tiene el comando @background_command? Si es así, usa el enqueuer.
530
+ command_bus = InMemoryCommandBus(registry=registry, enqueuer=enqueuer, serializer=serializer)
531
+
532
+ # El bus evalúa suscriptores: ¿Tienen @background_handler? Si es así, usa el enqueuer.
533
+ event_bus = InMemoryEventBus(enqueuer=enqueuer, serializer=serializer)
534
+ ```
535
+
536
+ #### 4. Ejecutar tareas genéricas
537
+
538
+ Para encolar la tarea genérica (`@background_task`), la llamas indirectamente pasándola por el enqueuer:
539
+
540
+ ```python
541
+ # Así se encola una tarea genérica sin CQRS:
542
+ await enqueuer.enqueue_task(
543
+ task_name=clean_old_records_task.__cqrs_task_name__,
544
+ payload={"days_retention": 30},
545
+ queue=clean_old_records_task.__cqrs_queue__
546
+ )
547
+ ```
548
+
549
+ #### 5. Levantar el Worker (Consumidor Universal)
550
+
551
+ En el entrypoint de tu worker (ej. Celery o Procrastinate), usa el `CQRSConsumer` de HexCore para deserializar y ejecutar los payloads interceptados. El consumidor usa resolución dinámica para invocar la función correcta automáticamente.
552
+
553
+ ```python
554
+ from hexcore.infrastructure.workers.consumer import CQRSConsumer
555
+
556
+ consumer = CQRSConsumer(
557
+ command_bus=command_bus, # Tu CommandBus configurado
558
+ event_bus=event_bus,
559
+ serializer=serializer
560
+ )
561
+
562
+ @app.task(name="process_cqrs_command")
563
+ async def process_cqrs_command(payload: dict):
564
+ # HexCore deserializa y ejecuta el Command usando el CommandBus local
565
+ await consumer.process_command(payload)
566
+
567
+ @app.task(name="process_cqrs_handler")
568
+ async def process_cqrs_handler(handler_name: str, payload: dict):
569
+ # HexCore resuelve y ejecuta exclusivamente el Event Handler asíncrono
570
+ await consumer.process_handler(handler_name, payload)
571
+
572
+ @app.task(name="process_generic_task")
573
+ async def process_generic_task(task_name: str, payload: dict):
574
+ # HexCore resuelve e inyecta los kwargs a la función pura
575
+ await consumer.process_task(task_name, payload)
576
+ ```
577
+
578
+ ---
579
+
580
+ ## Referencias
581
+
582
+ - [CONTRIBUTING.md](./CONTRIBUTING.md): Pautas de colaboración.
583
+ - [CHANGELOG.md](./CHANGELOG.md): Historial de cambios.
584
+ - [DOCS.md](./DOCS.md): Documentación básica de clases, funciones y ejemplos.