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
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Example: E-commerce App - Real-world P2M application
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from p2m.core import Render
|
|
6
|
+
from p2m.ui import Container, Text, Button, Input, Image, Row, Column, Card, Badge
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
# Mock product data
|
|
10
|
+
products = [
|
|
11
|
+
{
|
|
12
|
+
"id": 1,
|
|
13
|
+
"name": "Wireless Headphones",
|
|
14
|
+
"price": 79.99,
|
|
15
|
+
"rating": 4.5,
|
|
16
|
+
"image": "https://via.placeholder.com/200x200?text=Headphones",
|
|
17
|
+
"in_stock": True,
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"id": 2,
|
|
21
|
+
"name": "Smart Watch",
|
|
22
|
+
"price": 199.99,
|
|
23
|
+
"rating": 4.8,
|
|
24
|
+
"image": "https://via.placeholder.com/200x200?text=SmartWatch",
|
|
25
|
+
"in_stock": True,
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"id": 3,
|
|
29
|
+
"name": "USB-C Cable",
|
|
30
|
+
"price": 12.99,
|
|
31
|
+
"rating": 4.2,
|
|
32
|
+
"image": "https://via.placeholder.com/200x200?text=Cable",
|
|
33
|
+
"in_stock": False,
|
|
34
|
+
},
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
# Shopping cart
|
|
38
|
+
cart = []
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def add_to_cart(product_id: int):
|
|
42
|
+
"""Add product to cart"""
|
|
43
|
+
product = next((p for p in products if p["id"] == product_id), None)
|
|
44
|
+
if product:
|
|
45
|
+
cart.append(product)
|
|
46
|
+
print(f"Added {product['name']} to cart")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def remove_from_cart(product_id: int):
|
|
50
|
+
"""Remove product from cart"""
|
|
51
|
+
global cart
|
|
52
|
+
cart = [p for p in cart if p["id"] != product_id]
|
|
53
|
+
print(f"Removed product {product_id} from cart")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def render_product_card(product: dict):
|
|
57
|
+
"""Render a product card"""
|
|
58
|
+
card = Card(class_="bg-white rounded-lg shadow-md overflow-hidden hover:shadow-lg transition-shadow")
|
|
59
|
+
|
|
60
|
+
# Product image
|
|
61
|
+
image = Image(
|
|
62
|
+
src=product["image"],
|
|
63
|
+
alt=product["name"],
|
|
64
|
+
class_="w-full h-48 object-cover"
|
|
65
|
+
)
|
|
66
|
+
card.add(image)
|
|
67
|
+
|
|
68
|
+
# Product info
|
|
69
|
+
info = Container(class_="p-4")
|
|
70
|
+
|
|
71
|
+
# Name
|
|
72
|
+
name = Text(product["name"], class_="text-lg font-semibold text-gray-800 mb-2")
|
|
73
|
+
info.add(name)
|
|
74
|
+
|
|
75
|
+
# Rating and price row
|
|
76
|
+
rating_price = Row(class_="flex justify-between items-center mb-3")
|
|
77
|
+
|
|
78
|
+
rating = Badge(
|
|
79
|
+
label=f"⭐ {product['rating']}",
|
|
80
|
+
class_="bg-yellow-100 text-yellow-800 px-2 py-1 rounded text-sm"
|
|
81
|
+
)
|
|
82
|
+
rating_price.add(rating)
|
|
83
|
+
|
|
84
|
+
price = Text(f"${product['price']}", class_="text-xl font-bold text-blue-600")
|
|
85
|
+
rating_price.add(price)
|
|
86
|
+
|
|
87
|
+
info.add(rating_price)
|
|
88
|
+
|
|
89
|
+
# Stock status
|
|
90
|
+
stock_class = "bg-green-100 text-green-800" if product["in_stock"] else "bg-red-100 text-red-800"
|
|
91
|
+
stock_text = "In Stock" if product["in_stock"] else "Out of Stock"
|
|
92
|
+
stock = Badge(
|
|
93
|
+
label=stock_text,
|
|
94
|
+
class_=f"{stock_class} px-2 py-1 rounded text-xs font-semibold"
|
|
95
|
+
)
|
|
96
|
+
info.add(stock)
|
|
97
|
+
|
|
98
|
+
# Add to cart button
|
|
99
|
+
btn_class = "bg-blue-600 hover:bg-blue-700 text-white" if product["in_stock"] else "bg-gray-400 text-gray-600 cursor-not-allowed"
|
|
100
|
+
add_btn = Button(
|
|
101
|
+
"Add to Cart",
|
|
102
|
+
class_=f"{btn_class} w-full mt-4 px-4 py-2 rounded font-semibold",
|
|
103
|
+
on_click=lambda: add_to_cart(product["id"]) if product["in_stock"] else None
|
|
104
|
+
)
|
|
105
|
+
info.add(add_btn)
|
|
106
|
+
|
|
107
|
+
card.add(info)
|
|
108
|
+
return card
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def create_view():
|
|
112
|
+
"""Create the e-commerce app view"""
|
|
113
|
+
|
|
114
|
+
# Main container
|
|
115
|
+
main = Container(class_="bg-gray-50 min-h-screen")
|
|
116
|
+
|
|
117
|
+
# Header
|
|
118
|
+
header = Container(class_="bg-blue-600 text-white p-4 shadow-md")
|
|
119
|
+
|
|
120
|
+
title = Text("TechStore", class_="text-2xl font-bold mb-2")
|
|
121
|
+
header.add(title)
|
|
122
|
+
|
|
123
|
+
subtitle = Text("Your favorite tech products", class_="text-blue-100")
|
|
124
|
+
header.add(subtitle)
|
|
125
|
+
|
|
126
|
+
main.add(header)
|
|
127
|
+
|
|
128
|
+
# Search bar
|
|
129
|
+
search_section = Container(class_="bg-white p-4 border-b border-gray-200")
|
|
130
|
+
search_input = Input(
|
|
131
|
+
placeholder="Search products...",
|
|
132
|
+
class_="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
133
|
+
)
|
|
134
|
+
search_section.add(search_input)
|
|
135
|
+
main.add(search_section)
|
|
136
|
+
|
|
137
|
+
# Products section
|
|
138
|
+
products_section = Container(class_="p-4")
|
|
139
|
+
|
|
140
|
+
section_title = Text("Featured Products", class_="text-xl font-bold text-gray-800 mb-4")
|
|
141
|
+
products_section.add(section_title)
|
|
142
|
+
|
|
143
|
+
# Products grid
|
|
144
|
+
products_grid = Container(class_="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4")
|
|
145
|
+
|
|
146
|
+
for product in products:
|
|
147
|
+
product_card = render_product_card(product)
|
|
148
|
+
products_grid.add(product_card)
|
|
149
|
+
|
|
150
|
+
products_section.add(products_grid)
|
|
151
|
+
main.add(products_section)
|
|
152
|
+
|
|
153
|
+
# Cart summary
|
|
154
|
+
cart_section = Container(class_="bg-white p-4 border-t border-gray-200 sticky bottom-0")
|
|
155
|
+
|
|
156
|
+
cart_row = Row(class_="flex justify-between items-center")
|
|
157
|
+
|
|
158
|
+
cart_info = Column()
|
|
159
|
+
cart_label = Text("Shopping Cart", class_="font-semibold text-gray-800")
|
|
160
|
+
cart_info.add(cart_label)
|
|
161
|
+
|
|
162
|
+
cart_count = Text(
|
|
163
|
+
f"{len(cart)} items • ${sum(p['price'] for p in cart):.2f}",
|
|
164
|
+
class_="text-sm text-gray-600"
|
|
165
|
+
)
|
|
166
|
+
cart_info.add(cart_count)
|
|
167
|
+
|
|
168
|
+
cart_row.add(cart_info)
|
|
169
|
+
|
|
170
|
+
checkout_btn = Button(
|
|
171
|
+
"Checkout",
|
|
172
|
+
class_="bg-green-600 hover:bg-green-700 text-white px-6 py-2 rounded font-semibold",
|
|
173
|
+
on_click=lambda: print("Checkout clicked")
|
|
174
|
+
)
|
|
175
|
+
cart_row.add(checkout_btn)
|
|
176
|
+
|
|
177
|
+
cart_section.add(cart_row)
|
|
178
|
+
main.add(cart_section)
|
|
179
|
+
|
|
180
|
+
return main.build()
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def main():
|
|
184
|
+
"""Main entry point"""
|
|
185
|
+
Render.execute(create_view)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
if __name__ == "__main__":
|
|
189
|
+
main()
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Example: Todo App - Real-world P2M application
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from p2m.core import Render
|
|
6
|
+
from p2m.ui import Container, Text, Button, Input, List, Row, Column, Card, Badge
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
# App state (in real app, would be managed by a state manager)
|
|
10
|
+
todos = [
|
|
11
|
+
{"id": 1, "title": "Learn Python2Mobile", "completed": False},
|
|
12
|
+
{"id": 2, "title": "Build a mobile app", "completed": False},
|
|
13
|
+
{"id": 3, "title": "Deploy to production", "completed": False},
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def add_todo(title: str):
|
|
18
|
+
"""Add a new todo"""
|
|
19
|
+
new_id = max([t["id"] for t in todos]) + 1 if todos else 1
|
|
20
|
+
todos.append({"id": new_id, "title": title, "completed": False})
|
|
21
|
+
print(f"Added todo: {title}")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def toggle_todo(todo_id: int):
|
|
25
|
+
"""Toggle todo completion status"""
|
|
26
|
+
for todo in todos:
|
|
27
|
+
if todo["id"] == todo_id:
|
|
28
|
+
todo["completed"] = not todo["completed"]
|
|
29
|
+
print(f"Toggled todo {todo_id}")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def delete_todo(todo_id: int):
|
|
33
|
+
"""Delete a todo"""
|
|
34
|
+
global todos
|
|
35
|
+
todos = [t for t in todos if t["id"] != todo_id]
|
|
36
|
+
print(f"Deleted todo {todo_id}")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def render_todo_item(todo: dict):
|
|
40
|
+
"""Render a single todo item"""
|
|
41
|
+
row = Row(class_="bg-white p-4 rounded-lg shadow-sm border border-gray-200 flex items-center justify-between")
|
|
42
|
+
|
|
43
|
+
# Left side: checkbox and title
|
|
44
|
+
left = Column(class_="flex-1")
|
|
45
|
+
|
|
46
|
+
status_badge = Badge(
|
|
47
|
+
label="✓" if todo["completed"] else "○",
|
|
48
|
+
class_="bg-blue-100 text-blue-800 px-2 py-1 rounded text-sm font-semibold"
|
|
49
|
+
)
|
|
50
|
+
left.add(status_badge)
|
|
51
|
+
|
|
52
|
+
title_class = "text-gray-500 line-through" if todo["completed"] else "text-gray-800"
|
|
53
|
+
title = Text(todo["title"], class_=f"{title_class} text-lg font-medium")
|
|
54
|
+
left.add(title)
|
|
55
|
+
|
|
56
|
+
row.add(left)
|
|
57
|
+
|
|
58
|
+
# Right side: action buttons
|
|
59
|
+
right = Row(class_="space-x-2")
|
|
60
|
+
|
|
61
|
+
toggle_btn = Button(
|
|
62
|
+
"Toggle",
|
|
63
|
+
class_="bg-green-500 hover:bg-green-600 text-white px-3 py-1 rounded text-sm",
|
|
64
|
+
on_click=lambda: toggle_todo(todo["id"])
|
|
65
|
+
)
|
|
66
|
+
right.add(toggle_btn)
|
|
67
|
+
|
|
68
|
+
delete_btn = Button(
|
|
69
|
+
"Delete",
|
|
70
|
+
class_="bg-red-500 hover:bg-red-600 text-white px-3 py-1 rounded text-sm",
|
|
71
|
+
on_click=lambda: delete_todo(todo["id"])
|
|
72
|
+
)
|
|
73
|
+
right.add(delete_btn)
|
|
74
|
+
|
|
75
|
+
row.add(right)
|
|
76
|
+
return row
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def create_view():
|
|
80
|
+
"""Create the todo app view"""
|
|
81
|
+
|
|
82
|
+
# Main container
|
|
83
|
+
main = Container(class_="bg-gray-50 min-h-screen p-4")
|
|
84
|
+
|
|
85
|
+
# Header
|
|
86
|
+
header = Container(class_="mb-6")
|
|
87
|
+
title = Text("My Todo App", class_="text-3xl font-bold text-gray-800 mb-2")
|
|
88
|
+
header.add(title)
|
|
89
|
+
|
|
90
|
+
subtitle = Text(
|
|
91
|
+
f"You have {len([t for t in todos if not t['completed']])} active tasks",
|
|
92
|
+
class_="text-gray-600"
|
|
93
|
+
)
|
|
94
|
+
header.add(subtitle)
|
|
95
|
+
main.add(header)
|
|
96
|
+
|
|
97
|
+
# Add todo section
|
|
98
|
+
add_section = Card(class_="bg-white p-6 rounded-lg shadow-md mb-6")
|
|
99
|
+
|
|
100
|
+
add_title = Text("Add New Todo", class_="text-lg font-semibold text-gray-800 mb-4")
|
|
101
|
+
add_section.add(add_title)
|
|
102
|
+
|
|
103
|
+
input_row = Row(class_="space-x-2")
|
|
104
|
+
input_field = Input(
|
|
105
|
+
placeholder="Enter a new todo...",
|
|
106
|
+
class_="flex-1 px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
107
|
+
)
|
|
108
|
+
input_row.add(input_field)
|
|
109
|
+
|
|
110
|
+
add_btn = Button(
|
|
111
|
+
"Add",
|
|
112
|
+
class_="bg-blue-600 hover:bg-blue-700 text-white px-6 py-2 rounded-lg font-semibold",
|
|
113
|
+
on_click=lambda: add_todo(input_field.props.get("value", ""))
|
|
114
|
+
)
|
|
115
|
+
input_row.add(add_btn)
|
|
116
|
+
|
|
117
|
+
add_section.add(input_row)
|
|
118
|
+
main.add(add_section)
|
|
119
|
+
|
|
120
|
+
# Todo list section
|
|
121
|
+
list_section = Card(class_="bg-white p-6 rounded-lg shadow-md")
|
|
122
|
+
|
|
123
|
+
list_title = Text("Todo List", class_="text-lg font-semibold text-gray-800 mb-4")
|
|
124
|
+
list_section.add(list_title)
|
|
125
|
+
|
|
126
|
+
if not todos:
|
|
127
|
+
empty_msg = Text(
|
|
128
|
+
"No todos yet. Add one to get started!",
|
|
129
|
+
class_="text-gray-500 text-center py-8"
|
|
130
|
+
)
|
|
131
|
+
list_section.add(empty_msg)
|
|
132
|
+
else:
|
|
133
|
+
# Render each todo
|
|
134
|
+
for todo in todos:
|
|
135
|
+
todo_item = render_todo_item(todo)
|
|
136
|
+
list_section.add(todo_item)
|
|
137
|
+
|
|
138
|
+
main.add(list_section)
|
|
139
|
+
|
|
140
|
+
# Footer stats
|
|
141
|
+
footer = Container(class_="mt-6 text-center text-gray-600 text-sm")
|
|
142
|
+
completed_count = len([t for t in todos if t["completed"]])
|
|
143
|
+
stats_text = Text(
|
|
144
|
+
f"{completed_count}/{len(todos)} tasks completed",
|
|
145
|
+
class_="text-gray-600"
|
|
146
|
+
)
|
|
147
|
+
footer.add(stats_text)
|
|
148
|
+
main.add(footer)
|
|
149
|
+
|
|
150
|
+
return main.build()
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def main():
|
|
154
|
+
"""Main entry point"""
|
|
155
|
+
Render.execute(create_view)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
if __name__ == "__main__":
|
|
159
|
+
main()
|
p2m/__init__.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Python2Mobile - Write mobile apps in pure Python
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
__version__ = "0.1.0"
|
|
6
|
+
__author__ = "P2M Team"
|
|
7
|
+
__license__ = "MIT"
|
|
8
|
+
|
|
9
|
+
from p2m.core import Render
|
|
10
|
+
from p2m.ui import (
|
|
11
|
+
Container,
|
|
12
|
+
Text,
|
|
13
|
+
Button,
|
|
14
|
+
Input,
|
|
15
|
+
Image,
|
|
16
|
+
List,
|
|
17
|
+
Navigator,
|
|
18
|
+
Screen,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
"Render",
|
|
23
|
+
"Container",
|
|
24
|
+
"Text",
|
|
25
|
+
"Button",
|
|
26
|
+
"Input",
|
|
27
|
+
"Image",
|
|
28
|
+
"List",
|
|
29
|
+
"Navigator",
|
|
30
|
+
"Screen",
|
|
31
|
+
]
|