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 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
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