apytizer 0.0.1a0__py3-none-any.whl → 0.0.1b2__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.
- apytizer/__init__.py +2 -12
- apytizer/adapters/__init__.py +2 -3
- apytizer/adapters/transport_adapter.py +91 -0
- apytizer/apis/__init__.py +6 -0
- apytizer/apis/abstract_api.py +36 -0
- apytizer/apis/web_api.py +460 -0
- apytizer/connections/__init__.py +6 -0
- apytizer/connections/abstract_connection.py +28 -0
- apytizer/connections/http_connection.py +431 -0
- apytizer/decorators/__init__.py +5 -5
- apytizer/decorators/caching.py +60 -9
- apytizer/decorators/chunking.py +105 -0
- apytizer/decorators/connection.py +55 -20
- apytizer/decorators/json.py +70 -12
- apytizer/decorators/pagination.py +50 -32
- apytizer/endpoints/__init__.py +6 -0
- apytizer/endpoints/abstract_endpoint.py +38 -0
- apytizer/endpoints/web_endpoint.py +519 -0
- apytizer/engines/__init__.py +6 -0
- apytizer/engines/abstract_engine.py +45 -0
- apytizer/engines/http_engine.py +171 -0
- apytizer/errors.py +34 -0
- apytizer/factories/__init__.py +5 -0
- apytizer/factories/abstract_factory.py +17 -0
- apytizer/http_methods.py +34 -0
- apytizer/managers/__init__.py +12 -0
- apytizer/managers/abstract_manager.py +80 -0
- apytizer/managers/base_manager.py +116 -0
- apytizer/mappers/__init__.py +6 -0
- apytizer/mappers/abstract_mapper.py +48 -0
- apytizer/mappers/base_mapper.py +78 -0
- apytizer/media_types.py +118 -0
- apytizer/models/__init__.py +6 -0
- apytizer/models/abstract_model.py +119 -0
- apytizer/models/base_model.py +85 -0
- apytizer/protocols.py +38 -0
- apytizer/repositories/__init__.py +6 -0
- apytizer/repositories/abstract_repository.py +81 -0
- apytizer/repositories/managed_repository.py +92 -0
- apytizer/routes/__init__.py +6 -0
- apytizer/routes/abstract_route.py +32 -0
- apytizer/routes/base_route.py +138 -0
- apytizer/sessions/__init__.py +33 -0
- apytizer/sessions/abstract_session.py +63 -0
- apytizer/sessions/requests_session.py +125 -0
- apytizer/states/__init__.py +6 -0
- apytizer/states/abstract_state.py +71 -0
- apytizer/states/local_state.py +99 -0
- apytizer/utils/__init__.py +9 -4
- apytizer/utils/caching.py +39 -0
- apytizer/utils/dictionaries.py +376 -0
- apytizer/utils/errors.py +104 -0
- apytizer/utils/iterables.py +91 -0
- apytizer/utils/objects.py +145 -0
- apytizer/utils/strings.py +69 -0
- apytizer/utils/typing.py +29 -0
- apytizer-0.0.1b2.dist-info/METADATA +41 -0
- apytizer-0.0.1b2.dist-info/RECORD +60 -0
- {apytizer-0.0.1a0.dist-info → apytizer-0.0.1b2.dist-info}/WHEEL +1 -2
- apytizer/abstracts/__init__.py +0 -8
- apytizer/abstracts/api.py +0 -147
- apytizer/abstracts/endpoint.py +0 -177
- apytizer/abstracts/model.py +0 -50
- apytizer/abstracts/session.py +0 -39
- apytizer/adapters/transport.py +0 -40
- apytizer/base/__init__.py +0 -8
- apytizer/base/api.py +0 -510
- apytizer/base/endpoint.py +0 -443
- apytizer/base/model.py +0 -119
- apytizer/utils/generate_key.py +0 -18
- apytizer/utils/merge.py +0 -19
- apytizer-0.0.1a0.dist-info/METADATA +0 -27
- apytizer-0.0.1a0.dist-info/RECORD +0 -25
- apytizer-0.0.1a0.dist-info/top_level.txt +0 -1
- {apytizer-0.0.1a0.dist-info → apytizer-0.0.1b2.dist-info/licenses}/LICENSE +0 -0
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# src/apytizer/connection/base_connection.py
|
|
3
|
+
"""HTTP Connection Class.
|
|
4
|
+
|
|
5
|
+
This module defines the base HTTP connection class implementation.
|
|
6
|
+
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
# Standard Library Imports
|
|
10
|
+
from collections import ChainMap
|
|
11
|
+
import logging
|
|
12
|
+
from typing import Any
|
|
13
|
+
from typing import MutableMapping
|
|
14
|
+
from typing import Optional
|
|
15
|
+
from typing import Tuple
|
|
16
|
+
from typing import Union
|
|
17
|
+
from typing import final
|
|
18
|
+
from typing import TYPE_CHECKING
|
|
19
|
+
from urllib.parse import urljoin
|
|
20
|
+
|
|
21
|
+
# Third-Party Imports
|
|
22
|
+
from requests import Request
|
|
23
|
+
from requests import Response
|
|
24
|
+
|
|
25
|
+
# Local Imports
|
|
26
|
+
from .abstract_connection import AbstractConnection
|
|
27
|
+
from ..decorators import confirm_connection
|
|
28
|
+
from ..http_methods import HTTPMethod
|
|
29
|
+
from ..sessions import AbstractSession
|
|
30
|
+
from ..sessions import sessionmaker
|
|
31
|
+
from .. import errors
|
|
32
|
+
|
|
33
|
+
if TYPE_CHECKING:
|
|
34
|
+
from ..engines import HTTPEngine
|
|
35
|
+
|
|
36
|
+
__all__ = ["HttpConnection"]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
log = logging.getLogger("apytizer")
|
|
40
|
+
|
|
41
|
+
# Constants
|
|
42
|
+
DEFAULT_SESSION_FACTORY = sessionmaker()
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class HttpConnection(AbstractConnection):
|
|
46
|
+
"""Implements an HTTP connection.
|
|
47
|
+
|
|
48
|
+
The connection class provides an interface for interacting with an API.
|
|
49
|
+
It implements the standard HTTP methods (HEAD, GET, POST, PUT, PATCH,
|
|
50
|
+
DELETE, OPTIONS and TRACE) as well as a `request` method for sending
|
|
51
|
+
custom HTTP requests.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
engine: Engine.
|
|
55
|
+
session_factory (optional): Function for creating sessions.
|
|
56
|
+
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
def __init__(
|
|
60
|
+
self,
|
|
61
|
+
engine: "HTTPEngine",
|
|
62
|
+
*,
|
|
63
|
+
session_factory: sessionmaker = DEFAULT_SESSION_FACTORY,
|
|
64
|
+
) -> None:
|
|
65
|
+
self._engine = engine
|
|
66
|
+
self._session_factory = session_factory
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def headers(self) -> ChainMap[str, str]:
|
|
70
|
+
"""Connection headers."""
|
|
71
|
+
return self._engine.headers
|
|
72
|
+
|
|
73
|
+
@property
|
|
74
|
+
def params(self) -> ChainMap[str, Any]:
|
|
75
|
+
"""Connection parameters."""
|
|
76
|
+
return self._engine.params
|
|
77
|
+
|
|
78
|
+
@property
|
|
79
|
+
def session(self) -> Optional[AbstractSession]:
|
|
80
|
+
"""Session."""
|
|
81
|
+
return getattr(self, "_session", None)
|
|
82
|
+
|
|
83
|
+
@property
|
|
84
|
+
def timeout(self) -> Optional[Union[float, Tuple[float, float]]]:
|
|
85
|
+
"""Connection timeout."""
|
|
86
|
+
return self._engine.timeout
|
|
87
|
+
|
|
88
|
+
@property
|
|
89
|
+
def url(self) -> str:
|
|
90
|
+
"""Connection URL."""
|
|
91
|
+
return self._engine.url
|
|
92
|
+
|
|
93
|
+
@final
|
|
94
|
+
def __enter__(self) -> AbstractConnection:
|
|
95
|
+
"""Starts connection as context manager."""
|
|
96
|
+
self.start()
|
|
97
|
+
return self
|
|
98
|
+
|
|
99
|
+
@final
|
|
100
|
+
def __exit__(self, *_) -> None:
|
|
101
|
+
"""Ends connection as context manager."""
|
|
102
|
+
self.close()
|
|
103
|
+
|
|
104
|
+
def start(self) -> None:
|
|
105
|
+
"""Start connection."""
|
|
106
|
+
session = self._session_factory(self._engine)
|
|
107
|
+
setattr(self, "_session", session)
|
|
108
|
+
session.start()
|
|
109
|
+
|
|
110
|
+
def close(self) -> None:
|
|
111
|
+
"""Close connection."""
|
|
112
|
+
if self.session is not None:
|
|
113
|
+
self.session.close()
|
|
114
|
+
|
|
115
|
+
def head(
|
|
116
|
+
self,
|
|
117
|
+
route: Optional[str] = None,
|
|
118
|
+
*,
|
|
119
|
+
headers: Optional[MutableMapping[str, str]] = None,
|
|
120
|
+
params: Optional[MutableMapping[str, Any]] = None,
|
|
121
|
+
**kwargs: Any,
|
|
122
|
+
) -> Optional[Response]:
|
|
123
|
+
"""Sends an HTTP HEAD request.
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
route (optional): Route to which to send request. Default ``None``.
|
|
127
|
+
headers (optional): Request headers (overrides global headers).
|
|
128
|
+
params (optional): Request parameters (overrides global parameters).
|
|
129
|
+
**kwargs: Additional arguments to pass to request.
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
Response object.
|
|
133
|
+
|
|
134
|
+
.. _HTTP HEAD Method:
|
|
135
|
+
https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/HEAD
|
|
136
|
+
|
|
137
|
+
"""
|
|
138
|
+
response = self.request(
|
|
139
|
+
HTTPMethod.HEAD,
|
|
140
|
+
route,
|
|
141
|
+
headers=headers,
|
|
142
|
+
params=params,
|
|
143
|
+
**kwargs,
|
|
144
|
+
)
|
|
145
|
+
return response
|
|
146
|
+
|
|
147
|
+
def get(
|
|
148
|
+
self,
|
|
149
|
+
route: Optional[str] = None,
|
|
150
|
+
*,
|
|
151
|
+
headers: Optional[MutableMapping[str, str]] = None,
|
|
152
|
+
params: Optional[MutableMapping[str, Any]] = None,
|
|
153
|
+
**kwargs: Any,
|
|
154
|
+
) -> Optional[Response]:
|
|
155
|
+
"""Sends an HTTP GET request.
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
route (optional): Route to which to send request. Default ``None``.
|
|
159
|
+
headers (optional): Request headers (overrides global headers).
|
|
160
|
+
params (optional): Request parameters (overrides global parameters).
|
|
161
|
+
**kwargs: Additional arguments to pass to request.
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
Response object.
|
|
165
|
+
|
|
166
|
+
.. _HTTP GET Method:
|
|
167
|
+
https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/GET
|
|
168
|
+
|
|
169
|
+
"""
|
|
170
|
+
response = self.request(
|
|
171
|
+
HTTPMethod.GET,
|
|
172
|
+
route,
|
|
173
|
+
headers=headers,
|
|
174
|
+
params=params,
|
|
175
|
+
**kwargs,
|
|
176
|
+
)
|
|
177
|
+
return response
|
|
178
|
+
|
|
179
|
+
def post(
|
|
180
|
+
self,
|
|
181
|
+
route: Optional[str] = None,
|
|
182
|
+
*,
|
|
183
|
+
headers: Optional[MutableMapping[str, str]] = None,
|
|
184
|
+
params: Optional[MutableMapping[str, Any]] = None,
|
|
185
|
+
**kwargs: Any,
|
|
186
|
+
) -> Optional[Response]:
|
|
187
|
+
"""Sends an HTTP POST request.
|
|
188
|
+
|
|
189
|
+
Args:
|
|
190
|
+
route (optional): Route to which to send request. Default ``None``.
|
|
191
|
+
headers (optional): Request headers (overrides global headers).
|
|
192
|
+
params (optional): Request parameters (overrides global parameters).
|
|
193
|
+
**kwargs: Additional arguments to pass to request.
|
|
194
|
+
|
|
195
|
+
Returns:
|
|
196
|
+
Response object.
|
|
197
|
+
|
|
198
|
+
.. _HTTP POST Method:
|
|
199
|
+
https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/POST
|
|
200
|
+
|
|
201
|
+
"""
|
|
202
|
+
response = self.request(
|
|
203
|
+
HTTPMethod.POST,
|
|
204
|
+
route,
|
|
205
|
+
headers=headers,
|
|
206
|
+
params=params,
|
|
207
|
+
**kwargs,
|
|
208
|
+
)
|
|
209
|
+
return response
|
|
210
|
+
|
|
211
|
+
def put(
|
|
212
|
+
self,
|
|
213
|
+
route: Optional[str] = None,
|
|
214
|
+
*,
|
|
215
|
+
headers: Optional[MutableMapping[str, str]] = None,
|
|
216
|
+
params: Optional[MutableMapping[str, Any]] = None,
|
|
217
|
+
**kwargs: Any,
|
|
218
|
+
) -> Optional[Response]:
|
|
219
|
+
"""Sends an HTTP PUT request.
|
|
220
|
+
|
|
221
|
+
Args:
|
|
222
|
+
route (optional): Route to which to send request. Default ``None``.
|
|
223
|
+
headers (optional): Request headers (overrides global headers).
|
|
224
|
+
params (optional): Request parameters (overrides global parameters).
|
|
225
|
+
**kwargs: Additional arguments to pass to request.
|
|
226
|
+
|
|
227
|
+
Returns:
|
|
228
|
+
Response object.
|
|
229
|
+
|
|
230
|
+
.. _HTTP PUT Method:
|
|
231
|
+
https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/PUT
|
|
232
|
+
|
|
233
|
+
"""
|
|
234
|
+
response = self.request(
|
|
235
|
+
HTTPMethod.PUT,
|
|
236
|
+
route,
|
|
237
|
+
headers=headers,
|
|
238
|
+
params=params,
|
|
239
|
+
**kwargs,
|
|
240
|
+
)
|
|
241
|
+
return response
|
|
242
|
+
|
|
243
|
+
def patch(
|
|
244
|
+
self,
|
|
245
|
+
route: Optional[str] = None,
|
|
246
|
+
*,
|
|
247
|
+
headers: Optional[MutableMapping[str, str]] = None,
|
|
248
|
+
params: Optional[MutableMapping[str, Any]] = None,
|
|
249
|
+
**kwargs: Any,
|
|
250
|
+
) -> Optional[Response]:
|
|
251
|
+
"""Sends an HTTP PATCH request.
|
|
252
|
+
|
|
253
|
+
Args:
|
|
254
|
+
route (optional): Route to which to send request. Default ``None``.
|
|
255
|
+
headers (optional): Request headers (overrides global headers).
|
|
256
|
+
params (optional): Request parameters (overrides global parameters).
|
|
257
|
+
**kwargs: Additional arguments to pass to request.
|
|
258
|
+
|
|
259
|
+
Returns:
|
|
260
|
+
Response object.
|
|
261
|
+
|
|
262
|
+
.. _HTTP PATCH Method:
|
|
263
|
+
https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/PATCH
|
|
264
|
+
|
|
265
|
+
"""
|
|
266
|
+
response = self.request(
|
|
267
|
+
HTTPMethod.PATCH,
|
|
268
|
+
route,
|
|
269
|
+
headers=headers,
|
|
270
|
+
params=params,
|
|
271
|
+
**kwargs,
|
|
272
|
+
)
|
|
273
|
+
return response
|
|
274
|
+
|
|
275
|
+
def delete(
|
|
276
|
+
self,
|
|
277
|
+
route: Optional[str] = None,
|
|
278
|
+
*,
|
|
279
|
+
headers: Optional[MutableMapping[str, str]] = None,
|
|
280
|
+
params: Optional[MutableMapping[str, Any]] = None,
|
|
281
|
+
**kwargs: Any,
|
|
282
|
+
) -> Optional[Response]:
|
|
283
|
+
"""Sends an HTTP DELETE request.
|
|
284
|
+
|
|
285
|
+
Args:
|
|
286
|
+
route (optional): Route to which to send request. Default ``None``.
|
|
287
|
+
headers (optional): Request headers (overrides global headers).
|
|
288
|
+
params (optional): Request parameters (overrides global parameters).
|
|
289
|
+
**kwargs: Additional arguments to pass to request.
|
|
290
|
+
|
|
291
|
+
Returns:
|
|
292
|
+
Response object.
|
|
293
|
+
|
|
294
|
+
.. _HTTP DELETE Method:
|
|
295
|
+
https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/DELETE
|
|
296
|
+
|
|
297
|
+
"""
|
|
298
|
+
response = self.request(
|
|
299
|
+
HTTPMethod.DELETE,
|
|
300
|
+
route,
|
|
301
|
+
headers=headers,
|
|
302
|
+
params=params,
|
|
303
|
+
**kwargs,
|
|
304
|
+
)
|
|
305
|
+
return response
|
|
306
|
+
|
|
307
|
+
def options(
|
|
308
|
+
self,
|
|
309
|
+
route: Optional[str] = None,
|
|
310
|
+
*,
|
|
311
|
+
headers: Optional[MutableMapping[str, str]] = None,
|
|
312
|
+
params: Optional[MutableMapping[str, Any]] = None,
|
|
313
|
+
**kwargs: Any,
|
|
314
|
+
) -> Optional[Response]:
|
|
315
|
+
"""Sends an HTTP OPTIONS request.
|
|
316
|
+
|
|
317
|
+
Args:
|
|
318
|
+
route (optional): Route to which to send request. Default ``None``.
|
|
319
|
+
headers (optional): Request headers (overrides global headers).
|
|
320
|
+
params (optional): Request parameters (overrides global parameters).
|
|
321
|
+
**kwargs: Additional arguments to pass to request.
|
|
322
|
+
|
|
323
|
+
Returns:
|
|
324
|
+
Response object.
|
|
325
|
+
|
|
326
|
+
.. _HTTP OPTIONS Method:
|
|
327
|
+
https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/OPTIONS
|
|
328
|
+
|
|
329
|
+
"""
|
|
330
|
+
response = self.request(
|
|
331
|
+
HTTPMethod.OPTIONS,
|
|
332
|
+
route,
|
|
333
|
+
headers=headers,
|
|
334
|
+
params=params,
|
|
335
|
+
**kwargs,
|
|
336
|
+
)
|
|
337
|
+
return response
|
|
338
|
+
|
|
339
|
+
def trace(
|
|
340
|
+
self,
|
|
341
|
+
route: Optional[str] = None,
|
|
342
|
+
*,
|
|
343
|
+
headers: Optional[MutableMapping[str, str]] = None,
|
|
344
|
+
params: Optional[MutableMapping[str, Any]] = None,
|
|
345
|
+
**kwargs: Any,
|
|
346
|
+
) -> Optional[Response]:
|
|
347
|
+
"""Sends an HTTP TRACE request.
|
|
348
|
+
|
|
349
|
+
Args:
|
|
350
|
+
route (optional): Route to which to send request. Default ``None``.
|
|
351
|
+
headers (optional): Request headers (overrides global headers).
|
|
352
|
+
params (optional): Request parameters (overrides global parameters).
|
|
353
|
+
**kwargs: Additional arguments to pass to request.
|
|
354
|
+
|
|
355
|
+
Returns:
|
|
356
|
+
Response object.
|
|
357
|
+
|
|
358
|
+
.. _HTTP TRACE Method:
|
|
359
|
+
https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/TRACE
|
|
360
|
+
|
|
361
|
+
"""
|
|
362
|
+
response = self.request(
|
|
363
|
+
HTTPMethod.TRACE,
|
|
364
|
+
route,
|
|
365
|
+
headers=headers,
|
|
366
|
+
params=params,
|
|
367
|
+
**kwargs,
|
|
368
|
+
)
|
|
369
|
+
return response
|
|
370
|
+
|
|
371
|
+
def request(
|
|
372
|
+
self,
|
|
373
|
+
method: HTTPMethod,
|
|
374
|
+
/,
|
|
375
|
+
route: Optional[str] = None,
|
|
376
|
+
*,
|
|
377
|
+
headers: Optional[MutableMapping[str, str]] = None,
|
|
378
|
+
params: Optional[MutableMapping[str, Any]] = None,
|
|
379
|
+
**kwargs: Any,
|
|
380
|
+
) -> Optional[Response]:
|
|
381
|
+
"""Sends an HTTP request.
|
|
382
|
+
|
|
383
|
+
Args:
|
|
384
|
+
method: HTTP request method to use.
|
|
385
|
+
route (optional): Route to which to send request. Default ``None``.
|
|
386
|
+
headers (optional): Request headers (overrides global headers).
|
|
387
|
+
params (optional): Request parameters (overrides global parameters).
|
|
388
|
+
**kwargs: Additional arguments to pass to request.
|
|
389
|
+
|
|
390
|
+
Returns:
|
|
391
|
+
Response object.
|
|
392
|
+
|
|
393
|
+
.. _Requests Documentation:
|
|
394
|
+
https://docs.python-requests.org/en/latest/api/
|
|
395
|
+
|
|
396
|
+
"""
|
|
397
|
+
request = Request(
|
|
398
|
+
method.name,
|
|
399
|
+
urljoin(self.url, route),
|
|
400
|
+
headers=self.headers.new_child(headers),
|
|
401
|
+
params=self.params.new_child(params),
|
|
402
|
+
**kwargs,
|
|
403
|
+
)
|
|
404
|
+
response = self.send(request)
|
|
405
|
+
return response
|
|
406
|
+
|
|
407
|
+
@confirm_connection
|
|
408
|
+
def send(self, request: Request) -> Optional[Response]:
|
|
409
|
+
"""Sends an HTTP request.
|
|
410
|
+
|
|
411
|
+
Args:
|
|
412
|
+
request: Request to send.
|
|
413
|
+
|
|
414
|
+
Returns:
|
|
415
|
+
Response object.
|
|
416
|
+
|
|
417
|
+
"""
|
|
418
|
+
if self.session is None:
|
|
419
|
+
message = "session not started before sending request"
|
|
420
|
+
raise errors.SessionNotStarted(message)
|
|
421
|
+
|
|
422
|
+
log.debug(
|
|
423
|
+
"Sending HTTP %(method)s request to %(url)s",
|
|
424
|
+
{"method": request.method, "url": request.url},
|
|
425
|
+
)
|
|
426
|
+
response = self.session.send(request, timeout=self.timeout)
|
|
427
|
+
log.debug(
|
|
428
|
+
"Received response with status code %(status)s",
|
|
429
|
+
{"status": response.status_code},
|
|
430
|
+
)
|
|
431
|
+
return response
|
apytizer/decorators/__init__.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# -*- coding: utf-8 -*-
|
|
2
|
+
# src/apytizer/decorators/__init__.py
|
|
2
3
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
from .
|
|
6
|
-
from .
|
|
7
|
-
from .pagination import pagination
|
|
4
|
+
from .caching import *
|
|
5
|
+
from .connection import *
|
|
6
|
+
from .json import *
|
|
7
|
+
from .pagination import *
|
apytizer/decorators/caching.py
CHANGED
|
@@ -1,24 +1,28 @@
|
|
|
1
1
|
# -*- coding: utf-8 -*-
|
|
2
|
+
# src/apytizer/decorators/caching.py
|
|
2
3
|
|
|
3
4
|
# Standard Library Imports
|
|
4
5
|
import functools
|
|
5
|
-
import logging
|
|
6
6
|
import operator
|
|
7
|
+
from typing import Any
|
|
7
8
|
from typing import Callable
|
|
9
|
+
from typing import TypeVar
|
|
8
10
|
|
|
9
11
|
# Third-Party Imports
|
|
10
12
|
from cachetools import cachedmethod
|
|
11
13
|
|
|
12
14
|
# Local Imports
|
|
13
|
-
from ..utils import generate_key
|
|
15
|
+
from ..utils.caching import generate_key
|
|
14
16
|
|
|
17
|
+
__all__ = ["cache_response"]
|
|
15
18
|
|
|
16
|
-
log = logging.getLogger(__name__)
|
|
17
19
|
|
|
20
|
+
# Custom types
|
|
21
|
+
T = TypeVar("T")
|
|
18
22
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
Decorator function for handling caching.
|
|
23
|
+
|
|
24
|
+
def cache_response(func: Callable[..., T]) -> Callable[..., T]:
|
|
25
|
+
"""Decorator function for handling caching.
|
|
22
26
|
|
|
23
27
|
Args:
|
|
24
28
|
func: Decorated function.
|
|
@@ -27,10 +31,57 @@ def cache_response(func: Callable) -> Callable:
|
|
|
27
31
|
Wrapped function.
|
|
28
32
|
|
|
29
33
|
"""
|
|
34
|
+
cached_func = cachedmethod(
|
|
35
|
+
operator.attrgetter("cache"),
|
|
36
|
+
key=generate_key(func.__name__.upper()),
|
|
37
|
+
)(func)
|
|
38
|
+
|
|
30
39
|
@functools.wraps(func)
|
|
31
|
-
|
|
32
|
-
def wrapper(*args, **kwargs):
|
|
40
|
+
def wrapper(*args: Any, **kwargs: Any) -> T:
|
|
33
41
|
"""Wrapper applied to decorated function."""
|
|
34
|
-
|
|
42
|
+
try:
|
|
43
|
+
result = cached_func(*args, **kwargs)
|
|
44
|
+
|
|
45
|
+
except TypeError as error: # raised when cache is 'None'
|
|
46
|
+
if is_missing_cache(error):
|
|
47
|
+
result = func(*args, **kwargs)
|
|
48
|
+
|
|
49
|
+
else:
|
|
50
|
+
raise error
|
|
35
51
|
|
|
52
|
+
return result
|
|
53
|
+
|
|
54
|
+
functools.update_wrapper(wrapper, func)
|
|
36
55
|
return wrapper
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
# ----------------------------------------------------------------------------
|
|
59
|
+
# Validators
|
|
60
|
+
# ----------------------------------------------------------------------------
|
|
61
|
+
def is_missing_cache(e: Exception) -> bool:
|
|
62
|
+
"""Check whether exception was raised because of missing cache.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
e: Exception.
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
Whether exception was raised because of missing cache.
|
|
69
|
+
|
|
70
|
+
"""
|
|
71
|
+
expected = "'NoneType' object is not subscriptable"
|
|
72
|
+
result = is_nonetype_error(e) and expected in str(e)
|
|
73
|
+
return result
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def is_nonetype_error(e: Exception) -> bool:
|
|
77
|
+
"""Check whether exception is 'NoneType' error.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
e: Exception.
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
Whether exception wis 'NoneType' error.
|
|
84
|
+
|
|
85
|
+
"""
|
|
86
|
+
result = isinstance(e, TypeError) and "NoneType" in str(e)
|
|
87
|
+
return result
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# src/apytizer/decorators/chunking.py
|
|
3
|
+
|
|
4
|
+
# Standard Library Imports
|
|
5
|
+
import functools
|
|
6
|
+
from typing import Any
|
|
7
|
+
from typing import Callable
|
|
8
|
+
from typing import Dict
|
|
9
|
+
from typing import Iterable
|
|
10
|
+
from typing import List
|
|
11
|
+
from typing import Union
|
|
12
|
+
|
|
13
|
+
# Third-Party Imports
|
|
14
|
+
from requests import Response
|
|
15
|
+
|
|
16
|
+
# Local Imports
|
|
17
|
+
from .. import utils
|
|
18
|
+
|
|
19
|
+
__all__ = ["chunked_request"]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def chunked_request(
|
|
23
|
+
max_size: int,
|
|
24
|
+
) -> Callable[..., Callable[..., Union[Dict[str, Any], Response]]]:
|
|
25
|
+
"""Split request payload based on maximum request size.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
max_size: Maximum size of request.
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
Wrapped function.
|
|
32
|
+
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
def decorator(
|
|
36
|
+
func: Callable[[Any, List[Any]], Union[Dict[str, Any], Response]],
|
|
37
|
+
) -> Callable[..., Union[Dict[str, Any], Response]]:
|
|
38
|
+
"""Decorator function for handling chunking.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
func: Decorated function.
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
Wrapped function.
|
|
45
|
+
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
@functools.wraps(func)
|
|
49
|
+
def wrapper(
|
|
50
|
+
self: Any, data: List[Any], *args: Any, **kwargs: Any
|
|
51
|
+
) -> Union[Dict[str, Any], Response]:
|
|
52
|
+
"""Wrapper applied to decorated function.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
data: Data to chunk into multiple requests.
|
|
56
|
+
*args: Positional arguments to pass to wrapped function.
|
|
57
|
+
**kwargs: Keyword arguments to pass to wrapped function.
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
Results.
|
|
61
|
+
|
|
62
|
+
"""
|
|
63
|
+
if not isinstance(data, Iterable): # type: ignore
|
|
64
|
+
message = f"expected iterable object, got {type(data)} instead"
|
|
65
|
+
raise TypeError(message)
|
|
66
|
+
|
|
67
|
+
results = {}
|
|
68
|
+
for group in utils.split_list(data, max_size):
|
|
69
|
+
response = func(self, group, *args, **kwargs)
|
|
70
|
+
results = update_results(results, response)
|
|
71
|
+
|
|
72
|
+
return results
|
|
73
|
+
|
|
74
|
+
functools.update_wrapper(wrapper, func)
|
|
75
|
+
return wrapper
|
|
76
|
+
|
|
77
|
+
return decorator
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def update_results(
|
|
81
|
+
results: Dict[str, List[Any]],
|
|
82
|
+
response: Union[Dict[str, List[Any]], Response],
|
|
83
|
+
) -> Dict[str, Any]:
|
|
84
|
+
"""Update results with response data.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
results: Results to update.
|
|
88
|
+
response: Response with which to update results.
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
Updated results.
|
|
92
|
+
|
|
93
|
+
"""
|
|
94
|
+
if isinstance(response, Response) and response.ok:
|
|
95
|
+
return results
|
|
96
|
+
|
|
97
|
+
if not isinstance(response, dict): # type: ignore
|
|
98
|
+
message = f"expected type `dict`, got {type(response)} instead"
|
|
99
|
+
raise TypeError(message)
|
|
100
|
+
|
|
101
|
+
for key, value in response.items():
|
|
102
|
+
results.setdefault(key, [])
|
|
103
|
+
results[key].extend(value)
|
|
104
|
+
|
|
105
|
+
return results
|