caspian-utils 0.1.5 → 0.1.7
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.
- package/dist/docs/auth.md +47 -20
- package/dist/docs/fetch-data.md +102 -64
- package/dist/docs/pulsepoint.md +12 -10
- package/package.json +1 -1
package/dist/docs/auth.md
CHANGED
|
@@ -20,14 +20,14 @@ Treat `casp.auth` as the default authentication layer in Caspian app code. Do no
|
|
|
20
20
|
|
|
21
21
|
## Overview
|
|
22
22
|
|
|
23
|
-
Caspian authentication has two main layers:
|
|
24
|
-
|
|
25
|
-
- app-level policy, controlled by `src/lib/auth/auth_config.py`
|
|
26
|
-
- framework runtime behavior, implemented by `.venv/Lib/site-packages/casp/auth.py`
|
|
23
|
+
Caspian authentication has two main layers:
|
|
24
|
+
|
|
25
|
+
- app-level policy, controlled by `src/lib/auth/auth_config.py`
|
|
26
|
+
- framework runtime behavior, implemented by `.venv/Lib/site-packages/casp/auth.py`
|
|
27
27
|
|
|
28
28
|
The main public API includes:
|
|
29
29
|
|
|
30
|
-
- `AuthSettings` for centralized app auth policy
|
|
30
|
+
- `AuthSettings` for centralized app auth policy
|
|
31
31
|
- `configure_auth(...)` and `get_auth_settings()` for app startup and reads
|
|
32
32
|
- the global `auth` object for session lifecycle work
|
|
33
33
|
- `require_auth`, `require_role`, and `guest_only` for page-level protection
|
|
@@ -49,7 +49,7 @@ from casp.auth import (
|
|
|
49
49
|
|
|
50
50
|
## Default Auth Rule
|
|
51
51
|
|
|
52
|
-
- Define app-wide auth behavior in `src/lib/auth/auth_config.py` through `build_auth_settings()` and apply it once at startup with `configure_auth(...)`.
|
|
52
|
+
- Define app-wide auth behavior in `src/lib/auth/auth_config.py` through `build_auth_settings()` and apply it once at startup with `configure_auth(...)`.
|
|
53
53
|
- Use `auth.sign_in(...)` and `auth.sign_out(...)` instead of setting or clearing session keys directly.
|
|
54
54
|
- Prefer `pp.rpc(...)` plus `@rpc(require_auth=True)` for signout buttons or menus rendered in pages or components. Use a dedicated signout route only when you need a plain HTML form POST or a no-JavaScript fallback.
|
|
55
55
|
- Use `@require_auth`, `@require_role`, and `@guest_only` for page access rules.
|
|
@@ -61,15 +61,15 @@ from casp.auth import (
|
|
|
61
61
|
|
|
62
62
|
## Framework Internals Note
|
|
63
63
|
|
|
64
|
-
- The centralized app auth policy controller is `src/lib/auth/auth_config.py`.
|
|
65
|
-
- The installed framework implementation lives in `.venv/Lib/site-packages/casp/auth.py`.
|
|
66
|
-
- Treat `auth_config.py` as project code and `casp/auth.py` as framework runtime code. Do not edit `casp/auth.py` to control app route privacy, redirects, or RBAC.
|
|
64
|
+
- The centralized app auth policy controller is `src/lib/auth/auth_config.py`.
|
|
65
|
+
- The installed framework implementation lives in `.venv/Lib/site-packages/casp/auth.py`.
|
|
66
|
+
- Treat `auth_config.py` as project code and `casp/auth.py` as framework runtime code. Do not edit `casp/auth.py` to control app route privacy, redirects, or RBAC.
|
|
67
67
|
- If upstream docs and the installed implementation disagree, prefer the installed implementation for local project guidance.
|
|
68
68
|
- Use [core-runtime-map.md](./core-runtime-map.md) when an auth task crosses `main.py` bootstrap behavior such as development cookie scoping or middleware ownership.
|
|
69
69
|
|
|
70
70
|
## Centralized Auth Settings
|
|
71
71
|
|
|
72
|
-
Keep application auth policy in `src/lib/auth/auth_config.py`. This file is the controller for route privacy, redirects, and RBAC.
|
|
72
|
+
Keep application auth policy in `src/lib/auth/auth_config.py`. This file is the controller for route privacy, redirects, and RBAC.
|
|
73
73
|
|
|
74
74
|
Example:
|
|
75
75
|
|
|
@@ -80,12 +80,12 @@ from casp.auth import AuthSettings
|
|
|
80
80
|
|
|
81
81
|
def build_auth_settings() -> AuthSettings:
|
|
82
82
|
"""
|
|
83
|
-
Centralized app auth policy controller.
|
|
84
|
-
|
|
85
|
-
Keep secrets (AUTH_SECRET, AUTH_COOKIE_NAME) in .env.
|
|
86
|
-
Keep app-level session settings in .env (SESSION_LIFETIME_HOURS, etc).
|
|
87
|
-
Decide route privacy, redirects, and RBAC here at app setup time instead of
|
|
88
|
-
changing Caspian core runtime files.
|
|
83
|
+
Centralized app auth policy controller.
|
|
84
|
+
|
|
85
|
+
Keep secrets (AUTH_SECRET, AUTH_COOKIE_NAME) in .env.
|
|
86
|
+
Keep app-level session settings in .env (SESSION_LIFETIME_HOURS, etc).
|
|
87
|
+
Decide route privacy, redirects, and RBAC here at app setup time instead of
|
|
88
|
+
changing Caspian core runtime files.
|
|
89
89
|
"""
|
|
90
90
|
|
|
91
91
|
return AuthSettings(
|
|
@@ -103,7 +103,7 @@ def build_auth_settings() -> AuthSettings:
|
|
|
103
103
|
is_role_based=False,
|
|
104
104
|
role_identifier="role",
|
|
105
105
|
|
|
106
|
-
# RBAC policy is app-owned here; the runtime expects ROUTE/PATTERN -> [ROLES].
|
|
106
|
+
# RBAC policy is app-owned here; the runtime expects ROUTE/PATTERN -> [ROLES].
|
|
107
107
|
role_based_routes={},
|
|
108
108
|
|
|
109
109
|
# Redirects / prefixes
|
|
@@ -152,7 +152,7 @@ Make this decision at app setup time in `src/lib/auth/auth_config.py`.
|
|
|
152
152
|
- In the current runtime, `auth_routes=["/signin", "/signup"]` stays public by default, and most apps do not need to change it unless the user explicitly asks for different auth routes.
|
|
153
153
|
- In all-private mode, the default `public_routes=["/"]` keeps the home page public unless you change that list.
|
|
154
154
|
- `token_auto_refresh=True` does not make routes private. It only enables sliding-session refresh when the request lifecycle calls `auth.refresh_session()`.
|
|
155
|
-
- Do not modify Caspian core files for this decision. Keep the policy in `src/lib/auth/auth_config.py`.
|
|
155
|
+
- Do not modify Caspian core files for this decision. Keep the policy in `src/lib/auth/auth_config.py`.
|
|
156
156
|
- If you customize `src/lib/auth/auth_config.py`, add it to `excludeFiles` in `caspian.config.json` so update commands do not overwrite your local auth policy.
|
|
157
157
|
|
|
158
158
|
Example all-private setup with a few public exceptions:
|
|
@@ -720,6 +720,33 @@ In the installed implementation:
|
|
|
720
720
|
|
|
721
721
|
The installed `casp.auth` file includes provider helpers for Google and GitHub OAuth.
|
|
722
722
|
|
|
723
|
+
In the app-owned starter bootstrap, both providers are already registered. Treat Google and GitHub sign-in as a shipped feature, not something to build from scratch. The current `main.py` calls:
|
|
724
|
+
|
|
725
|
+
```python
|
|
726
|
+
Auth.set_providers(GithubProvider(), GoogleProvider())
|
|
727
|
+
```
|
|
728
|
+
|
|
729
|
+
so `AuthMiddleware` already delegates the provider signin and callback paths through `auth.auth_providers(...)` on every request. To add a working social sign-in to a page, you usually need only two things:
|
|
730
|
+
|
|
731
|
+
- a link or button that navigates to the provider signin path, and
|
|
732
|
+
- the provider credentials in `.env`.
|
|
733
|
+
|
|
734
|
+
The signin and callback paths live under `api_auth_prefix` (default `/api/auth`):
|
|
735
|
+
|
|
736
|
+
- Google sign-in link target: `/api/auth/signin/google`
|
|
737
|
+
- GitHub sign-in link target: `/api/auth/signin/github`
|
|
738
|
+
- Google callback (provider-side redirect URI): `/api/auth/callback/google`
|
|
739
|
+
- GitHub callback (provider-side redirect URI): `/api/auth/callback/github`
|
|
740
|
+
|
|
741
|
+
Minimal sign-in button:
|
|
742
|
+
|
|
743
|
+
```html
|
|
744
|
+
<a href="/api/auth/signin/google">Continue with Google</a>
|
|
745
|
+
<a href="/api/auth/signin/github">Continue with GitHub</a>
|
|
746
|
+
```
|
|
747
|
+
|
|
748
|
+
Do not reinvent this flow. When a task asks for Google or GitHub login, do not add `authlib`, a hand-written `httpx` token-exchange, a parallel session writer, or custom `/callback` route handlers. The shipped providers plus `auth.auth_providers(...)` already perform the redirect, the code exchange, payload normalization, `auth.sign_in(...)`, and the post-login redirect to `default_signin_redirect`. Reuse them and keep credentials in `.env`.
|
|
749
|
+
|
|
723
750
|
Available provider classes:
|
|
724
751
|
|
|
725
752
|
- `GoogleProvider(client_id, client_secret, redirect_uri, max_age="30d")`
|
|
@@ -767,7 +794,7 @@ Behavior:
|
|
|
767
794
|
|
|
768
795
|
Use this helper when custom form or fetch flows need access to the session CSRF token.
|
|
769
796
|
|
|
770
|
-
## Current Implementation Notes
|
|
797
|
+
## Current Implementation Notes
|
|
771
798
|
|
|
772
799
|
- The installed auth runtime is session-backed. It stores the auth payload and CSRF token in `request.session`.
|
|
773
800
|
- Expiration uses timestamps and the current duration parser only accepts `s`, `m`, `h`, and `d` units.
|
|
@@ -810,7 +837,7 @@ If an AI agent is deciding how to handle auth in Caspian, apply these rules firs
|
|
|
810
837
|
- Treat `public_routes` as the public exception list for all-private apps. In the current defaults, `/` stays public and `auth_routes=["/signin", "/signup"]` stays public too.
|
|
811
838
|
- For protected grouped sections, follow the section layout pattern in [routing.md](./routing.md) and keep auth-specific route policy in `src/lib/auth/auth_config.py`.
|
|
812
839
|
- Apply settings at startup in `main.py` with `configure_auth(build_auth_settings())`.
|
|
813
|
-
-
|
|
840
|
+
- Treat Google and GitHub OAuth as shipped: the starter `main.py` already registers both providers with `Auth.set_providers(GithubProvider(), GoogleProvider())`. For social sign-in, link to `/api/auth/signin/{google,github}` and set `.env` credentials; do not hand-roll OAuth, token exchange, or callback routes. Only call `Auth.set_providers(...)` yourself if you are changing which providers are registered.
|
|
814
841
|
- Use `auth.sign_in(...)` and `auth.sign_out(...)` for session lifecycle changes.
|
|
815
842
|
- Prefer `pp.rpc("signout")` plus `@rpc(require_auth=True)` for logout buttons or menus in pages and components.
|
|
816
843
|
- Use a dedicated `/signout` route only for plain HTML form POST, no-JavaScript fallback, or other full-navigation edge cases.
|
package/dist/docs/fetch-data.md
CHANGED
|
@@ -21,13 +21,13 @@ related:
|
|
|
21
21
|
|
|
22
22
|
This page explains how data fetching works in Caspian. Use route functions for initial page data and use RPC actions for browser-triggered reads, writes, streams, uploads, and normal CRUD work.
|
|
23
23
|
|
|
24
|
-
Treat RPC as the default way for browser code to talk to Python in Caspian. For CRUD operations and any browser-initiated backend reads after first render, default to `@rpc()` on the server and `pp.rpc()` in PulsePoint code. Do not reach for ad hoc fetch calls to custom JSON endpoints, alternate transport layers, or older helper names unless the task explicitly requires that shape.
|
|
25
|
-
|
|
26
|
-
WebSockets are the main exception for long-lived bidirectional live channels when `caspian.config.json` has `websocket: true`. If the task needs a native browser `WebSocket`, persistent broadcast channel, presence, live chat, or socket origin/auth handling, confirm that flag and read [websockets.md](./websockets.md) before adding endpoint or client code. Do not use WebSockets as a replacement for ordinary RPC form submits or CRUD work.
|
|
27
|
-
|
|
28
|
-
When `caspian.config.json` has `prisma: true`, the Python side of those route and RPC actions must use the generated Prisma Python ORM from `src/lib/prisma/**` for database reads and writes. The browser asks Python through `pp.rpc(...)`; Python asks the database through Prisma. Do not replace either side with a custom browser fetch, direct database driver, hand-written SQL helper, JSON active store, or second app-owned data-fetch abstraction.
|
|
29
|
-
|
|
30
|
-
Browser-triggered data work should still be PulsePoint-first at the event layer. Bind the initiating click, submit, input, upload, refresh, filter, or pagination control in authored HTML with `onclick`, `onsubmit`, `oninput`, `onchange`, or another native `on*` attribute handled by PulsePoint. For normal form submissions, read named controls with `Object.fromEntries(new FormData(event.currentTarget).entries())` in the submit handler and pass that object directly to `pp.rpc(...)`; the route's Python `@rpc()` action should validate, normalize, and decide what to persist. Do not set up first-party data actions by assigning ids and then wiring `querySelector(...)`, `addEventListener(...)`, manual `fetch(...)`, manual DOM repainting, or per-input `pp-ref` payload collection.
|
|
24
|
+
Treat RPC as the default way for browser code to talk to Python in Caspian. For CRUD operations and any browser-initiated backend reads after first render, default to `@rpc()` on the server and `pp.rpc()` in PulsePoint code. Do not reach for ad hoc fetch calls to custom JSON endpoints, alternate transport layers, or older helper names unless the task explicitly requires that shape.
|
|
25
|
+
|
|
26
|
+
WebSockets are the main exception for long-lived bidirectional live channels when `caspian.config.json` has `websocket: true`. If the task needs a native browser `WebSocket`, persistent broadcast channel, presence, live chat, or socket origin/auth handling, confirm that flag and read [websockets.md](./websockets.md) before adding endpoint or client code. Do not use WebSockets as a replacement for ordinary RPC form submits or CRUD work.
|
|
27
|
+
|
|
28
|
+
When `caspian.config.json` has `prisma: true`, the Python side of those route and RPC actions must use the generated Prisma Python ORM from `src/lib/prisma/**` for database reads and writes. The browser asks Python through `pp.rpc(...)`; Python asks the database through Prisma. Do not replace either side with a custom browser fetch, direct database driver, hand-written SQL helper, JSON active store, or second app-owned data-fetch abstraction.
|
|
29
|
+
|
|
30
|
+
Browser-triggered data work should still be PulsePoint-first at the event layer. Bind the initiating click, submit, input, upload, refresh, filter, or pagination control in authored HTML with `onclick`, `onsubmit`, `oninput`, `onchange`, or another native `on*` attribute handled by PulsePoint. For normal form submissions, read named controls with `Object.fromEntries(new FormData(event.currentTarget).entries())` in the submit handler and pass that object directly to `pp.rpc(...)`; the route's Python `@rpc()` action should validate, normalize, and decide what to persist. Do not set up first-party data actions by assigning ids and then wiring `querySelector(...)`, `addEventListener(...)`, manual `fetch(...)`, manual DOM repainting, or per-input `pp-ref` payload collection.
|
|
31
31
|
|
|
32
32
|
MCP is a separate integration surface. Do not place app-owned FastMCP tools in route `index.py` files or treat `@rpc()` actions as a replacement for MCP tools. Use `mcp.md` and `src/lib/mcp/` only when `caspian.config.json` has `mcp: true`. If `mcp` is false, do not assume those files exist.
|
|
33
33
|
|
|
@@ -35,7 +35,7 @@ MCP is a separate integration surface. Do not place app-owned FastMCP tools in r
|
|
|
35
35
|
|
|
36
36
|
Caspian has two main data-loading paths:
|
|
37
37
|
|
|
38
|
-
- `page()` for initial-render data, plus `layout()` for shared props or metadata during the render
|
|
38
|
+
- `page()` for initial-render data, plus `layout()` for shared props or metadata during the render
|
|
39
39
|
- `@rpc()` plus `pp.rpc()` for interactive fetches after the page is already loaded
|
|
40
40
|
|
|
41
41
|
In practice, most pages use both:
|
|
@@ -50,13 +50,13 @@ When a page belongs to a grouped subtree such as a dashboard, account area, admi
|
|
|
50
50
|
|
|
51
51
|
## Default Data Rule
|
|
52
52
|
|
|
53
|
-
- Use `page()` for route-level data required before HTML renders, and use `layout()` only for shared subtree props or metadata.
|
|
53
|
+
- Use `page()` for route-level data required before HTML renders, and use `layout()` only for shared subtree props or metadata.
|
|
54
54
|
- When a route renders UI and also needs backend work, keep the HTML in the sibling `index.html`; `index.py` should prepare data and call `render_page(__file__, ...)`, not inline the route markup.
|
|
55
|
-
- Use `@rpc()` on the server and `pp.rpc()` in PulsePoint code for all browser-triggered data work after first render, including CRUD operations and follow-up reads.
|
|
56
|
-
- When Prisma is enabled, route all Python database I/O inside `page()`, `layout()` when truly shared, `@rpc()` actions, and reusable helpers through the generated Prisma Python ORM.
|
|
57
|
-
- Trigger those browser actions through PulsePoint event attributes in the HTML, not through a separate DOM listener layer.
|
|
58
|
-
- For simple form submits, prefer `onsubmit="{submitForm(event)}"` plus `Object.fromEntries(new FormData(event.currentTarget).entries())` over `pp-ref` fields and effect-managed submit listeners.
|
|
59
|
-
- Keep custom REST or other endpoint patterns as explicit exceptions, not the baseline Caspian approach.
|
|
55
|
+
- Use `@rpc()` on the server and `pp.rpc()` in PulsePoint code for all browser-triggered data work after first render, including CRUD operations and follow-up reads.
|
|
56
|
+
- When Prisma is enabled, route all Python database I/O inside `page()`, `layout()` when truly shared, `@rpc()` actions, and reusable helpers through the generated Prisma Python ORM.
|
|
57
|
+
- Trigger those browser actions through PulsePoint event attributes in the HTML, not through a separate DOM listener layer.
|
|
58
|
+
- For simple form submits, prefer `onsubmit="{submitForm(event)}"` plus `Object.fromEntries(new FormData(event.currentTarget).entries())` over `pp-ref` fields and effect-managed submit listeners.
|
|
59
|
+
- Keep custom REST or other endpoint patterns as explicit exceptions, not the baseline Caspian approach.
|
|
60
60
|
|
|
61
61
|
## Initial Data In `index.py`
|
|
62
62
|
|
|
@@ -87,11 +87,11 @@ If a route's first-render HTML is public and stable enough to reuse across reque
|
|
|
87
87
|
Notes:
|
|
88
88
|
|
|
89
89
|
- Prefer `async def page()` when your database or API client is async-capable.
|
|
90
|
-
- Put shared section-level props in `layout.py` when multiple child routes need the same payload. The current layout engine supports synchronous and async `layout()` results, but route-specific data should stay in `page()`.
|
|
90
|
+
- Put shared section-level props in `layout.py` when multiple child routes need the same payload. The current layout engine supports synchronous and async `layout()` results, but route-specific data should stay in `page()`.
|
|
91
91
|
- Use a normal parent folder such as `dashboard/` when the section name should appear in the URL. Use a route-group folder such as `(reports)/` only when the shared wrapper should organize child routes without adding a URL segment.
|
|
92
92
|
- Keep reusable database or API clients under `src/lib/`; keep route-specific orchestration in `src/app/`.
|
|
93
93
|
|
|
94
|
-
If the data source is Prisma, confirm `caspian.config.json` has `prisma: true`, then see `database.md` for the project's schema, migration, and generation workflow. If the project includes an app-owned Python database layer under `src/lib/prisma/`, reuse it instead of creating another helper. In Prisma-enabled projects, direct driver access and custom fetch helpers are exceptions to avoid, not alternate defaults.
|
|
94
|
+
If the data source is Prisma, confirm `caspian.config.json` has `prisma: true`, then see `database.md` for the project's schema, migration, and generation workflow. If the project includes an app-owned Python database layer under `src/lib/prisma/`, reuse it instead of creating another helper. In Prisma-enabled projects, direct driver access and custom fetch helpers are exceptions to avoid, not alternate defaults.
|
|
95
95
|
|
|
96
96
|
## Interactive Data With RPC
|
|
97
97
|
|
|
@@ -113,18 +113,18 @@ from src.lib.prisma import prisma
|
|
|
113
113
|
async def list_todos():
|
|
114
114
|
return await prisma.todo.find_many()
|
|
115
115
|
|
|
116
|
-
@rpc(require_auth=True, limits="20/minute")
|
|
117
|
-
async def create_todo(title: str):
|
|
118
|
-
title = title.strip()
|
|
119
|
-
if not title:
|
|
120
|
-
raise ValueError("Title is required.")
|
|
121
|
-
|
|
122
|
-
return await prisma.todo.create(
|
|
123
|
-
data={"title": title, "completed": False}
|
|
124
|
-
)
|
|
116
|
+
@rpc(require_auth=True, limits="20/minute")
|
|
117
|
+
async def create_todo(title: str):
|
|
118
|
+
title = title.strip()
|
|
119
|
+
if not title:
|
|
120
|
+
raise ValueError("Title is required.")
|
|
121
|
+
|
|
122
|
+
return await prisma.todo.create(
|
|
123
|
+
data={"title": title, "completed": False}
|
|
124
|
+
)
|
|
125
125
|
```
|
|
126
126
|
|
|
127
|
-
Call it from the client with `pp.rpc()`:
|
|
127
|
+
Call it from the client with `pp.rpc()`:
|
|
128
128
|
|
|
129
129
|
```html
|
|
130
130
|
<script>
|
|
@@ -137,39 +137,39 @@ Call it from the client with `pp.rpc()`:
|
|
|
137
137
|
const todo = await pp.rpc("create_todo", { title });
|
|
138
138
|
console.log(todo);
|
|
139
139
|
}
|
|
140
|
-
</script>
|
|
141
|
-
```
|
|
142
|
-
|
|
143
|
-
Do not call `fetch("/api/todos")` from the browser for normal Caspian CRUD when the route can expose an `@rpc()` action. Do not use `fetch()` or a database driver in Python to work around Prisma when `src/lib/prisma/**` already provides the generated ORM client.
|
|
144
|
-
|
|
145
|
-
For form submissions, let the form event provide the submitted values. The inputs' `name` attributes define the RPC payload keys, and the Python action owns trimming, validation, coercion, and persistence decisions.
|
|
146
|
-
|
|
147
|
-
```html
|
|
148
|
-
<form onsubmit="{createTodoFromForm(event)}" novalidate>
|
|
149
|
-
<input name="title" required />
|
|
150
|
-
<button type="submit" disabled="{isSaving}">Add</button>
|
|
151
|
-
|
|
152
|
-
<script>
|
|
153
|
-
const [isSaving, setIsSaving] = pp.state(false);
|
|
154
|
-
|
|
155
|
-
async function createTodoFromForm(event) {
|
|
156
|
-
event.preventDefault();
|
|
157
|
-
|
|
158
|
-
const data = Object.fromEntries(new FormData(event.currentTarget).entries());
|
|
159
|
-
if (isSaving) return;
|
|
160
|
-
|
|
161
|
-
setIsSaving(true);
|
|
162
|
-
try {
|
|
163
|
-
await pp.rpc("create_todo", data, { abortPrevious: true });
|
|
164
|
-
} finally {
|
|
165
|
-
setIsSaving(false);
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
</script>
|
|
169
|
-
</form>
|
|
170
|
-
```
|
|
171
|
-
|
|
172
|
-
Use RPC for:
|
|
140
|
+
</script>
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Do not call `fetch("/api/todos")` from the browser for normal Caspian CRUD when the route can expose an `@rpc()` action. Do not use `fetch()` or a database driver in Python to work around Prisma when `src/lib/prisma/**` already provides the generated ORM client.
|
|
144
|
+
|
|
145
|
+
For form submissions, let the form event provide the submitted values. The inputs' `name` attributes define the RPC payload keys, and the Python action owns trimming, validation, coercion, and persistence decisions.
|
|
146
|
+
|
|
147
|
+
```html
|
|
148
|
+
<form onsubmit="{createTodoFromForm(event)}" novalidate>
|
|
149
|
+
<input name="title" required />
|
|
150
|
+
<button type="submit" disabled="{isSaving}">Add</button>
|
|
151
|
+
|
|
152
|
+
<script>
|
|
153
|
+
const [isSaving, setIsSaving] = pp.state(false);
|
|
154
|
+
|
|
155
|
+
async function createTodoFromForm(event) {
|
|
156
|
+
event.preventDefault();
|
|
157
|
+
|
|
158
|
+
const data = Object.fromEntries(new FormData(event.currentTarget).entries());
|
|
159
|
+
if (isSaving) return;
|
|
160
|
+
|
|
161
|
+
setIsSaving(true);
|
|
162
|
+
try {
|
|
163
|
+
await pp.rpc("create_todo", data, { abortPrevious: true });
|
|
164
|
+
} finally {
|
|
165
|
+
setIsSaving(false);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
</script>
|
|
169
|
+
</form>
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Use RPC for:
|
|
173
173
|
|
|
174
174
|
- CRUD reads and writes after the initial render
|
|
175
175
|
- Button-triggered refreshes
|
|
@@ -186,8 +186,8 @@ Important:
|
|
|
186
186
|
|
|
187
187
|
- `pp.rpc()` posts to the current route RPC bridge.
|
|
188
188
|
- Older docs may refer to `pp.fetchFunction()`. In this repo's current runtime, the supported helper is `pp.rpc()`.
|
|
189
|
-
- Use [pulsepoint-runtime-map.md](./pulsepoint-runtime-map.md) when debugging browser-side RPC options such as streaming, upload progress, abort behavior, redirects, or SPA interaction.
|
|
190
|
-
- Use [websockets.md](./websockets.md) when `caspian.config.json` has `websocket: true` and the browser and server need a persistent bidirectional channel instead of request/response RPC or one-way SSE streaming.
|
|
189
|
+
- Use [pulsepoint-runtime-map.md](./pulsepoint-runtime-map.md) when debugging browser-side RPC options such as streaming, upload progress, abort behavior, redirects, or SPA interaction.
|
|
190
|
+
- Use [websockets.md](./websockets.md) when `caspian.config.json` has `websocket: true` and the browser and server need a persistent bidirectional channel instead of request/response RPC or one-way SSE streaming.
|
|
191
191
|
|
|
192
192
|
## Streaming Responses
|
|
193
193
|
|
|
@@ -227,6 +227,43 @@ Client example:
|
|
|
227
227
|
|
|
228
228
|
Use streaming when the user should see partial progress instead of waiting for a single final payload.
|
|
229
229
|
|
|
230
|
+
This is the shipped, default mechanism for AI/LLM/chat token streaming in Caspian. When a task asks for a chat app, an assistant reply, or any "stream the model's tokens as they arrive" experience, use this RPC streaming path. Do not reinvent one-way streaming with raw `fetch` + `ReadableStream`, a browser `EventSource`, or a WebSocket. WebSockets are only for genuinely bidirectional channels (see [websockets.md](./websockets.md)); a one-way token stream from server to browser belongs in a generator `@rpc()` consumed by `pp.rpc(..., { onStream })`.
|
|
231
|
+
|
|
232
|
+
Bridging a Python LLM/SDK stream is the common case: `async for` over the provider's streaming response inside the `@rpc()` action and `yield` each chunk. The runtime turns the generator into an SSE response automatically.
|
|
233
|
+
|
|
234
|
+
```python
|
|
235
|
+
from casp.rpc import rpc
|
|
236
|
+
from anthropic import AsyncAnthropic
|
|
237
|
+
|
|
238
|
+
client = AsyncAnthropic()
|
|
239
|
+
|
|
240
|
+
@rpc(require_auth=True, limits="20/minute")
|
|
241
|
+
async def chat(message: str):
|
|
242
|
+
async with client.messages.stream(
|
|
243
|
+
model="claude-opus-4-8",
|
|
244
|
+
max_tokens=1024,
|
|
245
|
+
messages=[{"role": "user", "content": message}],
|
|
246
|
+
) as stream:
|
|
247
|
+
async for text in stream.text_stream:
|
|
248
|
+
yield text
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
Client side, append each chunk to reactive state so the UI grows token by token:
|
|
252
|
+
|
|
253
|
+
```html
|
|
254
|
+
<script>
|
|
255
|
+
const [answer, setAnswer] = pp.state("");
|
|
256
|
+
|
|
257
|
+
async function send(message) {
|
|
258
|
+
setAnswer("");
|
|
259
|
+
await pp.rpc("chat", { message }, {
|
|
260
|
+
onStream: (chunk) => setAnswer((current) => current + chunk),
|
|
261
|
+
onStreamComplete: () => console.log("done"),
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
</script>
|
|
265
|
+
```
|
|
266
|
+
|
|
230
267
|
In the current runtime, generator and async-generator RPC results are wrapped with `SSE(...)` inside `casp.rpc.py`, and route-level generator results are also wrapped by `main.py` before the response is returned.
|
|
231
268
|
|
|
232
269
|
## File Uploads
|
|
@@ -334,7 +371,7 @@ For initial route rendering with `render_page(...)`, prefer passing plain templa
|
|
|
334
371
|
|
|
335
372
|
## Recommended Decision Rule
|
|
336
373
|
|
|
337
|
-
Use `page()` when route-specific data is part of the page render. Use `layout()` for shared subtree props or metadata. Use `@rpc()` plus `pp.rpc()` when the browser needs to ask the server for more data after the page is already visible.
|
|
374
|
+
Use `page()` when route-specific data is part of the page render. Use `layout()` for shared subtree props or metadata. Use `@rpc()` plus `pp.rpc()` when the browser needs to ask the server for more data after the page is already visible.
|
|
338
375
|
|
|
339
376
|
A common pattern is:
|
|
340
377
|
|
|
@@ -366,8 +403,8 @@ If the first-render HTML is expensive to produce and safe to share between visit
|
|
|
366
403
|
If an AI agent is choosing how to load data in Caspian, apply these rules first.
|
|
367
404
|
|
|
368
405
|
- Put first-render data loading in `src/app/**/index.py`.
|
|
369
|
-
- Put shared section props in `layout.py` only when multiple child routes need the same shared data.
|
|
370
|
-
- Keep route-specific async I/O in `page()` or `@rpc()` so `layout.py` stays focused on shared subtree props or metadata.
|
|
406
|
+
- Put shared section props in `layout.py` only when multiple child routes need the same shared data.
|
|
407
|
+
- Keep route-specific async I/O in `page()` or `@rpc()` so `layout.py` stays focused on shared subtree props or metadata.
|
|
371
408
|
- For grouped subtrees, follow the section layout pattern in [routing.md](./routing.md) before deciding where `page()` data and `@rpc()` actions belong.
|
|
372
409
|
- Treat RPC as the default read and write layer between PulsePoint code and Python route logic, especially for CRUD and interactive backend reads.
|
|
373
410
|
- Use `@rpc()` for backend functions that should be callable from the browser.
|
|
@@ -377,6 +414,7 @@ If an AI agent is choosing how to load data in Caspian, apply these rules first.
|
|
|
377
414
|
- Use [cache.md](./cache.md) when a route's initial HTML should be reused across requests and invalidated after writes.
|
|
378
415
|
- Use [state.md](./state.md) when an RPC mutation needs transient request-scoped success or error state outside the direct response payload.
|
|
379
416
|
- Use `onStream` for streamed responses and `onUploadProgress` for upload-aware client calls.
|
|
417
|
+
- For AI/LLM/chat token streaming, default to a generator `@rpc()` that `yield`s chunks (bridge a Python SDK stream with `async for ... yield`) consumed by `pp.rpc(..., { onStream })`. Do not reinvent one-way streaming with raw `fetch`/`ReadableStream`, `EventSource`, or WebSockets.
|
|
380
418
|
- Add `require_auth`, `allowed_roles`, and `limits` to sensitive RPC actions.
|
|
381
419
|
- Keep reusable database clients and service wrappers in `src/lib/`.
|
|
382
420
|
- Use `.venv/Lib/site-packages/casp/rpc.py` only when the task is about Caspian core RPC internals or framework debugging.
|
package/dist/docs/pulsepoint.md
CHANGED
|
@@ -346,11 +346,12 @@ Important:
|
|
|
346
346
|
- The script lookup walks the current root and skips nested `pp-component` boundaries, so a parent does not consume a child component's script.
|
|
347
347
|
- If multiple matching runtime scripts exist in the same root, the first matching owned script wins. Generate one script per root.
|
|
348
348
|
- Authored route, layout, and component templates still need one top-level parent node so Caspian can inject the component boundary correctly after component expansion.
|
|
349
|
-
- A scriptless component root still mounts and can receive props, refs, events, and nested children, but it
|
|
349
|
+
- A scriptless component root still mounts and can receive props, refs, events, and nested children, but it does not create local bindings from those props on its own.
|
|
350
350
|
- Component scripts are plain JavaScript executed with `new Function(...)`. Do not use `import`, `export`, or top-level `await` inside them.
|
|
351
351
|
- The runtime auto-returns supported top-level bindings from the script. Do not rely on manual `return { ... }` objects.
|
|
352
|
-
- `pp.props` contains the current prop bag for the component.
|
|
353
|
-
- `pp.props.children` contains the root's initial inner HTML before the owned script is removed from the render template.
|
|
352
|
+
- `pp.props` contains the current prop bag for the component.
|
|
353
|
+
- `pp.props.children` contains the root's initial inner HTML before the owned script is removed from the render template.
|
|
354
|
+
- Props are not auto-injected as standalone top-level template or handler variables. Read them through `pp.props` or explicitly destructure them in the component script.
|
|
354
355
|
|
|
355
356
|
Bindings exported to the template:
|
|
356
357
|
|
|
@@ -569,12 +570,13 @@ When a child component needs the same token object, pass it from the provider sc
|
|
|
569
570
|
|
|
570
571
|
- Child component props are derived from DOM attributes.
|
|
571
572
|
- Attribute names are converted from kebab-case to camelCase for the prop bag.
|
|
572
|
-
- Native `on*` attributes and `pp-component` are not included in props.
|
|
573
|
-
- Empty attributes become boolean `true` props.
|
|
574
|
-
- Pure prop expressions such as `title="{pageTitle}"` are evaluated in the parent scope.
|
|
575
|
-
- Mixed strings such as `class="card {isActive ? 'active' : ''}"` are interpolated in the parent scope.
|
|
576
|
-
- Non-expression attribute values are passed through as strings.
|
|
577
|
-
- `children` is injected into props using the root's initial inner HTML.
|
|
573
|
+
- Native `on*` attributes and `pp-component` are not included in props.
|
|
574
|
+
- Empty attributes become boolean `true` props.
|
|
575
|
+
- Pure prop expressions such as `title="{pageTitle}"` are evaluated in the parent scope.
|
|
576
|
+
- Mixed strings such as `class="card {isActive ? 'active' : ''}"` are interpolated in the parent scope.
|
|
577
|
+
- Non-expression attribute values are passed through as strings.
|
|
578
|
+
- `children` is injected into props using the root's initial inner HTML.
|
|
579
|
+
- Inside the child component, those values should be accessed through `pp.props` or a top-level destructure such as `const { title } = pp.props`; they are not implicitly available as bare identifiers.
|
|
578
580
|
|
|
579
581
|
Nested components:
|
|
580
582
|
|
|
@@ -751,7 +753,7 @@ Upload with progress pattern:
|
|
|
751
753
|
</section>
|
|
752
754
|
```
|
|
753
755
|
|
|
754
|
-
Streaming pattern for server generators that return `text/event-stream
|
|
756
|
+
Streaming pattern for server generators that return `text/event-stream`. This is the default path for AI/LLM/chat token streaming: a generator `@rpc()` that `yield`s tokens (often by `async for`-ing over a Python SDK stream) consumed by `pp.rpc(..., { onStream })` that appends each chunk to reactive state. Do not reinvent one-way streaming with raw `fetch`/`ReadableStream`, `EventSource`, or a WebSocket; see [fetch-data.md](./fetch-data.md) for the Python side.
|
|
755
757
|
|
|
756
758
|
```html
|
|
757
759
|
<section>
|