Skip to content

Commit edc01ac

Browse files
authored
Merge pull request #17 from acoruss/feat/idempotency
feat: add helper utilities
2 parents 2fed65b + 27a4b28 commit edc01ac

5 files changed

Lines changed: 152 additions & 37 deletions

File tree

.coverage

0 Bytes
Binary file not shown.

coverage.xml

Lines changed: 55 additions & 37 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="1755788392017" lines-valid="190" lines-covered="190" line-rate="1" branches-valid="20" branches-covered="20" branch-rate="1" complexity="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">
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>
@@ -171,6 +171,22 @@
171171
</class>
172172
</classes>
173173
</package>
174+
<package name="helpers" line-rate="1" branch-rate="1" complexity="0">
175+
<classes>
176+
<class name="__init__.py" filename="helpers/__init__.py" complexity="0" line-rate="1" branch-rate="1">
177+
<methods/>
178+
<lines/>
179+
</class>
180+
<class name="idempontency.py" filename="helpers/idempontency.py" complexity="0" line-rate="1" branch-rate="1">
181+
<methods/>
182+
<lines>
183+
<line number="3" hits="1"/>
184+
<line number="6" hits="1"/>
185+
<line number="16" hits="1"/>
186+
</lines>
187+
</class>
188+
</classes>
189+
</package>
174190
<package name="http" line-rate="1" branch-rate="1" complexity="0">
175191
<classes>
176192
<class name="__init__.py" filename="http/__init__.py" complexity="0" line-rate="1" branch-rate="1">
@@ -224,60 +240,62 @@
224240
<line number="3" hits="1"/>
225241
<line number="5" hits="1"/>
226242
<line number="6" hits="1"/>
243+
<line number="7" hits="1"/>
227244
<line number="8" hits="1"/>
228245
<line number="10" hits="1"/>
229-
<line number="11" hits="1"/>
230246
<line number="12" hits="1"/>
231-
<line number="15" hits="1"/>
232-
<line number="16" hits="1"/>
233-
<line number="19" hits="1"/>
234-
<line number="22" hits="1"/>
235-
<line number="29" hits="1"/>
236-
<line number="30" hits="1"/>
237-
<line number="41" hits="1"/>
247+
<line number="13" hits="1"/>
248+
<line number="14" hits="1"/>
249+
<line number="17" hits="1"/>
250+
<line number="18" hits="1"/>
251+
<line number="21" hits="1"/>
252+
<line number="24" hits="1"/>
253+
<line number="31" hits="1"/>
254+
<line number="32" hits="1"/>
238255
<line number="43" hits="1"/>
239256
<line number="45" hits="1"/>
240-
<line number="63" hits="1"/>
241-
<line number="64" hits="1" branch="true" condition-coverage="100% (2/2)"/>
242-
<line number="65" hits="1"/>
243-
<line number="66" hits="1"/>
244-
<line number="67" hits="1"/>
245-
<line number="68" hits="1"/>
246-
<line number="69" hits="1"/>
257+
<line number="47" hits="1"/>
247258
<line number="70" hits="1"/>
248259
<line number="71" hits="1" branch="true" condition-coverage="100% (2/2)"/>
249260
<line number="72" hits="1"/>
250261
<line number="73" hits="1"/>
251262
<line number="74" hits="1"/>
252263
<line number="75" hits="1"/>
253-
<line number="76" hits="1" branch="true" condition-coverage="100% (2/2)"/>
264+
<line number="76" hits="1"/>
254265
<line number="77" hits="1"/>
255-
<line number="78" hits="1"/>
266+
<line number="78" hits="1" branch="true" condition-coverage="100% (2/2)"/>
256267
<line number="79" hits="1"/>
257-
<line number="80" hits="1" branch="true" condition-coverage="100% (2/2)"/>
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)"/>
258272
<line number="84" hits="1"/>
259273
<line number="85" hits="1"/>
260-
<line number="90" hits="1"/>
274+
<line number="86" hits="1"/>
275+
<line number="87" hits="1" branch="true" condition-coverage="100% (2/2)"/>
261276
<line number="91" hits="1"/>
262277
<line number="92" hits="1"/>
263-
<line number="93" hits="1"/>
264-
<line number="95" hits="1"/>
265-
<line number="96" hits="1"/>
266-
<line number="107" hits="1" branch="true" condition-coverage="100% (2/2)"/>
267-
<line number="108" hits="1"/>
268-
<line number="109" hits="1"/>
269-
<line number="110" hits="1"/>
270-
<line number="111" hits="1"/>
271-
<line number="112" hits="1"/>
272-
<line number="113" hits="1"/>
273-
<line number="122" hits="1"/>
274-
<line number="123" hits="1"/>
275-
<line number="124" hits="1"/>
276-
<line number="125" hits="1"/>
277-
<line number="126" hits="1"/>
278-
<line number="127" hits="1" branch="true" condition-coverage="100% (2/2)"/>
279-
<line number="128" hits="1"/>
278+
<line number="97" hits="1"/>
279+
<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"/>
286+
<line number="116" hits="1"/>
287+
<line number="117" hits="1"/>
288+
<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"/>
280293
<line number="131" hits="1"/>
294+
<line number="132" hits="1"/>
295+
<line number="133" hits="1"/>
296+
<line number="134" hits="1" branch="true" condition-coverage="100% (2/2)"/>
297+
<line number="135" hits="1"/>
298+
<line number="138" hits="1"/>
281299
</lines>
282300
</class>
283301
</classes>

