pierre-sdk 0.1.0.dev4__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.
pierre/_retry.py ADDED
@@ -0,0 +1,65 @@
1
+ """The retry rules: which results are transient, and how long to wait."""
2
+
3
+ import asyncio
4
+ import random
5
+ import re
6
+ import time
7
+ from email.utils import parsedate_to_datetime
8
+ from typing import Optional
9
+
10
+ # The 409 codes of a condition that ends by itself.
11
+ _RETRYABLE_CONFLICT_CODES = frozenset({"repository_thawing", "sync_in_progress"})
12
+
13
+ # The first backoff delay, in seconds.
14
+ _INITIAL_DELAY = 0.5
15
+ # The longest backoff delay.
16
+ _MAX_BACKOFF = 8.0
17
+ # The longest `Retry-After` that the SDK waits. A longer value falls back to the backoff.
18
+ _MAX_RETRY_AFTER = 60.0
19
+
20
+ # The HTTP date form that servers send (IMF-fixdate, RFC 9110), for example
21
+ # `Sun, 06 Nov 1994 08:49:37 GMT`.
22
+ _HTTP_DATE = re.compile(
23
+ r"[A-Z][a-z]{2}, [0-9]{2} [A-Z][a-z]{2} [0-9]{4} [0-9]{2}:[0-9]{2}:[0-9]{2} GMT", re.ASCII
24
+ )
25
+ _SECONDS = re.compile(r"[0-9]+(\.[0-9]+)?")
26
+
27
+ # The wait between two attempts. Tests replace them to record the delays.
28
+ sleep = time.sleep
29
+ async_sleep = asyncio.sleep
30
+
31
+
32
+ def transient_status(status: int, code: Optional[str]) -> bool:
33
+ """Whether an error status can succeed on a repeat."""
34
+ return (
35
+ status in (408, 429)
36
+ or status >= 500
37
+ or (status == 409 and code is not None and code in _RETRYABLE_CONFLICT_CODES)
38
+ )
39
+
40
+
41
+ def retry_delay(retry_after: Optional[str], attempt: int, now: Optional[float] = None) -> float:
42
+ """The wait in seconds before retry number `attempt + 1`: the `Retry-After` value when it is
43
+ 60 seconds or less, or else an exponential backoff from 0.5 seconds, up to 8 seconds, less up
44
+ to 25% jitter."""
45
+ after = _retry_after_seconds(retry_after, time.time() if now is None else now)
46
+ if after is not None and after <= _MAX_RETRY_AFTER:
47
+ return after
48
+ backoff: float = min(_INITIAL_DELAY * 2**attempt, _MAX_BACKOFF)
49
+ return backoff * (1 - random.random() * 0.25)
50
+
51
+
52
+ def _retry_after_seconds(value: Optional[str], now: float) -> Optional[float]:
53
+ """A `Retry-After` value in seconds or as an HTTP date, in seconds from `now`."""
54
+ if value is None:
55
+ return None
56
+ text = value.strip()
57
+ if _SECONDS.fullmatch(text):
58
+ return float(text)
59
+ if not _HTTP_DATE.fullmatch(text):
60
+ return None
61
+ try:
62
+ date = parsedate_to_datetime(text)
63
+ except (TypeError, ValueError):
64
+ return None
65
+ return max(0.0, date.timestamp() - now)