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 +138 -0
- sistine-0.1.0/README.md +114 -0
- sistine-0.1.0/pyproject.toml +41 -0
- sistine-0.1.0/setup.cfg +4 -0
- sistine-0.1.0/src/sistine/__init__.py +7 -0
- sistine-0.1.0/src/sistine/__main__.py +3 -0
- sistine-0.1.0/src/sistine/app.py +84 -0
- sistine-0.1.0/src/sistine/cli.py +151 -0
- sistine-0.1.0/src/sistine/el.py +128 -0
- sistine-0.1.0/src/sistine/query.py +36 -0
- sistine-0.1.0/src/sistine/server.py +124 -0
- sistine-0.1.0/src/sistine.egg-info/PKG-INFO +138 -0
- sistine-0.1.0/src/sistine.egg-info/SOURCES.txt +15 -0
- sistine-0.1.0/src/sistine.egg-info/dependency_links.txt +1 -0
- sistine-0.1.0/src/sistine.egg-info/entry_points.txt +2 -0
- sistine-0.1.0/src/sistine.egg-info/requires.txt +3 -0
- sistine-0.1.0/src/sistine.egg-info/top_level.txt +1 -0
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.
|
sistine-0.1.0/README.md
ADDED
|
@@ -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"]
|
sistine-0.1.0/setup.cfg
ADDED
|
@@ -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("&", "&")
|
|
74
|
+
.replace("<", "<")
|
|
75
|
+
.replace(">", ">")
|
|
76
|
+
.replace('"', """)
|
|
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 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
sistine
|