fastapi-basekit 0.3.1__tar.gz → 0.3.2__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 (116) hide show
  1. {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/PKG-INFO +1 -1
  2. {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/aio/beanie/repository/base.py +138 -58
  3. {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/aio/beanie/service/base.py +81 -30
  4. fastapi_basekit-0.3.2/fastapi_basekit/exceptions/__init__.py +3 -0
  5. fastapi_basekit-0.3.2/fastapi_basekit/exceptions/domain.py +57 -0
  6. {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit.egg-info/PKG-INFO +1 -1
  7. {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit.egg-info/SOURCES.txt +1 -0
  8. {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/pyproject.toml +1 -1
  9. fastapi_basekit-0.3.1/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/utils/__init__.py +0 -0
  10. {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/LICENSE +0 -0
  11. {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/README.md +0 -0
  12. {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/__init__.py +0 -0
  13. {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/aio/__init__.py +0 -0
  14. {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/aio/beanie/__init__.py +0 -0
  15. {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/aio/beanie/controller/__init__.py +0 -0
  16. {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/aio/beanie/controller/base.py +0 -0
  17. {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/aio/beanie/repository/__init__.py +0 -0
  18. {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/aio/beanie/service/__init__.py +0 -0
  19. {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/aio/controller/__init__.py +0 -0
  20. {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/aio/controller/base.py +0 -0
  21. {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/aio/permissions/__init__.py +0 -0
  22. {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/aio/permissions/base.py +0 -0
  23. {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/aio/sqlalchemy/__init__.py +0 -0
  24. {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/aio/sqlalchemy/controller/__init__.py +0 -0
  25. {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/aio/sqlalchemy/controller/base.py +0 -0
  26. {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/aio/sqlalchemy/repository/__init__.py +0 -0
  27. {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/aio/sqlalchemy/repository/base.py +0 -0
  28. {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/aio/sqlalchemy/service/__init__.py +0 -0
  29. {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/aio/sqlalchemy/service/base.py +0 -0
  30. {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/aio/sqlalchemy/session.py +0 -0
  31. {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/aio/sqlmodel/__init__.py +0 -0
  32. {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/aio/sqlmodel/controller/__init__.py +0 -0
  33. {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/aio/sqlmodel/controller/base.py +0 -0
  34. {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/aio/sqlmodel/repository/__init__.py +0 -0
  35. {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/aio/sqlmodel/repository/base.py +0 -0
  36. {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/aio/sqlmodel/service/__init__.py +0 -0
  37. {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/aio/sqlmodel/service/base.py +0 -0
  38. {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/cli/__init__.py +0 -0
  39. {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/cli/main.py +0 -0
  40. {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/exceptions/api_exceptions.py +0 -0
  41. {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/exceptions/handler.py +0 -0
  42. {fastapi_basekit-0.3.1/fastapi_basekit/exceptions → fastapi_basekit-0.3.2/fastapi_basekit/schema}/__init__.py +0 -0
  43. {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/schema/base.py +0 -0
  44. {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/schema/jwt.py +0 -0
  45. {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/schema/schema.py +0 -0
  46. {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/servicios/__init__.py +0 -0
  47. {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/servicios/thrid/__init__.py +0 -0
  48. {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/servicios/thrid/jwt.py +0 -0
  49. {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/templates/project/cookiecutter.json +0 -0
  50. {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/templates/project/hooks/post_gen_project.py +0 -0
  51. {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/templates/project/hooks/pre_gen_project.py +0 -0
  52. {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/.env.example +0 -0
  53. {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/.gitignore +0 -0
  54. {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/Dockerfile +0 -0
  55. {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/LICENSE +0 -0
  56. {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/Makefile +0 -0
  57. {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/README.md +0 -0
  58. {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/alembic/env.py +0 -0
  59. {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/alembic/script.py.mako +0 -0
  60. {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/alembic.ini +0 -0
  61. {fastapi_basekit-0.3.1/fastapi_basekit/schema → fastapi_basekit-0.3.2/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app}/__init__.py +0 -0
  62. {fastapi_basekit-0.3.1/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app → fastapi_basekit-0.3.2/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/api}/__init__.py +0 -0
  63. {fastapi_basekit-0.3.1/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/api → fastapi_basekit-0.3.2/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/api/v1}/__init__.py +0 -0
  64. {fastapi_basekit-0.3.1/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/api/v1 → fastapi_basekit-0.3.2/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/api/v1/endpoints}/__init__.py +0 -0
  65. {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/api/v1/endpoints/auth/__init__.py +0 -0
  66. {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/api/v1/endpoints/auth/auth.py +0 -0
  67. {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/api/v1/endpoints/user/__init__.py +0 -0
  68. {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/api/v1/endpoints/user/user.py +0 -0
  69. {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/api/v1/routers.py +0 -0
  70. {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/config/__init__.py +0 -0
  71. {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/config/database.py +0 -0
  72. {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/config/settings.py +0 -0
  73. {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/main.py +0 -0
  74. {fastapi_basekit-0.3.1/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/api/v1/endpoints → fastapi_basekit-0.3.2/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/middleware}/__init__.py +0 -0
  75. {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/middleware/auth.py +0 -0
  76. {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/middleware/permissions.py +0 -0
  77. {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/models/__init__.py +0 -0
  78. {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/models/auth.py +0 -0
  79. {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/models/base.py +0 -0
  80. {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/models/enums.py +0 -0
  81. {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/models/types.py +0 -0
  82. {fastapi_basekit-0.3.1/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/middleware → fastapi_basekit-0.3.2/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/permissions}/__init__.py +0 -0
  83. {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/permissions/user.py +0 -0
  84. {fastapi_basekit-0.3.1/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/permissions → fastapi_basekit-0.3.2/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/repositories}/__init__.py +0 -0
  85. {fastapi_basekit-0.3.1/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/repositories → fastapi_basekit-0.3.2/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/repositories/user}/__init__.py +0 -0
  86. {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/repositories/user/repository.py +0 -0
  87. {fastapi_basekit-0.3.1/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/repositories/user → fastapi_basekit-0.3.2/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/schemas}/__init__.py +0 -0
  88. {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/schemas/auth.py +0 -0
  89. {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/schemas/base.py +0 -0
  90. {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/schemas/user.py +0 -0
  91. {fastapi_basekit-0.3.1/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/schemas → fastapi_basekit-0.3.2/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/scripts}/__init__.py +0 -0
  92. {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/scripts/init.py +0 -0
  93. {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/scripts/init_admin.py +0 -0
  94. {fastapi_basekit-0.3.1/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/scripts → fastapi_basekit-0.3.2/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/services}/__init__.py +0 -0
  95. {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/services/auth_service.py +0 -0
  96. {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/services/dependency.py +0 -0
  97. {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/services/user_service.py +0 -0
  98. {fastapi_basekit-0.3.1/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/services → fastapi_basekit-0.3.2/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/utils}/__init__.py +0 -0
  99. {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/utils/exception_handlers.py +0 -0
  100. {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/utils/security.py +0 -0
  101. {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/docker-compose.yml +0 -0
  102. {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/pytest.ini +0 -0
  103. {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/requirements.txt +0 -0
  104. {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit.egg-info/dependency_links.txt +0 -0
  105. {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit.egg-info/entry_points.txt +0 -0
  106. {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit.egg-info/requires.txt +0 -0
  107. {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit.egg-info/top_level.txt +0 -0
  108. {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/setup.cfg +0 -0
  109. {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/tests/test_api_exceptions.py +0 -0
  110. {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/tests/test_base_response.py +0 -0
  111. {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/tests/test_base_service.py +0 -0
  112. {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/tests/test_controller_auto_permissions.py +0 -0
  113. {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/tests/test_crud_beanie_controller.py +0 -0
  114. {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/tests/test_crud_controller.py +0 -0
  115. {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/tests/test_jwt_service.py +0 -0
  116. {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/tests/test_sqlalchemy_base_service_order.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: fastapi-basekit
3
- Version: 0.3.1
3
+ Version: 0.3.2
4
4
  Summary: Utilities and base classes for FastAPI async projects (Beanie, SQLAlchemy or SQLModel)
5
5
  Author-email: Jerson Moreno <jerson.ml820@hotmail.com>
6
6
  License: MIT
@@ -207,27 +207,42 @@ class BaseRepository:
207
207
  # Apply ordering if provided and not already applied
208
208
  if order_by:
209
209
  query = query.sort(order_by)
210
-
210
+
211
211
  total = await query.count()
212
212
  items = await query.skip(count * (page - 1)).limit(count).to_list()
213
213
  return items, total
214
-
215
- async def list_with_aggregation(
214
+
215
+ def build_list_queryset(
216
+ self,
217
+ search: Optional[str] = None,
218
+ search_fields: Optional[List[str]] = None,
219
+ filters: Optional[dict] = None,
220
+ order_by: Optional[List[tuple]] = None,
221
+ **kwargs,
222
+ ) -> FindMany[Document]:
223
+ """Hook: returns the FindMany query used by `list` endpoints.
224
+
225
+ Beanie equivalent of SQLAlchemy `build_list_queryset`. Override at
226
+ repository OR service level (`BaseService.build_list_queryset`) to
227
+ customize filters, projections, or query options before pagination.
228
+ Default implementation delegates to `build_filter_query`.
229
+ """
230
+ return self.build_filter_query(
231
+ search=search,
232
+ search_fields=search_fields or [],
233
+ filters=filters or {},
234
+ order_by=order_by,
235
+ **kwargs,
236
+ )
237
+
238
+ def _build_match_stage(
216
239
  self,
217
240
  search: Optional[str],
218
- search_fields: List[str],
219
- filters: dict,
220
- order_by: str,
221
- page: int,
222
- count: int,
223
- **kwargs
224
- ) -> tuple[List[Document], int]:
225
- """List with support for nested ordering using aggregation ($facet optimized)."""
226
- field_path, direction, is_nested = self._parse_order_field(order_by)
227
- pipeline = []
228
-
229
- # 1. Match stage (same as before)
230
- match_conditions = {}
241
+ search_fields: Optional[List[str]],
242
+ filters: Optional[dict],
243
+ ) -> Dict[str, Any]:
244
+ """Build a `$match` stage dict (without the `$match` wrapper)."""
245
+ match_conditions: Dict[str, Any] = {}
231
246
  if filters:
232
247
  for key, value in filters.items():
233
248
  if isinstance(value, ObjectId):
@@ -236,7 +251,7 @@ class BaseRepository:
236
251
  match_conditions[f"{key}.$id"] = value.id
237
252
  else:
238
253
  match_conditions[key] = value
239
-
254
+
240
255
  if search and search_fields:
241
256
  search_conditions = [
242
257
  {field: {"$regex": f".*{search}.*", "$options": "i"}}
@@ -244,22 +259,47 @@ class BaseRepository:
244
259
  ]
245
260
  if search_conditions:
246
261
  if match_conditions:
247
- match_conditions = {"$and": [match_conditions, {"$or": search_conditions}]}
262
+ match_conditions = {
263
+ "$and": [
264
+ match_conditions,
265
+ {"$or": search_conditions},
266
+ ]
267
+ }
248
268
  else:
249
269
  match_conditions = {"$or": search_conditions}
250
-
270
+ return match_conditions
271
+
272
+ def build_list_pipeline(
273
+ self,
274
+ search: Optional[str] = None,
275
+ search_fields: Optional[List[str]] = None,
276
+ filters: Optional[dict] = None,
277
+ order_by: Optional[str] = None,
278
+ **kwargs,
279
+ ) -> List[Dict[str, Any]]:
280
+ """Hook: returns the aggregation pipeline used by `list` endpoints.
281
+
282
+ Beanie equivalent of SQL subqueries / JOINs. Override at repository
283
+ OR service level (`BaseService.build_list_pipeline`) to add `$lookup`,
284
+ `$project`, `$group`, etc. Default builds `$match` + optional `$sort`
285
+ (with auto `$lookup` for nested-link ordering). The `$facet` pagination
286
+ stage is appended later by `paginate_pipeline`.
287
+ """
288
+ pipeline: List[Dict[str, Any]] = []
289
+
290
+ match_conditions = self._build_match_stage(search, search_fields, filters)
251
291
  if match_conditions:
252
292
  pipeline.append({"$match": match_conditions})
253
-
254
- # 2. Lookup & Sort (same as before)
255
- collection_name = None
256
- first_field = None
257
-
293
+
294
+ if not order_by:
295
+ return pipeline
296
+
297
+ field_path, direction, is_nested = self._parse_order_field(order_by)
298
+
258
299
  if is_nested:
259
300
  parts = field_path.split(".")
260
301
  first_field = parts[0]
261
302
  collection_name = self._get_collection_name_from_field(first_field)
262
-
263
303
  if collection_name:
264
304
  pipeline.extend([
265
305
  {
@@ -267,65 +307,105 @@ class BaseRepository:
267
307
  "from": collection_name,
268
308
  "localField": f"{first_field}.$id",
269
309
  "foreignField": "_id",
270
- "as": f"{first_field}_data"
310
+ "as": f"{first_field}_data",
271
311
  }
272
312
  },
273
313
  {
274
314
  "$unwind": {
275
315
  "path": f"${first_field}_data",
276
- "preserveNullAndEmptyArrays": True
316
+ "preserveNullAndEmptyArrays": True,
277
317
  }
278
- }
318
+ },
279
319
  ])
280
320
  remaining_path = ".".join(parts[1:]) if len(parts) > 1 else ""
281
- sort_field = f"{first_field}_data.{remaining_path}" if remaining_path else f"{first_field}_data"
321
+ sort_field = (
322
+ f"{first_field}_data.{remaining_path}"
323
+ if remaining_path
324
+ else f"{first_field}_data"
325
+ )
282
326
  else:
283
327
  sort_field = field_path
284
328
  else:
285
329
  sort_field = field_path
286
-
330
+
287
331
  pipeline.append({"$sort": {sort_field: direction}})
288
-
289
- # 3. Facet for Single Query Pagination
290
- project_exclusion = {f"{first_field}_data": 0} if (is_nested and collection_name) else None
291
-
292
- facet_stage = {
293
- "$facet": {
294
- "metadata": [{"$count": "total"}],
295
- "data": [
296
- {"$skip": count * (page - 1)},
297
- {"$limit": count}
298
- ]
332
+ return pipeline
333
+
334
+ async def paginate_pipeline(
335
+ self,
336
+ pipeline: List[Dict[str, Any]],
337
+ page: int,
338
+ count: int,
339
+ validate: bool = True,
340
+ ) -> tuple[List[Any], int]:
341
+ """Run an aggregation pipeline with `$facet` pagination.
342
+
343
+ Args:
344
+ pipeline: Pipeline stages (without the final `$facet`).
345
+ page, count: Pagination params.
346
+ validate: If True, validates each row against `self.model`.
347
+ Set False when the pipeline projects a non-model shape (e.g.
348
+ joined columns) — the raw dicts are returned untouched.
349
+ """
350
+ full_pipeline = list(pipeline) + [
351
+ {
352
+ "$facet": {
353
+ "metadata": [{"$count": "total"}],
354
+ "data": [
355
+ {"$skip": count * (page - 1)},
356
+ {"$limit": count},
357
+ ],
358
+ }
299
359
  }
300
- }
301
-
302
- # Add projection to remove join artifacts if needed
303
- if project_exclusion:
304
- facet_stage["$facet"]["data"].append({"$project": project_exclusion})
305
-
306
- pipeline.append(facet_stage)
307
-
308
- # Execute Pipeline
309
- results = await self.model.aggregate(pipeline).to_list()
310
-
311
- # Process Results
360
+ ]
361
+
362
+ results = await self.model.aggregate(full_pipeline).to_list()
312
363
  if not results or not results[0].get("metadata"):
313
364
  return [], 0
314
-
365
+
315
366
  data = results[0]
316
367
  total = data["metadata"][0]["total"] if data["metadata"] else 0
317
368
  items_raw = data["data"]
318
-
319
- # Efficient Validation
320
- items = []
369
+
370
+ if not validate:
371
+ return items_raw, total
372
+
373
+ items: List[Any] = []
321
374
  for raw_item in items_raw:
322
375
  try:
323
376
  items.append(self.model.model_validate(raw_item))
324
377
  except Exception:
325
378
  continue
326
-
327
379
  return items, total
328
380
 
381
+ async def list_with_aggregation(
382
+ self,
383
+ search: Optional[str],
384
+ search_fields: List[str],
385
+ filters: dict,
386
+ order_by: str,
387
+ page: int,
388
+ count: int,
389
+ **kwargs,
390
+ ) -> tuple[List[Document], int]:
391
+ """Backward-compat wrapper: delegates to `build_list_pipeline` +
392
+ `paginate_pipeline`. Kept for callers that hit it directly.
393
+ """
394
+ pipeline = self.build_list_pipeline(
395
+ search=search,
396
+ search_fields=search_fields,
397
+ filters=filters,
398
+ order_by=order_by,
399
+ **kwargs,
400
+ )
401
+ # Strip join artifacts when default sort-lookup added them
402
+ field_path, _, is_nested = self._parse_order_field(order_by) if order_by else ("", 0, False)
403
+ if is_nested:
404
+ first_field = field_path.split(".")[0]
405
+ if self._get_collection_name_from_field(first_field):
406
+ pipeline.append({"$project": {f"{first_field}_data": 0}})
407
+ return await self.paginate_pipeline(pipeline, page, count, validate=True)
408
+
329
409
  async def get_by_id(
330
410
  self,
331
411
  obj_id: Union[str, ObjectId],
@@ -1,5 +1,7 @@
1
1
  from typing import Any, Dict, List, Optional, Union
2
2
 
3
+ from beanie import Document
4
+ from beanie.odm.queries.find import FindMany
3
5
  from fastapi import Request
4
6
  from pydantic import BaseModel
5
7
 
@@ -20,6 +22,8 @@ class BaseService:
20
22
  kwargs_query: Dict[str, Union[str, int]] = {}
21
23
  action: str = ""
22
24
  order_by: Optional[List[tuple]] = None
25
+ use_aggregation: bool = False
26
+ aggregation_validate: bool = True
23
27
 
24
28
  def __init__(
25
29
  self, repository: BaseRepository, request: Optional[Request] = None
@@ -69,61 +73,108 @@ class BaseService:
69
73
  raise NotFoundException(f"id={id} no encontrado")
70
74
  return obj
71
75
 
76
+ def build_list_queryset(
77
+ self,
78
+ search: Optional[str] = None,
79
+ search_fields: Optional[List[str]] = None,
80
+ filters: Optional[Dict[str, Any]] = None,
81
+ order_by: Optional[List[tuple]] = None,
82
+ **kwargs,
83
+ ) -> FindMany[Document]:
84
+ """Service-level hook over `repository.build_list_queryset`.
85
+
86
+ Override here to compose query options across repositories or to
87
+ decorate the FindMany before pagination.
88
+ """
89
+ return self.repository.build_list_queryset(
90
+ search=search,
91
+ search_fields=search_fields or self.search_fields,
92
+ filters=filters,
93
+ order_by=order_by,
94
+ **kwargs,
95
+ )
96
+
97
+ def build_list_pipeline(
98
+ self,
99
+ search: Optional[str] = None,
100
+ search_fields: Optional[List[str]] = None,
101
+ filters: Optional[Dict[str, Any]] = None,
102
+ order_by: Optional[str] = None,
103
+ **kwargs,
104
+ ) -> List[Dict[str, Any]]:
105
+ """Service-level hook over `repository.build_list_pipeline`.
106
+
107
+ Override here to add `$lookup`, `$project`, `$group`, etc. for
108
+ cross-collection joins (subquery-like). Set `use_aggregation = True`
109
+ on the service to force the pipeline path even without nested order.
110
+ Set `aggregation_validate = False` if the projection produces a
111
+ non-model shape (joined columns / flattened rows).
112
+ """
113
+ return self.repository.build_list_pipeline(
114
+ search=search,
115
+ search_fields=search_fields or self.search_fields,
116
+ filters=filters,
117
+ order_by=order_by,
118
+ **kwargs,
119
+ )
120
+
72
121
  async def list(
73
122
  self,
74
123
  search: Optional[str] = None,
75
124
  page: int = 1,
76
125
  count: int = 25,
77
126
  filters: Optional[Dict[str, Any]] = None,
78
- order_by: Optional[str] = None, # NEW: Dynamic ordering (e.g., "-created_at" or "tool__name")
127
+ order_by: Optional[str] = None, # Dynamic ordering (e.g., "-created_at" or "tool__name")
79
128
  ):
80
129
  kwargs = self.get_kwargs_query()
81
130
  applied_filters = self.get_filters(filters)
82
-
83
- # Determine which ordering to use
131
+
132
+ # Resolve ordering
84
133
  if order_by:
85
- # Dynamic ordering from parameter (takes precedence)
86
134
  order_str = order_by
87
135
  else:
88
- # Fall back to service-level ordering
89
136
  default_order = self.get_order()
90
137
  if default_order:
91
- # Convert list of tuples to string format
92
- # e.g., [("created_at", -1)] -> "-created_at"
93
138
  field, direction = default_order[0]
94
139
  order_str = f"{'-' if direction == -1 else ''}{field}"
95
140
  else:
96
141
  order_str = None
97
-
98
- # Check if we need aggregation (nested field with __ or .)
99
- if order_str and ("__" in order_str or "." in order_str):
100
- # Use aggregation pipeline for nested ordering
101
- return await self.repository.list_with_aggregation(
142
+
143
+ nested_order = bool(order_str and ("__" in order_str or "." in order_str))
144
+ use_pipeline = self.use_aggregation or nested_order
145
+
146
+ if use_pipeline:
147
+ pipeline = self.build_list_pipeline(
102
148
  search=search,
103
149
  search_fields=self.search_fields,
104
150
  filters=applied_filters,
105
151
  order_by=order_str,
106
- page=page,
107
- count=count,
108
152
  **kwargs,
109
153
  )
110
- else:
111
- # Use standard query for simple ordering
112
- order_list = None
113
- if order_str:
114
- # Parse the order string
115
- direction = -1 if order_str.startswith("-") else 1
116
- field = order_str.lstrip("-")
117
- order_list = [(field, direction)]
118
-
119
- query = self.repository.build_filter_query(
120
- search=search,
121
- search_fields=self.search_fields,
122
- filters=applied_filters,
123
- order_by=order_list,
124
- **kwargs,
154
+ return await self.repository.paginate_pipeline(
155
+ pipeline,
156
+ page=page,
157
+ count=count,
158
+ validate=self.aggregation_validate,
125
159
  )
126
- return await self.repository.paginate(query, page, count, order_by=order_list)
160
+
161
+ # FindMany path
162
+ order_list = None
163
+ if order_str:
164
+ direction = -1 if order_str.startswith("-") else 1
165
+ field = order_str.lstrip("-")
166
+ order_list = [(field, direction)]
167
+
168
+ query = self.build_list_queryset(
169
+ search=search,
170
+ search_fields=self.search_fields,
171
+ filters=applied_filters,
172
+ order_by=order_list,
173
+ **kwargs,
174
+ )
175
+ return await self.repository.paginate(
176
+ query, page, count, order_by=order_list
177
+ )
127
178
 
128
179
  async def create(
129
180
  self, payload: BaseModel, check_fields: Optional[List[str]] = None
@@ -0,0 +1,3 @@
1
+ from fastapi_basekit.exceptions.domain import DomainError
2
+
3
+ __all__ = ["DomainError"]
@@ -0,0 +1,57 @@
1
+ """Generic domain-error base class with declarative HTTP mapping.
2
+
3
+ Services raise `DomainError` (or a subclass) with a stable string `code` and
4
+ a human `message`. Endpoints catch and re-raise as HTTPException via
5
+ `exc.to_http()`. The status code per `code` is declared on the subclass via
6
+ `STATUS_CODE_MAP`, so each domain owns its mapping in one place — no inline
7
+ `if/elif` translator helpers in endpoint files.
8
+
9
+ Pattern:
10
+
11
+ class MyDomainError(DomainError):
12
+ STATUS_CODE_MAP = {
13
+ "unauthorized": status.HTTP_401_UNAUTHORIZED,
14
+ "not_found": status.HTTP_404_NOT_FOUND,
15
+ "conflict": status.HTTP_409_CONFLICT,
16
+ }
17
+
18
+ # Service:
19
+ raise MyDomainError("not_found", "Recurso no encontrado")
20
+
21
+ # Endpoint:
22
+ try:
23
+ await service.do_thing()
24
+ except MyDomainError as exc:
25
+ raise exc.to_http()
26
+ """
27
+
28
+ from typing import ClassVar
29
+
30
+ from fastapi import HTTPException, status
31
+
32
+
33
+ class DomainError(Exception):
34
+ """Base for service-layer domain errors with HTTP mapping baked in.
35
+
36
+ Subclasses override `STATUS_CODE_MAP` to map their codes to HTTP
37
+ statuses. Codes not present fall back to `DEFAULT_STATUS`.
38
+ """
39
+
40
+ DEFAULT_STATUS: ClassVar[int] = status.HTTP_400_BAD_REQUEST
41
+ STATUS_CODE_MAP: ClassVar[dict[str, int]] = {}
42
+
43
+ def __init__(self, code: str, message: str):
44
+ super().__init__(message)
45
+ self.code = code
46
+ self.message = message
47
+
48
+ def http_status(self) -> int:
49
+ return self.STATUS_CODE_MAP.get(self.code, self.DEFAULT_STATUS)
50
+
51
+ def to_http(self) -> HTTPException:
52
+ return HTTPException(
53
+ status_code=self.http_status(), detail=self.message
54
+ )
55
+
56
+ def __str__(self) -> str:
57
+ return f"{type(self).__name__}[{self.code}]: {self.message}"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: fastapi-basekit
3
- Version: 0.3.1
3
+ Version: 0.3.2
4
4
  Summary: Utilities and base classes for FastAPI async projects (Beanie, SQLAlchemy or SQLModel)
5
5
  Author-email: Jerson Moreno <jerson.ml820@hotmail.com>
6
6
  License: MIT
@@ -39,6 +39,7 @@ fastapi_basekit/cli/__init__.py
39
39
  fastapi_basekit/cli/main.py
40
40
  fastapi_basekit/exceptions/__init__.py
41
41
  fastapi_basekit/exceptions/api_exceptions.py
42
+ fastapi_basekit/exceptions/domain.py
42
43
  fastapi_basekit/exceptions/handler.py
43
44
  fastapi_basekit/schema/__init__.py
44
45
  fastapi_basekit/schema/base.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "fastapi-basekit"
7
- version = "0.3.1"
7
+ version = "0.3.2"
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"
File without changes