Python (sufleur-cli)

The pip wrapper around the Sufleur CLI and the Python code it generates.

The sufleur-cli package on PyPI wraps the Sufleur CLI for Python projects. It bundles the platform-appropriate Go binary inside the wheel and provides the typed runtime the generated code depends on.

The PyPI listing: pypi.org/project/sufleur-cli.

Install

uv add --dev sufleur-cli
# or: pip install sufleur-cli
# or: poetry add --group dev sufleur-cli

The wheel is built by GoReleaser per platform — installing on macOS arm64 pulls a wheel containing the macOS arm64 binary, on Linux x86_64 you get the Linux x86_64 binary, and so on. There's no separate download step at install time.

Peer dependencies

The generated code imports two libraries; install them in your project:

uv add chevron pydantic
# or: pip install chevron pydantic

chevron is the runtime Mustache implementation (chosen for full-spec compliance). pydantic powers the output-schema models. Both are intentionally peer deps.

Running the CLI

The package installs a sufleur console script:

sufleur init
sufleur add @acme/welcome-message
sufleur generate

You can also invoke it as a module — useful in venvs where the script shim isn't on PATH:

python -m sufleur_cli generate

See the CLI reference for every command and flag.

What the generated code looks like

sufleur generate writes a single file (default: ./generated/prompts.py) with TypedDicts for inputs, Pydantic BaseModels for outputs, and a typed get_prompt factory. Excerpt:

generated/prompts.py
from __future__ import annotations
from typing import Any, Literal, TypedDict
import chevron
import json
from pydantic import BaseModel
 
 
class _WelcomeMessage_UserPromptInput_User(TypedDict):
    age: int
    """User's age in years"""
    name: str
    """User's name"""
 
 
class WelcomeMessage_UserPromptInput(TypedDict):
    isWeatherGood: bool
    user: _WelcomeMessage_UserPromptInput_User
 
 
class WelcomeMessageOutput(BaseModel):
    email: str
    id: int
    name: str
 
 
PromptName = Literal["@acme/welcome-message"]
 
 
def get_prompt(prompt_name: PromptName): ...

A few things to note:

  1. Nested objects become their own TypedDict (with the _ prefix marking them as internal). @doc annotations land as field docstrings.
  2. Output schemas become Pydantic BaseModels — fully validated, with .model_dump() and friends available out of the box.
  3. The Literal type for PromptName means your editor autocompletes prompt names.

Calling a prompt

from generated.prompts import get_prompt
 
welcome = get_prompt('@acme/welcome-message')
 
result = welcome.render('userPrompt', {
    'isWeatherGood': True,
    'user': {'name': 'Ada', 'age': 32},
})
 
# `result['prompt']` is the rendered string.
print(result['prompt'])
 
# `welcome.metadata` exposes model, temperature, version, and (if set) the raw output_schema.
print(welcome.metadata['version'])  # "1.2.3"

If a prompt declares an outputSchema, the returned object exposes parse_output:

result = welcome.parse_output(llm_response_text)
if result['success']:
    user = result['data']        # typed as WelcomeMessageOutput
    print(user.email)
else:
    print(result['error'])

parse_output strips a fenced ```code block if the model wrapped its JSON, then validates against the Pydantic model. The return is always a TypedDict union — no exception path to catch.

Draft warnings

Pinning to a draft version is supported but produces a runtime warning the first time you call the prompt:

UserWarning: [sufleur] Prompt "@acme/welcome-message" is a draft version

Drafts are mutable in the registry, so they're appropriate for local iteration — not for production.

Versioning the generated file

Commit generated/prompts.py to your repo alongside sufleur.yaml and sufleur-lock.yaml. The trio is what makes prompt changes reviewable in PRs — reviewers see the new template, the version bump, and the calling code in the same diff.