http-api-tool 0.1.1__py3-none-any.whl

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.
@@ -0,0 +1,17 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ # SPDX-FileCopyrightText: 2025 The Linux Foundation
3
+
4
+ """
5
+ HTTP API Test Tool - A Python tool for HTTP/HTTPS API testing.
6
+
7
+ This package can be used both as a CLI tool (using Typer) and as a GitHub Action.
8
+ It uses pycurl for HTTP requests to avoid the shell escaping issues of the
9
+ original implementation.
10
+ """
11
+
12
+ __version__ = "0.1.1"
13
+
14
+ from .cli import app, main
15
+ from .verifier import HTTPAPITester
16
+
17
+ __all__ = ["HTTPAPITester", "app", "main", "__version__"]
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env python3
2
+
3
+ # SPDX-License-Identifier: Apache-2.0
4
+ # SPDX-FileCopyrightText: 2025 The Linux Foundation
5
+
6
+ """
7
+ Entry point script for http-api-tool.
8
+
9
+ This script provides the main entry point that can be used both as a CLI tool
10
+ and as a GitHub Action.
11
+ """
12
+
13
+ from http_api_tool import main
14
+
15
+ if __name__ == "__main__":
16
+ main()
http_api_tool/cli.py ADDED
@@ -0,0 +1,337 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ # SPDX-FileCopyrightText: 2025 The Linux Foundation
3
+
4
+ """
5
+ CLI interface for HTTP API testing.
6
+
7
+ This module provides the command-line interface using Typer for both standalone
8
+ CLI usage and GitHub Actions integration.
9
+ """
10
+
11
+ import os
12
+ import sys
13
+ import subprocess
14
+ from typing import Any, Dict, Optional
15
+ from urllib.parse import urlparse, urlunparse
16
+
17
+ import typer
18
+
19
+ from .verifier import HTTPAPITester
20
+
21
+ app = typer.Typer(help="A Python tool to test HTTP API services.")
22
+
23
+
24
+ def _get_docker_host_gateway() -> Optional[str]:
25
+ """Get the Docker host gateway IP that containers can use to reach the host."""
26
+ try:
27
+ # Try to get the gateway IP from the container's route table
28
+ result = subprocess.run(
29
+ ["sh", "-c", "ip route | grep '^default' | cut -d' ' -f3"],
30
+ capture_output=True,
31
+ text=True,
32
+ timeout=5,
33
+ )
34
+ if result.returncode == 0 and result.stdout.strip():
35
+ gateway_ip = result.stdout.strip()
36
+ if gateway_ip and gateway_ip != "localhost" and gateway_ip != "127.0.0.1":
37
+ return gateway_ip
38
+ except (subprocess.TimeoutExpired, FileNotFoundError):
39
+ pass
40
+ return None
41
+
42
+
43
+ def _transform_localhost_url(url: str) -> str:
44
+ """Transform localhost URLs to use Docker host gateway when running in a container."""
45
+ # Only transform if we're in a containerized environment (GitHub Actions)
46
+ if not os.environ.get("GITHUB_ACTIONS"):
47
+ return url
48
+
49
+ parsed = urlparse(url)
50
+ if parsed.hostname in ["localhost", "127.0.0.1"]:
51
+ gateway_ip = _get_docker_host_gateway()
52
+ if gateway_ip:
53
+ # Replace the hostname with the gateway IP
54
+ new_netloc = parsed.netloc.replace(parsed.hostname, gateway_ip)
55
+ new_parsed = parsed._replace(netloc=new_netloc)
56
+ transformed_url = urlunparse(new_parsed)
57
+ return transformed_url
58
+
59
+ return url
60
+
61
+
62
+ @app.callback() # type: ignore[misc]
63
+ def main_callback() -> None:
64
+ """
65
+ HTTP server/API testing tool
66
+
67
+ This script can be used both as a CLI tool (using Typer) and as a GitHub Action.
68
+ It uses pycurl for HTTP requests to avoid the shell escaping issues of the
69
+ original implementation.
70
+ """
71
+ pass
72
+
73
+
74
+ @app.command("test") # type: ignore[misc]
75
+ def verify(
76
+ url: str = typer.Option(..., help="URL of API server/interface to check"),
77
+ auth_string: Optional[str] = typer.Option(
78
+ None, help="Authentication string, colon separated username/password"
79
+ ),
80
+ service_name: str = typer.Option(
81
+ "API Service", help="Name of HTTP/HTTPS API service tested"
82
+ ),
83
+ initial_sleep_time: int = typer.Option(
84
+ 1, help="Time in seconds between API service connection attempts"
85
+ ),
86
+ max_delay: int = typer.Option(30, help="Maximum delay in seconds between retries"),
87
+ retries: int = typer.Option(
88
+ 3, help="Number of retries before declaring service unavailable"
89
+ ),
90
+ expected_http_code: int = typer.Option(
91
+ 200, help="HTTP response code to accept from the API service"
92
+ ),
93
+ regex: Optional[str] = typer.Option(
94
+ None, help="Verify server response with regular expression"
95
+ ),
96
+ show_header_json: bool = typer.Option(
97
+ False, help="Display response header as JSON in action output"
98
+ ),
99
+ curl_timeout: int = typer.Option(
100
+ 5, help="Maximum time in seconds for cURL to wait for a response"
101
+ ),
102
+ http_method: str = typer.Option(
103
+ "GET", help="HTTP method to use (GET, POST, PUT, etc.)"
104
+ ),
105
+ request_body: Optional[str] = typer.Option(
106
+ None, help="Data to send with POST/PUT/PATCH requests"
107
+ ),
108
+ content_type: str = typer.Option(
109
+ "application/json", help="Content type of the request body"
110
+ ),
111
+ request_headers: Optional[str] = typer.Option(
112
+ None, help="Custom HTTP headers sent in JSON format"
113
+ ),
114
+ verify_ssl: bool = typer.Option(True, help="Verify SSL certificates"),
115
+ ca_bundle_path: Optional[str] = typer.Option(
116
+ None, help="Path to CA bundle file for SSL verification"
117
+ ),
118
+ include_response_body: bool = typer.Option(
119
+ False, help="Include response body in outputs (base64 encoded)"
120
+ ),
121
+ follow_redirects: bool = typer.Option(True, help="Follow HTTP redirects"),
122
+ max_response_time: float = typer.Option(
123
+ 0, help="Maximum acceptable response time in seconds"
124
+ ),
125
+ connection_reuse: bool = typer.Option(
126
+ True, help="Reuse connections between requests"
127
+ ),
128
+ debug: bool = typer.Option(False, help="Enables debugging output"),
129
+ fail_on_timeout: bool = typer.Option(
130
+ False, help="Fail the action if response time exceeds max_response_time"
131
+ ),
132
+ ) -> None:
133
+ """Test HTTP API endpoint testing with retry logic."""
134
+ verifier = HTTPAPITester()
135
+
136
+ # Prepare config
137
+ config = {
138
+ "url": url,
139
+ "auth_string": auth_string,
140
+ "service_name": service_name,
141
+ "initial_sleep_time": initial_sleep_time,
142
+ "max_delay": max_delay,
143
+ "retries": retries,
144
+ "expected_http_code": expected_http_code,
145
+ "regex": regex,
146
+ "show_header_json": show_header_json,
147
+ "curl_timeout": curl_timeout,
148
+ "http_method": http_method,
149
+ "request_body": request_body,
150
+ "content_type": content_type,
151
+ "request_headers": request_headers,
152
+ "verify_ssl": verify_ssl,
153
+ "ca_bundle_path": ca_bundle_path,
154
+ "include_response_body": include_response_body,
155
+ "follow_redirects": follow_redirects,
156
+ "max_response_time": max_response_time,
157
+ "connection_reuse": connection_reuse,
158
+ "debug": debug,
159
+ "fail_on_timeout": fail_on_timeout,
160
+ }
161
+
162
+ # Transform URL if necessary
163
+ if isinstance(config["url"], str):
164
+ config["url"] = _transform_localhost_url(config["url"])
165
+
166
+ try:
167
+ result = verifier.test_api(**config)
168
+
169
+ # For CLI usage, print the results
170
+ if not os.environ.get("GITHUB_ACTIONS"):
171
+ typer.echo("✅ API test successful!")
172
+ typer.echo(f"Response Code: {result['response_http_code']}")
173
+ typer.echo(f"Total Time: {result['total_time']:.3f}s")
174
+ typer.echo(f"Connect Time: {result['connect_time']:.3f}s")
175
+ if result.get("regex_match"):
176
+ typer.echo(f"Regex Match: {'✅' if result['regex_match'] else '❌'}")
177
+
178
+ except Exception as e:
179
+ typer.echo(f"Error: {e}", err=True)
180
+ raise typer.Exit(1)
181
+
182
+
183
+ def _log_action_parameters(config: Dict[str, Any]) -> None:
184
+ """Log the action parameters in a user-friendly format."""
185
+ from .verifier import HTTPAPITester
186
+
187
+ # Create a temporary verifier instance to use sanitization methods
188
+ temp_verifier = HTTPAPITester()
189
+
190
+ print("📋 Configuration:")
191
+ # Sanitize URL for logging
192
+ url = config.get("url", "Not specified")
193
+ if url != "Not specified":
194
+ url = temp_verifier.sanitize_url_for_logging(url)
195
+ print(f" URL: {url}")
196
+ print(f" HTTP Method: {config.get('http_method', 'GET')}")
197
+ print(f" Service Name: {config.get('service_name', 'API Service')}")
198
+ print(f" Expected HTTP Code: {config.get('expected_http_code', '200')}")
199
+ print(f" Retries: {config.get('retries', '3')}")
200
+ print(f" Timeout: {config.get('curl_timeout', '5')} seconds")
201
+ print(f" SSL Verification: {config.get('verify_ssl', 'true')}")
202
+ print(f" Follow Redirects: {config.get('follow_redirects', 'true')}")
203
+
204
+ # Show optional parameters if they're set
205
+ if config.get("regex"):
206
+ print(f" Regex Pattern: {config['regex']}")
207
+ if config.get("request_body"):
208
+ body = config["request_body"]
209
+ sanitized_body = temp_verifier.sanitize_request_body_for_logging(body, 100)
210
+ print(f" Request Body: {sanitized_body}")
211
+ if config.get("request_headers"):
212
+ sanitized_headers = temp_verifier.sanitize_headers_for_logging(
213
+ config["request_headers"]
214
+ )
215
+ print(f" Custom Headers: {sanitized_headers}")
216
+ if config.get("auth_string"):
217
+ print(" Authentication: *** (hidden)")
218
+ max_time = config.get("max_response_time")
219
+ if max_time and float(max_time) > 0:
220
+ print(f" Max Response Time: {max_time} seconds")
221
+
222
+ debug_enabled = config.get("debug", "false").lower() == "true"
223
+ print(f" Debug Mode: {debug_enabled}")
224
+ print("=" * 50)
225
+ print()
226
+
227
+
228
+ def run_github_action() -> None:
229
+ """Run in GitHub Actions mode."""
230
+ verifier = HTTPAPITester()
231
+
232
+ # Print startup banner
233
+ print("🚀 HTTP API Tool")
234
+ print("=" * 50)
235
+
236
+ # Map GitHub Action inputs to function parameters
237
+ config = {}
238
+ input_mappings = {
239
+ "url": "INPUT_URL",
240
+ "auth_string": "INPUT_AUTH_STRING",
241
+ "service_name": "INPUT_SERVICE_NAME",
242
+ "initial_sleep_time": "INPUT_INITIAL_SLEEP_TIME",
243
+ "max_delay": "INPUT_MAX_DELAY",
244
+ "retries": "INPUT_RETRIES",
245
+ "expected_http_code": "INPUT_EXPECTED_HTTP_CODE",
246
+ "regex": "INPUT_REGEX",
247
+ "show_header_json": "INPUT_SHOW_HEADER_JSON",
248
+ "curl_timeout": "INPUT_CURL_TIMEOUT",
249
+ "http_method": "INPUT_HTTP_METHOD",
250
+ "request_body": "INPUT_REQUEST_BODY",
251
+ "content_type": "INPUT_CONTENT_TYPE",
252
+ "request_headers": "INPUT_REQUEST_HEADERS",
253
+ "verify_ssl": "INPUT_VERIFY_SSL",
254
+ "ca_bundle_path": "INPUT_CA_BUNDLE_PATH",
255
+ "include_response_body": "INPUT_INCLUDE_RESPONSE_BODY",
256
+ "follow_redirects": "INPUT_FOLLOW_REDIRECTS",
257
+ "max_response_time": "INPUT_MAX_RESPONSE_TIME",
258
+ "connection_reuse": "INPUT_CONNECTION_REUSE",
259
+ "debug": "INPUT_DEBUG",
260
+ "fail_on_timeout": "INPUT_FAIL_ON_TIMEOUT",
261
+ }
262
+
263
+ # Set defaults
264
+ defaults = {
265
+ "service_name": "API Service",
266
+ "initial_sleep_time": "1",
267
+ "max_delay": "30",
268
+ "retries": "3",
269
+ "expected_http_code": "200",
270
+ "curl_timeout": "5",
271
+ "http_method": "GET",
272
+ "content_type": "application/json",
273
+ "verify_ssl": "true",
274
+ "include_response_body": "false",
275
+ "follow_redirects": "true",
276
+ "max_response_time": "0",
277
+ "connection_reuse": "true",
278
+ "debug": "false",
279
+ "fail_on_timeout": "false",
280
+ "show_header_json": "false",
281
+ }
282
+
283
+ # Get inputs from environment with defaults
284
+ for param, env_var in input_mappings.items():
285
+ value = os.environ.get(env_var, defaults.get(param))
286
+ if value is not None:
287
+ config[param] = value
288
+
289
+ # Transform localhost URLs for Docker container networking
290
+ if "url" in config:
291
+ config["url"] = _transform_localhost_url(config["url"])
292
+
293
+ # Log the configuration
294
+ _log_action_parameters(config)
295
+
296
+ try:
297
+ result = verifier.test_api(**config)
298
+
299
+ # Write outputs to GitHub Actions
300
+ for key, value in result.items():
301
+ # Convert boolean values to lowercase strings for GitHub Actions
302
+ # Note: result dict contains mixed types: bool, int, float, str
303
+ value_any: Any = value # Help MyPy understand mixed types
304
+ if isinstance(value_any, bool):
305
+ output_value = str(value_any).lower()
306
+ else:
307
+ # Handle non-boolean values (int, float, str, etc.)
308
+ output_value = str(value_any)
309
+ verifier.write_github_output(key, output_value)
310
+
311
+ # Show completion message
312
+ print()
313
+ print("✅ HTTP API Tool")
314
+ print("=" * 50)
315
+
316
+ except Exception as e:
317
+ # Only log once to avoid duplication
318
+ print(f"Error: {e}", file=sys.stderr)
319
+ sys.exit(1)
320
+
321
+
322
+ def main() -> None:
323
+ """Main entry point - handles both GitHub Actions and CLI usage."""
324
+ # Check if we're in GitHub Actions context AND not being invoked for help
325
+ # AND no CLI subcommands are provided (indicating genuine GitHub Actions usage)
326
+ has_cli_command = any(cmd in sys.argv for cmd in ["test", "help", "--help", "-h"])
327
+
328
+ if (
329
+ os.environ.get("GITHUB_ACTIONS")
330
+ and not has_cli_command
331
+ and "--help" not in sys.argv
332
+ and "-h" not in sys.argv
333
+ ):
334
+ run_github_action()
335
+ else:
336
+ # Running as CLI tool or help was requested or explicit CLI command provided
337
+ app()