sistine 0.1.0__tar.gz

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.
sistine-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,138 @@
1
+ Metadata-Version: 2.4
2
+ Name: sistine
3
+ Version: 0.1.0
4
+ Summary: React-like framework for Python - return HTML/CSS/JS like React, route like Next.js
5
+ Author: ararya
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/ararya/sistine
8
+ Project-URL: Repository, https://github.com/ararya/sistine
9
+ Keywords: react,html,css,js,web,ui,framework,streamlit
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.10
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
17
+ Classifier: Topic :: Software Development :: User Interfaces
18
+ Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
19
+ Requires-Python: >=3.10
20
+ Description-Content-Type: text/markdown
21
+ Requires-Dist: starlette>=0.40.0
22
+ Requires-Dist: uvicorn>=0.30.0
23
+ Requires-Dist: streamlit>=1.28.0
24
+
25
+ # Sistine
26
+
27
+ <img width="1620" height="1620" alt="sistine-fibel" src="https://github.com/user-attachments/assets/84af899f-9fc2-418f-af78-f9408bc33d03" />
28
+
29
+ > React-like UI framework for Python, powered by **Streamlit**. Build incredibly flexible, custom-designed UIs with HTML/Tailwind, while leveraging Streamlit's native widgets and data caching.
30
+
31
+ ## Why Sistine?
32
+
33
+ Streamlit is fantastic for data apps, but often developers complain:
34
+ - Hard to customize CSS/Layouts or use Tailwind.
35
+ - Complex nested UIs are difficult to build.
36
+ - UI elements lack flexibility.
37
+
38
+ **Sistine fixes this!** You can write full custom HTML, inject Tailwind CSS, and separate your concerns using a React-like component approach, all while letting Streamlit handle the backend server, caching, and state management.
39
+
40
+ ## Installation
41
+
42
+ ```bash
43
+ pip install sistine
44
+ ```
45
+
46
+ ## Features
47
+
48
+ - **React-like Syntax (Chaining)**: Write clean components `el.div(cls="...")(children)`.
49
+ - **Tailwind CSS Built-in**: One command `app.use_tailwind()` to enable utility classes.
50
+ - **Native Streamlit Interoperability**: Mix `st.write()` or Streamlit buttons alongside Sistine components!
51
+ - **Auto-caching**: Use `@query` to fetch API/Database, and it automatically hooks into `st.cache_data`.
52
+ - **MVC Modularity**: Fully supports splitting code into Models, Views, and Controllers.
53
+
54
+ ## Quick Start
55
+
56
+ ```python
57
+ from sistine import streamlit as st, Sistine, el
58
+
59
+ app = Sistine(title="My App")
60
+ app.use_tailwind()
61
+
62
+ # 1. Modular React-like Component using Chaining Syntax
63
+ def Card(title: str, description: str):
64
+ return el.div(cls="bg-white p-6 rounded-xl shadow-md")(
65
+ el.h2(cls="text-2xl font-bold text-gray-800")(title),
66
+ el.p(cls="text-gray-600 mt-2")(description)
67
+ )
68
+
69
+ # 2. Define Routes Easily
70
+ @app.sistine("/")
71
+ def home():
72
+ # You can still use Streamlit native features!
73
+ st.toast("Welcome to Sistine!")
74
+
75
+ return str(
76
+ el.div(cls="min-h-screen bg-gray-50 p-10 flex flex-col items-center justify-center")(
77
+ el.h1(cls="text-4xl font-extrabold text-blue-600 mb-8")("Hello Sistine!"),
78
+ Card("Awesome Framework", "Return HTML/Tailwind seamlessly inside Streamlit.")
79
+ )
80
+ )
81
+
82
+ if __name__ == "__main__":
83
+ app.run(port=8080)
84
+ ```
85
+
86
+ Run your app normally:
87
+ ```bash
88
+ python app.py
89
+ ```
90
+
91
+ ## The Power of `@query`
92
+
93
+ Say goodbye to slow apps! Use `@query` for any API calls or expensive database queries. Sistine automatically caches it using `st.cache_data` in the background.
94
+
95
+ ```python
96
+ from sistine import query
97
+ import requests
98
+
99
+ @query
100
+ def fetch_users():
101
+ # Only hits the network once, subsequent calls are instantly cached!
102
+ res = requests.get("https://jsonplaceholder.typicode.com/users")
103
+ return res.json()
104
+ ```
105
+
106
+ ## Streamlit State & Widgets
107
+
108
+ Sistine runs *inside* Streamlit. You can use `st.session_state` to make your Sistine UI reactive!
109
+
110
+ ```python
111
+ from sistine import streamlit as st, Sistine, el
112
+
113
+ app = Sistine()
114
+
115
+ if "count" not in st.session_state:
116
+ st.session_state.count = 0
117
+
118
+ @app.sistine("/")
119
+ def counter():
120
+ # Native Streamlit Widget
121
+ if st.button("Add +1"):
122
+ st.session_state.count += 1
123
+
124
+ # Sistine UI that reacts to Streamlit state!
125
+ return str(
126
+ el.h1(cls="text-3xl")(f"Count is: {st.session_state.count}")
127
+ )
128
+ ```
129
+
130
+ ## Examples
131
+
132
+ Check out the [`examples/`](examples) directory for complete applications:
133
+ - `tailwind_app.py` - Basic Tailwind setup.
134
+ - `routing_app.py` - Multi-page routing with URL parameters (`/user/{id}`).
135
+ - `dashboard_app.py` - Complex UI layout (sidebar, grid cards).
136
+ - `pokeapi_app.py` - Working API integration showcasing `@query`.
137
+ - `state_app.py` - Interactivity using Streamlit's `session_state`.
138
+ - `mvc_app/` - Example of organizing a massive Sistine app using MVC pattern.
@@ -0,0 +1,114 @@
1
+ # Sistine
2
+
3
+ <img width="1620" height="1620" alt="sistine-fibel" src="https://github.com/user-attachments/assets/84af899f-9fc2-418f-af78-f9408bc33d03" />
4
+
5
+ > React-like UI framework for Python, powered by **Streamlit**. Build incredibly flexible, custom-designed UIs with HTML/Tailwind, while leveraging Streamlit's native widgets and data caching.
6
+
7
+ ## Why Sistine?
8
+
9
+ Streamlit is fantastic for data apps, but often developers complain:
10
+ - Hard to customize CSS/Layouts or use Tailwind.
11
+ - Complex nested UIs are difficult to build.
12
+ - UI elements lack flexibility.
13
+
14
+ **Sistine fixes this!** You can write full custom HTML, inject Tailwind CSS, and separate your concerns using a React-like component approach, all while letting Streamlit handle the backend server, caching, and state management.
15
+
16
+ ## Installation
17
+
18
+ ```bash
19
+ pip install sistine
20
+ ```
21
+
22
+ ## Features
23
+
24
+ - **React-like Syntax (Chaining)**: Write clean components `el.div(cls="...")(children)`.
25
+ - **Tailwind CSS Built-in**: One command `app.use_tailwind()` to enable utility classes.
26
+ - **Native Streamlit Interoperability**: Mix `st.write()` or Streamlit buttons alongside Sistine components!
27
+ - **Auto-caching**: Use `@query` to fetch API/Database, and it automatically hooks into `st.cache_data`.
28
+ - **MVC Modularity**: Fully supports splitting code into Models, Views, and Controllers.
29
+
30
+ ## Quick Start
31
+
32
+ ```python
33
+ from sistine import streamlit as st, Sistine, el
34
+
35
+ app = Sistine(title="My App")
36
+ app.use_tailwind()
37
+
38
+ # 1. Modular React-like Component using Chaining Syntax
39
+ def Card(title: str, description: str):
40
+ return el.div(cls="bg-white p-6 rounded-xl shadow-md")(
41
+ el.h2(cls="text-2xl font-bold text-gray-800")(title),
42
+ el.p(cls="text-gray-600 mt-2")(description)
43
+ )
44
+
45
+ # 2. Define Routes Easily
46
+ @app.sistine("/")
47
+ def home():
48
+ # You can still use Streamlit native features!
49
+ st.toast("Welcome to Sistine!")
50
+
51
+ return str(
52
+ el.div(cls="min-h-screen bg-gray-50 p-10 flex flex-col items-center justify-center")(
53
+ el.h1(cls="text-4xl font-extrabold text-blue-600 mb-8")("Hello Sistine!"),
54
+ Card("Awesome Framework", "Return HTML/Tailwind seamlessly inside Streamlit.")
55
+ )
56
+ )
57
+
58
+ if __name__ == "__main__":
59
+ app.run(port=8080)
60
+ ```
61
+
62
+ Run your app normally:
63
+ ```bash
64
+ python app.py
65
+ ```
66
+
67
+ ## The Power of `@query`
68
+
69
+ Say goodbye to slow apps! Use `@query` for any API calls or expensive database queries. Sistine automatically caches it using `st.cache_data` in the background.
70
+
71
+ ```python
72
+ from sistine import query
73
+ import requests
74
+
75
+ @query
76
+ def fetch_users():
77
+ # Only hits the network once, subsequent calls are instantly cached!
78
+ res = requests.get("https://jsonplaceholder.typicode.com/users")
79
+ return res.json()
80
+ ```
81
+
82
+ ## Streamlit State & Widgets
83
+
84
+ Sistine runs *inside* Streamlit. You can use `st.session_state` to make your Sistine UI reactive!
85
+
86
+ ```python
87
+ from sistine import streamlit as st, Sistine, el
88
+
89
+ app = Sistine()
90
+
91
+ if "count" not in st.session_state:
92
+ st.session_state.count = 0
93
+
94
+ @app.sistine("/")
95
+ def counter():
96
+ # Native Streamlit Widget
97
+ if st.button("Add +1"):
98
+ st.session_state.count += 1
99
+
100
+ # Sistine UI that reacts to Streamlit state!
101
+ return str(
102
+ el.h1(cls="text-3xl")(f"Count is: {st.session_state.count}")
103
+ )
104
+ ```
105
+
106
+ ## Examples
107
+
108
+ Check out the [`examples/`](examples) directory for complete applications:
109
+ - `tailwind_app.py` - Basic Tailwind setup.
110
+ - `routing_app.py` - Multi-page routing with URL parameters (`/user/{id}`).
111
+ - `dashboard_app.py` - Complex UI layout (sidebar, grid cards).
112
+ - `pokeapi_app.py` - Working API integration showcasing `@query`.
113
+ - `state_app.py` - Interactivity using Streamlit's `session_state`.
114
+ - `mvc_app/` - Example of organizing a massive Sistine app using MVC pattern.
@@ -0,0 +1,41 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "sistine"
7
+ version = "0.1.0"
8
+ description = "React-like framework for Python - return HTML/CSS/JS like React, route like Next.js"
9
+ authors = [
10
+ {name = "ararya"},
11
+ ]
12
+ license = "MIT"
13
+ readme = "README.md"
14
+ requires-python = ">=3.10"
15
+ keywords = ["react", "html", "css", "js", "web", "ui", "framework", "streamlit"]
16
+ classifiers = [
17
+ "Development Status :: 3 - Alpha",
18
+ "Intended Audience :: Developers",
19
+ "Programming Language :: Python :: 3",
20
+ "Programming Language :: Python :: 3.10",
21
+ "Programming Language :: Python :: 3.11",
22
+ "Programming Language :: Python :: 3.12",
23
+ "Topic :: Software Development :: Libraries :: Python Modules",
24
+ "Topic :: Software Development :: User Interfaces",
25
+ "Topic :: Internet :: WWW/HTTP :: Dynamic Content",
26
+ ]
27
+ dependencies = [
28
+ "starlette>=0.40.0",
29
+ "uvicorn>=0.30.0",
30
+ "streamlit>=1.28.0",
31
+ ]
32
+
33
+ [project.urls]
34
+ Homepage = "https://github.com/ararya/sistine"
35
+ Repository = "https://github.com/ararya/sistine"
36
+
37
+ [project.scripts]
38
+ sistine = "sistine.cli:main"
39
+
40
+ [tool.setuptools.packages.find]
41
+ where = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,7 @@
1
+ import streamlit
2
+
3
+ from .app import Sistine
4
+ from .el import el
5
+ from .query import query
6
+
7
+ __all__ = ["Sistine", "el", "query", "streamlit"]
@@ -0,0 +1,3 @@
1
+ from .cli import main
2
+
3
+ main()
@@ -0,0 +1,84 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Callable
4
+
5
+ from .server import StreamlitServer
6
+
7
+
8
+ _PAGE = """\
9
+ <!DOCTYPE html>
10
+ <html lang="en">
11
+ <head>
12
+ <meta charset="UTF-8">
13
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
14
+ <title>{title}</title>
15
+ <script src="https://unpkg.com/htmx.org@2.0.4"></script>
16
+ <style>
17
+ * {{ margin: 0; padding: 0; box-sizing: border-box; }}
18
+ body {{ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: #333; }}
19
+ {global_css}
20
+ </style>
21
+ {global_head}
22
+ {framework_meta}
23
+ </head>
24
+ <body hx-boost="true">{content}{framework_script}</body>
25
+ </html>"""
26
+
27
+
28
+ class Sistine:
29
+ def __init__(
30
+ self,
31
+ title: str = "Sistine App",
32
+ framework: Any = None,
33
+ ) -> None:
34
+ self._title = title
35
+ self._fw = framework
36
+ self._routes: dict[str, Callable[..., Any]] = {}
37
+ self._global_css: list[str] = []
38
+ self._global_head: list[str] = []
39
+ self._server = StreamlitServer(self)
40
+
41
+ if framework is not None:
42
+ fw_name = getattr(framework, "__name__", str(framework))
43
+ fw_ver = getattr(framework, "__version__", "?")
44
+ self._global_head.append(
45
+ f'<meta name="generator" content="Sistine + {fw_name} {fw_ver}">'
46
+ )
47
+
48
+ def css(self, code: str) -> None:
49
+ self._global_css.append(code)
50
+
51
+ def head(self, tag: str) -> None:
52
+ self._global_head.append(tag)
53
+
54
+ def use_tailwind(self) -> None:
55
+ self.head('<script src="https://cdn.tailwindcss.com"></script>')
56
+
57
+ def sistine(self, path: str) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
58
+ def decorator(fn: Callable[..., Any]) -> Callable[..., Any]:
59
+ self._routes[path] = fn
60
+ return fn
61
+ return decorator
62
+
63
+ def _render(self, html: str) -> str:
64
+ fw_meta = ""
65
+ fw_script = ""
66
+
67
+ if self._fw is not None:
68
+ fw_name = getattr(self._fw, "__name__", "framework")
69
+ fw_meta = f'<meta name="framework" content="{fw_name}">'
70
+ fw_script = (
71
+ f'<script>window.__SISTINE_FRAMEWORK__="{fw_name}";</script>'
72
+ )
73
+
74
+ return _PAGE.format(
75
+ title=self._title,
76
+ content=html,
77
+ global_css="\n".join(self._global_css),
78
+ global_head="\n".join(self._global_head),
79
+ framework_meta=fw_meta,
80
+ framework_script=fw_script,
81
+ )
82
+
83
+ def run(self, host: str = "127.0.0.1", port: int = 8080) -> None:
84
+ self._server.run(host, port)
@@ -0,0 +1,151 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import os
5
+ import runpy
6
+ import sys
7
+ from pathlib import Path
8
+
9
+
10
+ def cmd_init(args: argparse.Namespace) -> None:
11
+ project_name = args.name
12
+ base_dir = Path(project_name)
13
+
14
+ if (base_dir / "main.py").exists() or (base_dir / "controllers").exists():
15
+ print(f"Error: A Sistine project already seems to exist in '{project_name}'.")
16
+ sys.exit(1)
17
+
18
+ base_dir.mkdir(parents=True, exist_ok=True)
19
+ (base_dir / "models").mkdir()
20
+ (base_dir / "views").mkdir()
21
+ (base_dir / "controllers").mkdir()
22
+
23
+ # Generate main.py
24
+ (base_dir / "main.py").write_text(
25
+ "from sistine import Sistine\n"
26
+ "from controllers.home import register_routes\n\n"
27
+ "app = Sistine(title=\"My Sistine App\")\n"
28
+ "app.use_tailwind()\n\n"
29
+ "register_routes(app)\n\n"
30
+ "if __name__ == \"__main__\":\n"
31
+ " app.run()\n",
32
+ encoding="utf-8"
33
+ )
34
+
35
+ # Generate models
36
+ (base_dir / "models" / "__init__.py").touch()
37
+ (base_dir / "models" / "data.py").write_text(
38
+ "from sistine import query\n\n"
39
+ "@query\n"
40
+ "def get_welcome_message():\n"
41
+ " return \"Welcome to Sistine Layered Architecture!\"\n",
42
+ encoding="utf-8"
43
+ )
44
+
45
+ # Generate views
46
+ (base_dir / "views" / "__init__.py").touch()
47
+ (base_dir / "views" / "pages.py").write_text(
48
+ "from sistine import el\n\n"
49
+ "def HomePage(message: str):\n"
50
+ " return el.div(cls=\"min-h-screen bg-gray-50 flex items-center justify-center p-4\")(\n"
51
+ " el.div(cls=\"bg-white p-10 rounded-2xl shadow-xl text-center\")(\n"
52
+ " el.h1(cls=\"text-4xl font-extrabold text-blue-600 mb-4\")(\"Hello Sistine\"),\n"
53
+ " el.p(cls=\"text-gray-700 text-lg\")(message)\n"
54
+ " )\n"
55
+ " )\n",
56
+ encoding="utf-8"
57
+ )
58
+
59
+ # Generate controllers
60
+ (base_dir / "controllers" / "__init__.py").touch()
61
+ (base_dir / "controllers" / "home.py").write_text(
62
+ "from models.data import get_welcome_message\n"
63
+ "from views.pages import HomePage\n\n"
64
+ "def register_routes(app):\n"
65
+ " @app.sistine(\"/\")\n"
66
+ " def index():\n"
67
+ " msg = get_welcome_message()\n"
68
+ " return str(HomePage(msg))\n",
69
+ encoding="utf-8"
70
+ )
71
+
72
+ print(f"Successfully initialized Sistine project in '{project_name}'")
73
+ print("Run your app with:")
74
+ if project_name != ".":
75
+ print(f" cd {project_name}")
76
+ print(" sistine run")
77
+
78
+
79
+ def cmd_run(args: argparse.Namespace) -> None:
80
+ script = args.script
81
+ if not os.path.exists(script):
82
+ print(f"Error: {script} not found")
83
+ sys.exit(1)
84
+
85
+ import subprocess
86
+ sys.exit(subprocess.call([sys.executable, "-m", "streamlit", "run", script]))
87
+
88
+
89
+ def cmd_build(args: argparse.Namespace) -> None:
90
+ script = args.script
91
+ if not os.path.exists(script):
92
+ print(f"Error: {script} not found")
93
+ sys.exit(1)
94
+
95
+ out = args.out or "dist"
96
+
97
+ sys.argv[1:] = []
98
+ mod = runpy.run_path(script, run_name="__sistine_build__")
99
+
100
+ found = None
101
+ for name, val in mod.items():
102
+ if type(val).__name__ == "Sistine":
103
+ found = val
104
+ break
105
+
106
+ if found is None:
107
+ print("Error: no Sistine app instance found in", script)
108
+ sys.exit(1)
109
+
110
+ out_dir = Path(out)
111
+ out_dir.mkdir(parents=True, exist_ok=True)
112
+
113
+ built = 0
114
+ for route in found._routes:
115
+ path = route.strip("/") or "index"
116
+ html = found._serve(route)
117
+ filepath = out_dir / f"{path}.html"
118
+ filepath.parent.mkdir(parents=True, exist_ok=True)
119
+ filepath.write_text(html, encoding="utf-8")
120
+ print(f" Built {filepath}")
121
+ built += 1
122
+
123
+ print(f"\nBuilt {built} page(s) → {out_dir}/")
124
+
125
+
126
+ def main() -> None:
127
+ parser = argparse.ArgumentParser(prog="sistine", description="Sistine CLI")
128
+ sub = parser.add_subparsers(dest="command", required=True)
129
+
130
+ init_p = sub.add_parser("init", help="Initialize a new Sistine project with layered architecture")
131
+ init_p.add_argument("name", nargs="?", default=".", help="Project directory name (default: current directory)")
132
+
133
+ run_p = sub.add_parser("run", help="Run a Sistine app")
134
+ run_p.add_argument("script", nargs="?", default="main.py", help="Path to Python script (default: main.py)")
135
+
136
+ build_p = sub.add_parser("build", help="Build static HTML from a Sistine app")
137
+ build_p.add_argument("script", nargs="?", default="main.py", help="Path to Python script (default: main.py)")
138
+ build_p.add_argument("--out", default="dist", help="Output directory (default: dist)")
139
+
140
+ args = parser.parse_args()
141
+
142
+ if args.command == "init":
143
+ cmd_init(args)
144
+ elif args.command == "run":
145
+ cmd_run(args)
146
+ elif args.command == "build":
147
+ cmd_build(args)
148
+
149
+
150
+ if __name__ == "__main__":
151
+ main()
@@ -0,0 +1,128 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+
6
+ _SELF_CLOSING = {
7
+ "area", "base", "br", "col", "embed", "hr", "img", "input",
8
+ "link", "meta", "param", "source", "track", "wbr",
9
+ }
10
+
11
+
12
+ class _HTML:
13
+ __slots__ = ("_html", "_tag", "_attrs", "_children")
14
+
15
+ def __init__(
16
+ self,
17
+ html: str,
18
+ tag: str | None = None,
19
+ attrs: dict[str, Any] | None = None,
20
+ children: tuple[Any, ...] | None = None,
21
+ ) -> None:
22
+ self._html = html
23
+ self._tag = tag
24
+ self._attrs = attrs or {}
25
+ self._children = children or ()
26
+
27
+ def __str__(self) -> str:
28
+ return self._html
29
+
30
+ def __repr__(self) -> str:
31
+ return self._html
32
+
33
+ def __add__(self, other: Any) -> str:
34
+ return str(self) + str(other)
35
+
36
+ def __radd__(self, other: Any) -> str:
37
+ return str(other) + str(self)
38
+
39
+ def __call__(self, *children: Any, **attrs: Any) -> _HTML:
40
+ if self._tag is None:
41
+ raise TypeError("Cannot chain call on raw HTML")
42
+
43
+ new_attrs = self._attrs.copy()
44
+ new_attrs.update(attrs)
45
+ new_children = self._children + children
46
+
47
+ return _build_tag(self._tag, *new_children, **new_attrs)
48
+
49
+
50
+ def _attr_name(key: str) -> str:
51
+ if key == "cls" or key == "className" or key == "class_name":
52
+ return "class"
53
+ if key == "html_for":
54
+ return "for"
55
+ result: list[str] = []
56
+ for char in key:
57
+ if char.isupper():
58
+ result.append("-")
59
+ result.append(char.lower())
60
+ elif char == "_":
61
+ result.append("-")
62
+ else:
63
+ result.append(char)
64
+ return "".join(result).lstrip("-")
65
+
66
+
67
+ def _style_str(style: dict[str, str]) -> str:
68
+ return "; ".join(f"{_attr_name(k)}: {v}" for k, v in style.items())
69
+
70
+
71
+ def _escape(text: str) -> str:
72
+ return (
73
+ text.replace("&", "&amp;")
74
+ .replace("<", "&lt;")
75
+ .replace(">", "&gt;")
76
+ .replace('"', "&quot;")
77
+ )
78
+
79
+
80
+ def _build_tag(tag: str, *children: Any, **attrs: Any) -> _HTML:
81
+ parts: list[str] = [f"<{tag}"]
82
+
83
+ style = attrs.pop("style", None)
84
+ if style:
85
+ attrs["style"] = _style_str(style)
86
+
87
+ for key, value in attrs.items():
88
+ attr = _attr_name(key)
89
+ if value is None:
90
+ continue
91
+ if isinstance(value, bool):
92
+ if value:
93
+ parts.append(f' {attr}')
94
+ elif isinstance(value, (int, float)):
95
+ parts.append(f' {attr}="{value}"')
96
+ else:
97
+ parts.append(f' {attr}="{_escape(str(value))}"')
98
+
99
+ parts.append(">")
100
+
101
+ if tag in _SELF_CLOSING:
102
+ return _HTML("".join(parts), tag=tag, attrs=attrs, children=children)
103
+
104
+ for child in children:
105
+ if isinstance(child, _HTML):
106
+ parts.append(child._html)
107
+ elif isinstance(child, str):
108
+ parts.append(_escape(child))
109
+ elif isinstance(child, (int, float)):
110
+ parts.append(str(child))
111
+ elif child is None:
112
+ continue
113
+ else:
114
+ parts.append(str(child))
115
+
116
+ parts.append(f"</{tag}>")
117
+ return _HTML("".join(parts), tag=tag, attrs=attrs, children=children)
118
+
119
+
120
+ class _ElementBuilder:
121
+ def __getattr__(self, tag: str) -> Any:
122
+ def builder(*children: Any, **attrs: Any) -> _HTML:
123
+ return _build_tag(tag, *children, **attrs)
124
+
125
+ return builder
126
+
127
+
128
+ el = _ElementBuilder()
@@ -0,0 +1,36 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Callable, TypeVar, overload
4
+
5
+ try:
6
+ import streamlit as st
7
+ except ImportError:
8
+ st = None
9
+
10
+ F = TypeVar('F', bound=Callable[..., Any])
11
+
12
+ class Query:
13
+ def __init__(self, fn: Callable[..., Any]) -> None:
14
+ if st is not None:
15
+ # Wrap with Streamlit's cache_data for automatic API/data caching!
16
+ self.fn = st.cache_data(fn)
17
+ else:
18
+ self.fn = fn
19
+
20
+ self.__doc__ = fn.__doc__
21
+ self.__name__ = fn.__name__
22
+ self.__qualname__ = fn.__qualname__
23
+
24
+ def __call__(self, *args: Any, **kwargs: Any) -> Any:
25
+ return self.fn(*args, **kwargs)
26
+
27
+ @overload
28
+ def query(fn: F) -> F: ...
29
+
30
+ @overload
31
+ def query() -> Callable[[F], F]: ...
32
+
33
+ def query(fn: Any = None) -> Any:
34
+ if fn is None:
35
+ return lambda f: Query(f) # type: ignore
36
+ return Query(fn)
@@ -0,0 +1,124 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import sys
5
+ from typing import TYPE_CHECKING, Any, Callable
6
+
7
+ try:
8
+ import streamlit as st
9
+ import streamlit.components.v1 as components
10
+ except ImportError:
11
+ st = None
12
+ components = None
13
+
14
+ if TYPE_CHECKING:
15
+ from .app import Sistine
16
+
17
+
18
+ def is_running_in_streamlit() -> bool:
19
+ if st is None:
20
+ return False
21
+ try:
22
+ from streamlit.runtime.scriptrunner import get_script_run_ctx
23
+
24
+ return get_script_run_ctx() is not None
25
+ except Exception:
26
+ return False
27
+
28
+
29
+ class StreamlitServer:
30
+ def __init__(self, app: Sistine) -> None:
31
+ self._app = app
32
+
33
+ def _render_page(self, route_path: str, html_str: str) -> None:
34
+ if st is None:
35
+ return
36
+
37
+ # Hide default Streamlit paddings/styles to make it look like a custom app
38
+ st.markdown(
39
+ """
40
+ <style>
41
+ .block-container {
42
+ padding-top: 0rem;
43
+ padding-bottom: 0rem;
44
+ padding-left: 0rem;
45
+ padding-right: 0rem;
46
+ max-width: 100%;
47
+ }
48
+ header {visibility: hidden;}
49
+ #MainMenu {visibility: hidden;}
50
+ footer {visibility: hidden;}
51
+ </style>
52
+ """,
53
+ unsafe_allow_html=True,
54
+ )
55
+
56
+ # Render the HTML using components.html for isolation
57
+ # Height is large to simulate a full page, scrolling handles internal scroll
58
+ assert components is not None
59
+ components.html(html_str, height=1200, scrolling=True)
60
+
61
+ def run(self, host: str = "127.0.0.1", port: int = 8080) -> None:
62
+ if not is_running_in_streamlit():
63
+ # Not in Streamlit, restart using streamlit run
64
+ script = sys.argv[0]
65
+ print(
66
+ f"\n \033[1mSistine v0.1.0\033[0m — Starting Streamlit backend on http://{host}:{port}...\n"
67
+ )
68
+
69
+ import subprocess
70
+
71
+ cmd = [
72
+ sys.executable,
73
+ "-m",
74
+ "streamlit",
75
+ "run",
76
+ script,
77
+ "--server.address",
78
+ host,
79
+ "--server.port",
80
+ str(port),
81
+ ]
82
+ sys.exit(subprocess.call(cmd))
83
+
84
+ # We ARE in Streamlit.
85
+ assert st is not None
86
+ import inspect
87
+ import re
88
+
89
+ query_params = st.query_params
90
+ route = query_params.get("route", "/")
91
+
92
+ matched_fn = None
93
+ matched_kwargs = {}
94
+
95
+ for pattern, fn in self._app._routes.items():
96
+ # Convert `{id}` to `(?P<id>[^/]+)`
97
+ regex_pattern = re.sub(r"\{([^/]+)\}", r"(?P<\1>[^/]+)", pattern)
98
+ regex_pattern = f"^{regex_pattern}$"
99
+
100
+ match = re.match(regex_pattern, route)
101
+ if match:
102
+ matched_fn = fn
103
+ matched_kwargs = match.groupdict()
104
+ break
105
+
106
+ if matched_fn:
107
+ try:
108
+ sig = inspect.signature(matched_fn)
109
+ for k, v in matched_kwargs.items():
110
+ if k in sig.parameters:
111
+ anno = sig.parameters[k].annotation
112
+ if anno is int:
113
+ matched_kwargs[k] = int(v)
114
+ elif anno is float:
115
+ matched_kwargs[k] = float(v)
116
+
117
+ content = matched_fn(**matched_kwargs)
118
+ except Exception as e:
119
+ content = f"<h1>500</h1><p>{e}</p>"
120
+ else:
121
+ content = "<h1>404</h1><p>Page not found</p>"
122
+
123
+ html = self._app._render(str(content))
124
+ self._render_page(route, html)
@@ -0,0 +1,138 @@
1
+ Metadata-Version: 2.4
2
+ Name: sistine
3
+ Version: 0.1.0
4
+ Summary: React-like framework for Python - return HTML/CSS/JS like React, route like Next.js
5
+ Author: ararya
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/ararya/sistine
8
+ Project-URL: Repository, https://github.com/ararya/sistine
9
+ Keywords: react,html,css,js,web,ui,framework,streamlit
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.10
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
17
+ Classifier: Topic :: Software Development :: User Interfaces
18
+ Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
19
+ Requires-Python: >=3.10
20
+ Description-Content-Type: text/markdown
21
+ Requires-Dist: starlette>=0.40.0
22
+ Requires-Dist: uvicorn>=0.30.0
23
+ Requires-Dist: streamlit>=1.28.0
24
+
25
+ # Sistine
26
+
27
+ <img width="1620" height="1620" alt="sistine-fibel" src="https://github.com/user-attachments/assets/84af899f-9fc2-418f-af78-f9408bc33d03" />
28
+
29
+ > React-like UI framework for Python, powered by **Streamlit**. Build incredibly flexible, custom-designed UIs with HTML/Tailwind, while leveraging Streamlit's native widgets and data caching.
30
+
31
+ ## Why Sistine?
32
+
33
+ Streamlit is fantastic for data apps, but often developers complain:
34
+ - Hard to customize CSS/Layouts or use Tailwind.
35
+ - Complex nested UIs are difficult to build.
36
+ - UI elements lack flexibility.
37
+
38
+ **Sistine fixes this!** You can write full custom HTML, inject Tailwind CSS, and separate your concerns using a React-like component approach, all while letting Streamlit handle the backend server, caching, and state management.
39
+
40
+ ## Installation
41
+
42
+ ```bash
43
+ pip install sistine
44
+ ```
45
+
46
+ ## Features
47
+
48
+ - **React-like Syntax (Chaining)**: Write clean components `el.div(cls="...")(children)`.
49
+ - **Tailwind CSS Built-in**: One command `app.use_tailwind()` to enable utility classes.
50
+ - **Native Streamlit Interoperability**: Mix `st.write()` or Streamlit buttons alongside Sistine components!
51
+ - **Auto-caching**: Use `@query` to fetch API/Database, and it automatically hooks into `st.cache_data`.
52
+ - **MVC Modularity**: Fully supports splitting code into Models, Views, and Controllers.
53
+
54
+ ## Quick Start
55
+
56
+ ```python
57
+ from sistine import streamlit as st, Sistine, el
58
+
59
+ app = Sistine(title="My App")
60
+ app.use_tailwind()
61
+
62
+ # 1. Modular React-like Component using Chaining Syntax
63
+ def Card(title: str, description: str):
64
+ return el.div(cls="bg-white p-6 rounded-xl shadow-md")(
65
+ el.h2(cls="text-2xl font-bold text-gray-800")(title),
66
+ el.p(cls="text-gray-600 mt-2")(description)
67
+ )
68
+
69
+ # 2. Define Routes Easily
70
+ @app.sistine("/")
71
+ def home():
72
+ # You can still use Streamlit native features!
73
+ st.toast("Welcome to Sistine!")
74
+
75
+ return str(
76
+ el.div(cls="min-h-screen bg-gray-50 p-10 flex flex-col items-center justify-center")(
77
+ el.h1(cls="text-4xl font-extrabold text-blue-600 mb-8")("Hello Sistine!"),
78
+ Card("Awesome Framework", "Return HTML/Tailwind seamlessly inside Streamlit.")
79
+ )
80
+ )
81
+
82
+ if __name__ == "__main__":
83
+ app.run(port=8080)
84
+ ```
85
+
86
+ Run your app normally:
87
+ ```bash
88
+ python app.py
89
+ ```
90
+
91
+ ## The Power of `@query`
92
+
93
+ Say goodbye to slow apps! Use `@query` for any API calls or expensive database queries. Sistine automatically caches it using `st.cache_data` in the background.
94
+
95
+ ```python
96
+ from sistine import query
97
+ import requests
98
+
99
+ @query
100
+ def fetch_users():
101
+ # Only hits the network once, subsequent calls are instantly cached!
102
+ res = requests.get("https://jsonplaceholder.typicode.com/users")
103
+ return res.json()
104
+ ```
105
+
106
+ ## Streamlit State & Widgets
107
+
108
+ Sistine runs *inside* Streamlit. You can use `st.session_state` to make your Sistine UI reactive!
109
+
110
+ ```python
111
+ from sistine import streamlit as st, Sistine, el
112
+
113
+ app = Sistine()
114
+
115
+ if "count" not in st.session_state:
116
+ st.session_state.count = 0
117
+
118
+ @app.sistine("/")
119
+ def counter():
120
+ # Native Streamlit Widget
121
+ if st.button("Add +1"):
122
+ st.session_state.count += 1
123
+
124
+ # Sistine UI that reacts to Streamlit state!
125
+ return str(
126
+ el.h1(cls="text-3xl")(f"Count is: {st.session_state.count}")
127
+ )
128
+ ```
129
+
130
+ ## Examples
131
+
132
+ Check out the [`examples/`](examples) directory for complete applications:
133
+ - `tailwind_app.py` - Basic Tailwind setup.
134
+ - `routing_app.py` - Multi-page routing with URL parameters (`/user/{id}`).
135
+ - `dashboard_app.py` - Complex UI layout (sidebar, grid cards).
136
+ - `pokeapi_app.py` - Working API integration showcasing `@query`.
137
+ - `state_app.py` - Interactivity using Streamlit's `session_state`.
138
+ - `mvc_app/` - Example of organizing a massive Sistine app using MVC pattern.
@@ -0,0 +1,15 @@
1
+ README.md
2
+ pyproject.toml
3
+ src/sistine/__init__.py
4
+ src/sistine/__main__.py
5
+ src/sistine/app.py
6
+ src/sistine/cli.py
7
+ src/sistine/el.py
8
+ src/sistine/query.py
9
+ src/sistine/server.py
10
+ src/sistine.egg-info/PKG-INFO
11
+ src/sistine.egg-info/SOURCES.txt
12
+ src/sistine.egg-info/dependency_links.txt
13
+ src/sistine.egg-info/entry_points.txt
14
+ src/sistine.egg-info/requires.txt
15
+ src/sistine.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ sistine = sistine.cli:main
@@ -0,0 +1,3 @@
1
+ starlette>=0.40.0
2
+ uvicorn>=0.30.0
3
+ streamlit>=1.28.0
@@ -0,0 +1 @@
1
+ sistine