Skip to main content

stapel-tools

CI coverage pypi downloads python license

CLI scaffold and linting tools for Stapel/Django projects.

Install

pip install git+https://github.com/usestapel/stapel-tools.git

Or as a dev dependency in your project:

pip install -e path/to/stapel-tools

Commands

stapel-create-project — interactive project wizard

stapel-create-project                          # full wizard
stapel-create-project my-app --type monolith   # skip some wizard steps
stapel-create-project my-app \
    --type monolith \
    --title "My App" \
    --url https://myapp.com \
    --company-name "ACME" \
    --company-email hello@myapp.com \
    --modules auth billing              # fully non-interactive

Project types: monolith (recommended), microservices, minimal (no Docker, SQLite).

stapel-new-service — add a service to an existing project

stapel-new-service auth
stapel-new-service auth --title "Auth Service" --prefix iron-
stapel-new-service blog --celery
stapel-new-service blog --dry-run

stapel-new-library — scaffold a standalone stapel-* package repo

For contributing a new reusable package to the framework (or building your own to the same standard). Materializes the Stapel library standard: flat layout, STAPEL_<NAME> settings namespace, comm surface with JSON schemas, serializer seams, MODULE.md, community files, CI with the codecov ratchet/floor policy, ruff git hooks. The generated repo's own test suite is green out of the box.

stapel-new-library search                                 # L2 service module
stapel-new-library attributes --kind library              # L1 importable lib
stapel-new-library support-chat --title "Support chat" --dir ~/Projects

Kinds: module (default — Django app with models/views/comm surface; modules never import each other) and library (importable package without service identity, like stapel-attributes).

stapel-new-react-lib — scaffold a headless @stapel/<module>-react pair

The frontend counterpart to stapel-new-library: materializes a headless React/TS pair from the auth-react etalon (frontend-standard §9, frontend-core-architecture §4 checklist) into a stapel-react monorepo (packages/<module>-react). Emits the layer stack api → model → flows → headless → i18n, namespaced query keys, the create<Module>Runtime / <Module>Provider wiring, a module-scoped i18n bundle + errors map, a vitest smoke suite, and full package hygiene (ESM, sideEffects:false, isolatedDeclarations, src-in-tarball, size-limit, manifest/llms.txt exports). The createFlowMachine primitive is imported from @stapel/core, never copied.

stapel-new-react-lib notifications                        # → @stapel/notifications-react
stapel-new-react-lib billing --title "Billing"            # backend defaults to stapel-billing
stapel-new-react-lib profiles --react-dir ~/Projects/stapel/stapel-react

Fork-free: the generated package.json wires the monorepo's env-parametrized codegen drivers (scripts/gen-{flows,errors,manifest}.mjs) via env knobs rather than duplicating them. Each pair owns three per-package drift gates (gen:{flows,errors,manifest}:check); gen:api is core-owned. After scaffolding: pnpm install && pnpm --filter @stapel/<module>-react gen build lint test.

stapel-new-module — add a Django app to a service

cd svc-auth/
stapel-new-module users
stapel-new-module billing --title "Billing Plans"

stapel-remove-service — remove a service

stapel-remove-service auth
stapel-remove-service auth --prefix iron- --yes
stapel-remove-service auth --dry-run

stapel-codegen — emit the frontend codegen source artifacts

