situ 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.
- situ-0.1.0/LICENSE +21 -0
- situ-0.1.0/PKG-INFO +185 -0
- situ-0.1.0/README.md +149 -0
- situ-0.1.0/pyproject.toml +118 -0
- situ-0.1.0/src/situ/__init__.py +99 -0
- situ-0.1.0/src/situ/check.py +63 -0
- situ-0.1.0/src/situ/compiler/__init__.py +2 -0
- situ-0.1.0/src/situ/compiler/emitter/__init__.py +21 -0
- situ-0.1.0/src/situ/compiler/emitter/binders.py +267 -0
- situ-0.1.0/src/situ/compiler/emitter/core.py +480 -0
- situ-0.1.0/src/situ/compiler/emitter/ctx.py +34 -0
- situ-0.1.0/src/situ/compiler/emitter/expr.py +349 -0
- situ-0.1.0/src/situ/compiler/emitter/stmt.py +290 -0
- situ-0.1.0/src/situ/compiler/frontend/__init__.py +20 -0
- situ-0.1.0/src/situ/compiler/frontend/base.py +142 -0
- situ-0.1.0/src/situ/compiler/frontend/compose.py +750 -0
- situ-0.1.0/src/situ/compiler/frontend/scopes.py +159 -0
- situ-0.1.0/src/situ/compiler/frontend/styles.py +221 -0
- situ-0.1.0/src/situ/compiler/markers.py +59 -0
- situ-0.1.0/src/situ/compiler/resolve.py +139 -0
- situ-0.1.0/src/situ/compiler/unified.py +246 -0
- situ-0.1.0/src/situ/declui/__init__.py +50 -0
- situ-0.1.0/src/situ/declui/generate.py +1828 -0
- situ-0.1.0/src/situ/declui/mount.py +192 -0
- situ-0.1.0/src/situ/declui/predicates.py +354 -0
- situ-0.1.0/src/situ/declui/spec.py +93 -0
- situ-0.1.0/src/situ/infra/__init__.py +2 -0
- situ-0.1.0/src/situ/infra/db.py +26 -0
- situ-0.1.0/src/situ/infra/di.py +21 -0
- situ-0.1.0/src/situ/infra/templating.py +30 -0
- situ-0.1.0/src/situ/mount/__init__.py +37 -0
- situ-0.1.0/src/situ/mount/core.py +235 -0
- situ-0.1.0/src/situ/mount/factories.py +309 -0
- situ-0.1.0/src/situ/mount/flask.py +145 -0
- situ-0.1.0/src/situ/mount/runtime.py +72 -0
- situ-0.1.0/src/situ/py.typed +0 -0
- situ-0.1.0/src/situ/siting/__init__.py +1 -0
- situ-0.1.0/src/situ/siting/runtime.py +74 -0
- situ-0.1.0/src/situ/static/_rt.js +509 -0
- situ-0.1.0/src/situ/static/declui.css +236 -0
- situ-0.1.0/src/situ/templates/page.html +23 -0
situ-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Stéphane Fermigier
|
|
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.
|
situ-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: situ
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Site your state, derive the wire — a Python→JS component compiler with an explicit client/server seam, for Litestar.
|
|
5
|
+
Keywords: litestar,hypermedia,compiler,reactive,signals,web-ui,frontend,python-to-javascript
|
|
6
|
+
Author: Stéphane Fermigier
|
|
7
|
+
Author-email: Stéphane Fermigier <sfermigier@gmail.com>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Framework :: Litestar
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
|
|
18
|
+
Classifier: Topic :: Software Development :: Code Generators
|
|
19
|
+
Classifier: Typing :: Typed
|
|
20
|
+
Requires-Dist: litestar>=2.22
|
|
21
|
+
Requires-Dist: jinja2>=3.1
|
|
22
|
+
Requires-Dist: flask>=3.0 ; extra == 'flask'
|
|
23
|
+
Requires-Dist: attrs>=23.0 ; extra == 'model-adapters'
|
|
24
|
+
Requires-Dist: msgspec>=0.18 ; extra == 'model-adapters'
|
|
25
|
+
Requires-Dist: pydantic>=2.0 ; extra == 'model-adapters'
|
|
26
|
+
Requires-Dist: sqlalchemy>=2.0.50 ; extra == 'sqlalchemy'
|
|
27
|
+
Requires-Dist: aiosqlite>=0.20.0 ; extra == 'sqlalchemy'
|
|
28
|
+
Requires-Dist: dishka>=1.6 ; extra == 'sqlalchemy'
|
|
29
|
+
Requires-Dist: greenlet>=3.0 ; extra == 'sqlalchemy'
|
|
30
|
+
Requires-Python: >=3.12
|
|
31
|
+
Project-URL: Homepage, https://situ.hop3.abilian.com/
|
|
32
|
+
Provides-Extra: flask
|
|
33
|
+
Provides-Extra: model-adapters
|
|
34
|
+
Provides-Extra: sqlalchemy
|
|
35
|
+
Description-Content-Type: text/markdown
|
|
36
|
+
|
|
37
|
+
<p align="center">
|
|
38
|
+
<img src="assets/situ-wordmark.svg" alt="situ" width="220">
|
|
39
|
+
</p>
|
|
40
|
+
|
|
41
|
+
<p align="center"><em>Site your state, derive the wire.</em></p>
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
**situ** lets a Python developer write a reactive web UI as one component — real HTML on
|
|
46
|
+
top, Python signals and handlers below — and declare, per piece of state, *where it lives*.
|
|
47
|
+
A real Python→JS compiler reads that and emits a small, app-specific client island; the
|
|
48
|
+
client/server boundary stays explicit and is enforced at compile time. You author no
|
|
49
|
+
client JavaScript, and you can read every line the compiler ships.
|
|
50
|
+
|
|
51
|
+
The compiler and the mount core are framework-neutral; situ ships a
|
|
52
|
+
[Litestar](https://litestar.dev) adapter (the reference) and a Flask one, and a component
|
|
53
|
+
mounts on either unchanged. It is for Python full-stack and backend developers who reach for
|
|
54
|
+
hypermedia (htmx, Datastar) and hit the wall where a piece of state wants to live in the
|
|
55
|
+
browser — a live search, a selection, an open dialog.
|
|
56
|
+
|
|
57
|
+
> situ is a young, alpha library extracted from a research repo
|
|
58
|
+
> ([webui](https://github.com/sfermigier/webui)); the compiler accepts a **bounded**
|
|
59
|
+
> dialect of Python and fails *closed* on anything outside it. See *Status* below.
|
|
60
|
+
|
|
61
|
+
## The one idea: site each piece of state
|
|
62
|
+
|
|
63
|
+
Every signal declares its **site**, and the transport follows from that — you never write
|
|
64
|
+
a fetch, a target, or an SSE wire.
|
|
65
|
+
|
|
66
|
+
| Site | Where it lives | What the compiler derives |
|
|
67
|
+
|------|----------------|---------------------------|
|
|
68
|
+
| `Local[T]` | the browser | compiled to JS, zero network |
|
|
69
|
+
| `Url[T]` | the query string | a shareable link the server re-renders |
|
|
70
|
+
| `Server[Facade]` | the database | a POST command that morphs one region |
|
|
71
|
+
| `Synced[T]` | a local-first replica | a store reconciled by a sync engine |
|
|
72
|
+
|
|
73
|
+
The boundary is a **compile-time invariant**: a client read of `Server`-sited state is a
|
|
74
|
+
`CompileError`, so database-backed state cannot reach the browser by accident.
|
|
75
|
+
|
|
76
|
+
## Install
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
pip install situ # the compiler + the Litestar mount
|
|
80
|
+
pip install "situ[flask]" # + the Flask (WSGI) mount adapter
|
|
81
|
+
pip install "situ[sqlalchemy]" # + the optional SQLAlchemy/Dishka session helpers
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Requires Python 3.12+.
|
|
85
|
+
|
|
86
|
+
## Quickstart
|
|
87
|
+
|
|
88
|
+
A component is **two sibling files sharing a stem**, so each gets native editor tooling.
|
|
89
|
+
|
|
90
|
+
`counter.py` — the signals and handlers:
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
from situ import Local
|
|
94
|
+
|
|
95
|
+
count: Local[int] = 0 # client state — compiled to JS, no network
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def bump() -> None:
|
|
99
|
+
global count # names the local signal this handler writes
|
|
100
|
+
count = count + 1
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
`counter.html` — real HTML with shorthand reactive attributes:
|
|
104
|
+
|
|
105
|
+
```html
|
|
106
|
+
<div data-region>
|
|
107
|
+
<button @click="bump">+1</button>
|
|
108
|
+
<strong :text="count"></strong>
|
|
109
|
+
</div>
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
`app.py` — the controller is one mount call plus the wiring:
|
|
113
|
+
|
|
114
|
+
```python
|
|
115
|
+
from pathlib import Path
|
|
116
|
+
|
|
117
|
+
import situ
|
|
118
|
+
from litestar import Litestar
|
|
119
|
+
from litestar.plugins.jinja import JinjaTemplateEngine
|
|
120
|
+
from litestar.static_files import create_static_files_router
|
|
121
|
+
from litestar.template.config import TemplateConfig
|
|
122
|
+
from situ import mount_static_component
|
|
123
|
+
|
|
124
|
+
HERE = Path(__file__).parent
|
|
125
|
+
|
|
126
|
+
app = Litestar(
|
|
127
|
+
route_handlers=[
|
|
128
|
+
mount_static_component(
|
|
129
|
+
path="/counter",
|
|
130
|
+
stem=HERE / "counter", # counter.py + counter.html
|
|
131
|
+
template="page.html", # situ ships a minimal default
|
|
132
|
+
meta={"name": "Counter"},
|
|
133
|
+
),
|
|
134
|
+
# serve the runtime shim the generated island loads from /static/_rt.js
|
|
135
|
+
create_static_files_router(path="/static", directories=[situ.static_dir()]),
|
|
136
|
+
],
|
|
137
|
+
template_config=TemplateConfig(
|
|
138
|
+
directory=situ.templates_dir(), engine=JinjaTemplateEngine
|
|
139
|
+
),
|
|
140
|
+
)
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
litestar --app app:app run # then open http://localhost:8000/counter
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Two rules the runtime enforces: every reactive element lives inside `<header>` or the
|
|
148
|
+
single `<div data-region>` (where binders are wired at boot), and `data-region` must be
|
|
149
|
+
the first attribute on that `<div>`.
|
|
150
|
+
|
|
151
|
+
When a piece of state needs the database, declare it `Server[Facade]`, give the component a
|
|
152
|
+
`context` function and a facade, and switch to `mount_component` — the same component file,
|
|
153
|
+
now with a server seam. `mount_tree` composes a root component with typed children. To serve
|
|
154
|
+
the same compiled component on Flask instead, `situ.mount.flask.mount_flask` mounts it as a
|
|
155
|
+
`Blueprint` — see `examples/flask/`.
|
|
156
|
+
|
|
157
|
+
## What's in the box
|
|
158
|
+
|
|
159
|
+
- `situ.compiler` — the Python→JS compiler (`parse_front_end`, `load_front_end`,
|
|
160
|
+
`compile_app`, `splice_tree`) and the site markers. Pure standard library.
|
|
161
|
+
- `situ.mount` — a framework-neutral mount core (`situ.mount.core`: dispatch + render + the
|
|
162
|
+
command/region/feed/window protocol, no framework imported) with thin adapters over it: the
|
|
163
|
+
Litestar route factories (`mount_component`, `mount_static_component`, `mount_tree`) and a
|
|
164
|
+
Flask `Blueprint` adapter (`situ.mount.flask.mount_flask`). The Litestar bindings load
|
|
165
|
+
lazily, so the Flask path imports no Litestar.
|
|
166
|
+
- `situ.siting` — the `Signal` / `Signals` / `Site` contract.
|
|
167
|
+
- `situ.infra.templating` — a Jinja string-render helper (`render_string`); and, behind
|
|
168
|
+
`[sqlalchemy]`, `situ.infra.db` / `situ.infra.di` — a SQLAlchemy async engine + Dishka
|
|
169
|
+
session provider.
|
|
170
|
+
- `situ.static_dir()` / `situ.templates_dir()` — paths to the bundled `_rt.js` shim and
|
|
171
|
+
the default `page.html`, to wire into your Litestar static + template config.
|
|
172
|
+
|
|
173
|
+
## Status
|
|
174
|
+
|
|
175
|
+
Alpha. The compiler accepts a bounded dialect of Python and rejects the rest with a clear
|
|
176
|
+
error; it does not relocate database I/O to the client. Dishka is required only by the
|
|
177
|
+
Litestar `mount_component`, which reads `request.state.dishka_container` (the `[sqlalchemy]`
|
|
178
|
+
extra ships a ready provider, or wire your own); the Flask adapter takes a plain `resolve`
|
|
179
|
+
callable and needs no Dishka. The story behind the design — fifteen TodoMVC
|
|
180
|
+
implementations and a four-way master-detail comparison — lives in the
|
|
181
|
+
[webui research repo](https://github.com/sfermigier/webui).
|
|
182
|
+
|
|
183
|
+
## License
|
|
184
|
+
|
|
185
|
+
[MIT](LICENSE) © Stéphane Fermigier
|
situ-0.1.0/README.md
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="assets/situ-wordmark.svg" alt="situ" width="220">
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
<p align="center"><em>Site your state, derive the wire.</em></p>
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
**situ** lets a Python developer write a reactive web UI as one component — real HTML on
|
|
10
|
+
top, Python signals and handlers below — and declare, per piece of state, *where it lives*.
|
|
11
|
+
A real Python→JS compiler reads that and emits a small, app-specific client island; the
|
|
12
|
+
client/server boundary stays explicit and is enforced at compile time. You author no
|
|
13
|
+
client JavaScript, and you can read every line the compiler ships.
|
|
14
|
+
|
|
15
|
+
The compiler and the mount core are framework-neutral; situ ships a
|
|
16
|
+
[Litestar](https://litestar.dev) adapter (the reference) and a Flask one, and a component
|
|
17
|
+
mounts on either unchanged. It is for Python full-stack and backend developers who reach for
|
|
18
|
+
hypermedia (htmx, Datastar) and hit the wall where a piece of state wants to live in the
|
|
19
|
+
browser — a live search, a selection, an open dialog.
|
|
20
|
+
|
|
21
|
+
> situ is a young, alpha library extracted from a research repo
|
|
22
|
+
> ([webui](https://github.com/sfermigier/webui)); the compiler accepts a **bounded**
|
|
23
|
+
> dialect of Python and fails *closed* on anything outside it. See *Status* below.
|
|
24
|
+
|
|
25
|
+
## The one idea: site each piece of state
|
|
26
|
+
|
|
27
|
+
Every signal declares its **site**, and the transport follows from that — you never write
|
|
28
|
+
a fetch, a target, or an SSE wire.
|
|
29
|
+
|
|
30
|
+
| Site | Where it lives | What the compiler derives |
|
|
31
|
+
|------|----------------|---------------------------|
|
|
32
|
+
| `Local[T]` | the browser | compiled to JS, zero network |
|
|
33
|
+
| `Url[T]` | the query string | a shareable link the server re-renders |
|
|
34
|
+
| `Server[Facade]` | the database | a POST command that morphs one region |
|
|
35
|
+
| `Synced[T]` | a local-first replica | a store reconciled by a sync engine |
|
|
36
|
+
|
|
37
|
+
The boundary is a **compile-time invariant**: a client read of `Server`-sited state is a
|
|
38
|
+
`CompileError`, so database-backed state cannot reach the browser by accident.
|
|
39
|
+
|
|
40
|
+
## Install
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
pip install situ # the compiler + the Litestar mount
|
|
44
|
+
pip install "situ[flask]" # + the Flask (WSGI) mount adapter
|
|
45
|
+
pip install "situ[sqlalchemy]" # + the optional SQLAlchemy/Dishka session helpers
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Requires Python 3.12+.
|
|
49
|
+
|
|
50
|
+
## Quickstart
|
|
51
|
+
|
|
52
|
+
A component is **two sibling files sharing a stem**, so each gets native editor tooling.
|
|
53
|
+
|
|
54
|
+
`counter.py` — the signals and handlers:
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
from situ import Local
|
|
58
|
+
|
|
59
|
+
count: Local[int] = 0 # client state — compiled to JS, no network
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def bump() -> None:
|
|
63
|
+
global count # names the local signal this handler writes
|
|
64
|
+
count = count + 1
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
`counter.html` — real HTML with shorthand reactive attributes:
|
|
68
|
+
|
|
69
|
+
```html
|
|
70
|
+
<div data-region>
|
|
71
|
+
<button @click="bump">+1</button>
|
|
72
|
+
<strong :text="count"></strong>
|
|
73
|
+
</div>
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
`app.py` — the controller is one mount call plus the wiring:
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
from pathlib import Path
|
|
80
|
+
|
|
81
|
+
import situ
|
|
82
|
+
from litestar import Litestar
|
|
83
|
+
from litestar.plugins.jinja import JinjaTemplateEngine
|
|
84
|
+
from litestar.static_files import create_static_files_router
|
|
85
|
+
from litestar.template.config import TemplateConfig
|
|
86
|
+
from situ import mount_static_component
|
|
87
|
+
|
|
88
|
+
HERE = Path(__file__).parent
|
|
89
|
+
|
|
90
|
+
app = Litestar(
|
|
91
|
+
route_handlers=[
|
|
92
|
+
mount_static_component(
|
|
93
|
+
path="/counter",
|
|
94
|
+
stem=HERE / "counter", # counter.py + counter.html
|
|
95
|
+
template="page.html", # situ ships a minimal default
|
|
96
|
+
meta={"name": "Counter"},
|
|
97
|
+
),
|
|
98
|
+
# serve the runtime shim the generated island loads from /static/_rt.js
|
|
99
|
+
create_static_files_router(path="/static", directories=[situ.static_dir()]),
|
|
100
|
+
],
|
|
101
|
+
template_config=TemplateConfig(
|
|
102
|
+
directory=situ.templates_dir(), engine=JinjaTemplateEngine
|
|
103
|
+
),
|
|
104
|
+
)
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
litestar --app app:app run # then open http://localhost:8000/counter
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Two rules the runtime enforces: every reactive element lives inside `<header>` or the
|
|
112
|
+
single `<div data-region>` (where binders are wired at boot), and `data-region` must be
|
|
113
|
+
the first attribute on that `<div>`.
|
|
114
|
+
|
|
115
|
+
When a piece of state needs the database, declare it `Server[Facade]`, give the component a
|
|
116
|
+
`context` function and a facade, and switch to `mount_component` — the same component file,
|
|
117
|
+
now with a server seam. `mount_tree` composes a root component with typed children. To serve
|
|
118
|
+
the same compiled component on Flask instead, `situ.mount.flask.mount_flask` mounts it as a
|
|
119
|
+
`Blueprint` — see `examples/flask/`.
|
|
120
|
+
|
|
121
|
+
## What's in the box
|
|
122
|
+
|
|
123
|
+
- `situ.compiler` — the Python→JS compiler (`parse_front_end`, `load_front_end`,
|
|
124
|
+
`compile_app`, `splice_tree`) and the site markers. Pure standard library.
|
|
125
|
+
- `situ.mount` — a framework-neutral mount core (`situ.mount.core`: dispatch + render + the
|
|
126
|
+
command/region/feed/window protocol, no framework imported) with thin adapters over it: the
|
|
127
|
+
Litestar route factories (`mount_component`, `mount_static_component`, `mount_tree`) and a
|
|
128
|
+
Flask `Blueprint` adapter (`situ.mount.flask.mount_flask`). The Litestar bindings load
|
|
129
|
+
lazily, so the Flask path imports no Litestar.
|
|
130
|
+
- `situ.siting` — the `Signal` / `Signals` / `Site` contract.
|
|
131
|
+
- `situ.infra.templating` — a Jinja string-render helper (`render_string`); and, behind
|
|
132
|
+
`[sqlalchemy]`, `situ.infra.db` / `situ.infra.di` — a SQLAlchemy async engine + Dishka
|
|
133
|
+
session provider.
|
|
134
|
+
- `situ.static_dir()` / `situ.templates_dir()` — paths to the bundled `_rt.js` shim and
|
|
135
|
+
the default `page.html`, to wire into your Litestar static + template config.
|
|
136
|
+
|
|
137
|
+
## Status
|
|
138
|
+
|
|
139
|
+
Alpha. The compiler accepts a bounded dialect of Python and rejects the rest with a clear
|
|
140
|
+
error; it does not relocate database I/O to the client. Dishka is required only by the
|
|
141
|
+
Litestar `mount_component`, which reads `request.state.dishka_container` (the `[sqlalchemy]`
|
|
142
|
+
extra ships a ready provider, or wire your own); the Flask adapter takes a plain `resolve`
|
|
143
|
+
callable and needs no Dishka. The story behind the design — fifteen TodoMVC
|
|
144
|
+
implementations and a four-way master-detail comparison — lives in the
|
|
145
|
+
[webui research repo](https://github.com/sfermigier/webui).
|
|
146
|
+
|
|
147
|
+
## License
|
|
148
|
+
|
|
149
|
+
[MIT](LICENSE) © Stéphane Fermigier
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "situ"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Site your state, derive the wire — a Python→JS component compiler with an explicit client/server seam, for Litestar."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.12"
|
|
7
|
+
license = "MIT"
|
|
8
|
+
license-files = ["LICENSE"]
|
|
9
|
+
authors = [{ name = "Stéphane Fermigier", email = "sfermigier@gmail.com" }]
|
|
10
|
+
keywords = [
|
|
11
|
+
"litestar",
|
|
12
|
+
"hypermedia",
|
|
13
|
+
"compiler",
|
|
14
|
+
"reactive",
|
|
15
|
+
"signals",
|
|
16
|
+
"web-ui",
|
|
17
|
+
"frontend",
|
|
18
|
+
"python-to-javascript",
|
|
19
|
+
]
|
|
20
|
+
classifiers = [
|
|
21
|
+
"Development Status :: 3 - Alpha",
|
|
22
|
+
"Framework :: Litestar",
|
|
23
|
+
"Intended Audience :: Developers",
|
|
24
|
+
"Operating System :: OS Independent",
|
|
25
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
26
|
+
"Programming Language :: Python :: 3.12",
|
|
27
|
+
"Programming Language :: Python :: 3.13",
|
|
28
|
+
"Topic :: Internet :: WWW/HTTP :: Dynamic Content",
|
|
29
|
+
"Topic :: Software Development :: Code Generators",
|
|
30
|
+
"Typing :: Typed",
|
|
31
|
+
]
|
|
32
|
+
# The siting runtime + the compiler are pure stdlib; litestar + jinja2 are needed only
|
|
33
|
+
# for the `mount` route factories + the string-render helper, which are the common path.
|
|
34
|
+
dependencies = [
|
|
35
|
+
"litestar>=2.22",
|
|
36
|
+
"jinja2>=3.1",
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
[project.optional-dependencies]
|
|
40
|
+
# The bundled SQLAlchemy async engine + Dishka session provider (situ.infra.db / .di).
|
|
41
|
+
# Optional: mount_component reads `request.state.dishka_container` as a runtime contract,
|
|
42
|
+
# but you can wire your own DI/DB instead of these helpers.
|
|
43
|
+
sqlalchemy = [
|
|
44
|
+
"sqlalchemy>=2.0.50",
|
|
45
|
+
"aiosqlite>=0.20.0",
|
|
46
|
+
"dishka>=1.6",
|
|
47
|
+
"greenlet>=3.0", # required by SQLAlchemy's async engine
|
|
48
|
+
]
|
|
49
|
+
# declui's model-source adapters (`situ.declui.read_model`): generate a form from an attrs / msgspec /
|
|
50
|
+
# Pydantic model, not only a @dataclass. Each is imported lazily, only when such a model is used.
|
|
51
|
+
model-adapters = [
|
|
52
|
+
"attrs>=23.0",
|
|
53
|
+
"msgspec>=0.18",
|
|
54
|
+
"pydantic>=2.0",
|
|
55
|
+
]
|
|
56
|
+
# Serve a compiled situ component from a Flask (WSGI) app via `situ.mount.flask.mount_flask` — the
|
|
57
|
+
# proof that the mount is framework-neutral (no Litestar / Dishka on the path). See examples/flask/.
|
|
58
|
+
flask = [
|
|
59
|
+
"flask>=3.0",
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
[project.urls]
|
|
63
|
+
Homepage = "https://situ.hop3.abilian.com/"
|
|
64
|
+
|
|
65
|
+
[dependency-groups]
|
|
66
|
+
dev = [
|
|
67
|
+
"pytest>=8.0",
|
|
68
|
+
"httpx>=0.27", # litestar TestClient transport
|
|
69
|
+
"ruff>=0.6",
|
|
70
|
+
"mypy>=1.11",
|
|
71
|
+
"pytest-cov>=7.1.0",
|
|
72
|
+
"ty>=0.0.51",
|
|
73
|
+
"pyrefly>=1.1.1",
|
|
74
|
+
"nox>=2026.4.10",
|
|
75
|
+
"pre-commit>=4.6.0",
|
|
76
|
+
# mirror the [sqlalchemy] extra so the server-backed demo + tests run on `uv sync`
|
|
77
|
+
"dishka>=1.6",
|
|
78
|
+
"sqlalchemy>=2.0.50",
|
|
79
|
+
"aiosqlite>=0.20.0",
|
|
80
|
+
"greenlet>=3.0",
|
|
81
|
+
"litestar[standard]>=2.24.0",
|
|
82
|
+
"playwright>=1.60.0",
|
|
83
|
+
# the declui model-adapter sources, so their byte-identical tests run on `uv sync`
|
|
84
|
+
"attrs>=23.0",
|
|
85
|
+
"msgspec>=0.18",
|
|
86
|
+
"pydantic>=2.0",
|
|
87
|
+
"zensical>=0.0.46",
|
|
88
|
+
# the Flask adapter's demo + acceptance test (situ.mount.flask) run on `uv sync`
|
|
89
|
+
"flask>=3.0",
|
|
90
|
+
]
|
|
91
|
+
|
|
92
|
+
[build-system]
|
|
93
|
+
requires = ["uv_build>=0.8.4,<0.9.0"]
|
|
94
|
+
build-backend = "uv_build"
|
|
95
|
+
|
|
96
|
+
# [tool.hatch.build.targets.wheel]
|
|
97
|
+
# packages = ["src/situ", "src/situ_ui"]
|
|
98
|
+
|
|
99
|
+
# [tool.hatch.build.targets.sdist]
|
|
100
|
+
# include = ["src/situ", "src/situ_ui", "tests", "demos", "README.md", "LICENSE", "assets"]
|
|
101
|
+
|
|
102
|
+
[tool.pytest.ini_options]
|
|
103
|
+
testpaths = ["tests"]
|
|
104
|
+
pythonpath = ["."] # so tests can import the example apps under demos/
|
|
105
|
+
|
|
106
|
+
# ruff is configured in ruff.toml (a standalone file makes ruff ignore [tool.ruff] here).
|
|
107
|
+
|
|
108
|
+
[tool.mypy]
|
|
109
|
+
python_version = "3.12"
|
|
110
|
+
mypy_path = "src"
|
|
111
|
+
exclude = ["/static/"]
|
|
112
|
+
warn_unused_ignores = true
|
|
113
|
+
|
|
114
|
+
[[tool.mypy.overrides]]
|
|
115
|
+
# The SQLAlchemy/Dishka helpers (situ.infra.db / .di) live behind the optional
|
|
116
|
+
# `[sqlalchemy]` extra, so these imports may be absent in a base install.
|
|
117
|
+
module = ["dishka.*", "sqlalchemy.*"]
|
|
118
|
+
ignore_missing_imports = true
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""situ — site your state, derive the wire.
|
|
2
|
+
|
|
3
|
+
Declare where each piece of UI state lives (``Local`` / ``Url`` / ``Server`` / ``Synced``);
|
|
4
|
+
a real Python→JS compiler emits a small, app-specific client island and enforces the
|
|
5
|
+
client/server seam at compile time (a client read of ``Server``-sited state is a
|
|
6
|
+
``CompileError``). Mount a compiled component as a Litestar router with ``mount_component``.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import TYPE_CHECKING, Any
|
|
13
|
+
|
|
14
|
+
from .compiler.emitter import EmittedApp, compile_app
|
|
15
|
+
from .compiler.frontend import (
|
|
16
|
+
CompileError,
|
|
17
|
+
FrontEnd,
|
|
18
|
+
load_front_end,
|
|
19
|
+
parse_front_end,
|
|
20
|
+
splice_tree,
|
|
21
|
+
)
|
|
22
|
+
from .compiler.markers import (
|
|
23
|
+
Emit,
|
|
24
|
+
Inject,
|
|
25
|
+
Local,
|
|
26
|
+
Prop,
|
|
27
|
+
Provide,
|
|
28
|
+
Server,
|
|
29
|
+
Synced,
|
|
30
|
+
Url,
|
|
31
|
+
)
|
|
32
|
+
from .compiler.resolve import Context
|
|
33
|
+
from .mount import parse_id_set
|
|
34
|
+
from .siting.runtime import Signal, Signals, Site
|
|
35
|
+
|
|
36
|
+
if TYPE_CHECKING: # the checkers see real types; at runtime these load on first access
|
|
37
|
+
from .mount import mount_component, mount_static_component, mount_tree
|
|
38
|
+
|
|
39
|
+
__version__ = "0.1.0"
|
|
40
|
+
|
|
41
|
+
# the Litestar-backed mount factories load lazily, so `import situ` (or serving via the Flask
|
|
42
|
+
# adapter) pulls in no Litestar until you reach for one of them — situ's compiler + siting core is
|
|
43
|
+
# framework-neutral. See situ.mount.flask for the Flask adapter that proves it.
|
|
44
|
+
_LAZY_MOUNT = frozenset({"mount_component", "mount_static_component", "mount_tree"})
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def __getattr__(name: str) -> Any:
|
|
48
|
+
if name in _LAZY_MOUNT:
|
|
49
|
+
from . import mount # noqa: PLC0415
|
|
50
|
+
|
|
51
|
+
return getattr(mount, name)
|
|
52
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def static_dir() -> Path:
|
|
56
|
+
"""Filesystem path to situ's bundled browser assets (the ``_rt.js`` runtime shim).
|
|
57
|
+
|
|
58
|
+
Serve this directory at ``/static`` (e.g. a Litestar static-files router pointed
|
|
59
|
+
here) so a generated island can load the shim from ``/static/_rt.js``.
|
|
60
|
+
"""
|
|
61
|
+
return Path(__file__).resolve().parent / "static"
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def templates_dir() -> Path:
|
|
65
|
+
"""Filesystem path to situ's bundled Jinja templates (a default ``page.html``).
|
|
66
|
+
|
|
67
|
+
Add it to your Litestar ``TemplateConfig`` search path to render with the default page.
|
|
68
|
+
"""
|
|
69
|
+
return Path(__file__).resolve().parent / "templates"
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
__all__ = [
|
|
73
|
+
"CompileError",
|
|
74
|
+
"Context",
|
|
75
|
+
"Emit",
|
|
76
|
+
"EmittedApp",
|
|
77
|
+
"FrontEnd",
|
|
78
|
+
"Inject",
|
|
79
|
+
"Local",
|
|
80
|
+
"Prop",
|
|
81
|
+
"Provide",
|
|
82
|
+
"Server",
|
|
83
|
+
"Signal",
|
|
84
|
+
"Signals",
|
|
85
|
+
"Site",
|
|
86
|
+
"Synced",
|
|
87
|
+
"Url",
|
|
88
|
+
"__version__",
|
|
89
|
+
"compile_app",
|
|
90
|
+
"load_front_end",
|
|
91
|
+
"mount_component",
|
|
92
|
+
"mount_static_component",
|
|
93
|
+
"mount_tree",
|
|
94
|
+
"parse_front_end",
|
|
95
|
+
"parse_id_set",
|
|
96
|
+
"splice_tree",
|
|
97
|
+
"static_dir",
|
|
98
|
+
"templates_dir",
|
|
99
|
+
]
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""``situ check`` — a static pass: every PascalCase ``<Tag/>`` a component tree reaches must
|
|
2
|
+
resolve, checked by reading templates without booting the server.
|
|
3
|
+
|
|
4
|
+
Run it in ``make lint`` / pre-commit (and an editor/LSP later) so an unresolved component is caught
|
|
5
|
+
in the fast feedback loop, before the app even boots. Resolve ``ROOT`` against the union of one or more
|
|
6
|
+
``--from-dir`` contexts, optionally including the ``situ_ui`` kit with ``--kit``::
|
|
7
|
+
|
|
8
|
+
python -m situ.check demos/issues/components/issues --from-dir demos/issues/components
|
|
9
|
+
python -m situ.check demos/ui_gallery/components/gallery --kit
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import argparse
|
|
15
|
+
import sys
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
from situ.compiler.resolve import Context, check_tree
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def main(argv: list[str] | None = None) -> int:
|
|
22
|
+
p = argparse.ArgumentParser(prog="situ.check", description=__doc__)
|
|
23
|
+
p.add_argument(
|
|
24
|
+
"root", type=Path, help="the root component (a .html file or its stem)"
|
|
25
|
+
)
|
|
26
|
+
p.add_argument(
|
|
27
|
+
"--from-dir",
|
|
28
|
+
action="append",
|
|
29
|
+
default=[],
|
|
30
|
+
type=Path,
|
|
31
|
+
dest="dirs",
|
|
32
|
+
metavar="DIR",
|
|
33
|
+
help="a component directory to resolve tags from (repeatable)",
|
|
34
|
+
)
|
|
35
|
+
p.add_argument(
|
|
36
|
+
"--kit", action="store_true", help="also resolve against the situ_ui kit"
|
|
37
|
+
)
|
|
38
|
+
args = p.parse_args(argv)
|
|
39
|
+
|
|
40
|
+
ctx = Context.of({})
|
|
41
|
+
for d in args.dirs:
|
|
42
|
+
ctx = ctx.merge(Context.from_dir(d))
|
|
43
|
+
if args.kit:
|
|
44
|
+
import situ_ui # noqa: PLC0415 — lazy so situ.check has no hard dependency on the kit
|
|
45
|
+
|
|
46
|
+
ctx = ctx.merge(situ_ui.kit())
|
|
47
|
+
|
|
48
|
+
stem = args.root.with_suffix("") if args.root.suffix else args.root
|
|
49
|
+
errors = check_tree(stem, ctx)
|
|
50
|
+
for e in errors:
|
|
51
|
+
print(f"situ check: {e}", file=sys.stderr)
|
|
52
|
+
if errors:
|
|
53
|
+
print(
|
|
54
|
+
f"situ check: {len(errors)} unresolved component(s) under {stem.name}",
|
|
55
|
+
file=sys.stderr,
|
|
56
|
+
)
|
|
57
|
+
return 1
|
|
58
|
+
print(f"situ check: {stem.name} — all components resolve")
|
|
59
|
+
return 0
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
if __name__ == "__main__":
|
|
63
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""The owned Python→JS compiler back-end: the shared compile context (``ctx``), the
|
|
2
|
+
expression compiler (``expr``), the statement/action compilers (``stmt``), the binder
|
|
3
|
+
emitters + island assembly (``binders``), and the orchestrator (``core``) that drives
|
|
4
|
+
them into an ``EmittedApp``. The front-end that feeds it is ``..frontend``.
|
|
5
|
+
|
|
6
|
+
This package ``__init__`` is the back-end's public facade — import the names below from
|
|
7
|
+
``situ.compiler.emitter`` (the submodules import from each other, never from here)."""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from .core import EmittedApp, compile_app
|
|
12
|
+
from .ctx import EmitCtx
|
|
13
|
+
from .stmt import EVENT_CALL_BUILTINS, _compile_call_stmt
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"EVENT_CALL_BUILTINS",
|
|
17
|
+
"EmitCtx",
|
|
18
|
+
"EmittedApp",
|
|
19
|
+
"_compile_call_stmt",
|
|
20
|
+
"compile_app",
|
|
21
|
+
]
|