devnamdev2003 commited on
Commit
dd4466b
·
1 Parent(s): 2bb9fdd

Deploy Django API with Docker Build Caching

Browse files
.gitattributes CHANGED
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ local_ai_model/* filter=lfs diff=lfs merge=lfs -text
Dockerfile CHANGED
@@ -7,16 +7,20 @@ RUN useradd -m -u 1000 user
7
  WORKDIR /home/user/app
8
 
9
  # Copy requirements first (better cache)
10
- COPY requirements.txt .
11
 
12
  # Install dependencies
13
  RUN pip install --no-cache-dir -r requirements.txt
14
 
15
- # Copy project files
16
- COPY . .
17
-
18
- # Switch to non-root user
19
  USER user
 
 
 
 
 
20
 
21
  # Expose HF port
22
  EXPOSE 7860
 
7
  WORKDIR /home/user/app
8
 
9
  # Copy requirements first (better cache)
10
+ COPY --chown=user:user requirements.txt .
11
 
12
  # Install dependencies
13
  RUN pip install --no-cache-dir -r requirements.txt
14
 
15
+ # --- THE FIX: CACHE THE MODEL DURING BUILD ---
16
+ # This command downloads the model into the Docker image's cache directory.
17
+ # This means your app will start instantly without downloading it on boot!
 
18
  USER user
19
+ RUN python -c "from transformers import pipeline; pipeline('zero-shot-classification', model='valhalla/distilbart-mnli-12-3')"
20
+ # ---------------------------------------------
21
+
22
+ # Copy project files
23
+ COPY --chown=user:user . .
24
 
25
  # Expose HF port
26
  EXPOSE 7860
api/admin.py CHANGED
@@ -1,6 +1,6 @@
1
  from django.contrib import admin
2
  from .models import UserData, AIKey, AppVersion
3
- from .models import Contact
4
 
5
  @admin.register(Contact)
6
  class ContactAdmin(admin.ModelAdmin):
@@ -41,3 +41,11 @@ class UserDataAdmin(admin.ModelAdmin):
41
  return "Not set"
42
 
43
  get_user_name.short_description = "User Name"
 
 
 
 
 
 
 
 
 
1
  from django.contrib import admin
2
  from .models import UserData, AIKey, AppVersion
3
+ from .models import Contact , ExpenseLog
4
 
5
  @admin.register(Contact)
6
  class ContactAdmin(admin.ModelAdmin):
 
41
  return "Not set"
42
 
43
  get_user_name.short_description = "User Name"
44
+
45
+
46
+ @admin.register(ExpenseLog)
47
+ class ExpenseLogAdmin(admin.ModelAdmin):
48
+ list_display = ("text", "predicted_category", "confidence_score", "created_at")
49
+ list_filter = ("predicted_category", "created_at")
50
+ search_fields = ("text", "predicted_category")
51
+ ordering = ("-created_at",)
api/apps.py CHANGED
@@ -1,6 +1,49 @@
1
  from django.apps import AppConfig
 
 
 
2
 
3
 
4
  class ApiConfig(AppConfig):
5
- default_auto_field = 'django.db.models.BigAutoField'
6
- name = 'api'
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  from django.apps import AppConfig
2
+ import torch
3
+ from transformers import pipeline
4
+ import os
5
 
6
 
7
  class ApiConfig(AppConfig):
8
+ default_auto_field = "django.db.models.BigAutoField"
9
+ name = "api"
10
+
11
+ def ready(self):
12
+ # Prevent loading twice (Django sometimes runs ready() twice in dev mode)
13
+ if os.environ.get("RUN_MAIN", None) != "true" and not os.environ.get(
14
+ "KUBERNETES_PORT"
15
+ ):
16
+ pass # Keep going if running via Gunicorn or in production
17
+
18
+ print("Loading Expense Categorizer AI from cache...")
19
+
20
+ device = 0 if torch.cuda.is_available() else -1
21
+
22
+ # This will automatically use the cached version built by Docker
23
+ self.classifier = pipeline(
24
+ "zero-shot-classification",
25
+ model="valhalla/distilbart-mnli-12-3",
26
+ device=device,
27
+ )
28
+
29
+ self.categories = [
30
+ "Food & Drinks",
31
+ "Groceries",
32
+ "Shopping",
33
+ "Bills & Utilities",
34
+ "Entertainment",
35
+ "Health",
36
+ "Education",
37
+ "Subscriptions",
38
+ "Travel",
39
+ "Rent",
40
+ "Family & Friends",
41
+ "Miscellaneous",
42
+ "Gifts",
43
+ "Party",
44
+ "Personal Care",
45
+ "Home & Hygiene",
46
+ "Others",
47
+ "Recharge",
48
+ ]
49
+ print("AI Model loaded successfully!")
api/migrations/0002_expenselog.py ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Generated by Django 5.2.4 on 2026-03-06 16:28
2
+
3
+ from django.db import migrations, models
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+
8
+ dependencies = [
9
+ ('api', '0001_initial'),
10
+ ]
11
+
12
+ operations = [
13
+ migrations.CreateModel(
14
+ name='ExpenseLog',
15
+ fields=[
16
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
17
+ ('text', models.CharField(max_length=500)),
18
+ ('predicted_category', models.CharField(max_length=100)),
19
+ ('confidence_score', models.FloatField()),
20
+ ('created_at', models.DateTimeField(auto_now_add=True)),
21
+ ],
22
+ ),
23
+ ]
api/migrations/0003_expenselog_amount.py ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Generated by Django 5.2.4 on 2026-03-06 16:43
2
+
3
+ from django.db import migrations, models
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+
8
+ dependencies = [
9
+ ('api', '0002_expenselog'),
10
+ ]
11
+
12
+ operations = [
13
+ migrations.AddField(
14
+ model_name='expenselog',
15
+ name='amount',
16
+ field=models.FloatField(blank=True, null=True),
17
+ ),
18
+ ]
api/models.py CHANGED
@@ -2,6 +2,7 @@ from django.db import models
2
  from django.contrib.postgres.fields import JSONField
3
  import uuid
4
 
 
5
  class UserData(models.Model):
6
  user_id = models.CharField(max_length=100, unique=True)
7
  expenses = models.JSONField(default=dict, blank=True)
@@ -11,7 +12,7 @@ class UserData(models.Model):
11
  has_music_url_access = models.BooleanField(default=False)
12
  has_ai_access = models.BooleanField(default=False)
13
  data_backup_key = models.CharField(max_length=100, unique=True, blank=True)
14
-
15
  def __str__(self):
16
  return f"Data for User {self.user_id}"
17
 
@@ -38,7 +39,9 @@ class AppVersion(models.Model):
38
  def save(self, *args, **kwargs):
39
  # If this version is active → deactivate others
40
  if self.isActive:
41
- AppVersion.objects.filter(isActive=True).exclude(id=self.id).update(isActive=False)
 
 
42
 
43
  super().save(*args, **kwargs)
44
 
@@ -50,3 +53,17 @@ class Contact(models.Model):
50
  name = models.CharField(max_length=20)
51
  email = models.EmailField(max_length=50)
52
  message = models.TextField(max_length=500)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
  from django.contrib.postgres.fields import JSONField
3
  import uuid
4
 
5
+
6
  class UserData(models.Model):
7
  user_id = models.CharField(max_length=100, unique=True)
8
  expenses = models.JSONField(default=dict, blank=True)
 
12
  has_music_url_access = models.BooleanField(default=False)
13
  has_ai_access = models.BooleanField(default=False)
14
  data_backup_key = models.CharField(max_length=100, unique=True, blank=True)
15
+
16
  def __str__(self):
17
  return f"Data for User {self.user_id}"
18
 
 
39
  def save(self, *args, **kwargs):
40
  # If this version is active → deactivate others
41
  if self.isActive:
42
+ AppVersion.objects.filter(isActive=True).exclude(id=self.id).update(
43
+ isActive=False
44
+ )
45
 
46
  super().save(*args, **kwargs)
47
 
 
53
  name = models.CharField(max_length=20)
54
  email = models.EmailField(max_length=50)
55
  message = models.TextField(max_length=500)
56
+
57
+ def __str__(self):
58
+ return self.name
59
+
60
+
61
+ class ExpenseLog(models.Model):
62
+ text = models.CharField(max_length=500)
63
+ amount = models.FloatField(null=True, blank=True)
64
+ predicted_category = models.CharField(max_length=100)
65
+ confidence_score = models.FloatField()
66
+ created_at = models.DateTimeField(auto_now_add=True)
67
+
68
+ def __str__(self):
69
+ return f"{self.predicted_category} (${self.amount}): {self.text[:30]}"
api/serializers.py CHANGED
@@ -15,3 +15,10 @@ class ContactSerializer(serializers.ModelSerializer):
15
  class Meta:
16
  model = Contact
17
  fields = ["id", "name", "email", "message"]
 
 
 
 
 
 
 
 
15
  class Meta:
16
  model = Contact
17
  fields = ["id", "name", "email", "message"]
18
+
19
+
20
+ class ExpenseRequestSerializer(serializers.Serializer):
21
+ text = serializers.CharField(
22
+ max_length=500,
23
+ help_text="Enter the expense description to categorize (e.g., 'Paid 1500 for electricity')",
24
+ )
api/urls.py CHANGED
@@ -1,8 +1,11 @@
1
  from django.urls import path
2
- from .views import UserDataPostView, GetFieldView, ContactList1
3
 
4
  urlpatterns = [
5
  path("post/", UserDataPostView.as_view(), name="post_user_data"),
6
- path("<str:field>/<str:identifier>/", GetFieldView.as_view(), name="get_field_data"),
7
- path('contact/', ContactList1.as_view()),
8
- ]
 
 
 
 
1
  from django.urls import path
2
+ from .views import UserDataPostView, GetFieldView, ContactList1, CategorizeExpenseView
3
 
4
  urlpatterns = [
5
  path("post/", UserDataPostView.as_view(), name="post_user_data"),
6
+ path(
7
+ "<str:field>/<str:identifier>/", GetFieldView.as_view(), name="get_field_data"
8
+ ),
9
+ path("contact/", ContactList1.as_view()),
10
+ path("categorize/", CategorizeExpenseView.as_view(), name="categorize_expense"),
11
+ ]
api/views.py CHANGED
@@ -3,9 +3,13 @@ from rest_framework.response import Response
3
  from rest_framework import status
4
  from .models import UserData, AIKey, AppVersion, Contact
5
  from django.shortcuts import get_object_or_404
6
- from .serializers import ContactSerializer
7
- from rest_framework.generics import CreateAPIView
8
  import uuid
 
 
 
 
9
 
10
  class UserDataPostView(APIView):
11
  def post(self, request):
@@ -27,7 +31,7 @@ class UserDataPostView(APIView):
27
  setattr(obj, key, request.data[key])
28
  updated = True
29
 
30
- obj.save() # Save once after changes
31
 
32
  # AI key logic
33
  if obj.has_ai_access:
@@ -40,13 +44,9 @@ class UserDataPostView(APIView):
40
  "message": (
41
  "Data created successfully"
42
  if created
43
- else "Data updated successfully"
44
- if updated
45
- else "No changes made"
46
  ),
47
- "app_version": get_object_or_404(
48
- AppVersion, isActive=True
49
- ).version,
50
  "has_music_url_access": obj.has_music_url_access,
51
  "has_ai_access": obj.has_ai_access,
52
  "ai_key": AI_KEY,
@@ -59,8 +59,10 @@ class UserDataPostView(APIView):
59
  class GetFieldView(APIView):
60
  def get(self, request, field, identifier):
61
  # identifier can be user_id OR data_backup_key
62
- obj = UserData.objects.filter(user_id=identifier).first() \
63
- or UserData.objects.filter(data_backup_key=identifier).first()
 
 
64
 
65
  if not obj:
66
  return Response({"error": "User not found"}, status=404)
@@ -89,10 +91,59 @@ class ContactList1(CreateAPIView):
89
  serializer.save()
90
  return Response(
91
  {"message": "Contact saved successfully"},
92
- status=status.HTTP_201_CREATED
93
  )
94
 
95
  return Response(
96
  {"message": "Validation failed", "errors": serializer.errors},
97
- status=status.HTTP_400_BAD_REQUEST
98
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
  from rest_framework import status
4
  from .models import UserData, AIKey, AppVersion, Contact
5
  from django.shortcuts import get_object_or_404
6
+ from .serializers import ContactSerializer, ExpenseRequestSerializer
7
+ from rest_framework.generics import CreateAPIView, GenericAPIView
8
  import uuid
9
+ from .models import ExpenseLog
10
+ from django.apps import apps
11
+ import re
12
+
13
 
14
  class UserDataPostView(APIView):
15
  def post(self, request):
 
31
  setattr(obj, key, request.data[key])
32
  updated = True
33
 
34
+ obj.save() # Save once after changes
35
 
36
  # AI key logic
37
  if obj.has_ai_access:
 
44
  "message": (
45
  "Data created successfully"
46
  if created
47
+ else "Data updated successfully" if updated else "No changes made"
 
 
48
  ),
49
+ "app_version": get_object_or_404(AppVersion, isActive=True).version,
 
 
50
  "has_music_url_access": obj.has_music_url_access,
51
  "has_ai_access": obj.has_ai_access,
52
  "ai_key": AI_KEY,
 
59
  class GetFieldView(APIView):
60
  def get(self, request, field, identifier):
61
  # identifier can be user_id OR data_backup_key
62
+ obj = (
63
+ UserData.objects.filter(user_id=identifier).first()
64
+ or UserData.objects.filter(data_backup_key=identifier).first()
65
+ )
66
 
67
  if not obj:
68
  return Response({"error": "User not found"}, status=404)
 
91
  serializer.save()
92
  return Response(
93
  {"message": "Contact saved successfully"},
94
+ status=status.HTTP_201_CREATED,
95
  )
96
 
97
  return Response(
98
  {"message": "Validation failed", "errors": serializer.errors},
99
+ status=status.HTTP_400_BAD_REQUEST,
100
+ )
101
+
102
+
103
+ class CategorizeExpenseView(GenericAPIView):
104
+ # 2. Assign the serializer class to automatically populate the Swagger schema
105
+ serializer_class = ExpenseRequestSerializer
106
+
107
+ def post(self, request):
108
+ # 3. Validate input using the serializer
109
+ serializer = self.get_serializer(data=request.data)
110
+ if not serializer.is_valid():
111
+ return Response(serializer.errors, status=400)
112
+
113
+ expense_text = serializer.validated_data["text"]
114
+
115
+ # 4. Extract Amount using Regex
116
+ # Matches formats like 150, 1,500, 150.50, 1,500.50
117
+ amount_match = re.search(r"\d+(?:,\d+)*(?:\.\d+)?", expense_text)
118
+ extracted_amount = (
119
+ float(amount_match.group().replace(",", "")) if amount_match else None
120
+ )
121
+
122
+ # 5. Use the safe Django app registry to fetch the pre-loaded AI model
123
+ api_config = apps.get_app_config("api")
124
+
125
+ result = api_config.classifier(
126
+ expense_text,
127
+ candidate_labels=api_config.categories,
128
+ hypothesis_template="This expense is for {}.",
129
+ )
130
+
131
+ top_category = result["labels"][0]
132
+ confidence_score = result["scores"][0]
133
+
134
+ # 6. Save to database
135
+ ExpenseLog.objects.create(
136
+ text=expense_text,
137
+ amount=extracted_amount,
138
+ predicted_category=top_category,
139
+ confidence_score=confidence_score,
140
+ )
141
+
142
+ return Response(
143
+ {
144
+ "expense": expense_text,
145
+ "amount": extracted_amount,
146
+ "category": top_category,
147
+ "confidence": round(confidence_score * 100, 2),
148
+ }
149
+ )
dev.py ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import requests
2
+ import json
3
+
4
+ # Replace this URL with your actual Hugging Face Space URL once deployed
5
+ # For local testing (e.g., python manage.py runserver), use: "[http://127.0.0.1:8000/api/categorize/](http://127.0.0.1:8000/api/categorize/)"
6
+ API_URL = "http://127.0.0.1:8000/api/categorize/"
7
+
8
+
9
+ def test_categorize_expense(expense_text):
10
+ # The JSON body that your Django API expects
11
+ payload = {"text": expense_text}
12
+
13
+ headers = {"Content-Type": "application/json"}
14
+
15
+ print(f"Sending request to: {API_URL}")
16
+ print(f"Expense: '{expense_text}'\n")
17
+
18
+ try:
19
+ # Send the POST request to your API
20
+ response = requests.post(API_URL, json=payload, headers=headers)
21
+
22
+ # Check if the request was successful
23
+ if response.status_code == 200:
24
+ print("--- API Response ---")
25
+ # Parse and print the formatted JSON response
26
+ print(json.dumps(response.json(), indent=4))
27
+ else:
28
+ print(f"Error: API returned status code {response.status_code}")
29
+ print(response.text)
30
+
31
+ except requests.exceptions.RequestException as e:
32
+ print(f"Connection Error: Could not connect to the API. Details: {e}")
33
+
34
+
35
+ if __name__ == "__main__":
36
+ # Test cases to check your API
37
+ print("Starting API tests...\n")
38
+
39
+ test_categorize_expense("I spent 150 rupees on a burger today")
40
+
41
+ print("-" * 40)
42
+
43
+ test_categorize_expense("Paid 1,500.50 for the electricity and water bill")
44
+
45
+ print("-" * 40)
46
+
47
+ test_categorize_expense(
48
+ "Bought paracetamol and cough syrup"
49
+ ) # Will return amount: null