fastapi-basekit 0.4.0__tar.gz → 0.4.3__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.
- {fastapi_basekit-0.4.0 → fastapi_basekit-0.4.3}/PKG-INFO +1 -1
- {fastapi_basekit-0.4.0 → fastapi_basekit-0.4.3}/fastapi_basekit/aio/beanie/repository/base.py +63 -2
- {fastapi_basekit-0.4.0 → fastapi_basekit-0.4.3}/fastapi_basekit/aio/beanie/service/base.py +8 -0
- {fastapi_basekit-0.4.0 → fastapi_basekit-0.4.3}/fastapi_basekit/aio/controller/base.py +0 -1
- {fastapi_basekit-0.4.0 → fastapi_basekit-0.4.3}/fastapi_basekit/aio/sqlalchemy/controller/base.py +0 -1
- {fastapi_basekit-0.4.0 → fastapi_basekit-0.4.3}/fastapi_basekit/aio/sqlalchemy/repository/base.py +12 -1
- {fastapi_basekit-0.4.0 → fastapi_basekit-0.4.3}/fastapi_basekit/aio/sqlalchemy/service/base.py +58 -1
- {fastapi_basekit-0.4.0 → fastapi_basekit-0.4.3}/fastapi_basekit.egg-info/PKG-INFO +1 -1
- {fastapi_basekit-0.4.0 → fastapi_basekit-0.4.3}/fastapi_basekit.egg-info/SOURCES.txt +2 -0
- {fastapi_basekit-0.4.0 → fastapi_basekit-0.4.3}/pyproject.toml +1 -1
- {fastapi_basekit-0.4.0 → fastapi_basekit-0.4.3}/tests/test_action_param_leak.py +16 -0
- fastapi_basekit-0.4.3/tests/test_beanie_keyset_pagination.py +75 -0
- fastapi_basekit-0.4.3/tests/test_beanie_service_mutable_defaults.py +29 -0
- {fastapi_basekit-0.4.0 → fastapi_basekit-0.4.3}/LICENSE +0 -0
- {fastapi_basekit-0.4.0 → fastapi_basekit-0.4.3}/README.md +0 -0
- {fastapi_basekit-0.4.0 → fastapi_basekit-0.4.3}/fastapi_basekit/__init__.py +0 -0
- {fastapi_basekit-0.4.0 → fastapi_basekit-0.4.3}/fastapi_basekit/aio/__init__.py +0 -0
- {fastapi_basekit-0.4.0 → fastapi_basekit-0.4.3}/fastapi_basekit/aio/beanie/__init__.py +0 -0
- {fastapi_basekit-0.4.0 → fastapi_basekit-0.4.3}/fastapi_basekit/aio/beanie/controller/__init__.py +0 -0
- {fastapi_basekit-0.4.0 → fastapi_basekit-0.4.3}/fastapi_basekit/aio/beanie/controller/base.py +0 -0
- {fastapi_basekit-0.4.0 → fastapi_basekit-0.4.3}/fastapi_basekit/aio/beanie/repository/__init__.py +0 -0
- {fastapi_basekit-0.4.0 → fastapi_basekit-0.4.3}/fastapi_basekit/aio/beanie/service/__init__.py +0 -0
- {fastapi_basekit-0.4.0 → fastapi_basekit-0.4.3}/fastapi_basekit/aio/controller/__init__.py +0 -0
- {fastapi_basekit-0.4.0 → fastapi_basekit-0.4.3}/fastapi_basekit/aio/permissions/__init__.py +0 -0
- {fastapi_basekit-0.4.0 → fastapi_basekit-0.4.3}/fastapi_basekit/aio/permissions/base.py +0 -0
- {fastapi_basekit-0.4.0 → fastapi_basekit-0.4.3}/fastapi_basekit/aio/sqlalchemy/__init__.py +0 -0
- {fastapi_basekit-0.4.0 → fastapi_basekit-0.4.3}/fastapi_basekit/aio/sqlalchemy/controller/__init__.py +0 -0
- {fastapi_basekit-0.4.0 → fastapi_basekit-0.4.3}/fastapi_basekit/aio/sqlalchemy/repository/__init__.py +0 -0
- {fastapi_basekit-0.4.0 → fastapi_basekit-0.4.3}/fastapi_basekit/aio/sqlalchemy/service/__init__.py +0 -0
- {fastapi_basekit-0.4.0 → fastapi_basekit-0.4.3}/fastapi_basekit/aio/sqlalchemy/session.py +0 -0
- {fastapi_basekit-0.4.0 → fastapi_basekit-0.4.3}/fastapi_basekit/aio/sqlmodel/__init__.py +0 -0
- {fastapi_basekit-0.4.0 → fastapi_basekit-0.4.3}/fastapi_basekit/aio/sqlmodel/controller/__init__.py +0 -0
- {fastapi_basekit-0.4.0 → fastapi_basekit-0.4.3}/fastapi_basekit/aio/sqlmodel/controller/base.py +0 -0
- {fastapi_basekit-0.4.0 → fastapi_basekit-0.4.3}/fastapi_basekit/aio/sqlmodel/repository/__init__.py +0 -0
- {fastapi_basekit-0.4.0 → fastapi_basekit-0.4.3}/fastapi_basekit/aio/sqlmodel/repository/base.py +0 -0
- {fastapi_basekit-0.4.0 → fastapi_basekit-0.4.3}/fastapi_basekit/aio/sqlmodel/service/__init__.py +0 -0
- {fastapi_basekit-0.4.0 → fastapi_basekit-0.4.3}/fastapi_basekit/aio/sqlmodel/service/base.py +0 -0
- {fastapi_basekit-0.4.0 → fastapi_basekit-0.4.3}/fastapi_basekit/cli/__init__.py +0 -0
- {fastapi_basekit-0.4.0 → fastapi_basekit-0.4.3}/fastapi_basekit/cli/main.py +0 -0
- {fastapi_basekit-0.4.0 → fastapi_basekit-0.4.3}/fastapi_basekit/exceptions/__init__.py +0 -0
- {fastapi_basekit-0.4.0 → fastapi_basekit-0.4.3}/fastapi_basekit/exceptions/api_exceptions.py +0 -0
- {fastapi_basekit-0.4.0 → fastapi_basekit-0.4.3}/fastapi_basekit/exceptions/domain.py +0 -0
- {fastapi_basekit-0.4.0 → fastapi_basekit-0.4.3}/fastapi_basekit/exceptions/handler.py +0 -0
- {fastapi_basekit-0.4.0 → fastapi_basekit-0.4.3}/fastapi_basekit/openapi.py +0 -0
- {fastapi_basekit-0.4.0 → fastapi_basekit-0.4.3}/fastapi_basekit/schema/__init__.py +0 -0
- {fastapi_basekit-0.4.0 → fastapi_basekit-0.4.3}/fastapi_basekit/schema/base.py +0 -0
- {fastapi_basekit-0.4.0 → fastapi_basekit-0.4.3}/fastapi_basekit/schema/jwt.py +0 -0
- {fastapi_basekit-0.4.0 → fastapi_basekit-0.4.3}/fastapi_basekit/schema/schema.py +0 -0
- {fastapi_basekit-0.4.0 → fastapi_basekit-0.4.3}/fastapi_basekit/servicios/__init__.py +0 -0
- {fastapi_basekit-0.4.0 → fastapi_basekit-0.4.3}/fastapi_basekit/servicios/thrid/__init__.py +0 -0
- {fastapi_basekit-0.4.0 → fastapi_basekit-0.4.3}/fastapi_basekit/servicios/thrid/jwt.py +0 -0
- {fastapi_basekit-0.4.0 → fastapi_basekit-0.4.3}/fastapi_basekit/templates/project/cookiecutter.json +0 -0
- {fastapi_basekit-0.4.0 → fastapi_basekit-0.4.3}/fastapi_basekit/templates/project/hooks/post_gen_project.py +0 -0
- {fastapi_basekit-0.4.0 → fastapi_basekit-0.4.3}/fastapi_basekit/templates/project/hooks/pre_gen_project.py +0 -0
- {fastapi_basekit-0.4.0 → fastapi_basekit-0.4.3}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/.env.example +0 -0
- {fastapi_basekit-0.4.0 → fastapi_basekit-0.4.3}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/.gitignore +0 -0
- {fastapi_basekit-0.4.0 → fastapi_basekit-0.4.3}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/Dockerfile +0 -0
- {fastapi_basekit-0.4.0 → fastapi_basekit-0.4.3}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/LICENSE +0 -0
- {fastapi_basekit-0.4.0 → fastapi_basekit-0.4.3}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/Makefile +0 -0
- {fastapi_basekit-0.4.0 → fastapi_basekit-0.4.3}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/README.md +0 -0
- {fastapi_basekit-0.4.0 → fastapi_basekit-0.4.3}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/alembic/env.py +0 -0
- {fastapi_basekit-0.4.0 → fastapi_basekit-0.4.3}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/alembic/script.py.mako +0 -0
- {fastapi_basekit-0.4.0 → fastapi_basekit-0.4.3}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/alembic.ini +0 -0
- {fastapi_basekit-0.4.0 → fastapi_basekit-0.4.3}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/__init__.py +0 -0
- {fastapi_basekit-0.4.0 → fastapi_basekit-0.4.3}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/api/__init__.py +0 -0
- {fastapi_basekit-0.4.0 → fastapi_basekit-0.4.3}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/api/v1/__init__.py +0 -0
- {fastapi_basekit-0.4.0 → fastapi_basekit-0.4.3}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/api/v1/endpoints/__init__.py +0 -0
- {fastapi_basekit-0.4.0 → fastapi_basekit-0.4.3}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/api/v1/endpoints/auth/__init__.py +0 -0
- {fastapi_basekit-0.4.0 → fastapi_basekit-0.4.3}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/api/v1/endpoints/auth/auth.py +0 -0
- {fastapi_basekit-0.4.0 → fastapi_basekit-0.4.3}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/api/v1/endpoints/user/__init__.py +0 -0
- {fastapi_basekit-0.4.0 → fastapi_basekit-0.4.3}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/api/v1/endpoints/user/user.py +0 -0
- {fastapi_basekit-0.4.0 → fastapi_basekit-0.4.3}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/api/v1/routers.py +0 -0
- {fastapi_basekit-0.4.0 → fastapi_basekit-0.4.3}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/config/__init__.py +0 -0
- {fastapi_basekit-0.4.0 → fastapi_basekit-0.4.3}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/config/database.py +0 -0
- {fastapi_basekit-0.4.0 → fastapi_basekit-0.4.3}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/config/settings.py +0 -0
- {fastapi_basekit-0.4.0 → fastapi_basekit-0.4.3}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/main.py +0 -0
- {fastapi_basekit-0.4.0 → fastapi_basekit-0.4.3}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/middleware/__init__.py +0 -0
- {fastapi_basekit-0.4.0 → fastapi_basekit-0.4.3}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/middleware/auth.py +0 -0
- {fastapi_basekit-0.4.0 → fastapi_basekit-0.4.3}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/middleware/permissions.py +0 -0
- {fastapi_basekit-0.4.0 → fastapi_basekit-0.4.3}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/models/__init__.py +0 -0
- {fastapi_basekit-0.4.0 → fastapi_basekit-0.4.3}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/models/auth.py +0 -0
- {fastapi_basekit-0.4.0 → fastapi_basekit-0.4.3}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/models/base.py +0 -0
- {fastapi_basekit-0.4.0 → fastapi_basekit-0.4.3}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/models/enums.py +0 -0
- {fastapi_basekit-0.4.0 → fastapi_basekit-0.4.3}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/models/types.py +0 -0
- {fastapi_basekit-0.4.0 → fastapi_basekit-0.4.3}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/permissions/__init__.py +0 -0
- {fastapi_basekit-0.4.0 → fastapi_basekit-0.4.3}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/permissions/user.py +0 -0
- {fastapi_basekit-0.4.0 → fastapi_basekit-0.4.3}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/repositories/__init__.py +0 -0
- {fastapi_basekit-0.4.0 → fastapi_basekit-0.4.3}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/repositories/user/__init__.py +0 -0
- {fastapi_basekit-0.4.0 → fastapi_basekit-0.4.3}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/repositories/user/repository.py +0 -0
- {fastapi_basekit-0.4.0 → fastapi_basekit-0.4.3}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/schemas/__init__.py +0 -0
- {fastapi_basekit-0.4.0 → fastapi_basekit-0.4.3}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/schemas/auth.py +0 -0
- {fastapi_basekit-0.4.0 → fastapi_basekit-0.4.3}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/schemas/base.py +0 -0
- {fastapi_basekit-0.4.0 → fastapi_basekit-0.4.3}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/schemas/user.py +0 -0
- {fastapi_basekit-0.4.0 → fastapi_basekit-0.4.3}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/scripts/__init__.py +0 -0
- {fastapi_basekit-0.4.0 → fastapi_basekit-0.4.3}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/scripts/init.py +0 -0
- {fastapi_basekit-0.4.0 → fastapi_basekit-0.4.3}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/scripts/init_admin.py +0 -0
- {fastapi_basekit-0.4.0 → fastapi_basekit-0.4.3}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/services/__init__.py +0 -0
- {fastapi_basekit-0.4.0 → fastapi_basekit-0.4.3}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/services/auth_service.py +0 -0
- {fastapi_basekit-0.4.0 → fastapi_basekit-0.4.3}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/services/dependency.py +0 -0
- {fastapi_basekit-0.4.0 → fastapi_basekit-0.4.3}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/services/user_service.py +0 -0
- {fastapi_basekit-0.4.0 → fastapi_basekit-0.4.3}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/utils/__init__.py +0 -0
- {fastapi_basekit-0.4.0 → fastapi_basekit-0.4.3}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/utils/exception_handlers.py +0 -0
- {fastapi_basekit-0.4.0 → fastapi_basekit-0.4.3}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/utils/security.py +0 -0
- {fastapi_basekit-0.4.0 → fastapi_basekit-0.4.3}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/docker-compose.yml +0 -0
- {fastapi_basekit-0.4.0 → fastapi_basekit-0.4.3}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/pytest.ini +0 -0
- {fastapi_basekit-0.4.0 → fastapi_basekit-0.4.3}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/requirements.txt +0 -0
- {fastapi_basekit-0.4.0 → fastapi_basekit-0.4.3}/fastapi_basekit.egg-info/dependency_links.txt +0 -0
- {fastapi_basekit-0.4.0 → fastapi_basekit-0.4.3}/fastapi_basekit.egg-info/entry_points.txt +0 -0
- {fastapi_basekit-0.4.0 → fastapi_basekit-0.4.3}/fastapi_basekit.egg-info/requires.txt +0 -0
- {fastapi_basekit-0.4.0 → fastapi_basekit-0.4.3}/fastapi_basekit.egg-info/top_level.txt +0 -0
- {fastapi_basekit-0.4.0 → fastapi_basekit-0.4.3}/setup.cfg +0 -0
- {fastapi_basekit-0.4.0 → fastapi_basekit-0.4.3}/tests/test_api_exceptions.py +0 -0
- {fastapi_basekit-0.4.0 → fastapi_basekit-0.4.3}/tests/test_base_response.py +0 -0
- {fastapi_basekit-0.4.0 → fastapi_basekit-0.4.3}/tests/test_base_service.py +0 -0
- {fastapi_basekit-0.4.0 → fastapi_basekit-0.4.3}/tests/test_beanie_aggregation_hooks.py +0 -0
- {fastapi_basekit-0.4.0 → fastapi_basekit-0.4.3}/tests/test_beanie_aggregation_integration.py +0 -0
- {fastapi_basekit-0.4.0 → fastapi_basekit-0.4.3}/tests/test_controller_auto_permissions.py +0 -0
- {fastapi_basekit-0.4.0 → fastapi_basekit-0.4.3}/tests/test_crud_beanie_controller.py +0 -0
- {fastapi_basekit-0.4.0 → fastapi_basekit-0.4.3}/tests/test_crud_controller.py +0 -0
- {fastapi_basekit-0.4.0 → fastapi_basekit-0.4.3}/tests/test_crud_sqlmodel_repository_service.py +0 -0
- {fastapi_basekit-0.4.0 → fastapi_basekit-0.4.3}/tests/test_exception_handlers.py +0 -0
- {fastapi_basekit-0.4.0 → fastapi_basekit-0.4.3}/tests/test_filter_operators.py +0 -0
- {fastapi_basekit-0.4.0 → fastapi_basekit-0.4.3}/tests/test_jwt_service.py +0 -0
- {fastapi_basekit-0.4.0 → fastapi_basekit-0.4.3}/tests/test_openapi_simplify.py +0 -0
- {fastapi_basekit-0.4.0 → fastapi_basekit-0.4.3}/tests/test_sql_queryset_override.py +0 -0
- {fastapi_basekit-0.4.0 → fastapi_basekit-0.4.3}/tests/test_sqlalchemy_base_service_order.py +0 -0
{fastapi_basekit-0.4.0 → fastapi_basekit-0.4.3}/fastapi_basekit/aio/beanie/repository/base.py
RENAMED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import logging
|
|
1
2
|
from typing import Any, Dict, List, Optional, Type, Union
|
|
2
3
|
from typing import get_args, get_origin
|
|
3
4
|
import re
|
|
@@ -8,6 +9,8 @@ from beanie import Document, Link
|
|
|
8
9
|
from beanie.odm.queries.find import FindMany
|
|
9
10
|
from beanie.operators import Or, RegEx
|
|
10
11
|
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
11
14
|
|
|
12
15
|
class BaseRepository:
|
|
13
16
|
model: Type[Document]
|
|
@@ -212,6 +215,44 @@ class BaseRepository:
|
|
|
212
215
|
items = await query.skip(count * (page - 1)).limit(count).to_list()
|
|
213
216
|
return items, total
|
|
214
217
|
|
|
218
|
+
async def paginate_keyset(
|
|
219
|
+
self,
|
|
220
|
+
query: FindMany[Document],
|
|
221
|
+
limit: int,
|
|
222
|
+
cursor_field: str,
|
|
223
|
+
cursor_value: Optional[Any] = None,
|
|
224
|
+
ascending: bool = False,
|
|
225
|
+
) -> tuple[List[Document], bool]:
|
|
226
|
+
"""Keyset (cursor) pagination — O(1) por página, sin `skip` ni `count`.
|
|
227
|
+
|
|
228
|
+
Alternativa a `paginate` para listados que escalan: en vez de contar
|
|
229
|
+
toda la colección y saltar `count*(page-1)` documentos (coste que crece
|
|
230
|
+
con la profundidad), filtra por un cursor sobre `cursor_field` y trae
|
|
231
|
+
`limit + 1` para saber si quedan más — sin un `count()`.
|
|
232
|
+
|
|
233
|
+
Args:
|
|
234
|
+
query: FindMany ya filtrado (los filtros base del listado).
|
|
235
|
+
limit: tamaño de página.
|
|
236
|
+
cursor_field: campo Mongo indexado del cursor (ej. "_id",
|
|
237
|
+
"created_at", "timestamp"). Debe existir un índice que lo cubra.
|
|
238
|
+
cursor_value: valor del último item de la página previa. None =
|
|
239
|
+
primera página.
|
|
240
|
+
ascending: True = orden ascendente (`> cursor`); False = descendente
|
|
241
|
+
(`< cursor`), útil para "más recientes primero".
|
|
242
|
+
|
|
243
|
+
Returns:
|
|
244
|
+
(items, has_more). El caller deriva el próximo cursor del último
|
|
245
|
+
item devuelto (`getattr(items[-1], cursor_field)`).
|
|
246
|
+
"""
|
|
247
|
+
limit = max(1, int(limit))
|
|
248
|
+
op = "$gt" if ascending else "$lt"
|
|
249
|
+
if cursor_value is not None:
|
|
250
|
+
query = query.find({cursor_field: {op: cursor_value}})
|
|
251
|
+
query = query.sort((cursor_field, 1 if ascending else -1))
|
|
252
|
+
docs = await query.limit(limit + 1).to_list()
|
|
253
|
+
has_more = len(docs) > limit
|
|
254
|
+
return docs[:limit], has_more
|
|
255
|
+
|
|
215
256
|
def build_list_queryset(
|
|
216
257
|
self,
|
|
217
258
|
search: Optional[str] = None,
|
|
@@ -371,11 +412,31 @@ class BaseRepository:
|
|
|
371
412
|
return items_raw, total
|
|
372
413
|
|
|
373
414
|
items: List[Any] = []
|
|
415
|
+
dropped = 0
|
|
374
416
|
for raw_item in items_raw:
|
|
375
417
|
try:
|
|
376
418
|
items.append(self.model.model_validate(raw_item))
|
|
377
|
-
except Exception:
|
|
378
|
-
|
|
419
|
+
except Exception as exc:
|
|
420
|
+
# No tragar en silencio: una fila que no valida se pierde de la
|
|
421
|
+
# respuesta sin aviso (menos items que el limit, total inflado).
|
|
422
|
+
# Se loguea para que sea diagnosticable; la fila igual se omite
|
|
423
|
+
# para no romper el listado completo por un doc corrupto.
|
|
424
|
+
dropped += 1
|
|
425
|
+
logger.warning(
|
|
426
|
+
"paginate_pipeline: fila descartada por fallo de "
|
|
427
|
+
"validación contra %s: %s",
|
|
428
|
+
getattr(self.model, "__name__", self.model),
|
|
429
|
+
exc,
|
|
430
|
+
)
|
|
431
|
+
if dropped:
|
|
432
|
+
logger.warning(
|
|
433
|
+
"paginate_pipeline: %d/%d filas descartadas en %s "
|
|
434
|
+
"(total reportado=%d)",
|
|
435
|
+
dropped,
|
|
436
|
+
len(items_raw),
|
|
437
|
+
getattr(self.model, "__name__", self.model),
|
|
438
|
+
total,
|
|
439
|
+
)
|
|
379
440
|
return items, total
|
|
380
441
|
|
|
381
442
|
async def list_with_aggregation(
|
|
@@ -30,6 +30,14 @@ class BaseService:
|
|
|
30
30
|
):
|
|
31
31
|
self.repository = repository
|
|
32
32
|
self.request = request
|
|
33
|
+
# Copia por instancia de los defaults mutables (heredados como
|
|
34
|
+
# atributos de CLASE, compartidos por todo el proceso). Sin esto, una
|
|
35
|
+
# mutación en runtime (`self.search_fields.append(...)`) se filtraría a
|
|
36
|
+
# la clase y contaminaría otras requests. Respeta el override del
|
|
37
|
+
# subclass: `list(self.search_fields)` lee su atributo de clase.
|
|
38
|
+
self.search_fields = list(self.search_fields)
|
|
39
|
+
self.duplicate_check_fields = list(self.duplicate_check_fields)
|
|
40
|
+
self.kwargs_query = dict(self.kwargs_query)
|
|
33
41
|
endpoint_func = (
|
|
34
42
|
self.request.scope.get("endpoint") if self.request else None
|
|
35
43
|
)
|
{fastapi_basekit-0.4.0 → fastapi_basekit-0.4.3}/fastapi_basekit/aio/sqlalchemy/repository/base.py
RENAMED
|
@@ -783,4 +783,15 @@ class BaseRepository:
|
|
|
783
783
|
raise NotFoundException(message=f"{model.__name__} no encontrado")
|
|
784
784
|
await db.delete(record)
|
|
785
785
|
await db.flush()
|
|
786
|
-
return True
|
|
786
|
+
return True
|
|
787
|
+
|
|
788
|
+
async def save(self, obj: Any) -> Any:
|
|
789
|
+
"""Persiste una entidad nueva o mutada (add + flush)."""
|
|
790
|
+
self.session.add(obj)
|
|
791
|
+
await self.session.flush()
|
|
792
|
+
return obj
|
|
793
|
+
|
|
794
|
+
async def hard_delete(self, obj: Any) -> None:
|
|
795
|
+
"""Elimina físicamente la fila (sin soft delete)."""
|
|
796
|
+
await self.session.delete(obj)
|
|
797
|
+
await self.session.flush()
|
{fastapi_basekit-0.4.0 → fastapi_basekit-0.4.3}/fastapi_basekit/aio/sqlalchemy/service/base.py
RENAMED
|
@@ -2,8 +2,10 @@ from typing import Any, Dict, List, Optional
|
|
|
2
2
|
|
|
3
3
|
from fastapi import Request
|
|
4
4
|
from pydantic import BaseModel
|
|
5
|
+
from sqlalchemy import select
|
|
5
6
|
from ..repository.base import BaseRepository
|
|
6
7
|
from ....exceptions.api_exceptions import (
|
|
8
|
+
APIException,
|
|
7
9
|
NotFoundException,
|
|
8
10
|
DatabaseIntegrityException,
|
|
9
11
|
)
|
|
@@ -26,6 +28,17 @@ class BaseService:
|
|
|
26
28
|
action: str | None = None
|
|
27
29
|
kwargs_query: Dict[str, Any] = {}
|
|
28
30
|
|
|
31
|
+
# --- Política de borrado (ver `delete`) ---
|
|
32
|
+
# "hard" -> elimina físicamente (default, comportamiento histórico)
|
|
33
|
+
# "soft" -> marca deleted_at (requiere model con soft_delete())
|
|
34
|
+
# "soft_mangle" -> soft + renombra `mangle_fields` (`<valor>__del_<id>`)
|
|
35
|
+
# para liberar un valor único y poder recrear el registro
|
|
36
|
+
# "hard_if_unused" -> elimina físicamente si no está referenciado, si no -> 409
|
|
37
|
+
delete_mode: str = "hard"
|
|
38
|
+
mangle_fields: List[str] = []
|
|
39
|
+
# [(Model, "fk_attr"), ...] revisados en "hard_if_unused".
|
|
40
|
+
delete_references: List[Any] = []
|
|
41
|
+
|
|
29
42
|
def __init__(
|
|
30
43
|
self,
|
|
31
44
|
repository: BaseRepository,
|
|
@@ -184,5 +197,49 @@ class BaseService:
|
|
|
184
197
|
updated = await self.repository.update(id, update_data)
|
|
185
198
|
return updated
|
|
186
199
|
|
|
200
|
+
async def _referenced_label(self, obj: Any) -> Optional[str]:
|
|
201
|
+
"""Nombre de un modelo que aún referencia a `obj` (bloquea hard delete), o None."""
|
|
202
|
+
session = self.repository.session
|
|
203
|
+
for model, fk in self.delete_references:
|
|
204
|
+
query = select(model.id).where(getattr(model, fk) == obj.id)
|
|
205
|
+
if hasattr(model, "deleted_at"):
|
|
206
|
+
query = query.where(model.deleted_at.is_(None))
|
|
207
|
+
if (await session.execute(query.limit(1))).first():
|
|
208
|
+
return model.__name__
|
|
209
|
+
return None
|
|
210
|
+
|
|
211
|
+
async def apply_delete(self, obj: Any) -> bool:
|
|
212
|
+
"""Aplica la política de borrado (`delete_mode`) sobre una entidad ya cargada.
|
|
213
|
+
|
|
214
|
+
Útil cuando una subclase necesita resolver/scopear el objeto antes de borrar
|
|
215
|
+
(p. ej. tenant + region scope): hace `obj = self._get_scoped(id)` y luego
|
|
216
|
+
`await self.apply_delete(obj)`.
|
|
217
|
+
"""
|
|
218
|
+
mode = self.delete_mode
|
|
219
|
+
if mode in ("soft", "soft_mangle"):
|
|
220
|
+
if not hasattr(obj, "soft_delete"): # modelo sin soft delete -> físico
|
|
221
|
+
await self.repository.hard_delete(obj)
|
|
222
|
+
return True
|
|
223
|
+
obj.soft_delete()
|
|
224
|
+
if mode == "soft_mangle":
|
|
225
|
+
for field in self.mangle_fields:
|
|
226
|
+
current = getattr(obj, field, None)
|
|
227
|
+
if current is not None:
|
|
228
|
+
setattr(obj, field, f"{current}__del_{obj.id}")
|
|
229
|
+
await self.repository.save(obj)
|
|
230
|
+
elif mode == "hard_if_unused":
|
|
231
|
+
if await self._referenced_label(obj) is not None:
|
|
232
|
+
raise APIException(
|
|
233
|
+
message="No se puede eliminar: el registro está en uso.",
|
|
234
|
+
status_code="IN_USE", status=409,
|
|
235
|
+
)
|
|
236
|
+
await self.repository.hard_delete(obj)
|
|
237
|
+
else: # "hard"
|
|
238
|
+
await self.repository.hard_delete(obj)
|
|
239
|
+
return True
|
|
240
|
+
|
|
187
241
|
async def delete(self, id: str) -> bool:
|
|
188
|
-
|
|
242
|
+
obj = await self.repository.get(id)
|
|
243
|
+
if not obj:
|
|
244
|
+
raise NotFoundException(message=f"id={id} no encontrado")
|
|
245
|
+
return await self.apply_delete(obj)
|
|
@@ -110,6 +110,8 @@ tests/test_base_response.py
|
|
|
110
110
|
tests/test_base_service.py
|
|
111
111
|
tests/test_beanie_aggregation_hooks.py
|
|
112
112
|
tests/test_beanie_aggregation_integration.py
|
|
113
|
+
tests/test_beanie_keyset_pagination.py
|
|
114
|
+
tests/test_beanie_service_mutable_defaults.py
|
|
113
115
|
tests/test_controller_auto_permissions.py
|
|
114
116
|
tests/test_crud_beanie_controller.py
|
|
115
117
|
tests/test_crud_controller.py
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "fastapi-basekit"
|
|
7
|
-
version = "0.4.
|
|
7
|
+
version = "0.4.3"
|
|
8
8
|
description = "Utilities and base classes for FastAPI async projects (Beanie, SQLAlchemy or SQLModel)"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11"
|
|
@@ -33,3 +33,19 @@ def test_declared_query_filters_still_present():
|
|
|
33
33
|
get_list = spec["paths"]["/users/"]["get"]
|
|
34
34
|
names = {p["name"] for p in get_list.get("parameters", [])}
|
|
35
35
|
assert {"page", "count", "search", "is_active", "age_min"} <= names
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_action_is_filterable_not_excluded():
|
|
39
|
+
"""Regresión inversa: `action` NO debe estar en `_params_excluded_fields`.
|
|
40
|
+
|
|
41
|
+
El ClassVar ya evita el `?action=` espurio (test de arriba); excluir
|
|
42
|
+
`action` del set de filtros rompía filtrar por una columna `action`
|
|
43
|
+
declarada como query param (ej. audit-logs `?action=...`). v0.4.1.
|
|
44
|
+
"""
|
|
45
|
+
from fastapi_basekit.aio.sqlalchemy.controller.base import (
|
|
46
|
+
SQLAlchemyBaseController,
|
|
47
|
+
)
|
|
48
|
+
from fastapi_basekit.aio.controller.base import BaseController
|
|
49
|
+
|
|
50
|
+
assert "action" not in SQLAlchemyBaseController._params_excluded_fields
|
|
51
|
+
assert "action" not in BaseController._params_excluded_fields
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""Tests de `BaseRepository.paginate_keyset` (paginación por cursor, sin skip/count)."""
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
import mongomock_motor
|
|
5
|
+
from beanie import init_beanie
|
|
6
|
+
|
|
7
|
+
from example_crud_beanie.models import UserDocument
|
|
8
|
+
from example_crud_beanie.repository import UserBeanieRepository
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@pytest.fixture
|
|
12
|
+
async def init_db():
|
|
13
|
+
client = mongomock_motor.AsyncMongoMockClient()
|
|
14
|
+
await init_beanie(database=client.test_db, document_models=[UserDocument])
|
|
15
|
+
yield
|
|
16
|
+
client.close()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
async def _seed(n: int):
|
|
20
|
+
for i in range(n):
|
|
21
|
+
await UserDocument(
|
|
22
|
+
name=f"u{i:02d}", email=f"u{i}@x.com", age=i
|
|
23
|
+
).insert()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class TestKeysetPagination:
|
|
27
|
+
async def test_desc_latest_first_and_cursor_walk(self, init_db):
|
|
28
|
+
repo = UserBeanieRepository()
|
|
29
|
+
await _seed(25)
|
|
30
|
+
|
|
31
|
+
# Página 1: los más "nuevos" por age desc.
|
|
32
|
+
items, has_more = await repo.paginate_keyset(
|
|
33
|
+
repo.model.find(), limit=10, cursor_field="age", ascending=False
|
|
34
|
+
)
|
|
35
|
+
assert [d.age for d in items] == list(range(24, 14, -1))
|
|
36
|
+
assert has_more is True
|
|
37
|
+
|
|
38
|
+
# Página 2 vía cursor (age del último item).
|
|
39
|
+
items2, has_more2 = await repo.paginate_keyset(
|
|
40
|
+
repo.model.find(),
|
|
41
|
+
limit=10,
|
|
42
|
+
cursor_field="age",
|
|
43
|
+
cursor_value=items[-1].age,
|
|
44
|
+
ascending=False,
|
|
45
|
+
)
|
|
46
|
+
assert [d.age for d in items2] == list(range(14, 4, -1))
|
|
47
|
+
assert has_more2 is True
|
|
48
|
+
|
|
49
|
+
# Página 3 (final): quedan 5, sin has_more.
|
|
50
|
+
items3, has_more3 = await repo.paginate_keyset(
|
|
51
|
+
repo.model.find(),
|
|
52
|
+
limit=10,
|
|
53
|
+
cursor_field="age",
|
|
54
|
+
cursor_value=items2[-1].age,
|
|
55
|
+
ascending=False,
|
|
56
|
+
)
|
|
57
|
+
assert [d.age for d in items3] == list(range(4, -1, -1))
|
|
58
|
+
assert has_more3 is False
|
|
59
|
+
|
|
60
|
+
async def test_ascending_order(self, init_db):
|
|
61
|
+
repo = UserBeanieRepository()
|
|
62
|
+
await _seed(5)
|
|
63
|
+
items, has_more = await repo.paginate_keyset(
|
|
64
|
+
repo.model.find(), limit=3, cursor_field="age", ascending=True
|
|
65
|
+
)
|
|
66
|
+
assert [d.age for d in items] == [0, 1, 2]
|
|
67
|
+
assert has_more is True
|
|
68
|
+
|
|
69
|
+
async def test_no_skip_or_count_on_empty(self, init_db):
|
|
70
|
+
repo = UserBeanieRepository()
|
|
71
|
+
items, has_more = await repo.paginate_keyset(
|
|
72
|
+
repo.model.find(), limit=10, cursor_field="age", ascending=False
|
|
73
|
+
)
|
|
74
|
+
assert items == []
|
|
75
|
+
assert has_more is False
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Los defaults mutables del BaseService (search_fields, kwargs_query,
|
|
2
|
+
duplicate_check_fields) deben ser por instancia — mutar uno no puede filtrarse
|
|
3
|
+
a otra instancia ni al atributo de clase (contaminación cross-request)."""
|
|
4
|
+
|
|
5
|
+
from example_crud_beanie.repository import UserBeanieRepository
|
|
6
|
+
from example_crud_beanie.service import UserBeanieService
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class TestMutableDefaultsIsolation:
|
|
10
|
+
def test_subclass_override_preserved(self):
|
|
11
|
+
s = UserBeanieService(repository=UserBeanieRepository())
|
|
12
|
+
# El override del subclass sigue vigente (copiado, no perdido).
|
|
13
|
+
assert s.search_fields == ["name", "email"]
|
|
14
|
+
assert s.duplicate_check_fields == ["email"]
|
|
15
|
+
|
|
16
|
+
def test_mutation_does_not_leak_across_instances(self):
|
|
17
|
+
s1 = UserBeanieService(repository=UserBeanieRepository())
|
|
18
|
+
s2 = UserBeanieService(repository=UserBeanieRepository())
|
|
19
|
+
s1.search_fields.append("phone")
|
|
20
|
+
assert "phone" not in s2.search_fields
|
|
21
|
+
# El atributo de CLASE queda intacto.
|
|
22
|
+
assert "phone" not in UserBeanieService.search_fields
|
|
23
|
+
|
|
24
|
+
def test_kwargs_query_dict_isolated(self):
|
|
25
|
+
s1 = UserBeanieService(repository=UserBeanieRepository())
|
|
26
|
+
s2 = UserBeanieService(repository=UserBeanieRepository())
|
|
27
|
+
s1.kwargs_query["fetch_links"] = True
|
|
28
|
+
assert s2.kwargs_query == {}
|
|
29
|
+
assert UserBeanieService.kwargs_query == {}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fastapi_basekit-0.4.0 → fastapi_basekit-0.4.3}/fastapi_basekit/aio/beanie/controller/__init__.py
RENAMED
|
File without changes
|
{fastapi_basekit-0.4.0 → fastapi_basekit-0.4.3}/fastapi_basekit/aio/beanie/controller/base.py
RENAMED
|
File without changes
|
{fastapi_basekit-0.4.0 → fastapi_basekit-0.4.3}/fastapi_basekit/aio/beanie/repository/__init__.py
RENAMED
|
File without changes
|
{fastapi_basekit-0.4.0 → fastapi_basekit-0.4.3}/fastapi_basekit/aio/beanie/service/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fastapi_basekit-0.4.0 → fastapi_basekit-0.4.3}/fastapi_basekit/aio/sqlalchemy/service/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fastapi_basekit-0.4.0 → fastapi_basekit-0.4.3}/fastapi_basekit/aio/sqlmodel/controller/__init__.py
RENAMED
|
File without changes
|
{fastapi_basekit-0.4.0 → fastapi_basekit-0.4.3}/fastapi_basekit/aio/sqlmodel/controller/base.py
RENAMED
|
File without changes
|
{fastapi_basekit-0.4.0 → fastapi_basekit-0.4.3}/fastapi_basekit/aio/sqlmodel/repository/__init__.py
RENAMED
|
File without changes
|
{fastapi_basekit-0.4.0 → fastapi_basekit-0.4.3}/fastapi_basekit/aio/sqlmodel/repository/base.py
RENAMED
|
File without changes
|
{fastapi_basekit-0.4.0 → fastapi_basekit-0.4.3}/fastapi_basekit/aio/sqlmodel/service/__init__.py
RENAMED
|
File without changes
|
{fastapi_basekit-0.4.0 → fastapi_basekit-0.4.3}/fastapi_basekit/aio/sqlmodel/service/base.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fastapi_basekit-0.4.0 → fastapi_basekit-0.4.3}/fastapi_basekit/exceptions/api_exceptions.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fastapi_basekit-0.4.0 → fastapi_basekit-0.4.3}/fastapi_basekit/templates/project/cookiecutter.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fastapi_basekit-0.4.0 → fastapi_basekit-0.4.3}/fastapi_basekit.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fastapi_basekit-0.4.0 → fastapi_basekit-0.4.3}/tests/test_beanie_aggregation_integration.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fastapi_basekit-0.4.0 → fastapi_basekit-0.4.3}/tests/test_crud_sqlmodel_repository_service.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|