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/cli.py
ADDED
|
@@ -0,0 +1,470 @@
|
|
|
1
|
+
"""
|
|
2
|
+
P2M CLI - Command-line interface
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
import os
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from p2m.config import Config
|
|
9
|
+
from p2m.core.runtime import Render
|
|
10
|
+
from p2m.core.render_engine import RenderEngine
|
|
11
|
+
from p2m.core.validator import CodeValidator
|
|
12
|
+
from p2m.build.generator import CodeGenerator
|
|
13
|
+
from p2m.build.agent_generator import AgentCodeGenerator, agent_available, print_run_instructions
|
|
14
|
+
from p2m.imagine import imagine_command, run_imagine_agent, agent_available as imagine_agent_available
|
|
15
|
+
import sys
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@click.group()
|
|
19
|
+
def cli():
|
|
20
|
+
"""Python2Mobile - Write mobile apps in pure Python"""
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@cli.command()
|
|
25
|
+
@click.option("--port", default=None, type=int, help="Dev server port (default: from p2m.toml or 3000)")
|
|
26
|
+
@click.option("--no-frame", is_flag=True, help="Disable mobile frame")
|
|
27
|
+
@click.option("--skip-validation", is_flag=True, help="Skip code validation")
|
|
28
|
+
def run(port: int, no_frame: bool, skip_validation: bool):
|
|
29
|
+
"""Run app in development mode with hot reload"""
|
|
30
|
+
|
|
31
|
+
click.echo("š Python2Mobile Dev Server")
|
|
32
|
+
|
|
33
|
+
# Load config
|
|
34
|
+
config = Config()
|
|
35
|
+
|
|
36
|
+
# Port priority: CLI flag > p2m.toml > default 3000
|
|
37
|
+
if port is None:
|
|
38
|
+
port = config.devserver.port
|
|
39
|
+
|
|
40
|
+
# Validate code before running
|
|
41
|
+
if not skip_validation:
|
|
42
|
+
click.echo("š Validating code...")
|
|
43
|
+
validator = CodeValidator()
|
|
44
|
+
is_valid, errors, warnings = validator.validate_project(".", entry_file=config.project.entry)
|
|
45
|
+
|
|
46
|
+
if errors:
|
|
47
|
+
click.echo("\nā Validation failed:")
|
|
48
|
+
for error in errors:
|
|
49
|
+
click.echo(f" {error}")
|
|
50
|
+
click.echo("\nš” Fix the errors above or use --skip-validation to ignore", err=True)
|
|
51
|
+
return
|
|
52
|
+
|
|
53
|
+
if warnings:
|
|
54
|
+
click.echo(f"\nā ļø {len(warnings)} warning(s) found:")
|
|
55
|
+
for warning in warnings:
|
|
56
|
+
click.echo(f" {warning}")
|
|
57
|
+
|
|
58
|
+
if not errors:
|
|
59
|
+
click.echo("ā
Code validation passed\n")
|
|
60
|
+
|
|
61
|
+
click.echo(f"š± Starting on http://localhost:{port}")
|
|
62
|
+
|
|
63
|
+
# Load and execute the entry point
|
|
64
|
+
entry_file = config.project.entry
|
|
65
|
+
|
|
66
|
+
if not Path(entry_file).exists():
|
|
67
|
+
click.echo(f"ā Entry file not found: {entry_file}", err=True)
|
|
68
|
+
return
|
|
69
|
+
|
|
70
|
+
try:
|
|
71
|
+
import sys
|
|
72
|
+
import importlib.util
|
|
73
|
+
|
|
74
|
+
# Add project directory to sys.path so multi-file imports work
|
|
75
|
+
project_dir = str(Path(entry_file).parent.absolute())
|
|
76
|
+
if project_dir not in sys.path:
|
|
77
|
+
sys.path.insert(0, project_dir)
|
|
78
|
+
|
|
79
|
+
# Load the app module
|
|
80
|
+
spec = importlib.util.spec_from_file_location("p2m_app", entry_file)
|
|
81
|
+
module = importlib.util.module_from_spec(spec)
|
|
82
|
+
sys.modules["p2m_app"] = module
|
|
83
|
+
spec.loader.exec_module(module)
|
|
84
|
+
|
|
85
|
+
# Ensure create_view exists
|
|
86
|
+
if not hasattr(module, "create_view"):
|
|
87
|
+
click.echo("ā create_view() function not found in entry file", err=True)
|
|
88
|
+
return
|
|
89
|
+
|
|
90
|
+
# Initial render
|
|
91
|
+
component_tree = Render.execute(module.create_view)
|
|
92
|
+
engine = RenderEngine()
|
|
93
|
+
html = engine.render(component_tree, mobile_frame=not no_frame)
|
|
94
|
+
|
|
95
|
+
# Start dev server ā pass view_func for live re-renders
|
|
96
|
+
from p2m.devserver.server import start_server
|
|
97
|
+
click.echo(f"š Dev server running at http://localhost:{port}")
|
|
98
|
+
click.echo(f"š± Open in browser to see your app")
|
|
99
|
+
click.echo(f"š„ Interactive mode ā events are handled server-side\n")
|
|
100
|
+
start_server(html, port=port, view_func=module.create_view, project_dir=project_dir)
|
|
101
|
+
|
|
102
|
+
except Exception as e:
|
|
103
|
+
click.echo(f"ā Error: {e}", err=True)
|
|
104
|
+
import traceback
|
|
105
|
+
traceback.print_exc()
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@cli.command()
|
|
109
|
+
@click.option("--target", type=click.Choice(["flutter", "react-native", "web", "android", "ios"]),
|
|
110
|
+
default="flutter", help="Build target")
|
|
111
|
+
@click.option("--force", is_flag=True, help="Force rebuild")
|
|
112
|
+
@click.option("--skip-validation", is_flag=True, help="Skip code validation")
|
|
113
|
+
@click.option("--skip-tests", is_flag=True, help="Skip unit tests")
|
|
114
|
+
@click.option("--no-agent", is_flag=True, help="Use legacy LLM generator instead of Agno agent")
|
|
115
|
+
@click.option("--skip-validate-fix", is_flag=True,
|
|
116
|
+
help="Skip Stage 3 Validate & Fix (run toolchain + AI fixer)")
|
|
117
|
+
@click.option("--max-fix-iterations", default=5, show_default=True,
|
|
118
|
+
help="Max number of fix cycles in Stage 3")
|
|
119
|
+
@click.option("--skip-preflight", is_flag=True,
|
|
120
|
+
help="Skip platform prerequisite checks (flutter, swift, node, java)")
|
|
121
|
+
def build(target: str, force: bool, skip_validation: bool, skip_tests: bool, no_agent: bool,
|
|
122
|
+
skip_validate_fix: bool, max_fix_iterations: int, skip_preflight: bool):
|
|
123
|
+
"""Build app for production (Flutter, React Native, Web, Android, iOS)"""
|
|
124
|
+
|
|
125
|
+
click.echo(f"šØ Building for {target}...")
|
|
126
|
+
|
|
127
|
+
# āā Platform prerequisite check āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
128
|
+
if not skip_preflight:
|
|
129
|
+
from p2m.build.preflight import check_platform, print_preflight_result
|
|
130
|
+
preflight = check_platform(target)
|
|
131
|
+
if preflight is not None:
|
|
132
|
+
if preflight.ok:
|
|
133
|
+
# Show one-line success per tool
|
|
134
|
+
for c in preflight.checks:
|
|
135
|
+
ver = f" ({c.version})" if c.version else ""
|
|
136
|
+
click.echo(f" ā
{c.name}{ver}")
|
|
137
|
+
else:
|
|
138
|
+
click.echo(f"\nā Prerequisites missing for '{target}' ā cannot build.\n")
|
|
139
|
+
print_preflight_result(preflight)
|
|
140
|
+
click.echo(
|
|
141
|
+
"š” Install the missing tools above, then run `p2m build` again.\n"
|
|
142
|
+
" (Use --skip-preflight to bypass this check.)",
|
|
143
|
+
err=True,
|
|
144
|
+
)
|
|
145
|
+
sys.exit(1)
|
|
146
|
+
|
|
147
|
+
# Load config
|
|
148
|
+
config = Config()
|
|
149
|
+
|
|
150
|
+
# Validate code before building
|
|
151
|
+
if not skip_validation:
|
|
152
|
+
click.echo("š Validating code...")
|
|
153
|
+
validator = CodeValidator()
|
|
154
|
+
is_valid, errors, warnings = validator.validate_project(".", entry_file=config.project.entry)
|
|
155
|
+
|
|
156
|
+
if errors:
|
|
157
|
+
click.echo("\nā Validation failed:")
|
|
158
|
+
for error in errors:
|
|
159
|
+
click.echo(f" {error}")
|
|
160
|
+
click.echo("\nš” Fix the errors above or use --skip-validation to ignore", err=True)
|
|
161
|
+
sys.exit(1)
|
|
162
|
+
|
|
163
|
+
if warnings:
|
|
164
|
+
click.echo(f"\nā ļø {len(warnings)} warning(s) found:")
|
|
165
|
+
for warning in warnings:
|
|
166
|
+
click.echo(f" {warning}")
|
|
167
|
+
|
|
168
|
+
if not errors:
|
|
169
|
+
click.echo("ā
Code validation passed\n")
|
|
170
|
+
|
|
171
|
+
# Run unit tests before building
|
|
172
|
+
if not skip_tests:
|
|
173
|
+
tests_dir = Path("tests")
|
|
174
|
+
if tests_dir.is_dir():
|
|
175
|
+
import pytest
|
|
176
|
+
click.echo("š§Ŗ Running unit tests...")
|
|
177
|
+
project_dir = str(Path(".").resolve())
|
|
178
|
+
if project_dir not in sys.path:
|
|
179
|
+
sys.path.insert(0, project_dir)
|
|
180
|
+
result = pytest.main([str(tests_dir), "--tb=short", "-q"])
|
|
181
|
+
if result != 0:
|
|
182
|
+
click.echo("\nā Tests failed ā build aborted.", err=True)
|
|
183
|
+
click.echo("š” Fix the failing tests or use --skip-tests to bypass", err=True)
|
|
184
|
+
sys.exit(result)
|
|
185
|
+
click.echo("ā
All tests passed\n")
|
|
186
|
+
else:
|
|
187
|
+
click.echo("ā ļø No tests/ directory found ā skipping tests\n")
|
|
188
|
+
|
|
189
|
+
# Create output directory
|
|
190
|
+
output_dir = Path(config.build.output_dir) / target
|
|
191
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
192
|
+
|
|
193
|
+
# āā Agent-based generation (preferred when agno + API key are available) āā
|
|
194
|
+
use_agent = (not no_agent) and agent_available(config) and target in AgentCodeGenerator.SUPPORTED_TARGETS
|
|
195
|
+
|
|
196
|
+
if use_agent:
|
|
197
|
+
click.echo(f"š¤ Using Agno agent for {target} generation...")
|
|
198
|
+
try:
|
|
199
|
+
agent_gen = AgentCodeGenerator(
|
|
200
|
+
config,
|
|
201
|
+
project_dir=".",
|
|
202
|
+
validate_fix=not skip_validate_fix,
|
|
203
|
+
max_fix_iterations=max_fix_iterations,
|
|
204
|
+
)
|
|
205
|
+
agent_gen.generate(target, str(output_dir))
|
|
206
|
+
click.echo(f"ā
Build complete: {output_dir}")
|
|
207
|
+
return
|
|
208
|
+
except Exception as e:
|
|
209
|
+
# Re-raise immediately ā never silently fall back to the legacy generator.
|
|
210
|
+
# The legacy generator produces incomplete output and masks real errors.
|
|
211
|
+
# Fix the root cause instead.
|
|
212
|
+
import traceback
|
|
213
|
+
click.echo(f"ā Agent build failed: {e}", err=True)
|
|
214
|
+
click.echo(traceback.format_exc(), err=True)
|
|
215
|
+
raise SystemExit(1)
|
|
216
|
+
|
|
217
|
+
# āā Legacy LLM generator āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
218
|
+
try:
|
|
219
|
+
generator = CodeGenerator(config)
|
|
220
|
+
project_files = generator.load_project_files(".")
|
|
221
|
+
|
|
222
|
+
if not project_files:
|
|
223
|
+
click.echo("ā No Python files found in project", err=True)
|
|
224
|
+
return
|
|
225
|
+
|
|
226
|
+
click.echo(f"š Generating {target} code...")
|
|
227
|
+
|
|
228
|
+
if target == "flutter":
|
|
229
|
+
click.echo("š± Generating Flutter (Dart) for Android/iOS...")
|
|
230
|
+
generator.generate_flutter(project_files, str(output_dir))
|
|
231
|
+
elif target == "react-native":
|
|
232
|
+
click.echo("āļø Generating React Native (TypeScript)...")
|
|
233
|
+
generator.generate_react_native(project_files, str(output_dir))
|
|
234
|
+
elif target == "web":
|
|
235
|
+
click.echo("š Generating Web (HTML/CSS/JS)...")
|
|
236
|
+
generator.generate_web(project_files, str(output_dir))
|
|
237
|
+
elif target == "android":
|
|
238
|
+
click.echo("š¤ Generating Android (Java + XML)...")
|
|
239
|
+
generator.generate_android(project_files, str(output_dir))
|
|
240
|
+
elif target == "ios":
|
|
241
|
+
click.echo("š Generating iOS (Swift)...")
|
|
242
|
+
generator.generate_ios(project_files, str(output_dir))
|
|
243
|
+
|
|
244
|
+
click.echo(f"ā
Build complete: {output_dir}")
|
|
245
|
+
print_run_instructions(target, str(output_dir))
|
|
246
|
+
|
|
247
|
+
except Exception as e:
|
|
248
|
+
click.echo(f"ā Build failed: {e}", err=True)
|
|
249
|
+
import traceback
|
|
250
|
+
traceback.print_exc()
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
@cli.command()
|
|
254
|
+
@click.argument("name")
|
|
255
|
+
def new(name: str):
|
|
256
|
+
"""Create a new P2M project"""
|
|
257
|
+
|
|
258
|
+
click.echo(f"š¦ Creating new project: {name}")
|
|
259
|
+
|
|
260
|
+
project_dir = Path(name)
|
|
261
|
+
project_dir.mkdir(exist_ok=True)
|
|
262
|
+
|
|
263
|
+
# Create main.py
|
|
264
|
+
main_py = project_dir / "main.py"
|
|
265
|
+
main_py.write_text("""from p2m.core import Render
|
|
266
|
+
from p2m.ui import Container, Text, Button
|
|
267
|
+
|
|
268
|
+
def click_button():
|
|
269
|
+
print("Button clicked!")
|
|
270
|
+
|
|
271
|
+
def create_view():
|
|
272
|
+
container = Container(class_="bg-gray-100 min-h-screen flex items-center justify-center")
|
|
273
|
+
inner = Container(class_="text-center space-y-6 p-8 bg-white rounded-2xl shadow-lg")
|
|
274
|
+
|
|
275
|
+
text = Text("Welcome to P2M", class_="text-gray-800 text-2xl font-bold")
|
|
276
|
+
button = Button(
|
|
277
|
+
"Click Me",
|
|
278
|
+
class_="bg-blue-600 text-white font-semibold py-3 px-8 rounded-xl hover:bg-blue-700",
|
|
279
|
+
on_click=click_button
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
inner.add(text).add(button)
|
|
283
|
+
container.add(inner)
|
|
284
|
+
return container.build()
|
|
285
|
+
|
|
286
|
+
def main():
|
|
287
|
+
Render.execute(create_view)
|
|
288
|
+
|
|
289
|
+
if __name__ == "__main__":
|
|
290
|
+
main()
|
|
291
|
+
""")
|
|
292
|
+
|
|
293
|
+
# Create p2m.toml
|
|
294
|
+
toml_file = project_dir / "p2m.toml"
|
|
295
|
+
toml_file.write_text(f"""[project]
|
|
296
|
+
name = "{name}"
|
|
297
|
+
version = "0.1.0"
|
|
298
|
+
entry = "main.py"
|
|
299
|
+
|
|
300
|
+
[build]
|
|
301
|
+
target = ["android", "ios"]
|
|
302
|
+
generator = "flutter"
|
|
303
|
+
llm_provider = "openai"
|
|
304
|
+
llm_model = "gpt-4o"
|
|
305
|
+
output_dir = "./build"
|
|
306
|
+
cache = true
|
|
307
|
+
|
|
308
|
+
[devserver]
|
|
309
|
+
port = 3000
|
|
310
|
+
hot_reload = true
|
|
311
|
+
mobile_frame = true
|
|
312
|
+
|
|
313
|
+
[style]
|
|
314
|
+
system = "tailwind"
|
|
315
|
+
""")
|
|
316
|
+
|
|
317
|
+
click.echo(f"ā
Project created: {project_dir}")
|
|
318
|
+
click.echo(f"š Next steps:")
|
|
319
|
+
click.echo(f" cd {name}")
|
|
320
|
+
click.echo(f" p2m run")
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
@cli.command()
|
|
324
|
+
@click.argument("description")
|
|
325
|
+
@click.option("--provider", default="openai", help="LLM provider: openai, anthropic")
|
|
326
|
+
@click.option("--model", default=None, help="Model name (defaults based on provider)")
|
|
327
|
+
@click.option("--api-key", default=None, help="API key (or use environment variable)")
|
|
328
|
+
@click.option("--output", default=None, help="Output directory (defaults to project name)")
|
|
329
|
+
@click.option("--no-agent", is_flag=True, help="Use legacy single-file generator instead of agent")
|
|
330
|
+
@click.option("--base-url", default=None, help="Base URL for OpenAI-compatible provider")
|
|
331
|
+
@click.option("--x-api-key", default=None, help="Custom API key header (openai-compatible)")
|
|
332
|
+
@click.option("--no-validate", is_flag=True, help="Skip validation (legacy mode only)")
|
|
333
|
+
def imagine(description: str, provider: str, model: str, api_key: str,
|
|
334
|
+
output: str, no_agent: bool, base_url: str, x_api_key: str,
|
|
335
|
+
no_validate: bool):
|
|
336
|
+
"""Generate a complete P2M project from a natural language description"""
|
|
337
|
+
|
|
338
|
+
import re
|
|
339
|
+
|
|
340
|
+
click.echo("šØ Python2Mobile Imagine")
|
|
341
|
+
click.echo(f"š Description: {description}\n")
|
|
342
|
+
|
|
343
|
+
use_agent = not no_agent and imagine_agent_available(provider, api_key)
|
|
344
|
+
|
|
345
|
+
if use_agent:
|
|
346
|
+
# āā Agent mode: full multi-file project āāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
347
|
+
# Derive a short snake_case project name from the first 3 meaningful words
|
|
348
|
+
_stopwords = {"a", "an", "the", "with", "and", "or", "of", "to", "for",
|
|
349
|
+
"in", "on", "at", "by", "from", "that", "this", "is", "are"}
|
|
350
|
+
_words = [w for w in re.sub(r"[^a-z0-9 ]", "", description.lower()).split()
|
|
351
|
+
if w not in _stopwords]
|
|
352
|
+
project_name = "_".join(_words[:3]) or "p2m_app"
|
|
353
|
+
output_dir = output or project_name
|
|
354
|
+
|
|
355
|
+
click.echo(f"š¤ Using Agno agent ({provider})...")
|
|
356
|
+
click.echo(f" Project name : {project_name}")
|
|
357
|
+
click.echo(f" Output dir : {output_dir}\n")
|
|
358
|
+
|
|
359
|
+
Path(output_dir).mkdir(parents=True, exist_ok=True)
|
|
360
|
+
|
|
361
|
+
try:
|
|
362
|
+
run_imagine_agent(
|
|
363
|
+
description=description,
|
|
364
|
+
output_dir=output_dir,
|
|
365
|
+
project_name=project_name,
|
|
366
|
+
model_provider=provider,
|
|
367
|
+
model_name=model,
|
|
368
|
+
api_key=api_key,
|
|
369
|
+
)
|
|
370
|
+
except (ImportError, RuntimeError) as exc:
|
|
371
|
+
click.echo(f"ā Agent error: {exc}", err=True)
|
|
372
|
+
sys.exit(1)
|
|
373
|
+
|
|
374
|
+
click.echo(f"\nā
Project generated in: {output_dir}/")
|
|
375
|
+
click.echo(f"\nš” Next steps:")
|
|
376
|
+
click.echo(f" cd {output_dir}")
|
|
377
|
+
click.echo(f" p2m run")
|
|
378
|
+
click.echo(f" p2m test tests/")
|
|
379
|
+
click.echo(f" p2m build --target flutter")
|
|
380
|
+
|
|
381
|
+
else:
|
|
382
|
+
# āā Legacy mode: single generated_app.py āāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
383
|
+
if not no_agent:
|
|
384
|
+
click.echo("ā¹ļø agno not installed or API key missing ā using legacy generator.")
|
|
385
|
+
click.echo(" Install agno and set OPENAI_API_KEY for full project generation.\n")
|
|
386
|
+
|
|
387
|
+
output_file = output or "generated_app.py"
|
|
388
|
+
|
|
389
|
+
success, message, code = imagine_command(
|
|
390
|
+
description=description,
|
|
391
|
+
provider=provider,
|
|
392
|
+
model=model,
|
|
393
|
+
api_key=api_key,
|
|
394
|
+
base_url=base_url,
|
|
395
|
+
x_api_key=x_api_key,
|
|
396
|
+
output=output_file,
|
|
397
|
+
validate=not no_validate,
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
if success:
|
|
401
|
+
click.echo(f"\nā
{message}")
|
|
402
|
+
click.echo(f"\nš Generated code preview:")
|
|
403
|
+
click.echo("ā" * 60)
|
|
404
|
+
if code:
|
|
405
|
+
lines = code.split("\n")
|
|
406
|
+
for line in lines[:20]:
|
|
407
|
+
click.echo(line)
|
|
408
|
+
if len(lines) > 20:
|
|
409
|
+
click.echo(f"... ({len(lines) - 20} more lines)")
|
|
410
|
+
click.echo("ā" * 60)
|
|
411
|
+
click.echo(f"\nš” Next steps:")
|
|
412
|
+
click.echo(f" 1. Review the generated code: {output_file}")
|
|
413
|
+
click.echo(f" 2. Run the app: p2m run {output_file}")
|
|
414
|
+
click.echo(f" 3. Build for production: p2m build --target android")
|
|
415
|
+
else:
|
|
416
|
+
click.echo(f"ā Error: {message}", err=True)
|
|
417
|
+
if code:
|
|
418
|
+
click.echo(f"\nš Generated code (with errors):\n{code}", err=True)
|
|
419
|
+
sys.exit(1)
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
@cli.command()
|
|
423
|
+
@click.argument("path", default=".", required=False)
|
|
424
|
+
@click.option("--verbose", "-v", is_flag=True, help="Verbose output")
|
|
425
|
+
def test(path, verbose):
|
|
426
|
+
"""Run project tests with pytest"""
|
|
427
|
+
import pytest
|
|
428
|
+
project_dir = str(Path(path).resolve())
|
|
429
|
+
if project_dir not in sys.path:
|
|
430
|
+
sys.path.insert(0, project_dir)
|
|
431
|
+
pytest_args = [path, "--tb=short"]
|
|
432
|
+
if verbose:
|
|
433
|
+
pytest_args.append("-v")
|
|
434
|
+
click.echo(f"š§Ŗ Running tests in {path}...")
|
|
435
|
+
result = pytest.main(pytest_args)
|
|
436
|
+
sys.exit(result)
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
@cli.command()
|
|
440
|
+
def info():
|
|
441
|
+
"""Show project information"""
|
|
442
|
+
|
|
443
|
+
config = Config()
|
|
444
|
+
|
|
445
|
+
click.echo("š± Python2Mobile Project Info")
|
|
446
|
+
click.echo(f" Name: {config.project.name}")
|
|
447
|
+
click.echo(f" Version: {config.project.version}")
|
|
448
|
+
click.echo(f" Entry: {config.project.entry}")
|
|
449
|
+
click.echo(f" Build Target: {', '.join(config.build.target)}")
|
|
450
|
+
click.echo(f" Generator: {config.build.generator}")
|
|
451
|
+
click.echo(f" LLM Provider: {config.llm.provider}")
|
|
452
|
+
click.echo(f" LLM Model: {config.llm.model}")
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
def main():
|
|
456
|
+
"""Main CLI entry point"""
|
|
457
|
+
try:
|
|
458
|
+
cli()
|
|
459
|
+
except KeyboardInterrupt:
|
|
460
|
+
click.echo("\n\nāøļø Interrupted by user", err=True)
|
|
461
|
+
sys.exit(130)
|
|
462
|
+
except Exception as e:
|
|
463
|
+
click.echo(f"\nā Unexpected error: {e}", err=True)
|
|
464
|
+
import traceback
|
|
465
|
+
traceback.print_exc()
|
|
466
|
+
sys.exit(1)
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
if __name__ == "__main__":
|
|
470
|
+
main()
|
p2m/config.py
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Configuration management for P2M projects
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import toml
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any, Dict, Optional
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class ProjectConfig:
|
|
14
|
+
"""Project configuration"""
|
|
15
|
+
name: str
|
|
16
|
+
version: str
|
|
17
|
+
entry: str
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class BuildConfig:
|
|
22
|
+
"""Build configuration"""
|
|
23
|
+
target: list
|
|
24
|
+
generator: str
|
|
25
|
+
llm_provider: str
|
|
26
|
+
llm_model: str
|
|
27
|
+
output_dir: str
|
|
28
|
+
cache: bool
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class DevServerConfig:
|
|
33
|
+
"""DevServer configuration"""
|
|
34
|
+
port: int
|
|
35
|
+
hot_reload: bool
|
|
36
|
+
mobile_frame: bool
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass
|
|
40
|
+
class StyleConfig:
|
|
41
|
+
"""Style configuration"""
|
|
42
|
+
system: str
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass
|
|
46
|
+
class LLMConfig:
|
|
47
|
+
"""LLM provider configuration"""
|
|
48
|
+
provider: str
|
|
49
|
+
api_key: Optional[str] = None
|
|
50
|
+
model: Optional[str] = None
|
|
51
|
+
base_url: Optional[str] = None
|
|
52
|
+
x_api_key: Optional[str] = None
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class Config:
|
|
56
|
+
"""Main configuration handler"""
|
|
57
|
+
|
|
58
|
+
def __init__(self, config_path: Optional[str] = None):
|
|
59
|
+
self.config_path = Path(config_path or "p2m.toml")
|
|
60
|
+
self.config_data: Dict[str, Any] = {}
|
|
61
|
+
self.project: Optional[ProjectConfig] = None
|
|
62
|
+
self.build: Optional[BuildConfig] = None
|
|
63
|
+
self.devserver: Optional[DevServerConfig] = None
|
|
64
|
+
self.style: Optional[StyleConfig] = None
|
|
65
|
+
self.llm: Optional[LLMConfig] = None
|
|
66
|
+
|
|
67
|
+
if self.config_path.exists():
|
|
68
|
+
self.load()
|
|
69
|
+
else:
|
|
70
|
+
self._set_defaults()
|
|
71
|
+
|
|
72
|
+
def load(self) -> None:
|
|
73
|
+
"""Load configuration from TOML file"""
|
|
74
|
+
try:
|
|
75
|
+
self.config_data = toml.load(str(self.config_path))
|
|
76
|
+
self._parse_config()
|
|
77
|
+
except Exception as e:
|
|
78
|
+
raise RuntimeError(f"Failed to load config from {self.config_path}: {e}")
|
|
79
|
+
|
|
80
|
+
def _parse_config(self) -> None:
|
|
81
|
+
"""Parse configuration data into dataclass objects"""
|
|
82
|
+
# Project config
|
|
83
|
+
project_data = self.config_data.get("project", {})
|
|
84
|
+
self.project = ProjectConfig(
|
|
85
|
+
name=project_data.get("name", "MyApp"),
|
|
86
|
+
version=project_data.get("version", "0.1.0"),
|
|
87
|
+
entry=project_data.get("entry", "main.py"),
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
# Build config
|
|
91
|
+
build_data = self.config_data.get("build", {})
|
|
92
|
+
self.build = BuildConfig(
|
|
93
|
+
target=build_data.get("target", ["android", "ios"]),
|
|
94
|
+
generator=build_data.get("generator", "flutter"),
|
|
95
|
+
llm_provider=build_data.get("llm_provider", "openai"),
|
|
96
|
+
llm_model=build_data.get("llm_model", "gpt-4o"),
|
|
97
|
+
output_dir=build_data.get("output_dir", "./build"),
|
|
98
|
+
cache=build_data.get("cache", True),
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
# DevServer config
|
|
102
|
+
devserver_data = self.config_data.get("devserver", {})
|
|
103
|
+
self.devserver = DevServerConfig(
|
|
104
|
+
port=devserver_data.get("port", 3000),
|
|
105
|
+
hot_reload=devserver_data.get("hot_reload", True),
|
|
106
|
+
mobile_frame=devserver_data.get("mobile_frame", True),
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
# Style config
|
|
110
|
+
style_data = self.config_data.get("style", {})
|
|
111
|
+
self.style = StyleConfig(
|
|
112
|
+
system=style_data.get("system", "tailwind"),
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
# LLM config
|
|
116
|
+
llm_data = self.config_data.get("llm", {})
|
|
117
|
+
provider_data = llm_data.get(self.build.llm_provider, {})
|
|
118
|
+
|
|
119
|
+
# Resolve API key: p2m.toml [llm.<provider>] ā P2M_<PROVIDER>_API_KEY ā standard key env vars
|
|
120
|
+
_provider_upper = self.build.llm_provider.upper()
|
|
121
|
+
_standard_keys = {
|
|
122
|
+
"ANTHROPIC": os.getenv("ANTHROPIC_API_KEY"),
|
|
123
|
+
"OPENAI": os.getenv("OPENAI_API_KEY"),
|
|
124
|
+
}
|
|
125
|
+
_resolved_key = (
|
|
126
|
+
provider_data.get("api_key")
|
|
127
|
+
or os.getenv(f"P2M_{_provider_upper}_API_KEY")
|
|
128
|
+
or _standard_keys.get(_provider_upper)
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
self.llm = LLMConfig(
|
|
132
|
+
provider=self.build.llm_provider,
|
|
133
|
+
api_key=_resolved_key,
|
|
134
|
+
model=provider_data.get("model") or self.build.llm_model,
|
|
135
|
+
base_url=provider_data.get("base_url"),
|
|
136
|
+
x_api_key=provider_data.get("x_api_key"),
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
def _set_defaults(self) -> None:
|
|
140
|
+
"""Set default configuration"""
|
|
141
|
+
self.project = ProjectConfig(
|
|
142
|
+
name="MyApp",
|
|
143
|
+
version="0.1.0",
|
|
144
|
+
entry="main.py",
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
self.build = BuildConfig(
|
|
148
|
+
target=["android", "ios"],
|
|
149
|
+
generator="flutter",
|
|
150
|
+
llm_provider="openai",
|
|
151
|
+
llm_model="gpt-4o",
|
|
152
|
+
output_dir="./build",
|
|
153
|
+
cache=True,
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
self.devserver = DevServerConfig(
|
|
157
|
+
port=3000,
|
|
158
|
+
hot_reload=True,
|
|
159
|
+
mobile_frame=True,
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
self.style = StyleConfig(
|
|
163
|
+
system="tailwind",
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
self.llm = LLMConfig(
|
|
167
|
+
provider="openai",
|
|
168
|
+
api_key=os.getenv("OPENAI_API_KEY"),
|
|
169
|
+
model="gpt-4o",
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
def save(self, path: Optional[str] = None) -> None:
|
|
173
|
+
"""Save configuration to TOML file"""
|
|
174
|
+
output_path = Path(path or self.config_path)
|
|
175
|
+
|
|
176
|
+
config_dict = {
|
|
177
|
+
"project": {
|
|
178
|
+
"name": self.project.name,
|
|
179
|
+
"version": self.project.version,
|
|
180
|
+
"entry": self.project.entry,
|
|
181
|
+
},
|
|
182
|
+
"build": {
|
|
183
|
+
"target": self.build.target,
|
|
184
|
+
"generator": self.build.generator,
|
|
185
|
+
"llm_provider": self.build.llm_provider,
|
|
186
|
+
"llm_model": self.build.llm_model,
|
|
187
|
+
"output_dir": self.build.output_dir,
|
|
188
|
+
"cache": self.build.cache,
|
|
189
|
+
},
|
|
190
|
+
"devserver": {
|
|
191
|
+
"port": self.devserver.port,
|
|
192
|
+
"hot_reload": self.devserver.hot_reload,
|
|
193
|
+
"mobile_frame": self.devserver.mobile_frame,
|
|
194
|
+
},
|
|
195
|
+
"style": {
|
|
196
|
+
"system": self.style.system,
|
|
197
|
+
},
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
with open(output_path, "w") as f:
|
|
201
|
+
toml.dump(config_dict, f)
|
|
202
|
+
|
|
203
|
+
def get_llm_config(self) -> LLMConfig:
|
|
204
|
+
"""Get LLM configuration"""
|
|
205
|
+
return self.llm
|
p2m/core/__init__.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""
|
|
2
|
+
P2M Core Engine - Runtime, rendering, event dispatch, and state management
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from p2m.core.runtime import Render, Runtime
|
|
6
|
+
from p2m.core.render_engine import RenderEngine
|
|
7
|
+
from p2m.core.ast_walker import ASTWalker
|
|
8
|
+
from p2m.core import events
|
|
9
|
+
from p2m.core.state import AppState
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"Render",
|
|
13
|
+
"Runtime",
|
|
14
|
+
"RenderEngine",
|
|
15
|
+
"ASTWalker",
|
|
16
|
+
"events",
|
|
17
|
+
"AppState",
|
|
18
|
+
]
|