tempestroid 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.
- tempestroid-0.1.0/.gitignore +73 -0
- tempestroid-0.1.0/CHANGELOG.md +38 -0
- tempestroid-0.1.0/LICENSE +21 -0
- tempestroid-0.1.0/PKG-INFO +368 -0
- tempestroid-0.1.0/README.md +337 -0
- tempestroid-0.1.0/docs/arquitetura.en.md +95 -0
- tempestroid-0.1.0/docs/arquitetura.md +98 -0
- tempestroid-0.1.0/docs/guia/cli.en.md +58 -0
- tempestroid-0.1.0/docs/guia/cli.md +58 -0
- tempestroid-0.1.0/docs/guia/estilos.en.md +136 -0
- tempestroid-0.1.0/docs/guia/estilos.md +138 -0
- tempestroid-0.1.0/docs/guia/eventos.en.md +67 -0
- tempestroid-0.1.0/docs/guia/eventos.md +67 -0
- tempestroid-0.1.0/docs/guia/exemplos.en.md +47 -0
- tempestroid-0.1.0/docs/guia/exemplos.md +48 -0
- tempestroid-0.1.0/docs/guia/widgets.en.md +114 -0
- tempestroid-0.1.0/docs/guia/widgets.md +115 -0
- tempestroid-0.1.0/docs/index.en.md +52 -0
- tempestroid-0.1.0/docs/index.md +57 -0
- tempestroid-0.1.0/docs/inicio-rapido.en.md +85 -0
- tempestroid-0.1.0/docs/inicio-rapido.md +87 -0
- tempestroid-0.1.0/docs/instalacao.en.md +51 -0
- tempestroid-0.1.0/docs/instalacao.md +48 -0
- tempestroid-0.1.0/docs/plan.md +303 -0
- tempestroid-0.1.0/docs/referencia/api.en.md +74 -0
- tempestroid-0.1.0/docs/referencia/api.md +75 -0
- tempestroid-0.1.0/docs/referencia/dispositivo.en.md +64 -0
- tempestroid-0.1.0/docs/referencia/dispositivo.md +66 -0
- tempestroid-0.1.0/docs/research/android-runbook.md +187 -0
- tempestroid-0.1.0/docs/research/android-runtime.md +106 -0
- tempestroid-0.1.0/docs/roadmap.en.md +52 -0
- tempestroid-0.1.0/docs/roadmap.md +52 -0
- tempestroid-0.1.0/examples/README.md +65 -0
- tempestroid-0.1.0/examples/calculator/app.py +185 -0
- tempestroid-0.1.0/examples/colorpicker/app.py +169 -0
- tempestroid-0.1.0/examples/counter/app.py +115 -0
- tempestroid-0.1.0/examples/device_counter/app.py +47 -0
- tempestroid-0.1.0/examples/form/app.py +157 -0
- tempestroid-0.1.0/examples/gallery/app.py +195 -0
- tempestroid-0.1.0/examples/stopwatch/app.py +156 -0
- tempestroid-0.1.0/examples/todo/app.py +220 -0
- tempestroid-0.1.0/mkdocs.yml +126 -0
- tempestroid-0.1.0/pyproject.toml +120 -0
- tempestroid-0.1.0/tempestroid/__init__.py +198 -0
- tempestroid-0.1.0/tempestroid/bridge/__init__.py +35 -0
- tempestroid-0.1.0/tempestroid/bridge/device.py +148 -0
- tempestroid-0.1.0/tempestroid/bridge/handlers.py +103 -0
- tempestroid-0.1.0/tempestroid/bridge/jni.py +143 -0
- tempestroid-0.1.0/tempestroid/bridge/protocol.py +136 -0
- tempestroid-0.1.0/tempestroid/bridge/serializer.py +115 -0
- tempestroid-0.1.0/tempestroid/cli/__init__.py +27 -0
- tempestroid-0.1.0/tempestroid/cli/app_loader.py +114 -0
- tempestroid-0.1.0/tempestroid/cli/main.py +262 -0
- tempestroid-0.1.0/tempestroid/cli/packaging.py +212 -0
- tempestroid-0.1.0/tempestroid/cli/scaffold.py +178 -0
- tempestroid-0.1.0/tempestroid/cli/watcher.py +77 -0
- tempestroid-0.1.0/tempestroid/core/__init__.py +40 -0
- tempestroid-0.1.0/tempestroid/core/introspection.py +115 -0
- tempestroid-0.1.0/tempestroid/core/ir.py +131 -0
- tempestroid-0.1.0/tempestroid/core/reconciler.py +191 -0
- tempestroid-0.1.0/tempestroid/core/state.py +152 -0
- tempestroid-0.1.0/tempestroid/devserver/__init__.py +19 -0
- tempestroid-0.1.0/tempestroid/devserver/client.py +135 -0
- tempestroid-0.1.0/tempestroid/devserver/qr.py +34 -0
- tempestroid-0.1.0/tempestroid/devserver/server.py +149 -0
- tempestroid-0.1.0/tempestroid/native/__init__.py +11 -0
- tempestroid-0.1.0/tempestroid/native/dispatch.py +47 -0
- tempestroid-0.1.0/tempestroid/native/notifications.py +22 -0
- tempestroid-0.1.0/tempestroid/py.typed +0 -0
- tempestroid-0.1.0/tempestroid/renderers/__init__.py +9 -0
- tempestroid-0.1.0/tempestroid/renderers/compose/__init__.py +11 -0
- tempestroid-0.1.0/tempestroid/renderers/compose/style_translator.py +224 -0
- tempestroid-0.1.0/tempestroid/renderers/qt/__init__.py +20 -0
- tempestroid-0.1.0/tempestroid/renderers/qt/app_runner.py +61 -0
- tempestroid-0.1.0/tempestroid/renderers/qt/dev_loop.py +135 -0
- tempestroid-0.1.0/tempestroid/renderers/qt/renderer.py +1000 -0
- tempestroid-0.1.0/tempestroid/renderers/qt/simulator.py +85 -0
- tempestroid-0.1.0/tempestroid/renderers/qt/style_translator.py +224 -0
- tempestroid-0.1.0/tempestroid/style.py +466 -0
- tempestroid-0.1.0/tempestroid/widgets/__init__.py +82 -0
- tempestroid-0.1.0/tempestroid/widgets/base.py +170 -0
- tempestroid-0.1.0/tempestroid/widgets/button.py +25 -0
- tempestroid-0.1.0/tempestroid/widgets/events.py +151 -0
- tempestroid-0.1.0/tempestroid/widgets/indicators.py +39 -0
- tempestroid-0.1.0/tempestroid/widgets/inputs.py +193 -0
- tempestroid-0.1.0/tempestroid/widgets/layout.py +99 -0
- tempestroid-0.1.0/tempestroid/widgets/media.py +50 -0
- tempestroid-0.1.0/tempestroid/widgets/text.py +17 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# ─── Python ───────────────────────────────────────────────────────────────
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
*.so
|
|
6
|
+
*.egg
|
|
7
|
+
*.egg-info/
|
|
8
|
+
.eggs/
|
|
9
|
+
build/
|
|
10
|
+
dist/
|
|
11
|
+
wheels/
|
|
12
|
+
*.whl
|
|
13
|
+
.installed.cfg
|
|
14
|
+
|
|
15
|
+
# Virtual environments
|
|
16
|
+
.venv/
|
|
17
|
+
venv/
|
|
18
|
+
env/
|
|
19
|
+
ENV/
|
|
20
|
+
|
|
21
|
+
# Tooling caches
|
|
22
|
+
.pytest_cache/
|
|
23
|
+
.ruff_cache/
|
|
24
|
+
.mypy_cache/
|
|
25
|
+
.pyright/
|
|
26
|
+
.coverage
|
|
27
|
+
.coverage.*
|
|
28
|
+
htmlcov/
|
|
29
|
+
.tox/
|
|
30
|
+
.nox/
|
|
31
|
+
.cache/
|
|
32
|
+
|
|
33
|
+
# ─── Editors / OS ─────────────────────────────────────────────────────────
|
|
34
|
+
.vscode/
|
|
35
|
+
.idea/
|
|
36
|
+
*.swp
|
|
37
|
+
*~
|
|
38
|
+
.DS_Store
|
|
39
|
+
Thumbs.db
|
|
40
|
+
|
|
41
|
+
# ─── Environment / secrets ─────────────────────────────────────────────────
|
|
42
|
+
.env
|
|
43
|
+
.env.*
|
|
44
|
+
!.env.example
|
|
45
|
+
|
|
46
|
+
# ─── Trilho B: toolchain (regenerable build outputs of B0/B1) ───────────────
|
|
47
|
+
# Built CPython, native wheels, staged site-packages and cloned sources.
|
|
48
|
+
# Reproduce with toolchain/00_fetch_cpython.sh, 01_build_wheels.sh, 02_stage_deps.sh.
|
|
49
|
+
toolchain/dist/
|
|
50
|
+
toolchain/.src/
|
|
51
|
+
toolchain/wheelhouse/
|
|
52
|
+
|
|
53
|
+
# ─── Trilho B: android-host (Gradle / NDK build artifacts) ──────────────────
|
|
54
|
+
android-host/build/
|
|
55
|
+
android-host/app/build/
|
|
56
|
+
android-host/app/.cxx/
|
|
57
|
+
android-host/.gradle/
|
|
58
|
+
android-host/.kotlin/
|
|
59
|
+
android-host/local.properties
|
|
60
|
+
android-host/captures/
|
|
61
|
+
*.apk
|
|
62
|
+
*.aab
|
|
63
|
+
*.ap_
|
|
64
|
+
*.dex
|
|
65
|
+
|
|
66
|
+
# Keep the Gradle wrapper jar tracked (do not ignore it):
|
|
67
|
+
!android-host/gradle/wrapper/gradle-wrapper.jar
|
|
68
|
+
|
|
69
|
+
# MkDocs build output
|
|
70
|
+
site/
|
|
71
|
+
|
|
72
|
+
# `tempest build` embeds the user app here as a generated asset (phase C).
|
|
73
|
+
android-host/app/src/main/assets/tempest_app.py
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to **tempestroid** are documented here. The format is based
|
|
4
|
+
on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project
|
|
5
|
+
adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
|
+
|
|
7
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
## [0.1.0] — 2026-05-31
|
|
10
|
+
|
|
11
|
+
First public release. The framework (Trilho A) is complete and the Android
|
|
12
|
+
runtime (Trilho B, phases B0–B6) is validated on a real arm64 device.
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- **Typed declarative UI core.** A frozen-Pydantic `Style` model (`Color`,
|
|
17
|
+
`Edge`, `Border`, `Transition`, and the style enums) and a widget IR
|
|
18
|
+
(`Widget`, `Text`, `Button`, `Column`, `Row`, `Container`, plus value-bearing
|
|
19
|
+
inputs and utility widgets).
|
|
20
|
+
- **Renderer-agnostic reconciler** — `build → diff → patch` (`Insert` / `Remove`
|
|
21
|
+
/ `Update` / `Reorder` / `Replace`).
|
|
22
|
+
- **Qt desktop simulator** (`run_qt`, optional `qt` extra) with a `Style → Qt`
|
|
23
|
+
translator and an asyncio×Qt event loop via `qasync`.
|
|
24
|
+
- **Async-first state runtime** (`App`) with coalesced rebuilds and **stateful
|
|
25
|
+
hot reload** (`App.swap_view`).
|
|
26
|
+
- **`tempest` CLI** — `dev` (simulator + hot reload/restart cockpit), `serve`
|
|
27
|
+
(LAN code-push to a device), `spec` (typed contract as JSON), `new` (scaffold
|
|
28
|
+
a project), `build` (bundle an app into an APK), `run` (build + install +
|
|
29
|
+
logcat).
|
|
30
|
+
- **Android runtime (Trilho B).** Official CPython 3.14 (PEP 738) embedded via a
|
|
31
|
+
hand-rolled JNI bridge in a Kotlin/Gradle host; native wheels (`pydantic-core`)
|
|
32
|
+
via cibuildwheel; a Jetpack Compose renderer; a LAN dev server with QR pairing;
|
|
33
|
+
and native capabilities (notifications).
|
|
34
|
+
- **Conformance suite** pinning the Qt and Compose `Style` translators with
|
|
35
|
+
golden snapshots (phase D).
|
|
36
|
+
|
|
37
|
+
[Unreleased]: https://github.com/mauriciobenjamin700/tempestroid/compare/v0.1.0...HEAD
|
|
38
|
+
[0.1.0]: https://github.com/mauriciobenjamin700/tempestroid/releases/tag/v0.1.0
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Mauricio Benjamin
|
|
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.
|
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tempestroid
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Framework for building native Android apps in typed Python — declarative typed UI tree, Qt simulator + Compose device renderers.
|
|
5
|
+
Project-URL: Homepage, https://github.com/mauriciobenjamin700/tempestroid
|
|
6
|
+
Project-URL: Documentation, https://mauriciobenjamin700.github.io/tempestroid/
|
|
7
|
+
Project-URL: Repository, https://github.com/mauriciobenjamin700/tempestroid
|
|
8
|
+
Project-URL: Changelog, https://github.com/mauriciobenjamin700/tempestroid/blob/main/CHANGELOG.md
|
|
9
|
+
Project-URL: Issues, https://github.com/mauriciobenjamin700/tempestroid/issues
|
|
10
|
+
Author: Mauricio Benjamin
|
|
11
|
+
License-Expression: MIT
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Keywords: android,compose,cross-platform,declarative,framework,mobile,pydantic,qt,ui
|
|
14
|
+
Classifier: Development Status :: 4 - Beta
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: Operating System :: Android
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
|
|
23
|
+
Classifier: Topic :: Software Development :: User Interfaces
|
|
24
|
+
Classifier: Typing :: Typed
|
|
25
|
+
Requires-Python: >=3.11
|
|
26
|
+
Requires-Dist: pydantic>=2.7
|
|
27
|
+
Provides-Extra: qt
|
|
28
|
+
Requires-Dist: pyside6>=6.7; extra == 'qt'
|
|
29
|
+
Requires-Dist: qasync>=0.27; extra == 'qt'
|
|
30
|
+
Description-Content-Type: text/markdown
|
|
31
|
+
|
|
32
|
+
# tempestroid
|
|
33
|
+
|
|
34
|
+
> 📖 **Documentação / Docs:** site MkDocs bilíngue em [`docs/`](docs/index.md)
|
|
35
|
+
> com seletor **PT-BR / EN-US** no header — rode `uv run mkdocs serve` e abra
|
|
36
|
+
> <http://127.0.0.1:8000> (PT) ou <http://127.0.0.1:8000/en/> (EN). Guia do
|
|
37
|
+
> usuário, arquitetura e referência da API.
|
|
38
|
+
|
|
39
|
+
Build **native Android apps** in **typed Python**.
|
|
40
|
+
|
|
41
|
+
You write one declarative, fully typed widget tree (a Pydantic IR). A
|
|
42
|
+
**renderer-agnostic reconciler** diffs it into patches. Two leaf renderers apply
|
|
43
|
+
those patches: **Qt** for the desktop simulator, **Jetpack Compose** for the
|
|
44
|
+
device. The runtime is **async-first**, with an Expo-style dev loop: hot reload
|
|
45
|
+
in the Qt simulator and LAN code-push to a device over QR — both shipping today.
|
|
46
|
+
|
|
47
|
+
> This is a **framework, not a web service** — no FastAPI, SQLAlchemy, Redis, or
|
|
48
|
+
> HTTP layering. See [`docs/plan.md`](docs/plan.md) for the full design and the
|
|
49
|
+
> phase roadmap.
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## Why
|
|
54
|
+
|
|
55
|
+
- **Typed end to end.** Style model, widget primitives, events, and the
|
|
56
|
+
Python↔Kotlin boundary contract are all Pydantic v2 / fully typed. `pyright`
|
|
57
|
+
runs in strict mode.
|
|
58
|
+
- **One tree, two targets.** The reconciler is pure data-in → patches-out. All
|
|
59
|
+
platform divergence is confined to the two `Style` translators (Qt today,
|
|
60
|
+
Compose next).
|
|
61
|
+
- **Async-first.** Event handlers and lifecycle hooks may be sync or `async`;
|
|
62
|
+
Python runs on a background asyncio loop, never the UI thread.
|
|
63
|
+
- **Fast inner loop.** `tempest dev` watches your file and hot-restarts the Qt
|
|
64
|
+
simulator on save — no device or emulator needed for UI work.
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## How it works
|
|
69
|
+
|
|
70
|
+
```text
|
|
71
|
+
view(app) ──build──▶ Node tree (IR)
|
|
72
|
+
│
|
|
73
|
+
diff pure, renderer-agnostic
|
|
74
|
+
▼
|
|
75
|
+
[ Patch ] Insert / Remove / Update / Reorder / Replace
|
|
76
|
+
╱ ╲
|
|
77
|
+
Qt renderer Compose renderer
|
|
78
|
+
(simulator) (device, B4)
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
1. `view(app) -> Widget` builds a declarative widget tree from current state.
|
|
82
|
+
2. `build` lowers it to a `Node` IR; `diff` compares old vs. new and emits a
|
|
83
|
+
minimal `Patch` list.
|
|
84
|
+
3. A renderer applies patches to live widgets. State changes coalesce into one
|
|
85
|
+
rebuild per tick.
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## Install
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
uv sync # core + dev tooling + the Qt simulator
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
End users embedding the framework who want the simulator:
|
|
96
|
+
`pip install tempestroid[qt]`. The framework **core** needs only `pydantic` —
|
|
97
|
+
Qt is an optional extra.
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## Quick start
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
from dataclasses import dataclass
|
|
105
|
+
|
|
106
|
+
from tempestroid import App, Button, Column, Style, Text, Widget
|
|
107
|
+
from tempestroid.renderers.qt import run_qt
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@dataclass
|
|
111
|
+
class CounterState:
|
|
112
|
+
value: int = 0
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def make_state() -> CounterState:
|
|
116
|
+
return CounterState()
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def view(app: App[CounterState]) -> Widget:
|
|
120
|
+
def increment() -> None:
|
|
121
|
+
app.set_state(lambda s: setattr(s, "value", s.value + 1))
|
|
122
|
+
|
|
123
|
+
return Column(
|
|
124
|
+
style=Style(gap=8.0),
|
|
125
|
+
children=[
|
|
126
|
+
Text(content=f"Count: {app.state.value}", key="label"),
|
|
127
|
+
Button(label="+", on_click=increment, key="inc"),
|
|
128
|
+
],
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
if __name__ == "__main__":
|
|
133
|
+
raise SystemExit(run_qt(make_state(), view, title="counter"))
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Full example with sync **and** `async` handlers:
|
|
137
|
+
[`examples/counter/app.py`](examples/counter/app.py).
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
## Gallery
|
|
142
|
+
|
|
143
|
+
A set of runnable example apps lives in [`examples/`](examples/README.md). Each
|
|
144
|
+
exposes the same `make_state()` + `view(app)` contract, so it runs in the Qt
|
|
145
|
+
simulator (`uv run python examples/<name>/app.py`) **and** on a device via
|
|
146
|
+
code-push (`uv run tempest serve examples/<name>/app.py`) with no changes.
|
|
147
|
+
|
|
148
|
+
| App | What it shows |
|
|
149
|
+
|---|---|
|
|
150
|
+
| [`counter`](examples/counter/app.py) | Sync + `async` handlers, the basics. |
|
|
151
|
+
| [`todo`](examples/todo/app.py) | Type-to-add list — `Input` + `insert` / `remove` / `update` patches. |
|
|
152
|
+
| [`calculator`](examples/calculator/app.py) | Dense nested `Row`/`Column` button grid. |
|
|
153
|
+
| [`stopwatch`](examples/stopwatch/app.py) | Async loop ticking the UI via `asyncio.sleep`. |
|
|
154
|
+
| [`colorpicker`](examples/colorpicker/app.py) | Dynamic `Style` updates (swatches + toggles). |
|
|
155
|
+
| [`form`](examples/form/app.py) | The value-bearing inputs (`Input` / `Checkbox` / `DatePicker` / `FilePicker`) + their typed change events. |
|
|
156
|
+
| [`gallery`](examples/gallery/app.py) | The expanded set — `Slider` / `Switch` / `ProgressBar` / `Spinner` / `Image` / `Icon` / `ScrollView`, secure + regex + multiline text fields, and a `Style.transition`. |
|
|
157
|
+
|
|
158
|
+
The framework and the Qt simulator support the full widget set, including the
|
|
159
|
+
value-bearing inputs and the utility widgets (`Slider` / `Switch` /
|
|
160
|
+
`ProgressBar` / `Spinner` / `Image` / `Icon` / `ScrollView` / `TextArea`). The
|
|
161
|
+
device (Compose) renderer renders `Text` / `Button` / `Column` / `Row` /
|
|
162
|
+
`Container` plus the value widgets `Input` / `Checkbox` / `DatePicker` /
|
|
163
|
+
`FilePicker` (with their typed change events); the remaining utility widgets stay
|
|
164
|
+
empty-box on device until the Kotlin host grows the matching cases (see
|
|
165
|
+
[`examples/README.md`](examples/README.md)).
|
|
166
|
+
|
|
167
|
+
---
|
|
168
|
+
|
|
169
|
+
## CLI
|
|
170
|
+
|
|
171
|
+
```bash
|
|
172
|
+
uv run tempest new MyApp # scaffold a new app project
|
|
173
|
+
uv run python examples/counter/app.py # run an app directly in the Qt simulator
|
|
174
|
+
uv run tempest dev examples/counter/app.py # dev loop: edit + save → hot reload (state preserved)
|
|
175
|
+
uv run tempest serve examples/device_counter/app.py # push to a device over LAN, no APK rebuild
|
|
176
|
+
uv run tempest build MyApp/app.py # bundle the app into an APK
|
|
177
|
+
uv run tempest run MyApp/app.py # build + install on a device + stream logs
|
|
178
|
+
uv run tempest spec # print the typed contract (widgets/events) as JSON
|
|
179
|
+
uv run tempest --help
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
`tempest dev` cockpit commands: `r` (hot reload, state preserved), `R` (hot
|
|
183
|
+
restart, clean state), `s` (raise window), `q` (quit). Saving the file
|
|
184
|
+
hot-reloads; a reload incompatible with the live state falls back to a clean
|
|
185
|
+
restart. `tempest build`/`run` drive the `android-host` Gradle project + `adb`,
|
|
186
|
+
so they need an Android SDK/NDK and a checkout of the host tree.
|
|
187
|
+
|
|
188
|
+
| Command | Status | Notes |
|
|
189
|
+
|---|---|---|
|
|
190
|
+
| `tempest new <name>` | ✅ | Scaffold a runnable app project |
|
|
191
|
+
| `tempest dev <app>` | ✅ | Simulator + hot reload / hot restart (needs `qt` extra) |
|
|
192
|
+
| `tempest serve <app>` | ✅ | LAN code-push to a device + log relay (phase B5) |
|
|
193
|
+
| `tempest spec` | ✅ | Typed widget/event contract as JSON |
|
|
194
|
+
| `tempest build <app>` | ✅ | Bundle an app into an APK (needs Android SDK/NDK) |
|
|
195
|
+
| `tempest run <app>` | ✅ | Build + install on a device + stream logs |
|
|
196
|
+
|
|
197
|
+
---
|
|
198
|
+
|
|
199
|
+
## Public API
|
|
200
|
+
|
|
201
|
+
Everything below is importable from the top-level `tempestroid` package.
|
|
202
|
+
|
|
203
|
+
### Style (`tempestroid.style`)
|
|
204
|
+
|
|
205
|
+
Frozen Pydantic value objects, diffed by value.
|
|
206
|
+
|
|
207
|
+
- **`Style`** — the style model (layout, box model, paint, typography, sizing,
|
|
208
|
+
effects, animation). Notable fields: `opacity`, `shadow`, `align_self`,
|
|
209
|
+
`letter_spacing`, `line_height`, `max_lines`, `text_overflow`, `aspect_ratio`.
|
|
210
|
+
- **`Color`** — `Color.from_hex("#101418")`.
|
|
211
|
+
- **`Edge`** — insets; `Edge.all(24.0)`.
|
|
212
|
+
- **`Border`** (uniform) / **`SideBorder`** (per-side, e.g. a bottom divider).
|
|
213
|
+
- **`Corners`** — per-corner radii for `Style.radius` (e.g. top-rounded sheets).
|
|
214
|
+
- **`Shadow`** — `box-shadow` / elevation (`color` / `blur` / `offset_x` /
|
|
215
|
+
`offset_y`); Compose maps it to elevation, Qt to a `QGraphicsDropShadowEffect`.
|
|
216
|
+
- **`Gradient`** + **`GradientStop`** — a linear gradient usable wherever a
|
|
217
|
+
background `Color` is (QSS `qlineargradient` / Compose `Brush`).
|
|
218
|
+
- **`Transition`** — implicit animation (`duration_ms` / `curve` / `delay_ms`):
|
|
219
|
+
on rebuild the renderer tweens changed visual props instead of snapping
|
|
220
|
+
(Compose maps it to `animate*AsState`; Qt animation is renderer-imperative).
|
|
221
|
+
- Enums: **`FlexDirection`**, **`JustifyContent`**, **`AlignItems`**,
|
|
222
|
+
**`TextAlign`**, **`FontWeight`**, **`FontStyle`**, **`TextDecoration`**,
|
|
223
|
+
**`TextOverflow`**, **`GradientDirection`**, **`Curve`** (easing).
|
|
224
|
+
|
|
225
|
+
### Widgets (`tempestroid.widgets`)
|
|
226
|
+
|
|
227
|
+
The declarative IR — bare-noun widgets.
|
|
228
|
+
|
|
229
|
+
- **`Widget`** (base), **`Text`**, **`Button`**, **`Column`**, **`Row`**,
|
|
230
|
+
**`Container`**, **`ScrollView`** (scrollable container).
|
|
231
|
+
- Value-bearing inputs: **`Input`** (text — with `secure` password masking +
|
|
232
|
+
reveal toggle, regex `pattern`, `keyboard` type, `max_length`), **`TextArea`**
|
|
233
|
+
(multi-line), **`Checkbox`** (boolean), **`Switch`** (boolean toggle),
|
|
234
|
+
**`Slider`** (numeric range), **`DatePicker`** (ISO date), **`FilePicker`**
|
|
235
|
+
(file selection).
|
|
236
|
+
- Presentation widgets: **`Image`** (URL/asset, `fit`), **`Icon`** (named glyph),
|
|
237
|
+
**`ProgressBar`** (determinate/indeterminate), **`Spinner`** (activity).
|
|
238
|
+
- Enums: **`KeyboardType`** (text/number/email/phone/url/password),
|
|
239
|
+
**`ImageFit`** (contain/cover/fill/none).
|
|
240
|
+
- **`EventHandler`** — the typed handler-prop wrapper used by every handler field
|
|
241
|
+
(`on_click`, `on_change`, `on_select`); sync or `async`, zero- or one-argument.
|
|
242
|
+
|
|
243
|
+
### Events (`tempestroid.widgets`) — typed boundary contract
|
|
244
|
+
|
|
245
|
+
- **`Event`** (base), **`TapEvent`**, **`TextChangeEvent`** (carries `valid`
|
|
246
|
+
against the input's `pattern`), **`ToggleEvent`**, **`SlideEvent`**,
|
|
247
|
+
**`DateChangeEvent`**, **`FileSelectEvent`**.
|
|
248
|
+
- **`parse_event(event_type, raw)`** — boundary gate: validates a raw payload
|
|
249
|
+
into a typed event or raises **`EventValidationError`** with structured field
|
|
250
|
+
errors. This is the Python↔Kotlin contract for the device bridge. The bridge
|
|
251
|
+
passes the validated event to handlers that accept a positional argument.
|
|
252
|
+
|
|
253
|
+
### Core — IR + reconciler (`tempestroid.core`)
|
|
254
|
+
|
|
255
|
+
- **`Node`**, **`Path`** — the lowered IR.
|
|
256
|
+
- Patches: **`Insert`**, **`Remove`**, **`Update`**, **`Reorder`**,
|
|
257
|
+
**`Replace`**, and the **`Patch`** union.
|
|
258
|
+
- **`build(widget) -> Node`**, **`diff(old, new) -> list[Patch]`**.
|
|
259
|
+
- **`App[S]`** — renderer-agnostic state container: owns state, builds via
|
|
260
|
+
`view(app)`, diffs, hands patches to an `apply_patches` callback.
|
|
261
|
+
|
|
262
|
+
### Introspection (`tempestroid.core`)
|
|
263
|
+
|
|
264
|
+
- **`introspect()`** — full JSON contract `{"widgets": {...}, "events": {...}}`
|
|
265
|
+
(powers `tempest spec`).
|
|
266
|
+
- **`widget_catalog()`**, **`event_catalog()`**.
|
|
267
|
+
|
|
268
|
+
### Renderer (`tempestroid.renderers.qt`, needs `qt` extra)
|
|
269
|
+
|
|
270
|
+
- **`run_qt(state, view, *, title, size)`** — run an app in the Qt simulator.
|
|
271
|
+
- **`run_dev(app_path)`** — the `tempest dev` cockpit.
|
|
272
|
+
|
|
273
|
+
### Compose + bridge — device side (phases B3/B4)
|
|
274
|
+
|
|
275
|
+
The Python half is device-independent and tested without a phone; the JNI
|
|
276
|
+
transport (B3) and the Kotlin Compose renderer (B4) are implemented in
|
|
277
|
+
`android-host/` and verified on a real arm64 device.
|
|
278
|
+
|
|
279
|
+
- **`to_compose(style)`** (`tempestroid.renderers.compose`) — serializable
|
|
280
|
+
`Style → Compose` spec; the second `Style` translator (pairs with `Style → Qt`).
|
|
281
|
+
- **`serialize_node` / `serialize_patch`** — lower the IR/patches to JSON-able
|
|
282
|
+
dicts (handlers → path tokens, style → Compose spec).
|
|
283
|
+
- **`MountMessage` / `PatchMessage` / `EventMessage`** — the wire protocol across
|
|
284
|
+
the bridge: `mount` carries the full serialized tree, `patch` an incremental
|
|
285
|
+
patch list, `event` a device→Python callback addressed by handler token.
|
|
286
|
+
- **`DeviceApp`** + **`Bridge`** / **`LoopbackBridge`** — wire an `App` to a
|
|
287
|
+
device transport; the device-side analogue of `run_qt`. Events come back by
|
|
288
|
+
handler token, are validated by `parse_event`, and trigger coalesced patches.
|
|
289
|
+
- **`JniBridge`** + **`run_device`** — the real on-device transport (phase B3):
|
|
290
|
+
`JniBridge` ships messages to Kotlin via the native `_tempest_host` module;
|
|
291
|
+
`run_device(state, view)` boots a `DeviceApp` on a fresh asyncio loop and
|
|
292
|
+
marshals incoming events back onto it. Imports cleanly off-device (the native
|
|
293
|
+
module is loaded lazily), so the framework still develops/tests on the desktop.
|
|
294
|
+
|
|
295
|
+
### Dev server — LAN code-push (phase B5)
|
|
296
|
+
|
|
297
|
+
The Expo-style on-device inner loop: edit on the dev machine, hot-restart on the
|
|
298
|
+
phone without rebuilding the APK (`tempest serve <app>`).
|
|
299
|
+
|
|
300
|
+
- **`DevServer`** — serves the app source (`/version`, `/app`) and relays device
|
|
301
|
+
logs (`/log`) over HTTP.
|
|
302
|
+
- **`run_dev_client`** — the device poll loop: fetch on change → re-exec source →
|
|
303
|
+
hot-restart the `DeviceApp` (transport/fetch injected, so it's desktop-testable).
|
|
304
|
+
- **`serve_device(url)`** — device entry point wiring the real `JniBridge` + the
|
|
305
|
+
native sink + an `urllib` fetch into `run_dev_client`.
|
|
306
|
+
- **`render_qr(url)`** — ASCII QR for pairing (falls back to the plain URL).
|
|
307
|
+
|
|
308
|
+
### Native capabilities (phase B6)
|
|
309
|
+
|
|
310
|
+
Device-native features driven from Python as `{"kind": "native"}` commands the
|
|
311
|
+
Kotlin host routes to capability modules. Verified on device.
|
|
312
|
+
|
|
313
|
+
- **`notify(title, body="")`** — post a system notification from a handler.
|
|
314
|
+
The extension pattern (`native_command` envelope + a host module router) is in
|
|
315
|
+
place for further capabilities (camera, sensors, …).
|
|
316
|
+
|
|
317
|
+
---
|
|
318
|
+
|
|
319
|
+
## Project layout
|
|
320
|
+
|
|
321
|
+
```text
|
|
322
|
+
tempestroid/
|
|
323
|
+
├── style.py # Style + value objects (Color/Edge/Border/Corners/Shadow/Gradient/Transition) + enums (frozen Pydantic)
|
|
324
|
+
├── widgets/ # Widget base + layout/inputs/media/indicators widgets + events.py
|
|
325
|
+
├── core/ # ir.py, reconciler.py, state.py, introspection.py
|
|
326
|
+
├── renderers/qt/ # renderer, Style→Qt, run_qt, simulator, dev_loop
|
|
327
|
+
├── renderers/compose/ # Style→Compose translator (device renderer, Python side)
|
|
328
|
+
├── bridge/ # IR/patch serialization, handler registry, DeviceApp
|
|
329
|
+
└── cli/ # tempest entry point + app_loader + watcher
|
|
330
|
+
|
|
331
|
+
# Trilho B (Android), outside the Python package:
|
|
332
|
+
docs/research/ # web research + executable B0–B6 runbook
|
|
333
|
+
toolchain/ # fetch CPython 3.14 + cibuildwheel native wheels
|
|
334
|
+
android-host/ # Gradle/Kotlin host embedding official CPython via JNI
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
---
|
|
338
|
+
|
|
339
|
+
## Status
|
|
340
|
+
|
|
341
|
+
Track A (pure desktop CPython) is **complete: A0–A6**.
|
|
342
|
+
|
|
343
|
+
| Phase | Scope | Status |
|
|
344
|
+
|---|---|---|
|
|
345
|
+
| A0 | Foundation: package, tooling, `tempest --help` | ✅ |
|
|
346
|
+
| A1 | Style model + typed widget primitives | ✅ |
|
|
347
|
+
| A2 | Reconciler: `build → diff → patch` | ✅ |
|
|
348
|
+
| A3 | Qt renderer: patches → `QWidget`s, `Style → Qt` | ✅ |
|
|
349
|
+
| A4 | Async event loop: asyncio ⨉ Qt (`qasync`) | ✅ |
|
|
350
|
+
| A5 | `tempest dev`: watcher, hot restart, command loop | ✅ |
|
|
351
|
+
| A6 | Typed event contract + introspection | ✅ |
|
|
352
|
+
| B0–B6 | Android runtime: CPython 3.14 arm64, native wheels, Kotlin host, JNI bridge, Compose renderer, LAN code-push, native capabilities | ✅ |
|
|
353
|
+
| C | Polish: `new`/`build`/`run` + stateful hot reload | ✅ |
|
|
354
|
+
| D | Conformance golden snapshots (Qt vs Compose) | ✅ |
|
|
355
|
+
|
|
356
|
+
---
|
|
357
|
+
|
|
358
|
+
## Develop
|
|
359
|
+
|
|
360
|
+
```bash
|
|
361
|
+
uv run ruff check .
|
|
362
|
+
uv run pyright # strict mode
|
|
363
|
+
uv run pytest
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
Conventions: double quotes everywhere, every parameter/return/annotation typed,
|
|
367
|
+
Google-style English docstrings, absolute imports re-exported from each
|
|
368
|
+
`__init__.py`. See [`CLAUDE.md`](CLAUDE.md) for the full set.
|