Skip to content

Commit 32eec99

Browse files
Merge branch 'develop' into gastos
2 parents a747493 + 5c7c3f7 commit 32eec99

38 files changed

+1124
-91
lines changed

.github/workflows/pytest.yml

+1
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ jobs:
3333
- uses: pavelzw/pytest-action@v2
3434
env:
3535
SECRET_KEY: ${{ secrets.DJANGO_SECRET_KEY }}
36+
OPENAI_KEY: ${{ secrets.OPENAI_KEY }}
3637
AWS_REGION: us-east-2
3738
with:
3839
emoji: false

AI/AI.py

+21-8
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
1+
# flake8: noqa
12
import openai
23
import os
4+
from textwrap import dedent
5+
from dotenv import load_dotenv
36

47

58
def setup_api_key():
6-
openai.api_key = os.environ["OPENAI_KEY"]
9+
env_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), ".env")
10+
load_dotenv(dotenv_path=env_path)
11+
openai.api_key = os.getenv("OPENAI_KEY")
712

813

914
def classify_text(texto):
@@ -12,21 +17,29 @@ def classify_text(texto):
1217
messages=[
1318
{
1419
"role": "system",
15-
"content": "Eres un modelo que clasifica gastos en \
16-
categorías predefinidas.",
20+
"content": dedent(
21+
"""\
22+
Eres un modelo que clasifica gastos en categorías predefinidas.
23+
"""
24+
),
1725
},
1826
{
1927
"role": "user",
20-
"content": f"Clasifica el siguiente gasto en una de las \
21-
categorías: comida, transporte, vivienda, educación, salud, \
22-
entretenimiento,\
23-
ahorro, inversión.\n\nTexto: {texto}\n\nCategoría:",
28+
"content": dedent(
29+
f"""\
30+
Clasifica el siguiente gasto en una de las categorías: comida, transporte, vivienda, educación, salud, entretenimiento, ahorro, inversión.
31+
32+
Texto: {texto}
33+
34+
Categoría:
35+
"""
36+
),
2437
},
2538
],
2639
max_tokens=15,
2740
temperature=0,
2841
)
29-
category = response.choices[0].message["content"].strip()
42+
category = response["choices"][0]["message"]["content"].strip()
3043
return category
3144

3245

AI/tests.py

+63
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
# flake8: noqa
2+
import unittest
3+
from unittest.mock import patch
4+
from AI import classify_text, clean_category
5+
from textwrap import dedent
6+
7+
8+
class TestGastosClassifier(unittest.TestCase):
9+
10+
@patch("openai.ChatCompletion.create")
11+
def test_classify_text(self, mock_create):
12+
# Configurar el mock para devolver una respuesta simulada
13+
mock_create.return_value = {"choices": [{"message": {"role": "assistant", "content": "Comida."}}]}
14+
15+
# Llamar a la función a probar
16+
texto = "Compré una pizza."
17+
categoria = classify_text(texto)
18+
19+
# Verificar que la respuesta sea la esperada
20+
self.assertEqual(categoria, "Comida.")
21+
22+
# Verificar que la API fue llamada con los parámetros correctos
23+
mock_create.assert_called_once_with(
24+
model="gpt-4-turbo",
25+
messages=[
26+
{
27+
"role": "system",
28+
"content": dedent(
29+
"""\
30+
Eres un modelo que clasifica gastos en categorías predefinidas.
31+
"""
32+
),
33+
},
34+
{
35+
"role": "user",
36+
"content": dedent(
37+
f"""\
38+
Clasifica el siguiente gasto en una de las categorías: comida, transporte, vivienda, educación, salud, entretenimiento, ahorro, inversión.
39+
40+
Texto: {texto}
41+
42+
Categoría:
43+
"""
44+
),
45+
},
46+
],
47+
max_tokens=15,
48+
temperature=0,
49+
)
50+
51+
def test_clean_category(self):
52+
# Pruebas con diferentes casos
53+
self.assertEqual(clean_category("Categoría: Comida."), "Comida")
54+
self.assertEqual(clean_category("categoría: transporte"), "Transporte")
55+
self.assertEqual(clean_category("vivienda."), "Vivienda")
56+
57+
# Prueba de una categoría no permitida
58+
with self.assertRaises(ValueError):
59+
clean_category("Categoría: Viajes.")
60+
61+
62+
if __name__ == "__main__":
63+
unittest.main()

Pipfile

+1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ name = "pypi"
77
django = "*"
88
psycopg2-binary = "*"
99
python-dotenv = "*"
10+
openai = "==0.28"
1011
djangorestframework = "*"
1112
boto3 = "*"
1213
pyjwt = "*"

Pipfile.lock

+420-30
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

authentication/decorators.py

+13-15
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,31 @@
11
from functools import wraps
22
from rest_framework.response import Response
33
from rest_framework import status
4-
from rest_framework.decorators import api_view, authentication_classes
4+
from django.contrib.auth import get_user_model
55
from .services.cognito_authentication import CognitoAuthentication
6+
from .utils import get_user_id_from_token
7+
8+
User = get_user_model()
69

