HTeaLeaf 0.3.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.
Files changed (33) hide show
  1. htealeaf-0.3.0/PKG-INFO +95 -0
  2. htealeaf-0.3.0/README.md +86 -0
  3. htealeaf-0.3.0/pyproject.toml +17 -0
  4. htealeaf-0.3.0/setup.cfg +4 -0
  5. htealeaf-0.3.0/src/HTeaLeaf/Html/Component.py +219 -0
  6. htealeaf-0.3.0/src/HTeaLeaf/Html/Elements.py +137 -0
  7. htealeaf-0.3.0/src/HTeaLeaf/Html/__init__.py +2 -0
  8. htealeaf-0.3.0/src/HTeaLeaf/Magic/HelperMidleware.py +13 -0
  9. htealeaf-0.3.0/src/HTeaLeaf/Magic/LocalState.py +38 -0
  10. htealeaf-0.3.0/src/HTeaLeaf/Magic/MagicComponent.py +122 -0
  11. htealeaf-0.3.0/src/HTeaLeaf/Magic/Store.py +180 -0
  12. htealeaf-0.3.0/src/HTeaLeaf/Magic/__init__.py +0 -0
  13. htealeaf-0.3.0/src/HTeaLeaf/Magic/jslib/JSCode.py +110 -0
  14. htealeaf-0.3.0/src/HTeaLeaf/Magic/jslib/JSDO.py +63 -0
  15. htealeaf-0.3.0/src/HTeaLeaf/Magic/jslib/__init__.py +2 -0
  16. htealeaf-0.3.0/src/HTeaLeaf/Magic/jslib/common.py +43 -0
  17. htealeaf-0.3.0/src/HTeaLeaf/Magic/jslib/py2js.py +307 -0
  18. htealeaf-0.3.0/src/HTeaLeaf/Server/ASGI.py +80 -0
  19. htealeaf-0.3.0/src/HTeaLeaf/Server/CGI.py +46 -0
  20. htealeaf-0.3.0/src/HTeaLeaf/Server/Http/HttpHeader.py +40 -0
  21. htealeaf-0.3.0/src/HTeaLeaf/Server/Http/HttpRequest.py +81 -0
  22. htealeaf-0.3.0/src/HTeaLeaf/Server/Http/HttpResponse.py +75 -0
  23. htealeaf-0.3.0/src/HTeaLeaf/Server/Http/__init__.py +0 -0
  24. htealeaf-0.3.0/src/HTeaLeaf/Server/Server.py +236 -0
  25. htealeaf-0.3.0/src/HTeaLeaf/Server/WSGI.py +37 -0
  26. htealeaf-0.3.0/src/HTeaLeaf/Server/__init__.py +0 -0
  27. htealeaf-0.3.0/src/HTeaLeaf/__init__.py +0 -0
  28. htealeaf-0.3.0/src/HTeaLeaf/utils.py +3 -0
  29. htealeaf-0.3.0/src/HTeaLeaf.egg-info/PKG-INFO +95 -0
  30. htealeaf-0.3.0/src/HTeaLeaf.egg-info/SOURCES.txt +31 -0
  31. htealeaf-0.3.0/src/HTeaLeaf.egg-info/dependency_links.txt +1 -0
  32. htealeaf-0.3.0/src/HTeaLeaf.egg-info/requires.txt +2 -0
  33. htealeaf-0.3.0/src/HTeaLeaf.egg-info/top_level.txt +1 -0
