Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 31 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,31 @@
env.py
env.py

# Python cache files
__pycache__/
*.py[cod]
*$py.class
*.so
.Python

# Virtual environments
venv/
env/
ENV/
.venv

# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal

# IDE
.vscode/
.idea/
*.swp
*.swo
*~

# OS
.DS_Store
Thumbs.db
Binary file removed LLM_Metadata/__pycache__/__init__.cpython-311.pyc
Binary file not shown.
Binary file removed LLM_Metadata/__pycache__/admin.cpython-311.pyc
Binary file not shown.
Binary file removed LLM_Metadata/__pycache__/apps.cpython-311.pyc
Binary file not shown.
Binary file removed LLM_Metadata/__pycache__/forms.cpython-311.pyc
Binary file not shown.
Binary file removed LLM_Metadata/__pycache__/models.cpython-311.pyc
Binary file not shown.
Binary file removed LLM_Metadata/__pycache__/urls.cpython-311.pyc
Binary file not shown.
Binary file removed LLM_Metadata/__pycache__/utils.cpython-311.pyc
Binary file not shown.
Binary file removed LLM_Metadata/__pycache__/views.cpython-311.pyc
Binary file not shown.
145 changes: 145 additions & 0 deletions LLM_Metadata/api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
"""
API views using Django REST Framework with rate limiting and caching
"""
from rest_framework.decorators import api_view, throttle_classes
from rest_framework.response import Response
from rest_framework.throttling import UserRateThrottle, AnonRateThrottle
from rest_framework import status
from django.views.decorators.cache import cache_page
from django.utils.decorators import method_decorator
from django.core.cache import cache
from .models import Conversation
from django.contrib.auth.decorators import login_required
from django_ratelimit.decorators import ratelimit
import uuid
from django.utils import timezone


class ConversationRateThrottle(UserRateThrottle):
"""Custom throttle for conversation API - 50 requests per hour"""
rate = '50/hour'


class HealthCheckRateThrottle(AnonRateThrottle):
"""Custom throttle for health check API - 200 requests per hour"""
rate = '200/hour'


@api_view(['GET'])
@throttle_classes([HealthCheckRateThrottle])
@cache_page(60) # Cache for 1 minute
def api_health_check(request):
"""
API endpoint for health check with rate limiting and caching
Returns system health status
"""
try:
# Test database connection
total_conversations = Conversation.objects.count()
latest_conversation = Conversation.objects.first()

return Response({
'status': 'healthy',
'timestamp': timezone.now().isoformat(),
'database': {
'total_conversations': total_conversations,
'latest_conversation_date': latest_conversation.timestamp.isoformat() if latest_conversation else None,
},
'message': 'API is operational'
}, status=status.HTTP_200_OK)

