RESTFly 2.0.3__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.
restfly/_iterator.py ADDED
@@ -0,0 +1,302 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ from typing import Any, Self
5
+
6
+ from ._async import AsyncAPIClient
7
+ from ._sync import APIClient
8
+
9
+
10
+ class APIIterator:
11
+ """
12
+ The API iterator provides a scalable way to work through result sets of any
13
+ size. The iterator will walk through each page of data, returning one
14
+ record at a time. If it reaches the end of a page of records, then it will
15
+ request the next page of information and then continue to return records
16
+ from the next page (and the next, and the next) until the counter reaches
17
+ the total number of records that the API has reported.
18
+
19
+ Note that this Iterator is used as a base model for all of the iterators,
20
+ and while the mechanics of each iterator may vary, they should all behave
21
+ to the user in a similar manner.
22
+ """
23
+
24
+ count: int = 0
25
+ """ Current count of objects that have been returned. """
26
+
27
+ page_count: int = 0
28
+ """ Current count of objects from the current page that have been returned. """
29
+
30
+ num_pages: int = 0
31
+ """ The number of pages that have been requested. """
32
+
33
+ max_pages: int | None = None
34
+ """ The maximum number of pages to request before terminating. """
35
+
36
+ max_items: int | None = None
37
+ """ The maximum number of objects to return before terminating. """
38
+
39
+ total: int | None = None
40
+ """ The total number of objects that could be returned. """
41
+
42
+ page: list[Any]
43
+ """ The current page of data. """
44
+
45
+ _client: APIClient
46
+ """ The API Client object to use for calling the API. """
47
+
48
+ def __init__(self, client: APIClient, **kw):
49
+ """
50
+ Args:
51
+ api (restfly.session.APISession):
52
+ The APISession object to use for this iterator.
53
+ **kw (dict):
54
+ The various attributes to add/overload in the iterator.
55
+
56
+ Example:
57
+ >>> i = APIIterator(api, max_pages=1, max_items=100)
58
+ """
59
+ self._client = client
60
+ self.__dict__.update(kw)
61
+ self.page = []
62
+
63
+ # Create the logging facility
64
+ self._log = logging.getLogger(__name__)
65
+
66
+ def __getitem__(self, key: int) -> Any:
67
+ return self.page[key]
68
+
69
+ def __iter__(self) -> Self:
70
+ return self
71
+
72
+ def __next__(self) -> Any:
73
+ """
74
+ Ask for the next record
75
+ """
76
+ # If there are no more records to return, then we should raise a
77
+ # StopIteration exception to break the iterator out.
78
+ if (self.total and self.count >= self.total) or (
79
+ self.max_items and self.count >= self.max_items
80
+ ):
81
+ raise StopIteration()
82
+
83
+ # If we have worked through the current page of records and we still
84
+ # haven't hit to the total number of available records, then we should
85
+ # query the next page of records.
86
+ if self.page_count >= len(self.page) and (
87
+ not self.total or self.count + 1 <= self.total
88
+ ):
89
+ # If the number of pages requested reaches the total number of
90
+ # pages that should be requested, then stop iteration.
91
+ if self.max_pages and self.num_pages + 1 > self.max_pages:
92
+ raise StopIteration()
93
+
94
+ # Perform the _get_page call.
95
+ self._get_page()
96
+ self.page_count = 0
97
+ self.num_pages += 1
98
+
99
+ # If the length of the page is 0, then we don't have anything
100
+ # further to do and should stop iteration.
101
+ if len(self.page) == 0:
102
+ raise StopIteration()
103
+
104
+ # Get the relevant record, increment the counters, and return the
105
+ # record.
106
+ item = self._get_next_item()
107
+ self._increment_counters()
108
+ return item
109
+
110
+ def _increment_counters(self) -> None:
111
+ """
112
+ Handles incrementing all of the counters that are controlling the next item
113
+ to be retrieved.
114
+ """
115
+ self.count += 1
116
+ self.page_count += 1
117
+
118
+ def _get_next_item(self) -> Any:
119
+ """
120
+ Returns the next item in the page
121
+ """
122
+ return self[self.page_count]
123
+
124
+ def _get_page(self) -> None:
125
+ """
126
+ A method to be overloaded in order to instruct the iterator how to
127
+ retrieve the next page of data.
128
+
129
+ Example:
130
+ >>> class ExampleIterator(APIIterator):
131
+ ... def _get_page(self):
132
+ ... self.total = 100
133
+ ... items = range(10)
134
+ ... self.page = [{'id': i + self._offset} for i in items]
135
+ ... self._offset += self._limit
136
+ """
137
+ raise NotImplementedError("Get Page Method isn't implemented.")
138
+
139
+ def get(self, key: int, default: Any | None = None) -> Any:
140
+ """
141
+ Retrieves an item from the the current page based off of the key.
142
+
143
+ Args:
144
+ key (int): The index of the item to retrieve.
145
+ default (obj): The returned object if the item does not exist.
146
+
147
+ Examples:
148
+ >>> a = APIIterator()
149
+ >>> a.get(2)
150
+ None
151
+ """
152
+ try:
153
+ return self[key]
154
+ except IndexError:
155
+ return default
156
+
157
+
158
+ class AsyncAPIIterator:
159
+ """
160
+ The API iterator provides a scalable way to work through result sets of any
161
+ size. The iterator will walk through each page of data, returning one
162
+ record at a time. If it reaches the end of a page of records, then it will
163
+ request the next page of information and then continue to return records
164
+ from the next page (and the next, and the next) until the counter reaches
165
+ the total number of records that the API has reported.
166
+
167
+ Note that this Iterator is used as a base model for all of the iterators,
168
+ and while the mechanics of each iterator may vary, they should all behave
169
+ to the user in a similar manner.
170
+ """
171
+
172
+ count: int = 0
173
+ """ Current count of objects that have been returned. """
174
+
175
+ page_count: int = 0
176
+ """ Current count of objects from the current page that have been returned. """
177
+
178
+ num_pages: int = 0
179
+ """ The number of pages that have been requested. """
180
+
181
+ max_pages: int | None = None
182
+ """ The maximum number of pages to request before terminating. """
183
+
184
+ max_items: int | None = None
185
+ """ The maximum number of objects to return before terminating. """
186
+
187
+ total: int | None = None
188
+ """ The total number of objects that could be returned. """
189
+
190
+ page: list[Any]
191
+ """ The current page of data. """
192
+
193
+ _client: AsyncAPIClient
194
+ """ The API Client object to use for calling the API. """
195
+
196
+ def __init__(self, client: AsyncAPIClient, **kw):
197
+ """
198
+ Args:
199
+ api (restfly.session.APISession):
200
+ The APISession object to use for this iterator.
201
+ **kw (dict):
202
+ The various attributes to add/overload in the iterator.
203
+
204
+ Example:
205
+ >>> i = APIIterator(api, max_pages=1, max_items=100)
206
+ """
207
+ self._client = client
208
+ self.__dict__.update(kw)
209
+ self.page = []
210
+
211
+ # Create the logging facility
212
+ self._log = logging.getLogger(__name__)
213
+
214
+ def __getitem__(self, key: int) -> Any:
215
+ return self.page[key]
216
+
217
+ def __aiter__(self) -> Self:
218
+ return self
219
+
220
+ async def __anext__(self) -> Any:
221
+ """
222
+ Ask for the next record
223
+ """
224
+ # If there are no more records to return, then we should raise a
225
+ # StopIteration exception to break the iterator out.
226
+ if (self.total and self.count >= self.total) or (
227
+ self.max_items and self.count >= self.max_items
228
+ ):
229
+ raise StopAsyncIteration()
230
+
231
+ # If we have worked through the current page of records and we still
232
+ # haven't hit to the total number of available records, then we should
233
+ # query the next page of records.
234
+ if self.page_count >= len(self.page) and (
235
+ not self.total or self.count + 1 <= self.total
236
+ ):
237
+ # If the number of pages requested reaches the total number of
238
+ # pages that should be requested, then stop iteration.
239
+ if self.max_pages and self.num_pages + 1 > self.max_pages:
240
+ raise StopAsyncIteration()
241
+
242
+ # Perform the _get_page call.
243
+ await self._get_page()
244
+ self.page_count = 0
245
+ self.num_pages += 1
246
+
247
+ # If the length of the page is 0, then we don't have anything
248
+ # further to do and should stop iteration.
249
+ if len(self.page) == 0:
250
+ raise StopAsyncIteration()
251
+
252
+ # Get the relevant record, increment the counters, and return the
253
+ # record.
254
+ item = self._get_next_item()
255
+ self._increment_counters()
256
+ return item
257
+
258
+ def _increment_counters(self) -> None:
259
+ """
260
+ Handles incrementing all of the counters that are controlling the next item
261
+ to be retrieved.
262
+ """
263
+ self.count += 1
264
+ self.page_count += 1
265
+
266
+ def _get_next_item(self) -> Any:
267
+ """
268
+ Returns the next item in the page
269
+ """
270
+ return self[self.page_count]
271
+
272
+ async def _get_page(self) -> None:
273
+ """
274
+ A method to be overloaded in order to instruct the iterator how to
275
+ retrieve the next page of data.
276
+
277
+ Example:
278
+ >>> class ExampleIterator(APIIterator):
279
+ ... def _get_page(self):
280
+ ... self.total = 100
281
+ ... items = range(10)
282
+ ... self.page = [{'id': i + self._offset} for i in items]
283
+ ... self._offset += self._limit
284
+ """
285
+
286
+ async def get(self, key: int, default: Any | None = None) -> Any:
287
+ """
288
+ Retrieves an item from the the current page based off of the key.
289
+
290
+ Args:
291
+ key (int): The index of the item to retrieve.
292
+ default (obj): The returned object if the item does not exist.
293
+
294
+ Examples:
295
+ >>> a = APIIterator()
296
+ >>> a.get(2)
297
+ None
298
+ """
299
+ try:
300
+ return self[key]
301
+ except IndexError:
302
+ return default
restfly/_models.py ADDED
@@ -0,0 +1,98 @@
1
+ from typing import Any, Literal, Self
2
+
3
+ from pydantic import BaseModel
4
+
5
+ from ._async import AsyncAPIClient
6
+ from ._sync import APIClient
7
+
8
+
9
+ class APIModel(BaseModel):
10
+ __api_path__: str
11
+ """
12
+ String representation of the path using `model` to act as a placeholder for any
13
+ formatting that needs to occur from the model.
14
+ """
15
+
16
+ __api_client__: APIClient | AsyncAPIClient | None = None
17
+ """ APIClient object that the model is associated to. """
18
+
19
+ __api_save_method__: Literal["GET", "POST", "PUT", "PATCH", "DELETE"] = "PUT"
20
+ """ HTTP method associated with the "save/async_save" methods. """
21
+
22
+ __api_save_request_model_kwargs__: dict[str, Any] | None = None
23
+ """ Pydantic model_dump kwargs to associate to the save method. """
24
+
25
+ __api_remove_method__: Literal["GET", "POST", "PUT", "PATCH", "DELETE"] = "DELETE"
26
+ """ HTTP method associated to the "remove/async_remove" methods. """
27
+
28
+ def model_post_init(self, context: Any) -> Any:
29
+ if isinstance(context, dict):
30
+ self.__api_client__ = context.get("restfly_client")
31
+
32
+ def save(self) -> Self:
33
+ """
34
+ Saves (updates) the current state of the object back to the API.
35
+
36
+ Returns:
37
+ Updated copy of the object from the upstream API.
38
+ """
39
+ path = self.__api_path__.format(model=self)
40
+ if not isinstance(self.__api_client__, APIClient):
41
+ raise TypeError(
42
+ f"Cannot perform synchronous save using {self.__api_client__}."
43
+ )
44
+ return self.__api_client__._request(
45
+ method=self.__api_save_method__,
46
+ path=path,
47
+ json=self,
48
+ request_model_kwargs=self.__api_save_request_model_kwargs__,
49
+ response_model=self.__class__,
50
+ )
51
+
52
+ def remove(self) -> None:
53
+ """
54
+ Removes (deletes) the object from the API.
55
+ """
56
+ path = self.__api_path__.format(model=self)
57
+ if not isinstance(self.__api_client__, APIClient):
58
+ raise TypeError(
59
+ f"Cannot perform synchronous save using {self.__api_client__}."
60
+ )
61
+ self.__api_client__._request(
62
+ method=self.__api_remove_method__,
63
+ path=path,
64
+ )
65
+
66
+ async def async_save(self) -> Self:
67
+ """
68
+ Saves (updates) the current state of the object back to the API.
69
+
70
+ Returns:
71
+ Updated copy of the object from the upstream API.
72
+ """
73
+ path = self.__api_path__.format(model=self)
74
+ if not isinstance(self.__api_client__, AsyncAPIClient):
75
+ raise TypeError(
76
+ f"Cannot perform synchronous save using {self.__api_client__}."
77
+ )
78
+ return await self.__api_client__._request(
79
+ method=self.__api_save_method__,
80
+ path=path,
81
+ json=self,
82
+ request_model_kwargs=self.__api_save_request_model_kwargs__,
83
+ response_model=self.__class__,
84
+ )
85
+
86
+ async def async_remove(self) -> None:
87
+ """
88
+ Removes (deletes) the object from the API.
89
+ """
90
+ path = self.__api_path__.format(model=self)
91
+ if not isinstance(self.__api_client__, AsyncAPIClient):
92
+ raise TypeError(
93
+ f"Cannot perform synchronous save using {self.__api_client__}."
94
+ )
95
+ await self.__api_client__._request(
96
+ method=self.__api_remove_method__,
97
+ path=path,
98
+ )