python2mobile 1.0.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.
- examples/example_ecommerce_app.py +189 -0
- examples/example_todo_app.py +159 -0
- p2m/__init__.py +31 -0
- p2m/cli.py +470 -0
- p2m/config.py +205 -0
- p2m/core/__init__.py +18 -0
- p2m/core/api.py +191 -0
- p2m/core/ast_walker.py +171 -0
- p2m/core/database.py +192 -0
- p2m/core/events.py +56 -0
- p2m/core/render_engine.py +597 -0
- p2m/core/runtime.py +128 -0
- p2m/core/state.py +51 -0
- p2m/core/validator.py +284 -0
- p2m/devserver/__init__.py +9 -0
- p2m/devserver/server.py +84 -0
- p2m/i18n/__init__.py +7 -0
- p2m/i18n/translator.py +74 -0
- p2m/imagine/__init__.py +35 -0
- p2m/imagine/agent.py +463 -0
- p2m/imagine/legacy.py +217 -0
- p2m/llm/__init__.py +20 -0
- p2m/llm/anthropic_provider.py +78 -0
- p2m/llm/base.py +42 -0
- p2m/llm/compatible_provider.py +120 -0
- p2m/llm/factory.py +72 -0
- p2m/llm/ollama_provider.py +89 -0
- p2m/llm/openai_provider.py +79 -0
- p2m/testing/__init__.py +41 -0
- p2m/ui/__init__.py +43 -0
- p2m/ui/components.py +301 -0
- python2mobile-1.0.1.dist-info/METADATA +238 -0
- python2mobile-1.0.1.dist-info/RECORD +50 -0
- python2mobile-1.0.1.dist-info/WHEEL +5 -0
- python2mobile-1.0.1.dist-info/entry_points.txt +2 -0
- python2mobile-1.0.1.dist-info/top_level.txt +3 -0
- tests/test_basic_engine.py +281 -0
- tests/test_build_generation.py +603 -0
- tests/test_build_test_gate.py +150 -0
- tests/test_carousel_modal.py +84 -0
- tests/test_config_system.py +272 -0
- tests/test_i18n.py +101 -0
- tests/test_ifood_app_integration.py +172 -0
- tests/test_imagine_cli.py +133 -0
- tests/test_imagine_command.py +341 -0
- tests/test_llm_providers.py +321 -0
- tests/test_new_apps_integration.py +588 -0
- tests/test_ollama_functional.py +329 -0
- tests/test_real_world_apps.py +228 -0
- tests/test_run_integration.py +776 -0
p2m/core/runtime.py
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""
|
|
2
|
+
P2M Runtime Engine - Executes Python code in a safe sandbox
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
import ast
|
|
7
|
+
import inspect
|
|
8
|
+
from typing import Any, Callable, Dict, Optional
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Runtime:
|
|
13
|
+
"""Safe Python runtime for executing user code"""
|
|
14
|
+
|
|
15
|
+
def __init__(self, sandbox: bool = True):
|
|
16
|
+
self.sandbox = sandbox
|
|
17
|
+
self.globals: Dict[str, Any] = {}
|
|
18
|
+
self.locals: Dict[str, Any] = {}
|
|
19
|
+
self._setup_sandbox()
|
|
20
|
+
|
|
21
|
+
def _setup_sandbox(self) -> None:
|
|
22
|
+
"""Setup sandbox environment with allowed builtins"""
|
|
23
|
+
if self.sandbox:
|
|
24
|
+
# Restricted builtins for safety
|
|
25
|
+
allowed_builtins = {
|
|
26
|
+
"abs", "all", "any", "ascii", "bin", "bool", "bytearray",
|
|
27
|
+
"bytes", "chr", "dict", "dir", "divmod", "enumerate", "filter",
|
|
28
|
+
"float", "format", "frozenset", "getattr", "hasattr", "hash",
|
|
29
|
+
"hex", "id", "int", "isinstance", "issubclass", "iter", "len",
|
|
30
|
+
"list", "map", "max", "min", "next", "object", "oct", "ord",
|
|
31
|
+
"pow", "print", "range", "repr", "reversed", "round", "set",
|
|
32
|
+
"slice", "sorted", "str", "sum", "tuple", "type", "zip",
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
self.globals["__builtins__"] = {
|
|
36
|
+
name: __builtins__[name] for name in allowed_builtins
|
|
37
|
+
if name in __builtins__
|
|
38
|
+
}
|
|
39
|
+
else:
|
|
40
|
+
self.globals["__builtins__"] = __builtins__
|
|
41
|
+
|
|
42
|
+
def execute(self, code: str, globals_dict: Optional[Dict[str, Any]] = None) -> Any:
|
|
43
|
+
"""Execute Python code in the sandbox"""
|
|
44
|
+
try:
|
|
45
|
+
# Validate syntax
|
|
46
|
+
ast.parse(code)
|
|
47
|
+
|
|
48
|
+
# Merge globals
|
|
49
|
+
exec_globals = {**self.globals}
|
|
50
|
+
if globals_dict:
|
|
51
|
+
exec_globals.update(globals_dict)
|
|
52
|
+
|
|
53
|
+
# Execute
|
|
54
|
+
exec(code, exec_globals, self.locals)
|
|
55
|
+
return self.locals
|
|
56
|
+
except SyntaxError as e:
|
|
57
|
+
raise RuntimeError(f"Syntax error in code: {e}")
|
|
58
|
+
except Exception as e:
|
|
59
|
+
raise RuntimeError(f"Runtime error: {e}")
|
|
60
|
+
|
|
61
|
+
def execute_function(self, func: Callable, *args, **kwargs) -> Any:
|
|
62
|
+
"""Execute a function with given arguments"""
|
|
63
|
+
try:
|
|
64
|
+
return func(*args, **kwargs)
|
|
65
|
+
except Exception as e:
|
|
66
|
+
raise RuntimeError(f"Function execution error: {e}")
|
|
67
|
+
|
|
68
|
+
def load_module(self, module_path: str) -> Dict[str, Any]:
|
|
69
|
+
"""Load and execute a Python module"""
|
|
70
|
+
path = Path(module_path)
|
|
71
|
+
|
|
72
|
+
if not path.exists():
|
|
73
|
+
raise FileNotFoundError(f"Module not found: {module_path}")
|
|
74
|
+
|
|
75
|
+
if not path.suffix == ".py":
|
|
76
|
+
raise ValueError(f"Invalid module file: {module_path}")
|
|
77
|
+
|
|
78
|
+
with open(path, "r") as f:
|
|
79
|
+
code = f.read()
|
|
80
|
+
|
|
81
|
+
return self.execute(code)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class Render:
|
|
85
|
+
"""Main entry point for rendering P2M applications"""
|
|
86
|
+
|
|
87
|
+
_runtime: Optional[Runtime] = None
|
|
88
|
+
_component_tree: Optional[Any] = None
|
|
89
|
+
|
|
90
|
+
@classmethod
|
|
91
|
+
def execute(cls, view_func: Callable, sandbox: bool = True) -> Any:
|
|
92
|
+
"""Execute a view function and render the component tree"""
|
|
93
|
+
cls._runtime = Runtime(sandbox=sandbox)
|
|
94
|
+
|
|
95
|
+
try:
|
|
96
|
+
# Call the view function
|
|
97
|
+
component_tree = cls._runtime.execute_function(view_func)
|
|
98
|
+
cls._component_tree = component_tree
|
|
99
|
+
return component_tree
|
|
100
|
+
except Exception as e:
|
|
101
|
+
raise RuntimeError(f"Failed to execute view: {e}")
|
|
102
|
+
|
|
103
|
+
@classmethod
|
|
104
|
+
def get_component_tree(cls) -> Optional[Any]:
|
|
105
|
+
"""Get the current component tree"""
|
|
106
|
+
return cls._component_tree
|
|
107
|
+
|
|
108
|
+
@classmethod
|
|
109
|
+
def render_html(cls) -> str:
|
|
110
|
+
"""Render component tree to HTML"""
|
|
111
|
+
from p2m.core.render_engine import RenderEngine
|
|
112
|
+
|
|
113
|
+
if cls._component_tree is None:
|
|
114
|
+
raise RuntimeError("No component tree to render. Call execute() first.")
|
|
115
|
+
|
|
116
|
+
engine = RenderEngine()
|
|
117
|
+
return engine.render(cls._component_tree)
|
|
118
|
+
|
|
119
|
+
@classmethod
|
|
120
|
+
def render_json(cls) -> Dict[str, Any]:
|
|
121
|
+
"""Render component tree to JSON (for mobile apps)"""
|
|
122
|
+
from p2m.core.ast_walker import ASTWalker
|
|
123
|
+
|
|
124
|
+
if cls._component_tree is None:
|
|
125
|
+
raise RuntimeError("No component tree to render. Call execute() first.")
|
|
126
|
+
|
|
127
|
+
walker = ASTWalker()
|
|
128
|
+
return walker.walk(cls._component_tree)
|
p2m/core/state.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""
|
|
2
|
+
P2M State Management - Simple state container for P2M apps
|
|
3
|
+
"""
|
|
4
|
+
from typing import Any, Dict
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class AppState:
|
|
8
|
+
"""
|
|
9
|
+
Simple key-value state store.
|
|
10
|
+
Use module-level instances (singletons) so state is shared across files.
|
|
11
|
+
|
|
12
|
+
Example:
|
|
13
|
+
# state/store.py
|
|
14
|
+
from p2m.core.state import AppState
|
|
15
|
+
store = AppState(counter=0, todos=[])
|
|
16
|
+
|
|
17
|
+
# Any other file
|
|
18
|
+
from state.store import store
|
|
19
|
+
store.counter += 1
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def __init__(self, **initial: Any):
|
|
23
|
+
# Use object.__setattr__ to avoid recursion in __setattr__
|
|
24
|
+
object.__setattr__(self, "_data", dict(initial))
|
|
25
|
+
|
|
26
|
+
def get(self, key: str, default: Any = None) -> Any:
|
|
27
|
+
return self._data.get(key, default)
|
|
28
|
+
|
|
29
|
+
def set(self, key: str, value: Any) -> None:
|
|
30
|
+
self._data[key] = value
|
|
31
|
+
|
|
32
|
+
def update(self, **kwargs: Any) -> None:
|
|
33
|
+
self._data.update(kwargs)
|
|
34
|
+
|
|
35
|
+
def __getattr__(self, key: str) -> Any:
|
|
36
|
+
data = object.__getattribute__(self, "_data")
|
|
37
|
+
if key in data:
|
|
38
|
+
return data[key]
|
|
39
|
+
raise AttributeError(f"AppState has no attribute '{key}'")
|
|
40
|
+
|
|
41
|
+
def __setattr__(self, key: str, value: Any) -> None:
|
|
42
|
+
self._data[key] = value
|
|
43
|
+
|
|
44
|
+
def __contains__(self, key: str) -> bool:
|
|
45
|
+
return key in self._data
|
|
46
|
+
|
|
47
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
48
|
+
return dict(self._data)
|
|
49
|
+
|
|
50
|
+
def __repr__(self) -> str:
|
|
51
|
+
return f"AppState({self._data!r})"
|
p2m/core/validator.py
ADDED
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
"""
|
|
2
|
+
P2M Code Validator - Validates P2M code for syntax and structure errors
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import ast
|
|
6
|
+
import sys
|
|
7
|
+
from typing import List, Tuple, Dict, Any
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ValidationError:
|
|
12
|
+
"""Represents a validation error"""
|
|
13
|
+
|
|
14
|
+
def __init__(self, file: str, line: int, column: int, message: str, severity: str = "error"):
|
|
15
|
+
self.file = file
|
|
16
|
+
self.line = line
|
|
17
|
+
self.column = column
|
|
18
|
+
self.message = message
|
|
19
|
+
self.severity = severity # "error", "warning", "info"
|
|
20
|
+
|
|
21
|
+
def __str__(self) -> str:
|
|
22
|
+
return f"{self.file}:{self.line}:{self.column} [{self.severity.upper()}] {self.message}"
|
|
23
|
+
|
|
24
|
+
def __repr__(self) -> str:
|
|
25
|
+
return self.__str__()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class CodeValidator:
|
|
29
|
+
"""Validates P2M code"""
|
|
30
|
+
|
|
31
|
+
# Required imports for P2M apps
|
|
32
|
+
REQUIRED_IMPORTS = {
|
|
33
|
+
"Render": "from p2m.core import Render",
|
|
34
|
+
"Container": "from p2m.ui import Container",
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
# Required functions
|
|
38
|
+
REQUIRED_FUNCTIONS = [
|
|
39
|
+
"create_view",
|
|
40
|
+
"main",
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
# Recommended patterns
|
|
44
|
+
RECOMMENDED_PATTERNS = {
|
|
45
|
+
"create_view_returns_build": "create_view should return component.build()",
|
|
46
|
+
"main_calls_render": "main should call Render.execute(create_view)",
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
def __init__(self):
|
|
50
|
+
self.errors: List[ValidationError] = []
|
|
51
|
+
self.warnings: List[ValidationError] = []
|
|
52
|
+
|
|
53
|
+
def validate_file(self, file_path: str) -> Tuple[bool, List[ValidationError], List[ValidationError]]:
|
|
54
|
+
"""
|
|
55
|
+
Validate a single Python file
|
|
56
|
+
|
|
57
|
+
Returns: (is_valid, errors, warnings)
|
|
58
|
+
"""
|
|
59
|
+
self.errors = []
|
|
60
|
+
self.warnings = []
|
|
61
|
+
|
|
62
|
+
try:
|
|
63
|
+
with open(file_path, 'r') as f:
|
|
64
|
+
content = f.read()
|
|
65
|
+
|
|
66
|
+
# Check syntax
|
|
67
|
+
self._check_syntax(file_path, content)
|
|
68
|
+
|
|
69
|
+
# Check imports
|
|
70
|
+
self._check_imports(file_path, content)
|
|
71
|
+
|
|
72
|
+
# Check functions
|
|
73
|
+
self._check_functions(file_path, content)
|
|
74
|
+
|
|
75
|
+
# Check patterns
|
|
76
|
+
self._check_patterns(file_path, content)
|
|
77
|
+
|
|
78
|
+
except Exception as e:
|
|
79
|
+
self.errors.append(ValidationError(
|
|
80
|
+
file_path, 0, 0,
|
|
81
|
+
f"Failed to validate file: {str(e)}",
|
|
82
|
+
"error"
|
|
83
|
+
))
|
|
84
|
+
|
|
85
|
+
is_valid = len(self.errors) == 0
|
|
86
|
+
return is_valid, self.errors, self.warnings
|
|
87
|
+
|
|
88
|
+
def validate_project(self, project_dir: str = ".", entry_file: str = "main.py") -> Tuple[bool, List[ValidationError], List[ValidationError]]:
|
|
89
|
+
"""
|
|
90
|
+
Validate all Python files in a project.
|
|
91
|
+
|
|
92
|
+
The entry point file gets full validation (syntax + imports + functions + patterns).
|
|
93
|
+
All other module files get syntax-only validation.
|
|
94
|
+
|
|
95
|
+
Returns: (is_valid, all_errors, all_warnings)
|
|
96
|
+
"""
|
|
97
|
+
all_errors = []
|
|
98
|
+
all_warnings = []
|
|
99
|
+
|
|
100
|
+
project_path = Path(project_dir)
|
|
101
|
+
entry_path = Path(entry_file)
|
|
102
|
+
|
|
103
|
+
# Find all Python files
|
|
104
|
+
py_files = list(project_path.glob("**/*.py"))
|
|
105
|
+
|
|
106
|
+
if not py_files:
|
|
107
|
+
all_errors.append(ValidationError(
|
|
108
|
+
project_dir, 0, 0,
|
|
109
|
+
"No Python files found in project",
|
|
110
|
+
"error"
|
|
111
|
+
))
|
|
112
|
+
return False, all_errors, all_warnings
|
|
113
|
+
|
|
114
|
+
# Validate each file
|
|
115
|
+
for py_file in py_files:
|
|
116
|
+
# Skip __pycache__ and test files
|
|
117
|
+
if "__pycache__" in str(py_file) or py_file.name.startswith("test_"):
|
|
118
|
+
continue
|
|
119
|
+
|
|
120
|
+
# Check if this is the entry point file
|
|
121
|
+
is_entry = (py_file == entry_path or py_file.name == entry_path.name)
|
|
122
|
+
|
|
123
|
+
if is_entry:
|
|
124
|
+
# Full validation for entry point
|
|
125
|
+
is_valid, errors, warnings = self.validate_file(str(py_file))
|
|
126
|
+
else:
|
|
127
|
+
# Syntax-only validation for module files
|
|
128
|
+
is_valid, errors, warnings = self._validate_module(str(py_file))
|
|
129
|
+
|
|
130
|
+
all_errors.extend(errors)
|
|
131
|
+
all_warnings.extend(warnings)
|
|
132
|
+
|
|
133
|
+
is_valid = len(all_errors) == 0
|
|
134
|
+
return is_valid, all_errors, all_warnings
|
|
135
|
+
|
|
136
|
+
def _validate_module(self, file_path: str) -> Tuple[bool, List[ValidationError], List[ValidationError]]:
|
|
137
|
+
"""
|
|
138
|
+
Validate a module file (syntax-only, no import/function/pattern checks).
|
|
139
|
+
|
|
140
|
+
Returns: (is_valid, errors, warnings)
|
|
141
|
+
"""
|
|
142
|
+
errors = []
|
|
143
|
+
warnings = []
|
|
144
|
+
|
|
145
|
+
try:
|
|
146
|
+
with open(file_path, 'r') as f:
|
|
147
|
+
content = f.read()
|
|
148
|
+
|
|
149
|
+
# Only check syntax for module files
|
|
150
|
+
try:
|
|
151
|
+
ast.parse(content)
|
|
152
|
+
except SyntaxError as e:
|
|
153
|
+
errors.append(ValidationError(
|
|
154
|
+
file_path, e.lineno or 0, e.offset or 0,
|
|
155
|
+
f"Syntax error: {e.msg}",
|
|
156
|
+
"error"
|
|
157
|
+
))
|
|
158
|
+
except Exception as e:
|
|
159
|
+
errors.append(ValidationError(
|
|
160
|
+
file_path, 0, 0,
|
|
161
|
+
f"Failed to validate file: {str(e)}",
|
|
162
|
+
"error"
|
|
163
|
+
))
|
|
164
|
+
|
|
165
|
+
is_valid = len(errors) == 0
|
|
166
|
+
return is_valid, errors, warnings
|
|
167
|
+
|
|
168
|
+
def _check_syntax(self, file_path: str, content: str) -> None:
|
|
169
|
+
"""Check Python syntax"""
|
|
170
|
+
try:
|
|
171
|
+
ast.parse(content)
|
|
172
|
+
except SyntaxError as e:
|
|
173
|
+
self.errors.append(ValidationError(
|
|
174
|
+
file_path, e.lineno or 0, e.offset or 0,
|
|
175
|
+
f"Syntax error: {e.msg}",
|
|
176
|
+
"error"
|
|
177
|
+
))
|
|
178
|
+
|
|
179
|
+
def _check_imports(self, file_path: str, content: str) -> None:
|
|
180
|
+
"""Check for required imports"""
|
|
181
|
+
# Check for P2M imports
|
|
182
|
+
has_render_import = "from p2m.core import Render" in content or "from p2m import" in content
|
|
183
|
+
has_ui_import = "from p2m.ui import" in content
|
|
184
|
+
|
|
185
|
+
if not has_render_import:
|
|
186
|
+
self.warnings.append(ValidationError(
|
|
187
|
+
file_path, 1, 0,
|
|
188
|
+
"Missing P2M core import (from p2m.core import Render)",
|
|
189
|
+
"warning"
|
|
190
|
+
))
|
|
191
|
+
|
|
192
|
+
if not has_ui_import:
|
|
193
|
+
self.warnings.append(ValidationError(
|
|
194
|
+
file_path, 1, 0,
|
|
195
|
+
"Missing P2M UI imports (from p2m.ui import ...)",
|
|
196
|
+
"warning"
|
|
197
|
+
))
|
|
198
|
+
|
|
199
|
+
def _check_functions(self, file_path: str, content: str) -> None:
|
|
200
|
+
"""Check for required functions"""
|
|
201
|
+
try:
|
|
202
|
+
tree = ast.parse(content)
|
|
203
|
+
|
|
204
|
+
functions = {node.name for node in ast.walk(tree) if isinstance(node, ast.FunctionDef)}
|
|
205
|
+
|
|
206
|
+
for required_func in self.REQUIRED_FUNCTIONS:
|
|
207
|
+
if required_func not in functions:
|
|
208
|
+
self.errors.append(ValidationError(
|
|
209
|
+
file_path, 0, 0,
|
|
210
|
+
f"Missing required function: {required_func}()",
|
|
211
|
+
"error"
|
|
212
|
+
))
|
|
213
|
+
except:
|
|
214
|
+
pass # Already reported in syntax check
|
|
215
|
+
|
|
216
|
+
def _check_patterns(self, file_path: str, content: str) -> None:
|
|
217
|
+
"""Check for recommended patterns"""
|
|
218
|
+
|
|
219
|
+
# Check if create_view returns .build()
|
|
220
|
+
if "def create_view" in content:
|
|
221
|
+
# Look for .build() in the file
|
|
222
|
+
if ".build()" not in content:
|
|
223
|
+
self.warnings.append(ValidationError(
|
|
224
|
+
file_path, 0, 0,
|
|
225
|
+
"create_view should return component.build()",
|
|
226
|
+
"warning"
|
|
227
|
+
))
|
|
228
|
+
|
|
229
|
+
# Check if main calls Render.execute
|
|
230
|
+
if "def main" in content:
|
|
231
|
+
if "Render.execute" not in content:
|
|
232
|
+
self.warnings.append(ValidationError(
|
|
233
|
+
file_path, 0, 0,
|
|
234
|
+
"main should call Render.execute(create_view)",
|
|
235
|
+
"warning"
|
|
236
|
+
))
|
|
237
|
+
|
|
238
|
+
def print_report(self, is_valid: bool, errors: List[ValidationError], warnings: List[ValidationError]) -> None:
|
|
239
|
+
"""Print validation report"""
|
|
240
|
+
|
|
241
|
+
print("\n" + "="*70)
|
|
242
|
+
print("📋 Code Validation Report")
|
|
243
|
+
print("="*70)
|
|
244
|
+
|
|
245
|
+
if errors:
|
|
246
|
+
print(f"\n❌ Errors ({len(errors)}):")
|
|
247
|
+
for error in errors:
|
|
248
|
+
print(f" {error}")
|
|
249
|
+
|
|
250
|
+
if warnings:
|
|
251
|
+
print(f"\n⚠️ Warnings ({len(warnings)}):")
|
|
252
|
+
for warning in warnings:
|
|
253
|
+
print(f" {warning}")
|
|
254
|
+
|
|
255
|
+
if is_valid and not warnings:
|
|
256
|
+
print("\n✅ All validations passed!")
|
|
257
|
+
elif is_valid:
|
|
258
|
+
print(f"\n✅ Code is valid ({len(warnings)} warning(s))")
|
|
259
|
+
else:
|
|
260
|
+
print(f"\n❌ Validation failed ({len(errors)} error(s))")
|
|
261
|
+
|
|
262
|
+
print("="*70 + "\n")
|
|
263
|
+
|
|
264
|
+
def get_summary(self, is_valid: bool, errors: List[ValidationError], warnings: List[ValidationError]) -> str:
|
|
265
|
+
"""Get a summary of validation results"""
|
|
266
|
+
|
|
267
|
+
if not is_valid:
|
|
268
|
+
return f"❌ Validation failed: {len(errors)} error(s), {len(warnings)} warning(s)"
|
|
269
|
+
elif warnings:
|
|
270
|
+
return f"⚠️ Validation passed with {len(warnings)} warning(s)"
|
|
271
|
+
else:
|
|
272
|
+
return "✅ Validation passed"
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def validate_project(project_dir: str = ".", entry_file: str = "main.py") -> Tuple[bool, List[ValidationError], List[ValidationError]]:
|
|
276
|
+
"""Validate a P2M project"""
|
|
277
|
+
validator = CodeValidator()
|
|
278
|
+
return validator.validate_project(project_dir, entry_file=entry_file)
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def validate_file(file_path: str) -> Tuple[bool, List[ValidationError], List[ValidationError]]:
|
|
282
|
+
"""Validate a single P2M file"""
|
|
283
|
+
validator = CodeValidator()
|
|
284
|
+
return validator.validate_file(file_path)
|
p2m/devserver/server.py
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""
|
|
2
|
+
P2M DevServer - FastAPI + WebSocket live reload and event bridge.
|
|
3
|
+
|
|
4
|
+
Flow:
|
|
5
|
+
Browser click → WS → dispatch(action) → handler updates state
|
|
6
|
+
→ Render.execute(view_func) → render_content() → WS → browser innerHTML swap
|
|
7
|
+
"""
|
|
8
|
+
import json
|
|
9
|
+
from typing import Callable, Optional
|
|
10
|
+
|
|
11
|
+
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
|
|
12
|
+
from fastapi.responses import HTMLResponse
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def start_server(
|
|
16
|
+
html_content: str,
|
|
17
|
+
port: int = 3000,
|
|
18
|
+
view_func: Optional[Callable] = None,
|
|
19
|
+
project_dir: str = ".",
|
|
20
|
+
):
|
|
21
|
+
from p2m.core import events
|
|
22
|
+
from p2m.core.render_engine import RenderEngine
|
|
23
|
+
from p2m.core.runtime import Render
|
|
24
|
+
from fastapi.staticfiles import StaticFiles
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
|
|
27
|
+
app = FastAPI()
|
|
28
|
+
app.state.html_content = html_content
|
|
29
|
+
app.state.view_func = view_func
|
|
30
|
+
|
|
31
|
+
assets_path = Path(project_dir) / "assets"
|
|
32
|
+
if assets_path.is_dir():
|
|
33
|
+
app.mount("/assets", StaticFiles(directory=str(assets_path)), name="assets")
|
|
34
|
+
|
|
35
|
+
@app.get("/", response_class=HTMLResponse)
|
|
36
|
+
async def root():
|
|
37
|
+
return app.state.html_content
|
|
38
|
+
|
|
39
|
+
@app.websocket("/ws")
|
|
40
|
+
async def ws_endpoint(websocket: WebSocket):
|
|
41
|
+
await websocket.accept()
|
|
42
|
+
try:
|
|
43
|
+
while True:
|
|
44
|
+
raw = await websocket.receive_text()
|
|
45
|
+
try:
|
|
46
|
+
data = json.loads(raw)
|
|
47
|
+
etype = data.get("type", "")
|
|
48
|
+
action = data.get("action", "")
|
|
49
|
+
|
|
50
|
+
if etype == "click":
|
|
51
|
+
args = data.get("args", [])
|
|
52
|
+
events.dispatch(action, *args)
|
|
53
|
+
elif etype in ("change", "input"):
|
|
54
|
+
value = data.get("value", "")
|
|
55
|
+
events.dispatch(action, value)
|
|
56
|
+
|
|
57
|
+
# Re-render and push new content
|
|
58
|
+
if app.state.view_func:
|
|
59
|
+
tree = Render.execute(app.state.view_func)
|
|
60
|
+
engine = RenderEngine()
|
|
61
|
+
content = engine.render_content(tree)
|
|
62
|
+
await websocket.send_text(
|
|
63
|
+
json.dumps({"type": "render", "html": content})
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
except json.JSONDecodeError:
|
|
67
|
+
pass
|
|
68
|
+
except Exception as exc:
|
|
69
|
+
import traceback
|
|
70
|
+
traceback.print_exc()
|
|
71
|
+
try:
|
|
72
|
+
await websocket.send_text(
|
|
73
|
+
json.dumps({"type": "error", "message": str(exc)})
|
|
74
|
+
)
|
|
75
|
+
except Exception:
|
|
76
|
+
pass
|
|
77
|
+
|
|
78
|
+
except WebSocketDisconnect:
|
|
79
|
+
pass
|
|
80
|
+
except Exception as exc:
|
|
81
|
+
print(f"[P2M] WebSocket fatal: {exc}")
|
|
82
|
+
|
|
83
|
+
import uvicorn
|
|
84
|
+
uvicorn.run(app, host="0.0.0.0", port=port, log_level="warning")
|
p2m/i18n/__init__.py
ADDED
p2m/i18n/translator.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""
|
|
2
|
+
P2M i18n — Simple JSON-based translator.
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
from p2m.i18n import configure, set_locale, get_locale, t
|
|
6
|
+
|
|
7
|
+
configure("locales/", default_locale="pt")
|
|
8
|
+
t("search_placeholder") # "Buscar restaurantes..."
|
|
9
|
+
t("greeting", name="João") # "Olá, João!"
|
|
10
|
+
set_locale("en")
|
|
11
|
+
t("search_placeholder") # "Search restaurants..."
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import json
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Any, Dict, Optional
|
|
17
|
+
|
|
18
|
+
_locale: str = "en"
|
|
19
|
+
_translations: Dict[str, Dict[str, str]] = {}
|
|
20
|
+
_locales_dir: Optional[Path] = None
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def configure(locales_dir: str, default_locale: str = "en") -> None:
|
|
24
|
+
"""Point to the locales directory and set the default locale."""
|
|
25
|
+
global _locales_dir, _locale, _translations
|
|
26
|
+
_locales_dir = Path(locales_dir)
|
|
27
|
+
_translations = {}
|
|
28
|
+
_locale = default_locale
|
|
29
|
+
_load_locale(default_locale)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def set_locale(locale: str) -> None:
|
|
33
|
+
"""Switch to a different locale (loads JSON if not yet cached)."""
|
|
34
|
+
global _locale
|
|
35
|
+
_locale = locale
|
|
36
|
+
_load_locale(locale)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def get_locale() -> str:
|
|
40
|
+
"""Return the currently active locale code."""
|
|
41
|
+
return _locale
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def t(key: str, **kwargs: Any) -> str:
|
|
45
|
+
"""Look up *key* in the current locale's translations.
|
|
46
|
+
|
|
47
|
+
Falls back to the key string itself if the key is missing.
|
|
48
|
+
Supports keyword formatting: t("greeting", name="João") with
|
|
49
|
+
{"greeting": "Olá, {name}!"} → "Olá, João!"
|
|
50
|
+
"""
|
|
51
|
+
locale_dict = _translations.get(_locale, {})
|
|
52
|
+
template = locale_dict.get(key, key)
|
|
53
|
+
if kwargs:
|
|
54
|
+
try:
|
|
55
|
+
return template.format(**kwargs)
|
|
56
|
+
except (KeyError, ValueError):
|
|
57
|
+
return template
|
|
58
|
+
return template
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
# ── Internal ──────────────────────────────────────────────────────────────────
|
|
62
|
+
|
|
63
|
+
def _load_locale(locale: str) -> None:
|
|
64
|
+
if locale in _translations:
|
|
65
|
+
return
|
|
66
|
+
if _locales_dir is None:
|
|
67
|
+
_translations[locale] = {}
|
|
68
|
+
return
|
|
69
|
+
json_path = _locales_dir / f"{locale}.json"
|
|
70
|
+
if json_path.is_file():
|
|
71
|
+
with open(json_path, encoding="utf-8") as fh:
|
|
72
|
+
_translations[locale] = json.load(fh)
|
|
73
|
+
else:
|
|
74
|
+
_translations[locale] = {}
|
p2m/imagine/__init__.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""
|
|
2
|
+
P2M Imagine — Generate complete multi-file P2M projects from natural language.
|
|
3
|
+
|
|
4
|
+
Two modes:
|
|
5
|
+
- Agent mode (default): uses Agno + LLM to create full project structure
|
|
6
|
+
- Legacy mode (--no-agent): generates a single Python file via direct LLM call
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
from typing import Optional
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def agent_available(provider: str = "openai", api_key: Optional[str] = None) -> bool:
|
|
14
|
+
"""
|
|
15
|
+
Return True when agent-based generation is possible.
|
|
16
|
+
|
|
17
|
+
Requires:
|
|
18
|
+
- `agno` installed
|
|
19
|
+
- A valid API key (argument, env var, or config)
|
|
20
|
+
"""
|
|
21
|
+
try:
|
|
22
|
+
import agno # noqa: F401
|
|
23
|
+
except ImportError:
|
|
24
|
+
return False
|
|
25
|
+
|
|
26
|
+
if provider.lower() == "anthropic":
|
|
27
|
+
return bool(api_key or os.environ.get("ANTHROPIC_API_KEY"))
|
|
28
|
+
# openai / openai-compatible / default
|
|
29
|
+
return bool(api_key or os.environ.get("OPENAI_API_KEY"))
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
from p2m.imagine.legacy import imagine_command # noqa: E402
|
|
33
|
+
from p2m.imagine.agent import run_imagine_agent # noqa: E402
|
|
34
|
+
|
|
35
|
+
__all__ = ["imagine_command", "run_imagine_agent", "agent_available"]
|