710

811
def cognito_authenticated(func):
912
@wraps(func)
10-
def wrapper(self, request, *args, **kwargs):
11-
auth = CognitoAuthentication()
13+
def wrapper(view_instance, request, *args, **kwargs):
1214
try:
13-
result = auth.authenticate(request)
14-
if result is None:
15-
return Response(
16-
{"error": "Authentication credentials were not provided or are invalid."},
17-
status=status.HTTP_401_UNAUTHORIZED,
18-
)
19-
user, auth_error = result
20-
if auth_error or not user:
15+
user_id = get_user_id_from_token(request)
16+
try:
17+
user = User.objects.get(user_id=user_id)
18+
request.user = user
19+
except User.DoesNotExist:
2120
return Response(
22-
{"error": "Authentication credentials were not provided or are invalid."},
21+
{"error": "Authenticated user not found."},
2322
status=status.HTTP_401_UNAUTHORIZED,
2423
)
25-
request.user = user
26-
return func(self, request, *args, **kwargs)
24+
return func(view_instance, request, *args, **kwargs)
2725
except Exception as e:
2826
return Response(
2927
{"error": f"Authentication failed: {str(e)}"},
30-
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
28+
status=status.HTTP_401_UNAUTHORIZED,
3129
)
3230

3331
return wrapper

authentication/serializers.py

+7
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
11
from rest_framework import serializers
2+
from .models import User
3+
4+
5+
class UserSerializer(serializers.ModelSerializer):
6+
class Meta:
7+
model = User
8+
fields = ["user_id", "first_name", "phone", "email"]
29

310

411
class RegisterSerializer(serializers.Serializer):

authentication/tests/tests.py

+22
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
from rest_framework.test import APIClient
44
from rest_framework import status
55
from authentication.services.cognito_service import CognitoService
6+
from authentication.views import ProfileView
7+
from rest_framework.response import Response
68

79

810
class RegisterViewTests(TestCase):
@@ -53,3 +55,23 @@ def test_login_user_failure(self, mock_login_user):
5355
response = self.client.post(self.url, self.data, format="json")
5456
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
5557
self.assertEqual(response.data["error"], "Login failed")
58+
59+
60+
class ProfileViewTests(TestCase):
61+
def setUp(self):
62+
self.client = APIClient()
63+
self.url = "/auth/profile/"
64+
self.data = {"email": "johndoe@example.com", "password": "securepassword123"}
65+
66+
@patch.object(ProfileView, "get")
67+
@patch.object(CognitoService, "login_user")
68+
def test_get(self, mock_login_user, mock_get):
69+
mock_login_user.return_value = {
70+
"AuthenticationResult": {"AccessToken": "mock_access_token", "IdToken": "mock_id_token"}
71+
}
72+
mock_get.return_value = Response(status=status.HTTP_200_OK, data={"first_name": "John Doe"})
73+
request = self.client.get(self.url)
74+
request.headers["Authorization"] = "Bearer mock_access_token"
75+
response = self.client.get(self.url)
76+
self.assertEqual(response.status_code, status.HTTP_200_OK)
77+
self.assertEqual(response.data["first_name"], "John Doe")

authentication/urls.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
from django.urls import path
2-
from .views import RegisterView, LoginView
2+
from .views import RegisterView, LoginView, ProfileView
33

44
urlpatterns = [
55
path("register/", RegisterView.as_view(), name="register"),
66
path("login/", LoginView.as_view(), name="login"),
7+
path("profile/", ProfileView.as_view(), name="profile"),
78
]

authentication/utils.py

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import jwt
2+
3+
4+
def get_user_id_from_token(request):
5+
try:
6+
authorization_header = request.headers.get("Authorization")
7+
if not authorization_header:
8+
raise Exception("Authorization header not found")
9+
10+
token = authorization_header.split()[1]
11+
decoded_token = jwt.decode(token, options={"verify_signature": False})
12+
user_id = decoded_token.get("username")
13+
if not user_id:
14+
raise Exception("User ID not found in token")
15+
return user_id
16+
except jwt.DecodeError:
17+
raise Exception("Invalid token")
18+
except jwt.ExpiredSignatureError:
19+
raise Exception("Expired token")
20+
except Exception as e:
21+
raise Exception(f"Error decoding token: {e}")

authentication/views.py

+39
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44
from .serializers import RegisterSerializer, LoginSerializer
55
from .services.cognito_service import CognitoService
66
from django.conf import settings
7+
from .models import User
8+
from .decorators import cognito_authenticated
9+
import jwt
710

811

