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.

@@ -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"]
@@ -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()