http-api-tool 1.1.0__tar.gz → 1.1.1__tar.gz
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.
- {http_api_tool-1.1.0 → http_api_tool-1.1.1}/PKG-INFO +3 -3
- {http_api_tool-1.1.0 → http_api_tool-1.1.1}/README.md +2 -2
- {http_api_tool-1.1.0 → http_api_tool-1.1.1}/action.yaml +2 -2
- {http_api_tool-1.1.0 → http_api_tool-1.1.1}/src/http_api_tool/_version.py +2 -2
- {http_api_tool-1.1.0 → http_api_tool-1.1.1}/src/http_api_tool/cli.py +1 -1
- {http_api_tool-1.1.0 → http_api_tool-1.1.1}/src/http_api_tool/verifier.py +2 -2
- {http_api_tool-1.1.0 → http_api_tool-1.1.1}/tests/test_http_api_tool.py +115 -33
- {http_api_tool-1.1.0 → http_api_tool-1.1.1}/.actrc +0 -0
- {http_api_tool-1.1.0 → http_api_tool-1.1.1}/.codespell +0 -0
- {http_api_tool-1.1.0 → http_api_tool-1.1.1}/.dockerignore +0 -0
- {http_api_tool-1.1.0 → http_api_tool-1.1.1}/.editorconfig +0 -0
- {http_api_tool-1.1.0 → http_api_tool-1.1.1}/.github/cache-config.yaml +0 -0
- {http_api_tool-1.1.0 → http_api_tool-1.1.1}/.github/dependabot.yml +0 -0
- {http_api_tool-1.1.0 → http_api_tool-1.1.1}/.github/workflows/build-test-release.yaml +0 -0
- {http_api_tool-1.1.0 → http_api_tool-1.1.1}/.github/workflows/build-test.yaml +0 -0
- {http_api_tool-1.1.0 → http_api_tool-1.1.1}/.github/workflows/clear-action-cache.yaml +0 -0
- {http_api_tool-1.1.0 → http_api_tool-1.1.1}/.github/workflows/codeql.yml +0 -0
- {http_api_tool-1.1.0 → http_api_tool-1.1.1}/.github/workflows/openssf-scorecard.yaml +0 -0
- {http_api_tool-1.1.0 → http_api_tool-1.1.1}/.github/workflows/release-drafter.yaml +0 -0
- {http_api_tool-1.1.0 → http_api_tool-1.1.1}/.github/workflows/security-scans.yaml +0 -0
- {http_api_tool-1.1.0 → http_api_tool-1.1.1}/.gitignore +0 -0
- {http_api_tool-1.1.0 → http_api_tool-1.1.1}/.gitlint +0 -0
- {http_api_tool-1.1.0 → http_api_tool-1.1.1}/.pre-commit-config.yaml +0 -0
- {http_api_tool-1.1.0 → http_api_tool-1.1.1}/.readthedocs.yml +0 -0
- {http_api_tool-1.1.0 → http_api_tool-1.1.1}/.yamllint +0 -0
- {http_api_tool-1.1.0 → http_api_tool-1.1.1}/CHANGELOG-v0.2.0.md +0 -0
- {http_api_tool-1.1.0 → http_api_tool-1.1.1}/LICENSE +0 -0
- {http_api_tool-1.1.0 → http_api_tool-1.1.1}/LICENSES/Apache-2.0.txt +0 -0
- {http_api_tool-1.1.0 → http_api_tool-1.1.1}/REUSE.toml +0 -0
- {http_api_tool-1.1.0 → http_api_tool-1.1.1}/SECURITY.md +0 -0
- {http_api_tool-1.1.0 → http_api_tool-1.1.1}/UV-QUICK-REFERENCE.md +0 -0
- {http_api_tool-1.1.0 → http_api_tool-1.1.1}/docker/Containerfile +0 -0
- {http_api_tool-1.1.0 → http_api_tool-1.1.1}/pyproject.toml +0 -0
- {http_api_tool-1.1.0 → http_api_tool-1.1.1}/scripts/check-pip-security.py +0 -0
- {http_api_tool-1.1.0 → http_api_tool-1.1.1}/scripts/generate_requirements.py +0 -0
- {http_api_tool-1.1.0 → http_api_tool-1.1.1}/sonar-project.properties +0 -0
- {http_api_tool-1.1.0 → http_api_tool-1.1.1}/src/http_api_tool/__init__.py +0 -0
- {http_api_tool-1.1.0 → http_api_tool-1.1.1}/src/http_api_tool/__main__.py +0 -0
- {http_api_tool-1.1.0 → http_api_tool-1.1.1}/src/http_api_tool/curl_client.py +0 -0
- {http_api_tool-1.1.0 → http_api_tool-1.1.1}/src/http_api_tool/reporting.py +0 -0
- {http_api_tool-1.1.0 → http_api_tool-1.1.1}/src/http_api_tool/sanitize.py +0 -0
- {http_api_tool-1.1.0 → http_api_tool-1.1.1}/src/http_api_tool/validation.py +0 -0
- {http_api_tool-1.1.0 → http_api_tool-1.1.1}/uv.lock +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: http-api-tool
|
|
3
|
-
Version: 1.1.
|
|
3
|
+
Version: 1.1.1
|
|
4
4
|
Summary: A Python HTTP/HTTPS API testing tool for GitHub Actions and CLI usage
|
|
5
5
|
Project-URL: Homepage, https://github.com/lfreleng-actions/http-api-tool-docker
|
|
6
6
|
Project-URL: Repository, https://github.com/lfreleng-actions/http-api-tool-docker
|
|
@@ -153,7 +153,7 @@ docker run --rm --entrypoint=/usr/local/bin/uv \
|
|
|
153
153
|
- **Supported HTTP Methods**: Supports GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS
|
|
154
154
|
- **Authentication**: Basic Auth via auth_string and custom headers support
|
|
155
155
|
- **Response Validation**: Status code, regex pattern matching, response time limits
|
|
156
|
-
- **Retry Logic**:
|
|
156
|
+
- **Retry Logic**: Exponential backoff, doubling up to a configurable cap
|
|
157
157
|
- **Dual Usage**: Works as both a CLI tool and GitHub Action
|
|
158
158
|
- **Robust Error Handling**: Detailed error messages and proper exit codes
|
|
159
159
|
- **JSON Safety**: Uses pycurl instead of shell commands to avoid escaping issues
|
|
@@ -271,7 +271,7 @@ uv run python -m http_api_tool test \
|
|
|
271
271
|
| `url` | URL of API server/interface to check | Yes | - |
|
|
272
272
|
| `auth_string` | Authentication string, colon separated username/password | No | - |
|
|
273
273
|
| `service_name` | Name of HTTP/HTTPS API service tested | No | `API Service` |
|
|
274
|
-
| `initial_sleep_time` |
|
|
274
|
+
| `initial_sleep_time` | Delay in seconds before the first retry, doubling thereafter | No | `1` |
|
|
275
275
|
| `max_delay` | Max delay in seconds between retries | No | `30` |
|
|
276
276
|
| `retries` | Number of retries before declaring service unavailable | No | `3` |
|
|
277
277
|
| `expected_http_code` | HTTP response code to accept from the API service | No | `200` |
|
|
@@ -100,7 +100,7 @@ docker run --rm --entrypoint=/usr/local/bin/uv \
|
|
|
100
100
|
- **Supported HTTP Methods**: Supports GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS
|
|
101
101
|
- **Authentication**: Basic Auth via auth_string and custom headers support
|
|
102
102
|
- **Response Validation**: Status code, regex pattern matching, response time limits
|
|
103
|
-
- **Retry Logic**:
|
|
103
|
+
- **Retry Logic**: Exponential backoff, doubling up to a configurable cap
|
|
104
104
|
- **Dual Usage**: Works as both a CLI tool and GitHub Action
|
|
105
105
|
- **Robust Error Handling**: Detailed error messages and proper exit codes
|
|
106
106
|
- **JSON Safety**: Uses pycurl instead of shell commands to avoid escaping issues
|
|
@@ -218,7 +218,7 @@ uv run python -m http_api_tool test \
|
|
|
218
218
|
| `url` | URL of API server/interface to check | Yes | - |
|
|
219
219
|
| `auth_string` | Authentication string, colon separated username/password | No | - |
|
|
220
220
|
| `service_name` | Name of HTTP/HTTPS API service tested | No | `API Service` |
|
|
221
|
-
| `initial_sleep_time` |
|
|
221
|
+
| `initial_sleep_time` | Delay in seconds before the first retry, doubling thereafter | No | `1` |
|
|
222
222
|
| `max_delay` | Max delay in seconds between retries | No | `30` |
|
|
223
223
|
| `retries` | Number of retries before declaring service unavailable | No | `3` |
|
|
224
224
|
| `expected_http_code` | HTTP response code to accept from the API service | No | `200` |
|
|
@@ -39,9 +39,9 @@ inputs:
|
|
|
39
39
|
required: false
|
|
40
40
|
default: 'API Service'
|
|
41
41
|
initial_sleep_time:
|
|
42
|
-
description: '
|
|
42
|
+
description: 'Delay in seconds before the first retry, doubling thereafter'
|
|
43
43
|
required: false
|
|
44
|
-
#
|
|
44
|
+
# Wait a second before the first retry
|
|
45
45
|
default: '1'
|
|
46
46
|
max_delay:
|
|
47
47
|
description: 'Maximum delay in seconds between retries'
|
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '1.1.
|
|
22
|
-
__version_tuple__ = version_tuple = (1, 1,
|
|
21
|
+
__version__ = version = '1.1.1'
|
|
22
|
+
__version_tuple__ = version_tuple = (1, 1, 1)
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -113,7 +113,7 @@ def verify(
|
|
|
113
113
|
"API Service", help="Name of HTTP/HTTPS API service tested"
|
|
114
114
|
),
|
|
115
115
|
initial_sleep_time: int = typer.Option(
|
|
116
|
-
1, help="
|
|
116
|
+
1, help="Delay in seconds before the first retry, doubling thereafter"
|
|
117
117
|
),
|
|
118
118
|
max_delay: int = typer.Option(30, help="Maximum delay in seconds between retries"),
|
|
119
119
|
retries: int = typer.Option(
|
|
@@ -235,7 +235,7 @@ class HTTPAPITester:
|
|
|
235
235
|
result = _initial_result()
|
|
236
236
|
counter = 0
|
|
237
237
|
time_delay = 0
|
|
238
|
-
sleep_time = config["initial_sleep_time"]
|
|
238
|
+
sleep_time = min(config["initial_sleep_time"], config["max_delay"])
|
|
239
239
|
|
|
240
240
|
while True:
|
|
241
241
|
counter += 1
|
|
@@ -267,5 +267,5 @@ class HTTPAPITester:
|
|
|
267
267
|
self.reporter.log(f"Waiting for {sleep_time} seconds before retrying...")
|
|
268
268
|
time.sleep(sleep_time)
|
|
269
269
|
time_delay += sleep_time
|
|
270
|
-
sleep_time = min(sleep_time *
|
|
270
|
+
sleep_time = min(sleep_time * 2, config["max_delay"])
|
|
271
271
|
self.reporter.log(f"Sleep/wait time for next attempt: {sleep_time} seconds")
|
|
@@ -625,6 +625,50 @@ X-Custom-Header: test-value
|
|
|
625
625
|
assert result["curl_error"] == (7, "Failed to connect to host")
|
|
626
626
|
|
|
627
627
|
|
|
628
|
+
def failed_response() -> dict[str, Any]:
|
|
629
|
+
"""Build a perform_request result representing a connection failure."""
|
|
630
|
+
return {
|
|
631
|
+
"success": False,
|
|
632
|
+
"http_code": 0,
|
|
633
|
+
"total_time": 0,
|
|
634
|
+
"connect_time": 0,
|
|
635
|
+
"body_size": 0,
|
|
636
|
+
"header_size": 0,
|
|
637
|
+
"response_body": b"",
|
|
638
|
+
"response_headers": "",
|
|
639
|
+
"header_json": "{}",
|
|
640
|
+
"curl_error": (7, "Failed to connect"),
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
|
|
644
|
+
def retry_config(**overrides: Any) -> dict[str, Any]:
|
|
645
|
+
"""Build a test_api config, applying any per-test overrides."""
|
|
646
|
+
config: dict[str, Any] = {
|
|
647
|
+
"url": "https://example.com",
|
|
648
|
+
"service_name": "Test API",
|
|
649
|
+
"retries": 2,
|
|
650
|
+
"expected_http_code": 200,
|
|
651
|
+
"initial_sleep_time": 1,
|
|
652
|
+
"max_delay": 30,
|
|
653
|
+
"curl_timeout": 5,
|
|
654
|
+
"http_method": "GET",
|
|
655
|
+
"verify_ssl": True,
|
|
656
|
+
"connection_reuse": True,
|
|
657
|
+
"follow_redirects": True,
|
|
658
|
+
"debug": False,
|
|
659
|
+
"include_response_body": False,
|
|
660
|
+
"max_response_time": 0,
|
|
661
|
+
"fail_on_timeout": False,
|
|
662
|
+
}
|
|
663
|
+
config.update(overrides)
|
|
664
|
+
return config
|
|
665
|
+
|
|
666
|
+
|
|
667
|
+
def recorded_sleeps(mock_sleep: Mock) -> list[float]:
|
|
668
|
+
"""Extract the interval passed to each time.sleep call, in order."""
|
|
669
|
+
return [call.args[0] for call in mock_sleep.call_args_list]
|
|
670
|
+
|
|
671
|
+
|
|
628
672
|
@pytest.mark.integration
|
|
629
673
|
class TestIntegration:
|
|
630
674
|
"""Integration tests for the complete verification flow.
|
|
@@ -857,45 +901,83 @@ class TestIntegration:
|
|
|
857
901
|
self, _mock_sleep: Mock, mock_perform: Mock, mock_create_handle: Mock
|
|
858
902
|
) -> None:
|
|
859
903
|
"""Test API verification with exhausted retries."""
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
mock_create_handle.return_value = mock_curl
|
|
904
|
+
mock_create_handle.return_value = Mock()
|
|
905
|
+
mock_perform.return_value = failed_response()
|
|
863
906
|
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
"success": False,
|
|
867
|
-
"http_code": 0,
|
|
868
|
-
"total_time": 0,
|
|
869
|
-
"connect_time": 0,
|
|
870
|
-
"body_size": 0,
|
|
871
|
-
"header_size": 0,
|
|
872
|
-
"response_body": b"",
|
|
873
|
-
"response_headers": "",
|
|
874
|
-
"header_json": "{}",
|
|
875
|
-
"curl_error": (7, "Failed to connect"),
|
|
876
|
-
}
|
|
907
|
+
with pytest.raises(SystemExit):
|
|
908
|
+
_ = self.verifier.test_api(**retry_config(retries=2))
|
|
877
909
|
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
"verify_ssl": True,
|
|
888
|
-
"connection_reuse": True,
|
|
889
|
-
"follow_redirects": True,
|
|
890
|
-
"debug": False,
|
|
891
|
-
"include_response_body": False,
|
|
892
|
-
"max_response_time": 0,
|
|
893
|
-
"fail_on_timeout": False,
|
|
894
|
-
}
|
|
910
|
+
@patch("http_api_tool.verifier.create_curl_handle")
|
|
911
|
+
@patch("http_api_tool.verifier.perform_request")
|
|
912
|
+
@patch("time.sleep")
|
|
913
|
+
def test_test_api_backoff_doubles_each_attempt(
|
|
914
|
+
self, mock_sleep: Mock, mock_perform: Mock, mock_create_handle: Mock
|
|
915
|
+
) -> None:
|
|
916
|
+
"""Test that the retry interval doubles between attempts."""
|
|
917
|
+
mock_create_handle.return_value = Mock()
|
|
918
|
+
mock_perform.return_value = failed_response()
|
|
895
919
|
|
|
920
|
+
config = retry_config(retries=5, initial_sleep_time=1, max_delay=30)
|
|
896
921
|
with pytest.raises(SystemExit):
|
|
897
922
|
_ = self.verifier.test_api(**config)
|
|
898
923
|
|
|
924
|
+
# Five attempts means four waits, each twice the one before it.
|
|
925
|
+
assert recorded_sleeps(mock_sleep) == [1, 2, 4, 8]
|
|
926
|
+
|
|
927
|
+
@patch("http_api_tool.verifier.create_curl_handle")
|
|
928
|
+
@patch("http_api_tool.verifier.perform_request")
|
|
929
|
+
@patch("time.sleep")
|
|
930
|
+
def test_test_api_backoff_stops_at_max_delay(
|
|
931
|
+
self, mock_sleep: Mock, mock_perform: Mock, mock_create_handle: Mock
|
|
932
|
+
) -> None:
|
|
933
|
+
"""Test that the retry interval never exceeds max_delay."""
|
|
934
|
+
mock_create_handle.return_value = Mock()
|
|
935
|
+
mock_perform.return_value = failed_response()
|
|
936
|
+
|
|
937
|
+
config = retry_config(retries=4, initial_sleep_time=10, max_delay=15)
|
|
938
|
+
with pytest.raises(SystemExit):
|
|
939
|
+
_ = self.verifier.test_api(**config)
|
|
940
|
+
|
|
941
|
+
assert recorded_sleeps(mock_sleep) == [10, 15, 15]
|
|
942
|
+
|
|
943
|
+
@patch("http_api_tool.verifier.create_curl_handle")
|
|
944
|
+
@patch("http_api_tool.verifier.perform_request")
|
|
945
|
+
@patch("time.sleep")
|
|
946
|
+
def test_test_api_backoff_caps_the_first_wait(
|
|
947
|
+
self, mock_sleep: Mock, mock_perform: Mock, mock_create_handle: Mock
|
|
948
|
+
) -> None:
|
|
949
|
+
"""Test that max_delay caps initial_sleep_time as well."""
|
|
950
|
+
mock_create_handle.return_value = Mock()
|
|
951
|
+
mock_perform.return_value = failed_response()
|
|
952
|
+
|
|
953
|
+
config = retry_config(retries=3, initial_sleep_time=60, max_delay=30)
|
|
954
|
+
with pytest.raises(SystemExit):
|
|
955
|
+
_ = self.verifier.test_api(**config)
|
|
956
|
+
|
|
957
|
+
assert recorded_sleeps(mock_sleep) == [30, 30]
|
|
958
|
+
|
|
959
|
+
@patch("http_api_tool.verifier.create_curl_handle")
|
|
960
|
+
@patch("http_api_tool.verifier.perform_request")
|
|
961
|
+
@patch("time.sleep")
|
|
962
|
+
def test_test_api_reports_total_delay_on_failure(
|
|
963
|
+
self, _mock_sleep: Mock, mock_perform: Mock, mock_create_handle: Mock
|
|
964
|
+
) -> None:
|
|
965
|
+
"""Test that time_delay accumulates every backoff interval."""
|
|
966
|
+
mock_create_handle.return_value = Mock()
|
|
967
|
+
mock_perform.return_value = failed_response()
|
|
968
|
+
recorded: dict[str, Any] = {}
|
|
969
|
+
|
|
970
|
+
def capture(result: dict[str, Any], *_args: Any) -> None:
|
|
971
|
+
recorded.update(result)
|
|
972
|
+
|
|
973
|
+
config = retry_config(retries=4, initial_sleep_time=1, max_delay=30)
|
|
974
|
+
with patch.object(HTTPAPITester, "_report_exhausted", side_effect=capture):
|
|
975
|
+
with pytest.raises(SystemExit):
|
|
976
|
+
_ = self.verifier.test_api(**config)
|
|
977
|
+
|
|
978
|
+
# Waits of 1, 2, and 4 seconds precede the fourth and final attempt.
|
|
979
|
+
assert recorded["time_delay"] == 7
|
|
980
|
+
|
|
899
981
|
# NOTE: Response time testing is now covered by the testing.yaml workflow
|
|
900
982
|
# using a local go-httpbin service to avoid external dependencies in unit tests.
|
|
901
983
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|