Metadata-Version: 2.5
Name: model2data-studio
Version: 0.1.0
Summary: Generate a model2data studio project's data from the command line, so CI can rebuild fixtures instead of committing them.
Project-URL: Homepage, https://www.jbanalytica.com/model2data/
Project-URL: Studio, https://studio.jbanalytica.com
Project-URL: Pricing, https://www.jbanalytica.com/model2data/pricing/
Author: JB Analytica
License-Expression: MIT
License-File: LICENSE
Keywords: dbml,dbt,fixtures,mcp,synthetic data,test data
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Database
Classifier: Topic :: Software Development :: Testing
Requires-Python: >=3.10
Requires-Dist: httpx>=0.27.0
Requires-Dist: typer>=0.12.0
Provides-Extra: dev
Requires-Dist: mcp>=2.0.0; extra == 'dev'
Requires-Dist: pytest>=8.0.0; extra == 'dev'
Requires-Dist: ruff>=0.6.0; extra == 'dev'
Provides-Extra: mcp
Requires-Dist: mcp>=2.0.0; extra == 'mcp'
Description-Content-Type: text/markdown

# model2data-studio CLI

Pull a saved [model2data studio](https://studio.jbanalytica.com) project and
write its data, so a build can regenerate its fixtures instead of carrying a CSV
that was right once.

The schema is designed in the studio, where a diagram and a preview make it
worth designing. This is the part that runs afterwards, unattended, from the
same saved project — so the seeds a test suite runs against are the ones the
model says they should be, rather than whatever was committed the day someone
last remembered to export.

It is deliberately thin: no DBML parser, no generator, no schema knowledge. The
studio's API does all of that; this only knows how to ask, and where to put the
answer.

## Install

```bash
pip install model2data-studio          # or run it without installing:
uvx model2data-studio --help
```

It needs an API key from a **Pro** account on the studio: see
[pricing](https://www.jbanalytica.com/model2data/pricing/). The client is MIT
licensed; the studio it talks to is not open source.

## Configuration

Two environment variables, both overridable by a flag:

| | flag | default |
| --- | --- | --- |
| `MODEL2DATA_STUDIO_KEY` | `--key` | required |
| `MODEL2DATA_STUDIO_URL` | `--url` | `https://studio.jbanalytica.com` |

Create a key in the studio's account menu. The CLI sends it as a bearer token
and nothing else — it never prints it, and it is the only thing that identifies
the account, so put it in your CI provider's secrets rather than in a file.

Leave the key's access on **Generate and export**, which is the default and
covers everything below: reading a project, generating, exporting. A key with
**Read only** access is refused by `generate` with a 403 saying so. The two
levels above it are for things the CLI does not do — **Generate and save** adds
writing the project back, for a pipeline that pushes a schema up rather than
pulling data down, and **Full access** adds deleting a project and managing its
share links. A key that leaks out of a CI log can only do what it was given.

## Commands

```bash
model2data-studio projects              # id, name, rows, last edited
model2data-studio whoami                # the plan behind the key, and what it has used

model2data-studio generate --project <id> --out seeds/
model2data-studio generate --project <id> --out . --format dbt --adapter duckdb
```

`generate` takes every setting from the saved project — row counts, per-table
overrides, seed, locale, name — and each flag overrides one of them:

| | |
| --- | --- |
| `--format csv\|dbt` | seed CSVs, or a whole dbt project. dbt export is a Pro feature. |
| `--rows N` | the default row count for every table |
| `--rows-for TABLE=N` | one table's count, on top of the project's. Repeatable. |
| `--seed N` | pin the data. Same seed, same rows, forever. |
| `--as-of YYYY-MM-DD` | pin the day dates and timestamps are anchored on. Defaults to the project's, then today. |
| `--locale CODE` | which country's people and addresses to generate |
| `--business-hours` / `--no-business-hours` | weight timestamps toward weekdays and working hours. Pro. |
| `--growth N` | relative change in activity across the window, e.g. `0.5` for the end being half again as busy as the start. Pro. |
| `--seasonality N` | strength of an annual cycle peaking in Q4, `0.0`-`1.0`. Pro. |
| `--skew N` | how unevenly child rows spread over their parents, `0.0`-`1.0`. Pro. |
| `--name NAME` | the dbt project's name |
| `--adapter duckdb\|postgres` | the warehouse the dbt project targets |
| `--force` | replace files already in `--out` |
| `--check` | compare against `--out` instead of writing to it |

Every run also prints a row count for each table it generated, read back out
of the CSVs — a quick way to see what actually landed without opening a seed.

**Shaping flags name one field at a time.** `--growth` on its own overrides
just the growth trend; business hours and seasonality still come from the
project, the same way `--rows-for orders=9` overrides one table without
resetting every other one to the base count. Shaping is a Pro feature — a free
plan's key gets a 402 naming the plan boundary if the project or a flag asks
for anything but the uniform defaults.

**Pin a seed.** A project without one generates different rows every run, which
means a diff on every build and no way back to yesterday's data. If the project
has no seed and you pass no `--seed`, the CLI picks one and prints it, so a run
that surprised you can at least be reproduced.

**Pin the day too.** Dates and timestamps are generated relative to an anchor
day, so a seed alone reproduces its rows only for as long as that day is the
same. The studio pins one whenever a project's seed is on — **Dates as of**, in
the generation settings — and the CLI reads it back, which is what makes a run
here reproduce the rows the studio previewed rather than merely yesterday's CLI
run. `--as-of` overrides it. A project with no day pinned anchors on today — at
midnight, so every run on the same day agrees — and the CLI prints the day it
used, the same way it prints a picked seed. A pipeline that `--check`s
committed fixtures, or that needs a failing build to be reproducible on a
laptop next week, wants the day pinned one way or the other; with `--seed`, the
two together name one dataset for good.

**The project must have been opened in the studio at least once** since the
studio started saving its parse. The API never parses DBML — the browser does,
and posts the result — so a project with no stored parse has nothing to generate
from, and the CLI says so rather than failing later.

**`--check` never writes into `--out`.** It regenerates into a scratch
directory and diffs it against what is already there, printing what changed,
what is missing and what is left over, then exits 1 if they differ — so a CI
job can fail a build whose committed fixtures nobody regenerated after a schema
change, instead of shipping them stale. Exits 0 and prints `Up to date: N
files` when they match.

## Regenerating dbt seeds in CI

```yaml
name: Seeds

on: [push]

jobs:
  seeds:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: astral-sh/setup-uv@v5

      - name: Regenerate the seeds from the model
        env:
          MODEL2DATA_STUDIO_KEY: ${{ secrets.MODEL2DATA_STUDIO_KEY }}
        run: |
          uvx model2data-studio generate \
            --project ${{ vars.MODEL2DATA_PROJECT_ID }} \
            --out seeds/ --force --seed 1 --as-of 2026-01-01

      - name: Build against them
        run: uv run dbt build --target ci
```

`--force` because the checkout already holds the last run's files, and `--seed`
with `--as-of` because a pipeline that regenerates its fixtures should still be
able to explain a failure — with a pinned seed and a pinned day, a failing build
is reproducible on a laptop, however long after it ran.

Nothing is committed back: the seeds exist for the length of the job, and the
model in the studio is the thing under version control. If you would rather
commit them, add a step that opens a pull request when `git status` comes back
dirty — that turns a schema change into a reviewable diff of the data it
produces.

## Failing CI when the committed fixtures are stale

If seeds are committed rather than regenerated on every run, `--check` is the
step that catches someone editing the schema in the studio and forgetting to
pull the new fixtures into the repo:

```yaml
      - name: Check the committed seeds match the model
        env:
          MODEL2DATA_STUDIO_KEY: ${{ secrets.MODEL2DATA_STUDIO_KEY }}
        run: |
          uvx model2data-studio generate \
            --project ${{ vars.MODEL2DATA_PROJECT_ID }} \
            --out seeds/ --seed 1 --as-of 2026-01-01 --check
```

It never writes into `seeds/` — only into a scratch directory it diffs against
what's there — so this is safe to run against the checkout as-is, no `--force`
needed. `--as-of` is what lets the check pass on any day: the committed seeds
were generated relative to one day, and without it a schema with a date column
would compare against rows anchored on today and fail at the first midnight.

## The MCP server: generating from an agent instead of a shell

The same account, the same key, the same API — for the editor rather than the
terminal. The studio is where a schema is *designed*, with a diagram and a
preview to design it against; the agent in your editor is where the data is
wanted, and asking it for "two hundred believable orders from my webshop model"
should not mean pasting DBML into a chat.

```bash
uvx --from 'model2data-studio[mcp]' model2data-studio mcp   # the SDK is an extra, see below
```

`uvx` fetches it on first use, so there is nothing to install beforehand. With
the package installed (`pip install 'model2data-studio[mcp]'`),
`model2data-studio-mcp` starts the same server. For Claude Code:

```bash
claude mcp add model2data-studio \
  --env MODEL2DATA_STUDIO_KEY=m2d_... \
  -- uvx --from 'model2data-studio[mcp]' model2data-studio mcp
```

and for anything that reads a JSON config (Claude Desktop, Cursor, Zed):

```json
{
  "mcpServers": {
    "model2data-studio": {
      "command": "uvx",
      "args": ["--from", "model2data-studio[mcp]", "model2data-studio", "mcp"],
      "env": { "MODEL2DATA_STUDIO_KEY": "m2d_..." }
    }
  }
}
```

`MODEL2DATA_STUDIO_URL` works here too, for a self-hosted studio.

Five tools:

| | |
| --- | --- |
| `account` | the plan and its caps, worth reading before asking for a lot of rows |
| `list_projects` | the ids to pass to everything else |
| `describe_project` | one project's tables, columns, refs and enums as text, generation hints included |
| `preview_data` | generate and return the rows themselves, capped at fifty a table |
| `write_data` | generate and write the files, as `generate --out` does |

`preview_data` is the one worth having. An agent that can *read* generated rows
can act on them — write an assertion against them, notice that `shipped_at`
precedes `ordered_at`, spot a column coming out as placeholder sentences. It
answers with the rows inline and says how many there really are; `write_data` is
for when it has decided and wants the fixtures on disk.

Both go through the same `export.resolve` the CLI's `generate` does, so an agent
and a pipeline asking for the same project with the same arguments get the same
rows — including the part where an unpinned project has a seed picked for it and
is told which one, so the answer can be reproduced afterwards.

The MCP SDK is an optional extra rather than a dependency, on purpose: the
reason this package is two dependencies is that it installs on CI runners next
to a build that has nothing to do with the studio, and a runner regenerating
fixtures does not need an MCP SDK.

## Developing

```bash
cd cli
uv sync --extra dev
uv run pytest
uv run ruff check .
```

The tests drive the real Typer app, and the MCP tools through the server's own
`call_tool`, answering their HTTP with an `httpx.MockTransport` — so there is
nothing to run and nothing to reach.

## Releasing

Bump `version` in `pyproject.toml`, `__version__` in
`model2data_studio/__init__.py` and both versions in `server.json` (the release
workflow refuses a tag that disagrees with any of them), merge, then tag the
merge commit on `main` and push the tag:

```bash
git tag cli-v0.1.0 && git push origin cli-v0.1.0
```

`.github/workflows/release-cli.yml` runs the tests, builds, publishes to PyPI
through trusted publishing (no token stored anywhere) and then lists the new
version in the MCP registry.

<!-- mcp-name: io.github.JB-Analytica/model2data-studio -->