Runs inside a configured all-modules Django instance (e.g. stapel-example-monolith on sqlite) and emits the three language-agnostic backend artifacts the frontend TS client is generated from (docs/done/flow-system-v1.md §0.1):

  • schema.json — the unified drf-spectacular OpenAPI for every installed module (same document the instance serves at /schema/, produced offline via the spectacular management command — no server, byte-stable).
  • flows.json — the generate_flow_docs machine artifact.
  • errors.json — the generate_error_keys machine artifact: every error.<status>.<name> key the instance can raise, with its HTTP status, {param} slots, remediation hint and canonical English text (stapel-core's stapel_core/django/api/errors.py).

All three use a byte-stable JSON encoding, so regenerating without a code change yields zero diff — the invariant a drift gate rests on.

DJANGO_ENV=local DJANGO_SETTINGS_MODULE=config.settings.codegen \
    python -m stapel_tools.codegen --out codegen/generated

In stapel-example-monolith this is wrapped as make codegen (regenerate) and make codegen-check (drift gate). The generated schema.json then feeds stapel-react's pnpm gen:api (openapi-typescript → typed @stapel/core API).

stapel-analytics-report — typed-analytics summary report

Generates the analytics/flow report (frontend-guardrails §3.3) across a pnpm workspace of @stapel/*-react pairs and/or a customer app, from static generated artifacts (events.json/manifest.events, backend flows.json, manifest.machines) plus a syntactic scan of TS/TSX call sites (tracked()/trackedSubmit()/track(), data-analytics="flow"/"none" markers, eslint-disable … -- description). Two slices are always separated: app (customer code) and library (@stapel/* pairs).

# machine-readable report.json to stdout
stapel-analytics-report path/to/stapel-react

# report.json + report.md + report.html into a dir, with canonical backend prose
stapel-analytics-report path/to/stapel-react \
    --backend-flows path/to/monolith/codegen/generated/flows.json \
    --out ./analytics-report

stapel-analytics-report ./my-app --package packages/web --format md

Outputs report.json (for the Studio project passport), report.md, and a self-contained report.html. Per event: description, typed props, emit sites (file:line + component), linked flow. The flow report joins backend flows with frontend coverage and renders a [gated: <ENV>] badge (from gated_by, task G6 — absent means always-on). --capabilities is reserved (§3.4 env-aware).

stapel-catalog — module-catalog aggregator

Aggregates every module's docs/capabilities.json (the fourth contract artifact) into a catalog for stack advisors and CTO prompts. Emits catalog.json (the full machine aggregate — every source document verbatim plus roll-up totals and curated recipes), catalog.md (a compact, prompt-ready projection: header roll-up, then per module a provides one-liner, an axis table key | default | ops gated, extension-point names and requires) and llms.txt (the fleet's own root index, badge-canon §3 p.5: one line per module — its provides one-liner and a link to that module's own docs/llms.txt, the fifth contract artifact from stapel-llms-txt below). llms.txt is what an agent that does not yet know which module it needs should read FIRST — one small file instead of catalog.md in full or every modular docs/llms.txt. A module counts as "described" only once its own docs/llms.txt actually exists on disk (or in the wheel, under --from-installed) — a module without one yet is listed by name under "Not yet described", loudly, never silently dropped. All three outputs are deterministic (modules sorted by name, axes by key, no timestamps), so they are stable enough to commit into other repos' system prompts.

# scan a workspace (repos without capabilities.json are skipped with a warning)
stapel-catalog --workspace ~/Projects/stapel --out-dir ./catalog

# explicit module repos (or direct capabilities.json paths)
stapel-catalog ../stapel-auth ../stapel-billing --out-dir ./catalog

# add curated composite recipes (marketplace = N modules) as their own section
stapel-catalog --workspace ~/Projects/stapel --recipes recipes.yaml --out-dir ./catalog

# the environment IS the source: every installed stapel-* wheel, no checkout
stapel-catalog --from-installed --index -o ./stack_index.json

Freshness is a property, not a chore

Two rules keep a catalog from becoming a lie:

  1. --from-installed sources the aggregate from the current environment — every installed stapel-* distribution that ships docs/capabilities.json in its wheel (modules ship capabilities.json, flows.json, errors.json, CONFIG.MD and — once a module adopts stapel-llms-txtllms.txt as package-data, so an installed-sourced index is not a degraded one; the root llms.txt's "described" count comes from the same wheel-shipped files). The result is a pure function of the lockfile: it cannot drift away from the code the product actually runs, no matter whose discipline lapses.
  2. --check is the drift gate for a committed catalog: it rebuilds in memory and compares byte-for-byte against the artifact on disk, exiting non-zero on any mismatch. A committed aggregate without this gate in CI is a snapshot that will go stale silently — that is not a hypothesis, it is how both of this project's aggregates rotted. Wire it next to make contract-check.

stapel-tools deliberately commits no catalog of its own: the artifact belongs in the repo that consumes it, behind that repo's --check gate.

Recipes are curated, not derived — a minimal, dependency-free YAML subset:

recipes:
  - name: marketplace
    summary: Two-sided marketplace — accounts, profiles and listings.
    modules: [stapel-auth, stapel-profiles, stapel-listings]
    notes: reviews live in a separate target-generic module

stapel-docs — bilingual API/flow documentation

Renders docs/api.en.md + docs/api.ru.md at a generated project's root from artifacts it already ships: schema.json (endpoints, DTO fields — descriptions sourced straight from backend docstrings, R004 canon), flows.json (flow/step user stories) and errors.json (every error.<status>.<name> code). Where a module has already shipped a Russian translation (translations/flows.ru.json / translations/errors.ru.json — the stapel-translate precedent), the Russian doc uses it verbatim; where it hasn't (yet), the English text is shown with an honest (en) marker instead of a fabricated translation. Supports three schema layouts: a monolith's codegen/generated/schema.json aggregate (grouped back into per-module sections by path prefix), a per-service/vendored-lib <service>/docs/schema.json, or a literal <mod>/api/v1/schema.json.

stapel-docs .              # write docs/api.en.md + docs/api.ru.md
stapel-docs . --check      # drift gate (pre-commit's api-docs-check)

A project with no schema.json generated yet is a graceful no-op, exit 0.

stapel-gen-client — project-owned typed API-client override

Tier 2 of the answer to "if we override a module's backend, its frontend pair must handle that" (docs/pending/profile-fields.md "Owner addendum"): regenerates a typed TS client from the PROJECT'S OWN schema.json (not the library's upstream one) into frontend/src/api/generated-override/<module>/schema.ts — reusing openapi-typescript (the same generation core stapel-react's own scripts/gen-api.mjs uses) via npx, not a reimplementation. A pair's api layer can point at this file instead of its own bundled types once a host has overridden that module.

Only does anything once the project actually HAS an override — a non-empty STAPEL_SWAP = {...} anywhere in the project, or an explicit stapel.override.json with "clientOverride": true:

stapel-gen-client .              # no-op without an override signal
stapel-gen-client . --check      # drift gate (pre-commit's gen-client-check)
stapel-gen-client . --force      # generate even without a detected override

stapel-lint — project-specific static linter

stapel-lint                        # scan current directory
stapel-lint svc-auth/              # scan specific service
stapel-lint --stats                # show per-rule counts
stapel-lint --ignore R002          # skip a rule

Rules: R001 bare Response(), R002 serializers.ValidationError, R003 undocumented @action, R004 @dataclass without docstring, R005 hardcoded error string, R006 StapelResponse(dict).

Suppress per-line: # noqa: R001

stapel-migration-lint — expand/contract gate for Django migrations

stapel-migration-lint                        # lint every app under .
stapel-migration-lint svc-app/ --json        # machine output (+watermarks/floors)
stapel-migration-lint . --base-sha <sha>     # verify against the previous release
stapel-migration-lint . --strict             # warnings become errors

Static (AST) analysis — no Django settings needed, so it runs on customer project checkouts at release cut AND on stapel-* module repos in CI. Rules: MIG001 destructive op (RemoveField/DeleteModel/Rename*/narrowing AlterField) requires the # stapel: contract-phase file marker (destructive changes ship one release after the code stopped using the target); MIG002 with --base-sha the destroyed target must not be referenced by the app's code at the previous release's sha; MIG003 RunPython/RunSQL without a reverse requires # stapel: irreversible (lowers the app's reversible_floor in release.json); MIG004 NOT NULL AddField without default/db_default on an existing model (breaks N-1 rollback).

