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 "Дополнение владельца"): 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-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.26.0.tar.gz (423.5 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.26.0-py3-none-any.whl (324.1 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: stapel_tools-0.26.0.tar.gz
  • Upload date:
  • Size: 423.5 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.26.0.tar.gz
Algorithm Hash digest
SHA256 9a90af7ed2fa12385e79beae28b59320c2c9524124e9a44f32e8605e324271a9
MD5 2d5be5de5dbed0883afdd1eb2dce49b0
BLAKE2b-256 ab9f27f240a9103fa3d1a5ca4b410c08365c11eabd42363b00eae6a9197f1be6

See more details on using hashes here.

Provenance

The following attestation bundles were made for stapel_tools-0.26.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.26.0-py3-none-any.whl.

File metadata

  • Download URL: stapel_tools-0.26.0-py3-none-any.whl
  • Upload date:
  • Size: 324.1 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.26.0-py3-none-any.whl
Algorithm Hash digest
SHA256 b2fbd2efeb2fb35dbd1b01bea8f62d29230a7814d45e5a51e34e6f415fb18513
MD5 7380326caa68c20a91604f8f5de0e5fa
BLAKE2b-256 cbadc6a0e408404db7454364b7a2accf5777aa39bcb1db08cfb637d1c8d15ac8

See more details on using hashes here.

Provenance

The following attestation bundles were made for stapel_tools-0.26.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

0.32.0

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

This release

0.26.0 This release

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