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.
Files changed (50) hide show
  1. examples/example_ecommerce_app.py +189 -0
  2. examples/example_todo_app.py +159 -0
  3. p2m/__init__.py +31 -0
  4. p2m/cli.py +470 -0
  5. p2m/config.py +205 -0
  6. p2m/core/__init__.py +18 -0
  7. p2m/core/api.py +191 -0
  8. p2m/core/ast_walker.py +171 -0
  9. p2m/core/database.py +192 -0
  10. p2m/core/events.py +56 -0
  11. p2m/core/render_engine.py +597 -0
  12. p2m/core/runtime.py +128 -0
  13. p2m/core/state.py +51 -0
  14. p2m/core/validator.py +284 -0
  15. p2m/devserver/__init__.py +9 -0
  16. p2m/devserver/server.py +84 -0
  17. p2m/i18n/__init__.py +7 -0
  18. p2m/i18n/translator.py +74 -0
  19. p2m/imagine/__init__.py +35 -0
  20. p2m/imagine/agent.py +463 -0
  21. p2m/imagine/legacy.py +217 -0
  22. p2m/llm/__init__.py +20 -0
  23. p2m/llm/anthropic_provider.py +78 -0
  24. p2m/llm/base.py +42 -0
  25. p2m/llm/compatible_provider.py +120 -0
  26. p2m/llm/factory.py +72 -0
  27. p2m/llm/ollama_provider.py +89 -0
  28. p2m/llm/openai_provider.py +79 -0
  29. p2m/testing/__init__.py +41 -0
  30. p2m/ui/__init__.py +43 -0
  31. p2m/ui/components.py +301 -0
  32. python2mobile-1.0.1.dist-info/METADATA +238 -0
  33. python2mobile-1.0.1.dist-info/RECORD +50 -0
  34. python2mobile-1.0.1.dist-info/WHEEL +5 -0
  35. python2mobile-1.0.1.dist-info/entry_points.txt +2 -0
  36. python2mobile-1.0.1.dist-info/top_level.txt +3 -0
  37. tests/test_basic_engine.py +281 -0
  38. tests/test_build_generation.py +603 -0
  39. tests/test_build_test_gate.py +150 -0
  40. tests/test_carousel_modal.py +84 -0
  41. tests/test_config_system.py +272 -0
  42. tests/test_i18n.py +101 -0
  43. tests/test_ifood_app_integration.py +172 -0
  44. tests/test_imagine_cli.py +133 -0
  45. tests/test_imagine_command.py +341 -0
  46. tests/test_llm_providers.py +321 -0
  47. tests/test_new_apps_integration.py +588 -0
  48. tests/test_ollama_functional.py +329 -0
  49. tests/test_real_world_apps.py +228 -0
  50. 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)
@@ -0,0 +1,9 @@
1
+ """
2
+ P2M DevServer - Development server with hot reload
3
+ """
4
+
5
+ from p2m.devserver.server import start_server
6
+
7
+ __all__ = [
8
+ "start_server",
9
+ ]
@@ -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
@@ -0,0 +1,7 @@
1
+ """
2
+ P2M i18n — Internationalisation helpers.
3
+ """
4
+
5
+ from p2m.i18n.translator import configure, set_locale, get_locale, t
6
+
7
+ __all__ = ["configure", "set_locale", "get_locale", "t"]
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] = {}
@@ -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"]