canonicalwebteam.store-api 6.7.0__py3-none-any.whl → 6.8.1__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.

Potentially problematic release.


This version of canonicalwebteam.store-api might be problematic. Click here for more details.

@@ -14,6 +14,46 @@ class StoreApiConnectionError(StoreApiError):
14
14
  pass
15
15
 
16
16
 
17
+ class StoreApiInternalError(StoreApiConnectionError):
18
+ """
19
+ Store API internal error
20
+ """
21
+
22
+ pass
23
+
24
+
25
+ class StoreApiNotImplementedError(StoreApiConnectionError):
26
+ """
27
+ Store API doesn't implement this method
28
+ """
29
+
30
+ pass
31
+
32
+
33
+ class StoreApiBadGatewayError(StoreApiConnectionError):
34
+ """
35
+ Got and invalid response from Store API
36
+ """
37
+
38
+ pass
39
+
40
+
41
+ class StoreApiServiceUnavailableError(StoreApiConnectionError):
42
+ """
43
+ Store API is not available
44
+ """
45
+
46
+ pass
47
+
48
+
49
+ class StoreApiGatewayTimeoutError(StoreApiConnectionError):
50
+ """
51
+ Request to Store API timed out
52
+ """
53
+
54
+ pass
55
+
56
+
17
57
  class StoreApiResourceNotFound(StoreApiError):