@@ -0,0 +1,95 @@
1
+ Metadata-Version: 2.4
2
+ Name: HTeaLeaf
3
+ Version: 0.3.0
4
+ Summary: Declarative SSR Framework
5
+ Requires-Python: >=3.13
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: gunicorn>=23.0.0
8
+ Requires-Dist: uvicorn>=0.38.0
9
+
10
+ # πŸƒ TeaLeaf
11
+
12
+ **TeaLeaf** is a *declarative web framework for Python* β€”
13
+ it lets you build dynamic, reactive web apps using **pure Python**,
14
+ without writing templates or frontend JavaScript manually.
15
+
16
+ ---
17
+
18
+ ## ✨ Overview
19
+
20
+ TeaLeaf merges ideas from modern frontend frameworks like React, Svelte, and SolidJS
21
+ with the simplicity of traditional Python web servers.
22
+
23
+ You declare HTML directly in Python, manage reactive state via `Store` objects,
24
+ and TeaLeaf takes care of keeping everything in sync β€” automatically.
25
+
26
+ ---
27
+
28
+ ## πŸš€ Quick Example
29
+
30
+ ```python
31
+ from TeaLeaf.Server.WSGI import WSGI
32
+ from TeaLeaf.Magic.Store import Store, SuperStore
33
+ from TeaLeaf.Html.Elements import div, h3, button
34
+
35
+ # Create the server
36
+ app = WSGI()
37
+ SuperStore(app)
38
+
39
+ # Reactive server-side store
40
+ counter = Store({"count": 0})
41
+
42
+ @app.route("/")
43
+ def home():
44
+ return div(
45
+ button("-").attr(onclick=counter.do.update("count", -1)),
46
+ h3(counter.react("count")),
47
+ button("+").attr(onclick=counter.do.update("count", 1)),
48
+ )
49
+
50
+ application = app.wsgi_app
51
+
52
+ if __name__ == "__main__":
53
+ from wsgiref.simple_server import make_server
54
+ with make_server("", 8000, application) as server:
55
+ print("Serving at http://127.0.0.1:8000")
56
+ server.serve_forever()
57
+ ```
58
+
59
+ Open your browser and visit http://127.0.0.1:8000 β€”
60
+ you’ll see a fully reactive counter built with only Python
61
+
62
+
63
+ ## Key Features
64
+ - Declarative HTML components β€” build DOM structures with Python functions
65
+ - Path-based routing β€” simple, expressive route definitions
66
+ - Reactive server state (Store, AuthStore) β€” auto-sync between backend and UI
67
+ - JS transpilation (JSCode, JSDO) β€” write JavaScript logic directly in Python
68
+ - Session support β€” cookies and per-user AuthStore state
69
+
70
+ ## Roadmap
71
+
72
+ - [x] Declarative HTML components
73
+ - [x] Path mapping
74
+ - [x] Server Side State (Stores)
75
+ - [x] JS transcription from python
76
+ - [ ] Client Side state
77
+ - [ ] State hooks
78
+ - [ ] Template system
79
+ - [ ] Persistent Store (redis,SQL...)
80
+ - [ ] CLI
81
+ - [ ] Render optimisation
82
+
83
+ Documentation
84
+
85
+ Full documentation is available in the [Wiki](https://github.com/Az107/TeaLeaf/wiki/Welcome-to-the-TeaLeaf!)
86
+
87
+ ## Status
88
+
89
+ TeaLeaf is currently in alpha.
90
+ It’s stable enough for experimentation and small demos,
91
+ but the public API might still change before beta.
92
+
93
+ ## License
94
+
95
+ MIT License Β© 2025 β€” TeaLeaf Framework Made with πŸƒ and Python.
@@ -0,0 +1,86 @@
1
+ # πŸƒ TeaLeaf
2
+
3
+ **TeaLeaf** is a *declarative web framework for Python* β€”
4
+ it lets you build dynamic, reactive web apps using **pure Python**,
5
+ without writing templates or frontend JavaScript manually.
6
+
7
+ ---
8
+
9
+ ## ✨ Overview
10
+
11
+ TeaLeaf merges ideas from modern frontend frameworks like React, Svelte, and SolidJS
12
+ with the simplicity of traditional Python web servers.
13
+
14
+ You declare HTML directly in Python, manage reactive state via `Store` objects,
15
+ and TeaLeaf takes care of keeping everything in sync β€” automatically.
16
+
17
+ ---
18
+
19
+ ## πŸš€ Quick Example
20
+
21
+ ```python
22
+ from TeaLeaf.Server.WSGI import WSGI
23
+ from TeaLeaf.Magic.Store import Store, SuperStore
24
+ from TeaLeaf.Html.Elements import div, h3, button
25
+
26
+ # Create the server
27
+ app = WSGI()
28
+ SuperStore(app)
29
+
30
+ # Reactive server-side store
31
+ counter = Store({"count": 0})
32
+
33
+ @app.route("/")
34
+ def home():
35
+ return div(
36
+ button("-").attr(onclick=counter.do.update("count", -1)),
37
+ h3(counter.react("count")),
38
+ button("+").attr(onclick=counter.do.update("count", 1)),
39
+ )
40
+
41
+ application = app.wsgi_app
42
+
43
+ if __name__ == "__main__":
44
+ from wsgiref.simple_server import make_server
45
+ with make_server("", 8000, application) as server:
46
+ print("Serving at http://127.0.0.1:8000")
47
+ server.serve_forever()
48
+ ```
49
+
50
+ Open your browser and visit http://127.0.0.1:8000 β€”
51
+ you’ll see a fully reactive counter built with only Python
52
+
53
+
54
+ ## Key Features
55
+ - Declarative HTML components β€” build DOM structures with Python functions
56
+ - Path-based routing β€” simple, expressive route definitions
57
+ - Reactive server state (Store, AuthStore) β€” auto-sync between backend and UI
58
+ - JS transpilation (JSCode, JSDO) β€” write JavaScript logic directly in Python
59
+ - Session support β€” cookies and per-user AuthStore state
60
+
61
+ ## Roadmap
62
+
63
+ - [x] Declarative HTML components
64
+ - [x] Path mapping
65
+ - [x] Server Side State (Stores)
66
+ - [x] JS transcription from python
67
+ - [ ] Client Side state
68
+ - [ ] State hooks
69
+ - [ ] Template system
70
+ - [ ] Persistent Store (redis,SQL...)
71
+ - [ ] CLI
72
+ - [ ] Render optimisation
73
+
74
+ Documentation
75
+
76
+ Full documentation is available in the [Wiki](https://github.com/Az107/TeaLeaf/wiki/Welcome-to-the-TeaLeaf!)
77
+
78
+ ## Status
79
+
80
+ TeaLeaf is currently in alpha.
81
+ It’s stable enough for experimentation and small demos,
82
+ but the public API might still change before beta.
83
+
84
+ ## License
85
+
86
+ MIT License Β© 2025 β€” TeaLeaf Framework Made with πŸƒ and Python.
@@ -0,0 +1,17 @@
1
+ [project]
2
+ name = "HTeaLeaf"
3
+ version = "0.3.0"
4
+ description = "Declarative SSR Framework"
5
+ readme = "README.md"
6
+ requires-python = ">=3.13"
7
+ dependencies = [
8
+ "gunicorn>=23.0.0",
9
+ "uvicorn>=0.38.0",
10
+ ]
11
+
12
+ [build-system]
13
+ requires = ["setuptools"]
14
+ build-backend = "setuptools.build_meta"
15
+
16
+ [tool.setuptools.package-data]
17
+ "TeaLeaf.Server" = ['*.js']
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,219 @@
1
+ import hashlib
2
+ import json
3
+ from typing import Any, List, Union
4
+
5
+ from ..Magic.jslib.JSCode import JSCode
6
+
7
+
8
+ class Component:
9
+ """
10
+ Represents an HTML component with attributes, children, and optional inline styles.
11
+ This class allows constructing HTML elements programmatically and managing CSS styles.
12
+ """
13
+
14
+ def __init__(self, name, *childs: Union[str, List[Any], "Component", "JSCode"]) -> None:
15
+ """
16
+ Initializes a new Component instance.
17
+
18
+ :param name: The tag name of the HTML element.
19
+ :param childs: Optional children elements, which can be strings, lists, or other Component instances.
20
+ """
21
+
22
+ self.styles: str | None = None
23
+ self.name = name
24
+ self.children: list[Component | str | list | JSCode] = list(childs)
25
+ self.attributes: dict[str, str | None] = dict()
26
+ self._id: str = self._generate_id()
27
+
28
+ def _generate_id(self) -> str:
29
+ """Genera un ID determinista basado en el contenido del componente."""
30
+ content = {
31
+ "name": self.name,
32
+ "children": [
33
+ child._id if isinstance(child, Component) else str(child)
34
+ for child in self.children
35
+ ]
36
+ }
37
+ raw = json.dumps(content, sort_keys=True)
38
+ hash_str = hashlib.md5(raw.encode()).hexdigest()[:12]
39
+ return f"tl-{hash_str}"
40
+
41
+ def id(self, id: str):
42
+ """
43
+ Sets the ID of the component and adds it as an attribute.
44
+
45
+ :param id: The ID to assign.
46
+ :return: The component instance (for method chaining).
47
+ """
48
+
49
+ self._id = id
50
+ return self.attr(id=id)
51
+
52
+ def classes(self, classes):
53
+ """
54
+ Adds a CSS class attribute to the component.
55
+
56
+ :param classes: CSS class names (space-separated).
57
+ :return: The component instance (for method chaining).
58
+ """
59
+
60
+ self.attributes["class"] = classes
61
+ return self
62
+
63
+ def style(self, path: str | None = None, **attr):
64
+ """
65
+ Adds inline styles to the component.
66
+
67
+ :param path: Optional path to an external CSS file.
68
+ :param attr: CSS properties to apply (e.g., color="red", margin="10px").
69
+ :return: The component instance (for method chaining).
70
+ """
71
+
72
+
73
+ self.styles = (self.styles or "") + f"#{self._id} {{\n"
74
+ self.styles += "\n".join(
75
+ f" {k.replace('_', '-')}: {v};" for k, v in attr.items()
76
+ )
77
+ self.styles += "\n}\n"
78
+
79
+ if path:
80
+ with open(path, "r") as f:
81
+ self.styles += f.read()
82
+ return self
83
+
84
+ def attr(self,*args, **attr):
85
+ """
86
+ Adds custom attributes to the component.
87
+
88
+ :param attr: Dictionary of attribute names and values.
89
+ :return: The component instance (for method chaining).
90
+ """
91
+
92
+ for arg in args:
93
+ self.attributes[arg] = None
94
+
95
+ for k in attr:
96
+ # if type(attr[k]) is str:
97
+ value = attr[k]
98
+ # if isinstance(value, JSCode):
99
+ # value = f"{{{{{str(value)}}}}}"
100
+ self.attributes[k] = value
101
+ # elif type(attr[k]) is FunctionType:
102
+ # py_f = inspect.getsource(attr[k])
103
+ # self.attributes[k] = f"""() => pyodide.runPython(`{py_f}`)"""
104
+
105
+ return self
106
+
107
+ def append(self, child: Union[str, "Component", list]):
108
+ """
109
+ Appends a child element to the component.
110
+
111
+ :param child: A Component, string, or list of elements.
112
+ :return: The component instance (for method chaining).
113
+ """
114
+
115
+ self.children.append(child)
116
+ return self
117
+
118
+ def prepend(self, child: Union[str, "Component", list]):
119
+ """
120
+ Prepends a child element to the component.
121
+
122
+ :param child: A Component, string, or list of elements.
123
+ :return: The component instance (for method chaining).
124
+ """
125
+
126
+ self.children.insert(0, child)
127
+ return self
128
+
129
+ def __build_attr__(self) -> str:
130
+ return " " + " ".join(
131
+ f"{k}='{v}'" if v is not None else f"{k}" for k, v in self.attributes.items()
132
+ )
133
+
134
+ def __build_child__(self, children: list):
135
+ html_parts = []
136
+ css_parts = []
137
+ for child in children:
138
+ if isinstance(child, str):
139
+ html_parts.append(f"{child}")
140
+ elif isinstance(child, list):
141
+ html, css = self.__build_child__(child)
142
+ html_parts.append(html)
143
+ css_parts.append(css)
144
+ elif isinstance(child, Component):
145
+ html, css = child.build()
146
+ html_parts.append(html)
147
+ css_parts.append(css)
148
+ # elif isinstance(child, JSDO):
149
+ # print(f"JSCode: {child().raw}")
150
+ # html_parts.append(f"{{{{{child().raw}}}}}")
151
+ elif isinstance(child, JSCode):
152
+ # JSCode outside of an attribute should be a special tag {{jscode_name}}
153
+ print(f"JSCode: {child.raw}")
154
+ html_parts.append(f"{{{{{child.raw}}}}}")
155
+ else:
156
+ try:
157
+ html_parts.append(str(child))
158
+ except Exception:
159
+ continue
160
+ return "".join(html_parts), "".join(filter(None, css_parts))
161
+
162
+ def build(self) -> tuple[str, str]:
163
+ """
164
+ Builds the component's HTML and CSS separately.
165
+
166
+ :return: A tuple (HTML string, CSS string)
167
+ """
168
+
169
+ if self.styles is not None and "id" not in self.attributes:
170
+ self.attr(id=self._id)
171
+ if len(self.children) == 0:
172
+ result = f"<{self.name}{self.__build_attr__()}/>\n"
173
+ else:
174
+ endln = "\n" if len(self.children) > 1 else ""
175
+ result = f"<{self.name}{self.__build_attr__()}>{endln}"
176
+ html, styles = self.__build_child__(self.children)
177
+ result += html
178
+ if self.styles is None:
179
+ self.styles = styles
180
+ else:
181
+ self.styles += styles
182
+ result += f"\t</{self.name}>\n"
183
+ css: str = "" if self.styles is None else self.styles
184
+ return result, css
185
+
186
+ def render(self) -> str:
187
+ """
188
+ Builds and returns the full HTML including inline CSS inside a <style> tag.
189
+
190
+ :return: A complete HTML string with embedded CSS.
191
+ """
192
+
193
+ if len(self.children) == 0:
194
+ result = f"<{self.name}{self.__build_attr__()}/>\n"
195
+ else:
196
+ inner_result, css = self.__build_child__(self.children)
197
+ if self.styles is None:
198
+ self.styles = css
199
+ else:
200
+ self.styles += css
201
+ self.styles += "\n"
202
+ result = f"<{self.name}{self.__build_attr__()}>\n"
203
+ if self.styles is not None:
204
+ result += f"<style>{self.styles}</style>\n"
205
+ result += inner_result
206
+ result += f"</{self.name}>\n"
207
+
208
+ return result
209
+
210
+
211
+ class ComponentMeta(type):
212
+ def __new__(cls, name, bases, dct):
213
+ if name not in ("Component", "ComponentMeta"):
214
+
215
+ def init(self, *childs):
216
+ super(self.__class__, self).__init__(name, *childs)
217
+
218
+ dct["__init__"] = init
219
+ return super().__new__(cls, name, bases, dct)
@@ -0,0 +1,137 @@
1
+ from types import FunctionType
2
+ from typing import Any, List, Union
3
+
4
+ from HTeaLeaf.Magic.jslib import JSCode, JSFunction, js
5
+
6
+ from .Component import Component, ComponentMeta
7
+
8
+
9
+ class html(Component, metaclass=ComponentMeta):
10
+ pass
11
+
12
+ class head(Component, metaclass=ComponentMeta):
13
+ pass
14
+
15
+ class header(Component, metaclass=ComponentMeta):
16
+ pass
17
+
18
+ class link(Component, metaclass=ComponentMeta):
19
+ def __init__(self, *childs):
20
+ super().__init__("link", *childs)
21
+
22
+
23
+ class script(Component):
24
+ def __init__(self, *childs: Union[str, List[Any], "Component", JSFunction] ,src=None):
25
+ parsed_childs: Union[str, List[Any], "Component"] = []
26
+ for child in childs:
27
+ if type(child) is FunctionType:
28
+ parsed_childs.append(js(child))
29
+ elif type(child) is JSFunction:
30
+ parsed_childs.append(child)
31
+ else:
32
+ parsed_childs.append(child)
33
+ super().__init__("script", *parsed_childs)
34
+ self.unsafe = True
35
+ if src is not None:
36
+ self.attr(src=src)
37
+ self.children = [""]
38
+ # else:
39
+
40
+
41
+
42
+ class style(Component, metaclass=ComponentMeta):
43
+ pass
44
+
45
+
46
+ class body(Component, metaclass=ComponentMeta):
47
+ pass
48
+
49
+
50
+ class h1(Component, metaclass=ComponentMeta):
51
+ pass
52
+
53
+
54
+ class h2(Component, metaclass=ComponentMeta):
55
+ pass
56
+
57
+
58
+ class h3(Component, metaclass=ComponentMeta):
59
+ pass
60
+
61
+
62
+ class div(Component, metaclass=ComponentMeta):
63
+ pass
64
+
65
+ def row(self):
66
+ self.attr(style="display: flex; flex-direction: row")
67
+ return self
68
+
69
+ def column(self):
70
+ self.attr(style="display: flex; flex-direction: column")
71
+ return self
72
+
73
+
74
+ class button(Component, metaclass=ComponentMeta):
75
+
76
+ def reactive(self,path,component_id):
77
+ """
78
+ Makes the button reactive by linking it to a FetchComponent.
79
+
80
+ :param path: The URL to fetch new data from when clicked.
81
+ :param component: The FetchComponent to be updated.
82
+ """
83
+
84
+ js = f"""fetchAndUpdate('{path}','{{}}','{component_id}')"""
85
+ self.attr(onclick=js)
86
+ return self
87
+
88
+
89
+
90
+ class label(Component, metaclass=ComponentMeta):
91
+ pass
92
+
93
+ class checkbox(Component):
94
+ def __init__(self,checked = False, *childs):
95
+ super().__init__("input", *childs)
96
+ self.attr(type="checkbox")
97
+ if checked:
98
+ self.attr(checked="True")
99
+
100
+ class textInput(Component):
101
+ def __init__(self, *childs):
102
+ super().__init__("input", *childs)
103
+
104
+ class select(Component):
105
+ def __init__(self, items: List[str]):
106
+ super().__init__("select")
107
+ for item in items:
108
+ self.append(option(item))
109
+
110
+ class option(Component):
111
+ def __init__(self, value):
112
+ super().__init__("option", value)
113
+ self.attr(value=value)
114
+
115
+
116
+ class submit(Component):
117
+ def __init__(self, *childs):
118
+ super().__init__("input", *childs)
119
+ self.attr(type="submit")
120
+
121
+ class form(Component):
122
+ def __init__(self, *childs):
123
+ super().__init__("form", *childs)
124
+
125
+ def action(self, action):
126
+ self.attr(action=action)
127
+ return self
128
+
129
+ def method(self, method):
130
+ self.attr(method=method)
131
+ return self
132
+
133
+ def tl_if(condition: JSCode | str | bool, *childs):
134
+ if isinstance(condition, str):
135
+ condition = JSCode(condition)
136
+
137
+ return div(*childs).attr(style=f"display: {condition};")
@@ -0,0 +1,2 @@
1
+ # from .Elements import *
2
+ # from .Component import Component
@@ -0,0 +1,13 @@
1
+ from ..Html.Component import Component
2
+ from ..Html.Elements import script
3
+ from ..Server.Server import Server, ServerEvent
4
+
5
+
6
+ def enable_reactivity(server: Server):
7
+ helper_script = script(src="_engine/helper.js")
8
+
9
+ def event_handler(res_code, res_body, res_headers):
10
+ if isinstance(res_body, Component):
11
+ res_body.prepend(helper_script)
12
+
13
+ server.registry_hook(ServerEvent.on_response, event_handler)
@@ -0,0 +1,38 @@
1
+ import hashlib
2
+ import inspect
3
+ import json
4
+
5
+ from HTeaLeaf.Html.Elements import script
6
+
7
+ from ..Magic.jslib.JSCode import JSCode
8
+ from ..Magic.jslib.JSDO import JSDO
9
+
10
+ # class localState():
11
+ # def __init__(self, init_state):
12
+ # self.do = JSDO("LocalState",init_state)
13
+
14
+ # def
15
+
16
+ # def js(self):
17
+ # return self.do.js()
18
+
19
+ # def get(self):
20
+ # return self.do.get()
21
+
22
+ # def set(self, data):
23
+ # return self.do.set(data)
24
+
25
+
26
+
27
+ def use_state(init_state):
28
+ frame = inspect.stack()[1]
29
+ site = f"{frame.filename}:{frame.lineno}"
30
+ raw = f"{site}:{json.dumps(init_state, sort_keys=True)}"
31
+ id = hashlib.md5(raw.encode()).hexdigest()[:12]
32
+ name = f"localstate_{id}"
33
+
34
+ def new():
35
+ return script(f"const {name} = new LocalState({json.dumps(init_state, sort_keys=True)},\"{name}\");")
36
+
37
+
38
+ return new, JSCode(name)