A CLI, paired with a Claude Code Skill, that lets Claude (or you) answer natural-language questions about Open Targets Platform release data by generating and executing SQL against the platform's parquet files hosted on a public S3 bucket. No local data materialization, no hosted service — phase 1 runs entirely inside a local Claude Code session or a terminal.
See Product requirements document for the full design (architecture, guardrails, caching, testing strategy) and issues/ for the vertical slices phase 1 was broken into, plus follow-on issues filed since.
Claude Code session
└─ Skill (.claude/skills/otai/SKILL.md)
└─ invokes: uvx --from <repo-path> otai <subcommand> [args] [--format table]
└─ otai CLI (Python, Typer)
└─ DuckDB (httpfs, anonymous S3 access)
└─ s3://open-targets-public-data-releases/platform/<release>/output/*.parquet
otai never downloads or materializes Open Targets data locally — every
query reads live parquet files over S3 through DuckDB's read_parquet().
The first time a release is touched, otai fetches that release's
Croissant schema descriptor, caches
it forever (release data is immutable), and lazily builds one DuckDB view
per dataset; a small shared DuckDB file (~/.cache/otai/catalog.duckdb)
tracks which releases have already been built, with one schema namespace
per release so cross-release joins work in a single query.
The four commands:
list-releases— what releases exist on S3, which islatest, which are cached locally.list-datasets [--release X]— the datasets (tables) available in a release.describe-dataset <name> [--release X]— a dataset's columns, types, and relationships.run-sql "<query>" [--timeout SECONDS]— read-only SQL against the views, guarded bysqlglot-based validation: rejects anything but a singleSELECT/WITH(including mutations nested in a CTE or subquery) and rejects table-valued functions likeread_csv_auto/read_parquetas a data source (only plain, optionally schema-qualified table/view names are allowed —run-sqlcan only query the release catalog, never arbitrary local/remote files), plus a ~1000-row cap and a timeout (default ~45s, overridable per call with--timeoutfor a query that's legitimately slow rather than a mistake to fix). A proactiveEXPLAIN-based complexity check is scoped but not yet implemented (see issues/07).
Every command emits a JSON envelope ({"ok": true, "data": {...}} /
{"ok": false, "error": {"type": "...", "message": "..."}}) by default, or
a human-readable table with --format table.
Building a release's schema for the first time can take a while (each dataset resolves a glob against real S3) — a progress bar and log messages report on that, always on stderr so they never interfere with the JSON on stdout.
Everything below is optional — the defaults are correct for regular use.
| Variable | Default | Purpose |
|---|---|---|
OTAI_CACHE_DIR |
~/.cache/otai |
Where the shared DuckDB catalog, the "latest release" cache, and cached croissant.json files live. |
OTAI_BASE_URI |
the public Open Targets S3 bucket | Root the CLI reads parquet/croissant.json from. Tests point this at local fixtures; there's no reason to change it otherwise. |
OTAI_LOG_LEVEL |
INFO |
Verbosity of stderr logging (progress bars, cache hits/misses, catalog lock retries). DEBUG for more detail, WARNING to quiet it down — logging never touches stdout, so it's always safe to change. |
run-sql also takes a --timeout <seconds> CLI flag (see above) to
override the default timeout for one call, rather than an env var, since
it's a per-query decision rather than a standing configuration choice.
- uv — runs the CLI (
uvx) and manages the Python environment; every other Python dependency, includingduckdbitself, is declared inpyproject.tomland installed automatically the first time you runuvx/uv sync— there's nothing to install by hand. - git —
otaiisn't published to PyPI, so getting the source onto disk means cloning this repo. It's also whatmake dev's pre-commit hook installs into (.git/hooks/pre-commit) and what the contribution workflow runs on. Once you have the files, though, running the CLI itself doesn't touch git at all —uvx --from <path> otai ...works the same from a plain directory as from a git checkout.
The Skill lives at .claude/skills/otai/SKILL.md, checked into this repo,
and ships and versions together with the CLI. Claude Code auto-discovers
skills under a project's .claude/skills/ directory, so:
- Working directly in this repo: open it as your Claude Code project
(or a parent directory containing it) and the
otaiSkill is available immediately — no extra setup. - Using it from another project: copy (or symlink)
.claude/skills/otai/into that project's own.claude/skills/directory, and update the<repo-path>in the Skill's invocation examples to wherever you clonedotai.
Either way, there's no global install: every invocation runs uvx --from <repo-path> otai ..., so a local code change to otai is picked up on
the very next call, with no reinstall step.
uvx --from . otai list-releases
uvx --from . otai list-datasets [--release 26.03]
uvx --from . otai describe-dataset target [--release 26.03]
uvx --from . otai run-sql "SELECT count(*) FROM target"
uvx --from . otai run-sql "SELECT count(*) FROM colocalisation" --timeout 90Add --format table to any command for human-readable output; the default
is a JSON envelope ({"ok": true, "data": {...}} / {"ok": false, "error": {...}}).
From Claude Code, the otai Skill drives
these same commands automatically when you ask a question about Open
Targets data.
| Make Target | Description |
|---|---|
make dev |
Install dev dependencies and the pre-commit hook. |
make lint |
Run ruff (lint + format check) and ty. |
make test |
Run pytest. |
make clean |
Remove build/test artifacts. |
Work happens on feature branches; open a PR against main and let CI
(lint + type-check + tests) pass before merging. See
CONTRIBUTING.md for details.