Skip to content

Commit d1a4059

Browse files
Merge PR #851: Extract core modules and resolve complex routing conflicts
2 parents 0fa8afc + b2a8940 commit d1a4059

24 files changed

Lines changed: 109 additions & 37 deletions

‎backend/ai_service.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919

2020
#to generate book spine images dynamically based on title and author
2121

22-
from backend.spine_generator import create_spine
22+
from spine_generator import create_spine
2323

2424
def process_new_book(book_data):
2525
# 1. Save book to your database first

‎backend/app.py‎

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131

3232
import logging
3333
from datetime import datetime, timedelta, timezone
34-
from sanitizer import sanitize_payload
34+
from backend.core.security.sanitizer import sanitize_payload
3535
from reader_identity.routes import reader_identity_bp
3636

3737
# Load environment variables from config directory based on APP_ENV
@@ -51,7 +51,7 @@
5151
from models import db, User, Book, ShelfItem, BookNote, ReadingGoal, ReadingStats, Collection, CollectionItem, PriceHistory, PriceAlert, Review, register_user, login_user
5252
from price_tracker import get_price_tracker
5353
from cache_service import cache_service
54-
from validators import (
54+
from backend.core.validators.validators import (
5555
validate_request,
5656
validate_schema,
5757
validate_google_books_id,
@@ -95,7 +95,7 @@
9595
from collections import defaultdict, deque
9696
from math import ceil
9797
from time import time
98-
from error_responses import (
98+
from backend.core.responses.error_responses import (
9999
ErrorCodes, error_response, success_response,
100100
validation_error, missing_fields_error, invalid_json_error,
101101
auth_error, forbidden_error, unauthorized_access_error,
@@ -1061,11 +1061,11 @@ def handle_analyze_mood(validated_data):
10611061
@validate_schema(MoodTagsRequest)
10621062
def handle_mood_tags(validated_data):
10631063
"""Get mood tags for a book."""
1064-
from exceptions import (
1064+
from backend.core.exceptions.exceptions import (
10651065
LLMCircuitBreakerOpenError, AIServiceException,
10661066
ValidationException, InvalidInputError
10671067
)
1068-
from error_responses import handle_exception
1068+
from backend.core.responses.error_responses import handle_exception
10691069

10701070
try:
10711071

@@ -1092,11 +1092,11 @@ def handle_mood_tags(validated_data):
10921092
@validate_schema(MoodSearchRequest)
10931093
def handle_mood_search(validated_data):
10941094
"""Search for books based on mood/vibe with improved query parsing."""
1095-
from exceptions import (
1095+
from backend.core.exceptions.exceptions import (
10961096
LLMCircuitBreakerOpenError, AIServiceException,
10971097
ValidationException, InvalidInputError
10981098
)
1099-
from error_responses import handle_exception
1099+
from backend.core.responses.error_responses import handle_exception
11001100

11011101
try:
11021102

@@ -1199,12 +1199,12 @@ def handle_purchase_links():
11991199
@validate_schema(GenerateNoteRequest)
12001200
def handle_generate_note(validated_data):
12011201
"""Generate AI-powered book recommendation with vibe support."""
1202-
from exceptions import (
1202+
from backend.core.exceptions.exceptions import (
12031203
LLMCircuitBreakerOpenError, AIServiceException,
12041204
DatabaseQueryError, DatabaseIntegrityError,
12051205
ValidationException, InvalidInputError
12061206
)
1207-
from error_responses import handle_exception
1207+
from backend.core.responses.error_responses import handle_exception
12081208

12091209
try:
12101210

@@ -1312,8 +1312,8 @@ def handle_socket_chat(data):
13121312
def add_to_library(validated_data):
13131313
"""Add a book to the user's shelf."""
13141314
from sqlalchemy.exc import IntegrityError
1315-
from exceptions import DatabaseQueryError, DatabaseIntegrityError, ValidationException
1316-
from error_responses import handle_exception
1315+
from backend.core.exceptions.exceptions import DatabaseQueryError, DatabaseIntegrityError, ValidationException
1316+
from backend.core.responses.error_responses import handle_exception
13171317

13181318
try:
13191319

@@ -1741,8 +1741,8 @@ def register(validated_data):
17411741
@validate_schema(LoginRequest)
17421742
def login(validated_data):
17431743
"""Authenticate user and return JWT token."""
1744-
from exceptions import DatabaseQueryError, ValidationException
1745-
from error_responses import handle_exception
1744+
from backend.core.exceptions.exceptions import DatabaseQueryError, ValidationException
1745+
from backend.core.responses.error_responses import handle_exception
17461746

17471747
try:
17481748

‎backend/core/__init__.py‎

Whitespace-only changes.

‎backend/core/exceptions/__init__.py‎

Whitespace-only changes.

‎backend/core/rate_limit/__init__.py‎

Whitespace-only changes.

‎backend/core/rate_limit/limiter.py‎

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import os
2+
from collections import defaultdict, deque
3+
from math import ceil
4+
from time import time
5+
6+
# Read defaults from environment, matching original behaviour
7+
RATE_LIMIT_WINDOW = int(os.getenv('RATE_LIMIT_WINDOW', '60'))
8+
RATE_LIMIT_MAX_REQUESTS = int(os.getenv('RATE_LIMIT_MAX_REQUESTS', '30'))
9+
10+
# Internal sliding-window state
11+
_request_log = defaultdict(deque)
12+
_request_calls = 0
13+
14+
15+
def _cleanup_expired_keys(cutoff: float) -> None:
16+
"""Remove keys whose newest timestamp is already outside the window."""
17+
stale_keys = [key for key, dq in _request_log.items() if not dq or dq[-1] <= cutoff]
18+
for key in stale_keys:
19+
_request_log.pop(key, None)
20+
21+
22+
def _rate_limited(endpoint: str) -> tuple[bool, int]:
23+
"""Sliding window limiter per IP/endpoint."""
24+
# Lazy import to avoid circular imports
25+
from flask import request
26+
from app.config import app_config
27+
28+
if not app_config.rate_limit.enabled:
29+
return False, 0
30+
31+
global _request_calls
32+
key = f"{request.remote_addr}|{endpoint}"
33+
now = time()
34+
window_start = now - RATE_LIMIT_WINDOW
35+
_request_calls += 1
36+
37+
dq = _request_log[key]
38+
while dq and dq[0] <= window_start:
39+
dq.popleft()
40+
41+
if len(dq) >= RATE_LIMIT_MAX_REQUESTS:
42+
oldest = dq[0]
43+
retry_after = max(1, ceil(RATE_LIMIT_WINDOW - (now - oldest)))
44+
return True, retry_after
45+
46+
dq.append(now)
47+
48+
if _request_calls % 100 == 0:
49+
_cleanup_expired_keys(window_start)
50+
51+
return False, 0
52+
53+
54+
def rate_limit(endpoint_name: str):
55+
"""Decorator to apply rate limiting to an endpoint."""
56+
def decorator(f):
57+
def wrapped(*args, **kwargs):
58+
limited, retry_after = _rate_limited(endpoint_name)
59+
if limited:
60+
from flask import jsonify
61+
response = jsonify({
62+
"success": False,
63+
"error": "Rate limit exceeded. Try again shortly.",
64+
"retry_after": retry_after
65+
})
66+
response.status_code = 429
67+
response.headers['Retry-After'] = retry_after
68+
return response
69+
return f(*args, **kwargs)
70+
wrapped.__name__ = f.__name__
71+
return wrapped
72+
return decorator

‎backend/core/responses/__init__.py‎

Whitespace-only changes.
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -304,7 +304,7 @@ def handle_exception(exception: Exception, endpoint: str = "unknown") -> tuple:
304304
Returns:
305305
Tuple of (error response, status_code)
306306
"""
307-
from exceptions import (
307+
from core.exceptions.exceptions import (
308308
LLMRateLimitError, LLMTimeoutError, LLMConnectionError,
309309
LLMAuthenticationError, LLMCircuitBreakerOpenError,
310310
DatabaseException, DatabaseConnectionError, DatabaseQueryError,

‎backend/core/security/__init__.py‎

Whitespace-only changes.

0 commit comments

Comments
 (0)