forked from CenterForOpenScience/gravyvalet
-
Notifications
You must be signed in to change notification settings - Fork 0
/
cursor.py
93 lines (68 loc) · 2.36 KB
/
cursor.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
import base64
import dataclasses
import json
from typing import (
ClassVar,
Protocol,
)
def encode_cursor_dataclass(dataclass_instance) -> str:
_as_json = json.dumps(dataclasses.astuple(dataclass_instance))
_cursor_bytes = base64.b64encode(_as_json.encode())
return _cursor_bytes.decode()
def decode_cursor_dataclass(cursor: str, dataclass_class):
_as_list = json.loads(base64.b64decode(cursor))
return dataclass_class(*_as_list)
class Cursor(Protocol):
"""an abstract protocol for pagination cursors"""
@classmethod
def from_str(cls, cursor: str):
return decode_cursor_dataclass(cursor, cls)
@property
def this_cursor_str(self) -> str:
return encode_cursor_dataclass(self)
@property
def next_cursor_str(self) -> str | None: ...
@property
def prev_cursor_str(self) -> str | None: ...
@property
def first_cursor_str(self) -> str: ...
@property
def is_first_page(self) -> bool: ...
@property
def is_last_page(self) -> bool: ...
@property
def has_many_more(self) -> bool: ...
@dataclasses.dataclass
class OffsetCursor(Cursor):
offset: int
limit: int
total_count: int # use -1 to mean "many more"
MAX_INDEX: ClassVar[int] = 9999
@property
def next_cursor_str(self) -> str | None:
_next = dataclasses.replace(self, offset=(self.offset + self.limit))
return encode_cursor_dataclass(_next) if _next.is_valid_cursor() else None
@property
def prev_cursor_str(self) -> str | None:
_prev = dataclasses.replace(self, offset=(self.offset - self.limit))
return encode_cursor_dataclass(_prev) if _prev.is_valid_cursor() else None
@property
def first_cursor_str(self) -> str:
return encode_cursor_dataclass(dataclasses.replace(self, offset=0))
@property
def is_first_page(self) -> bool:
return self.offset == 0
@property
def is_last_page(self) -> bool:
return (self.offset + self.limit) >= self.total_count
@property
def has_many_more(self) -> bool:
return self.total_count == -1
def max_index(self) -> int:
return (
self.MAX_INDEX
if self.has_many_more
else min(self.total_count, self.MAX_INDEX)
)
def is_valid_cursor(self) -> bool:
return (self.limit > 0) and (0 <= self.offset < self.max_index())