python2mobile 1.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- examples/example_ecommerce_app.py +189 -0
- examples/example_todo_app.py +159 -0
- p2m/__init__.py +31 -0
- p2m/cli.py +470 -0
- p2m/config.py +205 -0
- p2m/core/__init__.py +18 -0
- p2m/core/api.py +191 -0
- p2m/core/ast_walker.py +171 -0
- p2m/core/database.py +192 -0
- p2m/core/events.py +56 -0
- p2m/core/render_engine.py +597 -0
- p2m/core/runtime.py +128 -0
- p2m/core/state.py +51 -0
- p2m/core/validator.py +284 -0
- p2m/devserver/__init__.py +9 -0
- p2m/devserver/server.py +84 -0
- p2m/i18n/__init__.py +7 -0
- p2m/i18n/translator.py +74 -0
- p2m/imagine/__init__.py +35 -0
- p2m/imagine/agent.py +463 -0
- p2m/imagine/legacy.py +217 -0
- p2m/llm/__init__.py +20 -0
- p2m/llm/anthropic_provider.py +78 -0
- p2m/llm/base.py +42 -0
- p2m/llm/compatible_provider.py +120 -0
- p2m/llm/factory.py +72 -0
- p2m/llm/ollama_provider.py +89 -0
- p2m/llm/openai_provider.py +79 -0
- p2m/testing/__init__.py +41 -0
- p2m/ui/__init__.py +43 -0
- p2m/ui/components.py +301 -0
- python2mobile-1.0.1.dist-info/METADATA +238 -0
- python2mobile-1.0.1.dist-info/RECORD +50 -0
- python2mobile-1.0.1.dist-info/WHEEL +5 -0
- python2mobile-1.0.1.dist-info/entry_points.txt +2 -0
- python2mobile-1.0.1.dist-info/top_level.txt +3 -0
- tests/test_basic_engine.py +281 -0
- tests/test_build_generation.py +603 -0
- tests/test_build_test_gate.py +150 -0
- tests/test_carousel_modal.py +84 -0
- tests/test_config_system.py +272 -0
- tests/test_i18n.py +101 -0
- tests/test_ifood_app_integration.py +172 -0
- tests/test_imagine_cli.py +133 -0
- tests/test_imagine_command.py +341 -0
- tests/test_llm_providers.py +321 -0
- tests/test_new_apps_integration.py +588 -0
- tests/test_ollama_functional.py +329 -0
- tests/test_real_world_apps.py +228 -0
- tests/test_run_integration.py +776 -0
p2m/core/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
|