devdox-ai-locust 0.1.2__tar.gz → 0.1.7__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.
Files changed (49) hide show
  1. {devdox_ai_locust-0.1.2 → devdox_ai_locust-0.1.7}/PKG-INFO +32 -5
  2. {devdox_ai_locust-0.1.2 → devdox_ai_locust-0.1.7}/README.md +31 -4
  3. {devdox_ai_locust-0.1.2 → devdox_ai_locust-0.1.7}/pyproject.toml +2 -2
  4. {devdox_ai_locust-0.1.2 → devdox_ai_locust-0.1.7}/src/devdox_ai_locust/cli.py +21 -5
  5. {devdox_ai_locust-0.1.2 → devdox_ai_locust-0.1.7}/src/devdox_ai_locust/config.py +1 -1
  6. {devdox_ai_locust-0.1.2 → devdox_ai_locust-0.1.7}/src/devdox_ai_locust/hybrid_loctus_generator.py +152 -43
  7. {devdox_ai_locust-0.1.2 → devdox_ai_locust-0.1.7}/src/devdox_ai_locust/locust_generator.py +85 -10
  8. devdox_ai_locust-0.1.7/src/devdox_ai_locust/prompt/test_data.j2 +186 -0
  9. devdox_ai_locust-0.1.7/src/devdox_ai_locust/prompt/workflow.j2 +483 -0
  10. {devdox_ai_locust-0.1.2 → devdox_ai_locust-0.1.7}/src/devdox_ai_locust/templates/endpoint_template.py.j2 +1 -1
  11. devdox_ai_locust-0.1.7/src/devdox_ai_locust/templates/mongo/data_provider.py.j2 +303 -0
  12. devdox_ai_locust-0.1.7/src/devdox_ai_locust/templates/mongo/db_config.py.j2 +271 -0
  13. devdox_ai_locust-0.1.7/src/devdox_ai_locust/templates/mongo/db_integration.j2 +352 -0
  14. {devdox_ai_locust-0.1.2 → devdox_ai_locust-0.1.7}/src/devdox_ai_locust/templates/readme.md.j2 +3 -1
  15. {devdox_ai_locust-0.1.2 → devdox_ai_locust-0.1.7}/src/devdox_ai_locust/templates/requirement.txt.j2 +5 -2
  16. {devdox_ai_locust-0.1.2 → devdox_ai_locust-0.1.7}/src/devdox_ai_locust/templates/test_data.py.j2 +5 -1
  17. {devdox_ai_locust-0.1.2 → devdox_ai_locust-0.1.7}/src/devdox_ai_locust.egg-info/PKG-INFO +32 -5
  18. {devdox_ai_locust-0.1.2 → devdox_ai_locust-0.1.7}/src/devdox_ai_locust.egg-info/SOURCES.txt +3 -1
  19. {devdox_ai_locust-0.1.2 → devdox_ai_locust-0.1.7}/tests/test_cli.py +5 -2
  20. {devdox_ai_locust-0.1.2 → devdox_ai_locust-0.1.7}/tests/test_config.py +4 -4
  21. {devdox_ai_locust-0.1.2 → devdox_ai_locust-0.1.7}/tests/test_hybrid_loctus_generator.py +206 -12
  22. devdox_ai_locust-0.1.2/src/devdox_ai_locust/prompt/test_data.j2 +0 -62
  23. devdox_ai_locust-0.1.2/src/devdox_ai_locust/prompt/workflow.j2 +0 -145
  24. devdox_ai_locust-0.1.2/tests/test_data.py +0 -505
  25. {devdox_ai_locust-0.1.2 → devdox_ai_locust-0.1.7}/LICENSE +0 -0
  26. {devdox_ai_locust-0.1.2 → devdox_ai_locust-0.1.7}/setup.cfg +0 -0
  27. {devdox_ai_locust-0.1.2 → devdox_ai_locust-0.1.7}/src/devdox_ai_locust/__init__.py +0 -0
  28. {devdox_ai_locust-0.1.2 → devdox_ai_locust-0.1.7}/src/devdox_ai_locust/prompt/domain.j2 +0 -0
  29. {devdox_ai_locust-0.1.2 → devdox_ai_locust-0.1.7}/src/devdox_ai_locust/prompt/locust.j2 +0 -0
  30. {devdox_ai_locust-0.1.2 → devdox_ai_locust-0.1.7}/src/devdox_ai_locust/prompt/validation.j2 +0 -0
  31. {devdox_ai_locust-0.1.2 → devdox_ai_locust-0.1.7}/src/devdox_ai_locust/py.typed +0 -0
  32. {devdox_ai_locust-0.1.2 → devdox_ai_locust-0.1.7}/src/devdox_ai_locust/schemas/__init__.py +0 -0
  33. {devdox_ai_locust-0.1.2 → devdox_ai_locust-0.1.7}/src/devdox_ai_locust/schemas/processing_result.py +0 -0
  34. {devdox_ai_locust-0.1.2 → devdox_ai_locust-0.1.7}/src/devdox_ai_locust/templates/base_workflow.py.j2 +0 -0
  35. {devdox_ai_locust-0.1.2 → devdox_ai_locust-0.1.7}/src/devdox_ai_locust/templates/config.py.j2 +0 -0
  36. {devdox_ai_locust-0.1.2 → devdox_ai_locust-0.1.7}/src/devdox_ai_locust/templates/custom_flows.py.j2 +0 -0
  37. {devdox_ai_locust-0.1.2 → devdox_ai_locust-0.1.7}/src/devdox_ai_locust/templates/env.example.j2 +0 -0
  38. {devdox_ai_locust-0.1.2 → devdox_ai_locust-0.1.7}/src/devdox_ai_locust/templates/fallback_locust.py.j2 +0 -0
  39. {devdox_ai_locust-0.1.2 → devdox_ai_locust-0.1.7}/src/devdox_ai_locust/templates/locust.py.j2 +0 -0
  40. {devdox_ai_locust-0.1.2 → devdox_ai_locust-0.1.7}/src/devdox_ai_locust/templates/utils.py.j2 +0 -0
  41. {devdox_ai_locust-0.1.2 → devdox_ai_locust-0.1.7}/src/devdox_ai_locust/utils/__init__.py +0 -0
  42. {devdox_ai_locust-0.1.2 → devdox_ai_locust-0.1.7}/src/devdox_ai_locust/utils/file_creation.py +0 -0
  43. {devdox_ai_locust-0.1.2 → devdox_ai_locust-0.1.7}/src/devdox_ai_locust/utils/open_ai_parser.py +0 -0
  44. {devdox_ai_locust-0.1.2 → devdox_ai_locust-0.1.7}/src/devdox_ai_locust/utils/swagger_utils.py +0 -0
  45. {devdox_ai_locust-0.1.2 → devdox_ai_locust-0.1.7}/src/devdox_ai_locust.egg-info/dependency_links.txt +0 -0
  46. {devdox_ai_locust-0.1.2 → devdox_ai_locust-0.1.7}/src/devdox_ai_locust.egg-info/entry_points.txt +0 -0
  47. {devdox_ai_locust-0.1.2 → devdox_ai_locust-0.1.7}/src/devdox_ai_locust.egg-info/requires.txt +0 -0
  48. {devdox_ai_locust-0.1.2 → devdox_ai_locust-0.1.7}/src/devdox_ai_locust.egg-info/top_level.txt +0 -0
  49. {devdox_ai_locust-0.1.2 → devdox_ai_locust-0.1.7}/tests/test_locust_generator.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devdox_ai_locust