stapel-adoption-lint — honesty gate for stapel-module adoption

stapel-adoption-lint .                       # lint the project in .
stapel-adoption-lint proj/ --json            # machine output
stapel-adoption-lint proj/ --strict          # warnings become errors
stapel-adoption-lint proj/ --workspace ~/ws  # extra root for sibling module repos

Catches modules "adopted" on paper but not in fact (a dependency without a mount, a route re-implemented over one the module ships, a done migration off main). ADO001 (error) a module installed (requirements/INSTALLED_APPS) and shipping a urlconf but not mounted in ROOT_URLCONF — declare intentional headless use with # stapel: headless <mod>; library-only modules are exempt. ADO002 (error) a project-owned urlpattern whose route (params normalized, so <int:pk>{id}) duplicates a path in an installed module's docs/schema.json. ADO003 (warning) STAPEL-MIGRATION.md records done work but the branch is not main/master nor merged into it. ADO004 (warning) a requirements pin never imported anywhere (dead pin, e.g. PyJWTjwt); stapel modules, settings-configured packages, and an entry-point-only runtime/tooling allowlist are exempt.

stapel-surface-lint — pre-merge gate against reinventing what the fleet ships

stapel-surface-lint .                        # lint the project in .
stapel-surface-lint proj/ --json             # machine output
stapel-surface-lint proj/ --workspace ~/ws   # extra root for sibling module repos
stapel-surface-lint proj/ --no-installed     # workspace checkouts only

