Skip to main content

A lightweight build system for content

Project description

Smash

Smash is a lightweight build system for content.

It lets you define, in plain Python, how files in a directory should be transformed — without global config, dependency graphs, or special formats.

Each directory contains its own smashlet_*.py files. These scripts declare what files to process and how. Smash runs them in order, automatically and predictably.

Built for developers who want local-first, scriptable workflows — not orchestration platforms or site engines.

Why Smash?

Traditional tools make local content workflows harder than they should be.

  • Make is powerful but brittle. One global file controls everything.
  • Luigi / Airflow are great for orchestration, but overkill for local pipelines.
  • Static site generators assume you're building a website.

Smash is different:

  • No global config
  • No DAGs
  • No templating assumptions

Just plain Python files, colocated with the content they transform.

How Smash Works

Smash scans your project for smashlet_*.py files. Each one defines a local transformation: which files to process, and how.

A smashlet declares:

  • INPUT_GLOB: which files to match (e.g. "*.md")
  • OUTPUT_DIR: where to write results
  • run() or run(context): a function that does the work

Smash runs all smashlets in modification time order (oldest first). If any outputs change, it loops again — until everything is up to date.

Each smashlet is isolated. No global config. No dependency graph. No magic.

Basic Usage

Initialize a new project:

smash init

This creates a .smash/ directory in your project root.

Add a smashlet:

touch content/smashlet_render.py

Run the build:

smash

Smash finds all smashlet*.py files, runs them in modification time order, and repeats until nothing changes.


🛠 Re-running Smashlets

By default, Smash skips smashlets that haven’t changed.

Use smash run to override this:

# Run all smashlets, even if unchanged
smash run

# Run just one smashlet
smash run content/smashlet_render.py

Useful when:

  • Debugging
  • Reprocessing all outputs
  • Forcing rebuilds without changing inputs

Builds are deterministic: no DAGs, no surprises.

Helpers and Context Access

Smash includes utility functions and automatic context injection to simplify common tasks in your smashlets.


File Access with smash.files

Use read(), write(), and resolve() for path-safe file operations:

from smash import read, write, resolve

def run(context):
    content = read("data/input.txt", context)
    write("out/output.txt", content.upper(), context)

These functions:

  • Resolve paths based on the smashlet’s location (context["cwd"])
  • Work regardless of current working directory
  • Make smashlets easier to debug, refactor, and reuse

Context Folder Injection

If your project or smashlet includes a context/ folder, all its files are automatically available:

context["context"]        # Parsed JSON/YAML/TXT files
context["context_files"]  # Raw Path objects for each file

Example:

def run(context):
    prompt = context["context_files"]["prompt.txt"].read_text()
    config = context["context"]["config"]

Supports:

Extension Behavior
.json Parsed with json
.yaml Parsed if pyyaml
.txt Plain text
others Available as Paths

Smashlet-local context overrides project-level context. Only files (not folders) are included.

Helper Functions

Smash includes reusable helpers to reduce boilerplate and promote consistency in smashlets.

Import them with:

from smash import read_text_files, write_output, ensure_dir, flatten_json_dir, smash_log

Included Helpers

  • read_text_files(paths) → Reads and returns the content of each file in paths as a list of strings.

  • write_output(path, content) → Writes content to a file. Creates parent directories if needed.

  • write_output_if_changed(path, content, context) → Writes only if content has changed. Prevents unnecessary rebuilds.

  • ensure_dir(path) → Ensures that a directory exists. Creates it if missing.

  • flatten_json_dir(path) → Reads all .json files in a directory and returns a flat dict: {filename_stem: parsed_json}

  • smash_log(msg) → Consistent log output for smashlets. Equivalent to smash.log().


Example

from smash import read_text_files, write_output_if_changed

def run(context):
    contents = read_text_files(context["inputs"])
    result = "\n\n---\n\n".join(contents)

    write_output_if_changed("dist/combined.md", result, context)
    return 1

Logging from Smashlets

Use smash.log() for consistent, structured output instead of print():

import smash

def run():
    smash.log("Rendering markdown files...")

You can specify a log level:

smash.log("Missing input file", level="warn")
smash.log("Build failed", level="error")

