# Shipy

The opinionated Indie Maker Python web framework for shipping MVPs stupid-fast.

- App, Request, Response, routing (imperative)
- Jinja2 templates via `shipy.render.render()`
- HTMX support for interactive UI without complex JavaScript
- SQLite helpers in `shipy.sql` (`query`, `one`, `execute`, `tx`)
- Built-in auth with users/sessions + `@login_required` decorator
- Request middleware for per-request data and short-circuiting
- Signed-cookie sessions with CSRF + flash in `shipy.session`
- CLI: `shipy new`, `shipy dev`, `shipy db init`, `shipy deploy`

Note: The SQL write helper is named `execute` (not `exec`, since `exec` is a Python keyword).

## Quick start

```bash
pip install shipy-web
shipy new myapp && cd myapp
shipy db init
shipy dev
```

Visit http://localhost:8000 and sign up to get started!

## Example

See `examples/hello/app/main.py:1` and `examples/hello/app/views/home/index.html:1`.

## CLI

- `shipy new <name>` — creates an auth-first scaffolded project in `./<name>`
- `shipy dev [--app app.main:app] [--host 127.0.0.1] [--port 8000]`
- `shipy db init [--db ./data/app.db] [--schema data/schema.sql]` — initialize database
- `shipy db backup [--db ./data/app.db] [--out data/backups]` — create database backup
- `shipy db run <path.sql> [--db ./data/app.db]` — execute SQL script
- `shipy db make-migration <name> [--dir data/migrations]` — create timestamped migration file
- `shipy db ls [--dir data/migrations]` — list migration files
- `shipy db shell [--db ./data/app.db]` — open interactive sqlite3 shell
- `shipy deploy` — deployment helpers

## Templates

Default template root is `app/views`. Use `shipy.render.render('path/to/template.html', ctx, request=req)`.

### HTMX Support

Shipy includes built-in HTMX support for interactive UI without complex JavaScript:

```python
from shipy.render import render_htmx, is_htmx_request

# Use render_htmx for HTMX-enhanced templates
def home(req):
    todos = query("SELECT * FROM todos ORDER BY created_at DESC")
    return render_htmx(req, "home/index.html", todos=todos)

# Check if request is from HTMX
def todo_create(req):
    if is_htmx_request(req):
        # Return partial for HTMX
        return render_htmx(req, "todos/list.html", todos=todos)
    else:
        # Return full page
        return render_req(req, "home/index.html", todos=todos)
```

HTMX templates have access to `htmx` context:

- `htmx.request` - true if request is from HTMX
- `htmx.target` - HTMX target element
- `htmx.trigger` - HTMX trigger event

```html
<!-- HTMX form with partial updates -->
<form hx-post="/todos" hx-target="#todo-list" hx-swap="innerHTML">
  <input name="title" placeholder="New todo" />
  <button>Add</button>
</form>

<div id="todo-list">
  {% for todo in todos %}
  <div hx-delete="/todos/{{ todo.id }}" hx-target="this" hx-swap="outerHTML">
    {{ todo.title }}
    <button>Delete</button>
  </div>
  {% endfor %}
</div>
```

HTMX is included by default in scaffolded apps via CDN.

## Middleware

Request middleware runs on every request after the Request is constructed but before route handlers. Use `@app.middleware("request")` to register middleware functions.

```python
# Attach commonly used data to req.state
@app.middleware("request")
def attach_user(req):
    req.state.user = current_user(req)
    req.state.csrf_token = get_csrf_token(req)

# Short-circuit with a Response (e.g., maintenance mode)
@app.middleware("request")
def maintenance_mode(req):
    if maintenance_enabled:
        return Response.text("Site under maintenance", 503)

# Use in route handlers
def home(req):
    user = req.state.user  # Available via middleware
    return render_req(req, "home.html", user=user)
```

Middleware can:

- Attach data to `req.state` for easy access in handlers
- Short-circuit requests by returning a Response
- Handle errors and logging
- Add per-request objects (DB connections, etc.)

## Sessions

Session data is stored in a signed cookie using `itsdangerous`. Set `SHIPY_SECRET` in your environment for production.

Flash messages are available via `req.session.flash(message, category)` and `req.session.get_flashed_messages()` in requests. CSRF token via `req.session.get_csrf_token()`.

### Authentication

Shipy includes built-in authentication helpers:

```python
from shipy.auth import current_user, login_required, login, logout

# Check if user is logged in
user = current_user(req)

# Protect routes with @login_required decorator
@login_required()
def secret(req):
    # req.state.user is guaranteed to exist here
    return render_req(req, "secret.html", user=req.state.user)

# Custom redirect for unauthenticated users
@login_required(redirect_to="/custom-login")
def admin(req):
    return render_req(req, "admin.html", user=req.state.user)

# Manual authentication (for custom logic)
def manual_auth(req):
    user = current_user(req)
    if not user:
        return Response.redirect("/login")
    return render_req(req, "page.html", user=user)
```

The `@login_required` decorator:

- Redirects unauthenticated users to `/login` (or custom path)
- Automatically attaches user to `req.state.user`
- Works with middleware for additional per-request data

## Database

SQLite helpers in `shipy/sql.py:1` use `./data/app.db` by default (created as needed).

Override the database path with `SHIPY_DB` environment variable:

```bash
SHIPY_DB=/path/to/custom.db shipy dev
```

- `query(sql, params=()) -> list[dict]`
- `one(sql, params=()) -> dict | None`
- `execute(sql, params=()) -> int`
- `with tx() as conn: conn.execute(...)`

Apply schema with `shipy db init --schema data/schema.sql`.

### Migrations

For one-off database changes, use the simple migration system:

```bash
# Create a migration file
shipy db make-migration "add user profiles"

# Edit the generated file in data/migrations/
# Then apply it
shipy db run data/migrations/20250926185037_add_user_profiles.sql

# List all migrations
shipy db ls

# Open interactive sqlite3 shell for debugging
shipy db shell
```

Migrations are plain SQL files wrapped in transactions for atomicity. No complex migration engine - just timestamped files you can read and understand.

## License

MIT.
