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.
Files changed (41) hide show
  1. situ-0.1.0/LICENSE +21 -0
  2. situ-0.1.0/PKG-INFO +185 -0
  3. situ-0.1.0/README.md +149 -0
  4. situ-0.1.0/pyproject.toml +118 -0
  5. situ-0.1.0/src/situ/__init__.py +99 -0
  6. situ-0.1.0/src/situ/check.py +63 -0
  7. situ-0.1.0/src/situ/compiler/__init__.py +2 -0
  8. situ-0.1.0/src/situ/compiler/emitter/__init__.py +21 -0
  9. situ-0.1.0/src/situ/compiler/emitter/binders.py +267 -0
  10. situ-0.1.0/src/situ/compiler/emitter/core.py +480 -0
  11. situ-0.1.0/src/situ/compiler/emitter/ctx.py +34 -0
  12. situ-0.1.0/src/situ/compiler/emitter/expr.py +349 -0
  13. situ-0.1.0/src/situ/compiler/emitter/stmt.py +290 -0
  14. situ-0.1.0/src/situ/compiler/frontend/__init__.py +20 -0
  15. situ-0.1.0/src/situ/compiler/frontend/base.py +142 -0
  16. situ-0.1.0/src/situ/compiler/frontend/compose.py +750 -0
  17. situ-0.1.0/src/situ/compiler/frontend/scopes.py +159 -0
  18. situ-0.1.0/src/situ/compiler/frontend/styles.py +221 -0
  19. situ-0.1.0/src/situ/compiler/markers.py +59 -0
  20. situ-0.1.0/src/situ/compiler/resolve.py +139 -0
  21. situ-0.1.0/src/situ/compiler/unified.py +246 -0
  22. situ-0.1.0/src/situ/declui/__init__.py +50 -0
  23. situ-0.1.0/src/situ/declui/generate.py +1828 -0
  24. situ-0.1.0/src/situ/declui/mount.py +192 -0
  25. situ-0.1.0/src/situ/declui/predicates.py +354 -0
  26. situ-0.1.0/src/situ/declui/spec.py +93 -0
  27. situ-0.1.0/src/situ/infra/__init__.py +2 -0
  28. situ-0.1.0/src/situ/infra/db.py +26 -0
  29. situ-0.1.0/src/situ/infra/di.py +21 -0
  30. situ-0.1.0/src/situ/infra/templating.py +30 -0
  31. situ-0.1.0/src/situ/mount/__init__.py +37 -0
  32. situ-0.1.0/src/situ/mount/core.py +235 -0
  33. situ-0.1.0/src/situ/mount/factories.py +309 -0
  34. situ-0.1.0/src/situ/mount/flask.py +145 -0
  35. situ-0.1.0/src/situ/mount/runtime.py +72 -0
  36. situ-0.1.0/src/situ/py.typed +0 -0
  37. situ-0.1.0/src/situ/siting/__init__.py +1 -0
  38. situ-0.1.0/src/situ/siting/runtime.py +74 -0
  39. situ-0.1.0/src/situ/static/_rt.js +509 -0
  40. situ-0.1.0/src/situ/static/declui.css +236 -0
  41. 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,2 @@
1
+ """The single-file-component compiler toolchain: the ``frontend`` package (parse + ``scopes`` +
2
+ ``compose``), the owned ``emitter`` package (the Python→JS back-end), and the site ``markers``."""
@@ -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
+ ]