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).
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).
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 themVS Code uses .venv313 by default for IntelliSense, linting, and debugging
(switch with Python: Select Interpreter or the versioned debug configs).
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_toolscriptkit 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.
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
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"What a script inherits from the package:
| API | Purpose |
|---|---|
ScriptSettings |
frozen-dataclass config base; dir_base → data/ / 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.
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.
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 placeLint 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.
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 pyrightThe 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 -qRun 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 -qTasks: pytest (3.13) (default test task), pytest (3.11/3.12),
pytest (all versions) — or the VS Code Testing beaker.
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.
Assume nothing about prior versioning knowledge; every step is spelled out.
- Make the change under
src/scriptkit/, with tests intests/. - Bump
__version__insrc/scriptkit/__init__.py. Do NOT edit a version inpyproject.toml— there isn't one to edit. The version is declareddynamicand hatch reads it from__version__automatically (see[tool.hatch.version]). - Only touch
pyproject.tomlif you add or change dependencies or extras ([project.dependencies]/[project.optional-dependencies]). Nothing else there changes per release. - Add a
CHANGELOG.mdentry under a new## [vX.Y.Z]heading (move items out of## [Unreleased]), groupedAdded/Changed/Fixed. - Go green:
(Or the
.\.venv313\Scripts\python.exe -m ruff check . .\.venv313\Scripts\python.exe -m pyright .\.venv313\Scripts\python.exe -m pytest -q
ruff check/pytest (all versions)VS Code tasks.) - Commit, tag, push:
git commit -am "Release vX.Y.Z: <summary>" git tag vX.Y.Z git push origin main --tags
- Move scripts onto the new version by bumping the pins where the default
lives:
src/scriptkit/templates/script_template.py, each script repo'snew-script.ps1/setup-venvs.ps1default-Tag, and the READMEs. Existing scripts keep their old pin on purpose — bump a script's header only when you want the new behavior. - Confirm CI is green and at least one
uv run <script>resolves the new tag.
- 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).