Skip to content

Commit d83408b

Browse files
Initial commit
0 parents  commit d83408b

13 files changed

+606
-0
lines changed

LICENSE

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
The MIT License (MIT)
2+
3+
Copyright (c) 2020 hebi@python-ninja.com
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

+109
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
# fastTEA: Build Elegant Web Applications in Python
2+
3+
fastTEA is a powerful and intuitive Python framework for building web applications with ease.
4+
Inspired by The Elm Architecture (TEA), fastTEA brings simplicity and predictability to your Python web development workflow.
5+
6+
![](./images/fasttea_small.png)
7+
8+
## Features
9+
10+
- **Simple and Intuitive**: Build web apps using a clear, archtectural approach.
11+
- **Based on The Elm Architecture**: Benefit from a time-tested, scalable architecture.
12+
- **FastAPI Backend**: Leverage the speed and simplicity of FastAPI.
13+
- **HTMX Integration**: Create dynamic UIs without writing JavaScript.
14+
- **Flexible CSS Framework Support**: Choose from Pico, Bootstrap, or Tailwind CSS.
15+
- **Type Safety**: Utilizes Pydantic for robust data validation.
16+
17+
## Installation
18+
19+
Install fastTEA using pip:
20+
21+
```bash
22+
pip install fasttea
23+
```
24+
25+
## Quick Start
26+
27+
Let's dive into a simple "Hello, World!" application to showcase the power and simplicity of fastTEA.
28+
29+
```python
30+
from fasttea import FastTEA, Model, Msg, Cmd, Element, CSSFramework
31+
from fasttea.html import div, h1, input_, button, p
32+
33+
class AppModel(Model):
34+
name: str = ""
35+
greeting: str = ""
36+
37+
app = FastTEA(AppModel(), css_framework=CSSFramework.PICO)
38+
39+
@app.update
40+
def update(msg: Msg, model: AppModel) -> tuple[AppModel, Cmd | None]:
41+
if msg.action == "update_name":
42+
model.name = msg.payload["name"]
43+
elif msg.action == "greet":
44+
model.greeting = f"Hello {model.name}!"
45+
return model, None
46+
47+
@app.view
48+
def view(model: AppModel) -> Element:
49+
return div({},
50+
h1({}, "FastTEA Hello Example"),
51+
input_({
52+
"id": "input",
53+
"type": "text",
54+
"value": model.name,
55+
"name": "name",
56+
"placeholder": "Enter your name"
57+
}, ""),
58+
button({
59+
"onClick": "greet",
60+
"getValue": "input"
61+
},"Greet"),
62+
p ({}, model.greeting)
63+
)
64+
65+
if __name__ == "__main__":
66+
app.run()
67+
```
68+
69+
## Core Principles Explained
70+
71+
1. **Model**: The `AppModel` class defines the application's state. In this example, it stores the user's name and the greeting message.
72+
73+
2. **Update**: The `update` function handles state changes based on messages. It takes the current model and a message, then returns the updated model and any commands to be executed.
74+
75+
3. **View**: The `view` function renders the UI based on the current model state. It returns a tree of `Element` objects that fastTEA converts to HTML.
76+
77+
4. **HTMX Integration**: fastTEA leverages HTMX for dynamic updates without writing JavaScript. The button in our example uses HTMX attributes to trigger a server request.
78+
79+
5. **CSS Framework**: fastTEA supports various CSS frameworks. In this example, we're using Pico CSS for a clean, minimal design.
80+
81+
![](./images/tea.png)
82+
83+
## Get Started Today!
84+
85+
fastTEA combines the best of Python, The Elm Architecture, and modern web technologies to provide a delightful development experience. Whether you're building a small prototype or a large-scale web application, fastTEA has you covered.
86+
87+
Start building your next web application with fastTEA and experience the joy of functional web development in Python!
88+
89+
### Coming Soon!
90+
91+
We're constantly working to improve fastTEA and add new features. Here's a sneak peek at what's coming:
92+
93+
1. **More Examples**: We're developing a variety of examples to showcase fastTEA's capabilities in different scenarios.
94+
95+
2. **Simple Chatbot**: A demonstration of how to implement a basic chatbot using fastTEA, showing off its real-time update capabilities.
96+
97+
3. **Form Processing**: Enhanced support for handling and processing form submissions, making it even easier to create data-entry applications.
98+
99+
4. **Client-Side Commands**: Introducing a way to define commands that can be executed locally in the browser, improving responsiveness and reducing server load for certain operations.
100+
101+
5. **Server-Triggered Commands**: Allowing the server to trigger these client-side commands, enabling more complex interactions between the server and client.
102+
103+
6. **UiBubbles and CmdBubbles**: More structure.
104+
105+
These upcoming features will make fastTEA even more powerful and flexible, opening up new possibilities for your web applications. Stay tuned for updates!
106+
107+
For more examples, documentation, and community support, visit our [GitHub repository](https://github.com/yourusername/fasttea).
108+
109+
Happy coding with fastTEA! 🍵✨

fasttea/__init__.py

+266
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,266 @@
1+
from fastapi import FastAPI , Request
2+
from fastapi.responses import HTMLResponse
3+
from pydantic import BaseModel
4+
from typing import Callable, Dict, Any, List, Union
5+
from enum import Enum
6+
import os
7+
import toml
8+
from rich import print
9+
10+
class CSSFramework(Enum):
11+
NONE = 0
12+
PICO = 1
13+
BOOTSTRAP = 2
14+
TAILWIND = 3 #Tailwind CSS as an option
15+
16+
class Model(BaseModel):
17+
"""Base class for the application state"""
18+
pass
19+
20+
class Msg(BaseModel):
21+
"""Base class for messages"""
22+
action: str
23+
value: Any = None
24+
25+
class Cmd(BaseModel):
26+
"""Base class for commands"""
27+
action: str
28+
payload: Dict[str, Any] = {}
29+
30+
class Element:
31+
_id:int = 0
32+
33+
def __init__(self, tag: str,
34+
attributes: Dict[str, Any],
35+
children: Union[List['Element'], 'Element', str]):
36+
self.tag = tag
37+
self.attributes = attributes
38+
self.children = children if isinstance(children, list) else [children]
39+
40+
def to_htmx(self) -> str:
41+
self.add_htmx_attributes()
42+
attrs = ' '.join(f"{k}='{v}'" for k, v in self.attributes.items() if v is not None)
43+
children_html = ''.join(child.to_htmx() if isinstance(child, Element) else str(child) for child in self.children)
44+
return f"<{self.tag} {attrs}>{children_html}</{self.tag}>"
45+
46+
def add_htmx_attributes(self):
47+
"""Add HTMX attributes to elements with onClick or onChange handlers"""
48+
if 'onClick' in self.attributes:
49+
action = self.attributes['onClick']
50+
self.attributes.pop('onClick')
51+
52+
if 'getValue' in self.attributes:
53+
id = self.attributes['getValue']
54+
self.attributes.pop('getValue')
55+
self.attributes["hx-vals"] = f'js:{{"action": "{action}","value": document.getElementById("{id}").value}}'
56+
else:
57+
self.attributes["hx-vals"] = f'{{"action": "{action}"}}'
58+
59+
self.attributes.update({
60+
"hx-post": "/update",
61+
"hx-trigger" : "click",
62+
"hx-target": "#app",
63+
"hx-swap": "innerHTML"
64+
})
65+
elif 'onChange' in self.attributes:
66+
action = self.attributes['onChange']
67+
self.attributes.pop('onChange')
68+
69+
if 'id' not in self.attributes:
70+
self.attributes['id'] = self.create_id()
71+
72+
id = self.attributes['id']
73+
74+
trigger = "change"
75+
if 'type' in self.attributes:
76+
if self.attributes['type'] == "text":
77+
trigger = "keyup changed delay:500ms"
78+
79+
self.attributes.update({
80+
"hx-post": "/update",
81+
"hx-trigger": trigger,
82+
"hx-vals": f'js:{{"action": "{action}","value": document.getElementById("{id}").value}}',
83+
"hx-target": "#app",
84+
"hx-swap": "innerHTML"
85+
})
86+
87+
def create_id(self)->str:
88+
value = f'id{Element._id}'
89+
Element._id += 1
90+
return value
91+
92+
class UIBubble:
93+
def __init__(self, css_framework: CSSFramework):
94+
self.css_framework = css_framework
95+
96+
def render(self) -> Element:
97+
raise NotImplementedError("Subclasses must implement this method")
98+
99+
class CmdBubble:
100+
def __init__(self, name):
101+
self.name = name
102+
self.handlers = {}
103+
self.init_js = ""
104+
105+
def cmd(self, action):
106+
def decorator(f):
107+
self.handlers[action] = f.__name__
108+
return f
109+
return decorator
110+
111+
def init(self, f):
112+
self.init_js = f()
113+
return f
114+
115+
class FastTEA:
116+
def __init__(self, initial_model: Model,
117+
css_framework: CSSFramework = CSSFramework.NONE,
118+
js_libraries: List[str] = [],
119+
debug=False):
120+
self.app = FastAPI()
121+
self.model = initial_model
122+
self.update_fn: Callable[[Msg, Model], tuple[Model, Union[Cmd, None]]] = lambda msg, model: (model, None)
123+
self.view_fn: Callable[[Model], Element] = lambda model: Element("div", [], [])
124+
self.css_framework = css_framework
125+
self.js_libraries = js_libraries
126+
self.cmd_bubbles: List[CmdBubble] = []
127+
self.cmd_handlers: Dict[str, Callable] = {} #dictionary to store command handlers
128+
self.debug = debug
129+
130+
file_path = './.fasttea/security.toml'
131+
self.security = {}
132+
133+
if os.path.exists(file_path):
134+
try:
135+
with open(file_path, 'r') as file:
136+
security_data = toml.load(file)
137+
self.security.update(security_data)
138+
except Exception as e:
139+
print(f"Reading file: {e}")
140+
141+
@self.app.get("/", response_class=HTMLResponse)
142+
async def root():
143+
if self.debug: print('fastTEA root')
144+
css_link = self._get_css_link()
145+
js_links = self._get_js_links()
146+
value = f"""
147+
<html>
148+
<head>
149+
<script src="https://unpkg.com/htmx.org@2.0.2"></script>
150+
{css_link}
151+
{js_links}
152+
<title>fastTEA Application</title>
153+
</head>
154+
<body>
155+
<main class="container">
156+
<div id="app" hx-get="/init" hx-trigger="load"></div>
157+
</main>
158+
<script>
159+
{self.init_js}
160+
const app = {{
161+
executeCmd(cmd) {{
162+
if (cmd.action in this.cmdHandlers) {{
163+
this.cmdHandlers[cmd.action](cmd.payload);
164+
}} else {{
165+
console.error(`No handler for command: ${{cmd.action}}`);
166+
}}
167+
}},
168+
cmdHandlers: {{}}
169+
}};
170+
{self.cmd_handlers_js}
171+
document.body.addEventListener('htmx:afterOnLoad', function(event) {{
172+
const cmdData = event.detail.xhr.getResponseHeader('HX-Trigger');
173+
if (cmdData) {{
174+
const cmd = JSON.parse(cmdData);
175+
app.executeCmd(cmd);
176+
}}
177+
}});
178+
</script>
179+
{self._get_js_link()}
180+
</body>
181+
</html>
182+
"""
183+
if self.debug:
184+
print(f'FastTEA root {value}')
185+
return value
186+
187+
@self.app.get("/init")
188+
async def init():
189+
view_element = self.view_fn(self.model)
190+
return HTMLResponse(view_element.to_htmx())
191+
192+
@self.app.post("/update")
193+
async def update(request: Request):
194+
form_data = await request.form()
195+
action = form_data.get("action")
196+
value = form_data.get("value")
197+
print(f'value {value}')
198+
msg = Msg(action=action, value=value)
199+
new_model, cmd = self.update_fn(msg, self.model)
200+
self.model = new_model
201+
view_element = self.view_fn(self.model)
202+
response = HTMLResponse(view_element.to_htmx())
203+
if cmd:
204+
response.headers["HX-Trigger"] = cmd.json()
205+
return response
206+
207+
def cmd_bubble(self, name):
208+
bubble = CmdBubble(name)
209+
self.cmd_bubbles.append(bubble)
210+
return bubble
211+
212+
@property
213+
def cmd_handlers_js(self):
214+
handlers = {}
215+
for bubble in self.cmd_bubbles:
216+
handlers.update(bubble.handlers)
217+
handlers.update({k: v.__name__ for k, v in self.cmd_handlers.items()}) # Include new command handlers
218+
return "app.cmdHandlers = {" + ",".join(f"'{k}': {v}" for k, v in handlers.items()) + "};"
219+
220+
@property
221+
def init_js(self):
222+
bubble_init_js = "\n".join(bubble.init_js for bubble in self.cmd_bubbles)
223+
#Generate JavaScript functions for new command handlers
224+
cmd_handlers_js = "\n".join(f"function {handler.__name__}(payload) {{ {handler(None)} }}" for handler in self.cmd_handlers.values())
225+
return f"{bubble_init_js}\n{cmd_handlers_js}"
226+
227+
def update(self, update_fn: Callable[[Msg, Model], tuple[Model, Union[Cmd, None]]]):
228+
"""Decorator to set the update function"""
229+
self.update_fn = update_fn
230+
return update_fn
231+
232+
def view(self, view_fn: Callable[[Model], Element]):
233+
"""Decorator to set the view function"""
234+
self.view_fn = view_fn
235+
return view_fn
236+
237+
def cmd(self, action: str):
238+
"""Decorator to handle cmd function"""
239+
def decorator(f: Callable):
240+
self.cmd_handlers[action] = f
241+
return f
242+
243+
return decorator
244+
245+
def _get_css_link(self):
246+
if self.css_framework == CSSFramework.PICO:
247+
return '<link rel="stylesheet" href="https://unpkg.com/@picocss/pico@1.*/css/pico.min.css">'
248+
elif self.css_framework == CSSFramework.BOOTSTRAP:
249+
return '<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">'
250+
elif self.css_framework == CSSFramework.TAILWIND:
251+
return '<script src="https://cdn.tailwindcss.com"></script>'
252+
else:
253+
return ''
254+
255+
def _get_js_links(self):
256+
return '\n'.join([f'<script src="{lib}"></script>' for lib in self.js_libraries])
257+
258+
def _get_js_link(self):
259+
if self.css_framework == CSSFramework.BOOTSTRAP:
260+
return '<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>'
261+
else:
262+
return ''
263+
264+
def run(self):
265+
import uvicorn
266+
uvicorn.run(self.app, host="127.0.0.1", port=5001)

0 commit comments

Comments
 (0)