Metadata-Version: 2.4
Name: caspian-utils
Version: 0.4.22
Summary: A utility package for Caspian projects
Home-page: https://github.com/TheSteelNinjaCode/caspian_utils
Author: Jefferson Abraham
Classifier: Programming Language :: Python :: 3
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Requires-Python: >=3.14
Description-Content-Type: text/markdown
Dynamic: author
Dynamic: classifier
Dynamic: description
Dynamic: description-content-type
Dynamic: home-page
Dynamic: requires-python
Dynamic: summary

# Caspian Utils (`casp`)

HTML-first utilities for Caspian applications.

`caspian-utils` is the shared Python runtime package behind Caspian templates, components, layouts, RPC handlers, and supporting utilities. This repository is not a full application starter, so this README documents the package surface that exists here.

- PyPI package: `caspian-utils`
- Python package: `casp`
- Python requirement: `>=3.14`

## Installation

```bash
pip install caspian-utils
```

## Core Model

Caspian is HTML-first.

- write UI in `.html`
- use native Jinja syntax for server-rendered values and control flow
- define reusable components in Python
- import components with `@import` comments inside HTML
- render components with `<x-component-name />` tags
- place child routes in layouts with a real HTML `<slot />`
- render pages and layouts from Python with helpers in `casp.layout`

There is no Caspian-only template syntax layer. Use standard Jinja `{{ ... }}` and `{% ... %}` directly.

## HTML-First Components

### Define a component in Python

Return the markup from `html(...)`, which renders the string through the same Jinja environment Caspian uses for template files.

```py
from casp.component_decorator import component, html


@component
def AlertBox(title: str, children: str = "", class_name: str = "") -> str:
    return html("""
    <section class="alert {{ class_name }}">
        <h2>{{ title }}</h2>
        <div>{{ children }}</div>
    </section>
    """, title=title, children=children, class_name=class_name)
```

Prefer `html(...)` over a Python f-string for component markup. An f-string consumes single braces, so `{likes}` intended for client-side reactivity has to be written `{{likes}}` and the two brace dialects start fighting. With `html(...)` they coexist:

| Syntax | Meaning |
| --- | --- |
| `{{ value }}` | server render (Python to HTML), autoescaped |
| `{{ value \| json }}` | serialize a server value into a `<script>`, returns `Markup` |
| `{# comment #}` | Jinja comment, stripped from output |
| `{ value }` | left untouched for client-side reactivity |

Autoescaping is on, so `{{ value }}` escapes user text automatically; trusted HTML needs `Markup(...)` or `| safe`. A `children` value is marked safe for you, so nested component markup renders without `| safe`.

For large markup or long scripts, keep the Python file thin and put the markup in a sibling `.html` file with `render_html`:

```py
from casp.component_decorator import component, render_html


@component
def AlertBox(title: str, children: str = "") -> str:
    return render_html(__file__, {"title": title, "children": children})
```

`render_html(__file__, {...})` and `render_html(__file__, title="Saved")` are both accepted. Either form must render exactly one top-level element, with any `<script>` nested inside it.

### Import and use it in HTML

```html
<!-- @import { AlertBox } from "../components/ui" -->

<main class="space-y-4">
  <x-alert-box title="Saved">
    <p>Your settings were updated.</p>
  </x-alert-box>
</main>
```

### Alias imports when needed

```html
<!-- @import { AlertBox as Notice } from "../components/ui" -->

<div>
  <x-notice title="Heads up" />
</div>
```

Import and tag rules:

- `AlertBox` becomes `<x-alert-box>`
- aliases also convert to kebab-case, so `Notice` becomes `<x-notice>`
- grouped imports use `<!-- @import { A, B as C } from "..." -->`
- single imports use `<!-- @import ComponentName from "..." -->`
- paths are resolved relative to the current template or component directory

Example: `<!-- @import { AlertBox } from "../components/ui" -->` resolves `AlertBox` from `../components/ui/AlertBox.py`.

## Rendering Pages

Use `casp.layout` to render HTML templates relative to a Python file.

```py
from casp.layout import render_page


def get_dashboard() -> str:
    return render_page(__file__, {
        "pageTitle": "Dashboard",
        "stats": ["Projects", "Tasks", "Alerts"],
    })
```

