fastapi-basekit 0.1.0__py3-none-any.whl

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 (31) hide show
  1. fastapi_basekit/__init__.py +8 -0
  2. fastapi_basekit/aio/__init__.py +1 -0
  3. fastapi_basekit/aio/beanie/__init__.py +6 -0
  4. fastapi_basekit/aio/beanie/controller/base.py +21 -0
  5. fastapi_basekit/aio/beanie/repository/__init__.py +1 -0
  6. fastapi_basekit/aio/beanie/repository/base.py +129 -0
  7. fastapi_basekit/aio/beanie/service/base.py +115 -0
  8. fastapi_basekit/aio/controller/__init__.py +0 -0
  9. fastapi_basekit/aio/controller/base.py +131 -0
  10. fastapi_basekit/aio/permissions/__init__.py +0 -0
  11. fastapi_basekit/aio/permissions/base.py +10 -0
  12. fastapi_basekit/aio/sqlalchemy/__init__.py +6 -0
  13. fastapi_basekit/aio/sqlalchemy/controller/base.py +68 -0
  14. fastapi_basekit/aio/sqlalchemy/repository/__init__.py +1 -0
  15. fastapi_basekit/aio/sqlalchemy/repository/base.py +208 -0
  16. fastapi_basekit/aio/sqlalchemy/service/__init__.py +1 -0
  17. fastapi_basekit/aio/sqlalchemy/service/base.py +134 -0
  18. fastapi_basekit/exceptions/__init__.py +0 -0
  19. fastapi_basekit/exceptions/api_exceptions.py +119 -0
  20. fastapi_basekit/exceptions/handler.py +97 -0
  21. fastapi_basekit/schema/__init__.py +0 -0
  22. fastapi_basekit/schema/base.py +25 -0
  23. fastapi_basekit/schema/jwt.py +6 -0
  24. fastapi_basekit/schema/schema.py +17 -0
  25. fastapi_basekit/servicios/__init__.py +1 -0
  26. fastapi_basekit/servicios/thrid/__init__.py +1 -0
  27. fastapi_basekit/servicios/thrid/jwt.py +76 -0
  28. fastapi_basekit-0.1.0.dist-info/METADATA +356 -0
  29. fastapi_basekit-0.1.0.dist-info/RECORD +31 -0
  30. fastapi_basekit-0.1.0.dist-info/WHEEL +5 -0
  31. fastapi_basekit-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,8 @@
