Installation · Getting Started · PyPI · Changelog
openframe-tooling is the developer tooling family of the OpenFrame Microservice
Development Suite — a monorepo of three independent tools that keep services in
the ecosystem honest: schema-compatible across service boundaries, scaffolded
consistently, and documented truthfully. Each tool is versioned and published
independently under its own PyPI name; install only what you need, or install
openframe-tooling[all] for the full set.
As of this release, all three tools are published at 0.1.0:
| Tool | What it does |
|---|---|
openframe-schemas |
Publishes @contract-decorated Pydantic models to a versioned registry and detects breaking changes between versions — a CI gate against silent cross-service drift. |
openframe-cli |
Scaffolds new OpenFrame services from Copier templates — hexagonal architecture, ApplicationBootstrap wiring, and boundary tests generated correct from day one. |
openframe-docs |
Doc-truth CI — derives documentation claims from source (version pins, code-block signatures, import paths) instead of trusting prose that drifts. |
All three build on the @contract marker and port contracts shipped in
openframe-core, and
openframe-cli scaffolds services that wire
openframe-adapters.
0.1.0 is an experimental first release — APIs may still change ahead of 1.0.
pip install openframe-tooling[schemas] # cross-service schema governance
pip install openframe-tooling[cli] # service scaffolding
pip install openframe-tooling[docs] # doc-truth CIpip install openframe-tooling[all] # all three toolsEach tool is a standalone PyPI package; the meta-package is a convenience, not a requirement.
pip install openframe-schemas # core mechanic
pip install openframe-schemas[cli] # + the openframe-schemas CLI
pip install openframe-cli # scaffolding CLI (copier + typer + rich)
pip install openframe-docs # doc-truth checkers (typer + rich only —
# infrastructure-free, no openframe-core dep)| Package | Install | Status | Description |
|---|---|---|---|
[schemas] |
openframe-schemas |
Published — 0.1.0 |
Publishes @contract-decorated Pydantic models to a versioned registry and detects breaking changes between versions |
[cli] |
openframe-cli |
Published — 0.1.0 |
Scaffolds new OpenFrame services from Copier templates — hexagonal architecture, ApplicationBootstrap wiring, and tests/test_architecture.py enforced from generation |
[docs] |
openframe-docs |
Published — 0.1.0 |
Doc-truth CI — derives documentation claims from source: version pins, code-block call signatures, and import paths, each independently CI-gateable |
openframe-tooling/
├── meta/openframe-tooling/ # metadata-only install-surface package ([schemas]/[cli]/[docs]/[all])
├── packages/
│ ├── openframe-schemas/ # export → registry → diff, plus the `openframe-schemas` CLI
│ ├── openframe-cli/ # `openframe new service` / `openframe check` + Copier templates
│ └── openframe-docs/ # check-pins / check-sigs / check-imports + the `openframe-docs` CLI
└── scripts/ # shared CI utilities (read_pkg_meta.py)
Each package under packages/ is independently versioned, tested (app-test.yml
across Python 3.11–3.14), and published to PyPI (python-build.yml) on push to
production.
# Whether you ran pip install openframe-tooling[schemas]
# or pip install openframe-schemas directly, the import is always the same:
from openframe.schemas import export_schema, ContractRegistry, diff_schemasThe @contract decorator lives in openframe-core (openframe.core.schemas)
— it's stdlib-only and adds no dependency to your domain layer. It attaches
metadata and returns the class unchanged; openframe-schemas is the
machinery that reads that metadata.
from datetime import datetime
from openframe.core.schemas import contract
from pydantic import BaseModel
@contract(name="item", version="1.0")
class Item(BaseModel):
id: str
name: str
description: str | None = None
status: str = "active"
created_at: datetime | None = NoneExport and publish:
from openframe.schemas import export_schema, ContractRegistry
schema = export_schema(Item)
# {"properties": {...}, "required": [...], "x-contract-name": "item", "x-contract-version": "1.0"}
registry = ContractRegistry("./contracts") # a directory of JSON files, committed to your repo
registry.publish(schema) # writes ./contracts/item/1.0.jsonDiff — catch breaking changes before a consumer does:
from openframe.schemas import diff_schemas
old = registry.get("item", "1.0")
new = export_schema(ItemV1_1) # e.g. "name" renamed to "title"
result = diff_schemas(old, new)
if result.is_breaking:
for change in result.breaking:
print(f" - {change.field}: {change.reason}")
# - name: field removedCLI — wire it in as a CI gate:
pip install openframe-schemas[cli]
openframe-schemas publish --model myservice.domain.item:Item --registry ./contracts
# Published item v1.0
openframe-schemas check --model myservice.domain.item:Item --registry ./contracts
# PASS — item v1.1 is backward-compatible with v1.0
# + tags: optional field added# .github/workflows/contract-check.yml
- name: Check contract compatibility
run: openframe-schemas check --model src.domain.item:Item --registry ./contracts
# exits 1 on a breaking change — fails the jobpip install openframe-tooling[cli]
openframe new service items-postgres --adapter openframe-adapters-db-postgres
# ✓ Generated items-postgres/
# ├── src/domain/items_postgres.py (@contract decorated)
# ├── src/bootstrap/app.py (ApplicationBootstrap)
# ├── tests/test_architecture.py (boundary enforcement from day 1)
# └── ... (11 more files)
cd items-postgres && pip install -e ".[dev]"
pytest tests/test_domain.py tests/test_service.py tests/test_architecture.py
# passes immediately — no human editing required--stage 2 scaffolds a multi-adapter service (persistence + cache + queue),
mirroring research-pipeline's three-adapter ApplicationBootstrap wiring:
openframe new service research-pipeline --stage 2 --entity Artifact \
-a openframe-adapters-db-mongo:persistence \
-a openframe-adapters-db-redis:cache \
-a openframe-adapters-queue-kafka:queueopenframe check runs in CI against any generated service — architecture
boundary tests always, plus openframe-schemas/openframe-docs checks if
those packages happen to be installed:
openframe check ./items-postgresThree independent checkers, each usable standalone — Checker 1 can be added to CI before Checkers 2 and 3 exist:
pip install openframe-tooling[docs]
# Checker 1 — README version-pin claims vs. the real pyproject.toml version
openframe-docs check-pins --package openframe-core \
--source ./openframe-core --check ./openframe-adapters
# Checker 2 — README code-block calls vs. real installed signatures
openframe-docs check-sigs --readme ./openframe-adapters/README.md \
--package openframe-adapters-queue-kafka
# Checker 3 — README import statements vs. what actually resolves
openframe-docs check-imports --readme ./openframe-core/README.md \
--package openframe-coreEach exits non-zero on drift and prints a per-finding path:line report, so any one can be wired into CI as its own gate:
# .github/workflows/doc-truth.yml
- name: Check README pins are not stale
run: openframe-docs check-pins --package openframe-core --source . --check .openframe-schemas handles three things and nothing else:
- Export — turn a
@contract-decorated Pydantic model into a tagged JSON Schema dict - Registry — a directory of versioned JSON files, committed alongside the domain models it governs
- Diff — classify field-level changes between two versions as breaking or safe
openframe-cli handles one thing: parameterising the templates that already
exist in the validation framework and putting them behind a single command,
so "doing it right" costs the same as "doing it at all." No new architectural
patterns are invented — the generated service is the proven service.
openframe-docs handles three independent checks and nothing else — no
ecosystem table generation, no docstring-driven reference generation, no
mkdocs integration (all Phase 2). Each checker reads real source (a
pyproject.toml version, an installed signature, an importable path) and
compares it to a documentation claim; it never trusts the claim on its own.
Everything else across all three tools — OpenAPI/AsyncAPI generation, typed
client stubs, a hosted schema registry, openframe new adapter, interactive
TUI mode — is deliberately out of scope for 0.1.0.
# openframe-schemas handles this:
schema = export_schema(Item) # model → tagged JSON Schema
registry.publish(schema) # one JSON file, committed to your repo
result = diff_schemas(old, new) # breaking vs. safe, field by field
# You handle this directly:
# migrating existing duplicated domain models (e.g. Item defined separately
# in two services) onto one shared @contract-decorated model is a service-
# level decision — openframe-schemas only proves whether they'd diverge.| Change | Classification |
|---|---|
| Field present in old, absent in new | Breaking — field removed |
| Field's type changed | Breaking — type changed |
| Field optional in old, required in new | Breaking — became required |
| New field, required | Breaking — new field added as required |
| New field, optional | Safe — optional field added |
| Field required in old, optional in new | Safe — became optional |
Nested model diffing is out of scope for Phase 1 — only top-level
properties are compared. x-contract-name / x-contract-version are
metadata, not schema fields, and are always ignored when diffing.
| Package | Install | Description |
|---|---|---|
openframe-core |
pip install openframe-core |
Ports · tracing · telemetry · exceptions · config · the @contract marker |
openframe-adapters |
pip install openframe-adapters[all] |
Database and queue adapters — Postgres, Mongo, Redis, Kafka, and more |
openframe-tooling |
pip install openframe-tooling[all] |
Developer tooling — schema governance, service scaffolding, doc-truth CI |
MIT — see LICENSE.
