hexcore 1.7.0__tar.gz → 2.0.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.
- hexcore-2.0.0/MANIFEST.in +1 -0
- {hexcore-1.7.0 → hexcore-2.0.0}/PKG-INFO +52 -1
- {hexcore-1.7.0 → hexcore-2.0.0}/README.md +51 -0
- hexcore-2.0.0/hexcore/application/dtos/__init__.py +23 -0
- hexcore-2.0.0/hexcore/application/dtos/query.py +66 -0
- hexcore-2.0.0/hexcore/application/use_cases/__init__.py +13 -0
- hexcore-2.0.0/hexcore/application/use_cases/query.py +37 -0
- {hexcore-1.7.0 → hexcore-2.0.0}/hexcore/config.py +57 -7
- hexcore-2.0.0/hexcore/domain/services.py +232 -0
- hexcore-2.0.0/hexcore/infrastructure/api/__init__.py +3 -0
- hexcore-2.0.0/hexcore/infrastructure/api/utils.py +172 -0
- {hexcore-1.7.0 → hexcore-2.0.0}/hexcore/infrastructure/cli.py +173 -114
- {hexcore-1.7.0 → hexcore-2.0.0}/hexcore/infrastructure/repositories/implementations.py +23 -0
- hexcore-2.0.0/hexcore/infrastructure/repositories/orms/beanie/utils.py +283 -0
- {hexcore-1.7.0 → hexcore-2.0.0}/hexcore/infrastructure/repositories/orms/sqlalchemy/session.py +10 -3
- {hexcore-1.7.0 → hexcore-2.0.0}/hexcore/infrastructure/repositories/orms/sqlalchemy/utils.py +116 -1
- {hexcore-1.7.0 → hexcore-2.0.0}/hexcore/infrastructure/repositories/utils.py +22 -112
- {hexcore-1.7.0 → hexcore-2.0.0}/hexcore/infrastructure/uow/__init__.py +44 -29
- {hexcore-1.7.0 → hexcore-2.0.0}/hexcore.egg-info/PKG-INFO +52 -1
- {hexcore-1.7.0 → hexcore-2.0.0}/hexcore.egg-info/SOURCES.txt +12 -46
- {hexcore-1.7.0 → hexcore-2.0.0}/pyproject.toml +5 -5
- hexcore-2.0.0/scripts/main.py +10 -0
- hexcore-2.0.0/tests/conftest.py +21 -0
- hexcore-2.0.0/tests/test_beanie_query_utils.py +138 -0
- hexcore-2.0.0/tests/test_config_loading.py +187 -0
- hexcore-2.0.0/tests/test_domain_service_query.py +153 -0
- hexcore-2.0.0/tests/test_infrastructure_query_path.py +150 -0
- hexcore-2.0.0/tests/test_query_field_validation.py +218 -0
- {hexcore-1.7.0 → hexcore-2.0.0}/tests/test_repositories_utils.py +45 -8
- {hexcore-1.7.0 → hexcore-2.0.0}/tests/test_uow_session_regression.py +72 -3
- hexcore-2.0.0/tests/test_use_cases_query.py +224 -0
- hexcore-1.7.0/hexcore/__init__.pyi +0 -11
- hexcore-1.7.0/hexcore/application/__init__.pyi +0 -4
- hexcore-1.7.0/hexcore/application/dtos/__init__.py +0 -7
- hexcore-1.7.0/hexcore/application/dtos/__init__.pyi +0 -3
- hexcore-1.7.0/hexcore/application/dtos/base.pyi +0 -4
- hexcore-1.7.0/hexcore/application/use_cases/__init__.py +0 -7
- hexcore-1.7.0/hexcore/application/use_cases/__init__.pyi +0 -3
- hexcore-1.7.0/hexcore/application/use_cases/base.pyi +0 -11
- hexcore-1.7.0/hexcore/config.pyi +0 -35
- hexcore-1.7.0/hexcore/domain/auth/__init__.pyi +0 -4
- hexcore-1.7.0/hexcore/domain/auth/permissions.pyi +0 -10
- hexcore-1.7.0/hexcore/domain/auth/value_objects.pyi +0 -11
- hexcore-1.7.0/hexcore/domain/base.pyi +0 -19
- hexcore-1.7.0/hexcore/domain/events.pyi +0 -35
- hexcore-1.7.0/hexcore/domain/exceptions.pyi +0 -2
- hexcore-1.7.0/hexcore/domain/repositories.pyi +0 -22
- hexcore-1.7.0/hexcore/domain/services.py +0 -11
- hexcore-1.7.0/hexcore/domain/services.pyi +0 -8
- hexcore-1.7.0/hexcore/domain/uow.pyi +0 -24
- hexcore-1.7.0/hexcore/infrastructure/__init__.pyi +0 -0
- hexcore-1.7.0/hexcore/infrastructure/api/__init__.pyi +0 -0
- hexcore-1.7.0/hexcore/infrastructure/api/utils.py +0 -20
- hexcore-1.7.0/hexcore/infrastructure/api/utils.pyi +0 -9
- hexcore-1.7.0/hexcore/infrastructure/cache/__init__.pyi +0 -10
- hexcore-1.7.0/hexcore/infrastructure/cache/cache_backends/__init__.pyi +0 -0
- hexcore-1.7.0/hexcore/infrastructure/cache/cache_backends/memory.pyi +0 -10
- hexcore-1.7.0/hexcore/infrastructure/cache/cache_backends/redis.pyi +0 -12
- hexcore-1.7.0/hexcore/infrastructure/cli.pyi +0 -21
- hexcore-1.7.0/hexcore/infrastructure/events/__init__.pyi +0 -0
- hexcore-1.7.0/hexcore/infrastructure/events/events_backends/__init__.pyi +0 -0
- hexcore-1.7.0/hexcore/infrastructure/events/events_backends/memory.pyi +0 -8
- hexcore-1.7.0/hexcore/infrastructure/repositories/__init__.pyi +0 -0
- hexcore-1.7.0/hexcore/infrastructure/repositories/base.pyi +0 -17
- hexcore-1.7.0/hexcore/infrastructure/repositories/decorators.pyi +0 -5
- hexcore-1.7.0/hexcore/infrastructure/repositories/implementations.pyi +0 -52
- hexcore-1.7.0/hexcore/infrastructure/repositories/orms/__init__.pyi +0 -0
- hexcore-1.7.0/hexcore/infrastructure/repositories/orms/beanie/__init__.pyi +0 -14
- hexcore-1.7.0/hexcore/infrastructure/repositories/orms/beanie/utils.py +0 -141
- hexcore-1.7.0/hexcore/infrastructure/repositories/orms/beanie/utils.pyi +0 -29
- hexcore-1.7.0/hexcore/infrastructure/repositories/orms/sqlalchemy/__init__.pyi +0 -19
- hexcore-1.7.0/hexcore/infrastructure/repositories/orms/sqlalchemy/session.pyi +0 -9
- hexcore-1.7.0/hexcore/infrastructure/repositories/orms/sqlalchemy/utils.pyi +0 -45
- hexcore-1.7.0/hexcore/infrastructure/repositories/utils.pyi +0 -14
- hexcore-1.7.0/hexcore/infrastructure/uow/__init__.pyi +0 -32
- hexcore-1.7.0/hexcore/infrastructure/uow/decorators.pyi +0 -4
- hexcore-1.7.0/hexcore/infrastructure/uow/helpers.pyi +0 -7
- hexcore-1.7.0/hexcore/types.pyi +0 -37
- hexcore-1.7.0/scripts/__init__.py +0 -0
- hexcore-1.7.0/scripts/fix_pyi_files.py +0 -32
- hexcore-1.7.0/scripts/fix_types_pyi_aliases.py +0 -8
- hexcore-1.7.0/scripts/main.py +0 -38
- {hexcore-1.7.0 → hexcore-2.0.0}/LICENSE +0 -0
- {hexcore-1.7.0 → hexcore-2.0.0}/hexcore/__init__.py +0 -0
- {hexcore-1.7.0 → hexcore-2.0.0}/hexcore/__main__.py +0 -0
- {hexcore-1.7.0 → hexcore-2.0.0}/hexcore/application/__init__.py +0 -0
- {hexcore-1.7.0 → hexcore-2.0.0}/hexcore/application/dtos/base.py +0 -0
- {hexcore-1.7.0 → hexcore-2.0.0}/hexcore/application/use_cases/base.py +0 -0
- {hexcore-1.7.0 → hexcore-2.0.0}/hexcore/domain/__init__.py +0 -0
- {hexcore-1.7.0 → hexcore-2.0.0}/hexcore/domain/auth/__init__.py +0 -0
- {hexcore-1.7.0 → hexcore-2.0.0}/hexcore/domain/auth/permissions.py +0 -0
- {hexcore-1.7.0 → hexcore-2.0.0}/hexcore/domain/auth/value_objects.py +0 -0
- {hexcore-1.7.0 → hexcore-2.0.0}/hexcore/domain/base.py +0 -0
- {hexcore-1.7.0 → hexcore-2.0.0}/hexcore/domain/events.py +0 -0
- {hexcore-1.7.0 → hexcore-2.0.0}/hexcore/domain/exceptions.py +0 -0
- {hexcore-1.7.0 → hexcore-2.0.0}/hexcore/domain/repositories.py +0 -0
- {hexcore-1.7.0 → hexcore-2.0.0}/hexcore/domain/uow.py +0 -0
- {hexcore-1.7.0 → hexcore-2.0.0}/hexcore/infrastructure/__init__.py +0 -0
- {hexcore-1.7.0 → hexcore-2.0.0}/hexcore/infrastructure/cache/__init__.py +0 -0
- {hexcore-1.7.0/hexcore/infrastructure/api → hexcore-2.0.0/hexcore/infrastructure/cache/cache_backends}/__init__.py +0 -0
- {hexcore-1.7.0 → hexcore-2.0.0}/hexcore/infrastructure/cache/cache_backends/memory.py +0 -0
- {hexcore-1.7.0 → hexcore-2.0.0}/hexcore/infrastructure/cache/cache_backends/redis.py +0 -0
- {hexcore-1.7.0/hexcore/infrastructure/cache/cache_backends → hexcore-2.0.0/hexcore/infrastructure/events}/__init__.py +0 -0
- {hexcore-1.7.0/hexcore/infrastructure/events → hexcore-2.0.0/hexcore/infrastructure/events/events_backends}/__init__.py +0 -0
- {hexcore-1.7.0 → hexcore-2.0.0}/hexcore/infrastructure/events/events_backends/memory.py +0 -0
- {hexcore-1.7.0/hexcore/infrastructure/events/events_backends → hexcore-2.0.0/hexcore/infrastructure/repositories}/__init__.py +0 -0
- {hexcore-1.7.0 → hexcore-2.0.0}/hexcore/infrastructure/repositories/base.py +0 -0
- {hexcore-1.7.0 → hexcore-2.0.0}/hexcore/infrastructure/repositories/decorators.py +0 -0
- {hexcore-1.7.0/hexcore/infrastructure/repositories → hexcore-2.0.0/hexcore/infrastructure/repositories/orms}/__init__.py +0 -0
- {hexcore-1.7.0 → hexcore-2.0.0}/hexcore/infrastructure/repositories/orms/beanie/__init__.py +0 -0
- {hexcore-1.7.0 → hexcore-2.0.0}/hexcore/infrastructure/repositories/orms/sqlalchemy/__init__.py +0 -0
- {hexcore-1.7.0 → hexcore-2.0.0}/hexcore/infrastructure/uow/decorators.py +0 -0
- {hexcore-1.7.0 → hexcore-2.0.0}/hexcore/infrastructure/uow/helpers.py +0 -0
- /hexcore-1.7.0/hexcore/domain/__init__.pyi → /hexcore-2.0.0/hexcore/py.typed +0 -0
- {hexcore-1.7.0 → hexcore-2.0.0}/hexcore/types.py +0 -0
- {hexcore-1.7.0 → hexcore-2.0.0}/hexcore.egg-info/dependency_links.txt +0 -0
- {hexcore-1.7.0 → hexcore-2.0.0}/hexcore.egg-info/entry_points.txt +0 -0
- {hexcore-1.7.0 → hexcore-2.0.0}/hexcore.egg-info/requires.txt +0 -0
- {hexcore-1.7.0 → hexcore-2.0.0}/hexcore.egg-info/top_level.txt +0 -0
- {hexcore-1.7.0/hexcore/infrastructure/repositories/orms → hexcore-2.0.0/scripts}/__init__.py +0 -0
- {hexcore-1.7.0 → hexcore-2.0.0}/setup.cfg +0 -0
- {hexcore-1.7.0 → hexcore-2.0.0}/tests/test_basic.py +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
recursive-include hexcore *.pyi py.typed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: hexcore
|
|
3
|
-
Version:
|
|
3
|
+
Version: 2.0.0
|
|
4
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
5
|
Author-email: "David Latosefki (Indroic)" <indroic@outlook.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -49,6 +49,57 @@ Este repositorio cuenta con un conjunto de skills adicionales para extender y pe
|
|
|
49
49
|
pip install hexcore
|
|
50
50
|
```
|
|
51
51
|
|
|
52
|
+
## Templates de Proyecto (CLI)
|
|
53
|
+
|
|
54
|
+
HexCore incluye templates base para bootstrap de proyectos:
|
|
55
|
+
|
|
56
|
+
```sh
|
|
57
|
+
hexcore init mi_proyecto --template hexagonal
|
|
58
|
+
hexcore init mi_proyecto --template vertical-slice
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
- `hexagonal`: crea `src/domain`, `src/application`, `src/infrastructure`.
|
|
62
|
+
- `vertical-slice`: crea `src/features`, `src/shared/domain`, `src/shared/application`, `src/shared/infrastructure`.
|
|
63
|
+
|
|
64
|
+
En ambos templates se generan:
|
|
65
|
+
- `config.py` en raíz con `repository_discovery_paths` de ejemplo.
|
|
66
|
+
- estructura de migraciones con Alembic.
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## Configuración v2 (Folder-Agnostic)
|
|
71
|
+
|
|
72
|
+
Desde v2, HexCore usa configuración explícita y no aplica fallback implícito para descubrir repositorios.
|
|
73
|
+
|
|
74
|
+
### 1. Configuración visible en raíz
|
|
75
|
+
|
|
76
|
+
Define un archivo `config.py` en la raíz del proyecto:
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
from hexcore.config import ServerConfig
|
|
80
|
+
|
|
81
|
+
config = ServerConfig(
|
|
82
|
+
repository_discovery_paths={
|
|
83
|
+
"myapp.features.users.infrastructure.repositories",
|
|
84
|
+
"myapp.features.billing.infrastructure.repositories",
|
|
85
|
+
}
|
|
86
|
+
)
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### 2. Prioridad para cargar configuración
|
|
90
|
+
|
|
91
|
+
`LazyConfig` resuelve módulos en este orden:
|
|
92
|
+
|
|
93
|
+
1. `HEXCORE_CONFIG_MODULE`
|
|
94
|
+
2. `HEXCORE_CONFIG_MODULES` (lista separada por comas)
|
|
95
|
+
3. módulos configurados por `LazyConfig.set_config_modules(...)`
|
|
96
|
+
4. `config` por defecto (raíz del proyecto)
|
|
97
|
+
|
|
98
|
+
### 3. Regla de discovery en v2
|
|
99
|
+
|
|
100
|
+
- Si `repository_discovery_paths` está vacío, no se cargan módulos de repositorios.
|
|
101
|
+
- UoW falla con error explícito para evitar comportamiento ambiguo.
|
|
102
|
+
|
|
52
103
|
---
|
|
53
104
|
|
|
54
105
|
## Pautas de Colaboración
|
|
@@ -26,6 +26,57 @@ Este repositorio cuenta con un conjunto de skills adicionales para extender y pe
|
|
|
26
26
|
pip install hexcore
|
|
27
27
|
```
|
|
28
28
|
|
|
29
|
+
## Templates de Proyecto (CLI)
|
|
30
|
+
|
|
31
|
+
HexCore incluye templates base para bootstrap de proyectos:
|
|
32
|
+
|
|
33
|
+
```sh
|
|
34
|
+
hexcore init mi_proyecto --template hexagonal
|
|
35
|
+
hexcore init mi_proyecto --template vertical-slice
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
- `hexagonal`: crea `src/domain`, `src/application`, `src/infrastructure`.
|
|
39
|
+
- `vertical-slice`: crea `src/features`, `src/shared/domain`, `src/shared/application`, `src/shared/infrastructure`.
|
|
40
|
+
|
|
41
|
+
En ambos templates se generan:
|
|
42
|
+
- `config.py` en raíz con `repository_discovery_paths` de ejemplo.
|
|
43
|
+
- estructura de migraciones con Alembic.
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## Configuración v2 (Folder-Agnostic)
|
|
48
|
+
|
|
49
|
+
Desde v2, HexCore usa configuración explícita y no aplica fallback implícito para descubrir repositorios.
|
|
50
|
+
|
|
51
|
+
### 1. Configuración visible en raíz
|
|
52
|
+
|
|
53
|
+
Define un archivo `config.py` en la raíz del proyecto:
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
from hexcore.config import ServerConfig
|
|
57
|
+
|
|
58
|
+
config = ServerConfig(
|
|
59
|
+
repository_discovery_paths={
|
|
60
|
+
"myapp.features.users.infrastructure.repositories",
|
|
61
|
+
"myapp.features.billing.infrastructure.repositories",
|
|
62
|
+
}
|
|
63
|
+
)
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### 2. Prioridad para cargar configuración
|
|
67
|
+
|
|
68
|
+
`LazyConfig` resuelve módulos en este orden:
|
|
69
|
+
|
|
70
|
+
1. `HEXCORE_CONFIG_MODULE`
|
|
71
|
+
2. `HEXCORE_CONFIG_MODULES` (lista separada por comas)
|
|
72
|
+
3. módulos configurados por `LazyConfig.set_config_modules(...)`
|
|
73
|
+
4. `config` por defecto (raíz del proyecto)
|
|
74
|
+
|
|
75
|
+
### 3. Regla de discovery en v2
|
|
76
|
+
|
|
77
|
+
- Si `repository_discovery_paths` está vacío, no se cargan módulos de repositorios.
|
|
78
|
+
- UoW falla con error explícito para evitar comportamiento ambiguo.
|
|
79
|
+
|
|
29
80
|
---
|
|
30
81
|
|
|
31
82
|
## Pautas de Colaboración
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Submódulo de DTOs base para la aplicación.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from .base import DTO
|
|
6
|
+
from .query import (
|
|
7
|
+
FilterConditionDTO,
|
|
8
|
+
FilterOperator,
|
|
9
|
+
QueryRequestDTO,
|
|
10
|
+
QueryResponseDTO,
|
|
11
|
+
SortConditionDTO,
|
|
12
|
+
SortDirection,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"DTO",
|
|
17
|
+
"FilterOperator",
|
|
18
|
+
"SortDirection",
|
|
19
|
+
"FilterConditionDTO",
|
|
20
|
+
"SortConditionDTO",
|
|
21
|
+
"QueryRequestDTO",
|
|
22
|
+
"QueryResponseDTO",
|
|
23
|
+
]
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import enum
|
|
2
|
+
import typing as t
|
|
3
|
+
|
|
4
|
+
from pydantic import Field
|
|
5
|
+
|
|
6
|
+
from .base import DTO
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class FilterOperator(str, enum.Enum):
|
|
10
|
+
EQ = "eq"
|
|
11
|
+
NE = "ne"
|
|
12
|
+
GT = "gt"
|
|
13
|
+
GTE = "gte"
|
|
14
|
+
LT = "lt"
|
|
15
|
+
LTE = "lte"
|
|
16
|
+
IN = "in"
|
|
17
|
+
NOT_IN = "not_in"
|
|
18
|
+
CONTAINS = "contains"
|
|
19
|
+
STARTSWITH = "startswith"
|
|
20
|
+
ENDSWITH = "endswith"
|
|
21
|
+
IS_NULL = "is_null"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class SortDirection(str, enum.Enum):
|
|
25
|
+
ASC = "asc"
|
|
26
|
+
DESC = "desc"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class FilterConditionDTO(DTO):
|
|
30
|
+
field: str = Field(min_length=1)
|
|
31
|
+
operator: FilterOperator = FilterOperator.EQ
|
|
32
|
+
value: t.Any = None
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class SortConditionDTO(DTO):
|
|
36
|
+
field: str = Field(min_length=1)
|
|
37
|
+
direction: SortDirection = SortDirection.ASC
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _empty_search_fields() -> list[str]:
|
|
41
|
+
return []
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _empty_filters() -> list[FilterConditionDTO]:
|
|
45
|
+
return []
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _empty_sort() -> list[SortConditionDTO]:
|
|
49
|
+
return []
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class QueryRequestDTO(DTO):
|
|
53
|
+
limit: int = Field(default=50, ge=1)
|
|
54
|
+
offset: int = Field(default=0, ge=0)
|
|
55
|
+
search: str | None = None
|
|
56
|
+
search_fields: list[str] = Field(default_factory=_empty_search_fields)
|
|
57
|
+
filters: list[FilterConditionDTO] = Field(default_factory=_empty_filters)
|
|
58
|
+
sort: list[SortConditionDTO] = Field(default_factory=_empty_sort)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class QueryResponseDTO(DTO):
|
|
62
|
+
items: list[t.Any] = Field(default_factory=list)
|
|
63
|
+
total: int = 0
|
|
64
|
+
limit: int = 0
|
|
65
|
+
offset: int = 0
|
|
66
|
+
has_next: bool = False
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Submódulo de casos de uso base para la aplicación.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from .base import UseCase
|
|
6
|
+
from .query import ListEntitiesUseCase, QueryEntitiesUseCase, SearchEntitiesUseCase
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"UseCase",
|
|
10
|
+
"QueryEntitiesUseCase",
|
|
11
|
+
"ListEntitiesUseCase",
|
|
12
|
+
"SearchEntitiesUseCase",
|
|
13
|
+
]
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import typing as t
|
|
4
|
+
|
|
5
|
+
from hexcore.application.dtos.query import QueryRequestDTO, QueryResponseDTO
|
|
6
|
+
from hexcore.domain.base import BaseEntity
|
|
7
|
+
from hexcore.domain.repositories import IBaseRepository
|
|
8
|
+
from hexcore.domain.services import BaseDomainService
|
|
9
|
+
|
|
10
|
+
from .base import UseCase
|
|
11
|
+
|
|
12
|
+
T = t.TypeVar("T", bound=BaseEntity)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class QueryEntitiesUseCase(UseCase[QueryRequestDTO, QueryResponseDTO], t.Generic[T]):
|
|
16
|
+
def __init__(
|
|
17
|
+
self,
|
|
18
|
+
repository: IBaseRepository[T],
|
|
19
|
+
service: BaseDomainService | None = None,
|
|
20
|
+
) -> None:
|
|
21
|
+
self.repository = repository
|
|
22
|
+
self.service = service or BaseDomainService()
|
|
23
|
+
|
|
24
|
+
async def execute(self, command: QueryRequestDTO) -> QueryResponseDTO:
|
|
25
|
+
return await self.service.list_entities(self.repository, command)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ListEntitiesUseCase(QueryEntitiesUseCase[T]):
|
|
29
|
+
async def execute(self, command: QueryRequestDTO) -> QueryResponseDTO:
|
|
30
|
+
command.search = None
|
|
31
|
+
command.search_fields = []
|
|
32
|
+
return await super().execute(command)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class SearchEntitiesUseCase(QueryEntitiesUseCase[T]):
|
|
36
|
+
async def execute(self, command: QueryRequestDTO) -> QueryResponseDTO:
|
|
37
|
+
return await super().execute(command)
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
import importlib
|
|
3
|
+
import os
|
|
3
4
|
import typing as t
|
|
4
|
-
from pydantic import BaseModel, ConfigDict
|
|
5
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
5
6
|
from pathlib import Path
|
|
6
7
|
from hexcore.infrastructure.cache import ICache
|
|
7
8
|
from hexcore.domain.events import IEventDispatcher
|
|
@@ -53,25 +54,74 @@ class ServerConfig(BaseModel):
|
|
|
53
54
|
# Event Dispatcher
|
|
54
55
|
event_dispatcher: IEventDispatcher = InMemoryEventDispatcher()
|
|
55
56
|
|
|
57
|
+
# Repository Discovery
|
|
58
|
+
# v2 (breaking): discovery explicito y folder-agnostic.
|
|
59
|
+
# Si se deja vacio, no se autoloadearan modulos de repositorios.
|
|
60
|
+
repository_discovery_paths: set[str] = Field(default_factory=set)
|
|
61
|
+
|
|
56
62
|
|
|
57
63
|
class LazyConfig:
|
|
58
64
|
"""
|
|
59
|
-
Loader de configuración flexible.
|
|
60
|
-
|
|
61
|
-
|
|
65
|
+
Loader de configuración flexible y agnóstico de estructura de carpetas.
|
|
66
|
+
|
|
67
|
+
Prioridad de resolución:
|
|
68
|
+
1) Variable de entorno HEXCORE_CONFIG_MODULE (módulo único)
|
|
69
|
+
2) Variable de entorno HEXCORE_CONFIG_MODULES (lista separada por comas)
|
|
70
|
+
3) Lista configurada por set_config_modules(...)
|
|
71
|
+
4) Valor por defecto: "config" (archivo config.py en la raíz del proyecto)
|
|
72
|
+
|
|
73
|
+
En cada módulo candidato se busca:
|
|
74
|
+
- atributo `config` (instancia o clase derivada de ServerConfig)
|
|
75
|
+
- o clase `ServerConfig` derivada de la base.
|
|
62
76
|
|
|
63
|
-
|
|
77
|
+
Si no se encuentra nada válido, usa ServerConfig() por defecto.
|
|
64
78
|
|
|
65
79
|
"""
|
|
66
80
|
|
|
67
81
|
_imported_config: t.Optional[ServerConfig] = None
|
|
82
|
+
_config_modules: tuple[str, ...] = ("config",)
|
|
83
|
+
|
|
84
|
+
@classmethod
|
|
85
|
+
def set_config_modules(cls, modules: t.Iterable[str]) -> None:
|
|
86
|
+
"""Define módulos candidatos para resolver configuración personalizada."""
|
|
87
|
+
normalized_modules = tuple(
|
|
88
|
+
module_name.strip() for module_name in modules if str(module_name).strip()
|
|
89
|
+
)
|
|
90
|
+
cls._config_modules = normalized_modules
|
|
91
|
+
cls._imported_config = None
|
|
92
|
+
|
|
93
|
+
@classmethod
|
|
94
|
+
def clear_cache(cls) -> None:
|
|
95
|
+
"""Limpia la configuración cacheada para forzar nueva resolución."""
|
|
96
|
+
cls._imported_config = None
|
|
97
|
+
|
|
98
|
+
@classmethod
|
|
99
|
+
def _iter_config_module_candidates(cls) -> tuple[str, ...]:
|
|
100
|
+
env_single_module = os.getenv("HEXCORE_CONFIG_MODULE", "").strip()
|
|
101
|
+
if env_single_module:
|
|
102
|
+
return (env_single_module,)
|
|
103
|
+
|
|
104
|
+
env_modules_raw = os.getenv("HEXCORE_CONFIG_MODULES", "").strip()
|
|
105
|
+
if env_modules_raw:
|
|
106
|
+
env_modules = tuple(
|
|
107
|
+
module_name.strip()
|
|
108
|
+
for module_name in env_modules_raw.split(",")
|
|
109
|
+
if module_name.strip()
|
|
110
|
+
)
|
|
111
|
+
if env_modules:
|
|
112
|
+
return env_modules
|
|
113
|
+
|
|
114
|
+
if cls._config_modules:
|
|
115
|
+
return cls._config_modules
|
|
116
|
+
|
|
117
|
+
return ("config",)
|
|
68
118
|
|
|
69
119
|
@classmethod
|
|
70
120
|
def get_config(cls) -> ServerConfig:
|
|
71
121
|
if cls._imported_config is not None:
|
|
72
122
|
return cls._imported_config
|
|
73
123
|
# Intenta importar la config personalizada
|
|
74
|
-
for modpath in (
|
|
124
|
+
for modpath in cls._iter_config_module_candidates():
|
|
75
125
|
try:
|
|
76
126
|
mod = importlib.import_module(modpath)
|
|
77
127
|
config_instance = getattr(mod, "config", None)
|
|
@@ -99,4 +149,4 @@ class LazyConfig:
|
|
|
99
149
|
return cls._imported_config
|
|
100
150
|
|
|
101
151
|
|
|
102
|
-
# Esto es solo para disparar el Workflow pra subir la ultima version a PyPI, por favor ignora este comentario
|
|
152
|
+
# Esto es solo para disparar el Workflow pra subir la ultima version a PyPI, por favor ignora este comentario
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import datetime as dt
|
|
4
|
+
import typing as t
|
|
5
|
+
|
|
6
|
+
from hexcore.application.dtos.query import (
|
|
7
|
+
FilterConditionDTO,
|
|
8
|
+
FilterOperator,
|
|
9
|
+
QueryRequestDTO,
|
|
10
|
+
QueryResponseDTO,
|
|
11
|
+
SortConditionDTO,
|
|
12
|
+
SortDirection,
|
|
13
|
+
)
|
|
14
|
+
from hexcore.domain.base import BaseEntity
|
|
15
|
+
from hexcore.domain.events import IEventDispatcher
|
|
16
|
+
from hexcore.domain.repositories import IBaseRepository
|
|
17
|
+
from hexcore.config import LazyConfig
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
T = t.TypeVar("T", bound=BaseEntity)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class BaseDomainService:
|
|
24
|
+
def __init__(
|
|
25
|
+
self,
|
|
26
|
+
event_dispatcher: IEventDispatcher = LazyConfig().get_config().event_dispatcher,
|
|
27
|
+
) -> None:
|
|
28
|
+
self.config = LazyConfig.get_config()
|
|
29
|
+
self.event_dispatcher = event_dispatcher
|
|
30
|
+
|
|
31
|
+
async def list_entities(
|
|
32
|
+
self,
|
|
33
|
+
repository: IBaseRepository[T],
|
|
34
|
+
query: QueryRequestDTO,
|
|
35
|
+
) -> QueryResponseDTO:
|
|
36
|
+
query_all = getattr(repository, "query_all", None)
|
|
37
|
+
if callable(query_all):
|
|
38
|
+
entities, total = await t.cast(
|
|
39
|
+
t.Callable[[QueryRequestDTO], t.Awaitable[tuple[list[T], int]]],
|
|
40
|
+
query_all,
|
|
41
|
+
)(query)
|
|
42
|
+
has_next = query.offset + len(entities) < total
|
|
43
|
+
return QueryResponseDTO(
|
|
44
|
+
items=entities,
|
|
45
|
+
total=total,
|
|
46
|
+
limit=query.limit,
|
|
47
|
+
offset=query.offset,
|
|
48
|
+
has_next=has_next,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
entities = await repository.list_all(limit=None, offset=0)
|
|
52
|
+
return self.query_entities(entities, query)
|
|
53
|
+
|
|
54
|
+
def query_entities(
|
|
55
|
+
self,
|
|
56
|
+
entities: t.Sequence[T],
|
|
57
|
+
query: QueryRequestDTO,
|
|
58
|
+
) -> QueryResponseDTO:
|
|
59
|
+
results: list[T] = list(entities)
|
|
60
|
+
results = self._apply_search(results, query.search, query.search_fields)
|
|
61
|
+
results = self._apply_filters(results, query.filters)
|
|
62
|
+
results = self._apply_sort(results, query.sort)
|
|
63
|
+
|
|
64
|
+
total = len(results)
|
|
65
|
+
paginated = results[query.offset : query.offset + query.limit]
|
|
66
|
+
has_next = query.offset + len(paginated) < total
|
|
67
|
+
|
|
68
|
+
return QueryResponseDTO(
|
|
69
|
+
items=paginated,
|
|
70
|
+
total=total,
|
|
71
|
+
limit=query.limit,
|
|
72
|
+
offset=query.offset,
|
|
73
|
+
has_next=has_next,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
def _apply_search(
|
|
77
|
+
self,
|
|
78
|
+
entities: list[T],
|
|
79
|
+
search_text: str | None,
|
|
80
|
+
search_fields: list[str],
|
|
81
|
+
) -> list[T]:
|
|
82
|
+
if not search_text:
|
|
83
|
+
return entities
|
|
84
|
+
|
|
85
|
+
normalized_query = search_text.lower().strip()
|
|
86
|
+
if not normalized_query:
|
|
87
|
+
return entities
|
|
88
|
+
|
|
89
|
+
fields = search_fields or self._infer_search_fields(entities)
|
|
90
|
+
if not fields:
|
|
91
|
+
return entities
|
|
92
|
+
|
|
93
|
+
matched: list[T] = []
|
|
94
|
+
for entity in entities:
|
|
95
|
+
for field_name in fields:
|
|
96
|
+
value = getattr(entity, field_name, None)
|
|
97
|
+
if value is None:
|
|
98
|
+
continue
|
|
99
|
+
if normalized_query in str(value).lower():
|
|
100
|
+
matched.append(entity)
|
|
101
|
+
break
|
|
102
|
+
return matched
|
|
103
|
+
|
|
104
|
+
def _infer_search_fields(self, entities: list[T]) -> list[str]:
|
|
105
|
+
if not entities:
|
|
106
|
+
return []
|
|
107
|
+
|
|
108
|
+
sample = entities[0]
|
|
109
|
+
sample_dict = sample.model_dump()
|
|
110
|
+
return [
|
|
111
|
+
field_name
|
|
112
|
+
for field_name, value in sample_dict.items()
|
|
113
|
+
if isinstance(value, (str, int, float, bool, dt.date, dt.datetime))
|
|
114
|
+
]
|
|
115
|
+
|
|
116
|
+
def _apply_filters(
|
|
117
|
+
self,
|
|
118
|
+
entities: list[T],
|
|
119
|
+
filters: list[FilterConditionDTO],
|
|
120
|
+
) -> list[T]:
|
|
121
|
+
if not filters:
|
|
122
|
+
return entities
|
|
123
|
+
|
|
124
|
+
return [
|
|
125
|
+
entity
|
|
126
|
+
for entity in entities
|
|
127
|
+
if all(self._matches_filter(entity, condition) for condition in filters)
|
|
128
|
+
]
|
|
129
|
+
|
|
130
|
+
def _matches_filter(
|
|
131
|
+
self,
|
|
132
|
+
entity: BaseEntity,
|
|
133
|
+
condition: FilterConditionDTO,
|
|
134
|
+
) -> bool:
|
|
135
|
+
value = getattr(entity, condition.field, None)
|
|
136
|
+
operator = condition.operator
|
|
137
|
+
expected = condition.value
|
|
138
|
+
|
|
139
|
+
if operator == FilterOperator.IS_NULL:
|
|
140
|
+
return value is None
|
|
141
|
+
|
|
142
|
+
if operator == FilterOperator.EQ:
|
|
143
|
+
return value == expected
|
|
144
|
+
if operator == FilterOperator.NE:
|
|
145
|
+
return value != expected
|
|
146
|
+
if operator == FilterOperator.GT:
|
|
147
|
+
return self._safe_compare(value, expected, lambda a, b: a > b)
|
|
148
|
+
if operator == FilterOperator.GTE:
|
|
149
|
+
return self._safe_compare(value, expected, lambda a, b: a >= b)
|
|
150
|
+
if operator == FilterOperator.LT:
|
|
151
|
+
return self._safe_compare(value, expected, lambda a, b: a < b)
|
|
152
|
+
if operator == FilterOperator.LTE:
|
|
153
|
+
return self._safe_compare(value, expected, lambda a, b: a <= b)
|
|
154
|
+
if operator == FilterOperator.IN:
|
|
155
|
+
return self._in_operator(value, expected)
|
|
156
|
+
if operator == FilterOperator.NOT_IN:
|
|
157
|
+
return not self._in_operator(value, expected)
|
|
158
|
+
if operator == FilterOperator.CONTAINS:
|
|
159
|
+
return self._text_operator(value, expected, mode="contains")
|
|
160
|
+
if operator == FilterOperator.STARTSWITH:
|
|
161
|
+
return self._text_operator(value, expected, mode="startswith")
|
|
162
|
+
if operator == FilterOperator.ENDSWITH:
|
|
163
|
+
return self._text_operator(value, expected, mode="endswith")
|
|
164
|
+
|
|
165
|
+
return False
|
|
166
|
+
|
|
167
|
+
def _safe_compare(
|
|
168
|
+
self,
|
|
169
|
+
left: t.Any,
|
|
170
|
+
right: t.Any,
|
|
171
|
+
comparator: t.Callable[[t.Any, t.Any], bool],
|
|
172
|
+
) -> bool:
|
|
173
|
+
if left is None or right is None:
|
|
174
|
+
return False
|
|
175
|
+
try:
|
|
176
|
+
return comparator(left, right)
|
|
177
|
+
except TypeError:
|
|
178
|
+
return False
|
|
179
|
+
|
|
180
|
+
def _in_operator(self, value: t.Any, expected: t.Any) -> bool:
|
|
181
|
+
if expected is None:
|
|
182
|
+
return False
|
|
183
|
+
if isinstance(expected, (str, bytes)):
|
|
184
|
+
return value == expected
|
|
185
|
+
if not isinstance(expected, (list, tuple, set, frozenset)):
|
|
186
|
+
return False
|
|
187
|
+
return value in expected
|
|
188
|
+
|
|
189
|
+
def _text_operator(self, value: t.Any, expected: t.Any, mode: str) -> bool:
|
|
190
|
+
if value is None or expected is None:
|
|
191
|
+
return False
|
|
192
|
+
left = str(value).lower()
|
|
193
|
+
right = str(expected).lower()
|
|
194
|
+
if mode == "contains":
|
|
195
|
+
return right in left
|
|
196
|
+
if mode == "startswith":
|
|
197
|
+
return left.startswith(right)
|
|
198
|
+
if mode == "endswith":
|
|
199
|
+
return left.endswith(right)
|
|
200
|
+
return False
|
|
201
|
+
|
|
202
|
+
def _apply_sort(
|
|
203
|
+
self,
|
|
204
|
+
entities: list[T],
|
|
205
|
+
sort_conditions: list[SortConditionDTO],
|
|
206
|
+
) -> list[T]:
|
|
207
|
+
if not sort_conditions:
|
|
208
|
+
return entities
|
|
209
|
+
|
|
210
|
+
ordered = list(entities)
|
|
211
|
+
for condition in reversed(sort_conditions):
|
|
212
|
+
reverse = condition.direction == SortDirection.DESC
|
|
213
|
+
ordered.sort(
|
|
214
|
+
key=lambda item: self._safe_sort_value(getattr(item, condition.field, None)),
|
|
215
|
+
reverse=reverse,
|
|
216
|
+
)
|
|
217
|
+
return ordered
|
|
218
|
+
|
|
219
|
+
def _safe_sort_value(self, value: t.Any) -> tuple[int, int, t.Any]:
|
|
220
|
+
if value is None:
|
|
221
|
+
return (1, 0, None)
|
|
222
|
+
if isinstance(value, bool):
|
|
223
|
+
return (0, 0, int(value))
|
|
224
|
+
if isinstance(value, (int, float)):
|
|
225
|
+
return (0, 1, float(value))
|
|
226
|
+
if isinstance(value, dt.datetime):
|
|
227
|
+
return (0, 2, value.timestamp())
|
|
228
|
+
if isinstance(value, dt.date):
|
|
229
|
+
return (0, 3, value.toordinal())
|
|
230
|
+
if isinstance(value, str):
|
|
231
|
+
return (0, 4, value.casefold())
|
|
232
|
+
return (0, 5, str(value))
|