Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified .coverage
Binary file not shown.
92 changes: 55 additions & 37 deletions coverage.xml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<?xml version="1.0" ?>
<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">
<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">
<!-- Generated by coverage.py: https://coverage.readthedocs.io/en/7.10.4 -->
<!-- Based on https://raw.githubusercontent.com/cobertura/web/master/htdocs/xml/coverage-04.dtd -->
<sources>
Expand Down Expand Up @@ -171,6 +171,22 @@
</class>
</classes>
</package>
<package name="helpers" line-rate="1" branch-rate="1" complexity="0">
<classes>
<class name="__init__.py" filename="helpers/__init__.py" complexity="0" line-rate="1" branch-rate="1">
<methods/>
<lines/>
</class>
<class name="idempontency.py" filename="helpers/idempontency.py" complexity="0" line-rate="1" branch-rate="1">
<methods/>
<lines>
<line number="3" hits="1"/>
<line number="6" hits="1"/>
<line number="16" hits="1"/>
</lines>
</class>
</classes>
</package>
<package name="http" line-rate="1" branch-rate="1" complexity="0">
<classes>
<class name="__init__.py" filename="http/__init__.py" complexity="0" line-rate="1" branch-rate="1">
Expand Down Expand Up @@ -224,60 +240,62 @@
<line number="3" hits="1"/>
<line number="5" hits="1"/>
<line number="6" hits="1"/>
<line number="7" hits="1"/>
<line number="8" hits="1"/>
<line number="10" hits="1"/>
<line number="11" hits="1"/>
<line number="12" hits="1"/>
<line number="15" hits="1"/>
<line number="16" hits="1"/>
<line number="19" hits="1"/>
<line number="22" hits="1"/>
<line number="29" hits="1"/>
<line number="30" hits="1"/>
<line number="41" hits="1"/>
<line number="13" hits="1"/>
<line number="14" hits="1"/>
<line number="17" hits="1"/>
<line number="18" hits="1"/>
<line number="21" hits="1"/>
<line number="24" hits="1"/>
<line number="31" hits="1"/>
<line number="32" hits="1"/>
<line number="43" hits="1"/>
<line number="45" hits="1"/>
<line number="63" hits="1"/>
<line number="64" hits="1" branch="true" condition-coverage="100% (2/2)"/>
<line number="65" hits="1"/>
<line number="66" hits="1"/>
<line number="67" hits="1"/>
<line number="68" hits="1"/>
<line number="69" hits="1"/>
<line number="47" hits="1"/>
<line number="70" hits="1"/>
<line number="71" hits="1" branch="true" condition-coverage="100% (2/2)"/>
<line number="72" hits="1"/>
<line number="73" hits="1"/>
<line number="74" hits="1"/>
<line number="75" hits="1"/>
<line number="76" hits="1" branch="true" condition-coverage="100% (2/2)"/>
<line number="76" hits="1"/>
<line number="77" hits="1"/>
<line number="78" hits="1"/>
<line number="78" hits="1" branch="true" condition-coverage="100% (2/2)"/>
<line number="79" hits="1"/>
<line number="80" hits="1" branch="true" condition-coverage="100% (2/2)"/>
<line number="80" hits="1"/>
<line number="81" hits="1"/>
<line number="82" hits="1"/>
<line number="83" hits="1" branch="true" condition-coverage="100% (2/2)"/>
<line number="84" hits="1"/>
<line number="85" hits="1"/>
<line number="90" hits="1"/>
<line number="86" hits="1"/>
<line number="87" hits="1" branch="true" condition-coverage="100% (2/2)"/>
<line number="91" hits="1"/>
<line number="92" hits="1"/>
<line number="93" hits="1"/>
<line number="95" hits="1"/>
<line number="96" hits="1"/>
<line number="107" hits="1" branch="true" condition-coverage="100% (2/2)"/>
<line number="108" hits="1"/>
<line number="109" hits="1"/>
<line number="110" hits="1"/>
<line number="111" hits="1"/>
<line number="112" hits="1"/>
<line number="113" hits="1"/>
<line number="122" hits="1"/>
<line number="123" hits="1"/>
<line number="124" hits="1"/>
<line number="125" hits="1"/>
<line number="126" hits="1"/>
<line number="127" hits="1" branch="true" condition-coverage="100% (2/2)"/>
<line number="128" hits="1"/>
<line number="97" hits="1"/>
<line number="98" hits="1"/>
<line number="99" hits="1"/>
<line number="100" hits="1"/>
<line number="102" hits="1"/>
<line number="103" hits="1"/>
<line number="114" hits="1" branch="true" condition-coverage="100% (2/2)"/>
<line number="115" hits="1"/>
<line number="116" hits="1"/>
<line number="117" hits="1"/>
<line number="118" hits="1"/>
<line number="119" hits="1"/>
<line number="120" hits="1"/>
<line number="129" hits="1"/>
<line number="130" hits="1"/>
<line number="131" hits="1"/>
<line number="132" hits="1"/>
<line number="133" hits="1"/>
<line number="134" hits="1" branch="true" condition-coverage="100% (2/2)"/>
<line number="135" hits="1"/>
<line number="138" hits="1"/>
</lines>
</class>
</classes>
Expand Down
1 change: 1 addition & 0 deletions gavaconnect/helpers/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Helper utilities for gavaconnect SDK."""
16 changes: 16 additions & 0 deletions gavaconnect/helpers/idempotency.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
"""Idempotency utilities for ensuring request uniqueness."""

import uuid


def idempotency_headers(key: str | None = None) -> dict[str, str]:
"""Generate idempotency headers for HTTP requests.

