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.
- cashing2fast_fastapi/__init__.py +12 -0
- cashing2fast_fastapi/__version__.py +1 -0
- cashing2fast_fastapi/dependencies.py +91 -0
- cashing2fast_fastapi/exceptions.py +6 -0
- cashing2fast_fastapi/services/__init__.py +7 -0
- cashing2fast_fastapi/services/billing_service.py +62 -0
- cashing2fast_fastapi/settings.py +68 -0
- cashing2fast_fastapi/utils/__init__.py +6 -0
- cashing2fast_fastapi/utils/redis_client.py +25 -0
- cashing2fast_fastapi-0.1.0.dist-info/METADATA +65 -0
- cashing2fast_fastapi-0.1.0.dist-info/RECORD +14 -0
- cashing2fast_fastapi-0.1.0.dist-info/WHEEL +5 -0
- cashing2fast_fastapi-0.1.0.dist-info/licenses/LICENSE +21 -0
- cashing2fast_fastapi-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -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,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,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,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
|