18
58
  """
19
59
  The requested resource is not found
@@ -0,0 +1,237 @@
1
+ from typing import Callable, Tuple, Type, TypeVar
2
+ from random import uniform
3
+ from sys import maxsize as MAX_INT
4
+ import functools
5
+
6
+
7
+ P = TypeVar("P")
8
+ R = TypeVar("R")
9
+
10
+
11
+ def retry(
12
+ func: Callable[P, R] = None,
13
+ *,
14
+ limit: int = MAX_INT,
15
+ delay_fn: Callable[[int], float] = (lambda x: 0.0),
16
+ sleep_fn: Callable[[float], None] = (lambda x: None),
17
+ callback_fn: Callable[[Exception], bool] = (lambda x: False),
18
+ logger_fn: Callable[[str], None] = (lambda x: None),
19
+ exceptions: Tuple[Type[Exception]] = (Exception),
20
+ ) -> Callable[P, R]:
21
+ """
22
+ Decorator that implements retry logic for `func` when any of the
23
+ Exceptions in `exceptions` happen.
24
+
25
+ Args:
26
+ func (Callable[P, R], optional): The function that will be retried.
27
+ Defaults to `None` when invoking the decorator using parameters,
28
+ e.g. `@retry(limit=3)`).
29
+ limit (int, optional): The maximum number of retry attempts.
30
+ Defaults to `sys.maxsize`, allowing for a very large number of
31
+ retries.
32
+ delay_fn (Callable[[int], float], optional): A function that accepts
33
+ the current attempt number (starting from 1) and returns a float
34
+ representing the delay in seconds before the next call to `func`.
35
+ Defaults to a function that always returns 0.0 (no delay).
36
+ sleep_fn (Callable[[float], None], optional): A function that takes
37
+ a float (delay in seconds) and pauses execution for that duration.
38
+ Users are responsible for ensuring this function is appropriate
39
+ for their environment (e.g., an async sleep for async contexts).
40
+ Defaults to a function that performs no actual sleep.
41
+ callback_fn (Callable[[Exception], bool], optional): A function that
42
+ is called every time an exception from `exceptions` is caught
43
+ during the retry loop. It receives the caught exception as an
44
+ argument and should return `True` to abort the retry loop, or
45
+ `False` to continue. Defaults to a function that always returns
46
+ `False`.
47
+ logger_fn (Callable[[str], None], optional): A function designed to
48
+ log errors that are caught and handled (not propagated) during
49
+ the retry loop. It's invoked each time an exception from
50
+ `exceptions` is caught. Defaults to a function that performs no
51
+ logging.
52
+ exceptions (Tuple[Type[Exception]], optional): A tuple containing the
53
+ types of exceptions that will trigger a retry. Defaults to
54
+ `(Exception)`, meaning any standard exception will cause a retry.
55
+
56
+ Returns:
57
+ Callable[P, R]: A decorated version of the input function `func`
58
+ that incorporates the defined retry logic. If `func` is initially
59
+ `None` (when used as `@retry(...)`), it returns a decorator
60
+ function ready to be applied to another callable.
61
+
62
+ Raises:
63
+ ValueError: if `limit` is less than 1
64
+ """
65
+
66
+ if func is None:
67
+ # if the decorator is applied using parameters (e.g. @retry(limit=3)),
68
+ # `func` will be None, so we must first do a partial application on
69
+ # the decorator itself to actually wrap `func` correctly
70
+ return functools.partial(
71
+ retry,
72
+ limit=limit,
73
+ delay_fn=delay_fn,
74
+ sleep_fn=sleep_fn,
75
+ callback_fn=callback_fn,
76
+ logger_fn=logger_fn,
77
+ exceptions=exceptions,
78
+ )
79
+
80
+ if limit < 1:
81
+ raise ValueError("The limit must be at least 1")
82
+
83
+ @functools.wraps(func)
84
+ def _retry(*args, **kwargs):
85
+ retry_attempts = 0
86
+ last_exception = None
87
+
88
+ while retry_attempts < limit:
89
+ if retry_attempts > 0:
90
+ # only sleep if we've already tried once
91
+ sleep_fn(delay_fn(retry_attempts))
92
+
93
+ try:
94
+ return func(*args, **kwargs)
95
+ except exceptions as e:
96
+ last_exception = e
97
+ retry_attempts += 1
98
+
99
+ if callback_fn(e):
100
+ # stop early if callback says so, raise `e` immediately
101
+ raise e
102
+
103
+ logger_fn(
104
+ f"@retry ({retry_attempts}/{limit}) `{func.__name__}`: {e}"
105
+ )
106
+
107
+ # if we made it here, it means we ran the loop and couldn't get a
108
+ # clean run, raise the last exception we caught and let the user
109
+ # deal with it
110
+ raise last_exception
111
+
112
+ return _retry
113
+
114
+
115
+ def delay_constant(d: float):
116
+ """
117
+ Create a constant delay function that always returns `d`.
118
+
119
+ Args:
120
+ d (float): The constant delay in seconds that the returned function
121
+ will always provide.
122
+
123
+ Returns:
124
+ Callable[[int], float]: A function that takes an integer
125
+ (representing the attempt number) and always returns the
126
+ specified `d` float.
127
+
128
+ Raises:
129
+ ValueError: if `d` is negative
130
+ """
131
+ if d < 0:
132
+ raise ValueError("The delay must be at least 0")
133
+
134
+ def _delay_constant(_: int):
135
+ return d
136
+
137
+ return _delay_constant
138
+
139
+
140
+ def delay_random(min_delay: float, max_delay: float):
141
+ """
142
+ Create a random delay function that returns a random value between
143
+ `min_delay` and `max_delay`.
144
+
145
+ Args:
146
+ min_delay (float): The minimum delay in seconds that the returned
147
+ function will provide.
148
+ max_delay (float): The maximum delay in seconds that the returned
149
+ function will provide.
150
+
151
+ Returns:
152
+ Callable[[int], float]: A function that takes an integer
153
+ (representing the attempt number) and returns a random float
154
+ between `min_delay` and `max_delay`.
155
+
156
+ Raises:
157
+ ValueError: if `min` is negative
158
+ ValueError: if `max` is less than or equal to `min`
159
+ """
160
+
161
+ if min_delay < 0:
162
+ raise ValueError("The minimum delay must be at least 0")
163
+ if max_delay <= min_delay:
164
+ raise ValueError("The maximum delay must be greater than the minimum")
165
+
166
+ def _delay_random(_: int):
167
+ return uniform(min_delay, max_delay)
168
+
169
+ return _delay_random
170
+
171
+
172
+ def delay_exponential(
173
+ delay_mult: float, exp_base: float, max_delay: float = float("inf")
174
+ ):
175
+ """
176
+ Create a function that implements an exponential backoff with an upper
177
+ limit. The delay is calculated based on the number of attempts made `n`,
178
+ according to the following formula:
179
+ `min(max_delay, delay_mult * exp_base^n)`.
180
+
181
+ Args:
182
+ delay_mult (float): The multiplier for the exponential delay
183
+ calculation. This value scales the base exponential growth.
184
+ exp_base (float): The base of the exponent for the delay
185
+ calculation. This determines how quickly the delay increases
186
+ with each attempt.
187
+ max_delay (float, optional): The upper limit for the delay in
188
+ seconds. The calculated exponential delay will not exceed this
189
+ value. Defaults to positive infinity.
190
+
191
+ Returns:
192
+ Callable[[int], float]: A function that takes an integer
193
+ (representing the attempt number `n`) and returns a float
194
+ indicating the calculated exponential delay.
195
+
196
+ Raises:
197
+ ValueError: if `delay_mult` is negative or zero
198
+ ValueError: if `exp_base` is less than 1
199
+ ValueError: if `max_delay` is negative
200
+ """
201
+ if delay_mult <= 0:
202
+ raise ValueError("The delay multiplier must be greater than 0")
203
+ if exp_base <= 1:
204
+ raise ValueError("The exponential base must be greater than 1")
205
+ if max_delay <= 0:
206
+ raise ValueError("The maximum delay must be greater than 0")
207
+
208
+ def _delay_exponential(attempt: int):
209
+ return min(max_delay, delay_mult * (exp_base**attempt))
210
+
211
+ return _delay_exponential
212
+
213
+
214
+ """
215
+ Example usage:
216
+
217
+ @retry(
218
+ limit=5,
219
+ logger_fn=print,
220
+ exceptions=(TypeError),
221
+ sleep_fn=sleep,
222
+ delay_fn=delay_exponential(1, 2)
223
+ )
224
+ def test_fn(val: int):
225
+ return None + val # this will raise TypeError
226
+
227
+ try:
228
+ test_fn(2)
229
+ except TypeError as e:
230
+ print(e)
231
+
232
+ This will attempt to run `test_fn` 5 times, will catch TypeError 4 times and
233
+ then it will let the exception propagate after executing the function for the
234
+ last time. Each time, it will take an exponentially longer amount of time to
235
+ run, making the total execution time around 30s (plus some small random delay
236
+ caused by the OS).
237
+ """
@@ -1,12 +1,17 @@
1
1
  from canonicalwebteam.exceptions import (
2
+ PublisherAgreementNotSigned,
3
+ PublisherMacaroonRefreshRequired,
4
+ PublisherMissingUsername,
5
+ StoreApiBadGatewayError,
2
6
  StoreApiConnectionError,
7
+ StoreApiGatewayTimeoutError,
8
+ StoreApiInternalError,
9
+ StoreApiNotImplementedError,
3
10
  StoreApiResourceNotFound,
4
11
  StoreApiResponseDecodeError,
5
12
  StoreApiResponseError,
6
13
  StoreApiResponseErrorList,
7
- PublisherAgreementNotSigned,
8
- PublisherMacaroonRefreshRequired,
9
- PublisherMissingUsername,
14
+ StoreApiServiceUnavailableError,
10
15
  )
11
16
 
12
17
 
@@ -17,7 +22,22 @@ class Base:
17
22
  def process_response(self, response):
18
23
  # 5xx responses are not in JSON format
19
24
  if response.status_code >= 500:
20
- raise StoreApiConnectionError("Service Unavailable")
25
+ if response.status_code == 500:
26
+ raise StoreApiInternalError("Internal error upstream")
27
+ elif response.status_code == 501:
28
+ raise StoreApiNotImplementedError(
29
+ "Service doesn't implement this method"
30
+ )
31
+ elif response.status_code == 502:
32
+ raise StoreApiBadGatewayError("Invalid response from upstream")
33
+ elif response.status_code == 503:
34
+ raise StoreApiServiceUnavailableError("Service is unavailable")
35
+ elif response.status_code == 504:
36
+ raise StoreApiGatewayTimeoutError("Upstream request timed out")
37
+ else:
38
+ raise StoreApiConnectionError(
39
+ f"Service unavailable, code {response.status_code}"
40
+ )
21
41
 
22
42
  try:
23
43
  body = response.json()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: canonicalwebteam.store-api
3
- Version: 6.7.0
3
+ Version: 6.8.1
4
4
  Summary:
5
5
  License: LGPL-3.0
6
6
  Author: Canonical Web Team
@@ -0,0 +1,12 @@
1
+ canonicalwebteam/__init__.py,sha256=ED6jHcYiuYpr_0vjGz0zx2lrrmJT9sDJCzIljoDfmlM,65
2
+ canonicalwebteam/exceptions.py,sha256=Uf9HxtLH5fAXPdDm6H14tA8jUxKQAWUmWIovzHLtdRw,2134
3
+ canonicalwebteam/retry_utils.py,sha256=zGGabWzHyoXPNdwXBS_gbz3W0Qpdg3wN1G6aNy_xef8,8517
4
+ canonicalwebteam/store_api/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
+ canonicalwebteam/store_api/base.py,sha256=_kx3_hPQHJPUtdf3z4NXmcwHlswV1g1mw483fZXD5LM,3331
6
+ canonicalwebteam/store_api/dashboard.py,sha256=M5JLjTTahN-bfiVz9SuP6ahLjqEvOalwmNim6X5Ky6o,22796
7
+ canonicalwebteam/store_api/devicegw.py,sha256=YXmVXdHCZhukNHJq-eaFUCxa2VxqLT8qt19UrqgXGN0,9777
8
+ canonicalwebteam/store_api/publishergw.py,sha256=u2D2Y76xC8ms16XTZcUe_KmRdcyzqOV2g0aN9HYZBrQ,29702
9
+ canonicalwebteam_store_api-6.8.1.dist-info/LICENSE,sha256=46mU2C5kSwOnkqkw9XQAJlhBL2JAf1_uCD8lVcXyMRg,7652
10
+ canonicalwebteam_store_api-6.8.1.dist-info/METADATA,sha256=EwLBQ8u54hVJw3XFgYLribWqsubU_gNtAMdVMyj6VxA,2253
11
+ canonicalwebteam_store_api-6.8.1.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
12
+ canonicalwebteam_store_api-6.8.1.dist-info/RECORD,,
@@ -1,11 +0,0 @@
1
- canonicalwebteam/__init__.py,sha256=ED6jHcYiuYpr_0vjGz0zx2lrrmJT9sDJCzIljoDfmlM,65
2
- canonicalwebteam/exceptions.py,sha256=A49LRhiEIfXNB2zC-zNcZEcdh0ugw6G7fBbXbs-BWxA,1517
3
- canonicalwebteam/store_api/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
- canonicalwebteam/store_api/base.py,sha256=DmfVcefWxGYNQxzYmaDVhmthTCg2vEwEnvczU9Bk5zE,2414
5
- canonicalwebteam/store_api/dashboard.py,sha256=M5JLjTTahN-bfiVz9SuP6ahLjqEvOalwmNim6X5Ky6o,22796
6
- canonicalwebteam/store_api/devicegw.py,sha256=YXmVXdHCZhukNHJq-eaFUCxa2VxqLT8qt19UrqgXGN0,9777
7
- canonicalwebteam/store_api/publishergw.py,sha256=u2D2Y76xC8ms16XTZcUe_KmRdcyzqOV2g0aN9HYZBrQ,29702
8
- canonicalwebteam_store_api-6.7.0.dist-info/LICENSE,sha256=46mU2C5kSwOnkqkw9XQAJlhBL2JAf1_uCD8lVcXyMRg,7652
9
- canonicalwebteam_store_api-6.7.0.dist-info/METADATA,sha256=PJgxFUHDwul_1i3n7LbmZrLHxPNMJB9GPGXDYBUUD_0,2253
10
- canonicalwebteam_store_api-6.7.0.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
11
- canonicalwebteam_store_api-6.7.0.dist-info/RECORD,,