```html
<!-- app/dashboard/index.html -->
<!-- @import { AlertBox } from "../components/ui" -->

<main class="space-y-6">
  <h1>{{ pageTitle }}</h1>

  <x-alert-box title="Welcome back" class="rounded border p-4">
    <p>Rendered through an imported Python component.</p>
  </x-alert-box>

  <ul>
    {% for label in stats %}
      <li>{{ label }}</li>
    {% endfor %}
  </ul>
</main>
```

`casp.layout` also exposes `render_layout()`, `render()`, `load_template()`, `compile_template()`, and layout discovery helpers for nested layout flows.

## Nested Layouts

Layouts are authored as HTML and use a real `<slot />` element as the child-route outlet.

```html
<!-- app/layout.html -->
<html>
  <head>
    <title>{{ metadata.title }}</title>
  </head>
  <body>
    <slot />
  </body>
</html>
```

During nested layout rendering, Caspian parses the layout HTML and replaces real `<slot>` elements with the current child page or nested layout. Escaped documentation text such as `&lt;slot /&gt;` is not treated as a layout outlet.

If a layout needs shared props or metadata, add a sibling `layout.py`:

```py
from casp.layout import Metadata

metadata = Metadata(title="Dashboard")


def layout():
    return {
        "shell_class": "dashboard-shell",
    }
```

Those props are available in `layout.html` as `{{ layout.shell_class }}`. The installed layout runtime supports sync or async `layout()` results, but layout work should stay focused on shared subtree props or metadata.

## Template Syntax

Caspian templates use native Jinja:

- `{{ value }}` for interpolation
- `{% if condition %}...{% endif %}` for conditionals
- `{% for item in items %}...{% endfor %}` for loops
- filters such as `{{ children | safe }}`

There is no additional Caspian template language on top of Jinja.

## Template Constraints

Some compiler rules are important when writing templates:

- every page, layout, and component must render exactly one top-level HTML element
- unknown `<x-...>` tags raise an error unless the component has been imported
- async components are supported by the component pipeline
- authored PulsePoint scripts should be plain `<script>` tags inside the single root; the runtime can rewrite them for browser execution

These constraints exist because Caspian injects `pp-component` metadata into the rendered root element.

## RPC Helpers

The package also includes the server-side RPC decorator and related request/serialization utilities.

```py
from casp.rpc import rpc


@rpc(require_auth=True, limits="30/minute")
async def save_profile(name: str):
    return {"ok": True, "name": name}
```

`casp.rpc` includes:

- RPC registration and route-scoped function lookup
- auth-aware decorators
- rate limiting
- serialization for common Python objects
- FastAPI-oriented request and response helpers

### Request gates

Every RPC call is checked before the decorated function runs. A call that fails a gate never reaches application code:

| Gate | Failure |
| --- | --- |
| `Origin` against the allow-list (skipped when the header is absent) | 403 `Invalid origin` |
| `Content-Type` is `application/json` or `multipart/form-data` when a body is present | 415 `Invalid content type` |
| `X-CSRF-Token` compared against the session token | 403 `Missing CSRF token` / `Invalid CSRF token` |
| `require_auth=True` | 401 `Authentication required` |
| `roles=[...]` | 403 `Permission denied` |
| per-route rate limit | 429 |

The allow-list is the request's own base URL, plus `APP_BASE_URL`, plus `CORS_ALLOWED_ORIGINS`, plus the forwarded origin when `TRUST_FORWARDED_HEADERS` is on. Outside production, `http://localhost:<port>` and `http://127.0.0.1:<port>` origins are also accepted.

Payload keys are filtered against the function signature, so a parameter is settable by the client only when it is declared. Declaring `**kwargs` opts the function into the entire payload.

### Environment

`casp.rpc` and `casp.runtime_security` read these on use, so loading `.env` at any point before the first request is sufficient — import order does not affect them.

