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/api.py ADDED
@@ -0,0 +1,191 @@
1
+ """
2
+ P2M API Client - HTTP request handling for P2M applications
3
+ Supports GET, POST, PUT, DELETE requests with proper error handling
4
+ """
5
+
6
+ import json
7
+ from typing import Dict, Any, Optional, Callable
8
+ from enum import Enum
9
+
10
+
11
+ class HTTPMethod(Enum):
12
+ """HTTP methods"""
13
+ GET = "GET"
14
+ POST = "POST"
15
+ PUT = "PUT"
16
+ DELETE = "DELETE"
17
+ PATCH = "PATCH"
18
+
19
+
20
+ class APIResponse:
21
+ """Represents an API response"""
22
+
23
+ def __init__(self, status_code: int, data: Any, error: Optional[str] = None):
24
+ self.status_code = status_code
25
+ self.data = data
26
+ self.error = error
27
+ self.success = 200 <= status_code < 300
28
+
29
+ def json(self) -> Dict[str, Any]:
30
+ """Get response as JSON"""
31
+ if isinstance(self.data, dict):
32
+ return self.data
33
+ try:
34
+ return json.loads(self.data) if isinstance(self.data, str) else self.data
35
+ except:
36
+ return {"error": "Invalid JSON"}
37
+
38
+ def __repr__(self) -> str:
39
+ return f"APIResponse(status={self.status_code}, success={self.success})"
40
+
41
+
42
+ class APIClient:
43
+ """HTTP client for making API requests"""
44
+
45
+ def __init__(self, base_url: str = "", default_headers: Optional[Dict[str, str]] = None):
46
+ """
47
+ Initialize API client
48
+
49
+ Args:
50
+ base_url: Base URL for all requests
51
+ default_headers: Default headers to include in all requests
52
+ """
53
+ self.base_url = base_url.rstrip('/')
54
+ self.default_headers = default_headers or {}
55
+ self.timeout = 30
56
+
57
+ def _build_url(self, endpoint: str) -> str:
58
+ """Build full URL from endpoint"""
59
+ if endpoint.startswith('http'):
60
+ return endpoint
61
+ return f"{self.base_url}/{endpoint.lstrip('/')}" if self.base_url else endpoint
62
+
63
+ def _merge_headers(self, headers: Optional[Dict[str, str]]) -> Dict[str, str]:
64
+ """Merge default headers with request-specific headers"""
65
+ merged = self.default_headers.copy()
66
+ if headers:
67
+ merged.update(headers)
68
+ return merged
69
+
70
+ async def get(
71
+ self,
72
+ endpoint: str,
73
+ params: Optional[Dict[str, Any]] = None,
74
+ headers: Optional[Dict[str, str]] = None
75
+ ) -> APIResponse:
76
+ """
77
+ Make a GET request
78
+
79
+ Args:
80
+ endpoint: API endpoint
81
+ params: Query parameters
82
+ headers: Request headers
83
+
84
+ Returns:
85
+ APIResponse object
86
+ """
87
+ url = self._build_url(endpoint)
88
+ headers = self._merge_headers(headers)
89
+
90
+ try:
91
+ # This will be implemented differently on each platform
92
+ # For now, return a placeholder
93
+ return APIResponse(200, {"message": "GET request to " + url})
94
+ except Exception as e:
95
+ return APIResponse(500, None, str(e))
96
+
97
+ async def post(
98
+ self,
99
+ endpoint: str,
100
+ data: Optional[Dict[str, Any]] = None,
101
+ headers: Optional[Dict[str, str]] = None
102
+ ) -> APIResponse:
103
+ """
104
+ Make a POST request
105
+
106
+ Args:
107
+ endpoint: API endpoint
108
+ data: Request body
109
+ headers: Request headers
110
+
111
+ Returns:
112
+ APIResponse object
113
+ """
114
+ url = self._build_url(endpoint)
115
+ headers = self._merge_headers(headers)
116
+ headers['Content-Type'] = 'application/json'
117
+
118
+ try:
119
+ # This will be implemented differently on each platform
120
+ # For now, return a placeholder
121
+ return APIResponse(201, {"message": "POST request to " + url})
122
+ except Exception as e:
123
+ return APIResponse(500, None, str(e))
124
+
125
+ async def put(
126
+ self,
127
+ endpoint: str,
128
+ data: Optional[Dict[str, Any]] = None,
129
+ headers: Optional[Dict[str, str]] = None
130
+ ) -> APIResponse:
131
+ """
132
+ Make a PUT request
133
+
134
+ Args:
135
+ endpoint: API endpoint
136
+ data: Request body
137
+ headers: Request headers
138
+
139
+ Returns:
140
+ APIResponse object
141
+ """
142
+ url = self._build_url(endpoint)
143
+ headers = self._merge_headers(headers)
144
+ headers['Content-Type'] = 'application/json'
145
+
146
+ try:
147
+ return APIResponse(200, {"message": "PUT request to " + url})
148
+ except Exception as e:
149
+ return APIResponse(500, None, str(e))
150
+
151
+ async def delete(
152
+ self,
153
+ endpoint: str,
154
+ headers: Optional[Dict[str, str]] = None
155
+ ) -> APIResponse:
156
+ """
157
+ Make a DELETE request
158
+
159
+ Args:
160
+ endpoint: API endpoint
161
+ headers: Request headers
162
+
163
+ Returns:
164
+ APIResponse object
165
+ """
166
+ url = self._build_url(endpoint)
167
+ headers = self._merge_headers(headers)
168
+
169
+ try:
170
+ return APIResponse(204, None)
171
+ except Exception as e:
172
+ return APIResponse(500, None, str(e))
173
+
174
+
175
+ # Global API client instance
176
+ _api_client: Optional[APIClient] = None
177
+
178
+
179
+ def init_api(base_url: str = "", default_headers: Optional[Dict[str, str]] = None) -> APIClient:
180
+ """Initialize global API client"""
181
+ global _api_client
182
+ _api_client = APIClient(base_url, default_headers)
183
+ return _api_client
184
+
185
+
186
+ def get_api() -> APIClient:
187
+ """Get global API client"""
188
+ global _api_client
189
+ if _api_client is None:
190
+ _api_client = APIClient()
191
+ return _api_client
p2m/core/ast_walker.py ADDED
@@ -0,0 +1,171 @@
1
+ """
2
+ P2M AST Walker - Analyzes component tree structure
3
+ """
4
+
5
+ import ast
6
+ from typing import Any, Dict, List, Optional
7
+
8
+
9
+ class ASTWalker:
10
+ """Walks and analyzes component tree"""
11
+
12
+ def __init__(self):
13
+ self.components: List[Dict[str, Any]] = []
14
+ self.handlers: Dict[str, Any] = {}
15
+
16
+ def walk(self, component_tree: Dict[str, Any]) -> Dict[str, Any]:
17
+ """Walk component tree and extract structure"""
18
+ self.components = []
19
+ self.handlers = {}
20
+
21
+ self._walk_component(component_tree)
22
+
23
+ return {
24
+ "components": self.components,
25
+ "handlers": self.handlers,
26
+ }
27
+
28
+ def _walk_component(self, component: Dict[str, Any], parent_id: str = "root") -> None:
29
+ """Recursively walk component tree"""
30
+ component_type = component.get("type", "unknown")
31
+ props = component.get("props", {})
32
+ children = component.get("children", [])
33
+
34
+ # Extract component info
35
+ component_info = {
36
+ "type": component_type,
37
+ "props": props,
38
+ "children_count": len(children),
39
+ }
40
+
41
+ self.components.append(component_info)
42
+
43
+ # Extract handlers
44
+ for key, value in props.items():
45
+ if key.startswith("on_") and value:
46
+ handler_name = value
47
+ self.handlers[handler_name] = {
48
+ "component": component_type,
49
+ "event": key,
50
+ }
51
+
52
+ # Walk children
53
+ for child in children:
54
+ if isinstance(child, dict):
55
+ self._walk_component(child, parent_id)
56
+
57
+ def extract_handlers(self, component_tree: Dict[str, Any]) -> Dict[str, Any]:
58
+ """Extract all event handlers from component tree"""
59
+ handlers = {}
60
+ self._extract_handlers_recursive(component_tree, handlers)
61
+ return handlers
62
+
63
+ def _extract_handlers_recursive(self, component: Dict[str, Any],
64
+ handlers: Dict[str, Any]) -> None:
65
+ """Recursively extract handlers"""
66
+ props = component.get("props", {})
67
+
68
+ for key, value in props.items():
69
+ if key.startswith("on_") and value:
70
+ handlers[value] = {
71
+ "component": component.get("type"),
72
+ "event": key,
73
+ }
74
+
75
+ children = component.get("children", [])
76
+ for child in children:
77
+ if isinstance(child, dict):
78
+ self._extract_handlers_recursive(child, handlers)
79
+
80
+ def analyze_structure(self, component_tree: Dict[str, Any]) -> Dict[str, Any]:
81
+ """Analyze component tree structure"""
82
+ analysis = {
83
+ "total_components": 0,
84
+ "component_types": {},
85
+ "depth": 0,
86
+ "handlers": {},
87
+ }
88
+
89
+ self._analyze_recursive(component_tree, analysis, 0)
90
+ return analysis
91
+
92
+ def _analyze_recursive(self, component: Dict[str, Any],
93
+ analysis: Dict[str, Any], depth: int) -> None:
94
+ """Recursively analyze structure"""
95
+ component_type = component.get("type", "unknown")
96
+
97
+ # Update counts
98
+ analysis["total_components"] += 1
99
+ analysis["component_types"][component_type] = analysis["component_types"].get(component_type, 0) + 1
100
+ analysis["depth"] = max(analysis["depth"], depth)
101
+
102
+ # Extract handlers
103
+ props = component.get("props", {})
104
+ for key, value in props.items():
105
+ if key.startswith("on_") and value:
106
+ analysis["handlers"][value] = {
107
+ "component": component_type,
108
+ "event": key,
109
+ }
110
+
111
+ # Analyze children
112
+ children = component.get("children", [])
113
+ for child in children:
114
+ if isinstance(child, dict):
115
+ self._analyze_recursive(child, analysis, depth + 1)
116
+
117
+
118
+ class CodeExtractor:
119
+ """Extracts Python code information for LLM"""
120
+
121
+ def __init__(self, file_path: str):
122
+ self.file_path = file_path
123
+ self.tree: Optional[ast.AST] = None
124
+ self.functions: Dict[str, str] = {}
125
+ self.imports: List[str] = []
126
+ self.classes: Dict[str, str] = {}
127
+
128
+ def extract(self) -> Dict[str, Any]:
129
+ """Extract code information"""
130
+ with open(self.file_path, "r") as f:
131
+ code = f.read()
132
+
133
+ self.tree = ast.parse(code)
134
+ self._extract_imports()
135
+ self._extract_functions()
136
+ self._extract_classes()
137
+
138
+ return {
139
+ "file": self.file_path,
140
+ "imports": self.imports,
141
+ "functions": self.functions,
142
+ "classes": self.classes,
143
+ "raw_code": code,
144
+ }
145
+
146
+ def _extract_imports(self) -> None:
147
+ """Extract import statements"""
148
+ for node in ast.walk(self.tree):
149
+ if isinstance(node, ast.Import):
150
+ for alias in node.names:
151
+ self.imports.append(alias.name)
152
+ elif isinstance(node, ast.ImportFrom):
153
+ module = node.module or ""
154
+ for alias in node.names:
155
+ self.imports.append(f"from {module} import {alias.name}")
156
+
157
+ def _extract_functions(self) -> None:
158
+ """Extract function definitions"""
159
+ for node in ast.walk(self.tree):
160
+ if isinstance(node, ast.FunctionDef):
161
+ self.functions[node.name] = ast.get_source_segment(
162
+ open(self.file_path).read(), node
163
+ ) or ""
164
+
165
+ def _extract_classes(self) -> None:
166
+ """Extract class definitions"""
167
+ for node in ast.walk(self.tree):
168
+ if isinstance(node, ast.ClassDef):
169
+ self.classes[node.name] = ast.get_source_segment(
170
+ open(self.file_path).read(), node
171
+ ) or ""
p2m/core/database.py ADDED
@@ -0,0 +1,192 @@
1
+ """
2
+ P2M Local Database - Persistent storage for P2M applications
3
+ Supports key-value storage with JSON serialization
4
+ """
5
+
6
+ import json
7
+ from typing import Dict, Any, Optional, List, Union
8
+ from abc import ABC, abstractmethod
9
+
10
+
11
+ class Database(ABC):
12
+ """Abstract base class for database implementations"""
13
+
14
+ @abstractmethod
15
+ async def set(self, key: str, value: Any) -> bool:
16
+ """Set a value in the database"""
17
+ pass
18
+
19
+ @abstractmethod
20
+ async def get(self, key: str, default: Any = None) -> Any:
21
+ """Get a value from the database"""
22
+ pass
23
+
24
+ @abstractmethod
25
+ async def delete(self, key: str) -> bool:
26
+ """Delete a value from the database"""
27
+ pass
28
+
29
+ @abstractmethod
30
+ async def clear(self) -> bool:
31
+ """Clear all data from the database"""
32
+ pass
33
+
34
+ @abstractmethod
35
+ async def keys(self) -> List[str]:
36
+ """Get all keys in the database"""
37
+ pass
38
+
39
+ @abstractmethod
40
+ async def has(self, key: str) -> bool:
41
+ """Check if a key exists in the database"""
42
+ pass
43
+
44
+
45
+ class LocalDatabase(Database):
46
+ """In-memory database implementation (for testing)"""
47
+
48
+ def __init__(self):
49
+ self._data: Dict[str, Any] = {}
50
+
51
+ async def set(self, key: str, value: Any) -> bool:
52
+ """Set a value in the database"""
53
+ try:
54
+ # Serialize to JSON to ensure compatibility
55
+ if isinstance(value, (dict, list)):
56
+ self._data[key] = json.dumps(value)
57
+ else:
58
+ self._data[key] = str(value)
59
+ return True
60
+ except Exception as e:
61
+ print(f"Error setting key {key}: {e}")
62
+ return False
63
+
64
+ async def get(self, key: str, default: Any = None) -> Any:
65
+ """Get a value from the database"""
66
+ try:
67
+ value = self._data.get(key, default)
68
+ if value is None:
69
+ return default
70
+
71
+ # Try to deserialize JSON
72
+ try:
73
+ return json.loads(value)
74
+ except:
75
+ return value
76
+ except Exception as e:
77
+ print(f"Error getting key {key}: {e}")
78
+ return default
79
+
80
+ async def delete(self, key: str) -> bool:
81
+ """Delete a value from the database"""
82
+ try:
83
+ if key in self._data:
84
+ del self._data[key]
85
+ return True
86
+ return False
87
+ except Exception as e:
88
+ print(f"Error deleting key {key}: {e}")
89
+ return False
90
+
91
+ async def clear(self) -> bool:
92
+ """Clear all data from the database"""
93
+ try:
94
+ self._data.clear()
95
+ return True
96
+ except Exception as e:
97
+ print(f"Error clearing database: {e}")
98
+ return False
99
+
100
+ async def keys(self) -> List[str]:
101
+ """Get all keys in the database"""
102
+ return list(self._data.keys())
103
+
104
+ async def has(self, key: str) -> bool:
105
+ """Check if a key exists in the database"""
106
+ return key in self._data
107
+
108
+
109
+ class Table:
110
+ """Represents a table in the database"""
111
+
112
+ def __init__(self, name: str, db: Database):
113
+ self.name = name
114
+ self.db = db
115
+
116
+ def _make_key(self, item_id: Union[str, int]) -> str:
117
+ """Create a table-scoped key"""
118
+ return f"{self.name}:{item_id}"
119
+
120
+ async def insert(self, item_id: Union[str, int], data: Dict[str, Any]) -> bool:
121
+ """Insert an item into the table"""
122
+ key = self._make_key(item_id)
123
+ return await self.db.set(key, data)
124
+
125
+ async def get(self, item_id: Union[str, int], default: Any = None) -> Any:
126
+ """Get an item from the table"""
127
+ key = self._make_key(item_id)
128
+ return await self.db.get(key, default)
129
+
130
+ async def update(self, item_id: Union[str, int], data: Dict[str, Any]) -> bool:
131
+ """Update an item in the table"""
132
+ key = self._make_key(item_id)
133
+ existing = await self.db.get(key, {})
134
+
135
+ if isinstance(existing, dict):
136
+ existing.update(data)
137
+ return await self.db.set(key, existing)
138
+
139
+ return await self.db.set(key, data)
140
+
141
+ async def delete(self, item_id: Union[str, int]) -> bool:
142
+ """Delete an item from the table"""
143
+ key = self._make_key(item_id)
144
+ return await self.db.delete(key)
145
+
146
+ async def all(self) -> List[Dict[str, Any]]:
147
+ """Get all items from the table"""
148
+ items = []
149
+ all_keys = await self.db.keys()
150
+
151
+ for key in all_keys:
152
+ if key.startswith(f"{self.name}:"):
153
+ item = await self.db.get(key)
154
+ if isinstance(item, dict):
155
+ items.append(item)
156
+
157
+ return items
158
+
159
+ async def clear(self) -> bool:
160
+ """Clear all items from the table"""
161
+ all_keys = await self.db.keys()
162
+
163
+ for key in all_keys:
164
+ if key.startswith(f"{self.name}:"):
165
+ await self.db.delete(key)
166
+
167
+ return True
168
+
169
+
170
+ # Global database instance
171
+ _database: Optional[Database] = None
172
+
173
+
174
+ def init_database(db: Optional[Database] = None) -> Database:
175
+ """Initialize global database"""
176
+ global _database
177
+ _database = db or LocalDatabase()
178
+ return _database
179
+
180
+
181
+ def get_database() -> Database:
182
+ """Get global database"""
183
+ global _database
184
+ if _database is None:
185
+ _database = LocalDatabase()
186
+ return _database
187
+
188
+
189
+ def get_table(name: str) -> Table:
190
+ """Get a table from the global database"""
191
+ db = get_database()
192
+ return Table(name, db)
p2m/core/events.py ADDED
@@ -0,0 +1,56 @@
1
+ """
2
+ P2M Event System - Global handler registry
3
+ Handlers are registered by name and dispatched on WebSocket events.
4
+ """
5
+ from typing import Any, Callable, Dict
6
+
7
+
8
+ _handlers: Dict[str, Callable] = {}
9
+
10
+
11
+ def register(name: str, func: Callable) -> None:
12
+ """Register a named event handler."""
13
+ _handlers[name] = func
14
+
15
+
16
+ def unregister(name: str) -> None:
17
+ _handlers.pop(name, None)
18
+
19
+
20
+ def clear() -> None:
21
+ _handlers.clear()
22
+
23
+
24
+ def dispatch(name: str, *args: Any) -> bool:
25
+ """Call handler by name. Returns True if handler found."""
26
+ handler = _handlers.get(name)
27
+ if not handler:
28
+ return False
29
+ try:
30
+ if args:
31
+ handler(*args)
32
+ else:
33
+ handler()
34
+ return True
35
+ except TypeError:
36
+ # Handler signature mismatch — try without args
37
+ try:
38
+ handler()
39
+ return True
40
+ except Exception as e:
41
+ print(f"[P2M] Handler '{name}' error: {e}")
42
+ except Exception as e:
43
+ print(f"[P2M] Handler '{name}' error: {e}")
44
+ return False
45
+
46
+
47
+ def on(name: str) -> Callable:
48
+ """Decorator: @events.on('my_action')"""
49
+ def decorator(func: Callable) -> Callable:
50
+ _handlers[name] = func
51
+ return func
52
+ return decorator
53
+
54
+
55
+ def has(name: str) -> bool:
56
+ return name in _handlers