Supported levels:

  • "info" (default)
  • "warn"
  • "error"
  • "debug"

Logs are uniform across all smashlets and can support future features like timestamps, filtering, or redirection.

Comparison

Smash solves a different problem than most build or pipeline tools.

🛠️ Make

  • Central Makefile controls everything
  • Small changes can trigger large rebuilds
  • Hard to scale with many inputs and rules

Smash: Local files define local logic. Easy to isolate and test.


🔁 Luigi / Airflow

  • Great for scheduled jobs and DAG orchestration
  • Requires boilerplate, config, and a scheduler
  • Not designed for lightweight, ad-hoc builds

Smash: Zero infra. Just run smash in your repo.


🌐 Static Site Generators

  • Assume you're building a website
  • Force routing, layout, and template logic
  • Hard to repurpose for other kinds of content

Smash: No layout engine. No site assumptions. Just Python and files.

Philosophy

Smash is built around a few simple principles:

  • 📦 Build logic should live with the files it transforms
  • ✨ Behavior should be predictable and explicit
  • 🤖 Everything should be understandable by both humans and LLMs
  • 💡 Smashlets should be self-contained, portable units
  • 🧱 No global state, no hidden dependencies

No DAGs. No YAMLs. No magic.


Contributing

Contributions are welcome — especially:

  • CLI improvements
  • Helper utilities
  • Plugins for new file types or workflows

📚 API Layers in Smash

Smash has three distinct API layers — each designed for a specific purpose and audience.

This clear separation makes the system simple to use, safe to extend, and easy to regenerate.


🟩 1. Public CLI API

Used from the terminal via commands like smash build, smash add

This is the main interface for using Smash as a tool. It includes:

  • smash init
  • smash build
  • smash add <name>
  • smash run
  • smash status

These commands are implemented internally but form a stable, user-facing interface.


🟨 2. Public Smashlet API

Used inside smashlet_*.py files via from smash import ...

This API is the safe, supported way to write logic inside smashlets.

It includes:

  • File I/O: read, write, resolve
  • Context-aware output: write_output, write_output_if_changed
  • Logging: log, log_step
  • Helpers: read_text_files, flatten_json_dir, etc.

Smashlets should only import from smash — never from smash_core.


🟥 3. Internal Core API

Used by contributors building or extending Smash itself

Implemented in smash_core/, this layer includes:

  • CLI command logic (commands/*.py)
  • Build engine logic (smashlets.py, context_loader.py)
  • Core utilities (project.py, files.py, log.py)

This code is for internal use only — it’s not part of the public interface and may change at any time.


✅ This separation keeps user code clean and safe,
🧠 while making the system easy to evolve, debug, and extend.

License

This project is licensed under the MIT License.
See LICENSE for details.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

smash_cli-0.2.0.tar.gz (12.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

smash_cli-0.2.0-py3-none-any.whl (15.1 kB view details)

Uploaded Python 3

File details

Details for the file smash_cli-0.2.0.tar.gz.

File metadata

  • Download URL: smash_cli-0.2.0.tar.gz
  • Upload date:
  • Size: 12.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.12.3

File hashes

Hashes for smash_cli-0.2.0.tar.gz
Algorithm Hash digest
SHA256 9d74b26730c15ca9c0f822699594f67c27bd7a8ec19c20a8bed11ec21d8d3407
MD5 14a0cdd464892d12c59614eb2e68e687
BLAKE2b-256 bceabd707f8574714cbcbc227c750a005da5275312ca22a6cc9e767bca6f1745

See more details on using hashes here.

File details

Details for the file smash_cli-0.2.0-py3-none-any.whl.

File metadata

  • Download URL: smash_cli-0.2.0-py3-none-any.whl
  • Upload date:
  • Size: 15.1 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.12.3

File hashes

Hashes for smash_cli-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 23f744e6c83b695645ff7abfaf9b8277f67e3ab807537a78743f2d59550c1820
MD5 3274fb783fcc50b1530d164fe4f235bb
BLAKE2b-256 53bac227204c58e677ede90684847b6f12eceb3ed0973b1fa934b3b033cf4600

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page