Reads the surface section of every capabilities.json the environment and the workspace expose (installed distributions first — the index is a function of the lockfile) and fails the branch that rebuilds one of its entries. SUR001 (error) a BasePermission subclass declared under a name an installed module already publishes as a permission_class — matched on the published name, so a product's own domain permissions stay quiet. SUR002 (error) a symbol listed in instead_of sits in permission_classes while its published replacement is used nowhere in the project; one finding per displaced symbol, never per call site. SUR003 (error) a gate_function imported and never mentioned again — re-export hubs, __all__, TYPE_CHECKING and any value reference are cleared first. SUR004 (error) a capability_field with consumer: frontend that the -react package reads nowhere outside its generated OpenAPI types; reported only to the publishing module and to the consuming package. Composed into stapel-verify. Silent, with a note, in an environment whose installed modules ship no docs/capabilities.json yet.

stapel-swap-lint — the anti-lock-in indirection gate

stapel-swap-lint .            # lint the project in .
stapel-swap-lint proj/ --json # machine output

SWAP001 (error) a direct import — or instantiation — of a class registered as the default= of a get_model() / get_presenter() call somewhere in the tree: one stray import silently defeats a host's config-swap for that call site, with no error, just an override that never takes effect. SWAP002 (error) a views.py filling in a DTO imported from a sibling dto.py by hand instead of going through a presenter.

SWAP003 (error) a hardcoded dotted path into another top-level package, resolved at runtimeimport_string("other_pkg.validators.validate"), importlib.import_module("other_pkg.thing"), apps.get_model("other_app", "Model"), apps.is_installed("other_app"), find_spec("other_pkg"), getattr(other_pkg_module, "symbol"). Prototype: stapel-workspaces 0.19.0, which asked Django's app registry whether stapel_profiles ran in this process and then resolved stapel_profiles.validators.validate_display_name by string. It worked in a monolith and answered a permanent 503 in a split deployment, because a symbol resolution has no remote form.

The line the rule draws — and the reason it does not outlaw the fleet's own extension mechanism — is where the value comes from, decided at the call site with no index and no configuration:

shape verdict
import_string(settings.STAPEL_RECORDINGS["STORAGE"]) silent — the host chose it
import_string(getattr(settings, "NORMALIZER", DEFAULT)) silent — a declared extension point
get_model(KEY, default="ourpkg.models.Thing") silent — a swap seam waiting to be overridden
import_string("ourpkg.validators.validate") silent — your own overridable entity
apps.is_installed("django.contrib.admin") silent — a question about the host's config
find_spec("pyvips") with pyvips in an extra silent — declared, pinned, installed with you
import_string("other_pkg.validators.validate") error — nobody chose it but the author

