devdox-ai-locust 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.
Potentially problematic release.
This version of devdox-ai-locust might be problematic. Click here for more details.
- devdox_ai_locust/__init__.py +9 -0
- devdox_ai_locust/cli.py +452 -0
- devdox_ai_locust/config.py +24 -0
- devdox_ai_locust/hybrid_loctus_generator.py +904 -0
- devdox_ai_locust/locust_generator.py +732 -0
- devdox_ai_locust/py.typed +0 -0
- devdox_ai_locust/schemas/__init__.py +0 -0
- devdox_ai_locust/schemas/processing_result.py +24 -0
- devdox_ai_locust/templates/base_workflow.py.j2 +180 -0
- devdox_ai_locust/templates/config.py.j2 +173 -0
- devdox_ai_locust/templates/custom_flows.py.j2 +95 -0
- devdox_ai_locust/templates/endpoint_template.py.j2 +34 -0
- devdox_ai_locust/templates/env.example.j2 +3 -0
- devdox_ai_locust/templates/fallback_locust.py.j2 +25 -0
- devdox_ai_locust/templates/locust.py.j2 +70 -0
- devdox_ai_locust/templates/readme.md.j2 +46 -0
- devdox_ai_locust/templates/requirement.txt.j2 +31 -0
- devdox_ai_locust/templates/test_data.py.j2 +276 -0
- devdox_ai_locust/templates/utils.py.j2 +335 -0
- devdox_ai_locust/utils/__init__.py +0 -0
- devdox_ai_locust/utils/file_creation.py +120 -0
- devdox_ai_locust/utils/open_ai_parser.py +431 -0
- devdox_ai_locust/utils/swagger_utils.py +94 -0
- devdox_ai_locust-0.1.1.dist-info/METADATA +424 -0
- devdox_ai_locust-0.1.1.dist-info/RECORD +29 -0
- devdox_ai_locust-0.1.1.dist-info/WHEEL +5 -0
- devdox_ai_locust-0.1.1.dist-info/entry_points.txt +3 -0
- devdox_ai_locust-0.1.1.dist-info/licenses/LICENSE +201 -0
- devdox_ai_locust-0.1.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""
|
|
2
|
+
DevDox AI Locust - AI-powered Locust load test generator
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from .hybrid_loctus_generator import HybridLocustGenerator
|
|
6
|
+
from .locust_generator import LocustTestGenerator
|
|
7
|
+
from .config import settings
|
|
8
|
+
|
|
9
|
+
__all__ = ["HybridLocustGenerator", "LocustTestGenerator", "settings"]
|
devdox_ai_locust/cli.py
ADDED
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
import click
|
|
2
|
+
import sys
|
|
3
|
+
import asyncio
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from datetime import datetime, timezone
|
|
6
|
+
from typing import Optional, Tuple, Union, List, Dict, Any
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
from rich.table import Table
|
|
9
|
+
from together import Together
|
|
10
|
+
|
|
11
|
+
from .hybrid_loctus_generator import HybridLocustGenerator
|
|
12
|
+
from .config import Settings
|
|
13
|
+
from devdox_ai_locust.utils.swagger_utils import get_api_schema
|
|
14
|
+
from devdox_ai_locust.utils.open_ai_parser import OpenAPIParser, Endpoint
|
|
15
|
+
from .schemas.processing_result import SwaggerProcessingRequest
|
|
16
|
+
|
|
17
|
+
console = Console()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _initialize_config(together_api_key: Optional[str]) -> Tuple[Settings, str]:
|
|
21
|
+
"""Initialize configuration and validate API key"""
|
|
22
|
+
config_obj = Settings()
|
|
23
|
+
if together_api_key:
|
|
24
|
+
api_key = together_api_key
|
|
25
|
+
else:
|
|
26
|
+
api_key = config_obj.API_KEY
|
|
27
|
+
|
|
28
|
+
if not api_key:
|
|
29
|
+
console.print(
|
|
30
|
+
"[red]Error:[/red] Together AI API key is required. "
|
|
31
|
+
"Set TOGETHER_API_KEY environment variable or use --together-api-key"
|
|
32
|
+
)
|
|
33
|
+
sys.exit(1)
|
|
34
|
+
|
|
35
|
+
return config_obj, api_key
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _setup_output_directory(output: Union[str, Path]) -> Path:
|
|
39
|
+
"""Create and return output directory path"""
|
|
40
|
+
output_dir = Path(output)
|
|
41
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
42
|
+
return output_dir
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _display_configuration(
|
|
46
|
+
swagger_url: str,
|
|
47
|
+
output_dir: Path,
|
|
48
|
+
users: int,
|
|
49
|
+
spawn_rate: float,
|
|
50
|
+
run_time: str,
|
|
51
|
+
host: Optional[str],
|
|
52
|
+
auth: bool,
|
|
53
|
+
custom_requirement: Optional[str],
|
|
54
|
+
dry_run: bool,
|
|
55
|
+
) -> None:
|
|
56
|
+
table = Table(title="Generation Configuration")
|
|
57
|
+
table.add_column("Setting", style="cyan")
|
|
58
|
+
table.add_column("Value", style="green")
|
|
59
|
+
|
|
60
|
+
table.add_row("Input Source", str(swagger_url))
|
|
61
|
+
table.add_row("Output Directory", str(output_dir))
|
|
62
|
+
|
|
63
|
+
table.add_row("Users", str(users))
|
|
64
|
+
table.add_row("Spawn Rate", str(spawn_rate))
|
|
65
|
+
table.add_row("Run Time", run_time)
|
|
66
|
+
table.add_row("Host", host or "Auto-detect")
|
|
67
|
+
table.add_row("Authentication", "Enabled" if auth else "Disabled")
|
|
68
|
+
table.add_row("Custom Requirement", custom_requirement or "None")
|
|
69
|
+
table.add_row("Dry Run", "Yes" if dry_run else "No")
|
|
70
|
+
|
|
71
|
+
console.print(table)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _show_results(
|
|
75
|
+
created_files: List[Dict[Any, Any]],
|
|
76
|
+
output_dir: Path,
|
|
77
|
+
start_time: datetime,
|
|
78
|
+
verbose: bool,
|
|
79
|
+
dry_run: bool,
|
|
80
|
+
users: int,
|
|
81
|
+
spawn_rate: float,
|
|
82
|
+
run_time: str,
|
|
83
|
+
host: Optional[str],
|
|
84
|
+
) -> None:
|
|
85
|
+
"""Display generation results and run instructions"""
|
|
86
|
+
end_time = datetime.now(timezone.utc)
|
|
87
|
+
processing_time = (end_time - start_time).total_seconds()
|
|
88
|
+
|
|
89
|
+
if not created_files:
|
|
90
|
+
console.print("[red]✗[/red] No test files were generated")
|
|
91
|
+
sys.exit(1)
|
|
92
|
+
|
|
93
|
+
console.print(f"[green]✓[/green] Tests generated successfully in: {output_dir}")
|
|
94
|
+
console.print(f"[blue]⏱️[/blue] Processing time: {processing_time:.2f} seconds")
|
|
95
|
+
|
|
96
|
+
_show_generated_files(created_files, verbose)
|
|
97
|
+
|
|
98
|
+
if not dry_run:
|
|
99
|
+
_show_run_instructions(output_dir, users, spawn_rate, run_time, host)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _show_generated_files(created_files: List[Dict[Any, Any]], verbose: bool) -> None:
|
|
103
|
+
"""Display list of generated files"""
|
|
104
|
+
if verbose or len(created_files) <= 10:
|
|
105
|
+
console.print("\n[bold]Generated files:[/bold]")
|
|
106
|
+
for file_path in created_files:
|
|
107
|
+
console.print(f" • {file_path}")
|
|
108
|
+
else:
|
|
109
|
+
console.print(f"\n[bold]Generated {len(created_files)} files[/bold]")
|
|
110
|
+
console.print("Use --verbose to see all file names")
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _show_run_instructions(
|
|
114
|
+
output_dir: Path, users: int, spawn_rate: float, run_time: str, host: Optional[str]
|
|
115
|
+
) -> None:
|
|
116
|
+
"""Display instructions for running the generated tests"""
|
|
117
|
+
console.print("\n[bold]To run tests:[/bold]")
|
|
118
|
+
console.print(f" cd {output_dir}")
|
|
119
|
+
|
|
120
|
+
default_host = host or "http://localhost:8000"
|
|
121
|
+
locustfile = output_dir / "locustfile.py"
|
|
122
|
+
|
|
123
|
+
if locustfile.exists():
|
|
124
|
+
main_file = "locustfile.py"
|
|
125
|
+
else:
|
|
126
|
+
py_files = list(output_dir.glob("*.py"))
|
|
127
|
+
main_file = py_files[0].name if py_files else "generated_test.py"
|
|
128
|
+
|
|
129
|
+
console.print(
|
|
130
|
+
f" locust -f {main_file} --users {users} --spawn-rate {spawn_rate} "
|
|
131
|
+
f"--run-time {run_time} --host {default_host}"
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
console.print("\n[bold]Alternative: Use the run command[/bold]")
|
|
135
|
+
console.print(
|
|
136
|
+
f" devdox_ai_locust run {output_dir}/{main_file} --host {default_host}"
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
async def _process_api_schema(
|
|
141
|
+
swagger_url: str, verbose: bool
|
|
142
|
+
) -> Tuple[Dict[str, Any], List[Endpoint], Dict[str, Any]]:
|
|
143
|
+
"""Fetch and parse API schema"""
|
|
144
|
+
source_request = SwaggerProcessingRequest(swagger_url=swagger_url)
|
|
145
|
+
api_schema = None
|
|
146
|
+
with console.status(
|
|
147
|
+
f"[bold green]Fetching API schema from {'URL' if swagger_url.startswith(('http://', 'https://')) else 'file'}..."
|
|
148
|
+
):
|
|
149
|
+
try:
|
|
150
|
+
async with asyncio.timeout(30):
|
|
151
|
+
api_schema = await get_api_schema(source_request)
|
|
152
|
+
|
|
153
|
+
if not api_schema:
|
|
154
|
+
console.print("[red]✗[/red] Failed to fetch API schema")
|
|
155
|
+
sys.exit(1)
|
|
156
|
+
|
|
157
|
+
except asyncio.TimeoutError:
|
|
158
|
+
console.print("[red]✗[/red] Timeout while fetching API schema")
|
|
159
|
+
sys.exit(1)
|
|
160
|
+
except Exception as e:
|
|
161
|
+
console.print(f"[red]✗[/red] Error fetching API schema: {e}")
|
|
162
|
+
sys.exit(1)
|
|
163
|
+
if not api_schema:
|
|
164
|
+
console.print("[red]✗[/red] Failed to fetch API schema")
|
|
165
|
+
sys.exit(1)
|
|
166
|
+
schema_length = len(api_schema) if api_schema else 0
|
|
167
|
+
console.print(
|
|
168
|
+
f"[green]✓[/green] Successfully fetched API schema ({schema_length} characters)"
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
# Parse schema
|
|
172
|
+
with console.status("[bold green]Parsing API schema..."):
|
|
173
|
+
parser = OpenAPIParser()
|
|
174
|
+
try:
|
|
175
|
+
schema_data = parser.parse_schema(api_schema)
|
|
176
|
+
if verbose:
|
|
177
|
+
console.print("✓ Schema data parsed successfully")
|
|
178
|
+
|
|
179
|
+
endpoints = parser.parse_endpoints()
|
|
180
|
+
api_info = parser.get_schema_info()
|
|
181
|
+
|
|
182
|
+
console.print(
|
|
183
|
+
f"[green]📋 Parsed {len(endpoints)} endpoints from {api_info.get('title', 'API')}[/green]"
|
|
184
|
+
)
|
|
185
|
+
return schema_data, endpoints, api_info
|
|
186
|
+
|
|
187
|
+
except Exception as e:
|
|
188
|
+
console.print(f"[red]✗[/red] Failed to parse API schema: {e}")
|
|
189
|
+
sys.exit(1)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
async def _generate_and_create_tests(
|
|
193
|
+
api_key: str,
|
|
194
|
+
endpoints: List[Endpoint],
|
|
195
|
+
api_info: Dict[str, Any],
|
|
196
|
+
output_dir: Path,
|
|
197
|
+
custom_requirement: Optional[str] = "",
|
|
198
|
+
host: Optional[str] = "0.0.0.0",
|
|
199
|
+
auth: bool = False,
|
|
200
|
+
) -> List[Dict[Any, Any]]:
|
|
201
|
+
"""Generate tests using AI and create test files"""
|
|
202
|
+
together_client = Together(api_key=api_key)
|
|
203
|
+
|
|
204
|
+
with console.status("[bold green]Generating Locust tests with AI..."):
|
|
205
|
+
generator = HybridLocustGenerator(ai_client=together_client)
|
|
206
|
+
|
|
207
|
+
test_files, test_directories = await generator.generate_from_endpoints(
|
|
208
|
+
endpoints=endpoints,
|
|
209
|
+
api_info=api_info,
|
|
210
|
+
custom_requirement=custom_requirement,
|
|
211
|
+
target_host=host,
|
|
212
|
+
include_auth=auth,
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
# Create test files
|
|
216
|
+
with console.status("[bold green]Creating test files..."):
|
|
217
|
+
created_files = []
|
|
218
|
+
|
|
219
|
+
# Create workflow files
|
|
220
|
+
if test_directories:
|
|
221
|
+
workflows_dir = output_dir / "workflows"
|
|
222
|
+
workflows_dir.mkdir(exist_ok=True)
|
|
223
|
+
for file_workflow in test_directories:
|
|
224
|
+
workflow_files = await generator._create_test_files_safely(
|
|
225
|
+
file_workflow, workflows_dir
|
|
226
|
+
)
|
|
227
|
+
created_files.extend(workflow_files)
|
|
228
|
+
|
|
229
|
+
# Create main test files
|
|
230
|
+
if test_files:
|
|
231
|
+
main_files = await generator._create_test_files_safely(
|
|
232
|
+
test_files, output_dir
|
|
233
|
+
)
|
|
234
|
+
created_files.extend(main_files)
|
|
235
|
+
|
|
236
|
+
return created_files
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
@click.group()
|
|
240
|
+
@click.version_option(version="0.1.0")
|
|
241
|
+
@click.option("--verbose", "-v", is_flag=True, help="Enable verbose output")
|
|
242
|
+
@click.pass_context
|
|
243
|
+
def cli(ctx: click.Context, verbose: bool) -> None:
|
|
244
|
+
"""DevDox AI LoadTest - Generate Locust tests from API documentation"""
|
|
245
|
+
ctx.ensure_object(dict)
|
|
246
|
+
ctx.obj["verbose"] = verbose
|
|
247
|
+
|
|
248
|
+
if verbose:
|
|
249
|
+
console.print("[green]Verbose mode enabled[/green]")
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
@cli.command()
|
|
253
|
+
@click.argument("swagger_url") # Can be URL or file path
|
|
254
|
+
@click.option(
|
|
255
|
+
"--output",
|
|
256
|
+
"-o",
|
|
257
|
+
type=click.Path(),
|
|
258
|
+
default="output",
|
|
259
|
+
help="Output directory for generated tests (default: output)",
|
|
260
|
+
)
|
|
261
|
+
@click.option("--users", "-u", type=int, default=10, help="Number of simulated users")
|
|
262
|
+
@click.option(
|
|
263
|
+
"--spawn-rate",
|
|
264
|
+
"-r",
|
|
265
|
+
type=float,
|
|
266
|
+
default=2,
|
|
267
|
+
help="Rate to spawn users (users per second)",
|
|
268
|
+
)
|
|
269
|
+
@click.option(
|
|
270
|
+
"--run-time", "-t", type=str, default="5m", help="Test run time (e.g., 5m, 1h)"
|
|
271
|
+
)
|
|
272
|
+
@click.option("--host", "-H", type=str, help="Target host URL")
|
|
273
|
+
@click.option("--auth/--no-auth", default=True, help="Include authentication in tests")
|
|
274
|
+
@click.option("--dry-run", is_flag=True, help="Generate tests without running them")
|
|
275
|
+
@click.option(
|
|
276
|
+
"--custom-requirement", type=str, help="Custom requirements for test generation"
|
|
277
|
+
)
|
|
278
|
+
@click.option(
|
|
279
|
+
"--together-api-key",
|
|
280
|
+
type=str,
|
|
281
|
+
envvar="TOGETHER_API_KEY",
|
|
282
|
+
help="Together AI API key (can also be set via TOGETHER_API_KEY env var)",
|
|
283
|
+
)
|
|
284
|
+
@click.pass_context
|
|
285
|
+
def generate(
|
|
286
|
+
ctx: click.Context,
|
|
287
|
+
swagger_url: str,
|
|
288
|
+
output: str,
|
|
289
|
+
users: int,
|
|
290
|
+
spawn_rate: float,
|
|
291
|
+
run_time: str,
|
|
292
|
+
host: Optional[str],
|
|
293
|
+
auth: bool,
|
|
294
|
+
dry_run: bool,
|
|
295
|
+
custom_requirement: Optional[str],
|
|
296
|
+
together_api_key: Optional[str],
|
|
297
|
+
) -> None: # Added return type annotation
|
|
298
|
+
"""Generate Locust test files from API documentation URL or file"""
|
|
299
|
+
|
|
300
|
+
try:
|
|
301
|
+
# Run the async generation
|
|
302
|
+
asyncio.run(
|
|
303
|
+
_async_generate(
|
|
304
|
+
ctx,
|
|
305
|
+
swagger_url,
|
|
306
|
+
output,
|
|
307
|
+
users,
|
|
308
|
+
spawn_rate,
|
|
309
|
+
run_time,
|
|
310
|
+
host,
|
|
311
|
+
auth,
|
|
312
|
+
dry_run,
|
|
313
|
+
custom_requirement,
|
|
314
|
+
together_api_key,
|
|
315
|
+
)
|
|
316
|
+
)
|
|
317
|
+
except Exception as e:
|
|
318
|
+
console.print(f"[red]Error:[/red] {e}")
|
|
319
|
+
if ctx.obj["verbose"]:
|
|
320
|
+
import traceback
|
|
321
|
+
|
|
322
|
+
console.print(traceback.format_exc())
|
|
323
|
+
sys.exit(1)
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
async def _async_generate(
|
|
327
|
+
ctx: click.Context,
|
|
328
|
+
swagger_url: str,
|
|
329
|
+
output: str,
|
|
330
|
+
users: int,
|
|
331
|
+
spawn_rate: float,
|
|
332
|
+
run_time: str,
|
|
333
|
+
host: Optional[str],
|
|
334
|
+
auth: bool,
|
|
335
|
+
dry_run: bool,
|
|
336
|
+
custom_requirement: Optional[str],
|
|
337
|
+
together_api_key: Optional[str],
|
|
338
|
+
) -> None:
|
|
339
|
+
"""Async function to handle the generation process"""
|
|
340
|
+
|
|
341
|
+
start_time = datetime.now(timezone.utc)
|
|
342
|
+
|
|
343
|
+
try:
|
|
344
|
+
_, api_key = _initialize_config(together_api_key)
|
|
345
|
+
output_dir = _setup_output_directory(output)
|
|
346
|
+
|
|
347
|
+
# Display configuration
|
|
348
|
+
if ctx.obj["verbose"]:
|
|
349
|
+
_display_configuration(
|
|
350
|
+
swagger_url,
|
|
351
|
+
output_dir,
|
|
352
|
+
users,
|
|
353
|
+
spawn_rate,
|
|
354
|
+
run_time,
|
|
355
|
+
host,
|
|
356
|
+
auth,
|
|
357
|
+
custom_requirement,
|
|
358
|
+
dry_run,
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
_, endpoints, api_info = await _process_api_schema(
|
|
362
|
+
swagger_url, ctx.obj["verbose"]
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
created_files = await _generate_and_create_tests(
|
|
366
|
+
api_key, endpoints, api_info, output_dir, custom_requirement, host, auth
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
# Show results
|
|
370
|
+
_show_results(
|
|
371
|
+
created_files,
|
|
372
|
+
output_dir,
|
|
373
|
+
start_time,
|
|
374
|
+
ctx.obj["verbose"],
|
|
375
|
+
dry_run,
|
|
376
|
+
users,
|
|
377
|
+
spawn_rate,
|
|
378
|
+
run_time,
|
|
379
|
+
host,
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
except Exception as e:
|
|
383
|
+
end_time = datetime.now(timezone.utc)
|
|
384
|
+
processing_time = (end_time - start_time).total_seconds()
|
|
385
|
+
console.print(
|
|
386
|
+
f"[red]✗[/red] Generation failed after {processing_time:.2f}s: {e}"
|
|
387
|
+
)
|
|
388
|
+
raise
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
@cli.command()
|
|
392
|
+
@click.argument("test_file", type=click.Path(exists=True))
|
|
393
|
+
@click.option("--users", "-u", type=int, default=10, help="Number of simulated users")
|
|
394
|
+
@click.option("--spawn-rate", "-r", type=float, default=2, help="Rate to spawn users")
|
|
395
|
+
@click.option("--run-time", "-t", type=str, default="5m", help="Test run time")
|
|
396
|
+
@click.option("--host", "-H", type=str, required=True, help="Target host URL")
|
|
397
|
+
@click.option("--headless", is_flag=True, help="Run in headless mode (no web UI)")
|
|
398
|
+
@click.pass_context
|
|
399
|
+
def run(
|
|
400
|
+
ctx: click.Context,
|
|
401
|
+
test_file: str,
|
|
402
|
+
users: int,
|
|
403
|
+
spawn_rate: float,
|
|
404
|
+
run_time: str,
|
|
405
|
+
host: str,
|
|
406
|
+
headless: bool,
|
|
407
|
+
) -> None:
|
|
408
|
+
"""Run generated Locust tests"""
|
|
409
|
+
|
|
410
|
+
try:
|
|
411
|
+
import subprocess
|
|
412
|
+
|
|
413
|
+
cmd = [
|
|
414
|
+
"locust",
|
|
415
|
+
"-f",
|
|
416
|
+
str(test_file),
|
|
417
|
+
"--users",
|
|
418
|
+
str(users),
|
|
419
|
+
"--spawn-rate",
|
|
420
|
+
str(spawn_rate),
|
|
421
|
+
"--run-time",
|
|
422
|
+
run_time,
|
|
423
|
+
"--host",
|
|
424
|
+
host,
|
|
425
|
+
]
|
|
426
|
+
|
|
427
|
+
if headless:
|
|
428
|
+
cmd.append("--headless")
|
|
429
|
+
|
|
430
|
+
if ctx.obj["verbose"]:
|
|
431
|
+
console.print(f"[blue]Running command:[/blue] {' '.join(cmd)}")
|
|
432
|
+
|
|
433
|
+
console.print("[green]Starting Locust test...[/green]")
|
|
434
|
+
subprocess.run(cmd, check=True)
|
|
435
|
+
|
|
436
|
+
except subprocess.CalledProcessError as e:
|
|
437
|
+
console.print(f"[red]Test execution failed:[/red] {e}")
|
|
438
|
+
sys.exit(1)
|
|
439
|
+
except FileNotFoundError:
|
|
440
|
+
console.print(
|
|
441
|
+
"[red]Locust not found. Please install locust: pip install locust[/red]"
|
|
442
|
+
)
|
|
443
|
+
sys.exit(1)
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
def main() -> None:
|
|
447
|
+
"""Main entry point for the CLI"""
|
|
448
|
+
cli()
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
if __name__ == "__main__":
|
|
452
|
+
main()
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Configuration settings for the DevDox AI Locust
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from pydantic_settings import BaseSettings
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Settings(BaseSettings):
|
|
9
|
+
"""Application settings."""
|
|
10
|
+
|
|
11
|
+
VERSION: str = "0.1.1"
|
|
12
|
+
|
|
13
|
+
API_KEY: str = "" # Fallback for backward compatibility
|
|
14
|
+
|
|
15
|
+
class Config:
|
|
16
|
+
"""Pydantic config class."""
|
|
17
|
+
|
|
18
|
+
env_file = ".env"
|
|
19
|
+
case_sensitive = True
|
|
20
|
+
extra = "ignore"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# Initialize settings instance
|
|
24
|
+
settings = Settings()
|