except Exception as e:
return Response({
'status': 'error',
'error': str(e),
'timestamp': timezone.now().isoformat(),
'message': 'Database connection failed'
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)


@api_view(['GET'])
@throttle_classes([ConversationRateThrottle])
def api_conversation_stats(request):
"""
API endpoint to get conversation statistics with caching
Returns user's conversation count and recent activity
"""
if not request.user.is_authenticated:
return Response({
'error': 'Authentication required'
}, status=status.HTTP_401_UNAUTHORIZED)

# Try to get cached data first
cache_key = f'conversation_stats_{request.user.id}'
cached_data = cache.get(cache_key)

if cached_data:
cached_data['cached'] = True
return Response(cached_data, status=status.HTTP_200_OK)

# If not cached, query database
user_conversations = Conversation.objects.filter(username=request.user.username)
total_count = user_conversations.count()
recent_conversations = user_conversations.order_by('-timestamp')[:5]

data = {
'user': request.user.username,
'total_conversations': total_count,
'recent_conversations': [
{
'id': conv.id,
'role': conv.role,
'content': conv.content[:100] + '...' if len(conv.content) > 100 else conv.content,
'timestamp': conv.timestamp.isoformat(),
'model_name': conv.model_name,
}
for conv in recent_conversations
],
'cached': False
}

# Cache the data for 5 minutes
cache.set(cache_key, data, 300)

return Response(data, status=status.HTTP_200_OK)


@api_view(['DELETE'])
@throttle_classes([ConversationRateThrottle])
def api_delete_conversation(request, conversation_id):
"""
API endpoint to delete a conversation with rate limiting
"""
if not request.user.is_authenticated:
return Response({
'error': 'Authentication required'
}, status=status.HTTP_401_UNAUTHORIZED)

try:
# Get conversations with this conversation_id belonging to the user
conversations = Conversation.objects.filter(
conversation_id=conversation_id,
username=request.user.username
)

if not conversations.exists():
return Response({
'error': 'Conversation not found or access denied'
}, status=status.HTTP_404_NOT_FOUND)

count = conversations.count()
conversations.delete()

# Invalidate cache for this user's stats
cache_key = f'conversation_stats_{request.user.id}'
cache.delete(cache_key)

return Response({
'message': f'Successfully deleted {count} conversation entries',
'conversation_id': str(conversation_id)
}, status=status.HTTP_200_OK)

except Exception as e:
return Response({
'error': str(e)
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
Binary file not shown.
Binary file not shown.
Binary file not shown.
204 changes: 204 additions & 0 deletions LLM_Metadata/test_api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
"""
Tests for API rate limiting and caching functionality
"""
from django.test import TestCase, Client
from django.contrib.auth.models import User
from django.core.cache import cache
from django.urls import reverse
from LLM_Metadata.models import Conversation
import uuid
from django.utils import timezone


class RateLimitingTests(TestCase):
"""Test rate limiting functionality"""

def setUp(self):
"""Set up test client and user"""
self.client = Client()
self.user = User.objects.create_user(
username='testuser',
password='testpass123'
)
self.client.login(username='testuser', password='testpass123')
cache.clear()

def test_health_check_endpoint_exists(self):
"""Test that health check endpoint is accessible"""
response = self.client.get('/health/')
self.assertEqual(response.status_code, 200)
self.assertIn('status', response.json())

def test_api_health_check_endpoint_exists(self):
"""Test that API health check endpoint is accessible"""
response = self.client.get('/api/health/')
self.assertEqual(response.status_code, 200)
data = response.json()
self.assertIn('status', data)
self.assertEqual(data['status'], 'healthy')


class CachingTests(TestCase):
"""Test caching functionality"""

def setUp(self):
"""Set up test client and user"""
self.client = Client()
self.user = User.objects.create_user(
username='testuser',
password='testpass123'
)
self.client.login(username='testuser', password='testpass123')

# Create some test conversations
for i in range(3):
Conversation.objects.create(
role='user' if i % 2 == 0 else 'assistant',
content=f'Test content {i}',
username=self.user.username,
conversation_id=uuid.uuid4(),
timestamp=timezone.now()
)

cache.clear()

def test_conversation_stats_caching(self):
"""Test that conversation stats are cached"""
# First request - not cached
response1 = self.client.get('/api/conversations/stats/')
self.assertEqual(response1.status_code, 200)
data1 = response1.json()
self.assertFalse(data1.get('cached', True))

# Second request - should be cached
response2 = self.client.get('/api/conversations/stats/')
self.assertEqual(response2.status_code, 200)
data2 = response2.json()
self.assertTrue(data2.get('cached', False))

# Data should be the same
self.assertEqual(data1['total_conversations'], data2['total_conversations'])

def test_cache_invalidation_on_delete(self):
"""Test that cache is invalidated when conversation is deleted"""
# Get initial stats (creates cache)
response1 = self.client.get('/api/conversations/stats/')
data1 = response1.json()
initial_count = data1['total_conversations']

# Delete a conversation
conversation = Conversation.objects.filter(username=self.user.username).first()
response = self.client.delete(
f'/api/conversations/{conversation.conversation_id}/'
)
self.assertEqual(response.status_code, 200)

# Get stats again - cache should be invalidated
response2 = self.client.get('/api/conversations/stats/')
data2 = response2.json()
self.assertFalse(data2.get('cached', True))
self.assertLess(data2['total_conversations'], initial_count)


class APIEndpointsTests(TestCase):
"""Test API endpoints functionality"""

def setUp(self):
"""Set up test client and user"""
self.client = Client()
self.user = User.objects.create_user(
username='testuser',
password='testpass123'
)
self.conversation_id = uuid.uuid4()

# Create test conversations
Conversation.objects.create(
role='user',
content='Test question',
username=self.user.username,
conversation_id=self.conversation_id,
timestamp=timezone.now()
)
Conversation.objects.create(
role='assistant',
content='Test response',
username=self.user.username,
conversation_id=self.conversation_id,
timestamp=timezone.now()
)

cache.clear()

def test_conversation_stats_requires_authentication(self):
"""Test that conversation stats endpoint requires authentication"""
response = self.client.get('/api/conversations/stats/')
self.assertEqual(response.status_code, 401)

def test_conversation_stats_returns_correct_data(self):
"""Test that conversation stats returns correct data"""
self.client.login(username='testuser', password='testpass123')
response = self.client.get('/api/conversations/stats/')
self.assertEqual(response.status_code, 200)

data = response.json()
self.assertEqual(data['user'], 'testuser')
self.assertEqual(data['total_conversations'], 2)
self.assertIsInstance(data['recent_conversations'], list)

def test_delete_conversation_api(self):
"""Test deleting conversation via API"""
self.client.login(username='testuser', password='testpass123')

# Delete conversation
response = self.client.delete(
f'/api/conversations/{self.conversation_id}/'
)
self.assertEqual(response.status_code, 200)

data = response.json()
self.assertIn('message', data)
self.assertEqual(data['conversation_id'], str(self.conversation_id))

# Verify conversations are deleted
remaining = Conversation.objects.filter(
conversation_id=self.conversation_id
).count()
self.assertEqual(remaining, 0)

def test_delete_nonexistent_conversation(self):
"""Test deleting a conversation that doesn't exist"""
self.client.login(username='testuser', password='testpass123')

fake_uuid = uuid.uuid4()
response = self.client.delete(f'/api/conversations/{fake_uuid}/')
self.assertEqual(response.status_code, 404)

def test_delete_conversation_requires_authentication(self):
"""Test that delete requires authentication"""
response = self.client.delete(
f'/api/conversations/{self.conversation_id}/'
)
self.assertEqual(response.status_code, 401)


class SettingsTests(TestCase):
"""Test that settings are configured correctly"""

def test_rest_framework_configured(self):
"""Test that REST framework is configured"""
from django.conf import settings
self.assertIn('rest_framework', settings.INSTALLED_APPS)
self.assertIn('DEFAULT_THROTTLE_CLASSES', settings.REST_FRAMEWORK)

def test_cache_configured(self):
"""Test that cache is configured"""
from django.conf import settings
self.assertIn('default', settings.CACHES)
self.assertIn('BACKEND', settings.CACHES['default'])

def test_ratelimit_configured(self):
"""Test that rate limiting is configured"""
from django.conf import settings
self.assertTrue(hasattr(settings, 'RATELIMIT_ENABLE'))
self.assertTrue(hasattr(settings, 'RATELIMIT_USE_CACHE'))
Loading