jongo 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 (44) hide show
  1. jongo-0.1.0/LICENSE +21 -0
  2. jongo-0.1.0/PKG-INFO +336 -0
  3. jongo-0.1.0/README.md +306 -0
  4. jongo-0.1.0/jongo/__init__.py +33 -0
  5. jongo-0.1.0/jongo/__main__.py +3 -0
  6. jongo-0.1.0/jongo/admin.py +486 -0
  7. jongo-0.1.0/jongo/app.py +492 -0
  8. jongo-0.1.0/jongo/auth.py +130 -0
  9. jongo-0.1.0/jongo/cli.py +227 -0
  10. jongo-0.1.0/jongo/compiler/__init__.py +5 -0
  11. jongo-0.1.0/jongo/compiler/bundle.py +213 -0
  12. jongo-0.1.0/jongo/compiler/dom.js +696 -0
  13. jongo-0.1.0/jongo/compiler/pyrt.js +780 -0
  14. jongo-0.1.0/jongo/compiler/scope.py +114 -0
  15. jongo-0.1.0/jongo/compiler/transpile.py +745 -0
  16. jongo-0.1.0/jongo/db/__init__.py +61 -0
  17. jongo-0.1.0/jongo/db/connection.py +220 -0
  18. jongo-0.1.0/jongo/db/fields.py +558 -0
  19. jongo-0.1.0/jongo/db/migrate.py +479 -0
  20. jongo-0.1.0/jongo/db/models.py +514 -0
  21. jongo-0.1.0/jongo/db/query.py +487 -0
  22. jongo-0.1.0/jongo/errors.py +62 -0
  23. jongo-0.1.0/jongo/errors_page.py +123 -0
  24. jongo-0.1.0/jongo/html.py +50 -0
  25. jongo-0.1.0/jongo/http.py +399 -0
  26. jongo-0.1.0/jongo/routing.py +114 -0
  27. jongo-0.1.0/jongo/rpc.py +215 -0
  28. jongo-0.1.0/jongo/scaffold/app.py.tmpl +92 -0
  29. jongo-0.1.0/jongo/scaffold/dot-gitignore.tmpl +6 -0
  30. jongo-0.1.0/jongo/server.py +172 -0
  31. jongo-0.1.0/jongo/styles.py +105 -0
  32. jongo-0.1.0/jongo/testing.py +140 -0
  33. jongo-0.1.0/jongo/vdom.py +515 -0
  34. jongo-0.1.0/jongo.egg-info/PKG-INFO +336 -0
  35. jongo-0.1.0/jongo.egg-info/SOURCES.txt +42 -0
  36. jongo-0.1.0/jongo.egg-info/dependency_links.txt +1 -0
  37. jongo-0.1.0/jongo.egg-info/entry_points.txt +2 -0
  38. jongo-0.1.0/jongo.egg-info/top_level.txt +1 -0
  39. jongo-0.1.0/pyproject.toml +48 -0
  40. jongo-0.1.0/setup.cfg +4 -0
  41. jongo-0.1.0/tests/test_admin.py +87 -0
  42. jongo-0.1.0/tests/test_app.py +262 -0
  43. jongo-0.1.0/tests/test_compiler.py +260 -0
  44. jongo-0.1.0/tests/test_db.py +1251 -0
