socialseed-e2e 0.1.0__py3-none-any.whl → 0.1.2__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.
Files changed (29) hide show
  1. socialseed_e2e/__init__.py +184 -20
  2. socialseed_e2e/__version__.py +2 -2
  3. socialseed_e2e/cli.py +353 -190
  4. socialseed_e2e/core/base_page.py +368 -49
  5. socialseed_e2e/core/config_loader.py +15 -3
  6. socialseed_e2e/core/headers.py +11 -4
  7. socialseed_e2e/core/loaders.py +6 -4
  8. socialseed_e2e/core/test_orchestrator.py +2 -0
  9. socialseed_e2e/core/test_runner.py +487 -0
  10. socialseed_e2e/templates/agent_docs/AGENT_GUIDE.md.template +412 -0
  11. socialseed_e2e/templates/agent_docs/EXAMPLE_TEST.md.template +152 -0
  12. socialseed_e2e/templates/agent_docs/FRAMEWORK_CONTEXT.md.template +55 -0
  13. socialseed_e2e/templates/agent_docs/WORKFLOW_GENERATION.md.template +182 -0
  14. socialseed_e2e/templates/data_schema.py.template +111 -70
  15. socialseed_e2e/templates/e2e.conf.template +19 -0
  16. socialseed_e2e/templates/service_page.py.template +82 -27
  17. socialseed_e2e/templates/test_module.py.template +21 -7
  18. socialseed_e2e/templates/verify_installation.py +192 -0
  19. socialseed_e2e/utils/__init__.py +29 -0
  20. socialseed_e2e/utils/ai_generator.py +463 -0
  21. socialseed_e2e/utils/pydantic_helpers.py +392 -0
  22. socialseed_e2e/utils/state_management.py +312 -0
  23. {socialseed_e2e-0.1.0.dist-info → socialseed_e2e-0.1.2.dist-info}/METADATA +64 -27
  24. socialseed_e2e-0.1.2.dist-info/RECORD +38 -0
  25. socialseed_e2e-0.1.0.dist-info/RECORD +0 -29
  26. {socialseed_e2e-0.1.0.dist-info → socialseed_e2e-0.1.2.dist-info}/WHEEL +0 -0
  27. {socialseed_e2e-0.1.0.dist-info → socialseed_e2e-0.1.2.dist-info}/entry_points.txt +0 -0
  28. {socialseed_e2e-0.1.0.dist-info → socialseed_e2e-0.1.2.dist-info}/licenses/LICENSE +0 -0
  29. {socialseed_e2e-0.1.0.dist-info → socialseed_e2e-0.1.2.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,192 @@
1
+ """Installation verification script for socialseed-e2e framework.
2
+
3
+ This script verifies that the framework is properly installed and configured,
4
+ and that all components work correctly.
5
+
6
+ Usage:
7
+ python verify_installation.py
8
+
9
+ Or after e2e init:
10
+ python verify_setup.py
11
+ """
12
+
13
+ import importlib
14
+ import sys
15
+ from pathlib import Path
16
+
17
+
18
+ def check_color(text: str, color: str) -> str:
19
+ """Add color to text for terminal output."""
20
+ colors = {
21
+ "green": "\033[92m",
22
+ "red": "\033[91m",
23
+ "yellow": "\033[93m",
24
+ "blue": "\033[94m",
25
+ "reset": "\033[0m",
26
+ }
27
+ return f"{colors.get(color, '')}{text}{colors['reset']}"
28
+
29
+
30
+ def print_section(title: str):
31
+ """Print a section header."""
32
+ print(f"\n{'=' * 60}")
33
+ print(f" {title}")
34
+ print(f"{'=' * 60}")
35
+
36
+
37
+ def print_check(name: str, status: bool, message: str = ""):
38
+ """Print a check result."""
39
+ symbol = "✓" if status else "✗"
40
+ color = "green" if status else "red"
41
+ msg = f" - {message}" if message else ""
42
+ print(f" {check_color(symbol, color)} {name}{msg}")
43
+
44
+
45
+ def verify_installation():
46
+ """Run all verification checks."""
47
+ all_passed = True
48
+
49
+ print("\n" + "=" * 60)
50
+ print(" SocialSeed E2E Framework - Installation Verification")
51
+ print("=" * 60)
52
+
53
+ # 1. Check Python version
54
+ print_section("Python Environment")
55
+ version = sys.version_info
56
+ python_ok = version.major == 3 and version.minor >= 10
57
+ print_check(f"Python {version.major}.{version.minor}.{version.micro}", python_ok)
58
+ all_passed = all_passed and python_ok
59
+
60
+ # 2. Check required packages
61
+ print_section("Required Packages")
62
+ required_packages = [
63
+ ("pydantic", "Pydantic v2"),
64
+ ("email_validator", "Email Validator"),
65
+ ]
66
+
67
+ for package, display_name in required_packages:
68
+ try:
69
+ importlib.import_module(package)
70
+ print_check(f"{display_name}", True)
71
+ except ImportError:
72
+ print_check(f"{display_name}", False, f"Not installed. Run: pip install {package}")
73
+ all_passed = False
74
+
75
+ # 3. Check framework packages
76
+ print_section("Framework Packages")
77
+ try:
78
+ importlib.import_module("socialseed_e2e")
79
+ print_check("socialseed_e2e (framework)", True)
80
+ except ImportError:
81
+ print_check("socialseed_e2e", False, "Framework not installed")
82
+ print(" → Install with: pip install socialseed-e2e")
83
+ all_passed = False
84
+
85
+ # 4. Check project structure
86
+ print_section("Project Structure")
87
+
88
+ project_root = Path(".").resolve()
89
+ required_paths = [
90
+ ("e2e.conf", "Configuration file"),
91
+ ("services/", "Services directory"),
92
+ ("requirements.txt", "Dependencies file"),
93
+ (".agent/AGENT_GUIDE.md", "AI Agent guide"),
94
+ (".agent/EXAMPLE_TEST.md", "Example documentation"),
95
+ ]
96
+
97
+ for path, description in required_paths:
98
+ full_path = project_root / path
99
+ exists = full_path.exists()
100
+ print_check(f"{path} ({description})", exists)
101
+ if not exists:
102
+ all_passed = False
103
+
104
+ # 5. Test Pydantic alias serialization
105
+ print_section("Pydantic Alias Serialization Test")
106
+ try:
107
+ from pydantic import BaseModel, Field
108
+
109
+ class TestModel(BaseModel):
110
+ model_config = {"populate_by_name": True}
111
+ field_name: str = Field(alias="fieldName", serialization_alias="fieldName")
112
+
113
+ # Test serialization
114
+ instance = TestModel(field_name="test")
115
+ data = instance.model_dump(by_alias=True)
116
+
117
+ if "fieldName" in data and data["fieldName"] == "test":
118
+ print_check("CamelCase serialization", True)
119
+ else:
120
+ print_check("CamelCase serialization", False, "alias not working correctly")
121
+ all_passed = False
122
+
123
+ # Test deserialization
124
+ instance2 = TestModel(fieldName="test2")
125
+ if instance2.field_name == "test2":
126
+ print_check("Field population by name", True)
127
+ else:
128
+ print_check("Field population by name", False, "populate_by_name not working")
129
+ all_passed = False
130
+
131
+ except Exception as e:
132
+ print_check("Pydantic configuration", False, str(e))
133
+ all_passed = False
134
+
135
+ # 6. Check for common issues
136
+ print_section("Common Issues Check")
137
+
138
+ # Check for relative imports in services
139
+ services_dir = project_root / "services"
140
+ if services_dir.exists():
141
+ has_relative_imports = False
142
+ for py_file in services_dir.rglob("*.py"):
143
+ if py_file.name.startswith("_"):
144
+ continue
145
+ try:
146
+ content = py_file.read_text()
147
+ if "from .." in content or "from . import" in content:
148
+ if py_file.name != "__init__.py":
149
+ has_relative_imports = True
150
+ print_check(
151
+ f"Relative imports in {py_file}",
152
+ False,
153
+ "Use absolute imports",
154
+ )
155
+ except Exception:
156
+ pass
157
+
158
+ if not has_relative_imports:
159
+ print_check("No relative imports found", True)
160
+
161
+ # 7. Services check
162
+ print_section("Configured Services")
163
+ if services_dir.exists():
164
+ services = [d.name for d in services_dir.iterdir() if d.is_dir()]
165
+ if services:
166
+ for service in services:
167
+ print_check(f"Service: {service}", True)
168
+ else:
169
+ print(" ℹ No services configured yet")
170
+ print(" → Create with: e2e new-service <name>")
171
+
172
+ # Final summary
173
+ print("\n" + "=" * 60)
174
+ if all_passed:
175
+ print(check_color(" ✓ All checks passed! Installation is ready.", "green"))
176
+ print("\n 🎉 You can now:")
177
+ print(" • Run: e2e new-service <name> to create a service")
178
+ print(" • Ask your AI agent to generate tests")
179
+ print(" • Read .agent/AGENT_GUIDE.md for patterns")
180
+ else:
181
+ print(check_color(" ✗ Some checks failed. Please review issues above.", "red"))
182
+ print("\n 🔧 To fix:")
183
+ print(" • Install dependencies: pip install -r requirements.txt")
184
+ print(" • Review .agent/AGENT_GUIDE.md for correct patterns")
185
+ print("=" * 60)
186
+
187
+ return all_passed
188
+
189
+
190
+ if __name__ == "__main__":
191
+ success = verify_installation()
192
+ sys.exit(0 if success else 1)
@@ -1,5 +1,19 @@
1
1
  """Utility functions for socialseed-e2e."""
2
2
 
3
+ from .pydantic_helpers import (
4
+ JavaCompatibleModel,
5
+ camel_field,
6
+ created_at_field,
7
+ current_password_field,
8
+ new_password_field,
9
+ refresh_token_field,
10
+ to_camel_dict,
11
+ updated_at_field,
12
+ user_id_field,
13
+ user_name_field,
14
+ validate_camelcase_model,
15
+ )
16
+ from .state_management import AuthStateMixin, DynamicStateMixin
3
17
  from .template_engine import TemplateEngine, to_camel_case, to_class_name, to_snake_case
4
18
  from .validators import (
5
19
  ValidationError,
@@ -20,6 +34,21 @@ from .validators import (
20
34
  )
21
35
 
22
36
  __all__ = [
37
+ # State management
38
+ "DynamicStateMixin",
39
+ "AuthStateMixin",
40
+ # Pydantic helpers for Java compatibility
41
+ "JavaCompatibleModel",
42
+ "camel_field",
43
+ "to_camel_dict",
44
+ "validate_camelcase_model",
45
+ "refresh_token_field",
46
+ "user_name_field",
47
+ "user_id_field",
48
+ "created_at_field",
49
+ "updated_at_field",
50
+ "new_password_field",
51
+ "current_password_field",
23
52
  # Template engine
24
53
  "TemplateEngine",
25
54
  "to_class_name",
@@ -0,0 +1,463 @@
1
+ """AI Code Generation Helper for socialseed-e2e.
2
+
3
+ This module provides intelligent code generation for AI agents
4
+ to automatically create tests from REST controller analysis.
5
+ """
6
+
7
+ import re
8
+ from dataclasses import dataclass
9
+ from pathlib import Path
10
+ from typing import Dict, List, Optional, Tuple
11
+
12
+
13
+ @dataclass
14
+ class EndpointInfo:
15
+ """Information about a REST endpoint."""
16
+
17
+ method: str
18
+ path: str
19
+ name: str
20
+ request_dto: Optional[str] = None
21
+ response_dto: Optional[str] = None
22
+ requires_auth: bool = False
23
+ path_params: List[str] = None
24
+ query_params: List[str] = None
25
+
26
+
27
+ @dataclass
28
+ class DtoField:
29
+ """Field information for DTO generation."""
30
+
31
+ name: str
32
+ type_hint: str
33
+ required: bool = True
34
+ validations: List[str] = None
35
+
36
+
37
+ class JavaControllerParser:
38
+ """Parser for Java Spring Boot controllers."""
39
+
40
+ @staticmethod
41
+ def parse_controller(java_code: str) -> List[EndpointInfo]:
42
+ """Parse Java controller code and extract endpoints.
43
+
44
+ Args:
45
+ java_code: Java source code
46
+
47
+ Returns:
48
+ List of EndpointInfo objects
49
+ """
50
+ endpoints = []
51
+
52
+ # Even more robust regex for Spring Boot methods
53
+ method_pattern = r'@(?:Post|Get|Put|Delete|Patch)Mapping\s*\(\s*(?:(?:value|path)\s*=\s*)?"([^"]+)"[^)]*\)\s*(?:@[^;{]+\s+)*?(?:public\s+)?\s*(?:ResponseEntity<[^>]+>|[\w<>\?]+)\s+(\w+)\s*\(([^)]*)\)'
54
+
55
+ # We need to find the HTTP method separately because we removed it from the capturing group to simplify the regex
56
+ all_mapping_patterns = [
57
+ (r"@PostMapping", "POST"),
58
+ (r"@GetMapping", "GET"),
59
+ (r"@PutMapping", "PUT"),
60
+ (r"@DeleteMapping", "DELETE"),
61
+ (r"@PatchMapping", "PATCH"),
62
+ ]
63
+
64
+ for mapping_tag, http_method in all_mapping_patterns:
65
+ pattern = (
66
+ mapping_tag
67
+ + r'\s*\(\s*(?:(?:value|path)\s*=\s*)?"([^"]+)"[^)]*\)\s*(?:@[^;{]+\s+)*?(?:public\s+)?\s*(?:ResponseEntity<[^>]+>|[\w<>\?]+)\s+(\w+)\s*\(([^)]*)\)'
68
+ )
69
+ matches = re.finditer(pattern, java_code, re.MULTILINE | re.DOTALL)
70
+ for match in matches:
71
+ path = match.group(1)
72
+ method_name = match.group(2)
73
+ arguments = match.group(3)
74
+
75
+ # Check for @RequestBody
76
+ request_dto = None
77
+ request_body_match = re.search(
78
+ r"@RequestBody\s+(?:@\w+\s+)*(\w+)\s+(\w+)", arguments
79
+ )
80
+ if request_body_match:
81
+ request_dto = request_body_match.group(1)
82
+
83
+ # Check for authentication requirements
84
+ requires_auth = (
85
+ "@PreAuthorize" in java_code or "@AuthenticationPrincipal" in java_code
86
+ )
87
+
88
+ # Extract path parameters
89
+ path_params = re.findall(r"\{(\w+)\}", path)
90
+
91
+ endpoints.append(
92
+ EndpointInfo(
93
+ method=http_method,
94
+ path=path,
95
+ name=method_name,
96
+ request_dto=request_dto,
97
+ requires_auth=requires_auth,
98
+ path_params=path_params,
99
+ )
100
+ )
101
+
102
+ return endpoints
103
+
104
+ @staticmethod
105
+ def parse_dto(java_code: str, dto_name: str) -> List[DtoField]:
106
+ """Parse a Java DTO and extract fields.
107
+
108
+ Args:
109
+ java_code: Java DTO source code
110
+ dto_name: Name of the DTO class
111
+
112
+ Returns:
113
+ List of DtoField objects
114
+ """
115
+ fields = []
116
+
117
+ # Pattern for record fields
118
+ record_pattern = rf"record\s+{dto_name}\s*\(\s*([^)]+)\)"
119
+ record_match = re.search(record_pattern, java_code, re.DOTALL)
120
+
121
+ if record_match:
122
+ # Parse record components
123
+ components = record_match.group(1)
124
+ # Split by comma, but be careful with generics
125
+ # Improved regex to capture annotations for each field
126
+ field_defs = re.findall(
127
+ r"((?:@\w+(?:\([^)]*\))?\s*)*)(\w+(?:<[^>]+>)?)\s+(\w+)", components
128
+ )
129
+
130
+ for annotations, type_hint, name in field_defs:
131
+ # Map Java types to Python
132
+ py_type = JavaControllerParser._map_java_type(type_hint.strip())
133
+
134
+ # Check for @Email specifically in this field's annotations
135
+ if "@Email" in annotations and py_type == "str":
136
+ py_type = "EmailStr"
137
+
138
+ fields.append(DtoField(name=name, type_hint=py_type, required=True))
139
+
140
+ return fields
141
+
142
+ @staticmethod
143
+ def _map_java_type(java_type: str) -> str:
144
+ """Map Java types to Python type hints."""
145
+ type_mapping = {
146
+ "String": "str",
147
+ "Integer": "int",
148
+ "int": "int",
149
+ "Long": "int",
150
+ "long": "int",
151
+ "Boolean": "bool",
152
+ "boolean": "bool",
153
+ "Double": "float",
154
+ "double": "float",
155
+ "Float": "float",
156
+ "float": "float",
157
+ "UUID": "UUID",
158
+ "Instant": "datetime",
159
+ "LocalDateTime": "datetime",
160
+ "Set": "Set",
161
+ "List": "List",
162
+ "Map": "Dict",
163
+ }
164
+
165
+ # Handle generics
166
+ for java, python in type_mapping.items():
167
+ if java_type.startswith(java + "<"):
168
+ inner_type = java_type[java_type.find("<") + 1 : java_type.rfind(">")]
169
+ inner_python = JavaControllerParser._map_java_type(inner_type.strip())
170
+ return f"{python}[{inner_python}]"
171
+ elif java_type == java:
172
+ return python
173
+
174
+ return "str" # Default to str
175
+
176
+
177
+ class PythonCodeGenerator:
178
+ """Generator for Python test code."""
179
+
180
+ @staticmethod
181
+ def generate_data_schema(
182
+ endpoints: List[EndpointInfo],
183
+ dto_definitions: Dict[str, List[DtoField]],
184
+ service_name: str,
185
+ ) -> str:
186
+ """Generate data_schema.py content.
187
+
188
+ Args:
189
+ endpoints: List of endpoints
190
+ dto_definitions: Dictionary mapping DTO names to fields
191
+ service_name: Name of the service
192
+
193
+ Returns:
194
+ Python code for data_schema.py
195
+ """
196
+ lines = [
197
+ '"""Data schema for {} API.'.format(service_name),
198
+ "",
199
+ "Auto-generated from Java controller analysis.",
200
+ '"""',
201
+ "from pydantic import BaseModel, Field, EmailStr",
202
+ "from typing import Optional, Set, List, Dict",
203
+ "from datetime import datetime",
204
+ "from uuid import UUID",
205
+ "",
206
+ "",
207
+ "# =============================================================================",
208
+ "# Request DTOs",
209
+ "# =============================================================================",
210
+ "",
211
+ ]
212
+
213
+ # Generate request DTOs
214
+ generated_dtos = set()
215
+ for endpoint in endpoints:
216
+ if endpoint.request_dto and endpoint.request_dto not in generated_dtos:
217
+ dto_name = endpoint.request_dto.replace("DTO", "Request")
218
+ fields = dto_definitions.get(endpoint.request_dto, [])
219
+
220
+ lines.append(f"class {dto_name}(BaseModel):")
221
+ lines.append(f' """{endpoint.request_dto} request."""')
222
+ lines.append(' model_config = {"populate_by_name": True}')
223
+ lines.append("")
224
+
225
+ for field in fields:
226
+ if field.name in [
227
+ "refresh_token",
228
+ "access_token",
229
+ "new_password",
230
+ "current_password",
231
+ "user_id",
232
+ "created_at",
233
+ "updated_at",
234
+ "last_login_at",
235
+ ]:
236
+ # Use camelCase alias
237
+ camel_name = PythonCodeGenerator._to_camel_case(field.name)
238
+ lines.append(f" {field.name}: {field.type_hint} = Field(")
239
+ lines.append(f" ...,")
240
+ lines.append(f' alias="{camel_name}",')
241
+ lines.append(f' serialization_alias="{camel_name}"')
242
+ lines.append(f" )")
243
+ else:
244
+ lines.append(f" {field.name}: {field.type_hint}")
245
+
246
+ lines.append("")
247
+ lines.append("")
248
+ generated_dtos.add(endpoint.request_dto)
249
+
250
+ # Generate ENDPOINTS constant
251
+ lines.append(
252
+ "# ============================================================================="
253
+ )
254
+ lines.append("# Endpoint Constants")
255
+ lines.append(
256
+ "# ============================================================================="
257
+ )
258
+ lines.append("")
259
+ lines.append("ENDPOINTS = {")
260
+
261
+ for endpoint in endpoints:
262
+ key = endpoint.name.lower().replace("get", "get_").replace("post", "create_")
263
+ key = (
264
+ key.replace("put", "update_")
265
+ .replace("delete", "delete_")
266
+ .replace("patch", "patch_")
267
+ )
268
+ key = re.sub(r"([a-z])([A-Z])", r"\1_\2", key).lower()
269
+
270
+ lines.append(f' "{key}": "{endpoint.path}",')
271
+
272
+ lines.append("}")
273
+ lines.append("")
274
+ lines.append("")
275
+
276
+ # Generate TEST_DATA
277
+ lines.append(
278
+ "# ============================================================================="
279
+ )
280
+ lines.append("# Test Data")
281
+ lines.append(
282
+ "# ============================================================================="
283
+ )
284
+ lines.append("")
285
+ lines.append("TEST_DATA = {")
286
+ lines.append(' "user": {')
287
+ lines.append(' "username": "testuser_e2e",')
288
+ lines.append(' "email": "testuser_e2e@example.com",')
289
+ lines.append(' "password": "TestPassword123!"')
290
+ lines.append(" }")
291
+ lines.append("}")
292
+ lines.append("")
293
+
294
+ return "\n".join(lines)
295
+
296
+ @staticmethod
297
+ def generate_page_class(
298
+ endpoints: List[EndpointInfo], service_name: str, service_class_name: str
299
+ ) -> str:
300
+ """Generate page class content.
301
+
302
+ Args:
303
+ endpoints: List of endpoints
304
+ service_name: Name of the service
305
+ service_class_name: PascalCase service name
306
+
307
+ Returns:
308
+ Python code for {service}_page.py
309
+ """
310
+ lines = [
311
+ '"""Page object for {} API.""".format(service_name)',
312
+ "from typing import Optional, Dict, Any",
313
+ "from playwright.sync_api import APIResponse",
314
+ "from socialseed_e2e import BasePage",
315
+ "",
316
+ "from services.{}.data_schema import (".format(service_name),
317
+ " ENDPOINTS,",
318
+ ]
319
+
320
+ # Import request DTOs
321
+ for endpoint in endpoints:
322
+ if endpoint.request_dto:
323
+ dto_name = endpoint.request_dto.replace("DTO", "Request")
324
+ lines.append(f" {dto_name},")
325
+
326
+ lines.append(")")
327
+ lines.append("")
328
+ lines.append("")
329
+ lines.append(f"class {service_class_name}Page(BasePage):")
330
+ lines.append(f' """Page object for {service_name} service API."""')
331
+ lines.append("")
332
+ lines.append(" def __init__(self, base_url: str, **kwargs):")
333
+ lines.append(" super().__init__(base_url=base_url, **kwargs)")
334
+ lines.append(" self.access_token: Optional[str] = None")
335
+ lines.append(" self.current_user: Optional[Dict[str, Any]] = None")
336
+ lines.append("")
337
+ lines.append(" def _get_headers(self, extra: Optional[Dict] = None) -> Dict[str, str]:")
338
+ lines.append(' """Build headers with authentication if available."""')
339
+ lines.append(' headers = {"Content-Type": "application/json"}')
340
+ lines.append(" if self.access_token:")
341
+ lines.append(' headers["Authorization"] = f"Bearer {self.access_token}"')
342
+ lines.append(" if extra:")
343
+ lines.append(" headers.update(extra)")
344
+ lines.append(" return headers")
345
+ lines.append("")
346
+
347
+ # Generate methods for each endpoint
348
+ for endpoint in endpoints:
349
+ method_name = PythonCodeGenerator._generate_method_name(endpoint)
350
+ lines.append(f" def {method_name}(self, request) -> APIResponse:")
351
+ lines.append(f' """{endpoint.name} endpoint."""')
352
+
353
+ # Handle path parameters
354
+ path = endpoint.path
355
+ if endpoint.path_params:
356
+ for param in endpoint.path_params:
357
+ path = path.replace(f"{{{param}}}", f"{{{param}}}")
358
+ lines.append(
359
+ f' path = ENDPOINTS["{key}"].format({", ".join(endpoint.path_params)})'
360
+ )
361
+ lines.append(
362
+ f" response = self.{endpoint.method.lower()}(path, data=request.model_dump(by_alias=True))"
363
+ )
364
+ else:
365
+ key = endpoint.name.lower().replace("get", "get_").replace("post", "create_")
366
+ key = (
367
+ key.replace("put", "update_")
368
+ .replace("delete", "delete_")
369
+ .replace("patch", "patch_")
370
+ )
371
+ key = re.sub(r"([a-z])([A-Z])", r"\1_\2", key).lower()
372
+ lines.append(f" response = self.{endpoint.method.lower()}(")
373
+ lines.append(f' ENDPOINTS["{key}"],')
374
+ lines.append(
375
+ f" data=request.model_dump(by_alias=True) # ✅ SIEMPRE by_alias=True"
376
+ )
377
+ lines.append(f" )")
378
+
379
+ lines.append(" return response")
380
+ lines.append("")
381
+
382
+ return "\n".join(lines)
383
+
384
+ @staticmethod
385
+ def _to_camel_case(snake_str: str) -> str:
386
+ """Convert snake_case to camelCase."""
387
+ components = snake_str.split("_")
388
+ return components[0] + "".join(x.capitalize() for x in components[1:])
389
+
390
+ @staticmethod
391
+ def _generate_method_name(endpoint: EndpointInfo) -> str:
392
+ """Generate Python method name from endpoint info."""
393
+ prefix = "do_"
394
+
395
+ # Map HTTP methods to action verbs
396
+ if endpoint.method == "GET":
397
+ if "ById" in endpoint.name or "By" in endpoint.name:
398
+ action = "get"
399
+ else:
400
+ action = "list"
401
+ elif endpoint.method == "POST":
402
+ action = "create"
403
+ elif endpoint.method == "PUT":
404
+ action = "update"
405
+ elif endpoint.method == "PATCH":
406
+ action = "patch"
407
+ elif endpoint.method == "DELETE":
408
+ action = "delete"
409
+ else:
410
+ action = endpoint.method.lower()
411
+
412
+ # Extract resource name
413
+ name = endpoint.name
414
+ name = re.sub(r"^(get|post|put|delete|patch)", "", name, flags=re.IGNORECASE)
415
+ name = re.sub(r"([a-z])([A-Z])", r"\1_\2", name).lower()
416
+
417
+ return f"{prefix}{action}_{name}".rstrip("_")
418
+
419
+
420
+ class AICodeGenerator:
421
+ """Main class for AI-assisted code generation."""
422
+
423
+ @staticmethod
424
+ def analyze_and_generate(
425
+ controller_code: str, dto_codes: Dict[str, str], service_name: str
426
+ ) -> Dict[str, str]:
427
+ """Analyze Java code and generate Python test code.
428
+
429
+ Args:
430
+ controller_code: Java controller source code
431
+ dto_codes: Dictionary of DTO name to source code
432
+ service_name: Name of the service
433
+
434
+ Returns:
435
+ Dictionary with 'data_schema' and 'page_class' keys
436
+ """
437
+ # Parse controller
438
+ endpoints = JavaControllerParser.parse_controller(controller_code)
439
+
440
+ # Parse DTOs
441
+ dto_definitions = {}
442
+ for dto_name, dto_code in dto_codes.items():
443
+ fields = JavaControllerParser.parse_dto(dto_code, dto_name)
444
+ dto_definitions[dto_name] = fields
445
+
446
+ # Generate service class name
447
+ service_class_name = "".join(word.capitalize() for word in service_name.split("_"))
448
+
449
+ # Generate code
450
+ data_schema = PythonCodeGenerator.generate_data_schema(
451
+ endpoints, dto_definitions, service_name
452
+ )
453
+
454
+ page_class = PythonCodeGenerator.generate_page_class(
455
+ endpoints, service_name, service_class_name
456
+ )
457
+
458
+ return {
459
+ "data_schema": data_schema,
460
+ "page_class": page_class,
461
+ "endpoints": endpoints,
462
+ "dto_definitions": dto_definitions,
463
+ }