uigen 0.1.0__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.
uigen/__init__.py ADDED
@@ -0,0 +1,19 @@
1
+ """uigen — Write UI logic once, deploy anywhere."""
2
+
3
+ from uigen.core.api import ui
4
+ from uigen.core.compiler import App
5
+ from uigen.core.schema import Field, Model
6
+ from uigen.theme import Theme, get_theme, list_themes, register_theme
7
+
8
+ __version__ = "0.1.0"
9
+
10
+ __all__ = [
11
+ "Model",
12
+ "Field",
13
+ "ui",
14
+ "App",
15
+ "Theme",
16
+ "get_theme",
17
+ "list_themes",
18
+ "register_theme",
19
+ ]
uigen/_cext.c ADDED
@@ -0,0 +1,79 @@
1
+ /**
2
+ * uigen C extension — fast HTML escaping
3
+ *
4
+ * This module provides C-accelerated HTML entity escaping.
5
+ *
6
+ * Build with: python setup.py build_ext --inplace
7
+ */
8
+
9
+ #define PY_SSIZE_T_CLEAN
10
+ #include <Python.h>
11
+ #include <string.h>
12
+ #include <stdlib.h>
13
+
14
+ /**
15
+ * Fast HTML entity escaping
16
+ */
17
+ static PyObject* escape_html(PyObject* self, PyObject* args) {
18
+ const char* input;
19
+ Py_ssize_t input_len;
20
+
21
+ if (!PyArg_ParseTuple(args, "s#", &input, &input_len)) {
22
+ return NULL;
23
+ }
24
+
25
+ /* Allocate output buffer (worst case: every char needs escaping) */
26
+ char* output = (char*)malloc(input_len * 7 + 1);
27
+ if (!output) {
28
+ return PyErr_NoMemory();
29
+ }
30
+
31
+ char* out = output;
32
+ for (Py_ssize_t i = 0; i < input_len; i++) {
33
+ char c = input[i];
34
+ switch (c) {
35
+ case '&': memcpy(out, "&amp;", 5); out += 5; break;
36
+ case '<': memcpy(out, "&lt;", 4); out += 4; break;
37
+ case '>': memcpy(out, "&gt;", 4); out += 4; break;
38
+ case '"': memcpy(out, "&quot;", 6); out += 6; break;
39
+ case '\'': memcpy(out, "&#39;", 5); out += 5; break;
40
+ default: *out++ = c; break;
41
+ }
42
+ }
43
+ *out = '\0';
44
+
45
+ PyObject* result = PyUnicode_FromString(output);
46
+ free(output);
47
+ return result;
48
+ }
49
+
50
+ /**
51
+ * Module method definitions
52
+ */
53
+ static PyMethodDef uigen_methods[] = {
54
+ {
55
+ "escape_html",
56
+ escape_html,
57
+ METH_VARARGS,
58
+ "Escape HTML special characters"
59
+ },
60
+ {NULL, NULL, 0, NULL}
61
+ };
62
+
63
+ /**
64
+ * Module definition
65
+ */
66
+ static struct PyModuleDef uigen_module = {
67
+ PyModuleDef_HEAD_INIT,
68
+ "uigen._cext",
69
+ "uigen C extension for fast HTML escaping",
70
+ -1,
71
+ uigen_methods
72
+ };
73
+
74
+ /**
75
+ * Module initialization
76
+ */
77
+ PyMODINIT_FUNC PyInit__cext(void) {
78
+ return PyModule_Create(&uigen_module);
79
+ }
uigen/accel.py ADDED
@@ -0,0 +1,60 @@
1
+ """Accelerated functions using C extension (optional)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+
8
+ def _check_cext() -> bool:
9
+ """Check if C extension is available."""
10
+ try:
11
+ import uigen._cext # noqa: F401
12
+ return True
13
+ except ImportError:
14
+ return False
15
+
16
+
17
+ HAS_CEXT = _check_cext()
18
+
19
+
20
+ def escape_html(text: str) -> str:
21
+ """Escape HTML special characters.
22
+
23
+ Uses C extension if available, falls back to pure Python.
24
+ """
25
+ if HAS_CEXT:
26
+ from uigen._cext import escape_html as _escape_html
27
+ return _escape_html(text)
28
+
29
+ # Pure Python fallback
30
+ replacements = {
31
+ "&": "&amp;",
32
+ "<": "&lt;",
33
+ ">": "&gt;",
34
+ '"': "&quot;",
35
+ "'": "&#39;",
36
+ }
37
+ for char, escaped in replacements.items():
38
+ text = text.replace(char, escaped)
39
+ return text
40
+
41
+
42
+ def concat_strings(parts: list[str]) -> str:
43
+ """Fast string concatenation.
44
+
45
+ Uses C extension if available, falls back to pure Python.
46
+ """
47
+ if HAS_CEXT:
48
+ from uigen._cext import concat_strings as _concat
49
+ return _concat(parts)
50
+
51
+ # Pure Python fallback
52
+ return "".join(parts)
53
+
54
+
55
+ def get_info() -> dict[str, Any]:
56
+ """Get information about the C extension."""
57
+ return {
58
+ "has_cext": HAS_CEXT,
59
+ "backend": "C" if HAS_CEXT else "Python",
60
+ }
uigen/cli.py ADDED
@@ -0,0 +1,139 @@
1
+ """CLI — command-line interface for uigen."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import sys
7
+ from pathlib import Path
8
+
9
+
10
+ def main(argv: list[str] | None = None) -> int:
11
+ """Main entry point for the uigen CLI."""
12
+ parser = argparse.ArgumentParser(
13
+ prog="uigen",
14
+ description="Write UI logic once, deploy anywhere.",
15
+ )
16
+ subparsers = parser.add_subparsers(dest="command")
17
+
18
+ # uigen init
19
+ init_parser = subparsers.add_parser("init", help="Initialize a new uigen project")
20
+ init_parser.add_argument("name", help="Project name")
21
+
22
+ # uigen generate
23
+ gen_parser = subparsers.add_parser("generate", help="Generate frontend code")
24
+ gen_parser.add_argument(
25
+ "--renderer", "-r",
26
+ default="lnative",
27
+ choices=["lnative", "lreact", "lflask", "ldjango"],
28
+ help="Renderer to use (default: lnative)",
29
+ )
30
+ gen_parser.add_argument(
31
+ "--output", "-o",
32
+ default="./dist",
33
+ help="Output directory (default: ./dist)",
34
+ )
35
+
36
+ # uigen renderers
37
+ subparsers.add_parser("renderers", help="List available renderers")
38
+
39
+ args = parser.parse_args(argv)
40
+
41
+ if args.command == "init":
42
+ return _init_project(args.name)
43
+ elif args.command == "generate":
44
+ return _generate(args.renderer, args.output)
45
+ elif args.command == "renderers":
46
+ _list_renderers()
47
+ return 0
48
+ else:
49
+ parser.print_help()
50
+ return 1
51
+
52
+
53
+ def _init_project(name: str) -> int:
54
+ """Initialize a new uigen project."""
55
+ project_dir = Path(name)
56
+
57
+ if project_dir.exists():
58
+ print(f"Error: Directory '{name}' already exists.", file=sys.stderr)
59
+ return 1
60
+
61
+ project_dir.mkdir(parents=True)
62
+
63
+ # Create main.py
64
+ main_file = project_dir / "main.py"
65
+ main_file.write_text(f'''"""Generated by uigen — {name}"""
66
+
67
+ from uigen import App, Model, ui
68
+
69
+
70
+ class User(Model):
71
+ name: str
72
+ email: str
73
+ role: str = "viewer"
74
+
75
+
76
+ # Build your UI here
77
+ page = ui.page(
78
+ ui.card(
79
+ ui.heading("Welcome to {name}"),
80
+ ui.text("Edit main.py to customize this page."),
81
+ ui.button("Get Started"),
82
+ ),
83
+ title="{name}",
84
+ )
85
+
86
+ app = App(title="{name}", pages=[page])
87
+
88
+ if __name__ == "__main__":
89
+ output = app.render("lnative", output="./dist")
90
+ print(f"Generated UI at {{output}}")
91
+ ''')
92
+
93
+ print(f"Created project: {name}")
94
+ print(f" cd {name}")
95
+ print(" python main.py")
96
+ return 0
97
+
98
+
99
+ def _generate(renderer: str, output: str) -> int:
100
+ """Generate frontend from main.py."""
101
+ main_file = Path("main.py")
102
+ if not main_file.exists():
103
+ print("Error: No main.py found. Run 'uigen init' first.", file=sys.stderr)
104
+ return 1
105
+
106
+ # Execute main.py to get the app object
107
+ import importlib.util
108
+ spec = importlib.util.spec_from_file_location("main", main_file)
109
+ if spec is None or spec.loader is None:
110
+ print("Error: Could not load main.py", file=sys.stderr)
111
+ return 1
112
+
113
+ module = importlib.util.module_from_spec(spec)
114
+ spec.loader.exec_module(module)
115
+
116
+ if not hasattr(module, "app"):
117
+ print(
118
+ "Error: main.py must define an 'app' variable (App instance)",
119
+ file=sys.stderr,
120
+ )
121
+ return 1
122
+
123
+ app = module.app
124
+ output_path = app.render(renderer, output=output)
125
+ print(f"Generated {renderer} output at {output_path}")
126
+ return 0
127
+
128
+
129
+ def _list_renderers() -> None:
130
+ """List available renderers."""
131
+ print("Available renderers:")
132
+ print(" lnative - Static HTML/CSS/JS (ready)")
133
+ print(" lreact - React components (coming soon)")
134
+ print(" lflask - Flask/Jinja2 templates (coming soon)")
135
+ print(" ldjango - Django templates (coming soon)")
136
+
137
+
138
+ if __name__ == "__main__":
139
+ sys.exit(main())
uigen/core/__init__.py ADDED
@@ -0,0 +1,7 @@
1
+ """Core components of uigen."""
2
+
3
+ from uigen.core.api import ui
4
+ from uigen.core.compiler import App
5
+ from uigen.core.schema import Field, Model
6
+
7
+ __all__ = ["Model", "Field", "ui", "App"]
uigen/core/api.py ADDED
@@ -0,0 +1,128 @@
1
+ """Public API — the ui object that users interact with."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from uigen.core.components import (
8
+ Button,
9
+ Card,
10
+ Component,
11
+ Form,
12
+ Grid,
13
+ Heading,
14
+ Input,
15
+ Modal,
16
+ Page,
17
+ Select,
18
+ Stack,
19
+ Table,
20
+ Text,
21
+ )
22
+ from uigen.core.schema import Model
23
+
24
+
25
+ class UI:
26
+ """The ui namespace — provides methods to create UI components."""
27
+
28
+ def page(
29
+ self, *children: Component, title: str = "", description: str = ""
30
+ ) -> Page:
31
+ """Create a page container."""
32
+ return Page(
33
+ children=list(children),
34
+ title=title,
35
+ description=description,
36
+ )
37
+
38
+ def card(self, *children: Component, **attrs: Any) -> Card:
39
+ """Create a card container."""
40
+ return Card(children=list(children), attrs=attrs)
41
+
42
+ def heading(self, text: str, level: int = 1) -> Heading:
43
+ """Create a heading element."""
44
+ return Heading(text=text, level=level)
45
+
46
+ def text(self, content: str) -> Text:
47
+ """Create a paragraph/text element."""
48
+ return Text(text=content)
49
+
50
+ def button(self, text: str, variant: str = "primary", onclick: str = "") -> Button:
51
+ """Create a button element."""
52
+ return Button(text=text, variant=variant, onclick=onclick)
53
+
54
+ def input(
55
+ self,
56
+ name: str,
57
+ label: str = "",
58
+ type: str = "text",
59
+ placeholder: str = "",
60
+ value: str = "",
61
+ required: bool = False,
62
+ ) -> Input:
63
+ """Create a form input."""
64
+ return Input(
65
+ name=name,
66
+ label=label or name.replace("_", " ").title(),
67
+ input_type=type,
68
+ placeholder=placeholder,
69
+ value=value,
70
+ required=required,
71
+ )
72
+
73
+ def select(
74
+ self, name: str, options: list[str], label: str = "", value: str = ""
75
+ ) -> Select:
76
+ """Create a select dropdown."""
77
+ return Select(
78
+ name=name,
79
+ options=options,
80
+ label=label or name.replace("_", " ").title(),
81
+ value=value,
82
+ )
83
+
84
+ def table(
85
+ self,
86
+ data: list[dict[str, Any]] | Model | None = None,
87
+ columns: list[str] | None = None,
88
+ ) -> Table:
89
+ """Create a data table.
90
+
91
+ Args:
92
+ data: List of dicts, a Model class (uses its fields), or None.
93
+ columns: Column names. If None, auto-detected from data.
94
+ """
95
+ rows: list[dict[str, Any]] = []
96
+ cols = columns or []
97
+
98
+ if data is None:
99
+ cols = cols or []
100
+ elif isinstance(data, type) and issubclass(data, Model):
101
+ cols = cols or data.field_names()
102
+ elif isinstance(data, list):
103
+ rows = data
104
+ if not cols and rows:
105
+ cols = list(rows[0].keys())
106
+
107
+ return Table(columns=cols, rows=rows)
108
+
109
+ def form(
110
+ self, *children: Component, action: str = "", method: str = "POST"
111
+ ) -> Form:
112
+ """Create a form container."""
113
+ return Form(children=list(children), action=action, method=method)
114
+
115
+ def modal(self, *children: Component, title: str = "") -> Modal:
116
+ """Create a modal dialog."""
117
+ return Modal(children=list(children), title=title)
118
+
119
+ def grid(self, *children: Component, columns: int = 2) -> Grid:
120
+ """Create a grid layout."""
121
+ return Grid(children=list(children), columns=columns)
122
+
123
+ def stack(self, *children: Component, spacing: str = "md") -> Stack:
124
+ """Create a vertical stack layout."""
125
+ return Stack(children=list(children), spacing=spacing)
126
+
127
+
128
+ ui = UI()
uigen/core/compiler.py ADDED
@@ -0,0 +1,212 @@
1
+ """Compiler — transforms UI definitions into renderer output."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ from uigen.core.components import Page
9
+ from uigen.core.schema import Model
10
+
11
+
12
+ class App:
13
+ """Main application class that wires models and pages to renderers."""
14
+
15
+ def __init__(
16
+ self,
17
+ title: str = "My App",
18
+ models: list[type[Model]] | None = None,
19
+ pages: list[Page] | None = None,
20
+ theme: str | None = None,
21
+ ) -> None:
22
+ self.title = title
23
+ self.models = models or []
24
+ self.pages = pages or []
25
+ self.theme = theme
26
+ self._renderer: Any = None
27
+
28
+ def add_page(self, page: Page) -> App:
29
+ """Add a page to the app."""
30
+ self.pages.append(page)
31
+ return self
32
+
33
+ def render(self, renderer_name: str, output: str = "./dist") -> Path:
34
+ """Render all pages using the specified renderer.
35
+
36
+ Args:
37
+ renderer_name: Name of the renderer (e.g., "lnative", "lreact").
38
+ output: Output directory path.
39
+
40
+ Returns:
41
+ Path to the output directory.
42
+ """
43
+ renderer = self._get_renderer(renderer_name)
44
+ output_path = Path(output)
45
+ output_path.mkdir(parents=True, exist_ok=True)
46
+
47
+ if renderer_name == "lreact":
48
+ return self._render_react(renderer, output_path)
49
+ elif renderer_name == "lflask":
50
+ return self._render_flask(renderer, output_path)
51
+ elif renderer_name == "ldjango":
52
+ return self._render_django(renderer, output_path)
53
+ else:
54
+ return self._render_native(renderer, output_path)
55
+
56
+ def _render_native(self, renderer: Any, output_path: Path) -> Path:
57
+ """Render pages as static HTML files."""
58
+ for i, page in enumerate(self.pages):
59
+ filename = "index.html" if i == 0 else f"page_{i}.html"
60
+ content = renderer.render_page(page, title=self.title)
61
+ (output_path / filename).write_text(content)
62
+ return output_path
63
+
64
+ def _render_react(self, renderer: Any, output_path: Path) -> Path:
65
+ """Render pages as a React project."""
66
+ components_dir = output_path / "src" / "components"
67
+ components_dir.mkdir(parents=True, exist_ok=True)
68
+
69
+ component_names = []
70
+ for i, page in enumerate(self.pages):
71
+ page_title = page.title or f"Page{i}"
72
+ component_name = renderer._to_pascal_case(page_title)
73
+ component_names.append(component_name)
74
+
75
+ content = renderer.render_page(page, title=self.title)
76
+ (components_dir / f"{component_name}.tsx").write_text(content)
77
+
78
+ # Generate package.json
79
+ (output_path / "package.json").write_text(
80
+ renderer.generate_package_json(self.title)
81
+ )
82
+
83
+ # Generate index.html
84
+ (output_path / "index.html").write_text(
85
+ renderer.generate_index_html(self.title)
86
+ )
87
+
88
+ # Generate App.tsx
89
+ (output_path / "src" / "App.tsx").write_text(
90
+ renderer.generate_app_tsx(component_names)
91
+ )
92
+
93
+ # Generate index.tsx
94
+ (output_path / "src" / "index.tsx").write_text(
95
+ """import React from 'react';
96
+ import ReactDOM from 'react-dom/client';
97
+ import App from './App';
98
+
99
+ const root = ReactDOM.createRoot(
100
+ document.getElementById('root') as HTMLElement
101
+ );
102
+ root.render(
103
+ <React.StrictMode>
104
+ <App />
105
+ </React.StrictMode>
106
+ );"""
107
+ )
108
+
109
+ return output_path
110
+
111
+ def _render_flask(self, renderer: Any, output_path: Path) -> Path:
112
+ """Render pages as a Flask project."""
113
+ templates_dir = output_path / "templates" / "pages"
114
+ static_dir = output_path / "static"
115
+ templates_dir.mkdir(parents=True, exist_ok=True)
116
+ static_dir.mkdir(parents=True, exist_ok=True)
117
+
118
+ route_names = []
119
+ for i, page in enumerate(self.pages):
120
+ page_title = page.title or f"Page{i}"
121
+ route_name = renderer._to_snake_case(page_title)
122
+ route_names.append(route_name)
123
+
124
+ content = renderer.render_page(page, title=self.title)
125
+ (templates_dir / f"{route_name}.html").write_text(content)
126
+
127
+ # Generate base.html
128
+ (output_path / "templates" / "base.html").write_text(
129
+ renderer.generate_base_html(self.title)
130
+ )
131
+
132
+ # Generate app.py
133
+ (output_path / "app.py").write_text(
134
+ renderer.generate_app_py(self.title, route_names)
135
+ )
136
+
137
+ # Generate requirements.txt
138
+ (output_path / "requirements.txt").write_text(
139
+ renderer.generate_requirements()
140
+ )
141
+
142
+ return output_path
143
+
144
+ def _render_django(self, renderer: Any, output_path: Path) -> Path:
145
+ """Render pages as a Django project."""
146
+ project_name = renderer._to_snake_case(self.title)
147
+ templates_dir = output_path / "templates" / "pages"
148
+ app_dir = output_path / project_name
149
+ templates_dir.mkdir(parents=True, exist_ok=True)
150
+ app_dir.mkdir(parents=True, exist_ok=True)
151
+
152
+ route_names = []
153
+ for i, page in enumerate(self.pages):
154
+ page_title = page.title or f"Page{i}"
155
+ route_name = renderer._to_snake_case(page_title)
156
+ route_names.append(route_name)
157
+
158
+ content = renderer.render_page(page, title=self.title)
159
+ (templates_dir / f"{route_name}.html").write_text(content)
160
+
161
+ # Generate base.html
162
+ (output_path / "templates" / "base.html").write_text(
163
+ renderer.generate_base_html(self.title)
164
+ )
165
+
166
+ # Generate views.py
167
+ (app_dir / "views.py").write_text(
168
+ renderer.generate_views_py(self.title, route_names)
169
+ )
170
+
171
+ # Generate urls.py
172
+ (app_dir / "urls.py").write_text(
173
+ renderer.generate_urls_py(route_names)
174
+ )
175
+
176
+ # Generate settings.py
177
+ (output_path / "settings.py").write_text(
178
+ renderer.generate_settings_py(self.title)
179
+ )
180
+
181
+ # Generate requirements.txt
182
+ (output_path / "requirements.txt").write_text(
183
+ renderer.generate_requirements()
184
+ )
185
+
186
+ return output_path
187
+
188
+ def _get_renderer(self, name: str) -> Any:
189
+ """Import and instantiate a renderer by name."""
190
+ if name == "lnative":
191
+ from uigen.renderers.lnative import LNativeRenderer
192
+ return LNativeRenderer(theme=self.theme)
193
+ elif name == "lreact":
194
+ from uigen.renderers.lreact import LReactRenderer
195
+ return LReactRenderer()
196
+ elif name == "lflask":
197
+ from uigen.renderers.lflask import LFlaskRenderer
198
+ return LFlaskRenderer()
199
+ elif name == "ldjango":
200
+ from uigen.renderers.ldjango import LDjangoRenderer
201
+ return LDjangoRenderer()
202
+ else:
203
+ available = "lnative, lreact, lflask, ldjango"
204
+ raise ValueError(
205
+ f"Unknown renderer: {name}. Available: {available}"
206
+ )
207
+
208
+ def __repr__(self) -> str:
209
+ return (
210
+ f"App(title={self.title!r}, "
211
+ f"pages={len(self.pages)}, models={len(self.models)})"
212
+ )