Skip to main content

alcove

CI Python 3.12

A personal ETL and data lake.

Overview

Alcove is an opinionated small-scale ETL framework for managing data files and directories in a content-addressable way.

Core principles

  • A reusable framework. Alcove provides a structured way of managing data files, scripts and their interdependencies that can be used across multiple projects.
  • First class metadata. Every data file has an accompanying metadata sidecar that can be used to store provenance, licensing and other information.
  • Content addressed. An alcove DAG is a Merkle tree of checksums that includes data, metadata and scripts, used to lazily rebuild only what is out of date.
  • Data versioning. Every step in the DAG has a URI that includes a version, which can be an ISO date or latest, to encourage a reproducible workflow that still allows for change.
  • SQL support. Alcove is a Python framework, but allows you to write steps in SQL which will be executed by DuckDB.
  • Parquet interchange. All derived tables are generated as Parquet, which makes reuse easier.
  • Artifacts too. Outputs that are not tables, such as rendered dashboards or fitted models, live in the same DAG as artifact steps.

Quick Start

# Install alcove
pip install alcove  # or: uv add alcove

# Initialize a new alcove
mkdir my-data-project && cd my-data-project
alcove init

# Add a data file to your alcove
alcove snapshot ~/Downloads/countries.csv countries/latest

# Create a derived table
alcove new-table derived/population.sql countries/latest

# Build all tables
alcove run

# Explore your data with DuckDB
alcove db

Usage

Install the package

You can install the alcove package from PyPI using pip, uv, or any other Python package manager:

# Using pip
pip install alcove

# Using uv (recommended)
uv add alcove

# For development
uv add --dev alcove

You can also install directly from GitHub for the latest development version:

pip install git+https://github.com/larsyencken/alcove

Using Alcove in your project

Starting a new project

To start a new project with alcove:

# Create and navigate to your project directory
mkdir my-data-project
cd my-data-project

# Set up your Python environment (optional, but recommended)
python -m venv .venv
source .venv/bin/activate  # On Windows: .venv\Scripts\activate

# Install alcove
pip install alcove

# Or with uv
uv add alcove

# Initialize the alcove
alcove init

Adding to an existing project

To add alcove to an existing project:

# Navigate to your project directory
cd your-project

# Install alcove
uv add alcove   # Or pip install alcove

# Initialize alcove in a subdirectory (optional)
mkdir data
cd data
alcove init

Initialise an alcove

From the folder where you want to store your data and metadata, run:

alcove init

This will create a alcove.yaml file, which will serve as the catalogue of all the data in your alcove.

Configure object storage

You will need to configure your S3-compatible storage credentials in a .env file, in the same directory as your alcove.yaml file. Define:

S3_ACCESS_KEY=your_application_key_id
S3_SECRET_KEY=your_application_key
S3_BUCKET_NAME=your_bucket_name
S3_ENDPOINT_URL=your_endpoint_url

Now your alcove is ready to use.

Adding a file or folder

From within your alcove folder, use the snapshot command to add a file or folder to your alcove:

alcove snapshot path/to/your/file_or_folder dataset_name

For example:

alcove snapshot ~/Downloads/countries.csv countries/latest

This will upload the file to your S3-compatible storage, and create a metadata file at data/<dataset_name>.meta.yaml directory for you to complete.

The metadata format has some minimum fields, but is meant for you to extend as needed for your own purposes. Best practice would be to retain the provenance and licence information of any data you add to your alcove, especially if it originates from a third party.

Creating a new table

To create a new table, use the new-table command:

alcove new-table <table-path> [dep1 [dep2 [...]]]

This creates a placeholder executable script that generates an example data file based on the file extension (.parquet or .sql).

Creating a Parquet table

alcove new-table path/to/your/table

This creates a placeholder Python script that generates an example Parquet file:

#!/usr/bin/env python3
import sys
import polars as pl

data = {
    "a": [1, 1, 3],
    "b": [2, 3, 5],
    "c": [3, 4, 6]
}

df = pl.DataFrame(data)

output_file = sys.argv[-1]
df.write_parquet(output_file)

Creating a SQL table

alcove new-table path/to/your/table.sql

This creates a placeholder SQL script:

-- SQL script to create a table
CREATE TABLE example_table AS
SELECT
    1 AS a,
    2 AS b,
    3 AS c

Opening in your editor

The command also supports the --edit option to open the metadata file in your editor:

alcove new-table path/to/your/table --edit

Creating an artifact

Not every derived output is a table. A rendered HTML dashboard, a fitted model or a bundle of charts is still a pure function of its dependencies, but has no place in DuckDB. Register these as artifact steps:

alcove new-artifact <artifact-path> [dep1 [dep2 [...]]]

For example:

alcove new-artifact reports/health/latest table://health/daily/latest

Write the build script at src/steps/artifacts/<artifact-path>.py. It is called with the path of each dependency, then an empty output directory, and must write at least one file into that directory:

#!/usr/bin/env python3
import sys
from pathlib import Path
import polars as pl