"Ours" is derived, never configured: every top-level package in the tree, every AppConfig label, the pyproject.toml distribution name, everything the manifest pins ([project.dependencies], any extra, any requirements*.txt), the standard library, and django. There is no allowlist to add yourself to. tests/, test_*.py, migrations/ and .vendor/ are out of scope; a dotted path inside a code template or a settings assignment is data, not resolution, and is never seen. Suppress a deliberate exception with # noqa: SWAP003. Composed into stapel-verify.

Measured across the fleet on release: 34 raw hits, 3 after triage.

stapel-llms-txt — generate the module's own docs/llms.txt

stapel-llms-txt .                       # emit <repo>/docs/llms.txt
stapel-llms-txt . --check               # drift gate (nonzero exit, no write)
stapel-llms-txt . --out /tmp/x          # render a checkout you must not write to
stapel-llms-txt . --budget 8000         # raise the ceiling DELIBERATELY
stapel-llms-txt . --skip-missing        # a repo with no contract is a loud no-op

The fifth per-module contract artifact, next to docs/{schema,flows,errors,capabilities}.json and under the same discipline: emitted by make contract, gated by make contract-check, committed, shipped in the wheel. It renders the module's surface slice for an agent's context — header + provides, then Configuration axes (key, kind, default, business label, gated operations and the OR-composed co-gates), Usage surface (the main section: name — path, instead of, consumer, the curated intent), Extension points, Fits with, then the compact METHOD /path — operationId catalog grouped by tag with the mount prefix factored out, the error codes one line each (code [status] remediation {slots} — localized prose stays in errors.<lang>.md), and the flow index. Sections whose source document is absent do not appear at all, so stapel-core — no OpenAPI, no axes — renders as surface + seams.

Three properties, copied from the frontend's scripts/gen-manifest.mjs rather than reinvented:

  • deterministic — every list has an explicit sort key, nothing carries a timestamp or an absolute path, so the drift gate compares bytes without false reds;
  • hard token budget — 4000 by default, the frontend's LLMS_TOKEN_BUDGET. Over budget fails with a per-section cost breakdown and writes nothing; it never truncates, because a cut context file is indistinguishable from a complete one at the point of use. The reported trim order is surface → axes → extension_points; --budget N in the module's Makefile is the deliberate alternative;
  • loud when there is nothing to say — a module with no docs/capabilities.json is an error naming the module, never an empty llms.txt. An empty context file answers "does the fleet have a mechanism for X?" with a confident no.

Per-module wiring is two lines:

contract:       python3 -m stapel_tools.llms_txt .
contract-check: python3 -m stapel_tools.llms_txt . --check

stapel-release-manifest — build the open release.json manifest

stapel-release-manifest . --release r4 --git-sha $(git rev-parse HEAD) \
    --image app=registry/tenant/proj/app:r4 --out release.json

Describes one gated build (release-management.md §1): per-app migration watermarks (max migration file at the sha — the artifact, not a DB), reversible_floor per app, contracts (stapel-* version pins), config_digest over the STAPEL_<MOD> settings blocks, and gate results (migration_lint computed via the shared analyzer; prodguard/ handover_scan recorded from --gate name=pass|fail). Output is byte-deterministic (sorted keys; --created-at/SOURCE_DATE_EPOCH). The platform bake step calls this during image build and bakes the file into the image at /app/release.json.

Project layout

