# Example: a complete package
URL: https://lsd.tools/docs/ex-full-package
Updated: 2026-07-18

> A real, shippable package is one tool.py (a load + shape + deliver, a @tool.command, and a @tool.view) plus an lsd_package.json manifest. This walkthrough shows all of it together and how the pieces mount as nodes, a CLI subcommand, and a sidebar panel.

Everything the other examples show, assembled into one shippable package: three pipeline steps, a CLI
command, and a dockable panel — plus the [`lsd_package.json`](/docs/api-manifest) manifest that makes
it installable. This is the shape to copy when you start a real package.

## The code — `tool.py`

```python
# tool.py
from lsdtools import Tool, Context, Source, Table, ui

tool = Tool("assays", label="Assays")


# ── pipeline: load → shape → deliver ─────────────────────────────────────────
@tool.load(source_kinds=("file", "sql"))
def load_assays(src: Source) -> Table:
    """Load assay rows (needs columns: hole, grade, tonnes)."""
    return src.read()


@tool.shape
def add_metal(t: Table, recovery: float = 0.9) -> Table:
    """Add a `metal` column = grade × tonnes × recovery."""
    return t.with_column("metal", t["grade"] * t["tonnes"] * recovery)


@tool.deliver
def write_summary(t: Table, path: str = "by_hole.parquet") -> None:
    """Total metal and tonnes per hole → a Parquet file."""
    t.group_by("hole").agg(metal="sum", tonnes="sum").write_parquet(path)


# ── a CLI command: lsd assays stats assays.csv ──────────────────────────────
@tool.command("stats", help="print recovered metal per hole for an assay CSV")
def stats(ctx: Context, src: str, recovery: float = 0.9) -> int:
    """Quick per-hole totals without opening the desktop."""
    from lsdtools import Engine
    metal = add_metal(load_assays(Source.file(src)), recovery=recovery)
    Engine().run(metal).output.group_by("hole").agg(metal="sum", tonnes="sum").print()
    return 0


# ── a dockable panel: a label + a live table of the add_metal output ─────────
@tool.view("assays.summary", title="Assay summary", location="sidebar", icon="lsd-chart-symbolic")
def summary_panel(ctx: Context):
    """A sidebar panel showing the current add_metal result."""
    return ui.rows(
        ui.label("Metal by hole"),
        ui.table(**ui.bind.step("assays.add_metal")),
    )
```

## The manifest — `lsd_package.json`

```json
{
    "_schema_version": 1,
    "name": "lsd-assays",
    "display_name": "Assays",
    "version": "0.1.0",
    "description": "Load assay CSVs, compute recovered metal, and summarise it per hole — with a CLI and a view.",
    "author": "You",
    "publisher": "you",
    "keywords": ["mining", "assays", "example"],
    "min_lsd": "0.1.0",
    "dependencies": {},
    "tool": "tool"
}
```

Folder layout — the manifest sits next to the code:

```
lsd-assays/
 lsd_package.json
 tool.py
```

## What each piece does

- **`tool = Tool("assays")`** — one `Tool` per package, at module scope. Its name namespaces every
 node identity (`assays.load_assays`, `assays.add_metal`, …).
- **load → shape → deliver** — the pipeline. `load_assays` produces a [`Table`](/docs/api-table) from
 the picked source; `add_metal` transforms it (`t["grade"] * t["tonnes"] * recovery` is one
 whole-column expression); `write_summary` aggregates per hole and writes Parquet.
- **`@tool.command("stats")`** — a `lsd assays stats <src>` subcommand built from the signature:
 `src` (no default) is a positional, `recovery` (default) is `--recovery`. See
 [a CLI command](/docs/ex-cli-command).
- **`@tool.view("assays.summary", location="sidebar")`** — a `(ctx) -> View` sidebar panel.
 `ui.bind.step(...)` binds the table view to the `add_metal` node's output, so it refreshes as the
 pipeline runs.
- **The manifest** — `_schema_version: 1` selects the flat v1 format. `name` is the pip/distribution
 name; `tool: "tool"` points the loader at the module-level `tool` object (use `"pkg.mod:tool"` when
 it lives deeper). `min_lsd` and `dependencies` gate loading. See the
 [full field table](/docs/api-manifest).

## Run it

Pipeline and deliver, from Python:

```python
from lsdtools import Engine, Source, Table

Table({
        "hole": ["DH1", "DH1", "DH2"],
        "grade": [1.2, 0.8, 2.0],
        "tonnes": [100.0, 150.0, 80.0],
}).write_csv("assays.csv")

metal = add_metal(load_assays(Source.file("assays.csv")), recovery=0.9)
Engine().run(write_summary(metal, path="by_hole.parquet"))

Table.read_parquet("by_hole.parquet").print()
# → hole metal tonnes
# DH1 216 250
# DH2 144 80
```

The same numbers straight from the CLI:

```bash
lsd assays stats assays.csv
# → hole metal tonnes
# DH1 216 250
# DH2 144 80
```

In the desktop, adding the package to a project surfaces the three nodes in the palette and the
**Assay summary** panel in the sidebar.

## Try changing it

- Add a `dependencies` entry (e.g. `{"lsd-spatial": ">=0.1.0"}`) and the loader resolves it first.
- Split the code into `steps.py` + `views.py` and set `"code": "assays"` to point at a package dir.
- Publish it — scaffold with `lsd package new assays`, then see [Publish to the store](/docs/pkg-publish).

## Related

**Learn** — [Package anatomy](/docs/pkg-anatomy) · [Develop a package](/docs/pkg-develop) · [The Package Manager](/docs/pkg-manager)

**API** — [lsd_package.json](/docs/api-manifest) · [Tool](/docs/api-tool) · [CLI commands](/docs/api-cli)

**Examples** — [A CLI command](/docs/ex-cli-command) · [A dockable panel](/docs/ex-panel) · [A node variant](/docs/ex-variant)