gavaconnect/helpers/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""Helper utilities for gavaconnect SDK."""

gavaconnect/helpers/idempotency.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
"""Idempotency utilities for ensuring request uniqueness."""
2+
3+
import uuid
4+
5+
6+
def idempotency_headers(key: str | None = None) -> dict[str, str]:
7+
"""Generate idempotency headers for HTTP requests.
8+
9+
Args:
10+
key: Optional idempotency key. If None, a UUID4 will be generated.
11+
12+
Returns:
13+
A dictionary containing the idempotency-key header.
14+
15+
"""
16+
return {"idempotency-key": key or str(uuid.uuid4())}

tests/test_idempotency.py

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
"""Tests for idempotency helpers."""
2+
3+
import uuid
4+
from unittest.mock import Mock, patch
5+
6+
from gavaconnect.helpers.idempotency import idempotency_headers
7+
8+
9+
class TestIdempotencyHeaders:
10+
"""Test idempotency_headers function."""
11+
12+
def test_with_provided_key(self):
13+
"""Test that provided key is used in headers."""
14+
test_key = "test-key-123"
15+
result = idempotency_headers(key=test_key)
16+
17+
assert result == {"idempotency-key": test_key}
18+
assert isinstance(result, dict)
19+
assert len(result) == 1
20+
21+
def test_with_none_key(self):
22+
"""Test that UUID is generated when key is None."""
23+
result = idempotency_headers(key=None)
24+
25+
assert "idempotency-key" in result
26+
assert isinstance(result["idempotency-key"], str)
27+
# Verify it's a valid UUID format
28+
uuid.UUID(result["idempotency-key"])
29+
30+
def test_with_no_key_parameter(self):
31+
"""Test that UUID is generated when no key parameter is provided."""
32+
result = idempotency_headers()
33+
34+
assert "idempotency-key" in result
35+
assert isinstance(result["idempotency-key"], str)
36+
# Verify it's a valid UUID format
37+
uuid.UUID(result["idempotency-key"])
38+
39+
def test_empty_string_key(self):
40+
"""Test that empty string key generates UUID."""
41+
result = idempotency_headers(key="")
42+
43+
assert "idempotency-key" in result
44+
assert isinstance(result["idempotency-key"], str)
45+
# Verify it's a valid UUID format (empty string is falsy)
46+
uuid.UUID(result["idempotency-key"])
47+
48+
def test_whitespace_key(self):
49+
"""Test that whitespace-only key is preserved."""
50+
test_key = " "
51+
result = idempotency_headers(key=test_key)
52+
53+
assert result == {"idempotency-key": test_key}
54+
55+
@patch("uuid.uuid4")
56+
def test_uuid_generation_called(self, mock_uuid4: Mock) -> None:
57+
"""Test that uuid.uuid4 is called when no key provided."""
58+
mock_uuid = uuid.UUID("12345678-1234-5678-1234-567812345678")
59+
mock_uuid4.return_value = mock_uuid
60+
61+
result = idempotency_headers()
62+
63+
mock_uuid4.assert_called_once()
64+
assert result["idempotency-key"] == str(mock_uuid)
65+
66+
def test_different_calls_generate_different_uuids(self):
67+
"""Test that consecutive calls without keys generate different UUIDs."""
68+
result1 = idempotency_headers()
69+
result2 = idempotency_headers()
70+
71+
assert result1["idempotency-key"] != result2["idempotency-key"]
72+
73+
def test_return_type_annotation(self):
74+
"""Test that return type matches annotation."""
75+
result = idempotency_headers("test")
76+
assert isinstance(result, dict)
77+
78+
for key, value in result.items():
79+
assert isinstance(key, str)
80+
assert isinstance(value, str)

0 commit comments

Comments
 (0)