evenis 0.2.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.
evenis-0.2.0/PKG-INFO ADDED
@@ -0,0 +1,86 @@
1
+ Metadata-Version: 2.4
2
+ Name: evenis
3
+ Version: 0.2.0
4
+ Summary: Python-only fullstack UI framework: UI, состояние, анимации и бэкенд — только на Python.
5
+ Author: evenis
6
+ License: MIT
7
+ Project-URL: Homepage, https://example.com/evenis
8
+ Keywords: ui,framework,fullstack,spa,virtual-dom,fastapi
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Topic :: Software Development :: User Interfaces
12
+ Requires-Python: >=3.9
13
+ Description-Content-Type: text/markdown
14
+ Provides-Extra: server
15
+ Requires-Dist: fastapi>=0.100; extra == "server"
16
+ Requires-Dist: uvicorn>=0.23; extra == "server"
17
+ Requires-Dist: websockets>=11; extra == "server"
18
+ Requires-Dist: pyjwt>=2.6; extra == "server"
19
+
20
+ # evenis
21
+
22
+ **Python-only fullstack UI framework.** Весь UI, состояние, анимации и бэкенд
23
+ описываются только на Python — ни строчки HTML/CSS/JS в пользовательском коде.
24
+ Компилятор выдаёт SPA с настоящим Virtual DOM, а бэкенд-функции автоматически
25
+ становятся API.
26
+
27
+ ```python
28
+ import evenis as ui
29
+ from evenis.server import server, action, LiveState
30
+
31
+ @server
32
+ async def get_users():
33
+ return await User.all() # → GET /__evenis/api/get_users
34
+
35
+ @action
36
+ async def add_user(name: str):
37
+ ... # → POST /__evenis/api/add_user
38
+ return await User.all()
39
+
40
+ online = LiveState(0, name="online") # WebSocket-состояние
41
+
42
+ app = ui.App(title="App", state={"users": []})
43
+
44
+ @app.page("/", load=get_users.load(into="users"))
45
+ def home():
46
+ return ui.Column(
47
+ ui.Text("Онлайн: {{ online }}"),
48
+ ui.For("users", lambda u, i: ui.Text(u.attr("name"))),
49
+ ui.PrimaryButton("Добавить", on_click=add_user(into="users", name="{{ draft }}")),
50
+ )
51
+ ```
52
+
53
+ ## Установка
54
+
55
+ ```bash
56
+ pip install evenis # ядро (сборка статических SPA)
57
+ pip install "evenis[server]" # + fullstack (FastAPI/WebSocket)
58
+ ```
59
+
60
+ ## Запуск
61
+
62
+ ```bash
63
+ python -m evenis build app.py # собрать статический сайт в dist/
64
+ python -m evenis dev app.py --port 8000 # dev-сервер + hot reload
65
+ python -m evenis serve app.py --port 8000 # fullstack: фронт + API + WebSocket
66
+ ```
67
+
68
+ ## Возможности
69
+
70
+ - **Только Python** — никакого HTML/CSS/JS в пользовательском коде.
71
+ - **Virtual DOM** — обновляется только изменившийся узел, фокус в полях сохраняется.
72
+ - **Компоненты и раскладка** — `Button`, `Card`, `Input`, `Row`, `Column`, `Grid`,
73
+ `AdaptiveLayout` (сам выбирает число колонок), `Motion`.
74
+ - **Состояние** — реактивный store, `computed`-сигналы, декларативные действия,
75
+ middleware-pipeline.
76
+ - **Анимации** — Motion, FLIP layout-переходы, настраиваемые кривые и длительности.
77
+ - **Fullstack** — `@server` / `@action` превращают функции в API, `LiveState` —
78
+ в WebSocket-состояние с push-обновлениями. Транспорт выбирается автоматически.
79
+
80
+ ## Примеры
81
+
82
+ См. каталог `examples/`: `hello_world.py`, `dashboard.py`, `ecommerce.py`,
83
+ `demo_app.py`, `fullstack.py`, `docs_site.py` (эта документация, написанная на
84
+ самом evenis).
85
+
86
+ MIT License.
evenis-0.2.0/README.md ADDED
@@ -0,0 +1,67 @@
1
+ # evenis
2
+
3
+ **Python-only fullstack UI framework.** Весь UI, состояние, анимации и бэкенд
4
+ описываются только на Python — ни строчки HTML/CSS/JS в пользовательском коде.
5
+ Компилятор выдаёт SPA с настоящим Virtual DOM, а бэкенд-функции автоматически
6
+ становятся API.
7
+
8
+ ```python
9
+ import evenis as ui
10
+ from evenis.server import server, action, LiveState
11
+
12
+ @server
13
+ async def get_users():
14
+ return await User.all() # → GET /__evenis/api/get_users
15
+
16
+ @action
17
+ async def add_user(name: str):
18
+ ... # → POST /__evenis/api/add_user
19
+ return await User.all()
20
+
21
+ online = LiveState(0, name="online") # WebSocket-состояние
22
+
23
+ app = ui.App(title="App", state={"users": []})
24
+
25
+ @app.page("/", load=get_users.load(into="users"))
26
+ def home():
27
+ return ui.Column(
28
+ ui.Text("Онлайн: {{ online }}"),
29
+ ui.For("users", lambda u, i: ui.Text(u.attr("name"))),
30
+ ui.PrimaryButton("Добавить", on_click=add_user(into="users", name="{{ draft }}")),
31
+ )
32
+ ```
33
+
34
+ ## Установка
35
+
36
+ ```bash
37
+ pip install evenis # ядро (сборка статических SPA)
38
+ pip install "evenis[server]" # + fullstack (FastAPI/WebSocket)
39
+ ```
40
+
41
+ ## Запуск
42
+
43
+ ```bash
44
+ python -m evenis build app.py # собрать статический сайт в dist/
45
+ python -m evenis dev app.py --port 8000 # dev-сервер + hot reload
46
+ python -m evenis serve app.py --port 8000 # fullstack: фронт + API + WebSocket
47
+ ```
48
+
49
+ ## Возможности
50
+
51
+ - **Только Python** — никакого HTML/CSS/JS в пользовательском коде.
52
+ - **Virtual DOM** — обновляется только изменившийся узел, фокус в полях сохраняется.
53
+ - **Компоненты и раскладка** — `Button`, `Card`, `Input`, `Row`, `Column`, `Grid`,
54
+ `AdaptiveLayout` (сам выбирает число колонок), `Motion`.
55
+ - **Состояние** — реактивный store, `computed`-сигналы, декларативные действия,
56
+ middleware-pipeline.
57
+ - **Анимации** — Motion, FLIP layout-переходы, настраиваемые кривые и длительности.
58
+ - **Fullstack** — `@server` / `@action` превращают функции в API, `LiveState` —
59
+ в WebSocket-состояние с push-обновлениями. Транспорт выбирается автоматически.
60
+
61
+ ## Примеры
62
+
63
+ См. каталог `examples/`: `hello_world.py`, `dashboard.py`, `ecommerce.py`,
64
+ `demo_app.py`, `fullstack.py`, `docs_site.py` (эта документация, написанная на
65
+ самом evenis).
66
+
67
+ MIT License.
@@ -0,0 +1,151 @@
1
+ """evenis — production-grade Python UI framework.
2
+
3
+ Двухслойная система:
4
+ • compile-time (Python): UI-дерево (AST) → layout → theme→CSS → HTML → JS inject;
5
+ • runtime (browser JS): SPA-роутинг, реактивный store, DOM-обновления, анимации.
6
+
7
+ Пользователь описывает UI ТОЛЬКО на Python — без HTML/CSS/JS.
8
+
9
+ Пример:
10
+ import evenis as ui
11
+
12
+ app = ui.App(title="Demo", state={"count": 0})
13
+
14
+ @app.page("/", title="Home")
15
+ def home():
16
+ return ui.Column(
17
+ ui.Heading("Счётчик"),
18
+ ui.Text("Значение: {{ count }}"),
19
+ ui.Button("+1", on_click=ui.increment("count"), variant="primary"),
20
+ )
21
+
22
+ app.build("dist")
23
+ """
24
+
25
+ from .core import App, Component, Element, For, Fragment, If, Node, Page, to_vnode
26
+ from .layout import (
27
+ AdaptiveLayout,
28
+ Column,
29
+ Container,
30
+ Grid,
31
+ Motion,
32
+ Responsive,
33
+ Row,
34
+ Scrollable,
35
+ Spacer,
36
+ Stack,
37
+ )
38
+ from .components import (
39
+ Avatar,
40
+ Badge,
41
+ Breadcrumb,
42
+ Button,
43
+ Card,
44
+ Chart,
45
+ Checkbox,
46
+ CodeBlock,
47
+ DangerButton,
48
+ DatePicker,
49
+ Divider,
50
+ Drawer,
51
+ Field,
52
+ FileUpload,
53
+ Form,
54
+ GhostButton,
55
+ Heading,
56
+ Html,
57
+ Image,
58
+ Input,
59
+ Link,
60
+ Markdown,
61
+ Modal,
62
+ Pagination,
63
+ PrimaryButton,
64
+ Progress,
65
+ Select,
66
+ Skeleton,
67
+ Slider,
68
+ Spinner,
69
+ Switch,
70
+ Table,
71
+ Tabs,
72
+ Text,
73
+ Textarea,
74
+ Tooltip,
75
+ )
76
+ from .style import Theme, default_theme, light_theme
77
+ from .state import (
78
+ append,
79
+ batch,
80
+ batch_if,
81
+ clipboard,
82
+ decrement,
83
+ debounce,
84
+ download_pdf,
85
+ emit,
86
+ fetch_json,
87
+ geolocate,
88
+ increment,
89
+ js,
90
+ log,
91
+ navigate,
92
+ notify,
93
+ prefetch,
94
+ redirect_if,
95
+ remove_at,
96
+ scroll_to,
97
+ set_from_event,
98
+ set_theme,
99
+ set_value,
100
+ shortcut,
101
+ sound,
102
+ throttle,
103
+ toast,
104
+ toggle,
105
+ toggle_theme,
106
+ vibrate,
107
+ )
108
+ from .router import Router
109
+ from .compiler import build_app, build_manifest
110
+ from .server import (
111
+ LiveState,
112
+ action,
113
+ enable_auth,
114
+ protected,
115
+ serve,
116
+ server,
117
+ )
118
+
119
+ __version__ = "0.1.0"
120
+
121
+ __all__ = [
122
+ # core
123
+ "App", "Page", "Component", "Element", "Node", "Fragment", "If", "For", "to_vnode",
124
+ # layout
125
+ "Row", "Column", "Grid", "Stack", "Container", "Spacer", "Responsive",
126
+ "AdaptiveLayout", "Scrollable", "Motion",
127
+ # components
128
+ "Text", "Heading", "Button", "PrimaryButton", "DangerButton", "GhostButton",
129
+ "Card", "Input", "Link", "Image", "Divider",
130
+ # widgets
131
+ "Textarea", "Select", "Switch", "Checkbox", "Slider", "DatePicker", "FileUpload",
132
+ "Field", "Form", "Badge", "Spinner", "Progress", "Skeleton", "Avatar",
133
+ "Tooltip", "Breadcrumb", "CodeBlock", "Markdown", "Html", "Table",
134
+ "Tabs", "Modal", "Drawer", "Pagination", "Chart",
135
+ # style
136
+ "Theme", "default_theme", "light_theme",
137
+ # state / actions
138
+ "set_value", "increment", "decrement", "toggle", "set_from_event",
139
+ "append", "remove_at", "navigate", "js", "fetch_json", "batch", "batch_if",
140
+ # ui side-effects
141
+ "toast", "notify", "clipboard", "sound", "vibrate", "geolocate",
142
+ "set_theme", "toggle_theme", "scroll_to", "emit", "download_pdf",
143
+ "debounce", "throttle", "shortcut",
144
+ # middleware
145
+ "redirect_if", "log", "prefetch",
146
+ # router / compiler
147
+ "Router", "build_app", "build_manifest",
148
+ # server (fullstack)
149
+ "server", "action", "LiveState", "protected", "enable_auth", "serve",
150
+ "__version__",
151
+ ]
@@ -0,0 +1,4 @@
1
+ from .cli import main
2
+
3
+ if __name__ == "__main__":
4
+ main()
@@ -0,0 +1,129 @@
1
+ """evenis.cli — одна команда для сборки и dev-режима.
2
+
3
+ evenis build app.py [--out dist] — собрать сайт в каталог.
4
+ evenis dev app.py [--out dist] [--port 8000]
5
+ — собрать с dev=True (hot reload), поднять статический сервер и
6
+ пересобирать при изменении исходников (polling watcher).
7
+
8
+ `app.py` должен экспортировать переменную `app` (экземпляр evenis.App) либо
9
+ функцию `app()`/`create_app()`, возвращающую App.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import argparse
15
+ import importlib.util
16
+ import os
17
+ import sys
18
+ import time
19
+
20
+
21
+ def _load_app(path: str):
22
+ spec = importlib.util.spec_from_file_location("_evenis_user_app", path)
23
+ if spec is None or spec.loader is None:
24
+ raise SystemExit(f"Не удалось загрузить модуль: {path}")
25
+ mod = importlib.util.module_from_spec(spec)
26
+ spec.loader.exec_module(mod)
27
+ candidate = getattr(mod, "app", None) or getattr(mod, "create_app", None)
28
+ if candidate is None:
29
+ raise SystemExit("В модуле нет `app` (экземпляр App) или фабрики create_app().")
30
+ app = candidate() if callable(candidate) else candidate
31
+ return app
32
+
33
+
34
+ def _snapshot(root: str) -> float:
35
+ """Максимальное mtime по дереву *.py — простой watcher без зависимостей."""
36
+ latest = 0.0
37
+ for dirpath, _dirs, files in os.walk(root):
38
+ if "__pycache__" in dirpath or "/dist" in dirpath:
39
+ continue
40
+ for f in files:
41
+ if f.endswith(".py"):
42
+ try:
43
+ latest = max(latest, os.path.getmtime(os.path.join(dirpath, f)))
44
+ except OSError:
45
+ pass
46
+ return latest
47
+
48
+
49
+ def cmd_build(args) -> None:
50
+ app = _load_app(args.entry)
51
+ out = app.build(args.out)
52
+ print(f"evenis: собрано → {out}")
53
+
54
+
55
+ def cmd_dev(args) -> None:
56
+ import functools
57
+ import http.server
58
+ import socketserver
59
+ import threading
60
+
61
+ entry = os.path.abspath(args.entry)
62
+ watch_root = os.path.dirname(entry)
63
+
64
+ def rebuild():
65
+ app = _load_app(entry)
66
+ app.dev = True
67
+ app.build(args.out)
68
+
69
+ rebuild()
70
+ out_abs = os.path.abspath(args.out)
71
+ print(f"evenis: dev-сборка → {out_abs}")
72
+
73
+ handler = functools.partial(http.server.SimpleHTTPRequestHandler, directory=out_abs)
74
+ httpd = socketserver.TCPServer(("", args.port), handler)
75
+ threading.Thread(target=httpd.serve_forever, daemon=True).start()
76
+ print(f"evenis: http://localhost:{args.port} (Ctrl+C для выхода)")
77
+
78
+ last = _snapshot(watch_root)
79
+ try:
80
+ while True:
81
+ time.sleep(0.6)
82
+ cur = _snapshot(watch_root)
83
+ if cur > last:
84
+ last = cur
85
+ try:
86
+ rebuild()
87
+ print("evenis: пересобрано, обновляю браузер…")
88
+ except Exception as e: # noqa: BLE001 - dev-цикл не должен падать
89
+ print(f"evenis: ошибка сборки: {e}", file=sys.stderr)
90
+ except KeyboardInterrupt:
91
+ print("\nevenis: остановлено.")
92
+ httpd.shutdown()
93
+
94
+
95
+ def cmd_serve(args) -> None:
96
+ from .server import serve
97
+
98
+ app = _load_app(args.entry)
99
+ serve(app, outdir=args.out, port=args.port, host=args.host)
100
+
101
+
102
+ def main(argv=None) -> None:
103
+ parser = argparse.ArgumentParser(prog="evenis", description="evenis — Python UI framework")
104
+ sub = parser.add_subparsers(dest="cmd", required=True)
105
+
106
+ b = sub.add_parser("build", help="собрать сайт в каталог")
107
+ b.add_argument("entry", help="путь к app.py (экспортирует app)")
108
+ b.add_argument("--out", default="dist", help="каталог вывода (по умолчанию dist)")
109
+ b.set_defaults(func=cmd_build)
110
+
111
+ d = sub.add_parser("dev", help="dev-сервер с hot reload")
112
+ d.add_argument("entry", help="путь к app.py (экспортирует app)")
113
+ d.add_argument("--out", default="dist", help="каталог вывода")
114
+ d.add_argument("--port", type=int, default=8000, help="порт (по умолчанию 8000)")
115
+ d.set_defaults(func=cmd_dev)
116
+
117
+ s = sub.add_parser("serve", help="fullstack-сервер (фронт + FastAPI API + WebSocket)")
118
+ s.add_argument("entry", help="путь к app.py (экспортирует app)")
119
+ s.add_argument("--out", default="dist", help="каталог вывода")
120
+ s.add_argument("--port", type=int, default=8000, help="желаемый порт (свободный подберётся рядом)")
121
+ s.add_argument("--host", default="127.0.0.1", help="хост (по умолчанию 127.0.0.1)")
122
+ s.set_defaults(func=cmd_serve)
123
+
124
+ args = parser.parse_args(argv)
125
+ args.func(args)
126
+
127
+
128
+ if __name__ == "__main__":
129
+ main()
@@ -0,0 +1,153 @@
1
+ """evenis.compiler — конвейер компиляции (compile-time слой).
2
+
3
+ Этапы:
4
+ 1. AST: собрать VNode-дерево каждой страницы (App.pages → manifest маршрутов).
5
+ 2. layout resolve: уже выполнен при построении VNode (Row/Column/... → классы).
6
+ 3. CSS generator: theme → style.css.
7
+ 4. HTML generator: SSR начального маршрута → index.html (первый кадр).
8
+ 5. JS runtime injector: runtime + манифест приложения → app.js.
9
+
10
+ Результат пишется в каталог `outdir` (по умолчанию dist/):
11
+ dist/index.html, dist/style.css, dist/app.js
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import json
17
+ import os
18
+ from typing import TYPE_CHECKING
19
+
20
+ from ..router import build_routes
21
+ from ..style.css import generate_css
22
+ from .html import render_page_html
23
+ from .runtime import RUNTIME_JS
24
+
25
+ if TYPE_CHECKING: # pragma: no cover
26
+ from ..core import App
27
+
28
+
29
+ def build_manifest(app: "App") -> dict:
30
+ """Собрать JSON-манифест приложения для рантайма."""
31
+ routes = build_routes(app.pages)
32
+ if not routes:
33
+ raise ValueError("В приложении нет страниц: добавьте хотя бы одну Page")
34
+ fallback = app.pages[0].path
35
+ computed = [{"name": k, "expr": v} for k, v in app.computed.items()]
36
+ manifest = {
37
+ "title": app.title,
38
+ "mount": "app",
39
+ "state": app.state,
40
+ "routes": routes,
41
+ "fallback": fallback,
42
+ "breakpoint": app.theme.breakpoint,
43
+ "computed": computed,
44
+ "middleware": app.middleware,
45
+ "features": app.features,
46
+ "dev": app.dev,
47
+ "theme": getattr(app, "theme_mode", "dark"),
48
+ }
49
+ if getattr(app, "persist", None):
50
+ manifest["persist"] = app.persist
51
+ if getattr(app, "shortcuts", None):
52
+ manifest["shortcuts"] = app.shortcuts
53
+ if getattr(app, "i18n", None):
54
+ manifest["i18n"] = app.i18n
55
+ if getattr(app, "on_error", None):
56
+ oe = app.on_error
57
+ manifest["onError"] = oe if isinstance(oe, list) else [oe]
58
+ # Серверный слой (если использовались @server/@action/LiveState).
59
+ try:
60
+ from ..server import REGISTRY, server_manifest
61
+
62
+ if REGISTRY.endpoints or REGISTRY.livestates:
63
+ sm = server_manifest()
64
+ manifest["server"] = sm
65
+ # начальные значения LiveState кладём прямо в state, чтобы первый кадр
66
+ # уже показывал актуальные данные ещё до WebSocket-подключения.
67
+ for k, v in sm.get("live", {}).items():
68
+ manifest["state"].setdefault(k, v)
69
+ except Exception:
70
+ pass
71
+ # Загрузка данных при входе на страницу (Page.load → server endpoint).
72
+ for path, route in routes.items():
73
+ page = next((p for p in app.pages if p.path == path), None)
74
+ if page is not None and getattr(page, "load", None):
75
+ route["load"] = page.load
76
+ return manifest
77
+
78
+
79
+ _HOT_RELOAD = """
80
+ <script>
81
+ (function(){
82
+ var cur=null;
83
+ setInterval(function(){
84
+ fetch('__evenis_buildid?ts='+Date.now()).then(function(r){return r.text();}).then(function(id){
85
+ if(cur===null){cur=id;return;} if(id!==cur){location.reload();}
86
+ }).catch(function(){});
87
+ }, 800);
88
+ })();
89
+ </script>"""
90
+
91
+
92
+ def _index_html(app: "App", manifest: dict) -> str:
93
+ initial_path = manifest["fallback"]
94
+ initial_page = manifest["routes"][initial_path]
95
+ # SSR-scope: state + i18n t() (если включён), чтобы первый кадр был локализован
96
+ scope = dict(app.state)
97
+ i18n = getattr(app, "i18n", None)
98
+ if i18n:
99
+ lang = i18n.get("lang", "en")
100
+ messages = i18n.get("messages", {}).get(lang, {})
101
+ scope["t"] = lambda k: messages.get(k, k)
102
+ ssr = render_page_html(initial_page, scope)
103
+ title = manifest["routes"][initial_path].get("title", app.title)
104
+ theme = manifest.get("theme", "dark")
105
+ lang_attr = (i18n or {}).get("lang", "ru")
106
+ hot = _HOT_RELOAD if app.dev else ""
107
+ return f"""<!doctype html>
108
+ <html lang="{lang_attr}" data-theme="{theme}">
109
+ <head>
110
+ <meta charset="utf-8" />
111
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
112
+ <title>{title} · {app.title}</title>
113
+ <link rel="stylesheet" href="style.css" />
114
+ </head>
115
+ <body>
116
+ <div id="app">{ssr}</div>
117
+ <script src="app.js"></script>{hot}
118
+ </body>
119
+ </html>
120
+ """
121
+
122
+
123
+ def _app_js(manifest: dict) -> str:
124
+ data = json.dumps(manifest, ensure_ascii=False)
125
+ return RUNTIME_JS + "\n;window.EVENIS.start(" + data + ");\n"
126
+
127
+
128
+ def build_app(app: "App", outdir: str = "dist") -> str:
129
+ """Скомпилировать App в готовый сайт. Возвращает абсолютный путь к outdir."""
130
+ os.makedirs(outdir, exist_ok=True)
131
+
132
+ manifest = build_manifest(app)
133
+
134
+ css = generate_css(app.theme, features=app.features)
135
+ html_doc = _index_html(app, manifest)
136
+ js = _app_js(manifest)
137
+
138
+ with open(os.path.join(outdir, "style.css"), "w", encoding="utf-8") as f:
139
+ f.write(css)
140
+ with open(os.path.join(outdir, "index.html"), "w", encoding="utf-8") as f:
141
+ f.write(html_doc)
142
+ with open(os.path.join(outdir, "app.js"), "w", encoding="utf-8") as f:
143
+ f.write(js)
144
+ if app.dev:
145
+ import time
146
+
147
+ with open(os.path.join(outdir, "__evenis_buildid"), "w", encoding="utf-8") as f:
148
+ f.write(str(time.time()))
149
+
150
+ return os.path.abspath(outdir)
151
+
152
+
153
+ __all__ = ["build_app", "build_manifest"]