1
+ from importlib.metadata import version, PackageNotFoundError
2
+
3
+ __all__ = ["aio", "exceptions", "schema", "servicios"]
4
+
5
+ try:
6
+ __version__ = version("fastapi-basekit")
7
+ except PackageNotFoundError:
8
+ __version__ = "0.0.0.dev0"
@@ -0,0 +1 @@
1
+ __all__ = ["controller", "beanie", "sqlalchemy"]
@@ -0,0 +1,6 @@
1
+ __all__ = ["repository", "service"]
2
+ from .controller.base import BeanieBaseController
3
+
4
+ __all__ = [
5
+ "BeanieBaseController",
6
+ ]
@@ -0,0 +1,21 @@
1
+ from typing import ClassVar, Type
2
+
3
+ from fastapi import Depends
4
+ from pydantic import BaseModel
5
+
6
+ from ...controller.base import BaseController
7
+ from ..service.base import BaseService
8
+
9
+
10
+ class BeanieBaseController(BaseController):
11
+ """BaseController para Beanie ODM (async).
12
+
13
+ Hereda el comportamiento del controlador base y conecta con
14
+ `Beanie` a través de su `BaseService` sin requerir `db` explícito.
15
+ """
16
+
17
+ service: BaseService = Depends()
18
+ schema_class: ClassVar[Type[BaseModel]]
19
+
20
+ # No es necesario sobreescribir métodos CRUD; el base usa
21
+ # `self.service` con la firma de Beanie (sin `db`).
@@ -0,0 +1,129 @@
1
+ from typing import Any, Dict, List, Optional, Type, Union
2
+
3
+ from bson import ObjectId
4
+ from pydantic import BaseModel
5
+ from beanie import Document
6
+ from beanie.odm.queries.find import FindMany
7
+ from beanie.operators import Or, RegEx
8
+
9
+
10
+ class BaseRepository:
11
+ model: Type[Document]
12
+
13
+ def _get_query_kwargs(
14
+ self,
15
+ fetch_links: bool = False,
16
+ nesting_depths_per_field: Optional[Dict[str, int]] = None,
17
+ projection: Optional[Union[List[str], Type[BaseModel]]] = None,
18
+ ):
19
+ kwargs = {
20
+ "fetch_links": fetch_links,
21
+ "nesting_depths_per_field": (
22
+ nesting_depths_per_field if fetch_links else None
23
+ ),
24
+ }
25
+ if projection is not None:
26
+ kwargs["projection"] = projection
27
+ return kwargs
28
+
29
+ def build_filter_query(
30
+ self,
31
+ search: Optional[str],
32
+ search_fields: List[str],
33
+ filters: dict = None,
34
+ **kwargs,
35
+ ) -> FindMany[Document]:
36
+ exprs = []
37
+
38
+ if search and search_fields:
39
+ exprs.append(
40
+ Or(
41
+ *[
42
+ RegEx(
43
+ getattr(self.model, f),
44
+ f".*{search}.*",
45
+ options="i",
46
+ )
47
+ for f in search_fields
48
+ ]
49
+ )
50
+ )
51
+
52
+ for k, v in (filters or {}).items():
53
+ if hasattr(self.model, k):
54
+ exprs.append(getattr(self.model, k) == v)
55
+
56
+ query = self.model.find(*exprs, **self._get_query_kwargs(**kwargs))
57
+ return query
58
+
59
+ async def paginate(
60
+ self, query: FindMany[Document], page: int, count: int
61
+ ) -> tuple[List[Document], int]:
62
+ total = await query.count()
63
+ items = await query.skip(count * (page - 1)).limit(count).to_list()
64
+ return items, total
65
+
66
+ async def get_by_id(
67
+ self,
68
+ obj_id: Union[str, ObjectId],
69
+ **kwargs,
70
+ ) -> Optional[Document]:
71
+ if not isinstance(obj_id, ObjectId):
72
+ obj_id = ObjectId(obj_id)
73
+ return await self.model.find_one(
74
+ self.model.id == obj_id,
75
+ **self._get_query_kwargs(**kwargs),
76
+ )
77
+
78
+ async def get_by_field(
79
+ self,
80
+ field_name: str,
81
+ value: Any,
82
+ **kwargs,
83
+ ) -> Optional[Document]:
84
+ if not hasattr(self.model, field_name):
85
+ raise AttributeError(
86
+ f"{self.model.__name__} no tiene el campo '{field_name}'"
87
+ )
88
+ return await self.model.find_one(
89
+ getattr(self.model, field_name) == value,
90
+ **self._get_query_kwargs(**kwargs),
91
+ )
92
+
93
+ async def get_by_fields(
94
+ self,
95
+ filters: Dict[str, Any],
96
+ **kwargs,
97
+ ) -> Optional[Document]:
98
+ exprs = [
99
+ getattr(self.model, f) == v
100
+ for f, v in filters.items()
101
+ if hasattr(self.model, f)
102
+ ]
103
+ if not exprs:
104
+ return None
105
+ return await self.model.find_one(
106
+ *exprs, **self._get_query_kwargs(**kwargs)
107
+ )
108
+
109
+ async def list_all(
110
+ self,
111
+ **kwargs,
112
+ ) -> List[Document]:
113
+ query = self.model.find_all(**self._get_query_kwargs(**kwargs))
114
+ return await query.to_list()
115
+
116
+ async def create(self, obj: Union[Document, Dict[str, Any]]) -> Document:
117
+ if isinstance(obj, dict):
118
+ obj = self.model(**obj)
119
+ await obj.insert()
120
+ return obj
121
+
122
+ async def update(self, obj: Document, data: Dict[str, Any]) -> Document:
123
+ for key, value in data.items():
124
+ setattr(obj, key, value)
125
+ await obj.save()
126
+ return obj
127
+
128
+ async def delete(self, obj: Document) -> None:
129
+ await obj.delete()
@@ -0,0 +1,115 @@
1
+ from typing import Any, Dict, List, Optional, Union
2
+
3
+ from fastapi import Request
4
+ from pydantic import BaseModel
5
+
6
+
7
+ from ...beanie.repository.base import BaseRepository
8
+ from ....exceptions.api_exceptions import (
9
+ NotFoundException,
10
+ DatabaseIntegrityException,
11
+ )
12
+
13
+
14
+ class BaseService:
15
+ """Servicio base específico para Beanie ODM (async)."""
16
+
17
+ repository: BaseRepository
18
+ search_fields: List[str] = []
19
+ duplicate_check_fields: List[str] = []
20
+ kwargs_query: Dict[str, Union[str, int]] = {}
21
+ action: str = ""
22
+
23
+ def __init__(
24
+ self, repository: BaseRepository, request: Optional[Request] = None
25
+ ):
26
+ self.repository = repository
27
+ self.request = request
28
+ endpoint_func = (
29
+ self.request.scope.get("endpoint") if self.request else None
30
+ )
31
+ self.action = endpoint_func.__name__ if endpoint_func else None
32
+
33
+ async def _check_duplicate(self, data: Dict[str, Any], fields: List[str]):
34
+ filters = {f: data[f] for f in fields if f in data}
35
+ if not filters:
36
+ return
37
+
38
+ existing = await self.repository.get_by_fields(filters)
39
+ if existing:
40
+ raise DatabaseIntegrityException(
41
+ message="Registro ya existe",
42
+ data=filters,
43
+ )
44
+
45
+ def get_kwargs_query(self) -> Dict[str, Any]:
46
+ return self.kwargs_query
47
+
48
+ def get_filters(
49
+ self,
50
+ filters: Optional[Dict[str, Any]] = None,
51
+ ) -> Dict[str, Any]:
52
+ filters = filters or {}
53
+ return filters
54
+
55
+ async def retrieve(self, id: str):
56
+ kwargs = self.get_kwargs_query()
57
+ obj = await self.repository.get_by_id(id, **kwargs)
58
+ if not obj:
59
+ raise NotFoundException(f"id={id} no encontrado")
60
+ return obj
61
+
62
+ async def list(
63
+ self,
64
+ search: Optional[str] = None,
65
+ page: int = 1,
66
+ count: int = 25,
67
+ filters: Optional[Dict[str, Any]] = None,
68
+ ):
69
+ kwargs = self.get_kwargs_query()
70
+ applied_filters = self.get_filters(filters)
71
+ query = self.repository.build_filter_query(
72
+ search=search,
73
+ search_fields=self.search_fields,
74
+ filters=applied_filters,
75
+ **kwargs,
76
+ )
77
+ return await self.repository.paginate(query, page, count)
78
+
79
+ async def create(
80
+ self, payload: BaseModel, check_fields: Optional[List[str]] = None
81
+ ) -> Any:
82
+ data = (
83
+ payload.model_dump() if not isinstance(payload, dict) else payload
84
+ )
85
+ fields = (
86
+ check_fields
87
+ if check_fields is not None
88
+ else self.duplicate_check_fields
89
+ )
90
+ if fields:
91
+ await self._check_duplicate(data, fields)
92
+ created = await self.repository.create(data)
93
+ kwargs = self.get_kwargs_query()
94
+ return (
95
+ await self.repository.get_by_id(created.id, **kwargs)
96
+ if kwargs
97
+ else created
98
+ )
99
+
100
+ async def update(self, id: str, data: BaseModel) -> Any:
101
+ kwargs = self.get_kwargs_query()
102
+ obj = await self.repository.get_by_id(id, **kwargs)
103
+ if not obj:
104
+ raise NotFoundException(f"id={id} no encontrado")
105
+ if isinstance(data, BaseModel):
106
+ data = data.model_dump(exclude_unset=True)
107
+ updated = await self.repository.update(obj, data)
108
+ return updated
109
+
110
+ async def delete(self, id: str) -> str:
111
+ obj = await self.repository.get_by_id(id)
112
+ if not obj:
113
+ raise NotFoundException(f"id={id} no encontrado")
114
+ await self.repository.delete(obj)
115
+ return "deleted"
File without changes
@@ -0,0 +1,131 @@
1
+ from typing import Any, ClassVar, Dict, List, Optional, Type
2
+ from fastapi import Depends, Request
3
+ from pydantic import BaseModel, TypeAdapter
4
+
5
+ from ..permissions.base import BasePermission
6
+
7
+ from ...schema.base import BasePaginationResponse, BaseResponse
8
+ from ...exceptions.api_exceptions import PermissionException
9
+
10
+
11
+ class BaseController:
12
+ """Montar rutas CRUD genericas y captura errores de negocio."""
13
+
14
+ service = Depends()
15
+ schema_class: ClassVar[Type[BaseModel]]
16
+ action: ClassVar[Optional[str]] = None
17
+ request: Request
18
+
19
+ def __init__(self) -> None:
20
+ endpoint_func = (
21
+ self.request.scope.get("endpoint") if self.request else None
22
+ )
23
+ self.action = endpoint_func.__name__ if endpoint_func else None
24
+
25
+ def get_schema_class(self) -> Type[BaseModel]:
26
+ assert self.schema_class is not None, (
27
+ "'%s' should either include a `schema_class` attribute, "
28
+ "or override the `get_serializer_class()` method."
29
+ % self.__class__.__name__
30
+ )
31
+ return self.schema_class
32
+
33
+ async def check_permissions_class(self):
34
+ permissions = self.check_permissions()
35
+ if permissions:
36
+ for permission in permissions:
37
+ obj = permission()
38
+ check = await obj.has_permission(self.request)
39
+ if not check:
40
+ raise PermissionException(obj.message_exception)
41
+
42
+ def check_permissions(self) -> List[Type[BasePermission]]:
43
+ pass
44
+
45
+ async def list(self):
46
+ params = self._params()
47
+ items, total = await self.service.list(**params)
48
+ pagination = {
49
+ "page": params.get("page"),
50
+ "count": params.get("count"),
51
+ "total": total,
52
+ }
53
+ return self.format_response(data=items, pagination=pagination)
54
+
55
+ async def retrieve(self, id: str):
56
+ item = await self.service.retrieve(id)
57
+ return self.format_response(data=item)
58
+
59
+ async def create(self, validated_data: Any):
60
+ result = await self.service.create(validated_data)
61
+ return self.format_response(result, message="Creado exitosamente")
62
+
63
+ async def update(self, id: str, validated_data: Any):
64
+ result = await self.service.update(id, validated_data)
65
+ return self.format_response(result, message="Actualizado exitosamente")
66
+
67
+ async def delete(self, id: str):
68
+ await self.service.delete(id)
69
+ return self.format_response(None, message="Eliminado exitosamente")
70
+
71
+ def format_response(
72
+ self,
73
+ data: Any,
74
+ pagination: Optional[Dict[str, Any]] = None,
75
+ message: Optional[str] = None,
76
+ status: str = "success",
77
+ ) -> BaseModel:
78
+ schema = self.get_schema_class()
79
+
80
+ if isinstance(data, list):
81
+ data_dicts = [self.to_dict(item) for item in data]
82
+ adapter = TypeAdapter(List[schema])
83
+ data_parsed = adapter.validate_python(data_dicts)
84
+ elif self.service.repository and isinstance(
85
+ data, self.service.repository.model
86
+ ):
87
+ data_parsed = self.to_dict(data)
88
+ data_parsed = schema.model_validate(data_parsed)
89
+ elif isinstance(data, dict):
90
+ data_parsed = schema.model_validate(data)
91
+ else:
92
+ data_parsed = data
93
+
94
+ if pagination:
95
+ return BasePaginationResponse(
96
+ data=data_parsed,
97
+ pagination=pagination,
98
+ message=message or "Operación exitosa",
99
+ status=status,
100
+ )
101
+ else:
102
+ return BaseResponse(
103
+ data=data_parsed,
104
+ message=message or "Operación exitosa",
105
+ status=status,
106
+ )
107
+
108
+ def _params(self) -> Dict[str, Any]:
109
+ query_params = self.request.query_params if self.request else {}
110
+
111
+ page = int(query_params.get("page", 1))
112
+ count = int(query_params.get("count", 10))
113
+ search = query_params.get("search")
114
+
115
+ filters = {
116
+ k: v
117
+ for k, v in query_params.items()
118
+ if k not in ["page", "count", "search"]
119
+ }
120
+
121
+ return {
122
+ "page": page,
123
+ "count": count,
124
+ "search": search,
125
+ "filters": filters,
126
+ }
127
+
128
+ def to_dict(self, obj: Any):
129
+ if hasattr(obj, "model_dump"):
130
+ return obj.model_dump()
131
+ return obj
File without changes
@@ -0,0 +1,10 @@
1
+ from fastapi import Request
2
+
3
+
4
+ class BasePermission:
5
+ message_exception: str = "Permiso denegado"
6
+ """Clase base de permisos, para extender según lógica."""
7
+
8
+ async def has_permission(self, request: Request) -> bool:
9
+ """Sobreescribir con la lógica de permiso."""
10
+ return True
@@ -0,0 +1,6 @@
1
+ __all__ = ["repository", "service", "controller"]
2
+ from .controller.base import SQLAlchemyBaseController
3
+
4
+ __all__ = [
5
+ "SQLAlchemyBaseController",
6
+ ]
@@ -0,0 +1,68 @@
1
+ from typing import Any, ClassVar, List, Optional, Type
2
+
3
+ from fastapi import Depends
4
+ from pydantic import BaseModel
5
+ from sqlalchemy.ext.asyncio import AsyncSession
6
+
7
+ from ...controller.base import BaseController
8
+ from ..service.base import BaseService
9
+
10
+
11
+ class SQLAlchemyBaseController(BaseController):
12
+ """BaseController para SQLAlchemy (AsyncSession).
13
+
14
+ - Acepta `db: AsyncSession` en operaciones CRUD.
15
+ - Construye y pasa parámetros específicos (joins, order_by, use_or).
16
+ """
17
+
18
+ service: BaseService = Depends()
19
+ schema_class: ClassVar[Type[BaseModel]]
20
+
21
+ async def list(
22
+ self,
23
+ db: AsyncSession,
24
+ *,
25
+ use_or: bool = False,
26
+ joins: Optional[List[str]] = None,
27
+ order_by: Optional[Any] = None,
28
+ ):
29
+ params = self._params()
30
+ service_params = {
31
+ "page": params.get("page"),
32
+ "count": params.get("count"),
33
+ "filters": params.get("filters"),
34
+ "use_or": use_or,
35
+ "joins": joins,
36
+ "order_by": order_by,
37
+ }
38
+ items, total = await self.service.list(db, **service_params)
39
+ pagination = {
40
+ "page": params.get("page"),
41
+ "count": params.get("count"),
42
+ "total": total,
43
+ }
44
+ return self.format_response(data=items, pagination=pagination)
45
+
46
+ async def retrieve(
47
+ self, db: AsyncSession, id: str, *, joins: Optional[List[str]] = None
48
+ ):
49
+ item = await self.service.retrieve(db, id, joins=joins)
50
+ return self.format_response(data=item)
51
+
52
+ async def create(
53
+ self,
54
+ db: AsyncSession,
55
+ validated_data: Any,
56
+ *,
57
+ check_fields: Optional[List[str]] = None,
58
+ ):
59
+ result = await self.service.create(db, validated_data, check_fields)
60
+ return self.format_response(result, message="Creado exitosamente")
61
+
62
+ async def update(self, db: AsyncSession, id: str, validated_data: Any):
63
+ result = await self.service.update(db, id, validated_data)
64
+ return self.format_response(result, message="Actualizado exitosamente")
65
+
66
+ async def delete(self, db: AsyncSession, id: str):
67
+ await self.service.delete(db, id)
68
+ return self.format_response(None, message="Eliminado exitosamente")