overload-cli 0.1.0__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.
- overload/__init__.py +3 -0
- overload/__main__.py +5 -0
- overload/cli.py +393 -0
- overload/collection/__init__.py +1 -0
- overload/collection/environment.py +23 -0
- overload/collection/models.py +88 -0
- overload/collection/parser.py +220 -0
- overload/collection/variables.py +84 -0
- overload/config_file.py +73 -0
- overload/engine/__init__.py +1 -0
- overload/engine/assertions.py +151 -0
- overload/engine/auth.py +87 -0
- overload/engine/events.py +50 -0
- overload/engine/http_client.py +274 -0
- overload/engine/load_patterns.py +730 -0
- overload/engine/models.py +254 -0
- overload/engine/rate_limiter.py +124 -0
- overload/engine/runner.py +86 -0
- overload/report/__init__.py +1 -0
- overload/report/exporters.py +77 -0
- overload/report/generator.py +71 -0
- overload/report/templates/report.html +369 -0
- overload/utils/__init__.py +1 -0
- overload/utils/naming.py +26 -0
- overload/web/__init__.py +1 -0
- overload/web/app.py +38 -0
- overload/web/routes/__init__.py +1 -0
- overload/web/routes/api.py +461 -0
- overload/web/routes/ws.py +77 -0
- overload/web/static/css/app.css +242 -0
- overload/web/static/js/app.js +241 -0
- overload/web/static/js/charts.js +385 -0
- overload/web/static/js/collection.js +344 -0
- overload/web/static/js/runner.js +625 -0
- overload/web/templates/index.html +23 -0
- overload_cli-0.1.0.dist-info/METADATA +267 -0
- overload_cli-0.1.0.dist-info/RECORD +40 -0
- overload_cli-0.1.0.dist-info/WHEEL +4 -0
- overload_cli-0.1.0.dist-info/entry_points.txt +2 -0
- overload_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
overload/__init__.py
ADDED
overload/__main__.py
ADDED
overload/cli.py
ADDED
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import asyncio
|
|
5
|
+
import json
|
|
6
|
+
import logging
|
|
7
|
+
import os
|
|
8
|
+
import sys
|
|
9
|
+
import webbrowser
|
|
10
|
+
|
|
11
|
+
from overload import __version__
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def main() -> None:
|
|
17
|
+
parser = argparse.ArgumentParser(
|
|
18
|
+
prog="overload",
|
|
19
|
+
description="Overload — Free load testing tool for Postman collections",
|
|
20
|
+
)
|
|
21
|
+
parser.add_argument("--version", action="version", version=f"overload {__version__}")
|
|
22
|
+
parser.add_argument("--debug", action="store_true", help="Enable debug logging")
|
|
23
|
+
|
|
24
|
+
subparsers = parser.add_subparsers(dest="command")
|
|
25
|
+
|
|
26
|
+
# UI command (also the default)
|
|
27
|
+
ui_parser = subparsers.add_parser("ui", help="Start the browser UI")
|
|
28
|
+
ui_parser.add_argument("--port", type=int, default=3000, help="Port number (default: 3000)")
|
|
29
|
+
ui_parser.add_argument("--no-browser", action="store_true", help="Don't open browser automatically")
|
|
30
|
+
ui_parser.add_argument("--host", default="127.0.0.1", help="Host to bind to (default: 127.0.0.1)")
|
|
31
|
+
|
|
32
|
+
# Run command
|
|
33
|
+
run_parser = subparsers.add_parser("run", help="Run a load test from CLI")
|
|
34
|
+
run_parser.add_argument("--collection", required=True, help="Path to Postman collection JSON")
|
|
35
|
+
run_parser.add_argument("--environment", help="Path to Postman environment JSON")
|
|
36
|
+
run_parser.add_argument(
|
|
37
|
+
"--pattern",
|
|
38
|
+
choices=["load", "stress", "spike", "soak", "ramp", "burst", "breakpoint", "custom", "ratelimit"],
|
|
39
|
+
default="burst",
|
|
40
|
+
help="Test pattern (default: burst)",
|
|
41
|
+
)
|
|
42
|
+
run_parser.add_argument("--requests", type=int, default=200, help="Total requests for burst (default: 200)")
|
|
43
|
+
run_parser.add_argument("--concurrency", type=int, default=20, help="Max concurrent requests (default: 20)")
|
|
44
|
+
run_parser.add_argument("--rps", type=int, default=50, help="Target requests per second")
|
|
45
|
+
run_parser.add_argument("--duration", type=int, default=300, help="Test duration in seconds")
|
|
46
|
+
run_parser.add_argument("--var", action="append", dest="vars", metavar="KEY=VALUE", help="Variable override")
|
|
47
|
+
run_parser.add_argument("--save-responses", action="store_true", help="Save response bodies")
|
|
48
|
+
run_parser.add_argument("--output", default="reports", help="Output directory for reports (default: reports/)")
|
|
49
|
+
run_parser.add_argument("--format", choices=["html", "json", "csv"], default="html", help="Report format")
|
|
50
|
+
run_parser.add_argument("--stages", help="Custom stages JSON: '[{\"duration\":60,\"rps\":100}]'")
|
|
51
|
+
run_parser.add_argument("--timeout", type=float, default=30.0, help="Request timeout in seconds")
|
|
52
|
+
run_parser.add_argument("--no-verify-ssl", action="store_true", help="Disable SSL verification")
|
|
53
|
+
run_parser.add_argument(
|
|
54
|
+
"--assert", action="append", dest="assertions", metavar="EXPR",
|
|
55
|
+
help="Assertion threshold, e.g. 'p95_latency_ms<500' (repeatable)",
|
|
56
|
+
)
|
|
57
|
+
run_parser.add_argument("--junit", metavar="PATH", help="Write JUnit XML report to PATH")
|
|
58
|
+
run_parser.add_argument("--open-report", action="store_true", help="Open HTML report in browser after run")
|
|
59
|
+
run_parser.add_argument("--config", metavar="PATH", help="Path to overload.config.yaml")
|
|
60
|
+
|
|
61
|
+
# Sequential command
|
|
62
|
+
seq_parser = subparsers.add_parser("sequential", help="Run collection requests sequentially")
|
|
63
|
+
seq_parser.add_argument("--collection", required=True, help="Path to Postman collection JSON")
|
|
64
|
+
seq_parser.add_argument("--environment", help="Path to Postman environment JSON")
|
|
65
|
+
seq_parser.add_argument("--iterations", type=int, default=1, help="Number of iterations (default: 1)")
|
|
66
|
+
seq_parser.add_argument("--delay", type=int, default=0, help="Delay between requests in ms (default: 0)")
|
|
67
|
+
seq_parser.add_argument("--var", action="append", dest="vars", metavar="KEY=VALUE", help="Variable override")
|
|
68
|
+
seq_parser.add_argument("--output", default=".", help="Output directory")
|
|
69
|
+
seq_parser.add_argument("--timeout", type=float, default=30.0, help="Request timeout in seconds")
|
|
70
|
+
|
|
71
|
+
args = parser.parse_args()
|
|
72
|
+
|
|
73
|
+
_setup_logging(args.debug if hasattr(args, "debug") else False)
|
|
74
|
+
|
|
75
|
+
if args.command == "run":
|
|
76
|
+
asyncio.run(_run_test(args))
|
|
77
|
+
elif args.command == "sequential":
|
|
78
|
+
asyncio.run(_run_sequential(args))
|
|
79
|
+
else:
|
|
80
|
+
_start_ui(args)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _setup_logging(debug: bool) -> None:
|
|
84
|
+
level = logging.DEBUG if debug else logging.WARNING
|
|
85
|
+
if os.environ.get("OVERLOAD_DEBUG"):
|
|
86
|
+
level = logging.DEBUG
|
|
87
|
+
logging.basicConfig(
|
|
88
|
+
level=level,
|
|
89
|
+
format="%(asctime)s %(levelname)s %(name)s: %(message)s",
|
|
90
|
+
datefmt="%H:%M:%S",
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _parse_vars(var_list: list[str] | None) -> dict[str, str]:
|
|
95
|
+
if not var_list:
|
|
96
|
+
return {}
|
|
97
|
+
result = {}
|
|
98
|
+
for v in var_list:
|
|
99
|
+
if "=" in v:
|
|
100
|
+
key, value = v.split("=", 1)
|
|
101
|
+
result[key.strip()] = value.strip()
|
|
102
|
+
return result
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _start_ui(args: argparse.Namespace) -> None:
|
|
106
|
+
import uvicorn
|
|
107
|
+
|
|
108
|
+
from overload.web.app import create_app
|
|
109
|
+
|
|
110
|
+
port = getattr(args, "port", 3000)
|
|
111
|
+
host = getattr(args, "host", "127.0.0.1")
|
|
112
|
+
no_browser = getattr(args, "no_browser", False)
|
|
113
|
+
|
|
114
|
+
app = create_app(working_dir=os.getcwd())
|
|
115
|
+
|
|
116
|
+
print(f"\n OVERLOAD — Load Testing Tool v{__version__}")
|
|
117
|
+
print(f" Starting on http://{host}:{port}")
|
|
118
|
+
print(" Press Ctrl+C to stop\n")
|
|
119
|
+
|
|
120
|
+
if not no_browser:
|
|
121
|
+
import threading
|
|
122
|
+
threading.Timer(1.0, lambda: webbrowser.open(f"http://{host}:{port}")).start()
|
|
123
|
+
|
|
124
|
+
uvicorn.run(app, host=host, port=port, log_level="warning")
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
async def _run_test(args: argparse.Namespace) -> None:
|
|
128
|
+
from overload.collection.environment import parse_environment
|
|
129
|
+
from overload.collection.parser import parse_collection
|
|
130
|
+
from overload.collection.variables import VariableContext
|
|
131
|
+
from overload.engine.http_client import HttpClient
|
|
132
|
+
from overload.engine.load_patterns import get_pattern
|
|
133
|
+
from overload.engine.models import PatternConfig, Stats, Threshold
|
|
134
|
+
from overload.engine.rate_limiter import run_rate_limit_test
|
|
135
|
+
from overload.report.exporters import export_csv, export_json
|
|
136
|
+
from overload.report.generator import generate_report
|
|
137
|
+
from overload.utils.naming import generate_run_id
|
|
138
|
+
|
|
139
|
+
file_config: dict = {}
|
|
140
|
+
file_thresholds: list[Threshold] = []
|
|
141
|
+
if args.config:
|
|
142
|
+
from overload.config_file import extract_config, extract_test_type, extract_thresholds, load_config
|
|
143
|
+
raw = load_config(args.config)
|
|
144
|
+
file_config = extract_config(raw)
|
|
145
|
+
file_thresholds = extract_thresholds(raw)
|
|
146
|
+
file_test_type = extract_test_type(raw)
|
|
147
|
+
if file_test_type and args.pattern == "burst":
|
|
148
|
+
args.pattern = file_test_type
|
|
149
|
+
|
|
150
|
+
print(f"\n OVERLOAD — {args.pattern.upper()} TEST")
|
|
151
|
+
print(f" Collection: {args.collection}")
|
|
152
|
+
if args.config:
|
|
153
|
+
print(f" Config: {args.config}")
|
|
154
|
+
|
|
155
|
+
collection = parse_collection(args.collection)
|
|
156
|
+
print(f" Requests: {len(collection.requests)}")
|
|
157
|
+
|
|
158
|
+
env_vars = {}
|
|
159
|
+
if args.environment:
|
|
160
|
+
env_vars = parse_environment(args.environment)
|
|
161
|
+
|
|
162
|
+
runtime_vars = _parse_vars(args.vars)
|
|
163
|
+
variables = VariableContext(
|
|
164
|
+
collection_vars=collection.variables,
|
|
165
|
+
environment_vars=env_vars,
|
|
166
|
+
runtime_vars=runtime_vars,
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
run_id = generate_run_id()
|
|
170
|
+
cancel_event = asyncio.Event()
|
|
171
|
+
print(f" Run ID: {run_id}\n")
|
|
172
|
+
|
|
173
|
+
def _cfg(cli_val, key, default, cast=None):
|
|
174
|
+
if cli_val != default:
|
|
175
|
+
return cli_val
|
|
176
|
+
file_val = file_config.get(key)
|
|
177
|
+
if file_val is not None:
|
|
178
|
+
return cast(file_val) if cast else file_val
|
|
179
|
+
return cli_val
|
|
180
|
+
|
|
181
|
+
concurrency = _cfg(args.concurrency, "concurrency", 20, int)
|
|
182
|
+
timeout = _cfg(args.timeout, "timeout_seconds", 30.0, float)
|
|
183
|
+
rps = _cfg(args.rps, "target_rps", 50, int)
|
|
184
|
+
duration = _cfg(args.duration, "hold_duration_seconds", 300, int)
|
|
185
|
+
requests = _cfg(args.requests, "total_requests", 200, int)
|
|
186
|
+
|
|
187
|
+
config = PatternConfig(
|
|
188
|
+
concurrency=concurrency,
|
|
189
|
+
timeout_seconds=timeout,
|
|
190
|
+
verify_ssl=not args.no_verify_ssl,
|
|
191
|
+
total_requests=requests,
|
|
192
|
+
target_rps=rps,
|
|
193
|
+
hold_duration_seconds=duration,
|
|
194
|
+
soak_rps=rps,
|
|
195
|
+
soak_duration_seconds=duration,
|
|
196
|
+
ramp_end_rps=rps,
|
|
197
|
+
start_rps=10,
|
|
198
|
+
spike_rps=rps,
|
|
199
|
+
rate_limit_cap=rps,
|
|
200
|
+
rate_limit_requests=requests,
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
if args.stages:
|
|
204
|
+
try:
|
|
205
|
+
config.stages = json.loads(args.stages)
|
|
206
|
+
except json.JSONDecodeError:
|
|
207
|
+
print(" Error: Invalid stages JSON")
|
|
208
|
+
sys.exit(1)
|
|
209
|
+
elif file_config.get("stages"):
|
|
210
|
+
config.stages = file_config["stages"]
|
|
211
|
+
|
|
212
|
+
stats = Stats()
|
|
213
|
+
ramp_rows: list[dict] = []
|
|
214
|
+
completed = 0
|
|
215
|
+
|
|
216
|
+
async def on_progress(progress):
|
|
217
|
+
nonlocal completed
|
|
218
|
+
if progress.completed_requests > completed:
|
|
219
|
+
completed = progress.completed_requests
|
|
220
|
+
pct = min(100, completed * 100 // max(progress.total_requests, completed, 1))
|
|
221
|
+
bar = "#" * (pct // 5) + "-" * (20 - pct // 5)
|
|
222
|
+
print(f"\r [{bar}] {pct}% ({completed} requests) — {progress.phase}", end="", flush=True)
|
|
223
|
+
if progress.phase == "complete":
|
|
224
|
+
print()
|
|
225
|
+
|
|
226
|
+
async with HttpClient(
|
|
227
|
+
timeout=config.timeout_seconds,
|
|
228
|
+
verify_ssl=config.verify_ssl,
|
|
229
|
+
max_connections=config.concurrency * 2,
|
|
230
|
+
) as client:
|
|
231
|
+
await client.prepare_collection_auth(collection.auth, variables)
|
|
232
|
+
if args.pattern == "ratelimit":
|
|
233
|
+
results, ramp_rows = await run_rate_limit_test(
|
|
234
|
+
client, collection.requests, variables, config,
|
|
235
|
+
run_id, cancel_event, on_progress,
|
|
236
|
+
)
|
|
237
|
+
stats.add_all(results)
|
|
238
|
+
else:
|
|
239
|
+
pattern = get_pattern(args.pattern)
|
|
240
|
+
results = await pattern.execute(
|
|
241
|
+
client, collection.requests, variables, config,
|
|
242
|
+
run_id, cancel_event, on_progress,
|
|
243
|
+
)
|
|
244
|
+
stats.add_all(results)
|
|
245
|
+
|
|
246
|
+
computed = stats.compute()
|
|
247
|
+
if computed:
|
|
248
|
+
print(f"\n Results:")
|
|
249
|
+
print(f" Total: {computed['total']} OK: {computed['ok']} Errors: {computed['errors']}")
|
|
250
|
+
lat = computed["latency"]
|
|
251
|
+
print(f" Latency — min: {lat['min']}ms p95: {lat['p95']}ms max: {lat['max']}ms")
|
|
252
|
+
print(f" Duration: {computed['duration_seconds']}s Avg RPS: {computed['avg_rps']}")
|
|
253
|
+
|
|
254
|
+
thresholds: list[Threshold] = []
|
|
255
|
+
if args.assertions:
|
|
256
|
+
from overload.engine.assertions import parse_threshold
|
|
257
|
+
for expr in args.assertions:
|
|
258
|
+
try:
|
|
259
|
+
thresholds.append(parse_threshold(expr))
|
|
260
|
+
except ValueError as e:
|
|
261
|
+
print(f"\n Error: {e}")
|
|
262
|
+
sys.exit(2)
|
|
263
|
+
elif file_thresholds:
|
|
264
|
+
thresholds = file_thresholds
|
|
265
|
+
|
|
266
|
+
verdict_failed = False
|
|
267
|
+
verdict_data = None
|
|
268
|
+
if thresholds and computed:
|
|
269
|
+
from overload.engine.assertions import evaluate, print_verdict, write_junit_xml
|
|
270
|
+
|
|
271
|
+
verdict = evaluate(computed, thresholds)
|
|
272
|
+
print_verdict(verdict)
|
|
273
|
+
verdict_data = {
|
|
274
|
+
"passed": verdict.passed,
|
|
275
|
+
"results": [
|
|
276
|
+
{
|
|
277
|
+
"metric": r.metric,
|
|
278
|
+
"operator": r.operator,
|
|
279
|
+
"expected": r.expected,
|
|
280
|
+
"actual": round(r.actual, 2),
|
|
281
|
+
"passed": r.passed,
|
|
282
|
+
}
|
|
283
|
+
for r in verdict.results
|
|
284
|
+
],
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if args.junit:
|
|
288
|
+
write_junit_xml(verdict, args.junit, test_name=f"overload-{args.pattern}")
|
|
289
|
+
print(f" JUnit XML: {os.path.abspath(args.junit)}")
|
|
290
|
+
|
|
291
|
+
verdict_failed = not verdict.passed
|
|
292
|
+
|
|
293
|
+
report_config_dict = {"pattern": args.pattern, "concurrency": concurrency}
|
|
294
|
+
report_path = ""
|
|
295
|
+
if args.format in ("html", "json"):
|
|
296
|
+
report_path = generate_report(
|
|
297
|
+
stats, args.pattern, report_config_dict,
|
|
298
|
+
run_id=run_id, ramp_rows=ramp_rows, output_dir=args.output,
|
|
299
|
+
verdict=verdict_data,
|
|
300
|
+
)
|
|
301
|
+
if report_path:
|
|
302
|
+
print(f"\n Report: {os.path.abspath(report_path)}")
|
|
303
|
+
|
|
304
|
+
if args.format == "json":
|
|
305
|
+
json_path = export_json(stats, args.pattern, run_id, args.output, ramp_rows)
|
|
306
|
+
if json_path:
|
|
307
|
+
print(f" JSON: {os.path.abspath(json_path)}")
|
|
308
|
+
|
|
309
|
+
if args.format == "csv":
|
|
310
|
+
csv_path = export_csv(stats, run_id, args.output)
|
|
311
|
+
if csv_path:
|
|
312
|
+
print(f" CSV: {os.path.abspath(csv_path)}")
|
|
313
|
+
|
|
314
|
+
if getattr(args, "open_report", False) and report_path:
|
|
315
|
+
webbrowser.open(f"file://{os.path.abspath(report_path)}")
|
|
316
|
+
|
|
317
|
+
print()
|
|
318
|
+
|
|
319
|
+
if verdict_failed:
|
|
320
|
+
sys.exit(1)
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
async def _run_sequential(args: argparse.Namespace) -> None:
|
|
324
|
+
from overload.collection.environment import parse_environment
|
|
325
|
+
from overload.collection.parser import parse_collection
|
|
326
|
+
from overload.collection.variables import VariableContext
|
|
327
|
+
from overload.engine.http_client import HttpClient
|
|
328
|
+
from overload.engine.models import PatternConfig, Stats
|
|
329
|
+
from overload.engine.runner import run_sequential
|
|
330
|
+
from overload.report.generator import generate_report
|
|
331
|
+
from overload.utils.naming import generate_run_id
|
|
332
|
+
|
|
333
|
+
print(f"\n OVERLOAD — SEQUENTIAL RUN")
|
|
334
|
+
print(f" Collection: {args.collection}")
|
|
335
|
+
|
|
336
|
+
collection = parse_collection(args.collection)
|
|
337
|
+
print(f" Requests: {len(collection.requests)}")
|
|
338
|
+
print(f" Iterations: {args.iterations} Delay: {args.delay}ms")
|
|
339
|
+
|
|
340
|
+
env_vars = {}
|
|
341
|
+
if args.environment:
|
|
342
|
+
env_vars = parse_environment(args.environment)
|
|
343
|
+
|
|
344
|
+
runtime_vars = _parse_vars(args.vars)
|
|
345
|
+
variables = VariableContext(
|
|
346
|
+
collection_vars=collection.variables,
|
|
347
|
+
environment_vars=env_vars,
|
|
348
|
+
runtime_vars=runtime_vars,
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
run_id = generate_run_id()
|
|
352
|
+
cancel_event = asyncio.Event()
|
|
353
|
+
print(f" Run ID: {run_id}\n")
|
|
354
|
+
|
|
355
|
+
config = PatternConfig(
|
|
356
|
+
iterations=args.iterations,
|
|
357
|
+
delay_ms=args.delay,
|
|
358
|
+
timeout_seconds=args.timeout,
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
async def on_progress(progress):
|
|
362
|
+
print(f"\r {progress.phase} — {progress.completed_requests}/{progress.total_requests}", end="", flush=True)
|
|
363
|
+
if progress.phase == "complete":
|
|
364
|
+
print()
|
|
365
|
+
|
|
366
|
+
async with HttpClient(timeout=config.timeout_seconds) as client:
|
|
367
|
+
results = await run_sequential(
|
|
368
|
+
client, collection.requests, variables, config,
|
|
369
|
+
run_id, cancel_event, on_progress,
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
stats = Stats()
|
|
373
|
+
stats.add_all(results)
|
|
374
|
+
computed = stats.compute()
|
|
375
|
+
|
|
376
|
+
if computed:
|
|
377
|
+
print(f"\n Results:")
|
|
378
|
+
print(f" Total: {computed['total']} OK: {computed['ok']} Errors: {computed['errors']}")
|
|
379
|
+
lat = computed["latency"]
|
|
380
|
+
print(f" Latency — min: {lat['min']}ms p95: {lat['p95']}ms max: {lat['max']}ms")
|
|
381
|
+
|
|
382
|
+
report_config = {"iterations": args.iterations, "delay_ms": args.delay}
|
|
383
|
+
report_path = generate_report(
|
|
384
|
+
stats, "sequential", report_config,
|
|
385
|
+
run_id=run_id, output_dir=args.output,
|
|
386
|
+
)
|
|
387
|
+
if report_path:
|
|
388
|
+
print(f"\n Report: {os.path.abspath(report_path)}")
|
|
389
|
+
print()
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
if __name__ == "__main__":
|
|
393
|
+
main()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def parse_environment(source: str | Path | dict) -> dict[str, str]:
|
|
8
|
+
if isinstance(source, dict):
|
|
9
|
+
data = source
|
|
10
|
+
else:
|
|
11
|
+
path = Path(source)
|
|
12
|
+
if not path.exists():
|
|
13
|
+
raise FileNotFoundError(f"Environment file not found: {path}")
|
|
14
|
+
with open(path, encoding="utf-8") as f:
|
|
15
|
+
data = json.load(f)
|
|
16
|
+
|
|
17
|
+
variables: dict[str, str] = {}
|
|
18
|
+
|
|
19
|
+
for entry in data.get("values", []):
|
|
20
|
+
if entry.get("enabled", True) and "key" in entry:
|
|
21
|
+
variables[entry["key"]] = entry.get("value", "")
|
|
22
|
+
|
|
23
|
+
return variables
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclass
|
|
7
|
+
class CollectionVariable:
|
|
8
|
+
key: str
|
|
9
|
+
value: str
|
|
10
|
+
type: str = "string"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class AuthConfig:
|
|
15
|
+
type: str
|
|
16
|
+
params: dict[str, str] = field(default_factory=dict)
|
|
17
|
+
|
|
18
|
+
def to_dict(self) -> dict:
|
|
19
|
+
return {"type": self.type, "params": self.params}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class QueryParam:
|
|
24
|
+
key: str
|
|
25
|
+
value: str
|
|
26
|
+
disabled: bool = False
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass
|
|
30
|
+
class RequestBody:
|
|
31
|
+
mode: str
|
|
32
|
+
content: str | dict | list = ""
|
|
33
|
+
content_type: str = ""
|
|
34
|
+
|
|
35
|
+
def to_dict(self) -> dict:
|
|
36
|
+
return {
|
|
37
|
+
"mode": self.mode,
|
|
38
|
+
"content": self.content,
|
|
39
|
+
"content_type": self.content_type,
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass
|
|
44
|
+
class ParsedRequest:
|
|
45
|
+
name: str
|
|
46
|
+
method: str
|
|
47
|
+
url_raw: str
|
|
48
|
+
headers: dict[str, str] = field(default_factory=dict)
|
|
49
|
+
body: RequestBody = field(default_factory=lambda: RequestBody(mode="none"))
|
|
50
|
+
auth: AuthConfig | None = None
|
|
51
|
+
query_params: list[QueryParam] = field(default_factory=list)
|
|
52
|
+
folder_path: list[str] = field(default_factory=list)
|
|
53
|
+
|
|
54
|
+
def to_dict(self) -> dict:
|
|
55
|
+
return {
|
|
56
|
+
"name": self.name,
|
|
57
|
+
"method": self.method,
|
|
58
|
+
"url_raw": self.url_raw,
|
|
59
|
+
"headers": self.headers,
|
|
60
|
+
"body": self.body.to_dict(),
|
|
61
|
+
"auth": self.auth.to_dict() if self.auth else None,
|
|
62
|
+
"query_params": [
|
|
63
|
+
{"key": q.key, "value": q.value, "disabled": q.disabled}
|
|
64
|
+
for q in self.query_params
|
|
65
|
+
],
|
|
66
|
+
"folder_path": self.folder_path,
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@dataclass
|
|
71
|
+
class ParsedCollection:
|
|
72
|
+
name: str
|
|
73
|
+
description: str
|
|
74
|
+
requests: list[ParsedRequest] = field(default_factory=list)
|
|
75
|
+
variables: list[CollectionVariable] = field(default_factory=list)
|
|
76
|
+
auth: AuthConfig | None = None
|
|
77
|
+
|
|
78
|
+
def to_dict(self) -> dict:
|
|
79
|
+
return {
|
|
80
|
+
"name": self.name,
|
|
81
|
+
"description": self.description,
|
|
82
|
+
"requests": [r.to_dict() for r in self.requests],
|
|
83
|
+
"variables": [
|
|
84
|
+
{"key": v.key, "value": v.value, "type": v.type}
|
|
85
|
+
for v in self.variables
|
|
86
|
+
],
|
|
87
|
+
"auth": self.auth.to_dict() if self.auth else None,
|
|
88
|
+
}
|