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,732 @@
1
+ """
2
+ Locust Test Generator
3
+
4
+ Generates Locust performance test files from parsed OpenAPI endpoints.
5
+ """
6
+
7
+ import json
8
+ import re
9
+ import secrets
10
+ from typing import List, Tuple, Dict, Any, Optional
11
+ import black
12
+ from jinja2 import Environment, FileSystemLoader
13
+ from pathlib import Path
14
+ import logging
15
+ from dataclasses import dataclass
16
+ from datetime import datetime
17
+
18
+
19
+ from devdox_ai_locust.utils.open_ai_parser import Endpoint, Parameter
20
+
21
+ logger = logging.getLogger(__name__)
22
+
23
+
24
+ @dataclass
25
+ class TestDataConfig:
26
+ """Configuration for test data generation"""
27
+
28
+ string_length: int = 10
29
+ integer_min: int = 1
30
+ integer_max: int = 1000
31
+ array_size: int = 3
32
+ use_realistic_data: bool = True
33
+
34
+
35
+ class LocustTestGenerator:
36
+ """Generates Locust performance test files from OpenAPI endpoints"""
37
+
38
+ def __init__(
39
+ self,
40
+ test_config: Optional[TestDataConfig] = None,
41
+ jinja_env: Optional[Environment] = None,
42
+ ):
43
+ self.test_config = test_config or TestDataConfig()
44
+ self.generated_files: Dict[str, str] = {}
45
+ self.auth_token = None
46
+ self.user_data: Dict[str, Any] = {}
47
+ self.request_count = 0
48
+
49
+ self.template_dir = self._find_project_root() / "templates"
50
+
51
+ self.template_dir.mkdir(parents=True, exist_ok=True)
52
+
53
+ self.jinja_env = jinja_env or self._setup_templates()
54
+
55
+ def _find_project_root(self) -> Path:
56
+ """Find the project root by looking for setup.py, pyproject.toml, or .git"""
57
+ current_path = Path(__file__).parent
58
+
59
+ return current_path
60
+
61
+ def _setup_templates(self) -> Environment:
62
+ """Initialize Jinja2 environment and create default templates"""
63
+ return Environment(
64
+ loader=FileSystemLoader(self.template_dir),
65
+ trim_blocks=True,
66
+ lstrip_blocks=True,
67
+ keep_trailing_newline=True,
68
+ autoescape=False,
69
+ )
70
+
71
+ def fix_indent(self, base_files: Dict[str, str]) -> Dict[str, str]:
72
+ """Fix indentation for generated files"""
73
+ try:
74
+ mode = black.Mode()
75
+ updated_data = {}
76
+
77
+ for key, value in base_files.items():
78
+ try:
79
+ formatted_code = black.format_str(value, mode=mode)
80
+
81
+ updated_data[key] = formatted_code
82
+ except black.InvalidInput:
83
+ # Not valid Python code, keep original
84
+ logger.debug(f"Skipping formatting for {key}: not valid Python")
85
+ updated_data[key] = value
86
+ except Exception as format_error:
87
+ # Other Black formatting errors, keep original and log
88
+ logger.warning(f"Failed to format {key}: {format_error}")
89
+ updated_data[key] = value
90
+
91
+ return updated_data
92
+
93
+ except Exception as e:
94
+ logger.error(f"exception occurred: {e}")
95
+ return base_files
96
+
97
+ def generate_from_endpoints(
98
+ self,
99
+ endpoints: List[Endpoint],
100
+ api_info: Dict[str, Any],
101
+ include_auth: bool = True,
102
+ target_host: Optional[str] = None,
103
+ ) -> Tuple[Dict[str, str], List[Dict[str, Any]], Dict[str, List[Endpoint]]]:
104
+ """
105
+ Generate complete Locust test suite from parsed endpoints
106
+
107
+ Args:
108
+ endpoints: List of parsed Endpoint objects
109
+ api_info: API information dictionary
110
+ output_dir: Output directory for generated files
111
+
112
+ Returns:
113
+ Dictionary of filename -> file content
114
+ """
115
+ try:
116
+ grouped_enpoint = self._group_endpoints_by_tag(endpoints, include_auth)
117
+
118
+ workflows_files = self.generate_workflows(grouped_enpoint, api_info)
119
+
120
+ self.generated_files = {
121
+ "locustfile.py": self._generate_main_locustfile(
122
+ endpoints, api_info, list(grouped_enpoint.keys())
123
+ ),
124
+ "test_data.py": self._generate_test_data_file(),
125
+ "config.py": self._generate_config_file(api_info),
126
+ "utils.py": self._generate_utils_file(),
127
+ "custom_flows.py": self._generate_custom_flows_file(),
128
+ "requirements.txt": self._generate_requirements_file(),
129
+ "README.md": self._generate_readme_file(api_info),
130
+ ".env.example": self._generate_env_example(api_info, target_host),
131
+ }
132
+
133
+ return self.generated_files, workflows_files, grouped_enpoint
134
+ except Exception as e:
135
+ logger.error(f"Failed to generate test suite: {e}")
136
+ empty_files: Dict[str, str] = {}
137
+ empty_workflows: List[Dict[str, Any]] = []
138
+ empty_grouped: Dict[str, List[Endpoint]] = {}
139
+ return empty_files, empty_workflows, empty_grouped
140
+
141
+ def generate_workflows(
142
+ self, endpoints: Dict[str, List[Any]], api_info: Dict[str, Any]
143
+ ) -> List[Dict[str, Any]]:
144
+ """
145
+ Generate the workflows Locust test files with proper structure and no duplicates
146
+ """
147
+ try:
148
+ workflows: List = []
149
+
150
+ for group, group_endpoints in endpoints.items():
151
+ task_methods: List[str] = []
152
+
153
+ for endpoint in group_endpoints:
154
+ try:
155
+ task_method = self._generate_task_method(endpoint)
156
+ if task_method:
157
+ task_methods.append(task_method)
158
+ except Exception as e:
159
+ logger.warning(
160
+ f"⚠️ Failed to generate task method for {getattr(endpoint, 'path', '?')}: {e}"
161
+ )
162
+ continue
163
+ if not task_methods:
164
+ logger.warning(f"No task methods generated from group {group}")
165
+ task_methods.append(self._generate_default_task_method())
166
+ indented_task_methods = self._indent_methods(
167
+ task_methods, indent_level=1
168
+ )
169
+ file_content = self._build_endpoint_template(
170
+ api_info, indented_task_methods, group
171
+ )
172
+ file_name = f"{group}_workflow.py".replace("-", "_")
173
+
174
+ workflows.append({file_name: file_content})
175
+
176
+ workflows.append(
177
+ {"base_workflow.py": self.generate_base_common_file(api_info)}
178
+ )
179
+
180
+ return workflows
181
+
182
+ except Exception as e:
183
+ logger.error(f"❌ Failed to generate test suite: {e}")
184
+
185
+ return []
186
+
187
+ def _build_endpoint_template(
188
+ self, api_info: Dict[str, Any], task_methods_content: str, group: str
189
+ ) -> str:
190
+ template = self.jinja_env.get_template("endpoint_template.py.j2")
191
+ return template.render(
192
+ api_info=api_info, group=group, task_methods_content=task_methods_content
193
+ )
194
+
195
+ def _generate_main_locustfile(
196
+ self, endpoints: List[Any], api_info: Dict[str, Any], groups: List[str]
197
+ ) -> str:
198
+ """
199
+ Generate the main Locust test file with proper structure and no duplicates
200
+
201
+ Args:
202
+ endpoints: List of parsed Endpoint objects
203
+ api_info: API information dictionary
204
+
205
+ Returns:
206
+ Complete locustfile.py content as string
207
+ """
208
+ try:
209
+ # Generate task methods for each endpoint
210
+ task_methods = []
211
+ for endpoint in endpoints:
212
+ try:
213
+ task_method = self._generate_task_method(endpoint)
214
+ if task_method:
215
+ task_methods.append(task_method)
216
+ except Exception as e:
217
+ logger.warning(
218
+ f"Failed to generate task method for {endpoint.path}: {e}"
219
+ )
220
+ continue
221
+
222
+ if not task_methods:
223
+ logger.warning("No task methods generated from endpoints")
224
+ # Generate a default task method
225
+ task_methods.append(self._generate_default_task_method())
226
+
227
+ # Properly indent task methods for class inclusion
228
+ indented_task_methods = self._indent_methods(task_methods, indent_level=1)
229
+ indented_task_methods = ""
230
+ # Generate the complete file content
231
+ return self._build_locustfile_template(
232
+ api_info=api_info,
233
+ task_methods_content=indented_task_methods,
234
+ groups=groups,
235
+ )
236
+
237
+ except Exception as e:
238
+ logger.error(f"Failed to generate test suite: {e}")
239
+ # Return fallback files
240
+ return self._generate_fallback_locustfile(api_info)
241
+
242
+ def _indent_methods(self, task_methods: List[str], indent_level: int = 1) -> str:
243
+ """Properly indent task methods for class inclusion"""
244
+ indented_methods = []
245
+ for method in task_methods:
246
+ lines = method.split("\n")
247
+ indented_lines = []
248
+ first_nonempty = True
249
+
250
+ for line in lines:
251
+ if line.strip():
252
+ stripped_line = line.lstrip()
253
+ if first_nonempty:
254
+ # First non-empty line → indent once (for @task or def)
255
+ indented_lines.append(" " * indent_level + stripped_line)
256
+ first_nonempty = False
257
+ else:
258
+ # Method body → indent deeper
259
+ indented_lines.append(
260
+ " " * (indent_level + 1) + stripped_line
261
+ )
262
+ else:
263
+ indented_lines.append("")
264
+ indented_methods.append("\n".join(indented_lines))
265
+
266
+ return "\n\n".join(indented_methods)
267
+
268
+ def _generate_fallback_locustfile(self, api_info: Dict[str, Any]) -> str:
269
+ """Generate a basic fallback locustfile when main generation fails"""
270
+
271
+ template = self.jinja_env.get_template("fallback_locust.py.j2")
272
+ return template.render(api_info=api_info)
273
+
274
+ from typing import Dict, Any
275
+
276
+ def _build_locustfile_template(
277
+ self, api_info: Dict[str, Any], task_methods_content: str, groups: List[str]
278
+ ) -> str:
279
+ import_group_tasks = ""
280
+ tasks = []
281
+ for group in groups:
282
+ file_name = group.lower().replace("-", "_")
283
+ class_name = group.replace("-", "")
284
+ import_group_tasks += f"""from workflows.{file_name}_workflow import {class_name}TaskMethods\n"""
285
+ tasks.append(f"{class_name}TaskMethods")
286
+ tasks_str = "[" + ",".join(tasks) + "]"
287
+ template = self.jinja_env.get_template("locust.py.j2")
288
+
289
+ # Prepare template context
290
+ context = {
291
+ "import_group_tasks": import_group_tasks,
292
+ "task_methods_content": task_methods_content,
293
+ "tasks_str": tasks_str,
294
+ "api_info": api_info,
295
+ "generated_task_classes": self._generate_user_classes(),
296
+ "generated_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
297
+ }
298
+
299
+ # Render the template
300
+ content = template.render(**context)
301
+
302
+ logger.info("README generated successfully using template")
303
+ return content
304
+
305
+ def _generate_default_task_method(self) -> str:
306
+ """Generate a default task method when no endpoints are available"""
307
+ return '''@task(1)
308
+ def default_health_check(self):
309
+ """Default health check task"""
310
+ try:
311
+ response_data = self.make_request(
312
+ method="get",
313
+ path="/health"
314
+ )
315
+
316
+ if response_data:
317
+ self._store_response_data("health_check", response_data)
318
+
319
+ except Exception as e:
320
+ logger.error(f"Health check task failed: {e}")
321
+ '''
322
+
323
+ def _generate_task_method(self, endpoint: Any) -> str:
324
+ """Generate a Locust task method for a single endpoint with improved structure"""
325
+ try:
326
+ method_name = self._generate_method_name(endpoint)
327
+ path_with_params = self._generate_path_with_params(endpoint)
328
+ weight = self._get_task_weight(getattr(endpoint, "method", "GET"))
329
+
330
+ # Build the task method with proper error handling
331
+ task_method = f'''@task({weight})
332
+ def {method_name}(self):
333
+ """
334
+ {getattr(endpoint, "summary", f"{getattr(endpoint, 'method', 'GET')} {getattr(endpoint, 'path', '')}")}
335
+ {getattr(endpoint, "description", "")}
336
+ """
337
+ try:
338
+ # Generate path parameters
339
+ {self._generate_path_params_code(endpoint)}
340
+
341
+ # Generate query parameters
342
+ {self._generate_query_params_code(endpoint)}
343
+
344
+ # Generate request body
345
+ {self._generate_request_body_code(endpoint)}
346
+
347
+ # Make the request
348
+ response_data = self.make_request(
349
+ method="{getattr(endpoint, "method", "GET").lower()}",
350
+ path=f"{path_with_params}",
351
+ {self._generate_request_kwargs(endpoint)}
352
+ )
353
+
354
+ if response_data:
355
+ # Store response data for dependent requests
356
+ self._store_response_data("{method_name}", response_data)
357
+
358
+ except Exception as e:
359
+ logger.error(f"Task {method_name} failed: {{e}}")
360
+ '''
361
+ return task_method
362
+
363
+ except Exception as e:
364
+ logger.error(f"Failed to generate task method for 535 endpoint: {e}")
365
+ return ""
366
+
367
+ def _generate_method_name(self, endpoint: Endpoint) -> str:
368
+ """Generate a valid Python method name from endpoint"""
369
+ if endpoint.operation_id:
370
+ # Use operation ID if available
371
+ name = endpoint.operation_id
372
+ else:
373
+ # Generate name from method and path
374
+ path_parts = [
375
+ part
376
+ for part in endpoint.path.split("/")
377
+ if part and not part.startswith("{")
378
+ ]
379
+ name = f"{endpoint.method.lower()}_{'_'.join(path_parts)}"
380
+
381
+ # Clean up the name
382
+ name = re.sub(r"[^\w]", "_", name)
383
+ name = re.sub(r"_+", "_", name)
384
+ name = name.strip("_")
385
+
386
+ return name if name else f"{endpoint.method.lower()}_endpoint"
387
+
388
+ def _generate_path_with_params(self, endpoint: Endpoint) -> str:
389
+ """Generate path with parameter placeholders"""
390
+ path = endpoint.path
391
+
392
+ # Replace path parameters with f-string format
393
+ for param in endpoint.parameters:
394
+ if param.location.value == "path":
395
+ path = path.replace(f"{{{param.name}}}", f"{{{param.name}}}")
396
+
397
+ return path
398
+
399
+ def _generate_path_params_code(self, endpoint: Endpoint) -> str:
400
+ """Generate code for path parameters"""
401
+ path_params = [p for p in endpoint.parameters if p.location.value == "path"]
402
+
403
+ if not path_params:
404
+ return "# No path parameters"
405
+
406
+ code_lines = []
407
+ for param in path_params:
408
+ if param.type.startswith("integer"):
409
+ code_lines.append(f"{param.name} = data_generator.generate_integer()")
410
+ elif param.type == "string":
411
+ if "id" in param.name.lower():
412
+ code_lines.append(f"{param.name} = data_generator.generate_id()")
413
+ else:
414
+ code_lines.append(
415
+ f"{param.name} = data_generator.generate_string()"
416
+ )
417
+
418
+ else:
419
+ code_lines.append(
420
+ f'{param.name} = data_generator.generate_value("{param.type}")'
421
+ )
422
+
423
+ return "\n".join(code_lines)
424
+
425
+ def _generate_query_params_code(self, endpoint: Endpoint) -> str:
426
+ """Generate code for query parameters"""
427
+ query_params = [p for p in endpoint.parameters if p.location.value == "query"]
428
+
429
+ if not query_params:
430
+ return "params = {}"
431
+
432
+ param_lines = []
433
+ for param in query_params:
434
+ if self._should_skip_optional_param(param):
435
+ continue
436
+
437
+ param_line = self._generate_param_line(param)
438
+ param_lines.append(param_line)
439
+
440
+ return self._format_params_dict(param_lines)
441
+
442
+ def _should_skip_optional_param(self, param: Parameter) -> bool:
443
+ """Randomly skip optional parameters 30% of the time"""
444
+ return not param.required and secrets.randbelow(100) > 70
445
+
446
+ def _generate_param_line(self, param: Parameter) -> str:
447
+ """Generate a single parameter line based on its type"""
448
+ param_generators = {
449
+ "integer": self._generate_integer_param,
450
+ "string": self._generate_string_param,
451
+ "boolean": self._generate_boolean_param,
452
+ }
453
+
454
+ # Handle integer types that might have prefixes like "integer64"
455
+ param_type = "integer" if param.type.startswith("integer") else param.type
456
+
457
+ generator = param_generators.get(param_type, self._generate_generic_param)
458
+ return generator(param)
459
+
460
+ def _generate_integer_param(self, param: Parameter) -> str:
461
+ """Generate integer parameter line"""
462
+ default = param.default if param.default is not None else "None"
463
+ return f'"{param.name}": data_generator.generate_integer(default={default}),'
464
+
465
+ def _generate_string_param(self, param: Parameter) -> str:
466
+ """Generate string parameter line"""
467
+ default = f'"{param.default}"' if param.default else "None"
468
+ return f'"{param.name}": data_generator.generate_string(default={default}),'
469
+
470
+ def _generate_boolean_param(self, param: Parameter) -> str:
471
+ """Generate boolean parameter line"""
472
+ return f'"{param.name}": data_generator.generate_boolean(),'
473
+
474
+ def _generate_generic_param(self, param: Parameter) -> str:
475
+ """Generate generic parameter line for unknown types"""
476
+ return f'"{param.name}": data_generator.generate_value("{param.type}"),'
477
+
478
+ def _format_params_dict(self, param_lines: list[str]) -> str:
479
+ """Format parameter lines into a dictionary structure"""
480
+ if not param_lines:
481
+ return "params = {}"
482
+
483
+ lines = ["params = {"] + [f" {line}" for line in param_lines] + ["}"]
484
+ return "\n".join(lines)
485
+
486
+ def _generate_request_body_code(self, endpoint: Endpoint) -> str:
487
+ """Generate code for request body"""
488
+ if not endpoint.request_body:
489
+ return "json_data = None"
490
+
491
+ if endpoint.request_body.content_type == "application/json":
492
+ return f"""json_data = data_generator.generate_json_data(
493
+ schema={json.dumps(endpoint.request_body.schema, indent=16)}
494
+ )"""
495
+ elif endpoint.request_body.content_type == "application/x-www-form-urlencoded":
496
+ return "data = data_generator.generate_form_data()"
497
+ else:
498
+ return "json_data = {}"
499
+
500
+ def _generate_request_kwargs(self, endpoint: Endpoint) -> str:
501
+ """Generate kwargs for the request method"""
502
+ kwargs = []
503
+
504
+ # Add query parameters
505
+ query_params = [p for p in endpoint.parameters if p.location.value == "query"]
506
+ if query_params:
507
+ kwargs.append("params=params")
508
+
509
+ # Add request body
510
+ if endpoint.request_body:
511
+ if endpoint.request_body.content_type == "application/json":
512
+ kwargs.append("json=json_data")
513
+ elif (
514
+ endpoint.request_body.content_type
515
+ == "application/x-www-form-urlencoded"
516
+ ):
517
+ kwargs.append("data=data")
518
+
519
+ # Add headers if needed
520
+ header_params = [p for p in endpoint.parameters if p.location.value == "header"]
521
+ if header_params:
522
+ kwargs.append("headers=headers")
523
+
524
+ return ",\n ".join(kwargs)
525
+
526
+ def _get_task_weight(self, method: str) -> int:
527
+ """Get task weight based on HTTP method"""
528
+ weights = {
529
+ "GET": 5, # Most frequent
530
+ "POST": 2, # Common
531
+ "PUT": 1, # Less frequent
532
+ "PATCH": 1, # Less frequent
533
+ "DELETE": 1, # Least frequent
534
+ "HEAD": 3, # Moderate
535
+ "OPTIONS": 1, # Rare
536
+ }
537
+ return weights.get(method.upper(), 1)
538
+
539
+ def _group_endpoints_by_tag(
540
+ self,
541
+ endpoints: List[Endpoint],
542
+ include_auth_endpoints: bool = True,
543
+ ) -> Dict[str, List[Endpoint]]:
544
+ """Group endpoints by their tags"""
545
+ grouped: Dict[str, List[Endpoint]] = {}
546
+ # Define authentication-related keywords to check for in paths
547
+ auth_keywords = [
548
+ "login",
549
+ "signin",
550
+ "sign-in",
551
+ "sign_in",
552
+ "logout",
553
+ "signout",
554
+ "sign-out",
555
+ "sign_out",
556
+ "auth",
557
+ "authenticate",
558
+ "authorization",
559
+ "token",
560
+ "refresh",
561
+ "verify",
562
+ "password",
563
+ "reset",
564
+ "forgot",
565
+ "register",
566
+ "signup",
567
+ "sign-up",
568
+ "sign_up",
569
+ "session",
570
+ "oauth",
571
+ "sso",
572
+ ]
573
+
574
+ def is_auth_endpoint(endpoint_path: str) -> bool:
575
+ """Check if endpoint path contains authentication-related keywords"""
576
+ path_lower = endpoint_path.lower()
577
+ return any(keyword in path_lower for keyword in auth_keywords)
578
+
579
+ for endpoint in endpoints:
580
+ tags = endpoint.tags if endpoint.tags else ["default"]
581
+ if is_auth_endpoint(endpoint.path) and include_auth_endpoints:
582
+ # Add to authentication group regardless of tags
583
+ if "Authentication" not in grouped:
584
+ grouped["Authentication"] = []
585
+ grouped["Authentication"].append(endpoint)
586
+
587
+ for tag in tags:
588
+ if tag not in grouped:
589
+ grouped[tag] = []
590
+ grouped[tag].append(endpoint)
591
+
592
+ return grouped
593
+
594
+ def _generate_all_task_methods_string(self, endpoints: List[Endpoint]) -> str:
595
+ """Generate all task methods as a properly indented string"""
596
+ methods = []
597
+ for endpoint in endpoints:
598
+ method_code = self._generate_task_method(endpoint)
599
+ methods.append(method_code)
600
+
601
+ return "\n".join(methods)
602
+
603
+ def _generate_user_classes(self) -> str:
604
+ """
605
+ **FIXED: Generate user classes with proper structure**
606
+ """
607
+
608
+ return '''
609
+ class LightUser(BaseAPIUser, BaseTaskMethods):
610
+ """Light user with occasional API usage patterns"""
611
+ weight = 3
612
+ wait_time = between(3, 8) # Longer wait times
613
+
614
+ def on_start(self):
615
+ super().on_start()
616
+ self.user_type = "light"
617
+
618
+
619
+ class RegularUser( BaseAPIUser, BaseTaskMethods):
620
+ """Regular user with normal API usage patterns"""
621
+ weight = 4
622
+ wait_time = between(1, 4) # Moderate wait times
623
+
624
+ def on_start(self):
625
+ super().on_start()
626
+ self.user_type = "regular"
627
+
628
+
629
+
630
+ class PowerUser( BaseAPIUser, BaseTaskMethods):
631
+ """Power user with heavy API usage patterns"""
632
+ weight = 3
633
+ wait_time = between(0.5, 2) # Shorter wait times
634
+
635
+ def on_start(self):
636
+ super().on_start()
637
+ self.user_type = "power"
638
+
639
+
640
+ '''
641
+
642
+ def _generate_test_data_file(self) -> str:
643
+ """Generate test_data.py file content"""
644
+ template = self.jinja_env.get_template("test_data.py.j2")
645
+ return template.render()
646
+
647
+ def _generate_config_file(self, api_info: Dict[str, Any]) -> str:
648
+ """Generate config.py file content"""
649
+ template = self.jinja_env.get_template("config.py.j2")
650
+ return template.render(api_info=api_info)
651
+
652
+ def _generate_utils_file(self) -> str:
653
+ """Generate utils.py file content"""
654
+ template = self.jinja_env.get_template("utils.py.j2")
655
+ return template.render()
656
+
657
+ def _generate_custom_flows_file(self) -> str:
658
+ """Generate custom_flows.py file content"""
659
+ template = self.jinja_env.get_template("custom_flows.py.j2")
660
+ return template.render()
661
+
662
+ def _generate_requirements_file(self) -> str:
663
+ """Generate requirements.txt file content"""
664
+ template = self.jinja_env.get_template("requirement.txt.j2")
665
+ content = template.render()
666
+ return content
667
+
668
+ def _generate_env_example(
669
+ self, api_info: Dict[str, Any], target_host: Optional[str] = None
670
+ ) -> str:
671
+ """Generate .env.example file content"""
672
+ try:
673
+ template = self.jinja_env.get_template("env.example.j2")
674
+ if target_host:
675
+ base_url = target_host
676
+ locust_host = target_host
677
+ else:
678
+ base_url = api_info.get("base_url", "http://localhost:8000")
679
+ locust_host = api_info.get("base_url", "http://localhost:8000")
680
+ # Prepare environment variables context
681
+ environment_vars = {
682
+ "API_BASE_URL": base_url,
683
+ "API_VERSION": api_info.get("version", "v1"),
684
+ "API_TITLE": api_info.get("title", "Your API Name"),
685
+ "LOCUST_USERS": "50",
686
+ "LOCUST_SPAWN_RATE": "5",
687
+ "LOCUST_RUN_TIME": "10m",
688
+ "LOCUST_HOST": locust_host,
689
+ "USE_REALISTIC_DATA": "true",
690
+ "DATA_SEED": "42",
691
+ "REQUEST_TIMEOUT": "30",
692
+ "MAX_RETRIES": "3",
693
+ }
694
+
695
+ context = {
696
+ "environment_vars": environment_vars,
697
+ "api_info": api_info,
698
+ "generated_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
699
+ }
700
+
701
+ content = template.render(**context)
702
+ logger.info("✅ .env.example generated successfully using template")
703
+ return content
704
+
705
+ except Exception as e:
706
+ logger.error(f"❌ Failed to generate .env.example from template: {e}")
707
+ return ""
708
+
709
+ def _generate_readme_file(self, api_info: Dict[str, Any]) -> str:
710
+ try:
711
+ # Get the template
712
+ template = self.jinja_env.get_template("readme.md.j2")
713
+
714
+ # Prepare template context
715
+ context = {
716
+ "api_info": api_info,
717
+ "generated_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
718
+ }
719
+
720
+ # Render the template
721
+ content = template.render(**context)
722
+
723
+ logger.info("README generated successfully using template")
724
+ return content
725
+
726
+ except Exception as e:
727
+ logger.error(f"Error generating README: {e}")
728
+ return ""
729
+
730
+ def generate_base_common_file(self, api_info: Dict[str, Any]) -> str:
731
+ template = self.jinja_env.get_template("base_workflow.py.j2")
732
+ return template.render(api_info=api_info)