wsg 0.1.0__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.
- wsg-0.1.0/PKG-INFO +58 -0
- wsg-0.1.0/README.md +47 -0
- wsg-0.1.0/pyproject.toml +21 -0
- wsg-0.1.0/pyproject.toml.orig +20 -0
- wsg-0.1.0/src/wsg/__init__.py +15 -0
- wsg-0.1.0/src/wsg/cli.py +40 -0
- wsg-0.1.0/src/wsg/config.py +15 -0
- wsg-0.1.0/src/wsg/graph.py +44 -0
- wsg-0.1.0/src/wsg/reports.py +27 -0
- wsg-0.1.0/src/wsg/runner.py +26 -0
- wsg-0.1.0/src/wsg/stats.py +105 -0
- wsg-0.1.0/src/wsg/worker.py +34 -0
wsg-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: wsg
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: API load testing tool that produces latency/throughput graphs
|
|
5
|
+
Author: Owen Zeng
|
|
6
|
+
Author-email: Owen Zeng <owenzeng315@gmail.com>
|
|
7
|
+
Requires-Dist: httpx>=0.28.1
|
|
8
|
+
Requires-Dist: matplotlib>=3.11.1
|
|
9
|
+
Requires-Python: >=3.13
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
|
|
12
|
+
# wsg
|
|
13
|
+
|
|
14
|
+
API load testing tool designed for performance visualization. Sends concurrent
|
|
15
|
+
requests to an endpoint and produces a latency/throughput graph from the
|
|
16
|
+
results.
|
|
17
|
+
|
|
18
|
+
## Install
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
uv tool install wsg
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Or, without a permanent install:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
uvx wsg <url> [options]
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
`pipx install wsg` / `pip install wsg` also work if you don't use `uv`.
|
|
31
|
+
|
|
32
|
+
## Usage
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
wsg <url> [options]
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### Options
|
|
39
|
+
|
|
40
|
+
| Flag | Description | Default |
|
|
41
|
+
|------|-------------|---------|
|
|
42
|
+
| `-X`, `--method` | HTTP method to use | `GET` |
|
|
43
|
+
| `-n`, `--total_requests` | Total number of requests to send | `100` |
|
|
44
|
+
| `-c`, `--concurrency` | Number of concurrent requests | `10` |
|
|
45
|
+
| `-H`, `--headers` | HTTP headers, e.g. `-H "Authorization: Bearer xyz"` | none |
|
|
46
|
+
| `-d`, `--body` | Request body for POST/PUT requests | none |
|
|
47
|
+
| `--timeout` | Timeout per request in seconds | `10.0` |
|
|
48
|
+
| `-o`, `--output_path` | Path to save the output graph | `results.png` |
|
|
49
|
+
|
|
50
|
+
### Example
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
wsg https://api.example.com/ping -n 500 -c 20 -o load_test.png
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
This sends 500 requests at a concurrency of 20 and saves a latency
|
|
57
|
+
distribution + throughput-over-time chart to `load_test.png` in the current
|
|
58
|
+
directory.
|
wsg-0.1.0/README.md
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# wsg
|
|
2
|
+
|
|
3
|
+
API load testing tool designed for performance visualization. Sends concurrent
|
|
4
|
+
requests to an endpoint and produces a latency/throughput graph from the
|
|
5
|
+
results.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
uv tool install wsg
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Or, without a permanent install:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
uvx wsg <url> [options]
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
`pipx install wsg` / `pip install wsg` also work if you don't use `uv`.
|
|
20
|
+
|
|
21
|
+
## Usage
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
wsg <url> [options]
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### Options
|
|
28
|
+
|
|
29
|
+
| Flag | Description | Default |
|
|
30
|
+
|------|-------------|---------|
|
|
31
|
+
| `-X`, `--method` | HTTP method to use | `GET` |
|
|
32
|
+
| `-n`, `--total_requests` | Total number of requests to send | `100` |
|
|
33
|
+
| `-c`, `--concurrency` | Number of concurrent requests | `10` |
|
|
34
|
+
| `-H`, `--headers` | HTTP headers, e.g. `-H "Authorization: Bearer xyz"` | none |
|
|
35
|
+
| `-d`, `--body` | Request body for POST/PUT requests | none |
|
|
36
|
+
| `--timeout` | Timeout per request in seconds | `10.0` |
|
|
37
|
+
| `-o`, `--output_path` | Path to save the output graph | `results.png` |
|
|
38
|
+
|
|
39
|
+
### Example
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
wsg https://api.example.com/ping -n 500 -c 20 -o load_test.png
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
This sends 500 requests at a concurrency of 20 and saves a latency
|
|
46
|
+
distribution + throughput-over-time chart to `load_test.png` in the current
|
|
47
|
+
directory.
|
wsg-0.1.0/pyproject.toml
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "wsg"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "API load testing tool that produces latency/throughput graphs"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.13"
|
|
7
|
+
dependencies = [
|
|
8
|
+
"httpx>=0.28.1",
|
|
9
|
+
"matplotlib>=3.11.1",
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
[[project.authors]]
|
|
13
|
+
name = "Owen Zeng"
|
|
14
|
+
email = "owenzeng315@gmail.com"
|
|
15
|
+
|
|
16
|
+
[project.scripts]
|
|
17
|
+
wsg = "wsg:main"
|
|
18
|
+
|
|
19
|
+
[build-system]
|
|
20
|
+
requires = ["uv_build>=0.12.10,<0.13.0"]
|
|
21
|
+
build-backend = "uv_build"
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "wsg"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "API load testing tool that produces latency/throughput graphs"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
authors = [
|
|
7
|
+
{ name = "Owen Zeng", email = "owenzeng315@gmail.com" }
|
|
8
|
+
]
|
|
9
|
+
requires-python = ">=3.13"
|
|
10
|
+
dependencies = [
|
|
11
|
+
"httpx>=0.28.1",
|
|
12
|
+
"matplotlib>=3.11.1",
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
[project.scripts]
|
|
16
|
+
wsg = "wsg:main"
|
|
17
|
+
|
|
18
|
+
[build-system]
|
|
19
|
+
requires = ["uv_build>=0.12.10,<0.13.0"]
|
|
20
|
+
build-backend = "uv_build"
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
|
|
3
|
+
from wsg.cli import parse_args
|
|
4
|
+
from wsg.runner import run
|
|
5
|
+
from wsg.stats import summarize
|
|
6
|
+
from wsg.reports import print_report
|
|
7
|
+
from wsg.graph import render
|
|
8
|
+
|
|
9
|
+
def main():
|
|
10
|
+
config = parse_args()
|
|
11
|
+
results, _ = asyncio.run(run(config))
|
|
12
|
+
summary = summarize(results)
|
|
13
|
+
print_report(summary, config.url, config.method)
|
|
14
|
+
render(results, config.output_path)
|
|
15
|
+
print(f'Graph saved to {config.output_path}')
|
wsg-0.1.0/src/wsg/cli.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
|
|
3
|
+
from wsg.config import Config
|
|
4
|
+
|
|
5
|
+
def parse_args():
|
|
6
|
+
'''
|
|
7
|
+
Parse the command-line arguments for the API load-testing tool.
|
|
8
|
+
'''
|
|
9
|
+
parser = argparse.ArgumentParser(description="A simple API load-testing tool")
|
|
10
|
+
parser.add_argument("url", type=str, help="The URL of the API endpoint to test")
|
|
11
|
+
parser.add_argument("-X", "--method", type=str, default="GET", help="HTTP method to use (default: GET)")
|
|
12
|
+
parser.add_argument("-n", "--total_requests", type=int, default=100, help="Total number of requests to send (default: 100)")
|
|
13
|
+
parser.add_argument("-c", "--concurrency", type=int, default=10, help="Number of concurrent requests to send (default: 10)")
|
|
14
|
+
parser.add_argument("-H", "--headers", type=str, nargs='*', help="HTTP headers to include in the requests (format: 'Header-Name: Header-Value')")
|
|
15
|
+
parser.add_argument("-d", "--body", type=str, help="Request body for POST/PUT requests")
|
|
16
|
+
parser.add_argument("--timeout", type=float, default=10.0, help="Timeout for each request in seconds (default: 10.0)")
|
|
17
|
+
parser.add_argument("-o", "--output_path", type=str, default="results.png", help="Path to save the output results (default: results.png)")
|
|
18
|
+
|
|
19
|
+
args = parser.parse_args()
|
|
20
|
+
|
|
21
|
+
if args.total_requests <= 0:
|
|
22
|
+
parser.error("--requests must be a positive integer")
|
|
23
|
+
if args.concurrency <= 0:
|
|
24
|
+
parser.error("--concurrency must be a positive integer")
|
|
25
|
+
if args.timeout <= 0:
|
|
26
|
+
parser.error("--timeout must be a positive number")
|
|
27
|
+
|
|
28
|
+
return Config(
|
|
29
|
+
url=args.url,
|
|
30
|
+
method=args.method,
|
|
31
|
+
total_requests=args.total_requests,
|
|
32
|
+
concurrency=args.concurrency,
|
|
33
|
+
headers={header.split(":", 1)[0].strip(): header.split(":", 1)[1].strip() for header in args.headers} if args.headers else {},
|
|
34
|
+
body=args.body,
|
|
35
|
+
timeout=args.timeout,
|
|
36
|
+
output_path=args.output_path
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
|
|
3
|
+
@dataclass
|
|
4
|
+
class Config:
|
|
5
|
+
'''
|
|
6
|
+
Configuration class for the WSG application.
|
|
7
|
+
'''
|
|
8
|
+
url: str
|
|
9
|
+
method: str = "GET"
|
|
10
|
+
total_requests: int = 100
|
|
11
|
+
concurrency: int = 10
|
|
12
|
+
headers: dict[str, str] = field(default_factory=dict)
|
|
13
|
+
body: str | None = None
|
|
14
|
+
timeout: float = 10.0
|
|
15
|
+
output_path: str = "results.png"
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import statistics
|
|
2
|
+
|
|
3
|
+
import matplotlib
|
|
4
|
+
matplotlib.use("Agg")
|
|
5
|
+
import matplotlib.pyplot as plt
|
|
6
|
+
|
|
7
|
+
from wsg.worker import Result
|
|
8
|
+
from wsg.stats import bucket_throughputs
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def render(results: list[Result], output_path: str) -> None:
|
|
12
|
+
'''
|
|
13
|
+
Render a latency histogram and a throughput-over-time chart from the
|
|
14
|
+
results, side by side in one figure, and save it to output_path.
|
|
15
|
+
'''
|
|
16
|
+
latencies_ms = [r.elapsed * 1000 for r in results if r.status_code is not None]
|
|
17
|
+
throughputs = bucket_throughputs(results)
|
|
18
|
+
|
|
19
|
+
fig, (hist_ax, throughput_ax) = plt.subplots(1, 2, figsize=(12, 5))
|
|
20
|
+
|
|
21
|
+
bin_count = min(50, max(10, len(latencies_ms) // 2)) if latencies_ms else 10
|
|
22
|
+
hist_ax.hist(latencies_ms, bins=bin_count, color="#1f77b4", edgecolor="white")
|
|
23
|
+
if latencies_ms:
|
|
24
|
+
mean_ms = statistics.mean(latencies_ms)
|
|
25
|
+
hist_ax.axvline(mean_ms, color="black", linestyle="--", linewidth=1, label=f"Mean: {mean_ms:.0f} ms")
|
|
26
|
+
hist_ax.legend()
|
|
27
|
+
hist_ax.set_xlabel("Latency (ms)")
|
|
28
|
+
hist_ax.set_ylabel("Number of requests")
|
|
29
|
+
hist_ax.set_title("Latency Distribution")
|
|
30
|
+
hist_ax.grid(axis="y", alpha=0.3)
|
|
31
|
+
|
|
32
|
+
bucket_centers = [i + 0.5 for i in range(len(throughputs))]
|
|
33
|
+
bars = throughput_ax.bar(bucket_centers, throughputs, width=0.6, color="#ff7f0e", edgecolor="black", linewidth=0.5)
|
|
34
|
+
throughput_ax.bar_label(bars, fmt="%.0f")
|
|
35
|
+
throughput_ax.set_xticks(range(len(throughputs) + 1))
|
|
36
|
+
throughput_ax.set_ylim(0, max(throughputs, default=0) * 1.15)
|
|
37
|
+
throughput_ax.set_xlabel("Time since start (s)")
|
|
38
|
+
throughput_ax.set_ylabel("Requests/sec")
|
|
39
|
+
throughput_ax.set_title("Throughput Over Time")
|
|
40
|
+
throughput_ax.grid(axis="y", alpha=0.3)
|
|
41
|
+
|
|
42
|
+
fig.tight_layout()
|
|
43
|
+
fig.savefig(output_path, dpi=120)
|
|
44
|
+
plt.close(fig)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from wsg.stats import Summary
|
|
2
|
+
|
|
3
|
+
def print_report(summary: Summary, url: str, method: str) -> None:
|
|
4
|
+
'''
|
|
5
|
+
Print the summary report of the WSG application.
|
|
6
|
+
'''
|
|
7
|
+
print(f'Target: {method} {url}')
|
|
8
|
+
print("=== Load Test Results ===")
|
|
9
|
+
print(f'Total requests: {summary.count}')
|
|
10
|
+
print(f'Successful requests: {summary.success_count}')
|
|
11
|
+
print(f'Failed requests: {summary.error_count}')
|
|
12
|
+
print(f'Error rate: {summary.error_rate:.2%}')
|
|
13
|
+
print("=== Load Test Latency Metrics ===")
|
|
14
|
+
print(f'Min latency: {summary.latency_min * 1000:.2f} ms')
|
|
15
|
+
print(f'Max latency: {summary.latency_max * 1000:.2f} ms')
|
|
16
|
+
print(f'Mean latency: {summary.latency_mean * 1000:.2f} ms')
|
|
17
|
+
print(f'50th percentile latency: {summary.p50 * 1000:.2f} ms')
|
|
18
|
+
print(f'90th percentile latency: {summary.p90 * 1000:.2f} ms')
|
|
19
|
+
print(f'95th percentile latency: {summary.p95 * 1000:.2f} ms')
|
|
20
|
+
print(f'99th percentile latency: {summary.p99 * 1000:.2f} ms')
|
|
21
|
+
print("=== Load Test Throughput ===")
|
|
22
|
+
print(f'Throughput Min: {summary.throughput_min:.2f} requests/sec')
|
|
23
|
+
print(f'Throughput Max: {summary.throughput_max:.2f} requests/sec')
|
|
24
|
+
print(f'Throughput Mean: {summary.throughput_mean:.2f} requests/sec')
|
|
25
|
+
print("=== Status Codes ===")
|
|
26
|
+
for code, count in summary.status_codes.items():
|
|
27
|
+
print(f' {code}: {count}')
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import httpx
|
|
3
|
+
import sys
|
|
4
|
+
import time
|
|
5
|
+
|
|
6
|
+
from wsg.config import Config
|
|
7
|
+
from wsg.worker import Result, send_request
|
|
8
|
+
|
|
9
|
+
async def run(config: Config) -> tuple[list[Result], float]:
|
|
10
|
+
'''
|
|
11
|
+
Run the load test using the provided configuration.
|
|
12
|
+
'''
|
|
13
|
+
async with httpx.AsyncClient() as client:
|
|
14
|
+
semaphore = asyncio.Semaphore(config.concurrency)
|
|
15
|
+
run_start = time.perf_counter()
|
|
16
|
+
tasks = [send_request(client, semaphore, config) for _ in range(config.total_requests)]
|
|
17
|
+
|
|
18
|
+
results = []
|
|
19
|
+
for coro in asyncio.as_completed(tasks):
|
|
20
|
+
results.append(await coro)
|
|
21
|
+
print(f"\rProgress: {len(results)}/{len(tasks)} ({len(results) / len(tasks):.0%})", end="", file=sys.stderr, flush=True)
|
|
22
|
+
print(file=sys.stderr)
|
|
23
|
+
|
|
24
|
+
for result in results:
|
|
25
|
+
result.start_time -= run_start
|
|
26
|
+
return (results, total_time := time.perf_counter() - run_start)
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from wsg.worker import Result
|
|
3
|
+
from collections import Counter
|
|
4
|
+
|
|
5
|
+
import statistics
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
StatusCode = int
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class Summary:
|
|
12
|
+
'''
|
|
13
|
+
Summary class to hold the results of the WSG application.
|
|
14
|
+
'''
|
|
15
|
+
count: int
|
|
16
|
+
success_count: int
|
|
17
|
+
error_count: int
|
|
18
|
+
error_rate: float
|
|
19
|
+
latency_min: float
|
|
20
|
+
latency_max: float
|
|
21
|
+
latency_mean: float
|
|
22
|
+
p50: float
|
|
23
|
+
p90: float
|
|
24
|
+
p95: float
|
|
25
|
+
p99: float
|
|
26
|
+
throughput_min: float
|
|
27
|
+
throughput_max: float
|
|
28
|
+
throughput_mean: float
|
|
29
|
+
status_codes: dict[StatusCode, int]
|
|
30
|
+
|
|
31
|
+
def bucket_throughputs(results: list[Result], bucket_size: float = 1.0) -> list[float]:
|
|
32
|
+
'''
|
|
33
|
+
Split successful results into fixed-size time buckets by completion
|
|
34
|
+
time (start_time + elapsed) and return the throughput (requests/sec)
|
|
35
|
+
measured in each bucket. The last bucket is dropped whenever there is
|
|
36
|
+
more than one, since it may cover only a partial slice of real time
|
|
37
|
+
and would understate its rate.
|
|
38
|
+
'''
|
|
39
|
+
completions = [r.start_time + r.elapsed for r in results if r.status_code is not None]
|
|
40
|
+
if not completions:
|
|
41
|
+
return []
|
|
42
|
+
|
|
43
|
+
bucket_counts = Counter(int(t // bucket_size) for t in completions)
|
|
44
|
+
buckets = [bucket_counts[i] / bucket_size for i in sorted(bucket_counts)]
|
|
45
|
+
|
|
46
|
+
if len(buckets) > 1:
|
|
47
|
+
buckets = buckets[:-1]
|
|
48
|
+
|
|
49
|
+
return buckets
|
|
50
|
+
|
|
51
|
+
def summarize(results: list[Result]) -> 'Summary':
|
|
52
|
+
'''
|
|
53
|
+
Summarize the results of the WSG application.
|
|
54
|
+
'''
|
|
55
|
+
success = []
|
|
56
|
+
failure = []
|
|
57
|
+
|
|
58
|
+
for i in results:
|
|
59
|
+
if i.status_code != None:
|
|
60
|
+
success.append(i)
|
|
61
|
+
else:
|
|
62
|
+
failure.append(i)
|
|
63
|
+
|
|
64
|
+
count = len(results)
|
|
65
|
+
success_count = len(success)
|
|
66
|
+
error_count = len(failure)
|
|
67
|
+
error_rate = error_count / count if count > 0 else 0.0
|
|
68
|
+
|
|
69
|
+
latency_list = []
|
|
70
|
+
for i in success:
|
|
71
|
+
latency_list.append(i.elapsed)
|
|
72
|
+
|
|
73
|
+
latency_min = min(latency_list) if latency_list else 0.0
|
|
74
|
+
latency_max = max(latency_list) if latency_list else 0.0
|
|
75
|
+
latency_mean = statistics.mean(latency_list) if latency_list else 0.0
|
|
76
|
+
|
|
77
|
+
p50 = statistics.median(latency_list) if latency_list else 0.0
|
|
78
|
+
p90 = statistics.quantiles(latency_list, n=10)[8] if latency_list else 0.0
|
|
79
|
+
p95 = statistics.quantiles(latency_list, n=20)[18] if latency_list else 0.0
|
|
80
|
+
p99 = statistics.quantiles(latency_list, n=100)[98] if latency_list else 0.0
|
|
81
|
+
|
|
82
|
+
throughput_buckets = bucket_throughputs(results)
|
|
83
|
+
throughput_min = min(throughput_buckets) if throughput_buckets else 0.0
|
|
84
|
+
throughput_max = max(throughput_buckets) if throughput_buckets else 0.0
|
|
85
|
+
throughput_mean = statistics.mean(throughput_buckets) if throughput_buckets else 0.0
|
|
86
|
+
|
|
87
|
+
status_codes = Counter(i.status_code for i in results if i.status_code is not None)
|
|
88
|
+
|
|
89
|
+
return Summary(
|
|
90
|
+
count=count,
|
|
91
|
+
success_count=success_count,
|
|
92
|
+
error_count=error_count,
|
|
93
|
+
error_rate=error_rate,
|
|
94
|
+
latency_min=latency_min,
|
|
95
|
+
latency_max=latency_max,
|
|
96
|
+
latency_mean=latency_mean,
|
|
97
|
+
p50=p50,
|
|
98
|
+
p90=p90,
|
|
99
|
+
p95=p95,
|
|
100
|
+
p99=p99,
|
|
101
|
+
throughput_min=throughput_min,
|
|
102
|
+
throughput_max=throughput_max,
|
|
103
|
+
throughput_mean=throughput_mean,
|
|
104
|
+
status_codes=dict(status_codes)
|
|
105
|
+
)
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from wsg.config import Config
|
|
3
|
+
import time
|
|
4
|
+
import httpx
|
|
5
|
+
import asyncio
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class Result:
|
|
9
|
+
'''
|
|
10
|
+
Result class to store the outcome of each request.
|
|
11
|
+
'''
|
|
12
|
+
status_code: int | None
|
|
13
|
+
elapsed: float
|
|
14
|
+
error: str | None
|
|
15
|
+
start_time: float
|
|
16
|
+
|
|
17
|
+
async def send_request(client: httpx.AsyncClient, semaphore: asyncio.Semaphore, config: Config) -> Result:
|
|
18
|
+
'''
|
|
19
|
+
Send a single HTTP request using the provided client and configuration.
|
|
20
|
+
'''
|
|
21
|
+
start = time.perf_counter()
|
|
22
|
+
try:
|
|
23
|
+
async with semaphore:
|
|
24
|
+
response = await client.request(
|
|
25
|
+
config.method, config.url,
|
|
26
|
+
headers=config.headers,
|
|
27
|
+
content=config.body,
|
|
28
|
+
timeout=config.timeout,
|
|
29
|
+
)
|
|
30
|
+
elapsed = time.perf_counter() - start
|
|
31
|
+
return Result(status_code=response.status_code, elapsed=elapsed, error=None, start_time=start)
|
|
32
|
+
except Exception as e:
|
|
33
|
+
elapsed = time.perf_counter() - start
|
|
34
|
+
return Result(status_code=None, elapsed=elapsed, error=str(e), start_time=start)
|