jongo-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Joshua Harty
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.
jongo-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,336 @@
1
+ Metadata-Version: 2.4
2
+ Name: jongo
3
+ Version: 0.1.0
4
+ Summary: A full-stack Python web framework: routes, server code and reactive UI in one language.
5
+ Author: Joshua Harty
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/slimboi34/jongo
8
+ Project-URL: Repository, https://github.com/slimboi34/jongo
9
+ Project-URL: Issues, https://github.com/slimboi34/jongo/issues
10
+ Keywords: web,framework,fullstack,frontend,orm,wsgi,transpiler,ssr
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Environment :: Web Environment
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Topic :: Database
22
+ Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
23
+ Classifier: Topic :: Internet :: WWW/HTTP :: WSGI :: Application
24
+ Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
25
+ Classifier: Typing :: Typed
26
+ Requires-Python: >=3.10
27
+ Description-Content-Type: text/markdown
28
+ License-File: LICENSE
29
+ Dynamic: license-file
30
+
31
+ # Jongo
32
+
33
+ **One language for the whole web app.** Jongo is a full-stack Python web framework in the spirit of Django: ORM, migrations, auth, sessions, CSRF protection and an admin site. The difference is that your frontend is Python too. Components render on the server for a fast first paint, then compile to JavaScript and come alive in the browser. They call your server code with a plain `await`.
34
+
35
+ There's no template language, no separate JS project and no build step. It has zero dependencies.
36
+
37
+ ```python
38
+ from jongo import Jongo, component, db, server, state
39
+ from jongo.html import *
40
+
41
+ app = Jongo(__name__, database="db.sqlite3")
42
+
43
+
44
+ class Todo(db.Model): # a database table
45
+ title = db.Text(max_length=200)
46
+ done = db.Bool(default=False)
47
+
48
+
49
+ @server # a server function, callable from the browser
50
+ def add_todo(title: str) -> dict:
51
+ return Todo.create(title=title).to_dict()
52
+
53
+
54
+ @component # UI: Python here, JavaScript in the browser
55
+ def TodoList(todos):
56
+ items = state(todos)
57
+ draft = state("")
58
+
59
+ async def add(event):
60
+ todo = await add_todo(draft.value) # runs on the server
61
+ items.value = items.value + [todo]
62
+ draft.value = ""
63
+
64
+ return div(
65
+ form(
66
+ input_(value=draft.value, on_input=lambda e: draft.set(e.target.value)),
67
+ button("Add"),
68
+ on_submit=add,
69
+ ),
70
+ ul([li(t["title"], key=t["id"]) for t in items.value]),
71
+ )
72
+
73
+
74
+ @app.page("/") # route + view + UI, together
75
+ def home():
76
+ return TodoList(todos=Todo.all())
77
+ ```
78
+
79
+ ```bash
80
+ jongo dev # → http://localhost:8000, reloads when you save
81
+ ```
82
+
83
+ ## Why
84
+
85
+ In Django, one feature is spread across `models.py`, `urls.py`, `views.py`, a template, a form class and usually some JavaScript. In Jongo it's one idea in one place:
86
+
87
+ | Concern | Django | Jongo |
88
+ |---|---|---|
89
+ | URL | `urls.py` | `@app.page("/todos/<id>")` |
90
+ | View | `views.py` | the decorated function |
91
+ | Template | `.html` + template language | Python functions: `div(h1(title))` |
92
+ | Interactivity | separate JavaScript | the same component, compiled |
93
+ | AJAX endpoint | view + URL + `fetch` + JSON | `@server` function, called with `await` |
94
+ | Input validation | form classes | type hints (`title: str`, `todo: Todo`) |
95
+ | Migrations | `makemigrations` + files | `jongo migrate` diffs models against the DB |
96
+
97
+ ## Quick start
98
+
99
+ ```bash
100
+ pip install -e path/to/jongo # Python 3.10+
101
+ jongo new mysite
102
+ cd mysite
103
+ jongo dev
104
+ ```
105
+
106
+ The example app lives in [`examples/todo/app.py`](examples/todo/app.py). It's a polished todo list with a detail page and a focus timer, all in one file.
107
+
108
+ ---
109
+
110
+ ## Pages and routing
111
+
112
+ ```python
113
+ @app.page("/posts/<id>", title="Post")
114
+ def post(request, id: int, preview: bool = False): # `id: int` makes the route only match numbers
115
+ ...
116
+ ```
117
+
118
+ - **Path parameters** use `<name>` or `<converter:name>`, with the converters `str int float slug path uuid`. An annotation like `id: int` picks the converter for you.
119
+ - **Arguments** are filled by name. `request` is the current request, path parameters come from the URL, and anything else is read from the query string and type-checked.
120
+ - **A page returns** UI, `Page(ui, title=..., status=..., head=[...])`, or a `Response`, such as `redirect("/login")`.
121
+ - **Plain handlers:** `@app.get`, `@app.post` and `@app.route(path, methods=[...])` return a `Response`, HTML `str`, JSON-able `dict`/`list`, or UI.
122
+ - **Layouts:** `@app.layout` wraps every page in a component. When a link switches pages, the layout's state survives.
123
+ - **Errors:** raise `NotFound("...")`, `Forbidden()` or `HTTPError(status, message)`, and customise the page with `@app.errorhandler(404)`.
124
+ - **Access control:** `login_required=True` and `admin_required=True` work on pages and routes.
125
+
126
+ Same-origin links are handled by the client-side router. It fetches the next page as JSON and patches the DOM, with no full reload. Opt out with `a(..., data_reload=True)`.
127
+
128
+ ## Components
129
+
130
+ ```python
131
+ @component
132
+ def Card(title, children, tone="plain"):
133
+ open_ = state(True)
134
+ return section(
135
+ h2(title, on_click=lambda e: open_.set(not open_.value)),
136
+ open_.value and div(children),
137
+ class_=["card", {"card-warning": tone == "warning"}],
138
+ style={"padding": 16, "border_radius": 12},
139
+ )
140
+ ```
141
+
142
+ **Elements** come from `from jongo.html import *`.
143
+
144
+ - **Children:** positional arguments are children, which can be strings, elements, lists or `None`.
145
+ - **Attributes:** keyword arguments become attributes.
146
+ - `class_` accepts a string, list or `{name: condition}` dict.
147
+ - `style` takes a dict. Snake_case becomes kebab-case, and numbers get `px`.
148
+ - `on_click`, `on_input`, `on_submit`… attach event handlers. Submit handlers call `preventDefault()` for you.
149
+ - `aria_label`, `data_id` → `aria-label`, `data-id`.
150
+ - **Name clashes:** Python builtins get a trailing underscore: `input_`, `del_`, `map_`.
151
+ - **Other helpers:** `key=` for list items, `raw(html)` for trusted markup, `h("my-element")` for custom tags.
152
+
153
+ **Hooks and browser helpers:**
154
+
155
+ | | |
156
+ |---|---|
157
+ | `state(initial)` | reactive value. Set `.value`, or call `.set(v)` / `.update(fn)`, to re-render |
158
+ | `effect(fn, deps=None)` | runs in the browser after render; `deps=[]` runs it once; return a cleanup function |
159
+ | `ref()` | pass as `ref=` to get the DOM node in `.current` |
160
+ | `navigate(url)` / `refresh()` | client-side navigation / re-run the current page |
161
+ | `form_values(event)` | dict of a form's fields |
162
+ | `js.window`, `js.localStorage`, `js.fetch`… | browser globals; `e.prevent_default()` maps to `preventDefault()` |
163
+
164
+ ### Python that runs in the browser
165
+
166
+ The compiler supports most everyday Python:
167
+ - functions (default, `*args`, keyword-only and `**kwargs` parameters), `lambda`, closures and `nonlocal`
168
+ - `if`/`elif`/`else`, `for`/`while` loops with `else`, `try`/`except`/`finally`, `raise`
169
+ - list/dict/set comprehensions, generator expressions, the walrus operator, unpacking
170
+ - f-strings with format specs, `%` formatting and `str.format`
171
+ - `async`/`await`
172
+
173
+ It keeps Python semantics:
174
+ - Empty lists are falsy.
175
+ - `[1] + [2]` concatenates.
176
+ - `xs[-1]` indexes from the end.
177
+ - `-7 // 2 == -4`.
178
+ - `==` compares structures.
179
+ - A missing dict key raises `KeyError`.
180
+
181
+ The common methods of `str`, `list`, `dict` and `set` work, as do the `math`, `random`, `json` and `time` modules. A test suite runs the same functions in Python and in Node and requires identical results.
182
+
183
+ Anything that can't run in a browser is a **compile error with a hint**, reported at startup:
184
+ - classes
185
+ - `with`
186
+ - imports inside functions
187
+ - server-only modules
188
+ - touching a database model directly
189
+
190
+ One deliberate improvement: closures created in a `for` loop capture each item, so `button(on_click=lambda e: remove(todo))` in a loop does what you mean.
191
+
192
+ ## Server functions
193
+
194
+ ```python
195
+ @server
196
+ def rename(request, todo: Todo, title: str) -> dict: # `todo: Todo` loads the row, 404 if missing
197
+ if request.user is None:
198
+ raise HTTPError(401, "Log in first")
199
+ todo.title = title
200
+ todo.save()
201
+ return todo.to_dict()
202
+ ```
203
+
204
+ - **In a component**, call it with `await rename(todo_id, "New title")`. Keyword arguments work too.
205
+ - **Arguments are validated** against the type hints before your code runs. Supported hints: `str`, `int`, `float`, `bool`, `list[...]`, `dict[...]`, `Optional`, `Literal`, dataclasses and models.
206
+ - **Return values** can be JSON-able data or UI elements (rendered in the browser), or a `redirect(url)` that navigates.
207
+ - **Failures** raise `ServerError` in the browser, with `.status`, `.type` and field `.errors`.
208
+ - **Options:** `@server(login_required=True)`, `@server(admin_required=True)`, and `@server(refresh=True)` to re-run the page loader after each call.
209
+ - **Security:** every call is CSRF-protected. Only functions you decorate are exposed.
210
+
211
+ ## Styles
212
+
213
+ ```python
214
+ from jongo import css, global_css
215
+
216
+ s = css(
217
+ card={"padding": 16, "border_radius": 12, ":hover": {"background": "#fafafa"},
218
+ "& h2": {"margin": 0}, "@media (max-width: 600px)": {"padding": 8}},
219
+ )
220
+ div(h2("Hi"), class_=s.card) # class="card-3f9a1c"
221
+ ```
222
+
223
+ Class names are scoped. All stylesheets are served together from `/_jongo/app.css`.
224
+
225
+ ## Database
226
+
227
+ ```python
228
+ class Author(db.Model):
229
+ name = db.Text(max_length=100, unique=True)
230
+
231
+ class Book(db.Model):
232
+ title = db.Text(max_length=200)
233
+ author = db.ForeignKey(Author, related_name="books")
234
+ published = db.Date(null=True)
235
+ tags = db.JSON(default=list)
236
+
237
+ class Meta:
238
+ ordering = ["-published"]
239
+
240
+ Book.filter(author__name__icontains="le guin", published__gte=date(1970, 1, 1)).exclude(tags=[])[:10]
241
+ Book.filter(db.Q(title__startswith="The") | db.Q(tags__contains="classic")).count()
242
+ author.books.create(title="The Dispossessed")
243
+ with db.transaction():
244
+ ...
245
+ ```
246
+
247
+ - **Fields:** `Text Int Float Bool DateTime Date JSON ForeignKey`.
248
+ - **Lookups:** `exact iexact contains icontains startswith endswith gt gte lt lte in isnull ne`, plus relation traversal with `__`.
249
+ - **Migrations have no files.** `jongo migrate` compares your models to the live SQLite schema.
250
+ - It creates tables, adds columns and indexes, and rebuilds a table when a column's type changes.
251
+ - It only drops columns when you pass `--allow-destructive`.
252
+ - `jongo migrate --plan` shows the SQL first.
253
+ - `jongo dev` applies the safe changes automatically.
254
+
255
+ ## Auth and admin
256
+
257
+ ```python
258
+ from jongo.auth import User, authenticate, login, logout
259
+
260
+ app.admin() # generated admin at /admin
261
+ ```
262
+
263
+ - **Users:** `User.create_user(...)`, then `authenticate`, `login(request, user)` and `logout(request)`. `request.user` is available in pages, routes and server functions.
264
+ - **Passwords** use PBKDF2-SHA256. Changing a password signs out the user's other sessions.
265
+ - **The admin site** lists, searches, sorts, creates, edits and deletes rows for every model, with forms built from your field types. Create the first account with `jongo createadmin`.
266
+
267
+ ## Sessions, CSRF, security
268
+
269
+ - **Sessions** are HMAC-signed cookies: `request.session["cart"] = [...]`. Set `JONGO_SECRET_KEY` in production. In dev, a key is generated into `.jongo/secret`.
270
+ - **CSRF:** unsafe requests need a token. Browser code sends it automatically. Classic HTML forms need `input_(type="hidden", name="csrf_token", value=request.csrf_token)`. Cross-origin `Origin` headers are rejected.
271
+ - **Escaping:** text is always HTML-escaped. `raw()` is the explicit escape hatch.
272
+ - **Data sent to the browser:** props are converted to JSON. `User.to_dict()` never includes password hashes.
273
+
274
+ ## Testing
275
+
276
+ ```python
277
+ def test_add(app):
278
+ client = app.test_client()
279
+ assert client.get("/").status == 200
280
+ todo = client.rpc(add_todo, "Write tests") # full HTTP round trip, CSRF included
281
+ assert client.navigate("/")["title"] == "Todos"
282
+ ```
283
+
284
+ ## CLI
285
+
286
+ | Command | |
287
+ |---|---|
288
+ | `jongo new NAME` | create a project |
289
+ | `jongo dev [app.py] [--port]` | dev server: auto-reload, live browser reload, error overlay, debug pages |
290
+ | `jongo run [--host --port --migrate]` | production server (threaded) |
291
+ | `jongo migrate [--plan] [--allow-destructive]` | sync the schema |
292
+ | `jongo createadmin` | create an admin user |
293
+ | `jongo routes` | list routes |
294
+ | `jongo shell` | Python shell with your models |
295
+ | `jongo build` | compile components and report errors (good for CI) |
296
+
297
+ ## Deployment
298
+
299
+ `app` is a standard WSGI application:
300
+
301
+ ```bash
302
+ JONGO_SECRET_KEY=... gunicorn app:app # or: jongo run --port 8000 --migrate
303
+ ```
304
+
305
+ ## How it works
306
+
307
+ ```
308
+ request ─▶ route ─▶ page function ─▶ UI tree ─┬─▶ rendered to HTML on the server ─▶ fast first paint
309
+ └─▶ serialised as JSON ──────────────▶ browser hydrates it
310
+ with components compiled
311
+ @component (Python source) ──ast──▶ JavaScript ──▶ /_jongo/app.js from the same Python
312
+ @server call in browser ──POST /_jongo/rpc/<id> (CSRF, JSON, type-checked)──▶ your function
313
+ ```
314
+
315
+ - `jongo/compiler/` turns Python ASTs into JavaScript. Free names resolve against the live Python objects, so the compiler knows whether `add_todo` is a server function, a component, a helper or a constant.
316
+ - `jongo/compiler/pyrt.js` provides Python semantics in the browser.
317
+ - `jongo/compiler/dom.js` is the virtual DOM, hooks, keyed diffing, hydration and router, in about 700 lines with no dependencies.
318
+ - `jongo/vdom.py` is the same tree model on the server.
319
+
320
+ ## Developing Jongo
321
+
322
+ ```bash
323
+ python3 -m venv .venv && .venv/bin/pip install -e . pytest
324
+ .venv/bin/python -m pytest # needs `node` for the compiler parity tests
325
+ cd examples/todo && ../../.venv/bin/jongo dev
326
+ ```
327
+
328
+ ## Status
329
+
330
+ Version 0.1, which is young. Known limits:
331
+ - SQLite only.
332
+ - No WebSockets yet.
333
+ - No classes in browser code.
334
+ - Components re-render their subtree without memoisation.
335
+
336
+ Bug reports and ideas are welcome.
jongo-0.1.0/README.md ADDED
@@ -0,0 +1,306 @@
1
+ # Jongo
2
+
3
+ **One language for the whole web app.** Jongo is a full-stack Python web framework in the spirit of Django: ORM, migrations, auth, sessions, CSRF protection and an admin site. The difference is that your frontend is Python too. Components render on the server for a fast first paint, then compile to JavaScript and come alive in the browser. They call your server code with a plain `await`.
4
+
5
+ There's no template language, no separate JS project and no build step. It has zero dependencies.
6
+
7
+ ```python
8
+ from jongo import Jongo, component, db, server, state
9
+ from jongo.html import *
10
+
11
+ app = Jongo(__name__, database="db.sqlite3")
12
+
13
+
14
+ class Todo(db.Model): # a database table
15
+ title = db.Text(max_length=200)
16
+ done = db.Bool(default=False)
17
+
18
+
19
+ @server # a server function, callable from the browser
20
+ def add_todo(title: str) -> dict:
21
+ return Todo.create(title=title).to_dict()
22
+
23
+
24
+ @component # UI: Python here, JavaScript in the browser
25
+ def TodoList(todos):
26
+ items = state(todos)
27
+ draft = state("")
28
+
29
+ async def add(event):
30
+ todo = await add_todo(draft.value) # runs on the server
31
+ items.value = items.value + [todo]
32
+ draft.value = ""
33
+
34
+ return div(
35
+ form(
36
+ input_(value=draft.value, on_input=lambda e: draft.set(e.target.value)),
37
+ button("Add"),
38
+ on_submit=add,
39
+ ),
40
+ ul([li(t["title"], key=t["id"]) for t in items.value]),
41
+ )
42
+
43
+
44
+ @app.page("/") # route + view + UI, together
45
+ def home():
46
+ return TodoList(todos=Todo.all())
47
+ ```
48
+
49
+ ```bash
50
+ jongo dev # → http://localhost:8000, reloads when you save
51
+ ```
52
+
53
+ ## Why
54
+
55
+ In Django, one feature is spread across `models.py`, `urls.py`, `views.py`, a template, a form class and usually some JavaScript. In Jongo it's one idea in one place:
56
+
57
+ | Concern | Django | Jongo |
58
+ |---|---|---|
59
+ | URL | `urls.py` | `@app.page("/todos/<id>")` |
60
+ | View | `views.py` | the decorated function |
61
+ | Template | `.html` + template language | Python functions: `div(h1(title))` |
62
+ | Interactivity | separate JavaScript | the same component, compiled |
63
+ | AJAX endpoint | view + URL + `fetch` + JSON | `@server` function, called with `await` |
64
+ | Input validation | form classes | type hints (`title: str`, `todo: Todo`) |
65
+ | Migrations | `makemigrations` + files | `jongo migrate` diffs models against the DB |
66
+
67
+ ## Quick start
68
+
69
+ ```bash
70
+ pip install -e path/to/jongo # Python 3.10+
71
+ jongo new mysite
72
+ cd mysite
73
+ jongo dev
74
+ ```
75
+
76
+ The example app lives in [`examples/todo/app.py`](examples/todo/app.py). It's a polished todo list with a detail page and a focus timer, all in one file.
77
+
78
+ ---
79
+
80
+ ## Pages and routing
81
+
82
+ ```python
83
+ @app.page("/posts/<id>", title="Post")
84
+ def post(request, id: int, preview: bool = False): # `id: int` makes the route only match numbers
85
+ ...
86
+ ```
87
+
88
+ - **Path parameters** use `<name>` or `<converter:name>`, with the converters `str int float slug path uuid`. An annotation like `id: int` picks the converter for you.
89
+ - **Arguments** are filled by name. `request` is the current request, path parameters come from the URL, and anything else is read from the query string and type-checked.
90
+ - **A page returns** UI, `Page(ui, title=..., status=..., head=[...])`, or a `Response`, such as `redirect("/login")`.
91
+ - **Plain handlers:** `@app.get`, `@app.post` and `@app.route(path, methods=[...])` return a `Response`, HTML `str`, JSON-able `dict`/`list`, or UI.
92
+ - **Layouts:** `@app.layout` wraps every page in a component. When a link switches pages, the layout's state survives.
93
+ - **Errors:** raise `NotFound("...")`, `Forbidden()` or `HTTPError(status, message)`, and customise the page with `@app.errorhandler(404)`.
94
+ - **Access control:** `login_required=True` and `admin_required=True` work on pages and routes.
95
+
96
+ Same-origin links are handled by the client-side router. It fetches the next page as JSON and patches the DOM, with no full reload. Opt out with `a(..., data_reload=True)`.
97
+
98
+ ## Components
99
+
100
+ ```python
101
+ @component
102
+ def Card(title, children, tone="plain"):
103
+ open_ = state(True)
104
+ return section(
105
+ h2(title, on_click=lambda e: open_.set(not open_.value)),
106
+ open_.value and div(children),
107
+ class_=["card", {"card-warning": tone == "warning"}],
108
+ style={"padding": 16, "border_radius": 12},
109
+ )
110
+ ```
111
+
112
+ **Elements** come from `from jongo.html import *`.
113
+
114
+ - **Children:** positional arguments are children, which can be strings, elements, lists or `None`.
115
+ - **Attributes:** keyword arguments become attributes.
116
+ - `class_` accepts a string, list or `{name: condition}` dict.
117
+ - `style` takes a dict. Snake_case becomes kebab-case, and numbers get `px`.
118
+ - `on_click`, `on_input`, `on_submit`… attach event handlers. Submit handlers call `preventDefault()` for you.
119
+ - `aria_label`, `data_id` → `aria-label`, `data-id`.
120
+ - **Name clashes:** Python builtins get a trailing underscore: `input_`, `del_`, `map_`.
121
+ - **Other helpers:** `key=` for list items, `raw(html)` for trusted markup, `h("my-element")` for custom tags.
122
+
123
+ **Hooks and browser helpers:**
124
+
125
+ | | |
126
+ |---|---|
127
+ | `state(initial)` | reactive value. Set `.value`, or call `.set(v)` / `.update(fn)`, to re-render |
128
+ | `effect(fn, deps=None)` | runs in the browser after render; `deps=[]` runs it once; return a cleanup function |
129
+ | `ref()` | pass as `ref=` to get the DOM node in `.current` |
130
+ | `navigate(url)` / `refresh()` | client-side navigation / re-run the current page |
131
+ | `form_values(event)` | dict of a form's fields |
132
+ | `js.window`, `js.localStorage`, `js.fetch`… | browser globals; `e.prevent_default()` maps to `preventDefault()` |
133
+
134
+ ### Python that runs in the browser
135
+
136
+ The compiler supports most everyday Python:
137
+ - functions (default, `*args`, keyword-only and `**kwargs` parameters), `lambda`, closures and `nonlocal`
138
+ - `if`/`elif`/`else`, `for`/`while` loops with `else`, `try`/`except`/`finally`, `raise`
139
+ - list/dict/set comprehensions, generator expressions, the walrus operator, unpacking
140
+ - f-strings with format specs, `%` formatting and `str.format`
141
+ - `async`/`await`
142
+
143
+ It keeps Python semantics:
144
+ - Empty lists are falsy.
145
+ - `[1] + [2]` concatenates.
146
+ - `xs[-1]` indexes from the end.
147
+ - `-7 // 2 == -4`.
148
+ - `==` compares structures.
149
+ - A missing dict key raises `KeyError`.
150
+
151
+ The common methods of `str`, `list`, `dict` and `set` work, as do the `math`, `random`, `json` and `time` modules. A test suite runs the same functions in Python and in Node and requires identical results.
152
+
153
+ Anything that can't run in a browser is a **compile error with a hint**, reported at startup:
154
+ - classes
155
+ - `with`
156
+ - imports inside functions
157
+ - server-only modules
158
+ - touching a database model directly
159
+
160
+ One deliberate improvement: closures created in a `for` loop capture each item, so `button(on_click=lambda e: remove(todo))` in a loop does what you mean.
161
+
162
+ ## Server functions
163
+
164
+ ```python
165
+ @server
166
+ def rename(request, todo: Todo, title: str) -> dict: # `todo: Todo` loads the row, 404 if missing
167
+ if request.user is None:
168
+ raise HTTPError(401, "Log in first")
169
+ todo.title = title
170
+ todo.save()
171
+ return todo.to_dict()
172
+ ```
173
+
174
+ - **In a component**, call it with `await rename(todo_id, "New title")`. Keyword arguments work too.
175
+ - **Arguments are validated** against the type hints before your code runs. Supported hints: `str`, `int`, `float`, `bool`, `list[...]`, `dict[...]`, `Optional`, `Literal`, dataclasses and models.
176
+ - **Return values** can be JSON-able data or UI elements (rendered in the browser), or a `redirect(url)` that navigates.
177
+ - **Failures** raise `ServerError` in the browser, with `.status`, `.type` and field `.errors`.
178
+ - **Options:** `@server(login_required=True)`, `@server(admin_required=True)`, and `@server(refresh=True)` to re-run the page loader after each call.
179
+ - **Security:** every call is CSRF-protected. Only functions you decorate are exposed.
180
+
181
+ ## Styles
182
+
183
+ ```python
184
+ from jongo import css, global_css
185
+
186
+ s = css(
187
+ card={"padding": 16, "border_radius": 12, ":hover": {"background": "#fafafa"},
188
+ "& h2": {"margin": 0}, "@media (max-width: 600px)": {"padding": 8}},
189
+ )
190
+ div(h2("Hi"), class_=s.card) # class="card-3f9a1c"
191
+ ```
192
+
193
+ Class names are scoped. All stylesheets are served together from `/_jongo/app.css`.
194
+
195
+ ## Database
196
+
197
+ ```python
198
+ class Author(db.Model):
199
+ name = db.Text(max_length=100, unique=True)
200
+
201
+ class Book(db.Model):
202
+ title = db.Text(max_length=200)
203
+ author = db.ForeignKey(Author, related_name="books")
204
+ published = db.Date(null=True)
205
+ tags = db.JSON(default=list)
206
+
207
+ class Meta:
208
+ ordering = ["-published"]
209
+
210
+ Book.filter(author__name__icontains="le guin", published__gte=date(1970, 1, 1)).exclude(tags=[])[:10]
211
+ Book.filter(db.Q(title__startswith="The") | db.Q(tags__contains="classic")).count()
212
+ author.books.create(title="The Dispossessed")
213
+ with db.transaction():
214
+ ...
215
+ ```
216
+
217
+ - **Fields:** `Text Int Float Bool DateTime Date JSON ForeignKey`.
218
+ - **Lookups:** `exact iexact contains icontains startswith endswith gt gte lt lte in isnull ne`, plus relation traversal with `__`.
219
+ - **Migrations have no files.** `jongo migrate` compares your models to the live SQLite schema.
220
+ - It creates tables, adds columns and indexes, and rebuilds a table when a column's type changes.
221
+ - It only drops columns when you pass `--allow-destructive`.
222
+ - `jongo migrate --plan` shows the SQL first.
223
+ - `jongo dev` applies the safe changes automatically.
224
+
225
+ ## Auth and admin
226
+
227
+ ```python
228
+ from jongo.auth import User, authenticate, login, logout
229
+
230
+ app.admin() # generated admin at /admin
231
+ ```
232
+
233
+ - **Users:** `User.create_user(...)`, then `authenticate`, `login(request, user)` and `logout(request)`. `request.user` is available in pages, routes and server functions.
234
+ - **Passwords** use PBKDF2-SHA256. Changing a password signs out the user's other sessions.
235
+ - **The admin site** lists, searches, sorts, creates, edits and deletes rows for every model, with forms built from your field types. Create the first account with `jongo createadmin`.
236
+
237
+ ## Sessions, CSRF, security
238
+
239
+ - **Sessions** are HMAC-signed cookies: `request.session["cart"] = [...]`. Set `JONGO_SECRET_KEY` in production. In dev, a key is generated into `.jongo/secret`.
240
+ - **CSRF:** unsafe requests need a token. Browser code sends it automatically. Classic HTML forms need `input_(type="hidden", name="csrf_token", value=request.csrf_token)`. Cross-origin `Origin` headers are rejected.
241
+ - **Escaping:** text is always HTML-escaped. `raw()` is the explicit escape hatch.
242
+ - **Data sent to the browser:** props are converted to JSON. `User.to_dict()` never includes password hashes.
243
+
244
+ ## Testing
245
+
246
+ ```python
247
+ def test_add(app):
248
+ client = app.test_client()
249
+ assert client.get("/").status == 200
250
+ todo = client.rpc(add_todo, "Write tests") # full HTTP round trip, CSRF included
251
+ assert client.navigate("/")["title"] == "Todos"
252
+ ```
253
+
254
+ ## CLI
255
+
256
+ | Command | |
257
+ |---|---|
258
+ | `jongo new NAME` | create a project |
259
+ | `jongo dev [app.py] [--port]` | dev server: auto-reload, live browser reload, error overlay, debug pages |
260
+ | `jongo run [--host --port --migrate]` | production server (threaded) |
261
+ | `jongo migrate [--plan] [--allow-destructive]` | sync the schema |
262
+ | `jongo createadmin` | create an admin user |
263
+ | `jongo routes` | list routes |
264
+ | `jongo shell` | Python shell with your models |
265
+ | `jongo build` | compile components and report errors (good for CI) |
266
+
267
+ ## Deployment
268
+
269
+ `app` is a standard WSGI application:
270
+
271
+ ```bash
272
+ JONGO_SECRET_KEY=... gunicorn app:app # or: jongo run --port 8000 --migrate
273
+ ```
274
+
275
+ ## How it works
276
+
277
+ ```
278
+ request ─▶ route ─▶ page function ─▶ UI tree ─┬─▶ rendered to HTML on the server ─▶ fast first paint
279
+ └─▶ serialised as JSON ──────────────▶ browser hydrates it
280
+ with components compiled
281
+ @component (Python source) ──ast──▶ JavaScript ──▶ /_jongo/app.js from the same Python
282
+ @server call in browser ──POST /_jongo/rpc/<id> (CSRF, JSON, type-checked)──▶ your function
283
+ ```
284
+
285
+ - `jongo/compiler/` turns Python ASTs into JavaScript. Free names resolve against the live Python objects, so the compiler knows whether `add_todo` is a server function, a component, a helper or a constant.
286
+ - `jongo/compiler/pyrt.js` provides Python semantics in the browser.
287
+ - `jongo/compiler/dom.js` is the virtual DOM, hooks, keyed diffing, hydration and router, in about 700 lines with no dependencies.
288
+ - `jongo/vdom.py` is the same tree model on the server.
289
+
290
+ ## Developing Jongo
291
+
292
+ ```bash
293
+ python3 -m venv .venv && .venv/bin/pip install -e . pytest
294
+ .venv/bin/python -m pytest # needs `node` for the compiler parity tests
295
+ cd examples/todo && ../../.venv/bin/jongo dev
296
+ ```
297
+
298
+ ## Status
299
+
300
+ Version 0.1, which is young. Known limits:
301
+ - SQLite only.
302
+ - No WebSockets yet.
303
+ - No classes in browser code.
304
+ - Components re-render their subtree without memoisation.
305
+
306
+ Bug reports and ideas are welcome.