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.
Files changed (43) hide show
  1. {http_api_tool-1.1.0 → http_api_tool-1.1.1}/PKG-INFO +3 -3
  2. {http_api_tool-1.1.0 → http_api_tool-1.1.1}/README.md +2 -2
  3. {http_api_tool-1.1.0 → http_api_tool-1.1.1}/action.yaml +2 -2
  4. {http_api_tool-1.1.0 → http_api_tool-1.1.1}/src/http_api_tool/_version.py +2 -2
  5. {http_api_tool-1.1.0 → http_api_tool-1.1.1}/src/http_api_tool/cli.py +1 -1
  6. {http_api_tool-1.1.0 → http_api_tool-1.1.1}/src/http_api_tool/verifier.py +2 -2
  7. {http_api_tool-1.1.0 → http_api_tool-1.1.1}/tests/test_http_api_tool.py +115 -33
  8. {http_api_tool-1.1.0 → http_api_tool-1.1.1}/.actrc +0 -0
  9. {http_api_tool-1.1.0 → http_api_tool-1.1.1}/.codespell +0 -0
  10. {http_api_tool-1.1.0 → http_api_tool-1.1.1}/.dockerignore +0 -0
  11. {http_api_tool-1.1.0 → http_api_tool-1.1.1}/.editorconfig +0 -0
  12. {http_api_tool-1.1.0 → http_api_tool-1.1.1}/.github/cache-config.yaml +0 -0
  13. {http_api_tool-1.1.0 → http_api_tool-1.1.1}/.github/dependabot.yml +0 -0
  14. {http_api_tool-1.1.0 → http_api_tool-1.1.1}/.github/workflows/build-test-release.yaml +0 -0
  15. {http_api_tool-1.1.0 → http_api_tool-1.1.1}/.github/workflows/build-test.yaml +0 -0
  16. {http_api_tool-1.1.0 → http_api_tool-1.1.1}/.github/workflows/clear-action-cache.yaml +0 -0
  17. {http_api_tool-1.1.0 → http_api_tool-1.1.1}/.github/workflows/codeql.yml +0 -0
  18. {http_api_tool-1.1.0 → http_api_tool-1.1.1}/.github/workflows/openssf-scorecard.yaml +0 -0
  19. {http_api_tool-1.1.0 → http_api_tool-1.1.1}/.github/workflows/release-drafter.yaml +0 -0
  20. {http_api_tool-1.1.0 → http_api_tool-1.1.1}/.github/workflows/security-scans.yaml +0 -0
  21. {http_api_tool-1.1.0 → http_api_tool-1.1.1}/.gitignore +0 -0
  22. {http_api_tool-1.1.0 → http_api_tool-1.1.1}/.gitlint +0 -0
  23. {http_api_tool-1.1.0 → http_api_tool-1.1.1}/.pre-commit-config.yaml +0 -0
  24. {http_api_tool-1.1.0 → http_api_tool-1.1.1}/.readthedocs.yml +0 -0
  25. {http_api_tool-1.1.0 → http_api_tool-1.1.1}/.yamllint +0 -0
  26. {http_api_tool-1.1.0 → http_api_tool-1.1.1}/CHANGELOG-v0.2.0.md +0 -0
  27. {http_api_tool-1.1.0 → http_api_tool-1.1.1}/LICENSE +0 -0
  28. {http_api_tool-1.1.0 → http_api_tool-1.1.1}/LICENSES/Apache-2.0.txt +0 -0
  29. {http_api_tool-1.1.0 → http_api_tool-1.1.1}/REUSE.toml +0 -0
  30. {http_api_tool-1.1.0 → http_api_tool-1.1.1}/SECURITY.md +0 -0
  31. {http_api_tool-1.1.0 → http_api_tool-1.1.1}/UV-QUICK-REFERENCE.md +0 -0
  32. {http_api_tool-1.1.0 → http_api_tool-1.1.1}/docker/Containerfile +0 -0
  33. {http_api_tool-1.1.0 → http_api_tool-1.1.1}/pyproject.toml +0 -0
  34. {http_api_tool-1.1.0 → http_api_tool-1.1.1}/scripts/check-pip-security.py +0 -0
  35. {http_api_tool-1.1.0 → http_api_tool-1.1.1}/scripts/generate_requirements.py +0 -0
  36. {http_api_tool-1.1.0 → http_api_tool-1.1.1}/sonar-project.properties +0 -0
  37. {http_api_tool-1.1.0 → http_api_tool-1.1.1}/src/http_api_tool/__init__.py +0 -0
  38. {http_api_tool-1.1.0 → http_api_tool-1.1.1}/src/http_api_tool/__main__.py +0 -0
  39. {http_api_tool-1.1.0 → http_api_tool-1.1.1}/src/http_api_tool/curl_client.py +0 -0
  40. {http_api_tool-1.1.0 → http_api_tool-1.1.1}/src/http_api_tool/reporting.py +0 -0
  41. {http_api_tool-1.1.0 → http_api_tool-1.1.1}/src/http_api_tool/sanitize.py +0 -0
  42. {http_api_tool-1.1.0 → http_api_tool-1.1.1}/src/http_api_tool/validation.py +0 -0
  43. {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.0
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**: Configurable exponential backoff with jitter
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` | Time in seconds between API service connection attempts | No | `1` |
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**: Configurable exponential backoff with jitter
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` | Time in seconds between API service connection attempts | No | `1` |
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: 'Time in seconds between API service connection attempts'
42
+ description: 'Delay in seconds before the first retry, doubling thereafter'
43
43
  required: false
44
- # Try once every second
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.0'
22
- __version_tuple__ = version_tuple = (1, 1, 0)
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="Time in seconds between API service connection attempts"
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 * (2 ** (counter - 1)), config["max_delay"])
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
- # Mock curl handle
861
- mock_curl = Mock()
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
- # Mock consistent failures
865
- mock_perform.return_value = {
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
- config = {
879
- "url": "https://example.com",
880
- "service_name": "Test API",
881
- "retries": 2,
882
- "expected_http_code": 200,
883
- "initial_sleep_time": 1,
884
- "max_delay": 30,
885
- "curl_timeout": 5,
886
- "http_method": "GET",
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