anderson-ufrj commited on
Commit
02f0242
·
1 Parent(s): b39bef0

fix: add pagination.py to git

Browse files

- Force add pagination.py that was being ignored
- Required for API models functionality
- Fixes ModuleNotFoundError in deployment

Files changed (1) hide show
  1. src/api/models/pagination.py +209 -0
src/api/models/pagination.py ADDED
@@ -0,0 +1,209 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Cursor-based pagination models for efficient data retrieval.
3
+
4
+ This module implements cursor pagination which is more efficient
5
+ than offset pagination for large datasets and real-time data.
6
+ """
7
+
8
+ from typing import Generic, List, Optional, TypeVar, Dict, Any
9
+ from datetime import datetime
10
+ from pydantic import BaseModel, Field
11
+ import base64
12
+ import json
13
+
14
+ from src.core import get_logger
15
+
16
+ logger = get_logger(__name__)
17
+
18
+ T = TypeVar('T')
19
+
20
+
21
+ class CursorInfo(BaseModel):
22
+ """Information encoded in a cursor."""
23
+ timestamp: datetime
24
+ id: str
25
+ direction: str = "next"
26
+
27
+ def encode(self) -> str:
28
+ """Encode cursor info to base64 string."""
29
+ data = {
30
+ "t": self.timestamp.isoformat(),
31
+ "i": self.id,
32
+ "d": self.direction
33
+ }
34
+ json_str = json.dumps(data, separators=(',', ':'))
35
+ return base64.urlsafe_b64encode(json_str.encode()).decode()
36
+
37
+ @classmethod
38
+ def decode(cls, cursor: str) -> "CursorInfo":
39
+ """Decode cursor from base64 string."""
40
+ try:
41
+ json_str = base64.urlsafe_b64decode(cursor).decode()
42
+ data = json.loads(json_str)
43
+ return cls(
44
+ timestamp=datetime.fromisoformat(data["t"]),
45
+ id=data["i"],
46
+ direction=data.get("d", "next")
47
+ )
48
+ except Exception as e:
49
+ logger.error(f"Invalid cursor: {e}")
50
+ raise ValueError("Invalid cursor format")
51
+
52
+
53
+ class CursorPaginationRequest(BaseModel):
54
+ """Request parameters for cursor pagination."""
55
+ cursor: Optional[str] = Field(None, description="Cursor for next/previous page")
56
+ limit: int = Field(20, ge=1, le=100, description="Number of items per page")
57
+ direction: str = Field("next", pattern="^(next|prev)$", description="Pagination direction")
58
+
59
+
60
+ class CursorPaginationResponse(BaseModel, Generic[T]):
61
+ """Response with cursor pagination metadata."""
62
+ items: List[T]
63
+ next_cursor: Optional[str] = None
64
+ prev_cursor: Optional[str] = None
65
+ has_more: bool = False
66
+ total_items: Optional[int] = None
67
+ metadata: Dict[str, Any] = Field(default_factory=dict)
68
+
69
+
70
+ class PaginationHelper:
71
+ """Helper class for cursor-based pagination."""
72
+
73
+ @staticmethod
74
+ def create_cursor(item: Dict[str, Any], direction: str = "next") -> str:
75
+ """Create cursor from an item."""
76
+ cursor_info = CursorInfo(
77
+ timestamp=item.get("timestamp", datetime.utcnow()),
78
+ id=str(item.get("id", "")),
79
+ direction=direction
80
+ )
81
+ return cursor_info.encode()
82
+
83
+ @staticmethod
84
+ def parse_cursor(cursor: Optional[str]) -> Optional[CursorInfo]:
85
+ """Parse cursor string to CursorInfo."""
86
+ if not cursor:
87
+ return None
88
+ return CursorInfo.decode(cursor)
89
+
90
+ @staticmethod
91
+ def paginate_list(
92
+ items: List[Dict[str, Any]],
93
+ request: CursorPaginationRequest,
94
+ key_field: str = "timestamp",
95
+ id_field: str = "id"
96
+ ) -> CursorPaginationResponse[Dict[str, Any]]:
97
+ """
98
+ Paginate a list of items using cursor pagination.
99
+
100
+ Args:
101
+ items: List of items to paginate (should be sorted)
102
+ request: Pagination request parameters
103
+ key_field: Field to use for cursor comparison
104
+ id_field: Unique identifier field
105
+
106
+ Returns:
107
+ Paginated response with cursors
108
+ """
109
+ # Parse cursor if provided
110
+ cursor_info = PaginationHelper.parse_cursor(request.cursor)
111
+
112
+ # Filter items based on cursor
113
+ if cursor_info:
114
+ if request.direction == "next":
115
+ # Get items after cursor
116
+ filtered_items = [
117
+ item for item in items
118
+ if item.get(key_field) > cursor_info.timestamp
119
+ or (item.get(key_field) == cursor_info.timestamp
120
+ and str(item.get(id_field)) > cursor_info.id)
121
+ ]
122
+ else: # prev
123
+ # Get items before cursor (reverse order)
124
+ filtered_items = [
125
+ item for item in reversed(items)
126
+ if item.get(key_field) < cursor_info.timestamp
127
+ or (item.get(key_field) == cursor_info.timestamp
128
+ and str(item.get(id_field)) < cursor_info.id)
129
+ ]
130
+ filtered_items = list(reversed(filtered_items))
131
+ else:
132
+ filtered_items = items
133
+
134
+ # Apply limit
135
+ page_items = filtered_items[:request.limit]
136
+ has_more = len(filtered_items) > request.limit
137
+
138
+ # Generate cursors
139
+ next_cursor = None
140
+ prev_cursor = None
141
+
142
+ if page_items:
143
+ # Next cursor from last item
144
+ if has_more or cursor_info:
145
+ next_cursor = PaginationHelper.create_cursor(
146
+ page_items[-1], "next"
147
+ )
148
+
149
+ # Previous cursor from first item
150
+ if cursor_info or (not cursor_info and request.direction == "prev"):
151
+ prev_cursor = PaginationHelper.create_cursor(
152
+ page_items[0], "prev"
153
+ )
154
+
155
+ return CursorPaginationResponse(
156
+ items=page_items,
157
+ next_cursor=next_cursor,
158
+ prev_cursor=prev_cursor,
159
+ has_more=has_more,
160
+ total_items=len(items),
161
+ metadata={
162
+ "page_size": len(page_items),
163
+ "direction": request.direction
164
+ }
165
+ )
166
+
167
+
168
+ class ChatMessagePagination:
169
+ """Specialized pagination for chat messages."""
170
+
171
+ @staticmethod
172
+ def paginate_messages(
173
+ messages: List[Dict[str, Any]],
174
+ cursor: Optional[str] = None,
175
+ limit: int = 50,
176
+ direction: str = "prev" # Default to loading older messages
177
+ ) -> CursorPaginationResponse[Dict[str, Any]]:
178
+ """
179
+ Paginate chat messages with cursor.
180
+
181
+ Chat typically loads older messages, so default direction is "prev".
182
+ """
183
+ request = CursorPaginationRequest(
184
+ cursor=cursor,
185
+ limit=limit,
186
+ direction=direction
187
+ )
188
+
189
+ # Sort messages by timestamp
190
+ sorted_messages = sorted(
191
+ messages,
192
+ key=lambda m: m.get("timestamp", datetime.min)
193
+ )
194
+
195
+ response = PaginationHelper.paginate_list(
196
+ sorted_messages,
197
+ request,
198
+ key_field="timestamp",
199
+ id_field="id"
200
+ )
201
+
202
+ # Add chat-specific metadata
203
+ response.metadata.update({
204
+ "oldest_message": sorted_messages[0].get("timestamp") if sorted_messages else None,
205
+ "newest_message": sorted_messages[-1].get("timestamp") if sorted_messages else None,
206
+ "unread_count": sum(1 for m in messages if not m.get("read", True))
207
+ })
208
+
209
+ return response