cashing2fast-fastapi 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.
@@ -0,0 +1,12 @@
1
+ from .__version__ import __version__
2
+ from .settings import settings
3
+ from .dependencies import require_billing_checks
4
+ from .utils.redis_client import get_redis_client, close_redis
5
+
6
+ __all__ = [
7
+ "__version__",
8
+ "settings",
9
+ "require_billing_checks",
10
+ "get_redis_client",
11
+ "close_redis",
12
+ ]
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
@@ -0,0 +1,91 @@
1
+ from typing import Any
2
+ from datetime import datetime, timezone
3
+ from fastapi import Depends, HTTPException, status, Request
4
+ from sqlmodel.ext.asyncio.session import AsyncSession
5
+ from oauth2fast_fastapi.dependencies import get_auth_session, oauth2_dependency
6
+ from oauth2fast_fastapi.utils.token_utils import verify_token
7
+ from tools2fast_fastapi import APIResponse
8
+
9
+ from .settings import settings
10
+ from .services import billing_service
11
+ from .exceptions import PaymentRequiredException
12
+
13
+ async def require_billing_checks(
14
+ request: Request,
15
+ token: str = Depends(oauth2_dependency),
16
+ session: AsyncSession = Depends(get_auth_session)
17
+ ):
18
+ """
19
+ Dependency to check if the user has billing limits.
20
+ """
21
+ # 1. Decode token to get email without hitting User DB yet
22
+ payload = verify_token(token)
23
+ if not payload:
24
+ raise HTTPException(
25
+ status_code=status.HTTP_401_UNAUTHORIZED,
26
+ detail="Token inválido",
27
+ headers={"WWW-Authenticate": "Bearer"},
28
+ )
29
+
30
+ email = payload.get("sub")
31
+ if not email:
32
+ raise HTTPException(
33
+ status_code=status.HTTP_401_UNAUTHORIZED,
34
+ detail="Token sin identificador",
35
+ )
36
+
37
+ # 2. Get User ID and created_at (cached in Redis)
38
+ try:
39
+ user_info = await billing_service.get_user_billing_info(email, session)
40
+ except Exception:
41
+ raise HTTPException(
42
+ status_code=status.HTTP_401_UNAUTHORIZED,
43
+ detail="Usuario no encontrado",
44
+ )
45
+
46
+ user_id = user_info["id"]
47
+ created_at = datetime.fromisoformat(user_info["created_at"])
48
+ if created_at.tzinfo is None:
49
+ created_at = created_at.replace(tzinfo=timezone.utc)
50
+
51
+ # 3. Calculate elapsed minutes
52
+ now = datetime.now(timezone.utc)
53
+ elapsed_minutes = (now - created_at).total_seconds() / 60
54
+
55
+ # 4. Evaluate phases
56
+
57
+ # Phase 1: Unlimited (Free period)
58
+ if elapsed_minutes <= settings.free_minutes:
59
+ return True
60
+
61
+ # Phase 2: Tracked (Redirect period)
62
+ if elapsed_minutes <= (settings.free_minutes + settings.redirect_minutes):
63
+ count = await billing_service.increment_request_count(user_id)
64
+ if count > settings.max_requests:
65
+ # Reseteo el valor en el redis para que la pueda seguir ocupando
66
+ # hasta llegar a ese número nuevamente después de la redirección
67
+ await billing_service.reset_request_count(user_id)
68
+ # Raise exception to stop execution and return 402
69
+ raise PaymentRequiredException(
70
+ message="Límite de peticiones alcanzado. Por favor, realiza un pago."
71
+ )
72
+ return True
73
+
74
+ # Phase 3: Blocked (Expired)
75
+ raise PaymentRequiredException(
76
+ message="Su periodo de uso ha expirado. Por favor, realice un pago para continuar."
77
+ )
78
+
79
+ def register_billing_exception_handler(app: Any):
80
+ """
81
+ Register the global exception handler for PaymentRequiredException.
82
+ """
83
+ from fastapi import Request
84
+ from fastapi.responses import JSONResponse
85
+
86
+ @app.exception_handler(PaymentRequiredException)
87
+ async def billing_exception_handler(request: Request, exc: PaymentRequiredException):
88
+ return APIResponse.payment_required(
89
+ message=exc.message,
90
+ error=exc.error
91
+ )
@@ -0,0 +1,6 @@
1
+ from typing import Any
2
+
3
+ class PaymentRequiredException(Exception):
4
+ def __init__(self, message: str = "Pago Requerido", error: Any = None):
5
+ self.message = message
6
+ self.error = error
@@ -0,0 +1,7 @@
1
+ from .billing_service import get_user_billing_info, increment_request_count, reset_request_count
2
+
3
+ __all__ = [
4
+ "get_user_billing_info",
5
+ "increment_request_count",
6
+ "reset_request_count",
7
+ ]
@@ -0,0 +1,62 @@
1
+ import json
2
+ from datetime import datetime
3
+ from typing import TypedDict, Any
4
+ from sqlmodel import select
5
+ from sqlmodel.ext.asyncio.session import AsyncSession
6
+ from oauth2fast_fastapi import User
7
+ from ..utils.redis_client import get_redis_client
8
+ from ..settings import settings
9
+
10
+ class UserAuthCache(TypedDict):
11
+ id: int
12
+ created_at: str
13
+
14
+ async def get_user_billing_info(email: str, session: AsyncSession) -> UserAuthCache:
15
+ """
16
+ Obtiene id y created_at del usuario, priorizando Redis.
17
+ Si no está en Redis, consulta DB y lo guarda.
18
+ """
19
+ redis = get_redis_client()
20
+ key = f"cashing:user_auth:{email}"
21
+
22
+ cached_data = await redis.get(key)
23
+ if cached_data:
24
+ try:
25
+ return json.loads(cached_data)
26
+ except json.JSONDecodeError:
27
+ pass
28
+
29
+ # Si no hay caché, buscar en DB
30
+ result = await session.exec(select(User).where(User.email == email))
31
+ user = result.one_or_none()
32
+
33
+ if not user:
34
+ raise ValueError(f"User with email {email} not found")
35
+
36
+ info: UserAuthCache = {
37
+ "id": user.id,
38
+ "created_at": user.created_at.isoformat()
39
+ }
40
+
41
+ # Guardar en caché
42
+ await redis.setex(
43
+ key,
44
+ settings.user_cache_ttl,
45
+ json.dumps(info)
46
+ )
47
+
48
+ return info
49
+
50
+ async def increment_request_count(user_id: int) -> int:
51
+ """Incrementa el contador de peticiones del usuario en Redis."""
52
+ redis = get_redis_client()
53
+ key = f"cashing:{user_id}:requests"
54
+
55
+ # Incrementa y devuelve el nuevo valor
56
+ return await redis.incr(key)
57
+
58
+ async def reset_request_count(user_id: int):
59
+ """Resetea el contador de peticiones del usuario en Redis."""
60
+ redis = get_redis_client()
61
+ key = f"cashing:{user_id}:requests"
62
+ await redis.set(key, 0)
@@ -0,0 +1,68 @@
1
+ """
2
+ Cashing2Fast FastAPI Settings
3
+
4
+ Configuration for cashing2fast-fastapi module using pydantic-settings.
5
+ Reads from environment variables with CASHING_ prefix.
6
+ """
7
+
8
+ import os
9
+ from pydantic import BaseModel, SecretStr
10
+ from pydantic_settings import BaseSettings, SettingsConfigDict
11
+
12
+ # Look for .env in the current working directory (where the app is running)
13
+ DOTENV_PATH = os.path.join(os.getcwd(), ".env")
14
+
15
+ class RedisSettings(BaseModel):
16
+ """Redis configuration for caching billing info."""
17
+ host: str = "localhost"
18
+ port: int = 6379
19
+ db: int = 0
20
+ password: SecretStr | None = None
21
+ decode_responses: bool = True
22
+
23
+ class Settings(BaseSettings):
24
+ """Billing and request limit configuration settings."""
25
+
26
+ model_config = SettingsConfigDict(
27
+ env_file=DOTENV_PATH,
28
+ env_file_encoding="utf-8",
29
+ env_prefix="CASHING_",
30
+ env_nested_delimiter="__",
31
+ extra="ignore",
32
+ )
33
+
34
+ # Redis settings
35
+ redis: RedisSettings = RedisSettings()
36
+
37
+ # Feature flags
38
+ redis_enabled: bool = True
39
+
40
+ # Billing Logic Limits
41
+ # Period where users are never penalized/redirected (in minutes)
42
+ free_minutes: int = 0
43
+ # Period following the free minutes where users are tracked (in minutes)
44
+ redirect_minutes: int = 60
45
+ # Number of allowed requests during the redirect_minutes phase
46
+ max_requests: int = 100
47
+
48
+ # Cache settings
49
+ # Default TTL for cached user info (created_at)
50
+ user_cache_ttl: int = 86400 # 24 hours
51
+
52
+ try:
53
+ settings = Settings()
54
+ except Exception as e:
55
+ # Use log2fast_fastapi for proper error logging if available
56
+ try:
57
+ from log2fast_fastapi import get_logger
58
+ logger = get_logger(__name__)
59
+ logger.exception(
60
+ "🚨 Error loading Cashing2Fast configuration",
61
+ extra_data={
62
+ "error": str(e),
63
+ "dotenv_path": DOTENV_PATH,
64
+ },
65
+ )
66
+ except ImportError:
67
+ print(f"🚨 Error loading Cashing2Fast configuration: {e}")
68
+ raise
@@ -0,0 +1,6 @@
1
+ from .redis_client import get_redis_client, close_redis
2
+
3
+ __all__ = [
4
+ "get_redis_client",
5
+ "close_redis",
6
+ ]
@@ -0,0 +1,25 @@
1
+ from redis.asyncio import Redis
2
+ from ..settings import settings
3
+
4
+ # Global redis pool
5
+ _redis_client: Redis | None = None
6
+
7
+ def get_redis_client() -> Redis:
8
+ """Get or initialize the Redis client from settings."""
9
+ global _redis_client
10
+ if _redis_client is None:
11
+ _redis_client = Redis(
12
+ host=settings.redis.host,
13
+ port=settings.redis.port,
14
+ db=settings.redis.db,
15
+ password=settings.redis.password.get_secret_value() if settings.redis.password else None,
16
+ decode_responses=settings.redis.decode_responses,
17
+ )
18
+ return _redis_client
19
+
20
+ async def close_redis() -> None:
21
+ """Close the Redis connection pool."""
22
+ global _redis_client
23
+ if _redis_client is not None:
24
+ await _redis_client.aclose()
25
+ _redis_client = None
@@ -0,0 +1,65 @@
1
+ Metadata-Version: 2.4
2
+ Name: cashing2fast-fastapi
3
+ Version: 0.1.0
4
+ Summary: Billing and request limit control for FastAPI
5
+ Author-email: Angel Daniel Sanchez Castillo <angeldaniel.sanchezcastillo@gmail.com>
6
+ License: MIT License
7
+
8
+ Copyright (c) 2026 Angel Daniel Sanchez Castillo
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+
28
+ Keywords: fastapi,billing,request-limit,redis,oauth2
29
+ Classifier: Development Status :: 3 - Alpha
30
+ Classifier: Intended Audience :: Developers
31
+ Classifier: License :: OSI Approved :: MIT License
32
+ Classifier: Programming Language :: Python :: 3
33
+ Classifier: Programming Language :: Python :: 3.10
34
+ Classifier: Programming Language :: Python :: 3.11
35
+ Classifier: Programming Language :: Python :: 3.12
36
+ Classifier: Framework :: FastAPI
37
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
38
+ Requires-Python: >=3.10
39
+ Description-Content-Type: text/markdown
40
+ License-File: LICENSE
41
+ Requires-Dist: oauth2fast-fastapi>=0.3.0
42
+ Requires-Dist: tools2fast-fastapi>=0.1.5
43
+ Requires-Dist: redis>=5.0.0
44
+ Requires-Dist: pydantic-settings>=2.0.0
45
+ Dynamic: license-file
46
+
47
+ # cashing2fast-fastapi
48
+ 🚀 Simple and fast cashing tools for FastAPI with minimal configuration
49
+
50
+ ## Documentación
51
+
52
+ - [Guía de Uso (Usage Guide)](docs/usage.md)
53
+ - [Ejemplos de configuración](examples/.env.examples)
54
+
55
+ ## Instalación
56
+
57
+ ```bash
58
+ uv add cashing2fast-fastapi
59
+ ```
60
+
61
+ ## Características
62
+
63
+ - 🎯 **Lógica de 3 Fases:** Tiempo gratuito, periodo de cobro por peticiones y bloqueo final.
64
+ - ⚡ **Redis Native:** Contadores atómicos y caché de usuario para evitar cargas innecesarias a DB.
65
+ - 🛡️ **Tools2Fast Integration:** Respuestas con formato estándar y código HTTP 402.
@@ -0,0 +1,14 @@
1
+ cashing2fast_fastapi/__init__.py,sha256=w9yMZm1ibdJn_sAzHQL_D55dGdslRAClI4GeT815HsA,302
2
+ cashing2fast_fastapi/__version__.py,sha256=kUR5RAFc7HCeiqdlX36dZOHkUI5wI6V_43RpEcD8b-0,22
3
+ cashing2fast_fastapi/dependencies.py,sha256=90EwjQvKKsoi_Lrx12iT6ioP6AApH8SD1TmXQDU-AtA,3346
4
+ cashing2fast_fastapi/exceptions.py,sha256=l8BcuZ4RzXWMNx23qWF9VYe6fSfjYvcIp6Aq-VZRAV4,201
5
+ cashing2fast_fastapi/settings.py,sha256=FE1s9WTDjDKWIK1rX0s4rCAkWpBcYzX135CfvYB_O88,2017
6
+ cashing2fast_fastapi/services/__init__.py,sha256=njESeZgOSorh96QTNZswD-CaAUas0koe4LwDagrv0nM,199
7
+ cashing2fast_fastapi/services/billing_service.py,sha256=1aMOWWoNK_f63HEZcm58mWwbieEi1iWWXaAfUjELioM,1793
8
+ cashing2fast_fastapi/utils/__init__.py,sha256=peCIbzMM-kRaltBlo2vriO_llMWKmiIcLi5D6RtMk_0,114
9
+ cashing2fast_fastapi/utils/redis_client.py,sha256=dBw9V2y3CZ6OSlVfRsggn2db7DOyMedSAof9KZ9dVmc,811
10
+ cashing2fast_fastapi-0.1.0.dist-info/licenses/LICENSE,sha256=CkISX1hNEwxxrPTOXet3IYMEH28Bn7SoUyKniRjg68I,1086
11
+ cashing2fast_fastapi-0.1.0.dist-info/METADATA,sha256=ihueuBUDENqJ7xFhlPWNcG6Wnin8lzqSzB2bOzDcFEo,2814
12
+ cashing2fast_fastapi-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
13
+ cashing2fast_fastapi-0.1.0.dist-info/top_level.txt,sha256=RhBlMDmmnW5ILWHO0Kn5P15VI_TqDYQ7JuZ6XvKhvRM,21
14
+ cashing2fast_fastapi-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Angel Daniel Sanchez Castillo
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ cashing2fast_fastapi