Skip to content

Repository files navigation

py-scriptkit

A small, standard-library-only toolkit that gives single-file Python scripts a consistent spine: dataclass-driven config (CLI + environment variables), structured logging, and a few utilities. Scripts depend on it by pinning a released version, so a script keeps running against the scriptkit it was born with even after the library moves on.

Repository: py-scriptkit · import name: scriptkit (import scriptkit).

File architecture

py-scriptkit/
├── src/scriptkit/          the package (import name = scriptkit)
│   ├── __init__.py         public API + __version__ (single source of truth)
│   ├── settings.py         ScriptSettings, build_parser_from_settings, parse_settings
│   ├── logging.py          get_logger / set_log_level (RichLogger or stdlib shim)
│   ├── times.py            timestamp()
│   ├── azure.py            lazy DefaultAzureCredential helper
│   ├── cli.py              `scriptkit new` scaffolder (console entry point)
│   ├── py.typed            PEP 561 marker — ships inline types to consumers
│   └── templates/
│       └── script_template.py   the canonical starting point for a new script
├── tests/                  pytest suite (one file per module)
├── .github/workflows/      ruff + pyright + pytest on {ubuntu,windows} × {3.11,3.12,3.13}
├── .vscode/                run / debug / test config
├── setup-venvs.ps1         builds the per-version dev venvs
├── pyproject.toml          package metadata (version is dynamic — read from __init__)
├── CHANGELOG.md            per-release notes, keyed by git tag
└── ROADMAP.md              phased action plan across the three repos

src/scriptkit/ is the standard "src layout": src/ keeps the package off the repo root so tests run against the installed copy, and scriptkit/ is the package folder (its name is the import name).

One-time setup (development)

Only needed to develop scriptkit itself (editing src/scriptkit/, running tests). Consuming scripts don't need this — they just uv run (see below).

Install uv:

powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"

Create the per-version dev virtualenvs (one editable .[dev] install per supported Python minor version):

.\setup-venvs.ps1            # builds .venv311 / .venv312 / .venv313
.\setup-venvs.ps1 -Force     # delete and recreate them

VS Code uses .venv313 by default for IntelliSense, linting, and debugging (switch with Python: Select Interpreter or the versioned debug configs).

Execution

Scaffolding a new script

scriptkit new writes a fresh, pinned copy of the bundled template — the cross-platform replacement for the old per-repo new-script.ps1. Run it from a dev .venv that has scriptkit installed, or straight from git with uvx:

# from a dev venv (scaffolds into ./scripts, pinned to that venv's scriptkit):
.\.venv313\Scripts\scriptkit.exe new my_tool

# or with no local install, straight from a tag:
uvx --from "scriptkit @ git+https://github.com/acalderhead/py-scriptkit.git@v0.4.0" scriptkit new my_tool

scriptkit new NAME [--tag vX.Y.Z] [--dir DIR] [--force] — the name is normalized to a snake_case filename; --tag defaults to the running scriptkit's own version; --dir defaults to scripts.

Using scriptkit in a script

Consumers don't install scriptkit — they pin a released tag in the script's PEP 723 header, and uv run fetches it on first run:

# /// script
# requires-python = ">=3.11"
# dependencies = [
#   "scriptkit @ git+https://github.com/acalderhead/py-scriptkit.git@v0.4.0",
# ]
# ///
from dataclasses import dataclass, field
from datetime import datetime
from typing import Literal

from scriptkit import ScriptSettings, get_logger, parse_settings

logger = get_logger(__file__)

@dataclass(frozen=True)
class Settings(ScriptSettings):
    name: str = "World"           # -> --name / APP_NAME, default "World"
    retries: int = 3              # scalars, bool, Path, Decimal, UUID, ...
    mode: Literal["fast", "slow"] = "fast"   # Enum / Literal -> choices
    tags: list[str] = field(default_factory=list)  # --tags a b c / APP_TAGS=a,b,c
    since: datetime | None = None            # Optional[...] + ISO date/datetime

Each settable field becomes a --flag and reads an APP_* env var; the type drives parsing (choices for Enum/Literal, nargs for list[X], fromisoformat for datetime/date, unwrapping for Optional[...]).

What a script inherits from the package:

