Metadata-Version: 2.4
Name: useful-views
Version: 0.1.0
Summary: A framework and data project structure in which every derived table records the recipe that made it.
Project-URL: Homepage, https://github.com/jameshorine/useful-views
Project-URL: Documentation, https://jameshorine.github.io/useful-views/
Project-URL: Issues, https://github.com/jameshorine/useful-views/issues
Author: James Horine
License-Expression: MIT
License-File: LICENSE
Keywords: data,data-package,parquet,pipelines,reproducibility,research
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Science/Research
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Scientific/Engineering
Requires-Python: >=3.11
Requires-Dist: pyarrow>=16
Description-Content-Type: text/markdown

<div align="center">

<picture>
  <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/jameshorine/useful-views/main/docs/useful-views-logo-dark.svg">
  <img src="https://raw.githubusercontent.com/jameshorine/useful-views/main/docs/useful-views-logo.svg" alt="useful-views" width="480">
</picture>

*Documented derived tables for data projects.*

[![PyPI](https://img.shields.io/pypi/v/useful-views)](https://pypi.org/project/useful-views/)
[![Python](https://img.shields.io/badge/python-3.11%E2%80%933.14-blue)](https://pypi.org/project/useful-views/)
[![License: MIT](https://img.shields.io/badge/license-MIT-green)](https://github.com/jameshorine/useful-views/blob/main/LICENSE)
[![Docs](https://img.shields.io/badge/docs-jameshorine.github.io-informational)](https://jameshorine.github.io/useful-views/)

</div>

## Overview

`useful-views` is a Python package that gives a data project a fixed
structure, so that every derived table (a "useful view") records the recipe
that made it. Every recipe records the steps that make that recipe.

Full documentation, including a command reference and a guide for AI tools:
https://jameshorine.github.io/useful-views/

## Installation

```sh
uv init --bare             # once, if the folder has no pyproject.toml yet
uv add useful-views polars # or, in an activated virtual environment: pip install useful-views polars
```

Install it into the same Python environment as the libraries your steps use
(polars, pandas, ...): steps run with the Python that runs useful-views. So
do not run it with `uvx` or `pipx`. The
[installation guide](https://jameshorine.github.io/useful-views/user-guide/installation.html)
covers pip, conda, Windows and notebooks.

## Usage

With the environment active (or with `uv run` in front of each command):

```sh
useful-views example pwt      # a ready-made example; for your own data: useful-views init my-project
cd pwt-example
useful-views sync             # run every recipe, upstream first, and record what each made
useful-views check            # report anything missing, refused or out of date
useful-views cookbook         # write cookbook.json, the recipes the project uses
```

The [quickstart](https://jameshorine.github.io/useful-views/user-guide/quickstart.html)
walks through it, in Python and on the command line.

### The resulting data project structure

Using `useful-views` in a data project results in this layout:

```
my-data-project/
├── README.md                     # written by humans; documents the data project
├── AGENTS.md                     # written once by init: instructions for AI coding assistants
├── datapackage.json              # generated: resources and recipes (never hand-edited)
├── cookbook.json                 # generated by useful-views cookbook: the recipes used, in run order
├── .gitignore                    # generated by init: ignores raw/, work/, and other data files
│
├── raw/                          # raw inputs: git-ignored, never written by a recipe
│   ├── .gitkeep                  # keeps the folder in git
│   └── my_data.extension         # any format
│
├── recipes/                      # written by humans, AI tools, or both
│   ├── recipe_01/                # one folder = one recipe, at least 1 step, maybe more!
│   │   ├── recipe.toml           # description, inputs, outputs
│   │   ├── 01_first_step.py      # one file = at least one step; top comment is its description
│   │   ├── 02_second_step.py
│   │   └── 03_third_step.py
│   │
│   └── recipe_02/                # a second recipe, taking a view as its input
│       ├── recipe.toml           # inputs = ["useful-views/recipe-01.parquet"]
│       └── 01_only_step.py       # smallest valid recipe: a file containing a single step
│
├── work/                         # intermediates, never recorded, git-ignored
│   ├── recipe_01/                # one scratch folder per recipe, emptied before each run
│   │   ├── step_1.extension      # any format
│   │   └── step_2.extension
│   └── recipe_02/
│
└── useful-views/                 # declared outputs only, flat
    ├── README.md                 # generated: catalog of the views and how each was made
    ├── recipe-01.parquet         # produced by recipes/recipe_01
    └── recipe-02.parquet         # produced by recipes/recipe_02
```

Raw inputs and work files can be in any format. Views are parquet files:
parquet keeps each column's type, so the record in `datapackage.json` and
the catalog can say exactly what every view contains. Other view formats
may be allowed in the future.


## Vocabulary

Using `useful-views` is meant to be easy! However, we have a few (hopefully) useful vocabulary terms to help our collective practice and understanding. 


The building blocks are raw inputs, steps and recipes.


| Term | What it is | Where it lives |
|---|---|---|
| **Raw input** | A file you start from. Nothing in `raw/` is produced by a recipe. | `raw/` |
| **Step** | One file that does at least one non-trivial action on the data. | A file in a recipe folder |
| **Recipe** | Like cooking: steps run in order. From the inputs it declares, it produces the useful views it declares. | A folder in `recipes/` |

For example, in polars, given:

```python
df = pl.DataFrame(
    {
        "foo": [-20.5, float("nan"), 80.0],
        "bar": [float("nan"), 110.0, 25.5],
        "ham": ["xxx", "yyy", None],
    }
)
```

calling `polars.DataFrame.drop_nans()` returns:

```
>>> df.drop_nans()
shape: (1, 3)
┌──────┬──────┬──────┐
│ foo  ┆ bar  ┆ ham  │
│ ---  ┆ ---  ┆ ---  │
│ f64  ┆ f64  ┆ str  │
╞══════╪══════╪══════╡
│ 80.0 ┆ 25.5 ┆ null │
└──────┴──────┴──────┘
```

This would be considered a step. So, again, a step is at least one
non-trivial transformation action on the data. That is the minimum. There is
no maximum yet: a step may do many transformations, and the author decides
where one step ends and the next begins.

**Work**: intermediate files, in `work/<recipe>/`, read only by later steps
of the same recipe.

**Useful view**: a declared output of a recipe, in `useful-views/`. Every
view is produced by exactly one recipe. A view can be an input to another
recipe. Anything read from outside the recipe that made it must be a view,
not a work file.

## Writing a recipe

A recipe folder holds one `recipe.toml` and at least one step file.

`recipes/pwt_panel/recipe.toml`:

```toml
description = "Country-year panel of core Penn World Table variables for Brazil, Germany, Spain and the United States."
inputs  = ["raw/pwt110_bra_deu_esp_usa.csv"]
outputs = ["useful-views/pwt-panel.parquet"]
```

`recipes/pwt_panel/01_reshape_long.py`:

```python
# Reshape the wide export (one column per year) into one row per country, variable and year.
import polars as pl

wide = pl.read_csv("raw/pwt110_bra_deu_esp_usa.csv")
long = wide.unpivot(
    index=["ISO code", "Country", "Variable code", "Variable name"],
    variable_name="year",
    value_name="value",
).with_columns(pl.col("year").cast(pl.Int32))
long.write_parquet("work/pwt_panel/long.parquet")
```

The conventions:

- Inputs and outputs are written as paths from the project root. The path in
  `recipe.toml` is the same path the step uses in its code.
- An input is a file under `raw/`, or a view under `useful-views/` made by
  another recipe.
- An output is always a file directly inside `useful-views/`.
- Step files are named `NN_name.py` and run in numeric order.
- The comment at the top of a step file is its description. It is required.
- A step is an ordinary script. It runs from the project root and does not
  import `useful_views`. Use whichever data library you like.
- Steps pass data to each other only through files in `work/<recipe>/`.

In detail:

- **Recipe folder name**: lowercase letters, digits and underscores, such as
  `pwt_panel`. Create one with `useful-views new-recipe <name>`; its template
  says TODO and is refused until you fill it in.
- **`recipe.toml`** has exactly three keys. `description` is one sentence,
  required. `inputs` and `outputs` are lists of at least one path each. Other
  keys are ignored with a warning.
- **Inputs**: a file anywhere under `raw/`, or a view directly inside
  `useful-views/` that another recipe makes and has already made
  successfully.
- **Outputs**: one or more `.parquet` files directly inside `useful-views/`.
  Each view is made by exactly one recipe.
- **Paths**: relative to the project root, with forward slashes, matching the
  letter case on disk. Write `raw/data.csv`, not `./raw/data.csv`,
  `raw\data.csv` or an absolute path. The error message gives the corrected
  string where it can; inside double quotes a backslash is a TOML escape, so
  then it only tells you to use forward slashes.
- **Step files**: `NN_name.py`, two digits, an underscore and a name. They
  run in numeric order; `00_` and gaps in the numbering are fine. A file
  named almost like a step, such as `1_clean.py`, `01-clean.py`,
  `01 clean.py` or `01_clean.py.txt`, is not run, and `run` warns about it.
  R, notebook, Quarto, Julia and Stata files named like a step
  (`02_clean.R`, `02_explore.ipynb`, ...) are refused: steps must be Python
  files. Any other file in the folder is ignored, unless it starts with
  `NN_` and ends in `.py` (then it is a step: keep old copies elsewhere).
- **Step description**: the block of `#` lines at the very top of the file,
  its lines joined with spaces into one line. Blank lines, a shebang, a
  coding line, notebook cell markers (`# %%`, `# In[1]:`, with any title on
  the same line), a Jupytext `# ---` header, an inline `# /// script` block
  and decoration lines like `# -----` before it are skipped; a docstring is
  not, so put the comment first. It must not start with TODO. Step files are
  UTF-8.
- **Steps** run one at a time as separate processes, with the project root as
  the working directory, using the same Python interpreter as useful-views,
  and with `work/<recipe>/` already created and empty. A step gets no
  keyboard input, so `input()` and `breakpoint()` fail inside `run`; to debug
  a step, run it yourself from the project root with the same Python, as in
  `python -X utf8 recipes/pwt_panel/01_reshape_long.py`, after the recipe has
  run once (so `work/<recipe>/` exists), then `run` the recipe again to
  record it. A step reads its recipe's
  inputs and earlier steps' files in `work/<recipe>/`, and writes the
  recipe's outputs. It never writes to `raw/`, and never reads another
  recipe's work files.

## Example data

Example projects live in a separate repository,
[useful-views-data](https://github.com/jameshorine/useful-views-data), the
way seaborn's example datasets live in seaborn-data. `useful-views example
<name> [path]` (Python: `example(name, path)`) downloads one into a new
folder, ready to run with `useful-views sync`. Each release of useful-views
pins a version of that repository and the checksum of every file, so an
example never changes under you and a damaged download is never used.

The `pwt` example uses an extract of the Penn World Table, version 11.0, for
Brazil, Germany, Spain and the United States. The Penn World Table is
licensed under a Creative Commons Attribution 4.0 International License; the
example's `README.md` gives the exact source and the citation.

## Commands

| Python | Command line | What it does |
|---|---|---|
| `init(path=".")` | `useful-views init [path]` | Create the structure above. Safe to run twice; never overwrites a file. |
| `example(name, path=None)` | `useful-views example <name> [path]` | Download a ready-to-run example project, such as `pwt`. See [Example data](#example-data). |
| `new_recipe(name)` | `useful-views new-recipe <name>` | Create a recipe folder with a template `recipe.toml` and one stub step. |
| `run(recipe)` | `useful-views run <recipe>` | Run one recipe's steps in order and record what it produced. |
| `sync(all=False)` | `useful-views sync [--all]` | Make the views match the recipe folders, like `uv sync`: run what is missing or changed, upstream first, and remove what is no longer declared. |
| `check()` | `useful-views check` | Report everything run and sync would refuse, every recipe sync would run, and what is missing or out of place. Changes nothing. |
| `cookbook(check=False)` | `useful-views cookbook [--check]` | Write `cookbook.json`, the ledger of the recipes used, in run order. `--check` verifies it, for CI. |
| `RULES` | `useful-views rules` | List the numbered rules a recipe must follow. |
| `root()` | none | Return the project root, for building paths in a notebook. |

`run` and `sync` differ the way `uv run` and `uv sync` do: `run` does the one
recipe you name, now; `sync` brings the whole project in line with its
recipe folders.

### What `run` does

Before changing anything on disk, `run` checks the recipe and how it fits
the project (see [Rules](#rules)). Then it:

1. deletes the recipe's declared views and empties `work/<recipe>/`, so a
   leftover file can never pass for a fresh result;
2. runs each step in order. Their output appears as they run, in a terminal
   or a notebook cell. The first step that fails stops the run;
3. checks that every declared view was written as a non-empty parquet file;
4. records the run: it rewrites `datapackage.json` and
   `useful-views/README.md`. Only this recipe's entries change; every other
   recipe's record is kept exactly as it was.

If a step fails or a view is missing, nothing is recorded: the recipe's
views are removed, and its record from the last successful run, if any, is
kept. `check` then reports the views as missing, or the recipe as never run.
The files in `work/<recipe>/` stay as the steps left them, so you can see how
far the run got. Running the same recipe twice leaves `datapackage.json` and
the catalog byte-for-byte unchanged.

`run` never runs other recipes. If an input is another recipe's view, run
that recipe first, or use `sync`. If other recipes read the views `run` just
made again, it warns and names them: their views are now out of date.

### What `sync` does

`sync` makes the project match its recipe folders, the way `uv sync` makes
an environment match its lock file. Records only ever come from real runs.

1. It checks every recipe against the [rules](#rules), judging each against
   the records as they will be when its turn comes. If any recipe would be
   refused, it lists every problem at once and changes nothing. A folder
   with no `recipe.toml` is skipped with a warning.
2. It removes the records of recipes whose folder was deleted, and any view a
   recipe no longer lists in its outputs, along with those view files. It
   warns about recipes that read a removed view.
3. It runs, upstream first, every recipe that has never run, whose
   `recipe.toml` or steps no longer match its record, or whose views are
   missing, and everything downstream of those. `--all` (`all=True`) runs
   every recipe. It stops at the first failure or interruption; recipes
   that ran before it stay recorded, and the message says what to run next.

On a fresh clone, place the raw files and run `useful-views sync` to make
every view. `sync` does not notice a change inside a step's code, a raw file
replaced under the same name, or a `git pull` or branch switch (the views
are not in git). After any of those, use `sync --all`, or `run` the changed
recipe and then each recipe downstream of it.

Only one command that changes a project runs at a time: a second `run`,
`sync` or `cookbook` started meanwhile is refused instead of overwriting the
first one's records.

### Steps are code

`run` and `sync` execute every step in `recipes/` as you, with your
permissions, your environment variables (including any passwords or tokens
in them) and your network access. There is no sandbox. Read the recipes of a
project you did not write, or changes someone proposes, before running them.
`check`, `rules` and `cookbook` never run a step.

### What `check` reports

| Kind | Meaning |
|---|---|
| `unreadable-recipe` | A recipe folder whose `recipe.toml` is missing or cannot be read. |
| `refused` | A recipe breaks a rule, so `run` and `sync` would refuse it. The finding names the rule. |
| `missing-input` | An input a `recipe.toml` declares is not on disk. |
| `never-run` | A recipe that has never run successfully. |
| `out-of-date` | A recipe `sync` would run again: its `recipe.toml` or steps changed, its outputs changed, or a view it reads will be made again. |
| `missing-recipe-folder` | `datapackage.json` records a recipe whose folder is gone. `sync` removes the record. |
| `missing-view` | A recorded view whose file is missing. |
| `stale-cookbook` | `cookbook.json` no longer matches the recorded recipes. Update it with `useful-views cookbook`. |
| `stale-catalog` | `useful-views/README.md` does not match `datapackage.json` (edited by hand, or left from a merge). `sync` writes it again. |
| `stray-file` | A file or folder in `useful-views/`, other than `README.md`, that no recipe makes. |

Each finding says what to do about it, and `check` uses the same plan as
`sync`, so a clean `check` means `sync` has nothing to refuse. It does not
notice changes inside a step's code or to a file's contents.

### Rules

`run` refuses a recipe, before changing anything on disk, when:

1. `recipe.toml` is missing or unreadable, lacks `description`, `inputs` or
   `outputs`, has an empty list, lists a path twice, or its description
   starts with TODO.
2. The recipe has no step file; a step has no top comment, or one that starts
   with TODO; or two steps share a number.
3. A file in the recipe folder is named like a step but is an R, notebook,
   Quarto, Julia or Stata file (`.R`, `.Rmd`, `.ipynb`, `.qmd`, `.jl`,
   `.do`): steps must be `.py` files.
4. An input is not a file under `raw/` or directly inside `useful-views/`, is
   written in the wrong form (or as a wildcard), has a name that cannot exist
   on Windows, does not exist with exactly that letter case, or is an empty
   or unreadable raw file.
5. An input inside `useful-views/` is not made by another recipe that has
   already run successfully.
6. The recipe would create a cycle: it reads its own view, directly or
   through a chain of other recipes.
7. An output is not a `.parquet` file directly inside `useful-views/`, has a
   name that cannot exist on Windows, is `useful-views/README.md`, or is
   already made by another recipe.
8. Recording it would give two files the same resource name (a view's file
   name, or a raw file's path below `raw/`, without the extension,
   lowercased, with `/` and any other ASCII character than a-z, 0-9, `.`,
   `_` or `-` turned into `-`; accented letters lose their accents, and
   letters of other scripts become their Unicode code point), such as
   `raw/data.csv` and
   `useful-views/data.parquet`, or `raw/My Data.csv` and
   `useful-views/my-data.parquet`.

`run` also refuses a recipe name that does not match a folder in `recipes/`,
or a folder name that is not allowed (lowercase letters, digits and
underscores, and not a name Windows reserves, such as `con` or `aux`). Every refusal names the rule it broke and
says what to fix, and in which file. `useful-views rules` lists the rules;
from Python they are `useful_views.RULES`. A refusal raises
`RecipeRefused`, whose `rule` is the number above (or `None` for the name
refusals), `rule_title` its title, and `path` the file to fix.

### Using it from Python

```python
import useful_views as uvs

uvs.run("pwt_panel")                 # works from any folder inside the project
uvs.check(root="~/data/my-project")  # or name the project folder explicitly
gdp = uvs.root() / "useful-views" / "gdp-per-capita.parquet"
```

- `new_recipe`, `run`, `sync`, `check`, `cookbook` and `root` find the
  project by walking up from the current folder to `datapackage.json`, so
  they work from a notebook in `notebooks/`. All of them but `root` also take
  `root=`: the project folder itself, the one holding `datapackage.json`.
  `init` and `example` take the folder to create.
- `init` and `example` return the project folder, `new_recipe` the recipe
  folder, `run` a `RunResult` (`recipe`, `outputs`, `warnings`), `sync` a
  `SyncResult` (`removed`, `dropped`, `deleted`, `ran`, `reasons`,
  `warnings`), `cookbook` a `CookbookResult` (`path`, `written`,
  `up_to_date`, `recipes`, `findings`), and `check` a list of findings
  (`kind`, `path`, `message`, `rule`, `recipe`, `reason`).
- Failures raise subclasses of `UsefulViewsError`: `RecipeRefused` (`rule`,
  `rule_title`, `path`, `refusals`), `StepFailed` (`step`, `returncode`,
  `stderr`), `OutputError` (`paths`, `problems`), `CookbookError`
  (`findings`), `ExampleError` (`kind`), `ProjectNotFound` and
  `ProjectError`. Each also has `recipe`, `sync_result` and `to_dict()`.
  Warnings are `UsefulViewsWarning`.
- On the command line, the exit code is 0 for success, 1 when `check` finds
  problems or `cookbook --check` finds the cookbook out of date, 2 for a
  usage error, 3 when an operation is refused or fails, 130 when interrupted
  and 143 when terminated. `useful-views --version` prints the version.
  `python -m useful_views ...` works the same as `useful-views ...`, for
  example when the `useful-views` command is not found.

### For scripts and AI tools

- `--json` on any command, `--help` and `--version` included, prints exactly
  one JSON document on standard output, also on errors, with any warnings in
  it; everything else goes to standard error.
- `-C <path>` runs a command in another folder, like `git -C`.
- Every error carries its facts as attributes and `to_dict()`: the recipe,
  the rule, the file to fix, and the failed step and its error output.
- `init` writes an `AGENTS.md` into each new project, with the rules an AI
  coding assistant must follow there.
- In CI, `useful-views cookbook --check` needs no data: it checks that
  `cookbook.json` and `datapackage.json` match the recipe folders.

See [Working with AI tools](https://jameshorine.github.io/useful-views/user-guide/guides/ai-tools.html).

## Generated files

**`datapackage.json`** is a [Data Package](https://datapackage.org/) that
records what each recipe made from what. `frictionless validate
datapackage.json` passes on it whenever every recorded file is present.

- `resources` lists every raw file a recorded recipe read, then every view,
  each with its columns and their types. A view also has `producedBy`: the
  recipe that made it and that recipe's inputs.
- `recipes` lists each recorded recipe with its description, inputs, outputs
  and steps, as they were at its last successful run.
- Column types come from the files themselves: a parquet file's own types,
  and for a raw CSV, types inferred from the whole file. In a raw CSV only an
  empty cell counts as missing, so a column that marks missing values with
  `NA` or `.` is recorded as `string`. Other raw formats
  (`.dta`, `.xlsx`, ...), and CSVs that are not plain comma-separated UTF-8,
  are recorded without columns.
- Never edit it by hand. If it is damaged, restore it from git.

**`useful-views/README.md`** is a catalog generated from `datapackage.json`:
for each view, the recipe's description, its inputs, its numbered steps, and
its columns.

**`cookbook.json`** is the ledger of the recipes the project uses, written by
`useful-views cookbook` the way `uv lock` writes `uv.lock`: every recorded
recipe in run order, upstream first, with its description, inputs, outputs
and steps, and the raw inputs the recipes read. To write it, the project must
be in sync. `useful-views cookbook --check` exits 1 when it is missing or out
of date, or when a recipe changed without a sync. It needs no data, so CI can
require an up-to-date cookbook. See
[Cookbook](https://jameshorine.github.io/useful-views/user-guide/concepts/cookbook.html)
for what it holds and a ready-made CI job.

**`.gitignore`** is written once by `init`, if there is none:

```
raw/*
!raw/.gitkeep
/work/
useful-views/*.parquet
__pycache__/
```

So raw data, work files and views stay out of git, while `recipes/`,
`datapackage.json`, `cookbook.json`, `AGENTS.md` and the catalog are
committed, with the files that list your steps' libraries (`pyproject.toml`
and `uv.lock`, or `requirements.txt`). A teammate installs those, places the
raw files and runs `useful-views sync` to make the views again.

## Who writes what

Humans write the project `README.md` and everything in `recipes/`; `init`
and `new-recipe` only start them from templates and never touch them again.
The package writes `datapackage.json`, `useful-views/README.md` and, when you
run `useful-views cookbook`, `cookbook.json`; never edit those by hand.
`init` writes `.gitignore` and `AGENTS.md` once, if they are not there; after
that they are yours. So no file has two authors at the same time.

## Planned, TODO, Not yet

- Hashing. Version 1 of `cookbook.json` records every recipe and its steps.
  Later versions will add the SHA-256 of every raw input, recipe file, step
  and view, and the environment the steps ran in, locking them the way
  `uv.lock` locks a Python environment. Then useful-views can report a view
  as stale, and `sync` can notice a change inside a step's code, a replaced
  raw file, or views left from before a `git pull`.
- Whether a step should have an upper limit on how much it does.
- Recording where a raw file came from and how to fetch it again.
- More view formats alongside parquet, such as CSV and Arrow.
- Raw inputs that are folders or sets of files, such as partitioned parquet
  datasets and shapefiles.
- Subfolders inside `useful-views/`, and notebooks as steps.

## License

useful-views is released under the MIT License; see `LICENSE`. The example
data is not part of the package and not under its license: it lives in the
[useful-views-data](https://github.com/jameshorine/useful-views-data)
repository (with a copy in `tests/data/` for the tests), and is an extract of
the Penn World Table, licensed CC BY 4.0. The example's `README.md` gives the
citation.

## Citing useful-views

If you use useful-views in your research, please cite it. GitHub's
**Cite this repository** button (from `CITATION.cff`) gives APA and BibTeX,
or use:

> Horine, J. (2026). *useful-views: Documented derived tables for
> data projects* (Version 0.1.0) [Computer software].
> https://github.com/jameshorine/useful-views

```bibtex
@software{horine_useful_views_2026,
  author  = {Horine, James},
  title   = {useful-views: Documented derived tables for data projects},
  year    = {2026},
  version = {0.1.0},
  url     = {https://github.com/jameshorine/useful-views},
  license = {MIT}
}
```

Please also cite the sources of any data you use with it; the `pwt` example
uses the Penn World Table, whose citation is in the example's `README.md`.
