creation-framework 0.1.0a2__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.
- creation/__init__.py +97 -0
- creation/assets/__init__.py +0 -0
- creation/cli/__init__.py +0 -0
- creation/cli/cli.py +431 -0
- creation/components/__init__.py +0 -0
- creation/components/component.py +390 -0
- creation/context/__init__.py +0 -0
- creation/context/context.py +42 -0
- creation/core/__init__.py +0 -0
- creation/core/lifecycle.py +24 -0
- creation/diff/__init__.py +0 -0
- creation/diff/keyed.py +195 -0
- creation/dom/__init__.py +0 -0
- creation/dom/dom.py +218 -0
- creation/error/__init__.py +0 -0
- creation/error/error.py +50 -0
- creation/hooks/__init__.py +5 -0
- creation/hooks/hooks.py +228 -0
- creation/js/creation.js +165 -0
- creation/js/kernel.js +345 -0
- creation/kernel/__init__.py +0 -0
- creation/kernel/kernel.py +321 -0
- creation/kernel/timers.py +147 -0
- creation/reactive/__init__.py +0 -0
- creation/reactive/reactive.py +222 -0
- creation/router/__init__.py +0 -0
- creation/router/router.py +242 -0
- creation/src/__init__.py +0 -0
- creation/src/app.py +11 -0
- creation/src/html.py +428 -0
- creation/store/__init__.py +0 -0
- creation/store/store.py +135 -0
- creation_framework-0.1.0a2.dist-info/METADATA +254 -0
- creation_framework-0.1.0a2.dist-info/RECORD +38 -0
- creation_framework-0.1.0a2.dist-info/WHEEL +5 -0
- creation_framework-0.1.0a2.dist-info/entry_points.txt +2 -0
- creation_framework-0.1.0a2.dist-info/licenses/LICENSE +21 -0
- creation_framework-0.1.0a2.dist-info/top_level.txt +1 -0
creation/__init__.py
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Creation - A Python web framework powered by Pyodide.
|
|
3
|
+
|
|
4
|
+
Core APIs:
|
|
5
|
+
- signal, computed, effect, batch - Reactive primitives
|
|
6
|
+
- component, @page, Link - Component and routing
|
|
7
|
+
- div, span, button, etc. - HTML elements with tw() styling
|
|
8
|
+
- set_timeout, set_interval - Browser timers
|
|
9
|
+
- create_store - Global state management
|
|
10
|
+
- use_effect, use_memo, use_ref - React-like hooks
|
|
11
|
+
|
|
12
|
+
Note: Browser-only modules (kernel, dom, timers) are only imported at runtime
|
|
13
|
+
in Pyodide, not at CLI time.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
__version__ = "0.1.0"
|
|
17
|
+
|
|
18
|
+
# Public API exports for IDE autocompletion and discoverability
|
|
19
|
+
__all__ = [
|
|
20
|
+
# Version
|
|
21
|
+
"__version__",
|
|
22
|
+
# Reactive primitives
|
|
23
|
+
"signal", "computed", "effect", "batch", "Signal", "Computed",
|
|
24
|
+
# Components
|
|
25
|
+
"component",
|
|
26
|
+
# Routing
|
|
27
|
+
"page", "Link", "navigate",
|
|
28
|
+
# Timers
|
|
29
|
+
"set_timeout", "set_interval", "clear_timeout", "clear_interval",
|
|
30
|
+
# Hooks
|
|
31
|
+
"use_effect", "use_memo", "use_ref", "use_callback",
|
|
32
|
+
# Lifecycle
|
|
33
|
+
"on_mount", "on_cleanup",
|
|
34
|
+
# HTML elements
|
|
35
|
+
"tw", "div", "span", "p", "button", "input", "a",
|
|
36
|
+
"h1", "h2", "h3", "h4", "h5", "h6",
|
|
37
|
+
# Store
|
|
38
|
+
"create_store", "init_store", "get_store",
|
|
39
|
+
# Context
|
|
40
|
+
"create_context", "use_context",
|
|
41
|
+
# Error handling
|
|
42
|
+
"ErrorBoundary",
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def __getattr__(name):
|
|
47
|
+
"""
|
|
48
|
+
Lazy import to avoid loading browser-only modules at CLI time.
|
|
49
|
+
This allows the CLI to work while still providing convenient imports
|
|
50
|
+
when running in Pyodide.
|
|
51
|
+
"""
|
|
52
|
+
# Reactive (safe to import)
|
|
53
|
+
if name in ("signal", "computed", "effect", "batch", "Signal", "Computed"):
|
|
54
|
+
from .reactive.reactive import signal, computed, effect, batch, Signal, Computed
|
|
55
|
+
return locals()[name]
|
|
56
|
+
|
|
57
|
+
# Store (safe to import)
|
|
58
|
+
if name in ("create_store", "init_store", "get_store"):
|
|
59
|
+
from .store.store import create_store, init_store, get_store
|
|
60
|
+
return locals()[name]
|
|
61
|
+
|
|
62
|
+
# The following require browser environment (js module)
|
|
63
|
+
# They will fail at CLI time but work in Pyodide
|
|
64
|
+
|
|
65
|
+
if name == "component":
|
|
66
|
+
from .components.component import component
|
|
67
|
+
return component
|
|
68
|
+
|
|
69
|
+
if name in ("page", "Link", "navigate"):
|
|
70
|
+
from .router.router import page, Link, navigate
|
|
71
|
+
return locals()[name]
|
|
72
|
+
|
|
73
|
+
if name in ("set_timeout", "set_interval", "clear_timeout", "clear_interval"):
|
|
74
|
+
from .kernel.timers import set_timeout, set_interval, clear_timeout, clear_interval
|
|
75
|
+
return locals()[name]
|
|
76
|
+
|
|
77
|
+
if name in ("use_effect", "use_memo", "use_ref", "use_callback"):
|
|
78
|
+
from .hooks.hooks import use_effect, use_memo, use_ref, use_callback
|
|
79
|
+
return locals()[name]
|
|
80
|
+
|
|
81
|
+
if name in ("tw", "div", "span", "p", "button", "input", "a", "h1", "h2", "h3", "h4", "h5", "h6"):
|
|
82
|
+
from .src.html import tw, div, span, p, button, input, a, h1, h2, h3, h4, h5, h6
|
|
83
|
+
return locals()[name]
|
|
84
|
+
|
|
85
|
+
if name == "ErrorBoundary":
|
|
86
|
+
from .error.error import ErrorBoundary
|
|
87
|
+
return ErrorBoundary
|
|
88
|
+
|
|
89
|
+
if name in ("create_context", "use_context"):
|
|
90
|
+
from .context.context import create_context, use_context
|
|
91
|
+
return locals()[name]
|
|
92
|
+
|
|
93
|
+
if name in ("on_mount", "on_cleanup"):
|
|
94
|
+
from .core.lifecycle import on_mount, on_cleanup
|
|
95
|
+
return locals()[name]
|
|
96
|
+
|
|
97
|
+
raise AttributeError(f"module 'creation' has no attribute '{name}'")
|
|
File without changes
|
creation/cli/__init__.py
ADDED
|
File without changes
|
creation/cli/cli.py
ADDED
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Creation CLI - FastAPI-style route discovery.
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
creation init <project> Create new project with app.py
|
|
6
|
+
creation build [target] Build for production
|
|
7
|
+
creation run [target] Run development server
|
|
8
|
+
|
|
9
|
+
Target can be:
|
|
10
|
+
- A Python file (app.py)
|
|
11
|
+
- A directory (src/)
|
|
12
|
+
- Omitted (scans current directory)
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import argparse
|
|
16
|
+
import os
|
|
17
|
+
import shutil
|
|
18
|
+
import http.server
|
|
19
|
+
import socketserver
|
|
20
|
+
import webbrowser
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from typing import List, Optional
|
|
23
|
+
|
|
24
|
+
ROOT = Path.cwd()
|
|
25
|
+
CREATION_DIR = ROOT / ".creation"
|
|
26
|
+
DIST = CREATION_DIR / "dist"
|
|
27
|
+
|
|
28
|
+
PKG_ROOT = Path(__file__).resolve().parent.parent
|
|
29
|
+
ENGINE_DIR = (PKG_ROOT / "creation") if (PKG_ROOT / "creation").exists() else PKG_ROOT
|
|
30
|
+
JS_DIR = PKG_ROOT / "js"
|
|
31
|
+
PYODIDE_DIR = PKG_ROOT / "assets" / "pyodide"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
INDEX_HTML = """<!DOCTYPE html>
|
|
35
|
+
<html>
|
|
36
|
+
<head>
|
|
37
|
+
<meta charset="utf-8" />
|
|
38
|
+
<title>Creation App</title>
|
|
39
|
+
</head>
|
|
40
|
+
<body>
|
|
41
|
+
<div id="app"></div>
|
|
42
|
+
|
|
43
|
+
<script src="/pyodide/pyodide.js"></script>
|
|
44
|
+
<script src="/kernel.js"></script>
|
|
45
|
+
<script src="/creation.js"></script>
|
|
46
|
+
|
|
47
|
+
<script>
|
|
48
|
+
// Start the full Creation engine (loads creation.zip, app.py, kernel, etc.)
|
|
49
|
+
Creation.start();
|
|
50
|
+
</script>
|
|
51
|
+
|
|
52
|
+
</body>
|
|
53
|
+
</html>
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def ensure_dirs():
|
|
58
|
+
DIST.mkdir(parents=True, exist_ok=True)
|
|
59
|
+
(DIST / "pyodide").mkdir(parents=True, exist_ok=True)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def discover_py_files(target: Optional[Path] = None) -> List[Path]:
|
|
63
|
+
"""
|
|
64
|
+
Discover Python files from target.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
target: Can be a file, directory, or None (uses current dir)
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
List of Python file paths
|
|
71
|
+
"""
|
|
72
|
+
if target is None:
|
|
73
|
+
target = ROOT
|
|
74
|
+
|
|
75
|
+
target = Path(target).resolve()
|
|
76
|
+
|
|
77
|
+
if target.is_file():
|
|
78
|
+
if target.suffix == ".py":
|
|
79
|
+
return [target]
|
|
80
|
+
return []
|
|
81
|
+
|
|
82
|
+
if target.is_dir():
|
|
83
|
+
# Find all .py files, excluding __pycache__, .creation, etc.
|
|
84
|
+
files = []
|
|
85
|
+
for p in target.rglob("*.py"):
|
|
86
|
+
# Skip hidden dirs, __pycache__, .creation, venv, etc.
|
|
87
|
+
parts = p.relative_to(target).parts
|
|
88
|
+
skip = False
|
|
89
|
+
for part in parts:
|
|
90
|
+
if part.startswith("__") or part.startswith(".") or part in ("venv", "env", ".venv", "node_modules"):
|
|
91
|
+
skip = True
|
|
92
|
+
break
|
|
93
|
+
if not skip:
|
|
94
|
+
files.append(p)
|
|
95
|
+
return files
|
|
96
|
+
|
|
97
|
+
return []
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def generate_app_py(target: Optional[Path] = None):
|
|
101
|
+
"""
|
|
102
|
+
Generate app.py that imports all discovered Python files.
|
|
103
|
+
Routes are registered via @page decorator when modules are imported.
|
|
104
|
+
"""
|
|
105
|
+
py_files = discover_py_files(target)
|
|
106
|
+
|
|
107
|
+
if not py_files:
|
|
108
|
+
print("[warning] No Python files found to import")
|
|
109
|
+
|
|
110
|
+
imports = []
|
|
111
|
+
base = target if target and target.is_dir() else ROOT
|
|
112
|
+
|
|
113
|
+
for f in py_files:
|
|
114
|
+
try:
|
|
115
|
+
rel = f.relative_to(base)
|
|
116
|
+
mod_path = rel.with_suffix("").as_posix().replace("/", ".")
|
|
117
|
+
imports.append(f"import {mod_path}")
|
|
118
|
+
except ValueError:
|
|
119
|
+
# File not relative to base, use absolute import style
|
|
120
|
+
mod_path = f.stem
|
|
121
|
+
imports.append(f"# {f.name}")
|
|
122
|
+
|
|
123
|
+
content = "\n".join([
|
|
124
|
+
"# Auto-generated app entry for Creation",
|
|
125
|
+
"# Routes are registered via @page decorator when modules import",
|
|
126
|
+
"",
|
|
127
|
+
*imports,
|
|
128
|
+
"",
|
|
129
|
+
"from creation.src.app import start",
|
|
130
|
+
"",
|
|
131
|
+
"start()",
|
|
132
|
+
])
|
|
133
|
+
|
|
134
|
+
(DIST / "app.py").write_text(content, encoding="utf-8")
|
|
135
|
+
print(f"[build] Generated app.py with {len(py_files)} module(s)")
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def copy_engine():
|
|
139
|
+
"""Copy the creation Python package into dist so Pyodide can import it."""
|
|
140
|
+
engine_src = ENGINE_DIR
|
|
141
|
+
engine_dst = DIST / engine_src.name
|
|
142
|
+
|
|
143
|
+
if engine_dst.exists():
|
|
144
|
+
shutil.rmtree(engine_dst)
|
|
145
|
+
|
|
146
|
+
shutil.copytree(
|
|
147
|
+
engine_src,
|
|
148
|
+
engine_dst,
|
|
149
|
+
ignore=shutil.ignore_patterns("*.pyc", "__pycache__", "dist", "*.egg-info"),
|
|
150
|
+
)
|
|
151
|
+
print(f"[build] Copied engine")
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def copy_assets():
|
|
155
|
+
"""Copy creation.js + kernel.js + pyodide folder from creation package → dist"""
|
|
156
|
+
creation_js = JS_DIR / "creation.js"
|
|
157
|
+
kernel_js = JS_DIR / "kernel.js"
|
|
158
|
+
|
|
159
|
+
if not creation_js.exists() or not kernel_js.exists():
|
|
160
|
+
print("[error] Missing creation.js or kernel.js inside creation/js/")
|
|
161
|
+
return
|
|
162
|
+
|
|
163
|
+
shutil.copy2(creation_js, DIST / "creation.js")
|
|
164
|
+
shutil.copy2(kernel_js, DIST / "kernel.js")
|
|
165
|
+
print("[build] Copied creation.js + kernel.js")
|
|
166
|
+
|
|
167
|
+
if PYODIDE_DIR.exists():
|
|
168
|
+
target_dir = DIST / "pyodide"
|
|
169
|
+
if target_dir.exists():
|
|
170
|
+
shutil.rmtree(target_dir)
|
|
171
|
+
shutil.copytree(PYODIDE_DIR, target_dir)
|
|
172
|
+
print("[build] Copied pyodide runtime")
|
|
173
|
+
else:
|
|
174
|
+
print("[warning] pyodide/ folder missing inside creation/assets/")
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def copy_user_code(target: Optional[Path] = None):
|
|
178
|
+
"""Copy user Python files to dist."""
|
|
179
|
+
py_files = discover_py_files(target)
|
|
180
|
+
base = target if target and target.is_dir() else ROOT
|
|
181
|
+
|
|
182
|
+
for f in py_files:
|
|
183
|
+
try:
|
|
184
|
+
rel = f.relative_to(base)
|
|
185
|
+
dest = DIST / rel
|
|
186
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
187
|
+
shutil.copy2(f, dest)
|
|
188
|
+
except ValueError:
|
|
189
|
+
# Not relative, copy to root of dist
|
|
190
|
+
shutil.copy2(f, DIST / f.name)
|
|
191
|
+
|
|
192
|
+
print(f"[build] Copied {len(py_files)} Python file(s)")
|
|
193
|
+
|
|
194
|
+
# Also copy public/ if it exists
|
|
195
|
+
public_dir = (target if target and target.is_dir() else ROOT) / "public"
|
|
196
|
+
if public_dir.exists():
|
|
197
|
+
for item in public_dir.iterdir():
|
|
198
|
+
dst = DIST / item.name
|
|
199
|
+
if item.is_dir():
|
|
200
|
+
if dst.exists():
|
|
201
|
+
shutil.rmtree(dst)
|
|
202
|
+
shutil.copytree(item, dst)
|
|
203
|
+
else:
|
|
204
|
+
shutil.copy2(item, dst)
|
|
205
|
+
print("[build] Copied public/")
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def write_index_html():
|
|
209
|
+
(DIST / "index.html").write_text(INDEX_HTML, encoding="utf-8")
|
|
210
|
+
print("[build] Wrote index.html")
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def pack_engine():
|
|
214
|
+
"""Pack engine + project python files into creation.zip."""
|
|
215
|
+
import zipfile
|
|
216
|
+
|
|
217
|
+
target_zip = DIST / "creation.zip"
|
|
218
|
+
if target_zip.exists():
|
|
219
|
+
target_zip.unlink()
|
|
220
|
+
|
|
221
|
+
with zipfile.ZipFile(target_zip, "w", zipfile.ZIP_DEFLATED) as zf:
|
|
222
|
+
# Pack the engine (creation package)
|
|
223
|
+
engine_path = DIST / ENGINE_DIR.name
|
|
224
|
+
if engine_path.exists():
|
|
225
|
+
for file in engine_path.rglob("*"):
|
|
226
|
+
if file.is_file() and "__pycache__" not in str(file):
|
|
227
|
+
arcname = file.relative_to(DIST)
|
|
228
|
+
zf.write(file, arcname)
|
|
229
|
+
|
|
230
|
+
# Pack user files (everything in dist that's .py except app.py in engine)
|
|
231
|
+
for file in DIST.rglob("*.py"):
|
|
232
|
+
if ENGINE_DIR.name not in file.parts or file == DIST / "app.py":
|
|
233
|
+
arcname = file.relative_to(DIST)
|
|
234
|
+
if str(arcname) not in [str(a) for a in zf.namelist()]:
|
|
235
|
+
zf.write(file, arcname)
|
|
236
|
+
|
|
237
|
+
print(f"[build] Packed creation.zip")
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def build_all(target: Optional[Path] = None):
|
|
241
|
+
"""Build the complete dist folder."""
|
|
242
|
+
print(f"[build] Building from {target or ROOT}")
|
|
243
|
+
ensure_dirs()
|
|
244
|
+
copy_engine()
|
|
245
|
+
copy_assets()
|
|
246
|
+
copy_user_code(target)
|
|
247
|
+
generate_app_py(target)
|
|
248
|
+
write_index_html()
|
|
249
|
+
pack_engine()
|
|
250
|
+
print("[build] Done!")
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
class SPAHandler(http.server.SimpleHTTPRequestHandler):
|
|
254
|
+
"""Handler that serves index.html for SPA routes."""
|
|
255
|
+
|
|
256
|
+
def do_GET(self):
|
|
257
|
+
path = self.path.split("?")[0]
|
|
258
|
+
file_path = DIST / path.lstrip("/")
|
|
259
|
+
|
|
260
|
+
if not file_path.exists() and not "." in path.split("/")[-1]:
|
|
261
|
+
self.path = "/index.html"
|
|
262
|
+
|
|
263
|
+
try:
|
|
264
|
+
return super().do_GET()
|
|
265
|
+
except BrokenPipeError:
|
|
266
|
+
pass # Client disconnected, ignore
|
|
267
|
+
|
|
268
|
+
def log_message(self, format, *args):
|
|
269
|
+
# Suppress noisy 404s for source maps, favicons, DevTools files
|
|
270
|
+
path = args[0] if args else ""
|
|
271
|
+
status = args[1] if len(args) > 1 else ""
|
|
272
|
+
|
|
273
|
+
# Files we don't care about 404s for
|
|
274
|
+
ignored = (".map", "favicon.ico", ".well-known", "chrome-devtools")
|
|
275
|
+
if status == "404" and any(x in path for x in ignored):
|
|
276
|
+
return
|
|
277
|
+
|
|
278
|
+
# Only log non-200 status
|
|
279
|
+
if status != "200":
|
|
280
|
+
print(f"[server] {path} {status}")
|
|
281
|
+
|
|
282
|
+
def handle(self):
|
|
283
|
+
try:
|
|
284
|
+
super().handle()
|
|
285
|
+
except BrokenPipeError:
|
|
286
|
+
pass # Suppress broken pipe errors
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def serve_dist(host: str = "127.0.0.1", port: int = 3000):
|
|
290
|
+
os.chdir(str(DIST))
|
|
291
|
+
handler = SPAHandler
|
|
292
|
+
httpd = socketserver.TCPServer((host, port), handler)
|
|
293
|
+
url = f"http://{host}:{port}"
|
|
294
|
+
print(f"[server] Dev server running at {url}")
|
|
295
|
+
webbrowser.open(url)
|
|
296
|
+
try:
|
|
297
|
+
httpd.serve_forever()
|
|
298
|
+
except KeyboardInterrupt:
|
|
299
|
+
print("\n[server] Stopped")
|
|
300
|
+
finally:
|
|
301
|
+
httpd.server_close()
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
# ============================================================================
|
|
305
|
+
# Commands
|
|
306
|
+
# ============================================================================
|
|
307
|
+
|
|
308
|
+
def cmd_init(project_name: str):
|
|
309
|
+
"""Create a new Creation project with minimal structure."""
|
|
310
|
+
target_dir = Path(project_name)
|
|
311
|
+
|
|
312
|
+
if target_dir.exists():
|
|
313
|
+
print(f"[init] {project_name} already exists")
|
|
314
|
+
return
|
|
315
|
+
|
|
316
|
+
target_dir.mkdir(parents=True)
|
|
317
|
+
|
|
318
|
+
# Create simple app.py (FastAPI style)
|
|
319
|
+
app_content = '''"""
|
|
320
|
+
My Creation App
|
|
321
|
+
"""
|
|
322
|
+
from creation.router.router import page
|
|
323
|
+
from creation.src.html import div, h1, h2, button, p
|
|
324
|
+
from creation.reactive.reactive import signal
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
@page("/")
|
|
328
|
+
def Home():
|
|
329
|
+
"""Home page with a simple counter."""
|
|
330
|
+
count = signal(0)
|
|
331
|
+
|
|
332
|
+
def increment(ev=None):
|
|
333
|
+
count(count() + 1)
|
|
334
|
+
|
|
335
|
+
return div(
|
|
336
|
+
h1("🚀 Welcome to Creation!"),
|
|
337
|
+
p("A Python-native reactive UI framework"),
|
|
338
|
+
|
|
339
|
+
div(
|
|
340
|
+
h2(lambda: f"Count: {count()}"),
|
|
341
|
+
button("+1", on_click=increment, style={
|
|
342
|
+
"padding": "0.5rem 1rem",
|
|
343
|
+
"fontSize": "1rem",
|
|
344
|
+
"cursor": "pointer"
|
|
345
|
+
}),
|
|
346
|
+
style={"marginTop": "2rem"}
|
|
347
|
+
),
|
|
348
|
+
|
|
349
|
+
style={
|
|
350
|
+
"fontFamily": "system-ui, sans-serif",
|
|
351
|
+
"padding": "2rem",
|
|
352
|
+
"maxWidth": "600px",
|
|
353
|
+
"margin": "0 auto"
|
|
354
|
+
}
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
@page("/about")
|
|
359
|
+
def About():
|
|
360
|
+
"""About page."""
|
|
361
|
+
return div(
|
|
362
|
+
h1("About"),
|
|
363
|
+
p("Built with Creation - 100% Python in the browser!"),
|
|
364
|
+
style={
|
|
365
|
+
"fontFamily": "system-ui, sans-serif",
|
|
366
|
+
"padding": "2rem"
|
|
367
|
+
}
|
|
368
|
+
)
|
|
369
|
+
'''
|
|
370
|
+
|
|
371
|
+
(target_dir / "app.py").write_text(app_content, encoding="utf-8")
|
|
372
|
+
|
|
373
|
+
# Create public directory for static files
|
|
374
|
+
(target_dir / "public").mkdir()
|
|
375
|
+
|
|
376
|
+
print(f"[init] Created project: {project_name}/")
|
|
377
|
+
print(f" └── app.py")
|
|
378
|
+
print(f" └── public/")
|
|
379
|
+
print(f"\nNext steps:")
|
|
380
|
+
print(f" cd {project_name}")
|
|
381
|
+
print(f" creation run")
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
def cmd_build(target: Optional[str] = None):
|
|
385
|
+
"""Build project for production."""
|
|
386
|
+
target_path = Path(target).resolve() if target else None
|
|
387
|
+
build_all(target_path)
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
def cmd_run(target: Optional[str] = None, host: str = "127.0.0.1", port: int = 3000):
|
|
391
|
+
"""Build and run development server."""
|
|
392
|
+
target_path = Path(target).resolve() if target else None
|
|
393
|
+
build_all(target_path)
|
|
394
|
+
serve_dist(host, port)
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
def main():
|
|
398
|
+
parser = argparse.ArgumentParser(
|
|
399
|
+
prog="creation",
|
|
400
|
+
description="Creation - Python-native web framework"
|
|
401
|
+
)
|
|
402
|
+
sub = parser.add_subparsers(dest="cmd")
|
|
403
|
+
|
|
404
|
+
# init command
|
|
405
|
+
p_init = sub.add_parser("init", help="Create new project")
|
|
406
|
+
p_init.add_argument("project", help="Project name")
|
|
407
|
+
|
|
408
|
+
# build command
|
|
409
|
+
p_build = sub.add_parser("build", help="Build for production")
|
|
410
|
+
p_build.add_argument("target", nargs="?", help="File or directory to build (default: current dir)")
|
|
411
|
+
|
|
412
|
+
# run command
|
|
413
|
+
p_run = sub.add_parser("run", help="Run development server")
|
|
414
|
+
p_run.add_argument("target", nargs="?", help="File or directory to run (default: current dir)")
|
|
415
|
+
p_run.add_argument("--host", default="127.0.0.1", help="Host to bind (default: 127.0.0.1)")
|
|
416
|
+
p_run.add_argument("--port", default=3000, type=int, help="Port to bind (default: 3000)")
|
|
417
|
+
|
|
418
|
+
args = parser.parse_args()
|
|
419
|
+
|
|
420
|
+
if args.cmd == "init":
|
|
421
|
+
cmd_init(args.project)
|
|
422
|
+
elif args.cmd == "build":
|
|
423
|
+
cmd_build(args.target)
|
|
424
|
+
elif args.cmd == "run":
|
|
425
|
+
cmd_run(args.target, args.host, args.port)
|
|
426
|
+
else:
|
|
427
|
+
parser.print_help()
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
if __name__ == "__main__":
|
|
431
|
+
main()
|
|
File without changes
|