Skip to content

Commit c0cfb07

Browse files
committed
Update coverage report and enhance idempotency utilities with retry logic
1 parent edc01ac commit c0cfb07

6 files changed

Lines changed: 336 additions & 111 deletions

File tree

.coverage

0 Bytes
Binary file not shown.

coverage.xml

Lines changed: 130 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<?xml version="1.0" ?>
2-
<coverage version="7.10.4" timestamp="1755791654413" lines-valid="195" lines-covered="195" line-rate="1" branches-valid="20" branches-covered="20" branch-rate="1" complexity="0">
2+
<coverage version="7.10.4" timestamp="1755794587301" lines-valid="275" lines-covered="243" line-rate="0.8836" branches-valid="44" branches-covered="32" branch-rate="0.7273" complexity="0">
33
<!-- Generated by coverage.py: https://coverage.readthedocs.io/en/7.10.4 -->
44
<!-- Based on https://raw.githubusercontent.com/cobertura/web/master/htdocs/xml/coverage-04.dtd -->
55
<sources>
@@ -28,19 +28,21 @@
2828
<methods/>
2929
<lines>
3030
<line number="3" hits="1"/>
31-
<line number="6" hits="1"/>
32-
<line number="7" hits="1"/>
33-
<line number="10" hits="1"/>
34-
<line number="11" hits="1"/>
31+
<line number="5" hits="1"/>
32+
<line number="8" hits="1"/>
33+
<line number="9" hits="1"/>
3534
<line number="12" hits="1"/>
35+
<line number="13" hits="1"/>
36+
<line number="14" hits="1"/>
3637
<line number="15" hits="1"/>
37-
<line number="16" hits="1"/>
38+
<line number="18" hits="1"/>
3839
<line number="19" hits="1"/>
39-
<line number="20" hits="1"/>
40-
<line number="21" hits="1"/>
4140
<line number="22" hits="1"/>
4241
<line number="23" hits="1"/>
4342
<line number="24" hits="1"/>
43+
<line number="25" hits="1"/>
44+
<line number="26" hits="1"/>
45+
<line number="27" hits="1"/>
4446
</lines>
4547
</class>
4648
<class name="errors.py" filename="errors.py" complexity="0" line-rate="1" branch-rate="1">
@@ -171,23 +173,54 @@
171173
</class>
172174
</classes>
173175
</package>
174-
<package name="helpers" line-rate="1" branch-rate="1" complexity="0">
176+
<package name="helpers" line-rate="0.6471" branch-rate="0.375" complexity="0">
175177
<classes>
176178
<class name="__init__.py" filename="helpers/__init__.py" complexity="0" line-rate="1" branch-rate="1">
177179
<methods/>
178180
<lines/>
179181
</class>
180-
<class name="idempontency.py" filename="helpers/idempontency.py" complexity="0" line-rate="1" branch-rate="1">
182+
<class name="idempotency.py" filename="helpers/idempotency.py" complexity="0" line-rate="0.6471" branch-rate="0.375">
181183
<methods/>
182184
<lines>
183185
<line number="3" hits="1"/>
186+
<line number="4" hits="1"/>
187+
<line number="5" hits="1"/>
184188
<line number="6" hits="1"/>
185-
<line number="16" hits="1"/>
189+
<line number="8" hits="1"/>
190+
<line number="10" hits="1"/>
191+
<line number="13" hits="1"/>
192+
<line number="23" hits="1"/>
193+
<line number="26" hits="1"/>
194+
<line number="28" hits="1"/>
195+
<line number="29" hits="1"/>
196+
<line number="32" hits="1"/>
197+
<line number="34" hits="1" branch="true" condition-coverage="100% (2/2)"/>
198+
<line number="35" hits="1"/>
199+
<line number="37" hits="1"/>
200+
<line number="38" hits="1"/>
201+
<line number="39" hits="1" branch="true" condition-coverage="50% (1/2)" missing-branches="44"/>
202+
<line number="40" hits="1"/>
203+
<line number="41" hits="0"/>
204+
<line number="42" hits="0"/>
205+
<line number="44" hits="0"/>
206+
<line number="45" hits="0"/>
207+
<line number="46" hits="0" branch="true" condition-coverage="0% (0/2)" missing-branches="47,53"/>
208+
<line number="47" hits="0" branch="true" condition-coverage="0% (0/2)" missing-branches="48,49"/>
209+
<line number="48" hits="0"/>
210+
<line number="49" hits="0"/>
211+
<line number="50" hits="0"/>
212+
<line number="51" hits="0"/>
213+
<line number="52" hits="0"/>
214+
<line number="53" hits="0"/>
215+
<line number="56" hits="1"/>
216+
<line number="57" hits="1"/>
217+
<line number="60" hits="1"/>
218+
<line number="62" hits="1"/>
186219
</lines>
187220
</class>
188221
</classes>
189222
</package>
190-
<package name="http" line-rate="1" branch-rate="1" complexity="0">
223+
<package name="http" line-rate="0.8529" branch-rate="0.7812" complexity="0">
191224
<classes>
192225
<class name="__init__.py" filename="http/__init__.py" complexity="0" line-rate="1" branch-rate="1">
193226
<methods/>
@@ -234,7 +267,7 @@
234267
<line number="36" hits="1"/>
235268
</lines>
236269
</class>
237-
<class name="transport.py" filename="http/transport.py" complexity="0" line-rate="1" branch-rate="1">
270+
<class name="transport.py" filename="http/transport.py" complexity="0" line-rate="0.8113" branch-rate="0.75">
238271
<methods/>
239272
<lines>
240273
<line number="3" hits="1"/>
@@ -246,56 +279,103 @@
246279
<line number="12" hits="1"/>
247280
<line number="13" hits="1"/>
248281
<line number="14" hits="1"/>
282+
<line number="15" hits="1"/>
249283
<line number="17" hits="1"/>
250284
<line number="18" hits="1"/>
251-
<line number="21" hits="1"/>
252-
<line number="24" hits="1"/>
253-
<line number="31" hits="1"/>
285+
<line number="19" hits="1"/>
286+
<line number="22" hits="1"/>
254287
<line number="32" hits="1"/>
255-
<line number="43" hits="1"/>
256-
<line number="45" hits="1"/>
257288
<line number="47" hits="1"/>
258-
<line number="70" hits="1"/>
259-
<line number="71" hits="1" branch="true" condition-coverage="100% (2/2)"/>
260-
<line number="72" hits="1"/>
261-
<line number="73" hits="1"/>
262-
<line number="74" hits="1"/>
263-
<line number="75" hits="1"/>
264-
<line number="76" hits="1"/>
265-
<line number="77" hits="1"/>
266-
<line number="78" hits="1" branch="true" condition-coverage="100% (2/2)"/>
267-
<line number="79" hits="1"/>
268-
<line number="80" hits="1"/>
269-
<line number="81" hits="1"/>
270-
<line number="82" hits="1"/>
271-
<line number="83" hits="1" branch="true" condition-coverage="100% (2/2)"/>
272-
<line number="84" hits="1"/>
273-
<line number="85" hits="1"/>
274-
<line number="86" hits="1"/>
275-
<line number="87" hits="1" branch="true" condition-coverage="100% (2/2)"/>
289+
<line number="48" hits="1"/>
290+
<line number="49" hits="1"/>
291+
<line number="50" hits="1"/>
292+
<line number="61" hits="1"/>
293+
<line number="63" hits="1"/>
294+
<line number="65" hits="1"/>
295+
<line number="87" hits="1"/>
296+
<line number="88" hits="1"/>
276297
<line number="91" hits="1"/>
277298
<line number="92" hits="1"/>
278-
<line number="97" hits="1"/>
299+
<line number="93" hits="1"/>
300+
<line number="94" hits="1"/>
301+
<line number="96" hits="1"/>
302+
<line number="97" hits="1" branch="true" condition-coverage="100% (2/2)"/>
279303
<line number="98" hits="1"/>
280-
<line number="99" hits="1"/>
281-
<line number="100" hits="1"/>
282-
<line number="102" hits="1"/>
283-
<line number="103" hits="1"/>
284-
<line number="114" hits="1" branch="true" condition-coverage="100% (2/2)"/>
285-
<line number="115" hits="1"/>
304+
<line number="99" hits="1" branch="true" condition-coverage="50% (1/2)" missing-branches="100"/>
305+
<line number="100" hits="0"/>
306+
<line number="101" hits="0"/>
307+
<line number="102" hits="0"/>
308+
<line number="104" hits="0"/>
309+
<line number="106" hits="1"/>
310+
<line number="107" hits="1"/>
311+
<line number="108" hits="1"/>
312+
<line number="109" hits="1"/>
313+
<line number="111" hits="1" branch="true" condition-coverage="100% (2/2)"/>
314+
<line number="112" hits="1"/>
315+
<line number="113" hits="1"/>
286316
<line number="116" hits="1"/>
287317
<line number="117" hits="1"/>
288318
<line number="118" hits="1"/>
289-
<line number="119" hits="1"/>
290-
<line number="120" hits="1"/>
291-
<line number="129" hits="1"/>
292-
<line number="130" hits="1"/>
293-
<line number="131" hits="1"/>
319+
<line number="119" hits="1" branch="true" condition-coverage="50% (1/2)" missing-branches="120"/>
320+
<line number="120" hits="0"/>
321+
<line number="121" hits="1"/>
322+
<line number="124" hits="1" branch="true" condition-coverage="50% (1/2)" missing-branches="125"/>
323+
<line number="125" hits="0"/>
324+
<line number="126" hits="0"/>
325+
<line number="127" hits="0"/>
326+
<line number="128" hits="0"/>
327+
<line number="131" hits="1" branch="true" condition-coverage="100% (2/2)"/>
294328
<line number="132" hits="1"/>
295329
<line number="133" hits="1"/>
296-
<line number="134" hits="1" branch="true" condition-coverage="100% (2/2)"/>
297-
<line number="135" hits="1"/>
330+
<line number="134" hits="0"/>
331+
<line number="135" hits="0"/>
332+
<line number="136" hits="1" branch="true" condition-coverage="50% (1/2)" missing-branches="150"/>
333+
<line number="137" hits="1"/>
298334
<line number="138" hits="1"/>
335+
<line number="139" hits="1"/>
336+
<line number="140" hits="1"/>
337+
<line number="142" hits="1" branch="true" condition-coverage="50% (1/2)" missing-branches="143"/>
338+
<line number="143" hits="0"/>
339+
<line number="144" hits="0"/>
340+
<line number="145" hits="0"/>
341+
<line number="146" hits="0"/>
342+
<line number="147" hits="1"/>
343+
<line number="150" hits="1"/>
344+
<line number="151" hits="1" branch="true" condition-coverage="100% (2/2)"/>
345+
<line number="156" hits="1"/>
346+
<line number="157" hits="1"/>
347+
<line number="165" hits="1" branch="true" condition-coverage="100% (2/2)"/>
348+
<line number="166" hits="1"/>
349+
<line number="167" hits="1"/>
350+
<line number="168" hits="1"/>
351+
<line number="169" hits="1"/>
352+
<line number="170" hits="1"/>
353+
<line number="171" hits="1" branch="true" condition-coverage="50% (1/2)" missing-branches="172"/>
354+
<line number="172" hits="0"/>
355+
<line number="174" hits="1" branch="true" condition-coverage="50% (1/2)" missing-branches="175"/>
356+
<line number="175" hits="0"/>
357+
<line number="176" hits="0"/>
358+
<line number="177" hits="0"/>
359+
<line number="178" hits="0"/>
360+
<line number="179" hits="1"/>
361+
<line number="182" hits="1"/>
362+
<line number="185" hits="1"/>
363+
<line number="186" hits="1"/>
364+
<line number="197" hits="1" branch="true" condition-coverage="100% (2/2)"/>
365+
<line number="198" hits="1"/>
366+
<line number="199" hits="1"/>
367+
<line number="200" hits="1"/>
368+
<line number="201" hits="1"/>
369+
<line number="202" hits="1"/>
370+
<line number="203" hits="1"/>
371+
<line number="212" hits="1"/>
372+
<line number="213" hits="1"/>
373+
<line number="214" hits="1"/>
374+
<line number="215" hits="1"/>
375+
<line number="216" hits="1"/>
376+
<line number="217" hits="1" branch="true" condition-coverage="100% (2/2)"/>
377+
<line number="218" hits="1"/>
378+
<line number="221" hits="1"/>
299379
</lines>
300380
</class>
301381
</classes>

