rtbnext 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,313 @@
1
+ """
2
+ Base Collections
3
+
4
+ Implements the base collections shared by resource collections.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import Callable, Generic, Iterator, Self, TypeVar, cast
10
+
11
+ from rtbnext.defaults import DEFAULT_PER_PAGE
12
+ from rtbnext.utils import ymd
13
+
14
+ R = TypeVar( "R" )
15
+ T = TypeVar( "T" )
16
+ U = TypeVar( "U" )
17
+
18
+ type ItemFactory[ T, R ] = Callable[ [ T ], R ]
19
+ type DateResolver[ T ] = Callable[ [ T ], str ]
20
+
21
+
22
+ class CollectionBase( Generic[ T, R ] ):
23
+ """
24
+ Provides common immutable collection operations.
25
+
26
+ This class serves as a base for resource collections, providing methods to
27
+ manipulate and access the underlying items while maintaining immutability.
28
+ """
29
+
30
+ def __init__(
31
+ self, items: list[ T ], *,
32
+ factory: ItemFactory[ T, R ] = lambda item: cast( R, item ),
33
+ total: int | None = None
34
+ ) -> None:
35
+ self._items, self._factory = items, factory
36
+ self._total = len( items ) if total is None else total
37
+
38
+ def _clone( self, items: list[ T ] ) -> Self:
39
+ """Creates a new collection instance with replaced items."""
40
+
41
+ return self.__class__( items, factory= self._factory, total= self._total )
42
+
43
+ def __getitem__( self, index: int ) -> Self | R:
44
+ return (
45
+ self._clone( self._items[ index ] )
46
+ if isinstance( index, slice )
47
+ else self._factory( self._items[ index ] )
48
+ )
49
+
50
+ def __contains__( self, item: object ) -> bool:
51
+ return item in self._items
52
+
53
+ def __len__( self ) -> int:
54
+ return self.count
55
+
56
+ def __iter__( self ) -> Iterator[ R ]:
57
+ return map( self._factory, self._items )
58
+
59
+ def __reversed__( self ) -> Iterator[ R ]:
60
+ return map( self._factory, self._items[ ::-1 ] )
61
+
62
+ @property
63
+ def items( self ) -> list[ T ]:
64
+ """Returns the raw items."""
65
+
66
+ return self._items.copy()
67
+
68
+ @property
69
+ def total( self ) -> int:
70
+ """Returns the total number of available items."""
71
+
72
+ return self._total
73
+
74
+ @property
75
+ def count( self ) -> int:
76
+ """Returns the number of items currently contained."""
77
+
78
+ return len( self._items )
79
+
80
+ @property
81
+ def first( self ) -> R | None:
82
+ """Returns the first item."""
83
+
84
+ return None if not self._items else self._factory( self._items[ 0 ] )
85
+
86
+ @property
87
+ def last( self ) -> R | None:
88
+ """Returns the last item."""
89
+
90
+ return None if not self._items else self._factory( self._items[ -1 ] )
91
+
92
+ def to_array( self ) -> list[ R ]:
93
+ """Returns all resolved items as list."""
94
+
95
+ return [ self._factory( item ) for item in self._items ]
96
+
97
+ def map( self, callback: Callable[ [ R, int ], U ] ) -> list[ U ]:
98
+ """Maps resolved items."""
99
+
100
+ return [ callback( item, index ) for index, item in enumerate( self ) ]
101
+
102
+ def reversed( self ) -> Self:
103
+ """Returns a collection containing reversed items."""
104
+
105
+ return self._clone( self._items[ ::-1 ] )
106
+
107
+ def take( self, count: int ) -> Self:
108
+ """Returns a collection containing the first items."""
109
+
110
+ return self._clone( self._items[ : count ] )
111
+
112
+ def skip( self, count: int ) -> Self:
113
+ """Returns a collection without the first items."""
114
+
115
+ return self._clone( self._items[ count : ] )
116
+
117
+ def slice( self, start: int | None = None, end: int | None = None ) -> Self:
118
+ """Returns a collection containing a sliced range of items."""
119
+
120
+ return self._clone( self._items[ start : end ] )
121
+
122
+
123
+ class CursorCollectionBase( CollectionBase[ T, R ], Generic[ T, R ] ):
124
+ """
125
+ Provides cursor-related immutable collection operations.
126
+
127
+ This class extends the base collection adding methods to handle cursor
128
+ operations for items.
129
+ """
130
+
131
+ def __init__(
132
+ self, items: list[ T ], *,
133
+ factory: ItemFactory[ T, R ] = lambda item: cast( R, item ),
134
+ total: int | None = None
135
+ ) -> None:
136
+ super().__init__( items, factory= factory, total= total )
137
+ self._cursor: int = -1
138
+
139
+ @property
140
+ def position( self ) -> int:
141
+ """Returns the current cursor position."""
142
+
143
+ return self._cursor
144
+
145
+ @property
146
+ def current( self ) -> R | None:
147
+ """Returns the current resolved item."""
148
+
149
+ return self.at( self._cursor )
150
+
151
+ @property
152
+ def next( self ) -> R | None:
153
+ """Returns the next resolved item."""
154
+
155
+ self._cursor += 1
156
+ return self.at( self._cursor )
157
+
158
+ @property
159
+ def prev( self ) -> R | None:
160
+ """Returns the previous resolved item."""
161
+
162
+ self._cursor -= 1
163
+ return self.at( self._cursor )
164
+
165
+ @property
166
+ def has_next( self ) -> bool:
167
+ """Returns whether a next item exists."""
168
+
169
+ return self._cursor + 1 < self.count
170
+
171
+ @property
172
+ def has_prev( self ) -> bool:
173
+ """Returns whether a previous item exists."""
174
+
175
+ return self._cursor > 0
176
+
177
+ def reset( self ) -> Self:
178
+ """Resets the cursor."""
179
+
180
+ self._cursor = -1
181
+ return self
182
+
183
+ def seek( self, pos: int ) -> Self:
184
+ """Moves the cursor to a specific index."""
185
+
186
+ self._cursor = pos
187
+ return self
188
+
189
+ def at( self, index: int ) -> R | None:
190
+ """Returns the resolved item at the given index."""
191
+
192
+ return self._factory( self._items[ index ] ) if 0 <= index < self.count else None
193
+
194
+ def page( self, page: int, per_page: int = DEFAULT_PER_PAGE ) -> Self:
195
+ """Returns a collection containing one page."""
196
+
197
+ start = max( page - 1, 0 ) * per_page
198
+ return self._clone( self._items[ start : start + per_page ] )
199
+
200
+ def pages( self, per_page: int = DEFAULT_PER_PAGE ) -> list[ Self ]:
201
+ """Returns all pages."""
202
+
203
+ return [
204
+ self._clone( self._items[ i : i + per_page ] )
205
+ for i in range( 0, self.count, per_page )
206
+ ]
207
+
208
+
209
+ class DateCollectionBase( CursorCollectionBase[ T, R ], Generic[ T, R ] ):
210
+ """
211
+ Provides date-related immutable collection operations.
212
+
213
+ This class extends the base collection adding special methods dealing with
214
+ dates, like `year`, `month`, or `between`.
215
+ """
216
+
217
+ def __init__(
218
+ self, items: list[ T ], *,
219
+ factory: ItemFactory[ T, R ] = lambda item: cast( R, item ),
220
+ date: DateResolver[ T ] = lambda item: cast( str, item ),
221
+ total: int | None = None
222
+ ) -> None:
223
+ super().__init__( items, factory= factory, total= total )
224
+ self._date = date or ( lambda item: item )
225
+
226
+ @property
227
+ def dates( self ) -> list[ str ]:
228
+ """Returns the resolved date values."""
229
+
230
+ return [ self._date( item ) for item in self._items ]
231
+
232
+ @property
233
+ def latest( self ) -> R | None:
234
+ """Returns the latest dated item."""
235
+
236
+ return self.first
237
+
238
+ @property
239
+ def oldest( self ) -> R | None:
240
+ """Returns the oldest dated item."""
241
+
242
+ return self.last
243
+
244
+ def seek( self, pos: str ) -> Self:
245
+ """Moves the cursor to a specific date."""
246
+
247
+ target = ymd( pos )
248
+ self._cursor = next( (
249
+ index for index, item in enumerate( self._items )
250
+ if self._date( item ) == target
251
+ ), -1 )
252
+
253
+ return self
254
+
255
+ def find( self, date: str ) -> R | None:
256
+ """Gets the item by exact date."""
257
+
258
+ target = ymd( date )
259
+
260
+ return next( (
261
+ self._factory( item ) for item in self._items
262
+ if self._date( item ) == target
263
+ ), None )
264
+
265
+ def year( self, year: int ) -> Self:
266
+ """Filters resources by year."""
267
+
268
+ prefix = f"{ year }-"
269
+
270
+ return self._clone( [
271
+ item for item in self._items
272
+ if self._date( item ).startswith( prefix )
273
+ ] )
274
+
275
+ def month( self, year: int, month: int ) -> Self:
276
+ """Filters resources by month."""
277
+
278
+ prefix = f"{ year }-{month:02d}-"
279
+
280
+ return self._clone( [
281
+ item for item in self._items
282
+ if self._date( item ).startswith( prefix )
283
+ ] )
284
+
285
+ def before( self, date: str ) -> Self:
286
+ """Returns resources before a date."""
287
+
288
+ target = ymd( date )
289
+ return self._clone( [ item for item in self._items if self._date( item ) < target ] )
290
+
291
+ def after( self, date: str ) -> Self:
292
+ """Returns resources after a date."""
293
+
294
+ target = ymd( date )
295
+ return self._clone( [ item for item in self._items if self._date( item ) > target ] )
296
+
297
+ def since( self, date: str ) -> Self:
298
+ """Returns resources from a date onward."""
299
+
300
+ target = ymd( date )
301
+ return self._clone( [ item for item in self._items if self._date( item ) >= target ] )
302
+
303
+ def until( self, date: str ) -> Self:
304
+ """Returns resources up to a date."""
305
+
306
+ target = ymd( date )
307
+ return self._clone( [ item for item in self._items if self._date( item ) <= target ] )
308
+
309
+ def between( self, start: str, end: str ) -> Self:
310
+ """Returns resources inside a date range."""
311
+
312
+ s, e = ymd( start ), ymd( end )
313
+ return self._clone( [ item for item in self._items if s <= self._date( item ) <= e ] )
@@ -0,0 +1,59 @@
1
+ """
2
+ Dateable Resource
3
+
4
+ Implements the resource wrapper for date-indexed endpoints.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import Callable, Generic, TypedDict, TypeVar
10
+
11
+ from rtbnext.core.loader import ResourceStateLoader
12
+ from rtbnext.core.parser import ParserFn
13
+ from rtbnext.resource.base import Resource
14
+ from rtbnext.resource.collection import DateCollectionBase
15
+
16
+ DateData = TypedDict( "DateData", { "dates": list[ str ] } )
17
+
18
+ D = TypeVar( "D", bound= DateData )
19
+ R = TypeVar( "R" )
20
+
21
+ type DateFn[ R ] = Callable[ [ str ], R ]
22
+
23
+
24
+ class DateCollection( DateCollectionBase[ str, R ], Generic[ R ] ):
25
+ """
26
+ Collection wrapper for date-indexed resources.
27
+
28
+ This class provides convenient filtering and mapping operations for resources
29
+ that are indexed by date values. It allows for lazy resolution of resources
30
+ based on their date keys, while maintaining the underlying collection of date
31
+ strings.
32
+ """
33
+
34
+
35
+ class DateableResource( Resource[ D ], Generic[ D, R ] ):
36
+ """
37
+ Resource wrapper for date-indexed endpoints.
38
+
39
+ This class provides lazy traversal over API endpoints that are indexed by
40
+ date values. It allows for convenient access to resources based on their
41
+ date keys, while maintaining the underlying resource data.
42
+ """
43
+
44
+ def __init__(
45
+ self, path: str, loader: ResourceStateLoader, parser: ParserFn[ D ], *,
46
+ date: DateFn[ R ]
47
+ ) -> None:
48
+ super().__init__( path, loader, parser )
49
+ self._date = date
50
+
51
+ def _collect_dates( self, dates: list[ str ] ) -> DateCollection[ R ]:
52
+ """Creates a date collection."""
53
+
54
+ return DateCollection( dates, factory= self._date )
55
+
56
+ async def get( self ) -> DateCollection[ R ]:
57
+ """Returns the indexed date collection."""
58
+
59
+ return await self._transform( lambda data: self._collect_dates( data[ "dates" ][ ::-1 ] ) )
@@ -0,0 +1,91 @@
1
+ """
2
+ Indexable Resource
3
+
4
+ Implement the resource wrapper for nested indexable endpoints.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import Callable, Generic, TypeVar
10
+
11
+ from rtbnext.core.loader import ResourceStateLoader
12
+ from rtbnext.core.parser import ParserFn
13
+ from rtbnext.resource.base import Resource
14
+
15
+ D = TypeVar( "D" )
16
+ R = TypeVar( "R" )
17
+
18
+ type Path = tuple[ str, ... ]
19
+ type IndexFn[ R ] = Callable[ [ Path ], R ]
20
+ type KeysFn = Callable[ [ object ], list[ str ] | None ]
21
+
22
+
23
+ class _IndexAccessor( Generic[ R ] ):
24
+ """Lazy accessor object for index traversal."""
25
+
26
+ def __init__( self, factory: IndexFn[ R ], keys: KeysFn, path: Path, value: object ) -> None:
27
+ self._factory, self._keys, self._path, self._value = factory, keys, path, value
28
+
29
+ def __getitem__( self, key: str ) -> R | _IndexAccessor[ R ]:
30
+ """Resolves a nested index key."""
31
+
32
+ available = self._keys( self._value )
33
+
34
+ if available is None or key not in available:
35
+ raise AttributeError( f"Unknown index key: { key }" )
36
+
37
+ path = ( *self._path, key )
38
+
39
+ if isinstance( self._value, dict ) and self._keys( child := self._value[ key ] ):
40
+ return _IndexAccessor( self._factory, self._keys, path, child )
41
+
42
+ return self._factory( path )
43
+
44
+ def __dir__( self ) -> list[ str ]:
45
+ """Returns available index keys."""
46
+
47
+ return sorted( [ *super().__dir__(), *( self._keys( self._value ) or [] ) ] )
48
+
49
+
50
+ class IndexableResource( Resource[ D ], Generic[ D, R ] ):
51
+ """
52
+ Resource wrapper for nested indexable endpoints.
53
+
54
+ This class provides lazy traversal over API indexes by exposing generated
55
+ accessors for nested keys.
56
+
57
+ The underlying resource data is loaded only once and transformed into a
58
+ reusable accessor tree.
59
+ """
60
+
61
+ def __init__(
62
+ self, path: str, loader: ResourceStateLoader, parser: ParserFn[ D ], *,
63
+ index: IndexFn[ R ],
64
+ keys: KeysFn | None = None
65
+ ) -> None:
66
+ super().__init__( path, loader, parser )
67
+
68
+ self._factory = index
69
+ self._keys = keys or self._default_keys
70
+
71
+ @staticmethod
72
+ def _default_keys( value: object ) -> list[ str ] | None:
73
+ """Extracts keys from common index structures."""
74
+
75
+ if isinstance( value, dict ):
76
+ return list( value.keys() )
77
+
78
+ if isinstance( value, list ):
79
+ return [ str( item ) for item in value ]
80
+
81
+ return None
82
+
83
+ def _traverse( self, value: object, path: Path = () ) -> _IndexAccessor[ R ]:
84
+ """Creates the lazy accessor tree."""
85
+
86
+ return _IndexAccessor( self._factory, self._keys, path, value )
87
+
88
+ async def get( self ) -> R:
89
+ """Returns the lazily generated index tree."""
90
+
91
+ return await self._transform( self._traverse )