*deps, out_dir = sys.argv[1:]
daily = pl.read_parquet(deps[0])

(Path(out_dir) / "index.html").write_text(f"<h1>{len(daily)} days</h1>")

The output lands in data/artifacts/<artifact-path>/, with a .meta.yaml sidecar recording a checksum of every file produced and of every input. Artifacts rebuild when any dependency or the script changes, can depend on snapshots, tables and other artifacts, and can be depended on by tables in turn. Artifact scripts must be Python; they do not appear in alcove db.

Steps as folders

A step can be a folder instead of a single file. Put a __main__.py entrypoint in src/steps/tables/<path>/ (or src/steps/artifacts/<path>/) and alcove runs the folder with Python, exactly as python <folder> would. Anything else in the folder, such as a helper module, a lookup CSV or an HTML template, is part of the step: every file is checksummed into the step's input manifest, so editing, adding or removing one rebuilds the step. Bytecode caches (__pycache__) are ignored.

src/steps/artifacts/reports/health/latest/
├── __main__.py      # reads sys.argv, writes into the output directory
└── template.html    # read relative to __file__

A single <path>.py or <path>.sql file takes precedence over a folder at the same path, and the folder takes precedence over a version-shared <parent>.py. The optional metadata config for a folder step sits beside it as <path>.meta.yaml, exactly as for a single-file step.

What counts as part of the folder: every regular file, following symlinked directories, so a template shared between steps through a symlink is still an input. Dangling symlinks are an error. Ignored: __pycache__, anything starting with a dot (editor swap files, .ruff_cache, .DS_Store), and editor backups ending in ~ or wrapped in #. Step scripts run with bytecode caching disabled and redirected, so a __pycache__ left inside the folder by running the script by hand is neither read nor written.

Executing SQL step definitions

If a .sql step definition is detected, it will be executed using DuckDB with an in-memory database. The SQL file can use {variable} to interpolate template variables. The following template variables are available:

  • {output_file}: The path to the output file.
  • {dependency}: The path of each dependency, simplified to a semantic name.

Command Reference

Alcove provides the following commands:

  • alcove init - Initialize a new alcove workspace
  • alcove snapshot <path> <dataset> - Add a file or directory to your alcove
  • alcove run - Build all tables and fetch outdated data
  • alcove list - List all datasets in alphabetical order
  • alcove audit - Validate the alcove metadata
  • alcove new-table <path> [deps...] - Create a new derived table
  • alcove new-artifact <path> [deps...] - Create a derived output that is not a table
  • alcove db [query] - Open a DuckDB shell or execute a query
  • alcove export-duckdb <file> - Export tables to a DuckDB file

Building your alcove

Run the run command to fetch any data that's out of date and build any derived tables:

alcove run

It downloads the snapshot files it needs before building anything, eight at a time; --jobs (-j) sets how many.

Development

Testing with MinIO

For testing with S3-compatible storage, this project uses automatically managed containers:

# Run tests with Docker-based MinIO
make test

All tests require Docker with MinIO container to be available.

Docker Context Support

The testing framework automatically detects your current Docker context and uses it for container operations. This ensures tests work properly with:

  • Docker Desktop
  • Colima
  • OrbStack
  • Remote Docker contexts

MinIO Configuration

With Docker, these credentials are automatically used:

Containers are automatically managed and kept running between test runs for performance. MinIO's health is verified before tests run to ensure proper S3 compatibility.

Bugs

Please report any issues at: https://github.com/larsyencken/alcove/issues