gavaconnect/config.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,16 @@
22

33
from dataclasses import dataclass, field
44

5+
from ._version import __version__
6+
57

68
@dataclass(slots=True)
79
class RetryPolicy:
810
"""Configuration for retry behavior."""
911

1012
max_attempts: int = 3
1113
base_backoff_s: float = 0.2
14+
max_cap_s: float = 60.0 # Maximum cap for backoff delay
1215
retry_on_status: tuple[int, ...] = (429, 500, 502, 503, 504)
1316

1417

@@ -21,4 +24,4 @@ class SDKConfig:
2124
read_timeout_s: float = 30.0
2225
total_timeout_s: float = 40.0
2326
retry: RetryPolicy = field(default_factory=RetryPolicy)
24-
user_agent: str = "gavaconnect-py/1.0.0"
27+
user_agent: str = f"gavaconnect-py/{__version__}"

gavaconnect/helpers/idempotency.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
"""Idempotency utilities for ensuring request uniqueness."""
22

3+
import datetime
34
import uuid
5+
from email.utils import parsedate_to_datetime
6+
from random import SystemRandom
7+
8+
import httpx
9+
10+
_rng = SystemRandom() # cryptographically strong; avoids Bandit B311
411

512

613
def idempotency_headers(key: str | None = None) -> dict[str, str]:
@@ -14,3 +21,43 @@ def idempotency_headers(key: str | None = None) -> dict[str, str]:
1421
1522
"""
1623
return {"idempotency-key": key or str(uuid.uuid4())}
24+
25+
26+
def _full_jitter(base: float, attempt: int, cap: float) -> float:
27+
"""AWS-style full jitter: sleep ~ U(0, min(cap, base*2^attempt))."""
28+
max_sleep = min(cap, base * (2**attempt))
29+
return _rng.uniform(0.0, max_sleep)
30+
31+
32+
def _parse_retry_after(value: str | None) -> float | None:
33+
"""Return seconds to wait from Retry-After which may be seconds or HTTP-date."""
34+
if not value:
35+
return None
36+
# numeric seconds
37+
try:
38+
secs = float(value)
39+
if secs >= 0:
40+
return secs
41+
except ValueError:
42+
pass
43+
# HTTP-date
44+
try:
45+
dt = parsedate_to_datetime(value)
46+
if dt is not None:
47+
if dt.tzinfo is None:
48+
dt = dt.replace(tzinfo=datetime.UTC)
49+
delta = (dt - datetime.datetime.now(datetime.UTC)).total_seconds()
50+
return max(0.0, delta)
51+
except Exception:
52+
return None
53+
54+
55+
def _is_idempotent(method: str) -> bool:
56+
return method.upper() in {"GET", "HEAD", "OPTIONS", "DELETE"}
57+
58+
59+
def _can_retry(method: str, req: httpx.Request) -> bool:
60+
# Allow retries for idempotent methods, or if caller provides Idempotency-Key for writes.
61+
return _is_idempotent(method) or (
62+
"idempotency-key" in (k.lower() for k in req.headers.keys())
63+
)

0 commit comments

Comments
 (0)