3
- Version: 0.1.2
3
+ Version: 0.1.7
4
4
  Summary: AI-powered Locust load test generator from API documentation
5
5
  Author-email: Hayat Bourji <hayat.bourgi@montyholding.com>
6
6
  Maintainer-email: Hayat Bourji <hayat.bourgi@montyholding.com>
@@ -67,11 +67,20 @@ Dynamic: license-file
67
67
  [![License: Apache 2.0](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
68
68
  [![Python 3.12+](https://img.shields.io/badge/python-3.12+-blue.svg)](https://www.python.org/downloads/)
69
69
  [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
70
+ [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=montymobile1_devdox-ai-locust&metric=alert_status)](https://sonarcloud.io/dashboard?id=montymobile1_devdox-ai-locust)
70
71
 
71
72
  > **AI-powered Locust load test generator from API documentation**
72
73
 
73
74
  DevDox AI Locust automatically generates comprehensive Locust load testing scripts from your API documentation (OpenAPI/Swagger specs). Using advanced AI capabilities, it creates realistic test scenarios, handles complex authentication flows, and generates production-ready performance tests.
74
75
 
76
+ ## [0.1.7] - 2025-11-11
77
+ ## 🆕 What's New in 0.1.7
78
+
79
+ **Improved Naming Convention:** Generated class names now automatically capitalize group names to ensure consistent and professional naming in generated Python code.
80
+
81
+ **MongoDB Environment Setup:** Added example MongoDB variables (MONGODB_URI, MONGODB_DB_NAME, etc.) in the .env.example file to make it easier to configure MongoDB connections.
82
+
83
+
75
84
  ## ✨ Features
76
85
 
77
86
  - 🤖 **AI-Enhanced Generation**: Uses Together AI to create intelligent, realistic load test scenarios
@@ -82,6 +91,7 @@ DevDox AI Locust automatically generates comprehensive Locust load testing scrip
82
91
  - 🛠️ **Template-Based**: Highly customizable Jinja2 templates for different testing needs
83
92
  - 🔄 **Hybrid Approach**: Combines rule-based generation with AI enhancement
84
93
  - 📈 **Comprehensive Coverage**: Handles various HTTP methods, content types, and response scenarios
94
+ - ⚡ **Asynchronous Processing**: Fast, non-blocking test generation with async/await
85
95
 
86
96
  ## 🚀 Quick Start
87
97
 
@@ -121,18 +131,35 @@ echo "API_KEY=your_together_ai_api_key_here" > .env
121
131
  # Generate from OpenAPI URL
122
132
  devdox_ai_locust generate --openapi-url https://api.example.com/openapi.json --output ./tests
123
133
 
124
- # Generate from local file
125
- dal generate --openapi-file ./api-spec.yaml --output ./load-tests
126
-
127
134
  # Generate with custom configuration
128
135
  devdox_ai_locust generate \
129
- https://petstore.swagger.io/v2/swagger.json \
136
+ https://petstore3.swagger.io/api/v3/openapi.json \
130
137
  --output ./petstore-tests \
131
138
  --together-api-key your_api_key \
132
139
 
140
+ # Generate with db integration
141
+ devdox_ai_locust generate \
142
+ https://petstore3.swagger.io/api/v3/openapi.json \
143
+ --output ./petstore-tests \
144
+ --db-type mongo \
133
145
  ```
134
146
 
135
147
 
148
+ ## 🚀 Installation with Inputs
149
+
150
+ Add this step to your GitHub Actions workflow:
151
+
152
+ ```yaml
153
+ - name: DevDox Locust Test Generator
154
+ uses: montymobile1/devdox-ai-locust@v0.1.6
155
+ with:
156
+ swagger_url: "https://portal-api.devdox.ai/openapi.json"
157
+ output: "generated_tests"
158
+ users: "15"
159
+ spawn_rate: "3"
160
+ run_time: "10m"
161
+ together_api_key: ${{ secrets.TOGETHER_API_KEY }}
162
+
136
163
 
137
164
  ## 📖 Documentation
138
165
 
@@ -3,11 +3,20 @@
3
3
  [![License: Apache 2.0](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
4
4
  [![Python 3.12+](https://img.shields.io/badge/python-3.12+-blue.svg)](https://www.python.org/downloads/)
5
5
  [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
6
+ [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=montymobile1_devdox-ai-locust&metric=alert_status)](https://sonarcloud.io/dashboard?id=montymobile1_devdox-ai-locust)
6
7
 
7
8
  > **AI-powered Locust load test generator from API documentation**
8
9
 
9
10
  DevDox AI Locust automatically generates comprehensive Locust load testing scripts from your API documentation (OpenAPI/Swagger specs). Using advanced AI capabilities, it creates realistic test scenarios, handles complex authentication flows, and generates production-ready performance tests.
10
11
 
12
+ ## [0.1.7] - 2025-11-11
13
+ ## 🆕 What's New in 0.1.7
14
+
15
+ **Improved Naming Convention:** Generated class names now automatically capitalize group names to ensure consistent and professional naming in generated Python code.
16
+
17
+ **MongoDB Environment Setup:** Added example MongoDB variables (MONGODB_URI, MONGODB_DB_NAME, etc.) in the .env.example file to make it easier to configure MongoDB connections.
18
+
19
+
11
20
  ## ✨ Features
12
21
 
13
22
  - 🤖 **AI-Enhanced Generation**: Uses Together AI to create intelligent, realistic load test scenarios
@@ -18,6 +27,7 @@ DevDox AI Locust automatically generates comprehensive Locust load testing scrip
18
27
  - 🛠️ **Template-Based**: Highly customizable Jinja2 templates for different testing needs
19
28
  - 🔄 **Hybrid Approach**: Combines rule-based generation with AI enhancement
20
29
  - 📈 **Comprehensive Coverage**: Handles various HTTP methods, content types, and response scenarios
30
+ - ⚡ **Asynchronous Processing**: Fast, non-blocking test generation with async/await
21
31
 
22
32
  ## 🚀 Quick Start
23
33
 
@@ -57,18 +67,35 @@ echo "API_KEY=your_together_ai_api_key_here" > .env
57
67
  # Generate from OpenAPI URL
58
68
  devdox_ai_locust generate --openapi-url https://api.example.com/openapi.json --output ./tests
59
69
 
60
- # Generate from local file
61
- dal generate --openapi-file ./api-spec.yaml --output ./load-tests
62
-
63
70
  # Generate with custom configuration
64
71
  devdox_ai_locust generate \
65
- https://petstore.swagger.io/v2/swagger.json \
72
+ https://petstore3.swagger.io/api/v3/openapi.json \
66
73
  --output ./petstore-tests \
67
74
  --together-api-key your_api_key \
68
75
 
76
+ # Generate with db integration
77
+ devdox_ai_locust generate \
78
+ https://petstore3.swagger.io/api/v3/openapi.json \
79
+ --output ./petstore-tests \
80
+ --db-type mongo \
69
81
  ```
70
82
 
71
83
 
84
+ ## 🚀 Installation with Inputs
85
+
86
+ Add this step to your GitHub Actions workflow:
87
+
88
+ ```yaml
89
+ - name: DevDox Locust Test Generator
90
+ uses: montymobile1/devdox-ai-locust@v0.1.6
91
+ with:
92
+ swagger_url: "https://portal-api.devdox.ai/openapi.json"
93
+ output: "generated_tests"
94
+ users: "15"
95
+ spawn_rate: "3"
96
+ run_time: "10m"
97
+ together_api_key: ${{ secrets.TOGETHER_API_KEY }}
98
+
72
99
 
73
100
  ## 📖 Documentation
74
101
 
@@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
5
5
 
6
6
  [project]
7
7
  name = "devdox_ai_locust"
8
- version = "0.1.2"
8
+ version = "0.1.7"
9
9
  description = "AI-powered Locust load test generator from API documentation"
10
10
  readme = "README.md"
11
11
  license = {text = "Apache-2.0" }
@@ -101,7 +101,7 @@ exclude = ["tests*"]
101
101
 
102
102
 
103
103
  [tool.setuptools.package-data]
104
- devdox_ai_locust = ["schemas/*.json", "templates/*.j2","prompt/*.j2", "py.typed","*.j2"]
104
+ devdox_ai_locust = ["schemas/*.json", "templates/*.j2","templates/mongo/*","prompt/*.j2", "py.typed","*.j2"]
105
105
 
106
106
  # flake8 configuration (legacy - remove if using ruff)
107
107
  [tool.flake8]
@@ -6,7 +6,7 @@ from datetime import datetime, timezone
6
6
  from typing import Optional, Tuple, Union, List, Dict, Any
7
7
  from rich.console import Console
8
8
  from rich.table import Table
9
- from together import Together
9
+ from together import AsyncTogether
10
10
 
11
11
  from .hybrid_loctus_generator import HybridLocustGenerator
12
12
  from .config import Settings
@@ -197,19 +197,20 @@ async def _generate_and_create_tests(
197
197
  custom_requirement: Optional[str] = "",
198
198
  host: Optional[str] = "0.0.0.0",
199
199
  auth: bool = False,
200
+ db_type: str = "",
200
201
  ) -> List[Dict[Any, Any]]:
201
202
  """Generate tests using AI and create test files"""
202
- together_client = Together(api_key=api_key)
203
+ together_client = AsyncTogether(api_key=api_key)
203
204
 
204
205
  with console.status("[bold green]Generating Locust tests with AI..."):
205
206
  generator = HybridLocustGenerator(ai_client=together_client)
206
-
207
207
  test_files, test_directories = await generator.generate_from_endpoints(
208
208
  endpoints=endpoints,
209
209
  api_info=api_info,
210
210
  custom_requirement=custom_requirement,
211
211
  target_host=host,
212
212
  include_auth=auth,
213
+ db_type=db_type,
213
214
  )
214
215
 
215
216
  # Create test files
@@ -271,6 +272,12 @@ def cli(ctx: click.Context, verbose: bool) -> None:
271
272
  )
272
273
  @click.option("--host", "-H", type=str, help="Target host URL")
273
274
  @click.option("--auth/--no-auth", default=True, help="Include authentication in tests")
275
+ @click.option(
276
+ "--db-type",
277
+ type=click.Choice(["", "mongo", "postgresql"], case_sensitive=False),
278
+ default="",
279
+ help="Database type for testing (empty for no database, mongo, or postgresql)",
280
+ )
274
281
  @click.option("--dry-run", is_flag=True, help="Generate tests without running them")
275
282
  @click.option(
276
283
  "--custom-requirement", type=str, help="Custom requirements for test generation"
@@ -291,6 +298,7 @@ def generate(
291
298
  run_time: str,
292
299
  host: Optional[str],
293
300
  auth: bool,
301
+ db_type: str,
294
302
  dry_run: bool,
295
303
  custom_requirement: Optional[str],
296
304
  together_api_key: Optional[str],
@@ -309,6 +317,7 @@ def generate(
309
317
  run_time,
310
318
  host,
311
319
  auth,
320
+ db_type,
312
321
  dry_run,
313
322
  custom_requirement,
314
323
  together_api_key,
@@ -332,6 +341,7 @@ async def _async_generate(
332
341
  run_time: str,
333
342
  host: Optional[str],
334
343
  auth: bool,
344
+ db_type: str,
335
345
  dry_run: bool,
336
346
  custom_requirement: Optional[str],
337
347
  together_api_key: Optional[str],
@@ -343,7 +353,6 @@ async def _async_generate(
343
353
  try:
344
354
  _, api_key = _initialize_config(together_api_key)
345
355
  output_dir = _setup_output_directory(output)
346
-
347
356
  # Display configuration
348
357
  if ctx.obj["verbose"]:
349
358
  _display_configuration(
@@ -363,7 +372,14 @@ async def _async_generate(
363
372
  )
364
373
 
365
374
  created_files = await _generate_and_create_tests(
366
- api_key, endpoints, api_info, output_dir, custom_requirement, host, auth
375
+ api_key,
376
+ endpoints,
377
+ api_info,
378
+ output_dir,
379
+ custom_requirement,
380
+ host,
381
+ auth,
382
+ db_type,
367
383
  )
368
384
 
369
385
  # Show results
@@ -8,7 +8,7 @@ from pydantic_settings import BaseSettings
8
8
  class Settings(BaseSettings):
9
9
  """Application settings."""
10
10
 
11
- VERSION: str = "0.1.2"
11
+ VERSION: str = "0.1.7"
12
12
 
13
13
  API_KEY: str = "" # Fallback for backward compatibility
14
14
 
@@ -19,12 +19,22 @@ import shutil
19
19
  from devdox_ai_locust.utils.open_ai_parser import Endpoint
20
20
  from devdox_ai_locust.utils.file_creation import FileCreationConfig, SafeFileCreator
21
21
  from devdox_ai_locust.locust_generator import LocustTestGenerator, TestDataConfig
22
- from together import Together
22
+ from together import AsyncTogether
23
23
 
24
24
  logger = logging.getLogger(__name__)
25
25
 
26
26
 
27
27
  test_data_file_path = "test_data.py"
28
+ data_provider_path = "data_provider.py"
29
+
30
+
31
+ @dataclass
32
+ class ErrorClassification:
33
+ """Classification of an error for retry logic"""
34
+
35
+ is_retryable: bool
36
+ backoff_seconds: float
37
+ error_type: str
28
38
 
29
39
 
30
40
  @dataclass
@@ -109,6 +119,7 @@ class EnhancementProcessor:
109
119
  base_files: Dict[str, str],
110
120
  directory_files: List[Dict[str, Any]],
111
121
  grouped_endpoints: Dict[str, List[Endpoint]],
122
+ db_type: str = "",
112
123
  ) -> Tuple[List[Dict[str, Any]], List[str]]:
113
124
  """Process workflow enhancements"""
114
125
  enhanced_directory_files: List[Dict[str, Any]] = []
@@ -125,10 +136,13 @@ class EnhancementProcessor:
125
136
  first_workflow = base_workflow_files[0]
126
137
  # Get the content from the dictionary - adjust key name as needed
127
138
  base_workflow_content = first_workflow.get("base_workflow.py", "")
128
-
129
139
  for workflow_item in directory_files:
130
140
  enhanced_workflow_item = await self._enhance_single_workflow(
131
- workflow_item, base_files, base_workflow_content, grouped_endpoints
141
+ workflow_item,
142
+ base_files,
143
+ base_workflow_content,
144
+ grouped_endpoints,
145
+ db_type,
132
146
  )
133
147
  if enhanced_workflow_item:
134
148
  enhanced_directory_files.append(enhanced_workflow_item["files"])
@@ -142,6 +156,7 @@ class EnhancementProcessor:
142
156
  base_files: Dict[str, str],
143
157
  base_workflow_files: str,
144
158
  grouped_endpoints: Dict[str, List[Endpoint]],
159
+ db_type: str = "",
145
160
  ) -> Dict[str, Any] | None:
146
161
  """Enhance a single workflow file"""
147
162
  for key, value in workflow_item.items():
@@ -155,6 +170,7 @@ class EnhancementProcessor:
155
170
  base_workflow=base_workflow_files,
156
171
  grouped_enpoints=workflow_endpoints_dict,
157
172
  auth_endpoints=auth_endpoints,
173
+ db_type=db_type,
158
174
  )
159
175
  if enhanced_workflow:
160
176
  return {
@@ -165,14 +181,19 @@ class EnhancementProcessor:
165
181
  return None
166
182
 
167
183
  async def process_test_data_enhancement(
168
- self, base_files: Dict[str, str], endpoints: List[Endpoint]
184
+ self, base_files: Dict[str, str], endpoints: List[Endpoint], db_type: str = ""
169
185
  ) -> Tuple[Dict[str, str], List[str]]:
170
186
  """Process test data enhancement"""
171
187
  enhanced_files = {}
172
188
  enhancements = []
173
189
  if self.ai_config and self.ai_config.enhance_test_data:
174
190
  enhanced_test_data = await self.locust_generator.enhance_test_data_file(
175
- base_files.get(test_data_file_path, ""), endpoints
191
+ base_files.get(test_data_file_path, ""),
192
+ endpoints,
193
+ db_type,
194
+ base_files.get(data_provider_path, ""),
195
+ base_files.get("db_config.py", ""),
196
+ data_provider_path,
176
197
  )
177
198
  if enhanced_test_data:
178
199
  enhanced_files[test_data_file_path] = enhanced_test_data
@@ -202,7 +223,7 @@ class HybridLocustGenerator:
202
223
 
203
224
  def __init__(
204
225
  self,
205
- ai_client: Together,
226
+ ai_client: AsyncTogether,
206
227
  ai_config: Optional[AIEnhancementConfig] = None,
207
228
  test_config: Optional[TestDataConfig] = None,
208
229
  prompt_dir: str = "prompt",
@@ -211,7 +232,12 @@ class HybridLocustGenerator:
211
232
  self.ai_config = ai_config or AIEnhancementConfig()
212
233
  self.template_generator = LocustTestGenerator(test_config)
213
234
  self.prompt_dir = self._find_project_root() / prompt_dir
235
+ self._api_semaphore = asyncio.Semaphore(5)
214
236
  self._setup_jinja_env()
237
+ self.MAX_RETRIES = 3
238
+ self.RATE_LIMIT_BACKOFF = 10
239
+ self.NON_RETRYABLE_CODES = ["401", "403", "unauthorized", "forbidden"]
240
+ self.RATE_LIMIT_INDICATORS = ["429", "rate limit"]
215
241
 
216
242
  def _find_project_root(self) -> Path:
217
243
  """Find the project root by looking for setup.py, pyproject.toml, or .git"""
@@ -229,6 +255,45 @@ class HybridLocustGenerator:
229
255
  autoescape=False,
230
256
  )
231
257
 
258
+ def _classify_error(self, error: Exception, attempt: int) -> ErrorClassification:
259
+ """
260
+ Classify an error to determine retry behavior.
261
+
262
+ Args:
263
+ error: The exception that occurred
264
+ attempt: Current attempt number (0-indexed)
265
+
266
+ Returns:
267
+ ErrorClassification with retry decision and backoff time
268
+ """
269
+ error_str = str(error).lower()
270
+
271
+ # Non-retryable errors (auth/permission)
272
+ if any(code in error_str for code in self.NON_RETRYABLE_CODES):
273
+ logger.error(f"Authentication error, not retrying: {error}")
274
+ return ErrorClassification(
275
+ is_retryable=False, backoff_seconds=0, error_type="auth"
276
+ )
277
+
278
+ # Rate limit errors (retryable with longer backoff)
279
+ if any(indicator in error_str for indicator in self.RATE_LIMIT_INDICATORS):
280
+ logger.warning(f"Rate limit hit on attempt {attempt + 1}")
281
+ return ErrorClassification(
282
+ is_retryable=True,
283
+ backoff_seconds=self.RATE_LIMIT_BACKOFF,
284
+ error_type="rate_limit",
285
+ )
286
+
287
+ # Other retryable errors (exponential backoff)
288
+ logger.warning(
289
+ f"Retryable error on attempt {attempt + 1}: {type(error).__name__}"
290
+ )
291
+ return ErrorClassification(
292
+ is_retryable=True,
293
+ backoff_seconds=2**attempt, # Exponential: 1s, 2s, 4s
294
+ error_type="retryable",
295
+ )
296
+
232
297
  async def generate_from_endpoints(
233
298
  self,
234
299
  endpoints: List[Endpoint],
@@ -236,6 +301,7 @@ class HybridLocustGenerator:
236
301
  custom_requirement: Optional[str] = None,
237
302
  target_host: Optional[str] = None,
238
303
  include_auth: bool = True,
304
+ db_type: str = "",
239
305
  ) -> Tuple[Dict[str, str], List[Dict[str, Any]]]:
240
306
  """
241
307
  Generate Locust tests using hybrid approach
@@ -255,6 +321,7 @@ class HybridLocustGenerator:
255
321
  api_info,
256
322
  include_auth=include_auth,
257
323
  target_host=target_host,
324
+ db_type=db_type,
258
325
  )
259
326
  )
260
327
 
@@ -269,6 +336,7 @@ class HybridLocustGenerator:
269
336
  directory_files,
270
337
  grouped_enpoints,
271
338
  custom_requirement,
339
+ db_type,
272
340
  )
273
341
  if enhancement_result.success:
274
342
  logger.info(
@@ -369,6 +437,7 @@ class HybridLocustGenerator:
369
437
  directory_files: List[Dict[str, Any]],
370
438
  grouped_endpoints: Dict[str, List[Endpoint]],
371
439
  custom_requirement: Optional[str] = None,
440
+ db_type: str = "",
372
441
  ) -> EnhancementResult:
373
442
  """Enhance base files with AI - Refactored for reduced cognitive complexity"""
374
443
  start_time = asyncio.get_event_loop().time()
@@ -381,6 +450,7 @@ class HybridLocustGenerator:
381
450
  directory_files,
382
451
  grouped_endpoints,
383
452
  custom_requirement,
453
+ db_type,
384
454
  )
385
455
 
386
456
  processing_time = asyncio.get_event_loop().time() - start_time
@@ -409,6 +479,7 @@ class HybridLocustGenerator:
409
479
  directory_files: List[Dict[str, Any]],
410
480
  grouped_endpoints: Dict[str, List[Endpoint]],
411
481
  custom_requirement: Optional[str] = None,
482
+ db_type: str = "",
412
483
  ) -> EnhancementResult:
413
484
  """Process all enhancements using the enhancement processor"""
414
485
  processor = EnhancementProcessor(self.ai_config, self)
@@ -423,7 +494,7 @@ class HybridLocustGenerator:
423
494
  processor.process_domain_flows_enhancement(
424
495
  endpoints, api_info, custom_requirement
425
496
  ),
426
- processor.process_test_data_enhancement(base_files, endpoints),
497
+ processor.process_test_data_enhancement(base_files, endpoints, db_type),
427
498
  processor.process_validation_enhancement(base_files, endpoints),
428
499
  ]
429
500
 
@@ -448,7 +519,7 @@ class HybridLocustGenerator:
448
519
  workflow_files,
449
520
  workflow_enhancements,
450
521
  ) = await processor.process_workflow_enhancements(
451
- base_files, directory_files, grouped_endpoints
522
+ base_files, directory_files, grouped_endpoints, db_type
452
523
  )
453
524
  enhanced_directory_files.extend(workflow_files)
454
525
  enhancements_applied.extend(workflow_enhancements)
@@ -504,6 +575,7 @@ class HybridLocustGenerator:
504
575
  base_workflow: str,
505
576
  grouped_enpoints: Dict[str, List[Endpoint]],
506
577
  auth_endpoints: List[Endpoint],
578
+ db_type: str = "",
507
579
  ) -> Optional[str]:
508
580
  try:
509
581
  template = self.jinja_env.get_template("workflow.j2")
@@ -515,6 +587,7 @@ class HybridLocustGenerator:
515
587
  base_workflow=base_workflow,
516
588
  auth_endpoints=auth_endpoints,
517
589
  base_content=base_content,
590
+ db_type=db_type,
518
591
  )
519
592
  enhanced_content = await self._call_ai_service(prompt)
520
593
  return enhanced_content
@@ -524,7 +597,13 @@ class HybridLocustGenerator:
524
597
  return ""
525
598
 
526
599
  async def enhance_test_data_file(
527
- self, base_content: str, endpoints: List[Endpoint]
600
+ self,
601
+ base_content: str,
602
+ endpoints: List[Endpoint],
603
+ db_type: str = "",
604
+ data_provider: str = "",
605
+ db_config: str = "",
606
+ data_provider_path: str = "",
528
607
  ) -> Optional[str]:
529
608
  """Enhance test data generation with domain knowledge"""
530
609
 
@@ -539,11 +618,14 @@ class HybridLocustGenerator:
539
618
  "base_content": base_content,
540
619
  "schemas_info": schemas_info,
541
620
  "endpoints": endpoints,
621
+ "db_type": db_type,
622
+ "data_provider_content": data_provider,
623
+ "db_config": db_config,
624
+ "data_provider_path": data_provider_path,
542
625
  }
543
626
 
544
627
  # Render enhanced content
545
628
  prompt = template.render(**context)
546
-
547
629
  enhanced_content = await self._call_ai_service(prompt)
548
630
  if enhanced_content and self._validate_python_code(enhanced_content):
549
631
  return enhanced_content
@@ -573,10 +655,8 @@ class HybridLocustGenerator:
573
655
 
574
656
  return ""
575
657
 
576
- async def _call_ai_service(self, prompt: str) -> Optional[str]:
577
- """Call AI service with retry logic and validation"""
578
-
579
- messages = [
658
+ def _build_messages(self, prompt: str) -> list[dict]:
659
+ return [
580
660
  {
581
661
  "role": "system",
582
662
  "content": "You are an expert Python developer specializing in Locust load testing. Generate clean, production-ready code with proper error handling. "
@@ -586,30 +666,42 @@ class HybridLocustGenerator:
586
666
  {"role": "user", "content": prompt},
587
667
  ]
588
668
 
589
- for attempt in range(3): # Retry logic
590
- try:
591
- response = await asyncio.wait_for(
592
- asyncio.to_thread(
593
- self.ai_client.chat.completions.create,
594
- model=self.ai_config.model,
595
- messages=messages,
596
- max_tokens=self.ai_config.max_tokens,
597
- temperature=self.ai_config.temperature,
598
- top_p=0.9,
599
- top_k=40,
600
- repetition_penalty=1.1,
601
- ),
602
- timeout=self.ai_config.timeout,
669
+ async def _make_api_call(self, messages: list[dict]) -> Optional[str]:
670
+ """Make API call - ONE job"""
671
+ async with self._api_semaphore:
672
+ api_call = self.ai_client.chat.completions.create(
673
+ model=self.ai_config.model,
674
+ messages=messages,
675
+ max_tokens=self.ai_config.max_tokens,
676
+ temperature=self.ai_config.temperature,
677
+ top_p=0.9,
678
+ top_k=40,
679
+ repetition_penalty=1.1,
680
+ )
681
+
682
+ # Wait for the API call with timeout
683
+ response = await asyncio.wait_for(
684
+ api_call,
685
+ timeout=self.ai_config.timeout,
686
+ )
687
+ if response.choices and response.choices[0].message:
688
+ content = response.choices[0].message.content.strip()
689
+ # Clean up the response
690
+ content = self._clean_ai_response(
691
+ self.extract_code_from_response(content)
603
692
  )
693
+ return content
604
694
 
605
- if response.choices and response.choices[0].message:
606
- content = response.choices[0].message.content.strip()
695
+ return None
607
696
 
608
- # Clean up the response
609
- content = self._clean_ai_response(
610
- self.extract_code_from_response(content)
611
- )
697
+ async def _call_ai_service(self, prompt: str) -> Optional[str]:
698
+ """Call AI service with retry logic and validation"""
699
+ messages = self._build_messages(prompt)
612
700
 
701
+ for attempt in range(self.MAX_RETRIES): # Retry logic
702
+ try:
703
+ async with self._api_semaphore:
704
+ content = await self._make_api_call(messages)
613
705
  if content:
614
706
  return content
615
707
 
@@ -617,24 +709,41 @@ class HybridLocustGenerator:
617
709
  logger.warning(f"AI service timeout on attempt {attempt + 1}")
618
710
 
619
711
  except Exception as e:
620
- logger.warning(f"AI service error on attempt {attempt + 1}: {e}")
712
+ classification = self._classify_error(e, attempt) # Helper 3
713
+
714
+ if not classification.is_retryable:
715
+ return ""
716
+
717
+ if attempt < self.MAX_RETRIES - 1:
718
+ await asyncio.sleep(classification.backoff_seconds)
621
719
 
622
- if attempt < 2: # Wait before retry
720
+ continue
721
+
722
+ if attempt < self.MAX_RETRIES - 1:
623
723
  await asyncio.sleep(2**attempt)
624
724
 
625
725
  return ""
626
726
 
627
727
  def extract_code_from_response(self, response_text: str) -> str:
628
728
  # Extract content between <code> tags
729
+ pattern = r"<code>(.*?)</code>"
730
+ matches = re.findall(pattern, response_text, re.DOTALL)
629
731
 
630
- code_match = re.search(r"<code>(.*?)</code>", response_text, re.DOTALL)
631
- if code_match:
632
- content = code_match.group(1).strip()
633
- # Additional validation - ensure we got actual content
634
- if content and len(content) > 0:
635
- return content
732
+ if not matches:
733
+ logger.warning("No <code> tags found, using full response")
734
+ return response_text.strip()
735
+
736
+ content = max(matches, key=len).strip()
737
+
738
+ # Content too short - use full response
739
+ if not content or len(content) <= 10:
740
+ logger.warning(
741
+ f"Code in tags too short ({len(content)} chars), using full response"
742
+ )
743
+ return response_text.strip()
636
744
 
637
- return response_text.strip()
745
+ logger.debug(f"Extracted {len(content)} chars from <code> tags")
746
+ return str(content)
638
747
 
639
748
  def _clean_ai_response(self, content: str) -> str:
640
749
  """Clean and validate AI response"""