API Purpose
ScriptSettings frozen-dataclass config base; dir_basedata/ / output/ path cascade
parse_settings(cls) build a CLI + env wiring from the dataclass (precedence: CLI > env > default)
get_logger / set_log_level RichLogger console output, or a stdlib fallback with the same semantic methods
timestamp(granularity) compact UTC stamps (year → second)
scriptkit.azure.get_credential lazy DefaultAzureCredential

The semantic logging vocabulary (stage, step, substep, config, metric, result, read, write, metadata, alert, check, plus stdlib info / warning / error / debug) works identically under RichLogger and the fallback. Add the [rich] extra to the pin (scriptkit[rich] @ git+...@v0.4.0) for decorated console output; without it, the same calls print plain [TAG]-prefixed lines.

Maintenance

All commands below assume the dev venvs from setup-venvs.ps1 exist (each is an editable .[dev] install, so ruff and pytest are present). Every one also has a VS Code task — run it from Terminal → Run Task instead of typing the path if you prefer.

Linting & formatting

Ruff is configured in pyproject.toml under [tool.ruff] (line length, pinned rule set, src/scriptkit/templates/ excluded). From the repo root:

.\.venv313\Scripts\python.exe -m ruff check .          # report lint issues
.\.venv313\Scripts\python.exe -m ruff check --fix .    # apply the safe auto-fixes
.\.venv313\Scripts\python.exe -m ruff format .         # reformat in place

Lint is Python-version-independent, so the default .venv313 is enough. Tasks: ruff check, ruff format. Format-on-save is already enabled for Python files (.vscode/settings.json), so day to day you mostly just save.

Type checking

Pyright (in the dev extra) checks the shipped package against the 3.11 baseline; the package ships a py.typed marker, so this gate protects the types consumers actually see. Configured under [tool.pyright] (checks src, skips the bundled template).

.\.venv313\Scripts\python.exe -m pyright

Tests

The suite lives in tests/ (one file per module; testpaths set in pyproject.toml). Run it on the default version:

.\.venv313\Scripts\python.exe -m pytest -q

Run it on all three supported versions — the local equivalent of the CI matrix, and every public API must pass on each:

.\.venv311\Scripts\python.exe -m pytest -q; .\.venv312\Scripts\python.exe -m pytest -q; .\.venv313\Scripts\python.exe -m pytest -q

Tasks: pytest (3.13) (default test task), pytest (3.11/3.12), pytest (all versions) — or the VS Code Testing beaker.

Versioning

Semantic Versioning: PATCH = fixes, MINOR = new backward-compatible features, MAJOR = breaking changes. Scripts pin a tag, so a published tag is a promise — never move a tag after release; only add new ones.

The release checklist

Assume nothing about prior versioning knowledge; every step is spelled out.

  1. Make the change under src/scriptkit/, with tests in tests/.
  2. Bump __version__ in src/scriptkit/__init__.py. Do NOT edit a version in pyproject.toml — there isn't one to edit. The version is declared dynamic and hatch reads it from __version__ automatically (see [tool.hatch.version]).
  3. Only touch pyproject.toml if you add or change dependencies or extras ([project.dependencies] / [project.optional-dependencies]). Nothing else there changes per release.
  4. Add a CHANGELOG.md entry under a new ## [vX.Y.Z] heading (move items out of ## [Unreleased]), grouped Added / Changed / Fixed.
  5. Go green:
    .\.venv313\Scripts\python.exe -m ruff check .
    .\.venv313\Scripts\python.exe -m pyright
    .\.venv313\Scripts\python.exe -m pytest -q
    (Or the ruff check / pytest (all versions) VS Code tasks.)
  6. Commit, tag, push:
    git commit -am "Release vX.Y.Z: <summary>"
    git tag vX.Y.Z
    git push origin main --tags
  7. Move scripts onto the new version by bumping the pins where the default lives: src/scriptkit/templates/script_template.py, each script repo's new-script.ps1 / setup-venvs.ps1 default -Tag, and the READMEs. Existing scripts keep their old pin on purpose — bump a script's header only when you want the new behavior.
  8. Confirm CI is green and at least one uv run <script> resolves the new tag.

Rules

  • Stay standard-library-only; heavy dependencies go behind an optional extra (like [rich] / [azure]), never in the base install.
  • Every public API has a test; CI must be green before tagging.
  • Anything printed stays ASCII-safe in logs (avoid characters a legacy Windows console can't render).

About

A zero-dep helper for single-file Python scripts. Maps dataclasses to CLI flags and env vars, wraps logging, and stays out of your way. Pin it via git tags.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages