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.
- socialseed_e2e/__init__.py +184 -20
- socialseed_e2e/__version__.py +2 -2
- socialseed_e2e/cli.py +353 -190
- socialseed_e2e/core/base_page.py +368 -49
- socialseed_e2e/core/config_loader.py +15 -3
- socialseed_e2e/core/headers.py +11 -4
- socialseed_e2e/core/loaders.py +6 -4
- socialseed_e2e/core/test_orchestrator.py +2 -0
- socialseed_e2e/core/test_runner.py +487 -0
- socialseed_e2e/templates/agent_docs/AGENT_GUIDE.md.template +412 -0
- socialseed_e2e/templates/agent_docs/EXAMPLE_TEST.md.template +152 -0
- socialseed_e2e/templates/agent_docs/FRAMEWORK_CONTEXT.md.template +55 -0
- socialseed_e2e/templates/agent_docs/WORKFLOW_GENERATION.md.template +182 -0
- socialseed_e2e/templates/data_schema.py.template +111 -70
- socialseed_e2e/templates/e2e.conf.template +19 -0
- socialseed_e2e/templates/service_page.py.template +82 -27
- socialseed_e2e/templates/test_module.py.template +21 -7
- socialseed_e2e/templates/verify_installation.py +192 -0
- socialseed_e2e/utils/__init__.py +29 -0
- socialseed_e2e/utils/ai_generator.py +463 -0
- socialseed_e2e/utils/pydantic_helpers.py +392 -0
- socialseed_e2e/utils/state_management.py +312 -0
- {socialseed_e2e-0.1.0.dist-info → socialseed_e2e-0.1.2.dist-info}/METADATA +64 -27
- socialseed_e2e-0.1.2.dist-info/RECORD +38 -0
- socialseed_e2e-0.1.0.dist-info/RECORD +0 -29
- {socialseed_e2e-0.1.0.dist-info → socialseed_e2e-0.1.2.dist-info}/WHEEL +0 -0
- {socialseed_e2e-0.1.0.dist-info → socialseed_e2e-0.1.2.dist-info}/entry_points.txt +0 -0
- {socialseed_e2e-0.1.0.dist-info → socialseed_e2e-0.1.2.dist-info}/licenses/LICENSE +0 -0
- {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)
|
socialseed_e2e/utils/__init__.py
CHANGED
|
@@ -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
|
+
}
|