Changelog

  • 0.5.1 (2026-10-09)

    • alcove run downloads snapshot files several at a time, 8 by default (--jobs/-j), and downloads each distinct file once however many snapshots hold it; previously it downloaded one file at a time, making a new S3 client for each. A failed download or Ctrl-C abandons the downloads still in progress rather than waiting for them
    • A file is added to the local cache under a temporary name and renamed into place, so an interrupted run can't leave a partial file that later reads as a cache hit (it can leave a *.partial file beside it, which is never read)
    • Requires boto3 1.36 or later. 0.5.0 already needed it, since older versions reject the checksum settings alcove passes for B2 (#60), but didn't say so
  • 0.5.0 (2026-10-07)

    • Snapshot datasets declared once as snapshot://foo/* are date-partitioned: each day is snapshotted as foo/YYYY-MM-DD, is discovered from its metadata file rather than listed in alcove.yaml, records when it was fetched, and is covered by a single data/.gitignore pattern; alcove run stops and names any day missing between a dataset's first partition and its last (see Wildcards)
    • alcove run moves partition-shaped snapshot data that has no metadata (such as a partition dropped on another clone, or missing from an older branch) into the dataset's .orphaned/ folder rather than letting its glob read it, restores it when a partition with that content returns, and deletes per-partition tables and artifacts whose version no longer exists; --dry-run reports what it would do
    • A wildcard step with wildcard dependencies is built for the versions they all have; previously it took the first dependency's versions (so a lagging second dependency failed with a KeyError), and another step referring to one of its versions directly narrowed it to that version
    • A declared partitioned dataset with no partitions yet, and wildcard steps fed by it, expand to no steps; AlcoveDB skips union views for wildcard tables with nothing built
    • alcove audit warns about snapshot metadata files that no step refers to
    • Fixed tables and artifacts not rebuilding when a latest dependency resolves to a newer version, or when a wildcard dependency gains or loses a partition (#70); steps that were stale this way rebuild on the first run after upgrading
    • Fixed latest resolving to a sibling dataset that shares a name prefix, and failing with a bare max() error when there are no versions (#69)
    • Changed the template value for several single-file snapshot versions to include their extension (data/snapshots/foo/????-??-??.csv), matching what a single version already gave, so one recipe ('{foo}') works for both; recipes that appended the extension themselves ('{foo}.csv') need updating
    • Added Alcove.versions() to list a dataset's versions
  • 0.4.0 (2026-09-22)

    • Added artifact:// steps for derived outputs that are not tables (e.g. rendered dashboards, models), built by a Python script into data/artifacts/<path>/
    • Added alcove new-artifact <path> [deps...]
    • Steps can be folders with a __main__.py entrypoint; every file in the folder (following symlinks, skipping caches and editor debris) is checksummed as an input, so templates and helper modules trigger rebuilds, and step scripts run with bytecode caching disabled
    • Fixed config validation rejecting ISO-date versions (e.g. snapshot://foo/2024-09-04) as dependencies; hyphens are now allowed in the version segment only
  • 0.3.0

    • Added wildcard * support in step URIs for date-partitioned tables (e.g. table://foo/* expands per snapshot version)
    • AlcoveDB now registers union views across all partitions of a wildcard group
    • Fixed DAG mutation bug in plan_and_run (steps dict is now copied before modification)
    • Table names now require underscores instead of dashes
    • Added documentation site with Zensical
  • 0.2.2

    • Fixed snapshot --force failing with FileExistsError when overwriting directory snapshots
    • Added programmatic API: alcove.connect() returns an AlcoveDB that queries tables as Polars DataFrames
    • Enforced dim_ columns as composite primary key when new tables are generated
  • 0.2.1 (2025-04-28)

    • Fixed gitignore handling by using data/.gitignore instead of .data-files
    • Always include tables/ in data/.gitignore
    • alcove audit --fix now migrates patterns from .gitignore and .data-files to data/.gitignore
  • 0.2.0 (2025-04-28)

    • Added .data-files file for managing alcove data ignores (#61)
    • alcove init now creates empty .data-files and ensures it's in .gitignore
    • alcove audit --fix can move patterns from .gitignore to .data-files
    • Prevents .gitignore from changing frequently with data file updates
  • 0.1.2 (2025-04-25)

    • Fixed B2 compatibility with recent boto3 versions by disabling checksum validation (#60)
    • Simplified testing approach by always requiring Docker with MinIO
    • Added PyPI package configuration and installation instructions
    • Improved documentation with quick start guide and command reference
  • 0.1.1 (2025-04-25)

    • Renamed project from "shelf" to "alcove"
    • Added automated Docker container management for testing with MinIO
    • Enhanced Docker context support for different environments (Docker Desktop, Colima, OrbStack)
    • Improved S3-compatible storage testing reliability
    • Fixed test fixtures to use consistent credentials
  • 0.1.0 (Initial release)

    • Initialise a repo with shelf.yaml
    • shelf snapshot and shelf run with file and directory support
    • Only fetch things that are out of date
    • shelf list to see what datasets are available
    • shelf audit to ensure your alcove is coherent and correct
    • shelf db to enter an interactive DuckDB shell with all your data

Metadata

Release files for alcove 0.5.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for alcove 0.5.1
File Size Uploaded Provenance
alcove-0.5.1.tar.gz 112.5 kB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for alcove 0.5.1
File Interpreter ABI Platform
alcove-0.5.1-py3-none-any.whl Python 3 none any Details

Total release size: 160.2 kB

Release files / alcove-0.5.1.tar.gz

Download URL alcove-0.5.1.tar.gz
Size 112.5 kB
Tags Source
SHA-256 checksum
How to use checksums
5c6e4a7c0aa5b2f7ef199a7bd810be42cf4a467756e41c7c69e9310586c15ede
BLAKE2b-256 checksum
How to use checksums
20d8102ba7e38a9c2f3210da1e134b676e7bb60996d847523dcbbc6f0488fa93
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.1.0 CPython/3.12.10

Release files / alcove-0.5.1-py3-none-any.whl

Download URL alcove-0.5.1-py3-none-any.whl
Size 47.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
df51627f9fc972d3858faaf87f6b8b5e835559dced01fe0264094214d6fbca9d
BLAKE2b-256 checksum
How to use checksums
b896c67b4b30634a9499e6d6d348064a401ef53bd4c01170ba96439688dcb4e2
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.1.0 CPython/3.12.10

Security for alcove 0.5.1

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page