Lockfile
esker.lock — format, lifecycle, hand-emitted TOML.
esker.lock is the project-scoped pin file, sibling to pyproject.toml. It records the exact version, content hash, and lineage hash of each bound dataset, so a clean clone + esker sync reproduces the cache byte-for-byte.
Created and maintained by esker add, esker upgrade, esker remove. Read by esker sync. Hand-edits are lossy on the next mutation.
Format
# generated; do not edit
[[dataset]]
name = "us.treasury.yields"
owner = "archie"
version = "1.0.0"
schema_version = "1.0.0"
content_hash = "sha256:79eeb5cb20248d4efa4fc516cb517f0afcd2e2d83bcdef63d9860de7ee3b613e"
lineage_hash = "sha256:5ef35cc43463ca5624f299a4bfd2a23a67327639072eb4af2008912667e523da"
resolved_at = "2026-04-27T15:20:08.878000+00:00"
Conventions:
- Comment header is literal
# generated; do not edit. Always rewritten. - One
[[dataset]]block per binding. Sorted alphabetically byname. - Padding aligns the
=(looks likename =with the value flush). lineage_hashline omitted ifNone.- Trailing blank line, then a final newline.
Reading uses tomllib (stdlib, Python 3.11+). Writing is hand-emitted — no TOML write dependency in scope.
Per-entry shape
| field | source | what |
|---|---|---|
name |
binding key | dataset domain |
owner |
binding value's owner | publisher handle |
version |
manifest's schema_version |
resolved version |
schema_version |
manifest's schema_version |
record-shape contract |
content_hash |
manifest's content_hash |
sha256 of parquet bytes |
lineage_hash |
manifest's lineage_hash |
sha256 of lineage.json |
resolved_at |
manifest's published_at |
when the version was resolved |
version and schema_version are currently always equal — the dataset's evolution sequence and its record-shape contract use the same number. They're stored as separate fields for forward-compat; if they diverge in a future release, both will be tracked.
Lifecycle
| event | mutation |
|---|---|
esker add <owner>/<name> |
inserts/replaces [[dataset]] row from the manifest |
esker remove <name> |
drops the row |
esker upgrade <name> |
replaces the row with the latest manifest's data |
esker sync |
reads only — never mutates |
Project root
The lockfile lives next to the nearest pyproject.toml in the cwd's parent chain. So cd-ing into a subdirectory and running esker add writes to the project root, not the subdir.
Hand-edits are lossy
Manual edits to esker.lock (extra fields, comments other than the generated header, custom keys) are dropped on the next add/upgrade/remove rewrite. The header # generated; do not edit is the warning.
If you want to change a pin, edit the binding in pyproject.toml instead — the lockfile follows.
Sync semantics
esker sync reads the lockfile and reconciles the local cache. Per entry:
- Fetch the manifest from the hub.
- If the hub's
content_hashdiffers from the lockfile entry → report drift, exit non-zero. Does not auto-upgrade. - Otherwise, ensure the parquet is in
~/.esker/cache/<owner>/<name>/<version>/data.parquetand content-hash verifies.
Hash drift is treated as a real error, not a silent recover. The fix is esker upgrade <name> — explicit, per-name. That's intentional: drift is a security signal, not a routine event.
Pinned bindings vs lockfile
A versioned binding (<owner>/<name>@<version> in pyproject.toml) short-circuits the lockfile entirely for that dataset — pyproject.toml is the durable intent.
An unversioned binding consults the lock for the pin. The lock is the auto-tracked latest, written by add and upgrade.
Global bindings have no lockfile. They always resolve to "latest" at fetch time.
See also
- Bindings — the project / global / lockfile resolution order
- CLI bindings —
add,remove,sync,upgrade - Manifests — source of truth for the lock entry's hashes