| Variable | Default | Effect |
| --- | --- | --- |
| `APP_ENV` | unset | Resolved fail-closed. Only `dev`, `development`, `local`, `staging`, `test`, or `testing` select development behavior; anything else, including unset or misspelled, is treated as production. |
| `AUTH_SECRET` | none | Session secret. Required in production; a missing or placeholder value raises. |
| `CORS_ALLOWED_ORIGINS` | unset | Comma-separated additions to the origin allow-list. |
| `APP_BASE_URL` | unset | Public origin, for deployments behind a proxy. |
| `TRUST_FORWARDED_HEADERS` | off | Honour `X-Forwarded-*` / `Forwarded` for the origin check and rate-limit bucket. Enable only behind a proxy you control. |
| `CONTENT_SECURITY_POLICY` | built-in policy | Replaces the default CSP wholesale. |
| `RATE_LIMIT_DEFAULT` | `200/minute` | Default limit passed to slowapi. |
| `RATE_LIMIT_RPC` | `60/minute` | Applied to `@rpc()` without explicit `limits`. |
| `RATE_LIMIT_AUTH` | `60/minute` | Applied to `@rpc(require_auth=True)` without explicit `limits`. |
| `RATE_LIMIT_MAX_BUCKETS` | `10000` | Cap on tracked rate-limit buckets. |
| `RATE_LIMIT_CLEANUP_INTERVAL` | `60` | Seconds between bucket sweeps. |
| `CASPIAN_ROOT` | auto-detected | Explicit project root override for config and file-index lookup. |

`APP_ENV` is the widest switch in the package: it gates the session secret check, the HSTS header, how much error detail reaches the client, and the localhost origin bypass above. Applications typically extend it to their own cookie and transport settings. Leaving it unset in development is what produces a working-looking app whose every RPC call returns 403 `Invalid origin`.

OAuth providers read `GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET`, `GOOGLE_REDIRECT_URI`, `GITHUB_CLIENT_ID`, and `GITHUB_CLIENT_SECRET`.

## Auth Policy

`casp.auth` provides the framework runtime for sessions, route checks, decorators, CSRF helpers, and OAuth providers. Application auth policy should live in an app-owned `src/lib/auth/auth_config.py` file, where the app builds `AuthSettings` and applies them during startup with `configure_auth(...)`.

Keep route privacy, redirects, and RBAC policy in that app config file instead of changing `casp.auth`.

## Main Modules

| Module | Purpose |
| --- | --- |
| `casp.layout` | Load, compile, and render pages and nested layouts with native Jinja and parser-based `<slot />` replacement |
| `casp.html_native` | BeautifulSoup-backed fragment parsing helpers used by layout and component transforms |
| `casp.component_decorator` | `@component`, `html()` for single-file components, `render_html()` for sibling templates, and component loading |
| `casp.components_compiler` | Parse `@import` directives and transform `<x-...>` component tags |
| `casp.html_attrs` | Attribute rendering, prop alias normalization, and Tailwind class merge helpers |
| `casp.scripts_type` | Rewrite authored PulsePoint scripts for browser runtime execution |
| `casp.rpc` | RPC decorator, registration, serialization, request gates (origin, content type, CSRF, auth, roles, rate limit), and request handling helpers |
| `casp.streaming` | Server-Sent Events helpers including `SSE` |
| `casp.auth` | Auth settings, session helpers, decorators, OAuth providers, and route checks |
| `casp.runtime_security` | `is_production_environment()` fail-closed `APP_ENV` resolution, safe public-file serving, security headers, and production secret checks |
| `casp.cache_handler` | Page cache helpers |
| `casp.state_manager` | Request-scoped and session-backed state helpers |
| `casp.validate` | Validation and sanitization helpers for strings, IDs, files, dates, and numbers |
| `casp.caspian_config` | Config loading and file index helpers |
| `casp.loading` | Route and `loading.html` file discovery |
| `casp.string_helpers` | Case conversion between component names and `<x-...>` tags |

## Dependencies

This package declares no `install_requires`, so `pip install caspian-utils` installs `casp` alone and pins nothing. The following packages are expected to be present in the host application's environment, where a Caspian project installs and pins them:

| Package | Required by |
| --- | --- |
| `fastapi` | `casp.rpc`, `casp.auth`, `casp.runtime_security` |
| `jinja2` and `markupsafe` | `casp.layout`, `casp.component_decorator`, `casp.html_attrs` |
| `beautifulsoup4` | `casp.html_native`, `casp.components_compiler` |
| `slowapi` | `casp.rpc` |
| `python-multipart` | multipart RPC payloads and file uploads, through FastAPI |
| `httpx2` | `casp.auth` OAuth provider calls |
| `cuid2` and `python-ulid` | `casp.validate` |

Importing `casp` in an environment missing any of these raises `ImportError` at import time.

## Repository

[TheSteelNinjaCode/caspian_utils](https://github.com/TheSteelNinjaCode/caspian_utils)

## License

MIT

## Author

Jefferson Abraham