912
cognito_service = CognitoService(
@@ -48,3 +51,39 @@ def post(self, request):
4851
except Exception as e:
4952
return Response({"error": str(e)}, status=status.HTTP_400_BAD_REQUEST)
5053
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
54+
55+
56+
class ProfileView(APIView):
57+
def get_user_id_from_token(self, request):
58+
try:
59+
authorization_header = request.headers.get("Authorization")
60+
if not authorization_header:
61+
raise Exception("Authorization header not found")
62+
63+
token = authorization_header.split()[1]
64+
decoded_token = jwt.decode(token, options={"verify_signature": False})
65+
user_id = decoded_token.get("username")
66+
if not user_id:
67+
raise Exception("User ID not found in token")
68+
return user_id
69+
except jwt.DecodeError:
70+
raise Exception("Invalid token")
71+
except jwt.ExpiredSignatureError:
72+
raise Exception("Expired token")
73+
except Exception as e:
74+
raise Exception(f"Error decoding token: {e}")
75+
76+
@cognito_authenticated
77+
def get(self, request):
78+
try:
79+
username = self.get_user_id_from_token(request)
80+
user = User.objects.get(user_id=username)
81+
return Response(
82+
{
83+
"user_id": user.user_id,
84+
"first_name": user.first_name,
85+
},
86+
status=status.HTTP_200_OK,
87+
)
88+
except User.DoesNotExist:
89+
return Response({"error": "User not found"}, status=status.HTTP_404_NOT_FOUND)

budget/views.py

+14-34
Original file line numberDiff line numberDiff line change
@@ -3,50 +3,30 @@
33
from .models import Budget
44
from .serializers import BudgetSerializer
55
from authentication.decorators import cognito_authenticated
6-
import jwt
6+
from authentication.utils import get_user_id_from_token
77

88

99
class BudgetViewSet(viewsets.ViewSet):
10-
def get_user_id_from_token(self, request):
11-
try:
12-
authorization_header = request.headers.get("Authorization")
13-
if not authorization_header:
14-
raise Exception("Authorization header not found")
15-
16-
token = authorization_header.split()[1]
17-
decoded_token = jwt.decode(token, options={"verify_signature": False})
18-
user_id = decoded_token.get("username")
19-
if not user_id:
20-
raise Exception("User ID not found in token")
21-
return user_id
22-
except jwt.DecodeError:
23-
raise Exception("Invalid token")
24-
except jwt.ExpiredSignatureError:
25-
raise Exception("Expired token")
26-
except Exception as e:
27-
raise Exception(f"Error decoding token: {e}")
28-
2910
@cognito_authenticated
3011
def list(self, request):
3112
try:
32-
username = self.get_user_id_from_token(request)
33-
budget = Budget.objects.filter(username=username).order_by("-created_at").first()
13+
user_id = get_user_id_from_token(request)
14+
budget = Budget.objects.filter(username=user_id).order_by("-created_at").first()
15+
if not budget:
16+
return Response({"error": "Budget not found"}, status=status.HTTP_404_NOT_FOUND)
3417
serializer = BudgetSerializer(budget)
3518
return Response(data={"amount": serializer.data["amount"]})
36-
except Budget.DoesNotExist:
37-
return Response({"error": "Budget not found"}, status=status.HTTP_404_NOT_FOUND)
3819
except Exception as e:
3920
return Response({"error": str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
4021

4122
@cognito_authenticated
4223
def create(self, request):
4324
try:
44-
user_id = self.get_user_id_from_token(request)
25+
user_id = get_user_id_from_token(request)
4526
data = request.data.copy()
4627
data["username"] = user_id
4728

4829
serializer = BudgetSerializer(data=data)
49-
print(serializer)
5030
if serializer.is_valid():
5131
serializer.save()
5232
return Response(data={"amount": serializer.data["amount"]}, status=status.HTTP_201_CREATED)
@@ -57,26 +37,26 @@ def create(self, request):
5737
@cognito_authenticated
5838
def destroy(self, request):
5939
try:
60-
username = self.get_user_id_from_token(request)
61-
budget = Budget.objects.filter(username=username).order_by("-created_at").first()
40+
user_id = get_user_id_from_token(request)
41+
budget = Budget.objects.filter(username=user_id).order_by("-created_at").first()
42+
if not budget:
43+
return Response({"error": "Budget not found"}, status=status.HTTP_404_NOT_FOUND)
6244
budget.delete()
6345
return Response(status=status.HTTP_204_NO_CONTENT)
64-
except Budget.DoesNotExist:
65-
return Response({"error": "Budget not found"}, status=status.HTTP_404_NOT_FOUND)
6646
except Exception as e:
6747
return Response({"error": str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
6848

6949
@cognito_authenticated
7050
def partial_update(self, request):
7151
try:
72-
username = self.get_user_id_from_token(request)
73-
budget = Budget.objects.filter(username=username).order_by("-created_at").first()
52+
user_id = get_user_id_from_token(request)
53+
budget = Budget.objects.filter(username=user_id).order_by("-created_at").first()
54+
if not budget:
55+
return Response({"error": "Budget not found"}, status=status.HTTP_404_NOT_FOUND)
7456
serializer = BudgetSerializer(budget, data=request.data, partial=True)
7557
if serializer.is_valid():
7658
serializer.save()
7759
return Response(data={"amount": serializer.data["amount"]})
7860
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
79-
except Budget.DoesNotExist:
80-
return Response({"error": "Budget not found"}, status=status.HTTP_404_NOT_FOUND)
8161
except Exception as e:
8262
return Response({"error": str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)

debt/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)