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/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
+ ]