Generated projects follow the mainstream Django community canon so the shape is familiar to anyone who has used the popular templates.

  • config/ — the settings/URLs/WSGI package. Not core/. This is the convention of cookiecutter-django, the HackSoft styleguide, and Two Scoops of Django (ROOT_URLCONF = "config.urls"). It also avoids colliding with the stapel-core package name. The monolith and microservices presets split it into config/settings/{base,dev,local,prod}.py; minimal keeps a single config/settings.py — a deliberate choice for a small, no-Docker project (the falco / Adam Johnson "one settings file until you need more" camp).

  • apps/ — a regular package holding your Django apps. Every module lives at apps/<module> with an apps/__init__.py, is listed as INSTALLED_APPS = ["apps.<module>"], and sets AppConfig.name = "apps.<module>" (the full dotted path — see Django ticket #24801). This is the wemake-django-template pattern, and it is uniform: the starter module and everything added later by stapel-new-module share the same import path.

myapp/                      # monolith / microservices service (svc-myapp/)
├── config/
│   ├── settings/{base,dev,local,prod}.py
│   ├── urls.py  wsgi.py  asgi.py
├── apps/
│   ├── __init__.py         # regular package (required)
│   └── myapp/              # apps.myapp — INSTALLED_APPS + AppConfig.name
├── tests/                  # outbox/mailtrap integration harness
└── manage.py

myapp/                      # minimal preset (no Docker, SQLite)
├── config/
│   ├── settings.py         # single file (deliberate)
│   ├── urls.py  wsgi.py
├── apps/__init__.py  apps/myapp/
└── manage.py

Available modules

Module Description
core Core framework (always included)
auth Authentication — JWT, OAuth, OTP
billing Billing & subscriptions
cdn File uploads & CDN
notifications Email / push notifications
profiles User profiles
translate Translations & i18n
workspaces Workspaces & multi-tenancy
gdpr GDPR — data export & deletion

Download files

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

Source Distribution

stapel_tools-0.32.0.tar.gz (516.0 kB view details)

Uploaded Source

Built Distribution

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

stapel_tools-0.32.0-py3-none-any.whl (390.9 kB view details)

Uploaded Python 3

File details

Details for the file stapel_tools-0.32.0.tar.gz.

File metadata

  • Download URL: stapel_tools-0.32.0.tar.gz
  • Upload date:
  • Size: 516.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for stapel_tools-0.32.0.tar.gz
Algorithm Hash digest
SHA256 0a8bcecefb48cb937bd4be6c40f2c2e41cc54f2f42fa98f6def7f680cddfc902
MD5 1f504849802a07f31fd1280737016ba2
BLAKE2b-256 31a2413ce954651df588c443416d13e75244c40af80e34b4124e04b77269e5d0

See more details on using hashes here.

Provenance

The following attestation bundles were made for stapel_tools-0.32.0.tar.gz:

Publisher: publish.yml on usestapel/stapel-tools

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file stapel_tools-0.32.0-py3-none-any.whl.

File metadata

  • Download URL: stapel_tools-0.32.0-py3-none-any.whl
  • Upload date:
  • Size: 390.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for stapel_tools-0.32.0-py3-none-any.whl
Algorithm Hash digest
SHA256 fe353ec4346a6ab92a92e50849a4b8e8989d27f211b6df5c120058388530cd57
MD5 330eac228c4ee91828194371253b9a36
BLAKE2b-256 f5f8ac12debdfefb15453eef3f9b5fc1ba01715ac86d171e1942d065544524ae

See more details on using hashes here.

Provenance

The following attestation bundles were made for stapel_tools-0.32.0-py3-none-any.whl:

Publisher: publish.yml on usestapel/stapel-tools

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

0.42.0

2 files

0.41.2

2 files

0.40.0

2 files

0.39.1

2 files

0.39.0

2 files

0.38.0

2 files

0.37.0

2 files

0.36.0

2 files

0.35.0

2 files

0.34.0

2 files

This release

0.32.0 This release

2 files

0.31.0

2 files

0.29.1

2 files

0.29.0

2 files

0.28.1

2 files

0.28.0

2 files

0.27.0

2 files

0.26.0

2 files

0.25.0

2 files

0.24.0

2 files

0.23.0

2 files

0.22.1

2 files

0.22.0

2 files

0.21.0

2 files

0.20.0

2 files

0.12.0

2 files

0.11.5

2 files

0.11.4

2 files

0.10.4

2 files

0.10.3

2 files

0.10.2

2 files

0.9.3

2 files

0.3.1

2 files

Supported by

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