Skip to content

Commit 6631180

Browse files
committed
a first version of CLAUDE.md, there could be more here
1 parent 37c7579 commit 6631180

1 file changed

Lines changed: 378 additions & 0 deletions

File tree

CLAUDE.md

Lines changed: 378 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,378 @@
1+
# py4web Development Guide for Claude
2+
3+
## Project Overview
4+
5+
py4web is a Python web framework for rapid development of database-driven web applications. It is the modern successor to web2py, built on ombott (optimized Bottle fork) and PyDAL.
6+
7+
## App Structure
8+
9+
Every py4web app is a Python package (folder with `__init__.py`) inside the `apps/` directory. Standard layout:
10+
11+
```
12+
apps/myapp/
13+
__init__.py # App init: version check, import controllers and db
14+
settings.py # All configuration (DB_URI, session type, auth, etc.)
15+
common.py # Initialize fixtures: db, session, auth, T, cache, flash, logger
16+
models.py # Database table definitions using PyDAL
17+
controllers.py # Action handlers (routes)
18+
tasks.py # Scheduler/Celery tasks (optional)
19+
templates/
20+
layout.html # Base template (extend this)
21+
index.html # Page templates
22+
auth.html # Auth forms
23+
static/
24+
js/utils.js # Standard JS utility library (Q selector, AJAX, etc.)
25+
css/no.css # Default minimal CSS framework
26+
databases/ # SQLite files and migration metadata
27+
translations/ # i18n JSON files
28+
uploads/ # User-uploaded files
29+
```
30+
31+
## Python Conventions
32+
33+
### Actions (Controllers)
34+
35+
```python
36+
from py4web import action, URL, redirect, request, abort
37+
from .common import db, session, auth, T, flash, cache
38+
39+
# Basic action
40+
@action("index")
41+
@action.uses("index.html", auth, T)
42+
def index():
43+
user = auth.get_user()
44+
return dict(message="Hello")
45+
46+
# With URL parameters
47+
@action("item/<id:int>")
48+
@action.uses(db, auth.user)
49+
def item(id):
50+
record = db.thing[id] or abort(404)
51+
return dict(record=record)
52+
53+
# API endpoint returning JSON (no template)
54+
@action("api/data", method="GET")
55+
@action.uses(db, auth.user)
56+
def api_data():
57+
rows = db(db.thing).select()
58+
return dict(data=rows.as_list())
59+
60+
# POST action
61+
@action("api/create", method="POST")
62+
@action.uses(db, auth.user)
63+
def api_create():
64+
db.thing.insert(**request.json)
65+
return dict(ok=True)
66+
```
67+
68+
**Rules:**
69+
- `@action("path")` defines the route. The URL is `/{app_name}/{path}`.
70+
- `@action.uses(...)` declares fixtures. Template must be **first** if present.
71+
- `@action.uses(auth.user)` requires login; `@action.uses(auth)` makes login optional.
72+
- Actions return `dict` (rendered by template or as JSON), `str`, or raise `redirect()`/`abort()`.
73+
- Use `auth.get_user()` to get current user dict, `auth.user_id` for just the ID.
74+
- Use `request.query.get("param")` for query params, `request.json` for JSON body, `request.forms` for form data.
75+
- Multiple routes: stack `@action` decorators on the same function.
76+
- HTTP methods: `@action("path", method=["GET", "POST"])`.
77+
78+
### Convenience Decorators
79+
80+
`common.py` defines `authenticated` and `unauthenticated` ActionFactory decorators:
81+
82+
```python
83+
# Instead of @action.uses(db, session, T, auth.user)
84+
@authenticated()
85+
def protected():
86+
return dict()
87+
88+
# Instead of @action.uses(db, session, T, auth)
89+
@unauthenticated()
90+
def public():
91+
return dict()
92+
```
93+
94+
### Database Models
95+
96+
```python
97+
from pydal.validators import *
98+
from .common import db, Field, auth
99+
100+
db.define_table("thing",
101+
Field("name", requires=IS_NOT_EMPTY()),
102+
Field("description", "text"),
103+
Field("quantity", "integer", default=0),
104+
Field("owner", "reference auth_user"),
105+
auth.signature, # Adds created_by, created_on, modified_by, modified_on
106+
)
107+
db.commit()
108+
```
109+
110+
**Rules:**
111+
- Define tables in `models.py` at module level (executed once at startup).
112+
- Always call `db.commit()` after table definitions.
113+
- `auth.signature` adds ownership/timestamp fields automatically.
114+
- Use PyDAL field types: `string`, `text`, `integer`, `float`, `boolean`, `date`, `datetime`, `reference tablename`, `upload`, `json`, `list:string`, `list:integer`.
115+
- Validators: `IS_NOT_EMPTY()`, `IS_IN_SET(["a","b"])`, `IS_INT_IN_RANGE(0, 100)`, `IS_EMAIL()`, `IS_MATCH(regex)`, etc.
116+
- Only field attributes (readable, writable, requires, default) are thread-safe to modify in actions.
117+
118+
### Database Queries
119+
120+
```python
121+
# Select
122+
rows = db(db.thing.owner == auth.user_id).select(orderby=~db.thing.created_on, limitby=(0, 100))
123+
124+
# Insert
125+
id = db.thing.insert(name="foo", description="bar")
126+
127+
# Update
128+
db(db.thing.id == id).update(name="new name")
129+
130+
# Delete
131+
db(db.thing.id == id).delete()
132+
133+
# Joins
134+
rows = db(db.thing.owner == db.auth_user.id).select(db.thing.ALL, db.auth_user.email)
135+
136+
# Count
137+
n = db(db.thing.owner == auth.user_id).count()
138+
139+
# Complex queries with & (AND) and | (OR)
140+
query = (db.thing.name.contains("foo")) & (db.thing.quantity > 0)
141+
rows = db(query).select()
142+
```
143+
144+
### Forms
145+
146+
```python
147+
form = Form(db.thing, csrf_session=session)
148+
if form.accepted:
149+
redirect(URL("index"))
150+
return dict(form=form)
151+
```
152+
153+
- `Form(db.table)` for create, `Form(db.table, record_id)` for edit.
154+
- Check `form.accepted`, `form.deleted`, `form.errors`.
155+
156+
### Grid
157+
158+
```python
159+
grid = Grid(db.thing, formstyle=FormStyleDefault)
160+
return dict(grid=grid)
161+
```
162+
163+
### Authorization with Tags
164+
165+
```python
166+
from pydal.tools.tags import Tags
167+
groups = Tags(db.auth_user, "groups")
168+
groups.add(user_id, "manager")
169+
if "manager" in groups.get(user_id):
170+
...
171+
```
172+
173+
### URL Generation
174+
175+
```python
176+
URL("action_name") # /{app}/action_name
177+
URL("action", 1, 2) # /{app}/action/1/2
178+
URL("action", vars=dict(x=1)) # /{app}/action?x=1
179+
URL("static", "js/index.js") # /{app}/static/js/index.js
180+
URL("action", scheme=True) # https://host/{app}/action
181+
```
182+
183+
### Custom Fixtures
184+
185+
```python
186+
from py4web.core import Fixture
187+
188+
class MyFixture(Fixture):
189+
def on_request(self, context):
190+
# Called before action
191+
pass
192+
def on_success(self, context):
193+
# Called after successful action
194+
pass
195+
def on_error(self, context):
196+
# Called on error
197+
pass
198+
```
199+
200+
### URL Signing (CSRF/tamper protection for callbacks)
201+
202+
```python
203+
from py4web.utils.url_signer import URLSigner
204+
url_signer = URLSigner(session)
205+
206+
@action("callback", method="POST")
207+
@action.uses(db, session, url_signer.verify())
208+
def callback():
209+
...
210+
211+
# In controller, pass signed URL:
212+
return dict(callback_url=URL("callback", signer=url_signer))
213+
```
214+
215+
## Template Conventions (YATL)
216+
217+
py4web uses `[[...]]` delimiters (NOT `{{...}}` or `{%...%}`) to avoid conflicts with Vue.js/Angular.
218+
219+
```html
220+
[[extend "layout.html"]]
221+
222+
<!-- Output (auto-escaped) -->
223+
[[=variable]]
224+
225+
<!-- Raw output (no escaping) -->
226+
[[=XML(html_string)]]
227+
228+
<!-- Conditionals -->
229+
[[if condition:]]
230+
<p>yes</p>
231+
[[elif other:]]
232+
<p>other</p>
233+
[[else:]]
234+
<p>no</p>
235+
[[pass]]
236+
237+
<!-- Loops -->
238+
[[for item in items:]]
239+
<p>[[=item.name]]</p>
240+
[[pass]]
241+
242+
<!-- Blocks (for template inheritance) -->
243+
[[block page_head]]
244+
<link rel="stylesheet" href="[[=URL('static','css/custom.css')]]">
245+
[[end]]
246+
247+
<!-- Include -->
248+
[[include "component.html"]]
249+
```
250+
251+
**Rules:**
252+
- Always `[[extend "layout.html"]]` at top for consistent look.
253+
- Use `[[block name]]...[[end]]` to override sections from layout.
254+
- Actions return `dict(...)` and template variables are accessed directly by name.
255+
- `[[=form]]` renders a Form object. `[[=grid]]` renders a Grid.
256+
- `BEAUTIFY(__vars__)` in `generic.html` for debugging.
257+
258+
## JavaScript Conventions
259+
260+
### Q Utility Library (utils.js)
261+
262+
All apps include `utils.js` which provides a lightweight jQuery alternative:
263+
264+
```javascript
265+
// DOM selection
266+
Q("selector") // querySelectorAll wrapper
267+
Q(".myclass")[0].style.display = "none"
268+
269+
// AJAX
270+
Q.get(url) // GET, returns Promise
271+
Q.post(url, data) // POST with JSON body
272+
Q.put(url, data) // PUT
273+
Q.delete(url) // DELETE
274+
Q.ajax("GET", url, data, headers) // Full control
275+
276+
// Cookies
277+
Q.get_cookie("name")
278+
279+
// Forms
280+
Q.trap_form(action, elem_id) // AJAX form submission
281+
282+
// Components
283+
Q.handle_components() // Process <ajax-component> tags
284+
Q.flash({message, class}) // Show flash alert
285+
286+
// Translation
287+
T("string") // Client-side i18n
288+
```
289+
290+
### Vue.js Pattern (for reactive UIs)
291+
292+
```javascript
293+
// static/js/index.js
294+
var app = {
295+
data() {
296+
return { items: [], content: "" };
297+
},
298+
methods: {
299+
submit() {
300+
axios.post(create_url, { content: this.content })
301+
.then(() => { this.content = ""; this.reload(); });
302+
},
303+
reload() {
304+
axios.get(data_url).then(r => { this.items = r.data.items; });
305+
},
306+
},
307+
mounted() {
308+
this.reload();
309+
},
310+
};
311+
312+
Vue.createApp(app).mount("#app");
313+
```
314+
315+
- Use Vue 3 with `createApp()`.
316+
- Use Axios for HTTP requests.
317+
- Pass URLs from controller via template variables (do NOT hardcode paths in JS).
318+
- Template uses `v-model`, `v-for`, `@click`, `v-if` etc.
319+
320+
### Inline JS in Templates
321+
322+
For simple interactivity without Vue:
323+
324+
```html
325+
<script>
326+
function callback(elem) {
327+
Q.post(elem.getAttribute("data-url"), {})
328+
.then(() => location.reload());
329+
}
330+
</script>
331+
<button data-url="[[=URL('my_action')]]" onclick="callback(this)">Click</button>
332+
```
333+
334+
## CSS Conventions
335+
336+
- Default framework: `no.css` (classless, styles semantic HTML automatically).
337+
- Alternative: Bulma (used in birdwatching app).
338+
- Grid classes: `.columns`, `.c25`, `.c33`, `.c50`, `.c66`, `.c75`.
339+
- Color classes: `.success`, `.warning`, `.error`, `.info`, `.black`, `.white`.
340+
- Use `class="padded"` for spacing.
341+
342+
## Important Rules
343+
344+
1. **Fixtures declare dependencies.** Every action must list what it needs via `@action.uses()`. No global middleware.
345+
2. **Template first.** When using a template fixture, it must be the first argument in `@action.uses("template.html", ...)`.
346+
3. **No hardcoded URLs.** Always use `URL()` to generate paths. Pass URLs to JavaScript via template variables.
347+
4. **Settings in settings.py.** Never hardcode configuration values in controllers or models.
348+
5. **Shared state in common.py.** All fixtures (`db`, `session`, `auth`, `T`, `cache`, `flash`) are initialized in `common.py` and imported elsewhere.
349+
6. **`db.commit()` after table definitions.** Required in `models.py` after `db.define_table()` calls.
350+
7. **Thread safety.** Table definitions are NOT thread-safe. Only field attributes (readable, writable, requires, default) can be modified per-request.
351+
8. **DAL transactions are automatic.** Commit on success, rollback on error. No manual commit needed in actions.
352+
9. **YATL delimiters are `[[...]]`**, not `{{...}}`. This avoids conflicts with Vue.js.
353+
10. **Use `auth.signature`** on tables that need ownership tracking.
354+
11. **Static files** go in `static/` and are served at `/{app}/static/...`.
355+
12. **Translations** go in `translations/` as JSON files keyed by language code.
356+
357+
## Testing
358+
359+
Run tests with:
360+
```bash
361+
python -m pytest tests/
362+
```
363+
364+
## Building & Running
365+
366+
```bash
367+
# Install
368+
pip install py4web
369+
370+
# Setup (first time)
371+
py4web setup apps
372+
373+
# Run development server
374+
py4web run apps
375+
376+
# Run on specific port
377+
py4web run --port 8000 apps
378+
```

0 commit comments

Comments
 (0)