Args:
key: Optional idempotency key. If None, a UUID4 will be generated.

Returns:
A dictionary containing the idempotency-key header.

"""
return {"idempotency-key": key or str(uuid.uuid4())}
80 changes: 80 additions & 0 deletions tests/test_idempotency.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
"""Tests for idempotency helpers."""

import uuid
from unittest.mock import Mock, patch

from gavaconnect.helpers.idempotency import idempotency_headers


class TestIdempotencyHeaders:
"""Test idempotency_headers function."""

def test_with_provided_key(self):
"""Test that provided key is used in headers."""
test_key = "test-key-123"
result = idempotency_headers(key=test_key)

assert result == {"idempotency-key": test_key}
assert isinstance(result, dict)
assert len(result) == 1

def test_with_none_key(self):
"""Test that UUID is generated when key is None."""
result = idempotency_headers(key=None)

assert "idempotency-key" in result
assert isinstance(result["idempotency-key"], str)
# Verify it's a valid UUID format
uuid.UUID(result["idempotency-key"])

def test_with_no_key_parameter(self):
"""Test that UUID is generated when no key parameter is provided."""
result = idempotency_headers()

assert "idempotency-key" in result
assert isinstance(result["idempotency-key"], str)
# Verify it's a valid UUID format
uuid.UUID(result["idempotency-key"])

def test_empty_string_key(self):
"""Test that empty string key generates UUID."""
result = idempotency_headers(key="")

assert "idempotency-key" in result
assert isinstance(result["idempotency-key"], str)
# Verify it's a valid UUID format (empty string is falsy)
uuid.UUID(result["idempotency-key"])

def test_whitespace_key(self):
"""Test that whitespace-only key is preserved."""
test_key = " "
result = idempotency_headers(key=test_key)

assert result == {"idempotency-key": test_key}

@patch("uuid.uuid4")
def test_uuid_generation_called(self, mock_uuid4: Mock) -> None:
"""Test that uuid.uuid4 is called when no key provided."""
mock_uuid = uuid.UUID("12345678-1234-5678-1234-567812345678")
mock_uuid4.return_value = mock_uuid

result = idempotency_headers()

mock_uuid4.assert_called_once()
assert result["idempotency-key"] == str(mock_uuid)

def test_different_calls_generate_different_uuids(self):
"""Test that consecutive calls without keys generate different UUIDs."""
result1 = idempotency_headers()
result2 = idempotency_headers()

assert result1["idempotency-key"] != result2["idempotency-key"]

def test_return_type_annotation(self):
"""Test that return type matches annotation."""
result = idempotency_headers("test")
assert isinstance(result, dict)

for key, value in result.items():
assert isinstance(key, str)
assert isinstance(value, str)
Loading