Coverage for apps / ai / api_discover.py: 78%

49 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-04-12 10:49 +0000

1"""AI discover suggestions API endpoints.""" 

2 

3import logging 

4from typing import List 

5 

6from django.conf import settings 

7from django_ratelimit.decorators import ratelimit 

8from ninja import Router, Schema, Status 

9 

10from apps.core.auth import SessionAuth 

11from apps.profiles.models import Profile 

12 

13from .api import ErrorOut, handle_ai_errors 

14from .models import AIDiscoverySuggestion 

15from .services.quota import release_quota, reserve_quota 

16from .services.discover import get_discover_suggestions, CACHE_DURATION_HOURS 

17 

18security_logger = logging.getLogger("security") 

19 

20router = Router(tags=["ai"]) 

21 

22 

23# Schemas 

24 

25 

26class DiscoverSuggestionOut(Schema): 

27 type: str 

28 title: str 

29 description: str 

30 search_query: str 

31 

32 

33class DiscoverOut(Schema): 

34 suggestions: List[DiscoverSuggestionOut] 

35 refreshed_at: str 

36 

37 

38# Endpoints 

39 

40 

41@router.get( 

42 "/discover/{profile_id}/", response={200: DiscoverOut, 404: ErrorOut, 429: dict, 503: ErrorOut}, auth=SessionAuth() 

43) 

44@ratelimit(key="ip", rate="20/h", method="GET", block=False) 

45@handle_ai_errors 

46def discover_endpoint(request, profile_id: int, refresh: bool = False): 

47 """Get AI discovery suggestions for a profile. 

48 

49 Returns cached suggestions if still valid (within 24 hours), 

50 otherwise generates new suggestions via AI. 

51 

52 For new users (no favorites), only seasonal suggestions are returned. 

53 Pass ?refresh=true to force regeneration. 

54 """ 

55 if getattr(request, "limited", False): 

56 security_logger.warning("Rate limit hit: /ai/discover from %s", request.META.get("REMOTE_ADDR")) 

57 return Status(429, {"error": "rate_limited", "message": "Too many requests. Please try again later."}) 

58 

59 # In passkey mode, verify profile ownership 

60 if settings.AUTH_MODE != "home": 

61 session_profile_id = request.session.get("profile_id") 

62 if session_profile_id != profile_id: 

63 return Status(404, {"error": "not_found", "message": "Not found"}) 

64 

65 try: 

66 profile = Profile.objects.select_related("user").get(pk=profile_id) 

67 except Profile.DoesNotExist: 

68 return Status(404, {"error": "not_found", "message": f"Profile {profile_id} not found"}) 

69 

70 # Only count against quota when OpenRouter is actually called (not cache hits) 

71 from datetime import timedelta 

72 

73 from django.utils import timezone 

74 

75 cache_cutoff = timezone.now() - timedelta(hours=CACHE_DURATION_HOURS) 

76 has_cached = ( 

77 not refresh and AIDiscoverySuggestion.objects.filter(profile=profile, created_at__gte=cache_cutoff).exists() 

78 ) 

79 

80 if not has_cached: 

81 allowed, info = reserve_quota(profile, "discover") 

82 if not allowed: 

83 return Status(429, {"error": "quota_exceeded", "message": "Daily limit reached for discover", **info}) 

84 

85 try: 

86 result = get_discover_suggestions(profile_id, force_refresh=refresh) 

87 return result 

88 except Profile.DoesNotExist: 

89 if not has_cached: 

90 release_quota(profile, "discover") 

91 return Status( 

92 404, 

93 { 

94 "error": "not_found", 

95 "message": f"Profile {profile_id} not found", 

96 }, 

97 ) 

98 except Exception: 

99 if not has_cached: 

100 release_quota(profile, "discover") 

101 raise 

← Back to Dashboard