Metadata-Version: 2.5
Name: dvc-catalog
Version: 0.0.1
Summary: dvc-catalog CLI: publish and retrieve datasets via the DVC Data Catalog
License-Expression: Apache-2.0
License-File: LICENSE
License-File: NOTICE
Requires-Python: >=3.12
Requires-Dist: configobj>=5.0.6
Requires-Dist: dvc>=3.55
Requires-Dist: httpx>=0.27
Requires-Dist: pathspec>=0.12
Requires-Dist: pyyaml>=6.0
Requires-Dist: typer>=0.12
Provides-Extra: dev
Requires-Dist: pytest-cov>=5.0; extra == 'dev'
Requires-Dist: pytest>=8.3; extra == 'dev'
Requires-Dist: types-pyyaml>=6.0; extra == 'dev'
Description-Content-Type: text/markdown

# dvc-catalog CLI

Client wrapper that runs `dvc push`/`pull` and syncs metadata + `.dvc` files
with the DVC Data Catalog. It is configured **the DVC way**: all catalog
settings live under the repo's `.dvc/` directory, and each dataset's metadata
lives in a YAML sidecar next to its `.dvc` file. There are **no metadata flags** —
publishing is driven entirely by committed config.

## Commands

```bash
dvc-catalog init                      # scan repo for .dvc files, scaffold config + sidecars
dvc-catalog login                     # store a token in .dvc/catalog.local (gitignored)
dvc-catalog whoami                    # show configured url / project / auth state
dvc-catalog status                    # list every .dvc and whether its sidecar is complete
dvc-catalog push [targets]            # validate, run `dvc push`, then register metadata
dvc-catalog get <dataset>[@<version>] # write the catalog's byte-exact .dvc file(s)
dvc-catalog pull <dataset>[@<version>] # get + dvc pull in one step
```

## Configuration layout

| File | Committed? | Purpose |
| ---- | ---------- | ------- |
| `.dvc/catalog` | yes (INI) | Catalog connection: `[catalog]` `url` + project UUID, slug, or unambiguous name. |
| `.dvc/catalog.local` | no (gitignored) | Secrets: `[auth]` `token`. Written by `login`. |
| `<path>.dvc.meta` | yes (YAML) | Per-dataset metadata next to each `.dvc` file. |
| `.dvc/config` | yes (DVC's own) | Read-only: remotes + `core.remote`, used to describe storage. |

A sidecar (`data/images.dvc` → `data/images.dvc.meta`):

```yaml
description: "Curated training images"   # required
owner: "nina"                            # required: user UUID, exact username, or email
tags: [images, cv]                       # optional: only sent when declared
license: MIT                             # optional: only sent when declared
custom_fields:                           # optional: only sent when declared
  sensitivity: low
```

`owner` must name a real catalog account in the project's organization. A team
or group name does not resolve and the push is rejected.

`push` refuses to run unless `.dvc/catalog` is configured, a token is stored, the
configured project is readable, and every selected dataset has a **complete**
sidecar (`description` + `owner`). The owner is resolved within the project's
organization; usernames match exactly, while email addresses match
case-insensitively. Re-sending the owner a dataset already has is not a
transfer, so a teammate holding `write` can keep publishing; naming anyone else
requires `admin` on the dataset. With no targets, every `.dvc` in the repo is
selected; pass targets to restrict it. Use `--dry-run` to validate configuration,
authentication, project access, pointer files, metadata, and DVC storage
configuration without changing the DVC remote or catalog.

### What a push overwrites

`description` and `owner` are required and are therefore always sent. Each of
`tags`, `license` and `custom_fields` is sent **only when the sidecar declares
it**, and then replaces the catalog's value outright rather than merging into
it. A key the sidecar omits is left exactly as the catalog holds it, so a push
from a repository that says nothing about tags cannot erase tags curated in the
web UI. Declaring an empty value (`tags: []`, `license: ""`, `custom_fields: {}`)
is the deliberate way to clear one. `dvc-catalog init` therefore scaffolds these
three keys commented out.

Each pointer path maps deterministically to one catalog dataset. Its display name
is the repository-relative path without `.dvc` (`data/images.dvc` becomes
`data/images`), and its slug is a normalized form plus a short digest of the full
path. This keeps repeated pushes idempotent and prevents equal file names in
different directories from colliding. A successful push:

1. runs `dvc push` for the selected pointer files;
2. creates or updates each mapped dataset from its committed sidecar; and
3. publishes an immutable version containing the byte-exact `.dvc` text, Git SHA,
   and DVC remote snapshot.

### DVC remote configuration in registration payloads

Every registration payload includes a `dvc_config` snapshot describing exactly
which named DVC remotes that dataset's push depends on. It is built purely from
`outs[]` in the dataset's own `.dvc` file and the project's committed
**`.dvc/config`**:

* `default_remote` — the project's `core.remote`, but only when some
  push-enabled output actually relies on it (no explicit `outs[].remote`).
* `remotes` — every option found in `.dvc/config` for each required remote,
  verbatim, keyed by remote name. A remote referenced only through another
  remote's `remote://<name>/...` URL is included too.
* `missing_remotes` — required remotes with no matching section in
  `.dvc/config`, or a section with a blank/absent `url`.
* `unresolved_default` — `true` when an output has no explicit remote and
  `.dvc/config` declares no `core.remote` to fall back to.

Only `.dvc/config` is read — never `.dvc/config.local`, and never any
global/system DVC configuration or `dvc config --list`. Its option values are
reported as-is with no secret detection or redaction. If someone commits a
credential to `.dvc/config`, that committed value will therefore be published;
credentials belong in the excluded local/global/system scopes instead.

An incomplete or entirely absent remote configuration never blocks a push.
`dvc-catalog push --dry-run` prints a
`dvc storage: complete|partial|unavailable` label per dataset, and the catalog
stores that same availability with the published version.

## Retrieving a version

`get` accepts a dataset UUID, slug, or exact name in the configured project.
Appending `@` selects a version by sequence number, version UUID, content hash,
or `latest`; omitting it selects the latest version. Every `.dvc` artifact in the
version is downloaded and written to its original repository-relative path.
Response bytes are not decoded or normalized, so the resulting files are
byte-identical to those that were published. Unsafe absolute, parent-traversing,
symlinked, duplicate, or non-`.dvc` paths are rejected before any file is
changed.

`pull` performs the same fetch and then runs `dvc pull` with every pointer it
wrote, reusing the repository's existing DVC installation and remote
credentials.

## Development

```bash
uv sync --extra dev
uv run dvc-catalog --help
uv run pytest
```
